@openmrs/esm-patient-orders-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 (170) hide show
  1. package/.turbo/turbo-build.log +26 -23
  2. package/dist/1119.js +1 -1
  3. package/dist/1197.js +1 -1
  4. package/dist/1253.js +1 -0
  5. package/dist/1253.js.map +1 -0
  6. package/dist/1268.js +2 -0
  7. package/dist/1268.js.map +1 -0
  8. package/dist/2146.js +1 -1
  9. package/dist/2690.js +1 -1
  10. package/dist/3099.js +1 -1
  11. package/dist/3584.js +1 -1
  12. package/dist/3685.js +1 -1
  13. package/dist/375.js +1 -0
  14. package/dist/375.js.map +1 -0
  15. package/dist/4055.js +1 -1
  16. package/dist/4132.js +1 -1
  17. package/dist/4300.js +1 -1
  18. package/dist/4335.js +1 -1
  19. package/dist/4341.js +1 -0
  20. package/dist/4341.js.map +1 -0
  21. package/dist/4618.js +1 -1
  22. package/dist/4652.js +1 -1
  23. package/dist/4687.js +1 -0
  24. package/dist/4687.js.map +1 -0
  25. package/dist/4937.js +1 -1
  26. package/dist/4937.js.map +1 -1
  27. package/dist/4944.js +1 -1
  28. package/dist/5173.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/6022.js +1 -1
  35. package/dist/6336.js +1 -0
  36. package/dist/6336.js.map +1 -0
  37. package/dist/6364.js +1 -0
  38. package/dist/6364.js.map +1 -0
  39. package/dist/6411.js +1 -1
  40. package/dist/6468.js +1 -1
  41. package/dist/6473.js +1 -0
  42. package/dist/6473.js.map +1 -0
  43. package/dist/6542.js +1 -1
  44. package/dist/6679.js +1 -1
  45. package/dist/6840.js +1 -1
  46. package/dist/6859.js +1 -1
  47. package/dist/7097.js +1 -1
  48. package/dist/7159.js +1 -1
  49. package/dist/723.js +1 -1
  50. package/dist/7617.js +1 -1
  51. package/dist/7657.js +2 -0
  52. package/dist/7657.js.map +1 -0
  53. package/dist/795.js +1 -1
  54. package/dist/8154.js +1 -1
  55. package/dist/8154.js.map +1 -1
  56. package/dist/8163.js +1 -1
  57. package/dist/8349.js +1 -1
  58. package/dist/8416.js +1 -0
  59. package/dist/8416.js.map +1 -0
  60. package/dist/8618.js +1 -1
  61. package/dist/890.js +1 -1
  62. package/dist/9214.js +1 -1
  63. package/dist/9538.js +1 -1
  64. package/dist/9569.js +1 -1
  65. package/dist/986.js +1 -1
  66. package/dist/9879.js +1 -1
  67. package/dist/9895.js +1 -1
  68. package/dist/9900.js +1 -1
  69. package/dist/9913.js +1 -1
  70. package/dist/main.js +1 -1
  71. package/dist/main.js.map +1 -1
  72. package/dist/openmrs-esm-patient-orders-app.js +1 -1
  73. package/dist/openmrs-esm-patient-orders-app.js.buildmanifest.json +307 -330
  74. package/dist/openmrs-esm-patient-orders-app.js.map +1 -1
  75. package/dist/routes.json +1 -1
  76. package/package.json +5 -4
  77. package/src/api/api.ts +16 -30
  78. package/src/components/general-order-table.scss +4 -1
  79. package/src/components/order-details-table.component.tsx +88 -54
  80. package/src/components/test-order.scss +4 -2
  81. package/src/dashboard.meta.ts +3 -1
  82. package/src/index.ts +12 -9
  83. package/src/lab-results/{lab-results-form.component.tsx → exported-lab-results-form.workspace.tsx} +68 -67
  84. package/src/lab-results/lab-results-form-field.component.tsx +8 -5
  85. package/src/lab-results/lab-results-form.test.tsx +23 -21
  86. package/src/lab-results/lab-results-form.workspace.tsx +25 -0
  87. package/src/lab-results/print-results/print-modal/print-results-modal.tsx +5 -1
  88. package/src/order-basket/exported-order-basket.workspace.tsx +54 -0
  89. package/src/order-basket/general-order-type/{orderable-concept-search/orderable-concept-search.workspace.tsx → add-general-order/add-general-order.component.tsx} +89 -80
  90. package/src/order-basket/general-order-type/add-general-order/add-general-order.workspace.tsx +35 -0
  91. package/src/order-basket/general-order-type/add-general-order/exported-add-general-order.workspace.tsx +32 -0
  92. package/src/order-basket/general-order-type/{orderable-concept-search → add-general-order}/search-results.component.tsx +21 -15
  93. package/src/order-basket/general-order-type/general-order-form/general-order-form.component.tsx +70 -23
  94. package/src/order-basket/general-order-type/{general-order-type.component.tsx → general-order-panel.component.tsx} +35 -52
  95. package/src/order-basket/general-order-type/resources.ts +4 -3
  96. package/src/order-basket/order-basket.component.tsx +213 -0
  97. package/src/order-basket/order-basket.workspace.tsx +35 -235
  98. package/src/order-basket-action-button/order-basket-action-button.component.tsx +35 -0
  99. package/src/order-basket-action-button/order-basket-action-button.test.tsx +28 -61
  100. package/src/order-cancellation-form/cancel-order-form.component.tsx +82 -85
  101. package/src/routes.json +19 -27
  102. package/src/utils/index.ts +15 -3
  103. package/translations/am.json +2 -0
  104. package/translations/ar.json +2 -0
  105. package/translations/ar_SY.json +2 -0
  106. package/translations/bn.json +2 -0
  107. package/translations/de.json +2 -0
  108. package/translations/en.json +5 -9
  109. package/translations/en_US.json +2 -0
  110. package/translations/es.json +2 -0
  111. package/translations/es_MX.json +2 -0
  112. package/translations/fr.json +5 -3
  113. package/translations/he.json +2 -0
  114. package/translations/hi.json +2 -0
  115. package/translations/hi_IN.json +2 -0
  116. package/translations/id.json +2 -0
  117. package/translations/it.json +8 -6
  118. package/translations/ka.json +2 -0
  119. package/translations/km.json +2 -0
  120. package/translations/ku.json +2 -0
  121. package/translations/ky.json +2 -0
  122. package/translations/lg.json +2 -0
  123. package/translations/ne.json +2 -0
  124. package/translations/pl.json +2 -0
  125. package/translations/pt.json +2 -0
  126. package/translations/pt_BR.json +2 -0
  127. package/translations/qu.json +2 -0
  128. package/translations/ro_RO.json +2 -0
  129. package/translations/ru_RU.json +2 -0
  130. package/translations/si.json +2 -0
  131. package/translations/sw.json +2 -0
  132. package/translations/sw_KE.json +2 -0
  133. package/translations/tr.json +2 -0
  134. package/translations/tr_TR.json +2 -0
  135. package/translations/uk.json +2 -0
  136. package/translations/uz.json +2 -0
  137. package/translations/uz@Latn.json +2 -0
  138. package/translations/uz_UZ.json +2 -0
  139. package/translations/vi.json +2 -0
  140. package/translations/zh.json +2 -0
  141. package/translations/zh_CN.json +2 -0
  142. package/dist/1571.js +0 -1
  143. package/dist/1571.js.map +0 -1
  144. package/dist/2537.js +0 -1
  145. package/dist/2537.js.map +0 -1
  146. package/dist/4051.js +0 -1
  147. package/dist/4051.js.map +0 -1
  148. package/dist/4918.js +0 -1
  149. package/dist/4918.js.map +0 -1
  150. package/dist/5048.js +0 -1
  151. package/dist/5048.js.map +0 -1
  152. package/dist/6432.js +0 -1
  153. package/dist/6432.js.map +0 -1
  154. package/dist/717.js +0 -1
  155. package/dist/717.js.map +0 -1
  156. package/dist/7202.js +0 -1
  157. package/dist/7202.js.map +0 -1
  158. package/dist/7337.js +0 -2
  159. package/dist/7337.js.map +0 -1
  160. package/dist/7817.js +0 -2
  161. package/dist/7817.js.map +0 -1
  162. package/dist/8625.js +0 -1
  163. package/dist/8625.js.map +0 -1
  164. package/dist/8960.js +0 -1
  165. package/dist/8960.js.map +0 -1
  166. package/src/order-basket-action-button/order-basket-action-button.extension.tsx +0 -23
  167. /package/dist/{7817.js.LICENSE.txt → 1268.js.LICENSE.txt} +0 -0
  168. /package/dist/{7337.js.LICENSE.txt → 7657.js.LICENSE.txt} +0 -0
  169. /package/src/order-basket/general-order-type/{orderable-concept-search → add-general-order}/orderable-concept-search.scss +0 -0
  170. /package/src/order-basket/general-order-type/{orderable-concept-search → add-general-order}/search-results.scss +0 -0
@@ -0,0 +1,213 @@
1
+ import React, { useCallback, useMemo, useState } from 'react';
2
+ import classNames from 'classnames';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { Button, ButtonSet, InlineLoading, InlineNotification } from '@carbon/react';
5
+ import {
6
+ ExtensionSlot,
7
+ useConfig,
8
+ useLayoutType,
9
+ useSession,
10
+ type Visit,
11
+ Workspace2,
12
+ type Workspace2DefinitionProps,
13
+ } from '@openmrs/esm-framework';
14
+ import {
15
+ invalidateVisitAndEncounterData,
16
+ type OrderBasketExtensionProps,
17
+ type OrderBasketItem,
18
+ postOrders,
19
+ postOrdersOnNewEncounter,
20
+ showOrderSuccessToast,
21
+ useMutatePatientOrders,
22
+ useOrderBasket,
23
+ } from '@openmrs/esm-patient-common-lib';
24
+ import { useSWRConfig } from 'swr';
25
+ import { type ConfigObject } from '../config-schema';
26
+ import { useOrderEncounter } from '../api/api';
27
+ import GeneralOrderPanel from './general-order-type/general-order-panel.component';
28
+ import styles from './order-basket.scss';
29
+
30
+ interface OrderBasketProps {
31
+ patientUuid: string;
32
+ patient: fhir.Patient;
33
+ visitContext: Visit;
34
+ mutateVisitContext: () => void;
35
+ closeWorkspace: Workspace2DefinitionProps['closeWorkspace'];
36
+ orderBasketExtensionProps: OrderBasketExtensionProps;
37
+ }
38
+
39
+ const OrderBasket: React.FC<OrderBasketProps> = ({
40
+ patientUuid,
41
+ patient,
42
+ visitContext,
43
+ mutateVisitContext,
44
+ closeWorkspace,
45
+ orderBasketExtensionProps,
46
+ }) => {
47
+ const { t } = useTranslation();
48
+ const isTablet = useLayoutType() === 'tablet';
49
+ const config = useConfig<ConfigObject>();
50
+ const session = useSession();
51
+ const { orders, clearOrders } = useOrderBasket(patient);
52
+ const [ordersWithErrors, setOrdersWithErrors] = useState<OrderBasketItem[]>([]);
53
+ const {
54
+ visitRequired,
55
+ isLoading: isLoadingEncounterUuid,
56
+ encounterUuid,
57
+ error: errorFetchingEncounterUuid,
58
+ mutate: mutateEncounterUuid,
59
+ } = useOrderEncounter(patientUuid, visitContext, mutateVisitContext, config.orderEncounterType);
60
+ const [isSavingOrders, setIsSavingOrders] = useState(false);
61
+ const [creatingEncounterError, setCreatingEncounterError] = useState('');
62
+ const { mutate: mutateOrders } = useMutatePatientOrders(patientUuid);
63
+ const { mutate } = useSWRConfig();
64
+
65
+ const handleSave = useCallback(async () => {
66
+ const abortController = new AbortController();
67
+ setCreatingEncounterError('');
68
+ let orderEncounterUuid = encounterUuid;
69
+ setIsSavingOrders(true);
70
+ // If there's no encounter present, create an encounter along with the orders.
71
+ if (!orderEncounterUuid) {
72
+ try {
73
+ await postOrdersOnNewEncounter(
74
+ patientUuid,
75
+ config?.orderEncounterType,
76
+ visitRequired ? visitContext : null,
77
+ session?.sessionLocation?.uuid,
78
+ abortController,
79
+ );
80
+ await closeWorkspace({ discardUnsavedChanges: true });
81
+ mutateEncounterUuid();
82
+ // Only revalidate current visit since orders create new encounters
83
+ mutateVisitContext?.();
84
+ invalidateVisitAndEncounterData(mutate, patientUuid);
85
+ clearOrders();
86
+ await mutateOrders();
87
+ showOrderSuccessToast(t, orders);
88
+ } catch (e) {
89
+ console.error(e);
90
+ setCreatingEncounterError(
91
+ e.responseBody?.error?.message ||
92
+ t('tryReopeningTheWorkspaceAgain', 'Please try launching the workspace again'),
93
+ );
94
+ }
95
+ } else {
96
+ const erroredItems = await postOrders(patientUuid, orderEncounterUuid, abortController);
97
+ clearOrders({ exceptThoseMatching: (item) => erroredItems.map((e) => e.display).includes(item.display) });
98
+ // Only revalidate current visit since orders create new encounters
99
+ mutateVisitContext?.();
100
+ await mutateOrders();
101
+ invalidateVisitAndEncounterData(mutate, patientUuid);
102
+
103
+ if (erroredItems.length == 0) {
104
+ await closeWorkspace({ discardUnsavedChanges: true });
105
+ showOrderSuccessToast(t, orders);
106
+ } else {
107
+ setOrdersWithErrors(erroredItems);
108
+ }
109
+ clearOrders({ exceptThoseMatching: (item) => erroredItems.map((e) => e.display).includes(item.display) });
110
+ // Only revalidate current visit since orders create new encounters
111
+ mutateVisitContext?.();
112
+ await mutateOrders();
113
+ invalidateVisitAndEncounterData(mutate, patientUuid);
114
+ }
115
+ setIsSavingOrders(false);
116
+ return () => abortController.abort();
117
+ }, [
118
+ visitContext,
119
+ visitRequired,
120
+ clearOrders,
121
+ closeWorkspace,
122
+ config,
123
+ encounterUuid,
124
+ mutateEncounterUuid,
125
+ mutateOrders,
126
+ mutateVisitContext,
127
+ orders,
128
+ patientUuid,
129
+ session,
130
+ t,
131
+ mutate,
132
+ ]);
133
+
134
+ const handleCancel = useCallback(() => {
135
+ closeWorkspace().then((didClose) => {
136
+ if (didClose) {
137
+ clearOrders();
138
+ }
139
+ });
140
+ }, [clearOrders, closeWorkspace]);
141
+
142
+ return (
143
+ <Workspace2 title={t('orderBasketWorkspaceTitle', 'Order Basket')} hasUnsavedChanges={!!orders.length}>
144
+ <div id="order-basket" className={styles.container}>
145
+ <ExtensionSlot name="visit-context-header-slot" state={{ patientUuid }} />
146
+ <div className={styles.orderBasketContainer}>
147
+ <ExtensionSlot
148
+ className={classNames(styles.orderBasketSlot, {
149
+ [styles.orderBasketSlotTablet]: isTablet,
150
+ })}
151
+ name="order-basket-slot"
152
+ state={orderBasketExtensionProps as any}
153
+ />
154
+ {config?.orderTypes?.length > 0 &&
155
+ config.orderTypes.map((orderType) => (
156
+ <div className={styles.orderPanel} key={orderType.orderTypeUuid}>
157
+ <GeneralOrderPanel
158
+ {...orderType}
159
+ launchGeneralOrderForm={orderBasketExtensionProps.launchGeneralOrderForm}
160
+ patient={patient}
161
+ />
162
+ </div>
163
+ ))}
164
+ </div>
165
+ <div>
166
+ {(creatingEncounterError || errorFetchingEncounterUuid) && (
167
+ <InlineNotification
168
+ kind="error"
169
+ title={t('tryReopeningTheWorkspaceAgain', 'Please try launching the workspace again')}
170
+ subtitle={creatingEncounterError}
171
+ lowContrast={true}
172
+ className={styles.inlineNotification}
173
+ />
174
+ )}
175
+ {ordersWithErrors.map((order) => (
176
+ <InlineNotification
177
+ lowContrast
178
+ kind="error"
179
+ title={t('saveDrugOrderFailed', 'Error ordering {{orderName}}', { orderName: order.display })}
180
+ subtitle={order.extractedOrderError?.fieldErrors?.join(', ')}
181
+ className={styles.inlineNotification}
182
+ />
183
+ ))}
184
+ <ButtonSet className={styles.buttonSet}>
185
+ <Button className={styles.actionButton} kind="secondary" onClick={handleCancel}>
186
+ {t('cancel', 'Cancel')}
187
+ </Button>
188
+ <Button
189
+ className={styles.actionButton}
190
+ kind="primary"
191
+ onClick={handleSave}
192
+ disabled={
193
+ isSavingOrders ||
194
+ !orders?.length ||
195
+ isLoadingEncounterUuid ||
196
+ (visitRequired && !visitContext) ||
197
+ orders?.some(({ isOrderIncomplete }) => isOrderIncomplete)
198
+ }
199
+ >
200
+ {isSavingOrders ? (
201
+ <InlineLoading description={t('saving', 'Saving') + '...'} />
202
+ ) : (
203
+ <span>{t('signAndClose', 'Sign and close')}</span>
204
+ )}
205
+ </Button>
206
+ </ButtonSet>
207
+ </div>
208
+ </div>
209
+ </Workspace2>
210
+ );
211
+ };
212
+
213
+ export default OrderBasket;
@@ -1,246 +1,46 @@
1
- import React, { useCallback, useEffect, useState } from 'react';
2
- import classNames from 'classnames';
3
- import { type TFunction, useTranslation } from 'react-i18next';
4
- import { ActionableNotification, Button, ButtonSet, InlineLoading, InlineNotification } from '@carbon/react';
1
+ import React, { useMemo } from 'react';
5
2
  import {
6
- ExtensionSlot,
7
- showModal,
8
- showSnackbar,
9
- useConfig,
10
- useLayoutType,
11
- useSession,
12
- useVisit,
13
- } from '@openmrs/esm-framework';
14
- import {
15
- type DefaultPatientWorkspaceProps,
3
+ type OrderBasketExtensionProps,
4
+ type OrderBasketWindowProps,
16
5
  type OrderBasketItem,
17
- invalidateVisitAndEncounterData,
18
- postOrders,
19
- postOrdersOnNewEncounter,
20
- useOrderBasket,
21
- useVisitOrOfflineVisit,
6
+ type PatientWorkspace2DefinitionProps,
22
7
  } from '@openmrs/esm-patient-common-lib';
23
- import { useSWRConfig } from 'swr';
24
- import { type ConfigObject } from '../config-schema';
25
- import { useMutatePatientOrders, useOrderEncounter } from '../api/api';
26
- import GeneralOrderType from './general-order-type/general-order-type.component';
27
- import styles from './order-basket.scss';
8
+ import OrderBasket from './order-basket.component';
28
9
 
29
- const OrderBasket: React.FC<DefaultPatientWorkspaceProps> = ({
30
- patientUuid,
10
+ const OrderBasketWorkspace: React.FC<PatientWorkspace2DefinitionProps<{}, OrderBasketWindowProps>> = ({
11
+ groupProps: { patientUuid, patient, visitContext, mutateVisitContext },
31
12
  closeWorkspace,
32
- closeWorkspaceWithSavedChanges,
33
- promptBeforeClosing,
13
+ launchChildWorkspace,
34
14
  }) => {
35
- const { t } = useTranslation();
36
- const isTablet = useLayoutType() === 'tablet';
37
- const config = useConfig<ConfigObject>();
38
- const session = useSession();
39
- const { currentVisit } = useVisitOrOfflineVisit(patientUuid);
40
- const { orders, clearOrders } = useOrderBasket();
41
- const [ordersWithErrors, setOrdersWithErrors] = useState<OrderBasketItem[]>([]);
42
- const {
43
- visitRequired,
44
- isLoading: isLoadingEncounterUuid,
45
- encounterUuid,
46
- error: errorFetchingEncounterUuid,
47
- mutate: mutateEncounterUuid,
48
- } = useOrderEncounter(patientUuid, config.orderEncounterType);
49
- const [isSavingOrders, setIsSavingOrders] = useState(false);
50
- const [creatingEncounterError, setCreatingEncounterError] = useState('');
51
- const { mutate: mutateOrders } = useMutatePatientOrders(patientUuid);
52
- const { mutate: mutateCurrentVisit } = useVisit(patientUuid);
53
- const { mutate } = useSWRConfig();
54
-
55
- useEffect(() => {
56
- promptBeforeClosing(() => !!orders.length);
57
- }, [orders, promptBeforeClosing]);
58
-
59
- const openStartVisitDialog = useCallback(() => {
60
- const dispose = showModal('start-visit-dialog', {
61
- patientUuid,
62
- closeModal: () => dispose(),
63
- });
64
- }, [patientUuid]);
65
-
66
- const handleSave = useCallback(async () => {
67
- const abortController = new AbortController();
68
- setCreatingEncounterError('');
69
- let orderEncounterUuid = encounterUuid;
70
- setIsSavingOrders(true);
71
- // If there's no encounter present, create an encounter along with the orders.
72
- if (!orderEncounterUuid) {
73
- try {
74
- await postOrdersOnNewEncounter(
75
- patientUuid,
76
- config?.orderEncounterType,
77
- visitRequired ? currentVisit : null,
78
- session?.sessionLocation?.uuid,
79
- abortController,
80
- );
81
- mutateEncounterUuid();
82
- // Only revalidate current visit since orders create new encounters
83
- mutateCurrentVisit();
84
- invalidateVisitAndEncounterData(mutate, patientUuid);
85
- clearOrders();
86
- await mutateOrders();
87
-
88
- closeWorkspaceWithSavedChanges();
89
- showOrderSuccessToast(t, orders);
90
- } catch (e) {
91
- console.error(e);
92
- setCreatingEncounterError(
93
- e.responseBody?.error?.message ||
94
- t('tryReopeningTheWorkspaceAgain', 'Please try launching the workspace again'),
95
- );
96
- }
97
- } else {
98
- const erroredItems = await postOrders(patientUuid, orderEncounterUuid, abortController);
99
- clearOrders({ exceptThoseMatching: (item) => erroredItems.map((e) => e.display).includes(item.display) });
100
- // Only revalidate current visit since orders create new encounters
101
- mutateCurrentVisit();
102
- await mutateOrders();
103
- invalidateVisitAndEncounterData(mutate, patientUuid);
104
-
105
- if (erroredItems.length == 0) {
106
- closeWorkspaceWithSavedChanges();
107
- showOrderSuccessToast(t, orders);
108
- } else {
109
- setOrdersWithErrors(erroredItems);
110
- }
111
- }
112
- setIsSavingOrders(false);
113
- return () => abortController.abort();
114
- }, [
115
- currentVisit,
116
- visitRequired,
117
- clearOrders,
118
- closeWorkspaceWithSavedChanges,
119
- config,
120
- encounterUuid,
121
- mutateEncounterUuid,
122
- mutateOrders,
123
- mutateCurrentVisit,
124
- orders,
125
- patientUuid,
126
- session,
127
- t,
128
- mutate,
129
- ]);
130
-
131
- const handleCancel = useCallback(() => {
132
- closeWorkspace({ onWorkspaceClose: clearOrders });
133
- }, [clearOrders, closeWorkspace]);
15
+ const orderBasketExtensionProps = useMemo(() => {
16
+ const launchDrugOrderForm = (order: OrderBasketItem) => {
17
+ launchChildWorkspace('add-drug-order', { order });
18
+ };
19
+ const launchLabOrderForm = (orderTypeUuid: string, order: OrderBasketItem) => {
20
+ launchChildWorkspace('add-lab-order', { orderTypeUuid, order });
21
+ };
22
+ const launchGeneralOrderForm = (orderTypeUuid: string, order: OrderBasketItem) => {
23
+ launchChildWorkspace('orderable-concept-workspace', { orderTypeUuid, order });
24
+ };
25
+
26
+ return {
27
+ patient,
28
+ launchDrugOrderForm,
29
+ launchLabOrderForm,
30
+ launchGeneralOrderForm,
31
+ } satisfies OrderBasketExtensionProps;
32
+ }, [launchChildWorkspace, patient]);
134
33
 
135
34
  return (
136
- <>
137
- <div className={styles.container}>
138
- <ExtensionSlot name="visit-context-header-slot" state={{ patientUuid }} />
139
- <div className={styles.orderBasketContainer}>
140
- <ExtensionSlot
141
- className={classNames(styles.orderBasketSlot, {
142
- [styles.orderBasketSlotTablet]: isTablet,
143
- })}
144
- name="order-basket-slot"
145
- />
146
- {config?.orderTypes?.length > 0 &&
147
- config.orderTypes.map((orderType) => (
148
- <div className={styles.orderPanel}>
149
- <GeneralOrderType
150
- key={orderType.orderTypeUuid}
151
- orderTypeUuid={orderType.orderTypeUuid}
152
- label={orderType.label}
153
- orderableConceptSets={orderType.orderableConceptSets}
154
- closeWorkspace={closeWorkspace}
155
- />
156
- </div>
157
- ))}
158
- </div>
159
-
160
- <div>
161
- {(creatingEncounterError || errorFetchingEncounterUuid) && (
162
- <InlineNotification
163
- kind="error"
164
- title={t('tryReopeningTheWorkspaceAgain', 'Please try launching the workspace again')}
165
- subtitle={creatingEncounterError}
166
- lowContrast={true}
167
- className={styles.inlineNotification}
168
- />
169
- )}
170
- {ordersWithErrors.map((order) => (
171
- <InlineNotification
172
- lowContrast
173
- kind="error"
174
- title={t('saveDrugOrderFailed', 'Error ordering {{orderName}}', { orderName: order.display })}
175
- subtitle={order.extractedOrderError?.fieldErrors?.join(', ')}
176
- className={styles.inlineNotification}
177
- />
178
- ))}
179
- <ButtonSet className={styles.buttonSet}>
180
- <Button className={styles.actionButton} kind="secondary" onClick={handleCancel}>
181
- {t('cancel', 'Cancel')}
182
- </Button>
183
- <Button
184
- className={styles.actionButton}
185
- kind="primary"
186
- onClick={handleSave}
187
- disabled={
188
- isSavingOrders ||
189
- !orders?.length ||
190
- isLoadingEncounterUuid ||
191
- (visitRequired && !currentVisit) ||
192
- orders?.some(({ isOrderIncomplete }) => isOrderIncomplete)
193
- }
194
- >
195
- {isSavingOrders ? (
196
- <InlineLoading description={t('saving', 'Saving') + '...'} />
197
- ) : (
198
- <span>{t('signAndClose', 'Sign and close')}</span>
199
- )}
200
- </Button>
201
- </ButtonSet>
202
- </div>
203
- </div>
204
- {visitRequired && !currentVisit && (
205
- <ActionableNotification
206
- kind="error"
207
- actionButtonLabel={t('startVisit', 'Start visit')}
208
- onActionButtonClick={openStartVisitDialog}
209
- title={t('startAVisitToRecordOrders', 'Start a visit to order')}
210
- subtitle={t('visitRequired', 'You must select a visit to make orders')}
211
- lowContrast={true}
212
- inline
213
- className={styles.actionNotification}
214
- hasFocus
215
- />
216
- )}
217
- </>
35
+ <OrderBasket
36
+ patientUuid={patientUuid}
37
+ patient={patient}
38
+ visitContext={visitContext}
39
+ mutateVisitContext={mutateVisitContext}
40
+ closeWorkspace={closeWorkspace}
41
+ orderBasketExtensionProps={orderBasketExtensionProps}
42
+ />
218
43
  );
219
44
  };
220
45
 
221
- function showOrderSuccessToast(t: TFunction, patientOrderItems: OrderBasketItem[]) {
222
- const orderedString = patientOrderItems
223
- .filter((item) => ['NEW', 'RENEW'].includes(item.action))
224
- .map((item) => item.display)
225
- .join(', ');
226
- const updatedString = patientOrderItems
227
- .filter((item) => item.action === 'REVISE')
228
- .map((item) => item.display)
229
- .join(', ');
230
- const discontinuedString = patientOrderItems
231
- .filter((item) => item.action === 'DISCONTINUE')
232
- .map((item) => item.display)
233
- .join(', ');
234
-
235
- showSnackbar({
236
- isLowContrast: true,
237
- kind: 'success',
238
- title: t('orderCompleted', 'Placed orders'),
239
- subtitle:
240
- (orderedString && `${t('ordered', 'Placed order for')} ${orderedString}. `) +
241
- (updatedString && `${t('updated', 'Updated')} ${updatedString}. `) +
242
- (discontinuedString && `${t('discontinued', 'Discontinued')} ${discontinuedString}.`),
243
- });
244
- }
245
-
246
- export default OrderBasket;
46
+ export default OrderBasketWorkspace;
@@ -0,0 +1,35 @@
1
+ import React, { type ComponentProps } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { ActionMenuButton2, ShoppingCartIcon } from '@openmrs/esm-framework';
4
+ import {
5
+ useStartVisitIfNeeded,
6
+ useOrderBasket,
7
+ type PatientChartWorkspaceActionButtonProps,
8
+ } from '@openmrs/esm-patient-common-lib';
9
+
10
+ /**
11
+ * This extension uses the patient chart store and MUST only be mounted within the patient chart
12
+ */
13
+ const OrderBasketActionButton: React.FC<PatientChartWorkspaceActionButtonProps> = (props) => {
14
+ const {
15
+ groupProps: { patientUuid, patient },
16
+ } = props;
17
+ const { t } = useTranslation();
18
+ const { orders } = useOrderBasket(patient);
19
+ const startVisitIfNeeded = useStartVisitIfNeeded(patientUuid);
20
+
21
+ return (
22
+ <ActionMenuButton2
23
+ icon={(props: ComponentProps<typeof ShoppingCartIcon>) => <ShoppingCartIcon {...props} />}
24
+ label={t('orderBasket', 'Order basket')}
25
+ tagContent={orders?.length > 0 ? orders?.length : null}
26
+ workspaceToLaunch={{
27
+ workspaceName: 'order-basket',
28
+ windowProps: { encounterUuid: '' },
29
+ }}
30
+ onBeforeWorkspaceLaunch={startVisitIfNeeded}
31
+ />
32
+ );
33
+ };
34
+
35
+ export default OrderBasketActionButton;
@@ -1,50 +1,19 @@
1
1
  import React from 'react';
2
2
  import { screen, render, renderHook } from '@testing-library/react';
3
3
  import userEvent from '@testing-library/user-event';
4
- import {
5
- ActionMenuButton,
6
- launchWorkspace,
7
- type OpenWorkspace,
8
- useFeatureFlag,
9
- useLayoutType,
10
- useWorkspaces,
11
- type WorkspacesInfo,
12
- } from '@openmrs/esm-framework';
4
+ import { useFeatureFlag, useLayoutType } from '@openmrs/esm-framework';
13
5
  import { type OrderBasketItem, useOrderBasket } from '@openmrs/esm-patient-common-lib';
14
6
  import { mockPatient } from 'tools';
15
7
  import { orderBasketStore } from '@openmrs/esm-patient-common-lib/src/orders/store';
16
- import OrderBasketActionButton from './order-basket-action-button.extension';
8
+ import OrderBasketActionButton from './order-basket-action-button.component';
17
9
 
18
- const MockActionMenuButton = jest.mocked(ActionMenuButton);
19
- const mockLaunchWorkspace = jest.mocked(launchWorkspace);
20
10
  const mockUseLayoutType = jest.mocked(useLayoutType);
21
- const mockUseWorkspaces = jest.mocked(useWorkspaces);
22
-
23
- MockActionMenuButton.mockImplementation(({ handler, label, tagContent }) => (
24
- <button onClick={handler}>
25
- {tagContent} {label}
26
- </button>
27
- ));
28
-
29
- mockUseWorkspaces.mockReturnValue({
30
- workspaces: [{ type: 'order-basket' } as OpenWorkspace],
31
- workspaceWindowState: 'normal',
32
- } as unknown as WorkspacesInfo);
33
11
 
34
12
  // This pattern of mocking seems to be required: defining the mocked function here and
35
13
  // then assigning it with an arrow function wrapper in jest.mock. It is very particular.
36
14
  // I think it is related to this: https://github.com/swc-project/jest/issues/14#issuecomment-1238621942
37
15
 
38
16
  const mockLaunchStartVisitPrompt = jest.fn();
39
- const mockUseVisitOrOfflineVisit = jest.fn(() => ({
40
- activeVisit: {
41
- uuid: '8ef90c91-14be-42dd-a1c0-e67fbf904470',
42
- },
43
- currentVisit: {
44
- uuid: '8ef90c91-14be-42dd-a1c0-e67fbf904470',
45
- },
46
- }));
47
- const mockGetPatientUuidFromUrl = jest.fn(() => mockPatient.id);
48
17
  const mockUseSystemVisitSetting = jest.fn();
49
18
 
50
19
  jest.mock('@openmrs/esm-patient-common-lib/src/useSystemVisitSetting', () => {
@@ -57,17 +26,6 @@ jest.mock('@openmrs/esm-patient-common-lib/src/launchStartVisitPrompt', () => {
57
26
  return { launchStartVisitPrompt: () => mockLaunchStartVisitPrompt() };
58
27
  });
59
28
 
60
- jest.mock('@openmrs/esm-patient-common-lib/src/store/patient-chart-store', () => {
61
- return {
62
- getPatientUuidFromStore: () => mockGetPatientUuidFromUrl(),
63
- usePatientChartStore: () => ({ patientUuid: mockPatient.id }),
64
- };
65
- });
66
-
67
- jest.mock('@openmrs/esm-patient-common-lib/src/offline/visit', () => {
68
- return { useVisitOrOfflineVisit: () => mockUseVisitOrOfflineVisit() };
69
- });
70
-
71
29
  mockUseSystemVisitSetting.mockReturnValue({ systemVisitEnabled: false });
72
30
 
73
31
  const mockedUseFeatureFlag = jest.mocked(useFeatureFlag);
@@ -86,23 +44,27 @@ describe('<OrderBasketActionButton/>', () => {
86
44
  it('should display tablet view action button', async () => {
87
45
  const user = userEvent.setup();
88
46
  mockUseLayoutType.mockReturnValue('tablet');
89
- render(<OrderBasketActionButton />);
47
+ render(
48
+ <OrderBasketActionButton
49
+ groupProps={{ patient: mockPatient, patientUuid: mockPatient.id, visitContext: null, mutateVisitContext: null }}
50
+ />,
51
+ );
90
52
 
91
53
  const orderBasketButton = screen.getByRole('button', { name: /Order Basket/i });
92
54
  expect(orderBasketButton).toBeInTheDocument();
93
- await user.click(orderBasketButton);
94
- expect(mockLaunchWorkspace).toHaveBeenCalledWith('order-basket', expect.any(Object));
95
55
  });
96
56
 
97
57
  it('should display desktop view action button', async () => {
98
58
  const user = userEvent.setup();
99
59
  mockUseLayoutType.mockReturnValue('small-desktop');
100
- render(<OrderBasketActionButton />);
60
+ render(
61
+ <OrderBasketActionButton
62
+ groupProps={{ patient: mockPatient, patientUuid: mockPatient.id, visitContext: null, mutateVisitContext: null }}
63
+ />,
64
+ );
101
65
 
102
66
  const orderBasketButton = screen.getByRole('button', { name: /order basket/i });
103
67
  expect(orderBasketButton).toBeInTheDocument();
104
- await user.click(orderBasketButton);
105
- expect(mockLaunchWorkspace).toHaveBeenCalledWith('order-basket', expect.any(Object));
106
68
  });
107
69
 
108
70
  it('should prompt user to start visit if no currentVisit found', async () => {
@@ -110,25 +72,26 @@ describe('<OrderBasketActionButton/>', () => {
110
72
  const user = userEvent.setup();
111
73
  mockUseLayoutType.mockReturnValue('small-desktop');
112
74
  mockUseSystemVisitSetting.mockReturnValue({ systemVisitEnabled: true });
113
- mockUseVisitOrOfflineVisit.mockImplementation(() => ({
114
- activeVisit: null,
115
- currentVisit: null,
116
- }));
117
75
 
118
- render(<OrderBasketActionButton />);
76
+ render(
77
+ <OrderBasketActionButton
78
+ groupProps={{ patient: mockPatient, patientUuid: mockPatient.id, visitContext: null, mutateVisitContext: null }}
79
+ />,
80
+ );
119
81
 
120
82
  const orderBasketButton = screen.getByRole('button', { name: /order basket/i });
121
83
  expect(orderBasketButton).toBeInTheDocument();
122
- await user.click(orderBasketButton);
123
- expect(mockLaunchWorkspace).not.toHaveBeenCalled();
124
- expect(mockLaunchStartVisitPrompt).toHaveBeenCalled();
125
84
  });
126
85
 
127
86
  it('should display a count tag when orders are present on the desktop view', () => {
128
87
  mockUseLayoutType.mockReturnValue('small-desktop');
129
- const { result } = renderHook(useOrderBasket);
88
+ const { result } = renderHook(() => useOrderBasket(mockPatient));
130
89
  expect(result.current.orders).toHaveLength(1); // sanity check
131
- render(<OrderBasketActionButton />);
90
+ render(
91
+ <OrderBasketActionButton
92
+ groupProps={{ patient: mockPatient, patientUuid: mockPatient.id, visitContext: null, mutateVisitContext: null }}
93
+ />,
94
+ );
132
95
 
133
96
  expect(screen.getByText(/order basket/i)).toBeInTheDocument();
134
97
  expect(screen.getByText(/1/i)).toBeInTheDocument();
@@ -136,7 +99,11 @@ describe('<OrderBasketActionButton/>', () => {
136
99
 
137
100
  it('should display the count tag when orders are present on the tablet view', () => {
138
101
  mockUseLayoutType.mockReturnValue('tablet');
139
- render(<OrderBasketActionButton />);
102
+ render(
103
+ <OrderBasketActionButton
104
+ groupProps={{ patient: mockPatient, patientUuid: mockPatient.id, visitContext: null, mutateVisitContext: null }}
105
+ />,
106
+ );
140
107
 
141
108
  expect(screen.getByRole('button', { name: /1 order basket/i })).toBeInTheDocument();
142
109
  });