import { createMemo, createSignal, For, onMount, Show } from 'solid-js'; import AdminShell from '~/components/AdminShell'; const API = '/api/gateway'; // ---------- Types ---------- type SidebarItem = { key: string; label: string; visible: boolean; order: number }; type Field = { id: string; label: string; type: 'text' | 'number' | 'select' | 'date'; required: boolean; placeholder?: string }; type Tab = { id: string; title: string; fields: Field[] }; type Widget = { id: string; title: string; metric: string; description?: string }; type Section = { id: string; title: string; tabs: Tab[]; widgets: Widget[] }; type Dashboard = { id: string; roleId: string; roleName: string; title: string; description?: string; status: string; version: number; sidebar: SidebarItem[]; sections: Section[] }; type InternalRole = { id: string; name: string }; const FIELD_TYPES: Field['type'][] = ['text', 'number', 'select', 'date']; const makeId = (prefix: string) => `${prefix}_${Math.random().toString(36).slice(2, 10)}`; // ---------- Preview ---------- function PreviewSection(props: { section: Section }) { const [activeTabId, setActiveTabId] = createSignal(props.section.tabs[0]?.id || ''); const activeTab = createMemo(() => props.section.tabs.find((t) => t.id === activeTabId()) || props.section.tabs[0] || null); return (
{props.section.title}

Preview tabs, fields, and widgets.

0}>
{props.section.tabs.map((tab) => ( ))}
0}>
{activeTab()!.fields.map((field) => (
{field.type === 'select' ? : }
))}
Add fields to this tab to preview.
0}>
{props.section.widgets.map((w) => (
{w.title}
{w.metric}
{w.description || 'Widget description'}
))}
); } function DashboardPreview(props: { dashboard: Dashboard }) { const visibleSidebar = createMemo(() => [...props.dashboard.sidebar].filter((i) => i.visible).sort((a, b) => a.order - b.order) ); const [activeSidebarKey, setActiveSidebarKey] = createSignal(''); const selectedLabel = createMemo(() => visibleSidebar().find((i) => i.key === activeSidebarKey())?.label || 'Overview'); return (

{props.dashboard.title}

{props.dashboard.roleName}

A

Dashboard Preview

{selectedLabel()}

{props.dashboard.description || 'Preview of the internal dashboard layout.'}

{(section) => }
Add sections, tabs, fields, and widgets to preview here.
); } // ---------- Main Page ---------- export default function InternalDashboardManagementPage() { const [dashboards, setDashboards] = createSignal([]); const [roles, setRoles] = createSignal([]); const [selectedId, setSelectedId] = createSignal(''); const [activeTab, setActiveTab] = createSignal<'overview' | 'sidebar' | 'sections' | 'preview'>('overview'); const [loading, setLoading] = createSignal(true); const [saving, setSaving] = createSignal(false); const [creating, setCreating] = createSignal(false); const [error, setError] = createSignal(''); onMount(async () => { await Promise.all([loadDashboards(), loadRoles()]); }); const loadDashboards = async () => { try { setLoading(true); setError(''); const res = await fetch(`${API}/api/admin/dashboard-config?audience=INTERNAL`); if (!res.ok) throw new Error('Failed to load internal dashboards'); const data = await res.json(); const rows = (Array.isArray(data) ? data : (data.dashboards || [])).map((item: any) => ({ id: item.id, roleId: item.role_id || '', roleName: item.config_json?.roleName || '', title: item.config_json?.title || 'Untitled Dashboard', description: item.config_json?.description || '', status: item.is_active ? 'published' : 'draft', version: item.config_json?.version || 1, sidebar: item.config_json?.sidebar || [], sections: item.config_json?.sections || [], })); setDashboards(rows); } catch (err: any) { setError(err.message || 'Failed to load dashboards'); } finally { setLoading(false); } }; const loadRoles = async () => { try { const res = await fetch(`${API}/api/admin/roles?audience=INTERNAL`); if (!res.ok) return; const data = await res.json(); setRoles((Array.isArray(data) ? data : (data.roles || [])).map((r: any) => ({ id: r.id, name: r.name }))); } catch { setRoles([]); } }; const selected = () => dashboards().find((d) => d.id === selectedId()) || null; const update = (patch: Partial) => setDashboards((prev) => prev.map((d) => d.id === selectedId() ? { ...d, ...patch } : d)); const updateSection = (sectionId: string, patch: Partial
) => update({ sections: selected()!.sections.map((s) => s.id === sectionId ? { ...s, ...patch } : s) }); const updateTab = (sectionId: string, tabId: string, patch: Partial) => { const section = selected()!.sections.find((s) => s.id === sectionId)!; updateSection(sectionId, { tabs: section.tabs.map((t) => t.id === tabId ? { ...t, ...patch } : t) }); }; const updateField = (sId: string, tId: string, fId: string, patch: Partial) => { const tab = selected()!.sections.find((s) => s.id === sId)!.tabs.find((t) => t.id === tId)!; updateTab(sId, tId, { fields: tab.fields.map((f) => f.id === fId ? { ...f, ...patch } : f) }); }; const updateWidget = (sId: string, wId: string, patch: Partial) => { const section = selected()!.sections.find((s) => s.id === sId)!; updateSection(sId, { widgets: section.widgets.map((w) => w.id === wId ? { ...w, ...patch } : w) }); }; const addSidebarItem = () => { const items = selected()!.sidebar; update({ sidebar: [...items, { key: makeId('sb'), label: 'New Sidebar Item', visible: true, order: items.length + 1 }] }); }; const removeSidebarItem = (key: string) => update({ sidebar: selected()!.sidebar.filter((i) => i.key !== key).map((i, idx) => ({ ...i, order: idx + 1 })) }); const addSection = () => update({ sections: [...selected()!.sections, { id: makeId('sec'), title: 'New Section', tabs: [], widgets: [] }] }); const removeSection = (id: string) => update({ sections: selected()!.sections.filter((s) => s.id !== id) }); const addTab = (sId: string) => { const section = selected()!.sections.find((s) => s.id === sId)!; updateSection(sId, { tabs: [...section.tabs, { id: makeId('tab'), title: 'New Tab', fields: [] }] }); }; const removeTab = (sId: string, tId: string) => { const section = selected()!.sections.find((s) => s.id === sId)!; updateSection(sId, { tabs: section.tabs.filter((t) => t.id !== tId) }); }; const addField = (sId: string, tId: string) => { const tab = selected()!.sections.find((s) => s.id === sId)!.tabs.find((t) => t.id === tId)!; updateTab(sId, tId, { fields: [...tab.fields, { id: makeId('fld'), label: 'New Field', type: 'text', required: false, placeholder: 'Enter value' }] }); }; const removeField = (sId: string, tId: string, fId: string) => { const tab = selected()!.sections.find((s) => s.id === sId)!.tabs.find((t) => t.id === tId)!; updateTab(sId, tId, { fields: tab.fields.filter((f) => f.id !== fId) }); }; const addWidget = (sId: string) => { const section = selected()!.sections.find((s) => s.id === sId)!; updateSection(sId, { widgets: [...section.widgets, { id: makeId('wgt'), title: 'New Widget', metric: '0', description: 'Describe this widget.' }] }); }; const removeWidget = (sId: string, wId: string) => { const section = selected()!.sections.find((s) => s.id === sId)!; updateSection(sId, { widgets: section.widgets.filter((w) => w.id !== wId) }); }; const createDashboard = async () => { try { setCreating(true); setError(''); let newId = makeId('local'); try { const res = await fetch(`${API}/api/admin/dashboard-config`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ audience: 'INTERNAL', config_json: { title: 'New Internal Dashboard', version: 1, sidebar: [], sections: [] } }), }); if (res.ok) { const data = await res.json(); newId = data.id; } // If POST fails, fall through to local draft mode silently } catch { /* backend unavailable — use local draft */ } const nd: Dashboard = { id: newId, roleId: '', roleName: '', title: 'New Internal Dashboard', status: 'draft', version: 1, sidebar: [], sections: [] }; setDashboards((prev) => [nd, ...prev]); setSelectedId(newId); setActiveTab('overview'); } finally { setCreating(false); } }; const saveSelected = async () => { const d = selected(); if (!d) return; try { setSaving(true); setError(''); const res = await fetch(`${API}/api/admin/dashboard-config/${d.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ role_id: d.roleId || null, config_json: { title: d.title, description: d.description, roleName: d.roleName, version: d.version, sidebar: d.sidebar, sections: d.sections } }), }); if (!res.ok) throw new Error('Failed to save dashboard'); } catch (err: any) { setError(err.message || 'Failed to save dashboard'); } finally { setSaving(false); } }; // ---------- List view ---------- return (

Internal Dashboard Management

Build and manage internal role dashboards — sidebar, sections, tabs, fields, and widgets.

{error()}
{(d) => ( )}
Role Dashboard Status Version Action
Loading internal dashboards...
No internal dashboards found. Create the first one.
{d.roleName || 'Not linked'} {d.title} {d.status} v{d.version}
{/* ---------- Builder view ---------- */}

Internal Dashboard Builder

Manage sidebar, sections, tabs, fields, and widgets from one place.

{error()}
{/* Tab bar */}
{(['overview', 'sidebar', 'sections', 'preview'] as const).map((t) => ( ))}
{/* Overview */}
update({ title: e.currentTarget.value })} />
update({ description: e.currentTarget.value })} />

Linked Role

{selected()!.roleName || 'No role selected yet'}

{selected()!.roleId || 'Select an internal role to bind this dashboard.'}

{/* Sidebar */}

Internal Sidebar

{(item, idx) => (
update({ sidebar: selected()!.sidebar.map((i) => i.key === item.key ? { ...i, label: e.currentTarget.value } : i) })} /> update({ sidebar: selected()!.sidebar.map((i) => i.key === item.key ? { ...i, order: Number(e.currentTarget.value) || idx() + 1 } : i) })} style="width:80px" />
)}

No sidebar items yet. Add the first item.

{/* Sections, Tabs & Fields */}

Sections

{(section) => (
updateSection(section.id, { title: e.currentTarget.value })} placeholder="Section title" />
{/* Tabs */}

Tabs and Fields

{(tab) => (
updateTab(section.id, tab.id, { title: e.currentTarget.value })} placeholder="Tab title" />
{(field) => (
updateField(section.id, tab.id, field.id, { label: e.currentTarget.value })} placeholder="Field label" style="width:100%;margin-bottom:4px" /> updateField(section.id, tab.id, field.id, { placeholder: e.currentTarget.value })} placeholder="Placeholder text" style="width:100%" />
)}

No fields in this tab yet.

)}

No tabs yet. Add a tab above.

{/* Widgets */}

Widgets

{(widget) => (
updateWidget(section.id, widget.id, { title: e.currentTarget.value })} placeholder="Widget title" /> updateWidget(section.id, widget.id, { metric: e.currentTarget.value })} placeholder="Metric value e.g. 42" />