@openmrs/esm-patient-vitals-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 (55) hide show
  1. package/.turbo/turbo-build.log +18 -18
  2. package/dist/439.js +1 -0
  3. package/dist/5415.js +1 -1
  4. package/dist/5415.js.map +1 -1
  5. package/dist/5670.js +1 -1
  6. package/dist/5670.js.map +1 -1
  7. package/dist/6336.js +1 -0
  8. package/dist/6336.js.map +1 -0
  9. package/dist/6589.js +1 -0
  10. package/dist/7299.js +1 -1
  11. package/dist/7545.js +2 -0
  12. package/dist/7545.js.map +1 -0
  13. package/dist/8371.js +1 -0
  14. package/dist/8803.js +1 -1
  15. package/dist/8803.js.map +1 -1
  16. package/dist/8953.js +1 -1
  17. package/dist/9228.js +1 -0
  18. package/dist/9228.js.map +1 -0
  19. package/dist/main.js +1 -1
  20. package/dist/main.js.map +1 -1
  21. package/dist/openmrs-esm-patient-vitals-app.js +1 -1
  22. package/dist/openmrs-esm-patient-vitals-app.js.buildmanifest.json +171 -105
  23. package/dist/openmrs-esm-patient-vitals-app.js.map +1 -1
  24. package/dist/routes.json +1 -1
  25. package/package.json +3 -3
  26. package/src/biometrics/biometrics-base.component.tsx +4 -2
  27. package/src/biometrics/biometrics-main.component.tsx +11 -2
  28. package/src/biometrics/biometrics-overview.component.tsx +11 -2
  29. package/src/biometrics/biometrics-overview.test.tsx +3 -0
  30. package/src/biometrics/paginated-biometrics.component.tsx +3 -1
  31. package/src/common/data.resource.ts +20 -18
  32. package/src/common/helpers.ts +38 -9
  33. package/src/common/types.ts +13 -1
  34. package/src/components/action-menu/vitals-biometrics-action-menu.component.tsx +3 -3
  35. package/src/index.ts +1 -1
  36. package/src/routes.json +1 -1
  37. package/src/utils.ts +2 -1
  38. package/src/vitals/paginated-vitals.component.tsx +3 -1
  39. package/src/vitals/vitals-overview.component.tsx +2 -1
  40. package/src/vitals-and-biometrics-header/{vitals-header.component.tsx → vitals-header.extension.tsx} +31 -21
  41. package/src/vitals-and-biometrics-header/vitals-header.scss +11 -7
  42. package/src/vitals-and-biometrics-header/vitals-header.test.tsx +107 -11
  43. package/src/vitals-biometrics-form/vitals-biometrics-form.test.tsx +4 -2
  44. package/src/vitals-biometrics-form/vitals-biometrics-form.workspace.tsx +5 -6
  45. package/src/vitals-biometrics-form/vitals-biometrics-input.component.tsx +4 -1
  46. package/translations/cs.json +80 -0
  47. package/translations/sq.json +80 -0
  48. package/translations/zh_TW.json +80 -0
  49. package/dist/5639.js +0 -1
  50. package/dist/5639.js.map +0 -1
  51. package/dist/5810.js +0 -1
  52. package/dist/5810.js.map +0 -1
  53. package/dist/6712.js +0 -2
  54. package/dist/6712.js.map +0 -1
  55. /package/dist/{6712.js.LICENSE.txt → 7545.js.LICENSE.txt} +0 -0
@@ -5,15 +5,24 @@ import BiometricsBase from './biometrics-base.component';
5
5
  interface BiometricsProps {
6
6
  patientUuid: string;
7
7
  basePath: string;
8
+ patient: fhir.Patient;
8
9
  }
9
10
 
10
- const BiometricsOverview: React.FC<BiometricsProps> = ({ patientUuid, basePath }) => {
11
+ const BiometricsOverview: React.FC<BiometricsProps> = ({ patientUuid, patient, basePath }) => {
11
12
  const { t } = useTranslation();
12
13
  const pageSize = 5;
13
14
  const pageUrl = `\${openmrsSpaBase}/patient/${patientUuid}/chart/Vitals & Biometrics`;
14
15
  const urlLabel = t('seeAll', 'See all');
15
16
 
16
- return <BiometricsBase patientUuid={patientUuid} pageSize={pageSize} urlLabel={urlLabel} pageUrl={pageUrl} />;
17
+ return (
18
+ <BiometricsBase
19
+ patientUuid={patientUuid}
20
+ patient={patient}
21
+ pageSize={pageSize}
22
+ urlLabel={urlLabel}
23
+ pageUrl={pageUrl}
24
+ />
25
+ );
17
26
  };
18
27
 
19
28
  export default BiometricsOverview;
@@ -12,6 +12,9 @@ import BiometricsOverview from './biometrics-overview.component';
12
12
  const testProps = {
13
13
  basePath: patientChartBasePath,
14
14
  patientUuid: mockPatient.id,
15
+ patient: mockPatient,
16
+ visitContext: null,
17
+ mutateVisitContext: null,
15
18
  };
16
19
 
17
20
  const mockUseConfig = jest.mocked(useConfig<ConfigObject>);
@@ -23,6 +23,7 @@ interface PaginatedBiometricsProps {
23
23
  pageUrl: string;
24
24
  urlLabel: string;
25
25
  tableHeaders: Array<BiometricsTableHeader>;
26
+ patient: fhir.Patient;
26
27
  }
27
28
 
28
29
  const PaginatedBiometrics: React.FC<PaginatedBiometricsProps> = ({
@@ -31,6 +32,7 @@ const PaginatedBiometrics: React.FC<PaginatedBiometricsProps> = ({
31
32
  pageUrl,
32
33
  urlLabel,
33
34
  tableHeaders,
35
+ patient,
34
36
  }) => {
35
37
  const isTablet = useLayoutType() === 'tablet';
36
38
 
@@ -110,7 +112,7 @@ const PaginatedBiometrics: React.FC<PaginatedBiometricsProps> = ({
110
112
  <TableCell key={cell.id}>{cell.value?.content ?? cell.value}</TableCell>
111
113
  ))}
112
114
  <TableCell className="cds--table-column-menu" id="actions">
113
- <VitalsAndBiometricsActionMenu encounterUuid={row.id} />
115
+ <VitalsAndBiometricsActionMenu patient={patient} encounterUuid={row.id} />
114
116
  </TableCell>
115
117
  </TableRow>
116
118
  ))}
@@ -11,7 +11,12 @@ import {
11
11
  import useSWRImmutable from 'swr/immutable';
12
12
  import useSWRInfinite from 'swr/infinite';
13
13
  import { type ConfigObject } from '../config-schema';
14
- import { assessValue, calculateBodyMassIndex, getReferenceRangesForConcept, interpretBloodPressure } from './helpers';
14
+ import {
15
+ assessValue,
16
+ calculateBodyMassIndex,
17
+ interpretBloodPressure,
18
+ mapFhirInterpretationToObservationInterpretation,
19
+ } from './helpers';
15
20
  import type {
16
21
  FHIRObservationResource,
17
22
  FHIRSearchBundleResponse,
@@ -323,23 +328,13 @@ export function useVitalsAndBiometrics(patientUuid: string, mode: VitalsAndBiome
323
328
  vitalSigns.diastolic,
324
329
  concepts,
325
330
  conceptRanges,
331
+ vitalSigns.systolicRenderInterpretation,
332
+ vitalSigns.diastolicRenderInterpretation,
326
333
  );
327
- result.pulseRenderInterpretation = assessValue(
328
- vitalSigns.pulse,
329
- getReferenceRangesForConcept(concepts.pulseUuid, conceptRanges),
330
- );
331
- result.temperatureRenderInterpretation = assessValue(
332
- vitalSigns.temperature,
333
- getReferenceRangesForConcept(concepts.temperatureUuid, conceptRanges),
334
- );
335
- result.spo2RenderInterpretation = assessValue(
336
- vitalSigns.spo2,
337
- getReferenceRangesForConcept(concepts.oxygenSaturationUuid, conceptRanges),
338
- );
339
- result.respiratoryRateRenderInterpretation = assessValue(
340
- vitalSigns.respiratoryRate,
341
- getReferenceRangesForConcept(concepts.respiratoryRateUuid, conceptRanges),
342
- );
334
+ result.pulseRenderInterpretation = vitalSigns.pulseRenderInterpretation;
335
+ result.temperatureRenderInterpretation = vitalSigns.temperatureRenderInterpretation;
336
+ result.spo2RenderInterpretation = vitalSigns.spo2RenderInterpretation;
337
+ result.respiratoryRateRenderInterpretation = vitalSigns.respiratoryRateRenderInterpretation;
343
338
  }
344
339
 
345
340
  return result;
@@ -472,7 +467,14 @@ function mapVitalsAndBiometrics(resource: FHIRObservationResource): MappedVitals
472
467
  return {
473
468
  code: resource?.code?.coding?.[0]?.code,
474
469
  encounterId: extractEncounterUuid(resource.encounter),
475
- interpretation: assessValue(resource?.valueQuantity?.value, referenceRanges),
470
+ // Use Observation.interpretation from FHIR when available (preferred).
471
+ // Fallback to calculation for backward compatibility: existing observations may not have
472
+ // interpretation set if they were created before interpretation was added, or if reference
473
+ // ranges weren't available at creation time (OpenMRS core only sets interpretation when
474
+ // ObsReferenceRange is present).
475
+ interpretation: resource.interpretation?.[0]?.coding?.[0]?.display
476
+ ? mapFhirInterpretationToObservationInterpretation(resource.interpretation?.[0]?.coding?.[0]?.display)
477
+ : assessValue(resource?.valueQuantity?.value, referenceRanges),
476
478
  recordedDate: resource?.effectiveDateTime,
477
479
  value: resource?.valueQuantity?.value,
478
480
  };
@@ -1,6 +1,6 @@
1
1
  import { type OpenmrsResource } from '@openmrs/esm-framework';
2
2
  import { type ConceptMetadata } from '../common';
3
- import type { ObsReferenceRanges, ObservationInterpretation } from './types';
3
+ import type { FHIRInterpretation, ObsReferenceRanges, ObservationInterpretation } from './types';
4
4
  import { type VitalsBiometricsFormData } from '../vitals-biometrics-form/schema';
5
5
  import { type VitalsAndBiometricsFieldValuesMap } from './data.resource';
6
6
 
@@ -33,24 +33,53 @@ export function assessValue(value: number | undefined, range?: ObsReferenceRange
33
33
  return 'normal';
34
34
  }
35
35
 
36
+ export function mapFhirInterpretationToObservationInterpretation(
37
+ interpretation: FHIRInterpretation,
38
+ ): ObservationInterpretation {
39
+ const normalized = interpretation?.trim();
40
+ switch (normalized) {
41
+ case 'Critically Low':
42
+ return 'critically_low';
43
+ case 'Critically High':
44
+ return 'critically_high';
45
+ case 'High':
46
+ return 'high';
47
+ case 'Low':
48
+ return 'low';
49
+ case 'Normal':
50
+ return 'normal';
51
+ default:
52
+ return 'normal';
53
+ }
54
+ }
55
+
36
56
  export function interpretBloodPressure(
37
57
  systolic: number | undefined,
38
58
  diastolic: number | undefined,
39
59
  concepts: { systolicBloodPressureUuid?: string; diastolicBloodPressureUuid?: string } | undefined,
40
60
  conceptMetadata: Array<ConceptMetadata> | undefined,
61
+ systolicInterpretation?: ObservationInterpretation,
62
+ diastolicInterpretation?: ObservationInterpretation,
41
63
  ): ObservationInterpretation {
42
64
  if (!conceptMetadata) {
43
65
  return 'normal';
44
66
  }
45
67
 
46
- const systolicAssessment = assessValue(
47
- systolic,
48
- getReferenceRangesForConcept(concepts?.systolicBloodPressureUuid, conceptMetadata),
49
- );
50
-
51
- const diastolicAssessment = concepts?.diastolicBloodPressureUuid
52
- ? assessValue(diastolic, getReferenceRangesForConcept(concepts.diastolicBloodPressureUuid, conceptMetadata))
53
- : 'normal';
68
+ // Use interpretation from FHIR Observation when available (preferred).
69
+ // Fallback to calculation for backward compatibility: existing observations may not have
70
+ // interpretation set if they were created before interpretation was added, or if reference
71
+ // ranges weren't available at creation time.
72
+ const systolicAssessment =
73
+ systolicInterpretation ??
74
+ (concepts?.systolicBloodPressureUuid
75
+ ? assessValue(systolic, getReferenceRangesForConcept(concepts.systolicBloodPressureUuid, conceptMetadata))
76
+ : 'normal');
77
+
78
+ const diastolicAssessment =
79
+ diastolicInterpretation ??
80
+ (concepts?.diastolicBloodPressureUuid
81
+ ? assessValue(diastolic, getReferenceRangesForConcept(concepts.diastolicBloodPressureUuid, conceptMetadata))
82
+ : 'normal');
54
83
 
55
84
  if (systolicAssessment === 'critically_high' || diastolicAssessment === 'critically_high') {
56
85
  return 'critically_high';
@@ -20,12 +20,14 @@ export type ObservationInterpretation = 'critically_low' | 'critically_high' | '
20
20
 
21
21
  export type MappedVitals = {
22
22
  code: string;
23
- interpretation: string;
23
+ interpretation: ObservationInterpretation;
24
24
  recordedDate: string | Date;
25
25
  value: number;
26
26
  encounterId: string;
27
27
  };
28
28
 
29
+ export type FHIRInterpretation = 'Critically Low' | 'Critically High' | 'High' | 'Low' | 'Normal';
30
+
29
31
  export interface FHIRObservationResource {
30
32
  resourceType: string;
31
33
  id: string;
@@ -82,13 +84,23 @@ export interface FHIRObservationResource {
82
84
  hasMember?: Array<{
83
85
  reference: string;
84
86
  }>;
87
+ interpretation?: Array<{
88
+ coding: Array<{
89
+ code: string;
90
+ display: FHIRInterpretation;
91
+ system: string;
92
+ }>;
93
+ text: string;
94
+ }>;
85
95
  }
86
96
 
87
97
  export interface PatientVitalsAndBiometrics {
88
98
  id: string;
89
99
  date: string;
90
100
  systolic?: number;
101
+ systolicRenderInterpretation?: ObservationInterpretation;
91
102
  diastolic?: number;
103
+ diastolicRenderInterpretation?: ObservationInterpretation;
92
104
  bloodPressureRenderInterpretation?: ObservationInterpretation;
93
105
  pulse?: number;
94
106
  pulseRenderInterpretation?: ObservationInterpretation;
@@ -1,18 +1,18 @@
1
1
  import React, { useCallback } from 'react';
2
2
  import { useTranslation } from 'react-i18next';
3
3
  import { Layer, OverflowMenu, OverflowMenuItem } from '@carbon/react';
4
- import { getPatientUuidFromStore } from '@openmrs/esm-patient-common-lib';
5
4
  import { launchWorkspace, showModal, useLayoutType } from '@openmrs/esm-framework';
6
5
  import { patientVitalsBiometricsFormWorkspace } from '../../constants';
7
6
  import styles from './vitals-biometrics-action-menu.scss';
8
7
 
9
8
  interface VitalsAndBiometricsActionMenuProps {
9
+ patient: fhir.Patient;
10
10
  encounterUuid: string;
11
11
  }
12
12
 
13
- export const VitalsAndBiometricsActionMenu = ({ encounterUuid }: VitalsAndBiometricsActionMenuProps) => {
13
+ export const VitalsAndBiometricsActionMenu = ({ encounterUuid, patient }: VitalsAndBiometricsActionMenuProps) => {
14
14
  const { t } = useTranslation();
15
- const patientUuid = getPatientUuidFromStore();
15
+ const patientUuid = patient.id;
16
16
  const isTablet = useLayoutType() === 'tablet';
17
17
 
18
18
  const handleLaunchVitalsAndBiometricsForm = useCallback(() => {
package/src/index.ts CHANGED
@@ -11,7 +11,7 @@ import { configSchema } from './config-schema';
11
11
  import biometricsDetailedSummaryComponent from './biometrics/biometrics-main.component';
12
12
  import biometricsOverviewComponent from './biometrics/biometrics-overview.component';
13
13
  import { dashboardMeta } from './dashboard.meta';
14
- import vitalsHeaderComponent from './vitals-and-biometrics-header/vitals-header.component';
14
+ import vitalsHeaderComponent from './vitals-and-biometrics-header/vitals-header.extension';
15
15
  import vitalsMainComponent from './vitals/vitals-main.component';
16
16
  import vitalsSummaryComponent from './vitals/vitals-summary.component';
17
17
 
package/src/routes.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json.openmrs.org/routes.schema.json",
3
3
  "backendDependencies": {
4
4
  "fhir2": ">=1.2",
5
- "webservices.rest": "^2.2.0"
5
+ "webservices.rest": ">=2.2.0"
6
6
  },
7
7
  "extensions": [
8
8
  {
package/src/utils.ts CHANGED
@@ -10,10 +10,11 @@ import { invalidateCachedVitalsAndBiometrics } from './common';
10
10
  * @param currentVisit - The current visit.
11
11
  * @param config - The configuration object.
12
12
  */
13
- export function useLaunchVitalsAndBiometricsForm() {
13
+ export function useLaunchVitalsAndBiometricsForm(patientUuid: string) {
14
14
  const config = useConfig<ConfigObject>();
15
15
  const { useFormEngine, formName, formUuid } = config.vitals;
16
16
  const launchVitalsAndBiometricsForm = useLaunchWorkspaceRequiringVisit(
17
+ patientUuid,
17
18
  useFormEngine ? 'patient-form-entry-workspace' : patientVitalsBiometricsFormWorkspace,
18
19
  );
19
20
 
@@ -24,6 +24,7 @@ interface PaginatedVitalsProps {
24
24
  tableHeaders: Array<VitalsTableHeader>;
25
25
  tableRows: Array<VitalsTableRow>;
26
26
  urlLabel: string;
27
+ patient: fhir.Patient;
27
28
  }
28
29
 
29
30
  const PaginatedVitals: React.FC<PaginatedVitalsProps> = ({
@@ -33,6 +34,7 @@ const PaginatedVitals: React.FC<PaginatedVitalsProps> = ({
33
34
  tableHeaders,
34
35
  tableRows,
35
36
  urlLabel,
37
+ patient,
36
38
  }) => {
37
39
  const { t } = useTranslation();
38
40
  const isTablet = useLayoutType() === 'tablet';
@@ -132,7 +134,7 @@ const PaginatedVitals: React.FC<PaginatedVitalsProps> = ({
132
134
  );
133
135
  })}
134
136
  <TableCell className="cds--table-column-menu" id="actions">
135
- <VitalsAndBiometricsActionMenu encounterUuid={row.id} />
137
+ <VitalsAndBiometricsActionMenu patient={patient} encounterUuid={row.id} />
136
138
  </TableCell>
137
139
  </TableRow>
138
140
  ))}
@@ -40,7 +40,7 @@ const VitalsOverview: React.FC<VitalsOverviewProps> = ({ patientUuid, patient, p
40
40
  const isTablet = useLayoutType() === 'tablet';
41
41
  const [isPrinting, setIsPrinting] = useState(false);
42
42
  const contentToPrintRef = useRef(null);
43
- const launchVitalsBiometricsForm = useLaunchVitalsAndBiometricsForm();
43
+ const launchVitalsBiometricsForm = useLaunchVitalsAndBiometricsForm(patientUuid);
44
44
 
45
45
  const { excludePatientIdentifierCodeTypes } = useConfig();
46
46
  const { data: vitals, error, isLoading, isValidating } = useVitalsAndBiometrics(patientUuid);
@@ -243,6 +243,7 @@ const VitalsOverview: React.FC<VitalsOverviewProps> = ({ patientUuid, patient, p
243
243
  tableHeaders={tableHeaders}
244
244
  tableRows={tableRows}
245
245
  urlLabel={urlLabel}
246
+ patient={patient}
246
247
  />
247
248
  </div>
248
249
  )}
@@ -8,8 +8,7 @@ dayjs.extend(duration);
8
8
  import { Trans, useTranslation } from 'react-i18next';
9
9
  import { Button, InlineLoading, Tag } from '@carbon/react';
10
10
  import { ArrowRight } from '@carbon/react/icons';
11
- import { ConfigurableLink, formatDate, parseDate, useConfig, useWorkspaces } from '@openmrs/esm-framework';
12
- import { useVisitOrOfflineVisit } from '@openmrs/esm-patient-common-lib';
11
+ import { ConfigurableLink, formatDate, parseDate, useConfig, useVisit, useWorkspaces } from '@openmrs/esm-framework';
13
12
  import {
14
13
  assessValue,
15
14
  getReferenceRangesForConcept,
@@ -41,11 +40,11 @@ const VitalsHeader: React.FC<VitalsHeaderProps> = ({ patientUuid, hideLinks = fa
41
40
  const latestVitals = vitals?.[0];
42
41
  const [showDetailsPanel, setShowDetailsPanel] = useState(false);
43
42
  const toggleDetailsPanel = () => setShowDetailsPanel(!showDetailsPanel);
44
- const { currentVisit } = useVisitOrOfflineVisit(patientUuid);
43
+ const { activeVisit } = useVisit(patientUuid);
45
44
  const { workspaces } = useWorkspaces();
46
45
 
47
46
  const isWorkspaceOpen = useCallback(() => Boolean(workspaces?.length), [workspaces]);
48
- const launchForm = useLaunchVitalsAndBiometricsForm();
47
+ const launchForm = useLaunchVitalsAndBiometricsForm(patientUuid);
49
48
 
50
49
  const launchVitalsAndBiometricsForm = useCallback(
51
50
  (event: React.MouseEvent<HTMLButtonElement>) => {
@@ -62,7 +61,7 @@ const VitalsHeader: React.FC<VitalsHeaderProps> = ({ patientUuid, hideLinks = fa
62
61
  }
63
62
 
64
63
  if (latestVitals && Object.keys(latestVitals)?.length && conceptRanges?.length) {
65
- const hasActiveVisit = Boolean(currentVisit?.uuid);
64
+ const hasActiveVisit = Boolean(activeVisit?.uuid);
66
65
  const now = dayjs();
67
66
  const vitalsTakenTimeAgo = dayjs.duration(now.diff(latestVitals?.date));
68
67
  const vitalsOverdueThresholdHours = config.vitals.vitalsOverdueThresholdHours;
@@ -161,25 +160,30 @@ const VitalsHeader: React.FC<VitalsHeaderProps> = ({ patientUuid, hideLinks = fa
161
160
  latestVitals?.diastolic,
162
161
  config?.concepts,
163
162
  conceptRanges,
163
+ latestVitals?.systolicRenderInterpretation,
164
+ latestVitals?.diastolicRenderInterpretation,
164
165
  )}
165
166
  unitName={t('bp', 'BP')}
166
167
  unitSymbol={(latestVitals?.systolic && conceptUnits.get(config.concepts.systolicBloodPressureUuid)) ?? ''}
167
168
  value={`${latestVitals?.systolic ?? '--'} / ${latestVitals?.diastolic ?? '--'}`}
168
169
  />
169
170
  <VitalsHeaderItem
170
- interpretation={assessValue(
171
- latestVitals?.pulse,
172
- getReferenceRangesForConcept(config.concepts.pulseUuid, conceptRanges),
173
- )}
171
+ interpretation={
172
+ latestVitals?.pulseRenderInterpretation ??
173
+ assessValue(latestVitals?.pulse, getReferenceRangesForConcept(config.concepts.pulseUuid, conceptRanges))
174
+ }
174
175
  unitName={t('heartRate', 'Heart rate')}
175
176
  unitSymbol={(latestVitals?.pulse && conceptUnits.get(config.concepts.pulseUuid)) ?? ''}
176
177
  value={latestVitals?.pulse ?? '--'}
177
178
  />
178
179
  <VitalsHeaderItem
179
- interpretation={assessValue(
180
- latestVitals?.respiratoryRate,
181
- getReferenceRangesForConcept(config.concepts.respiratoryRateUuid, conceptRanges),
182
- )}
180
+ interpretation={
181
+ latestVitals?.respiratoryRateRenderInterpretation ??
182
+ assessValue(
183
+ latestVitals?.respiratoryRate,
184
+ getReferenceRangesForConcept(config.concepts.respiratoryRateUuid, conceptRanges),
185
+ )
186
+ }
183
187
  unitName={t('respiratoryRate', 'R. rate')}
184
188
  unitSymbol={
185
189
  (latestVitals?.respiratoryRate && conceptUnits.get(config.concepts.respiratoryRateUuid)) ?? ''
@@ -187,19 +191,25 @@ const VitalsHeader: React.FC<VitalsHeaderProps> = ({ patientUuid, hideLinks = fa
187
191
  value={latestVitals?.respiratoryRate ?? '--'}
188
192
  />
189
193
  <VitalsHeaderItem
190
- interpretation={assessValue(
191
- latestVitals?.spo2,
192
- getReferenceRangesForConcept(config.concepts.oxygenSaturationUuid, conceptRanges),
193
- )}
194
+ interpretation={
195
+ latestVitals?.spo2RenderInterpretation ??
196
+ assessValue(
197
+ latestVitals?.spo2,
198
+ getReferenceRangesForConcept(config.concepts.oxygenSaturationUuid, conceptRanges),
199
+ )
200
+ }
194
201
  unitName={t('spo2', 'SpO2')}
195
202
  unitSymbol={(latestVitals?.spo2 && conceptUnits.get(config.concepts.oxygenSaturationUuid)) ?? ''}
196
203
  value={latestVitals?.spo2 ?? '--'}
197
204
  />
198
205
  <VitalsHeaderItem
199
- interpretation={assessValue(
200
- latestVitals?.temperature,
201
- getReferenceRangesForConcept(config.concepts.temperatureUuid, conceptRanges),
202
- )}
206
+ interpretation={
207
+ latestVitals?.temperatureRenderInterpretation ??
208
+ assessValue(
209
+ latestVitals?.temperature,
210
+ getReferenceRangesForConcept(config.concepts.temperatureUuid, conceptRanges),
211
+ )
212
+ }
203
213
  unitName={t('temperatureAbbreviated', 'Temp')}
204
214
  unitSymbol={(latestVitals?.temperature && conceptUnits.get(config.concepts.temperatureUuid)) ?? ''}
205
215
  value={latestVitals?.temperature ?? '--'}
@@ -3,6 +3,8 @@
3
3
  @use '@carbon/type';
4
4
  @use '@openmrs/esm-styleguide/src/vars' as *;
5
5
 
6
+ // Designs: https://app.zeplin.io/project/60d59321e8100b0324762e05/screen/648c44d9d4052c613e7f23da
7
+
6
8
  .container {
7
9
  background-color: $openmrs-background-grey;
8
10
  padding: layout.$spacing-03 layout.$spacing-05 layout.$spacing-05;
@@ -52,21 +54,23 @@
52
54
 
53
55
  .rowContainer {
54
56
  width: 100%;
57
+ container-type: inline-size;
58
+ container-name: vitals-header-row-container;
55
59
  }
56
60
 
57
61
  .row {
58
- display: flex;
59
- justify-content: space-between;
60
62
  align-items: center;
61
- max-width: 100vw;
63
+ width: 100%;
64
+ // Wide viewport: 8 columns (default)
65
+ display: grid;
66
+ grid-template-columns: repeat(8, minmax(0, 1fr));
67
+ column-gap: layout.$spacing-05;
62
68
  }
63
69
 
64
- :global(.omrs-breakpoint-lt-desktop),
65
- .workspaceOpen {
70
+ // Narrow viewport: 4 columns (when workspace is opened or container is reduced)
71
+ @container vitals-header-row-container (max-width: 80rem) {
66
72
  .row {
67
- display: grid;
68
73
  grid-template-columns: repeat(4, minmax(0, 1fr));
69
- column-gap: layout.$spacing-11;
70
74
  }
71
75
  }
72
76
 
@@ -2,18 +2,18 @@ import React from 'react';
2
2
  import dayjs from 'dayjs';
3
3
  import { screen } from '@testing-library/react';
4
4
  import userEvent from '@testing-library/user-event';
5
- import { type WorkspacesInfo, getDefaultsFromConfigSchema, useConfig, useWorkspaces } from '@openmrs/esm-framework';
6
- import { mockPatient, getByTextWithMarkup, renderWithSwr, waitForLoadingToFinish } from 'tools';
7
5
  import {
8
- formattedVitals,
9
- mockConceptUnits,
10
- mockCurrentVisit,
11
- mockVitalsConceptMetadata,
12
- mockVitalsConfig,
13
- } from '__mocks__';
6
+ type WorkspacesInfo,
7
+ getDefaultsFromConfigSchema,
8
+ useConfig,
9
+ useVisit,
10
+ useWorkspaces,
11
+ } from '@openmrs/esm-framework';
12
+ import { mockPatient, getByTextWithMarkup, renderWithSwr, waitForLoadingToFinish } from 'tools';
13
+ import { formattedVitals, mockConceptUnits, mockVisit, mockVitalsConceptMetadata, mockVitalsConfig } from '__mocks__';
14
14
  import { configSchema, type ConfigObject } from '../config-schema';
15
- import { useVitalsAndBiometrics } from '../common';
16
- import VitalsHeader from './vitals-header.component';
15
+ import { type PatientVitalsAndBiometrics, useVitalsAndBiometrics } from '../common';
16
+ import VitalsHeader from './vitals-header.extension';
17
17
 
18
18
  const testProps = {
19
19
  patientUuid: mockPatient.id,
@@ -23,6 +23,7 @@ const testProps = {
23
23
  const mockUseConfig = jest.mocked(useConfig<ConfigObject>);
24
24
  const mockUseVitalsAndBiometrics = jest.mocked(useVitalsAndBiometrics);
25
25
  const mockUseWorkspaces = jest.mocked(useWorkspaces);
26
+ const mockUseVisit = jest.mocked(useVisit);
26
27
 
27
28
  mockUseWorkspaces.mockReturnValue({ workspaces: [] } as WorkspacesInfo);
28
29
  const mockLaunchWorkspaceRequiringVisit = jest.fn();
@@ -35,7 +36,6 @@ jest.mock('@openmrs/esm-patient-common-lib', () => {
35
36
 
36
37
  return {
37
38
  ...originalModule,
38
- useVisitOrOfflineVisit: jest.fn().mockImplementation(() => ({ currentVisit: mockCurrentVisit })),
39
39
  useLaunchWorkspaceRequiringVisit: jest.fn().mockImplementation(() => mockUseLaunchWorkspaceRequiringVisit),
40
40
  };
41
41
  });
@@ -76,6 +76,7 @@ describe('VitalsHeader', () => {
76
76
  });
77
77
 
78
78
  it('renders the most recently recorded values in the vitals header', async () => {
79
+ mockUseVisit.mockReturnValueOnce({ activeVisit: mockVisit } as ReturnType<typeof useVisit>);
79
80
  mockUseVitalsAndBiometrics.mockReturnValue({
80
81
  data: [
81
82
  {
@@ -129,6 +130,7 @@ describe('VitalsHeader', () => {
129
130
  });
130
131
 
131
132
  it('displays correct overdue tag for vitals 5 days old', async () => {
133
+ mockUseVisit.mockReturnValueOnce({ activeVisit: mockVisit } as ReturnType<typeof useVisit>);
132
134
  const fiveDaysAgo = dayjs().subtract(5, 'days').toISOString();
133
135
  const vitalsData = [
134
136
  {
@@ -260,4 +262,98 @@ describe('VitalsHeader', () => {
260
262
  expect(screen.queryByRole('link', { name: /vitals history/i })).not.toBeInTheDocument();
261
263
  expect(screen.queryByRole('button', { name: /record vitals/i })).not.toBeInTheDocument();
262
264
  });
265
+
266
+ it('uses backend interpretation without recalculating', async () => {
267
+ // Pulse 240 would normally be calculated as "critically_high" (>= 230)
268
+ // Temperature 41 would normally be calculated as "high" (> 37.5 but < 43)
269
+ // SpO2 70 would normally be calculated as "critically_low" (< 95)
270
+ // Respiratory Rate 5 would normally be calculated as "critically_low" (< 12)
271
+ // Backend sends the interpretation for all vitals as "normal", except for Respiratory Rate which is "critically_low" (< 12)
272
+ // It should use backend's interpretation and NOT recalculate
273
+ const vitalsWithConflictingInterpretation: PatientVitalsAndBiometrics[] = [
274
+ {
275
+ id: '0',
276
+ date: '2021-05-19T04:26:51.000Z',
277
+ pulse: 240,
278
+ temperature: 41,
279
+ respiratoryRate: 5,
280
+ diastolic: 145,
281
+ systolic: 240,
282
+ spo2: 70,
283
+ diastolicRenderInterpretation: 'normal',
284
+ systolicRenderInterpretation: 'normal',
285
+ bloodPressureRenderInterpretation: 'normal',
286
+ pulseRenderInterpretation: 'normal',
287
+ temperatureRenderInterpretation: 'normal',
288
+ respiratoryRateRenderInterpretation: 'critically_low',
289
+ spo2RenderInterpretation: 'normal',
290
+ },
291
+ ];
292
+
293
+ mockUseVitalsAndBiometrics.mockReturnValue({
294
+ data: vitalsWithConflictingInterpretation,
295
+ } as ReturnType<typeof useVitalsAndBiometrics>);
296
+
297
+ renderWithSwr(<VitalsHeader {...testProps} />);
298
+
299
+ await waitForLoadingToFinish();
300
+
301
+ expect(getByTextWithMarkup(/BP\s*240 \/ 145\s*mmHg/i)).toBeInTheDocument();
302
+ expect(getByTextWithMarkup(/Heart rate\s*240\s*beats\/min/i)).toBeInTheDocument();
303
+ expect(getByTextWithMarkup(/Temp\s*41\s*DEG C/i)).toBeInTheDocument();
304
+ expect(getByTextWithMarkup(/R\. Rate\s*5\s*breaths\/min/i)).toBeInTheDocument();
305
+ expect(getByTextWithMarkup(/SpO2\s*70\s*/i)).toBeInTheDocument();
306
+
307
+ expect(screen.getAllByTitle(/abnormal value/i)).toHaveLength(1);
308
+ expect(screen.getByTitle(/abnormal value/i)).toHaveClass('critically-low');
309
+ });
310
+
311
+ it('recalculates interpretation when backend does not provide interpretation', async () => {
312
+ // All vitals are abnormal, and backend does not provide interpretation for any of them.
313
+ // It should fallback to recalculating and mark them as abnormal.
314
+ const vitalsWithoutInterpretation: PatientVitalsAndBiometrics[] = [
315
+ {
316
+ id: '0',
317
+ date: '2021-05-19T04:26:51.000Z',
318
+ pulse: 240, // should be marked as "critically_high"
319
+ temperature: 41, // should be marked as "high"
320
+ respiratoryRate: 5, // should be marked as "low"
321
+ diastolic: 145,
322
+ systolic: 240, // blood pressure should be marked as "high"
323
+ spo2: 70, // should be marked as "low"
324
+ },
325
+ ];
326
+
327
+ mockUseVitalsAndBiometrics.mockReturnValue({
328
+ data: vitalsWithoutInterpretation,
329
+ } as ReturnType<typeof useVitalsAndBiometrics>);
330
+
331
+ renderWithSwr(<VitalsHeader {...testProps} />);
332
+
333
+ await waitForLoadingToFinish();
334
+
335
+ expect(getByTextWithMarkup(/BP\s*240 \/ 145\s*mmHg/i)).toBeInTheDocument();
336
+ expect(getByTextWithMarkup(/Heart rate\s*240\s*beats\/min/i)).toBeInTheDocument();
337
+ expect(getByTextWithMarkup(/Temp\s*41\s*DEG C/i)).toBeInTheDocument();
338
+ expect(getByTextWithMarkup(/R\. Rate\s*5\s*breaths\/min/i)).toBeInTheDocument();
339
+ expect(getByTextWithMarkup(/SpO2\s*70\s*/i)).toBeInTheDocument();
340
+
341
+ const abnormalValueElements = screen.getAllByTitle(/abnormal value/i);
342
+ expect(abnormalValueElements).toHaveLength(5);
343
+
344
+ const lowElements = abnormalValueElements.filter((element) => {
345
+ return element.className === 'low';
346
+ });
347
+ expect(lowElements).toHaveLength(2);
348
+
349
+ const highElements = abnormalValueElements.filter((element) => {
350
+ return element.className === 'high';
351
+ });
352
+ expect(highElements).toHaveLength(2);
353
+
354
+ const criticallyHighElements = abnormalValueElements.filter((element) => {
355
+ return element.className === 'critically-high';
356
+ });
357
+ expect(criticallyHighElements).toHaveLength(1);
358
+ });
263
359
  });