@openmrs/esm-fast-data-entry-app 1.4.2-pre.622 → 1.4.2-pre.626
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.
- package/dist/4300.js +1 -1
- package/dist/6466.js +1 -1
- package/dist/6466.js.map +1 -1
- package/dist/792.js +1 -1
- package/dist/792.js.map +1 -1
- package/dist/8091.js +1 -1
- package/dist/8091.js.map +1 -1
- package/dist/9814.js.map +1 -1
- package/dist/{7277.js → 9823.js} +1 -1
- package/dist/9823.js.map +1 -0
- package/dist/main.js +1 -1
- package/dist/main.js.map +1 -1
- package/dist/openmrs-esm-fast-data-entry-app.js +1 -1
- package/dist/openmrs-esm-fast-data-entry-app.js.buildmanifest.json +45 -45
- package/dist/routes.json +1 -1
- package/package.json +1 -1
- package/src/add-group-modal/AddGroupModal.test.tsx +198 -0
- package/src/add-group-modal/AddGroupModal.tsx +22 -4
- package/src/config-schema.ts +6 -0
- package/src/context/GroupFormWorkflowContext.tsx +14 -0
- package/src/form-entry-workflow/patient-search-header/PatientSearchHeader.test.tsx +121 -0
- package/src/form-entry-workflow/patient-search-header/PatientSearchHeader.tsx +28 -5
- package/src/group-form-entry-workflow/group-search-header/GroupSearchHeader.tsx +33 -8
- package/src/hooks/location-tag.resource.ts +1 -14
- package/src/hooks/useStartVisit.ts +3 -4
- package/src/types.ts +14 -0
- package/translations/en.json +3 -0
- package/dist/7277.js.map +0 -1
|
@@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next';
|
|
|
5
5
|
import {
|
|
6
6
|
ExtensionSlot,
|
|
7
7
|
fetchCurrentPatient,
|
|
8
|
-
|
|
8
|
+
showSnackbar,
|
|
9
9
|
useConfig,
|
|
10
10
|
usePatient,
|
|
11
11
|
useSession,
|
|
@@ -188,7 +188,23 @@ const AddGroupModal = ({
|
|
|
188
188
|
useEffect(() => {
|
|
189
189
|
if (!selectedPatientUuid || !hsuIdentifier) return;
|
|
190
190
|
|
|
191
|
-
|
|
191
|
+
const locationMismatch = sessionLocation.uuid != hsuIdentifier.location.uuid;
|
|
192
|
+
|
|
193
|
+
if (locationMismatch && config.enforcePatientListLocationMatch) {
|
|
194
|
+
showSnackbar({
|
|
195
|
+
kind: 'error',
|
|
196
|
+
title: t('locationMismatch', 'Location Mismatch'),
|
|
197
|
+
subtitle: t(
|
|
198
|
+
'patientLocationMismatchEnforced',
|
|
199
|
+
'Cannot add patient from {{hsuLocation}} to a session at {{sessionLocation}}',
|
|
200
|
+
{
|
|
201
|
+
hsuLocation: hsuIdentifier.location?.display,
|
|
202
|
+
sessionLocation: sessionLocation?.display,
|
|
203
|
+
},
|
|
204
|
+
),
|
|
205
|
+
});
|
|
206
|
+
setSelectedPatientUuid(null);
|
|
207
|
+
} else if (config.patientLocationMismatchCheck && locationMismatch) {
|
|
192
208
|
setPatientLocationMismatchModalOpen(true);
|
|
193
209
|
} else {
|
|
194
210
|
addSelectedPatientToList();
|
|
@@ -199,6 +215,8 @@ const AddGroupModal = ({
|
|
|
199
215
|
hsuIdentifier,
|
|
200
216
|
addSelectedPatientToList,
|
|
201
217
|
config.patientLocationMismatchCheck,
|
|
218
|
+
config.enforcePatientListLocationMatch,
|
|
219
|
+
t,
|
|
202
220
|
]);
|
|
203
221
|
|
|
204
222
|
const handleSubmit = () => {
|
|
@@ -236,10 +254,10 @@ const AddGroupModal = ({
|
|
|
236
254
|
|
|
237
255
|
useEffect(() => {
|
|
238
256
|
if (error) {
|
|
239
|
-
|
|
257
|
+
showSnackbar({
|
|
240
258
|
kind: 'error',
|
|
241
259
|
title: t('postError', 'POST Error'),
|
|
242
|
-
|
|
260
|
+
subtitle: error.message ?? t('unknownPostError', 'An unknown error occurred while saving data'),
|
|
243
261
|
});
|
|
244
262
|
if (error.fieldErrors) {
|
|
245
263
|
setErrors(
|
package/src/config-schema.ts
CHANGED
|
@@ -113,6 +113,11 @@ export const configSchema = {
|
|
|
113
113
|
'Whether to prompt for confirmation if the selected patient is not at the same location as the current session.',
|
|
114
114
|
_default: false,
|
|
115
115
|
},
|
|
116
|
+
enforcePatientListLocationMatch: {
|
|
117
|
+
_type: Type.Boolean,
|
|
118
|
+
_description: 'If true, prevents adding patients from a different location than the current session.',
|
|
119
|
+
_default: false,
|
|
120
|
+
},
|
|
116
121
|
};
|
|
117
122
|
|
|
118
123
|
export type Form = {
|
|
@@ -130,4 +135,5 @@ export type Config = {
|
|
|
130
135
|
formCategories: Array<Category>;
|
|
131
136
|
formCategoriesToShow: Array<string>;
|
|
132
137
|
patientLocationMismatchCheck: Type.Boolean;
|
|
138
|
+
enforcePatientListLocationMatch: Type.Boolean;
|
|
133
139
|
};
|
|
@@ -12,6 +12,20 @@ export interface GroupType {
|
|
|
12
12
|
id: string;
|
|
13
13
|
name: string;
|
|
14
14
|
members: Array<Type.Object>;
|
|
15
|
+
location?: {
|
|
16
|
+
uuid: string;
|
|
17
|
+
display?: string;
|
|
18
|
+
};
|
|
19
|
+
cohortMembers?: Array<{
|
|
20
|
+
patient: {
|
|
21
|
+
uuid?: string;
|
|
22
|
+
person?: {
|
|
23
|
+
names?: Array<{
|
|
24
|
+
display?: string;
|
|
25
|
+
}>;
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
}>;
|
|
15
29
|
}
|
|
16
30
|
|
|
17
31
|
export interface MetaType {
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen, fireEvent, cleanup, waitFor } from '@testing-library/react';
|
|
3
|
+
import '@testing-library/jest-dom';
|
|
4
|
+
import PatientSearchHeader from './PatientSearchHeader';
|
|
5
|
+
import FormWorkflowContext from '../../context/FormWorkflowContext';
|
|
6
|
+
import { showSnackbar, useConfig, useSession, type ConfigSchema, type Session } from '@openmrs/esm-framework';
|
|
7
|
+
import { useHsuIdIdentifier } from '../../hooks/location-tag.resource';
|
|
8
|
+
|
|
9
|
+
jest.mock('@openmrs/esm-framework', () => ({
|
|
10
|
+
ExtensionSlot: ({ state }) => (
|
|
11
|
+
<button data-testid="mock-search-select" onClick={() => state.selectPatientAction('patient-123')}>
|
|
12
|
+
Select Patient
|
|
13
|
+
</button>
|
|
14
|
+
),
|
|
15
|
+
interpolateUrl: jest.fn((url) => url),
|
|
16
|
+
navigate: jest.fn(),
|
|
17
|
+
showSnackbar: jest.fn(),
|
|
18
|
+
useConfig: jest.fn(),
|
|
19
|
+
useSession: jest.fn(),
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
jest.mock('react-i18next', () => ({
|
|
23
|
+
useTranslation: () => ({
|
|
24
|
+
t: (key: string, defaultValue: string, interpolation: { hsuLocation?: string; sessionLocation?: string }) => {
|
|
25
|
+
if (interpolation?.hsuLocation) {
|
|
26
|
+
return `Error: Patient at ${interpolation.hsuLocation} cannot be added to session at ${interpolation.sessionLocation}`;
|
|
27
|
+
}
|
|
28
|
+
return defaultValue || key;
|
|
29
|
+
},
|
|
30
|
+
}),
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
jest.mock('../../hooks/location-tag.resource', () => ({
|
|
34
|
+
useHsuIdIdentifier: jest.fn(),
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
jest.mock('react-router-dom', () => ({
|
|
38
|
+
Link: ({ children }) => <div>{children}</div>,
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
const mockShowSnackbar = showSnackbar as jest.MockedFunction<typeof showSnackbar>;
|
|
42
|
+
const mockUseConfig = useConfig as jest.MockedFunction<typeof useConfig>;
|
|
43
|
+
const mockUseSession = useSession as jest.MockedFunction<typeof useSession>;
|
|
44
|
+
const mockUseHsuIdIdentifier = useHsuIdIdentifier as jest.MockedFunction<typeof useHsuIdIdentifier>;
|
|
45
|
+
|
|
46
|
+
describe('PatientSearchHeader - Enforcement Feature', () => {
|
|
47
|
+
const mockContext = {
|
|
48
|
+
addPatient: jest.fn(),
|
|
49
|
+
workflowState: 'NEW_PATIENT',
|
|
50
|
+
activeFormUuid: 'form-123',
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const sessionLocation = { uuid: 'loc-session', display: 'General Hospital' };
|
|
54
|
+
const mismatchedHsuLocation = {
|
|
55
|
+
location: { uuid: 'loc-other', display: 'Remote Clinic' },
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
beforeEach(() => {
|
|
59
|
+
mockUseSession.mockReturnValue({ sessionLocation } as Session);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
afterEach(() => {
|
|
63
|
+
jest.clearAllMocks();
|
|
64
|
+
cleanup();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('triggers an error Snackbar when enforcePatientListLocationMatch is enabled and locations mismatch', async () => {
|
|
68
|
+
mockUseConfig.mockReturnValue({
|
|
69
|
+
enforcePatientListLocationMatch: true,
|
|
70
|
+
patientLocationMismatchCheck: false,
|
|
71
|
+
} as ConfigSchema);
|
|
72
|
+
|
|
73
|
+
mockUseHsuIdIdentifier.mockReturnValue({
|
|
74
|
+
hsuIdentifier: mismatchedHsuLocation,
|
|
75
|
+
} as unknown as ReturnType<typeof useHsuIdIdentifier>);
|
|
76
|
+
|
|
77
|
+
render(
|
|
78
|
+
<FormWorkflowContext.Provider value={mockContext as never}>
|
|
79
|
+
<PatientSearchHeader />
|
|
80
|
+
</FormWorkflowContext.Provider>,
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const searchBar = screen.getByTestId('mock-search-select');
|
|
84
|
+
fireEvent.click(searchBar);
|
|
85
|
+
|
|
86
|
+
await waitFor(() => {
|
|
87
|
+
expect(mockShowSnackbar).toHaveBeenCalledWith(
|
|
88
|
+
expect.objectContaining({
|
|
89
|
+
kind: 'error',
|
|
90
|
+
title: 'Location Mismatch',
|
|
91
|
+
subtitle: expect.stringContaining('Remote Clinic'),
|
|
92
|
+
}),
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
expect(mockContext.addPatient).not.toHaveBeenCalled();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('does NOT trigger snackbar and adds patient if locations match even if enforcement is on', async () => {
|
|
100
|
+
mockUseConfig.mockReturnValue({
|
|
101
|
+
enforcePatientListLocationMatch: true,
|
|
102
|
+
} as ConfigSchema);
|
|
103
|
+
|
|
104
|
+
mockUseHsuIdIdentifier.mockReturnValue({
|
|
105
|
+
hsuIdentifier: { location: { uuid: 'loc-session', display: 'General Hospital' } },
|
|
106
|
+
} as unknown as ReturnType<typeof useHsuIdIdentifier>);
|
|
107
|
+
|
|
108
|
+
render(
|
|
109
|
+
<FormWorkflowContext.Provider value={mockContext as never}>
|
|
110
|
+
<PatientSearchHeader />
|
|
111
|
+
</FormWorkflowContext.Provider>,
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
fireEvent.click(screen.getByTestId('mock-search-select'));
|
|
115
|
+
|
|
116
|
+
await waitFor(() => {
|
|
117
|
+
expect(mockContext.addPatient).toHaveBeenCalledWith('patient-123');
|
|
118
|
+
expect(mockShowSnackbar).not.toHaveBeenCalled();
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Add, Close } from '@carbon/react/icons';
|
|
2
|
-
import { ExtensionSlot, interpolateUrl, navigate, useConfig, useSession } from '@openmrs/esm-framework';
|
|
2
|
+
import { ExtensionSlot, interpolateUrl, navigate, useConfig, useSession, showSnackbar } from '@openmrs/esm-framework';
|
|
3
3
|
import { Button } from '@carbon/react';
|
|
4
4
|
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
|
5
5
|
import { Link } from 'react-router-dom';
|
|
@@ -16,6 +16,7 @@ const PatientSearchHeader = () => {
|
|
|
16
16
|
const { sessionLocation } = useSession();
|
|
17
17
|
const config = useConfig();
|
|
18
18
|
const { addPatient, workflowState, activeFormUuid } = useContext(FormWorkflowContext);
|
|
19
|
+
const { t } = useTranslation();
|
|
19
20
|
|
|
20
21
|
const onPatientMismatchedLocationModalConfirm = useCallback(() => {
|
|
21
22
|
addPatient(selectedPatientUuid);
|
|
@@ -34,15 +35,37 @@ const PatientSearchHeader = () => {
|
|
|
34
35
|
useEffect(() => {
|
|
35
36
|
if (!selectedPatientUuid || !hsuIdentifier) return;
|
|
36
37
|
|
|
37
|
-
|
|
38
|
+
const locationMismatch = sessionLocation.uuid !== hsuIdentifier.location.uuid;
|
|
39
|
+
|
|
40
|
+
if (config.enforcePatientListLocationMatch && locationMismatch) {
|
|
41
|
+
showSnackbar({
|
|
42
|
+
kind: 'error',
|
|
43
|
+
title: t('locationMismatch', 'Location Mismatch'),
|
|
44
|
+
subtitle: t(
|
|
45
|
+
'patientLocationMismatchEnforced',
|
|
46
|
+
'Cannot add patient from {{hsuLocation}} to a session at {{sessionLocation}}',
|
|
47
|
+
{
|
|
48
|
+
hsuLocation: hsuIdentifier.location?.display,
|
|
49
|
+
sessionLocation: sessionLocation?.display,
|
|
50
|
+
},
|
|
51
|
+
),
|
|
52
|
+
});
|
|
53
|
+
setSelectedPatientUuid(null);
|
|
54
|
+
} else if (config.patientLocationMismatchCheck && locationMismatch) {
|
|
38
55
|
setPatientLocationMismatchModalOpen(true);
|
|
39
56
|
} else {
|
|
40
57
|
addPatient(selectedPatientUuid);
|
|
41
58
|
setSelectedPatientUuid(null);
|
|
42
59
|
}
|
|
43
|
-
}, [
|
|
44
|
-
|
|
45
|
-
|
|
60
|
+
}, [
|
|
61
|
+
selectedPatientUuid,
|
|
62
|
+
sessionLocation,
|
|
63
|
+
hsuIdentifier,
|
|
64
|
+
addPatient,
|
|
65
|
+
config.patientLocationMismatchCheck,
|
|
66
|
+
config.enforcePatientListLocationMatch,
|
|
67
|
+
t,
|
|
68
|
+
]);
|
|
46
69
|
|
|
47
70
|
if (workflowState !== 'NEW_PATIENT') return null;
|
|
48
71
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Close, Add } from '@carbon/react/icons';
|
|
2
2
|
import { Button } from '@carbon/react';
|
|
3
3
|
import React, { useCallback, useContext, useState } from 'react';
|
|
4
|
+
import { useConfig, useSession, showSnackbar } from '@openmrs/esm-framework';
|
|
4
5
|
import GroupFormWorkflowContext from '../../context/GroupFormWorkflowContext';
|
|
5
6
|
import styles from './styles.scss';
|
|
6
7
|
import { useTranslation } from 'react-i18next';
|
|
@@ -9,16 +10,40 @@ import AddGroupModal from '../../add-group-modal/AddGroupModal';
|
|
|
9
10
|
|
|
10
11
|
const GroupSearchHeader = () => {
|
|
11
12
|
const { t } = useTranslation();
|
|
13
|
+
const config = useConfig();
|
|
14
|
+
const { sessionLocation } = useSession();
|
|
12
15
|
const { activeGroupUuid, setGroup, destroySession } = useContext(GroupFormWorkflowContext);
|
|
13
16
|
const [isOpen, setOpen] = useState(false);
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
|
|
18
|
+
const handleSelectGroup = useCallback(
|
|
19
|
+
(group) => {
|
|
20
|
+
if (config.enforcePatientListLocationMatch && group.location && sessionLocation.uuid !== group.location.uuid) {
|
|
21
|
+
showSnackbar({
|
|
22
|
+
kind: 'error',
|
|
23
|
+
title: t('locationMismatch', 'Location Mismatch'),
|
|
24
|
+
subtitle: t(
|
|
25
|
+
'groupLocationMismatchEnforced',
|
|
26
|
+
'Cannot select group from {{groupLocation}} for a session at {{sessionLocation}}',
|
|
27
|
+
{
|
|
28
|
+
groupLocation: group.location?.display,
|
|
29
|
+
sessionLocation: sessionLocation?.display,
|
|
30
|
+
},
|
|
31
|
+
),
|
|
32
|
+
});
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (group.cohortMembers) {
|
|
37
|
+
group.cohortMembers.sort((a, b) => {
|
|
38
|
+
const aName = a?.patient?.person?.names?.[0]?.display;
|
|
39
|
+
const bName = b?.patient?.person?.names?.[0]?.display;
|
|
40
|
+
return aName.localeCompare(bName, undefined, { sensitivity: 'base' });
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
setGroup(group);
|
|
44
|
+
},
|
|
45
|
+
[config.enforcePatientListLocationMatch, sessionLocation, setGroup, t],
|
|
46
|
+
);
|
|
22
47
|
|
|
23
48
|
const handleCancel = useCallback(() => {
|
|
24
49
|
setOpen(false);
|
|
@@ -1,19 +1,6 @@
|
|
|
1
1
|
import useSWR from 'swr';
|
|
2
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
|
-
}
|
|
3
|
+
import { type Identifier } from '../types';
|
|
17
4
|
|
|
18
5
|
export function useHsuIdIdentifier(patientUuid: string) {
|
|
19
6
|
const hsuIdType = '05a29f94-c0ed-11e2-94be-8c13b969e334';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useCallback, useState } from 'react';
|
|
2
2
|
import { useTranslation } from 'react-i18next';
|
|
3
|
-
import { showNotification,
|
|
3
|
+
import { showNotification, showSnackbar, openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
|
|
4
4
|
|
|
5
5
|
const useStartVisit = ({ showSuccessNotification = true, showErrorNotification = true }) => {
|
|
6
6
|
const { t } = useTranslation();
|
|
@@ -14,10 +14,9 @@ const useStartVisit = ({ showSuccessNotification = true, showErrorNotification =
|
|
|
14
14
|
setError(false);
|
|
15
15
|
setSuccess(result);
|
|
16
16
|
if (showSuccessNotification) {
|
|
17
|
-
|
|
18
|
-
critical: true,
|
|
17
|
+
showSnackbar({
|
|
19
18
|
kind: 'success',
|
|
20
|
-
|
|
19
|
+
subtitle: t('visitStartedSuccessfully', `${result?.data?.visitType?.display} started successfully`),
|
|
21
20
|
title: t('visitStarted', 'Visit started'),
|
|
22
21
|
});
|
|
23
22
|
}
|
package/src/types.ts
CHANGED
|
@@ -23,3 +23,17 @@ export interface SpecificQuestionConfig {
|
|
|
23
23
|
defaultAnswer?: string;
|
|
24
24
|
disabled?: boolean;
|
|
25
25
|
}
|
|
26
|
+
|
|
27
|
+
export interface Identifier {
|
|
28
|
+
uuid: string;
|
|
29
|
+
identifier: string;
|
|
30
|
+
display: string;
|
|
31
|
+
identifierType: {
|
|
32
|
+
uuid: string;
|
|
33
|
+
display: string;
|
|
34
|
+
};
|
|
35
|
+
location: {
|
|
36
|
+
uuid: string;
|
|
37
|
+
display: string;
|
|
38
|
+
};
|
|
39
|
+
}
|
package/translations/en.json
CHANGED
|
@@ -27,8 +27,10 @@
|
|
|
27
27
|
"formsAppMenuLink": "Fast Data Entry",
|
|
28
28
|
"formsFilled": "Forms filled",
|
|
29
29
|
"goToForm": "Go To Form",
|
|
30
|
+
"groupLocationMismatchEnforced": "Cannot select group from {{groupLocation}} for a session at {{sessionLocation}}",
|
|
30
31
|
"groupNameError": "Please enter a group name.",
|
|
31
32
|
"identifier": "Identifier",
|
|
33
|
+
"locationMismatch": "Location Mismatch",
|
|
32
34
|
"markAbsentPatients": "The patients in this group. Patients that are not present in the session should be marked as absent.",
|
|
33
35
|
"members": "members",
|
|
34
36
|
"name": "Name",
|
|
@@ -44,6 +46,7 @@
|
|
|
44
46
|
"orLabelName": "OR label name",
|
|
45
47
|
"patientIsPresent": "Patient is present",
|
|
46
48
|
"patientLocationMismatch": "The selected HSU location ({{hsuLocation}}) does not match the current session location ({{sessionLocation}}). Are you sure you want to proceed?",
|
|
49
|
+
"patientLocationMismatchEnforced": "Cannot add patient from {{hsuLocation}} to a session at {{sessionLocation}}",
|
|
47
50
|
"patientsInGroup": "Patients in group",
|
|
48
51
|
"postError": "POST Error",
|
|
49
52
|
"practitionerName": "Practitioner Name",
|