"use client"; import React from "react"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import ReactECharts from "echarts-for-react"; import SidebarLayout from "@/components/SidebarLayout"; import { fetchApi, getAccessToken, getBackendUrl, parseDateTime, getLocalDateString } from "@/app/utils/api"; import { Users, UserCheck, UserMinus, Clock, TrendingUp, Activity, ArrowRight, AlertTriangle, CheckCircle, Zap, ShieldAlert, Calendar, Award, Server, Cpu, X } from "lucide-react"; import Link from "next/link"; // Pure SVG sparkline helper for premium look function Sparkline({ color, data }: { color: string; data: number[] }) { const width = 90; const height = 28; const max = Math.max(...data); const min = Math.min(...data); const range = max - min || 1; const points = data .map((val, i) => { const x = (i / (data.length - 1)) * width; const y = height - ((val - min) / range) * (height - 6) - 3; return `${x},${y}`; }) .join(" "); const colors = { blue: "#3b82f6", emerald: "#10b981", amber: "#f59e0b", rose: "#f43f5e", indigo: "#6366f1", }; const strokeColor = colors[color as keyof typeof colors] || "#3b82f6"; return ( ); } function StatCard({ label, value, icon: Icon, color, loading, sublabel, sparkData }: { label: string; value: string | number; icon: any; color: "blue" | "emerald" | "amber" | "rose" | "indigo"; loading?: boolean; sublabel?: string; sparkData: number[]; }) { const colors = { blue: { icon: "text-zinc-600", bg: "bg-zinc-100", border: "border-zinc-200 hover:border-zinc-400", val: "text-zinc-900" }, emerald: { icon: "text-zinc-800", bg: "bg-zinc-100", border: "border-zinc-200 hover:border-zinc-400", val: "text-zinc-900" }, amber: { icon: "text-zinc-650", bg: "bg-zinc-100", border: "border-zinc-200 hover:border-zinc-400", val: "text-zinc-900" }, rose: { icon: "text-zinc-700", bg: "bg-zinc-100", border: "border-zinc-200 hover:border-zinc-400", val: "text-zinc-900" }, indigo: { icon: "text-zinc-900", bg: "bg-zinc-100", border: "border-zinc-200 hover:border-zinc-400", val: "text-zinc-900" }, }; const c = colors[color]; return (

{label}

{loading ? (
) : (

{value}

)} {sublabel &&

{sublabel}

}
{!loading && }
); } const AVATAR_COLORS = [ "from-blue-500/20 to-indigo-500/20 text-blue-400 border-blue-500/15", "from-emerald-500/20 to-teal-500/20 text-emerald-400 border-emerald-500/15", "from-amber-500/20 to-orange-500/20 text-amber-400 border-amber-500/15", "from-indigo-500/20 to-purple-500/20 text-indigo-400 border-indigo-500/15", "from-cyan-500/20 to-blue-500/20 text-cyan-400 border-cyan-500/15", ]; function ScanLogItem({ log }: { log: any }) { const isSuccess = log.status === "Match Success"; const isSpoof = log.is_spoof; const isUnknown = log.status === "Unknown Person"; let dotColor = "bg-slate-600"; let statusColor = "text-slate-500"; let statusBg = "bg-slate-500/8 border-slate-500/15"; let statusLabel = log.status; if (isSuccess) { dotColor = "bg-emerald-400"; statusColor = "text-emerald-400"; statusBg = "bg-emerald-500/8 border-emerald-500/15"; statusLabel = "Matched"; } else if (isSpoof) { dotColor = "bg-rose-500 animate-ping"; statusColor = "text-rose-400"; statusBg = "bg-rose-500/8 border-rose-500/20"; statusLabel = "Spoof Blocked"; } else if (isUnknown) { dotColor = "bg-amber-400"; statusColor = "text-amber-400"; statusBg = "bg-amber-500/8 border-amber-500/20"; statusLabel = "Unknown"; } // Generate a nice random avatar gradient color based on employee ID const colorIndex = log.employee ? (log.employee.id % AVATAR_COLORS.length) : 0; const avatarCls = AVATAR_COLORS[colorIndex]; return (
{log.employee ? (
{log.employee.name.charAt(0).toUpperCase()}
) : (
?
)}

{log.employee ? log.employee.name : "Unknown Person"}

· {log.camera}

{log.employee ? `${log.employee.designation} (${log.employee.employee_id})` : "Unauthorized access attempt"}

{parseDateTime(log.timestamp)?.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }) || ""}

{statusLabel}
); } const getDeptTheme = (code: string) => { const themes = { ENG: { bg: "bg-blue-50/50 hover:bg-blue-50/70 border-blue-200 text-blue-900", iconBg: "bg-blue-500/10 text-blue-600 border-blue-500/20", barBg: "bg-blue-100", barFill: "bg-gradient-to-r from-blue-500 to-blue-600" }, HR: { bg: "bg-purple-50/50 hover:bg-purple-50/70 border-purple-200 text-purple-900", iconBg: "bg-purple-500/10 text-purple-600 border-purple-500/20", barBg: "bg-purple-100", barFill: "bg-gradient-to-r from-purple-500 to-purple-600" }, MKT: { bg: "bg-amber-50/50 hover:bg-amber-50/70 border-amber-200 text-amber-900", iconBg: "bg-amber-500/10 text-amber-600 border-amber-500/20", barBg: "bg-amber-100", barFill: "bg-gradient-to-r from-amber-500 to-amber-600" }, FIN: { bg: "bg-emerald-50/50 hover:bg-emerald-50/70 border-emerald-200 text-emerald-900", iconBg: "bg-emerald-500/10 text-emerald-600 border-emerald-500/20", barBg: "bg-emerald-100", barFill: "bg-gradient-to-r from-emerald-500 to-emerald-600" }, OPS: { bg: "bg-rose-50/50 hover:bg-rose-50/70 border-rose-200 text-rose-900", iconBg: "bg-rose-500/10 text-rose-600 border-rose-500/20", barBg: "bg-rose-100", barFill: "bg-gradient-to-r from-rose-500 to-rose-600" }, }; return themes[code as keyof typeof themes] || { bg: "bg-slate-50/50 hover:bg-slate-50/70 border-slate-200 text-slate-900", iconBg: "bg-slate-500/10 text-slate-600 border-slate-500/20", barBg: "bg-slate-100", barFill: "bg-gradient-to-r from-slate-500 to-slate-600" }; }; export default function DashboardPage() { const [currentTime, setCurrentTime] = React.useState(""); const [currentDate, setCurrentDate] = React.useState(""); const [statusFilter, setStatusFilter] = React.useState<"ALL" | "MATCHED" | "UNKNOWN" | "SPOOF">("ALL"); const [selectedDept, setSelectedDept] = React.useState(null); const queryClient = useQueryClient(); const { data: deptAttendance, isLoading: loadingDeptAttendance } = useQuery({ queryKey: ["dept-attendance", selectedDept?.id], queryFn: () => { if (!selectedDept) return []; const todayStr = getLocalDateString(); return fetchApi(`/attendance/daily?department_id=${selectedDept.id}&date_val=${todayStr}`); }, enabled: !!selectedDept }); React.useEffect(() => { const updateTime = () => { const now = new Date(); setCurrentTime(now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' })); setCurrentDate(now.toLocaleDateString([], { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })); }; updateTime(); const interval = setInterval(updateTime, 1000); return () => clearInterval(interval); }, []); React.useEffect(() => { const token = getAccessToken(); if (!token) return; const backendUrl = getBackendUrl(); const sseUrl = `${backendUrl}/analytics/live-stream?token=${encodeURIComponent(token)}`; const eventSource = new EventSource(sseUrl); eventSource.onmessage = (event) => { try { const newLog = JSON.parse(event.data); // 1. Prepend the new log to the "recent-activity" list queryClient.setQueryData(["recent-activity"], (oldData: any[] | undefined) => { if (!oldData) return [newLog]; const filtered = oldData.filter((log: any) => log.id !== newLog.id); const cutoff = Date.now() - 24 * 60 * 60 * 1000; return [newLog, ...filtered] .filter((log: any) => { const logDate = parseDateTime(log.timestamp); return logDate ? logDate.getTime() >= cutoff : false; }) .slice(0, 10); }); // 2. Invalidate other dashboard stats to trigger refetch queryClient.invalidateQueries({ queryKey: ["dashboard-summary"] }); queryClient.invalidateQueries({ queryKey: ["department-distribution"] }); queryClient.invalidateQueries({ queryKey: ["attendance-trends"] }); } catch (err) { console.error("Error handling SSE message:", err); } }; eventSource.onerror = (err) => { console.error("SSE connection error:", err); }; return () => { eventSource.close(); }; }, [queryClient]); const { data: summary, isLoading: loadingSummary } = useQuery({ queryKey: ["dashboard-summary"], queryFn: () => fetchApi("/analytics/dashboard-summary"), refetchInterval: 60000 }); const { data: trends, isLoading: loadingTrends } = useQuery({ queryKey: ["attendance-trends"], queryFn: () => fetchApi("/analytics/attendance-trends?days=7") }); const { data: deptStats, isLoading: loadingDept } = useQuery({ queryKey: ["department-distribution"], queryFn: () => fetchApi("/analytics/department-distribution") }); const { data: recentLogs, isLoading: loadingLogs } = useQuery({ queryKey: ["recent-activity"], queryFn: () => fetchApi("/analytics/recent-activity?limit=6"), refetchInterval: 60000 }); // Map real database trends to the card sparklines const trendsLoaded = trends && trends.length > 0; const staffSpark = trendsLoaded ? trends.map(() => summary?.total_employees || 0) : [0, 0, 0, 0, 0, 0, summary?.total_employees || 0]; const presentSpark = trendsLoaded ? trends.map((t: any) => t.present) : [0, 0, 0, 0, 0, 0, summary?.present_today || 0]; const lateSpark = trendsLoaded ? trends.map((t: any) => t.late) : [0, 0, 0, 0, 0, 0, summary?.late_today || 0]; const absentSpark = trendsLoaded ? trends.map((t: any) => Math.max(0, (summary?.total_employees || 0) - t.present)) : [0, 0, 0, 0, 0, 0, summary?.absent_today || 0]; const rateSpark = trendsLoaded ? trends.map((t: any) => Math.round((t.present / (summary?.total_employees || 1)) * 100)) : [0, 0, 0, 0, 0, 0, summary?.attendance_percentage || 0]; const trendOption = () => { if (!trends) return {}; return { backgroundColor: "transparent", tooltip: { trigger: "axis", backgroundColor: "#ffffff", borderColor: "rgba(24,24,27,0.08)", borderWidth: 1, shadowColor: "rgba(0,0,0,0.02)", shadowBlur: 10, textStyle: { color: "#18181b", fontSize: 11, fontFamily: "var(--font-inter)" }, extraCssText: "border-radius:12px;padding:8px 12px;box-shadow: 0 4px 16px rgba(0,0,0,0.04);" }, legend: { data: ["Present", "Late Arrivals"], textStyle: { color: "#71717a", fontSize: 10, fontFamily: "var(--font-inter)" }, bottom: 0, icon: "circle", itemWidth: 8, itemHeight: 8, itemGap: 24 }, grid: { top: 20, left: 36, right: 16, bottom: 40 }, xAxis: { type: "category", data: trends.map((t: any) => { const parts = t.date.split("-"); let d; if (parts.length === 3) { d = new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2])); } else { d = new Date(t.date); } return d.toLocaleDateString([], { weekday: "short", month: "short", day: "numeric" }); }), axisLine: { lineStyle: { color: "rgba(24,24,27,0.06)" } }, axisTick: { show: false }, axisLabel: { color: "#71717a", fontSize: 10, fontFamily: "var(--font-inter)" } }, yAxis: { type: "value", splitLine: { lineStyle: { color: "rgba(24,24,27,0.04)", type: "dashed" } }, axisLabel: { color: "#71717a", fontSize: 10, fontFamily: "var(--font-inter)" }, axisLine: { show: false } }, series: [ { name: "Present", type: "line", smooth: 0.35, showSymbol: false, symbolSize: 6, data: trends.map((t: any) => t.present), lineStyle: { color: "#18181b", width: 2.25, shadowColor: "rgba(24,24,27,0.1)", shadowBlur: 8, shadowOffsetY: 4 }, itemStyle: { color: "#18181b" }, areaStyle: { color: { type: "linear", x: 0, y: 0, x2: 0, y2: 1, colorStops: [ { offset: 0, color: "rgba(24,24,27,0.06)" }, { offset: 1, color: "rgba(24,24,27,0)" } ] } } }, { name: "Late Arrivals", type: "bar", data: trends.map((t: any) => t.late), itemStyle: { color: "#71717a", borderRadius: [3, 3, 0, 0] }, barWidth: 6, barMaxWidth: 10 } ] }; }; const deptOption = () => { if (!deptStats) return {}; return { backgroundColor: "transparent", tooltip: { trigger: "axis", axisPointer: { type: "shadow" }, backgroundColor: "#ffffff", borderColor: "rgba(24,24,27,0.08)", borderWidth: 1, textStyle: { color: "#18181b", fontSize: 11 }, extraCssText: "border-radius:12px;padding:8px 12px;box-shadow: 0 4px 16px rgba(0,0,0,0.04);" }, grid: { top: 10, left: 90, right: 20, bottom: 20 }, xAxis: { type: "value", splitLine: { lineStyle: { color: "rgba(24,24,27,0.04)", type: "dashed" } }, axisLabel: { color: "#71717a", fontSize: 9 }, axisLine: { show: false } }, yAxis: { type: "category", data: deptStats.map((d: any) => d.name || d.code), axisLine: { show: false }, axisTick: { show: false }, axisLabel: { color: "#18181b", fontSize: 10, fontFamily: "var(--font-inter)" } }, series: [ { name: "Total Employees", type: "bar", data: deptStats.map((d: any) => d.total_employees), itemStyle: { color: "rgba(24,24,27,0.04)", borderRadius: [0, 4, 4, 0] }, barGap: "-100%", barWidth: 10 }, { name: "Present Today", type: "bar", data: deptStats.map((d: any) => d.present_today), barWidth: 10, itemStyle: { borderRadius: [0, 4, 4, 0], color: { type: "linear", x: 0, y: 0, x2: 1, y2: 0, colorStops: [ { offset: 0, color: "#18181b" }, { offset: 1, color: "#27272a" } ] } } } ] }; }; return (
{/* ─── Header ─── */}

Dashboard Overview

Live biometric monitoring and attendance intelligence platform

{/* Real-time Clock widget */} {currentTime && (
{currentDate} | {currentTime}
)}
Live Feed Active
{/* ─── KPI Grid ─── */}
{/* ─── Charts Row ─── */}
{/* Trend Chart */}

Attendance Trends

Biometric logs for the past 7 days

{loadingTrends ? (
) : ( )}
{/* Live Feed */}

Live Activity Feed

Real-time scan logs ticker

STREAMING
{/* Status Filter pills */}
{(["ALL", "MATCHED", "UNKNOWN", "SPOOF"] as const).map((filter) => ( ))}
{loadingLogs ? ( Array.from({ length: 4 }).map((_, i) => (
)) ) : (() => { const filtered = recentLogs?.filter((log: any) => { if (statusFilter === "ALL") return true; if (statusFilter === "MATCHED") return log.status === "Match Success"; if (statusFilter === "UNKNOWN") return log.status === "Unknown Person"; if (statusFilter === "SPOOF") return log.is_spoof || log.status === "Spoof Rejected" || log.status === "Spoof Blocked"; return true; }); if (!filtered || filtered.length === 0) { return (

No events

No recent events matching this filter.

); } return filtered.map((log: any) => ( )); })()}
Access Complete Ledger Logs
{/* ─── Bottom Section: Department Breakdown ─── */}

Department Distribution

Staff presence stats by company department

{loadingDept ? (
{Array.from({ length: 5 }).map((_, i) => (
))}
) : !deptStats || deptStats.length === 0 ? (
No department stats found.
) : (
{deptStats.map((d: any) => { const theme = getDeptTheme(d.code); const percent = d.total_employees > 0 ? Math.round((d.present_today / d.total_employees) * 100) : 0; return (
setSelectedDept(d)} className={`p-4 rounded-2xl border transition-all duration-300 flex flex-col justify-between h-32 hover:scale-[1.02] cursor-pointer ${theme.bg}`} > {/* Top Header Row */}
{d.code}
{d.present_today} / {d.total_employees}
{/* Department Name & Subheading */}

{d.department}

{percent}% present today

{/* Progress Bar */}
0 ? percent : 0}%` }} />
); })}
)}
{/* Department Detail Modal */} {selectedDept && (
{selectedDept.code} Department

{selectedDept.department}

{selectedDept.present_today} Present / {selectedDept.total_employees} Total Employees Today

{loadingDeptAttendance ? ( Array.from({ length: 3 }).map((_, i) => (
)) ) : !deptAttendance || deptAttendance.length === 0 ? (
No attendance records found for this department today.
) : (
{deptAttendance.map((rec: any) => { const avatarColor = AVATAR_COLORS[rec.employee.id % AVATAR_COLORS.length]; return (
{rec.employee.name.charAt(0).toUpperCase()}

{rec.employee.name}

{rec.employee.employee_id} · {rec.employee.designation}

IN: {rec.check_in ? parseDateTime(rec.check_in)?.toLocaleTimeString([], {hour:"2-digit",minute:"2-digit"}) : "—"}

OUT: {rec.check_out ? parseDateTime(rec.check_out)?.toLocaleTimeString([], {hour:"2-digit",minute:"2-digit"}) : "—"}

{rec.status}
); })}
)}
)} ); }