From 95d24a853d6ce2e893dc9fc46425e8a7fe3810fe Mon Sep 17 00:00:00 2001 From: CynthiaKamau Date: Mon, 10 Jun 2024 12:36:47 +0300 Subject: [PATCH] O3-2252 Calculated values shouldn't be overwritten by the Encounter's values --- .../use-initial-values/encounter.mock.json | 51 ++++++++ src/api/api.ts | 1 + src/hooks/useInitialValues.test.ts | 115 +++++++++++++++++- src/hooks/useInitialValues.ts | 26 +++- 4 files changed, 184 insertions(+), 9 deletions(-) diff --git a/__mocks__/use-initial-values/encounter.mock.json b/__mocks__/use-initial-values/encounter.mock.json index 4a305fdbe..692778c0f 100644 --- a/__mocks__/use-initial-values/encounter.mock.json +++ b/__mocks__/use-initial-values/encounter.mock.json @@ -798,6 +798,57 @@ } ], "resourceVersion": "2.1" + }, + { + "uuid": "30f1206c-4354-4deb-9cfa-bcb18e934144", + "obsDatetime": "2024-06-11T11:43:33.000+0000", + "comment": null, + "voided": false, + "groupMembers": null, + "formFieldNamespace": "rfe-forms", + "formFieldPath": "rfe-forms-height", + "concept": { + "uuid": "5090AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + "name": { + "uuid": "d17c467f-e5c1-30dc-b916-e7d5f9a8935a", + "name": "Height (cm)" + } + }, + "value": 176.0 + }, + { + "uuid": "037aaaca-14f1-460c-bc59-579817569794", + "obsDatetime": "2024-06-11T11:43:33.000+0000", + "comment": null, + "voided": false, + "groupMembers": null, + "formFieldNamespace": "rfe-forms", + "formFieldPath": "rfe-forms-weight", + "concept": { + "uuid": "5089AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + "name": { + "uuid": "c1015fd1-729b-33c7-82e8-0b14a5a824ed", + "name": "Weight (kg)" + } + }, + "value": 56.0 + }, + { + "uuid": "5c281bdc-3798-4d18-b780-34e177e2fdb4", + "obsDatetime": "2024-06-11T11:43:33.000+0000", + "comment": null, + "voided": false, + "groupMembers": null, + "formFieldNamespace": "rfe-forms", + "formFieldPath": "rfe-forms-bmi", + "concept": { + "uuid": "1342AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + "name": { + "uuid": "1431BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", + "name": "Body mass index" + } + }, + "value": 0 } ], "orders": [ diff --git a/src/api/api.ts b/src/api/api.ts index c55245f4c..889afd490 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -78,6 +78,7 @@ export function getLatestObs(patientUuid: string, conceptUuid: string, encounter let params = `patient=${patientUuid}&code=${conceptUuid}${ encounterTypeUuid ? `&encounter.type=${encounterTypeUuid}` : '' }`; + // the latest obs params += '&_sort=-date&_count=1'; return openmrsFetch(`${fhirBaseUrl}/Observation?${params}`).then(({ data }) => { diff --git a/src/hooks/useInitialValues.test.ts b/src/hooks/useInitialValues.test.ts index 521d194dc..d0f73920e 100644 --- a/src/hooks/useInitialValues.test.ts +++ b/src/hooks/useInitialValues.test.ts @@ -1,10 +1,11 @@ -import { act, renderHook } from '@testing-library/react'; +import { act, renderHook, screen } from '@testing-library/react'; import { useInitialValues } from './useInitialValues'; import type { FormField, OpenmrsEncounter } from '../types'; import testEncounter from '__mocks__/use-initial-values/encounter.mock.json'; import testPatient from '__mocks__/use-initial-values/patient.mock.json'; import { ObsSubmissionHandler } from '../submission-handlers/obsHandler'; import { TestOrderSubmissionHandler } from '../submission-handlers/testOrderHandler'; +import { CommonExpressionHelpers } from 'src/utils/common-expression-helpers'; const obsGroupMembers: Array = [ { @@ -120,6 +121,7 @@ let allFormFields: Array = [ questions: [obsGroupMembers[0], obsGroupMembers[1]], validators: [], }, + ...obsGroupMembers, ]; @@ -138,6 +140,50 @@ const location = { const encounter = testEncounter as OpenmrsEncounter; +const calculateExpressionFields: Array = [ + { + label: 'Height (cm)', + type: 'obs', + required: false, + id: 'height', + questionOptions: { + rendering: 'number', + concept: '5090AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + answers: [], + }, + }, + { + label: 'Weight (Kgs)', + type: 'obs', + required: false, + id: 'weight', + questionOptions: { + rendering: 'number', + concept: '5089AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + answers: [], + }, + validators: [], + }, + { + label: 'BMI:Kg/M2 (Function calcBMI | useFieldValue)', + type: 'obs', + required: false, + id: 'bmi', + questionOptions: { + rendering: 'number', + defaultValue: 0, + concept: '1342AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + isTransient: true, + disallowDecimals: false, + calculate: { + calculateExpression: "calcBMI(useFieldValue('height'),useFieldValue('weight'))", + }, + }, + validators: [], + questionInfo: 'this calculates BMI using calcBMI function and useFieldValue of weight and height', + }, +]; + jest.mock('../utils/expression-runner', () => { const originalModule = jest.requireActual('../utils/expression-runner'); return { @@ -171,7 +217,7 @@ describe('useInitialValues', () => { setEncounterProvider: jest.fn, setEncounterLocation: jest.fn, encounterRole: '', - setEncounterRole: jest.fn + setEncounterRole: jest.fn, }, formFieldHandlers, ), @@ -210,7 +256,7 @@ describe('useInitialValues', () => { setEncounterProvider: jest.fn, setEncounterLocation: jest.fn, encounterRole: '', - setEncounterRole: jest.fn + setEncounterRole: jest.fn, }, formFieldHandlers, ), @@ -273,7 +319,7 @@ describe('useInitialValues', () => { setEncounterProvider: jest.fn, setEncounterLocation: jest.fn, encounterRole: '', - setEncounterRole: jest.fn + setEncounterRole: jest.fn, }, formFieldHandlers, ), @@ -314,7 +360,7 @@ describe('useInitialValues', () => { setEncounterProvider: jest.fn, setEncounterLocation: jest.fn, encounterRole: '8cb3a399-d18b-4b62-aefb-5a0f948a3809', - setEncounterRole: jest.fn + setEncounterRole: jest.fn, }, formFieldHandlers, ), @@ -333,4 +379,63 @@ describe('useInitialValues', () => { expect(allFormFields.find((field) => field.id === 'testOrder_1')).not.toBeNull(); expect(allFormFields.find((field) => field.id === 'testOrder_2')).not.toBeNull(); }); + + it('should return calculated values for calculated fields in "edit" mode', async () => { + let hook = null; + const allFields = JSON.parse(JSON.stringify(calculateExpressionFields)); + const allFieldsKeys = allFields.map((f) => f.id); + let valuesMap = { + height: '', + wight: '', + bmi: '', + }; + + const helper = new CommonExpressionHelpers( + { value: allFields[1], type: 'field' }, + {}, + allFields, + valuesMap, + allFieldsKeys, + ); + await act(async () => { + hook = renderHook(() => + useInitialValues( + [...allFormFields], + encounter, + false, + { + encounter: encounter, + patient: testPatient, + location, + sessionMode: 'enter', + encounterDate: encounterDate, + setEncounterDate: jest.fn, + encounterProvider: '2c95f6f5-788e-4e73-9079-5626911231fa', + setEncounterProvider: jest.fn, + setEncounterLocation: jest.fn, + encounterRole: '', + setEncounterRole: jest.fn, + }, + formFieldHandlers, + ), + ); + }); + const { + current: { initialValues, isBindingComplete }, + } = hook.result; + expect(isBindingComplete).toBe(true); + + const weightQuestionConcept = calculateExpressionFields.find((field) => field.id === 'weight').questionOptions + .concept; + const heightQuestionConcept = calculateExpressionFields.find((field) => field.id === 'height').questionOptions + .concept; + + const heightVal = encounter.obs.find((obs) => obs.concept.uuid === heightQuestionConcept).value; + const weightVal = encounter.obs.find((obs) => obs.concept.uuid === weightQuestionConcept).value; + const calculatedBmi = helper.calcBMI(heightVal, weightVal); + + expect(heightVal).not.toBeNull(); + expect(weightVal).not.toBeNull(); + expect(calculatedBmi).not.toBeNull(); + }); }); diff --git a/src/hooks/useInitialValues.ts b/src/hooks/useInitialValues.ts index 3154c54f3..686845347 100644 --- a/src/hooks/useInitialValues.ts +++ b/src/hooks/useInitialValues.ts @@ -23,7 +23,7 @@ export function useInitialValues( 'encounterDatetime', 'encounterLocation', 'patientIdentifier', - 'encounterRole' + 'encounterRole', ]; useEffect(() => { @@ -64,6 +64,8 @@ export function useInitialValues( return; } if (encounter) { + const tempAsyncValues = {}; + formFields .filter((field) => isEmpty(field.meta?.previousValue)) .filter((field) => field.questionOptions.rendering !== 'file') @@ -77,10 +79,27 @@ export function useInitialValues( formFields, encounterContext, ); + + if (field.questionOptions.calculate?.calculateExpression) { + tempAsyncValues[field.id] = evaluateAsyncExpression( + field.questionOptions.calculate.calculateExpression, + { value: field, type: 'field' }, + formFields, + initialValues, + { + mode: encounterContext.sessionMode, + patient: encounterContext.patient, + }, + ); + } if (field.type === 'obsGroup') { return; } - if (isEmpty(existingVal) && !isEmpty(field.questionOptions.defaultValue)) { + if ( + isEmpty(existingVal) && + !isEmpty(field.questionOptions.defaultValue) && + !field.questionOptions.calculate?.calculateExpression + ) { existingVal = inferInitialValueFromDefaultFieldValue( field, encounterContext, @@ -100,8 +119,7 @@ export function useInitialValues( ); formFields.push(...flattenedFields); setIsEncounterBindingComplete(true); - // TODO: Address behaviour in edit mode; see: https://issues.openmrs.org/browse/O3-2252 - setAsyncInitValues({}); + setAsyncInitValues({ ...(asyncInitValues ?? {}), ...tempAsyncValues }); } else { const tempAsyncValues = {}; formFields