import { useEffect, useMemo, useState } from "react"; import { Outlet, useLocation, useNavigate } from "react-router-dom"; import { useAuthServiceContext } from "../../../services/AuthService"; import { useStaffPermissionServiceContext } from "../../../services/StaffPermissionService"; import { useOrganizationServiceContext } from "../../../services/OrganizationService"; import { HealthcareService, Location, Organization, } from "fhir/r4"; import { useFhirHealthcareServices, useFhirLocations, useFhirPractitioners, } from "../../../hooks"; import { onlySites } from "../../../utils/roomLocation"; import EditLocationPage from "./LocationsPage/LocationPage/LocationPage"; import LocationDetailsModal from "./LocationDetailsModal"; import PracticeDashboardPage, { type ListCount } from "./PracticeDashboardPage"; import AddServiceModal from "./AddServiceModal"; import ServicesListModal from "./ServicesListModal"; import { getClinicianSeatHolders } from "./ClinicianSeatsCard.tsx"; import { isStaffExperience } from "../../../utils/mountContext"; import { BaaRecord, useBAAService } from "../../../services/BAAService.tsx"; import { useOrganizationOwnerService, type OwnerState } from "../../../services/OrganizationOwnerService.tsx"; import { referencesPractitioner } from "../../../components/OrganizationOwnerNotice/ownerReference"; import { useBillingContext } from "../../../services/BillingContext"; import { latestJoinedInvitee, latestPendingInvitee, pendingInvitationsLine, useStaffInvitations } from "../UsersPage/StaffInvitations"; import { announcePracticeUnlocked } from "../../../utils/practiceUnlocked"; import { useRefreshSessionOrganization } from "../../../hooks/useRefreshSessionOrganization"; import { findPrimaryLocation, hasPracticeContact, hasPrimaryLocation, hasReviewedServices, practiceStatusRead, type SetupFacts, } from "../../../utils/practiceSetupSteps"; import { msaSetupFact } from "../../../utils/msaAgreement"; import { observedBaaActive } from "./BaaPage/baaSignOutcome"; import { olderThan } from "../../../utils/versionOrder"; type OrganizationEditorState = | { kind: "location"; resourceId: string; location?: Location | null; setAsPrimaryAddress?: boolean; } | { kind: "service"; resourceId: string; healthcareService?: HealthcareService | null; } | null; // A refused or failed read is not an empty list. const listCount = (error: unknown, bundle: unknown, count: number): ListCount => error ? "failed" : bundle ? count : "checking"; const OrganizationPage = () => { const navigate = useNavigate(); const location = useLocation(); const normalizedPath = location.pathname.replace(/\/+$/, ""); const isOverviewRoute = normalizedPath === "/dashboard/organization"; const { wpOrganization, currentUser, USER_ROLES } = useAuthServiceContext(); const { activeWorkspace, canView, canManage } = useStaffPermissionServiceContext(); const isStaffContext = isStaffExperience(); const canViewOrganization = !isStaffContext || canView("organization"); const canManageOrganization = isStaffContext && canManage("organization"); const staffInvitations = useStaffInvitations(isStaffContext && canManage("staff")); /** * - THE SERVER AUTHORIZES THIS PERSON AND THE CLIENT GAVE THEM NO DOOR. * * The three BAA write routes gate on OWNERSHIP (`Baa/ownerGate.ts`), and that fence was deliberately * WIDENED from `[ADMINS]` after a sitting Owner on staging turned out to be a receptionist - an * admins-only fence denied her before any handler ran. This line re-created that one-way door in a * worse place: it wraps the ENTIRE Agreements block, so the reader saw no disabled control and no * explanation - just an absence. The portal is dead until the BAA is active, so a practice whose * Owner is not an Admin could not go live at all. * * NOT "show it to everyone who is not an Admin". That would satisfy *"the Owner can now sign"* and * quietly hand the practice's BAA to the front desk. It follows OWNERSHIP - the same predicate the * server uses - so a non-Owner non-Admin still sees nothing. * * AND IT CANNOT OVER-GRANT, which is what makes the OR safe: the server gate is unconditional and * refuses everyone who is not the sitting Owner, admins included. Showing the section is showing a * door, never opening one. * * `=== true` deliberately: `undefined` is "the owner read has not answered", including when it * FAILED, and a failed read must not be read as "you are the owner". */ const [viewerIsOwner, setViewerIsOwner] = useState( undefined, ); const canManageBaa = isStaffContext && (activeWorkspace === USER_ROLES.ADMIN || viewerIsOwner === true); const { getBaa } = useBAAService(); const { fetchOwner } = useOrganizationOwnerService(); const practitionerId = currentUser?.fhir?.resourceType === "Practitioner" ? currentUser.fhir.id : undefined; const [baaRecord, setBaaRecord] = useState(undefined); const [baaChecked, setBaaChecked] = useState(false); // "checking" until the read answers; undefined when it failed. const [owner, setOwner] = useState("checking"); const [ownerReads, setOwnerReads] = useState(0); // Bumped after the owner accepts the MSA, so the agreement status and the setup steps read it again. const [agreementReads, setAgreementReads] = useState(0); useEffect(() => { if (!isOverviewRoute || !wpOrganization.current?.id) return; let active = true; setViewerIsOwner(undefined); setOwner("checking"); fetchOwner(wpOrganization.current.id) .then((state) => { if (active) { setOwner(state); setViewerIsOwner(referencesPractitioner(state.ownerPractitionerId, practitionerId)); } }) .catch(() => { if (active) { setOwner(undefined); setViewerIsOwner(undefined); } }); return () => { active = false; }; }, [fetchOwner, isOverviewRoute, practitionerId, wpOrganization.current?.id, ownerReads]); const { fetchMyPracticeHostname, } = useOrganizationServiceContext(); // Whether the practice holds its permanent web address: "checking" until read, undefined on failure. const [addressClaimed, setAddressClaimed] = useState("checking"); const [addressReads, setAddressReads] = useState(0); useEffect(() => { if (!isOverviewRoute || !canManageOrganization) return; let active = true; setAddressClaimed("checking"); fetchMyPracticeHostname() .then((mine) => { if (active) setAddressClaimed(mine.claimed); }) .catch(() => { if (active) setAddressClaimed(undefined); }); return () => { active = false; }; }, [canManageOrganization, fetchMyPracticeHostname, isOverviewRoute, wpOrganization.current?.id, addressReads]); const { locationsList, locationsBundle, error: locationsError, loadLocations } = useFhirLocations({ autoLoad: isOverviewRoute && canViewOrganization, }); const billing = useBillingContext(); const { healthcareServicesList, healthcareServicesBundle, error: servicesError, loadHealthcareServices, } = useFhirHealthcareServices({ autoLoad: isOverviewRoute && canViewOrganization, }); // - `loading` is passed on DELIBERATELY. `practitionersList` initialises to `[]`, so an // unloaded list and a practice with no clinicians are the same value, and the seat card cannot tell them // apart on its own. Without this it renders "0 active clinician seats - your first clinician is free" // before the staff have arrived, which is a promise about money made from data we do not have yet. const { practitionersList, practitionersBundle, error: practitionersError, loadPractitioners } = useFhirPractitioners({ autoLoad: isOverviewRoute && canViewOrganization, }); const [displayOrg, setDisplayOrg] = useState( wpOrganization.current, ); const [activeEditor, setActiveEditor] = useState(null); const [showAddService, setShowAddService] = useState(false); const [showServicesList, setShowServicesList] = useState(false); const [detailsLocation, setDetailsLocation] = useState(null); useEffect(() => { setDisplayOrg(wpOrganization.current); }, [wpOrganization.current]); useEffect(() => { if (isStaffContext && !canViewOrganization) { navigate('/dashboard/profile', { replace: true }); } }, [canViewOrganization, isStaffContext, navigate]); useEffect(() => { if (!isOverviewRoute || !canManageBaa) return; setBaaRecord(undefined); setBaaChecked(false); getBaa() .then((record) => { setBaaRecord(record); }) .catch(() => setBaaRecord(undefined)) .finally(() => setBaaChecked(true)); }, [canManageBaa, getBaa, isOverviewRoute, ownerReads, agreementReads]); const siteLocationsList = useMemo( () => onlySites(locationsList), [locationsList], ); // Setup progress from confirmed server state: a read in flight is "checking" and a failed one is // undefined, so neither is reported as done or as still to do. const setupFacts: Omit = { agencyEnrolled: !billing.billingChecked ? "checking" : billing.entitlementsUnavailable ? undefined : Boolean(billing.subscriptionPayer), ownerAssigned: owner === "checking" ? "checking" : owner ? !owner.needsOwner : undefined, baaActive: canManageBaa ? baaChecked ? observedBaaActive(baaRecord) : "checking" : viewerIsOwner === undefined && owner === "checking" ? "checking" : undefined, primaryLocationReady: locationsError ? undefined : locationsBundle ? hasPrimaryLocation(displayOrg, locationsList) : "checking", contactReady: hasPracticeContact(displayOrg), servicesReviewed: hasReviewedServices(displayOrg), ...(canManageBaa ? { msaAccepted: baaChecked ? (baaRecord ? msaSetupFact(baaRecord.msa) : undefined) : "checking" as const } : {}), }; const primaryLocation = findPrimaryLocation(displayOrg, locationsList); // The owner gate's refusal reaches the lists as a bare 403; only the exempt owner read says why. const operability = owner && owner !== "checking" ? owner.operability : undefined; const shut = operability?.operable === false && operability.message ? { ...operability, message: operability.message } : undefined; const statusRead = practiceStatusRead({ listError: Boolean(locationsError || servicesError || practitionersError), listsLoaded: Boolean(locationsBundle && healthcareServicesBundle && practitionersBundle), billingChecked: billing.billingChecked, billingUnavailable: billing.entitlementsUnavailable, }); const refreshSessionOrganization = useRefreshSessionOrganization(); const reloadRestoredSetup = () => { void refreshSessionOrganization(); // The list services answer from a five-minute cache unless forced, which is the pre-Restore setup. void loadLocations(true); void loadHealthcareServices(true); void loadPractitioners(true); }; const retryStatus = () => { if (locationsError) void loadLocations(); if (servicesError) void loadHealthcareServices(); if (practitionersError) void loadPractitioners(); if (billing.entitlementsUnavailable === "failed") void billing.refreshBilling(); }; // Accepting an owner lifts the server's owner gate, so the reads it refused are asked again. const rereadAfterOwnerChange = () => { setOwnerReads((n) => n + 1); announcePracticeUnlocked(); retryStatus(); if (billing.entitlementsUnavailable === "denied") void billing.refreshBilling(); }; const openServiceEditor = ( healthcareService?: HealthcareService | null, ) => { if (!canManageOrganization) return; setActiveEditor({ kind: "service", resourceId: healthcareService?.id || "new", healthcareService, }); }; const closeEditor = () => { setActiveEditor(null); }; const handleLocationEditorComplete = () => { closeEditor(); }; const handleServiceEditorComplete = () => { closeEditor(); }; if (!isOverviewRoute) { return ; } return (
setAgreementReads((n) => n + 1)} onOwnerChanged={rereadAfterOwnerChange} onOperatingScopeChanged={(saved) => setDisplayOrg((org) => (org && org.id === saved.id && !olderThan(saved, org) ? saved : org))} addressClaimed={addressClaimed} onAddressClaimed={() => setAddressClaimed(true)} statusRead={statusRead} onRetryStatus={retryStatus} onRetryAddress={() => setAddressReads((n) => n + 1)} primaryLocation={primaryLocation} canManageOrganization={canManageOrganization} canViewStaff={!isStaffContext || canView("staff")} viewerIsOwner={viewerIsOwner} shut={shut} onSandboxRestored={reloadRestoredSetup} /> {activeEditor?.kind === "location" && ( )} {detailsLocation && ( setDetailsLocation(null)} onOpenStaff={(member) => { setDetailsLocation(null); if (member.practitioner.id) { navigate(`/dashboard/staff/${member.practitioner.id}`, { state: { practitioner: member.practitioner, roles: member.roles, }, }); } }} /> )} {activeEditor?.kind === "service" && ( )} {showAddService && ( setShowAddService(false)} /> )} {showServicesList && ( { setShowServicesList(false); openServiceEditor(service); }} onRequestClose={() => setShowServicesList(false)} /> )}
); }; export default OrganizationPage;