diff --git a/src/api/private.ts b/src/api/private.ts index 02221b9..3362a54 100644 --- a/src/api/private.ts +++ b/src/api/private.ts @@ -113,6 +113,11 @@ export const applicationStartFn = async (id: number) => { return response.data; }; +export const applicationLapseFn = async (id: number) => { + const response = await authApi.post(`applications/${id}/lapse`); + return response.data; +}; + export const verifyDataFieldFn = async (awardData: IUpdateBorrower) => { const { application_id, ...payload } = awardData; const response = await authApi.put(`applications/${application_id}/verify-data-field`, payload); diff --git a/src/assets/icons/approve.svg b/src/assets/icons/approve.svg new file mode 100644 index 0000000..6293673 --- /dev/null +++ b/src/assets/icons/approve.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/lapse.svg b/src/assets/icons/lapse.svg new file mode 100644 index 0000000..8e20cc7 --- /dev/null +++ b/src/assets/icons/lapse.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/icons/reject.svg b/src/assets/icons/reject.svg new file mode 100644 index 0000000..dfc5f73 --- /dev/null +++ b/src/assets/icons/reject.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/hooks/useLapseApplication.ts b/src/hooks/useLapseApplication.ts new file mode 100644 index 0000000..452efa4 --- /dev/null +++ b/src/hooks/useLapseApplication.ts @@ -0,0 +1,49 @@ +import { type UseMutateFunction, useMutation, useQueryClient } from "@tanstack/react-query"; +import { useT } from "@transifex/react"; +import axios from "axios"; +import { useSnackbar } from "notistack"; +import { useNavigate } from "react-router-dom"; + +import { applicationLapseFn } from "../api/private"; +import { DISPATCH_ACTIONS, QUERY_KEYS } from "../constants"; +import type { IApplication } from "../schemas/application"; +import useApplicationContext from "./useSecureApplicationContext"; + +type IUseLapseApplication = { + lapseApplicationMutation: UseMutateFunction; + isLoading: boolean; +}; + +export default function useLapseApplication(): IUseLapseApplication { + const t = useT(); + const navigate = useNavigate(); + const queryClient = useQueryClient(); + const applicationContext = useApplicationContext(); + const { enqueueSnackbar } = useSnackbar(); + + const { mutate: lapseApplicationMutation, isLoading } = useMutation( + (id) => applicationLapseFn(id), + { + onSuccess: (data) => { + queryClient.setQueryData([QUERY_KEYS.applications, data.id], data); + applicationContext.dispatch({ type: DISPATCH_ACTIONS.SET_APPLICATION, payload: data }); + navigate(`/applications/${data.id}/stage-five-lapsed`); + }, + onError: (error) => { + if (axios.isAxiosError(error) && error.response) { + if (error.response.data?.detail) { + enqueueSnackbar(t("Error: {error}", { error: error.response.data.detail }), { + variant: "error", + }); + } + } else { + enqueueSnackbar(t("Error lapsing the application. {error}", { error }), { + variant: "error", + }); + } + }, + }, + ); + + return { lapseApplicationMutation, isLoading }; +} diff --git a/src/layout/SecureApplicationLayout.tsx b/src/layout/SecureApplicationLayout.tsx index 2f0594e..7c4bea6 100644 --- a/src/layout/SecureApplicationLayout.tsx +++ b/src/layout/SecureApplicationLayout.tsx @@ -54,7 +54,7 @@ export default function SecureApplicationLayout() { if (lastSegment !== "view") { if (application.status === APPLICATION_STATUS.LAPSED) { - navigate("./view"); + if (lastSegment !== "stage-five-lapsed") navigate("./stage-five-lapsed"); } else if (application.status === APPLICATION_STATUS.APPROVED) { if (lastSegment !== "application-completed") navigate("./application-completed"); } else if (application.status === APPLICATION_STATUS.REJECTED) { diff --git a/src/pages/fi/LapseApplicationDialog.tsx b/src/pages/fi/LapseApplicationDialog.tsx new file mode 100644 index 0000000..3528681 --- /dev/null +++ b/src/pages/fi/LapseApplicationDialog.tsx @@ -0,0 +1,61 @@ +import { Box, Dialog } from "@mui/material"; +import { useT } from "@transifex/react"; +import useApplicationContext from "src/hooks/useSecureApplicationContext"; +import Button from "src/stories/button/Button"; +import Title from "src/stories/title/Title"; + +import useLapseApplication from "src/hooks/useLapseApplication"; + +export interface LapseApplicationDialogProps { + open: boolean; + handleClose: () => void; +} + +export function LapseApplicationDialog({ open, handleClose }: LapseApplicationDialogProps) { + const t = useT(); + const applicationContext = useApplicationContext(); + const application = applicationContext.state.data; + const { isLoading, lapseApplicationMutation } = useLapseApplication(); + + const rootElement = document.getElementById("root-app"); + + return ( + + + + + <div> + {t( + "Are you sure you want to mark this application as lapsed? This action can't be undone. The application won't be listed in your application list anymore and you won't be able to approve or reject the application.", + )} + </div> + + <div className="mt-4 grid grid-cols-1 gap-4 md:flex md:justify-end md:gap-0"> + <div> + <Button + primary={false} + disabled={isLoading} + className="md:mr-4" + label={t("Cancel")} + onClick={handleClose} + /> + </div> + + <div> + <Button + label={t("Lapse")} + disabled={isLoading} + onClick={() => { + if (application?.id) { + lapseApplicationMutation(application?.id); + } + }} + /> + </div> + </div> + </Box> + </Dialog> + ); +} + +export default LapseApplicationDialog; diff --git a/src/pages/fi/StageFive.tsx b/src/pages/fi/StageFive.tsx index 31cea86..0d182d5 100644 --- a/src/pages/fi/StageFive.tsx +++ b/src/pages/fi/StageFive.tsx @@ -13,12 +13,14 @@ import FormInput from "src/stories/form-input/FormInput"; import Text from "src/stories/text/Text"; import Title from "src/stories/title/Title"; -import CheckChecked from "../../assets/icons/check-checked.svg"; -import WarnRed from "../../assets/icons/warn-red.svg"; +import Approve from "../../assets/icons/approve.svg"; +import Lapse from "../../assets/icons/lapse.svg"; +import Reject from "../../assets/icons/reject.svg"; import CreditProductReview from "../../components/CreditProductReview"; import useApproveApplication from "../../hooks/useApproveApplication"; import useLangContext from "../../hooks/useLangContext"; import { type ApproveApplicationInput, type FormApprovedInput, approveSchema } from "../../schemas/application"; +import LapseApplicationDialog from "./LapseApplicationDialog"; import RejectApplicationDialog from "./RejectApplicationDialog"; export function StageFive() { @@ -27,6 +29,7 @@ export function StageFive() { const applicationContext = useApplicationContext(); const application = applicationContext.state.data; const [openDialog, setOpenDialog] = useState<boolean>(false); + const [openLapseDialog, setOpenLapseDialog] = useState<boolean>(false); const langContext = useLangContext(); const StepImage = langContext.state.selected.startsWith("en") ? StepImageEN : StepImageES; @@ -41,6 +44,14 @@ export function StageFive() { setOpenDialog(true); }; + const handleCloseLapse = () => { + setOpenLapseDialog(false); + }; + + const onLapseApplication = () => { + setOpenLapseDialog(true); + }; + const methods = useForm<FormApprovedInput>({ resolver: zodResolver(approveSchema), }); @@ -110,7 +121,7 @@ export function StageFive() { type="currency" placeholder={t("Credit amount")} /> - <div className="mt-6 md:mb-8 grid grid-cols-1 gap-4 md:flex md:gap-0"> + <div className="mt-6 md:mb-8 grid grid-cols-1 gap-5 md:flex md:gap-0"> <div> <Button primary={false} className="md:mr-4" label={t("Go Home")} onClick={onGoHomeHandler} /> </div> @@ -119,18 +130,27 @@ export function StageFive() { <Button primary={false} className="md:mr-4" label={t("Go Back")} onClick={onGoBackHandler} /> </div> + <div> + <Button className="md:mr-4" icon={Approve} label={t("Approve")} type="submit" disabled={isLoading} /> + </div> + <div> <Button className="md:mr-4" - icon={CheckChecked} - label={t("Approve")} - type="submit" + label={t("Reject")} + icon={Reject} + onClick={onRejectApplication} disabled={isLoading} /> </div> - <div> - <Button label={t("Reject")} icon={WarnRed} onClick={onRejectApplication} disabled={isLoading} /> + <Button + className="md:mr-4" + label={t("Lapse")} + icon={Lapse} + onClick={onLapseApplication} + disabled={isLoading} + /> </div> </div> <Text className="mb-10 text-m font-light"> @@ -141,6 +161,7 @@ export function StageFive() { </Box> </FormProvider> <RejectApplicationDialog open={openDialog} handleClose={handleClose} /> + <LapseApplicationDialog open={openLapseDialog} handleClose={handleCloseLapse} /> </> ); } diff --git a/src/pages/fi/StageFiveLapsed.tsx b/src/pages/fi/StageFiveLapsed.tsx new file mode 100644 index 0000000..3437105 --- /dev/null +++ b/src/pages/fi/StageFiveLapsed.tsx @@ -0,0 +1,45 @@ +import { useT } from "@transifex/react"; +import { useNavigate } from "react-router-dom"; +import StepImageEN from "src/assets/pages/en/stage-five.svg"; +import StepImageES from "src/assets/pages/es/stage-five.svg"; +import useApplicationContext from "src/hooks/useSecureApplicationContext"; +import Button from "src/stories/button/Button"; +import Text from "src/stories/text/Text"; +import Title from "src/stories/title/Title"; + +import useLangContext from "../../hooks/useLangContext"; + +export function StageFiveLapsed() { + const t = useT(); + const navigate = useNavigate(); + const applicationContext = useApplicationContext(); + const application = applicationContext.state.data; + + const langContext = useLangContext(); + const StepImage = langContext.state.selected.startsWith("en") ? StepImageEN : StepImageES; + + const onGoHomeHandler = () => { + navigate("/"); + }; + + return ( + <> + <Title type="page" label={t("Application Approval Process")} className="mb-4" /> + <Text className="text-lg mb-12">{application?.borrower.legal_name}</Text> + <img className="mb-14 ml-8" src={StepImage} alt="step" /> + <Title type="section" label={t("Stage 5: Approve")} className="mb-8" /> + + <Text className="mb-8"> + {t("The credit application has been lapsed. You won't see this application in your application list anymore.")} + </Text> + + <div className="mt-6 md:mb-8 grid grid-cols-1 gap-4 md:flex md:gap-0"> + <div> + <Button className="md:mr-4" label={t("Back to home")} onClick={onGoHomeHandler} /> + </div> + </div> + </> + ); +} + +export default StageFiveLapsed; diff --git a/src/pages/fi/StageFiveRejected.tsx b/src/pages/fi/StageFiveRejected.tsx index 2c5bbfe..3708671 100644 --- a/src/pages/fi/StageFiveRejected.tsx +++ b/src/pages/fi/StageFiveRejected.tsx @@ -9,7 +9,7 @@ import Title from "src/stories/title/Title"; import useLangContext from "../../hooks/useLangContext"; -export function StageFiveApproved() { +export function StageFiveRejected() { const t = useT(); const navigate = useNavigate(); const applicationContext = useApplicationContext(); @@ -42,4 +42,4 @@ export function StageFiveApproved() { ); } -export default StageFiveApproved; +export default StageFiveRejected; diff --git a/src/routes/AppRouter.tsx b/src/routes/AppRouter.tsx index 67a54f5..f21181f 100644 --- a/src/routes/AppRouter.tsx +++ b/src/routes/AppRouter.tsx @@ -32,6 +32,7 @@ import SecureApplicationContextProvider from "src/providers/SecureApplicationCon import StateContextProvider from "src/providers/StateContextProvider"; import ProtectedRoute from "src/routes/ProtectedRoute"; +import StageFiveLapsed from "src/pages/fi/StageFiveLapsed"; import { USER_TYPES } from "../constants"; import PageLayout from "../layout/PageLayout"; import SecureApplicationLayout from "../layout/SecureApplicationLayout"; @@ -364,6 +365,10 @@ const router = createBrowserRouter([ path: "stage-five-rejected", element: <StageFiveRejected />, }, + { + path: "stage-five-lapsed", + element: <StageFiveLapsed />, + }, { path: "application-completed", element: <ApplicationCompleted />,