@openmrs/esm-patient-orders-app 11.3.1-pre.9388 → 11.3.1-pre.9398
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 +10 -13
- package/dist/1571.js +1 -1
- package/dist/1571.js.map +1 -1
- package/dist/2717.js +1 -0
- package/dist/2717.js.map +1 -0
- package/dist/4300.js +1 -1
- package/dist/5134.js +1 -1
- package/dist/5134.js.map +1 -1
- package/dist/8625.js +1 -1
- package/dist/8625.js.map +1 -1
- package/dist/8803.js +1 -1
- package/dist/8803.js.map +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 +45 -45
- package/dist/routes.json +1 -1
- package/package.json +2 -2
- package/src/components/test-order.component.tsx +53 -49
- package/src/lab-results/lab-results-form.component.tsx +121 -41
- package/src/lab-results/lab-results-form.test.tsx +432 -283
- package/src/lab-results/lab-results-schema.resource.tsx +13 -4
- package/src/lab-results/lab-results.resource.ts +113 -0
- package/translations/en.json +2 -0
- package/dist/7202.js +0 -1
- package/dist/7202.js.map +0 -1
|
@@ -5,30 +5,41 @@ import { type Control, useForm } from 'react-hook-form';
|
|
|
5
5
|
import { useTranslation } from 'react-i18next';
|
|
6
6
|
import { useSWRConfig } from 'swr';
|
|
7
7
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
8
|
-
import {
|
|
9
|
-
|
|
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';
|
|
10
17
|
import { type ObservationValue } from '../types/encounter';
|
|
11
18
|
import {
|
|
12
|
-
|
|
19
|
+
createCompositeObservationPayload,
|
|
13
20
|
isCoded,
|
|
14
21
|
isNumeric,
|
|
15
22
|
isPanel,
|
|
16
23
|
isText,
|
|
17
24
|
updateObservation,
|
|
18
25
|
updateOrderResult,
|
|
19
|
-
|
|
20
|
-
|
|
26
|
+
useCompletedLabResultsArray,
|
|
27
|
+
useOrderConceptsByUuids,
|
|
21
28
|
} from './lab-results.resource';
|
|
22
|
-
import {
|
|
23
|
-
|
|
29
|
+
import { createLabResultsFormCompositeSchema } from './lab-results-schema.resource';
|
|
24
30
|
import ResultFormField from './lab-results-form-field.component';
|
|
25
31
|
import styles from './lab-results-form.scss';
|
|
32
|
+
import orderStyles from '../order-basket/order-basket.scss';
|
|
26
33
|
|
|
27
34
|
export interface LabResultsFormProps extends DefaultPatientWorkspaceProps {
|
|
28
35
|
order: Order;
|
|
29
36
|
invalidateLabOrders?: () => void;
|
|
30
37
|
}
|
|
31
38
|
|
|
39
|
+
interface OrderBasketSlotProps {
|
|
40
|
+
patient: fhir.Patient;
|
|
41
|
+
}
|
|
42
|
+
|
|
32
43
|
const LabResultsForm: React.FC<LabResultsFormProps> = ({
|
|
33
44
|
closeWorkspace,
|
|
34
45
|
closeWorkspaceWithSavedChanges,
|
|
@@ -43,11 +54,15 @@ const LabResultsForm: React.FC<LabResultsFormProps> = ({
|
|
|
43
54
|
const { t } = useTranslation();
|
|
44
55
|
const abortController = useAbortController();
|
|
45
56
|
const isTablet = useLayoutType() === 'tablet';
|
|
46
|
-
const
|
|
57
|
+
const [orderConceptUuids, setOrderConceptUuids] = useState([order.concept.uuid]);
|
|
58
|
+
const { isLoading: isLoadingResultConcepts, concepts: conceptArray } = useOrderConceptsByUuids(orderConceptUuids);
|
|
47
59
|
const [showEmptyFormErrorNotification, setShowEmptyFormErrorNotification] = useState(false);
|
|
48
|
-
const
|
|
49
|
-
const { completeLabResult, isLoading, mutate: mutateResults } = useCompletedLabResults(order);
|
|
60
|
+
const compositeSchema = useMemo(() => createLabResultsFormCompositeSchema(conceptArray), [conceptArray]);
|
|
50
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);
|
|
51
66
|
|
|
52
67
|
const mutateOrderData = useCallback(() => {
|
|
53
68
|
mutate(
|
|
@@ -57,6 +72,16 @@ const LabResultsForm: React.FC<LabResultsFormProps> = ({
|
|
|
57
72
|
);
|
|
58
73
|
}, [mutate, order.patient.uuid]);
|
|
59
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
|
+
|
|
60
85
|
const {
|
|
61
86
|
control,
|
|
62
87
|
formState: { errors, isDirty, isSubmitting },
|
|
@@ -64,40 +89,53 @@ const LabResultsForm: React.FC<LabResultsFormProps> = ({
|
|
|
64
89
|
handleSubmit,
|
|
65
90
|
} = useForm<Record<string, ObservationValue>>({
|
|
66
91
|
defaultValues: {} as Record<string, ObservationValue>,
|
|
67
|
-
resolver: zodResolver(
|
|
92
|
+
resolver: zodResolver(compositeSchema),
|
|
68
93
|
mode: 'all',
|
|
69
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;
|
|
70
105
|
|
|
71
106
|
useEffect(() => {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
+
}
|
|
92
130
|
}
|
|
93
|
-
}
|
|
94
|
-
}, [
|
|
131
|
+
});
|
|
132
|
+
}, [conceptArray, completeLabResults, order?.fulfillerStatus, setValue]);
|
|
95
133
|
|
|
96
134
|
useEffect(() => {
|
|
97
135
|
promptBeforeClosing(() => isDirty);
|
|
98
136
|
}, [isDirty, promptBeforeClosing]);
|
|
99
137
|
|
|
100
|
-
if (
|
|
138
|
+
if (isLoadingResultConcepts) {
|
|
101
139
|
return (
|
|
102
140
|
<div className={styles.loaderContainer}>
|
|
103
141
|
<InlineLoading
|
|
@@ -133,6 +171,7 @@ const LabResultsForm: React.FC<LabResultsFormProps> = ({
|
|
|
133
171
|
// Handle update operation for completed lab order results
|
|
134
172
|
if (order.fulfillerStatus === 'COMPLETED') {
|
|
135
173
|
const updateTasks = Object.entries(formValues).map(([conceptUuid, value]) => {
|
|
174
|
+
const completeLabResult = completeLabResults.find((r) => r.concept.uuid === conceptUuid);
|
|
136
175
|
const obs = completeLabResult?.groupMembers?.find((v) => v.concept.uuid === conceptUuid) ?? completeLabResult;
|
|
137
176
|
return updateObservation(obs?.uuid, { value });
|
|
138
177
|
});
|
|
@@ -162,7 +201,7 @@ const LabResultsForm: React.FC<LabResultsFormProps> = ({
|
|
|
162
201
|
// Handle Creation logic
|
|
163
202
|
|
|
164
203
|
// Set the observation status to 'FINAL' as we're not capturing it in the form
|
|
165
|
-
const obsPayload =
|
|
204
|
+
const obsPayload = createCompositeObservationPayload(conceptArray, order, formValues, 'FINAL');
|
|
166
205
|
const orderDiscontinuationPayload = {
|
|
167
206
|
previousOrder: order.uuid,
|
|
168
207
|
type: 'testorder',
|
|
@@ -210,17 +249,58 @@ const LabResultsForm: React.FC<LabResultsFormProps> = ({
|
|
|
210
249
|
<Form className={styles.form} onSubmit={handleSubmit(saveLabResults)}>
|
|
211
250
|
<Layer level={isTablet ? 1 : 0}>
|
|
212
251
|
<div className={styles.grid}>
|
|
213
|
-
{
|
|
252
|
+
{conceptArray?.length > 0 && (
|
|
214
253
|
<Stack gap={5}>
|
|
215
254
|
{!isLoading ? (
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
+
))
|
|
221
262
|
) : (
|
|
222
263
|
<InlineLoading description={t('loadingInitialValues', 'Loading initial values') + '...'} />
|
|
223
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
|
+
)}
|
|
224
304
|
</Stack>
|
|
225
305
|
)}
|
|
226
306
|
{showEmptyFormErrorNotification && (
|