"use client"; import { useState } from "react"; import type { PlanView } from "@/lib/ops/projections"; /* The billing desk for one facility. What the columns say, what they mean today, and the five * acts from lib/ops/controls.ts. No money moves here: an invoice is raised in the accounting tool * and its payment recorded with "Record a payment". Every act reloads, so the panel always shows * what the server holds. */ const PLAN_OPTS: [string, string][] = [["", "No plan set"], ["hosted_small", "Hosted Small · free, 60 staff records"], ["hosted_facility", "Hosted Facility"], ["health_service", "Health Service"], ["private", "Private"]]; const STATE_WORDS: Record = { grandfathered: "Free — grandfathered, everything, for good", free: "Free", trial: "Trial", active: "Paid", grace: "Grace — period ended, still writable", read_only: "Read-only", }; export default function Plan({ facilityId, view, owner }: { facilityId: string; view: PlanView; owner: boolean }) { const [editing, setEditing] = useState(false); const [p, setP] = useState(view.plan); const [n, setN] = useState(view.planNote); const [g, setG] = useState(view.grandfathered); const [err, setErr] = useState(""); const [busy, setBusy] = useState(""); async function act(body: Record, tag: string) { setBusy(tag); setErr(""); try { const r = await fetch("/api/ops/controls", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ action: "plan", facilityId, ...body }) }); const j = await r.json().catch(() => ({})); if (!r.ok) { setErr(j.error || "That didn’t work."); return; } window.location.reload(); } catch { setErr("No connection — check the network and try again."); } finally { setBusy(""); } } const fmt = (d: Date | string | null) => d ? new Date(d).toLocaleDateString("en-AU", { day: "numeric", month: "short", year: "numeric" }) : "—"; const label: React.CSSProperties = { display: "block", fontSize: 11, fontWeight: 700, letterSpacing: "0.1em", textTransform: "uppercase", color: "var(--color-neutral-600)", marginBottom: 4 }; const dl: React.CSSProperties = { display: "grid", gridTemplateColumns: "auto 1fr", gap: "6px 16px", fontSize: 13, margin: "10px 0 0" }; const dt: React.CSSProperties = { color: "var(--color-neutral-600)" }; const dd: React.CSSProperties = { margin: 0, fontWeight: 600, fontVariantNumeric: "tabular-nums" }; const b = (text: string, body: Record, opts: { confirm?: string; disabled?: boolean } = {}) => ( ); return (
{err &&
{err}
}
{view.label}{view.grandfathered ? " · grandfathered" : ""}
{STATE_WORDS[view.state]}
{!editing && }
Recorded as
{view.planStatus}
Trial ends
{fmt(view.trialEndsAt)}
Paid until
{fmt(view.paidUntil)}
{view.graceEndsAt && <>
Grace ends
{fmt(view.graceEndsAt)}
}
Staff ceiling
{view.maxStaff === null ? "none" : view.maxStaff}
Billing contact
on the record — reveal above
{view.planNote &&
{view.planNote}
} {editing && (
{ e.preventDefault(); void act({ act: "set", plan: p, planNote: n, grandfathered: g }, "Save"); }} style={{ display: "grid", gap: 10, marginTop: 14, paddingTop: 14, borderTop: "1px solid var(--color-divider)" }}>