diff --git a/pages/accountLists/[accountListId]/reports/housingAllowance/[requestId].page.tsx b/pages/accountLists/[accountListId]/reports/housingAllowance/[requestId].page.tsx index ff93fe8a63..5200730408 100644 --- a/pages/accountLists/[accountListId]/reports/housingAllowance/[requestId].page.tsx +++ b/pages/accountLists/[accountListId]/reports/housingAllowance/[requestId].page.tsx @@ -26,6 +26,7 @@ const RequestPageWrapper = styled(Box)(({ theme }) => ({ const HousingAllowanceRequestPage: React.FC = () => { const { t } = useTranslation(); const router = useRouter(); + const [isNavListOpen, setIsNavListOpen] = useState(false); const { requestId, mode } = router.query; if (!requestId) { @@ -54,8 +55,6 @@ const HousingAllowanceRequestPage: React.FC = () => { mode: pageType, }); - const [isNavListOpen, setIsNavListOpen] = useState(false); - const handleNavListToggle = () => { setIsNavListOpen(!isNavListOpen); }; diff --git a/pages/accountLists/[accountListId]/reports/salaryCalculator/salaryCalculator.page.test.tsx b/pages/accountLists/[accountListId]/reports/salaryCalculator/salaryCalculator.page.test.tsx index bec27e1f71..336a8149d3 100644 --- a/pages/accountLists/[accountListId]/reports/salaryCalculator/salaryCalculator.page.test.tsx +++ b/pages/accountLists/[accountListId]/reports/salaryCalculator/salaryCalculator.page.test.tsx @@ -12,10 +12,10 @@ const TestComponent = () => ( ); describe('SalaryCalculatorPage', () => { - it('renders the Salary Calculator header', () => { - const { getByRole } = render(); + it('renders the Salary Calculator header', async () => { + const { findByRole } = render(); expect( - getByRole('heading', { name: /Salary Calculator/i }), + await findByRole('heading', { name: /Salary Calculator/i }), ).toBeInTheDocument(); }); diff --git a/src/components/Reports/AdditionalSalaryRequest/AboutForm/AboutForm.tsx b/src/components/Reports/AdditionalSalaryRequest/AboutForm/AboutForm.tsx index 64f57dadc3..961b901a65 100644 --- a/src/components/Reports/AdditionalSalaryRequest/AboutForm/AboutForm.tsx +++ b/src/components/Reports/AdditionalSalaryRequest/AboutForm/AboutForm.tsx @@ -9,7 +9,7 @@ import { AdditionalSalaryRequestSection } from '../SharedComponents/AdditionalSa import { SpouseComponent } from '../SharedComponents/SpouseComponent'; export const AboutForm: React.FC = () => { - const { currentStep } = useAdditionalSalaryRequest(); + const { currentIndex } = useAdditionalSalaryRequest(); const { t } = useTranslation(); const theme = useTheme(); @@ -20,7 +20,7 @@ export const AboutForm: React.FC = () => { const remainingAllowableSalary = 17500.0; return ( - + You can use this form to electronically submit additional salary diff --git a/src/components/Reports/AdditionalSalaryRequest/AdditionalSalaryRequest.tsx b/src/components/Reports/AdditionalSalaryRequest/AdditionalSalaryRequest.tsx index 31937368de..57fd374ee4 100644 --- a/src/components/Reports/AdditionalSalaryRequest/AdditionalSalaryRequest.tsx +++ b/src/components/Reports/AdditionalSalaryRequest/AdditionalSalaryRequest.tsx @@ -74,13 +74,15 @@ export const AdditionalSalaryRequest: React.FC = () => { const { isDrawerOpen, toggleDrawer, steps, currentIndex, percentComplete } = useAdditionalSalaryRequest(); + const iconPanelItems = useIconPanelItems(isDrawerOpen, toggleDrawer); + return ( } sidebarTitle={t('Additional Salary Request')} isSidebarOpen={isDrawerOpen} diff --git a/src/components/Reports/AdditionalSalaryRequest/CompleteForm/CompleteForm.tsx b/src/components/Reports/AdditionalSalaryRequest/CompleteForm/CompleteForm.tsx index c3dcd92222..d39320a123 100644 --- a/src/components/Reports/AdditionalSalaryRequest/CompleteForm/CompleteForm.tsx +++ b/src/components/Reports/AdditionalSalaryRequest/CompleteForm/CompleteForm.tsx @@ -13,7 +13,7 @@ import { NetAdditionalSalary } from './NetAdditionalSalary/NetAdditionalSalary'; export const CompleteForm: React.FC = () => { const { t } = useTranslation(); - const { currentStep } = useAdditionalSalaryRequest(); + const { currentIndex } = useAdditionalSalaryRequest(); const theme = useTheme(); const name = 'Doc, John'; @@ -22,7 +22,7 @@ export const CompleteForm: React.FC = () => { const remainingAllowableSalary = 17500.0; return ( - + { - const { currentStep } = useAdditionalSalaryRequest(); + const { currentIndex } = useAdditionalSalaryRequest(); const { t } = useTranslation(); const accountListId = useAccountListId(); const pageLink = `/accountLists/${accountListId}/reports/additionalSalaryRequest`; - switch (currentStep) { - case AdditionalSalaryRequestSectionEnum.AboutForm: - return ; - case AdditionalSalaryRequestSectionEnum.CompleteForm: - return ; - case AdditionalSalaryRequestSectionEnum.Receipt: - return ( - - - - ); - } + const steps = [ + , + , + + + , + ]; + + return steps[currentIndex] ?? null; }; diff --git a/src/components/Reports/AdditionalSalaryRequest/Shared/AdditionalSalaryRequestContext.test.tsx b/src/components/Reports/AdditionalSalaryRequest/Shared/AdditionalSalaryRequestContext.test.tsx index 6d4d476978..21d66fb800 100644 --- a/src/components/Reports/AdditionalSalaryRequest/Shared/AdditionalSalaryRequestContext.test.tsx +++ b/src/components/Reports/AdditionalSalaryRequest/Shared/AdditionalSalaryRequestContext.test.tsx @@ -7,13 +7,13 @@ import { useAdditionalSalaryRequest } from './AdditionalSalaryRequestContext'; import { getHeader } from './Helper/getHeader'; const TestComponent: React.FC = () => { - const { currentStep, handleNextStep, isDrawerOpen, toggleDrawer } = + const { currentIndex, handleNextStep, isDrawerOpen, toggleDrawer } = useAdditionalSalaryRequest(); const { t } = useTranslation(); return (
-

{getHeader(t, currentStep)}

+

{getHeader(t, currentIndex)}

Drawer: {isDrawerOpen ? 'open' : 'closed'}
diff --git a/src/components/Reports/AdditionalSalaryRequest/Shared/AdditionalSalaryRequestContext.tsx b/src/components/Reports/AdditionalSalaryRequest/Shared/AdditionalSalaryRequestContext.tsx index 0a1cbebaef..0ddaa23ee4 100644 --- a/src/components/Reports/AdditionalSalaryRequest/Shared/AdditionalSalaryRequestContext.tsx +++ b/src/components/Reports/AdditionalSalaryRequest/Shared/AdditionalSalaryRequestContext.tsx @@ -9,14 +9,12 @@ import { amount } from 'src/lib/yupHelpers'; import { FormEnum } from '../../Shared/CalculationReports/Shared/sharedTypes'; import { Steps } from '../../Shared/CalculationReports/StepsList/StepsList'; import { CompleteFormValues } from '../AdditionalSalaryRequest'; -import { AdditionalSalaryRequestSectionEnum } from '../AdditionalSalaryRequestHelper'; import { calculateCompletionPercentage } from './calculateCompletionPercentage'; export type AdditionalSalaryRequestType = { steps: Steps[]; currentIndex: number; percentComplete: number; - currentStep: AdditionalSalaryRequestSectionEnum; handleNextStep: () => void; handlePreviousStep: () => void; isDrawerOpen: boolean; @@ -43,16 +41,13 @@ interface Props { initialValues?: CompleteFormValues; } -const objects = Object.values(AdditionalSalaryRequestSectionEnum); - export const AdditionalSalaryRequestProvider: React.FC = ({ children, initialValues: providedInitialValues, }) => { const { t } = useTranslation(); - const { steps, nextStep, previousStep, currentIndex } = useStepList( - FormEnum.AdditionalSalary, - ); + const { steps, handleNextStep, handlePreviousStep, currentIndex } = + useStepList(FormEnum.AdditionalSalary); const locale = useLocale(); const createCurrencyValidation = useCallback( @@ -132,25 +127,6 @@ export const AdditionalSalaryRequestProvider: React.FC = ({ [createCurrencyValidation, t], ); - // Step Handlers - const [currentStep, setCurrentStep] = useState( - AdditionalSalaryRequestSectionEnum.AboutForm, - ); - - 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 [isDrawerOpen, setIsDrawerOpen] = useState(true); const toggleDrawer = useCallback(() => { setIsDrawerOpen((prev) => !prev); @@ -185,7 +161,6 @@ export const AdditionalSalaryRequestProvider: React.FC = ({ steps, currentIndex, percentComplete, - currentStep, handleNextStep, handlePreviousStep, isDrawerOpen, @@ -197,11 +172,12 @@ export const AdditionalSalaryRequestProvider: React.FC = ({ steps, currentIndex, percentComplete, - currentStep, handleNextStep, handlePreviousStep, isDrawerOpen, toggleDrawer, + setIsDrawerOpen, + handleCancel, ], ); diff --git a/src/components/Reports/AdditionalSalaryRequest/Shared/Helper/getHeader.ts b/src/components/Reports/AdditionalSalaryRequest/Shared/Helper/getHeader.ts index 73dd4e090b..caabb3d773 100644 --- a/src/components/Reports/AdditionalSalaryRequest/Shared/Helper/getHeader.ts +++ b/src/components/Reports/AdditionalSalaryRequest/Shared/Helper/getHeader.ts @@ -1,16 +1,14 @@ import { TFunction } from 'i18next'; -import { AdditionalSalaryRequestSectionEnum } from '../../AdditionalSalaryRequestHelper'; -export const getHeader = ( - t: TFunction, - step: AdditionalSalaryRequestSectionEnum, -): string => { +export const getHeader = (t: TFunction, step: number): string => { switch (step) { - case AdditionalSalaryRequestSectionEnum.AboutForm: + case 0: return 'About this Form'; - case AdditionalSalaryRequestSectionEnum.CompleteForm: + case 1: return 'Complete the Form'; - case AdditionalSalaryRequestSectionEnum.Receipt: + case 2: return 'Receipt'; + default: + return ''; } }; 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 2934360f02..6bc9659b6d 100644 --- a/src/components/Reports/MinisterHousingAllowance/MinisterHousingAllowance.graphql +++ b/src/components/Reports/MinisterHousingAllowance/MinisterHousingAllowance.graphql @@ -1,31 +1,36 @@ +fragment RequestAttributes on MhaRequestAttributes { + rentOrOwn + rentalValue + furnitureCostsOne + avgUtilityOne + mortgageOrRentPayment + furnitureCostsTwo + repairCosts + avgUtilityTwo + unexpectedExpenses + overallAmount + phoneNumber + emailAddress + iUnderstandMhaPolicy + hrApprovedAt + approvedOverallAmount + availableDate + boardApprovedAt + deadlineDate + spouseSpecific + staffSpecific + submittedAt + changesRequestedAt +} + query MinistryHousingAllowanceRequests { ministryHousingAllowanceRequests { nodes { id personNumber + updatedAt requestAttributes { - rentOrOwn - rentalValue - furnitureCostsOne - avgUtilityOne - mortgageOrRentPayment - furnitureCostsTwo - repairCosts - avgUtilityTwo - unexpectedExpenses - overallAmount - phoneNumber - emailAddress - iUnderstandMhaPolicy - hrApprovedAt - approvedOverallAmount - availableDate - boardApprovedAt - deadlineDate - spouseSpecific - staffSpecific - submittedAt - changesRequestedAt + ...RequestAttributes } status user { @@ -42,29 +47,9 @@ query MinistryHousingAllowanceRequest($ministryHousingAllowanceRequestId: ID!) { ministryHousingAllowanceRequest(id: $ministryHousingAllowanceRequestId) { id personNumber + updatedAt requestAttributes { - rentOrOwn - rentalValue - furnitureCostsOne - avgUtilityOne - mortgageOrRentPayment - furnitureCostsTwo - repairCosts - avgUtilityTwo - unexpectedExpenses - overallAmount - phoneNumber - emailAddress - iUnderstandMhaPolicy - hrApprovedAt - approvedOverallAmount - availableDate - boardApprovedAt - deadlineDate - spouseSpecific - staffSpecific - submittedAt - changesRequestedAt + ...RequestAttributes } status user { @@ -87,10 +72,7 @@ mutation CreateHousingAllowanceRequest( status personNumber requestAttributes { - rentOrOwn - rentalValue - furnitureCostsOne - avgUtilityOne + ...RequestAttributes } user { id @@ -108,19 +90,40 @@ mutation UpdateMinistryHousingAllowanceRequest( ministryHousingAllowanceRequest { id requestAttributes { - rentOrOwn - rentalValue - furnitureCostsOne - avgUtilityOne - mortgageOrRentPayment - furnitureCostsTwo - repairCosts - avgUtilityTwo - unexpectedExpenses - overallAmount - phoneNumber - emailAddress - iUnderstandMhaPolicy + ...RequestAttributes + } + } + } +} + +mutation DeleteMinistryHousingAllowanceRequest( + $input: MinistryHousingAllowanceRequestDeleteMutationInput! +) { + deleteMinistryHousingAllowanceRequest(input: $input) { + id + } +} + +mutation SubmitMinistryHousingAllowanceRequest( + $input: MinistryHousingAllowanceRequestSubmitMutationInput! +) { + submitMinistryHousingAllowanceRequest(input: $input) { + ministryHousingAllowanceRequest { + requestAttributes { + ...RequestAttributes + } + } + } +} + +mutation DuplicateMinistryHousingAllowanceRequest( + $input: DuplicateMinistryHousingAllowanceRequestMutationInput! +) { + duplicateMinistryHousingAllowanceRequest(input: $input) { + ministryHousingAllowanceRequest { + id + requestAttributes { + ...RequestAttributes } } } diff --git a/src/components/Reports/MinisterHousingAllowance/MinisterHousingAllowance.tsx b/src/components/Reports/MinisterHousingAllowance/MinisterHousingAllowance.tsx index 9e2507e5eb..87e4fa14e6 100644 --- a/src/components/Reports/MinisterHousingAllowance/MinisterHousingAllowance.tsx +++ b/src/components/Reports/MinisterHousingAllowance/MinisterHousingAllowance.tsx @@ -29,8 +29,11 @@ export const MinisterHousingAllowanceReport = () => { const { enqueueSnackbar } = useSnackbar(); const accountListId = useAccountListId(); - const { data, error: requestsError } = - useMinistryHousingAllowanceRequestsQuery(); + const { + data, + error: requestsError, + loading, + } = useMinistryHousingAllowanceRequestsQuery(); const requests = data?.ministryHousingAllowanceRequests.nodes ?? []; const { @@ -58,9 +61,11 @@ export const MinisterHousingAllowanceReport = () => { const onCreateMHARequest = async () => { await createMHA({ variables: { - requestAttributes: {}, + requestAttributes: { + phoneNumber: userHcmData?.staffInfo.primaryPhoneNumber, + emailAddress: userHcmData?.staffInfo.emailAddress, + }, }, - refetchQueries: ['MinistryHousingAllowanceRequests'], onCompleted: ({ createMinistryHousingAllowanceRequest: newRequest }) => { enqueueSnackbar( t("Successfully created MHA Request. You'll be redirected shortly."), @@ -107,6 +112,7 @@ export const MinisterHousingAllowanceReport = () => { request.status === MhaStatusEnum.BoardApproved && isCurrentRequestPending, ); + return ( { {requestsError ? ( - ) : !requests ? ( + ) : loading ? ( ) : ( <> @@ -146,14 +152,13 @@ export const MinisterHousingAllowanceReport = () => { {t('Request New MHA')} )} + {previousApprovedRequest && ( + + + + )} )} - - {previousApprovedRequest && ( - - - - )} } /> diff --git a/src/components/Reports/MinisterHousingAllowance/MinisterHousingAllowanceSkeleton.tsx b/src/components/Reports/MinisterHousingAllowance/MinisterHousingAllowanceSkeleton.tsx index 2fa037b925..40271ba4de 100644 --- a/src/components/Reports/MinisterHousingAllowance/MinisterHousingAllowanceSkeleton.tsx +++ b/src/components/Reports/MinisterHousingAllowance/MinisterHousingAllowanceSkeleton.tsx @@ -6,7 +6,7 @@ export const MinisterHousingAllowanceReportSkeleton: React.FC = () => { return ( <> { }} /> { }} /> { }} /> ; + mocks?: { + HcmData?: HcmDataQuery; + MinistryHousingAllowanceRequest?: MinistryHousingAllowanceRequestQuery; + UpdateMinistryHousingAllowanceRequest?: UpdateMinistryHousingAllowanceRequestMutation; + }; } const TestComponent: React.FC = ({ type, contextValue, + mocks, }) => { const content = contextValue ? ( = ({ return ( - - onCall={mutationSpy} - > - {content} - + + + mocks={mocks} + onCall={mutationSpy} + > + {content} + + ); }; describe('RequestPage', () => { - it('renders steps list', () => { - const { getByText } = render(); - - expect(getByText(/1. about this form/i)).toBeInTheDocument(); + it('renders steps list', async () => { + const { getByText, findByText } = render( + , + ); + + expect(await findByText(/1. about this form/i)).toBeInTheDocument(); expect(getByText(/2. rent or own?/i)).toBeInTheDocument(); expect(getByText(/3. edit your mha/i)).toBeInTheDocument(); expect(getByText(/4. receipt/i)).toBeInTheDocument(); }); describe('Edit Page', () => { - it('starts on step 2 and updates steps when Continue clicked', () => { - const { getByRole, getAllByRole, getByText, queryByTestId } = render( - , - ); + it('starts on step 2 and updates steps when Continue clicked', async () => { + const { getByRole, getAllByRole, getByText, queryByTestId, findByRole } = + render( + , + ); - expect(getByRole('progressbar')).toHaveAttribute('aria-valuenow', '50'); + expect(await findByRole('progressbar')).toHaveAttribute( + 'aria-valuenow', + '50', + ); expect(queryByTestId('ArrowBackIcon')).toBeInTheDocument(); const continueButton = getByRole('button', { name: 'Continue' }); userEvent.click(continueButton); - const steps = getAllByRole('listitem'); + const stepItems = getAllByRole('listitem'); - const [firstStep, secondStep, thirdStep] = steps; + const [firstStep, secondStep, thirdStep] = stepItems; expect(firstStep).toHaveTextContent('1. About this Form'); expect( @@ -141,17 +182,19 @@ describe('RequestPage', () => { }); it('should show an option is preselected', async () => { - const { findAllByRole, getByRole, findByRole } = render( + const { findAllByRole, findByRole } = render( { />, ); - const continueButton = getByRole('button', { name: 'Continue' }); + const continueButton = await findByRole('button', { name: 'Continue' }); userEvent.click(continueButton); expect(await findByRole('radio', { name: 'Rent' })).toBeChecked(); @@ -169,20 +212,22 @@ describe('RequestPage', () => { }); it('opens confirmation modal when changing selection', async () => { - const { getByRole, getByText, queryByText } = render( + const { getByRole, getByText, queryByText, findByRole } = render( { />, ); - const ownRadio = getByRole('radio', { name: 'Own' }); + const ownRadio = await findByRole('radio', { name: 'Own' }); userEvent.click(ownRadio); expect(ownRadio).not.toBeChecked(); @@ -215,11 +260,22 @@ describe('RequestPage', () => { describe('New Page', () => { it('updates steps when Continue clicked', async () => { - const { getByRole, getAllByRole, getByText, queryByTestId } = render( - , - ); + const { getByRole, getAllByRole, getByText, queryByTestId, findByRole } = + render( + , + ); - expect(getByRole('progressbar')).toHaveAttribute('aria-valuenow', '25'); + expect(await findByRole('progressbar')).toHaveAttribute( + 'aria-valuenow', + '25', + ); expect(queryByTestId('ArrowBackIcon')).toBeInTheDocument(); const continueButton = getByRole('button', { name: 'Continue' }); @@ -272,11 +328,18 @@ describe('RequestPage', () => { it('should show validation error if continue is clicked without selecting an option', async () => { const { getByRole, findByRole } = render( , ); @@ -299,15 +362,17 @@ describe('RequestPage', () => { { pageType: PageEnum.New, steps, - currentStep: StepsEnum.RentOrOwn, + currentIndex: 1, handleNextStep, handlePreviousStep, hasCalcValues: true, setHasCalcValues, updateMutation, setIsPrint, + userEligibleForMHA: true, requestData: { id: 'request-id', + status: MhaStatusEnum.InProgress, requestAttributes: { rentOrOwn: null, rentalValue: 1000, @@ -349,7 +414,7 @@ describe('RequestPage', () => { describe('View Page', () => { it('renders empty panel layout,', () => { - const { getByText, queryByRole, queryByTestId } = render( + const { getByText, queryByRole, getByTestId } = render( { expect(getByText('Your MHA')).toBeInTheDocument(); expect(queryByRole('progressbar')).not.toBeInTheDocument(); - expect(queryByTestId('ArrowBackIcon')).not.toBeInTheDocument(); + expect(getByTestId('ArrowBackIcon')).toBeInTheDocument(); }); it('should have disabled text fields', () => { @@ -383,4 +448,31 @@ describe('RequestPage', () => { expect(input).toBeDisabled(); }); }); + + describe('Permission Denied', () => { + it('renders permission denied layout', () => { + const { getByText, queryByRole, getByTestId } = render( + , + ); + + expect(getByText('Your MHA')).toBeInTheDocument(); + expect(queryByRole('progressbar')).not.toBeInTheDocument(); + expect(getByTestId('ArrowBackIcon')).toBeInTheDocument(); + expect( + getByText('You do not have permission to edit this request.'), + ).toBeInTheDocument(); + }); + }); }); diff --git a/src/components/Reports/MinisterHousingAllowance/RequestPage/RequestPage.tsx b/src/components/Reports/MinisterHousingAllowance/RequestPage/RequestPage.tsx index fbad31c8a9..59ffc4418c 100644 --- a/src/components/Reports/MinisterHousingAllowance/RequestPage/RequestPage.tsx +++ b/src/components/Reports/MinisterHousingAllowance/RequestPage/RequestPage.tsx @@ -2,9 +2,11 @@ import { Container, Stack } from '@mui/material'; import { Formik } from 'formik'; import { useTranslation } from 'react-i18next'; import * as yup from 'yup'; -import { MhaRentOrOwnEnum } from 'src/graphql/types.generated'; +import Loading from 'src/components/Loading/Loading'; +import { MhaRentOrOwnEnum, MhaStatusEnum } from 'src/graphql/types.generated'; import { useAccountListId } from 'src/hooks/useAccountListId'; import i18n from 'src/lib/i18n'; +import theme from 'src/theme'; import { PanelLayout } from '../../Shared/CalculationReports/PanelLayout/PanelLayout'; import { useIconPanelItems } from '../../Shared/CalculationReports/PanelLayout/useIconPanelItems'; import { Receipt } from '../../Shared/CalculationReports/ReceiptStep/Receipt'; @@ -17,11 +19,14 @@ import { mainContentWidth } from '../MinisterHousingAllowance'; import { useMinisterHousingAllowance } from '../Shared/Context/MinisterHousingAllowanceContext'; import { getRequestUrl } from '../Shared/Helper/getRequestUrl'; import { mocks } from '../Shared/mockData'; -import { StepsEnum } from '../Shared/sharedTypes'; +import { NoEditAccess } from '../Steps/NoEditAccess/NoEditAccess'; +import { NoRequestAccess } from '../Steps/NoRequestAccess/NoRequestAccess'; import { AboutForm } from '../Steps/StepOne/AboutForm'; import { Calculation } from '../Steps/StepThree/Calculation'; import { RentOwn } from '../Steps/StepTwo/RentOwn'; +const permissionDeniedWidth = theme.spacing(100); + export interface FormValues { rentOrOwn: MhaRentOrOwnEnum | undefined; } @@ -39,7 +44,6 @@ export const RequestPage: React.FC = () => { requestId, steps, handleNextStep, - currentStep, pageType, isDrawerOpen, toggleDrawer, @@ -47,8 +51,15 @@ export const RequestPage: React.FC = () => { currentIndex, setIsComplete, requestData, + loading, + userEligibleForMHA, } = useMinisterHousingAllowance(); + const canEdit = + !requestData || + requestData.status === MhaStatusEnum.InProgress || + requestData.status === MhaStatusEnum.ActionRequired; + const request = requestData?.requestAttributes; const value = request?.rentOrOwn ?? undefined; @@ -65,12 +76,21 @@ export const RequestPage: React.FC = () => { const availableDate = mocks[4].mhaDetails.staffMHA?.availableDate ?? ''; const deadlineDate = mocks[4].mhaDetails.staffMHA?.deadlineDate ?? ''; + const iconPanelItems = useIconPanelItems(isDrawerOpen, toggleDrawer); + + const editLink = getRequestUrl(accountListId, requestId, 'edit'); + const viewLink = getRequestUrl(accountListId, requestId, 'view'); + + if (loading) { + return ; + } + return isView ? ( @@ -86,10 +106,38 @@ export const RequestPage: React.FC = () => { } /> + ) : !canEdit ? ( + + + + + + } + /> + ) : !userEligibleForMHA ? ( + + + + + + } + /> ) : ( { {({ values }) => ( - {currentStep === StepsEnum.AboutForm ? ( + {currentIndex === 0 && ( - ) : currentStep === StepsEnum.RentOrOwn ? ( - - ) : currentStep === StepsEnum.CalcForm ? ( + )} + {currentIndex === 1 && } + {currentIndex === 2 && ( - ) : currentStep === StepsEnum.Receipt ? ( + )} + {currentIndex === 3 && ( - ) : null} + )} )} diff --git a/src/components/Reports/MinisterHousingAllowance/Shared/AutoSave/AutosaveCustomTextField.test.tsx b/src/components/Reports/MinisterHousingAllowance/Shared/AutoSave/AutosaveCustomTextField.test.tsx index 9b42543a55..a9cc3be41e 100644 --- a/src/components/Reports/MinisterHousingAllowance/Shared/AutoSave/AutosaveCustomTextField.test.tsx +++ b/src/components/Reports/MinisterHousingAllowance/Shared/AutoSave/AutosaveCustomTextField.test.tsx @@ -3,6 +3,7 @@ import { ThemeProvider } from '@mui/material/styles'; import { render, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { Formik } from 'formik'; +import { SnackbarProvider } from 'notistack'; import * as yup from 'yup'; import { GqlMockedProvider } from '__tests__/util/graphqlMocking'; import { PageEnum } from 'src/components/Reports/Shared/CalculationReports/Shared/sharedTypes'; @@ -23,30 +24,32 @@ const defaultSchema = yup.object({ const TestComponent: React.FC = () => ( - - onCall={mutationSpy} - > - + + onCall={mutationSpy} > - - - - - + + + + + + + ); diff --git a/src/components/Reports/MinisterHousingAllowance/Shared/AutoSave/useSaveField.test.tsx b/src/components/Reports/MinisterHousingAllowance/Shared/AutoSave/useSaveField.test.tsx index be6389c04e..1b3c1c6097 100644 --- a/src/components/Reports/MinisterHousingAllowance/Shared/AutoSave/useSaveField.test.tsx +++ b/src/components/Reports/MinisterHousingAllowance/Shared/AutoSave/useSaveField.test.tsx @@ -1,5 +1,6 @@ import { ThemeProvider } from '@mui/material/styles'; import { renderHook, waitFor } from '@testing-library/react'; +import { SnackbarProvider } from 'notistack'; import { GqlMockedProvider } from '__tests__/util/graphqlMocking'; import { PageEnum } from 'src/components/Reports/Shared/CalculationReports/Shared/sharedTypes'; import theme from 'src/theme'; @@ -11,6 +12,18 @@ import { import { useSaveField } from './useSaveField'; const mutationSpy = jest.fn(); +const mockEnqueue = jest.fn(); + +jest.mock('notistack', () => ({ + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + ...jest.requireActual('notistack'), + useSnackbar: () => { + return { + enqueueSnackbar: mockEnqueue, + }; + }, +})); interface TestComponentProps { children: React.ReactNode; @@ -19,25 +32,27 @@ interface TestComponentProps { const TestComponent: React.FC = ({ children }) => { return ( - - onCall={mutationSpy} - > - + + onCall={mutationSpy} > - {children} - - + + {children} + + + ); }; @@ -69,5 +84,42 @@ describe('useSaveField', () => { }, ), ); + + await waitFor(() => + expect(mockEnqueue).toHaveBeenCalledWith( + expect.stringContaining('Saved successfully'), + { variant: 'success' }, + ), + ); + }); + + 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 09c5377632..4364c1d72a 100644 --- a/src/components/Reports/MinisterHousingAllowance/Shared/AutoSave/useSaveField.ts +++ b/src/components/Reports/MinisterHousingAllowance/Shared/AutoSave/useSaveField.ts @@ -1,4 +1,6 @@ import { useCallback } from 'react'; +import { useSnackbar } from 'notistack'; +import { useTranslation } from 'react-i18next'; import { MinistryHousingAllowanceRequestAttributesInput } from 'pages/api/graphql-rest.page.generated'; import { calculateAnnualTotals } from 'src/hooks/useAnnualTotal'; import { useUpdateMinistryHousingAllowanceRequestMutation } from '../../MinisterHousingAllowance.generated'; @@ -10,11 +12,12 @@ interface UseSaveFieldOptions { } export const useSaveField = ({ formValues }: UseSaveFieldOptions) => { + const { t } = useTranslation(); + const { enqueueSnackbar } = useSnackbar(); + const { requestData } = useMinisterHousingAllowance(); const [updateMinistryHousingAllowanceRequest] = - useUpdateMinistryHousingAllowanceRequestMutation({ - refetchQueries: ['MinistryHousingAllowanceRequest'], - }); + useUpdateMinistryHousingAllowanceRequestMutation({}); const values = requestData?.requestAttributes; const saveField = useCallback( @@ -32,35 +35,41 @@ export const useSaveField = ({ formValues }: UseSaveFieldOptions) => { const { annualTotal: overallAmount } = calculateAnnualTotals(updatedValues); - try { - await updateMinistryHousingAllowanceRequest({ - variables: { - input: { - requestId: requestData.id, + await updateMinistryHousingAllowanceRequest({ + variables: { + input: { + requestId: requestData.id, + requestAttributes: { + ...attributes, + overallAmount, + }, + }, + }, + optimisticResponse: { + updateMinistryHousingAllowanceRequest: { + __typename: 'MinistryHousingAllowanceRequestUpdateMutationPayload', + ministryHousingAllowanceRequest: { + ...requestData, requestAttributes: { + __typename: 'MhaRequestAttributes', + ...values, ...attributes, overallAmount, }, }, }, - optimisticResponse: { - updateMinistryHousingAllowanceRequest: { - __typename: - 'MinistryHousingAllowanceRequestUpdateMutationPayload', - ministryHousingAllowanceRequest: { - ...requestData, - requestAttributes: { - ...values, - ...attributes, - overallAmount, - }, - }, - }, - }, - }); - } catch (error) {} + }, + onCompleted: () => { + const hasValue = Object.values(attributes).some( + (value) => value !== null, + ); + if (hasValue) { + enqueueSnackbar(t('Saved successfully'), { variant: 'success' }); + } + }, + }); }, - [formValues, updateMinistryHousingAllowanceRequest, requestData], + [formValues, updateMinistryHousingAllowanceRequest, requestData, t], ); return saveField; diff --git a/src/components/Reports/MinisterHousingAllowance/Shared/Context/MinisterHousingAllowanceContext.test.tsx b/src/components/Reports/MinisterHousingAllowance/Shared/Context/MinisterHousingAllowanceContext.test.tsx index 130984ca37..fbaa4b8f9f 100644 --- a/src/components/Reports/MinisterHousingAllowance/Shared/Context/MinisterHousingAllowanceContext.test.tsx +++ b/src/components/Reports/MinisterHousingAllowance/Shared/Context/MinisterHousingAllowanceContext.test.tsx @@ -7,7 +7,6 @@ import { GqlMockedProvider } from '__tests__/util/graphqlMocking'; import { render } from '__tests__/util/testingLibraryReactMock'; import { PageEnum } from 'src/components/Reports/Shared/CalculationReports/Shared/sharedTypes'; import theme from 'src/theme'; -import { StepsEnum } from '../sharedTypes'; import { MinisterHousingAllowanceProvider, useMinisterHousingAllowance, @@ -38,7 +37,7 @@ function FailedConsumer() { function TestConsumer() { const { steps, - currentStep, + currentIndex, handleNextStep, handlePreviousStep, percentComplete, @@ -49,7 +48,7 @@ function TestConsumer() {
{steps.length}
{percentComplete}
-
{currentStep}
+
{currentIndex}
@@ -66,63 +65,65 @@ describe('MinisterHousingAllowanceContext', () => { }); it('provides initial state for new page', async () => { - const { getByTestId, getByRole } = render(); + const { findByTestId, getByTestId, getByRole } = render(); - expect(getByTestId('steps')).toHaveTextContent('4'); - expect(getByTestId('currentStep')).toHaveTextContent(StepsEnum.AboutForm); + expect(await findByTestId('steps')).toHaveTextContent('4'); + expect(getByTestId('currentIndex')).toHaveTextContent('0'); expect(getByTestId('percentComplete')).toHaveTextContent('25'); - await userEvent.click(getByRole('button', { name: 'Next' })); - expect(getByTestId('currentStep')).toHaveTextContent(StepsEnum.RentOrOwn); + 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); + userEvent.click(getByRole('button', { name: 'Next' })); + expect(getByTestId('currentIndex')).toHaveTextContent('2'); expect(getByTestId('percentComplete')).toHaveTextContent('75'); - await userEvent.click(getByRole('button', { name: 'Next' })); - expect(getByTestId('currentStep')).toHaveTextContent(StepsEnum.Receipt); + userEvent.click(getByRole('button', { name: 'Next' })); + expect(getByTestId('currentIndex')).toHaveTextContent('3'); expect(getByTestId('percentComplete')).toHaveTextContent('100'); - await userEvent.click(getByRole('button', { name: 'Previous' })); - expect(getByTestId('currentStep')).toHaveTextContent(StepsEnum.CalcForm); + userEvent.click(getByRole('button', { name: 'Previous' })); + expect(getByTestId('currentIndex')).toHaveTextContent('2'); expect(getByTestId('percentComplete')).toHaveTextContent('75'); }); it('provides initial state for edit page', async () => { - const { getByTestId, getByRole } = render( + const { findByTestId, getByTestId, getByRole } = render( , ); - expect(getByTestId('steps')).toHaveTextContent('4'); - expect(getByTestId('currentStep')).toHaveTextContent(StepsEnum.RentOrOwn); + expect(await findByTestId('steps')).toHaveTextContent('4'); + expect(getByTestId('currentIndex')).toHaveTextContent('1'); expect(getByTestId('percentComplete')).toHaveTextContent('50'); - await userEvent.click(getByRole('button', { name: 'Next' })); - expect(getByTestId('currentStep')).toHaveTextContent(StepsEnum.CalcForm); + userEvent.click(getByRole('button', { name: 'Next' })); + expect(getByTestId('currentIndex')).toHaveTextContent('2'); expect(getByTestId('percentComplete')).toHaveTextContent('75'); - await userEvent.click(getByRole('button', { name: 'Next' })); - expect(getByTestId('currentStep')).toHaveTextContent(StepsEnum.Receipt); + userEvent.click(getByRole('button', { name: 'Next' })); + expect(getByTestId('currentIndex')).toHaveTextContent('3'); expect(getByTestId('percentComplete')).toHaveTextContent('100'); - await userEvent.click(getByRole('button', { name: 'Previous' })); - expect(getByTestId('currentStep')).toHaveTextContent(StepsEnum.CalcForm); + userEvent.click(getByRole('button', { name: 'Previous' })); + expect(getByTestId('currentIndex')).toHaveTextContent('2'); expect(getByTestId('percentComplete')).toHaveTextContent('75'); - await userEvent.click(getByRole('button', { name: 'Previous' })); - expect(getByTestId('currentStep')).toHaveTextContent(StepsEnum.RentOrOwn); + userEvent.click(getByRole('button', { name: 'Previous' })); + expect(getByTestId('currentIndex')).toHaveTextContent('1'); expect(getByTestId('percentComplete')).toHaveTextContent('50'); - await userEvent.click(getByRole('button', { name: 'Previous' })); - expect(getByTestId('currentStep')).toHaveTextContent(StepsEnum.AboutForm); + userEvent.click(getByRole('button', { name: 'Previous' })); + expect(getByTestId('currentIndex')).toHaveTextContent('0'); expect(getByTestId('percentComplete')).toHaveTextContent('25'); }); - it('renders children correctly', () => { - const { getByRole } = render(); + it('renders children correctly', async () => { + const { getByRole, findByRole } = render(); - expect(getByRole('button', { name: 'Previous' })).toBeInTheDocument(); + expect( + await findByRole('button', { name: 'Previous' }), + ).toBeInTheDocument(); expect(getByRole('button', { name: 'Next' })).toBeInTheDocument(); }); }); diff --git a/src/components/Reports/MinisterHousingAllowance/Shared/Context/MinisterHousingAllowanceContext.tsx b/src/components/Reports/MinisterHousingAllowance/Shared/Context/MinisterHousingAllowanceContext.tsx index 8fd2142579..94ded2bac7 100644 --- a/src/components/Reports/MinisterHousingAllowance/Shared/Context/MinisterHousingAllowanceContext.tsx +++ b/src/components/Reports/MinisterHousingAllowance/Shared/Context/MinisterHousingAllowanceContext.tsx @@ -24,7 +24,6 @@ import { useMinistryHousingAllowanceRequestQuery, useUpdateMinistryHousingAllowanceRequestMutation, } from '../../MinisterHousingAllowance.generated'; -import { StepsEnum } from '../sharedTypes'; import { hasPopulatedValues } from './Helper/hasPopulatedValues'; export type HcmData = HcmDataQuery['hcm'][number]; @@ -33,7 +32,6 @@ export type ContextType = { steps: Steps[]; currentIndex: number; percentComplete: number; - currentStep: StepsEnum; handleNextStep: () => void; handlePreviousStep: () => void; pageType: PageEnum | undefined; @@ -50,11 +48,13 @@ export type ContextType = { spouseHcmData?: HcmData | null; preferredName: string; spousePreferredName: string; + userEligibleForMHA: boolean; requestData?: | MinistryHousingAllowanceRequestQuery['ministryHousingAllowanceRequest'] | null; requestError?: ApolloError; + loading?: boolean; requestId?: string; updateMutation: ReturnType< @@ -81,20 +81,21 @@ interface Props { children?: React.ReactNode; } -const objects = Object.values(StepsEnum); - export const MinisterHousingAllowanceProvider: React.FC = ({ requestId, type, children, }) => { - const { data: requestData, error: requestError } = - useMinistryHousingAllowanceRequestQuery({ - variables: { - ministryHousingAllowanceRequestId: requestId ?? '', - }, - skip: !requestId, - }); + const { + data: requestData, + error: requestError, + loading, + } = useMinistryHousingAllowanceRequestQuery({ + variables: { + ministryHousingAllowanceRequestId: requestId ?? '', + }, + skip: !requestId, + }); const hasValues = hasPopulatedValues( requestData?.ministryHousingAllowanceRequest?.requestAttributes ?? null, @@ -105,8 +106,8 @@ export const MinisterHousingAllowanceProvider: React.FC = ({ const pageType = type; const { steps: initialSteps, - nextStep, - previousStep, + handleNextStep, + handlePreviousStep, currentIndex, percentComplete, } = useStepList(FormEnum.MHA, type); @@ -153,41 +154,23 @@ export const MinisterHousingAllowanceProvider: React.FC = ({ [spouseHcmData], ); + const userEligibleForMHA = useMemo( + () => userHcmData?.mhaEit?.mhaEligibility ?? false, + [userHcmData], + ); + const [isDrawerOpen, setIsDrawerOpen] = useState(true); const toggleDrawer = useCallback(() => { setIsDrawerOpen((prev) => !prev); }, []); - const [hasCalcValues, setHasCalcValues] = useState(hasValues ? true : false); + const [hasCalcValues, setHasCalcValues] = useState(hasValues); 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, @@ -202,18 +185,19 @@ export const MinisterHousingAllowanceProvider: React.FC = ({ spouseHcmData, preferredName, spousePreferredName, + userEligibleForMHA, isPrint, setIsPrint, setIsComplete, requestData: requestData?.ministryHousingAllowanceRequest ?? null, requestError, + loading, requestId, updateMutation, }), [ steps, currentIndex, - currentStep, percentComplete, handleNextStep, handlePreviousStep, @@ -228,11 +212,13 @@ export const MinisterHousingAllowanceProvider: React.FC = ({ spouseHcmData, preferredName, spousePreferredName, + userEligibleForMHA, isPrint, setIsPrint, setIsComplete, requestData, requestError, + loading, requestId, updateMutation, ], diff --git a/src/components/Reports/MinisterHousingAllowance/SharedComponents/CurrentBoardApproved.test.tsx b/src/components/Reports/MinisterHousingAllowance/SharedComponents/CurrentBoardApproved.test.tsx index 60ddbdd7a5..823bbe50f2 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,13 +14,24 @@ 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', requestAttributes: { ...mockMHARequest.requestAttributes, hrApprovedAt: '2023-01-15', @@ -30,7 +43,7 @@ const TestComponent: React.FC = ({ contextValue }) => { return ( - + = ({ contextValue }) => { describe('CurrentBoardApproved Component', () => { it('should render correctly for married person', () => { - const { getByText } = render( + const { queryByText, queryByRole, 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(queryByText('Current Board Approved MHA')).toBeInTheDocument(); + expect( + queryByRole('columnheader', { name: /spouse/i }), + ).toBeInTheDocument(); + expect( + queryByRole('columnheader', { name: /mha approved by board/i }), + ).toBeInTheDocument(); + expect( + queryByRole('columnheader', { name: /mha claimed in salary/i }), + ).toBeInTheDocument(); + + expect(queryByRole('cell', { name: 'John' })).toBeInTheDocument(); + expect(getAllByText('$1,500.00')).toHaveLength(2); + expect(getAllByText('Approved on: 1/15/2023')).toHaveLength(2); + expect(queryByText('$1,000.00')).toBeInTheDocument(); + expect(getAllByText('Last updated: 12/1/2022')).toHaveLength(2); - expect(getByText('$1,500.00')).toBeInTheDocument(); - expect(getByText('John')).toBeInTheDocument(); - expect(getByText('$1,000.00')).toBeInTheDocument(); - expect(getByText('Jane')).toBeInTheDocument(); - expect(getByText('$500.00')).toBeInTheDocument(); + expect(queryByRole('cell', { name: 'Jane' })).toBeInTheDocument(); + expect(queryByText('$500.00')).toBeInTheDocument(); }); it('should render correctly for single person', () => { - const { getByText, queryByText } = render( + const { queryByText, queryByRole, getAllByText } = render( { />, ); - expect(getByText('Current Board Approved MHA')).toBeInTheDocument(); - expect(getByText(/APPROVAL DATE/i)).toBeInTheDocument(); - expect(getByText('CURRENT MHA CLAIMED')).toBeInTheDocument(); + expect(queryByText('Current Board Approved MHA')).toBeInTheDocument(); + expect( + queryByRole('columnheader', { name: /spouse/i }), + ).toBeInTheDocument(); + expect( + queryByRole('columnheader', { name: /mha approved by board/i }), + ).toBeInTheDocument(); + expect( + queryByRole('columnheader', { name: /mha claimed in salary/i }), + ).toBeInTheDocument(); - expect(getByText('$1,500.00')).toBeInTheDocument(); - expect(getByText('John')).toBeInTheDocument(); - expect(getByText('$1,000.00')).toBeInTheDocument(); + expect(queryByRole('cell', { name: 'John' })).toBeInTheDocument(); + expect(getAllByText('$1,500.00')).toHaveLength(1); + expect(queryByText('$1,000.00')).toBeInTheDocument(); // 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: '1', + }, + }, + ); + }); + + await waitFor(() => { + expect(mockPush).toHaveBeenCalledWith( + `/accountLists/account-list-1/reports/housingAllowance/${newRequestId}?mode=edit`, + ); + }); + }); }); diff --git a/src/components/Reports/MinisterHousingAllowance/SharedComponents/CurrentBoardApproved.tsx b/src/components/Reports/MinisterHousingAllowance/SharedComponents/CurrentBoardApproved.tsx index 4dcba80578..cbd8aef87d 100644 --- a/src/components/Reports/MinisterHousingAllowance/SharedComponents/CurrentBoardApproved.tsx +++ b/src/components/Reports/MinisterHousingAllowance/SharedComponents/CurrentBoardApproved.tsx @@ -1,11 +1,23 @@ +import { useRouter } from 'next/router'; 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'; 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'; @@ -20,8 +32,11 @@ export const CurrentBoardApproved: React.FC = ({ const { t } = useTranslation(); const locale = useLocale(); const accountListId = useAccountListId(); + const router = useRouter(); const currency = 'USD'; + const [duplicateMHA] = useDuplicateMinistryHousingAllowanceRequestMutation(); + const { isMarried, preferredName, spousePreferredName } = useMinisterHousingAllowance(); const requestId = request?.id; @@ -29,89 +44,204 @@ export const CurrentBoardApproved: React.FC = ({ const { hrApprovedAt, approvedOverallAmount, staffSpecific, spouseSpecific } = request?.requestAttributes || {}; + const lastUpdated = request?.updatedAt ?? null; + + const viewLink = getRequestUrl(accountListId, requestId, 'view'); + + const handleDuplicateRequest = async () => { + if (!requestId) { + return; + } + + await duplicateMHA({ + variables: { + input: { + requestId: requestId, + }, + }, + onCompleted: (data) => { + const newRequestId = + data?.duplicateMinistryHousingAllowanceRequest + ?.ministryHousingAllowanceRequest.id; + + if (newRequestId) { + router.push(getRequestUrl(accountListId, newRequestId, 'edit')); + } + }, + }); + }; + 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 ? spousePreferredName : 'N/A'} + + + + + + {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/SharedComponents/CurrentRequest.test.tsx b/src/components/Reports/MinisterHousingAllowance/SharedComponents/CurrentRequest.test.tsx index 2f4f93181d..b5deb47c85 100644 --- a/src/components/Reports/MinisterHousingAllowance/SharedComponents/CurrentRequest.test.tsx +++ b/src/components/Reports/MinisterHousingAllowance/SharedComponents/CurrentRequest.test.tsx @@ -1,46 +1,105 @@ import React from 'react'; import { ThemeProvider } from '@mui/material/styles'; -import { render } 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'; import { MhaStatusEnum } from 'src/graphql/types.generated'; import theme from 'src/theme'; -import { MinisterHousingAllowanceProvider } from '../Shared/Context/MinisterHousingAllowanceContext'; +import { DeleteMinistryHousingAllowanceRequestMutation } from '../MinisterHousingAllowance.generated'; import { mockMHARequest } from '../mockData'; import { CurrentRequest, getDotColor, getDotVariant } from './CurrentRequest'; -const TestComponent: React.FC = () => { +const mutationSpy = jest.fn(); +const mockEnqueue = jest.fn(); + +jest.mock('notistack', () => ({ + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + ...jest.requireActual('notistack'), + useSnackbar: () => { + return { + enqueueSnackbar: mockEnqueue, + }; + }, +})); + +interface TestComponentProps { + requestId?: string; +} + +const TestComponent: React.FC = () => { return ( - - + + + onCall={mutationSpy} + > - - + + ); }; 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 () => { + 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: '1', + }, + }, + }), + }), + ); + }); + + await waitFor(() => { + expect(mockEnqueue).toHaveBeenCalledWith( + expect.stringContaining('MHA request cancelled successfully.'), + { variant: 'success' }, + ); + }); }); }); diff --git a/src/components/Reports/MinisterHousingAllowance/SharedComponents/CurrentRequest.tsx b/src/components/Reports/MinisterHousingAllowance/SharedComponents/CurrentRequest.tsx index aff5d02f70..bece438545 100644 --- a/src/components/Reports/MinisterHousingAllowance/SharedComponents/CurrentRequest.tsx +++ b/src/components/Reports/MinisterHousingAllowance/SharedComponents/CurrentRequest.tsx @@ -7,14 +7,16 @@ 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'; 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,11 +29,24 @@ export const CurrentRequest: React.FC = ({ request }) => { const locale = useLocale(); const accountListId = useAccountListId(); const currency = 'USD'; + const { enqueueSnackbar } = useSnackbar(); const requestId = request.id; const { status, requestAttributes } = request; + const canEdit = [ + MhaStatusEnum.InProgress, + MhaStatusEnum.ActionRequired, + ].includes(status); + const approved = [ + MhaStatusEnum.HrApproved, + MhaStatusEnum.BoardApproved, + ].includes(status); + + const editLink = getRequestUrl(accountListId, requestId, 'edit'); + const viewLink = getRequestUrl(accountListId, requestId, 'view'); + const { boardApprovedAt, deadlineDate, @@ -40,6 +55,32 @@ 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'], + awaitRefetchQueries: true, + }); + + const handleCancelRequest = async () => { + await deleteRequestMutation({ + variables: { + input: { + requestId: requestId ?? '', + }, + }, + onCompleted: () => { + enqueueSnackbar(t('MHA request cancelled successfully.'), { + variant: 'success', + }); + }, + }); + }; + return ( = ({ 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={() => {}} + 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, { @@ -77,7 +136,9 @@ export const CurrentRequest: React.FC = ({ request }) => { @@ -99,7 +160,9 @@ export const CurrentRequest: React.FC = ({ request }) => { @@ -115,7 +178,9 @@ export const CurrentRequest: React.FC = ({ request }) => { @@ -131,7 +196,9 @@ export const CurrentRequest: React.FC = ({ request }) => { @@ -147,7 +214,9 @@ export const CurrentRequest: React.FC = ({ request }) => { diff --git a/src/components/Reports/MinisterHousingAllowance/Steps/NoEditAccess/NoEditAccess.test.tsx b/src/components/Reports/MinisterHousingAllowance/Steps/NoEditAccess/NoEditAccess.test.tsx new file mode 100644 index 0000000000..e35658638d --- /dev/null +++ b/src/components/Reports/MinisterHousingAllowance/Steps/NoEditAccess/NoEditAccess.test.tsx @@ -0,0 +1,28 @@ +import { ThemeProvider } from '@mui/material/styles'; +import { render } from '@testing-library/react'; +import theme from 'src/theme'; +import { NoEditAccess } from './NoEditAccess'; + +const Components = () => ( + + + +); + +describe('NoEditAccess', () => { + it('should render the NoEditAccess component and support link', () => { + const { getByText, getByRole } = render(); + + expect( + getByRole('heading', { + name: 'You do not have permission to edit this request.', + }), + ).toBeInTheDocument(); + expect( + getByText( + /our records show that this request is either approved or under review/i, + ), + ).toBeInTheDocument(); + expect(getByRole('link', { name: 'support@mpdx.org' })).toBeInTheDocument(); + }); +}); diff --git a/src/components/Reports/MinisterHousingAllowance/Steps/NoEditAccess/NoEditAccess.tsx b/src/components/Reports/MinisterHousingAllowance/Steps/NoEditAccess/NoEditAccess.tsx new file mode 100644 index 0000000000..c18076889d --- /dev/null +++ b/src/components/Reports/MinisterHousingAllowance/Steps/NoEditAccess/NoEditAccess.tsx @@ -0,0 +1,40 @@ +import Link from 'next/link'; +import React from 'react'; +import { Box, Container, Typography } from '@mui/material'; +import { useTranslation } from 'react-i18next'; +import theme from 'src/theme'; + +export const NoEditAccess: React.FC = () => { + const { t } = useTranslation(); + + return ( + + + + {t('You do not have permission to edit this request.')} + + + + {t( + 'Our records show that this request is either approved or under review. If you believe you should have access to edit this request, please contact ', + )} + + support@mpdx.org + + . + + + + ); +}; diff --git a/src/components/Reports/MinisterHousingAllowance/Steps/NoRequestAccess/NoRequestAccess.test.tsx b/src/components/Reports/MinisterHousingAllowance/Steps/NoRequestAccess/NoRequestAccess.test.tsx new file mode 100644 index 0000000000..01585ec5f0 --- /dev/null +++ b/src/components/Reports/MinisterHousingAllowance/Steps/NoRequestAccess/NoRequestAccess.test.tsx @@ -0,0 +1,26 @@ +import { ThemeProvider } from '@mui/material/styles'; +import { render } from '@testing-library/react'; +import theme from 'src/theme'; +import { NoRequestAccess } from './NoRequestAccess'; + +const Components = () => ( + + + +); + +describe('NoRequestAccess', () => { + it('should render the NoRequestAccess component and support link', () => { + const { getByText, getByRole } = render(); + + expect( + getByRole('heading', { + name: 'You do not have permission to request a ministry housing allowance.', + }), + ).toBeInTheDocument(); + expect( + getByText(/our records show that you are not eligible to apply for/i), + ).toBeInTheDocument(); + expect(getByRole('link', { name: 'support@mpdx.org' })).toBeInTheDocument(); + }); +}); diff --git a/src/components/Reports/MinisterHousingAllowance/Steps/NoRequestAccess/NoRequestAccess.tsx b/src/components/Reports/MinisterHousingAllowance/Steps/NoRequestAccess/NoRequestAccess.tsx new file mode 100644 index 0000000000..96e70f8556 --- /dev/null +++ b/src/components/Reports/MinisterHousingAllowance/Steps/NoRequestAccess/NoRequestAccess.tsx @@ -0,0 +1,41 @@ +import Link from 'next/link'; +import React from 'react'; +import { Box, Container, Typography } from '@mui/material'; +import { useTranslation } from 'react-i18next'; +import theme from 'src/theme'; + +export const NoRequestAccess: React.FC = () => { + const { t } = useTranslation(); + + return ( + + + + {t( + 'You do not have permission to request a ministry housing allowance.', + )} + + + + {t( + "Our records show that you are not eligible to apply for Minister's Housing Allowance. If you believe otherwise, please contact ", + )} + + support@mpdx.org + + + + + ); +}; diff --git a/src/components/Reports/MinisterHousingAllowance/Steps/StepOne/AboutForm.test.tsx b/src/components/Reports/MinisterHousingAllowance/Steps/StepOne/AboutForm.test.tsx index d6071b7596..2172ea9089 100644 --- a/src/components/Reports/MinisterHousingAllowance/Steps/StepOne/AboutForm.test.tsx +++ b/src/components/Reports/MinisterHousingAllowance/Steps/StepOne/AboutForm.test.tsx @@ -31,11 +31,11 @@ const TestComponent: React.FC = () => ( ); describe('AboutForm', () => { - it('renders form and formatted dates', () => { - const { getByText, getByRole } = render(); + it('renders form and formatted dates', async () => { + const { getByText, getByRole, findByRole } = render(); expect( - getByRole('heading', { name: 'About this Form' }), + await findByRole('heading', { name: 'About this Form' }), ).toBeInTheDocument(); expect( getByText( 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 6274decc61..55e45d0424 100644 --- a/src/components/Reports/MinisterHousingAllowance/Steps/StepThree/CalcComponents/CostOfHome.test.tsx +++ b/src/components/Reports/MinisterHousingAllowance/Steps/StepThree/CalcComponents/CostOfHome.test.tsx @@ -5,6 +5,7 @@ import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; import { render, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { Formik } from 'formik'; +import { SnackbarProvider } from 'notistack'; import * as yup from 'yup'; import TestRouter from '__tests__/util/TestRouter'; import { GqlMockedProvider } from '__tests__/util/graphqlMocking'; @@ -38,21 +39,23 @@ const TestComponent: React.FC = ({ }) => ( - - onCall={mutationSpy} - > - - - - - - - - + + + onCall={mutationSpy} + > + + + + + + + + + ); @@ -119,7 +122,7 @@ describe('CostOfHome', () => { furnitureCostsTwo: null, repairCosts: null, avgUtilityTwo: null, - unexpectedCosts: null, + unexpectedExpenses: null, }, }, } as unknown as ContextType @@ -130,25 +133,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..e337082bfb 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={currencyFormat(0, currency, locale)} 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={currencyFormat(0, currency, locale)} 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={currencyFormat(0, currency, locale)} 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={currencyFormat(0, currency, locale)} 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={currencyFormat(0, currency, locale)} InputProps={{ disableUnderline: true, inputMode: 'decimal' }} fieldName="unexpectedExpenses" schema={schema} diff --git a/src/components/Reports/MinisterHousingAllowance/Steps/StepThree/CalcComponents/EndingSection.test.tsx b/src/components/Reports/MinisterHousingAllowance/Steps/StepThree/CalcComponents/EndingSection.test.tsx index 70a95a6605..7901050ea1 100644 --- a/src/components/Reports/MinisterHousingAllowance/Steps/StepThree/CalcComponents/EndingSection.test.tsx +++ b/src/components/Reports/MinisterHousingAllowance/Steps/StepThree/CalcComponents/EndingSection.test.tsx @@ -4,6 +4,7 @@ import { AdapterLuxon } from '@mui/x-date-pickers/AdapterLuxon'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; import { render } from '@testing-library/react'; import { Formik } from 'formik'; +import { SnackbarProvider } from 'notistack'; import * as yup from 'yup'; import TestRouter from '__tests__/util/TestRouter'; import { GqlMockedProvider } from '__tests__/util/graphqlMocking'; @@ -23,24 +24,26 @@ const TestComponent: React.FC = () => ( - - - - - - - + + + + + + + + + ); describe('EndingSection', () => { - it('renders the component', () => { - const { getByText, getByRole } = render(); + it('renders the component', async () => { + const { getByText, getByRole, findByText } = render(); expect( - getByText(/if the above information is correct, please confirm/i), + await findByText(/if the above information is correct, please confirm/i), ).toBeInTheDocument(); expect( getByRole('textbox', { name: 'Telephone Number' }), 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..c411402a6e 100644 --- a/src/components/Reports/MinisterHousingAllowance/Steps/StepThree/CalcComponents/FairRentalValue.test.tsx +++ b/src/components/Reports/MinisterHousingAllowance/Steps/StepThree/CalcComponents/FairRentalValue.test.tsx @@ -5,6 +5,7 @@ import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; import { render, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { Formik } from 'formik'; +import { SnackbarProvider } from 'notistack'; import * as yup from 'yup'; import TestRouter from '__tests__/util/TestRouter'; import { GqlMockedProvider } from '__tests__/util/graphqlMocking'; @@ -33,21 +34,23 @@ interface TestComponentProps { const TestComponent: React.FC = ({ contextValue }) => ( - - onCall={mutationSpy} - > - - - - - - - - + + + onCall={mutationSpy} + > + + + + + + + + + ); @@ -95,15 +98,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..b22f4e7f16 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={currencyFormat(0, currency, locale)} 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={currencyFormat(0, currency, locale)} 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={currencyFormat(0, currency, locale)} InputProps={{ disableUnderline: true, inputMode: 'decimal' }} fieldName="avgUtilityOne" schema={schema} diff --git a/src/components/Reports/MinisterHousingAllowance/Steps/StepThree/CalcComponents/PersonInfo.test.tsx b/src/components/Reports/MinisterHousingAllowance/Steps/StepThree/CalcComponents/PersonInfo.test.tsx index d817b26a2c..d7e26b9e79 100644 --- a/src/components/Reports/MinisterHousingAllowance/Steps/StepThree/CalcComponents/PersonInfo.test.tsx +++ b/src/components/Reports/MinisterHousingAllowance/Steps/StepThree/CalcComponents/PersonInfo.test.tsx @@ -31,9 +31,11 @@ const TestComponent: React.FC = () => ( describe('PersonInfo', () => { it('renders personal contact information', async () => { - const { findByText, getByText } = render(); + const { findByText } = render(); - expect(getByText('Personal Contact Information')).toBeInTheDocument(); + expect( + await findByText('Personal Contact Information'), + ).toBeInTheDocument(); expect(await findByText('John Doe')).toBeInTheDocument(); expect( diff --git a/src/components/Reports/MinisterHousingAllowance/Steps/StepThree/CalcComponents/RequestSummaryCard.test.tsx b/src/components/Reports/MinisterHousingAllowance/Steps/StepThree/CalcComponents/RequestSummaryCard.test.tsx index 85872d157c..0ec9d9ca26 100644 --- a/src/components/Reports/MinisterHousingAllowance/Steps/StepThree/CalcComponents/RequestSummaryCard.test.tsx +++ b/src/components/Reports/MinisterHousingAllowance/Steps/StepThree/CalcComponents/RequestSummaryCard.test.tsx @@ -29,12 +29,12 @@ const TestComponent: React.FC = ({ rentOrOwn }) => ( ); describe('RequestSummaryCard', () => { - it('renders the card for own', () => { - const { getByText } = render( + it('renders the card for own', async () => { + const { getByText, findByText } = render( , ); - expect(getByText('Your MHA Request Summary')).toBeInTheDocument(); + expect(await findByText('Your MHA Request Summary')).toBeInTheDocument(); expect(getByText('Own')).toBeInTheDocument(); expect(getByText('Your Annual MHA Total')).toBeInTheDocument(); expect( @@ -42,12 +42,12 @@ describe('RequestSummaryCard', () => { ).toBeInTheDocument(); }); - it('renders the card for rent', () => { - const { getByText } = render( + it('renders the card for rent', async () => { + const { getByText, findByText } = render( , ); - expect(getByText('Your MHA Request Summary')).toBeInTheDocument(); + expect(await findByText('Your MHA Request Summary')).toBeInTheDocument(); expect(getByText('Rent')).toBeInTheDocument(); }); }); diff --git a/src/components/Reports/MinisterHousingAllowance/Steps/StepThree/Calculation.test.tsx b/src/components/Reports/MinisterHousingAllowance/Steps/StepThree/Calculation.test.tsx index 6a26d376e9..71f0d1d2e8 100644 --- a/src/components/Reports/MinisterHousingAllowance/Steps/StepThree/Calculation.test.tsx +++ b/src/components/Reports/MinisterHousingAllowance/Steps/StepThree/Calculation.test.tsx @@ -5,12 +5,16 @@ import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; import { render, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { Formik } from 'formik'; +import { SnackbarProvider } from 'notistack'; import TestRouter from '__tests__/util/TestRouter'; 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 +26,19 @@ const mutationSpy = jest.fn(); const setHasCalcValues = jest.fn(); const setIsPrint = jest.fn(); const updateMutation = jest.fn(); +const handleNextStep = jest.fn(); +const mockEnqueue = jest.fn(); + +jest.mock('notistack', () => ({ + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + ...jest.requireActual('notistack'), + useSnackbar: () => { + return { + enqueueSnackbar: mockEnqueue, + }; + }, +})); interface TestComponentProps { contextValue: Partial; @@ -39,31 +56,34 @@ const TestComponent: React.FC = ({ - - onCall={mutationSpy} - > - - - - - - + + + onCall={mutationSpy} + > + + + + + + + ); describe('Calculation', () => { - it('renders the component', () => { - const { getByText, getByRole } = render( + it('renders the component', async () => { + const { getByText, getByRole, findByRole } = render( { ); expect( - getByRole('heading', { name: 'Calculate Your MHA Request' }), + await findByRole('heading', { name: 'Calculate Your MHA Request' }), ).toBeInTheDocument(); expect( getByText(/please enter dollar amounts for each category below/i), @@ -101,7 +121,7 @@ describe('Calculation', () => { requestData: { id: 'request-id', requestAttributes: { - unexpectedCosts: null, + unexpectedExpenses: null, }, }, } as unknown as ContextType @@ -109,24 +129,24 @@ describe('Calculation', () => { />, ); - const row = getByRole('row', { + const row = await findByRole('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'); + userEvent.type(input, '100'); expect(input).toHaveValue('100'); - await userEvent.clear(input); + userEvent.clear(input); expect(input).toHaveValue(''); input.focus(); - await userEvent.tab(); + userEvent.tab(); expect(await findByText('Required field.')).toBeInTheDocument(); const submitButton = getByRole('button', { name: /submit/i }); - await userEvent.click(submitButton); + userEvent.click(submitButton); expect(await findByRole('alert')).toBeInTheDocument(); expect( @@ -135,7 +155,7 @@ describe('Calculation', () => { }); it('should show validation error when checkbox is not checked', async () => { - const { findByText, getByRole, getByText, findByRole } = render( + const { findByText, findByRole, getByText } = render( { />, ); - const submitButton = getByRole('button', { name: /submit/i }); + const submitButton = await findByRole('button', { name: /submit/i }); - await userEvent.click(submitButton); + userEvent.click(submitButton); expect( await findByText('This box must be checked to continue.'), @@ -163,8 +183,8 @@ describe('Calculation', () => { ).toBeInTheDocument(); }); - it('shows validation errors when inputs are invalid', async () => { - const { getByRole, findByText } = render( + it('shows validation errors when email and phone are invalid', async () => { + const { getByRole, findByText, findByRole } = render( { />, ); - const phone = getByRole('textbox', { name: 'Telephone Number' }); + const phone = await findByRole('textbox', { name: 'Telephone Number' }); const email = getByRole('textbox', { name: 'Email' }); expect(phone).toHaveValue('1234567890'); expect(email).toHaveValue('john.doe@cru.org'); - await userEvent.clear(phone); - await userEvent.tab(); + userEvent.clear(phone); + userEvent.tab(); expect(await findByText('Phone Number is required.')).toBeInTheDocument(); - await userEvent.clear(email); - await userEvent.tab(); + userEvent.clear(email); + userEvent.tab(); expect(await findByText('Email is required.')).toBeInTheDocument(); - await userEvent.type(phone, 'abc'); - await userEvent.tab(); + userEvent.type(phone, 'abc'); + userEvent.tab(); expect(await findByText('Invalid phone number')).toBeInTheDocument(); - await userEvent.type(email, 'invalid-email'); - await userEvent.tab(); + userEvent.type(email, 'invalid-email'); + userEvent.tab(); expect(await findByText('Invalid email address.')).toBeInTheDocument(); }); + it('shows validation error when input is 0', async () => { + const { findByRole, findByText } = render( + , + ); + + const row = await findByRole('row', { + name: /average monthly amount for unexpected/i, + }); + const input = within(row).getByPlaceholderText(/\$0/i); + + userEvent.type(input, '0'); + + input.focus(); + 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( { setHasCalcValues, setIsPrint, updateMutation, + handleNextStep, requestData: { id: 'request-id', requestAttributes: { @@ -222,7 +277,7 @@ describe('Calculation', () => { furnitureValue: null, repairCosts: null, utilityCosts: null, - unexpectedCosts: null, + unexpectedExpenses: null, iUnderstandMhaPolicy: false, phoneNumber: '1234567890', emailAddress: 'john.doe@cru.org', @@ -234,44 +289,43 @@ describe('Calculation', () => { />, ); - const row1 = getByRole('row', { + const row1 = await findByRole('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'); - await userEvent.type(input3, '300'); - await userEvent.type(input4, '400'); - await userEvent.type(input5, '500'); + userEvent.type(input1, '1000'); + userEvent.type(input2, '200'); + userEvent.type(input3, '300'); + userEvent.type(input4, '400'); + userEvent.type(input5, '500'); const checkbox = getByRole('checkbox', { name: /i understand that my approved/i, }); - await userEvent.click(checkbox); + userEvent.click(checkbox); const submitButton = getByRole('button', { name: /submit/i }); - await userEvent.click(submitButton); - + userEvent.click(submitButton); expect(await findByRole('dialog')).toBeInTheDocument(); expect( @@ -285,6 +339,34 @@ 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 }); + + userEvent.click(confirmButton); + + await waitFor(() => { + expect(mutationSpy).toHaveBeenCalledTimes(6); + }); + + expect(mutationSpy).toHaveGraphqlOperation( + 'SubmitMinistryHousingAllowanceRequest', + { + input: { + requestId: 'request-id', + }, + }, + ); + + await waitFor(() => { + expect(mockEnqueue).toHaveBeenCalledWith( + expect.stringContaining('MHA request submitted successfully.'), + { variant: 'success' }, + ); + }); }); it('should change text when dates are null', () => { @@ -310,7 +392,7 @@ describe('Calculation', () => { }); it('should update checkbox value when clicked', async () => { - const { getByRole } = render( + const { findByRole } = render( { />, ); - const checkbox = getByRole('checkbox', { + const checkbox = await findByRole('checkbox', { name: /i understand that my approved/i, }); expect(checkbox).not.toBeChecked(); - await userEvent.click(checkbox); + userEvent.click(checkbox); expect(checkbox).toBeChecked(); await waitFor(() => @@ -352,8 +434,8 @@ describe('Calculation', () => { }); describe('isViewPage behavior', () => { - it('renders view only mode', () => { - const { getByRole, queryByRole, getByText } = render( + it('renders view only mode', async () => { + const { findByRole, queryByRole, getByText } = render( { ); expect( - getByRole('heading', { name: 'Your MHA Request' }), + await findByRole('heading', { name: 'Your MHA Request' }), ).toBeInTheDocument(); expect(getByText('Personal Contact Information')).toBeInTheDocument(); diff --git a/src/components/Reports/MinisterHousingAllowance/Steps/StepThree/Calculation.tsx b/src/components/Reports/MinisterHousingAllowance/Steps/StepThree/Calculation.tsx index b5128f56d2..7403eab3a8 100644 --- a/src/components/Reports/MinisterHousingAllowance/Steps/StepThree/Calculation.tsx +++ b/src/components/Reports/MinisterHousingAllowance/Steps/StepThree/Calculation.tsx @@ -15,8 +15,10 @@ import { } from '@mui/material'; import { Formik } from 'formik'; import { DateTime } from 'luxon'; +import { useSnackbar } from 'notistack'; import { Trans, useTranslation } from 'react-i18next'; import * as yup from 'yup'; +import Loading from 'src/components/Loading'; import { PageEnum } from 'src/components/Reports/Shared/CalculationReports/Shared/sharedTypes'; import { SimpleScreenOnly, @@ -28,6 +30,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'; @@ -61,11 +64,26 @@ export interface CalculationFormValues { const getValidationSchema = (rentOrOwn?: MhaRentOrOwnEnum) => { 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.'), ), @@ -82,9 +100,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.')), }); } @@ -100,9 +127,13 @@ export const Calculation: React.FC = ({ }) => { const { t } = useTranslation(); const locale = useLocale(); - const { query } = useRouter(); + const { enqueueSnackbar } = useSnackbar(); + const router = useRouter(); + const { query } = router; const print = query.print === 'true'; + const [submitMutation] = useSubmitMinistryHousingAllowanceRequestMutation(); + const { handleNextStep, handlePreviousStep, @@ -111,6 +142,7 @@ export const Calculation: React.FC = ({ setIsPrint, isPrint, requestData, + loading, updateMutation, userHcmData, } = useMinisterHousingAllowance(); @@ -191,6 +223,10 @@ export const Calculation: React.FC = ({ const schema = getValidationSchema(rentOrOwn); + if (loading) { + return ; + } + return ( initialValues={initialValues} @@ -198,7 +234,15 @@ export const Calculation: React.FC = ({ validateOnChange validateOnBlur onSubmit={() => { - handleNextStep(); + try { + submitMutation({ + variables: { input: { requestId: requestData?.id ?? '' } }, + }); + enqueueSnackbar(t('MHA request submitted successfully.'), { + variant: 'success', + }); + handleNextStep(); + } catch (error) {} }} > {({ diff --git a/src/components/Reports/MinisterHousingAllowance/Steps/StepTwo/RentOwn.test.tsx b/src/components/Reports/MinisterHousingAllowance/Steps/StepTwo/RentOwn.test.tsx index d25f979a5f..443dca90b8 100644 --- a/src/components/Reports/MinisterHousingAllowance/Steps/StepTwo/RentOwn.test.tsx +++ b/src/components/Reports/MinisterHousingAllowance/Steps/StepTwo/RentOwn.test.tsx @@ -3,9 +3,11 @@ import { ThemeProvider } from '@mui/material/styles'; import { render, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { Formik } from 'formik'; +import { SnackbarProvider } from 'notistack'; import TestRouter from '__tests__/util/TestRouter'; 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 { @@ -18,6 +20,18 @@ const submit = jest.fn(); const mutationSpy = jest.fn(); const updateMutation = jest.fn(); const setHasCalcValues = jest.fn(); +const mockEnqueue = jest.fn(); + +jest.mock('notistack', () => ({ + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + ...jest.requireActual('notistack'), + useSnackbar: () => { + return { + enqueueSnackbar: mockEnqueue, + }; + }, +})); interface TestComponentProps { contextValue: Partial; @@ -26,39 +40,44 @@ interface TestComponentProps { const TestComponent: React.FC = ({ contextValue }) => ( - - onCall={mutationSpy} - > - - - - - - + + + onCall={mutationSpy} + > + + + + + + + ); describe('RentOwn', () => { it('renders form and options for new page', async () => { - const { getByRole, getByText, findAllByRole } = render( + const { getByRole, getByText, findAllByRole, findByRole } = render( , ); - expect(getByRole('heading', { name: 'Rent or Own?' })).toBeInTheDocument(); + expect( + await findByRole('heading', { name: 'Rent or Own?' }), + ).toBeInTheDocument(); expect(getByText('Rent')).toBeInTheDocument(); expect(getByText('Own')).toBeInTheDocument(); @@ -71,7 +90,7 @@ describe('RentOwn', () => { variables: { input: { requestAttributes: { - rentOrOwn: 'RENT', + rentOrOwn: MhaRentOrOwnEnum.Rent, rentalValue: null, furnitureCostsOne: null, avgUtilityOne: null, @@ -89,6 +108,13 @@ describe('RentOwn', () => { }), ); + await waitFor(() => { + expect(mockEnqueue).toHaveBeenCalledWith( + expect.stringContaining('All inputs have been cleared successfully'), + { variant: 'success' }, + ); + }); + expect(getByRole('radio', { name: 'Rent' })).toBeChecked(); }); diff --git a/src/components/Reports/MinisterHousingAllowance/Steps/StepTwo/RentOwn.tsx b/src/components/Reports/MinisterHousingAllowance/Steps/StepTwo/RentOwn.tsx index 05384a2e6c..af99b72cf2 100644 --- a/src/components/Reports/MinisterHousingAllowance/Steps/StepTwo/RentOwn.tsx +++ b/src/components/Reports/MinisterHousingAllowance/Steps/StepTwo/RentOwn.tsx @@ -9,6 +9,7 @@ import { Typography, } from '@mui/material'; import { useFormikContext } from 'formik'; +import { useSnackbar } from 'notistack'; import { useTranslation } from 'react-i18next'; import { PageEnum } from 'src/components/Reports/Shared/CalculationReports/Shared/sharedTypes'; import { MhaRentOrOwnEnum } from 'src/graphql/types.generated'; @@ -16,9 +17,11 @@ import { DirectionButtons } from '../../../Shared/CalculationReports/DirectionBu import { SubmitModal } from '../../../Shared/CalculationReports/SubmitModal/SubmitModal'; import { FormValues } from '../../RequestPage/RequestPage'; import { useMinisterHousingAllowance } from '../../Shared/Context/MinisterHousingAllowanceContext'; +import { RentOwnSkeleton } from './RentOwnSkeleton'; export const RentOwn: React.FC = () => { const { t } = useTranslation(); + const { enqueueSnackbar } = useSnackbar(); const { values, @@ -37,31 +40,37 @@ export const RentOwn: React.FC = () => { setHasCalcValues, handlePreviousStep, requestData, + loading, updateMutation, } = useMinisterHousingAllowance(); const updateRequest = (id: string, rentOrOwn: MhaRentOrOwnEnum) => { - updateMutation({ - variables: { - input: { - requestId: id, - requestAttributes: { - rentOrOwn, - rentalValue: null, - furnitureCostsOne: null, - avgUtilityOne: null, - mortgageOrRentPayment: null, - furnitureCostsTwo: null, - repairCosts: null, - avgUtilityTwo: null, - unexpectedExpenses: null, - overallAmount: null, - iUnderstandMhaPolicy: null, + try { + updateMutation({ + variables: { + input: { + requestId: id, + requestAttributes: { + rentOrOwn, + rentalValue: null, + furnitureCostsOne: null, + avgUtilityOne: null, + mortgageOrRentPayment: null, + furnitureCostsTwo: null, + repairCosts: null, + avgUtilityTwo: null, + unexpectedExpenses: null, + overallAmount: null, + iUnderstandMhaPolicy: null, + }, }, }, - }, - }); - setHasCalcValues(false); + }); + enqueueSnackbar(t('All inputs have been cleared successfully'), { + variant: 'success', + }); + setHasCalcValues(false); + } catch (error) {} }; const [pendingValue, setPendingValue] = useState( @@ -100,7 +109,17 @@ export const RentOwn: React.FC = () => { } } else { handleChange(event); - updateRequest(requestData.id, selectedValue); + + updateMutation({ + variables: { + input: { + requestId: requestData.id, + requestAttributes: { + rentOrOwn: selectedValue, + }, + }, + }, + }); } }; @@ -138,28 +157,32 @@ export const RentOwn: React.FC = () => { )} - - + ) : ( + - } - label={t('Rent')} - /> - } - label={t('Own')} - /> - - + + } + label={t('Rent')} + /> + } + label={t('Own')} + /> + + + )} {isRequestingChange && ( { + return ( + <> + + + + ); +}; diff --git a/src/components/Reports/MinisterHousingAllowance/mockData.ts b/src/components/Reports/MinisterHousingAllowance/mockData.ts index db378169de..b408163ff4 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: { @@ -33,5 +34,6 @@ export const mockMHARequest: MHARequest = { hrApprovedAt: '2019-11-01T15:30:45.123Z', spouseSpecific: null, staffSpecific: 15000, + changesRequestedAt: null, }, }; diff --git a/src/components/Reports/SalaryCalculator/CurrentStep.tsx b/src/components/Reports/SalaryCalculator/CurrentStep.tsx index d93a3f7f0e..3cf659feb5 100644 --- a/src/components/Reports/SalaryCalculator/CurrentStep.tsx +++ b/src/components/Reports/SalaryCalculator/CurrentStep.tsx @@ -1,24 +1,20 @@ 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(); - switch (currentStep) { - case SalaryCalculatorSectionEnum.EffectiveDate: - return ; - case SalaryCalculatorSectionEnum.YourInformation: - return ; - case SalaryCalculatorSectionEnum.SalaryCalculation: - return ; - case SalaryCalculatorSectionEnum.Summary: - return ; - case SalaryCalculatorSectionEnum.Receipt: - return ; - } + const steps = [ + , + , + , + , + , + ]; + + return steps[currentIndex] ?? null; }; diff --git a/src/components/Reports/SalaryCalculator/EffectiveDateStep/EffectiveDateStep.test.tsx b/src/components/Reports/SalaryCalculator/EffectiveDateStep/EffectiveDateStep.test.tsx index 187adb8a45..0173388707 100644 --- a/src/components/Reports/SalaryCalculator/EffectiveDateStep/EffectiveDateStep.test.tsx +++ b/src/components/Reports/SalaryCalculator/EffectiveDateStep/EffectiveDateStep.test.tsx @@ -32,36 +32,38 @@ describe('EffectiveDateStep', () => { Settings.now = () => now; }); - it('renders the heading', () => { - const { getByRole } = render(); + it('renders the heading', async () => { + const { findByRole } = render(); expect( - getByRole('heading', { name: 'Effective Date' }), + await findByRole('heading', { name: 'Effective Date' }), ).toBeInTheDocument(); }); - it('renders the date selection dropdown', () => { - const { getByRole } = render(); + it('renders the date selection dropdown', async () => { + const { findByRole } = render(); expect( - getByRole('combobox', { name: 'Select a future date' }), + await findByRole('combobox', { name: 'Select a future date' }), ).toBeInTheDocument(); }); - it('renders text content', () => { - const { getByText } = render(); + it('renders text content', async () => { + const { findByText } = render(); expect( - getByText( + await findByText( 'Please select the date of the paycheck you would like this change to first occur.', ), ).toBeInTheDocument(); }); - it('renders an empty dropdown when no effective dates are available', () => { - const { getByRole } = render(); + it('renders an empty dropdown when no effective dates are available', async () => { + const { findByRole } = render(); - const dropdown = getByRole('combobox', { name: 'Select a future date' }); + const dropdown = await findByRole('combobox', { + name: 'Select a future date', + }); // The dropdown should be empty since hcm.effectiveDates doesn't exist yet expect(dropdown).toBeInTheDocument(); }); diff --git a/src/components/Reports/SalaryCalculator/Receipt/Receipt.test.tsx b/src/components/Reports/SalaryCalculator/Receipt/Receipt.test.tsx index 157d54363a..5de5e3c23f 100644 --- a/src/components/Reports/SalaryCalculator/Receipt/Receipt.test.tsx +++ b/src/components/Reports/SalaryCalculator/Receipt/Receipt.test.tsx @@ -41,10 +41,10 @@ This may affect your selected effective date. We will review your request throug ); }); - it('should show summary when user clicks to view receipt', () => { - const { getByText, getByRole } = render(); + it('should show summary when user clicks to view receipt', async () => { + const { getByText, findByRole } = render(); - userEvent.click(getByRole('button', { name: /View or print/ })); + userEvent.click(await findByRole('button', { name: /View or print/ })); expect(getByText('New Salary Calculation Summary')).toBeInTheDocument(); }); }); diff --git a/src/components/Reports/SalaryCalculator/SalaryCalculation/RequestSummaryCard/RequestSummaryCard.test.tsx b/src/components/Reports/SalaryCalculator/SalaryCalculation/RequestSummaryCard/RequestSummaryCard.test.tsx index 1b96c9878a..0a0654b789 100644 --- a/src/components/Reports/SalaryCalculator/SalaryCalculation/RequestSummaryCard/RequestSummaryCard.test.tsx +++ b/src/components/Reports/SalaryCalculator/SalaryCalculation/RequestSummaryCard/RequestSummaryCard.test.tsx @@ -38,9 +38,9 @@ const TestComponent: React.FC = (props) => ( describe('RequestSummaryCard', () => { it('renders status message', async () => { - const { getByTestId } = render(); + const { findByTestId } = render(); - expect(getByTestId('RequestSummaryCard-status')).toHaveTextContent( + expect(await findByTestId('RequestSummaryCard-status')).toHaveTextContent( 'Your gross request is within your Maximum Allowable Salary.', ); }); diff --git a/src/components/Reports/SalaryCalculator/SalaryCalculatorContext/SalaryCalculatorContext.test.tsx b/src/components/Reports/SalaryCalculator/SalaryCalculatorContext/SalaryCalculatorContext.test.tsx index 9cc1fea89e..fc6007ea0a 100644 --- a/src/components/Reports/SalaryCalculator/SalaryCalculatorContext/SalaryCalculatorContext.test.tsx +++ b/src/components/Reports/SalaryCalculator/SalaryCalculatorContext/SalaryCalculatorContext.test.tsx @@ -26,8 +26,8 @@ describe('SalaryCalculator', () => { }); it('renders individual step items', async () => { - const { getAllByRole } = render(); + const { findAllByRole } = render(); - expect(getAllByRole('listitem')).toHaveLength(5); + expect(await findAllByRole('listitem')).toHaveLength(5); }); }); diff --git a/src/components/Reports/SalaryCalculator/SalaryCalculatorContext/SalaryCalculatorContext.tsx b/src/components/Reports/SalaryCalculator/SalaryCalculatorContext/SalaryCalculatorContext.tsx index ad43e5b6fb..e1615fcf10 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,16 @@ 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, + handleNextStep, + handlePreviousStep, + currentIndex, + percentComplete, + } = useStepList(FormEnum.SalaryCalc); const [isDrawerOpen, setDrawerOpen] = useState(true); const { data: hcmData } = useHcmQuery(); @@ -99,7 +82,6 @@ export const SalaryCalculatorProvider: React.FC< steps, currentIndex, percentComplete, - currentStep, handleNextStep, handlePreviousStep, isDrawerOpen, @@ -114,7 +96,6 @@ export const SalaryCalculatorProvider: React.FC< steps, currentIndex, percentComplete, - currentStep, handleNextStep, handlePreviousStep, isDrawerOpen, @@ -124,6 +105,19 @@ export const SalaryCalculatorProvider: React.FC< ], ); + if (!calculationData) { + return ( + + + + ); + } + return ( {children} diff --git a/src/components/Reports/SalaryCalculator/StepNavigation/StepNavigation.test.tsx b/src/components/Reports/SalaryCalculator/StepNavigation/StepNavigation.test.tsx index ecc5ae7eaa..385c3c3ff1 100644 --- a/src/components/Reports/SalaryCalculator/StepNavigation/StepNavigation.test.tsx +++ b/src/components/Reports/SalaryCalculator/StepNavigation/StepNavigation.test.tsx @@ -10,10 +10,9 @@ const TestComponent: React.FC = () => ( ); describe('StepNavigation', () => { - it('renders back and continue buttons', () => { - const { getByText } = render(); - - expect(getByText('Back')).toBeInTheDocument(); + it('renders back and continue buttons', async () => { + const { getByText, findByText } = render(); + expect(await findByText('Back')).toBeInTheDocument(); expect(getByText('Continue')).toBeInTheDocument(); }); }); diff --git a/src/components/Reports/SalaryCalculator/StepNavigation/StepNavigation.tsx b/src/components/Reports/SalaryCalculator/StepNavigation/StepNavigation.tsx index f7882cdae6..3fe744266f 100644 --- a/src/components/Reports/SalaryCalculator/StepNavigation/StepNavigation.tsx +++ b/src/components/Reports/SalaryCalculator/StepNavigation/StepNavigation.tsx @@ -4,7 +4,6 @@ import ChevronRightIcon from '@mui/icons-material/ChevronRight'; import { Box, Button, Stack, Typography } from '@mui/material'; import { useTheme } from '@mui/material/styles'; import { useTranslation } from 'react-i18next'; -import { SalaryCalculatorSectionEnum } from '../SalaryCalculatorContext/Helper/sharedTypes'; import { useSalaryCalculator } from '../SalaryCalculatorContext/SalaryCalculatorContext'; import { useSubmitSalaryCalculationMutation } from './SubmitSalaryCalculation.generated'; @@ -76,17 +75,13 @@ export const SubmitButton: React.FC = () => { export const StepNavigation: React.FC = () => { const theme = useTheme(); - const { currentStep } = useSalaryCalculator(); + const { currentIndex } = useSalaryCalculator(); return ( - {currentStep === SalaryCalculatorSectionEnum.Summary ? ( - - ) : ( - - )} + {currentIndex === 3 ? : } ); diff --git a/src/components/Reports/SalaryCalculator/Summary/ContactInfoForm.test.tsx b/src/components/Reports/SalaryCalculator/Summary/ContactInfoForm.test.tsx index 502d1ab7d8..961e062d4d 100644 --- a/src/components/Reports/SalaryCalculator/Summary/ContactInfoForm.test.tsx +++ b/src/components/Reports/SalaryCalculator/Summary/ContactInfoForm.test.tsx @@ -9,10 +9,12 @@ const TestComponent: React.FC = () => ( ); describe('ContactInfoForm', () => { - it('renders the inputs', () => { - const { getByRole } = render(); + it('renders the inputs', async () => { + const { getByRole, findByRole } = render(); - expect(getByRole('textbox', { name: 'Phone Number' })).toBeInTheDocument(); + expect( + await findByRole('textbox', { name: 'Phone Number' }), + ).toBeInTheDocument(); expect(getByRole('textbox', { name: 'Email' })).toBeInTheDocument(); }); }); diff --git a/src/components/Reports/SalaryCalculator/YourInformation/MhaRequestSection/MhaRequestSection.test.tsx b/src/components/Reports/SalaryCalculator/YourInformation/MhaRequestSection/MhaRequestSection.test.tsx index f11f7e0ae3..1f02149138 100644 --- a/src/components/Reports/SalaryCalculator/YourInformation/MhaRequestSection/MhaRequestSection.test.tsx +++ b/src/components/Reports/SalaryCalculator/YourInformation/MhaRequestSection/MhaRequestSection.test.tsx @@ -1,4 +1,4 @@ -import { render, waitFor } from '@testing-library/react'; +import { render } from '@testing-library/react'; import { SalaryCalculatorTestWrapper } from '../../SalaryCalculatorTestWrapper'; import { MhaRequestSection } from './MhaRequestSection'; @@ -33,11 +33,13 @@ describe('MhaRequestSection', () => { }); it('should render new requested MHA input fields for both spouses', async () => { - const { getAllByRole } = render(); + const { findAllByRole } = render(); - await waitFor(() => { - expect(getAllByRole('textbox')).toHaveLength(2); - }); + const enabledTextbox = (await findAllByRole('textbox')).filter( + (textbox) => !textbox.hasAttribute('disabled'), + ); + + expect(enabledTextbox).toHaveLength(2); }); it('should display progress bar', async () => { diff --git a/src/components/Reports/SalaryCalculator/YourInformation/PersonalInformationSection/PersonalInformationSection.test.tsx b/src/components/Reports/SalaryCalculator/YourInformation/PersonalInformationSection/PersonalInformationSection.test.tsx index d96bd87758..5a98647d52 100644 --- a/src/components/Reports/SalaryCalculator/YourInformation/PersonalInformationSection/PersonalInformationSection.test.tsx +++ b/src/components/Reports/SalaryCalculator/YourInformation/PersonalInformationSection/PersonalInformationSection.test.tsx @@ -28,11 +28,13 @@ describe('PersonalInformationSection', () => { ).toBeInTheDocument(); }); - it('should display category row headers', () => { - const { getByRole } = render(); + it('should display category row headers', async () => { + const { getByRole, findByRole } = render(); expect( - getByRole('combobox', { name: 'Nearest Geographic Multiplier Location' }), + await findByRole('combobox', { + name: 'Nearest Geographic Multiplier Location', + }), ).toBeInTheDocument(); expect(getByRole('cell', { name: 'Tenure' })).toBeInTheDocument(); expect(getByRole('cell', { name: 'Age' })).toBeInTheDocument(); diff --git a/src/components/Reports/Shared/CalculationReports/DirectionButtons/DirectionButtons.test.tsx b/src/components/Reports/Shared/CalculationReports/DirectionButtons/DirectionButtons.test.tsx index b47386e4bf..48ec0786c8 100644 --- a/src/components/Reports/Shared/CalculationReports/DirectionButtons/DirectionButtons.test.tsx +++ b/src/components/Reports/Shared/CalculationReports/DirectionButtons/DirectionButtons.test.tsx @@ -67,50 +67,54 @@ jest.mock('next/router', () => ({ })); describe('DirectionButtons', () => { - it('renders Back and Submit buttons', () => { - const { getByRole } = render( + it('renders Back and Submit buttons', async () => { + const { getByRole, findByRole } = render( , ); - expect(getByRole('button', { name: /back/i })).toBeInTheDocument(); + expect(await findByRole('button', { name: /back/i })).toBeInTheDocument(); expect(getByRole('button', { name: /submit/i })).toBeInTheDocument(); }); it('calls handleNext when Continue is clicked', async () => { - const { getByRole } = render(); + const { findByRole } = render(); - await userEvent.click(getByRole('button', { name: 'Continue' })); + userEvent.click(await findByRole('button', { name: 'Continue' })); expect(handleNextStep).toHaveBeenCalled(); }); it('calls overrideNext when provided and Continue is clicked', async () => { - const { getByRole } = render(); + const { findByRole } = render( + , + ); - await userEvent.click(getByRole('button', { name: 'Continue' })); + userEvent.click(await findByRole('button', { name: 'Continue' })); expect(handleNextStep).not.toHaveBeenCalled(); expect(overrideNext).toHaveBeenCalled(); }); it('calls handlePreviousStep when Back is clicked', async () => { - const { getByRole } = render(); + const { findByRole } = render(); - await userEvent.click(getByRole('button', { name: /back/i })); + userEvent.click(await findByRole('button', { name: /back/i })); expect(handlePreviousStep).toHaveBeenCalled(); }); - it('renders custom button title when provided', () => { - const { getByRole } = render(); + it('renders custom button title when provided', async () => { + const { findByRole } = render(); - expect(getByRole('button', { name: title })).toBeInTheDocument(); + expect(await findByRole('button', { name: title })).toBeInTheDocument(); }); - it('renders Cancel button when handleCancel is provided', () => { - const { getByRole } = render(); + it('renders Cancel button when handleCancel is provided', async () => { + const { findByRole } = render( + , + ); - expect(getByRole('button', { name: /cancel/i })).toBeInTheDocument(); + expect(await findByRole('button', { name: /cancel/i })).toBeInTheDocument(); }); it('does not render Cancel button when handleCancel is not provided', () => { @@ -120,9 +124,11 @@ describe('DirectionButtons', () => { }); it('calls handleCancel when Cancel button is clicked', async () => { - const { getByRole } = render(); + const { findByRole } = render( + , + ); - userEvent.click(getByRole('button', { name: /cancel/i })); + userEvent.click(await findByRole('button', { name: /cancel/i })); expect(handleCancel).toHaveBeenCalled(); }); diff --git a/src/components/Reports/Shared/CalculationReports/PanelLayout/BackArrow.test.tsx b/src/components/Reports/Shared/CalculationReports/PanelLayout/BackArrow.test.tsx new file mode 100644 index 0000000000..bcd9697ed5 --- /dev/null +++ b/src/components/Reports/Shared/CalculationReports/PanelLayout/BackArrow.test.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { ThemeProvider } from '@mui/material/styles'; +import { render } from '__tests__/util/testingLibraryReactMock'; +import theme from 'src/theme'; +import { BackArrow } from './BackArrow'; + +const href = '/test-back'; +const title = 'Go back'; + +interface TestComponentProps { + backTitle?: string; +} + +const TestComponent: React.FC = ({ backTitle }) => ( + + + +); + +describe('BackArrow', () => { + it('renders BackArrow with provided title', () => { + const { getByRole } = render(); + + expect(getByRole('button', { name: title })).toBeInTheDocument(); + }); + + it('renders BackArrow with default title when backTitle is not provided', () => { + const { getByRole } = render(); + + expect( + getByRole('button', { name: 'Back to dashboard' }), + ).toBeInTheDocument(); + }); +}); diff --git a/src/components/Reports/Shared/CalculationReports/PanelLayout/BackArrow.tsx b/src/components/Reports/Shared/CalculationReports/PanelLayout/BackArrow.tsx new file mode 100644 index 0000000000..cf22ccde8c --- /dev/null +++ b/src/components/Reports/Shared/CalculationReports/PanelLayout/BackArrow.tsx @@ -0,0 +1,35 @@ +import NextLink from 'next/link'; +import React from 'react'; +import ArrowBackIcon from '@mui/icons-material/ArrowBack'; +import { IconButton, Link } from '@mui/material'; +import { useTranslation } from 'react-i18next'; + +interface BackArrowProps { + backHref: string; + backTitle?: string; +} + +export const BackArrow: React.FC = ({ + backHref, + backTitle, +}) => { + const { t } = useTranslation(); + + return ( + + ({ + color: theme.palette.mpdxGrayDark.main, + })} + > + + + + ); +}; diff --git a/src/components/Reports/Shared/CalculationReports/PanelLayout/PanelLayout.tsx b/src/components/Reports/Shared/CalculationReports/PanelLayout/PanelLayout.tsx index 7f595e7b87..12d5cad746 100644 --- a/src/components/Reports/Shared/CalculationReports/PanelLayout/PanelLayout.tsx +++ b/src/components/Reports/Shared/CalculationReports/PanelLayout/PanelLayout.tsx @@ -1,9 +1,6 @@ -import NextLink from 'next/link'; import React from 'react'; import { CheckCircleOutline } from '@mui/icons-material'; -import ArrowBackIcon from '@mui/icons-material/ArrowBack'; -import { Box, Divider, IconButton, Link, Stack } from '@mui/material'; -import { useTranslation } from 'react-i18next'; +import { Box, Divider, IconButton, Stack } from '@mui/material'; import { CircularProgressWithLabel } from 'src/components/Reports/Shared/CalculationReports/CircularProgressWithLabel/CircularProgressWithLabel'; import { MainContent, @@ -16,6 +13,7 @@ import { import { Steps } from 'src/components/Reports/Shared/CalculationReports/StepsList/StepsList'; import theme from 'src/theme'; import { PanelTypeEnum } from '../Shared/sharedTypes'; +import { BackArrow } from './BackArrow'; export interface IconPanelItem { key: string; @@ -54,15 +52,17 @@ export const PanelLayout: React.FC = ({ currentIndex, steps, }) => { - const { t } = useTranslation(); - const isLastStep = steps ? currentIndex === steps.length - 1 : false; return ( {panelType === PanelTypeEnum.Empty ? ( <> - + + {backHref && ( + + )} + {sidebarTitle && ( @@ -106,21 +106,7 @@ export const PanelLayout: React.FC = ({ {item.icon} ))} - - ({ - color: theme.palette.mpdxGrayDark.main, - })} - > - - - + )} diff --git a/src/components/Reports/Shared/CalculationReports/ReceiptStep/Receipt.test.tsx b/src/components/Reports/Shared/CalculationReports/ReceiptStep/Receipt.test.tsx index 45b576c59e..ca5d497ac1 100644 --- a/src/components/Reports/Shared/CalculationReports/ReceiptStep/Receipt.test.tsx +++ b/src/components/Reports/Shared/CalculationReports/ReceiptStep/Receipt.test.tsx @@ -64,11 +64,13 @@ const TestComponent: React.FC = ({ ); describe('Receipt', () => { - it('renders the component in new page', () => { - const { getByRole } = render(); + it('renders the component in new page', async () => { + const { getByRole, findByRole } = render( + , + ); expect( - getByRole('heading', { + await findByRole('heading', { name: 'Thank you for Submitting your Test Title!', }), ).toBeInTheDocument(); @@ -83,13 +85,13 @@ describe('Receipt', () => { expect(getByRole('button', { name: /view form/i })).toBeInTheDocument(); }); - it('renders the component in edit page', () => { - const { getByRole } = render( + it('renders the component in edit page', async () => { + const { getByRole, findByRole } = render( , ); expect( - getByRole('heading', { + await findByRole('heading', { name: 'Thank you for updating your Test Title!', }), ).toBeInTheDocument(); @@ -104,8 +106,8 @@ describe('Receipt', () => { expect(getByRole('button', { name: /view form/i })).toBeInTheDocument(); }); - it('should change text when dates are null', () => { - const { getByText } = render( + it('should change text when dates are null', async () => { + const { findByText } = render( { ); expect( - getByText( + await findByText( /we will review your information and you will receive notice for your approval soon./i, ), ).toBeInTheDocument(); }); - it('should go to edit link when clicked', () => { - const { getByRole } = render( + it('should go to edit link when clicked', async () => { + const { findByRole } = render( , ); - const editButton = getByRole('link', { + const editButton = await findByRole('link', { name: /edit your mha request \(not available after/i, }); @@ -135,12 +137,12 @@ describe('Receipt', () => { ); }); - it('should render custom alert text when provided', () => { - const { getByText } = render( + it('should render custom alert text when provided', async () => { + const { findByText } = render( , ); - expect(getByText(alertText)).toBeInTheDocument(); + expect(await findByText(alertText)).toBeInTheDocument(); }); it('should not render edit link when not provided', () => { @@ -153,12 +155,12 @@ describe('Receipt', () => { expect(editButton).not.toBeInTheDocument(); }); - it('should render edit link when provided', () => { - const { getByRole } = render( + it('should render edit link when provided', async () => { + const { findByRole } = render( , ); - const editButton = getByRole('link', { + const editButton = await findByRole('link', { name: /edit your mha request \(not available after/i, }); @@ -167,11 +169,11 @@ describe('Receipt', () => { }); it('should go to view link when View clicked', async () => { - const { getByRole } = render( + const { findByRole } = render( , ); - const viewButton = getByRole('link', { name: /view form/i }); + const viewButton = await findByRole('link', { name: /view form/i }); expect(viewButton).toHaveAttribute( 'href', @@ -183,11 +185,11 @@ describe('Receipt', () => { }); it('should go to view link when Print clicked', async () => { - const { getByRole } = render( + const { findByRole } = render( , ); - const viewButton = getByRole('link', { name: /print a copy/i }); + const viewButton = await findByRole('link', { name: /print a copy/i }); expect(viewButton).toHaveAttribute( 'href', 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 d639e7a4a0..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} @@ -76,45 +79,47 @@ const TestComponent: React.FC = ({ }; describe('CardSkeleton', () => { - it('should render card header no subtitle', () => { - const { getByText, getByTestId, queryByText } = render(); + it('should render card header no subtitle', async () => { + const { getByText, getByTestId, queryByText, findByTestId } = render( + , + ); - expect(getByTestId('mock-icon')).toBeInTheDocument(); + expect(await findByTestId('mock-icon')).toBeInTheDocument(); expect(getByText(title)).toBeInTheDocument(); expect(queryByText(subtitle)).not.toBeInTheDocument(); expect(getByTestId('FileDownloadIcon')).toBeInTheDocument(); }); - it('should render card header with subtitle', () => { - const { getByText, getByTestId } = render( + it('should render card header with subtitle', async () => { + const { getByText, getByTestId, findByTestId } = render( , ); - expect(getByTestId('mock-icon')).toBeInTheDocument(); + expect(await findByTestId('mock-icon')).toBeInTheDocument(); expect(getByText(title)).toBeInTheDocument(); expect(getByText(subtitle)).toBeInTheDocument(); expect(getByTestId('FileDownloadIcon')).toBeInTheDocument(); }); - it('should render children', () => { - const { getByText } = render(); + it('should render children', async () => { + const { findByText } = render(); - expect(getByText('Test Children')).toBeInTheDocument(); + expect(await findByText('Test Children')).toBeInTheDocument(); }); - it('should render action buttons', () => { - const { getByText } = render(); + it('should render action buttons', async () => { + const { getByText, findByText } = render(); - expect(getByText(titleOne)).toBeInTheDocument(); + expect(await findByText(titleOne)).toBeInTheDocument(); expect(getByText(titleTwo)).toBeInTheDocument(); }); - it('should go to correct link when first button is clicked', () => { - const { getByRole } = render( + it('should go to correct link when first button is clicked', async () => { + const { findByRole } = render( , ); - const firstButton = getByRole('link', { name: titleOne }); + const firstButton = await findByRole('link', { name: titleOne }); expect(firstButton).toHaveAttribute( 'href', @@ -122,12 +127,12 @@ describe('CardSkeleton', () => { ); }); - it('should go to correct link when second button is clicked', () => { - const { getByRole } = render( + it('should go to correct link when second button is clicked', async () => { + const { findByRole } = render( , ); - const secondButton = getByRole('link', { name: titleTwo }); + const secondButton = await findByRole('link', { name: titleTwo }); expect(secondButton).toHaveAttribute( 'href', @@ -139,14 +144,14 @@ describe('CardSkeleton', () => { const { getByRole, findByRole, getByText, queryByRole } = render( , ); - const cancelButton = getByRole('button', { name: 'Cancel Request' }); + const cancelButton = await findByRole('button', { name: 'Cancel Request' }); - await userEvent.click(cancelButton); + userEvent.click(cancelButton); expect(await findByRole('dialog')).toBeInTheDocument(); expect(getByText('Do you want to cancel?')).toBeInTheDocument(); - await userEvent.click(getByRole('button', { name: /yes, cancel/i })); + userEvent.click(getByRole('button', { name: /yes, cancel/i })); expect(queryByRole('dialog')).not.toBeInTheDocument(); }); @@ -165,11 +170,11 @@ describe('CardSkeleton', () => { }); it('calls handleDownload when download icon is clicked', async () => { - const { getByTestId } = render(); + const { findByTestId } = render(); - const downloadIcon = getByTestId('FileDownloadIcon'); + const downloadIcon = await findByTestId('FileDownloadIcon'); - await userEvent.click(downloadIcon); + userEvent.click(downloadIcon); expect(handleDownload).toHaveBeenCalled(); }); @@ -178,17 +183,23 @@ describe('CardSkeleton', () => { const { getByRole, findByRole, getByText, queryByRole } = render( , ); - const cancelButton = getByRole('button', { name: 'Cancel Request' }); + const cancelButton = await findByRole('button', { name: 'Cancel Request' }); - await userEvent.click(cancelButton); + userEvent.click(cancelButton); expect(await findByRole('dialog')).toBeInTheDocument(); expect(getByText('Do you want to cancel?')).toBeInTheDocument(); - await userEvent.click(getByRole('button', { name: /yes, cancel/i })); + userEvent.click(getByRole('button', { name: /yes, cancel/i })); expect(handleConfirmCancel).toHaveBeenCalled(); 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 b4d7df9ab7..e4d137c639 100644 --- a/src/components/Reports/Shared/CalculationReports/StatusCard/StatusCard.tsx +++ b/src/components/Reports/Shared/CalculationReports/StatusCard/StatusCard.tsx @@ -11,14 +11,13 @@ import { CardHeader, Divider, IconButton, + SxProps, + Theme, Typography, } from '@mui/material'; 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; @@ -33,8 +32,11 @@ interface StatusCardProps { isRequest?: boolean; hideDownload?: boolean; hideActions?: boolean; + hideLinkTwoButton?: boolean; handleDownload?: () => void; handleConfirmCancel: () => void; + handleLinkTwo?: () => void; + styling?: SxProps; } export const StatusCard: React.FC = ({ @@ -51,8 +53,11 @@ export const StatusCard: React.FC = ({ isRequest, hideDownload, hideActions, + hideLinkTwoButton, handleDownload, handleConfirmCancel, + handleLinkTwo, + styling, }) => { const { t } = useTranslation(); @@ -73,14 +78,14 @@ export const StatusCard: React.FC = ({ {subtitle ? ( - + {title} @@ -102,7 +107,7 @@ export const StatusCard: React.FC = ({ } /> - {children} + {children} {!hideActions && ( @@ -114,14 +119,17 @@ export const StatusCard: React.FC = ({ > {linkOneText} - + {!hideLinkTwoButton && ( + + )} {isRequest && (