// ===================================================================== // "My candidates" screen — every candidate the current user is involved in. // Three buckets: Owner · Referred by me · On the interview panel. // Notion-style view tabs (Board / Table), filtered by bucket via a left rail. // ===================================================================== const { useState: useStateMC, useMemo: useMemoMC } = React; // Current user (demo). Matches the avatar in the topnav. const ME = { id: "fm", name: "Fangchang Ma", initials: "FM", color: "#7595D5", refLabel: "Fangchang M." }; // Deterministic hash so "on panel" is stable across renders. function hashStr(s) { let h = 0; for (let i = 0; i < s.length; i++) h = ((h << 5) - h + s.charCodeAt(i)) | 0; return Math.abs(h); } // Build my candidate rows (with `why` and `detail` annotations) from the global CANDIDATES list. function useMyRows(viewer) { return useMemoMC(() => { const visible = visibleCandidates(viewer, CANDIDATES); const rows = []; for (const c of visible) { // Owner = the viewer is the recruiter for this candidate if (c.recruiter === viewer.id || c.submittedById === viewer.id) { rows.push({ c, why: "owner", detail: null, key: "o-" + c.id }); } // Referred-by-me: only shows for ME's reference label if (c.referredBy === ME.refLabel && viewer.id === ME.id) { rows.push({ c, why: "referred", detail: null, key: "r-" + c.id }); } // On panel = viewer is in the candidate's panelMembers list if ((c.panelMembers || []).includes(viewer.id) && c.recruiter !== viewer.id) { const myRound = panelRoundFor(c); rows.push({ c, why: "panel", detail: myRound.label, round: myRound, key: "p-" + c.id }); } } const stageOrder = STAGES.map(s => s.id); rows.sort((a, b) => { const sa = stageOrder.indexOf(a.c.stage), sb = stageOrder.indexOf(b.c.stage); if (sa !== sb) return sa - sb; return b.c.days - a.c.days; }); return rows; }, [viewer]); } function panelRoundFor(c) { switch (c.stage) { case "intro": return { label: "Intro · Mon", round: "Intro" }; case "tech1": return { label: "Tech 1 · Wed", round: "Tech 1" }; case "tech2": return { label: "Tech 2 · scored", round: "Tech 2", scored: true }; case "tech3": return { label: "Tech 3 · Thu", round: "Tech 3" }; case "onsite": return { label: "Onsite · System design", round: "Onsite" }; case "offer": return { label: "Onsite · scored", round: "Onsite", scored: true }; case "hired": return { label: "Onsite · scored", round: "Onsite", scored: true }; default: return { label: "Up next", round: "Panel" }; } } // Map why → color tokens used for stripes/dots/pills const WHY_META = { owner: { label: "Owner", color: "#6B8E76", bg: "rgba(150,177,173,0.20)", border: "rgba(107,142,118,0.35)", fg: "#3F5E45" }, referred: { label: "Referred", color: "#C9923D", bg: "rgba(227,165,78,0.18)", border: "rgba(227,165,78,0.40)", fg: "#8A5418" }, panel: { label: "On panel", color: "#7595D5", bg: "rgba(117,149,213,0.18)", border: "rgba(117,149,213,0.40)", fg: "#3A588A" }, }; // --- Filter rail --- function MyRail({ filter, setFilter, counts }) { const items = [ { id: "all", label: "All my candidates", count: counts.all, hint: "Anyone I touch" }, { id: "owner", label: "Owner", count: counts.owner, hint: "I run the process" }, { id: "referred", label: "Referred by me", count: counts.referred, hint: "I sent them in" }, { id: "panel", label: "On the panel", count: counts.panel, hint: "I run an interview" }, ]; return ( ); } // "Why am I here?" pill (used in table) function WhyPill({ kind, detail }) { const m = WHY_META[kind]; return ( {m.label} {detail && · {detail}} ); } // Small dot used on kanban cards to indicate the "why" function WhyDot({ kind, detail }) { const m = WHY_META[kind]; return ( {m.label} {detail && · {detail}} ); } // Kanban card variant — uses the SAME .ats-card markup as the main pipeline, // with a small "why" stripe along the left edge. function MyKanbanCard({ row }) { const { c, why, detail } = row; const m = WHY_META[why]; const isStale = c.days >= 12 && !["hired","rejected","not_interested","declined","backburner"].includes(c.stage); return (
); } // Kanban column — uses the SAME .ats-col classes as the main pipeline, // minus drag/drop (this is a read-only "my view"). function MyKanbanColumn({ stage, rows }) { const isTerminal = ["rejected","declined","hired","backburner"].includes(stage.id); return (
{stage.label} {rows.length}
{rows.length === 0 ? (
Nothing here yet
) : rows.map(r => )}
); } // Board: kanban grouped by stage function MyBoardView({ rows }) { return (
{STAGES.map(s => ( r.c.stage === s.id)} /> ))}
); } // Table row — uses the SAME .ats-sheet markup as the main pipeline table. function MyCandidateRow({ row }) { const { c, why, detail } = row; const stage = STAGE_BY_ID[c.stage]; const isStale = c.days >= 12 && !["hired","rejected","not_interested","declined","backburner"].includes(c.stage); const job = JOBS.find(j => j.id === c.jobId); return (
{c.name} {c.starred && } {c.title} · {c.company}
{job ? job.title.replace("Real-time Systems", "RT Systems") : "—"} {c.days}d {c.lastActivity} ); } function MyCandidatesTable({ rows }) { return (
{rows.map(r => )}
Candidate Why I'm here Stage Role Days Last activity
); } // Bucket header inside All-table view function BucketHeader({ icon, title, count, hint }) { return (
{icon}
{title}
{hint}
{count}
); } // Grouped table view (used when filter === "all" + view === "table"): // shows three sections, each preceded by a bucket header. function MyGroupedTableView({ rows, counts }) { const owner = rows.filter(r => r.why === "owner"); const referred = rows.filter(r => r.why === "referred"); const panel = rows.filter(r => r.why === "panel"); return ( <> {owner.length > 0 && ( <> } title="I own these" count={counts.owner} hint="My pipeline · I drive the process" /> )} {referred.length > 0 && ( <> } title="Referrals I sent in" count={counts.referred} hint="People I vouched for · check in on momentum" /> )} {panel.length > 0 && ( <> } title="On the panel" count={counts.panel} hint="I'm running an interview round · score before they advance" /> )} ); } // Top "My week" KPI strip function MyWeekStrip({ counts }) { const items = [ { label: "Awaiting my score", value: 2, hint: "Tech-2 panels · score by Fri" }, { label: "Need my decision", value: 1, hint: "Edward Z. · offer ask" }, { label: "Interviews this week",value: 4, hint: "Mon · Tue · Thu · Fri" }, { label: "Referrals in flight", value: counts.referred, hint: "across active stages" }, ]; return (
{items.map((i, idx) => (
{i.value}
{i.label}
{i.hint}
))}
); } // Notion-style view tabs (Board / Table) const BoardIcon = () => ( ); const TableIcon = () => ( ); function MyViewTabs({ activeId, onSelect, onAction }) { const VIEWS = [ { id: "board", label: "Board view", icon: , desc: "Kanban grouped by stage" }, { id: "table", label: "Table view", icon: , desc: "Grouped by bucket" }, ]; return (
{VIEWS.map(v => ( ))}
); } // --- Main screen --- function MyCandidatesScreen() { const viewer = useViewer(); const [filter, setFilter] = useStateMC("all"); const [view, setView] = useStateMC("board"); const [, showToast, ToastHost] = useAtsToast(); const allRows = useMyRows(viewer); const counts = { all: allRows.length, owner: allRows.filter(r => r.why === "owner").length, referred: allRows.filter(r => r.why === "referred").length, panel: allRows.filter(r => r.why === "panel").length, }; const visibleRows = filter === "all" ? allRows : allRows.filter(r => r.why === filter); // Listen for page changes to update active tab const [currentPage, setCurrentPage] = React.useState( typeof window.__atsCurrentPage === 'string' ? window.__atsCurrentPage : 'my-candidates' ); 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 (

My candidates

Everyone you've put your name on — as the owner, the referrer, or someone running an interview. Triage what's blocked on you.

{view === "board" && } {view === "table" && ( filter === "all" ? : )} {visibleRows.length === 0 && (
Nothing here yet. You're not currently {filter === "owner" ? "owning" : filter === "referred" ? "referring" : "on a panel for"} any candidate.
)} {ToastHost}
); } Object.assign(window, { MyCandidatesScreen });