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 = (

{a.title}

{a.detail ? (

{a.detail}

) : null}
) return (
  • {to ? ( {body} ) : ( body )} {a.severity}
  • ) } // 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 = { 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 Aggregating recruiting operations… } if (isError || !d) { return ( Could not load the recruiting dashboard. {(error as Error)?.message} ) } const benchEmpty = d.headline.identified === 0 return (

    as of {d.as_of}

    } accent="var(--color-recruit)" /> } /> } />
    {/* 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. */}
    {benchEmpty ? (

    No candidates yet. The funnel fills as candidates are added and screened.

    ) : (
      {d.funnel.map((s) => (
    • {s.label} {s.count} ({num(s.pct).toFixed(1)}%)
    • ))}
    )}
    {benchEmpty ? (

    No candidates on the bench yet.

    ) : (
      {d.clearance.map((c) => (
    • {c.label} {c.count} ({num(c.pct).toFixed(1)}%)
    • ))}
    )}
    {d.decisions.total === 0 ? (

    No slate members yet. Generate a slate in slateMATCH.

    ) : (
    • Approved {d.decisions.approved}
    • Rejected {d.decisions.rejected}
    • Pending {d.decisions.pending}
    • Approve rate {num(d.decisions.approve_rate).toFixed(1)}%
    )}
    {d.band_mix.total === 0 ? (

    Nothing screened yet.

    ) : (
    • Proposal ready {d.band_mix.green}
    • Potential {d.band_mix.yellow}
    • Recruiting required {d.band_mix.red}
    )}
    {d.top_jobs.length === 0 ? (

    No open requirements yet.

    ) : ( // 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.
      {d.top_jobs.map((j) => (
    • {j.title}

      {j.customer_agency || 'n/a'}

      {j.submitted}
    • ))}
    )}
    {d.recruiters.length === 0 ? (

    No active users in this organization yet.

    ) : ( {d.recruiters.map((r) => ( ))}
    Recruiter Screened Decided Approved
    {r.full_name} {r.screened} {r.decided} {r.approved}
    )}
    {/* 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. */} {alerts.isError ? (

    This is unavailable for your role.

    ) : rollingOff.length === 0 ? (

    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.

    ) : ( <>

    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.

      {rollingOff.map((a) => ( ))}
    )}
    {alerts.isError ? (

    Alerts are unavailable for your role.

    ) : otherAlerts.length === 0 ? (

    No open alerts. The watch loop runs daily and posts clearance expiry, recompete and coverage findings here.

    ) : (
      {otherAlerts.slice(0, 6).map((a) => ( ))}
    )}
    {recs.isError ? (

    Recommendations are unavailable for your role.

    ) : !recs.data || recs.data.recommendations.length === 0 ? (

    No recommendations yet. These are generated from open requirements and the current bench.

    ) : (
      {recs.data.recommendations.map((r) => (
    • {r.action} confidence {(num(r.confidence) * 100).toFixed(0)}%

      {r.rationale}

      {r.rationale_source === 'deterministic' ? (

      deterministic rationale

      ) : null}
    • ))}
    )}
      {d.empty_panels.map((p) => (
    • {p.title}

      {p.reason}

    • ))}
    ) }