@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.
- package/.turbo/turbo-build.log +24 -16
- package/dist/130.js +1 -1
- package/dist/130.js.map +1 -1
- package/dist/169.js +1 -0
- package/dist/169.js.map +1 -0
- package/dist/269.js +1 -0
- package/dist/269.js.map +1 -0
- package/dist/346.js +1 -0
- package/dist/346.js.map +1 -0
- package/dist/348.js +1 -0
- package/dist/348.js.map +1 -0
- package/dist/466.js +1 -0
- package/dist/466.js.map +1 -0
- package/dist/501.js +1 -0
- package/dist/501.js.map +1 -0
- package/dist/574.js +1 -1
- package/dist/577.js +1 -0
- package/dist/577.js.map +1 -0
- package/dist/659.js +1 -0
- package/dist/659.js.map +1 -0
- package/dist/749.js +1 -0
- package/dist/749.js.map +1 -0
- package/dist/76.js +1 -0
- package/dist/76.js.map +1 -0
- package/dist/767.js +1 -0
- package/dist/767.js.map +1 -0
- package/dist/793.js +2 -0
- package/dist/793.js.map +1 -0
- package/dist/803.js +1 -0
- package/dist/803.js.map +1 -0
- package/dist/940.js +1 -0
- package/dist/940.js.map +1 -0
- package/dist/960.js +1 -0
- package/dist/960.js.map +1 -0
- package/dist/kenyaemr-esm-ward-app.js +1 -1
- package/dist/kenyaemr-esm-ward-app.js.buildmanifest.json +330 -42
- package/dist/kenyaemr-esm-ward-app.js.map +1 -1
- package/dist/main.js +1 -1
- package/dist/main.js.map +1 -1
- package/dist/routes.json +1 -1
- package/package.json +2 -2
- package/src/action-menu-buttons/transfer-workspace-siderail.component.tsx +27 -0
- package/src/beds/empty-bed.component.tsx +1 -1
- package/src/beds/empty-bed.scss +6 -6
- package/src/beds/occupied-bed.component.tsx +5 -5
- package/src/beds/occupied-bed.scss +2 -3
- package/src/beds/occupied-bed.test.tsx +37 -21
- package/src/beds/unassigned-patient.component.tsx +20 -0
- package/src/beds/unassigned-patient.scss +6 -0
- package/src/config-schema-admission-request-note.ts +9 -0
- package/src/config-schema-extension-colored-obs-tags.ts +91 -0
- package/src/config-schema.ts +165 -231
- package/src/createDashboardLink.component.tsx +42 -0
- package/src/hooks/useAdmissionLocation.ts +12 -7
- package/src/hooks/useCurrentWardCardConfig.ts +32 -0
- package/src/hooks/useEmrConfiguration.ts +112 -0
- package/src/hooks/useInpatientAdmission.ts +28 -0
- package/src/hooks/useInpatientRequest.ts +39 -9
- package/src/hooks/useLocation.test.ts +38 -0
- package/src/hooks/useLocation.ts +9 -0
- package/src/hooks/useLocations.ts +54 -0
- package/src/hooks/useMostRecentObs.ts +27 -0
- package/src/hooks/useRestPatient.ts +18 -0
- package/src/hooks/useWardLocation.test.ts +108 -0
- package/src/hooks/useWardLocation.ts +26 -0
- package/src/index.ts +71 -4
- package/src/location-selector/location-selector.component.tsx +118 -0
- package/src/location-selector/location-selector.scss +48 -0
- package/src/root.component.tsx +2 -1
- package/src/routes.json +79 -12
- package/src/types/index.ts +87 -46
- package/src/ward-patient-card/card-rows/admission-request-note.extension.tsx +27 -0
- package/src/ward-patient-card/card-rows/colored-obs-tags-card-row.extension.tsx +13 -0
- package/src/ward-patient-card/row-elements/ward-patient-age.tsx +7 -13
- package/src/ward-patient-card/row-elements/ward-patient-bed-number.tsx +2 -2
- package/src/ward-patient-card/row-elements/ward-patient-coded-obs-tags.tsx +51 -50
- package/src/ward-patient-card/row-elements/ward-patient-gender.component.tsx +27 -0
- package/src/ward-patient-card/row-elements/ward-patient-header-address.tsx +16 -15
- package/src/ward-patient-card/row-elements/ward-patient-identifier.tsx +53 -0
- package/src/ward-patient-card/row-elements/ward-patient-name.tsx +7 -7
- package/src/ward-patient-card/row-elements/ward-patient-obs.resource.ts +4 -4
- package/src/ward-patient-card/row-elements/ward-patient-obs.tsx +45 -44
- package/src/ward-patient-card/row-elements/ward-patient-time-on-ward.tsx +22 -0
- package/src/ward-patient-card/row-elements/ward-patient-time-since-admission.tsx +22 -0
- package/src/ward-patient-card/ward-patient-card-element.component.tsx +65 -0
- package/src/ward-patient-card/ward-patient-card.component.tsx +64 -0
- package/src/ward-patient-card/ward-patient-card.scss +61 -12
- package/src/ward-patient-workspace/ward-patient-action-button.extension.tsx +18 -0
- package/src/ward-patient-workspace/ward-patient.style.scss +11 -0
- package/src/ward-patient-workspace/ward-patient.workspace.tsx +51 -0
- package/src/ward-view/ward-bed.component.tsx +0 -1
- package/src/ward-view/ward-view.component.tsx +114 -76
- package/src/ward-view/ward-view.resource.ts +2 -2
- package/src/ward-view/ward-view.scss +4 -4
- package/src/ward-view/ward-view.test.tsx +76 -49
- package/src/ward-view-header/admission-requests-bar.component.tsx +29 -28
- package/src/ward-view-header/admission-requests-bar.test.tsx +11 -15
- package/src/ward-view-header/admission-requests.scss +20 -25
- package/src/ward-view-header/ward-view-header.component.tsx +7 -7
- package/src/ward-view-header/ward-view-header.scss +2 -2
- package/src/ward-workspace/admission-request-card/admission-request-card-actions.component.tsx +29 -0
- package/src/ward-workspace/admission-request-card/admission-request-card-header.component.tsx +51 -0
- package/src/ward-workspace/admission-request-card/admission-request-card.component.tsx +16 -0
- package/src/ward-workspace/admission-request-card/admission-request-card.scss +49 -0
- package/src/ward-workspace/admission-request-workspace/admission-requests-workspace.scss +12 -0
- package/src/ward-workspace/admission-request-workspace/admission-requests-workspace.test.tsx +48 -0
- package/src/ward-workspace/admission-request-workspace/admission-requests.workspace.tsx +61 -0
- package/src/ward-workspace/admit-patient-form-workspace/admit-patient-form.scss +35 -0
- package/src/ward-workspace/admit-patient-form-workspace/admit-patient-form.test.tsx +341 -0
- package/src/ward-workspace/admit-patient-form-workspace/admit-patient-form.workspace.tsx +267 -0
- package/src/ward-workspace/admit-patient-form-workspace/types.ts +7 -0
- package/src/ward-workspace/patient-banner/patient-banner.component.tsx +29 -0
- package/src/ward-workspace/patient-banner/style.scss +23 -0
- package/src/ward-workspace/patient-transfer-bed-swap/patient-bed-swap-form.component.tsx +210 -0
- package/src/ward-workspace/patient-transfer-bed-swap/patient-transfer-request-form.component.tsx +238 -0
- package/src/ward-workspace/patient-transfer-bed-swap/patient-transfer-swap.scss +73 -0
- package/src/ward-workspace/patient-transfer-bed-swap/patient-transfer-swap.workspace.tsx +44 -0
- package/src/ward-workspace/ward-patient-notes/form/notes-form.component.tsx +180 -0
- package/src/ward-workspace/ward-patient-notes/form/notes-form.scss +30 -0
- package/src/ward-workspace/ward-patient-notes/form/notes-form.test.tsx +116 -0
- package/src/ward-workspace/ward-patient-notes/history/note.component.tsx +53 -0
- package/src/ward-workspace/ward-patient-notes/history/notes-container.component.tsx +55 -0
- package/src/ward-workspace/ward-patient-notes/history/notes-container.test.tsx +84 -0
- package/src/ward-workspace/ward-patient-notes/history/styles.scss +61 -0
- package/src/ward-workspace/ward-patient-notes/notes-action-button.extension.tsx +18 -0
- package/src/ward-workspace/ward-patient-notes/notes.resource.ts +71 -0
- package/src/ward-workspace/ward-patient-notes/notes.workspace.tsx +25 -0
- package/src/ward-workspace/ward-patient-notes/types.ts +44 -0
- package/src/ward.resource.ts +25 -0
- package/translations/en.json +63 -2
- package/dist/443.js +0 -1
- package/dist/443.js.map +0 -1
- package/dist/589.js +0 -1
- package/dist/589.js.map +0 -1
- package/dist/695.js +0 -2
- package/dist/695.js.map +0 -1
- package/src/hooks/useAdmittedPatients.ts +0 -13
- package/src/ward-patient-card/row-elements/row-elements.scss +0 -16
- package/src/ward-patient-card/ward-patient-card-row.resources.tsx +0 -92
- package/src/ward-patient-card/ward-patient-card.tsx +0 -20
- package/src/ward-workspace/admission-request-card.component.tsx +0 -23
- package/src/ward-workspace/admission-request-card.scss +0 -34
- package/src/ward-workspace/admission-request-workspace.test.tsx +0 -38
- package/src/ward-workspace/admission-requests-workspace.component.tsx +0 -21
- package/src/ward-workspace/admission-requests-workspace.scss +0 -13
- /package/dist/{695.js.LICENSE.txt → 793.js.LICENSE.txt} +0 -0
package/src/ward-workspace/patient-transfer-bed-swap/patient-transfer-request-form.component.tsx
ADDED
|
@@ -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
|
+
}
|