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));
+ }}
/>