"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 (
{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 */}
);
})}
)}
{/* 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}
);
})}
)}
)}
);
}