/** * Recruiter/Admin Dashboard + HR/Recruiter Reporting Dashboard (combined — * see frontend/README.md for why these two backlog items share one view). * Candidate list/funnel + cost/token usage, both from the real API shapes: * GET /api/candidates and GET /api/observability/summary. * * Rows expand in place to a full recruiter-facing profile (contact info, * skills, projects, certifications, culture fit, red flags, HR notes, * etc.) — whatever fields the candidate record actually has. Real * candidates (src/data/candidates.json, see export_real_candidates.py) * carry the full set; the small FALLBACK_CANDIDATES set in * mockApiClient.js only has the table-summary fields, so the detail panel * only renders a field when it's actually present rather than showing * empty sections for every mock candidate. */ function initialsFor(name) { var parts = (name || "").trim().split(/\s+/).filter(Boolean); if (!parts.length) return "?"; return (parts[0][0] + (parts[1] ? parts[1][0] : "")).toUpperCase(); } var SORTABLE_COLUMNS = [ { key: "Name", label: "Name" }, { key: "JD Match", label: "JD Match" }, { key: "Candidate Type", label: "Type" }, { key: "Resume Strength Score", label: "Score" }, { key: "ranking_tier", label: "Tier" }, ]; function sortValue(candidate, key) { var v = candidate[key]; if (key === "JD Match" && typeof v === "string") return parseFloat(v) || 0; return v; } function Dashboard(props) { var api = props.api; var filterState = React.useState(""); var filter = filterState[0]; var setFilter = filterState[1]; var expandedState = React.useState(null); var expandedId = expandedState[0]; var setExpandedId = expandedState[1]; var searchState = React.useState(""); var search = searchState[0]; var setSearch = searchState[1]; var sortState = React.useState({ key: null, dir: 1 }); var sort = sortState[0]; var setSort = sortState[1]; // Snapshot-cached, per the "reports show their last snapshot, never // reload on revisit" requirement -- keyed by filter, so switching the // tier filter shows that filter's own cached snapshot instantly once // it's been loaded once, rather than always re-fetching. var snapshot = CDPI_REPORT_SNAPSHOT.useReportSnapshot("dashboard_candidates:" + (filter || "all"), function () { return api.listCandidates(filter || undefined); }); var observabilitySnapshot = CDPI_REPORT_SNAPSHOT.useReportSnapshot("dashboard_observability", function () { return api.observabilitySummary(); }); var meta = snapshot.state; var data = { loading: meta.loading && !meta.data, error: meta.error, candidates: (meta.data && meta.data.candidates) || [], observability: observabilitySnapshot.state.data, }; function refreshAll() { snapshot.refresh(); observabilitySnapshot.refresh(); } var tierCounts = { "Recommended for Fast-Track": 0, "To be Considered": 0, "Rejected": 0 }; data.candidates.forEach(function (c) { if (tierCounts[c.ranking_tier] !== undefined) tierCounts[c.ranking_tier] += 1; }); var visibleCandidates = data.candidates.filter(function (c) { return !search.trim() || (c.Name || "").toLowerCase().indexOf(search.trim().toLowerCase()) !== -1; }); if (sort.key) { visibleCandidates = visibleCandidates.slice().sort(function (a, b) { var av = sortValue(a, sort.key), bv = sortValue(b, sort.key); if (av == null) av = ""; if (bv == null) bv = ""; if (av < bv) return -1 * sort.dir; if (av > bv) return 1 * sort.dir; return 0; }); } function toggleSort(key) { setSort(function (s) { if (s.key === key) return { key: key, dir: s.dir * -1 }; return { key: key, dir: 1 }; }); } return (

Recruiter Dashboard

Showing the snapshot from {meta.lastUpdated ? new Date(meta.lastUpdated).toLocaleString() : "just now"} — click Refresh for current data.

{data.error &&
Couldn't load data: {data.error}
}
{data.observability && (
)}
{data.loading ? (

Loading candidates…

) : (
{SORTABLE_COLUMNS.map(function (col) { var active = sort.key === col.key; return ( ); })} {visibleCandidates.map(function (c) { var isExpanded = expandedId === c.candidate_id; return ( {isExpanded && ( )} ); })}
{initialsFor(c.Name)}
{c.Name}
{c["JD Match"]} {c["Candidate Type"]} {c["Resume Strength Score"]}
)}
); } /** Full recruiter-facing profile for one candidate — only renders a field * when it's actually present on the record (see Dashboard doc comment). */ function CandidateDetail(props) { var c = props.candidate; function hasContent(value) { if (value === null || value === undefined || value === "") return false; if (Array.isArray(value)) return value.length > 0; return true; } function Field(fieldProps) { if (!hasContent(fieldProps.value)) return null; return (
{fieldProps.label}
{fieldProps.children || fieldProps.value}
); } function ChipList(listProps) { if (!hasContent(listProps.items)) return null; return (
{listProps.items.map(function (item, i) { return {item}; })}
); } // Field (above) only has a `value` prop to check visibility against, so // it can't tell whether a passed as children is actually // empty — it would render the label with nothing next to it. This // variant checks the underlying items array itself and hides the whole // row (label included) when there's nothing to show. function ChipField(fieldProps) { if (!hasContent(fieldProps.items)) return null; return (
{fieldProps.label}
); } var missing = c["Missing Keywords"]; var missingFlat = missing && typeof missing === "object" && !Array.isArray(missing) ? Object.keys(missing).reduce(function (acc, k) { return acc.concat(missing[k] || []); }, []) : missing; return (

Contact

{hasContent(c["Profile Summary"]) && (

Profile Summary

{c["Profile Summary"]}

)} {(hasContent(c["Top Matching Keywords"]) || hasContent(missingFlat)) && (

Skills

)} {hasContent(c["Projects"]) && (

Projects

{c["Projects"].map(function (p, i) { return (
{p["Project Name"]} {p["Relevance to JD"] && — {p["Relevance to JD"]} relevance} {p["Impact"] &&

{p["Impact"]}

}
); })}
)} {(hasContent(c["Certifications & Courses"]) || hasContent(c["Skills That Will Contribute to the Company"]) || hasContent(c["Soft Skills & Leadership Qualities"])) && (

Certifications & Soft Skills

)}

Fit & Experience

{(hasContent(c["Potential Concerns"]) || hasContent(c["Red Flags & Risk Analysis"])) && (

Risk Signals

)} {hasContent(c["HR Notes"]) && (

HR Notes

{c["HR Notes"]}

)}
); } function StatTile(props) { return (
{props.value}
{props.label}
); } function TierBadge(props) { var toneClass = props.tier === "Recommended for Fast-Track" ? "badge-good" : props.tier === "Rejected" ? "badge-bad" : "badge-neutral"; // Reads as an AI suggestion a recruiter can accept or override, not a // decision already made -- see uiLabels.js's own doc comment. return {tierDisplayLabel(props.tier)}; }