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,117 @@
import { useState, useEffect } from 'react'
import { Link } from 'react-router-dom'
import { Users, TreePine, CreditCard, Activity, TrendingUp } from 'lucide-react'
import { cn } from '@/lib/utils'
import { PageHeader } from '@/components/admin'
import { adminApi } from '@/api/admin'
import type { DashboardMetrics, ActivityEntry } from '@/types/admin'
interface MetricCardProps {
label: string
value: number | string
icon: React.ReactNode
}
function MetricCard({ label, value, icon }: MetricCardProps) {
return (
<div className="rounded-lg border border-border bg-card p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">{label}</p>
<p className="mt-1 text-3xl font-bold text-foreground">{value}</p>
</div>
<div className="rounded-lg bg-muted/50 p-3 text-muted-foreground">{icon}</div>
</div>
</div>
)
}
export function DashboardPage() {
const [metrics, setMetrics] = useState<DashboardMetrics | null>(null)
const [activity, setActivity] = useState<ActivityEntry[]>([])
const [loading, setLoading] = useState(true)
useEffect(() => {
Promise.allSettled([
adminApi.getDashboardMetrics(),
adminApi.getDashboardActivity(),
]).then(([metricsResult, activityResult]) => {
if (metricsResult.status === 'fulfilled') setMetrics(metricsResult.value)
if (activityResult.status === 'fulfilled') setActivity(activityResult.value)
setLoading(false)
})
}, [])
const quickLinks = [
{ to: '/admin/users', label: 'Manage Users', icon: Users },
{ to: '/admin/plan-limits', label: 'Plan Limits', icon: TrendingUp },
{ to: '/admin/feature-flags', label: 'Feature Flags', icon: Activity },
{ to: '/admin/audit-logs', label: 'Audit Logs', icon: Activity },
]
return (
<div className="space-y-6">
<PageHeader title="Dashboard" description="Platform overview and quick actions" />
{loading ? (
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
{Array.from({ length: 4 }).map((_, i) => (
<div key={i} className="h-32 animate-pulse rounded-lg bg-muted" />
))}
</div>
) : metrics && (
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
<MetricCard label="Total Users" value={metrics.total_users} icon={<Users className="h-6 w-6" />} />
<MetricCard label="Active Subscriptions" value={metrics.active_subscriptions} icon={<CreditCard className="h-6 w-6" />} />
<MetricCard label="Paid Accounts" value={metrics.paid_accounts} icon={<CreditCard className="h-6 w-6" />} />
<MetricCard label="Total Trees" value={metrics.total_trees} icon={<TreePine className="h-6 w-6" />} />
</div>
)}
{/* Recent Activity */}
{activity.length > 0 && (
<div>
<h2 className="font-heading text-lg font-semibold text-foreground">Recent Activity</h2>
<div className="mt-3 space-y-2">
{activity.slice(0, 10).map((entry) => (
<div key={entry.id} className="flex items-center justify-between rounded-md border border-border bg-card px-4 py-3 text-sm">
<div>
<span className="font-medium text-foreground">{entry.action}</span>
<span className="ml-2 text-muted-foreground">{entry.resource_type}</span>
{entry.user_email && (
<span className="ml-2 text-muted-foreground">by {entry.user_email}</span>
)}
</div>
<span className="text-xs text-muted-foreground">
{new Date(entry.created_at).toLocaleString()}
</span>
</div>
))}
</div>
</div>
)}
{/* Quick Links */}
<div>
<h2 className="font-heading text-lg font-semibold text-foreground">Quick Links</h2>
<div className="mt-3 grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
{quickLinks.map((link) => (
<Link
key={link.to}
to={link.to}
className={cn(
'flex items-center gap-3 rounded-lg border border-border bg-card p-4',
'text-sm font-medium text-foreground transition-colors hover:bg-accent'
)}
>
<link.icon className="h-5 w-5 text-muted-foreground" />
{link.label}
</Link>
))}
</div>
</div>
</div>
)
}
export default DashboardPage