update ui design for all pages

This commit is contained in:
2026-08-12 18:56:56 +05:30
parent 25c4ee4a6a
commit 3beb66059b
58 changed files with 7892 additions and 1891 deletions

View File

@@ -4,7 +4,9 @@ import {
Area, Bar, CartesianGrid, Cell, ComposedChart, Legend, Line, ResponsiveContainer,
Tooltip, XAxis, YAxis,
} from 'recharts';
import { ArrowRight, ChevronRight, Download, Plus } from 'lucide-react';
import {
AlertTriangle, ArrowRight, Building2, CalendarCheck, CheckCircle2, ChevronRight, Clock, Download, Filter, Plus, Sparkles, Users, Zap,
} from 'lucide-react';
import { cn } from '@/lib/utils';
import {
AXIS_PROPS, Avatar, Badge, Button, CHART_TONES, ChartTooltip, EmptyState, SegmentedToggle,
@@ -18,43 +20,16 @@ import { AdminPage } from '@/pages/admin/_shell';
/**
* Control Center — the KROW Admin command centre.
*
* Composed as one workspace rather than a grid of cards. Sections are separated by
* headings and rules, and a surface appears only where something genuinely needs
* lifting off the page: the metric strip, the two charts, the queue and the lists.
* That is the difference between a console and a dashboard — a dashboard frames
* every thought in its own box, and the boxes end up competing with the data.
*
* The reading order is deliberate, from state to action:
*
* snapshot → activity → where it leaks → which roles work
* → what to do → what is live → who is strongest → what just happened
*
* Every figure comes from the same fact sheet Owliver reads, so the page and the
* panel beside it cannot disagree. Nothing falls back to a placeholder number: an
* empty platform gets an empty state, because a console that invents "22
* candidates" to look populated is worse than one that admits it has none.
*/
/** Ranges for the activity chart. Local to that chart — see `activitySeries`. */
const RANGES = {
'7d': { label: '7D', days: 7, step: 1 },
'30d': { label: '30D', days: 30, step: 3 },
'90d': { label: '90D', days: 90, step: 9 },
};
/** Below this conversion, a stage transition is called out as the leak. */
const WEAK_TRANSITION = 60;
/* ── Section furniture ──────────────────────────────────────────────────── */
/**
* A section heading with an optional right-hand control.
*
* Local to this page rather than shared: the Control Center's sections carry chart
* controls and "view all" links in the same slot, and generalising that across the
* other Admin pages would mean a prop per case.
*/
function Band({ id, title, meta, children, action }) {
return (
<section aria-labelledby={id} className="space-y-3">
@@ -70,7 +45,6 @@ function Band({ id, title, meta, children, action }) {
);
}
/** The "view all" affordance, consistent across every section that has one. */
function ViewAll({ label, onClick }) {
return (
<button
@@ -87,53 +61,111 @@ function ViewAll({ label, onClick }) {
/* ── 1. Operations snapshot ─────────────────────────────────────────────── */
/**
* One grouped surface, six figures, hairline separators.
*
* Six cards would be six borders, six shadows and six landing points for the eye,
* all to carry six numbers that belong to a single thought: the current state of
* operations. As one banded strip it reads in a single pass.
*/
const snapshotConfig = {
'Open positions': {
icon: Building2,
bg: 'bg-indigo-50/80 dark:bg-indigo-950/50',
iconColor: 'text-indigo-600 dark:text-indigo-400',
border: 'border-indigo-200/50 dark:border-indigo-800/40 hover:border-indigo-400',
text: 'text-indigo-600 dark:text-indigo-400',
accent: 'from-indigo-500 to-blue-500',
},
Candidates: {
icon: Users,
bg: 'bg-blue-50/80 dark:bg-blue-950/50',
iconColor: 'text-blue-600 dark:text-blue-400',
border: 'border-blue-200/50 dark:border-blue-800/40 hover:border-blue-400',
text: 'text-blue-600 dark:text-blue-400',
accent: 'from-blue-500 to-cyan-500',
},
'In review': {
icon: Clock,
bg: 'bg-amber-50/80 dark:bg-amber-950/50',
iconColor: 'text-amber-600 dark:text-amber-400',
border: 'border-amber-200/50 dark:border-amber-800/40 hover:border-amber-400',
text: 'text-amber-600 dark:text-amber-400',
accent: 'from-amber-500 to-orange-500',
},
'AI screened': {
icon: Sparkles,
bg: 'bg-purple-50/80 dark:bg-purple-950/50',
iconColor: 'text-purple-600 dark:text-purple-400',
border: 'border-purple-200/50 dark:border-purple-800/40 hover:border-purple-400',
text: 'text-purple-600 dark:text-purple-400',
accent: 'from-purple-500 to-indigo-500',
},
Hired: {
icon: CheckCircle2,
bg: 'bg-emerald-50/80 dark:bg-emerald-950/50',
iconColor: 'text-emerald-600 dark:text-emerald-400',
border: 'border-emerald-200/50 dark:border-emerald-800/40 hover:border-emerald-400',
text: 'text-emerald-600 dark:text-emerald-400',
accent: 'from-emerald-500 to-teal-500',
},
'Avg KROW score': {
icon: Zap,
bg: 'bg-blue-50/80 dark:bg-blue-950/50',
iconColor: 'text-blue-600 dark:text-blue-400',
border: 'border-blue-200/50 dark:border-blue-800/40 hover:border-blue-400',
text: 'text-blue-600 dark:text-blue-400',
accent: 'from-blue-600 to-indigo-600',
},
};
function Snapshot({ metrics, loading }) {
return (
<div className="grid grid-cols-2 divide-x divide-y divide-border overflow-hidden rounded-xl border border-border bg-surface sm:grid-cols-3 sm:divide-y-0 xl:grid-cols-6">
{metrics.map((m) => (
<div key={m.label} className="min-w-0 px-4 py-3.5">
<p className="text-[11px] font-medium uppercase leading-tight tracking-wide text-ink-4">
{m.label}
</p>
{loading ? (
<div className="mt-1.5 h-7 w-14 animate-pulse rounded bg-surface-sunken" />
) : (
<p
className={cn(
'mt-1 font-heading text-title-lg font-bold leading-none tabular-nums',
m.tone === 'brand' ? 'text-krow-blue'
: m.tone === 'warning' ? 'text-warning'
: m.tone === 'success' ? 'text-success' : 'text-ink-1'
<div className="grid grid-cols-2 gap-3.5 sm:grid-cols-3 xl:grid-cols-6">
{metrics.map((m) => {
const cfg = snapshotConfig[m.label] || snapshotConfig['Candidates'];
const Icon = cfg.icon;
return (
<div
key={m.label}
className={cn(
'group relative flex flex-col justify-between rounded-xl border bg-surface p-4 shadow-xs backdrop-blur-xs transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md',
cfg.border
)}
>
<div>
<div className="flex items-center justify-between gap-1.5">
<span className="truncate text-[10px] font-bold uppercase tracking-wider text-ink-4">
{m.label}
</span>
<div className={cn('flex h-7 w-7 shrink-0 items-center justify-center rounded-lg shadow-xs', cfg.bg, cfg.iconColor)}>
<Icon className="h-3.5 w-3.5" />
</div>
</div>
{loading ? (
<div className="mt-2.5 h-8 w-16 animate-pulse rounded-lg bg-surface-sunken" />
) : (
<div className="mt-2 flex items-baseline gap-1.5">
<span className={cn('font-heading text-title-xl font-bold leading-none tabular-nums', cfg.text)}>
{m.value}
</span>
</div>
)}
>
{m.value}
</p>
)}
{m.sub && <p className="mt-1 truncate text-caption text-ink-3">{m.sub}</p>}
</div>
))}
</div>
<div className="mt-3 space-y-2">
{m.sub && (
<span className="inline-flex items-center rounded-full bg-surface-subtle px-2 py-0.5 text-[10px] font-semibold text-ink-3 border border-border/60">
{m.sub}
</span>
)}
<div className={cn('h-0.5 w-full rounded-full bg-gradient-to-r opacity-40 group-hover:opacity-100 transition-opacity', cfg.accent)} />
</div>
</div>
);
})}
</div>
);
}
/* ── 3. Pipeline intelligence ───────────────────────────────────────────── */
/**
* The funnel as an analytical strip: stage, count, conversion in, drop-off.
*
* Not progress bars in cards. The question here is never "how full is this stage"
* — it is "where are we losing people", which is a property of the *transitions*
* rather than the stages. So each row reports what it received, what it kept and
* what it lost, and the weakest transition is called out in the existing warning
* token so the leak is impossible to miss.
*/
function PipelineFunnel({ funnel, transitions, weakest, total }) {
if (!total) {
return (
@@ -144,75 +176,126 @@ function PipelineFunnel({ funnel, transitions, weakest, total }) {
);
}
const stageIcons = [Users, Sparkles, Filter, CalendarCheck, CheckCircle2];
return (
<div className="overflow-hidden rounded-xl border border-border bg-surface">
{funnel.map((stage, i) => {
const transition = i === 0 ? null : transitions[i - 1];
const isWeak = transition && weakest
&& transition.from === weakest.from && transition.to === weakest.to;
const share = (stage.count / total) * 100;
<div className="space-y-4">
{/* 5 Stage Funnel Cards */}
<div className="grid grid-cols-1 gap-3.5 sm:grid-cols-2 lg:grid-cols-5">
{funnel.map((stage, i) => {
const transition = i === 0 ? null : transitions[i - 1];
const isWeak = transition && weakest
&& transition.from === weakest.from && transition.to === weakest.to;
const share = Math.round((stage.count / total) * 100);
const StageIcon = stageIcons[i] || Users;
return (
<div
key={stage.key}
className={cn(
'flex items-center gap-4 border-b border-border px-4 py-3 last:border-0',
isWeak && 'bg-warning-muted'
)}
>
<div className="w-36 shrink-0">
<p className="text-body-sm font-medium text-ink-1">{stage.label}</p>
<p className="text-[10px] text-ink-4">{Math.round(share)}% of applied</p>
</div>
{/* The bar's width is this stage's share of the top of the funnel, so
the narrowing is literal rather than described. */}
<div className="min-w-0 flex-1">
<div className="h-2.5 overflow-hidden rounded-full bg-surface-sunken">
<div
className={cn('h-full rounded-full', isWeak ? 'bg-warning' : 'bg-krow-blue')}
style={{ width: `${Math.max(share, stage.count ? 3 : 0)}%` }}
/>
</div>
</div>
<p className="w-10 shrink-0 text-right font-heading text-body font-bold tabular-nums text-ink-1">
{stage.count}
</p>
<div className="w-24 shrink-0 text-right">
{transition ? (
<>
<p
return (
<div
key={stage.key}
className={cn(
'group relative flex flex-col justify-between rounded-xl border p-4 shadow-xs backdrop-blur-xs transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md',
isWeak
? 'border-amber-500/50 bg-gradient-to-br from-amber-50/70 via-surface to-amber-50/20 dark:from-amber-950/40 dark:to-surface'
: 'border-border/80 bg-surface hover:border-blue-500/40'
)}
>
<div>
{/* Header: Stage Name & Icon */}
<div className="flex items-center justify-between gap-1.5">
<div className="flex items-center gap-1.5 min-w-0">
<span className="text-[10px] font-extrabold text-ink-4 tabular-nums">0{i + 1}</span>
<h4 className="truncate font-heading text-body-sm font-bold text-ink-1">
{stage.label}
</h4>
</div>
<div
className={cn(
'text-body-sm font-semibold tabular-nums',
isWeak ? 'text-warning' : transition.rate >= 80 ? 'text-success' : 'text-ink-2'
'flex h-7 w-7 shrink-0 items-center justify-center rounded-lg shadow-xs',
isWeak
? 'bg-amber-500 text-white shadow-amber-500/25'
: i === funnel.length - 1
? 'bg-emerald-600 text-white shadow-emerald-500/25'
: 'bg-blue-50 text-blue-600 dark:bg-blue-950 dark:text-blue-400'
)}
>
{transition.rate}%
</p>
<p className="text-[10px] tabular-nums text-ink-4">
{transition.lost ? `−${transition.lost} dropped` : 'no drop-off'}
</p>
</>
) : (
<p className="text-[10px] text-ink-4">entry point</p>
)}
<StageIcon className="h-3.5 w-3.5" />
</div>
</div>
{/* Main Candidate Count & Pool Share */}
<div className="mt-3 flex items-baseline justify-between">
<span className="font-heading text-title-xl font-bold leading-none tabular-nums text-ink-1">
{stage.count}
</span>
<span
className={cn(
'rounded-full px-2 py-0.5 text-[10px] font-semibold tabular-nums border',
isWeak
? 'bg-amber-100 text-amber-800 border-amber-300 dark:bg-amber-950 dark:text-amber-300'
: 'bg-surface-subtle text-ink-3 border-border/60'
)}
>
{share}% pool
</span>
</div>
</div>
{/* Stage Fill Bar & Transition Metrics */}
<div className="mt-4 space-y-2">
<div className="h-1.5 w-full overflow-hidden rounded-full bg-surface-sunken">
<div
className={cn(
'h-full rounded-full transition-all duration-500',
isWeak
? 'bg-gradient-to-r from-amber-500 to-orange-500'
: i === funnel.length - 1
? 'bg-gradient-to-r from-emerald-500 to-teal-500'
: 'bg-gradient-to-r from-blue-600 to-indigo-600'
)}
style={{ width: `${Math.max(share, stage.count ? 5 : 0)}%` }}
/>
</div>
<div className="flex items-center justify-between text-[10px] text-ink-4 pt-0.5">
{transition ? (
<>
<span className={cn(isWeak ? 'font-bold text-amber-600 dark:text-amber-400' : 'text-ink-3')}>
{transition.rate}% pass
</span>
<span>{transition.lost ? `−${transition.lost} lost` : '0 lost'}</span>
</>
) : (
<span className="text-ink-4">Entry Stage</span>
)}
</div>
</div>
</div>
);
})}
</div>
{/* Primary Pipeline Bottleneck Alert Banner */}
{weakest && weakest.lost > 0 && (
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 rounded-xl border border-amber-500/40 bg-gradient-to-r from-amber-50/90 via-surface to-orange-50/40 dark:from-amber-950/40 dark:via-surface dark:to-orange-950/30 p-4 shadow-xs">
<div className="flex items-start gap-3">
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-amber-500 text-white shadow-md shadow-amber-500/20">
<AlertTriangle className="h-5 w-5" />
</div>
<div>
<div className="flex items-center gap-2">
<span className="text-[10px] font-bold uppercase tracking-wider text-amber-700 dark:text-amber-400">
Primary Pipeline Bottleneck
</span>
<span className="rounded-full bg-amber-200/60 dark:bg-amber-900/60 px-2 py-0.5 text-[10px] font-bold text-amber-800 dark:text-amber-300">
{weakest.from} → {weakest.to}
</span>
</div>
<p className="mt-0.5 text-body-sm text-ink-2">
Only <span className="font-bold text-amber-700 dark:text-amber-400">{weakest.rate}%</span> pass through and{' '}
<span className="font-bold text-ink-1">{weakest.lost} candidates</span> are lost at this transition. Recovering this conversion yields the highest ROI.
</p>
</div>
</div>
);
})}
{weakest && weakest.lost > 0 && (
<div className="border-t border-border bg-surface-subtle px-4 py-2.5">
<p className="text-caption leading-relaxed text-ink-2">
<span className="font-semibold text-ink-1">
{weakest.from} → {weakest.to}
</span>{' '}
is the leak: {weakest.rate}% pass through and {weakest.lost}{' '}
{weakest.lost === 1 ? 'candidate is' : 'candidates are'} lost. Every other
transition performs better, so this is the cheapest conversion to recover.
</p>
</div>
)}
</div>
@@ -497,7 +580,7 @@ export default function ControlCenter() {
subtitle="Workforce operations · hiring intelligence · platform health"
actions={
<>
<Button size="sm" shape="rounded" onClick={() => navigate('/positions/new')}>
<Button size="sm" shape="rounded" onClick={() => navigate('/admin/positions/new')}>
<Plus aria-hidden="true" /> Create Position
</Button>
<Button variant="outline" size="sm" shape="rounded" onClick={exportSnapshot}>