@openmrs/esm-patient-orders-app 11.3.1-patch.9310 → 11.3.1-patch.9508

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/.turbo/turbo-build.log +15 -18
  2. package/dist/1119.js +1 -1
  3. package/dist/1197.js +1 -1
  4. package/dist/1571.js +1 -0
  5. package/dist/1571.js.map +1 -0
  6. package/dist/2146.js +1 -1
  7. package/dist/2690.js +1 -1
  8. package/dist/2717.js +1 -0
  9. package/dist/2717.js.map +1 -0
  10. package/dist/3099.js +1 -1
  11. package/dist/3584.js +1 -1
  12. package/dist/4055.js +1 -1
  13. package/dist/4132.js +1 -1
  14. package/dist/4300.js +1 -1
  15. package/dist/4335.js +1 -1
  16. package/dist/439.js +1 -0
  17. package/dist/4618.js +1 -1
  18. package/dist/4652.js +1 -1
  19. package/dist/4937.js +1 -1
  20. package/dist/4937.js.map +1 -1
  21. package/dist/4944.js +1 -1
  22. package/dist/5134.js +2 -0
  23. package/dist/5134.js.map +1 -0
  24. package/dist/5173.js +1 -1
  25. package/dist/5241.js +1 -1
  26. package/dist/5442.js +1 -1
  27. package/dist/5661.js +1 -1
  28. package/dist/5670.js +1 -1
  29. package/dist/5670.js.map +1 -1
  30. package/dist/6022.js +1 -1
  31. package/dist/6468.js +1 -1
  32. package/dist/6589.js +1 -0
  33. package/dist/6679.js +1 -1
  34. package/dist/6840.js +1 -1
  35. package/dist/6859.js +1 -1
  36. package/dist/7097.js +1 -1
  37. package/dist/7159.js +1 -1
  38. package/dist/723.js +1 -1
  39. package/dist/7617.js +1 -1
  40. package/dist/795.js +1 -1
  41. package/dist/8163.js +1 -1
  42. package/dist/8349.js +1 -1
  43. package/dist/8371.js +1 -0
  44. package/dist/8618.js +1 -1
  45. package/dist/8625.js +1 -0
  46. package/dist/8625.js.map +1 -0
  47. package/dist/8803.js +1 -0
  48. package/dist/8803.js.map +1 -0
  49. package/dist/890.js +1 -1
  50. package/dist/8960.js +1 -0
  51. package/dist/8960.js.map +1 -0
  52. package/dist/9214.js +1 -1
  53. package/dist/9538.js +1 -1
  54. package/dist/9569.js +1 -1
  55. package/dist/986.js +1 -1
  56. package/dist/9879.js +1 -1
  57. package/dist/9895.js +1 -1
  58. package/dist/9900.js +1 -1
  59. package/dist/9913.js +1 -1
  60. package/dist/main.js +1 -1
  61. package/dist/main.js.map +1 -1
  62. package/dist/openmrs-esm-patient-orders-app.js +1 -1
  63. package/dist/openmrs-esm-patient-orders-app.js.buildmanifest.json +276 -259
  64. package/dist/openmrs-esm-patient-orders-app.js.map +1 -1
  65. package/dist/routes.json +1 -1
  66. package/package.json +2 -2
  67. package/src/api/api.ts +25 -0
  68. package/src/components/order-details-table.component.tsx +40 -53
  69. package/src/components/test-order.component.tsx +53 -49
  70. package/src/index.ts +9 -12
  71. package/src/lab-results/lab-results-form.component.tsx +343 -0
  72. package/src/lab-results/lab-results-form.test.tsx +455 -306
  73. package/src/lab-results/lab-results-schema.resource.tsx +13 -4
  74. package/src/lab-results/lab-results.resource.ts +113 -0
  75. package/src/order-basket/general-order-type/general-order-form/general-order-form.component.tsx +25 -71
  76. package/src/order-basket/general-order-type/{general-order-panel.component.tsx → general-order-type.component.tsx} +41 -23
  77. package/src/order-basket/general-order-type/{add-general-order/add-general-order.component.tsx → orderable-concept-search/orderable-concept-search.workspace.tsx} +83 -78
  78. package/src/order-basket/general-order-type/{add-general-order → orderable-concept-search}/search-results.component.tsx +14 -15
  79. package/src/order-basket/general-order-type/resources.ts +2 -3
  80. package/src/order-basket/order-basket.workspace.tsx +251 -35
  81. package/src/order-basket-action-button/order-basket-action-button.extension.tsx +31 -0
  82. package/src/order-basket-action-button/order-basket-action-button.test.tsx +36 -27
  83. package/src/order-cancellation-form/cancel-order-form.component.tsx +85 -82
  84. package/src/routes.json +25 -17
  85. package/src/utils/index.ts +3 -15
  86. package/translations/am.json +6 -20
  87. package/translations/ar.json +6 -20
  88. package/translations/ar_SY.json +6 -20
  89. package/translations/bn.json +6 -20
  90. package/translations/cs.json +114 -0
  91. package/translations/de.json +6 -20
  92. package/translations/en.json +12 -4
  93. package/translations/en_US.json +6 -20
  94. package/translations/es.json +6 -20
  95. package/translations/es_MX.json +6 -20
  96. package/translations/fr.json +7 -21
  97. package/translations/he.json +6 -20
  98. package/translations/hi.json +6 -20
  99. package/translations/hi_IN.json +6 -20
  100. package/translations/id.json +6 -20
  101. package/translations/it.json +6 -20
  102. package/translations/ka.json +6 -20
  103. package/translations/km.json +6 -20
  104. package/translations/ku.json +6 -20
  105. package/translations/ky.json +6 -20
  106. package/translations/lg.json +6 -20
  107. package/translations/ne.json +6 -20
  108. package/translations/pl.json +6 -20
  109. package/translations/pt.json +6 -20
  110. package/translations/pt_BR.json +6 -20
  111. package/translations/qu.json +6 -20
  112. package/translations/ro_RO.json +6 -20
  113. package/translations/ru_RU.json +6 -20
  114. package/translations/si.json +6 -20
  115. package/translations/sq.json +114 -0
  116. package/translations/sw.json +6 -20
  117. package/translations/sw_KE.json +6 -20
  118. package/translations/tr.json +6 -20
  119. package/translations/tr_TR.json +6 -20
  120. package/translations/uk.json +6 -20
  121. package/translations/uz.json +6 -20
  122. package/translations/uz@Latn.json +6 -20
  123. package/translations/uz_UZ.json +6 -20
  124. package/translations/vi.json +6 -20
  125. package/translations/zh.json +6 -20
  126. package/translations/zh_CN.json +6 -20
  127. package/translations/zh_TW.json +114 -0
  128. package/dist/1253.js +0 -1
  129. package/dist/1253.js.map +0 -1
  130. package/dist/1268.js +0 -2
  131. package/dist/1268.js.map +0 -1
  132. package/dist/375.js +0 -1
  133. package/dist/375.js.map +0 -1
  134. package/dist/4341.js +0 -1
  135. package/dist/4341.js.map +0 -1
  136. package/dist/4687.js +0 -1
  137. package/dist/4687.js.map +0 -1
  138. package/dist/6364.js +0 -1
  139. package/dist/6364.js.map +0 -1
  140. package/dist/6473.js +0 -1
  141. package/dist/6473.js.map +0 -1
  142. package/dist/8416.js +0 -1
  143. package/dist/8416.js.map +0 -1
  144. package/src/lab-results/exported-lab-results-form.workspace.tsx +0 -264
  145. package/src/lab-results/lab-results-form.workspace.tsx +0 -25
  146. package/src/order-basket/exported-order-basket.workspace.tsx +0 -54
  147. package/src/order-basket/general-order-type/add-general-order/add-general-order.workspace.tsx +0 -35
  148. package/src/order-basket/general-order-type/add-general-order/exported-add-general-order.workspace.tsx +0 -32
  149. package/src/order-basket/order-basket.component.tsx +0 -213
  150. package/src/order-basket-action-button/order-basket-action-button.component.tsx +0 -35
  151. /package/dist/{1268.js.LICENSE.txt → 5134.js.LICENSE.txt} +0 -0
  152. /package/src/order-basket/general-order-type/{add-general-order → orderable-concept-search}/orderable-concept-search.scss +0 -0
  153. /package/src/order-basket/general-order-type/{add-general-order → orderable-concept-search}/search-results.scss +0 -0
@@ -0,0 +1,343 @@
1
+ import React, { useCallback, useEffect, useMemo, useState } from 'react';
2
+ import { Button, ButtonSet, Form, Layer, InlineLoading, InlineNotification, Stack } from '@carbon/react';
3
+ import classNames from 'classnames';
4
+ import { type Control, useForm } from 'react-hook-form';
5
+ import { useTranslation } from 'react-i18next';
6
+ import { useSWRConfig } from 'swr';
7
+ import { zodResolver } from '@hookform/resolvers/zod';
8
+ import {
9
+ restBaseUrl,
10
+ showSnackbar,
11
+ useAbortController,
12
+ useLayoutType,
13
+ ExtensionSlot,
14
+ usePatient,
15
+ } from '@openmrs/esm-framework';
16
+ import { type DefaultPatientWorkspaceProps, type Order, useOrderBasket } from '@openmrs/esm-patient-common-lib';
17
+ import { type ObservationValue } from '../types/encounter';
18
+ import {
19
+ createCompositeObservationPayload,
20
+ isCoded,
21
+ isNumeric,
22
+ isPanel,
23
+ isText,
24
+ updateObservation,
25
+ updateOrderResult,
26
+ useCompletedLabResultsArray,
27
+ useOrderConceptsByUuids,
28
+ } from './lab-results.resource';
29
+ import { createLabResultsFormCompositeSchema } from './lab-results-schema.resource';
30
+ import ResultFormField from './lab-results-form-field.component';
31
+ import styles from './lab-results-form.scss';
32
+ import orderStyles from '../order-basket/order-basket.scss';
33
+
34
+ export interface LabResultsFormProps extends DefaultPatientWorkspaceProps {
35
+ order: Order;
36
+ invalidateLabOrders?: () => void;
37
+ }
38
+
39
+ interface OrderBasketSlotProps {
40
+ patient: fhir.Patient;
41
+ }
42
+
43
+ const LabResultsForm: React.FC<LabResultsFormProps> = ({
44
+ closeWorkspace,
45
+ closeWorkspaceWithSavedChanges,
46
+ order,
47
+ promptBeforeClosing,
48
+ /* Callback to refresh lab orders in the Laboratory app after results are saved.
49
+ * This ensures the orders list stays in sync across the different tabs in the Laboratory app.
50
+ * @see https://github.com/openmrs/openmrs-esm-laboratory-app/pull/117
51
+ */
52
+ invalidateLabOrders,
53
+ }) => {
54
+ const { t } = useTranslation();
55
+ const abortController = useAbortController();
56
+ const isTablet = useLayoutType() === 'tablet';
57
+ const [orderConceptUuids, setOrderConceptUuids] = useState([order.concept.uuid]);
58
+ const { isLoading: isLoadingResultConcepts, concepts: conceptArray } = useOrderConceptsByUuids(orderConceptUuids);
59
+ const [showEmptyFormErrorNotification, setShowEmptyFormErrorNotification] = useState(false);
60
+ const compositeSchema = useMemo(() => createLabResultsFormCompositeSchema(conceptArray), [conceptArray]);
61
+ const { mutate } = useSWRConfig();
62
+ const { isLoading: isLoadingPatient, patient } = usePatient(order.patient.uuid);
63
+ const { orders, clearOrders } = useOrderBasket(patient);
64
+ const [isSavingOrders, setIsSavingOrders] = useState(false);
65
+ const { isLoading, completeLabResults, mutate: mutateResults } = useCompletedLabResultsArray(order);
66
+
67
+ const mutateOrderData = useCallback(() => {
68
+ mutate(
69
+ (key) => typeof key === 'string' && key.startsWith(`${restBaseUrl}/order?patient=${order.patient.uuid}`),
70
+ undefined,
71
+ { revalidate: true },
72
+ );
73
+ }, [mutate, order.patient.uuid]);
74
+
75
+ const handleCancel = useCallback(() => {
76
+ clearOrders();
77
+ }, [clearOrders]);
78
+
79
+ const handleSave = useCallback(() => {
80
+ const newConceptUuids = orders.map((order) => order['testType']['conceptUuid']);
81
+ setOrderConceptUuids([order.concept.uuid, ...newConceptUuids]);
82
+ clearOrders();
83
+ }, [clearOrders, orders, order.concept.uuid]);
84
+
85
+ const {
86
+ control,
87
+ formState: { errors, isDirty, isSubmitting },
88
+ setValue,
89
+ handleSubmit,
90
+ } = useForm<Record<string, ObservationValue>>({
91
+ defaultValues: {} as Record<string, ObservationValue>,
92
+ resolver: zodResolver(compositeSchema),
93
+ mode: 'all',
94
+ });
95
+ useEffect(() => {
96
+ if (Array.isArray(completeLabResults) && completeLabResults.length > 1) {
97
+ const conceptUuids = completeLabResults.map((r) => r.concept.uuid);
98
+ setOrderConceptUuids(conceptUuids);
99
+ }
100
+ }, [completeLabResults]);
101
+
102
+ const extensionProps = {
103
+ patient,
104
+ } satisfies OrderBasketSlotProps;
105
+
106
+ useEffect(() => {
107
+ conceptArray.forEach((concept, index) => {
108
+ const completeLabResult = completeLabResults.find((r) => r.concept.uuid === concept.uuid);
109
+ if (concept && completeLabResult && order?.fulfillerStatus === 'COMPLETED') {
110
+ if (isCoded(concept) && typeof completeLabResult?.value === 'object' && completeLabResult?.value?.uuid) {
111
+ setValue(concept.uuid, completeLabResult.value.uuid);
112
+ } else if (isNumeric(concept) && completeLabResult?.value) {
113
+ setValue(concept.uuid, parseFloat(completeLabResult.value as string));
114
+ } else if (isText(concept) && completeLabResult?.value) {
115
+ setValue(concept.uuid, completeLabResult?.value);
116
+ } else if (isPanel(concept)) {
117
+ concept.setMembers.forEach((member) => {
118
+ const obs = completeLabResult.groupMembers.find((v) => v.concept.uuid === member.uuid);
119
+ let value: ObservationValue;
120
+ if (isCoded(member)) {
121
+ value = typeof obs?.value === 'object' ? obs.value.uuid : obs?.value;
122
+ } else if (isNumeric(member)) {
123
+ value = obs?.value ? parseFloat(obs.value as string) : undefined;
124
+ } else if (isText(member)) {
125
+ value = obs?.value;
126
+ }
127
+ if (value) setValue(member.uuid, value);
128
+ });
129
+ }
130
+ }
131
+ });
132
+ }, [conceptArray, completeLabResults, order?.fulfillerStatus, setValue]);
133
+
134
+ useEffect(() => {
135
+ promptBeforeClosing(() => isDirty);
136
+ }, [isDirty, promptBeforeClosing]);
137
+
138
+ if (isLoadingResultConcepts) {
139
+ return (
140
+ <div className={styles.loaderContainer}>
141
+ <InlineLoading
142
+ className={styles.loader}
143
+ description={t('loadingTestDetails', 'Loading test details') + '...'}
144
+ iconDescription={t('loading', 'Loading')}
145
+ status="active"
146
+ />
147
+ </div>
148
+ );
149
+ }
150
+
151
+ const saveLabResults = async (formValues: Record<string, unknown>) => {
152
+ const isEmptyForm = Object.values(formValues).every(
153
+ (value) => value === '' || value === null || value === undefined,
154
+ );
155
+ if (isEmptyForm) {
156
+ setShowEmptyFormErrorNotification(true);
157
+ return;
158
+ }
159
+
160
+ const showNotification = (kind: 'error' | 'success', message: string) => {
161
+ showSnackbar({
162
+ title:
163
+ kind === 'success'
164
+ ? t('saveLabResults', 'Save lab results')
165
+ : t('errorSavingLabResults', 'Error saving lab results'),
166
+ kind: kind,
167
+ subtitle: message,
168
+ });
169
+ };
170
+
171
+ // Handle update operation for completed lab order results
172
+ if (order.fulfillerStatus === 'COMPLETED') {
173
+ const updateTasks = Object.entries(formValues).map(([conceptUuid, value]) => {
174
+ const completeLabResult = completeLabResults.find((r) => r.concept.uuid === conceptUuid);
175
+ const obs = completeLabResult?.groupMembers?.find((v) => v.concept.uuid === conceptUuid) ?? completeLabResult;
176
+ return updateObservation(obs?.uuid, { value });
177
+ });
178
+ const updateResults = await Promise.allSettled(updateTasks);
179
+ const failedObsconceptUuids = updateResults.reduce((prev, curr, index) => {
180
+ if (curr.status === 'rejected') {
181
+ return [...prev, Object.keys(formValues).at(index)];
182
+ }
183
+ return prev;
184
+ }, []);
185
+
186
+ if (failedObsconceptUuids.length) {
187
+ showNotification('error', 'Could not save obs with concept uuids ' + failedObsconceptUuids.join(', '));
188
+ } else {
189
+ closeWorkspaceWithSavedChanges();
190
+ showNotification(
191
+ 'success',
192
+ t('successfullySavedLabResults', 'Lab results for {{orderNumber}} have been successfully updated', {
193
+ orderNumber: order?.orderNumber,
194
+ }),
195
+ );
196
+ }
197
+ mutateResults();
198
+ return setShowEmptyFormErrorNotification(false);
199
+ }
200
+
201
+ // Handle Creation logic
202
+
203
+ // Set the observation status to 'FINAL' as we're not capturing it in the form
204
+ const obsPayload = createCompositeObservationPayload(conceptArray, order, formValues, 'FINAL');
205
+ const orderDiscontinuationPayload = {
206
+ previousOrder: order.uuid,
207
+ type: 'testorder',
208
+ action: 'DISCONTINUE',
209
+ careSetting: order.careSetting.uuid,
210
+ encounter: order.encounter.uuid,
211
+ patient: order.patient.uuid,
212
+ concept: order.concept.uuid,
213
+ orderer: order.orderer,
214
+ };
215
+ const resultsStatusPayload = {
216
+ fulfillerStatus: 'COMPLETED',
217
+ fulfillerComment: 'Test Results Entered',
218
+ };
219
+
220
+ try {
221
+ await updateOrderResult(
222
+ order.uuid,
223
+ order.encounter.uuid,
224
+ obsPayload,
225
+ resultsStatusPayload,
226
+ orderDiscontinuationPayload,
227
+ abortController,
228
+ );
229
+
230
+ closeWorkspaceWithSavedChanges();
231
+ mutateOrderData();
232
+ mutateResults();
233
+ invalidateLabOrders?.();
234
+
235
+ showNotification(
236
+ 'success',
237
+ t('successfullySavedLabResults', 'Lab results for {{orderNumber}} have been successfully updated', {
238
+ orderNumber: order?.orderNumber,
239
+ }),
240
+ );
241
+ } catch (err) {
242
+ showNotification('error', err?.message);
243
+ } finally {
244
+ setShowEmptyFormErrorNotification(false);
245
+ }
246
+ };
247
+
248
+ return (
249
+ <Form className={styles.form} onSubmit={handleSubmit(saveLabResults)}>
250
+ <Layer level={isTablet ? 1 : 0}>
251
+ <div className={styles.grid}>
252
+ {conceptArray?.length > 0 && (
253
+ <Stack gap={5}>
254
+ {!isLoading ? (
255
+ conceptArray.map((c) => (
256
+ <ResultFormField
257
+ defaultValue={completeLabResults.find((r) => r.concept.uuid === c.uuid)}
258
+ concept={c}
259
+ control={control as unknown as Control<Record<string, unknown>>}
260
+ />
261
+ ))
262
+ ) : (
263
+ <InlineLoading description={t('loadingInitialValues', 'Loading initial values') + '...'} />
264
+ )}
265
+ {order.fulfillerStatus !== 'COMPLETED' && (
266
+ <div className={orderStyles.orderBasketContainer}>
267
+ <div className={styles.heading}>
268
+ <span>{t('addOrderTests', 'Add Tests to this order')}</span>
269
+ </div>
270
+ <ExtensionSlot
271
+ className={classNames(orderStyles.orderBasketSlot, {
272
+ [orderStyles.orderBasketSlotTablet]: isTablet,
273
+ })}
274
+ name="result-order-basket-slot"
275
+ state={extensionProps}
276
+ />
277
+ </div>
278
+ )}
279
+
280
+ {orders?.length > 0 && (
281
+ <div className={orderStyles.orderBasketContainer}>
282
+ <ButtonSet className={styles.buttonSet}>
283
+ <Button size="sm" className={styles.actionButton} kind="secondary" onClick={handleCancel}>
284
+ {t('cancelOrder', 'Cancel order')}
285
+ </Button>
286
+ <Button
287
+ className={styles.actionButton}
288
+ kind="primary"
289
+ onClick={handleSave}
290
+ size="sm"
291
+ disabled={
292
+ isSavingOrders || !orders?.length || orders?.some(({ isOrderIncomplete }) => isOrderIncomplete)
293
+ }
294
+ >
295
+ {isSavingOrders ? (
296
+ <InlineLoading description={t('saving', 'Saving') + '...'} />
297
+ ) : (
298
+ <span>{t('saveTests', 'Save Tests')}</span>
299
+ )}
300
+ </Button>
301
+ </ButtonSet>
302
+ </div>
303
+ )}
304
+ </Stack>
305
+ )}
306
+ {showEmptyFormErrorNotification && (
307
+ <InlineNotification
308
+ className={styles.emptyFormError}
309
+ lowContrast
310
+ title={t('error', 'Error')}
311
+ subtitle={t('pleaseFillField', 'Please fill at least one field') + '.'}
312
+ />
313
+ )}
314
+ </div>
315
+ </Layer>
316
+
317
+ <ButtonSet
318
+ className={classNames({
319
+ [styles.tablet]: isTablet,
320
+ [styles.desktop]: !isTablet,
321
+ })}
322
+ >
323
+ <Button className={styles.button} kind="secondary" disabled={isSubmitting} onClick={() => closeWorkspace()}>
324
+ {t('discard', 'Discard')}
325
+ </Button>
326
+ <Button
327
+ className={styles.button}
328
+ kind="primary"
329
+ disabled={isSubmitting || Object.keys(errors).length > 0}
330
+ type="submit"
331
+ >
332
+ {isSubmitting ? (
333
+ <InlineLoading description={t('saving', 'Saving') + '...'} />
334
+ ) : (
335
+ t('saveAndClose', 'Save and close')
336
+ )}
337
+ </Button>
338
+ </ButtonSet>
339
+ </Form>
340
+ );
341
+ };
342
+
343
+ export default LabResultsForm;