Initial commit
This commit is contained in:
163
src/pages/AdminDashboard.jsx
Normal file
163
src/pages/AdminDashboard.jsx
Normal file
@@ -0,0 +1,163 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user