/** @module @category UI */ import React, { useMemo, useId } from 'react'; import classNames from 'classnames'; import { getCoreTranslation } from '@openmrs/esm-translations'; import { calculateInterpretation, normalizeInterpretation, type ObservationInterpretation, type OBSERVATION_INTERPRETATION, type ObsReferenceRanges, } from './interpretation-utils'; import { useConceptReferenceRange } from './use-concept-reference-range'; import styles from './numeric-observation.module.scss'; export interface NumericObservationProps { /** The observation value to display */ value: string | number; /** Unit of measurement */ unit?: string; /** Label for the observation (only shown for card variant)*/ label?: string; /** Pre-calculated interpretation (either ObservationInterpretation or OBSERVATION_INTERPRETATION format) */ interpretation?: ObservationInterpretation | OBSERVATION_INTERPRETATION; /** Reference range for calculating interpretation */ referenceRange?: ObsReferenceRanges; /** Concept UUID to fetch reference range from */ conceptUuid?: string; /** * Display style variant, defaults to 'card' * - 'card': Card-style container with colored borders and backgrounds, typically used in header/summary views (e.g., vitals header) * - 'cell': Table cell styling with background colors, typically used in data tables (e.g., test results table). If using the cell variant inside a Carbon Table Cell, make sure to set the padding to 0. */ variant?: 'card' | 'cell'; patientUuid: string; } /** * Generic numeric observation component for displaying numeric observation values with interpretation-based styling. * Supports both vitals and test results display patterns. */ export const NumericObservation: React.FC = ({ value, unit, label, interpretation: providedInterpretation, referenceRange: providedReferenceRange, conceptUuid, variant = 'card', patientUuid, }) => { const generatedId = useId(); const { referenceRange: fetchedReferenceRange, isLoading: isLoadingConcept } = useConceptReferenceRange( providedReferenceRange ? undefined : conceptUuid, patientUuid, ); const referenceRange = providedReferenceRange ?? fetchedReferenceRange; const calculatedInterpretation = useMemo(() => { if (providedInterpretation) { return normalizeInterpretation(providedInterpretation); } if (referenceRange && !isLoadingConcept) { return calculateInterpretation(value, referenceRange); } return 'normal'; }, [providedInterpretation, referenceRange, value, isLoadingConcept]); const interpretation = calculatedInterpretation ?? 'normal'; const flaggedCritical = interpretation === 'critically_low' || interpretation === 'critically_high' || interpretation === 'off_scale_low' || interpretation === 'off_scale_high'; const flaggedAbnormal = interpretation !== 'normal'; const labelId = label ? `omrs-numeric-obs-label-${label.replaceAll(/\s+/g, '-').toLowerCase()}-${generatedId}` : undefined; const valueId = `omrs-numeric-obs-value-${generatedId}`; const unitId = `omrs-numeric-obs-unit-${generatedId}`; const displayValue = value || getCoreTranslation('notAvailable', 'Not available'); const interpretationClasses = classNames({ [styles['critically-low']]: interpretation === 'critically_low' || interpretation === 'off_scale_low', [styles['critically-high']]: interpretation === 'critically_high' || interpretation === 'off_scale_high', [styles.low]: interpretation === 'low', [styles.high]: interpretation === 'high', [styles['off-scale-low']]: interpretation === 'off_scale_low', [styles['off-scale-high']]: interpretation === 'off_scale_high', }); const cardContainerClasses = classNames({ [styles.container]: true, [styles.card]: true, [styles['critical-value']]: flaggedCritical, [styles['abnormal-value']]: flaggedAbnormal && !flaggedCritical, }); const cellClasses = classNames({ [styles.cell]: true, [interpretationClasses]: true, }); if (variant === 'cell') { return (
{displayValue} {unit ? ` ${unit}` : ''}
); } return (
{label && (
{label} {flaggedAbnormal && ( )}
)}
{displayValue} {value && unit && ( {unit} )}
); };