@openmrs/esm-fast-data-entry-app 1.2.1-pre.238 → 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
 
@@ -26,6 +26,11 @@ export const configSchema = {
26
26
  _type: Type.String,
27
27
  _description: 'Name of form',
28
28
  },
29
+ disableGroupSession: {
30
+ _type: Type.Boolean,
31
+ _description: 'Disable group sessions for this form',
32
+ _default: false,
33
+ },
29
34
  },
30
35
  },
31
36
  },
@@ -36,6 +41,7 @@ export const configSchema = {
36
41
  {
37
42
  formUUID: '0cefb866-110c-4f16-af58-560932a1db1f',
38
43
  name: 'Adult Triage',
44
+ disableGroupSession: false,
39
45
  },
40
46
  ],
41
47
  },
@@ -45,6 +51,7 @@ export const configSchema = {
45
51
  {
46
52
  formUUID: '9f26aad4-244a-46ca-be49-1196df1a8c9a',
47
53
  name: 'POC Sample Form 1',
54
+ disableGroupSession: false,
48
55
  },
49
56
  ],
50
57
  },
@@ -121,11 +128,18 @@ export const configSchema = {
121
128
  },
122
129
  _default: [],
123
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
+ },
124
137
  };
125
138
 
126
139
  export type Form = {
127
140
  formUUID: Type.UUID;
128
141
  name: Type.String;
142
+ disableGroupSession: Type.Boolean;
129
143
  };
130
144
 
131
145
  export type Category = {
@@ -136,4 +150,5 @@ export type Category = {
136
150
  export type Config = {
137
151
  formCategories: Array<Category>;
138
152
  formCategoriesToShow: Array<string>;
153
+ patientLocationMismatchCheck: Type.Boolean;
139
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
 
@@ -24,17 +24,29 @@ export const getFormPermissions = (forms) => {
24
24
  return output;
25
25
  };
26
26
 
27
- // Function adds `id` field to rows so they will be accepted by DataTable
28
- // "display" is prefered for display name if present, otherwise fall back on "name'"
29
- const prepareRowsForTable = (rawFormData) => {
30
- if (rawFormData) {
31
- return rawFormData?.map((form) => ({
32
- ...form,
33
- id: form.uuid,
34
- display: form.display || form.name,
35
- }));
36
- }
37
- return null;
27
+ /**
28
+ * Prepares the raw form data to be used in a DataTable.
29
+ * Adds an `id` field based on the `uuid` property of the form.
30
+ * Sets the `display` field based on the `display` property if present, otherwise falls back to the `name` field.
31
+ * Also attaches the `disableGroupSession` flag from form categories config, if available.
32
+ *
33
+ * @param {Array} rawFormData
34
+ * @param {Array} formCategories
35
+ * @returns {Array}
36
+ */
37
+ const prepareRowsForTable = (rawFormData = [], formCategories = []) => {
38
+ const formCategoryMap = new Map(
39
+ formCategories.flatMap(({ forms }) =>
40
+ forms.map(({ formUUID, disableGroupSession }) => [formUUID, disableGroupSession]),
41
+ ),
42
+ );
43
+
44
+ return rawFormData.map((form) => ({
45
+ ...form,
46
+ id: form.uuid,
47
+ display: form.display || form.name,
48
+ disableGroupSession: formCategoryMap.get(form.uuid),
49
+ }));
38
50
  };
39
51
 
40
52
  const FormsPage = () => {
@@ -42,7 +54,7 @@ const FormsPage = () => {
42
54
  const { t } = useTranslation();
43
55
  const { formCategories, formCategoriesToShow } = config;
44
56
  const { forms, isLoading, error } = useGetAllForms();
45
- const cleanRows = prepareRowsForTable(forms);
57
+ const cleanRows = prepareRowsForTable(forms, formCategories);
46
58
  const { user } = useSession();
47
59
  const savedFormsData = localStorage.getItem(fdeWorkflowStorageName + ':' + user?.uuid);
48
60
  const savedGroupFormsData = localStorage.getItem(fdeGroupWorkflowStorageName + ':' + user?.uuid);
@@ -45,7 +45,7 @@ const FormsTable = ({ rows, error, isLoading, activeForms, activeGroupForms }) =
45
45
  {activeForms.includes(row.uuid) ? t('resumeSession', 'Resume Session') : t('fillForm', 'Fill Form')}
46
46
  </Link>
47
47
  ),
48
- actions2: (
48
+ actions2: !row.disableGroupSession && (
49
49
  <Link to={`groupform/${row.uuid}`}>
50
50
  {activeGroupForms.includes(row.uuid)
51
51
  ? t('resumeGroupSession', 'Resume Group Session')
@@ -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",