diff --git a/src/components/Reports/GoalCalculator/CalculatorSettings/Categories/OneTimeGoalsCategory/OneTimeGoalsCategory.tsx b/src/components/Reports/GoalCalculator/CalculatorSettings/Categories/OneTimeGoalsCategory/OneTimeGoalsCategory.tsx deleted file mode 100644 index f91274c4de..0000000000 --- a/src/components/Reports/GoalCalculator/CalculatorSettings/Categories/OneTimeGoalsCategory/OneTimeGoalsCategory.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import React from 'react'; -import { Form, Formik } from 'formik'; -import { useTranslation } from 'react-i18next'; -import * as yup from 'yup'; -import { useGoalCalculator } from '../../../Shared/GoalCalculatorContext'; -import { StyledSectionTitle } from '../../../SharedComponents/styledComponents/StyledSectionTitle'; -import { OneTimeGoalsCategoryForm } from './OneTimeGoalsCategoryForm/OneTimeGoalsCategoryForm'; - -interface OneTimeGoalsCategoryProps {} - -interface OneTimeGoalsFormValues { - // One-time goals fields - additionalGoals: Array<{ - label: string; - amount: number; - }>; -} - -export const OneTimeGoalsCategory: React.FC = () => { - const { handleContinue } = useGoalCalculator(); - const initialValues: OneTimeGoalsFormValues = { - additionalGoals: [], - }; - const { t } = useTranslation(); - - const validationSchema = yup.object({ - additionalGoals: yup.array().of( - yup.object({ - label: yup - .string() - .min(2, t('Label must be at least 2 characters')) - .required(t('Label is required')), - amount: yup - .number() - .min(0, t('Amount must be positive')) - .required(t('Amount is required')), - }), - ), - }); - - const handleSubmit = () => { - // Handle form submission here - // TODO: Implement form submission logic - handleContinue(); - }; - - return ( - <> - - {t('What are your one-time financial goals?')} - - - -
- - -
- - ); -}; diff --git a/src/components/Reports/GoalCalculator/CalculatorSettings/Categories/OneTimeGoalsCategory/OneTimeGoalsCategoryForm/OneTimeGoalsCategoryForm.test.tsx b/src/components/Reports/GoalCalculator/CalculatorSettings/Categories/OneTimeGoalsCategory/OneTimeGoalsCategoryForm/OneTimeGoalsCategoryForm.test.tsx deleted file mode 100644 index 9193cb5a15..0000000000 --- a/src/components/Reports/GoalCalculator/CalculatorSettings/Categories/OneTimeGoalsCategory/OneTimeGoalsCategoryForm/OneTimeGoalsCategoryForm.test.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import React from 'react'; -import { fireEvent, render } from '@testing-library/react'; -import { Formik } from 'formik'; -import TestWrapper from '__tests__/util/TestWrapper'; -import { OneTimeGoalsCategoryForm } from './OneTimeGoalsCategoryForm'; - -const defaultValues = { - additionalGoals: [ - { label: 'Emergency Fund', amount: 5000 }, - { label: 'Equipment', amount: 2000 }, - ], -}; - -const renderWithFormik = (initialValues = defaultValues) => { - return render( - - {}}> - - - , - ); -}; - -describe('OneTimeGoalsCategoryForm', () => { - it('renders existing goals', () => { - const { getByDisplayValue } = renderWithFormik(); - - expect(getByDisplayValue('Emergency Fund')).toBeInTheDocument(); - expect(getByDisplayValue('5000')).toBeInTheDocument(); - expect(getByDisplayValue('Equipment')).toBeInTheDocument(); - expect(getByDisplayValue('2000')).toBeInTheDocument(); - }); - - it('renders add goal button', () => { - const { getByRole } = renderWithFormik(); - - expect(getByRole('button', { name: '+ Add Goal' })).toBeInTheDocument(); - }); - - it('adds a new goal when add button is clicked', () => { - const { getByRole, getAllByLabelText } = renderWithFormik(); - - const addButton = getByRole('button', { name: '+ Add Goal' }); - fireEvent.click(addButton); - - expect(getAllByLabelText('Goal Label')).toHaveLength(3); - expect(getAllByLabelText('Amount')).toHaveLength(3); - }); - - it('removes a goal when delete button is clicked', () => { - const { getAllByLabelText } = renderWithFormik(); - - expect(getAllByLabelText('Goal Label')).toHaveLength(2); - expect(getAllByLabelText('Delete goal')).toHaveLength(2); - - const deleteButtons = getAllByLabelText('Delete goal'); - fireEvent.click(deleteButtons[0]); - - expect(getAllByLabelText('Goal Label')).toHaveLength(1); - expect(getAllByLabelText('Delete goal')).toHaveLength(1); - }); - - it('updates goal label when input changes', () => { - const { getByDisplayValue } = renderWithFormik(); - - const labelInput = getByDisplayValue('Emergency Fund'); - fireEvent.change(labelInput, { target: { value: 'Updated Fund' } }); - - expect(getByDisplayValue('Updated Fund')).toBeInTheDocument(); - }); - - it('updates goal amount when input changes', () => { - const { getByDisplayValue } = renderWithFormik(); - - const amountInput = getByDisplayValue('5000'); - fireEvent.change(amountInput, { target: { value: '7500' } }); - - expect(getByDisplayValue('7500')).toBeInTheDocument(); - }); - - it('renders with empty goals list', () => { - const { getByRole, queryAllByLabelText } = renderWithFormik({ - additionalGoals: [], - }); - - expect(queryAllByLabelText('Goal Label')).toHaveLength(0); - expect(queryAllByLabelText('Amount')).toHaveLength(0); - - expect(getByRole('button', { name: '+ Add Goal' })).toBeInTheDocument(); - }); - - it('renders currency adornment for amount fields', () => { - const { getAllByLabelText } = renderWithFormik(); - - const amountFields = getAllByLabelText('Amount'); - amountFields.forEach((field) => { - expect(field.closest('.MuiInputBase-root')).toBeInTheDocument(); - }); - }); - - it('sets correct input attributes for amount fields', () => { - const { getAllByLabelText } = renderWithFormik(); - - const amountFields = getAllByLabelText('Amount'); - amountFields.forEach((field) => { - expect(field).toHaveAttribute('type', 'number'); - expect(field).toHaveAttribute('min', '0'); - expect(field).toHaveAttribute('step', '0.01'); - }); - }); -}); diff --git a/src/components/Reports/GoalCalculator/CalculatorSettings/Categories/OneTimeGoalsCategory/OneTimeGoalsCategoryForm/OneTimeGoalsCategoryForm.tsx b/src/components/Reports/GoalCalculator/CalculatorSettings/Categories/OneTimeGoalsCategory/OneTimeGoalsCategoryForm/OneTimeGoalsCategoryForm.tsx deleted file mode 100644 index 157e1cec92..0000000000 --- a/src/components/Reports/GoalCalculator/CalculatorSettings/Categories/OneTimeGoalsCategory/OneTimeGoalsCategoryForm/OneTimeGoalsCategoryForm.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import React, { Fragment } from 'react'; -import DeleteIcon from '@mui/icons-material/Delete'; -import { Box, Button, Grid, IconButton, TextField } from '@mui/material'; -import { styled } from '@mui/material/styles'; -import { useFormikContext } from 'formik'; -import { useTranslation } from 'react-i18next'; -import { CurrencyAdornment } from '../../../../Shared/Adornments'; - -const StyledAddGoalButton = styled(Button)(({ theme }) => ({ - marginTop: theme.spacing(1), - borderStyle: 'dashed', - borderColor: theme.palette.primary.main, - color: theme.palette.primary.main, - '&:hover': { - borderStyle: 'dashed', - borderColor: theme.palette.primary.dark, - backgroundColor: theme.palette.primary.light, - opacity: 0.3, - }, -})); - -const StyledIconButton = styled(IconButton)(({ theme }) => ({ - color: 'black', - padding: theme.spacing(0.5), -})); - -interface OneTimeGoalsFormValues { - // One-time goals fields - additionalGoals: Array<{ - label: string; - amount: number; - }>; -} - -export const OneTimeGoalsCategoryForm: React.FC = () => { - const { t } = useTranslation(); - const { values, setFieldValue } = useFormikContext(); - - const addGoalField = () => { - const newGoal = { label: '', amount: 0 }; - const updatedGoals = [...values.additionalGoals, newGoal]; - setFieldValue('additionalGoals', updatedGoals); - }; - - const removeGoalField = (index: number) => { - const updatedGoals = values.additionalGoals.filter((_, i) => i !== index); - setFieldValue('additionalGoals', updatedGoals); - }; - - return ( - - - {/* Dynamic Additional Goals Fields */} - {values.additionalGoals.map((goal, index) => ( - - - - setFieldValue( - `additionalGoals.${index}.label`, - e.target.value, - ) - } - variant="outlined" - placeholder={t('e.g., Emergency Fund, Equipment')} - /> - - - - setFieldValue( - `additionalGoals.${index}.amount`, - parseFloat(e.target.value) || 0, - ) - } - variant="outlined" - inputProps={{ min: 0, step: 0.01 }} - InputProps={{ - startAdornment: , - endAdornment: ( - removeGoalField(index)} - size="small" - aria-label={t('Delete goal')} - > - - - ), - }} - /> - - - ))} - - {/* Add Goal Button */} - - - {t('+ Add Goal')} - - - - - ); -}; diff --git a/src/components/Reports/GoalCalculator/CalculatorSettings/Categories/OneTimeGoalsCategory/OneTimeGoalsHelperPanel/OneTimeGoalsHelperPanel.tsx b/src/components/Reports/GoalCalculator/CalculatorSettings/Categories/OneTimeGoalsCategory/OneTimeGoalsHelperPanel/OneTimeGoalsHelperPanel.tsx deleted file mode 100644 index ac90938acd..0000000000 --- a/src/components/Reports/GoalCalculator/CalculatorSettings/Categories/OneTimeGoalsCategory/OneTimeGoalsHelperPanel/OneTimeGoalsHelperPanel.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react'; -import { Box, Typography } from '@mui/material'; -import { styled } from '@mui/system'; -import { useTranslation } from 'react-i18next'; - -const StyledHelperPanelBox = styled(Box)({ - padding: '16px', -}); - -export const OneTimeGoalsHelperPanel = () => { - const { t } = useTranslation(); - return ( - - - {t( - 'To set a one-time savings goal, click "Add One-time Goal" to create a new row in the goal table. In the left cell of the new row, enter the name of your goal—for example, "Car." Then, in the right cell, enter the full dollar amount you need to raise, such as $15,000.', - )} - - - ); -}; diff --git a/src/components/Reports/GoalCalculator/CalculatorSettings/Categories/SpecialIncomeCategory/SpecialIncomeCategory.tsx b/src/components/Reports/GoalCalculator/CalculatorSettings/Categories/SpecialIncomeCategory/SpecialIncomeCategory.tsx deleted file mode 100644 index 01c50040ea..0000000000 --- a/src/components/Reports/GoalCalculator/CalculatorSettings/Categories/SpecialIncomeCategory/SpecialIncomeCategory.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import React from 'react'; -import { Form, Formik } from 'formik'; -import { useTranslation } from 'react-i18next'; -import * as yup from 'yup'; -import { useGoalCalculator } from '../../../Shared/GoalCalculatorContext'; -import { StyledSectionTitle } from '../../../SharedComponents/styledComponents/StyledSectionTitle'; -import { SpecialIncomeCategoryForm } from './SpecialIncomeCategoryForm/SpecialIncomeCategoryForm'; - -interface SpecialIncomeCategoryProps {} - -interface SpecialIncomeFormValues { - // Special income fields - incidentalIncome: number; - propertyIncome: number; - spouseIncome: number; - additionalIncomes: Array<{ - label: string; - amount: number; - }>; -} - -export const SpecialIncomeCategory: React.FC< - SpecialIncomeCategoryProps -> = () => { - const { handleContinue } = useGoalCalculator(); - const { t } = useTranslation(); - const initialValues: SpecialIncomeFormValues = { - incidentalIncome: 0, - propertyIncome: 0, - spouseIncome: 0, - additionalIncomes: [], - }; - - const validationSchema = yup.object({ - incidentalIncome: yup - .number() - .min(0, t('Incident income must be positive')) - .required(t('Incident income is required')), - propertyIncome: yup - .number() - .min(0, t('Property income must be positive')) - .required(t('Property income is required')), - spouseIncome: yup.number().min(0, t('Spouse income must be positive')), - additionalIncomes: yup.array().of( - yup.object({ - label: yup - .string() - .min(2, t('Label must be at least 2 characters')) - .required(t('Label is required')), - amount: yup - .number() - .min(0, t('Amount must be positive')) - .required(t('Amount is required')), - }), - ), - }); - - const handleSubmit = () => { - // Handle form submission here - // TODO: Implement form submission logic - handleContinue(); - }; - - return ( - <> - - {t( - 'Do you have any additional sources of income? If you have income from outside sources (other than Cru) that you use as part of your budget, please include it below. Please enter the NET amounts used in your monthly budget.', - )} - - - -
- - -
- - ); -}; diff --git a/src/components/Reports/GoalCalculator/CalculatorSettings/Categories/SpecialIncomeCategory/SpecialIncomeCategoryForm/SpecialIncomeCategoryForm.tsx b/src/components/Reports/GoalCalculator/CalculatorSettings/Categories/SpecialIncomeCategory/SpecialIncomeCategoryForm/SpecialIncomeCategoryForm.tsx deleted file mode 100644 index 3896380ef7..0000000000 --- a/src/components/Reports/GoalCalculator/CalculatorSettings/Categories/SpecialIncomeCategory/SpecialIncomeCategoryForm/SpecialIncomeCategoryForm.tsx +++ /dev/null @@ -1,207 +0,0 @@ -import React from 'react'; -import DeleteIcon from '@mui/icons-material/Delete'; -import InfoIcon from '@mui/icons-material/Info'; -import { - Box, - Button, - Grid, - IconButton, - TextField, - styled, -} from '@mui/material'; -import { Field, useFormikContext } from 'formik'; -import { useTranslation } from 'react-i18next'; -import { useGoalCalculator } from 'src/components/Reports/GoalCalculator/Shared/GoalCalculatorContext'; -import { CurrencyAdornment } from '../../../../Shared/Adornments'; -import { SpouseIncomeHelperPanel } from '../SpecialInterestHelperPanel/SpouseIncomeHelperPanel'; - -const StyledDeleteIconButton = styled(IconButton)(({ theme }) => ({ - color: 'black', - padding: theme.spacing(0.5), -})); - -const StyledAddIncomeButton = styled(Button)(({ theme }) => ({ - marginTop: theme.spacing(1), - borderStyle: 'dashed', - borderColor: theme.palette.primary.main, - color: theme.palette.primary.main, - '&:hover': { - borderStyle: 'dashed', - borderColor: theme.palette.primary.dark, - backgroundColor: theme.palette.primary.light, - opacity: 0.3, - }, -})); - -interface SpecialIncomeFormValues { - // Special income fields - incidentalIncome: number; - propertyIncome: number; - spouseIncome: number; - additionalIncomes: Array<{ - label: string; - amount: number; - }>; -} - -export const SpecialIncomeCategoryForm: React.FC = () => { - const { t } = useTranslation(); - const { setRightPanelContent } = useGoalCalculator(); - const { values, errors, touched, setFieldValue } = - useFormikContext(); - - const addIncomeField = () => { - const newIncome = { label: '', amount: 0 }; - const updatedIncomes = [...values.additionalIncomes, newIncome]; - setFieldValue('additionalIncomes', updatedIncomes); - }; - - const removeIncomeField = (index: number) => { - const updatedIncomes = values.additionalIncomes.filter( - (_, i) => i !== index, - ); - setFieldValue('additionalIncomes', updatedIncomes); - }; - - return ( - - - - - {({ field }) => ( - , - }} - /> - )} - - - - - - {({ field }) => ( - , - }} - /> - )} - - - - - {({ field }) => ( - , - endAdornment: ( - - setRightPanelContent() - } - > - - - ), - }} - /> - )} - - - - {/* Dynamic Additional Income Fields */} - {values.additionalIncomes.map((income, index) => ( - - - - setFieldValue( - `additionalIncomes.${index}.label`, - e.target.value, - ) - } - variant="outlined" - placeholder={t('e.g., Freelance, Side Business')} - /> - - - - setFieldValue( - `additionalIncomes.${index}.amount`, - parseFloat(e.target.value) || 0, - ) - } - variant="outlined" - inputProps={{ min: 0, step: 0.01 }} - InputProps={{ - startAdornment: , - endAdornment: ( - removeIncomeField(index)} - size="small" - aria-label={t('Delete income')} - > - - - ), - }} - /> - - - ))} - - {/* Add Income Button */} - - - {t('+ Add Income')} - - - - - ); -}; diff --git a/src/components/Reports/GoalCalculator/CalculatorSettings/Categories/SpecialIncomeCategory/SpecialInterestHelperPanel/SpecialInterestHelperPanel.test.tsx b/src/components/Reports/GoalCalculator/CalculatorSettings/Categories/SpecialIncomeCategory/SpecialInterestHelperPanel/SpecialInterestHelperPanel.test.tsx deleted file mode 100644 index 4641453bfb..0000000000 --- a/src/components/Reports/GoalCalculator/CalculatorSettings/Categories/SpecialIncomeCategory/SpecialInterestHelperPanel/SpecialInterestHelperPanel.test.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import React from 'react'; -import { render } from '@testing-library/react'; -import { SpecialInterestHelperPanel } from './SpecialInterestHelperPanel'; - -describe('SpecialInterestHelperPanel', () => { - it('renders the helper panel with correct text', () => { - const { getByText } = render(); - - expect( - getByText( - 'If you have income from outside sources (other than Cru) that you use as part of your budget, please include it below. Use "NET" numbers and not "Gross".', - ), - ).toBeInTheDocument(); - }); -}); diff --git a/src/components/Reports/GoalCalculator/CalculatorSettings/Categories/SpecialIncomeCategory/SpecialInterestHelperPanel/SpecialInterestHelperPanel.tsx b/src/components/Reports/GoalCalculator/CalculatorSettings/Categories/SpecialIncomeCategory/SpecialInterestHelperPanel/SpecialInterestHelperPanel.tsx deleted file mode 100644 index 8fa7f0b13d..0000000000 --- a/src/components/Reports/GoalCalculator/CalculatorSettings/Categories/SpecialIncomeCategory/SpecialInterestHelperPanel/SpecialInterestHelperPanel.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react'; -import { Box, Typography } from '@mui/material'; -import { styled } from '@mui/system'; -import { useTranslation } from 'react-i18next'; - -const StyledHelperPanelBox = styled(Box)({ - padding: '16px', -}); - -export const SpecialInterestHelperPanel = () => { - const { t } = useTranslation(); - return ( - - - {t( - 'If you have income from outside sources (other than Cru) that you use as part of your budget, please include it below. Use "NET" numbers and not "Gross".', - )} - - - ); -}; diff --git a/src/components/Reports/GoalCalculator/CalculatorSettings/Categories/SpecialIncomeCategory/SpecialInterestHelperPanel/SpouseIncomeHelperPanel.test.tsx b/src/components/Reports/GoalCalculator/CalculatorSettings/Categories/SpecialIncomeCategory/SpecialInterestHelperPanel/SpouseIncomeHelperPanel.test.tsx deleted file mode 100644 index 5c8495976e..0000000000 --- a/src/components/Reports/GoalCalculator/CalculatorSettings/Categories/SpecialIncomeCategory/SpecialInterestHelperPanel/SpouseIncomeHelperPanel.test.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import React from 'react'; -import { ThemeProvider } from '@mui/material/styles'; -import { render } from '@testing-library/react'; -import TestWrapper from '__tests__/util/TestWrapper'; -import theme from 'src/theme'; -import { SpouseIncomeHelperPanel } from './SpouseIncomeHelperPanel'; - -describe('SpouseIncomeHelperPanel', () => { - it('renders the helper panel with correct text', () => { - const { getByText } = render( - - - - - , - ); - - expect( - getByText( - "The amount entered here will be reflected in your total MPD goal. To look at your goal without spouse's salary, leave this blank.", - ), - ).toBeInTheDocument(); - }); -}); diff --git a/src/components/Reports/GoalCalculator/CalculatorSettings/Categories/SpecialIncomeCategory/SpecialInterestHelperPanel/SpouseIncomeHelperPanel.tsx b/src/components/Reports/GoalCalculator/CalculatorSettings/Categories/SpecialIncomeCategory/SpecialInterestHelperPanel/SpouseIncomeHelperPanel.tsx deleted file mode 100644 index a475462224..0000000000 --- a/src/components/Reports/GoalCalculator/CalculatorSettings/Categories/SpecialIncomeCategory/SpecialInterestHelperPanel/SpouseIncomeHelperPanel.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import React from 'react'; -import { Box, Typography } from '@mui/material'; -import { styled } from '@mui/system'; -import { useTranslation } from 'react-i18next'; - -const StyledNoticeTypography = styled(Typography)(({ theme }) => ({ - marginTop: theme.spacing(2), - color: theme.palette.error.main, - fontStyle: 'italic', -})); -const StyledHelperPanelBox = styled(Box)({ - padding: '16px', -}); - -export const SpouseIncomeHelperPanel = () => { - const { t } = useTranslation(); - return ( - - - {t( - "The amount entered here will be reflected in your total MPD goal. To look at your goal without spouse's salary, leave this blank.", - )} - - - ); -}; diff --git a/src/components/Reports/GoalCalculator/CalculatorSettings/SettingsStep.tsx b/src/components/Reports/GoalCalculator/CalculatorSettings/SettingsStep.tsx index f4ff60517a..7b403c945b 100644 --- a/src/components/Reports/GoalCalculator/CalculatorSettings/SettingsStep.tsx +++ b/src/components/Reports/GoalCalculator/CalculatorSettings/SettingsStep.tsx @@ -1,13 +1,19 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; +import { useGoalCalculator } from '../Shared/GoalCalculatorContext'; import { GoalCalculatorLayout } from '../Shared/GoalCalculatorLayout'; import { GoalCalculatorSection } from '../Shared/GoalCalculatorSection'; import { GoalCalculatorGrid } from '../SharedComponents/GoalCalculatorGrid/GoalCalculatorGrid'; import { SectionPage } from '../SharedComponents/SectionPage'; +import { InformationCategory } from './Categories/InformationCategory/InformationCategory'; import { SettingsSectionList } from './SettingsSectionList'; export const SettingsStep: React.FC = () => { + const { goalCalculationResult } = useGoalCalculator(); + const { data } = goalCalculationResult; const { t } = useTranslation(); + const specialFamilyPrimaryBudgetCategories = + data?.goalCalculation.specialFamily.primaryBudgetCategories; return ( { title={t('Information')} subtitle={t('Take a moment to verify your information.')} > - - - - - - - + + {specialFamilyPrimaryBudgetCategories?.map((category) => ( + + ))} } /> diff --git a/src/components/Reports/GoalCalculator/GoalCalculatorTestWrapper.tsx b/src/components/Reports/GoalCalculator/GoalCalculatorTestWrapper.tsx index 7c1c88fe70..53c5f3d05e 100644 --- a/src/components/Reports/GoalCalculator/GoalCalculatorTestWrapper.tsx +++ b/src/components/Reports/GoalCalculator/GoalCalculatorTestWrapper.tsx @@ -4,7 +4,10 @@ import { SnackbarProvider } from 'notistack'; import { DeepPartial } from 'ts-essentials'; import TestRouter from '__tests__/util/TestRouter'; import { GqlMockedProvider } from '__tests__/util/graphqlMocking'; -import { PrimaryBudgetCategoryEnum } from 'src/graphql/types.generated'; +import { + PrimaryBudgetCategoryEnum, + SubBudgetCategoryEnum, +} from 'src/graphql/types.generated'; import theme from 'src/theme'; import { GoalCalculationQuery } from './Shared/GoalCalculation.generated'; import { GoalCalculatorProvider } from './Shared/GoalCalculatorContext'; @@ -19,24 +22,56 @@ export const goalCalculationMock = { ministryFamily: { primaryBudgetCategories: [ { + id: 'category-ministry', label: 'Ministry & Medical Mileage', category: PrimaryBudgetCategoryEnum.MinistryAndMedicalMileage, + directInput: 0, + subBudgetCategories: [], }, { + id: 'category-transfers', label: 'Account Transfers', category: PrimaryBudgetCategoryEnum.AccountTransfers, + directInput: 0, + subBudgetCategories: [], + }, + { + id: 'category-1', + label: 'Internet & Mobile', + category: PrimaryBudgetCategoryEnum.Utilities, + directInput: null, // null means Line Item mode, which shows subcategories + subBudgetCategories: [ + { + id: 'sub-1', + label: 'Internet', + amount: 60, + category: SubBudgetCategoryEnum.UtilitiesInternet, + }, + { + id: 'sub-2', + label: 'Phone/Mobile', + amount: 40, + category: SubBudgetCategoryEnum.UtilitiesPhoneMobile, + }, + ], }, ], }, specialFamily: { primaryBudgetCategories: [ { + id: 'category-special', label: 'Special Income', category: PrimaryBudgetCategoryEnum.SpecialIncome, + directInput: 0, + subBudgetCategories: [], }, { + id: 'category-goal', label: 'One Time Goal', category: PrimaryBudgetCategoryEnum.OneTimeGoal, + directInput: 0, + subBudgetCategories: [], }, ], }, diff --git a/src/components/Reports/GoalCalculator/RightPanels/UtilitiesPanel.tsx b/src/components/Reports/GoalCalculator/RightPanels/SubUtilitiesPanel.tsx similarity index 55% rename from src/components/Reports/GoalCalculator/RightPanels/UtilitiesPanel.tsx rename to src/components/Reports/GoalCalculator/RightPanels/SubUtilitiesPanel.tsx index 6c10c9fc72..3bb2215235 100644 --- a/src/components/Reports/GoalCalculator/RightPanels/UtilitiesPanel.tsx +++ b/src/components/Reports/GoalCalculator/RightPanels/SubUtilitiesPanel.tsx @@ -3,15 +3,13 @@ import { Alert } from '@mui/material'; import { useTranslation } from 'react-i18next'; import { RightPanel } from './RightPanel'; -export const UtilitiesPanel: React.FC = () => { +export const SubUtilitiesPanel: React.FC = () => { const { t } = useTranslation(); return ( - + - {t( - 'For mobile phone and internet expenses, only include the portion not reimbursed as a ministry expense.', - )} + {t('Only the portion not reimbursed as ministry expense.')} ); diff --git a/src/components/Reports/GoalCalculator/RightPanels/rightPanels.tsx b/src/components/Reports/GoalCalculator/RightPanels/rightPanels.tsx index 4d54426ec7..6661d625ba 100644 --- a/src/components/Reports/GoalCalculator/RightPanels/rightPanels.tsx +++ b/src/components/Reports/GoalCalculator/RightPanels/rightPanels.tsx @@ -1,18 +1,17 @@ import React from 'react'; -import { PrimaryBudgetCategoryEnum } from 'src/graphql/types.generated'; +import { + PrimaryBudgetCategoryEnum, + SubBudgetCategoryEnum, +} from 'src/graphql/types.generated'; import { MedicalPanel } from './MedicalPanel'; import { MileagePanel } from './MileagePanel'; import { SavingsPanel } from './SavingsPanel'; -import { UtilitiesPanel } from './UtilitiesPanel'; +import { SubUtilitiesPanel } from './SubUtilitiesPanel'; export const getPrimaryCategoryRightPanel = ( category: PrimaryBudgetCategoryEnum, ) => { switch (category) { - case PrimaryBudgetCategoryEnum.Saving: - return ; - case PrimaryBudgetCategoryEnum.Utilities: - return ; case PrimaryBudgetCategoryEnum.Medical: return ; case PrimaryBudgetCategoryEnum.MinistryAndMedicalMileage: @@ -21,3 +20,17 @@ export const getPrimaryCategoryRightPanel = ( return null; } }; + +export const getSubCategoryRightPanel = ( + subCategory: SubBudgetCategoryEnum, +) => { + switch (subCategory) { + case SubBudgetCategoryEnum.UtilitiesInternet: + case SubBudgetCategoryEnum.UtilitiesPhoneMobile: + return ; + case SubBudgetCategoryEnum.SavingEmergencyFund: + return ; + default: + return null; + } +}; diff --git a/src/components/Reports/GoalCalculator/Shared/GoalCalculatorContext.tsx b/src/components/Reports/GoalCalculator/Shared/GoalCalculatorContext.tsx index ca1b70bbb9..d9e1e3947f 100644 --- a/src/components/Reports/GoalCalculator/Shared/GoalCalculatorContext.tsx +++ b/src/components/Reports/GoalCalculator/Shared/GoalCalculatorContext.tsx @@ -1,10 +1,12 @@ import { useRouter } from 'next/router'; import React, { Dispatch, + RefObject, SetStateAction, createContext, useCallback, useMemo, + useRef, useState, } from 'react'; import { useSnackbar } from 'notistack'; @@ -38,6 +40,10 @@ export type GoalCalculatorType = { setDrawerOpen: (open: boolean) => void; goalCalculationResult: ReturnType; + + scrollToSection: (title: string) => void; + registerSection: (title: string, ref: RefObject) => void; + unregisterSection: (title: string) => void; }; const GoalCalculatorContext = createContext(null); @@ -80,6 +86,26 @@ export const GoalCalculatorProvider: React.FC = ({ children }) => { const currentStep = steps[stepIndex]; + const sectionRefs = useRef(new Map>()); + + const scrollToSection = useCallback((title: string) => { + const ref = sectionRefs.current.get(title); + if (ref?.current) { + ref.current.scrollIntoView({ behavior: 'smooth', block: 'start' }); + } + }, []); + + const registerSection = useCallback( + (title: string, ref: RefObject) => { + sectionRefs.current.set(title, ref); + }, + [], + ); + + const unregisterSection = useCallback((title: string) => { + sectionRefs.current.delete(title); + }, []); + const handleStepChange = useCallback( (newStep: GoalCalculatorStepEnum) => { const newIndex = steps.findIndex((step) => step.step === newStep); @@ -127,6 +153,9 @@ export const GoalCalculatorProvider: React.FC = ({ children }) => { selectedReport, setSelectedReport, goalCalculationResult, + scrollToSection, + registerSection, + unregisterSection, }), [ steps, @@ -142,6 +171,9 @@ export const GoalCalculatorProvider: React.FC = ({ children }) => { selectedReport, setSelectedReport, goalCalculationResult, + scrollToSection, + registerSection, + unregisterSection, ], ); diff --git a/src/components/Reports/GoalCalculator/Shared/GoalCalculatorLayout.tsx b/src/components/Reports/GoalCalculator/Shared/GoalCalculatorLayout.tsx index dd0078ec15..39282a6262 100644 --- a/src/components/Reports/GoalCalculator/Shared/GoalCalculatorLayout.tsx +++ b/src/components/Reports/GoalCalculator/Shared/GoalCalculatorLayout.tsx @@ -51,12 +51,13 @@ const StyledDrawer = styled('nav', { easing: theme.transitions.easing.sharp, duration: theme.transitions.duration.enteringScreen, }), - overflow: 'hidden', - borderRight: open ? `1px solid ${theme.palette.cruGrayLight.main}` : 'none', + overflow: 'scroll', + height: `calc(100vh - ${navBarHeight} - ${multiPageHeaderHeight})`, [theme.breakpoints.down('sm')]: { position: 'absolute', top: multiPageHeaderHeight, left: `calc(${iconPanelWidth} + 1px)`, + borderRight: `1px solid ${theme.palette.divider}`, height: '100%', backgroundColor: theme.palette.common.white, zIndex: 270, @@ -142,7 +143,6 @@ export const GoalCalculatorLayout: React.FC = ({ {sectionListPanel} {isDrawerOpen && } - {mainContent} ); diff --git a/src/components/Reports/GoalCalculator/Shared/GoalCalculatorSection.test.tsx b/src/components/Reports/GoalCalculator/Shared/GoalCalculatorSection.test.tsx index a13c113464..343aeb4d59 100644 --- a/src/components/Reports/GoalCalculator/Shared/GoalCalculatorSection.test.tsx +++ b/src/components/Reports/GoalCalculator/Shared/GoalCalculatorSection.test.tsx @@ -74,4 +74,12 @@ describe('GoalCalculatorSection', () => { expect(getByRole('button', { name: 'Print' })).toBeInTheDocument(); }); + + it('renders titleExtra content', () => { + const { getByText } = render( + Extra Title Content} />, + ); + + expect(getByText('Extra Title Content')).toBeInTheDocument(); + }); }); diff --git a/src/components/Reports/GoalCalculator/Shared/GoalCalculatorSection.tsx b/src/components/Reports/GoalCalculator/Shared/GoalCalculatorSection.tsx index fa294c7eae..69bac53575 100644 --- a/src/components/Reports/GoalCalculator/Shared/GoalCalculatorSection.tsx +++ b/src/components/Reports/GoalCalculator/Shared/GoalCalculatorSection.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect, useRef } from 'react'; import InfoIcon from '@mui/icons-material/Info'; import PrintIcon from '@mui/icons-material/Print'; import { Box, Button, IconButton, Stack, Typography } from '@mui/material'; @@ -11,6 +11,7 @@ export interface GoalCalculatorSectionProps { rightPanelContent?: JSX.Element; printable?: boolean; children: React.ReactNode; + titleExtra?: React.ReactNode; } export const GoalCalculatorSection: React.FC = ({ @@ -19,22 +20,33 @@ export const GoalCalculatorSection: React.FC = ({ rightPanelContent, printable = false, children, + titleExtra, }) => { - const { setRightPanelContent } = useGoalCalculator(); + const { setRightPanelContent, registerSection, unregisterSection } = + useGoalCalculator(); const { t } = useTranslation(); + const sectionRef = useRef(null); const handlePrint = () => { window.print(); }; + useEffect(() => { + registerSection(title, sectionRef); + return () => { + unregisterSection(title); + }; + }, [title, registerSection, unregisterSection]); + return ( -
+
- + {title} {rightPanelContent && ( { rightPanelContent && setRightPanelContent(rightPanelContent); @@ -45,6 +57,8 @@ export const GoalCalculatorSection: React.FC = ({ )} + {titleExtra} + {printable && ( - + } + rightPanelContent={ + getPrimaryCategoryRightPanel(category.category) ?? undefined + } + > + {promptText && {t(promptText)}} + {directInput ? ( @@ -260,32 +480,49 @@ const GoalCalculatorGridForm: React.FC = ({ size="small" label={t('Total')} type="number" - value={values.lumpSumAmount} - onChange={(e) => setFieldValue('lumpSumAmount', e.target.value)} + value={lumpSumValue} + onChange={(e) => handleLumpSumChange(e.target.value)} + error={!!directInputError} + helperText={directInputError} sx={{ mb: 2 }} /> ) : ( <> - } > {t('Add Line Item')} - - - - - + + + { + // Don't allow editing the total row or label field when canDelete is false + if (params.id === 'total') { + return false; + } + if (params.field === 'label' && !params.row.canDelete) { + return false; + } + return true; + }} + /> )} - + + {Object.keys(cellErrors).length > 0 && + Object.entries(cellErrors).map(([cellKey, error]) => ( + + {error} + + ))} + ); }; diff --git a/src/components/Reports/GoalCalculator/SharedComponents/SectionList.tsx b/src/components/Reports/GoalCalculator/SharedComponents/SectionList.tsx index 1d198cb10d..35aa81bab3 100644 --- a/src/components/Reports/GoalCalculator/SharedComponents/SectionList.tsx +++ b/src/components/Reports/GoalCalculator/SharedComponents/SectionList.tsx @@ -31,11 +31,13 @@ const CategoryListItemIcon = styled(ListItemIcon)(({ theme }) => ({ interface ListItemContentProps { title: string; complete: boolean; + onClick?: () => void; } const ListItemContent: React.FC = ({ title, complete, + onClick, }) => ( <> = ({ color: complete ? theme.palette.mpdxBlue.main : theme.palette.cruGrayDark.main, + cursor: onClick ? 'pointer' : 'default', })} + onClick={onClick} > {complete ? : } ); @@ -64,11 +70,17 @@ interface SectionListProps { } export const SectionList: React.FC = ({ sections }) => { + const { scrollToSection } = useGoalCalculator(); + return ( {sections.map(({ title, complete }, index) => ( - + scrollToSection(title)} + /> ))} diff --git a/src/lib/apollo/cache.ts b/src/lib/apollo/cache.ts index 20ba095057..75c56e103a 100644 --- a/src/lib/apollo/cache.ts +++ b/src/lib/apollo/cache.ts @@ -66,6 +66,12 @@ export const createCache = () => }, }, }, + PrimaryBudgetCategory: { + fields: { + // Always overwrite the existing sub budget categories with the incoming categories + subBudgetCategories: { merge: false }, + }, + }, // Disable cache normalization for 12 month report contacts because a contact in one currency group should not be // merged a contact with the same id in a different currency group FourteenMonthReportContact: { keyFields: false },