@openmrs/esm-patient-tests-app 11.3.1-pre.9277 → 11.3.1-pre.9283

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.
@@ -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;
@@ -151,6 +153,118 @@ export function exist(...args: any[]): boolean {
151
153
  return true;
152
154
  }
153
155
 
156
+ /**
157
+ * Extracts reference ranges from FHIR Observation referenceRange field.
158
+ * Handles different range types: normal, treatment, and absolute.
159
+ */
160
+ export function extractObservationReferenceRanges(
161
+ resource: FHIRObservationResource | ObsRecord,
162
+ ): ReferenceRanges | undefined {
163
+ if (!resource.referenceRange || resource.referenceRange.length === 0) {
164
+ return undefined;
165
+ }
166
+
167
+ const ranges: ReferenceRanges = {
168
+ units: resource.valueQuantity?.unit,
169
+ };
170
+
171
+ resource.referenceRange.forEach((range) => {
172
+ const rangeType = range.type?.coding?.[0]?.code;
173
+ const rangeSystem = range.type?.coding?.[0]?.system;
174
+
175
+ if (rangeSystem === 'http://terminology.hl7.org/CodeSystem/referencerange-meaning') {
176
+ if (rangeType === 'normal') {
177
+ ranges.hiNormal = range.high?.value;
178
+ ranges.lowNormal = range.low?.value;
179
+ } else if (rangeType === 'treatment') {
180
+ ranges.hiCritical = range.high?.value;
181
+ ranges.lowCritical = range.low?.value;
182
+ }
183
+ } else if (rangeSystem === 'http://fhir.openmrs.org/ext/obs/reference-range' && rangeType === 'absolute') {
184
+ ranges.hiAbsolute = range.high?.value;
185
+ ranges.lowAbsolute = range.low?.value;
186
+ }
187
+ });
188
+
189
+ // Only return if we found at least one range value
190
+ if (
191
+ ranges.hiNormal !== undefined ||
192
+ ranges.lowNormal !== undefined ||
193
+ ranges.hiCritical !== undefined ||
194
+ ranges.lowCritical !== undefined ||
195
+ ranges.hiAbsolute !== undefined ||
196
+ ranges.lowAbsolute !== undefined
197
+ ) {
198
+ return ranges;
199
+ }
200
+
201
+ return undefined;
202
+ }
203
+
204
+ /**
205
+ * Extracts and maps FHIR Observation interpretation to OBSERVATION_INTERPRETATION.
206
+ * Supports both interpretation codes (e.g., "LL", "N", "H") and display values (e.g., "Critically Low", "Normal").
207
+ */
208
+ export function extractObservationInterpretation(
209
+ resource: FHIRObservationResource | ObsRecord,
210
+ ): OBSERVATION_INTERPRETATION | undefined {
211
+ if (!resource.interpretation || resource.interpretation.length === 0) {
212
+ return undefined;
213
+ }
214
+
215
+ const interpretation = resource.interpretation[0];
216
+ const code = interpretation.coding?.[0]?.code;
217
+ const display = interpretation.coding?.[0]?.display || interpretation.text;
218
+
219
+ // Map FHIR interpretation codes (HL7 v3 ObservationInterpretation codes)
220
+ if (code) {
221
+ switch (code.toUpperCase()) {
222
+ case 'LL':
223
+ return 'CRITICALLY_LOW';
224
+ case 'HH':
225
+ return 'CRITICALLY_HIGH';
226
+ case 'L':
227
+ return 'LOW';
228
+ case 'H':
229
+ return 'HIGH';
230
+ case 'N':
231
+ return 'NORMAL';
232
+ case 'LU':
233
+ return 'OFF_SCALE_LOW';
234
+ case 'HU':
235
+ return 'OFF_SCALE_HIGH';
236
+ default:
237
+ // Fall through to display mapping
238
+ break;
239
+ }
240
+ }
241
+
242
+ // Map FHIR interpretation display values
243
+ if (display) {
244
+ const normalized = display.trim().toLowerCase();
245
+ switch (normalized) {
246
+ case 'critically low':
247
+ return 'CRITICALLY_LOW';
248
+ case 'critically high':
249
+ return 'CRITICALLY_HIGH';
250
+ case 'low':
251
+ return 'LOW';
252
+ case 'high':
253
+ return 'HIGH';
254
+ case 'normal':
255
+ return 'NORMAL';
256
+ case 'off scale low':
257
+ return 'OFF_SCALE_LOW';
258
+ case 'off scale high':
259
+ return 'OFF_SCALE_HIGH';
260
+ default:
261
+ return undefined;
262
+ }
263
+ }
264
+
265
+ return undefined;
266
+ }
267
+
154
268
  export const assessValue =
155
269
  (meta: ObsMetaInfo) =>
156
270
  (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];
@@ -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) => {
@@ -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
 
package/src/types.ts CHANGED
@@ -67,6 +67,14 @@ export interface FHIRObservationResource {
67
67
  hasMember?: Array<{
68
68
  reference: string;
69
69
  }>;
70
+ interpretation?: Array<{
71
+ coding: Array<{
72
+ code: string;
73
+ display: string;
74
+ system?: string;
75
+ }>;
76
+ text?: string;
77
+ }>;
70
78
  }
71
79
 
72
80
  export interface Concept {
@@ -107,7 +115,7 @@ export interface ConceptMeta {
107
115
  range: string;
108
116
  }
109
117
 
110
- export interface ObsRecord extends FHIRObservationResource {
118
+ export interface ObsRecord extends Omit<FHIRObservationResource, 'interpretation'> {
111
119
  conceptUuid: string;
112
120
  relatedObs: Array<ObsRecord>;
113
121
  meta: ConceptMeta;