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 | 720d47fae476b0d38477ae4401697d372b1ea2bc |
| Branch | feature/opportunity-to-outcome |
| Path or archive member | frontend/src/tools/recruitmatch/RecruiterDashboard.tsx |
| Original SHA-256 | 03eefcfc2c9862eed4011410b062230d8b0bdcac5501a479094194ae5b547394 |
| Git blob SHA | 94e410ec817ddc78282241509160826fffde4005 |
| Reading-copy SHA-256 | 03eefcfc2c9862eed4011410b062230d8b0bdcac5501a479094194ae5b547394 |
Recovered source transcript
Download complete source text ↓384 lines · 17.2 KB
import { Briefcase, ClipboardList, RefreshCw, ShieldCheck, Users } from 'lucide-react'
import { Link } from 'react-router-dom'
import { ToolHeader } from '../../components/ToolHeader'
import { Bar, Panel, Stat } from '../../components/ui'
import { num } from '../../lib/format'
import type { Alert, RMFunnelStage } from '../../lib/types'
import { recruitBrand } from './brand'
import { useAlerts, useRecommendations, useRecruitDashboard } from './queries'
// REDEPLOYMENT GETS ITS OWN PANEL rather than a slot in the mixed alert feed.
//
// A contractor rolling off is the warmest and cheapest candidate a staffing firm has: already cleared,
// already known to the client, already proven on the work. Buried in a feed truncated at six and sorted by
// nothing in particular, it is the signal most likely to be missed, and missing it is how a firm loses a
// billable person it already had.
const REDEPLOYMENT = 'redeployment_due'
// alertLink opens an alert onto the thing it is about, where that thing is a record this product has a screen
// for. An alert a recruiter cannot act on from is a notification, not a worklist.
function alertLink(a: Alert): string | null {
if (a.subject_type === 'candidate' && a.subject_id) {
return `/bench/candidate/${encodeURIComponent(a.subject_id)}`
}
return null
}
// AlertRow renders one alert, as a link when there is somewhere to go and as plain text otherwise, so a
// non actionable alert never looks like a dead button.
function AlertRow({ a }: { a: Alert }) {
const to = alertLink(a)
const body = (
<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>
)
return (
<li className="flex items-start justify-between gap-3 py-2.5">
{to ? (
<Link to={to} className="min-w-0 flex-1 hover:underline">
{body}
</Link>
) : (
body
)}
<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>
)
}
// 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()
// Split once, here, so the two panels below cannot disagree about which alerts belong to which. The
// redeployment list is NOT truncated: six is a reasonable glance at a mixed feed and a wrong cap on a
// worklist, because the seventh person rolling off is just as billable as the first.
const all = alerts.data ?? []
const rollingOff = all.filter((a) => a.alert_type === REDEPLOYMENT)
const otherAlerts = all.filter((a) => a.alert_type !== REDEPLOYMENT)
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>
{/* ROLLING OFF sits ABOVE the other panels and outside the two column grid, because it is the panel a
recruiter should see first and act on today. Everything else on this screen reports; this one asks
for something to be done, and it has a deadline attached. */}
<Panel
title="Rolling off"
hint={rollingOff.length === 1 ? '1 contractor' : `${rollingOff.length} contractors`}
>
{alerts.isError ? (
<p className="py-6 text-center text-sm text-warn">This is unavailable for your role.</p>
) : rollingOff.length === 0 ? (
<p className="py-6 text-center text-sm text-faint">
Nobody is finishing an assignment in the next six weeks, or everybody who is has their next one
already lined up. People with a next assignment booked are left out on purpose.
</p>
) : (
<>
<p className="pb-2 text-[11px] leading-relaxed text-faint">
These people finish soon and have nothing booked after. They are already cleared, already known
to the client, and already proven on the work, which makes them the cheapest placement available
to you this month.
</p>
<ul className="divide-y divide-line/50">
{rollingOff.map((a) => (
<AlertRow key={a.id} a={a} />
))}
</ul>
</>
)}
</Panel>
<div className="grid grid-cols-1 items-start gap-4 xl:grid-cols-2">
<Panel
title="Alerts"
hint={alerts.isLoading ? 'loading' : `${otherAlerts.length} unread`}
>
{alerts.isError ? (
<p className="py-6 text-center text-sm text-warn">Alerts are unavailable for your role.</p>
) : otherAlerts.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">
{otherAlerts.slice(0, 6).map((a) => (
<AlertRow key={a.id} a={a} />
))}
</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>
)
}