@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.
- package/.turbo/turbo-build.log +26 -23
- package/dist/1119.js +1 -1
- package/dist/1197.js +1 -1
- package/dist/1253.js +1 -0
- package/dist/1253.js.map +1 -0
- package/dist/1268.js +2 -0
- package/dist/1268.js.map +1 -0
- package/dist/2146.js +1 -1
- package/dist/2690.js +1 -1
- package/dist/3099.js +1 -1
- package/dist/3584.js +1 -1
- package/dist/3685.js +1 -1
- package/dist/375.js +1 -0
- package/dist/375.js.map +1 -0
- package/dist/4055.js +1 -1
- package/dist/4132.js +1 -1
- package/dist/4300.js +1 -1
- package/dist/4335.js +1 -1
- package/dist/4341.js +1 -0
- package/dist/4341.js.map +1 -0
- package/dist/4618.js +1 -1
- package/dist/4652.js +1 -1
- package/dist/4687.js +1 -0
- package/dist/4687.js.map +1 -0
- package/dist/4937.js +1 -1
- package/dist/4937.js.map +1 -1
- package/dist/4944.js +1 -1
- package/dist/5173.js +1 -1
- package/dist/5241.js +1 -1
- package/dist/5442.js +1 -1
- package/dist/5661.js +1 -1
- package/dist/5670.js +1 -0
- package/dist/5670.js.map +1 -0
- package/dist/6022.js +1 -1
- package/dist/6336.js +1 -0
- package/dist/6336.js.map +1 -0
- package/dist/6364.js +1 -0
- package/dist/6364.js.map +1 -0
- package/dist/6411.js +1 -1
- package/dist/6468.js +1 -1
- package/dist/6473.js +1 -0
- package/dist/6473.js.map +1 -0
- package/dist/6542.js +1 -1
- package/dist/6679.js +1 -1
- package/dist/6840.js +1 -1
- package/dist/6859.js +1 -1
- package/dist/7097.js +1 -1
- package/dist/7159.js +1 -1
- package/dist/723.js +1 -1
- package/dist/7617.js +1 -1
- package/dist/7657.js +2 -0
- package/dist/7657.js.map +1 -0
- package/dist/795.js +1 -1
- package/dist/8154.js +1 -1
- package/dist/8154.js.map +1 -1
- package/dist/8163.js +1 -1
- package/dist/8349.js +1 -1
- package/dist/8416.js +1 -0
- package/dist/8416.js.map +1 -0
- package/dist/8618.js +1 -1
- package/dist/890.js +1 -1
- package/dist/9214.js +1 -1
- package/dist/9538.js +1 -1
- package/dist/9569.js +1 -1
- package/dist/986.js +1 -1
- package/dist/9879.js +1 -1
- package/dist/9895.js +1 -1
- package/dist/9900.js +1 -1
- package/dist/9913.js +1 -1
- package/dist/main.js +1 -1
- package/dist/main.js.map +1 -1
- package/dist/openmrs-esm-patient-orders-app.js +1 -1
- package/dist/openmrs-esm-patient-orders-app.js.buildmanifest.json +307 -330
- package/dist/openmrs-esm-patient-orders-app.js.map +1 -1
- package/dist/routes.json +1 -1
- package/package.json +5 -4
- package/src/api/api.ts +16 -30
- package/src/components/general-order-table.scss +4 -1
- package/src/components/order-details-table.component.tsx +88 -54
- package/src/components/test-order.scss +4 -2
- package/src/dashboard.meta.ts +3 -1
- package/src/index.ts +12 -9
- package/src/lab-results/{lab-results-form.component.tsx → exported-lab-results-form.workspace.tsx} +68 -67
- package/src/lab-results/lab-results-form-field.component.tsx +8 -5
- package/src/lab-results/lab-results-form.test.tsx +23 -21
- package/src/lab-results/lab-results-form.workspace.tsx +25 -0
- package/src/lab-results/print-results/print-modal/print-results-modal.tsx +5 -1
- package/src/order-basket/exported-order-basket.workspace.tsx +54 -0
- 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
- package/src/order-basket/general-order-type/add-general-order/add-general-order.workspace.tsx +35 -0
- package/src/order-basket/general-order-type/add-general-order/exported-add-general-order.workspace.tsx +32 -0
- package/src/order-basket/general-order-type/{orderable-concept-search → add-general-order}/search-results.component.tsx +21 -15
- package/src/order-basket/general-order-type/general-order-form/general-order-form.component.tsx +70 -23
- package/src/order-basket/general-order-type/{general-order-type.component.tsx → general-order-panel.component.tsx} +35 -52
- package/src/order-basket/general-order-type/resources.ts +4 -3
- package/src/order-basket/order-basket.component.tsx +213 -0
- package/src/order-basket/order-basket.workspace.tsx +35 -235
- package/src/order-basket-action-button/order-basket-action-button.component.tsx +35 -0
- package/src/order-basket-action-button/order-basket-action-button.test.tsx +28 -61
- package/src/order-cancellation-form/cancel-order-form.component.tsx +82 -85
- package/src/routes.json +19 -27
- package/src/utils/index.ts +15 -3
- package/translations/am.json +2 -0
- package/translations/ar.json +2 -0
- package/translations/ar_SY.json +2 -0
- package/translations/bn.json +2 -0
- package/translations/de.json +2 -0
- package/translations/en.json +5 -9
- package/translations/en_US.json +2 -0
- package/translations/es.json +2 -0
- package/translations/es_MX.json +2 -0
- package/translations/fr.json +5 -3
- package/translations/he.json +2 -0
- package/translations/hi.json +2 -0
- package/translations/hi_IN.json +2 -0
- package/translations/id.json +2 -0
- package/translations/it.json +8 -6
- package/translations/ka.json +2 -0
- package/translations/km.json +2 -0
- package/translations/ku.json +2 -0
- package/translations/ky.json +2 -0
- package/translations/lg.json +2 -0
- package/translations/ne.json +2 -0
- package/translations/pl.json +2 -0
- package/translations/pt.json +2 -0
- package/translations/pt_BR.json +2 -0
- package/translations/qu.json +2 -0
- package/translations/ro_RO.json +2 -0
- package/translations/ru_RU.json +2 -0
- package/translations/si.json +2 -0
- package/translations/sw.json +2 -0
- package/translations/sw_KE.json +2 -0
- package/translations/tr.json +2 -0
- package/translations/tr_TR.json +2 -0
- package/translations/uk.json +2 -0
- package/translations/uz.json +2 -0
- package/translations/uz@Latn.json +2 -0
- package/translations/uz_UZ.json +2 -0
- package/translations/vi.json +2 -0
- package/translations/zh.json +2 -0
- package/translations/zh_CN.json +2 -0
- package/dist/1571.js +0 -1
- package/dist/1571.js.map +0 -1
- package/dist/2537.js +0 -1
- package/dist/2537.js.map +0 -1
- package/dist/4051.js +0 -1
- package/dist/4051.js.map +0 -1
- package/dist/4918.js +0 -1
- package/dist/4918.js.map +0 -1
- package/dist/5048.js +0 -1
- package/dist/5048.js.map +0 -1
- package/dist/6432.js +0 -1
- package/dist/6432.js.map +0 -1
- package/dist/717.js +0 -1
- package/dist/717.js.map +0 -1
- package/dist/7202.js +0 -1
- package/dist/7202.js.map +0 -1
- package/dist/7337.js +0 -2
- package/dist/7337.js.map +0 -1
- package/dist/7817.js +0 -2
- package/dist/7817.js.map +0 -1
- package/dist/8625.js +0 -1
- package/dist/8625.js.map +0 -1
- package/dist/8960.js +0 -1
- package/dist/8960.js.map +0 -1
- package/src/order-basket-action-button/order-basket-action-button.extension.tsx +0 -23
- /package/dist/{7817.js.LICENSE.txt → 1268.js.LICENSE.txt} +0 -0
- /package/dist/{7337.js.LICENSE.txt → 7657.js.LICENSE.txt} +0 -0
- /package/src/order-basket/general-order-type/{orderable-concept-search → add-general-order}/orderable-concept-search.scss +0 -0
- /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, {
|
|
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
|
-
|
|
7
|
-
|
|
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
|
-
|
|
18
|
-
postOrders,
|
|
19
|
-
postOrdersOnNewEncounter,
|
|
20
|
-
useOrderBasket,
|
|
21
|
-
useVisitOrOfflineVisit,
|
|
6
|
+
type PatientWorkspace2DefinitionProps,
|
|
22
7
|
} from '@openmrs/esm-patient-common-lib';
|
|
23
|
-
import
|
|
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
|
|
30
|
-
patientUuid,
|
|
10
|
+
const OrderBasketWorkspace: React.FC<PatientWorkspace2DefinitionProps<{}, OrderBasketWindowProps>> = ({
|
|
11
|
+
groupProps: { patientUuid, patient, visitContext, mutateVisitContext },
|
|
31
12
|
closeWorkspace,
|
|
32
|
-
|
|
33
|
-
promptBeforeClosing,
|
|
13
|
+
launchChildWorkspace,
|
|
34
14
|
}) => {
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
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.
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
});
|