Skip to content

Commit

Permalink
Add option to remove resources not relevant to measure
Browse files Browse the repository at this point in the history
  • Loading branch information
elsaperelli committed Dec 29, 2024
1 parent 76ef6c1 commit e05c843
Show file tree
Hide file tree
Showing 8 changed files with 316 additions and 7 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ Test patients can be created in the app by clicking on the "Create" button in th

#### Importing a Patient Bundle

Test cases can be imported by clicking the "Import" button in the left panel, which will open a file dropzone that accepts JSON files of FHIR Patient Bundles and `.zip` files composed of FHIR Patient Bundles. Patient Bundles may contain additional FHIR resources. These FHIR resources are also loaded into the app and will belong to the patient contained in the Patient Bundle.
Test cases can be imported by clicking the "Import" button in the left panel, which will open a file dropzone that accepts JSON files of FHIR Patient Bundles and `.zip` files composed of FHIR Patient Bundles. Patient Bundles may contain additional FHIR resources. These FHIR resources are also loaded into the app and will belong to the patient contained in the Patient Bundle. When importing a Patient Bundle or Bundles, there is a switch to remove resources not relevant to the Measure. When set, the resources that are not included in the provided Measure's data requirements will be removed from the Patient Bundle.

#### Selecting Desired Measure Populations

Expand Down
9 changes: 5 additions & 4 deletions __tests__/components/modals/ImportModal.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import '@testing-library/jest-dom';
import { act, fireEvent, render, screen } from '@testing-library/react';
import ImportModal, { ImportModalProps } from '../../../components/modals/ImportModal';
import { mantineRecoilWrap } from '../../helpers/testHelpers';

describe('ImportModal', () => {
it('should render a modal when set to open', () => {
Expand All @@ -9,7 +10,7 @@ describe('ImportModal', () => {
onClose: jest.fn(),
onImportSubmit: jest.fn()
};
render(<ImportModal {...testImportModalProps} />);
render(mantineRecoilWrap(<ImportModal {...testImportModalProps} />));

const modal = screen.getByRole('dialog');
expect(modal).toBeInTheDocument();
Expand All @@ -21,7 +22,7 @@ describe('ImportModal', () => {
onClose: jest.fn(),
onImportSubmit: jest.fn()
};
render(<ImportModal {...testImportModalProps} />);
render(mantineRecoilWrap(<ImportModal {...testImportModalProps} />));

const modal = screen.queryByRole('dialog');
expect(modal).not.toBeInTheDocument();
Expand All @@ -34,7 +35,7 @@ describe('ImportModal', () => {
onImportSubmit: jest.fn()
};

render(<ImportModal {...testImportModalProps} />);
render(mantineRecoilWrap(<ImportModal {...testImportModalProps} />));

const submitButton = screen.getByRole('button', { name: 'Import' });
expect(submitButton).toBeInTheDocument();
Expand Down Expand Up @@ -72,7 +73,7 @@ describe('ImportModal', () => {
onImportSubmit: jest.fn()
};

render(<ImportModal {...testImportModalProps} />);
render(mantineRecoilWrap(<ImportModal {...testImportModalProps} />));

const cancelButton = screen.getByRole('button', { name: 'Cancel' });
expect(cancelButton).toBeInTheDocument();
Expand Down
47 changes: 46 additions & 1 deletion components/modals/ImportModal.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,25 @@
import { Modal, Button, Center, Group, Grid, Text, Collapse, ScrollArea } from '@mantine/core';
import {
Modal,
Button,
Center,
Group,
Grid,
Text,
Collapse,
ScrollArea,
Switch,
Popover,
ActionIcon,
Anchor
} from '@mantine/core';
import { showNotification } from '@mantine/notifications';
import { Dropzone } from '@mantine/dropzone';
import { IconAlertCircle, IconCaretDown, IconCaretRight, IconFileCheck, IconFileImport } from '@tabler/icons';
import { useState } from 'react';
import JSZip from 'jszip';
import { useRecoilState } from 'recoil';
import { resourceSwitchOn } from '../../state/atoms/resourceSwitch';
import { InfoCircle } from 'tabler-icons-react';

export interface ImportModalProps {
open: boolean;
Expand All @@ -16,6 +32,8 @@ export default function ImportModal({ open, onClose, onImportSubmit }: ImportMod
const [fileDisplay, setFileDisplay] = useState<string | string[] | null>(null);
const [showZipFileExpansion, setShowZipFileExpansion] = useState(false);
const [isZipInfoExpanded, setIsZipInfoExpanded] = useState(false);
const [switchOn, setSwitchOn] = useRecoilState(resourceSwitchOn);
const [minimizeResourcesPopoverOpened, setMinimizeResourcesPopoverOpened] = useState(false);

const closeAndReset = () => {
setFiles([]);
Expand Down Expand Up @@ -139,6 +157,33 @@ export default function ImportModal({ open, onClose, onImportSubmit }: ImportMod
)}
<Center>
<Group pt={12}>
<Switch
label="Remove resources not relevant to the Measure"
onLabel="ON"
offLabel="OFF"
checked={switchOn}
onChange={event => setSwitchOn(event.currentTarget.checked)}
/>
<Popover
opened={minimizeResourcesPopoverOpened}
onClose={() => setMinimizeResourcesPopoverOpened(false)}
width={500}
>
<Popover.Target>
<ActionIcon
aria-label={'More Information'}
onClick={() => setMinimizeResourcesPopoverOpened(o => !o)}
>
<InfoCircle size={20} />
</ActionIcon>
</Popover.Target>
<Popover.Dropdown>
If set to minimize the resources on the Test Case, only resources relevant to the measure will be
included. Resources relevant to the measure are defined as resources included in the data requirements
of the measure.
<Anchor href="https://github.com/projecttacoma/fqm-testify#importing-a-patient-bundle">here</Anchor>.
</Popover.Dropdown>
</Popover>
<Button
onClick={() => {
onImportSubmit(files);
Expand Down
8 changes: 8 additions & 0 deletions components/patient-creation/PatientCreationPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ import { detailedResultLookupState } from '../../state/atoms/detailedResultLooku
import { calculateDetailedResult } from '../../util/MeasureCalculation';
import { trustMetaProfileState } from '../../state/atoms/trustMetaProfile';
import { dataRequirementsState } from '../../state/selectors/dataRequirements';
import { minimizeTestCaseResources } from '../../util/ValueSetHelper';
import { resourceSwitchOn } from '../../state/atoms/resourceSwitch';
import { dataRequirementsLookupByType } from '../../state/selectors/dataRequirementsByType';

function PatientCreationPanel() {
const [isPatientModalOpen, setIsPatientModalOpen] = useState(false);
Expand All @@ -52,6 +55,8 @@ function PatientCreationPanel() {
}, [measureBundle]);
const trustMetaProfile = useRecoilValue(trustMetaProfileState);
const dataRequirements = useRecoilValue(dataRequirementsState);
const drLookupByType = useRecoilValue(dataRequirementsLookupByType);
const switchOn = useRecoilValue(resourceSwitchOn);

const openPatientModal = (patientId?: string, copy = false) => {
if (patientId && Object.keys(currentPatients).includes(patientId)) {
Expand Down Expand Up @@ -336,6 +341,9 @@ function PatientCreationPanel() {
);
return;
}
if (switchOn) {
testCase.resources = minimizeTestCaseResources(testCase, measureBundle.content, drLookupByType);
}

draftState[testCase.patient.id] = testCase;
successCount += 1;
Expand Down
10 changes: 10 additions & 0 deletions state/atoms/resourceSwitch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { atom } from 'recoil';

/**
* Atom indicating if resources on a patient bundle are to be minimized using the
* data requirements of the provided measure bundle
*/
export const resourceSwitchOn = atom<boolean>({
key: 'resourceSwitchOn',
default: false
});
1 change: 0 additions & 1 deletion state/selectors/dataRequirements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { selector } from 'recoil';
import { measureBundleState } from '../atoms/measureBundle';
import { Calculator } from 'fqm-execution';
import { valueSetMapState } from './valueSetsMap';

import { measurementPeriodState } from '../atoms/measurementPeriod';
import { getDataRequirementFiltersString } from '../../util/fhir/codes';

Expand Down
50 changes: 50 additions & 0 deletions state/selectors/dataRequirementsByType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { selector } from 'recoil';
import { dataRequirementsState } from './dataRequirements';

export interface DataRequirementsLookupByTypeProps {
keepAll: boolean;
valueSets: string[];
directCodes: fhir4.Coding[];
}

export const dataRequirementsLookupByType = selector<Record<string, DataRequirementsLookupByTypeProps>>({
key: 'dataRequirementsLookupByType',
get: async ({ get }) => {
const dataRequirements = get(dataRequirementsState);
const result: Record<string, DataRequirementsLookupByTypeProps> = {};

if (dataRequirements !== null) {
dataRequirements.forEach((dr, i) => {
if (result[dr.type]) {
if (result[dr.type].keepAll === false) {
if (dr.codeFilter === undefined) {
result[dr.type].keepAll = true;
} else {
dr.codeFilter.forEach(cf => {
if (cf.valueSet) {
result[dr.type].valueSets = result[dr.type].valueSets.concat(cf.valueSet);
} else if (cf.code) {
result[dr.type].directCodes = result[dr.type].directCodes.concat(cf.code);
}
});
}
}
} else {
if (dr.codeFilter === undefined) {
result[dr.type] = { keepAll: true, valueSets: [], directCodes: [] };
} else {
dr.codeFilter.forEach(cf => {
if (cf.valueSet) {
result[dr.type] = { keepAll: false, valueSets: [cf.valueSet], directCodes: [] };
} else if (cf.code) {
result[dr.type] = { keepAll: false, valueSets: [], directCodes: cf.code };
}
});
}
}
});
}

return result;
}
});
Loading

0 comments on commit e05c843

Please sign in to comment.