@kenyaemr/esm-ward-app 7.0.3-pre.88 → 7.0.3-pre.94

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 (146) hide show
  1. package/.turbo/turbo-build.log +24 -16
  2. package/dist/130.js +1 -1
  3. package/dist/130.js.map +1 -1
  4. package/dist/169.js +1 -0
  5. package/dist/169.js.map +1 -0
  6. package/dist/269.js +1 -0
  7. package/dist/269.js.map +1 -0
  8. package/dist/346.js +1 -0
  9. package/dist/346.js.map +1 -0
  10. package/dist/348.js +1 -0
  11. package/dist/348.js.map +1 -0
  12. package/dist/466.js +1 -0
  13. package/dist/466.js.map +1 -0
  14. package/dist/501.js +1 -0
  15. package/dist/501.js.map +1 -0
  16. package/dist/574.js +1 -1
  17. package/dist/577.js +1 -0
  18. package/dist/577.js.map +1 -0
  19. package/dist/659.js +1 -0
  20. package/dist/659.js.map +1 -0
  21. package/dist/749.js +1 -0
  22. package/dist/749.js.map +1 -0
  23. package/dist/76.js +1 -0
  24. package/dist/76.js.map +1 -0
  25. package/dist/767.js +1 -0
  26. package/dist/767.js.map +1 -0
  27. package/dist/793.js +2 -0
  28. package/dist/793.js.map +1 -0
  29. package/dist/803.js +1 -0
  30. package/dist/803.js.map +1 -0
  31. package/dist/940.js +1 -0
  32. package/dist/940.js.map +1 -0
  33. package/dist/960.js +1 -0
  34. package/dist/960.js.map +1 -0
  35. package/dist/kenyaemr-esm-ward-app.js +1 -1
  36. package/dist/kenyaemr-esm-ward-app.js.buildmanifest.json +330 -42
  37. package/dist/kenyaemr-esm-ward-app.js.map +1 -1
  38. package/dist/main.js +1 -1
  39. package/dist/main.js.map +1 -1
  40. package/dist/routes.json +1 -1
  41. package/package.json +2 -2
  42. package/src/action-menu-buttons/transfer-workspace-siderail.component.tsx +27 -0
  43. package/src/beds/empty-bed.component.tsx +1 -1
  44. package/src/beds/empty-bed.scss +6 -6
  45. package/src/beds/occupied-bed.component.tsx +5 -5
  46. package/src/beds/occupied-bed.scss +2 -3
  47. package/src/beds/occupied-bed.test.tsx +37 -21
  48. package/src/beds/unassigned-patient.component.tsx +20 -0
  49. package/src/beds/unassigned-patient.scss +6 -0
  50. package/src/config-schema-admission-request-note.ts +9 -0
  51. package/src/config-schema-extension-colored-obs-tags.ts +91 -0
  52. package/src/config-schema.ts +165 -231
  53. package/src/createDashboardLink.component.tsx +42 -0
  54. package/src/hooks/useAdmissionLocation.ts +12 -7
  55. package/src/hooks/useCurrentWardCardConfig.ts +32 -0
  56. package/src/hooks/useEmrConfiguration.ts +112 -0
  57. package/src/hooks/useInpatientAdmission.ts +28 -0
  58. package/src/hooks/useInpatientRequest.ts +39 -9
  59. package/src/hooks/useLocation.test.ts +38 -0
  60. package/src/hooks/useLocation.ts +9 -0
  61. package/src/hooks/useLocations.ts +54 -0
  62. package/src/hooks/useMostRecentObs.ts +27 -0
  63. package/src/hooks/useRestPatient.ts +18 -0
  64. package/src/hooks/useWardLocation.test.ts +108 -0
  65. package/src/hooks/useWardLocation.ts +26 -0
  66. package/src/index.ts +71 -4
  67. package/src/location-selector/location-selector.component.tsx +118 -0
  68. package/src/location-selector/location-selector.scss +48 -0
  69. package/src/root.component.tsx +2 -1
  70. package/src/routes.json +79 -12
  71. package/src/types/index.ts +87 -46
  72. package/src/ward-patient-card/card-rows/admission-request-note.extension.tsx +27 -0
  73. package/src/ward-patient-card/card-rows/colored-obs-tags-card-row.extension.tsx +13 -0
  74. package/src/ward-patient-card/row-elements/ward-patient-age.tsx +7 -13
  75. package/src/ward-patient-card/row-elements/ward-patient-bed-number.tsx +2 -2
  76. package/src/ward-patient-card/row-elements/ward-patient-coded-obs-tags.tsx +51 -50
  77. package/src/ward-patient-card/row-elements/ward-patient-gender.component.tsx +27 -0
  78. package/src/ward-patient-card/row-elements/ward-patient-header-address.tsx +16 -15
  79. package/src/ward-patient-card/row-elements/ward-patient-identifier.tsx +53 -0
  80. package/src/ward-patient-card/row-elements/ward-patient-name.tsx +7 -7
  81. package/src/ward-patient-card/row-elements/ward-patient-obs.resource.ts +4 -4
  82. package/src/ward-patient-card/row-elements/ward-patient-obs.tsx +45 -44
  83. package/src/ward-patient-card/row-elements/ward-patient-time-on-ward.tsx +22 -0
  84. package/src/ward-patient-card/row-elements/ward-patient-time-since-admission.tsx +22 -0
  85. package/src/ward-patient-card/ward-patient-card-element.component.tsx +65 -0
  86. package/src/ward-patient-card/ward-patient-card.component.tsx +64 -0
  87. package/src/ward-patient-card/ward-patient-card.scss +61 -12
  88. package/src/ward-patient-workspace/ward-patient-action-button.extension.tsx +18 -0
  89. package/src/ward-patient-workspace/ward-patient.style.scss +11 -0
  90. package/src/ward-patient-workspace/ward-patient.workspace.tsx +51 -0
  91. package/src/ward-view/ward-bed.component.tsx +0 -1
  92. package/src/ward-view/ward-view.component.tsx +114 -76
  93. package/src/ward-view/ward-view.resource.ts +2 -2
  94. package/src/ward-view/ward-view.scss +4 -4
  95. package/src/ward-view/ward-view.test.tsx +76 -49
  96. package/src/ward-view-header/admission-requests-bar.component.tsx +29 -28
  97. package/src/ward-view-header/admission-requests-bar.test.tsx +11 -15
  98. package/src/ward-view-header/admission-requests.scss +20 -25
  99. package/src/ward-view-header/ward-view-header.component.tsx +7 -7
  100. package/src/ward-view-header/ward-view-header.scss +2 -2
  101. package/src/ward-workspace/admission-request-card/admission-request-card-actions.component.tsx +29 -0
  102. package/src/ward-workspace/admission-request-card/admission-request-card-header.component.tsx +51 -0
  103. package/src/ward-workspace/admission-request-card/admission-request-card.component.tsx +16 -0
  104. package/src/ward-workspace/admission-request-card/admission-request-card.scss +49 -0
  105. package/src/ward-workspace/admission-request-workspace/admission-requests-workspace.scss +12 -0
  106. package/src/ward-workspace/admission-request-workspace/admission-requests-workspace.test.tsx +48 -0
  107. package/src/ward-workspace/admission-request-workspace/admission-requests.workspace.tsx +61 -0
  108. package/src/ward-workspace/admit-patient-form-workspace/admit-patient-form.scss +35 -0
  109. package/src/ward-workspace/admit-patient-form-workspace/admit-patient-form.test.tsx +341 -0
  110. package/src/ward-workspace/admit-patient-form-workspace/admit-patient-form.workspace.tsx +267 -0
  111. package/src/ward-workspace/admit-patient-form-workspace/types.ts +7 -0
  112. package/src/ward-workspace/patient-banner/patient-banner.component.tsx +29 -0
  113. package/src/ward-workspace/patient-banner/style.scss +23 -0
  114. package/src/ward-workspace/patient-transfer-bed-swap/patient-bed-swap-form.component.tsx +210 -0
  115. package/src/ward-workspace/patient-transfer-bed-swap/patient-transfer-request-form.component.tsx +238 -0
  116. package/src/ward-workspace/patient-transfer-bed-swap/patient-transfer-swap.scss +73 -0
  117. package/src/ward-workspace/patient-transfer-bed-swap/patient-transfer-swap.workspace.tsx +44 -0
  118. package/src/ward-workspace/ward-patient-notes/form/notes-form.component.tsx +180 -0
  119. package/src/ward-workspace/ward-patient-notes/form/notes-form.scss +30 -0
  120. package/src/ward-workspace/ward-patient-notes/form/notes-form.test.tsx +116 -0
  121. package/src/ward-workspace/ward-patient-notes/history/note.component.tsx +53 -0
  122. package/src/ward-workspace/ward-patient-notes/history/notes-container.component.tsx +55 -0
  123. package/src/ward-workspace/ward-patient-notes/history/notes-container.test.tsx +84 -0
  124. package/src/ward-workspace/ward-patient-notes/history/styles.scss +61 -0
  125. package/src/ward-workspace/ward-patient-notes/notes-action-button.extension.tsx +18 -0
  126. package/src/ward-workspace/ward-patient-notes/notes.resource.ts +71 -0
  127. package/src/ward-workspace/ward-patient-notes/notes.workspace.tsx +25 -0
  128. package/src/ward-workspace/ward-patient-notes/types.ts +44 -0
  129. package/src/ward.resource.ts +25 -0
  130. package/translations/en.json +63 -2
  131. package/dist/443.js +0 -1
  132. package/dist/443.js.map +0 -1
  133. package/dist/589.js +0 -1
  134. package/dist/589.js.map +0 -1
  135. package/dist/695.js +0 -2
  136. package/dist/695.js.map +0 -1
  137. package/src/hooks/useAdmittedPatients.ts +0 -13
  138. package/src/ward-patient-card/row-elements/row-elements.scss +0 -16
  139. package/src/ward-patient-card/ward-patient-card-row.resources.tsx +0 -92
  140. package/src/ward-patient-card/ward-patient-card.tsx +0 -20
  141. package/src/ward-workspace/admission-request-card.component.tsx +0 -23
  142. package/src/ward-workspace/admission-request-card.scss +0 -34
  143. package/src/ward-workspace/admission-request-workspace.test.tsx +0 -38
  144. package/src/ward-workspace/admission-requests-workspace.component.tsx +0 -21
  145. package/src/ward-workspace/admission-requests-workspace.scss +0 -13
  146. /package/dist/{695.js.LICENSE.txt → 793.js.LICENSE.txt} +0 -0
@@ -0,0 +1,238 @@
1
+ import React, { useCallback, useEffect, useMemo, useState } from 'react';
2
+ import { ResponsiveWrapper, showSnackbar, useSession } from '@openmrs/esm-framework';
3
+ import styles from './patient-transfer-swap.scss';
4
+ import { useTranslation } from 'react-i18next';
5
+ import { useAdmissionLocation } from '../../hooks/useAdmissionLocation';
6
+ import { z } from 'zod';
7
+ import { Controller, useForm } from 'react-hook-form';
8
+ import { zodResolver } from '@hookform/resolvers/zod';
9
+ import LocationSelector from '../../location-selector/location-selector.component';
10
+ import useEmrConfiguration from '../../hooks/useEmrConfiguration';
11
+ import { createEncounter } from '../../ward.resource';
12
+ import useWardLocation from '../../hooks/useWardLocation';
13
+ import type { ObsPayload, WardPatientWorkspaceProps } from '../../types';
14
+ import { useInpatientRequest } from '../../hooks/useInpatientRequest';
15
+ import { Form, ButtonSet, Button, TextArea, InlineNotification, RadioButtonGroup, RadioButton } from '@carbon/react';
16
+
17
+ export default function PatientTransferForm({
18
+ closeWorkspaceWithSavedChanges,
19
+ wardPatient,
20
+ promptBeforeClosing,
21
+ }: WardPatientWorkspaceProps) {
22
+ const { patient } = wardPatient ?? {};
23
+ const { t } = useTranslation();
24
+ const [showErrorNotifications, setShowErrorNotifications] = useState(false);
25
+ const [isSubmitting, setIsSubmitting] = useState(false);
26
+ const { emrConfiguration, isLoadingEmrConfiguration, errorFetchingEmrConfiguration } = useEmrConfiguration();
27
+ const { currentProvider } = useSession();
28
+ const { location } = useWardLocation();
29
+ const dispositionsWithTypeTransfer = useMemo(
30
+ () => emrConfiguration?.dispositions.filter(({ type }) => type === 'TRANSFER'),
31
+ [emrConfiguration],
32
+ );
33
+ const { mutate: mutateAdmissionLocation } = useAdmissionLocation();
34
+ const { mutate: mutateInpatientRequest } = useInpatientRequest();
35
+
36
+ const zodSchema = useMemo(
37
+ () =>
38
+ z.object({
39
+ location: z.string({
40
+ required_error: t('pleaseSelectTransferLocation', 'Please select transfer location'),
41
+ }),
42
+ note: z.string().optional(),
43
+ transferType:
44
+ dispositionsWithTypeTransfer?.length > 1
45
+ ? z.string({
46
+ required_error: t('pleaseSelectTransferType', 'Please select transfer type'),
47
+ })
48
+ : z.string().optional(),
49
+ }),
50
+ [t, dispositionsWithTypeTransfer],
51
+ );
52
+
53
+ type FormValues = z.infer<typeof zodSchema>;
54
+
55
+ const formDefaultValues: Partial<FormValues> = useMemo(() => {
56
+ const defaultValues: FormValues = {};
57
+ if (dispositionsWithTypeTransfer?.length === 1) {
58
+ defaultValues.transferType = dispositionsWithTypeTransfer[0].uuid;
59
+ }
60
+ return defaultValues;
61
+ }, [dispositionsWithTypeTransfer]);
62
+
63
+ const {
64
+ formState: { errors, isDirty },
65
+ control,
66
+ handleSubmit,
67
+ getValues,
68
+ setValue,
69
+ } = useForm<FormValues>({ resolver: zodResolver(zodSchema), defaultValues: formDefaultValues });
70
+
71
+ useEffect(() => {
72
+ if (dispositionsWithTypeTransfer?.length === 1) {
73
+ setValue('transferType', dispositionsWithTypeTransfer[0].uuid);
74
+ }
75
+ }, [dispositionsWithTypeTransfer]);
76
+
77
+ useEffect(() => {
78
+ promptBeforeClosing(() => isDirty);
79
+ return () => promptBeforeClosing(null);
80
+ }, [isDirty]);
81
+
82
+ const onSubmit = useCallback(
83
+ (values: FormValues) => {
84
+ setIsSubmitting(true);
85
+ setShowErrorNotifications(false);
86
+ const obs: Array<ObsPayload> = [
87
+ {
88
+ concept: emrConfiguration.dispositionDescriptor.internalTransferLocationConcept.uuid,
89
+ value: values.location,
90
+ },
91
+ {
92
+ concept: emrConfiguration.dispositionDescriptor.dispositionConcept.uuid,
93
+ value: dispositionsWithTypeTransfer.find(({ uuid }) => uuid === values.transferType)?.conceptCode,
94
+ },
95
+ ];
96
+
97
+ if (values.note) {
98
+ obs.push({
99
+ concept: emrConfiguration.consultFreeTextCommentsConcept.uuid,
100
+ value: values.note,
101
+ });
102
+ }
103
+
104
+ createEncounter({
105
+ patient: patient.uuid,
106
+ encounterType: emrConfiguration.visitNoteEncounterType.uuid,
107
+ location: location.uuid,
108
+ encounterProviders: [
109
+ {
110
+ encounterRole: emrConfiguration.clinicianEncounterRole.uuid,
111
+ provider: currentProvider?.uuid,
112
+ },
113
+ ],
114
+ obs: [
115
+ {
116
+ concept: emrConfiguration.dispositionDescriptor.dispositionSetConcept.uuid,
117
+ groupMembers: obs,
118
+ },
119
+ ],
120
+ })
121
+ .then(() => {
122
+ showSnackbar({
123
+ title: t('patientTransferRequestCreated', 'Patient transfer request created'),
124
+ kind: 'success',
125
+ });
126
+ closeWorkspaceWithSavedChanges();
127
+ mutateAdmissionLocation();
128
+ mutateInpatientRequest();
129
+ })
130
+ .catch((err: Error) => {
131
+ showSnackbar({
132
+ title: t('errorCreatingTransferRequest', 'Error creating transfer request'),
133
+ subtitle: err.message,
134
+ kind: 'error',
135
+ });
136
+ })
137
+ .finally(() => setIsSubmitting(false));
138
+ },
139
+ [
140
+ setShowErrorNotifications,
141
+ currentProvider,
142
+ location,
143
+ emrConfiguration,
144
+ patient.uuid,
145
+ dispositionsWithTypeTransfer,
146
+ mutateAdmissionLocation,
147
+ mutateInpatientRequest,
148
+ ],
149
+ );
150
+
151
+ const onError = useCallback(() => {
152
+ setIsSubmitting(false);
153
+ setShowErrorNotifications(true);
154
+ }, []);
155
+
156
+ return (
157
+ <Form onSubmit={handleSubmit(onSubmit, onError)} className={styles.formContainer}>
158
+ <div>
159
+ {errorFetchingEmrConfiguration && (
160
+ <div className={styles.formError}>
161
+ <InlineNotification
162
+ kind="error"
163
+ title={t('somePartsOfTheFormDidntLoad', "Some parts of the form didn't load")}
164
+ subtitle={t(
165
+ 'fetchingEmrConfigurationFailed',
166
+ 'Fetching EMR configuration failed. Try refreshing the page or contact your system administrator.',
167
+ )}
168
+ lowContrast
169
+ hideCloseButton
170
+ />
171
+ </div>
172
+ )}
173
+ <div className={styles.field}>
174
+ <h2 className={styles.productiveHeading02}>{t('selectALocation', 'Select a location')}</h2>
175
+ <Controller
176
+ name="location"
177
+ control={control}
178
+ render={({ field, fieldState: { error } }) => (
179
+ <LocationSelector {...field} invalid={!!error?.message} invalidText={error?.message} />
180
+ )}
181
+ />
182
+ </div>
183
+ {dispositionsWithTypeTransfer?.length > 1 && (
184
+ <div className={styles.field}>
185
+ <h2 className={styles.productiveHeading02}>{t('transferType', 'Transfer type')}</h2>
186
+ <Controller
187
+ name="transferType"
188
+ control={control}
189
+ render={({ field, fieldState: { error } }) => (
190
+ <ResponsiveWrapper>
191
+ <RadioButtonGroup
192
+ orientation="vertical"
193
+ {...field}
194
+ invalid={!!error?.message}
195
+ invalidText={error?.message}>
196
+ {dispositionsWithTypeTransfer.map((disposition) => (
197
+ <RadioButton id={disposition.uuid} labelText={disposition.name} value={disposition.uuid} />
198
+ ))}
199
+ </RadioButtonGroup>
200
+ </ResponsiveWrapper>
201
+ )}
202
+ />
203
+ </div>
204
+ )}
205
+ <div className={styles.field}>
206
+ <h2 className={styles.productiveHeading02}>{t('notes', 'Notes')}</h2>
207
+ <Controller
208
+ name="note"
209
+ control={control}
210
+ render={({ field, fieldState: { error } }) => (
211
+ <ResponsiveWrapper>
212
+ <TextArea {...field} invalid={!!error?.message} invalidText={error?.message} />
213
+ </ResponsiveWrapper>
214
+ )}
215
+ />
216
+ </div>
217
+ {showErrorNotifications && (
218
+ <div className={styles.notifications}>
219
+ {Object.values(errors).map((error) => (
220
+ <InlineNotification lowContrast subtitle={error?.message} hideCloseButton />
221
+ ))}
222
+ </div>
223
+ )}
224
+ </div>
225
+ <ButtonSet className={styles.buttonSet}>
226
+ <Button size="xl" kind="secondary" onClick={closeWorkspaceWithSavedChanges}>
227
+ {t('cancel', 'Cancel')}
228
+ </Button>
229
+ <Button
230
+ type="submit"
231
+ size="xl"
232
+ disabled={isLoadingEmrConfiguration || isSubmitting || errorFetchingEmrConfiguration}>
233
+ {t('save', 'Save')}
234
+ </Button>
235
+ </ButtonSet>
236
+ </Form>
237
+ );
238
+ }
@@ -0,0 +1,73 @@
1
+ @use '@carbon/type';
2
+ @use '@carbon/layout';
3
+ @use '@openmrs/esm-styleguide/src/vars' as *;
4
+
5
+ .workspaceContent {
6
+ padding: layout.$spacing-05;
7
+ height: 100%;
8
+ display: flex;
9
+ flex-direction: column;
10
+ color: #393939;
11
+ }
12
+
13
+ .patientWorkspaceBanner {
14
+ margin: (-(layout.$spacing-05)) (-(layout.$spacing-05)) layout.$spacing-05 (-(layout.$spacing-05));
15
+ }
16
+
17
+ .field {
18
+ margin-bottom: layout.$spacing-05;
19
+ & > h2 {
20
+ margin-bottom: layout.$spacing-03;
21
+ }
22
+ }
23
+
24
+ .contentSwitcher {
25
+ margin-top: layout.$spacing-03;
26
+ }
27
+
28
+ .workspaceForm {
29
+ margin-top: layout.$spacing-05;
30
+ flex: 1;
31
+ }
32
+
33
+ .productiveHeading02 {
34
+ @include type.type-style('heading-compact-02');
35
+ }
36
+
37
+ .formContainer {
38
+ display: flex;
39
+ flex-direction: column;
40
+ justify-content: space-between;
41
+ height: 100%;
42
+ }
43
+
44
+ .buttonSet {
45
+ display: flex;
46
+ align-items: center;
47
+ margin: 0 (-(layout.$spacing-05)) (-(layout.$spacing-05)) (-(layout.$spacing-05));
48
+
49
+ button {
50
+ max-width: unset !important;
51
+ width: 50% !important;
52
+
53
+ > svg {
54
+ fill: currentColor !important;
55
+ }
56
+ }
57
+ }
58
+
59
+ .radioButtonGroup {
60
+ margin-top: layout.$spacing-03;
61
+ fieldset {
62
+ flex-direction: column;
63
+ align-items: flex-start;
64
+
65
+ :global(.cds--radio-button-wrapper) {
66
+ margin-bottom: layout.$spacing-04;
67
+ }
68
+ }
69
+ }
70
+
71
+ .notifications {
72
+ margin-top: layout.$spacing-05;
73
+ }
@@ -0,0 +1,44 @@
1
+ import React, { useState } from 'react';
2
+ import { useFeatureFlag } from '@openmrs/esm-framework';
3
+ import { ContentSwitcher, Switch } from '@carbon/react';
4
+ import { useTranslation } from 'react-i18next';
5
+ import PatientTransferForm from './patient-transfer-request-form.component';
6
+ import PatientBedSwapForm from './patient-bed-swap-form.component';
7
+ import styles from './patient-transfer-swap.scss';
8
+ import WardPatientWorkspaceBanner from '../patient-banner/patient-banner.component';
9
+ import type { WardPatientWorkspaceProps } from '../../types';
10
+
11
+ const TransferSection = {
12
+ TRANSFER: 'transfer',
13
+ BED_SWAP: 'bed-swap',
14
+ } as const;
15
+
16
+ type TransferSectionValues = (typeof TransferSection)[keyof typeof TransferSection];
17
+
18
+ export default function PatientTransferAndSwapWorkspace(props: WardPatientWorkspaceProps) {
19
+ const { t } = useTranslation();
20
+ const [selectedSection, setSelectedSection] = useState<TransferSectionValues>(TransferSection.TRANSFER);
21
+ const isBedManagementModuleInstalled = useFeatureFlag('bedmanagement-module');
22
+
23
+ return (
24
+ <div className={styles.workspaceContent}>
25
+ <div className={styles.patientWorkspaceBanner}>
26
+ <WardPatientWorkspaceBanner {...props?.wardPatient} />
27
+ </div>
28
+ {isBedManagementModuleInstalled && (
29
+ <div>
30
+ <h2 className={styles.productiveHeading02}>{t('typeOfTransfer', 'Type of transfer')}</h2>
31
+ <div className={styles.contentSwitcher}>
32
+ <ContentSwitcher onChange={({ name }) => setSelectedSection(name)}>
33
+ <Switch name={TransferSection.TRANSFER} text={t('transfer', 'Transfer')} />
34
+ <Switch name={TransferSection.BED_SWAP} text={t('bedSwap', 'Bed swap')} />
35
+ </ContentSwitcher>
36
+ </div>
37
+ </div>
38
+ )}
39
+ <div className={styles.workspaceForm}>
40
+ {selectedSection === 'transfer' ? <PatientTransferForm {...props} /> : <PatientBedSwapForm {...props} />}
41
+ </div>
42
+ </div>
43
+ );
44
+ }
@@ -0,0 +1,180 @@
1
+ import React, { useCallback, useEffect, 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
+ createErrorHandler,
9
+ type DefaultWorkspaceProps,
10
+ type PatientUuid,
11
+ ResponsiveWrapper,
12
+ showSnackbar,
13
+ translateFrom,
14
+ useSession,
15
+ } from '@openmrs/esm-framework';
16
+ import { savePatientNote } from '../notes.resource';
17
+ import styles from './notes-form.scss';
18
+ import { moduleName } from '../../../constant';
19
+ import useEmrConfiguration from '../../../hooks/useEmrConfiguration';
20
+
21
+ type NotesFormData = z.infer<typeof noteFormSchema>;
22
+
23
+ const noteFormSchema = z.object({
24
+ wardClinicalNote: z.string().refine((val) => val.trim().length > 0, {
25
+ //t('clinicalNoteErrorMessage','Clinical note is required')
26
+ message: translateFrom(moduleName, 'clinicalNoteErrorMessage', 'Clinical note is required'),
27
+ }),
28
+ });
29
+
30
+ interface PatientNotesFormProps extends DefaultWorkspaceProps {
31
+ patientUuid: PatientUuid;
32
+ }
33
+
34
+ const PatientNotesForm: React.FC<PatientNotesFormProps> = ({
35
+ closeWorkspaceWithSavedChanges,
36
+ patientUuid,
37
+ promptBeforeClosing,
38
+ }) => {
39
+ const { emrConfiguration, isLoadingEmrConfiguration, errorFetchingEmrConfiguration } = useEmrConfiguration();
40
+ const { t } = useTranslation();
41
+ const session = useSession();
42
+ const [isSubmitting, setIsSubmitting] = useState(false);
43
+ const [rows, setRows] = useState<number>();
44
+
45
+ const {
46
+ control,
47
+ handleSubmit,
48
+ formState: { errors, isDirty },
49
+ } = useForm<NotesFormData>({
50
+ mode: 'onSubmit',
51
+ resolver: zodResolver(noteFormSchema),
52
+ defaultValues: {
53
+ wardClinicalNote: '',
54
+ },
55
+ });
56
+
57
+ useEffect(() => {
58
+ promptBeforeClosing(() => isDirty);
59
+ }, [isDirty, promptBeforeClosing]);
60
+
61
+ const locationUuid = session?.sessionLocation?.uuid;
62
+ const providerUuid = session?.currentProvider?.uuid;
63
+
64
+ const onSubmit = useCallback(
65
+ (data: NotesFormData) => {
66
+ const { wardClinicalNote } = data;
67
+ setIsSubmitting(true);
68
+
69
+ const notePayload = {
70
+ patient: patientUuid,
71
+ location: locationUuid,
72
+ encounterType: emrConfiguration?.visitNoteEncounterType.uuid,
73
+ encounterProviders: [
74
+ {
75
+ encounterRole: emrConfiguration?.clinicianEncounterRole.uuid,
76
+ provider: providerUuid,
77
+ },
78
+ ],
79
+ obs: wardClinicalNote
80
+ ? [
81
+ {
82
+ concept: { uuid: emrConfiguration?.consultFreeTextCommentsConcept.uuid, display: '' },
83
+ value: wardClinicalNote,
84
+ },
85
+ ]
86
+ : [],
87
+ };
88
+
89
+ const abortController = new AbortController();
90
+
91
+ savePatientNote(notePayload, abortController)
92
+ .then(() => {
93
+ closeWorkspaceWithSavedChanges();
94
+ showSnackbar({
95
+ isLowContrast: true,
96
+ subtitle: t('patientNoteNowVisible', 'It should be now visible in the notes history'),
97
+ kind: 'success',
98
+ title: t('visitNoteSaved', 'Patient note saved'),
99
+ });
100
+ })
101
+ .catch((err) => {
102
+ createErrorHandler();
103
+
104
+ showSnackbar({
105
+ title: t('patientNoteSaveError', 'Error saving patient note'),
106
+ kind: 'error',
107
+ isLowContrast: false,
108
+ subtitle: err?.message,
109
+ });
110
+ })
111
+ .finally(() => {
112
+ setIsSubmitting(false);
113
+ });
114
+ },
115
+ [emrConfiguration, patientUuid, locationUuid, providerUuid],
116
+ );
117
+
118
+ const onError = (errors) => console.error(errors);
119
+
120
+ return (
121
+ <Form className={styles.form} onSubmit={handleSubmit(onSubmit, onError)}>
122
+ {errorFetchingEmrConfiguration && (
123
+ <div className={styles.formError}>
124
+ <InlineNotification
125
+ kind="error"
126
+ title={t('somePartsOfTheFormDidntLoad', "Some parts of the form didn't load")}
127
+ subtitle={t(
128
+ 'fetchingEmrConfigurationFailed',
129
+ 'Fetching EMR configuration failed. Try refreshing the page or contact your system administrator.',
130
+ )}
131
+ lowContrast
132
+ hideCloseButton
133
+ />
134
+ </div>
135
+ )}
136
+ <Stack className={styles.formContainer} gap={2}>
137
+ <Row className={styles.row}>
138
+ <Column sm={1}>
139
+ <span className={styles.columnLabel}>{t('note', 'Note')}</span>
140
+ </Column>
141
+ <Column sm={3}>
142
+ <Controller
143
+ name="wardClinicalNote"
144
+ control={control}
145
+ render={({ field: { onChange, onBlur, value } }) => (
146
+ <ResponsiveWrapper>
147
+ <TextArea
148
+ id="additionalNote"
149
+ rows={rows}
150
+ labelText={t('clinicalNoteLabel', 'Write your notes')}
151
+ placeholder={t('wardClinicalNotePlaceholder', 'Write any notes here')}
152
+ value={value}
153
+ onBlur={onBlur}
154
+ invalid={!!errors.wardClinicalNote}
155
+ invalidText={errors.wardClinicalNote?.message}
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
+ />
163
+ </ResponsiveWrapper>
164
+ )}
165
+ />
166
+ </Column>
167
+ </Row>
168
+ </Stack>
169
+ <Button
170
+ kind="primary"
171
+ className={styles.saveButton}
172
+ disabled={isSubmitting || isLoadingEmrConfiguration || errorFetchingEmrConfiguration}
173
+ type="submit">
174
+ {isSubmitting ? <InlineLoading description={t('saving', 'Saving...')} /> : <span>{t('save', 'Save')}</span>}
175
+ </Button>
176
+ </Form>
177
+ );
178
+ };
179
+
180
+ export default PatientNotesForm;
@@ -0,0 +1,30 @@
1
+ @use '@carbon/colors';
2
+ @use '@carbon/layout';
3
+ @use '@carbon/type';
4
+
5
+ .formContainer {
6
+ margin: layout.$spacing-05;
7
+ }
8
+
9
+ .form {
10
+ display: flex;
11
+ flex-direction: column;
12
+ justify-content: space-between;
13
+ height: 100%;
14
+ }
15
+
16
+ .row {
17
+ margin: layout.$spacing-05 0 0;
18
+ }
19
+
20
+ .columnLabel {
21
+ @include type.type-style('heading-compact-02');
22
+ color: colors.$gray-70;
23
+ }
24
+
25
+ .saveButton {
26
+ margin-left: layout.$spacing-04;
27
+ display: flex;
28
+ align-content: flex-start;
29
+ align-items: baseline;
30
+ }
@@ -0,0 +1,116 @@
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 } from '../notes.resource';
6
+ import PatientNotesForm from './notes-form.component';
7
+ import { emrConfigurationMock, mockPatient, mockSession } from '__mocks__';
8
+ import useEmrConfiguration from '../../../hooks/useEmrConfiguration';
9
+
10
+ const testProps = {
11
+ patientUuid: mockPatient.uuid,
12
+ closeWorkspace: jest.fn(),
13
+ closeWorkspaceWithSavedChanges: jest.fn(),
14
+ promptBeforeClosing: jest.fn(),
15
+ setTitle: jest.fn(),
16
+ onWorkspaceClose: jest.fn(),
17
+ setOnCloseCallback: jest.fn(),
18
+ };
19
+
20
+ const mockSavePatientNote = savePatientNote as jest.Mock;
21
+ const mockedShowSnackbar = jest.mocked(showSnackbar);
22
+ const mockedCreateErrorHandler = jest.mocked(createErrorHandler);
23
+ const mockedTranslateFrom = jest.mocked(translateFrom);
24
+ const mockedResponsiveWrapper = jest.mocked(ResponsiveWrapper);
25
+ const mockedUseSession = jest.mocked(useSession);
26
+
27
+ jest.mock('../notes.resource', () => ({
28
+ savePatientNote: jest.fn(),
29
+ }));
30
+
31
+ jest.mock('../../../hooks/useEmrConfiguration', () => jest.fn());
32
+
33
+ const mockedUseEmrConfiguration = jest.mocked(useEmrConfiguration);
34
+
35
+ mockedUseEmrConfiguration.mockReturnValue({
36
+ emrConfiguration: emrConfigurationMock,
37
+ mutateEmrConfiguration: jest.fn(),
38
+ isLoadingEmrConfiguration: false,
39
+ errorFetchingEmrConfiguration: null,
40
+ });
41
+
42
+ test('renders the visit notes form with all the relevant fields and values', () => {
43
+ renderWardPatientNotesForm();
44
+
45
+ expect(screen.getByRole('textbox', { name: /Write your notes/i })).toBeInTheDocument();
46
+ expect(screen.getByRole('button', { name: /Save/i })).toBeInTheDocument();
47
+ });
48
+
49
+ test('renders a success snackbar upon successfully recording a visit note', async () => {
50
+ const successPayload = {
51
+ encounterProviders: expect.arrayContaining([
52
+ {
53
+ encounterRole: emrConfigurationMock.clinicianEncounterRole.uuid,
54
+ provider: undefined,
55
+ },
56
+ ]),
57
+ encounterType: emrConfigurationMock.visitNoteEncounterType.uuid,
58
+ location: undefined,
59
+ obs: expect.arrayContaining([
60
+ {
61
+ concept: { display: '', uuid: '162169AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' },
62
+ value: 'Sample clinical note',
63
+ },
64
+ ]),
65
+ patient: mockPatient.uuid,
66
+ };
67
+
68
+ mockSavePatientNote.mockResolvedValue({ status: 201, body: 'Condition created' });
69
+
70
+ renderWardPatientNotesForm();
71
+
72
+ const note = screen.getByRole('textbox', { name: /Write your notes/i });
73
+ await userEvent.clear(note);
74
+ await userEvent.type(note, 'Sample clinical note');
75
+ expect(note).toHaveValue('Sample clinical note');
76
+
77
+ const submitButton = screen.getByRole('button', { name: /Save/i });
78
+ await userEvent.click(submitButton);
79
+
80
+ expect(mockSavePatientNote).toHaveBeenCalledTimes(1);
81
+ expect(mockSavePatientNote).toHaveBeenCalledWith(expect.objectContaining(successPayload), new AbortController());
82
+ });
83
+
84
+ test('renders an error snackbar if there was a problem recording a visit note', async () => {
85
+ const error = {
86
+ message: 'Internal Server Error',
87
+ response: {
88
+ status: 500,
89
+ statusText: 'Internal Server Error',
90
+ },
91
+ };
92
+
93
+ mockSavePatientNote.mockRejectedValueOnce(error);
94
+ renderWardPatientNotesForm();
95
+
96
+ const note = screen.getByRole('textbox', { name: /Write your notes/i });
97
+ await userEvent.clear(note);
98
+ await userEvent.type(note, 'Sample clinical note');
99
+ expect(note).toHaveValue('Sample clinical note');
100
+
101
+ const submitButton = screen.getByRole('button', { name: /Save/i });
102
+
103
+ await userEvent.click(submitButton);
104
+
105
+ expect(mockedShowSnackbar).toHaveBeenCalledWith({
106
+ isLowContrast: false,
107
+ kind: 'error',
108
+ subtitle: 'Internal Server Error',
109
+ title: 'Error saving patient note',
110
+ });
111
+ });
112
+
113
+ function renderWardPatientNotesForm() {
114
+ mockedUseSession.mockReturnValue(mockSession);
115
+ render(<PatientNotesForm {...testProps} />);
116
+ }