@openmrs/esm-patient-tests-app 11.3.1-pre.9435 → 11.3.1-pre.9437

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/.turbo/turbo-build.log +3 -3
  2. package/dist/1477.js +1 -1
  3. package/dist/1477.js.map +1 -1
  4. package/dist/1935.js +1 -1
  5. package/dist/1935.js.map +1 -1
  6. package/dist/3509.js +1 -1
  7. package/dist/3509.js.map +1 -1
  8. package/dist/4300.js +1 -1
  9. package/dist/6301.js +1 -1
  10. package/dist/6301.js.map +1 -1
  11. package/dist/main.js +1 -1
  12. package/dist/main.js.map +1 -1
  13. package/dist/openmrs-esm-patient-tests-app.js +1 -1
  14. package/dist/openmrs-esm-patient-tests-app.js.buildmanifest.json +15 -15
  15. package/dist/routes.json +1 -1
  16. package/package.json +2 -2
  17. package/src/index.ts +1 -1
  18. package/src/routes.json +1 -1
  19. package/src/test-orders/add-test-order/add-test-order.test.tsx +1 -1
  20. package/src/test-orders/add-test-order/add-test-order.workspace.tsx +1 -1
  21. package/src/test-orders/add-test-order/test-order-form.component.tsx +1 -1
  22. package/src/test-orders/add-test-order/test-type-search.component.tsx +1 -1
  23. package/src/test-orders/lab-order-basket-panel/lab-order-basket-panel.test.tsx +2 -2
  24. package/src/test-results/filter/filter-context.test.tsx +556 -0
  25. package/src/test-results/filter/filter-context.tsx +1 -1
  26. package/src/test-results/filter/filter-reducer.test.ts +540 -0
  27. package/src/test-results/filter/filter-reducer.ts +1 -1
  28. package/src/test-results/filter/filter-set.test.tsx +694 -0
  29. package/src/test-results/grouped-timeline/grouped-timeline.test.tsx +1 -1
  30. package/src/test-results/grouped-timeline/useObstreeData.test.ts +471 -0
  31. package/src/test-results/individual-results-table-tablet/usePanelData.tsx +40 -26
  32. package/src/test-results/loadPatientTestData/helpers.ts +29 -12
  33. package/src/test-results/loadPatientTestData/usePatientResultsData.ts +18 -7
  34. package/src/test-results/overview/external-overview.extension.tsx +1 -2
  35. package/src/test-results/print-modal/print-modal.extension.tsx +1 -1
  36. package/src/test-results/results-viewer/results-viewer.extension.tsx +7 -3
  37. package/src/test-results/tree-view/tree-view.component.tsx +6 -1
  38. package/src/test-results/tree-view/tree-view.test.tsx +117 -1
  39. package/src/test-results/trendline/trendline.component.tsx +88 -52
  40. package/src/test-results/ui-elements/reset-filters-empty-state/filter-empty-data-illustration.tsx +2 -2
  41. package/translations/en.json +1 -1
@@ -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
+ });
@@ -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
- observations
165
- ? observations
166
- .filter((obs) => !!obs.hasMember)
167
- .map((obs) => {
168
- const relatedObs = [];
169
- obs.hasMember.forEach((memb) => {
170
- const membUuid = memb.reference.split('/')[1];
171
- const memberObservationIndex = individualObservations.findIndex((obs) => obs.id === membUuid);
172
- if (memberObservationIndex > -1) {
173
- relatedObs.push(individualObservations[memberObservationIndex]);
174
- individualObservations.splice(memberObservationIndex, 1);
175
- }
176
- });
177
- return {
178
- ...obs,
179
- relatedObs,
180
- };
181
- })
182
- : [],
183
- [individualObservations, observations],
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 = [...individualObservations, ...setObservations].sort(
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
- }, [individualObservations, setObservations]);
212
+ }, [remainingIndividualObservations, setObservations]);
199
213
 
200
214
  const panelsData = useMemo(
201
215
  () => ({
@@ -39,7 +39,7 @@ export function addUserDataToCache(patientUuid: string, data: PatientData, indic
39
39
  }
40
40
  }
41
41
 
42
- async function getLatestObsUuid(patientUuid: string): Promise<string> {
42
+ async function getLatestObsUuid(patientUuid: string): Promise<string | undefined> {
43
43
  const request = fhirObservationRequests({
44
44
  patient: patientUuid,
45
45
  category: 'laboratory',
@@ -61,12 +61,13 @@ async function getLatestObsUuid(patientUuid: string): Promise<string> {
61
61
  * @param { string } indicator UUID of the newest observation
62
62
  */
63
63
  export function getUserDataFromCache(patientUuid: string): [PatientData | undefined, Promise<boolean>] {
64
- const [data] = patientResultsDataCache[patientUuid] || [];
64
+ const cacheEntry = patientResultsDataCache[patientUuid];
65
+ const [data, , indicator] = cacheEntry || [];
65
66
 
66
67
  return [
67
68
  data,
68
- !!data
69
- ? getLatestObsUuid(patientUuid).then((obsUuid) => obsUuid !== patientResultsDataCache?.[patientUuid]?.[2])
69
+ !!data && indicator
70
+ ? getLatestObsUuid(patientUuid).then((obsUuid) => obsUuid !== indicator)
70
71
  : Promise.resolve(true),
71
72
  ];
72
73
  }
@@ -109,31 +110,47 @@ export const loadObsEntries = async (patientUuid: string): Promise<Array<ObsReco
109
110
 
110
111
  let responses = await Promise.all(retrieveFromIterator(requests, CHUNK_PREFETCH_COUNT));
111
112
 
112
- const total = responses[0].total;
113
+ const total = responses[0]?.total ?? 0;
113
114
 
114
115
  if (total > CHUNK_PREFETCH_COUNT * PAGE_SIZE) {
115
116
  const missingRequestsCount = Math.ceil(total / PAGE_SIZE) - CHUNK_PREFETCH_COUNT;
116
117
  responses = [...responses, ...(await Promise.all(retrieveFromIterator(requests, missingRequestsCount)))];
117
118
  }
118
119
 
119
- return responses.slice(0, Math.ceil(total / PAGE_SIZE)).flatMap((res) => res.entry.map((e) => e.resource));
120
+ return responses.slice(0, Math.ceil(total / PAGE_SIZE)).flatMap((res) => res?.entry?.map((e) => e.resource) ?? []);
120
121
  };
121
122
 
122
- export const getEntryConceptClassUuid = (entry) => entry.code.coding[0].code;
123
+ export const getEntryConceptClassUuid = (entry: ObsRecord | FHIRObservationResource): string =>
124
+ entry?.code?.coding?.[0]?.code ?? '';
123
125
 
124
126
  const conceptCache: Record<ConceptUuid, Promise<ConceptRecord>> = {};
125
127
  /**
126
128
  * fetch all concepts for all given observation entries
127
129
  */
128
130
  export function loadPresentConcepts(entries: Array<ObsRecord>): Promise<Array<ConceptRecord>> {
129
- return Promise.all(
130
- [...new Set(entries.map(getEntryConceptClassUuid))].map(
131
+ const conceptUuids = [...new Set(entries.map(getEntryConceptClassUuid).filter(Boolean))];
132
+
133
+ return Promise.allSettled(
134
+ conceptUuids.map(
131
135
  (conceptUuid) =>
132
136
  conceptCache[conceptUuid] ||
133
- (conceptCache[conceptUuid] = fetch(`${window.openmrsBase}${restBaseUrl}/concept/${conceptUuid}?v=full`).then(
134
- (res) => res.json(),
135
- )),
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
+ })),
136
149
  ),
150
+ ).then((results) =>
151
+ results
152
+ .filter((result): result is PromiseFulfilledResult<ConceptRecord> => result.status === 'fulfilled')
153
+ .map((result) => result.value),
137
154
  );
138
155
  }
139
156
 
@@ -1,4 +1,4 @@
1
- import React, { useEffect, useState } from 'react';
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
 
@@ -14,18 +14,29 @@ const usePatientResultsData = (patientUuid: string): LoadingState => {
14
14
  loaded: false,
15
15
  error: undefined,
16
16
  });
17
+ const isMountedRef = useRef(true);
17
18
 
18
19
  useEffect(() => {
19
- let unmounted = false;
20
+ isMountedRef.current = true;
20
21
  if (patientUuid) {
21
22
  const [data, reloadedDataPromise] = loadPatientData(patientUuid);
22
- if (!!data) setState({ sortedObs: data, loaded: true, error: undefined });
23
- reloadedDataPromise.then((reloadedData) => {
24
- if (reloadedData !== data && !unmounted) setState({ sortedObs: reloadedData, loaded: true, error: undefined });
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
- unmounted = true;
39
+ isMountedRef.current = false;
29
40
  };
30
41
  }, [patientUuid]);
31
42