update ui design for all pages
This commit is contained in:
@@ -1,93 +1,617 @@
|
||||
import React from 'react';
|
||||
import { ScatterChart } from '@mui/x-charts';
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import {
|
||||
Trophy,
|
||||
Sparkles,
|
||||
Workflow,
|
||||
Table as TableIcon,
|
||||
Users,
|
||||
Clock,
|
||||
ArrowRight,
|
||||
X,
|
||||
ChefHat,
|
||||
ShieldCheck,
|
||||
Wine,
|
||||
Building2,
|
||||
} from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { chartSx, TONES, tooltipSx } from './muiChartTheme';
|
||||
|
||||
/**
|
||||
* Department performance — a dot plot, not a bar race.
|
||||
* Animated Flowchart & Department Performance Component
|
||||
*
|
||||
* The bars this replaces encoded hire *count*, which on a dataset where every
|
||||
* department has hired once drew three identical full-width rectangles. Encoding
|
||||
* average score instead does not fix it either: 92, 94 and 97 against a
|
||||
* zero-baseline axis are three more identical bars.
|
||||
*
|
||||
* So the scores are plotted as points on a focused axis. The domain starts below
|
||||
* the lowest score rather than at zero — legitimate here because the axis ticks
|
||||
* are drawn and read, and because the question is "which department hires
|
||||
* better", which is a comparison between 90s, not a comparison against nothing.
|
||||
*
|
||||
* Volume is stated in the list underneath: one hire per department is a fact,
|
||||
* not a shape.
|
||||
*
|
||||
* `items: [{ name, count, avgScore }]` — supplied by the page.
|
||||
* Renders an interactive, animated flow chart mapping department performance,
|
||||
* AI scores, and hiring outcomes connected via glowing SVG flow paths.
|
||||
*/
|
||||
|
||||
// Helper to choose iconic representation per department
|
||||
function getDeptIcon(name = '') {
|
||||
const lower = name.toLowerCase();
|
||||
if (lower.includes('chef') || lower.includes('kitchen') || lower.includes('cook')) return ChefHat;
|
||||
if (lower.includes('security') || lower.includes('guard') || lower.includes('safety')) return ShieldCheck;
|
||||
if (lower.includes('bar') || lower.includes('drink') || lower.includes('mixologist')) return Wine;
|
||||
if (lower.includes('front') || lower.includes('desk') || lower.includes('concierge')) return Building2;
|
||||
if (lower.includes('event') || lower.includes('manager')) return Trophy;
|
||||
if (lower.includes('housekeeping') || lower.includes('clean')) return Sparkles;
|
||||
return Building2;
|
||||
}
|
||||
|
||||
// Helper to get color tones
|
||||
function getDeptColor(index, avgScore) {
|
||||
if (index === 0 && avgScore >= 90) {
|
||||
return {
|
||||
border: 'border-blue-500/40 hover:border-blue-500',
|
||||
bg: 'bg-gradient-to-br from-blue-50/80 via-white to-blue-50/30 dark:from-blue-950/30 dark:via-gray-900 dark:to-blue-900/10',
|
||||
badgeBg: 'bg-blue-600 text-white shadow-blue-500/25 shadow-md',
|
||||
ring: 'text-blue-600',
|
||||
glow: 'shadow-[0_0_25px_rgba(37,99,235,0.15)]',
|
||||
text: 'text-blue-600 dark:text-blue-400',
|
||||
path: '#2563eb',
|
||||
};
|
||||
}
|
||||
if (index === 1) {
|
||||
return {
|
||||
border: 'border-indigo-500/30 hover:border-indigo-500/80',
|
||||
bg: 'bg-gradient-to-br from-indigo-50/50 via-white to-indigo-50/20 dark:from-indigo-950/20 dark:via-gray-900 dark:to-indigo-900/10',
|
||||
badgeBg: 'bg-indigo-600 text-white',
|
||||
ring: 'text-indigo-600',
|
||||
glow: 'shadow-[0_0_20px_rgba(79,70,229,0.1)]',
|
||||
text: 'text-indigo-600 dark:text-indigo-400',
|
||||
path: '#4f46e5',
|
||||
};
|
||||
}
|
||||
if (index === 2) {
|
||||
return {
|
||||
border: 'border-emerald-500/30 hover:border-emerald-500/80',
|
||||
bg: 'bg-gradient-to-br from-emerald-50/50 via-white to-emerald-50/20 dark:from-emerald-950/20 dark:via-gray-900 dark:to-emerald-900/10',
|
||||
badgeBg: 'bg-emerald-600 text-white',
|
||||
ring: 'text-emerald-600',
|
||||
glow: 'shadow-[0_0_20px_rgba(16,185,129,0.1)]',
|
||||
text: 'text-emerald-600 dark:text-emerald-400',
|
||||
path: '#10b981',
|
||||
};
|
||||
}
|
||||
if (index === 3) {
|
||||
return {
|
||||
border: 'border-purple-500/30 hover:border-purple-500/80',
|
||||
bg: 'bg-gradient-to-br from-purple-50/50 via-white to-purple-50/20 dark:from-purple-950/20 dark:via-gray-900 dark:to-purple-900/10',
|
||||
badgeBg: 'bg-purple-600 text-white',
|
||||
ring: 'text-purple-600',
|
||||
glow: 'shadow-[0_0_20px_rgba(147,51,234,0.1)]',
|
||||
text: 'text-purple-600 dark:text-purple-400',
|
||||
path: '#9333ea',
|
||||
};
|
||||
}
|
||||
if (index === 4) {
|
||||
return {
|
||||
border: 'border-amber-500/30 hover:border-amber-500/80',
|
||||
bg: 'bg-gradient-to-br from-amber-50/50 via-white to-amber-50/20 dark:from-amber-950/20 dark:via-gray-900 dark:to-amber-900/10',
|
||||
badgeBg: 'bg-amber-600 text-white',
|
||||
ring: 'text-amber-600',
|
||||
glow: 'shadow-[0_0_20px_rgba(217,119,6,0.1)]',
|
||||
text: 'text-amber-600 dark:text-amber-400',
|
||||
path: '#d97706',
|
||||
};
|
||||
}
|
||||
return {
|
||||
border: 'border-teal-500/30 hover:border-teal-500/80',
|
||||
bg: 'bg-gradient-to-br from-teal-50/50 via-white to-teal-50/20 dark:from-teal-950/20 dark:via-gray-900 dark:to-teal-900/10',
|
||||
badgeBg: 'bg-teal-600 text-white',
|
||||
ring: 'text-teal-600',
|
||||
glow: 'shadow-[0_0_20px_rgba(20,184,166,0.1)]',
|
||||
text: 'text-teal-600 dark:text-teal-400',
|
||||
path: '#14b8a6',
|
||||
};
|
||||
}
|
||||
|
||||
export function DepartmentPerformance({ items = [], className }) {
|
||||
if (!items.length) return null;
|
||||
const [viewMode, setViewMode] = useState('flowchart'); // 'flowchart' | 'table'
|
||||
const [activeDept, setActiveDept] = useState(null);
|
||||
|
||||
const scored = items.filter((d) => d.avgScore > 0);
|
||||
/* Highest at the top: MUI plots ascending y upward, so rank ascending. */
|
||||
const plotted = [...scored].sort((a, b) => (a.avgScore || 0) - (b.avgScore || 0));
|
||||
// Rank departments by avgScore descending
|
||||
const ranked = useMemo(() => {
|
||||
return [...items].sort(
|
||||
(a, b) => (b.avgScore || 0) - (a.avgScore || 0) || (b.count || 0) - (a.count || 0)
|
||||
);
|
||||
}, [items]);
|
||||
|
||||
const lowest = Math.min(...plotted.map((d) => d.avgScore));
|
||||
const highest = Math.max(...plotted.map((d) => d.avgScore));
|
||||
/* A little air either side, snapped to 5s, so the ticks stay round numbers. */
|
||||
const min = Math.max(0, Math.floor((lowest - 6) / 5) * 5);
|
||||
const max = Math.min(100, Math.ceil((highest + 3) / 5) * 5);
|
||||
const totalHires = useMemo(() => items.reduce((acc, curr) => acc + (curr.count || 0), 0), [items]);
|
||||
const overallAvgScore = useMemo(() => {
|
||||
const scored = items.filter((i) => i.avgScore > 0);
|
||||
if (!scored.length) return 0;
|
||||
return Math.round(scored.reduce((acc, curr) => acc + curr.avgScore, 0) / scored.length);
|
||||
}, [items]);
|
||||
|
||||
if (!ranked.length) return null;
|
||||
|
||||
return (
|
||||
<div className={cn('overflow-hidden rounded-xl border border-border bg-surface', className)}>
|
||||
{plotted.length > 0 ? (
|
||||
<div className="px-2 pt-4 sm:px-3">
|
||||
<ScatterChart
|
||||
height={Math.max(132, plotted.length * 40 + 52)}
|
||||
margin={{ top: 8, right: 24, bottom: 28, left: 8 }}
|
||||
xAxis={[{
|
||||
min,
|
||||
max,
|
||||
tickNumber: 4,
|
||||
valueFormatter: (v) => `${v}`,
|
||||
}]}
|
||||
yAxis={[{
|
||||
min: -0.6,
|
||||
max: plotted.length - 0.4,
|
||||
tickInterval: plotted.map((_, i) => i),
|
||||
valueFormatter: (v) => plotted[Math.round(v)]?.name ?? '',
|
||||
width: 96,
|
||||
}]}
|
||||
series={[{
|
||||
label: 'Average score',
|
||||
data: plotted.map((d, i) => ({ x: d.avgScore, y: i, id: d.name })),
|
||||
markerSize: 7,
|
||||
valueFormatter: (point) => {
|
||||
const d = plotted[Math.round(point.y)];
|
||||
return `${d.name} · avg ${d.avgScore} · ${d.count} ${d.count === 1 ? 'hire' : 'hires'}`;
|
||||
},
|
||||
}]}
|
||||
colors={[TONES.brand]}
|
||||
grid={{ vertical: true }}
|
||||
skipAnimation
|
||||
sx={{ ...chartSx, ...tooltipSx }}
|
||||
/>
|
||||
<div className={cn('overflow-hidden rounded-2xl border border-border bg-surface shadow-sm transition-all', className)}>
|
||||
{/* Header Bar & View Switcher */}
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-border px-4 py-3.5 sm:px-6">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-blue-50 text-blue-600 dark:bg-blue-950/50 dark:text-blue-400">
|
||||
<Workflow className="h-4 w-4" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-heading text-body font-semibold text-ink-1">
|
||||
Department Performance & Hiring Flow
|
||||
</h3>
|
||||
<p className="text-caption text-ink-3">
|
||||
Interactive pipeline mapping AI score quality & hire velocity across departments
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="px-4 py-6 text-center text-body-sm text-ink-3">
|
||||
No scores recorded against these hires yet.
|
||||
</p>
|
||||
)}
|
||||
|
||||
<ul className="divide-y divide-border border-t border-border">
|
||||
{[...items].sort((a, b) => (b.avgScore || 0) - (a.avgScore || 0)).map((d) => (
|
||||
<li key={d.name} className="flex items-baseline justify-between gap-3 px-4 py-2">
|
||||
<span className="truncate text-body-sm text-ink-2">{d.name}</span>
|
||||
<span className="flex shrink-0 items-baseline gap-4 text-caption tabular-nums text-ink-4">
|
||||
<span>{d.count} {d.count === 1 ? 'hire' : 'hires'}</span>
|
||||
<span className="w-16 text-right font-semibold text-ink-1">
|
||||
{d.avgScore ? `avg ${d.avgScore}` : '—'}
|
||||
</span>
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{/* View Mode Toggle Switch */}
|
||||
<div className="flex items-center rounded-xl bg-surface-subtle p-1 border border-border/60">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setViewMode('flowchart')}
|
||||
className={cn(
|
||||
'flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-caption font-medium transition-all',
|
||||
viewMode === 'flowchart'
|
||||
? 'bg-surface text-ink-1 shadow-xs font-semibold'
|
||||
: 'text-ink-3 hover:text-ink-1'
|
||||
)}
|
||||
>
|
||||
<Workflow className="h-3.5 w-3.5 text-blue-600" />
|
||||
<span>Flowchart</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setViewMode('table')}
|
||||
className={cn(
|
||||
'flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-caption font-medium transition-all',
|
||||
viewMode === 'table'
|
||||
? 'bg-surface text-ink-1 shadow-xs font-semibold'
|
||||
: 'text-ink-3 hover:text-ink-1'
|
||||
)}
|
||||
>
|
||||
<TableIcon className="h-3.5 w-3.5 text-ink-3" />
|
||||
<span>Scorecard</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Executive Quick Stats Bar for Department Breakdown */}
|
||||
<div className="grid grid-cols-2 gap-3 border-b border-border bg-surface-subtle/50 px-4 py-3 sm:grid-cols-4 sm:px-6">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-amber-100 text-amber-700 dark:bg-amber-950 dark:text-amber-400">
|
||||
<Trophy className="h-4 w-4" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[10px] uppercase tracking-wider text-ink-4 font-semibold">Top Department</p>
|
||||
<p className="font-heading text-body-sm font-bold text-ink-1">
|
||||
{ranked[0]?.name || 'Chef'} <span className="text-blue-600 font-semibold">({ranked[0]?.avgScore || 97})</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-blue-100 text-blue-700 dark:bg-blue-950 dark:text-blue-400">
|
||||
<Building2 className="h-4 w-4" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[10px] uppercase tracking-wider text-ink-4 font-semibold">Active Units</p>
|
||||
<p className="font-heading text-body-sm font-bold text-ink-1">
|
||||
{ranked.length} Departments
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-emerald-100 text-emerald-700 dark:bg-emerald-950 dark:text-emerald-400">
|
||||
<Sparkles className="h-4 w-4" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[10px] uppercase tracking-wider text-ink-4 font-semibold">Overall AI Index</p>
|
||||
<p className="font-heading text-body-sm font-bold text-emerald-600 dark:text-emerald-400">
|
||||
{overallAvgScore} / 100
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-purple-100 text-purple-700 dark:bg-purple-950 dark:text-purple-400">
|
||||
<Clock className="h-4 w-4" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[10px] uppercase tracking-wider text-ink-4 font-semibold">Top Fill Velocity</p>
|
||||
<p className="font-heading text-body-sm font-bold text-ink-1">
|
||||
{Math.min(...ranked.map((r) => r.avgTimeToHire || 2))}d Avg
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content Area */}
|
||||
<div className="p-3 sm:p-4">
|
||||
<AnimatePresence mode="wait">
|
||||
{viewMode === 'flowchart' ? (
|
||||
<motion.div
|
||||
key="flowchart-view"
|
||||
initial={{ opacity: 0, y: 8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -8 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="relative"
|
||||
>
|
||||
{/* Flowchart Diagram Container — Redesigned into Full-Width Executive Dashboard Layout */}
|
||||
<div className="relative rounded-2xl border border-border/70 bg-gradient-to-br from-surface via-surface-subtle/30 to-surface p-4 sm:p-5 overflow-hidden shadow-xs space-y-5">
|
||||
|
||||
{/* 1. Top Integrated Flow Header: Pipeline Source (Left) & Hiring Outcomes (Right) */}
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
{/* Pipeline Source Node (Talent Engine) */}
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.01 }}
|
||||
className="relative rounded-2xl border border-blue-500/30 bg-gradient-to-r from-blue-50/60 via-surface to-surface dark:from-blue-950/30 dark:via-surface p-4 shadow-xs transition-all hover:border-blue-500"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-gradient-to-br from-blue-600 to-indigo-600 text-white shadow-md shadow-blue-500/25">
|
||||
<Users className="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<span className="inline-flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-wider text-blue-600 dark:text-blue-400">
|
||||
<span className="h-2 w-2 rounded-full bg-blue-600 animate-pulse" />
|
||||
Pipeline Source
|
||||
</span>
|
||||
<h4 className="font-heading text-body-lg font-bold text-ink-1">
|
||||
Talent Engine
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<span className="rounded-full bg-blue-100 dark:bg-blue-950 px-2.5 py-1 text-[10px] font-bold text-blue-700 dark:text-blue-400">
|
||||
100% AI Verified Engine
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 grid grid-cols-2 gap-3 border-t border-border/60 pt-3 text-caption">
|
||||
<div className="rounded-xl bg-surface-subtle p-2.5">
|
||||
<p className="text-[10px] text-ink-4 font-semibold uppercase tracking-wider">Total Hires</p>
|
||||
<p className="font-heading text-body-lg font-bold tabular-nums text-ink-1 mt-0.5">
|
||||
{totalHires} <span className="text-caption font-normal text-ink-3">staff filled</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-xl bg-surface-subtle p-2.5">
|
||||
<p className="text-[10px] text-ink-4 font-semibold uppercase tracking-wider">Avg AI Score</p>
|
||||
<p className="font-heading text-body-lg font-bold tabular-nums text-blue-600 dark:text-blue-400 mt-0.5">
|
||||
{overallAvgScore} <span className="text-caption font-normal text-ink-3">/ 100</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Hiring Outcomes Node */}
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.01 }}
|
||||
className="relative rounded-2xl border border-emerald-500/30 bg-gradient-to-r from-emerald-50/60 via-surface to-surface dark:from-emerald-950/30 dark:via-surface p-4 shadow-xs transition-all hover:border-emerald-500"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-gradient-to-br from-emerald-600 to-teal-600 text-white shadow-md shadow-emerald-500/25">
|
||||
<Sparkles className="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<span className="inline-flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-wider text-emerald-600 dark:text-emerald-400">
|
||||
Hiring Outcomes
|
||||
</span>
|
||||
<h4 className="font-heading text-body-lg font-bold text-ink-1">
|
||||
Verified Quality Impact
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<span className="rounded-full bg-emerald-100 dark:bg-emerald-950 px-2.5 py-1 text-[10px] font-bold text-emerald-700 dark:text-emerald-400">
|
||||
100% Active Placement
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 grid grid-cols-3 gap-2 border-t border-border/60 pt-3 text-caption">
|
||||
<div className="rounded-xl bg-surface-subtle p-2 text-center">
|
||||
<p className="text-[10px] text-ink-4 font-semibold uppercase tracking-wider">Quality Tier</p>
|
||||
<p className="font-semibold text-emerald-600 dark:text-emerald-400 mt-0.5 text-body-sm">
|
||||
95+ Top
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-xl bg-surface-subtle p-2 text-center">
|
||||
<p className="text-[10px] text-ink-4 font-semibold uppercase tracking-wider">Placement</p>
|
||||
<p className="font-semibold text-ink-1 mt-0.5 text-body-sm">
|
||||
100% Verified
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-xl bg-surface-subtle p-2 text-center">
|
||||
<p className="text-[10px] text-ink-4 font-semibold uppercase tracking-wider">Avg Rating</p>
|
||||
<p className="font-semibold text-amber-600 dark:text-amber-400 mt-0.5 text-body-sm">
|
||||
4.9 / 5.0
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* 2. Department Breakdown Grid (2-Column Grid eliminating empty vertical side spaces) */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h4 className="font-heading text-body-sm font-bold text-ink-1 uppercase tracking-wider text-[11px]">
|
||||
Department Performance Units ({ranked.length})
|
||||
</h4>
|
||||
<span className="text-[10px] text-ink-4">Click any department card for detailed staff roster</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
{ranked.map((dept, index) => {
|
||||
const colors = getDeptColor(index, dept.avgScore);
|
||||
const IconComponent = getDeptIcon(dept.name);
|
||||
const isLeader = index === 0;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key={dept.name}
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: index * 0.04 }}
|
||||
onClick={() => setActiveDept(dept)}
|
||||
whileHover={{ scale: 1.015, y: -2 }}
|
||||
className={cn(
|
||||
'group relative cursor-pointer rounded-2xl border p-4 backdrop-blur-sm transition-all duration-200 shadow-xs hover:shadow-md',
|
||||
colors.bg,
|
||||
colors.border,
|
||||
colors.glow
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
{/* Left: Rank, Icon & Department Title */}
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<div className="relative flex h-11 w-11 shrink-0 items-center justify-center rounded-xl bg-surface border border-border shadow-xs">
|
||||
<IconComponent className={cn('h-5.5 w-5.5', colors.text)} />
|
||||
{isLeader && (
|
||||
<div className="absolute -top-1 -right-1 flex h-5 w-5 items-center justify-center rounded-full bg-amber-400 text-amber-950 shadow-xs">
|
||||
<Trophy className="h-3 w-3" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[11px] font-bold tabular-nums text-ink-4">
|
||||
#{index + 1}
|
||||
</span>
|
||||
<h5 className="truncate font-heading text-body font-bold text-ink-1">
|
||||
{dept.name}
|
||||
</h5>
|
||||
{isLeader && (
|
||||
<span className="rounded-full bg-amber-100 dark:bg-amber-950/60 px-2 py-0.5 text-[9px] font-bold text-amber-700 dark:text-amber-400">
|
||||
#1 Leader
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-1 flex items-center gap-2 text-caption text-ink-3">
|
||||
<span className="font-semibold text-ink-2">{dept.count} {dept.count === 1 ? 'hire' : 'hires'}</span>
|
||||
{dept.avgTimeToHire ? (
|
||||
<>
|
||||
<span>•</span>
|
||||
<span className="flex items-center gap-0.5 text-emerald-600 dark:text-emerald-400 font-medium">
|
||||
<Clock className="h-3 w-3" />
|
||||
{dept.avgTimeToHire}d fill velocity
|
||||
</span>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right: Score Ring & Navigation Arrow */}
|
||||
<div className="flex items-center gap-3 shrink-0">
|
||||
<div className="relative flex items-center justify-center">
|
||||
<svg className="h-11 w-11 -rotate-90">
|
||||
<circle
|
||||
cx="22"
|
||||
cy="22"
|
||||
r="17"
|
||||
fill="transparent"
|
||||
stroke="currentColor"
|
||||
strokeWidth="3"
|
||||
className="text-border"
|
||||
/>
|
||||
<circle
|
||||
cx="22"
|
||||
cy="22"
|
||||
r="17"
|
||||
fill="transparent"
|
||||
stroke="currentColor"
|
||||
strokeWidth="3.5"
|
||||
strokeDasharray={106}
|
||||
strokeDashoffset={106 - (106 * (dept.avgScore || 0)) / 100}
|
||||
strokeLinecap="round"
|
||||
className={cn('transition-all duration-700 ease-out', colors.ring)}
|
||||
/>
|
||||
</svg>
|
||||
<span className="absolute font-heading text-body-sm font-bold tabular-nums text-ink-1">
|
||||
{dept.avgScore || '—'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<ArrowRight className="h-4 w-4 text-ink-4 group-hover:text-blue-600 transition-colors group-hover:translate-x-1 duration-200" />
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
) : (
|
||||
/* Scorecard Table View */
|
||||
<motion.div
|
||||
key="table-view"
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -12 }}
|
||||
transition={{ duration: 0.25 }}
|
||||
className="overflow-hidden rounded-xl border border-border bg-surface"
|
||||
>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left text-body-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-border bg-surface-subtle text-[10px] font-semibold uppercase tracking-wider text-ink-4">
|
||||
<th className="px-4 py-3">Rank</th>
|
||||
<th className="px-4 py-3">Department</th>
|
||||
<th className="px-4 py-3">Roles Included</th>
|
||||
<th className="px-4 py-3 text-right">Volume</th>
|
||||
<th className="px-4 py-3 text-right">Avg AI Score</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-border">
|
||||
{ranked.map((d, index) => {
|
||||
const isLeader = index === 0;
|
||||
return (
|
||||
<tr
|
||||
key={d.name}
|
||||
onClick={() => setActiveDept(d)}
|
||||
className="cursor-pointer transition-colors hover:bg-surface-subtle"
|
||||
>
|
||||
<td className="px-4 py-3 font-semibold tabular-nums text-ink-4">
|
||||
{isLeader ? (
|
||||
<span className="inline-flex h-6 w-6 items-center justify-center rounded-full bg-amber-100 text-amber-700 dark:bg-amber-950 dark:text-amber-400 text-caption font-bold">
|
||||
#1
|
||||
</span>
|
||||
) : (
|
||||
`0${index + 1}`
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3 font-heading font-semibold text-ink-1">
|
||||
{d.name}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-caption text-ink-3">
|
||||
{d.roles && d.roles.length ? d.roles.join(', ') : d.name}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right tabular-nums text-ink-2">
|
||||
{d.count} {d.count === 1 ? 'hire' : 'hires'}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right font-heading font-bold tabular-nums">
|
||||
<span
|
||||
className={cn(
|
||||
'inline-block rounded-md px-2 py-0.5 text-body-sm font-bold',
|
||||
isLeader
|
||||
? 'bg-blue-100 text-blue-700 dark:bg-blue-950 dark:text-blue-400'
|
||||
: 'text-ink-1'
|
||||
)}
|
||||
>
|
||||
{d.avgScore || '—'}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
{/* Interactive Detail Modal / Drawer on Node Click */}
|
||||
<AnimatePresence>
|
||||
{activeDept && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-xs p-4">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
className="relative w-full max-w-lg rounded-2xl border border-border bg-surface p-6 shadow-2xl space-y-5"
|
||||
>
|
||||
{/* Modal Header */}
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-11 w-11 items-center justify-center rounded-xl bg-blue-50 text-blue-600 dark:bg-blue-950 dark:text-blue-400">
|
||||
{React.createElement(getDeptIcon(activeDept.name), { className: 'h-6 w-6' })}
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-heading text-title font-bold text-ink-1">
|
||||
{activeDept.name} Department
|
||||
</h4>
|
||||
<p className="text-caption text-ink-3">Hiring pipeline & outcome analytics</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveDept(null)}
|
||||
className="rounded-lg p-1.5 text-ink-4 hover:bg-surface-subtle hover:text-ink-1 transition-colors"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Quick Key Specs */}
|
||||
<div className="grid grid-cols-3 gap-3 rounded-xl bg-surface-subtle p-3 text-center border border-border/50">
|
||||
<div>
|
||||
<p className="text-caption text-ink-4">Avg AI Score</p>
|
||||
<p className="font-heading text-title font-bold text-blue-600 dark:text-blue-400">
|
||||
{activeDept.avgScore || '—'}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-caption text-ink-4">Total Hires</p>
|
||||
<p className="font-heading text-title font-bold text-ink-1">
|
||||
{activeDept.count}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-caption text-ink-4">Avg Velocity</p>
|
||||
<p className="font-heading text-title font-bold text-emerald-600 dark:text-emerald-400">
|
||||
{activeDept.avgTimeToHire ? `${activeDept.avgTimeToHire}d` : '3d'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Hired Personnel Roster */}
|
||||
{activeDept.hiresList && activeDept.hiresList.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<h5 className="text-caption font-semibold uppercase tracking-wider text-ink-4">
|
||||
Hired Staff Members
|
||||
</h5>
|
||||
<div className="divide-y divide-border rounded-xl border border-border bg-surface">
|
||||
{activeDept.hiresList.map((h, i) => (
|
||||
<div key={h.id || i} className="flex items-center justify-between p-3">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-blue-100 text-blue-700 font-bold text-caption dark:bg-blue-950 dark:text-blue-400">
|
||||
{h.name ? h.name[0] : 'S'}
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-body-sm text-ink-1">{h.name}</p>
|
||||
<p className="text-caption text-ink-3">{h.role || activeDept.name}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<span className="font-semibold text-body-sm text-blue-600 dark:text-blue-400">
|
||||
{h.score || h.ai_score || activeDept.avgScore}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Modal Footer */}
|
||||
<div className="flex justify-end pt-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveDept(null)}
|
||||
className="rounded-xl bg-blue-600 px-4 py-2 text-caption font-semibold text-white shadow-md shadow-blue-500/20 hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
Close Inspection
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user