import { useCallback, useEffect, useRef, useState } from "react";
import { useNavigate, useSearchParams } from "react-router-dom";
import { ChevronRight, CreditCard, FileText, Scale, Share2, ShieldCheck, TriangleAlert, UsersRound, Wallet } from "lucide-react";
import { useAuthServiceContext } from "../../../services/AuthService";
import { useStaffPermissionServiceContext } from "../../../services/StaffPermissionService";
import { useBillingContext } from "../../../services/BillingContext";
import { useFhirPractitioners } from "../../../hooks";
import { useBAAService, type BaaRecord } from "../../../services/BAAService";
import { useOrganizationOwnerService, type OwnerState } from "../../../services/OrganizationOwnerService";
import { useOrganizationContractorsService, type OrganizationContractorRow } from "../../../services/OrganizationContractorsService";
import OrganizationOwnerNotice from "../../../components/OrganizationOwnerNotice/OrganizationOwnerNotice";
import { ownerCandidatesOf } from "../../../components/OrganizationOwnerNotice/ownerCandidates";
import { referencesPractitioner } from "../../../components/OrganizationOwnerNotice/ownerReference";
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "../../../components/ui/sheet";
import { formatFhirHumanName } from "../../../utils/fhirHumanName";
import { practiceTimeZoneOf } from "../../../utils/practiceTimezone";
import { chargeDay } from "../../SubscriptionPage/chargeDate";
import { formatDayMonthYear } from "../../../utils/dateUtils";
import ContractorAccessSection from "./ContractorAccessSection";
import AgencyAutopaySection, { autopayStatus } from "./AgencyAutopaySection";
import Part2DeclarationSection from "./Part2DeclarationSection";
import { useContractorAutopayService, type AutopayPage } from "../../../services/ContractorAutopayService";
import { isSaveRetired, useSaveScope } from "../../../utils/saveOperation";
import { isAutopayOperationId, autopayHolder, readAutopayHold, writeAutopayHold, newAutopayOperationId } from "../../../utils/autopaySetup";
import PracticeClosureStatus from "./PracticeClosure/PracticeClosureStatus";
import { PRACTICE_CLOSURE_PATH } from "./PracticeClosure/closureText";
import { isUnderClosure, usePracticeClosure } from "../../../services/PracticeClosureService";
import { readOperatingScope } from "../../../utils/operatingScope";
import "./PracticeAccountPage.css";
type Panel = "agency" | "owner" | "autopay" | "part2";
const formatMoney = (cents: number) =>
new Intl.NumberFormat(undefined, { style: "currency", currency: "USD" }).format(cents / 100);
// The card on file, or nothing when the server sent no usable detail.
const cardLabel = (method: { brand: string; last4: string; label: string } | null) =>
method?.label?.trim() || (method?.last4 ? `${method.brand ? `${method.brand} ` : "Card "}ending ${method.last4}` : undefined);
// admin-v2 .acctrow: one account destination on the shared list card, its icon at the left and the
// chevron at the right. A row that opens nothing (a status that could not be read) draws no chevron.
const AccountRow = ({ icon: Icon, title, details, onOpen }: {
icon: typeof FileText;
title: string;
details: (string | undefined)[];
onOpen?: () => void;
}) => {
const body = (
<>
{title}
{details.filter(Boolean).map((detail) => {detail} )}
{onOpen && }
>
);
const layout = "ak-row min-h-16 w-full items-start gap-[14px] text-left";
return onOpen
? {body}
:
{body}
;
};
// The owner banner is static; ownership that needs action keeps the owner notice at the top.
const PracticeAccountPage = () => {
const navigate = useNavigate();
const { currentOrganization, currentUser, USER_ROLES, actingAs, actingAsSessionRef } = useAuthServiceContext();
const receivingOnly = readOperatingScope(currentOrganization) === "receiving";
const { activeWorkspace, canManage } = useStaffPermissionServiceContext();
const billing = useBillingContext();
const { practitionersList } = useFhirPractitioners();
const { getBaa } = useBAAService();
const { fetchOwner } = useOrganizationOwnerService();
const { listContractors } = useOrganizationContractorsService();
const { listArrangements } = useContractorAutopayService();
const canManageOrganization = canManage("organization");
const isAdminWorkspace = activeWorkspace === USER_ROLES.ADMIN;
const practitionerId = currentUser?.fhir?.resourceType === "Practitioner" ? currentUser.fhir.id : undefined;
const isViewer = (reference: string | undefined) => referencesPractitioner(reference, practitionerId);
const [owner, setOwner] = useState("checking");
const [baaRecord, setBaaRecord] = useState("checking");
const [contractors, setContractors] = useState("checking");
const [panel, setPanel] = useState(null);
// A billing warning may link straight to contractor access; it opens once, and only where that row is offered.
const [searchParams] = useSearchParams();
const askedForAgency = searchParams.get("panel") === "agency";
const openedFromLink = useRef(false);
useEffect(() => {
if (openedFromLink.current || !askedForAgency || !isAdminWorkspace) return;
openedFromLink.current = true;
setPanel("agency");
}, [askedForAgency, isAdminWorkspace]);
const autopayViewer = `${currentUser?.fhir?.resourceType}/${currentUser?.fhir?.id}|${actingAs?.relatedPersonId ?? ""}|${actingAsSessionRef?.current ?? ""}`;
const autopayOwner = JSON.stringify([currentOrganization?.id, autopayViewer, isAdminWorkspace, canManageOrganization]);
const autopayScope = useSaveScope(autopayOwner);
const [autopayRead, setAutopayRead] = useState<{ owner: string; page?: AutopayPage; checking: boolean }>({ owner: autopayOwner, checking: true });
const autopayPage = autopayRead.owner === autopayOwner ? autopayRead.page : undefined;
const arrangements = autopayRead.owner !== autopayOwner || autopayRead.checking ? "checking" : autopayPage?.arrangements;
const [autopayRefresh, setAutopayRefresh] = useState(0);
const [autopayReadingMore, setAutopayReadingMore] = useState(false);
const autopayCursors = useRef(new Set());
const { view: closure, reload: reloadClosure } = usePracticeClosure(currentOrganization?.id);
const [closureNotice, setClosureNotice] = useState<{ organizationId?: string; text: string }>();
const closureRead = typeof closure === "string" ? undefined : closure;
const underClosure = closureRead ? isUnderClosure(closureRead) : false;
const noticeFor = currentOrganization?.id;
const onReopened = (notice: string) => {
setClosureNotice({ organizationId: noticeFor, text: notice });
reloadClosure();
void billing.refreshBilling();
};
const ownerState = owner === "checking" ? undefined : owner;
const viewerIsOwner = isViewer(ownerState?.ownerPractitionerId);
const canReadBaa = canManageOrganization || viewerIsOwner === true;
useEffect(() => {
if (!currentOrganization?.id) return;
let active = true;
// A re-read keeps the last answer on screen until the new one arrives.
fetchOwner(currentOrganization.id)
.then((state) => active && setOwner(state))
.catch(() => active && setOwner(undefined));
return () => {
active = false;
};
}, [currentOrganization?.id, fetchOwner]);
// The notice reports every ownership change it makes, including a handover.
const onOwnerStateChange = useCallback((state: OwnerState | undefined) => {
if (state) setOwner(state);
}, []);
useEffect(() => {
if (!canReadBaa) return;
let active = true;
setBaaRecord("checking");
getBaa()
.then((record) => active && setBaaRecord(record))
.catch(() => active && setBaaRecord(undefined));
return () => {
active = false;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [canReadBaa, currentOrganization?.id]);
useEffect(() => {
if (!isAdminWorkspace) return;
let active = true;
setContractors("checking");
listContractors()
.then(({ contractors: rows }) => active && setContractors(rows))
.catch(() => active && setContractors(undefined));
return () => {
active = false;
};
}, [isAdminWorkspace, listContractors, currentOrganization?.id, panel]);
useEffect(() => {
const organizationId = currentOrganization?.id;
if (!isAdminWorkspace || !canManageOrganization || !organizationId) return;
const operation = autopayScope.start();
setAutopayRead({ owner: autopayOwner, checking: true });
setAutopayReadingMore(false);
autopayCursors.current.clear();
listArrangements({ ...operation.transport, organizationId })
.then((page) => { operation.admit(); if (page.nextCursor) autopayCursors.current.add(page.nextCursor); setAutopayRead({ owner: autopayOwner, page, checking: false }); })
.catch((failure) => { if (operation.belongs() && !isSaveRetired(failure)) setAutopayRead({ owner: autopayOwner, checking: false }); });
return () => operation.retire();
}, [autopayOwner, isAdminWorkspace, listArrangements, currentOrganization?.id, autopayRefresh, autopayScope]);
const readMoreAutopay = async () => {
const organizationId = currentOrganization?.id;
if (!isAdminWorkspace || !canManageOrganization || !organizationId || !autopayPage?.nextCursor || autopayReadingMore) return;
const operation = autopayScope.start();
const original = autopayPage;
setAutopayReadingMore(true);
try {
const next = await operation.step(() => listArrangements({ ...operation.transport, organizationId }, original.nextCursor));
operation.admit();
if ((next.nextCursor && autopayCursors.current.has(next.nextCursor)) || next.arrangements.some((row) => original.arrangements.some((prior) => prior.contractorId === row.contractorId && JSON.stringify(prior) !== JSON.stringify(row)))) throw new Error("The auto-pay list could not be completed.");
if (next.nextCursor) autopayCursors.current.add(next.nextCursor);
setAutopayRead({ owner: autopayOwner, checking: false, page: { ...next, arrangements: [...original.arrangements, ...next.arrangements.filter((row) => !original.arrangements.some((prior) => prior.contractorId === row.contractorId))] } });
} catch {
if (operation.belongs()) setAutopayRead({ owner: autopayOwner, checking: false, page: { ...original, complete: false } });
} finally { if (operation.belongs()) setAutopayReadingMore(false); operation.retire(); }
};
useEffect(() => {
if ((!isAdminWorkspace || !canManageOrganization) && panel === "autopay") setPanel(null);
}, [isAdminWorkspace, canManageOrganization, panel]);
useEffect(() => {
const returnedOperation = searchParams.get("autopayOperation");
const contractorId = searchParams.get("autopayContractor");
if (!isAdminWorkspace || !currentOrganization?.id || !isAutopayOperationId(returnedOperation) || !contractorId) return;
const row = autopayPage?.arrangements.find((entry) => entry.contractorId === contractorId && entry.setup?.operationId === returnedOperation);
if (!row) return;
const holder = autopayHolder(autopayViewer, currentOrganization.id, contractorId);
if (!readAutopayHold(holder)) writeAutopayHold(holder, { operationId: returnedOperation, opening: newAutopayOperationId(), attempt: newAutopayOperationId(), sent: true });
setPanel("autopay");
}, [searchParams, autopayViewer, currentOrganization?.id, isAdminWorkspace, autopayPage]);
const ownerNeedsAttention = Boolean(
ownerState && (ownerState.needsOwner || isViewer(ownerState.pendingTransferToPractitionerId)),
);
const nameOf = (reference: string | undefined) => {
const id = reference?.split("/").pop();
const found = practitionersList.find((entry) => entry.practitioner.id === id)?.practitioner.name?.[0];
return found ? formatFhirHumanName(found) : undefined;
};
// A custom agreement awaiting counter-signature arrives in `pending`, beside whatever is in force.
const baaAwaitingCountersign =
baaRecord !== "checking" && baaRecord !== undefined && (Boolean(baaRecord.pending) || baaRecord.status === "pending");
const baaSummary =
baaRecord === "checking"
? "Checking…"
: baaRecord === undefined
? "BAA status unavailable."
: baaRecord.currentUnconfirmed
? "Your signature is recorded; the agreement now in force could not be confirmed."
: baaRecord.status === "active"
? `Signed${baaRecord.signedAt ? ` ${formatDayMonthYear(baaRecord.signedAt)}` : ""}${baaRecord.signedBy?.name ? ` by ${baaRecord.signedBy.name}${baaRecord.signedBy.title ? `, ${baaRecord.signedBy.title}` : ""}` : ""}.${baaAwaitingCountersign ? " A replacement is waiting for our counter-signature." : ""}`
: baaAwaitingCountersign
? "Waiting for our counter-signature."
: "Not yet signed.";
const subscription = billing.subscription;
// Billing is the practice's, so its dates are the practice's calendar days, as on the Subscription page.
const practiceZone = practiceTimeZoneOf(currentOrganization) ?? undefined;
const subscriptionFacts = subscription
? [
`${formatMoney(subscription.monthlyTotalCents)}/month`,
subscription.cancelAtPeriodEnd
? subscription.paidThrough ? `paid through ${formatDayMonthYear(chargeDay(subscription.paidThrough, subscription.paidThroughInstant, practiceZone))}, will not renew` : "will not renew"
: subscription.nextChargeAt ? `next charge ${formatDayMonthYear(chargeDay(subscription.nextChargeAt, subscription.nextChargeInstant, practiceZone))}` : undefined,
cardLabel(subscription.paymentMethod),
].filter(Boolean).join(" · ") + "."
: undefined;
const activeAgencies = Array.isArray(contractors) ? contractors.filter((c) => !c.revokedAt) : [];
const agencySummary =
contractors === "checking"
? "Checking…"
: contractors === undefined
? "Contractor access could not be checked."
: activeAgencies.length === 0
? "No contractor currently has access. Review access history or authorize a contractor."
: `${activeAgencies.map((c) => c.name || "A contractor").join(", ")} ${activeAgencies.length === 1 ? "has" : "have"} platform manager access.`;
const ownerName = nameOf(ownerState?.ownerPractitionerId);
const ownerSince = ownerState?.acceptedAt ? formatDayMonthYear(ownerState.acceptedAt) : undefined;
const ownerSummary =
owner === "checking"
? "Checking…"
: owner === undefined
? "Ownership could not be checked."
: ownerState?.needsOwner
? "This practice has no owner yet."
: [
ownerName && ownerSince
? `${ownerName} · owner since ${ownerSince}.`
: ownerName ? `${ownerName} owns this practice.` : viewerIsOwner ? "You own this practice." : "This practice has an owner.",
ownerState?.pendingTransferToPractitionerId
? `The owner stays the same until ${nameOf(ownerState.pendingTransferToPractitionerId) ?? "the nominated person"} accepts.`
: undefined,
].filter(Boolean).join(" ");
return (
Practice account
Manage your agreement, subscription, ownership and contractor access.
{closureNotice && closureNotice.organizationId === currentOrganization?.id && (
{closureNotice.text}
)}
{underClosure && closureRead ? (
) : ownerNeedsAttention ? (
) : viewerIsOwner ? (
You own this practice
Only the practice owner can transfer ownership or close this practice.
) : null}
{!underClosure && (
navigate("../baa", { relative: "path" })} />
{!receivingOnly && (
navigate("/dashboard/subscription")}
/>
)}
{isAdminWorkspace && setPanel("agency")} />}
{isAdminWorkspace && Array.isArray(arrangements) && arrangements.length > 0 && (
(row.contractorName || "Your contractor") + ": " + autopayStatus(row) + ".")}
onOpen={() => setPanel("autopay")}
/>
)}
{isAdminWorkspace && (arrangements === undefined || autopayPage?.complete === false) && (
setPanel("autopay")} />
)}
setPanel("owner")} />
{/* Admins only, as the route admits: the practice's own declaration, opened and read fresh each time. */}
{isAdminWorkspace && currentOrganization?.id && (
setPanel("part2")}
/>
)}
{closureRead?.isOwner ? (
navigate(PRACTICE_CLOSURE_PATH)}
/>
) : closure === "denied" || closure === "failed" ? (
) : null}
)}
!open && setPanel(null)}>
{panel === "agency" ? "Contractor access" : panel === "autopay" ? "Contractor auto-pay" : panel === "part2" ? "Part 2 program status" : "Practice owner"}
{panel === "agency"
? "Who from an outside contractor can manage this practice."
: panel === "autopay"
? "What your contractor may charge for its services, and the card it uses."
: panel === "part2"
? "Whether this practice's programs are covered by 42 CFR Part 2."
: "The one person nobody else can remove from this practice's account."}
{panel === "agency" && (
)}
{panel === "autopay" && isAdminWorkspace && currentOrganization?.id && (
{Array.isArray(arrangements) &&
}
{arrangements === "checking" &&
Checking contractor auto-pay…
}
{(arrangements === undefined || autopayPage?.complete === false) &&
Contractor auto-pay could not be checked completely.
}
{autopayPage?.nextCursor &&
void readMoreAutopay()}>Check remaining setups }
setAutopayRefresh((value) => value + 1)}>Check contractor auto-pay
)}
{panel === "part2" && isAdminWorkspace && currentOrganization?.id && (
)}
{panel === "owner" && (
)}
);
};
export default PracticeAccountPage;