@ampath/esm-laboratory-app 1.3.0-next.2 → 1.3.0-next.21

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 (154) hide show
  1. package/README.md +2 -1
  2. package/dist/1119.js +1 -1
  3. package/dist/1197.js +1 -1
  4. package/dist/1222.js +1 -1
  5. package/dist/1222.js.map +1 -1
  6. package/dist/1243.js +1 -1
  7. package/dist/1243.js.map +1 -1
  8. package/dist/1270.js +1 -0
  9. package/dist/1270.js.map +1 -0
  10. package/dist/2146.js +1 -1
  11. package/dist/2690.js +1 -1
  12. package/dist/3099.js +1 -1
  13. package/dist/312.js +1 -1
  14. package/dist/312.js.map +1 -1
  15. package/dist/3169.js +2 -0
  16. package/dist/3169.js.map +1 -0
  17. package/dist/3352.js +1 -1
  18. package/dist/3352.js.map +1 -1
  19. package/dist/3535.js +1 -1
  20. package/dist/3535.js.map +1 -1
  21. package/dist/3584.js +1 -1
  22. package/dist/3872.js +1 -0
  23. package/dist/3872.js.map +1 -0
  24. package/dist/4044.js +1 -1
  25. package/dist/4044.js.map +1 -1
  26. package/dist/4055.js +1 -1
  27. package/dist/4132.js +1 -1
  28. package/dist/4300.js +1 -1
  29. package/dist/4335.js +1 -1
  30. package/dist/439.js +1 -1
  31. package/dist/4618.js +1 -1
  32. package/dist/4622.js +1 -0
  33. package/dist/4622.js.map +1 -0
  34. package/dist/4652.js +1 -1
  35. package/dist/4748.js +1 -1
  36. package/dist/4748.js.map +1 -1
  37. package/dist/4920.js +1 -1
  38. package/dist/4920.js.map +1 -1
  39. package/dist/4944.js +1 -1
  40. package/dist/5088.js +1 -1
  41. package/dist/5088.js.map +1 -1
  42. package/dist/5173.js +1 -1
  43. package/dist/5241.js +1 -1
  44. package/dist/53.js +1 -1
  45. package/dist/53.js.map +1 -1
  46. package/dist/533.js +1 -0
  47. package/dist/533.js.map +1 -0
  48. package/dist/5348.js +1 -1
  49. package/dist/5348.js.map +1 -1
  50. package/dist/5380.js +1 -1
  51. package/dist/5380.js.map +1 -1
  52. package/dist/5442.js +1 -1
  53. package/dist/5661.js +1 -1
  54. package/dist/5780.js +1 -1
  55. package/dist/5780.js.map +1 -1
  56. package/dist/6022.js +1 -1
  57. package/dist/611.js +1 -0
  58. package/dist/611.js.map +1 -0
  59. package/dist/6468.js +1 -1
  60. package/dist/6589.js +1 -1
  61. package/dist/6679.js +1 -1
  62. package/dist/6753.js +1 -1
  63. package/dist/6753.js.map +1 -1
  64. package/dist/6777.js +1 -1
  65. package/dist/6777.js.map +1 -1
  66. package/dist/679.js +1 -1
  67. package/dist/679.js.map +1 -1
  68. package/dist/6840.js +1 -1
  69. package/dist/6859.js +1 -1
  70. package/dist/7044.js +1 -0
  71. package/dist/7044.js.map +1 -0
  72. package/dist/7097.js +1 -1
  73. package/dist/7129.js +1 -1
  74. package/dist/7129.js.map +1 -1
  75. package/dist/7159.js +1 -1
  76. package/dist/723.js +1 -1
  77. package/dist/7617.js +1 -1
  78. package/dist/7689.js +1 -0
  79. package/dist/7689.js.map +1 -0
  80. package/dist/791.js +1 -1
  81. package/dist/791.js.map +1 -1
  82. package/dist/795.js +1 -1
  83. package/dist/8163.js +1 -1
  84. package/dist/8349.js +1 -1
  85. package/dist/8371.js +1 -1
  86. package/dist/841.js +1 -1
  87. package/dist/841.js.map +1 -1
  88. package/dist/8618.js +1 -1
  89. package/dist/8764.js +2 -0
  90. package/dist/8764.js.map +1 -0
  91. package/dist/8898.js +1 -1
  92. package/dist/8898.js.map +1 -1
  93. package/dist/890.js +1 -1
  94. package/dist/9214.js +1 -1
  95. package/dist/9321.js +1 -1
  96. package/dist/9321.js.map +1 -1
  97. package/dist/9452.js +1 -1
  98. package/dist/9452.js.map +1 -1
  99. package/dist/9569.js +1 -1
  100. package/dist/9695.js +1 -1
  101. package/dist/9695.js.map +1 -1
  102. package/dist/986.js +1 -1
  103. package/dist/9879.js +1 -1
  104. package/dist/9900.js +1 -1
  105. package/dist/9910.js +1 -1
  106. package/dist/9910.js.map +1 -1
  107. package/dist/9913.js +1 -1
  108. package/dist/main.js +1 -1
  109. package/dist/main.js.map +1 -1
  110. package/dist/openmrs-esm-laboratory-app.js +1 -1
  111. package/dist/openmrs-esm-laboratory-app.js.buildmanifest.json +274 -172
  112. package/dist/openmrs-esm-laboratory-app.js.map +1 -1
  113. package/dist/routes.json +1 -1
  114. package/package.json +3 -1
  115. package/src/bill/bill.resource.ts +60 -0
  116. package/src/components/lab-results/_interpretation.scss +67 -0
  117. package/src/components/lab-results/lab-results.component.tsx +188 -0
  118. package/src/components/lab-results/lab-results.resource.ts +0 -0
  119. package/src/components/lab-results/lab-results.scss +39 -0
  120. package/src/components/lab-results/utils.ts +99 -0
  121. package/src/components/orders-table/list-order-details.component.tsx +9 -5
  122. package/src/components/orders-table/ordered-actions-extension-slot/ordered-actions-extension-slot.tsx +84 -0
  123. package/src/components/orders-table/orders-data-table.component.tsx +59 -12
  124. package/src/components/orders-table/orders-data-table.test.tsx +2 -2
  125. package/src/components/orders-table/priority-tag.component.tsx +60 -0
  126. package/src/components/orders-table/priority-tag.scss +12 -0
  127. package/src/config-schema.ts +64 -7
  128. package/src/constants.ts +1 -1
  129. package/src/index.ts +8 -1
  130. package/src/lab-tabs/actions/add-lab-request-results-action.component.tsx +22 -4
  131. package/src/lab-tabs/actions/amend-lab-results-action.component.tsx +3 -1
  132. package/src/lab-tabs/actions/generate-bill-request-action.component.tsx +46 -0
  133. package/src/lab-tabs/actions/pickup-lab-request-action.component.tsx +6 -4
  134. package/src/lab-tabs/data-table-extensions/pending-review-lab-request-table.extension.tsx +1 -1
  135. package/src/lab-tabs/modals/approval-lab-results-modal.component.tsx +79 -29
  136. package/src/lab-tabs/modals/approval-lab-results-modal.scss +9 -0
  137. package/src/lab-tiles/pending-review-lab-results-tile.component.tsx +1 -1
  138. package/src/laboratory.resource.ts +390 -7
  139. package/src/routes.json +17 -2
  140. package/src/types.ts +183 -1
  141. package/src/utils/utils.ts +35 -0
  142. package/translations/en.json +12 -0
  143. package/dist/3106.js +0 -1
  144. package/dist/3106.js.map +0 -1
  145. package/dist/4535.js +0 -1
  146. package/dist/4535.js.map +0 -1
  147. package/dist/5048.js +0 -2
  148. package/dist/5048.js.map +0 -1
  149. package/dist/5339.js +0 -1
  150. package/dist/5339.js.map +0 -1
  151. package/dist/8627.js +0 -2
  152. package/dist/8627.js.map +0 -1
  153. /package/dist/{8627.js.LICENSE.txt → 3169.js.LICENSE.txt} +0 -0
  154. /package/dist/{5048.js.LICENSE.txt → 8764.js.LICENSE.txt} +0 -0
@@ -1,8 +1,23 @@
1
- import React, { useState } from 'react';
1
+ import React, { useCallback, useEffect, useState } from 'react';
2
2
  import { Button, ModalBody, ModalFooter, ModalHeader } from '@carbon/react';
3
- import { ExtensionSlot, showNotification, showSnackbar, useAbortController, type Order } from '@openmrs/esm-framework';
3
+ import {
4
+ type Config,
5
+ ExtensionSlot,
6
+ showNotification,
7
+ showSnackbar,
8
+ useAbortController,
9
+ useConfig,
10
+ type Order,
11
+ showModal,
12
+ } from '@openmrs/esm-framework';
4
13
  import { useTranslation } from 'react-i18next';
5
- import { setFulfillerStatus, useInvalidateLabOrders } from '../../laboratory.resource';
14
+ import {
15
+ setFulfillerStatus,
16
+ updateObservationAndOrder,
17
+ useInvalidateLabOrders,
18
+ useMappedLabConcepts,
19
+ } from '../../laboratory.resource';
20
+ import styles from './approval-lab-results-modal.scss';
6
21
 
7
22
  interface ApproveLabResultsModal {
8
23
  closeModal: () => void;
@@ -14,31 +29,66 @@ const ApproveLabResultsModal: React.FC<ApproveLabResultsModal> = ({ order, close
14
29
  const [isSubmitting, setIsSubmitting] = useState(false);
15
30
  const abortController = useAbortController();
16
31
  const invalidateLabOrders = useInvalidateLabOrders();
32
+ const { laboratoryOrderTypeUuid } = useConfig<Config>();
33
+ const { completeLabResults, values, mutateResults } = useMappedLabConcepts(order);
17
34
 
18
35
  const handleApproval = () => {
19
36
  setIsSubmitting(true);
20
- setFulfillerStatus(order.uuid, 'COMPLETED', abortController).then(
21
- () => {
22
- invalidateLabOrders();
23
- setIsSubmitting(false);
24
- closeModal();
25
- showSnackbar({
26
- isLowContrast: true,
27
- title: t('resultsApproved', 'Results Approved'),
28
- kind: 'success',
29
- subtitle: t('labResultsApprovedSuccessfully', 'Lab results have been successfully approved and finalized'),
37
+ if (completeLabResults && completeLabResults.length > 0) {
38
+ updateObservationAndOrder(order, 'FINAL', 'COMPLETED', abortController, values, completeLabResults)
39
+ .then(() => {
40
+ invalidateLabOrders();
41
+ setIsSubmitting(false);
42
+ closeModal();
43
+ showSnackbar({
44
+ isLowContrast: true,
45
+ title: t('resultsApproved', 'Results Approved'),
46
+ kind: 'success',
47
+ subtitle: t('labResultsApprovedSuccessfully', 'Lab results have been successfully approved and finalized'),
48
+ });
49
+ })
50
+ .catch((error) => {
51
+ setIsSubmitting(false);
52
+ showNotification({
53
+ title: t('errorApprovingResults', 'Error approving results'),
54
+ kind: 'error',
55
+ critical: true,
56
+ description: error?.message,
57
+ });
30
58
  });
31
- },
32
- (error) => {
33
- setIsSubmitting(false);
34
- showNotification({
35
- title: t('errorApprovingResults', 'Error approving results'),
36
- kind: 'error',
37
- critical: true,
38
- description: error?.message,
39
- });
40
- },
41
- );
59
+ }
60
+ // setFulfillerStatus(order.uuid, 'COMPLETED', abortController).then(
61
+ // () => {
62
+ // // invalidateLabOrders();
63
+ // setIsSubmitting(false);
64
+ // closeModal();
65
+ // showSnackbar({
66
+ // isLowContrast: true,
67
+ // title: t('resultsApproved', 'Results Approved'),
68
+ // kind: 'success',
69
+ // subtitle: t('labResultsApprovedSuccessfully', 'Lab results have been successfully approved and finalized'),
70
+ // });
71
+ // },
72
+ // (error) => {
73
+ // setIsSubmitting(false);
74
+ // showNotification({
75
+ // title: t('errorApprovingResults', 'Error approving results'),
76
+ // kind: 'error',
77
+ // critical: true,
78
+ // description: error?.message,
79
+ // });
80
+ // },
81
+ // );
82
+ };
83
+
84
+ const handleReject = () => {
85
+ closeModal();
86
+ setTimeout(() => {
87
+ const dispose = showModal('reject-lab-request-modal', {
88
+ closeModal: () => dispose(),
89
+ order,
90
+ });
91
+ }, 0);
42
92
  };
43
93
 
44
94
  return (
@@ -51,13 +101,13 @@ const ApproveLabResultsModal: React.FC<ApproveLabResultsModal> = ({ order, close
51
101
  'You are about to approve and finalize these lab results. Once approved, the results will be marked as complete and made available to clinicians. Are you sure you want to proceed?',
52
102
  )}
53
103
  </p>
54
- <>
55
- <ExtensionSlot state={{ order: order }} name="completed-lab-order-results-slot" />
56
- </>
104
+ <div className={styles.resultsContainer}>
105
+ <ExtensionSlot state={{ order: order }} name="completed-lab-order-results-slot-1" />
106
+ </div>
57
107
  </ModalBody>
58
108
  <ModalFooter>
59
- <Button kind="secondary" onClick={closeModal}>
60
- {t('cancel', 'Cancel')}
109
+ <Button kind="danger" onClick={handleReject}>
110
+ {t('rejectLabResults', 'Reject lab results')}
61
111
  </Button>
62
112
  <Button type="submit" onClick={handleApproval} disabled={isSubmitting}>
63
113
  {t('approveResults', 'Approve Results')}
@@ -0,0 +1,9 @@
1
+ .resultsContainer {
2
+ max-height: 500px;
3
+ overflow-y: auto;
4
+ overflow-x: hidden;
5
+ border: 1px solid #e0e0e0;
6
+ border-radius: 4px;
7
+ padding: 16px;
8
+ margin-top: 16px;
9
+ }
@@ -6,7 +6,7 @@ import LabSummaryTile from '../components/summary-tile/lab-summary-tile.componen
6
6
  const PendingReviewLabRequestsTile = () => {
7
7
  const { t } = useTranslation();
8
8
  const { labOrders } = useLabOrders({
9
- status: 'DRAFT',
9
+ status: 'ON_HOLD',
10
10
  excludeCanceled: false,
11
11
  });
12
12
 
@@ -1,9 +1,28 @@
1
- import { useCallback } from 'react';
1
+ import { useCallback, useEffect, useMemo, useState } from 'react';
2
2
  import dayjs from 'dayjs';
3
3
  import useSWR, { mutate } from 'swr';
4
- import { openmrsFetch, type Order, restBaseUrl, useAppContext, useConfig } from '@openmrs/esm-framework';
5
- import type { DateFilterContext, FulfillerStatus } from './types';
4
+ import {
5
+ openmrsFetch,
6
+ type Order,
7
+ restBaseUrl,
8
+ useAppContext,
9
+ useConfig,
10
+ useSession,
11
+ type FetchResponse,
12
+ showSnackbar,
13
+ } from '@openmrs/esm-framework';
14
+ import type {
15
+ DateFilterContext,
16
+ Encounter,
17
+ FulfillerStatus,
18
+ LabOrderConcept,
19
+ Observation,
20
+ ObservationStatus,
21
+ ObservationValue,
22
+ QueueEntryResult,
23
+ } from './types';
6
24
  import { type Config } from './config-schema';
25
+ import { getEtlBaseUrl } from './utils/utils';
7
26
 
8
27
  const useLabOrdersDefaultParams: UseLabOrdersParams = {
9
28
  status: null,
@@ -36,11 +55,12 @@ export function useLabOrders(params: Partial<UseLabOrdersParams> = useLabOrdersD
36
55
  const { dateRange } = useAppContext<DateFilterContext>('laboratory-date-filter') ?? {
37
56
  dateRange: [dayjs().startOf('day').toDate(), new Date()],
38
57
  };
58
+ const { sessionLocation } = useSession();
39
59
 
40
- const { laboratoryOrderTypeUuid } = useConfig();
41
- const customRepresentation = `custom:(uuid,orderNumber,patient:(uuid,display,person:(uuid,display,age,birthdate,gender)${
60
+ const { laboratoryOrderTypeUuid, filterByCurrentLocation } = useConfig<Config>();
61
+ const customRepresentation = `custom:(uuid,orderNumber,patient:(uuid,display,person:(uuid,display,age,birthdate,gender,attributes)${
42
62
  includePatientId ? ',identifiers' : ''
43
- }),concept:(uuid,display),action,careSetting:(uuid,display,description,careSettingType,display),previousOrder,dateActivated,scheduledDate,dateStopped,autoExpireDate,encounter:(uuid,display),orderer:(uuid,display),orderReason,orderReasonNonCoded,orderType:(uuid,display,name,description,conceptClasses,parent),urgency,instructions,commentToFulfiller,display,fulfillerStatus,fulfillerComment,accessionNumber,specimenSource,laterality,clinicalHistory,frequency,numberOfRepeats)`;
63
+ }),concept:(uuid,display),action,careSetting:(uuid,display,description,careSettingType,display),previousOrder,dateActivated,scheduledDate,dateStopped,autoExpireDate,encounter:(uuid,display,location:(uuid)),orderer:(uuid,display),orderReason,orderReasonNonCoded,orderType:(uuid,display,name,description,conceptClasses,parent),urgency,instructions,commentToFulfiller,display,fulfillerStatus,fulfillerComment,accessionNumber,specimenSource,laterality,clinicalHistory,frequency,numberOfRepeats)`;
44
64
  let url = `${restBaseUrl}/order?orderTypes=${laboratoryOrderTypeUuid}&v=${customRepresentation}`;
45
65
  url = status ? url + `&fulfillerStatus=${status}` : url;
46
66
  url = excludeCanceled ? `${url}&excludeCanceledAndExpired=true&excludeDiscontinueOrders=true` : url;
@@ -54,9 +74,14 @@ export function useLabOrders(params: Partial<UseLabOrdersParams> = useLabOrdersD
54
74
  data: { results: Array<Order> };
55
75
  }>(`${url}`, openmrsFetch);
56
76
 
57
- const filteredOrders = data?.data?.results?.filter(
77
+ let filteredOrders = data?.data?.results?.filter(
58
78
  (order) => !newOrdersOnly || (order?.action === 'NEW' && order?.fulfillerStatus === null),
59
79
  );
80
+
81
+ if (filterByCurrentLocation) {
82
+ filteredOrders = filteredOrders?.filter((order) => order.encounter?.location?.uuid === sessionLocation?.uuid);
83
+ }
84
+
60
85
  return {
61
86
  labOrders: filteredOrders ?? [],
62
87
  isLoading,
@@ -106,3 +131,361 @@ export function useInvalidateLabOrders() {
106
131
  );
107
132
  }, [laboratoryOrderTypeUuid]);
108
133
  }
134
+
135
+ export function useQueueEntries(patientUuid: string = '') {
136
+ const [etlBaseUrl, setEtlBaseUrl] = useState('');
137
+ const { sessionLocation } = useSession();
138
+ const { serviceUuid } = useConfig<Config>();
139
+
140
+ useEffect(() => {
141
+ const fetchEtlBaseUrl = async () => {
142
+ const baseUrl = await getEtlBaseUrl();
143
+ setEtlBaseUrl(baseUrl);
144
+ };
145
+ fetchEtlBaseUrl();
146
+ }, []);
147
+
148
+ const url = `${etlBaseUrl}/queue-entry?locationUuid=${sessionLocation?.uuid}&serviceUuid=${serviceUuid}`;
149
+ const { data, error, mutate, isLoading, isValidating } = useSWR<{
150
+ data: { data: Array<QueueEntryResult> };
151
+ }>(etlBaseUrl ? `${url}` : null, openmrsFetch);
152
+
153
+ let filteredQueueEntries = data?.data?.data;
154
+
155
+ if (patientUuid) {
156
+ filteredQueueEntries = filteredQueueEntries?.filter((queueEntry) => queueEntry.patient_uuid === patientUuid);
157
+ }
158
+
159
+ return {
160
+ queueEntries: filteredQueueEntries ?? [],
161
+ isLoading,
162
+ isError: error,
163
+ mutate,
164
+ isValidating,
165
+ };
166
+ }
167
+
168
+ const labEncounterRepresentation =
169
+ 'custom:(uuid,encounterDatetime,encounterType:(uuid,display),location:(uuid,name),patient:(uuid,display,person:(uuid,display,gender,age)),encounterProviders:(uuid,provider:(uuid,name)),obs:(uuid,obsDatetime,voided,groupMembers:(uuid,concept:(uuid,name:(uuid,name)),value:(uuid,display,name:(uuid,name),names:(uuid,conceptNameType,name)),interpretation),formFieldNamespace,formFieldPath,order:(uuid,display),concept:(uuid,name:(uuid,name)),value:(uuid,display,name:(uuid,name),names:(uuid,conceptNameType,name)),interpretation))';
170
+ const labConceptRepresentation =
171
+ 'custom:(uuid,display,name,datatype,set,answers,hiNormal,hiAbsolute,hiCritical,lowNormal,lowAbsolute,lowCritical,units,allowDecimal,' +
172
+ 'setMembers:(uuid,display,answers,datatype,hiNormal,hiAbsolute,hiCritical,lowNormal,lowAbsolute,lowCritical,units,allowDecimal,set,setMembers:(uuid)))';
173
+ const conceptObsRepresentation = 'custom:(uuid,display,concept:(uuid,display),groupMembers,value)';
174
+
175
+ export function useLabEncounter(encounterUuid: string) {
176
+ const apiUrl = `${restBaseUrl}/encounter/${encounterUuid}?v=${labEncounterRepresentation}`;
177
+
178
+ const { data, error, isLoading, isValidating, mutate } = useSWR<FetchResponse<Encounter>, Error>(
179
+ apiUrl,
180
+ openmrsFetch,
181
+ );
182
+
183
+ return {
184
+ encounter: data?.data,
185
+ isLoading,
186
+ error: error,
187
+ isValidating,
188
+ mutate,
189
+ };
190
+ }
191
+
192
+ export function useObservation(obsUuid: string) {
193
+ const url = `${restBaseUrl}/obs/${obsUuid}?v=${conceptObsRepresentation}`;
194
+
195
+ const { data, error, isLoading, isValidating, mutate } = useSWR<{ data: Observation }, Error>(
196
+ obsUuid ? url : null,
197
+ openmrsFetch,
198
+ );
199
+ return {
200
+ data: data?.data,
201
+ isLoading,
202
+ error,
203
+ isValidating,
204
+ mutate,
205
+ };
206
+ }
207
+
208
+ export function useObservations(obsUuids: Array<string>) {
209
+ const fetchMultipleObservations = async (): Promise<Array<Observation>> => {
210
+ const results = await Promise.all(
211
+ obsUuids.map(async (uuid) => {
212
+ const url = `${restBaseUrl}/obs/${uuid}?v=${conceptObsRepresentation}`;
213
+ const res = await openmrsFetch(url);
214
+ return res.data;
215
+ }),
216
+ );
217
+
218
+ return results;
219
+ };
220
+
221
+ const { data, error, isLoading, isValidating, mutate } = useSWR<Observation[], Error>(
222
+ obsUuids && obsUuids.length > 0 ? ['observations', ...obsUuids] : null,
223
+ fetchMultipleObservations,
224
+ );
225
+
226
+ return {
227
+ data: data ?? [],
228
+ isLoading,
229
+ error,
230
+ isValidating,
231
+ mutate,
232
+ };
233
+ }
234
+
235
+ export function useCompletedLabResults(order: Order) {
236
+ const {
237
+ encounter,
238
+ isLoading: isLoadingEncounter,
239
+ mutate: mutateLabOrders,
240
+ error: encounterError,
241
+ } = useLabEncounter(order.encounter.uuid);
242
+ const {
243
+ data: observation,
244
+ isLoading: isLoadingObs,
245
+ error: isErrorObs,
246
+ mutate: mutateObs,
247
+ } = useObservation(encounter?.obs.find((obs) => obs?.concept?.uuid === order?.concept?.uuid)?.uuid ?? '');
248
+
249
+ return {
250
+ isLoading: isLoadingEncounter || isLoadingObs,
251
+ completeLabResult: observation,
252
+ mutate: () => {
253
+ mutateLabOrders();
254
+ mutateObs();
255
+ },
256
+ error: isErrorObs ?? encounterError,
257
+ };
258
+ }
259
+
260
+ export function useCompletedLabResultsArray(order: Order) {
261
+ const {
262
+ encounter,
263
+ isLoading: isLoadingEncounter,
264
+ mutate: mutateLabOrders,
265
+ error: encounterError,
266
+ } = useLabEncounter(order.encounter.uuid);
267
+
268
+ const obsUuids = encounter?.obs.filter((o) => o?.order.uuid === order?.uuid).map((o) => o.uuid);
269
+
270
+ const { data: observations, isLoading: isLoadingObs, error: errorObs, mutate: mutateObs } = useObservations(obsUuids);
271
+
272
+ return {
273
+ isLoading: isLoadingEncounter || isLoadingObs,
274
+ completeLabResults: observations,
275
+ mutate: () => {
276
+ mutateLabOrders();
277
+ mutateObs();
278
+ },
279
+ error: errorObs ?? encounterError,
280
+ };
281
+ }
282
+
283
+ function getUrlForConcept(conceptUuid: string) {
284
+ return `${restBaseUrl}/concept/${conceptUuid}?v=${labConceptRepresentation}`;
285
+ }
286
+
287
+ async function fetchAllSetMembers(conceptUuid: string): Promise<LabOrderConcept> {
288
+ const conceptResponse = await openmrsFetch<LabOrderConcept>(getUrlForConcept(conceptUuid));
289
+ const concept = conceptResponse.data;
290
+ const secondLevelSetMembers = concept.set
291
+ ? concept.setMembers
292
+ .map((member) => (member.set ? member.setMembers.map((lowerMember) => lowerMember.uuid) : []))
293
+ .flat()
294
+ : [];
295
+ if (secondLevelSetMembers.length > 0) {
296
+ const concepts = await Promise.all(secondLevelSetMembers.map((uuid) => fetchAllSetMembers(uuid)));
297
+ const uuidMap = concepts.reduce((acc, c) => {
298
+ acc[c.uuid] = c;
299
+ return acc;
300
+ }, {} as Record<string, LabOrderConcept>);
301
+ concept.setMembers = concept.setMembers.map((member) => {
302
+ if (member.set) {
303
+ member.setMembers = member.setMembers.map((lowerMember) => uuidMap[lowerMember.uuid]);
304
+ }
305
+ return member;
306
+ });
307
+ }
308
+
309
+ return concept;
310
+ }
311
+
312
+ export function useOrderConceptByUuid(uuid: string) {
313
+ const apiUrl = `${restBaseUrl}/concept/${uuid}?v=${labConceptRepresentation}`;
314
+
315
+ const { data, error, isLoading, isValidating, mutate } = useSWR<LabOrderConcept, Error>(uuid, fetchAllSetMembers);
316
+ /**
317
+ * We are fetching 2 levels of set members at one go.
318
+ */
319
+
320
+ const results = useMemo(
321
+ () => ({
322
+ concept: data,
323
+ isLoading,
324
+ error,
325
+ isValidating,
326
+ mutate,
327
+ }),
328
+ [data, error, isLoading, isValidating, mutate],
329
+ );
330
+
331
+ return results;
332
+ }
333
+
334
+ export function useOrderConceptsByUuids(uuids: Array<string>) {
335
+ const { data, error, isLoading, isValidating, mutate } = useSWR<Array<LabOrderConcept>, Error>(
336
+ uuids.length ? ['concepts', ...uuids] : null,
337
+ () => Promise.all(uuids.map((uuid) => fetchAllSetMembers(uuid))),
338
+ );
339
+
340
+ const results = useMemo(
341
+ () => ({
342
+ concepts: data ?? [],
343
+ isLoading,
344
+ error,
345
+ isValidating,
346
+ mutate,
347
+ }),
348
+ [data, error, isLoading, isValidating, mutate],
349
+ );
350
+
351
+ return results;
352
+ }
353
+
354
+ export function updateObservation(observationUuid: string, payload: Record<string, any>) {
355
+ return openmrsFetch(`${restBaseUrl}/obs/${observationUuid}`, {
356
+ method: 'POST',
357
+ headers: {
358
+ 'Content-Type': 'application/json',
359
+ },
360
+ body: JSON.stringify(payload),
361
+ });
362
+ }
363
+
364
+ export const isCoded = (concept: LabOrderConcept) => concept.datatype?.display === 'Coded';
365
+ export const isNumeric = (concept: LabOrderConcept) => concept.datatype?.display === 'Numeric';
366
+ export const isPanel = (concept: LabOrderConcept) => concept.setMembers?.length > 0;
367
+ export const isText = (concept: LabOrderConcept) => concept.datatype?.display === 'Text';
368
+
369
+ export function useMappedLabConcepts(order: Order) {
370
+ const [orderConceptUuids, setOrderConceptUuids] = useState([]);
371
+ const {
372
+ isLoading: isLoadingLabResultsArray,
373
+ completeLabResults,
374
+ mutate: mutateResults,
375
+ } = useCompletedLabResultsArray(order);
376
+ const { isLoading: isLoadingResultConcepts, concepts: conceptArray } = useOrderConceptsByUuids(orderConceptUuids);
377
+ const [object, setObject] = useState({});
378
+
379
+ useEffect(() => {
380
+ if (Array.isArray(completeLabResults) && completeLabResults.length > 1) {
381
+ const conceptUuids = completeLabResults.map((r) => r.concept.uuid);
382
+ setOrderConceptUuids(conceptUuids);
383
+ }
384
+
385
+ const mapLabConcepts = () => {
386
+ if (Array.isArray(conceptArray) && conceptArray.length > 1) {
387
+ conceptArray.forEach((concept, index) => {
388
+ const completeLabResult = completeLabResults.find((r) => r.concept.uuid === concept.uuid);
389
+ if (concept && completeLabResult) {
390
+ if (isCoded(concept) && typeof completeLabResult?.value === 'object' && completeLabResult?.value?.uuid) {
391
+ const v = completeLabResult.value.uuid;
392
+ setObject((obj) => ({ ...obj, [concept.uuid]: v }));
393
+ // object[concept.uuid] = completeLabResult.value.uuid;
394
+ // setValue(concept.uuid, completeLabResult.value.uuid);
395
+ } else if (isNumeric(concept) && completeLabResult?.value) {
396
+ const v = parseFloat(completeLabResult.value as string);
397
+ setObject((obj) => ({ ...obj, [concept.uuid]: v }));
398
+ // object[concept.uuid] = parseFloat(completeLabResult.value as string);
399
+ // setValue(concept.uuid, parseFloat(completeLabResult.value as string));
400
+ } else if (isText(concept) && completeLabResult?.value) {
401
+ const v = completeLabResult?.value;
402
+ setObject((obj) => ({ ...obj, [concept.uuid]: v }));
403
+ // object[concept.uuid] = completeLabResult?.value;
404
+ // setValue(concept.uuid, completeLabResult?.value);
405
+ } else if (isPanel(concept)) {
406
+ concept.setMembers.forEach((member) => {
407
+ const obs = completeLabResult.groupMembers.find((v) => v.concept.uuid === member.uuid);
408
+ let value: ObservationValue;
409
+ if (isCoded(member)) {
410
+ value = typeof obs?.value === 'object' ? obs.value.uuid : obs?.value;
411
+ } else if (isNumeric(member)) {
412
+ value = obs?.value ? parseFloat(obs.value as string) : undefined;
413
+ } else if (isText(member)) {
414
+ value = obs?.value;
415
+ }
416
+ if (value) {
417
+ const v = value;
418
+ setObject((obj) => ({ ...obj, [member.uuid]: v }));
419
+ // object[member.uuid] = value; //setValue(member.uuid, value);
420
+ }
421
+ });
422
+ }
423
+ }
424
+ });
425
+ }
426
+ };
427
+
428
+ mapLabConcepts();
429
+
430
+ // if (conceptArray) {
431
+ // mapLabConcepts();
432
+ // }
433
+ }, [completeLabResults, conceptArray]);
434
+
435
+ return {
436
+ values: object,
437
+ completeLabResults,
438
+ isLoading: isLoadingLabResultsArray || isLoadingResultConcepts,
439
+ mutateResults,
440
+ };
441
+ }
442
+
443
+ export async function updateObservationAndOrder(
444
+ order: Order,
445
+ observationStatus: ObservationStatus,
446
+ fulfillerStatus: FulfillerStatus,
447
+ abortController: AbortController,
448
+ values,
449
+ completeLabResults: Array<any>,
450
+ ) {
451
+ // const { completeLabResults, values, mutateResults } = useMappedLabConcepts(order);
452
+
453
+ const updateTasks = Object.entries(values)
454
+ .filter(([, value]) => value !== undefined && value !== null && value !== '')
455
+ .map(([conceptUuid, value]) => {
456
+ let obs = completeLabResults.find((r) => r.concept.uuid === conceptUuid);
457
+ if (!obs) {
458
+ for (const result of completeLabResults) {
459
+ obs = result.groupMembers?.find((m) => m.concept.uuid === conceptUuid);
460
+ if (obs) break;
461
+ }
462
+ }
463
+ const status = observationStatus;
464
+ return updateObservation(obs?.uuid, { value, status });
465
+ });
466
+ const updateResults = await Promise.allSettled(updateTasks);
467
+ const failedObsconceptUuids = updateResults.reduce((prev, curr, index) => {
468
+ if (curr.status === 'rejected') {
469
+ return [...prev, Object.keys(values).at(index)];
470
+ }
471
+ return prev;
472
+ }, []);
473
+
474
+ await setFulfillerStatus(order.uuid, fulfillerStatus, abortController);
475
+
476
+ // mutateResults();
477
+
478
+ const showNotification = (kind: 'error' | 'success', message: string) => {
479
+ showSnackbar({
480
+ title: kind === 'success' ? 'Save lab results' : 'Error saving lab results',
481
+ kind: kind,
482
+ subtitle: message,
483
+ });
484
+ };
485
+
486
+ if (failedObsconceptUuids.length) {
487
+ showNotification('error', 'Could not save obs with concept uuids ' + failedObsconceptUuids.join(', '));
488
+ } else {
489
+ showNotification('success', 'Lab results have been successfully updated');
490
+ }
491
+ }
package/src/routes.json CHANGED
@@ -13,12 +13,14 @@
13
13
  {
14
14
  "name": "laboratory-dashboard",
15
15
  "slot": "laboratory-dashboard-slot",
16
- "component": "root"
16
+ "component": "root",
17
+ "privileges": ["O3 View Labs Dashboard"]
17
18
  },
18
19
  {
19
20
  "name": "laboratory-dashboard-link",
20
21
  "slot": "homepage-dashboard-slot",
21
22
  "component": "laboratoryDashboardLink",
23
+ "privileges": ["O3 View Labs Dashboard"],
22
24
  "meta": {
23
25
  "name": "laboratory",
24
26
  "slot": "laboratory-dashboard-slot",
@@ -152,6 +154,19 @@
152
154
  "name": "add-lab-request-results-action",
153
155
  "component": "addLabRequestResultsAction",
154
156
  "slot": "inprogress-tests-actions-slot"
157
+ },
158
+ {
159
+ "name": "generate-bill-request-action",
160
+ "component": "generateBillRequestAction",
161
+ "slot": "tests-ordered-actions-slot"
162
+ },
163
+ {
164
+ "name": "lab-result-1",
165
+ "component": "labResult",
166
+ "slot": "completed-lab-order-results-slot-1",
167
+ "meta": {
168
+ "fullWidth": false
169
+ }
155
170
  }
156
171
  ],
157
172
  "workspaces2": [
@@ -201,4 +216,4 @@
201
216
  "scopePattern": "/home/laboratory"
202
217
  }
203
218
  ]
204
- }
219
+ }