diff --git a/query-connector/e2e/query_workflow.spec.ts b/query-connector/e2e/query_workflow.spec.ts index a11bdf0e3..d85f33121 100644 --- a/query-connector/e2e/query_workflow.spec.ts +++ b/query-connector/e2e/query_workflow.spec.ts @@ -134,13 +134,13 @@ test.describe("querying with the Query Connector", () => { .filter({ hasText: "Sexual overexposure" }) .getByRole("row"), ).toHaveCount(5); - // Conditions + // Conditions + Medication Requests (Reason Code) await expect( page .getByRole("table") .filter({ hasText: "Chlamydial infection, unspecified" }) .getByRole("row"), - ).toHaveCount(3); + ).toHaveCount(10); // Diagnostic Reports await expect( page diff --git a/query-connector/src/app/CustomQuery.ts b/query-connector/src/app/CustomQuery.ts index 4f1b8b642..a567cb1fc 100644 --- a/query-connector/src/app/CustomQuery.ts +++ b/query-connector/src/app/CustomQuery.ts @@ -2,7 +2,6 @@ interface CustomQuerySpec { labCodes?: string[]; snomedCodes?: string[]; rxnormCodes?: string[]; - classTypeCodes?: string[]; hasSecondEncounterQuery?: boolean; } @@ -19,7 +18,6 @@ export class CustomQuery { labCodes: string[] = []; snomedCodes: string[] = []; rxnormCodes: string[] = []; - classTypeCodes: string[] = []; // Need default initialization of query strings outstide constructor, // since the `try` means we might not find the JSON spec @@ -29,7 +27,6 @@ export class CustomQuery { medicationRequestQuery: string = ""; socialHistoryQuery: string = ""; encounterQuery: string = ""; - encounterClassTypeQuery: string = ""; immunizationQuery: string = ""; // Some queries need to be batched in waves because their encounter references @@ -49,7 +46,6 @@ export class CustomQuery { this.labCodes = jsonSpec?.labCodes || []; this.snomedCodes = jsonSpec?.snomedCodes || []; this.rxnormCodes = jsonSpec?.rxnormCodes || []; - this.classTypeCodes = jsonSpec?.classTypeCodes || []; this.hasSecondEncounterQuery = jsonSpec?.hasSecondEncounterQuery || false; this.compileQueries(patientId); } catch (error) { @@ -68,7 +64,6 @@ export class CustomQuery { const labsFilter = this.labCodes.join(","); const snomedFilter = this.snomedCodes.join(","); const rxnormFilter = this.rxnormCodes.join(","); - const classTypeFilter = this.classTypeCodes.join(","); this.observationQuery = labsFilter !== "" @@ -91,10 +86,6 @@ export class CustomQuery { snomedFilter !== "" ? `/Encounter?subject=${patientId}&reason-code=${snomedFilter}` : ""; - this.encounterClassTypeQuery = - classTypeFilter !== "" - ? `/Encounter?subject=${patientId}&class=${classTypeFilter}` - : ""; this.immunizationQuery = `/Immunization?patient=${patientId}`; } @@ -111,7 +102,6 @@ export class CustomQuery { this.medicationRequestQuery, this.socialHistoryQuery, this.encounterQuery, - this.encounterClassTypeQuery, this.immunizationQuery, ]; const filteredRequests = queryRequests.filter((q) => q !== ""); @@ -138,8 +128,6 @@ export class CustomQuery { return this.socialHistoryQuery; case "encounter": return this.encounterQuery; - case "encounterClass": - return this.encounterClassTypeQuery; case "immunization": return this.immunizationQuery; default: diff --git a/query-connector/src/app/format-service.tsx b/query-connector/src/app/format-service.tsx index fc11edddc..e9bf1ce21 100644 --- a/query-connector/src/app/format-service.tsx +++ b/query-connector/src/app/format-service.tsx @@ -300,7 +300,6 @@ export const formatValueSetsAsQuerySpec = async ( labCodes: labCodes, snomedCodes: snomedCodes, rxnormCodes: rxnormCodes, - classTypeCodes: [] as string[], hasSecondEncounterQuery: secondEncounter, }; @@ -320,3 +319,21 @@ export const formatImmunizationRoute = (immunization: Immunization): string => { ); return readable?.[0].display ?? initial; }; + +/** + * Formats a Coding object for display. If the object has a coding array, + * the first coding object is used. + * @param coding - The Coding object. + * @returns The Coding data formatted for display. + */ +export function formatCoding(coding: Coding | undefined) { + if (!coding) { + return ""; + } + return ( + <> + {" "} + {coding?.display}
{coding?.code}
{coding?.system}{" "} + + ); +} diff --git a/query-connector/src/app/query-service.ts b/query-connector/src/app/query-service.ts index 8dc41c578..3dd724714 100644 --- a/query-connector/src/app/query-service.ts +++ b/query-connector/src/app/query-service.ts @@ -43,7 +43,6 @@ export type QueryStruct = { labCodes: string[]; snomedCodes: string[]; rxnormCodes: string[]; - classTypeCodes: string[]; hasSecondEncounterQuery: boolean; }; diff --git a/query-connector/src/app/query/components/resultsView/tableComponents/ConditionsTable.tsx b/query-connector/src/app/query/components/resultsView/tableComponents/ConditionsTable.tsx index 86463db0c..6d3f23547 100644 --- a/query-connector/src/app/query/components/resultsView/tableComponents/ConditionsTable.tsx +++ b/query-connector/src/app/query/components/resultsView/tableComponents/ConditionsTable.tsx @@ -20,20 +20,11 @@ export interface ConditionTableProps { * @returns - The ConditionTable component. */ const ConditionsTable: React.FC = ({ conditions }) => { - const anyResolution = checkIfSomeElementWithPropertyExists( - conditions, + const availableElements = checkIfSomeElementWithPropertyExists(conditions, [ "abatementDateTime", - ); - - const anyStatus = checkIfSomeElementWithPropertyExists( - conditions, "clinicalStatus", - ); - - const anyOnset = checkIfSomeElementWithPropertyExists( - conditions, "onsetDateTime", - ); + ]); return ( = ({ conditions }) => { - {anyStatus && } - {anyOnset && } - {anyResolution && } + {availableElements.clinicalStatus && } + {availableElements.onsetDateTime && } + {availableElements.abatementDateTime && } {conditions.map((condition) => ( - {anyStatus && ( + {availableElements.clinicalStatus && ( )} - {anyOnset && } - {anyResolution && ( + {availableElements.onsetDateTime && ( + + )} + {availableElements.abatementDateTime && ( )} diff --git a/query-connector/src/app/query/components/resultsView/tableComponents/EncounterTable.tsx b/query-connector/src/app/query/components/resultsView/tableComponents/EncounterTable.tsx index 761d1807b..1ee8b1274 100644 --- a/query-connector/src/app/query/components/resultsView/tableComponents/EncounterTable.tsx +++ b/query-connector/src/app/query/components/resultsView/tableComponents/EncounterTable.tsx @@ -1,7 +1,11 @@ import React from "react"; import Table from "@/app/query/designSystem/table/Table"; import { Encounter } from "fhir/r4"; -import { formatCodeableConcept, formatDate } from "../../../../format-service"; +import { + formatCodeableConcept, + formatCoding, + formatDate, +} from "../../../../format-service"; import { checkIfSomeElementWithPropertyExists } from "./utils"; import styles from "./resultsTables.module.scss"; @@ -21,21 +25,19 @@ export interface EncounterTableProps { const EncounterTable: React.FC = ({ encounters: encounters, }) => { - const anyClinicType = - checkIfSomeElementWithPropertyExists(encounters, "class") || - checkIfSomeElementWithPropertyExists(encounters, "serviceType"); - - const anyServiceType = checkIfSomeElementWithPropertyExists( - encounters, + const availableElements = checkIfSomeElementWithPropertyExists(encounters, [ + "class", "serviceProvider", - ); + "serviceType", + ]); + return (
ConditionStatusOnsetResolutionStatusOnsetResolution
{formatCodeableConcept(condition.code ?? {})}{formatCodeableConcept(condition.clinicalStatus ?? {})}{formatDate(condition.onsetDateTime)}{formatDate(condition.onsetDateTime)}{formatDate(condition.abatementDateTime)}
- {anyClinicType && } - {anyServiceType && } + {availableElements?.class && } + {availableElements?.serviceProvider && } @@ -45,15 +47,12 @@ const EncounterTable: React.FC = ({ {encounters.map((encounter) => ( - {anyClinicType && ( - + {availableElements?.class && ( + + )} + {availableElements?.serviceProvider && ( + )} - {anyServiceType && } diff --git a/query-connector/src/app/query/components/resultsView/tableComponents/MedicationRequestTable.tsx b/query-connector/src/app/query/components/resultsView/tableComponents/MedicationRequestTable.tsx index 02ecfde56..95088bc11 100644 --- a/query-connector/src/app/query/components/resultsView/tableComponents/MedicationRequestTable.tsx +++ b/query-connector/src/app/query/components/resultsView/tableComponents/MedicationRequestTable.tsx @@ -21,9 +21,9 @@ export interface MedicationRequestTableProps { const MedicationRequestTable: React.FC = ({ medicationRequests, }) => { - const anyReasonCode = checkIfSomeElementWithPropertyExists( + const availableElements = checkIfSomeElementWithPropertyExists( medicationRequests, - "reasonCode", + ["reasonCode"], ); return ( @@ -32,7 +32,7 @@ const MedicationRequestTable: React.FC = ({ - {anyReasonCode && } + {availableElements.reasonCode && } @@ -45,7 +45,7 @@ const MedicationRequestTable: React.FC = ({ medicationRequest.medicationCodeableConcept, )} - {anyReasonCode && ( + {availableElements.reasonCode && ( diff --git a/query-connector/src/app/query/components/resultsView/tableComponents/ObservationTable.tsx b/query-connector/src/app/query/components/resultsView/tableComponents/ObservationTable.tsx index 56d6644c6..09bf0bab4 100644 --- a/query-connector/src/app/query/components/resultsView/tableComponents/ObservationTable.tsx +++ b/query-connector/src/app/query/components/resultsView/tableComponents/ObservationTable.tsx @@ -22,23 +22,19 @@ export interface ObservationTableProps { const ObservationTable: React.FC = ({ observations, }) => { - const anyObsInterpretation = checkIfSomeElementWithPropertyExists( - observations, + const availableElements = checkIfSomeElementWithPropertyExists(observations, [ "interpretation", - ); - const anyReferenceRange = checkIfSomeElementWithPropertyExists( - observations, "referenceRange", - ); + ]); return (
Visit ReasonClinic TypeService ProviderClinic TypeService ProviderEncounter Status Encounter Start Encounter End
{formatCodeableConcept(encounter?.reasonCode?.[0])} - {formatCodeableConcept(encounter?.class)}

- {encounter?.serviceType - ? formatCodeableConcept(encounter.serviceType) - : ""} -
{formatCoding(encounter?.class)}{encounter?.serviceProvider?.display}{encounter?.serviceProvider?.display}{encounter?.status} {formatDate(encounter?.period?.start)} {formatDate(encounter?.period?.end)}
Order Date MedicationReason CodeReason CodeStatus
{formatCodeableConcept(medicationRequest?.reasonCode?.[0])}
- {anyObsInterpretation && } + {availableElements.interpretation && } - {anyReferenceRange && } + {availableElements.referenceRange && } @@ -46,7 +42,7 @@ const ObservationTable: React.FC = ({ - {anyObsInterpretation && ( + {availableElements.interpretation && ( )} - {anyReferenceRange && } + {availableElements.referenceRange && ( + + )} ))} diff --git a/query-connector/src/app/query/components/resultsView/tableComponents/resultsTables.module.scss b/query-connector/src/app/query/components/resultsView/tableComponents/resultsTables.module.scss index 753fe73fe..8fb4c762b 100644 --- a/query-connector/src/app/query/components/resultsView/tableComponents/resultsTables.module.scss +++ b/query-connector/src/app/query/components/resultsView/tableComponents/resultsTables.module.scss @@ -9,36 +9,50 @@ $demographicsColumnSpacing: 1fr 3fr; $immunizationColumnSpacing: 1fr 3fr 1fr 1fr; .demographicsRow { - grid-template-columns: $demographicsColumnSpacing; + grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); gap: $columnGap; + word-wrap: break-word; + overflow-wrap: break-word; } .conditionRow { - grid-template-columns: $conditionColumnSpacing; + grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); gap: $columnGap; + word-wrap: break-word; + overflow-wrap: break-word; } .observationRow { - grid-template-columns: $observationColumnSpacing; + grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); gap: $columnGap; + word-wrap: break-word; + overflow-wrap: break-word; } .medicationRow { - grid-template-columns: $medicationColumnSpacing; + grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); gap: $columnGap; + word-wrap: break-word; + overflow-wrap: break-word; } .encountersRow { - grid-template-columns: $encountersColumnSpacing; + grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); gap: $columnGap; + word-wrap: break-word; + overflow-wrap: break-word; } .diagnosticsRow { - grid-template-columns: $diagnosticsColumnSpacing; + grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); gap: $columnGap; + word-wrap: break-word; + overflow-wrap: break-word; } .immunizationRow { - grid-template-columns: $immunizationColumnSpacing; + grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); gap: $columnGap; + word-wrap: break-word; + overflow-wrap: break-word; } diff --git a/query-connector/src/app/query/components/resultsView/tableComponents/utils.ts b/query-connector/src/app/query/components/resultsView/tableComponents/utils.ts index af3af7f83..d3ae02848 100644 --- a/query-connector/src/app/query/components/resultsView/tableComponents/utils.ts +++ b/query-connector/src/app/query/components/resultsView/tableComponents/utils.ts @@ -1,27 +1,31 @@ -type Lengthwise = { - length: number; -}; - -function isLengthwise(thing: unknown): thing is Lengthwise { - return typeof thing === "object" && thing !== null && "length" in thing; -} - /** * Helper function to not display tables in results view where there are no * elements in a column to display. Allows for non-lengthwise properties * @param array - an array of items to display in a table - * @param propertyToCheck - the property that we want to conditionally display + * @param propertiesToCheck - the property that we want to conditionally display * in a table column if some element exists * @returns true or false for whether there is any element that exists for the * column to be rendered */ -export function checkIfSomeElementWithPropertyExists( - array: T[], - propertyToCheck: K, -): boolean { - return array - .map((e) => e[propertyToCheck]) - .some((prop) => { - prop != undefined && isLengthwise(prop) ? prop.length > 0 : true; - }); +export function checkIfSomeElementWithPropertyExists< + T extends object, + K extends keyof T, +>(array: T[], propertiesToCheck: K[]): Record { + const result: Record = propertiesToCheck.reduce( + (accumulation, p) => { + accumulation[p] = false; + return accumulation; + }, + {} as Record, + ); + + for (const resource of array) { + for (const property of propertiesToCheck) { + if (property in resource) { + result[property] = true; + } + } + } + + return result; } diff --git a/query-connector/src/app/tests/unit/format-service.test.tsx b/query-connector/src/app/tests/unit/format-service.test.tsx index ea9105c9f..ebcb50d6e 100644 --- a/query-connector/src/app/tests/unit/format-service.test.tsx +++ b/query-connector/src/app/tests/unit/format-service.test.tsx @@ -2,6 +2,7 @@ import { render, screen } from "@testing-library/react"; import { formatAddress, formatContact, + formatCoding, formatCodeableConcept, formatDate, formatIdentifier, @@ -17,6 +18,7 @@ import { ContactPoint, Identifier, CodeableConcept, + Coding, } from "fhir/r4"; describe("formatDate", () => { @@ -495,3 +497,35 @@ describe("GetPhoneQueryFormats", () => { expect(await GetPhoneQueryFormats(inputPhone)).toEqual(expectedResult); }); }); + +describe("formatCoding", () => { + it("should return an empty string when coding is undefined", () => { + const result = formatCoding(undefined); + expect(result).toBe(""); + }); + + it("should return the display, code, and system", () => { + const coding: Coding = { + display: "Example Display", + code: "Example Code", + system: "Example System", + }; + + const { getByText } = render(formatCoding(coding)); + expect(getByText(/Example Display/)).toBeInTheDocument(); + expect(getByText(/Example Code/)).toBeInTheDocument(); + expect(getByText(/Example System/)).toBeInTheDocument(); + }); + + it("should return the parts of the display, code, and system", () => { + const coding: Coding = { + display: "Example Display", + system: "Example System", + }; + + const { queryByText } = render(formatCoding(coding)); + expect(queryByText(/Example Display/)).toBeInTheDocument(); + expect(queryByText(/Example Code/)).not.toBeInTheDocument(); + expect(queryByText(/Example System/)).toBeInTheDocument(); + }); +});
Date TypeInterpretationInterpretationValueReference RangeReference Range
{formatDate(obs?.issued || obs?.effectiveDateTime)} {formatCodeableConcept(obs.code)} {obs?.interpretation && obs.interpretation.length > 0 ? formatCodeableConcept(obs.interpretation[0]) @@ -54,7 +50,9 @@ const ObservationTable: React.FC = ({ {formatValue(obs)}{formatReferenceRange(obs)}{formatReferenceRange(obs)}