Files
resolutionflow/frontend/src/pages/admin/DashboardPage.tsx
chihlasm a71f082e25 feat: extract admin account management rework from PR 124 (#138)
* 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>
2026-04-13 04:44:51 -04:00

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