diff --git a/frontend/src/features/permits/components/dashboard/ApplicationStepPage.tsx b/frontend/src/features/permits/components/dashboard/ApplicationStepPage.tsx index f19605cd8..dc491ad50 100644 --- a/frontend/src/features/permits/components/dashboard/ApplicationStepPage.tsx +++ b/frontend/src/features/permits/components/dashboard/ApplicationStepPage.tsx @@ -6,10 +6,8 @@ import "../../../../common/components/dashboard/Dashboard.scss"; import { Banner } from "../../../../common/components/dashboard/components/banner/Banner"; import { ApplicationForm } from "../../pages/Application/ApplicationForm"; import { ApplicationContext } from "../../context/ApplicationContext"; -import { ApplicationReview } from "../../pages/Application/ApplicationReview"; import { getCompanyIdFromSession } from "../../../../common/apiManager/httpRequestHandler"; import { Loading } from "../../../../common/pages/Loading"; -import { ApplicationInQueueReview } from "../../../queue/components/ApplicationInQueueReview"; import { useApplicationForStepsQuery } from "../../hooks/hooks"; import { PERMIT_STATUSES } from "../../types/PermitStatus"; import { @@ -25,12 +23,12 @@ import { } from "../../types/PermitType"; import { - APPLICATION_STEP_CONTEXTS, APPLICATION_STEPS, ApplicationStep, ApplicationStepContext, ERROR_ROUTES, } from "../../../../routes/constants"; +import { ApplicationReview } from "../../pages/Application/ApplicationReview"; const displayHeaderText = (stepKey: ApplicationStep) => { switch (stepKey) { @@ -121,12 +119,11 @@ export const ApplicationStepPage = ({ const renderApplicationStep = () => { if (applicationStep === APPLICATION_STEPS.REVIEW) { - return applicationStepContext === APPLICATION_STEP_CONTEXTS.QUEUE ? ( - - ) : ( - ); } return ( diff --git a/frontend/src/features/permits/pages/Application/ApplicationForm.tsx b/frontend/src/features/permits/pages/Application/ApplicationForm.tsx index d91a09f96..bb5317341 100644 --- a/frontend/src/features/permits/pages/Application/ApplicationForm.tsx +++ b/frontend/src/features/permits/pages/Application/ApplicationForm.tsx @@ -52,6 +52,7 @@ import { ApplicationStepContext, ERROR_ROUTES, } from "../../../../routes/constants"; +import { useApplicationInQueueMetadata } from "../../../queue/hooks/hooks"; const FEATURE = "application"; @@ -183,6 +184,18 @@ export const ApplicationForm = ({ return updatedViolations; }; + const { data: applicationMetadata } = useApplicationInQueueMetadata({ + applicationId: getDefaultRequiredVal("", currentFormData.permitId), + companyId, + }); + + const assignedUser = getDefaultRequiredVal( + "", + applicationMetadata?.assignedUser, + ); + + const currentUserIsAssignedUser = assignedUser === idirUserDetails?.userName; + // Check to see if all application values were already saved const isApplicationSaved = () => { // Check if all current form field values match field values already saved in application context @@ -194,6 +207,10 @@ export const ApplicationForm = ({ // When "Continue" button is clicked const onContinue = async (data: ApplicationFormData) => { + if (!currentUserIsAssignedUser) { + return; + } + const updatedViolations = await triggerPolicyValidation(); // prevent CV client continuing if there are policy engine validation errors if (Object.keys(updatedViolations).length > 0 && !isStaffUser) { @@ -204,6 +221,7 @@ export const ApplicationForm = ({ const vehicleData = serializePermitVehicleDetails( data.permitData.vehicleDetails, ); + const savedVehicleDetails = await handleSaveVehicle(vehicleData); // Save application before continuing @@ -235,6 +253,10 @@ export const ApplicationForm = ({ additionalSuccessAction?: (permitId: string) => void, savedVehicleInventoryDetails?: Nullable, ) => { + if (!currentUserIsAssignedUser) { + return; + } + if (isNull(savedVehicleInventoryDetails)) { return navigate(ERROR_ROUTES.UNEXPECTED); } @@ -251,7 +273,6 @@ export const ApplicationForm = ({ }, }, }; - await saveApplication( { data: applicationToBeSaved, diff --git a/frontend/src/features/permits/pages/Application/ApplicationReview.tsx b/frontend/src/features/permits/pages/Application/ApplicationReview.tsx index e0009363a..eb84cd7ee 100644 --- a/frontend/src/features/permits/pages/Application/ApplicationReview.tsx +++ b/frontend/src/features/permits/pages/Application/ApplicationReview.tsx @@ -23,23 +23,56 @@ import { DEFAULT_PERMIT_TYPE, PERMIT_TYPES } from "../../types/PermitType"; import { PERMIT_REVIEW_CONTEXTS } from "../../types/PermitReviewContext"; import { usePolicyEngine } from "../../../policy/hooks/usePolicyEngine"; import { useCommodityOptions } from "../../hooks/useCommodityOptions"; -import { useSubmitApplicationForReview } from "../../../queue/hooks/hooks"; import { deserializeApplicationResponse } from "../../helpers/serialize/deserializeApplication"; import OnRouteBCContext from "../../../../common/authentication/OnRouteBCContext"; import { APPLICATIONS_ROUTES, + APPLICATION_QUEUE_ROUTES, APPLICATION_STEPS, + APPLICATION_STEP_CONTEXTS, + ApplicationStepContext, ERROR_ROUTES, + IDIR_ROUTES, } from "../../../../routes/constants"; +import { CASE_ACTIVITY_TYPES } from "../../../queue/types/CaseActivityType"; +import { QueueBreadcrumb } from "../../../queue/components/QueueBreadcrumb"; +import { RejectApplicationModal } from "../../../queue/components/RejectApplicationModal"; +import { + useApplicationInQueueMetadata, + useSubmitApplicationForReview, + useUpdateApplicationInQueueStatus, +} from "../../../queue/hooks/hooks"; +import { Nullable } from "../../../../common/types/common"; +import { UnavailableApplicationModal } from "../../../queue/components/UnavailableApplicationModal"; export const ApplicationReview = ({ - companyId, + companyIdProp, + applicationStepContext, + applicationData: queueApplicationData, }: { - companyId: number; + applicationStepContext: ApplicationStepContext; + companyIdProp?: Nullable; + applicationData?: Nullable; }) => { const { applicationData, setApplicationData: setApplicationContextData } = useContext(ApplicationContext); + const isQueueContext = + applicationStepContext === APPLICATION_STEP_CONTEXTS.QUEUE; + + const application = isQueueContext ? queueApplicationData : applicationData; + + const companyId = getDefaultRequiredVal( + 0, + companyIdProp, + queueApplicationData?.companyId, + ); + + const applicationId = getDefaultRequiredVal( + queueApplicationData?.permitId, + applicationData?.permitId, + ); + const { idirUserDetails } = useContext(OnRouteBCContext); const isStaffUser = Boolean(idirUserDetails?.userRole); @@ -49,12 +82,15 @@ export const ApplicationReview = ({ const { data: companyInfo } = useCompanyInfoDetailsQuery(companyId); const doingBusinessAs = companyInfo?.alternateName; - const permitType = getDefaultRequiredVal(DEFAULT_PERMIT_TYPE, applicationData?.permitType); + const permitType = getDefaultRequiredVal( + DEFAULT_PERMIT_TYPE, + application?.permitType, + ); const fee = isNoFeePermitType ? "0" : `${calculateFeeByDuration( permitType, - getDefaultRequiredVal(0, applicationData?.permitData?.permitDuration), + getDefaultRequiredVal(0, application?.permitData?.permitDuration), )}`; const { setSnackBar } = useContext(SnackBarContext); @@ -71,20 +107,25 @@ export const ApplicationReview = ({ const trailerSubTypesQuery = useTrailerSubTypesQuery(); const methods = useForm(); - // For the confirmation checkboxes - const [allConfirmed, setAllConfirmed] = useState(false); + const [allConfirmed, setAllConfirmed] = useState(isQueueContext); const [hasAttemptedSubmission, setHasAttemptedSubmission] = useState(false); const { mutateAsync: saveApplication } = useSaveApplicationMutation(); const addToCartMutation = useAddToCart(); - - // Submit for review (if applicable) + const { mutateAsync: submitForReview } = useSubmitApplicationForReview(); const { - mutateAsync: submitForReview, - } = useSubmitApplicationForReview(); + mutateAsync: updateApplication, + data: updateApplicationResponse, + isPending: updateApplicationMutationPending, + } = useUpdateApplicationInQueueStatus(); - const back = () => { - navigate(APPLICATIONS_ROUTES.DETAILS(permitId), { replace: true }); + const handleEdit = () => { + navigate( + isQueueContext + ? APPLICATION_QUEUE_ROUTES.EDIT(companyId, applicationId) + : APPLICATIONS_ROUTES.DETAILS(permitId), + { replace: true }, + ); }; const handleSaveApplication = async ( @@ -98,42 +139,45 @@ export const ApplicationReview = ({ if (!allConfirmed) return; - const companyId = applicationData?.companyId; - const permitId = applicationData?.permitId; - const applicationNumber = applicationData?.applicationNumber; + const companyId = application?.companyId; + const permitId = application?.permitId; + const applicationNumber = application?.applicationNumber; if (!companyId || !permitId || !applicationNumber) { return navigate(ERROR_ROUTES.UNEXPECTED); } - await saveApplication({ - data: { - ...applicationData, - permitData: { - ...applicationData.permitData, - doingBusinessAs, // always set most recent DBA from company info + await saveApplication( + { + data: { + ...application, + permitData: { + ...application?.permitData, + doingBusinessAs, + }, }, + companyId, }, - companyId, - }, { - onSuccess: ({ data: savedApplication }) => { - setApplicationContextData( - deserializeApplicationResponse(savedApplication), - ); - followUpAction(companyId, permitId, applicationNumber); - }, - onError: (e) => { - console.error(e); - if (isAxiosError(e)) { - navigate(ERROR_ROUTES.UNEXPECTED, { - state: { - correlationId: e?.response?.headers["x-correlation-id"], - }, - }); - } else { - navigate(ERROR_ROUTES.UNEXPECTED); - } + { + onSuccess: ({ data: savedApplication }) => { + setApplicationContextData( + deserializeApplicationResponse(savedApplication), + ); + followUpAction(companyId, permitId, applicationNumber); + }, + onError: (e) => { + console.error(e); + if (isAxiosError(e)) { + navigate(ERROR_ROUTES.UNEXPECTED, { + state: { + correlationId: e?.response?.headers["x-correlation-id"], + }, + }); + } else { + navigate(ERROR_ROUTES.UNEXPECTED); + } + }, }, - }); + ); }; const proceedWithAddToCart = async ( @@ -156,64 +200,137 @@ export const ApplicationReview = ({ const setShowSnackbar = () => true; const handleAddToCart = async () => { - await handleSaveApplication(async (companyId, permitId, applicationNumber) => { - await proceedWithAddToCart(companyId, [permitId], () => { + await handleSaveApplication( + async (companyId, permitId, applicationNumber) => { + await proceedWithAddToCart(companyId, [permitId], () => { + setSnackBar({ + showSnackbar: true, + setShowSnackbar, + message: `Application ${applicationNumber} added to cart`, + alertType: "success", + }); + + refetchCartCount(); + navigate(APPLICATIONS_ROUTES.BASE); + }); + }, + ); + }; + const continueBtnText = + permitType === PERMIT_TYPES.STOS && !isStaffUser + ? "Submit for Review" + : undefined; + + const handleSubmitForReview = async () => { + if (permitType !== PERMIT_TYPES.STOS || isStaffUser) return; + + await handleSaveApplication( + async (companyId, permitId, applicationNumber) => { + await submitForReview({ companyId, applicationId: permitId }); setSnackBar({ showSnackbar: true, - setShowSnackbar, - message: `Application ${applicationNumber} added to cart`, + setShowSnackbar: () => true, + message: `Application ${applicationNumber} submitted for review`, alertType: "success", }); - - refetchCartCount(); navigate(APPLICATIONS_ROUTES.BASE); - }); + }, + ); + }; + + const { data: applicationMetadata } = useApplicationInQueueMetadata({ + applicationId: permitId, + companyId, + }); + + const assignedUser = getDefaultRequiredVal( + "", + applicationMetadata?.assignedUser, + ); + + const currentUserIsAssignedUser = assignedUser === idirUserDetails?.userName; + + const handleApprove = async () => { + if (!currentUserIsAssignedUser) { + setShowUnavailableApplicationModal(true); + return; + } + + setHasAttemptedSubmission(true); + + await updateApplication({ + applicationId: permitId, + companyId, + caseActivityType: CASE_ACTIVITY_TYPES.APPROVED, }); }; - const continueBtnText = permitType === PERMIT_TYPES.STOS && !isStaffUser - ? "Submit for Review" : undefined; + const [showRejectApplicationModal, setShowRejectApplicationModal] = + useState(false); - const handleSubmitForReview = async () => { - if (permitType !== PERMIT_TYPES.STOS) return; - if (isStaffUser) return; + const handleRejectButton = () => { + if (!currentUserIsAssignedUser) { + setShowUnavailableApplicationModal(true); + return; + } + setShowRejectApplicationModal(true); + }; - await handleSaveApplication(async (companyId, permitId, applicationNumber) => { - await submitForReview({ - companyId, - applicationId: permitId, - }, { - onSuccess: () => { - setSnackBar({ - showSnackbar: true, - setShowSnackbar, - message: `Application ${applicationNumber} submitted for review`, - alertType: "success", - }); - - navigate(APPLICATIONS_ROUTES.BASE); - }, - onError: () => { - navigate(ERROR_ROUTES.UNEXPECTED); - }, - }); + const handleReject = async (comment: string) => { + setHasAttemptedSubmission(true); + await updateApplication({ + applicationId: permitId, + companyId, + caseActivityType: CASE_ACTIVITY_TYPES.REJECTED, + comment, }); }; + const [showUnavailableApplicationModal, setShowUnavailableApplicationModal] = + useState(false); + + const updateApplicationResponseStatus = updateApplicationResponse?.status; + + const handleCloseApplication = () => { + navigate(IDIR_ROUTES.STAFF_HOME); + }; + + const handleCloseUnavailableApplicationModal = () => { + setShowUnavailableApplicationModal(false); + showRejectApplicationModal && setShowRejectApplicationModal(false); + }; + + useEffect(() => { + if (updateApplicationResponseStatus === 201) { + navigate(IDIR_ROUTES.STAFF_HOME); + } + }, [updateApplicationResponse, updateApplicationResponseStatus, navigate]); + useEffect(() => { window.scrollTo(0, 0); }, []); return (
- + {isQueueContext ? ( + + ) : ( + + )} + + {isQueueContext && showRejectApplicationModal && ( + setShowRejectApplicationModal(false)} + onConfirm={handleReject} + isPending={updateApplicationMutationPending} + /> + )} + + {isQueueContext && showUnavailableApplicationModal && ( + + )}
); }; diff --git a/frontend/src/features/permits/pages/Application/components/review/ReviewActions.tsx b/frontend/src/features/permits/pages/Application/components/review/ReviewActions.tsx index 4e9d069f7..4798ffee7 100644 --- a/frontend/src/features/permits/pages/Application/components/review/ReviewActions.tsx +++ b/frontend/src/features/permits/pages/Application/components/review/ReviewActions.tsx @@ -78,7 +78,7 @@ export const ReviewActions = ({ ) : null} - {reviewContext === PERMIT_REVIEW_CONTEXTS.QUEUE ? ( + {reviewContext === PERMIT_REVIEW_CONTEXTS.QUEUE && ( <> + + + + ); +}; diff --git a/frontend/src/features/queue/hooks/hooks.ts b/frontend/src/features/queue/hooks/hooks.ts index 08c843a5b..15f012633 100644 --- a/frontend/src/features/queue/hooks/hooks.ts +++ b/frontend/src/features/queue/hooks/hooks.ts @@ -1,6 +1,5 @@ import { useContext } from "react"; import { useNavigate } from "react-router-dom"; -import { AxiosError } from "axios"; import { MRT_PaginationState, MRT_SortingState } from "material-react-table"; import { keepPreviousData, @@ -16,9 +15,13 @@ import OnRouteBCContext from "../../../common/authentication/OnRouteBCContext"; import { IDIRUserRoleType } from "../../../common/authentication/types"; import { Nullable } from "../../../common/types/common"; import { useTableControls } from "../../permits/hooks/useTableControls"; -import { APPLICATION_QUEUE_STATUSES, ApplicationQueueStatus } from "../types/ApplicationQueueStatus"; +import { + APPLICATION_QUEUE_STATUSES, + ApplicationQueueStatus, +} from "../types/ApplicationQueueStatus"; import { claimApplicationInQueue, + getApplicationInQueueMetadata, getApplicationsInQueue, getClaimedApplicationsInQueue, getUnclaimedApplicationsInQueue, @@ -33,23 +36,23 @@ const QUEUE_QUERY_KEYS_BASE = "queue"; * * eg. ["queue"] and ["queue", undefined] refers to all (ApplicationQueueStatus) queue items * (regardless of pagination and sorting) - * + * * eg. ["queue", "IN_REVIEW"] only refers to "IN_REVIEW" queue items */ const QUEUE_QUERY_KEYS = { ALL_ITEMS: [QUEUE_QUERY_KEYS_BASE] as const, - WITH_STATUS: (status?: ApplicationQueueStatus) => [ - ...QUEUE_QUERY_KEYS.ALL_ITEMS, - status, - ] as const, + WITH_STATUS: (status?: ApplicationQueueStatus) => + [...QUEUE_QUERY_KEYS.ALL_ITEMS, status] as const, WITH_PAGINATION: ( pagination: MRT_PaginationState, sorting: MRT_SortingState, status?: ApplicationQueueStatus, - ) => [ - ...QUEUE_QUERY_KEYS.WITH_STATUS(status), - { pagination, sorting }, - ] as const, + ) => + [...QUEUE_QUERY_KEYS.WITH_STATUS(status), { pagination, sorting }] as const, + APPLICATION_METADATA: (applicationId: string) => [ + QUEUE_QUERY_KEYS_BASE, + { applicationId }, + ], }; /** @@ -195,8 +198,9 @@ export const useUpdateApplicationInQueueStatus = () => { onSuccess: () => { invalidate(); }, - onError: (err: AxiosError) => { + onError: (err: any) => { if (err.response?.status === 422) { + // console.log({ err }); return err; } else { navigate(ERROR_ROUTES.UNEXPECTED, { @@ -215,14 +219,8 @@ export const useSubmitApplicationForReview = () => { const { invalidate } = useInvalidateApplicationsInQueue(); return useMutation({ - mutationFn: async (data: { - companyId: number; - applicationId: string; - }) => { - return submitApplicationForReview( - data.companyId, - data.applicationId, - ); + mutationFn: async (data: { companyId: number; applicationId: string }) => { + return submitApplicationForReview(data.companyId, data.applicationId); }, onSuccess: () => { invalidate(); @@ -245,3 +243,20 @@ export const useInvalidateApplicationsInQueue = () => { }, }; }; + +/** + * Hook that fetches additional information for a given applicationId. + * @returns Application Metadata including the "assignedUser" property used by the queue feature + */ +export const useApplicationInQueueMetadata = ({ + companyId, + applicationId, +}: { + companyId: number; + applicationId: string; +}) => { + return useQuery({ + queryKey: QUEUE_QUERY_KEYS.APPLICATION_METADATA(applicationId), + queryFn: () => getApplicationInQueueMetadata(companyId, applicationId), + }); +}; diff --git a/frontend/src/features/queue/pages/ReviewApplicationInQueue.tsx b/frontend/src/features/queue/pages/ReviewApplicationInQueue.tsx deleted file mode 100644 index b09ada4c1..000000000 --- a/frontend/src/features/queue/pages/ReviewApplicationInQueue.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { Box } from "@mui/material"; -import { Navigate, useParams } from "react-router-dom"; -import { Banner } from "../../../common/components/dashboard/components/banner/Banner"; -import { Loading } from "../../../common/pages/Loading"; -import { useApplicationDetailsQuery } from "../../permits/hooks/hooks"; -import { ApplicationInQueueReview } from "../components/ApplicationInQueueReview"; -import { - applyWhenNotNullable, - getDefaultRequiredVal, -} from "../../../common/helpers/util"; -import { ERROR_ROUTES } from "../../../routes/constants"; -import { deserializeApplicationResponse } from "../../permits/helpers/serialize/deserializeApplication"; -import { UniversalUnexpected } from "../../../common/pages/UniversalUnexpected"; - -export const ReviewApplicationInQueue = () => { - const { companyId: companyIdParam, permitId: permitIdParam } = useParams(); - - const companyId: number = applyWhenNotNullable( - (id) => Number(id), - companyIdParam, - 0, - ); - const permitId = getDefaultRequiredVal("", permitIdParam); - - const { - query: { data: applicationData, isLoading: applicationDataIsLoading }, - } = useApplicationDetailsQuery({ - companyId, - permitId, - }); - - if (!companyId || !permitId) { - return ; - } - - if (applicationDataIsLoading) { - return ; - } - - if (!applicationData) { - return ; - } - - return ( -
- - - - - -
- ); -}; diff --git a/vehicles/src/modules/case-management/case-management.service.ts b/vehicles/src/modules/case-management/case-management.service.ts index 28e8eba56..212cd1118 100644 --- a/vehicles/src/modules/case-management/case-management.service.ts +++ b/vehicles/src/modules/case-management/case-management.service.ts @@ -565,6 +565,7 @@ export class CaseManagementService { } else if (existingCase.assignedUser?.userGUID !== currentUser.userGUID) { throwUnprocessableEntityException( `Application no longer available. This application is claimed by ${existingCase.assignedUser?.userName}`, + { currentClaimant: existingCase.assignedUser?.userName }, ); } else if (existingCase.caseStatusType !== CaseStatusType.IN_PROGRESS) { throwUnprocessableEntityException('Application no longer available.');