update hired page
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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. */}
|
||||
|
||||
Reference in New Issue
Block a user