Skip to content

Commit 398d769

Browse files
authored
Evaluate button (#111)
* POST version of evaluate map posted ID to local test data Update comment * fix subject reportType * Small UI improvements * tsc type issue * Simplify $evaluate parameter dates * Response is Parameters and use valueDate
1 parent 6857f92 commit 398d769

1 file changed

Lines changed: 174 additions & 11 deletions

File tree

components/calculation/PopulationCalculation.tsx

Lines changed: 174 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,26 @@
1-
import { Button, Center, Drawer, Group, Modal, Tooltip } from '@mantine/core';
1+
import {
2+
Button,
3+
Center,
4+
CopyButton,
5+
Drawer,
6+
Grid,
7+
Group,
8+
Modal,
9+
Radio,
10+
Select,
11+
Space,
12+
Text,
13+
Tooltip
14+
} from '@mantine/core';
15+
import CodeMirror from '@uiw/react-codemirror';
216
import { useRecoilValue } from 'recoil';
317
import { Calculator, CalculatorTypes } from 'fqm-execution';
4-
import { patientTestCaseState } from '../../state/atoms/patientTestCase';
18+
import { patientTestCaseState, TestCaseInfo } from '../../state/atoms/patientTestCase';
519
import { measureBundleState } from '../../state/atoms/measureBundle';
620
import { useState } from 'react';
721
import { measurementPeriodFormattedState } from '../../state/atoms/measurementPeriod';
822
import { showNotification } from '@mantine/notifications';
9-
import { IconAlertCircle, IconCircleCheck } from '@tabler/icons';
23+
import { IconAlertCircle, IconCircleCheck, IconCopy } from '@tabler/icons';
1024
import { getPatientInfoString, getPatientNameString } from '../../util/fhir/patient';
1125
import { createDataExchangeMeasureReport, createPatientBundle } from '../../util/fhir/resourceCreation';
1226
import PopulationResultTable, { LabeledDetailedResult } from './PopulationResultsTable';
@@ -35,6 +49,10 @@ export default function PopulationCalculation() {
3549
const [clauseCoverageHTML, setClauseCoverageHTML] = useState<string | null>(null);
3650
const [clauseUncoverageHTML, setClauseUncoverageHTML] = useState<string | null>(null);
3751
const trustMetaProfile = useRecoilValue(trustMetaProfileState);
52+
const [reportTypeValue, setReportTypeValue] = useState('population');
53+
const [subjectValue, setSubjectValue] = useState<string | null>('');
54+
const [subjectData, setSubjectData] = useState<{ value: string; label: string }[]>([]);
55+
const [evaluateText, setEvaluateText] = useState<string>('');
3856

3957
/**
4058
* Creates object that maps patient ids to their name/DOB info strings.
@@ -53,15 +71,19 @@ export default function PopulationCalculation() {
5371
* without using Measure/$deqm-submit-data endpoint
5472
* Each transaction bundle should contain DEQM Data Exchange MeasureReports with data-of-interest
5573
* and should be for a single subject (will do a separate POST for each patient)
56-
* @returns { string|undefined[] } the evaluation service ids for POSTed patients (may be different than sent IDs or undefined if send was unsuccessful)
74+
* @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
5775
*/
58-
const submitDataToEvaluationService = async (): Promise<(string | undefined)[]> => {
76+
const submitDataToEvaluationService = async (): Promise<
77+
({ postedId: string; testCaseInfo: TestCaseInfo } | undefined)[]
78+
> => {
5979
// collect data and POST to evaluation service (TODO: should be $submit-data when available)
6080

6181
const measure = measureBundle.content?.entry?.find(e => e.resource?.resourceType === 'Measure')
6282
?.resource as fhir4.Measure;
6383

64-
const postedIds: Promise<string | undefined>[] = Object.keys(currentPatients).map(async id => {
84+
const postedIds: Promise<{ postedId: string; testCaseInfo: TestCaseInfo } | undefined>[] = Object.keys(
85+
currentPatients
86+
).map(async id => {
6587
const bundle = createPatientBundle(
6688
currentPatients[id].patient,
6789
minimizeTestCaseResources(currentPatients[id], measureBundle.content, drLookupByType),
@@ -87,9 +109,10 @@ export default function PopulationCalculation() {
87109
}
88110

89111
// should return transaction response bundles from which we can pull the posted patient id
90-
return responseBody.entry
112+
const locationId = responseBody.entry
91113
?.find(e => e.response?.location?.includes('Patient/'))
92114
?.response?.location?.split('Patient/')[1];
115+
return locationId ? { postedId: locationId, testCaseInfo: currentPatients[id] } : undefined;
93116
});
94117

95118
return Promise.all(postedIds);
@@ -101,15 +124,21 @@ export default function PopulationCalculation() {
101124
const submitData = () => {
102125
submitDataToEvaluationService()
103126
.then(postedIds => {
104-
const resolvedIds = postedIds?.filter(id => id !== undefined);
127+
const resolvedIds = postedIds?.filter(idObj => idObj !== undefined);
105128
if (resolvedIds.length > 0) {
106129
showNotification({
107130
icon: <IconCircleCheck />,
108131
title: 'Successfully sent data',
109132
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
110133
color: 'green'
111134
});
112-
// TODO: use resolvedIds to populate evaluate modal
135+
setSubjectData(
136+
resolvedIds.map(idObj => {
137+
return idObj
138+
? { value: idObj.postedId, label: getPatientNameString(idObj.testCaseInfo.patient) }
139+
: { value: '', label: '' };
140+
})
141+
);
113142
setEnableEvaluateButton(true);
114143
} else {
115144
showNotification({
@@ -132,6 +161,77 @@ export default function PopulationCalculation() {
132161
});
133162
};
134163

164+
// TODO: should be constrained to Parameters in the future
165+
const sendEvaluate = async (): Promise<fhir4.Bundle | fhir4.Parameters | undefined> => {
166+
const parameters: fhir4.Parameters = {
167+
resourceType: 'Parameters',
168+
parameter: [
169+
{
170+
name: 'measureId',
171+
valueString: evaluationMeasureId
172+
},
173+
{
174+
name: 'periodStart',
175+
valueDate: measurementPeriodFormatted?.start.split('T')[0]
176+
},
177+
{
178+
name: 'periodEnd',
179+
valueDate: measurementPeriodFormatted?.end.split('T')[0]
180+
},
181+
{
182+
name: 'reportType',
183+
valueString: reportTypeValue
184+
}
185+
]
186+
};
187+
if (reportTypeValue === 'subject' && subjectValue) {
188+
parameters.parameter?.push({
189+
name: 'subject',
190+
valueString: `Patient/${subjectValue}`
191+
});
192+
}
193+
const response = await fetch(`${evaluationServiceUrl}/Measure/$evaluate`, {
194+
method: 'POST',
195+
body: JSON.stringify(parameters),
196+
headers: { 'Content-Type': 'application/json+fhir' }
197+
});
198+
const responseBody: fhir4.Bundle | fhir4.Parameters | fhir4.OperationOutcome = await response.json();
199+
if (responseBody.resourceType === 'OperationOutcome') {
200+
showNotification({
201+
icon: <IconAlertCircle />,
202+
title: 'Evaluation operation failed',
203+
message: `Evaluation call failed with details: "${responseBody.issue[0].details?.text ?? response.status}"`,
204+
color: 'red'
205+
});
206+
return;
207+
}
208+
209+
return responseBody;
210+
};
211+
212+
/**
213+
* Wrapper function that calls sendEvaluate() and populates the results view
214+
*/
215+
const onEvaluate = (): void => {
216+
setEvaluateText(''); // clear text until evaluation is done
217+
sendEvaluate()
218+
.then(response => {
219+
if (response) {
220+
setEvaluateText(JSON.stringify(response, null, 2));
221+
}
222+
})
223+
.catch(e => {
224+
if (e instanceof Error) {
225+
showNotification({
226+
icon: <IconAlertCircle />,
227+
title: 'Evaluation Error',
228+
message: e.message,
229+
color: 'red'
230+
});
231+
}
232+
});
233+
};
234+
135235
/**
136236
* Uses fqm-execution library to perform calculation on all patients and return their
137237
* detailed results.
@@ -324,8 +424,71 @@ export default function PopulationCalculation() {
324424
</Drawer>
325425
</>
326426
)}
327-
<Modal centered size="xl" withCloseButton={true} opened={opened} onClose={close} title="Evaluate">
328-
TODO: Evaluate Modal
427+
<Modal centered size="xl" withCloseButton={true} opened={opened} onClose={close}>
428+
<Grid align="center" justify="center">
429+
<Grid.Col>
430+
<Center>
431+
<Text weight={700} align="center" lineClamp={2}>
432+
Evaluate
433+
</Text>
434+
</Center>
435+
</Grid.Col>
436+
<Grid.Col>
437+
<Center>
438+
<Radio.Group
439+
value={reportTypeValue}
440+
onChange={setReportTypeValue}
441+
name="reportType"
442+
label="Select the report type:"
443+
>
444+
<Group mt="xs">
445+
<Radio value="population" label="population" />
446+
<Radio value="subject" label="subject" />
447+
</Group>
448+
</Radio.Group>
449+
<Space w="xl" />
450+
<Select
451+
label="Select subject:"
452+
data={subjectData}
453+
value={subjectValue}
454+
onChange={setSubjectValue}
455+
disabled={reportTypeValue === 'population'}
456+
searchable={true}
457+
/>
458+
</Center>
459+
</Grid.Col>
460+
461+
<Grid.Col>
462+
<Center>
463+
<Group pt={8}>
464+
<Button onClick={() => onEvaluate()}>Evaluate</Button>
465+
<Button variant="default" onClick={close}>
466+
Cancel
467+
</Button>
468+
</Group>
469+
</Center>
470+
</Grid.Col>
471+
<Grid.Col>
472+
<CopyButton value={evaluateText}>
473+
{({ copied, copy }) => (
474+
<Button color={copied ? 'teal' : 'blue'} onClick={copy}>
475+
<IconCopy />
476+
</Button>
477+
)}
478+
</CopyButton>
479+
<Text lineClamp={4}>
480+
<CodeMirror
481+
data-autofocus
482+
data-testid="codemirror"
483+
height="300px"
484+
value={evaluateText}
485+
theme="light"
486+
editable={false}
487+
placeholder="Waiting for $evaluate response..."
488+
/>
489+
</Text>
490+
</Grid.Col>
491+
</Grid>
329492
</Modal>
330493
</Center>
331494
)}

0 commit comments

Comments
 (0)