update hired page

This commit is contained in:
2026-08-11 19:17:40 +05:30
parent 8a256a4de7
commit 25c4ee4a6a
23 changed files with 2921 additions and 380 deletions

View File

@@ -9,6 +9,7 @@ import {
} from '@/components/ds';
import { useApplications, useInterviews, useJobPostings, useStaff, useWorkerProfiles } from '@/lib/krowHooks';
import { buildFacts } from '@/components/ai-assistant/insights';
import { HiringFlow } from '@/components/charts/HiringFlow';
import { AdminPage, SectionTitle } from '@/pages/admin/_shell';
/**
@@ -185,50 +186,32 @@ export default function AdminAnalytics() {
</ResponsiveContainer>
</section>
<div className="grid gap-6 lg:grid-cols-2">
{/* Pipeline conversion — stage-to-stage rates are the point, so each
step shows its own pass-through rather than only its count. */}
<section aria-labelledby="conversion" className="space-y-3">
<SectionTitle id="conversion" title="Pipeline conversion" meta={`${f.total} entered`} />
<div className="space-y-3">
{f.funnel.map((stage, i) => {
const rate = i === 0 ? 100 : f.transitions[i - 1]?.rate ?? 0;
const lost = i === 0 ? 0 : f.transitions[i - 1]?.lost ?? 0;
return (
<div key={stage.key}>
<div className="mb-1 flex items-baseline justify-between gap-2">
<span className="text-body-sm text-ink-2">{stage.label}</span>
<span className="flex items-baseline gap-2.5">
{lost > 0 && <span className="text-[10px] text-destructive">−{lost}</span>}
<span className={rate < 60 && i > 0 ? 'text-[11px] font-semibold text-warning' : 'text-[11px] text-ink-4'}>
{rate}%
</span>
<span className="w-8 text-right text-body-sm font-bold tabular-nums text-ink-1">{stage.count}</span>
</span>
</div>
<ProgressBar value={stage.count} max={f.total || 1} size="sm" tone={rate < 60 && i > 0 ? 'warning' : 'brand'} />
</div>
);
})}
</div>
</section>
{/* The funnel takes the full width: it is a horizontal sequence, and in a
half-width column the stage strip truncates its own labels. */}
<section aria-labelledby="conversion" className="space-y-3">
<SectionTitle id="conversion" title="Pipeline conversion" meta={`${f.total} entered`} />
<HiringFlow
stages={f.funnel}
transitions={f.transitions}
weakestKey={f.bottleneck?.to}
/>
</section>
{/* Score distribution — where the hireable pool actually sits. */}
<section aria-labelledby="quality" className="space-y-3">
<SectionTitle id="quality" title="Hiring quality" meta={`${f.scored.length} scored · avg ${f.avgScore}`} />
<ResponsiveContainer width="100%" height={196}>
<BarChart data={scoreDistribution}>
<CartesianGrid strokeDasharray="3 3" stroke={CHART_TONES.grid} vertical={false} />
<XAxis dataKey="label" {...AXIS_PROPS} />
<YAxis {...AXIS_PROPS} allowDecimals={false} width={28} />
<Tooltip content={<ChartTooltip />} />
<Bar dataKey="count" name="Candidates" radius={[4, 4, 0, 0]} maxBarSize={56}>
{scoreDistribution.map((d) => <Cell key={d.label} fill={bandColor(d.label)} />)}
</Bar>
</BarChart>
</ResponsiveContainer>
</section>
</div>
{/* Score distribution — where the hireable pool actually sits. */}
<section aria-labelledby="quality" className="space-y-3">
<SectionTitle id="quality" title="Hiring quality" meta={`${f.scored.length} scored · avg ${f.avgScore}`} />
<ResponsiveContainer width="100%" height={196}>
<BarChart data={scoreDistribution}>
<CartesianGrid strokeDasharray="3 3" stroke={CHART_TONES.grid} vertical={false} />
<XAxis dataKey="label" {...AXIS_PROPS} />
<YAxis {...AXIS_PROPS} allowDecimals={false} width={28} />
<Tooltip content={<ChartTooltip />} />
<Bar dataKey="count" name="Candidates" radius={[4, 4, 0, 0]} maxBarSize={56}>
{scoreDistribution.map((d) => <Cell key={d.label} fill={bandColor(d.label)} />)}
</Bar>
</BarChart>
</ResponsiveContainer>
</section>
{/* Velocity and screening efficiency. Both are ratios rather than shapes,
so they read better as figures with an explanation than as another

View File

@@ -1,12 +1,10 @@
import React, { useMemo } from 'react';
import { Area, AreaChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
import { ArrowRight, Award, Clock, TrendingUp, Users } from 'lucide-react';
import { cn } from '@/lib/utils';
import {
AXIS_PROPS, Avatar, Badge, CHART_TONES, ChartTooltip, DataTable, MetricStrip,
ProgressBar, StatusBadge, Surface,
} from '@/components/ds';
import { Avatar, Badge, DataTable, MetricStrip, StatusBadge, Surface } from '@/components/ds';
import { useApplications, useJobPostings, useStaff } from '@/lib/krowHooks';
import { DepartmentPerformance } from '@/components/charts/DepartmentPerformance';
import { HiringTrendChart } from '@/components/charts/HiringTrendChart';
import { AdminPage, SectionTitle } from '@/pages/admin/_shell';
/**
@@ -65,8 +63,6 @@ export default function AdminHiredHistory() {
return [...map.values()].map((e) => ({ ...e, avgScore: avg(e.scores) })).sort((a, b) => b.count - a.count);
}, [hires]);
const maxDept = Math.max(...byDepartment.map((d) => d.count), 1);
/**
* Outcomes per position: how many were hired, how fast, and how well they
* scored. This is the retrospective question Positions cannot answer, because
@@ -150,12 +146,13 @@ export default function AdminHiredHistory() {
<section aria-labelledby="trend" className="space-y-3">
<SectionTitle id="trend" title="Hiring trend" meta="Cumulative hires by month" />
<Surface variant="solid" radius="lg" padding="none" elevation="xs">
{/* A trend needs at least two points to be a trend. With one month of
hires a line chart draws a single dot in an empty grid, which reads
as broken rather than as "not enough history yet" — so say the
latter, and give the figure that one month does support. */}
{trend.length < 2 ? (
{/* A trend needs at least two points to be a trend. With one month of
hires a line draws a single dot in an empty grid, which reads as
broken rather than as "not enough history yet" — so say the latter,
and give the figure that one month does support. */}
<HiringTrendChart
points={trend}
emptyState={(
<div className="px-4 py-8 text-center">
<p className="font-heading text-body font-semibold text-ink-1">
Not enough history for a trend
@@ -166,46 +163,15 @@ export default function AdminHiredHistory() {
: 'No hires recorded yet. Hiring volume over time appears here once the first role closes.'}
</p>
</div>
) : (
<div className="px-2 py-4">
<ResponsiveContainer width="100%" height={200}>
<AreaChart data={trend}>
<defs>
<linearGradient id="hireFill" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor={CHART_TONES.brand} stopOpacity={0.22} />
<stop offset="100%" stopColor={CHART_TONES.brand} stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" stroke={CHART_TONES.grid} vertical={false} />
<XAxis dataKey="label" {...AXIS_PROPS} />
<YAxis {...AXIS_PROPS} allowDecimals={false} width={28} />
<Tooltip content={<ChartTooltip />} />
<Area type="monotone" dataKey="cumulative" name="Cumulative hires" stroke={CHART_TONES.brand} strokeWidth={2} fill="url(#hireFill)" />
</AreaChart>
</ResponsiveContainer>
</div>
)}
</Surface>
/>
</section>
<section aria-labelledby="dept" className="space-y-3">
<SectionTitle id="dept" title="Department breakdown" meta={`${byDepartment.length} departments`} />
<Surface variant="solid" radius="lg" padding="lg" elevation="xs">
<div className="space-y-3">
{byDepartment.map((d) => (
<div key={d.department}>
<div className="mb-1 flex items-baseline justify-between gap-2">
<span className="text-body-sm text-ink-2">{d.department}</span>
<span className="flex items-baseline gap-2.5">
{d.avgScore > 0 && <span className="text-[10px] text-ink-4">avg {d.avgScore}</span>}
<span className="text-body-sm font-bold tabular-nums text-ink-1">{d.count}</span>
</span>
</div>
<ProgressBar value={d.count} max={maxDept} size="sm" />
</div>
))}
</div>
</Surface>
<DepartmentPerformance
items={byDepartment.map((d) => ({ name: d.department, count: d.count, avgScore: d.avgScore }))}
/>
</section>
{/* Position outcomes — the retrospective read Positions cannot give. */}