Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / frontend/src/tools/recruitmatch/Analytics.tsx |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 720d47fae476b0d38477ae4401697d372b1ea2bc |
| Branch | feature/opportunity-to-outcome |
| Path or archive member | frontend/src/tools/recruitmatch/Analytics.tsx |
| Original SHA-256 | ca3469d36a2aa3549fccf5c928a39442c9f87b1babf5604446dffe2740c3f916 |
| Git blob SHA | 84e0cfb97f49fdb593410de76b41ab918de80fcd |
| Reading-copy SHA-256 | ca3469d36a2aa3549fccf5c928a39442c9f87b1babf5604446dffe2740c3f916 |
Recovered source transcript
Download complete source text ↓406 lines · 18.7 KB
import { useMemo, useState } from 'react'
import { AlertTriangle, ArrowRight, Globe, Info, TrendingUp } from 'lucide-react'
import { ToolHeader } from '../../components/ToolHeader'
import { Bar, Panel } from '../../components/ui'
import type { RTimeToFill } from '../../lib/types'
import { recruitBrand } from './brand'
import { useAnalytics } from './analyticsQueries'
const chip = (on: boolean) =>
`rounded-full px-3 py-1 text-xs font-semibold transition ${
on
? 'bg-recruit/25 text-recruit shadow-[inset_0_0_0_1px_var(--color-recruit)]'
: 'bg-navy-900 text-faint hover:text-muted border border-line/60'
}`
const money = (v?: string) => {
if (!v) return ''
const [w, f = ''] = v.split('.')
const t = f.replace(/0+$/, '')
return '$' + w + '.' + (t.length <= 2 ? t.padEnd(2, '0') : t)
}
// The time to fill card, which is the whole point of this screen.
//
// Both figures are shown side by side and labelled. The naive one is what every other tool reports; the
// corrected one keeps the still open orders in the calculation. When they differ, the gap IS the
// survivorship bias, and showing only one of them is how that bias goes unnoticed.
function TimeToFillCard({ title, hint, ttf }: { title: string; hint: string; ttf: RTimeToFill }) {
const biased =
ttf.median_reached && ttf.naive_median_days && ttf.median_days
? Number(ttf.median_days) - Number(ttf.naive_median_days)
: null
return (
<Panel title={title} hint={hint}>
{ttf.cohort === 0 ? (
<p className="py-6 text-center text-[11px] text-faint">No requisitions opened in this window.</p>
) : (
<>
<div className="grid grid-cols-2 gap-3">
<div className="rounded-xl border border-recruit/40 bg-navy-950/40 p-3">
<p className="text-[10px] font-bold uppercase tracking-wider text-faint">Median, corrected</p>
{ttf.median_reached ? (
<p className="mt-1 text-2xl font-extrabold tracking-tight text-recruit">
{Number(ttf.median_days).toFixed(1)}
<span className="ml-1 text-sm font-semibold text-muted">days</span>
</p>
) : (
// Absent is not zero. Fewer than half the cohort has filled, so a median does not exist
// yet, and inventing one would be a fabrication.
<p className="mt-1 text-lg font-extrabold tracking-tight text-warn">Not yet reached</p>
)}
<p className="mt-0.5 text-[10px] text-faint">
keeps the {ttf.still_open} still open in the calculation
</p>
</div>
<div className="rounded-xl border border-line/60 bg-navy-950/40 p-3">
<p className="text-[10px] font-bold uppercase tracking-wider text-faint">Median, filled only</p>
<p className="mt-1 text-2xl font-extrabold tracking-tight text-muted">
{ttf.naive_median_days ? Number(ttf.naive_median_days).toFixed(1) : '—'}
{ttf.naive_median_days && <span className="ml-1 text-sm font-semibold text-faint">days</span>}
</p>
<p className="mt-0.5 text-[10px] text-faint">ignores everything still open</p>
</div>
</div>
{!ttf.median_reached && ttf.naive_median_days && (
<p className="mt-3 rounded-lg border border-warn/40 bg-navy-950/40 p-3 text-[11px] text-muted">
<AlertTriangle size={11} className="mr-1 inline" />
Only {ttf.filled} of {ttf.cohort} have filled, so half the cohort has not finished and there
is no median yet. The {Number(ttf.naive_median_days).toFixed(1)} day figure beside it counts
only the ones that finished, which is why it looks fast.
</p>
)}
{biased !== null && biased > 0.05 && (
<p className="mt-3 rounded-lg border border-line/60 bg-navy-950/40 p-3 text-[11px] text-muted">
<Info size={11} className="mr-1 inline" />
Counting only filled orders understates this by {biased.toFixed(1)} days.
</p>
)}
<p className="mt-3 text-[10px] text-faint">
{ttf.cohort} opened · {ttf.filled} filled · {ttf.still_open} still open
</p>
</>
)}
</Panel>
)
}
export function Analytics() {
const [days, setDays] = useState(90)
// Memoised on the window length, not recomputed per render.
//
// Without this, Date.now() produces a slightly different instant every render, the query key changes
// with it, and React Query refetches forever: the screen sits on "Computing" while the API answers
// correctly every time. The API being right is exactly why this only shows up by looking at the page.
const since = useMemo(() => new Date(Date.now() - days * 864e5).toISOString(), [days])
const q = useAnalytics(since)
if (q.isLoading) return <Panel className="grid place-items-center py-24 text-muted">Computing…</Panel>
if (q.isError || !q.data) {
return (
<Panel className="grid place-items-center py-24 text-warn">
Could not load analytics. {(q.error as Error)?.message}
</Panel>
)
}
const v = q.data
const maxRung = v.funnel.length ? v.funnel[0].count : 0
const maxAging = Math.max(1, ...v.aging.map((a) => a.count))
return (
<div className="space-y-4">
<ToolHeader name={recruitBrand.name} tagline="Analytics" accent={recruitBrand.accent}>
<span className="flex items-center gap-1.5 text-[11px] text-faint">
<Globe size={11} />
{v.time_zone}
</span>
</ToolHeader>
<div className="flex flex-wrap items-center gap-2">
{[30, 90, 180, 365].map((d) => (
<button key={d} className={chip(days === d)} onClick={() => setDays(d)}>
{d} days
</button>
))}
<span className="ml-auto text-[11px] text-faint">
Cohort is every job order OPENED in the window, not every one filled.
</span>
</div>
<div className="grid gap-4 lg:grid-cols-2">
<TimeToFillCard title="Time to offer" hint="opened until an offer was accepted" ttf={v.time_to_offer} />
<TimeToFillCard title="Time to start" hint="opened until someone started" ttf={v.time_to_start} />
</div>
<TimeToFillCard
title="Working time to start"
hint="time to start, minus time the order spent on hold"
ttf={v.worked_time_to_start}
/>
<div className="grid gap-4 lg:grid-cols-2">
<Panel title="Outcomes" hint="both denominators">
<div className="grid grid-cols-4 gap-2">
{[
{ l: 'Filled', n: v.fill_rate.filled },
{ l: 'Closed', n: v.fill_rate.closed_unfilled },
{ l: 'Cancelled', n: v.fill_rate.cancelled },
{ l: 'Open', n: v.fill_rate.still_open },
].map((x) => (
<div key={x.l} className="rounded-lg border border-line/60 bg-navy-950/40 p-2 text-center">
<p className="text-[10px] font-bold uppercase tracking-wider text-faint">{x.l}</p>
<p className="text-xl font-extrabold">{x.n}</p>
</div>
))}
</div>
{v.fill_rate.suppressed ? (
// The counts are always shown; only the ratio is withheld, because a rate over a handful of
// orders is arithmetically correct and meaningless.
<p className="mt-3 rounded-lg border border-line/60 bg-navy-950/40 p-3 text-[11px] text-muted">
Too few resolved job orders to report a rate. The counts above are exact.
</p>
) : (
<div className="mt-3 space-y-1.5 text-[11px]">
<p className="flex justify-between">
<span className="text-muted">Fill rate, counting cancellations</span>
<span className="font-semibold">{v.fill_rate.rate_of_resolved}%</span>
</p>
<p className="flex justify-between">
<span className="text-muted">Fill rate, excluding cancellations</span>
<span className="font-semibold">{v.fill_rate.rate_of_decided ?? '—'}%</span>
</p>
<p className="text-[10px] text-faint">
A cancelled order is a role the client pulled. Both denominators are shown because the
definition genuinely forks and picking one silently makes two reports disagree.
</p>
</div>
)}
</Panel>
<Panel title="Open job orders by age" hint={`oldest ${v.oldest_open_days}d`}>
{v.fill_rate.still_open === 0 ? (
<p className="py-6 text-center text-[11px] text-faint">Nothing open.</p>
) : (
<div className="space-y-2">
{v.aging.map((a) => (
<div key={a.label} className="flex items-center gap-3">
<span className="w-28 shrink-0 text-[11px] text-muted">{a.label}</span>
<div className="h-5 flex-1 overflow-hidden rounded bg-navy-950/60">
<div
className="h-full rounded"
style={{
width: `${Math.max((a.count / maxAging) * 100, a.count > 0 ? 6 : 0)}%`,
background:
a.min_days >= 60
? 'var(--color-risk)'
: a.min_days >= 30
? 'var(--color-warn)'
: 'var(--color-blue-soft)',
}}
/>
</div>
<span className="w-8 shrink-0 text-right text-[11px] font-semibold">{a.count}</span>
</div>
))}
<p className="text-[10px] text-faint">
A distribution, not an average. An average age hides the tail, and the tail is the problem.
</p>
</div>
)}
</Panel>
</div>
<div className="grid gap-4 lg:grid-cols-2">
<Panel title="Candidate funnel" hint="everyone who ever reached each stage">
<div className="space-y-2">
{v.funnel.map((s) => (
<div key={s.stage} className="flex items-center gap-3">
<span className="w-24 shrink-0 text-[11px] text-muted">{s.label}</span>
<div className="h-5 flex-1 overflow-hidden rounded bg-navy-950/60">
<div
className="h-full rounded bg-gradient-to-r from-blue to-recruit"
style={{ width: `${maxRung > 0 ? Math.max((s.count / maxRung) * 100, s.count > 0 ? 6 : 0) : 0}%` }}
/>
</div>
<span className="w-16 shrink-0 text-right text-[11px]">
{s.count} · {s.pct}%
</span>
</div>
))}
</div>
{v.conversion.length > 0 && v.funnel[0].count > 0 && (
<ul className="mt-3 space-y-1 border-t border-line/50 pt-3">
{v.conversion.map((c) => (
<li key={`${c.from}-${c.to}`} className="flex justify-between text-[11px]">
<span className="text-muted">
{c.from_label} <ArrowRight size={9} className="inline" /> {c.to_label}
</span>
<span className="font-semibold">{c.rate}%</span>
</li>
))}
</ul>
)}
</Panel>
<Panel title="Source effectiveness" hint="attributed on first touch">
{v.sources.length === 0 ? (
<p className="py-6 text-center text-[11px] text-faint">No candidates in this window.</p>
) : (
<ul className="space-y-1.5">
{v.sources.map((s) => (
<li key={s.source} className="flex items-center justify-between gap-3 text-[11px]">
<span className="min-w-0 truncate text-muted">{s.source}</span>
<span className="shrink-0 text-faint">
{s.applications} candidates · {s.placed} placed
{s.suppressed ? (
<span className="ml-2 text-faint">rate withheld</span>
) : (
<span className="ml-2 font-semibold text-ink">{s.rate}%</span>
)}
</span>
</li>
))}
</ul>
)}
<p className="mt-3 text-[10px] text-faint">
First touch, never last. Attributing a placement to the most recent contact is how every
channel eventually looks like a referral.
</p>
</Panel>
</div>
<div className="grid gap-4 lg:grid-cols-2">
<Panel title="Placements" hint="contract and permanent kept apart">
<div className="grid grid-cols-3 gap-2">
{[
{ l: 'Live', n: v.placements.live },
{ l: 'Started', n: v.placements.started },
{ l: 'Fell off', n: v.placements.fell_off },
].map((x) => (
<div key={x.l} className="rounded-lg border border-line/60 bg-navy-950/40 p-2 text-center">
<p className="text-[10px] font-bold uppercase tracking-wider text-faint">{x.l}</p>
<p className="text-xl font-extrabold">{x.n}</p>
</div>
))}
</div>
<div className="mt-3 space-y-1 text-[11px]">
{v.placements.contract_gross_per_hour && (
<p className="flex justify-between">
<span className="text-muted">Live contract spread</span>
<span className="font-semibold">{money(v.placements.contract_gross_per_hour)}/hr</span>
</p>
)}
{v.placements.direct_hire_fees && (
<p className="flex justify-between">
<span className="text-muted">Direct hire fees</span>
<span className="font-semibold">{money(v.placements.direct_hire_fees)}</span>
</p>
)}
{v.placements.guarantee_breaches > 0 && (
<p className="flex justify-between" style={{ color: 'var(--color-risk)' }}>
<span>Guarantee breaches</span>
<span className="font-semibold">{v.placements.guarantee_breaches}</span>
</p>
)}
<p className="text-[10px] text-faint">
This is the SPREAD, bill minus pay, before the cost of employing anybody. It is not margin and
it is not profit. It is also not annualised, because doing so would assume a full time full year
assignment nobody agreed to.
</p>
</div>
</Panel>
<Panel title="Recruiter effort" hint="logged, not captured">
{v.recruiters.length === 0 ? (
<p className="py-6 text-center text-[11px] text-faint">No activity logged in this window.</p>
) : (
<ul className="space-y-1.5">
{v.recruiters.map((r) => (
<li key={r.user_id || r.name} className="flex items-center justify-between gap-3 text-[11px]">
<span className="min-w-0 truncate">{r.name}</span>
<span className="shrink-0 text-faint">
{r.logged} logged · {r.connects} connected
{r.connect_rate && <span className="ml-2 font-semibold text-ink">{r.connect_rate}%</span>}
</span>
</li>
))}
</ul>
)}
<p className="mt-3 text-[10px] text-faint">
<TrendingUp size={10} className="mr-1 inline" />
Attempts and conversations are counted separately. A hundred dials with two conversations is a
different day from a hundred conversations.
</p>
</Panel>
{/* DESK OUTCOMES sits beside recruiter EFFORT deliberately. Effort says what somebody did, outcomes
say what it produced, and a desk logging two hundred calls and placing nobody reads identically to
one logging forty and placing four in an effort report alone. */}
<Panel title="Desk outcomes" hint="what the effort produced">
{v.desk_outcomes.length === 0 ? (
<p className="py-6 text-center text-[11px] text-faint">
Nothing was submitted or placed in this window.
</p>
) : (
<ul className="space-y-1.5">
{v.desk_outcomes.map((d) => (
<li key={d.user_id || d.name} className="flex items-center justify-between gap-3 text-[11px]">
<span className="min-w-0 truncate">{d.name}</span>
<span className="shrink-0 text-faint">
{d.submissions} submitted · {d.placements} placed
{d.placement_rate && (
<span className="ml-2 font-semibold text-ink">{d.placement_rate}%</span>
)}
</span>
</li>
))}
</ul>
)}
<p className="mt-3 text-[10px] leading-relaxed text-faint">
A submission counts from the stage history, so somebody submitted and later rejected still counts.
The work happened.
</p>
</Panel>
{/* REDEPLOYMENT is the metric that measures whether the firm keeps what it already won, and nothing
in this product could compute it until the redeployment watch existed. */}
<Panel
title="Redeployment"
hint={`within ${v.redeployment.window_days} days of finishing`}
>
{v.redeployment.ended === 0 ? (
<p className="py-6 text-center text-[11px] text-faint">
Nobody finished an assignment in this window, so there is nothing to measure yet.
</p>
) : (
<>
<div className="flex items-baseline gap-2">
<span className="text-3xl font-extrabold tabular-nums text-ink">
{v.redeployment.rate ?? '0'}%
</span>
<span className="text-[11px] text-faint">
{v.redeployment.redeployed} of {v.redeployment.ended} kept
</span>
</div>
<div className="mt-2">
<Bar
value={Number(v.redeployment.rate ?? 0)}
color={Number(v.redeployment.rate ?? 0) >= 50 ? 'var(--color-ok)' : 'var(--color-warn)'}
/>
</div>
</>
)}
<p className="mt-3 text-[10px] leading-relaxed text-faint">
A contractor rolling off is already cleared, already known to the client and already proven on the
work, which makes them the cheapest placement available. Everyone is counted once however many
assignments they ended.
</p>
</Panel>
</div>
</div>
)
}