@openmrs/esm-fast-data-entry-app 1.2.1-pre.240 → 1.2.1-pre.244

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,10 +2,19 @@ import React, { useCallback, useContext, useEffect, useMemo, useState } from 're
2
2
  import { ComposedModal, Button, ModalHeader, ModalFooter, ModalBody, TextInput, FormLabel } from '@carbon/react';
3
3
  import { TrashCan } from '@carbon/react/icons';
4
4
  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';
6
13
  import styles from './styles.scss';
7
14
  import GroupFormWorkflowContext from '../context/GroupFormWorkflowContext';
8
15
  import { usePostCohort } from '../hooks';
16
+ import PatientLocationMismatchModal from '../form-entry-workflow/patient-search-header/PatienMismatchedLocationModal';
17
+ import { useHsuIdIdentifier } from '../hooks/location-tag.resource';
9
18
 
10
19
  const MemExtension = React.memo(ExtensionSlot);
11
20
 
@@ -112,6 +121,10 @@ const AddGroupModal = ({
112
121
  const [patientList, setPatientList] = useState(patients || []);
113
122
  const { post, result, error } = usePostCohort();
114
123
  const config = useConfig();
124
+ const [patientLocationMismatchModalOpen, setPatientLocationMismatchModalOpen] = useState(false);
125
+ const [selectedPatientUuid, setSelectedPatientUuid] = useState();
126
+ const { hsuIdentifier } = useHsuIdIdentifier(selectedPatientUuid);
127
+ const { sessionLocation } = useSession();
115
128
 
116
129
  const removePatient = useCallback(
117
130
  (patientUuid: string) =>
@@ -147,27 +160,44 @@ const AddGroupModal = ({
147
160
  [name, patientList.length],
148
161
  );
149
162
 
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
+ ]);
171
201
 
172
202
  const handleSubmit = () => {
173
203
  if (validate()) {
@@ -216,33 +246,47 @@ const AddGroupModal = ({
216
246
  }
217
247
  }, [error, t]);
218
248
 
249
+ const onPatientLocationMismatchModalCancel = () => {
250
+ setSelectedPatientUuid(null);
251
+ };
252
+
219
253
  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
+ </>
246
290
  );
247
291
  };
248
292
 
@@ -128,6 +128,12 @@ export const configSchema = {
128
128
  },
129
129
  _default: [],
130
130
  },
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
+ },
131
137
  };
132
138
 
133
139
  export type Form = {
@@ -144,4 +150,5 @@ export type Category = {
144
150
  export type Config = {
145
151
  formCategories: Array<Category>;
146
152
  formCategoriesToShow: Array<string>;
153
+ patientLocationMismatchCheck: Type.Boolean;
147
154
  };
@@ -73,7 +73,7 @@ const FormWorkspace = () => {
73
73
 
74
74
  useEffect(() => {
75
75
  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
77
77
  updateEncounter({ uuid: encounter.uuid, visit: visit.uuid });
78
78
  }
79
79
  }, [encounter, visit, updateEncounter]);
@@ -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;
@@ -1,17 +1,46 @@
1
1
  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';
3
3
  import { Button } from '@carbon/react';
4
- import React, { useContext } from 'react';
4
+ import React, { useCallback, useContext, useEffect, useState } from 'react';
5
5
  import { Link } from 'react-router-dom';
6
6
  import FormWorkflowContext from '../../context/FormWorkflowContext';
7
7
  import styles from './styles.scss';
8
8
  import { useTranslation } from 'react-i18next';
9
+ import { useHsuIdIdentifier } from '../../hooks/location-tag.resource';
10
+ import PatientLocationMismatchModal from './PatienMismatchedLocationModal';
9
11
 
10
12
  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();
11
18
  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
+
15
44
  const { t } = useTranslation();
16
45
 
17
46
  if (workflowState !== 'NEW_PATIENT') return null;
@@ -20,34 +49,44 @@ const PatientSearchHeader = () => {
20
49
  const patientRegistrationUrl = interpolateUrl(`\${openmrsSpaBase}/patient-registration?afterUrl=${afterUrl}`);
21
50
 
22
51
  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} />
47
70
  </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
+ </>
51
90
  );
52
91
  };
53
92
 
@@ -0,0 +1,33 @@
1
+ import useSWR from 'swr';
2
+ import { openmrsFetch } from '@openmrs/esm-framework';
3
+
4
+ export interface Identifier {
5
+ uuid: string;
6
+ identifier: string;
7
+ display: string;
8
+ identifierType: {
9
+ uuid: string;
10
+ display: string;
11
+ };
12
+ location: {
13
+ uuid: string;
14
+ display: string;
15
+ };
16
+ }
17
+
18
+ export function useHsuIdIdentifier(patientUuid: string) {
19
+ const hsuIdType = '05a29f94-c0ed-11e2-94be-8c13b969e334';
20
+ const url = patientUuid ? `ws/rest/v1/patient/${patientUuid}/identifier` : null;
21
+ const { data, error, isValidating } = useSWR<{ data: { results: Array<Identifier> } }, Error>(url, openmrsFetch);
22
+
23
+ const hsuIdentifier = data?.data?.results.length
24
+ ? data.data.results.find((id: Identifier) => id.identifierType.uuid == hsuIdType)
25
+ : undefined;
26
+
27
+ return {
28
+ hsuIdentifier: hsuIdentifier,
29
+ isLoading: !data && !error,
30
+ isError: error,
31
+ isValidating,
32
+ };
33
+ }
@@ -9,6 +9,8 @@
9
9
  "chooseGroupError": "Please choose a group.",
10
10
  "clearSearch": "Clear",
11
11
  "complete": "Complete",
12
+ "confirmPatientSelection": "Confirm patient selection",
13
+ "continue": "Continue",
12
14
  "createGroup": "Create Group",
13
15
  "createNewGroup": "Create New Group",
14
16
  "createNewPatient": "Create new patient",
@@ -41,6 +43,7 @@
41
43
  "or": "or",
42
44
  "orLabelName": "OR label name",
43
45
  "patientIsPresent": "Patient is present",
46
+ "patientLocationMismatch": "The selected HSU location ({{hsuLocation}}) does not match the current session location ({{sessionLocation}}). Are you sure you want to proceed?",
44
47
  "patientsInGroup": "Patients in group",
45
48
  "postError": "POST Error",
46
49
  "practitionerName": "Practitioner Name",
@@ -9,6 +9,8 @@
9
9
  "chooseGroupError": "Por favor, elige un grupo.",
10
10
  "clearSearch": "Limpiar",
11
11
  "complete": "Completar",
12
+ "confirmPatientSelection": "Confirmar la selección del paciente",
13
+ "continue": "Continue",
12
14
  "createGroup": "Crear Grupo",
13
15
  "createNewGroup": "Crear Nuevo Grupo",
14
16
  "createNewPatient": "Crear nuevo paciente",
@@ -41,6 +43,7 @@
41
43
  "or": "o",
42
44
  "orLabelName": "Nombre de Etiqueta OR",
43
45
  "patientIsPresent": "Paciente está presente",
46
+ "patientLocationMismatch": "La ubicación de HSU seleccionada ({{hsuLocation}}) no coincide con la ubicación de la sesión actual ({{sessionLocation}}). ¿Está seguro de que desea continuar?",
44
47
  "patientsInGroup": "Pacientes en el grupo",
45
48
  "postError": "Error de POST",
46
49
  "practitionerName": "Nombre del Practicante",
@@ -9,6 +9,8 @@
9
9
  "chooseGroupError": "Veuillez sélectionner un groupe",
10
10
  "clearSearch": "Clair",
11
11
  "complete": "Terminer",
12
+ "confirmPatientSelection": "Confirmer la sélection du patient",
13
+ "continue": "Continue",
12
14
  "createGroup": "Créer un groupe",
13
15
  "createNewGroup": "Créer un nouveau groupe",
14
16
  "createNewPatient": "Créer un nouveau patient",
@@ -41,6 +43,7 @@
41
43
  "or": "ou",
42
44
  "orLabelName": "OU nom de l'étiquette",
43
45
  "patientIsPresent": "Le patient est présent",
46
+ "patientLocationMismatch": "L'emplacement HSU sélectionné ({{hsuLocation}}) ne correspond pas à l'emplacement de la session actuelle ({{sessionLocation}}). Voulez-vous vraiment continuer\u00a0?",
44
47
  "patientsInGroup": "Patients dans le groupe",
45
48
  "postError": "Erreur de POST",
46
49
  "practitionerName": "Nom du praticien",