"use client"; import { useEffect, useState } from "react"; import { useSnap } from "@/lib/client"; import { Field, LiveRegion } from "@/components/ui"; import { PRICES } from "@/lib/plan"; /* Settings › Plan. Admin only, and only once plans are live. * * Numbers and short labels, no selling: the plan's name, where it stands, the staff count against * the ceiling when there is one, what the hosted backups keep, and the billing contact. Three * things to do: ask for an invoice, which emails the owner and nothing more; pay by card, which * leaves for a page Stripe hosts and appears only when the server has Stripe keys (`cardsOn`) and * the room is not grandfathered; and download the backup, which is the same file Data offers and is * here because a room deciding whether to pay should have its way out beside the button. */ const STATE_WORDS: Record = { grandfathered: "Free · everything included, for good", free: "Free", trial: "Trial", active: "Paid", grace: "Ended · still writable", read_only: "Read-only", }; export default function PlanTab() { const { s, mutate } = useSnap(); const p = s.plan; const cardsOn = (s.plan as { cardsOn?: boolean }).cardsOn === true; const card = (s.plan as { card?: boolean }).card === true; const [email, setEmail] = useState(p.billingEmail); const [want, setWant] = useState<"hosted_facility" | "health_service">("hosted_facility"); const [msg, setMsg] = useState(""); const [busy, setBusy] = useState(""); const fmt = (iso: string | null) => iso ? new Date(iso).toLocaleDateString("en-AU", { day: "numeric", month: "short", year: "numeric" }) : "—"; // Back from Stripe: ?card=ok or ?card=cancelled, said once and taken off the address. useEffect(() => { const q = new URLSearchParams(window.location.search); const back = q.get("card"); if (!back) return; if (back === "ok") setMsg("Payment set up. The plan updates when Stripe confirms it — a minute at most."); else if (back === "cancelled") setMsg("Card payment cancelled. Nothing was charged."); q.delete("card"); const rest = q.toString(); window.history.replaceState(null, "", window.location.pathname + (rest ? `?${rest}` : "")); }, []); const row = (k: string, v: React.ReactNode) => (
{k}
{v}
); async function run(op: "plan.billing" | "plan.invoice", payload: Record, done: string) { setBusy(op); setMsg(""); const r = await mutate(op, payload); setBusy(""); setMsg(r.ok ? done : r.error); } /** Checkout or the Billing Portal: both answer a URL to leave for. */ async function stripeGo(kind: "checkout" | "portal") { setBusy(kind); setMsg(""); try { const r = await fetch(`/api/billing/${kind}`, { method: "POST", headers: { "content-type": "application/json" }, body: "{}" }); const j = (await r.json().catch(() => ({}))) as { url?: string; error?: string }; if (r.ok && j.url) { window.location.assign(j.url); return; } setMsg(j.error || "Could not reach the payment page."); } catch { setMsg("Could not reach the payment page."); } setBusy(""); } const ends = p.state === "trial" ? "Trial ends" : p.state === "active" ? "Paid until" : p.state === "grace" ? "Writable until" : null; const paidOrTrial = p.state === "active" || p.state === "trial"; // The card button: a room that could buy (free, trial, grace, read-only) and is not already // paying by card. A card-backed room gets Manage card instead. const showPay = cardsOn && !p.grandfathered && !card && p.state !== "active"; const showManage = cardsOn && !p.grandfathered && card; return ( <>
Plan
{p.label}
{row("Status", {STATE_WORDS[p.state] || p.state}{p.state === "trial" && p.daysLeft !== null ? ` · ${p.daysLeft} days left` : ""}{p.state === "grace" && p.graceDaysLeft !== null ? ` · ${p.graceDaysLeft} days` : ""})} {row("Staff records", p.maxStaff === null ? String(p.staff) : `${p.staff} of ${p.maxStaff}`)} {row("Backups", `Nightly · ${p.backupDays} days`)} {ends && row(ends, fmt(p.state === "grace" ? p.graceEndsAt : p.endsAt))} {card && row("Paid by", "Card · monthly")} {p.org && row("Health service", p.org.name)} {row("Billing contact", p.billingEmail || "—")} {p.invoicer && row("Invoiced by", p.invoicer)}
{p.readOnly && (
Reports, exports and the backup still work. Writes start again when a payment is recorded.
)} {!p.grandfathered && ( <>
{cardsOn ? "Invoice or card" : "Invoice"}
{(c) => setEmail(e.target.value)} />} {(c) => ( )}
{showPay && ( <> Hosted Facility · ${PRICES.hostedMonthly} a month, ex GST )} {showManage && } Download backup
{paidOrTrial && !card &&
A renewal invoice is requested the same way.
} )} {p.grandfathered && (
Download backup
)} ); }