// =====================================================================
// People & Permissions — Settings screen.
// Shows everyone with access to the ATS plus the role-capability matrix.
// =====================================================================
const { useState: useStateP } = React;
function _formatLastActive(iso) {
if (!iso) return "";
const d = new Date(iso);
const now = new Date();
const diffMs = now - d;
const diffMin = Math.floor(diffMs / 60000);
if (diffMin < 1) return "Just now";
if (diffMin < 60) return diffMin + "m ago";
const diffHr = Math.floor(diffMin / 60);
if (diffHr < 24) return diffHr + "h ago";
const diffDay = Math.floor(diffHr / 24);
if (diffDay < 30) return diffDay + "d ago";
return d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
}
const ADMIN_NAMES = ["fangchang", "edward", "karren", "nicole"];
function _isAdmin(t) {
if (t.raw_role === "admin" || t.role === "Admin") return true;
const first = (t.name || "").split(" ")[0].toLowerCase();
return ADMIN_NAMES.includes(first);
}
function _buildPeople() {
const internal = TEAM.map(t => ({
id: t.id, name: t.name, email: t.email || "", initials: t.initials, color: t.color,
role: _isAdmin(t) ? "admin" : "interviewer",
title: t.role, last_login: t.last_login || null,
scopeRoles: "all", comp: true,
}));
const external = [{
id: "ext-jon-luzha", name: "Jon Luzha", email: "", initials: "JL",
color: "#9C6B4F", role: "external", title: "Alexander Chapman",
last_login: null, scopeRoles: "ownSubs", agencyName: "Alexander Chapman",
comp: false,
}];
return [...internal, ...external];
}
const CAPABILITIES = [
{ id: "see_all", name: "See all candidates", hint: "Across every role in the workspace",
perms: { admin: "yes", interviewer: "yes", external: "no" },
cellHint: { external: "Own submissions only" } },
{ id: "read_write", name: "Read & write candidate data", hint: "Emails, Slack, notes, scores, profiles, stage changes",
perms: { admin: "yes", interviewer: "yes", external: "partial" },
cellHint: { external: "Own submissions only" } },
{ id: "hired_access", name: "Read & write Hired candidates", hint: "View and manage candidates in the Hired stage",
perms: { admin: "yes", interviewer: "no", external: "no" } },
{ id: "manage_users", name: "Manage users & permissions", hint: "Invite, change roles, remove access",
perms: { admin: "yes", interviewer: "no", external: "no" } },
];
function PermCell({ value, hint }) {
const ch = value === "yes" ? "✓" : value === "no" ? "—" : "◐";
const cls = "ats-perm-cell ats-perm-cell--" + (value === "yes" ? "yes" : value === "no" ? "no" : "partial");
return (
);
}
function RolePill({ role }) {
const r = ROLES[role];
return (
{r.label}
);
}
// Short-form role title for compact scope chips
function shortRoleTitle(t) {
return t
.replace("Research Scientist, Multimodal", "Research Sci · Multimodal")
.replace("Staff Engineer, Real-time Systems", "Staff Eng · Real-time")
.replace("ML Infrastructure Engineer", "ML Infra")
.replace("Perception & Computer Vision", "Perception & CV")
.replace("Senior Product Designer", "Sr Product Designer")
.replace("Founding Frontend Engineer", "Founding FE");
}
function ScopeCell({ p }) {
const jobs = p.scopeRoles;
return (
{jobs === "all" ? (
All roles · {JOBS.filter(j => j.status === "open" || j.status === "active").length} open
) : jobs === "ownSubs" ? (
{p.agencyName}
their submissions only
) : (
{jobs.map(j => (
{shortRoleTitle(j)}
))}
{p.panelOnly && · panel only }
)}
);
}
function PermissionsScreen() {
const allPeople = _buildPeople();
const [, showToast, ToastHost] = useAtsToast();
const [rowMenu, setRowMenu] = useState(null); // email of row with open menu
// Listen for page changes to update active tab
const [currentPage, setCurrentPage] = React.useState(
typeof window.__atsCurrentPage === 'string' ? window.__atsCurrentPage : 'settings'
);
React.useEffect(() => {
const handlePageChange = (e) => setCurrentPage(e.detail);
window.addEventListener('ats-page-change', handlePageChange);
return () => window.removeEventListener('ats-page-change', handlePageChange);
}, []);
const activeTabMap = {
'pipeline': 'Pipeline',
'my-candidates': 'My candidates',
'settings': 'Permissions'
};
return (
{
const job = JOBS.find(j => j.id === id);
if (job) showToast(`${job.title} — open the Pipeline artboard to view candidates`);
else if (id === "__all__") showToast("All roles selected");
else if (id === null) showToast("Showing open roles");
else showToast("Saved view selected");
}} />
People & permissions
Who has access to your hiring data, what they can see, and what they can do.
External agents only see candidates they personally submitted.
{/* People */}
People with access
{allPeople.length} accounts
showToast("Invite teammate — form coming soon", { tone: "success" })}>Invite teammate
showToast("Add agency recruiter — form coming soon", { tone: "success" })}>+ Add agency recruiter
Person
Role
Scope
Last active
{allPeople.map(p => (
{p.last_login ? _formatLastActive(p.last_login) : ""}
{ e.stopPropagation(); setRowMenu(rowMenu === p.id ? null : p.id); }}>⋯
{rowMenu === p.id && (
<>
setRowMenu(null)} style={{ position: "fixed", inset: 0, zIndex: 40 }} />
{ setRowMenu(null); showToast(`Changed role for ${p.name}`); }}>Change role…
{ setRowMenu(null); showToast(`Scope edited for ${p.name}`); }}>Edit scope…
{ setRowMenu(null); showToast(`Password reset sent to ${p.email}`, { tone: "success" }); }}>Send password reset
{ setRowMenu(null); showToast(`Access removed for ${p.name}`, { tone: "warn" }); }}>Remove access
>
)}
))}
{/* Matrix */}
Permissions matrix
What each role can see and do
Capability
Admin
Interviewer
External Agent
{CAPABILITIES.map(cap => (
{cap.name}
{cap.hint && {cap.hint}
}
{["admin","interviewer","external"].map(r => (
))}
))}
{ToastHost}
);
}
Object.assign(window, { PermissionsScreen });