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 | 2178e7298d0e5d9af83afa12402fd587f0ba6ed8 |
| Branch | feature/aioe-upgrade |
| Path or archive member | frontend/src/tools/recruitmatch/Analytics.tsx |
| Original SHA-256 | 77b299df0dae762f8ed2637b1cd467ce7b8c385d9222dbc7035304fb93be3849 |
| Git blob SHA | 8acf0b800cfee9c9aab82f96bdcc2a146b92e129 |
| Reading-copy SHA-256 | 77b299df0dae762f8ed2637b1cd467ce7b8c385d9222dbc7035304fb93be3849 |
Recovered source transcript
Download complete source text ↓341 lines · 15.6 KB
import { useMemo, useState } from 'react'
import { AlertTriangle, ArrowRight, Globe, Info, TrendingUp } from 'lucide-react'
import { ToolHeader } from '../../components/ToolHeader'
import { 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 margin</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">
Hourly margin is not annualised. 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>
</div>
</div>
)
}