@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.
@@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next';
5
5
  import {
6
6
  ExtensionSlot,
7
7
  fetchCurrentPatient,
8
- showToast,
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
- if (config.patientLocationMismatchCheck && sessionLocation.uuid != hsuIdentifier.location.uuid) {
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
- showToast({
257
+ showSnackbar({
240
258
  kind: 'error',
241
259
  title: t('postError', 'POST Error'),
242
- description: error.message ?? t('unknownPostError', 'An unknown error occurred while saving data'),
260
+ subtitle: error.message ?? t('unknownPostError', 'An unknown error occurred while saving data'),
243
261
  });
244
262
  if (error.fieldErrors) {
245
263
  setErrors(
@@ -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
- if (config.patientLocationMismatchCheck && hsuIdentifier && sessionLocation.uuid != hsuIdentifier.location.uuid) {
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
- }, [selectedPatientUuid, sessionLocation, hsuIdentifier, addPatient, config.patientLocationMismatchCheck]);
44
-
45
- const { t } = useTranslation();
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
- const handleSelectGroup = (group) => {
15
- group.cohortMembers.sort((a, b) => {
16
- const aName = a?.patient?.person?.names?.[0]?.display;
17
- const bName = b?.patient?.person?.names?.[0]?.display;
18
- return aName.localeCompare(bName, undefined, { sensitivity: 'base' });
19
- });
20
- setGroup(group);
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, showToast, openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
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
- showToast({
18
- critical: true,
17
+ showSnackbar({
19
18
  kind: 'success',
20
- description: t('visitStartedSuccessfully', `${result?.data?.visitType?.display} started successfully`),
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
+ }
@@ -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",