TypeScript 97.5%
SQL 1.4%
Python 0.8%
1import Link from "next/link";2import { CreditCard, Receipt } from "lucide-react";3import { requireAdmin } from "@/lib/session";4import { getBillingOverview } from "@/lib/queries/admin";5import { formatNumber, formatUsd } from "@/lib/format";6import { PageHeader } from "@/components/ui/page-header";7import { Alert } from "@/components/ui/alert";8import { Badge, StatusBadge } from "@/components/ui/badge";9import { EmptyState } from "@/components/ui/empty-state";10import { Stat, StatGrid } from "@/components/ui/stat";11import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";12import { DateCell, MarginText, Mono, Panel, PlanBadge, numCell } from "@/components/admin/primitives";1314export const dynamic = "force-dynamic";1516export default async function AdminBillingPage() {17 await requireAdmin();18 const b = await getBillingOverview();19 const paying = b.distribution.filter((d) => d.price > 0).reduce((s, d) => s + d.orgs, 0);2021 return (22 <>23 <PageHeader eyebrow="Revenue" title="Billing" description="Private platform: a single Unlimited plan with no list price, so MRR is always zero. Usage revenue reflects metered price_usd only. Stripe checkout and invoicing are not connected." />2425 {!b.stripeConfigured ? (26 <Alert variant="info" title="Stripe is not connected" className="mb-4">27 `STRIPE_SECRET_KEY` is not set and there is nothing to bill: every organization is on the Unlimited plan (private platform, no billing). Subscriptions and billing events below stay empty unless a Stripe integration is added later.28 </Alert>29 ) : null}3031 <StatGrid cols={4} className="mb-6">32 <Stat label="MRR estimate" value={formatUsd(b.mrr)} hint="orgs × plan list price" />33 <Stat label="Paying organizations" value={formatNumber(paying)} hint={`${b.stripeCustomers} with a Stripe customer id`} />34 <Stat label="Usage revenue 30d" value={formatUsd(b.usageRevenue30d)} hint="metered price_usd" />35 <Stat label="Usage margin 30d" value={<MarginText value={b.usageRevenue30d - b.usageCost30d} pct={b.usageRevenue30d > 0 ? ((b.usageRevenue30d - b.usageCost30d) / b.usageRevenue30d) * 100 : null} />} hint="vs upstream cost" />36 </StatGrid>3738 <Panel title="Plan distribution" flush>39 <Table>40 <TableHeader>41 <TableRow>42 <TableHead>Plan</TableHead>43 <TableHead className="text-right">Organizations</TableHead>44 <TableHead className="text-right">List price / mo</TableHead>45 <TableHead className="text-right">MRR</TableHead>46 <TableHead></TableHead>47 </TableRow>48 </TableHeader>49 <TableBody>50 {b.distribution.map((d) => (51 <TableRow key={d.plan}>52 <TableCell>53 <PlanBadge plan={d.plan} />54 </TableCell>55 <TableCell className={numCell}>{formatNumber(d.orgs)}</TableCell>56 <TableCell className={numCell}>{formatUsd(d.price)}</TableCell>57 <TableCell className={numCell}>{formatUsd(d.mrr)}</TableCell>58 <TableCell className="text-right">59 <Link href={`/admin/organizations?plan=${d.plan}`} className="text-[12.5px] text-accent hover:underline">60 view →61 </Link>62 </TableCell>63 </TableRow>64 ))}65 {b.unknown.map((u) => (66 <TableRow key={u.plan}>67 <TableCell>68 <Badge variant="danger">{u.plan}</Badge> <span className="text-[12px] text-fg-muted">legacy plan value — treated as Unlimited; run `pnpm db:seed` to normalize</span>69 </TableCell>70 <TableCell className={numCell}>{u.n}</TableCell>71 <TableCell className={numCell}>—</TableCell>72 <TableCell className={numCell}>—</TableCell>73 <TableCell />74 </TableRow>75 ))}76 </TableBody>77 </Table>78 </Panel>7980 <div className="mt-6 grid gap-6 xl:grid-cols-2">81 <Panel title="Subscriptions" flush>82 {b.subs.length === 0 ? (83 <div className="p-4">84 <EmptyState icon={CreditCard} title="No subscriptions" description="Stripe is not connected yet. Rows appear here when checkout goes live and webhooks create subscriptions." compact />85 </div>86 ) : (87 <Table>88 <TableHeader>89 <TableRow>90 <TableHead>Organization</TableHead>91 <TableHead>Plan</TableHead>92 <TableHead>Status</TableHead>93 <TableHead>Period</TableHead>94 <TableHead>Stripe</TableHead>95 </TableRow>96 </TableHeader>97 <TableBody>98 {b.subs.map((s) => (99 <TableRow key={s.id}>100 <TableCell>101 <Link href={`/admin/organizations/${s.organizationId}`} className="hover:underline">102 {s.orgName ?? s.organizationId}103 </Link>104 </TableCell>105 <TableCell>106 <PlanBadge plan={s.plan} />107 </TableCell>108 <TableCell>109 <StatusBadge status={s.status} />110 {s.cancelAtPeriodEnd ? <Badge variant="warning" className="ml-1">cancels</Badge> : null}111 </TableCell>112 <TableCell className="text-[12.5px] text-fg-muted">113 <DateCell value={s.currentPeriodStart} relative={false} /> → <DateCell value={s.currentPeriodEnd} relative={false} />114 </TableCell>115 <TableCell>116 <Mono>{s.stripeSubscriptionId ?? "—"}</Mono>117 </TableCell>118 </TableRow>119 ))}120 </TableBody>121 </Table>122 )}123 </Panel>124 <Panel title="Billing events" flush>125 {b.events.length === 0 ? (126 <div className="p-4">127 <EmptyState icon={Receipt} title="No billing events" description="Stripe webhook events (invoices, payment failures, plan changes) will be listed here once the integration is live." compact />128 </div>129 ) : (130 <Table>131 <TableHeader>132 <TableRow>133 <TableHead>When</TableHead>134 <TableHead>Type</TableHead>135 <TableHead>Organization</TableHead>136 <TableHead>Stripe event</TableHead>137 </TableRow>138 </TableHeader>139 <TableBody>140 {b.events.map((e) => (141 <TableRow key={e.id}>142 <TableCell>143 <DateCell value={e.createdAt} relative={false} />144 </TableCell>145 <TableCell>146 <Mono>{e.type}</Mono>147 </TableCell>148 <TableCell>{e.orgName ?? e.organizationId ?? "—"}</TableCell>149 <TableCell>150 <Mono>{e.stripeEventId ?? "—"}</Mono>151 </TableCell>152 </TableRow>153 ))}154 </TableBody>155 </Table>156 )}157 </Panel>158 </div>159 </>160 );161}162