@openmrs/esm-patient-chart-app 11.3.1-pre.9452 → 11.3.1-pre.9458

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 (86) hide show
  1. package/.turbo/turbo-build.log +11 -11
  2. package/dist/2540.js +1 -1
  3. package/dist/2540.js.map +1 -1
  4. package/dist/2761.js.map +1 -1
  5. package/dist/3697.js +1 -0
  6. package/dist/3697.js.map +1 -0
  7. package/dist/4300.js +1 -1
  8. package/dist/506.js +1 -1
  9. package/dist/506.js.map +1 -1
  10. package/dist/5827.js +1 -0
  11. package/dist/5827.js.map +1 -0
  12. package/dist/6411.js +1 -1
  13. package/dist/6411.js.map +1 -1
  14. package/dist/6568.js +1 -1
  15. package/dist/6568.js.map +1 -1
  16. package/dist/6924.js +1 -1
  17. package/dist/6924.js.map +1 -1
  18. package/dist/7818.js +1 -0
  19. package/dist/7818.js.map +1 -0
  20. package/dist/7822.js +1 -1
  21. package/dist/7822.js.map +1 -1
  22. package/dist/8260.js +1 -1
  23. package/dist/8260.js.map +1 -1
  24. package/dist/{3119.js → 8278.js} +1 -1
  25. package/dist/{3119.js.map → 8278.js.map} +1 -1
  26. package/dist/8454.js +1 -1
  27. package/dist/8454.js.map +1 -1
  28. package/dist/8709.js +1 -1
  29. package/dist/8709.js.map +1 -1
  30. package/dist/{6997.js → 9294.js} +1 -1
  31. package/dist/9294.js.map +1 -0
  32. package/dist/main.js +1 -1
  33. package/dist/main.js.map +1 -1
  34. package/dist/openmrs-esm-patient-chart-app.js +1 -1
  35. package/dist/openmrs-esm-patient-chart-app.js.buildmanifest.json +134 -158
  36. package/dist/openmrs-esm-patient-chart-app.js.map +1 -1
  37. package/dist/routes.json +1 -1
  38. package/package.json +2 -2
  39. package/src/actions-buttons/mark-patient-deceased.component.tsx +2 -2
  40. package/src/actions-buttons/start-visit.component.tsx +10 -5
  41. package/src/actions-buttons/start-visit.test.tsx +9 -5
  42. package/src/clinical-views/encounter-list/encounter-list-tabs.extension.tsx +1 -1
  43. package/src/clinical-views/utils/helpers.ts +2 -2
  44. package/src/index.ts +12 -18
  45. package/src/mark-patient-deceased/mark-patient-deceased-form.test.tsx +15 -9
  46. package/src/mark-patient-deceased/mark-patient-deceased-form.workspace.tsx +147 -138
  47. package/src/patient-chart/chart-review/dashboard-view.component.tsx +2 -2
  48. package/src/patient-chart/patient-chart.component.tsx +45 -42
  49. package/src/patient-chart/patient-chart.resources.ts +52 -10
  50. package/src/patient-chart/patient-chart.scss +0 -4
  51. package/src/routes.json +17 -6
  52. package/src/visit/hooks/useDeleteVisit.tsx +1 -1
  53. package/src/visit/start-visit-button.component.tsx +2 -2
  54. package/src/visit/start-visit-button.test.tsx +2 -2
  55. package/src/visit/visit-action-items/edit-visit-details.component.tsx +29 -8
  56. package/src/visit/visit-form/exported-visit-form.workspace.tsx +2 -10
  57. package/src/visit/visit-form/visit-form.test.tsx +27 -18
  58. package/src/visit/visit-form/visit-form.workspace.tsx +38 -675
  59. package/src/visit/visit-history-table/visit-actions-cell.component.tsx +3 -2
  60. package/src/visit/visit-history-table/visit-date-cell.component.tsx +1 -0
  61. package/src/visit/visit-history-table/visit-diagnoses-cell.component.tsx +1 -0
  62. package/src/visit/visit-history-table/visit-history-table.component.tsx +3 -2
  63. package/src/visit/visit-history-table/visit-type-cell.component.tsx +1 -0
  64. package/src/visit/visit-prompt/delete-visit-dialog.test.tsx +1 -1
  65. package/src/visit/visit-prompt/{end-visit-dialog.component.tsx → end-visit-dialog.modal.tsx} +1 -1
  66. package/src/visit/visit-prompt/end-visit-dialog.test.tsx +1 -1
  67. package/src/visit/visit-prompt/{start-visit-dialog.component.tsx → start-visit-dialog.modal.tsx} +10 -4
  68. package/src/visit/visit-prompt/start-visit-dialog.test.tsx +3 -3
  69. package/src/visit/visits-widget/current-visit-summary.extension.tsx +3 -3
  70. package/src/visit/visits-widget/past-visits-components/encounters-table/encounters-table.component.tsx +12 -35
  71. package/src/visit/visits-widget/visit-context/retrospective-data-date-time-picker/retrospective-date-time-picker.component.tsx +1 -0
  72. package/src/visit/visits-widget/visit-context/visit-context-switcher.modal.tsx +2 -2
  73. package/src/visit/visits-widget/visit-context/visit-context-switcher.test.tsx +22 -20
  74. package/src/visit/visits-widget/visit-detail-overview.component.tsx +3 -2
  75. package/src/visit/visits-widget/visit-detail-overview.test.tsx +4 -4
  76. package/translations/en.json +1 -1
  77. package/dist/276.js +0 -1
  78. package/dist/276.js.map +0 -1
  79. package/dist/3905.js +0 -1
  80. package/dist/3905.js.map +0 -1
  81. package/dist/5048.js +0 -1
  82. package/dist/5048.js.map +0 -1
  83. package/dist/6997.js.map +0 -1
  84. package/dist/7810.js +0 -1
  85. package/dist/7810.js.map +0 -1
  86. /package/src/visit/visit-prompt/{delete-visit-dialog.component.tsx → delete-visit-dialog.modal.tsx} +0 -0
@@ -1,705 +1,68 @@
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';
1
+ import React from 'react';
5
2
  import { useSWRConfig } from 'swr';
3
+ import { launchWorkspaceGroup2, useVisit, type Visit } from '@openmrs/esm-framework';
6
4
  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,
39
5
  invalidateVisitByUuid,
40
- invalidateVisitAndEncounterData,
41
- useActivePatientEnrollment,
6
+ type PatientWorkspace2DefinitionProps,
42
7
  usePatientChartStore,
43
- type DefaultPatientWorkspaceProps,
44
8
  } from '@openmrs/esm-patient-common-lib';
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';
67
-
68
- interface VisitAttribute {
69
- attributeType: string;
70
- value: string;
71
- }
72
-
73
- /**
74
- * Extra visit information provided by extensions via the extra-visit-attribute-slot.
75
- * Extensions can use this to add custom attributes to visits.
76
- */
77
- export interface ExtraVisitInfo {
78
- /**
79
- * Optional callback that extensions can provide to perform final
80
- * preparation or validation before the visit is created/updated.
81
- */
82
- handleCreateExtraVisitInfo?: () => void;
83
- /**
84
- * Array of visit attributes to be included in the visit payload.
85
- * Each attribute must have an attributeType (UUID) and a value (string).
86
- */
87
- attributes?: Array<VisitAttribute>;
88
- }
9
+ import ExportedVisitForm from './exported-visit-form.workspace';
89
10
 
90
- interface VisitFormProps extends DefaultPatientWorkspaceProps {
11
+ export interface VisitFormProps {
91
12
  /**
92
13
  * A unique string identifying where the visit form is opened from.
93
14
  * This string is passed into various extensions within the form to
94
15
  * affect how / if they should be rendered.
95
16
  */
96
- handleReturnToSearchList?: () => void;
97
17
  openedFrom: string;
98
18
  showPatientHeader?: boolean;
99
- visitToEdit?: Visit;
100
19
  }
20
+
101
21
  /**
102
22
  * This form is used for starting a new visit and for editing
103
23
  * an existing visit
24
+ *
25
+ * This workspace must only be used within the patient chart.
26
+ * @see exported-visit-form.workspace.tsx
104
27
  */
105
- const VisitForm: React.FC<VisitFormProps> = ({
106
- closeWorkspace,
107
- handleReturnToSearchList,
108
- openedFrom,
109
- patient,
110
- patientUuid,
111
- promptBeforeClosing,
112
- showPatientHeader = false,
113
- visitToEdit,
28
+ const VisitForm: React.FC<PatientWorkspace2DefinitionProps<VisitFormProps, {}>> = ({
29
+ workspaceProps: { openedFrom, showPatientHeader = false },
30
+ groupProps: { patient, patientUuid, visitContext },
31
+ ...rest
114
32
  }) => {
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);
125
33
  const { mutate: mutateActiveVisit } = useVisit(patientUuid);
126
34
  const { mutate: globalMutate } = useSWRConfig();
127
- const allVisitTypes = useConditionalVisitTypes();
128
35
  const { setVisitContext } = usePatientChartStore(patientUuid);
129
36
 
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
- };
37
+ const onVisitStarted = (visit: Visit) => {
38
+ // For visit creation, we need to update:
39
+ // 1. Current visit data (for critical components like visit summary, action buttons)
40
+ // 2. Visit history table (for the paginated visit list)
41
+ const mutateSavedOrUpdatedVisit = () => invalidateVisitByUuid(globalMutate, visit.uuid);
42
+ mutateActiveVisit();
43
+ setVisitContext?.(visit, mutateSavedOrUpdatedVisit);
279
44
 
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)
332
-
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,
45
+ launchWorkspaceGroup2('patient-chart', {
46
+ patient,
420
47
  patientUuid,
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
-
48
+ visitContext: visit,
49
+ mutateVisitContext: mutateSavedOrUpdatedVisit,
50
+ });
51
+ };
437
52
  return (
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>
53
+ <ExportedVisitForm
54
+ {...rest}
55
+ workspaceProps={{
56
+ openedFrom,
57
+ showPatientHeader,
58
+ onVisitStarted,
59
+ patient,
60
+ patientUuid,
61
+ visitContext,
62
+ }}
63
+ groupProps={{}}
64
+ />
655
65
  );
656
66
  };
657
67
 
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
-
705
68
  export default VisitForm;