import type { Observation, ObservationComponent, Quantity } from 'fhir/r4'; import { BLOOD_PRESSURE } from './pairedObservation'; import { keepStoredQuantity, unitChoice } from './observationEdit'; // A LOINC term (8480-6); Parts (LP...) and answers (LA...) cannot code an observation. const LOINC_TERM = /^[0-9]{1,7}-[0-9]$/; export const isLoincTerm = (code: unknown): boolean => typeof code === 'string' && LOINC_TERM.test(code); /** The number a value box holds, or undefined when it holds none. A typed 0 is a value. */ export const enteredNumber = (raw: unknown): number | undefined => { if (raw === null || raw === undefined) return undefined; if (typeof raw === 'string' && raw.trim() === '') return undefined; const value = typeof raw === 'number' ? raw : Number(raw); return Number.isFinite(value) ? value : undefined; }; /** What a form component is called: its own text, else its coding, never an invented ordinal. */ export const componentLabel = (component: any, index: number): string => { const coding = component?.code?.coding?.[0]; return component?.code?.text?.trim() || coding?.display?.trim() || coding?.code?.trim() || `Component ${index + 1}`; }; /** Form components with a number and no unit, by label: each is refused before the save. */ export const componentsWithoutUnit = (components: any[] | undefined): string[] => (components ?? []).flatMap((component, index) => enteredNumber(component?.valueQuantity?.value) !== undefined && !component?.valueQuantity?.unit ? [componentLabel(component, index)] : []); export const hasComponentValue = (components: any[] | undefined): boolean => (components ?? []).some((component) => enteredNumber(component?.valueQuantity?.value) !== undefined); /** The Blood Pressure profile's starting form: the panel code and its two halves in mm[Hg], unread. */ export const bloodPressureFormStart = () => ({ code: { coding: [{ system: BLOOD_PRESSURE.panel.system, code: BLOOD_PRESSURE.panel.code, display: BLOOD_PRESSURE.panel.display }], text: BLOOD_PRESSURE.panel.display, }, halves: BLOOD_PRESSURE.halves.map((half) => ({ code: { coding: [{ system: half.system, code: half.code, display: half.display }], text: half.display }, valueType: 'quantity', valueQuantity: { value: '', unit: BLOOD_PRESSURE.unit.code, system: BLOOD_PRESSURE.unit.system, code: BLOOD_PRESSURE.unit.code }, })), }); const LOINC = 'http://loinc.org'; type Coded = { coding?: { system?: string; code?: string }[] } | undefined; const loincCodesOf = (concept: Coded): string[] => (concept?.coding ?? []).filter((coding) => coding.system === LOINC && coding.code).map((coding) => coding.code as string); const loincCodeOf = (concept: Coded): string | undefined => loincCodesOf(concept)[0]; const codedAs = (concept: Coded, code: string): boolean => loincCodesOf(concept).includes(code); const BLOOD_PRESSURE_PANELS = new Set([BLOOD_PRESSURE.panel.code, '55284-4']); const BLOOD_PRESSURE_PROFILE = 'http://hl7.org/fhir/us/core/StructureDefinition/us-core-blood-pressure'; /** A blood pressure by what the observation is, from any of its codings, whether or not its halves carry values. */ export const isBloodPressurePanel = (observation: Observation): boolean => loincCodesOf(observation.code).some((code) => BLOOD_PRESSURE_PANELS.has(code)) || (observation.meta?.profile ?? []).includes(BLOOD_PRESSURE_PROFILE); /** A quantity as recorded, its comparator kept: "<128 mm[Hg]". */ export const quantityText = (quantity: Quantity | undefined, withUnit = true): string | undefined => { if (quantity?.value === undefined || quantity.value === null) return undefined; const unit = quantity.unit || quantity.code || ''; return `${quantity.comparator ?? ''}${quantity.value}${withUnit && unit ? ` ${unit}` : ''}`; }; const conceptText = (concept: { text?: string; coding?: { display?: string; code?: string }[] } | undefined): string | undefined => concept?.text || concept?.coding?.find((coding) => coding.display)?.display || concept?.coding?.find((coding) => coding.code)?.code; /** What a component recorded, whichever value it holds; undefined only when it holds none. */ export const componentValueText = (component: ObservationComponent): string | undefined => { if (component.valueQuantity) return quantityText(component.valueQuantity); if (component.valueCodeableConcept) return conceptText(component.valueCodeableConcept) ?? 'a coded value'; if (component.valueString !== undefined) return component.valueString; if (component.valueBoolean !== undefined) return component.valueBoolean ? 'Yes' : 'No'; if (component.valueInteger !== undefined) return String(component.valueInteger); if (component.valueRange) return `${quantityText(component.valueRange.low) ?? ''} to ${quantityText(component.valueRange.high) ?? ''}`.trim(); if (component.valueRatio) return `${quantityText(component.valueRatio.numerator, false) ?? ''}:${quantityText(component.valueRatio.denominator, false) ?? ''}`; if (component.valueTime !== undefined) return component.valueTime; if (component.valueDateTime !== undefined) return component.valueDateTime; if (component.valuePeriod) return `${component.valuePeriod.start ?? ''} to ${component.valuePeriod.end ?? ''}`; if (component.valueSampledData) return 'sampled data, not shown here'; return undefined; }; const nameOf = (component: ObservationComponent): string => component.code?.coding?.find((coding) => coding.display)?.display || component.code?.text || loincCodeOf(component.code) || 'Component'; /** The component as said: its value, or why it has none, or that none was recorded. */ const componentStatus = (component: ObservationComponent): string => { const value = componentValueText(component); if (value !== undefined) return value; const reason = conceptText(component.dataAbsentReason); return reason ? `unavailable (${reason})` : 'not recorded'; }; const componentText = (component: ObservationComponent): string => `${nameOf(component)} ${componentStatus(component)}`; const BLOOD_PRESSURE_HALVES = BLOOD_PRESSURE.halves; /** One component coded as both halves: one measurement cannot be both pressures, so it is neither. */ const codedAsBothHalves = (component: ObservationComponent): boolean => BLOOD_PRESSURE_HALVES.every((half) => codedAs(component.code, half.code)); const halvesOf = (observation: Observation): ObservationComponent[][] => BLOOD_PRESSURE_HALVES.map((half) => (observation.component ?? []) .filter((component) => codedAs(component.code, half.code) && !codedAsBothHalves(component))); const describeOther = (component: ObservationComponent): string => (codedAsBothHalves(component) ? `${nameOf(component)} ${componentStatus(component)}, coded as both systolic and diastolic, so read as neither` : componentText(component)); /** * A blood pressure panel's reading from its halves, never from a panel-level number: "128/82 mm[Hg]" when * each half has one recorded number. A half recorded twice with different values is named as conflicting, * with every value, in an order that does not depend on the record's. Other measurements in the panel follow. */ export const bloodPressureReading = (observation: Observation): string | undefined => { if (!isBloodPressurePanel(observation)) return undefined; const halves = halvesOf(observation); const claimed = new Set(halves.flat()); const others = (observation.component ?? []).filter((component) => !claimed.has(component)).map(describeOther); const said = halves.map((list) => [...new Set(list.map(componentStatus))].sort()); const [high, low] = halves.map((list, index) => (said[index].length === 1 ? list[0]?.valueQuantity : undefined)); const highUnit = high?.unit || high?.code || ''; const lowUnit = low?.unit || low?.code || ''; let pair: string; if (quantityText(high) && quantityText(low)) { pair = highUnit === lowUnit ? `${quantityText(high, false)}/${quantityText(low, false)}${highUnit ? ` ${highUnit}` : ''}` : `${quantityText(high)}/${quantityText(low)}`; } else { pair = BLOOD_PRESSURE_HALVES.map((half, index) => { if (said[index].length === 0) return `${half.display} not recorded`; if (said[index].length === 1) return `${half.display} ${said[index][0]}`; return `${half.display} conflicting: ${said[index].join(', ')}`; }).join(', '); } return [pair, ...others].join(', '); }; /** Every component's reading, each named, for a result with no value of its own. */ export const componentReadings = (observation: Observation): string | undefined => { const parts = (observation.component ?? []).map(componentText); return parts.length > 0 ? parts.join(', ') : undefined; }; /** A recorded value this form cannot edit as a number, if the component holds one. */ export const otherValueArm = (component: ObservationComponent | undefined): string | undefined => component && !component.valueQuantity && componentValueText(component) !== undefined ? 'a recorded value that is not a number' : undefined; export const PAIR_EDIT_REFUSED = 'This reading holds more than this form can change. Ask your care team to update it.'; export const MEASUREMENT_CHANGE_REFUSED = "This reading is a blood pressure. Record a different measurement as a new reading."; /** * A patient's edit of a stored blood pressure, over that record: each half keeps everything stored unless its * number changed, other components and a stored panel value stay. A reading this form cannot show is refused. */ export const patientPairEdit = (stored: Observation, built: Observation): { observation: Observation } | { refused: string } => { const halves = halvesOf(stored); if ((stored.component ?? []).some(codedAsBothHalves)) return { refused: PAIR_EDIT_REFUSED }; if (halves.some((list) => list.length !== 1 || typeof list[0].valueQuantity?.value !== 'number')) return { refused: PAIR_EDIT_REFUSED }; const component = (stored.component ?? []).map((part) => { const index = halves.findIndex((list) => list[0] === part); if (index < 0) return part; const value = (built.component ?? []).find((half) => codedAs(half.code, BLOOD_PRESSURE_HALVES[index].code))?.valueQuantity?.value; if (value === undefined || value === part.valueQuantity?.value) return part; return { ...part, valueQuantity: keepStoredQuantity(part.valueQuantity, value, unitChoice(part.valueQuantity)) }; }); return { observation: { ...built, code: stored.code, component, ...(stored.valueQuantity ? { valueQuantity: stored.valueQuantity } : {}) } }; }; const sameJson = (a: unknown, b: unknown): boolean => JSON.stringify(a ?? null) === JSON.stringify(b ?? null); /** A stored component the edit opened on, whose recorded number the form now leaves empty. */ /** A number typed over a stored component that holds another kind of value, which this form cannot replace. */ export const numberOverOtherValue = (component: any, stored: ObservationComponent[] | undefined): boolean => { const base = typeof component?.storedIndex === 'number' ? stored?.[component.storedIndex] : undefined; return Boolean(otherValueArm(base)) && enteredNumber(component?.valueQuantity?.value) !== undefined; }; export const clearsStoredValue = (component: any, stored: ObservationComponent[] | undefined): boolean => { const base = typeof component?.storedIndex === 'number' ? stored?.[component.storedIndex] : undefined; return base?.valueQuantity?.value !== undefined && enteredNumber(component?.valueQuantity?.value) === undefined; }; /** * The components a save sends. One the edit opened on starts from what is stored and changes only what the * form changed, so codings, quantity qualifiers, absent-result reasons and other value arms survive. A new * one is sent only with a number. One removed from the form is not sent. */ export const componentsForSave = ( form: any[] | undefined, stored: ObservationComponent[] | undefined, interpretationOf: (entries: any[]) => any[] | undefined, ): ObservationComponent[] => (form ?? []).flatMap((component, index) => { const value = enteredNumber(component?.valueQuantity?.value); const unit = component?.valueQuantity?.unit || ''; const coding = component?.code?.coding?.[0]; const base = typeof component?.storedIndex === 'number' ? stored?.[component.storedIndex] : undefined; if (!base) { if (value === undefined) return []; const built: ObservationComponent = { code: { ...(coding?.code && { coding: [{ system: coding.system || LOINC, code: coding.code, ...(coding.display && { display: coding.display }) }] }), text: componentLabel(component, index), }, valueQuantity: keepStoredQuantity(undefined, value, unit), }; const interpretation = interpretationOf(component?.interpretation); if (interpretation) built.interpretation = interpretation; return [built]; } const out: ObservationComponent = JSON.parse(JSON.stringify(base)); const stored0 = base.code?.coding?.[0]; if ((coding?.code ?? '') !== (stored0?.code ?? '') || (coding?.display ?? '') !== (stored0?.display ?? '')) { out.code = { ...(coding?.code && { coding: [{ system: stored0?.system || LOINC, code: coding.code, ...(coding.display && { display: coding.display }) }] }), text: componentLabel(component, index), }; } // A component holding another kind of value keeps it: one component never carries two values. if (value !== undefined && !otherValueArm(base)) { out.valueQuantity = keepStoredQuantity(base.valueQuantity, value, unit); delete out.dataAbsentReason; } if (!sameJson(component?.interpretation ?? [], base.interpretation ?? [])) { const interpretation = interpretationOf(component?.interpretation); if (interpretation) out.interpretation = interpretation; else delete out.interpretation; } return [out]; });