import { uniq } from 'lodash-es'; import { type PatientData, type ObsRecord, type ConceptUuid, type ObsUuid, type ObsMetaInfo, } from '@openmrs/esm-patient-common-lib'; import { addUserDataToCache, assessValue, extractMetaInformation, extractObservationReferenceRanges, extractObservationInterpretation, getEntryConceptClassUuid, getUserDataFromCache, loadObsEntries, loadPresentConcepts, } from './helpers'; import { selectReferenceRange, formatReferenceRange, type ReferenceRanges, } from '../grouped-timeline/reference-range-helpers'; function parseSingleObsData( testConceptNameMap: Record, memberRefs: Record, metaInfomation: Record, ) { return (entry: ObsRecord) => { entry.conceptClass = getEntryConceptClassUuid(entry); // Extract observation-level reference ranges from FHIR Observation referenceRange field const observationRanges = extractObservationReferenceRanges(entry); // Extract observation-level interpretation from FHIR Observation interpretation field const observationInterpretation = extractObservationInterpretation(entry); if (entry.hasMember) { // is a panel entry.members = new Array(entry.hasMember.length); entry.hasMember.forEach((memb, i) => { memberRefs[memb.reference.split('/')[1]] = [entry.members, i]; }); } else { // is a single test // Extract value FIRST before computing interpretation if (entry.valueQuantity) { entry.value = String(entry.valueQuantity.value); delete entry.valueQuantity; } else if (entry.valueCodeableConcept) { entry.value = entry.valueCodeableConcept.coding?.[0]?.display; delete entry.valueCodeableConcept; } else if (entry.valueString) { entry.value = entry.valueString; delete entry.valueString; } const conceptMeta = metaInfomation[entry.conceptClass]; // Node-level (concept-level) reference ranges const nodeRanges: ReferenceRanges = { hiAbsolute: conceptMeta.hiAbsolute, hiCritical: conceptMeta.hiCritical, hiNormal: conceptMeta.hiNormal, lowAbsolute: conceptMeta.lowAbsolute, lowCritical: conceptMeta.lowCritical, lowNormal: conceptMeta.lowNormal, units: conceptMeta.units, }; // Merge observation-level and concept-level ranges (observation takes precedence) const selectedRanges = selectReferenceRange(observationRanges, nodeRanges); // Create merged meta with observation-level ranges taking precedence const mergedMeta: ObsMetaInfo = { ...conceptMeta, // Update meta with merged ranges hiAbsolute: selectedRanges?.hiAbsolute ?? conceptMeta.hiAbsolute, hiCritical: selectedRanges?.hiCritical ?? conceptMeta.hiCritical, hiNormal: selectedRanges?.hiNormal ?? conceptMeta.hiNormal, lowAbsolute: selectedRanges?.lowAbsolute ?? conceptMeta.lowAbsolute, lowCritical: selectedRanges?.lowCritical ?? conceptMeta.lowCritical, lowNormal: selectedRanges?.lowNormal ?? conceptMeta.lowNormal, units: selectedRanges?.units ?? conceptMeta.units, // Update range string with merged ranges range: selectedRanges ? formatReferenceRange(selectedRanges, selectedRanges.units) : conceptMeta.range, }; // Always update assessValue to use merged ranges (computed after mergedMeta to avoid unsafe cast) // This ensures assessValue is computed even when only concept-level ranges exist mergedMeta.assessValue = assessValue(mergedMeta); entry.meta = mergedMeta; // Use observation-level interpretation if available, otherwise compute using merged ranges entry.interpretation = observationInterpretation ?? (mergedMeta.assessValue ? mergedMeta.assessValue(entry.value) : 'NORMAL'); } entry.name = testConceptNameMap[entry.conceptClass]; }; } async function reloadData(patientUuid: string) { const entries = await loadObsEntries(patientUuid); const allConcepts = await loadPresentConcepts(entries); const testConcepts = allConcepts.filter((x) => x.conceptClass.name === 'Test' || x.conceptClass.name === 'LabSet'); const testConceptUuids: ConceptUuid[] = testConcepts.map((x) => x.uuid); const testConceptNameMap: Record = Object.fromEntries( testConcepts.map(({ uuid, display }) => [uuid, display]), ); const obsByClass: Record = Object.fromEntries(testConceptUuids.map((x) => [x, []])); const metaInfomation = extractMetaInformation(testConcepts); // obs that are not panels const singleEntries: ObsRecord[] = []; // a record of observation uuids that are members of panels, mapped to the place where to put them const memberRefs: Record = {}; const parseEntry = parseSingleObsData(testConceptNameMap, memberRefs, metaInfomation); entries.forEach((entry) => { // remove non test entries (due to unclean FHIR reponse) if (!testConceptUuids.includes(getEntryConceptClassUuid(entry))) { return; } parseEntry(entry); if (entry.members) { obsByClass[entry.conceptClass].push(entry); } else { singleEntries.push(entry); } }); singleEntries.forEach((entry) => { const { id } = entry; const memRef = memberRefs[id]; if (memRef) { memRef[0][memRef[1]] = entry; } if (obsByClass[entry.conceptClass]) { obsByClass[entry.conceptClass].push(entry); } }); // At this point all panels have their members as coming from the backend (i.e. the `hasMembers` field). // The panels should display *all* data though, i.e. also the test results that are not listed on `hasMembers`, // but share the same concept class as another existing member. // -> Go through each panel and add those single entries that are not yet present in the panel. Object.values(obsByClass) .filter((observations) => observations.some((obs) => obs.members)) .forEach((observations) => { const allSingleMembers = observations.flatMap((obs) => obs.members); const allMemberConcepts = uniq(allSingleMembers.map((member) => member.conceptClass)); for (const concept of allMemberConcepts) { const missingEntries = singleEntries.filter( (x) => x.conceptClass === concept && !allSingleMembers.some((member) => member.id === x.id), ); for (const missingEntry of missingEntries) { observations.push({ ...missingEntry, members: [missingEntry], }); } } }); const sortedObs: PatientData = Object.fromEntries( Object.entries(obsByClass) // remove concepts that did not have any observations .filter((x) => x[1].length) // replace the uuid key with the display name and sort the observations by date .map(([uuid, val]) => { const { display, conceptClass: { display: type }, } = testConcepts.find((x) => x.uuid === uuid); return [ display, { entries: val.sort((ent1, ent2) => Date.parse(ent2.effectiveDateTime) - Date.parse(ent1.effectiveDateTime)), type, uuid, }, ]; }), ); if (entries.length > 0) { addUserDataToCache(patientUuid, sortedObs, entries[0].id); } return sortedObs; } function loadPatientData(patientUuid: string): [PatientData | undefined, Promise] { const [cachedPatientData, shouldReload] = getUserDataFromCache(patientUuid); return [cachedPatientData, shouldReload.then((reload) => (reload ? reloadData(patientUuid) : cachedPatientData))]; } export default loadPatientData;