'use client' import { useEffect, useMemo, useState } from 'react' import { ADMIN_API_BASE } from '@/lib/api' type EmployeeRole = 'OWNER' | 'MANAGER' | 'AGENT' type Plan = 'STARTER' | 'GROWTH' | 'PRO' type MenuItemType = 'INTERNAL_PAGE' | 'EXTERNAL_LINK' | 'PARENT_MENU' | 'SECTION_LABEL' | 'DIVIDER' type CompanyOption = { id: string name: string subscription?: { plan?: string | null } | null status: string } type MenuItem = { id: string systemKey: string | null label: string itemType: MenuItemType routeOrUrl: string | null icon: string | null parentId: string | null displayOrder: number openInNewTab: boolean isRequired: boolean isActive: boolean parent?: { id: string; label: string } | null roleVisibilities: Array<{ role: EmployeeRole }> subscriptionAssignments: Array<{ plan: Plan; displayOrder: number; isActive: boolean }> companyAssignments: Array<{ companyId: string displayOrder: number isActive: boolean company: { id: string; name: string } }> } type AuditLog = { id: string action: string resource: string resourceId: string | null createdAt: string adminUser: { firstName: string; lastName: string; email: string } | null } type PreviewItem = { id: string label: string systemKey: string | null itemType: MenuItemType routeOrUrl: string | null displayOrder: number visible: boolean source: 'subscription' | 'company' | 'none' reasons: string[] } type PreviewResponse = { company: { id: string; name: string; status: string; subscription: { plan: Plan; status: string } | null } role: EmployeeRole subscriptionPlan: Plan | null items: PreviewItem[] } type FormState = { label: string systemKey: string itemType: MenuItemType routeOrUrl: string icon: string parentId: string displayOrder: string openInNewTab: boolean isRequired: boolean isActive: boolean roles: EmployeeRole[] plans: Plan[] companyIds: string[] } const ROLES: EmployeeRole[] = ['OWNER', 'MANAGER', 'AGENT'] const PLANS: Plan[] = ['STARTER', 'GROWTH', 'PRO'] const ITEM_TYPES: MenuItemType[] = ['INTERNAL_PAGE', 'EXTERNAL_LINK', 'PARENT_MENU', 'SECTION_LABEL', 'DIVIDER'] const INPUT = 'mt-1 w-full rounded-xl border border-zinc-700 bg-zinc-800 px-3 py-2 text-sm text-zinc-100 outline-none focus:ring-2 focus:ring-orange-500' const LABEL = 'text-xs font-medium uppercase tracking-wider text-zinc-500' async function api(path: string, options?: RequestInit): Promise { const res = await fetch(`${ADMIN_API_BASE}${path}`, { ...options, credentials: 'include', headers: { 'Content-Type': 'application/json', ...(options?.headers ?? {}), }, }) const json = await res.json() if (!res.ok) throw new Error(json?.message ?? 'Request failed') return (json?.data ?? json) as T } function emptyForm(): FormState { return { label: '', systemKey: '', itemType: 'INTERNAL_PAGE', routeOrUrl: '', icon: '', parentId: '', displayOrder: '0', openInNewTab: false, isRequired: false, isActive: true, roles: ['OWNER', 'MANAGER', 'AGENT'], plans: ['STARTER', 'GROWTH', 'PRO'], companyIds: [], } } function formFromItem(item: MenuItem): FormState { return { label: item.label, systemKey: item.systemKey ?? '', itemType: item.itemType, routeOrUrl: item.routeOrUrl ?? '', icon: item.icon ?? '', parentId: item.parentId ?? '', displayOrder: String(item.displayOrder), openInNewTab: item.openInNewTab, isRequired: item.isRequired, isActive: item.isActive, roles: item.roleVisibilities.map((entry) => entry.role), plans: item.subscriptionAssignments.map((entry) => entry.plan), companyIds: item.companyAssignments.map((entry) => entry.companyId), } } function chip(text: string, tone = 'default') { const toneClass = tone === 'success' ? 'bg-emerald-950/40 text-emerald-400' : tone === 'warn' ? 'bg-orange-950/40 text-orange-400' : 'bg-zinc-800 text-zinc-300' return ( {text} ) } export default function MenuManagementPage() { const [items, setItems] = useState([]) const [companies, setCompanies] = useState([]) const [auditLogs, setAuditLogs] = useState([]) const [preview, setPreview] = useState(null) const [previewCompanyId, setPreviewCompanyId] = useState('') const [previewRole, setPreviewRole] = useState('OWNER') const [editingId, setEditingId] = useState(null) const [form, setForm] = useState(emptyForm()) const [loading, setLoading] = useState(true) const [saving, setSaving] = useState(false) const [previewing, setPreviewing] = useState(false) const [error, setError] = useState(null) const [success, setSuccess] = useState(null) const parentOptions = useMemo( () => items.filter((item) => item.itemType === 'PARENT_MENU' && item.id !== editingId), [items, editingId], ) useEffect(() => { async function load() { try { setLoading(true) setError(null) const [menuItems, companiesPage, auditPage] = await Promise.all([ api('/admin/menu-items'), api<{ data: CompanyOption[] }>('/admin/companies?page=1&pageSize=100'), api<{ data: AuditLog[] }>('/admin/menu-audit-logs?page=1&pageSize=20'), ]) setItems(menuItems) setCompanies(companiesPage.data) setAuditLogs(auditPage.data) if (!previewCompanyId && companiesPage.data[0]?.id) { setPreviewCompanyId(companiesPage.data[0].id) } } catch (err: any) { setError(err.message ?? 'Failed to load menu management data.') } finally { setLoading(false) } } load() }, []) function updateForm(key: K, value: FormState[K]) { setForm((prev) => ({ ...prev, [key]: value })) } function toggleArrayValue(key: K, value: FormState[K][number]) { setForm((prev) => { const values = prev[key] as string[] return { ...prev, [key]: values.includes(value as string) ? values.filter((entry) => entry !== value) : [...values, value as string], } }) } async function refreshLists() { const [menuItems, auditPage] = await Promise.all([ api('/admin/menu-items'), api<{ data: AuditLog[] }>('/admin/menu-audit-logs?page=1&pageSize=20'), ]) setItems(menuItems) setAuditLogs(auditPage.data) } async function submitForm(event: React.FormEvent) { event.preventDefault() try { setSaving(true) setError(null) setSuccess(null) const payload = { label: form.label, systemKey: form.systemKey || null, itemType: form.itemType, routeOrUrl: form.routeOrUrl || null, icon: form.icon || null, parentId: form.parentId || null, displayOrder: Number(form.displayOrder || 0), openInNewTab: form.openInNewTab, isRequired: form.isRequired, isActive: form.isActive, roles: form.roles, subscriptionPlans: form.plans.map((plan) => ({ plan, displayOrder: Number(form.displayOrder || 0), isActive: true, })), companyAssignments: form.companyIds.map((companyId) => ({ companyId, displayOrder: Number(form.displayOrder || 0), isActive: true, })), } if (editingId) { await api(`/admin/menu-items/${editingId}`, { method: 'PUT', body: JSON.stringify(payload), }) setSuccess('Menu item updated.') } else { await api('/admin/menu-items', { method: 'POST', body: JSON.stringify(payload), }) setSuccess('Menu item created.') } setEditingId(null) setForm(emptyForm()) await refreshLists() } catch (err: any) { setError(err.message ?? 'Failed to save menu item.') } finally { setSaving(false) } } function startCreate() { setEditingId(null) setForm(emptyForm()) setSuccess(null) setError(null) } function startEdit(item: MenuItem) { setEditingId(item.id) setForm(formFromItem(item)) setSuccess(null) setError(null) } async function toggleStatus(item: MenuItem) { try { setError(null) await api(`/admin/menu-items/${item.id}/status`, { method: 'PATCH', body: JSON.stringify({ isActive: !item.isActive }), }) await refreshLists() } catch (err: any) { setError(err.message ?? 'Failed to update status.') } } async function removeItem(item: MenuItem) { if (!window.confirm(`Delete "${item.label}"?`)) return try { setError(null) await api(`/admin/menu-items/${item.id}`, { method: 'DELETE' }) if (editingId === item.id) { setEditingId(null) setForm(emptyForm()) } await refreshLists() } catch (err: any) { setError(err.message ?? 'Failed to delete menu item.') } } async function runPreview() { if (!previewCompanyId) return try { setPreviewing(true) setError(null) const result = await api('/admin/menu-preview', { method: 'POST', body: JSON.stringify({ companyId: previewCompanyId, role: previewRole }), }) setPreview(result) } catch (err: any) { setError(err.message ?? 'Failed to load menu preview.') } finally { setPreviewing(false) } } if (loading) { return (
Loading menu management…
) } return (

Platform

Menu Management

Control company dashboard navigation by plan, role, and company-specific exceptions from one admin surface.

{(error || success) && (
{error &&
{error}
} {success &&
{success}
}
)}

Menu items

{items.length} configured items

{items.map((item) => ( ))}
Item Type Assignments Status Actions
{item.label} {item.isRequired && chip('Required', 'warn')} {item.systemKey && chip(item.systemKey)}

{item.routeOrUrl || 'No route'} • order {item.displayOrder} {item.parent ? ` • child of ${item.parent.label}` : ''}

{item.itemType}
{item.subscriptionAssignments.length > 0 ? item.subscriptionAssignments.map((assignment) => ( {assignment.plan} )) : No plan assignments}
{item.companyAssignments.slice(0, 3).map((assignment) => ( {assignment.company.name} ))} {item.companyAssignments.length > 3 && chip(`+${item.companyAssignments.length - 3} more`)}
{item.roleVisibilities.map((entry) => ( {entry.role} ))}
{item.isActive ? chip('Active', 'success') : chip('Inactive')}

{editingId ? 'Edit menu item' : 'New menu item'}

Configure menu metadata, role visibility, plan defaults, and company-specific overrides.

{editingId && ( )}

Role visibility

{ROLES.map((role) => ( ))}

Subscription plans

{PLANS.map((plan) => ( ))}

Company-specific assignments

{companies.map((company) => ( ))}

Preview company menu

Test the generated menu for a company and role before troubleshooting or saving follow-up changes.

{preview && (

{preview.company.name}

Plan: {preview.subscriptionPlan ?? 'None'} • Role: {preview.role} • Company status: {preview.company.status}

{preview.items.map((item) => (
{item.label} {item.visible ? chip('Visible', 'success') : chip('Hidden')} {chip(item.source === 'none' ? 'unassigned' : item.source)}

{item.routeOrUrl || 'No route'} • order {item.displayOrder}

    {item.reasons.map((reason, index) => (
  • {reason}
  • ))}
))}
)}

Recent menu audit activity

Recent platform-side changes to menu items and assignments.

{auditLogs.map((entry) => (

{entry.action}

{new Date(entry.createdAt).toLocaleString()}

{entry.adminUser ? `${entry.adminUser.firstName} ${entry.adminUser.lastName} • ${entry.adminUser.email}` : 'Unknown admin'}

))}
) }