@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.
Files changed (180) hide show
  1. package/.turbo/turbo-build.log +22 -22
  2. package/dist/1119.js +1 -1
  3. package/dist/1197.js +1 -1
  4. package/dist/{6231.js → 1477.js} +1 -1
  5. package/dist/1477.js.map +1 -0
  6. package/dist/1638.js +1 -1
  7. package/dist/1638.js.map +1 -1
  8. package/dist/1935.js +1 -1
  9. package/dist/1935.js.map +1 -1
  10. package/dist/2146.js +1 -1
  11. package/dist/2690.js +1 -1
  12. package/dist/3099.js +1 -1
  13. package/dist/34.js +1 -1
  14. package/dist/34.js.map +1 -1
  15. package/dist/3509.js +1 -1
  16. package/dist/3509.js.map +1 -1
  17. package/dist/3584.js +1 -1
  18. package/dist/4055.js +1 -1
  19. package/dist/4132.js +1 -1
  20. package/dist/4300.js +1 -1
  21. package/dist/4335.js +1 -1
  22. package/dist/439.js +1 -0
  23. package/dist/4618.js +1 -1
  24. package/dist/4652.js +1 -1
  25. package/dist/4944.js +1 -1
  26. package/dist/5173.js +1 -1
  27. package/dist/5241.js +1 -1
  28. package/dist/5442.js +1 -1
  29. package/dist/5661.js +1 -1
  30. package/dist/5670.js +1 -1
  31. package/dist/5670.js.map +1 -1
  32. package/dist/6022.js +1 -1
  33. package/dist/6113.js +1 -0
  34. package/dist/6113.js.map +1 -0
  35. package/dist/6301.js +1 -1
  36. package/dist/6301.js.map +1 -1
  37. package/dist/6336.js +1 -0
  38. package/dist/6336.js.map +1 -0
  39. package/dist/6468.js +1 -1
  40. package/dist/6589.js +1 -0
  41. package/dist/6679.js +1 -1
  42. package/dist/6840.js +1 -1
  43. package/dist/6859.js +1 -1
  44. package/dist/7097.js +1 -1
  45. package/dist/7159.js +1 -1
  46. package/dist/7202.js +1 -0
  47. package/dist/7202.js.map +1 -0
  48. package/dist/723.js +1 -1
  49. package/dist/7617.js +1 -1
  50. package/dist/790.js +1 -1
  51. package/dist/790.js.map +1 -1
  52. package/dist/795.js +1 -1
  53. package/dist/8163.js +1 -1
  54. package/dist/8307.js +2 -0
  55. package/dist/8307.js.map +1 -0
  56. package/dist/8349.js +1 -1
  57. package/dist/8371.js +1 -0
  58. package/dist/8555.js +2 -0
  59. package/dist/8555.js.map +1 -0
  60. package/dist/8618.js +1 -1
  61. package/dist/890.js +1 -1
  62. package/dist/9214.js +1 -1
  63. package/dist/9538.js +1 -1
  64. package/dist/9569.js +1 -1
  65. package/dist/986.js +1 -1
  66. package/dist/9879.js +1 -1
  67. package/dist/9895.js +1 -1
  68. package/dist/9900.js +1 -1
  69. package/dist/9913.js +1 -1
  70. package/dist/main.js +1 -1
  71. package/dist/main.js.map +1 -1
  72. package/dist/openmrs-esm-patient-tests-app.js +1 -1
  73. package/dist/openmrs-esm-patient-tests-app.js.buildmanifest.json +319 -249
  74. package/dist/openmrs-esm-patient-tests-app.js.map +1 -1
  75. package/dist/routes.json +1 -1
  76. package/package.json +3 -3
  77. package/src/edit-test-results/modal/edit-lab-results.modal.tsx +6 -2
  78. package/src/index.ts +1 -1
  79. package/src/routes.json +2 -2
  80. package/src/test-orders/add-test-order/add-test-order.test.tsx +13 -10
  81. package/src/test-orders/add-test-order/add-test-order.workspace.tsx +43 -7
  82. package/src/test-orders/add-test-order/test-order-form.component.tsx +41 -7
  83. package/src/test-orders/add-test-order/test-type-search.component.tsx +56 -8
  84. package/src/test-orders/lab-order-basket-panel/lab-icon.component.tsx +27 -0
  85. package/src/test-orders/lab-order-basket-panel/lab-order-basket-panel.extension.tsx +62 -15
  86. package/src/test-orders/lab-order-basket-panel/lab-order-basket-panel.scss +26 -11
  87. package/src/test-orders/lab-order-basket-panel/lab-order-basket-panel.test.tsx +18 -5
  88. package/src/test-results/filter/filter-context.test.tsx +556 -0
  89. package/src/test-results/filter/filter-context.tsx +1 -1
  90. package/src/test-results/filter/filter-reducer.test.ts +540 -0
  91. package/src/test-results/filter/filter-reducer.ts +1 -1
  92. package/src/test-results/filter/filter-set.component.tsx +75 -48
  93. package/src/test-results/filter/filter-set.test.tsx +694 -0
  94. package/src/test-results/filter/filter-types.ts +24 -1
  95. package/src/test-results/grouped-timeline/grid.component.tsx +4 -2
  96. package/src/test-results/grouped-timeline/grouped-timeline.component.tsx +20 -22
  97. package/src/test-results/grouped-timeline/grouped-timeline.test.tsx +52 -2
  98. package/src/test-results/grouped-timeline/reference-range-helpers.test.ts +308 -0
  99. package/src/test-results/grouped-timeline/reference-range-helpers.ts +161 -0
  100. package/src/test-results/grouped-timeline/timeline-data-group.component.tsx +13 -6
  101. package/src/test-results/grouped-timeline/useObstreeData.test.ts +471 -0
  102. package/src/test-results/grouped-timeline/useObstreeData.ts +108 -13
  103. package/src/test-results/individual-results-table/individual-results-table.component.tsx +18 -6
  104. package/src/test-results/individual-results-table/individual-results-table.test.tsx +65 -3
  105. package/src/test-results/individual-results-table-tablet/helper.tsx +8 -2
  106. package/src/test-results/individual-results-table-tablet/individual-results-table-tablet.component.tsx +5 -5
  107. package/src/test-results/individual-results-table-tablet/lab-set-panel.component.tsx +2 -1
  108. package/src/test-results/individual-results-table-tablet/usePanelData.tsx +40 -26
  109. package/src/test-results/loadPatientTestData/helpers.test.ts +834 -0
  110. package/src/test-results/loadPatientTestData/helpers.ts +143 -12
  111. package/src/test-results/loadPatientTestData/loadPatientData.ts +66 -11
  112. package/src/test-results/loadPatientTestData/usePatientResultsData.ts +20 -9
  113. package/src/test-results/overview/common-datatable.component.tsx +1 -1
  114. package/src/test-results/overview/external-overview.extension.tsx +1 -2
  115. package/src/test-results/overview/useOverviewData.ts +22 -10
  116. package/src/test-results/print-modal/print-modal.extension.tsx +1 -1
  117. package/src/test-results/results-viewer/results-viewer.extension.tsx +12 -7
  118. package/src/test-results/tree-view/tree-view.component.tsx +31 -8
  119. package/src/test-results/tree-view/tree-view.test.tsx +119 -2
  120. package/src/test-results/trendline/trendline-resource.tsx +48 -5
  121. package/src/test-results/trendline/trendline.component.tsx +88 -52
  122. package/src/test-results/ui-elements/{resetFiltersEmptyState → reset-filters-empty-state}/filter-empty-data-illustration.tsx +2 -2
  123. package/src/test-results/ui-elements/{resetFiltersEmptyState → reset-filters-empty-state}/filter-empty-state.component.tsx +5 -6
  124. package/src/types.ts +20 -1
  125. package/translations/am.json +3 -4
  126. package/translations/ar.json +3 -4
  127. package/translations/ar_SY.json +3 -4
  128. package/translations/bn.json +3 -4
  129. package/translations/cs.json +119 -0
  130. package/translations/de.json +3 -4
  131. package/translations/en.json +3 -2
  132. package/translations/en_US.json +3 -4
  133. package/translations/es.json +3 -4
  134. package/translations/es_MX.json +3 -4
  135. package/translations/fr.json +5 -6
  136. package/translations/he.json +3 -4
  137. package/translations/hi.json +3 -4
  138. package/translations/hi_IN.json +3 -4
  139. package/translations/id.json +3 -4
  140. package/translations/it.json +3 -4
  141. package/translations/ka.json +3 -4
  142. package/translations/km.json +3 -4
  143. package/translations/ku.json +3 -4
  144. package/translations/ky.json +3 -4
  145. package/translations/lg.json +3 -4
  146. package/translations/ne.json +3 -4
  147. package/translations/pl.json +3 -4
  148. package/translations/pt.json +3 -4
  149. package/translations/pt_BR.json +3 -4
  150. package/translations/qu.json +3 -4
  151. package/translations/ro_RO.json +3 -4
  152. package/translations/ru_RU.json +3 -4
  153. package/translations/si.json +3 -4
  154. package/translations/sq.json +119 -0
  155. package/translations/sw.json +3 -4
  156. package/translations/sw_KE.json +3 -4
  157. package/translations/tr.json +3 -4
  158. package/translations/tr_TR.json +3 -4
  159. package/translations/uk.json +3 -4
  160. package/translations/uz.json +3 -4
  161. package/translations/uz@Latn.json +3 -4
  162. package/translations/uz_UZ.json +3 -4
  163. package/translations/vi.json +3 -4
  164. package/translations/zh.json +3 -4
  165. package/translations/zh_CN.json +3 -4
  166. package/translations/zh_TW.json +119 -0
  167. package/dist/1479.js +0 -1
  168. package/dist/1479.js.map +0 -1
  169. package/dist/2537.js +0 -1
  170. package/dist/2537.js.map +0 -1
  171. package/dist/4918.js +0 -1
  172. package/dist/4918.js.map +0 -1
  173. package/dist/5836.js +0 -2
  174. package/dist/5836.js.map +0 -1
  175. package/dist/6231.js.map +0 -1
  176. package/dist/7053.js +0 -2
  177. package/dist/7053.js.map +0 -1
  178. /package/dist/{7053.js.LICENSE.txt → 8307.js.LICENSE.txt} +0 -0
  179. /package/dist/{5836.js.LICENSE.txt → 8555.js.LICENSE.txt} +0 -0
  180. /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 [data] = patientResultsDataCache[patientUuid] || [];
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 !== patientResultsDataCache?.[patientUuid]?.[2])
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].total;
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.entry.map((e) => e.resource));
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) => entry.code.coding[0].code;
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
- return Promise.all(
128
- [...new Set(entries.map(getEntryConceptClassUuid))].map(
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`).then(
132
- (res) => res.json(),
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
- if (entry.valueQuantity) {
38
- entry.value = entry.valueQuantity.value;
39
- delete entry.valueQuantity;
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
- if (entry.valueCodeableConcept) {
43
- entry.value = entry?.valueCodeableConcept.coding[0].display;
44
- delete entry.valueCodeableConcept;
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 React 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
 
@@ -9,23 +9,34 @@ type LoadingState = {
9
9
  };
10
10
 
11
11
  const usePatientResultsData = (patientUuid: string): LoadingState => {
12
- const [state, setState] = React.useState<LoadingState>({
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
- React.useEffect(() => {
19
- let unmounted = false;
19
+ useEffect(() => {
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
 
@@ -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]?.value?.interpretation as OBSERVATION_INTERPRETATION}
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: entry.meta.assessValue ? entry.meta.assessValue(entry.value) : '--',
32
- value: entry.value,
33
+ interpretation,
34
+ value: {
35
+ value: entry.value,
36
+ interpretation,
37
+ },
33
38
  },
34
39
  ];
35
40
  } else {
36
- return entry.members.map((gm) => ({
37
- id: gm.id,
38
- key: gm.id,
39
- name: gm.name,
40
- range: gm.meta?.range || '--',
41
- interpretation: gm.meta.assessValue(gm.value),
42
- value: gm.value,
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.coding[0].code),
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 { type TFunction, useTranslation } from 'react-i18next';
4
- import { ContentSwitcher, Switch, Button } from '@carbon/react';
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?: string;
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={!isLoading ? (roots as Roots) : []} isLoading={isLoading}>
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', 'Test results data')}
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', 'Test results data')}
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 className={styles.groupPanelsTables} loadingPanelData={isLoading} />
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
- <div className={styles.panelViewTimeline}>
163
- <GroupedPanelsTables className={styles.groupPanelsTables} loadingPanelData={isLoading} />
164
- </div>
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}