@openmrs/esm-patient-tests-app 11.3.1-patch.9064 → 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.
- package/.turbo/turbo-build.log +22 -22
- package/dist/1119.js +1 -1
- package/dist/1197.js +1 -1
- package/dist/{6231.js → 1477.js} +1 -1
- package/dist/1477.js.map +1 -0
- package/dist/1638.js +1 -1
- package/dist/1638.js.map +1 -1
- package/dist/1935.js +1 -1
- package/dist/1935.js.map +1 -1
- package/dist/2146.js +1 -1
- package/dist/2690.js +1 -1
- package/dist/3099.js +1 -1
- package/dist/34.js +1 -1
- package/dist/34.js.map +1 -1
- package/dist/3509.js +1 -1
- package/dist/3509.js.map +1 -1
- package/dist/3584.js +1 -1
- package/dist/4055.js +1 -1
- package/dist/4132.js +1 -1
- package/dist/4300.js +1 -1
- package/dist/4335.js +1 -1
- package/dist/439.js +1 -0
- package/dist/4618.js +1 -1
- package/dist/4652.js +1 -1
- package/dist/4944.js +1 -1
- package/dist/5173.js +1 -1
- package/dist/5241.js +1 -1
- package/dist/5442.js +1 -1
- package/dist/5661.js +1 -1
- package/dist/5670.js +1 -1
- package/dist/5670.js.map +1 -1
- package/dist/6022.js +1 -1
- package/dist/6113.js +1 -0
- package/dist/6113.js.map +1 -0
- package/dist/6301.js +1 -1
- package/dist/6301.js.map +1 -1
- package/dist/6336.js +1 -0
- package/dist/6336.js.map +1 -0
- package/dist/6468.js +1 -1
- package/dist/6589.js +1 -0
- package/dist/6679.js +1 -1
- package/dist/6840.js +1 -1
- package/dist/6859.js +1 -1
- package/dist/7097.js +1 -1
- package/dist/7159.js +1 -1
- package/dist/7202.js +1 -0
- package/dist/7202.js.map +1 -0
- package/dist/723.js +1 -1
- package/dist/7617.js +1 -1
- package/dist/790.js +1 -1
- package/dist/790.js.map +1 -1
- package/dist/795.js +1 -1
- package/dist/8163.js +1 -1
- package/dist/8307.js +2 -0
- package/dist/8307.js.map +1 -0
- package/dist/8349.js +1 -1
- package/dist/8371.js +1 -0
- package/dist/8555.js +2 -0
- package/dist/8555.js.map +1 -0
- package/dist/8618.js +1 -1
- package/dist/890.js +1 -1
- package/dist/9214.js +1 -1
- package/dist/9538.js +1 -1
- package/dist/9569.js +1 -1
- package/dist/986.js +1 -1
- package/dist/9879.js +1 -1
- package/dist/9895.js +1 -1
- package/dist/9900.js +1 -1
- package/dist/9913.js +1 -1
- package/dist/main.js +1 -1
- package/dist/main.js.map +1 -1
- package/dist/openmrs-esm-patient-tests-app.js +1 -1
- package/dist/openmrs-esm-patient-tests-app.js.buildmanifest.json +319 -249
- package/dist/openmrs-esm-patient-tests-app.js.map +1 -1
- package/dist/routes.json +1 -1
- package/package.json +3 -3
- package/src/edit-test-results/modal/edit-lab-results.modal.tsx +6 -2
- package/src/index.ts +1 -1
- package/src/routes.json +2 -2
- package/src/test-orders/add-test-order/add-test-order.test.tsx +13 -10
- package/src/test-orders/add-test-order/add-test-order.workspace.tsx +43 -7
- package/src/test-orders/add-test-order/test-order-form.component.tsx +41 -7
- package/src/test-orders/add-test-order/test-type-search.component.tsx +56 -8
- package/src/test-orders/lab-order-basket-panel/lab-icon.component.tsx +27 -0
- package/src/test-orders/lab-order-basket-panel/lab-order-basket-panel.extension.tsx +62 -15
- package/src/test-orders/lab-order-basket-panel/lab-order-basket-panel.scss +26 -11
- package/src/test-orders/lab-order-basket-panel/lab-order-basket-panel.test.tsx +18 -5
- package/src/test-results/filter/filter-context.test.tsx +556 -0
- package/src/test-results/filter/filter-context.tsx +1 -1
- package/src/test-results/filter/filter-reducer.test.ts +540 -0
- package/src/test-results/filter/filter-reducer.ts +1 -1
- package/src/test-results/filter/filter-set.component.tsx +75 -48
- package/src/test-results/filter/filter-set.test.tsx +694 -0
- package/src/test-results/filter/filter-types.ts +24 -1
- package/src/test-results/grouped-timeline/grid.component.tsx +4 -2
- package/src/test-results/grouped-timeline/grouped-timeline.component.tsx +20 -22
- package/src/test-results/grouped-timeline/grouped-timeline.test.tsx +52 -2
- package/src/test-results/grouped-timeline/reference-range-helpers.test.ts +308 -0
- package/src/test-results/grouped-timeline/reference-range-helpers.ts +161 -0
- package/src/test-results/grouped-timeline/timeline-data-group.component.tsx +13 -6
- package/src/test-results/grouped-timeline/useObstreeData.test.ts +471 -0
- package/src/test-results/grouped-timeline/useObstreeData.ts +108 -13
- package/src/test-results/individual-results-table/individual-results-table.component.tsx +18 -6
- package/src/test-results/individual-results-table/individual-results-table.test.tsx +65 -3
- package/src/test-results/individual-results-table-tablet/helper.tsx +8 -2
- package/src/test-results/individual-results-table-tablet/individual-results-table-tablet.component.tsx +5 -5
- package/src/test-results/individual-results-table-tablet/lab-set-panel.component.tsx +2 -1
- package/src/test-results/individual-results-table-tablet/usePanelData.tsx +40 -26
- package/src/test-results/loadPatientTestData/helpers.test.ts +834 -0
- package/src/test-results/loadPatientTestData/helpers.ts +143 -12
- package/src/test-results/loadPatientTestData/loadPatientData.ts +66 -11
- package/src/test-results/loadPatientTestData/usePatientResultsData.ts +20 -9
- package/src/test-results/overview/common-datatable.component.tsx +1 -1
- package/src/test-results/overview/external-overview.extension.tsx +1 -2
- package/src/test-results/overview/useOverviewData.ts +22 -10
- package/src/test-results/print-modal/print-modal.extension.tsx +1 -1
- package/src/test-results/results-viewer/results-viewer.extension.tsx +12 -7
- package/src/test-results/tree-view/tree-view.component.tsx +31 -8
- package/src/test-results/tree-view/tree-view.test.tsx +119 -2
- package/src/test-results/trendline/trendline-resource.tsx +48 -5
- package/src/test-results/trendline/trendline.component.tsx +88 -52
- package/src/test-results/ui-elements/{resetFiltersEmptyState → reset-filters-empty-state}/filter-empty-data-illustration.tsx +2 -2
- package/src/test-results/ui-elements/{resetFiltersEmptyState → reset-filters-empty-state}/filter-empty-state.component.tsx +5 -6
- package/src/types.ts +20 -1
- package/translations/am.json +3 -4
- package/translations/ar.json +3 -4
- package/translations/ar_SY.json +3 -4
- package/translations/bn.json +3 -4
- package/translations/cs.json +119 -0
- package/translations/de.json +3 -4
- package/translations/en.json +3 -2
- package/translations/en_US.json +3 -4
- package/translations/es.json +3 -4
- package/translations/es_MX.json +3 -4
- package/translations/fr.json +5 -6
- package/translations/he.json +3 -4
- package/translations/hi.json +3 -4
- package/translations/hi_IN.json +3 -4
- package/translations/id.json +3 -4
- package/translations/it.json +3 -4
- package/translations/ka.json +3 -4
- package/translations/km.json +3 -4
- package/translations/ku.json +3 -4
- package/translations/ky.json +3 -4
- package/translations/lg.json +3 -4
- package/translations/ne.json +3 -4
- package/translations/pl.json +3 -4
- package/translations/pt.json +3 -4
- package/translations/pt_BR.json +3 -4
- package/translations/qu.json +3 -4
- package/translations/ro_RO.json +3 -4
- package/translations/ru_RU.json +3 -4
- package/translations/si.json +3 -4
- package/translations/sq.json +119 -0
- package/translations/sw.json +3 -4
- package/translations/sw_KE.json +3 -4
- package/translations/tr.json +3 -4
- package/translations/tr_TR.json +3 -4
- package/translations/uk.json +3 -4
- package/translations/uz.json +3 -4
- package/translations/uz@Latn.json +3 -4
- package/translations/uz_UZ.json +3 -4
- package/translations/vi.json +3 -4
- package/translations/zh.json +3 -4
- package/translations/zh_CN.json +3 -4
- package/translations/zh_TW.json +119 -0
- package/dist/1479.js +0 -1
- package/dist/1479.js.map +0 -1
- package/dist/2537.js +0 -1
- package/dist/2537.js.map +0 -1
- package/dist/4918.js +0 -1
- package/dist/4918.js.map +0 -1
- package/dist/5836.js +0 -2
- package/dist/5836.js.map +0 -1
- package/dist/6231.js.map +0 -1
- package/dist/7053.js +0 -2
- package/dist/7053.js.map +0 -1
- /package/dist/{7053.js.LICENSE.txt → 8307.js.LICENSE.txt} +0 -0
- /package/dist/{5836.js.LICENSE.txt → 8555.js.LICENSE.txt} +0 -0
- /package/src/test-results/ui-elements/{resetFiltersEmptyState/index.scss → reset-filters-empty-state/filter-empty-state.scss} +0 -0
|
@@ -7,6 +7,8 @@ import {
|
|
|
7
7
|
type ObsMetaInfo,
|
|
8
8
|
type OBSERVATION_INTERPRETATION,
|
|
9
9
|
} from '@openmrs/esm-patient-common-lib';
|
|
10
|
+
import type { FHIRObservationResource } from '../../types';
|
|
11
|
+
import { type ReferenceRanges } from '../grouped-timeline/reference-range-helpers';
|
|
10
12
|
|
|
11
13
|
const PAGE_SIZE = 300;
|
|
12
14
|
const CHUNK_PREFETCH_COUNT = 1;
|
|
@@ -37,7 +39,7 @@ export function addUserDataToCache(patientUuid: string, data: PatientData, indic
|
|
|
37
39
|
}
|
|
38
40
|
}
|
|
39
41
|
|
|
40
|
-
async function getLatestObsUuid(patientUuid: string): Promise<string> {
|
|
42
|
+
async function getLatestObsUuid(patientUuid: string): Promise<string | undefined> {
|
|
41
43
|
const request = fhirObservationRequests({
|
|
42
44
|
patient: patientUuid,
|
|
43
45
|
category: 'laboratory',
|
|
@@ -59,12 +61,13 @@ async function getLatestObsUuid(patientUuid: string): Promise<string> {
|
|
|
59
61
|
* @param { string } indicator UUID of the newest observation
|
|
60
62
|
*/
|
|
61
63
|
export function getUserDataFromCache(patientUuid: string): [PatientData | undefined, Promise<boolean>] {
|
|
62
|
-
const
|
|
64
|
+
const cacheEntry = patientResultsDataCache[patientUuid];
|
|
65
|
+
const [data, , indicator] = cacheEntry || [];
|
|
63
66
|
|
|
64
67
|
return [
|
|
65
68
|
data,
|
|
66
|
-
!!data
|
|
67
|
-
? getLatestObsUuid(patientUuid).then((obsUuid) => obsUuid !==
|
|
69
|
+
!!data && indicator
|
|
70
|
+
? getLatestObsUuid(patientUuid).then((obsUuid) => obsUuid !== indicator)
|
|
68
71
|
: Promise.resolve(true),
|
|
69
72
|
];
|
|
70
73
|
}
|
|
@@ -107,31 +110,47 @@ export const loadObsEntries = async (patientUuid: string): Promise<Array<ObsReco
|
|
|
107
110
|
|
|
108
111
|
let responses = await Promise.all(retrieveFromIterator(requests, CHUNK_PREFETCH_COUNT));
|
|
109
112
|
|
|
110
|
-
const total = responses[0]
|
|
113
|
+
const total = responses[0]?.total ?? 0;
|
|
111
114
|
|
|
112
115
|
if (total > CHUNK_PREFETCH_COUNT * PAGE_SIZE) {
|
|
113
116
|
const missingRequestsCount = Math.ceil(total / PAGE_SIZE) - CHUNK_PREFETCH_COUNT;
|
|
114
117
|
responses = [...responses, ...(await Promise.all(retrieveFromIterator(requests, missingRequestsCount)))];
|
|
115
118
|
}
|
|
116
119
|
|
|
117
|
-
return responses.slice(0, Math.ceil(total / PAGE_SIZE)).flatMap((res) => res
|
|
120
|
+
return responses.slice(0, Math.ceil(total / PAGE_SIZE)).flatMap((res) => res?.entry?.map((e) => e.resource) ?? []);
|
|
118
121
|
};
|
|
119
122
|
|
|
120
|
-
export const getEntryConceptClassUuid = (entry) =>
|
|
123
|
+
export const getEntryConceptClassUuid = (entry: ObsRecord | FHIRObservationResource): string =>
|
|
124
|
+
entry?.code?.coding?.[0]?.code ?? '';
|
|
121
125
|
|
|
122
126
|
const conceptCache: Record<ConceptUuid, Promise<ConceptRecord>> = {};
|
|
123
127
|
/**
|
|
124
128
|
* fetch all concepts for all given observation entries
|
|
125
129
|
*/
|
|
126
130
|
export function loadPresentConcepts(entries: Array<ObsRecord>): Promise<Array<ConceptRecord>> {
|
|
127
|
-
|
|
128
|
-
|
|
131
|
+
const conceptUuids = [...new Set(entries.map(getEntryConceptClassUuid).filter(Boolean))];
|
|
132
|
+
|
|
133
|
+
return Promise.allSettled(
|
|
134
|
+
conceptUuids.map(
|
|
129
135
|
(conceptUuid) =>
|
|
130
136
|
conceptCache[conceptUuid] ||
|
|
131
|
-
(conceptCache[conceptUuid] = fetch(`${window.openmrsBase}${restBaseUrl}/concept/${conceptUuid}?v=full`)
|
|
132
|
-
(res) =>
|
|
133
|
-
|
|
137
|
+
(conceptCache[conceptUuid] = fetch(`${window.openmrsBase}${restBaseUrl}/concept/${conceptUuid}?v=full`)
|
|
138
|
+
.then((res) => {
|
|
139
|
+
if (!res.ok) {
|
|
140
|
+
throw new Error(`Failed to fetch concept ${conceptUuid}: ${res.statusText}`);
|
|
141
|
+
}
|
|
142
|
+
return res.json();
|
|
143
|
+
})
|
|
144
|
+
.catch((error) => {
|
|
145
|
+
// Remove failed promise from cache so it can be retried
|
|
146
|
+
delete conceptCache[conceptUuid];
|
|
147
|
+
throw error;
|
|
148
|
+
})),
|
|
134
149
|
),
|
|
150
|
+
).then((results) =>
|
|
151
|
+
results
|
|
152
|
+
.filter((result): result is PromiseFulfilledResult<ConceptRecord> => result.status === 'fulfilled')
|
|
153
|
+
.map((result) => result.value),
|
|
135
154
|
);
|
|
136
155
|
}
|
|
137
156
|
|
|
@@ -151,6 +170,118 @@ export function exist(...args: any[]): boolean {
|
|
|
151
170
|
return true;
|
|
152
171
|
}
|
|
153
172
|
|
|
173
|
+
/**
|
|
174
|
+
* Extracts reference ranges from FHIR Observation referenceRange field.
|
|
175
|
+
* Handles different range types: normal, treatment, and absolute.
|
|
176
|
+
*/
|
|
177
|
+
export function extractObservationReferenceRanges(
|
|
178
|
+
resource: FHIRObservationResource | ObsRecord,
|
|
179
|
+
): ReferenceRanges | undefined {
|
|
180
|
+
if (!resource.referenceRange || resource.referenceRange.length === 0) {
|
|
181
|
+
return undefined;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const ranges: ReferenceRanges = {
|
|
185
|
+
units: resource.valueQuantity?.unit,
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
resource.referenceRange.forEach((range) => {
|
|
189
|
+
const rangeType = range.type?.coding?.[0]?.code;
|
|
190
|
+
const rangeSystem = range.type?.coding?.[0]?.system;
|
|
191
|
+
|
|
192
|
+
if (rangeSystem === 'http://terminology.hl7.org/CodeSystem/referencerange-meaning') {
|
|
193
|
+
if (rangeType === 'normal') {
|
|
194
|
+
ranges.hiNormal = range.high?.value;
|
|
195
|
+
ranges.lowNormal = range.low?.value;
|
|
196
|
+
} else if (rangeType === 'treatment') {
|
|
197
|
+
ranges.hiCritical = range.high?.value;
|
|
198
|
+
ranges.lowCritical = range.low?.value;
|
|
199
|
+
}
|
|
200
|
+
} else if (rangeSystem === 'http://fhir.openmrs.org/ext/obs/reference-range' && rangeType === 'absolute') {
|
|
201
|
+
ranges.hiAbsolute = range.high?.value;
|
|
202
|
+
ranges.lowAbsolute = range.low?.value;
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// Only return if we found at least one range value
|
|
207
|
+
if (
|
|
208
|
+
ranges.hiNormal !== undefined ||
|
|
209
|
+
ranges.lowNormal !== undefined ||
|
|
210
|
+
ranges.hiCritical !== undefined ||
|
|
211
|
+
ranges.lowCritical !== undefined ||
|
|
212
|
+
ranges.hiAbsolute !== undefined ||
|
|
213
|
+
ranges.lowAbsolute !== undefined
|
|
214
|
+
) {
|
|
215
|
+
return ranges;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Extracts and maps FHIR Observation interpretation to OBSERVATION_INTERPRETATION.
|
|
223
|
+
* Supports both interpretation codes (e.g., "LL", "N", "H") and display values (e.g., "Critically Low", "Normal").
|
|
224
|
+
*/
|
|
225
|
+
export function extractObservationInterpretation(
|
|
226
|
+
resource: FHIRObservationResource | ObsRecord,
|
|
227
|
+
): OBSERVATION_INTERPRETATION | undefined {
|
|
228
|
+
if (!resource.interpretation || resource.interpretation.length === 0) {
|
|
229
|
+
return undefined;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const interpretation = resource.interpretation[0];
|
|
233
|
+
const code = interpretation.coding?.[0]?.code;
|
|
234
|
+
const display = interpretation.coding?.[0]?.display || interpretation.text;
|
|
235
|
+
|
|
236
|
+
// Map FHIR interpretation codes (HL7 v3 ObservationInterpretation codes)
|
|
237
|
+
if (code) {
|
|
238
|
+
switch (code.toUpperCase()) {
|
|
239
|
+
case 'LL':
|
|
240
|
+
return 'CRITICALLY_LOW';
|
|
241
|
+
case 'HH':
|
|
242
|
+
return 'CRITICALLY_HIGH';
|
|
243
|
+
case 'L':
|
|
244
|
+
return 'LOW';
|
|
245
|
+
case 'H':
|
|
246
|
+
return 'HIGH';
|
|
247
|
+
case 'N':
|
|
248
|
+
return 'NORMAL';
|
|
249
|
+
case 'LU':
|
|
250
|
+
return 'OFF_SCALE_LOW';
|
|
251
|
+
case 'HU':
|
|
252
|
+
return 'OFF_SCALE_HIGH';
|
|
253
|
+
default:
|
|
254
|
+
// Fall through to display mapping
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Map FHIR interpretation display values
|
|
260
|
+
if (display) {
|
|
261
|
+
const normalized = display.trim().toLowerCase();
|
|
262
|
+
switch (normalized) {
|
|
263
|
+
case 'critically low':
|
|
264
|
+
return 'CRITICALLY_LOW';
|
|
265
|
+
case 'critically high':
|
|
266
|
+
return 'CRITICALLY_HIGH';
|
|
267
|
+
case 'low':
|
|
268
|
+
return 'LOW';
|
|
269
|
+
case 'high':
|
|
270
|
+
return 'HIGH';
|
|
271
|
+
case 'normal':
|
|
272
|
+
return 'NORMAL';
|
|
273
|
+
case 'off scale low':
|
|
274
|
+
return 'OFF_SCALE_LOW';
|
|
275
|
+
case 'off scale high':
|
|
276
|
+
return 'OFF_SCALE_HIGH';
|
|
277
|
+
default:
|
|
278
|
+
return undefined;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return undefined;
|
|
283
|
+
}
|
|
284
|
+
|
|
154
285
|
export const assessValue =
|
|
155
286
|
(meta: ObsMetaInfo) =>
|
|
156
287
|
(value: string): OBSERVATION_INTERPRETATION => {
|
|
@@ -7,13 +7,21 @@ import {
|
|
|
7
7
|
type ObsMetaInfo,
|
|
8
8
|
} from '@openmrs/esm-patient-common-lib';
|
|
9
9
|
import {
|
|
10
|
+
addUserDataToCache,
|
|
11
|
+
assessValue,
|
|
12
|
+
extractMetaInformation,
|
|
13
|
+
extractObservationReferenceRanges,
|
|
14
|
+
extractObservationInterpretation,
|
|
10
15
|
getEntryConceptClassUuid,
|
|
11
16
|
getUserDataFromCache,
|
|
12
17
|
loadObsEntries,
|
|
13
18
|
loadPresentConcepts,
|
|
14
|
-
extractMetaInformation,
|
|
15
|
-
addUserDataToCache,
|
|
16
19
|
} from './helpers';
|
|
20
|
+
import {
|
|
21
|
+
selectReferenceRange,
|
|
22
|
+
formatReferenceRange,
|
|
23
|
+
type ReferenceRanges,
|
|
24
|
+
} from '../grouped-timeline/reference-range-helpers';
|
|
17
25
|
|
|
18
26
|
function parseSingleObsData(
|
|
19
27
|
testConceptNameMap: Record<ConceptUuid, string>,
|
|
@@ -23,6 +31,11 @@ function parseSingleObsData(
|
|
|
23
31
|
return (entry: ObsRecord) => {
|
|
24
32
|
entry.conceptClass = getEntryConceptClassUuid(entry);
|
|
25
33
|
|
|
34
|
+
// Extract observation-level reference ranges from FHIR Observation referenceRange field
|
|
35
|
+
const observationRanges = extractObservationReferenceRanges(entry);
|
|
36
|
+
// Extract observation-level interpretation from FHIR Observation interpretation field
|
|
37
|
+
const observationInterpretation = extractObservationInterpretation(entry);
|
|
38
|
+
|
|
26
39
|
if (entry.hasMember) {
|
|
27
40
|
// is a panel
|
|
28
41
|
entry.members = new Array(entry.hasMember.length);
|
|
@@ -31,17 +44,59 @@ function parseSingleObsData(
|
|
|
31
44
|
});
|
|
32
45
|
} else {
|
|
33
46
|
// is a single test
|
|
34
|
-
entry.meta = metaInfomation[entry.conceptClass];
|
|
35
|
-
}
|
|
36
47
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
48
|
+
// Extract value FIRST before computing interpretation
|
|
49
|
+
if (entry.valueQuantity) {
|
|
50
|
+
entry.value = String(entry.valueQuantity.value);
|
|
51
|
+
delete entry.valueQuantity;
|
|
52
|
+
} else if (entry.valueCodeableConcept) {
|
|
53
|
+
entry.value = entry.valueCodeableConcept.coding?.[0]?.display;
|
|
54
|
+
delete entry.valueCodeableConcept;
|
|
55
|
+
} else if (entry.valueString) {
|
|
56
|
+
entry.value = entry.valueString;
|
|
57
|
+
delete entry.valueString;
|
|
58
|
+
}
|
|
41
59
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
60
|
+
const conceptMeta = metaInfomation[entry.conceptClass];
|
|
61
|
+
|
|
62
|
+
// Node-level (concept-level) reference ranges
|
|
63
|
+
const nodeRanges: ReferenceRanges = {
|
|
64
|
+
hiAbsolute: conceptMeta.hiAbsolute,
|
|
65
|
+
hiCritical: conceptMeta.hiCritical,
|
|
66
|
+
hiNormal: conceptMeta.hiNormal,
|
|
67
|
+
lowAbsolute: conceptMeta.lowAbsolute,
|
|
68
|
+
lowCritical: conceptMeta.lowCritical,
|
|
69
|
+
lowNormal: conceptMeta.lowNormal,
|
|
70
|
+
units: conceptMeta.units,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// Merge observation-level and concept-level ranges (observation takes precedence)
|
|
74
|
+
const selectedRanges = selectReferenceRange(observationRanges, nodeRanges);
|
|
75
|
+
|
|
76
|
+
// Create merged meta with observation-level ranges taking precedence
|
|
77
|
+
const mergedMeta: ObsMetaInfo = {
|
|
78
|
+
...conceptMeta,
|
|
79
|
+
// Update meta with merged ranges
|
|
80
|
+
hiAbsolute: selectedRanges?.hiAbsolute ?? conceptMeta.hiAbsolute,
|
|
81
|
+
hiCritical: selectedRanges?.hiCritical ?? conceptMeta.hiCritical,
|
|
82
|
+
hiNormal: selectedRanges?.hiNormal ?? conceptMeta.hiNormal,
|
|
83
|
+
lowAbsolute: selectedRanges?.lowAbsolute ?? conceptMeta.lowAbsolute,
|
|
84
|
+
lowCritical: selectedRanges?.lowCritical ?? conceptMeta.lowCritical,
|
|
85
|
+
lowNormal: selectedRanges?.lowNormal ?? conceptMeta.lowNormal,
|
|
86
|
+
units: selectedRanges?.units ?? conceptMeta.units,
|
|
87
|
+
// Update range string with merged ranges
|
|
88
|
+
range: selectedRanges ? formatReferenceRange(selectedRanges, selectedRanges.units) : conceptMeta.range,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// Always update assessValue to use merged ranges (computed after mergedMeta to avoid unsafe cast)
|
|
92
|
+
// This ensures assessValue is computed even when only concept-level ranges exist
|
|
93
|
+
mergedMeta.assessValue = assessValue(mergedMeta);
|
|
94
|
+
|
|
95
|
+
entry.meta = mergedMeta;
|
|
96
|
+
|
|
97
|
+
// Use observation-level interpretation if available, otherwise compute using merged ranges
|
|
98
|
+
entry.interpretation =
|
|
99
|
+
observationInterpretation ?? (mergedMeta.assessValue ? mergedMeta.assessValue(entry.value) : 'NORMAL');
|
|
45
100
|
}
|
|
46
101
|
|
|
47
102
|
entry.name = testConceptNameMap[entry.conceptClass];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { useEffect, useState, useRef } from 'react';
|
|
2
2
|
import { type PatientData } from '@openmrs/esm-patient-common-lib';
|
|
3
3
|
import loadPatientData from './loadPatientData';
|
|
4
4
|
|
|
@@ -9,23 +9,34 @@ type LoadingState = {
|
|
|
9
9
|
};
|
|
10
10
|
|
|
11
11
|
const usePatientResultsData = (patientUuid: string): LoadingState => {
|
|
12
|
-
const [state, setState] =
|
|
12
|
+
const [state, setState] = useState<LoadingState>({
|
|
13
13
|
sortedObs: {},
|
|
14
14
|
loaded: false,
|
|
15
15
|
error: undefined,
|
|
16
16
|
});
|
|
17
|
+
const isMountedRef = useRef(true);
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
isMountedRef.current = true;
|
|
20
21
|
if (patientUuid) {
|
|
21
22
|
const [data, reloadedDataPromise] = loadPatientData(patientUuid);
|
|
22
|
-
if (!!data)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
if (!!data && isMountedRef.current) {
|
|
24
|
+
setState({ sortedObs: data, loaded: true, error: undefined });
|
|
25
|
+
}
|
|
26
|
+
reloadedDataPromise
|
|
27
|
+
.then((reloadedData) => {
|
|
28
|
+
if (reloadedData !== data && isMountedRef.current) {
|
|
29
|
+
setState({ sortedObs: reloadedData, loaded: true, error: undefined });
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
.catch((error) => {
|
|
33
|
+
if (isMountedRef.current) {
|
|
34
|
+
setState({ sortedObs: {}, loaded: true, error });
|
|
35
|
+
}
|
|
36
|
+
});
|
|
26
37
|
}
|
|
27
38
|
return () => {
|
|
28
|
-
|
|
39
|
+
isMountedRef.current = false;
|
|
29
40
|
};
|
|
30
41
|
}, [patientUuid]);
|
|
31
42
|
|
|
@@ -77,7 +77,7 @@ const CommonDataTable: React.FC<CommonDataTableProps> = ({ title, data, descript
|
|
|
77
77
|
{rows.map((row, i) => (
|
|
78
78
|
<TypedTableRow
|
|
79
79
|
key={row.id}
|
|
80
|
-
interpretation={data[i]?.
|
|
80
|
+
interpretation={data[i]?.interpretation as OBSERVATION_INTERPRETATION}
|
|
81
81
|
{...getRowProps({ row })}
|
|
82
82
|
>
|
|
83
83
|
{row.cells.map((cell) => {
|
|
@@ -37,7 +37,6 @@ function getFilteredOverviewData(sortedObs: PatientData, filter) {
|
|
|
37
37
|
|
|
38
38
|
function useFilteredOverviewData(patientUuid: string, filter: (filterProps: PanelFilterProps) => boolean = () => true) {
|
|
39
39
|
const { sortedObs, loaded, error } = usePatientResultsData(patientUuid);
|
|
40
|
-
|
|
41
40
|
const overviewData = useMemo(() => getFilteredOverviewData(sortedObs, filter), [filter, sortedObs]);
|
|
42
41
|
|
|
43
42
|
return { overviewData, loaded, error };
|
|
@@ -46,8 +45,8 @@ function useFilteredOverviewData(patientUuid: string, filter: (filterProps: Pane
|
|
|
46
45
|
const ExternalOverview: React.FC<ExternalOverviewProps> = ({ patientUuid, filter }) => {
|
|
47
46
|
const { t } = useTranslation();
|
|
48
47
|
const { overviewData, loaded } = useFilteredOverviewData(patientUuid, filter);
|
|
49
|
-
|
|
50
48
|
const cardTitle = t('recentResults', 'Recent Results');
|
|
49
|
+
|
|
51
50
|
const handleSeeAll = useCallback(() => {
|
|
52
51
|
navigate({ to: `\${openmrsSpaBase}/patient/${patientUuid}/chart/Results` });
|
|
53
52
|
}, [patientUuid]);
|
|
@@ -23,24 +23,36 @@ export type OverviewPanelEntry = [string, string, Array<OverviewPanelData>, Date
|
|
|
23
23
|
|
|
24
24
|
export function parseSingleEntry(entry: ObsRecord, type: string, panelName: string): Array<OverviewPanelData> {
|
|
25
25
|
if (type === 'Test') {
|
|
26
|
+
// Use observation-level interpretation set during data loading
|
|
27
|
+
const interpretation = entry.interpretation || 'NORMAL';
|
|
26
28
|
return [
|
|
27
29
|
{
|
|
28
30
|
id: entry.id,
|
|
29
31
|
name: panelName,
|
|
30
32
|
range: entry.meta?.range || '--',
|
|
31
|
-
interpretation
|
|
32
|
-
value:
|
|
33
|
+
interpretation,
|
|
34
|
+
value: {
|
|
35
|
+
value: entry.value,
|
|
36
|
+
interpretation,
|
|
37
|
+
},
|
|
33
38
|
},
|
|
34
39
|
];
|
|
35
40
|
} else {
|
|
36
|
-
return entry.members.map((
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
return entry.members.map((member) => {
|
|
42
|
+
// Use observation-level interpretation set during data loading
|
|
43
|
+
const interpretation = member.interpretation || 'NORMAL';
|
|
44
|
+
return {
|
|
45
|
+
id: member.id,
|
|
46
|
+
key: member.id,
|
|
47
|
+
name: member.name,
|
|
48
|
+
range: member.meta?.range || '--',
|
|
49
|
+
interpretation,
|
|
50
|
+
value: {
|
|
51
|
+
value: member.value,
|
|
52
|
+
interpretation,
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
});
|
|
44
56
|
}
|
|
45
57
|
}
|
|
46
58
|
|
|
@@ -87,7 +87,7 @@ function PrintModal({
|
|
|
87
87
|
|
|
88
88
|
const identifiers =
|
|
89
89
|
patient?.identifier?.filter(
|
|
90
|
-
(identifier) => !excludePatientIdentifierCodeTypes?.uuids.includes(identifier.type
|
|
90
|
+
(identifier) => !excludePatientIdentifierCodeTypes?.uuids.includes(identifier.type?.coding?.[0]?.code),
|
|
91
91
|
) ?? [];
|
|
92
92
|
|
|
93
93
|
return {
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import React, { useContext, useEffect, useRef, useState } from 'react';
|
|
2
2
|
import classNames from 'classnames';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import type { TFunction } from 'i18next';
|
|
5
|
+
import { ContentSwitcher, Switch, Button, DataTableSkeleton } from '@carbon/react';
|
|
5
6
|
import { EmptyState, ErrorState } from '@openmrs/esm-patient-common-lib';
|
|
6
7
|
import { RenewIcon, useConfig, useLayoutType } from '@openmrs/esm-framework';
|
|
7
8
|
import { type ConfigObject } from '../../config-schema';
|
|
8
9
|
import { type viewOpts } from '../../types';
|
|
10
|
+
import { type Roots } from '../filter/filter-context';
|
|
9
11
|
import { FilterContext, FilterProvider } from '../filter';
|
|
10
12
|
import { useGetManyObstreeData } from '../grouped-timeline';
|
|
11
13
|
import IndividualResultsTableTablet from '../individual-results-table-tablet/individual-results-table-tablet.component';
|
|
12
14
|
import TreeView from '../tree-view/tree-view.component';
|
|
13
15
|
import styles from './results-viewer.scss';
|
|
14
|
-
import { type Roots } from '../filter/filter-context';
|
|
15
16
|
|
|
16
17
|
type panelOpts = 'tree' | 'panel';
|
|
17
18
|
|
|
@@ -22,22 +23,26 @@ interface RefreshDataButtonProps {
|
|
|
22
23
|
|
|
23
24
|
interface ResultsViewerProps {
|
|
24
25
|
basePath: string;
|
|
25
|
-
patientUuid
|
|
26
|
+
patientUuid: string;
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
const RoutedResultsViewer: React.FC<ResultsViewerProps> = ({ basePath, patientUuid }) => {
|
|
29
30
|
const { t } = useTranslation();
|
|
30
31
|
const config = useConfig<ConfigObject>();
|
|
31
32
|
const conceptUuids = config.resultsViewerConcepts.map((concept) => concept.conceptUuid) ?? [];
|
|
32
|
-
const { roots, isLoading, error } = useGetManyObstreeData(conceptUuids);
|
|
33
|
+
const { roots, isLoading, error } = useGetManyObstreeData(patientUuid, conceptUuids);
|
|
33
34
|
|
|
34
35
|
if (error) {
|
|
35
36
|
return <ErrorState error={error} headerTitle={t('dataLoadError', 'Data Load Error')} />;
|
|
36
37
|
}
|
|
37
38
|
|
|
39
|
+
if (isLoading) {
|
|
40
|
+
return <DataTableSkeleton role="progressbar" />;
|
|
41
|
+
}
|
|
42
|
+
|
|
38
43
|
if (roots?.length) {
|
|
39
44
|
return (
|
|
40
|
-
<FilterProvider roots={
|
|
45
|
+
<FilterProvider roots={roots as Roots} isLoading={isLoading}>
|
|
41
46
|
<ResultsViewer patientUuid={patientUuid} basePath={basePath} />
|
|
42
47
|
</FilterProvider>
|
|
43
48
|
);
|
|
@@ -46,7 +51,7 @@ const RoutedResultsViewer: React.FC<ResultsViewerProps> = ({ basePath, patientUu
|
|
|
46
51
|
return (
|
|
47
52
|
<EmptyState
|
|
48
53
|
headerTitle={t('testResults_title', 'Test Results')}
|
|
49
|
-
displayText={t('testResultsData', '
|
|
54
|
+
displayText={t('testResultsData', 'test results data')}
|
|
50
55
|
/>
|
|
51
56
|
);
|
|
52
57
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React, { useContext, useState, useMemo } from 'react';
|
|
2
2
|
import classNames from 'classnames';
|
|
3
|
+
import { AccordionSkeleton, Button, DataTableSkeleton, Layer } from '@carbon/react';
|
|
3
4
|
import { useTranslation } from 'react-i18next';
|
|
4
|
-
import { AccordionSkeleton, DataTableSkeleton, Button, Layer } from '@carbon/react';
|
|
5
5
|
import { useLayoutType, TreeViewAltIcon, useConfig } from '@openmrs/esm-framework';
|
|
6
6
|
import { EmptyState, ErrorState } from '@openmrs/esm-patient-common-lib';
|
|
7
7
|
import { type ConfigObject } from '../../config-schema';
|
|
@@ -19,9 +19,10 @@ interface TreeViewProps {
|
|
|
19
19
|
error?: string;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
const GroupedPanelsTables: React.FC<{ className: string; loadingPanelData: boolean }> = ({
|
|
22
|
+
const GroupedPanelsTables: React.FC<{ patientUuid: string; className: string; loadingPanelData: boolean }> = ({
|
|
23
23
|
className,
|
|
24
24
|
loadingPanelData,
|
|
25
|
+
patientUuid,
|
|
25
26
|
}) => {
|
|
26
27
|
const { t } = useTranslation();
|
|
27
28
|
const { checkboxes, someChecked, tableData } = useContext(FilterContext);
|
|
@@ -70,6 +71,7 @@ const GroupedPanelsTables: React.FC<{ className: string; loadingPanelData: boole
|
|
|
70
71
|
})}
|
|
71
72
|
>
|
|
72
73
|
<IndividualResultsTable
|
|
74
|
+
patientUuid={patientUuid}
|
|
73
75
|
isLoading={loadingPanelData}
|
|
74
76
|
subRows={filteredSubRows}
|
|
75
77
|
index={index}
|
|
@@ -88,7 +90,7 @@ const TreeView: React.FC<TreeViewProps> = ({ patientUuid, expanded, view }) => {
|
|
|
88
90
|
const [showTreeOverlay, setShowTreeOverlay] = useState(false);
|
|
89
91
|
const config = useConfig<ConfigObject>();
|
|
90
92
|
const conceptUuids = config?.resultsViewerConcepts?.map((c) => c.conceptUuid) ?? [];
|
|
91
|
-
const { roots, error } = useGetManyObstreeData(conceptUuids);
|
|
93
|
+
const { roots, error } = useGetManyObstreeData(patientUuid, conceptUuids);
|
|
92
94
|
|
|
93
95
|
const { timelineData, tableData, totalResultsCount, filteredResultsCount, resetTree, isLoading } =
|
|
94
96
|
useContext(FilterContext);
|
|
@@ -97,11 +99,16 @@ const TreeView: React.FC<TreeViewProps> = ({ patientUuid, expanded, view }) => {
|
|
|
97
99
|
return <ErrorState error={error} headerTitle={t('dataLoadError', 'Data Load Error')} />;
|
|
98
100
|
}
|
|
99
101
|
|
|
102
|
+
// Don't show empty state while loading - wait for data to finish loading
|
|
103
|
+
if (isLoading) {
|
|
104
|
+
return <DataTableSkeleton role="progressbar" />;
|
|
105
|
+
}
|
|
106
|
+
|
|
100
107
|
if (!roots || roots.length === 0) {
|
|
101
108
|
return (
|
|
102
109
|
<EmptyState
|
|
103
110
|
headerTitle={t('testResults_title', 'Test Results')}
|
|
104
|
-
displayText={t('testResultsData', '
|
|
111
|
+
displayText={t('testResultsData', 'test results data')}
|
|
105
112
|
/>
|
|
106
113
|
);
|
|
107
114
|
}
|
|
@@ -113,7 +120,11 @@ const TreeView: React.FC<TreeViewProps> = ({ patientUuid, expanded, view }) => {
|
|
|
113
120
|
{!isLoading && view === 'over-time' ? (
|
|
114
121
|
<GroupedTimeline patientUuid={patientUuid} />
|
|
115
122
|
) : view === 'individual-test' ? (
|
|
116
|
-
<GroupedPanelsTables
|
|
123
|
+
<GroupedPanelsTables
|
|
124
|
+
patientUuid={patientUuid}
|
|
125
|
+
className={styles.groupPanelsTables}
|
|
126
|
+
loadingPanelData={isLoading}
|
|
127
|
+
/>
|
|
117
128
|
) : (
|
|
118
129
|
<DataTableSkeleton role="progressbar" />
|
|
119
130
|
)}
|
|
@@ -159,9 +170,21 @@ const TreeView: React.FC<TreeViewProps> = ({ patientUuid, expanded, view }) => {
|
|
|
159
170
|
{isLoading ? (
|
|
160
171
|
<DataTableSkeleton />
|
|
161
172
|
) : view === 'individual-test' ? (
|
|
162
|
-
|
|
163
|
-
<
|
|
164
|
-
|
|
173
|
+
tableData && tableData.length > 0 ? (
|
|
174
|
+
<div className={styles.panelViewTimeline}>
|
|
175
|
+
<GroupedPanelsTables
|
|
176
|
+
patientUuid={patientUuid}
|
|
177
|
+
className={styles.groupPanelsTables}
|
|
178
|
+
loadingPanelData={isLoading}
|
|
179
|
+
/>
|
|
180
|
+
</div>
|
|
181
|
+
) : (
|
|
182
|
+
<GroupedPanelsTables
|
|
183
|
+
patientUuid={patientUuid}
|
|
184
|
+
className={styles.groupPanelsTables}
|
|
185
|
+
loadingPanelData={isLoading}
|
|
186
|
+
/>
|
|
187
|
+
)
|
|
165
188
|
) : view === 'over-time' ? (
|
|
166
189
|
<GroupedTimeline patientUuid={patientUuid} />
|
|
167
190
|
) : null}
|