import type { ClinicalImpression, Condition, DiagnosticReport, DocumentReference, Encounter, Observation } from "fhir/r4"; import { bloodPressureReading, isBloodPressurePanel } from './observationPanelValue'; /** * WHAT ONE VISIT PRODUCED - the container half of the approved record. * * EVENTS ARE CONTAINERS. A visit happened, so it opens onto what came out of it: what the clinician * wrote, and the vitals taken that day. That is most of what a patient opens a visit to read. * * IT ONLY EVER CLAIMS WHAT IT CAN PROVE. The card's third control: *"nothing claims a medicine or a * result it cannot prove belongs to that visit."* Everything here is joined by an explicit * `encounter` reference on the resource itself. NEVER by same-day proximity - a lab drawn on the day * of a visit is not evidence it was ordered at it, and a container that guesses is worse than one that * omits, because a patient cannot tell a guess from a fact. * * Shape only: no clock, no fetch, no render. */ export interface VisitNote { id: string; /** What the clinician wrote, as they wrote it. */ text: string; /** Who wrote it, for the heading - *"What Dr. Reyes wrote"*. */ author?: string; /** When it was written, for the stamp under it. */ writtenAt?: string; } export interface VisitVital { id: string; /** "Blood pressure" - the patient's word for it, from the observation's own text. */ label: string; /** "128/78", "178 lb". Already assembled, because a component should not do unit arithmetic. */ value: string; /** Where the value was read from, for field history. */ valuePath: "valueQuantity" | "component"; /** "in range" / "above the usual range", when the observation carries an interpretation. */ interpretation?: string; /** True only for a reassuring interpretation - it is the one that renders green. */ reassuring?: boolean; } export interface VisitDiagnosis { id: string; /** "Acute pharyngitis" - the condition's own words. */ label: string; /** * "resolved", "remission" - present ONLY when the condition is no longer active. * * A visit is a historical container, so a diagnosis made at it belongs in it even after it has * resolved - dropping resolved ones would rewrite what happened that day. But listing a resolved * condition bare, under a heading that says it was diagnosed here, reads as something the patient * still has. The status is what stops the container from misleading about the present. */ status?: string; } export interface VisitDocument { id: string; /** "Referral letter from my previous clinic" - the document's own description, or its type. */ label: string; /** The day it was filed, for the stamp beside it. */ filedOn?: string; } export interface VisitResult { id: string; /** "CBC panel" - the report's own words. */ label: string; when?: string; } export interface VisitDetail { id: string; when?: string; /** The clinician, for the second line and for the note's heading. */ clinician?: string; practice?: string; /** "Annual physical". */ reason?: string; notes: VisitNote[]; vitals: VisitVital[]; diagnoses: VisitDiagnosis[]; documents: VisitDocument[]; results: VisitResult[]; } /** Does this resource point AT this encounter? The only join this module accepts. */ const belongsTo = (encounterId: string, reference?: { reference?: string }): boolean => reference?.reference?.split("/").pop() === encounterId; /** * The value, assembled once so no component does unit arithmetic. * * A blood pressure is a COMPONENT observation with no value of its own - systolic and diastolic are * two children. Reading `valueQuantity` on it returns nothing, and a vitals list that silently omits the * blood pressure is the omission a patient is least likely to notice and most likely to need. */ export const vitalValue = (observation: Observation): string | undefined => { // A blood pressure reads from its halves, never from a panel-level number stored beside them. const pressure = bloodPressureReading(observation); if (pressure) return pressure; const quantity = observation.valueQuantity; if (quantity?.value !== undefined) { return `${quantity.value}${quantity.unit ? ` ${quantity.unit}` : ""}`.trim(); } const parts = (observation.component ?? []) .map((component) => component.valueQuantity?.value) .filter((value): value is number => value !== undefined); if (parts.length >= 2) { const unit = observation.component?.[0]?.valueQuantity?.unit; return `${parts[0]}/${parts[1]}${unit ? ` ${unit}` : ""}`.trim(); } return observation.valueString?.trim() || undefined; }; /** Green is reserved for reassurance. Anything else keeps the quiet treatment. */ const REASSURING = new Set(["N", "NORMAL"]); const interpretationOf = (observation: Observation): { text?: string; reassuring: boolean } => { const coding = observation.interpretation?.[0]?.coding?.[0]; const text = observation.interpretation?.[0]?.text?.trim() || coding?.display?.trim(); const code = coding?.code?.toUpperCase(); return { text: text || (code && REASSURING.has(code) ? "in range" : undefined), reassuring: Boolean(code && REASSURING.has(code)), }; }; /** Active-equivalent statuses. Anything else is worth saying out loud next to the diagnosis. */ const STILL_ACTIVE = new Set(["active", "recurrence", "relapse"]); export const buildVisitDetail = ( encounter: Encounter, impressions: readonly ClinicalImpression[], observations: readonly Observation[], conditions: readonly Condition[] = [], documents: readonly DocumentReference[] = [], reports: readonly DiagnosticReport[] = [], ): VisitDetail => { const encounterId = encounter.id ?? ""; return { id: encounterId, when: encounter.period?.start, clinician: encounter.participant?.[0]?.individual?.display?.trim(), practice: encounter.serviceProvider?.display?.trim(), reason: encounter.reasonCode?.[0]?.text?.trim() || encounter.type?.[0]?.text?.trim(), notes: impressions .filter((impression) => belongsTo(encounterId, impression.encounter)) .map((impression) => ({ id: impression.id ?? "", text: impression.summary?.trim() ?? "", author: impression.assessor?.display?.trim(), writtenAt: impression.date, })) // ⛔ An impression with no summary is not a note the patient can read. Rendering an empty // bordered box under "What your clinician wrote" says something was written and withheld. .filter((note) => note.text.length > 0), vitals: observations .filter((observation) => belongsTo(encounterId, observation.encounter)) .map((observation) => { const { text, reassuring } = interpretationOf(observation); return { id: observation.id ?? "", label: observation.code?.text?.trim() || observation.code?.coding?.[0]?.display?.trim() || "", value: vitalValue(observation) ?? "", valuePath: observation.valueQuantity?.value !== undefined && !isBloodPressurePanel(observation) ? "valueQuantity" as const : "component" as const, interpretation: text, reassuring, }; }) // Same rule: a row with no name or no value is not a reading, it is a hole with a border. .filter((vital) => vital.label.length > 0 && vital.value.length > 0), /* * WHAT THE CLINICIAN CONCLUDED - and it is the plainest thing a visit produces. * * Measured on staging: a visit carrying a Sore throat diagnosis rendered "Nothing was recorded * at this visit yet", and a second visit carrying Acute pharyngitis showed only the vitals. In * both cases the Condition named that exact encounter, so the join was available and simply not * asked for - and the empty state was not merely thin, it was a positive claim that nothing * happened. * * Joined the same way as everything else here: an explicit `encounter` reference, never * same-day proximity. */ diagnoses: conditions .filter((condition) => belongsTo(encounterId, condition.encounter)) .map((condition) => { const status = condition.clinicalStatus?.coding?.[0]?.code?.trim().toLowerCase() || condition.clinicalStatus?.text?.trim().toLowerCase(); return { id: condition.id ?? "", label: condition.code?.text?.trim() || condition.code?.coding?.[0]?.display?.trim() || "", status: status && !STILL_ACTIVE.has(status) ? status : undefined, }; }) // A diagnosis with no name is not readable, and a bare row under this heading would say // something was concluded and withheld. .filter((diagnosis) => diagnosis.label.length > 0), /** * WHAT WAS FILED WITH THIS VISIT - the half that makes attaching a document worth doing. * * A document nobody attached to anything is a file in a list. Attaching it to the visit it * belongs to is what puts it where somebody reading that day will find it, and this is the * reading end of that: the container the patient already opens shows what was filed against it, * with no new destination anywhere. "An event is a container you open" is the whole design and * this is one more thing that came out of the visit. * * Joined the same way as everything else in this module: an explicit reference on the document * itself, never same-day proximity. `context.encounter` is a LIST on a DocumentReference - one * document can belong to several visits - so any entry naming this encounter counts. */ documents: documents .filter((document) => (document.context?.encounter ?? []).some((reference) => belongsTo(encounterId, reference))) .map((document) => ({ id: document.id ?? "", label: document.description?.trim() || document.type?.text?.trim() || document.type?.coding?.[0]?.display?.trim() || "", filedOn: document.date, })) // A document with no name of any kind is a row that says something is here and refuses to // say what, which is worse than the document not being listed. .filter((document) => document.label.length > 0), // Joined only by the report's encounter, never by the day it was drawn. results: reports .filter((report) => belongsTo(encounterId, report.encounter)) .map((report) => ({ id: report.id ?? "", label: report.code?.text?.trim() || report.code?.coding?.[0]?.display?.trim() || "", when: report.effectiveDateTime || report.issued, })) .filter((result) => result.id.length > 0 && result.label.length > 0), }; };