@openmrs/esm-patient-tests-app 11.3.1-patch.9064 → 11.3.1-patch.9310

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