diff --git a/src/routes/admin/index.tsx b/src/routes/admin/index.tsx index bba0787..25500e7 100644 --- a/src/routes/admin/index.tsx +++ b/src/routes/admin/index.tsx @@ -1,5 +1,14 @@ -import { For, Show, createEffect, createMemo, createSignal, onCleanup, onMount, createResource } from 'solid-js'; -import { useSearchParams } from '@solidjs/router'; +import { + For, + Show, + createEffect, + createMemo, + createSignal, + onCleanup, + onMount, + createResource, +} from "solid-js"; +import { useSearchParams } from "@solidjs/router"; import { BarChart3, Building2, @@ -15,37 +24,38 @@ import { Settings2, TrendingUp, Users, -} from 'lucide-solid'; +} 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'; +} from "~/lib/admin/dashboard"; +import type { RuntimeDashboardLayout } from "~/lib/runtime/types"; +import { loadAdminDashboardLayout, saveAdminDashboardLayout } from "~/lib/runtime/storage"; -const API = ''; +const API = "/api"; async function fetchMetrics() { - const accessToken = typeof sessionStorage !== 'undefined' - ? (sessionStorage.getItem('nxtgauge_admin_access_token') || '').trim() - : ''; + 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', + Accept: "application/json", ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}), }, - credentials: 'include', + credentials: "include", }); - if (!res.ok) throw new Error('Failed to load metrics'); + 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 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; @@ -64,59 +74,63 @@ 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])), + 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: 'live', - type: 'summary', - statusLabel: 'Live Data', - subtitle: 'Powered by USER_MANAGEMENT', + state: "live", + type: "summary", + statusLabel: "Live Data", + subtitle: "Powered by USER_MANAGEMENT", }, kpi_active_companies: { - state: 'live', - type: 'summary', - statusLabel: 'Live Data', - subtitle: 'Powered by COMPANY_MANAGEMENT', + state: "live", + type: "summary", + statusLabel: "Live Data", + subtitle: "Powered by COMPANY_MANAGEMENT", }, kpi_open_leads: { - state: 'live', - type: 'summary', - statusLabel: 'Live Data', - subtitle: 'Powered by REQUIREMENTS_MANAGEMENT', + state: "live", + type: "summary", + statusLabel: "Live Data", + subtitle: "Powered by REQUIREMENTS_MANAGEMENT", }, kpi_pending_approvals: { - state: 'live', - type: 'summary', - statusLabel: 'Live Data', - subtitle: 'Powered by APPROVAL_MANAGEMENT', + state: "live", + type: "summary", + statusLabel: "Live Data", + subtitle: "Powered by APPROVAL_MANAGEMENT", }, kpi_total_revenue: { - state: 'live', - type: 'summary', - statusLabel: 'Live Data', - subtitle: 'Powered by REVENUE_LEDGER', + state: "live", + type: "summary", + statusLabel: "Live Data", + subtitle: "Powered by REVENUE_LEDGER", }, kpi_credits_purchased: { - state: 'empty', - type: 'summary', - statusLabel: 'No Data', - subtitle: 'Powered by CREDIT_MANAGEMENT', - emptyMessage: 'No credit activity available yet', + state: "empty", + type: "summary", + statusLabel: "No Data", + subtitle: "Powered by CREDIT_MANAGEMENT", + emptyMessage: "No credit activity available yet", }, chart_leads_trend: { - state: 'live', - type: 'analytics', - statusLabel: 'Live Data', - subtitle: 'Weekly leads performance overview • Powered by REPORTS', + state: "live", + type: "analytics", + statusLabel: "Live Data", + subtitle: "Weekly leads performance overview • Powered by REPORTS", }, chart_revenue_overview: { - state: 'live', - type: 'analytics', - statusLabel: 'Live Data', - subtitle: 'Weekly revenue overview • Powered by REVENUE_LEDGER', + state: "live", + type: "analytics", + statusLabel: "Live Data", + subtitle: "Weekly revenue overview • Powered by REVENUE_LEDGER", }, }; @@ -129,7 +143,7 @@ function sanitizeLayout(layout: RuntimeDashboardLayout | null | undefined): Runt const normalizedOrder: string[] = []; for (const key of incomingOrder) { - const normalizedKey = String(key || ''); + const normalizedKey = String(key || ""); if (!knownKeys.has(normalizedKey) || seen.has(normalizedKey)) continue; seen.add(normalizedKey); normalizedOrder.push(normalizedKey); @@ -144,14 +158,16 @@ function sanitizeLayout(layout: RuntimeDashboardLayout | null | undefined): Runt for (const definition of ADMIN_DASHBOARD_WIDGETS) { const key = definition.widgetKey; - visibility[key] = typeof layout?.visibility?.[key] === 'boolean' - ? Boolean(layout?.visibility?.[key]) - : definition.defaultVisible; + 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; + const rawSize = String(layout?.size?.[key] || "").toUpperCase(); + size[key] = + rawSize === "S" || rawSize === "M" || rawSize === "L" + ? (rawSize as DashboardWidgetSize) + : definition.defaultSize; } return { @@ -173,24 +189,24 @@ function reorderKeys(order: string[], draggedKey: string, targetKey: string): st } 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 ; - if (widgetKey.includes('approvals')) return ; + 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 ; + if (widgetKey.includes("approvals")) 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]'; + 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'; + return mode === "3x3" ? "xl:col-span-4" : "xl:col-span-3"; } function EmptyPreview() { @@ -218,9 +234,9 @@ function PendingPreview() { function LivePreview(props: { value?: string; trend?: string; trendUp?: boolean }) { return (
-

{props.value || '0'}

+

{props.value || "0"}

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

); @@ -232,47 +248,49 @@ export default function AdminHomePage() { const [settingsOpen, setSettingsOpen] = createSignal(false); const [isHydrating, setIsHydrating] = createSignal(true); const [isAutoSaving, setIsAutoSaving] = createSignal(false); - const [autoSaveNotice, setAutoSaveNotice] = createSignal(''); - const [lastSavedSnapshot, setLastSavedSnapshot] = createSignal(''); + 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 [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_')) { + if (key.startsWith("kpi_")) { const idMap: Record = { - kpi_total_users: 'users', - kpi_active_companies: 'companies', - kpi_open_leads: 'leads', - kpi_pending_approvals: 'approvals', - kpi_total_revenue: 'revenue', - kpi_credits_purchased: 'credits', + kpi_total_users: "users", + kpi_active_companies: "companies", + kpi_open_leads: "leads", + kpi_pending_approvals: "approvals", + kpi_total_revenue: "revenue", + kpi_credits_purchased: "credits", }; - if (metrics.loading) return { state: 'pending', statusLabel: 'Loading...' }; + 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' }; + if (m) return { state: "live", statusLabel: "Live Data", data: m }; + return { state: "empty", statusLabel: "No Data" }; } - if (metrics.loading) return { state: 'pending', statusLabel: 'Loading...' }; + if (metrics.loading) return { state: "pending", statusLabel: "Loading..." }; const m = metrics(); - if (key === 'chart_leads_trend') { + if (key === "chart_leads_trend") { const data = m?.trend_series; - if (data && data.length > 0) return { state: 'live', statusLabel: 'Live Data', data: { trend_series: data } }; - return { state: 'empty', statusLabel: 'No Data' }; + if (data && data.length > 0) + return { state: "live", statusLabel: "Live Data", data: { trend_series: data } }; + return { state: "empty", statusLabel: "No Data" }; } - if (key === 'chart_revenue_overview') { + if (key === "chart_revenue_overview") { const data = m?.rev_series; - if (data && data.length > 0) return { state: 'live', statusLabel: 'Live Data', data: { rev_series: data } }; - return { state: 'empty', statusLabel: 'No Data' }; + if (data && data.length > 0) + return { state: "live", statusLabel: "Live Data", data: { rev_series: data } }; + return { state: "empty", statusLabel: "No Data" }; } const meta = WIDGET_META[key]; - return { state: meta?.state || 'empty', statusLabel: meta?.statusLabel || 'No Data' }; + return { state: meta?.state || "empty", statusLabel: meta?.statusLabel || "No Data" }; }; const orderedWidgets = createMemo(() => { @@ -285,26 +303,28 @@ export default function AdminHomePage() { 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); + 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; + if (mode === "all") return true; + return (WIDGET_META[definition.widgetKey]?.type || "summary") === mode; }); const mode = sortMode(); - if (mode === 'layout') return rows; + if (mode === "layout") return rows; const next = rows.slice(); next.sort((a, b) => { - if (mode === 'name') return a.title.localeCompare(b.title); + 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; + if (info.state === "live") return 1; + if (info.state === "empty") return 2; return 3; }; return rank(a.widgetKey) - rank(b.widgetKey); @@ -331,16 +351,16 @@ export default function AdminHomePage() { if (nextSnapshot === lastSavedSnapshot()) return; setIsAutoSaving(true); - setAutoSaveNotice(''); + setAutoSaveNotice(""); const timer = setTimeout(async () => { const ok = await saveAdminDashboardLayout(nextLayout); setIsAutoSaving(false); if (ok) { setLastSavedSnapshot(nextSnapshot); - setAutoSaveNotice('Layout saved automatically.'); + setAutoSaveNotice("Layout saved automatically."); } else { - setAutoSaveNotice('Auto-save failed. Please try again.'); + setAutoSaveNotice("Auto-save failed. Please try again."); } }, 450); @@ -360,18 +380,18 @@ export default function AdminHomePage() { const resetLayout = () => { const normalized = sanitizeLayout(DEFAULT_LAYOUT); setLayout(normalized); - setAutoSaveNotice('Layout reset to default.'); + setAutoSaveNotice("Layout reset to default."); }; const handleDragStart = (event: DragEvent, widgetKey: string) => { - event.dataTransfer?.setData('text/plain', widgetKey); - if (event.dataTransfer) event.dataTransfer.effectAllowed = 'move'; + 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(); + const dragged = event.dataTransfer?.getData("text/plain") || draggingKey(); if (!dragged) return; setLayout((current) => ({ @@ -382,224 +402,254 @@ export default function AdminHomePage() { }; return ( -
- -
-

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

+
+ +
+

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

+
+
+
+
+
+

Dashboard Overview

+

+ Manage widget layout, visibility, sizing, and dashboard presentation +

- -
-
-
-

Dashboard Overview

-

Manage widget layout, visibility, sizing, and dashboard presentation

-
-
- -
+
+
+
- -
- {/* Settings header */} -
-
-

Widget Settings

-

Choose visible widgets and select a grid layout.

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

-
+
- -
- + {/* 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; - 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}

-
- - - - - - - - - +
+

{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} + +
+
+ ); + }} +
+
); } diff --git a/src/routes/admin/roles/create.tsx b/src/routes/admin/roles/create.tsx index 63ef240..b7366a3 100644 --- a/src/routes/admin/roles/create.tsx +++ b/src/routes/admin/roles/create.tsx @@ -203,6 +203,7 @@ export default function CreateInternalRolePage() { can_manage_system_settings: canManage(), permission_keys: [...selectedKeys()], }), + }); const raw = await res.text(); let message = ""; if (raw) {