import React, { useMemo } from 'react'; import { Activity, Award, Clock, Gauge, Lightbulb, Target, TrendingUp, TriangleAlert, } from 'lucide-react'; import { cn } from '@/lib/utils'; import { Badge, MetricStrip, Surface } from '@/components/ds'; import { useApplications, useJobPostings, useStaff } from '@/lib/krowHooks'; import { DepartmentPerformance } from '@/components/charts/DepartmentPerformance'; import { HiringFlow } from '@/components/charts/HiringFlow'; import { HiringTrendChart } from '@/components/charts/HiringTrendChart'; import { useSize } from '@/hooks/use-size'; import { AdminPage, SectionTitle } from '@/pages/admin/_shell'; import { SkillSurface } from '@/components/skills/SkillSurface'; import { buildEfficiency, buildFunnel, buildInsights, buildTrend, byDepartment, byPosition, hiresWithFill, summarise, } from '@/lib/hiringRecords'; /** * Admin Analytics — how hiring is performing. * * This page and Hired History used to be the same component rendered twice with * a different title, which is why they read as duplicates: identical sections, * identical tabs, identical figures. They are not the same question. * * **Analytics asks "how is our hiring performing?"** — it is metrics, trends, * comparison and what to do about them. Every section here is an aggregate: a * rate, a distribution, a shape over time. No individual hire is named anywhere * on this page, because a name is a record and records are Hired History's job. * * It is also structurally different, not just differently coloured. Analytics is * one scrolling dashboard — the seven readings are meant to be taken together, * and a comparison you have to remember across a tab switch is not a comparison. * Hired History is a filtered record you search, so it keeps its controls and its * table. Same design system, same blue, same cards; different jobs. * * Both read `lib/hiringRecords.js`, so a total here and the same total there * cannot disagree. */ const INSIGHT_TONE = { success: { ring: 'border-emerald-500/30 bg-emerald-50/40 dark:bg-emerald-950/20', dot: 'text-emerald-600 dark:text-emerald-400', Icon: TrendingUp }, warning: { ring: 'border-amber-500/30 bg-amber-50/40 dark:bg-amber-950/20', dot: 'text-amber-600 dark:text-amber-400', Icon: TriangleAlert }, risk: { ring: 'border-red-500/30 bg-red-50/40 dark:bg-red-950/20', dot: 'text-red-600 dark:text-red-400', Icon: TriangleAlert }, info: { ring: 'border-border bg-surface', dot: 'text-krow-blue', Icon: Lightbulb }, }; /** One finding, with the evidence underneath it. */ function Insight({ item }) { const tone = INSIGHT_TONE[item.tone] || INSIGHT_TONE.info; const { Icon } = tone; return (
{item.title}
{item.body}
No role has enough dated hires to measure velocity yet.
); } return ({role.role}
{role.count} hire{role.count === 1 ? '' : 's'}
No applications on file yet. The funnel appears once the first candidate applies.
Not enough history for a trend
{hires.length ? `All ${hires.length} hire${hires.length === 1 ? '' : 's'} closed in ${trend[0]?.label || 'a single month'}. A month-on-month line appears once hiring spans a second month.` : 'No hires recorded yet. Hiring volume over time appears here once the first role closes.'}
)} />| {h} | ))}|||||
|---|---|---|---|---|---|
| {r.role} | {r.count} | {r.avgScore || '—'} | {r.avgDays ? `${r.avgDays}d` : '—'} | {r.rated}/{r.count} |
{r.avgRating != null
? |
{efficiency.median ? `${efficiency.median}d` : '—'} median
{efficiency.within48h ? `${efficiency.within48h}% of roles close within 48 hours.` : 'Velocity appears once hires carry an application date.'}
There is not enough hiring on file to draw a finding from yet. Insights appear as applications, hires and reviews accumulate.