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 (
No requisitions opened in this window. Median, corrected
{Number(ttf.median_days).toFixed(1)}
days
Not yet reached
keeps the {ttf.still_open} still open in the calculation
Median, filled only
{ttf.naive_median_days ? Number(ttf.naive_median_days).toFixed(1) : '—'}
{ttf.naive_median_days && days}
ignores everything still open
{ttf.cohort} opened · {ttf.filled} filled · {ttf.still_open} still open
{x.l}
{x.n}
Too few resolved job orders to report a rate. The counts above are exact.
) : (Fill rate, counting cancellations {v.fill_rate.rate_of_resolved}%
Fill rate, excluding cancellations {v.fill_rate.rate_of_decided ?? '—'}%
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.
Nothing open.
) : (A distribution, not an average. An average age hides the tail, and the tail is the problem.
No candidates in this window.
) : (First touch, never last. Attributing a placement to the most recent contact is how every channel eventually looks like a referral.
{x.l}
{x.n}
Live contract spread {money(v.placements.contract_gross_per_hour)}/hr
)} {v.placements.direct_hire_fees && (Direct hire fees {money(v.placements.direct_hire_fees)}
)} {v.placements.guarantee_breaches > 0 && (Guarantee breaches {v.placements.guarantee_breaches}
)}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.
No activity logged in this window.
) : (
Nothing was submitted or placed in this window.
) : (A submission counts from the stage history, so somebody submitted and later rejected still counts. The work happened.
Nobody finished an assignment in this window, so there is nothing to measure yet.
) : ( <>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.