fix architecture and write new tests

This commit is contained in:
root
2026-06-10 00:40:19 -04:00
parent 560da1cadf
commit 80a597bc10
377 changed files with 84020 additions and 1337 deletions
@@ -86,20 +86,15 @@ 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-stone-200 bg-white px-3 py-2 text-sm text-blue-950 outline-none focus:border-orange-500 dark:border-blue-800 dark:bg-[#07101e] dark:text-white'
const LABEL = 'text-xs font-semibold uppercase tracking-[0.14em] text-stone-500 dark:text-slate-400'
function getToken() {
return typeof window === 'undefined' ? '' : localStorage.getItem('admin_token') ?? ''
}
'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<T>(path: string, options?: RequestInit): Promise<T> {
const token = getToken()
const res = await fetch(`${ADMIN_API_BASE}${path}`, {
...options,
credentials: 'include',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
...(options?.headers ?? {}),
},
})
@@ -148,13 +143,13 @@ function formFromItem(item: MenuItem): FormState {
function chip(text: string, tone = 'default') {
const toneClass =
tone === 'success'
? 'border-emerald-200 bg-emerald-50 text-emerald-700 dark:border-emerald-900/60 dark:bg-emerald-900/20 dark:text-emerald-300'
? 'bg-emerald-950/40 text-emerald-400'
: tone === 'warn'
? 'border-orange-200 bg-orange-50 text-orange-700 dark:border-orange-900/60 dark:bg-orange-900/20 dark:text-orange-300'
: 'border-stone-200 bg-stone-50 text-stone-700 dark:border-blue-800 dark:bg-[#07101e] dark:text-slate-300'
? 'bg-orange-950/40 text-orange-400'
: 'bg-zinc-800 text-zinc-300'
return (
<span className={`inline-flex rounded-full border px-2.5 py-1 text-[11px] font-medium ${toneClass}`}>
<span className={`inline-flex rounded-full px-2.5 py-1 text-[11px] font-medium ${toneClass}`}>
{text}
</span>
)
@@ -347,8 +342,8 @@ export default function MenuManagementPage() {
if (loading) {
return (
<div className="px-8 py-10">
<div className="rounded-3xl border border-stone-200/80 bg-white/80 p-8 text-sm text-stone-500 shadow-sm dark:border-blue-900 dark:bg-[#0d1b38]/70 dark:text-slate-400">
<div className="shell py-8">
<div className="panel p-8 text-sm text-zinc-500">
Loading menu management
</div>
</div>
@@ -356,20 +351,20 @@ export default function MenuManagementPage() {
}
return (
<div className="space-y-8 px-8 py-10 text-stone-900 dark:text-white">
<section className="rounded-[2rem] border border-stone-200/80 bg-white/85 p-8 shadow-sm dark:border-blue-900 dark:bg-[#0d1b38]/78">
<div className="shell py-8 space-y-6 text-zinc-100">
<section className="panel p-8">
<div className="flex flex-wrap items-start justify-between gap-4">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-orange-600 dark:text-orange-300">Platform Navigation</p>
<h1 className="mt-2 text-3xl font-semibold text-blue-950 dark:text-white">Menu Management</h1>
<p className="mt-2 max-w-3xl text-sm text-stone-500 dark:text-slate-400">
<p className="text-xs uppercase tracking-[0.2em] text-orange-400">Platform</p>
<h1 className="mt-1 text-3xl font-black">Menu Management</h1>
<p className="mt-1 max-w-3xl text-sm text-zinc-400">
Control company dashboard navigation by plan, role, and company-specific exceptions from one admin surface.
</p>
</div>
<button
type="button"
onClick={startCreate}
className="rounded-full bg-orange-600 px-5 py-2.5 text-sm font-semibold text-white transition hover:bg-orange-500"
className="rounded-lg bg-orange-500 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-orange-400"
>
Create menu item
</button>
@@ -378,62 +373,62 @@ export default function MenuManagementPage() {
{(error || success) && (
<section className="space-y-3">
{error && <div className="rounded-2xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700 dark:border-red-900/50 dark:bg-red-900/20 dark:text-red-300">{error}</div>}
{success && <div className="rounded-2xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-700 dark:border-emerald-900/50 dark:bg-emerald-900/20 dark:text-emerald-300">{success}</div>}
{error && <div className="panel p-4 text-sm text-red-400">{error}</div>}
{success && <div className="panel p-4 text-sm text-emerald-400">{success}</div>}
</section>
)}
<div className="grid gap-8 xl:grid-cols-[1.4fr,0.95fr]">
<section className="rounded-[2rem] border border-stone-200/80 bg-white/85 p-6 shadow-sm dark:border-blue-900 dark:bg-[#0d1b38]/78">
<section className="panel overflow-hidden">
<div className="flex items-center justify-between">
<div>
<h2 className="text-xl font-semibold text-blue-950 dark:text-white">Menu items</h2>
<p className="mt-1 text-sm text-stone-500 dark:text-slate-400">{items.length} configured items</p>
<h2 className="px-5 pt-5 text-xl font-semibold text-zinc-100">Menu items</h2>
<p className="px-5 pb-1 pt-1 text-sm text-zinc-400">{items.length} configured items</p>
</div>
</div>
<div className="mt-6 overflow-x-auto">
<div className="overflow-x-auto">
<table className="min-w-full text-left text-sm">
<thead>
<tr className="border-b border-stone-200 dark:border-blue-900">
<th className="px-3 py-3 text-xs font-semibold uppercase tracking-[0.14em] text-stone-500 dark:text-slate-400">Item</th>
<th className="px-3 py-3 text-xs font-semibold uppercase tracking-[0.14em] text-stone-500 dark:text-slate-400">Type</th>
<th className="px-3 py-3 text-xs font-semibold uppercase tracking-[0.14em] text-stone-500 dark:text-slate-400">Assignments</th>
<th className="px-3 py-3 text-xs font-semibold uppercase tracking-[0.14em] text-stone-500 dark:text-slate-400">Status</th>
<th className="px-3 py-3 text-xs font-semibold uppercase tracking-[0.14em] text-stone-500 dark:text-slate-400">Actions</th>
<tr className="border-b border-zinc-800">
<th className="px-5 py-3 text-xs font-medium uppercase tracking-wider text-zinc-500">Item</th>
<th className="px-5 py-3 text-xs font-medium uppercase tracking-wider text-zinc-500">Type</th>
<th className="px-5 py-3 text-xs font-medium uppercase tracking-wider text-zinc-500">Assignments</th>
<th className="px-5 py-3 text-xs font-medium uppercase tracking-wider text-zinc-500">Status</th>
<th className="px-5 py-3 text-xs font-medium uppercase tracking-wider text-zinc-500">Actions</th>
</tr>
</thead>
<tbody>
<tbody className="divide-y divide-zinc-800/60">
{items.map((item) => (
<tr key={item.id} className="border-b border-stone-100 align-top last:border-b-0 dark:border-blue-900/60">
<td className="px-3 py-4">
<tr key={item.id} className="align-top transition-colors hover:bg-zinc-800/30">
<td className="px-5 py-4">
<div className="space-y-1">
<div className="flex flex-wrap items-center gap-2">
<span className="font-semibold text-blue-950 dark:text-white">{item.label}</span>
<span className="font-semibold text-zinc-100">{item.label}</span>
{item.isRequired && chip('Required', 'warn')}
{item.systemKey && chip(item.systemKey)}
</div>
<p className="text-xs text-stone-500 dark:text-slate-400">
<p className="text-xs text-zinc-500">
{item.routeOrUrl || 'No route'} order {item.displayOrder}
{item.parent ? ` • child of ${item.parent.label}` : ''}
</p>
</div>
</td>
<td className="px-3 py-4 text-xs text-stone-600 dark:text-slate-300">{item.itemType}</td>
<td className="px-3 py-4">
<td className="px-5 py-4 text-xs text-zinc-300">{item.itemType}</td>
<td className="px-5 py-4">
<div className="space-y-2">
<div className="flex flex-wrap gap-2">
{item.subscriptionAssignments.length > 0
? item.subscriptionAssignments.map((assignment) => (
<span key={`${item.id}-${assignment.plan}`} className="inline-flex rounded-full border border-stone-200 bg-stone-50 px-2.5 py-1 text-[11px] font-medium text-stone-700 dark:border-blue-800 dark:bg-[#07101e] dark:text-slate-300">
<span key={`${item.id}-${assignment.plan}`} className="inline-flex rounded-full bg-zinc-800 px-2.5 py-1 text-[11px] font-medium text-zinc-300">
{assignment.plan}
</span>
))
: <span className="text-xs text-stone-400 dark:text-slate-500">No plan assignments</span>}
: <span className="text-xs text-zinc-500">No plan assignments</span>}
</div>
<div className="flex flex-wrap gap-2">
{item.companyAssignments.slice(0, 3).map((assignment) => (
<span key={`${item.id}-${assignment.companyId}`} className="inline-flex rounded-full border border-stone-200 bg-stone-50 px-2.5 py-1 text-[11px] font-medium text-stone-700 dark:border-blue-800 dark:bg-[#07101e] dark:text-slate-300">
<span key={`${item.id}-${assignment.companyId}`} className="inline-flex rounded-full bg-zinc-800 px-2.5 py-1 text-[11px] font-medium text-zinc-300">
{assignment.company.name}
</span>
))}
@@ -441,25 +436,25 @@ export default function MenuManagementPage() {
</div>
<div className="flex flex-wrap gap-2">
{item.roleVisibilities.map((entry) => (
<span key={`${item.id}-${entry.role}`} className="inline-flex rounded-full border border-stone-200 bg-stone-50 px-2.5 py-1 text-[11px] font-medium text-stone-700 dark:border-blue-800 dark:bg-[#07101e] dark:text-slate-300">
<span key={`${item.id}-${entry.role}`} className="inline-flex rounded-full bg-zinc-800 px-2.5 py-1 text-[11px] font-medium text-zinc-300">
{entry.role}
</span>
))}
</div>
</div>
</td>
<td className="px-3 py-4">
<td className="px-5 py-4">
{item.isActive ? chip('Active', 'success') : chip('Inactive')}
</td>
<td className="px-3 py-4">
<td className="px-5 py-4">
<div className="flex flex-wrap gap-2">
<button type="button" onClick={() => startEdit(item)} className="rounded-full border border-stone-200 px-3 py-1.5 text-xs font-medium text-stone-700 hover:border-orange-400 hover:text-orange-600 dark:border-blue-800 dark:text-slate-300">
<button type="button" onClick={() => startEdit(item)} className="rounded-lg border border-zinc-700 bg-zinc-800 px-3 py-1.5 text-xs text-zinc-300 transition-colors hover:bg-zinc-700">
Edit
</button>
<button type="button" onClick={() => toggleStatus(item)} className="rounded-full border border-stone-200 px-3 py-1.5 text-xs font-medium text-stone-700 hover:border-orange-400 hover:text-orange-600 dark:border-blue-800 dark:text-slate-300">
<button type="button" onClick={() => toggleStatus(item)} className="rounded-lg border border-zinc-700 bg-zinc-800 px-3 py-1.5 text-xs text-zinc-300 transition-colors hover:bg-zinc-700">
{item.isActive ? 'Disable' : 'Enable'}
</button>
<button type="button" onClick={() => removeItem(item)} className="rounded-full border border-red-200 px-3 py-1.5 text-xs font-medium text-red-700 hover:bg-red-50 dark:border-red-900/60 dark:text-red-300 dark:hover:bg-red-900/20">
<button type="button" onClick={() => removeItem(item)} className="rounded-lg border border-red-900/60 bg-red-950/20 px-3 py-1.5 text-xs text-red-300 transition-colors hover:bg-red-900/30">
Delete
</button>
</div>
@@ -471,16 +466,16 @@ export default function MenuManagementPage() {
</div>
</section>
<section className="rounded-[2rem] border border-stone-200/80 bg-white/85 p-6 shadow-sm dark:border-blue-900 dark:bg-[#0d1b38]/78">
<div className="flex items-center justify-between">
<section className="panel p-6">
<div className="flex items-center justify-between gap-4">
<div>
<h2 className="text-xl font-semibold text-blue-950 dark:text-white">{editingId ? 'Edit menu item' : 'New menu item'}</h2>
<p className="mt-1 text-sm text-stone-500 dark:text-slate-400">
<h2 className="text-xl font-semibold text-zinc-100">{editingId ? 'Edit menu item' : 'New menu item'}</h2>
<p className="mt-1 text-sm text-zinc-400">
Configure menu metadata, role visibility, plan defaults, and company-specific overrides.
</p>
</div>
{editingId && (
<button type="button" onClick={startCreate} className="text-sm font-medium text-orange-600 hover:text-orange-500">
<button type="button" onClick={startCreate} className="text-sm font-medium text-orange-400 hover:text-orange-300">
Clear
</button>
)}
@@ -537,18 +532,18 @@ export default function MenuManagementPage() {
</label>
<div className="grid gap-3 sm:grid-cols-2">
<label className="flex items-center gap-2 rounded-xl border border-stone-200 px-3 py-2 text-sm dark:border-blue-800">
<label className="flex items-center gap-2 rounded-xl border border-zinc-700 bg-zinc-900/40 px-3 py-2 text-sm text-zinc-200">
<input type="checkbox" checked={form.isActive} onChange={(e) => updateForm('isActive', e.target.checked)} />
Active
</label>
<label className="flex items-center gap-2 rounded-xl border border-stone-200 px-3 py-2 text-sm dark:border-blue-800">
<label className="flex items-center gap-2 rounded-xl border border-zinc-700 bg-zinc-900/40 px-3 py-2 text-sm text-zinc-200">
<input type="checkbox" checked={form.openInNewTab} onChange={(e) => updateForm('openInNewTab', e.target.checked)} />
New tab
</label>
</div>
</div>
<label className="flex items-center gap-2 rounded-xl border border-stone-200 px-3 py-2 text-sm dark:border-blue-800">
<label className="flex items-center gap-2 rounded-xl border border-zinc-700 bg-zinc-900/40 px-3 py-2 text-sm text-zinc-200">
<input type="checkbox" checked={form.isRequired} onChange={(e) => updateForm('isRequired', e.target.checked)} />
Required system item
</label>
@@ -557,7 +552,7 @@ export default function MenuManagementPage() {
<p className={LABEL}>Role visibility</p>
<div className="mt-2 flex flex-wrap gap-2">
{ROLES.map((role) => (
<label key={role} className="flex items-center gap-2 rounded-full border border-stone-200 px-3 py-2 text-sm dark:border-blue-800">
<label key={role} className="flex items-center gap-2 rounded-full border border-zinc-700 bg-zinc-900/40 px-3 py-2 text-sm text-zinc-200">
<input type="checkbox" checked={form.roles.includes(role)} onChange={() => toggleArrayValue('roles', role)} />
{role}
</label>
@@ -569,7 +564,7 @@ export default function MenuManagementPage() {
<p className={LABEL}>Subscription plans</p>
<div className="mt-2 flex flex-wrap gap-2">
{PLANS.map((plan) => (
<label key={plan} className="flex items-center gap-2 rounded-full border border-stone-200 px-3 py-2 text-sm dark:border-blue-800">
<label key={plan} className="flex items-center gap-2 rounded-full border border-zinc-700 bg-zinc-900/40 px-3 py-2 text-sm text-zinc-200">
<input type="checkbox" checked={form.plans.includes(plan)} onChange={() => toggleArrayValue('plans', plan)} />
{plan}
</label>
@@ -579,14 +574,14 @@ export default function MenuManagementPage() {
<div>
<p className={LABEL}>Company-specific assignments</p>
<div className="mt-2 max-h-56 space-y-2 overflow-y-auto rounded-2xl border border-stone-200 p-3 dark:border-blue-800">
<div className="mt-2 max-h-56 space-y-2 overflow-y-auto rounded-2xl border border-zinc-700 bg-zinc-900/30 p-3">
{companies.map((company) => (
<label key={company.id} className="flex items-center justify-between gap-3 rounded-xl px-2 py-2 text-sm hover:bg-stone-50 dark:hover:bg-[#07101e]">
<label key={company.id} className="flex items-center justify-between gap-3 rounded-xl px-2 py-2 text-sm text-zinc-200 transition-colors hover:bg-zinc-800/40">
<span className="flex items-center gap-2">
<input type="checkbox" checked={form.companyIds.includes(company.id)} onChange={() => toggleArrayValue('companyIds', company.id)} />
<span>{company.name}</span>
</span>
<span className="text-xs text-stone-400 dark:text-slate-500">
<span className="text-xs text-zinc-500">
{company.subscription?.plan ?? 'No plan'} {company.status}
</span>
</label>
@@ -597,7 +592,7 @@ export default function MenuManagementPage() {
<button
type="submit"
disabled={saving}
className="w-full rounded-full bg-orange-600 px-5 py-2.5 text-sm font-semibold text-white transition hover:bg-orange-500 disabled:opacity-60"
className="w-full rounded-lg bg-orange-500 px-5 py-2.5 text-sm font-medium text-white transition-colors hover:bg-orange-400 disabled:opacity-60"
>
{saving ? 'Saving…' : editingId ? 'Update menu item' : 'Create menu item'}
</button>
@@ -606,11 +601,11 @@ export default function MenuManagementPage() {
</div>
<div className="grid gap-8 xl:grid-cols-[1.1fr,0.9fr]">
<section className="rounded-[2rem] border border-stone-200/80 bg-white/85 p-6 shadow-sm dark:border-blue-900 dark:bg-[#0d1b38]/78">
<section className="panel p-6">
<div className="flex flex-wrap items-end gap-4">
<div className="flex-1">
<h2 className="text-xl font-semibold text-blue-950 dark:text-white">Preview company menu</h2>
<p className="mt-1 text-sm text-stone-500 dark:text-slate-400">
<h2 className="text-xl font-semibold text-zinc-100">Preview company menu</h2>
<p className="mt-1 text-sm text-zinc-400">
Test the generated menu for a company and role before troubleshooting or saving follow-up changes.
</p>
</div>
@@ -632,7 +627,7 @@ export default function MenuManagementPage() {
type="button"
onClick={runPreview}
disabled={previewing || !previewCompanyId}
className="rounded-full bg-blue-950 px-5 py-2.5 text-sm font-semibold text-white transition hover:bg-blue-900 disabled:opacity-60 dark:bg-orange-600 dark:hover:bg-orange-500"
className="rounded-lg bg-orange-500 px-5 py-2.5 text-sm font-medium text-white transition-colors hover:bg-orange-400 disabled:opacity-60"
>
{previewing ? 'Previewing…' : 'Run preview'}
</button>
@@ -640,25 +635,25 @@ export default function MenuManagementPage() {
{preview && (
<div className="mt-6 space-y-4">
<div className="rounded-2xl border border-stone-200 p-4 dark:border-blue-800">
<p className="text-sm font-semibold text-blue-950 dark:text-white">{preview.company.name}</p>
<p className="mt-1 text-xs text-stone-500 dark:text-slate-400">
<div className="rounded-2xl border border-zinc-700 bg-zinc-900/30 p-4">
<p className="text-sm font-semibold text-zinc-100">{preview.company.name}</p>
<p className="mt-1 text-xs text-zinc-500">
Plan: {preview.subscriptionPlan ?? 'None'} Role: {preview.role} Company status: {preview.company.status}
</p>
</div>
<div className="space-y-3">
{preview.items.map((item) => (
<div key={item.id} className="rounded-2xl border border-stone-200 p-4 dark:border-blue-800">
<div key={item.id} className="rounded-2xl border border-zinc-700 bg-zinc-900/30 p-4">
<div className="flex flex-wrap items-center gap-2">
<span className="font-semibold text-blue-950 dark:text-white">{item.label}</span>
<span className="font-semibold text-zinc-100">{item.label}</span>
{item.visible ? chip('Visible', 'success') : chip('Hidden')}
{chip(item.source === 'none' ? 'unassigned' : item.source)}
</div>
<p className="mt-1 text-xs text-stone-500 dark:text-slate-400">
<p className="mt-1 text-xs text-zinc-500">
{item.routeOrUrl || 'No route'} order {item.displayOrder}
</p>
<ul className="mt-3 space-y-1 text-sm text-stone-600 dark:text-slate-300">
<ul className="mt-3 space-y-1 text-sm text-zinc-300">
{item.reasons.map((reason, index) => (
<li key={`${item.id}-${index}`}>{reason}</li>
))}
@@ -670,22 +665,22 @@ export default function MenuManagementPage() {
)}
</section>
<section className="rounded-[2rem] border border-stone-200/80 bg-white/85 p-6 shadow-sm dark:border-blue-900 dark:bg-[#0d1b38]/78">
<h2 className="text-xl font-semibold text-blue-950 dark:text-white">Recent menu audit activity</h2>
<p className="mt-1 text-sm text-stone-500 dark:text-slate-400">
<section className="panel p-6">
<h2 className="text-xl font-semibold text-zinc-100">Recent menu audit activity</h2>
<p className="mt-1 text-sm text-zinc-400">
Recent platform-side changes to menu items and assignments.
</p>
<div className="mt-6 space-y-3">
{auditLogs.map((entry) => (
<div key={entry.id} className="rounded-2xl border border-stone-200 p-4 dark:border-blue-800">
<div key={entry.id} className="rounded-2xl border border-zinc-700 bg-zinc-900/30 p-4">
<div className="flex items-center justify-between gap-3">
<p className="text-sm font-semibold text-blue-950 dark:text-white">{entry.action}</p>
<span className="text-xs text-stone-400 dark:text-slate-500">
<p className="text-sm font-semibold text-zinc-100">{entry.action}</p>
<span className="text-xs text-zinc-500">
{new Date(entry.createdAt).toLocaleString()}
</span>
</div>
<p className="mt-1 text-xs text-stone-500 dark:text-slate-400">
<p className="mt-1 text-xs text-zinc-500">
{entry.adminUser
? `${entry.adminUser.firstName} ${entry.adminUser.lastName}${entry.adminUser.email}`
: 'Unknown admin'}