import { createMemo, createSignal, For, onMount, Show } from 'solid-js'; import AdminShell from '~/components/AdminShell'; const API = '/api/gateway'; const LEGACY_INTERNAL_PREVIEW_BASE = String(import.meta.env.VITE_LEGACY_ADMIN_PREVIEW_URL || 'http://localhost:3002').replace(/\/+$/, ''); // ---------- 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; key: string; name: string }; const FIELD_TYPES: Field['type'][] = ['text', 'number', 'select', 'date']; const makeId = (prefix: string) => `${prefix}_${Math.random().toString(36).slice(2, 10)}`; function normalizeInternalDashboardFromConfig(base: Dashboard, configJson: any): Dashboard { const sidebarRaw = Array.isArray(configJson?.sidebar) ? configJson.sidebar : []; const sectionsRaw = Array.isArray(configJson?.sections) ? configJson.sections : []; const legacyNav = Array.isArray(configJson?.nav) ? configJson.nav : []; const sidebar: SidebarItem[] = sidebarRaw.length > 0 ? sidebarRaw.map((item: any, index: number) => ({ key: String(item?.key || makeId('sb')), label: String(item?.label || `Menu ${index + 1}`), visible: item?.visible !== false, order: Number(item?.order) || index + 1, })) : legacyNav.map((item: any, index: number) => ({ key: String(item?.key || makeId('sb')), label: String(item?.label || `Menu ${index + 1}`), visible: true, order: index + 1, })); const sections: Section[] = sectionsRaw.map((section: any, sectionIndex: number) => ({ id: String(section?.id || `section_${sectionIndex + 1}`), title: String(section?.title || `Section ${sectionIndex + 1}`), tabs: Array.isArray(section?.tabs) ? section.tabs.map((tab: any, tabIndex: number) => ({ id: String(tab?.id || `tab_${tabIndex + 1}`), title: String(tab?.title || `Tab ${tabIndex + 1}`), fields: Array.isArray(tab?.fields) ? tab.fields.map((field: any, fieldIndex: number) => ({ id: String(field?.id || `field_${fieldIndex + 1}`), label: String(field?.label || `Field ${fieldIndex + 1}`), type: field?.type === 'number' || field?.type === 'select' || field?.type === 'date' ? field.type : 'text', required: Boolean(field?.required), placeholder: String(field?.placeholder || ''), })) : [], })) : [], widgets: Array.isArray(section?.widgets) ? section.widgets.map((widget: any, widgetIndex: number) => ({ id: String(widget?.id || `widget_${widgetIndex + 1}`), title: String(widget?.title || `Widget ${widgetIndex + 1}`), metric: String(widget?.metric || '0'), description: String(widget?.description || ''), })) : [], })); return { ...base, title: String(configJson?.title || base.title || 'Internal Dashboard'), description: String(configJson?.description || ''), roleName: String(configJson?.roleName || base.roleName || ''), sidebar, sections, }; } // ---------- 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 [previewMode, setPreviewMode] = createSignal<'configured' | 'live'>('configured'); 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 || [])) .filter((item: any) => String(item?.audience || '').toUpperCase() === 'INTERNAL') .map((item: any) => ({ id: String(item.id || ''), roleId: String(item.role_id || ''), roleName: '', title: 'Internal Dashboard', description: '', status: item.is_active ? 'published' : 'draft', version: Number(item.version) || 1, sidebar: [], 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 || [])) .filter((r: any) => String(r?.audience || '').toUpperCase() === 'INTERNAL') .map((r: any) => ({ id: String(r.id || ''), key: String(r.key || ''), name: String(r.name || 'Internal Role') })), ); } catch { setRoles([]); } }; const hydrateDashboard = async (configId: string) => { const base = dashboards().find((item) => item.id === configId); if (!base || !base.roleId) return; try { const res = await fetch(`${API}/api/admin/dashboard-config/${base.roleId}?audience=INTERNAL`); if (!res.ok) return; const detail = await res.json(); const role = roles().find((item) => item.id === base.roleId); const hydrated = normalizeInternalDashboardFromConfig( { ...base, roleName: role?.name || base.roleName, status: detail?.is_active ? 'published' : base.status, version: Number(detail?.version) || base.version, }, detail?.config_json || {}, ); setDashboards((prev) => prev.map((item) => (item.id === configId ? hydrated : item))); } catch { // Keep list-summary data if detail fetch fails. } }; const openDashboard = async (configId: string) => { setSelectedId(configId); setActiveTab('overview'); await hydrateDashboard(configId); }; const selected = () => dashboards().find((d) => d.id === selectedId()) || null; const livePreviewUrl = createMemo(() => { const roleId = String(selected()?.roleId || '').trim(); if (!roleId) return `${LEGACY_INTERNAL_PREVIEW_BASE}/internal-dashboard-management`; return `${LEGACY_INTERNAL_PREVIEW_BASE}/internal-dashboard-management?roleId=${encodeURIComponent(roleId)}`; }); const livePreviewRoleLabel = createMemo(() => String(selected()?.roleName || '').trim() || 'Unlinked Role'); 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'); const defaultRole = roles()[0]; try { if (defaultRole?.id) { const res = await fetch(`${API}/api/admin/dashboard-config`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ role_id: defaultRole.id, audience: 'INTERNAL', config_json: { title: `${defaultRole.name} Dashboard`, description: '', roleName: defaultRole.name, version: 1, sidebar: [], sections: [], nav: [] }, }), }); if (res.ok) { const data = await res.json(); newId = data.id; } } } catch { /* backend unavailable — use local draft */ } const nd: Dashboard = { id: newId, roleId: defaultRole?.id || '', roleName: defaultRole?.name || '', title: `${defaultRole?.name || '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; if (!d.roleId) { setError('Please select an internal role before saving.'); return; } try { setSaving(true); setError(''); const nav = d.sidebar .filter((item) => item.visible) .sort((a, b) => a.order - b.order) .map((item) => ({ key: item.key, label: item.label, path: '/internal-dashboard-management', })); const res = await fetch(`${API}/api/admin/dashboard-config`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ role_id: d.roleId, audience: 'INTERNAL', config_json: { title: d.title, description: d.description, roleName: d.roleName, version: d.version, sidebar: d.sidebar, sections: d.sections, nav }, }), }); if (!res.ok) throw new Error('Failed to save dashboard'); await loadDashboards(); const next = dashboards().find((item) => item.roleId === d.roleId); if (next) await openDashboard(next.id); } catch (err: any) { setError(err.message || 'Failed to save dashboard'); } finally { setSaving(false); } }; // ---------- List view ---------- return (

Internal Dashboard Management

Open one internal dashboard at a time from the list below and edit it using simple tabs.

Back to Dashboard

Internal Dashboard List

Choose one internal dashboard to open in the builder, or create a new dashboard for an internal role.

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

Internal Dashboard Builder

Manage menu items, sections, tabs, form fields, and summary cards 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 a team role to connect this dashboard.'}

{/* Sidebar */}

Menu

{(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 menu items yet. Add your first menu item.

{/* Sections, Tabs & Fields */}

Sections

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

Tabs and Form 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="Help text inside the input" 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" />