143 lines
4.7 KiB
TypeScript
143 lines
4.7 KiB
TypeScript
import {
|
|||
|
|
getAdminOverviewStats,
|
||
|
|
getSignupsTrend,
|
||
|
|
getAtRiskSubscriptions,
|
||
|
|
} from "@/lib/db/admin-queries"
|
||
|
|
import { StatsCard } from "@/components/dashboard/stats-card"
|
||
|
|
import { PlanDonut, SignupsBars } from "@/components/admin/admin-charts"
|
||
|
|
import { getPlanLabel } from "@/lib/stripe/plans"
|
||
|
|
import { formatCurrency } from "@/lib/utils"
|
||
|
|
import type { Plan } from "@/types"
|
||
|
|
import {
|
||
|
|
DollarSign, TrendingUp, Users, Activity, CreditCard,
|
||
|
|
UserPlus, Building2, Home, Banknote, Brain, AlertTriangle,
|
||
|
|
} from "lucide-react"
|
||
|
|
|
||
|
|
export const dynamic = "force-dynamic"
|
||
|
|
|
||
|
|
const STATUS_LABELS: Record<string, string> = {
|
||
|
|
past_due: "Past due",
|
||
|
|
unpaid: "Unpaid",
|
||
|
|
incomplete: "Incomplete",
|
||
|
|
}
|
||
|
|
|
||
|
|
export default async function AdminOverviewPage() {
|
||
|
|
const [stats, signupsTrend, atRisk] = await Promise.all([
|
||
|
|
getAdminOverviewStats(),
|
||
|
|
getSignupsTrend(6),
|
||
|
|
getAtRiskSubscriptions(),
|
||
|
|
])
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="min-h-full">
|
||
|
|
{/* Heading */}
|
||
|
|
<div className="mb-6">
|
||
|
|
<h1 className="text-xl font-bold text-white">Platform Overview</h1>
|
||
|
|
<p className="text-sm text-white/40 mt-0.5">Key metrics across all accounts</p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* KPI grid */}
|
||
|
|
<div className="grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-5 gap-3 sm:gap-4 mb-6">
|
||
|
|
<StatsCard
|
||
|
|
label="MRR"
|
||
|
|
value={formatCurrency(stats.mrr)}
|
||
|
|
sub="Recurring monthly"
|
||
|
|
icon={DollarSign}
|
||
|
|
variant="success"
|
||
|
|
/>
|
||
|
|
<StatsCard
|
||
|
|
label="ARR"
|
||
|
|
value={formatCurrency(stats.arr)}
|
||
|
|
sub="Annualized run-rate"
|
||
|
|
icon={TrendingUp}
|
||
|
|
variant="success"
|
||
|
|
/>
|
||
|
|
<StatsCard
|
||
|
|
label="Total Users"
|
||
|
|
value={stats.totalUsers}
|
||
|
|
sub={`${stats.paidUsers} paid · ${stats.freeUsers} free`}
|
||
|
|
icon={Users}
|
||
|
|
/>
|
||
|
|
<StatsCard
|
||
|
|
label="Active (30d)"
|
||
|
|
value={stats.activeUsers30d}
|
||
|
|
sub="Recently active"
|
||
|
|
icon={Activity}
|
||
|
|
/>
|
||
|
|
<StatsCard
|
||
|
|
label="Paid Users"
|
||
|
|
value={stats.paidUsers}
|
||
|
|
sub={`${stats.freeUsers} free`}
|
||
|
|
icon={CreditCard}
|
||
|
|
variant="success"
|
||
|
|
/>
|
||
|
|
<StatsCard
|
||
|
|
label="New Signups"
|
||
|
|
value={stats.newSignupsThisMonth}
|
||
|
|
sub="This month"
|
||
|
|
icon={UserPlus}
|
||
|
|
/>
|
||
|
|
<StatsCard
|
||
|
|
label="Total Properties"
|
||
|
|
value={stats.totalProperties}
|
||
|
|
sub={`${stats.totalUnits} unit${stats.totalUnits !== 1 ? "s" : ""}`}
|
||
|
|
icon={Building2}
|
||
|
|
/>
|
||
|
|
<StatsCard
|
||
|
|
label="Total Tenants"
|
||
|
|
value={stats.totalTenants}
|
||
|
|
sub="Across platform"
|
||
|
|
icon={Home}
|
||
|
|
/>
|
||
|
|
<StatsCard
|
||
|
|
label="Rent Collected"
|
||
|
|
value={formatCurrency(stats.rentCollectedThisMonth)}
|
||
|
|
sub="This month"
|
||
|
|
icon={Banknote}
|
||
|
|
variant="success"
|
||
|
|
/>
|
||
|
|
<StatsCard
|
||
|
|
label="AI Calls"
|
||
|
|
value={stats.aiCallsThisMonth}
|
||
|
|
sub="This month"
|
||
|
|
icon={Brain}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Charts row */}
|
||
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 sm:gap-5 mb-6">
|
||
|
|
<PlanDonut data={stats.planDistribution} />
|
||
|
|
<SignupsBars data={signupsTrend} />
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* At-risk subscriptions */}
|
||
|
|
{atRisk.length > 0 && (
|
||
|
|
<div className="rounded-2xl border border-white/[0.06] bg-[#16161f] overflow-hidden">
|
||
|
|
<div className="flex items-center gap-2 border-b border-white/[0.06] px-5 py-4">
|
||
|
|
<AlertTriangle className="h-4 w-4 text-rose-400 shrink-0" />
|
||
|
|
<h2 className="text-sm font-semibold text-white">At-risk subscriptions</h2>
|
||
|
|
<span className="flex h-5 min-w-5 items-center justify-center rounded-full bg-rose-500/20 px-1.5 text-[10px] font-bold text-rose-400">
|
||
|
|
{atRisk.length}
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
<div className="divide-y divide-white/[0.04]">
|
||
|
|
{atRisk.slice(0, 8).map((s) => (
|
||
|
|
<div key={s.id} className="flex items-center justify-between px-5 py-3 hover:bg-white/[0.02] transition-colors">
|
||
|
|
<div className="min-w-0">
|
||
|
|
<p className="text-sm font-medium text-white truncate">{s.email}</p>
|
||
|
|
<p className="text-xs text-white/40 truncate">
|
||
|
|
{s.full_name || "—"} · {getPlanLabel(s.plan as Plan)}
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
<span className="ml-3 shrink-0 rounded-full border border-rose-500/20 bg-rose-500/10 px-2.5 py-1 text-[10px] font-semibold text-rose-400">
|
||
|
|
{STATUS_LABELS[s.subscription_status ?? ""] ?? s.subscription_status ?? "Unknown"}
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
)
|
||
|
|
}
|