diff --git a/__tests__/components/calculation/PopulationCalculation.test.tsx b/__tests__/components/calculation/PopulationCalculation.test.tsx
index ba2224fb..4baa8749 100644
--- a/__tests__/components/calculation/PopulationCalculation.test.tsx
+++ b/__tests__/components/calculation/PopulationCalculation.test.tsx
@@ -8,6 +8,7 @@ import { Calculator } from 'fqm-execution';
import MeasureUpload from '../../../components/measure-upload/MeasureFileUpload';
import { DetailedResult } from '../../../util/types';
import { RouterContext } from 'next/dist/shared/lib/router-context.shared-runtime';
+import { Suspense } from 'react';
const MOCK_DETAILED_RESULT: DetailedResult = {
patientId: '',
@@ -27,52 +28,64 @@ const MOCK_BUNDLE: fhir4.Bundle = {
};
describe('PopulationCalculation', () => {
- it('should not render Calculate Population Results button by default', () => {
- render(
- mantineRecoilWrap(
- <>
-
-
-
- >
- )
- );
+ it('should not render Calculate Population Results button by default', async () => {
+ await act(async () => {
+ render(
+ mantineRecoilWrap(
+ <>
+
+
+
+
+
+ >
+ )
+ );
+ });
const calculateButton = screen.queryByTestId('calculate-all-button');
expect(calculateButton).not.toBeInTheDocument();
});
- it('should not render Show Table button by default', () => {
- render(
- mantineRecoilWrap(
- <>
-
-
-
- >
- )
- );
+ it('should not render Show Table button by default', async () => {
+ await act(async () => {
+ render(
+ mantineRecoilWrap(
+ <>
+
+
+
+
+
+ >
+ )
+ );
+ });
const showTableButton = screen.queryByTestId('show-table-button');
expect(showTableButton).not.toBeInTheDocument();
});
- it('should not render Show Clause Coverage button by default', () => {
- render(
- mantineRecoilWrap(
- <>
-
-
-
- >
- )
- );
+ it('should not render Show Clause Coverage button by default', async () => {
+ await act(async () => {
+ render(
+ mantineRecoilWrap(
+ <>
+
+
+
+
+
+ >
+ )
+ );
+ });
const showClauseCoverageButton = screen.queryByTestId('show-coverage-button');
expect(showClauseCoverageButton).not.toBeInTheDocument();
});
- it('should render Calculate Population Results button when measure bundle is present and at least one patient created', () => {
+ it('should render Calculate Population Results button when measure bundle is present and at least one patient created', async () => {
const MockMB = getMockRecoilState(measureBundleState, {
fileName: 'testName',
content: MOCK_BUNDLE,
@@ -93,17 +106,29 @@ describe('PopulationCalculation', () => {
}
});
- render(
- mantineRecoilWrap(
- <>
-
-
-
-
-
- >
- )
- );
+ jest.spyOn(Calculator, 'calculateDataRequirements').mockResolvedValue({
+ results: {
+ resourceType: 'Library',
+ status: 'draft',
+ type: {}
+ }
+ });
+
+ await act(async () => {
+ render(
+ mantineRecoilWrap(
+ <>
+
+
+
+
+
+
+
+ >
+ )
+ );
+ });
const calculateButton = screen.getByRole('button', { name: 'Calculate Population Results' }) as HTMLButtonElement;
expect(calculateButton).toBeInTheDocument();
@@ -161,9 +186,11 @@ describe('PopulationCalculation', () => {
-
-
-
+
+
+
+
+
>
)
);
@@ -234,9 +261,11 @@ describe('PopulationCalculation', () => {
-
-
-
+
+
+
+
+
>
)
);
@@ -307,9 +336,11 @@ describe('PopulationCalculation', () => {
-
-
-
+
+
+
+
+
>
)
);
diff --git a/components/calculation/PopulationCalculation.tsx b/components/calculation/PopulationCalculation.tsx
index 17ac8386..aefca502 100644
--- a/components/calculation/PopulationCalculation.tsx
+++ b/components/calculation/PopulationCalculation.tsx
@@ -1,4 +1,4 @@
-import { Button, Center, Drawer, Group, Tooltip } from '@mantine/core';
+import { Button, Center, Drawer, Group, Modal, Tooltip } from '@mantine/core';
import { useRecoilValue } from 'recoil';
import { Calculator, CalculatorTypes } from 'fqm-execution';
import { patientTestCaseState } from '../../state/atoms/patientTestCase';
@@ -6,13 +6,17 @@ import { measureBundleState } from '../../state/atoms/measureBundle';
import { useState } from 'react';
import { measurementPeriodFormattedState } from '../../state/atoms/measurementPeriod';
import { showNotification } from '@mantine/notifications';
-import { IconAlertCircle } from '@tabler/icons';
-import { getPatientInfoString } from '../../util/fhir/patient';
-import { createPatientBundle } from '../../util/fhir/resourceCreation';
+import { IconAlertCircle, IconCircleCheck } from '@tabler/icons';
+import { getPatientInfoString, getPatientNameString } from '../../util/fhir/patient';
+import { createDataExchangeMeasureReport, createPatientBundle } from '../../util/fhir/resourceCreation';
import PopulationResultTable, { LabeledDetailedResult } from './PopulationResultsTable';
import { DetailedResult } from '../../util/types';
import { useRouter } from 'next/router';
import { trustMetaProfileState } from '../../state/atoms/trustMetaProfile';
+import { useDisclosure } from '@mantine/hooks';
+import { evaluationState } from '../../state/atoms/evaluation';
+import { dataRequirementsLookupByType } from '../../state/selectors/dataRequirementsLookupByType';
+import { minimizeTestCaseResources } from '../../util/ValueSetHelper';
export default function PopulationCalculation() {
const router = useRouter();
@@ -20,10 +24,14 @@ export default function PopulationCalculation() {
const currentPatients = useRecoilValue(patientTestCaseState);
const measureBundle = useRecoilValue(measureBundleState);
const measurementPeriodFormatted = useRecoilValue(measurementPeriodFormattedState);
+ const { evaluationServiceUrl, evaluationMeasureId } = useRecoilValue(evaluationState);
+ const drLookupByType = useRecoilValue(dataRequirementsLookupByType);
const [detailedResults, setDetailedResults] = useState([]);
- const [opened, setOpened] = useState(false);
+ const [drawerOpened, setDrawerOpened] = useState(false);
+ const [opened, { open, close }] = useDisclosure(false);
const [enableTableButton, setEnableTableButton] = useState(false);
const [enableClauseCoverageButton, setEnableClauseCoverageButton] = useState(false);
+ const [enableEvaluateButton, setEnableEvaluateButton] = useState(false);
const [clauseCoverageHTML, setClauseCoverageHTML] = useState(null);
const [clauseUncoverageHTML, setClauseUncoverageHTML] = useState(null);
const trustMetaProfile = useRecoilValue(trustMetaProfileState);
@@ -40,6 +48,90 @@ export default function PopulationCalculation() {
return patientLabels;
};
+ /**
+ * POSTS patient data in conformance with https://build.fhir.org/ig/HL7/davinci-deqm/OperationDefinition-submit-data.html
+ * 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)
+ */
+ const submitDataToEvaluationService = async (): Promise<(string | 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 bundle = createPatientBundle(
+ currentPatients[id].patient,
+ minimizeTestCaseResources(currentPatients[id], measureBundle.content, drLookupByType),
+ currentPatients[id].fullUrl,
+ createDataExchangeMeasureReport(measure, measurementPeriodFormatted as fhir4.Period, id)
+ );
+ const response = await fetch(`${evaluationServiceUrl}/`, {
+ method: 'POST',
+ body: JSON.stringify(bundle),
+ headers: { 'Content-Type': 'application/json+fhir' }
+ });
+ const responseBody: fhir4.Bundle | fhir4.OperationOutcome = await response.json();
+ if (responseBody.resourceType === 'OperationOutcome') {
+ showNotification({
+ icon: ,
+ title: 'Patient data submission failed',
+ message: `Submitting data for Patient: ${getPatientNameString(
+ currentPatients[id].patient
+ )} failed with details: "${responseBody.issue[0].details?.text ?? response.status}"`,
+ color: 'red'
+ });
+ return;
+ }
+
+ // should return transaction response bundles from which we can pull the posted patient id
+ return responseBody.entry
+ ?.find(e => e.response?.location?.includes('Patient/'))
+ ?.response?.location?.split('Patient/')[1];
+ });
+
+ return Promise.all(postedIds);
+ };
+
+ /**
+ * Wrapper function that calls submitDataToEvaluationService() and resolves patient data for future evaluation
+ */
+ const submitData = () => {
+ submitDataToEvaluationService()
+ .then(postedIds => {
+ const resolvedIds = postedIds?.filter(id => id !== undefined);
+ if (resolvedIds.length > 0) {
+ showNotification({
+ icon: ,
+ title: 'Successfully sent data',
+ 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
+ setEnableEvaluateButton(true);
+ } else {
+ showNotification({
+ icon: ,
+ title: 'No patient information',
+ message: 'No patient information was successfully POSTed to the Evaluation Service',
+ color: 'red'
+ });
+ }
+ })
+ .catch(e => {
+ if (e instanceof Error) {
+ showNotification({
+ icon: ,
+ title: 'Data Submission Error',
+ message: e.message,
+ color: 'red'
+ });
+ }
+ });
+ };
+
/**
* Uses fqm-execution library to perform calculation on all patients and return their
* detailed results.
@@ -99,7 +191,7 @@ export default function PopulationCalculation() {
});
});
setDetailedResults(labeledDetailedResults);
- setOpened(true);
+ setDrawerOpened(true);
setEnableTableButton(true);
setEnableClauseCoverageButton(true);
}
@@ -140,7 +232,7 @@ export default function PopulationCalculation() {
aria-label="Show Table"
styles={{ root: { marginTop: 20 } }}
disabled={!enableTableButton}
- onClick={() => setOpened(true)}
+ onClick={() => setDrawerOpened(true)}
variant="outline"
>
Show Table
@@ -175,12 +267,37 @@ export default function PopulationCalculation() {
Show Clause Coverage
+
+
+
+
{detailedResults.length > 0 && (
<>
setOpened(false)}
+ opened={drawerOpened}
+ onClose={() => setDrawerOpened(false)}
position="bottom"
padding="md"
overlayProps={{
@@ -207,6 +324,9 @@ export default function PopulationCalculation() {
>
)}
+
+ TODO: Evaluate Modal
+
)}
>
diff --git a/util/fhir/resourceCreation.ts b/util/fhir/resourceCreation.ts
index 4190dfdc..a3356791 100644
--- a/util/fhir/resourceCreation.ts
+++ b/util/fhir/resourceCreation.ts
@@ -1,7 +1,7 @@
import { v4 as uuidv4 } from 'uuid';
import { getRandomFirstName, getRandomLastName } from '../randomizer';
import _ from 'lodash';
-import { getResourcePrimaryDates } from './dates';
+import { getResourcePrimaryDates, jsDateToFHIRDate } from './dates';
import { getResourcePatientReference } from './patient';
import { getResourceCode } from './codes';
import { Enums } from 'fqm-execution';
@@ -181,6 +181,42 @@ export function createFHIRResourceString(
return JSON.stringify(resource, null, 2);
}
+/**
+ * Creates a FHIR data exchange MeasureReport from measure and subject data to be submitted with associated patient
+ * https://build.fhir.org/ig/HL7/davinci-deqm/StructureDefinition-datax-measurereport-deqm.html
+ * @param measure FHIR Measure
+ * @param measurementPeriod FHIR Period representing the measurement period
+ * @param subjectId the patient id the MeasureReport is associated with
+ * @returns { fhir4.MeasureReport } a data exchange measure report used to send Measure-relevant data to a server
+ */
+export function createDataExchangeMeasureReport(
+ measure: fhir4.Measure,
+ measurementPeriod: fhir4.Period,
+ subjectId: string
+): fhir4.MeasureReport {
+ return {
+ resourceType: 'MeasureReport',
+ id: uuidv4(),
+ measure: measure.url?.includes('|') ? measure.url : `${measure.url}|${measure.version}`, //canonical measure/version
+ period: measurementPeriod,
+ status: 'complete',
+ type: 'data-collection',
+ subject: { reference: `Patient/${subjectId}` },
+ date: jsDateToFHIRDate(new Date()),
+ reporter: { reference: 'Organization/fqm-testify' }, //TODO: do we need to send an organization resource?
+ meta: {
+ profile: ['http://hl7.org/fhir/us/davinci-deqm/StructureDefinition/datax-measurereport-deqm']
+ },
+ extension: [
+ {
+ url: 'http://hl7.org/fhir/us/davinci-deqm/StructureDefinition/extension-submitDataUpdateType',
+ valueCode: 'snapshot'
+ }
+ ],
+ contained: [{ resourceType: 'Organization', id: 'fqm-testify' }]
+ };
+}
+
/**
* Creates a FHIR cqfm test case MeasureReport from measure and subject data to be exported with associated patient
* @param mb FHIR MeasureBundle