import { For, Show, createEffect, createMemo, createSignal, onCleanup, onMount, createResource } from 'solid-js'; import { useSearchParams } from '@solidjs/router'; import { BarChart3, Building2, CircleDashed, Coins, EllipsisVertical, Eye, EyeOff, GripVertical, LineChart, RotateCcw, Search, Settings2, TrendingUp, Users, } from 'lucide-solid'; import { ADMIN_DASHBOARD_WIDGETS, type DashboardWidgetDefinition, type DashboardWidgetSize, } from '~/lib/admin/dashboard'; import type { RuntimeDashboardLayout } from '~/lib/runtime/types'; import { loadAdminDashboardLayout, saveAdminDashboardLayout } from '~/lib/runtime/storage'; const API = '/api/gateway'; async function fetchMetrics() { const accessToken = typeof sessionStorage !== 'undefined' ? (sessionStorage.getItem('nxtgauge_admin_access_token') || '').trim() : ''; const res = await fetch(`${API}/api/admin/dashboard/metrics`, { headers: { Accept: 'application/json', ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}), }, credentials: 'include', }); if (!res.ok) throw new Error('Failed to load metrics'); return res.json(); } type WidgetStateKind = 'live' | 'empty' | 'pending'; type WidgetType = 'summary' | 'analytics'; type SortMode = 'layout' | 'name' | 'status'; type FilterMode = 'all' | WidgetType; type GridLayoutMode = '3x4' | '3x3'; type WidgetMeta = { state: WidgetStateKind; type: WidgetType; statusLabel: string; subtitle: string; emptyMessage?: string; pendingMessage?: string; }; const WIDGET_DEFINITION_MAP = Object.fromEntries( ADMIN_DASHBOARD_WIDGETS.map((definition) => [definition.widgetKey, definition]) ); const DEFAULT_LAYOUT: RuntimeDashboardLayout = { order: ADMIN_DASHBOARD_WIDGETS.slice() .sort((a, b) => a.order - b.order) .map((definition) => definition.widgetKey), visibility: Object.fromEntries(ADMIN_DASHBOARD_WIDGETS.map((definition) => [definition.widgetKey, definition.defaultVisible])), size: Object.fromEntries(ADMIN_DASHBOARD_WIDGETS.map((definition) => [definition.widgetKey, definition.defaultSize])), }; const WIDGET_META: Record = { kpi_total_users: { state: 'empty', type: 'summary', statusLabel: 'No Data', subtitle: 'Powered by USER_MANAGEMENT', emptyMessage: 'No user data available yet', }, kpi_active_companies: { state: 'empty', type: 'summary', statusLabel: 'No Data', subtitle: 'Powered by COMPANY_MANAGEMENT', emptyMessage: 'No company data available yet', }, kpi_open_leads: { state: 'empty', type: 'summary', statusLabel: 'No Data', subtitle: 'Powered by REQUIREMENTS_MANAGEMENT', emptyMessage: 'No lead data available yet', }, kpi_credits_purchased: { state: 'empty', type: 'summary', statusLabel: 'No Data', subtitle: 'Powered by CREDIT_MANAGEMENT', emptyMessage: 'No credit activity available yet', }, chart_leads_trend: { state: 'pending', type: 'analytics', statusLabel: 'Module Pending', subtitle: 'Monthly leads performance overview • Powered by REPORTS', pendingMessage: 'This widget will connect to live reporting data once the Reports module is completed', }, chart_revenue_overview: { state: 'pending', type: 'analytics', statusLabel: 'Module Pending', subtitle: 'Monthly revenue vs expenses comparison • Powered by REVENUE_LEDGER', pendingMessage: 'This widget will connect to ledger-based analytics once the Revenue Ledger module is completed', }, }; function sanitizeLayout(layout: RuntimeDashboardLayout | null | undefined): RuntimeDashboardLayout { const knownKeys = new Set(ADMIN_DASHBOARD_WIDGETS.map((definition) => definition.widgetKey)); const fallbackOrder = DEFAULT_LAYOUT.order; const incomingOrder = Array.isArray(layout?.order) ? layout!.order : []; const seen = new Set(); const normalizedOrder: string[] = []; for (const key of incomingOrder) { const normalizedKey = String(key || ''); if (!knownKeys.has(normalizedKey) || seen.has(normalizedKey)) continue; seen.add(normalizedKey); normalizedOrder.push(normalizedKey); } for (const key of fallbackOrder) { if (!seen.has(key)) normalizedOrder.push(key); } const visibility: Record = {}; const size: Record = {}; for (const definition of ADMIN_DASHBOARD_WIDGETS) { const key = definition.widgetKey; visibility[key] = typeof layout?.visibility?.[key] === 'boolean' ? Boolean(layout?.visibility?.[key]) : definition.defaultVisible; const rawSize = String(layout?.size?.[key] || '').toUpperCase(); size[key] = rawSize === 'S' || rawSize === 'M' || rawSize === 'L' ? (rawSize as DashboardWidgetSize) : definition.defaultSize; } return { order: normalizedOrder, visibility, size, }; } function reorderKeys(order: string[], draggedKey: string, targetKey: string): string[] { if (draggedKey === targetKey) return order; const next = order.slice(); const fromIndex = next.indexOf(draggedKey); const toIndex = next.indexOf(targetKey); if (fromIndex === -1 || toIndex === -1) return order; next.splice(fromIndex, 1); next.splice(toIndex, 0, draggedKey); return next; } function iconForWidget(widgetKey: string) { const cls = 'text-[#FA5014]'; if (widgetKey.includes('users')) return ; if (widgetKey.includes('companies')) return ; if (widgetKey.includes('leads')) return ; if (widgetKey.includes('credits')) return ; if (widgetKey.includes('revenue')) return ; return ; } function badgeClass(state: WidgetStateKind): string { if (state === 'live') return 'border-[#FDBA8C] bg-[#FFF1EB] text-[#FA5014]'; if (state === 'pending') return 'border-[#D1D5DB] bg-[#F3F4F6] text-[#6B7280]'; return 'border-[#E5E7EB] bg-white text-[#6B7280]'; } function widgetSpan(mode: GridLayoutMode): string { return mode === '3x3' ? 'xl:col-span-4' : 'xl:col-span-3'; } function EmptyPreview() { return (
--
); } function PendingPreview() { return (
); } function LivePreview(props: { value?: string; trend?: string; trendUp?: boolean }) { return (

{props.value || '0'}

{props.trendUp ? '↗' : '↘'} {props.trend || '0%'}

); } export default function AdminHomePage() { const [searchParams] = useSearchParams(); const [layout, setLayout] = createSignal(sanitizeLayout(DEFAULT_LAYOUT)); const [settingsOpen, setSettingsOpen] = createSignal(false); const [isHydrating, setIsHydrating] = createSignal(true); const [isAutoSaving, setIsAutoSaving] = createSignal(false); const [autoSaveNotice, setAutoSaveNotice] = createSignal(''); const [lastSavedSnapshot, setLastSavedSnapshot] = createSignal(''); const [draggingKey, setDraggingKey] = createSignal(null); const [openMenuId, setOpenMenuId] = createSignal(null); const [search, setSearch] = createSignal(''); const [filterMode, setFilterMode] = createSignal('all'); const [sortMode, setSortMode] = createSignal('layout'); const [gridLayout, setGridLayout] = createSignal('3x4'); const [metrics] = createResource(fetchMetrics); const getWidgetState = (key: string) => { if (key.startsWith('kpi_')) { const idMap: Record = { kpi_total_users: 'users', kpi_active_companies: 'companies', kpi_open_leads: 'leads', kpi_credits_purchased: 'credits', }; if (metrics.loading) return { state: 'pending', statusLabel: 'Loading...' }; const m = metrics()?.kpis?.find((k: any) => k.id === idMap[key]); if (m) return { state: 'live', statusLabel: 'Live Data', data: m }; return { state: 'empty', statusLabel: 'No Data' }; } const meta = WIDGET_META[key]; return { state: meta?.state || 'empty', statusLabel: meta?.statusLabel || 'No Data' }; }; const orderedWidgets = createMemo(() => { const current = layout(); const rows = current.order .map((key) => WIDGET_DEFINITION_MAP[key]) .filter((definition): definition is DashboardWidgetDefinition => Boolean(definition)) .filter((definition) => current.visibility[definition.widgetKey] !== false) .filter((definition) => { const query = search().trim().toLowerCase(); if (!query) return true; const meta = WIDGET_META[definition.widgetKey]; return definition.title.toLowerCase().includes(query) || definition.moduleKey.toLowerCase().includes(query) || (meta?.subtitle || '').toLowerCase().includes(query); }) .filter((definition) => { const mode = filterMode(); if (mode === 'all') return true; return (WIDGET_META[definition.widgetKey]?.type || 'summary') === mode; }); const mode = sortMode(); if (mode === 'layout') return rows; const next = rows.slice(); next.sort((a, b) => { if (mode === 'name') return a.title.localeCompare(b.title); const rank = (key: string) => { const info = getWidgetState(key); if (info.state === 'live') return 1; if (info.state === 'empty') return 2; return 3; }; return rank(a.widgetKey) - rank(b.widgetKey); }); return next; }); onMount(async () => { try { const persistedLayout = await loadAdminDashboardLayout(); const normalized = sanitizeLayout(persistedLayout || DEFAULT_LAYOUT); setLayout(normalized); setLastSavedSnapshot(JSON.stringify(normalized)); } finally { setIsHydrating(false); } }); createEffect(() => { if (isHydrating()) return; const nextLayout = layout(); const nextSnapshot = JSON.stringify(nextLayout); if (nextSnapshot === lastSavedSnapshot()) return; setIsAutoSaving(true); setAutoSaveNotice(''); const timer = setTimeout(async () => { const ok = await saveAdminDashboardLayout(nextLayout); setIsAutoSaving(false); if (ok) { setLastSavedSnapshot(nextSnapshot); setAutoSaveNotice('Layout saved automatically.'); } else { setAutoSaveNotice('Auto-save failed. Please try again.'); } }, 450); onCleanup(() => clearTimeout(timer)); }); const setWidgetVisibility = (widgetKey: string, visible: boolean) => { setLayout((current) => ({ ...current, visibility: { ...current.visibility, [widgetKey]: visible, }, })); }; const resetLayout = () => { const normalized = sanitizeLayout(DEFAULT_LAYOUT); setLayout(normalized); setAutoSaveNotice('Layout reset to default.'); }; const handleDragStart = (event: DragEvent, widgetKey: string) => { event.dataTransfer?.setData('text/plain', widgetKey); if (event.dataTransfer) event.dataTransfer.effectAllowed = 'move'; setDraggingKey(widgetKey); }; const handleDrop = (event: DragEvent, targetKey: string) => { event.preventDefault(); const dragged = event.dataTransfer?.getData('text/plain') || draggingKey(); if (!dragged) return; setLayout((current) => ({ ...current, order: reorderKeys(current.order, dragged, targetKey), })); setDraggingKey(null); }; return (

You don’t have access to {String(searchParams.denied || '').replace(/_/g, ' ')}.

Dashboard Overview

Manage widget layout, visibility, sizing, and dashboard presentation

{/* Settings header */}

Widget Settings

Choose visible widgets and select a grid layout.

{/* Filter controls */}
setSearch(event.currentTarget.value)} placeholder="Search widgets" class="h-10 w-full rounded-lg border border-[#E5E7EB] bg-white pl-10 pr-3 text-sm text-[#111827] outline-none focus:border-[#FA5014]" />
{/* Widget list */}
{(definition) => { const visible = () => layout().visibility[definition.widgetKey] !== false; const meta = WIDGET_META[definition.widgetKey]; const stateInfo = getWidgetState(definition.widgetKey); const state = stateInfo.state; return (
{iconForWidget(definition.widgetKey)}

{definition.title}

{definition.moduleKey}

{stateInfo.statusLabel}
); }}

{isAutoSaving() ? 'Saving layout...' : autoSaveNotice()}

{(definition) => { const meta = WIDGET_META[definition.widgetKey]; const stateInfo = getWidgetState(definition.widgetKey); const state = stateInfo.state; const isOpenMenu = () => openMenuId() === definition.widgetKey; return (
handleDragStart(event, definition.widgetKey)} onDragOver={(event) => event.preventDefault()} onDrop={(event) => handleDrop(event, definition.widgetKey)} onDragEnd={() => setDraggingKey(null)} class={`relative aspect-square min-h-[235px] rounded-2xl border border-[#E5E7EB] bg-white p-5 shadow-sm md:min-h-[260px] md:p-6 ${widgetSpan(gridLayout())} ${ state === 'pending' ? 'opacity-95' : '' } ${draggingKey() === definition.widgetKey ? 'opacity-60' : ''}`} > <>
{['Edit Widget', 'Duplicate Widget', 'Hide Widget', 'Remove Widget'].map((item) => ( ))}
{iconForWidget(definition.widgetKey)}

{definition.title}

{stateInfo.statusLabel}
); }}
); }