import { useMemo, useState, type ReactNode } from 'react'; import type { Condition, DiagnosticReport, Encounter, EpisodeOfCare, MedicationRequest, Observation } from 'fhir/r4'; import { TriangleAlert } from 'lucide-react'; import { useFhirConditions } from '../../../hooks/useFhirConditions'; import { buildRecordEvents, usePatientRecordStream } from '../../../hooks/usePatientRecordStream'; import type { SnapshotTypeResult } from '../../../hooks/usePatientSnapshot'; import { isArchived } from '../../../utils/archivedResource'; import { usePatientServiceContext } from '../../../services/PatientService'; import { useActiveEncounterContext } from './ActiveEncounterContext'; import { applyRecordFilters } from '../../../utils/patientRecordFacets'; import { practiceIsWorthShowing, type RecordEvent } from '../../../utils/patientRecordStream'; import ConditionHeader from '../PatientsPage/PatientPage/RecordStream/ConditionHeader'; import { formatFhirHumanName } from '../../../utils/fhirHumanName'; import { availableRecordEmpty, formatCutoff, useRecordAccessStanding, type RecordAccessStanding, type RecordFreshness } from '../../../hooks/useRecordAccessStanding'; import './EncounterChartPanel.css'; /** * WHAT THIS VISIT IS ABOUT, INSIDE THE VISIT -. * * What stood here was `Chart snapshot`, the six FHIR groups, and on a real patient the whole clinical * half rendered as one line: *"Nothing recorded currently in: Clinical - Diagnostics - Medications - * Device - Care - Records."* That sentence is honest and it is the whole problem. It is a list of * CONTAINERS, and a clinician in a room is not asking which resource types hold something - they are * asking what they already know about the thing they are seeing this person for. A snapshot that reads * empty for most patients teaches clinicians the record is empty, so they stop looking, and then it is * empty at the moment it matters. * * THIS IS PLACEMENT, NOT INVENTION, AND THAT CONSTRAINT IS THE POINT. The About lens already exists * and the patient already has it; `ConditionHeader` below is the SAME component their own record * renders, over the same `RecordEvent` stream. Deliberately NOT a clinician-side summary sitting * beside the patient's: two summaries of one record drift, and the one that goes stale is the one that * gets read out loud in a room. * * THE ROWS DO NOT NAVIGATE, and that is a finding rather than a shortcut. `RecordEventRow` - the * patient's row - opens `visit/:visitId` and `result/:resultId`, and both are mounted `selfView` ONLY * (App.tsx). A staff user who reaches the record index is redirected to `profile`. So there is no * staff-facing record for a row to open, and reusing the patient's row here would give a clinician a * control that silently goes nowhere. Reading is what this panel is for - the drawn version says so * itself, *"dense on purpose: it is read at a glance, not browsed"* - so the rows are text until a * staff record route exists to send them to. Recorded on; not invented here. * * Drawn in `mockups/practitioner/encounter.html`, the panel marked PROPOSED. */ /** Active-ish: FHIR's own `active | recurrence | relapse`. Resolved problems are not what a visit is about. */ const IS_CURRENT = new Set(['active', 'recurrence', 'relapse']); const isCurrent = (condition: Condition): boolean => { const code = condition.clinicalStatus?.coding?.[0]?.code; // No clinical status at all is COMMON in imported records and must not silently drop the problem - // an absent status is unknown, not resolved. return !code || IS_CURRENT.has(code); }; const nameOf = (condition: Condition): string | undefined => condition.code?.text?.trim() || condition.code?.coding?.find((coding) => coding.display?.trim())?.display?.trim(); /** `14 Aug`. Undated events keep their place in the stream and say so rather than being dropped. */ const dayOf = (when?: string): string => { if (!when) return '—'; const at = new Date(when); if (Number.isNaN(at.getTime())) return '—'; return at.toLocaleDateString(undefined, { day: 'numeric', month: 'short' }); }; /** * THE WAY OUT OF THE VISIT AND INTO THE RECORD, which the drawn panel carries and the built one did not. * * The encounter workspace offers a clinical note, a private note, save-to-record and add-diagnosis. That * is the whole set the approved deck draws too - so the set is right and nothing else belongs in the * visit. What the deck ALSO draws, three times, at the foot of its panels, is `Open the full record`, and * the built workspace had no route out at all. Every other clinical form - an allergy, a procedure, a * medication request, a follow-up task - is reached from the chart, so with no door the visit could reach * none of them. * * It keeps the visit in hand. `/dashboard/encounters/:encounterStateId/patients/:id` is the chart mounted * INSIDE an encounter, which is why the clinical forms opened from there already default to this visit. * Navigating to the plain `/dashboard/patients/:id` would reach the same forms with no visit to file * against, which is the one thing this must not do. */ const OpenFullRecord = ({ encounterId, patientId }: { encounterId?: string; patientId?: string }) => { // No id, no link - rather than a control that navigates somewhere wrong. The panel already refuses to // render at all while the ambient patient and the encounter's patient disagree. if (!encounterId || !patientId) return null; // A NAVIGATION LINK in the shared `ak-link` recipe, left-aligned and without an arrow (Codex P06 ruling, // 2026-09-30; Captain 13:41 on the arrow). The app is a HashRouter, so the hash href is the same route // the button used to navigate to. return ( Open the full record ); }; const EventLine = ({ event, showPractice }: { event: RecordEvent; showPractice: boolean }) => (
{streamError}
)}{streamError}
; // A problem list read as of a cutoff and not in full: never "no problems", and a listed one may be short. const incompleteNote = conditionsIncomplete && ({problems.length === 0 ? 'Conditions could not be read in full, so what this list holds is not known.' : 'Conditions could not be read in full, so this list may be incomplete.'}
); // A 403 is an answer about access, never an empty chart; any other failure can be tried again. if (problems.length === 0) { return ({emptyExplanation(recordStanding, freshness)}
> )}{caption}
: null; // The open visit's standing, read once: the problem list answers to it, and so do its words. const { active: standingFor } = useActiveEncounterContext(); const { standing: recordStanding } = useRecordAccessStanding({ patientId: standingFor?.item.patientId, enabled: Boolean(standingFor?.item.patientId) }); const { conditionsList, loading: conditionsLoading, error: conditionsError, answered: conditionsAnswered, refused: conditionsRefused, failureIsThisPatients, loadConditions, incomplete: conditionsIncomplete, freshness: conditionsFreshness, } = useFhirConditions({ standing: recordStanding }); const { events, showPractice, loading: streamLoading, error: streamError, partialError: streamPartialError } = usePatientRecordStream(); /** * WHOSE CONDITIONS ARE THESE? A RENDER-TIME IDENTITY GATE, and it is not optional here. * * `useFhirConditions` and `usePatientRecordStream` both read the AMBIENT patient context - whichever * chart was loaded last - and this panel takes no patient of its own. So opening a second patient's * visit rendered the FIRST patient's problems under the words "What this visit is about". * * OBSERVED, not theorised, 2026-08-28: started a visit for `Patient One`, who has ZERO conditions on * the datastore, and the panel showed `Diabetes mellitus` and `Acute pharyngitis` - both belonging to * the patient whose chart had been open before. It survived a full page reload. * * `useActiveEncounterPatient` already carries this exact gate, with the reason written on it: * *"pairing a stale patient with the new encounter is a wrong-name title AND a wrong-subject clinical * write... an effect cannot fix it - the first committed frame is already wrong."* That is why this * is a render-time comparison rather than an effect that clears state: an effect runs after the * clinician has already read the wrong list. * * MISMATCH RENDERS THE LOADING STATE, deliberately, because it IS still loading - the ambient * context is mid-swap to this encounter's patient. Showing nothing is honest; showing somebody * else's problems is the failure this panel is meant to prevent. */ const { active } = useActiveEncounterContext(); const { patientState } = usePatientServiceContext(); const encounterPatientId = active?.item.patientId; const loadedPatientId = patientState?.id; const identityMatches = Boolean(encounterPatientId) && loadedPatientId === encounterPatientId; const problems = useMemo(() => currentProblems(conditionsList), [conditionsList]); if (!identityMatches || conditionsLoading || streamLoading || !conditionsAnswered) { // Named only once the loaded patient is this visit's; mid-switch it is still "this patient". const whose = identityMatches ? formatFhirHumanName(patientState?.name, { emptyText: '' }) : ''; return ({whose ? `Checking ${whose}’s record.` : 'Checking this patient’s record.'}
Your current access does not allow this record to be displayed. This is not an empty chart.
We couldn’t load this patient’s reference. No clinical conclusions can be drawn from this state.
Checking this patient’s record.
We couldn’t load this patient’s reference. No clinical conclusions can be drawn from this state.