import { useState } from "react"; import { useNavigate } from "react-router-dom"; import { Check, ChevronsDown, Circle } from "lucide-react"; import { useBookAppointmentDialog } from "../../../components/BookAppointmentDialog/BookAppointmentDialogProvider.tsx"; import { type PatientSystemTaskDefinition } from "../../../utils/patientSystemTasks.ts"; import { chooseBannerOccupant, offersMessageAction, recordImportOfferEligible, type UrgentNotice, } from "./patientHomeBanner.ts"; import { usePatientUrgentNotice } from "./usePatientUrgentNotice.ts"; import { usePatientSetupSummary } from "./usePatientSetupSummary.ts"; import PatientTaskDialog from "../../../components/tasks/PatientTaskDialog.tsx"; import { useMountedFirstSignInGateStatus } from "../../../components/FirstSignInGate/firstSignInGateStatus.tsx"; import { isOrglessPatientsHost } from "../../../utils/hostedMount"; import { RecordImportOfferSlot } from "./RecordImportOffer.tsx"; import "./PatientHomeBanner.css"; /** * The patient home's ONE banner slot., design spec §4. * * The two states are told apart by STRUCTURE, not by hue: heavier rail, coloured title, no progress * meter, actions on the face of it. Do not make the urgent state louder - three solid fills were * tried in the design session and all three were rejected as hard on the eyes. *Nothing in this * product should be unpleasant to look at, including the alarm*, and a patient reading that their * procedure is cancelled is at the worst possible moment to also be assaulted. * The two are never on screen together, so the requirement is recognition over time rather than * discrimination at a glance - much weaker than it first appears, and what makes a comfortable urgent * state possible at all. * * DEPENDENCY, declared rather than worked around: the setup state's ground and rail are specified * as `--rose-soft` / `--rose`, and **those tokens do not exist** - the palette landed blue/green/amber/ * red soft only. Defining a token value is lane, so the setup banner renders on the card * ground with its 4px rail in the ink family until they land. Every STRUCTURAL difference the design * leans on is built and pinned; only the tint is owed. * * R2, SETTLED (Spencer, 2026-08-14): *"The red card (banner) should be the urgent notice piece."* That * CORRECTS the mock, which showed "Finish setting up - 4 of 7 done" in the rose treatment. Red is reserved * for the thing that actually needs the patient now; a setup checklist is not that. The assignment below * already followed the rule - what did not exist was the TREATMENT itself: * - `border-l-6` is not a Tailwind class, so the urgent rail fell back to 1px; * - `border-l-destructive` and `border-l-secondary` were never generated, so BOTH banners drew their * rail in the neutral border colour and the accent that tells the states apart was never on screen. * Both are fixed - see the rail comment below and the `border-l-*` block in index.css. */ const SetupBanner = ({ tasks, completedCount, totalCount, }: { tasks: PatientSystemTaskDefinition[]; completedCount: number; totalCount: number; }) => { const navigate = useNavigate(); const next = tasks.find((task) => !task.completed); const [expanded, setExpanded] = useState(completedCount === 0); return (
{expanded && ( )}
); }; const UrgentBanner = ({ notice, canMessage }: { notice: UrgentNotice; canMessage: boolean }) => { const navigate = useNavigate(); const { openBookAppointment } = useBookAppointmentDialog(); const [openTaskId, setOpenTaskId] = useState(); const primary = notice.primaryAction; return ( <>

{notice.title}

{notice.detail &&

{notice.detail}

} {notice.deadline &&

{notice.deadline}

}
{primary.href ? ( {primary.label} ) : ( )} {/* 🔑 The "or" is not cosmetic: two stacked buttons read as a SEQUENCE — do this, then that — and on a cancelled-procedure notice a patient reading it that way either does both or stalls. The message option exists because a call is not available to everyone at that moment, a call leaves NO RECORD in a product whose thesis is continuity, and the pooled front desk is already the correct recipient. */} {canMessage && offersMessageAction(notice) && ( <> or )}
!next && setOpenTaskId(undefined)} taskId={openTaskId} /> ); }; /** The slot. One occupant, chosen by `chooseBannerOccupant` - never by two components each deciding. */ // `canMessage` is false for a signed-in 12-17 patient: messaging is 18+, so no message action shows. export const PatientHomeBanner = ({ canMessage = true }: { canMessage?: boolean } = {}) => { // The three approved triggers, read from records the product already holds. const urgent = usePatientUrgentNotice(); /** * THE SETUP SUMMARY IS ASKED FOR, NOT COMPUTED HERE, and the move is what fixed a defect one cell * below this banner. * * It was private to this component, so the home page - the only thing that renders the banner and the * queue as siblings - could not ask whether the banner was already offering the patient something. * The cell beneath therefore read "Nothing needs you yet" over a banner listing five outstanding * setup items. The computation, its starved-inputs history and its not-yet-read guard all moved * intact into the hook. */ const summary = usePatientSetupSummary(); const gate = useMountedFirstSignInGateStatus(); const offer = recordImportOfferEligible({ personalHost: isOrglessPatientsHost(), gate, setup: summary }); const occupant = chooseBannerOccupant(urgent, summary, offer); if (occupant.kind === "urgent") return ; if (occupant.kind === "records-offer") { // Put off, or with no account to key that to, the slot falls back to what it would otherwise hold. const otherwise = chooseBannerOccupant(undefined, summary); return ( ) : null} /> ); } if (occupant.kind === "setup") { return ( ); } return null; }; export default PatientHomeBanner;