diff --git a/query-connector/flyway/sql/V01_06__add_dibbsConceptType.sql b/query-connector/flyway/sql/V01_06__add_dibbsConceptType.sql index 2c123b5ae..3b194618c 100644 --- a/query-connector/flyway/sql/V01_06__add_dibbsConceptType.sql +++ b/query-connector/flyway/sql/V01_06__add_dibbsConceptType.sql @@ -1,5 +1,5 @@ ALTER TABLE valuesets -ADD COLUMN dibbsConceptType text GENERATED ALWAYS AS ( +ADD COLUMN dibbs_concept_type text GENERATED ALWAYS AS ( CASE WHEN type IN ('lotc', 'lrtc', 'ostc') THEN 'labs' WHEN type = 'mrtc' THEN 'medications' diff --git a/query-connector/src/app/api/query/route.ts b/query-connector/src/app/api/query/route.ts index 1c843d752..e260ef9be 100644 --- a/query-connector/src/app/api/query/route.ts +++ b/query-connector/src/app/api/query/route.ts @@ -18,7 +18,7 @@ import { import { handleRequestError } from "./error-handling-service"; import { getSavedQueryByName, - mapQueryRowsToValueSetItems, + mapQueryRowsToConceptValueSets, } from "@/app/database-service"; /** @@ -92,7 +92,7 @@ export async function POST(request: NextRequest) { // Lookup default parameters for particular use-case search const queryName = UseCaseToQueryName[use_case as USE_CASES]; const queryResults = await getSavedQueryByName(queryName); - const vsItems = await mapQueryRowsToValueSetItems(queryResults); + const valueSets = await mapQueryRowsToConceptValueSets(queryResults); // Add params & patient identifiers to UseCaseRequest const UseCaseRequest: UseCaseQueryRequest = { @@ -111,7 +111,7 @@ export async function POST(request: NextRequest) { const UseCaseQueryResponse: QueryResponse = await UseCaseQuery( UseCaseRequest, - vsItems, + valueSets, ); // Bundle data diff --git a/query-connector/src/app/constants.ts b/query-connector/src/app/constants.ts index 1b3b6fd48..d0121f784 100644 --- a/query-connector/src/app/constants.ts +++ b/query-connector/src/app/constants.ts @@ -302,38 +302,6 @@ export const metadata = { description: "Try out TEFCA with queries for public health use cases.", }; -// TODO: Remove ValueSetItem, ValueSet, and valueSetTypeToClincalServiceTypeMap once -// ticket #2789 is resolved - -/*Type to specify the expected components for each item in a value set that will be -displayed in the CustomizeQuery component*/ -export interface ValueSetItem { - code: string; - display: string; - system: string; - include: boolean; - author: string; - clinicalServiceType: string; - valueSetName: string; -} - -/*Type to specify the expected expected types of valueset items that will be displayed -as separate tabs in the CusomizeQuery component*/ -export interface ValueSet { - labs: ValueSetItem[]; - medications: ValueSetItem[]; - conditions: ValueSetItem[]; -} - -export type ValueSetType = keyof ValueSet; - -export const valueSetTypeToClincalServiceTypeMap = { - labs: ["ostc", "lotc", "lrtc"], - medications: ["mrtc"], - conditions: ["dxtc", "sdtc"], -}; -/// TODO: Remove the above once ticket #2789 is resolved - /* * The expected type of a ValueSet concept. */ @@ -346,17 +314,17 @@ export interface Concept { /* * The expected type of a ValueSet. */ -// export interface ValueSet { -// valueset_id: string; -// valueset_version: string; -// valueset_name: string; -// author: string; -// system: string; -// ersdConceptType?: string; -// dibbsConceptType: string; -// includeValueSet: boolean; -// concepts: Concept[]; -// } +export interface ValueSet { + valueSetId: string; + valueSetVersion: string; + valueSetName: string; + author: string; + system: string; + ersdConceptType?: string; + dibbsConceptType: string; + includeValueSet: boolean; + concepts: Concept[]; +} /* * The expected type of ValueSets grouped by dibbsConceptType for the purpose of display. @@ -366,6 +334,8 @@ export interface ValueSetDisplay { medications: ValueSet[]; conditions: ValueSet[]; } +export type DibbsValueSetType = keyof ValueSetDisplay; + // Define the type guard for FHIR resources // Define the FHIR Resource types diff --git a/query-connector/src/app/database-service.ts b/query-connector/src/app/database-service.ts index a7bee9daa..3a0548ded 100644 --- a/query-connector/src/app/database-service.ts +++ b/query-connector/src/app/database-service.ts @@ -1,11 +1,11 @@ "use server"; import { Pool, PoolConfig, QueryResultRow } from "pg"; import { Bundle, OperationOutcome } from "fhir/r4"; -import { ValueSetItem, valueSetTypeToClincalServiceTypeMap } from "./constants"; import { encode } from "base-64"; +import { ValueSet } from "./constants"; const getQuerybyNameSQL = ` -select q.query_name, q.id, qtv.valueset_id, vs.name as valueset_name, vs.author as author, vs.type, qic.concept_id, qic.include, c.code, c.code_system, c.display +select q.query_name, q.id, qtv.valueset_id, vs.name as valueset_name, vs.version, vs.author as author, vs.type, vs.dibbs_concept_type as dibbs_concept_type, qic.concept_id, qic.include, c.code, c.code_system, c.display from query q left join query_to_valueset qtv on q.id = qtv.query_id left join valuesets vs on qtv.valueset_id = vs.id @@ -48,45 +48,48 @@ export const getSavedQueryByName = async (name: string) => { }; /** - * Helper function to filter the valueset-mapped rows of results returned from - * the DB for particular types of related clinical services. - * @param vsItems A list of value sets mapped from DB rows. - * @param type One of "labs", "medications", or "conditions". - * @returns A list of rows containing only the predicate service type. + * Maps the results returned from the DIBBs value set and coding system database + * into a collection of value sets, each containing one or more Concepts build out + * of the coding information in the DB. + * @param rows The Rows returned from the DB Query. + * @returns A list of ValueSets, which hold the Concepts pulled from the DB. */ -export const filterValueSets = async ( - vsItems: ValueSetItem[], - type: "labs" | "medications" | "conditions", +export const mapQueryRowsToConceptValueSets = async ( + rows: QueryResultRow[], ) => { - // Assign clinical code type based on desired filter - // Mapping is established in TCR, so follow that convention - let valuesetFilters = valueSetTypeToClincalServiceTypeMap[type]; - const results = vsItems.filter((vs) => - valuesetFilters.includes(vs.clinicalServiceType), - ); - return results; -}; + // Create groupings of rows (each of which is a single Concept) by their ValueSet ID + const vsIdGroupedRows = rows.reduce((conceptsByVSId, r) => { + if (!(r["valueset_id"] in conceptsByVSId)) { + conceptsByVSId[r["valueset_id"]] = []; + } + conceptsByVSId[r["valueset_id"]].push(r); + return conceptsByVSId; + }, {}); -/** - * Helper function that transforms and groups a set of database rows into a list of - * ValueSet items grouped by author and code_system for display on the CustomizeQuery page. - * @param rows The rows returned from the DB. - * @returns A list of ValueSetItems grouped by author and system. - */ -export const mapQueryRowsToValueSetItems = async (rows: QueryResultRow[]) => { - const vsItems = rows.map((r) => { - const vsTranslation: ValueSetItem = { - code: r["code"], - display: r["display"], - system: r["code_system"], - include: r["include"], - author: r["author"], - valueSetName: r["valueset_name"], - clinicalServiceType: r["type"], + // Each "prop" of the struct is now a ValueSet ID + // Iterate over them to create formal Concept Groups attached to a formal VS + const valueSets = Object.keys(vsIdGroupedRows).map((vsID) => { + const conceptGroup: QueryResultRow[] = vsIdGroupedRows[vsID]; + const valueSet: ValueSet = { + valueSetId: conceptGroup[0]["valueset_id"], + valueSetVersion: conceptGroup[0]["version"], + valueSetName: conceptGroup[0]["valueset_name"], + author: conceptGroup[0]["author"], + system: conceptGroup[0]["code_system"], + ersdConceptType: conceptGroup[0]["type"], + dibbsConceptType: conceptGroup[0]["dibbs_concept_type"], + includeValueSet: conceptGroup.find((c) => c["include"]) ? true : false, + concepts: conceptGroup.map((c) => { + return { + code: c["code"], + display: c["display"], + include: c["include"], + }; + }), }; - return vsTranslation; + return valueSet; }); - return vsItems; + return valueSets; }; /* diff --git a/query-connector/src/app/format-service.tsx b/query-connector/src/app/format-service.tsx index f1d9f34fe..cf3c38187 100644 --- a/query-connector/src/app/format-service.tsx +++ b/query-connector/src/app/format-service.tsx @@ -6,7 +6,7 @@ import { ContactPoint, Identifier, } from "fhir/r4"; -import { ValueSetItem } from "./constants"; +import { ValueSet } from "./constants"; import { QueryStruct } from "./query-service"; /** @@ -261,29 +261,38 @@ export async function GetPhoneQueryFormats(phone: string) { } /** - * Formats a statefully updated list of value set items into a JSON structure + * Formats a statefully updated list of value sets into a JSON structure * used for executing custom queries. * @param useCase The base use case being queried for. - * @param vsItems The list of value set items the user wants included. + * @param valueSets The list of value sets the user wants included. * @returns A structured specification of a query that can be executed. */ -export const formatValueSetItemsAsQuerySpec = async ( +export const formatValueSetsAsQuerySpec = async ( useCase: string, - vsItems: ValueSetItem[], + valueSets: ValueSet[], ) => { let secondEncounter: boolean = false; if (["cancer", "chlamydia", "gonorrhea", "syphilis"].includes(useCase)) { secondEncounter = true; } - const labCodes: string[] = vsItems + const labCodes: string[] = valueSets .filter((vs) => vs.system === "http://loinc.org") - .map((vs) => vs.code); - const snomedCodes: string[] = vsItems + .reduce((acc, vs) => { + vs.concepts.forEach((concept) => acc.push(concept.code)); + return acc; + }, [] as string[]); + const snomedCodes: string[] = valueSets .filter((vs) => vs.system === "http://snomed.info/sct") - .map((vs) => vs.code); - const rxnormCodes: string[] = vsItems + .reduce((acc, vs) => { + vs.concepts.forEach((concept) => acc.push(concept.code)); + return acc; + }, [] as string[]); + const rxnormCodes: string[] = valueSets .filter((vs) => vs.system === "http://www.nlm.nih.gov/research/umls/rxnorm") - .map((vs) => vs.code); + .reduce((acc, vs) => { + vs.concepts.forEach((concept) => acc.push(concept.code)); + return acc; + }, [] as string[]); const spec: QueryStruct = { labCodes: labCodes, diff --git a/query-connector/src/app/query-service.ts b/query-connector/src/app/query-service.ts index 441f703b4..d57335729 100644 --- a/query-connector/src/app/query-service.ts +++ b/query-connector/src/app/query-service.ts @@ -6,13 +6,14 @@ import FHIRClient from "./fhir-servers"; import { USE_CASES, FHIR_SERVERS, - ValueSetItem, + ValueSet, isFhirResource, FhirResource, } from "./constants"; + import { CustomQuery } from "./CustomQuery"; import { GetPhoneQueryFormats } from "./format-service"; -import { formatValueSetItemsAsQuerySpec } from "./format-service"; +import { formatValueSetsAsQuerySpec } from "./format-service"; /** * The query response when the request source is from the Viewer UI. @@ -146,7 +147,7 @@ async function patientQuery( */ export async function UseCaseQuery( request: UseCaseQueryRequest, - queryValueSets: ValueSetItem[], + queryValueSets: ValueSet[], queryResponse: QueryResponse = {}, ): Promise { const fhirClient = new FHIRClient(request.fhir_server); @@ -187,15 +188,12 @@ export async function UseCaseQuery( */ async function generalizedQuery( useCase: USE_CASES, - queryValueSets: ValueSetItem[], + queryValueSets: ValueSet[], patientId: string, fhirClient: FHIRClient, queryResponse: QueryResponse, ): Promise { - const querySpec = await formatValueSetItemsAsQuerySpec( - useCase, - queryValueSets, - ); + const querySpec = await formatValueSetsAsQuerySpec(useCase, queryValueSets); const builtQuery = new CustomQuery(querySpec, patientId); let response: fetch.Response | fetch.Response[]; diff --git a/query-connector/src/app/query/SelectQuery.tsx b/query-connector/src/app/query/SelectQuery.tsx index 38bbbdf59..716282c52 100644 --- a/query-connector/src/app/query/SelectQuery.tsx +++ b/query-connector/src/app/query/SelectQuery.tsx @@ -1,6 +1,6 @@ "use client"; import React, { useEffect, useState } from "react"; -import { FHIR_SERVERS, USE_CASES, ValueSetItem } from "../constants"; +import { FHIR_SERVERS, USE_CASES, ValueSet } from "../constants"; import CustomizeQuery from "./components/CustomizeQuery"; import SelectSavedQuery from "./components/selectQuery/SelectSavedQuery"; @@ -57,8 +57,8 @@ const SelectQuery: React.FC = ({ setFhirServer, setShowCustomizeQuery, }) => { - const [queryValueSets, setQueryValueSets] = useState( - [] as ValueSetItem[], + const [queryValueSets, setQueryValueSets] = useState( + [] as ValueSet[], ); const [loadingQueryValueSets, setLoadingQueryValueSets] = useState(false); @@ -97,6 +97,7 @@ const SelectQuery: React.FC = ({ } const displayLoading = loadingResultResponse || loadingQueryValueSets; + return (
{displayLoading && } diff --git a/query-connector/src/app/query/components/CustomizeQuery.tsx b/query-connector/src/app/query/components/CustomizeQuery.tsx index 0987709e0..4fc626de4 100644 --- a/query-connector/src/app/query/components/CustomizeQuery.tsx +++ b/query-connector/src/app/query/components/CustomizeQuery.tsx @@ -3,9 +3,9 @@ import React, { useState, useEffect } from "react"; import { Button } from "@trussworks/react-uswds"; import { - ValueSetType, - ValueSetItem, + DibbsValueSetType, USE_CASES, + ValueSet, demoQueryValToLabelMap, } from "../../constants"; import { UseCaseQueryResponse } from "@/app/query-service"; @@ -16,15 +16,15 @@ import CustomizeQueryAccordionHeader from "./customizeQuery/CustomizeQueryAccord import CustomizeQueryAccordionBody from "./customizeQuery/CustomizeQueryAccordionBody"; import Accordion from "../designSystem/Accordion"; import CustomizeQueryNav from "./customizeQuery/CustomizeQueryNav"; -import { mapValueSetItemsToValueSetTypes } from "./customizeQuery/customizeQueryUtils"; +import { mapValueSetsToValueSetTypes } from "./customizeQuery/customizeQueryUtils"; import Backlink from "./backLink/Backlink"; import { RETURN_LABEL } from "../stepIndicator/StepIndicator"; interface CustomizeQueryProps { useCaseQueryResponse: UseCaseQueryResponse; queryType: USE_CASES; - queryValuesets: ValueSetItem[]; - setQueryValuesets: (queryVS: ValueSetItem[]) => void; + queryValuesets: ValueSet[]; + setQueryValuesets: (queryVS: ValueSet[]) => void; goBack: () => void; } @@ -45,9 +45,9 @@ const CustomizeQuery: React.FC = ({ setQueryValuesets, goBack, }) => { - const [activeTab, setActiveTab] = useState("labs"); + const [activeTab, setActiveTab] = useState("labs"); const { labs, conditions, medications } = - mapValueSetItemsToValueSetTypes(queryValuesets); + mapValueSetsToValueSetTypes(queryValuesets); const [valueSetOptions, setValueSetOptions] = useState({ labs: labs, conditions: conditions, @@ -55,33 +55,64 @@ const CustomizeQuery: React.FC = ({ }); // Compute counts of each tab-type - const countLabs = Object.values(valueSetOptions.labs).flatMap( - (group) => group.items, - ).length; - const countConditions = Object.values(valueSetOptions.conditions).flatMap( - (group) => group.items, - ).length; - const countMedications = Object.values(valueSetOptions.medications).flatMap( - (group) => group.items, - ).length; + const countLabs = Object.values(valueSetOptions.labs).reduce( + (runningSum, gvs) => { + gvs.items.forEach((vs) => { + runningSum += vs.concepts.length; + }); + return runningSum; + }, + 0, + ); + const countConditions = Object.values(valueSetOptions.conditions).reduce( + (runningSum, gvs) => { + gvs.items.forEach((vs) => { + runningSum += vs.concepts.length; + }); + return runningSum; + }, + 0, + ); + const countMedications = Object.values(valueSetOptions.medications).reduce( + (runningSum, gvs) => { + gvs.items.forEach((vs) => { + runningSum += vs.concepts.length; + }); + return runningSum; + }, + 0, + ); // Keeps track of which side nav tab to display to users - const handleTabChange = (tab: ValueSetType) => { + const handleTabChange = (tab: DibbsValueSetType) => { setActiveTab(tab); }; - // Handles the toggle of the 'include' state for individual items - const toggleInclude = (groupIndex: string, itemIndex: number) => { + // Handles the toggle of the 'include' state for individual concepts in + // the accordion + const toggleInclude = ( + groupIndex: string, + valueSetIndex: number, + conceptIndex: number, + ) => { const updatedGroups = valueSetOptions[activeTab]; - const updatedItems = [...updatedGroups[groupIndex].items]; // Clone the current group items - updatedItems[itemIndex] = { - ...updatedItems[itemIndex], - include: !updatedItems[itemIndex].include, // Toggle the include state + const updatedValueSets = [...updatedGroups[groupIndex].items]; // Clone the current group items + const updatedConceptsInIndexedValueSet = [ + ...updatedValueSets[valueSetIndex].concepts, + ]; + updatedConceptsInIndexedValueSet[conceptIndex] = { + ...updatedConceptsInIndexedValueSet[conceptIndex], + include: !updatedConceptsInIndexedValueSet[conceptIndex].include, // Toggle the include for the clicked concept + }; + + updatedValueSets[valueSetIndex] = { + ...updatedValueSets[valueSetIndex], + concepts: updatedConceptsInIndexedValueSet, // Update the concepts in the accessed value set }; updatedGroups[groupIndex] = { ...updatedGroups[groupIndex], - items: updatedItems, // Update the group's items + items: updatedValueSets, // Update the whole group's items }; setValueSetOptions((prevState) => ({ @@ -98,7 +129,10 @@ const CustomizeQuery: React.FC = ({ updatedGroups[groupIndex].items = updatedGroups[groupIndex].items.map( (item) => ({ ...item, - include: checked, // Set all items in this group to checked or unchecked + includeValueSet: checked, // Set all items in this group to checked or unchecked + concepts: item.concepts.map((ic) => { + return { ...ic, include: checked }; + }), }), ); @@ -115,7 +149,10 @@ const CustomizeQuery: React.FC = ({ ...group, items: group.items.map((item) => ({ ...item, - include: checked, // Set all items in this group to checked or unchecked + includeValueSet: checked, // Set all items in this group to checked or unchecked + concepts: item.concepts.map((ic) => { + return { ...ic, include: checked }; + }), })), }), ); @@ -130,10 +167,10 @@ const CustomizeQuery: React.FC = ({ // by the entire query branch of the app const handleApplyChanges = () => { const selectedItems = Object.keys(valueSetOptions).reduce((acc, key) => { - const items = valueSetOptions[key as ValueSetType]; + const items = valueSetOptions[key as DibbsValueSetType]; acc = acc.concat(Object.values(items).flatMap((dict) => dict.items)); return acc; - }, [] as ValueSetItem[]); + }, [] as ValueSet[]); setQueryValuesets(selectedItems); goBack(); showRedirectConfirmation({ @@ -147,7 +184,7 @@ const CustomizeQuery: React.FC = ({ const items = Object.values(valueSetOptions[activeTab]).flatMap( (group) => group.items, ); - const selectedCount = items.filter((item) => item.include).length; + const selectedCount = items.filter((item) => item.includeValueSet).length; const topCheckbox = document.getElementById( "select-all", ) as HTMLInputElement; diff --git a/query-connector/src/app/query/components/customizeQuery/CustomizeQueryAccordionBody.tsx b/query-connector/src/app/query/components/customizeQuery/CustomizeQueryAccordionBody.tsx index b602f609f..e67b1a6a3 100644 --- a/query-connector/src/app/query/components/customizeQuery/CustomizeQueryAccordionBody.tsx +++ b/query-connector/src/app/query/components/customizeQuery/CustomizeQueryAccordionBody.tsx @@ -5,16 +5,27 @@ import Table from "../../designSystem/Table"; type CustomizeQueryAccordionBodyProps = { group: GroupedValueSet; - toggleInclude: (groupIndex: string, itemIndex: number) => void; + toggleInclude: ( + groupIndex: string, + valueSetIndex: number, + conceptIndex: number, + ) => void; groupIndex: string; }; +type ValueSetIndexedConcept = { + vsIndex: number; + code: string; + display: string; + include: boolean; +}; + /** * Styling component to render the body table for the customize query components * @param param0 - props for rendering * @param param0.group - Matched concept associated with the query that * contains valuesets to filter query on - * @param param0.toggleInclude - Listener event to handle a valueset inclusion/ + * @param param0.toggleInclude - Listener event to handle a concept inclusion/ * exclusion check * @param param0.groupIndex - Index corresponding to group * @returns JSX Fragment for the accordion body @@ -32,32 +43,39 @@ const CustomizeQueryAccordionBody: React.FC< - {group.items.map((item, index) => ( - - { - e.stopPropagation(); - toggleInclude(groupIndex, index); - }} - > - {item.include && ( - - )} - - - {item.code} - - - {item.display} - - - ))} + {group.items + .reduce((acc, vs, vsIndex) => { + vs.concepts.forEach((c) => { + acc.push({ ...c, vsIndex: vsIndex }); + }); + return acc; + }, [] as ValueSetIndexedConcept[]) + .map((item, conceptIndex) => ( + + { + e.stopPropagation(); + toggleInclude(groupIndex, item.vsIndex, conceptIndex); + }} + > + {item.include && ( + + )} + + + {item.code} + + + {item.display} + + + ))} ); diff --git a/query-connector/src/app/query/components/customizeQuery/CustomizeQueryAccordionHeader.tsx b/query-connector/src/app/query/components/customizeQuery/CustomizeQueryAccordionHeader.tsx index 77f9240c5..2ccc28fe1 100644 --- a/query-connector/src/app/query/components/customizeQuery/CustomizeQueryAccordionHeader.tsx +++ b/query-connector/src/app/query/components/customizeQuery/CustomizeQueryAccordionHeader.tsx @@ -22,8 +22,15 @@ const CustomizeQueryAccordionHeader: React.FC = ({ groupIndex, group, }) => { - const selectedTotal = group.items.length; - const selectedCount = group.items.filter((item) => item.include).length; + const selectedTotal = group.items.reduce((sum, vs) => { + sum += vs.concepts.length; + return sum; + }, 0); + const selectedCount = group.items.reduce((sum, vs) => { + const includedConcepts = vs.concepts.filter((c) => c.include); + sum += includedConcepts.length; + return sum; + }, 0); return (
= ({ className={`hide-checkbox-label ${styles.customizeQueryCheckbox}`} onClick={(e) => { e.stopPropagation(); - handleSelectAllChange( - groupIndex, - selectedCount !== group.items.length, - ); + handleSelectAllChange(groupIndex, selectedCount !== selectedTotal); }} > - {selectedCount === group.items.length && ( + {selectedCount === selectedTotal && ( = ({ aria-label="Checkmark icon indicating addition" /> )} - {selectedCount > 0 && selectedCount < group.items.length && ( + {selectedCount > 0 && selectedCount < selectedTotal && ( void; + activeTab: DibbsValueSetType; + handleTabChange: (tabName: DibbsValueSetType) => void; handleSelectAllForTab: (checked: boolean) => void; valueSetOptions: { - [key in ValueSetType]: { [vsNameAuthorSystem: string]: GroupedValueSet }; + [key in DibbsValueSetType]: { + [vsNameAuthorSystem: string]: GroupedValueSet; + }; }; }; @@ -32,11 +34,15 @@ const CustomizeQueryNav: React.FC = ({ (group) => group.items.length > 0, ); const allItemsDeselected = Object.values(valueSetOptions[activeTab]) - .flatMap((groupedValSets) => groupedValSets.items.flatMap((i) => i.include)) + .flatMap((groupedValSets) => + groupedValSets.items.flatMap((i) => i.includeValueSet), + ) .every((p) => !p); const allItemsSelected = Object.values(valueSetOptions[activeTab]) - .flatMap((groupedValSets) => groupedValSets.items.flatMap((i) => i.include)) + .flatMap((groupedValSets) => + groupedValSets.items.flatMap((i) => i.includeValueSet), + ) .every((p) => p); return ( diff --git a/query-connector/src/app/query/components/customizeQuery/customizeQueryUtils.ts b/query-connector/src/app/query/components/customizeQuery/customizeQueryUtils.ts index 3560be53e..2f4f7eee5 100644 --- a/query-connector/src/app/query/components/customizeQuery/customizeQueryUtils.ts +++ b/query-connector/src/app/query/components/customizeQuery/customizeQueryUtils.ts @@ -1,14 +1,10 @@ -import { - ValueSetItem, - ValueSetType, - valueSetTypeToClincalServiceTypeMap, -} from "@/app/constants"; +import { DibbsValueSetType, ValueSet } from "@/app/constants"; export type GroupedValueSet = { valueSetName: string; author: string; system: string; - items: ValueSetItem[]; + items: ValueSet[]; }; /** @@ -21,7 +17,7 @@ export type GroupedValueSet = { * of valueSetName:author:system and the values are all the value set items that * share those identifiers in common, structed as a GroupedValueSet */ -function groupValueSetsByNameAuthorSystem(valueSetsToGroup: ValueSetItem[]) { +function groupValueSetsByNameAuthorSystem(valueSetsToGroup: ValueSet[]) { const results = valueSetsToGroup.reduce( (acc, row) => { // Check if both author and code_system are defined @@ -30,7 +26,7 @@ function groupValueSetsByNameAuthorSystem(valueSetsToGroup: ValueSetItem[]) { const valueSetName = row?.valueSetName; if (!author || !system || !valueSetName) { console.warn( - `Skipping malformed row: Missing author (${author}) or system (${system}) for code (${row?.code})`, + `Skipping malformed row: Missing author (${author}) or system (${system}) for ValueSet (${row?.valueSetId})`, ); return acc; } @@ -45,13 +41,17 @@ function groupValueSetsByNameAuthorSystem(valueSetsToGroup: ValueSetItem[]) { }; } acc[groupKey].items.push({ - code: row["code"], - display: row["display"], - system: row["system"], - include: row["include"], - author: row["author"], - valueSetName: row["valueSetName"], - clinicalServiceType: row["clinicalServiceType"], + valueSetId: row.valueSetId, + valueSetVersion: row.valueSetVersion, + valueSetName: row.valueSetName, + author: row.author, + system: row.system, + ersdConceptType: row.ersdConceptType, + dibbsConceptType: row.dibbsConceptType, + includeValueSet: row.includeValueSet, + concepts: row.concepts.map((c) => { + return { ...c }; + }), }); return acc; }, @@ -62,7 +62,7 @@ function groupValueSetsByNameAuthorSystem(valueSetsToGroup: ValueSetItem[]) { } export type TypeIndexedGroupedValueSetDictionary = { - [valueSetType in ValueSetType]: { + [valueSetType in DibbsValueSetType]: { [vsNameAuthorSystem: string]: GroupedValueSet; }; }; @@ -74,17 +74,18 @@ export type TypeIndexedGroupedValueSetDictionary = { * object, with index of labs, conditions, medications that we display on the * customize query page, where each dictionary is a separate accordion grouping * of ValueSetItems that users can select to filter their custom queries with - * @param vsItemArray - an array of ValueSetItems to group + * @param vsArray - an array of ValueSets to group * @returns A dictionary of * dictionaries, where the first index is the ValueSetType, which indexes a * dictionary of GroupedValueSets. The subdictionary is indexed by * valueSetName:author:system */ -export function mapValueSetItemsToValueSetTypes(vsItemArray: ValueSetItem[]) { - const valueSetsByNameAuthorSystem = - groupValueSetsByNameAuthorSystem(vsItemArray); +export function mapValueSetsToValueSetTypes(vsArray: ValueSet[]) { + const valueSetsByNameAuthorSystem = groupValueSetsByNameAuthorSystem(vsArray); const results: { - [vsType in ValueSetType]: { [vsNameAuthorSystem: string]: GroupedValueSet }; + [vsType in DibbsValueSetType]: { + [vsNameAuthorSystem: string]: GroupedValueSet; + }; } = { labs: {}, conditions: {}, @@ -102,7 +103,7 @@ export function mapValueSetItemsToValueSetTypes(vsItemArray: ValueSetItem[]) { // GroupedValueSets (ie the groupings on the other tabs) that we don't // want to display, so we should filter those out. if (items.length > 0) { - results[valueSetTypeKey as ValueSetType][nameAuthorSystem] = { + results[valueSetTypeKey as DibbsValueSetType][nameAuthorSystem] = { ...groupedValueSet, items: items, }; @@ -115,26 +116,22 @@ export function mapValueSetItemsToValueSetTypes(vsItemArray: ValueSetItem[]) { } /** - * Helper function to map an array of value set items into their lab, medication, + * Helper function to map an array of value sets into their lab, medication, * condition buckets to be displayed on the customize query page - * @param vsItems A list of value sets mapped from DB rows. + * @param valueSets - A list of value sets mapped from DB rows. * @returns Dict of list of rows containing only the predicate service type * mapped to one of "labs", "medications", or "conditions". */ -export const mapValueSetsToValueSetType = (vsItems: ValueSetItem[]) => { - const results: { [vsType in ValueSetType]: ValueSetItem[] } = { +export const mapValueSetsToValueSetType = (valueSets: ValueSet[]) => { + const results: { [vsType in DibbsValueSetType]: ValueSet[] } = { labs: [], medications: [], conditions: [], }; - ( - Object.keys(valueSetTypeToClincalServiceTypeMap) as Array - ).forEach((vsType) => { - const itemsToInclude = vsItems.filter((vs) => { - return valueSetTypeToClincalServiceTypeMap[vsType].includes( - vs.clinicalServiceType, - ); - }); + (Object.keys(results) as Array).forEach((vsType) => { + const itemsToInclude = valueSets.filter( + (vs) => vs.dibbsConceptType === vsType, + ); results[vsType] = itemsToInclude; }); diff --git a/query-connector/src/app/query/components/selectQuery/queryHooks.ts b/query-connector/src/app/query/components/selectQuery/queryHooks.ts index a12211a20..6c1d97ed5 100644 --- a/query-connector/src/app/query/components/selectQuery/queryHooks.ts +++ b/query-connector/src/app/query/components/selectQuery/queryHooks.ts @@ -2,12 +2,12 @@ import { FHIR_SERVERS, USE_CASES, UseCaseToQueryName, - ValueSetItem, + ValueSet, hyperUnluckyPatient, } from "@/app/constants"; import { getSavedQueryByName, - mapQueryRowsToValueSetItems, + mapQueryRowsToConceptValueSets, } from "@/app/database-service"; import { UseCaseQuery, UseCaseQueryResponse } from "@/app/query-service"; import { Patient } from "fhir/r4"; @@ -23,7 +23,7 @@ type SetStateCallback = React.Dispatch>; */ export async function fetchUseCaseValueSets( selectedQuery: USE_CASES, - valueSetStateCallback: SetStateCallback, + valueSetStateCallback: SetStateCallback, isSubscribed: boolean, setIsLoading: (isLoading: boolean) => void, ) { @@ -32,11 +32,11 @@ export async function fetchUseCaseValueSets( setIsLoading(true); const queryResults = await getSavedQueryByName(queryName); - const vsItems = await mapQueryRowsToValueSetItems(queryResults); + const valueSets = await mapQueryRowsToConceptValueSets(queryResults); // Only update if the fetch hasn't altered state yet if (isSubscribed) { - valueSetStateCallback(vsItems); + valueSetStateCallback(valueSets); } setIsLoading(false); } @@ -56,7 +56,7 @@ export async function fetchUseCaseValueSets( export async function fetchQueryResponse(p: { patientForQuery: Patient | undefined; selectedQuery: USE_CASES; - queryValueSets: ValueSetItem[]; + queryValueSets: ValueSet[]; fhirServer: FHIR_SERVERS; queryResponseStateCallback: SetStateCallback; setIsLoading: (isLoading: boolean) => void; @@ -82,14 +82,21 @@ export async function fetchQueryResponse(p: { use_case: p.selectedQuery, }; + // Need to also filter down by concepts to only display desired info + const filteredValueSets = p.queryValueSets + .filter((item) => item.includeValueSet) + .map((fvs) => { + const conceptFilteredVS: ValueSet = { + ...fvs, + concepts: fvs.concepts.filter((c) => c.include), + }; + return conceptFilteredVS; + }); + p.setIsLoading(true); - const queryResponse = await UseCaseQuery( - newRequest, - p.queryValueSets.filter((item) => item.include), - { - Patient: [p.patientForQuery], - }, - ); + const queryResponse = await UseCaseQuery(newRequest, filteredValueSets, { + Patient: [p.patientForQuery], + }); p.queryResponseStateCallback(queryResponse); p.setIsLoading(false); }