From 1fcad0be34b7a61d9c4ff06f09c77d597373f388 Mon Sep 17 00:00:00 2001 From: LaurenD Date: Sat, 12 Jul 2025 22:19:25 -0400 Subject: [PATCH 1/6] POST version of evaluate map posted ID to local test data Update comment --- .../calculation/PopulationCalculation.tsx | 169 ++++++++++++++++-- 1 file changed, 159 insertions(+), 10 deletions(-) diff --git a/components/calculation/PopulationCalculation.tsx b/components/calculation/PopulationCalculation.tsx index aefca50..340918a 100644 --- a/components/calculation/PopulationCalculation.tsx +++ b/components/calculation/PopulationCalculation.tsx @@ -1,7 +1,8 @@ -import { Button, Center, Drawer, Group, Modal, Tooltip } from '@mantine/core'; +import { Button, Center, Drawer, Grid, Group, Modal, Radio, Select, Space, Text, Tooltip } from '@mantine/core'; +import CodeMirror from '@uiw/react-codemirror'; import { useRecoilValue } from 'recoil'; import { Calculator, CalculatorTypes } from 'fqm-execution'; -import { patientTestCaseState } from '../../state/atoms/patientTestCase'; +import { patientTestCaseState, TestCaseInfo } from '../../state/atoms/patientTestCase'; import { measureBundleState } from '../../state/atoms/measureBundle'; import { useState } from 'react'; import { measurementPeriodFormattedState } from '../../state/atoms/measurementPeriod'; @@ -35,6 +36,10 @@ export default function PopulationCalculation() { const [clauseCoverageHTML, setClauseCoverageHTML] = useState(null); const [clauseUncoverageHTML, setClauseUncoverageHTML] = useState(null); const trustMetaProfile = useRecoilValue(trustMetaProfileState); + const [reportTypeValue, setReportTypeValue] = useState('population'); + const [subjectValue, setSubjectValue] = useState(''); + const [subjectData, setSubjectData] = useState<{ value: string; label: string }[]>([]); + const [evaluateText, setEvaluateText] = useState(''); /** * Creates object that maps patient ids to their name/DOB info strings. @@ -53,15 +58,19 @@ export default function PopulationCalculation() { * without using Measure/$deqm-submit-data endpoint * Each transaction bundle should contain DEQM Data Exchange MeasureReports with data-of-interest * and should be for a single subject (will do a separate POST for each patient) - * @returns { string|undefined[] } the evaluation service ids for POSTed patients (may be different than sent IDs or undefined if send was unsuccessful) + * @returns { {postedId: string, testCaseInfo: testCaseInfo}|undefined[] } objects with the evaluation service ids for POSTed patients (may be different than sent IDs or undefined if send was unsuccessful) and the corresponding test case information */ - const submitDataToEvaluationService = async (): Promise<(string | undefined)[]> => { + const submitDataToEvaluationService = async (): Promise< + ({ postedId: string; testCaseInfo: TestCaseInfo } | undefined)[] + > => { // collect data and POST to evaluation service (TODO: should be $submit-data when available) const measure = measureBundle.content?.entry?.find(e => e.resource?.resourceType === 'Measure') ?.resource as fhir4.Measure; - const postedIds: Promise[] = Object.keys(currentPatients).map(async id => { + const postedIds: Promise<{ postedId: string; testCaseInfo: TestCaseInfo } | undefined>[] = Object.keys( + currentPatients + ).map(async id => { const bundle = createPatientBundle( currentPatients[id].patient, minimizeTestCaseResources(currentPatients[id], measureBundle.content, drLookupByType), @@ -87,9 +96,10 @@ export default function PopulationCalculation() { } // should return transaction response bundles from which we can pull the posted patient id - return responseBody.entry + const locationId = responseBody.entry ?.find(e => e.response?.location?.includes('Patient/')) ?.response?.location?.split('Patient/')[1]; + return locationId ? { postedId: locationId, testCaseInfo: currentPatients[id] } : undefined; }); return Promise.all(postedIds); @@ -101,7 +111,7 @@ export default function PopulationCalculation() { const submitData = () => { submitDataToEvaluationService() .then(postedIds => { - const resolvedIds = postedIds?.filter(id => id !== undefined); + const resolvedIds = postedIds?.filter(idObj => idObj !== undefined); if (resolvedIds.length > 0) { showNotification({ icon: , @@ -109,7 +119,13 @@ export default function PopulationCalculation() { message: `Successfully sent data for ${resolvedIds.length} patients for measure ${evaluationMeasureId}`, //TODO: update to canonical, currently using evaluationMeasureId here whereas it will be used for $submit-data in the future color: 'green' }); - // TODO: use resolvedIds to populate evaluate modal + setSubjectData( + resolvedIds.map(idObj => { + return { value: idObj.postedId, label: getPatientNameString(idObj.testCaseInfo.patient) }; + }) + ); + + // TODO: fix this to have better labels setEnableEvaluateButton(true); } else { showNotification({ @@ -132,6 +148,84 @@ export default function PopulationCalculation() { }); }; + const sendEvaluate = async (): Promise => { + const parameters: fhir4.Parameters = { + resourceType: 'Parameters', + parameter: [ + { + name: 'measureId', + valueString: evaluationMeasureId + }, + { + name: 'periodStart', + valueString: measurementPeriodFormatted?.start + }, + { + name: 'periodEnd', + valueString: measurementPeriodFormatted?.end + }, + { + name: 'reportType', + valueString: reportTypeValue + } + ] + }; + if (reportTypeValue === 'individual' && subjectValue) { + parameters.parameter?.push({ + name: 'subject', + valueString: `Patient/${subjectValue}` + }); + } + const response = await fetch(`${evaluationServiceUrl}/Measure/$evaluate`, { + method: 'POST', + body: JSON.stringify(parameters), + headers: { 'Content-Type': 'application/json+fhir' } + }); + if (!response.ok) { + showNotification({ + icon: , + title: 'Evaluation service failure', + message: `Evaluation call failed with code ${response.status}`, + color: 'red' + }); + return; + } + const responseBody: fhir4.Bundle | fhir4.OperationOutcome = await response.json(); + if (responseBody.resourceType === 'OperationOutcome') { + showNotification({ + icon: , + title: 'Evaluation operation failed', + message: `Evaluation call failed with message: "${responseBody.issue[0].details?.text}"`, + color: 'red' + }); + return; + } + + return responseBody; + }; + + /** + * Wrapper function that calls sendEvaluate() and populates the results view + */ + const onEvaluate = (): void => { + sendEvaluate() + .then(responseBundle => { + if (responseBundle) { + setEvaluateText(JSON.stringify(responseBundle, null, 2)); + } + }) + .catch(e => { + if (e instanceof Error) { + showNotification({ + icon: , + title: 'Evaluation Error', + message: e.message, + color: 'red' + }); + } + }); + }; + /** * Uses fqm-execution library to perform calculation on all patients and return their * detailed results. @@ -324,8 +418,63 @@ export default function PopulationCalculation() { )} - - TODO: Evaluate Modal + + + +
+ + Evaluate + +
+
+ +
+ + + + + + + +