@openmrs/esm-patient-tests-app 11.3.1-patch.9064 → 11.3.1-patch.9310
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 -19
- package/dist/1479.js +1 -1
- package/dist/1479.js.map +1 -1
- package/dist/3509.js +1 -1
- package/dist/4055.js +1 -1
- package/dist/4300.js +1 -1
- package/dist/{1935.js → 5348.js} +1 -1
- package/dist/5348.js.map +1 -0
- package/dist/5670.js +1 -1
- package/dist/5670.js.map +1 -1
- package/dist/6231.js +1 -1
- package/dist/6231.js.map +1 -1
- 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/790.js +1 -1
- package/dist/790.js.map +1 -1
- package/dist/8307.js +2 -0
- package/dist/8307.js.map +1 -0
- package/dist/9540.js +2 -0
- package/dist/9540.js.map +1 -0
- package/dist/9838.js +1 -0
- package/dist/9838.js.map +1 -0
- 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 +172 -193
- 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 +8 -4
- package/src/routes.json +3 -4
- package/src/test-orders/add-test-order/add-test-order.component.tsx +125 -0
- package/src/test-orders/add-test-order/add-test-order.test.tsx +23 -43
- package/src/test-orders/add-test-order/add-test-order.workspace.tsx +21 -116
- package/src/test-orders/add-test-order/exported-add-test-order.workspace.tsx +30 -0
- package/src/test-orders/add-test-order/test-order-form.component.tsx +67 -25
- package/src/test-orders/add-test-order/test-order.ts +3 -3
- package/src/test-orders/add-test-order/test-type-search.component.tsx +40 -24
- package/src/test-orders/api.ts +6 -2
- package/src/test-orders/lab-order-basket-panel/lab-order-basket-item-tile.component.tsx +1 -1
- package/src/test-orders/lab-order-basket-panel/lab-order-basket-panel.extension.tsx +30 -48
- package/src/test-orders/lab-order-basket-panel/lab-order-basket-panel.test.tsx +15 -4
- package/src/test-results/filter/filter-types.ts +19 -0
- package/src/test-results/grouped-timeline/grouped-timeline.test.tsx +49 -0
- package/src/test-results/grouped-timeline/reference-range-helpers.test.ts +272 -0
- package/src/test-results/grouped-timeline/reference-range-helpers.ts +112 -0
- package/src/test-results/grouped-timeline/timeline-data-group.component.tsx +10 -3
- package/src/test-results/grouped-timeline/useObstreeData.ts +71 -9
- package/src/test-results/individual-results-table/individual-results-table.component.tsx +23 -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 +2 -2
- package/src/test-results/individual-results-table-tablet/lab-set-panel.component.tsx +5 -1
- package/src/test-results/loadPatientTestData/helpers.test.ts +834 -0
- package/src/test-results/loadPatientTestData/helpers.ts +114 -0
- package/src/test-results/loadPatientTestData/loadPatientData.ts +66 -11
- package/src/test-results/loadPatientTestData/usePatientResultsData.ts +3 -3
- package/src/test-results/overview/common-datatable.component.tsx +1 -1
- package/src/test-results/overview/useOverviewData.ts +22 -10
- package/src/test-results/results-viewer/results-viewer.extension.tsx +4 -3
- package/src/test-results/tree-view/tree-view.component.tsx +14 -4
- package/src/test-results/trendline/trendline-resource.tsx +48 -5
- package/src/types.ts +20 -10
- package/translations/en.json +2 -0
- package/translations/fr.json +2 -2
- package/dist/1935.js.map +0 -1
- package/dist/2537.js +0 -1
- package/dist/2537.js.map +0 -1
- package/dist/34.js +0 -1
- package/dist/34.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/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 → 9540.js.LICENSE.txt} +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { render, screen } from '@testing-library/react';
|
|
3
3
|
import userEvent from '@testing-library/user-event';
|
|
4
|
+
import { getByTextWithMarkup } from 'tools';
|
|
4
5
|
import { showModal } from '@openmrs/esm-framework';
|
|
5
6
|
import { mockGroupedResults } from '__mocks__';
|
|
6
7
|
import { type FilterContextProps } from '../filter/filter-types';
|
|
@@ -70,6 +71,54 @@ describe('GroupedTimeline', () => {
|
|
|
70
71
|
expect(screen.getByText('2.9')).toBeInTheDocument();
|
|
71
72
|
});
|
|
72
73
|
|
|
74
|
+
it('displays most recent observation range when available', () => {
|
|
75
|
+
const contextWithObservationRanges = {
|
|
76
|
+
...mockFilterContext,
|
|
77
|
+
timelineData: {
|
|
78
|
+
...mockFilterContext.timelineData,
|
|
79
|
+
data: {
|
|
80
|
+
...mockFilterContext.timelineData.data,
|
|
81
|
+
rowData: [
|
|
82
|
+
{
|
|
83
|
+
...mockFilterContext.timelineData.data.rowData[0],
|
|
84
|
+
range: '0 – 50', // Node-level range
|
|
85
|
+
units: 'umol/L',
|
|
86
|
+
entries: [
|
|
87
|
+
{
|
|
88
|
+
obsDatetime: '2024-05-31 01:39:03.0',
|
|
89
|
+
value: '261.9',
|
|
90
|
+
interpretation: 'NORMAL',
|
|
91
|
+
lowNormal: 35,
|
|
92
|
+
hiNormal: 50,
|
|
93
|
+
range: '35 – 50', // Observation-level range (most recent)
|
|
94
|
+
// Note: Units are only at the concept/node level, not observation-level
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
obsDatetime: '2023-11-09 23:15:03.0',
|
|
98
|
+
value: '21.5',
|
|
99
|
+
interpretation: 'NORMAL',
|
|
100
|
+
lowNormal: 20,
|
|
101
|
+
hiNormal: 45,
|
|
102
|
+
range: '20 – 45', // Older observation-level range
|
|
103
|
+
// Note: Units are only at the concept/node level, not observation-level
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
renderGroupedTimeline(contextWithObservationRanges as FilterContextProps);
|
|
113
|
+
|
|
114
|
+
// Should display most recent observation's range (35 – 50) not node-level (0 – 50)
|
|
115
|
+
// Range and units are displayed separately in the same element
|
|
116
|
+
const rangeElement = getByTextWithMarkup(/35 – 50/);
|
|
117
|
+
expect(rangeElement).toBeInTheDocument();
|
|
118
|
+
// Verify that the same element also contains the units
|
|
119
|
+
expect(rangeElement).toHaveTextContent('35 – 50 umol/L');
|
|
120
|
+
});
|
|
121
|
+
|
|
73
122
|
it('correctly filters rows based on checkbox selection when someChecked is true', () => {
|
|
74
123
|
renderGroupedTimeline({
|
|
75
124
|
...mockFilterContext,
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import {
|
|
2
|
+
selectReferenceRange,
|
|
3
|
+
formatReferenceRange,
|
|
4
|
+
getMostRecentObservationWithRange,
|
|
5
|
+
rangeAlreadyHasUnits,
|
|
6
|
+
type ReferenceRanges,
|
|
7
|
+
} from './reference-range-helpers';
|
|
8
|
+
|
|
9
|
+
describe('Reference Range Helpers', () => {
|
|
10
|
+
describe('selectReferenceRange', () => {
|
|
11
|
+
it('returns null when both ranges are null', () => {
|
|
12
|
+
expect(selectReferenceRange(undefined, undefined)).toBeNull();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('returns node-level range when observation range is not available', () => {
|
|
16
|
+
const nodeRanges: ReferenceRanges = {
|
|
17
|
+
lowNormal: 0,
|
|
18
|
+
hiNormal: 50,
|
|
19
|
+
units: 'mg/dL',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
expect(selectReferenceRange(undefined, nodeRanges)).toEqual(nodeRanges);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('returns observation-level range when node range is not available', () => {
|
|
26
|
+
const observationRanges: ReferenceRanges = {
|
|
27
|
+
lowNormal: 35,
|
|
28
|
+
hiNormal: 147,
|
|
29
|
+
units: 'U/L',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
expect(selectReferenceRange(observationRanges, undefined)).toEqual(observationRanges);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('merges ranges with observation taking precedence', () => {
|
|
36
|
+
const observationRanges: ReferenceRanges = {
|
|
37
|
+
lowNormal: 35,
|
|
38
|
+
hiNormal: 147,
|
|
39
|
+
lowCritical: 25,
|
|
40
|
+
units: 'U/L',
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const nodeRanges: ReferenceRanges = {
|
|
44
|
+
lowNormal: 0,
|
|
45
|
+
hiNormal: 270,
|
|
46
|
+
hiCritical: 541,
|
|
47
|
+
units: 'U/L',
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const result = selectReferenceRange(observationRanges, nodeRanges);
|
|
51
|
+
|
|
52
|
+
expect(result).toEqual({
|
|
53
|
+
lowNormal: 35, // From observation
|
|
54
|
+
hiNormal: 147, // From observation
|
|
55
|
+
lowCritical: 25, // From observation
|
|
56
|
+
hiCritical: 541, // From node (observation doesn't have it)
|
|
57
|
+
units: 'U/L',
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('handles partial observation ranges', () => {
|
|
62
|
+
const observationRanges: ReferenceRanges = {
|
|
63
|
+
hiNormal: 147,
|
|
64
|
+
// Missing lowNormal
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const nodeRanges: ReferenceRanges = {
|
|
68
|
+
lowNormal: 0,
|
|
69
|
+
hiNormal: 270,
|
|
70
|
+
units: 'U/L',
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const result = selectReferenceRange(observationRanges, nodeRanges);
|
|
74
|
+
|
|
75
|
+
expect(result).toEqual({
|
|
76
|
+
lowNormal: 0, // From node (observation doesn't have it)
|
|
77
|
+
hiNormal: 147, // From observation
|
|
78
|
+
units: 'U/L',
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('formatReferenceRange', () => {
|
|
84
|
+
it('returns "--" when ranges is null', () => {
|
|
85
|
+
expect(formatReferenceRange(null)).toBe('--');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('formats range with both lowNormal and hiNormal', () => {
|
|
89
|
+
const ranges: ReferenceRanges = {
|
|
90
|
+
lowNormal: 0,
|
|
91
|
+
hiNormal: 50,
|
|
92
|
+
units: 'mg/dL',
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
expect(formatReferenceRange(ranges)).toBe('0 – 50 mg/dL');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('formats range without units', () => {
|
|
99
|
+
const ranges: ReferenceRanges = {
|
|
100
|
+
lowNormal: 0,
|
|
101
|
+
hiNormal: 50,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
expect(formatReferenceRange(ranges)).toBe('0 – 50');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('returns "--" when lowNormal or hiNormal is missing', () => {
|
|
108
|
+
const ranges1: ReferenceRanges = {
|
|
109
|
+
hiNormal: 50,
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const ranges2: ReferenceRanges = {
|
|
113
|
+
lowNormal: 0,
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
expect(formatReferenceRange(ranges1)).toBe('--');
|
|
117
|
+
expect(formatReferenceRange(ranges2)).toBe('--');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('uses provided units parameter when ranges.units is not available', () => {
|
|
121
|
+
const ranges: ReferenceRanges = {
|
|
122
|
+
lowNormal: 0,
|
|
123
|
+
hiNormal: 50,
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
expect(formatReferenceRange(ranges, 'mg/dL')).toBe('0 – 50 mg/dL');
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe('getMostRecentObservationWithRange', () => {
|
|
131
|
+
it('returns null when observations array is empty', () => {
|
|
132
|
+
expect(getMostRecentObservationWithRange([])).toBeNull();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('returns null when no observations have range data', () => {
|
|
136
|
+
const observations = [
|
|
137
|
+
{ obsDatetime: '2024-01-01', value: '10' },
|
|
138
|
+
{ obsDatetime: '2024-01-02', value: '20' },
|
|
139
|
+
];
|
|
140
|
+
|
|
141
|
+
expect(getMostRecentObservationWithRange(observations)).toBeNull();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('returns the most recent observation with range data', () => {
|
|
145
|
+
const observations = [
|
|
146
|
+
{
|
|
147
|
+
obsDatetime: '2024-01-01',
|
|
148
|
+
value: '10',
|
|
149
|
+
lowNormal: 0,
|
|
150
|
+
hiNormal: 50,
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
obsDatetime: '2024-01-03',
|
|
154
|
+
value: '30',
|
|
155
|
+
lowNormal: 35,
|
|
156
|
+
hiNormal: 147,
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
obsDatetime: '2024-01-02',
|
|
160
|
+
value: '20',
|
|
161
|
+
// No range data
|
|
162
|
+
},
|
|
163
|
+
];
|
|
164
|
+
|
|
165
|
+
const result = getMostRecentObservationWithRange(observations);
|
|
166
|
+
|
|
167
|
+
expect(result).toEqual({
|
|
168
|
+
obsDatetime: '2024-01-03',
|
|
169
|
+
value: '30',
|
|
170
|
+
lowNormal: 35,
|
|
171
|
+
hiNormal: 147,
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('handles observations with only lowNormal', () => {
|
|
176
|
+
const observations = [
|
|
177
|
+
{
|
|
178
|
+
obsDatetime: '2024-01-01',
|
|
179
|
+
value: '10',
|
|
180
|
+
lowNormal: 0,
|
|
181
|
+
},
|
|
182
|
+
];
|
|
183
|
+
|
|
184
|
+
const result = getMostRecentObservationWithRange(observations);
|
|
185
|
+
|
|
186
|
+
expect(result).toEqual({
|
|
187
|
+
obsDatetime: '2024-01-01',
|
|
188
|
+
value: '10',
|
|
189
|
+
lowNormal: 0,
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('handles observations with only hiNormal', () => {
|
|
194
|
+
const observations = [
|
|
195
|
+
{
|
|
196
|
+
obsDatetime: '2024-01-01',
|
|
197
|
+
value: '10',
|
|
198
|
+
hiNormal: 50,
|
|
199
|
+
},
|
|
200
|
+
];
|
|
201
|
+
|
|
202
|
+
const result = getMostRecentObservationWithRange(observations);
|
|
203
|
+
|
|
204
|
+
expect(result).toEqual({
|
|
205
|
+
obsDatetime: '2024-01-01',
|
|
206
|
+
value: '10',
|
|
207
|
+
hiNormal: 50,
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('filters out undefined entries', () => {
|
|
212
|
+
const observations = [
|
|
213
|
+
undefined,
|
|
214
|
+
{
|
|
215
|
+
obsDatetime: '2024-01-01',
|
|
216
|
+
value: '10',
|
|
217
|
+
lowNormal: 0,
|
|
218
|
+
hiNormal: 50,
|
|
219
|
+
},
|
|
220
|
+
undefined,
|
|
221
|
+
];
|
|
222
|
+
|
|
223
|
+
const result = getMostRecentObservationWithRange(observations);
|
|
224
|
+
|
|
225
|
+
expect(result).toEqual({
|
|
226
|
+
obsDatetime: '2024-01-01',
|
|
227
|
+
value: '10',
|
|
228
|
+
lowNormal: 0,
|
|
229
|
+
hiNormal: 50,
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
describe('rangeAlreadyHasUnits', () => {
|
|
235
|
+
it('returns false when range is undefined', () => {
|
|
236
|
+
expect(rangeAlreadyHasUnits(undefined, 'U/L')).toBe(false);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('returns false when units is undefined', () => {
|
|
240
|
+
expect(rangeAlreadyHasUnits('0 – 50', undefined)).toBe(false);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('returns false when both are undefined', () => {
|
|
244
|
+
expect(rangeAlreadyHasUnits(undefined, undefined)).toBe(false);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('returns true when range ends with units', () => {
|
|
248
|
+
expect(rangeAlreadyHasUnits('0 – 50 U/L', 'U/L')).toBe(true);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('returns true when range ends with units with space', () => {
|
|
252
|
+
expect(rangeAlreadyHasUnits('0 – 50 U/L', 'U/L')).toBe(true);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('returns false when range does not end with units', () => {
|
|
256
|
+
expect(rangeAlreadyHasUnits('0 – 50', 'U/L')).toBe(false);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('returns false when units appear in the middle of range', () => {
|
|
260
|
+
expect(rangeAlreadyHasUnits('5 mg/dL value', 'mg/dL')).toBe(false);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('handles trimmed strings correctly', () => {
|
|
264
|
+
expect(rangeAlreadyHasUnits(' 0 – 50 U/L ', ' U/L ')).toBe(true);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('returns false for empty strings', () => {
|
|
268
|
+
expect(rangeAlreadyHasUnits('', 'U/L')).toBe(false);
|
|
269
|
+
expect(rangeAlreadyHasUnits('0 – 50', '')).toBe(false);
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { exist } from '../loadPatientTestData/helpers';
|
|
2
|
+
import type { OBSERVATION_INTERPRETATION } from '@openmrs/esm-patient-common-lib';
|
|
3
|
+
|
|
4
|
+
export interface ReferenceRanges {
|
|
5
|
+
hiAbsolute?: number;
|
|
6
|
+
hiCritical?: number;
|
|
7
|
+
hiNormal?: number;
|
|
8
|
+
lowAbsolute?: number;
|
|
9
|
+
lowCritical?: number;
|
|
10
|
+
lowNormal?: number;
|
|
11
|
+
units?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Merges observation-level and node-level reference ranges.
|
|
16
|
+
* Observation-level ranges take precedence when available.
|
|
17
|
+
*/
|
|
18
|
+
export function selectReferenceRange(
|
|
19
|
+
observationRanges?: ReferenceRanges,
|
|
20
|
+
nodeRanges?: ReferenceRanges,
|
|
21
|
+
): ReferenceRanges | null {
|
|
22
|
+
if (!observationRanges && !nodeRanges) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!observationRanges) {
|
|
27
|
+
return nodeRanges || null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (!nodeRanges) {
|
|
31
|
+
return observationRanges;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Merge: observation takes precedence for available fields.
|
|
35
|
+
// Note: Units are only at the concept/node level, so units will always come from nodeRanges.
|
|
36
|
+
return {
|
|
37
|
+
hiAbsolute: observationRanges.hiAbsolute ?? nodeRanges.hiAbsolute,
|
|
38
|
+
hiCritical: observationRanges.hiCritical ?? nodeRanges.hiCritical,
|
|
39
|
+
hiNormal: observationRanges.hiNormal ?? nodeRanges.hiNormal,
|
|
40
|
+
lowAbsolute: observationRanges.lowAbsolute ?? nodeRanges.lowAbsolute,
|
|
41
|
+
lowCritical: observationRanges.lowCritical ?? nodeRanges.lowCritical,
|
|
42
|
+
lowNormal: observationRanges.lowNormal ?? nodeRanges.lowNormal,
|
|
43
|
+
units: observationRanges.units ?? nodeRanges.units,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Formats reference range string using lowNormal and hiNormal.
|
|
49
|
+
* Note: Display format using lowAbsolute/hiAbsolute with >/< is handled in a separate ticket.
|
|
50
|
+
*/
|
|
51
|
+
export function formatReferenceRange(ranges: ReferenceRanges | null, units?: string): string {
|
|
52
|
+
if (!ranges) {
|
|
53
|
+
return '--';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const { lowNormal, hiNormal } = ranges;
|
|
57
|
+
const displayUnits = ranges.units || units || '';
|
|
58
|
+
|
|
59
|
+
if (exist(lowNormal, hiNormal)) {
|
|
60
|
+
return `${lowNormal} – ${hiNormal}${displayUnits ? ` ${displayUnits}` : ''}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return '--';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Checks if a formatted range string already includes the units.
|
|
68
|
+
* This prevents duplicate units when appending units to a range that already has them.
|
|
69
|
+
* @param range The formatted range string (e.g., "0 – 50 U/L")
|
|
70
|
+
* @param units The units string (e.g., "U/L")
|
|
71
|
+
* @returns true if the range already ends with the units, false otherwise
|
|
72
|
+
*/
|
|
73
|
+
export function rangeAlreadyHasUnits(range: string | undefined, units: string | undefined): boolean {
|
|
74
|
+
if (!range || !units) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Check if range ends with units (with optional space before)
|
|
79
|
+
// This is more precise than includes() to avoid false positives
|
|
80
|
+
const trimmedRange = range.trim();
|
|
81
|
+
const trimmedUnits = units.trim();
|
|
82
|
+
return trimmedRange.endsWith(trimmedUnits) || trimmedRange.endsWith(` ${trimmedUnits}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Finds the most recent observation that has reference range data.
|
|
87
|
+
*/
|
|
88
|
+
export function getMostRecentObservationWithRange<
|
|
89
|
+
T extends { obsDatetime: string; lowNormal?: number; hiNormal?: number },
|
|
90
|
+
>(observations: Array<T | undefined>): T | null {
|
|
91
|
+
if (!observations || observations.length === 0) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Filter out undefined and find observations with range data
|
|
96
|
+
const validObservations = observations.filter(
|
|
97
|
+
(obs): obs is T => obs !== undefined && (obs.lowNormal !== undefined || obs.hiNormal !== undefined),
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
if (validObservations.length === 0) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Sort by obsDatetime descending (most recent first)
|
|
105
|
+
const sorted = [...validObservations].sort((a, b) => {
|
|
106
|
+
const dateA = new Date(a.obsDatetime).getTime();
|
|
107
|
+
const dateB = new Date(b.obsDatetime).getTime();
|
|
108
|
+
return dateB - dateA;
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
return sorted[0];
|
|
112
|
+
}
|
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
TimelineDataGroupProps,
|
|
11
11
|
} from './grouped-timeline-types';
|
|
12
12
|
import FilterContext from '../filter/filter-context';
|
|
13
|
+
import { getMostRecentObservationWithRange } from './reference-range-helpers';
|
|
13
14
|
import styles from './grouped-timeline.scss';
|
|
14
15
|
|
|
15
16
|
export const ShadowBox: React.FC = () => <div className={styles['shadow-box']} />;
|
|
@@ -196,14 +197,20 @@ const DataRows: React.FC<DataRowsProps> = ({ patientUuid, timeColumns, rowData,
|
|
|
196
197
|
<Grid dataColumns={timeColumns.length} padding style={{ gridColumn: 'span 2' }}>
|
|
197
198
|
{rowData.map((row, index) => {
|
|
198
199
|
const obs = row.entries;
|
|
199
|
-
const {
|
|
200
|
+
const { obs: values } = row;
|
|
200
201
|
const isString = isNaN(parseFloat(values?.[0]?.value));
|
|
202
|
+
|
|
203
|
+
// Note: Units are only at the concept/node level, not observation-level
|
|
204
|
+
const mostRecentObsWithRange = getMostRecentObservationWithRange(row.entries);
|
|
205
|
+
const displayRange = mostRecentObsWithRange?.range ?? row.range ?? '';
|
|
206
|
+
const displayUnits = row.units ?? '';
|
|
207
|
+
|
|
201
208
|
return (
|
|
202
209
|
<React.Fragment key={index}>
|
|
203
210
|
<NewRowStartCell
|
|
204
211
|
{...{
|
|
205
|
-
units,
|
|
206
|
-
range,
|
|
212
|
+
units: displayUnits,
|
|
213
|
+
range: displayRange,
|
|
207
214
|
title: row.display,
|
|
208
215
|
shadow: showShadow,
|
|
209
216
|
conceptUuid: row.conceptUuid,
|
|
@@ -2,8 +2,9 @@ import { useMemo } from 'react';
|
|
|
2
2
|
import useSWR from 'swr';
|
|
3
3
|
import useSWRInfinite from 'swr/infinite';
|
|
4
4
|
import { openmrsFetch, restBaseUrl, type FetchResponse } from '@openmrs/esm-framework';
|
|
5
|
-
import {
|
|
5
|
+
import { type OBSERVATION_INTERPRETATION } from '@openmrs/esm-patient-common-lib';
|
|
6
6
|
import { assessValue, exist } from '../loadPatientTestData/helpers';
|
|
7
|
+
import { selectReferenceRange, formatReferenceRange, type ReferenceRanges } from './reference-range-helpers';
|
|
7
8
|
|
|
8
9
|
export const getName = (prefix: string | undefined, name: string) => {
|
|
9
10
|
return prefix ? `${prefix}-${name}` : name;
|
|
@@ -13,11 +14,28 @@ interface ObsTreeNode {
|
|
|
13
14
|
flatName?: string;
|
|
14
15
|
display: string;
|
|
15
16
|
hasData: boolean;
|
|
17
|
+
hiAbsolute?: number;
|
|
18
|
+
hiCritical?: number;
|
|
16
19
|
hiNormal?: number;
|
|
20
|
+
lowAbsolute?: number;
|
|
21
|
+
lowCritical?: number;
|
|
17
22
|
lowNormal?: number;
|
|
23
|
+
units?: string;
|
|
18
24
|
range?: string;
|
|
19
25
|
subSets: Array<ObsTreeNode>;
|
|
20
|
-
obs: Array<{
|
|
26
|
+
obs: Array<{
|
|
27
|
+
value: string;
|
|
28
|
+
interpretation?: OBSERVATION_INTERPRETATION;
|
|
29
|
+
obsDatetime?: string;
|
|
30
|
+
// Observation-level reference ranges (criteria-based)
|
|
31
|
+
// Note: Units are only at the concept/node level, not observation-level
|
|
32
|
+
hiAbsolute?: number;
|
|
33
|
+
hiCritical?: number;
|
|
34
|
+
hiNormal?: number;
|
|
35
|
+
lowAbsolute?: number;
|
|
36
|
+
lowCritical?: number;
|
|
37
|
+
lowNormal?: number;
|
|
38
|
+
}>;
|
|
21
39
|
}
|
|
22
40
|
|
|
23
41
|
const augmentObstreeData = (node: ObsTreeNode, prefix: string | undefined) => {
|
|
@@ -40,20 +58,65 @@ const augmentObstreeData = (node: ObsTreeNode, prefix: string | undefined) => {
|
|
|
40
58
|
outData.subSets = outData.subSets.map((subNode: ObsTreeNode) => augmentObstreeData(subNode, outData.flatName));
|
|
41
59
|
outData.hasData = outData.subSets.some((subNode: ObsTreeNode) => subNode.hasData);
|
|
42
60
|
}
|
|
61
|
+
// Format node-level range for display (using lowNormal/hiNormal)
|
|
43
62
|
if (exist(outData?.hiNormal, outData?.lowNormal)) {
|
|
44
|
-
outData.range =
|
|
63
|
+
outData.range = formatReferenceRange(
|
|
64
|
+
{
|
|
65
|
+
lowNormal: outData.lowNormal,
|
|
66
|
+
hiNormal: outData.hiNormal,
|
|
67
|
+
units: outData.units,
|
|
68
|
+
},
|
|
69
|
+
outData.units,
|
|
70
|
+
);
|
|
45
71
|
}
|
|
72
|
+
|
|
46
73
|
if (outData?.obs?.length) {
|
|
47
|
-
|
|
48
|
-
|
|
74
|
+
outData.obs = outData.obs.map((ob) => {
|
|
75
|
+
// Note: Units are only at the concept/node level, not observation-level
|
|
76
|
+
const observationRanges: ReferenceRanges | undefined =
|
|
77
|
+
ob.lowNormal !== undefined || ob.hiNormal !== undefined
|
|
78
|
+
? {
|
|
79
|
+
hiAbsolute: ob.hiAbsolute,
|
|
80
|
+
hiCritical: ob.hiCritical,
|
|
81
|
+
hiNormal: ob.hiNormal,
|
|
82
|
+
lowAbsolute: ob.lowAbsolute,
|
|
83
|
+
lowCritical: ob.lowCritical,
|
|
84
|
+
lowNormal: ob.lowNormal,
|
|
85
|
+
}
|
|
86
|
+
: undefined;
|
|
87
|
+
|
|
88
|
+
const nodeRanges: ReferenceRanges | undefined = {
|
|
89
|
+
hiAbsolute: outData.hiAbsolute,
|
|
90
|
+
hiCritical: outData.hiCritical,
|
|
91
|
+
hiNormal: outData.hiNormal,
|
|
92
|
+
lowAbsolute: outData.lowAbsolute,
|
|
93
|
+
lowCritical: outData.lowCritical,
|
|
94
|
+
lowNormal: outData.lowNormal,
|
|
95
|
+
units: outData.units,
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const selectedRanges = selectReferenceRange(observationRanges, nodeRanges);
|
|
99
|
+
const assess = selectedRanges ? assessValue(selectedRanges) : assessValue(nodeRanges);
|
|
100
|
+
const interpretation = ob.interpretation ?? assess(ob.value);
|
|
101
|
+
|
|
102
|
+
// Always use node-level units since observation-level ranges don't have units
|
|
103
|
+
const displayRange = observationRanges
|
|
104
|
+
? formatReferenceRange(observationRanges, outData.units)
|
|
105
|
+
: outData.range || '--';
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
...ob,
|
|
109
|
+
interpretation,
|
|
110
|
+
range: displayRange,
|
|
111
|
+
};
|
|
112
|
+
});
|
|
49
113
|
outData.hasData = true;
|
|
50
114
|
}
|
|
51
115
|
|
|
52
116
|
return { ...outData } as ObsTreeNode;
|
|
53
117
|
};
|
|
54
118
|
|
|
55
|
-
const useGetObstreeData = (conceptUuid: string) => {
|
|
56
|
-
const { patientUuid } = usePatientChartStore();
|
|
119
|
+
const useGetObstreeData = (patientUuid: string, conceptUuid: string) => {
|
|
57
120
|
const response = useSWR<FetchResponse<ObsTreeNode>, Error>(
|
|
58
121
|
`${restBaseUrl}/obstree?patient=${patientUuid}&concept=${conceptUuid}`,
|
|
59
122
|
openmrsFetch,
|
|
@@ -74,8 +137,7 @@ const useGetObstreeData = (conceptUuid: string) => {
|
|
|
74
137
|
return result;
|
|
75
138
|
};
|
|
76
139
|
|
|
77
|
-
const useGetManyObstreeData = (uuidArray: Array<string>) => {
|
|
78
|
-
const { patientUuid } = usePatientChartStore();
|
|
140
|
+
const useGetManyObstreeData = (patientUuid: string, uuidArray: Array<string>) => {
|
|
79
141
|
const getObstreeUrl = (index: number) => {
|
|
80
142
|
if (index < uuidArray.length && patientUuid) {
|
|
81
143
|
return `${restBaseUrl}/obstree?patient=${patientUuid}&concept=${uuidArray[index]}`;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React, { useCallback, useMemo } from 'react';
|
|
2
2
|
import classNames from 'classnames';
|
|
3
3
|
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import { rangeAlreadyHasUnits } from '../grouped-timeline/reference-range-helpers';
|
|
4
5
|
import {
|
|
5
6
|
DataTable,
|
|
6
7
|
DataTableSkeleton,
|
|
@@ -13,11 +14,12 @@ import {
|
|
|
13
14
|
TableRow,
|
|
14
15
|
} from '@carbon/react';
|
|
15
16
|
import { showModal, useLayoutType, formatDate, parseDate } from '@openmrs/esm-framework';
|
|
16
|
-
import {
|
|
17
|
+
import { type OBSERVATION_INTERPRETATION } from '@openmrs/esm-patient-common-lib';
|
|
17
18
|
import { type GroupedObservation } from '../../types';
|
|
18
19
|
import styles from './individual-results-table.scss';
|
|
19
20
|
|
|
20
21
|
interface IndividualResultsTableProps {
|
|
22
|
+
patientUuid;
|
|
21
23
|
isLoading: boolean;
|
|
22
24
|
subRows: GroupedObservation;
|
|
23
25
|
index: number;
|
|
@@ -50,10 +52,15 @@ const getClasses = (interpretation: OBSERVATION_INTERPRETATION) => {
|
|
|
50
52
|
}
|
|
51
53
|
};
|
|
52
54
|
|
|
53
|
-
const IndividualResultsTable: React.FC<IndividualResultsTableProps> = ({
|
|
55
|
+
const IndividualResultsTable: React.FC<IndividualResultsTableProps> = ({
|
|
56
|
+
patientUuid,
|
|
57
|
+
isLoading,
|
|
58
|
+
subRows,
|
|
59
|
+
index,
|
|
60
|
+
title,
|
|
61
|
+
}) => {
|
|
54
62
|
const { t } = useTranslation();
|
|
55
63
|
const layout = useLayoutType();
|
|
56
|
-
const patientUuid = getPatientUuidFromStore();
|
|
57
64
|
const isDesktop = layout === 'small-desktop' || layout === 'large-desktop';
|
|
58
65
|
|
|
59
66
|
const headerTitle = t(title);
|
|
@@ -85,9 +92,19 @@ const IndividualResultsTable: React.FC<IndividualResultsTableProps> = ({ isLoadi
|
|
|
85
92
|
() =>
|
|
86
93
|
subRows?.entries.length &&
|
|
87
94
|
subRows.entries.map((row, i) => {
|
|
88
|
-
|
|
95
|
+
// Use observation-level range/units if available, otherwise fallback to node-level
|
|
96
|
+
// MappedObservation has range and units fields, but they may come from node-level
|
|
97
|
+
const displayRange = row.range ?? '';
|
|
98
|
+
const displayUnits = row.units ?? '';
|
|
89
99
|
const isString = isNaN(parseFloat(row.value));
|
|
90
100
|
|
|
101
|
+
// Check if range already includes units to avoid duplication
|
|
102
|
+
// formatReferenceRange includes units, so if range has units, don't append again
|
|
103
|
+
const hasUnits = rangeAlreadyHasUnits(displayRange, displayUnits);
|
|
104
|
+
const referenceRangeDisplay = hasUnits
|
|
105
|
+
? displayRange
|
|
106
|
+
: `${displayRange || '--'} ${displayUnits || ''}`.trim() || '--';
|
|
107
|
+
|
|
91
108
|
return {
|
|
92
109
|
...row,
|
|
93
110
|
id: `${i}-${index}`,
|
|
@@ -106,10 +123,10 @@ const IndividualResultsTable: React.FC<IndividualResultsTableProps> = ({ isLoadi
|
|
|
106
123
|
</span>
|
|
107
124
|
),
|
|
108
125
|
value: {
|
|
109
|
-
value: `${row.value} ${
|
|
126
|
+
value: `${row.value} ${displayUnits}`,
|
|
110
127
|
interpretation: row?.interpretation,
|
|
111
128
|
},
|
|
112
|
-
referenceRange:
|
|
129
|
+
referenceRange: referenceRangeDisplay,
|
|
113
130
|
};
|
|
114
131
|
}),
|
|
115
132
|
[index, subRows, launchResultsDialog],
|