diff --git a/packages/esm-patient-vitals-app/src/vitals/vitals-biometrics-form/vitals-biometrics-form.component.tsx b/packages/esm-patient-vitals-app/src/vitals/vitals-biometrics-form/vitals-biometrics-form.component.tsx index b58a75cfd9..da68bcfb9a 100644 --- a/packages/esm-patient-vitals-app/src/vitals/vitals-biometrics-form/vitals-biometrics-form.component.tsx +++ b/packages/esm-patient-vitals-app/src/vitals/vitals-biometrics-form/vitals-biometrics-form.component.tsx @@ -79,7 +79,7 @@ const VitalsAndBiometricForms: React.FC = ({ patientUuid, const [showErrorMessage, setShowErrorMessage] = useState(false); const [hasInvalidVitals, setHasInvalidVitals] = useState(false); - const { control, handleSubmit, getValues, watch, setValue } = useForm({ + const { control, handleSubmit, watch, setValue } = useForm({ mode: 'all', resolver: zodResolver(vitalsBiometricsFormSchema), }); @@ -227,6 +227,32 @@ const VitalsAndBiometricForms: React.FC = ({ patientUuid,

{t('recordVitals', 'Record vitals')}

+ + + = ({ patientUuid, diastolicBloodPressure, ) } - title={t('bloodPressure', 'Blood Pressure')} + title={t('bloodPressure', 'Blood pressure')} unitSymbol={conceptUnits.get(config.concepts.systolicBloodPressureUuid) ?? ''} /> @@ -286,17 +312,16 @@ const VitalsAndBiometricForms: React.FC = ({ patientUuid, getReferenceRangesForConcept(config.concepts.pulseUuid, conceptMetadata), )} isWithinNormalRange={isValueWithinReferenceRange(conceptMetadata, config.concepts['pulseUuid'], pulse)} - title={t('pulse', 'Pulse')} + title={t('heartRate', 'Heart rate')} unitSymbol={conceptUnits.get(config.concepts.pulseUuid) ?? ''} /> - = ({ patientUuid, respiratoryRate, )} showErrorMessage={showErrorMessage} - title={t('respirationRate', 'Respiration Rate')} + title={t('respirationRate', 'Respiration rate')} unitSymbol={conceptUnits.get(config.concepts.respiratoryRateUuid) ?? ''} /> @@ -322,7 +347,7 @@ const VitalsAndBiometricForms: React.FC = ({ patientUuid, control={control} fields={[ { - name: t('oxygenSaturation', 'Oxygen Saturation'), + name: t('oxygenSaturation', 'Oxygen saturation'), type: 'number', min: concepts.oxygenSaturationRange.lowAbsolute, max: concepts.oxygenSaturationRange.highAbsolute, @@ -343,33 +368,6 @@ const VitalsAndBiometricForms: React.FC = ({ patientUuid, unitSymbol={conceptUnits.get(config.concepts.oxygenSaturationUuid) ?? ''} /> - - - - diff --git a/packages/esm-patient-vitals-app/src/vitals/vitals-biometrics-form/vitals-biometrics-form.test.tsx b/packages/esm-patient-vitals-app/src/vitals/vitals-biometrics-form/vitals-biometrics-form.test.tsx index 36300636ac..faa973a989 100644 --- a/packages/esm-patient-vitals-app/src/vitals/vitals-biometrics-form/vitals-biometrics-form.test.tsx +++ b/packages/esm-patient-vitals-app/src/vitals/vitals-biometrics-form/vitals-biometrics-form.test.tsx @@ -192,7 +192,7 @@ describe('VitalsBiometricsForm', () => { ); }); - it('renders an error notification if there was a problem saving vital biometrics', async () => { + it('renders an error notification if there was a problem saving vitals and biometrics', async () => { const user = userEvent.setup(); const error = { diff --git a/packages/esm-patient-vitals-app/src/vitals/vitals-biometrics-form/vitals-biometrics-input.component.tsx b/packages/esm-patient-vitals-app/src/vitals/vitals-biometrics-form/vitals-biometrics-input.component.tsx index 485531e389..0380967dd1 100644 --- a/packages/esm-patient-vitals-app/src/vitals/vitals-biometrics-form/vitals-biometrics-input.component.tsx +++ b/packages/esm-patient-vitals-app/src/vitals/vitals-biometrics-form/vitals-biometrics-input.component.tsx @@ -26,6 +26,7 @@ interface ResponsiveWrapperProps { children: React.ReactNode; isTablet: boolean; } + interface VitalsBiometricInputProps { control: Control; muacColorCode?: string; @@ -69,14 +70,14 @@ const VitalsBiometricInput: React.FC = ({ const { t } = useTranslation(); const isTablet = useLayoutType() === 'tablet'; const [invalid, setInvalid] = useState(false); - const [isFocused, setFocused] = useState(false); + const [isFocused, setIsFocused] = useState(false); const handleFocus = () => { - setFocused(true); + setIsFocused(true); }; const handleBlur = () => { - setFocused(false); + setIsFocused(false); }; const isFlaggedCritical = @@ -100,8 +101,9 @@ const VitalsBiometricInput: React.FC = ({ }); const textInputClasses = classNames(styles.textInputContainer, { - [styles.focused]: isFocused, [styles['critical-value']]: !isFocused && isFlaggedCritical, + [styles.focused]: isFocused, + [styles.readonly]: readOnly, [muacColorCode]: useMuacColors, [errorMessageClass]: true, }); @@ -134,8 +136,8 @@ const VitalsBiometricInput: React.FC = ({ ({ ...jest.requireActual('react-hook-form'), @@ -55,44 +56,347 @@ jest.mock('react-hook-form', () => ({ }), })); +const mockConceptMetadata = [ + { + uuid: '5085AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + display: 'Systolic blood pressure', + hiNormal: 140, + hiAbsolute: 250, + hiCritical: 180, + lowNormal: 100, + lowAbsolute: 0, + lowCritical: 85, + units: 'mmHg', + }, + { + uuid: '5086AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + display: 'Diastolic blood pressure', + hiNormal: 90, + hiAbsolute: 150, + hiCritical: 120, + lowNormal: 55, + lowAbsolute: 0, + lowCritical: 40, + units: 'mmHg', + }, + { + uuid: '5088AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + display: 'Temperature (c)', + hiNormal: null, + hiAbsolute: 43, + hiCritical: null, + lowNormal: null, + lowAbsolute: 25, + lowCritical: null, + units: 'DEG C', + }, + { + uuid: '5090AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + display: 'Height (cm)', + hiNormal: null, + hiAbsolute: 272, + hiCritical: null, + lowNormal: null, + lowAbsolute: 10, + lowCritical: null, + units: 'cm', + }, + { + uuid: '5089AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + display: 'Weight (kg)', + hiNormal: null, + hiAbsolute: 250, + hiCritical: null, + lowNormal: null, + lowAbsolute: 0, + lowCritical: null, + units: 'kg', + }, + { + uuid: '5087AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + display: 'Pulse', + hiNormal: 100, + hiAbsolute: 230, + hiCritical: 130, + lowNormal: 55, + lowAbsolute: 0, + lowCritical: 49, + units: 'beats/min', + }, + { + uuid: '5092AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + display: 'Arterial blood oxygen saturation (pulse oximeter)', + hiNormal: null, + hiAbsolute: 100, + hiCritical: null, + lowNormal: 95, + lowAbsolute: 0, + lowCritical: 90, + units: '%', + }, + { + uuid: '1343AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + display: 'Mid-upper arm circumference', + hiNormal: null, + hiAbsolute: null, + hiCritical: null, + lowNormal: null, + lowAbsolute: null, + lowCritical: null, + units: 'cm', + }, + { + uuid: '5242AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + display: 'Respiratory rate', + hiNormal: 18, + hiAbsolute: 999, + hiCritical: 26, + lowNormal: 12, + lowAbsolute: 0, + lowCritical: 8, + units: 'breaths/min', + }, + { + uuid: '5283AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + display: 'Karnofsky performance score', + hiNormal: null, + hiAbsolute: null, + hiCritical: null, + lowNormal: null, + lowAbsolute: null, + lowCritical: null, + units: '%', + }, +]; + +jest.mock('@openmrs/esm-patient-common-lib', () => { + const originalModule = jest.requireActual('@openmrs/esm-patient-common-lib'); + + return { + ...originalModule, + useVitalsConceptMetadata: jest.fn().mockImplementation(() => ({ + data: new Map([ + ['5085AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', 'mmHg'], + ['5086AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', 'mmHg'], + ['5088AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', 'DEG C'], + ['5090AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', 'cm'], + ['5089AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', 'kg'], + ['5087AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', 'beats/min'], + ['5092AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', '%'], + ['1343AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', 'cm'], + ['5242AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', 'breaths/min'], + ['5283AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', '%'], + ]), + conceptMetadata: [ + { + uuid: '5085AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + display: 'Systolic blood pressure', + hiNormal: 140, + hiAbsolute: 250, + hiCritical: 180, + lowNormal: 100, + lowAbsolute: 0, + lowCritical: 85, + units: 'mmHg', + }, + { + uuid: '5086AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + display: 'Diastolic blood pressure', + hiNormal: 90, + hiAbsolute: 150, + hiCritical: 120, + lowNormal: 55, + lowAbsolute: 0, + lowCritical: 40, + units: 'mmHg', + }, + { + uuid: '5088AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + display: 'Temperature (c)', + hiNormal: null, + hiAbsolute: 43, + hiCritical: null, + lowNormal: null, + lowAbsolute: 25, + lowCritical: null, + units: 'DEG C', + }, + { + uuid: '5090AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + display: 'Height (cm)', + hiNormal: null, + hiAbsolute: 272, + hiCritical: null, + lowNormal: null, + lowAbsolute: 10, + lowCritical: null, + units: 'cm', + }, + { + uuid: '5089AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + display: 'Weight (kg)', + hiNormal: null, + hiAbsolute: 250, + hiCritical: null, + lowNormal: null, + lowAbsolute: 0, + lowCritical: null, + units: 'kg', + }, + { + uuid: '5087AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + display: 'Pulse', + hiNormal: 100, + hiAbsolute: 230, + hiCritical: 130, + lowNormal: 55, + lowAbsolute: 0, + lowCritical: 49, + units: 'beats/min', + }, + { + uuid: '5092AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + display: 'Arterial blood oxygen saturation (pulse oximeter)', + hiNormal: null, + hiAbsolute: 100, + hiCritical: null, + lowNormal: 95, + lowAbsolute: 0, + lowCritical: 90, + units: '%', + }, + { + uuid: '1343AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + display: 'Mid-upper arm circumference', + hiNormal: null, + hiAbsolute: null, + hiCritical: null, + lowNormal: null, + lowAbsolute: null, + lowCritical: null, + units: 'cm', + }, + { + uuid: '5242AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + display: 'Respiratory rate', + hiNormal: 18, + hiAbsolute: 999, + hiCritical: 26, + lowNormal: 12, + lowAbsolute: 0, + lowCritical: 8, + units: 'breaths/min', + }, + { + uuid: '5283AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + display: 'Karnofsky performance score', + hiNormal: null, + hiAbsolute: null, + hiCritical: null, + lowNormal: null, + lowAbsolute: null, + lowCritical: null, + units: '%', + }, + ], + })), + }; +}); + +jest.mock('@openmrs/esm-framework', () => { + const originalModule = jest.requireActual('@openmrs/esm-framework'); + + return { + ...originalModule, + useConfig: jest.fn().mockReturnValue({ + concepts: { + pulseUuid: '5087AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + }, + }), + }; +}); + +const testProps = { + control: undefined, + isWithinNormalRange: true, + fields: [], + interpretation: undefined, + placeholder: '', + title: '', + unitSymbol: '', +}; + describe('VitalsBiometricsInput', () => { - it('should display the correct text input with correct value', async () => { - const user = userEvent.setup(); - - render( - , - ); + it('renders number inputs based correctly on the props provided', () => { + testProps.fields = [ + { + id: 'pulse', + name: 'Heart rate', + type: 'number', + }, + ]; + testProps.title = 'Heart rate'; + testProps.unitSymbol = 'bpm'; + + renderVitalsBiometricsInput(); - expect(screen.getByText(/Heart Rate/i)).toBeInTheDocument(); + const heartRateInput = screen.getByRole('spinbutton', { name: /heart rate/i }); + expect(heartRateInput).toBeInTheDocument(); + expect(screen.getByPlaceholderText('--')).toBeInTheDocument(); + expect(screen.getByTitle(/heart rate/i)).toBeInTheDocument(); expect(screen.getByText(/bpm/i)).toBeInTheDocument(); - expect(screen.getByRole('spinbutton')).toBeInTheDocument(); + }); + + it('renders textarea inputs correctly based on the props provided', () => { + testProps.fields = [ + { + id: 'generalPatientNote', + name: 'Notes', + type: 'textArea', + }, + ]; + testProps.placeholder = 'Type any additional notes here'; + testProps.title = 'Notes'; + + renderVitalsBiometricsInput(); - const inputTextBox = await screen.findByRole('spinbutton'); - await user.type(inputTextBox, '75'); + const noteInput = screen.getByRole('textbox', { name: /notes/i }); + expect(noteInput).toBeInTheDocument(); + expect(screen.getByPlaceholderText(/type any additional notes here/i)).toBeInTheDocument(); + expect(screen.getByTitle(/notes/i)).toBeInTheDocument(); }); - it('should display the correct text area with correct value', async () => { - render( - , + it('should validate the input based on the provided interpretation and reference range values', () => { + const config = useConfig(); + + testProps.fields = [ + { + id: 'pulse', + name: 'Heart rate', + min: 0, + max: 230, + type: 'number', + }, + ]; + testProps.interpretation = assessValue( + 300, + getReferenceRangesForConcept(config.concepts.pulseUuid, mockConceptMetadata), ); + testProps.title = 'Heart rate'; + testProps.unitSymbol = 'bpm'; + + renderVitalsBiometricsInput(); - expect(screen.getByRole('textbox')).toBeInTheDocument(); + screen.findByRole('spinbutton'); + + expect(screen.getByRole('spinbutton', { name: /heart rate/i })).toBeInTheDocument(); + const abnormalValueFlag = screen.getByTitle(/abnormal value/i); + expect(abnormalValueFlag).toBeInTheDocument(); + const criticallyHighFlag = abnormalValueFlag.querySelector('span.critically-high'); + expect(criticallyHighFlag).toBeInTheDocument(); }); }); + +function renderVitalsBiometricsInput() { + render(); +} diff --git a/packages/esm-patient-vitals-app/src/vitals/vitals-chart.component.tsx b/packages/esm-patient-vitals-app/src/vitals/vitals-chart.component.tsx index 17430ea10f..6913479f4d 100644 --- a/packages/esm-patient-vitals-app/src/vitals/vitals-chart.component.tsx +++ b/packages/esm-patient-vitals-app/src/vitals/vitals-chart.component.tsx @@ -42,7 +42,7 @@ const VitalsChart: React.FC = ({ patientVitals, conceptUnits, }, { id: 'oxygenSaturation', - title: withUnit(t('spo2', 'SPO2'), conceptUnits.get(config.concepts.oxygenSaturationUuid) ?? '-'), + title: withUnit(t('spo2', 'SpO2'), conceptUnits.get(config.concepts.oxygenSaturationUuid) ?? '-'), value: 'spo2', }, { @@ -52,7 +52,7 @@ const VitalsChart: React.FC = ({ patientVitals, conceptUnits, }, { id: 'respiratoryRate', - title: withUnit(t('respiratoryRate', 'R. Rate'), conceptUnits.get(config.concepts.respiratoryRateUuid) ?? '-'), + title: withUnit(t('respiratoryRate', 'R. rate'), conceptUnits.get(config.concepts.respiratoryRateUuid) ?? '-'), value: 'respiratoryRate', }, { diff --git a/packages/esm-patient-vitals-app/translations/am.json b/packages/esm-patient-vitals-app/translations/am.json index 6491940411..aa838aac19 100644 --- a/packages/esm-patient-vitals-app/translations/am.json +++ b/packages/esm-patient-vitals-app/translations/am.json @@ -28,7 +28,7 @@ "pulse": "Pulse", "recordBiometrics": "Record biometrics", "recordVitals": "Record vitals", - "respirationRate": "Respiration Rate", + "respirationRate": "Respiration rate", "respiratoryRate": "R. rate", "saveAndClose": "Save and close", "seeAll": "See all", diff --git a/packages/esm-patient-vitals-app/translations/en.json b/packages/esm-patient-vitals-app/translations/en.json index 797ca6ba14..9d76834f3f 100644 --- a/packages/esm-patient-vitals-app/translations/en.json +++ b/packages/esm-patient-vitals-app/translations/en.json @@ -28,8 +28,8 @@ "pulse": "Pulse", "recordBiometrics": "Record biometrics", "recordVitals": "Record vitals", - "respirationRate": "Respiration Rate", - "respiratoryRate": "R. Rate", + "respirationRate": "Respiration rate", + "respiratoryRate": "R. rate", "saveAndClose": "Save and close", "seeAll": "See all", "spo2": "SpO2",