import { useMemo } from "react"; import { isOrglessPatientsHost } from "../../../utils/hostedMount"; import { useAuthServiceContext, usePatientServiceContext } from "../../../services"; import { useFhirAllergyIntolerances, useFhirAppointments, useFhirFamilyMemberHistories, useFhirClinicalLists, useFhirCoverages, useFhirMedicationStatements, useFhirRelatedPersons, } from "../../../hooks"; import { buildPatientSystemTasks, summarizePatientSystemTasks, type PatientSystemTaskSummary, } from "../../../utils/patientSystemTasks.ts"; /** * - THE PATIENT'S OWN SETUP STEPS, COMPUTED ONCE FOR EVERY SURFACE THAT ASKS. * * It lived inside `PatientHomeBanner` and was private to it, which is why the home page could not * answer the only question it needed to: **is the banner already offering the patient something?** * * WHAT THAT COST. The banner listed five outstanding setup items, and the cell directly beneath it * read *"Nothing needs you yet"* - measured on the patient portal on 2026-09-13. Two surfaces, one * screen, opposite claims. It is the ninth UI convention exactly: **never let an absence carry a * message**, because a screen that speaks by showing nothing cannot distinguish the reasons for * nothing. * * ONE COMPUTATION, NOT TWO. Every input here comes from a service context that caches per patient, so * a second caller costs no second read and - the part that matters - **cannot arrive at a different * answer**. That is the same argument the page's own invoice provider makes about money: three * independent loads can disagree, and a screen quoting two numbers is worse than a slow one. */ const PRACTICE_SCOPED_SETUP_STEPS = new Set(["system-related-persons", "system-insurance", "system-first-appointment"]); export const usePatientSetupSummary = (): PatientSystemTaskSummary | undefined => { const { currentUser } = useAuthServiceContext(); const { patientState } = usePatientServiceContext(); /** * - THIS CALL SITE USED TO STARVE THE THING IT CALLS. * * `buildPatientSystemTasks` computes five setup steps and was handed `medicationStatements: []`, * `allergyIntolerances: []` and a related-persons hook with `autoLoad: false`. Three of the five * could therefore never be anything but incomplete, so the maximum reachable was 2 of 5 and the * banner could never disappear - which is the one thing the spec requires of it, since disappearing * is the entire reason it is allowed to sit above the account summary. * * The completion logic was never wrong. Ed completed every item on his own laptop and the count * stayed at 1 of 5, which no test asked because none of them referenced these inputs at all. */ // Caregivers and insurance are still read through a practice, so the org-less portal leaves those // two steps to the practice portals rather than waiting on reads it cannot make. const orgless = isOrglessPatientsHost(); const { relatedPersonsList, relatedPersonsBundle } = useFhirRelatedPersons({ autoLoad: !orgless }); const { medicationStatementsList, medicationStatementsBundle } = useFhirMedicationStatements(); const { allergyIntolerancesList, allergyIntolerancesBundle } = useFhirAllergyIntolerances(); const { coverageList, coveragesBundle } = useFhirCoverages({ autoLoad: !orgless }); const { clinicalListsBundle, hasConfirmedNoCurrentMedications, hasConfirmedNoKnownAllergies, hasConfirmedNoKnownFamilyHistory, hasConfirmedNoRelatedPersons, } = useFhirClinicalLists(); const { familyMemberHistoriesList, familyMemberHistoriesBundle } = useFhirFamilyMemberHistories(); const { appointmentsList, appointmentsBundle } = useFhirAppointments({ autoLoad: !orgless }); /** * HAVE THE READS ANSWERED YET? - and until they have, there is NO summary to offer. * * Every one of these lists is empty before its fetch resolves, and empty reads as *not done*. So a * summary built from them mid-flight tells a patient who has finished everything that they are at * 2 of 5, then corrects itself - asserting a count from data nobody has read, which is the same * defect as a screen reporting a zero it never measured. * * `undefined` is the answer for "not yet known", and both readers of this hook are built for it. * Not keyed on `loading`: that starts false and only rises after the first paint, so it says * "idle" during the exact window this guard exists for. A delivered bundle is the fact. */ const readsAnswered = Boolean( (orgless || relatedPersonsBundle) && medicationStatementsBundle && allergyIntolerancesBundle && clinicalListsBundle && (orgless || coveragesBundle) && familyMemberHistoriesBundle && (orgless || appointmentsBundle), ); return useMemo(() => { if (!readsAnswered) return undefined; const patient = patientState ?? (currentUser?.fhir?.resourceType === "Patient" ? currentUser.fhir : undefined); // Reads the tasks the system ALREADY computes and summarises them with the module's own // function. Deriving a second count here is the defect this app has shipped four times. const tasks = buildPatientSystemTasks({ patient, relatedPersons: relatedPersonsList ?? [], medicationStatements: medicationStatementsList ?? [], allergyIntolerances: allergyIntolerancesList ?? [], coverages: coverageList ?? [], hasConfirmedNoCurrentMedications, hasConfirmedNoKnownAllergies, familyMemberHistories: familyMemberHistoriesList ?? [], hasConfirmedNoKnownFamilyHistory, hasConfirmedNoRelatedPersons, appointments: appointmentsList ?? [], }); return summarizePatientSystemTasks( orgless ? tasks.filter((task) => !PRACTICE_SCOPED_SETUP_STEPS.has(task.id)) : tasks, ); }, [ orgless, readsAnswered, patientState, currentUser, relatedPersonsList, medicationStatementsList, allergyIntolerancesList, coverageList, hasConfirmedNoCurrentMedications, hasConfirmedNoKnownAllergies, familyMemberHistoriesList, hasConfirmedNoKnownFamilyHistory, hasConfirmedNoRelatedPersons, appointmentsList, ]); };