first commit

This commit is contained in:
2026-09-07 20:36:18 +05:30
commit fd366f7e4a
231 changed files with 52631 additions and 0 deletions

View File

@@ -0,0 +1,115 @@
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 (
<div className={cn('overflow-hidden rounded-xl border border-border bg-surface', className)}>
{/* The shape. Axis-free: the strip underneath carries every figure, so a
y-axis would repeat what is already written. */}
<div className="px-2 pt-3 sm:px-4">
<BarChart
height={168}
margin={{ top: 12, right: 8, bottom: 24, left: 8 }}
xAxis={[{
scaleType: 'band',
data: stages.map((s) => 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. */
}}
/>
</div>
{/* The reading. Scrolls rather than wraps on a phone, so the sequence
survives at any width. */}
<div className="overflow-x-auto border-t border-border">
<ol className="flex min-w-[36rem] items-stretch">
{stages.map((stage, i) => {
const rate = rateFor(i);
const lost = lostFor(i);
const weak = isWeak(i);
return (
<li key={stage.key} className="flex min-w-0 flex-1 items-center">
{i > 0 && (
<span className="flex shrink-0 flex-col items-center px-1.5" aria-hidden="true">
<ChevronRight className={cn('h-3.5 w-3.5', weak ? 'text-warning' : 'text-ink-4')} />
</span>
)}
<div className="min-w-0 flex-1 px-3 py-3">
<p className="truncate text-caption text-ink-3">{stage.label}</p>
<p className="mt-0.5 font-heading text-title font-bold tabular-nums text-ink-1">
{stage.count}
</p>
<p className="mt-1 flex flex-wrap items-baseline gap-x-2 text-caption tabular-nums">
<span className={weak ? 'font-semibold text-warning' : 'text-ink-4'}>
{i === 0 ? `${Math.round((stage.count / total) * 100)}% entered` : `${rate}% through`}
</span>
{lost > 0 && <span className="text-ink-4">−{lost}</span>}
</p>
</div>
</li>
);
})}
</ol>
</div>
</div>
);
}