import { withRead, type ReadOptions } from '../hooks/anchorView/readOptions'; import { holdWrite, isHeld, overlayHeldBundle, startWrite, writtenSince } from '../utils/writeThroughHold'; import type { SaveTransport } from '../utils/saveOperation'; import type { SelfReport } from '../utils/selfReport'; import { asRecordChanged, ifMatch, PAGE_OUT_OF_DATE_MESSAGE, RecordChangedError } from '../utils/recordChanged'; import { useState, useCallback, createContext, useContext, ReactNode, useEffect, useRef } from 'react'; import { Bundle, Observation, Patient, ObservationComponent, Encounter, Resource } from 'fhir/r4'; import { environment } from '../environments/environment.ts'; import { useFhirServiceContext } from './FhirService.tsx'; import { useHttpService } from './HttpService.tsx'; import { formatFhirHumanName } from '../utils/fhirHumanName.tsx'; import { useEncounterServiceContext } from './EncounterService.tsx'; import { usePatientServiceContext } from './PatientService.tsx'; import { formatFhirCodeableConcept } from '../utils/fhirCodeableConcept.tsx'; import { applyArchive, isArchived, type ArchiveReason } from '../utils/archivedResource'; import { statusLabel } from '../utils/fhirStatusVocabulary'; import { fetchAllPagesAsBundle } from '../utils/fetchAllPagesAsBundle'; import { searchPageSize } from '../utils/pageSizeOverride'; import { useCacheOwner } from '../utils/cacheOwner'; import { observationOperationUrl, operationStandingOf, type OperationStanding, type WithheldSave } from '../utils/observationOperation'; const API_URL = environment.fhirClinicalApi; export const useObservationService = () => { const { get, post, put, delete: del } = useHttpService(); const { patientState } = usePatientServiceContext(); const { transformToCodeableConcept, transformFromCodeableConcept, createReferenceObject, formatDateTime, formatDateTimeForInput, formatDateTimeForDisplay, formatToFhirInstant, unpackBundle, addFhirPeriodIfApplicable, addPeriodControlsIfApplicable, transformToFhirQuantity, transformFromFhirQuantity, transformToReferenceCollection, transformFromReferenceCollection, transformToFhirTiming, transformFromFhirTiming, extractReferenceId } = useFhirServiceContext(); const { getEncounterDisplay } = useEncounterServiceContext(); const [observationsList, setObservationsList] = useState([]); const [shouldReload, setShouldReload] = useState(false); const [lastUpserted, setLastUpserted] = useState(null); const [unsaved, setUnsaved] = useState(false); const [observationsBundle, setObservationsBundle] = useState | null>(null); const observationsBundleRef = useRef | null>(null); const lastFetchTimeRef = useRef(0); const usCoreObservationProfilePrefix = 'http://hl7.org/fhir/us/core/StructureDefinition'; // US Core Vital Signs Profile const usCoreVitalSignsProfile = { category: [ { system: 'http://terminology.hl7.org/CodeSystem/observation-category', code: 'vital-signs', display: 'Vital Signs' } ] }; // US Core Average Blood Pressure Profile const usCoreAverageBloodPressureProfile = { ...usCoreVitalSignsProfile, observationProfile: `${usCoreObservationProfilePrefix}/us-core-average-blood-pressure`, code: { system: 'http://loinc.org', code: '96607-7', display: 'Blood pressure panel mean systolic and mean diastolic' }, component: [ { code: { system: 'http://loinc.org', code: '96608-5', display: 'Systolic blood pressure mean' }, valueType: 'quantity', valueQuantity: { value: null, unit: { system: 'http://unitsofmeasure.org', code: 'mm[Hg]', display: 'mm[Hg]', } }, dataAbsentReason: null, interpretation: [], referenceRange: [] }, { code: { system: 'http://loinc.org', code: '96609-3', display: 'Diastolic blood pressure mean' }, valueType: 'quantity', valueQuantity: { value: null, unit: { system: 'http://unitsofmeasure.org', code: 'mm[Hg]', display: 'mm[Hg]', } }, dataAbsentReason: null, interpretation: [], referenceRange: [] } ] }; // US Core Care Experience Preference Profile const usCoreCareExperiencePreferenceProfile = { category: [ { system: 'http://hl7.org/fhir/us/core/CodeSystem/us-core-category', code: 'care-experience-preference', display: 'Care Experience Preference' } ], observationProfile: `${usCoreObservationProfilePrefix}/us-core-care-experience-preference`, code: { system: 'http://loinc.org', code: '95541-9', display: 'Care experience preference' }, valueType: 'string', valueString: '', }; // US Core Observation ADI Documentation Profile const usCoreObservationAdiDocumentationProfile = { category: [ { system: 'http://hl7.org/fhir/us/core/CodeSystem/us-core-category', code: 'observation-adi-documentation', display: 'Observation ADI Documentation' } ], observationProfile: `${usCoreObservationProfilePrefix}/us-core-observation-adi-documentation`, code: { system: 'http://loinc.org', code: '45473-6', display: 'Advance healthcare directive completed' }, valueType: 'codeableConcept', valueCodeableConcept: null }; // US Core Observation Clinical Result Profile const usCoreObservationClinicalResultProfile = { observationProfile: `${usCoreObservationProfilePrefix}/us-core-observation-clinical-result`, code: { system: 'http://loinc.org', code: '45473-6', display: 'Advance healthcare directive completed' }, valueType: 'quantity', valueQuantity: { value: null, }, }; // US Core Laboratory Result Observation Profile const usCoreObservationLabProfile = { category: [ { system: 'http://terminology.hl7.org/CodeSystem/observation-category', code: 'laboratory', display: 'Laboratory' } ], observationProfile: `${usCoreObservationProfilePrefix}/us-core-observation-lab`, valueType: 'quantity', valueQuantity: { value: null, } }; // US Core Observation Occupation Profile const usCoreObservationOccupationProfile = { category: [ { system: 'http://terminology.hl7.org/CodeSystem/observation-category', code: 'social-history', display: 'Social History' } ], observationProfile: `${usCoreObservationProfilePrefix}/us-core-observation-occupation`, code: { system: 'http://loinc.org', code: '11341-5', display: 'History of Occupation' }, valueType: 'codeableConcept', valueCodeableConcept: null, component: [ { code: { system: 'http://loinc.org', code: '86188-0', display: 'History of Occupation industry' }, valueType: 'codeableConcept', valueCodeableConcept: null, dataAbsentReason: null, interpretation: [], referenceRange: [] } ] }; // US Core Observation Pregnancy Intent Profile const usCoreObservationPregnancyintentProfile = { category: [ { system: 'http://terminology.hl7.org/CodeSystem/observation-category', code: 'social-history', display: 'Social History' } ], observationProfile: `${usCoreObservationProfilePrefix}/us-core-observation-pregnancyintent`, code: { system: 'http://loinc.org', code: '86645-9', display: 'Pregnancy intention in the next year - Reported' }, valueType: 'codeableConcept', valueCodeableConcept: null }; // US Core Observation Pregnancy Status Profile const usCoreObservationPregnancystatusProfile = { category: [ { system: 'http://terminology.hl7.org/CodeSystem/observation-category', code: 'social-history', display: 'Social History' } ], observationProfile: `${usCoreObservationProfilePrefix}/us-core-observation-pregnancystatus`, code: { system: 'http://loinc.org', code: '82810-3', display: 'Pregnancy status' }, valueType: 'codeableConcept', valueCodeableConcept: null }; // US Core Observation Screening Assessment Profile const usCoreObservationScreeningAssessmentProfile = { category: [ { system: 'http://terminology.hl7.org/CodeSystem/observation-category', code: 'survey', display: 'Survey' } ], observationProfile: `${usCoreObservationProfilePrefix}/us-core-observation-screening-assessment` }; // US Core Observation Sexual Orientation Profile const usCoreObservationSexualOrientationProfile = { observationProfile: `${usCoreObservationProfilePrefix}/us-core-observation-sexual-orientation`, code: { system: 'http://loinc.org', code: '76690-7', display: 'Sexual orientation' }, valueType: 'codeableConcept', valueCodeableConcept: null }; // US Core Simple Observation Profile const usCoreSimpleObservationProfile = { observationProfile: `${usCoreObservationProfilePrefix}/us-core-simple-observation` }; // US Core Smoking Status Observation Profile const usCoreSmokingstatusProfile = { category: [ { system: 'http://terminology.hl7.org/CodeSystem/observation-category', code: 'social-history', display: 'Social History' } ], observationProfile: `${usCoreObservationProfilePrefix}/us-core-smokingstatus`, valueType: 'codeableConcept', valueCodeableConcept: null }; // US Core Treatment Intervention Preference Profile const usCoreTreatmentInterventionPreferenceProfile = { category: [ { system: 'http://hl7.org/fhir/us/core/CodeSystem/us-core-category', code: 'treatment-intervention-preference', display: 'Treatment Intervention Preference' } ], observationProfile: `${usCoreObservationProfilePrefix}/us-core-treatment-intervention-preference`, code: { system: 'http://loinc.org', code: '75773-2', display: 'Goals, preferences, and priorities for medical treatment Narrative - Reported' } }; // US Core Pediatric BMI for Age Observation Profile const usCorePediatricMmiForAgeProfile = { ...usCoreVitalSignsProfile, observationProfile: `${usCoreObservationProfilePrefix}/pediatric-bmi-for-age`, code: { system: 'http://loinc.org', code: '59576-9', display: 'Body mass index (BMI) [Percentile] Per age and sex' }, valueType: 'quantity', valueQuantity: { value: null, unit: { system: 'http://unitsofmeasure.org', code: '%', display: '%', } }, }; // US Core Pediatric Weight for Height Observation Profile const pediatricWeightForHeightProfile = { ...usCoreVitalSignsProfile, observationProfile: `${usCoreObservationProfilePrefix}/pediatric-weight-for-height`, code: { system: 'http://loinc.org', code: '77606-2', display: 'Weight-for-length Per age and sex' }, valueType: 'quantity', valueQuantity: { value: null, unit: { system: 'http://unitsofmeasure.org', code: '%', display: '%', } }, }; // US Core Blood Pressure Profile const usCoreBloodPressureProfile = { ...usCoreVitalSignsProfile, observationProfile: `${usCoreObservationProfilePrefix}/us-core-blood-pressure`, code: { system: 'http://loinc.org', code: '85354-9', display: 'Blood pressure panel with all children optional' }, component: [ { code: { system: 'http://loinc.org', code: '8480-6', display: 'Systolic blood pressure' }, valueType: 'quantity', valueQuantity: { value: null, unit: { system: 'http://unitsofmeasure.org', code: 'mm[Hg]', display: 'mm[Hg]', } }, dataAbsentReason: null, interpretation: [], referenceRange: [] }, { code: { system: 'http://loinc.org', code: '8462-4', display: 'Diastolic blood pressure' }, valueType: 'quantity', valueQuantity: { value: null, unit: { system: 'http://unitsofmeasure.org', code: 'mm[Hg]', display: 'mm[Hg]', } }, dataAbsentReason: null, interpretation: [], referenceRange: [] } ] }; // US Core BMI Profile const usCoreBmiProfile = { ...usCoreVitalSignsProfile, observationProfile: `${usCoreObservationProfilePrefix}/us-core-bmi`, code: { system: 'http://loinc.org', code: '39156-5', display: 'Body mass index (BMI) [Ratio]' }, valueType: 'quantity', valueQuantity: { value: null, unit: { system: 'http://unitsofmeasure.org', code: 'kg/m2', display: 'kg/m2', } }, }; // US Core Body Height Profile us-core-body-height const usCoreBodyHeightProfile = { ...usCoreVitalSignsProfile, observationProfile: `${usCoreObservationProfilePrefix}/us-core-body-height`, code: { system: 'http://loinc.org', code: '8302-2', display: 'Body height' }, valueType: 'quantity', valueQuantity: { value: null, }, }; // US Core Body Temperature Profile const usCoreBodyTemperatureProfile = { ...usCoreVitalSignsProfile, observationProfile: `${usCoreObservationProfilePrefix}/us-core-body-temperature`, code: { system: 'http://loinc.org', code: '8310-5', display: 'Body temperature' }, valueType: 'quantity', valueQuantity: { value: null, }, }; // US Core Body Weight Profile const usCoreBodyWeightProfile = { ...usCoreVitalSignsProfile, observationProfile: `${usCoreObservationProfilePrefix}/us-core-body-weight`, code: { system: 'http://loinc.org', code: '29463-7', display: 'Body weight' }, valueType: 'quantity', valueQuantity: { value: null, }, }; // US Core Head Circumference Profile us-core-head-circumference const usCoreHeadCircumferenceProfile = { ...usCoreVitalSignsProfile, observationProfile: `${usCoreObservationProfilePrefix}/us-core-head-circumference`, code: { system: 'http://loinc.org', code: '9843-4', display: 'Head Occipital-frontal circumference' }, valueType: 'quantity', valueQuantity: { value: null, }, }; // US Core Heart Rate Profile const usCoreHeartRateProfile = { ...usCoreVitalSignsProfile, observationProfile: `${usCoreObservationProfilePrefix}/us-core-heart-rate`, code: { system: 'http://loinc.org', code: '8867-4', display: 'Heart rate' }, valueType: 'quantity', valueQuantity: { value: null, unit: { system: 'http://unitsofmeasure.org', code: '/min', display: '/min', } } }; // US Core Pulse Oximetry Profile const usCorePulseOximetryProfile = { ...usCoreVitalSignsProfile, observationProfile: `${usCoreObservationProfilePrefix}/us-core-pulse-oximetry`, code: [ { system: 'http://loinc.org', code: '59408-5', display: 'Oxygen saturation in Arterial blood by Pulse oximetry' }, { system: 'http://loinc.org', code: '2708-6', display: 'Oxygen saturation in Arterial blood' }, ], valueType: 'quantity', valueQuantity: { value: null, unit: { system: 'http://unitsofmeasure.org', code: '%', display: '%', } }, component: [ { code: { system: 'http://loinc.org', code: '3151-8', display: 'Inhaled oxygen flow rate' }, valueType: 'quantity', valueQuantity: { value: null, unit: { system: 'http://unitsofmeasure.org', code: 'L/min', display: 'L/min', } }, dataAbsentReason: null, interpretation: [], referenceRange: [] }, { code: { system: 'http://loinc.org', code: '3150-0', display: 'Inhaled oxygen concentration' }, valueType: 'quantity', valueQuantity: { value: null, unit: { system: 'http://unitsofmeasure.org', code: '%', display: '%', } }, dataAbsentReason: null, interpretation: [], referenceRange: [] } ] }; // US Core Respiratory Rate Profile const usCoreRespiratoryRateProfile = { ...usCoreVitalSignsProfile, observationProfile: `${usCoreObservationProfilePrefix}/us-core-respiratory-rate`, code: { system: 'http://loinc.org', code: '9279-1', display: 'Respiratory rate' }, valueType: 'quantity', valueQuantity: { value: null, unit: { system: 'http://unitsofmeasure.org', code: '/min', display: '/min', } } }; // US Core Pediatric Head Occipital Frontal Circumference Percentile Profile const headOccipitalFrontalCircumferencePercentileProfile = { ...usCoreVitalSignsProfile, observationProfile: `${usCoreObservationProfilePrefix}/head-occipital-frontal-circumference-percentile`, code: { system: 'http://loinc.org', code: '8289-1', display: 'Head Occipital-frontal circumference Percentile' }, valueType: 'quantity', valueQuantity: { value: null, unit: { system: 'http://unitsofmeasure.org', code: '%', display: '%', } } }; useEffect(() => { observationsBundleRef.current = observationsBundle; }, [observationsBundle]); // WHOSE RECORDS ARE THESE? The reset below fires only when there is NO patient, which moving // between two charts never is - so the previous patient's bundle survived the switch and was // served as a cache hit, with no request and nothing on screen to say the list was not theirs. const cacheOwner = useCacheOwner('Observation', () => { setLastUpserted(null); setObservationsBundle(null); setObservationsList([]); lastFetchTimeRef.current = 0; }); useEffect(() => { if (!patientState) { setObservationsBundle(null); setObservationsList([]); lastFetchTimeRef.current = 0; cacheOwner.release(); } }, [patientState, cacheOwner.release]); const fetchObservations = useCallback(async (patient: Patient, transport?: SaveTransport, options?: ReadOptions): Promise> => { const bundle = await fetchAllPagesAsBundle((pageParam) => { let params = new URLSearchParams(); params.append('_count', searchPageSize('100')); params.append('subject', patient.id as string); if (pageParam) params.set('page', pageParam); return get>(`${API_URL}/Observation?${params.toString()}`, withRead({ ...transport }, options)); }); // Held writes back in front of the search, scoped to the patient being read - this service is a // singleton and staff move between charts, so one patient's held record must never appear on // another's list. return overlayHeldBundle('Observation', bundle, (observation) => extractReferenceId(observation.subject) === patient.id) as Bundle; }, [get, extractReferenceId]); const fetchObservationsWithPolling = useCallback(async (patient: Patient, forceReload = false): Promise> => { const now = Date.now(); const cacheAge = now - lastFetchTimeRef.current; const cacheFresh = cacheAge < 5 * 60 * 1000; // cacheOwner.isFor is the load-bearing clause: a fresh bundle belonging to the PREVIOUS // patient satisfies every other condition here and would be returned as a hit. if (!forceReload && !shouldReload && !writtenSince('Observation', lastFetchTimeRef.current) && observationsBundleRef.current && cacheFresh && cacheOwner.isCurrentFor(patient.id)) { return observationsBundleRef.current; } const operation = cacheOwner.begin(); let retries = 10; const delay = 3000; let found = false; let bundle: Bundle | null = null; let entries: Observation[]; do { bundle = await fetchObservations(patient); entries = unpackBundle(bundle).filter((r) => !isArchived(r)); found = lastUpserted ? entries.some(e => e.id === lastUpserted) || isHeld('Observation', lastUpserted) : true; if (!found) { await new Promise(resolve => setTimeout(resolve, delay)); } retries--; } while (!found && retries > 0 && cacheOwner.publishes(operation)); if (!cacheOwner.publishes(operation)) return cacheOwner.answerFor(operation, bundle); setObservationsBundle(bundle); setObservationsList(entries.filter((r) => !isArchived(r))); setLastUpserted(null); setShouldReload(false); lastFetchTimeRef.current = now; // Stamped WITH the data, never before it - on a throw the owner must still name the list // that is actually in state. cacheOwner.claim(patient.id, operation); return bundle; }, [fetchObservations, unpackBundle, lastUpserted, shouldReload]); // `fresh`: the record as stored now, never the cached copy - what a refused save shows as "saved now". const fetchObservation = useCallback(async (id: string, fresh = false, transport?: SaveTransport): Promise => { const cachedObservation = !fresh && cacheOwner.isCurrent(cacheOwner.claimed) ? observationsList.find(observation => observation.id === id) : undefined; if (cachedObservation) { return cachedObservation; } return get(`${API_URL}/Observation/${id}`, { ...transport }); }, [get, observationsList]); const fetchObservationHistory = useCallback(async (id: string, options?: ReadOptions): Promise> => { return get>(`${API_URL}/Observation/${id}/_history`, options); }, [get]); // One save, named by `operationId` and sent unchanged on every retry of it, so a resend is never a second reading. // A saved reading this viewer may not see now answers WithheldSave: saved all the same. const createObservationOnce = useCallback(async (observation: Observation, operationId: string, transport?: SaveTransport): Promise => post(`${API_URL}/Observation?subject=${encodeURIComponent(extractReferenceId(observation.subject) as string)}`, observation, { headers: { 'X-Operation-Id': operationId }, ...transport }), [post, extractReferenceId]); /** Where one earlier save stands, for this viewer only. A 404 is no operation of theirs under that id. */ const fetchObservationOperation = useCallback(async (operationId: string, patientId: string, transport?: SaveTransport): Promise => { try { return operationStandingOf(await get(observationOperationUrl(API_URL, operationId, patientId), { ...transport })); } catch (error) { if ((error as { status?: unknown } | null)?.status === 404) return { state: 'unknown' }; throw error; } }, [get]); /** * Finishes a pending save with what the server admitted for it, never what an editor now holds. A fenced save is * resumed only by presenting the continuation its status handed out. */ const finishObservationOperation = useCallback(async (operationId: string, patientId: string, transport?: SaveTransport, continuation?: string): Promise => post(observationOperationUrl(API_URL, operationId, patientId), continuation ? { continuation } : undefined, { headers: { 'X-Operation-Id': operationId }, ...transport }), [post]); // A patient's own report about themself goes to its route as typed fields; the server builds the record and records // where the patient said it came from. Only the patient reporting for themself is offered this. const recordSelfReport = useCallback(async (patientId: string, report: SelfReport, transport?: SaveTransport): Promise => post(`${environment.orgAuthorizationApi}/self-report?subject=${encodeURIComponent(`Patient/${patientId}`)}`, report, { ...transport }), [post]); // The patient changing their own report: typed fields again, to the edit of that report, only over the version they // were shown. A refusal for a changed record is a RecordChangedError, as the ordinary update's. // The saved version is held for the viewer who saved it: a patient with no practice gets no broadcast, so the next // read takes it over a search that has not caught up, and the next edit opens on it. const editSelfReport = useCallback(async (observationId: string, report: SelfReport, basedOn: string | undefined, transport?: SaveTransport): Promise => { if (!basedOn) throw new RecordChangedError(PAGE_OUT_OF_DATE_MESSAGE, true); const startedIn = startWrite(); const saved = await put(`${environment.orgAuthorizationApi}/Observation/${encodeURIComponent(observationId)}/self-report`, report, { headers: ifMatch(basedOn), ...transport }) .catch((error: unknown) => { throw asRecordChanged(error); }); holdWrite(saved as Observation, Date.now(), startedIn); return saved; }, [put]); // CONDITIONAL: written only over `basedOn`, the version the editor opened on (or reviewed), never the version // echoed in the body. A refusal is a RecordChangedError: 412 another write landed first, 428 no version named. const updateObservation = useCallback(async (observation: Observation, basedOn: string | undefined, transport?: SaveTransport): Promise => { if (!basedOn) throw new RecordChangedError(PAGE_OUT_OF_DATE_MESSAGE, true); return put(`${API_URL}/Observation/${observation.id}`, observation, { headers: ifMatch(basedOn), ...transport }) .catch((error: unknown) => { throw asRecordChanged(error); }); }, [put]); const deleteObservation = useCallback(async (observation: Observation): Promise => { return del(`${API_URL}/Observation/${observation.id}`); }, [del]); const archiveObservation = useCallback( async (observation: Observation, reason: ArchiveReason, note?: string, transport?: SaveTransport): Promise => // Built on the record it is given, so written only over that record's version. updateObservation(applyArchive(observation, reason, note), observation.meta?.versionId, transport), [updateObservation], ); const transformToFhirObservation = useCallback((item: any, patient?: Patient, encounterState?: Encounter): Observation => { const { identifier, status, category, code, encounter, effectiveType, effectiveDateTime, effectiveTiming, effectiveInstant, issued, valueType, valueQuantity, valueCodeableConcept, valueString, valueBoolean, valueInteger, valueRange, valueRatio, valueSampledData, valueTime, valueDateTime, valuePeriod, dataAbsentReason, interpretation, note, bodySite, method, referenceRange, component, meta, observationProfile, basedOn, partOf, performer, device } = item; const transformedBasedOn = transformToReferenceCollection(basedOn); const transformedPartOf = transformToReferenceCollection(partOf); const transformedPerformer = transformToReferenceCollection(performer); let observation: Observation = { resourceType: 'Observation', status: status, ...((meta || observationProfile) && { meta: { ...meta, ...(observationProfile && { profile: [observationProfile] }) } }), ...(patient && { subject: createReferenceObject('Patient', patient.id as string, formatFhirHumanName(patient.name)) }), ...(encounterState && { encounter: createReferenceObject('Encounter', encounterState?.id as string, getEncounterDisplay(encounterState)) }), ...(encounter && { encounter: encounter }), ...(transformedPartOf && transformedPartOf.length !== 0 && { partOf: transformedPartOf }), ...(transformedBasedOn && transformedBasedOn.length !== 0 && { basedOn: transformedBasedOn }), ...(transformedPerformer && transformedPerformer.length !== 0 && { performer: transformedPerformer }), ...(device && { device: device }), ...(identifier && identifier.length !== 0 && { identifier }), ...(category && category.length !== 0 && { category: transformToCodeableConcept(category) }), ...(code && { code: transformToCodeableConcept(code) }), ...(effectiveType === 'dateTime' && effectiveDateTime && { effectiveDateTime: formatDateTime(effectiveDateTime) }), ...(effectiveType === 'timing' && effectiveTiming && { effectiveTiming: transformToFhirTiming(effectiveTiming) }), //...(effectiveInstant && { effectiveInstant: formatDate(effectiveInstant) }), ...(issued && { issued: formatToFhirInstant(issued) }), ...(valueType === 'quantity' && valueQuantity && { valueQuantity: transformToFhirQuantity(valueQuantity) }), ...(valueType === 'codeableConcept' && valueCodeableConcept && { valueCodeableConcept: transformToCodeableConcept(valueCodeableConcept) }), ...(valueType === 'string' && valueString && { valueString }), ...(valueType === 'boolean' && valueBoolean !== undefined && { valueBoolean }), ...(valueType === 'integer' && valueInteger !== undefined && { valueInteger }), ...(valueType === 'range' && valueRange && { valueRange }), ...(valueType === 'ratio' && valueRatio && { valueRatio }), ...(valueType === 'sampledData' && valueSampledData && { valueSampledData }), ...(valueType === 'time' && valueTime && { valueTime }), ...(valueType === 'dateTime' && valueDateTime && { valueDateTime: formatDateTime(valueDateTime) }), ...(valueType === 'period' && valuePeriod && { valuePeriod }), ...(dataAbsentReason && { dataAbsentReason: transformToCodeableConcept(dataAbsentReason) }), ...(interpretation && interpretation.length !== 0 && { interpretation: transformToCodeableConcept(interpretation) }), ...(note && note.length !== 0 && { note }), ...(bodySite && { bodySite: transformToCodeableConcept(bodySite) }), ...(method && { method: transformToCodeableConcept(method) }), ...(referenceRange && referenceRange.length !== 0 && { referenceRange: referenceRange.map((range: any) => ({ ...(range.low && { low: range.low }), ...(range.high && { high: range.high }), ...(range.type && { type: transformToCodeableConcept(range.type) }), ...(range.appliesTo && range.appliesTo.length !== 0 && { appliesTo: transformToCodeableConcept(range.appliesTo) }), ...(range.age && { age: range.age }), ...(range.text && { text: range.text }) })) }), ...(component && component.length > 0 && { component: component.map((comp: any) => { const { code, valueType, valueQuantity, valueCodeableConcept, valueString, valueBoolean, valueInteger, valueRange, valueRatio, valueSampledData, valueTime, valueDateTime, valuePeriod, dataAbsentReason, interpretation, referenceRange } = comp; const componentItem: ObservationComponent = { code: transformToCodeableConcept(code), ...(valueType === 'quantity' && valueQuantity && { valueQuantity: transformToFhirQuantity(valueQuantity) }), ...(valueType === 'codeableConcept' && valueCodeableConcept && { valueCodeableConcept: transformToCodeableConcept(valueCodeableConcept) }), ...(valueType === 'string' && valueString && { valueString: valueString }), ...(valueType === 'boolean' && valueBoolean !== undefined && { valueBoolean: valueBoolean }), ...(valueType === 'integer' && valueInteger !== undefined && { valueInteger: valueInteger }), ...(valueType === 'range' && valueRange && { valueRange: valueRange }), ...(valueType === 'ratio' && valueRatio && { valueRatio: valueRatio }), ...(valueType === 'sampledData' && valueSampledData && { valueSampledData: valueSampledData }), ...(valueType === 'time' && valueTime && { valueTime: valueTime }), ...(valueType === 'dateTime' && valueDateTime && { valueDateTime: formatDateTime(valueDateTime) }), ...(valueType === 'period' && valuePeriod && { valuePeriod: valuePeriod }), ...(dataAbsentReason && { dataAbsentReason: transformToCodeableConcept(dataAbsentReason) }), ...(interpretation && interpretation.length !== 0 && { interpretation: transformToCodeableConcept(interpretation) }), ...(referenceRange && referenceRange.length !== 0 && { referenceRange: comp.referenceRange.map((range: any) => ({ ...(range.low && { low: range.low }), ...(range.high && { high: range.high }), ...(range.type && { type: transformToCodeableConcept(range.type) }), ...(range.appliesTo && range.appliesTo.length > 0 && { appliesTo: transformToCodeableConcept(range.appliesTo) }), ...(range.age && { age: range.age }), ...(range.text && { text: range.text }) })) }) }; return componentItem; }) }) }; if (effectiveType === 'period') { return addFhirPeriodIfApplicable(observation, item, 'effectivePeriod'); } return observation; }, [ createReferenceObject, transformToCodeableConcept, formatDateTime, formatToFhirInstant, transformToFhirQuantity, transformToReferenceCollection, getEncounterDisplay, transformToFhirTiming, addFhirPeriodIfApplicable ]); const transformFromFhirObservation = useCallback((observation: Observation): any => { const { identifier, basedOn, partOf, status, category, code, focus, encounter, effectiveDateTime, effectivePeriod, effectiveTiming, effectiveInstant, issued, performer, valueQuantity, valueCodeableConcept, valueString, valueBoolean, valueInteger, valueRange, valueRatio, valueSampledData, valueTime, valueDateTime, valuePeriod, dataAbsentReason, interpretation, note, bodySite, method, specimen, device, referenceRange, hasMember, derivedFrom, component, meta } = observation; let valueType = ''; if (valueQuantity) valueType = 'quantity'; else if (valueCodeableConcept) valueType = 'codeableConcept'; else if (valueString !== undefined) valueType = 'string'; else if (valueBoolean !== undefined) valueType = 'boolean'; else if (valueInteger !== undefined) valueType = 'integer'; else if (valueRange) valueType = 'range'; else if (valueRatio) valueType = 'ratio'; else if (valueSampledData) valueType = 'sampledData'; else if (valueTime) valueType = 'time'; else if (valueDateTime) valueType = 'dateTime'; else if (valuePeriod) valueType = 'period'; const formData = { meta: meta, observationProfileSlug: meta?.profile?.find(profile => profile.startsWith(usCoreObservationProfilePrefix + '/'))?.replace(usCoreObservationProfilePrefix + '/', '') || '', identifier: identifier ?? [], basedOn: transformFromReferenceCollection(basedOn ?? []), partOf: transformFromReferenceCollection(partOf ?? []), performer: transformFromReferenceCollection(performer ?? []), device: device, status: status, category: transformFromCodeableConcept(category ?? []), code: transformFromCodeableConcept(code), focus: focus ?? [], encounter: encounter, effectiveType: effectiveDateTime ? 'dateTime' : effectivePeriod ? 'period' : effectiveTiming ? 'timing' : '', effectiveDateTime: formatDateTimeForInput(effectiveDateTime), effectiveTiming: transformFromFhirTiming(effectiveTiming), effectiveInstant: effectiveInstant ?? '', issued: formatDateTimeForInput(issued ?? ''), valueType: valueType, valueQuantity: transformFromFhirQuantity(valueQuantity), valueCodeableConcept: transformFromCodeableConcept(valueCodeableConcept), valueString: valueString ?? '', valueBoolean: valueBoolean !== undefined ? valueBoolean : false, valueInteger: valueInteger ?? 0, valueRange: valueRange, valueRatio: valueRatio, valueSampledData: valueSampledData, valueTime: valueTime ?? '', valueDateTime: formatDateTimeForInput(valueDateTime), valuePeriod: valuePeriod, dataAbsentReason: transformFromCodeableConcept(dataAbsentReason), interpretation: transformFromCodeableConcept(interpretation ?? []), note: note ?? [], bodySite: transformFromCodeableConcept(bodySite), method: transformFromCodeableConcept(method), referenceRange: (referenceRange ?? []).map(range => ({ low: range.low || null, high: range.high || null, type: transformFromCodeableConcept(range.type), appliesTo: transformFromCodeableConcept(range.appliesTo ?? []), age: range.age || null, text: range.text || '' })), component: (component || []).map(comp => { let compValueType = ''; if (comp.valueQuantity) compValueType = 'quantity'; else if (comp.valueCodeableConcept) compValueType = 'codeableConcept'; else if (comp.valueString !== undefined) compValueType = 'string'; else if (comp.valueBoolean !== undefined) compValueType = 'boolean'; else if (comp.valueInteger !== undefined) compValueType = 'integer'; else if (comp.valueRange) compValueType = 'range'; else if (comp.valueRatio) compValueType = 'ratio'; else if (comp.valueSampledData) compValueType = 'sampledData'; else if (comp.valueTime) compValueType = 'time'; else if (comp.valueDateTime) compValueType = 'dateTime'; else if (comp.valuePeriod) compValueType = 'period'; const extendedComponent: any = { code: transformFromCodeableConcept(comp.code), valueType: compValueType, valueQuantity: transformFromFhirQuantity(comp.valueQuantity), valueCodeableConcept: transformFromCodeableConcept(comp.valueCodeableConcept), valueString: comp.valueString ?? '', valueBoolean: comp.valueBoolean !== undefined ? comp.valueBoolean : false, valueInteger: comp.valueInteger ?? 0, valueRange: comp.valueRange, valueRatio: comp.valueRatio, valueSampledData: comp.valueSampledData, valueTime: comp.valueTime ?? '', valueDateTime: formatDateTimeForInput(comp.valueDateTime), valuePeriod: comp.valuePeriod, dataAbsentReason: transformFromCodeableConcept(comp.dataAbsentReason), interpretation: transformFromCodeableConcept(comp.interpretation ?? []), referenceRange: (comp.referenceRange || []).map(range => ({ low: range.low || null, high: range.high || null, type: transformFromCodeableConcept(range.type), appliesTo: transformFromCodeableConcept(range.appliesTo ?? []), age: range.age, text: range.text ?? '' })) }; return extendedComponent; }) }; if (effectivePeriod) { return addPeriodControlsIfApplicable(formData, observation, 'effectivePeriod'); } return formData; }, [ transformFromCodeableConcept, transformFromFhirQuantity, transformFromReferenceCollection, transformFromFhirTiming, formatDateTimeForInput, addPeriodControlsIfApplicable, formatDateTimeForInput ]); const getObservationDisplay = (observation: Observation): string => { if (!observation) { return 'Unknown Observation'; } const observationName = formatFhirCodeableConcept(observation.code) || 'Observation'; // - the human label, never the code. `statusLabel` returns `undefined` for a code we // have no words for, and this omits the status entirely in that case rather than falling back to // the raw value - the fallback IS the defect. `utils/fhirStatusVocabulary.ts` const statusText = statusLabel(observation.status); const status = statusText ? ` ยท ${statusText}` : ''; let dateSuffix = ''; if (observation.effectiveDateTime) { dateSuffix = ` (${formatDateTimeForDisplay(observation.effectiveDateTime)})`; } else if (observation.effectiveInstant) { dateSuffix = ` (${formatDateTimeForDisplay(observation.effectiveInstant)})`; } else if (observation.issued) { dateSuffix = ` (${formatDateTimeForDisplay(observation.issued)})`; } return `${observationName}${status}${dateSuffix}`; }; return { observationsBundle, // WHO THE CACHED LIST IS FOR, so a consumer can ask whether it belongs to the patient on // screen rather than trusting an app-wide context with no patient in its name. cachedForPatientId: cacheOwner.ownerId, observationsList, setObservationsList, shouldReload, setShouldReload, lastUpserted, setLastUpserted, unsaved, setUnsaved, fetchObservations, fetchObservationsWithPolling, fetchObservation, fetchObservationHistory, createObservationOnce, fetchObservationOperation, finishObservationOperation, recordSelfReport, editSelfReport, updateObservation, deleteObservation, archiveObservation, transformToFhirObservation, transformFromFhirObservation, getObservationDisplay, // US Core Observation profiles usCoreAverageBloodPressureProfile, usCoreCareExperiencePreferenceProfile, usCoreObservationAdiDocumentationProfile, usCoreObservationClinicalResultProfile, usCoreObservationLabProfile, usCoreObservationOccupationProfile, usCoreObservationPregnancyintentProfile, usCoreObservationPregnancystatusProfile, usCoreObservationScreeningAssessmentProfile, usCoreObservationSexualOrientationProfile, usCoreSimpleObservationProfile, usCoreSmokingstatusProfile, usCoreTreatmentInterventionPreferenceProfile, // Vital Signs usCoreVitalSignsProfile, usCoreBloodPressureProfile, usCorePediatricMmiForAgeProfile, pediatricWeightForHeightProfile, usCoreBmiProfile, usCoreBodyHeightProfile, usCoreBodyTemperatureProfile, usCoreBodyWeightProfile, usCoreHeadCircumferenceProfile, usCoreHeartRateProfile, usCorePulseOximetryProfile, usCoreRespiratoryRateProfile, headOccipitalFrontalCircumferencePercentileProfile }; }; const ObservationServiceContext = createContext | undefined>(undefined); export const ObservationServiceProvider: React.FC<{ children: ReactNode }> = ({ children }) => { const observationService = useObservationService(); return ( {children} ); }; /** Absent outside its provider, for readers that must never take a surface down with them. */ export const useOptionalObservationServiceContext = () => useContext(ObservationServiceContext); export const useObservationServiceContext = () => { const context = useContext(ObservationServiceContext); if (context === undefined) { throw new Error('useObservationServiceContext must be used within a ObservationServiceProvider'); } return context; };