@openmrs/esm-patient-tests-app 11.3.1-patch.9310 → 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 +19 -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 -0
- package/dist/1935.js.map +1 -0
- package/dist/2146.js +1 -1
- package/dist/2690.js +1 -1
- package/dist/3099.js +1 -1
- package/dist/34.js +1 -0
- package/dist/34.js.map +1 -0
- 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/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/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/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 +302 -211
- package/dist/openmrs-esm-patient-tests-app.js.map +1 -1
- package/dist/routes.json +1 -1
- package/package.json +2 -2
- package/src/edit-test-results/modal/edit-lab-results.modal.tsx +2 -2
- package/src/index.ts +1 -1
- package/src/routes.json +4 -3
- package/src/test-orders/add-test-order/add-test-order.test.tsx +44 -21
- package/src/test-orders/add-test-order/add-test-order.workspace.tsx +152 -21
- package/src/test-orders/add-test-order/test-order-form.component.tsx +60 -68
- package/src/test-orders/add-test-order/test-order.ts +3 -3
- package/src/test-orders/add-test-order/test-type-search.component.tsx +60 -28
- package/src/test-orders/api.ts +2 -6
- 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-item-tile.component.tsx +1 -1
- package/src/test-orders/lab-order-basket-panel/lab-order-basket-panel.extension.tsx +95 -30
- 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 +12 -10
- 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 +5 -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 +3 -2
- package/src/test-results/grouped-timeline/reference-range-helpers.test.ts +37 -1
- package/src/test-results/grouped-timeline/reference-range-helpers.ts +50 -1
- package/src/test-results/grouped-timeline/timeline-data-group.component.tsx +4 -4
- package/src/test-results/grouped-timeline/useObstreeData.test.ts +471 -0
- package/src/test-results/grouped-timeline/useObstreeData.ts +37 -4
- package/src/test-results/individual-results-table/individual-results-table.component.tsx +2 -7
- package/src/test-results/individual-results-table-tablet/individual-results-table-tablet.component.tsx +3 -3
- package/src/test-results/individual-results-table-tablet/lab-set-panel.component.tsx +2 -5
- package/src/test-results/individual-results-table-tablet/usePanelData.tsx +40 -26
- package/src/test-results/loadPatientTestData/helpers.ts +29 -12
- package/src/test-results/loadPatientTestData/usePatientResultsData.ts +18 -7
- package/src/test-results/overview/external-overview.extension.tsx +1 -2
- package/src/test-results/print-modal/print-modal.extension.tsx +1 -1
- package/src/test-results/results-viewer/results-viewer.extension.tsx +8 -4
- package/src/test-results/tree-view/tree-view.component.tsx +18 -5
- package/src/test-results/tree-view/tree-view.test.tsx +119 -2
- 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 +9 -0
- 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 -4
- package/translations/en_US.json +3 -4
- package/translations/es.json +3 -4
- package/translations/es_MX.json +3 -4
- package/translations/fr.json +3 -4
- 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/5348.js +0 -1
- package/dist/5348.js.map +0 -1
- package/dist/6231.js.map +0 -1
- package/dist/9540.js +0 -2
- package/dist/9540.js.map +0 -1
- package/dist/9838.js +0 -1
- package/dist/9838.js.map +0 -1
- package/src/test-orders/add-test-order/add-test-order.component.tsx +0 -125
- package/src/test-orders/add-test-order/exported-add-test-order.workspace.tsx +0 -30
- /package/dist/{9540.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
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
import { renderHook, waitFor } from '@testing-library/react';
|
|
2
|
+
import { openmrsFetch } from '@openmrs/esm-framework';
|
|
3
|
+
import { useGetManyObstreeData, useGetObstreeData, type ObsTreeNode } from './useObstreeData';
|
|
4
|
+
|
|
5
|
+
const mockOpenmrsFetch = jest.mocked(openmrsFetch);
|
|
6
|
+
|
|
7
|
+
describe('useObstreeData', () => {
|
|
8
|
+
describe('augmentObstreeData via useGetObstreeData', () => {
|
|
9
|
+
it('should add flatName to nodes', async () => {
|
|
10
|
+
const mockResponse = {
|
|
11
|
+
data: {
|
|
12
|
+
display: 'Hemoglobin',
|
|
13
|
+
conceptUuid: '21AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
14
|
+
hasData: false,
|
|
15
|
+
subSets: [],
|
|
16
|
+
obs: [
|
|
17
|
+
{
|
|
18
|
+
value: '12.5',
|
|
19
|
+
obsDatetime: '2024-01-01',
|
|
20
|
+
interpretation: 'NORMAL',
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
mockOpenmrsFetch.mockResolvedValue(mockResponse as any);
|
|
27
|
+
|
|
28
|
+
const { result } = renderHook(() => useGetObstreeData('patient-uuid', 'hemoglobin-uuid'));
|
|
29
|
+
|
|
30
|
+
await waitFor(() => expect(result.current.loading).toBe(false));
|
|
31
|
+
|
|
32
|
+
const data = result.current.data as ObsTreeNode;
|
|
33
|
+
expect(data.flatName).toBe('Hemoglobin');
|
|
34
|
+
expect(data.hasData).toBe(true);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should build hierarchical flatName for nested nodes', async () => {
|
|
38
|
+
const mockResponse = {
|
|
39
|
+
data: {
|
|
40
|
+
display: 'Complete Blood Count',
|
|
41
|
+
conceptUuid: 'cbc-uuid',
|
|
42
|
+
hasData: false,
|
|
43
|
+
subSets: [
|
|
44
|
+
{
|
|
45
|
+
display: 'Hemoglobin',
|
|
46
|
+
conceptUuid: '21AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
47
|
+
hasData: false,
|
|
48
|
+
subSets: [],
|
|
49
|
+
obs: [
|
|
50
|
+
{
|
|
51
|
+
value: '12.5',
|
|
52
|
+
obsDatetime: '2024-01-01',
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
obs: [],
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
mockOpenmrsFetch.mockResolvedValue(mockResponse as any);
|
|
62
|
+
|
|
63
|
+
const { result } = renderHook(() => useGetObstreeData('patient-uuid', 'cbc-uuid'));
|
|
64
|
+
|
|
65
|
+
await waitFor(() => expect(result.current.loading).toBe(false));
|
|
66
|
+
|
|
67
|
+
const data = result.current.data as ObsTreeNode;
|
|
68
|
+
expect(data.flatName).toBe('Complete Blood Count');
|
|
69
|
+
expect(data.subSets[0].flatName).toBe('Complete Blood Count-Hemoglobin');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should handle Bloodwork prefix specially to avoid long names', async () => {
|
|
73
|
+
const mockResponse = {
|
|
74
|
+
data: {
|
|
75
|
+
display: 'Bloodwork',
|
|
76
|
+
conceptUuid: 'bloodwork-uuid',
|
|
77
|
+
hasData: false,
|
|
78
|
+
subSets: [
|
|
79
|
+
{
|
|
80
|
+
display: 'Hemoglobin',
|
|
81
|
+
conceptUuid: '21AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
82
|
+
hasData: false,
|
|
83
|
+
subSets: [],
|
|
84
|
+
obs: [{ value: '12.5', obsDatetime: '2024-01-01' }],
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
obs: [],
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
mockOpenmrsFetch.mockResolvedValue(mockResponse as any);
|
|
92
|
+
|
|
93
|
+
const { result } = renderHook(() => useGetObstreeData('patient-uuid', 'bloodwork-uuid'));
|
|
94
|
+
|
|
95
|
+
await waitFor(() => expect(result.current.loading).toBe(false));
|
|
96
|
+
|
|
97
|
+
const data = result.current.data as ObsTreeNode;
|
|
98
|
+
expect(data.flatName).toBe('Bloodwork');
|
|
99
|
+
// Bloodwork children should use simplified names
|
|
100
|
+
expect(data.subSets[0].flatName).toBe('Hemoglobin');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should set hasData to true when node has observations', async () => {
|
|
104
|
+
const mockResponse = {
|
|
105
|
+
data: {
|
|
106
|
+
display: 'Hemoglobin',
|
|
107
|
+
conceptUuid: '21AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
108
|
+
hasData: false,
|
|
109
|
+
subSets: [],
|
|
110
|
+
obs: [
|
|
111
|
+
{
|
|
112
|
+
value: '12.5',
|
|
113
|
+
obsDatetime: '2024-01-01',
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
mockOpenmrsFetch.mockResolvedValue(mockResponse as any);
|
|
120
|
+
|
|
121
|
+
const { result } = renderHook(() => useGetObstreeData('patient-uuid', 'hemoglobin-uuid'));
|
|
122
|
+
|
|
123
|
+
await waitFor(() => expect(result.current.loading).toBe(false));
|
|
124
|
+
|
|
125
|
+
const data = result.current.data as ObsTreeNode;
|
|
126
|
+
expect(data.hasData).toBe(true);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should propagate hasData to parent when child has data', async () => {
|
|
130
|
+
const mockResponse = {
|
|
131
|
+
data: {
|
|
132
|
+
display: 'Complete Blood Count',
|
|
133
|
+
conceptUuid: 'cbc-uuid',
|
|
134
|
+
hasData: false,
|
|
135
|
+
subSets: [
|
|
136
|
+
{
|
|
137
|
+
display: 'Hemoglobin',
|
|
138
|
+
conceptUuid: '21AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
139
|
+
hasData: false,
|
|
140
|
+
subSets: [],
|
|
141
|
+
obs: [{ value: '12.5', obsDatetime: '2024-01-01' }],
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
obs: [],
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
mockOpenmrsFetch.mockResolvedValue(mockResponse as any);
|
|
149
|
+
|
|
150
|
+
const { result } = renderHook(() => useGetObstreeData('patient-uuid', 'cbc-uuid'));
|
|
151
|
+
|
|
152
|
+
await waitFor(() => expect(result.current.loading).toBe(false));
|
|
153
|
+
|
|
154
|
+
const data = result.current.data as ObsTreeNode;
|
|
155
|
+
expect(data.hasData).toBe(true);
|
|
156
|
+
expect(data.subSets[0].hasData).toBe(true);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should add interpretation to observations without one', async () => {
|
|
160
|
+
const mockResponse = {
|
|
161
|
+
data: {
|
|
162
|
+
display: 'Hemoglobin',
|
|
163
|
+
conceptUuid: '21AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
164
|
+
lowNormal: 10.4,
|
|
165
|
+
hiNormal: 17.8,
|
|
166
|
+
units: 'g/dL',
|
|
167
|
+
hasData: false,
|
|
168
|
+
subSets: [],
|
|
169
|
+
obs: [
|
|
170
|
+
{
|
|
171
|
+
value: '5.0', // Below normal
|
|
172
|
+
obsDatetime: '2024-01-01',
|
|
173
|
+
},
|
|
174
|
+
],
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
mockOpenmrsFetch.mockResolvedValue(mockResponse as any);
|
|
179
|
+
|
|
180
|
+
const { result } = renderHook(() => useGetObstreeData('patient-uuid', 'hemoglobin-uuid'));
|
|
181
|
+
|
|
182
|
+
await waitFor(() => expect(result.current.loading).toBe(false));
|
|
183
|
+
|
|
184
|
+
// Interpretation is added by assessValue helper
|
|
185
|
+
const data = result.current.data as ObsTreeNode;
|
|
186
|
+
expect(data.obs[0].interpretation).toBeDefined();
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should preserve existing interpretation if present', async () => {
|
|
190
|
+
const mockResponse = {
|
|
191
|
+
data: {
|
|
192
|
+
display: 'Hemoglobin',
|
|
193
|
+
conceptUuid: '21AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
194
|
+
lowNormal: 10.4,
|
|
195
|
+
hiNormal: 17.8,
|
|
196
|
+
units: 'g/dL',
|
|
197
|
+
hasData: false,
|
|
198
|
+
subSets: [],
|
|
199
|
+
obs: [
|
|
200
|
+
{
|
|
201
|
+
value: '12.5',
|
|
202
|
+
obsDatetime: '2024-01-01',
|
|
203
|
+
interpretation: 'NORMAL',
|
|
204
|
+
},
|
|
205
|
+
],
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
mockOpenmrsFetch.mockResolvedValue(mockResponse as any);
|
|
210
|
+
|
|
211
|
+
const { result } = renderHook(() => useGetObstreeData('patient-uuid', 'hemoglobin-uuid'));
|
|
212
|
+
|
|
213
|
+
await waitFor(() => expect(result.current.loading).toBe(false));
|
|
214
|
+
|
|
215
|
+
const data = result.current.data as ObsTreeNode;
|
|
216
|
+
expect(data.obs[0].interpretation).toBe('NORMAL');
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('should use observation-level reference ranges when available', async () => {
|
|
220
|
+
const mockResponse = {
|
|
221
|
+
data: {
|
|
222
|
+
display: 'Hemoglobin',
|
|
223
|
+
conceptUuid: '21AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
224
|
+
lowNormal: 10.4,
|
|
225
|
+
hiNormal: 17.8,
|
|
226
|
+
units: 'g/dL',
|
|
227
|
+
hasData: false,
|
|
228
|
+
subSets: [],
|
|
229
|
+
obs: [
|
|
230
|
+
{
|
|
231
|
+
value: '12.5',
|
|
232
|
+
obsDatetime: '2024-01-01',
|
|
233
|
+
// Observation-specific reference ranges (criteria-based)
|
|
234
|
+
lowNormal: 12.0,
|
|
235
|
+
hiNormal: 16.0,
|
|
236
|
+
},
|
|
237
|
+
],
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
mockOpenmrsFetch.mockResolvedValue(mockResponse as any);
|
|
242
|
+
|
|
243
|
+
const { result } = renderHook(() => useGetObstreeData('patient-uuid', 'hemoglobin-uuid'));
|
|
244
|
+
|
|
245
|
+
await waitFor(() => expect(result.current.loading).toBe(false));
|
|
246
|
+
|
|
247
|
+
// Observation should have a range property (formatted by helper)
|
|
248
|
+
const data = result.current.data as ObsTreeNode;
|
|
249
|
+
expect(data.obs[0]).toHaveProperty('range');
|
|
250
|
+
expect((data.obs[0] as any).range).toBeDefined();
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
describe('filterTreesWithData via useGetManyObstreeData', () => {
|
|
255
|
+
it('should filter out leaf nodes without data', async () => {
|
|
256
|
+
mockOpenmrsFetch.mockResolvedValue({
|
|
257
|
+
data: {
|
|
258
|
+
display: 'Complete Blood Count',
|
|
259
|
+
conceptUuid: 'cbc-uuid',
|
|
260
|
+
hasData: false,
|
|
261
|
+
subSets: [
|
|
262
|
+
{
|
|
263
|
+
display: 'Hemoglobin',
|
|
264
|
+
conceptUuid: '21AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
265
|
+
hasData: false,
|
|
266
|
+
subSets: [],
|
|
267
|
+
obs: [{ value: '12.5', obsDatetime: '2024-01-01' }],
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
display: 'Platelets',
|
|
271
|
+
conceptUuid: 'platelets-uuid',
|
|
272
|
+
hasData: false,
|
|
273
|
+
subSets: [],
|
|
274
|
+
obs: [], // No data
|
|
275
|
+
},
|
|
276
|
+
],
|
|
277
|
+
obs: [],
|
|
278
|
+
},
|
|
279
|
+
} as any);
|
|
280
|
+
|
|
281
|
+
const { result } = renderHook(() => useGetManyObstreeData('patient-uuid', ['cbc-uuid']));
|
|
282
|
+
|
|
283
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
284
|
+
|
|
285
|
+
expect(result.current.roots).toHaveLength(1);
|
|
286
|
+
expect(result.current.roots[0].subSets).toHaveLength(1);
|
|
287
|
+
expect(result.current.roots[0].subSets[0].display).toBe('Hemoglobin');
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('should keep parent nodes even if they have no direct observations', async () => {
|
|
291
|
+
mockOpenmrsFetch.mockResolvedValue({
|
|
292
|
+
data: {
|
|
293
|
+
display: 'Hematology',
|
|
294
|
+
conceptUuid: 'hematology-uuid',
|
|
295
|
+
hasData: false,
|
|
296
|
+
subSets: [
|
|
297
|
+
{
|
|
298
|
+
display: 'Complete Blood Count',
|
|
299
|
+
conceptUuid: 'cbc-uuid',
|
|
300
|
+
hasData: false,
|
|
301
|
+
subSets: [
|
|
302
|
+
{
|
|
303
|
+
display: 'Hemoglobin',
|
|
304
|
+
conceptUuid: '21AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
305
|
+
hasData: false,
|
|
306
|
+
subSets: [],
|
|
307
|
+
obs: [{ value: '12.5', obsDatetime: '2024-01-01' }],
|
|
308
|
+
},
|
|
309
|
+
],
|
|
310
|
+
obs: [],
|
|
311
|
+
},
|
|
312
|
+
],
|
|
313
|
+
obs: [],
|
|
314
|
+
},
|
|
315
|
+
} as any);
|
|
316
|
+
|
|
317
|
+
const { result } = renderHook(() => useGetManyObstreeData('patient-uuid', ['hematology-uuid']));
|
|
318
|
+
|
|
319
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
320
|
+
|
|
321
|
+
expect(result.current.roots).toHaveLength(1);
|
|
322
|
+
expect(result.current.roots[0].display).toBe('Hematology');
|
|
323
|
+
expect(result.current.roots[0].subSets).toHaveLength(1);
|
|
324
|
+
expect(result.current.roots[0].subSets[0].display).toBe('Complete Blood Count');
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it('should keep parent nodes even when all leaf nodes have no data', async () => {
|
|
328
|
+
mockOpenmrsFetch.mockResolvedValue({
|
|
329
|
+
data: {
|
|
330
|
+
display: 'Complete Blood Count',
|
|
331
|
+
conceptUuid: 'cbc-uuid',
|
|
332
|
+
hasData: false,
|
|
333
|
+
subSets: [
|
|
334
|
+
{
|
|
335
|
+
display: 'Hemoglobin',
|
|
336
|
+
conceptUuid: '21AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
337
|
+
hasData: false,
|
|
338
|
+
subSets: [],
|
|
339
|
+
obs: [], // No data
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
display: 'Hematocrit',
|
|
343
|
+
conceptUuid: 'hematocrit-uuid',
|
|
344
|
+
hasData: false,
|
|
345
|
+
subSets: [],
|
|
346
|
+
obs: [], // No data
|
|
347
|
+
},
|
|
348
|
+
],
|
|
349
|
+
obs: [],
|
|
350
|
+
},
|
|
351
|
+
} as any);
|
|
352
|
+
|
|
353
|
+
const { result } = renderHook(() => useGetManyObstreeData('patient-uuid', ['cbc-uuid']));
|
|
354
|
+
|
|
355
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
356
|
+
|
|
357
|
+
// Parent node is kept with filtered subSets
|
|
358
|
+
expect(result.current.roots).toHaveLength(1);
|
|
359
|
+
expect(result.current.roots[0].display).toBe('Complete Blood Count');
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it('should handle nodes that are both parent and have observations', async () => {
|
|
363
|
+
mockOpenmrsFetch.mockResolvedValue({
|
|
364
|
+
data: {
|
|
365
|
+
display: 'Test Node',
|
|
366
|
+
conceptUuid: 'test-uuid',
|
|
367
|
+
hasData: false,
|
|
368
|
+
subSets: [
|
|
369
|
+
{
|
|
370
|
+
display: 'Child Test',
|
|
371
|
+
conceptUuid: 'child-uuid',
|
|
372
|
+
hasData: false,
|
|
373
|
+
subSets: [],
|
|
374
|
+
obs: [{ value: '100', obsDatetime: '2024-01-01' }],
|
|
375
|
+
},
|
|
376
|
+
],
|
|
377
|
+
obs: [{ value: '50', obsDatetime: '2024-01-01' }], // Parent also has obs
|
|
378
|
+
},
|
|
379
|
+
} as any);
|
|
380
|
+
|
|
381
|
+
const { result } = renderHook(() => useGetManyObstreeData('patient-uuid', ['test-uuid']));
|
|
382
|
+
|
|
383
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
384
|
+
|
|
385
|
+
expect(result.current.roots).toHaveLength(1);
|
|
386
|
+
expect(result.current.roots[0].hasData).toBe(true);
|
|
387
|
+
expect(result.current.roots[0].obs).toHaveLength(1);
|
|
388
|
+
expect(result.current.roots[0].subSets).toHaveLength(1);
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
describe('useGetManyObstreeData', () => {
|
|
393
|
+
it('should tag root nodes with requested conceptUuid', async () => {
|
|
394
|
+
mockOpenmrsFetch.mockResolvedValue({
|
|
395
|
+
data: {
|
|
396
|
+
display: 'Hemoglobin',
|
|
397
|
+
hasData: false,
|
|
398
|
+
subSets: [],
|
|
399
|
+
obs: [{ value: '12.5', obsDatetime: '2024-01-01' }],
|
|
400
|
+
},
|
|
401
|
+
} as any);
|
|
402
|
+
|
|
403
|
+
const { result } = renderHook(() => useGetManyObstreeData('patient-uuid', ['hemoglobin-uuid']));
|
|
404
|
+
|
|
405
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
406
|
+
|
|
407
|
+
expect(result.current.roots[0].conceptUuid).toBe('hemoglobin-uuid');
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
describe('Edge cases', () => {
|
|
412
|
+
it('should handle nodes without subSets or obs', async () => {
|
|
413
|
+
mockOpenmrsFetch.mockResolvedValue({
|
|
414
|
+
data: {
|
|
415
|
+
display: 'Empty Node',
|
|
416
|
+
conceptUuid: 'empty-uuid',
|
|
417
|
+
hasData: false,
|
|
418
|
+
subSets: [],
|
|
419
|
+
obs: [],
|
|
420
|
+
},
|
|
421
|
+
} as any);
|
|
422
|
+
|
|
423
|
+
const { result } = renderHook(() => useGetObstreeData('patient-uuid', 'empty-uuid'));
|
|
424
|
+
|
|
425
|
+
await waitFor(() => expect(result.current.loading).toBe(false));
|
|
426
|
+
|
|
427
|
+
const data = result.current.data as ObsTreeNode;
|
|
428
|
+
expect(data.flatName).toBe('Empty Node');
|
|
429
|
+
// hasData may be set to false or true depending on augmentation logic
|
|
430
|
+
expect(data).toHaveProperty('hasData');
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
it('should handle deeply nested structures', async () => {
|
|
434
|
+
mockOpenmrsFetch.mockResolvedValue({
|
|
435
|
+
data: {
|
|
436
|
+
display: 'Level1',
|
|
437
|
+
conceptUuid: 'level1-uuid',
|
|
438
|
+
hasData: false,
|
|
439
|
+
subSets: [
|
|
440
|
+
{
|
|
441
|
+
display: 'Level2',
|
|
442
|
+
conceptUuid: 'level2-uuid',
|
|
443
|
+
hasData: false,
|
|
444
|
+
subSets: [
|
|
445
|
+
{
|
|
446
|
+
display: 'Level3',
|
|
447
|
+
conceptUuid: 'level3-uuid',
|
|
448
|
+
hasData: false,
|
|
449
|
+
subSets: [],
|
|
450
|
+
obs: [{ value: '100', obsDatetime: '2024-01-01' }],
|
|
451
|
+
},
|
|
452
|
+
],
|
|
453
|
+
obs: [],
|
|
454
|
+
},
|
|
455
|
+
],
|
|
456
|
+
obs: [],
|
|
457
|
+
},
|
|
458
|
+
} as any);
|
|
459
|
+
|
|
460
|
+
const { result } = renderHook(() => useGetObstreeData('patient-uuid', 'level1-uuid'));
|
|
461
|
+
|
|
462
|
+
await waitFor(() => expect(result.current.loading).toBe(false));
|
|
463
|
+
|
|
464
|
+
const data = result.current.data as ObsTreeNode;
|
|
465
|
+
expect(data.flatName).toBe('Level1');
|
|
466
|
+
expect(data.subSets[0].flatName).toBe('Level1-Level2');
|
|
467
|
+
expect(data.subSets[0].subSets[0].flatName).toBe('Level1-Level2-Level3');
|
|
468
|
+
expect(data.hasData).toBe(true);
|
|
469
|
+
});
|
|
470
|
+
});
|
|
471
|
+
});
|
|
@@ -10,9 +10,10 @@ export const getName = (prefix: string | undefined, name: string) => {
|
|
|
10
10
|
return prefix ? `${prefix}-${name}` : name;
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
-
interface ObsTreeNode {
|
|
13
|
+
export interface ObsTreeNode {
|
|
14
14
|
flatName?: string;
|
|
15
15
|
display: string;
|
|
16
|
+
conceptUuid?: string;
|
|
16
17
|
hasData: boolean;
|
|
17
18
|
hiAbsolute?: number;
|
|
18
19
|
hiCritical?: number;
|
|
@@ -116,6 +117,28 @@ const augmentObstreeData = (node: ObsTreeNode, prefix: string | undefined) => {
|
|
|
116
117
|
return { ...outData } as ObsTreeNode;
|
|
117
118
|
};
|
|
118
119
|
|
|
120
|
+
const filterTreesWithData = (node: ObsTreeNode): ObsTreeNode | null => {
|
|
121
|
+
// If this is a leaf node (has obs array), only keep it if it has data
|
|
122
|
+
if (node.obs !== undefined) {
|
|
123
|
+
return node.hasData ? node : null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// This is an intermediate/parent node - always keep it to preserve hierarchy
|
|
127
|
+
if (node.subSets && node.subSets.length > 0) {
|
|
128
|
+
// Recursively filter only the leaf children
|
|
129
|
+
const filteredSubSets = node.subSets
|
|
130
|
+
.map((subSet) => filterTreesWithData(subSet))
|
|
131
|
+
.filter((subSet): subSet is ObsTreeNode => subSet !== null);
|
|
132
|
+
|
|
133
|
+
// Always keep parent nodes to maintain test hierarchy structure
|
|
134
|
+
// The UI can choose to grey out parents with no data based on hasData flag
|
|
135
|
+
return { ...node, subSets: filteredSubSets };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Parent node with empty subSets - keep it to preserve hierarchy
|
|
139
|
+
return node;
|
|
140
|
+
};
|
|
141
|
+
|
|
119
142
|
const useGetObstreeData = (patientUuid: string, conceptUuid: string) => {
|
|
120
143
|
const response = useSWR<FetchResponse<ObsTreeNode>, Error>(
|
|
121
144
|
`${restBaseUrl}/obstree?patient=${patientUuid}&concept=${conceptUuid}`,
|
|
@@ -152,10 +175,14 @@ const useGetManyObstreeData = (patientUuid: string, uuidArray: Array<string>) =>
|
|
|
152
175
|
|
|
153
176
|
const result = useMemo(() => {
|
|
154
177
|
return (
|
|
155
|
-
data?.map((resp) => {
|
|
178
|
+
data?.map((resp, index) => {
|
|
156
179
|
if (resp?.data) {
|
|
157
180
|
const { data, ...rest } = resp;
|
|
158
181
|
const newData = augmentObstreeData(data, '');
|
|
182
|
+
// Tag the root node with the conceptUuid we requested
|
|
183
|
+
if (index < uuidArray.length && newData) {
|
|
184
|
+
newData.conceptUuid = uuidArray[index];
|
|
185
|
+
}
|
|
159
186
|
return { ...rest, loading: false, data: newData };
|
|
160
187
|
} else {
|
|
161
188
|
return {
|
|
@@ -172,8 +199,14 @@ const useGetManyObstreeData = (patientUuid: string, uuidArray: Array<string>) =>
|
|
|
172
199
|
},
|
|
173
200
|
]
|
|
174
201
|
);
|
|
175
|
-
}, [data]);
|
|
176
|
-
|
|
202
|
+
}, [data, uuidArray]);
|
|
203
|
+
|
|
204
|
+
const roots = result
|
|
205
|
+
.map((item) => item.data)
|
|
206
|
+
.filter((node): node is ObsTreeNode => 'display' in node)
|
|
207
|
+
.map((data: ObsTreeNode) => filterTreesWithData(data))
|
|
208
|
+
.filter(Boolean);
|
|
209
|
+
|
|
177
210
|
const isLoading = result.some((item) => item.loading);
|
|
178
211
|
|
|
179
212
|
return { roots, isLoading, error };
|
|
@@ -1,7 +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 {
|
|
4
|
+
import { formatRangeWithUnits } from '../grouped-timeline/reference-range-helpers';
|
|
5
5
|
import {
|
|
6
6
|
DataTable,
|
|
7
7
|
DataTableSkeleton,
|
|
@@ -98,12 +98,7 @@ const IndividualResultsTable: React.FC<IndividualResultsTableProps> = ({
|
|
|
98
98
|
const displayUnits = row.units ?? '';
|
|
99
99
|
const isString = isNaN(parseFloat(row.value));
|
|
100
100
|
|
|
101
|
-
|
|
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() || '--';
|
|
101
|
+
const referenceRangeDisplay = formatRangeWithUnits(displayRange, displayUnits);
|
|
107
102
|
|
|
108
103
|
return {
|
|
109
104
|
...row,
|
|
@@ -4,12 +4,12 @@ import { useTranslation } from 'react-i18next';
|
|
|
4
4
|
import { DataTableSkeleton, Button, Search, Form } from '@carbon/react';
|
|
5
5
|
import { CloseIcon, SearchIcon, useLayoutType } from '@openmrs/esm-framework';
|
|
6
6
|
import { EmptyState } from '@openmrs/esm-patient-common-lib';
|
|
7
|
-
import
|
|
8
|
-
import
|
|
7
|
+
import FilterContext from '../filter/filter-context';
|
|
8
|
+
import FilterEmptyState from '../ui-elements/reset-filters-empty-state/filter-empty-state.component';
|
|
9
9
|
import LabSetPanel from './lab-set-panel.component';
|
|
10
10
|
import Overlay from '../tablet-overlay/tablet-overlay.component';
|
|
11
|
-
import FilterContext from '../filter/filter-context';
|
|
12
11
|
import TimelineDataGroup from '../grouped-timeline/timeline-data-group.component';
|
|
12
|
+
import type { GroupedObservation } from '../../types';
|
|
13
13
|
import styles from './individual-results-table-tablet.scss';
|
|
14
14
|
|
|
15
15
|
interface IndividualResultsTableTabletProps {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React, { useMemo } from 'react';
|
|
2
2
|
import classNames from 'classnames';
|
|
3
3
|
import { useTranslation } from 'react-i18next';
|
|
4
|
-
import {
|
|
4
|
+
import { formatRangeWithUnits } from '../grouped-timeline/reference-range-helpers';
|
|
5
5
|
import {
|
|
6
6
|
DataTable,
|
|
7
7
|
TableContainer,
|
|
@@ -71,10 +71,7 @@ const LabSetPanel: React.FC<LabSetPanelProps> = ({ panel, activePanel, setActive
|
|
|
71
71
|
hasRange
|
|
72
72
|
? panel.entries.map((test) => {
|
|
73
73
|
const units = test.units ?? '';
|
|
74
|
-
|
|
75
|
-
// formatReferenceRange includes units, so if range has units, don't append again
|
|
76
|
-
const hasUnits = rangeAlreadyHasUnits(test.range, units);
|
|
77
|
-
const range = hasUnits ? test.range : test.range ? `${test.range} ${units}` : '--';
|
|
74
|
+
const range = formatRangeWithUnits(test.range, units);
|
|
78
75
|
return {
|
|
79
76
|
id: test.conceptUuid,
|
|
80
77
|
testName: test.display,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo } from 'react';
|
|
2
|
-
import { type FetchResponse, openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
|
|
3
2
|
import useSWRInfinite from 'swr/infinite';
|
|
3
|
+
import { type FetchResponse, openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
|
|
4
4
|
import { extractMetaInformation, getConceptUuid } from './helper';
|
|
5
5
|
import {
|
|
6
6
|
type Concept,
|
|
@@ -159,32 +159,46 @@ export default function usePanelData(patientUuid: string) {
|
|
|
159
159
|
[observations],
|
|
160
160
|
);
|
|
161
161
|
|
|
162
|
-
const setObservations: Array<ObsRecord> = useMemo(
|
|
163
|
-
()
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
162
|
+
const setObservations: Array<ObsRecord> = useMemo(() => {
|
|
163
|
+
if (!observations) {
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Create a map of individual observations for efficient lookup
|
|
168
|
+
const individualObsMap = new Map(individualObservations.map((obs) => [obs.id, obs]));
|
|
169
|
+
const usedIndividualObsIds = new Set<string>();
|
|
170
|
+
|
|
171
|
+
return observations
|
|
172
|
+
.filter((obs) => !!obs.hasMember)
|
|
173
|
+
.map((obs) => {
|
|
174
|
+
const relatedObs: Array<ObsRecord> = [];
|
|
175
|
+
obs.hasMember.forEach((memb) => {
|
|
176
|
+
const membUuid = memb.reference.split('/')[1];
|
|
177
|
+
const memberObs = individualObsMap.get(membUuid);
|
|
178
|
+
if (memberObs && !usedIndividualObsIds.has(membUuid)) {
|
|
179
|
+
relatedObs.push(memberObs);
|
|
180
|
+
usedIndividualObsIds.add(membUuid);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
return {
|
|
184
|
+
...obs,
|
|
185
|
+
relatedObs,
|
|
186
|
+
};
|
|
187
|
+
});
|
|
188
|
+
}, [individualObservations, observations]);
|
|
189
|
+
|
|
190
|
+
const remainingIndividualObservations = useMemo(() => {
|
|
191
|
+
const usedIds = new Set<string>();
|
|
192
|
+
setObservations.forEach((setObs) => {
|
|
193
|
+
setObs.relatedObs.forEach((relatedObs) => {
|
|
194
|
+
usedIds.add(relatedObs.id);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
return individualObservations.filter((obs) => !usedIds.has(obs.id));
|
|
198
|
+
}, [individualObservations, setObservations]);
|
|
185
199
|
|
|
186
200
|
const panels = useMemo(() => {
|
|
187
|
-
const allPanels = [...
|
|
201
|
+
const allPanels = [...remainingIndividualObservations, ...setObservations].sort(
|
|
188
202
|
(obs1, obs2) => Date.parse(obs2.effectiveDateTime) - Date.parse(obs1.effectiveDateTime),
|
|
189
203
|
);
|
|
190
204
|
const usedConcepts: Set<string> = new Set();
|
|
@@ -195,7 +209,7 @@ export default function usePanelData(patientUuid: string) {
|
|
|
195
209
|
latestPanels.push(panel);
|
|
196
210
|
});
|
|
197
211
|
return latestPanels;
|
|
198
|
-
}, [
|
|
212
|
+
}, [remainingIndividualObservations, setObservations]);
|
|
199
213
|
|
|
200
214
|
const panelsData = useMemo(
|
|
201
215
|
() => ({
|