Fix admin dashboard API endpoint and close missing bracket in create role form
This commit is contained in:
parent
95cfa352a8
commit
2c4e579725
2 changed files with 359 additions and 308 deletions
|
|
@ -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<string, WidgetMeta> = {
|
||||
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,12 +158,14 @@ 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'
|
||||
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'
|
||||
const rawSize = String(layout?.size?.[key] || "").toUpperCase();
|
||||
size[key] =
|
||||
rawSize === "S" || rawSize === "M" || rawSize === "L"
|
||||
? (rawSize as DashboardWidgetSize)
|
||||
: definition.defaultSize;
|
||||
}
|
||||
|
|
@ -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 <Users size={22} class={cls} />;
|
||||
if (widgetKey.includes('companies')) return <Building2 size={22} class={cls} />;
|
||||
if (widgetKey.includes('leads')) return <TrendingUp size={22} class={cls} />;
|
||||
if (widgetKey.includes('credits')) return <Coins size={22} class={cls} />;
|
||||
if (widgetKey.includes('revenue')) return <BarChart3 size={22} class={cls} />;
|
||||
if (widgetKey.includes('approvals')) return <CircleDashed size={22} class={cls} />;
|
||||
const cls = "text-[#FA5014]";
|
||||
if (widgetKey.includes("users")) return <Users size={22} class={cls} />;
|
||||
if (widgetKey.includes("companies")) return <Building2 size={22} class={cls} />;
|
||||
if (widgetKey.includes("leads")) return <TrendingUp size={22} class={cls} />;
|
||||
if (widgetKey.includes("credits")) return <Coins size={22} class={cls} />;
|
||||
if (widgetKey.includes("revenue")) return <BarChart3 size={22} class={cls} />;
|
||||
if (widgetKey.includes("approvals")) return <CircleDashed size={22} class={cls} />;
|
||||
return <LineChart size={22} class={cls} />;
|
||||
}
|
||||
|
||||
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 (
|
||||
<div class="flex h-full flex-col items-center justify-center">
|
||||
<p class="text-[26px] font-bold leading-none text-[#111827]">{props.value || '0'}</p>
|
||||
<p class="text-[26px] font-bold leading-none text-[#111827]">{props.value || "0"}</p>
|
||||
<p class="mt-2 inline-flex items-center gap-1 text-xs font-semibold text-[#FA5014]">
|
||||
{props.trendUp ? '↗' : '↘'} {props.trend || '0%'}
|
||||
{props.trendUp ? "↗" : "↘"} {props.trend || "0%"}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -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<string | null>(null);
|
||||
const [openMenuId, setOpenMenuId] = createSignal<string | null>(null);
|
||||
|
||||
const [search, setSearch] = createSignal('');
|
||||
const [filterMode, setFilterMode] = createSignal<FilterMode>('all');
|
||||
const [sortMode, setSortMode] = createSignal<SortMode>('layout');
|
||||
const [gridLayout, setGridLayout] = createSignal<GridLayoutMode>('3x4');
|
||||
const [search, setSearch] = createSignal("");
|
||||
const [filterMode, setFilterMode] = createSignal<FilterMode>("all");
|
||||
const [sortMode, setSortMode] = createSignal<SortMode>("layout");
|
||||
const [gridLayout, setGridLayout] = createSignal<GridLayoutMode>("3x4");
|
||||
|
||||
const [metrics] = createResource(fetchMetrics);
|
||||
|
||||
const getWidgetState = (key: string) => {
|
||||
if (key.startsWith('kpi_')) {
|
||||
if (key.startsWith("kpi_")) {
|
||||
const idMap: Record<string, string> = {
|
||||
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) => ({
|
||||
|
|
@ -384,15 +404,25 @@ export default function AdminHomePage() {
|
|||
return (
|
||||
<div class="w-full">
|
||||
<Show when={Boolean(searchParams.denied)}>
|
||||
<div class="rounded-2xl border border-[#E5E7EB] bg-white shadow-sm" style="margin-bottom: 18px">
|
||||
<p class="notice">You don’t have access to {String(searchParams.denied || '').replace(/_/g, ' ')}.</p>
|
||||
<div
|
||||
class="rounded-2xl border border-[#E5E7EB] bg-white shadow-sm"
|
||||
style="margin-bottom: 18px"
|
||||
>
|
||||
<p class="notice">
|
||||
You don’t have access to {String(searchParams.denied || "").replace(/_/g, " ")}.
|
||||
</p>
|
||||
</div>
|
||||
</Show>
|
||||
<div class="rounded-2xl border border-[#E5E7EB] bg-white px-6 py-5 shadow-sm md:px-8" style="margin-bottom: 28px">
|
||||
<div
|
||||
class="rounded-2xl border border-[#E5E7EB] bg-white px-6 py-5 shadow-sm md:px-8"
|
||||
style="margin-bottom: 28px"
|
||||
>
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<h1 class="text-[22px] font-bold leading-tight text-[#111827]">Dashboard Overview</h1>
|
||||
<p class="mt-0.5 text-[13px] text-[#6B7280]">Manage widget layout, visibility, sizing, and dashboard presentation</p>
|
||||
<p class="mt-0.5 text-[13px] text-[#6B7280]">
|
||||
Manage widget layout, visibility, sizing, and dashboard presentation
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex shrink-0">
|
||||
|
|
@ -406,7 +436,7 @@ export default function AdminHomePage() {
|
|||
}}
|
||||
>
|
||||
<Settings2 size={14} class="text-[#FA5014]" />
|
||||
{settingsOpen() ? 'Close Settings' : 'Customize Widgets'}
|
||||
{settingsOpen() ? "Close Settings" : "Customize Widgets"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -418,7 +448,9 @@ export default function AdminHomePage() {
|
|||
<div class="flex flex-wrap items-start justify-between gap-4">
|
||||
<div>
|
||||
<h2 class="text-[15px] font-semibold text-[#111827]">Widget Settings</h2>
|
||||
<p class="mt-1 text-[13px] text-[#6B7280]">Choose visible widgets and select a grid layout.</p>
|
||||
<p class="mt-1 text-[13px] text-[#6B7280]">
|
||||
Choose visible widgets and select a grid layout.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -433,7 +465,11 @@ export default function AdminHomePage() {
|
|||
{/* Filter controls */}
|
||||
<div class="mt-5 flex flex-wrap gap-3">
|
||||
<div class="relative min-w-[200px] flex-1">
|
||||
<Search size={14} class="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-[#FA5014]" style="z-index:1" />
|
||||
<Search
|
||||
size={14}
|
||||
class="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-[#FA5014]"
|
||||
style="z-index:1"
|
||||
/>
|
||||
<input
|
||||
value={search()}
|
||||
onInput={(event) => setSearch(event.currentTarget.value)}
|
||||
|
|
@ -489,24 +525,28 @@ export default function AdminHomePage() {
|
|||
</div>
|
||||
<div class="min-w-0">
|
||||
<p class="text-sm font-semibold text-[#111827]">{definition.title}</p>
|
||||
<p class="mt-0.5 text-[11px] font-medium uppercase tracking-wide text-[#9CA3AF]">{definition.moduleKey}</p>
|
||||
<p class="mt-0.5 text-[11px] font-medium uppercase tracking-wide text-[#9CA3AF]">
|
||||
{definition.moduleKey}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex shrink-0 items-center gap-2">
|
||||
<span class={`hidden rounded-full border px-2 py-0.5 text-[10px] font-semibold sm:inline-flex ${badgeClass(state as WidgetStateKind)}`}>
|
||||
<span
|
||||
class={`hidden rounded-full border px-2 py-0.5 text-[10px] font-semibold sm:inline-flex ${badgeClass(state as WidgetStateKind)}`}
|
||||
>
|
||||
{stateInfo.statusLabel}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
class={`inline-flex h-8 items-center gap-1.5 rounded-md border px-3 text-xs font-semibold ${
|
||||
visible()
|
||||
? 'border-[#FDBA8C] bg-[#FFF1EB] text-[#FA5014]'
|
||||
: 'border-[#E5E7EB] bg-white text-[#6B7280]'
|
||||
? "border-[#FDBA8C] bg-[#FFF1EB] text-[#FA5014]"
|
||||
: "border-[#E5E7EB] bg-white text-[#6B7280]"
|
||||
}`}
|
||||
onClick={() => setWidgetVisibility(definition.widgetKey, !visible())}
|
||||
>
|
||||
{visible() ? <Eye size={12} /> : <EyeOff size={12} />}
|
||||
{visible() ? 'Visible' : 'Hidden'}
|
||||
{visible() ? "Visible" : "Hidden"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -516,7 +556,9 @@ export default function AdminHomePage() {
|
|||
</div>
|
||||
|
||||
<Show when={isAutoSaving() || autoSaveNotice()}>
|
||||
<p class="mt-4 text-xs text-[#6B7280]">{isAutoSaving() ? 'Saving layout...' : autoSaveNotice()}</p>
|
||||
<p class="mt-4 text-xs text-[#6B7280]">
|
||||
{isAutoSaving() ? "Saving layout..." : autoSaveNotice()}
|
||||
</p>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
|
|
@ -537,8 +579,8 @@ export default function AdminHomePage() {
|
|||
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' : ''}`}
|
||||
state === "pending" ? "opacity-95" : ""
|
||||
} ${draggingKey() === definition.widgetKey ? "opacity-60" : ""}`}
|
||||
>
|
||||
<Show when={settingsOpen()}>
|
||||
<>
|
||||
|
|
@ -560,7 +602,8 @@ export default function AdminHomePage() {
|
|||
|
||||
<Show when={isOpenMenu()}>
|
||||
<div class="absolute right-0 top-10 z-20 w-40 rounded-lg border border-[#E5E7EB] bg-white p-1 shadow-md">
|
||||
{['Edit Widget', 'Duplicate Widget', 'Hide Widget', 'Remove Widget'].map((item) => (
|
||||
{["Edit Widget", "Duplicate Widget", "Hide Widget", "Remove Widget"].map(
|
||||
(item) => (
|
||||
<button
|
||||
type="button"
|
||||
class="block w-full rounded-md px-2 py-2 text-left text-xs font-medium text-[#111827] hover:bg-[#F9FAFB]"
|
||||
|
|
@ -568,7 +611,8 @@ export default function AdminHomePage() {
|
|||
>
|
||||
{item}
|
||||
</button>
|
||||
))}
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
|
@ -576,22 +620,28 @@ export default function AdminHomePage() {
|
|||
</Show>
|
||||
|
||||
<div class="flex h-full flex-col items-center justify-center px-4">
|
||||
<div class="mb-3 text-[#FA5014]">
|
||||
{iconForWidget(definition.widgetKey)}
|
||||
</div>
|
||||
<h3 class="text-center text-[15px] font-semibold text-[#111827]">{definition.title}</h3>
|
||||
<div class="mb-3 text-[#FA5014]">{iconForWidget(definition.widgetKey)}</div>
|
||||
<h3 class="text-center text-[15px] font-semibold text-[#111827]">
|
||||
{definition.title}
|
||||
</h3>
|
||||
<div class="mt-4 h-[116px] w-full max-w-[210px] rounded-xl border border-[#E5E7EB] bg-[#F9FAFB] p-4">
|
||||
<Show when={state === 'live'}>
|
||||
<LivePreview value={stateInfo.data?.value} trend={stateInfo.data?.trend} trendUp={stateInfo.data?.trendUp} />
|
||||
<Show when={state === "live"}>
|
||||
<LivePreview
|
||||
value={stateInfo.data?.value}
|
||||
trend={stateInfo.data?.trend}
|
||||
trendUp={stateInfo.data?.trendUp}
|
||||
/>
|
||||
</Show>
|
||||
<Show when={state === 'empty'}>
|
||||
<Show when={state === "empty"}>
|
||||
<EmptyPreview />
|
||||
</Show>
|
||||
<Show when={state === 'pending'}>
|
||||
<Show when={state === "pending"}>
|
||||
<PendingPreview />
|
||||
</Show>
|
||||
</div>
|
||||
<span class={`mt-4 inline-flex rounded-full border px-2 py-1 text-[11px] font-semibold ${badgeClass(state as WidgetStateKind)}`}>
|
||||
<span
|
||||
class={`mt-4 inline-flex rounded-full border px-2 py-1 text-[11px] font-semibold ${badgeClass(state as WidgetStateKind)}`}
|
||||
>
|
||||
{stateInfo.statusLabel}
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue