feat: dashboard operations intelligence — analytics cards + signal breakdown
Deploy to VPS / deploy (push) Has been cancelled
Deploy to VPS / deploy (push) Has been cancelled
Add real-time operational metrics to the HQ dashboard: - Signals This Month: count with trend % vs previous 30 days - Pending Actions: highlights when tickets need attention (rose border) - Email Delivery: success rate percentage + sent/failed counts - B2B Clients: approved vs total registered Plus a Signal Breakdown panel showing all-time distribution by type (Orders, Diagnostics, Consultations, B2B Access) with proportional bars. All queries run in parallel via Promise.all for minimal latency. Wrapped in try/catch so the dashboard never breaks if DB is slow. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,16 @@ import {
|
||||
Server,
|
||||
Image as ImageIcon,
|
||||
Settings as SettingsIcon,
|
||||
TrendingUp,
|
||||
TrendingDown,
|
||||
Minus,
|
||||
MailCheck,
|
||||
AlertCircle,
|
||||
UserCheck,
|
||||
Package,
|
||||
Sparkles,
|
||||
Stethoscope,
|
||||
KeyRound,
|
||||
} from "lucide-react";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { logoutAdmin } from "@/app/hq-command/login/actions";
|
||||
@@ -25,8 +35,48 @@ import { logoutAdmin } from "@/app/hq-command/login/actions";
|
||||
export const revalidate = 0;
|
||||
|
||||
export default async function DashboardPage() {
|
||||
const nodesCount = await prisma.globalNode.count({ where: { isActive: true } });
|
||||
const appsCount = await prisma.application.count({ where: { isActive: true } });
|
||||
const now = new Date();
|
||||
const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
||||
const sixtyDaysAgo = new Date(now.getTime() - 60 * 24 * 60 * 60 * 1000);
|
||||
|
||||
let nodesCount = 0, appsCount = 0;
|
||||
let signalsPending = 0, signalsThisMonth = 0, signalsLastMonth = 0;
|
||||
let signalsOrders = 0, signalsDiag = 0, signalsConsult = 0, signalsAccess = 0;
|
||||
let emailSent = 0, emailFailed = 0;
|
||||
let clientsTotal = 0, clientsApproved = 0;
|
||||
|
||||
try {
|
||||
[
|
||||
nodesCount, appsCount,
|
||||
signalsPending, signalsThisMonth, signalsLastMonth,
|
||||
signalsOrders, signalsDiag, signalsConsult, signalsAccess,
|
||||
emailSent, emailFailed,
|
||||
clientsTotal, clientsApproved,
|
||||
] = await Promise.all([
|
||||
prisma.globalNode.count({ where: { isActive: true } }),
|
||||
prisma.application.count({ where: { isActive: true } }),
|
||||
prisma.operationsSignal.count({ where: { status: "PENDING" } }),
|
||||
prisma.operationsSignal.count({ where: { createdAt: { gte: thirtyDaysAgo } } }),
|
||||
prisma.operationsSignal.count({ where: { createdAt: { gte: sixtyDaysAgo, lt: thirtyDaysAgo } } }),
|
||||
prisma.operationsSignal.count({ where: { type: "ORDER" } }),
|
||||
prisma.operationsSignal.count({ where: { type: "DIAGNOSTIC" } }),
|
||||
prisma.operationsSignal.count({ where: { type: "CONSULTATION" } }),
|
||||
prisma.operationsSignal.count({ where: { type: "ACCESS_REQUEST" } }),
|
||||
prisma.operationsSignal.count({ where: { emailSentAt: { not: null } } }),
|
||||
prisma.operationsSignal.count({ where: { emailError: { not: null }, emailSentAt: null } }),
|
||||
prisma.clientUser.count(),
|
||||
prisma.clientUser.count({ where: { isApproved: true } }),
|
||||
]);
|
||||
} catch (e) {
|
||||
console.error("[dashboard] Analytics query failed:", e);
|
||||
}
|
||||
|
||||
const signalsTotal = signalsOrders + signalsDiag + signalsConsult + signalsAccess;
|
||||
const emailTotal = emailSent + emailFailed;
|
||||
const emailRate = emailTotal > 0 ? Math.round((emailSent / emailTotal) * 100) : 100;
|
||||
const monthTrend = signalsLastMonth > 0
|
||||
? Math.round(((signalsThisMonth - signalsLastMonth) / signalsLastMonth) * 100)
|
||||
: signalsThisMonth > 0 ? 100 : 0;
|
||||
|
||||
const modules = [
|
||||
{
|
||||
@@ -184,6 +234,90 @@ export default async function DashboardPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Operations Intelligence ─────────────────────────────────────── */}
|
||||
<div className="mb-6">
|
||||
<span className="text-[10px] uppercase tracking-widest text-[#86868B] font-semibold">Operations Intelligence</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
|
||||
{/* Signals This Month */}
|
||||
<div className="bg-[#111] border border-white/5 p-5 rounded-2xl shadow-lg">
|
||||
<span className="text-[10px] uppercase tracking-widest text-[#86868B] block mb-2">Signals · 30d</span>
|
||||
<div className="flex items-end justify-between">
|
||||
<span className="text-2xl font-medium text-white">{signalsThisMonth}</span>
|
||||
{monthTrend !== 0 ? (
|
||||
<span className={`flex items-center gap-1 text-xs font-medium ${monthTrend > 0 ? "text-emerald-400" : "text-rose-400"}`}>
|
||||
{monthTrend > 0 ? <TrendingUp size={14} /> : <TrendingDown size={14} />}
|
||||
{monthTrend > 0 ? "+" : ""}{monthTrend}%
|
||||
</span>
|
||||
) : (
|
||||
<span className="flex items-center gap-1 text-xs text-[#86868B]"><Minus size={14} /> flat</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pending Actions */}
|
||||
<div className={`bg-[#111] border p-5 rounded-2xl shadow-lg ${signalsPending > 0 ? "border-rose-500/30" : "border-white/5"}`}>
|
||||
<span className="text-[10px] uppercase tracking-widest text-[#86868B] block mb-2">Pending Actions</span>
|
||||
<div className="flex items-end justify-between">
|
||||
<span className={`text-2xl font-medium ${signalsPending > 0 ? "text-rose-400" : "text-emerald-400"}`}>{signalsPending}</span>
|
||||
{signalsPending > 0 ? <AlertCircle size={18} className="text-rose-400/50" /> : <Activity size={18} className="text-emerald-400/30" />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Email Delivery */}
|
||||
<div className="bg-[#111] border border-white/5 p-5 rounded-2xl shadow-lg">
|
||||
<span className="text-[10px] uppercase tracking-widest text-[#86868B] block mb-2">Email Delivery</span>
|
||||
<div className="flex items-end justify-between">
|
||||
<span className="text-2xl font-medium text-white">{emailRate}%</span>
|
||||
<div className="flex items-center gap-2 text-[10px] text-[#86868B]">
|
||||
<span className="flex items-center gap-1"><MailCheck size={12} className="text-emerald-400" />{emailSent}</span>
|
||||
{emailFailed > 0 && <span className="text-rose-400">{emailFailed} fail</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* B2B Clients */}
|
||||
<div className="bg-[#111] border border-white/5 p-5 rounded-2xl shadow-lg">
|
||||
<span className="text-[10px] uppercase tracking-widest text-[#86868B] block mb-2">B2B Clients</span>
|
||||
<div className="flex items-end justify-between">
|
||||
<span className="text-2xl font-medium text-white">{clientsApproved}<span className="text-sm text-[#86868B] font-normal">/{clientsTotal}</span></span>
|
||||
<UserCheck size={18} className="text-emerald-400/30" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Signal Type Breakdown */}
|
||||
{signalsTotal > 0 && (
|
||||
<div className="bg-[#111] border border-white/5 rounded-2xl p-5 mb-12 shadow-lg">
|
||||
<span className="text-[10px] uppercase tracking-widest text-[#86868B] block mb-4">Signal Breakdown · All Time</span>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
{[
|
||||
{ label: "Orders", count: signalsOrders, icon: Package, color: "text-amber-400", bg: "bg-amber-500/10", bar: "bg-amber-500" },
|
||||
{ label: "Diagnostics", count: signalsDiag, icon: Stethoscope, color: "text-rose-400", bg: "bg-rose-500/10", bar: "bg-rose-500" },
|
||||
{ label: "Consultations", count: signalsConsult, icon: Sparkles, color: "text-[#00F0FF]", bg: "bg-[#00F0FF]/10", bar: "bg-[#00F0FF]" },
|
||||
{ label: "B2B Access", count: signalsAccess, icon: KeyRound, color: "text-emerald-400", bg: "bg-emerald-500/10", bar: "bg-emerald-500" },
|
||||
].map(s => {
|
||||
const pct = signalsTotal > 0 ? (s.count / signalsTotal) * 100 : 0;
|
||||
return (
|
||||
<div key={s.label} className={`${s.bg} rounded-xl p-4 flex flex-col gap-3`}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<s.icon size={14} className={s.color} />
|
||||
<span className={`text-[10px] uppercase tracking-widest font-bold ${s.color}`}>{s.label}</span>
|
||||
</div>
|
||||
<span className="text-lg font-medium text-white">{s.count}</span>
|
||||
</div>
|
||||
<div className="h-1.5 bg-black/30 rounded-full overflow-hidden">
|
||||
<div className={`h-full ${s.bar} rounded-full transition-all duration-700`} style={{ width: `${pct}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mb-6">
|
||||
<span className="text-[10px] uppercase tracking-widest text-[#86868B] font-semibold">Core Modules</span>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user