diff --git a/src/components/Tool/Appeal/InitialPage/AddAppealForm/AddAppealForm.test.tsx b/src/components/Tool/Appeal/InitialPage/AddAppealForm/AddAppealForm.test.tsx index 8a134ca4b..b317b2137 100644 --- a/src/components/Tool/Appeal/InitialPage/AddAppealForm/AddAppealForm.test.tsx +++ b/src/components/Tool/Appeal/InitialPage/AddAppealForm/AddAppealForm.test.tsx @@ -139,6 +139,7 @@ describe('AddAppealForm', () => { userEvent.clear(initialGoal); userEvent.clear(letterCost); userEvent.clear(adminPercent); + userEvent.tab(); expect(goalAmount).toHaveValue(0); @@ -153,6 +154,8 @@ describe('AddAppealForm', () => { userEvent.type(letterCost, '-5'); userEvent.clear(adminPercent); userEvent.type(adminPercent, '-5'); + userEvent.tab(); + expect( await findByText(/must use a positive whole number for initial goal/i), ).toBeInTheDocument(); @@ -169,6 +172,8 @@ describe('AddAppealForm', () => { userEvent.type(letterCost, '0'); userEvent.clear(adminPercent); userEvent.type(adminPercent, '50'); + userEvent.tab(); + await waitFor(() => { expect( queryByText(/initial goal is required/i), @@ -192,6 +197,8 @@ describe('AddAppealForm', () => { userEvent.type(letterCost, '0.1'); userEvent.clear(adminPercent); userEvent.type(adminPercent, '5.1'); + userEvent.tab(); + expect( await findByText(/must use a positive whole number for initial goal/i), ).toBeInTheDocument(); @@ -201,7 +208,7 @@ describe('AddAppealForm', () => { expect( await findByText(/must use a positive whole number for admin cost/i), ).toBeInTheDocument(); - }); + }, 6000); it('should calculate the Goal amount correctly', async () => { const { getByRole } = render(); @@ -222,6 +229,28 @@ describe('AddAppealForm', () => { expect(goalAmount).toHaveValue(3333.33); }); + + it('should allow the user to manually enter the Goal amount', async () => { + const { getByRole } = render(); + + const initialGoal = getByRole('spinbutton', { name: 'Initial Goal' }); + const goalAmount = getByRole('spinbutton', { name: 'Goal' }); + + userEvent.clear(initialGoal); + userEvent.type(initialGoal, '2500'); + + expect(goalAmount).toHaveValue(2840.91); + + userEvent.clear(goalAmount); + userEvent.type(goalAmount, '3000'); + + expect(goalAmount).toHaveValue(3000); + + userEvent.clear(initialGoal); + userEvent.type(initialGoal, '250'); + + expect(goalAmount).toHaveValue(284.09); + }); }); describe('Select all buttons', () => { diff --git a/src/components/Tool/Appeal/InitialPage/AddAppealForm/AddAppealForm.tsx b/src/components/Tool/Appeal/InitialPage/AddAppealForm/AddAppealForm.tsx index 33c1cf386..520b01b47 100644 --- a/src/components/Tool/Appeal/InitialPage/AddAppealForm/AddAppealForm.tsx +++ b/src/components/Tool/Appeal/InitialPage/AddAppealForm/AddAppealForm.tsx @@ -71,13 +71,16 @@ export const contactExclusions: ContactExclusion[] = [ ]; export const calculateGoal = ( - initialGoal: number, - letterCost: number, - adminPercentage: number, + initialGoal: number | string, + letterCost: number | string, + adminPercentage: number | string, ): number => { - const adminPercent = 1 - adminPercentage / 100; + const adminPercent = 1 - Number(adminPercentage) / 100; - return Math.round(((initialGoal + letterCost) / adminPercent) * 100) / 100; + const totalGoal = (Number(initialGoal) + Number(letterCost)) / adminPercent; + + // Round to two decimal places + return Math.round(totalGoal * 100) / 100; }; const gqlStatusesToDBStatusMap: { [key: string]: string } = { @@ -200,6 +203,15 @@ const appealFormSchema = yup.object({ i18n.t('Must use a positive whole number for Admin Cost'), isPositiveInteger, ), + goal: yup + .number() + .typeError(i18n.t('Goal must be a valid number')) + .required(i18n.t('Goal is required')) + .test( + i18n.t('Is positive?'), + i18n.t('Must use a positive number for Goal'), + (value) => parseFloat(value as unknown as string) >= 0, + ), statuses: yup.array().of( yup.object({ name: yup.string(), @@ -222,6 +234,7 @@ type FormikRefType = React.RefObject< initialGoal: number; letterCost: number; adminPercentage: number; + goal: number; statuses: Pick[]; tags: never[]; exclusions: ContactExclusion[]; @@ -309,11 +322,7 @@ const AddAppealForm: React.FC = ({ const onSubmit = async (props: Attributes) => { const attributes: AppealCreateInput = { name: props.name, - amount: calculateGoal( - props.initialGoal, - props.letterCost, - props.adminPercentage, - ), + amount: props.goal, }; const inclusionFilter = buildInclusionFilter({ @@ -368,6 +377,7 @@ const AddAppealForm: React.FC = ({ initialGoal: appealGoal ?? 0, letterCost: 0, adminPercentage: 12, + goal: 0, statuses: appealStatuses ?? [ { name: '-- All Active --', @@ -392,6 +402,7 @@ const AddAppealForm: React.FC = ({ initialGoal, letterCost, adminPercentage, + goal, statuses, tags, exclusions, @@ -438,6 +449,17 @@ const AddAppealForm: React.FC = ({ error={errors.initialGoal} helperText={errors.initialGoal} as={TextField} + onChange={(event) => { + setFieldValue('initialGoal', event.target.value); + setFieldValue( + 'goal', + calculateGoal( + event.target.value, + letterCost, + adminPercentage, + ), + ); + }} /> @@ -469,6 +491,17 @@ const AddAppealForm: React.FC = ({ error={errors.letterCost} helperText={errors.letterCost} as={TextField} + onChange={(event) => { + setFieldValue('letterCost', event.target.value); + setFieldValue( + 'goal', + calculateGoal( + initialGoal, + event.target.value, + adminPercentage, + ), + ); + }} /> @@ -500,6 +533,17 @@ const AddAppealForm: React.FC = ({ error={errors.adminPercentage} helperText={errors.adminPercentage} as={TextField} + onChange={(event) => { + setFieldValue('adminPercentage', event.target.value); + setFieldValue( + 'goal', + calculateGoal( + initialGoal, + letterCost, + event.target.value, + ), + ); + }} /> @@ -529,11 +573,10 @@ const AddAppealForm: React.FC = ({ variant="outlined" size="small" className={classes.input} - value={calculateGoal( - initialGoal, - letterCost, - adminPercentage, - ).toFixed(2)} + value={goal} + onChange={(event) => { + setFieldValue('goal', Number(event.target.value)); + }} />