import { useState } from "react"; import { useAuthServiceContext, usePatientServiceContext } from "../../../services"; import { useFhirTasks, useRouteAuthorization } from "../../../hooks"; import { NO_PACKETS, PacketSummaryReader, packetRefreshKey, type PatientPacketSummaries } from "../../../hooks/usePatientPacketSummaries.ts"; import AccountSummary from "./AccountSummary.tsx"; import HomeWelcome from "./HomeWelcome.tsx"; import PatientHomeBanner from "./PatientHomeBanner.tsx"; import PatientQueue from "./PatientQueue.tsx"; import { usePatientUrgentNotice } from "./usePatientUrgentNotice.ts"; import { usePatientSetupSummary } from "./usePatientSetupSummary.ts"; import { chooseBannerOccupant, offersMessageAction } from "./patientHomeBanner.ts"; import EverythingElse from "./EverythingElse.tsx"; import NextAppointment from "./NextAppointment.tsx"; import { usePatientAppointmentsStayCurrent } from "./usePatientAppointmentsStayCurrent.ts"; import { PatientUnpaidInvoicesProvider } from "./usePatientUnpaidInvoices.tsx"; import { PatientHomeReferralsProvider } from "./PatientHomeReferralsProvider.tsx"; import { isOrglessPatientsHost } from "../../../utils/hostedMount"; import { isAdolescentSelf } from "../../../utils/adolescentFeatureGate.ts"; import { readsForAnother } from "../../../utils/visitListProvesNoneBooked.ts"; import PracticesWaitingOnYou from "./PracticesWaitingOnYou.tsx"; const HomePage = () => { const { USER_ROLES, currentUser, userRole, actingAs } = useAuthServiceContext(); usePatientAppointmentsStayCurrent(); // Asked ONCE, HERE, because this is the only component that renders the banner and the queue as // siblings and can therefore see both. The queue's empty state offers "Message the front desk", and // `UrgentBanner` renders the identical button when `offersMessageAction` - so on an urgent notice the // two would sit one above the other, and the cell below would be claiming nothing needs the patient // while the banner above says something does. // Deliberately NOT read inside `PatientQueue`: that hook reaches `usePatientServiceContext`, which // gave the queue a service dependency for a button and turned all five of tests red on a // context error. A prop keeps the queue renderable with nothing but a router. const urgentNotice = usePatientUrgentNotice(); // Messaging is 18+: a signed-in 12-17 patient gets no message action anywhere on the home. const canMessage = !isAdolescentSelf(currentUser); // The patient's packets, read here for the queue below as the banner facts are, and only while a packet form is listed. const { tasksList } = useFhirTasks({ autoLoad: false }); const patientId = usePatientServiceContext().patientState?.id; const packetRefresh = packetRefreshKey(tasksList ?? []); const [packetRead, setPacketRead] = useState(NO_PACKETS); /** * AND THE SECOND THING ONLY THIS COMPONENT CAN SEE: is the banner already offering the patient their * setup steps? * * MEASURED 2026-09-13 on the patient portal: the banner listed FIVE outstanding setup items and the * cell directly beneath it read *"Nothing needs you yet"*. Spencer settled the shape in his own * patient-home run - the cell *"stands down when the banner is already offering it - for truthfulness, * not tidiness: otherwise this cell announces nothing needs the patient while the banner says * something does."* * * ASKED THROUGH `chooseBannerOccupant`, NOT off the summary, and that is the load-bearing part. The * slot holds ONE occupant and urgency wins it; a queue that read "is there a setup summary" would * stand down for a banner that is showing an urgent notice instead, trading a false *"nothing needs * you"* for a silent one. The question is whether the banner is offering THESE items, so the same * chooser that decides what renders decides what the cell may claim. */ const setupSummary = usePatientSetupSummary(); const bannerOccupant = chooseBannerOccupant(urgentNotice, setupSummary); useRouteAuthorization({ allowedRoles: [USER_ROLES.PATIENT, USER_ROLES.RELATED_PERSON], }); // With no practice there are no visits, messages or bills to rank; setup and the doors remain, and a // practice still waiting on the patient's agreements shows what it is waiting on. if (isOrglessPatientsHost()) { // The banner and the billing door read bills here too, so they share the one owned load. return (
); } return ( // - ONE INVOICE LOAD FOR THE WHOLE PAGE. The banner, the summary band and the queue all // ask the same question about money, and each used to fetch it for itself: four `Invoice` requests // per page load, measured in a browser. It is a correctness fix, not a performance one - three // independent loads can DISAGREE, and a payment landing between them shows a band and a banner // quoting different amounts owed. The hook's own header already asked for one load; only a shared // provider can actually deliver it. // It must wrap all three consumers. Unprovided the hook returns `undefined` rather than throwing, // which is safe but silent - so the guard asserts this wrapper is here. {/* Card 280 โ€” the CONTAINER the wide layouts measure. Not the viewport: this app is a mount inside someone else's page, so the only width that is true is this element's. */}
{/* ๐Ÿ”ป THE IN-PAGE HEADER CARD IS GONE โ€” card 305 R1, from Spencer's review of the built page. It repeated the practice's name that the frame already shows, so the name was on screen TWICE and the card read as a stray rather than as the header. Its contents did not move into the page; they moved UP, into the frame band, which is where a header belongs. โญ Nothing was dropped: the phone is still one tap and both utilities are still always visible (D6) โ€” see components/PortalFrameHeader/patientFrameSlots.tsx. */} {/* Card 279 / spec ยง4 โ€” ONE banner slot, zero or one occupant, urgency decides. Above the summary because it is the only thing allowed to interrupt the ranking, and it earns that by disappearing completely when it is done. */} {/* Under the banner, where the deck frames put the page title. */} {/* Card 278 / D3b โ€” the four counts that ARE the navigation. Sits between the header and the queue, which is where the spec puts it: it answers "is there anything I haven't seen?" before the queue answers "what needs me first". */} {/* Card 280 / spec ยง7b โ€” two columns above ~1060px of CONTAINER width, one below. ๐Ÿšจ The old `lg:col-span-3` grid was a VIEWPORT breakpoint, which is wrong for a mount inside someone else's page: the window can say 1440 while the app has 900. The widths now come from container queries in index.css, and that file carries the arithmetic. */}
{/* Card 273 / D1 โ€” ONE list ordered by urgency. It REPLACES the per-type Messages and Tasks cards that used to sit here, and that replacement is the point rather than a side effect: a card per kind is a section per kind, and a section can be pushed to the bottom, which is how messages got buried. ๐Ÿ“Œ Nothing is lost โ€” both surfaces still exist at their own routes, reachable from the door list and from the summary band's counts. What the home no longer carries is the Tasks card's COMPLETED tab, which has no place in a queue of things that need you. */} {packetRefresh && }
{/* โ›” REFERENCE, NEVER QUEUE. Things you act on one side, things you look at the other โ€” the same rule the encounter workspace landed on. "Everything else" belongs here for the same reason: it is the door to everything, not a thing to do. */}
); }; export default HomePage; // import { useAuthServiceContext, useConfigServiceContext } from '../../../services'; // import AllergyIntolerances from '../ClinicalDataPage/AllergyIntolerances/AllergyIntolerances.tsx'; // import FamilyMemberHistories from '../ClinicalDataPage/FamilyMembersHistory/FamilyMemberHistories.tsx'; // import PatientTasks from '../PatientsPage/PatientPage/PatientTasks/PatientTasks.tsx'; // import { useRouteAuthorization } from '../../../hooks'; // import MedicationStatements from '../PatientMedicationsPage/MedicationStatements/MedicationStatements.tsx'; // const HomePage = () => { // const { USER_ROLES } = useAuthServiceContext(); // const { getGridHeight } = useConfigServiceContext(); // useRouteAuthorization({ // allowedRoles: [USER_ROLES.PATIENT, USER_ROLES.RELATED_PERSON] // }); // return ( // // // // // // // ); // }; // export default HomePage;