diff --git a/containers/ecr-viewer/src/app/api/fhirPath.yml b/containers/ecr-viewer/src/app/api/fhirPath.yml index 45171907e6..46aeb97a91 100644 --- a/containers/ecr-viewer/src/app/api/fhirPath.yml +++ b/containers/ecr-viewer/src/app/api/fhirPath.yml @@ -43,16 +43,12 @@ encounterStartDate: "Bundle.entry.resource.where(resourceType = 'Encounter').per encounterDiagnosis: "Bundle.entry.resource.where(resourceType = 'Encounter').diagnosis" encounterType: "Bundle.entry.resource.where(resourceType='Encounter')[0].class.display" encounterID: "Bundle.entry.resource.where(resourceType='Encounter')[0].identifier" -facilityCity: "Bundle.entry.resource.where(resourceType = 'Location')[0].address.city" facilityContact: "Bundle.entry.resource.where(resourceType = 'Location')[0].telecom.where(system = 'phone')[0].value" facilityContactAddress: "Bundle.entry.resource.where(resourceType = 'Encounter')[0].serviceProvider" -facilityCountry: "Bundle.entry.resource.where(resourceType = 'Location')[0].address.country" facilityLocation: "Bundle.entry.resource.where(resourceType = 'Encounter')[0].location[0].location.reference" facilityName: "Bundle.entry.resource.where(resourceType = 'Encounter')[0].location[0].location.display" -facilityState: "Bundle.entry.resource.where(resourceType = 'Location')[0].address.state" -facilityStreetAddress: "Bundle.entry.resource.where(resourceType = 'Location')[0].address.line[0]" +facilityAddress: "Bundle.entry.resource.where(resourceType = 'Location')[0].address" facilityType: "Bundle.entry.resource.where(resourceType = 'Encounter')[0].location[0].extension.where(url = 'http://build.fhir.org/ig/HL7/case-reporting/StructureDefinition-us-ph-location-definitions.html#Location.type').valueCodeableConcept.coding[0].display" -facilityZipCode: "Bundle.entry.resource.where(resourceType = 'Location')[0].address.postalCode" compositionEncounterRef: "Bundle.entry.resource.where(resourceType = 'Composition').encounter.reference" encounterIndividualRef: "Encounter.participant.where(type.coding.code = 'ATND').individual.reference" diff --git a/containers/ecr-viewer/src/app/services/ecrMetadataService.ts b/containers/ecr-viewer/src/app/services/ecrMetadataService.ts index f126230e6a..263c261d6d 100644 --- a/containers/ecr-viewer/src/app/services/ecrMetadataService.ts +++ b/containers/ecr-viewer/src/app/services/ecrMetadataService.ts @@ -179,13 +179,7 @@ export const evaluateEcrMetadata = ( }, { title: "Custodian Address", - value: formatAddress( - custodian?.address?.[0].line, - custodian?.address?.[0].city, - custodian?.address?.[0].state, - custodian?.address?.[0].postalCode, - custodian?.address?.[0].country, - ), + value: formatAddress(custodian?.address?.[0]), }, { title: "Custodian Contact", @@ -233,13 +227,7 @@ const evaluateEcrAuthorDetails = ( { title: "Author Address", value: practitioner?.address?.map((address) => - formatAddress( - address.line, - address.city, - address.state, - address.postalCode, - address.country, - ), + formatAddress(address), ), }, { @@ -253,13 +241,7 @@ const evaluateEcrAuthorDetails = ( { title: "Author Facility Address", value: organization?.address?.map((address) => - formatAddress( - address.line, - address.city, - address.state, - address.postalCode, - address.country, - ), + formatAddress(address), ), }, { diff --git a/containers/ecr-viewer/src/app/services/ecrSummaryService.tsx b/containers/ecr-viewer/src/app/services/ecrSummaryService.tsx index 85530763fd..192fe8f7fc 100644 --- a/containers/ecr-viewer/src/app/services/ecrSummaryService.tsx +++ b/containers/ecr-viewer/src/app/services/ecrSummaryService.tsx @@ -1,6 +1,7 @@ -import { Bundle, Condition, Extension, Observation } from "fhir/r4"; +import { Address, Bundle, Condition, Extension, Observation } from "fhir/r4"; import { evaluateData, PathMappings } from "@/app/view-data/utils/utils"; import { + formatAddress, formatContactPoint, formatDate, formatStartEndDateTime, @@ -8,7 +9,6 @@ import { import { evaluate } from "@/app/view-data/utils/evaluate"; import { evaluatePatientName, - evaluatePatientAddress, evaluateEncounterDiagnosis, } from "./evaluateFhirDataService"; import { DisplayDataProps } from "@/app/view-data/components/DataDisplay"; @@ -72,7 +72,9 @@ export const evaluateEcrSummaryPatientDetails = ( }, { title: "Patient Address", - value: evaluatePatientAddress(fhirBundle, fhirPathMappings), + value: findCurrentAddress( + evaluate(fhirBundle, fhirPathMappings.patientAddressList), + ), }, { title: "Patient Contact", @@ -83,6 +85,34 @@ export const evaluateEcrSummaryPatientDetails = ( ]); }; +/** + * Find the most current home address. + * @param addresses - List of addresses. + * @returns A string with the formatted current address or an empty string if no address. + */ +export const findCurrentAddress = (addresses: Address[]) => { + // current home address is first pick + let address = addresses.find( + (a) => a.use === "home" && !!a.period?.start && !a.period?.end, + ); + // then current address + if (!address) { + address = addresses.find((a) => !!a.period?.start && !a.period?.end); + } + + // then home address + if (!address) { + address = addresses.find((a) => a.use == "home"); + } + + // then first address + if (!address) { + address = addresses[0]; + } + + return formatAddress(address); +}; + /** * Evaluates and retrieves encounter details from the FHIR bundle using the provided path mappings. * @param fhirBundle - The FHIR bundle containing patient data. diff --git a/containers/ecr-viewer/src/app/services/evaluateFhirDataService.ts b/containers/ecr-viewer/src/app/services/evaluateFhirDataService.ts index 48b573b0db..f50adbea2b 100644 --- a/containers/ecr-viewer/src/app/services/evaluateFhirDataService.ts +++ b/containers/ecr-viewer/src/app/services/evaluateFhirDataService.ts @@ -1,4 +1,5 @@ import { + Address, Bundle, CodeableConcept, Coding, @@ -112,19 +113,18 @@ export const evaluatePatientAddress = ( fhirBundle: Bundle, mappings: PathMappings, ) => { - const addresses = evaluate(fhirBundle, mappings.patientAddressList); + const addresses = evaluate( + fhirBundle, + mappings.patientAddressList, + ) as Address[]; if (addresses.length > 0) { return addresses .map((address) => { - return formatAddress( - address.line, - address.city, - address.state, - address.postalCode, - address.country, - addresses.length > 1 ? address?.use : undefined, - ); + return formatAddress(address, { + includeUse: addresses.length > 1, + includePeriod: true, + }); }) .join("\n\n"); } else { @@ -390,7 +390,8 @@ export const evaluateFacilityData = ( referenceString = facilityContactAddressRef[0].reference; } const facilityContactAddress = referenceString - ? evaluateReference(fhirBundle, mappings, referenceString)?.address?.[0] + ? (evaluateReference(fhirBundle, mappings, referenceString) + ?.address?.[0] as Address) : undefined; const facilityData = [ @@ -401,22 +402,12 @@ export const evaluateFacilityData = ( { title: "Facility Address", value: formatAddress( - evaluate(fhirBundle, mappings["facilityStreetAddress"]), - evaluate(fhirBundle, mappings["facilityCity"])[0], - evaluate(fhirBundle, mappings["facilityState"])[0], - evaluate(fhirBundle, mappings["facilityZipCode"])[0], - evaluate(fhirBundle, mappings["facilityCountry"])[0], + evaluate(fhirBundle, mappings["facilityAddress"])[0], ), }, { title: "Facility Contact Address", - value: formatAddress( - facilityContactAddress?.line, - facilityContactAddress?.city, - facilityContactAddress?.state, - facilityContactAddress?.postalCode, - facilityContactAddress?.country, - ), + value: formatAddress(facilityContactAddress), }, { title: "Facility Contact", @@ -471,15 +462,7 @@ export const evaluateProviderData = ( }, { title: "Provider Address", - value: practitioner?.address?.map((address) => - formatAddress( - address.line, - address.city, - address.state, - address.postalCode, - address.country, - ), - ), + value: practitioner?.address?.map((address) => formatAddress(address)), }, { title: "Provider Contact", @@ -491,15 +474,7 @@ export const evaluateProviderData = ( }, { title: "Provider Facility Address", - value: organization?.address?.map((address) => - formatAddress( - address.line, - address.city, - address.state, - address.postalCode, - address.country, - ), - ), + value: organization?.address?.map((address) => formatAddress(address)), }, { title: "Provider ID", @@ -579,13 +554,7 @@ export const evaluateEmergencyContact = ( .map((contact) => { const relationship = contact.relationship?.[0].coding?.[0]?.display; - const address = formatAddress( - contact.address?.line, - contact.address?.city, - contact.address?.state, - contact.address?.postalCode, - contact.address?.country, - ); + const address = formatAddress(contact.address); const phoneNumbers = formatContactPoint(contact.telecom); diff --git a/containers/ecr-viewer/src/app/services/formatService.tsx b/containers/ecr-viewer/src/app/services/formatService.tsx index 582e1af3c9..b11512df91 100644 --- a/containers/ecr-viewer/src/app/services/formatService.tsx +++ b/containers/ecr-viewer/src/app/services/formatService.tsx @@ -1,6 +1,6 @@ import React from "react"; import { ToolTipElement } from "@/app/view-data/components/ToolTipElement"; -import { ContactPoint, HumanName } from "fhir/r4"; +import { Address, ContactPoint, HumanName } from "fhir/r4"; import { RenderableNode, safeParse } from "../view-data/utils/utils"; import sanitizeHtml from "sanitize-html"; @@ -48,36 +48,46 @@ export const formatName = ( return segments.filter(Boolean).join(" "); }; +const DEFAULT_ADDRESS_CONFIG = { includeUse: false, includePeriod: false }; +type AddressConfig = { includeUse?: boolean; includePeriod?: boolean }; + /** * Formats an address based on its components. - * @param streetAddress - An array containing street address lines. - * @param city - The city name. - * @param state - The state or region name. - * @param zipCode - The ZIP code or postal code. - * @param country - The country name. - * @param [use] - Optional address use. + * @param address - Object with address parts + * @param address.line - An array containing street address lines. + * @param address.city - The city name. + * @param address.state - The state or region name. + * @param address.postalCode - The ZIP code or postal code. + * @param address.country - The country name. + * @param address.use - Optional address use. + * @param address.period - Optional address use. + * @param config - Configuration object to customize formatting + * @param config.includeUse - Include the use (e.g. `Home:`) on the address if available (default: false) + * @param config.includePeriod - Include the perios (e.g. `Dates: 12/11/2023 - Present`) on the address if available (default: false) * @returns The formatted address string. */ export const formatAddress = ( - streetAddress: string[] | undefined, - city: string | undefined, - state: string | undefined, - zipCode: string | undefined, - country: string | undefined, - use?: string | undefined, + { line, city, state, postalCode, country, use, period }: Address = {}, + config: AddressConfig = {}, ) => { - let address = { - use: use || "", - streetAddress: streetAddress || [], - cityState: [city, state], - zipCodeCountry: [zipCode, country], + const { includeUse, includePeriod } = { + ...DEFAULT_ADDRESS_CONFIG, + ...config, + }; + + const formatDateLine = () => { + const stDt = formatDate(period?.start); + const endDt = formatDate(period?.end); + if (!stDt && !endDt) return false; + return `Dates: ${stDt ?? "Unknown"} - ${endDt ?? "Present"}`; }; return [ - address.use ? toSentenceCase(address.use) + ":" : "", - address.streetAddress.filter(Boolean).join("\n"), - address.cityState.filter(Boolean).join(", "), - address.zipCodeCountry.filter(Boolean).join(", "), + includeUse && use && toSentenceCase(use) + ":", + (line || []).filter(Boolean).join("\n"), + [city, state].filter(Boolean).join(", "), + [postalCode, country].filter(Boolean).join(", "), + includePeriod && formatDateLine(), ] .filter(Boolean) .join("\n"); diff --git a/containers/ecr-viewer/src/app/services/labsService.tsx b/containers/ecr-viewer/src/app/services/labsService.tsx index b9e07dd284..bb2e085fe3 100644 --- a/containers/ecr-viewer/src/app/services/labsService.tsx +++ b/containers/ecr-viewer/src/app/services/labsService.tsx @@ -560,13 +560,7 @@ export const evaluateLabOrganizationData = ( matchingOrg = findIdenticalOrg(orgMappings, matchingOrg); } const orgAddress = matchingOrg?.address?.[0]; - const formattedAddress = formatAddress( - orgAddress?.line, - orgAddress?.city, - orgAddress?.state, - orgAddress?.postalCode, - orgAddress?.country, - ); + const formattedAddress = formatAddress(orgAddress); const contactInfo = formatPhoneNumber(matchingOrg?.telecom?.[0].value); const name = matchingOrg?.name ?? ""; diff --git a/containers/ecr-viewer/src/app/tests/components/EcrSummary.test.tsx b/containers/ecr-viewer/src/app/tests/components/EcrSummary.test.tsx index 6b4994daf5..f1c00691cd 100644 --- a/containers/ecr-viewer/src/app/tests/components/EcrSummary.test.tsx +++ b/containers/ecr-viewer/src/app/tests/components/EcrSummary.test.tsx @@ -3,6 +3,7 @@ import { axe } from "jest-axe"; import EcrSummary, { ConditionSummary, } from "../../view-data/components/EcrSummary"; +import { findCurrentAddress } from "@/app/services/ecrSummaryService"; describe("EcrSummary", () => { const patientDetails = [ @@ -186,3 +187,56 @@ describe("EcrSummary", () => { expect(screen.getByText("2 CONDITIONS FOUND")); }); }); + +describe("findCurrentAddress", () => { + const base = { + line: ["123 Main St"], + }; + + it("should return empty when no addresses available", () => { + const actual = findCurrentAddress([]); + + expect(actual).toEqual(""); + }); + + it("should return first address when no use or period", () => { + const actual = findCurrentAddress([ + { ...base, city: "1" }, + { ...base, city: "2" }, + ]); + + expect(actual).toEqual("123 Main St\n1"); + }); + + it("should return first home address when no current period", () => { + const actual = findCurrentAddress([ + { ...base, use: "work", city: "1" }, + { ...base, use: "home", city: "2" }, + { ...base, use: "home", city: "3" }, + { + ...base, + use: "home", + city: "3", + period: { start: "2020-03-01", end: "2020-04-01" }, + }, + ]); + + expect(actual).toEqual("123 Main St\n2"); + }); + + it("should return current home address", () => { + const actual = findCurrentAddress([ + { ...base, use: "work", city: "1" }, + { ...base, use: "home", city: "2" }, + { ...base, use: "home", city: "3", period: { start: "2024-03-13" } }, + { + ...base, + use: "home", + city: "4", + period: { start: "2024-03-10", end: "2024-03-12" }, + }, + ]); + + expect(actual).toEqual("123 Main St\n3"); + }); +}); diff --git a/containers/ecr-viewer/src/app/tests/services/formatService.test.tsx b/containers/ecr-viewer/src/app/tests/services/formatService.test.tsx index 8372f305ba..87c09aaf70 100644 --- a/containers/ecr-viewer/src/app/tests/services/formatService.test.tsx +++ b/containers/ecr-viewer/src/app/tests/services/formatService.test.tsx @@ -887,48 +887,121 @@ describe("getFirstNonCommentChild", () => { describe("Format address", () => { it("should format a full address", () => { - const actual = formatAddress( - ["123 Main street", "Unit 2"], - "City", - "ST", - "00000", - "USA", - ); + const actual = formatAddress({ + line: ["123 Main street", "Unit 2"], + city: "City", + state: "ST", + postalCode: "00000", + country: "USA", + }); expect(actual).toEqual("123 Main street\nUnit 2\nCity, ST\n00000, USA"); }); it("should skip undefined values", () => { - const actual = formatAddress( - ["123 Main street", "Unit 2"], - undefined, - "ST", - "00000", - "USA", - ); + const actual = formatAddress({ + line: ["123 Main street", "Unit 2"], + state: "ST", + postalCode: "00000", + country: "USA", + }); expect(actual).toEqual("123 Main street\nUnit 2\nST\n00000, USA"); }); it("should return empty string if no values are available", () => { - const actual = formatAddress( - undefined, - undefined, - undefined, - undefined, - undefined, - ); + const actual = formatAddress(); expect(actual).toEqual(""); }); it("should skip extra address lines that are empty string", () => { + const actual = formatAddress({ + line: ["Street 1", "", "Unit 3", "", "Floor 4"], + }); + + expect(actual).toEqual("Street 1\nUnit 3\nFloor 4"); + }); + + it("should include the use, when asked for and available", () => { const actual = formatAddress( - ["Street 1", "", "Unit 3", "", "Floor 4"], - undefined, - undefined, - undefined, - undefined, + { + line: ["123 Main street", "Unit 2"], + city: "City", + state: "ST", + postalCode: "00000", + country: "USA", + use: "home", + }, + { includeUse: true }, ); + expect(actual).toEqual( + "Home:\n123 Main street\nUnit 2\nCity, ST\n00000, USA", + ); + }); - expect(actual).toEqual("Street 1\nUnit 3\nFloor 4"); + it("should include the dates, when asked for and available", () => { + const actual = formatAddress( + { + line: ["123 Main street", "Unit 2"], + city: "City", + state: "ST", + postalCode: "00000", + country: "USA", + use: "home", + period: { start: "03/13/2024", end: "04/14/2024" }, + }, + { includePeriod: true }, + ); + expect(actual).toEqual( + "123 Main street\nUnit 2\nCity, ST\n00000, USA\nDates: 03/13/2024 - 04/14/2024", + ); + }); + + it("should include the start date and present, when asked for and available", () => { + const actual = formatAddress( + { + line: ["123 Main street", "Unit 2"], + city: "City", + state: "ST", + postalCode: "00000", + country: "USA", + use: "home", + period: { start: "03/13/2024" }, + }, + { includePeriod: true }, + ); + expect(actual).toEqual( + "123 Main street\nUnit 2\nCity, ST\n00000, USA\nDates: 03/13/2024 - Present", + ); + }); + + it("should include the end date and unknown, when asked for and available", () => { + const actual = formatAddress( + { + line: ["123 Main street", "Unit 2"], + city: "City", + state: "ST", + postalCode: "00000", + country: "USA", + use: "home", + period: { end: "03/13/2024" }, + }, + { includePeriod: true }, + ); + expect(actual).toEqual( + "123 Main street\nUnit 2\nCity, ST\n00000, USA\nDates: Unknown - 03/13/2024", + ); + }); + + it("should not include the use or period, when not asked for and available", () => { + const actual = formatAddress({ + line: ["123 Main street", "Unit 2"], + city: "City", + state: "ST", + postalCode: "00000", + country: "USA", + use: "home", + period: { start: "03/13/2024" }, + }); + expect(actual).toEqual("123 Main street\nUnit 2\nCity, ST\n00000, USA"); }); }); diff --git a/containers/fhir-converter/Dockerfile b/containers/fhir-converter/Dockerfile index f08be0479c..032523e7c1 100644 --- a/containers/fhir-converter/Dockerfile +++ b/containers/fhir-converter/Dockerfile @@ -1,7 +1,7 @@ FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build # Download FHIR-Converter -RUN git clone https://github.com/skylight-hq/FHIR-Converter.git --branch v7.0-skylight-15 --single-branch /build/FHIR-Converter +RUN git clone https://github.com/skylight-hq/FHIR-Converter.git --branch v7.0-skylight-16 --single-branch /build/FHIR-Converter WORKDIR /build/FHIR-Converter diff --git a/containers/fhir-converter/tests/integration/__snapshots__/test_FHIR-Converter.ambr b/containers/fhir-converter/tests/integration/__snapshots__/test_FHIR-Converter.ambr index 6cbc02ae05..4b4fe3127a 100644 --- a/containers/fhir-converter/tests/integration/__snapshots__/test_FHIR-Converter.ambr +++ b/containers/fhir-converter/tests/integration/__snapshots__/test_FHIR-Converter.ambr @@ -2483,6 +2483,9 @@ 'line': list([ '564 Abbey Lane', ]), + 'period': dict({ + 'start': '2022-06-10', + }), 'postalCode': '90015', 'state': 'CA', 'use': 'home', @@ -14316,6 +14319,9 @@ 'line': list([ '9 Post Lane', ]), + 'period': dict({ + 'start': '2019-04-10', + }), 'postalCode': '92663', 'state': 'CA', 'use': 'home',