feat: implement full admin panel with dashboard, user management, and platform settings

Adds complete super_admin panel with 9 pages and account owner categories page.
Backend includes 5 new DB tables, ~25 API endpoints, settings manager with
in-memory cache, and 29 integration tests. Frontend includes reusable admin
components (DataTable, Pagination, ActionMenu, etc.) with code-split lazy loading.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Michael Chihlas
2026-02-08 06:05:59 -05:00
parent 4f57c84d43
commit b570f8415f
50 changed files with 4589 additions and 5 deletions

View File

@@ -0,0 +1,104 @@
import { useState, useEffect } from 'react'
import { PageHeader } from '@/components/admin'
import { adminApi } from '@/api/admin'
import { toast } from '@/lib/toast'
import { cn } from '@/lib/utils'
export function SettingsPage() {
const [settings, setSettings] = useState<Record<string, unknown>>({})
const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false)
useEffect(() => {
adminApi.listSettings()
.then((data) => setSettings(data.settings || {}))
.catch(() => toast.error('Failed to load settings'))
.finally(() => setLoading(false))
}, [])
const maintenanceMode = Boolean(settings.maintenance_mode)
const maintenanceMessage = String(settings.maintenance_message || '')
const handleSave = async () => {
setSaving(true)
try {
const data = await adminApi.updateSettings(settings)
setSettings(data.settings || {})
toast.success('Settings saved')
} catch {
toast.error('Failed to save settings')
} finally {
setSaving(false)
}
}
if (loading) {
return (
<div className="space-y-6">
<PageHeader title="Platform Settings" description="Global platform configuration" />
<div className="h-40 animate-pulse rounded-lg bg-muted" />
</div>
)
}
return (
<div className="space-y-6">
<PageHeader title="Platform Settings" description="Global platform configuration" />
<div className="max-w-xl space-y-6 rounded-lg border border-border bg-card p-6">
<div className="flex items-center justify-between">
<div>
<h3 className="font-medium text-foreground">Maintenance Mode</h3>
<p className="text-sm text-muted-foreground">
When enabled, users will see a maintenance message instead of the app.
</p>
</div>
<button
onClick={() => setSettings({ ...settings, maintenance_mode: !maintenanceMode })}
className={cn(
'h-6 w-10 rounded-full transition-colors',
maintenanceMode ? 'bg-destructive' : 'bg-muted'
)}
>
<div className={cn(
'h-4 w-4 rounded-full bg-white transition-transform',
maintenanceMode ? 'translate-x-5' : 'translate-x-1'
)} />
</button>
</div>
{maintenanceMode && (
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Maintenance Message</label>
<textarea
value={maintenanceMessage}
onChange={(e) => setSettings({ ...settings, maintenance_message: e.target.value })}
rows={3}
placeholder="We're performing scheduled maintenance. Please check back later."
className={cn(
'w-full rounded-md border border-border bg-background px-3 py-2 text-sm',
'placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring'
)}
/>
</div>
)}
<div className="border-t border-border pt-4">
<button
onClick={handleSave}
disabled={saving}
className={cn(
'rounded-md px-4 py-2 text-sm font-medium',
'bg-primary text-primary-foreground hover:bg-primary/90',
'disabled:opacity-50'
)}
>
{saving ? 'Saving...' : 'Save Settings'}
</button>
</div>
</div>
</div>
)
}
export default SettingsPage