* feat: reorganize admin panel around accounts * feat: expand admin customer account controls * feat: add admin account detail management * fix: remove unused admin account icon import * refactor: design critique fixes for account pages - Admin accounts: replace dense card grid with compact DataTable - Account settings: remove redundant hero card, stat grid, header pills - Fix bg-accent (orange) misuse on decorative elements across 7 files - Add ConfirmButton for destructive actions (deactivate, remove member) - Replace single-field modals with inline editing (plan, trial) - Add contextual help: display code tooltip, improved empty states - Non-owner aside explanation for hidden owner-only sections - Admin sidebar: group 11 items into 5 labeled sections - Rename UsersPage.tsx → AccountsPage.tsx to match route - Fix border radius consistency, hide zero-count badges Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: use get_admin_db for all new admin account endpoints All admin endpoints query across tenants without a tenant context. get_db (app-role, subject to RLS) was never imported and would crash at runtime — replace all 6 occurrences with get_admin_db (BYPASSRLS). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
118 lines
4.5 KiB
TypeScript
118 lines
4.5 KiB
TypeScript
import { useState, useEffect } from 'react'
|
|
import { Link } from 'react-router-dom'
|
|
import { Users, TreePine, CreditCard, Activity, TrendingUp, Building2 } 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="bg-card border border-border rounded-xl 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-accent 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/accounts', label: 'Manage Accounts', icon: Building2 },
|
|
{ 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-accent" />
|
|
))}
|
|
</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="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 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="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 bg-card border border-border rounded-xl 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
|