import type { PatientSystemTaskSummary } from "../../../utils/patientSystemTasks.ts"; /** * The patient home's ONE banner slot., design spec §4. * * ONE SLOT, ZERO OR ONE OCCUPANT, AND URGENCY DECIDES - the ranked queue's own principle, one level * up. This module exists so that rule is **enforced rather than assumed**: there is one function that * answers *what is in the slot*, and it can only return one thing. Two components each deciding * whether to render is how a screen ends up with an emergency and a progress meter side by side. * * THE SETUP BANNER IS NEVER THE ONLY HOME for its tasks - they also sit in the queue and under its * peer count, so ignoring or collapsing the banner never loses them. It is a promotion, not a hiding * place, which is what makes it safe to remove entirely at 100%. */ /** * A notice raised by one of the THREE approved triggers (spec §9, Spencer 2026-08-13): * a cancelled or moved appointment - a practice asking to be called - a failed payment that blocks * care. A CLOSED set - a fourth is the same conversation again, not a config change, because the * moment anything short of an emergency lands here the notice stops meaning emergency. */ export type UrgentNotice = { id: string; /** Which of the three raised it. Present so the closed set is checkable in code, not just in prose. */ trigger: "appointment-changed" | "practice-asks-for-a-call" | "payment-blocks-care"; title: string; detail?: string; /** * The practice's own instruction - *Call (319) 555-0142*, *Reschedule*, *Pay now*. * Taken FROM the notice rather than hardcoded per trigger: what the practice is asking for is a * property of the notice, and a per-type default would eventually say the wrong thing confidently. */ // `opens` is a third form beside `href` and `to`, for a destination that is no longer a route: the // booking PAGE was retired and the dialog that replaced it is hosted at the portal frame. A notice // is derived data with no access to a hook, so it names the surface and the banner opens it. // "task" opens the task overlay in place, as the queue does; `to` stays as the routed fallback. primaryAction: { label: string; href?: string; to?: string; opens?: "book-appointment" | "task"; taskId?: string }; /** * When the notice is time-boxed ("call before 5:00 PM today"). A tonal *Message the front desk* * beside a deadline is a promise we cannot keep, so a deadlined notice offers the instruction ALONE * until the message path can set an expectation. See the open decision on. */ deadline?: string; }; export type BannerOccupant = | { kind: "urgent"; notice: UrgentNotice } | { kind: "records-offer" } | { kind: "setup"; summary: PatientSystemTaskSummary } | { kind: "none" }; /** * May the slot offer to bring the patient's records in? Only on the personal host, once the mounted * first-sign-in gate has cleared and the profile step is known complete. Medications, allergies and history * are not required first: importing them is the point. Unread setup or an unmounted gate is never completion. */ export const recordImportOfferEligible = ({ personalHost, gate, setup }: { personalHost: boolean; gate: "unknown" | "outstanding" | "clear" | undefined; setup: PatientSystemTaskSummary | undefined; }): boolean => personalHost && gate === "clear" && Boolean(setup?.tasks.some((task) => task.id === "system-profile" && task.completed)); /** * What is in the slot. * * It must END COMPLETELY at 7 of 7 - gone, not collapsed, and never a tick saying "You're all set". * A congratulation that persists is how a temporary component becomes permanent furniture, and * disappearing is the entire reason it is allowed to sit above the account summary in the first place. */ export const chooseBannerOccupant = ( urgent: UrgentNotice | undefined, setup: PatientSystemTaskSummary | undefined, recordsOffer = false, ): BannerOccupant => { // Urgency decides. The setup banner stands down into the queue as ordinary rows while a notice is // here - still visible, not shouting beside an emergency. if (urgent) return { kind: "urgent", notice: urgent }; // The optional records offer comes before the remaining setup steps, which stay in the task list. if (recordsOffer) return { kind: "records-offer" }; if (!setup || setup.totalCount === 0 || setup.completed) return { kind: "none" }; return { kind: "setup", summary: setup }; }; /** * A DEADLINED NOTICE SHOWS THE INSTRUCTION ALONE. The message option exists because a call is not * available to everyone at that moment and a call leaves no record - but beside "before 5:00 PM * today" a tonal button that looks equivalent promises a response we have not committed to. */ export const offersMessageAction = (notice: UrgentNotice): boolean => !notice.deadline;