Skip to content

Commit b8c5a45

Browse files
committed
preliminary submit data approximation and dataX measure report
Update test id
1 parent be9c622 commit b8c5a45

2 files changed

Lines changed: 176 additions & 10 deletions

File tree

components/calculation/PopulationCalculation.tsx

Lines changed: 140 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,37 @@
1-
import { Button, Center, Drawer, Group, Tooltip } from '@mantine/core';
1+
import { Button, Center, Drawer, Group, Modal, Tooltip } from '@mantine/core';
22
import { useRecoilValue } from 'recoil';
33
import { Calculator, CalculatorTypes } from 'fqm-execution';
44
import { patientTestCaseState } from '../../state/atoms/patientTestCase';
55
import { measureBundleState } from '../../state/atoms/measureBundle';
66
import { useState } from 'react';
77
import { measurementPeriodFormattedState } from '../../state/atoms/measurementPeriod';
88
import { showNotification } from '@mantine/notifications';
9-
import { IconAlertCircle } from '@tabler/icons';
10-
import { getPatientInfoString } from '../../util/fhir/patient';
11-
import { createPatientBundle } from '../../util/fhir/resourceCreation';
9+
import { IconAlertCircle, IconCircleCheck } from '@tabler/icons';
10+
import { getPatientInfoString, getPatientNameString } from '../../util/fhir/patient';
11+
import { createDataExchangeMeasureReport, createPatientBundle } from '../../util/fhir/resourceCreation';
1212
import PopulationResultTable, { LabeledDetailedResult } from './PopulationResultsTable';
1313
import { DetailedResult } from '../../util/types';
1414
import { useRouter } from 'next/router';
1515
import { trustMetaProfileState } from '../../state/atoms/trustMetaProfile';
16+
import { useDisclosure } from '@mantine/hooks';
17+
import { evaluationState } from '../../state/atoms/evaluation';
18+
import { dataRequirementsLookupByType } from '../../state/selectors/dataRequirementsLookupByType';
19+
import { minimizeTestCaseResources } from '../../util/ValueSetHelper';
1620

1721
export default function PopulationCalculation() {
1822
const router = useRouter();
1923

2024
const currentPatients = useRecoilValue(patientTestCaseState);
2125
const measureBundle = useRecoilValue(measureBundleState);
2226
const measurementPeriodFormatted = useRecoilValue(measurementPeriodFormattedState);
27+
const { evaluationServiceUrl, evaluationMeasureId } = useRecoilValue(evaluationState);
28+
const drLookupByType = useRecoilValue(dataRequirementsLookupByType);
2329
const [detailedResults, setDetailedResults] = useState<LabeledDetailedResult[]>([]);
24-
const [opened, setOpened] = useState(false);
30+
const [drawerOpened, setDrawerOpened] = useState(false);
31+
const [opened, { open, close }] = useDisclosure(false);
2532
const [enableTableButton, setEnableTableButton] = useState(false);
2633
const [enableClauseCoverageButton, setEnableClauseCoverageButton] = useState(false);
34+
const [enableEvaluateButton, setEnableEvaluateButton] = useState(false);
2735
const [clauseCoverageHTML, setClauseCoverageHTML] = useState<string | null>(null);
2836
const [clauseUncoverageHTML, setClauseUncoverageHTML] = useState<string | null>(null);
2937
const trustMetaProfile = useRecoilValue(trustMetaProfileState);
@@ -40,6 +48,101 @@ export default function PopulationCalculation() {
4048
return patientLabels;
4149
};
4250

51+
/**
52+
* POSTS patient data in conformance with https://build.fhir.org/ig/HL7/davinci-deqm/OperationDefinition-submit-data.html
53+
* without using Measure/$deqm-submit-data endpoint
54+
* Each transaction bundle should contain DEQM Data Exchange MeasureReports with data-of-interest
55+
* and should be for a single subject (will do a separate POST for each patient)
56+
* @returns { string[] } the evaluation service ids for POSTed patients (may be different than sent IDs or undefined if send was unsuccessful)
57+
*/
58+
const submitDataToEvaluationService = async (): Promise<(string | undefined)[]> => {
59+
// collect data and POST to evaluation service (TODO: should be $submit-data when available)
60+
61+
const measure = measureBundle.content?.entry?.find(e => e.resource?.resourceType === 'Measure')
62+
?.resource as fhir4.Measure;
63+
64+
const postedIds: Promise<string | undefined>[] = Object.keys(currentPatients).map(async id => {
65+
const bundle = createPatientBundle(
66+
currentPatients[id].patient,
67+
minimizeTestCaseResources(currentPatients[id], measureBundle.content, drLookupByType),
68+
currentPatients[id].fullUrl,
69+
createDataExchangeMeasureReport(measure, measurementPeriodFormatted as fhir4.Period, id)
70+
);
71+
const response = await fetch(`${evaluationServiceUrl}/`, {
72+
method: 'POST',
73+
body: JSON.stringify(bundle),
74+
headers: { 'Content-Type': 'application/json+fhir' }
75+
});
76+
if (!response.ok) {
77+
showNotification({
78+
icon: <IconAlertCircle />,
79+
title: 'Evaluation service failure',
80+
message: `Submitting data for Patient: ${getPatientNameString(
81+
currentPatients[id].patient
82+
)} failed with code ${response.status}`,
83+
color: 'red'
84+
});
85+
return;
86+
}
87+
const responseBody: fhir4.Bundle | fhir4.OperationOutcome = await response.json();
88+
if (responseBody.resourceType === 'OperationOutcome') {
89+
showNotification({
90+
icon: <IconAlertCircle />,
91+
title: 'Patient data submission failed',
92+
message: `Submitting data for Patient: ${getPatientNameString(
93+
currentPatients[id].patient
94+
)} failed with message: "${responseBody.issue[0].details?.text}"`,
95+
color: 'red'
96+
});
97+
return;
98+
}
99+
100+
// should return transaction response bundles from which we can pull the posted patient id
101+
return responseBody.entry
102+
?.find(e => e.response?.location?.includes('Patient/'))
103+
?.response?.location?.split('Patient/')[1];
104+
});
105+
106+
return Promise.all(postedIds);
107+
};
108+
109+
/**
110+
* Wrapper function that calls submitDataToEvaluationService() and resolves patient data for future evaluation
111+
*/
112+
const submitData = () => {
113+
submitDataToEvaluationService()
114+
.then(postedIds => {
115+
const resolvedIds = postedIds?.filter(id => id !== undefined);
116+
if (resolvedIds) {
117+
showNotification({
118+
icon: <IconCircleCheck />,
119+
title: 'Successfully sent data',
120+
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 $submitdata in the future
121+
color: 'green'
122+
});
123+
// TODO: use resolvedIds to populate evaluate modal
124+
setEnableEvaluateButton(true);
125+
} else {
126+
showNotification({
127+
icon: <IconAlertCircle />,
128+
title: 'No patient information',
129+
message: 'No patient information was successfully POSTed to the Evaluation Service',
130+
color: 'red'
131+
});
132+
}
133+
})
134+
.catch(e => {
135+
if (e instanceof Error) {
136+
showNotification({
137+
icon: <IconAlertCircle />,
138+
title: 'Data Submission Error',
139+
message: e.message,
140+
color: 'red'
141+
});
142+
}
143+
});
144+
};
145+
43146
/**
44147
* Uses fqm-execution library to perform calculation on all patients and return their
45148
* detailed results.
@@ -99,7 +202,7 @@ export default function PopulationCalculation() {
99202
});
100203
});
101204
setDetailedResults(labeledDetailedResults);
102-
setOpened(true);
205+
setDrawerOpened(true);
103206
setEnableTableButton(true);
104207
setEnableClauseCoverageButton(true);
105208
}
@@ -140,7 +243,7 @@ export default function PopulationCalculation() {
140243
aria-label="Show Table"
141244
styles={{ root: { marginTop: 20 } }}
142245
disabled={!enableTableButton}
143-
onClick={() => setOpened(true)}
246+
onClick={() => setDrawerOpened(true)}
144247
variant="outline"
145248
>
146249
&nbsp;Show Table
@@ -175,12 +278,37 @@ export default function PopulationCalculation() {
175278
&nbsp;Show Clause Coverage
176279
</Button>
177280
</Tooltip>
281+
<Button
282+
data-testid="submit-data-button"
283+
aria-label="Submit Data"
284+
styles={{ root: { marginTop: 20 } }}
285+
onClick={() => submitData()}
286+
variant="outline"
287+
>
288+
&nbsp;Submit Data
289+
</Button>
290+
<Tooltip
291+
label="Disabled until data submission has succeeeded"
292+
openDelay={1000}
293+
disabled={enableEvaluateButton}
294+
>
295+
<Button
296+
data-testid="evaluate-button"
297+
aria-label="Evaluate"
298+
styles={{ root: { marginTop: 20 } }}
299+
disabled={!enableEvaluateButton}
300+
onClick={open}
301+
variant="outline"
302+
>
303+
&nbsp;Evaluate
304+
</Button>
305+
</Tooltip>
178306
</Group>
179307
{detailedResults.length > 0 && (
180308
<>
181309
<Drawer
182-
opened={opened}
183-
onClose={() => setOpened(false)}
310+
opened={drawerOpened}
311+
onClose={() => setDrawerOpened(false)}
184312
position="bottom"
185313
padding="md"
186314
overlayProps={{
@@ -207,6 +335,9 @@ export default function PopulationCalculation() {
207335
</Drawer>
208336
</>
209337
)}
338+
<Modal centered size="xl" withCloseButton={true} opened={opened} onClose={close} title="Evaluate">
339+
TODO: Evaluate Modal
340+
</Modal>
210341
</Center>
211342
)}
212343
</>

util/fhir/resourceCreation.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { v4 as uuidv4 } from 'uuid';
22
import { getRandomFirstName, getRandomLastName } from '../randomizer';
33
import _ from 'lodash';
4-
import { getResourcePrimaryDates } from './dates';
4+
import { getResourcePrimaryDates, jsDateToFHIRDate } from './dates';
55
import { getResourcePatientReference } from './patient';
66
import { getResourceCode } from './codes';
77
import { Enums } from 'fqm-execution';
@@ -181,6 +181,41 @@ export function createFHIRResourceString(
181181
return JSON.stringify(resource, null, 2);
182182
}
183183

184+
/**
185+
* Creates a FHIR data exchange MeasureReport from measure and subject data to be submitted with associated patient
186+
* https://build.fhir.org/ig/HL7/davinci-deqm/StructureDefinition-datax-measurereport-deqm.html
187+
* @param measure FHIR Measure
188+
* @param measurementPeriod FHIR Period representing the measurement period
189+
* @param subjectId the patient id the MeasureReport is associated with
190+
* @returns { fhir4.MeasureReport } a data exchange measure report used to send Measure-relevant data to a server
191+
*/
192+
export function createDataExchangeMeasureReport(
193+
measure: fhir4.Measure,
194+
measurementPeriod: fhir4.Period,
195+
subjectId: string
196+
): fhir4.MeasureReport {
197+
return {
198+
resourceType: 'MeasureReport',
199+
id: uuidv4(),
200+
measure: measure.url?.includes('|') ? measure.url : `${measure.url}|${measure.version}`, //canonical measure/version
201+
period: measurementPeriod,
202+
status: 'complete',
203+
type: 'data-collection',
204+
subject: { reference: `Patient/${subjectId}` }, // patient reference, TODO: is the local id sufficient?
205+
date: jsDateToFHIRDate(new Date()),
206+
reporter: { reference: `Organization/testOrganization` }, //TODO: do we need to send an organization resource?
207+
meta: {
208+
profile: ['http://hl7.org/fhir/us/davinci-deqm/StructureDefinition/datax-measurereport-deqm']
209+
},
210+
extension: [
211+
{
212+
url: 'http://hl7.org/fhir/us/davinci-deqm/StructureDefinition/extension-submitDataUpdateType',
213+
valueCode: 'snapshot'
214+
}
215+
]
216+
};
217+
}
218+
184219
/**
185220
* Creates a FHIR cqfm test case MeasureReport from measure and subject data to be exported with associated patient
186221
* @param mb FHIR MeasureBundle

0 commit comments

Comments
 (0)