From 6ea3d43cf7aebeff2236141416453eba1e60a54b Mon Sep 17 00:00:00 2001 From: Katelyn Grimes Date: Thu, 4 Dec 2025 13:30:14 -0500 Subject: [PATCH 01/10] Added delete mutation to mha --- .../MinisterHousingAllowance.graphql | 8 +++ .../SharedComponents/CurrentRequest.test.tsx | 62 ++++++++++++++++--- .../SharedComponents/CurrentRequest.tsx | 24 ++++++- 3 files changed, 84 insertions(+), 10 deletions(-) diff --git a/src/components/Reports/MinisterHousingAllowance/MinisterHousingAllowance.graphql b/src/components/Reports/MinisterHousingAllowance/MinisterHousingAllowance.graphql index 2934360f02..d1c4a8185a 100644 --- a/src/components/Reports/MinisterHousingAllowance/MinisterHousingAllowance.graphql +++ b/src/components/Reports/MinisterHousingAllowance/MinisterHousingAllowance.graphql @@ -125,3 +125,11 @@ mutation UpdateMinistryHousingAllowanceRequest( } } } + +mutation DeleteMinistryHousingAllowanceRequest( + $input: MinistryHousingAllowanceRequestDeleteMutationInput! +) { + deleteMinistryHousingAllowanceRequest(input: $input) { + id + } +} diff --git a/src/components/Reports/MinisterHousingAllowance/SharedComponents/CurrentRequest.test.tsx b/src/components/Reports/MinisterHousingAllowance/SharedComponents/CurrentRequest.test.tsx index 2f4f93181d..f45c815164 100644 --- a/src/components/Reports/MinisterHousingAllowance/SharedComponents/CurrentRequest.test.tsx +++ b/src/components/Reports/MinisterHousingAllowance/SharedComponents/CurrentRequest.test.tsx @@ -1,24 +1,43 @@ import React from 'react'; import { ThemeProvider } from '@mui/material/styles'; -import { render } from '@testing-library/react'; +import { render, waitFor } from '@testing-library/react'; +import { SnackbarProvider } from 'notistack'; import TestRouter from '__tests__/util/TestRouter'; import { GqlMockedProvider } from '__tests__/util/graphqlMocking'; import { MhaStatusEnum } from 'src/graphql/types.generated'; import theme from 'src/theme'; -import { MinisterHousingAllowanceProvider } from '../Shared/Context/MinisterHousingAllowanceContext'; +import { DeleteMinistryHousingAllowanceRequestMutation } from '../MinisterHousingAllowance.generated'; +import { + ContextType, + MinisterHousingAllowanceContext, +} from '../Shared/Context/MinisterHousingAllowanceContext'; import { mockMHARequest } from '../mockData'; import { CurrentRequest, getDotColor, getDotVariant } from './CurrentRequest'; +const mutationSpy = jest.fn(); + const TestComponent: React.FC = () => { return ( - - - - - - - + + + onCall={mutationSpy} + > + + + + + + + ); }; @@ -42,6 +61,31 @@ describe('CurrentRequest Component', () => { expect(getByText(/MHA Available on/i)).toBeInTheDocument(); expect(getByText(/Nov 20, 2019/i)).toBeInTheDocument(); }); + + it('should call delete mutation on cancel request', async () => { + const { getByText, findByText } = render(); + + const cancelButton = getByText('Cancel Request'); + cancelButton.click(); + + const confirmButton = await findByText('Yes, Cancel'); + confirmButton.click(); + + await waitFor(() => { + expect(mutationSpy).toHaveBeenCalledWith( + expect.objectContaining({ + operation: expect.objectContaining({ + operationName: 'DeleteMinistryHousingAllowanceRequest', + variables: { + input: { + requestId: 'request-id', + }, + }, + }), + }), + ); + }); + }); }); describe('getDotColor', () => { diff --git a/src/components/Reports/MinisterHousingAllowance/SharedComponents/CurrentRequest.tsx b/src/components/Reports/MinisterHousingAllowance/SharedComponents/CurrentRequest.tsx index aff5d02f70..e8a6a59c82 100644 --- a/src/components/Reports/MinisterHousingAllowance/SharedComponents/CurrentRequest.tsx +++ b/src/components/Reports/MinisterHousingAllowance/SharedComponents/CurrentRequest.tsx @@ -9,12 +9,14 @@ import { } from '@mui/lab'; import { Box, Typography } from '@mui/material'; import { DateTime } from 'luxon'; +import { useSnackbar } from 'notistack'; import { useTranslation } from 'react-i18next'; import { MhaStatusEnum } from 'src/graphql/types.generated'; import { useAccountListId } from 'src/hooks/useAccountListId'; import { useLocale } from 'src/hooks/useLocale'; import { currencyFormat, dateFormat } from 'src/lib/intlFormat'; import { StatusCard } from '../../Shared/CalculationReports/StatusCard/StatusCard'; +import { useDeleteMinistryHousingAllowanceRequestMutation } from '../MinisterHousingAllowance.generated'; import { getRequestUrl } from '../Shared/Helper/getRequestUrl'; import { MHARequest } from './types'; @@ -27,6 +29,7 @@ export const CurrentRequest: React.FC = ({ request }) => { const locale = useLocale(); const accountListId = useAccountListId(); const currency = 'USD'; + const { enqueueSnackbar } = useSnackbar(); const requestId = request.id; @@ -40,6 +43,25 @@ export const CurrentRequest: React.FC = ({ request }) => { approvedOverallAmount, } = requestAttributes || {}; + const [deleteRequestMutation] = + useDeleteMinistryHousingAllowanceRequestMutation(); + + const handleCancelRequest = async () => { + try { + await deleteRequestMutation({ + variables: { + input: { + requestId: requestId ?? '', + }, + }, + }); + } catch (error) { + enqueueSnackbar(t('Failed to cancel your MHA request.'), { + variant: 'error', + }); + } + }; + return ( = ({ request }) => { linkTwoText={t('Edit Request')} linkTwo={getRequestUrl(accountListId, requestId, 'edit')} isRequest={true} - handleConfirmCancel={() => {}} + handleConfirmCancel={handleCancelRequest} > From 09a2df079501e55dc306925283be6110a33668bc Mon Sep 17 00:00:00 2001 From: Katelyn Grimes Date: Fri, 5 Dec 2025 14:19:42 -0500 Subject: [PATCH 02/10] Added submit mutation to mha --- .../MinisterHousingAllowance.graphql | 25 +++++++++++++ .../CalcComponents/CostOfHome.test.tsx | 2 +- .../Steps/StepThree/Calculation.test.tsx | 35 ++++++++++++++++--- .../Steps/StepThree/Calculation.tsx | 6 ++++ 4 files changed, 63 insertions(+), 5 deletions(-) diff --git a/src/components/Reports/MinisterHousingAllowance/MinisterHousingAllowance.graphql b/src/components/Reports/MinisterHousingAllowance/MinisterHousingAllowance.graphql index d1c4a8185a..5fb707ab3a 100644 --- a/src/components/Reports/MinisterHousingAllowance/MinisterHousingAllowance.graphql +++ b/src/components/Reports/MinisterHousingAllowance/MinisterHousingAllowance.graphql @@ -133,3 +133,28 @@ mutation DeleteMinistryHousingAllowanceRequest( id } } + +mutation SubmitMinistryHousingAllowanceRequest( + $input: MinistryHousingAllowanceRequestSubmitMutationInput! +) { + submitMinistryHousingAllowanceRequest(input: $input) { + ministryHousingAllowanceRequest { + requestAttributes { + rentOrOwn + rentalValue + furnitureCostsOne + avgUtilityOne + mortgageOrRentPayment + furnitureCostsTwo + repairCosts + avgUtilityTwo + unexpectedExpenses + overallAmount + phoneNumber + emailAddress + iUnderstandMhaPolicy + submittedDate + } + } + } +} diff --git a/src/components/Reports/MinisterHousingAllowance/Steps/StepThree/CalcComponents/CostOfHome.test.tsx b/src/components/Reports/MinisterHousingAllowance/Steps/StepThree/CalcComponents/CostOfHome.test.tsx index 6274decc61..5f55c740cd 100644 --- a/src/components/Reports/MinisterHousingAllowance/Steps/StepThree/CalcComponents/CostOfHome.test.tsx +++ b/src/components/Reports/MinisterHousingAllowance/Steps/StepThree/CalcComponents/CostOfHome.test.tsx @@ -119,7 +119,7 @@ describe('CostOfHome', () => { furnitureCostsTwo: null, repairCosts: null, avgUtilityTwo: null, - unexpectedCosts: null, + unexpectedExpenses: null, }, }, } as unknown as ContextType diff --git a/src/components/Reports/MinisterHousingAllowance/Steps/StepThree/Calculation.test.tsx b/src/components/Reports/MinisterHousingAllowance/Steps/StepThree/Calculation.test.tsx index 6a26d376e9..4e3c958abd 100644 --- a/src/components/Reports/MinisterHousingAllowance/Steps/StepThree/Calculation.test.tsx +++ b/src/components/Reports/MinisterHousingAllowance/Steps/StepThree/Calculation.test.tsx @@ -10,7 +10,10 @@ import { GqlMockedProvider } from '__tests__/util/graphqlMocking'; import { PageEnum } from 'src/components/Reports/Shared/CalculationReports/Shared/sharedTypes'; import { MhaRentOrOwnEnum } from 'src/graphql/types.generated'; import theme from 'src/theme'; -import { UpdateMinistryHousingAllowanceRequestMutation } from '../../MinisterHousingAllowance.generated'; +import { + SubmitMinistryHousingAllowanceRequestMutation, + UpdateMinistryHousingAllowanceRequestMutation, +} from '../../MinisterHousingAllowance.generated'; import { ContextType, MinisterHousingAllowanceContext, @@ -22,6 +25,7 @@ const mutationSpy = jest.fn(); const setHasCalcValues = jest.fn(); const setIsPrint = jest.fn(); const updateMutation = jest.fn(); +const handleNextStep = jest.fn(); interface TestComponentProps { contextValue: Partial; @@ -41,6 +45,7 @@ const TestComponent: React.FC = ({ onCall={mutationSpy} > @@ -101,7 +106,7 @@ describe('Calculation', () => { requestData: { id: 'request-id', requestAttributes: { - unexpectedCosts: null, + unexpectedExpenses: null, }, }, } as unknown as ContextType @@ -163,7 +168,7 @@ describe('Calculation', () => { ).toBeInTheDocument(); }); - it('shows validation errors when inputs are invalid', async () => { + it('shows validation errors when email and phone are invalid', async () => { const { getByRole, findByText } = render( { setHasCalcValues, setIsPrint, updateMutation, + handleNextStep, requestData: { id: 'request-id', requestAttributes: { @@ -222,7 +228,7 @@ describe('Calculation', () => { furnitureValue: null, repairCosts: null, utilityCosts: null, - unexpectedCosts: null, + unexpectedExpenses: null, iUnderstandMhaPolicy: false, phoneNumber: '1234567890', emailAddress: 'john.doe@cru.org', @@ -285,6 +291,27 @@ describe('Calculation', () => { expect(getByRole('button', { name: /go back/i })).toBeInTheDocument(); expect(getByRole('button', { name: /yes, continue/i })).toBeInTheDocument(); + + expect(mutationSpy).not.toHaveGraphqlOperation( + 'SubmitMinistryHousingAllowanceRequest', + ); + + const confirmButton = getByRole('button', { name: /yes, continue/i }); + + await userEvent.click(confirmButton); + + await waitFor(() => { + expect(mutationSpy).toHaveBeenCalledTimes(6); + }); + + expect(mutationSpy).toHaveGraphqlOperation( + 'SubmitMinistryHousingAllowanceRequest', + { + input: { + requestId: 'request-id', + }, + }, + ); }); it('should change text when dates are null', () => { diff --git a/src/components/Reports/MinisterHousingAllowance/Steps/StepThree/Calculation.tsx b/src/components/Reports/MinisterHousingAllowance/Steps/StepThree/Calculation.tsx index b5128f56d2..e01d869926 100644 --- a/src/components/Reports/MinisterHousingAllowance/Steps/StepThree/Calculation.tsx +++ b/src/components/Reports/MinisterHousingAllowance/Steps/StepThree/Calculation.tsx @@ -28,6 +28,7 @@ import i18n from 'src/lib/i18n'; import { dateFormatShort } from 'src/lib/intlFormat'; import { phoneNumber } from 'src/lib/yupHelpers'; import { DirectionButtons } from '../../../Shared/CalculationReports/DirectionButtons/DirectionButtons'; +import { useSubmitMinistryHousingAllowanceRequestMutation } from '../../MinisterHousingAllowance.generated'; import { hasPopulatedValues } from '../../Shared/Context/Helper/hasPopulatedValues'; import { useMinisterHousingAllowance } from '../../Shared/Context/MinisterHousingAllowanceContext'; import { CostOfHome } from './CalcComponents/CostOfHome'; @@ -103,6 +104,8 @@ export const Calculation: React.FC = ({ const { query } = useRouter(); const print = query.print === 'true'; + const [submitMutation] = useSubmitMinistryHousingAllowanceRequestMutation(); + const { handleNextStep, handlePreviousStep, @@ -198,6 +201,9 @@ export const Calculation: React.FC = ({ validateOnChange validateOnBlur onSubmit={() => { + submitMutation({ + variables: { input: { requestId: requestData?.id ?? '' } }, + }); handleNextStep(); }} > From df01a4f103def24c54eca97d0b3dd45b5fd1ad55 Mon Sep 17 00:00:00 2001 From: Katelyn Grimes Date: Mon, 8 Dec 2025 08:59:29 -0500 Subject: [PATCH 03/10] Updated current board approved card design --- .../MainPages/EligibleDisplay.tsx | 5 +- .../MainPages/IneligibleDisplay.tsx | 11 +- .../MinisterHousingAllowance.graphql | 2 + .../MinisterHousingAllowance.tsx | 1 + .../CurrentBoardApproved.test.tsx | 37 ++- .../SharedComponents/CurrentBoardApproved.tsx | 242 +++++++++++++----- .../Steps/StepOne/AboutForm.tsx | 2 +- .../CalcComponents/CostOfHome.test.tsx | 10 +- .../StepThree/CalcComponents/CostOfHome.tsx | 10 +- .../CalcComponents/FairRentalValue.test.tsx | 6 +- .../CalcComponents/FairRentalValue.tsx | 6 +- .../Steps/StepThree/Calculation.test.tsx | 12 +- .../MinisterHousingAllowance/mockData.ts | 1 + .../StatusCard/StatusCard.tsx | 12 +- 14 files changed, 240 insertions(+), 117 deletions(-) diff --git a/src/components/Reports/MinisterHousingAllowance/MainPages/EligibleDisplay.tsx b/src/components/Reports/MinisterHousingAllowance/MainPages/EligibleDisplay.tsx index b1ea0ab5a9..121ec8c478 100644 --- a/src/components/Reports/MinisterHousingAllowance/MainPages/EligibleDisplay.tsx +++ b/src/components/Reports/MinisterHousingAllowance/MainPages/EligibleDisplay.tsx @@ -30,9 +30,8 @@ export const EligibleDisplay: React.FC = ({

Our records indicate that you have an approved MHA amount. To view your MHA amount, click on the "View Current MHA" button - below. If you would like to apply for a new MHA, click on the - "Duplicate Last Year's MHA" button below or - "Request New MHA" below. + below. If you would like to apply for a new MHA, click + "Update Current MHA".

)} diff --git a/src/components/Reports/MinisterHousingAllowance/MainPages/IneligibleDisplay.tsx b/src/components/Reports/MinisterHousingAllowance/MainPages/IneligibleDisplay.tsx index f33e8031f8..55515dc0cd 100644 --- a/src/components/Reports/MinisterHousingAllowance/MainPages/IneligibleDisplay.tsx +++ b/src/components/Reports/MinisterHousingAllowance/MainPages/IneligibleDisplay.tsx @@ -34,10 +34,13 @@ export const IneligibleDisplay: React.FC = () => { Completing a Minister's Housing Allowance will submit the request for {preferredName}. {spousePreferredName} has not completed the required IBS courses to meet eligibility criteria. - When you calculate your salary, you will see the approved amount - that can be applied to {preferredName}'s salary. If you - believe this is incorrect, please contact Personnel Records at - 407-826-2252 or MHA@cru.org. +

+

+ Once approved, when you calculate your salary, you will see the + approved amount that can be applied to {preferredName}'s + salary. If you believe this is incorrect, please contact + Personnel Records at 407-826-2252 or{' '} + MHA@cru.org.

diff --git a/src/components/Reports/MinisterHousingAllowance/MinisterHousingAllowance.graphql b/src/components/Reports/MinisterHousingAllowance/MinisterHousingAllowance.graphql index 5fb707ab3a..ff557aceeb 100644 --- a/src/components/Reports/MinisterHousingAllowance/MinisterHousingAllowance.graphql +++ b/src/components/Reports/MinisterHousingAllowance/MinisterHousingAllowance.graphql @@ -3,6 +3,7 @@ query MinistryHousingAllowanceRequests { nodes { id personNumber + updatedAt requestAttributes { rentOrOwn rentalValue @@ -42,6 +43,7 @@ query MinistryHousingAllowanceRequest($ministryHousingAllowanceRequestId: ID!) { ministryHousingAllowanceRequest(id: $ministryHousingAllowanceRequestId) { id personNumber + updatedAt requestAttributes { rentOrOwn rentalValue diff --git a/src/components/Reports/MinisterHousingAllowance/MinisterHousingAllowance.tsx b/src/components/Reports/MinisterHousingAllowance/MinisterHousingAllowance.tsx index 9e2507e5eb..40775be5eb 100644 --- a/src/components/Reports/MinisterHousingAllowance/MinisterHousingAllowance.tsx +++ b/src/components/Reports/MinisterHousingAllowance/MinisterHousingAllowance.tsx @@ -107,6 +107,7 @@ export const MinisterHousingAllowanceReport = () => { request.status === MhaStatusEnum.BoardApproved && isCurrentRequestPending, ); + return ( = ({ contextValue }) => { const approvedMHARequest = { ...mockMHARequest, + updatedAt: '2022-12-01', requestAttributes: { ...mockMHARequest.requestAttributes, hrApprovedAt: '2023-01-15', @@ -45,7 +46,7 @@ const TestComponent: React.FC = ({ contextValue }) => { describe('CurrentBoardApproved Component', () => { it('should render correctly for married person', () => { - const { getByText } = render( + const { getByText, getByRole, getAllByText } = render( { ); expect(getByText('Current Board Approved MHA')).toBeInTheDocument(); - expect(getByText(/APPROVAL DATE/i)).toBeInTheDocument(); - expect(getByText(/1\/15\/2023/i)).toBeInTheDocument(); - expect(getByText('CURRENT MHA CLAIMED')).toBeInTheDocument(); + expect(getByRole('columnheader', { name: /spouse/i })).toBeInTheDocument(); + expect( + getByRole('columnheader', { name: /mha approved by board/i }), + ).toBeInTheDocument(); + expect( + getByRole('columnheader', { name: /mha claimed in salary/i }), + ).toBeInTheDocument(); - expect(getByText('$1,500.00')).toBeInTheDocument(); - expect(getByText('John')).toBeInTheDocument(); + expect(getByRole('cell', { name: 'John' })).toBeInTheDocument(); + expect(getAllByText('$1,500.00')).toHaveLength(2); + expect(getAllByText('Approved on: 1/15/2023')).toHaveLength(2); expect(getByText('$1,000.00')).toBeInTheDocument(); - expect(getByText('Jane')).toBeInTheDocument(); + expect(getAllByText('Last updated: 12/1/2022')).toHaveLength(2); + + expect(getByRole('cell', { name: 'Jane' })).toBeInTheDocument(); expect(getByText('$500.00')).toBeInTheDocument(); }); it('should render correctly for single person', () => { - const { getByText, queryByText } = render( + const { getByText, queryByText, getByRole, getAllByText } = render( { ); expect(getByText('Current Board Approved MHA')).toBeInTheDocument(); - expect(getByText(/APPROVAL DATE/i)).toBeInTheDocument(); - expect(getByText('CURRENT MHA CLAIMED')).toBeInTheDocument(); + expect(getByRole('columnheader', { name: /spouse/i })).toBeInTheDocument(); + expect( + getByRole('columnheader', { name: /mha approved by board/i }), + ).toBeInTheDocument(); + expect( + getByRole('columnheader', { name: /mha claimed in salary/i }), + ).toBeInTheDocument(); - expect(getByText('$1,500.00')).toBeInTheDocument(); - expect(getByText('John')).toBeInTheDocument(); + expect(getByRole('cell', { name: 'John' })).toBeInTheDocument(); + expect(getAllByText('$1,500.00')).toHaveLength(1); expect(getByText('$1,000.00')).toBeInTheDocument(); // Spouse data should not be rendered diff --git a/src/components/Reports/MinisterHousingAllowance/SharedComponents/CurrentBoardApproved.tsx b/src/components/Reports/MinisterHousingAllowance/SharedComponents/CurrentBoardApproved.tsx index 4dcba80578..5fe0e51f57 100644 --- a/src/components/Reports/MinisterHousingAllowance/SharedComponents/CurrentBoardApproved.tsx +++ b/src/components/Reports/MinisterHousingAllowance/SharedComponents/CurrentBoardApproved.tsx @@ -1,5 +1,15 @@ import { HomeSharp } from '@mui/icons-material'; -import { Grid, Skeleton, Typography } from '@mui/material'; +import { + Grid, + Skeleton, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Typography, +} from '@mui/material'; import { DateTime } from 'luxon'; import { useTranslation } from 'react-i18next'; import { useAccountListId } from 'src/hooks/useAccountListId'; @@ -29,89 +39,179 @@ export const CurrentBoardApproved: React.FC = ({ const { hrApprovedAt, approvedOverallAmount, staffSpecific, spouseSpecific } = request?.requestAttributes || {}; + const lastUpdated = request?.updatedAt ?? null; + return ( {}} + styling={{ p: 0 }} > - - - - {t('APPROVAL DATE')}:{' '} - {hrApprovedAt ? ( - dateFormatShort(DateTime.fromISO(hrApprovedAt), locale) - ) : ( - + + + + + + {t('Spouse')} + + + {t('MHA Approved by Board')} + + + {t('MHA Claimed in Salary')} + + + + + + {preferredName} + + + + + {currencyFormat(approvedOverallAmount, currency, locale, { + showTrailingZeros: true, + })} + + + + + {t('Approved on')}:{' '} + {hrApprovedAt ? ( + dateFormatShort(DateTime.fromISO(hrApprovedAt), locale) + ) : ( + + )} + + + + + + + + + {currencyFormat(staffSpecific, currency, locale, { + showTrailingZeros: true, + })} + + + + + {t('Last updated')}:{' '} + {lastUpdated ? ( + dateFormatShort(DateTime.fromISO(lastUpdated), locale) + ) : ( + + )} + + + + + + {isMarried && ( + + + {spousePreferredName} + + + + + + {currencyFormat( + approvedOverallAmount, + currency, + locale, + { + showTrailingZeros: true, + }, + )} + + + + + {t('Approved on')}:{' '} + {hrApprovedAt ? ( + dateFormatShort( + DateTime.fromISO(hrApprovedAt), + locale, + ) + ) : ( + + )} + + + + + + + + + {currencyFormat(spouseSpecific, currency, locale, { + showTrailingZeros: true, + })} + + + + + {t('Last updated')}:{' '} + {lastUpdated ? ( + dateFormatShort(DateTime.fromISO(lastUpdated), locale) + ) : ( + + )} + + + + + )} - - - - - {t('CURRENT MHA CLAIMED')} - - - - - - - {currencyFormat(approvedOverallAmount || 0, currency, locale, { - showTrailingZeros: true, - })} - - - - - - {preferredName} - - - {isMarried && ( - {spousePreferredName} - )} - - - - - - {currencyFormat(staffSpecific ?? 0, currency, locale, { - showTrailingZeros: true, - })} - - - - {isMarried && ( - - {currencyFormat(spouseSpecific ?? 0, currency, locale, { - showTrailingZeros: true, - })} - - )} - - - - + +
+
); }; diff --git a/src/components/Reports/MinisterHousingAllowance/Steps/StepOne/AboutForm.tsx b/src/components/Reports/MinisterHousingAllowance/Steps/StepOne/AboutForm.tsx index 947b5d711f..35c4657833 100644 --- a/src/components/Reports/MinisterHousingAllowance/Steps/StepOne/AboutForm.tsx +++ b/src/components/Reports/MinisterHousingAllowance/Steps/StepOne/AboutForm.tsx @@ -72,7 +72,7 @@ export const AboutForm: React.FC = ({ diff --git a/src/components/Reports/MinisterHousingAllowance/Steps/StepThree/CalcComponents/CostOfHome.test.tsx b/src/components/Reports/MinisterHousingAllowance/Steps/StepThree/CalcComponents/CostOfHome.test.tsx index 5f55c740cd..91ad607c0a 100644 --- a/src/components/Reports/MinisterHousingAllowance/Steps/StepThree/CalcComponents/CostOfHome.test.tsx +++ b/src/components/Reports/MinisterHousingAllowance/Steps/StepThree/CalcComponents/CostOfHome.test.tsx @@ -130,25 +130,25 @@ describe('CostOfHome', () => { const row1 = getByRole('row', { name: /monthly rent/i, }); - const input1 = within(row1).getByPlaceholderText(/enter amount/i); + const input1 = within(row1).getByPlaceholderText(/\$0/i); const row2 = getByRole('row', { name: /monthly value for furniture/i }); - const input2 = within(row2).getByPlaceholderText(/enter amount/i); + const input2 = within(row2).getByPlaceholderText(/\$0/i); const row3 = getByRole('row', { name: /estimated monthly cost of repairs/i, }); - const input3 = within(row3).getByPlaceholderText(/enter amount/i); + const input3 = within(row3).getByPlaceholderText(/\$0/i); const row4 = getByRole('row', { name: /average monthly utility costs/i, }); - const input4 = within(row4).getByPlaceholderText(/enter amount/i); + const input4 = within(row4).getByPlaceholderText(/\$0/i); const row5 = getByRole('row', { name: /average monthly amount for unexpected/i, }); - const input5 = within(row5).getByPlaceholderText(/enter amount/i); + const input5 = within(row5).getByPlaceholderText(/\$0/i); await userEvent.type(input1, '1000'); userEvent.tab(); diff --git a/src/components/Reports/MinisterHousingAllowance/Steps/StepThree/CalcComponents/CostOfHome.tsx b/src/components/Reports/MinisterHousingAllowance/Steps/StepThree/CalcComponents/CostOfHome.tsx index 3dfb4bddf6..6937a67a1a 100644 --- a/src/components/Reports/MinisterHousingAllowance/Steps/StepThree/CalcComponents/CostOfHome.tsx +++ b/src/components/Reports/MinisterHousingAllowance/Steps/StepThree/CalcComponents/CostOfHome.tsx @@ -56,7 +56,7 @@ export const CostOfHome: React.FC = ({ fullWidth size="small" variant="standard" - placeholder={t('Enter Amount')} + placeholder={t('$0')} InputProps={{ disableUnderline: true, inputMode: 'decimal' }} fieldName="mortgageOrRentPayment" schema={schema} @@ -87,7 +87,7 @@ export const CostOfHome: React.FC = ({ fullWidth size="small" variant="standard" - placeholder={t('Enter Amount')} + placeholder={t('$0')} InputProps={{ disableUnderline: true, inputMode: 'decimal' }} fieldName="furnitureCostsTwo" schema={schema} @@ -116,7 +116,7 @@ export const CostOfHome: React.FC = ({ fullWidth size="small" variant="standard" - placeholder={t('Enter Amount')} + placeholder={t('$0')} InputProps={{ disableUnderline: true, inputMode: 'decimal' }} fieldName="repairCosts" schema={schema} @@ -150,7 +150,7 @@ export const CostOfHome: React.FC = ({ fullWidth size="small" variant="standard" - placeholder={t('Enter Amount')} + placeholder={t('$0')} InputProps={{ disableUnderline: true, inputMode: 'decimal' }} fieldName="avgUtilityTwo" schema={schema} @@ -179,7 +179,7 @@ export const CostOfHome: React.FC = ({ fullWidth size="small" variant="standard" - placeholder={t('Enter Amount')} + placeholder={t('$0')} InputProps={{ disableUnderline: true, inputMode: 'decimal' }} fieldName="unexpectedExpenses" schema={schema} diff --git a/src/components/Reports/MinisterHousingAllowance/Steps/StepThree/CalcComponents/FairRentalValue.test.tsx b/src/components/Reports/MinisterHousingAllowance/Steps/StepThree/CalcComponents/FairRentalValue.test.tsx index a7266717b2..9ffc477787 100644 --- a/src/components/Reports/MinisterHousingAllowance/Steps/StepThree/CalcComponents/FairRentalValue.test.tsx +++ b/src/components/Reports/MinisterHousingAllowance/Steps/StepThree/CalcComponents/FairRentalValue.test.tsx @@ -95,15 +95,15 @@ describe('FairRentalValue', () => { const row1 = getByRole('row', { name: /monthly market rental value of your home/i, }); - const input1 = within(row1).getByPlaceholderText(/enter amount/i); + const input1 = within(row1).getByPlaceholderText(/\$0/i); const row2 = getByRole('row', { name: /monthly value for furniture/i }); - const input2 = within(row2).getByPlaceholderText(/enter amount/i); + const input2 = within(row2).getByPlaceholderText(/\$0/i); const row3 = getByRole('row', { name: /average monthly utility costs/i, }); - const input3 = within(row3).getByPlaceholderText(/enter amount/i); + const input3 = within(row3).getByPlaceholderText(/\$0/i); await userEvent.type(input1, '1000'); await userEvent.type(input2, '200'); diff --git a/src/components/Reports/MinisterHousingAllowance/Steps/StepThree/CalcComponents/FairRentalValue.tsx b/src/components/Reports/MinisterHousingAllowance/Steps/StepThree/CalcComponents/FairRentalValue.tsx index 8aff485713..443bac6eb3 100644 --- a/src/components/Reports/MinisterHousingAllowance/Steps/StepThree/CalcComponents/FairRentalValue.tsx +++ b/src/components/Reports/MinisterHousingAllowance/Steps/StepThree/CalcComponents/FairRentalValue.tsx @@ -54,7 +54,7 @@ export const FairRentalValue: React.FC = ({ schema }) => { fullWidth size="small" variant="standard" - placeholder={t('Enter Amount')} + placeholder={t('$0')} InputProps={{ disableUnderline: true, inputMode: 'decimal' }} fieldName="rentalValue" schema={schema} @@ -91,7 +91,7 @@ export const FairRentalValue: React.FC = ({ schema }) => { fullWidth size="small" variant="standard" - placeholder={t('Enter Amount')} + placeholder={t('$0')} InputProps={{ disableUnderline: true, inputMode: 'decimal' }} fieldName="furnitureCostsOne" schema={schema} @@ -120,7 +120,7 @@ export const FairRentalValue: React.FC = ({ schema }) => { fullWidth size="small" variant="standard" - placeholder={t('Enter Amount')} + placeholder={t('$0')} InputProps={{ disableUnderline: true, inputMode: 'decimal' }} fieldName="avgUtilityOne" schema={schema} diff --git a/src/components/Reports/MinisterHousingAllowance/Steps/StepThree/Calculation.test.tsx b/src/components/Reports/MinisterHousingAllowance/Steps/StepThree/Calculation.test.tsx index 4e3c958abd..19c42ae910 100644 --- a/src/components/Reports/MinisterHousingAllowance/Steps/StepThree/Calculation.test.tsx +++ b/src/components/Reports/MinisterHousingAllowance/Steps/StepThree/Calculation.test.tsx @@ -117,7 +117,7 @@ describe('Calculation', () => { const row = getByRole('row', { name: /average monthly amount for unexpected/i, }); - const input = within(row).getByPlaceholderText(/enter amount/i); + const input = within(row).getByPlaceholderText(/\$0/i); await userEvent.type(input, '100'); expect(input).toHaveValue('100'); @@ -243,25 +243,25 @@ describe('Calculation', () => { const row1 = getByRole('row', { name: /monthly rent/i, }); - const input1 = within(row1).getByPlaceholderText(/enter amount/i); + const input1 = within(row1).getByPlaceholderText(/\$0/i); const row2 = getByRole('row', { name: /monthly value for furniture/i }); - const input2 = within(row2).getByPlaceholderText(/enter amount/i); + const input2 = within(row2).getByPlaceholderText(/\$0/i); const row3 = getByRole('row', { name: /estimated monthly cost of repairs/i, }); - const input3 = within(row3).getByPlaceholderText(/enter amount/i); + const input3 = within(row3).getByPlaceholderText(/\$0/i); const row4 = getByRole('row', { name: /average monthly utility costs/i, }); - const input4 = within(row4).getByPlaceholderText(/enter amount/i); + const input4 = within(row4).getByPlaceholderText(/\$0/i); const row5 = getByRole('row', { name: /average monthly amount for unexpected/i, }); - const input5 = within(row5).getByPlaceholderText(/enter amount/i); + const input5 = within(row5).getByPlaceholderText(/\$0/i); await userEvent.type(input1, '1000'); await userEvent.type(input2, '200'); diff --git a/src/components/Reports/MinisterHousingAllowance/mockData.ts b/src/components/Reports/MinisterHousingAllowance/mockData.ts index db378169de..4e1bc7d7c5 100644 --- a/src/components/Reports/MinisterHousingAllowance/mockData.ts +++ b/src/components/Reports/MinisterHousingAllowance/mockData.ts @@ -4,6 +4,7 @@ import { MHARequest } from './SharedComponents/types'; export const mockMHARequest: MHARequest = { id: '1', personNumber: '123456', + updatedAt: '2019-09-15T12:00:00.000Z', status: MhaStatusEnum.Pending, feedback: null, user: { diff --git a/src/components/Reports/Shared/CalculationReports/StatusCard/StatusCard.tsx b/src/components/Reports/Shared/CalculationReports/StatusCard/StatusCard.tsx index b4d7df9ab7..9baa2fc596 100644 --- a/src/components/Reports/Shared/CalculationReports/StatusCard/StatusCard.tsx +++ b/src/components/Reports/Shared/CalculationReports/StatusCard/StatusCard.tsx @@ -11,6 +11,8 @@ import { CardHeader, Divider, IconButton, + SxProps, + Theme, Typography, } from '@mui/material'; import { useTranslation } from 'react-i18next'; @@ -35,6 +37,7 @@ interface StatusCardProps { hideActions?: boolean; handleDownload?: () => void; handleConfirmCancel: () => void; + styling?: SxProps; } export const StatusCard: React.FC = ({ @@ -53,6 +56,7 @@ export const StatusCard: React.FC = ({ hideActions, handleDownload, handleConfirmCancel, + styling, }) => { const { t } = useTranslation(); @@ -73,14 +77,14 @@ export const StatusCard: React.FC = ({ {subtitle ? ( - + {title} @@ -102,7 +106,7 @@ export const StatusCard: React.FC = ({ } /> - {children} + {children} {!hideActions && ( @@ -117,7 +121,7 @@ export const StatusCard: React.FC = ({ @@ -69,23 +68,23 @@ describe('MinisterHousingAllowanceContext', () => { const { getByTestId, getByRole } = render(); expect(getByTestId('steps')).toHaveTextContent('4'); - expect(getByTestId('currentStep')).toHaveTextContent(StepsEnum.AboutForm); + expect(getByTestId('currentIndex')).toHaveTextContent('0'); expect(getByTestId('percentComplete')).toHaveTextContent('25'); await userEvent.click(getByRole('button', { name: 'Next' })); - expect(getByTestId('currentStep')).toHaveTextContent(StepsEnum.RentOrOwn); + expect(getByTestId('currentIndex')).toHaveTextContent('1'); expect(getByTestId('percentComplete')).toHaveTextContent('50'); await userEvent.click(getByRole('button', { name: 'Next' })); - expect(getByTestId('currentStep')).toHaveTextContent(StepsEnum.CalcForm); + expect(getByTestId('currentIndex')).toHaveTextContent('2'); expect(getByTestId('percentComplete')).toHaveTextContent('75'); await userEvent.click(getByRole('button', { name: 'Next' })); - expect(getByTestId('currentStep')).toHaveTextContent(StepsEnum.Receipt); + expect(getByTestId('currentIndex')).toHaveTextContent('3'); expect(getByTestId('percentComplete')).toHaveTextContent('100'); await userEvent.click(getByRole('button', { name: 'Previous' })); - expect(getByTestId('currentStep')).toHaveTextContent(StepsEnum.CalcForm); + expect(getByTestId('currentIndex')).toHaveTextContent('2'); expect(getByTestId('percentComplete')).toHaveTextContent('75'); }); @@ -95,27 +94,31 @@ describe('MinisterHousingAllowanceContext', () => { ); expect(getByTestId('steps')).toHaveTextContent('4'); - expect(getByTestId('currentStep')).toHaveTextContent(StepsEnum.RentOrOwn); + expect(getByTestId('currentIndex')).toHaveTextContent('0'); + expect(getByTestId('percentComplete')).toHaveTextContent('25'); + + await userEvent.click(getByRole('button', { name: 'Next' })); + expect(getByTestId('currentIndex')).toHaveTextContent('1'); expect(getByTestId('percentComplete')).toHaveTextContent('50'); await userEvent.click(getByRole('button', { name: 'Next' })); - expect(getByTestId('currentStep')).toHaveTextContent(StepsEnum.CalcForm); + expect(getByTestId('currentIndex')).toHaveTextContent('2'); expect(getByTestId('percentComplete')).toHaveTextContent('75'); await userEvent.click(getByRole('button', { name: 'Next' })); - expect(getByTestId('currentStep')).toHaveTextContent(StepsEnum.Receipt); + expect(getByTestId('currentIndex')).toHaveTextContent('3'); expect(getByTestId('percentComplete')).toHaveTextContent('100'); await userEvent.click(getByRole('button', { name: 'Previous' })); - expect(getByTestId('currentStep')).toHaveTextContent(StepsEnum.CalcForm); + expect(getByTestId('currentIndex')).toHaveTextContent('2'); expect(getByTestId('percentComplete')).toHaveTextContent('75'); await userEvent.click(getByRole('button', { name: 'Previous' })); - expect(getByTestId('currentStep')).toHaveTextContent(StepsEnum.RentOrOwn); + expect(getByTestId('currentIndex')).toHaveTextContent('1'); expect(getByTestId('percentComplete')).toHaveTextContent('50'); await userEvent.click(getByRole('button', { name: 'Previous' })); - expect(getByTestId('currentStep')).toHaveTextContent(StepsEnum.AboutForm); + expect(getByTestId('currentIndex')).toHaveTextContent('0'); expect(getByTestId('percentComplete')).toHaveTextContent('25'); }); diff --git a/src/components/Reports/MinisterHousingAllowance/Shared/Context/MinisterHousingAllowanceContext.tsx b/src/components/Reports/MinisterHousingAllowance/Shared/Context/MinisterHousingAllowanceContext.tsx index 8fd2142579..f8683bdac9 100644 --- a/src/components/Reports/MinisterHousingAllowance/Shared/Context/MinisterHousingAllowanceContext.tsx +++ b/src/components/Reports/MinisterHousingAllowance/Shared/Context/MinisterHousingAllowanceContext.tsx @@ -9,6 +9,7 @@ import { useState, } from 'react'; import { ApolloError } from '@apollo/client'; +import { Box, CircularProgress } from '@mui/material'; import { FormEnum, PageEnum, @@ -24,7 +25,6 @@ import { useMinistryHousingAllowanceRequestQuery, useUpdateMinistryHousingAllowanceRequestMutation, } from '../../MinisterHousingAllowance.generated'; -import { StepsEnum } from '../sharedTypes'; import { hasPopulatedValues } from './Helper/hasPopulatedValues'; export type HcmData = HcmDataQuery['hcm'][number]; @@ -33,7 +33,6 @@ export type ContextType = { steps: Steps[]; currentIndex: number; percentComplete: number; - currentStep: StepsEnum; handleNextStep: () => void; handlePreviousStep: () => void; pageType: PageEnum | undefined; @@ -81,8 +80,6 @@ interface Props { children?: React.ReactNode; } -const objects = Object.values(StepsEnum); - export const MinisterHousingAllowanceProvider: React.FC = ({ requestId, type, @@ -109,6 +106,7 @@ export const MinisterHousingAllowanceProvider: React.FC = ({ previousStep, currentIndex, percentComplete, + isLoading, } = useStepList(FormEnum.MHA, type); const [isComplete, setIsComplete] = useState(false); @@ -161,36 +159,13 @@ export const MinisterHousingAllowanceProvider: React.FC = ({ const [hasCalcValues, setHasCalcValues] = useState(hasValues ? true : false); const [isPrint, setIsPrint] = useState(false); - const [currentStep, setCurrentStep] = useState(StepsEnum.AboutForm); - - useEffect(() => { - if (type === PageEnum.Edit) { - setCurrentStep(StepsEnum.RentOrOwn); - } - }, [type]); - - const handleNextStep = useCallback(() => { - const next = objects[currentIndex + 1]; - nextStep(); - - setCurrentStep(next); - }, [currentIndex, objects, nextStep]); - - const handlePreviousStep = useCallback(() => { - const next = objects[currentIndex - 1]; - previousStep(); - - setCurrentStep(next); - }, [currentIndex, objects, previousStep]); - const contextValue = useMemo( () => ({ steps, currentIndex, - currentStep, percentComplete, - handleNextStep, - handlePreviousStep, + handleNextStep: nextStep, + handlePreviousStep: previousStep, pageType, hasCalcValues, setHasCalcValues, @@ -213,10 +188,9 @@ export const MinisterHousingAllowanceProvider: React.FC = ({ [ steps, currentIndex, - currentStep, percentComplete, - handleNextStep, - handlePreviousStep, + nextStep, + previousStep, pageType, hasCalcValues, setHasCalcValues, @@ -238,6 +212,19 @@ export const MinisterHousingAllowanceProvider: React.FC = ({ ], ); + if (isLoading) { + return ( + + + + ); + } + return ( {children} diff --git a/src/components/Reports/MinisterHousingAllowance/SharedComponents/CurrentBoardApproved.test.tsx b/src/components/Reports/MinisterHousingAllowance/SharedComponents/CurrentBoardApproved.test.tsx index c2bdd3ca68..e756008a94 100644 --- a/src/components/Reports/MinisterHousingAllowance/SharedComponents/CurrentBoardApproved.test.tsx +++ b/src/components/Reports/MinisterHousingAllowance/SharedComponents/CurrentBoardApproved.test.tsx @@ -1,9 +1,11 @@ import React from 'react'; import { ThemeProvider } from '@mui/material/styles'; -import { render } from '@testing-library/react'; +import { render, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import TestRouter from '__tests__/util/TestRouter'; import { GqlMockedProvider } from '__tests__/util/graphqlMocking'; import theme from 'src/theme'; +import { DuplicateMinistryHousingAllowanceRequestMutation } from '../MinisterHousingAllowance.generated'; import { ContextType, HcmData, @@ -12,11 +14,21 @@ import { import { mockMHARequest } from '../mockData'; import { CurrentBoardApproved } from './CurrentBoardApproved'; +const newRequestId = 'new-request-id'; +const mutationSpy = jest.fn(); +const mockPush = jest.fn(); interface TestComponentProps { contextValue: Partial; + router?: { + push?: jest.Mock; + query?: { accountListId?: string }; + }; } -const TestComponent: React.FC = ({ contextValue }) => { +const TestComponent: React.FC = ({ + contextValue, + router = {}, +}) => { const approvedMHARequest = { ...mockMHARequest, updatedAt: '2022-12-01', @@ -31,8 +43,12 @@ const TestComponent: React.FC = ({ contextValue }) => { return ( - - + + + onCall={mutationSpy} + > @@ -118,4 +134,66 @@ describe('CurrentBoardApproved Component', () => { // Spouse data should not be rendered expect(queryByText('Jane')).not.toBeInTheDocument(); }); + + it('should navigate to edit page with new requestId after duplicate mutation', async () => { + const { getByText } = render( + + + + mocks={{ + DuplicateMinistryHousingAllowanceRequest: { + duplicateMinistryHousingAllowanceRequest: { + ministryHousingAllowanceRequest: { + id: newRequestId, + }, + }, + }, + }} + onCall={mutationSpy} + > + + + + + + , + ); + + const updateButton = getByText('Update Current MHA'); + userEvent.click(updateButton); + + await waitFor(() => { + expect(mutationSpy).toHaveGraphqlOperation( + 'DuplicateMinistryHousingAllowanceRequest', + { + input: { + requestId: 'old-request-id', + }, + }, + ); + }); + + await waitFor(() => { + expect(mockPush).toHaveBeenCalledWith( + `/accountLists/account-list-1/reports/housingAllowance/${newRequestId}/edit`, + ); + }); + }); }); diff --git a/src/components/Reports/MinisterHousingAllowance/SharedComponents/CurrentBoardApproved.tsx b/src/components/Reports/MinisterHousingAllowance/SharedComponents/CurrentBoardApproved.tsx index 5fe0e51f57..031f5fae25 100644 --- a/src/components/Reports/MinisterHousingAllowance/SharedComponents/CurrentBoardApproved.tsx +++ b/src/components/Reports/MinisterHousingAllowance/SharedComponents/CurrentBoardApproved.tsx @@ -1,3 +1,4 @@ +import { useRouter } from 'next/router'; import { HomeSharp } from '@mui/icons-material'; import { Grid, @@ -16,6 +17,7 @@ import { useAccountListId } from 'src/hooks/useAccountListId'; import { useLocale } from 'src/hooks/useLocale'; import { currencyFormat, dateFormatShort } from 'src/lib/intlFormat'; import { StatusCard } from '../../Shared/CalculationReports/StatusCard/StatusCard'; +import { useDuplicateMinistryHousingAllowanceRequestMutation } from '../MinisterHousingAllowance.generated'; import { useMinisterHousingAllowance } from '../Shared/Context/MinisterHousingAllowanceContext'; import { getRequestUrl } from '../Shared/Helper/getRequestUrl'; import { MHARequest } from './types'; @@ -30,8 +32,16 @@ export const CurrentBoardApproved: React.FC = ({ const { t } = useTranslation(); const locale = useLocale(); const accountListId = useAccountListId(); + const router = useRouter(); const currency = 'USD'; + const [duplicateMHA] = useDuplicateMinistryHousingAllowanceRequestMutation({ + refetchQueries: [ + 'MinistryHousingAllowanceRequests', + 'MinistryHousingAllowanceRequest', + ], + }); + const { isMarried, preferredName, spousePreferredName } = useMinisterHousingAllowance(); const requestId = request?.id; @@ -41,6 +51,32 @@ export const CurrentBoardApproved: React.FC = ({ const lastUpdated = request?.updatedAt ?? null; + const handleDuplicateRequest = async () => { + if (!requestId) { + return; + } + + try { + const result = await duplicateMHA({ + variables: { + input: { + requestId: requestId, + }, + }, + }); + + const newRequestId = + result.data?.duplicateMinistryHousingAllowanceRequest + ?.ministryHousingAllowanceRequest?.id; + + if (newRequestId) { + router.push( + `/accountLists/${accountListId}/reports/housingAllowance/${newRequestId}/edit`, + ); + } + } catch (error) {} + }; + return ( = ({ linkOneText={t('View Current MHA')} linkOne={getRequestUrl(accountListId, requestId, 'view')} linkTwoText={t('Update Current MHA')} - linkTwo="" + handleLinkTwo={handleDuplicateRequest} isRequest={false} handleConfirmCancel={() => {}} styling={{ p: 0 }} diff --git a/src/components/Reports/MinisterHousingAllowance/Steps/StepThree/Calculation.test.tsx b/src/components/Reports/MinisterHousingAllowance/Steps/StepThree/Calculation.test.tsx index 19c42ae910..3ad678a973 100644 --- a/src/components/Reports/MinisterHousingAllowance/Steps/StepThree/Calculation.test.tsx +++ b/src/components/Reports/MinisterHousingAllowance/Steps/StepThree/Calculation.test.tsx @@ -211,6 +211,40 @@ describe('Calculation', () => { expect(await findByText('Invalid email address.')).toBeInTheDocument(); }); + it('shows validation error when input is 0', async () => { + const { getByRole, findByText } = render( + , + ); + + const row = getByRole('row', { + name: /average monthly amount for unexpected/i, + }); + const input = within(row).getByPlaceholderText(/\$0/i); + + await userEvent.type(input, '0'); + + input.focus(); + await userEvent.tab(); + + expect(input).toHaveValue('$0.00'); + + expect(await findByText('Must be greater than $0.')).toBeInTheDocument(); + }); + it('shows confirmation modal when submit is clicked', async () => { const { getByRole, getByText, findByRole } = render( { const baseSchema = { - mortgageOrRentPayment: yup.number().required(i18n.t('Required field.')), - furnitureCostsTwo: yup.number().required(i18n.t('Required field.')), - repairCosts: yup.number().required(i18n.t('Required field.')), - avgUtilityTwo: yup.number().required(i18n.t('Required field.')), - unexpectedExpenses: yup.number().required(i18n.t('Required field.')), + mortgageOrRentPayment: yup + .number() + .moreThan(0, i18n.t('Must be greater than $0.')) + .required(i18n.t('Required field.')), + furnitureCostsTwo: yup + .number() + .moreThan(0, i18n.t('Must be greater than $0.')) + .required(i18n.t('Required field.')), + repairCosts: yup + .number() + .moreThan(0, i18n.t('Must be greater than $0.')) + .required(i18n.t('Required field.')), + avgUtilityTwo: yup + .number() + .moreThan(0, i18n.t('Must be greater than $0.')) + .required(i18n.t('Required field.')), + unexpectedExpenses: yup + .number() + .moreThan(0, i18n.t('Must be greater than $0.')) + .required(i18n.t('Required field.')), phoneNumber: phoneNumber(i18n.t).required( i18n.t('Phone Number is required.'), ), @@ -83,9 +98,18 @@ const getValidationSchema = (rentOrOwn?: MhaRentOrOwnEnum) => { if (rentOrOwn === MhaRentOrOwnEnum.Own) { return yup.object({ ...baseSchema, - rentalValue: yup.number().required(i18n.t('Required field.')), - furnitureCostsOne: yup.number().required(i18n.t('Required field.')), - avgUtilityOne: yup.number().required(i18n.t('Required field.')), + rentalValue: yup + .number() + .moreThan(0, i18n.t('Must be greater than $0.')) + .required(i18n.t('Required field.')), + furnitureCostsOne: yup + .number() + .moreThan(0, i18n.t('Must be greater than $0.')) + .required(i18n.t('Required field.')), + avgUtilityOne: yup + .number() + .moreThan(0, i18n.t('Must be greater than $0.')) + .required(i18n.t('Required field.')), }); } diff --git a/src/components/Reports/SalaryCalculator/CurrentStep.tsx b/src/components/Reports/SalaryCalculator/CurrentStep.tsx index d93a3f7f0e..9339bdb6b5 100644 --- a/src/components/Reports/SalaryCalculator/CurrentStep.tsx +++ b/src/components/Reports/SalaryCalculator/CurrentStep.tsx @@ -1,24 +1,27 @@ +import { Typography } from '@mui/material'; +import { useTranslation } from 'react-i18next'; import { EffectiveDateStep } from './EffectiveDateStep/EffectiveDateStep'; import { ReceiptStep } from './Receipt/Receipt'; -import { SalaryCalculationStep } from './SalaryCalculation/SalaryCalculation'; -import { SalaryCalculatorSectionEnum } from './SalaryCalculatorContext/Helper/sharedTypes'; import { useSalaryCalculator } from './SalaryCalculatorContext/SalaryCalculatorContext'; import { SummaryStep } from './Summary/Summary'; import { YourInformationStep } from './YourInformation/YourInformation'; export const CurrentStep: React.FC = () => { - const { currentStep } = useSalaryCalculator(); + const { currentIndex } = useSalaryCalculator(); + const { t } = useTranslation(); - switch (currentStep) { - case SalaryCalculatorSectionEnum.EffectiveDate: + switch (currentIndex) { + case 0: return ; - case SalaryCalculatorSectionEnum.YourInformation: + case 1: return ; - case SalaryCalculatorSectionEnum.SalaryCalculation: - return ; - case SalaryCalculatorSectionEnum.Summary: + case 2: + return {t('Salary Calculation')}; + case 3: return ; - case SalaryCalculatorSectionEnum.Receipt: + case 4: return ; + default: + return null; } }; diff --git a/src/components/Reports/SalaryCalculator/SalaryCalculatorContext/SalaryCalculatorContext.tsx b/src/components/Reports/SalaryCalculator/SalaryCalculatorContext/SalaryCalculatorContext.tsx index ad43e5b6fb..3e2fcdc3f5 100644 --- a/src/components/Reports/SalaryCalculator/SalaryCalculatorContext/SalaryCalculatorContext.tsx +++ b/src/components/Reports/SalaryCalculator/SalaryCalculatorContext/SalaryCalculatorContext.tsx @@ -7,6 +7,7 @@ import React, { useMemo, useState, } from 'react'; +import { Box, CircularProgress } from '@mui/material'; import { useStepList } from 'src/hooks/useStepList'; import { FormEnum } from '../../Shared/CalculationReports/Shared/sharedTypes'; import { Steps } from '../../Shared/CalculationReports/StepsList/StepsList'; @@ -27,7 +28,6 @@ export interface SalaryCalculatorContextType { currentIndex: number; percentComplete: number; - currentStep: SalaryCalculatorSectionEnum; handleNextStep: () => void; handlePreviousStep: () => void; @@ -58,33 +58,17 @@ interface SalaryCalculatorContextProps { children?: React.ReactNode; } -const objects = Object.values(SalaryCalculatorSectionEnum); - export const SalaryCalculatorProvider: React.FC< SalaryCalculatorContextProps > = ({ children }) => { - const { steps, nextStep, previousStep, currentIndex, percentComplete } = - useStepList(FormEnum.SalaryCalc); - - // Step Handlers - const [currentStep, setCurrentStep] = useState( - SalaryCalculatorSectionEnum.EffectiveDate, - ); - - const handleNextStep = useCallback(() => { - const next = objects[currentIndex + 1]; - nextStep(); - - setCurrentStep(next); - }, [currentIndex, objects, nextStep]); - - const handlePreviousStep = useCallback(() => { - const next = objects[currentIndex - 1]; - previousStep(); - - setCurrentStep(next); - }, [currentIndex, objects, previousStep]); - // End Step Handlers + const { + steps, + nextStep, + previousStep, + currentIndex, + percentComplete, + isLoading, + } = useStepList(FormEnum.SalaryCalc); const [isDrawerOpen, setDrawerOpen] = useState(true); const { data: hcmData } = useHcmQuery(); @@ -99,9 +83,8 @@ export const SalaryCalculatorProvider: React.FC< steps, currentIndex, percentComplete, - currentStep, - handleNextStep, - handlePreviousStep, + handleNextStep: nextStep, + handlePreviousStep: previousStep, isDrawerOpen, setDrawerOpen, toggleDrawer, @@ -114,9 +97,8 @@ export const SalaryCalculatorProvider: React.FC< steps, currentIndex, percentComplete, - currentStep, - handleNextStep, - handlePreviousStep, + nextStep, + previousStep, isDrawerOpen, toggleDrawer, hcmData, @@ -124,6 +106,19 @@ export const SalaryCalculatorProvider: React.FC< ], ); + if (isLoading || !calculationData) { + return ( + + + + ); + } + return ( {children} diff --git a/src/components/Reports/Shared/CalculationReports/CustomAutosave/Helper/formatHelper.ts b/src/components/Reports/Shared/CalculationReports/CustomAutosave/Helper/formatHelper.ts deleted file mode 100644 index 52f3b1a02f..0000000000 --- a/src/components/Reports/Shared/CalculationReports/CustomAutosave/Helper/formatHelper.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { currencyFormat } from 'src/lib/intlFormat'; - -export const parseInput = (s: string) => { - const cleaned = s.replace(/[^\d.-]/g, ''); - if (cleaned === '' || cleaned === '-' || cleaned === '.') { - return undefined; - } - const n = Number(cleaned); - return Number.isFinite(n) ? n : undefined; -}; - -export const display = ( - isEditing: (name: string) => boolean, - name: string, - value: string, - currency: string, - locale: string, -) => { - return isEditing(name) - ? value - ? String(value) - : '' - : value - ? currencyFormat(Number(value), currency, locale, { - showTrailingZeros: true, - }) - : ''; -}; diff --git a/src/components/Reports/Shared/CalculationReports/StatusCard/StatusCard.tsx b/src/components/Reports/Shared/CalculationReports/StatusCard/StatusCard.tsx index 9baa2fc596..be1a292d88 100644 --- a/src/components/Reports/Shared/CalculationReports/StatusCard/StatusCard.tsx +++ b/src/components/Reports/Shared/CalculationReports/StatusCard/StatusCard.tsx @@ -37,6 +37,7 @@ interface StatusCardProps { hideActions?: boolean; handleDownload?: () => void; handleConfirmCancel: () => void; + handleLinkTwo?: () => void; styling?: SxProps; } @@ -56,6 +57,7 @@ export const StatusCard: React.FC = ({ hideActions, handleDownload, handleConfirmCancel, + handleLinkTwo, styling, }) => { const { t } = useTranslation(); @@ -119,8 +121,9 @@ export const StatusCard: React.FC = ({ {linkOneText} )} + {previousApprovedRequest && ( + + + + )} )} - - {previousApprovedRequest && ( - - - - )} } /> diff --git a/src/components/Reports/MinisterHousingAllowance/RequestPage/RequestPage.tsx b/src/components/Reports/MinisterHousingAllowance/RequestPage/RequestPage.tsx index b392cb0ac5..786884725b 100644 --- a/src/components/Reports/MinisterHousingAllowance/RequestPage/RequestPage.tsx +++ b/src/components/Reports/MinisterHousingAllowance/RequestPage/RequestPage.tsx @@ -67,6 +67,9 @@ export const RequestPage: React.FC = () => { const iconPanelItems = useIconPanelItems(isDrawerOpen, toggleDrawer); + const editLink = getRequestUrl(accountListId, requestId, 'edit'); + const viewLink = getRequestUrl(accountListId, requestId, 'view'); + if (loading) { return ; } @@ -136,9 +139,9 @@ export const RequestPage: React.FC = () => { { ), ); }); + + it('should not show snackbar if all values are null', async () => { + const { result } = renderHook( + () => + useSaveField({ + formValues: { rentalValue: 50 }, + }), + { + wrapper: TestComponent, + }, + ); + + result.current({ rentalValue: null }); + + await waitFor(() => + expect(mutationSpy).toHaveGraphqlOperation( + 'UpdateMinistryHousingAllowanceRequest', + { + input: { + requestId: 'request-id', + requestAttributes: { + rentalValue: null, + }, + }, + }, + ), + ); + + expect(mockEnqueue).not.toHaveBeenCalled(); + }); }); diff --git a/src/components/Reports/MinisterHousingAllowance/Shared/AutoSave/useSaveField.ts b/src/components/Reports/MinisterHousingAllowance/Shared/AutoSave/useSaveField.ts index 15e3b1cc0e..ff36be9ab5 100644 --- a/src/components/Reports/MinisterHousingAllowance/Shared/AutoSave/useSaveField.ts +++ b/src/components/Reports/MinisterHousingAllowance/Shared/AutoSave/useSaveField.ts @@ -17,9 +17,7 @@ export const useSaveField = ({ formValues }: UseSaveFieldOptions) => { const { requestData } = useMinisterHousingAllowance(); const [updateMinistryHousingAllowanceRequest] = - useUpdateMinistryHousingAllowanceRequestMutation({ - refetchQueries: ['MinistryHousingAllowanceRequest'], - }); + useUpdateMinistryHousingAllowanceRequestMutation({}); const values = requestData?.requestAttributes; const saveField = useCallback( @@ -62,7 +60,12 @@ export const useSaveField = ({ formValues }: UseSaveFieldOptions) => { }, }, onCompleted: () => { - enqueueSnackbar(t('Saved successfully'), { variant: 'success' }); + const hasValue = Object.values(attributes).some( + (value) => value !== null, + ); + if (hasValue) { + enqueueSnackbar(t('Saved successfully'), { variant: 'success' }); + } }, }); }, diff --git a/src/components/Reports/MinisterHousingAllowance/SharedComponents/CurrentBoardApproved.tsx b/src/components/Reports/MinisterHousingAllowance/SharedComponents/CurrentBoardApproved.tsx index 5ff649ebe7..3a43f25388 100644 --- a/src/components/Reports/MinisterHousingAllowance/SharedComponents/CurrentBoardApproved.tsx +++ b/src/components/Reports/MinisterHousingAllowance/SharedComponents/CurrentBoardApproved.tsx @@ -35,12 +35,9 @@ export const CurrentBoardApproved: React.FC = ({ const router = useRouter(); const currency = 'USD'; - const [duplicateMHA] = useDuplicateMinistryHousingAllowanceRequestMutation({ - refetchQueries: [ - 'MinistryHousingAllowanceRequests', - 'MinistryHousingAllowanceRequest', - ], - }); + const [duplicateMHA] = useDuplicateMinistryHousingAllowanceRequestMutation( + {}, + ); const { isMarried, preferredName, spousePreferredName } = useMinisterHousingAllowance(); @@ -51,6 +48,8 @@ export const CurrentBoardApproved: React.FC = ({ const lastUpdated = request?.updatedAt ?? null; + const viewLink = getRequestUrl(accountListId, requestId, 'view'); + const handleDuplicateRequest = async () => { if (!requestId) { return; @@ -69,9 +68,7 @@ export const CurrentBoardApproved: React.FC = ({ ?.ministryHousingAllowanceRequest.id; if (newRequestId) { - router.push( - `/accountLists/${accountListId}/reports/housingAllowance/${newRequestId}/edit`, - ); + router.push(getRequestUrl(accountListId, newRequestId, 'edit')); } }; @@ -83,7 +80,7 @@ export const CurrentBoardApproved: React.FC = ({ icon={HomeSharp} iconColor="success.main" linkOneText={t('View Current MHA')} - linkOne={getRequestUrl(accountListId, requestId, 'view')} + linkOne={viewLink} linkTwoText={t('Update Current MHA')} handleLinkTwo={handleDuplicateRequest} isRequest={false} diff --git a/src/components/Reports/MinisterHousingAllowance/SharedComponents/CurrentRequest.test.tsx b/src/components/Reports/MinisterHousingAllowance/SharedComponents/CurrentRequest.test.tsx index 9d370c9dda..6421e070f5 100644 --- a/src/components/Reports/MinisterHousingAllowance/SharedComponents/CurrentRequest.test.tsx +++ b/src/components/Reports/MinisterHousingAllowance/SharedComponents/CurrentRequest.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { ThemeProvider } from '@mui/material/styles'; -import { render, waitFor } from '@testing-library/react'; +import { render, screen, waitFor } from '@testing-library/react'; import { SnackbarProvider } from 'notistack'; import TestRouter from '__tests__/util/TestRouter'; import { GqlMockedProvider } from '__tests__/util/graphqlMocking'; @@ -56,22 +56,26 @@ const TestComponent: React.FC = () => { describe('CurrentRequest Component', () => { it('should render correctly', () => { - const { getByText } = render(); + const { getByText, queryByText } = render(); expect(getByText('Current MHA Request')).toBeInTheDocument(); expect(getByText('View Request')).toBeInTheDocument(); - expect(getByText('Edit Request')).toBeInTheDocument(); + + expect( + getByText(/this request is still pending board approval/i), + ).toBeInTheDocument(); + expect(queryByText('Edit Request')).not.toBeInTheDocument(); expect(getByText('$15,000.00')).toBeInTheDocument(); - expect(getByText(/Requested on/i)).toBeInTheDocument(); - expect(getByText(/Oct 1, 2019/i)).toBeInTheDocument(); - expect(getByText(/Deadline for changes/i)).toBeInTheDocument(); - expect(getByText(/Oct 23, 2019/i)).toBeInTheDocument(); - expect(getByText(/Board Approval on/i)).toBeInTheDocument(); - expect(getByText(/Oct 30, 2019/i)).toBeInTheDocument(); - expect(getByText(/MHA Available on/i)).toBeInTheDocument(); - expect(getByText(/Nov 20, 2019/i)).toBeInTheDocument(); + screen.logTestingPlaygroundURL(); + + expect(getByText(/Requested on: Oct 1, 2019/i)).toBeInTheDocument(); + expect( + getByText(/Deadline for changes: Oct 23, 2019/i), + ).toBeInTheDocument(); + expect(getByText(/Board Approval on: Oct 30, 2019/i)).toBeInTheDocument(); + expect(getByText(/MHA Available on: Nov 20, 2019/i)).toBeInTheDocument(); }); it('should call delete mutation on cancel request', async () => { diff --git a/src/components/Reports/MinisterHousingAllowance/SharedComponents/CurrentRequest.tsx b/src/components/Reports/MinisterHousingAllowance/SharedComponents/CurrentRequest.tsx index a195618de1..73e2241144 100644 --- a/src/components/Reports/MinisterHousingAllowance/SharedComponents/CurrentRequest.tsx +++ b/src/components/Reports/MinisterHousingAllowance/SharedComponents/CurrentRequest.tsx @@ -7,7 +7,7 @@ import { TimelineItem, TimelineSeparator, } from '@mui/lab'; -import { Box, Typography } from '@mui/material'; +import { Alert, Box, Typography } from '@mui/material'; import { DateTime } from 'luxon'; import { useSnackbar } from 'notistack'; import { useTranslation } from 'react-i18next'; @@ -35,6 +35,16 @@ export const CurrentRequest: React.FC = ({ request }) => { const { status, requestAttributes } = request; + const canEdit = + status === MhaStatusEnum.InProgress || + status === MhaStatusEnum.ActionRequired; + const approved = + status === MhaStatusEnum.HrApproved || + status === MhaStatusEnum.BoardApproved; + + const editLink = getRequestUrl(accountListId, requestId, 'edit'); + const viewLink = getRequestUrl(accountListId, requestId, 'view'); + const { boardApprovedAt, deadlineDate, @@ -43,12 +53,14 @@ export const CurrentRequest: React.FC = ({ request }) => { approvedOverallAmount, } = requestAttributes || {}; + const pastDeadlineDate = deadlineDate + ? DateTime.fromISO(deadlineDate) < DateTime.now() + : false; + const hideEditButton = !canEdit || pastDeadlineDate; + const [deleteRequestMutation] = useDeleteMinistryHousingAllowanceRequestMutation({ - refetchQueries: [ - 'MinistryHousingAllowanceRequests', - 'MinistryHousingAllowanceRequest', - ], + refetchQueries: ['MinistryHousingAllowanceRequests'], awaitRefetchQueries: true, }); @@ -64,11 +76,7 @@ export const CurrentRequest: React.FC = ({ request }) => { enqueueSnackbar(t('MHA request cancelled successfully.'), { variant: 'success', }); - } catch (error) { - enqueueSnackbar(t('Failed to cancel your MHA request.'), { - variant: 'error', - }); - } + } catch (error) {} }; return ( @@ -78,13 +86,31 @@ export const CurrentRequest: React.FC = ({ request }) => { icon={AddHomeSharp} iconColor="warning.main" linkOneText={t('View Request')} - linkOne={getRequestUrl(accountListId, requestId, 'view')} + linkOne={viewLink} linkTwoText={t('Edit Request')} - linkTwo={getRequestUrl(accountListId, requestId, 'edit')} + linkTwo={editLink} + hideLinkTwoButton={hideEditButton} isRequest={true} handleConfirmCancel={handleCancelRequest} > + {status === MhaStatusEnum.Pending && ( + + {t( + 'This request is still pending board approval. You cannot make changes at this time.', + )} + + )} + {pastDeadlineDate && !approved && ( + + {t( + 'The deadline to make changes to this request was {{date}}. Please contact support if you need further assistance.', + { + date: dateFormat(DateTime.fromISO(deadlineDate ?? ''), locale), + }, + )} + + )} {currencyFormat(approvedOverallAmount || 0, currency, locale, { @@ -108,7 +134,9 @@ export const CurrentRequest: React.FC = ({ request }) => { @@ -130,7 +158,9 @@ export const CurrentRequest: React.FC = ({ request }) => { @@ -146,7 +176,9 @@ export const CurrentRequest: React.FC = ({ request }) => { @@ -162,7 +194,9 @@ export const CurrentRequest: React.FC = ({ request }) => { @@ -178,7 +212,9 @@ export const CurrentRequest: React.FC = ({ request }) => { diff --git a/src/components/Reports/Shared/CalculationReports/ReceiptStep/Receipt.tsx b/src/components/Reports/Shared/CalculationReports/ReceiptStep/Receipt.tsx index 4cbd9678a5..6ca5253c9d 100644 --- a/src/components/Reports/Shared/CalculationReports/ReceiptStep/Receipt.tsx +++ b/src/components/Reports/Shared/CalculationReports/ReceiptStep/Receipt.tsx @@ -46,7 +46,7 @@ export const Receipt: React.FC = ({ ? t(`approval effective ${available}`) : t('approval soon'); - const printLink = `${viewLink}?print=true`; + const printLink = `${viewLink}&print=true`; return ( diff --git a/src/components/Reports/Shared/CalculationReports/StatusCard/StatusCard.test.tsx b/src/components/Reports/Shared/CalculationReports/StatusCard/StatusCard.test.tsx index 26e4e37681..cd0f989201 100644 --- a/src/components/Reports/Shared/CalculationReports/StatusCard/StatusCard.test.tsx +++ b/src/components/Reports/Shared/CalculationReports/StatusCard/StatusCard.test.tsx @@ -30,6 +30,7 @@ interface TestComponentProps { subtitle?: string; isRequest?: boolean; hideDownload?: boolean; + hideLinkTwoButton?: boolean; hideActions?: boolean; linkOne?: string; linkTwo?: string; @@ -40,6 +41,7 @@ const TestComponent: React.FC = ({ isRequest, hideDownload = false, hideActions = false, + hideLinkTwoButton, linkOne, linkTwo, }) => { @@ -60,6 +62,7 @@ const TestComponent: React.FC = ({ isRequest={isRequest} hideDownload={hideDownload} hideActions={hideActions} + hideLinkTwoButton={hideLinkTwoButton} linkOne={linkOne} linkTwo={linkTwo} handleDownload={handleDownload} @@ -193,4 +196,10 @@ describe('CardSkeleton', () => { expect(queryByRole('dialog')).not.toBeInTheDocument(); }); + + it('should hide second button when hideLinkTwoButton is true', () => { + const { queryByText } = render(); + + expect(queryByText(titleTwo)).not.toBeInTheDocument(); + }); }); diff --git a/src/components/Reports/Shared/CalculationReports/StatusCard/StatusCard.tsx b/src/components/Reports/Shared/CalculationReports/StatusCard/StatusCard.tsx index 5743a98136..e4d137c639 100644 --- a/src/components/Reports/Shared/CalculationReports/StatusCard/StatusCard.tsx +++ b/src/components/Reports/Shared/CalculationReports/StatusCard/StatusCard.tsx @@ -18,9 +18,6 @@ import { import { useTranslation } from 'react-i18next'; import { SubmitModal } from '../SubmitModal/SubmitModal'; -//TODO: handle cancel request -//TODO: handle duplicate last years mha and view current mha links - interface StatusCardProps { formType: string; title: string; @@ -35,6 +32,7 @@ interface StatusCardProps { isRequest?: boolean; hideDownload?: boolean; hideActions?: boolean; + hideLinkTwoButton?: boolean; handleDownload?: () => void; handleConfirmCancel: () => void; handleLinkTwo?: () => void; @@ -55,6 +53,7 @@ export const StatusCard: React.FC = ({ isRequest, hideDownload, hideActions, + hideLinkTwoButton, handleDownload, handleConfirmCancel, handleLinkTwo, @@ -120,15 +119,17 @@ export const StatusCard: React.FC = ({ > {linkOneText} - + {!hideLinkTwoButton && ( + + )} {isRequest && (