import React from 'react'; import { BarChart } from '@mui/x-charts'; import { ChevronRight } from 'lucide-react'; import { cn } from '@/lib/utils'; import { chartSx, TONES, tooltipSx } from './muiChartTheme'; /** * The hiring flow — one surface, not six cards. * * A funnel is a shape before it is a set of numbers, so the magnitudes are drawn * once as a bar series and the stage-by-stage reading sits underneath it as a * connected strip: count, pass-through rate, and what was lost getting there. * Reading down a column gives one stage; reading across gives the journey. * * The weakest transition is the only thing coloured differently, because on a * funnel the one worth acting on is the one losing the most people. * * `stages` and `transitions` come from the page's own fact sheet — this * component derives no figures of its own. */ /** @param {any} props */ export function HiringFlow({ stages = [], transitions = [], weakestKey, className = '' }) { if (!stages.length) return null; const total = stages[0]?.count || 1; const rateFor = (i) => (i === 0 ? 100 : transitions[i - 1]?.rate ?? 0); const lostFor = (i) => (i === 0 ? 0 : transitions[i - 1]?.lost ?? 0); const isWeak = (i) => i > 0 && transitions[i - 1] && transitions[i - 1].to === weakestKey; return (
{/* The shape. Axis-free: the strip underneath carries every figure, so a y-axis would repeat what is already written. */}
s.label), categoryGapRatio: 0.5, barGapRatio: 0, /* Per-stage colour through the documented axis API rather than a CSS nth-child guess at MUI's internal markup: the weakest transition is amber, every other stage is brand blue. */ colorMap: { type: 'ordinal', values: stages.map((s) => s.label), colors: stages.map((_, i) => (isWeak(i) ? TONES.warning : TONES.brand)), }, }]} /* Pinned to the data: MUI's automatic domain rounds up to roughly double the top stage, which leaves the funnel occupying half the plot and reading much flatter than it is. */ yAxis={[{ min: 0, max: Math.max(total, 1), tickNumber: 3 }]} series={[{ data: stages.map((s) => s.count), label: 'Candidates', valueFormatter: (value, ctx) => { const i = ctx?.dataIndex ?? 0; const lost = lostFor(i); return `${value} · ${rateFor(i)}% through${lost ? ` · ${lost} lost` : ''}`; }, }]} colors={[TONES.brand]} borderRadius={6} grid={{ horizontal: true }} skipAnimation sx={{ ...chartSx, ...tooltipSx, /* The weakest transition's bar picks up the warning tone; every other bar stays brand blue. */ }} />
{/* The reading. Scrolls rather than wraps on a phone, so the sequence survives at any width. */}
    {stages.map((stage, i) => { const rate = rateFor(i); const lost = lostFor(i); const weak = isWeak(i); return (
  1. {i > 0 && ( )}

    {stage.label}

    {stage.count}

    {i === 0 ? `${Math.round((stage.count / total) * 100)}% entered` : `${rate}% through`} {lost > 0 && −{lost}}

  2. ); })}
); }