Skip to content

Commit 4714fe6

Browse files
authored
(feat) Confirmation modal when selecting patient outside session location
* Confirmation modal when selecting patient outside session location * Confirmation modal when selecting patient outside session location * Made patient mismatch prompt configurable * Cleanup
1 parent 3b131a3 commit 4714fe6

File tree

9 files changed

+259
-81
lines changed

9 files changed

+259
-81
lines changed

src/add-group-modal/AddGroupModal.tsx

Lines changed: 92 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,19 @@ import React, { useCallback, useContext, useEffect, useMemo, useState } from 're
22
import { ComposedModal, Button, ModalHeader, ModalFooter, ModalBody, TextInput, FormLabel } from '@carbon/react';
33
import { TrashCan } from '@carbon/react/icons';
44
import { useTranslation } from 'react-i18next';
5-
import { ExtensionSlot, fetchCurrentPatient, showToast, useConfig, usePatient } from '@openmrs/esm-framework';
5+
import {
6+
ExtensionSlot,
7+
fetchCurrentPatient,
8+
showToast,
9+
useConfig,
10+
usePatient,
11+
useSession,
12+
} from '@openmrs/esm-framework';
613
import styles from './styles.scss';
714
import GroupFormWorkflowContext from '../context/GroupFormWorkflowContext';
815
import { usePostCohort } from '../hooks';
16+
import PatientLocationMismatchModal from '../form-entry-workflow/patient-search-header/PatienMismatchedLocationModal';
17+
import { useHsuIdIdentifier } from '../hooks/location-tag.resource';
918

1019
const MemExtension = React.memo(ExtensionSlot);
1120

@@ -112,6 +121,10 @@ const AddGroupModal = ({
112121
const [patientList, setPatientList] = useState(patients || []);
113122
const { post, result, error } = usePostCohort();
114123
const config = useConfig();
124+
const [patientLocationMismatchModalOpen, setPatientLocationMismatchModalOpen] = useState(false);
125+
const [selectedPatientUuid, setSelectedPatientUuid] = useState();
126+
const { hsuIdentifier } = useHsuIdIdentifier(selectedPatientUuid);
127+
const { sessionLocation } = useSession();
115128

116129
const removePatient = useCallback(
117130
(patientUuid: string) =>
@@ -147,27 +160,44 @@ const AddGroupModal = ({
147160
[name, patientList.length],
148161
);
149162

150-
const updatePatientList = useCallback(
151-
(patientUuid) => {
152-
function getPatientName(patient) {
153-
return [patient?.name?.[0]?.given, patient?.name?.[0]?.family].join(' ');
154-
}
155-
if (!patientList.find((p) => p.uuid === patientUuid)) {
156-
fetchCurrentPatient(patientUuid).then((result) => {
157-
const newPatient = { uuid: patientUuid, ...result };
158-
setPatientList(
159-
[...patientList, newPatient].sort((a, b) =>
160-
getPatientName(a).localeCompare(getPatientName(b), undefined, {
161-
sensitivity: 'base',
162-
}),
163-
),
164-
);
165-
});
166-
}
167-
setErrors((errors) => ({ ...errors, patientList: null }));
168-
},
169-
[patientList, setPatientList],
170-
);
163+
const addSelectedPatientToList = useCallback(() => {
164+
function getPatientName(patient) {
165+
return [patient?.name?.[0]?.given, patient?.name?.[0]?.family].join(' ');
166+
}
167+
if (!patientList.find((p) => p.uuid === selectedPatientUuid)) {
168+
fetchCurrentPatient(selectedPatientUuid).then((result) => {
169+
const newPatient = { uuid: selectedPatientUuid, ...result };
170+
setPatientList(
171+
[...patientList, newPatient].sort((a, b) =>
172+
getPatientName(a).localeCompare(getPatientName(b), undefined, {
173+
sensitivity: 'base',
174+
}),
175+
),
176+
);
177+
});
178+
}
179+
setErrors((errors) => ({ ...errors, patientList: null }));
180+
}, [selectedPatientUuid, patientList, setPatientList]);
181+
182+
const updatePatientList = (patientUuid) => {
183+
setSelectedPatientUuid(patientUuid);
184+
};
185+
186+
useEffect(() => {
187+
if (!selectedPatientUuid || !hsuIdentifier) return;
188+
189+
if (config.patientLocationMismatchCheck && hsuIdentifier && sessionLocation.uuid != hsuIdentifier.location.uuid) {
190+
setPatientLocationMismatchModalOpen(true);
191+
} else {
192+
addSelectedPatientToList();
193+
}
194+
}, [
195+
selectedPatientUuid,
196+
sessionLocation,
197+
hsuIdentifier,
198+
addSelectedPatientToList,
199+
config.patientLocationMismatchCheck,
200+
]);
171201

172202
const handleSubmit = () => {
173203
if (validate()) {
@@ -216,33 +246,47 @@ const AddGroupModal = ({
216246
}
217247
}, [error, t]);
218248

249+
const onPatientLocationMismatchModalCancel = () => {
250+
setSelectedPatientUuid(null);
251+
};
252+
219253
return (
220-
<div className={styles.modal}>
221-
<ComposedModal open={isOpen} onClose={handleCancel}>
222-
<ModalHeader>{isCreate ? t('createNewGroup', 'Create New Group') : t('editGroup', 'Edit Group')}</ModalHeader>
223-
<ModalBody>
224-
<NewGroupForm
225-
{...{
226-
name,
227-
setName,
228-
patientList,
229-
updatePatientList,
230-
errors,
231-
validate,
232-
removePatient,
233-
}}
234-
/>
235-
</ModalBody>
236-
<ModalFooter>
237-
<Button kind="secondary" onClick={handleCancel}>
238-
{t('cancel', 'Cancel')}
239-
</Button>
240-
<Button kind="primary" onClick={handleSubmit}>
241-
{isCreate ? t('createGroup', 'Create Group') : t('save', 'Save')}
242-
</Button>
243-
</ModalFooter>
244-
</ComposedModal>
245-
</div>
254+
<>
255+
<div className={styles.modal}>
256+
<ComposedModal open={isOpen} onClose={handleCancel}>
257+
<ModalHeader>{isCreate ? t('createNewGroup', 'Create New Group') : t('editGroup', 'Edit Group')}</ModalHeader>
258+
<ModalBody>
259+
<NewGroupForm
260+
{...{
261+
name,
262+
setName,
263+
patientList,
264+
updatePatientList,
265+
errors,
266+
validate,
267+
removePatient,
268+
}}
269+
/>
270+
</ModalBody>
271+
<ModalFooter>
272+
<Button kind="secondary" onClick={handleCancel}>
273+
{t('cancel', 'Cancel')}
274+
</Button>
275+
<Button kind="primary" onClick={handleSubmit}>
276+
{isCreate ? t('createGroup', 'Create Group') : t('save', 'Save')}
277+
</Button>
278+
</ModalFooter>
279+
</ComposedModal>
280+
</div>
281+
<PatientLocationMismatchModal
282+
open={patientLocationMismatchModalOpen}
283+
setOpen={setPatientLocationMismatchModalOpen}
284+
onConfirm={addSelectedPatientToList}
285+
onCancel={onPatientLocationMismatchModalCancel}
286+
sessionLocation={sessionLocation}
287+
hsuLocation={hsuIdentifier?.location}
288+
/>
289+
</>
246290
);
247291
};
248292

src/config-schema.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,12 @@ export const configSchema = {
128128
},
129129
_default: [],
130130
},
131+
patientLocationMismatchCheck: {
132+
_type: Type.Boolean,
133+
_description:
134+
'Whether to prompt for confirmation if the selected patient is not at the same location as the current session.',
135+
_default: false,
136+
},
131137
};
132138

133139
export type Form = {
@@ -144,4 +150,5 @@ export type Category = {
144150
export type Config = {
145151
formCategories: Array<Category>;
146152
formCategoriesToShow: Array<string>;
153+
patientLocationMismatchCheck: Type.Boolean;
147154
};

src/form-entry-workflow/FormEntryWorkflow.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ const FormWorkspace = () => {
7373

7474
useEffect(() => {
7575
if (encounter && visit) {
76-
// Update encounter so that it belongs to the created visit
76+
// Update the encounter so that it belongs to the created visit
7777
updateEncounter({ uuid: encounter.uuid, visit: visit.uuid });
7878
}
7979
}, [encounter, visit, updateEncounter]);
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { Button, ComposedModal, ModalBody, ModalFooter, ModalHeader } from '@carbon/react';
2+
import React from 'react';
3+
import { useTranslation } from 'react-i18next';
4+
5+
const PatientLocationMismatchModal = ({ open, setOpen, onConfirm, onCancel, sessionLocation, hsuLocation }) => {
6+
const { t } = useTranslation();
7+
8+
const hsuDisplay = hsuLocation?.display || t('unknown', 'Unknown');
9+
const sessionDisplay = sessionLocation?.display || t('unknown', 'Unknown');
10+
11+
const handleCancel = () => {
12+
onCancel?.();
13+
setOpen(false);
14+
};
15+
16+
const handleConfirm = () => {
17+
onConfirm?.();
18+
setOpen(false);
19+
};
20+
21+
return (
22+
<ComposedModal open={open}>
23+
<ModalHeader>{t('confirmPatientSelection', 'Confirm patient selection')}</ModalHeader>
24+
<ModalBody>
25+
{t(
26+
'patientLocationMismatch',
27+
`The selected HSU location (${hsuLocation}) does not match the current session location (${sessionLocation}). Are you sure you want to proceed?`,
28+
{
29+
hsuLocation: hsuDisplay,
30+
sessionLocation: sessionDisplay,
31+
},
32+
)}
33+
</ModalBody>
34+
<ModalFooter>
35+
<Button kind="secondary" onClick={handleCancel}>
36+
{t('cancel', 'Cancel')}
37+
</Button>
38+
<Button kind="primary" onClick={handleConfirm}>
39+
{t('continue', 'Continue')}
40+
</Button>
41+
</ModalFooter>
42+
</ComposedModal>
43+
);
44+
};
45+
46+
export default PatientLocationMismatchModal;

src/form-entry-workflow/patient-search-header/PatientSearchHeader.tsx

Lines changed: 71 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,46 @@
11
import { Add, Close } from '@carbon/react/icons';
2-
import { ExtensionSlot, interpolateUrl, navigate } from '@openmrs/esm-framework';
2+
import { ExtensionSlot, interpolateUrl, navigate, useConfig, useSession } from '@openmrs/esm-framework';
33
import { Button } from '@carbon/react';
4-
import React, { useContext } from 'react';
4+
import React, { useCallback, useContext, useEffect, useState } from 'react';
55
import { Link } from 'react-router-dom';
66
import FormWorkflowContext from '../../context/FormWorkflowContext';
77
import styles from './styles.scss';
88
import { useTranslation } from 'react-i18next';
9+
import { useHsuIdIdentifier } from '../../hooks/location-tag.resource';
10+
import PatientLocationMismatchModal from './PatienMismatchedLocationModal';
911

1012
const PatientSearchHeader = () => {
13+
const [patientLocationMismatchModalOpen, setPatientLocationMismatchModalOpen] = useState(false);
14+
const [selectedPatientUuid, setSelectedPatientUuid] = useState();
15+
const { hsuIdentifier } = useHsuIdIdentifier(selectedPatientUuid);
16+
const { sessionLocation } = useSession();
17+
const config = useConfig();
1118
const { addPatient, workflowState, activeFormUuid } = useContext(FormWorkflowContext);
12-
const handleSelectPatient = (patient) => {
13-
addPatient(patient);
14-
};
19+
20+
const onPatientMismatchedLocationModalConfirm = useCallback(() => {
21+
addPatient(selectedPatientUuid);
22+
setSelectedPatientUuid(null);
23+
}, [addPatient, selectedPatientUuid]);
24+
25+
const onPatientMismatchedLocationModalCancel = useCallback(() => {
26+
setPatientLocationMismatchModalOpen(false);
27+
setSelectedPatientUuid(null);
28+
}, []);
29+
30+
const handleSelectPatient = useCallback((patientUuid) => {
31+
setSelectedPatientUuid(patientUuid);
32+
}, []);
33+
34+
useEffect(() => {
35+
if (!selectedPatientUuid || !hsuIdentifier) return;
36+
37+
if (config.patientLocationMismatchCheck && hsuIdentifier && sessionLocation.uuid != hsuIdentifier.location.uuid) {
38+
setPatientLocationMismatchModalOpen(true);
39+
} else {
40+
addPatient(selectedPatientUuid);
41+
}
42+
}, [selectedPatientUuid, sessionLocation, hsuIdentifier, addPatient, config.patientLocationMismatchCheck]);
43+
1544
const { t } = useTranslation();
1645

1746
if (workflowState !== 'NEW_PATIENT') return null;
@@ -20,34 +49,44 @@ const PatientSearchHeader = () => {
2049
const patientRegistrationUrl = interpolateUrl(`\${openmrsSpaBase}/patient-registration?afterUrl=${afterUrl}`);
2150

2251
return (
23-
<div className={styles.searchHeaderContainer}>
24-
<span className={styles.padded}>{t('nextPatient', 'Next patient')}:</span>
25-
<span className={styles.searchBarWrapper}>
26-
<ExtensionSlot
27-
name="patient-search-bar-slot"
28-
state={{
29-
selectPatientAction: handleSelectPatient,
30-
buttonProps: {
31-
kind: 'primary',
32-
},
33-
}}
34-
/>
35-
</span>
36-
<span className={styles.padded}>{t('or', 'or')}</span>
37-
<span>
38-
<Button onClick={() => navigate({ to: patientRegistrationUrl })}>
39-
{t('createNewPatient', 'Create new patient')} <Add size={20} />
40-
</Button>
41-
</span>
42-
<span style={{ flexGrow: 1 }} />
43-
<span>
44-
<Link to="../">
45-
<Button kind="ghost">
46-
{t('cancel', 'Cancel')} <Close size={20} />
52+
<>
53+
<div className={styles.searchHeaderContainer}>
54+
<span className={styles.padded}>{t('nextPatient', 'Next patient')}:</span>
55+
<span className={styles.searchBarWrapper}>
56+
<ExtensionSlot
57+
name="patient-search-bar-slot"
58+
state={{
59+
selectPatientAction: handleSelectPatient,
60+
buttonProps: {
61+
kind: 'primary',
62+
},
63+
}}
64+
/>
65+
</span>
66+
<span className={styles.padded}>{t('or', 'or')}</span>
67+
<span>
68+
<Button onClick={() => navigate({ to: patientRegistrationUrl })}>
69+
{t('createNewPatient', 'Create new patient')} <Add size={20} />
4770
</Button>
48-
</Link>
49-
</span>
50-
</div>
71+
</span>
72+
<span style={{ flexGrow: 1 }} />
73+
<span>
74+
<Link to="../">
75+
<Button kind="ghost">
76+
{t('cancel', 'Cancel')} <Close size={20} />
77+
</Button>
78+
</Link>
79+
</span>
80+
</div>
81+
<PatientLocationMismatchModal
82+
open={patientLocationMismatchModalOpen}
83+
setOpen={setPatientLocationMismatchModalOpen}
84+
onConfirm={onPatientMismatchedLocationModalConfirm}
85+
onCancel={onPatientMismatchedLocationModalCancel}
86+
sessionLocation={sessionLocation}
87+
hsuLocation={hsuIdentifier?.location}
88+
/>
89+
</>
5190
);
5291
};
5392

0 commit comments

Comments
 (0)