import { useEffect, useRef, useState } from "react"; import { Link, useNavigate } from "react-router-dom"; import { Card } from "../../../../components/ui/card"; import { initialsOf } from "../../../../components/ui/org-badge"; import { useAuthServiceContext } from "../../../../services/AuthService"; import { usePracticeClosureService, type ClosureRead, type PlanQuote, type RestoreState, } from "../../../../services/PracticeClosureService"; import { onViewerChange, viewerEpoch } from "../../../../utils/writeThroughHold"; import { CLOSURE_CARD, NOTICE_TONE, counted, formatBillingDate, formatDay, formatMoney } from "./closureText"; import "./PracticeClosure.css"; interface Outcome { tone: "stop" | "wait" | "done"; title: string; text: string; support?: boolean; } export const BILLING_LINK_PENDING = "The practice is reopened on its paid plan, but its billing record is still" + " being updated. That finishes on its own within a few minutes; nothing more will be charged."; // Only for answers that refused before anything was sent for this attempt. const STILL_CLOSED = "Nothing was sent for this attempt and the practice is still closed. Try again."; const REOPEN_OUTCOMES: Record = { "payment-pending": { tone: "wait", title: "Waiting on the payment", text: "The previous plan is waiting on its payment, so the practice is still closed. It reopens once that" + " payment goes through.", }, "restore-unconfirmed": { tone: "stop", title: "The billing result is not confirmed", text: "The billing result is not confirmed. The practice is still closed. Check this attempt again or contact" + " support.", support: true, }, "plan-not-restored": { tone: "stop", title: "The previous plan could not be restored", text: "The practice is still closed. Check the payment method and try again.", }, "plan-unavailable": { tone: "stop", title: "Part of the previous plan is no longer sold", text: "It has no current price to confirm in its place. Nothing was charged and the practice is still" + " closed. Contact Akashic support to restore it.", support: true, }, "still-closing": { tone: "wait", title: "Still closing", text: "The practice can be reopened once its closure has finished.", }, "restore-in-progress": { tone: "wait", title: "Still being confirmed", text: "A reopen attempt for this practice is still being confirmed, so the practice is still closed. Check" + " this attempt again in a few minutes.", }, "needs-resolution": { tone: "stop", title: "This reopen needs Akashic support", text: "The billing result of this attempt needs a person to resolve it. The practice is still closed." + " Contact Akashic support.", support: true, }, "reopen-not-finished": { tone: "stop", title: "Reopening has not finished", text: "The previous plan is restored, but the practice has not finished reopening. Try again to finish;" + " nothing will be charged again.", }, "reopen-not-started": { tone: "stop", title: "Reopening did not start", text: STILL_CLOSED }, "quote-unavailable": { tone: "stop", title: "The current price could not be read", text: STILL_CLOSED }, "billing-unavailable": { tone: "stop", title: "Billing could not be checked", text: "The result of the last attempt could not be checked, so it is not confirmed. The practice is still" + " closed. Check this attempt again later.", }, conflict: { tone: "stop", title: "The practice changed while you were reopening it", text: STILL_CLOSED }, "not-the-owner": { tone: "stop", title: "Only the owner can reopen this practice", text: "The practice stays closed until its owner reopens it.", }, }; const PRICE_CHANGED: Outcome = { tone: "wait", title: "The price changed", text: "The current price moved since it was shown. Review the new total before confirming.", }; const UNCONFIRMED: Outcome = { tone: "stop", title: "Reopening could not be confirmed", text: "There was no answer about this attempt. Check this attempt again before doing anything else.", }; /** Answers and saved states that leave an attempt unresolved: it can only be checked again, never confirmed anew. */ const HELD_CODES = new Set(["restore-unconfirmed", "restore-in-progress", "payment-pending", "billing-unavailable"]); const HELD_STATES: Record = { "in-progress": "restore-in-progress", unconfirmed: "restore-unconfirmed", "needs-resolution": "needs-resolution", "payment-pending": "payment-pending", "not-sent": undefined, paid: undefined, }; const NOT_RESUMED: Outcome = { tone: "stop", title: "Closing could not be resumed", text: "Cancelling its appointments has not started again. Try again.", }; // Closing's progress is not counted exactly (see ClosureRecord.progress), so it is said without numbers. const PROCESSING = "Processing appointment cancellations and patient notices."; const UNREACHED = "Some patients could not be sent a notice the way they chose, or chose no way to be told."; const OutcomeBox = ({ outcome, onSupport }: { outcome: Outcome; onSupport: () => void }) => (
{outcome.title} {outcome.text} {outcome.support && ( )}
); const QuoteSummary = ({ quote, paidThrough, timeZone }: { quote: PlanQuote; paidThrough?: string | null; timeZone: string | null }) => { const money = (amount: number) => formatMoney(amount, quote.currency); const each = (amount: number | null, interval?: string) => `${amount === null ? "a price set by quantity" : money(amount)}${interval ? ` per ${interval}` : ""}`; const due = quote.charged === "renewal" ? `Due when the current paid period ends${paidThrough ? ` on ${formatBillingDate(paidThrough, timeZone)}` : ""}` : "Charged when you confirm"; return (

Previous plan and add-ons

    {quote.items.map((item, index) => (
  • {item.usage ? ( {item.name ? `${item.name}: ` : ""}Billed on use at {each(item.unitAmount, item.interval)} ) : ( {item.name ? `${item.name}: ` : ""}{item.quantity.toLocaleString()} × {each(item.unitAmount, item.interval)} )} {item.usage ? "Not in this total" : item.unitAmount === null ? "In the total" : money(item.unitAmount * item.quantity)}
  • ))}
{quote.discount > 0 && (
Discount−{money(quote.discount)}
)} {quote.tax > 0 && (
Tax{money(quote.tax)}
)}
{due} {money(quote.total)}
{quote.excludesUsage && (

Lines billed on use are charged for what is used, so they are not in this total.

)} {quote.renewalDiscountPending && (

Your contractor's volume discount is set when the renewal is drafted, so it is not in this total.

)} {quote.payer === "contractor" && (

Your contractor pays for this plan and keeps paying for it once the practice reopens; this charge is theirs, not yours.

)}
); }; interface Props { read: ClosureRead; organizationId: string; practiceName: string; onReopened: (notice: string) => void; onChanged: () => void; timeZone: string | null; logoUrl?: string; /** The Owner's account while closed shows the plan and the reopen, without the closure band. */ surface?: "closure" | "account"; } /** Where a closure stands. Only the Owner is offered anything to do; everyone else sees status. */ const SMALLEST_USABLE_IMAGE = 8; const PracticeClosureStatus = ({ read, organizationId, practiceName, onReopened, onChanged, timeZone, logoUrl, surface = "closure" }: Props) => { const navigate = useNavigate(); const { closePractice, reopenPractice } = usePracticeClosureService(); const { actingAsSessionRef } = useAuthServiceContext(); // Only an acting-as session moves this; a context that keeps none never changes it. const session = () => actingAsSessionRef?.current ?? 0; const [busy, setBusy] = useState(false); const [outcome, setOutcome] = useState(); const [quote, setQuote] = useState<{ plan: PlanQuote; paidThrough?: string | null }>(); const [logoFailed, setLogoFailed] = useState(false); // Set by an answer that left the attempt unresolved, until a check or a fresh read says otherwise. const [held, setHeld] = useState(false); // A late answer for another practice, viewer, acting-as session or opening, or after this view is gone, must // not change what this one shows. The opening counter retires every earlier attempt at once: a fresh one, a // practice change, a live viewer change, or an unmount each move it past whatever token an in-flight call holds. const current = useRef({ organizationId, alive: true, opening: 0 }); useEffect(() => { current.current.organizationId = organizationId; current.current.opening += 1; }, [organizationId]); useEffect(() => { const view = current.current; view.alive = true; // Retires synchronously on the live viewer epoch's own change, never waiting on this view's rerender. A retired // request no longer holds the view busy; its answer still never publishes. const unsubscribe = onViewerChange(() => { view.opening += 1; if (view.alive) setBusy(false); }); return () => { view.alive = false; view.opening += 1; unsubscribe(); }; }, []); type Opening = { organizationId: string; opening: number; epoch: number; session: number }; const openAttempt = (): Opening => { current.current.opening += 1; return { organizationId, opening: current.current.opening, epoch: viewerEpoch(), session: session(), }; }; const isCurrent = (attempt: Opening): boolean => current.current.alive && current.current.opening === attempt.opening && current.current.organizationId === attempt.organizationId && viewerEpoch() === attempt.epoch && session() === attempt.session; // Held by the request itself: a token or proof wait that outlasts its opening never goes on to send for it. const admitFor = (attempt: Opening) => () => { if (!isCurrent(attempt)) throw new Error("This close or reopen belongs to an opening that has since been retired."); }; // Busy is released by the latest opening only, even one retired by a session change, so the view never sticks. const release = (attempt: Opening) => { if (current.current.alive && current.current.opening === attempt.opening) setBusy(false); }; const closure = read.closure; const totals = closure?.totals; const closing = closure?.status === "closing"; const closedOn = closure?.closedAt ? formatDay(closure.closedAt) : undefined; const paidThrough = closure?.billing?.paidThrough; const support = () => navigate("/dashboard/support"); if (!read.isOwner) { return (

{practiceName} is {closing ? "closing" : "closed"}

{closedOn ? `The owner closed it on ${closedOn}. ` : ""}Everything it held is archived; nothing is deleted.

{closing &&

{PROCESSING}

}

This workspace is unavailable while the practice is closed. Only the practice owner can open it again.

); } const resume = async () => { const attempt = openAttempt(); setBusy(true); setOutcome(undefined); try { const result = await closePractice( attempt.organizationId, practiceName, closure?.acknowledgedAppointments ?? 0, { admit: admitFor(attempt) }, ); if (!isCurrent(attempt)) return; if (result.status === 200 || result.status === 202) onChanged(); else setOutcome(NOT_RESUMED); } catch { if (!isCurrent(attempt)) return; setOutcome(NOT_RESUMED); onChanged(); } finally { release(attempt); } }; const reopen = async (confirmedTotal?: number) => { const attempt = openAttempt(); setBusy(true); setOutcome(undefined); try { const result = await reopenPractice(attempt.organizationId, confirmedTotal, { admit: admitFor(attempt) }); if (!isCurrent(attempt)) return; const unresolved = HELD_CODES.has(result.code ?? ""); setHeld(unresolved); if (unresolved) setQuote(undefined); if (result.code === "confirm-plan" && result.plan) { // Asked again after confirming means the price moved. if (confirmedTotal !== undefined) setOutcome(PRICE_CHANGED); setQuote({ plan: result.plan, paidThrough: result.paidThrough ?? paidThrough }); } else if (result.status === 200) { onReopened(`${practiceName} is open again. Cancelled appointments stay cancelled.`); } else if (result.code === "billing-link-pending") { onReopened(BILLING_LINK_PENDING); } else if (result.code === "not-closed") { onChanged(); } else { const refusal = REOPEN_OUTCOMES[result.code ?? ""] ?? (result.status === 403 ? REOPEN_OUTCOMES["not-the-owner"] : undefined); setOutcome(refusal ?? UNCONFIRMED); // An answer this page cannot place may still have reopened it; the closure read says which. An unresolved // attempt is read again too, so the saved state, not this answer, decides what is offered next. if (!refusal || unresolved) onChanged(); } } catch { if (!isCurrent(attempt)) return; setHeld(true); setQuote(undefined); setOutcome(UNCONFIRMED); onChanged(); } finally { release(attempt); } }; const byYou = <>Closed by you{closedOn ? ` on ${closedOn}` : ""} · archived, nothing deleted; if (closing) { const planNotStopped = closure?.billing?.state === "failed"; const stalled = planNotStopped || closure?.run?.state === "needs-retry"; return (

Closing {practiceName}

{byYou}

{PROCESSING}

{closure?.acknowledgedAppointments !== undefined && (

{`You confirmed ${counted(closure.acknowledgedAppointments, "appointment", "appointments")} before closing.`}

)} {totals && totals.unresolved > 0 &&

{UNREACHED}

} {stalled && (
{planNotStopped ? "The plan has not been stopped from renewing yet" : "Some steps need checking"}
)} {outcome && }

You can reopen the practice once closing has finished.

); } const hasPlan = closure?.billing?.state === "ending"; const cancelled = (closure?.acknowledgedAppointments ?? 0) > 0 || (totals?.cancelled ?? 0) > 0; const savedHeld = closure?.status === "closed" && closure.restore ? HELD_STATES[closure.restore.state] : undefined; const checkOnly = hasPlan && (held || savedHeld !== undefined); const shown = outcome ?? (savedHeld ? REOPEN_OUTCOMES[savedHeld] : undefined); return ( <> {surface === "account" ? ( <>

Practice account

{practiceName} is closed ยท only you can see this

Plan

{!hasPlan ? "No paid plan to restore. Reopening charges nothing." : paidThrough ? `Paid through ${formatBillingDate(paidThrough, timeZone)}. The plan does not renew while the practice is closed.` : "The plan does not renew while the practice is closed."}

) : ( <>
{/* The header's practice mark: its logo on the disc, else its initials. */} {logoUrl && !logoFailed ? ( {practiceName} setLogoFailed(true)} // A logo too small to show anything counts as none, as in the frame header: Tab Close stores // a 1x1 red PNG, which drew a blank red disc where admin-v2 draws the initials. onLoad={(event) => { const { naturalWidth, naturalHeight } = event.currentTarget; if (naturalWidth < SMALLEST_USABLE_IMAGE || naturalHeight < SMALLEST_USABLE_IMAGE) setLogoFailed(true); }} /> ) : ( )}

{practiceName}

{byYou}

{/* One description, so the bold leads stay in the notice's one text run. */} This practice is closed. It takes no bookings, sends nothing, and its staff have no workspace here. Everything it held is archived and unchanged. You can open it again, and you are the only person who can.
)} {totals && totals.unresolved > 0 &&

{UNREACHED}

}

Open {practiceName} again

Staff get their workspaces back and the practice appears to patients again. {hasPlan && " Your previous plan, seats and add-ons are ready to restore; review the current price before confirming paid billing."} {cancelled && ( <> {" "} Cancelled appointments are not restored ; email notices were attempted where addresses were available, and patients would have to book again. )}

{surface !== "account" && hasPlan && paidThrough && (

{`The paid period runs through ${formatBillingDate(paidThrough, timeZone)}; the plan does not renew while the practice is closed.`}

)} {quote && } {shown && }
{checkOnly ? ( ) : hasPlan && !quote ? ( ) : ( )}

If you cannot

Unable to reopen the practice?{" "} Contact Akashic support for help recovering access and reopening it.

); }; export default PracticeClosureStatus;