From 49222c59ae46178fa1f0dd2ede015e5e6930f8ec Mon Sep 17 00:00:00 2001 From: Angela The Date: Tue, 21 Jan 2025 11:28:34 -0800 Subject: [PATCH] Fix field standardization in eCR Library (#3160) * add toTitleCase, patient and emg contact name * make emg contact relationship title case * make demographics, sex title case * make demographics, sex title case * apply title case to address lines and cities * update facility contact format in summary * email should be lowercase * Change toTitleCase func to const Co-authored-by: Mary McGrath * [pre-commit.ci] auto fixes from pre-commit hooks * add unit tests for single-character and multi-line strings --------- Co-authored-by: Mary McGrath Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .../src/app/services/ecrSummaryService.tsx | 10 +++- .../app/services/evaluateFhirDataService.ts | 10 +++- .../src/app/services/formatService.tsx | 24 +++++++--- .../__snapshots__/LabInfo.test.tsx.snap | 4 +- .../tests/services/ecrMetadataService.test.ts | 10 ++-- .../services/evaluateFhirDataServices.test.ts | 20 ++++---- .../app/tests/services/formatService.test.tsx | 47 +++++++++++++++---- .../ecr-viewer/src/app/tests/utils.test.tsx | 5 +- 8 files changed, 92 insertions(+), 38 deletions(-) diff --git a/containers/ecr-viewer/src/app/services/ecrSummaryService.tsx b/containers/ecr-viewer/src/app/services/ecrSummaryService.tsx index df4dac1bc4..77db92b8b8 100644 --- a/containers/ecr-viewer/src/app/services/ecrSummaryService.tsx +++ b/containers/ecr-viewer/src/app/services/ecrSummaryService.tsx @@ -10,7 +10,9 @@ import { formatAddress, formatContactPoint, formatDate, + formatPhoneNumber, formatStartEndDateTime, + toTitleCase, } from "@/app/services/formatService"; import { evaluate } from "@/app/view-data/utils/evaluate"; import { @@ -48,7 +50,9 @@ export const evaluateEcrSummaryPatientDetails = ( }, { title: "Sex", - value: evaluate(fhirBundle, fhirPathMappings.patientGender)[0], + value: toTitleCase( + evaluate(fhirBundle, fhirPathMappings.patientGender)[0], + ), }, { title: "Patient Address", @@ -122,7 +126,9 @@ export const evaluateEcrSummaryEncounterDetails = ( }, { title: "Facility Contact", - value: evaluate(fhirBundle, fhirPathMappings.facilityContact), + value: formatPhoneNumber( + evaluate(fhirBundle, fhirPathMappings.facilityContact)[0], + ), }, ]); }; diff --git a/containers/ecr-viewer/src/app/services/evaluateFhirDataService.ts b/containers/ecr-viewer/src/app/services/evaluateFhirDataService.ts index 030360a021..707e0f7a88 100644 --- a/containers/ecr-viewer/src/app/services/evaluateFhirDataService.ts +++ b/containers/ecr-viewer/src/app/services/evaluateFhirDataService.ts @@ -27,6 +27,7 @@ import { formatPhoneNumber, formatStartEndDate, formatStartEndDateTime, + toTitleCase, } from "./formatService"; import fhirpath_r4_model from "fhirpath/fhir-context/r4"; import { Element } from "fhir/r4"; @@ -328,7 +329,10 @@ export const evaluateDemographicsData = ( title: "Date of Death", value: evaluate(fhirBundle, mappings.patientDOD)[0], }, - { title: "Sex", value: evaluate(fhirBundle, mappings.patientGender)[0] }, + { + title: "Sex", + value: toTitleCase(evaluate(fhirBundle, mappings.patientGender)[0]), + }, { title: "Race", value: evaluatePatientRace(fhirBundle, mappings), @@ -596,7 +600,9 @@ export const evaluateEmergencyContact = ( return contacts .map((contact) => { - const relationship = contact.relationship?.[0].coding?.[0]?.display; + const relationship = toSentenceCase( + contact.relationship?.[0].coding?.[0]?.display, + ); const contactName = contact.name ? formatName(contact.name) : ""; diff --git a/containers/ecr-viewer/src/app/services/formatService.tsx b/containers/ecr-viewer/src/app/services/formatService.tsx index 16c7533a85..78b032e795 100644 --- a/containers/ecr-viewer/src/app/services/formatService.tsx +++ b/containers/ecr-viewer/src/app/services/formatService.tsx @@ -39,9 +39,9 @@ export const formatName = ( const segments = [ ...(withUse && use ? [`${toSentenceCase(use)}:`] : []), - ...(prefix ?? []), - ...(given ?? []), - family ?? "", + ...(prefix?.map(toTitleCase) ?? []), + ...(given?.map(toTitleCase) ?? []), + toTitleCase(family ?? ""), ...(suffix ?? []), ]; @@ -84,8 +84,8 @@ export const formatAddress = ( return [ includeUse && use && toSentenceCase(use) + ":", - (line || []).filter(Boolean).join("\n"), - [city, state].filter(Boolean).join(", "), + (line?.map(toTitleCase) || []).filter(Boolean).join("\n"), + [toTitleCase(city), state].filter(Boolean).join(", "), [postalCode, country].filter(Boolean).join(", "), includePeriod && formatDateLine(), ] @@ -592,6 +592,18 @@ export function toSentenceCase(str: string | undefined) { if (!str) return str; return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase(); } +/** + * Converts a string to title case, making the first character of each word uppercase. + * @param str - The string to convert to title case. + * @returns The converted title-case string. If the input is empty or not a string, the original input is returned. + */ +export const toTitleCase = (str: string | undefined) => { + if (!str) return str; + return str.replace( + /\w\S*/g, + (text) => text.charAt(0).toUpperCase() + text.substring(1).toLowerCase(), + ); +}; /** * Adds a caption to a table element. @@ -680,7 +692,7 @@ export const formatContactPoint = ( [phoneNumberUse, formatPhoneNumber(value)].filter((c) => c).join(": "), ); } else if (system === "email") { - contactArr.push(value); + contactArr.push(value.toLowerCase()); } else { const _use = toSentenceCase(use ?? ""); const _system = toSentenceCase(system ?? ""); diff --git a/containers/ecr-viewer/src/app/tests/components/__snapshots__/LabInfo.test.tsx.snap b/containers/ecr-viewer/src/app/tests/components/__snapshots__/LabInfo.test.tsx.snap index daf8f5b30e..af95f5936d 100644 --- a/containers/ecr-viewer/src/app/tests/components/__snapshots__/LabInfo.test.tsx.snap +++ b/containers/ecr-viewer/src/app/tests/components/__snapshots__/LabInfo.test.tsx.snap @@ -933,9 +933,9 @@ Burbank, CA
- 4605 TVC VUMC + 4605 Tvc Vumc 1301 Medical Center Drive -NASHVILLE, TN +Nashville, TN 37232-5310, USA
diff --git a/containers/ecr-viewer/src/app/tests/services/ecrMetadataService.test.ts b/containers/ecr-viewer/src/app/tests/services/ecrMetadataService.test.ts index c897a37659..ee88696f84 100644 --- a/containers/ecr-viewer/src/app/tests/services/ecrMetadataService.test.ts +++ b/containers/ecr-viewer/src/app/tests/services/ecrMetadataService.test.ts @@ -58,7 +58,7 @@ describe("Evaluate Ecr Metadata", () => { }, { title: "Custodian Address", - value: "3401 West End Ave\nNASHVILLE, TN\n37203, USA", + value: "3401 West End Ave\nNashville, TN\n37203, USA", }, { title: "Custodian Contact", @@ -123,7 +123,7 @@ describe("Evaluate Ecr Metadata", () => { }, { title: "Author Facility Address", - value: ["3401 West End Ave\nNASHVILLE, TN\n37203, USA"], + value: ["3401 West End Ave\nNashville, TN\n37203, USA"], }, { title: "Author Facility Contact", @@ -158,7 +158,7 @@ describe("Evaluate Ecr Metadata", () => { }, { title: "Author Facility Address", - value: ["3401 West End Ave\nNASHVILLE, TN\n37203, USA"], + value: ["3401 West End Ave\nNashville, TN\n37203, USA"], }, { title: "Author Facility Contact", @@ -182,7 +182,7 @@ describe("Evaluate Ecr Metadata", () => { }, { title: "Author Address", - value: ["3401 West End Ave\nNASHVILLE, TN\n37203, USA"], + value: ["3401 West End Ave\nNashville, TN\n37203, USA"], }, { title: "Author Contact", @@ -194,7 +194,7 @@ describe("Evaluate Ecr Metadata", () => { }, { title: "Author Facility Address", - value: ["3401 West End Ave\nNASHVILLE, TN\n37203, USA"], + value: ["3401 West End Ave\nNashville, TN\n37203, USA"], }, { title: "Author Facility Contact", diff --git a/containers/ecr-viewer/src/app/tests/services/evaluateFhirDataServices.test.ts b/containers/ecr-viewer/src/app/tests/services/evaluateFhirDataServices.test.ts index f15e855519..4c42c46f75 100644 --- a/containers/ecr-viewer/src/app/tests/services/evaluateFhirDataServices.test.ts +++ b/containers/ecr-viewer/src/app/tests/services/evaluateFhirDataServices.test.ts @@ -318,7 +318,7 @@ describe("Evaluate Emergency Contact", () => { mappings, ); expect(actual).toEqual( - `sister\nAnastasia Bubbletea Pizza\n999 Single Court\nBEVERLY HILLS, CA\n90210, USA\nHome: 615-995-9999`, + `Sister\nAnastasia Bubbletea Pizza\n999 Single Court\nBeverly Hills, CA\n90210, USA\nHome: 615-995-9999`, ); }); it("should return multiple emergency contacts", () => { @@ -396,7 +396,7 @@ describe("Evaluate Emergency Contact", () => { mappings, ); expect(actual).toEqual( - `sister\nAnastasia Bubbletea Pizza\n999 Single Court\nBEVERLY HILLS, CA\n90210, USA\nHome: 615-995-9999\n\nbrother\nAlberto Bonanza Bartholomew Eggbert\nHome: 615-995-1000\nHome Fax: 615-995-1001`, + `Sister\nAnastasia Bubbletea Pizza\n999 Single Court\nBeverly Hills, CA\n90210, USA\nHome: 615-995-9999\n\nBrother\nAlberto Bonanza Bartholomew Eggbert\nHome: 615-995-1000\nHome Fax: 615-995-1001`, ); }); it("should not return empty space when address is not available in", () => { @@ -438,7 +438,7 @@ describe("Evaluate Emergency Contact", () => { mappings, ); expect(actual).toEqual( - `sister\nAnastasia Bubbletea Pizza\nHome: 615-995-9999`, + `Sister\nAnastasia Bubbletea Pizza\nHome: 615-995-9999`, ); }); it("should return undefined if a patient has no contact", () => { @@ -456,7 +456,7 @@ describe("Evaluate Patient Address", () => { BundleWithPatient as unknown as Bundle, mappings, ); - expect(actual).toEqual("1050 CARPENTER ST\nEDWARDS, CA\n93523-2800, US"); + expect(actual).toEqual("1050 Carpenter St\nEdwards, CA\n93523-2800, US"); }); it("should return all 3 of the addresses", () => { const actual = evaluatePatientAddress( @@ -465,8 +465,8 @@ describe("Evaluate Patient Address", () => { ); expect(actual).toEqual( "Home:\n" + - "1050 CARPENTER ST\n" + - "EDWARDS, CA\n" + + "1050 Carpenter St\n" + + "Edwards, CA\n" + "93523-2800, US\n" + "\n" + "Vacation:\n" + @@ -489,7 +489,7 @@ describe("Evaluate Patient Name", () => { mappings, false, ); - expect(actual).toEqual("ABEL CASTILLO"); + expect(actual).toEqual("Abel Castillo"); }); it("should return all 2 of the names", () => { const actual = evaluatePatientName( @@ -498,7 +498,7 @@ describe("Evaluate Patient Name", () => { false, ); expect(actual).toEqual( - "Official: ABEL CASTILLO\n" + "Nickname: Billy The Kid", + "Official: Abel Castillo\n" + "Nickname: Billy The Kid", ); }); it("should only return the official name for the banner", () => { @@ -507,7 +507,7 @@ describe("Evaluate Patient Name", () => { mappings, true, ); - expect(actual).toEqual("ABEL CASTILLO"); + expect(actual).toEqual("Abel Castillo"); }); it("should only return the official name for the banner", () => { const actual = evaluatePatientName( @@ -515,7 +515,7 @@ describe("Evaluate Patient Name", () => { mappings, true, ); - expect(actual).toEqual("ABEL CASTILLO"); + expect(actual).toEqual("Abel Castillo"); }); }); 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 4790596d3c..3a5a6330f4 100644 --- a/containers/ecr-viewer/src/app/tests/services/formatService.test.tsx +++ b/containers/ecr-viewer/src/app/tests/services/formatService.test.tsx @@ -13,6 +13,7 @@ import { getFirstNonCommentChild, formatAddress, formatPhoneNumber, + toTitleCase, } from "@/app/services/formatService"; import { ContactPoint, HumanName } from "fhir/r4"; @@ -619,6 +620,34 @@ describe("toSentenceCase", () => { }); }); +describe("toTitleCase", () => { + it("should return string in title case", () => { + const input = "ABEL CASTILLO"; + const expected = "Abel Castillo"; + + const result = toTitleCase(input); + expect(result).toEqual(expected); + }); + it("should return single-character word in title case", () => { + const input = "a"; + const expected = "A"; + + const result = toTitleCase(input); + expect(result).toEqual(expected); + }); + it("should return multi-line string in title case", () => { + const input = "facility name 1\nfacility name 2"; + const expected = "Facility Name 1\nFacility Name 2"; + + const result = toTitleCase(input); + expect(result).toEqual(expected); + }); + it("should return undefined if string is empty", () => { + const result = toTitleCase(undefined); + expect(result).toBeUndefined(); + }); +}); + describe("removeHtmlElements", () => { it("should remove all HTML tags from string", () => { const input = "

Hello
there

"; @@ -896,13 +925,13 @@ describe("getFirstNonCommentChild", () => { describe("Format address", () => { it("should format a full address", () => { const actual = formatAddress({ - line: ["123 Main street", "Unit 2"], - city: "City", + 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"); + expect(actual).toEqual("123 Main Street\nUnit 2\nCity, ST\n00000, USA"); }); it("should skip undefined values", () => { const actual = formatAddress({ @@ -911,7 +940,7 @@ describe("Format address", () => { postalCode: "00000", country: "USA", }); - expect(actual).toEqual("123 Main street\nUnit 2\nST\n00000, USA"); + expect(actual).toEqual("123 Main Street\nUnit 2\nST\n00000, USA"); }); it("should return empty string if no values are available", () => { @@ -941,7 +970,7 @@ describe("Format address", () => { { includeUse: true }, ); expect(actual).toEqual( - "Home:\n123 Main street\nUnit 2\nCity, ST\n00000, USA", + "Home:\n123 Main Street\nUnit 2\nCity, ST\n00000, USA", ); }); @@ -959,7 +988,7 @@ describe("Format address", () => { { includePeriod: true }, ); expect(actual).toEqual( - "123 Main street\nUnit 2\nCity, ST\n00000, USA\nDates: 03/13/2024 - 04/14/2024", + "123 Main Street\nUnit 2\nCity, ST\n00000, USA\nDates: 03/13/2024 - 04/14/2024", ); }); @@ -977,7 +1006,7 @@ describe("Format address", () => { { includePeriod: true }, ); expect(actual).toEqual( - "123 Main street\nUnit 2\nCity, ST\n00000, USA\nDates: 03/13/2024 - Present", + "123 Main Street\nUnit 2\nCity, ST\n00000, USA\nDates: 03/13/2024 - Present", ); }); @@ -995,7 +1024,7 @@ describe("Format address", () => { { includePeriod: true }, ); expect(actual).toEqual( - "123 Main street\nUnit 2\nCity, ST\n00000, USA\nDates: Unknown - 03/13/2024", + "123 Main Street\nUnit 2\nCity, ST\n00000, USA\nDates: Unknown - 03/13/2024", ); }); @@ -1009,7 +1038,7 @@ describe("Format address", () => { use: "home", period: { start: "03/13/2024" }, }); - expect(actual).toEqual("123 Main street\nUnit 2\nCity, ST\n00000, USA"); + expect(actual).toEqual("123 Main Street\nUnit 2\nCity, ST\n00000, USA"); }); }); diff --git a/containers/ecr-viewer/src/app/tests/utils.test.tsx b/containers/ecr-viewer/src/app/tests/utils.test.tsx index 3d9b2421d3..53b7da26b3 100644 --- a/containers/ecr-viewer/src/app/tests/utils.test.tsx +++ b/containers/ecr-viewer/src/app/tests/utils.test.tsx @@ -167,8 +167,9 @@ describe("Utils", () => { const actual = evaluatePatientName( BundleWithPatient as unknown as Bundle, mappings, + false, ); - expect(actual).toEqual("ABEL CASTILLO"); + expect(actual).toEqual("Abel Castillo"); }); }); describe("Extract Patient Address", () => { @@ -183,7 +184,7 @@ describe("Utils", () => { mappings, ); - expect(actual).toEqual("1050 CARPENTER ST\nEDWARDS, CA\n93523-2800, US"); + expect(actual).toEqual("1050 Carpenter St\nEdwards, CA\n93523-2800, US"); }); }); describe("Calculate Patient Age", () => {