@openmrs/esm-patient-orders-app 11.3.1-pre.9447 → 11.3.1-pre.9455

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 (70) hide show
  1. package/.turbo/turbo-build.log +14 -14
  2. package/dist/1253.js +1 -0
  3. package/dist/1253.js.map +1 -0
  4. package/dist/375.js +1 -0
  5. package/dist/375.js.map +1 -0
  6. package/dist/4300.js +1 -1
  7. package/dist/4341.js +1 -0
  8. package/dist/4341.js.map +1 -0
  9. package/dist/4558.js +1 -0
  10. package/dist/4558.js.map +1 -0
  11. package/dist/5937.js +1 -0
  12. package/dist/5937.js.map +1 -0
  13. package/dist/6473.js +1 -0
  14. package/dist/6473.js.map +1 -0
  15. package/dist/6483.js +1 -0
  16. package/dist/6483.js.map +1 -0
  17. package/dist/8376.js +1 -0
  18. package/dist/8376.js.map +1 -0
  19. package/dist/8389.js +1 -0
  20. package/dist/8389.js.map +1 -0
  21. package/dist/8416.js +1 -0
  22. package/dist/8416.js.map +1 -0
  23. package/dist/8894.js +1 -0
  24. package/dist/8894.js.map +1 -0
  25. package/dist/main.js +1 -1
  26. package/dist/main.js.map +1 -1
  27. package/dist/openmrs-esm-patient-orders-app.js +1 -1
  28. package/dist/openmrs-esm-patient-orders-app.js.buildmanifest.json +230 -107
  29. package/dist/openmrs-esm-patient-orders-app.js.map +1 -1
  30. package/dist/routes.json +1 -1
  31. package/package.json +2 -2
  32. package/src/api/api.ts +0 -25
  33. package/src/components/order-details-table.component.tsx +53 -40
  34. package/src/index.ts +19 -11
  35. package/src/lab-results/{lab-results-form.component.tsx → exported-lab-results-form.workspace.tsx} +105 -108
  36. package/src/lab-results/lab-results-form.test.tsx +24 -38
  37. package/src/lab-results/lab-results-form.workspace.tsx +26 -0
  38. package/src/order-basket/exported-order-basket.workspace.tsx +59 -0
  39. package/src/order-basket/general-order-type/{orderable-concept-search/orderable-concept-search.workspace.tsx → add-general-order/add-general-order.component.tsx} +78 -83
  40. package/src/order-basket/general-order-type/add-general-order/add-general-order.workspace.tsx +38 -0
  41. package/src/order-basket/general-order-type/add-general-order/exported-add-general-order.workspace.tsx +35 -0
  42. package/src/order-basket/general-order-type/{orderable-concept-search → add-general-order}/search-results.component.tsx +15 -14
  43. package/src/order-basket/general-order-type/general-order-form/general-order-form.component.tsx +71 -25
  44. package/src/order-basket/general-order-type/{general-order-type.component.tsx → general-order-panel.component.tsx} +23 -41
  45. package/src/order-basket/general-order-type/resources.ts +3 -2
  46. package/src/order-basket/order-basket.component.tsx +213 -0
  47. package/src/order-basket/order-basket.workspace.tsx +42 -252
  48. package/src/order-basket-action-button/order-basket-action-button.component.tsx +35 -0
  49. package/src/order-basket-action-button/order-basket-action-button.test.tsx +27 -36
  50. package/src/routes.json +17 -25
  51. package/src/utils/index.ts +15 -3
  52. package/translations/en.json +4 -20
  53. package/dist/1571.js +0 -1
  54. package/dist/1571.js.map +0 -1
  55. package/dist/2717.js +0 -1
  56. package/dist/2717.js.map +0 -1
  57. package/dist/4937.js +0 -1
  58. package/dist/4937.js.map +0 -1
  59. package/dist/8625.js +0 -1
  60. package/dist/8625.js.map +0 -1
  61. package/dist/8803.js +0 -1
  62. package/dist/8803.js.map +0 -1
  63. package/dist/8960.js +0 -1
  64. package/dist/8960.js.map +0 -1
  65. package/src/order-basket-action-button/order-basket-action-button.extension.tsx +0 -31
  66. package/src/order-cancellation-form/cancel-order-form.component.tsx +0 -180
  67. package/src/order-cancellation-form/cancel-order-form.scss +0 -87
  68. package/src/order-cancellation-form/cancel-order.resource.tsx +0 -15
  69. /package/src/order-basket/general-order-type/{orderable-concept-search → add-general-order}/orderable-concept-search.scss +0 -0
  70. /package/src/order-basket/general-order-type/{orderable-concept-search → add-general-order}/search-results.scss +0 -0
@@ -1,15 +1,7 @@
1
1
  import React, { type ComponentProps, useCallback, useEffect, useMemo, useState } from 'react';
2
2
  import { Button, Tile } from '@carbon/react';
3
3
  import classNames from 'classnames';
4
- import {
5
- AddIcon,
6
- ChevronDownIcon,
7
- ChevronUpIcon,
8
- type DefaultWorkspaceProps,
9
- MaybeIcon,
10
- useLayoutType,
11
- launchWorkspace,
12
- } from '@openmrs/esm-framework';
4
+ import { AddIcon, ChevronDownIcon, ChevronUpIcon, MaybeIcon, useLayoutType } from '@openmrs/esm-framework';
13
5
  import { useTranslation } from 'react-i18next';
14
6
  import { type OrderBasketItem, useOrderBasket, useOrderType } from '@openmrs/esm-patient-common-lib';
15
7
  import { type OrderTypeDefinition } from '../../config-schema';
@@ -18,11 +10,24 @@ import OrderBasketItemTile from './order-basket-item-tile.component';
18
10
  import styles from './general-order-panel.scss';
19
11
 
20
12
  interface GeneralOrderTypeProps extends OrderTypeDefinition {
21
- closeWorkspace: DefaultWorkspaceProps['closeWorkspace'];
22
13
  patient: fhir.Patient;
14
+ orderTypeUuid: string;
15
+ launchGeneralOrderForm(orderTypeUuid: string, order?: OrderBasketItem): void;
23
16
  }
24
17
 
25
- const GeneralOrderType: React.FC<GeneralOrderTypeProps> = ({ patient, orderTypeUuid, closeWorkspace, label, icon }) => {
18
+ /**
19
+ * The extension is slotted into order-basket-slot in the main Order Basket workspace by default.
20
+ * It renders the "Add +" button for general orders, and lists pending general orders in the order basket.
21
+ *
22
+ * Designs: https://app.zeplin.io/project/60d59321e8100b0324762e05/screen/62c6bb9500e7671a618efa56
23
+ */
24
+ const GeneralOrderPanel: React.FC<GeneralOrderTypeProps> = ({
25
+ patient,
26
+ orderTypeUuid,
27
+ label,
28
+ icon,
29
+ launchGeneralOrderForm,
30
+ }) => {
26
31
  const { t } = useTranslation();
27
32
  const isTablet = useLayoutType() === 'tablet';
28
33
  const { orderType, isLoadingOrderType } = useOrderType(orderTypeUuid);
@@ -65,29 +70,6 @@ const GeneralOrderType: React.FC<GeneralOrderTypeProps> = ({ patient, orderTypeU
65
70
  };
66
71
  }, [orders]);
67
72
 
68
- const openConceptSearch = () => {
69
- closeWorkspace({
70
- ignoreChanges: true,
71
- onWorkspaceClose: () =>
72
- launchWorkspace('orderable-concept-workspace', {
73
- orderTypeUuid,
74
- }),
75
- closeWorkspaceGroup: false,
76
- });
77
- };
78
-
79
- const openOrderForm = (order: OrderBasketItem) => {
80
- closeWorkspace({
81
- ignoreChanges: true,
82
- onWorkspaceClose: () =>
83
- launchWorkspace('orderable-concept-workspace', {
84
- order,
85
- orderTypeUuid,
86
- }),
87
- closeWorkspaceGroup: false,
88
- });
89
- };
90
-
91
73
  const removeOrder = useCallback(
92
74
  (order: OrderBasketItem) => {
93
75
  const newOrders = [...orders];
@@ -120,7 +102,7 @@ const GeneralOrderType: React.FC<GeneralOrderTypeProps> = ({ patient, orderTypeU
120
102
  kind="ghost"
121
103
  renderIcon={(props: ComponentProps<typeof AddIcon>) => <AddIcon size={16} {...props} />}
122
104
  iconDescription={t('addMedication', 'Add medication')}
123
- onClick={openConceptSearch}
105
+ onClick={() => launchGeneralOrderForm(orderTypeUuid)}
124
106
  size={isTablet ? 'md' : 'sm'}
125
107
  >
126
108
  {t('add', 'Add')}
@@ -148,7 +130,7 @@ const GeneralOrderType: React.FC<GeneralOrderTypeProps> = ({ patient, orderTypeU
148
130
  <OrderBasketItemTile
149
131
  key={`incomplete-${order.action}-${order.concept?.uuid}-${index}`}
150
132
  orderBasketItem={order}
151
- onItemClick={() => openOrderForm(order)}
133
+ onItemClick={() => launchGeneralOrderForm(orderTypeUuid, order)}
152
134
  onRemoveClick={() => removeOrder(order)}
153
135
  />
154
136
  ))}
@@ -160,7 +142,7 @@ const GeneralOrderType: React.FC<GeneralOrderTypeProps> = ({ patient, orderTypeU
160
142
  <OrderBasketItemTile
161
143
  key={`new-${order.action}-${order.concept?.uuid}-${index}`}
162
144
  orderBasketItem={order}
163
- onItemClick={() => openOrderForm(order)}
145
+ onItemClick={() => launchGeneralOrderForm(orderTypeUuid, order)}
164
146
  onRemoveClick={() => removeOrder(order)}
165
147
  />
166
148
  ))}
@@ -173,7 +155,7 @@ const GeneralOrderType: React.FC<GeneralOrderTypeProps> = ({ patient, orderTypeU
173
155
  <OrderBasketItemTile
174
156
  key={`renewed-${item.action}-${item.concept?.uuid}-${index}`}
175
157
  orderBasketItem={item}
176
- onItemClick={() => openOrderForm(item)}
158
+ onItemClick={() => launchGeneralOrderForm(orderTypeUuid, item)}
177
159
  onRemoveClick={() => removeOrder(item)}
178
160
  />
179
161
  ))}
@@ -186,7 +168,7 @@ const GeneralOrderType: React.FC<GeneralOrderTypeProps> = ({ patient, orderTypeU
186
168
  <OrderBasketItemTile
187
169
  key={`revised-${item.action}-${item.concept?.uuid}-${index}`}
188
170
  orderBasketItem={item}
189
- onItemClick={() => openOrderForm(item)}
171
+ onItemClick={() => launchGeneralOrderForm(orderTypeUuid, item)}
190
172
  onRemoveClick={() => removeOrder(item)}
191
173
  />
192
174
  ))}
@@ -199,7 +181,7 @@ const GeneralOrderType: React.FC<GeneralOrderTypeProps> = ({ patient, orderTypeU
199
181
  <OrderBasketItemTile
200
182
  key={`discontinued-${item.action}-${item.concept?.uuid}-${index}`}
201
183
  orderBasketItem={item}
202
- onItemClick={() => openOrderForm(item)}
184
+ onItemClick={() => launchGeneralOrderForm(orderTypeUuid, item)}
203
185
  onRemoveClick={() => removeOrder(item)}
204
186
  />
205
187
  ))}
@@ -211,4 +193,4 @@ const GeneralOrderType: React.FC<GeneralOrderTypeProps> = ({ patient, orderTypeU
211
193
  );
212
194
  };
213
195
 
214
- export default GeneralOrderType;
196
+ export default GeneralOrderPanel;
@@ -1,4 +1,4 @@
1
- import { toOmrsIsoString } from '@openmrs/esm-framework';
1
+ import { toOmrsIsoString, type Visit } from '@openmrs/esm-framework';
2
2
  import {
3
3
  type OrderBasketItem,
4
4
  priorityOptions,
@@ -7,13 +7,14 @@ import {
7
7
  type OrderableConcept,
8
8
  } from '@openmrs/esm-patient-common-lib';
9
9
 
10
- export function createEmptyOrder(concept: OrderableConcept, orderer: string): OrderBasketItem {
10
+ export function createEmptyOrder(concept: OrderableConcept, orderer: string, visit: Visit): OrderBasketItem {
11
11
  return {
12
12
  action: 'NEW',
13
13
  urgency: priorityOptions[0].value as OrderUrgency,
14
14
  display: concept.display,
15
15
  concept,
16
16
  orderer,
17
+ visit,
17
18
  };
18
19
  }
19
20
 
@@ -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,262 +1,52 @@
1
- import React, { useCallback, useEffect, useState } from 'react';
2
- import classNames from 'classnames';
3
- import { useTranslation } from 'react-i18next';
4
- import type { TFunction } from 'i18next';
5
- import { ActionableNotification, Button, ButtonSet, InlineLoading, InlineNotification } from '@carbon/react';
1
+ import React, { useMemo } from 'react';
6
2
  import {
7
- ExtensionSlot,
8
- showModal,
9
- showSnackbar,
10
- useConfig,
11
- useLayoutType,
12
- useSession,
13
- type Visit,
14
- } from '@openmrs/esm-framework';
15
- import {
16
- type DefaultPatientWorkspaceProps,
3
+ type OrderBasketExtensionProps,
4
+ type OrderBasketWindowProps,
17
5
  type OrderBasketItem,
18
- invalidateVisitAndEncounterData,
19
- postOrders,
20
- postOrdersOnNewEncounter,
21
- useOrderBasket,
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';
28
-
29
- interface OrderBasketSlotProps {
30
- patientUuid: string;
31
- patient: fhir.Patient;
32
- visitContext: Visit;
33
- mutateVisitContext: () => void;
34
- }
35
-
36
- const OrderBasket: React.FC<DefaultPatientWorkspaceProps> = ({
37
- patientUuid,
38
- patient,
8
+ import OrderBasket from './order-basket.component';
9
+
10
+ /**
11
+ * This workspace renders the main order basket, which contains the buttons to add a drug order and to add a lab order.
12
+ *
13
+ * This workspace must only be used within the patient chart
14
+ * @see exported-order-basket.workspace.tsx
15
+ */
16
+ const OrderBasketWorkspace: React.FC<PatientWorkspace2DefinitionProps<{}, OrderBasketWindowProps>> = ({
17
+ groupProps: { patientUuid, patient, visitContext, mutateVisitContext },
39
18
  closeWorkspace,
40
- closeWorkspaceWithSavedChanges,
41
- promptBeforeClosing,
42
- visitContext,
43
- mutateVisitContext,
19
+ launchChildWorkspace,
44
20
  }) => {
45
- const { t } = useTranslation();
46
- const isTablet = useLayoutType() === 'tablet';
47
- const config = useConfig<ConfigObject>();
48
- const session = useSession();
49
- const { orders, clearOrders } = useOrderBasket(patient);
50
- const [ordersWithErrors, setOrdersWithErrors] = useState<OrderBasketItem[]>([]);
51
- const {
52
- visitRequired,
53
- isLoading: isLoadingEncounterUuid,
54
- encounterUuid,
55
- error: errorFetchingEncounterUuid,
56
- mutate: mutateEncounterUuid,
57
- } = useOrderEncounter(patientUuid, visitContext, mutateVisitContext, config.orderEncounterType);
58
- const [isSavingOrders, setIsSavingOrders] = useState(false);
59
- const [creatingEncounterError, setCreatingEncounterError] = useState('');
60
- const { mutate: mutateOrders } = useMutatePatientOrders(patientUuid);
61
- const { mutate } = useSWRConfig();
62
-
63
- useEffect(() => {
64
- promptBeforeClosing(() => !!orders.length);
65
- }, [orders, promptBeforeClosing]);
66
-
67
- const openStartVisitDialog = useCallback(() => {
68
- const dispose = showModal('start-visit-dialog', {
69
- patientUuid,
70
- closeModal: () => dispose(),
71
- });
72
- }, [patientUuid]);
73
-
74
- const handleSave = useCallback(async () => {
75
- const abortController = new AbortController();
76
- setCreatingEncounterError('');
77
- let orderEncounterUuid = encounterUuid;
78
- setIsSavingOrders(true);
79
- // If there's no encounter present, create an encounter along with the orders.
80
- if (!orderEncounterUuid) {
81
- try {
82
- await postOrdersOnNewEncounter(
83
- patientUuid,
84
- config?.orderEncounterType,
85
- visitRequired ? visitContext : null,
86
- session?.sessionLocation?.uuid,
87
- abortController,
88
- );
89
- mutateEncounterUuid();
90
- // Only revalidate current visit since orders create new encounters
91
- mutateVisitContext?.();
92
- clearOrders();
93
- await mutateOrders();
94
-
95
- closeWorkspaceWithSavedChanges();
96
- showOrderSuccessToast(t, orders);
97
- } catch (e) {
98
- console.error(e);
99
- setCreatingEncounterError(
100
- e.responseBody?.error?.message ||
101
- t('tryReopeningTheWorkspaceAgain', 'Please try launching the workspace again'),
102
- );
103
- }
104
- } else {
105
- const erroredItems = await postOrders(patientUuid, orderEncounterUuid, abortController);
106
- clearOrders({ exceptThoseMatching: (item) => erroredItems.map((e) => e.display).includes(item.display) });
107
- // Only revalidate current visit since orders create new encounters
108
- mutateVisitContext?.();
109
- await mutateOrders();
110
- invalidateVisitAndEncounterData(mutate, patientUuid);
111
-
112
- if (erroredItems.length == 0) {
113
- closeWorkspaceWithSavedChanges();
114
- showOrderSuccessToast(t, orders);
115
- } else {
116
- setOrdersWithErrors(erroredItems);
117
- }
118
- }
119
- setIsSavingOrders(false);
120
- return () => abortController.abort();
121
- }, [
122
- visitContext,
123
- visitRequired,
124
- clearOrders,
125
- closeWorkspaceWithSavedChanges,
126
- config,
127
- encounterUuid,
128
- mutateEncounterUuid,
129
- mutateOrders,
130
- mutateVisitContext,
131
- orders,
132
- patientUuid,
133
- session,
134
- t,
135
- mutate,
136
- ]);
137
-
138
- const handleCancel = useCallback(() => {
139
- closeWorkspace({ onWorkspaceClose: clearOrders });
140
- }, [clearOrders, closeWorkspace]);
141
-
142
- const extensionProps = {
143
- patientUuid,
144
- patient,
145
- visitContext,
146
- mutateVisitContext,
147
- } satisfies OrderBasketSlotProps;
21
+ const orderBasketExtensionProps = useMemo(() => {
22
+ const launchDrugOrderForm = (order: OrderBasketItem) => {
23
+ launchChildWorkspace('add-drug-order', { order });
24
+ };
25
+ const launchLabOrderForm = (orderTypeUuid: string, order: OrderBasketItem) => {
26
+ launchChildWorkspace('add-lab-order', { orderTypeUuid, order });
27
+ };
28
+ const launchGeneralOrderForm = (orderTypeUuid: string, order: OrderBasketItem) => {
29
+ launchChildWorkspace('orderable-concept-workspace', { orderTypeUuid, order });
30
+ };
31
+
32
+ return {
33
+ patient,
34
+ launchDrugOrderForm,
35
+ launchLabOrderForm,
36
+ launchGeneralOrderForm,
37
+ } satisfies OrderBasketExtensionProps;
38
+ }, [launchChildWorkspace, patient]);
148
39
 
149
40
  return (
150
- <>
151
- <div className={styles.container}>
152
- <ExtensionSlot name="visit-context-header-slot" state={extensionProps} />
153
- <div className={styles.orderBasketContainer}>
154
- <ExtensionSlot
155
- className={classNames(styles.orderBasketSlot, {
156
- [styles.orderBasketSlotTablet]: isTablet,
157
- })}
158
- name="order-basket-slot"
159
- state={extensionProps}
160
- />
161
- {config?.orderTypes?.length > 0 &&
162
- config.orderTypes.map((orderType) => (
163
- <div className={styles.orderPanel} key={orderType.orderTypeUuid}>
164
- <GeneralOrderType
165
- key={orderType.orderTypeUuid}
166
- orderTypeUuid={orderType.orderTypeUuid}
167
- label={orderType.label}
168
- orderableConceptSets={orderType.orderableConceptSets}
169
- closeWorkspace={closeWorkspace}
170
- patient={patient}
171
- />
172
- </div>
173
- ))}
174
- </div>
175
-
176
- <div>
177
- {(creatingEncounterError || errorFetchingEncounterUuid) && (
178
- <InlineNotification
179
- kind="error"
180
- title={t('tryReopeningTheWorkspaceAgain', 'Please try launching the workspace again')}
181
- subtitle={creatingEncounterError}
182
- lowContrast={true}
183
- className={styles.inlineNotification}
184
- />
185
- )}
186
- {ordersWithErrors.map((order) => (
187
- <InlineNotification
188
- lowContrast
189
- kind="error"
190
- title={t('saveDrugOrderFailed', 'Error ordering {{orderName}}', { orderName: order.display })}
191
- subtitle={order.extractedOrderError?.fieldErrors?.join(', ')}
192
- className={styles.inlineNotification}
193
- />
194
- ))}
195
- <ButtonSet className={styles.buttonSet}>
196
- <Button className={styles.actionButton} kind="secondary" onClick={handleCancel}>
197
- {t('cancel', 'Cancel')}
198
- </Button>
199
- <Button
200
- className={styles.actionButton}
201
- kind="primary"
202
- onClick={handleSave}
203
- disabled={
204
- isSavingOrders ||
205
- !orders?.length ||
206
- isLoadingEncounterUuid ||
207
- (visitRequired && !visitContext) ||
208
- orders?.some(({ isOrderIncomplete }) => isOrderIncomplete)
209
- }
210
- >
211
- {isSavingOrders ? (
212
- <InlineLoading description={t('saving', 'Saving') + '...'} />
213
- ) : (
214
- <span>{t('signAndClose', 'Sign and close')}</span>
215
- )}
216
- </Button>
217
- </ButtonSet>
218
- </div>
219
- </div>
220
- {visitRequired && !visitContext && (
221
- <ActionableNotification
222
- kind="error"
223
- actionButtonLabel={t('startVisit', 'Start visit')}
224
- onActionButtonClick={openStartVisitDialog}
225
- title={t('startAVisitToRecordOrders', 'Start a visit to order')}
226
- subtitle={t('visitRequired', 'You must select a visit to make orders')}
227
- lowContrast={true}
228
- inline
229
- className={styles.actionNotification}
230
- hasFocus
231
- />
232
- )}
233
- </>
41
+ <OrderBasket
42
+ patientUuid={patientUuid}
43
+ patient={patient}
44
+ visitContext={visitContext}
45
+ mutateVisitContext={mutateVisitContext}
46
+ closeWorkspace={closeWorkspace}
47
+ orderBasketExtensionProps={orderBasketExtensionProps}
48
+ />
234
49
  );
235
50
  };
236
51
 
237
- function showOrderSuccessToast(t: TFunction, patientOrderItems: OrderBasketItem[]) {
238
- const orderedString = patientOrderItems
239
- .filter((item) => ['NEW', 'RENEW'].includes(item.action))
240
- .map((item) => item.display)
241
- .join(', ');
242
- const updatedString = patientOrderItems
243
- .filter((item) => item.action === 'REVISE')
244
- .map((item) => item.display)
245
- .join(', ');
246
- const discontinuedString = patientOrderItems
247
- .filter((item) => item.action === 'DISCONTINUE')
248
- .map((item) => item.display)
249
- .join(', ');
250
-
251
- showSnackbar({
252
- isLowContrast: true,
253
- kind: 'success',
254
- title: t('orderCompleted', 'Placed orders'),
255
- subtitle:
256
- (orderedString && `${t('ordered', 'Placed order for')} ${orderedString}. `) +
257
- (updatedString && `${t('updated', 'Updated')} ${updatedString}. `) +
258
- (discontinuedString && `${t('discontinued', 'Discontinued')} ${discontinuedString}.`),
259
- });
260
- }
261
-
262
- export default OrderBasket;
52
+ export default OrderBasketWorkspace;