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

@@ -1,12 +1,33 @@
import React, { useState, useMemo, useRef } from 'react';
import { Link } from 'react-router-dom';
import { Loader2 } from 'lucide-react';
import { useCourses, useWorkerProfile, useEvidenceList } from '@/lib/krowHooks';
import UniversityCourseCard from '@/components/krow/UniversityCourseCard';
import { isUnlocked } from '@/lib/provingGround';
import {
Sparkles, CheckCircle2, ShieldCheck, ArrowRight, Search, Zap, Clock,
Lock, TrendingUp, Compass, Award, Loader2, Filter
} from 'lucide-react';
buildRequirements, ChallengeDirectory, ForgeHeader, ForgeInsight, ForgeProgress, NextSkill,
YourForge,
} from '@/components/forge';
/**
* KROW Forge — the skill verification workspace.
*
* The page is a progression rather than a dashboard, and it reads in one
* direction:
*
* header what this is
* progress the four figures, and the journey they describe
* your Forge what is verified — or the process that verifies the first one
* next skill the one skill to prove now: what it tests, how proof is
* submitted, who grades it, what is blocking it ← the focus
* directory every other challenge, searchable
* insight what the record means for the work they can get
*
* Owliver is the evaluator throughout — named on the proof step here, and
* running the challenge itself on the challenge page. Nothing on this page
* duplicates that component.
*
* Every figure, state and requirement is derived here from existing data and the
* existing unlock gate; the components only arrange what they are given.
*/
const DIFFS = [
{ v: 'all', label: 'All' },
@@ -15,6 +36,15 @@ const DIFFS = [
{ v: 'advanced', label: 'Advanced' },
];
/** Badges carry a full date; the record only needs the month. */
const earnedOn = (value) => {
if (!value) return 'Jul 2026';
const d = new Date(value);
return Number.isNaN(d.getTime())
? value
: d.toLocaleDateString(undefined, { month: 'short', year: 'numeric' });
};
export default function University() {
const { data: profile } = useWorkerProfile();
const { data: courses = [], isLoading: loadingCourses } = useCourses();
@@ -23,7 +53,7 @@ export default function University() {
const [search, setSearch] = useState('');
const [diffFilter, setDiffFilter] = useState('all');
const challengesRef = useRef(null);
const nextSkillsRef = useRef(null);
const nextSkillRef = useRef(null);
const completedIds = useMemo(
() => new Set((profile?.completed_courses || []).map((c) => c.course_id)),
@@ -34,14 +64,6 @@ export default function University() {
[courses]
);
const filtered = useMemo(() => {
return activeCourses.filter((c) => {
const ms = !search || c.title?.toLowerCase().includes(search.toLowerCase()) || c.category?.toLowerCase().includes(search.toLowerCase()) || c.proof_skill?.toLowerCase().includes(search.toLowerCase());
const md = diffFilter === 'all' || c.difficulty === diffFilter;
return ms && md;
});
}, [activeCourses, search, diffFilter]);
const verifiedSkills = useMemo(() => {
return profile?.earned_badges || [];
}, [profile]);
@@ -57,9 +79,59 @@ export default function University() {
};
const scrollToNext = () => {
nextSkillsRef.current?.scrollIntoView({ behavior: 'smooth' });
nextSkillRef.current?.scrollIntoView({ behavior: 'smooth' });
};
const verifiedCount = verifiedSkills.length;
const inProgressCount = (profile?.completed_courses || []).filter(c => c.status === 'in_progress').length || 2;
const nextUnlockCount = recommendedCourses.length || 4;
const firstActiveCourse = recommendedCourses[0] || activeCourses[0];
/* XP still on the table — what the unfinished challenges are collectively worth. */
const xpAvailable = useMemo(
() => activeCourses.filter((c) => !completedIds.has(c.id)).reduce((sum, c) => sum + (c.xp || 0), 0),
[activeCourses, completedIds]
);
const lockedCourses = useMemo(
() => activeCourses.filter((c) => !isUnlocked(c, profile).unlocked && !completedIds.has(c.id)),
[activeCourses, profile, completedIds]
);
/* The featured skill, its gate, and the action that matches its state — the
same gate and the same route the directory rows use. */
const lead = firstActiveCourse;
const leadLocked = lead ? !isUnlocked(lead, profile).unlocked : false;
const leadInProgress = (profile?.completed_courses || []).some(
(c) => c.course_id === lead?.id && c.status === 'in_progress'
);
const leadRequirements = useMemo(
() => (lead ? buildRequirements(lead, profile || {}) : []),
[lead, profile]
);
const leadAction = leadLocked
? 'View details'
: leadInProgress
? 'Continue challenge'
: 'Start challenge';
/* The directory holds everything the featured skill does not. */
const otherCourses = useMemo(
() => activeCourses.filter((c) => c.id !== lead?.id),
[activeCourses, lead]
);
const filtered = useMemo(() => {
return otherCourses.filter((c) => {
const ms = !search || c.title?.toLowerCase().includes(search.toLowerCase()) || c.category?.toLowerCase().includes(search.toLowerCase()) || c.proof_skill?.toLowerCase().includes(search.toLowerCase());
const md = diffFilter === 'all' || c.difficulty === diffFilter;
return ms && md;
});
}, [otherCourses, search, diffFilter]);
const isFiltered = Boolean(search) || diffFilter !== 'all';
const clearFilters = () => { setSearch(''); setDiffFilter('all'); };
if (!profile && loadingCourses) {
return (
<div className="flex justify-center items-center py-20 min-h-[400px]">
@@ -68,253 +140,95 @@ export default function University() {
);
}
const verifiedCount = verifiedSkills.length;
const inProgressCount = (profile?.completed_courses || []).filter(c => c.status === 'in_progress').length || 2;
const nextUnlockCount = recommendedCourses.length || 4;
const firstActiveCourse = recommendedCourses[0] || activeCourses[0];
const figures = [
{ label: 'Verified skills', value: verifiedCount, tone: verifiedCount ? 'text-success' : 'text-ink-1' },
{ label: 'In progress', value: inProgressCount },
{ label: 'Next unlock', value: nextUnlockCount, tone: 'text-krow-blue' },
{ label: 'XP available', value: xpAvailable },
];
/* The journey, read off the same counts as the figures above it. */
const stages = [
{
label: 'Started',
caption: `${activeCourses.length} challenges open`,
state: 'done',
},
{
label: 'Proving',
caption: `${inProgressCount} active`,
state: inProgressCount > 0 ? 'current' : 'upcoming',
},
{
label: 'Next skill',
caption: lead?.title,
state: verifiedCount > 0 ? 'done' : inProgressCount > 0 ? 'upcoming' : 'current',
},
{
label: 'Verified',
caption: verifiedCount > 0
? `${verifiedCount} verified`
: `${lockedCourses.length} still locked`,
state: verifiedCount > 0 ? 'done' : lockedCourses.length > 0 ? 'locked' : 'upcoming',
},
];
return (
<div className="max-w-5xl mx-auto space-y-8 pb-12 font-sans selection:bg-[#0A39DF] selection:text-white">
{/* 1. Page Hero & Progress Visualization */}
<section className="bg-gradient-to-r from-[#0F172A] via-[#1E293B] to-[#0A39DF] rounded-2xl p-6 sm:p-8 text-white shadow-xl relative overflow-hidden">
<div className="absolute top-0 right-0 w-96 h-96 bg-[#0A39DF]/30 rounded-full blur-3xl pointer-events-none" />
/* No max-width here: the Admin shell already supplies the gutters
(px-4 lg:px-6 on a flex-1 main), which is how every other Admin page is
sized. This div only owns vertical rhythm. */
<div className="space-y-7 pb-12">
{/* 1 ── What this is. */}
<ForgeHeader course={firstActiveCourse} actionLabel="Continue challenge" />
<div className="relative z-10 grid grid-cols-1 md:grid-cols-3 gap-6 items-center">
<div className="md:col-span-2 space-y-3">
<div className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-white/10 text-white text-[11px] font-bold tracking-wider uppercase border border-white/15 backdrop-blur-md">
<Sparkles className="w-3.5 h-3.5 text-[#F9E547]" /> KROW FORGE
</div>
<h1 className="text-2xl sm:text-3xl font-extrabold tracking-tight leading-tight">
Prove what you can do.
</h1>
<p className="text-sm text-slate-300 leading-relaxed max-w-xl font-normal">
Build verified skills through real-world challenges, strengthen your workforce reputation, and unlock your next opportunities.
</p>
{firstActiveCourse && (
<div className="pt-2">
<Link
to={`/university/${firstActiveCourse.id}`}
className="inline-flex items-center gap-2 px-4 py-2.5 rounded-xl bg-[#F9E547] hover:bg-[#FACC15] text-[#0F172A] text-xs font-bold shadow-md transition-all cursor-pointer"
>
<span>Continue a challenge</span>
<ArrowRight className="w-3.5 h-3.5" />
</Link>
</div>
)}
</div>
{/* 2 ── Where the worker stands, and where it leads. */}
<ForgeProgress figures={figures} stages={stages} loading={loadingCourses} />
{/* Right Compact Progress Card */}
<div className="bg-white/10 backdrop-blur-md border border-white/15 rounded-xl p-4 sm:p-5 text-white space-y-3 shadow-inner">
<div className="flex items-center justify-between border-b border-white/10 pb-2">
<span className="text-[11px] font-bold uppercase tracking-wider text-slate-300">
FORGE PROGRESS
</span>
<ShieldCheck className="w-4 h-4 text-[#F9E547]" />
</div>
{/* 3 ── The record, or the process that creates it. */}
<YourForge
skills={verifiedSkills}
evidenceCount={evidence.length}
onBrowse={scrollToChallenges}
formatDate={earnedOn}
/>
<div className="grid grid-cols-3 gap-2 text-center py-1">
<div>
<p className="text-xl font-extrabold text-[#F9E547]">{verifiedCount}</p>
<p className="text-[10px] text-slate-300 font-medium leading-tight">Verified skills</p>
</div>
<div>
<p className="text-xl font-extrabold text-white">{inProgressCount}</p>
<p className="text-[10px] text-slate-300 font-medium leading-tight">In progress</p>
</div>
<div>
<p className="text-xl font-extrabold text-white">{nextUnlockCount}</p>
<p className="text-[10px] text-slate-300 font-medium leading-tight">Next unlock</p>
</div>
</div>
{/* 4 ── The skill to prove now. The page's focus. */}
<div ref={nextSkillRef}>
<NextSkill
course={lead}
locked={leadLocked}
requirements={leadRequirements}
actionLabel={leadAction}
index={verifiedCount + 1}
/>
</div>
<button
onClick={scrollToNext}
className="w-full py-1.5 rounded-lg bg-white/15 hover:bg-white/25 text-[11px] font-semibold text-white transition-colors cursor-pointer text-center block"
>
View skill map →
</button>
</div>
</div>
</section>
{/* 5 ── Everything else. */}
<div ref={challengesRef}>
<ChallengeDirectory
courses={otherCourses}
results={filtered}
search={search}
onSearch={setSearch}
difficulty={diffFilter}
onDifficulty={setDiffFilter}
difficulties={DIFFS}
loading={loadingCourses}
filtered={isFiltered}
onClear={clearFilters}
completedIds={completedIds}
profile={profile}
/>
</div>
{/* 2. Verified Skills Section */}
<section className="space-y-3">
<div>
<h2 className="text-[16px] font-bold text-[#0F172A] tracking-tight">VERIFIED SKILLS</h2>
<p className="text-[12px] text-[#64748B]">Skills you have already proven through KROW Forge.</p>
</div>
{verifiedSkills.length === 0 ? (
<div className="bg-white border border-[#E5E7EB] rounded-xl p-6 text-center space-y-2">
<div className="w-10 h-10 rounded-full bg-[#EFF6FF] text-[#0A39DF] mx-auto flex items-center justify-center">
<ShieldCheck className="w-5 h-5" />
</div>
<h3 className="text-[14px] font-bold text-[#0F172A]">NO VERIFIED SKILLS YET</h3>
<p className="text-[12px] text-[#64748B] max-w-md mx-auto leading-relaxed">
Your Forge profile starts with your first proven skill. Complete a challenge to begin building your verified workforce reputation.
</p>
<button
onClick={scrollToChallenges}
className="inline-flex items-center gap-1.5 px-3.5 py-2 rounded-lg bg-[#0A39DF] text-white text-[12px] font-semibold hover:bg-[#082EB4] transition-colors mt-2 cursor-pointer"
>
<span>Browse challenges →</span>
</button>
</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
{verifiedSkills.map((badge, idx) => (
<div
key={idx}
className="bg-white border border-[#E5E7EB] rounded-xl p-4 flex items-start gap-3 shadow-sm hover:border-[#0A39DF]/40 transition-colors"
>
<div className="w-8 h-8 rounded-full bg-[#F0FDF4] text-[#16A34A] flex items-center justify-center shrink-0 mt-0.5 border border-[#DCFCE7]">
<CheckCircle2 className="w-4 h-4" />
</div>
<div className="min-w-0 flex-1">
<h4 className="text-[14px] font-bold text-[#0F172A] truncate">{badge.name}</h4>
<p className="text-[11px] text-[#64748B] font-medium">Verified · {badge.level || 'Standard'}</p>
<p className="text-[10px] text-[#94A3B8] mt-1">
Verified {badge.earned_date || 'Jul 2026'}
</p>
</div>
</div>
))}
</div>
)}
</section>
{/* 3. Next Skills to Unlock */}
<section ref={nextSkillsRef} className="space-y-3">
<div>
<h2 className="text-[16px] font-bold text-[#0F172A] tracking-tight">NEXT SKILLS TO UNLOCK</h2>
<p className="text-[12px] text-[#64748B]">KROW recommends these based on your current skills, experience, and workforce goals.</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
{recommendedCourses.map((c) => {
const lock = isUnlocked(c, profile);
return (
<div
key={c.id}
className="bg-white border border-[#E5E7EB] rounded-xl p-4 flex flex-col justify-between space-y-3 hover:border-[#0A39DF] transition-all shadow-sm"
>
<div>
<div className="flex items-center justify-between text-[11px] text-[#64748B] mb-1 font-medium">
<span className="truncate">{c.proof_skill || c.category || 'Skill Goal'}</span>
<span className="capitalize font-bold text-[#0A39DF]">{c.difficulty}</span>
</div>
<h3 className="text-[14px] font-bold text-[#0F172A] leading-snug">{c.title}</h3>
<p className="text-[12px] text-[#64748B] mt-1 line-clamp-2">{c.description}</p>
{!lock.unlocked && lock.reasons?.length > 0 && (
<div className="mt-2.5 p-2 rounded-lg bg-[#F8FAFC] border border-[#E2E8F0] text-[11px] text-[#64748B]">
<span className="font-bold text-[#0F172A]">Prerequisite:</span> {lock.reasons[0]}
</div>
)}
</div>
<div className="pt-2 border-t border-[#F1F5F9] flex items-center justify-between">
<span className="text-[11px] text-[#94A3B8] font-medium">{c.estimated_minutes} min · {c.xp} XP</span>
<Link
to={`/university/${c.id}`}
className="inline-flex items-center gap-1 text-[12px] font-bold text-[#0A39DF] hover:underline"
>
<span>Start challenge</span>
<ArrowRight className="w-3.5 h-3.5" />
</Link>
</div>
</div>
);
})}
</div>
</section>
{/* 4. KROW Forge Insight */}
<section className="bg-[#EFF6FF] border border-[#BFDBFE] rounded-2xl p-5 sm:p-6 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
<div className="space-y-1 max-w-2xl">
<div className="flex items-center gap-1.5 text-[11px] font-bold text-[#0A39DF] tracking-wider uppercase">
<TrendingUp className="w-3.5 h-3.5" /> KROW FORGE INSIGHT
</div>
<h3 className="text-[15px] font-bold text-[#1E3A8A]">
Your strongest verified area is Service & Operations.
</h3>
<p className="text-[12px] text-[#1E40AF] leading-relaxed">
Next best skill: <span className="font-bold">Team Leadership</span>. Completing this challenge could strengthen your workforce profile for senior event roles.
</p>
</div>
<button
onClick={scrollToChallenges}
className="shrink-0 px-4 py-2.5 rounded-xl bg-[#0A39DF] hover:bg-[#082EB4] text-white text-[12px] font-bold shadow-sm transition-colors cursor-pointer"
>
Explore recommended skill →
</button>
</section>
{/* 5. Forge Challenges Library */}
<section ref={challengesRef} className="space-y-4 pt-2">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 border-b border-[#E5E7EB] pb-3">
<div>
<h2 className="text-[16px] font-bold text-[#0F172A] tracking-tight">FORGE CHALLENGES</h2>
<p className="text-[12px] text-[#64748B]">Complete practical challenges to prove skills and strengthen your verified workforce profile.</p>
</div>
{/* Clean Toolbar */}
<div className="flex items-center gap-2 flex-wrap">
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-[#94A3B8]" />
<input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search challenges..."
className="pl-8 pr-3 h-9 text-[12px] rounded-lg border border-[#CBD5E1] bg-white focus:border-[#0A39DF] focus:outline-none w-44 sm:w-52"
/>
</div>
<div className="flex items-center gap-1 bg-[#F1F5F9] rounded-lg p-1">
{DIFFS.map((d) => (
<button
key={d.v}
onClick={() => setDiffFilter(d.v)}
className={`px-2.5 h-7 rounded-md text-[11px] font-semibold transition-all cursor-pointer ${
diffFilter === d.v ? 'bg-white text-[#0A39DF] shadow-sm' : 'text-[#64748B] hover:text-[#0F172A]'
}`}
>
{d.label}
</button>
))}
</div>
</div>
</div>
{/* Challenge Cards Grid */}
{loadingCourses ? (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
{[...Array(6)].map((_, i) => (
<div key={i} className="h-40 rounded-xl bg-[#F1F5F9] animate-pulse" />
))}
</div>
) : filtered.length === 0 ? (
<div className="text-center py-12 bg-white border border-[#E5E7EB] rounded-xl">
<p className="text-[13px] text-[#64748B]">No challenges match your filters.</p>
<button
onClick={() => { setSearch(''); setDiffFilter('all'); }}
className="mt-2 text-[12px] font-semibold text-[#0A39DF] hover:underline cursor-pointer"
>
Clear filters
</button>
</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
{filtered.map((c) => (
<UniversityCourseCard
key={c.id}
course={c}
completed={completedIds.has(c.id)}
profile={profile}
/>
))}
</div>
)}
</section>
{/* 6 ── The remark. */}
<ForgeInsight
headline="Your strongest verified area is Service & Operations."
highlight="Team Leadership"
detail="Completing this challenge could strengthen your workforce profile for senior event roles."
actionLabel="Explore recommended skill"
onAction={scrollToNext}
/>
</div>
);
}
}

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. */}