diff --git a/src/app/hq-command/dashboard/page.tsx b/src/app/hq-command/dashboard/page.tsx index af755fd..9aa4d21 100644 --- a/src/app/hq-command/dashboard/page.tsx +++ b/src/app/hq-command/dashboard/page.tsx @@ -18,15 +18,65 @@ 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"; +import { logoutAdmin } from "@/app/hq-command/login/actions"; -export const revalidate = 0; +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() { + {/* ── Operations Intelligence ─────────────────────────────────────── */} +
+ Operations Intelligence +
+ +
+ {/* Signals This Month */} +
+ Signals · 30d +
+ {signalsThisMonth} + {monthTrend !== 0 ? ( + 0 ? "text-emerald-400" : "text-rose-400"}`}> + {monthTrend > 0 ? : } + {monthTrend > 0 ? "+" : ""}{monthTrend}% + + ) : ( + flat + )} +
+
+ + {/* Pending Actions */} +
0 ? "border-rose-500/30" : "border-white/5"}`}> + Pending Actions +
+ 0 ? "text-rose-400" : "text-emerald-400"}`}>{signalsPending} + {signalsPending > 0 ? : } +
+
+ + {/* Email Delivery */} +
+ Email Delivery +
+ {emailRate}% +
+ {emailSent} + {emailFailed > 0 && {emailFailed} fail} +
+
+
+ + {/* B2B Clients */} +
+ B2B Clients +
+ {clientsApproved}/{clientsTotal} + +
+
+
+ + {/* Signal Type Breakdown */} + {signalsTotal > 0 && ( +
+ Signal Breakdown · All Time +
+ {[ + { 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 ( +
+
+
+ + {s.label} +
+ {s.count} +
+
+
+
+
+ ); + })} +
+
+ )} +
Core Modules