From c4ab29bdd69adfbd17d1e61644e666815c60a6a8 Mon Sep 17 00:00:00 2001 From: angelathe Date: Tue, 24 Mar 2026 11:07:40 -0400 Subject: [PATCH 01/14] first pass, index fhir resources by type & id, use index on labs --- .../src/app/utils/evaluate/fhir-paths.ts | 20 +- .../src/app/utils/evaluate/index.ts | 24 ++ .../EcrDocument/accordion-items.tsx | 48 +++- .../app/view-data/components/EcrSummary.tsx | 238 +++++++++--------- .../ecr-viewer/src/app/view-data/page.tsx | 125 +++++---- .../view-data/services/ecrSummaryService.tsx | 50 ++-- .../services/fhirResourcesIndexService.tsx | 58 +++++ .../app/view-data/services/labsService.tsx | 168 ++++++++++--- 8 files changed, 486 insertions(+), 245 deletions(-) create mode 100644 containers/ecr-viewer/src/app/view-data/services/fhirResourcesIndexService.tsx diff --git a/containers/ecr-viewer/src/app/utils/evaluate/fhir-paths.ts b/containers/ecr-viewer/src/app/utils/evaluate/fhir-paths.ts index 0997c326f..0fade9171 100644 --- a/containers/ecr-viewer/src/app/utils/evaluate/fhir-paths.ts +++ b/containers/ecr-viewer/src/app/utils/evaluate/fhir-paths.ts @@ -148,9 +148,7 @@ export type PathTypes = { procedureSpecimen: CodeableConcept; procedureMethod: CodeableConcept; procedurePriority: CodeableConcept; - diagnosticReports: DiagnosticReport; diagnosticReportStatus: string; - observations: Observation; labResultDiv: string; specimenCollectionTime: TimeX; specimenReceivedTime: TimeX; @@ -164,7 +162,6 @@ export type PathTypes = { observationResultStatus: string; labReportInterpretation: ValueX; observationInterpretation: Coding; - organizations: Organization; organizationType: ValueX; patientTravelHistory: Observation; travelHistoryLocation: string; @@ -628,21 +625,13 @@ const _fhirPathMappings: { [K in FhirPathKeys]: Omit, "name"> } = { }, // === Lab Info === - diagnosticReports: { - type: "DiagnosticReport", - path: "entry.resource.DiagnosticReport", - }, diagnosticReportStatus: { type: "string", path: "iif(extension('http://terminology.hl7.org/CodeSystem/v2-0123').valueCodeableConcept.coding.display.exists(), extension('http://terminology.hl7.org/CodeSystem/v2-0123').valueCodeableConcept.coding.display, status)", }, - observations: { - type: "Observation", - path: "entry.resource.Observation", - }, labResultDiv: { type: "string", - path: "entry.resource.section.where(code.coding.exists(system = 'http://loinc.org' and code = '30954-2')).text.`div`", + path: "section.where(code.coding.exists(system = 'http://loinc.org' and code = '30954-2')).text.`div`", }, specimenCollectionTime: { type: "TimeX", @@ -668,6 +657,7 @@ const _fhirPathMappings: { [K in FhirPathKeys]: Omit, "name"> } = { type: "string", path: "(valueQuantity.value.toString() | valueString | valueCodeableConcept.coding.display | iif(valueQuantity.unit.exists(), iif(valueQuantity.unit = '%', valueQuantity.unit, ' ' + valueQuantity.unit), '') | iif(interpretation.coding.display.exists(), ' (' + interpretation.coding.display + ')', '')).join('')", }, + // TODO: Generalize observationReferenceRange: { type: "ObservationReferenceRange", path: "referenceRange", @@ -695,10 +685,6 @@ const _fhirPathMappings: { [K in FhirPathKeys]: Omit, "name"> } = { }, // Organization - organizations: { - type: "Organization", - path: "entry.resource.Organization", - }, organizationType: { type: "ValueX", path: "type.coding.display", @@ -743,7 +729,7 @@ const _fhirPathMappings: { [K in FhirPathKeys]: Omit, "name"> } = { // Stamped stampedImmunizations: { type: "Immunization", - path: "entry.resource.where(extension('https://reportstream.cdc.gov/fhir/StructureDefinition/condition-code').valueCoding.code = %snomedCode and resourceType = 'Immunization')", + path: "Immunization.where(extension('https://reportstream.cdc.gov/fhir/StructureDefinition/condition-code').valueCoding.code = %snomedCode)", }, // Generic diff --git a/containers/ecr-viewer/src/app/utils/evaluate/index.ts b/containers/ecr-viewer/src/app/utils/evaluate/index.ts index 7c05cead3..a0ea083d0 100644 --- a/containers/ecr-viewer/src/app/utils/evaluate/index.ts +++ b/containers/ecr-viewer/src/app/utils/evaluate/index.ts @@ -30,6 +30,7 @@ import { import { notEmpty } from "@/app/utils/data-utils"; import fhirPathMappings, { PathTypes, ValueX, FhirPath } from "./fhir-paths"; +import { FhirIndex } from "@/app/view-data/services/fhirResourcesIndexService"; // TODO: Follow up on FHIR/fhirpath typing // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -341,6 +342,29 @@ export const evaluateReference = ( return result; }; +export const evaluateReference2 = ( + // fhirData: FhirData, + fhirIndex: FhirIndex, + ref?: string | Reference +): T | undefined => { + if (typeof ref !== "string") { + ref = formatReference(ref); + } + if (!ref) return undefined; + + const [resourceType, id] = ref.split("/"); + const result = fhirIndex.fhirResourcesById[id]; + // TODO: Add type checking + + if (result && result?.resourceType !== resourceType) { + console.error( + `Resource type mismatch: Expected ${resourceType}, but got ${result?.resourceType}` + ); + } + + return result as T; +}; + type RefPathTypes = { [K in keyof PathTypes]: PathTypes[K] extends string | { reference?: string } ? K diff --git a/containers/ecr-viewer/src/app/view-data/components/EcrDocument/accordion-items.tsx b/containers/ecr-viewer/src/app/view-data/components/EcrDocument/accordion-items.tsx index 16f13cfe2..a19d22f99 100644 --- a/containers/ecr-viewer/src/app/view-data/components/EcrDocument/accordion-items.tsx +++ b/containers/ecr-viewer/src/app/view-data/components/EcrDocument/accordion-items.tsx @@ -1,6 +1,6 @@ import React from "react"; -import { Bundle } from "fhir/r4"; +import { Bundle, DiagnosticReport } from "fhir/r4"; import { AccordionItem } from "@/app/types"; import { evaluateAll } from "@/app/utils/evaluate"; @@ -25,6 +25,7 @@ import { evaluatePregnancyData, } from "@/app/view-data/services/evaluateFhirDataService"; import { evaluateLabInfoData } from "@/app/view-data/services/labsService"; +import { FhirIndex, getResourcesByType } from "@/app/view-data/services/fhirResourcesIndexService"; import { evaluateClinicalData } from "./clinical-data"; @@ -35,20 +36,51 @@ import { evaluateClinicalData } from "./clinical-data"; */ export const getEcrDocumentAccordionItems = ( fhirBundle: Bundle, + fhirIndex: FhirIndex ): AccordionItem[] => { + const t0 = performance.now(); const demographicsData = evaluateDemographicsData(fhirBundle); + const t11 = performance.now(); + console.log("Time to run evaluateDemographics", t11 - t0); const socialData = evaluateSocialData(fhirBundle); + const t12 = performance.now(); + console.log("Time to run evaluateSocialData", t12 - t0); const pregnancyData = evaluatePregnancyData(fhirBundle); + const t13 = performance.now(); + console.log("Time to run evaluatePregnancyData", t13 - t0); const hospitalEncounterData = evaluateHospitalEncounterData(fhirBundle); + const t14 = performance.now(); + console.log("Time to run evaluateHospitalEncounterData", t14 - t0); const encounterData = evaluateEncounterData(fhirBundle); + const t15 = performance.now(); + console.log("Time to run evaluateEncounterData", t15 - t0); const providerData = evaluateProviderData(fhirBundle); + const t16 = performance.now(); + console.log("Time to run evaluateProviderData", t16 - t0); const clinicalData = evaluateClinicalData(fhirBundle); + const t17 = performance.now(); + console.log("Time to run evaluateClinicalData", t17 - t0); const ecrMetadata = evaluateEcrMetadata(fhirBundle); + const t18 = performance.now(); + console.log("Time to run evaluateEcrMetadata", t18 - t0); const facilityData = evaluateFacilityData(fhirBundle); + const t19 = performance.now(); + console.log("Time to run evaluateFacilityData", t19 - t0); + const lab0 = performance.now(); + + const diagnosticReports = getResourcesByType(fhirIndex, 'DiagnosticReport') + const lab1 = performance.now(); + console.log("Time to evaluate diagnosticReports", lab1 - lab0); const labInfoData = evaluateLabInfoData( fhirBundle, - evaluateAll(fhirBundle, fhirPathMappings.diagnosticReports), + diagnosticReports, + fhirIndex ); + const t20 = performance.now(); + console.log("Time to run evaluateLabInfoData", t20 - t0); + + const t1 = performance.now(); + console.log("Time to evaluate all : ", t1 - t0); const hasUnavailableData = () => { const unavailableDataArrays = [ @@ -70,9 +102,11 @@ export const getEcrDocumentAccordionItems = ( ecrMetadata.eicrAuthorDetails.map((details) => details.unavailableData), ]; return unavailableDataArrays.some( - (array) => Array.isArray(array) && array.length > 0, + (array) => Array.isArray(array) && array.length > 0 ); }; + + const t2 = performance.now(); const accordionItems: AccordionItem[] = [ { title: "Patient Info", @@ -123,7 +157,7 @@ export const getEcrDocumentAccordionItems = ( { title: "Clinical Info", content: Object.values(clinicalData).some( - (section) => section.availableData.length > 0, + (section) => section.availableData.length > 0 ) ? ( 0 || ecrMetadata.eicrAuthorDetails.find( - (authorDetails) => authorDetails.availableData.length > 0, + (authorDetails) => authorDetails.availableData.length > 0 ) || ecrMetadata.ecrCustodianDetails.availableData.length > 0 ? ( authorDetails.unavailableData, + (authorDetails) => authorDetails.unavailableData )} /> ) : ( @@ -246,6 +280,8 @@ export const getEcrDocumentAccordionItems = ( headingLevel: "h3", }; }); + const t3 = performance.now(); + console.log("Time to build accordion components", t3 - t2); return accordionItems; }; diff --git a/containers/ecr-viewer/src/app/view-data/components/EcrSummary.tsx b/containers/ecr-viewer/src/app/view-data/components/EcrSummary.tsx index f0426426a..09370de47 100644 --- a/containers/ecr-viewer/src/app/view-data/components/EcrSummary.tsx +++ b/containers/ecr-viewer/src/app/view-data/components/EcrSummary.tsx @@ -6,11 +6,13 @@ import { AccordionItem } from "@/app/types"; import { toKebabCase } from "@/app/utils/format-utils"; import { DataDisplay, DataTableDisplay, DisplayDataProps } from "./DataDisplay"; +import { evaluateEcrSummaryConditionSummary, evaluateEcrSummaryEncounterDetails, evaluateEcrSummaryPatientDetails } from "@/app/view-data/services/ecrSummaryService"; +import { Bundle } from "fhir/r4"; +import { FhirIndex } from "@/app/view-data/services/fhirResourcesIndexService"; interface EcrSummaryProps { - patientDetails: DisplayDataProps[]; - encounterDetails: DisplayDataProps[]; - conditionSummary: ConditionSummary[]; + fhirBundle: Bundle; + fhirIndex: FhirIndex; snomed?: string; } @@ -33,125 +35,135 @@ export interface ConditionSummary { * @returns a react element for eCR Summary */ const EcrSummary: React.FC = ({ - patientDetails, - encounterDetails, - conditionSummary, - snomed, + fhirBundle, + fhirIndex, + snomed }) => { - const conditionSummaryAccordionItems: AccordionItem[] = conditionSummary.map( - (condition) => { - const hasImmunizationDetails = condition.immunizationDetails.length > 0; - const hasClinicalDetails = condition.clinicalDetails.length > 0; - return { - title: condition.title, - id: toKebabCase(condition.title), - headingLevel: "h4", - className: "side-nav-ignore border-1px border-accent-cool-darker", - expanded: snomed === condition.snomed || conditionSummary.length === 1, - content: ( - <> - {condition.conditionDetails.map((item, i) => ( - - ))} - {(hasImmunizationDetails || hasClinicalDetails) && ( - <> -
- Clinical Sections Relevant to Reportable Condition -
- {hasImmunizationDetails && ( + const t0 = performance.now(); + const patientDetails = evaluateEcrSummaryPatientDetails(fhirBundle).availableData; + const encounterDetails = evaluateEcrSummaryEncounterDetails(fhirBundle).availableData; + const conditionSummary = evaluateEcrSummaryConditionSummary(fhirBundle, fhirIndex, snomed); + const t1 = performance.now(); + console.log("Evaluate eCR summary: ", t1 - t0); + try { + const conditionSummaryAccordionItems: AccordionItem[] = conditionSummary.map( + (condition) => { + const hasImmunizationDetails = condition.immunizationDetails.length > 0; + const hasClinicalDetails = condition.clinicalDetails.length > 0; + return { + title: condition.title, + id: toKebabCase(condition.title), + headingLevel: "h4", + className: "side-nav-ignore border-1px border-accent-cool-darker", + expanded: snomed === condition.snomed || conditionSummary.length === 1, + content: ( + <> + {condition.conditionDetails.map((item, i) => ( + + ))} + {(hasImmunizationDetails || hasClinicalDetails) && ( + <> +
+ Clinical Sections Relevant to Reportable Condition +
+ {hasImmunizationDetails && ( +
+ {condition.immunizationDetails.map((item, i) => ( + + ))} +
+ )} + {hasClinicalDetails && ( +
+ {condition.clinicalDetails.map((item, i) => ( + + ))} +
+ )} + + )} + {condition.labDetails.length > 0 && ( + <> +
+ Lab Results Relevant to Reportable Condition +
- {condition.immunizationDetails.map((item, i) => ( - + {condition.labDetails.map((item, index) => ( + ))}
- )} - {hasClinicalDetails && ( -
- {condition.clinicalDetails.map((item, i) => ( - - ))} -
- )} - - )} - {condition.labDetails.length > 0 && ( - <> -
- Lab Results Relevant to Reportable Condition -
-
- {condition.labDetails.map((item, index) => ( - - ))} -
- - )} - - ), - }; - }, - ); - return ( -
-
-

- Patient Summary -

-
- {patientDetails.map((item, i) => ( - - ))} + + )} + + ), + }; + }, + ); + return ( +
+
+

+ Patient Summary +

+
+ {patientDetails.map((item, i) => ( + + ))} +
-
-
-

- Encounter Summary -

-
- {encounterDetails.map((item, i) => ( - - ))} +
+

+ Encounter Summary +

+
+ {encounterDetails.map((item, i) => ( + + ))} +
-
-
-

-
Condition Summary
-
- - {numConditionsText(conditionSummaryAccordionItems.length)} - +
+

+
Condition Summary
+
+ + {numConditionsText(conditionSummaryAccordionItems.length)} + +
+

+
+
-

-
-
-
- ); + ); + } finally { + const t2 = performance.now() + console.log("EcrSummary: ", t2 - t0); + } }; /** diff --git a/containers/ecr-viewer/src/app/view-data/page.tsx b/containers/ecr-viewer/src/app/view-data/page.tsx index 2cb58986f..453cec36d 100644 --- a/containers/ecr-viewer/src/app/view-data/page.tsx +++ b/containers/ecr-viewer/src/app/view-data/page.tsx @@ -11,16 +11,12 @@ import EcrDocument from "./components/EcrDocument"; import { getEcrDocumentAccordionItems } from "./components/EcrDocument/accordion-items"; import EcrSummary from "./components/EcrSummary"; import SideNav from "./components/SideNav"; -import { - evaluateEcrSummaryConditionSummary, - evaluateEcrSummaryEncounterDetails, - evaluateEcrSummaryPatientDetails, -} from "./services/ecrSummaryService"; import { evaluatePatientDOB, evaluatePatientName, } from "./services/evaluateFhirDataService"; import { getFhirData, isSuccessResponse } from "./services/fhirDataService"; +import { getFhirIndex } from "@/app/view-data/services/fhirResourcesIndexService"; /** * Functional component for rendering the eCR Viewer page. @@ -33,67 +29,70 @@ const ECRViewerPage = async ({ }: { searchParams: Promise<{ id?: string; "snomed-code"?: string }>; }) => { - const _searchParams = await searchParams; - const fhirId = _searchParams.id ?? ""; - const snomedCode = _searchParams["snomed-code"] ?? ""; - - const user = await getLoggedInUserSession(); - // If we have a user that means we're using IDP auth and not NBS Auth, so we - // need to check if they're authorized to view this eCR - if (user) { - const authed = await isLoggedInUserEcrAuthed(fhirId); - if (!authed) notFound(); - } - - const resp = await getFhirData({ eicr_id: fhirId }); - if (!isSuccessResponse(resp)) { - if (resp.status === 404) { - return ; - } else { - return ( - -
-            {`${resp.status}: ${resp.payload.message}`}
-          
-
- ); + const t0 = performance.now() + try { + const _searchParams = await searchParams; + const fhirId = _searchParams.id ?? ""; + const snomedCode = _searchParams["snomed-code"] ?? ""; + + const user = await getLoggedInUserSession(); + // If we have a user that means we're using IDP auth and not NBS Auth, so we + // need to check if they're authorized to view this eCR + if (user) { + const authed = await isLoggedInUserEcrAuthed(fhirId); + if (!authed) notFound(); } - } - - const fhirBundle = resp.payload.fhirBundle; - const patientName = evaluatePatientName(fhirBundle, true); - const patientDOB = evaluatePatientDOB(fhirBundle); - - const accordionItems = getEcrDocumentAccordionItems(fhirBundle); - return ( - - -
-
-

- eCR Summary -

-
- Provides key info upfront to help you understand the eCR at a glance + + const resp = await getFhirData({ eicr_id: fhirId }); + if (!isSuccessResponse(resp)) { + if (resp.status === 404) { + return ; + } else { + return ( + +
+              {`${resp.status}: ${resp.payload.message}`}
+            
+
+ ); + } + } + + const fhirBundle = resp.payload.fhirBundle; + const fhirIndex = getFhirIndex(fhirBundle); + const patientName = evaluatePatientName(fhirBundle, true); + const patientDOB = evaluatePatientDOB(fhirBundle); + + const t2 = performance.now() + const accordionItems = getEcrDocumentAccordionItems(fhirBundle, fhirIndex); + const t3 = performance.now() + console.log("Time to run getEcrDocumentAccordionItems: ", t3 - t2 ); + return ( + + +
+
+

+ eCR Summary +

+
+ Provides key info upfront to help you understand the eCR at a + glance +
+ +
- - -
- - ); + + ); + } finally { + const t1 = performance.now() + console.log("Time to run EcrViewerPage: ", t1 - t0 ); + } }; export default ECRViewerPage; diff --git a/containers/ecr-viewer/src/app/view-data/services/ecrSummaryService.tsx b/containers/ecr-viewer/src/app/view-data/services/ecrSummaryService.tsx index 38b157836..712bff1f4 100644 --- a/containers/ecr-viewer/src/app/view-data/services/ecrSummaryService.tsx +++ b/containers/ecr-viewer/src/app/view-data/services/ecrSummaryService.tsx @@ -3,8 +3,10 @@ import React from "react"; import { Bundle, Condition, + DiagnosticReport, DomainResource, Encounter, + Immunization, Observation, Organization, } from "fhir/r4"; @@ -48,6 +50,7 @@ import { } from "./evaluateFhirDataService"; import { evaluateLabInfoData } from "./labsService"; import { getReportabilityRulesReasons } from "./reportabilityService"; +import { FhirIndex, getResourcesByType } from "./fhirResourcesIndexService"; /** * Evaluates and retrieves patient details from the FHIR bundle using the provided path mappings. @@ -158,7 +161,8 @@ export const evaluateEcrSummaryEncounterDetails = (fhirBundle: Bundle) => { */ export const evaluateEcrSummaryConditionSummary = ( fhirBundle: Bundle, - snomedCode?: string, + fhirIndex: FhirIndex, + snomedCode?: string ): ConditionSummary[] => { const rrConditions = evaluateAll(fhirBundle, fhirPathMappings.rrConditions); const conditionsList: { @@ -166,7 +170,7 @@ export const evaluateEcrSummaryConditionSummary = ( } = {}; for (const observation of rrConditions) { const coding = observation?.valueCodeableConcept?.coding?.find( - (coding) => coding.system === "http://snomed.info/sct", + (coding) => coding.system === "http://snomed.info/sct" ); const displayText = @@ -185,12 +189,12 @@ export const evaluateEcrSummaryConditionSummary = ( observation?.hasMember?.forEach((ref) => { const rrInfoObs: Observation | undefined = evaluateReference( fhirBundle, - ref.reference, + ref.reference ); const { rules } = getReportabilityRulesReasons(rrInfoObs); rules.forEach((rule: string) => - conditionsList[conditionListKey].ruleSummaries.add(rule), + conditionsList[conditionListKey].ruleSummaries.add(rule) ); }); } @@ -210,7 +214,7 @@ export const evaluateEcrSummaryConditionSummary = ( {[...conditionsList[conditionsListKey].ruleSummaries].map( (summary) => (

{summary}

- ), + ) )}
), @@ -222,12 +226,13 @@ export const evaluateEcrSummaryConditionSummary = ( ), clinicalDetails: evaluateEcrSummaryRelevantClinicalDetails( fhirBundle, - conditionsListKey, + conditionsListKey ), labDetails: evaluateEcrSummaryRelevantLabResults( fhirBundle, + fhirIndex, conditionsListKey, - false, + false ), }; @@ -293,8 +298,9 @@ export const evaluateEcrSummaryRelevantClinicalDetails = ( */ export const evaluateEcrSummaryRelevantLabResults = ( fhirBundle: Bundle, + fhirIndex: FhirIndex, snomedCode: string, - lastDividerLine: boolean = true, + lastDividerLine: boolean = true ): DisplayDataProps[] => { let resultsArray: DisplayDataProps[] = []; @@ -302,19 +308,26 @@ export const evaluateEcrSummaryRelevantLabResults = ( return []; } - const labReports = evaluateAll( - fhirBundle, - fhirPathMappings.diagnosticReports, - ); + // const labReports = evaluateAll( + // fhirBundle, + // fhirPathMappings.diagnosticReports, + // ); + + const labReports = getResourcesByType(fhirIndex, 'DiagnosticReport'); const labsWithCode = getRelevantResources(labReports, snomedCode); const labsWithCodeIds = new Set(labsWithCode.map((lab) => lab.id)); - const observationsList = evaluateAll( - fhirBundle, - fhirPathMappings.observations, + // const observationsList = evaluateAll( + // fhirBundle, + // fhirPathMappings.observations, + // ); + const observationsList = getResourcesByType( + fhirIndex, + "Observation" ); + // console.log(observationsList); const relevantObsIds = new Set( - getRelevantResources(observationsList, snomedCode).map((entry) => entry.id), + getRelevantResources(observationsList, snomedCode).map((entry) => entry.id) ); const labsFromObsWithCode = labReports.filter((lab) => { @@ -339,14 +352,15 @@ export const evaluateEcrSummaryRelevantLabResults = ( const relevantLabElements = evaluateLabInfoData( fhirBundle, relevantLabs, - "h4", + fhirIndex, + "h4" ); resultsArray = relevantLabElements.flatMap((element) => element.diagnosticReportDataItems.map((reportItem) => ({ value: , dividerLine: false, - })), + })) ); if (lastDividerLine) { diff --git a/containers/ecr-viewer/src/app/view-data/services/fhirResourcesIndexService.tsx b/containers/ecr-viewer/src/app/view-data/services/fhirResourcesIndexService.tsx new file mode 100644 index 000000000..82b9829a1 --- /dev/null +++ b/containers/ecr-viewer/src/app/view-data/services/fhirResourcesIndexService.tsx @@ -0,0 +1,58 @@ +import { Bundle, Resource } from "fhir/r4"; + +export type ResourceType = Resource["resourceType"]; +export type FhirResourceByTypeIndex = { + // This says: If the key is 'Patient', the value is Record + [K in ResourceType]?: Record>; +}; + +export interface FhirResourceByIdIndex { + [id: string]: Resource +} + +export interface FhirIndex { + fhirResourcesByType: FhirResourceByTypeIndex; + fhirResourcesById: FhirResourceByIdIndex; +} + +/** + * Extracts all lab `Observation` resources from a given FHIR bundle across all diagnostic reports. + * @param fhirBundle - The FHIR bundle containing related resources for the lab report. + * @returns An object of `Observation` resources with the observation `id` (string) as the key. + * If no matching observations are found, an empty object is returned. + */ +export const getFhirIndex = (fhirBundle: Bundle): FhirIndex => { + const fhirResourcesByType: FhirResourceByTypeIndex = {}; + const fhirResourcesById: FhirResourceByIdIndex = {}; + + fhirBundle.entry?.forEach((entry) => { + const resource = entry.resource; + const resourceType = resource?.resourceType; + const resourceId = resource?.id; + + if (resourceType && resourceId) { + fhirResourcesById[resourceId] = resource; + + fhirResourcesByType[resourceType] ??= {}; + fhirResourcesByType[resourceType][resourceId] = resource; + } + }); + + return { fhirResourcesByType, fhirResourcesById }; +}; + +/** + * Grabs all resources of a specific type. + */ +export function getResourcesByType( + fhirIndex: FhirIndex, + type: T["resourceType"] +): T[] { + const resourceMap = fhirIndex.fhirResourcesByType[type]; + + if (!resourceMap) return []; + + // We cast to 'any' internally to satisfy the complex mapped type, + // but the external return type is perfectly narrowed to T[] + return Object.values(resourceMap ?? {}) as T[]; +} \ No newline at end of file diff --git a/containers/ecr-viewer/src/app/view-data/services/labsService.tsx b/containers/ecr-viewer/src/app/view-data/services/labsService.tsx index bc3ee5d40..9ae5bdcf0 100644 --- a/containers/ecr-viewer/src/app/view-data/services/labsService.tsx +++ b/containers/ecr-viewer/src/app/view-data/services/labsService.tsx @@ -4,6 +4,7 @@ import React, { ReactNode } from "react"; import { HeadingLevel, Tag } from "@trussworks/react-uswds"; import { Bundle, + Composition, Device, DiagnosticReport, Observation, @@ -36,6 +37,7 @@ import { evaluateAll, evaluateOne, evaluateReference, + evaluateReference2, evaluateValue, } from "@/app/utils/evaluate"; import fhirPathMappings from "@/app/utils/evaluate/fhir-paths"; @@ -53,6 +55,7 @@ import EvaluateTable, { } from "@/app/view-data/components/EvaluateTable"; import { FieldValue } from "@/app/view-data/components/FieldValue"; import { sortResourcesByDate } from "@/app/view-data/utils/fhir-data-utils"; +import { FhirIndex, getResourcesByType } from "@/app/view-data/services/fhirResourcesIndexService"; export interface ResultObject { [key: string]: AccordionItem[]; @@ -83,23 +86,61 @@ const ABNORMAL_OBSERVATION_INTERPRETATIONS: Record = { export const evaluateLabInfoData = ( fhirBundle: Bundle, labReports: DiagnosticReport[], - accordionHeadingLevel: HeadingLevel = "h5", + fhirIndex: FhirIndex, + accordionHeadingLevel: HeadingLevel = "h5" ): LabReportElementData[] => { - // the keys are the organization id, the value is an array of jsx elements of diagnostic reports + const t0 = performance.now(); + let organizationItems: ResultObject = {}; - const jsonLabs = getAllLabJsonObjects(fhirBundle); + const jsonLabs = getAllLabJsonObjects(fhirBundle, fhirIndex); + + const tgetall0 = performance.now(); + console.log("Time to getAllLabJsonObjects", tgetall0 - t0); + // const labObs = fhirIndex.fhirResourcesByType["Observation"]; + + const tgetall1 = performance.now(); + console.log("Time to get all lab obs: ", tgetall1 - tgetall0); + // console.log(allObs); + + const totals = { + getObservations: 0, + getJsonLab: 0, + getLabsContent: 0, + evaluateValue: 0, + }; for (const report of labReports) { - const obs = getObservations(report, fhirBundle); + let t; + + t = performance.now(); + const obs = getObservations(report, fhirIndex); + totals.getObservations += performance.now() - t; + + t = performance.now(); const labReportJson = getJsonLab(jsonLabs, obs); + totals.getJsonLab += performance.now() - t; ensureReportHasDateTime(report, obs); - const content = getLabsContent(report, obs, fhirBundle, labReportJson); + + t = performance.now(); + const content = getLabsContent( + report, + obs, + fhirBundle, + fhirIndex, + labReportJson + ); const organizationId = (report.performer?.[0].reference ?? "").replace( "Organization/", - "", + "" ); + totals.getLabsContent += performance.now() - t; + const title = formatCodeableConcept(report.code) ?? "Unknown"; + + t = performance.now(); + totals.evaluateValue += performance.now() - t; + const item = { title: (
@@ -133,9 +174,58 @@ export const evaluateLabInfoData = ( ); } - return combineOrgAndReportData(organizationItems, fhirBundle); + const t1 = performance.now(); + + console.log("evaluateLabInfoData TOTAL:", (t1 - t0).toFixed(2), "ms"); + console.log("Breakdown:"); + Object.entries(totals) + .sort((a, b) => b[1] - a[1]) + .forEach(([k, v]) => { + console.log(` ${k}: ${v.toFixed(2)} ms`); + }); + + return combineOrgAndReportData(organizationItems, fhirBundle, fhirIndex); }; +// /** +// * Extracts all lab `Observation` resources from a given FHIR bundle across all diagnostic reports. +// * @param fhirBundle - The FHIR bundle containing related resources for the lab report. +// * @returns An object of `Observation` resources with the observation `id` (string) as the key. +// * If no matching observations are found, an empty object is returned. +// */ +// export const getAllObservationResources = ( +// fhirBundle: Bundle +// ): Record => { +// // init the object +// const labObs: Record = {}; + +// // Get lab Composition section +// // const labObsRefs = evaluateAll( +// // fhirBundle, +// // fhirPathMappings.labResultObservations +// // ); +// // console.log(labObsRefs); + +// // For each reference +// // labObsRefs.forEach((ref) => { +// // // TODO ANGELA: don't do reference lookups. just iterate through bundle once and record all observations +// // const ob = evaluateReference(fhirBundle, ref.reference); +// // if (ob && ob.id) { +// // labObs[ob.id] = ob +// // }; +// // }); + +// const obs = evaluateAll(fhirBundle, fhirPathMappings.observations); +// obs.forEach((ob) => { +// if (ob && ob.id) { +// labObs[ob.id] = ob +// }; +// }) + +// //return the object +// return labObs; +// }; + /** * Extracts an array of `Observation` resources from a given FHIR bundle based on a list of observation references. * @param report - The lab report containing the results to be processed. @@ -146,18 +236,23 @@ export const evaluateLabInfoData = ( */ export const getObservations = ( report: DiagnosticReport, - fhirBundle: Bundle, -): Array => { - return sortResourcesByDate( - (report.result || []) - .map((obsRef) => - evaluateReference(fhirBundle, obsRef.reference), - ) - .filter(notEmpty), - fhirPathMappings.effectiveX, - ); + fhirIndex: FhirIndex, +): Observation[] => { + const test = (report.result || []) + .map((obsRef) => { + if (obsRef.reference) { + const [_, id] = obsRef.reference.split("/"); + return fhirIndex.fhirResourcesById[id] as Observation; + } + }) + .filter(notEmpty); + try { + return sortResourcesByDate(test, fhirPathMappings.effectiveX); + } finally { + } }; + const ensureReportHasDateTime = ( report: DiagnosticReport, obs: Observation[], @@ -233,9 +328,15 @@ export const getJsonLab = ( * @param fhirBundle - The FHIR Bundle object containing relevant FHIR resources. * @returns The JSON representation of the lab report. */ -export const getAllLabJsonObjects = (fhirBundle: Bundle): HtmlTableJson[] => { +export const getAllLabJsonObjects = (fhirBundle: Bundle, fhirIndex: FhirIndex): HtmlTableJson[] => { // Get lab reports HTML String (for all lab reports) & convert to JSON - const labsString = evaluateValue(fhirBundle, fhirPathMappings.labResultDiv); + const compositionLabs = getResourcesByType(fhirIndex, 'Composition')[0]; + const labsString = evaluateValue( + compositionLabs, + fhirPathMappings.labResultDiv + ); + // const labsString = evaluateValue(fhirBundle, fhirPathMappings.labResultDiv); + // console.log(labsString); return formatTablesToJSON(labsString); }; @@ -440,6 +541,7 @@ export const returnAnalysisTime = ( export const evaluateDiagnosticReportData = ( obs: Observation[], fhirBundle: Bundle, + fhirIndex: FhirIndex, ): React.JSX.Element | undefined => { if (!obs.length) return undefined; @@ -463,7 +565,8 @@ export const evaluateDiagnosticReportData = ( columnName: "Test Method", infoPath: "observationDeviceReference", applyToValue: (ref) => { - const device = evaluateReference(fhirBundle, ref); + // TODO: Angela review evaluateReference2 + const device = evaluateReference2(fhirIndex, ref); return safeParse(device?.deviceName?.[0]?.name ?? ""); }, className: "minw-10 width-20", @@ -564,12 +667,14 @@ export const evaluateOrganismsReportData = ( export const combineOrgAndReportData = ( organizationItems: ResultObject, fhirBundle: Bundle, + fhirIndex: FhirIndex, ): LabReportElementData[] => { return Object.keys(organizationItems).map((key: string) => { const organizationId = key.replace("Organization/", ""); const orgData = evaluateLabOrganizationData( organizationId, fhirBundle, + fhirIndex, organizationItems[key].length, ); return { @@ -590,9 +695,10 @@ export const combineOrgAndReportData = ( export const evaluateLabOrganizationData = ( id: string, fhirBundle: Bundle, + fhirIndex: FhirIndex, labReportCount: number, ) => { - const orgMappings = evaluateAll(fhirBundle, fhirPathMappings.organizations); + const orgMappings = getResourcesByType(fhirIndex, 'Organization'); let matchingOrg: Organization = orgMappings.filter( (organization) => organization.id === id, )[0]; @@ -678,16 +784,22 @@ const groupItemByOrgId = ( * @param labReportJson - The JSON representation of the lab results from HTML. * @returns An array of JSX elements representing the lab report content. */ +// TODO ANGELA: Review const getLabsContent = ( report: DiagnosticReport, obs: Observation[], fhirBundle: Bundle, + fhirIndex: FhirIndex, labReportJson?: HtmlTableJson, ) => { - const labTableDiagnostic = evaluateDiagnosticReportData(obs, fhirBundle); - const labTableOrganisms = evaluateOrganismsReportData(obs); - const labSpecimen = evaluateReference( + const labTableDiagnostic = evaluateDiagnosticReportData( + obs, fhirBundle, + fhirIndex + ); + const labTableOrganisms = evaluateOrganismsReportData(obs); + const labSpecimen = evaluateReference2( + fhirIndex, report.specimen?.[0]?.reference, ); @@ -728,7 +840,7 @@ const getLabsContent = ( evaluateValue(labSpecimen, fhirPathMappings.specimenBodySite) || returnFieldValueFromLabHtmlString( labReportJson, - "Anatomical Location / Laterality", + "Anatomical Location / Laterality" ), className: "lab-text-content", }, @@ -736,7 +848,7 @@ const getLabsContent = ( title: "Collection Method/Volume", value: returnFieldValueFromLabHtmlString( labReportJson, - "Collection Method / Volume", + "Collection Method / Volume" ), className: "lab-text-content", }, @@ -744,7 +856,7 @@ const getLabsContent = ( title: "Resulting Agency Comment", value: returnFieldValueFromLabHtmlString( labReportJson, - "Resulting Agency Comment", + "Resulting Agency Comment" ), className: "lab-text-content", }, @@ -752,7 +864,7 @@ const getLabsContent = ( title: "Authorizing Provider", value: returnFieldValueFromLabHtmlString( labReportJson, - "Authorizing Provider", + "Authorizing Provider" ), className: "lab-text-content", }, From 35562dd2fed832cdf11036a7cc4d6361d1febbd5 Mon Sep 17 00:00:00 2001 From: angelathe Date: Tue, 24 Mar 2026 13:42:10 -0400 Subject: [PATCH 02/14] cleanup --- .../src/app/utils/evaluate/fhir-paths.ts | 3 +- .../src/app/utils/evaluate/index.ts | 4 +- .../EcrDocument/accordion-items.tsx | 42 +--- .../app/view-data/components/EcrSummary.tsx | 235 +++++++++--------- .../ecr-viewer/src/app/view-data/page.tsx | 14 +- .../view-data/services/ecrSummaryService.tsx | 16 +- .../app/view-data/services/labsService.tsx | 114 ++------- 7 files changed, 152 insertions(+), 276 deletions(-) diff --git a/containers/ecr-viewer/src/app/utils/evaluate/fhir-paths.ts b/containers/ecr-viewer/src/app/utils/evaluate/fhir-paths.ts index 0fade9171..ce384090d 100644 --- a/containers/ecr-viewer/src/app/utils/evaluate/fhir-paths.ts +++ b/containers/ecr-viewer/src/app/utils/evaluate/fhir-paths.ts @@ -657,7 +657,6 @@ const _fhirPathMappings: { [K in FhirPathKeys]: Omit, "name"> } = { type: "string", path: "(valueQuantity.value.toString() | valueString | valueCodeableConcept.coding.display | iif(valueQuantity.unit.exists(), iif(valueQuantity.unit = '%', valueQuantity.unit, ' ' + valueQuantity.unit), '') | iif(interpretation.coding.display.exists(), ' (' + interpretation.coding.display + ')', '')).join('')", }, - // TODO: Generalize observationReferenceRange: { type: "ObservationReferenceRange", path: "referenceRange", @@ -729,7 +728,7 @@ const _fhirPathMappings: { [K in FhirPathKeys]: Omit, "name"> } = { // Stamped stampedImmunizations: { type: "Immunization", - path: "Immunization.where(extension('https://reportstream.cdc.gov/fhir/StructureDefinition/condition-code').valueCoding.code = %snomedCode)", + path: "entry.resource.where(extension('https://reportstream.cdc.gov/fhir/StructureDefinition/condition-code').valueCoding.code = %snomedCode and resourceType = 'Immunization')", }, // Generic diff --git a/containers/ecr-viewer/src/app/utils/evaluate/index.ts b/containers/ecr-viewer/src/app/utils/evaluate/index.ts index a0ea083d0..8e20ebe96 100644 --- a/containers/ecr-viewer/src/app/utils/evaluate/index.ts +++ b/containers/ecr-viewer/src/app/utils/evaluate/index.ts @@ -342,8 +342,8 @@ export const evaluateReference = ( return result; }; +// TODO ANGELA: Add JSdoc export const evaluateReference2 = ( - // fhirData: FhirData, fhirIndex: FhirIndex, ref?: string | Reference ): T | undefined => { @@ -354,7 +354,7 @@ export const evaluateReference2 = ( const [resourceType, id] = ref.split("/"); const result = fhirIndex.fhirResourcesById[id]; - // TODO: Add type checking + // TODO ANGELA: Add type checking if (result && result?.resourceType !== resourceType) { console.error( diff --git a/containers/ecr-viewer/src/app/view-data/components/EcrDocument/accordion-items.tsx b/containers/ecr-viewer/src/app/view-data/components/EcrDocument/accordion-items.tsx index a19d22f99..c4979d6e4 100644 --- a/containers/ecr-viewer/src/app/view-data/components/EcrDocument/accordion-items.tsx +++ b/containers/ecr-viewer/src/app/view-data/components/EcrDocument/accordion-items.tsx @@ -3,8 +3,6 @@ import React from "react"; import { Bundle, DiagnosticReport } from "fhir/r4"; import { AccordionItem } from "@/app/types"; -import { evaluateAll } from "@/app/utils/evaluate"; -import fhirPathMappings from "@/app/utils/evaluate/fhir-paths"; import { toKebabCase } from "@/app/utils/format-utils"; import ClinicalInfo from "@/app/view-data/components/ClinicalInfo"; import Demographics from "@/app/view-data/components/Demographics"; @@ -32,56 +30,29 @@ import { evaluateClinicalData } from "./clinical-data"; /** * Functional component for an accordion container displaying various sections of eCR information. * @param fhirBundle - The FHIR bundle containing patient information. + * @param fhirIndex - FHIR resources indexed by type & by ID * @returns The JSX element representing the accordion container. */ export const getEcrDocumentAccordionItems = ( fhirBundle: Bundle, fhirIndex: FhirIndex ): AccordionItem[] => { - const t0 = performance.now(); const demographicsData = evaluateDemographicsData(fhirBundle); - const t11 = performance.now(); - console.log("Time to run evaluateDemographics", t11 - t0); const socialData = evaluateSocialData(fhirBundle); - const t12 = performance.now(); - console.log("Time to run evaluateSocialData", t12 - t0); const pregnancyData = evaluatePregnancyData(fhirBundle); - const t13 = performance.now(); - console.log("Time to run evaluatePregnancyData", t13 - t0); const hospitalEncounterData = evaluateHospitalEncounterData(fhirBundle); - const t14 = performance.now(); - console.log("Time to run evaluateHospitalEncounterData", t14 - t0); const encounterData = evaluateEncounterData(fhirBundle); - const t15 = performance.now(); - console.log("Time to run evaluateEncounterData", t15 - t0); const providerData = evaluateProviderData(fhirBundle); - const t16 = performance.now(); - console.log("Time to run evaluateProviderData", t16 - t0); const clinicalData = evaluateClinicalData(fhirBundle); - const t17 = performance.now(); - console.log("Time to run evaluateClinicalData", t17 - t0); const ecrMetadata = evaluateEcrMetadata(fhirBundle); - const t18 = performance.now(); - console.log("Time to run evaluateEcrMetadata", t18 - t0); const facilityData = evaluateFacilityData(fhirBundle); - const t19 = performance.now(); - console.log("Time to run evaluateFacilityData", t19 - t0); - const lab0 = performance.now(); - const diagnosticReports = getResourcesByType(fhirIndex, 'DiagnosticReport') - const lab1 = performance.now(); - console.log("Time to evaluate diagnosticReports", lab1 - lab0); const labInfoData = evaluateLabInfoData( fhirBundle, - diagnosticReports, - fhirIndex + fhirIndex, + diagnosticReports ); - const t20 = performance.now(); - console.log("Time to run evaluateLabInfoData", t20 - t0); - - const t1 = performance.now(); - console.log("Time to evaluate all : ", t1 - t0); - + const hasUnavailableData = () => { const unavailableDataArrays = [ demographicsData.unavailableData, @@ -106,7 +77,6 @@ export const getEcrDocumentAccordionItems = ( ); }; - const t2 = performance.now(); const accordionItems: AccordionItem[] = [ { title: "Patient Info", @@ -280,8 +250,6 @@ export const getEcrDocumentAccordionItems = ( headingLevel: "h3", }; }); - const t3 = performance.now(); - console.log("Time to build accordion components", t3 - t2); - + return accordionItems; }; diff --git a/containers/ecr-viewer/src/app/view-data/components/EcrSummary.tsx b/containers/ecr-viewer/src/app/view-data/components/EcrSummary.tsx index 09370de47..5f8b4c90e 100644 --- a/containers/ecr-viewer/src/app/view-data/components/EcrSummary.tsx +++ b/containers/ecr-viewer/src/app/view-data/components/EcrSummary.tsx @@ -11,8 +11,9 @@ import { Bundle } from "fhir/r4"; import { FhirIndex } from "@/app/view-data/services/fhirResourcesIndexService"; interface EcrSummaryProps { - fhirBundle: Bundle; - fhirIndex: FhirIndex; + patientDetails: DisplayDataProps[]; + encounterDetails: DisplayDataProps[]; + conditionSummary: ConditionSummary[]; snomed?: string; } @@ -35,135 +36,127 @@ export interface ConditionSummary { * @returns a react element for eCR Summary */ const EcrSummary: React.FC = ({ - fhirBundle, - fhirIndex, + patientDetails, + encounterDetails, + conditionSummary, snomed }) => { - const t0 = performance.now(); - const patientDetails = evaluateEcrSummaryPatientDetails(fhirBundle).availableData; - const encounterDetails = evaluateEcrSummaryEncounterDetails(fhirBundle).availableData; - const conditionSummary = evaluateEcrSummaryConditionSummary(fhirBundle, fhirIndex, snomed); - const t1 = performance.now(); - console.log("Evaluate eCR summary: ", t1 - t0); - try { - const conditionSummaryAccordionItems: AccordionItem[] = conditionSummary.map( - (condition) => { - const hasImmunizationDetails = condition.immunizationDetails.length > 0; - const hasClinicalDetails = condition.clinicalDetails.length > 0; - return { - title: condition.title, - id: toKebabCase(condition.title), - headingLevel: "h4", - className: "side-nav-ignore border-1px border-accent-cool-darker", - expanded: snomed === condition.snomed || conditionSummary.length === 1, - content: ( - <> - {condition.conditionDetails.map((item, i) => ( - - ))} - {(hasImmunizationDetails || hasClinicalDetails) && ( - <> -
- Clinical Sections Relevant to Reportable Condition -
- {hasImmunizationDetails && ( -
- {condition.immunizationDetails.map((item, i) => ( - - ))} -
- )} - {hasClinicalDetails && ( -
- {condition.clinicalDetails.map((item, i) => ( - - ))} -
- )} - - )} - {condition.labDetails.length > 0 && ( - <> -
- Lab Results Relevant to Reportable Condition -
+ + const conditionSummaryAccordionItems: AccordionItem[] = conditionSummary.map( + (condition) => { + const hasImmunizationDetails = condition.immunizationDetails.length > 0; + const hasClinicalDetails = condition.clinicalDetails.length > 0; + return { + title: condition.title, + id: toKebabCase(condition.title), + headingLevel: "h4", + className: "side-nav-ignore border-1px border-accent-cool-darker", + expanded: snomed === condition.snomed || conditionSummary.length === 1, + content: ( + <> + {condition.conditionDetails.map((item, i) => ( + + ))} + {(hasImmunizationDetails || hasClinicalDetails) && ( + <> +
+ Clinical Sections Relevant to Reportable Condition +
+ {hasImmunizationDetails && (
- {condition.labDetails.map((item, index) => ( - + {condition.immunizationDetails.map((item, i) => ( + ))}
- - )} - - ), - }; - }, - ); - return ( -
-
-

- Patient Summary -

-
- {patientDetails.map((item, i) => ( - - ))} -
+ )} + {hasClinicalDetails && ( +
+ {condition.clinicalDetails.map((item, i) => ( + + ))} +
+ )} + + )} + {condition.labDetails.length > 0 && ( + <> +
+ Lab Results Relevant to Reportable Condition +
+
+ {condition.labDetails.map((item, index) => ( + + ))} +
+ + )} + + ), + }; + }, + ); + + return ( +
+
+

+ Patient Summary +

+
+ {patientDetails.map((item, i) => ( + + ))}
-
-

- Encounter Summary -

-
- {encounterDetails.map((item, i) => ( - - ))} -
+
+
+

+ Encounter Summary +

+
+ {encounterDetails.map((item, i) => ( + + ))}
-
-

-
Condition Summary
-
- - {numConditionsText(conditionSummaryAccordionItems.length)} - -
-

-
- +
+
+

+
Condition Summary
+
+ + {numConditionsText(conditionSummaryAccordionItems.length)} +
+

+
+
- ); - } finally { - const t2 = performance.now() - console.log("EcrSummary: ", t2 - t0); - } +
+ ); }; /** diff --git a/containers/ecr-viewer/src/app/view-data/page.tsx b/containers/ecr-viewer/src/app/view-data/page.tsx index 453cec36d..6aa66e694 100644 --- a/containers/ecr-viewer/src/app/view-data/page.tsx +++ b/containers/ecr-viewer/src/app/view-data/page.tsx @@ -17,6 +17,7 @@ import { } from "./services/evaluateFhirDataService"; import { getFhirData, isSuccessResponse } from "./services/fhirDataService"; import { getFhirIndex } from "@/app/view-data/services/fhirResourcesIndexService"; +import { evaluateEcrSummaryConditionSummary, evaluateEcrSummaryEncounterDetails, evaluateEcrSummaryPatientDetails } from "./services/ecrSummaryService"; /** * Functional component for rendering the eCR Viewer page. @@ -81,8 +82,17 @@ const ECRViewerPage = async ({
diff --git a/containers/ecr-viewer/src/app/view-data/services/ecrSummaryService.tsx b/containers/ecr-viewer/src/app/view-data/services/ecrSummaryService.tsx index 712bff1f4..fd788f022 100644 --- a/containers/ecr-viewer/src/app/view-data/services/ecrSummaryService.tsx +++ b/containers/ecr-viewer/src/app/view-data/services/ecrSummaryService.tsx @@ -156,7 +156,8 @@ export const evaluateEcrSummaryEncounterDetails = (fhirBundle: Bundle) => { /** * Evaluates and retrieves all condition details in a bundle. * @param fhirBundle - The FHIR bundle containing patient data. - * @param snomedCode - The SNOMED code identifying the main snomed code. + * @param fhirIndex - FHIR resources indexed by type & by ID + * @param snomedCode - (Optional) The SNOMED code identifying the main snomed code. * @returns An array of condition summary objects. */ export const evaluateEcrSummaryConditionSummary = ( @@ -292,6 +293,7 @@ export const evaluateEcrSummaryRelevantClinicalDetails = ( /** * Evaluates and retrieves relevant lab results from the FHIR bundle using the provided SNOMED code and path mappings. * @param fhirBundle - The FHIR bundle containing patient data. + * @param fhirIndex - FHIR resources indexed by type & by ID * @param snomedCode - String containing the SNOMED code search parameter. * @param lastDividerLine - Boolean to determine if a divider line should be added to the end of the lab results. Default to true * @returns An array of lab result details objects containing title and value pairs. @@ -308,24 +310,14 @@ export const evaluateEcrSummaryRelevantLabResults = ( return []; } - // const labReports = evaluateAll( - // fhirBundle, - // fhirPathMappings.diagnosticReports, - // ); - const labReports = getResourcesByType(fhirIndex, 'DiagnosticReport'); const labsWithCode = getRelevantResources(labReports, snomedCode); const labsWithCodeIds = new Set(labsWithCode.map((lab) => lab.id)); - // const observationsList = evaluateAll( - // fhirBundle, - // fhirPathMappings.observations, - // ); const observationsList = getResourcesByType( fhirIndex, "Observation" ); - // console.log(observationsList); const relevantObsIds = new Set( getRelevantResources(observationsList, snomedCode).map((entry) => entry.id) ); @@ -351,8 +343,8 @@ export const evaluateEcrSummaryRelevantLabResults = ( } const relevantLabElements = evaluateLabInfoData( fhirBundle, - relevantLabs, fhirIndex, + relevantLabs, "h4" ); diff --git a/containers/ecr-viewer/src/app/view-data/services/labsService.tsx b/containers/ecr-viewer/src/app/view-data/services/labsService.tsx index 9ae5bdcf0..596c70f1e 100644 --- a/containers/ecr-viewer/src/app/view-data/services/labsService.tsx +++ b/containers/ecr-viewer/src/app/view-data/services/labsService.tsx @@ -79,54 +79,31 @@ const ABNORMAL_OBSERVATION_INTERPRETATIONS: Record = { /** * Evaluates lab information and RR data from the provided FHIR bundle and mappings. * @param fhirBundle - The FHIR bundle containing lab and RR data. + * @param fhirIndex - FHIR resources indexed by type & by ID * @param labReports - An array of DiagnosticReport objects * @param accordionHeadingLevel - Heading level for the title of AccordionLabResults. * @returns An array of the Diagnostic reports Elements and Organization Display Data */ export const evaluateLabInfoData = ( fhirBundle: Bundle, - labReports: DiagnosticReport[], fhirIndex: FhirIndex, + labReports: DiagnosticReport[], accordionHeadingLevel: HeadingLevel = "h5" ): LabReportElementData[] => { - const t0 = performance.now(); let organizationItems: ResultObject = {}; - const jsonLabs = getAllLabJsonObjects(fhirBundle, fhirIndex); - - const tgetall0 = performance.now(); - console.log("Time to getAllLabJsonObjects", tgetall0 - t0); - // const labObs = fhirIndex.fhirResourcesByType["Observation"]; - - const tgetall1 = performance.now(); - console.log("Time to get all lab obs: ", tgetall1 - tgetall0); - // console.log(allObs); - - const totals = { - getObservations: 0, - getJsonLab: 0, - getLabsContent: 0, - evaluateValue: 0, - }; + const jsonLabs = getAllLabJsonObjects(fhirIndex); for (const report of labReports) { let t; - t = performance.now(); const obs = getObservations(report, fhirIndex); - totals.getObservations += performance.now() - t; - - t = performance.now(); const labReportJson = getJsonLab(jsonLabs, obs); - totals.getJsonLab += performance.now() - t; - ensureReportHasDateTime(report, obs); - t = performance.now(); const content = getLabsContent( report, obs, - fhirBundle, fhirIndex, labReportJson ); @@ -134,13 +111,9 @@ export const evaluateLabInfoData = ( "Organization/", "" ); - totals.getLabsContent += performance.now() - t; const title = formatCodeableConcept(report.code) ?? "Unknown"; - t = performance.now(); - totals.evaluateValue += performance.now() - t; - const item = { title: (
@@ -174,62 +147,13 @@ export const evaluateLabInfoData = ( ); } - const t1 = performance.now(); - - console.log("evaluateLabInfoData TOTAL:", (t1 - t0).toFixed(2), "ms"); - console.log("Breakdown:"); - Object.entries(totals) - .sort((a, b) => b[1] - a[1]) - .forEach(([k, v]) => { - console.log(` ${k}: ${v.toFixed(2)} ms`); - }); - - return combineOrgAndReportData(organizationItems, fhirBundle, fhirIndex); + return combineOrgAndReportData(organizationItems, fhirIndex); }; -// /** -// * Extracts all lab `Observation` resources from a given FHIR bundle across all diagnostic reports. -// * @param fhirBundle - The FHIR bundle containing related resources for the lab report. -// * @returns An object of `Observation` resources with the observation `id` (string) as the key. -// * If no matching observations are found, an empty object is returned. -// */ -// export const getAllObservationResources = ( -// fhirBundle: Bundle -// ): Record => { -// // init the object -// const labObs: Record = {}; - -// // Get lab Composition section -// // const labObsRefs = evaluateAll( -// // fhirBundle, -// // fhirPathMappings.labResultObservations -// // ); -// // console.log(labObsRefs); - -// // For each reference -// // labObsRefs.forEach((ref) => { -// // // TODO ANGELA: don't do reference lookups. just iterate through bundle once and record all observations -// // const ob = evaluateReference(fhirBundle, ref.reference); -// // if (ob && ob.id) { -// // labObs[ob.id] = ob -// // }; -// // }); - -// const obs = evaluateAll(fhirBundle, fhirPathMappings.observations); -// obs.forEach((ob) => { -// if (ob && ob.id) { -// labObs[ob.id] = ob -// }; -// }) - -// //return the object -// return labObs; -// }; - /** - * Extracts an array of `Observation` resources from a given FHIR bundle based on a list of observation references. + * Extracts an array of `Observation` resources from a the FHIR index based on a list of observation references. * @param report - The lab report containing the results to be processed. - * @param fhirBundle - The FHIR bundle containing related resources for the lab report. + * @param fhirIndex - FHIR resources indexed by type & by ID * @returns An array of `Observation` resources from the FHIR bundle that correspond to the * given references. If no matching observations are found or if the input references array is empty, an empty array * is returned. @@ -325,10 +249,10 @@ export const getJsonLab = ( /** * Retrieves the JSON representation of a lab report from the labs HTML string. - * @param fhirBundle - The FHIR Bundle object containing relevant FHIR resources. + * @param fhirIndex - FHIR resources indexed by type & by ID * @returns The JSON representation of the lab report. */ -export const getAllLabJsonObjects = (fhirBundle: Bundle, fhirIndex: FhirIndex): HtmlTableJson[] => { +export const getAllLabJsonObjects = (fhirIndex: FhirIndex): HtmlTableJson[] => { // Get lab reports HTML String (for all lab reports) & convert to JSON const compositionLabs = getResourcesByType(fhirIndex, 'Composition')[0]; const labsString = evaluateValue( @@ -535,12 +459,11 @@ export const returnAnalysisTime = ( /** * Evaluates diagnostic report data and generates the lab observations for each report. * @param obs - An object containing an array of result observations. - * @param fhirBundle - The FHIR bundle containing diagnostic report data. + * @param fhirIndex - FHIR resources indexed by type & by ID * @returns - An array of React elements representing the lab observations. */ export const evaluateDiagnosticReportData = ( obs: Observation[], - fhirBundle: Bundle, fhirIndex: FhirIndex, ): React.JSX.Element | undefined => { if (!obs.length) return undefined; @@ -565,7 +488,7 @@ export const evaluateDiagnosticReportData = ( columnName: "Test Method", infoPath: "observationDeviceReference", applyToValue: (ref) => { - // TODO: Angela review evaluateReference2 + // TODO ANGELA: review evaluateReference2 const device = evaluateReference2(fhirIndex, ref); return safeParse(device?.deviceName?.[0]?.name ?? ""); }, @@ -661,19 +584,17 @@ export const evaluateOrganismsReportData = ( /** * Combines the org display data with the diagnostic report elements * @param organizationItems - Object containing the keys of org data, values of the diagnostic report elements - * @param fhirBundle - The FHIR bundle containing lab and RR data. + * @param fhirIndex - FHIR resources indexed by type & by ID * @returns An array of the Diagnostic reports Elements and Organization Display Data */ export const combineOrgAndReportData = ( organizationItems: ResultObject, - fhirBundle: Bundle, fhirIndex: FhirIndex, ): LabReportElementData[] => { return Object.keys(organizationItems).map((key: string) => { const organizationId = key.replace("Organization/", ""); const orgData = evaluateLabOrganizationData( organizationId, - fhirBundle, fhirIndex, organizationItems[key].length, ); @@ -688,13 +609,12 @@ export const combineOrgAndReportData = ( /** * Finds the Organization that matches the id and creates a DisplayDataProps array * @param id - id of the organization - * @param fhirBundle - The FHIR bundle containing lab and RR data. + * @param fhirIndex - FHIR resources indexed by type & by ID * @param labReportCount - A number representing the amount of lab reports for a specific organization * @returns The organization display data as an array */ export const evaluateLabOrganizationData = ( id: string, - fhirBundle: Bundle, fhirIndex: FhirIndex, labReportCount: number, ) => { @@ -780,23 +700,17 @@ const groupItemByOrgId = ( * Retrieves the content for a lab report. * @param report - The DiagnosticReport resource. * @param obs - The observations associated with the report - * @param fhirBundle - The FHIR Bundle. + * @param fhirIndex - FHIR resources indexed by type & by ID * @param labReportJson - The JSON representation of the lab results from HTML. * @returns An array of JSX elements representing the lab report content. */ -// TODO ANGELA: Review const getLabsContent = ( report: DiagnosticReport, obs: Observation[], - fhirBundle: Bundle, fhirIndex: FhirIndex, labReportJson?: HtmlTableJson, ) => { - const labTableDiagnostic = evaluateDiagnosticReportData( - obs, - fhirBundle, - fhirIndex - ); + const labTableDiagnostic = evaluateDiagnosticReportData(obs, fhirIndex); const labTableOrganisms = evaluateOrganismsReportData(obs); const labSpecimen = evaluateReference2( fhirIndex, From fd592c13a8544a0386b6057ec451509377537060 Mon Sep 17 00:00:00 2001 From: angelathe Date: Tue, 24 Mar 2026 13:42:22 -0400 Subject: [PATCH 03/14] fix unit tests --- .../EcrDocument/EcrDocument.test.tsx | 7 +- .../app/view-data/components/LabInfo.test.tsx | 46 +++++----- .../services/ecrSummaryService.test.tsx | 75 ++++++++++----- .../view-data/services/labsService.test.tsx | 91 +++++++++++-------- 4 files changed, 138 insertions(+), 81 deletions(-) diff --git a/containers/ecr-viewer/tests/unit/app/view-data/components/EcrDocument/EcrDocument.test.tsx b/containers/ecr-viewer/tests/unit/app/view-data/components/EcrDocument/EcrDocument.test.tsx index fcdf2d292..a35d045ec 100644 --- a/containers/ecr-viewer/tests/unit/app/view-data/components/EcrDocument/EcrDocument.test.tsx +++ b/containers/ecr-viewer/tests/unit/app/view-data/components/EcrDocument/EcrDocument.test.tsx @@ -17,6 +17,7 @@ import { evaluateClinicalData, returnCareTeamTable, } from "@/app/view-data/components/EcrDocument/clinical-data"; +import { getFhirIndex } from "@/app/view-data/services/fhirResourcesIndexService"; const BundleWithPatient = _BundleWithPatient as Bundle; @@ -27,8 +28,12 @@ describe("Snapshot test for eCR Document", () => { type: "batch", entry: [], }; + const fhirIndexBundleEmpty = getFhirIndex(bundleEmpty); - const items = getEcrDocumentAccordionItems(bundleEmpty); + const items = getEcrDocumentAccordionItems( + bundleEmpty, + fhirIndexBundleEmpty + ); const { container } = render(); diff --git a/containers/ecr-viewer/tests/unit/app/view-data/components/LabInfo.test.tsx b/containers/ecr-viewer/tests/unit/app/view-data/components/LabInfo.test.tsx index 9ac36729c..825be94dd 100644 --- a/containers/ecr-viewer/tests/unit/app/view-data/components/LabInfo.test.tsx +++ b/containers/ecr-viewer/tests/unit/app/view-data/components/LabInfo.test.tsx @@ -2,10 +2,10 @@ import React from "react"; import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; -import { Bundle } from "fhir/r4"; +import { Bundle, DiagnosticReport } from "fhir/r4"; -import BundleLab from "../../../../../../../test-data/fhir/BundleLab.json"; -import BundleLabNoLabIds from "../../../../../../../test-data/fhir/BundleLabNoLabIds.json"; +import _BundleLab from "../../../../../../../test-data/fhir/BundleLab.json"; +import _BundleLabNoLabIds from "../../../../../../../test-data/fhir/BundleLabNoLabIds.json"; import { evaluateAll } from "@/app/utils/evaluate"; import fhirPathMappings from "@/app/utils/evaluate/fhir-paths"; import LabInfo from "@/app/view-data/components/LabInfo"; @@ -13,14 +13,22 @@ import { evaluateLabInfoData, LabReportElementData, } from "@/app/view-data/services/labsService"; +import { getFhirIndex, getResourcesByType } from "@/app/view-data/services/fhirResourcesIndexService"; + +const BundleLab = _BundleLab as unknown as Bundle; +const fhirIndexBundleLab = getFhirIndex(BundleLab); +const BundleLabNoLabIds = _BundleLabNoLabIds as unknown as Bundle; +const fhirIndexBundleLabNoLabIds = getFhirIndex(BundleLabNoLabIds); describe("LabInfo", () => { describe("when labResults is LabReportElementData[]", () => { let labInfoJsx: React.ReactElement; beforeAll(() => { + const diagnosticReports = getResourcesByType(fhirIndexBundleLab, "DiagnosticReport"); const labInfoOrg = evaluateLabInfoData( - BundleLab as unknown as Bundle, - evaluateAll(BundleLab as Bundle, fhirPathMappings.diagnosticReports), + BundleLab, + fhirIndexBundleLab, + diagnosticReports ) as LabReportElementData[]; // Empty out one of the lab names for testing @@ -100,15 +108,19 @@ describe("LabInfo", () => { }); describe("when labResults is DisplayDataProps[]", () => { - it("should be collapsed by default", () => { - const labInfo = evaluateLabInfoData( - BundleLabNoLabIds as unknown as Bundle, - evaluateAll( - BundleLabNoLabIds as Bundle, - fhirPathMappings.diagnosticReports, - ), + let labInfo: LabReportElementData[]; + beforeAll(() => { + const diagnosticReports = getResourcesByType( + fhirIndexBundleLabNoLabIds, + "DiagnosticReport" ); - + labInfo = evaluateLabInfoData( + BundleLabNoLabIds, + fhirIndexBundleLabNoLabIds, + diagnosticReports + ); + }); + it("should be collapsed by default", () => { render(); screen .getAllByTestId("accordionButton", { exact: false }) @@ -129,14 +141,6 @@ describe("LabInfo", () => { }); it("should match snapshot test", () => { - const labInfo = evaluateLabInfoData( - BundleLabNoLabIds as unknown as Bundle, - evaluateAll( - BundleLabNoLabIds as Bundle, - fhirPathMappings.diagnosticReports, - ), - ); - const { container } = render(); expect(container).toMatchSnapshot(); }); diff --git a/containers/ecr-viewer/tests/unit/app/view-data/services/ecrSummaryService.test.tsx b/containers/ecr-viewer/tests/unit/app/view-data/services/ecrSummaryService.test.tsx index 8143798e7..9aebbb516 100644 --- a/containers/ecr-viewer/tests/unit/app/view-data/services/ecrSummaryService.test.tsx +++ b/containers/ecr-viewer/tests/unit/app/view-data/services/ecrSummaryService.test.tsx @@ -4,16 +4,28 @@ import { render, screen } from "@testing-library/react"; import { Bundle } from "fhir/r4"; import BundleWithClinicalInfo from "../../../../../../../test-data/fhir/BundleClinicalInfo.json"; -import BundleEcrSummary from "../../../../../../../test-data/fhir/BundleEcrSummary.json"; -import BundleLab from "../../../../../../../test-data/fhir/BundleLab.json"; +import _BundleEcrSummary from "../../../../../../../test-data/fhir/BundleEcrSummary.json"; +import _BundleLab from "../../../../../../../test-data/fhir/BundleLab.json"; import BundlePatient from "../../../../../../../test-data/fhir/BundlePatient.json"; -import BundleRRConditionValueString from "../../../../../../../test-data/fhir/BundleRRConditionValueString.json"; +import _BundleRRConditionValueString from "../../../../../../../test-data/fhir/BundleRRConditionValueString.json"; import { evaluateEcrSummaryConditionSummary, evaluateEcrSummaryPatientDetails, evaluateEcrSummaryRelevantClinicalDetails, evaluateEcrSummaryRelevantLabResults, } from "@/app/view-data/services/ecrSummaryService"; +import { FhirIndex, getFhirIndex } from "@/app/view-data/services/fhirResourcesIndexService"; + +const BundleLab = _BundleLab as unknown as Bundle; +const fhirIndexBundleLab = getFhirIndex(BundleLab); + +const BundleEcrSummary = _BundleEcrSummary as unknown as Bundle; +const fhirIndexBundleEcrSummary = getFhirIndex(BundleEcrSummary); + +const BundleRRConditionValueString = _BundleRRConditionValueString as unknown as Bundle; +const fhirIndexBundleRRConditionValueString = getFhirIndex( + BundleRRConditionValueString +); describe("ecrSummaryService Tests", () => { describe("Evaluate eCR Summary Relevant Clinical Details", () => { @@ -57,8 +69,9 @@ describe("ecrSummaryService Tests", () => { describe("Evaluate eCR Summary Relevant Lab Results", () => { it("should return an empty list when no SNOMED code is provided", () => { const actual = evaluateEcrSummaryRelevantLabResults( - BundleLab as unknown as Bundle, - "", + BundleLab, + fhirIndexBundleLab, + "" ); expect(actual).toBeEmpty(); @@ -66,8 +79,9 @@ describe("ecrSummaryService Tests", () => { it("should return 'No Data' string when the provided SNOMED code has no matches", () => { const actual = evaluateEcrSummaryRelevantLabResults( - BundleLab as unknown as Bundle, - "invalid-snomed-code", + BundleLab, + fhirIndexBundleLab, + "invalid-snomed-code" ); expect(actual).toBeEmpty(); @@ -75,7 +89,8 @@ describe("ecrSummaryService Tests", () => { it("should return the correct lab result(s) when the provided SNOMED code matches", () => { const result = evaluateEcrSummaryRelevantLabResults( - BundleLab as unknown as Bundle, + BundleLab, + fhirIndexBundleLab, "test-snomed", ); expect(result).toHaveLength(3); // 2 results, plus last item is divider line @@ -95,7 +110,8 @@ describe("ecrSummaryService Tests", () => { it("should not include the last empty divider line when lastDividerLine is false", () => { const result = evaluateEcrSummaryRelevantLabResults( - BundleLab as unknown as Bundle, + BundleLab, + fhirIndexBundleLab, "test-snomed", false, ); @@ -107,7 +123,8 @@ describe("ecrSummaryService Tests", () => { describe("Evaluate eCR Summary Condition Summary", () => { it("should return titles based on snomed code, and return human-readable name if available", () => { const actual = evaluateEcrSummaryConditionSummary( - BundleEcrSummary as unknown as Bundle, + BundleEcrSummary, + fhirIndexBundleEcrSummary ); expect(actual[0].title).toEqual("Hepatitis C"); @@ -117,14 +134,16 @@ describe("ecrSummaryService Tests", () => { }); it("should return human-readable name if available", () => { const actual = evaluateEcrSummaryConditionSummary( - BundleRRConditionValueString as unknown as Bundle, + BundleRRConditionValueString, + fhirIndexBundleRRConditionValueString ); expect(actual[0].title).toEqual("COVID"); }); it("should return summaries based on snomed code", () => { const actual = evaluateEcrSummaryConditionSummary( - BundleEcrSummary as unknown as Bundle, + BundleEcrSummary, + fhirIndexBundleEcrSummary ); render( actual[1].conditionDetails.map((detail, i) => ( @@ -148,7 +167,8 @@ describe("ecrSummaryService Tests", () => { }); it("should return clinical details based on snomed code", () => { const actual = evaluateEcrSummaryConditionSummary( - BundleEcrSummary as unknown as Bundle, + BundleEcrSummary, + fhirIndexBundleEcrSummary ); render( @@ -161,7 +181,8 @@ describe("ecrSummaryService Tests", () => { }); it("should return lab details based on snomed code", () => { const actual = evaluateEcrSummaryConditionSummary( - BundleEcrSummary as unknown as Bundle, + BundleEcrSummary, + fhirIndexBundleEcrSummary ); render( @@ -177,7 +198,8 @@ describe("ecrSummaryService Tests", () => { }); it("should return immunization details based on snomed code", () => { const actual = evaluateEcrSummaryConditionSummary( - BundleEcrSummary as unknown as Bundle, + BundleEcrSummary, + fhirIndexBundleEcrSummary, ); render( actual[1].immunizationDetails.map((detail) => ( @@ -188,10 +210,10 @@ describe("ecrSummaryService Tests", () => { expect(screen.getByText("SARS-CoV-2 PCR Vaccine")).toBeInTheDocument(); }); it("should not display non-related immunization details", () => { - const actual = evaluateEcrSummaryConditionSummary({ - ...BundleEcrSummary, + const BundleNonRelatedImmuns = { + ..._BundleEcrSummary, entry: [ - ...BundleEcrSummary.entry, + ..._BundleEcrSummary.entry, { fullUrl: "urn:uuid:6689c3f5-f256-9c28-bd98-89905630f28d", resource: { @@ -219,7 +241,14 @@ describe("ecrSummaryService Tests", () => { }, }, ], - } as unknown as Bundle); + } as unknown as Bundle; + const fhirIndexBundleNonRelatedImmuns = getFhirIndex( + BundleNonRelatedImmuns + ); + const actual = evaluateEcrSummaryConditionSummary( + BundleNonRelatedImmuns, + fhirIndexBundleNonRelatedImmuns + ); render( actual[1].immunizationDetails.map((detail) => ( {detail.value} @@ -230,13 +259,14 @@ describe("ecrSummaryService Tests", () => { expect(screen.queryByText("anthrax")).not.toBeInTheDocument(); }); it("should return empty array if none found", () => { - const actual = evaluateEcrSummaryConditionSummary({} as Bundle); + const actual = evaluateEcrSummaryConditionSummary({} as Bundle, {} as FhirIndex); expect(actual).toBeEmpty(); }); it("should return the the requested snomed first", () => { const verifyNotFirst = evaluateEcrSummaryConditionSummary( - BundleEcrSummary as unknown as Bundle, + BundleEcrSummary, + fhirIndexBundleEcrSummary ); expect(verifyNotFirst[0].title).not.toEqual( @@ -244,7 +274,8 @@ describe("ecrSummaryService Tests", () => { ); const actual = evaluateEcrSummaryConditionSummary( - BundleEcrSummary as unknown as Bundle, + BundleEcrSummary, + fhirIndexBundleEcrSummary, "840539006", ); expect(actual[0].title).toEqual( diff --git a/containers/ecr-viewer/tests/unit/app/view-data/services/labsService.test.tsx b/containers/ecr-viewer/tests/unit/app/view-data/services/labsService.test.tsx index b9f34c7cd..b3994bca9 100644 --- a/containers/ecr-viewer/tests/unit/app/view-data/services/labsService.test.tsx +++ b/containers/ecr-viewer/tests/unit/app/view-data/services/labsService.test.tsx @@ -31,11 +31,19 @@ import { getObservations, LabInterpretationTag, } from "@/app/view-data/services/labsService"; +import { FhirIndex, getFhirIndex, getResourcesByType } from "@/app/view-data/services/fhirResourcesIndexService"; const BundleLab = _BundleLab as unknown as Bundle; +const fhirIndexBundleLab = getFhirIndex(BundleLab); + const BundleLabInvalidResultsDiv = _BundleLabInvalidResultsDiv as unknown as Bundle; +const fhirIndexBundleLabInvalidResultsDiv = getFhirIndex( + BundleLabInvalidResultsDiv +); + const BundleLabNoLabIds = _BundleLabNoLabIds as unknown as Bundle; +const fhirIndexBundleLabNoLabIds = getFhirIndex(BundleLabNoLabIds); const pathLabReportNormal = "Bundle.entry.resource.where(resourceType = 'DiagnosticReport').where(id = 'c090d379-9aea-f26e-4ddc-378223841e3b')"; @@ -174,10 +182,10 @@ const labReportAbnormal = evaluateOneAndCheck( pathLabReportAbnormal, "DiagnosticReport", ); -const jsonLabs = getAllLabJsonObjects(BundleLab); +const jsonLabs = getAllLabJsonObjects(fhirIndexBundleLab); const labReportAbnormalJsonObject = getJsonLab( jsonLabs, - getObservations(labReportAbnormal!, BundleLab), + getObservations(labReportAbnormal!, fhirIndexBundleLab) ); const pathLabOrganismsTableAndNarr = @@ -203,7 +211,7 @@ describe("LabsService tests", () => { code: {}, status: "entered-in-error", }, - BundleLab, + fhirIndexBundleLab ); const expectedObservationPath = @@ -228,7 +236,7 @@ describe("LabsService tests", () => { code: {}, status: "final", }, - BundleLab, + fhirIndexBundleLab ); expect(result).toStrictEqual([]); }); @@ -238,10 +246,10 @@ describe("LabsService tests", () => { it("returns correct Json Object for table with data-id", () => { const expectedResult = labReportNormalJsonObject; - const jsonLabs = getAllLabJsonObjects(BundleLab); + const jsonLabs = getAllLabJsonObjects(fhirIndexBundleLab); const result = getJsonLab( jsonLabs, - getObservations(labReportNormal!, BundleLab), + getObservations(labReportNormal!, fhirIndexBundleLab) ); expect(result).toEqual(expectedResult); @@ -254,20 +262,20 @@ describe("LabsService tests", () => { "DiagnosticReport", ); - const jsonLabs = getAllLabJsonObjects(BundleLabNoLabIds); + const jsonLabs = getAllLabJsonObjects(fhirIndexBundleLabNoLabIds); const result = getJsonLab( jsonLabs, - getObservations(labReportWithoutIds!, BundleLabNoLabIds), + getObservations(labReportWithoutIds!, fhirIndexBundleLabNoLabIds) ); expect(result).toBeUndefined(); }); it("returns undefined if lab results html contains no tables", () => { - const jsonLabs = getAllLabJsonObjects(BundleLab); + const jsonLabs = getAllLabJsonObjects(fhirIndexBundleLab); const result = getJsonLab( jsonLabs, - getObservations(labReportNormal!, BundleLabInvalidResultsDiv), + getObservations(labReportNormal!, fhirIndexBundleLabInvalidResultsDiv) ); expect(result).toBeUndefined(); @@ -483,9 +491,12 @@ describe("LabsService tests", () => { it("should fallback to checkAbnormalTag logic when lab report is abnormal, but not one of the abnormal observation interpretations", () => { render( , + /> ); const tagElement = screen.getByText("Abnormal"); expect(tagElement).toBeInTheDocument(); @@ -660,7 +671,7 @@ describe("LabsService tests", () => { describe("evaluateOrganismsReportData", () => { it("should return the correct organisms table when the data exists for a lab report", () => { const result = evaluateOrganismsReportData( - getObservations(labOrganismsTableAndNarr!, BundleLab), + getObservations(labOrganismsTableAndNarr!, fhirIndexBundleLab) )!; render(result); @@ -672,7 +683,7 @@ describe("LabsService tests", () => { }); it("should return undefined if lab organisms data does not exist for a lab report", () => { const result = evaluateOrganismsReportData( - getObservations(labReportNormal!, BundleLab), + getObservations(labReportNormal!, fhirIndexBundleLab) ); expect(result).toBeUndefined(); @@ -681,13 +692,10 @@ describe("LabsService tests", () => { describe("Evaluate Diagnostic Report", () => { it("should evaluate diagnostic report results", () => { - const report = evaluateAll( - BundleLab, - fhirPathMappings.diagnosticReports, - )[0]; + const report = getResourcesByType(fhirIndexBundleLab, 'DiagnosticReport')[0]; const actual = evaluateDiagnosticReportData( - getObservations(report, BundleLab), - BundleLab, + getObservations(report, fhirIndexBundleLab), + fhirIndexBundleLab ); render(actual); @@ -708,19 +716,19 @@ describe("LabsService tests", () => { status: "final", }; const actual = evaluateDiagnosticReportData( - getObservations(diagnosticReport, null as unknown as Bundle), - null as unknown as Bundle, + getObservations(diagnosticReport, null as unknown as FhirIndex), + null as unknown as FhirIndex, ); expect(actual).toBeUndefined(); }); it("should evaluate test method results", () => { - const report = evaluateAll( - BundleLab, - fhirPathMappings.diagnosticReports, + const report = getResourcesByType( + fhirIndexBundleLab, + "DiagnosticReport" )[0]; const actual = evaluateDiagnosticReportData( - getObservations(report, BundleLab), - BundleLab, + getObservations(report, fhirIndexBundleLab), + fhirIndexBundleLab ); render(actual); @@ -730,13 +738,13 @@ describe("LabsService tests", () => { ).not.toBeEmpty(); }); it("should display comment", async () => { - const report = evaluateAll( - BundleLab, - fhirPathMappings.diagnosticReports, + const report = getResourcesByType( + fhirIndexBundleLab, + "DiagnosticReport" )[2]; const actual = evaluateDiagnosticReportData( - getObservations(report, BundleLab), - BundleLab, + getObservations(report, fhirIndexBundleLab), + fhirIndexBundleLab, ); render(actual!); @@ -765,7 +773,7 @@ describe("LabsService tests", () => { it("should return a matching org", () => { const result = evaluateLabOrganizationData( "14394818-a1e9-4882-ca8b-FAKE793bb5cc", - BundleLab, + fhirIndexBundleLab, 0, ); expect(result[0].value).toEqual("Tatooine Hospital"); @@ -776,7 +784,10 @@ describe("LabsService tests", () => { {} as AccordionItem, ], }; - const result = combineOrgAndReportData(testResultObject, BundleLab); + const result = combineOrgAndReportData( + testResultObject, + fhirIndexBundleLab + ); expect(result[0].organizationDisplayDataProps).toBeArray(); }); }); @@ -785,7 +796,8 @@ describe("LabsService tests", () => { it("should return a list of LabReportElementData if the lab results in the HTML table have ID's", () => { const result = evaluateLabInfoData( BundleLab, - evaluateAll(BundleLab, fhirPathMappings.diagnosticReports), + fhirIndexBundleLab, + getResourcesByType(fhirIndexBundleLab, 'DiagnosticReport') ); expect(result[0]).toHaveProperty("diagnosticReportDataItems"); expect(result[0]).toHaveProperty("organizationDisplayDataProps"); @@ -794,7 +806,8 @@ describe("LabsService tests", () => { it("should return a list of LabReportElementData even if the lab results in the HTML table do not have ID's", () => { const result = evaluateLabInfoData( BundleLabNoLabIds, - evaluateAll(BundleLabNoLabIds, fhirPathMappings.diagnosticReports), + fhirIndexBundleLabNoLabIds, + getResourcesByType(fhirIndexBundleLabNoLabIds, 'DiagnosticReport') ); expect(result[0]).toHaveProperty("diagnosticReportDataItems"); expect(result[0]).toHaveProperty("organizationDisplayDataProps"); @@ -803,7 +816,11 @@ describe("LabsService tests", () => { it("should properly count the number of labs", () => { const result = evaluateLabInfoData( BundleLab, - evaluateAll(BundleLab, fhirPathMappings.diagnosticReports), + fhirIndexBundleLab, + getResourcesByType( + fhirIndexBundleLab, + "DiagnosticReport" + ) ); const props = (result[0] as LabReportElementData) .organizationDisplayDataProps; From f5e8f035a0f2301553c1d8554572b9c8ce624002 Mon Sep 17 00:00:00 2001 From: angelathe Date: Tue, 24 Mar 2026 16:26:01 -0400 Subject: [PATCH 04/14] more cleanup, remove direct lookups on fhirIndex by adding another helper function --- .../src/app/utils/evaluate/index.ts | 7 +-- .../ecr-viewer/src/app/view-data/page.tsx | 1 + .../services/fhirResourcesIndexService.tsx | 58 ++++++++++++------- .../app/view-data/services/labsService.tsx | 17 ++---- 4 files changed, 47 insertions(+), 36 deletions(-) diff --git a/containers/ecr-viewer/src/app/utils/evaluate/index.ts b/containers/ecr-viewer/src/app/utils/evaluate/index.ts index 8e20ebe96..0e2254fed 100644 --- a/containers/ecr-viewer/src/app/utils/evaluate/index.ts +++ b/containers/ecr-viewer/src/app/utils/evaluate/index.ts @@ -30,7 +30,7 @@ import { import { notEmpty } from "@/app/utils/data-utils"; import fhirPathMappings, { PathTypes, ValueX, FhirPath } from "./fhir-paths"; -import { FhirIndex } from "@/app/view-data/services/fhirResourcesIndexService"; +import { FhirIndex, getResourceById } from "@/app/view-data/services/fhirResourcesIndexService"; // TODO: Follow up on FHIR/fhirpath typing // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -353,9 +353,8 @@ export const evaluateReference2 = ( if (!ref) return undefined; const [resourceType, id] = ref.split("/"); - const result = fhirIndex.fhirResourcesById[id]; - // TODO ANGELA: Add type checking - + const result = getResourceById(fhirIndex, resourceType as T["resourceType"], id); + if (result && result?.resourceType !== resourceType) { console.error( `Resource type mismatch: Expected ${resourceType}, but got ${result?.resourceType}` diff --git a/containers/ecr-viewer/src/app/view-data/page.tsx b/containers/ecr-viewer/src/app/view-data/page.tsx index 6aa66e694..62c1cca3d 100644 --- a/containers/ecr-viewer/src/app/view-data/page.tsx +++ b/containers/ecr-viewer/src/app/view-data/page.tsx @@ -25,6 +25,7 @@ import { evaluateEcrSummaryConditionSummary, evaluateEcrSummaryEncounterDetails, * @param params.searchParams searchParams for page * @returns The main eCR Viewer JSX component. */ +// TODO ANGELA: Remove performance.now() calls const ECRViewerPage = async ({ searchParams, }: { diff --git a/containers/ecr-viewer/src/app/view-data/services/fhirResourcesIndexService.tsx b/containers/ecr-viewer/src/app/view-data/services/fhirResourcesIndexService.tsx index 82b9829a1..9a0113034 100644 --- a/containers/ecr-viewer/src/app/view-data/services/fhirResourcesIndexService.tsx +++ b/containers/ecr-viewer/src/app/view-data/services/fhirResourcesIndexService.tsx @@ -1,18 +1,19 @@ import { Bundle, Resource } from "fhir/r4"; -export type ResourceType = Resource["resourceType"]; -export type FhirResourceByTypeIndex = { - // This says: If the key is 'Patient', the value is Record - [K in ResourceType]?: Record>; +type ResourceType = Resource["resourceType"]; +type ResourceWithType = Resource & { resourceType: K }; + +export type FhirIndexByType = { + [K in ResourceType]?: ResourceWithType[]; }; -export interface FhirResourceByIdIndex { - [id: string]: Resource -} +export type FhirIndexByTypeAndId = { + [K in ResourceType]?: Record>; +}; export interface FhirIndex { - fhirResourcesByType: FhirResourceByTypeIndex; - fhirResourcesById: FhirResourceByIdIndex; + fhirIndexByType: FhirIndexByType; + fhirIndexByTypeAndId: FhirIndexByTypeAndId; } /** @@ -22,8 +23,8 @@ export interface FhirIndex { * If no matching observations are found, an empty object is returned. */ export const getFhirIndex = (fhirBundle: Bundle): FhirIndex => { - const fhirResourcesByType: FhirResourceByTypeIndex = {}; - const fhirResourcesById: FhirResourceByIdIndex = {}; + const fhirIndexByType: FhirIndexByType = {}; + const fhirIndexByTypeAndId: FhirIndexByTypeAndId = {}; fhirBundle.entry?.forEach((entry) => { const resource = entry.resource; @@ -31,28 +32,45 @@ export const getFhirIndex = (fhirBundle: Bundle): FhirIndex => { const resourceId = resource?.id; if (resourceType && resourceId) { - fhirResourcesById[resourceId] = resource; + // by Type (array) + fhirIndexByType[resourceType] ??= []; + fhirIndexByType[resourceType].push(resource); - fhirResourcesByType[resourceType] ??= {}; - fhirResourcesByType[resourceType][resourceId] = resource; + // by Type and ID (map) + fhirIndexByTypeAndId[resourceType] ??= {}; + fhirIndexByTypeAndId[resourceType][resourceId] = resource; } }); - return { fhirResourcesByType, fhirResourcesById }; + return { fhirIndexByType, fhirIndexByTypeAndId }; }; /** - * Grabs all resources of a specific type. + * Returns array of all resources of a specific type. */ +// TODO ANGELA: ADD TESTS export function getResourcesByType( fhirIndex: FhirIndex, type: T["resourceType"] ): T[] { - const resourceMap = fhirIndex.fhirResourcesByType[type]; + const resourceMap = fhirIndex.fhirIndexByType[type]; if (!resourceMap) return []; - // We cast to 'any' internally to satisfy the complex mapped type, - // but the external return type is perfectly narrowed to T[] - return Object.values(resourceMap ?? {}) as T[]; + return resourceMap as T[]; +} + +/** + * Returns FHIR resource by ID and check resource type. + */ +// TODO ANGELA: ADD TESTS. Returns undefined if the ID doesn’t exist. Returns undefined if the ID exists but is the wrong resourceType. +export function getResourceById( + fhirIndex: FhirIndex, + type: T["resourceType"], + id: string +): T | undefined { + + const resource = fhirIndex.fhirIndexByTypeAndId[type]?.[id]; + if (resource?.resourceType === type) return resource as T; + return undefined; } \ No newline at end of file diff --git a/containers/ecr-viewer/src/app/view-data/services/labsService.tsx b/containers/ecr-viewer/src/app/view-data/services/labsService.tsx index 596c70f1e..9aa939a09 100644 --- a/containers/ecr-viewer/src/app/view-data/services/labsService.tsx +++ b/containers/ecr-viewer/src/app/view-data/services/labsService.tsx @@ -55,7 +55,7 @@ import EvaluateTable, { } from "@/app/view-data/components/EvaluateTable"; import { FieldValue } from "@/app/view-data/components/FieldValue"; import { sortResourcesByDate } from "@/app/view-data/utils/fhir-data-utils"; -import { FhirIndex, getResourcesByType } from "@/app/view-data/services/fhirResourcesIndexService"; +import { FhirIndex, getResourceById, getResourcesByType } from "@/app/view-data/services/fhirResourcesIndexService"; export interface ResultObject { [key: string]: AccordionItem[]; @@ -90,7 +90,7 @@ export const evaluateLabInfoData = ( labReports: DiagnosticReport[], accordionHeadingLevel: HeadingLevel = "h5" ): LabReportElementData[] => { - + // the keys are the organization id, the value is an array of jsx elements of diagnostic reports let organizationItems: ResultObject = {}; const jsonLabs = getAllLabJsonObjects(fhirIndex); @@ -101,12 +101,7 @@ export const evaluateLabInfoData = ( const labReportJson = getJsonLab(jsonLabs, obs); ensureReportHasDateTime(report, obs); - const content = getLabsContent( - report, - obs, - fhirIndex, - labReportJson - ); + const content = getLabsContent(report, obs, fhirIndex, labReportJson); const organizationId = (report.performer?.[0].reference ?? "").replace( "Organization/", "" @@ -143,7 +138,7 @@ export const evaluateLabInfoData = ( organizationItems = groupItemByOrgId( organizationItems, organizationId, - item, + item ); } @@ -166,7 +161,7 @@ export const getObservations = ( .map((obsRef) => { if (obsRef.reference) { const [_, id] = obsRef.reference.split("/"); - return fhirIndex.fhirResourcesById[id] as Observation; + return getResourceById(fhirIndex, "Observation", id) } }) .filter(notEmpty); @@ -259,8 +254,6 @@ export const getAllLabJsonObjects = (fhirIndex: FhirIndex): HtmlTableJson[] => { compositionLabs, fhirPathMappings.labResultDiv ); - // const labsString = evaluateValue(fhirBundle, fhirPathMappings.labResultDiv); - // console.log(labsString); return formatTablesToJSON(labsString); }; From f94a03bcb6eb1f568f39007c86476bbcc88bde25 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 20:27:54 +0000 Subject: [PATCH 05/14] [pre-commit.ci] auto fixes from pre-commit hooks --- .../src/app/utils/evaluate/index.ts | 17 ++++-- .../EcrDocument/accordion-items.tsx | 26 ++++---- .../app/view-data/components/EcrSummary.tsx | 11 ++-- .../ecr-viewer/src/app/view-data/page.tsx | 28 +++++---- .../view-data/services/ecrSummaryService.tsx | 29 +++++---- .../services/fhirResourcesIndexService.tsx | 9 ++- .../app/view-data/services/labsService.tsx | 35 +++++++---- .../EcrDocument/EcrDocument.test.tsx | 2 +- .../app/view-data/components/LabInfo.test.tsx | 16 +++-- .../services/ecrSummaryService.test.tsx | 35 ++++++----- .../view-data/services/labsService.test.tsx | 60 ++++++++++++------- 11 files changed, 164 insertions(+), 104 deletions(-) diff --git a/containers/ecr-viewer/src/app/utils/evaluate/index.ts b/containers/ecr-viewer/src/app/utils/evaluate/index.ts index 0e2254fed..2a5f257b0 100644 --- a/containers/ecr-viewer/src/app/utils/evaluate/index.ts +++ b/containers/ecr-viewer/src/app/utils/evaluate/index.ts @@ -30,7 +30,10 @@ import { import { notEmpty } from "@/app/utils/data-utils"; import fhirPathMappings, { PathTypes, ValueX, FhirPath } from "./fhir-paths"; -import { FhirIndex, getResourceById } from "@/app/view-data/services/fhirResourcesIndexService"; +import { + FhirIndex, + getResourceById, +} from "@/app/view-data/services/fhirResourcesIndexService"; // TODO: Follow up on FHIR/fhirpath typing // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -345,7 +348,7 @@ export const evaluateReference = ( // TODO ANGELA: Add JSdoc export const evaluateReference2 = ( fhirIndex: FhirIndex, - ref?: string | Reference + ref?: string | Reference, ): T | undefined => { if (typeof ref !== "string") { ref = formatReference(ref); @@ -353,11 +356,15 @@ export const evaluateReference2 = ( if (!ref) return undefined; const [resourceType, id] = ref.split("/"); - const result = getResourceById(fhirIndex, resourceType as T["resourceType"], id); - + const result = getResourceById( + fhirIndex, + resourceType as T["resourceType"], + id, + ); + if (result && result?.resourceType !== resourceType) { console.error( - `Resource type mismatch: Expected ${resourceType}, but got ${result?.resourceType}` + `Resource type mismatch: Expected ${resourceType}, but got ${result?.resourceType}`, ); } diff --git a/containers/ecr-viewer/src/app/view-data/components/EcrDocument/accordion-items.tsx b/containers/ecr-viewer/src/app/view-data/components/EcrDocument/accordion-items.tsx index c4979d6e4..d05b1eb48 100644 --- a/containers/ecr-viewer/src/app/view-data/components/EcrDocument/accordion-items.tsx +++ b/containers/ecr-viewer/src/app/view-data/components/EcrDocument/accordion-items.tsx @@ -23,7 +23,10 @@ import { evaluatePregnancyData, } from "@/app/view-data/services/evaluateFhirDataService"; import { evaluateLabInfoData } from "@/app/view-data/services/labsService"; -import { FhirIndex, getResourcesByType } from "@/app/view-data/services/fhirResourcesIndexService"; +import { + FhirIndex, + getResourcesByType, +} from "@/app/view-data/services/fhirResourcesIndexService"; import { evaluateClinicalData } from "./clinical-data"; @@ -35,7 +38,7 @@ import { evaluateClinicalData } from "./clinical-data"; */ export const getEcrDocumentAccordionItems = ( fhirBundle: Bundle, - fhirIndex: FhirIndex + fhirIndex: FhirIndex, ): AccordionItem[] => { const demographicsData = evaluateDemographicsData(fhirBundle); const socialData = evaluateSocialData(fhirBundle); @@ -46,13 +49,16 @@ export const getEcrDocumentAccordionItems = ( const clinicalData = evaluateClinicalData(fhirBundle); const ecrMetadata = evaluateEcrMetadata(fhirBundle); const facilityData = evaluateFacilityData(fhirBundle); - const diagnosticReports = getResourcesByType(fhirIndex, 'DiagnosticReport') + const diagnosticReports = getResourcesByType( + fhirIndex, + "DiagnosticReport", + ); const labInfoData = evaluateLabInfoData( fhirBundle, fhirIndex, - diagnosticReports + diagnosticReports, ); - + const hasUnavailableData = () => { const unavailableDataArrays = [ demographicsData.unavailableData, @@ -73,7 +79,7 @@ export const getEcrDocumentAccordionItems = ( ecrMetadata.eicrAuthorDetails.map((details) => details.unavailableData), ]; return unavailableDataArrays.some( - (array) => Array.isArray(array) && array.length > 0 + (array) => Array.isArray(array) && array.length > 0, ); }; @@ -127,7 +133,7 @@ export const getEcrDocumentAccordionItems = ( { title: "Clinical Info", content: Object.values(clinicalData).some( - (section) => section.availableData.length > 0 + (section) => section.availableData.length > 0, ) ? ( 0 || ecrMetadata.eicrAuthorDetails.find( - (authorDetails) => authorDetails.availableData.length > 0 + (authorDetails) => authorDetails.availableData.length > 0, ) || ecrMetadata.ecrCustodianDetails.availableData.length > 0 ? ( authorDetails.unavailableData + (authorDetails) => authorDetails.unavailableData, )} /> ) : ( @@ -250,6 +256,6 @@ export const getEcrDocumentAccordionItems = ( headingLevel: "h3", }; }); - + return accordionItems; }; diff --git a/containers/ecr-viewer/src/app/view-data/components/EcrSummary.tsx b/containers/ecr-viewer/src/app/view-data/components/EcrSummary.tsx index 5f8b4c90e..a7fce85e5 100644 --- a/containers/ecr-viewer/src/app/view-data/components/EcrSummary.tsx +++ b/containers/ecr-viewer/src/app/view-data/components/EcrSummary.tsx @@ -6,7 +6,11 @@ import { AccordionItem } from "@/app/types"; import { toKebabCase } from "@/app/utils/format-utils"; import { DataDisplay, DataTableDisplay, DisplayDataProps } from "./DataDisplay"; -import { evaluateEcrSummaryConditionSummary, evaluateEcrSummaryEncounterDetails, evaluateEcrSummaryPatientDetails } from "@/app/view-data/services/ecrSummaryService"; +import { + evaluateEcrSummaryConditionSummary, + evaluateEcrSummaryEncounterDetails, + evaluateEcrSummaryPatientDetails, +} from "@/app/view-data/services/ecrSummaryService"; import { Bundle } from "fhir/r4"; import { FhirIndex } from "@/app/view-data/services/fhirResourcesIndexService"; @@ -39,9 +43,8 @@ const EcrSummary: React.FC = ({ patientDetails, encounterDetails, conditionSummary, - snomed + snomed, }) => { - const conditionSummaryAccordionItems: AccordionItem[] = conditionSummary.map( (condition) => { const hasImmunizationDetails = condition.immunizationDetails.length > 0; @@ -104,7 +107,7 @@ const EcrSummary: React.FC = ({ }; }, ); - + return (
; }) => { - const t0 = performance.now() + const t0 = performance.now(); try { const _searchParams = await searchParams; const fhirId = _searchParams.id ?? ""; const snomedCode = _searchParams["snomed-code"] ?? ""; - + const user = await getLoggedInUserSession(); // If we have a user that means we're using IDP auth and not NBS Auth, so we // need to check if they're authorized to view this eCR @@ -44,7 +48,7 @@ const ECRViewerPage = async ({ const authed = await isLoggedInUserEcrAuthed(fhirId); if (!authed) notFound(); } - + const resp = await getFhirData({ eicr_id: fhirId }); if (!isSuccessResponse(resp)) { if (resp.status === 404) { @@ -59,16 +63,16 @@ const ECRViewerPage = async ({ ); } } - + const fhirBundle = resp.payload.fhirBundle; const fhirIndex = getFhirIndex(fhirBundle); const patientName = evaluatePatientName(fhirBundle, true); const patientDOB = evaluatePatientDOB(fhirBundle); - - const t2 = performance.now() + + const t2 = performance.now(); const accordionItems = getEcrDocumentAccordionItems(fhirBundle, fhirIndex); - const t3 = performance.now() - console.log("Time to run getEcrDocumentAccordionItems: ", t3 - t2 ); + const t3 = performance.now(); + console.log("Time to run getEcrDocumentAccordionItems: ", t3 - t2); return ( @@ -92,7 +96,7 @@ const ECRViewerPage = async ({ conditionSummary={evaluateEcrSummaryConditionSummary( fhirBundle, fhirIndex, - snomedCode + snomedCode, )} snomed={snomedCode} /> @@ -101,8 +105,8 @@ const ECRViewerPage = async ({ ); } finally { - const t1 = performance.now() - console.log("Time to run EcrViewerPage: ", t1 - t0 ); + const t1 = performance.now(); + console.log("Time to run EcrViewerPage: ", t1 - t0); } }; diff --git a/containers/ecr-viewer/src/app/view-data/services/ecrSummaryService.tsx b/containers/ecr-viewer/src/app/view-data/services/ecrSummaryService.tsx index fd788f022..9077d1894 100644 --- a/containers/ecr-viewer/src/app/view-data/services/ecrSummaryService.tsx +++ b/containers/ecr-viewer/src/app/view-data/services/ecrSummaryService.tsx @@ -163,7 +163,7 @@ export const evaluateEcrSummaryEncounterDetails = (fhirBundle: Bundle) => { export const evaluateEcrSummaryConditionSummary = ( fhirBundle: Bundle, fhirIndex: FhirIndex, - snomedCode?: string + snomedCode?: string, ): ConditionSummary[] => { const rrConditions = evaluateAll(fhirBundle, fhirPathMappings.rrConditions); const conditionsList: { @@ -171,7 +171,7 @@ export const evaluateEcrSummaryConditionSummary = ( } = {}; for (const observation of rrConditions) { const coding = observation?.valueCodeableConcept?.coding?.find( - (coding) => coding.system === "http://snomed.info/sct" + (coding) => coding.system === "http://snomed.info/sct", ); const displayText = @@ -190,12 +190,12 @@ export const evaluateEcrSummaryConditionSummary = ( observation?.hasMember?.forEach((ref) => { const rrInfoObs: Observation | undefined = evaluateReference( fhirBundle, - ref.reference + ref.reference, ); const { rules } = getReportabilityRulesReasons(rrInfoObs); rules.forEach((rule: string) => - conditionsList[conditionListKey].ruleSummaries.add(rule) + conditionsList[conditionListKey].ruleSummaries.add(rule), ); }); } @@ -215,7 +215,7 @@ export const evaluateEcrSummaryConditionSummary = ( {[...conditionsList[conditionsListKey].ruleSummaries].map( (summary) => (

{summary}

- ) + ), )}
), @@ -227,13 +227,13 @@ export const evaluateEcrSummaryConditionSummary = ( ), clinicalDetails: evaluateEcrSummaryRelevantClinicalDetails( fhirBundle, - conditionsListKey + conditionsListKey, ), labDetails: evaluateEcrSummaryRelevantLabResults( fhirBundle, fhirIndex, conditionsListKey, - false + false, ), }; @@ -302,7 +302,7 @@ export const evaluateEcrSummaryRelevantLabResults = ( fhirBundle: Bundle, fhirIndex: FhirIndex, snomedCode: string, - lastDividerLine: boolean = true + lastDividerLine: boolean = true, ): DisplayDataProps[] => { let resultsArray: DisplayDataProps[] = []; @@ -310,16 +310,19 @@ export const evaluateEcrSummaryRelevantLabResults = ( return []; } - const labReports = getResourcesByType(fhirIndex, 'DiagnosticReport'); + const labReports = getResourcesByType( + fhirIndex, + "DiagnosticReport", + ); const labsWithCode = getRelevantResources(labReports, snomedCode); const labsWithCodeIds = new Set(labsWithCode.map((lab) => lab.id)); const observationsList = getResourcesByType( fhirIndex, - "Observation" + "Observation", ); const relevantObsIds = new Set( - getRelevantResources(observationsList, snomedCode).map((entry) => entry.id) + getRelevantResources(observationsList, snomedCode).map((entry) => entry.id), ); const labsFromObsWithCode = labReports.filter((lab) => { @@ -345,14 +348,14 @@ export const evaluateEcrSummaryRelevantLabResults = ( fhirBundle, fhirIndex, relevantLabs, - "h4" + "h4", ); resultsArray = relevantLabElements.flatMap((element) => element.diagnosticReportDataItems.map((reportItem) => ({ value: , dividerLine: false, - })) + })), ); if (lastDividerLine) { diff --git a/containers/ecr-viewer/src/app/view-data/services/fhirResourcesIndexService.tsx b/containers/ecr-viewer/src/app/view-data/services/fhirResourcesIndexService.tsx index 9a0113034..4fb54aa2a 100644 --- a/containers/ecr-viewer/src/app/view-data/services/fhirResourcesIndexService.tsx +++ b/containers/ecr-viewer/src/app/view-data/services/fhirResourcesIndexService.tsx @@ -51,10 +51,10 @@ export const getFhirIndex = (fhirBundle: Bundle): FhirIndex => { // TODO ANGELA: ADD TESTS export function getResourcesByType( fhirIndex: FhirIndex, - type: T["resourceType"] + type: T["resourceType"], ): T[] { const resourceMap = fhirIndex.fhirIndexByType[type]; - + if (!resourceMap) return []; return resourceMap as T[]; @@ -67,10 +67,9 @@ export function getResourcesByType( export function getResourceById( fhirIndex: FhirIndex, type: T["resourceType"], - id: string + id: string, ): T | undefined { - const resource = fhirIndex.fhirIndexByTypeAndId[type]?.[id]; if (resource?.resourceType === type) return resource as T; return undefined; -} \ No newline at end of file +} diff --git a/containers/ecr-viewer/src/app/view-data/services/labsService.tsx b/containers/ecr-viewer/src/app/view-data/services/labsService.tsx index 9aa939a09..ad3a58954 100644 --- a/containers/ecr-viewer/src/app/view-data/services/labsService.tsx +++ b/containers/ecr-viewer/src/app/view-data/services/labsService.tsx @@ -55,7 +55,11 @@ import EvaluateTable, { } from "@/app/view-data/components/EvaluateTable"; import { FieldValue } from "@/app/view-data/components/FieldValue"; import { sortResourcesByDate } from "@/app/view-data/utils/fhir-data-utils"; -import { FhirIndex, getResourceById, getResourcesByType } from "@/app/view-data/services/fhirResourcesIndexService"; +import { + FhirIndex, + getResourceById, + getResourcesByType, +} from "@/app/view-data/services/fhirResourcesIndexService"; export interface ResultObject { [key: string]: AccordionItem[]; @@ -88,7 +92,7 @@ export const evaluateLabInfoData = ( fhirBundle: Bundle, fhirIndex: FhirIndex, labReports: DiagnosticReport[], - accordionHeadingLevel: HeadingLevel = "h5" + accordionHeadingLevel: HeadingLevel = "h5", ): LabReportElementData[] => { // the keys are the organization id, the value is an array of jsx elements of diagnostic reports let organizationItems: ResultObject = {}; @@ -104,7 +108,7 @@ export const evaluateLabInfoData = ( const content = getLabsContent(report, obs, fhirIndex, labReportJson); const organizationId = (report.performer?.[0].reference ?? "").replace( "Organization/", - "" + "", ); const title = formatCodeableConcept(report.code) ?? "Unknown"; @@ -138,7 +142,7 @@ export const evaluateLabInfoData = ( organizationItems = groupItemByOrgId( organizationItems, organizationId, - item + item, ); } @@ -161,7 +165,7 @@ export const getObservations = ( .map((obsRef) => { if (obsRef.reference) { const [_, id] = obsRef.reference.split("/"); - return getResourceById(fhirIndex, "Observation", id) + return getResourceById(fhirIndex, "Observation", id); } }) .filter(notEmpty); @@ -171,7 +175,6 @@ export const getObservations = ( } }; - const ensureReportHasDateTime = ( report: DiagnosticReport, obs: Observation[], @@ -249,10 +252,13 @@ export const getJsonLab = ( */ export const getAllLabJsonObjects = (fhirIndex: FhirIndex): HtmlTableJson[] => { // Get lab reports HTML String (for all lab reports) & convert to JSON - const compositionLabs = getResourcesByType(fhirIndex, 'Composition')[0]; + const compositionLabs = getResourcesByType( + fhirIndex, + "Composition", + )[0]; const labsString = evaluateValue( compositionLabs, - fhirPathMappings.labResultDiv + fhirPathMappings.labResultDiv, ); return formatTablesToJSON(labsString); }; @@ -611,7 +617,10 @@ export const evaluateLabOrganizationData = ( fhirIndex: FhirIndex, labReportCount: number, ) => { - const orgMappings = getResourcesByType(fhirIndex, 'Organization'); + const orgMappings = getResourcesByType( + fhirIndex, + "Organization", + ); let matchingOrg: Organization = orgMappings.filter( (organization) => organization.id === id, )[0]; @@ -747,7 +756,7 @@ const getLabsContent = ( evaluateValue(labSpecimen, fhirPathMappings.specimenBodySite) || returnFieldValueFromLabHtmlString( labReportJson, - "Anatomical Location / Laterality" + "Anatomical Location / Laterality", ), className: "lab-text-content", }, @@ -755,7 +764,7 @@ const getLabsContent = ( title: "Collection Method/Volume", value: returnFieldValueFromLabHtmlString( labReportJson, - "Collection Method / Volume" + "Collection Method / Volume", ), className: "lab-text-content", }, @@ -763,7 +772,7 @@ const getLabsContent = ( title: "Resulting Agency Comment", value: returnFieldValueFromLabHtmlString( labReportJson, - "Resulting Agency Comment" + "Resulting Agency Comment", ), className: "lab-text-content", }, @@ -771,7 +780,7 @@ const getLabsContent = ( title: "Authorizing Provider", value: returnFieldValueFromLabHtmlString( labReportJson, - "Authorizing Provider" + "Authorizing Provider", ), className: "lab-text-content", }, diff --git a/containers/ecr-viewer/tests/unit/app/view-data/components/EcrDocument/EcrDocument.test.tsx b/containers/ecr-viewer/tests/unit/app/view-data/components/EcrDocument/EcrDocument.test.tsx index a35d045ec..54d72ddef 100644 --- a/containers/ecr-viewer/tests/unit/app/view-data/components/EcrDocument/EcrDocument.test.tsx +++ b/containers/ecr-viewer/tests/unit/app/view-data/components/EcrDocument/EcrDocument.test.tsx @@ -32,7 +32,7 @@ describe("Snapshot test for eCR Document", () => { const items = getEcrDocumentAccordionItems( bundleEmpty, - fhirIndexBundleEmpty + fhirIndexBundleEmpty, ); const { container } = render(); diff --git a/containers/ecr-viewer/tests/unit/app/view-data/components/LabInfo.test.tsx b/containers/ecr-viewer/tests/unit/app/view-data/components/LabInfo.test.tsx index 825be94dd..a9a418444 100644 --- a/containers/ecr-viewer/tests/unit/app/view-data/components/LabInfo.test.tsx +++ b/containers/ecr-viewer/tests/unit/app/view-data/components/LabInfo.test.tsx @@ -13,7 +13,10 @@ import { evaluateLabInfoData, LabReportElementData, } from "@/app/view-data/services/labsService"; -import { getFhirIndex, getResourcesByType } from "@/app/view-data/services/fhirResourcesIndexService"; +import { + getFhirIndex, + getResourcesByType, +} from "@/app/view-data/services/fhirResourcesIndexService"; const BundleLab = _BundleLab as unknown as Bundle; const fhirIndexBundleLab = getFhirIndex(BundleLab); @@ -24,11 +27,14 @@ describe("LabInfo", () => { describe("when labResults is LabReportElementData[]", () => { let labInfoJsx: React.ReactElement; beforeAll(() => { - const diagnosticReports = getResourcesByType(fhirIndexBundleLab, "DiagnosticReport"); + const diagnosticReports = getResourcesByType( + fhirIndexBundleLab, + "DiagnosticReport", + ); const labInfoOrg = evaluateLabInfoData( BundleLab, fhirIndexBundleLab, - diagnosticReports + diagnosticReports, ) as LabReportElementData[]; // Empty out one of the lab names for testing @@ -112,12 +118,12 @@ describe("LabInfo", () => { beforeAll(() => { const diagnosticReports = getResourcesByType( fhirIndexBundleLabNoLabIds, - "DiagnosticReport" + "DiagnosticReport", ); labInfo = evaluateLabInfoData( BundleLabNoLabIds, fhirIndexBundleLabNoLabIds, - diagnosticReports + diagnosticReports, ); }); it("should be collapsed by default", () => { diff --git a/containers/ecr-viewer/tests/unit/app/view-data/services/ecrSummaryService.test.tsx b/containers/ecr-viewer/tests/unit/app/view-data/services/ecrSummaryService.test.tsx index 9aebbb516..df2635398 100644 --- a/containers/ecr-viewer/tests/unit/app/view-data/services/ecrSummaryService.test.tsx +++ b/containers/ecr-viewer/tests/unit/app/view-data/services/ecrSummaryService.test.tsx @@ -14,7 +14,10 @@ import { evaluateEcrSummaryRelevantClinicalDetails, evaluateEcrSummaryRelevantLabResults, } from "@/app/view-data/services/ecrSummaryService"; -import { FhirIndex, getFhirIndex } from "@/app/view-data/services/fhirResourcesIndexService"; +import { + FhirIndex, + getFhirIndex, +} from "@/app/view-data/services/fhirResourcesIndexService"; const BundleLab = _BundleLab as unknown as Bundle; const fhirIndexBundleLab = getFhirIndex(BundleLab); @@ -22,9 +25,10 @@ const fhirIndexBundleLab = getFhirIndex(BundleLab); const BundleEcrSummary = _BundleEcrSummary as unknown as Bundle; const fhirIndexBundleEcrSummary = getFhirIndex(BundleEcrSummary); -const BundleRRConditionValueString = _BundleRRConditionValueString as unknown as Bundle; +const BundleRRConditionValueString = + _BundleRRConditionValueString as unknown as Bundle; const fhirIndexBundleRRConditionValueString = getFhirIndex( - BundleRRConditionValueString + BundleRRConditionValueString, ); describe("ecrSummaryService Tests", () => { @@ -71,7 +75,7 @@ describe("ecrSummaryService Tests", () => { const actual = evaluateEcrSummaryRelevantLabResults( BundleLab, fhirIndexBundleLab, - "" + "", ); expect(actual).toBeEmpty(); @@ -81,7 +85,7 @@ describe("ecrSummaryService Tests", () => { const actual = evaluateEcrSummaryRelevantLabResults( BundleLab, fhirIndexBundleLab, - "invalid-snomed-code" + "invalid-snomed-code", ); expect(actual).toBeEmpty(); @@ -124,7 +128,7 @@ describe("ecrSummaryService Tests", () => { it("should return titles based on snomed code, and return human-readable name if available", () => { const actual = evaluateEcrSummaryConditionSummary( BundleEcrSummary, - fhirIndexBundleEcrSummary + fhirIndexBundleEcrSummary, ); expect(actual[0].title).toEqual("Hepatitis C"); @@ -135,7 +139,7 @@ describe("ecrSummaryService Tests", () => { it("should return human-readable name if available", () => { const actual = evaluateEcrSummaryConditionSummary( BundleRRConditionValueString, - fhirIndexBundleRRConditionValueString + fhirIndexBundleRRConditionValueString, ); expect(actual[0].title).toEqual("COVID"); @@ -143,7 +147,7 @@ describe("ecrSummaryService Tests", () => { it("should return summaries based on snomed code", () => { const actual = evaluateEcrSummaryConditionSummary( BundleEcrSummary, - fhirIndexBundleEcrSummary + fhirIndexBundleEcrSummary, ); render( actual[1].conditionDetails.map((detail, i) => ( @@ -168,7 +172,7 @@ describe("ecrSummaryService Tests", () => { it("should return clinical details based on snomed code", () => { const actual = evaluateEcrSummaryConditionSummary( BundleEcrSummary, - fhirIndexBundleEcrSummary + fhirIndexBundleEcrSummary, ); render( @@ -182,7 +186,7 @@ describe("ecrSummaryService Tests", () => { it("should return lab details based on snomed code", () => { const actual = evaluateEcrSummaryConditionSummary( BundleEcrSummary, - fhirIndexBundleEcrSummary + fhirIndexBundleEcrSummary, ); render( @@ -243,11 +247,11 @@ describe("ecrSummaryService Tests", () => { ], } as unknown as Bundle; const fhirIndexBundleNonRelatedImmuns = getFhirIndex( - BundleNonRelatedImmuns + BundleNonRelatedImmuns, ); const actual = evaluateEcrSummaryConditionSummary( BundleNonRelatedImmuns, - fhirIndexBundleNonRelatedImmuns + fhirIndexBundleNonRelatedImmuns, ); render( actual[1].immunizationDetails.map((detail) => ( @@ -259,14 +263,17 @@ describe("ecrSummaryService Tests", () => { expect(screen.queryByText("anthrax")).not.toBeInTheDocument(); }); it("should return empty array if none found", () => { - const actual = evaluateEcrSummaryConditionSummary({} as Bundle, {} as FhirIndex); + const actual = evaluateEcrSummaryConditionSummary( + {} as Bundle, + {} as FhirIndex, + ); expect(actual).toBeEmpty(); }); it("should return the the requested snomed first", () => { const verifyNotFirst = evaluateEcrSummaryConditionSummary( BundleEcrSummary, - fhirIndexBundleEcrSummary + fhirIndexBundleEcrSummary, ); expect(verifyNotFirst[0].title).not.toEqual( diff --git a/containers/ecr-viewer/tests/unit/app/view-data/services/labsService.test.tsx b/containers/ecr-viewer/tests/unit/app/view-data/services/labsService.test.tsx index b3994bca9..4be7eb800 100644 --- a/containers/ecr-viewer/tests/unit/app/view-data/services/labsService.test.tsx +++ b/containers/ecr-viewer/tests/unit/app/view-data/services/labsService.test.tsx @@ -31,7 +31,11 @@ import { getObservations, LabInterpretationTag, } from "@/app/view-data/services/labsService"; -import { FhirIndex, getFhirIndex, getResourcesByType } from "@/app/view-data/services/fhirResourcesIndexService"; +import { + FhirIndex, + getFhirIndex, + getResourcesByType, +} from "@/app/view-data/services/fhirResourcesIndexService"; const BundleLab = _BundleLab as unknown as Bundle; const fhirIndexBundleLab = getFhirIndex(BundleLab); @@ -39,7 +43,7 @@ const fhirIndexBundleLab = getFhirIndex(BundleLab); const BundleLabInvalidResultsDiv = _BundleLabInvalidResultsDiv as unknown as Bundle; const fhirIndexBundleLabInvalidResultsDiv = getFhirIndex( - BundleLabInvalidResultsDiv + BundleLabInvalidResultsDiv, ); const BundleLabNoLabIds = _BundleLabNoLabIds as unknown as Bundle; @@ -185,7 +189,7 @@ const labReportAbnormal = evaluateOneAndCheck( const jsonLabs = getAllLabJsonObjects(fhirIndexBundleLab); const labReportAbnormalJsonObject = getJsonLab( jsonLabs, - getObservations(labReportAbnormal!, fhirIndexBundleLab) + getObservations(labReportAbnormal!, fhirIndexBundleLab), ); const pathLabOrganismsTableAndNarr = @@ -211,7 +215,7 @@ describe("LabsService tests", () => { code: {}, status: "entered-in-error", }, - fhirIndexBundleLab + fhirIndexBundleLab, ); const expectedObservationPath = @@ -236,7 +240,7 @@ describe("LabsService tests", () => { code: {}, status: "final", }, - fhirIndexBundleLab + fhirIndexBundleLab, ); expect(result).toStrictEqual([]); }); @@ -249,7 +253,7 @@ describe("LabsService tests", () => { const jsonLabs = getAllLabJsonObjects(fhirIndexBundleLab); const result = getJsonLab( jsonLabs, - getObservations(labReportNormal!, fhirIndexBundleLab) + getObservations(labReportNormal!, fhirIndexBundleLab), ); expect(result).toEqual(expectedResult); @@ -265,7 +269,7 @@ describe("LabsService tests", () => { const jsonLabs = getAllLabJsonObjects(fhirIndexBundleLabNoLabIds); const result = getJsonLab( jsonLabs, - getObservations(labReportWithoutIds!, fhirIndexBundleLabNoLabIds) + getObservations(labReportWithoutIds!, fhirIndexBundleLabNoLabIds), ); expect(result).toBeUndefined(); @@ -275,7 +279,10 @@ describe("LabsService tests", () => { const jsonLabs = getAllLabJsonObjects(fhirIndexBundleLab); const result = getJsonLab( jsonLabs, - getObservations(labReportNormal!, fhirIndexBundleLabInvalidResultsDiv) + getObservations( + labReportNormal!, + fhirIndexBundleLabInvalidResultsDiv, + ), ); expect(result).toBeUndefined(); @@ -493,10 +500,10 @@ describe("LabsService tests", () => { + />, ); const tagElement = screen.getByText("Abnormal"); expect(tagElement).toBeInTheDocument(); @@ -671,7 +678,7 @@ describe("LabsService tests", () => { describe("evaluateOrganismsReportData", () => { it("should return the correct organisms table when the data exists for a lab report", () => { const result = evaluateOrganismsReportData( - getObservations(labOrganismsTableAndNarr!, fhirIndexBundleLab) + getObservations(labOrganismsTableAndNarr!, fhirIndexBundleLab), )!; render(result); @@ -683,7 +690,7 @@ describe("LabsService tests", () => { }); it("should return undefined if lab organisms data does not exist for a lab report", () => { const result = evaluateOrganismsReportData( - getObservations(labReportNormal!, fhirIndexBundleLab) + getObservations(labReportNormal!, fhirIndexBundleLab), ); expect(result).toBeUndefined(); @@ -692,10 +699,13 @@ describe("LabsService tests", () => { describe("Evaluate Diagnostic Report", () => { it("should evaluate diagnostic report results", () => { - const report = getResourcesByType(fhirIndexBundleLab, 'DiagnosticReport')[0]; + const report = getResourcesByType( + fhirIndexBundleLab, + "DiagnosticReport", + )[0]; const actual = evaluateDiagnosticReportData( getObservations(report, fhirIndexBundleLab), - fhirIndexBundleLab + fhirIndexBundleLab, ); render(actual); @@ -724,11 +734,11 @@ describe("LabsService tests", () => { it("should evaluate test method results", () => { const report = getResourcesByType( fhirIndexBundleLab, - "DiagnosticReport" + "DiagnosticReport", )[0]; const actual = evaluateDiagnosticReportData( getObservations(report, fhirIndexBundleLab), - fhirIndexBundleLab + fhirIndexBundleLab, ); render(actual); @@ -740,7 +750,7 @@ describe("LabsService tests", () => { it("should display comment", async () => { const report = getResourcesByType( fhirIndexBundleLab, - "DiagnosticReport" + "DiagnosticReport", )[2]; const actual = evaluateDiagnosticReportData( getObservations(report, fhirIndexBundleLab), @@ -786,7 +796,7 @@ describe("LabsService tests", () => { }; const result = combineOrgAndReportData( testResultObject, - fhirIndexBundleLab + fhirIndexBundleLab, ); expect(result[0].organizationDisplayDataProps).toBeArray(); }); @@ -797,7 +807,10 @@ describe("LabsService tests", () => { const result = evaluateLabInfoData( BundleLab, fhirIndexBundleLab, - getResourcesByType(fhirIndexBundleLab, 'DiagnosticReport') + getResourcesByType( + fhirIndexBundleLab, + "DiagnosticReport", + ), ); expect(result[0]).toHaveProperty("diagnosticReportDataItems"); expect(result[0]).toHaveProperty("organizationDisplayDataProps"); @@ -807,7 +820,10 @@ describe("LabsService tests", () => { const result = evaluateLabInfoData( BundleLabNoLabIds, fhirIndexBundleLabNoLabIds, - getResourcesByType(fhirIndexBundleLabNoLabIds, 'DiagnosticReport') + getResourcesByType( + fhirIndexBundleLabNoLabIds, + "DiagnosticReport", + ), ); expect(result[0]).toHaveProperty("diagnosticReportDataItems"); expect(result[0]).toHaveProperty("organizationDisplayDataProps"); @@ -819,8 +835,8 @@ describe("LabsService tests", () => { fhirIndexBundleLab, getResourcesByType( fhirIndexBundleLab, - "DiagnosticReport" - ) + "DiagnosticReport", + ), ); const props = (result[0] as LabReportElementData) .organizationDisplayDataProps; From fd386250bc756a4678a73c090885d8a078897c38 Mon Sep 17 00:00:00 2001 From: angelathe Date: Tue, 24 Mar 2026 19:40:53 -0400 Subject: [PATCH 06/14] cleanup, add jsdocs --- .../src/app/utils/evaluate/index.ts | 13 ++++++- .../services/fhirResourcesIndexService.tsx | 34 +++++++++++++++---- .../app/view-data/services/labsService.tsx | 1 - 3 files changed, 40 insertions(+), 8 deletions(-) diff --git a/containers/ecr-viewer/src/app/utils/evaluate/index.ts b/containers/ecr-viewer/src/app/utils/evaluate/index.ts index 0e2254fed..b62dc6f09 100644 --- a/containers/ecr-viewer/src/app/utils/evaluate/index.ts +++ b/containers/ecr-viewer/src/app/utils/evaluate/index.ts @@ -342,7 +342,18 @@ export const evaluateReference = ( return result; }; -// TODO ANGELA: Add JSdoc +/** + * Evaluates a reference to return a resource. The resulting type of the expected resource + * must be provided as a type parameter. This will also be checked at runtime and an + * error logged if it does not match. + * + * Expects a single element to be returned from the reference. + * + * @param fhirIndex - FHIR resources indexed by type & by ID + * @param ref - The reference string (e.g., "Patient/123"). + * @returns The FHIR Resource or undefined if not found. + */ +// TODO ANGELA: Eventually want this to replace evaluateReference completely export const evaluateReference2 = ( fhirIndex: FhirIndex, ref?: string | Reference diff --git a/containers/ecr-viewer/src/app/view-data/services/fhirResourcesIndexService.tsx b/containers/ecr-viewer/src/app/view-data/services/fhirResourcesIndexService.tsx index 9a0113034..43b3559da 100644 --- a/containers/ecr-viewer/src/app/view-data/services/fhirResourcesIndexService.tsx +++ b/containers/ecr-viewer/src/app/view-data/services/fhirResourcesIndexService.tsx @@ -17,10 +17,17 @@ export interface FhirIndex { } /** - * Extracts all lab `Observation` resources from a given FHIR bundle across all diagnostic reports. - * @param fhirBundle - The FHIR bundle containing related resources for the lab report. - * @returns An object of `Observation` resources with the observation `id` (string) as the key. - * If no matching observations are found, an empty object is returned. + * Builds an index of FHIR resources from a given FHIR bundle. + * + * Extracts all resources from a given FHIR bundle and organizes them into two maps: + * 1. `fhirIndexByType` – an map of resources keyed by `resourceType` mapping to an array of all resources of that type. + * 2. `fhirIndexByTypeAndId` – a map of resources keyed by `resourceType` and then by `id`. + * + * @param fhirBundle - FHIR bundle + * @returns A `FhirIndex` object containing: + * - `fhirIndexByType`: FHIR resources grouped by type as arrays. + * - `fhirIndexByTypeAndId`: FHIR resources grouped by type and ID for fast lookup. + * Both will return empty arrays/objects if no resources of a given type exist. */ export const getFhirIndex = (fhirBundle: Bundle): FhirIndex => { const fhirIndexByType: FhirIndexByType = {}; @@ -46,9 +53,15 @@ export const getFhirIndex = (fhirBundle: Bundle): FhirIndex => { }; /** - * Returns array of all resources of a specific type. + * Returns array of all resources of a specific type (i.e. "Observation"). + * + * @template T - The expected FHIR Resource type (e.g., Observation, Patient). + * @param fhirIndex - FHIR resources indexed by type & by ID + * @param type - The resourceType to retrieve (e.g., "Observation"). + * + * @returns Array of FHIR resources of type `T`, or empty array if + * no resources of specified type exist. */ -// TODO ANGELA: ADD TESTS export function getResourcesByType( fhirIndex: FhirIndex, type: T["resourceType"] @@ -62,6 +75,15 @@ export function getResourcesByType( /** * Returns FHIR resource by ID and check resource type. + * Expects only one resource to be returned. + * + * @template T - The expected FHIR Resource type (e.g., Observation, Patient). + * @param fhirIndex - FHIR resources indexed by type & by ID + * @param type - The resourceType to retrieve (e.g., "Observation"). + * @param id - The unique identifier of the resource. + * + * @returns FHIR resource of type `T` if it exists and resourceType matches `type` + * Returns undefined if no resource exists with given ID and resourceType */ // TODO ANGELA: ADD TESTS. Returns undefined if the ID doesn’t exist. Returns undefined if the ID exists but is the wrong resourceType. export function getResourceById( diff --git a/containers/ecr-viewer/src/app/view-data/services/labsService.tsx b/containers/ecr-viewer/src/app/view-data/services/labsService.tsx index 9aa939a09..565f0dc0c 100644 --- a/containers/ecr-viewer/src/app/view-data/services/labsService.tsx +++ b/containers/ecr-viewer/src/app/view-data/services/labsService.tsx @@ -481,7 +481,6 @@ export const evaluateDiagnosticReportData = ( columnName: "Test Method", infoPath: "observationDeviceReference", applyToValue: (ref) => { - // TODO ANGELA: review evaluateReference2 const device = evaluateReference2(fhirIndex, ref); return safeParse(device?.deviceName?.[0]?.name ?? ""); }, From 38d6e22c35b9983e5d6aebd524d4af0fc088281e Mon Sep 17 00:00:00 2001 From: angelathe Date: Tue, 24 Mar 2026 20:53:18 -0400 Subject: [PATCH 07/14] add tests --- .../services/fhirResourcesIndexService.tsx | 4 +- .../tests/unit/app/utils/evaluate.test.ts | 48 +++++++ .../fhirResourcesIndexService.test.tsx | 136 ++++++++++++++++++ 3 files changed, 187 insertions(+), 1 deletion(-) create mode 100644 containers/ecr-viewer/tests/unit/app/view-data/services/fhirResourcesIndexService.test.tsx diff --git a/containers/ecr-viewer/src/app/view-data/services/fhirResourcesIndexService.tsx b/containers/ecr-viewer/src/app/view-data/services/fhirResourcesIndexService.tsx index 43b3559da..950f3f909 100644 --- a/containers/ecr-viewer/src/app/view-data/services/fhirResourcesIndexService.tsx +++ b/containers/ecr-viewer/src/app/view-data/services/fhirResourcesIndexService.tsx @@ -62,6 +62,8 @@ export const getFhirIndex = (fhirBundle: Bundle): FhirIndex => { * @returns Array of FHIR resources of type `T`, or empty array if * no resources of specified type exist. */ +// TODO ANGELA: How to guarantee that type & `T` match? `T` only exists at compile, whereas type exists at `runtime` +// Is it enough to get this? Argument of type '"Composition"' is not assignable to parameter of type '"Observation"'. export function getResourcesByType( fhirIndex: FhirIndex, type: T["resourceType"] @@ -85,7 +87,7 @@ export function getResourcesByType( * @returns FHIR resource of type `T` if it exists and resourceType matches `type` * Returns undefined if no resource exists with given ID and resourceType */ -// TODO ANGELA: ADD TESTS. Returns undefined if the ID doesn’t exist. Returns undefined if the ID exists but is the wrong resourceType. +// TODO ANGELA: How to guarantee that type & `T` match? `T` only exists at compile, whereas type exists at `runtime` export function getResourceById( fhirIndex: FhirIndex, type: T["resourceType"], diff --git a/containers/ecr-viewer/tests/unit/app/utils/evaluate.test.ts b/containers/ecr-viewer/tests/unit/app/utils/evaluate.test.ts index 38df2eb54..5df6a214e 100644 --- a/containers/ecr-viewer/tests/unit/app/utils/evaluate.test.ts +++ b/containers/ecr-viewer/tests/unit/app/utils/evaluate.test.ts @@ -8,6 +8,7 @@ import { evaluateAllAndCheck, evaluateOne, evaluateReference, + evaluateReference2, evaluateValue, } from "@/app/utils/evaluate"; import fhirPathMappings from "@/app/utils/evaluate/fhir-paths"; @@ -321,3 +322,50 @@ describe("Evaluate Reference", () => { expect(actual?.resourceType).toEqual("Patient"); }); }); + +// TODO ANGELA: Rename? +describe("Evaluate Reference 2", () => { + const resource1 = { + fullUrl: "urn:uuid:1", + resource: { + resourceType: "Observation", + id: "1", + }, + }; + const resource2 = { + fullUrl: "urn:uuid:2", + resource: { + resourceType: "Observation", + id: "2", + }, + }; + const fhirIndexBundleSample = { + fhirIndexByType: { + Observation: [resource1.resource, resource2.resource], + }, + fhirIndexByTypeAndId: { + Observation: { + "1": resource1.resource, + "2": resource2.resource, + }, + }, + }; + + it("should return undefined if resource not found", () => { + const actual = evaluateReference2( + fhirIndexBundleSample, + "Observation/not-valid-id" + ); + + expect(actual).toBeUndefined(); + }); + it("should return the resource if the resource is available", () => { + const actual = evaluateReference2( + fhirIndexBundleSample, + "Observation/2" + ); + + expect(actual?.id).toEqual("2"); + expect(actual?.resourceType).toEqual("Observation"); + }); +}); diff --git a/containers/ecr-viewer/tests/unit/app/view-data/services/fhirResourcesIndexService.test.tsx b/containers/ecr-viewer/tests/unit/app/view-data/services/fhirResourcesIndexService.test.tsx new file mode 100644 index 000000000..46cd5a37c --- /dev/null +++ b/containers/ecr-viewer/tests/unit/app/view-data/services/fhirResourcesIndexService.test.tsx @@ -0,0 +1,136 @@ +import { Bundle, Composition, Observation, Patient } from "fhir/r4"; +import { getFhirIndex, getResourceById, getResourcesByType } from "@/app/view-data/services/fhirResourcesIndexService"; + + +const resource1 = { + fullUrl: "urn:uuid:1", + resource: { + resourceType: "Composition", + id: "1", + }, +}; +const resource2 = { + fullUrl: "urn:uuid:2", + resource: { + resourceType: "Observation", + id: "2", + }, +}; +const resource3 = { + fullUrl: "urn:uuid:3", + resource: { + resourceType: "Observation", + id: "3", + }, +}; +const BundleSample = { + resourceType: "Bundle", + type: "document", + entry: [ + resource1, resource2, resource3 + ], +} as unknown as Bundle; +const fhirIndexBundleSample = { + fhirIndexByType: { + Composition: [resource1.resource], + Observation: [resource2.resource, resource3.resource], + }, + fhirIndexByTypeAndId: { + Composition: { + "1": resource1.resource, + }, + Observation: { + "2": resource2.resource, + "3": resource3.resource, + }, + }, +}; + +describe("fhirResourcesIndexService Tests", () => { + describe("getFhirIndex Tests", () => { + it("Returns a valid FhirIndex", () => { + const actual = getFhirIndex(BundleSample); + + expect(actual).toEqual(fhirIndexBundleSample); + }); + it("Returns an empty FhirIndex when a bundle has no resources", () => { + const bundleEmpty = {} as unknown as Bundle; + const actual = getFhirIndex(bundleEmpty); + const expected = { + fhirIndexByType: {}, + fhirIndexByTypeAndId: {} + } + expect(actual).toEqual(expected); + }); + it("Does not index resources with no id", () => { + const resourceNoId = { + fullUrl: "urn:uuid:", + resource: { + resourceType: "Observation", + id: "" + } + } + const bundleWithResourceNoId = { + ...BundleSample, + entry: [...(BundleSample.entry ?? []), resourceNoId], + } as Bundle; + + const actual = getFhirIndex(bundleWithResourceNoId); + expect(actual).toEqual(fhirIndexBundleSample); + }); + it("Does not index resources with no resourceType", () => { + // Note: this should not be possible. Adding test for safeguarding anyways + const resourceNoType = { + fullUrl: "urn:uuid:4", + resource: { + resourceType: "", + id: "4", + }, + }; + const bundleWithResourceNoType = { + ...BundleSample, + entry: [...(BundleSample.entry ?? []), resourceNoType], + } as Bundle; + + const actual = getFhirIndex(bundleWithResourceNoType); + expect(actual).toEqual(fhirIndexBundleSample); + }); + }); + describe("getResourcesByType Tests", () => { + it("Returns all resources of a specified type", () => { + const actual = getResourcesByType(fhirIndexBundleSample, "Observation"); + expect(actual.length).toEqual(2); + expect(actual).toEqual([resource2.resource, resource3.resource]); + }); + it("Returns an empty array when no resources exist of specified type", () => { + const actual = getResourcesByType(fhirIndexBundleSample, "Patient"); + expect(actual).toEqual([]); + }); + }); + describe("getResourceById Tests", () => { + it("Returns resource with specified ID", () => { + const actual = getResourceById( + fhirIndexBundleSample, + "Composition", + "1" + ); + expect(actual).toEqual(resource1.resource); + }); + it("Returns undefined when no resource exists with spcified ID", () => { + const actual = getResourceById( + fhirIndexBundleSample, + "Observation", + "4" + ); + expect(actual).toEqual(undefined); + }); + it("Returns undefined if resource exists with specified ID BUT has the wrong resource Type", () => { + const actual = getResourceById( + fhirIndexBundleSample, + "Composition", + "2" + ); + expect(actual).toEqual(undefined); + }); + }); +}); From 10d16d45e5b81574bae9d1cc9fdbec3c9336574e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 25 Mar 2026 00:55:17 +0000 Subject: [PATCH 08/14] [pre-commit.ci] auto fixes from pre-commit hooks --- .../src/app/utils/evaluate/index.ts | 2 +- .../services/fhirResourcesIndexService.tsx | 12 +++--- .../tests/unit/app/utils/evaluate.test.ts | 4 +- .../fhirResourcesIndexService.test.tsx | 41 +++++++++++-------- 4 files changed, 33 insertions(+), 26 deletions(-) diff --git a/containers/ecr-viewer/src/app/utils/evaluate/index.ts b/containers/ecr-viewer/src/app/utils/evaluate/index.ts index 4e7cbb0c6..44fa0687d 100644 --- a/containers/ecr-viewer/src/app/utils/evaluate/index.ts +++ b/containers/ecr-viewer/src/app/utils/evaluate/index.ts @@ -351,7 +351,7 @@ export const evaluateReference = ( * error logged if it does not match. * * Expects a single element to be returned from the reference. - * + * * @param fhirIndex - FHIR resources indexed by type & by ID * @param ref - The reference string (e.g., "Patient/123"). * @returns The FHIR Resource or undefined if not found. diff --git a/containers/ecr-viewer/src/app/view-data/services/fhirResourcesIndexService.tsx b/containers/ecr-viewer/src/app/view-data/services/fhirResourcesIndexService.tsx index bd5c255f7..842c47e46 100644 --- a/containers/ecr-viewer/src/app/view-data/services/fhirResourcesIndexService.tsx +++ b/containers/ecr-viewer/src/app/view-data/services/fhirResourcesIndexService.tsx @@ -18,7 +18,7 @@ export interface FhirIndex { /** * Builds an index of FHIR resources from a given FHIR bundle. - * + * * Extracts all resources from a given FHIR bundle and organizes them into two maps: * 1. `fhirIndexByType` – an map of resources keyed by `resourceType` mapping to an array of all resources of that type. * 2. `fhirIndexByTypeAndId` – a map of resources keyed by `resourceType` and then by `id`. @@ -54,12 +54,12 @@ export const getFhirIndex = (fhirBundle: Bundle): FhirIndex => { /** * Returns array of all resources of a specific type (i.e. "Observation"). - * + * * @template T - The expected FHIR Resource type (e.g., Observation, Patient). * @param fhirIndex - FHIR resources indexed by type & by ID * @param type - The resourceType to retrieve (e.g., "Observation"). - * - * @returns Array of FHIR resources of type `T`, or empty array if + * + * @returns Array of FHIR resources of type `T`, or empty array if * no resources of specified type exist. */ // TODO ANGELA: How to guarantee that type & `T` match? `T` only exists at compile, whereas type exists at `runtime` @@ -78,12 +78,12 @@ export function getResourcesByType( /** * Returns FHIR resource by ID and check resource type. * Expects only one resource to be returned. - * + * * @template T - The expected FHIR Resource type (e.g., Observation, Patient). * @param fhirIndex - FHIR resources indexed by type & by ID * @param type - The resourceType to retrieve (e.g., "Observation"). * @param id - The unique identifier of the resource. - * + * * @returns FHIR resource of type `T` if it exists and resourceType matches `type` * Returns undefined if no resource exists with given ID and resourceType */ diff --git a/containers/ecr-viewer/tests/unit/app/utils/evaluate.test.ts b/containers/ecr-viewer/tests/unit/app/utils/evaluate.test.ts index 5df6a214e..92df9fe52 100644 --- a/containers/ecr-viewer/tests/unit/app/utils/evaluate.test.ts +++ b/containers/ecr-viewer/tests/unit/app/utils/evaluate.test.ts @@ -354,7 +354,7 @@ describe("Evaluate Reference 2", () => { it("should return undefined if resource not found", () => { const actual = evaluateReference2( fhirIndexBundleSample, - "Observation/not-valid-id" + "Observation/not-valid-id", ); expect(actual).toBeUndefined(); @@ -362,7 +362,7 @@ describe("Evaluate Reference 2", () => { it("should return the resource if the resource is available", () => { const actual = evaluateReference2( fhirIndexBundleSample, - "Observation/2" + "Observation/2", ); expect(actual?.id).toEqual("2"); diff --git a/containers/ecr-viewer/tests/unit/app/view-data/services/fhirResourcesIndexService.test.tsx b/containers/ecr-viewer/tests/unit/app/view-data/services/fhirResourcesIndexService.test.tsx index 46cd5a37c..3f632c057 100644 --- a/containers/ecr-viewer/tests/unit/app/view-data/services/fhirResourcesIndexService.test.tsx +++ b/containers/ecr-viewer/tests/unit/app/view-data/services/fhirResourcesIndexService.test.tsx @@ -1,6 +1,9 @@ import { Bundle, Composition, Observation, Patient } from "fhir/r4"; -import { getFhirIndex, getResourceById, getResourcesByType } from "@/app/view-data/services/fhirResourcesIndexService"; - +import { + getFhirIndex, + getResourceById, + getResourcesByType, +} from "@/app/view-data/services/fhirResourcesIndexService"; const resource1 = { fullUrl: "urn:uuid:1", @@ -26,9 +29,7 @@ const resource3 = { const BundleSample = { resourceType: "Bundle", type: "document", - entry: [ - resource1, resource2, resource3 - ], + entry: [resource1, resource2, resource3], } as unknown as Bundle; const fhirIndexBundleSample = { fhirIndexByType: { @@ -58,8 +59,8 @@ describe("fhirResourcesIndexService Tests", () => { const actual = getFhirIndex(bundleEmpty); const expected = { fhirIndexByType: {}, - fhirIndexByTypeAndId: {} - } + fhirIndexByTypeAndId: {}, + }; expect(actual).toEqual(expected); }); it("Does not index resources with no id", () => { @@ -67,9 +68,9 @@ describe("fhirResourcesIndexService Tests", () => { fullUrl: "urn:uuid:", resource: { resourceType: "Observation", - id: "" - } - } + id: "", + }, + }; const bundleWithResourceNoId = { ...BundleSample, entry: [...(BundleSample.entry ?? []), resourceNoId], @@ -98,12 +99,18 @@ describe("fhirResourcesIndexService Tests", () => { }); describe("getResourcesByType Tests", () => { it("Returns all resources of a specified type", () => { - const actual = getResourcesByType(fhirIndexBundleSample, "Observation"); + const actual = getResourcesByType( + fhirIndexBundleSample, + "Observation", + ); expect(actual.length).toEqual(2); expect(actual).toEqual([resource2.resource, resource3.resource]); }); it("Returns an empty array when no resources exist of specified type", () => { - const actual = getResourcesByType(fhirIndexBundleSample, "Patient"); + const actual = getResourcesByType( + fhirIndexBundleSample, + "Patient", + ); expect(actual).toEqual([]); }); }); @@ -112,7 +119,7 @@ describe("fhirResourcesIndexService Tests", () => { const actual = getResourceById( fhirIndexBundleSample, "Composition", - "1" + "1", ); expect(actual).toEqual(resource1.resource); }); @@ -120,17 +127,17 @@ describe("fhirResourcesIndexService Tests", () => { const actual = getResourceById( fhirIndexBundleSample, "Observation", - "4" + "4", ); - expect(actual).toEqual(undefined); + expect(actual).toEqual(undefined); }); it("Returns undefined if resource exists with specified ID BUT has the wrong resource Type", () => { const actual = getResourceById( fhirIndexBundleSample, "Composition", - "2" + "2", ); - expect(actual).toEqual(undefined); + expect(actual).toEqual(undefined); }); }); }); From c966f178ab3c093ba257cc263a712b03c0b7863c Mon Sep 17 00:00:00 2001 From: Victor Chaparro Date: Tue, 31 Mar 2026 09:39:26 -0600 Subject: [PATCH 09/14] Added missing property text under Administered Medications --- .../components/AdministeredMedication.tsx | 6 ++++++ .../components/EcrDocument/clinical-data.tsx | 3 +-- .../view-data/components/AdminMedTable.test.tsx | 4 +++- .../components/EcrDocument/EcrDocument.test.tsx | 14 ++++++++++++++ containers/fhir-converter/Dockerfile | 2 +- test-data/fhir/BundleAdmissionMedications.json | 5 +++-- 6 files changed, 28 insertions(+), 6 deletions(-) diff --git a/containers/ecr-viewer/src/app/view-data/components/AdministeredMedication.tsx b/containers/ecr-viewer/src/app/view-data/components/AdministeredMedication.tsx index 5f20f2ec3..1ff90f52f 100644 --- a/containers/ecr-viewer/src/app/view-data/components/AdministeredMedication.tsx +++ b/containers/ecr-viewer/src/app/view-data/components/AdministeredMedication.tsx @@ -14,6 +14,7 @@ export type AdministeredMedicationTableData = { dose?: string; route?: string; therapeuticResponse?: string; + text?: string; }; /** @@ -47,6 +48,10 @@ export const AdministeredMedication = ({ columnName: "Therapeutic Response Observation", className: "bg-gray-5 minw-15", }, + { + columnName: "Text", + className: "bg-gray-5 minw-15", + }, ]; return ( @@ -58,6 +63,7 @@ export const AdministeredMedication = ({ {entry?.dose ?? noData} {entry?.route ?? noData} {entry?.therapeuticResponse ?? noData} + {entry?.text ?? noData} ))} diff --git a/containers/ecr-viewer/src/app/view-data/components/EcrDocument/clinical-data.tsx b/containers/ecr-viewer/src/app/view-data/components/EcrDocument/clinical-data.tsx index b2b69acb7..f9832808c 100644 --- a/containers/ecr-viewer/src/app/view-data/components/EcrDocument/clinical-data.tsx +++ b/containers/ecr-viewer/src/app/view-data/components/EcrDocument/clinical-data.tsx @@ -2,7 +2,6 @@ import { Bundle, CareTeamParticipant, Device, - Dosage, Element, Location, Medication, @@ -13,7 +12,6 @@ import { Organization, Period, Practitioner, - Quantity, Reference, ServiceRequest, } from "fhir/r4"; @@ -219,6 +217,7 @@ const evaluateAdministeredMedication = ( dose: doseValue, route: formatCodeableConcept(medicationAdministration?.dosage?.route), therapeuticResponse: therapeuticResponseText, + text: medicationAdministration?.dosage?.text, }; }); }; diff --git a/containers/ecr-viewer/tests/unit/app/view-data/components/AdminMedTable.test.tsx b/containers/ecr-viewer/tests/unit/app/view-data/components/AdminMedTable.test.tsx index c4a1f1eda..c70cc3085 100644 --- a/containers/ecr-viewer/tests/unit/app/view-data/components/AdminMedTable.test.tsx +++ b/containers/ecr-viewer/tests/unit/app/view-data/components/AdminMedTable.test.tsx @@ -21,6 +21,7 @@ describe("AdminMedTable", () => { dose: "325 mg", route: "Intravenous", therapeuticResponse: "Improved condition", + text: "Take with food", }, { name: "aleve tablet", @@ -36,10 +37,11 @@ describe("AdminMedTable", () => { expect(screen.getByText("09/29/2022")).toBeVisible(); expect(screen.getByText("Intravenous")).toBeVisible(); expect(screen.getByText("Improved condition")).toBeVisible(); + expect(screen.getByText("Take with food")).toBeVisible(); expect(screen.getByText("aleve tablet")).toBeVisible(); expect(screen.getByText("250 mg")).toBeVisible(); expect(screen.getByText("09/29/2022 4:53 AM")).toBeVisible(); - expect(screen.getAllByText("No data")).toBeArrayOfSize(2); + expect(screen.getAllByText("No data")).toBeArrayOfSize(3); }); }); diff --git a/containers/ecr-viewer/tests/unit/app/view-data/components/EcrDocument/EcrDocument.test.tsx b/containers/ecr-viewer/tests/unit/app/view-data/components/EcrDocument/EcrDocument.test.tsx index 54d72ddef..732360f6d 100644 --- a/containers/ecr-viewer/tests/unit/app/view-data/components/EcrDocument/EcrDocument.test.tsx +++ b/containers/ecr-viewer/tests/unit/app/view-data/components/EcrDocument/EcrDocument.test.tsx @@ -4,6 +4,7 @@ import { render, screen } from "@testing-library/react"; import { Bundle } from "fhir/r4"; import { axe } from "jest-axe"; +import BundleAdmissionMedications from "@/../../../test-data/fhir/BundleAdmissionMedications.json"; import BundleCareTeam from "@/../../../test-data/fhir/BundleCareTeam.json"; import BundleWithMiscNotes from "@/../../../test-data/fhir/BundleMiscNotes.json"; import * as _BundleWithPatient from "@/../../../test-data/fhir/BundlePatient.json"; @@ -128,6 +129,19 @@ describe("Snapshot test for eCR Document", () => { }); }); + describe("Evaluate Administered Medications", () => { + it("should include dosage text when present", () => { + const actual = evaluateClinicalData( + BundleAdmissionMedications as unknown as Bundle, + ); + const adminMedsItem = actual.treatmentData.availableData.find( + (d) => d.title === "Administered Medications", + ); + render(adminMedsItem!.value as React.JSX.Element); + expect(screen.getByText("Take with water once daily")).toBeInTheDocument(); + }); + }); + describe("Evaluate Care Team Table", () => { it("should evaluate care team table results", () => { const actual: React.JSX.Element = returnCareTeamTable( diff --git a/containers/fhir-converter/Dockerfile b/containers/fhir-converter/Dockerfile index 62c1165ab..b8d1a648f 100644 --- a/containers/fhir-converter/Dockerfile +++ b/containers/fhir-converter/Dockerfile @@ -2,7 +2,7 @@ FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build # Download FHIR-Converter -RUN git clone https://github.com/CDCgov/dibbs-FHIR-Converter.git --branch 8.0.3 --depth 1 /App +RUN git clone https://github.com/CDCgov/dibbs-FHIR-Converter.git --branch laura-victor/1345-dosage-text --depth 1 /App WORKDIR /App diff --git a/test-data/fhir/BundleAdmissionMedications.json b/test-data/fhir/BundleAdmissionMedications.json index e270dbb10..2dcb8b8cb 100644 --- a/test-data/fhir/BundleAdmissionMedications.json +++ b/test-data/fhir/BundleAdmissionMedications.json @@ -1579,7 +1579,8 @@ "rateQuantity": { "value": 12, "unit": "h" - } + }, + "text": "Take with water once daily" }, "extension": [ { @@ -5217,4 +5218,4 @@ } } ] -} +} \ No newline at end of file From 9465de9553405723654a252c026d375218334b33 Mon Sep 17 00:00:00 2001 From: Victor Chaparro Date: Tue, 31 Mar 2026 09:53:20 -0600 Subject: [PATCH 10/14] Merge conflicts --- .../seed-scripts/docker-compose-seed.yaml | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/containers/ecr-viewer/seed-scripts/docker-compose-seed.yaml b/containers/ecr-viewer/seed-scripts/docker-compose-seed.yaml index 6f3cf6af7..a3f60f7ec 100644 --- a/containers/ecr-viewer/seed-scripts/docker-compose-seed.yaml +++ b/containers/ecr-viewer/seed-scripts/docker-compose-seed.yaml @@ -3,12 +3,16 @@ name: ecr-viewer include: - ../docker-compose.yaml services: + ecr-viewer: + environment: + - ORCHESTRATION_URL=http://localhost:2080 + fhir-converter-service: platform: linux/amd64 build: context: ../../fhir-converter/ ports: - - "8082:8080" + - "2082:8080" environment: - WEB_CONCURRENCY=${WEB_CONCURRENCY:-3} @@ -17,7 +21,7 @@ services: build: context: ../../ingestion ports: - - "8083:8080" + - "2083:8080" logging: driver: "json-file" environment: @@ -30,7 +34,7 @@ services: build: context: ../../message-parser ports: - - "8085:8080" + - "2085:8080" environment: - WEB_CONCURRENCY=${WEB_CONCURRENCY:-3} logging: @@ -41,7 +45,7 @@ services: build: context: ../../trigger-code-reference ports: - - "8086:8080" + - "2086:8080" environment: - WEB_CONCURRENCY=${WEB_CONCURRENCY:-3} logging: @@ -57,7 +61,7 @@ services: - message-parser-service - ecr-viewer ports: - - "8080:8080" + - "2080:8080" logging: driver: "json-file" env_file: @@ -81,7 +85,7 @@ services: keycloak: condition: service_healthy ports: - - "8081:8081" + - "2081:8081" volumes: - ./baseECR:/code/baseECR environment: From 3c506097194cc0b6bef87644e140c265f00a392e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 31 Mar 2026 15:54:03 +0000 Subject: [PATCH 11/14] [pre-commit.ci] auto fixes from pre-commit hooks --- .../app/view-data/components/EcrDocument/EcrDocument.test.tsx | 4 +++- test-data/fhir/BundleAdmissionMedications.json | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/containers/ecr-viewer/tests/unit/app/view-data/components/EcrDocument/EcrDocument.test.tsx b/containers/ecr-viewer/tests/unit/app/view-data/components/EcrDocument/EcrDocument.test.tsx index 732360f6d..35272be6a 100644 --- a/containers/ecr-viewer/tests/unit/app/view-data/components/EcrDocument/EcrDocument.test.tsx +++ b/containers/ecr-viewer/tests/unit/app/view-data/components/EcrDocument/EcrDocument.test.tsx @@ -138,7 +138,9 @@ describe("Snapshot test for eCR Document", () => { (d) => d.title === "Administered Medications", ); render(adminMedsItem!.value as React.JSX.Element); - expect(screen.getByText("Take with water once daily")).toBeInTheDocument(); + expect( + screen.getByText("Take with water once daily"), + ).toBeInTheDocument(); }); }); diff --git a/test-data/fhir/BundleAdmissionMedications.json b/test-data/fhir/BundleAdmissionMedications.json index 2dcb8b8cb..6a4fa6e6d 100644 --- a/test-data/fhir/BundleAdmissionMedications.json +++ b/test-data/fhir/BundleAdmissionMedications.json @@ -5218,4 +5218,4 @@ } } ] -} \ No newline at end of file +} From 5fe0efe8ab016eb42f8f42c7abc72919ec3a51ff Mon Sep 17 00:00:00 2001 From: Victor Chaparro Date: Tue, 31 Mar 2026 10:06:18 -0600 Subject: [PATCH 12/14] Rollback docker compose seed changes --- .../seed-scripts/docker-compose-seed.yaml | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/containers/ecr-viewer/seed-scripts/docker-compose-seed.yaml b/containers/ecr-viewer/seed-scripts/docker-compose-seed.yaml index a3f60f7ec..6f3cf6af7 100644 --- a/containers/ecr-viewer/seed-scripts/docker-compose-seed.yaml +++ b/containers/ecr-viewer/seed-scripts/docker-compose-seed.yaml @@ -3,16 +3,12 @@ name: ecr-viewer include: - ../docker-compose.yaml services: - ecr-viewer: - environment: - - ORCHESTRATION_URL=http://localhost:2080 - fhir-converter-service: platform: linux/amd64 build: context: ../../fhir-converter/ ports: - - "2082:8080" + - "8082:8080" environment: - WEB_CONCURRENCY=${WEB_CONCURRENCY:-3} @@ -21,7 +17,7 @@ services: build: context: ../../ingestion ports: - - "2083:8080" + - "8083:8080" logging: driver: "json-file" environment: @@ -34,7 +30,7 @@ services: build: context: ../../message-parser ports: - - "2085:8080" + - "8085:8080" environment: - WEB_CONCURRENCY=${WEB_CONCURRENCY:-3} logging: @@ -45,7 +41,7 @@ services: build: context: ../../trigger-code-reference ports: - - "2086:8080" + - "8086:8080" environment: - WEB_CONCURRENCY=${WEB_CONCURRENCY:-3} logging: @@ -61,7 +57,7 @@ services: - message-parser-service - ecr-viewer ports: - - "2080:8080" + - "8080:8080" logging: driver: "json-file" env_file: @@ -85,7 +81,7 @@ services: keycloak: condition: service_healthy ports: - - "2081:8081" + - "8081:8081" volumes: - ./baseECR:/code/baseECR environment: From 10bab10636adc8fc85fd5749a5bb583b3b69fdb3 Mon Sep 17 00:00:00 2001 From: Victor Chaparro Date: Tue, 31 Mar 2026 10:06:18 -0600 Subject: [PATCH 13/14] Rollback docker compose seed changes --- .../seed-scripts/docker-compose-seed.yaml | 16 ++++++---------- .../src/app/view-data/components/EcrSummary.tsx | 8 -------- 2 files changed, 6 insertions(+), 18 deletions(-) diff --git a/containers/ecr-viewer/seed-scripts/docker-compose-seed.yaml b/containers/ecr-viewer/seed-scripts/docker-compose-seed.yaml index a3f60f7ec..6f3cf6af7 100644 --- a/containers/ecr-viewer/seed-scripts/docker-compose-seed.yaml +++ b/containers/ecr-viewer/seed-scripts/docker-compose-seed.yaml @@ -3,16 +3,12 @@ name: ecr-viewer include: - ../docker-compose.yaml services: - ecr-viewer: - environment: - - ORCHESTRATION_URL=http://localhost:2080 - fhir-converter-service: platform: linux/amd64 build: context: ../../fhir-converter/ ports: - - "2082:8080" + - "8082:8080" environment: - WEB_CONCURRENCY=${WEB_CONCURRENCY:-3} @@ -21,7 +17,7 @@ services: build: context: ../../ingestion ports: - - "2083:8080" + - "8083:8080" logging: driver: "json-file" environment: @@ -34,7 +30,7 @@ services: build: context: ../../message-parser ports: - - "2085:8080" + - "8085:8080" environment: - WEB_CONCURRENCY=${WEB_CONCURRENCY:-3} logging: @@ -45,7 +41,7 @@ services: build: context: ../../trigger-code-reference ports: - - "2086:8080" + - "8086:8080" environment: - WEB_CONCURRENCY=${WEB_CONCURRENCY:-3} logging: @@ -61,7 +57,7 @@ services: - message-parser-service - ecr-viewer ports: - - "2080:8080" + - "8080:8080" logging: driver: "json-file" env_file: @@ -85,7 +81,7 @@ services: keycloak: condition: service_healthy ports: - - "2081:8081" + - "8081:8081" volumes: - ./baseECR:/code/baseECR environment: diff --git a/containers/ecr-viewer/src/app/view-data/components/EcrSummary.tsx b/containers/ecr-viewer/src/app/view-data/components/EcrSummary.tsx index a7fce85e5..f0426426a 100644 --- a/containers/ecr-viewer/src/app/view-data/components/EcrSummary.tsx +++ b/containers/ecr-viewer/src/app/view-data/components/EcrSummary.tsx @@ -6,13 +6,6 @@ import { AccordionItem } from "@/app/types"; import { toKebabCase } from "@/app/utils/format-utils"; import { DataDisplay, DataTableDisplay, DisplayDataProps } from "./DataDisplay"; -import { - evaluateEcrSummaryConditionSummary, - evaluateEcrSummaryEncounterDetails, - evaluateEcrSummaryPatientDetails, -} from "@/app/view-data/services/ecrSummaryService"; -import { Bundle } from "fhir/r4"; -import { FhirIndex } from "@/app/view-data/services/fhirResourcesIndexService"; interface EcrSummaryProps { patientDetails: DisplayDataProps[]; @@ -107,7 +100,6 @@ const EcrSummary: React.FC = ({ }; }, ); - return (
Date: Tue, 31 Mar 2026 11:33:30 -0600 Subject: [PATCH 14/14] Added missing snapshot update --- .../components/__snapshots__/Clinical.test.tsx.snap | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/containers/ecr-viewer/tests/unit/app/view-data/components/__snapshots__/Clinical.test.tsx.snap b/containers/ecr-viewer/tests/unit/app/view-data/components/__snapshots__/Clinical.test.tsx.snap index e073d413d..99969ea92 100644 --- a/containers/ecr-viewer/tests/unit/app/view-data/components/__snapshots__/Clinical.test.tsx.snap +++ b/containers/ecr-viewer/tests/unit/app/view-data/components/__snapshots__/Clinical.test.tsx.snap @@ -1034,6 +1034,12 @@ Result: 2 m > Therapeutic Response Observation + + Text + @@ -1061,6 +1067,13 @@ Result: 2 m No data + + + No data + +