Files
doormilxpress_astryx/src/pages/AdminDashboard.jsx
2026-08-07 23:53:15 +05:30

163 lines
9.2 KiB
JavaScript

import React from 'react';
import { useNavigate } from 'react-router-dom';
import { motion } from 'framer-motion';
import { useJobPostings, useApplications, useStaff } from '@/lib/krowHooks';
import PipelineFunnel from '@/components/krow/PipelineFunnel';
import KROWCandidateLeaderboard from '@/components/krow/KROWCandidateLeaderboard';
import { Briefcase, Users, Brain, ClipboardCheck, UserCheck, BadgeCheck, Gauge, Plus, ChevronRight, Sparkles } from 'lucide-react';
import { AssistantTrigger } from '@/components/ai-assistant';
const METRICS = [
{ icon: Briefcase, label: 'Open Positions', bg: 'bg-[#0A39DF]', glow: 'shadow-blue-600/20' },
{ icon: Users, label: 'Applicants', bg: 'bg-[#333F48]', glow: 'shadow-slate-600/20' },
{ icon: Brain, label: 'AI Screened', bg: 'bg-[#0A39DF]', glow: 'shadow-blue-600/20' },
{ icon: ClipboardCheck, label: 'Shortlisted', bg: 'bg-[#F9E547]', glow: 'shadow-yellow-400/30' },
{ icon: UserCheck, label: 'Hired', bg: 'bg-[#0A39DF]', glow: 'shadow-blue-600/20' },
{ icon: BadgeCheck, label: 'Active Staff', bg: 'bg-[#333F48]', glow: 'shadow-slate-600/20' },
{ icon: Gauge, label: 'Avg KROW Score', bg: 'bg-[#0A39DF]', glow: 'shadow-blue-600/20' },
{ icon: Sparkles, label: 'AI Interviews', bg: 'bg-[#F9E547]', glow: 'shadow-yellow-400/30' },
];
function MetricCard({ icon: Icon, value, label, bg, glow, index }) {
const isYellow = bg === 'bg-[#F9E547]';
return (
<motion.div
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.05, duration: 0.4, ease: 'easeOut' }}
whileHover={{ y: -4, transition: { duration: 0.2 } }}
className="glass-card border border-white/60 rounded-2xl p-4 shadow-sm hover:shadow-lg transition-shadow"
>
<div className={`w-10 h-10 rounded-xl ${bg} flex items-center justify-center shadow-lg ${glow} mb-3`}>
<Icon className={`w-5 h-5 ${isYellow ? 'text-[#0A39DF]' : 'text-white'}`} />
</div>
<div className="text-3xl font-heading font-bold gradient-text">{value}</div>
<div className="text-[11px] text-[#9CA3AF] font-medium tracking-wide mt-0.5">{label}</div>
</motion.div>
);
}
export default function AdminDashboard() {
const navigate = useNavigate();
const { data: postings = [] } = useJobPostings();
const { data: applications = [] } = useApplications();
const { data: staff = [] } = useStaff();
const openPositions = postings.filter(p => p.status === 'active');
const screened = applications.filter(a => ['ai_screened', 'shortlisted', 'interview', 'hired'].includes(a.status));
const shortlisted = applications.filter(a => ['shortlisted', 'interview', 'hired'].includes(a.status));
const hired = applications.filter(a => a.status === 'hired');
const activeStaff = staff.filter(s => s.status === 'active' || s.status === 'onboarding');
const scored = applications.filter(a => a.ai_score > 0);
const avgScore = scored.length ? Math.round(scored.reduce((s, a) => s + a.ai_score, 0) / scored.length) : 0;
const interviews = applications.filter(a => a.status === 'interview' || a.interview_id).length;
const values = [
openPositions.length,
applications.length,
screened.length,
shortlisted.length,
hired.length,
activeStaff.length,
avgScore,
interviews,
];
const statsForPosting = (id) => {
const apps = applications.filter(a => a.job_posting_id === id);
return { applied: apps.length, screened: apps.filter(a => a.status !== 'applied').length, hired: apps.filter(a => a.status === 'hired').length };
};
const recentStaff = [...activeStaff].sort((a, b) => new Date(b.hire_date || 0) - new Date(a.hire_date || 0)).slice(0, 6);
return (
<div className="space-y-8">
{/* Hero */}
<motion.div initial={{ opacity: 0, y: -12 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.5 }} className="flex items-center justify-between flex-wrap gap-4">
<div>
<span className="text-[11px] font-medium tracking-wide text-[#0A39DF] bg-[#EFF6FF] px-2.5 py-1 rounded-full">Admin · Employer</span>
<h1 className="text-3xl font-heading font-bold text-[#111827] mt-2">Workforce <span className="gradient-text">Control Center</span></h1>
<p className="text-[14px] text-[#6B7280] mt-1">Full hiring pipeline, staffing & talent intelligence in one view</p>
</div>
<div className="flex items-center gap-2">
<button onClick={() => navigate('/positions/new')} className="inline-flex items-center gap-2 bg-[#F9E547] text-[#0A39DF] text-[13px] font-bold px-5 py-2.5 rounded-full shadow-lg hover:scale-[1.03] transition-transform">
<Plus className="w-4 h-4" /> Create Position
</button>
<AssistantTrigger />
</div>
</motion.div>
{/* Metrics */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{METRICS.map((m, i) => (
<MetricCard key={m.label} icon={m.icon} value={values[i]} label={m.label} bg={m.bg} glow={m.glow} index={i} />
))}
</div>
{/* Pipeline funnel + Active positions */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<PipelineFunnel applications={applications} />
<motion.div initial={{ opacity: 0, x: 16 }} animate={{ opacity: 1, x: 0 }} transition={{ delay: 0.3 }} className="glass-card border border-white/60 rounded-2xl p-6 shadow-sm">
<div className="flex items-center justify-between mb-5">
<h3 className="font-heading font-semibold text-[#111827] text-[16px]">Active Positions</h3>
<button onClick={() => navigate('/positions')} className="text-[12px] text-[#0A39DF] hover:underline inline-flex items-center gap-1 font-medium">View all <ChevronRight className="w-3 h-3" /></button>
</div>
<div className="space-y-1">
{openPositions.length === 0 && (
<div className="text-center py-10">
<div className="w-12 h-12 rounded-full bg-white/50 flex items-center justify-center mx-auto mb-3"><Briefcase className="w-5 h-5 text-[#9CA3AF]" /></div>
<p className="text-[13px] text-[#9CA3AF]">No active positions</p>
</div>
)}
{openPositions.slice(0, 6).map((p, idx) => {
const s = statsForPosting(p.id);
return (
<motion.div key={p.id} initial={{ opacity: 0, x: 8 }} animate={{ opacity: 1, x: 0 }} transition={{ delay: 0.4 + idx * 0.05 }} onClick={() => navigate(`/positions/${p.id}`)} className="flex items-center gap-3 p-2.5 rounded-xl hover:bg-white/50 transition-colors cursor-pointer group">
<div className="w-1 h-9 rounded-full bg-[#0A39DF] shrink-0" />
<div className="flex-1 min-w-0">
<div className="text-[13px] font-medium text-[#111827] truncate">{p.title}</div>
<div className="text-[11px] text-[#6B7280]">{s.applied} applied · {s.screened} screened · {s.hired} hired</div>
</div>
<ChevronRight className="w-4 h-4 text-[#D1D5DB] group-hover:text-[#0A39DF] transition-colors" />
</motion.div>
);
})}
</div>
</motion.div>
</div>
{/* Leaderboard + Active staff */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<KROWCandidateLeaderboard applications={applications} />
<motion.div initial={{ opacity: 0, x: 16 }} animate={{ opacity: 1, x: 0 }} transition={{ delay: 0.4 }} className="glass-card border border-white/60 rounded-2xl p-6 shadow-sm">
<div className="flex items-center justify-between mb-5">
<h3 className="font-heading font-semibold text-[#111827] text-[16px]">Active Staff</h3>
<span className="text-[12px] text-[#6B7280]">{activeStaff.length} on roster</span>
</div>
<div className="space-y-1">
{recentStaff.length === 0 && (
<div className="text-center py-10">
<div className="w-12 h-12 rounded-full bg-white/50 flex items-center justify-center mx-auto mb-3"><UserCheck className="w-5 h-5 text-[#9CA3AF]" /></div>
<p className="text-[13px] text-[#9CA3AF]">No hired staff yet</p>
</div>
)}
{recentStaff.map((s, idx) => (
<div key={s.id} className="flex items-center gap-3 p-2.5 rounded-xl hover:bg-white/50 transition-colors">
<div className="w-9 h-9 rounded-full bg-[#333F48] flex items-center justify-center text-white text-[12px] font-semibold shrink-0">
{s.name?.split(' ').map(w => w[0]).slice(0, 2).join('').toUpperCase()}
</div>
<div className="flex-1 min-w-0">
<div className="text-[13px] font-medium text-[#111827] truncate">{s.name}</div>
<div className="text-[11px] text-[#6B7280] truncate">{s.role} · <span className="text-[#0A39DF] font-medium">{s.profile_tier}</span></div>
</div>
<span className={`text-[10px] px-2 py-0.5 rounded-full font-medium ${s.status === 'active' ? 'bg-[#ECFDF5] text-[#065F46]' : 'bg-[#FFFBEB] text-[#92400E]'}`}>{s.status}</span>
</div>
))}
</div>
</motion.div>
</div>
</div>
);
}