// Pipeline Kanban — Variant A with drag-to-move support const { useState: useStateA, useMemo: useMemoA, useRef: useRefA } = React; function DraggableCandidateCard({ c, dense = true, onDragStart, onDragEnd, isDragging, onOpen, onRemind }) { const isStale = c.days >= 12 && !["hired","rejected","not_interested","declined","backburner"].includes(c.stage); return (
{ // Only open if user didn't just finish dragging if (!isDragging) onOpen && onOpen(c.id); }} onDragStart={(e) => { e.dataTransfer.setData("text/candidate-id", c.id); e.dataTransfer.effectAllowed = "move"; onDragStart && onDragStart(c.id); }} onDragEnd={() => onDragEnd && onDragEnd()} className={ "ats-card" + (dense ? " ats-card--xdense" : "") + (c.flagged ? " is-flagged" : "") + (c.starred ? " is-starred" : "") + (isDragging ? " is-dragging" : "") } >
{c.name} {c.duplicateIds && c.duplicateIds.length > 0 && ( 1 ? "s" : ""} found`}> )} {c.starred && } ⋮⋮
{c.title.replace("Senior ","Sr ").replace("Research Scientist","Research Sci")} · {c.company}
{c.duplicateIds && c.duplicateIds.length > 0 && Duplicate} {c.days}d
); } // alias for back-compat const CandidateCard = DraggableCandidateCard; function KanbanColumn({ stage, candidates, onDragStart, onDragEnd, onDrop, draggingId, hoverStageId, onDragOver, onOpen, onRemind, onAdd, compact }) { const isTerminal = ["rejected","declined","hired","backburner"].includes(stage.id); const isHover = hoverStageId === stage.id && draggingId; return (
{ e.preventDefault(); e.dataTransfer.dropEffect = "move"; onDragOver && onDragOver(stage.id); }} onDrop={(e) => { e.preventDefault(); const id = e.dataTransfer.getData("text/candidate-id"); if (id) onDrop && onDrop(id, stage.id); }} >
{!compact && } {stage.label} {candidates.length}
{!compact && ( )}
{candidates.map(c => ( ))} {candidates.length === 0 && (
{isHover ? "Drop to move here" : "Nothing here yet"}
)} {isHover && candidates.length > 0 &&
}
{!isTerminal && !compact && ( )}
); } function PipelineToolbar() { return (
Stalled > 7d ×
); } function PipelineMetrics({ candidates, scopeLabel }) { // All metrics are derived from the candidates passed in, so they update // when the user filters by role / saved view in the sidebar. const active = candidates.filter(c => !["rejected","not_interested","declined","hired"].includes(c.stage)); const inLoop = candidates.filter(c => ["intro","tech1","tech2","tech3","onsite","offer"].includes(c.stage)); const offers = candidates.filter(c => c.stage === "offer"); const sourcedThisWeek = candidates.filter(c => c.stage === "sourced" && c.days <= 7); const agencySourcesThisWeek = sourcedThisWeek.filter(c => ["jon","nick","techire","agency"].includes(c.source)).length; const roleCount = new Set(candidates.map(c => c.jobId)).size; // Scope-aware deltas — feel like real movement on the active scope, but // never imply more activity than there is data for. const weeklyDelta = Math.max(1, Math.round(active.length * 0.18)); const onsiteCohort = candidates.filter(c => c.stage === "onsite"); // Average days-in-stage for onsite candidates as a proxy for "time-to-onsite" const avgOnsite = onsiteCohort.length ? Math.round(onsiteCohort.reduce((s, c) => s + c.days, 0) / onsiteCohort.length) + 6 : 14; // For the interview-loop subtitle, count distinct roles those candidates are on. const loopRoles = new Set(inLoop.map(c => c.jobId)).size; return (
Active candidates
{active.length}
+{weeklyDelta} this week
In interview loop
{inLoop.length}
{loopRoles === 0 ? "—" : `across ${loopRoles} role${loopRoles === 1 ? "" : "s"}`}
Offers extended
{offers.length}
{offers.length === 0 ? "none open" : `${offers.length} awaiting response`}
Avg. time-to-onsite
{avgOnsite}d
+2d vs last cycle
Sourced this week
{sourcedThisWeek.length}
{sourcedThisWeek.length === 0 ? "no new sources" : `${agencySourcesThisWeek} via agency`}
); } function PipelineKanban() { // Make candidates stateful so we can move them between columns const [candidates, setCandidates] = useStateA(() => CANDIDATES.map(c => ({ ...c }))); React.useEffect(() => { const sync = () => setCandidates(prev => { return CANDIDATES.map(g => { const local = prev.find(p => p.id === g.id); return local ? { ...local, ...g } : { ...g }; }); }); window.addEventListener("candidates-updated", sync); return () => window.removeEventListener("candidates-updated", sync); }, []); const [draggingId, setDraggingId] = useStateA(null); const [hoverStageId, setHoverStageId] = useStateA(null); const [toast, setToast] = useStateA(null); const [openCandidateId, setOpenCandidateId] = useStateA(null); const [addModalOpen, setAddModalOpen] = useStateA(false); const [, showToast, ToastHost] = useAtsToast(); const [activeJob, setActiveJob] = useStateA(null); const handleSidebarSelect = (id) => { setActiveJob(id); const job = JOBS.find(j => j.id === id); if (job) showToast(`${job.title} — view filtered`); else if (id === "__all__") showToast("All roles"); else if (id === null) showToast("All open roles"); else showToast("Saved view selected"); }; const openCandidate = (id) => { setOpenCandidateId(id); window.location.hash = "candidate/" + id; }; const closeModal = () => { setOpenCandidateId(null); history.replaceState(null, "", window.location.pathname + window.location.search); }; const handleDrop = (id, stageId) => { const moved = candidates.find(c => c.id === id); setCandidates(prev => prev.map(c => { if (c.id !== id) return c; if (c.stage === stageId) return c; return { ...c, stage: stageId, days: 0, lastActivity: `Moved to ${STAGE_BY_ID[stageId].label} just now` }; })); if (moved && moved.stage !== stageId) { setToast({ name: moved.name, from: STAGE_BY_ID[moved.stage].label, to: STAGE_BY_ID[stageId].label, }); setTimeout(() => setToast(null), 3500); updateCandidateStage(id, stageId); } setDraggingId(null); setHoverStageId(null); }; return (

Pipeline — all open roles

{candidates.length} candidates across {JOBS.length} roles · Last synced from LinkedIn 4 minutes ago · Drag any card between columns to advance.
{STAGES.map(s => ( c.stage === s.id)} onDragStart={setDraggingId} onDragEnd={() => { setDraggingId(null); setHoverStageId(null); }} onDrop={handleDrop} onDragOver={setHoverStageId} draggingId={draggingId} hoverStageId={hoverStageId} onOpen={openCandidate} onAdd={() => setAddModalOpen(true)} /> ))}
{toast && (
Moved {toast.name} from {toast.from}{toast.to}
)} {ToastHost}
{openCandidateId && ( x.id === openCandidateId)} onClose={closeModal} onDelete={() => { setCandidates(prev => prev.filter(x => x.id !== openCandidateId)); closeModal(); }} /> )} {addModalOpen && ( setAddModalOpen(false)} /> )}
); } Object.assign(window, { CandidateCard, DraggableCandidateCard, KanbanColumn, PipelineKanban });