@openmrs/esm-patient-chart-app 11.3.1-patch.9310 → 11.3.1-patch.9508
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 +18 -21
- package/dist/1119.js +1 -1
- package/dist/1197.js +1 -1
- package/dist/2146.js +1 -1
- package/dist/2540.js +1 -0
- package/dist/2540.js.map +1 -0
- package/dist/2690.js +1 -1
- package/dist/276.js +1 -0
- package/dist/276.js.map +1 -0
- package/dist/2761.js.map +1 -1
- package/dist/3099.js +1 -1
- package/dist/{8278.js → 3119.js} +1 -1
- package/dist/{8278.js.map → 3119.js.map} +1 -1
- package/dist/3584.js +1 -1
- package/dist/3905.js +1 -0
- package/dist/3905.js.map +1 -0
- package/dist/4055.js +1 -1
- package/dist/4132.js +1 -1
- package/dist/4300.js +1 -1
- package/dist/4335.js +1 -1
- package/dist/439.js +1 -0
- package/dist/4618.js +1 -1
- package/dist/4652.js +1 -1
- package/dist/4944.js +1 -1
- package/dist/5048.js +1 -0
- package/dist/5048.js.map +1 -0
- package/dist/506.js +2 -0
- package/dist/506.js.map +1 -0
- package/dist/5173.js +1 -1
- package/dist/5241.js +1 -1
- package/dist/5442.js +1 -1
- package/dist/5661.js +1 -1
- package/dist/5670.js +1 -1
- package/dist/6022.js +1 -1
- package/dist/6411.js +1 -1
- package/dist/6411.js.map +1 -1
- package/dist/6468.js +1 -1
- package/dist/6568.js +1 -1
- package/dist/6568.js.map +1 -1
- package/dist/6589.js +1 -0
- package/dist/6679.js +1 -1
- package/dist/6840.js +1 -1
- package/dist/6859.js +1 -1
- package/dist/6924.js +1 -1
- package/dist/6924.js.map +1 -1
- package/dist/{9294.js → 6997.js} +1 -1
- package/dist/6997.js.map +1 -0
- package/dist/7097.js +1 -1
- package/dist/7159.js +1 -1
- package/dist/723.js +1 -1
- package/dist/7617.js +1 -1
- package/dist/7810.js +1 -0
- package/dist/7810.js.map +1 -0
- package/dist/7822.js +1 -1
- package/dist/7822.js.map +1 -1
- package/dist/795.js +1 -1
- package/dist/8163.js +1 -1
- package/dist/8260.js +1 -1
- package/dist/8260.js.map +1 -1
- package/dist/8349.js +1 -1
- package/dist/8371.js +1 -0
- package/dist/8454.js +1 -1
- package/dist/8454.js.map +1 -1
- package/dist/8618.js +1 -1
- package/dist/8709.js +1 -1
- package/dist/8709.js.map +1 -1
- package/dist/890.js +1 -1
- package/dist/9214.js +1 -1
- package/dist/9538.js +1 -1
- package/dist/9569.js +1 -1
- package/dist/986.js +1 -1
- package/dist/9879.js +1 -1
- package/dist/9895.js +1 -1
- package/dist/9900.js +1 -1
- package/dist/9913.js +1 -1
- package/dist/main.js +1 -1
- package/dist/main.js.map +1 -1
- package/dist/openmrs-esm-patient-chart-app.js +1 -1
- package/dist/openmrs-esm-patient-chart-app.js.buildmanifest.json +375 -285
- package/dist/openmrs-esm-patient-chart-app.js.map +1 -1
- package/dist/routes.json +1 -1
- package/package.json +2 -2
- package/src/actions-buttons/mark-patient-deceased.component.tsx +2 -2
- package/src/actions-buttons/start-visit.component.tsx +5 -10
- package/src/actions-buttons/start-visit.test.tsx +5 -9
- package/src/clinical-views/encounter-list/encounter-list-tabs.extension.tsx +2 -2
- package/src/clinical-views/utils/encounter-list-config-builder.ts +19 -6
- package/src/clinical-views/utils/helpers.ts +5 -4
- package/src/index.ts +18 -12
- package/src/mark-patient-deceased/mark-patient-deceased-form.test.tsx +9 -15
- package/src/mark-patient-deceased/mark-patient-deceased-form.workspace.tsx +138 -147
- package/src/patient-banner-tags/visit-attribute-tags.extension.tsx +21 -13
- package/src/patient-banner-tags/visit-attribute-tags.scss +8 -0
- package/src/patient-chart/chart-review/dashboard-view.component.tsx +2 -2
- package/src/patient-chart/chart-review/dashboard-view.scss +5 -0
- package/src/patient-chart/patient-chart.component.tsx +41 -50
- package/src/patient-chart/patient-chart.resources.ts +10 -52
- package/src/routes.json +7 -18
- package/src/visit/hooks/useDeleteVisit.tsx +1 -1
- package/src/visit/start-visit-button.component.tsx +2 -2
- package/src/visit/start-visit-button.test.tsx +2 -2
- package/src/visit/visit-action-items/edit-visit-details.component.tsx +8 -29
- package/src/visit/visit-form/base-visit-type.component.tsx +30 -21
- package/src/visit/visit-form/exported-visit-form.workspace.tsx +3 -0
- package/src/visit/visit-form/visit-form.test.tsx +18 -27
- package/src/visit/visit-form/visit-form.workspace.tsx +653 -35
- package/src/visit/visit-history-table/visit-actions-cell.component.tsx +2 -3
- package/src/visit/visit-history-table/visit-date-cell.component.tsx +0 -1
- package/src/visit/visit-history-table/visit-diagnoses-cell.component.tsx +0 -1
- package/src/visit/visit-history-table/visit-history-table.component.tsx +2 -3
- package/src/visit/visit-history-table/visit-type-cell.component.tsx +0 -1
- package/src/visit/visit-prompt/delete-visit-dialog.test.tsx +1 -1
- package/src/visit/visit-prompt/{end-visit-dialog.modal.tsx → end-visit-dialog.component.tsx} +1 -1
- package/src/visit/visit-prompt/end-visit-dialog.test.tsx +1 -1
- package/src/visit/visit-prompt/{start-visit-dialog.modal.tsx → start-visit-dialog.component.tsx} +4 -10
- package/src/visit/visit-prompt/start-visit-dialog.test.tsx +3 -3
- package/src/visit/visits-widget/current-visit-summary.extension.tsx +3 -3
- package/src/visit/visits-widget/past-visits-components/encounters-table/encounters-table.component.tsx +35 -12
- package/src/visit/visits-widget/visit-context/retrospective-data-date-time-picker/retrospective-date-time-picker.component.tsx +0 -1
- package/src/visit/visits-widget/visit-context/visit-context-switcher.modal.tsx +2 -2
- package/src/visit/visits-widget/visit-context/visit-context-switcher.test.tsx +20 -22
- package/src/visit/visits-widget/visit-detail-overview.component.tsx +2 -3
- package/src/visit/visits-widget/visit-detail-overview.test.tsx +4 -4
- package/translations/am.json +1 -2
- package/translations/ar.json +2 -3
- package/translations/ar_SY.json +1 -2
- package/translations/bn.json +1 -2
- package/translations/cs.json +196 -0
- package/translations/de.json +1 -2
- package/translations/en.json +1 -1
- package/translations/en_US.json +1 -2
- package/translations/es.json +2 -3
- package/translations/es_MX.json +1 -2
- package/translations/fr.json +7 -8
- package/translations/he.json +2 -3
- package/translations/hi.json +1 -2
- package/translations/hi_IN.json +1 -2
- package/translations/id.json +2 -3
- package/translations/it.json +2 -3
- package/translations/ka.json +2 -3
- package/translations/km.json +2 -3
- package/translations/ku.json +1 -2
- package/translations/ky.json +1 -2
- package/translations/lg.json +1 -2
- package/translations/ne.json +1 -2
- package/translations/pl.json +1 -2
- package/translations/pt.json +2 -3
- package/translations/pt_BR.json +2 -3
- package/translations/qu.json +1 -2
- package/translations/ro_RO.json +2 -3
- package/translations/ru_RU.json +1 -2
- package/translations/si.json +1 -2
- package/translations/sq.json +196 -0
- package/translations/sw.json +1 -2
- package/translations/sw_KE.json +1 -2
- package/translations/tr.json +1 -2
- package/translations/tr_TR.json +1 -2
- package/translations/uk.json +1 -2
- package/translations/uz.json +1 -2
- package/translations/uz@Latn.json +1 -2
- package/translations/uz_UZ.json +1 -2
- package/translations/vi.json +2 -3
- package/translations/zh.json +2 -3
- package/translations/zh_CN.json +2 -3
- package/translations/zh_TW.json +196 -0
- package/dist/1815.js +0 -2
- package/dist/1815.js.map +0 -1
- package/dist/3697.js +0 -1
- package/dist/3697.js.map +0 -1
- package/dist/5827.js +0 -1
- package/dist/5827.js.map +0 -1
- package/dist/7818.js +0 -1
- package/dist/7818.js.map +0 -1
- package/dist/9294.js.map +0 -1
- package/dist/9329.js +0 -1
- package/dist/9329.js.map +0 -1
- /package/dist/{1815.js.LICENSE.txt → 506.js.LICENSE.txt} +0 -0
- /package/src/visit/visit-prompt/{delete-visit-dialog.modal.tsx → delete-visit-dialog.component.tsx} +0 -0
|
@@ -1,12 +1,69 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import classNames from 'classnames';
|
|
3
|
+
import { Controller, FormProvider, useForm } from 'react-hook-form';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
2
5
|
import { useSWRConfig } from 'swr';
|
|
3
|
-
import { launchWorkspaceGroup2, useVisit, type Visit } from '@openmrs/esm-framework';
|
|
4
6
|
import {
|
|
7
|
+
Button,
|
|
8
|
+
ButtonSet,
|
|
9
|
+
ContentSwitcher,
|
|
10
|
+
Form,
|
|
11
|
+
FormGroup,
|
|
12
|
+
InlineLoading,
|
|
13
|
+
InlineNotification,
|
|
14
|
+
RadioButton,
|
|
15
|
+
RadioButtonGroup,
|
|
16
|
+
Row,
|
|
17
|
+
Stack,
|
|
18
|
+
Switch,
|
|
19
|
+
} from '@carbon/react';
|
|
20
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
21
|
+
import {
|
|
22
|
+
Extension,
|
|
23
|
+
ExtensionSlot,
|
|
24
|
+
OpenmrsFetchError,
|
|
25
|
+
saveVisit,
|
|
26
|
+
showSnackbar,
|
|
27
|
+
updateVisit,
|
|
28
|
+
useConfig,
|
|
29
|
+
useConnectivity,
|
|
30
|
+
useEmrConfiguration,
|
|
31
|
+
useLayoutType,
|
|
32
|
+
useVisit,
|
|
33
|
+
type AssignedExtension,
|
|
34
|
+
type NewVisitPayload,
|
|
35
|
+
type Visit,
|
|
36
|
+
} from '@openmrs/esm-framework';
|
|
37
|
+
import {
|
|
38
|
+
createOfflineVisitForPatient,
|
|
5
39
|
invalidateVisitByUuid,
|
|
6
|
-
|
|
40
|
+
invalidateVisitAndEncounterData,
|
|
41
|
+
useActivePatientEnrollment,
|
|
7
42
|
usePatientChartStore,
|
|
43
|
+
type DefaultPatientWorkspaceProps,
|
|
8
44
|
} from '@openmrs/esm-patient-common-lib';
|
|
9
|
-
import
|
|
45
|
+
import { MemoizedRecommendedVisitType } from './recommended-visit-type.component';
|
|
46
|
+
import {
|
|
47
|
+
convertToDate,
|
|
48
|
+
createVisitAttribute,
|
|
49
|
+
deleteVisitAttribute,
|
|
50
|
+
extractErrorMessagesFromResponse,
|
|
51
|
+
updateVisitAttribute,
|
|
52
|
+
useConditionalVisitTypes,
|
|
53
|
+
useVisitFormCallbacks,
|
|
54
|
+
useVisitFormSchemaAndDefaultValues,
|
|
55
|
+
visitStatuses,
|
|
56
|
+
type ErrorObject,
|
|
57
|
+
type VisitFormCallbacks,
|
|
58
|
+
type VisitFormData,
|
|
59
|
+
} from './visit-form.resource';
|
|
60
|
+
import BaseVisitType from './base-visit-type.component';
|
|
61
|
+
import LocationSelector from './location-selector.component';
|
|
62
|
+
import VisitAttributeTypeFields from './visit-attribute-type.component';
|
|
63
|
+
import VisitDateTimeSection from './visit-date-time.component';
|
|
64
|
+
import { useVisitAttributeTypes } from '../hooks/useVisitAttributeType';
|
|
65
|
+
import { type ChartConfig } from '../../config-schema';
|
|
66
|
+
import styles from './visit-form.scss';
|
|
10
67
|
|
|
11
68
|
interface VisitAttribute {
|
|
12
69
|
attributeType: string;
|
|
@@ -30,58 +87,619 @@ export interface ExtraVisitInfo {
|
|
|
30
87
|
attributes?: Array<VisitAttribute>;
|
|
31
88
|
}
|
|
32
89
|
|
|
33
|
-
|
|
90
|
+
interface VisitFormProps extends DefaultPatientWorkspaceProps {
|
|
34
91
|
/**
|
|
35
92
|
* A unique string identifying where the visit form is opened from.
|
|
36
93
|
* This string is passed into various extensions within the form to
|
|
37
94
|
* affect how / if they should be rendered.
|
|
38
95
|
*/
|
|
96
|
+
handleReturnToSearchList?: () => void;
|
|
39
97
|
openedFrom: string;
|
|
40
98
|
showPatientHeader?: boolean;
|
|
99
|
+
visitToEdit?: Visit;
|
|
41
100
|
}
|
|
42
|
-
|
|
43
101
|
/**
|
|
44
102
|
* This form is used for starting a new visit and for editing
|
|
45
103
|
* an existing visit
|
|
46
104
|
*/
|
|
47
|
-
const VisitForm: React.FC<
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
105
|
+
const VisitForm: React.FC<VisitFormProps> = ({
|
|
106
|
+
closeWorkspace,
|
|
107
|
+
handleReturnToSearchList,
|
|
108
|
+
openedFrom,
|
|
109
|
+
patient,
|
|
110
|
+
patientUuid,
|
|
111
|
+
promptBeforeClosing,
|
|
112
|
+
showPatientHeader = false,
|
|
113
|
+
visitToEdit,
|
|
51
114
|
}) => {
|
|
115
|
+
const { t } = useTranslation();
|
|
116
|
+
const isTablet = useLayoutType() === 'tablet';
|
|
117
|
+
const isOnline = useConnectivity();
|
|
118
|
+
const config = useConfig<ChartConfig>();
|
|
119
|
+
const { emrConfiguration } = useEmrConfiguration();
|
|
120
|
+
const [visitTypeContentSwitcherIndex, setVisitTypeContentSwitcherIndex] = useState(
|
|
121
|
+
config.showRecommendedVisitTypeTab ? 0 : 1,
|
|
122
|
+
);
|
|
123
|
+
const visitHeaderSlotState = useMemo(() => ({ patientUuid }), [patientUuid]);
|
|
124
|
+
const { activePatientEnrollment, isLoading } = useActivePatientEnrollment(patientUuid);
|
|
52
125
|
const { mutate: mutateActiveVisit } = useVisit(patientUuid);
|
|
53
126
|
const { mutate: globalMutate } = useSWRConfig();
|
|
127
|
+
const allVisitTypes = useConditionalVisitTypes();
|
|
54
128
|
const { setVisitContext } = usePatientChartStore(patientUuid);
|
|
55
129
|
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
130
|
+
const [errorFetchingResources, setErrorFetchingResources] = useState<{
|
|
131
|
+
blockSavingForm: boolean;
|
|
132
|
+
} | null>(null);
|
|
133
|
+
const { visitAttributeTypes } = useVisitAttributeTypes();
|
|
134
|
+
const [visitFormCallbacks, setVisitFormCallbacks] = useVisitFormCallbacks();
|
|
135
|
+
const [extraVisitInfo, setExtraVisitInfo] = useState<ExtraVisitInfo | null>(null);
|
|
136
|
+
|
|
137
|
+
const { visitFormSchema, defaultValues, firstEncounterDateTime, lastEncounterDateTime } =
|
|
138
|
+
useVisitFormSchemaAndDefaultValues(visitToEdit);
|
|
139
|
+
|
|
140
|
+
const methods = useForm<VisitFormData>({
|
|
141
|
+
mode: 'all',
|
|
142
|
+
resolver: zodResolver(visitFormSchema),
|
|
143
|
+
defaultValues,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const {
|
|
147
|
+
handleSubmit,
|
|
148
|
+
control,
|
|
149
|
+
getValues,
|
|
150
|
+
formState: { errors, isDirty, isSubmitting },
|
|
151
|
+
reset,
|
|
152
|
+
} = methods;
|
|
153
|
+
|
|
154
|
+
// default values are cached so form needs to be reset when they change (e.g. when default visit location finishes loading)
|
|
155
|
+
useEffect(() => {
|
|
156
|
+
reset(defaultValues);
|
|
157
|
+
}, [defaultValues, reset]);
|
|
158
|
+
|
|
159
|
+
useEffect(() => {
|
|
160
|
+
promptBeforeClosing(() => isDirty);
|
|
161
|
+
}, [isDirty, promptBeforeClosing]);
|
|
162
|
+
|
|
163
|
+
const isValidVisitAttributesArray = useCallback((attributes: unknown): boolean => {
|
|
164
|
+
return (
|
|
165
|
+
Array.isArray(attributes) &&
|
|
166
|
+
attributes.length > 0 &&
|
|
167
|
+
attributes.every((attr) => attr?.attributeType?.trim().length > 0 && attr?.value?.trim().length > 0)
|
|
168
|
+
);
|
|
169
|
+
}, []);
|
|
170
|
+
|
|
171
|
+
const handleVisitAttributes = useCallback(
|
|
172
|
+
(visitAttributes: { [p: string]: string }, visitUuid: string) => {
|
|
173
|
+
const existingVisitAttributeTypes =
|
|
174
|
+
visitToEdit?.attributes?.map((attribute) => attribute.attributeType.uuid) || [];
|
|
175
|
+
|
|
176
|
+
const promises = [];
|
|
177
|
+
|
|
178
|
+
for (const [attributeType, value] of Object.entries(visitAttributes)) {
|
|
179
|
+
if (attributeType && existingVisitAttributeTypes.includes(attributeType)) {
|
|
180
|
+
const attributeToEdit = visitToEdit.attributes.find((attr) => attr.attributeType.uuid === attributeType);
|
|
181
|
+
|
|
182
|
+
if (attributeToEdit) {
|
|
183
|
+
// continue to next attribute if the previous value is same as new value
|
|
184
|
+
const isSameValue =
|
|
185
|
+
typeof attributeToEdit.value === 'object'
|
|
186
|
+
? attributeToEdit.value.uuid === value
|
|
187
|
+
: attributeToEdit.value === value;
|
|
188
|
+
|
|
189
|
+
if (isSameValue) {
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (value) {
|
|
194
|
+
// Update attribute with new value
|
|
195
|
+
promises.push(
|
|
196
|
+
updateVisitAttribute(visitUuid, attributeToEdit.uuid, value).catch((err) => {
|
|
197
|
+
showSnackbar({
|
|
198
|
+
title: t('errorUpdatingVisitAttribute', 'Error updating the {{attributeName}} visit attribute', {
|
|
199
|
+
attributeName: attributeToEdit.attributeType.display,
|
|
200
|
+
}),
|
|
201
|
+
kind: 'error',
|
|
202
|
+
isLowContrast: false,
|
|
203
|
+
subtitle: err?.message,
|
|
204
|
+
});
|
|
205
|
+
return Promise.reject(err); // short-circuit promise chain
|
|
206
|
+
}),
|
|
207
|
+
);
|
|
208
|
+
} else {
|
|
209
|
+
// Delete attribute if no value is provided
|
|
210
|
+
promises.push(
|
|
211
|
+
deleteVisitAttribute(visitUuid, attributeToEdit.uuid).catch((err) => {
|
|
212
|
+
showSnackbar({
|
|
213
|
+
title: t('errorDeletingVisitAttribute', 'Error deleting the {{attributeName}} visit attribute', {
|
|
214
|
+
attributeName: attributeToEdit.attributeType.display,
|
|
215
|
+
}),
|
|
216
|
+
kind: 'error',
|
|
217
|
+
isLowContrast: false,
|
|
218
|
+
subtitle: err?.message,
|
|
219
|
+
});
|
|
220
|
+
return Promise.reject(err); // short-circuit promise chain
|
|
221
|
+
}),
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
} else {
|
|
226
|
+
if (value) {
|
|
227
|
+
promises.push(
|
|
228
|
+
createVisitAttribute(visitUuid, attributeType, value).catch((err) => {
|
|
229
|
+
showSnackbar({
|
|
230
|
+
title: t('errorCreatingVisitAttribute', 'Error creating the {{attributeName}} visit attribute', {
|
|
231
|
+
attributeName: visitAttributeTypes?.find((type) => type.uuid === attributeType)?.display,
|
|
232
|
+
}),
|
|
233
|
+
kind: 'error',
|
|
234
|
+
isLowContrast: false,
|
|
235
|
+
subtitle: err?.message,
|
|
236
|
+
});
|
|
237
|
+
return Promise.reject(err); // short-circuit promise chain
|
|
238
|
+
}),
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return Promise.all(promises);
|
|
245
|
+
},
|
|
246
|
+
[visitToEdit, t, visitAttributeTypes],
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
const onSubmit = useCallback(
|
|
250
|
+
(data: VisitFormData) => {
|
|
251
|
+
const {
|
|
252
|
+
visitStatus,
|
|
253
|
+
visitStartTimeFormat,
|
|
254
|
+
visitStartDate,
|
|
255
|
+
visitLocation,
|
|
256
|
+
visitStartTime,
|
|
257
|
+
visitType,
|
|
258
|
+
visitAttributes,
|
|
259
|
+
visitStopDate,
|
|
260
|
+
visitStopTime,
|
|
261
|
+
visitStopTimeFormat,
|
|
262
|
+
} = data;
|
|
263
|
+
|
|
264
|
+
const { handleCreateExtraVisitInfo, attributes: extraAttributes } = extraVisitInfo ?? {};
|
|
265
|
+
const hasStartTime = ['ongoing', 'past'].includes(visitStatus);
|
|
266
|
+
const hasStopTime = 'past' === visitStatus;
|
|
267
|
+
const startDatetime = convertToDate(visitStartDate, visitStartTime, visitStartTimeFormat);
|
|
268
|
+
const stopDatetime = convertToDate(visitStopDate, visitStopTime, visitStopTimeFormat);
|
|
269
|
+
|
|
270
|
+
let payload: NewVisitPayload = {
|
|
271
|
+
visitType: visitType,
|
|
272
|
+
location: visitLocation?.uuid,
|
|
273
|
+
startDatetime: hasStartTime ? startDatetime : null,
|
|
274
|
+
stopDatetime: hasStopTime ? stopDatetime : null,
|
|
275
|
+
// The request throws 400 (Bad request) error when the patient is passed in the update payload for existing visit
|
|
276
|
+
...(!visitToEdit && { patient: patientUuid }),
|
|
277
|
+
...(isValidVisitAttributesArray(extraAttributes) && { attributes: extraAttributes }),
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
handleCreateExtraVisitInfo?.();
|
|
281
|
+
|
|
282
|
+
const abortController = new AbortController();
|
|
283
|
+
if (isOnline) {
|
|
284
|
+
const visitRequest = visitToEdit?.uuid
|
|
285
|
+
? updateVisit(visitToEdit?.uuid, payload, abortController)
|
|
286
|
+
: saveVisit(payload, abortController);
|
|
287
|
+
|
|
288
|
+
visitRequest
|
|
289
|
+
.then((response) => {
|
|
290
|
+
showSnackbar({
|
|
291
|
+
isLowContrast: true,
|
|
292
|
+
kind: 'success',
|
|
293
|
+
subtitle: !visitToEdit
|
|
294
|
+
? t('visitStartedSuccessfully', '{{visit}} started successfully', {
|
|
295
|
+
visit: response?.data?.visitType?.display ?? t('visit', 'Visit'),
|
|
296
|
+
})
|
|
297
|
+
: t('visitDetailsUpdatedSuccessfully', '{{visit}} updated successfully', {
|
|
298
|
+
visit: response?.data?.visitType?.display ?? t('pastVisit', 'Past visit'),
|
|
299
|
+
}),
|
|
300
|
+
title: !visitToEdit
|
|
301
|
+
? t('visitStarted', 'Visit started')
|
|
302
|
+
: t('visitDetailsUpdated', 'Visit details updated'),
|
|
303
|
+
});
|
|
304
|
+
return response;
|
|
305
|
+
})
|
|
306
|
+
.catch((error) => {
|
|
307
|
+
const errorDescription =
|
|
308
|
+
OpenmrsFetchError && error instanceof OpenmrsFetchError
|
|
309
|
+
? typeof error.responseBody === 'string'
|
|
310
|
+
? error.responseBody
|
|
311
|
+
: extractErrorMessagesFromResponse(error.responseBody as ErrorObject, t)
|
|
312
|
+
: error?.message;
|
|
313
|
+
|
|
314
|
+
showSnackbar({
|
|
315
|
+
title: !visitToEdit
|
|
316
|
+
? t('startVisitError', 'Error starting visit')
|
|
317
|
+
: t('errorUpdatingVisitDetails', 'Error updating visit details'),
|
|
318
|
+
kind: 'error',
|
|
319
|
+
isLowContrast: false,
|
|
320
|
+
subtitle: errorDescription,
|
|
321
|
+
});
|
|
322
|
+
return Promise.reject(error); // short-circuit promise chain
|
|
323
|
+
})
|
|
324
|
+
.then((response) => {
|
|
325
|
+
// now that visit is created / updated, we run post-submit actions
|
|
326
|
+
// to update visit attributes or any other OnVisitCreatedOrUpdated actions
|
|
327
|
+
const visit = response.data;
|
|
328
|
+
|
|
329
|
+
// For visit creation, we need to update:
|
|
330
|
+
// 1. Current visit data (for critical components like visit summary, action buttons)
|
|
331
|
+
// 2. Visit history table (for the paginated visit list)
|
|
63
332
|
|
|
64
|
-
|
|
65
|
-
|
|
333
|
+
// Update patient's visit data for critical components
|
|
334
|
+
const mutateSavedOrUpdatedVisit = () => invalidateVisitByUuid(globalMutate, visit.uuid);
|
|
335
|
+
mutateActiveVisit();
|
|
336
|
+
setVisitContext?.(visit, mutateSavedOrUpdatedVisit);
|
|
337
|
+
visitToEdit && mutateSavedOrUpdatedVisit();
|
|
338
|
+
|
|
339
|
+
// Use targeted SWR invalidation instead of global mutateVisit
|
|
340
|
+
// This will invalidate visit history and encounter tables for this patient
|
|
341
|
+
// (if visitContext is updated, it should have been invalidated with mutateSavedOrUpdatedVisit)
|
|
342
|
+
invalidateVisitAndEncounterData(globalMutate, patientUuid);
|
|
343
|
+
|
|
344
|
+
// handleVisitAttributes already has code to show error snackbar when attribute fails to update
|
|
345
|
+
// no need for catch block here
|
|
346
|
+
const visitAttributesRequest = handleVisitAttributes(visitAttributes, response.data.uuid).then(
|
|
347
|
+
(visitAttributesResponses) => {
|
|
348
|
+
if (visitAttributesResponses.length > 0) {
|
|
349
|
+
showSnackbar({
|
|
350
|
+
isLowContrast: true,
|
|
351
|
+
kind: 'success',
|
|
352
|
+
title: t(
|
|
353
|
+
'additionalVisitInformationUpdatedSuccessfully',
|
|
354
|
+
'Additional visit information updated successfully',
|
|
355
|
+
),
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
},
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
const onVisitCreatedOrUpdatedRequests = [...visitFormCallbacks.values()].map((callbacks) =>
|
|
362
|
+
callbacks.onVisitCreatedOrUpdated(visit),
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
return Promise.all([visitAttributesRequest, ...onVisitCreatedOrUpdatedRequests]);
|
|
366
|
+
})
|
|
367
|
+
.then(() => {
|
|
368
|
+
closeWorkspace({ ignoreChanges: true });
|
|
369
|
+
})
|
|
370
|
+
.catch(() => {
|
|
371
|
+
// do nothing, this catches any reject promises used for short-circuiting
|
|
372
|
+
});
|
|
373
|
+
} else {
|
|
374
|
+
createOfflineVisitForPatient(
|
|
375
|
+
patientUuid,
|
|
376
|
+
visitLocation.uuid,
|
|
377
|
+
config.offlineVisitTypeUuid,
|
|
378
|
+
payload.startDatetime,
|
|
379
|
+
).then(
|
|
380
|
+
(visit) => {
|
|
381
|
+
// Use same targeted approach for offline visits for consistency
|
|
382
|
+
const mutateSavedOrUpdatedVisit = () => invalidateVisitByUuid(globalMutate, visit.uuid);
|
|
383
|
+
mutateActiveVisit();
|
|
384
|
+
setVisitContext?.(visit, mutateSavedOrUpdatedVisit);
|
|
385
|
+
visitToEdit && mutateSavedOrUpdatedVisit();
|
|
386
|
+
|
|
387
|
+
// Also invalidate visit history and encounter tables
|
|
388
|
+
invalidateVisitAndEncounterData(globalMutate, patientUuid);
|
|
389
|
+
closeWorkspace({ ignoreChanges: true });
|
|
390
|
+
showSnackbar({
|
|
391
|
+
isLowContrast: true,
|
|
392
|
+
kind: 'success',
|
|
393
|
+
subtitle: t('visitStartedSuccessfully', '{{visit}} started successfully', {
|
|
394
|
+
visit: t('offlineVisit', 'Offline Visit'),
|
|
395
|
+
}),
|
|
396
|
+
title: t('visitStarted', 'Visit started'),
|
|
397
|
+
});
|
|
398
|
+
},
|
|
399
|
+
(error: Error) => {
|
|
400
|
+
showSnackbar({
|
|
401
|
+
title: t('startVisitError', 'Error starting visit'),
|
|
402
|
+
kind: 'error',
|
|
403
|
+
isLowContrast: false,
|
|
404
|
+
subtitle: error?.message,
|
|
405
|
+
});
|
|
406
|
+
},
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
},
|
|
412
|
+
[
|
|
413
|
+
closeWorkspace,
|
|
414
|
+
config.offlineVisitTypeUuid,
|
|
415
|
+
extraVisitInfo,
|
|
416
|
+
globalMutate,
|
|
417
|
+
handleVisitAttributes,
|
|
418
|
+
isOnline,
|
|
419
|
+
setVisitContext,
|
|
66
420
|
patientUuid,
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
421
|
+
t,
|
|
422
|
+
visitFormCallbacks,
|
|
423
|
+
visitToEdit,
|
|
424
|
+
isValidVisitAttributesArray,
|
|
425
|
+
mutateActiveVisit,
|
|
426
|
+
],
|
|
427
|
+
);
|
|
428
|
+
|
|
429
|
+
const handleDiscard = useCallback(() => {
|
|
430
|
+
if (handleReturnToSearchList) {
|
|
431
|
+
handleReturnToSearchList();
|
|
432
|
+
} else {
|
|
433
|
+
closeWorkspace();
|
|
434
|
+
}
|
|
435
|
+
}, [handleReturnToSearchList, closeWorkspace]);
|
|
436
|
+
|
|
71
437
|
return (
|
|
72
|
-
<
|
|
73
|
-
{
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
438
|
+
<FormProvider {...methods}>
|
|
439
|
+
<Form className={styles.form} onSubmit={handleSubmit(onSubmit)} data-openmrs-role="Start Visit Form">
|
|
440
|
+
{showPatientHeader && patient && (
|
|
441
|
+
<ExtensionSlot
|
|
442
|
+
name="patient-header-slot"
|
|
443
|
+
state={{
|
|
444
|
+
patient,
|
|
445
|
+
patientUuid: patientUuid,
|
|
446
|
+
hideActionsOverflow: true,
|
|
447
|
+
}}
|
|
448
|
+
/>
|
|
449
|
+
)}
|
|
450
|
+
{errorFetchingResources && (
|
|
451
|
+
<InlineNotification
|
|
452
|
+
kind={errorFetchingResources?.blockSavingForm ? 'error' : 'warning'}
|
|
453
|
+
lowContrast
|
|
454
|
+
className={styles.inlineNotification}
|
|
455
|
+
title={t('partOfFormDidntLoad', 'Part of the form did not load')}
|
|
456
|
+
subtitle={t('refreshToTryAgain', 'Please refresh to try again')}
|
|
457
|
+
/>
|
|
458
|
+
)}
|
|
459
|
+
<div>
|
|
460
|
+
{isTablet && (
|
|
461
|
+
<Row className={styles.headerGridRow}>
|
|
462
|
+
<ExtensionSlot
|
|
463
|
+
name="visit-form-header-slot"
|
|
464
|
+
className={styles.dataGridRow}
|
|
465
|
+
state={visitHeaderSlotState}
|
|
466
|
+
/>
|
|
467
|
+
</Row>
|
|
468
|
+
)}
|
|
469
|
+
<Stack gap={4} className={styles.container}>
|
|
470
|
+
<section>
|
|
471
|
+
<FormGroup legendText={t('theVisitIs', 'The visit is')}>
|
|
472
|
+
<Controller
|
|
473
|
+
name="visitStatus"
|
|
474
|
+
control={control}
|
|
475
|
+
render={({ field: { onChange, value } }) => {
|
|
476
|
+
const validVisitStatuses = visitToEdit ? ['ongoing', 'past'] : visitStatuses;
|
|
477
|
+
const idx = validVisitStatuses.indexOf(value);
|
|
478
|
+
const selectedIndex = idx >= 0 ? idx : 0;
|
|
479
|
+
|
|
480
|
+
// For some reason, Carbon throws NPE when trying to conditionally
|
|
481
|
+
// render a <Switch> component
|
|
482
|
+
return visitToEdit ? (
|
|
483
|
+
<ContentSwitcher selectedIndex={selectedIndex} onChange={({ name }) => onChange(name)} size="md">
|
|
484
|
+
<Switch name="ongoing" text={t('ongoing', 'Ongoing')} />
|
|
485
|
+
<Switch name="past" text={t('ended', 'Ended')} />
|
|
486
|
+
</ContentSwitcher>
|
|
487
|
+
) : (
|
|
488
|
+
<ContentSwitcher selectedIndex={selectedIndex} onChange={({ name }) => onChange(name)} size="md">
|
|
489
|
+
<Switch name="new" text={t('new', 'New')} />
|
|
490
|
+
<Switch name="ongoing" text={t('ongoing', 'Ongoing')} />
|
|
491
|
+
<Switch name="past" text={t('inThePast', 'In the past')} />
|
|
492
|
+
</ContentSwitcher>
|
|
493
|
+
);
|
|
494
|
+
}}
|
|
495
|
+
/>
|
|
496
|
+
</FormGroup>
|
|
497
|
+
</section>
|
|
498
|
+
<VisitDateTimeSection {...{ control, firstEncounterDateTime, lastEncounterDateTime }} />
|
|
499
|
+
{/* Upcoming appointments. This get shown when config.showUpcomingAppointments is true. */}
|
|
500
|
+
{config.showUpcomingAppointments && (
|
|
501
|
+
<section>
|
|
502
|
+
<div className={styles.sectionField}>
|
|
503
|
+
<VisitFormExtensionSlot
|
|
504
|
+
name="visit-form-top-slot"
|
|
505
|
+
patientUuid={patientUuid}
|
|
506
|
+
visitFormOpenedFrom={openedFrom}
|
|
507
|
+
setVisitFormCallbacks={setVisitFormCallbacks}
|
|
508
|
+
/>
|
|
509
|
+
</div>
|
|
510
|
+
</section>
|
|
511
|
+
)}
|
|
512
|
+
|
|
513
|
+
{/* This field lets the user select a location for the visit. The location is required for the visit to be saved. Defaults to the active session location */}
|
|
514
|
+
<LocationSelector control={control} />
|
|
515
|
+
|
|
516
|
+
{/* Lists available program types. This feature is dependent on the `showRecommendedVisitTypeTab` config being set
|
|
517
|
+
to true. */}
|
|
518
|
+
{config.showRecommendedVisitTypeTab && (
|
|
519
|
+
<section>
|
|
520
|
+
<h1 className={styles.sectionTitle}>{t('program', 'Program')}</h1>
|
|
521
|
+
<FormGroup legendText={t('selectProgramType', 'Select program type')} className={styles.sectionField}>
|
|
522
|
+
<Controller
|
|
523
|
+
name="programType"
|
|
524
|
+
control={control}
|
|
525
|
+
render={({ field: { onChange } }) => (
|
|
526
|
+
<RadioButtonGroup
|
|
527
|
+
orientation="vertical"
|
|
528
|
+
onChange={(uuid: string) =>
|
|
529
|
+
onChange(activePatientEnrollment.find(({ program }) => program.uuid === uuid)?.uuid)
|
|
530
|
+
}
|
|
531
|
+
name="program-type-radio-group"
|
|
532
|
+
>
|
|
533
|
+
{activePatientEnrollment.map(({ uuid, display, program }) => (
|
|
534
|
+
<RadioButton
|
|
535
|
+
key={uuid}
|
|
536
|
+
className={styles.radioButton}
|
|
537
|
+
id={uuid}
|
|
538
|
+
labelText={display}
|
|
539
|
+
value={program.uuid}
|
|
540
|
+
/>
|
|
541
|
+
))}
|
|
542
|
+
</RadioButtonGroup>
|
|
543
|
+
)}
|
|
544
|
+
/>
|
|
545
|
+
</FormGroup>
|
|
546
|
+
</section>
|
|
547
|
+
)}
|
|
548
|
+
|
|
549
|
+
{/* Lists available visit types if no atFacilityVisitType enabled. The content switcher only gets shown when recommended visit types are enabled */}
|
|
550
|
+
{!emrConfiguration?.atFacilityVisitType && (
|
|
551
|
+
<section>
|
|
552
|
+
<h1 className={styles.sectionTitle}>{t('visitType_title', 'Visit Type')}</h1>
|
|
553
|
+
<div className={styles.sectionField}>
|
|
554
|
+
{config.showRecommendedVisitTypeTab ? (
|
|
555
|
+
<>
|
|
556
|
+
<ContentSwitcher
|
|
557
|
+
selectedIndex={visitTypeContentSwitcherIndex}
|
|
558
|
+
onChange={({ index }) => setVisitTypeContentSwitcherIndex(index)}
|
|
559
|
+
size="md"
|
|
560
|
+
>
|
|
561
|
+
<Switch name="recommended" text={t('recommended', 'Recommended')} />
|
|
562
|
+
<Switch name="all" text={t('all', 'All')} />
|
|
563
|
+
</ContentSwitcher>
|
|
564
|
+
{visitTypeContentSwitcherIndex === 0 && !isLoading && (
|
|
565
|
+
<MemoizedRecommendedVisitType
|
|
566
|
+
patientUuid={patientUuid}
|
|
567
|
+
patientProgramEnrollment={(() => {
|
|
568
|
+
return activePatientEnrollment?.find(
|
|
569
|
+
({ program }) => program.uuid === getValues('programType'),
|
|
570
|
+
);
|
|
571
|
+
})()}
|
|
572
|
+
locationUuid={getValues('visitLocation')?.uuid}
|
|
573
|
+
/>
|
|
574
|
+
)}
|
|
575
|
+
{visitTypeContentSwitcherIndex === 1 && <BaseVisitType visitTypes={allVisitTypes} />}
|
|
576
|
+
</>
|
|
577
|
+
) : (
|
|
578
|
+
// Defaults to showing all possible visit types if recommended visits are not enabled
|
|
579
|
+
<BaseVisitType visitTypes={allVisitTypes} />
|
|
580
|
+
)}
|
|
581
|
+
</div>
|
|
582
|
+
|
|
583
|
+
{errors?.visitType && (
|
|
584
|
+
<section>
|
|
585
|
+
<div className={styles.sectionField}>
|
|
586
|
+
<InlineNotification
|
|
587
|
+
role="alert"
|
|
588
|
+
style={{ margin: '0', minWidth: '100%' }}
|
|
589
|
+
kind="error"
|
|
590
|
+
lowContrast={true}
|
|
591
|
+
title={t('missingVisitType', 'Missing visit type')}
|
|
592
|
+
subtitle={t('selectVisitType', 'Please select a Visit Type')}
|
|
593
|
+
/>
|
|
594
|
+
</div>
|
|
595
|
+
</section>
|
|
596
|
+
)}
|
|
597
|
+
</section>
|
|
598
|
+
)}
|
|
599
|
+
|
|
600
|
+
<ExtensionSlot state={{ patientUuid, setExtraVisitInfo }} name="extra-visit-attribute-slot" />
|
|
601
|
+
|
|
602
|
+
{/* Visit type attribute fields. These get shown when visit attribute types are configured */}
|
|
603
|
+
<section>
|
|
604
|
+
<h1 className={styles.sectionTitle}>{isTablet && t('visitAttributes', 'Visit attributes')}</h1>
|
|
605
|
+
<div className={styles.sectionField}>
|
|
606
|
+
<VisitAttributeTypeFields setErrorFetchingResources={setErrorFetchingResources} />
|
|
607
|
+
</div>
|
|
608
|
+
</section>
|
|
609
|
+
|
|
610
|
+
{/* Queue location and queue fields. These get shown when config.showServiceQueueFields is true,
|
|
611
|
+
or when the form is opened from the queues app */}
|
|
612
|
+
<section>
|
|
613
|
+
<div className={styles.sectionField}>
|
|
614
|
+
<VisitFormExtensionSlot
|
|
615
|
+
name="visit-form-bottom-slot"
|
|
616
|
+
patientUuid={patientUuid}
|
|
617
|
+
visitFormOpenedFrom={openedFrom}
|
|
618
|
+
setVisitFormCallbacks={setVisitFormCallbacks}
|
|
619
|
+
/>
|
|
620
|
+
</div>
|
|
621
|
+
</section>
|
|
622
|
+
</Stack>
|
|
623
|
+
</div>
|
|
624
|
+
<ButtonSet
|
|
625
|
+
className={classNames(styles.buttonSet, {
|
|
626
|
+
[styles.tablet]: isTablet,
|
|
627
|
+
[styles.desktop]: !isTablet,
|
|
628
|
+
})}
|
|
629
|
+
>
|
|
630
|
+
<Button className={styles.button} kind="secondary" onClick={handleDiscard}>
|
|
631
|
+
{t('discard', 'Discard')}
|
|
632
|
+
</Button>
|
|
633
|
+
<Button
|
|
634
|
+
className={styles.button}
|
|
635
|
+
disabled={isSubmitting || errorFetchingResources?.blockSavingForm}
|
|
636
|
+
kind="primary"
|
|
637
|
+
type="submit"
|
|
638
|
+
>
|
|
639
|
+
{isSubmitting ? (
|
|
640
|
+
<InlineLoading
|
|
641
|
+
className={styles.spinner}
|
|
642
|
+
description={
|
|
643
|
+
visitToEdit
|
|
644
|
+
? t('updatingVisit', 'Updating visit') + '...'
|
|
645
|
+
: t('startingVisit', 'Starting visit') + '...'
|
|
646
|
+
}
|
|
647
|
+
/>
|
|
648
|
+
) : (
|
|
649
|
+
<span>{visitToEdit ? t('updateVisit', 'Update visit') : t('startVisit', 'Start visit')}</span>
|
|
650
|
+
)}
|
|
651
|
+
</Button>
|
|
652
|
+
</ButtonSet>
|
|
653
|
+
</Form>
|
|
654
|
+
</FormProvider>
|
|
84
655
|
);
|
|
85
656
|
};
|
|
86
657
|
|
|
658
|
+
interface VisitFormExtensionSlotProps {
|
|
659
|
+
name: string;
|
|
660
|
+
patientUuid: string;
|
|
661
|
+
visitFormOpenedFrom: string;
|
|
662
|
+
setVisitFormCallbacks: React.Dispatch<React.SetStateAction<Map<string, VisitFormCallbacks>>>;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
type VisitFormExtensionState = {
|
|
666
|
+
patientUuid: string;
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* This function allows an extension to register callbacks for visit form submission.
|
|
670
|
+
* This callbacks can be used to make further requests. The callbacks should handle its own UI notification
|
|
671
|
+
* on success / failure, and its returned Promise MUST resolve on success and MUST reject on failure.
|
|
672
|
+
* @param callback
|
|
673
|
+
* @returns
|
|
674
|
+
*/
|
|
675
|
+
setVisitFormCallbacks(callbacks: VisitFormCallbacks);
|
|
676
|
+
|
|
677
|
+
visitFormOpenedFrom: string;
|
|
678
|
+
patientChartConfig: ChartConfig;
|
|
679
|
+
};
|
|
680
|
+
|
|
681
|
+
const VisitFormExtensionSlot: React.FC<VisitFormExtensionSlotProps> = React.memo(
|
|
682
|
+
({ name, patientUuid, visitFormOpenedFrom, setVisitFormCallbacks }) => {
|
|
683
|
+
const config = useConfig<ChartConfig>();
|
|
684
|
+
|
|
685
|
+
return (
|
|
686
|
+
<ExtensionSlot name={name}>
|
|
687
|
+
{(extension: AssignedExtension) => {
|
|
688
|
+
const state: VisitFormExtensionState = {
|
|
689
|
+
patientUuid,
|
|
690
|
+
setVisitFormCallbacks: (callbacks) => {
|
|
691
|
+
setVisitFormCallbacks((old) => {
|
|
692
|
+
return new Map(old).set(extension.id, callbacks);
|
|
693
|
+
});
|
|
694
|
+
},
|
|
695
|
+
visitFormOpenedFrom,
|
|
696
|
+
patientChartConfig: config,
|
|
697
|
+
};
|
|
698
|
+
return <Extension state={state} />;
|
|
699
|
+
}}
|
|
700
|
+
</ExtensionSlot>
|
|
701
|
+
);
|
|
702
|
+
},
|
|
703
|
+
);
|
|
704
|
+
|
|
87
705
|
export default VisitForm;
|