Files
Qris-Soundbox/ui/admin-dashboard-overview/index.html

1028 lines
56 KiB
HTML

<!DOCTYPE html>
<html class="light" lang="en"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>Soundbox Ops - Admin Console</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&amp;family=Inter:wght@400;500;600;700&amp;family=JetBrains+Mono&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"surface-container-lowest": "#ffffff",
"on-tertiary": "#ffffff",
"secondary-fixed-dim": "#b7c8e1",
"warning": "#F59E0B",
"on-primary-fixed-variant": "#003ea8",
"inverse-surface": "#2e3039",
"surface": "#faf8ff",
"surface-container-low": "#f3f3fe",
"outline": "#737686",
"on-primary": "#ffffff",
"tertiary-fixed": "#ffdbcd",
"primary": "#004ac6",
"on-error-container": "#93000a",
"surface-tint": "#0053db",
"tertiary-container": "#bc4800",
"surface-variant": "#e1e2ed",
"on-tertiary-fixed": "#360f00",
"surface-container-high": "#e7e7f3",
"info": "#0EA5E9",
"slate-500": "#64748B",
"tertiary-fixed-dim": "#ffb596",
"on-surface": "#191b23",
"outline-variant": "#c3c6d7",
"error": "#ba1a1a",
"inverse-on-surface": "#f0f0fb",
"on-primary-fixed": "#00174b",
"surface-bright": "#faf8ff",
"surface-container": "#ededf9",
"error-container": "#ffdad6",
"slate-900": "#0F172A",
"inverse-primary": "#b4c5ff",
"on-tertiary-fixed-variant": "#7d2d00",
"slate-700": "#334155",
"slate-200": "#E2E8F0",
"on-background": "#191b23",
"on-error": "#ffffff",
"on-secondary": "#ffffff",
"secondary": "#505f76",
"on-secondary-fixed": "#0b1c30",
"on-secondary-fixed-variant": "#38485d",
"danger": "#DC2626",
"on-primary-container": "#eeefff",
"success": "#16A34A",
"on-tertiary-container": "#ffede6",
"surface-container-highest": "#e1e2ed",
"primary-fixed": "#dbe1ff",
"on-surface-variant": "#434655",
"secondary-container": "#d0e1fb",
"primary-container": "#2563eb",
"background": "#F8FAFC",
"primary-fixed-dim": "#b4c5ff",
"tertiary": "#943700",
"secondary-fixed": "#d3e4fe",
"surface-dim": "#d9d9e5",
"on-secondary-container": "#54647a",
"slate-100": "#F1F5F9"
},
"borderRadius": {
"DEFAULT": "0.125rem",
"lg": "0.25rem",
"xl": "0.5rem",
"full": "0.75rem"
},
"spacing": {
"page-padding": "24px",
"gutter": "24px",
"topbar-height": "72px",
"card-padding": "20px",
"row-height": "52px"
},
"fontFamily": {
"display-lg": ["Plus Jakarta Sans"],
"label-md": ["Inter"],
"headline-md": ["Plus Jakarta Sans"],
"body-md": ["Inter"],
"headline-lg": ["Plus Jakarta Sans"],
"body-lg": ["Inter"],
"metric-lg": ["Inter"],
"metric-sm": ["Inter"]
},
"fontSize": {
"display-lg": ["36px", {"lineHeight": "44px", "letterSpacing": "-0.02em", "fontWeight": "600"}],
"label-md": ["12px", {"lineHeight": "16px", "letterSpacing": "0.01em", "fontWeight": "500"}],
"headline-md": ["20px", {"lineHeight": "28px", "fontWeight": "600"}],
"body-md": ["14px", {"lineHeight": "20px", "fontWeight": "400"}],
"headline-lg": ["28px", {"lineHeight": "36px", "fontWeight": "600"}],
"body-lg": ["16px", {"lineHeight": "24px", "fontWeight": "400"}],
"metric-lg": ["32px", {"lineHeight": "40px", "fontWeight": "600"}],
"metric-sm": ["14px", {"lineHeight": "20px", "fontWeight": "600"}]
}
},
},
}
</script>
<style>
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
}
.custom-scrollbar::-webkit-scrollbar {
width: 4px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: transparent;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: #E2E8F0;
border-radius: 10px;
}
.glass-card {
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(8px);
}
</style>
</head>
<body class="bg-background text-on-background font-body-md text-body-md overflow-x-hidden">
<!-- SideNavBar -->
<aside class="hidden lg:flex w-64 h-full fixed left-0 top-0 bg-surface-container-lowest dark:bg-slate-900 border-r border-slate-200 dark:border-slate-700 flex-col py-6 px-4 gap-2 z-50">
<div class="mb-8 px-2">
<h1 class="font-headline-md text-headline-md font-bold text-primary dark:text-primary-fixed">Soundbox Ops</h1>
<p class="font-label-md text-label-md text-on-surface-variant opacity-70">Admin Console</p>
</div>
<nav class="flex-1 space-y-1">
<!-- Active Tab: Overview -->
<a class="bg-secondary-container dark:bg-secondary text-on-secondary-container dark:text-on-secondary font-bold rounded-lg flex items-center gap-3 px-3 py-2.5 transition-transform active:scale-95" href="/ui/admin-dashboard-overview">
<span class="material-symbols-outlined" data-icon="dashboard">dashboard</span>
<span>Overview</span>
</a>
<a class="text-on-surface-variant dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors flex items-center gap-3 px-3 py-2.5 rounded-lg" href="/ui/merchant-list-management">
<span class="material-symbols-outlined" data-icon="storefront">storefront</span>
<span>Merchant Management</span>
</a>
<a class="text-on-surface-variant dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors flex items-center gap-3 px-3 py-2.5 rounded-lg" href="/ui/device-registry-monitoring">
<span class="material-symbols-outlined" data-icon="speaker_group">speaker_group</span>
<span>Device Registry</span>
</a>
<a class="text-on-surface-variant dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors flex items-center gap-3 px-3 py-2.5 rounded-lg" href="/ui/transaction-history-monitoring">
<span class="material-symbols-outlined" data-icon="receipt_long">receipt_long</span>
<span>Transactions</span>
</a>
<a class="text-on-surface-variant dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors flex items-center gap-3 px-3 py-2.5 rounded-lg" href="/ui/settlement-batch-management">
<span class="material-symbols-outlined" data-icon="account_balance">account_balance</span>
<span>Ledger &amp; Settlement</span>
</a>
<a class="text-on-surface-variant dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors flex items-center gap-3 px-3 py-2.5 rounded-lg" href="/ui/admin-system-audit-logs">
<span class="material-symbols-outlined" data-icon="history_edu">history_edu</span>
<span>Audit Control</span>
</a>
</nav>
<div class="mt-auto pt-6 border-t border-slate-100 dark:border-slate-800 space-y-1">
<button id="dashboard-register-device" class="w-full bg-primary text-on-primary py-2.5 rounded-lg font-bold mb-4 flex items-center justify-center gap-2 hover:opacity-90 active:scale-95 transition-all">
<span class="material-symbols-outlined" data-icon="add">add</span>
Register New Device
</button>
<a class="text-on-surface-variant dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors flex items-center gap-3 px-3 py-2 rounded-lg" href="/ui/admin-fee-pricing-management">
<span class="material-symbols-outlined" data-icon="settings">settings</span>
<span>Settings</span>
</a>
<a class="text-on-surface-variant dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors flex items-center gap-3 px-3 py-2 rounded-lg" href="/ui/hub">
<span class="material-symbols-outlined" data-icon="help">help</span>
<span>Support</span>
</a>
</div>
</aside>
<!-- TopNavBar -->
<header class="fixed top-0 right-0 h-[72px] bg-surface-container-lowest dark:bg-slate-900 flex justify-between items-center w-full lg:w-[calc(100%-256px)] lg:ml-64 px-4 sm:px-page-padding z-40 border-b border-slate-200 dark:border-slate-700">
<div class="flex items-center gap-3 lg:gap-6 flex-1 min-w-0">
<div class="relative w-full max-w-md hidden sm:block">
<span class="material-symbols-outlined absolute left-3 top-1/2 -translate-y-1/2 text-on-surface-variant text-body-lg">search</span>
<input class="w-full pl-10 pr-4 py-2 bg-slate-100 dark:bg-slate-800 border-none rounded-full focus:ring-2 focus:ring-primary/20 text-body-md" placeholder="Search devices, merchants, or transactions..." type="text"/>
</div>
<div class="flex items-center gap-4 overflow-x-auto whitespace-nowrap">
<a class="text-primary dark:text-primary-fixed border-b-2 border-primary h-[72px] flex items-center px-2 font-bold" href="/ui/admin-dashboard-overview">Dashboard</a>
<a class="text-on-surface-variant dark:text-slate-400 hover:text-primary transition-colors h-[72px] hidden md:flex items-center px-2" href="/ui/device-registry-monitoring">System Health</a>
</div>
</div>
<div class="flex items-center gap-2 sm:gap-4 shrink-0">
<button class="w-10 h-10 flex items-center justify-center rounded-full hover:bg-slate-100 dark:hover:bg-slate-800 text-on-surface-variant relative">
<span class="material-symbols-outlined" data-icon="notifications">notifications</span>
<span class="absolute top-2 right-2 w-2 h-2 bg-error rounded-full"></span>
</button>
<button class="w-10 h-10 flex items-center justify-center rounded-full hover:bg-slate-100 dark:hover:bg-slate-800 text-on-surface-variant">
<span class="material-symbols-outlined" data-icon="calendar_today">calendar_today</span>
</button>
<div class="h-8 w-[1px] bg-slate-200 dark:bg-slate-700 mx-2 hidden sm:block"></div>
<div class="flex items-center gap-3">
<div class="text-right hidden sm:block">
<p class="font-bold text-body-md leading-none">Admin User</p>
<p class="text-label-md text-on-surface-variant opacity-70 leading-none mt-1">Super Administrator</p>
</div>
<img alt="Administrator Profile" class="w-10 h-10 rounded-full border-2 border-primary/10" src="https://lh3.googleusercontent.com/aida-public/AB6AXuB61eQv0UesiiqGw5OUHVQbaA_dyExJL4b7KTMpoWwbtef5ADmEto1ZpJkVAh1u1v3gjZ4jWeIcJxM3QEAc5Lbb_RiJRBzspl31-ArZ_BOk81uoa33eL3GnXH4FQKEPtgNy56dMsXrd4pnpPsXM2KL0-S9UFwfsxrXUHcnjlDarnrsdlP5lbKfQJmTVO2kF1h-uQxj5OxwomQxAJLT-B9Zy3ZCWaEsh9DYUtAp7zrAcvDT1PoMzxfS1012kLlCi3xUH0GrWeChmQXY"/>
</div>
</div>
</header>
<!-- Main Content Canvas -->
<main class="lg:ml-64 pt-[72px] min-h-screen p-4 sm:p-page-padding max-w-[1600px]">
<!-- Dashboard Header & Welcome -->
<div class="mb-8 flex flex-col sm:flex-row justify-between items-start sm:items-end gap-4">
<div>
<h2 class="font-display-lg text-[28px] leading-9 sm:text-display-lg text-on-surface mb-1">Operational Overview</h2>
<p class="text-body-lg text-on-surface-variant">Real-time status of your QRIS soundbox ecosystem.</p>
</div>
<div class="flex gap-2 w-full sm:w-auto">
<button class="flex items-center gap-2 px-4 py-2 border border-slate-200 rounded-lg hover:bg-slate-50 transition-colors font-bold text-body-md">
<span class="material-symbols-outlined text-[20px]" data-icon="filter_list">filter_list</span>
Filter View
</button>
<button class="flex items-center gap-2 px-4 py-2 bg-primary text-on-primary rounded-lg hover:opacity-90 transition-colors font-bold text-body-md">
<span class="material-symbols-outlined text-[20px]" data-icon="download">download</span>
Export Report
</button>
</div>
</div>
<!-- Bento Layout: KPI Cards -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-6 mb-8">
<!-- Total Merchants -->
<div class="bg-surface-container-lowest border border-slate-200 p-card-padding rounded-xl hover:shadow-lg transition-shadow">
<div class="flex justify-between items-start mb-4">
<div class="p-2 bg-primary/10 rounded-lg text-primary">
<span class="material-symbols-outlined" data-icon="store">store</span>
</div>
<span class="text-success font-metric-sm flex items-center gap-1">
+4.2% <span class="material-symbols-outlined text-[16px]" data-icon="trending_up">trending_up</span>
</span>
</div>
<p class="text-label-md font-label-md text-on-surface-variant uppercase tracking-wider mb-1">Total Merchants</p>
<p id="kpi-total-merchants" class="text-metric-lg font-metric-lg text-on-surface">1,240</p>
</div>
<!-- Devices Online -->
<div class="bg-surface-container-lowest border border-slate-200 p-card-padding rounded-xl hover:shadow-lg transition-shadow">
<div class="flex justify-between items-start mb-4">
<div class="p-2 bg-success/10 rounded-lg text-success">
<span class="material-symbols-outlined" data-icon="sensors">sensors</span>
</div>
<span class="text-on-surface-variant font-label-md">94.4% Active</span>
</div>
<p class="text-label-md font-label-md text-on-surface-variant uppercase tracking-wider mb-1">Devices Online</p>
<p class="text-metric-lg font-metric-lg text-on-surface"><span id="kpi-devices-online">850</span><span class="text-body-lg text-slate-400 font-normal"> / <span id="kpi-devices-total">900</span></span></p>
</div>
<!-- Today's Volume -->
<div class="bg-surface-container-lowest border border-slate-200 p-card-padding rounded-xl hover:shadow-lg transition-shadow">
<div class="flex justify-between items-start mb-4">
<div class="p-2 bg-warning/10 rounded-lg text-warning">
<span class="material-symbols-outlined" data-icon="payments">payments</span>
</div>
<span class="text-success font-metric-sm flex items-center gap-1">
+12% <span class="material-symbols-outlined text-[16px]" data-icon="trending_up">trending_up</span>
</span>
</div>
<p class="text-label-md font-label-md text-on-surface-variant uppercase tracking-wider mb-1">Today's Transactions</p>
<p id="kpi-todays-volume" class="text-metric-lg font-metric-lg text-on-surface">Rp450M</p>
</div>
<!-- Success Rate -->
<div class="bg-surface-container-lowest border border-slate-200 p-card-padding rounded-xl hover:shadow-lg transition-shadow">
<div class="flex justify-between items-start mb-4">
<div class="p-2 bg-info/10 rounded-lg text-info">
<span class="material-symbols-outlined" data-icon="verified">verified</span>
</div>
<span class="text-success font-metric-sm flex items-center gap-1">
Stable <span class="material-symbols-outlined text-[16px]" data-icon="check_circle">check_circle</span>
</span>
</div>
<p class="text-label-md font-label-md text-on-surface-variant uppercase tracking-wider mb-1">Success Rate</p>
<p id="kpi-success-rate" class="text-metric-lg font-metric-lg text-on-surface">99.2%</p>
</div>
<!-- Pending Payouts -->
<div class="bg-surface-container-lowest border border-slate-200 p-card-padding rounded-xl hover:shadow-lg transition-shadow border-l-4 border-l-warning">
<div class="flex justify-between items-start mb-4">
<div class="p-2 bg-warning/10 rounded-lg text-warning">
<span class="material-symbols-outlined" data-icon="account_balance_wallet">account_balance_wallet</span>
</div>
<a class="text-primary font-bold text-label-md hover:underline" href="/ui/settlement-batch-management">View All</a>
</div>
<p class="text-label-md font-label-md text-on-surface-variant uppercase tracking-wider mb-1">Pending Payouts</p>
<p id="kpi-pending-settlements" class="text-metric-lg font-metric-lg text-on-surface">Rp0</p>
</div>
</div>
<!-- Main Chart & Sidebar Rail Grid -->
<div class="grid grid-cols-1 lg:grid-cols-12 gap-8 mb-8">
<!-- Left: Chart & Table Column -->
<div class="lg:col-span-8 space-y-8">
<!-- Transaction Trend Chart -->
<div class="bg-surface-container-lowest border border-slate-200 rounded-xl p-6">
<div class="flex justify-between items-center mb-6">
<div>
<h3 class="text-headline-md font-headline-md text-on-surface">Transaction Volume Trend</h3>
<p class="text-body-md text-on-surface-variant">Last 7 days performance metrics</p>
</div>
<div class="flex items-center gap-4">
<div class="flex items-center gap-2">
<span class="w-3 h-3 bg-primary rounded-full"></span>
<span class="text-label-md font-label-md">Current Period</span>
</div>
<div class="flex items-center gap-2">
<span class="w-3 h-3 bg-slate-200 rounded-full"></span>
<span class="text-label-md font-label-md text-on-surface-variant">Previous Period</span>
</div>
</div>
</div>
<div id="transaction-trend-chart" class="h-[320px] w-full flex items-end gap-2 px-4">
<div class="flex-1 flex flex-col justify-end gap-1">
<div class="w-full bg-slate-100 rounded-t h-[40%] relative group">
<div class="absolute bottom-0 left-0 right-0 bg-primary/40 h-[70%] group-hover:bg-primary transition-colors cursor-pointer"></div>
</div>
<span class="text-center text-label-md text-slate-400">Mon</span>
</div>
<div class="flex-1 flex flex-col justify-end gap-1">
<div class="w-full bg-slate-100 rounded-t h-[60%] relative group">
<div class="absolute bottom-0 left-0 right-0 bg-primary/40 h-[85%] group-hover:bg-primary transition-colors cursor-pointer"></div>
</div>
<span class="text-center text-label-md text-slate-400">Tue</span>
</div>
<div class="flex-1 flex flex-col justify-end gap-1">
<div class="w-full bg-slate-100 rounded-t h-[55%] relative group">
<div class="absolute bottom-0 left-0 right-0 bg-primary/40 h-[65%] group-hover:bg-primary transition-colors cursor-pointer"></div>
</div>
<span class="text-center text-label-md text-slate-400">Wed</span>
</div>
<div class="flex-1 flex flex-col justify-end gap-1">
<div class="w-full bg-slate-100 rounded-t h-[80%] relative group">
<div class="absolute bottom-0 left-0 right-0 bg-primary/40 h-[95%] group-hover:bg-primary transition-colors cursor-pointer"></div>
</div>
<span class="text-center text-label-md text-slate-400">Thu</span>
</div>
<div class="flex-1 flex flex-col justify-end gap-1">
<div class="w-full bg-slate-100 rounded-t h-[70%] relative group">
<div class="absolute bottom-0 left-0 right-0 bg-primary/40 h-[80%] group-hover:bg-primary transition-colors cursor-pointer"></div>
</div>
<span class="text-center text-label-md text-slate-400">Fri</span>
</div>
<div class="flex-1 flex flex-col justify-end gap-1">
<div class="w-full bg-slate-100 rounded-t h-[45%] relative group">
<div class="absolute bottom-0 left-0 right-0 bg-primary/40 h-[50%] group-hover:bg-primary transition-colors cursor-pointer"></div>
</div>
<span class="text-center text-label-md text-slate-400">Sat</span>
</div>
<div class="flex-1 flex flex-col justify-end gap-1">
<div class="w-full bg-slate-100 rounded-t h-[90%] relative group">
<div class="absolute bottom-0 left-0 right-0 bg-primary/40 h-[100%] group-hover:bg-primary transition-colors cursor-pointer"></div>
</div>
<span class="text-center text-label-md text-slate-400 font-bold text-primary">Sun</span>
</div>
</div>
</div>
<!-- Pending Merchant Table -->
<div class="bg-surface-container-lowest border border-slate-200 rounded-xl overflow-hidden shadow-sm">
<div class="p-6 border-b border-slate-100 flex justify-between items-center">
<div>
<h3 class="text-headline-md font-headline-md text-on-surface">Pending Merchant Onboarding</h3>
<p class="text-body-md text-on-surface-variant">New applications requiring review</p>
</div>
<a class="text-primary font-bold text-body-md hover:underline" href="/ui/admin-onboarding-review-queue">View Full Queue</a>
</div>
<div class="overflow-x-auto">
<table class="w-full text-left border-collapse">
<thead>
<tr class="bg-slate-50/50">
<th class="px-6 py-4 text-label-md font-label-md text-on-surface-variant uppercase tracking-wider">Merchant Name</th>
<th class="px-6 py-4 text-label-md font-label-md text-on-surface-variant uppercase tracking-wider">Category</th>
<th class="px-6 py-4 text-label-md font-label-md text-on-surface-variant uppercase tracking-wider">Submission Date</th>
<th class="px-6 py-4 text-label-md font-label-md text-on-surface-variant uppercase tracking-wider">Status</th>
<th class="px-6 py-4 text-label-md font-label-md text-on-surface-variant uppercase tracking-wider text-right">Actions</th>
</tr>
</thead>
<tbody id="pending-merchants-body" class="divide-y divide-slate-100">
<tr class="hover:bg-slate-50 transition-colors group">
<td class="px-6 py-4">
<div class="flex items-center gap-3">
<div class="w-8 h-8 rounded bg-slate-100 flex items-center justify-center text-primary font-bold">KB</div>
<div>
<p class="font-bold text-on-surface">Kopi Bahagia</p>
<p class="text-label-md text-slate-400">ID: MERCH-9021</p>
</div>
</div>
</td>
<td class="px-6 py-4 text-on-surface-variant">F&amp;B - Cafe</td>
<td class="px-6 py-4 text-on-surface-variant">Oct 24, 2023</td>
<td class="px-6 py-4">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-label-md font-bold bg-warning/10 text-warning">
Pending Review
</span>
</td>
<td class="px-6 py-4 text-right">
<button class="text-primary font-bold hover:text-on-primary-fixed-variant transition-colors">Review</button>
</td>
</tr>
<tr class="hover:bg-slate-50 transition-colors group">
<td class="px-6 py-4">
<div class="flex items-center gap-3">
<div class="w-8 h-8 rounded bg-slate-100 flex items-center justify-center text-primary font-bold">JM</div>
<div>
<p class="font-bold text-on-surface">Jaya Mart</p>
<p class="text-label-md text-slate-400">ID: MERCH-8843</p>
</div>
</div>
</td>
<td class="px-6 py-4 text-on-surface-variant">Retail - Grocery</td>
<td class="px-6 py-4 text-on-surface-variant">Oct 23, 2023</td>
<td class="px-6 py-4">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-label-md font-bold bg-warning/10 text-warning">
Pending Review
</span>
</td>
<td class="px-6 py-4 text-right">
<button class="text-primary font-bold hover:text-on-primary-fixed-variant transition-colors">Review</button>
</td>
</tr>
<tr class="hover:bg-slate-50 transition-colors group">
<td class="px-6 py-4">
<div class="flex items-center gap-3">
<div class="w-8 h-8 rounded bg-slate-100 flex items-center justify-center text-primary font-bold">AL</div>
<div>
<p class="font-bold text-on-surface">Apotek Lestari</p>
<p class="text-label-md text-slate-400">ID: MERCH-8712</p>
</div>
</div>
</td>
<td class="px-6 py-4 text-on-surface-variant">Healthcare - Pharma</td>
<td class="px-6 py-4 text-on-surface-variant">Oct 23, 2023</td>
<td class="px-6 py-4">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-label-md font-bold bg-warning/10 text-warning">
Pending Review
</span>
</td>
<td class="px-6 py-4 text-right">
<button class="text-primary font-bold hover:text-on-primary-fixed-variant transition-colors">Review</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Right: Health & Alerts Rail Column -->
<div class="lg:col-span-4 space-y-8">
<!-- Device Health Distribution -->
<div class="bg-surface-container-lowest border border-slate-200 rounded-xl p-6">
<h3 class="text-headline-md font-headline-md text-on-surface mb-6">Device Health</h3>
<div class="flex justify-center mb-8 relative">
<svg class="w-48 h-48 transform -rotate-90">
<circle class="text-slate-100" cx="96" cy="96" fill="transparent" r="80" stroke="currentColor" stroke-width="20"></circle>
<circle class="text-success" cx="96" cy="96" fill="transparent" r="80" stroke="currentColor" stroke-dasharray="502.6" stroke-dashoffset="50.2" stroke-width="20"></circle>
<circle class="text-warning" cx="96" cy="96" fill="transparent" r="80" stroke="currentColor" stroke-dasharray="502.6" stroke-dashoffset="440" stroke-width="20"></circle>
<circle class="text-danger" cx="96" cy="96" fill="transparent" r="80" stroke="currentColor" stroke-dasharray="502.6" stroke-dashoffset="480" stroke-width="20"></circle>
</svg>
<div class="absolute inset-0 flex flex-col items-center justify-center">
<p id="dashboard-health-percent" class="text-metric-lg font-metric-lg leading-none">94%</p>
<p class="text-label-md text-slate-400 mt-1">Healthy</p>
</div>
</div>
<div class="space-y-4">
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<span class="w-3 h-3 bg-success rounded-full"></span>
<span class="text-body-md">Online / Ready</span>
</div>
<span id="device-online-count" class="font-bold">850</span>
</div>
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<span class="w-3 h-3 bg-warning rounded-full"></span>
<span class="text-body-md">Degraded / Slow</span>
</div>
<span id="device-stale-count" class="font-bold">35</span>
</div>
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<span class="w-3 h-3 bg-danger rounded-full"></span>
<span class="text-body-md">Offline / Error</span>
</div>
<span id="device-offline-count" class="font-bold">15</span>
</div>
</div>
</div>
<!-- Settlement / Finance Summary -->
<div class="bg-surface-container-lowest border border-slate-200 rounded-xl p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-headline-md font-headline-md text-on-surface">Settlement Finance</h3>
<a class="text-primary font-bold text-label-md hover:underline" href="/ui/settlement-batch-management">Open</a>
</div>
<div class="grid grid-cols-2 gap-3 text-body-md">
<div>
<p class="text-label-md text-on-surface-variant uppercase">Pending</p>
<p id="finance-pending-payout" class="font-bold text-warning">Rp0</p>
</div>
<div>
<p class="text-label-md text-on-surface-variant uppercase">Paid</p>
<p id="finance-paid-payout" class="font-bold text-success">Rp0</p>
</div>
<div>
<p class="text-label-md text-on-surface-variant uppercase">Fees</p>
<p id="finance-total-fees" class="font-bold">Rp0</p>
</div>
<div>
<p class="text-label-md text-on-surface-variant uppercase">Adjustment</p>
<p id="finance-adjustment-amount" class="font-bold">Rp0</p>
</div>
<div>
<p class="text-label-md text-on-surface-variant uppercase">Batches</p>
<p id="finance-batch-count" class="font-bold">0</p>
</div>
</div>
<div class="mt-4 pt-4 border-t border-slate-100">
<div class="flex items-center justify-between mb-2">
<p class="text-label-md text-on-surface-variant uppercase">Recent Batches</p>
<span id="finance-created-batches" class="text-label-md font-bold text-warning">0 open</span>
</div>
<div id="finance-recent-batches" class="space-y-2">
<div class="p-3 rounded-lg bg-slate-50 border border-slate-100 text-on-surface-variant text-label-md">Loading settlement batches...</div>
</div>
</div>
</div>
<!-- Dynamic QR Expiry Scheduler -->
<div class="bg-surface-container-lowest border border-slate-200 rounded-xl p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-headline-md font-headline-md text-on-surface">Dynamic QR Expiry</h3>
<span id="expiry-scheduler-badge" class="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-bold bg-slate-100 text-slate-600">Loading</span>
</div>
<div class="grid grid-cols-2 gap-3 text-body-md">
<div>
<p class="text-label-md text-on-surface-variant uppercase">Interval</p>
<p id="expiry-scheduler-interval" class="font-bold">-</p>
</div>
<div>
<p class="text-label-md text-on-surface-variant uppercase">Limit</p>
<p id="expiry-scheduler-limit" class="font-bold">-</p>
</div>
<div>
<p class="text-label-md text-on-surface-variant uppercase">Last Run</p>
<p id="expiry-scheduler-last-run" class="font-bold">-</p>
</div>
<div>
<p class="text-label-md text-on-surface-variant uppercase">Expired</p>
<p id="expiry-scheduler-expired" class="font-bold">-</p>
</div>
</div>
<p id="expiry-scheduler-detail" class="mt-3 pt-3 border-t border-slate-100 text-label-md text-on-surface-variant">-</p>
</div>
<!-- MQTT Broker Status -->
<div class="bg-surface-container-lowest border border-slate-200 rounded-xl p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-headline-md font-headline-md text-on-surface">MQTT Broker</h3>
<span id="mqtt-status-badge" class="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-bold bg-slate-100 text-slate-600">Loading</span>
</div>
<div class="space-y-3 text-body-md">
<div class="flex justify-between gap-4">
<span class="text-on-surface-variant">Mode</span>
<span id="mqtt-mode" class="font-bold">-</span>
</div>
<div class="flex justify-between gap-4">
<span class="text-on-surface-variant">Broker</span>
<span id="mqtt-broker-url" class="font-mono text-right text-[12px]">-</span>
</div>
<div class="flex justify-between gap-4">
<span class="text-on-surface-variant">Client</span>
<span id="mqtt-client-id" class="font-mono text-right text-[12px]">-</span>
</div>
<div class="pt-3 border-t border-slate-100">
<p class="text-label-md text-on-surface-variant uppercase mb-2">Last Message</p>
<p id="mqtt-last-message" class="font-mono text-[12px] text-slate-600 truncate">-</p>
</div>
</div>
</div>
<!-- Recent Alerts / Incidents -->
<div class="bg-surface-container-lowest border border-slate-200 rounded-xl flex flex-col">
<div class="p-6 border-b border-slate-100 flex justify-between items-center">
<h3 class="text-headline-md font-headline-md text-on-surface">Recent Alerts</h3>
<span id="alert-critical-count" class="bg-error/10 text-error px-2 py-0.5 rounded text-label-md font-bold">0 Critical</span>
</div>
<div id="recent-alerts-list" class="p-4 space-y-4 max-h-[480px] overflow-y-auto custom-scrollbar">
<div class="p-4 rounded-lg bg-slate-50 border border-slate-100 text-on-surface-variant">Loading operational alerts...</div>
</div>
<a class="w-full py-4 text-on-surface-variant hover:bg-slate-50 transition-colors font-bold text-body-md border-t border-slate-100 text-center" href="/ui/device-registry-monitoring">
Open Device Operations
</a>
</div>
</div>
</div>
<!-- System Activity Log (Compact Footer-level detail) -->
<div class="bg-slate-900 text-white rounded-xl p-6 mb-8 overflow-hidden relative">
<div class="flex justify-between items-center mb-4">
<h3 class="text-headline-md font-headline-md">Audit Activity Stream</h3>
<div class="flex gap-2">
<span class="px-2 py-1 bg-white/10 rounded text-[10px] font-bold uppercase tracking-widest">Live Stream</span>
<span class="px-2 py-1 bg-success/20 text-success rounded text-[10px] font-bold uppercase tracking-widest">Operational</span>
</div>
</div>
<div id="audit-activity-stream" class="font-mono text-[13px] space-y-2 opacity-80">
<p><span class="text-info">INFO:</span> Loading audit events...</p>
</div>
<!-- Decorative backdrop for "raw data" feel -->
<div class="absolute -right-4 -bottom-4 opacity-10 pointer-events-none">
<span class="material-symbols-outlined text-[120px]" data-icon="terminal">terminal</span>
</div>
</div>
</main>
<script src="/ui/shared/admin-api.js"></script>
<script>
const AdminDashboard = (() => {
const api = window.AdminUIAPI;
const merchantPendingBody = document.getElementById("pending-merchants-body");
const kpiTotalMerchants = document.getElementById("kpi-total-merchants");
const kpiDevicesOnline = document.getElementById("kpi-devices-online");
const kpiDevicesTotal = document.getElementById("kpi-devices-total");
const kpiTodaysVolume = document.getElementById("kpi-todays-volume");
const kpiSuccessRate = document.getElementById("kpi-success-rate");
const kpiPendingSettlements = document.getElementById("kpi-pending-settlements");
const healthPercent = document.getElementById("dashboard-health-percent");
const devicesOnlineEl = document.getElementById("device-online-count");
const devicesStaleEl = document.getElementById("device-stale-count");
const devicesOfflineEl = document.getElementById("device-offline-count");
const financePendingPayout = document.getElementById("finance-pending-payout");
const financePaidPayout = document.getElementById("finance-paid-payout");
const financeTotalFees = document.getElementById("finance-total-fees");
const financeAdjustmentAmount = document.getElementById("finance-adjustment-amount");
const financeBatchCount = document.getElementById("finance-batch-count");
const financeCreatedBatches = document.getElementById("finance-created-batches");
const financeRecentBatches = document.getElementById("finance-recent-batches");
const chartHost = document.getElementById("transaction-trend-chart");
const alertsHost = document.getElementById("recent-alerts-list");
const alertCriticalCount = document.getElementById("alert-critical-count");
const auditStream = document.getElementById("audit-activity-stream");
const mqttBadge = document.getElementById("mqtt-status-badge");
const mqttMode = document.getElementById("mqtt-mode");
const mqttBrokerUrl = document.getElementById("mqtt-broker-url");
const mqttClientId = document.getElementById("mqtt-client-id");
const mqttLastMessage = document.getElementById("mqtt-last-message");
const expiryBadge = document.getElementById("expiry-scheduler-badge");
const expiryInterval = document.getElementById("expiry-scheduler-interval");
const expiryLimit = document.getElementById("expiry-scheduler-limit");
const expiryLastRun = document.getElementById("expiry-scheduler-last-run");
const expiryExpired = document.getElementById("expiry-scheduler-expired");
const expiryDetail = document.getElementById("expiry-scheduler-detail");
const toMerchantName = (merchant) =>
merchant?.brand_name || merchant?.legal_name || "Unknown Merchant";
const normalize = (value) => String(value || "").toLowerCase();
const todayRange = () => {
const start = new Date();
start.setHours(0, 0, 0, 0);
const end = new Date(start);
end.setDate(start.getDate() + 1);
return { from: start.toISOString(), to: end.toISOString() };
};
const lastSevenDays = () => {
const days = [];
const start = new Date();
start.setHours(0, 0, 0, 0);
start.setDate(start.getDate() - 6);
for (let i = 0; i < 7; i += 1) {
const date = new Date(start);
date.setDate(start.getDate() + i);
days.push({
key: date.toISOString().slice(0, 10),
label: date.toLocaleDateString("en-GB", { weekday: "short" }),
amount: 0,
count: 0
});
}
return days;
};
const renderPendingMerchants = (merchants) => {
if (!merchantPendingBody) return;
const rows = merchants.slice(0, 5);
if (!rows.length) {
merchantPendingBody.innerHTML = '<tr><td class="px-6 py-4 text-center text-on-surface-variant" colspan="5">No pending onboarding merchants</td></tr>';
return;
}
merchantPendingBody.innerHTML = rows.map((merchant) => {
const name = toMerchantName(merchant);
const initials = name.substring(0, 2).toUpperCase();
return `
<tr class="hover:bg-slate-50 transition-colors group">
<td class="px-6 py-4">
<div class="flex items-center gap-3">
<div class="w-8 h-8 rounded bg-slate-100 flex items-center justify-center text-primary font-bold">${initials}</div>
<div>
<p class="font-bold text-on-surface">${name}</p>
<p class="text-label-md text-slate-400">ID: ${merchant.merchant_code || merchant.id}</p>
</div>
</div>
</td>
<td class="px-6 py-4 text-on-surface-variant">${merchant.category || "General"}</td>
<td class="px-6 py-4 text-on-surface-variant">${api.formatDateTime(merchant.updated_at || merchant.created_at)}</td>
<td class="px-6 py-4"><span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-bold bg-warning/10 text-warning">PENDING</span></td>
<td class="px-6 py-4 text-right"><a class="text-primary font-bold hover:text-on-primary-fixed-variant transition-colors" href="/ui/admin-onboarding-review-queue">Review</a></td>
</tr>
`;
}).join("");
};
const updateStats = (summary, merchants, todayTransactions, devices) => {
const totalMerchants = merchants.length;
const online = Number(summary?.devices_online ?? summary?.active_devices ?? 0);
const degraded = Number(summary?.devices_degraded || 0);
const stale = Number(summary?.devices_stale || 0);
const offline = Number(summary?.devices_offline || 0);
const totalDevices = devices.length || online + degraded + stale + offline;
const successRate = Number(summary?.success_rate_today || 0);
const pendingSettlementAmount = Number(summary?.settlement_pending_amount || 0);
const paidToday = todayTransactions.filter((tx) => normalize(tx.status) === "paid");
const totalAmount = paidToday.reduce((acc, tx) => acc + Number(tx.amount || 0), 0);
const activePercent = totalDevices > 0 ? Math.round((online / totalDevices) * 100) : 0;
if (kpiTotalMerchants) kpiTotalMerchants.textContent = totalMerchants.toLocaleString("id-ID");
if (kpiDevicesOnline) kpiDevicesOnline.textContent = online.toLocaleString("id-ID");
if (kpiDevicesTotal) kpiDevicesTotal.textContent = totalDevices.toLocaleString("id-ID");
if (kpiTodaysVolume) kpiTodaysVolume.textContent = api.formatMoney(totalAmount);
if (kpiSuccessRate) kpiSuccessRate.textContent = `${successRate.toFixed(2)}%`;
if (kpiPendingSettlements) kpiPendingSettlements.textContent = api.formatMoney(pendingSettlementAmount);
if (healthPercent) healthPercent.textContent = `${activePercent}%`;
if (devicesOnlineEl) devicesOnlineEl.textContent = online.toLocaleString("id-ID");
if (devicesStaleEl) devicesStaleEl.textContent = (stale + degraded).toLocaleString("id-ID");
if (devicesOfflineEl) devicesOfflineEl.textContent = offline.toLocaleString("id-ID");
};
const renderTrend = (transactions) => {
if (!chartHost) return;
const buckets = lastSevenDays();
const bucketMap = new Map(buckets.map((bucket) => [bucket.key, bucket]));
transactions.forEach((tx) => {
const key = String(tx.created_at || "").slice(0, 10);
const bucket = bucketMap.get(key);
if (!bucket) return;
bucket.count += 1;
if (normalize(tx.status) === "paid") {
bucket.amount += Number(tx.amount || 0);
}
});
const maxAmount = Math.max(...buckets.map((bucket) => bucket.amount), 1);
chartHost.innerHTML = buckets.map((bucket) => {
const height = Math.max(8, Math.round((bucket.amount / maxAmount) * 100));
return `
<div class="flex-1 flex flex-col justify-end gap-1 min-w-0">
<div class="w-full bg-slate-100 rounded-t h-[260px] relative group overflow-hidden">
<div class="absolute bottom-0 left-0 right-0 bg-primary/70 group-hover:bg-primary transition-colors cursor-pointer" style="height:${height}%"></div>
<div class="absolute inset-x-1 bottom-2 text-center text-[10px] font-bold text-slate-700 opacity-0 group-hover:opacity-100 transition-opacity">${api.formatMoney(bucket.amount)}</div>
</div>
<span class="text-center text-label-md text-slate-400">${bucket.label}</span>
<span class="text-center text-[10px] text-slate-400">${bucket.count} tx</span>
</div>
`;
}).join("");
};
const renderMqttStatus = (status) => {
if (!status) return;
const publisher = status.publisher || status;
const connected = publisher.connected === true;
const mode = publisher.mode || "simulator";
if (mqttMode) mqttMode.textContent = mode.toUpperCase();
if (mqttBrokerUrl) mqttBrokerUrl.textContent = publisher.broker_url || "-";
if (mqttClientId) mqttClientId.textContent = publisher.client_id || "-";
if (mqttBadge) {
mqttBadge.textContent = mode === "broker" ? (connected ? "Connected" : "Configured") : "Simulator";
mqttBadge.className = `inline-flex items-center px-2.5 py-1 rounded-full text-xs font-bold ${connected ? "bg-success/10 text-success" : mode === "broker" ? "bg-warning/10 text-warning" : "bg-slate-100 text-slate-600"}`;
}
const latest = Array.isArray(status.last_messages) ? status.last_messages[0] : null;
if (mqttLastMessage) {
mqttLastMessage.textContent = latest ? `${latest.publish_status} ${latest.message_type} -> ${latest.topic}` : "No MQTT trace yet";
}
};
const renderExpiryScheduler = (status) => {
if (!status) return;
const enabled = status.enabled === true;
const running = status.running === true;
if (expiryBadge) {
expiryBadge.textContent = running ? "Running" : enabled ? "Enabled" : "Disabled";
expiryBadge.className = `inline-flex items-center px-2.5 py-1 rounded-full text-xs font-bold ${enabled ? "bg-success/10 text-success" : "bg-slate-100 text-slate-600"}`;
}
if (expiryInterval) expiryInterval.textContent = `${Math.round(Number(status.interval_ms || 0) / 1000)}s`;
if (expiryLimit) expiryLimit.textContent = String(status.limit || "-");
if (expiryLastRun) expiryLastRun.textContent = api.formatDateTime(status.last_finished_at || status.last_started_at);
if (expiryExpired) expiryExpired.textContent = String(status.last_result?.expired_count ?? 0);
if (expiryDetail) {
const scanned = status.last_result?.scanned ?? 0;
const skipped = status.last_result?.skipped_count ?? 0;
expiryDetail.textContent = status.last_error
? `Last error: ${status.last_error}`
: `Last sweep scanned ${scanned} dynamic transaction(s), skipped ${skipped}.`;
}
};
const renderFinanceSummary = (summary, batches) => {
const rows = Array.isArray(batches) ? batches : [];
const pendingAmount = Number(summary?.settlement_pending_amount || 0);
const paidAmount = Number(summary?.settlement_paid_amount || 0);
const adjustedPaidAmount = Number(summary?.settlement_adjusted_paid_amount ?? paidAmount);
const adjustmentAmount = Number(summary?.settlement_adjustment_amount || 0);
const feeAmount = Number(summary?.settlement_platform_fee_amount || 0);
const createdCount = Number(summary?.settlement_created_batches || 0);
const paidCount = Number(summary?.settlement_paid_batches || 0);
const totalCount = Number(summary?.settlement_total_batches || rows.length || 0);
if (financePendingPayout) financePendingPayout.textContent = api.formatMoney(pendingAmount);
if (financePaidPayout) financePaidPayout.textContent = api.formatMoney(adjustedPaidAmount);
if (financeTotalFees) financeTotalFees.textContent = api.formatMoney(feeAmount);
if (financeAdjustmentAmount) {
financeAdjustmentAmount.textContent = api.formatMoney(adjustmentAmount);
financeAdjustmentAmount.className = `font-bold ${adjustmentAmount < 0 ? "text-danger" : adjustmentAmount > 0 ? "text-success" : ""}`;
}
if (financeBatchCount) financeBatchCount.textContent = `${totalCount.toLocaleString("id-ID")} total`;
if (financeCreatedBatches) financeCreatedBatches.textContent = `${createdCount.toLocaleString("id-ID")} open`;
if (!financeRecentBatches) return;
if (!rows.length) {
financeRecentBatches.innerHTML = '<div class="p-3 rounded-lg bg-slate-50 border border-slate-100 text-on-surface-variant text-label-md">No settlement batches yet</div>';
return;
}
financeRecentBatches.innerHTML = rows.slice(0, 4).map((batch) => {
const isPaid = normalize(batch.status) === "paid";
const badgeClass = isPaid ? "bg-success/10 text-success" : "bg-warning/10 text-warning";
return `
<div class="flex items-center justify-between gap-3 p-3 rounded-lg border border-slate-100 bg-slate-50">
<div class="min-w-0">
<p class="font-bold text-on-surface text-label-md truncate">${batch.batch_code || batch.id}</p>
<p class="text-[11px] text-on-surface-variant">${api.formatDateTime(batch.created_at)} · ${Number(batch.entry_count || 0).toLocaleString("id-ID")} item</p>
</div>
<div class="text-right shrink-0">
<p class="font-bold text-label-md">${api.formatMoney(batch.net_payable_amount || 0)}</p>
<div class="flex items-center justify-end gap-2 mt-1">
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-bold ${badgeClass}">${String(batch.status || "-").toUpperCase()}</span>
<button class="finance-export-batch text-primary text-[11px] font-bold hover:underline" data-batch-id="${batch.id}" data-batch-code="${batch.batch_code || batch.id}">CSV</button>
</div>
</div>
</div>
`;
}).join("");
financeRecentBatches.querySelectorAll(".finance-export-batch").forEach((button) => {
button.addEventListener("click", () => downloadSettlementCsv(button.dataset.batchId, button.dataset.batchCode));
});
};
const downloadSettlementCsv = async (batchId, batchCode) => {
if (!batchId) return;
const token = api.requireToken();
const response = await fetch(`/admin/settlement-batches/${encodeURIComponent(batchId)}/export.csv`, {
headers: { Authorization: `Bearer ${token}` }
});
if (!response.ok) {
throw new Error(`CSV download failed with status ${response.status}`);
}
const blob = await response.blob();
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = `${batchCode || batchId}-payout-report.csv`;
document.body.appendChild(link);
link.click();
link.remove();
URL.revokeObjectURL(url);
};
const alertCard = ({ severity, icon, title, detail, time, actionHref }) => {
const styles = severity === "critical"
? { box: "bg-error/5 border-error/10", icon: "text-error" }
: severity === "warning"
? { box: "bg-warning/5 border-warning/10", icon: "text-warning" }
: { box: "bg-info/5 border-info/10", icon: "text-info" };
return `
<div class="p-4 rounded-lg ${styles.box} border">
<div class="flex gap-3">
<span class="material-symbols-outlined ${styles.icon}">${icon}</span>
<div class="flex-1">
<p class="font-bold text-on-surface text-body-md">${title}</p>
<p class="text-label-md text-on-surface-variant mb-2">${detail}</p>
<div class="flex items-center justify-between">
<span class="text-label-md text-slate-400">${time}</span>
<a class="text-primary font-bold text-label-md" href="${actionHref || "/ui/device-registry-monitoring"}">Open</a>
</div>
</div>
</div>
</div>
`;
};
const renderAlerts = ({ devices, failedNotifications, mqttStatus }) => {
if (!alertsHost) return;
const alerts = [];
const publisherStatus = mqttStatus?.publisher || mqttStatus || {};
devices
.filter((device) => ["offline", "stale", "degraded"].includes(normalize(device.health_summary?.status || device.derived_status)))
.slice(0, 4)
.forEach((device) => {
const status = normalize(device.health_summary?.status || device.derived_status);
alerts.push({
severity: status === "offline" ? "critical" : "warning",
icon: status === "offline" ? "error" : "warning",
title: `${device.device_code || device.id} ${status.toUpperCase()}`,
detail: (device.health_summary?.reasons || []).join(", ") || `Last heartbeat: ${api.formatDateTime(device.latest_heartbeat?.timestamp)}`,
time: typeof device.health_summary?.age_seconds === "number" ? `${device.health_summary.age_seconds}s age` : "Device health",
actionHref: `/ui/device-technical-detail?device_id=${device.id}`
});
});
failedNotifications.slice(0, 3).forEach((notification) => {
alerts.push({
severity: "critical",
icon: "notifications_off",
title: "Failed notification",
detail: `${notification.transaction_id || "-"} ${notification.reason || ""}`,
time: api.formatDateTime(notification.created_at),
actionHref: "/ui/transaction-history-monitoring"
});
});
if (publisherStatus?.mode === "broker" && publisherStatus.connected !== true) {
alerts.push({
severity: "warning",
icon: "cell_tower",
title: "MQTT broker configured",
detail: "No active publisher session yet. It will connect on first publish.",
time: publisherStatus.broker_url || "broker mode",
actionHref: "/ui/device-registry-monitoring"
});
}
if (publisherStatus?.forced_fail_all || Number(publisherStatus?.forced_fail_device_count || 0) > 0) {
alerts.push({
severity: "critical",
icon: "error",
title: "MQTT forced failure enabled",
detail: `${publisherStatus.forced_fail_device_count || 0} device-specific forced failure rules`,
time: "Publisher config",
actionHref: "/ui/device-registry-monitoring"
});
}
const critical = alerts.filter((item) => item.severity === "critical").length;
if (alertCriticalCount) alertCriticalCount.textContent = `${critical} Critical`;
alertsHost.innerHTML = alerts.length
? alerts.slice(0, 8).map(alertCard).join("")
: '<div class="p-4 rounded-lg bg-success/5 border border-success/10 text-success font-bold">No active operational alerts</div>';
};
const renderAuditStream = (logs) => {
if (!auditStream) return;
const rows = (Array.isArray(logs) ? logs : []).slice(0, 6);
if (!rows.length) {
auditStream.innerHTML = '<p><span class="text-info">INFO:</span> No audit events available</p>';
return;
}
auditStream.innerHTML = rows.map((entry) => {
const action = entry.action || "audit.event";
const isSuccess = /create|approve|paid|ack|retry|bind|update|patch/.test(action);
return `<p><span class="text-primary-fixed-dim">[${api.formatDateTime(entry.created_at || entry.timestamp)}]</span> <span class="${isSuccess ? "text-success" : "text-info"}">${isSuccess ? "EVENT" : "INFO"}:</span> ${action} ${entry.entity_type || ""} ${entry.entity_id || ""}</p>`;
}).join("");
};
const load = async () => {
try {
api.requireToken();
const { from, to } = todayRange();
const [summary, merchants, devices, todayTx, allTx, failedNotifications, mqttStatus, expiryStatus, auditLogs, settlementBatches] = await Promise.all([
api.getDashboardSummary(),
api.listMerchants(),
api.listDevices(),
api.listTransactions({ from, to }),
api.listTransactions(),
api.listFailedNotifications({ limit: 10 }),
api.getMqttStatus({ limit: 5 }),
api.getDynamicQrExpiryScheduler(),
api.listAuditLogs({ limit: 6 }),
api.listSettlementBatches({ limit: 6 })
]);
renderPendingMerchants((merchants || []).filter((merchant) => merchant.onboarding_status === "pending"));
updateStats(summary, merchants || [], todayTx || [], devices || []);
renderFinanceSummary(summary, settlementBatches || []);
renderTrend(allTx || []);
renderMqttStatus(mqttStatus);
renderExpiryScheduler(expiryStatus);
renderAlerts({ devices: devices || [], failedNotifications: failedNotifications || [], mqttStatus });
renderAuditStream(auditLogs || []);
} catch (error) {
console.error("[admin-dashboard] failed loading data", error);
if (alertsHost) {
alertsHost.innerHTML = '<div class="p-4 rounded-lg bg-error/5 border border-error/10 text-error font-bold">Unable to load dashboard data</div>';
}
}
};
return { load };
})();
AdminDashboard.load();
// Micro-interactions for hovering and state visual feedback
document.querySelectorAll(".hover\\:shadow-lg").forEach((card) => {
card.addEventListener("mouseenter", () => {
card.style.transform = "translateY(-2px)";
card.style.transition = "all 0.3s ease";
});
card.addEventListener("mouseleave", () => {
card.style.transform = "translateY(0)";
});
});
</script>
<!-- ui-nav -->
<div id="__sb_nav" style="position:fixed;left:16px;bottom:16px;z-index:9999;background:#fff;border:1px solid #e2e8f0;padding:8px 10px;border-radius:8px;box-shadow:0 6px 24px rgba(15,23,42,0.12);font-family:Inter,Arial,sans-serif;font-size:12px;line-height:1.4">
<a href="/ui" style="margin-right:8px;color:#2563eb;text-decoration:none;font-weight:600">UI Catalog</a>
<a href="/ui/hub" style="margin-right:8px;color:#2563eb;text-decoration:none;font-weight:600">Hub</a>
<a href="/ui/admin-login" style="margin-right:8px;color:#2563eb;text-decoration:none;font-weight:600">Admin Login</a>
<a href="/ui/merchant-login" style="margin-right:8px;color:#2563eb;text-decoration:none;font-weight:600">Merchant Login</a>
<a href="/ui/admin-dashboard-overview" style="margin-right:0;color:#2563eb;text-decoration:none;font-weight:600">Dashboard</a>
</div>
'
</body></html>