From c8a5b4b6adcb8a87a52c371fc9dcb095b31b5ee1 Mon Sep 17 00:00:00 2001 From: Yohanna Lisnichuk Date: Mon, 2 Dec 2024 16:36:57 -0300 Subject: [PATCH 1/2] feat: add lapse application button --- src/api/private.ts | 5 +++ src/assets/icons/approve.svg | 3 ++ src/assets/icons/lapse.svg | 6 +++ src/assets/icons/reject.svg | 5 +++ src/hooks/useLapseApplication.ts | 49 ++++++++++++++++++++++ src/layout/SecureApplicationLayout.tsx | 2 +- src/pages/fi/LapseApplicationDialog.tsx | 55 +++++++++++++++++++++++++ src/pages/fi/StageFive.tsx | 26 +++++++++--- src/pages/fi/StageFiveLapsed.tsx | 45 ++++++++++++++++++++ src/pages/fi/StageFiveRejected.tsx | 4 +- src/routes/AppRouter.tsx | 5 +++ 11 files changed, 197 insertions(+), 8 deletions(-) create mode 100644 src/assets/icons/approve.svg create mode 100644 src/assets/icons/lapse.svg create mode 100644 src/assets/icons/reject.svg create mode 100644 src/hooks/useLapseApplication.ts create mode 100644 src/pages/fi/LapseApplicationDialog.tsx create mode 100644 src/pages/fi/StageFiveLapsed.tsx 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..35d20c5 --- /dev/null +++ b/src/pages/fi/LapseApplicationDialog.tsx @@ -0,0 +1,55 @@ +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..0232e70 100644 --- a/src/pages/fi/StageFive.tsx +++ b/src/pages/fi/StageFive.tsx @@ -13,13 +13,15 @@ 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 Reject from "../../assets/icons/reject.svg"; +import Lapse from "../../assets/icons/lapse.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 RejectApplicationDialog from "./RejectApplicationDialog"; +import LapseApplicationDialog from "./LapseApplicationDialog"; export function StageFive() { const t = useT(); @@ -27,6 +29,8 @@ 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 +45,14 @@ export function StageFive() { setOpenDialog(true); }; + const handleCloseLapse = () => { + setOpenLapseDialog(false); + }; + + const onLapseApplication = () => { + setOpenLapseDialog(true); + }; + const methods = useForm<FormApprovedInput>({ resolver: zodResolver(approveSchema), }); @@ -110,7 +122,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> @@ -122,7 +134,7 @@ export function StageFive() { <div> <Button className="md:mr-4" - icon={CheckChecked} + icon={Approve} label={t("Approve")} type="submit" disabled={isLoading} @@ -130,7 +142,10 @@ export function StageFive() { </div> <div> - <Button label={t("Reject")} icon={WarnRed} onClick={onRejectApplication} disabled={isLoading} /> + <Button className="md:mr-4" label={t("Reject")} icon={Reject} onClick={onRejectApplication} disabled={isLoading} /> + </div> + <div> + <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 +156,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..92f4a5f 100644 --- a/src/routes/AppRouter.tsx +++ b/src/routes/AppRouter.tsx @@ -52,6 +52,7 @@ import Applications from "../pages/ocp/Applications"; import { LoadCreditProduct } from "../pages/ocp/CreditProductForm"; import { LenderForm, LoadLender } from "../pages/ocp/LenderForm"; import Settings from "../pages/ocp/Settings"; +import StageFiveLapsed from "src/pages/fi/StageFiveLapsed"; // Create a React Query client const queryClient = new QueryClient({ @@ -364,6 +365,10 @@ const router = createBrowserRouter([ path: "stage-five-rejected", element: <StageFiveRejected />, }, + { + path: "stage-five-lapsed", + element: <StageFiveLapsed />, + }, { path: "application-completed", element: <ApplicationCompleted />, From fe492d10468135d3330eacdf2af10a7c51ef570d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 2 Dec 2024 19:39:13 +0000 Subject: [PATCH 2/2] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/pages/fi/LapseApplicationDialog.tsx | 58 ++++++++++++++----------- src/pages/fi/StageFive.tsx | 27 +++++++----- src/routes/AppRouter.tsx | 2 +- 3 files changed, 49 insertions(+), 38 deletions(-) diff --git a/src/pages/fi/LapseApplicationDialog.tsx b/src/pages/fi/LapseApplicationDialog.tsx index 35d20c5..3528681 100644 --- a/src/pages/fi/LapseApplicationDialog.tsx +++ b/src/pages/fi/LapseApplicationDialog.tsx @@ -17,39 +17,45 @@ export function LapseApplicationDialog({ open, handleClose }: LapseApplicationDi const application = applicationContext.state.data; const { isLoading, lapseApplicationMutation } = useLapseApplication(); - const rootElement = document.getElementById("root-app"); return ( <Dialog fullWidth maxWidth="sm" container={rootElement} open={open} onClose={handleClose}> - <Box - component="form" - className="flex flex-col py-7 px-8" - autoComplete="off" - > - <Title type="section" label={t("Confirm you want to mark this application as lapsed")} className="mb-4" /> - - <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> + <Box component="form" className="flex flex-col py-7 px-8" autoComplete="off"> + <Title type="section" label={t("Confirm you want to mark this application as lapsed")} className="mb-4" /> + + <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> - </Box> + </div> + </Box> </Dialog> ); } export default LapseApplicationDialog; - diff --git a/src/pages/fi/StageFive.tsx b/src/pages/fi/StageFive.tsx index 0232e70..0d182d5 100644 --- a/src/pages/fi/StageFive.tsx +++ b/src/pages/fi/StageFive.tsx @@ -14,14 +14,14 @@ import Text from "src/stories/text/Text"; import Title from "src/stories/title/Title"; import Approve from "../../assets/icons/approve.svg"; -import Reject from "../../assets/icons/reject.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 RejectApplicationDialog from "./RejectApplicationDialog"; import LapseApplicationDialog from "./LapseApplicationDialog"; +import RejectApplicationDialog from "./RejectApplicationDialog"; export function StageFive() { const t = useT(); @@ -31,7 +31,6 @@ export function StageFive() { const [openDialog, setOpenDialog] = useState<boolean>(false); const [openLapseDialog, setOpenLapseDialog] = useState<boolean>(false); - const langContext = useLangContext(); const StepImage = langContext.state.selected.startsWith("en") ? StepImageEN : StepImageES; @@ -131,21 +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={Approve} - label={t("Approve")} - type="submit" + label={t("Reject")} + icon={Reject} + onClick={onRejectApplication} disabled={isLoading} /> </div> - - <div> - <Button className="md:mr-4" label={t("Reject")} icon={Reject} onClick={onRejectApplication} disabled={isLoading} /> - </div> <div> - <Button className="md:mr-4" label={t("Lapse")} icon={Lapse} onClick={onLapseApplication} 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"> diff --git a/src/routes/AppRouter.tsx b/src/routes/AppRouter.tsx index 92f4a5f..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"; @@ -52,7 +53,6 @@ import Applications from "../pages/ocp/Applications"; import { LoadCreditProduct } from "../pages/ocp/CreditProductForm"; import { LenderForm, LoadLender } from "../pages/ocp/LenderForm"; import Settings from "../pages/ocp/Settings"; -import StageFiveLapsed from "src/pages/fi/StageFiveLapsed"; // Create a React Query client const queryClient = new QueryClient({