/* pipeline.jsx — Full Kanban pipeline page (P0 rewrite). * Requires: app.jsx, email-store.jsx, email-composer.jsx */ /* ── Local icon helpers ──────────────────────────────────────────────────── */ const IcMail = (p) => (); const IcPhone = (p) => (); const IcMapPin = (p) => (); const IcFileText = (p) => (); const IcTrash = (p) => (); const IcUser = (p) => (); const IcMoreH = (p) => (); const IcArrowRight = (p) => (); const IcLinkedIn = (p) => (); /* ── Stage definitions ───────────────────────────────────────────────────── */ const KN_STAGES = [ { id: "intro_screening", label: "Intro screening", hint: "30-min recruiter call", accent: "var(--neutral-60)" }, { id: "hiring_manager", label: "Hiring manager", hint: "First leadership interview", accent: "var(--indigo-50)" }, { id: "technical_test", label: "Technical test", hint: "Take-home or paired exercise", accent: "var(--violet-50)" }, { id: "offer", label: "Offer", hint: "Offer extended", accent: "var(--yellow-50)" }, { id: "hired", label: "Hired", hint: "Signed and starting", accent: "var(--green-50)" }, { id: "rejected", label: "Rejected", hint: "Closed out of pipeline", accent: "var(--red-50)" }, ]; const KN_STAGE_MAP = {}; KN_STAGES.forEach(s => { KN_STAGE_MAP[s.id] = s; }); // Drawer-only stage map — includes "applied" so candidates triaged from the // Candidates page can be moved into the pipeline. Not shown as a Kanban column. KN_STAGE_MAP["applied"] = { id: "applied", label: "Applied", hint: "Awaiting screening", accent: "var(--neutral-40)" }; const CANONICAL_TO_STAGE_ID = { "Applied": "applied", "Screening": "intro_screening", "Interview": "hiring_manager", "Take-home": "technical_test", "Offer": "offer", "Hired": "hired", "Rejected": "rejected", }; const STAGE_BG = { intro_screening: "var(--neutral-20)", hiring_manager: "var(--indigo-10)", technical_test: "var(--violet-10)", offer: "var(--yellow-10)", hired: "var(--green-10)", rejected: "var(--red-10)", }; const scoreClass = (s) => s >= 75 ? "high" : s >= 50 ? "mid" : "low"; /* ── Departments list (for new posting modal) ───────────────────────────── */ const DEPARTMENTS_LIST = [ { id: "dept_eng", name: "Engineering" }, { id: "dept_design", name: "Design" }, { id: "dept_product", name: "Product" }, { id: "dept_ops", name: "Operations & Compliance" }, { id: "dept_growth", name: "Growth & Marketing" }, { id: "dept_partner", name: "Partnerships" }, ]; /* ── KnMoveMenu — kebab popover ─────────────────────────────────────────── */ function KnMoveMenu({ opp, onMove, onReject, onClose }) { const { useEffect, useRef } = React; const ref = useRef(null); useEffect(() => { const fn = (e) => { if (ref.current && !ref.current.contains(e.target)) onClose(); }; document.addEventListener("mousedown", fn); return () => document.removeEventListener("mousedown", fn); }, [onClose]); const visibleStages = KN_STAGES.filter(s => s.id !== opp.stageId && s.id !== "rejected"); return (
Move to
{visibleStages.map(s => ( ))}
); } /* ── KnCard ──────────────────────────────────────────────────────────────── */ function KnCard({ opp, isDragging, onDragStart, onDragEnd, onClick, onMove, onReject }) { const { useState, useRef } = React; const [menuOpen, setMenuOpen] = useState(false); const tone = scoreClass(opp.score || 0); const scoreColor = tone === "high" ? "var(--green-70)" : tone === "mid" ? "var(--indigo-50)" : "var(--neutral-60)"; return (
{/* header row */}
{opp.name}
{opp.roleLabel || opp.role || ""}
{opp.score}
{/* time + source */}
{opp.lastTouch || "—"} {opp.loc && ( {opp.loc} )}
{/* reason tags */} {opp.reasons && opp.reasons.length > 0 && (
{opp.reasons.slice(0, 2).map((r, i) => ( {r} ))}
)} {/* kebab menu */} {menuOpen && (
e.stopPropagation()}> setMenuOpen(false)} />
)}
); } /* ── KnColumn ────────────────────────────────────────────────────────────── */ function KnColumn({ stage, opps, dragOver, draggingId, onDragOverCol, onDragLeaveCol, onDropCol, onDragStartCard, onDragEndCard, onCardClick, onMove, onReject }) { const count = opps.length; const isDragTarget = dragOver === stage.id; const stageAccent = stage.accent; return (
{ e.preventDefault(); onDragOverCol(stage.id); }} onDragLeave={() => onDragLeaveCol(stage.id)} onDrop={(e) => { e.preventDefault(); onDropCol(stage.id); }} > {/* column header */}
{stage.label} {count}
{stage.hint}
{/* drop zone */}
{opps.map(opp => ( { e.stopPropagation(); onDragStartCard(e, opp); }} onDragEnd={onDragEndCard} onClick={() => onCardClick(opp)} onMove={onMove} onReject={onReject} /> ))} {count === 0 && (
{isDragTarget ? "Drop here to move" : "No candidates"}
)}
); } /* ── PipelineDrawer ──────────────────────────────────────────────────────── */ /* Rating helpers — shared between the feedback list and the Add form */ const RATINGS = [ { id: "strong_yes", label: "Strong yes", tone: "var(--green-70)", bg: "var(--green-20)" }, { id: "yes", label: "Yes", tone: "var(--green-60)", bg: "var(--green-10)" }, { id: "mixed", label: "Mixed", tone: "var(--yellow-70)", bg: "var(--yellow-10)" }, { id: "no", label: "No", tone: "var(--red-60)", bg: "var(--red-10)" }, { id: "strong_no", label: "Strong no", tone: "var(--red-70)", bg: "var(--red-20)" }, ]; const ratingMeta = (id) => RATINGS.find(r => r.id === id) || RATINGS[2]; function AddFeedbackForm({ onCancel, onSubmit }) { const [rating, setRating] = React.useState("yes"); const [notes, setNotes] = React.useState(""); const [strengthsRaw, setStrengthsRaw] = React.useState(""); const [concernsRaw, setConcernsRaw] = React.useState(""); const [submitting, setSubmitting] = React.useState(false); const handleSubmit = () => { if (!notes.trim() || submitting) return; setSubmitting(true); const strengths = strengthsRaw.split("\n").map(s => s.trim()).filter(Boolean); const concerns = concernsRaw .split("\n").map(s => s.trim()).filter(Boolean); onSubmit({ rating, notes: notes.trim(), strengths, concerns }) .catch(err => { console.error("[ATS] add feedback failed", err); alert("Feedback failed to save — see console."); }) .finally(() => setSubmitting(false)); }; return (
{RATINGS.map(r => ( ))}