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 }) => (
  • {dayOf(event.when)} {event.lead ? `${event.lead} · ` : ''}{event.title} {event.detail && {event.detail}} {showPractice && event.organizationName && ( {event.organizationName} )}
  • ); type Problem = { id: string; name: string }; /** The problems a reference lists: current ones only, never one archived as entered in error. */ const currentProblems = (conditions: Condition[] | undefined): Problem[] => (conditions ?? []) .filter((condition) => !isArchived(condition) && isCurrent(condition)) .map((condition) => ({ id: condition.id, name: nameOf(condition) })) .filter((problem): problem is Problem => Boolean(problem.id && problem.name)); interface ReferenceBodyProps { captionLine: ReactNode; problems: Problem[]; events: RecordEvent[]; showPractice: boolean; streamError?: string; /** The problem list was served as of a cutoff and not in full: what it left out is unknown. */ conditionsIncomplete?: boolean; /** The open visit's standing and whether its problem list was read again after a reauthorization. */ recordStanding?: RecordAccessStanding; freshness?: RecordFreshness; encounterId?: string; patientId?: string; } // The sentence under an empty problem list, from the same standing as its heading: never a claim about the patient's // current state unless the read was current and fresh. const emptyExplanation = (standing: RecordAccessStanding, freshness: RecordFreshness): string => { const tail = 'This does not mean the patient has no conditions.'; switch (standing.kind) { case 'current': if (freshness === 'updating') return `Conditions are being read again after reauthorization. ${tail}`; if (freshness === 'not-updated') return `Conditions could not be read again after reauthorization. ${tail}`; return `No current conditions are recorded. ${tail}`; case 'withdrawn': return standing.at ? `No conditions are recorded in the record available through ${formatCutoff(standing.at)}. ${tail}` : `No conditions are recorded in the record available to this practice. ${tail}`; case 'declined': case 'not-given': return `No conditions are recorded in the record available to this practice. ${tail}`; case 'loading': return `Checking record access. ${tail}`; case 'unknown': return `Record access could not be checked. ${tail}`; } }; /** * THE REFERENCE ITSELF - the problem list, its drill-down, the empty state and the way into the record. * Shared by the open visit (ambient data, gated below) and the finished visit (its own snapshot), so the * two cannot draw the same card two ways. */ const ReferenceBody = ({ captionLine, problems, events, showPractice, streamError, conditionsIncomplete = false, recordStanding: given, freshness = 'fresh', encounterId, patientId }: ReferenceBodyProps) => { // The encounter page is a practice's view of this patient: its empty line describes the record available. The open // visit hands its standing in; a finished visit's snapshot reads its own, for wording only (it stays historical). const own = useRecordAccessStanding({ patientId, enabled: Boolean(patientId) && !given }); const recordStanding = given ?? own.standing; const [aboutId, setAboutId] = useState(null); const about = useMemo(() => problems.find((problem) => problem.id === aboutId) ?? null, [problems, aboutId]); // The same filter the patient's own record applies, over the same events - so "everything about this // one" cannot mean two different sets on the two surfaces. const shown = useMemo( () => (about ? applyRecordFilters(events, { about: about.id }) : []), [events, about], ); if (about) { return (
    {captionLine}
    Everything about this one
    {streamError && (

    {streamError}

    )} {shown.length > 0 && ( <>
      {shown.slice(0, 6).map((event) => ( ))}
    )}
    ); } // What the record stream could not read is said on every branch, before anything is selected. const streamNote = 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 (
    {captionLine} {streamNote} {conditionsIncomplete ? incompleteNote : ( <>

    {availableRecordEmpty("No problems recorded yet", recordStanding, freshness)}

    {emptyExplanation(recordStanding, freshness)}

    )}
    ); } return (
    {captionLine} {streamNote}

    Current conditions

    {incompleteNote}
    ); }; /** `caption` names whose record this is, inside the panel's own card, where a meeting shows it. */ export const EncounterChartPanel = ({ caption }: { caption?: ReactNode } = {}) => { const captionLine = caption ?

    {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 (
    {captionLine}

    Loading patient reference…

    {whose ? `Checking ${whose}’s record.` : 'Checking this patient’s record.'}

    ); } if (conditionsError && failureIsThisPatients && conditionsRefused) { return (
    {captionLine}

    Your current access does not allow this record to be displayed. This is not an empty chart.

    ); } if (conditionsError && failureIsThisPatients) { return (
    {captionLine}

    We couldn’t load this patient’s reference. No clinical conclusions can be drawn from this state.

    ); } return ( ); }; // A prescription, read for the record's events only - never a form mounted here. type Prescription = MedicationRequest; const snapshotResources = (results: SnapshotTypeResult[], resourceType: string): T[] => (results.find((result) => result.resourceType === resourceType)?.items ?? []).map((item) => item.resource as unknown as T); /** * A FINISHED VISIT'S REFERENCE (deck "Finished": "the note goes with the visit; the chart and the two * ways back stay"; Codex P06 ruling item 2, Captain 2026-09-30 option B). * * PATIENT-EXPLICIT, never ambient. A finished visit selected on the board has no open encounter, and * whatever chart was loaded last may be somebody else's - the failure the gate above exists for. So * this reads only the snapshot the focus panel already loaded for THIS patient, and builds the same * events the record stream builds. */ export const FinishedVisitReference = ({ results, running, encounterId, patientId }: { results: SnapshotTypeResult[]; running: boolean; encounterId?: string; patientId?: string; }) => { const conditionsRead = results.find((result) => result.resourceType === 'Condition'); const problems = useMemo( () => currentProblems(snapshotResources(results, 'Condition')), [results], ); const events = useMemo(() => buildRecordEvents({ encounters: snapshotResources(results, 'Encounter'), reports: snapshotResources(results, 'DiagnosticReport'), observations: snapshotResources(results, 'Observation'), episodes: snapshotResources(results, 'EpisodeOfCare'), prescriptions: snapshotResources(results, 'MedicationRequest'), }), [results]); const showPractice = useMemo(() => practiceIsWorthShowing(events), [events]); // Unanswered is still loading, never empty: "No problems recorded yet" is a claim about the record. const answered = conditionsRead?.status === 'ready' || conditionsRead?.status === 'error'; if (!answered) { return (

    Loading patient reference…

    Checking this patient’s record.

    ); } if (conditionsRead?.status === 'error') { return (

    We couldn’t load this patient’s reference. No clinical conclusions can be drawn from this state.

    ); } return ( ); }; export default EncounterChartPanel;