@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.
@@ -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 { restBaseUrl, showSnackbar, useAbortController, useLayoutType } from '@openmrs/esm-framework';
9
- import { type DefaultPatientWorkspaceProps, type Order } from '@openmrs/esm-patient-common-lib';
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
- createObservationPayload,
19
+ createCompositeObservationPayload,
13
20
  isCoded,
14
21
  isNumeric,
15
22
  isPanel,
16
23
  isText,
17
24
  updateObservation,
18
25
  updateOrderResult,
19
- useCompletedLabResults,
20
- useOrderConceptByUuid,
26
+ useCompletedLabResultsArray,
27
+ useOrderConceptsByUuids,
21
28
  } from './lab-results.resource';
22
- import { createLabResultsFormSchema } from './lab-results-schema.resource';
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 { concept, isLoading: isLoadingConcepts } = useOrderConceptByUuid(order.concept.uuid);
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 schema = useMemo(() => createLabResultsFormSchema(concept), [concept]);
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(schema),
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
- if (concept && completeLabResult && order?.fulfillerStatus === 'COMPLETED') {
73
- if (isCoded(concept) && typeof completeLabResult?.value === 'object' && completeLabResult?.value?.uuid) {
74
- setValue(concept.uuid, completeLabResult.value.uuid);
75
- } else if (isNumeric(concept) && completeLabResult?.value) {
76
- setValue(concept.uuid, parseFloat(completeLabResult.value as string));
77
- } else if (isText(concept) && completeLabResult?.value) {
78
- setValue(concept.uuid, completeLabResult?.value);
79
- } else if (isPanel(concept)) {
80
- concept.setMembers.forEach((member) => {
81
- const obs = completeLabResult.groupMembers.find((v) => v.concept.uuid === member.uuid);
82
- let value: ObservationValue;
83
- if (isCoded(member)) {
84
- value = typeof obs?.value === 'object' ? obs.value.uuid : obs?.value;
85
- } else if (isNumeric(member)) {
86
- value = obs?.value ? parseFloat(obs.value as string) : undefined;
87
- } else if (isText(member)) {
88
- value = obs?.value;
89
- }
90
- if (value) setValue(member.uuid, value);
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
- }, [concept, completeLabResult, order, setValue]);
131
+ });
132
+ }, [conceptArray, completeLabResults, order?.fulfillerStatus, setValue]);
95
133
 
96
134
  useEffect(() => {
97
135
  promptBeforeClosing(() => isDirty);
98
136
  }, [isDirty, promptBeforeClosing]);
99
137
 
100
- if (isLoadingConcepts) {
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 = createObservationPayload(concept, order, formValues, 'FINAL');
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
- {concept && (
252
+ {conceptArray?.length > 0 && (
214
253
  <Stack gap={5}>
215
254
  {!isLoading ? (
216
- <ResultFormField
217
- defaultValue={completeLabResult}
218
- concept={concept}
219
- control={control as unknown as Control<Record<string, unknown>>}
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 && (