nxtgauge-admin-solid/src/routes/admin/internal-dashboard-management/index.tsx
Ashwin Kumar 33619a1b27 style: apply consistent page header pattern across all admin routes
- Replace text-2xl font-bold with text-xl font-semibold in all page headers
- Replace bg-[#fd6216] orange buttons with bg-[#0a1d37] navy buttons
- Wrap all pages in -mx-6 -mt-6 flex flex-col layout for edge-to-edge headers
- Replace .field/.actions CSS classes with explicit Tailwind utility classes
- Apply data-table/table-card shared CSS classes to remaining list pages
- Remove duplicate tab bar from roles/index.tsx (AdminShell TAB_SETS handles it)
- Move Create Internal Role button to page header in roles/index.tsx

Pages updated: applications, modules, responses, verification-status,
company/create, company/[id], employees/[id]/edit, users/[id]/edit,
users/details/[id], roles/index, roles/[id]/index, roles/[id]/edit,
role-ui-configs, runtime-roles/[roleKey], onboarding-schemas/*,
external/internal-dashboard-management, approval/[id], approval,
jobs/[id], leads/[id], photographer/[id], requirements/[id],
kb/articles/[id], kb/articles/[id]/edit, verification/[id],
verification-status/[id], help/[id], help/support-bridge

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 07:37:02 +01:00

747 lines
36 KiB
TypeScript

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 (
<div class="preview-section">
<h5 style="margin:0 0 4px;font-size:17px;font-weight:700;color:#050026">{props.section.title}</h5>
<p style="margin:0;font-size:13px;color:#64748b">Preview tabs, fields, and widgets.</p>
<Show when={props.section.tabs.length > 0}>
<div class="preview-tabs">
{props.section.tabs.map((tab) => (
<button type="button" class={`preview-tab-btn ${activeTabId() === tab.id ? 'active' : ''}`} onClick={() => setActiveTabId(tab.id)}>
{tab.title}
</button>
))}
</div>
</Show>
<Show when={activeTab()}>
<Show when={activeTab()!.fields.length > 0}>
<div class="preview-fields-grid">
{activeTab()!.fields.map((field) => (
<div class="preview-field">
<label>{field.label}{field.required ? <span style="color:#fd6216"> *</span> : null}</label>
{field.type === 'select'
? <select><option>{field.placeholder || `Select ${field.label}`}</option><option>Option A</option><option>Option B</option></select>
: <input type={field.type === 'number' ? 'number' : field.type === 'date' ? 'date' : 'text'} placeholder={field.placeholder || `Enter ${field.label}`} />}
</div>
))}
</div>
</Show>
<Show when={activeTab()!.fields.length === 0}>
<div style="margin-top:16px;border:1px dashed #cbd5e1;border-radius:12px;padding:24px;text-align:center;font-size:13px;color:#94a3b8">Add fields to this tab to preview.</div>
</Show>
</Show>
<Show when={props.section.widgets.length > 0}>
<div class="preview-widget-grid">
{props.section.widgets.map((w) => (
<div class="preview-widget">
<div class="w-label">{w.title}</div>
<div class="w-metric">{w.metric}</div>
<div class="w-desc">{w.description || 'Widget description'}</div>
</div>
))}
</div>
</Show>
</div>
);
}
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 (
<div class="preview-shell">
<div class="preview-header">
<div>
<h3 style="margin:0;font-size:17px;font-weight:700;color:#050026">{props.dashboard.title}</h3>
<p style="margin:2px 0 0;font-size:13px;color:#64748b">{props.dashboard.roleName}</p>
</div>
<div style="display:flex;align-items:center;gap:12px">
<div style="width:36px;height:36px;border-radius:999px;background:#fff1e8;display:flex;align-items:center;justify-content:center;color:#c2410c;font-weight:700;font-size:14px">A</div>
</div>
</div>
<div class="preview-layout">
<aside class="preview-sidebar">
{visibleSidebar().map((item) => (
<button
type="button"
class={`preview-sidebar-item ${activeSidebarKey() === item.key ? 'active' : ''}`}
onClick={() => setActiveSidebarKey(item.key)}
>
<span style="width:16px;height:16px;border-radius:4px;background:#cbd5e1;flex-shrink:0;display:inline-block" />
{item.label}
</button>
))}
<Show when={visibleSidebar().length === 0}>
<p style="font-size:12px;color:#94a3b8;padding:8px">No sidebar items added yet.</p>
</Show>
</aside>
<div class="preview-content">
<p style="margin:0;font-size:11px;font-weight:700;letter-spacing:.18em;text-transform:uppercase;color:#fd6216">Dashboard Preview</p>
<h4 style="margin:4px 0 4px;font-size:22px;font-weight:700;color:#050026">{selectedLabel()}</h4>
<p style="margin:0 0 16px;font-size:13px;color:#64748b">{props.dashboard.description || 'Preview of the internal dashboard layout.'}</p>
<For each={props.dashboard.sections}>
{(section) => <PreviewSection section={section} />}
</For>
<Show when={props.dashboard.sections.length === 0}>
<div style="border:1px dashed #cbd5e1;border-radius:12px;padding:32px;text-align:center;font-size:13px;color:#94a3b8">Add sections, tabs, fields, and widgets to preview here.</div>
</Show>
</div>
</div>
</div>
);
}
// ---------- Main Page ----------
export default function InternalDashboardManagementPage() {
const [dashboards, setDashboards] = createSignal<Dashboard[]>([]);
const [roles, setRoles] = createSignal<InternalRole[]>([]);
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<Dashboard>) =>
setDashboards((prev) => prev.map((d) => d.id === selectedId() ? { ...d, ...patch } : d));
const updateSection = (sectionId: string, patch: Partial<Section>) =>
update({ sections: selected()!.sections.map((s) => s.id === sectionId ? { ...s, ...patch } : s) });
const updateTab = (sectionId: string, tabId: string, patch: Partial<Tab>) => {
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<Field>) => {
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<Widget>) => {
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 (
<AdminShell>
<div class="flex flex-col -mx-6 -mt-6 min-h-full">
<div class="bg-white border-b border-gray-200 px-6 py-4 flex items-center justify-between">
<div>
<h1 class="text-xl font-semibold text-gray-900">Internal Dashboard Management</h1>
<p class="text-sm text-gray-500 mt-0.5">Open one internal dashboard at a time from the list below and edit it using simple tabs.</p>
</div>
<a class="rounded-lg border border-gray-200 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href="/admin">Back to Dashboard</a>
</div>
<div class="p-6 flex-1">
<div class="hidden" style="margin-bottom:14px">
<a class="hidden" href="#builder">View Dashboards</a>
<a class="hidden" href="#builder">Open Builder</a>
</div>
<div id="builder" />
<Show when={!selected()}>
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h2 class="text-lg font-semibold text-gray-900">Internal Dashboard List</h2>
<p class="mt-1 text-sm text-gray-500">Choose one internal dashboard to open in the builder, or create a new dashboard for an internal role.</p>
</div>
<button class="inline-flex items-center rounded-lg bg-[#0a1d37] px-4 py-2 text-sm font-medium text-white hover:bg-[#0f2a4e] transition-colors" onClick={createDashboard} disabled={creating()}>
{creating() ? 'Creating...' : 'Create Internal Dashboard'}
</button>
</div>
<Show when={error()}><div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{error()}</div></Show>
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="padding: 0; overflow: hidden;">
<div class="overflow-x-auto">
<table class="w-full text-sm list-table-soft-head">
<thead>
<tr>
<th>Role</th>
<th>Role ID</th>
<th>Dashboard</th>
<th>Status</th>
<th>Version</th>
<th class="text-right">Action</th>
</tr>
</thead>
<tbody>
<Show when={loading()}>
<tr><td colspan="6" style="text-align:center;padding:32px;color:#64748b">Loading internal dashboards...</td></tr>
</Show>
<Show when={!loading() && dashboards().length === 0}>
<tr><td colspan="6" style="text-align:center;padding:32px;color:#94a3b8">No internal dashboards found. Create the first one.</td></tr>
</Show>
<For each={dashboards()}>
{(d) => (
<tr>
<td style="color:#475569">{d.roleName || 'Not linked'}</td>
<td style="color:#475569">{d.roleId || 'Not linked'}</td>
<td style="font-weight:600;color:#0f172a">{d.title}</td>
<td><span class={`inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600 ${d.status === 'published' ? 'active' : ''}`}>{d.status}</span></td>
<td style="color:#64748b">v{d.version}</td>
<td>
<div class="flex items-center justify-end gap-1">
<button class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" onClick={() => void openDashboard(d.id)}>View Builder</button>
</div>
</td>
</tr>
)}
</For>
</tbody>
</table>
</div>
</section>
</Show>
{/* ---------- Builder view ---------- */}
<Show when={selected()}>
<div class="builder-header">
<div>
<h2>Internal Dashboard Builder</h2>
<p>Manage menu items, sections, tabs, form fields, and summary cards from one place.</p>
</div>
<div class="builder-header-actions">
<button class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" onClick={() => setSelectedId('')}>Back to List</button>
<button class="inline-flex items-center rounded-lg bg-[#0a1d37] px-4 py-2 text-sm font-medium text-white hover:bg-[#0f2a4e] transition-colors" onClick={saveSelected} disabled={saving()}>
{saving() ? 'Saving...' : 'Save Dashboard'}
</button>
</div>
</div>
<Show when={error()}><div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{error()}</div></Show>
{/* Tab bar */}
<div class="builder-tab-bar">
{(['overview', 'sidebar', 'sections', 'preview'] as const).map((t) => (
<button
type="button"
class={`builder-tab-btn ${activeTab() === t ? 'active' : ''}`}
onClick={() => setActiveTab(t)}
>
{t === 'sections' ? 'Sections, Tabs & Fields' : t.charAt(0).toUpperCase() + t.slice(1)}
</button>
))}
</div>
{/* Overview */}
<Show when={activeTab() === 'overview'}>
<div class="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2">
<div class="field">
<label>Team Role</label>
<select
value={selected()!.roleId}
onChange={(e) => {
const role = roles().find((r) => r.id === e.currentTarget.value);
update({ roleId: e.currentTarget.value, roleName: role?.name || '', title: role ? `${role.name} Dashboard` : selected()!.title });
}}
>
<option value="">Select team role</option>
{roles().map((r) => <option value={r.id}>{r.name}</option>)}
</select>
</div>
<div class="field">
<label>Dashboard Title</label>
<input value={selected()!.title} onInput={(e) => update({ title: e.currentTarget.value })} />
</div>
<div class="field">
<label>Short description</label>
<input value={selected()!.description || ''} onInput={(e) => update({ description: e.currentTarget.value })} />
</div>
<div class="info-box">
<p style="margin:0 0 4px;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.1em;color:#64748b">Linked Role</p>
<p style="margin:0;font-weight:600;color:#0f172a">{selected()!.roleName || 'No role selected yet'}</p>
<p style="margin:2px 0 0;font-size:12px;color:#64748b">{selected()!.roleId || 'Select a team role to connect this dashboard.'}</p>
</div>
</div>
</Show>
{/* Sidebar */}
<Show when={activeTab() === 'sidebar'}>
<div class="builder-section">
<div class="sub-card-header">
<h4>Menu</h4>
<button class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors orange" onClick={addSidebarItem}>Add Menu Item</button>
</div>
<For each={selected()!.sidebar}>
{(item, idx) => (
<div class="builder-item builder-item-row-4">
<input
value={item.label}
placeholder="Menu label"
onInput={(e) => update({ sidebar: selected()!.sidebar.map((i) => i.key === item.key ? { ...i, label: e.currentTarget.value } : i) })}
/>
<input
type="number"
value={item.order}
onInput={(e) => update({ sidebar: selected()!.sidebar.map((i) => i.key === item.key ? { ...i, order: Number(e.currentTarget.value) || idx() + 1 } : i) })}
style="width:80px"
/>
<label class="checkbox-label" style="justify-content:center">
<input
type="checkbox"
checked={item.visible}
onChange={(e) => update({ sidebar: selected()!.sidebar.map((i) => i.key === item.key ? { ...i, visible: e.currentTarget.checked } : i) })}
/>
Show
</label>
<button class="inline-flex items-center rounded-lg border border-red-200 bg-red-50 px-4 py-2 text-sm font-medium text-red-600 hover:bg-red-100 transition-colors" onClick={() => removeSidebarItem(item.key)}>Remove</button>
</div>
)}
</For>
<Show when={selected()!.sidebar.length === 0}>
<p class="notice">No menu items yet. Add your first menu item.</p>
</Show>
</div>
</Show>
{/* Sections, Tabs & Fields */}
<Show when={activeTab() === 'sections'}>
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px">
<h3 style="margin:0;font-size:15px;font-weight:700">Sections</h3>
<button class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors orange" onClick={addSection}>Add Section</button>
</div>
<For each={selected()!.sections}>
{(section) => (
<div class="builder-section">
<div class="builder-section-header">
<input
value={section.title}
onInput={(e) => updateSection(section.id, { title: e.currentTarget.value })}
placeholder="Section title"
/>
<button class="inline-flex items-center rounded-lg border border-red-200 bg-red-50 px-4 py-2 text-sm font-medium text-red-600 hover:bg-red-100 transition-colors" onClick={() => removeSection(section.id)}>Remove</button>
</div>
{/* Tabs */}
<div class="sub-card">
<div class="sub-card-header">
<h4>Tabs and Form Fields</h4>
<button class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors orange" onClick={() => addTab(section.id)}>Add Tab</button>
</div>
<For each={section.tabs}>
{(tab) => (
<div class="nested-card">
<div class="nested-card-header">
<input
value={tab.title}
onInput={(e) => updateTab(section.id, tab.id, { title: e.currentTarget.value })}
placeholder="Tab title"
/>
<button class="inline-flex items-center rounded-lg border border-red-200 bg-red-50 px-4 py-2 text-sm font-medium text-red-600 hover:bg-red-100 transition-colors" onClick={() => removeTab(section.id, tab.id)}>Remove</button>
</div>
<div style="display:flex;justify-content:flex-end;margin-bottom:8px">
<button class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors orange" onClick={() => addField(section.id, tab.id)}>Add Field</button>
</div>
<For each={tab.fields}>
{(field) => (
<div class="field-row">
<div>
<input
value={field.label}
onInput={(e) => updateField(section.id, tab.id, field.id, { label: e.currentTarget.value })}
placeholder="Field label"
style="width:100%;margin-bottom:4px"
/>
<input
value={field.placeholder || ''}
onInput={(e) => updateField(section.id, tab.id, field.id, { placeholder: e.currentTarget.value })}
placeholder="Help text inside the input"
style="width:100%"
/>
</div>
<select
value={field.type}
onChange={(e) => updateField(section.id, tab.id, field.id, { type: e.currentTarget.value as Field['type'] })}
>
{FIELD_TYPES.map((t) => <option value={t}>{t}</option>)}
</select>
<label class="checkbox-label" style="justify-content:center">
<input
type="checkbox"
checked={field.required}
onChange={(e) => updateField(section.id, tab.id, field.id, { required: e.currentTarget.checked })}
/>
Required
</label>
<button class="inline-flex items-center rounded-lg border border-red-200 bg-red-50 px-4 py-2 text-sm font-medium text-red-600 hover:bg-red-100 transition-colors" onClick={() => removeField(section.id, tab.id, field.id)}>Remove</button>
</div>
)}
</For>
<Show when={tab.fields.length === 0}>
<p class="notice" style="text-align:center">No fields in this tab yet.</p>
</Show>
</div>
)}
</For>
<Show when={section.tabs.length === 0}>
<p class="notice">No tabs yet. Add a tab above.</p>
</Show>
</div>
{/* Widgets */}
<div class="sub-card" style="margin-top:8px">
<div class="sub-card-header">
<h4>Widgets</h4>
<button class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors orange" onClick={() => addWidget(section.id)}>Add Widget</button>
</div>
<For each={section.widgets}>
{(widget) => (
<div class="widget-item">
<input value={widget.title} onInput={(e) => updateWidget(section.id, widget.id, { title: e.currentTarget.value })} placeholder="Widget title" />
<input value={widget.metric} onInput={(e) => updateWidget(section.id, widget.id, { metric: e.currentTarget.value })} placeholder="Metric value e.g. 42" />
<textarea rows={2} value={widget.description || ''} onInput={(e) => updateWidget(section.id, widget.id, { description: e.currentTarget.value })} placeholder="Widget description" />
<div style="display:flex;justify-content:flex-end">
<button class="inline-flex items-center rounded-lg border border-red-200 bg-red-50 px-4 py-2 text-sm font-medium text-red-600 hover:bg-red-100 transition-colors" style="font-size:12px;padding:5px 10px" onClick={() => removeWidget(section.id, widget.id)}>Remove Widget</button>
</div>
</div>
)}
</For>
<Show when={section.widgets.length === 0}>
<p class="notice">No widgets yet.</p>
</Show>
</div>
</div>
)}
</For>
<Show when={selected()!.sections.length === 0}>
<div style="text-align:center;padding:32px;border:1px dashed #cbd5e1;border-radius:12px;color:#94a3b8;font-size:13px">No sections yet. Add the first section above.</div>
</Show>
</Show>
{/* Preview */}
<Show when={activeTab() === 'preview'}>
<div style="display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap;margin-bottom:12px">
<p class="notice" style="margin:0">Preview this dashboard as a sample view or open the live version.</p>
<div class="admin-segmented" style="margin:0">
<button
type="button"
class={`admin-segment ${previewMode() === 'configured' ? 'active' : ''}`}
onClick={() => setPreviewMode('configured')}
>
Sample Preview
</button>
<button
type="button"
class={`admin-segment ${previewMode() === 'live' ? 'active' : ''}`}
onClick={() => setPreviewMode('live')}
>
Live Preview
</button>
</div>
</div>
<Show when={previewMode() === 'configured'}>
<DashboardPreview dashboard={selected()!} />
</Show>
<Show when={previewMode() === 'live'}>
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:10px">
<span class="meta-chip">Role: {livePreviewRoleLabel()}</span>
<Show when={selected()!.roleId}>
<span class="meta-chip">Role ID: {selected()!.roleId}</span>
</Show>
<a class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href={livePreviewUrl()} target="_blank" rel="noreferrer">Open Full Page Preview</a>
</div>
<iframe
src={livePreviewUrl()}
title="Live Internal Dashboard Preview"
style="width:100%;height:760px;border:1px solid #e2e8f0;border-radius:14px;background:#fff"
/>
</Show>
</Show>
</Show>
</div>
</div>
</AdminShell>
);
}