import React from "react"; import { FileText, History } from "lucide-react"; import type { SidebarMenuItem } from "./patientNav.tsx"; import { PATIENT_RECORD_SECTIONS } from "./PatientsPage/PatientPage/patientRecordSections.ts"; /** * ONE CUT THROUGH THE PATIENT'S RECORD, shared by the navigation drawer, the record page's own * "Browse your record" list, and the breadcrumb. * * The drawer used to carry its own grouping - Clinical Data, Diagnostics, Care Provision, Device * Data, Management - written out as JSX beside the destinations themselves. The record page grouped * the SAME destinations by `PATIENT_RECORD_SECTIONS`, in the patient's words. So a patient met one * vocabulary in the drawer and a different one on the page, and the drawer is what they meet first. * * The fix is structural rather than a rename: the drawer's groups are BUILT from the sections, so * there is no second grouping left to disagree with. This file holds the destinations, the sections * file holds the filing, and neither can drift from the other because neither has a copy. */ type PatientRecordDestination = { id: string; /** What the patient sees. Their words, never a resource type. */ label: string; to: string; /** Defaults to a document icon. Only the two destinations that are not documents override it. */ icon?: React.ReactNode; /** The breadcrumb parent, where the destination sits outside the record. Defaults to the record. */ crumbParent?: string; }; export const FAMILY_HISTORY_PATH = "/dashboard/patient/clinical/family-history"; const documentIcon = ; /** * Every destination inside the record, flat. The order here does not decide display order - a * section's `owns` array does - so this list is grouped by route for reading, not for rendering. * * Every id must be claimed by exactly one section, either in its `owns` (offered as a row) or in * its `alsoClaims` (still reachable, but the stream is where the patient meets it). */ export const PATIENT_RECORD_DESTINATIONS: PatientRecordDestination[] = [ // Under My Profile in the approved deck: "Dashboard › My Profile › Family health history". { id: "family-history", label: "Family health history", to: FAMILY_HISTORY_PATH, crumbParent: "/dashboard/profile" }, { id: "allergy-intolerances", label: "Allergies", to: "/dashboard/patient/clinical/allergy-intolerances" }, { id: "conditions", label: "Conditions", to: "/dashboard/patient/clinical/conditions" }, { id: "procedures", label: "Procedures & surgery", to: "/dashboard/patient/clinical/procedures" }, { id: "adverse-events", label: "Bad reactions", to: "/dashboard/patient/clinical/adverse-events" }, { id: "clinical-impressions", label: "Notes from your visits", to: "/dashboard/patient/clinical/clinical-impressions" }, { id: "detected-issues", label: "Things to check", to: "/dashboard/patient/clinical/detected-issues" }, { id: "clinical-results", label: "Test results", to: "/dashboard/patient/diagnostics/diagnostics-reports" }, { id: "personal-readings", label: "Readings & measurements", to: "/dashboard/patient/diagnostics/observations" }, { id: "imaging-studies", label: "Scans & X-rays", to: "/dashboard/patient/diagnostics/imaging-studies" }, { id: "media", label: "Photos & recordings", to: "/dashboard/patient/diagnostics/media" }, { id: "body-structures", label: "Body sites", to: "/dashboard/patient/diagnostics/body-structures" }, { id: "medication-requests", label: "Prescriptions", to: "/dashboard/patient/medications/medication-requests" }, { id: "medication-statements", label: "Medicines you told us about", to: "/dashboard/patient/medications/medication-statements" }, { id: "medication-administrations", label: "Medicines given to you", to: "/dashboard/patient/medications/medication-administrations" }, { id: "medication-dispenses", label: "Medicines dispensed", to: "/dashboard/patient/medications/medication-dispenses" }, { id: "immunizations", label: "Vaccinations", to: "/dashboard/patient/medications/immunizations" }, { id: "immunization-recommendations", label: "Vaccinations due", to: "/dashboard/patient/medications/immunization-recommendations" }, { id: "immunization-evaluations", label: "Vaccination checks", to: "/dashboard/patient/medications/immunization-evaluations" }, { id: "care-plans", label: "Care plans", to: "/dashboard/patient/care-provision/care-plans" }, // Its own page in the patient's navigation (Spencer, 2026-09-02: care plans and goals "separated into // their own pages"), and the deck trails it "Dashboard > Goals". { id: "goals", label: "Goals", to: "/dashboard/patient/care-provision/goals", crumbParent: "/dashboard" }, // "CLINICAL care team", not "care team". Collaboration Teams is a different product surface, and // the two were one word apart in both the label AND the URL until 2026-09-02. Spencer ruled on // 2026-08-13 that "care-teams should be collaboration-teams unless you're looking at a true FHIR // care team" - so the collaboration surface now lives at /dashboard/collaboration-teams and THIS // one, which is the true FHIR CareTeam, keeps the name it earns. // The label feeds the drawer, the record's own browse list and the breadcrumb from this single // table, so the three cannot disagree about which of the two a patient is looking at. { id: "care-teams", label: "Your clinical care team", to: "/dashboard/patient/care-provision/care-teams" }, { id: "outside-providers", label: "Other providers you see", to: "/dashboard/patient/care-provision/outside-providers" }, { id: "nutrition-orders", label: "Diet & nutrition", to: "/dashboard/patient/care-provision/nutrition-orders" }, { id: "risk-assessments", label: "Risk assessments", to: "/dashboard/patient/care-provision/risk-assessments" }, { id: "vision-prescriptions", label: "Glasses & contacts", to: "/dashboard/patient/care-provision/vision-prescriptions" }, { id: "devices", label: "Devices", to: "/dashboard/patient/device-data/devices" }, { id: "device-use-statements", label: "Devices you use", to: "/dashboard/patient/device-data/device-use-statements" }, { id: "device-requests", label: "Devices ordered for you", to: "/dashboard/patient/device-data/device-requests" }, // Reachable, but no longer offered as a row: the record's own index IS the stream of visits, and a // flat list of those same visits with nothing to open is what the stream replaced. Both stay in this // table so each page still has a name for its breadcrumb rather than a humanised URL slug. { id: "encounters", label: "Visits", to: "/dashboard/patient/management/encounters" }, { id: "episodes-of-care", label: "Courses of care", to: "/dashboard/patient/management/episodes-of-care" }, { id: "documents", label: "Documents", to: "/dashboard/patient/documents" }, { id: "questionnaire-responses", label: "Forms you filled in", to: "/dashboard/patient/questionnaire-responses" }, // INSURANCE IS NOT A RECORD DESTINATION, 2026-08-30. "Insurance stops pretending to be part of the // record" - and Spencer's 08-29 ruling gave it a door of its own on the patient home, which is how a // patient reaches it now. Leaving it here gave the same screen two routes in with two different names: // `getBreadcrumbConfig` named /dashboard/patient/insurance as a literal parented to the DASHBOARD and // the record generator named it again parented to the RECORD, and the literal silently won. Same // two-vocabularies defect that took "Things to do" out of this list. // Its route is /dashboard/MY-HISTORY, not /dashboard/patient/... like its siblings. // `renderPatientRoutes` is shared with the STAFF patient view and this feed is the patient's own // awareness tool, so it is deliberately mounted outside it. Tidying the path to match would 404. { id: "my-history", label: "My record activity", to: "/dashboard/my-history", icon: , // The deck crumbs it "Dashboard › My Profile › My record activity". crumbParent: "/dashboard/profile", }, ]; const byId = new Map(PATIENT_RECORD_DESTINATIONS.map((destination) => [destination.id, destination])); /** The destinations a section OFFERS, in the section's own order. */ const offeredBy = (owns: string[]): PatientRecordDestination[] => owns.map((id) => byId.get(id)).filter((d): d is PatientRecordDestination => Boolean(d)); /** * The record's children in the navigation drawer. * * A section holding several destinations becomes an expandable group. A section holding ONE becomes * a plain row carrying the SECTION's label, because a disclosure that reveals a single row is a * click that tells the patient nothing. */ export const patientRecordNavItems = (navigate: (to: string) => void): SidebarMenuItem[] => PATIENT_RECORD_SECTIONS.flatMap((section): SidebarMenuItem[] => { const destinations = offeredBy(section.owns); if (destinations.length === 0) return []; if (destinations.length === 1) { const only = destinations[0]; return [ { id: only.id, label: section.label, icon: only.icon ?? documentIcon, to: only.to, activeMatch: "prefix", activePaths: [only.to], onClick: () => navigate(only.to), }, ]; } return [ { id: section.id, label: section.label, icon: documentIcon, // No `to` and no `activePaths`: a section's destinations sit under several different route // prefixes now, so no single path means "somewhere in this section". The group lights up // through `hasActiveDescendant` instead, which asks its children rather than guessing. children: destinations.map((destination) => ({ id: destination.id, label: destination.label, icon: destination.icon ?? documentIcon, to: destination.to, activeMatch: "prefix", activePaths: [destination.to], onClick: () => navigate(destination.to), })), }, ]; }); /** Section ids that render as an expandable group, so only one of them is open at a time. */ export const PATIENT_RECORD_GROUP_IDS = PATIENT_RECORD_SECTIONS.filter( (section) => offeredBy(section.owns).length > 1, ).map((section) => section.id); /** * Which section group to open for a route, or undefined where none applies. Derived from the * destinations rather than from a path prefix: one prefix no longer means one section, since the * destinations under `/dashboard/patient/clinical` alone now belong to four different ones. */ export const patientRecordGroupForPath = (pathname: string): string | undefined => { let best: { section: string; length: number } | undefined; for (const section of PATIENT_RECORD_SECTIONS) { const destinations = offeredBy(section.owns); if (destinations.length < 2) continue; for (const destination of destinations) { if (pathname !== destination.to && !pathname.startsWith(destination.to + "/")) continue; if (!best || destination.to.length > best.length) { best = { section: section.id, length: destination.to.length }; } } } return best?.section; }; /** * Breadcrumb entries for every record destination, built from the same table the drawer renders. * * EVERY ONE HANGS DIRECTLY OFF THE RECORD, unless it names a `crumbParent`. The old config carried a crumb for the intermediate route * segment as well - "My Visits & Results > Conditions & notes > Procedures & surgery" - and that * middle crumb is now unnameable, because the destinations under one segment belong to several * different sections. It was also a link to a redirect: /dashboard/patient/clinical sends you to * Family health history, so the crumb for the page you were standing on went somewhere else. */ export const patientRecordBreadcrumbs = (): Record => ({ ...Object.fromEntries( PATIENT_RECORD_DESTINATIONS.map((destination) => [ destination.to, { label: destination.label, parent: destination.crumbParent ?? "/dashboard/patient" }, ]), ), // THE TWO CONTAINERS THE STREAM OPENS. They are not destinations - nothing navigates to them from a // list, a row does - so they are not in the table above. They still need naming here: without an // entry the crumb falls to the humaniser, which reads "Dashboard > Visit" and drops the record from // the trail entirely. The deck keeps the record's own trail with one open - "Dashboard > My visits & // results" beside the list on a laptop and on the phone page alike - so each IS the record's crumb. "/dashboard/patient/visit/:visitId": { label: "My Visits & Results", parent: "/dashboard" }, "/dashboard/patient/result/:resultId": { label: "My Visits & Results", parent: "/dashboard" }, [`${FAMILY_HISTORY_PATH}/:familyHistoryId`]: { label: "Relative", parent: FAMILY_HISTORY_PATH }, });