import type { Appointment } from "fhir/r4"; import { CalendarDays, Clock4, MapPin, Stethoscope, Video } from "lucide-react"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "../ui/dialog.tsx"; import { useFhirLocations } from "../../hooks"; import { isVirtualLocation } from "../../utils/roomLocation.ts"; import { appointmentLocationId, formatLocationAddress, } from "../../utils/appointmentDirections.ts"; import { formatDate } from "../../utils/dateUtils.ts"; import { useFhirTasks } from "../../hooks"; import { tasksByAppointmentId } from "../../utils/appointmentTasks.ts"; import AppointmentTaskList from "./AppointmentTaskList.tsx"; import PatientAppointmentActions from "./PatientAppointmentActions.tsx"; import { VideoVisitJoinNotice, timeOf } from "./VideoVisitJoinNotice.tsx"; /** * A patient's appointment, opened WHERE THEY ARE. R6, and the fix for worst instance. * * Spencer, 2026-08-14: *"The goal for this overview page was to try and prevent the need of being * directed off to another section for quick access stuff... they'd only have to navigate off this page if * they actually need the full page for that item."* * * WHAT THIS REPLACES, and why it was the worst of the four regressions: clicking an appointment on the * home ran `navigate("/dashboard/my-appointments")` - it dumped the patient on the LIST, where they had * to find the same appointment and click it again. That is not a dead control, so nothing looks broken; * it is a downgrade that reads as a feature. **Losing a modal looks like navigation, and navigation looks * intentional**, which is exactly how it survived review. * * DELIBERATELY NOT the receptionist's appointment dialog. That one carries arrival timeliness, status * controls, staff tabs and the visit-invitation panel - a patient shown those is being shown the * practice's operational surface for their own visit. */ export interface PatientAppointmentDialogProps { open: boolean; onOpenChange: (open: boolean) => void; appointment: Appointment | undefined; /** Resolved by the caller, which already has the practitioner records loaded. */ practitionerName?: string; /** The practice's own name, used only as the fallback `getAppointmentLocationName` already applies. */ organizationName?: string; } /** * The body, mounted ONLY while the dialog is open. * * SPLIT OUT AFTER THE FULL SUITE WENT RED, and the failure was pointing at a real defect rather than * at the tests: `useFhirLocations` ran on every render of the closed dialog, so merely HAVING an * appointment dialog on a page made that page require the auth and location providers. A closed dialog * must cost nothing - the patient home renders one for the queue and another for the appointment card, * and neither should be loading a practice's locations to show nothing. * * Hooks cannot be conditional, so the condition has to be the COMPONENT BOUNDARY. */ const AppointmentDetail = ({ appointment, practitionerName, organizationName, onOpenChange, }: Required> & { appointment: Appointment; practitionerName?: string; organizationName?: string; }) => { const { locationsList } = useFhirLocations(); //. `autoLoad: false` - every surface that opens this dialog already loads the patient's // tasks, so this reads their state rather than firing a second request that could resolve to a // different answer. And it is inside `AppointmentDetail`, so a CLOSED dialog still costs nothing. const { tasksList } = useFhirTasks({ autoLoad: false }); const locationId = appointmentLocationId(appointment); const location = locationId ? locationsList?.find((candidate) => candidate.id === locationId) : undefined; // ONE FACT DECIDES BOTH HALVES: the place's own address. A telehealth Location has none ( - // telehealth IS a Location), so it yields no Directions without anyone asking "is this telehealth?", // and the same rule also covers a site nobody has finished configuring. // Directions itself moved into `PatientAppointmentActions` with the other two controls; // the ADDRESS stays here because it is content in this dialog's `
`, not an action. const address = formatLocationAddress(location); const isVideoVisit = location ? isVirtualLocation(location) : false; const placeName = location?.name?.trim() || organizationName?.trim(); const start = appointment?.start; // - THIS DIALOG RESTORED A CLICK AND NOT THE CONTENT. It was added to fix (an // appointment click used to dump the patient on the list); before that they at least landed somewhere // that COULD show these, and afterwards the dialog was a dead end for them. Same resolver and same // map the home's appointment card uses, so the two cannot disagree about what is due. const appointmentTasks = appointment.id ? tasksByAppointmentId(tasksList ?? [], [appointment]).get(appointment.id) ?? [] : []; return ( <> Your appointment The details of this appointment, and what you can do about it.
{practitionerName && (
)}
{isVideoVisit ? (
{/* ๐Ÿ”ป R6 AMENDED (Spencer, 2026-08-14) โ€” THERE IS NO JOIN BUTTON HERE OR ON THE CARD. There is ONE join affordance, or two that can disagree: `AvailableMeetingBanner` is already fixed top-centre over every page, and a second Join on the home would be the WEAKER of the two (only on this page, below the fold on a phone) while looking equally authoritative. โ‡’ So this sets the expectation and says WHERE the button will appear, because "it will pop up" does not tell someone where to look. */} {/* โญ THE NAMED LIST, not the compact count โ€” this is where the patient ACTS on them, so each one is a control that opens it. The one-line "1 of 2 done" form belongs on a row that must read without being opened. โ›” The dialog closes on the way, or the patient lands on the task page with a modal still over it. */} onOpenChange(false)} /> {/* ๐Ÿ”‘ CARD 409 โ€” Cancel ยท Directions now live in ONE component that the patient's Appointments page mounts too. They used to exist only here, so the same appointment offered its actions from the home and none of them from the list. โ›” The fix was not to rebuild them over there. โš ๏ธ Reschedule was the third and it is GONE โ€” it opened the new-booking flow carrying nothing, so a patient who used it ended up holding two appointments. */} onOpenChange(false)} /> ); }; export const PatientAppointmentDialog = ({ open, onOpenChange, appointment, practitionerName, organizationName, }: PatientAppointmentDialogProps) => ( {/* โญ The body mounts only when there is something to show โ€” see AppointmentDetail's note: a closed dialog must not make its host page depend on the auth and location providers. */} {appointment && ( )} ); export default PatientAppointmentDialog;