"use client"; import { useEffect, useState } from "react"; import { useRouter } from "next/navigation"; import { apiFetch, ApiError } from "@/lib/api"; import { AuthUser } from "@/lib/types"; import { useToast } from "@/components/ui/Toast"; import { SkeletonList } from "@/components/ui/Skeleton"; import AccountLinks from "@/components/AccountLinks"; type TierId = "STARTER" | "GROWTH" | "ENTERPRISE"; type SubStatus = "TRIALING" | "ACTIVE" | "PAST_DUE" | "SUSPENDED"; interface BizRow { id: string; name: string; tier: TierId; tierName: string; priceKsh: number; subscriptionStatus: SubStatus; trialEndsAt: string | null; currentPeriodEnd: string | null; isActive: boolean; createdAt: string; userCount: number; branchCount: number; maxUsers: number | null; maxBranches: number | null; owner: { name: string; email: string; phoneNumber: string } | null; } interface Stats { total: number; active: number; trialing: number; suspended: number; mrr: number; } const TIER_OPTIONS: TierId[] = ["STARTER", "GROWTH", "ENTERPRISE"]; const STATUS_OPTIONS: SubStatus[] = ["TRIALING", "ACTIVE", "PAST_DUE", "SUSPENDED"]; function ksh(n: number) { return "KSh " + n.toLocaleString("en-KE"); } function fmtDate(d: string | null) { if (!d) return "—"; return new Date(d).toLocaleDateString("en-KE", { year: "numeric", month: "short", day: "numeric" }); } const STATUS_STYLES: Record = { ACTIVE: "bg-confirm-500/15 text-confirm-600", TRIALING: "bg-flow-500/15 text-flow-600", PAST_DUE: "bg-cash-500/15 text-cash-600", SUSPENDED: "bg-alert-500/15 text-alert-600", }; export default function PlatformPage() { const router = useRouter(); const { show } = useToast(); const [ready, setReady] = useState(false); const [rows, setRows] = useState([]); const [stats, setStats] = useState(null); const [loading, setLoading] = useState(true); const [savingId, setSavingId] = useState(null); const [ownerLink, setOwnerLink] = useState<{ business: string; name: string; email: string; url: string } | null>(null); useEffect(() => { (async () => { let me: AuthUser; try { const result = await apiFetch<{ user: AuthUser }>("/api/auth/me"); me = result.user; } catch { router.replace("/login"); return; } if (!me.isPlatformAdmin) { router.replace(me.role === "DELIVERY" ? "/terminal" : "/admin/dashboard"); return; } setReady(true); await load(); setLoading(false); })(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); async function load() { const result = await apiFetch<{ businesses: BizRow[]; stats: Stats }>("/api/platform/businesses"); setRows(result.businesses); setStats(result.stats); } async function patch(id: string, update: Record, label: string) { setSavingId(id); try { await apiFetch("/api/platform/businesses/" + id, { method: "PATCH", body: JSON.stringify(update), }); show(label, "success"); await load(); } catch (err) { const message = err instanceof ApiError ? err.message : "Update failed"; show(message, "error"); } setSavingId(null); } // A one-time password link for a business's owner (an owner who forgot // their password has nobody inside the business who can reset it). async function ownerReset(b: BizRow) { if (!window.confirm(`Create a password link for the owner of ${b.name}? Send it only to the owner, by a channel you know is theirs.`)) { return; } try { const r = await apiFetch<{ inviteUrl: string; email: string; name: string }>( "/api/platform/businesses/" + b.id + "/owner-reset", { method: "POST" } ); setOwnerLink({ business: b.name, name: r.name, email: r.email, url: r.inviteUrl }); } catch (err) { show(err instanceof ApiError ? err.message : "Could not create the link", "error"); } } if (!ready) { return (
); } return (

Operator

Platform Command Center

Every business on your platform, across all tenants. Change plans, extend billing periods, and suspend accounts here.

{stats && (
)} {ownerLink && (

Password link for {ownerLink.name} ({ownerLink.email}), owner of {ownerLink.business}

It works once and expires within 24 hours (7 days if they never set a password). Whoever opens it can take over the account: send it only to the owner, by a channel you know is theirs.

e.target.select()} aria-label="Owner password link" className="mt-3 w-full rounded-lg border border-white/20 bg-depth-900 px-3 py-2 font-mono text-xs text-white" />
)} {loading ? ( ) : rows.length === 0 ? (
No businesses have signed up yet.
) : (
{rows.map((b) => ( ))}
)}
); } function StatCard({ label, value, accent }: { label: string; value: string; accent?: string }) { return (

{label}

{value}

); } function BusinessCard({ b, saving, onPatch, onOwnerReset, }: { b: BizRow; saving: boolean; onPatch: (id: string, update: Record, label: string) => void; onOwnerReset: (b: BizRow) => void; }) { const overBranches = b.maxBranches !== null && b.branchCount > b.maxBranches; const overUsers = b.maxUsers !== null && b.userCount > b.maxUsers; return (

{b.name}

{b.subscriptionStatus}
{b.owner && (

{b.owner.name} · {b.owner.email} · {b.owner.phoneNumber}

)}

Joined {fmtDate(b.createdAt)}

{b.tierName} · {ksh(b.priceKsh)}/mo

Trial ends {fmtDate(b.trialEndsAt)} · Paid thru {fmtDate(b.currentPeriodEnd)}

Branches: {b.branchCount} {b.maxBranches === null ? " / ∞" : " / " + b.maxBranches} Users: {b.userCount} {b.maxUsers === null ? " / ∞" : " / " + b.maxUsers}
{b.subscriptionStatus !== "SUSPENDED" ? ( ) : ( )} {b.owner && ( )}
); }