@openmrs/esm-ward-app 9.2.1-pre.7244 → 9.2.1-pre.7261

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.
Files changed (111) hide show
  1. package/.turbo/turbo-build.log +8 -8
  2. package/dist/1741.js +1 -1
  3. package/dist/1741.js.map +1 -1
  4. package/dist/1987.js +1 -0
  5. package/dist/1987.js.map +1 -0
  6. package/dist/2216.js +1 -0
  7. package/dist/2216.js.map +1 -0
  8. package/dist/2728.js +1 -1
  9. package/dist/2728.js.map +1 -1
  10. package/dist/283.js +1 -1
  11. package/dist/283.js.map +1 -1
  12. package/dist/2948.js +1 -1
  13. package/dist/2948.js.map +1 -1
  14. package/dist/3365.js +1 -1
  15. package/dist/3365.js.map +1 -1
  16. package/dist/3413.js +1 -1
  17. package/dist/3413.js.map +1 -1
  18. package/dist/3673.js +1 -0
  19. package/dist/3673.js.map +1 -0
  20. package/dist/3982.js +1 -1
  21. package/dist/3982.js.map +1 -1
  22. package/dist/4189.js +1 -0
  23. package/dist/4189.js.map +1 -0
  24. package/dist/4300.js +1 -1
  25. package/dist/5603.js +1 -0
  26. package/dist/5603.js.map +1 -0
  27. package/dist/581.js +1 -1
  28. package/dist/581.js.map +1 -1
  29. package/dist/7179.js +1 -1
  30. package/dist/7179.js.map +1 -1
  31. package/dist/7512.js +1 -1
  32. package/dist/7512.js.map +1 -1
  33. package/dist/7661.js +1 -1
  34. package/dist/7661.js.map +1 -1
  35. package/dist/8501.js +1 -1
  36. package/dist/8501.js.map +1 -1
  37. package/dist/8522.js +1 -1
  38. package/dist/8522.js.map +1 -1
  39. package/dist/8610.js +1 -1
  40. package/dist/8610.js.map +1 -1
  41. package/dist/89.js +2 -1
  42. package/dist/89.js.map +1 -1
  43. package/dist/9117.js +1 -1
  44. package/dist/9117.js.map +1 -1
  45. package/dist/917.js +1 -0
  46. package/dist/917.js.map +1 -0
  47. package/dist/9756.js +1 -0
  48. package/dist/9756.js.map +1 -0
  49. package/dist/main.js +1 -1
  50. package/dist/main.js.map +1 -1
  51. package/dist/openmrs-esm-ward-app.js +1 -1
  52. package/dist/openmrs-esm-ward-app.js.buildmanifest.json +226 -177
  53. package/dist/openmrs-esm-ward-app.js.map +1 -1
  54. package/dist/routes.json +1 -1
  55. package/package.json +1 -1
  56. package/src/action-menu-buttons/clinical-forms-workspace-siderail.component.tsx +16 -24
  57. package/src/action-menu-buttons/discharge-workspace-siderail.component.tsx +6 -6
  58. package/src/action-menu-buttons/order-basket-action-button.component.tsx +31 -0
  59. package/src/action-menu-buttons/transfer-workspace-siderail.component.tsx +7 -18
  60. package/src/hooks/useEmrConfiguration.ts +19 -19
  61. package/src/index.ts +14 -16
  62. package/src/routes.json +127 -80
  63. package/src/types/index.ts +7 -3
  64. package/src/ward-patient-card/row-elements/ward-patient-pending-transfer.component.tsx +9 -4
  65. package/src/ward-patient-card/ward-patient-card.component.tsx +3 -11
  66. package/src/ward-view-header/admission-requests-bar.component.tsx +10 -6
  67. package/src/ward-view-header/admission-requests-bar.test.tsx +3 -3
  68. package/src/ward-workspace/admission-request-card/admission-request-card-actions.component.tsx +8 -6
  69. package/src/ward-workspace/admission-request-workspace/admission-requests-action-button.extension.tsx +6 -7
  70. package/src/ward-workspace/admission-request-workspace/admission-requests-empty-state.component.tsx +16 -29
  71. package/src/ward-workspace/admission-request-workspace/admission-requests-workspace.test.tsx +23 -8
  72. package/src/ward-workspace/admission-request-workspace/admission-requests.workspace.tsx +28 -28
  73. package/src/ward-workspace/admit-patient-button.component.tsx +3 -2
  74. package/src/ward-workspace/admit-patient-form-workspace/admit-patient-form.test.tsx +17 -16
  75. package/src/ward-workspace/admit-patient-form-workspace/admit-patient-form.workspace.tsx +72 -69
  76. package/src/ward-workspace/cancel-admission-request-workspace/cancel-admission-request.component.tsx +176 -0
  77. package/src/ward-workspace/cancel-admission-request-workspace/cancel-admission-request.test.tsx +11 -9
  78. package/src/ward-workspace/cancel-admission-request-workspace/cancel-admission-request.workspace.tsx +17 -167
  79. package/src/ward-workspace/cancel-admission-request-workspace/ward-patient-cancel-admission-request.workspace.tsx +16 -0
  80. package/src/ward-workspace/create-admission-encounter/create-admission-encounter-action-button.extension.tsx +23 -34
  81. package/src/ward-workspace/create-admission-encounter/create-admission-encounter.test.tsx +9 -4
  82. package/src/ward-workspace/create-admission-encounter/create-admission-encounter.workspace.tsx +39 -19
  83. package/src/ward-workspace/patient-details/ward-patient-action-button.component.tsx +17 -0
  84. package/src/ward-workspace/patient-details/ward-patient.workspace.tsx +27 -7
  85. package/src/ward-workspace/patient-discharge/patient-discharge.workspace.tsx +46 -40
  86. package/src/ward-workspace/patient-transfer-bed-swap/patient-admit-or-transfer-request-form.component.tsx +21 -13
  87. package/src/ward-workspace/patient-transfer-bed-swap/patient-bed-swap-form.component.tsx +10 -14
  88. package/src/ward-workspace/patient-transfer-bed-swap/patient-transfer-swap.workspace.tsx +42 -24
  89. package/src/ward-workspace/patient-transfer-request-workspace/patient-transfer-request.workspace.tsx +22 -8
  90. package/src/ward-workspace/ward-patient-notes/notes-action-button.component.tsx +17 -0
  91. package/src/ward-workspace/ward-patient-notes/{form/notes-form.scss → notes.scss} +0 -1
  92. package/src/ward-workspace/ward-patient-notes/notes.test.tsx +134 -0
  93. package/src/ward-workspace/ward-patient-notes/notes.workspace.tsx +174 -13
  94. package/translations/en.json +3 -1
  95. package/dist/1663.js +0 -1
  96. package/dist/1663.js.map +0 -1
  97. package/dist/2557.js +0 -1
  98. package/dist/2557.js.map +0 -1
  99. package/dist/7232.js +0 -2
  100. package/dist/7232.js.map +0 -1
  101. package/dist/7886.js +0 -1
  102. package/dist/7886.js.map +0 -1
  103. package/dist/9045.js +0 -1
  104. package/dist/9045.js.map +0 -1
  105. package/src/ward-workspace/admission-request-workspace/admission-requests-context.ts +0 -20
  106. package/src/ward-workspace/patient-clinical-forms-workspace/patient-clinical-forms.workspace.tsx +0 -29
  107. package/src/ward-workspace/patient-details/ward-patient-action-button.extension.tsx +0 -18
  108. package/src/ward-workspace/ward-patient-notes/form/notes-form.component.tsx +0 -186
  109. package/src/ward-workspace/ward-patient-notes/form/notes-form.test.tsx +0 -116
  110. package/src/ward-workspace/ward-patient-notes/notes-action-button.extension.tsx +0 -18
  111. /package/dist/{7232.js.LICENSE.txt → 89.js.LICENSE.txt} +0 -0
@@ -11,19 +11,32 @@ import {
11
11
  TextArea,
12
12
  } from '@carbon/react';
13
13
  import { zodResolver } from '@hookform/resolvers/zod';
14
- import { ResponsiveWrapper, showSnackbar, useAppContext } from '@openmrs/esm-framework';
14
+ import { ResponsiveWrapper, showSnackbar, useAppContext, Workspace2 } from '@openmrs/esm-framework';
15
15
  import classNames from 'classnames';
16
16
  import React, { useCallback, useEffect, useMemo, useState } from 'react';
17
17
  import { Controller, useForm } from 'react-hook-form';
18
18
  import { useTranslation } from 'react-i18next';
19
19
  import { z } from 'zod';
20
20
  import LocationSelector from '../../location-selector/location-selector.component';
21
- import type { ObsPayload, WardPatientWorkspaceProps, WardViewContext } from '../../types';
21
+ import type { ObsPayload, WardPatient, WardPatientWorkspaceProps, WardViewContext } from '../../types';
22
22
  import { useCreateEncounter } from '../../ward.resource';
23
23
  import styles from './patient-transfer-swap.scss';
24
24
  import WardPatientName from '../../ward-patient-card/row-elements/ward-patient-name.component';
25
25
  import WardPatientIdentifier from '../../ward-patient-card/row-elements/ward-patient-identifier.component';
26
26
 
27
+ export interface PatientAdmitOrTransferFormProps {
28
+ wardPatient: WardPatient;
29
+
30
+ /**
31
+ * Related patients that are in the same bed as wardPatient. On transfer or bed swap
32
+ * these related patients have the option to be transferred / swapped together
33
+ */
34
+ relatedTransferPatients?: WardPatient[];
35
+
36
+ onSuccess(): void;
37
+ onCancel(): void;
38
+ }
39
+
27
40
  /**
28
41
  * Form to fill out for:
29
42
  * - an admitted patient without pending transfer request, to initiate a transfer request for a patient
@@ -31,11 +44,11 @@ import WardPatientIdentifier from '../../ward-patient-card/row-elements/ward-pat
31
44
  * - an un-admitted patient, to create a request to admit
32
45
  */
33
46
  export default function PatientAdmitOrTransferForm({
34
- closeWorkspaceWithSavedChanges,
35
47
  wardPatient,
36
- promptBeforeClosing,
37
48
  relatedTransferPatients = [],
38
- }: WardPatientWorkspaceProps) {
49
+ onSuccess,
50
+ onCancel,
51
+ }: PatientAdmitOrTransferFormProps) {
39
52
  const { t } = useTranslation();
40
53
  const { patient, inpatientRequest, visit } = wardPatient ?? {};
41
54
  const [showErrorNotifications, setShowErrorNotifications] = useState(false);
@@ -90,11 +103,6 @@ export default function PatientAdmitOrTransferForm({
90
103
  }
91
104
  }, [dispositionsWithTypeTransfer, setValue]);
92
105
 
93
- useEffect(() => {
94
- promptBeforeClosing(() => isDirty);
95
- return () => promptBeforeClosing(null);
96
- }, [isDirty, promptBeforeClosing]);
97
-
98
106
  const onSubmit = useCallback(
99
107
  (values: FormValues) => {
100
108
  setIsSubmitting(true);
@@ -144,6 +152,7 @@ export default function PatientAdmitOrTransferForm({
144
152
  title: t('patientTransferRequestCreated', 'Patient transfer request created'),
145
153
  kind: 'success',
146
154
  });
155
+ onSuccess();
147
156
  })
148
157
  .catch((err: Error) => {
149
158
  showSnackbar({
@@ -154,12 +163,11 @@ export default function PatientAdmitOrTransferForm({
154
163
  })
155
164
  .finally(() => {
156
165
  setIsSubmitting(false);
157
- closeWorkspaceWithSavedChanges();
158
166
  wardPatientGroupDetails.mutate();
159
167
  });
160
168
  },
161
169
  [
162
- closeWorkspaceWithSavedChanges,
170
+ onSuccess,
163
171
  createEncounter,
164
172
  dispositionsWithTypeTransfer,
165
173
  emrConfiguration,
@@ -303,7 +311,7 @@ export default function PatientAdmitOrTransferForm({
303
311
  )}
304
312
  </Stack>
305
313
  <ButtonSet className={styles.buttonSet}>
306
- <Button size="xl" kind="secondary" onClick={closeWorkspaceWithSavedChanges}>
314
+ <Button size="xl" kind="secondary" onClick={onCancel}>
307
315
  {t('cancel', 'Cancel')}
308
316
  </Button>
309
317
  <Button
@@ -1,4 +1,4 @@
1
- import React, { useCallback, useEffect, useMemo, useState } from 'react';
1
+ import React, { useCallback, useMemo, useState } from 'react';
2
2
  import classNames from 'classnames';
3
3
  import { Button, ButtonSet, Form, InlineNotification, CheckboxGroup, Checkbox, Stack } from '@carbon/react';
4
4
  import { Controller, useForm } from 'react-hook-form';
@@ -7,19 +7,20 @@ import { zodResolver } from '@hookform/resolvers/zod';
7
7
  import { z } from 'zod';
8
8
  import { showSnackbar, useAppContext } from '@openmrs/esm-framework';
9
9
  import { assignPatientToBed, removePatientFromBed, useCreateEncounter } from '../../ward.resource';
10
- import type { WardPatientWorkspaceProps, WardViewContext } from '../../types';
10
+ import type { WardViewContext } from '../../types';
11
11
  import BedSelector from '../bed-selector.component';
12
12
  import WardPatientIdentifier from '../../ward-patient-card/row-elements/ward-patient-identifier.component';
13
13
  import WardPatientName from '../../ward-patient-card/row-elements/ward-patient-name.component';
14
14
  import styles from './patient-transfer-swap.scss';
15
+ import { type PatientAdmitOrTransferFormProps } from './patient-admit-or-transfer-request-form.component';
15
16
 
16
17
  export default function PatientBedSwapForm({
17
- promptBeforeClosing,
18
- closeWorkspaceWithSavedChanges,
19
18
  wardPatient,
20
19
  relatedTransferPatients = [],
21
- }: WardPatientWorkspaceProps) {
22
- const { patient, visit } = wardPatient;
20
+ onCancel,
21
+ onSuccess
22
+ }: PatientAdmitOrTransferFormProps) {
23
+ const { patient } = wardPatient;
23
24
  const { t } = useTranslation();
24
25
  const [showErrorNotifications, setShowErrorNotifications] = useState(false);
25
26
  const { createEncounter, emrConfiguration, isLoadingEmrConfiguration, errorFetchingEmrConfiguration } =
@@ -47,11 +48,6 @@ export default function PatientBedSwapForm({
47
48
  handleSubmit,
48
49
  } = useForm<FormValues>({ resolver: zodResolver(zodSchema) });
49
50
 
50
- useEffect(() => {
51
- promptBeforeClosing(() => isDirty);
52
- return () => promptBeforeClosing(null);
53
- }, [isDirty, promptBeforeClosing]);
54
-
55
51
  const beds = useMemo(() => wardPatientGroupDetails?.bedLayouts ?? [], [wardPatientGroupDetails]);
56
52
 
57
53
  const onSubmit = useCallback(
@@ -112,6 +108,7 @@ export default function PatientBedSwapForm({
112
108
  }),
113
109
  });
114
110
  }
111
+ onSuccess();
115
112
  }
116
113
  })
117
114
  .catch((error: Error) => {
@@ -124,7 +121,6 @@ export default function PatientBedSwapForm({
124
121
  .finally(() => {
125
122
  setIsSubmitting(false);
126
123
  wardPatientGroupDetails.mutate();
127
- closeWorkspaceWithSavedChanges();
128
124
  });
129
125
  },
130
126
  [
@@ -134,7 +130,7 @@ export default function PatientBedSwapForm({
134
130
  emrConfiguration,
135
131
  t,
136
132
  wardPatientGroupDetails,
137
- closeWorkspaceWithSavedChanges,
133
+ onSuccess,
138
134
  selectedRelatedPatient,
139
135
  relatedTransferPatients,
140
136
  wardPatient,
@@ -222,7 +218,7 @@ export default function PatientBedSwapForm({
222
218
  )}
223
219
  </Stack>
224
220
  <ButtonSet className={styles.buttonSet}>
225
- <Button size="xl" kind="secondary" onClick={closeWorkspaceWithSavedChanges}>
221
+ <Button size="xl" kind="secondary" onClick={onCancel}>
226
222
  {t('cancel', 'Cancel')}
227
223
  </Button>
228
224
  <Button
@@ -1,10 +1,12 @@
1
1
  import { ContentSwitcher, Switch } from '@carbon/react';
2
- import { useFeatureFlag } from '@openmrs/esm-framework';
2
+ import { closeWorkspaceGroup2, useFeatureFlag, Workspace2, Workspace2DefinitionProps } from '@openmrs/esm-framework';
3
3
  import React, { useState } from 'react';
4
4
  import { useTranslation } from 'react-i18next';
5
- import type { WardPatientWorkspaceProps } from '../../types';
5
+ import type { WardPatientWorkspaceDefinition, WardPatientWorkspaceProps } from '../../types';
6
6
  import WardPatientWorkspaceBanner from '../patient-banner/patient-banner.component';
7
- import PatientAdmitOrTransferForm from './patient-admit-or-transfer-request-form.component';
7
+ import PatientAdmitOrTransferForm, {
8
+ type PatientAdmitOrTransferFormProps,
9
+ } from './patient-admit-or-transfer-request-form.component';
8
10
  import PatientBedSwapForm from './patient-bed-swap-form.component';
9
11
  import styles from './patient-transfer-swap.scss';
10
12
 
@@ -19,34 +21,50 @@ type TransferSectionValues = (typeof TransferSection)[keyof typeof TransferSecti
19
21
  * This workspace opens the form to either transfer a patient to a different ward location
20
22
  * or to change their currently assigned bed
21
23
  */
22
- export default function PatientTransferAndSwapWorkspace(props: WardPatientWorkspaceProps) {
24
+ export default function PatientTransferAndSwapWorkspace({
25
+ groupProps: { wardPatient },
26
+ closeWorkspace,
27
+ }: WardPatientWorkspaceDefinition) {
23
28
  const { t } = useTranslation();
24
29
  const [selectedSection, setSelectedSection] = useState<TransferSectionValues>(TransferSection.TRANSFER);
25
30
  const isBedManagementModuleInstalled = useFeatureFlag('bedmanagement-module');
26
31
 
32
+ const props: PatientAdmitOrTransferFormProps = {
33
+ wardPatient,
34
+ onSuccess: async () => {
35
+ await closeWorkspace({ discardUnsavedChanges: true });
36
+ closeWorkspaceGroup2();
37
+ },
38
+ onCancel: () => {
39
+ closeWorkspace();
40
+ },
41
+ };
42
+
27
43
  return (
28
- <div className={styles.flexWrapper}>
29
- <div className={styles.patientWorkspaceBanner}>
30
- <WardPatientWorkspaceBanner wardPatient={props?.wardPatient} />
31
- </div>
32
- {isBedManagementModuleInstalled && (
33
- <div className={styles.contentSwitcherWrapper}>
34
- <h2 className={styles.productiveHeading02}>{t('typeOfTransfer', 'Type of transfer')}</h2>
35
- <div className={styles.contentSwitcher}>
36
- <ContentSwitcher onChange={({ name }) => setSelectedSection(name)}>
37
- <Switch name={TransferSection.TRANSFER} text={t('transfer', 'Transfer')} />
38
- <Switch name={TransferSection.BED_SWAP} text={t('bedSwap', 'Bed swap')} />
39
- </ContentSwitcher>
40
- </div>
44
+ <Workspace2 title={t('transfers', 'Transfers')}>
45
+ <div className={styles.flexWrapper}>
46
+ <div className={styles.patientWorkspaceBanner}>
47
+ <WardPatientWorkspaceBanner wardPatient={wardPatient} />
41
48
  </div>
42
- )}
43
- <div className={styles.workspaceForm}>
44
- {selectedSection === TransferSection.TRANSFER ? (
45
- <PatientAdmitOrTransferForm {...props} />
46
- ) : (
47
- <PatientBedSwapForm {...props} />
49
+ {isBedManagementModuleInstalled && (
50
+ <div className={styles.contentSwitcherWrapper}>
51
+ <h2 className={styles.productiveHeading02}>{t('typeOfTransfer', 'Type of transfer')}</h2>
52
+ <div className={styles.contentSwitcher}>
53
+ <ContentSwitcher onChange={({ name }) => setSelectedSection(name)}>
54
+ <Switch name={TransferSection.TRANSFER} text={t('transfer', 'Transfer')} />
55
+ <Switch name={TransferSection.BED_SWAP} text={t('bedSwap', 'Bed swap')} />
56
+ </ContentSwitcher>
57
+ </div>
58
+ </div>
48
59
  )}
60
+ <div className={styles.workspaceForm}>
61
+ {selectedSection === TransferSection.TRANSFER ? (
62
+ <PatientAdmitOrTransferForm {...props} />
63
+ ) : (
64
+ <PatientBedSwapForm {...props} />
65
+ )}
66
+ </div>
49
67
  </div>
50
- </div>
68
+ </Workspace2>
51
69
  );
52
70
  }
@@ -1,23 +1,37 @@
1
1
  import React from 'react';
2
+ import { closeWorkspaceGroup2, Workspace2, type Workspace2DefinitionProps } from '@openmrs/esm-framework';
2
3
  import { type WardPatientWorkspaceProps } from '../../types';
3
4
  import WardPatientWorkspaceBanner from '../patient-banner/patient-banner.component';
4
5
  import PatientAdmitOrTransferForm from '../patient-transfer-bed-swap/patient-admit-or-transfer-request-form.component';
5
6
  import styles from './patient-transfer-request.scss';
6
-
7
- interface PatientTransferRequestWorkspaceProps extends WardPatientWorkspaceProps {}
7
+ import { useTranslation } from 'react-i18next';
8
8
 
9
9
  /**
10
10
  * This workspace is launched when the "Transfer elsewhere" / "Admit elsewhere"
11
11
  * button on a pending request patient card is clicked on
12
12
  */
13
- const PatientTransferRequestWorkspace: React.FC<PatientTransferRequestWorkspaceProps> = (props) => {
14
- const { wardPatient } = props;
13
+ const PatientTransferRequestWorkspace: React.FC<Workspace2DefinitionProps<WardPatientWorkspaceProps>> = ({
14
+ workspaceProps: { wardPatient },
15
+ closeWorkspace,
16
+ }) => {
17
+ const { t } = useTranslation();
18
+ const isTransfer = wardPatient.inpatientRequest.dispositionType == 'TRANSFER';
15
19
 
16
20
  return (
17
- <div className={styles.patientTransferRequestWorkspace}>
18
- <WardPatientWorkspaceBanner {...{ wardPatient }} />
19
- <PatientAdmitOrTransferForm {...props} />
20
- </div>
21
+ <Workspace2
22
+ title={isTransfer ? t('transferElsewhere', 'Transfer elsewhere') : t('admitElsewhere', 'Admit elsewhere')}>
23
+ <div className={styles.patientTransferRequestWorkspace}>
24
+ <WardPatientWorkspaceBanner {...{ wardPatient }} />
25
+ <PatientAdmitOrTransferForm
26
+ wardPatient={wardPatient}
27
+ onSuccess={async () => {
28
+ await closeWorkspace({ discardUnsavedChanges: true });
29
+ closeWorkspaceGroup2();
30
+ }}
31
+ onCancel={() => closeWorkspace()}
32
+ />
33
+ </div>
34
+ </Workspace2>
21
35
  );
22
36
  };
23
37
 
@@ -0,0 +1,17 @@
1
+ import { ActionMenuButton2, StickyNoteAddIcon } from '@openmrs/esm-framework';
2
+ import React from 'react';
3
+ import { useTranslation } from 'react-i18next';
4
+
5
+ export default function WardPatientNotesActionButton() {
6
+ const { t } = useTranslation();
7
+
8
+ return (
9
+ <ActionMenuButton2
10
+ icon={(props) => <StickyNoteAddIcon {...props} size={16} />}
11
+ label={t('PatientNote', 'Patient Note')}
12
+ workspaceToLaunch={{
13
+ workspaceName: 'ward-patient-notes-workspace',
14
+ }}
15
+ />
16
+ );
17
+ }
@@ -10,7 +10,6 @@
10
10
  display: flex;
11
11
  flex-direction: column;
12
12
  justify-content: space-between;
13
- height: 100%;
14
13
  }
15
14
 
16
15
  .row {
@@ -0,0 +1,134 @@
1
+ import React from 'react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { render, screen } from '@testing-library/react';
4
+ import { createErrorHandler, ResponsiveWrapper, showSnackbar, translateFrom, useSession } from '@openmrs/esm-framework';
5
+ import { savePatientNote, usePatientNotes } from './notes.resource';
6
+ import WardPatientNotesWorkspace from './notes.workspace';
7
+ import { emrConfigurationMock, mockInpatientRequestAlice, mockPatient, mockPatientAlice, mockSession } from '__mocks__';
8
+ import useEmrConfiguration from '../../hooks/useEmrConfiguration';
9
+ import { type WardPatient, type WardPatientWorkspaceDefinition } from '../../types';
10
+
11
+ const mockWardPatientAlice: WardPatient = {
12
+ visit: mockInpatientRequestAlice.visit,
13
+ patient: mockPatientAlice,
14
+ bed: null,
15
+ inpatientAdmission: null,
16
+ inpatientRequest: mockInpatientRequestAlice,
17
+ };
18
+
19
+ const testProps: WardPatientWorkspaceDefinition = {
20
+ groupProps: {
21
+ wardPatient: mockWardPatientAlice,
22
+ },
23
+ closeWorkspace: jest.fn(),
24
+ launchChildWorkspace: jest.fn(),
25
+ workspaceProps: undefined,
26
+ windowProps: undefined,
27
+ workspaceName: '',
28
+ windowName: '',
29
+ isRootWorkspace: false,
30
+ };
31
+
32
+ const mockSavePatientNote = savePatientNote as jest.Mock;
33
+ const mockedShowSnackbar = jest.mocked(showSnackbar);
34
+
35
+ jest.mock('./notes.resource', () => ({
36
+ savePatientNote: jest.fn(),
37
+ usePatientNotes: jest.fn(),
38
+ }));
39
+
40
+ jest.mock('../../hooks/useEmrConfiguration', () => jest.fn());
41
+
42
+ const mockedUseEmrConfiguration = jest.mocked(useEmrConfiguration);
43
+ const mockedUsePatientNotes = jest.mocked(usePatientNotes);
44
+
45
+ mockedUseEmrConfiguration.mockReturnValue({
46
+ emrConfiguration: emrConfigurationMock,
47
+ mutateEmrConfiguration: jest.fn(),
48
+ isLoadingEmrConfiguration: false,
49
+ errorFetchingEmrConfiguration: null,
50
+ });
51
+
52
+ describe('<WardPatientNotesWorkspace>', () => {
53
+ mockedUsePatientNotes.mockReturnValue({
54
+ patientNotes: [],
55
+ errorFetchingPatientNotes: undefined,
56
+ isLoadingPatientNotes: false,
57
+ mutatePatientNotes: jest.fn(),
58
+ });
59
+
60
+ test('renders the visit notes form with all the relevant fields and values', () => {
61
+ renderWardPatientNotesForm();
62
+
63
+ expect(screen.getByRole('textbox', { name: /Write your notes/i })).toBeInTheDocument();
64
+ expect(screen.getByRole('button', { name: /Save/i })).toBeInTheDocument();
65
+ });
66
+
67
+ test('renders a success snackbar upon successfully recording a visit note', async () => {
68
+ const successPayload = {
69
+ encounterProviders: expect.arrayContaining([
70
+ {
71
+ encounterRole: emrConfigurationMock?.clinicianEncounterRole?.uuid,
72
+ provider: undefined,
73
+ },
74
+ ]),
75
+ encounterType: emrConfigurationMock?.inpatientNoteEncounterType?.uuid,
76
+ location: undefined,
77
+ obs: expect.arrayContaining([
78
+ {
79
+ concept: { display: '', uuid: '162169AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' },
80
+ value: 'Sample clinical note',
81
+ },
82
+ ]),
83
+ patient: mockPatientAlice.uuid,
84
+ };
85
+
86
+ mockSavePatientNote.mockResolvedValue({ status: 201, body: 'Condition created' });
87
+
88
+ renderWardPatientNotesForm();
89
+
90
+ const note = screen.getByRole('textbox', { name: /Write your notes/i });
91
+ await userEvent.clear(note);
92
+ await userEvent.type(note, 'Sample clinical note');
93
+ expect(note).toHaveValue('Sample clinical note');
94
+
95
+ const submitButton = screen.getByRole('button', { name: /Save/i });
96
+ await userEvent.click(submitButton);
97
+
98
+ expect(mockSavePatientNote).toHaveBeenCalledTimes(1);
99
+ expect(mockSavePatientNote).toHaveBeenCalledWith(expect.objectContaining(successPayload), new AbortController());
100
+ });
101
+
102
+ test('renders an error snackbar if there was a problem recording a visit note', async () => {
103
+ const error = {
104
+ message: 'Internal Server Error',
105
+ response: {
106
+ status: 500,
107
+ statusText: 'Internal Server Error',
108
+ },
109
+ };
110
+
111
+ mockSavePatientNote.mockRejectedValueOnce(error);
112
+ renderWardPatientNotesForm();
113
+
114
+ const note = screen.getByRole('textbox', { name: /Write your notes/i });
115
+ await userEvent.clear(note);
116
+ await userEvent.type(note, 'Sample clinical note');
117
+ expect(note).toHaveValue('Sample clinical note');
118
+
119
+ const submitButton = screen.getByRole('button', { name: /Save/i });
120
+
121
+ await userEvent.click(submitButton);
122
+
123
+ expect(mockedShowSnackbar).toHaveBeenCalledWith({
124
+ isLowContrast: false,
125
+ kind: 'error',
126
+ subtitle: 'Internal Server Error',
127
+ title: 'Error saving patient note',
128
+ });
129
+ });
130
+ });
131
+
132
+ function renderWardPatientNotesForm() {
133
+ render(<WardPatientNotesWorkspace {...testProps} />);
134
+ }
@@ -1,24 +1,185 @@
1
- import React from 'react';
2
- import { type WardPatientWorkspaceProps } from '../../types';
1
+ import React, { useCallback, useState } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { z } from 'zod';
4
+ import { zodResolver } from '@hookform/resolvers/zod';
5
+ import { Controller, useForm } from 'react-hook-form';
6
+ import { Button, Column, Form, InlineLoading, InlineNotification, Row, Stack, TextArea } from '@carbon/react';
7
+ import {
8
+ closeWorkspaceGroup2,
9
+ ResponsiveWrapper,
10
+ showSnackbar,
11
+ translateFrom,
12
+ useSession,
13
+ Workspace2,
14
+ } from '@openmrs/esm-framework';
15
+ import { moduleName } from '../../constant';
16
+ import { savePatientNote } from './notes.resource';
17
+ import useEmrConfiguration from '../../hooks/useEmrConfiguration';
18
+ import styles from './notes.scss';
19
+ import { type WardPatientWorkspaceDefinition, type EncounterPayload } from '../../types';
3
20
  import WardPatientWorkspaceBanner from '../patient-banner/patient-banner.component';
4
- import PatientNotesForm from './form/notes-form.component';
5
21
  import PatientNotesHistory from './history/notes-container.component';
6
22
 
7
- const WardPatientNotesWorkspace: React.FC<WardPatientWorkspaceProps> = (props) => {
8
- const { wardPatient, ...restWorkspaceProps } = props;
9
- const patientUuid = wardPatient?.patient?.uuid;
23
+ type NotesFormData = z.infer<typeof noteFormSchema>;
10
24
 
11
- const notesFormState = {
12
- patientUuid,
13
- ...restWorkspaceProps,
14
- };
25
+ const noteFormSchema = z.object({
26
+ wardClinicalNote: z.string().refine((val) => val.trim().length > 0, {
27
+ //t('clinicalNoteErrorMessage','Clinical note is required')
28
+ message: translateFrom(moduleName, 'clinicalNoteErrorMessage', 'Clinical note is required'),
29
+ }),
30
+ });
31
+
32
+ const WardPatientNotesWorkspace: React.FC<WardPatientWorkspaceDefinition> = ({
33
+ groupProps: { wardPatient },
34
+ closeWorkspace,
35
+ }) => {
36
+ const patientUuid = wardPatient.patient.uuid;
37
+ const { emrConfiguration, isLoadingEmrConfiguration, errorFetchingEmrConfiguration } = useEmrConfiguration();
38
+ const { t } = useTranslation();
39
+ const session = useSession();
40
+
41
+ const [isSubmitting, setIsSubmitting] = useState(false);
42
+ const [rows, setRows] = useState(0);
43
+
44
+ const {
45
+ control,
46
+ handleSubmit,
47
+ formState: { errors, isDirty },
48
+ } = useForm<NotesFormData>({
49
+ mode: 'onSubmit',
50
+ resolver: zodResolver(noteFormSchema),
51
+ defaultValues: {
52
+ wardClinicalNote: '',
53
+ },
54
+ });
55
+
56
+ const locationUuid = session?.sessionLocation?.uuid;
57
+ const providerUuid = session?.currentProvider?.uuid;
58
+
59
+ const onSubmit = useCallback(
60
+ (data: NotesFormData) => {
61
+ const { wardClinicalNote } = data;
62
+ setIsSubmitting(true);
63
+
64
+ const notePayload: EncounterPayload = {
65
+ patient: patientUuid,
66
+ location: locationUuid,
67
+ encounterType: emrConfiguration?.inpatientNoteEncounterType?.uuid,
68
+ encounterProviders: [
69
+ {
70
+ encounterRole: emrConfiguration?.clinicianEncounterRole?.uuid,
71
+ provider: providerUuid,
72
+ },
73
+ ],
74
+ obs: wardClinicalNote
75
+ ? [
76
+ {
77
+ concept: { uuid: emrConfiguration?.consultFreeTextCommentsConcept.uuid, display: '' },
78
+ value: wardClinicalNote,
79
+ },
80
+ ]
81
+ : [],
82
+ };
83
+
84
+ const abortController = new AbortController();
85
+
86
+ savePatientNote(notePayload, abortController)
87
+ .then(async () => {
88
+ showSnackbar({
89
+ isLowContrast: true,
90
+ kind: 'success',
91
+ subtitle: t('patientNoteNowVisible', 'It should be now visible in the notes history'),
92
+ title: t('visitNoteSaved', 'Patient note saved'),
93
+ });
94
+ await closeWorkspace({ discardUnsavedChanges: true });
95
+ closeWorkspaceGroup2();
96
+ })
97
+ .catch((err) => {
98
+ showSnackbar({
99
+ isLowContrast: false,
100
+ kind: 'error',
101
+ subtitle: err?.message,
102
+ title: t('patientNoteSaveError', 'Error saving patient note'),
103
+ });
104
+ })
105
+ .finally(() => setIsSubmitting(false));
106
+ },
107
+ [
108
+ emrConfiguration?.clinicianEncounterRole?.uuid,
109
+ emrConfiguration?.consultFreeTextCommentsConcept?.uuid,
110
+ emrConfiguration?.inpatientNoteEncounterType?.uuid,
111
+ locationUuid,
112
+ patientUuid,
113
+ providerUuid,
114
+ t,
115
+ closeWorkspace,
116
+ ],
117
+ );
118
+
119
+ const onError = (errors) => console.error(errors);
15
120
 
16
121
  return (
17
- <div>
122
+ <Workspace2 hasUnsavedChanges={isDirty} title={t('inpatientNotesWorkspaceTitle', 'In-patient notes')}>
18
123
  <WardPatientWorkspaceBanner {...{ wardPatient }} />
19
- <PatientNotesForm {...notesFormState} />
124
+ <Form className={styles.form} onSubmit={handleSubmit(onSubmit, onError)}>
125
+ {errorFetchingEmrConfiguration && (
126
+ <div className={styles.formError}>
127
+ <InlineNotification
128
+ kind="error"
129
+ title={t('somePartsOfTheFormDidntLoad', "Some parts of the form didn't load")}
130
+ subtitle={t(
131
+ 'fetchingEmrConfigurationFailed',
132
+ 'Fetching EMR configuration failed. Try refreshing the page or contact your system administrator.',
133
+ )}
134
+ lowContrast
135
+ hideCloseButton
136
+ />
137
+ </div>
138
+ )}
139
+ <Stack className={styles.formContainer} gap={2}>
140
+ <Row className={styles.row}>
141
+ <Column sm={1}>
142
+ <span className={styles.columnLabel}>{t('note', 'Note')}</span>
143
+ </Column>
144
+ <Column sm={3}>
145
+ <Controller
146
+ name="wardClinicalNote"
147
+ control={control}
148
+ render={({ field: { onChange, onBlur, value } }) => (
149
+ <ResponsiveWrapper>
150
+ <TextArea
151
+ id="additionalNote"
152
+ invalid={!!errors.wardClinicalNote}
153
+ invalidText={errors.wardClinicalNote?.message}
154
+ labelText={t('clinicalNoteLabel', 'Write your notes')}
155
+ onBlur={onBlur}
156
+ onChange={(event) => {
157
+ onChange(event);
158
+ const textAreaLineHeight = 24; // This is the default line height for Carbon's TextArea component
159
+ const newRows = Math.ceil(event.target.scrollHeight / textAreaLineHeight);
160
+ setRows(newRows);
161
+ }}
162
+ placeholder={t('wardClinicalNotePlaceholder', 'Write any notes here')}
163
+ rows={rows}
164
+ value={value}
165
+ />
166
+ </ResponsiveWrapper>
167
+ )}
168
+ />
169
+ </Column>
170
+ </Row>
171
+ </Stack>
172
+ <Button
173
+ className={styles.saveButton}
174
+ disabled={isSubmitting || isLoadingEmrConfiguration || errorFetchingEmrConfiguration}
175
+ kind="primary"
176
+ type="submit">
177
+ {isSubmitting ? <InlineLoading description={t('saving', 'Saving...')} /> : <span>{t('save', 'Save')}</span>}
178
+ </Button>
179
+ </Form>
180
+
20
181
  <PatientNotesHistory patientUuid={patientUuid} visitUuid={wardPatient?.visit?.uuid} />
21
- </div>
182
+ </Workspace2>
22
183
  );
23
184
  };
24
185