// =====================================================================
// "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 (
{ME.name}
CEO · Nuance Labs
{items.map(it => (
setFilter(it.id)}
>
{it.label}
{it.count}
{it.hint}
))}
Saved views
Stalled with me > 7d 3
Awaiting my score 2
Offer pending response 1
);
}
// "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 (
{c.name}
{c.duplicateIds && c.duplicateIds.length > 0 && (
1 ? "s" : ""} found`}>
)}
{c.starred &&
★ }
{m.label}{detail ? ` · ${detail}` : ""}
{c.title.replace("Senior ","Sr ").replace("Research Scientist","Research Sci")}
·
{c.company}
{c.duplicateIds && c.duplicateIds.length > 0 && Duplicate }
{c.days}d
);
}
// 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 (
Candidate
Why I'm here
Stage
Role
Days
Last activity
{rows.map(r => )}
);
}
// Bucket header inside All-table view
function BucketHeader({ icon, title, count, hint }) {
return (
);
}
// 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 => (
onSelect(v.id)}
title={v.desc}
>
{v.icon}
{v.label}
))}
onAction && onAction("Filter panel — coming soon")}>Filter
onAction && onAction("Sort panel — coming soon")}>Sort
onAction && onAction("Group panel — coming soon")}>Group
onAction && onAction("More options — coming soon")}>⋯
);
}
// --- 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.
showToast("Refer someone — form coming soon", { tone: "success" })}>+ Refer someone
{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 });