Files
doormilxpress_astryx/src/components/charts/DepartmentPerformance.jsx

618 lines
30 KiB
JavaScript

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';
/**
* Animated Flowchart & Department Performance Component
*
* 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 }) {
const [viewMode, setViewMode] = useState('flowchart'); // 'flowchart' | 'table'
const [activeDept, setActiveDept] = useState(null);
// 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 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-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>
{/* 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>
);
}