diff --git a/packages/esm-patient-orders-app/src/lab-results/lab-results-form.test.tsx b/packages/esm-patient-orders-app/src/lab-results/lab-results-form.test.tsx index c2c827e11a..5f9cafbaa0 100644 --- a/packages/esm-patient-orders-app/src/lab-results/lab-results-form.test.tsx +++ b/packages/esm-patient-orders-app/src/lab-results/lab-results-form.test.tsx @@ -59,6 +59,7 @@ describe('LabResultsForm', () => { lowCritical: 40, lowNormal: 50, units: 'mg/dL', + allowDecimal: false, } as LabOrderConcept, isLoading: false, error: null, @@ -96,6 +97,58 @@ describe('LabResultsForm', () => { }); }); + test('validate when we have a concept with allowDecimal set to true', async () => { + const user = userEvent.setup(); + // if allowDecimal is true, we should allow decimal values + mockUseOrderConceptByUuid.mockReturnValue({ + concept: { + uuid: 'concept-uuid', + display: 'Test Concept', + setMembers: [], + datatype: { display: 'Numeric', hl7Abbreviation: 'NM' }, + hiAbsolute: 100, + lowAbsolute: null, + lowCritical: null, + lowNormal: null, + hiCritical: null, + hiNormal: null, + units: 'mg/dL', + allowDecimal: true, + } as LabOrderConcept, + isLoading: false, + error: null, + isValidating: false, + mutate: jest.fn(), + }); + render(); + + const input = await screen.findByLabelText(`Test Concept (0 - 100 mg/dL)`); + await user.type(input, '50.5'); + + const saveButton = screen.getByRole('button', { name: /Save and close/i }); + await user.click(saveButton); + + await waitFor(() => { + expect(screen.queryByText('Test Concept must be a whole number')).not.toBeInTheDocument(); + }); + }); + + test('validate when we have a concept with allowDecimal set to null', async () => { + const user = userEvent.setup(); + render(); + + const input = await screen.findByLabelText(`Test Concept (0 - 100 mg/dL)`); + await user.type(input, '50.5'); + + const saveButton = screen.getByRole('button', { name: /Save and close/i }); + await user.click(saveButton); + + // if allowDecimal is null or false, we should not allow decimal values + await waitFor(() => { + expect(screen.getByText('Test Concept must be a whole number')).toBeInTheDocument(); + }); + }); + test('validate numeric input with negative value', async () => { const user = userEvent.setup(); render(); diff --git a/packages/esm-patient-orders-app/src/lab-results/lab-results.resource.ts b/packages/esm-patient-orders-app/src/lab-results/lab-results.resource.ts index f42209bc1b..569dc1ae6b 100644 --- a/packages/esm-patient-orders-app/src/lab-results/lab-results.resource.ts +++ b/packages/esm-patient-orders-app/src/lab-results/lab-results.resource.ts @@ -10,8 +10,8 @@ const labEncounterRepresentation = 'obs:(uuid,obsDatetime,voided,groupMembers,formFieldNamespace,formFieldPath,order:(uuid,display),concept:(uuid,name:(uuid,name)),' + 'value:(uuid,display,name:(uuid,name),names:(uuid,conceptNameType,name))))'; const labConceptRepresentation = - 'custom:(uuid,display,name,datatype,set,answers,hiNormal,hiAbsolute,hiCritical,lowNormal,lowAbsolute,lowCritical,units,' + - 'setMembers:(uuid,display,answers,datatype,hiNormal,hiAbsolute,hiCritical,lowNormal,lowAbsolute,lowCritical,units))'; + 'custom:(uuid,display,name,datatype,set,answers,hiNormal,hiAbsolute,hiCritical,lowNormal,lowAbsolute,lowCritical,units,allowDecimal,' + + 'setMembers:(uuid,display,answers,datatype,hiNormal,hiAbsolute,hiCritical,lowNormal,lowAbsolute,lowCritical,units,allowDecimal))'; const conceptObsRepresentation = 'custom:(uuid,display,concept:(uuid,display),groupMembers,value)'; type NullableNumber = number | null | undefined; @@ -33,6 +33,7 @@ export interface LabOrderConcept { lowNormal?: NullableNumber; lowAbsolute?: NullableNumber; lowCritical?: NullableNumber; + allowDecimal?: boolean | null; units?: string; } @@ -126,26 +127,26 @@ export async function updateOrderResult( orderPayload: OrderDiscontinuationPayload, abortController: AbortController, ) { - const updateOrderCall = await openmrsFetch(`${restBaseUrl}/order`, { + const saveEncounter = await openmrsFetch(`${restBaseUrl}/encounter/${encounterUuid}`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, signal: abortController.signal, - body: orderPayload, + body: obsPayload, }); - if (updateOrderCall.status === 201) { - const saveEncounter = await openmrsFetch(`${restBaseUrl}/encounter/${encounterUuid}`, { + if (saveEncounter.ok) { + const updateOrderCall = await openmrsFetch(`${restBaseUrl}/order`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, signal: abortController.signal, - body: obsPayload, + body: orderPayload, }); - if (saveEncounter.ok) { + if (updateOrderCall.status === 201) { const fulfillOrder = await openmrsFetch(`${restBaseUrl}/order/${orderUuid}/fulfillerdetails/`, { method: 'POST', headers: { diff --git a/packages/esm-patient-orders-app/src/lab-results/useLabResultsFormSchema.tsx b/packages/esm-patient-orders-app/src/lab-results/useLabResultsFormSchema.tsx index 2a3ee3e4f0..8982e90eaf 100644 --- a/packages/esm-patient-orders-app/src/lab-results/useLabResultsFormSchema.tsx +++ b/packages/esm-patient-orders-app/src/lab-results/useLabResultsFormSchema.tsx @@ -105,16 +105,33 @@ const createNumericSchema = ( upperLimit: number | null | undefined, lowerLimit: number | null | undefined, ): z.ZodType => { + const { allowDecimal, display } = labOrderConcept; + const processNumber = (val: unknown) => { + if (val === '' || val === null || val === undefined) return undefined; + const parsed = Number(val); + if (isNaN(parsed)) return undefined; + return parsed; + }; + let baseSchema = z - .preprocess((val) => { - if (val === '' || val === null || val === undefined) return undefined; - const parsed = Number(val); - return isNaN(parsed) ? undefined : parsed; - }, z.number().optional()) + .preprocess(processNumber, z.number().optional()) .refine((val) => val === undefined || !isNaN(val), { - message: `${labOrderConcept.display} must be a valid number`, - }); - + message: `${display} must be a valid number`, + }) + .refine( + (val) => { + if (val === undefined) return true; + if (!allowDecimal) { + return Number.isInteger(val); + } + return true; + }, + { + message: !allowDecimal ? `${display} must be a whole number` : `${display} must be a valid number`, + }, + ); + + // Add range validations const hasLowerLimit = lowerLimit !== null && lowerLimit !== undefined; const hasUpperLimit = upperLimit !== null && upperLimit !== undefined; @@ -124,19 +141,19 @@ const createNumericSchema = ( if (hasLowerLimit && hasUpperLimit) { return baseSchema.refine((val) => val === undefined || (val >= lowerLimit && val <= upperLimit), { - message: `${labOrderConcept.display} must be between ${lowerLimit} and ${upperLimit}`, + message: `${display} must be between ${lowerLimit} and ${upperLimit}`, }); } if (hasLowerLimit) { return baseSchema.refine((val) => val === undefined || val >= lowerLimit, { - message: `${labOrderConcept.display} must be greater than or equal to ${lowerLimit}`, + message: `${display} must be greater than or equal to ${lowerLimit}`, }); } if (hasUpperLimit) { return baseSchema.refine((val) => val === undefined || val <= upperLimit, { - message: `${labOrderConcept.display} must be less than or equal to ${upperLimit}`, + message: `${display} must be less than or equal to ${upperLimit}`, }); } };