import type { PracticeOperability } from "../../../services/OrganizationOwnerService"; import { useEffect, useRef, useState, type CSSProperties } from "react"; import { useDirtyGuard } from "../../../hooks/useDirtyReport"; import { Link, useNavigate, useSearchParams } from "react-router-dom"; import { BellRing, Building2, CalendarCheck2, CalendarDays, ChevronRight, ClipboardPlus, CreditCard, FlaskConical, Hospital, IdCardLanyard, MessagesSquare, RotateCw, Share2, ShieldCheck, TriangleAlert, Video, } from "lucide-react"; import type { Location, Organization } from "fhir/r4"; import { useBillingContext } from "../../../services/BillingContext"; import { useOrgAuthorizationServiceContext } from "../../../services/OrgAuthorizationService"; import { resolveOrgAppointmentPolicy } from "../../../config/appointmentPolicy"; import { getOrganizationPolicyFlags } from "../../../config/bookingPolicy"; import { readOrganizationNpi } from "../../../utils/organizationNpi"; import { offersDiagnosticsSetup, readDiagnosticsDeclaration } from "../../../utils/organizationDiagnostics"; import { acceptsInSystemReferrals, resolveOrgReferralRouting } from "../../../config/referralRouting"; import { readOperatingScope } from "../../../utils/operatingScope"; import { useOrganizationServiceContext } from "../../../services/OrganizationService"; import type { BaaRecord } from "../../../services/BAAService"; import { observedBaaActive, signedRecordFor, type SignProof } from "./BaaPage/baaSignOutcome"; import { addressGate, buildSetupSteps, hidesUntilOperating, needsFounderOwnerConfirmation, practiceCategoryCodes, setupSettledDespiteUnreadStatus, setupView, portalPagesReady, withPortalPages, type PracticeStatusRead, type SetupFacts, type SetupStepId, } from "../../../utils/practiceSetupSteps"; import { useRunWhenLeaving } from "../../../hooks/useRunWhenLeaving"; import { useOwnedSettingSave } from "../../../hooks/useOwnedSettingSave"; import { formatSingleAddress } from "../../../utils/fhirAddress"; import { formatPhoneNumber } from "../../../utils/phoneNumber"; import { locationTimeZoneOf, validLocationTimeZoneOf } from "../../../utils/practiceTimezone"; import { olderThan } from "../../../utils/versionOrder"; import { formatDayMonthYear } from "../../../utils/dateUtils"; import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "../../../components/ui/sheet"; import { OverlayActions, OverlayFooter, OverlayFooterHost } from "../../../components/ui/overlay-actions"; import { Spinner } from "../../../components/ui/spinner"; import DiscardChangesDialog from "../../../components/DiscardChangesDialog/DiscardChangesDialog"; import { useSheetBackGuard } from "../../../hooks/useSheetBackGuard"; import BookingAndRemindersPage from "./BookingAndRemindersPage"; import AgreementsPage from "./AgreementsPage"; import ClinicianReadinessPage, { useClinicianReadiness } from "./ClinicianReadinessPage"; import { videoVisitsCard } from "./videoVisitsCard"; import FirstClinicianPanel from "./FirstClinicianPanel"; import IdentityPanel from "./IdentityPanel"; import IdentityFacts from "./IdentityFacts"; import AddLocationForm from "./LocationsPage/AddLocationForm"; import ConfirmPracticeOffice from "../../../components/ReceivingOffice/ConfirmPracticeOffice"; import LocationTimeZoneRepair from "./LocationsPage/LocationTimeZoneRepair"; import BaaSignForm from "./BaaPage/BaaSignForm"; import MsaAcceptForm from "./BaaPage/MsaAcceptForm"; import PracticeAddressPanel, { type View as AddressView } from "../../../components/PracticeAddress/PracticeAddressPanel"; import OrganizationOwnerNotice from "../../../components/OrganizationOwnerNotice/OrganizationOwnerNotice"; import ConfirmPracticeOwner from "./ConfirmPracticeOwner"; import { getStaffCreationBlockers } from "../../../utils/organizationSetup"; import { ownerCandidatesOf } from "../../../components/OrganizationOwnerNotice/ownerCandidates"; import { useAuthServiceContext } from "../../../services/AuthService"; import { formatFhirHumanName } from "../../../utils/fhirHumanName"; import { useFhirPractitioners } from "../../../hooks"; import { getOrganizationRoleCodes, STAFF_WORKSPACES } from "../../../utils/staffRoleCatalog"; import PracticeCategoriesStep from "./PracticeCategoriesStep"; import PracticeDoor from "./PracticeDoor"; import PracticeSetupGuide, { setupStepCopy } from "./PracticeSetupGuide"; import PatientPagesDoor from "./PatientPagesDoor"; import PortalPagesPending from "./PortalPagesPending"; import { isHostedMount } from "../../../utils/hostedMount"; import PatientPortalPageSettings from "../PatientPortalPage/PatientPortalPageSettings"; import { getPatientPortalUrl } from "../../../utils/patientPortalUrl"; import { getStaffPortalUrl } from "../../../utils/staffPortalUrl"; import { getPortalPages, type PortalPageState, type PortalPageType } from "../../../utils/wpAdminApi"; import { portalPagesReadable } from "../../../utils/portalPagesReadable"; import { onboardingCollapseKey } from "../../../components/OrgOnboarding/onboardingCollapse"; import PracticeSandboxStep, { SandboxWayBack, showsSandboxStep, showsSandboxWayBack } from "./PracticeSandboxStep"; import SandboxSamplePanel from "../../../components/SandboxMode/SandboxSamplePanel"; import { environment } from "../../../environments/environment"; import { PracticeMark } from "../MyProvidersPage/OrgListCard"; import "./PracticeDashboardPage.css"; // "role-limited" is scope, not a fault: drawn neutral and never counted as needing attention. type CapabilityStatus = "working" | "needs-setup" | "needs-attention" | "role-limited"; type Capability = { id: string; icon: React.ReactNode; title: string; detail: string; status: CapabilityStatus; statusLabel: string; onOpen?: () => void; }; type ManageCard = { id: string; icon: React.ReactNode; title: string; detail: string; onOpen?: () => void; retry?: { what: string; run: () => void }; }; type PracticeDashboardPageProps = { organization?: Organization | null; locationsCount: ListCount; servicesCount: ListCount; peopleCount: ListCount; /** Unanswered clinician invitations, e.g. "1 pending invitation"; absent when there are none. */ pendingInvitations?: string; /** The latest invitee still to answer, named on the clinician step while it waits on them. */ pendingInvitee?: { name: string; email: string }; /** The latest clinician who joined; the step names them until somebody can be booked. */ joinedInvitee?: { name: string }; clinicianSeatCount: ListCount; baaRecord: BaaRecord | undefined; /** False when this reader is never sent the BAA read (administrators only): a scope, not a failure. */ baaReadable?: boolean; setupFacts: Omit; /** What was observed now, with this practice's own receipt carried as its own fact (see signedRecordFor). */ onBaaSigned: (record: BaaRecord) => void; /** The owner accepted the MSA; the agreement status is read again. */ onMsaAccepted?: () => void; onOwnerChanged: () => void; /** The signed-in practitioner is the practice's accepted owner. */ viewerIsOwner?: boolean; /** Set when the owner gate refuses this practice; its message says why and what to do. */ shut?: PracticeOperability & { message: string }; addressClaimed: boolean | "checking" | undefined; onAddressClaimed: () => void; onRetryAddress: () => void; statusRead: PracticeStatusRead; onRetryStatus: () => void; primaryLocation?: Location; canManageOrganization: boolean; /** staff: view; a door to the staff directory is offered only to someone who may read it. */ canViewStaff?: boolean; onSandboxRestored?: () => void; /** The practice chose to operate here; the page shows the full setup from the stored scope. */ /** The practice as stored after it switched to operating. */ onOperatingScopeChanged?: (organization: Organization) => void; }; type Overlay = | "address" | "identity" | "booking-reminders" | "agreements" | "clinician-readiness" | "setup-owner" | "setup-msa" | "setup-baa" | "setup-location" | "setup-contact" | "setup-services" | "setup-clinician" | "portal-pages"; // Sheets whose cards stand on the page ground; on the white sheet a card's lift reads on one side only. const PAPER_OVERLAYS = new Set(["clinician-readiness", "portal-pages"]); type OverlayCopy = { title: string; step?: SetupStepId; subtitle?: string; unsaved?: string }; // A list that failed or was refused is not loaded, which is not the same as an empty practice. export type ListCount = number | "checking" | "failed"; // An ownerless practice's reads are refused by the owner gate; "Couldn't load" would send the admin to retry. const WAITING_FOR_OWNER = "Waiting for an owner"; const WAITING_FOR_AGENCY = "Waiting for the contractor to sign its agreement"; const countLine = (count: ListCount, what: string, one: string, many: string, blocked?: string) => count === "failed" ? blocked ?? "Couldn't load " + what : count === "checking" ? "Checking " + what : count + " " + (count === 1 ? one : many); const BOOKING_MODE_LABEL = { direct: "Direct booking", review: "Staff review", "request-only": "Request only" } as const; const formatMonthly = (cents: number) => new Intl.NumberFormat(undefined, { style: "currency", currency: "USD" }).format(cents / 100); const PRIMARY_ZONE_HELP = "Schedules use this time zone. Patients see appointment times in their own time zone, with a notice when the two differ."; type LocationStep = { mode: "add" } | { mode: "settle"; location: Location }; // No primary yet: add it. Otherwise the step settles that site, as saved or as needing its zone. const locationStepFor = (primary: Location | undefined): LocationStep => primary ? { mode: "settle", location: primary } : { mode: "add" }; // The current read of the step site, unless it is an older version of the one the step accepted. const notOlder = (current: Location | undefined, accepted: Location): Location => current && (current.id !== accepted.id || !olderThan(current, accepted)) ? current : accepted; // The address sheet is titled by the view it is on, as the deck titles its confirm and saved boards. const ADDRESS_VIEW_TITLE: Record, string> = { confirm: "Save this permanent address?", saved: "Practice web address saved", }; const OVERLAY_COPY: Record = { address: { title: "Choose your practice web address", subtitle: "Comes first, before the rest of practice setup", unsaved: "Your address has not been saved.", }, identity: { title: "Identity", unsaved: "Your changes to Identity have not been saved." }, "booking-reminders": { title: "Booking rules", subtitle: "How this practice takes appointments", unsaved: "Your booking rule changes have not been saved." }, agreements: { title: "Agreements" }, "clinician-readiness": { title: "Clinician appointment readiness status", subtitle: "Active clinician seats · setup needed first" }, "setup-owner": { title: "Nominate your practice owner", step: "owner", subtitle: "the person accountable for this practice", }, "setup-msa": { title: "Accept the Master Subscription Agreement", step: "msa", subtitle: "your practice owner accepts it before the BAA", unsaved: "The agreement hasn't been accepted. Accepting later leaves this step unfinished. Your other saved setup stays unchanged.", }, "setup-baa": { title: "Sign your Business Associate Agreement", step: "baa", subtitle: "required before patients can use your portal", unsaved: "Your agreement hasn't been signed. Signing later leaves this step unfinished. Your other saved setup stays unchanged.", }, "setup-location": { title: "Your practice's primary location", step: "location", subtitle: "it becomes your first location", unsaved: "This location hasn't been saved. Discarding it leaves this step unfinished. Your other saved setup stays unchanged.", }, "setup-contact": { title: "Practice contact information", step: "contact", subtitle: "general phone and email", unsaved: "Your phone and email changes haven't been saved. Discarding them leaves this step unfinished. Your other saved setup stays unchanged.", }, "setup-services": { title: "Add your services", step: "services", subtitle: "practice service categories", unsaved: "The category you chose hasn't been added. Discarding it leaves this step unfinished. Your other saved setup stays unchanged.", }, "setup-clinician": { title: "Invite your first clinician", step: "clinician", subtitle: "invitation details", unsaved: "This invitation hasn't been saved or sent. Discarding it leaves this step unfinished. Your other saved setup stays unchanged.", }, "portal-pages": { title: "Create your portal pages", subtitle: "Practice setup · WordPress website" }, }; // The two-letter initials shown on the round mark when there is no uploaded logo — same // derivation the header avatar elsewhere in the app uses, kept local since this is the only // caller that needs just the initials rather than the full display component. const initialsOf = (name: string | undefined): string => { const words = (name ?? "").trim().split(/\s+/).filter(Boolean); if (words.length === 0) return "?"; if (words.length === 1) return words[0].slice(0, 2).toUpperCase(); return (words[0][0] + words[1][0]).toUpperCase(); }; // admin-v2 dashboard.html "Practice overview" — the Admin workspace landing surface, renamed // from a form-first Organization page to a status-first one. See // mockups/admin-v2/_implementation-notes.md: "The Admin landing surface is Practice Dashboard, // corresponding to the application's Organization route." // // Every status below reads a real signal already computed elsewhere in the app (the same // resolvers AppointmentPolicySection, ReferralRoutingSection and the billing context itself // use) rather than a second opinion assembled here. Where the underlying capability has no // client yet (Stripe Connect for patient payments), the row says "Needs setup" honestly instead // of guessing. const PracticeDashboardPage = ({ organization, locationsCount, servicesCount, peopleCount, pendingInvitations, pendingInvitee, joinedInvitee, clinicianSeatCount, baaRecord, baaReadable = true, setupFacts, onBaaSigned, onMsaAccepted, onOwnerChanged, viewerIsOwner, shut, addressClaimed, onAddressClaimed, onRetryAddress, statusRead, onRetryStatus, primaryLocation, canManageOrganization, canViewStaff = true, onSandboxRestored, onOperatingScopeChanged, }: PracticeDashboardPageProps) => { const navigate = useNavigate(); const billing = useBillingContext(); const { getAgreements } = useOrgAuthorizationServiceContext(); const [consentConfigured, setConsentConfigured] = useState(undefined); // admin-v2 dashboard.html shows these three as slide-over panels ON the dashboard, not a // page navigation away from it ("Booking rules" over a dimmed Practice Dashboard). Locations, // Services, Staff and Practice account keep their existing full-page routes - those are // pre-existing management surfaces, not new admin-v2 content. const [activeOverlay, setActiveOverlay] = useState(null); // A setup step whose save failed and can be retried says so in its step line. const [stepNotSaved, setStepNotSaved] = useState(false); useEffect(() => setStepNotSaved(false), [activeOverlay]); const [clinicianSetup, setClinicianSetup] = useState(false); const inClinicianSetup = activeOverlay === "setup-clinician" && clinicianSetup; const [clinicianMode, setClinicianMode] = useState<"invite" | "self">("invite"); const [addressView, setAddressView] = useState("choose"); const smsAddOn = billing.entitlements ? billing.entitlements.smsRemindersEnabled === true : undefined; const textReminders = smsAddOn === true && billing.smsTransactionalConfigured !== false; const inClinicianSelf = activeOverlay === "setup-clinician" && !clinicianSetup && clinicianMode === "self"; const [clinicianSeat, setClinicianSeat] = useState(false); const inClinicianSeat = activeOverlay === "setup-clinician" && !clinicianSetup && clinicianSeat; const overlayDirty = useDirtyGuard(); const [identityUnsaved, setIdentityUnsaved] = useState(); const [confirmDiscard, setConfirmDiscard] = useState(false); const [leavingTo, setLeavingTo] = useState(null); // What the location step is for, decided as it opens: adding the primary location, repairing a primary that has // no usable zone, or showing one already saved. A primary that appears while the add form is in use never swaps the // form out from under its own save; the form finishes and closes the step itself. const [locationStep, setLocationStep] = useState({ mode: "add" }); const locationFormUsed = useRef(false); // Every open and close is a new opening; an answer for an earlier one changes nothing on screen. const overlayOpening = useRef(0); const openOverlay = (overlay: Overlay) => { overlayOpening.current += 1; overlayDirty.reset(); if (overlay === "setup-location") { locationFormUsed.current = false; setLocationStep(locationStepFor(primaryLocation)); } setActiveOverlay(overlay); }; useEffect(() => { if (activeOverlay !== "setup-location" || locationStep.mode !== "add" || !primaryLocation || locationFormUsed.current) return; setLocationStep(locationStepFor(primaryLocation)); // eslint-disable-next-line react-hooks/exhaustive-deps }, [activeOverlay, locationStep.mode, primaryLocation]); // The step keeps the newest version of its site seen while open, so a later, older read never brings back a repair. useEffect(() => { if (locationStep.mode !== "settle" || !primaryLocation || primaryLocation.id !== locationStep.location.id) return; if (olderThan(locationStep.location, primaryLocation)) setLocationStep({ mode: "settle", location: primaryLocation }); }, [locationStep, primaryLocation]); // The step location as accepted: an older read of the same site never replaces the newest one seen. const stepLocation = locationStep.mode === "add" ? primaryLocation : notOlder(primaryLocation, locationStep.location); const stepZone = validLocationTimeZoneOf(stepLocation); // A link with ?open=portal-pages opens that setup step; the request is read once and cleared. const [searchParams, setSearchParams] = useSearchParams(); useEffect(() => { if (searchParams.get("open") === "portal-pages") { openOverlay("portal-pages"); setSearchParams((params) => { params.delete("open"); return params; }, { replace: true }); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [searchParams]); const closeOverlay = () => { overlayOpening.current += 1; overlayDirty.reset(); setActiveOverlay(null); }; // X, Escape, the scrim and browser Back all arrive here. Unsaved edits get the approved choice rather than // vanishing; Escape inside that choice keeps editing. const requestCloseOverlay = () => { if (overlayDirty.isDirty()) { setConfirmDiscard(true); return; } closeOverlay(); }; const sheetGuard = useSheetBackGuard(activeOverlay !== null, overlayDirty.isDirty, (to) => { setLeavingTo(to ?? null); requestCloseOverlay(); }); // From the Services door the categories sheet also leads to the services themselves, where each is edited and // turned on or off for booking. An unsaved category choice gets the same discard choice as any other way out. const openServicesList = () => { const to = "/dashboard/organization/healthcare-services"; if (overlayDirty.isDirty()) { setLeavingTo(to); setConfirmDiscard(true); return; } closeOverlay(); sheetGuard.leaveTo(to); }; const clinicians = useClinicianReadiness(); // Seating the clinician and giving them services or hours happen inside the setup sheet; read readiness again when // it closes, so the step does not stay "to do" until a reload. useRunWhenLeaving(activeOverlay, "setup-clinician", clinicians.retry); const { practitionersList } = useFhirPractitioners(); const operatingScope = readOperatingScope(organization); const coordinatorAssigned = practitionersList.some(({ roles }) => roles.some((role) => getOrganizationRoleCodes(role).includes("referral-coordinator"))); const { updateOperatingScope, fetchOrganizationAsCaller } = useOrganizationServiceContext(); const { setWpOrganization } = useAuthServiceContext(); // The switch belongs to this practice as opened here. When its answer is lost, what the practice stores is read // before the switch is offered again; nothing is sent twice on its own. const { state: operateState, save: saveOperating, check: checkOperating } = useOwnedSettingSave(organization?.id, { read: (id, transport) => fetchOrganizationAsCaller(id, undefined, id, transport), publish: setWpOrganization, }); const operating = operateState.status === "saving" || operateState.status === "checking"; const operateHeld = operating || operateState.status === "unconfirmed"; const operateError = operateState.message ?? (operateState.stored && readOperatingScope(operateState.stored) !== "full" ? "The earlier switch could not be confirmed. This practice is currently set up to receive referrals." : undefined); const startOperating = () => saveOperating((transport) => updateOperatingScope("full", transport), { onSaved: (answer) => { if (answer.organization) onOperatingScopeChanged?.(answer.organization); }, refusal: "This practice could not be switched to operating. Nothing has changed; try again.", }); const readyClinician = clinicians.readiness.find((r) => r.inPersonReady || r.telehealthReady); const setupSteps = buildSetupSteps({ ...setupFacts, ...(baaRecord?.currentUnconfirmed ? { baaActive: undefined } : {}), clinicianReady: clinicians.error ? undefined : clinicians.loading ? "checking" : Boolean(readyClinician), operatingScope, coordinatorAssigned, routingChosen: acceptsInSystemReferrals(organization), }); // A finished practice whose status could not be read shows that, not the setup guide again - but only when the // unknown steps are the ones those lists feed. An unread BAA or ownership still opens the guide. const guideView = canManageOrganization ? setupView(setupSteps) : "complete"; const essentialsView = guideView === "guide" && (statusRead === "failed" || statusRead === "denied") && setupSettledDespiteUnreadStatus(setupSteps, setupFacts.ownerAssigned) ? "complete" : guideView; // On the WordPress client the guide outlives the essentials until both portal pages exist (admin-v2 notes :47). const { availableWorkspaces, currentUser, selectedPractitionerRole, getValidToken } = useAuthServiceContext(); const [sitePages, setSitePages] = useState>(); const pagesStage = operatingScope !== "receiving" && !isHostedMount(); const readSitePages = pagesStage && !(getPatientPortalUrl(organization) && getStaffPortalUrl(organization)) && portalPagesReadable(); useEffect(() => { setSitePages(undefined); if (!readSitePages) return; let active = true; getPortalPages({ getToken: getValidToken, organizationId: organization?.id }) .then((pages) => { if (active) setSitePages(pages); }) .catch((err) => console.error("Error reading the portal pages:", err)); return () => { active = false; }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [readSitePages, organization?.id]); const portalPagesPending = pagesStage && !portalPagesReady(organization, sitePages); const setup = withPortalPages(essentialsView, portalPagesPending); const selfCandidate = currentUser?.fhir?.resourceType === "Practitioner" && currentUser.fhir.id && currentUser.fhir.name?.[0] ? { id: currentUser.fhir.id, name: formatFhirHumanName(currentUser.fhir.name[0]) } : undefined; // Agency wording only when an agency enrolled this practice and the viewer is its Platform Manager: a // founding admin holds that workspace too, and an unread enrolment gets the practice wording. The accepted // owner is inside the practice, so it never reads the agency's "their". const ownerless = setupFacts.ownerAssigned === false; const blocked = shut ? (shut.refusal === "agency-baa-not-signed" ? WAITING_FOR_AGENCY : WAITING_FOR_OWNER) : ownerless ? WAITING_FOR_OWNER : undefined; const agencySetup = setupFacts.agencyEnrolled === true && viewerIsOwner !== true && availableWorkspaces.includes(STAFF_WORKSPACES.PLATFORM_MANAGER); // Numbered as opened: a step finished inside its sheet can leave the list, as the owner step does. const [stepPosition, setStepPosition] = useState({ number: 0, total: 0 }); const openSetupStep = (id: SetupStepId) => { // The two referral steps are settled on their own pages rather than in a setup sheet. if (id === "coordinator") { navigate("/dashboard/staff"); return; } if (id === "routing") { navigate("referrals", { relative: "route" }); return; } setStepPosition({ number: setupSteps.findIndex((s) => s.id === id) + 1, total: setupSteps.length }); openOverlay(`setup-${id}`); }; // The permanent address comes before the five setup steps (hostname ruling, 2026-09-18). const address = canManageOrganization ? addressGate(addressClaimed) : "claimed"; const needsAddress = address === "choose"; const confirmOwner = canManageOrganization && address === "claimed" && needsFounderOwnerConfirmation(setupFacts); // A count still loading or unread is not a missing location; only a read zero is. const staffBlockers = getStaffCreationBlockers(organization, typeof locationsCount === "number" ? locationsCount : 1); useEffect(() => { let active = true; getAgreements(organization?.id) .then((agreements) => { if (active) setConsentConfigured(agreements.some((a) => a.text?.trim())); }) .catch(() => { if (active) setConsentConfigured(undefined); }); return () => { active = false; }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [organization?.id]); const name = organization?.name && organization.name.trim().length > 0 ? organization.name : "Your practice"; // Day first, as the deck writes it ("Tuesday 8 September"); en-US would put the month first. const today = new Intl.DateTimeFormat("en-GB", { weekday: "long", day: "numeric", month: "long" }).format(new Date()); const npi = readOrganizationNpi(organization ?? undefined); const referralPolicy = resolveOrgReferralRouting(organization ?? undefined); const telehealthEnabled = resolveOrgAppointmentPolicy(organization ?? undefined).telehealthEnabled; const videoCard = videoVisitsCard({ telehealthEnabled, clinicians }); const baaActive = observedBaaActive(baaRecord); const portalReady = baaActive && consentConfigured === true; const counts = typeof locationsCount === "number" && typeof peopleCount === "number" && typeof clinicianSeatCount === "number" ? { locations: locationsCount, people: peopleCount, seats: clinicianSeatCount } : undefined; const services = typeof servicesCount === "number" ? servicesCount : undefined; const canBookOnline = Boolean(counts && services !== undefined && counts.locations > 0 && services > 0 && counts.seats > 0); const referralsReady = Boolean(npi) && referralPolicy.destination !== undefined; const messagingReady = counts ? counts.people > 0 : undefined; const retryIf = (count: ListCount, what: string) => count === "failed" ? { what, run: onRetryStatus } : undefined; const allCapabilities: Capability[] = [ { id: "video", icon: