Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / frontend/src/tools/recruitmatch/RecruiterDashboard.tsx |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 2178e7298d0e5d9af83afa12402fd587f0ba6ed8 |
| Branch | feature/aioe-upgrade |
| Path or archive member | frontend/src/tools/recruitmatch/RecruiterDashboard.tsx |
| Original SHA-256 | bf0ad4317e1ece3da4c17aac69d4b11d869396d6dd8a10b25defe6f9c7c484dd |
| Git blob SHA | 3de0acce8ec9f7dcb71ca5a09fe5eed68301747c |
| Reading-copy SHA-256 | bf0ad4317e1ece3da4c17aac69d4b11d869396d6dd8a10b25defe6f9c7c484dd |
Recovered source transcript
Download complete source text ↓307 lines · 14.2 KB
import { Briefcase, ClipboardList, RefreshCw, ShieldCheck, Users } from 'lucide-react'
import { ToolHeader } from '../../components/ToolHeader'
import { Bar, Panel, Stat } from '../../components/ui'
import { num } from '../../lib/format'
import type { RMFunnelStage } from '../../lib/types'
import { recruitBrand } from './brand'
import { useAlerts, useRecommendations, useRecruitDashboard } from './queries'
// Severity maps onto the shared status palette. An unknown severity falls back to muted rather than
// throwing, because the alert_type CHECK can gain values without the UI shipping at the same time.
function severityColor(s: string): string {
if (s === 'urgent') return 'var(--color-risk)'
if (s === 'watch') return 'var(--color-warn)'
return 'var(--color-muted)'
}
// Funnel colors are a fixed progression, deliberately NOT the engine bands. A screen through rate of 50
// percent is not a red candidate: banding a conversion percentage with scoring thresholds would conflate
// two different scales. The backend emits no color for the same reason.
const STAGE_COLOR: Record<string, string> = {
identified: 'var(--color-blue-soft)',
screened: 'var(--color-screen)',
submitted: 'var(--color-recruit)',
approved: 'var(--color-ok)',
}
function stageColor(s: RMFunnelStage): string {
return STAGE_COLOR[s.key] ?? 'var(--color-blue)'
}
export function RecruiterDashboard() {
const { data: d, isLoading, isError, error, refetch, isFetching } = useRecruitDashboard()
const alerts = useAlerts()
const recs = useRecommendations()
if (isLoading) {
return <Panel className="grid place-items-center py-24 text-muted">Aggregating recruiting operations…</Panel>
}
if (isError || !d) {
return (
<Panel className="grid place-items-center py-24 text-warn">
Could not load the recruiting dashboard. {(error as Error)?.message}
</Panel>
)
}
const benchEmpty = d.headline.identified === 0
return (
<div className="space-y-4">
<ToolHeader name={recruitBrand.name} tagline={recruitBrand.tagline} accent={recruitBrand.accent}>
<button
onClick={() => void refetch()}
disabled={isFetching}
className="flex items-center gap-2 rounded-lg border border-line/70 px-3 py-2 text-sm font-semibold text-muted hover:text-ink disabled:opacity-50"
>
<RefreshCw size={14} className={isFetching ? 'animate-spin' : ''} />
Refresh
</button>
</ToolHeader>
<p className="text-[11px] font-bold uppercase tracking-wider text-faint">as of {d.as_of}</p>
<div className="grid grid-cols-2 gap-3 md:grid-cols-3 xl:grid-cols-6">
<Stat label="Open roles" value={d.headline.active_jobs} icon={<Briefcase size={16} />} accent="var(--color-recruit)" />
<Stat label="Candidates" value={d.headline.identified} icon={<Users size={16} />} />
<Stat label="On bench" value={d.headline.on_bench} icon={<ShieldCheck size={16} />} />
<Stat label="Screened" value={d.headline.screened} />
<Stat label="Submitted" value={d.headline.submitted} />
<Stat label="Approved" value={d.headline.approved} accent="var(--color-ok)" />
</div>
{/* items-start stops the grid stretching every panel to the tallest column, which is what left
the short lists sitting in a tall empty box. */}
<div className="grid grid-cols-1 items-start gap-4 xl:grid-cols-12">
<Panel title="Pipeline overview" hint="share of the bench" className="xl:col-span-5">
{benchEmpty ? (
<p className="py-6 text-center text-sm text-faint">
No candidates yet. The funnel fills as candidates are added and screened.
</p>
) : (
<ul className="space-y-3">
{d.funnel.map((s) => (
<li key={s.key}>
<div className="flex items-baseline justify-between">
<span className="text-sm font-semibold">{s.label}</span>
<span className="text-sm tabular-nums text-muted">
{s.count} <span className="text-faint">({num(s.pct).toFixed(1)}%)</span>
</span>
</div>
<Bar value={num(s.pct)} color={stageColor(s)} />
</li>
))}
</ul>
)}
</Panel>
<Panel title="Clearance" hint="self reported indicator" className="xl:col-span-4">
{benchEmpty ? (
<p className="py-6 text-center text-sm text-faint">No candidates on the bench yet.</p>
) : (
<ul className="space-y-3">
{d.clearance.map((c) => (
<li key={c.code}>
<div className="flex items-baseline justify-between">
<span className="text-sm font-semibold">{c.label}</span>
<span className="text-sm tabular-nums text-muted">
{c.count} <span className="text-faint">({num(c.pct).toFixed(1)}%)</span>
</span>
</div>
<Bar value={num(c.pct)} color="var(--color-recruit)" />
</li>
))}
</ul>
)}
</Panel>
<div className="space-y-4 xl:col-span-3">
<Panel title="Review queue">
{d.decisions.total === 0 ? (
<p className="py-6 text-center text-sm text-faint">
No slate members yet. Generate a slate in slateMATCH.
</p>
) : (
<ul className="space-y-2.5">
<li className="flex items-center justify-between">
<span className="text-sm" style={{ color: 'var(--color-ok)' }}>Approved</span>
<span className="tabular-nums">{d.decisions.approved}</span>
</li>
<li className="flex items-center justify-between">
<span className="text-sm" style={{ color: 'var(--color-risk)' }}>Rejected</span>
<span className="tabular-nums">{d.decisions.rejected}</span>
</li>
<li className="flex items-center justify-between">
<span className="text-sm" style={{ color: 'var(--color-warn)' }}>Pending</span>
<span className="tabular-nums">{d.decisions.pending}</span>
</li>
<li className="flex items-center justify-between border-t border-line/50 pt-2.5">
<span className="text-[11px] font-bold uppercase tracking-wider text-muted">Approve rate</span>
<span className="tabular-nums font-bold">{num(d.decisions.approve_rate).toFixed(1)}%</span>
</li>
</ul>
)}
</Panel>
<Panel title="Screening bands">
{d.band_mix.total === 0 ? (
<p className="py-6 text-center text-sm text-faint">Nothing screened yet.</p>
) : (
<ul className="space-y-2.5">
<li className="flex items-center justify-between">
<span className="text-sm" style={{ color: 'var(--color-ok)' }}>Proposal ready</span>
<span className="tabular-nums">{d.band_mix.green}</span>
</li>
<li className="flex items-center justify-between">
<span className="text-sm" style={{ color: 'var(--color-warn)' }}>Potential</span>
<span className="tabular-nums">{d.band_mix.yellow}</span>
</li>
<li className="flex items-center justify-between">
<span className="text-sm" style={{ color: 'var(--color-risk)' }}>Recruiting required</span>
<span className="tabular-nums">{d.band_mix.red}</span>
</li>
</ul>
)}
</Panel>
</div>
</div>
<div className="grid grid-cols-1 items-start gap-4 xl:grid-cols-2">
<Panel title="Open roles by submissions" hint={`${d.top_jobs.length} shown`}>
{d.top_jobs.length === 0 ? (
<p className="py-6 text-center text-sm text-faint">No open requirements yet.</p>
) : (
// A list rather than a table. Federal requirement titles and agency names are long enough
// that a three column table forced the agency to wrap onto three lines and left every row a
// different height. The agency reads as a subtitle instead, so the title gets the full width.
<ul className="divide-y divide-line/50">
{d.top_jobs.map((j) => (
<li key={j.requirement_id} className="flex items-center justify-between gap-4 py-2.5">
<div className="min-w-0">
<p className="truncate text-sm font-semibold" title={j.title}>{j.title}</p>
<p className="truncate text-[11px] text-faint" title={j.customer_agency || 'n/a'}>
{j.customer_agency || 'n/a'}
</p>
</div>
<span className="shrink-0 text-sm tabular-nums text-muted">{j.submitted}</span>
</li>
))}
</ul>
)}
</Panel>
<Panel title="Recruiter activity" hint="attribution, not a score">
{d.recruiters.length === 0 ? (
<p className="py-6 text-center text-sm text-faint">No active users in this organization yet.</p>
) : (
<table className="w-full text-sm">
<thead>
<tr className="text-left text-[11px] uppercase tracking-wider text-faint">
<th className="pb-2 font-semibold">Recruiter</th>
<th className="pb-2 text-right font-semibold">Screened</th>
<th className="pb-2 text-right font-semibold">Decided</th>
<th className="pb-2 text-right font-semibold">Approved</th>
</tr>
</thead>
<tbody className="divide-y divide-line/50">
{d.recruiters.map((r) => (
<tr key={r.user_id} className="hover:bg-navy-800/40">
<td className="py-2.5 font-semibold">{r.full_name}</td>
<td className="py-2.5 text-right tabular-nums">{r.screened}</td>
<td className="py-2.5 text-right tabular-nums">{r.decided}</td>
<td className="py-2.5 text-right tabular-nums">{r.approved}</td>
</tr>
))}
</tbody>
</table>
)}
</Panel>
</div>
<div className="grid grid-cols-1 items-start gap-4 xl:grid-cols-2">
<Panel
title="Alerts"
hint={alerts.isLoading ? 'loading' : alerts.data ? `${alerts.data.length} unread` : ''}
>
{alerts.isError ? (
<p className="py-6 text-center text-sm text-warn">Alerts are unavailable for your role.</p>
) : !alerts.data || alerts.data.length === 0 ? (
<p className="py-6 text-center text-sm text-faint">
No open alerts. The watch loop runs daily and posts clearance expiry, recompete and
coverage findings here.
</p>
) : (
<ul className="divide-y divide-line/50">
{alerts.data.slice(0, 6).map((a) => (
<li key={a.id} className="flex items-start justify-between gap-3 py-2.5">
<div className="min-w-0">
<p className="truncate text-sm font-semibold" title={a.title}>{a.title}</p>
{a.detail ? (
<p className="truncate text-[11px] text-faint" title={a.detail}>{a.detail}</p>
) : null}
</div>
<span
className="shrink-0 rounded-full px-2 py-0.5 text-[10px] font-bold uppercase tracking-wide"
style={{
color: severityColor(a.severity),
background: `color-mix(in srgb, ${severityColor(a.severity)} 14%, transparent)`,
}}
>
{a.severity}
</span>
</li>
))}
</ul>
)}
</Panel>
<Panel title="Recommended actions" hint="human decides, never automatic">
{recs.isError ? (
<p className="py-6 text-center text-sm text-warn">
Recommendations are unavailable for your role.
</p>
) : !recs.data || recs.data.recommendations.length === 0 ? (
<p className="py-6 text-center text-sm text-faint">
No recommendations yet. These are generated from open requirements and the current bench.
</p>
) : (
<ul className="divide-y divide-line/50">
{recs.data.recommendations.map((r) => (
<li key={r.id} className="py-2.5">
<div className="flex items-baseline justify-between gap-3">
<span className="text-sm font-semibold capitalize">{r.action}</span>
<span className="shrink-0 text-[11px] tabular-nums text-faint">
confidence {(num(r.confidence) * 100).toFixed(0)}%
</span>
</div>
<p className="mt-0.5 text-[11px] leading-relaxed text-muted">{r.rationale}</p>
{r.rationale_source === 'deterministic' ? (
<p className="mt-0.5 text-[10px] uppercase tracking-wide text-faint">
deterministic rationale
</p>
) : null}
</li>
))}
</ul>
)}
</Panel>
</div>
<Panel title="Not tracked yet" hint="named feeds, never invented numbers">
<ul className="grid grid-cols-1 gap-2 sm:grid-cols-2 xl:grid-cols-3">
{d.empty_panels.map((p) => (
<li key={p.key} className="rounded-lg border border-line/60 bg-navy-950/40 p-3">
<div className="flex items-center gap-2">
<ClipboardList size={13} className="text-faint" />
<p className="text-sm font-semibold text-muted">{p.title}</p>
</div>
<p className="mt-1 text-[11px] leading-relaxed text-faint">{p.reason}</p>
</li>
))}
</ul>
</Panel>
</div>
)
}