diff --git a/database/mssql/test/versions/v_43_1_test.sql b/database/mssql/test/versions/v_43_1_test.sql index 92807f1ed..7b004ecec 100644 --- a/database/mssql/test/versions/v_43_1_test.sql +++ b/database/mssql/test/versions/v_43_1_test.sql @@ -2,4 +2,4 @@ SET NOCOUNT ON SELECT COUNT(*) FROM $(DB_NAME).[dops].[ORBC_DOCUMENT_TEMPLATE] -WHERE TEMPLATE_NAME IN ('PERMIT','PERMIT_STOS_VOID','PERMIT_STOS_REVOKED') \ No newline at end of file +WHERE TEMPLATE_NAME IN ('PERMIT','PERMIT_STOS_VOID','PERMIT_STOS_REVOKED') diff --git a/database/mssql/test/versions/v_43_test.sh b/database/mssql/test/versions/v_43_test.sh index 867f594cb..db30936c7 100644 --- a/database/mssql/test/versions/v_43_test.sh +++ b/database/mssql/test/versions/v_43_test.sh @@ -21,4 +21,4 @@ if [[ $TEST_43_2_RESULT -eq 3 ]]; then echo "Test 43.2 passed: STOS templates setup successfully in ORBC_DOCUMENT" else echo "******** Test 43.2 failed: Failed to setup STOS permit templates" -fi \ No newline at end of file +fi diff --git a/database/mssql/test/versions/v_45_test.sh b/database/mssql/test/versions/v_45_test.sh index e9e2da838..5d62366c2 100644 --- a/database/mssql/test/versions/v_45_test.sh +++ b/database/mssql/test/versions/v_45_test.sh @@ -13,4 +13,4 @@ if [[ $TEST_45_1_RESULT -eq 7 ]]; then echo "Test 45.1 passed: GL_PROJ_CODE column created in ORBC_PAYMENT_METHOD_TYPE" else echo "******** Test 45.1 failed: GL_PROJ_CODE column missing in ORBC_PAYMENT_METHOD_TYPE" -fi \ No newline at end of file +fi diff --git a/frontend/src/common/components/form/CountryAndProvince.tsx b/frontend/src/common/components/form/CountryAndProvince.tsx index cf7c35326..398e55681 100644 --- a/frontend/src/common/components/form/CountryAndProvince.tsx +++ b/frontend/src/common/components/form/CountryAndProvince.tsx @@ -44,6 +44,8 @@ interface CountryAndProvinceProps { isProvinceRequired?: boolean; countryClassName?: string; provinceClassName?: string; + readOnly?: boolean; + disabled?: boolean; } /** @@ -62,6 +64,8 @@ export const CountryAndProvince = ({ isProvinceRequired = true, countryClassName, provinceClassName, + disabled, + readOnly, }: CountryAndProvinceProps): JSX.Element => { const { resetField, watch, setValue } = useFormContext(); @@ -175,7 +179,10 @@ export const CountryAndProvince = ({ ))} className={countryClassName} + disabled={disabled} + readOnly={readOnly} /> + {shouldDisplayProvince && ( ({ ))} className={provinceClassName} + disabled={disabled} + readOnly={readOnly} /> )} diff --git a/frontend/src/common/constants/bannerMessages.ts b/frontend/src/common/constants/bannerMessages.ts index d7bb09f4d..c4e187dfd 100644 --- a/frontend/src/common/constants/bannerMessages.ts +++ b/frontend/src/common/constants/bannerMessages.ts @@ -17,6 +17,10 @@ export const BANNER_MESSAGES = { "Only vehicles in the Vehicle Inventory can be designated to LOA(s).", SELECT_VEHICLES_LOA_INFO: "If you do not see the vehicle(s) you wish to designate here, please make sure you add them to the client's Vehicle Inventory first and come back to this page.", + FIND_LOA_DETAILS: + "To find details about the LOA go to the Special Authorizations page.", + LOA_VEHICLE_CANNOT_BE_EDITED_IN_PERMIT: + "Vehicle details cannot be edited in the permit application if you are using an LOA.", REJECTED_APPLICATIONS: "Rejected applications appear in Applications in Progress.", }; diff --git a/frontend/src/common/helpers/util.ts b/frontend/src/common/helpers/util.ts index 2f1eaa244..24b5e0b89 100644 --- a/frontend/src/common/helpers/util.ts +++ b/frontend/src/common/helpers/util.ts @@ -274,3 +274,27 @@ export const setRedirectInSession = (redirectUri: string) => { } } }; + +/** + * Determine whether or not two arrays have the same items. + * @param arr1 First array + * @param arr2 Second array + * @returns Whether or not the two arrays contain the same items + */ +export const areArraysEqual = ( + arr1: T[], + arr2: T[], +) => { + const set1 = new Set(arr1); + const set2 = new Set(arr2); + + for (const val of set1) { + if (!set2.has(val)) return false; + } + + for (const val of set2) { + if (!set1.has(val)) return false; + } + + return true; +}; diff --git a/frontend/src/features/permits/components/dashboard/tests/integration/fixtures/getActiveApplication.ts b/frontend/src/features/permits/components/dashboard/tests/integration/fixtures/getActiveApplication.ts index d58558a43..4365f4503 100644 --- a/frontend/src/features/permits/components/dashboard/tests/integration/fixtures/getActiveApplication.ts +++ b/frontend/src/features/permits/components/dashboard/tests/integration/fixtures/getActiveApplication.ts @@ -192,6 +192,7 @@ export const getDefaultApplication = () => { vehicleDetails, commodities: conditions, mailingAddress, + loas: [], }, }; }; diff --git a/frontend/src/features/permits/context/ApplicationFormContext.ts b/frontend/src/features/permits/context/ApplicationFormContext.ts new file mode 100644 index 000000000..835503fde --- /dev/null +++ b/frontend/src/features/permits/context/ApplicationFormContext.ts @@ -0,0 +1,82 @@ +import { createContext } from "react"; +import { Dayjs } from "dayjs"; + +import { PermitVehicleDetails } from "../types/PermitVehicleDetails"; +import { LOADetail } from "../../settings/types/SpecialAuthorization"; +import { ApplicationFormData } from "../types/application"; +import { getDefaultValues } from "../helpers/getDefaultApplicationFormData"; +import { DEFAULT_PERMIT_TYPE } from "../types/PermitType"; +import { PermitCondition } from "../types/PermitCondition"; +import { PowerUnit, Trailer, VehicleSubType } from "../../manageVehicles/types/Vehicle"; +import { Nullable } from "../../../common/types/common"; +import { CompanyProfile } from "../../manageProfile/types/manageProfile.d"; +import { + PAST_START_DATE_STATUSES, + PastStartDateStatus, +} from "../../../common/components/form/subFormComponents/CustomDatePicker"; + +interface ApplicationFormContextType { + initialFormData: ApplicationFormData; + formData: ApplicationFormData; + durationOptions: { + value: number; + label: string; + }[]; + vehicleOptions: (PowerUnit | Trailer)[]; + powerUnitSubtypes: VehicleSubType[]; + trailerSubtypes: VehicleSubType[]; + isLcvDesignated: boolean; + feature: string; + companyInfo?: Nullable; + isAmendAction: boolean; + createdDateTime?: Nullable; + updatedDateTime?: Nullable; + pastStartDateStatus: PastStartDateStatus; + companyLOAs: LOADetail[]; + revisionHistory: { + permitId: number; + name: string; + revisionDateTime: string; + comment: string; + }[]; + onLeave?: () => void; + onSave?: () => Promise; + onCancel?: () => void; + onContinue: () => Promise; + onSetDuration: (duration: number) => void; + onSetExpiryDate: (expiry: Dayjs) => void; + onSetConditions: (conditions: PermitCondition[]) => void; + onToggleSaveVehicle: (saveVehicle: boolean) => void; + onSetVehicle: (vehicleDetails: PermitVehicleDetails) => void; + onClearVehicle: (saveVehicle: boolean) => void; + onUpdateLOAs: (updatedLOAs: LOADetail[]) => void; +} + +export const ApplicationFormContext = createContext({ + initialFormData: getDefaultValues(DEFAULT_PERMIT_TYPE, undefined), + formData: getDefaultValues(DEFAULT_PERMIT_TYPE, undefined), + durationOptions: [], + vehicleOptions: [], + powerUnitSubtypes: [], + trailerSubtypes: [], + isLcvDesignated: false, + feature: "", + companyInfo: undefined, + isAmendAction: false, + createdDateTime: undefined, + updatedDateTime: undefined, + pastStartDateStatus: PAST_START_DATE_STATUSES.ALLOWED, + companyLOAs: [], + revisionHistory: [], + onLeave: undefined, + onSave: undefined, + onCancel: undefined, + onContinue: async () => undefined, + onSetDuration: () => undefined, + onSetExpiryDate: () => undefined, + onSetConditions: () => undefined, + onToggleSaveVehicle: () => undefined, + onSetVehicle: () => undefined, + onClearVehicle: () => undefined, + onUpdateLOAs: () => undefined, +}); diff --git a/frontend/src/features/permits/helpers/dateSelection.ts b/frontend/src/features/permits/helpers/dateSelection.ts index ac034f061..68d6a6cec 100644 --- a/frontend/src/features/permits/helpers/dateSelection.ts +++ b/frontend/src/features/permits/helpers/dateSelection.ts @@ -1,3 +1,5 @@ +import { Dayjs } from "dayjs"; + import { BASE_DAYS_IN_YEAR, TERM_DURATION_INTERVAL_DAYS } from "../constants/constants"; import { PERMIT_TYPES, PermitType } from "../types/PermitType"; import { @@ -13,6 +15,9 @@ import { TROW_DURATION_INTERVAL_DAYS, TROW_DURATION_OPTIONS, } from "../constants/trow"; +import { getExpiryDate } from "./permitState"; +import { LOADetail } from "../../settings/types/SpecialAuthorization"; +import { getMostRecentExpiryFromLOAs } from "./permitLOA"; /** * Get list of selectable duration options for a given permit type. @@ -62,3 +67,68 @@ export const getDurationIntervalDays = (permitType: PermitType) => { return TERM_DURATION_INTERVAL_DAYS; // This needs to be updated once more permit types are added } }; + +/** + * Get the minimum permit expiry date. + * @param permitType Permit type + * @param startDate Expected start date of the permit + * @returns The earliest date that the permit will expire on + */ +export const getMinPermitExpiryDate = ( + permitType: PermitType, + startDate: Dayjs, +) => { + const minDuration = minDurationForPermitType(permitType); + return getExpiryDate(startDate, minDuration); +}; + +/** + * Get available duration options for a permit based on selected LOAs and start date. + * @param fullDurationOptions Full duration select options for a permit + * @param selectedLOAs Selected LOAs for a permit + * @param startDate Start date for a permit + * @returns Updated available duration select options + */ +export const getAvailableDurationOptions = ( + fullDurationOptions: { + value: number; + label: string; + }[], + selectedLOAs: LOADetail[], + startDate: Dayjs, +) => { + const mostRecentLOAExpiry = getMostRecentExpiryFromLOAs(selectedLOAs); + if (!mostRecentLOAExpiry) return fullDurationOptions; + + return fullDurationOptions + .filter(({ value: durationDays }) => !mostRecentLOAExpiry.isBefore(getExpiryDate(startDate, durationDays))); +}; + +/** + * Update permit duration if durations options change. + * Selected duration must be between min allowable permit duration and max available duration in the options. + * @param permitType Permit type + * @param currentDuration Currently selected duration for the permit + * @param durationOptions Available list of selectable duration options for the permit + * @returns Current permit duration if valid, or updated duration if no longer valid + */ +export const handleUpdateDurationIfNeeded = ( + permitType: PermitType, + currentDuration: number, + durationOptions: { + value: number; + label: string; + }[], +) => { + const minAllowableDuration = minDurationForPermitType(permitType); + const maxDurationInOptions = Math.max(...durationOptions.map(durationOption => durationOption.value)); + + if (currentDuration > maxDurationInOptions) { + if (maxDurationInOptions < minAllowableDuration) { + return minAllowableDuration; + } + return maxDurationInOptions; + } + + return currentDuration; +}; diff --git a/frontend/src/features/permits/helpers/equality.ts b/frontend/src/features/permits/helpers/equality.ts index 7ce7f3e7d..8d384c04b 100644 --- a/frontend/src/features/permits/helpers/equality.ts +++ b/frontend/src/features/permits/helpers/equality.ts @@ -4,6 +4,7 @@ import { PermitMailingAddress } from "../types/PermitMailingAddress"; import { PermitContactDetails } from "../types/PermitContactDetails"; import { PermitVehicleDetails } from "../types/PermitVehicleDetails"; import { PermitData } from "../types/PermitData"; +import { areLOADetailsEqual, LOADetail } from "../../settings/types/SpecialAuthorization"; import { PermitCondition } from "../types/PermitCondition"; import { DATE_FORMATS, @@ -113,6 +114,45 @@ const areVehicleDetailsEqual = ( .reduce((prevIsEqual, currIsEqual) => prevIsEqual && currIsEqual, true); }; +/** + * Compare whether or not the LOAs for two permits are equal. + * @param loas1 LOAs for first permit + * @param loas2 LOAs for second permit + * @returns true when the selected LOAs are the same, false otherwise + */ +export const arePermitLOAsEqual = ( + loas1: Nullable, + loas2: Nullable, +) => { + const isLoas1Empty = !loas1 || loas1.length === 0; + const isLoas2Empty = !loas2 || loas2.length === 0; + + if (isLoas1Empty && isLoas2Empty) return true; + if ((isLoas1Empty && !isLoas2Empty) || (!isLoas1Empty && isLoas2Empty)) + return false; + + const loaMap1 = new Map( + (loas1 as LOADetail[]).map((loa) => [loa.loaNumber, loa]), + ); + const loaMap2 = new Map( + (loas2 as LOADetail[]).map((loa) => [loa.loaNumber, loa]), + ); + + for (const [loaNumber, loa] of loaMap1) { + if (!areLOADetailsEqual(loa, loaMap2.get(loaNumber))) { + return false; + } + } + + for (const [loaNumber, loa] of loaMap2) { + if (!areLOADetailsEqual(loa, loaMap1.get(loaNumber))) { + return false; + } + } + + return true; +}; + /** * Compare whether or not two application data info are equal. * @param data1 first application data info @@ -133,6 +173,7 @@ export const areApplicationDataEqual = ( areVehicleDetailsEqual(data1.vehicleDetails, data2.vehicleDetails) && areConditionsEqual(data1.commodities, data2.commodities) && areMailingAddressesEqual(data1.mailingAddress, data2.mailingAddress) && + arePermitLOAsEqual(data1.loas, data2.loas) && ((!data1.companyName && !data2.companyName) || data1.companyName === data2.companyName) && ((!data1.doingBusinessAs && !data2.doingBusinessAs) || diff --git a/frontend/src/features/permits/helpers/getDefaultApplicationFormData.ts b/frontend/src/features/permits/helpers/getDefaultApplicationFormData.ts index f83f9e175..e08a2a6ce 100644 --- a/frontend/src/features/permits/helpers/getDefaultApplicationFormData.ts +++ b/frontend/src/features/permits/helpers/getDefaultApplicationFormData.ts @@ -1,7 +1,7 @@ import dayjs, { Dayjs } from "dayjs"; import { BCeIDUserDetailContext } from "../../../common/authentication/OnRouteBCContext"; -import { getMandatoryConditions, sortConditions } from "./conditions"; +import { getMandatoryConditions } from "./conditions"; import { Nullable } from "../../../common/types/common"; import { PERMIT_STATUSES } from "../types/PermitStatus"; import { calculateFeeByDuration } from "./feeSummary"; @@ -9,12 +9,9 @@ import { PermitType } from "../types/PermitType"; import { getExpiryDate } from "./permitState"; import { PermitMailingAddress } from "../types/PermitMailingAddress"; import { PermitContactDetails } from "../types/PermitContactDetails"; -import { PermitVehicleDetails } from "../types/PermitVehicleDetails"; import { Application, ApplicationFormData } from "../types/application"; import { minDurationForPermitType } from "./dateSelection"; -import { PermitCondition } from "../types/PermitCondition"; -import { LCV_CONDITION } from "../constants/constants"; -import { isVehicleSubtypeLCV } from "../../manageVehicles/helpers/vehicleSubtypes"; +import { getDefaultVehicleDetails } from "./permitVehicles"; import { getEndOfDate, getStartOfDate, @@ -100,27 +97,6 @@ export const getDefaultMailingAddress = ( postalCode: getDefaultRequiredVal("", alternateAddress?.postalCode), }; -/** - * Gets default values for vehicle details, or populate with values from existing vehicle details. - * @param vehicleDetails existing vehicle details, if any - * @returns default values for vehicle details - */ -export const getDefaultVehicleDetails = ( - vehicleDetails?: Nullable, -) => ({ - vehicleId: getDefaultRequiredVal("", vehicleDetails?.vehicleId), - unitNumber: getDefaultRequiredVal("", vehicleDetails?.unitNumber), - vin: getDefaultRequiredVal("", vehicleDetails?.vin), - plate: getDefaultRequiredVal("", vehicleDetails?.plate), - make: getDefaultRequiredVal("", vehicleDetails?.make), - year: applyWhenNotNullable((year) => year, vehicleDetails?.year, null), - countryCode: getDefaultRequiredVal("", vehicleDetails?.countryCode), - provinceCode: getDefaultRequiredVal("", vehicleDetails?.provinceCode), - vehicleType: getDefaultRequiredVal("", vehicleDetails?.vehicleType), - vehicleSubType: getDefaultRequiredVal("", vehicleDetails?.vehicleSubType), - saveVehicle: false, -}); - export const getDurationOrDefault = ( defaultDuration: number, duration?: Nullable, @@ -155,87 +131,6 @@ export const getExpiryDateOrDefault = ( ); }; -/** - * Applying LCV designation to application data. - * @param applicationData Existing application data - * @param isLcvDesignated Whether or not the LCV designation is to be used - * @returns Application data after applying the LCV check - */ -export const applyLCVToApplicationData = >( - applicationData: T, - isLcvDesignated: boolean, -): T => { - // If application doesn't exist, no need to apply LCV at all - if (!applicationData) return applicationData; - - if (!isLcvDesignated) { - // If LCV not designated, remove LCV condition from application data - const filteredConditions = applicationData.permitData.commodities.filter( - ({ condition }: PermitCondition) => condition !== LCV_CONDITION.condition, - ); - - if (isVehicleSubtypeLCV(applicationData.permitData.vehicleDetails.vehicleSubType)) { - // Furthermore, if selected vehicle has LCV subtype, clear the vehicle - return { - ...applicationData, - permitData: { - ...applicationData.permitData, - commodities: [...filteredConditions], - vehicleDetails: getDefaultVehicleDetails(), - }, - }; - } - - // Otherwise, keep the existing vehicle - return { - ...applicationData, - permitData: { - ...applicationData.permitData, - commodities: [...filteredConditions], - }, - }; - } - - // If LCV is designated, and vehicle subtype in the application isn't LCV but conditions have LCV, - // then remove that LCV condition from the application - if ( - !isVehicleSubtypeLCV(applicationData.permitData.vehicleDetails.vehicleSubType) - && applicationData.permitData.commodities.some(({ condition }) => condition === LCV_CONDITION.condition) - ) { - const filteredConditions = applicationData.permitData.commodities.filter( - ({ condition }: PermitCondition) => condition !== LCV_CONDITION.condition, - ); - - return { - ...applicationData, - permitData: { - ...applicationData.permitData, - commodities: [...filteredConditions], - }, - }; - } - - // If LCV is designated, and vehicle subtype in the application is LCV but conditions don't have LCV, - // then add that LCV condition into the application - if ( - isVehicleSubtypeLCV(applicationData.permitData.vehicleDetails.vehicleSubType) - && !applicationData.permitData.commodities.some(({ condition }) => condition === LCV_CONDITION.condition) - ) { - const conditionsWithLCV = sortConditions([...applicationData.permitData.commodities, LCV_CONDITION]); - - return { - ...applicationData, - permitData: { - ...applicationData.permitData, - commodities: [...conditionsWithLCV], - }, - }; - } - - // In other cases, the application data is valid - return applicationData; -}; - /** * Gets default values for the application data, or populate with values from existing application and relevant data. * @param permitType permit type for the application @@ -329,6 +224,7 @@ export const getDefaultValues = ( applicationData?.permitData?.vehicleDetails, ), feeSummary: `${calculateFeeByDuration(defaultPermitType, durationOrDefault)}`, + loas: getDefaultRequiredVal([], applicationData?.permitData?.loas), }, }; }; diff --git a/frontend/src/features/permits/helpers/mappers.ts b/frontend/src/features/permits/helpers/mappers.ts index 5903f48be..e60bc999b 100644 --- a/frontend/src/features/permits/helpers/mappers.ts +++ b/frontend/src/features/permits/helpers/mappers.ts @@ -99,6 +99,7 @@ export const clonePermit = (permit: Permit): Permit => { ...permit.permitData.vehicleDetails, }, commodities: [...permit.permitData.commodities], + loas: [...getDefaultRequiredVal([], permit.permitData.loas)], mailingAddress: { ...permit.permitData.mailingAddress, }, diff --git a/frontend/src/features/permits/helpers/permitLCV.ts b/frontend/src/features/permits/helpers/permitLCV.ts new file mode 100644 index 000000000..de37957f6 --- /dev/null +++ b/frontend/src/features/permits/helpers/permitLCV.ts @@ -0,0 +1,104 @@ +import { Nullable } from "../../../common/types/common"; +import { isVehicleSubtypeLCV } from "../../manageVehicles/helpers/vehicleSubtypes"; +import { LCV_CONDITION } from "../constants/constants"; +import { Application, ApplicationFormData } from "../types/application"; +import { PermitCondition } from "../types/PermitCondition"; +import { PermitVehicleDetails } from "../types/PermitVehicleDetails"; +import { sortConditions } from "./conditions"; +import { getDefaultVehicleDetails } from "./permitVehicles"; + +/** + * Get updated vehicle details based on LCV designation. + * @param isLcvDesignated Whether or not the LCV designation is to be used + * @param prevSelectedVehicle Previous selected vehicle details + * @returns Updated vehicle details + */ +export const getUpdatedVehicleDetailsForLCV = ( + isLcvDesignated: boolean, + prevSelectedVehicle: PermitVehicleDetails, +) => { + if (!isLcvDesignated && isVehicleSubtypeLCV(prevSelectedVehicle.vehicleSubType)) { + // If LCV isn't designated, and selected vehicle has LCV subtype, clear the vehicle + return getDefaultVehicleDetails(); + } + + // Otherwise keep the existing vehicle details + return prevSelectedVehicle; +}; + +/** + * Get updated permit conditions based on LCV designation and selected vehicle subtype. + * @param isLcvDesignated Whether or not the LCV designation is to be used + * @param prevSelectedConditions Previously selected permit conditions + * @param vehicleSubtype Selected vehicle subtype + * @returns Updated permit conditions + */ +export const getUpdatedConditionsForLCV = ( + isLcvDesignated: boolean, + prevSelectedConditions: PermitCondition[], + vehicleSubtype: string, +) => { + if (!isLcvDesignated) { + // If LCV not designated, remove LCV condition + return prevSelectedConditions.filter( + ({ condition }: PermitCondition) => condition !== LCV_CONDITION.condition, + ); + } + + // If LCV is designated, and vehicle subtype isn't LCV but conditions have LCV, + // then remove that LCV condition + if ( + !isVehicleSubtypeLCV(vehicleSubtype) + && prevSelectedConditions.some(({ condition }) => condition === LCV_CONDITION.condition) + ) { + return prevSelectedConditions.filter( + ({ condition }: PermitCondition) => condition !== LCV_CONDITION.condition, + ); + } + + // If LCV is designated, and vehicle subtype is LCV but conditions don't have LCV, + // then add that LCV condition + if ( + isVehicleSubtypeLCV(vehicleSubtype) + && !prevSelectedConditions.some(({ condition }) => condition === LCV_CONDITION.condition) + ) { + return sortConditions([...prevSelectedConditions, LCV_CONDITION]); + } + + // In other cases, the conditions are valid + return prevSelectedConditions; +}; + +/** + * Applying LCV designation to application data. + * @param applicationData Existing application data + * @param isLcvDesignated Whether or not the LCV designation is to be used + * @returns Application data after applying the LCV check + */ +export const applyLCVToApplicationData = >( + applicationData: T, + isLcvDesignated: boolean, +): T => { + // If application doesn't exist, no need to apply LCV at all + if (!applicationData) return applicationData; + + const updatedVehicleDetails = getUpdatedVehicleDetailsForLCV( + isLcvDesignated, + applicationData.permitData.vehicleDetails, + ); + + const updatedConditions = getUpdatedConditionsForLCV( + isLcvDesignated, + applicationData.permitData.commodities, + updatedVehicleDetails.vehicleSubType, + ); + + return { + ...applicationData, + permitData: { + ...applicationData.permitData, + commodities: [...updatedConditions], + vehicleDetails: updatedVehicleDetails, + }, + }; +}; diff --git a/frontend/src/features/permits/helpers/permitLOA.ts b/frontend/src/features/permits/helpers/permitLOA.ts new file mode 100644 index 000000000..afb151a0c --- /dev/null +++ b/frontend/src/features/permits/helpers/permitLOA.ts @@ -0,0 +1,229 @@ +import dayjs, { Dayjs } from "dayjs"; + +import { LOADetail } from "../../settings/types/SpecialAuthorization"; +import { PermitType } from "../types/PermitType"; +import { getEndOfDate, toLocalDayjs } from "../../../common/helpers/formatDate"; +import { Nullable } from "../../../common/types/common"; +import { Application, ApplicationFormData } from "../types/application"; +import { getDefaultRequiredVal } from "../../../common/helpers/util"; +import { PowerUnit, Trailer, VEHICLE_TYPES } from "../../manageVehicles/types/Vehicle"; +import { PermitVehicleDetails } from "../types/PermitVehicleDetails"; +import { filterVehicles, getDefaultVehicleDetails } from "./permitVehicles"; +import { + durationOptionsForPermitType, + getAvailableDurationOptions, + getMinPermitExpiryDate, + handleUpdateDurationIfNeeded, +} from "./dateSelection"; + +/** + * Filter valid LOAs for a given permit type. + * @param loas LOAs to filter + * @param permitType The permit type that the LOA can be applicable for + * @returns LOAs that can be applicable for the given permit type + */ +export const filterLOAsForPermitType = ( + loas: LOADetail[], + permitType: PermitType, +) => { + return loas.filter(loa => loa.loaPermitType.includes(permitType)); +}; + +/** + * Filter non-expired LOAs that do not expire before the start date of a permit. + * @param loas LOAs to filter + * @param permitStart The start date of the permit + * @returns LOAs that do not expire before the start date of the permit + */ +export const filterNonExpiredLOAs = ( + loas: LOADetail[], + permitStart: Dayjs, +) => { + return loas.filter(loa => ( + !loa.expiryDate + || !permitStart.isAfter( + getEndOfDate(toLocalDayjs(loa.expiryDate)), + ) + )); +}; + +/** + * Get the most recent expiry date for a list of LOAs. + * @param loas LOAs with or without expiry dates + * @returns The most recent expiry date for all the LOAs, or null if none of the LOAs expire + */ +export const getMostRecentExpiryFromLOAs = (loas: LOADetail[]) => { + const expiringLOAs = loas.filter(loa => Boolean(loa.expiryDate)); + if (expiringLOAs.length === 0) return null; + + const firstLOAExpiryDate = getEndOfDate(dayjs(expiringLOAs[0].expiryDate)); + return expiringLOAs.map(loa => loa.expiryDate) + .reduce((prevExpiry, currExpiry) => { + const prevExpiryDate = getEndOfDate(dayjs(prevExpiry)); + const currExpiryDate = getEndOfDate(dayjs(currExpiry)); + return prevExpiryDate.isAfter(currExpiryDate) ? currExpiryDate : prevExpiryDate; + }, firstLOAExpiryDate); +}; + +/** + * Get updated selectable LOAs with up-to-date information and selection state. + * This removes non-existent selected LOAs, and updates any existing selected LOAs with up-to-date info. + * @param upToDateLOAs Most recent up-to-date company LOAs + * @param prevSelectedLOAs Previously selected LOAs + * @param minPermitExpiryDate Min expiry date for a permit + * @returns Updated list of selectable LOAs with up-to-date information and selection state + */ +export const getUpdatedLOASelection = ( + upToDateLOAs: LOADetail[], + prevSelectedLOAs: LOADetail[], + minPermitExpiryDate: Dayjs, +) => { + // Each LOA should only be selected once, but there's a chance that an up-to-date LOA is also a previously selected LOA, + // which means that LOA should only be shown once. + // Thus, any overlapping LOA between the up-to-date LOAs and previously selected LOAs should only be included once, + // and all non-overlapping LOAs that are not part of the up-to-date LOAs should be removed + const prevSelectedLOANumbers = new Set([...prevSelectedLOAs.map(loa => loa.loaNumber)]); + + return upToDateLOAs.map(loa => { + const wasSelected = prevSelectedLOANumbers.has(loa.loaNumber); + const isExpiringBeforeMinPermitExpiry = Boolean(loa.expiryDate) + && minPermitExpiryDate.isAfter(getEndOfDate(dayjs(loa.expiryDate))); + + // Deselect and disable any LOAs expiring before min permit expiry date + const isSelected = wasSelected && !isExpiringBeforeMinPermitExpiry; + const isEnabled = !isExpiringBeforeMinPermitExpiry; + + return { + loa, + checked: isSelected, + disabled: !isEnabled, + }; + }); +}; + +/** + * Get updated vehicle details and options based on selected LOAs. + * @param selectedLOAs LOAs that are selected for the permit + * @param vehicleOptions Provided vehicle options for selection + * @param prevSelectedVehicle Previously selected vehicle details in the permit form + * @param ineligiblePowerUnitSubtypes Ineligible power unit subtypes + * @param ineligibleTrailerSubtypes Ineligible trailer subtypes + * @returns Updated vehicle details and filtered vehicle options + */ +export const getUpdatedVehicleDetailsForLOAs = ( + selectedLOAs: LOADetail[], + vehicleOptions: (PowerUnit | Trailer)[], + prevSelectedVehicle: PermitVehicleDetails, + ineligiblePowerUnitSubtypes: string[], + ineligibleTrailerSubtypes: string[], +) => { + const filteredVehicles = filterVehicles( + vehicleOptions, + ineligiblePowerUnitSubtypes, + ineligibleTrailerSubtypes, + selectedLOAs, + ); + + const filteredVehicleIds = filteredVehicles.map(filteredVehicle => ({ + filteredVehicleType: filteredVehicle.vehicleType, + filteredVehicleId: filteredVehicle.vehicleType === VEHICLE_TYPES.TRAILER + ? (filteredVehicle as Trailer).trailerId + : (filteredVehicle as PowerUnit).powerUnitId, + })); + + // If vehicle selected is an existing vehicle but is not in list of vehicle options + // Clear the selected vehicle + const { vehicleId, vehicleType } = prevSelectedVehicle; + if (vehicleId && !filteredVehicleIds.some(({ + filteredVehicleType, + filteredVehicleId, + }) => filteredVehicleType === vehicleType && filteredVehicleId === vehicleId)) { + return { + filteredVehicleOptions: filteredVehicles, + updatedVehicle: getDefaultVehicleDetails(), + }; + } + + return { + filteredVehicleOptions: filteredVehicles, + updatedVehicle: prevSelectedVehicle, + }; +}; + +/** + * Applying the most up-to-date LOA info to application data. + * @param applicationData Existing application data + * @param upToDateLOAs Most recent up-to-date company LOAs + * @param inventoryVehicles Vehicle options from the inventory + * @param ineligiblePowerUnitSubtypes Ineligible power unit subtypes that cannot be used for vehicles + * @param ineligibleTrailerSubtypes Ineligible trailer subtypes that cannot be used for vehicles + * @returns Application data after applying the up-to-date LOAs + */ +export const applyUpToDateLOAsToApplication = >( + applicationData: T, + upToDateLOAs: LOADetail[], + inventoryVehicles: (PowerUnit | Trailer)[], + ineligiblePowerUnitSubtypes: string[], + ineligibleTrailerSubtypes: string[], +): T => { + // If application doesn't exist, no need to apply LOAs to it at all + if (!applicationData) return applicationData; + + // Applicable LOAs must be: + // 1. Applicable for the current permit type + // 2. Have expiry date that is on or after the start date for an application + const applicableLOAs = filterNonExpiredLOAs( + filterLOAsForPermitType( + getDefaultRequiredVal([], upToDateLOAs), + applicationData.permitType, + ), + applicationData.permitData.startDate, + ); + + // Update selected LOAs in the permit data + const prevSelectedLOAs = getDefaultRequiredVal([], applicationData.permitData.loas); + const minPermitExpiryDate = getMinPermitExpiryDate( + applicationData.permitType, + applicationData.permitData.startDate, + ); + + const newSelectedLOAs = getUpdatedLOASelection( + applicableLOAs, + prevSelectedLOAs, + minPermitExpiryDate, + ) + .filter(({ checked }) => checked) + .map(({ loa }) => loa); + + // Update duration in permit if selected LOAs changed + const durationOptions = getAvailableDurationOptions( + durationOptionsForPermitType(applicationData.permitType), + newSelectedLOAs, + applicationData.permitData.startDate, + ); + + const updatedDuration = handleUpdateDurationIfNeeded( + applicationData.permitType, + applicationData.permitData.permitDuration, + durationOptions, + ); + + // Update vehicle details in permit if selected LOAs changed + const { updatedVehicle } = getUpdatedVehicleDetailsForLOAs( + newSelectedLOAs, + inventoryVehicles, + applicationData.permitData.vehicleDetails, + ineligiblePowerUnitSubtypes, + ineligibleTrailerSubtypes, + ); + + return { + ...applicationData, + permitData: { + ...applicationData.permitData, + permitDuration: updatedDuration, + loas: newSelectedLOAs, + vehicleDetails: updatedVehicle, + }, + }; +}; diff --git a/frontend/src/features/permits/helpers/permitVehicles.ts b/frontend/src/features/permits/helpers/permitVehicles.ts index 0c158e37f..aa667e489 100644 --- a/frontend/src/features/permits/helpers/permitVehicles.ts +++ b/frontend/src/features/permits/helpers/permitVehicles.ts @@ -1,6 +1,11 @@ import { PERMIT_TYPES, PermitType } from "../types/PermitType"; import { TROW_INELIGIBLE_POWERUNITS, TROW_INELIGIBLE_TRAILERS } from "../constants/trow"; import { TROS_INELIGIBLE_POWERUNITS, TROS_INELIGIBLE_TRAILERS } from "../constants/tros"; +import { LOADetail } from "../../settings/types/SpecialAuthorization"; +import { applyWhenNotNullable, getDefaultRequiredVal } from "../../../common/helpers/util"; +import { Nullable } from "../../../common/types/common"; +import { PermitVehicleDetails } from "../types/PermitVehicleDetails"; +import { isVehicleSubtypeLCV } from "../../manageVehicles/helpers/vehicleSubtypes"; import { PowerUnit, Trailer, @@ -32,12 +37,52 @@ export const getIneligibleTrailerSubtypes = (permitType: PermitType) => { } }; +/** + * Get all ineligible power unit and trailer subtypes based on LCV designation and permit type. + * @param permitType Permit type + * @param isLcvDesignated Whether or not the LCV designation is used + * @returns All ineligible power unit and trailer subtypes + */ +export const getIneligibleSubtypes = ( + permitType: PermitType, + isLcvDesignated: boolean, +) => { + return { + ineligibleTrailerSubtypes: getIneligibleTrailerSubtypes(permitType), + ineligiblePowerUnitSubtypes: getIneligiblePowerUnitSubtypes(permitType) + .filter(subtype => !isLcvDesignated || !isVehicleSubtypeLCV(subtype.typeCode)), + }; +}; + +/** + * Gets default values for vehicle details, or populate with values from existing vehicle details. + * @param vehicleDetails existing vehicle details, if any + * @returns default values for vehicle details + */ +export const getDefaultVehicleDetails = ( + vehicleDetails?: Nullable, +) => ({ + vehicleId: getDefaultRequiredVal("", vehicleDetails?.vehicleId), + unitNumber: getDefaultRequiredVal("", vehicleDetails?.unitNumber), + vin: getDefaultRequiredVal("", vehicleDetails?.vin), + plate: getDefaultRequiredVal("", vehicleDetails?.plate), + make: getDefaultRequiredVal("", vehicleDetails?.make), + year: applyWhenNotNullable((year) => year, vehicleDetails?.year, null), + countryCode: getDefaultRequiredVal("", vehicleDetails?.countryCode), + provinceCode: getDefaultRequiredVal("", vehicleDetails?.provinceCode), + vehicleType: getDefaultRequiredVal("", vehicleDetails?.vehicleType), + vehicleSubType: getDefaultRequiredVal("", vehicleDetails?.vehicleSubType), + saveVehicle: false, +}); + /** * A helper method that filters eligible power unit or trailer subtypes for dropdown lists. * @param allVehicleSubtypes List of both eligible and ineligible vehicle subtypes * @param vehicleType Type of vehicle * @param ineligiblePowerUnitSubtypes List of provided ineligible power unit subtypes * @param ineligibleTrailerSubtypes List of provided ineligible trailer subtypes + * @param allowedPowerUnitSubtypes List of provided allowed power unit subtypes + * @param allowedTrailerSubtypes List of provided allowed trailer subtypes * @returns List of only eligible power unit or trailer subtypes */ export const filterVehicleSubtypes = ( @@ -45,12 +90,19 @@ export const filterVehicleSubtypes = ( vehicleType: VehicleType, ineligiblePowerUnitSubtypes: VehicleSubType[], ineligibleTrailerSubtypes: VehicleSubType[], + allowedPowerUnitSubtypes: string[], + allowedTrailerSubtypes: string[], ) => { const ineligibleSubtypes = vehicleType === VEHICLE_TYPES.TRAILER ? ineligibleTrailerSubtypes : ineligiblePowerUnitSubtypes; + const allowedSubtypes = vehicleType === VEHICLE_TYPES.TRAILER + ? allowedTrailerSubtypes : allowedPowerUnitSubtypes; + return allVehicleSubtypes.filter((vehicleSubtype) => { - return !ineligibleSubtypes.some( + return allowedSubtypes.some( + allowedSubtype => vehicleSubtype.typeCode === allowedSubtype + ) || !ineligibleSubtypes.some( (ineligibleSubtype) => vehicleSubtype.typeCode === ineligibleSubtype.typeCode, ); }); @@ -61,24 +113,44 @@ export const filterVehicleSubtypes = ( * @param vehicles List of both eligible and ineligible vehicles * @param ineligiblePowerUnitSubtypes List of ineligible power unit subtypes * @param ineligibleTrailerSubtypes List of ineligible trailer subtypes + * @param loas LOAs that potentially bypass ineligible vehicle restrictions * @returns List of only eligible vehicles */ export const filterVehicles = ( vehicles: Vehicle[], - ineligiblePowerUnitSubtypes: VehicleSubType[], - ineligibleTrailerSubtypes: VehicleSubType[], + ineligiblePowerUnitSubtypes: string[], + ineligibleTrailerSubtypes: string[], + loas: LOADetail[], ) => { + const permittedPowerUnitIds = new Set([ + ...loas.map(loa => loa.powerUnits) + .reduce((prevPowerUnits, currPowerUnits) => [ + ...prevPowerUnits, + ...currPowerUnits, + ], []), + ]); + + const permittedTrailerIds = new Set([ + ...loas.map(loa => loa.trailers) + .reduce((prevTrailers, currTrailers) => [ + ...prevTrailers, + ...currTrailers, + ], []), + ]); + return vehicles.filter((vehicle) => { if (vehicle.vehicleType === VEHICLE_TYPES.TRAILER) { const trailer = vehicle as Trailer; - return !ineligibleTrailerSubtypes.some((ineligibleSubtype) => { - return trailer.trailerTypeCode === ineligibleSubtype.typeCode; - }); + return permittedTrailerIds.has(trailer.trailerId as string) + || !ineligibleTrailerSubtypes.some((ineligibleSubtype) => { + return trailer.trailerTypeCode === ineligibleSubtype; + }); } const powerUnit = vehicle as PowerUnit; - return !ineligiblePowerUnitSubtypes.some((ineligibleSubtype) => { - return powerUnit.powerUnitTypeCode === ineligibleSubtype.typeCode; - }); + return permittedPowerUnitIds.has(powerUnit.powerUnitId as string) + || !ineligiblePowerUnitSubtypes.some((ineligibleSubtype) => { + return powerUnit.powerUnitTypeCode === ineligibleSubtype; + }); }); }; diff --git a/frontend/src/features/permits/hooks/useApplicationFormContext.ts b/frontend/src/features/permits/hooks/useApplicationFormContext.ts new file mode 100644 index 000000000..cb67852d6 --- /dev/null +++ b/frontend/src/features/permits/hooks/useApplicationFormContext.ts @@ -0,0 +1,114 @@ +import { useContext } from "react"; + +import { ApplicationFormContext } from "../context/ApplicationFormContext"; +import { usePermitDateSelection } from "./usePermitDateSelection"; +import { LOADetail } from "../../settings/types/SpecialAuthorization"; +import { usePermitConditions } from "./usePermitConditions"; +import { getStartOfDate } from "../../../common/helpers/formatDate"; +import { getIneligibleSubtypes } from "../helpers/permitVehicles"; +import { usePermitVehicleForLOAs } from "./usePermitVehicleForLOAs"; + +export const useApplicationFormContext = () => { + const { + initialFormData, + formData, + durationOptions, + vehicleOptions, + powerUnitSubtypes, + trailerSubtypes, + isLcvDesignated, + feature, + companyInfo, + isAmendAction, + createdDateTime, + updatedDateTime, + pastStartDateStatus, + companyLOAs, + revisionHistory, + onLeave, + onSave, + onCancel, + onContinue, + onSetDuration, + onSetExpiryDate, + onSetConditions, + onToggleSaveVehicle, + onSetVehicle, + onClearVehicle, + onUpdateLOAs, + } = useContext(ApplicationFormContext); + + const permitType = formData.permitType; + const { + loas: currentSelectedLOAs, + permitDuration, + startDate: permitStartDate, + commodities: permitConditions, + vehicleDetails: vehicleFormData, + } = formData.permitData; + + // Update duration options and expiry when needed + const { availableDurationOptions } = usePermitDateSelection( + permitType, + getStartOfDate(permitStartDate), + durationOptions, + currentSelectedLOAs as LOADetail[], + permitDuration, + onSetDuration, + onSetExpiryDate, + ); + + // Update permit conditions when LCV designation or vehicle subtype changes + usePermitConditions( + permitConditions, + isLcvDesignated, + vehicleFormData.vehicleSubType, + onSetConditions, + ); + + // Check to see if vehicle details is still valid after LOA has been deselected + const { + ineligiblePowerUnitSubtypes, + ineligibleTrailerSubtypes, + } = getIneligibleSubtypes(permitType, isLcvDesignated); + + const { filteredVehicleOptions } = usePermitVehicleForLOAs( + vehicleFormData, + vehicleOptions, + currentSelectedLOAs as LOADetail[], + ineligiblePowerUnitSubtypes.map(({ typeCode }) => typeCode), + ineligibleTrailerSubtypes.map(({ typeCode }) => typeCode), + () => onClearVehicle(Boolean(vehicleFormData.saveVehicle)), + ); + + return { + initialFormData, + formData, + availableDurationOptions, + powerUnitSubtypes, + trailerSubtypes, + isLcvDesignated, + ineligiblePowerUnitSubtypes, + ineligibleTrailerSubtypes, + filteredVehicleOptions, + feature, + companyInfo, + isAmendAction, + createdDateTime, + updatedDateTime, + pastStartDateStatus, + companyLOAs, + revisionHistory, + onLeave, + onSave, + onCancel, + onContinue, + onSetDuration, + onSetExpiryDate, + onSetConditions, + onToggleSaveVehicle, + onSetVehicle, + onClearVehicle, + onUpdateLOAs, + }; +}; \ No newline at end of file diff --git a/frontend/src/features/permits/hooks/useDefaultApplicationFormData.ts b/frontend/src/features/permits/hooks/useDefaultApplicationFormData.ts deleted file mode 100644 index 52dcddfba..000000000 --- a/frontend/src/features/permits/hooks/useDefaultApplicationFormData.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { useEffect, useMemo } from "react"; -import { useForm } from "react-hook-form"; - -import { Application, ApplicationFormData } from "../types/application"; -import { BCeIDUserDetailContext } from "../../../common/authentication/OnRouteBCContext"; -import { CompanyProfile } from "../../manageProfile/types/manageProfile"; -import { Nullable } from "../../../common/types/common"; -import { PermitType } from "../types/PermitType"; -import { - applyLCVToApplicationData, - getDefaultValues, -} from "../helpers/getDefaultApplicationFormData"; - -/** - * Custom hook for populating the form using fetched application data, as well as current company id and user details. - * This also involves resetting certain form values whenever new/updated application data is fetched. - * @param permitType Permit type for the application - * @param isLcvDesignated Whether or not the company is designated to use LCV for permits - * @param companyInfo Company information for filling out the form - * @param applicationData Application data received to fill out the form, preferrably from ApplicationContext/backend - * @param userDetails User details for filling out the form - * @returns Current application form data and methods to manage the form - */ -export const useDefaultApplicationFormData = ( - permitType: PermitType, - isLcvDesignated: boolean, - companyInfo: Nullable, - applicationData?: Nullable, - userDetails?: BCeIDUserDetailContext, -) => { - // Used to populate/initialize the form with - // This will be updated whenever new application, company, and user data is fetched - const initialFormData = useMemo(() => applyLCVToApplicationData( - getDefaultValues( - permitType, - companyInfo, - applicationData, - userDetails, - ), - isLcvDesignated, - ), [ - permitType, - companyInfo, - applicationData, - userDetails, - isLcvDesignated, - ]); - - // Register default values with react-hook-form - const formMethods = useForm({ - defaultValues: initialFormData, - reValidateMode: "onBlur", - }); - - const { watch, reset } = formMethods; - const currentFormData = watch(); - - // Reset the form with updated default form data whenever fetched data changes - useEffect(() => { - reset(initialFormData); - }, [initialFormData]); - - return { - initialFormData, - currentFormData, - formMethods, - }; -}; diff --git a/frontend/src/features/permits/hooks/useInitApplicationFormData.ts b/frontend/src/features/permits/hooks/useInitApplicationFormData.ts new file mode 100644 index 000000000..4c60e4147 --- /dev/null +++ b/frontend/src/features/permits/hooks/useInitApplicationFormData.ts @@ -0,0 +1,133 @@ +import { useEffect, useMemo } from "react"; +import { useForm } from "react-hook-form"; +import dayjs, { Dayjs } from "dayjs"; + +import { Application, ApplicationFormData } from "../types/application"; +import { BCeIDUserDetailContext } from "../../../common/authentication/OnRouteBCContext"; +import { CompanyProfile } from "../../manageProfile/types/manageProfile"; +import { Nullable } from "../../../common/types/common"; +import { PermitType } from "../types/PermitType"; +import { LOADetail } from "../../settings/types/SpecialAuthorization"; +import { applyUpToDateLOAsToApplication } from "../helpers/permitLOA"; +import { getDefaultValues } from "../helpers/getDefaultApplicationFormData"; +import { applyLCVToApplicationData } from "../helpers/permitLCV"; +import { PowerUnit, Trailer } from "../../manageVehicles/types/Vehicle"; +import { getIneligibleSubtypes } from "../helpers/permitVehicles"; +import { PermitCondition } from "../types/PermitCondition"; +import { EMPTY_VEHICLE_DETAILS, PermitVehicleDetails } from "../types/PermitVehicleDetails"; + +/** + * Custom hook for populating the form using fetched application data, as well as current company id and user details. + * This also involves resetting certain form values whenever new/updated application data is fetched. + * @param permitType Permit type for the application + * @param isLcvDesignated Whether or not the company is designated to use LCV for permits + * @param loas Most up-to-date LOAs belonging to the company + * @param companyInfo Company information for filling out the form + * @param applicationData Application data received to fill out the form, preferrably from ApplicationContext/backend + * @param userDetails User details for filling out the form + * @returns Current application form data, methods to manage the form, and selectable input options + */ +export const useInitApplicationFormData = ( + permitType: PermitType, + isLcvDesignated: boolean, + loas: LOADetail[], + inventoryVehicles: (PowerUnit | Trailer)[], + companyInfo: Nullable, + applicationData?: Nullable, + userDetails?: BCeIDUserDetailContext, +) => { + // Used to populate/initialize the form with + // This will be updated whenever new application, company, and user data is fetched + const initialFormData = useMemo(() => { + const ineligibleSubtypes = getIneligibleSubtypes(permitType, isLcvDesignated); + const ineligiblePowerUnitSubtypes= ineligibleSubtypes.ineligiblePowerUnitSubtypes + .map(({ typeCode }) => typeCode); + + const ineligibleTrailerSubtypes = ineligibleSubtypes.ineligibleTrailerSubtypes + .map(({ typeCode }) => typeCode); + + return applyUpToDateLOAsToApplication( + applyLCVToApplicationData( + getDefaultValues( + permitType, + companyInfo, + applicationData, + userDetails, + ), + isLcvDesignated, + ), + loas, + inventoryVehicles, + ineligiblePowerUnitSubtypes, + ineligibleTrailerSubtypes, + ); + }, [ + permitType, + companyInfo, + applicationData, + userDetails, + isLcvDesignated, + loas, + inventoryVehicles, + ]); + + // Register default values with react-hook-form + const formMethods = useForm({ + defaultValues: initialFormData, + reValidateMode: "onBlur", + }); + + const { watch, reset, setValue } = formMethods; + const currentFormData = watch(); + + // Reset the form with updated default form data whenever fetched data changes + useEffect(() => { + reset(initialFormData); + }, [initialFormData]); + + const onSetDuration = (duration: number) => { + setValue("permitData.permitDuration", duration); + }; + + const onSetExpiryDate = (expiry: Dayjs) => { + setValue("permitData.expiryDate", dayjs(expiry)); + }; + + const onSetConditions = (conditions: PermitCondition[]) => { + setValue("permitData.commodities", [...conditions]); + }; + + const onToggleSaveVehicle = (saveVehicle: boolean) => { + setValue("permitData.vehicleDetails.saveVehicle", saveVehicle); + }; + + const onSetVehicle = (vehicleDetails: PermitVehicleDetails) => { + setValue("permitData.vehicleDetails", { + ...vehicleDetails, + }); + }; + + const onClearVehicle = (saveVehicle: boolean) => { + setValue("permitData.vehicleDetails", { + ...EMPTY_VEHICLE_DETAILS, + saveVehicle, + }); + }; + + const onUpdateLOAs = (updatedLOAs: LOADetail[]) => { + setValue("permitData.loas", updatedLOAs); + }; + + return { + initialFormData, + currentFormData, + formMethods, + onSetDuration, + onSetExpiryDate, + onSetConditions, + onToggleSaveVehicle, + onSetVehicle, + onClearVehicle, + onUpdateLOAs, + }; +}; diff --git a/frontend/src/features/permits/hooks/usePermitConditions.ts b/frontend/src/features/permits/hooks/usePermitConditions.ts new file mode 100644 index 000000000..2d98456d9 --- /dev/null +++ b/frontend/src/features/permits/hooks/usePermitConditions.ts @@ -0,0 +1,34 @@ +import { useEffect } from "react"; + +import { areArraysEqual } from "../../../common/helpers/util"; +import { PermitCondition } from "../types/PermitCondition"; +import { getUpdatedConditionsForLCV } from "../helpers/permitLCV"; + +export const usePermitConditions = ( + selectedConditions: PermitCondition[], + isLcvDesignated: boolean, + vehicleSubtype: string, + onSetConditions: (conditions: PermitCondition[]) => void, +) => { + // If conditions were changed as a result of LCV or vehicle subtype, update permit conditions + const updatedConditions = getUpdatedConditionsForLCV( + isLcvDesignated, + selectedConditions, + vehicleSubtype, + ); + + useEffect(() => { + if (!areArraysEqual( + updatedConditions.map(({ condition }) => condition), + selectedConditions.map(({ condition }: PermitCondition) => condition), + )) { + onSetConditions(updatedConditions); + } + }, [ + updatedConditions, + selectedConditions, + onSetConditions, + ]); + + return { updatedConditions }; +}; diff --git a/frontend/src/features/permits/hooks/usePermitDateSelection.ts b/frontend/src/features/permits/hooks/usePermitDateSelection.ts new file mode 100644 index 000000000..ca89a2f08 --- /dev/null +++ b/frontend/src/features/permits/hooks/usePermitDateSelection.ts @@ -0,0 +1,60 @@ +import { useEffect } from "react"; +import { Dayjs } from "dayjs"; + +import { getExpiryDate } from "../helpers/permitState"; +import { LOADetail } from "../../settings/types/SpecialAuthorization"; +import { PermitType } from "../types/PermitType"; +import { getAvailableDurationOptions, handleUpdateDurationIfNeeded } from "../helpers/dateSelection"; + +/** + * Hook that manages permit date selection based on changing permit data. + * @param permitType Permit type + * @param startDate Selected start date for the permit + * @param durationOptions All possible duration options for the permit + * @param selectedLOAs Selected LOAs for the permit + * @param selectedDuration Selected duration for the permit + * @returns Updated valid duration options + */ +export const usePermitDateSelection = ( + permitType: PermitType, + startDate: Dayjs, + durationOptions: { + value: number; + label: string; + }[], + selectedLOAs: LOADetail[], + selectedDuration: number, + onSetDuration: (duration: number) => void, + onSetExpiryDate: (expiry: Dayjs) => void, +) => { + // Limit permit duration options based on selected LOAs + const availableDurationOptions = getAvailableDurationOptions( + durationOptions, + selectedLOAs, + startDate, + ); + + // If duration options change, check if the current permit duration is still selectable + const updatedDuration = handleUpdateDurationIfNeeded( + permitType, + selectedDuration, + availableDurationOptions, + ); + + useEffect(() => { + onSetDuration(updatedDuration); + }, [ + updatedDuration, + onSetDuration, + ]); + + const expiryDate = getExpiryDate(startDate, selectedDuration); + useEffect(() => { + onSetExpiryDate(expiryDate); + }, [ + expiryDate, + onSetExpiryDate, + ]); + + return { availableDurationOptions }; +}; diff --git a/frontend/src/features/permits/hooks/usePermitVehicleForLOAs.ts b/frontend/src/features/permits/hooks/usePermitVehicleForLOAs.ts new file mode 100644 index 000000000..082f1a1b5 --- /dev/null +++ b/frontend/src/features/permits/hooks/usePermitVehicleForLOAs.ts @@ -0,0 +1,44 @@ +import { useEffect } from "react"; + +import { PermitVehicleDetails } from "../types/PermitVehicleDetails"; +import { PowerUnit, Trailer } from "../../manageVehicles/types/Vehicle"; +import { LOADetail } from "../../settings/types/SpecialAuthorization"; +import { getUpdatedVehicleDetailsForLOAs } from "../helpers/permitLOA"; + +export const usePermitVehicleForLOAs = ( + vehicleFormData: PermitVehicleDetails, + vehicleOptions: (PowerUnit | Trailer)[], + selectedLOAs: LOADetail[], + ineligiblePowerUnitSubtypes: string[], + ineligibleTrailerSubtypes: string[], + onClearVehicle: () => void, +) => { + // Check to see if vehicle details is still valid after LOA has been deselected + const { + filteredVehicleOptions, + updatedVehicle, + } = getUpdatedVehicleDetailsForLOAs( + selectedLOAs, + vehicleOptions, + vehicleFormData, + ineligiblePowerUnitSubtypes, + ineligibleTrailerSubtypes, + ); + + const vehicleIdInForm = vehicleFormData.vehicleId; + const updatedVehicleId = updatedVehicle.vehicleId; + useEffect(() => { + // If vehicle originally selected exists but the updated vehicle is cleared, clear the vehicle + if (vehicleIdInForm && !updatedVehicleId) { + onClearVehicle(); + } + }, [ + vehicleIdInForm, + updatedVehicleId, + onClearVehicle, + ]); + + return { + filteredVehicleOptions, + }; +}; diff --git a/frontend/src/features/permits/hooks/usePermitVehicleManagement.ts b/frontend/src/features/permits/hooks/usePermitVehicleManagement.ts index 61cd9f57a..2eb6c3262 100644 --- a/frontend/src/features/permits/hooks/usePermitVehicleManagement.ts +++ b/frontend/src/features/permits/hooks/usePermitVehicleManagement.ts @@ -1,7 +1,9 @@ +import { useCallback, useMemo } from "react"; + import { getDefaultRequiredVal } from "../../../common/helpers/util"; import { mapToVehicleObjectById } from "../helpers/mappers"; import { Nullable } from "../../../common/types/common"; -import { getDefaultVehicleDetails } from "../helpers/getDefaultApplicationFormData"; +import { getDefaultVehicleDetails } from "../helpers/permitVehicles"; import { PermitVehicleDetails } from "../types/PermitVehicleDetails"; import { usePowerUnitSubTypesQuery, @@ -25,6 +27,68 @@ import { VehicleType, } from "../../manageVehicles/types/Vehicle"; +const transformByVehicleType = ( + vehicleFormData: PermitVehicleDetails, + existingVehicle?: Vehicle, +): Vehicle => { + const defaultPowerUnit: PowerUnit = { + powerUnitId: "", + unitNumber: "", + vin: vehicleFormData.vin, + plate: vehicleFormData.plate, + make: vehicleFormData.make, + year: vehicleFormData.year, + countryCode: vehicleFormData.countryCode, + provinceCode: vehicleFormData.provinceCode, + powerUnitTypeCode: vehicleFormData.vehicleSubType, + }; + + const defaultTrailer: Trailer = { + trailerId: "", + unitNumber: "", + vin: vehicleFormData.vin, + plate: vehicleFormData.plate, + make: vehicleFormData.make, + year: vehicleFormData.year, + countryCode: vehicleFormData.countryCode, + provinceCode: vehicleFormData.provinceCode, + trailerTypeCode: vehicleFormData.vehicleSubType, + }; + + switch (vehicleFormData.vehicleType) { + case VEHICLE_TYPES.TRAILER: + return { + ...defaultTrailer, + trailerId: getDefaultRequiredVal( + "", + (existingVehicle as Trailer)?.trailerId, + ), + unitNumber: getDefaultRequiredVal( + "", + existingVehicle?.unitNumber, + vehicleFormData.unitNumber, + ), + } as Trailer; + case VEHICLE_TYPES.POWER_UNIT: + default: + return { + ...defaultPowerUnit, + unitNumber: getDefaultRequiredVal( + "", + existingVehicle?.unitNumber, + vehicleFormData.unitNumber, + ), + powerUnitId: getDefaultRequiredVal( + "", + (existingVehicle as PowerUnit)?.powerUnitId, + ), + } as PowerUnit; + } +}; + +const modifyVehicleSuccess = (status: number) => + status === 201 || status === 200; + export const usePermitVehicleManagement = (companyId: number) => { // Mutations used to add/update vehicle details const addPowerUnitMutation = useAddPowerUnitMutation(); @@ -38,7 +102,7 @@ export const usePermitVehicleManagement = (companyId: number) => { const { data: powerUnitSubtypesData } = usePowerUnitSubTypesQuery(); const { data: trailerSubtypesData } = useTrailerSubTypesQuery(); - const fetchedVehicles = [ + const fetchedVehicles = useMemo(() => [ ...getDefaultRequiredVal( [], powerUnitsData, @@ -53,12 +117,12 @@ export const usePermitVehicleManagement = (companyId: number) => { ...trailer, vehicleType: VEHICLE_TYPES.TRAILER, })), - ]; + ], [powerUnitsData, trailersData]); const powerUnitSubTypes = getDefaultRequiredVal([], powerUnitSubtypesData); const trailerSubTypes = getDefaultRequiredVal([], trailerSubtypesData); - const handleSaveVehicle = async ( + const handleSaveVehicle = useCallback(async ( vehicleData?: Nullable, ): Promise> => { // Check if the "add/update vehicle" checkbox was checked by the user @@ -76,68 +140,6 @@ export const usePermitVehicleManagement = (companyId: number) => { vehicleId, ); - const transformByVehicleType = ( - vehicleFormData: PermitVehicleDetails, - existingVehicle?: Vehicle, - ): Vehicle => { - const defaultPowerUnit: PowerUnit = { - powerUnitId: "", - unitNumber: "", - vin: vehicleFormData.vin, - plate: vehicleFormData.plate, - make: vehicleFormData.make, - year: vehicleFormData.year, - countryCode: vehicleFormData.countryCode, - provinceCode: vehicleFormData.provinceCode, - powerUnitTypeCode: vehicleFormData.vehicleSubType, - }; - - const defaultTrailer: Trailer = { - trailerId: "", - unitNumber: "", - vin: vehicleFormData.vin, - plate: vehicleFormData.plate, - make: vehicleFormData.make, - year: vehicleFormData.year, - countryCode: vehicleFormData.countryCode, - provinceCode: vehicleFormData.provinceCode, - trailerTypeCode: vehicleFormData.vehicleSubType, - }; - - switch (vehicleFormData.vehicleType) { - case VEHICLE_TYPES.TRAILER: - return { - ...defaultTrailer, - trailerId: getDefaultRequiredVal( - "", - (existingVehicle as Trailer)?.trailerId, - ), - unitNumber: getDefaultRequiredVal( - "", - existingVehicle?.unitNumber, - vehicleFormData.unitNumber, - ), - } as Trailer; - case VEHICLE_TYPES.POWER_UNIT: - default: - return { - ...defaultPowerUnit, - unitNumber: getDefaultRequiredVal( - "", - existingVehicle?.unitNumber, - vehicleFormData.unitNumber, - ), - powerUnitId: getDefaultRequiredVal( - "", - (existingVehicle as PowerUnit)?.powerUnitId, - ), - } as PowerUnit; - } - }; - - const modifyVehicleSuccess = (status: number) => - status === 201 || status === 200; - // If the vehicle type is a power unit then create a power unit object if (vehicle.vehicleType === VEHICLE_TYPES.POWER_UNIT) { const powerUnit = transformByVehicleType( @@ -204,7 +206,7 @@ export const usePermitVehicleManagement = (companyId: number) => { } return undefined; - }; + }, [fetchedVehicles]); return { handleSaveVehicle, diff --git a/frontend/src/features/permits/pages/Amend/components/AmendPermitForm.tsx b/frontend/src/features/permits/pages/Amend/components/AmendPermitForm.tsx index eb59e5c27..4379cda5b 100644 --- a/frontend/src/features/permits/pages/Amend/components/AmendPermitForm.tsx +++ b/frontend/src/features/permits/pages/Amend/components/AmendPermitForm.tsx @@ -1,4 +1,4 @@ -import { useContext } from "react"; +import { useContext, useMemo } from "react"; import { FieldValues, FormProvider } from "react-hook-form"; import { useNavigate, useParams } from "react-router-dom"; @@ -11,8 +11,7 @@ import { PermitForm } from "../../Application/components/form/PermitForm"; import { Application } from "../../../types/application"; import { useCompanyInfoDetailsQuery } from "../../../../manageProfile/apiManager/hooks"; import { Breadcrumb } from "../../../../../common/components/breadcrumb/Breadcrumb"; -import { AmendRevisionHistory } from "./form/AmendRevisionHistory"; -import { AmendReason } from "./form/AmendReason"; +import { ApplicationFormContext } from "../../../context/ApplicationFormContext"; import { Nullable } from "../../../../../common/types/common"; import { ERROR_ROUTES } from "../../../../../routes/constants"; import { applyWhenNotNullable, getDefaultRequiredVal } from "../../../../../common/helpers/util"; @@ -20,6 +19,7 @@ import { PermitVehicleDetails } from "../../../types/PermitVehicleDetails"; import { AmendPermitFormData } from "../types/AmendPermitFormData"; import { getDatetimes } from "./helpers/getDatetimes"; import { PAST_START_DATE_STATUSES } from "../../../../../common/components/form/subFormComponents/CustomDatePicker"; +import { useFetchLOAs } from "../../../../settings/hooks/LOA"; import { useFetchSpecialAuthorizations } from "../../../../settings/hooks/specialAuthorizations"; import { dayjsToUtcStr, @@ -54,15 +54,34 @@ export const AmendPermitForm = () => { const companyId: number = applyWhenNotNullable(id => Number(id), companyIdParam, 0); const navigate = useNavigate(); + const { data: activeLOAs } = useFetchLOAs(companyId, false); const { data: companyInfo } = useCompanyInfoDetailsQuery(companyId); - const doingBusinessAs = companyInfo?.alternateName; - const { data: specialAuthorizations } = useFetchSpecialAuthorizations(companyId); const isLcvDesignated = Boolean(specialAuthorizations?.isLcvAllowed); - const { formData, formMethods } = useAmendPermitForm( + const { + handleSaveVehicle, + vehicleOptions, + powerUnitSubTypes, + trailerSubTypes, + } = usePermitVehicleManagement(companyId); + + const { + initialFormData, + formData, + formMethods, + onSetDuration, + onSetExpiryDate, + onSetConditions, + onToggleSaveVehicle, + onSetVehicle, + onClearVehicle, + onUpdateLOAs, + } = useAmendPermitForm( currentStepIndex === 0, isLcvDesignated, + getDefaultRequiredVal([], activeLOAs), + vehicleOptions, companyInfo, permit, amendmentApplication, @@ -73,17 +92,13 @@ export const AmendPermitForm = () => { permit, ); + const applicableLOAs = getDefaultRequiredVal([], activeLOAs) + .filter(loa => loa.loaPermitType.includes(formData.permitType)); + const amendPermitMutation = useAmendPermit(companyId); const modifyAmendmentMutation = useModifyAmendmentApplication(); const snackBar = useContext(SnackBarContext); - const { - handleSaveVehicle, - vehicleOptions, - powerUnitSubTypes, - trailerSubTypes, - } = usePermitVehicleManagement(companyId); - const { handleSubmit } = formMethods; // Helper method to return form field values as an Permit object @@ -198,31 +213,67 @@ export const AmendPermitForm = () => { (duration) => duration.value <= permitOldDuration, ); + const applicationFormContextData = useMemo(() => ({ + initialFormData, + formData, + durationOptions, + vehicleOptions, + powerUnitSubtypes: powerUnitSubTypes, + trailerSubtypes: trailerSubTypes, + isLcvDesignated, + feature: FEATURE, + companyInfo, + isAmendAction: true, + createdDateTime, + updatedDateTime, + pastStartDateStatus: PAST_START_DATE_STATUSES.WARNING, + companyLOAs: applicableLOAs, + revisionHistory, + onLeave: undefined, + onSave: undefined, + onCancel: goHome, + onContinue: handleSubmit(onContinue), + onSetDuration, + onSetExpiryDate, + onSetConditions, + onToggleSaveVehicle, + onSetVehicle, + onClearVehicle, + onUpdateLOAs, + }), [ + initialFormData, + formData, + durationOptions, + vehicleOptions, + powerUnitSubTypes, + trailerSubTypes, + isLcvDesignated, + companyInfo, + createdDateTime, + updatedDateTime, + applicableLOAs, + revisionHistory, + goHome, + onContinue, + onSetDuration, + onSetExpiryDate, + onSetConditions, + onToggleSaveVehicle, + onSetVehicle, + onClearVehicle, + onUpdateLOAs, + ]); + return (
- - - - + +
); diff --git a/frontend/src/features/permits/pages/Amend/components/AmendPermitReview.tsx b/frontend/src/features/permits/pages/Amend/components/AmendPermitReview.tsx index 1448d1e60..91a08b3b3 100644 --- a/frontend/src/features/permits/pages/Amend/components/AmendPermitReview.tsx +++ b/frontend/src/features/permits/pages/Amend/components/AmendPermitReview.tsx @@ -160,6 +160,7 @@ export const AmendPermitReview = () => { }} calculatedFee={`${amountToRefund}`} doingBusinessAs={doingBusinessAs} + loas={amendmentApplication?.permitData?.loas} > {amendmentApplication?.comment ? ( diff --git a/frontend/src/features/permits/pages/Amend/hooks/useAmendPermitForm.ts b/frontend/src/features/permits/pages/Amend/hooks/useAmendPermitForm.ts index 1f1d665d6..275194cde 100644 --- a/frontend/src/features/permits/pages/Amend/hooks/useAmendPermitForm.ts +++ b/frontend/src/features/permits/pages/Amend/hooks/useAmendPermitForm.ts @@ -1,21 +1,30 @@ import { useEffect, useMemo } from "react"; import { useForm } from "react-hook-form"; +import dayjs, { Dayjs } from "dayjs"; import { Nullable } from "../../../../../common/types/common"; import { Permit } from "../../../types/permit"; import { Application } from "../../../types/application"; import { applyWhenNotNullable } from "../../../../../common/helpers/util"; import { CompanyProfile } from "../../../../manageProfile/types/manageProfile"; -import { applyLCVToApplicationData } from "../../../helpers/getDefaultApplicationFormData"; +import { applyLCVToApplicationData } from "../../../helpers/permitLCV"; +import { PermitCondition } from "../../../types/PermitCondition"; +import { EMPTY_VEHICLE_DETAILS, PermitVehicleDetails } from "../../../types/PermitVehicleDetails"; +import { LOADetail } from "../../../../settings/types/SpecialAuthorization"; +import { getIneligibleSubtypes } from "../../../helpers/permitVehicles"; import { AmendPermitFormData, getDefaultFormDataFromApplication, getDefaultFormDataFromPermit, } from "../types/AmendPermitFormData"; +import { applyUpToDateLOAsToApplication } from "../../../helpers/permitLOA"; +import { PowerUnit, Trailer } from "../../../../manageVehicles/types/Vehicle"; export const useAmendPermitForm = ( repopulateFormData: boolean, isLcvDesignated: boolean, + loas: LOADetail[], + inventoryVehicles: (PowerUnit | Trailer)[], companyInfo: Nullable, permit?: Nullable, amendmentApplication?: Nullable, @@ -23,36 +32,74 @@ export const useAmendPermitForm = ( // Default form data values to populate the amend form with const defaultFormData = useMemo(() => { if (amendmentApplication) { - return applyLCVToApplicationData( - getDefaultFormDataFromApplication( - companyInfo, - amendmentApplication, - ), + const ineligibleSubtypes = getIneligibleSubtypes( + amendmentApplication.permitType, isLcvDesignated, ); + + const ineligiblePowerUnitSubtypes= ineligibleSubtypes.ineligiblePowerUnitSubtypes + .map(({ typeCode }) => typeCode); + + const ineligibleTrailerSubtypes = ineligibleSubtypes.ineligibleTrailerSubtypes + .map(({ typeCode }) => typeCode); + + return applyUpToDateLOAsToApplication( + applyLCVToApplicationData( + getDefaultFormDataFromApplication( + companyInfo, + amendmentApplication, + ), + isLcvDesignated, + ), + loas, + inventoryVehicles, + ineligiblePowerUnitSubtypes, + ineligibleTrailerSubtypes, + ); } // Permit doesn't have existing amendment application // Populate form data with permit, with initial empty comment - return applyLCVToApplicationData( - getDefaultFormDataFromPermit( - companyInfo, - applyWhenNotNullable( - (p) => ({ - ...p, - comment: "", - }), - permit, - ), + const defaultPermitFormData = getDefaultFormDataFromPermit( + companyInfo, + applyWhenNotNullable( + (p) => ({ + ...p, + comment: "", + }), + permit, ), + ); + + const ineligibleSubtypes = getIneligibleSubtypes( + defaultPermitFormData.permitType, isLcvDesignated, ); + + const ineligiblePowerUnitSubtypes= ineligibleSubtypes.ineligiblePowerUnitSubtypes + .map(({ typeCode }) => typeCode); + + const ineligibleTrailerSubtypes = ineligibleSubtypes.ineligibleTrailerSubtypes + .map(({ typeCode }) => typeCode); + + return applyUpToDateLOAsToApplication( + applyLCVToApplicationData( + defaultPermitFormData, + isLcvDesignated, + ), + loas, + inventoryVehicles, + ineligiblePowerUnitSubtypes, + ineligibleTrailerSubtypes, + ); }, [ amendmentApplication, permit, repopulateFormData, companyInfo, isLcvDesignated, + loas, + inventoryVehicles, ]); // Register default values with react-hook-form @@ -61,15 +108,56 @@ export const useAmendPermitForm = ( reValidateMode: "onBlur", }); - const { reset, watch } = formMethods; + const { reset, watch, setValue } = formMethods; const formData = watch(); useEffect(() => { reset(defaultFormData); }, [defaultFormData]); + const onSetDuration = (duration: number) => { + setValue("permitData.permitDuration", duration); + }; + + const onSetExpiryDate = (expiry: Dayjs) => { + setValue("permitData.expiryDate", dayjs(expiry)); + }; + + const onSetConditions = (conditions: PermitCondition[]) => { + setValue("permitData.commodities", [...conditions]); + }; + + const onToggleSaveVehicle = (saveVehicle: boolean) => { + setValue("permitData.vehicleDetails.saveVehicle", saveVehicle); + }; + + const onSetVehicle = (vehicleDetails: PermitVehicleDetails) => { + setValue("permitData.vehicleDetails", { + ...vehicleDetails, + }); + }; + + const onClearVehicle = (saveVehicle: boolean) => { + setValue("permitData.vehicleDetails", { + ...EMPTY_VEHICLE_DETAILS, + saveVehicle, + }); + }; + + const onUpdateLOAs = (updatedLOAs: LOADetail[]) => { + setValue("permitData.loas", updatedLOAs); + }; + return { + initialFormData: defaultFormData, formData, formMethods, + onSetDuration, + onSetExpiryDate, + onSetConditions, + onToggleSaveVehicle, + onSetVehicle, + onClearVehicle, + onUpdateLOAs, }; }; diff --git a/frontend/src/features/permits/pages/Application/ApplicationForm.tsx b/frontend/src/features/permits/pages/Application/ApplicationForm.tsx index 0b3f7ac88..c5d9b48d6 100644 --- a/frontend/src/features/permits/pages/Application/ApplicationForm.tsx +++ b/frontend/src/features/permits/pages/Application/ApplicationForm.tsx @@ -1,6 +1,6 @@ import { FieldValues, FormProvider } from "react-hook-form"; import { useNavigate } from "react-router-dom"; -import { useContext, useState } from "react"; +import { useContext, useMemo, useState } from "react"; import dayjs from "dayjs"; import "./ApplicationForm.scss"; @@ -11,7 +11,7 @@ import { useSaveApplicationMutation } from "../../hooks/hooks"; import { SnackBarContext } from "../../../../App"; import { LeaveApplicationDialog } from "../../components/dialog/LeaveApplicationDialog"; import { areApplicationDataEqual } from "../../helpers/equality"; -import { useDefaultApplicationFormData } from "../../hooks/useDefaultApplicationFormData"; +import { useInitApplicationFormData } from "../../hooks/useInitApplicationFormData"; import OnRouteBCContext from "../../../../common/authentication/OnRouteBCContext"; import { PermitForm } from "./components/form/PermitForm"; import { usePermitVehicleManagement } from "../../hooks/usePermitVehicleManagement"; @@ -22,7 +22,10 @@ import { PermitVehicleDetails } from "../../types/PermitVehicleDetails"; import { durationOptionsForPermitType } from "../../helpers/dateSelection"; import { getCompanyIdFromSession } from "../../../../common/apiManager/httpRequestHandler"; import { PAST_START_DATE_STATUSES } from "../../../../common/components/form/subFormComponents/CustomDatePicker"; +import { useFetchLOAs } from "../../../settings/hooks/LOA"; import { useFetchSpecialAuthorizations } from "../../../settings/hooks/specialAuthorizations"; +import { ApplicationFormContext } from "../../context/ApplicationFormContext"; +import { filterLOAsForPermitType, filterNonExpiredLOAs } from "../../helpers/permitLOA"; import { applyWhenNotNullable, getDefaultRequiredVal, @@ -64,11 +67,19 @@ export const ApplicationForm = ({ permitType }: { permitType: PermitType }) => { companyInfo?.companyId, ); + const { data: activeLOAs } = useFetchLOAs(companyId, false); const { data: specialAuthorizations } = useFetchSpecialAuthorizations(companyId); const isLcvDesignated = Boolean(specialAuthorizations?.isLcvAllowed); - + + const { + handleSaveVehicle, + vehicleOptions, + powerUnitSubTypes, + trailerSubTypes, + } = usePermitVehicleManagement(companyId); + // Use a custom hook that performs the following whenever page is rendered (or when application context is updated/changed): - // 1. Get all data needed to generate default values for the application form (from application context, company, user details) + // 1. Get all data needed to initialize the application form (from application context, company, user details) // 2. Generate those default values and register them to the form // 3. Listens for changes to application context (which happens when application is fetched/submitted/updated) // 4. Updates form values (override existing ones) whenever the application context data changes @@ -76,14 +87,34 @@ export const ApplicationForm = ({ permitType }: { permitType: PermitType }) => { initialFormData, currentFormData, formMethods, - } = useDefaultApplicationFormData( + onSetDuration, + onSetExpiryDate, + onSetConditions, + onToggleSaveVehicle, + onSetVehicle, + onClearVehicle, + onUpdateLOAs, + } = useInitApplicationFormData( permitType, isLcvDesignated, + getDefaultRequiredVal([], activeLOAs), + vehicleOptions, companyInfo, applicationContext?.applicationData, userDetails, ); + // Applicable LOAs must be: + // 1. Applicable for the current permit type + // 2. Have expiry date that is on or after the start date for an application + const applicableLOAs = filterNonExpiredLOAs( + filterLOAsForPermitType( + getDefaultRequiredVal([], activeLOAs), + permitType, + ), + currentFormData.permitData.startDate, + ); + const createdDateTime = applyWhenNotNullable( (date) => dayjs(date), applicationContext?.applicationData?.createdDateTime, @@ -94,18 +125,9 @@ export const ApplicationForm = ({ permitType }: { permitType: PermitType }) => { applicationContext?.applicationData?.updatedDateTime, ); - const doingBusinessAs = companyInfo?.alternateName; - const saveApplicationMutation = useSaveApplicationMutation(); const snackBar = useContext(SnackBarContext); - const { - handleSaveVehicle, - vehicleOptions, - powerUnitSubTypes, - trailerSubTypes, - } = usePermitVehicleManagement(companyId); - // Show leave application dialog const [showLeaveApplicationDialog, setShowLeaveApplicationDialog] = useState(false); @@ -242,33 +264,73 @@ export const ApplicationForm = ({ permitType }: { permitType: PermitType }) => { setShowLeaveApplicationDialog(false); }; + const durationOptions = durationOptionsForPermitType(permitType); + const pastStartDateStatus = isStaffUser + ? PAST_START_DATE_STATUSES.WARNING + : PAST_START_DATE_STATUSES.FAIL; + + const applicationFormContextData = useMemo(() => ({ + initialFormData, + formData: currentFormData, + durationOptions, + vehicleOptions, + powerUnitSubtypes: powerUnitSubTypes, + trailerSubtypes: trailerSubTypes, + isLcvDesignated, + feature: FEATURE, + companyInfo, + isAmendAction: false, + createdDateTime, + updatedDateTime, + pastStartDateStatus, + companyLOAs: applicableLOAs, + revisionHistory: [], + onLeave: handleLeaveApplication, + onSave, + onCancel: undefined, + onContinue: handleSubmit(onContinue), + onSetDuration, + onSetExpiryDate, + onSetConditions, + onToggleSaveVehicle, + onSetVehicle, + onClearVehicle, + onUpdateLOAs, + }), [ + initialFormData, + currentFormData, + durationOptions, + vehicleOptions, + powerUnitSubTypes, + trailerSubTypes, + isLcvDesignated, + companyInfo, + createdDateTime, + updatedDateTime, + pastStartDateStatus, + applicableLOAs, + handleLeaveApplication, + onSave, + onContinue, + onSetDuration, + onSetExpiryDate, + onSetConditions, + onToggleSaveVehicle, + onSetVehicle, + onClearVehicle, + onUpdateLOAs, + ]); + return (
- + + + { const { - applicationData: applicationContextData, + applicationData, setApplicationData: setApplicationContextData, } = useContext(ApplicationContext); - const companyId = getDefaultRequiredVal(0, applicationContextData?.companyId); + const companyId = getDefaultRequiredVal(0, applicationData?.companyId); const { data: specialAuth } = useFetchSpecialAuthorizations(companyId); - const isLcvDesignated = Boolean(specialAuth?.isLcvAllowed); const isNoFeePermitType = Boolean(specialAuth?.noFeeType); const { data: companyInfo } = useCompanyInfoQuery(); const doingBusinessAs = companyInfo?.alternateName; - const applicationData = applyLCVToApplicationData( - applicationContextData, - isLcvDesignated, - ); - const fee = isNoFeePermitType ? "0" : `${calculateFeeByDuration( @@ -177,6 +170,7 @@ export const ApplicationReview = () => { } doingBusinessAs={doingBusinessAs} calculatedFee={fee} + loas={applicationData?.permitData?.loas} />
diff --git a/frontend/src/features/permits/pages/Application/components/form/ConditionsTable.scss b/frontend/src/features/permits/pages/Application/components/form/ConditionsTable.scss index 751c404d9..1761d3f56 100644 --- a/frontend/src/features/permits/pages/Application/components/form/ConditionsTable.scss +++ b/frontend/src/features/permits/pages/Application/components/form/ConditionsTable.scss @@ -19,6 +19,9 @@ } & &__cell { + padding-top: 0.5rem; + padding-bottom: 0.5rem; + &--checkbox { padding-left: 0; } @@ -30,7 +33,13 @@ } } - & &__checkbox#{&}__checkbox--disabled { - color: $disabled-colour; + & &__form-control#{&}__form-control--disabled { + .condition-checkbox { + color: $disabled-colour; + } + + .condition-description { + color: $bc-black; + } } } diff --git a/frontend/src/features/permits/pages/Application/components/form/ConditionsTable.tsx b/frontend/src/features/permits/pages/Application/components/form/ConditionsTable.tsx index a7dd946d6..3cb90f67d 100644 --- a/frontend/src/features/permits/pages/Application/components/form/ConditionsTable.tsx +++ b/frontend/src/features/permits/pages/Application/components/form/ConditionsTable.tsx @@ -73,15 +73,15 @@ export const ConditionsTable = ({ void; +}) => { + return ( + + + + + + LOA # + + + + Expiry Date + + + + + + {loas.map((selectableLOA) => ( + + + onSelectLOA?.(selectableLOA.loa.loaNumber)} + /> + } + key={selectableLOA.loa.loaNumber} + label={selectableLOA.loa.loaNumber} + classes={{ + root: "loa-table__form-control", + disabled: "loa-table__form-control loa-table__form-control--disabled", + }} + slotProps={{ + typography: { + className: "loa-number", + }, + }} + /> + + + + {applyWhenNotNullable( + expiryDate => toLocal(expiryDate, DATE_FORMATS.DATEONLY_SLASH), + selectableLOA.loa.expiryDate, + "Never expires", + )} + + + ))} + +
+
+ ); +}; diff --git a/frontend/src/features/permits/pages/Application/components/form/PermitForm.tsx b/frontend/src/features/permits/pages/Application/components/form/PermitForm.tsx index 11356c1b1..245f1315b 100644 --- a/frontend/src/features/permits/pages/Application/components/form/PermitForm.tsx +++ b/frontend/src/features/permits/pages/Application/components/form/PermitForm.tsx @@ -1,7 +1,4 @@ import { Box } from "@mui/material"; -import dayjs, { Dayjs } from "dayjs"; -import { useFormContext } from "react-hook-form"; -import { useEffect } from "react"; import "./PermitForm.scss"; import { FormActions } from "./FormActions"; @@ -9,170 +6,120 @@ import { ApplicationDetails } from "../../../../components/form/ApplicationDetai import { ContactDetails } from "../../../../components/form/ContactDetails"; import { PermitDetails } from "./PermitDetails"; import { VehicleDetails } from "./VehicleDetails/VehicleDetails"; -import { CompanyProfile } from "../../../../../manageProfile/types/manageProfile.d"; -import { Nullable } from "../../../../../../common/types/common"; -import { EMPTY_VEHICLE_DETAILS, PermitVehicleDetails } from "../../../../types/PermitVehicleDetails"; -import { PastStartDateStatus } from "../../../../../../common/components/form/subFormComponents/CustomDatePicker"; +import { PermitLOA } from "./PermitLOA"; +import { LOADetail } from "../../../../../settings/types/SpecialAuthorization"; import { isVehicleSubtypeLCV } from "../../../../../manageVehicles/helpers/vehicleSubtypes"; -import { PermitCondition } from "../../../../types/PermitCondition"; -import { LCV_CONDITION } from "../../../../constants/constants"; -import { sortConditions } from "../../../../helpers/conditions"; import { getStartOfDate } from "../../../../../../common/helpers/formatDate"; -import { getExpiryDate } from "../../../../helpers/permitState"; -import { - PowerUnit, - Trailer, - VehicleSubType, -} from "../../../../../manageVehicles/types/Vehicle"; +import { useApplicationFormContext } from "../../../../hooks/useApplicationFormContext"; +import { AmendReason } from "../../../Amend/components/form/AmendReason"; +import { AmendRevisionHistory } from "../../../Amend/components/form/AmendRevisionHistory"; + +export const PermitForm = () => { + const { + formData, + availableDurationOptions, + powerUnitSubtypes, + trailerSubtypes, + isLcvDesignated, + ineligiblePowerUnitSubtypes, + ineligibleTrailerSubtypes, + filteredVehicleOptions, + feature, + companyInfo, + isAmendAction, + createdDateTime, + updatedDateTime, + pastStartDateStatus, + companyLOAs, + revisionHistory, + onLeave, + onSave, + onCancel, + onContinue, + onSetConditions, + onToggleSaveVehicle, + onSetVehicle, + onClearVehicle, + onUpdateLOAs, + } = useApplicationFormContext(); + + const permitType = formData.permitType; + const applicationNumber = formData.applicationNumber; + const permitNumber = formData.permitNumber; + const startDate = getStartOfDate(formData.permitData.startDate); + const expiryDate = formData.permitData.expiryDate; + const permitConditions = formData.permitData.commodities; + const vehicleFormData = formData.permitData.vehicleDetails; + const currentSelectedLOAs = formData.permitData.loas as LOADetail[]; -import { - getIneligiblePowerUnitSubtypes, - getIneligibleTrailerSubtypes, -} from "../../../../helpers/permitVehicles"; - -interface PermitFormProps { - feature: string; - onLeave?: () => void; - onSave?: () => Promise; - onCancel?: () => void; - onContinue: () => Promise; - isAmendAction: boolean; - permitNumber?: Nullable; - createdDateTime?: Nullable; - updatedDateTime?: Nullable; - vehicleOptions: (PowerUnit | Trailer)[]; - powerUnitSubTypes: VehicleSubType[]; - trailerSubTypes: VehicleSubType[]; - children?: React.ReactNode; - companyInfo?: Nullable; - durationOptions: { - value: number; - label: string; - }[]; - doingBusinessAs?: Nullable; - pastStartDateStatus: PastStartDateStatus; - isLcvDesignated: boolean; -} - -export const PermitForm = (props: PermitFormProps) => { - const { watch, setValue } = useFormContext(); - - const permitType = watch("permitType"); - const applicationNumber = watch("applicationNumber"); - const permitStartDate = watch("permitData.startDate"); - const startDate = getStartOfDate(permitStartDate); - const permitDuration = watch("permitData.permitDuration"); - const permitConditions = watch("permitData.commodities"); - const vehicleFormData = watch("permitData.vehicleDetails"); - - const handleSetConditions = (conditions: PermitCondition[]) => { - setValue("permitData.commodities", [...conditions]); - }; - - const handleToggleSaveVehicle = (saveVehicle: boolean) => { - setValue("permitData.vehicleDetails.saveVehicle", saveVehicle); - }; - - const handleSetVehicle = (vehicleDetails: PermitVehicleDetails) => { - setValue("permitData.vehicleDetails", { - ...vehicleDetails, - }); - }; - - const handleClearVehicle = (saveVehicle: boolean) => { - setValue("permitData.vehicleDetails", { - ...EMPTY_VEHICLE_DETAILS, - saveVehicle, - }); - }; - - const handleSetExpiryDate = (expiry: Dayjs) => { - setValue("permitData.expiryDate", dayjs(expiry)); - }; - - const isLcvDesignated = props.isLcvDesignated; - const ineligiblePowerUnitSubtypes = getIneligiblePowerUnitSubtypes(permitType) - .filter(subtype => !isLcvDesignated || !isVehicleSubtypeLCV(subtype.typeCode)); - - // Permit expiry date === Permit start date + Permit duration - 1 - const expiryDate = getExpiryDate(startDate, permitDuration); - useEffect(() => { - handleSetExpiryDate(expiryDate); - }, [expiryDate]); - - const isAmendAction = props.isAmendAction; - - const vehicleSubtype = vehicleFormData.vehicleSubType; - useEffect(() => { - if ( - !isVehicleSubtypeLCV(vehicleSubtype) - && permitConditions.some(({ condition }: PermitCondition) => condition === LCV_CONDITION.condition) - ) { - // If vehicle subtype in the form isn't LCV but conditions have LCV, - // then remove that LCV condition from the form - handleSetConditions(permitConditions.filter( - ({ condition }: PermitCondition) => condition !== LCV_CONDITION.condition, - )); - } else if ( - isVehicleSubtypeLCV(vehicleSubtype) - && !permitConditions.some(({ condition }: PermitCondition) => condition === LCV_CONDITION.condition) - ) { - // If vehicle subtype in the form is LCV but conditions don't have LCV, - // then add that LCV condition into the form - handleSetConditions(sortConditions([...permitConditions, LCV_CONDITION])); - } - }, [vehicleSubtype, permitConditions]); - return ( - + + + - {props.children} + + {isAmendAction ? ( + <> + + + + ) : null} ); diff --git a/frontend/src/features/permits/pages/Application/components/form/PermitLOA.scss b/frontend/src/features/permits/pages/Application/components/form/PermitLOA.scss new file mode 100644 index 000000000..dea8fc6f2 --- /dev/null +++ b/frontend/src/features/permits/pages/Application/components/form/PermitLOA.scss @@ -0,0 +1,40 @@ +@use "../../../../../../themes/orbcStyles"; + +@include orbcStyles.permit-main-box-style(".permit-loa"); +@include orbcStyles.permit-left-box-style(".permit-loa__header"); +@include orbcStyles.permit-right-box-style(".permit-loa__body"); + +.permit-loa { + & &__header { + h3 { + padding-top: 1rem; + } + } + + .loa-title { + color: orbcStyles.$bc-black; + padding-top: 1rem; + + &__title { + font-weight: bold; + font-size: 1.25rem; + + &--optional { + font-weight: normal; + font-size: 1.25rem; + margin-left: 0.25rem; + } + } + } + + .loa-info { + margin-top: 1.5rem; + margin-bottom: 1.5rem; + width: calc(100% - 3rem); + + &__message { + font-size: 1rem; + font-weight: normal; + } + } +} diff --git a/frontend/src/features/permits/pages/Application/components/form/PermitLOA.tsx b/frontend/src/features/permits/pages/Application/components/form/PermitLOA.tsx new file mode 100644 index 000000000..04d79e251 --- /dev/null +++ b/frontend/src/features/permits/pages/Application/components/form/PermitLOA.tsx @@ -0,0 +1,104 @@ +import { useEffect, useMemo } from "react"; +import { Dayjs } from "dayjs"; +import { Box, Typography } from "@mui/material"; + +import "./PermitLOA.scss"; +import { InfoBcGovBanner } from "../../../../../../common/components/banners/InfoBcGovBanner"; +import { BANNER_MESSAGES } from "../../../../../../common/constants/bannerMessages"; +import { LOADetail } from "../../../../../settings/types/SpecialAuthorization"; +import { LOATable } from "./LOATable"; +import { PermitType } from "../../../../types/PermitType"; +import { getMinPermitExpiryDate } from "../../../../helpers/dateSelection"; +import { areArraysEqual } from "../../../../../../common/helpers/util"; +import { getUpdatedLOASelection } from "../../../../helpers/permitLOA"; + +export const PermitLOA = ({ + permitType, + startDate, + selectedLOAs, + companyLOAs, + onUpdateLOAs, +}: { + permitType: PermitType; + startDate: Dayjs; + selectedLOAs: LOADetail[]; + companyLOAs: LOADetail[]; + onUpdateLOAs: (updatedLOAs: LOADetail[]) => void, +}) => { + const minPermitExpiryDate = getMinPermitExpiryDate(permitType, startDate); + + // Only show the current active company LOAs as selectable LOAs + const loasForTable = useMemo(() => getUpdatedLOASelection( + companyLOAs, + selectedLOAs, + minPermitExpiryDate, + ), [ + companyLOAs, + selectedLOAs, + minPermitExpiryDate, + ]); + + // Since certain LOAs might have been removed from the table, we need to make sure + // that the selected LOAs in the permit form matches the selection state of the table + const selectedLOAsInTable = loasForTable + .filter(selectableLOA => selectableLOA.checked) + .map(selectableLOA => selectableLOA.loa); + + const selectedLOANumbers = selectedLOAs.map(loa => loa.loaNumber); + + useEffect(() => { + const selectedNumbersInTable = selectedLOAsInTable.map(loa => loa.loaNumber); + if (!areArraysEqual(selectedLOANumbers, selectedNumbersInTable)) { + onUpdateLOAs([...selectedLOAsInTable]); + } + }, [selectedLOANumbers, selectedLOAsInTable]); + + const handleSelectLOA = (loaNumber: number) => { + const loa = loasForTable.find(loaRow => loaRow.loa.loaNumber === loaNumber); + if (!loa || loa?.disabled) return; + + const isLOASelected = Boolean(loa?.checked); + if (isLOASelected) { + // Deselect the LOA + onUpdateLOAs( + selectedLOAs.filter(selectedLOA => selectedLOA.loaNumber !== loaNumber), + ); + } else { + // Select the LOA + const { loa: loaToSelect } = loa; + onUpdateLOAs([...selectedLOAs, loaToSelect]); + } + }; + + return ( + + + + Letter of Authorization (LOA) + + + + +
+ Select the relevant LOA(s) + (optional) +
+ + + {BANNER_MESSAGES.LOA_VEHICLE_CANNOT_BE_EDITED_IN_PERMIT} + + } + /> + + +
+
+ ); +}; diff --git a/frontend/src/features/permits/pages/Application/components/form/VehicleDetails/VehicleDetails.tsx b/frontend/src/features/permits/pages/Application/components/form/VehicleDetails/VehicleDetails.tsx index c9b3acba4..1e0667ece 100644 --- a/frontend/src/features/permits/pages/Application/components/form/VehicleDetails/VehicleDetails.tsx +++ b/frontend/src/features/permits/pages/Application/components/form/VehicleDetails/VehicleDetails.tsx @@ -25,6 +25,7 @@ import { SelectVehicleDropdown } from "./customFields/SelectVehicleDropdown"; import { BANNER_MESSAGES } from "../../../../../../../common/constants/bannerMessages"; import { PermitVehicleDetails } from "../../../../../types/PermitVehicleDetails"; import { EMPTY_VEHICLE_SUBTYPE } from "../../../../../../manageVehicles/helpers/vehicleSubtypes"; +import { LOADetail } from "../../../../../../settings/types/SpecialAuthorization"; import { PowerUnit, Trailer, @@ -82,6 +83,8 @@ const getEligibleSubtypeOptions = ( trailerSubtypes: VehicleSubType[], ineligiblePowerUnitSubtypes: VehicleSubType[], ineligibleTrailerSubtypes: VehicleSubType[], + allowedLOAPowerUnitSubtypes: string[], + allowedLOATrailerSubtypes: string[], vehicleType?: string, ) => { if ( @@ -102,6 +105,8 @@ const getEligibleSubtypeOptions = ( vehicleType, ineligiblePowerUnitSubtypes, ineligibleTrailerSubtypes, + allowedLOAPowerUnitSubtypes, + allowedLOATrailerSubtypes, ); }; @@ -113,6 +118,7 @@ export const VehicleDetails = ({ trailerSubtypes, ineligiblePowerUnitSubtypes, ineligibleTrailerSubtypes, + selectedLOAs, onSetSaveVehicle, onSetVehicle, onClearVehicle, @@ -124,6 +130,7 @@ export const VehicleDetails = ({ trailerSubtypes: VehicleSubType[]; ineligiblePowerUnitSubtypes: VehicleSubType[]; ineligibleTrailerSubtypes: VehicleSubType[]; + selectedLOAs: LOADetail[]; onSetSaveVehicle: (saveVehicle: boolean) => void; onSetVehicle: (vehicleDetails: PermitVehicleDetails) => void; onClearVehicle: (saveVehicle: boolean) => void; @@ -166,6 +173,50 @@ export const VehicleDetails = ({ EMPTY_VEHICLE_SUBTYPE, ]); + // Find vehicle subtypes that are allowed by LOAs + const permittedLOAPowerUnitIds = new Set([ + ...selectedLOAs.map(loa => loa.powerUnits) + .reduce((prevPowerUnits, currPowerUnits) => [ + ...prevPowerUnits, + ...currPowerUnits, + ], []), + ]); + + const permittedLOATrailerIds = new Set([ + ...selectedLOAs.map(loa => loa.trailers) + .reduce((prevTrailers, currTrailers) => [ + ...prevTrailers, + ...currTrailers, + ], []), + ]); + + const powerUnitsInInventory = vehicleOptions + .filter(vehicle => vehicle.vehicleType === VEHICLE_TYPES.POWER_UNIT) as PowerUnit[]; + + const trailersInInventory = vehicleOptions + .filter(vehicle => vehicle.vehicleType === VEHICLE_TYPES.TRAILER) as Trailer[]; + + const permittedLOAPowerUnitSubtypes = powerUnitsInInventory + .filter(powerUnit => permittedLOAPowerUnitIds.has(powerUnit.powerUnitId as string)) + .map(powerUnit => powerUnit.powerUnitTypeCode); + + const permittedLOATrailerSubtypes = trailersInInventory + .filter(trailer => permittedLOATrailerIds.has(trailer.trailerId as string)) + .map(trailer => trailer.trailerTypeCode); + + // Check if selected vehicle is an LOA vehicle + const isSelectedVehicleAllowedByLOA = Boolean(vehicleFormData.vehicleId) + && ( + permittedLOAPowerUnitIds.has(vehicleFormData.vehicleId as string) + || permittedLOATrailerIds.has(vehicleFormData.vehicleId as string) + ) + && ( + powerUnitsInInventory.map(powerUnit => powerUnit.powerUnitId) + .includes(vehicleFormData.vehicleId as string) + || trailersInInventory.map(trailer => trailer.trailerId) + .includes(vehicleFormData.vehicleId as string) + ); + useEffect(() => { // Update subtype options when vehicle type changes const subtypes = getEligibleSubtypeOptions( @@ -173,6 +224,8 @@ export const VehicleDetails = ({ trailerSubtypes, ineligiblePowerUnitSubtypes, ineligibleTrailerSubtypes, + permittedLOAPowerUnitSubtypes, + permittedLOATrailerSubtypes, vehicleType, ); setSubtypeOptions(subtypes); @@ -182,6 +235,8 @@ export const VehicleDetails = ({ ineligiblePowerUnitSubtypes, ineligibleTrailerSubtypes, vehicleType, + permittedLOAPowerUnitSubtypes, + permittedLOATrailerSubtypes, ]); // Set the "Save to Inventory" radio button to false on render @@ -257,6 +312,13 @@ export const VehicleDetails = ({ } }; + // If the selected vehicle is an LOA vehicle, it should not be edited/saved to inventory + useEffect(() => { + if (isSelectedVehicleAllowedByLOA) { + setSaveVehicle(false); + } + }, [isSelectedVehicleAllowedByLOA]); + return ( @@ -305,8 +367,9 @@ export const VehicleDetails = ({ vehicleOptions={vehicleOptions} handleClearVehicle={() => onClearVehicle(saveVehicle)} handleSelectVehicle={onSelectVehicle} - ineligiblePowerUnitSubtypes={ineligiblePowerUnitSubtypes} - ineligibleTrailerSubtypes={ineligibleTrailerSubtypes} + ineligiblePowerUnitSubtypes={ineligiblePowerUnitSubtypes.map(({ typeCode }) => typeCode)} + ineligibleTrailerSubtypes={ineligibleTrailerSubtypes.map(({ typeCode }) => typeCode)} + loas={selectedLOAs} /> @@ -324,6 +387,8 @@ export const VehicleDetails = ({ width: formFieldStyle.width, customHelperText: "last 6 digits", }} + readOnly={isSelectedVehicleAllowedByLOA} + disabled={isSelectedVehicleAllowedByLOA} /> ))} + readOnly={isSelectedVehicleAllowedByLOA} + disabled={isSelectedVehicleAllowedByLOA} /> @@ -459,6 +534,8 @@ export const VehicleDetails = ({ "data-testid": "save-vehicle-yes", } as CustomInputHTMLAttributes } + readOnly={isSelectedVehicleAllowedByLOA} + disabled={isSelectedVehicleAllowedByLOA} /> } label="Yes" @@ -473,6 +550,8 @@ export const VehicleDetails = ({ "data-testid": "save-vehicle-no", } as CustomInputHTMLAttributes } + readOnly={isSelectedVehicleAllowedByLOA} + disabled={isSelectedVehicleAllowedByLOA} /> } label="No" diff --git a/frontend/src/features/permits/pages/Application/components/form/VehicleDetails/customFields/SelectVehicleDropdown.tsx b/frontend/src/features/permits/pages/Application/components/form/VehicleDetails/customFields/SelectVehicleDropdown.tsx index 06f7b96cc..4505eefc4 100644 --- a/frontend/src/features/permits/pages/Application/components/form/VehicleDetails/customFields/SelectVehicleDropdown.tsx +++ b/frontend/src/features/permits/pages/Application/components/form/VehicleDetails/customFields/SelectVehicleDropdown.tsx @@ -17,13 +17,12 @@ import { VEHICLE_CHOOSE_FROM } from "../../../../../../constants/constants"; import { EMPTY_VEHICLE_UNIT_NUMBER } from "../../../../../../../../common/constants/constants"; import { Nullable } from "../../../../../../../../common/types/common"; import { PermitVehicleDetails } from "../../../../../../types/PermitVehicleDetails"; - +import { LOADetail } from "../../../../../../../settings/types/SpecialAuthorization"; import { PowerUnit, Trailer, VEHICLE_TYPES, Vehicle, - VehicleSubType, } from "../../../../../../../manageVehicles/types/Vehicle"; const GroupHeader = styled("div")(({ theme }) => ({ @@ -53,6 +52,7 @@ export const SelectVehicleDropdown = ({ handleClearVehicle, ineligiblePowerUnitSubtypes, ineligibleTrailerSubtypes, + loas, }: { chooseFrom: string; selectedVehicle: Nullable; @@ -60,8 +60,9 @@ export const SelectVehicleDropdown = ({ vehicleOptions: Vehicle[]; handleSelectVehicle: (vehicle: Vehicle) => void; handleClearVehicle: () => void; - ineligiblePowerUnitSubtypes: VehicleSubType[]; - ineligibleTrailerSubtypes: VehicleSubType[]; + ineligiblePowerUnitSubtypes: string[]; + ineligibleTrailerSubtypes: string[]; + loas: LOADetail[]; }) => { const sortedVehicles = sortVehicles(chooseFrom, vehicleOptions); @@ -69,6 +70,7 @@ export const SelectVehicleDropdown = ({ sortedVehicles, ineligiblePowerUnitSubtypes, ineligibleTrailerSubtypes, + loas, ); const selectedOption = selectedVehicle diff --git a/frontend/src/features/permits/pages/Application/components/form/tests/helpers/prepare.tsx b/frontend/src/features/permits/pages/Application/components/form/tests/helpers/prepare.tsx index 700da08c0..6abb845f1 100644 --- a/frontend/src/features/permits/pages/Application/components/form/tests/helpers/prepare.tsx +++ b/frontend/src/features/permits/pages/Application/components/form/tests/helpers/prepare.tsx @@ -61,6 +61,7 @@ const TestFormWrapper = (props: React.PropsWithChildren) => { permitDuration: defaultDuration, expiryDate: getExpiryDate(currentDt, defaultDuration), commodities: [], + loas: [], }, }, reValidateMode: "onBlur", diff --git a/frontend/src/features/permits/pages/Application/components/pay/CVPayInPersonInfo.scss b/frontend/src/features/permits/pages/Application/components/pay/CVPayInPersonInfo.scss index 575c02b8c..b33423f60 100644 --- a/frontend/src/features/permits/pages/Application/components/pay/CVPayInPersonInfo.scss +++ b/frontend/src/features/permits/pages/Application/components/pay/CVPayInPersonInfo.scss @@ -1,7 +1,7 @@ @import "../../../../../../themes/orbcStyles"; .cv-pay-in-person-info { - padding: 1.5rem 0; + padding: 0; &__heading { color: $bc-black; diff --git a/frontend/src/features/permits/pages/Application/components/pay/ChoosePaymentMethod.tsx b/frontend/src/features/permits/pages/Application/components/pay/ChoosePaymentMethod.tsx index d624a74ff..b5587df1d 100644 --- a/frontend/src/features/permits/pages/Application/components/pay/ChoosePaymentMethod.tsx +++ b/frontend/src/features/permits/pages/Application/components/pay/ChoosePaymentMethod.tsx @@ -1,17 +1,21 @@ import { Controller, useFormContext } from "react-hook-form"; import { Box, RadioGroup, Typography } from "@mui/material"; + +import "./ChoosePaymentMethod.scss"; +import { CVPayInPersonInfo } from "./CVPayInPersonInfo"; import { PaymentOption } from "./PaymentOption"; import { PaymentMethodTypeCode } from "../../../../../../common/types/paymentMethods"; import { DEFAULT_EMPTY_CARD_TYPE, DEFAULT_EMPTY_PAYMENT_TYPE, } from "./types/PaymentMethodData"; -import "./ChoosePaymentMethod.scss"; export const ChoosePaymentMethod = ({ availablePaymentMethods, + showPayInPersonInfo, }: { availablePaymentMethods: PaymentMethodTypeCode[]; + showPayInPersonInfo: boolean; }) => { const { control, watch, setValue, clearErrors } = useFormContext(); const currPaymentMethod = watch("paymentMethod"); @@ -67,6 +71,10 @@ export const ChoosePaymentMethod = ({ handlePaymentMethodChange={handlePaymentMethodChange} /> ))} + + {showPayInPersonInfo ? ( + + ) : null} )} /> diff --git a/frontend/src/features/permits/pages/Application/components/review/PermitReview.tsx b/frontend/src/features/permits/pages/Application/components/review/PermitReview.tsx index c4c9fbfc8..498be851a 100644 --- a/frontend/src/features/permits/pages/Application/components/review/PermitReview.tsx +++ b/frontend/src/features/permits/pages/Application/components/review/PermitReview.tsx @@ -17,6 +17,8 @@ import { PermitContactDetails } from "../../../../types/PermitContactDetails"; import { PermitVehicleDetails } from "../../../../types/PermitVehicleDetails"; import { Application } from "../../../../types/application"; import { PermitCondition } from "../../../../types/PermitCondition"; +import { ReviewPermitLOAs } from "./ReviewPermitLOAs"; +import { LOADetail } from "../../../../../settings/types/SpecialAuthorization"; import { PERMIT_REVIEW_CONTEXTS, PermitReviewContext, @@ -56,6 +58,7 @@ interface PermitReviewProps { oldFields?: Nullable>; calculatedFee: string; doingBusinessAs?: Nullable; + loas?: Nullable; } export const PermitReview = (props: PermitReviewProps) => { @@ -83,6 +86,10 @@ export const PermitReview = (props: PermitReviewProps) => { oldFields={props.oldFields?.permitData?.contactDetails} /> + + - + Description - + + Conditions + {reviewConditions.map((row: PermitCondition) => { return ( @@ -42,7 +48,11 @@ export const ReviewConditionsTable = ({ key={row.condition} data-testid="review-permit-condition" > - + - + ; +}) => { + return loas && loas.length > 0 ? ( + + + + Letter of Authorization (LOA) + + + + + + + Selected LOA(s) + + + ({ + loa, + checked: true, + disabled: true, + }))} + /> + + + + ) : null; +}; diff --git a/frontend/src/features/permits/pages/Application/tests/helpers/ApplicationReview/prepare.tsx b/frontend/src/features/permits/pages/Application/tests/helpers/ApplicationReview/prepare.tsx index b3b5b3f76..63f36255d 100644 --- a/frontend/src/features/permits/pages/Application/tests/helpers/ApplicationReview/prepare.tsx +++ b/frontend/src/features/permits/pages/Application/tests/helpers/ApplicationReview/prepare.tsx @@ -49,6 +49,7 @@ export const defaultApplicationData = { startDate: getStartOfDate(toLocalDayjs(permitData.startDate)), expiryDate: getEndOfDate(toLocalDayjs(permitData.expiryDate)), }, + permitStatus: PERMIT_STATUSES.IN_PROGRESS, } as Application; export const companyInfo = getDefaultCompanyInfo(); diff --git a/frontend/src/features/permits/pages/ShoppingCart/ShoppingCartPage.scss b/frontend/src/features/permits/pages/ShoppingCart/ShoppingCartPage.scss index 519f18cd9..b0d208663 100644 --- a/frontend/src/features/permits/pages/ShoppingCart/ShoppingCartPage.scss +++ b/frontend/src/features/permits/pages/ShoppingCart/ShoppingCartPage.scss @@ -20,7 +20,7 @@ & &__info { border-bottom: 1px solid $bc-border-grey; margin-bottom: 1.5rem; - padding: 1.5rem 0; + padding: 0 0 1.5rem 0; .info { &__body { diff --git a/frontend/src/features/permits/pages/ShoppingCart/ShoppingCartPage.tsx b/frontend/src/features/permits/pages/ShoppingCart/ShoppingCartPage.tsx index 229e2ee1d..2cc8aac35 100644 --- a/frontend/src/features/permits/pages/ShoppingCart/ShoppingCartPage.tsx +++ b/frontend/src/features/permits/pages/ShoppingCart/ShoppingCartPage.tsx @@ -21,7 +21,6 @@ import { EditCartItemDialog } from "../../components/cart/EditCartItemDialog"; import { UpdateCartDialog } from "../../components/cart/UpdateCartDialog"; import { BCeID_USER_ROLE } from "../../../../common/authentication/types"; import { Loading } from "../../../../common/pages/Loading"; -import { CVPayInPersonInfo } from "../Application/components/pay/CVPayInPersonInfo"; import { PAYMENT_METHOD_TYPE_CODE, PaymentCardTypeCode, @@ -391,6 +390,7 @@ export const ShoppingCartPage = () => { {PPC_EMAIL}

+ { - - {!isStaffActingAsCompany && } + {!isFeeZero ? ( + + ) : null} {paymentFailed ? : null} diff --git a/frontend/src/features/permits/types/PermitData.ts b/frontend/src/features/permits/types/PermitData.ts index 381d12e89..c33774c62 100644 --- a/frontend/src/features/permits/types/PermitData.ts +++ b/frontend/src/features/permits/types/PermitData.ts @@ -4,6 +4,7 @@ import { Nullable } from "../../../common/types/common"; import { PermitContactDetails } from "./PermitContactDetails"; import { PermitVehicleDetails } from "./PermitVehicleDetails"; import { PermitMailingAddress } from "./PermitMailingAddress"; +import { LOADetail } from "../../settings/types/SpecialAuthorization"; import { PermitCondition } from "./PermitCondition"; export interface PermitData { @@ -18,4 +19,5 @@ export interface PermitData { companyName?: Nullable; doingBusinessAs?: Nullable; clientNumber?: Nullable; + loas?: Nullable; } diff --git a/frontend/src/features/permits/types/PermitType.ts b/frontend/src/features/permits/types/PermitType.ts index df4fc1046..1d2bb9341 100644 --- a/frontend/src/features/permits/types/PermitType.ts +++ b/frontend/src/features/permits/types/PermitType.ts @@ -116,3 +116,13 @@ export const permitTypeDisplayText = (permitType?: Nullable) => { export const isPermitTypeValid = (permitType?: Nullable) => { return permitType && (Object.values(PERMIT_TYPES) as string[]).includes(permitType.toUpperCase()); }; + +/** + * Determine whether or not a permit type is considered a term permit. + * @param permitType Type of permit + * @returns Whether or not the permit of that type is considered a term permit + */ +export const isTermPermitType = (permitType: PermitType) => { + return permitType === PERMIT_TYPES.TROS + || permitType === PERMIT_TYPES.TROW; +}; diff --git a/frontend/src/features/settings/apiManager/endpoints/endpoints.ts b/frontend/src/features/settings/apiManager/endpoints/endpoints.ts index 5da008e7c..509fafb76 100644 --- a/frontend/src/features/settings/apiManager/endpoints/endpoints.ts +++ b/frontend/src/features/settings/apiManager/endpoints/endpoints.ts @@ -20,18 +20,18 @@ export const SPECIAL_AUTH_API_ROUTES = { }, LOA: { ALL: (companyId: number | string, expired: boolean) => - `${SPECIAL_AUTH_API_BASE}/${companyId}/loas${expired ? "?expired=true" : ""}`, - DETAIL: (companyId: number | string, loaId: string) => + `${SPECIAL_AUTH_API_BASE}/${companyId}/loas?expired=${expired}`, + DETAIL: (companyId: number | string, loaId: number) => `${SPECIAL_AUTH_API_BASE}/${companyId}/loas/${loaId}`, CREATE: (companyId: number | string) => `${SPECIAL_AUTH_API_BASE}/${companyId}/loas`, - UPDATE: (companyId: number | string, loaId: string) => + UPDATE: (companyId: number | string, loaId: number) => `${SPECIAL_AUTH_API_BASE}/${companyId}/loas/${loaId}`, - REMOVE: (companyId: number | string, loaId: string) => + REMOVE: (companyId: number | string, loaId: number) => `${SPECIAL_AUTH_API_BASE}/${companyId}/loas/${loaId}`, - DOWNLOAD: (companyId: number | string, loaId: string) => + DOWNLOAD: (companyId: number | string, loaId: number) => `${SPECIAL_AUTH_API_BASE}/${companyId}/loas/${loaId}/documents?download=proxy`, - REMOVE_DOCUMENT: (companyId: number | string, loaId: string) => + REMOVE_DOCUMENT: (companyId: number | string, loaId: number) => `${SPECIAL_AUTH_API_BASE}/${companyId}/loas/${loaId}/documents`, }, }; diff --git a/frontend/src/features/settings/apiManager/specialAuthorization.ts b/frontend/src/features/settings/apiManager/specialAuthorization.ts index 689054947..7448be6ca 100644 --- a/frontend/src/features/settings/apiManager/specialAuthorization.ts +++ b/frontend/src/features/settings/apiManager/specialAuthorization.ts @@ -4,6 +4,7 @@ import { LOADetail, NoFeePermitType, SpecialAuthorizationData } from "../types/S import { LOAFormData, serializeLOAFormData } from "../types/LOAFormData"; import { SPECIAL_AUTH_API_ROUTES } from "./endpoints/endpoints"; import { streamDownloadFile } from "../../../common/helpers/util"; +import { RequiredOrNull } from "../../../common/types/common"; import { httpDELETERequest, httpGETRequest, @@ -12,7 +13,6 @@ import { httpPUTRequest, httpPUTRequestWithFile, } from "../../../common/apiManager/httpRequestHandler"; -import { RequiredOrNull } from "../../../common/types/common"; /** * Get the LOAs for a given company. @@ -38,7 +38,7 @@ export const getLOAs = async ( */ export const getLOADetail = async ( companyId: number | string, - loaId: string, + loaId: number, ): Promise => { const response = await httpGETRequest( SPECIAL_AUTH_API_ROUTES.LOA.DETAIL(companyId, loaId), @@ -72,7 +72,7 @@ export const createLOA = async ( export const updateLOA = async ( LOAData: { companyId: number | string; - loaId: string; + loaId: number; data: LOAFormData; }, ): Promise> => { @@ -91,7 +91,7 @@ export const updateLOA = async ( export const removeLOA = async ( LOAData: { companyId: number | string; - loaId: string; + loaId: number; }, ): Promise> => { const { companyId, loaId } = LOAData; @@ -107,7 +107,7 @@ export const removeLOA = async ( * @returns A Promise containing the dms reference string for the LOA download stream */ export const downloadLOA = async ( - loaId: string, + loaId: number, companyId: string | number, ) => { const url = SPECIAL_AUTH_API_ROUTES.LOA.DOWNLOAD(companyId, loaId); @@ -123,7 +123,7 @@ export const downloadLOA = async ( export const removeLOADocument = async ( LOAData: { companyId: number | string; - loaId: string; + loaId: number; }, ): Promise> => { const { companyId, loaId } = LOAData; diff --git a/frontend/src/features/settings/components/SpecialAuthorizations/LOA/expired/ExpiredLOAModal.tsx b/frontend/src/features/settings/components/SpecialAuthorizations/LOA/expired/ExpiredLOAModal.tsx index 8d90836ea..06ec26564 100644 --- a/frontend/src/features/settings/components/SpecialAuthorizations/LOA/expired/ExpiredLOAModal.tsx +++ b/frontend/src/features/settings/components/SpecialAuthorizations/LOA/expired/ExpiredLOAModal.tsx @@ -17,8 +17,8 @@ export const ExpiredLOAModal = ({ showModal: boolean; allowEditLOA: boolean; handleCancel: () => void; - handleEdit: (loaId: string) => void; - handleDownload: (loaId: string) => void; + handleEdit: (loaId: number) => void; + handleDownload: (loaId: number) => void; expiredLOAs: LOADetail[]; }) => { return ( diff --git a/frontend/src/features/settings/components/SpecialAuthorizations/LOA/list/LOADownloadCell.tsx b/frontend/src/features/settings/components/SpecialAuthorizations/LOA/list/LOADownloadCell.tsx index 386b6215f..666f7a19c 100644 --- a/frontend/src/features/settings/components/SpecialAuthorizations/LOA/list/LOADownloadCell.tsx +++ b/frontend/src/features/settings/components/SpecialAuthorizations/LOA/list/LOADownloadCell.tsx @@ -7,18 +7,17 @@ export const LOADownloadCell = ({ onDownload, props: { row }, }: { - onDownload: (loaId: string) => void; + onDownload: (loaId: number) => void; props: { row: MRT_Row; }; }) => { - const loaId = `${row.original.loaId}`; const loaHasDocument = Boolean(row.original.documentId); return loaHasDocument ? ( onDownload(loaId)} + onClick={() => onDownload(row.original.loaId)} > Download Letter diff --git a/frontend/src/features/settings/components/SpecialAuthorizations/LOA/list/LOAList.tsx b/frontend/src/features/settings/components/SpecialAuthorizations/LOA/list/LOAList.tsx index 0383c1db8..69d3910e8 100644 --- a/frontend/src/features/settings/components/SpecialAuthorizations/LOA/list/LOAList.tsx +++ b/frontend/src/features/settings/components/SpecialAuthorizations/LOA/list/LOAList.tsx @@ -28,11 +28,11 @@ export const LOAList = ({ loas: LOADetail[]; isActive: boolean; allowEditLOA: boolean; - onEdit: (loaId: string) => void; - onDelete?: (loaId: string) => void; - onDownload: (loaId: string) => void; + onEdit: (loaId: number) => void; + onDelete?: (loaId: number) => void; + onDownload: (loaId: number) => void; }) => { - const handleEditLOA = (loaId: string) => { + const handleEditLOA = (loaId: number) => { if (!allowEditLOA) return; onEdit(loaId); }; diff --git a/frontend/src/features/settings/components/SpecialAuthorizations/LOA/list/LOAListColumnDef.tsx b/frontend/src/features/settings/components/SpecialAuthorizations/LOA/list/LOAListColumnDef.tsx index a365964ad..bec21ad11 100644 --- a/frontend/src/features/settings/components/SpecialAuthorizations/LOA/list/LOAListColumnDef.tsx +++ b/frontend/src/features/settings/components/SpecialAuthorizations/LOA/list/LOAListColumnDef.tsx @@ -8,8 +8,8 @@ import { LOADownloadCell } from "./LOADownloadCell"; export const LOAListColumnDef = ( allowEditLOA: boolean, - onEditLOA: (loaId: string) => void, - onDownload: (loaId: string) => void, + onEditLOA: (loaId: number) => void, + onDownload: (loaId: number) => void, ): MRT_ColumnDef[] => [ { Cell: ( diff --git a/frontend/src/features/settings/components/SpecialAuthorizations/LOA/list/LOANumberCell.tsx b/frontend/src/features/settings/components/SpecialAuthorizations/LOA/list/LOANumberCell.tsx index b8184d0e6..4301b5c81 100644 --- a/frontend/src/features/settings/components/SpecialAuthorizations/LOA/list/LOANumberCell.tsx +++ b/frontend/src/features/settings/components/SpecialAuthorizations/LOA/list/LOANumberCell.tsx @@ -9,21 +9,19 @@ export const LOANumberCell = ({ props: { row }, }: { allowEditLOA: boolean; - onEditLOA: (loaId: string) => void; + onEditLOA: (loaId: number) => void; props: { row: MRT_Row; }; }) => { - const loaId = `${row.original.loaId}`; - const loaNumber = `${row.original.loaNumber}`; return allowEditLOA ? ( onEditLOA(loaId)} + onClick={() => onEditLOA(row.original.loaId)} > - {loaNumber} + {row.original.loaNumber} ) : ( - <>{loaNumber} + <>{row.original.loaNumber} ); }; diff --git a/frontend/src/features/settings/hooks/LOA.ts b/frontend/src/features/settings/hooks/LOA.ts index 75749bde1..bb3d8cb6e 100644 --- a/frontend/src/features/settings/hooks/LOA.ts +++ b/frontend/src/features/settings/hooks/LOA.ts @@ -12,7 +12,7 @@ import { const QUERY_KEYS = { LOAS: (expired: boolean) => ["loas", expired], - LOA: (loaId?: Nullable) => ["loa", loaId], + LOA: (loaId?: Nullable) => ["loa", loaId], }; /** @@ -37,7 +37,7 @@ export const useFetchLOAs = (companyId: number | string, expired: boolean) => { * @param loaId id of the LOA to fetch * @returns Query result of the LOA details */ -export const useFetchLOADetail = (companyId: number, loaId?: Nullable) => { +export const useFetchLOADetail = (companyId: number, loaId?: Nullable) => { return useQuery({ queryKey: QUERY_KEYS.LOA(loaId), queryFn: () => { diff --git a/frontend/src/features/settings/pages/SpecialAuthorizations/LOA/LOASteps.tsx b/frontend/src/features/settings/pages/SpecialAuthorizations/LOA/LOASteps.tsx index 13d721c62..b2205fd39 100644 --- a/frontend/src/features/settings/pages/SpecialAuthorizations/LOA/LOASteps.tsx +++ b/frontend/src/features/settings/pages/SpecialAuthorizations/LOA/LOASteps.tsx @@ -26,7 +26,7 @@ export const LOASteps = ({ companyId, onExit, }: { - loaId?: Nullable; + loaId?: Nullable; companyId: number; onExit: () => void; }) => { diff --git a/frontend/src/features/settings/pages/SpecialAuthorizations/SpecialAuthorizations.tsx b/frontend/src/features/settings/pages/SpecialAuthorizations/SpecialAuthorizations.tsx index b3b53a0b2..410938068 100644 --- a/frontend/src/features/settings/pages/SpecialAuthorizations/SpecialAuthorizations.tsx +++ b/frontend/src/features/settings/pages/SpecialAuthorizations/SpecialAuthorizations.tsx @@ -46,9 +46,9 @@ export const SpecialAuthorizations = ({ const isLcvAllowed = getDefaultRequiredVal(false, specialAuthorizations?.isLcvAllowed); const [showExpiredLOAs, setShowExpiredLOAs] = useState(false); - const [loaToDelete, setLoaToDelete] = useState>(null); + const [loaToDelete, setLoaToDelete] = useState>(null); const [showLOASteps, setShowLOASteps] = useState(false); - const [loaToEdit, setLoaToEdit] = useState>(null); + const [loaToEdit, setLoaToEdit] = useState>(null); const { userClaims, @@ -143,7 +143,7 @@ export const SpecialAuthorizations = ({ setLoaToEdit(null); }; - const handleEditLOA = (loaId: string) => { + const handleEditLOA = (loaId: number) => { if (!canWriteLOA) return; setShowLOASteps(true); setLoaToEdit(loaId); @@ -156,7 +156,7 @@ export const SpecialAuthorizations = ({ expiredLOAsQuery.refetch(); }; - const handleOpenDeleteModal = (loaId: string) => { + const handleOpenDeleteModal = (loaId: number) => { if (!canWriteLOA) return; setLoaToDelete(loaId); }; @@ -165,7 +165,7 @@ export const SpecialAuthorizations = ({ setLoaToDelete(null); }; - const handleDeleteLOA = async (loaId: string) => { + const handleDeleteLOA = async (loaId: number) => { try { if (canWriteLOA) { await removeLOAMutation.mutateAsync({ @@ -182,7 +182,7 @@ export const SpecialAuthorizations = ({ } }; - const handleDownloadLOA = async (loaId: string) => { + const handleDownloadLOA = async (loaId: number) => { if (loaId && canReadLOA) { try { const { blobObj: blobObjWithoutType } = await downloadLOA( diff --git a/frontend/src/features/settings/types/SpecialAuthorization.ts b/frontend/src/features/settings/types/SpecialAuthorization.ts index 7766de8e7..4392c906c 100644 --- a/frontend/src/features/settings/types/SpecialAuthorization.ts +++ b/frontend/src/features/settings/types/SpecialAuthorization.ts @@ -1,3 +1,4 @@ +import { areArraysEqual } from "../../../common/helpers/util"; import { Nullable, RequiredOrNull } from "../../../common/types/common"; import { PermitType } from "../../permits/types/PermitType"; @@ -29,8 +30,8 @@ export const noFeePermitTypeDescription = (noFeePermitType: NoFeePermitType) => }; export interface LOADetail { - loaId: string; - loaNumber: string; + loaId: number; + loaNumber: number; companyId: number; startDate: string; expiryDate?: Nullable; @@ -40,6 +41,8 @@ export interface LOADetail { comment?: Nullable; powerUnits: string[]; trailers: string[]; + originalLoaId: number; + previousLoaId?: Nullable; } export interface CreateLOARequestData { @@ -62,6 +65,33 @@ export interface UpdateLOARequestData { trailers: string[]; } +/** + * Determine whether or not two LOAs have the same details. + * @param loa1 First LOA + * @param loa2 Second LOA + * @returns Whether or not the two LOAs have the same details + */ +export const areLOADetailsEqual = ( + loa1?: Nullable, + loa2?: Nullable, +) => { + if (!loa1 && !loa2) return true; + if (!loa1 || !loa2) return false; + + return loa1.loaId === loa2.loaId + && loa1.loaNumber === loa2.loaNumber + && loa1.companyId === loa2.companyId + && loa1.startDate === loa2.startDate + && loa1.expiryDate === loa2.expiryDate + && loa1.documentId === loa2.documentId + && loa1.fileName === loa2.fileName + && areArraysEqual(loa1.loaPermitType, loa2.loaPermitType) + && loa1.comment === loa2.comment + && areArraysEqual(loa1.powerUnits, loa2.powerUnits) + && areArraysEqual(loa1.trailers, loa2.trailers) + && loa1.originalLoaId === loa2.originalLoaId + && loa1.previousLoaId === loa2.previousLoaId; +}; export interface SpecialAuthorizationData { companyId: number; specialAuthId: number; diff --git a/frontend/src/features/wizard/subcomponents/ClientAndPermitReferenceInfoBox.tsx b/frontend/src/features/wizard/subcomponents/ClientAndPermitReferenceInfoBox.tsx index 4044f9ae3..b65330939 100644 --- a/frontend/src/features/wizard/subcomponents/ClientAndPermitReferenceInfoBox.tsx +++ b/frontend/src/features/wizard/subcomponents/ClientAndPermitReferenceInfoBox.tsx @@ -2,6 +2,7 @@ import { faCircleInfo } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Card, CardContent, CardMedia, Stack } from "@mui/material"; import { BC_COLOURS } from "../../../themes/bcGovStyles"; +import { PPC_EMAIL, TOLL_FREE_NUMBER } from "../../../common/constants/constants"; /** * React component to display an info box about how to locate @@ -36,8 +37,8 @@ export const ClientAndPermitReferenceInfoBox = () => { If you need further assistance, please contact the
Provincial Permit Centre at{" "} - Toll-free: 1-800-559-9688 or
{" "} - Email: ppcpermit@gov.bc.ca + Toll-free: {TOLL_FREE_NUMBER} or
{" "} + Email: {PPC_EMAIL} , + @InjectRepository(PermitLoa) + private permitLoaRepository: Repository, + @InjectRepository(LoaDetail) + private loaDetail: Repository, private dataSource: DataSource, private readonly dopsService: DopsService, private readonly paymentService: PaymentService, @@ -1198,4 +1212,83 @@ export class ApplicationService { return result; } + @LogAsyncMethodExecution() + async createPermitLoa( + currentUser: IUserJWT, + permitId: string, + createPermitLoaDto: CreatePermitLoaDto, + ): Promise { + const { loaIds: inputLoaIds } = createPermitLoaDto; + const existingPermitLoa = await this.findAllPermitLoa(permitId); + const permit = await this.findOne(permitId); + const existingLoaIds = existingPermitLoa.map((x) => x.loaId); + const loaIdsToDelete = existingLoaIds.filter( + (value) => !inputLoaIds.includes(value), + ); + const loaIdsToInsert = inputLoaIds.filter( + (value) => !existingLoaIds.includes(value), + ); + + if (loaIdsToInsert.length) { + const loaDetails = await this.loaDetail.find({ + where: { + loaId: In(loaIdsToInsert), + company: { companyId: permit.company.companyId }, + }, + }); + if(loaDetails.length != loaIdsToInsert.length) + throw new BadRequestException('One or more loa(s) does not exist') + // Transform the permit LOA IDs from an array of numbers into individual records. + const singlePermitLoa = loaIdsToInsert.map((loaId) => ({ + permitId, + loaIds: [loaId], + })); + + const permitLoas = await this.classMapper.mapArrayAsync( + singlePermitLoa, + CreatePermitLoaDto, + PermitLoa, + { + extraArgs: () => ({ + permitId, + userName: currentUser.userName, + userGUID: currentUser.userGUID, + timestamp: new Date(), + directory: currentUser.orbcUserDirectory, + }), + }, + ); + + // Save new PermitLoas in bulk + await this.permitLoaRepository.save(permitLoas); + } + + // Delete old PermitLoas in a single query + if (loaIdsToDelete?.length) + await this.permitLoaRepository.delete({ + permitId: permitId, + loa: { loaId: In(loaIdsToDelete) }, + }); + return await this.findAllPermitLoa(permitId); + } + @LogAsyncMethodExecution() + async findAllPermitLoa(permitId: string): Promise { + const savedPermitLoa = await this.permitLoaRepository + .createQueryBuilder('permitLoa') + .innerJoinAndSelect('permitLoa.loa', 'loa') + .innerJoinAndSelect('loa.company', 'company') + .innerJoinAndSelect('loa.loaVehicles', 'loaVehicles') + .innerJoinAndSelect('loa.loaPermitTypes', 'loaPermitTypes') + .where('permitLoa.permitId = :permitId', { + permitId: permitId, + }) + .getMany(); + const readPermitLoaDto: ReadPermitLoaDto[] = + await this.classMapper.mapArrayAsync( + savedPermitLoa, + PermitLoa, + ReadPermitLoaDto, + ); + return readPermitLoaDto; + } } diff --git a/vehicles/src/modules/permit-application-payment/application/company-application.controller.ts b/vehicles/src/modules/permit-application-payment/application/company-application.controller.ts index 0e674868b..56610a8f2 100644 --- a/vehicles/src/modules/permit-application-payment/application/company-application.controller.ts +++ b/vehicles/src/modules/permit-application-payment/application/company-application.controller.ts @@ -52,6 +52,9 @@ import { ApplicationQueueStatus, convertApplicationQueueStatus, } from '../../../common/enum/case-status-type.enum'; +import { ApplicationIdIdPathParamDto } from './dto/request/pathParam/applicationId.path-params.dto'; +import { CreatePermitLoaDto } from './dto/request/create-permit-loa.dto'; +import { ReadPermitLoaDto } from './dto/response/read-permit-loa.dto'; @ApiBearerAuth() @ApiTags('Company Application') @@ -367,4 +370,32 @@ export class CompanyApplicationController { } return deleteResult; } + @ApiOperation({ + summary: 'Designate LoA to permit.', + description: + 'Designate LoA to permit. Returns the created permit LoA object from the database.', + }) + @ApiCreatedResponse({ + description: 'Permit Loa Details', + type: ReadPermitLoaDto, + isArray: true, + }) + @Permissions({ + allowedBCeIDRoles: CLIENT_USER_ROLE_LIST, + allowedIdirRoles: IDIR_USER_ROLE_LIST, + }) + @Post(':applicationId/loas') + async createPermitLoa( + @Req() request: Request, + @Param() { applicationId }: ApplicationIdIdPathParamDto, + @Body() createPermitLoaDto: CreatePermitLoaDto, + ): Promise { + const currentUser = request.user as IUserJWT; + const result = await this.applicationService.createPermitLoa( + currentUser, + applicationId, + createPermitLoaDto, + ); + return result; + } } diff --git a/vehicles/src/modules/permit-application-payment/application/dto/request/create-permit-loa.dto.ts b/vehicles/src/modules/permit-application-payment/application/dto/request/create-permit-loa.dto.ts new file mode 100644 index 000000000..fbecc9fdf --- /dev/null +++ b/vehicles/src/modules/permit-application-payment/application/dto/request/create-permit-loa.dto.ts @@ -0,0 +1,14 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { ArrayMinSize, IsInt, IsPositive } from 'class-validator'; + +export class CreatePermitLoaDto { + @ApiProperty({ + description: 'Loa Ids to be assigned to the permit.', + isArray: true, + example: [1], + }) + @IsInt({ each: true }) + @IsPositive({ each: true }) + @ArrayMinSize(1) + loaIds: number[]; +} diff --git a/vehicles/src/modules/permit-application-payment/application/dto/response/read-permit-loa.dto.ts b/vehicles/src/modules/permit-application-payment/application/dto/response/read-permit-loa.dto.ts new file mode 100644 index 000000000..c68ade7f8 --- /dev/null +++ b/vehicles/src/modules/permit-application-payment/application/dto/response/read-permit-loa.dto.ts @@ -0,0 +1,10 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { ReadLoaDto } from 'src/modules/special-auth/dto/response/read-loa.dto'; + +export class ReadPermitLoaDto extends ReadLoaDto { + @ApiProperty({ + description: 'Permit Loa id', + example: 1, + }) + permitLoaId: number; +} diff --git a/vehicles/src/modules/permit-application-payment/application/entities/permit-loa.entity.ts b/vehicles/src/modules/permit-application-payment/application/entities/permit-loa.entity.ts new file mode 100644 index 000000000..b9c76c214 --- /dev/null +++ b/vehicles/src/modules/permit-application-payment/application/entities/permit-loa.entity.ts @@ -0,0 +1,29 @@ +import { + Column, + Entity, + JoinColumn, + OneToOne, + PrimaryGeneratedColumn, +} from 'typeorm'; +import { AutoMap } from '@automapper/classes'; +import { Base } from 'src/modules/common/entities/base.entity'; +import { LoaDetail } from 'src/modules/special-auth/entities/loa-detail.entity'; + +@Entity({ name: 'permit.ORBC_PERMIT_LOA' }) +export class PermitLoa extends Base { + @AutoMap() + @PrimaryGeneratedColumn({ type: 'int', name: 'PERMIT_LOA_ID' }) + permitLoaId: number; + + @AutoMap(() => LoaDetail) + @OneToOne(() => LoaDetail, (LoaDetail) => LoaDetail.loaId) + @JoinColumn({ name: 'LOA_ID' }) + loa: LoaDetail; + + @AutoMap() + @Column({ + type: 'bigint', + name: 'PERMIT_ID', + }) + permitId: string; +} diff --git a/vehicles/src/modules/permit-application-payment/application/profile/application.profile.ts b/vehicles/src/modules/permit-application-payment/application/profile/application.profile.ts index 1f63de139..9569c7c09 100644 --- a/vehicles/src/modules/permit-application-payment/application/profile/application.profile.ts +++ b/vehicles/src/modules/permit-application-payment/application/profile/application.profile.ts @@ -27,6 +27,10 @@ import { convertCaseStatus, } from '../../../../common/enum/case-status-type.enum'; import { ReadCaseActivityDto } from '../../../case-management/dto/response/read-case-activity.dto'; +import { CreatePermitLoaDto } from '../dto/request/create-permit-loa.dto'; +import { PermitLoa } from '../entities/permit-loa.entity'; +import { ReadPermitLoaDto } from '../dto/response/read-permit-loa.dto'; +import * as dayjs from 'dayjs'; @Injectable() export class ApplicationProfile extends AutomapperProfile { @@ -444,6 +448,135 @@ export class ApplicationProfile extends AutomapperProfile { }), ), ); + createMap( + mapper, + CreatePermitLoaDto, + PermitLoa, + forMember( + (d) => d.permitId, + mapWithArguments((_, { permitId }) => { + return permitId; + }), + ), + forMember( + (d) => d.loa.loaId, + mapFrom((s) => { + return s.loaIds[0]; + }), + ), + forMember( + (d) => d.createdUserGuid, + mapWithArguments((_, { userGUID }) => { + return userGUID; + }), + ), + forMember( + (d) => d.createdUser, + mapWithArguments((_, { userName }) => { + return userName; + }), + ), + forMember( + (d) => d.createdUserDirectory, + mapWithArguments((_, { directory }) => { + return directory; + }), + ), + + forMember( + (d) => d.createdDateTime, + mapWithArguments((_, { timestamp }) => { + return timestamp; + }), + ), + + forMember( + (d) => d.updatedUserGuid, + mapWithArguments((_, { userGUID }) => { + return userGUID; + }), + ), + forMember( + (d) => d.updatedUser, + mapWithArguments((_, { userName }) => { + return userName; + }), + ), + forMember( + (d) => d.updatedUserDirectory, + mapWithArguments((_, { directory }) => { + return directory; + }), + ), + + forMember( + (d) => d.updatedDateTime, + mapWithArguments((_, { timestamp }) => { + return timestamp; + }), + ), + ); + + createMap( + mapper, + PermitLoa, + ReadPermitLoaDto, + forMember( + (d) => d.permitLoaId, + mapFrom((s) => { + return s.permitLoaId; + }), + ), + forMember( + (d) => d.loaId, + mapFrom((s) => { + return s.loa.loaId; + }), + ), + forMember( + (d) => d.companyId, + mapFrom((s) => { + return s.loa.company.companyId; + }), + ), + forMember( + (d) => d.startDate, + mapFrom((s) => { + return dayjs(s.loa.startDate).format('YYYY-MM-DD'); + }), + ), + forMember( + (d) => d.expiryDate, + mapFrom((s) => { + if (s.loa.expiryDate) + return dayjs(s.loa.expiryDate).format('YYYY-MM-DD'); + }), + ), + forMember( + (d) => d.loaPermitType, + mapFrom((s) => { + return s.loa.loaPermitTypes.map((lpt) => lpt.permitType); + }), + ), + forMember( + (d) => d.powerUnits, + mapFrom((s) => { + if (s.loa.loaVehicles) + return s.loa.loaVehicles + .filter((lv) => lv.powerUnit) + .map((lv) => lv.powerUnit); + }), + ), + forMember( + (d) => d.trailers, + mapFrom((s) => { + if (s.loa.loaVehicles) + return s.loa.loaVehicles + .filter((lv) => lv.trailer) + .map((lv) => lv.trailer); + }), + ), + ); }; } } diff --git a/vehicles/src/modules/special-auth/loa.service.ts b/vehicles/src/modules/special-auth/loa.service.ts index 8363942d5..68c81028f 100644 --- a/vehicles/src/modules/special-auth/loa.service.ts +++ b/vehicles/src/modules/special-auth/loa.service.ts @@ -94,6 +94,7 @@ export class LoaService { companyId, savedLoaDetail.loaId, ); + const readLoaDto = await this.classMapper.mapAsync( refreshedLoaDetailsEntity, LoaDetail, @@ -173,7 +174,6 @@ export class LoaService { where: { loaId: loaId, company: { companyId: companyId }, - isActive: true, }, relations: ['company', 'loaVehicles', 'loaPermitTypes'], }); diff --git a/vehicles/src/modules/special-auth/profile/loa.profile.ts b/vehicles/src/modules/special-auth/profile/loa.profile.ts index a48e44ac0..0a6594429 100644 --- a/vehicles/src/modules/special-auth/profile/loa.profile.ts +++ b/vehicles/src/modules/special-auth/profile/loa.profile.ts @@ -9,11 +9,11 @@ import { AutomapperProfile, InjectMapper } from '@automapper/nestjs'; import { Injectable } from '@nestjs/common'; import { CreateLoaDto } from '../dto/request/create-loa.dto'; import { LoaDetail } from '../entities/loa-detail.entity'; -import { ReadLoaDto } from '../dto/response/read-loa.dto'; import { LoaPermitType } from '../entities/loa-permit-type-details.entity'; import { LoaVehicle } from '../entities/loa-vehicles.entity'; import * as dayjs from 'dayjs'; import { UpdateLoaDto } from '../dto/request/update-loa.dto'; +import { ReadLoaDto } from '../dto/response/read-loa.dto'; @Injectable() export class LoaProfile extends AutomapperProfile { @@ -385,7 +385,6 @@ export class LoaProfile extends AutomapperProfile { ), ), ); - createMap( mapper, LoaDetail,