@openmrs/esm-patient-chart-app 11.3.1-patch.9064 → 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.
Files changed (187) hide show
  1. package/.turbo/turbo-build.log +21 -21
  2. package/dist/1119.js +1 -1
  3. package/dist/1197.js +1 -1
  4. package/dist/2146.js +1 -1
  5. package/dist/2540.js +1 -0
  6. package/dist/2540.js.map +1 -0
  7. package/dist/2690.js +1 -1
  8. package/dist/276.js +1 -1
  9. package/dist/276.js.map +1 -1
  10. package/dist/2761.js +1 -1
  11. package/dist/2761.js.map +1 -1
  12. package/dist/2859.js +1 -1
  13. package/dist/2859.js.map +1 -1
  14. package/dist/3099.js +1 -1
  15. package/dist/3119.js +1 -1
  16. package/dist/3119.js.map +1 -1
  17. package/dist/3584.js +1 -1
  18. package/dist/3905.js +1 -1
  19. package/dist/4055.js +1 -1
  20. package/dist/4132.js +1 -1
  21. package/dist/4300.js +1 -1
  22. package/dist/4335.js +1 -1
  23. package/dist/439.js +1 -0
  24. package/dist/4618.js +1 -1
  25. package/dist/4652.js +1 -1
  26. package/dist/4718.js +1 -1
  27. package/dist/4754.js +1 -1
  28. package/dist/4944.js +1 -1
  29. package/dist/5048.js +1 -1
  30. package/dist/5048.js.map +1 -1
  31. package/dist/506.js +2 -0
  32. package/dist/506.js.map +1 -0
  33. package/dist/5173.js +1 -1
  34. package/dist/5205.js +1 -1
  35. package/dist/5241.js +1 -1
  36. package/dist/5442.js +1 -1
  37. package/dist/5661.js +1 -1
  38. package/dist/5670.js +1 -1
  39. package/dist/5670.js.map +1 -1
  40. package/dist/6022.js +1 -1
  41. package/dist/6336.js +1 -0
  42. package/dist/6336.js.map +1 -0
  43. package/dist/6411.js +1 -1
  44. package/dist/6468.js +1 -1
  45. package/dist/6529.js +1 -1
  46. package/dist/6568.js +1 -0
  47. package/dist/6568.js.map +1 -0
  48. package/dist/6589.js +1 -0
  49. package/dist/6679.js +1 -1
  50. package/dist/6840.js +1 -1
  51. package/dist/6859.js +1 -1
  52. package/dist/6924.js +1 -0
  53. package/dist/6924.js.map +1 -0
  54. package/dist/{4727.js → 6997.js} +1 -1
  55. package/dist/6997.js.map +1 -0
  56. package/dist/7097.js +1 -1
  57. package/dist/7159.js +1 -1
  58. package/dist/723.js +1 -1
  59. package/dist/7617.js +1 -1
  60. package/dist/7810.js +1 -0
  61. package/dist/7810.js.map +1 -0
  62. package/dist/7816.js +2 -0
  63. package/dist/7816.js.map +1 -0
  64. package/dist/7822.js +1 -1
  65. package/dist/7822.js.map +1 -1
  66. package/dist/795.js +1 -1
  67. package/dist/8163.js +1 -1
  68. package/dist/8260.js +1 -0
  69. package/dist/8260.js.map +1 -0
  70. package/dist/8349.js +1 -1
  71. package/dist/8371.js +1 -0
  72. package/dist/8454.js +1 -1
  73. package/dist/8454.js.map +1 -1
  74. package/dist/8618.js +1 -1
  75. package/dist/8709.js +1 -1
  76. package/dist/890.js +1 -1
  77. package/dist/9214.js +1 -1
  78. package/dist/9538.js +1 -1
  79. package/dist/9569.js +1 -1
  80. package/dist/986.js +1 -1
  81. package/dist/9879.js +1 -1
  82. package/dist/9895.js +1 -1
  83. package/dist/9900.js +1 -1
  84. package/dist/9913.js +1 -1
  85. package/dist/main.js +1 -1
  86. package/dist/main.js.map +1 -1
  87. package/dist/openmrs-esm-patient-chart-app.js +1 -1
  88. package/dist/openmrs-esm-patient-chart-app.js.buildmanifest.json +436 -321
  89. package/dist/openmrs-esm-patient-chart-app.js.map +1 -1
  90. package/dist/routes.json +1 -1
  91. package/package.json +3 -3
  92. package/src/actions-buttons/delete-visit.component.tsx +8 -3
  93. package/src/actions-buttons/stop-visit.component.tsx +1 -1
  94. package/src/clinical-views/encounter-list/{encounter-list-tabs.component.tsx → encounter-list-tabs.extension.tsx} +11 -7
  95. package/src/clinical-views/encounter-list/tag.component.test.tsx +306 -0
  96. package/src/clinical-views/encounter-list/tag.component.tsx +27 -28
  97. package/src/clinical-views/utils/encounter-list-config-builder.ts +19 -6
  98. package/src/clinical-views/utils/helpers.ts +3 -2
  99. package/src/config-schema.ts +0 -7
  100. package/src/index.ts +9 -4
  101. package/src/mark-patient-deceased/mark-patient-deceased-form.test.tsx +9 -4
  102. package/src/patient-banner-tags/{visit-attribute-tags.component.tsx → visit-attribute-tags.extension.tsx} +28 -15
  103. package/src/patient-banner-tags/visit-attribute-tags.scss +8 -0
  104. package/src/patient-chart/chart-review/dashboard-view.scss +5 -0
  105. package/src/patient-chart/patient-chart.component.tsx +39 -65
  106. package/src/patient-chart/patient-chart.resources.ts +108 -0
  107. package/src/routes.json +1 -1
  108. package/src/visit/hooks/useDeleteVisit.test.tsx +39 -42
  109. package/src/visit/hooks/useDeleteVisit.tsx +33 -17
  110. package/src/visit/visit-form/base-visit-type.component.tsx +30 -21
  111. package/src/visit/visit-form/exported-visit-form.workspace.tsx +700 -0
  112. package/src/visit/visit-form/visit-attribute-type.component.tsx +2 -1
  113. package/src/visit/visit-form/visit-form.resource.ts +2 -1
  114. package/src/visit/visit-form/visit-form.test.tsx +3 -9
  115. package/src/visit/visit-form/visit-form.workspace.tsx +53 -15
  116. package/src/visit/visit-prompt/delete-visit-dialog.component.tsx +10 -4
  117. package/src/visit/visit-prompt/delete-visit-dialog.test.tsx +20 -2
  118. package/src/visit/visit-prompt/end-visit-dialog.component.tsx +7 -1
  119. package/src/visit/visit-prompt/end-visit-dialog.test.tsx +19 -0
  120. package/src/visit/visits-widget/active-visit-buttons/active-visit-buttons.tsx +7 -6
  121. package/src/visit/visits-widget/{current-visit-summary.component.tsx → current-visit-summary.extension.tsx} +13 -20
  122. package/src/visit/visits-widget/current-visit-summary.test.tsx +45 -25
  123. package/src/visit/visits-widget/past-visits-components/encounters-table/encounters-table.component.tsx +4 -3
  124. package/src/visit/visits-widget/past-visits-components/encounters-table/encounters-table.resource.ts +0 -1
  125. package/src/visit/visits-widget/visit-context/retrospective-data-date-time-picker/retrospective-date-time-picker.component.tsx +6 -8
  126. package/src/visit/visits-widget/visit-context/{visit-context-header.component.tsx → visit-context-header.extension.tsx} +17 -15
  127. package/src/visit/visits-widget/visit-context/visit-context-header.test.tsx +35 -29
  128. package/src/visit/visits-widget/visit-context/visit-context-switcher.modal.tsx +13 -11
  129. package/src/visit/visits-widget/visit-context/visit-context-switcher.test.tsx +50 -30
  130. package/src/visit/visits-widget/visit.resource.tsx +1 -1
  131. package/translations/am.json +1 -2
  132. package/translations/ar.json +2 -3
  133. package/translations/ar_SY.json +1 -2
  134. package/translations/bn.json +1 -2
  135. package/translations/cs.json +196 -0
  136. package/translations/de.json +1 -2
  137. package/translations/en.json +2 -3
  138. package/translations/en_US.json +1 -2
  139. package/translations/es.json +2 -3
  140. package/translations/es_MX.json +1 -2
  141. package/translations/fr.json +22 -23
  142. package/translations/he.json +2 -3
  143. package/translations/hi.json +1 -2
  144. package/translations/hi_IN.json +1 -2
  145. package/translations/id.json +2 -3
  146. package/translations/it.json +2 -3
  147. package/translations/ka.json +2 -3
  148. package/translations/km.json +2 -3
  149. package/translations/ku.json +1 -2
  150. package/translations/ky.json +1 -2
  151. package/translations/lg.json +1 -2
  152. package/translations/ne.json +1 -2
  153. package/translations/pl.json +1 -2
  154. package/translations/pt.json +2 -3
  155. package/translations/pt_BR.json +2 -3
  156. package/translations/qu.json +1 -2
  157. package/translations/ro_RO.json +2 -3
  158. package/translations/ru_RU.json +1 -2
  159. package/translations/si.json +1 -2
  160. package/translations/sq.json +196 -0
  161. package/translations/sw.json +1 -2
  162. package/translations/sw_KE.json +1 -2
  163. package/translations/tr.json +1 -2
  164. package/translations/tr_TR.json +1 -2
  165. package/translations/uk.json +1 -2
  166. package/translations/uz.json +1 -2
  167. package/translations/uz@Latn.json +1 -2
  168. package/translations/uz_UZ.json +1 -2
  169. package/translations/vi.json +2 -3
  170. package/translations/zh.json +2 -3
  171. package/translations/zh_CN.json +2 -3
  172. package/translations/zh_TW.json +196 -0
  173. package/dist/1568.js +0 -2
  174. package/dist/1568.js.map +0 -1
  175. package/dist/2442.js +0 -1
  176. package/dist/2442.js.map +0 -1
  177. package/dist/2537.js +0 -1
  178. package/dist/2537.js.map +0 -1
  179. package/dist/3042.js +0 -1
  180. package/dist/3042.js.map +0 -1
  181. package/dist/4713.js +0 -1
  182. package/dist/4713.js.map +0 -1
  183. package/dist/4727.js.map +0 -1
  184. package/dist/6650.js +0 -2
  185. package/dist/6650.js.map +0 -1
  186. /package/dist/{1568.js.LICENSE.txt → 506.js.LICENSE.txt} +0 -0
  187. /package/dist/{6650.js.LICENSE.txt → 7816.js.LICENSE.txt} +0 -0
@@ -115,7 +115,8 @@ const AttributeTypeField: React.FC<AttributeTypeFieldProps> = ({
115
115
  } = useConceptAnswersForVisitAttributeType(data?.datatypeConfig);
116
116
  const { t } = useTranslation();
117
117
  const baseId = useId();
118
- const labelText = !required ? `${data?.display} (${t('optional', 'optional')})` : data?.display;
118
+ const displayText = data?.display?.trim() || data?.name?.trim() || '--';
119
+ const labelText = !required ? `${displayText} (${t('optional', 'optional')})` : displayText;
119
120
 
120
121
  const {
121
122
  formState: { errors },
@@ -1,5 +1,6 @@
1
1
  import { useMemo, useState } from 'react';
2
- import { useTranslation, type TFunction } from 'react-i18next';
2
+ import { useTranslation } from 'react-i18next';
3
+ import type { TFunction } from 'i18next';
3
4
  import dayjs from 'dayjs';
4
5
  import { z } from 'zod';
5
6
  import {
@@ -11,7 +11,6 @@ import {
11
11
  useConfig,
12
12
  useEmrConfiguration,
13
13
  useLocations,
14
- useVisitContextStore,
15
14
  useVisitTypes,
16
15
  type Visit,
17
16
  } from '@openmrs/esm-framework';
@@ -59,6 +58,7 @@ const visitAttributes = {
59
58
  const mockCloseWorkspace = jest.fn();
60
59
  const mockPromptBeforeClosing = jest.fn();
61
60
  const mockSetTitle = jest.fn();
61
+ const mockMutateVisitContext = jest.fn();
62
62
 
63
63
  const testProps = {
64
64
  openedFrom: 'test',
@@ -68,6 +68,8 @@ const testProps = {
68
68
  closeWorkspaceWithSavedChanges: mockCloseWorkspace,
69
69
  promptBeforeClosing: mockPromptBeforeClosing,
70
70
  setTitle: mockSetTitle,
71
+ visitContext: null,
72
+ mutateVisitContext: mockMutateVisitContext,
71
73
  };
72
74
 
73
75
  const mockSaveVisit = jest.mocked(saveVisit);
@@ -181,14 +183,6 @@ mockSaveVisit.mockResolvedValue({
181
183
  },
182
184
  } as unknown as FetchResponse<Visit>);
183
185
 
184
- jest.mocked(useVisitContextStore).mockReturnValue({
185
- manuallySetVisitUuid: null,
186
- patientUuid: null,
187
- setVisitContext: jest.fn(),
188
- mutateVisitCallbacks: {},
189
- mutateVisit: jest.fn(),
190
- });
191
-
192
186
  describe('Visit form', () => {
193
187
  beforeEach(() => {
194
188
  mockUseConfig.mockReturnValue({
@@ -36,12 +36,12 @@ import {
36
36
  } from '@openmrs/esm-framework';
37
37
  import {
38
38
  createOfflineVisitForPatient,
39
+ invalidateVisitByUuid,
39
40
  invalidateVisitAndEncounterData,
40
41
  useActivePatientEnrollment,
42
+ usePatientChartStore,
41
43
  type DefaultPatientWorkspaceProps,
42
44
  } from '@openmrs/esm-patient-common-lib';
43
- import { type ChartConfig } from '../../config-schema';
44
- import { useVisitAttributeTypes } from '../hooks/useVisitAttributeType';
45
45
  import { MemoizedRecommendedVisitType } from './recommended-visit-type.component';
46
46
  import {
47
47
  convertToDate,
@@ -61,8 +61,32 @@ import BaseVisitType from './base-visit-type.component';
61
61
  import LocationSelector from './location-selector.component';
62
62
  import VisitAttributeTypeFields from './visit-attribute-type.component';
63
63
  import VisitDateTimeSection from './visit-date-time.component';
64
+ import { useVisitAttributeTypes } from '../hooks/useVisitAttributeType';
65
+ import { type ChartConfig } from '../../config-schema';
64
66
  import styles from './visit-form.scss';
65
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
+ }
89
+
66
90
  interface VisitFormProps extends DefaultPatientWorkspaceProps {
67
91
  /**
68
92
  * A unique string identifying where the visit form is opened from.
@@ -98,16 +122,17 @@ const VisitForm: React.FC<VisitFormProps> = ({
98
122
  );
99
123
  const visitHeaderSlotState = useMemo(() => ({ patientUuid }), [patientUuid]);
100
124
  const { activePatientEnrollment, isLoading } = useActivePatientEnrollment(patientUuid);
101
- const { mutate: mutateCurrentVisit } = useVisit(patientUuid);
125
+ const { mutate: mutateActiveVisit } = useVisit(patientUuid);
102
126
  const { mutate: globalMutate } = useSWRConfig();
103
127
  const allVisitTypes = useConditionalVisitTypes();
128
+ const { setVisitContext } = usePatientChartStore(patientUuid);
104
129
 
105
130
  const [errorFetchingResources, setErrorFetchingResources] = useState<{
106
131
  blockSavingForm: boolean;
107
132
  } | null>(null);
108
133
  const { visitAttributeTypes } = useVisitAttributeTypes();
109
134
  const [visitFormCallbacks, setVisitFormCallbacks] = useVisitFormCallbacks();
110
- const [extraVisitInfo, setExtraVisitInfo] = useState(null);
135
+ const [extraVisitInfo, setExtraVisitInfo] = useState<ExtraVisitInfo | null>(null);
111
136
 
112
137
  const { visitFormSchema, defaultValues, firstEncounterDateTime, lastEncounterDateTime } =
113
138
  useVisitFormSchemaAndDefaultValues(visitToEdit);
@@ -135,6 +160,14 @@ const VisitForm: React.FC<VisitFormProps> = ({
135
160
  promptBeforeClosing(() => isDirty);
136
161
  }, [isDirty, promptBeforeClosing]);
137
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
+
138
171
  const handleVisitAttributes = useCallback(
139
172
  (visitAttributes: { [p: string]: string }, visitUuid: string) => {
140
173
  const existingVisitAttributeTypes =
@@ -241,12 +274,10 @@ const VisitForm: React.FC<VisitFormProps> = ({
241
274
  stopDatetime: hasStopTime ? stopDatetime : null,
242
275
  // The request throws 400 (Bad request) error when the patient is passed in the update payload for existing visit
243
276
  ...(!visitToEdit && { patient: patientUuid }),
244
- ...(config.showExtraVisitAttributesSlot && extraAttributes && { attributes: extraAttributes }),
277
+ ...(isValidVisitAttributesArray(extraAttributes) && { attributes: extraAttributes }),
245
278
  };
246
279
 
247
- if (config.showExtraVisitAttributesSlot) {
248
- handleCreateExtraVisitInfo?.();
249
- }
280
+ handleCreateExtraVisitInfo?.();
250
281
 
251
282
  const abortController = new AbortController();
252
283
  if (isOnline) {
@@ -299,12 +330,15 @@ const VisitForm: React.FC<VisitFormProps> = ({
299
330
  // 1. Current visit data (for critical components like visit summary, action buttons)
300
331
  // 2. Visit history table (for the paginated visit list)
301
332
 
302
- // Update current visit data for critical components (useVisit hook)
303
- mutateCurrentVisit();
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();
304
338
 
305
339
  // Use targeted SWR invalidation instead of global mutateVisit
306
340
  // This will invalidate visit history and encounter tables for this patient
307
- // (current visit is already updated with mutateCurrentVisit)
341
+ // (if visitContext is updated, it should have been invalidated with mutateSavedOrUpdatedVisit)
308
342
  invalidateVisitAndEncounterData(globalMutate, patientUuid);
309
343
 
310
344
  // handleVisitAttributes already has code to show error snackbar when attribute fails to update
@@ -343,9 +377,12 @@ const VisitForm: React.FC<VisitFormProps> = ({
343
377
  config.offlineVisitTypeUuid,
344
378
  payload.startDatetime,
345
379
  ).then(
346
- () => {
380
+ (visit) => {
347
381
  // Use same targeted approach for offline visits for consistency
348
- mutateCurrentVisit();
382
+ const mutateSavedOrUpdatedVisit = () => invalidateVisitByUuid(globalMutate, visit.uuid);
383
+ mutateActiveVisit();
384
+ setVisitContext?.(visit, mutateSavedOrUpdatedVisit);
385
+ visitToEdit && mutateSavedOrUpdatedVisit();
349
386
 
350
387
  // Also invalidate visit history and encounter tables
351
388
  invalidateVisitAndEncounterData(globalMutate, patientUuid);
@@ -375,16 +412,17 @@ const VisitForm: React.FC<VisitFormProps> = ({
375
412
  [
376
413
  closeWorkspace,
377
414
  config.offlineVisitTypeUuid,
378
- config.showExtraVisitAttributesSlot,
379
415
  extraVisitInfo,
380
416
  globalMutate,
381
417
  handleVisitAttributes,
382
418
  isOnline,
383
- mutateCurrentVisit,
419
+ setVisitContext,
384
420
  patientUuid,
385
421
  t,
386
422
  visitFormCallbacks,
387
423
  visitToEdit,
424
+ isValidVisitAttributesArray,
425
+ mutateActiveVisit,
388
426
  ],
389
427
  );
390
428
 
@@ -8,12 +8,18 @@ import styles from './start-visit-dialog.scss';
8
8
  interface DeleteVisitDialogProps {
9
9
  closeModal: () => void;
10
10
  patientUuid: string;
11
- visit: Visit;
11
+ activeVisit: Visit;
12
+ mutateActiveVisit: () => void;
12
13
  }
13
14
 
14
- const DeleteVisitDialog: React.FC<DeleteVisitDialogProps> = ({ closeModal, patientUuid, visit }) => {
15
+ const DeleteVisitDialog: React.FC<DeleteVisitDialogProps> = ({
16
+ closeModal,
17
+ mutateActiveVisit,
18
+ patientUuid,
19
+ activeVisit,
20
+ }) => {
15
21
  const { t } = useTranslation();
16
- const { isDeletingVisit, initiateDeletingVisit } = useDeleteVisit(visit, closeModal);
22
+ const { isDeletingVisit, initiateDeletingVisit } = useDeleteVisit(activeVisit, mutateActiveVisit, closeModal);
17
23
 
18
24
  return (
19
25
  <div>
@@ -24,7 +30,7 @@ const DeleteVisitDialog: React.FC<DeleteVisitDialogProps> = ({ closeModal, patie
24
30
  <ModalBody>
25
31
  <p className={styles.body}>
26
32
  {t('confirmDeleteVisitText', 'Deleting this {{visit}} will delete its associated encounters.', {
27
- visit: visit?.visitType?.display ?? t('visit', 'Visit'),
33
+ visit: activeVisit?.visitType?.display ?? t('visit', 'Visit'),
28
34
  })}
29
35
  </p>
30
36
  </ModalBody>
@@ -9,6 +9,7 @@ import DeleteVisitDialog from './delete-visit-dialog.component';
9
9
  const mockCloseModal = jest.fn();
10
10
  const mockOpenmrsFetch = jest.mocked(openmrsFetch);
11
11
  const mockShowSnackbar = jest.mocked(showSnackbar);
12
+ const mockActiveVisit = jest.fn();
12
13
 
13
14
  describe('Delete visit', () => {
14
15
  it('voids the visit and voids its associated encounters', async () => {
@@ -21,7 +22,14 @@ describe('Delete visit', () => {
21
22
 
22
23
  mockOpenmrsFetch.mockResolvedValue(response as FetchResponse);
23
24
 
24
- render(<DeleteVisitDialog visit={mockCurrentVisit} closeModal={mockCloseModal} patientUuid={mockPatient.id} />);
25
+ render(
26
+ <DeleteVisitDialog
27
+ activeVisit={mockCurrentVisit}
28
+ mutateActiveVisit={mockActiveVisit}
29
+ closeModal={mockCloseModal}
30
+ patientUuid={mockPatient.id}
31
+ />,
32
+ );
25
33
 
26
34
  const cancelButton = screen.getByRole('button', { name: /^cancel$/i });
27
35
  const deleteVisitButton = screen.getByRole('button', { name: /delete visit$/i });
@@ -46,6 +54,7 @@ describe('Delete visit', () => {
46
54
  subtitle: 'Facility Visit deleted successfully',
47
55
  }),
48
56
  );
57
+ expect(mockActiveVisit).toHaveBeenCalledTimes(1);
49
58
  });
50
59
 
51
60
  it('displays an error notification if there was problem with deleting a visit', async () => {
@@ -53,7 +62,14 @@ describe('Delete visit', () => {
53
62
 
54
63
  mockOpenmrsFetch.mockRejectedValueOnce({ message: 'Internal server error', status: 500 });
55
64
 
56
- render(<DeleteVisitDialog visit={mockCurrentVisit} closeModal={mockCloseModal} patientUuid={mockPatient.id} />);
65
+ render(
66
+ <DeleteVisitDialog
67
+ activeVisit={mockCurrentVisit}
68
+ mutateActiveVisit={mockActiveVisit}
69
+ closeModal={mockCloseModal}
70
+ patientUuid={mockPatient.id}
71
+ />,
72
+ );
57
73
 
58
74
  const cancelButton = screen.getByRole('button', { name: /^cancel$/i });
59
75
  const deleteVisitButton = screen.getByRole('button', { name: /delete visit$/i });
@@ -74,5 +90,7 @@ describe('Delete visit', () => {
74
90
  kind: 'error',
75
91
  title: 'Error deleting visit',
76
92
  });
93
+
94
+ expect(mockActiveVisit).toHaveBeenCalledTimes(1);
77
95
  });
78
96
  });
@@ -2,6 +2,7 @@ import React from 'react';
2
2
  import { useTranslation } from 'react-i18next';
3
3
  import { Button, ModalBody, ModalFooter, ModalHeader } from '@carbon/react';
4
4
  import { showSnackbar, updateVisit, useVisit } from '@openmrs/esm-framework';
5
+ import { usePatientChartStore } from '@openmrs/esm-patient-common-lib';
5
6
  import styles from './end-visit-dialog.scss';
6
7
 
7
8
  interface EndVisitDialogProps {
@@ -13,10 +14,13 @@ interface EndVisitDialogProps {
13
14
  * This modal shows up when user clicks on the "End visit" button in the action menu within the
14
15
  * patient banner. It should only show when the patient has an active visit. See stop-visit.component.tsx
15
16
  * for the button.
17
+ *
18
+ * This dialog uses the patient chart store and SHOULD only be mounted within the patient chart
16
19
  */
17
20
  const EndVisitDialog: React.FC<EndVisitDialogProps> = ({ patientUuid, closeModal }) => {
18
21
  const { t } = useTranslation();
19
22
  const { activeVisit, mutate } = useVisit(patientUuid);
23
+ const { visitContext, setVisitContext } = usePatientChartStore(patientUuid);
20
24
 
21
25
  const handleEndVisit = () => {
22
26
  if (activeVisit) {
@@ -31,7 +35,9 @@ const EndVisitDialog: React.FC<EndVisitDialogProps> = ({ patientUuid, closeModal
31
35
  mutate();
32
36
  window.dispatchEvent(new CustomEvent('queue-entry-updated'));
33
37
  closeModal();
34
-
38
+ if (visitContext?.uuid === activeVisit.uuid) {
39
+ setVisitContext(null, null);
40
+ }
35
41
  showSnackbar({
36
42
  isLowContrast: true,
37
43
  kind: 'success',
@@ -4,6 +4,7 @@ import { screen, render } from '@testing-library/react';
4
4
  import { showSnackbar, updateVisit, useVisit, type Visit, type FetchResponse } from '@openmrs/esm-framework';
5
5
  import { mockCurrentVisit } from '__mocks__';
6
6
  import EndVisitDialog from './end-visit-dialog.component';
7
+ import { usePatientChartStore } from '@openmrs/esm-patient-common-lib';
7
8
 
8
9
  const endVisitPayload = {
9
10
  stopDatetime: expect.any(Date),
@@ -15,6 +16,22 @@ const mockShowSnackbar = jest.mocked(showSnackbar);
15
16
  const mockUseVisit = jest.mocked(useVisit);
16
17
  const mockUpdateVisit = jest.mocked(updateVisit);
17
18
 
19
+ const mockUsePatientChartStore = jest.mocked(usePatientChartStore);
20
+ const mockSetVisitContext = jest.fn();
21
+
22
+ jest.mock('@openmrs/esm-patient-common-lib', () => ({
23
+ usePatientChartStore: jest.fn(),
24
+ }));
25
+
26
+ mockUsePatientChartStore.mockReturnValue({
27
+ patientUuid: 'patient-123',
28
+ patient: null,
29
+ visitContext: mockCurrentVisit,
30
+ mutateVisitContext: jest.fn(),
31
+ setPatient: jest.fn(),
32
+ setVisitContext: mockSetVisitContext,
33
+ });
34
+
18
35
  describe('End visit dialog', () => {
19
36
  beforeEach(() => {
20
37
  mockUseVisit.mockReturnValue({
@@ -66,6 +83,8 @@ describe('End visit dialog', () => {
66
83
  kind: 'success',
67
84
  title: 'Visit ended',
68
85
  });
86
+
87
+ expect(mockSetVisitContext).toHaveBeenCalledTimes(1);
69
88
  });
70
89
 
71
90
  test('displays an error snackbar if there was a problem ending a visit', async () => {
@@ -20,13 +20,14 @@ const ActiveVisitActions: React.FC<ActiveVisitActionsInterface> = ({ visit, pati
20
20
  const isTablet = layout === 'tablet';
21
21
  const isMobile = layout === 'phone';
22
22
 
23
- const launchAllergiesFormWorkspace = useLaunchWorkspaceRequiringVisit('patient-allergy-form-workspace');
24
- const launchAppointmentsFormWorkspace = useLaunchWorkspaceRequiringVisit('appointments-form-workspace');
25
- const launchClinicalFormsWorkspace = useLaunchWorkspaceRequiringVisit('clinical-forms-workspace');
26
- const launchConditionsFormWorkspace = useLaunchWorkspaceRequiringVisit('conditions-form-workspace');
27
- const launchOrderBasketFormWorkspace = useLaunchWorkspaceRequiringVisit('order-basket');
28
- const launchVisitNotesFormWorkspace = useLaunchWorkspaceRequiringVisit('visit-notes-form-workspace');
23
+ const launchAllergiesFormWorkspace = useLaunchWorkspaceRequiringVisit(patientUuid, 'patient-allergy-form-workspace');
24
+ const launchAppointmentsFormWorkspace = useLaunchWorkspaceRequiringVisit(patientUuid, 'appointments-form-workspace');
25
+ const launchClinicalFormsWorkspace = useLaunchWorkspaceRequiringVisit(patientUuid, 'clinical-forms-workspace');
26
+ const launchConditionsFormWorkspace = useLaunchWorkspaceRequiringVisit(patientUuid, 'conditions-form-workspace');
27
+ const launchOrderBasketFormWorkspace = useLaunchWorkspaceRequiringVisit(patientUuid, 'order-basket');
28
+ const launchVisitNotesFormWorkspace = useLaunchWorkspaceRequiringVisit(patientUuid, 'visit-notes-form-workspace');
29
29
  const launchVitalsAndBiometricsFormWorkspace = useLaunchWorkspaceRequiringVisit(
30
+ patientUuid,
30
31
  'patient-vitals-biometrics-form-workspace',
31
32
  );
32
33
 
@@ -1,8 +1,7 @@
1
1
  import React from 'react';
2
2
  import { useTranslation } from 'react-i18next';
3
- import { InlineLoading } from '@carbon/react';
4
- import { ErrorState, launchWorkspace, useVisit } from '@openmrs/esm-framework';
5
- import { CardHeader, EmptyState } from '@openmrs/esm-patient-common-lib';
3
+ import { launchWorkspace } from '@openmrs/esm-framework';
4
+ import { CardHeader, EmptyState, usePatientChartStore } from '@openmrs/esm-patient-common-lib';
6
5
  import VisitSummary from './past-visits-components/visit-summary.component';
7
6
  import styles from './current-visit-summary.scss';
8
7
 
@@ -10,29 +9,23 @@ interface CurrentVisitSummaryProps {
10
9
  patientUuid: string;
11
10
  }
12
11
 
12
+ /**
13
+ * This extension is not used in the refapp.
14
+ * This extension uses the patient chart store and SHOULD only be mounted within the patient chart
15
+ */
13
16
  const CurrentVisitSummary: React.FC<CurrentVisitSummaryProps> = ({ patientUuid }) => {
14
17
  const { t } = useTranslation();
15
- const { isLoading, currentVisit, error, isValidating } = useVisit(patientUuid);
18
+ const { patientUuid: storePatientUuid, visitContext } = usePatientChartStore(patientUuid);
16
19
 
17
- if (isLoading) {
18
- return (
19
- <InlineLoading
20
- status="active"
21
- iconDescription={t('loading', 'Loading')}
22
- description={t('loadingVisit', 'Loading current visit...')}
23
- />
24
- );
25
- }
26
-
27
- if (!!error) {
28
- return <ErrorState headerTitle={t('failedToLoadCurrentVisit', 'Failed loading current visit')} error={error} />;
20
+ if (patientUuid !== storePatientUuid) {
21
+ return null;
29
22
  }
30
23
 
31
- if (!currentVisit) {
24
+ if (!visitContext) {
32
25
  return (
33
26
  <EmptyState
34
27
  headerTitle={t('currentVisit', 'Current visit')}
35
- displayText={t('noActiveVisitMessage', 'active visit')}
28
+ displayText={t('activeVisits__lower', 'active visits')}
36
29
  launchForm={() =>
37
30
  launchWorkspace('start-visit-workspace-form', { openedFrom: 'patient-chart-current-visit-summary' })
38
31
  }
@@ -43,10 +36,10 @@ const CurrentVisitSummary: React.FC<CurrentVisitSummaryProps> = ({ patientUuid }
43
36
  return (
44
37
  <div className={styles.container}>
45
38
  <CardHeader title={t('currentVisit', 'Current visit')}>
46
- <span>{isValidating ? <InlineLoading /> : null}</span>
39
+ <span />
47
40
  </CardHeader>
48
41
  <div className={styles.visitSummaryCard}>
49
- <VisitSummary visit={currentVisit} patientUuid={patientUuid} />
42
+ <VisitSummary visit={visitContext} patientUuid={patientUuid} />
50
43
  </div>
51
44
  </div>
52
45
  );
@@ -1,34 +1,52 @@
1
1
  import React from 'react';
2
2
  import { render, screen } from '@testing-library/react';
3
- import { useVisit, getConfig } from '@openmrs/esm-framework';
4
- import { waitForLoadingToFinish } from 'tools';
5
- import CurrentVisitSummary from './current-visit-summary.component';
3
+ import { getConfig } from '@openmrs/esm-framework';
4
+ import { mockPatient, waitForLoadingToFinish } from 'tools';
5
+ import { usePatientChartStore } from '@openmrs/esm-patient-common-lib';
6
+ import CurrentVisitSummary from './current-visit-summary.extension';
6
7
 
7
8
  const mockGetConfig = jest.mocked(getConfig);
8
- const mockUseVisits = jest.mocked(useVisit);
9
+ const mockUsePatientChartStore = jest.mocked(usePatientChartStore);
10
+
11
+ jest.mock('@openmrs/esm-patient-common-lib', () => ({
12
+ ...jest.requireActual('@openmrs/esm-patient-common-lib'),
13
+ usePatientChartStore: jest.fn(),
14
+ }));
9
15
 
10
16
  describe('CurrentVisitSummary', () => {
11
17
  test('renders an empty state when there is no active visit', () => {
12
- mockUseVisits.mockReturnValueOnce({
13
- activeVisit: null,
14
- currentVisit: null,
15
- currentVisitIsRetrospective: false,
16
- error: null,
17
- isLoading: false,
18
- isValidating: false,
19
- mutate: jest.fn(),
18
+ mockUsePatientChartStore.mockReturnValue({
19
+ patientUuid: mockPatient.id,
20
+ patient: mockPatient,
21
+ visitContext: null,
22
+ mutateVisitContext: null,
23
+ setPatient: jest.fn(),
24
+ setVisitContext: jest.fn(),
20
25
  });
21
-
22
- render(<CurrentVisitSummary patientUuid="some-uuid" />);
26
+ render(<CurrentVisitSummary patientUuid={mockPatient.id} />);
23
27
  expect(screen.getByText(/current visit/i)).toBeInTheDocument();
24
- expect(screen.getByText('There are no active visit to display for this patient')).toBeInTheDocument();
28
+ expect(screen.getByText('There are no active visits to display for this patient')).toBeInTheDocument();
29
+ });
30
+
31
+ test('returns null when patientUuid does not match store patientUuid', () => {
32
+ mockUsePatientChartStore.mockReturnValue({
33
+ patientUuid: 'different-patient-id',
34
+ patient: mockPatient,
35
+ visitContext: null,
36
+ mutateVisitContext: null,
37
+ setPatient: jest.fn(),
38
+ setVisitContext: jest.fn(),
39
+ });
40
+ render(<CurrentVisitSummary patientUuid={mockPatient.id} />);
41
+ expect(screen.queryByText(/current visit/i)).not.toBeInTheDocument();
25
42
  });
26
43
 
27
- test('renders a visit summary when for the active visit', async () => {
44
+ test('renders a visit summary when visit context exists', async () => {
28
45
  mockGetConfig.mockResolvedValue({ htmlFormEntryForms: [] });
29
- mockUseVisits.mockReturnValueOnce({
30
- activeVisit: null,
31
- currentVisit: {
46
+ mockUsePatientChartStore.mockReturnValue({
47
+ patientUuid: mockPatient.id,
48
+ patient: mockPatient,
49
+ visitContext: {
32
50
  uuid: 'some-uuid',
33
51
  display: 'Visit 1',
34
52
  startDatetime: '2021-03-23T10:00:00.000+0300',
@@ -42,15 +60,17 @@ describe('CurrentVisitSummary', () => {
42
60
  display: 'Visit Type 1',
43
61
  },
44
62
  encounters: [],
63
+ patient: {
64
+ uuid: mockPatient.id,
65
+ display: 'Test Patient',
66
+ },
45
67
  },
46
- currentVisitIsRetrospective: false,
47
- error: null,
48
- isLoading: false,
49
- isValidating: false,
50
- mutate: jest.fn(),
68
+ mutateVisitContext: null,
69
+ setPatient: jest.fn(),
70
+ setVisitContext: jest.fn(),
51
71
  });
52
72
 
53
- render(<CurrentVisitSummary patientUuid="some-uuid" />);
73
+ render(<CurrentVisitSummary patientUuid={mockPatient.id} />);
54
74
 
55
75
  await waitForLoadingToFinish();
56
76
 
@@ -44,6 +44,7 @@ import {
44
44
  type HtmlFormEntryForm,
45
45
  launchFormEntryOrHtmlForms,
46
46
  invalidateVisitAndEncounterData,
47
+ usePatientChartStore,
47
48
  } from '@openmrs/esm-patient-common-lib';
48
49
  import { jsonSchemaResourceName } from '../../../../constants';
49
50
  import {
@@ -80,7 +81,7 @@ const EncountersTable: React.FC<EncountersTableProps> = ({
80
81
  const pageSizes = [10, 20, 30, 40, 50];
81
82
  const desktopLayout = isDesktop(useLayoutType());
82
83
  const session = useSession();
83
- const { mutate: mutateCurrentVisit } = useVisit(patientUuid);
84
+ const { mutateVisitContext } = usePatientChartStore(patientUuid);
84
85
  const { mutate } = useSWRConfig();
85
86
  const responsiveSize = desktopLayout ? 'sm' : 'lg';
86
87
  const { data: encounterTypes, isLoading: isLoadingEncounterTypes } = useEncounterTypes();
@@ -133,7 +134,7 @@ const EncountersTable: React.FC<EncountersTableProps> = ({
133
134
  deleteEncounter(encounterUuid, abortController)
134
135
  .then(() => {
135
136
  // Update current visit data for critical components
136
- mutateCurrentVisit();
137
+ mutateVisitContext?.();
137
138
 
138
139
  // Also invalidate visit history and encounter tables since the encounter was deleted
139
140
  invalidateVisitAndEncounterData(mutate, patientUuid);
@@ -160,7 +161,7 @@ const EncountersTable: React.FC<EncountersTableProps> = ({
160
161
  },
161
162
  });
162
163
  },
163
- [mutate, mutateCurrentVisit, patientUuid, t],
164
+ [mutate, mutateVisitContext, patientUuid, t],
164
165
  );
165
166
 
166
167
  if (isLoadingEncounterTypes || isLoading) {
@@ -8,7 +8,6 @@ import {
8
8
  type Encounter,
9
9
  type EncounterType,
10
10
  type Obs,
11
- type OpenmrsResource,
12
11
  useOpenmrsFetchAll,
13
12
  useOpenmrsPagination,
14
13
  } from '@openmrs/esm-framework';
@@ -1,10 +1,9 @@
1
1
  import { SelectItem, TimePickerSelect, TimePicker, Checkbox } from '@carbon/react';
2
- import { OpenmrsDatePicker, ResponsiveWrapper, useFeatureFlag, useVisit } from '@openmrs/esm-framework';
2
+ import { OpenmrsDatePicker, ResponsiveWrapper, useFeatureFlag, type Visit } from '@openmrs/esm-framework';
3
3
  import React, { useEffect, useState } from 'react';
4
4
  import { type Control, Controller, useForm } from 'react-hook-form';
5
5
  import { useTranslation } from 'react-i18next';
6
6
  import styles from './restrospective-date-time-picker.scss';
7
- import { useSystemVisitSetting } from '@openmrs/esm-patient-common-lib';
8
7
 
9
8
  type FormValues = {
10
9
  retrospectiveDate: Date;
@@ -13,23 +12,22 @@ type FormValues = {
13
12
  };
14
13
 
15
14
  type RetrospectiveDateTimePickerProps = {
16
- patientUuid: string;
15
+ visitContext: Visit;
17
16
  control?: Control<FormValues>;
18
17
  onChange?: (data: FormValues) => void;
19
18
  };
20
19
 
21
20
  const RetrospectiveDateTimePicker = ({
22
- patientUuid,
21
+ visitContext,
23
22
  control: propControl,
24
23
  onChange,
25
24
  }: RetrospectiveDateTimePickerProps) => {
26
25
  const { t } = useTranslation();
27
26
  const isRdeEnabled = useFeatureFlag('rde');
28
27
 
29
- const { currentVisit } = useVisit(patientUuid);
30
- const isActiveVisit = !Boolean(currentVisit && currentVisit.stopDatetime);
31
- const maxDate = currentVisit?.stopDatetime;
32
- const minDate = currentVisit?.startDatetime;
28
+ const isActiveVisit = !Boolean(visitContext && visitContext.stopDatetime);
29
+ const maxDate = visitContext?.stopDatetime;
30
+ const minDate = visitContext?.startDatetime;
33
31
 
34
32
  const [manuallyEnableDateTimePicker, setManuallyEnableDateTimePicker] = useState<boolean>(false);
35
33