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' ;
216import { useRecoilValue } from 'recoil' ;
317import { Calculator , CalculatorTypes } from 'fqm-execution' ;
4- import { patientTestCaseState } from '../../state/atoms/patientTestCase' ;
18+ import { patientTestCaseState , TestCaseInfo } from '../../state/atoms/patientTestCase' ;
519import { measureBundleState } from '../../state/atoms/measureBundle' ;
620import { useState } from 'react' ;
721import { measurementPeriodFormattedState } from '../../state/atoms/measurementPeriod' ;
822import { showNotification } from '@mantine/notifications' ;
9- import { IconAlertCircle , IconCircleCheck } from '@tabler/icons' ;
23+ import { IconAlertCircle , IconCircleCheck , IconCopy } from '@tabler/icons' ;
1024import { getPatientInfoString , getPatientNameString } from '../../util/fhir/patient' ;
1125import { createDataExchangeMeasureReport , createPatientBundle } from '../../util/fhir/resourceCreation' ;
1226import 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