From bce7f490c4e602dee1f864b5cc2acd95eec46598 Mon Sep 17 00:00:00 2001 From: wjames111 Date: Wed, 10 Sep 2025 11:06:12 -0400 Subject: [PATCH 01/17] Removes empty files. --- .../OneTimeGoalsCategory.tsx | 65 ------ .../OneTimeGoalsCategoryForm.test.tsx | 111 ---------- .../OneTimeGoalsCategoryForm.tsx | 118 ---------- .../OneTimeGoalsHelperPanel.tsx | 21 -- .../SpecialIncomeCategory.tsx | 84 ------- .../SpecialIncomeCategoryForm.tsx | 207 ------------------ .../SpecialInterestHelperPanel.test.tsx | 15 -- .../SpecialInterestHelperPanel.tsx | 21 -- .../SpouseIncomeHelperPanel.test.tsx | 24 -- .../SpouseIncomeHelperPanel.tsx | 26 --- 10 files changed, 692 deletions(-) delete mode 100644 src/components/Reports/GoalCalculator/CalculatorSettings/Categories/OneTimeGoalsCategory/OneTimeGoalsCategory.tsx delete mode 100644 src/components/Reports/GoalCalculator/CalculatorSettings/Categories/OneTimeGoalsCategory/OneTimeGoalsCategoryForm/OneTimeGoalsCategoryForm.test.tsx delete mode 100644 src/components/Reports/GoalCalculator/CalculatorSettings/Categories/OneTimeGoalsCategory/OneTimeGoalsCategoryForm/OneTimeGoalsCategoryForm.tsx delete mode 100644 src/components/Reports/GoalCalculator/CalculatorSettings/Categories/OneTimeGoalsCategory/OneTimeGoalsHelperPanel/OneTimeGoalsHelperPanel.tsx delete mode 100644 src/components/Reports/GoalCalculator/CalculatorSettings/Categories/SpecialIncomeCategory/SpecialIncomeCategory.tsx delete mode 100644 src/components/Reports/GoalCalculator/CalculatorSettings/Categories/SpecialIncomeCategory/SpecialIncomeCategoryForm/SpecialIncomeCategoryForm.tsx delete mode 100644 src/components/Reports/GoalCalculator/CalculatorSettings/Categories/SpecialIncomeCategory/SpecialInterestHelperPanel/SpecialInterestHelperPanel.test.tsx delete mode 100644 src/components/Reports/GoalCalculator/CalculatorSettings/Categories/SpecialIncomeCategory/SpecialInterestHelperPanel/SpecialInterestHelperPanel.tsx delete mode 100644 src/components/Reports/GoalCalculator/CalculatorSettings/Categories/SpecialIncomeCategory/SpecialInterestHelperPanel/SpouseIncomeHelperPanel.test.tsx delete mode 100644 src/components/Reports/GoalCalculator/CalculatorSettings/Categories/SpecialIncomeCategory/SpecialInterestHelperPanel/SpouseIncomeHelperPanel.tsx 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.", - )} - - - ); -}; From cc2785fb8d7b4b72de93866e6ecf0b1bb8c1804a Mon Sep 17 00:00:00 2001 From: wjames111 Date: Wed, 10 Sep 2025 11:09:18 -0400 Subject: [PATCH 02/17] Maps through specialFamilyPrimaryBudgetCategories rather then use hardcoded values. --- .../CalculatorSettings/SettingsStep.tsx | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) 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) => ( + + ))} } /> From a325806af1dbfbeef6de72f5b68a22dc097d6fea Mon Sep 17 00:00:00 2001 From: wjames111 Date: Wed, 10 Sep 2025 11:10:12 -0400 Subject: [PATCH 03/17] Removes double border on side panel. --- .../Reports/GoalCalculator/Shared/GoalCalculatorLayout.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/Reports/GoalCalculator/Shared/GoalCalculatorLayout.tsx b/src/components/Reports/GoalCalculator/Shared/GoalCalculatorLayout.tsx index dd0078ec15..46aae8c072 100644 --- a/src/components/Reports/GoalCalculator/Shared/GoalCalculatorLayout.tsx +++ b/src/components/Reports/GoalCalculator/Shared/GoalCalculatorLayout.tsx @@ -52,7 +52,6 @@ const StyledDrawer = styled('nav', { duration: theme.transitions.duration.enteringScreen, }), overflow: 'hidden', - borderRight: open ? `1px solid ${theme.palette.cruGrayLight.main}` : 'none', [theme.breakpoints.down('sm')]: { position: 'absolute', top: multiPageHeaderHeight, @@ -142,7 +141,6 @@ export const GoalCalculatorLayout: React.FC = ({ {sectionListPanel} {isDrawerOpen && } - {mainContent} ); From 2a0f847bd0ad5bb977bc655a20c4d402d0cb9d15 Mon Sep 17 00:00:00 2001 From: wjames111 Date: Wed, 10 Sep 2025 11:11:27 -0400 Subject: [PATCH 04/17] Adds additional titleExtra prop for GoalCalculatorGrid. --- .../GoalCalculator/Shared/GoalCalculatorSection.test.tsx | 8 ++++++++ .../GoalCalculator/Shared/GoalCalculatorSection.tsx | 7 ++++++- 2 files changed, 14 insertions(+), 1 deletion(-) 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..f5d34bd915 100644 --- a/src/components/Reports/GoalCalculator/Shared/GoalCalculatorSection.tsx +++ b/src/components/Reports/GoalCalculator/Shared/GoalCalculatorSection.tsx @@ -11,6 +11,7 @@ export interface GoalCalculatorSectionProps { rightPanelContent?: JSX.Element; printable?: boolean; children: React.ReactNode; + titleExtra?: React.ReactNode; } export const GoalCalculatorSection: React.FC = ({ @@ -19,6 +20,7 @@ export const GoalCalculatorSection: React.FC = ({ rightPanelContent, printable = false, children, + titleExtra, }) => { const { setRightPanelContent } = useGoalCalculator(); const { t } = useTranslation(); @@ -30,11 +32,12 @@ export const GoalCalculatorSection: React.FC = ({ return (
- + {title} {rightPanelContent && ( { rightPanelContent && setRightPanelContent(rightPanelContent); @@ -45,6 +48,8 @@ export const GoalCalculatorSection: React.FC = ({ )} + {titleExtra} + {printable && ( - + } + rightPanelContent={ + getPrimaryCategoryRightPanel(category.category) ?? undefined + } + > + {promptText && {t(promptText)}} + {directInput ? ( @@ -260,32 +453,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/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 }, From 35d0c7b68c62c7d7b0fa59f3dbf4f42ad7a55d52 Mon Sep 17 00:00:00 2001 From: wjames111 Date: Wed, 10 Sep 2025 11:14:45 -0400 Subject: [PATCH 06/17] Removes GoalCalculatorSection from ExpensesStep and uses it directly in GoalCalculatorGrid. --- .../ExpensesStep/ExpensesStep.test.tsx | 15 +-------------- .../ExpensesStep/ExpensesStep.tsx | 12 +----------- 2 files changed, 2 insertions(+), 25 deletions(-) diff --git a/src/components/Reports/GoalCalculator/SharedComponents/ExpensesStep/ExpensesStep.test.tsx b/src/components/Reports/GoalCalculator/SharedComponents/ExpensesStep/ExpensesStep.test.tsx index 2cc86c5e9a..bba2e5768a 100644 --- a/src/components/Reports/GoalCalculator/SharedComponents/ExpensesStep/ExpensesStep.test.tsx +++ b/src/components/Reports/GoalCalculator/SharedComponents/ExpensesStep/ExpensesStep.test.tsx @@ -37,23 +37,10 @@ const TestComponent: React.FC = ({ ); describe('ExpensesStep', () => { - it('renders with instructions and budget categories', () => { + it('renders with instructions', () => { const { getByRole } = render(); expect(getByRole('heading', { name: 'Instructions' })).toBeInTheDocument(); - expect( - getByRole('heading', { name: 'Ministry & Medical Mileage' }), - ).toBeInTheDocument(); - expect( - getByRole('heading', { name: 'Account Transfers' }), - ).toBeInTheDocument(); - }); - - it('renders section list with family sections', async () => { - const { findAllByText, getAllByText } = render(); - - expect(await findAllByText('Ministry & Medical Mileage')).toHaveLength(2); - expect(getAllByText('Account Transfers')).toHaveLength(2); }); it('renders GoalCalculatorGrid for each category', () => { diff --git a/src/components/Reports/GoalCalculator/SharedComponents/ExpensesStep/ExpensesStep.tsx b/src/components/Reports/GoalCalculator/SharedComponents/ExpensesStep/ExpensesStep.tsx index 234ae82a85..25caaea1bc 100644 --- a/src/components/Reports/GoalCalculator/SharedComponents/ExpensesStep/ExpensesStep.tsx +++ b/src/components/Reports/GoalCalculator/SharedComponents/ExpensesStep/ExpensesStep.tsx @@ -1,8 +1,6 @@ import React from 'react'; -import { getPrimaryCategoryRightPanel } from '../../RightPanels/rightPanels'; import { BudgetFamilyFragment } from '../../Shared/GoalCalculation.generated'; import { GoalCalculatorLayout } from '../../Shared/GoalCalculatorLayout'; -import { GoalCalculatorSection } from '../../Shared/GoalCalculatorSection'; import { getFamilySections } from '../../Shared/familySections'; import { GoalCalculatorGrid } from '../GoalCalculatorGrid/GoalCalculatorGrid'; import { SectionList } from '../SectionList'; @@ -25,15 +23,7 @@ export const ExpensesStep: React.FC = ({ {instructions} {family?.primaryBudgetCategories.map((category) => ( - - - + ))} } From 13973bb4bbba4bf0cd3a8e2133c2b7bd06d6202f Mon Sep 17 00:00:00 2001 From: wjames111 Date: Wed, 10 Sep 2025 16:35:57 -0400 Subject: [PATCH 07/17] Adds support for subCategory right panels. --- .../RightPanels/SubUtilitiesPanel.tsx | 16 +++++ .../RightPanels/rightPanels.tsx | 18 +++++- .../GoalCalculatorGrid.test.tsx | 63 +++++++++++++++++++ .../GoalCalculatorGrid/GoalCalculatorGrid.tsx | 34 +++++++++- 4 files changed, 127 insertions(+), 4 deletions(-) create mode 100644 src/components/Reports/GoalCalculator/RightPanels/SubUtilitiesPanel.tsx diff --git a/src/components/Reports/GoalCalculator/RightPanels/SubUtilitiesPanel.tsx b/src/components/Reports/GoalCalculator/RightPanels/SubUtilitiesPanel.tsx new file mode 100644 index 0000000000..3e7bc7df7e --- /dev/null +++ b/src/components/Reports/GoalCalculator/RightPanels/SubUtilitiesPanel.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import { Typography } from '@mui/material'; +import { useTranslation } from 'react-i18next'; +import { RightPanel } from './RightPanel'; + +export const SubUtilitiesPanel: React.FC = () => { + const { t } = useTranslation(); + + return ( + + + {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..82a8e0e217 100644 --- a/src/components/Reports/GoalCalculator/RightPanels/rightPanels.tsx +++ b/src/components/Reports/GoalCalculator/RightPanels/rightPanels.tsx @@ -1,8 +1,12 @@ 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 { SubUtilitiesPanel } from './SubUtilitiesPanel'; import { UtilitiesPanel } from './UtilitiesPanel'; export const getPrimaryCategoryRightPanel = ( @@ -21,3 +25,15 @@ export const getPrimaryCategoryRightPanel = ( return null; } }; + +export const getSubCategoryRightPanel = ( + subCategory: SubBudgetCategoryEnum, +) => { + switch (subCategory) { + case SubBudgetCategoryEnum.UtilitiesInternet: + case SubBudgetCategoryEnum.UtilitiesPhoneMobile: + return ; + default: + return null; + } +}; diff --git a/src/components/Reports/GoalCalculator/SharedComponents/GoalCalculatorGrid/GoalCalculatorGrid.test.tsx b/src/components/Reports/GoalCalculator/SharedComponents/GoalCalculatorGrid/GoalCalculatorGrid.test.tsx index a267be6de7..bff59ecfc2 100644 --- a/src/components/Reports/GoalCalculator/SharedComponents/GoalCalculatorGrid/GoalCalculatorGrid.test.tsx +++ b/src/components/Reports/GoalCalculator/SharedComponents/GoalCalculatorGrid/GoalCalculatorGrid.test.tsx @@ -4,6 +4,7 @@ import userEvent from '@testing-library/user-event'; import { PrimaryBudgetCategory, PrimaryBudgetCategoryEnum, + SubBudgetCategoryEnum, } from 'src/graphql/types.generated'; import { GoalCalculatorTestWrapper } from '../../GoalCalculatorTestWrapper'; import { useGoalCalculator } from '../../Shared/GoalCalculatorContext'; @@ -22,6 +23,12 @@ const TestComponent: React.FC = () => { ) : null; }; +const RightPanel: React.FC = () => { + const { rightPanelContent } = useGoalCalculator(); + + return ; +}; + describe('GoalCalculatorGrid', () => { it('allows entering a value in the lump sum text field', async () => { const mutationSpy = jest.fn(); @@ -307,4 +314,60 @@ describe('GoalCalculatorGrid', () => { ); }); }); + + it('renders subCategoryPanel', async () => { + const propsWithSubCategory = { + category: { + id: 'category-1', + label: 'Internet & Mobile', + category: PrimaryBudgetCategoryEnum.Utilities, + directInput: null, // null means Line Item mode, which shows subcategories + createdAt: '2024-01-01T00:00:00Z', + updatedAt: '2024-01-01T00:00:00Z', + subBudgetCategories: [ + { + id: 'sub-1', + label: 'Internet', + amount: 60, + category: SubBudgetCategoryEnum.UtilitiesInternet, + createdAt: '2024-01-01T00:00:00Z', + updatedAt: '2024-01-01T00:00:00Z', + }, + { + id: 'sub-2', + label: 'Phone/Mobile', + amount: 40, + category: SubBudgetCategoryEnum.UtilitiesPhoneMobile, + createdAt: '2024-01-01T00:00:00Z', + updatedAt: '2024-01-01T00:00:00Z', + }, + ], + } as unknown as PrimaryBudgetCategory, + }; + + const { getByText, getAllByRole, findByText } = render( + + + + , + ); + + const lineItemButton = getByText('Line Item'); + expect(lineItemButton.closest('button')).toHaveClass('MuiButton-contained'); + + expect(await findByText('Internet')).toBeInTheDocument(); + expect(getByText('Phone/Mobile')).toBeInTheDocument(); + + const infoButtons = getAllByRole('button', { + name: 'Show additional info', + }); + expect(infoButtons.length).toBeGreaterThan(0); + + userEvent.click(infoButtons[1]); + await waitFor(() => { + expect( + getByText('Only the portion not reimbursed as ministry expense.'), + ).toBeInTheDocument(); + }); + }); }); diff --git a/src/components/Reports/GoalCalculator/SharedComponents/GoalCalculatorGrid/GoalCalculatorGrid.tsx b/src/components/Reports/GoalCalculator/SharedComponents/GoalCalculatorGrid/GoalCalculatorGrid.tsx index 04b57f4bd4..6d33e86947 100644 --- a/src/components/Reports/GoalCalculator/SharedComponents/GoalCalculatorGrid/GoalCalculatorGrid.tsx +++ b/src/components/Reports/GoalCalculator/SharedComponents/GoalCalculatorGrid/GoalCalculatorGrid.tsx @@ -3,6 +3,7 @@ import AddIcon from '@mui/icons-material/Add'; import DeleteIcon from '@mui/icons-material/Delete'; import DoNotDisturbAltIcon from '@mui/icons-material/DoNotDisturbAlt'; import FunctionsIcon from '@mui/icons-material/Functions'; +import InfoIcon from '@mui/icons-material/Info'; import ViewHeadlineIcon from '@mui/icons-material/ViewHeadline'; import { Box, @@ -10,6 +11,7 @@ import { ButtonGroup, Card, FormHelperText, + IconButton, TextField, Typography, styled, @@ -22,12 +24,17 @@ import { } from '@mui/x-data-grid'; import { useTranslation } from 'react-i18next'; import * as yup from 'yup'; +import { SubBudgetCategoryEnum } from 'src/graphql/types.generated'; import { useAccountListId } from 'src/hooks/useAccountListId'; import { useDebouncedCallback } from 'src/hooks/useDebounce'; import { useLocale } from 'src/hooks/useLocale'; import { currencyFormat } from 'src/lib/intlFormat'; -import { getPrimaryCategoryRightPanel } from '../../RightPanels/rightPanels'; +import { + getPrimaryCategoryRightPanel, + getSubCategoryRightPanel, +} from '../../RightPanels/rightPanels'; import { BudgetFamilyFragment } from '../../Shared/GoalCalculation.generated'; +import { useGoalCalculator } from '../../Shared/GoalCalculatorContext'; import { GoalCalculatorSection } from '../../Shared/GoalCalculatorSection'; import { UpdateSubBudgetCategoriesFragment, @@ -83,6 +90,7 @@ export const GoalCalculatorGrid: React.FC = ({ const locale = useLocale(); const { label: categoryName } = category; const accountListId = useAccountListId() ?? ''; + const { setRightPanelContent } = useGoalCalculator(); const gridData = useMemo( () => @@ -91,6 +99,7 @@ export const GoalCalculatorGrid: React.FC = ({ label: subCategory.label, amount: subCategory.amount, canDelete: !subCategory.category, + category: subCategory.category, })), [category.subBudgetCategories], ); @@ -340,12 +349,31 @@ export const GoalCalculatorGrid: React.FC = ({ const renderLabelCell = (params: GridRenderCellParams) => { const cellKey = `${params.id}-label`; const hasError = cellErrors[cellKey]; + const rowCategory = params.row.category as SubBudgetCategoryEnum | null; + const rightPanelContent = rowCategory + ? getSubCategoryRightPanel(rowCategory) + : null; + + const content = ( + + {params.value} + {rightPanelContent && ( + setRightPanelContent(rightPanelContent)} + aria-label={t('Show additional info')} + > + + + )} + + ); if (hasError) { - return {params.value}; + return {content}; } - return params.value; + return content; }; const renderAmountCell = (params: GridRenderCellParams) => { From 89ce9b1f9f8e68d76710eaac8d8b887b69c6a1ea Mon Sep 17 00:00:00 2001 From: wjames111 Date: Wed, 10 Sep 2025 16:42:32 -0400 Subject: [PATCH 08/17] Fix test. --- .../GoalCalculatorGrid/GoalCalculatorGrid.test.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/components/Reports/GoalCalculator/SharedComponents/GoalCalculatorGrid/GoalCalculatorGrid.test.tsx b/src/components/Reports/GoalCalculator/SharedComponents/GoalCalculatorGrid/GoalCalculatorGrid.test.tsx index bff59ecfc2..1c574408a0 100644 --- a/src/components/Reports/GoalCalculator/SharedComponents/GoalCalculatorGrid/GoalCalculatorGrid.test.tsx +++ b/src/components/Reports/GoalCalculator/SharedComponents/GoalCalculatorGrid/GoalCalculatorGrid.test.tsx @@ -364,10 +364,8 @@ describe('GoalCalculatorGrid', () => { expect(infoButtons.length).toBeGreaterThan(0); userEvent.click(infoButtons[1]); - await waitFor(() => { - expect( - getByText('Only the portion not reimbursed as ministry expense.'), - ).toBeInTheDocument(); - }); + expect( + await findByText('Only the portion not reimbursed as ministry expense.'), + ).toBeInTheDocument(); }); }); From 8b063625feee91862d5d304a122601e868d423e5 Mon Sep 17 00:00:00 2001 From: wjames111 Date: Wed, 10 Sep 2025 16:44:20 -0400 Subject: [PATCH 09/17] fixup! Fix test. --- .../GoalCalculatorGrid/GoalCalculatorGrid.test.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/components/Reports/GoalCalculator/SharedComponents/GoalCalculatorGrid/GoalCalculatorGrid.test.tsx b/src/components/Reports/GoalCalculator/SharedComponents/GoalCalculatorGrid/GoalCalculatorGrid.test.tsx index 1c574408a0..49dfe0e93a 100644 --- a/src/components/Reports/GoalCalculator/SharedComponents/GoalCalculatorGrid/GoalCalculatorGrid.test.tsx +++ b/src/components/Reports/GoalCalculator/SharedComponents/GoalCalculatorGrid/GoalCalculatorGrid.test.tsx @@ -352,18 +352,14 @@ describe('GoalCalculatorGrid', () => { , ); - const lineItemButton = getByText('Line Item'); - expect(lineItemButton.closest('button')).toHaveClass('MuiButton-contained'); - - expect(await findByText('Internet')).toBeInTheDocument(); + expect(getByText('Internet')).toBeInTheDocument(); expect(getByText('Phone/Mobile')).toBeInTheDocument(); const infoButtons = getAllByRole('button', { name: 'Show additional info', }); - expect(infoButtons.length).toBeGreaterThan(0); - userEvent.click(infoButtons[1]); + userEvent.click(infoButtons[0]); expect( await findByText('Only the portion not reimbursed as ministry expense.'), ).toBeInTheDocument(); From 9c997ee77d7ac2297ac06ccbe2162263ce73f162 Mon Sep 17 00:00:00 2001 From: wjames111 Date: Thu, 11 Sep 2025 10:45:07 -0400 Subject: [PATCH 10/17] Fix oval hover affect on info icon. --- .../SharedComponents/GoalCalculatorGrid/GoalCalculatorGrid.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/Reports/GoalCalculator/SharedComponents/GoalCalculatorGrid/GoalCalculatorGrid.tsx b/src/components/Reports/GoalCalculator/SharedComponents/GoalCalculatorGrid/GoalCalculatorGrid.tsx index 6d33e86947..458022f75e 100644 --- a/src/components/Reports/GoalCalculator/SharedComponents/GoalCalculatorGrid/GoalCalculatorGrid.tsx +++ b/src/components/Reports/GoalCalculator/SharedComponents/GoalCalculatorGrid/GoalCalculatorGrid.tsx @@ -359,7 +359,6 @@ export const GoalCalculatorGrid: React.FC = ({ {params.value} {rightPanelContent && ( setRightPanelContent(rightPanelContent)} aria-label={t('Show additional info')} > From 28c7ac1c60fdcef7cd7d7064f547126b801fd92c Mon Sep 17 00:00:00 2001 From: wjames111 Date: Thu, 11 Sep 2025 13:35:39 -0400 Subject: [PATCH 11/17] Fixes mocked fields. --- .../GoalCalculatorTestWrapper.tsx | 37 +++++++++++++- .../GoalCalculatorGrid.test.tsx | 48 ++++++------------- 2 files changed, 50 insertions(+), 35 deletions(-) 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/SharedComponents/GoalCalculatorGrid/GoalCalculatorGrid.test.tsx b/src/components/Reports/GoalCalculator/SharedComponents/GoalCalculatorGrid/GoalCalculatorGrid.test.tsx index 49dfe0e93a..4fc70da938 100644 --- a/src/components/Reports/GoalCalculator/SharedComponents/GoalCalculatorGrid/GoalCalculatorGrid.test.tsx +++ b/src/components/Reports/GoalCalculator/SharedComponents/GoalCalculatorGrid/GoalCalculatorGrid.test.tsx @@ -4,13 +4,14 @@ import userEvent from '@testing-library/user-event'; import { PrimaryBudgetCategory, PrimaryBudgetCategoryEnum, - SubBudgetCategoryEnum, } from 'src/graphql/types.generated'; import { GoalCalculatorTestWrapper } from '../../GoalCalculatorTestWrapper'; import { useGoalCalculator } from '../../Shared/GoalCalculatorContext'; import { GoalCalculatorGrid } from './GoalCalculatorGrid'; -const TestComponent: React.FC = () => { +const TestComponent: React.FC<{ primaryBudgetCategoryIndex?: number }> = ({ + primaryBudgetCategoryIndex = 0, +}) => { const { goalCalculationResult: { data }, } = useGoalCalculator(); @@ -18,7 +19,11 @@ const TestComponent: React.FC = () => { return data ? ( ) : null; }; @@ -316,41 +321,14 @@ describe('GoalCalculatorGrid', () => { }); it('renders subCategoryPanel', async () => { - const propsWithSubCategory = { - category: { - id: 'category-1', - label: 'Internet & Mobile', - category: PrimaryBudgetCategoryEnum.Utilities, - directInput: null, // null means Line Item mode, which shows subcategories - createdAt: '2024-01-01T00:00:00Z', - updatedAt: '2024-01-01T00:00:00Z', - subBudgetCategories: [ - { - id: 'sub-1', - label: 'Internet', - amount: 60, - category: SubBudgetCategoryEnum.UtilitiesInternet, - createdAt: '2024-01-01T00:00:00Z', - updatedAt: '2024-01-01T00:00:00Z', - }, - { - id: 'sub-2', - label: 'Phone/Mobile', - amount: 40, - category: SubBudgetCategoryEnum.UtilitiesPhoneMobile, - createdAt: '2024-01-01T00:00:00Z', - updatedAt: '2024-01-01T00:00:00Z', - }, - ], - } as unknown as PrimaryBudgetCategory, - }; - const { getByText, getAllByRole, findByText } = render( - + , ); + const lumpSumButton = await findByText('Line Item'); + userEvent.click(lumpSumButton); expect(getByText('Internet')).toBeInTheDocument(); expect(getByText('Phone/Mobile')).toBeInTheDocument(); @@ -361,7 +339,9 @@ describe('GoalCalculatorGrid', () => { userEvent.click(infoButtons[0]); expect( - await findByText('Only the portion not reimbursed as ministry expense.'), + await findByText( + 'For mobile phone and internet expenses, only include the portion not reimbursed as a ministry expense.', + ), ).toBeInTheDocument(); }); }); From a1de88cf6a74de3edddf4f20fee5822c509b8928 Mon Sep 17 00:00:00 2001 From: wjames111 Date: Thu, 11 Sep 2025 13:38:42 -0400 Subject: [PATCH 12/17] Put sidePanel content for utilities in an Alert. --- .../GoalCalculator/RightPanels/SubUtilitiesPanel.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/Reports/GoalCalculator/RightPanels/SubUtilitiesPanel.tsx b/src/components/Reports/GoalCalculator/RightPanels/SubUtilitiesPanel.tsx index 3e7bc7df7e..3bb2215235 100644 --- a/src/components/Reports/GoalCalculator/RightPanels/SubUtilitiesPanel.tsx +++ b/src/components/Reports/GoalCalculator/RightPanels/SubUtilitiesPanel.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Typography } from '@mui/material'; +import { Alert } from '@mui/material'; import { useTranslation } from 'react-i18next'; import { RightPanel } from './RightPanel'; @@ -8,9 +8,9 @@ export const SubUtilitiesPanel: React.FC = () => { return ( - + {t('Only the portion not reimbursed as ministry expense.')} - + ); }; From de7cc5f563aab64d50ad9e2dac557476dff1c261 Mon Sep 17 00:00:00 2001 From: wjames111 Date: Thu, 11 Sep 2025 13:41:28 -0400 Subject: [PATCH 13/17] Remove Utilities panel for SubUtilitiesPanel. --- .../RightPanels/UtilitiesPanel.tsx | 18 ------------------ .../GoalCalculator/RightPanels/rightPanels.tsx | 4 +--- 2 files changed, 1 insertion(+), 21 deletions(-) delete mode 100644 src/components/Reports/GoalCalculator/RightPanels/UtilitiesPanel.tsx diff --git a/src/components/Reports/GoalCalculator/RightPanels/UtilitiesPanel.tsx b/src/components/Reports/GoalCalculator/RightPanels/UtilitiesPanel.tsx deleted file mode 100644 index 6c10c9fc72..0000000000 --- a/src/components/Reports/GoalCalculator/RightPanels/UtilitiesPanel.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react'; -import { Alert } from '@mui/material'; -import { useTranslation } from 'react-i18next'; -import { RightPanel } from './RightPanel'; - -export const UtilitiesPanel: React.FC = () => { - const { t } = useTranslation(); - - return ( - - - {t( - 'For mobile phone and internet expenses, only include the portion not reimbursed as a ministry expense.', - )} - - - ); -}; diff --git a/src/components/Reports/GoalCalculator/RightPanels/rightPanels.tsx b/src/components/Reports/GoalCalculator/RightPanels/rightPanels.tsx index 82a8e0e217..b162c86bd7 100644 --- a/src/components/Reports/GoalCalculator/RightPanels/rightPanels.tsx +++ b/src/components/Reports/GoalCalculator/RightPanels/rightPanels.tsx @@ -7,7 +7,7 @@ import { MedicalPanel } from './MedicalPanel'; import { MileagePanel } from './MileagePanel'; import { SavingsPanel } from './SavingsPanel'; import { SubUtilitiesPanel } from './SubUtilitiesPanel'; -import { UtilitiesPanel } from './UtilitiesPanel'; +// import { UtilitiesPanel } from './UtilitiesPanel'; export const getPrimaryCategoryRightPanel = ( category: PrimaryBudgetCategoryEnum, @@ -15,8 +15,6 @@ export const getPrimaryCategoryRightPanel = ( switch (category) { case PrimaryBudgetCategoryEnum.Saving: return ; - case PrimaryBudgetCategoryEnum.Utilities: - return ; case PrimaryBudgetCategoryEnum.Medical: return ; case PrimaryBudgetCategoryEnum.MinistryAndMedicalMileage: From 224854c6ac3d9cbab16adfd7716a7e3d55e2b77a Mon Sep 17 00:00:00 2001 From: wjames111 Date: Fri, 12 Sep 2025 12:23:12 -0400 Subject: [PATCH 14/17] Makes SavingsPanel a subCategory Panel rather then a primary one. --- .../Reports/GoalCalculator/RightPanels/rightPanels.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/Reports/GoalCalculator/RightPanels/rightPanels.tsx b/src/components/Reports/GoalCalculator/RightPanels/rightPanels.tsx index b162c86bd7..6661d625ba 100644 --- a/src/components/Reports/GoalCalculator/RightPanels/rightPanels.tsx +++ b/src/components/Reports/GoalCalculator/RightPanels/rightPanels.tsx @@ -7,14 +7,11 @@ import { MedicalPanel } from './MedicalPanel'; import { MileagePanel } from './MileagePanel'; import { SavingsPanel } from './SavingsPanel'; import { SubUtilitiesPanel } from './SubUtilitiesPanel'; -// import { UtilitiesPanel } from './UtilitiesPanel'; export const getPrimaryCategoryRightPanel = ( category: PrimaryBudgetCategoryEnum, ) => { switch (category) { - case PrimaryBudgetCategoryEnum.Saving: - return ; case PrimaryBudgetCategoryEnum.Medical: return ; case PrimaryBudgetCategoryEnum.MinistryAndMedicalMileage: @@ -31,6 +28,8 @@ export const getSubCategoryRightPanel = ( case SubBudgetCategoryEnum.UtilitiesInternet: case SubBudgetCategoryEnum.UtilitiesPhoneMobile: return ; + case SubBudgetCategoryEnum.SavingEmergencyFund: + return ; default: return null; } From 7e05229bcc6537abc8b46184205cfe8be86fcc65 Mon Sep 17 00:00:00 2001 From: wjames111 Date: Fri, 12 Sep 2025 13:58:47 -0400 Subject: [PATCH 15/17] Add scrollTo functionality for the main panel. --- .../Shared/GoalCalculatorContext.tsx | 32 +++++++++++++++++++ .../Shared/GoalCalculatorSection.tsx | 15 +++++++-- .../SharedComponents/SectionList.tsx | 14 +++++++- 3 files changed, 57 insertions(+), 4 deletions(-) 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/GoalCalculatorSection.tsx b/src/components/Reports/GoalCalculator/Shared/GoalCalculatorSection.tsx index f5d34bd915..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'; @@ -22,15 +22,24 @@ export const GoalCalculatorSection: React.FC = ({ 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 ( -
+
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)} + /> ))} From d82f862046318832d5bce1d91445eb24373a8639 Mon Sep 17 00:00:00 2001 From: wjames111 Date: Fri, 12 Sep 2025 14:06:38 -0400 Subject: [PATCH 16/17] Fixes sectionList overflow causing gap between details panel and header. --- .../Reports/GoalCalculator/Shared/GoalCalculatorLayout.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/Reports/GoalCalculator/Shared/GoalCalculatorLayout.tsx b/src/components/Reports/GoalCalculator/Shared/GoalCalculatorLayout.tsx index 46aae8c072..5a7c60f467 100644 --- a/src/components/Reports/GoalCalculator/Shared/GoalCalculatorLayout.tsx +++ b/src/components/Reports/GoalCalculator/Shared/GoalCalculatorLayout.tsx @@ -51,7 +51,8 @@ const StyledDrawer = styled('nav', { easing: theme.transitions.easing.sharp, duration: theme.transitions.duration.enteringScreen, }), - overflow: 'hidden', + overflow: 'scroll', + height: `calc(100vh - ${navBarHeight} - ${multiPageHeaderHeight})`, [theme.breakpoints.down('sm')]: { position: 'absolute', top: multiPageHeaderHeight, From 6d0217301770063bbee07e7167dc637f9764d3f3 Mon Sep 17 00:00:00 2001 From: wjames111 Date: Fri, 12 Sep 2025 14:09:26 -0400 Subject: [PATCH 17/17] Adds border on mobile to SectionList. --- .../Reports/GoalCalculator/Shared/GoalCalculatorLayout.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/Reports/GoalCalculator/Shared/GoalCalculatorLayout.tsx b/src/components/Reports/GoalCalculator/Shared/GoalCalculatorLayout.tsx index 5a7c60f467..39282a6262 100644 --- a/src/components/Reports/GoalCalculator/Shared/GoalCalculatorLayout.tsx +++ b/src/components/Reports/GoalCalculator/Shared/GoalCalculatorLayout.tsx @@ -57,6 +57,7 @@ const StyledDrawer = styled('nav', { position: 'absolute', top: multiPageHeaderHeight, left: `calc(${iconPanelWidth} + 1px)`, + borderRight: `1px solid ${theme.palette.divider}`, height: '100%', backgroundColor: theme.palette.common.white, zIndex: 270,