@openmrs/esm-patient-chart-app 11.3.0 → 11.3.1-patch.9310

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