diff --git a/frontend/src/common/helpers/equality.ts b/frontend/src/common/helpers/equality.ts new file mode 100644 index 000000000..2a128acc3 --- /dev/null +++ b/frontend/src/common/helpers/equality.ts @@ -0,0 +1,77 @@ +import { Nullable } from "../types/common"; + +/** + * Check if two nullable values are different. + * @param val1 First nullable value to be compared + * @param val2 Second nullable value to be compared + * @returns true when only one of the values are empty, or both are non-empty and different, false otherwise + */ +export const areValuesDifferent = ( + val1?: Nullable, + val2?: Nullable, +): boolean => { + if (!val1 && !val2) return false; // Both empty implicitly means that values are the same + + if ((val1 && !val2) || (!val1 && val2) || (val1 && val2 && val1 !== val2)) { + return true; // Only one empty, or both are non-empty but different means that values are different + } + + return false; // Values are considered equal otherwise +}; + +/** + * Determine whether or not two arrays, each with only unique primitive values, have the same values. + * @param arr1 First array consisting of only non-duplicate primitive values + * @param arr2 Second array consisting of only non-duplicate primitive values + * @returns Whether or not the two arrays have the same values + */ +export const doUniqueArraysHaveSameItems = ( + 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; +}; + +/** + * Determine whether or not two arrays, each with objects of a certain type identifiable by keys, + * have the same objects. + * @param arr1 First array consisting of identifiable objects + * @param arr2 Second array consisting of identifiable objects + * @param key Function that returns the identifier of an object of the given type + * @param equalFn Function that compares equality of two objects of the given type + * @returns Whether or not the two arrays have the same objects + */ +export const doUniqueArraysHaveSameObjects = ( + arr1: T[], + arr2: T[], + key: (item: T) => K, + equalFn: (item1: T, item2: T) => boolean, +) => { + const map1 = new Map(arr1.map(item => [key(item), item])); + const map2 = new Map(arr2.map(item => [key(item), item])); + + for (const [key, item] of map1) { + const itemInOtherMapWithSameKey = map2.get(key); + if (!itemInOtherMapWithSameKey || !equalFn(item, itemInOtherMapWithSameKey)) + return false; + } + + for (const [key, item] of map2) { + const itemInOtherMapWithSameKey = map1.get(key); + if (!itemInOtherMapWithSameKey || !equalFn(item, itemInOtherMapWithSameKey)) + return false; + } + + return true; +}; diff --git a/frontend/src/common/helpers/util.ts b/frontend/src/common/helpers/util.ts index 24b5e0b89..5fea00c4c 100644 --- a/frontend/src/common/helpers/util.ts +++ b/frontend/src/common/helpers/util.ts @@ -122,25 +122,6 @@ export const getDefaultRequiredVal = ( return defaultVals.find((val) => val != null) ?? fallbackDefault; }; -/** - * Check if two nullable values are different. - * @param val1 First nullable value to be compared - * @param val2 Second nullable value to be compared - * @returns boolean value indicating if values are different. - */ -export const areValuesDifferent = ( - val1?: Nullable, - val2?: Nullable, -): boolean => { - if (!val1 && !val2) return false; // both empty === equal - - if ((val1 && !val2) || (!val1 && val2) || (val1 && val2 && val1 !== val2)) { - return true; // one empty, or both non-empty but different === different - } - - return false; // values are equal otherwise -}; - /** * Returns the file name for a file from API response. * @param headers The collection of headers in an API response. @@ -274,27 +255,3 @@ 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/common/hooks/useMemoizedArray.ts b/frontend/src/common/hooks/useMemoizedArray.ts new file mode 100644 index 000000000..cf6599158 --- /dev/null +++ b/frontend/src/common/hooks/useMemoizedArray.ts @@ -0,0 +1,33 @@ +import { useEffect, useState } from "react"; +import { doUniqueArraysHaveSameObjects } from "../helpers/equality"; + +/** + * Hook that memoizes an array of objects. + * The memoized array only changes when the items in the array change. + * eg. If items === [{a: 1}, {a: 2}], and later [{a: 2}, {a: 1}] is passed in, + * the hook returns the same items [{a: 1}, {a: 2}]. + * @param items Array of objects + * @param key Function that returns identifier for each object + * @param equalFn Function that determines whether or not two objects are equal + * @returns Memoized array of objects + */ +export const useMemoizedArray = ( + items: T[], + key: (item: T) => K, + equalFn: (item1: T, item2: T) => boolean, +) => { + const [arrayItems, setArrayItems] = useState(items); + + useEffect(() => { + if (!doUniqueArraysHaveSameObjects( + arrayItems, + items, + key, + equalFn, + )) { + setArrayItems(items); + } + }, [items]); + + return arrayItems; +}; diff --git a/frontend/src/common/hooks/useMemoizedObject.ts b/frontend/src/common/hooks/useMemoizedObject.ts new file mode 100644 index 000000000..4d4d0725f --- /dev/null +++ b/frontend/src/common/hooks/useMemoizedObject.ts @@ -0,0 +1,26 @@ +import { useEffect, useState } from "react"; + +/** + * Hook that memoizes an object. + * The memoized object only changes when its contents change. + * eg. If obj === {a: 1, b: 2}, and later {b: 2, a: 1} is passed in, + * the hook returns the same obj {a: 1, b: 2}. + * @param obj An object + * @param equalFn Function that determines whether or not two objects are equal + * @returns Memoized object + */ +export const useMemoizedObject = ( + obj: T, + equalFn: (obj1: T, obj2: T) => boolean, +) => { + const [memoizedObj, setMemoizedObj] = useState(obj); + + useEffect(() => { + if (!equalFn(memoizedObj, obj)) { + setMemoizedObj(obj); + } + }, [obj]); + + return memoizedObj; +}; + diff --git a/frontend/src/features/manageVehicles/helpers/vehicleSubtypes.ts b/frontend/src/features/manageVehicles/helpers/vehicleSubtypes.ts index 596fe3766..144302cb1 100644 --- a/frontend/src/features/manageVehicles/helpers/vehicleSubtypes.ts +++ b/frontend/src/features/manageVehicles/helpers/vehicleSubtypes.ts @@ -1,3 +1,10 @@ +import { + BaseVehicle, + PowerUnit, + Trailer, + VEHICLE_TYPES, +} from "../types/Vehicle"; + /** * Determine whether or not a vehicle subtype ic considered to be LCV. * @param subtype Vehicle subtype @@ -12,3 +19,14 @@ export const EMPTY_VEHICLE_SUBTYPE = { type: "", description: "", }; + +export const selectedVehicleSubtype = (vehicle: BaseVehicle) => { + switch (vehicle.vehicleType) { + case VEHICLE_TYPES.POWER_UNIT: + return (vehicle as PowerUnit).powerUnitTypeCode; + case VEHICLE_TYPES.TRAILER: + return (vehicle as Trailer).trailerTypeCode; + default: + return ""; + } +}; diff --git a/frontend/src/features/permits/context/ApplicationFormContext.ts b/frontend/src/features/permits/context/ApplicationFormContext.ts index 835503fde..42a74b64e 100644 --- a/frontend/src/features/permits/context/ApplicationFormContext.ts +++ b/frontend/src/features/permits/context/ApplicationFormContext.ts @@ -2,7 +2,7 @@ import { createContext } from "react"; import { Dayjs } from "dayjs"; import { PermitVehicleDetails } from "../types/PermitVehicleDetails"; -import { LOADetail } from "../../settings/types/SpecialAuthorization"; +import { LOADetail } from "../../settings/types/LOADetail"; import { ApplicationFormData } from "../types/application"; import { getDefaultValues } from "../helpers/getDefaultApplicationFormData"; import { DEFAULT_PERMIT_TYPE } from "../types/PermitType"; @@ -10,6 +10,7 @@ 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 { PermitLOA } from "../types/PermitLOA"; import { PAST_START_DATE_STATUSES, PastStartDateStatus, @@ -49,7 +50,7 @@ interface ApplicationFormContextType { onToggleSaveVehicle: (saveVehicle: boolean) => void; onSetVehicle: (vehicleDetails: PermitVehicleDetails) => void; onClearVehicle: (saveVehicle: boolean) => void; - onUpdateLOAs: (updatedLOAs: LOADetail[]) => void; + onUpdateLOAs: (updatedLOAs: PermitLOA[]) => void; } export const ApplicationFormContext = createContext({ diff --git a/frontend/src/features/permits/helpers/conditions.ts b/frontend/src/features/permits/helpers/conditions.ts index e52ddc1d9..681c5d263 100644 --- a/frontend/src/features/permits/helpers/conditions.ts +++ b/frontend/src/features/permits/helpers/conditions.ts @@ -1,3 +1,4 @@ +import { isVehicleSubtypeLCV } from "../../manageVehicles/helpers/vehicleSubtypes"; import { LCV_CONDITION } from "../constants/constants"; import { MANDATORY_TROS_CONDITIONS, TROS_CONDITIONS } from "../constants/tros"; import { MANDATORY_TROW_CONDITIONS, TROW_CONDITIONS } from "../constants/trow"; @@ -83,3 +84,86 @@ export const getDefaultConditions = ( })), ); }; + +/** + * 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; +}; + +/** + * Get permit condition selection state, including all selected, unselected, and disabled conditions. + * @param permitType Permit type + * @param isLcvDesignated Whether or not the LCV designation is to be used + * @param vehicleSubtype Selected vehicle subtype + * @param prevSelectedConditions Previously selected permit conditions + * @returns Permit condition selection state + */ +export const getPermitConditionSelectionState = ( + permitType: PermitType, + isLcvDesignated: boolean, + vehicleSubtype: string, + prevSelectedConditions: PermitCondition[], +): PermitCondition[] => { + const defaultConditionsForPermitType = getDefaultConditions( + permitType, + isLcvDesignated && isVehicleSubtypeLCV(vehicleSubtype), + ); + + const updatedConditionsInForm = getUpdatedConditionsForLCV( + isLcvDesignated, + prevSelectedConditions, + vehicleSubtype, + ); + + return defaultConditionsForPermitType.map((defaultCondition) => { + // Select all conditions that were previously selected + const existingCondition = updatedConditionsInForm.find( + (c) => c.condition === defaultCondition.condition, + ); + + return { + ...defaultCondition, + checked: existingCondition + ? existingCondition.checked + : defaultCondition.checked, + }; + }); +}; diff --git a/frontend/src/features/permits/helpers/dateSelection.ts b/frontend/src/features/permits/helpers/dateSelection.ts index 68d6a6cec..4472fa941 100644 --- a/frontend/src/features/permits/helpers/dateSelection.ts +++ b/frontend/src/features/permits/helpers/dateSelection.ts @@ -2,6 +2,9 @@ import { Dayjs } from "dayjs"; import { BASE_DAYS_IN_YEAR, TERM_DURATION_INTERVAL_DAYS } from "../constants/constants"; import { PERMIT_TYPES, PermitType } from "../types/PermitType"; +import { getExpiryDate } from "./permitState"; +import { getMostRecentExpiryFromLOAs } from "./permitLOA"; +import { PermitLOA } from "../types/PermitLOA"; import { MAX_TROS_DURATION, MIN_TROS_DURATION, @@ -15,9 +18,6 @@ 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. @@ -94,7 +94,7 @@ export const getAvailableDurationOptions = ( value: number; label: string; }[], - selectedLOAs: LOADetail[], + selectedLOAs: PermitLOA[], startDate: Dayjs, ) => { const mostRecentLOAExpiry = getMostRecentExpiryFromLOAs(selectedLOAs); diff --git a/frontend/src/features/permits/helpers/equality.ts b/frontend/src/features/permits/helpers/equality.ts index 8d384c04b..e1d2ddb0f 100644 --- a/frontend/src/features/permits/helpers/equality.ts +++ b/frontend/src/features/permits/helpers/equality.ts @@ -4,12 +4,13 @@ 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 { arePermitLOADetailsEqual, PermitLOA } from "../types/PermitLOA"; import { DATE_FORMATS, dayjsToLocalStr, } from "../../../common/helpers/formatDate"; +import { doUniqueArraysHaveSameObjects } from "../../../common/helpers/equality"; /** * Compare whether or not two mailing addresses are equal. @@ -121,8 +122,8 @@ const areVehicleDetailsEqual = ( * @returns true when the selected LOAs are the same, false otherwise */ export const arePermitLOAsEqual = ( - loas1: Nullable, - loas2: Nullable, + loas1: Nullable, + loas2: Nullable, ) => { const isLoas1Empty = !loas1 || loas1.length === 0; const isLoas2Empty = !loas2 || loas2.length === 0; @@ -131,26 +132,12 @@ export const arePermitLOAsEqual = ( if ((isLoas1Empty && !isLoas2Empty) || (!isLoas1Empty && isLoas2Empty)) return false; - const loaMap1 = new Map( - (loas1 as LOADetail[]).map((loa) => [loa.loaNumber, loa]), + return doUniqueArraysHaveSameObjects( + loas1 as PermitLOA[], + loas2 as PermitLOA[], + (loa) => loa.loaNumber, + arePermitLOADetailsEqual, ); - 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; }; /** diff --git a/frontend/src/features/permits/helpers/permitLCV.ts b/frontend/src/features/permits/helpers/permitLCV.ts index de37957f6..60237460e 100644 --- a/frontend/src/features/permits/helpers/permitLCV.ts +++ b/frontend/src/features/permits/helpers/permitLCV.ts @@ -1,10 +1,8 @@ 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 { getPermitConditionSelectionState } from "./conditions"; import { getDefaultVehicleDetails } from "./permitVehicles"; /** @@ -26,49 +24,6 @@ export const getUpdatedVehicleDetailsForLCV = ( 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 @@ -87,11 +42,12 @@ export const applyLCVToApplicationData = checked); return { ...applicationData, diff --git a/frontend/src/features/permits/helpers/permitLOA.ts b/frontend/src/features/permits/helpers/permitLOA.ts index afb151a0c..c24c11568 100644 --- a/frontend/src/features/permits/helpers/permitLOA.ts +++ b/frontend/src/features/permits/helpers/permitLOA.ts @@ -1,6 +1,6 @@ import dayjs, { Dayjs } from "dayjs"; -import { LOADetail } from "../../settings/types/SpecialAuthorization"; +import { LOADetail } from "../../settings/types/LOADetail"; import { PermitType } from "../types/PermitType"; import { getEndOfDate, toLocalDayjs } from "../../../common/helpers/formatDate"; import { Nullable } from "../../../common/types/common"; @@ -9,6 +9,7 @@ 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 { PermitLOA } from "../types/PermitLOA"; import { durationOptionsForPermitType, getAvailableDurationOptions, @@ -52,7 +53,7 @@ export const filterNonExpiredLOAs = ( * @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[]) => { +export const getMostRecentExpiryFromLOAs = (loas: PermitLOA[]) => { const expiringLOAs = loas.filter(loa => Boolean(loa.expiryDate)); if (expiringLOAs.length === 0) return null; @@ -75,7 +76,7 @@ export const getMostRecentExpiryFromLOAs = (loas: LOADetail[]) => { */ export const getUpdatedLOASelection = ( upToDateLOAs: LOADetail[], - prevSelectedLOAs: LOADetail[], + prevSelectedLOAs: PermitLOA[], 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, @@ -94,7 +95,18 @@ export const getUpdatedLOASelection = ( const isEnabled = !isExpiringBeforeMinPermitExpiry; return { - loa, + loa: { + loaId: loa.loaId, + loaNumber: loa.loaNumber, + companyId: loa.companyId, + startDate: loa.startDate, + expiryDate: loa.expiryDate, + loaPermitType: loa.loaPermitType, + powerUnits: loa.powerUnits, + trailers: loa.trailers, + originalLoaId: loa.originalLoaId, + previousLoaId: loa.previousLoaId, + }, checked: isSelected, disabled: !isEnabled, }; @@ -111,7 +123,7 @@ export const getUpdatedLOASelection = ( * @returns Updated vehicle details and filtered vehicle options */ export const getUpdatedVehicleDetailsForLOAs = ( - selectedLOAs: LOADetail[], + selectedLOAs: PermitLOA[], vehicleOptions: (PowerUnit | Trailer)[], prevSelectedVehicle: PermitVehicleDetails, ineligiblePowerUnitSubtypes: string[], diff --git a/frontend/src/features/permits/helpers/permitVehicles.ts b/frontend/src/features/permits/helpers/permitVehicles.ts index aa667e489..e250c87e0 100644 --- a/frontend/src/features/permits/helpers/permitVehicles.ts +++ b/frontend/src/features/permits/helpers/permitVehicles.ts @@ -1,11 +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 { PermitLOA } from "../types/PermitLOA"; 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 { EMPTY_VEHICLE_SUBTYPE, isVehicleSubtypeLCV } from "../../manageVehicles/helpers/vehicleSubtypes"; import { PowerUnit, Trailer, @@ -14,6 +14,7 @@ import { VEHICLE_TYPES, Vehicle, } from "../../manageVehicles/types/Vehicle"; +import { sortVehicleSubTypes } from "./sorter"; export const getIneligiblePowerUnitSubtypes = (permitType: PermitType) => { switch (permitType) { @@ -120,7 +121,7 @@ export const filterVehicles = ( vehicles: Vehicle[], ineligiblePowerUnitSubtypes: string[], ineligibleTrailerSubtypes: string[], - loas: LOADetail[], + loas: PermitLOA[], ) => { const permittedPowerUnitIds = new Set([ ...loas.map(loa => loa.powerUnits) @@ -154,3 +155,67 @@ export const filterVehicles = ( }); }); }; + +/** + * Get vehicle subtype options for given vehicle type. + * @param vehicleType Vehicle type + * @param powerUnitSubtypes Vehicle subtypes for power units + * @param trailerSubtypes Vehicle subtypes for trailers + * @returns Correct vehicle subtype options for vehicle type + */ +export const getSubtypeOptions = ( + vehicleType: string, + powerUnitSubtypes: VehicleSubType[], + trailerSubtypes: VehicleSubType[], +) => { + if (vehicleType === VEHICLE_TYPES.POWER_UNIT) { + return [...powerUnitSubtypes]; + } + if (vehicleType === VEHICLE_TYPES.TRAILER) { + return [...trailerSubtypes]; + } + return [EMPTY_VEHICLE_SUBTYPE]; +}; + +/** + * Get eligible subset of vehicle subtype options given lists of available subtypes and criteria. + * @param powerUnitSubtypes All available power unit subtypes + * @param trailerSubtypes All available trailer subtypes + * @param ineligiblePowerUnitSubtypes List of ineligible power unit subtypes + * @param ineligibleTrailerSubtypes List of ineligible trailer subtypes + * @param allowedLOAPowerUnitSubtypes List of power unit subtypes allowed by LOAs + * @param allowedLOATrailerSubtypes List of trailer subtypes allowed by LOAs + * @param vehicleType Vehicle type + * @returns Eligible subset of vehicle subtype options + */ +export const getEligibleSubtypeOptions = ( + powerUnitSubtypes: VehicleSubType[], + trailerSubtypes: VehicleSubType[], + ineligiblePowerUnitSubtypes: VehicleSubType[], + ineligibleTrailerSubtypes: VehicleSubType[], + allowedLOAPowerUnitSubtypes: string[], + allowedLOATrailerSubtypes: string[], + vehicleType?: string, +) => { + if ( + vehicleType !== VEHICLE_TYPES.POWER_UNIT && + vehicleType !== VEHICLE_TYPES.TRAILER + ) { + return [EMPTY_VEHICLE_SUBTYPE]; + } + + // Sort vehicle subtypes alphabetically + const sortedVehicleSubtypes = sortVehicleSubTypes( + vehicleType, + getSubtypeOptions(vehicleType, powerUnitSubtypes, trailerSubtypes), + ); + + return filterVehicleSubtypes( + sortedVehicleSubtypes, + vehicleType, + ineligiblePowerUnitSubtypes, + ineligibleTrailerSubtypes, + allowedLOAPowerUnitSubtypes, + allowedLOATrailerSubtypes, + ); +}; diff --git a/frontend/src/features/permits/hooks/useApplicationFormContext.ts b/frontend/src/features/permits/hooks/useApplicationFormContext.ts index cb67852d6..6d3ec01a0 100644 --- a/frontend/src/features/permits/hooks/useApplicationFormContext.ts +++ b/frontend/src/features/permits/hooks/useApplicationFormContext.ts @@ -2,11 +2,15 @@ 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"; +import { arePermitLOADetailsEqual, PermitLOA } from "../types/PermitLOA"; +import { useMemoizedArray } from "../../../common/hooks/useMemoizedArray"; +import { getDefaultRequiredVal } from "../../../common/helpers/util"; +import { arePermitConditionEqual } from "../types/PermitCondition"; +import { useMemoizedObject } from "../../../common/hooks/useMemoizedObject"; export const useApplicationFormContext = () => { const { @@ -38,67 +42,146 @@ export const useApplicationFormContext = () => { onUpdateLOAs, } = useContext(ApplicationFormContext); - const permitType = formData.permitType; const { - loas: currentSelectedLOAs, + permitType, + applicationNumber, + permitNumber, + } = formData; + + const { + expiryDate: permitExpiryDate, + loas, permitDuration, startDate: permitStartDate, - commodities: permitConditions, + commodities, vehicleDetails: vehicleFormData, } = formData.permitData; + const createdAt = useMemoizedObject( + createdDateTime, + (dateObj1, dateObj2) => Boolean( + (!dateObj1 && !dateObj2) || (dateObj1 && dateObj2 && dateObj1.isSame(dateObj2)), + ), + ); + + const updatedAt = useMemoizedObject( + updatedDateTime, + (dateObj1, dateObj2) => Boolean( + (!dateObj1 && !dateObj2) || (dateObj1 && dateObj2 && dateObj1.isSame(dateObj2)), + ), + ); + + const startDate = useMemoizedObject( + getStartOfDate(permitStartDate), + (dateObj1, dateObj2) => dateObj1.isSame(dateObj2), + ); + + const expiryDate = useMemoizedObject( + permitExpiryDate, + (dateObj1, dateObj2) => dateObj1.isSame(dateObj2), + ); + + const currentSelectedLOAs = useMemoizedArray( + getDefaultRequiredVal([], loas), + ({ loaNumber }) => loaNumber, + arePermitLOADetailsEqual, + ); + + const permitConditions = useMemoizedArray( + commodities, + ({ condition }) => condition, + arePermitConditionEqual, + ); + // Update duration options and expiry when needed const { availableDurationOptions } = usePermitDateSelection( permitType, - getStartOfDate(permitStartDate), + startDate, durationOptions, - currentSelectedLOAs as LOADetail[], + currentSelectedLOAs as PermitLOA[], permitDuration, onSetDuration, onSetExpiryDate, ); // Update permit conditions when LCV designation or vehicle subtype changes - usePermitConditions( + const { allConditions } = usePermitConditions( + permitType, permitConditions, isLcvDesignated, vehicleFormData.vehicleSubType, onSetConditions, ); + // Get ineligible vehicle subtypes + const ineligibleSubtypes = getIneligibleSubtypes(permitType, isLcvDesignated); + const ineligiblePowerUnitSubtypes = useMemoizedArray( + ineligibleSubtypes.ineligiblePowerUnitSubtypes, + (subtype) => subtype.typeCode, + (subtype1, subtype2) => subtype1.typeCode === subtype2.typeCode, + ); + + const ineligibleTrailerSubtypes = useMemoizedArray( + ineligibleSubtypes.ineligibleTrailerSubtypes, + (subtype) => subtype.typeCode, + (subtype1, subtype2) => subtype1.typeCode === subtype2.typeCode, + ); + // Check to see if vehicle details is still valid after LOA has been deselected + // Also get vehicle subtype options, and whether or not selected vehicle is an LOA vehicle const { - ineligiblePowerUnitSubtypes, - ineligibleTrailerSubtypes, - } = getIneligibleSubtypes(permitType, isLcvDesignated); - - const { filteredVehicleOptions } = usePermitVehicleForLOAs( + filteredVehicleOptions, + subtypeOptions, + isSelectedLOAVehicle, + } = usePermitVehicleForLOAs( vehicleFormData, vehicleOptions, - currentSelectedLOAs as LOADetail[], - ineligiblePowerUnitSubtypes.map(({ typeCode }) => typeCode), - ineligibleTrailerSubtypes.map(({ typeCode }) => typeCode), + currentSelectedLOAs as PermitLOA[], + powerUnitSubtypes, + trailerSubtypes, + ineligiblePowerUnitSubtypes, + ineligibleTrailerSubtypes, () => onClearVehicle(Boolean(vehicleFormData.saveVehicle)), ); + const memoizedCompanyLOAs = useMemoizedArray( + companyLOAs, + ({ loaNumber }) => loaNumber, + arePermitLOADetailsEqual, + ); + + const memoizedRevisionHistory = useMemoizedArray( + revisionHistory, + (historyItem) => `${historyItem.permitId}-${historyItem.revisionDateTime}`, + (historyItem1, historyItem2) => + historyItem1.permitId === historyItem2.permitId + && historyItem1.revisionDateTime === historyItem2.revisionDateTime + && historyItem1.name === historyItem2.name + && historyItem1.comment === historyItem2.comment, + ); + return { initialFormData, - formData, + permitType, + applicationNumber, + permitNumber, + startDate, + expiryDate, + currentSelectedLOAs, + vehicleFormData, + allConditions, availableDurationOptions, - powerUnitSubtypes, - trailerSubtypes, - isLcvDesignated, - ineligiblePowerUnitSubtypes, - ineligibleTrailerSubtypes, filteredVehicleOptions, + subtypeOptions, + isSelectedLOAVehicle, feature, companyInfo, isAmendAction, - createdDateTime, - updatedDateTime, + createdDateTime: createdAt, + updatedDateTime: updatedAt, pastStartDateStatus, - companyLOAs, - revisionHistory, + companyLOAs: memoizedCompanyLOAs, + revisionHistory: memoizedRevisionHistory, onLeave, onSave, onCancel, diff --git a/frontend/src/features/permits/hooks/useInitApplicationFormData.ts b/frontend/src/features/permits/hooks/useInitApplicationFormData.ts index 4c60e4147..1b410ff0b 100644 --- a/frontend/src/features/permits/hooks/useInitApplicationFormData.ts +++ b/frontend/src/features/permits/hooks/useInitApplicationFormData.ts @@ -1,4 +1,4 @@ -import { useEffect, useMemo } from "react"; +import { useCallback, useEffect, useMemo } from "react"; import { useForm } from "react-hook-form"; import dayjs, { Dayjs } from "dayjs"; @@ -7,7 +7,7 @@ import { BCeIDUserDetailContext } from "../../../common/authentication/OnRouteBC import { CompanyProfile } from "../../manageProfile/types/manageProfile"; import { Nullable } from "../../../common/types/common"; import { PermitType } from "../types/PermitType"; -import { LOADetail } from "../../settings/types/SpecialAuthorization"; +import { LOADetail } from "../../settings/types/LOADetail"; import { applyUpToDateLOAsToApplication } from "../helpers/permitLOA"; import { getDefaultValues } from "../helpers/getDefaultApplicationFormData"; import { applyLCVToApplicationData } from "../helpers/permitLCV"; @@ -15,6 +15,7 @@ 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"; +import { PermitLOA } from "../types/PermitLOA"; /** * Custom hook for populating the form using fetched application data, as well as current company id and user details. @@ -30,7 +31,7 @@ import { EMPTY_VEHICLE_DETAILS, PermitVehicleDetails } from "../types/PermitVehi export const useInitApplicationFormData = ( permitType: PermitType, isLcvDesignated: boolean, - loas: LOADetail[], + companyLOAs: LOADetail[], inventoryVehicles: (PowerUnit | Trailer)[], companyInfo: Nullable, applicationData?: Nullable, @@ -56,7 +57,7 @@ export const useInitApplicationFormData = ( ), isLcvDesignated, ), - loas, + companyLOAs, inventoryVehicles, ineligiblePowerUnitSubtypes, ineligibleTrailerSubtypes, @@ -67,7 +68,7 @@ export const useInitApplicationFormData = ( applicationData, userDetails, isLcvDesignated, - loas, + companyLOAs, inventoryVehicles, ]); @@ -85,38 +86,38 @@ export const useInitApplicationFormData = ( reset(initialFormData); }, [initialFormData]); - const onSetDuration = (duration: number) => { + const onSetDuration = useCallback((duration: number) => { setValue("permitData.permitDuration", duration); - }; + }, [setValue]); - const onSetExpiryDate = (expiry: Dayjs) => { + const onSetExpiryDate = useCallback((expiry: Dayjs) => { setValue("permitData.expiryDate", dayjs(expiry)); - }; + }, [setValue]); - const onSetConditions = (conditions: PermitCondition[]) => { + const onSetConditions = useCallback((conditions: PermitCondition[]) => { setValue("permitData.commodities", [...conditions]); - }; + }, [setValue]); - const onToggleSaveVehicle = (saveVehicle: boolean) => { + const onToggleSaveVehicle = useCallback((saveVehicle: boolean) => { setValue("permitData.vehicleDetails.saveVehicle", saveVehicle); - }; + }, [setValue]); - const onSetVehicle = (vehicleDetails: PermitVehicleDetails) => { + const onSetVehicle = useCallback((vehicleDetails: PermitVehicleDetails) => { setValue("permitData.vehicleDetails", { ...vehicleDetails, }); - }; + }, [setValue]); - const onClearVehicle = (saveVehicle: boolean) => { + const onClearVehicle = useCallback((saveVehicle: boolean) => { setValue("permitData.vehicleDetails", { ...EMPTY_VEHICLE_DETAILS, saveVehicle, }); - }; + }, [setValue]); - const onUpdateLOAs = (updatedLOAs: LOADetail[]) => { + const onUpdateLOAs = useCallback((updatedLOAs: PermitLOA[]) => { setValue("permitData.loas", updatedLOAs); - }; + }, [setValue]); return { initialFormData, diff --git a/frontend/src/features/permits/hooks/usePermitConditions.ts b/frontend/src/features/permits/hooks/usePermitConditions.ts index 2d98456d9..0b3786fce 100644 --- a/frontend/src/features/permits/hooks/usePermitConditions.ts +++ b/frontend/src/features/permits/hooks/usePermitConditions.ts @@ -1,34 +1,46 @@ -import { useEffect } from "react"; +import { useEffect, useMemo } from "react"; -import { areArraysEqual } from "../../../common/helpers/util"; import { PermitCondition } from "../types/PermitCondition"; -import { getUpdatedConditionsForLCV } from "../helpers/permitLCV"; +import { doUniqueArraysHaveSameItems } from "../../../common/helpers/equality"; +import { PermitType } from "../types/PermitType"; +import { getPermitConditionSelectionState } from "../helpers/conditions"; export const usePermitConditions = ( + permitType: PermitType, 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( + // All possible conditions to be used for conditions table, including non-selected ones + const allConditions = useMemo(() => { + return getPermitConditionSelectionState( + permitType, + isLcvDesignated, + vehicleSubtype, + selectedConditions, + ); + }, [ + permitType, isLcvDesignated, - selectedConditions, vehicleSubtype, - ); + selectedConditions, + ]); + + const updatedConditions = allConditions + .filter(({ checked }) => checked); useEffect(() => { - if (!areArraysEqual( + if (!doUniqueArraysHaveSameItems( updatedConditions.map(({ condition }) => condition), - selectedConditions.map(({ condition }: PermitCondition) => condition), + selectedConditions.map(({ condition }) => condition), )) { onSetConditions(updatedConditions); } }, [ updatedConditions, selectedConditions, - onSetConditions, ]); - return { updatedConditions }; + return { allConditions }; }; diff --git a/frontend/src/features/permits/hooks/usePermitDateSelection.ts b/frontend/src/features/permits/hooks/usePermitDateSelection.ts index ca89a2f08..c45462ddb 100644 --- a/frontend/src/features/permits/hooks/usePermitDateSelection.ts +++ b/frontend/src/features/permits/hooks/usePermitDateSelection.ts @@ -2,9 +2,10 @@ 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"; +import { PermitLOA } from "../types/PermitLOA"; +import { useMemoizedArray } from "../../../common/hooks/useMemoizedArray"; /** * Hook that manages permit date selection based on changing permit data. @@ -22,16 +23,20 @@ export const usePermitDateSelection = ( value: number; label: string; }[], - selectedLOAs: LOADetail[], + selectedLOAs: PermitLOA[], selectedDuration: number, onSetDuration: (duration: number) => void, onSetExpiryDate: (expiry: Dayjs) => void, ) => { // Limit permit duration options based on selected LOAs - const availableDurationOptions = getAvailableDurationOptions( - durationOptions, - selectedLOAs, - startDate, + const availableDurationOptions = useMemoizedArray( + getAvailableDurationOptions( + durationOptions, + selectedLOAs, + startDate, + ), + (option) => option.value, + (option1, option2) => option1.value === option2.value, ); // If duration options change, check if the current permit duration is still selectable @@ -45,7 +50,6 @@ export const usePermitDateSelection = ( onSetDuration(updatedDuration); }, [ updatedDuration, - onSetDuration, ]); const expiryDate = getExpiryDate(startDate, selectedDuration); @@ -53,7 +57,6 @@ export const usePermitDateSelection = ( onSetExpiryDate(expiryDate); }, [ expiryDate, - onSetExpiryDate, ]); return { availableDurationOptions }; diff --git a/frontend/src/features/permits/hooks/usePermitVehicleForLOAs.ts b/frontend/src/features/permits/hooks/usePermitVehicleForLOAs.ts index 082f1a1b5..a6ac81b82 100644 --- a/frontend/src/features/permits/hooks/usePermitVehicleForLOAs.ts +++ b/frontend/src/features/permits/hooks/usePermitVehicleForLOAs.ts @@ -1,29 +1,45 @@ -import { useEffect } from "react"; +import { useEffect, useMemo } 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"; +import { PermitLOA } from "../types/PermitLOA"; +import { getEligibleSubtypeOptions } from "../helpers/permitVehicles"; +import { + PowerUnit, + Trailer, + VEHICLE_TYPES, + VehicleSubType, +} from "../../manageVehicles/types/Vehicle"; export const usePermitVehicleForLOAs = ( vehicleFormData: PermitVehicleDetails, vehicleOptions: (PowerUnit | Trailer)[], - selectedLOAs: LOADetail[], - ineligiblePowerUnitSubtypes: string[], - ineligibleTrailerSubtypes: string[], + selectedLOAs: PermitLOA[], + powerUnitSubtypes: VehicleSubType[], + trailerSubtypes: VehicleSubType[], + ineligiblePowerUnitSubtypes: VehicleSubType[], + ineligibleTrailerSubtypes: VehicleSubType[], onClearVehicle: () => void, ) => { // Check to see if vehicle details is still valid after LOA has been deselected const { - filteredVehicleOptions, updatedVehicle, - } = getUpdatedVehicleDetailsForLOAs( + filteredVehicleOptions, + } = useMemo(() => { + return getUpdatedVehicleDetailsForLOAs( + selectedLOAs, + vehicleOptions, + vehicleFormData, + ineligiblePowerUnitSubtypes.map(({ typeCode }) => typeCode), + ineligibleTrailerSubtypes.map(({ typeCode }) => typeCode), + ); + }, [ selectedLOAs, vehicleOptions, vehicleFormData, ineligiblePowerUnitSubtypes, ineligibleTrailerSubtypes, - ); + ]); const vehicleIdInForm = vehicleFormData.vehicleId; const updatedVehicleId = updatedVehicle.vehicleId; @@ -35,10 +51,87 @@ export const usePermitVehicleForLOAs = ( }, [ vehicleIdInForm, updatedVehicleId, - onClearVehicle, + ]); + + // Get vehicle subtypes that are allowed by LOAs + const vehicleType = vehicleFormData.vehicleType; + const { + subtypeOptions, + isSelectedLOAVehicle, + } = useMemo(() => { + 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, + ], []), + ]); + + // Try to find all of the unfiltered vehicles in the inventory, and get a list of their subtypes + // as some of these unfiltered subtypes can potentially be used by a selected LOA + 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 isSelectedLOAVehicle = Boolean(vehicleIdInForm) + && ( + permittedLOAPowerUnitIds.has(vehicleIdInForm as string) + || permittedLOATrailerIds.has(vehicleIdInForm as string) + ) + && ( + powerUnitsInInventory.map(powerUnit => powerUnit.powerUnitId) + .includes(vehicleIdInForm as string) + || trailersInInventory.map(trailer => trailer.trailerId) + .includes(vehicleIdInForm as string) + ); + + const subtypeOptions = getEligibleSubtypeOptions( + powerUnitSubtypes, + trailerSubtypes, + ineligiblePowerUnitSubtypes, + ineligibleTrailerSubtypes, + permittedLOAPowerUnitSubtypes, + permittedLOATrailerSubtypes, + vehicleType, + ); + + return { + subtypeOptions, + isSelectedLOAVehicle, + }; + }, [ + selectedLOAs, + vehicleOptions, + vehicleType, + vehicleIdInForm, + powerUnitSubtypes, + trailerSubtypes, + ineligiblePowerUnitSubtypes, + ineligibleTrailerSubtypes, ]); return { filteredVehicleOptions, + subtypeOptions, + isSelectedLOAVehicle, }; }; diff --git a/frontend/src/features/permits/pages/Amend/components/AmendPermitForm.tsx b/frontend/src/features/permits/pages/Amend/components/AmendPermitForm.tsx index 4379cda5b..5e4a44f4b 100644 --- a/frontend/src/features/permits/pages/Amend/components/AmendPermitForm.tsx +++ b/frontend/src/features/permits/pages/Amend/components/AmendPermitForm.tsx @@ -21,6 +21,7 @@ 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 { filterLOAsForPermitType, filterNonExpiredLOAs } from "../../../helpers/permitLOA"; import { dayjsToUtcStr, nowUtc, @@ -55,6 +56,11 @@ export const AmendPermitForm = () => { const navigate = useNavigate(); const { data: activeLOAs } = useFetchLOAs(companyId, false); + const companyLOAs = useMemo(() => getDefaultRequiredVal( + [], + activeLOAs, + ), [activeLOAs]); + const { data: companyInfo } = useCompanyInfoDetailsQuery(companyId); const { data: specialAuthorizations } = useFetchSpecialAuthorizations(companyId); const isLcvDesignated = Boolean(specialAuthorizations?.isLcvAllowed); @@ -80,7 +86,7 @@ export const AmendPermitForm = () => { } = useAmendPermitForm( currentStepIndex === 0, isLcvDesignated, - getDefaultRequiredVal([], activeLOAs), + companyLOAs, vehicleOptions, companyInfo, permit, @@ -92,8 +98,16 @@ export const AmendPermitForm = () => { permit, ); - const applicableLOAs = getDefaultRequiredVal([], activeLOAs) - .filter(loa => loa.loaPermitType.includes(formData.permitType)); + // 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( + companyLOAs, + formData.permitType, + ), + formData.permitData.startDate, + ); const amendPermitMutation = useAmendPermit(companyId); const modifyAmendmentMutation = useModifyAmendmentApplication(); diff --git a/frontend/src/features/permits/pages/Amend/hooks/useAmendPermitForm.ts b/frontend/src/features/permits/pages/Amend/hooks/useAmendPermitForm.ts index 275194cde..6cb6abc82 100644 --- a/frontend/src/features/permits/pages/Amend/hooks/useAmendPermitForm.ts +++ b/frontend/src/features/permits/pages/Amend/hooks/useAmendPermitForm.ts @@ -1,4 +1,4 @@ -import { useEffect, useMemo } from "react"; +import { useCallback, useEffect, useMemo } from "react"; import { useForm } from "react-hook-form"; import dayjs, { Dayjs } from "dayjs"; @@ -10,20 +10,21 @@ import { CompanyProfile } from "../../../../manageProfile/types/manageProfile"; 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 { LOADetail } from "../../../../settings/types/LOADetail"; import { getIneligibleSubtypes } from "../../../helpers/permitVehicles"; +import { applyUpToDateLOAsToApplication } from "../../../helpers/permitLOA"; +import { PowerUnit, Trailer } from "../../../../manageVehicles/types/Vehicle"; +import { PermitLOA } from "../../../types/PermitLOA"; 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[], + companyLOAs: LOADetail[], inventoryVehicles: (PowerUnit | Trailer)[], companyInfo: Nullable, permit?: Nullable, @@ -51,7 +52,7 @@ export const useAmendPermitForm = ( ), isLcvDesignated, ), - loas, + companyLOAs, inventoryVehicles, ineligiblePowerUnitSubtypes, ineligibleTrailerSubtypes, @@ -87,7 +88,7 @@ export const useAmendPermitForm = ( defaultPermitFormData, isLcvDesignated, ), - loas, + companyLOAs, inventoryVehicles, ineligiblePowerUnitSubtypes, ineligibleTrailerSubtypes, @@ -98,7 +99,7 @@ export const useAmendPermitForm = ( repopulateFormData, companyInfo, isLcvDesignated, - loas, + companyLOAs, inventoryVehicles, ]); @@ -115,38 +116,38 @@ export const useAmendPermitForm = ( reset(defaultFormData); }, [defaultFormData]); - const onSetDuration = (duration: number) => { + const onSetDuration = useCallback((duration: number) => { setValue("permitData.permitDuration", duration); - }; + }, [setValue]); - const onSetExpiryDate = (expiry: Dayjs) => { + const onSetExpiryDate = useCallback((expiry: Dayjs) => { setValue("permitData.expiryDate", dayjs(expiry)); - }; + }, [setValue]); - const onSetConditions = (conditions: PermitCondition[]) => { + const onSetConditions = useCallback((conditions: PermitCondition[]) => { setValue("permitData.commodities", [...conditions]); - }; + }, [setValue]); - const onToggleSaveVehicle = (saveVehicle: boolean) => { + const onToggleSaveVehicle = useCallback((saveVehicle: boolean) => { setValue("permitData.vehicleDetails.saveVehicle", saveVehicle); - }; + }, [setValue]); - const onSetVehicle = (vehicleDetails: PermitVehicleDetails) => { + const onSetVehicle = useCallback((vehicleDetails: PermitVehicleDetails) => { setValue("permitData.vehicleDetails", { ...vehicleDetails, }); - }; + }, [setValue]); - const onClearVehicle = (saveVehicle: boolean) => { + const onClearVehicle = useCallback((saveVehicle: boolean) => { setValue("permitData.vehicleDetails", { ...EMPTY_VEHICLE_DETAILS, saveVehicle, }); - }; + }, [setValue]); - const onUpdateLOAs = (updatedLOAs: LOADetail[]) => { + const onUpdateLOAs = useCallback((updatedLOAs: PermitLOA[]) => { setValue("permitData.loas", updatedLOAs); - }; + }, [setValue]); return { initialFormData: defaultFormData, diff --git a/frontend/src/features/permits/pages/Application/ApplicationForm.tsx b/frontend/src/features/permits/pages/Application/ApplicationForm.tsx index c5d9b48d6..4e91f7dad 100644 --- a/frontend/src/features/permits/pages/Application/ApplicationForm.tsx +++ b/frontend/src/features/permits/pages/Application/ApplicationForm.tsx @@ -68,6 +68,11 @@ export const ApplicationForm = ({ permitType }: { permitType: PermitType }) => { ); const { data: activeLOAs } = useFetchLOAs(companyId, false); + const companyLOAs = useMemo(() => getDefaultRequiredVal( + [], + activeLOAs, + ), [activeLOAs]); + const { data: specialAuthorizations } = useFetchSpecialAuthorizations(companyId); const isLcvDesignated = Boolean(specialAuthorizations?.isLcvAllowed); @@ -97,7 +102,7 @@ export const ApplicationForm = ({ permitType }: { permitType: PermitType }) => { } = useInitApplicationFormData( permitType, isLcvDesignated, - getDefaultRequiredVal([], activeLOAs), + companyLOAs, vehicleOptions, companyInfo, applicationContext?.applicationData, @@ -109,7 +114,7 @@ export const ApplicationForm = ({ permitType }: { permitType: PermitType }) => { // 2. Have expiry date that is on or after the start date for an application const applicableLOAs = filterNonExpiredLOAs( filterLOAsForPermitType( - getDefaultRequiredVal([], activeLOAs), + companyLOAs, permitType, ), currentFormData.permitData.startDate, 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 3cb90f67d..1e0f799d8 100644 --- a/frontend/src/features/permits/pages/Application/components/form/ConditionsTable.tsx +++ b/frontend/src/features/permits/pages/Application/components/form/ConditionsTable.tsx @@ -12,46 +12,34 @@ import { import "./ConditionsTable.scss"; import { CustomExternalLink } from "../../../../../../common/components/links/CustomExternalLink"; -import { PermitType } from "../../../../types/PermitType"; -import { getDefaultConditions } from "../../../../helpers/conditions"; import { PermitCondition } from "../../../../types/PermitCondition"; export const ConditionsTable = ({ - conditionsInPermit, - permitType, - includeLcvCondition = false, + allConditions, onSetConditions, }: { - conditionsInPermit: PermitCondition[]; - permitType: PermitType; - includeLcvCondition?: boolean; + allConditions: PermitCondition[]; onSetConditions: (conditions: PermitCondition[]) => void; }) => { - const defaultConditions = getDefaultConditions(permitType, includeLcvCondition); - const allConditions = defaultConditions.map((defaultCondition) => { - // Application exists at this point, thus select all conditions that were selected in the application - const existingCondition = conditionsInPermit.find( - (c) => c.condition === defaultCondition.condition, - ); - - return { - ...defaultCondition, - checked: existingCondition - ? existingCondition.checked - : defaultCondition.checked, - }; - }); - const handleSelect = (checkedCondition: string) => { - const updatedConditions = allConditions.map((condition) => { - if (condition.condition === checkedCondition) { - condition.checked = !condition.checked; - } - return condition; - }).filter(condition => condition.checked); + const conditionInTable = allConditions.find(({ condition }) => condition === checkedCondition); + if (!conditionInTable || conditionInTable.disabled) return; - onSetConditions(updatedConditions); - } + const isConditionChecked = Boolean(conditionInTable.checked); + if (isConditionChecked) { + onSetConditions( + allConditions.filter(({ condition, checked }) => checked && condition !== checkedCondition), + ); + } else { + onSetConditions([ + ...allConditions.filter(({ checked }) => checked), + { + ...conditionInTable, + checked: true, + }, + ]); + } + }; return ( diff --git a/frontend/src/features/permits/pages/Application/components/form/LOATable.tsx b/frontend/src/features/permits/pages/Application/components/form/LOATable.tsx index 879b42739..ce6d0dd6d 100644 --- a/frontend/src/features/permits/pages/Application/components/form/LOATable.tsx +++ b/frontend/src/features/permits/pages/Application/components/form/LOATable.tsx @@ -11,12 +11,12 @@ import { } from "@mui/material"; import "./LOATable.scss"; -import { LOADetail } from "../../../../../settings/types/SpecialAuthorization"; import { applyWhenNotNullable } from "../../../../../../common/helpers/util"; import { DATE_FORMATS, toLocal } from "../../../../../../common/helpers/formatDate"; +import { PermitLOA } from "../../../../types/PermitLOA"; interface SelectableLOA { - loa: LOADetail; + loa: PermitLOA; checked: boolean; disabled: boolean; } diff --git a/frontend/src/features/permits/pages/Application/components/form/PermitDetails.tsx b/frontend/src/features/permits/pages/Application/components/form/PermitDetails.tsx index 6c9da8679..b161b6b8c 100644 --- a/frontend/src/features/permits/pages/Application/components/form/PermitDetails.tsx +++ b/frontend/src/features/permits/pages/Application/components/form/PermitDetails.tsx @@ -11,7 +11,6 @@ import { requiredMessage } from "../../../../../../common/helpers/validationMess import { ONROUTE_WEBPAGE_LINKS } from "../../../../../../routes/constants"; import { CustomExternalLink } from "../../../../../../common/components/links/CustomExternalLink"; import { BANNER_MESSAGES } from "../../../../../../common/constants/bannerMessages"; -import { PermitType } from "../../../../types/PermitType"; import { PermitCondition } from "../../../../types/PermitCondition"; import { DATE_FORMATS } from "../../../../../../common/helpers/formatDate"; import { @@ -27,25 +26,21 @@ import { export const PermitDetails = ({ feature, expiryDate, - conditionsInPermit, + allConditions, durationOptions, disableStartDate, - permitType, pastStartDateStatus, - includeLcvCondition, onSetConditions, }: { feature: string; expiryDate: Dayjs; - conditionsInPermit: PermitCondition[]; + allConditions: PermitCondition[]; durationOptions: { value: number; label: string; }[]; disableStartDate: boolean; - permitType: PermitType; pastStartDateStatus: PastStartDateStatus; - includeLcvCondition?: boolean; onSetConditions: (conditions: PermitCondition[]) => void; }) => { const formattedExpiryDate = dayjs(expiryDate).format(DATE_FORMATS.SHORT); @@ -131,9 +126,7 @@ export const PermitDetails = ({ /> 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 245f1315b..c51f818fc 100644 --- a/frontend/src/features/permits/pages/Application/components/form/PermitForm.tsx +++ b/frontend/src/features/permits/pages/Application/components/form/PermitForm.tsx @@ -6,24 +6,25 @@ import { ApplicationDetails } from "../../../../components/form/ApplicationDetai import { ContactDetails } from "../../../../components/form/ContactDetails"; import { PermitDetails } from "./PermitDetails"; import { VehicleDetails } from "./VehicleDetails/VehicleDetails"; -import { PermitLOA } from "./PermitLOA"; -import { LOADetail } from "../../../../../settings/types/SpecialAuthorization"; -import { isVehicleSubtypeLCV } from "../../../../../manageVehicles/helpers/vehicleSubtypes"; -import { getStartOfDate } from "../../../../../../common/helpers/formatDate"; +import { PermitLOASection } from "./PermitLOASection"; import { useApplicationFormContext } from "../../../../hooks/useApplicationFormContext"; import { AmendReason } from "../../../Amend/components/form/AmendReason"; import { AmendRevisionHistory } from "../../../Amend/components/form/AmendRevisionHistory"; export const PermitForm = () => { const { - formData, + permitType, + applicationNumber, + permitNumber, + startDate, + expiryDate, + currentSelectedLOAs, + vehicleFormData, + allConditions, availableDurationOptions, - powerUnitSubtypes, - trailerSubtypes, - isLcvDesignated, - ineligiblePowerUnitSubtypes, - ineligibleTrailerSubtypes, filteredVehicleOptions, + subtypeOptions, + isSelectedLOAVehicle, feature, companyInfo, isAmendAction, @@ -43,15 +44,6 @@ export const PermitForm = () => { 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[]; - return ( @@ -70,7 +62,7 @@ export const PermitForm = () => { - { @@ -97,11 +84,8 @@ export const PermitForm = () => { feature={feature} vehicleFormData={vehicleFormData} vehicleOptions={filteredVehicleOptions} - powerUnitSubtypes={powerUnitSubtypes} - trailerSubtypes={trailerSubtypes} - ineligiblePowerUnitSubtypes={ineligiblePowerUnitSubtypes} - ineligibleTrailerSubtypes={ineligibleTrailerSubtypes} - selectedLOAs={currentSelectedLOAs} + subtypeOptions={subtypeOptions} + isSelectedLOAVehicle={isSelectedLOAVehicle} onSetSaveVehicle={onToggleSaveVehicle} onSetVehicle={onSetVehicle} onClearVehicle={onClearVehicle} diff --git a/frontend/src/features/permits/pages/Application/components/form/PermitLOA.scss b/frontend/src/features/permits/pages/Application/components/form/PermitLOASection.scss similarity index 70% rename from frontend/src/features/permits/pages/Application/components/form/PermitLOA.scss rename to frontend/src/features/permits/pages/Application/components/form/PermitLOASection.scss index dea8fc6f2..f5aa24d5a 100644 --- a/frontend/src/features/permits/pages/Application/components/form/PermitLOA.scss +++ b/frontend/src/features/permits/pages/Application/components/form/PermitLOASection.scss @@ -1,10 +1,10 @@ @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"); +@include orbcStyles.permit-main-box-style(".permit-loa-section"); +@include orbcStyles.permit-left-box-style(".permit-loa-section__header"); +@include orbcStyles.permit-right-box-style(".permit-loa-section__body"); -.permit-loa { +.permit-loa-section { & &__header { h3 { padding-top: 1rem; diff --git a/frontend/src/features/permits/pages/Application/components/form/PermitLOA.tsx b/frontend/src/features/permits/pages/Application/components/form/PermitLOASection.tsx similarity index 83% rename from frontend/src/features/permits/pages/Application/components/form/PermitLOA.tsx rename to frontend/src/features/permits/pages/Application/components/form/PermitLOASection.tsx index 04d79e251..b99c1177e 100644 --- a/frontend/src/features/permits/pages/Application/components/form/PermitLOA.tsx +++ b/frontend/src/features/permits/pages/Application/components/form/PermitLOASection.tsx @@ -2,17 +2,18 @@ import { useEffect, useMemo } from "react"; import { Dayjs } from "dayjs"; import { Box, Typography } from "@mui/material"; -import "./PermitLOA.scss"; +import "./PermitLOASection.scss"; import { InfoBcGovBanner } from "../../../../../../common/components/banners/InfoBcGovBanner"; import { BANNER_MESSAGES } from "../../../../../../common/constants/bannerMessages"; -import { LOADetail } from "../../../../../settings/types/SpecialAuthorization"; +import { LOADetail } from "../../../../../settings/types/LOADetail"; 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"; +import { doUniqueArraysHaveSameItems } from "../../../../../../common/helpers/equality"; +import { PermitLOA } from "../../../../types/PermitLOA"; -export const PermitLOA = ({ +export const PermitLOASection = ({ permitType, startDate, selectedLOAs, @@ -21,9 +22,9 @@ export const PermitLOA = ({ }: { permitType: PermitType; startDate: Dayjs; - selectedLOAs: LOADetail[]; + selectedLOAs: PermitLOA[]; companyLOAs: LOADetail[]; - onUpdateLOAs: (updatedLOAs: LOADetail[]) => void, + onUpdateLOAs: (updatedLOAs: PermitLOA[]) => void, }) => { const minPermitExpiryDate = getMinPermitExpiryDate(permitType, startDate); @@ -48,7 +49,7 @@ export const PermitLOA = ({ useEffect(() => { const selectedNumbersInTable = selectedLOAsInTable.map(loa => loa.loaNumber); - if (!areArraysEqual(selectedLOANumbers, selectedNumbersInTable)) { + if (!doUniqueArraysHaveSameItems(selectedLOANumbers, selectedNumbersInTable)) { onUpdateLOAs([...selectedLOAsInTable]); } }, [selectedLOANumbers, selectedLOAsInTable]); @@ -71,14 +72,14 @@ export const PermitLOA = ({ }; return ( - - + + Letter of Authorization (LOA) - +
Select the relevant LOA(s) (optional) 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 1e0667ece..c1da9da1c 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 @@ -17,19 +17,15 @@ import { CustomFormComponent } from "../../../../../../../common/components/form import { InfoBcGovBanner } from "../../../../../../../common/components/banners/InfoBcGovBanner"; import { mapToVehicleObjectById } from "../../../../../helpers/mappers"; import { getDefaultRequiredVal } from "../../../../../../../common/helpers/util"; -import { sortVehicleSubTypes } from "../../../../../helpers/sorter"; -import { filterVehicleSubtypes } from "../../../../../helpers/permitVehicles"; import { CustomInputHTMLAttributes } from "../../../../../../../common/types/formElements"; import { SelectUnitOrPlate } from "./customFields/SelectUnitOrPlate"; 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 { selectedVehicleSubtype } from "../../../../../../manageVehicles/helpers/vehicleSubtypes"; import { PowerUnit, Trailer, - BaseVehicle, VehicleSubType, VEHICLE_TYPES, Vehicle, @@ -51,74 +47,12 @@ import { requiredMessage, } from "../../../../../../../common/helpers/validationMessages"; -const selectedVehicleSubtype = (vehicle: BaseVehicle) => { - switch (vehicle.vehicleType) { - case VEHICLE_TYPES.POWER_UNIT: - return (vehicle as PowerUnit).powerUnitTypeCode; - case VEHICLE_TYPES.TRAILER: - return (vehicle as Trailer).trailerTypeCode; - default: - return ""; - } -}; - -// Returns correct subtype options based on vehicle type -const getSubtypeOptions = ( - vehicleType: string, - powerUnitSubtypes: VehicleSubType[], - trailerSubtypes: VehicleSubType[], -) => { - if (vehicleType === VEHICLE_TYPES.POWER_UNIT) { - return [...powerUnitSubtypes]; - } - if (vehicleType === VEHICLE_TYPES.TRAILER) { - return [...trailerSubtypes]; - } - return [EMPTY_VEHICLE_SUBTYPE]; -}; - -// Returns eligible subset of subtype options to be used by select field for vehicle subtype -const getEligibleSubtypeOptions = ( - powerUnitSubtypes: VehicleSubType[], - trailerSubtypes: VehicleSubType[], - ineligiblePowerUnitSubtypes: VehicleSubType[], - ineligibleTrailerSubtypes: VehicleSubType[], - allowedLOAPowerUnitSubtypes: string[], - allowedLOATrailerSubtypes: string[], - vehicleType?: string, -) => { - if ( - vehicleType !== VEHICLE_TYPES.POWER_UNIT && - vehicleType !== VEHICLE_TYPES.TRAILER - ) { - return [EMPTY_VEHICLE_SUBTYPE]; - } - - // Sort vehicle subtypes alphabetically - const sortedVehicleSubtypes = sortVehicleSubTypes( - vehicleType, - getSubtypeOptions(vehicleType, powerUnitSubtypes, trailerSubtypes), - ); - - return filterVehicleSubtypes( - sortedVehicleSubtypes, - vehicleType, - ineligiblePowerUnitSubtypes, - ineligibleTrailerSubtypes, - allowedLOAPowerUnitSubtypes, - allowedLOATrailerSubtypes, - ); -}; - export const VehicleDetails = ({ feature, vehicleFormData, vehicleOptions, - powerUnitSubtypes, - trailerSubtypes, - ineligiblePowerUnitSubtypes, - ineligibleTrailerSubtypes, - selectedLOAs, + subtypeOptions, + isSelectedLOAVehicle, onSetSaveVehicle, onSetVehicle, onClearVehicle, @@ -126,11 +60,8 @@ export const VehicleDetails = ({ feature: string; vehicleFormData: PermitVehicleDetails; vehicleOptions: Vehicle[]; - powerUnitSubtypes: VehicleSubType[]; - trailerSubtypes: VehicleSubType[]; - ineligiblePowerUnitSubtypes: VehicleSubType[]; - ineligibleTrailerSubtypes: VehicleSubType[]; - selectedLOAs: LOADetail[]; + subtypeOptions: VehicleSubType[]; + isSelectedLOAVehicle: boolean; onSetSaveVehicle: (saveVehicle: boolean) => void; onSetVehicle: (vehicleDetails: PermitVehicleDetails) => void; onClearVehicle: (saveVehicle: boolean) => void; @@ -168,77 +99,6 @@ export const VehicleDetails = ({ const disableVehicleTypeSelect = shouldDisableVehicleTypeSelect(); - // Options for the vehicle subtype field (based on vehicle type) - const [subtypeOptions, setSubtypeOptions] = useState([ - 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( - powerUnitSubtypes, - trailerSubtypes, - ineligiblePowerUnitSubtypes, - ineligibleTrailerSubtypes, - permittedLOAPowerUnitSubtypes, - permittedLOATrailerSubtypes, - vehicleType, - ); - setSubtypeOptions(subtypes); - }, [ - powerUnitSubtypes, - trailerSubtypes, - ineligiblePowerUnitSubtypes, - ineligibleTrailerSubtypes, - vehicleType, - permittedLOAPowerUnitSubtypes, - permittedLOATrailerSubtypes, - ]); - // Set the "Save to Inventory" radio button to false on render useEffect(() => { onSetSaveVehicle(saveVehicle); @@ -314,10 +174,10 @@ export const VehicleDetails = ({ // If the selected vehicle is an LOA vehicle, it should not be edited/saved to inventory useEffect(() => { - if (isSelectedVehicleAllowedByLOA) { + if (isSelectedLOAVehicle) { setSaveVehicle(false); } - }, [isSelectedVehicleAllowedByLOA]); + }, [isSelectedLOAVehicle]); return ( @@ -367,9 +227,6 @@ export const VehicleDetails = ({ vehicleOptions={vehicleOptions} handleClearVehicle={() => onClearVehicle(saveVehicle)} handleSelectVehicle={onSelectVehicle} - ineligiblePowerUnitSubtypes={ineligiblePowerUnitSubtypes.map(({ typeCode }) => typeCode)} - ineligibleTrailerSubtypes={ineligibleTrailerSubtypes.map(({ typeCode }) => typeCode)} - loas={selectedLOAs} /> @@ -387,8 +244,8 @@ export const VehicleDetails = ({ width: formFieldStyle.width, customHelperText: "last 6 digits", }} - readOnly={isSelectedVehicleAllowedByLOA} - disabled={isSelectedVehicleAllowedByLOA} + readOnly={isSelectedLOAVehicle} + disabled={isSelectedLOAVehicle} /> ))} - readOnly={isSelectedVehicleAllowedByLOA} - disabled={isSelectedVehicleAllowedByLOA} + readOnly={isSelectedLOAVehicle} + disabled={isSelectedLOAVehicle} /> @@ -534,8 +391,8 @@ export const VehicleDetails = ({ "data-testid": "save-vehicle-yes", } as CustomInputHTMLAttributes } - readOnly={isSelectedVehicleAllowedByLOA} - disabled={isSelectedVehicleAllowedByLOA} + readOnly={isSelectedLOAVehicle} + disabled={isSelectedLOAVehicle} /> } label="Yes" @@ -550,8 +407,8 @@ export const VehicleDetails = ({ "data-testid": "save-vehicle-no", } as CustomInputHTMLAttributes } - readOnly={isSelectedVehicleAllowedByLOA} - disabled={isSelectedVehicleAllowedByLOA} + readOnly={isSelectedLOAVehicle} + disabled={isSelectedLOAVehicle} /> } 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 4505eefc4..feeab5e40 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 @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { Autocomplete, FormControl, @@ -12,12 +12,10 @@ import { import "./SelectVehicleDropdown.scss"; import { getDefaultRequiredVal } from "../../../../../../../../common/helpers/util"; import { sortVehicles } from "../../../../../../helpers/sorter"; -import { filterVehicles } from "../../../../../../helpers/permitVehicles"; 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, @@ -50,9 +48,6 @@ export const SelectVehicleDropdown = ({ vehicleOptions, handleSelectVehicle, handleClearVehicle, - ineligiblePowerUnitSubtypes, - ineligibleTrailerSubtypes, - loas, }: { chooseFrom: string; selectedVehicle: Nullable; @@ -60,18 +55,11 @@ export const SelectVehicleDropdown = ({ vehicleOptions: Vehicle[]; handleSelectVehicle: (vehicle: Vehicle) => void; handleClearVehicle: () => void; - ineligiblePowerUnitSubtypes: string[]; - ineligibleTrailerSubtypes: string[]; - loas: LOADetail[]; }) => { - const sortedVehicles = sortVehicles(chooseFrom, vehicleOptions); - - const eligibleVehicles = filterVehicles( - sortedVehicles, - ineligiblePowerUnitSubtypes, - ineligibleTrailerSubtypes, - loas, - ); + const eligibleVehicles = useMemo(() => sortVehicles( + chooseFrom, + vehicleOptions, + ), [chooseFrom, vehicleOptions]); const selectedOption = selectedVehicle ? getDefaultRequiredVal( 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 6abb845f1..851441d4e 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 @@ -79,20 +79,31 @@ export const renderTestComponent = ( const user = userEvent.setup(userEventOptions); let selectedConditions = [...conditions]; const expiryDate = getExpiryDate(startDate, duration); + const allConditions = getDefaultConditions(permitType, false) + .map(condition => { + const existingCondition = selectedConditions + .find(c => c.condition === condition.condition); + + return { + ...condition, + checked: existingCondition + ? existingCondition.checked + : condition.checked, + }; + }); + const renderedComponent = render( ({ label: duration.text, value: duration.days, }))} disableStartDate={false} - permitType={permitType} pastStartDateStatus={PAST_START_DATE_STATUSES.FAIL} - includeLcvCondition={false} onSetConditions={(updatedConditions) => { selectedConditions = [...updatedConditions]; }} 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 498be851a..e91c28ca2 100644 --- a/frontend/src/features/permits/pages/Application/components/review/PermitReview.tsx +++ b/frontend/src/features/permits/pages/Application/components/review/PermitReview.tsx @@ -18,7 +18,7 @@ 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 { PermitLOA } from "../../../../types/PermitLOA"; import { PERMIT_REVIEW_CONTEXTS, PermitReviewContext, @@ -58,7 +58,7 @@ interface PermitReviewProps { oldFields?: Nullable>; calculatedFee: string; doingBusinessAs?: Nullable; - loas?: Nullable; + loas?: Nullable; } export const PermitReview = (props: PermitReviewProps) => { diff --git a/frontend/src/features/permits/pages/Application/components/review/ReviewContactDetails.tsx b/frontend/src/features/permits/pages/Application/components/review/ReviewContactDetails.tsx index 1ba8f1fd3..473a5d03e 100644 --- a/frontend/src/features/permits/pages/Application/components/review/ReviewContactDetails.tsx +++ b/frontend/src/features/permits/pages/Application/components/review/ReviewContactDetails.tsx @@ -4,10 +4,8 @@ import "./ReviewContactDetails.scss"; import { DiffChip } from "./DiffChip"; import { Nullable } from "../../../../../../common/types/common"; import { PermitContactDetails } from "../../../../types/PermitContactDetails"; -import { - areValuesDifferent, - getDefaultRequiredVal, -} from "../../../../../../common/helpers/util"; +import { getDefaultRequiredVal } from "../../../../../../common/helpers/util"; +import { areValuesDifferent } from "../../../../../../common/helpers/equality"; const nameDisplay = (firstName?: Nullable, lastName?: Nullable) => { if (!firstName) return getDefaultRequiredVal("", lastName); diff --git a/frontend/src/features/permits/pages/Application/components/review/ReviewPermitDetails.tsx b/frontend/src/features/permits/pages/Application/components/review/ReviewPermitDetails.tsx index 486fe17c1..ff44a2fdb 100644 --- a/frontend/src/features/permits/pages/Application/components/review/ReviewPermitDetails.tsx +++ b/frontend/src/features/permits/pages/Application/components/review/ReviewPermitDetails.tsx @@ -8,11 +8,8 @@ import { DiffChip } from "./DiffChip"; import { Nullable } from "../../../../../../common/types/common"; import { PermitCondition } from "../../../../types/PermitCondition"; import { BASE_DAYS_IN_YEAR } from "../../../../constants/constants"; -import { - applyWhenNotNullable, - areValuesDifferent, -} from "../../../../../../common/helpers/util"; - +import { applyWhenNotNullable } from "../../../../../../common/helpers/util"; +import { areValuesDifferent } from "../../../../../../common/helpers/equality"; import { DATE_FORMATS, dayjsToLocalStr, diff --git a/frontend/src/features/permits/pages/Application/components/review/ReviewPermitLOAs.tsx b/frontend/src/features/permits/pages/Application/components/review/ReviewPermitLOAs.tsx index 3bf34c5a0..e0c98aa1b 100644 --- a/frontend/src/features/permits/pages/Application/components/review/ReviewPermitLOAs.tsx +++ b/frontend/src/features/permits/pages/Application/components/review/ReviewPermitLOAs.tsx @@ -3,12 +3,12 @@ import { Box, Typography } from "@mui/material"; import "./ReviewPermitLOAs.scss"; import { LOATable } from "../form/LOATable"; import { Nullable } from "../../../../../../common/types/common"; -import { LOADetail } from "../../../../../settings/types/SpecialAuthorization"; +import { PermitLOA } from "../../../../types/PermitLOA"; export const ReviewPermitLOAs = ({ loas, }: { - loas?: Nullable; + loas?: Nullable; }) => { return loas && loas.length > 0 ? ( diff --git a/frontend/src/features/permits/pages/Application/components/review/ReviewVehicleInfo.tsx b/frontend/src/features/permits/pages/Application/components/review/ReviewVehicleInfo.tsx index b7cae4459..284727a32 100644 --- a/frontend/src/features/permits/pages/Application/components/review/ReviewVehicleInfo.tsx +++ b/frontend/src/features/permits/pages/Application/components/review/ReviewVehicleInfo.tsx @@ -4,7 +4,7 @@ import { faCircleCheck } from "@fortawesome/free-regular-svg-icons"; import "./ReviewVehicleInfo.scss"; import { DiffChip } from "./DiffChip"; -import { areValuesDifferent } from "../../../../../../common/helpers/util"; +import { areValuesDifferent } from "../../../../../../common/helpers/equality"; import { Nullable } from "../../../../../../common/types/common"; import { PermitVehicleDetails } from "../../../../types/PermitVehicleDetails"; import { diff --git a/frontend/src/features/permits/types/PermitCondition.ts b/frontend/src/features/permits/types/PermitCondition.ts index 9c57bba4a..24e43ae7a 100644 --- a/frontend/src/features/permits/types/PermitCondition.ts +++ b/frontend/src/features/permits/types/PermitCondition.ts @@ -5,3 +5,11 @@ export interface PermitCondition { checked: boolean; disabled?: boolean; } + +export const arePermitConditionEqual = ( + condition1: PermitCondition, + condition2: PermitCondition, +) => { + return condition1.condition === condition2.condition + && condition1.checked === condition2.checked; +}; diff --git a/frontend/src/features/permits/types/PermitData.ts b/frontend/src/features/permits/types/PermitData.ts index c33774c62..c937ef514 100644 --- a/frontend/src/features/permits/types/PermitData.ts +++ b/frontend/src/features/permits/types/PermitData.ts @@ -4,8 +4,8 @@ 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"; +import { PermitLOA } from "./PermitLOA"; export interface PermitData { startDate: Dayjs; @@ -19,5 +19,5 @@ export interface PermitData { companyName?: Nullable; doingBusinessAs?: Nullable; clientNumber?: Nullable; - loas?: Nullable; + loas?: Nullable; } diff --git a/frontend/src/features/permits/types/PermitLOA.ts b/frontend/src/features/permits/types/PermitLOA.ts new file mode 100644 index 000000000..6d78b5af3 --- /dev/null +++ b/frontend/src/features/permits/types/PermitLOA.ts @@ -0,0 +1,41 @@ +import { areValuesDifferent, doUniqueArraysHaveSameItems } from "../../../common/helpers/equality"; +import { Nullable } from "../../../common/types/common"; +import { PermitType } from "./PermitType"; + +export interface PermitLOA { + loaId: number; + loaNumber: number; + companyId: number; + startDate: string; + expiryDate?: Nullable; + loaPermitType: PermitType[]; + powerUnits: string[]; + trailers: string[]; + originalLoaId: number; + previousLoaId?: Nullable; +} + +/** + * Determine whether or not two permit LOAs have the same details. + * @param loa1 First permit LOA + * @param loa2 Second permit LOA + * @returns Whether or not the two permit LOAs have the same details + */ +export const arePermitLOADetailsEqual = ( + 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 + && !areValuesDifferent(loa1.expiryDate, loa2.expiryDate) + && doUniqueArraysHaveSameItems(loa1.loaPermitType, loa2.loaPermitType) + && doUniqueArraysHaveSameItems(loa1.powerUnits, loa2.powerUnits) + && doUniqueArraysHaveSameItems(loa1.trailers, loa2.trailers) + && loa1.originalLoaId === loa2.originalLoaId + && !areValuesDifferent(loa1.previousLoaId, loa2.previousLoaId); +}; diff --git a/frontend/src/features/settings/apiManager/loa.ts b/frontend/src/features/settings/apiManager/loa.ts new file mode 100644 index 000000000..161f204f7 --- /dev/null +++ b/frontend/src/features/settings/apiManager/loa.ts @@ -0,0 +1,131 @@ +import { AxiosResponse } from "axios"; + +import { LOADetail } from "../types/LOADetail"; +import { LOAFormData, serializeLOAFormData } from "../types/LOAFormData"; +import { SPECIAL_AUTH_API_ROUTES } from "./endpoints/endpoints"; +import { streamDownloadFile } from "../../../common/helpers/util"; +import { + httpDELETERequest, + httpGETRequest, + httpGETRequestStream, + httpPOSTRequestWithFile, + httpPUTRequestWithFile, +} from "../../../common/apiManager/httpRequestHandler"; + +/** + * Get the LOAs for a given company. + * @param companyId Company id of the company to get LOAs for + * @param expired Whether or not to only fetch expired LOAs + * @returns LOAs for the given company + */ +export const getLOAs = async ( + companyId: number | string, + expired: boolean, +): Promise => { + const response = await httpGETRequest( + SPECIAL_AUTH_API_ROUTES.LOA.ALL(companyId, expired), + ); + return response.data; +}; + +/** + * Get the LOA detail for a specific LOA. + * @param companyId Company id of the company to get LOA for + * @param loaId id of the LOA to fetch + * @returns LOA detail for a given LOA + */ +export const getLOADetail = async ( + companyId: number | string, + loaId: number, +): Promise => { + const response = await httpGETRequest( + SPECIAL_AUTH_API_ROUTES.LOA.DETAIL(companyId, loaId), + ); + return response.data; +}; + +/** + * Create an LOA for a company. + * @param LOAData Information about the LOA to be created for the company + * @returns Result of creating the LOA, or error on fail + */ +export const createLOA = async ( + LOAData: { + companyId: number | string; + data: LOAFormData; + }, +): Promise> => { + const { companyId, data } = LOAData; + return await httpPOSTRequestWithFile( + SPECIAL_AUTH_API_ROUTES.LOA.CREATE(companyId), + serializeLOAFormData(data), + ); +}; + +/** + * Update an LOA for a company. + * @param LOAData Information about the LOA to be updated for the company + * @returns Result of updating the LOA, or error on fail + */ +export const updateLOA = async ( + LOAData: { + companyId: number | string; + loaId: number; + data: LOAFormData; + }, +): Promise> => { + const { companyId, loaId, data } = LOAData; + return await httpPUTRequestWithFile( + SPECIAL_AUTH_API_ROUTES.LOA.UPDATE(companyId, loaId), + serializeLOAFormData(data), + ); +}; + +/** + * Remove an LOA for a company. + * @param LOAData LOA id and id of the company to remove it from + * @returns Result of removing the LOA, or error on fail + */ +export const removeLOA = async ( + LOAData: { + companyId: number | string; + loaId: number; + }, +): Promise> => { + const { companyId, loaId } = LOAData; + return await httpDELETERequest( + SPECIAL_AUTH_API_ROUTES.LOA.REMOVE(companyId, loaId), + ); +}; + +/** + * Download LOA. + * @param loaId id of the LOA to download + * @param companyId id of the company that the LOA belongs to + * @returns A Promise containing the dms reference string for the LOA download stream + */ +export const downloadLOA = async ( + loaId: number, + companyId: string | number, +) => { + const url = SPECIAL_AUTH_API_ROUTES.LOA.DOWNLOAD(companyId, loaId); + const response = await httpGETRequestStream(url); + return await streamDownloadFile(response); +}; + +/** + * Remove an LOA document. + * @param LOAData LOA id and id of the company to remove it from + * @returns Result of removing the LOA document, or error on fail + */ +export const removeLOADocument = async ( + LOAData: { + companyId: number | string; + loaId: number; + }, +): Promise> => { + const { companyId, loaId } = LOAData; + return await httpDELETERequest( + SPECIAL_AUTH_API_ROUTES.LOA.REMOVE_DOCUMENT(companyId, loaId), + ); +}; diff --git a/frontend/src/features/settings/apiManager/specialAuthorization.ts b/frontend/src/features/settings/apiManager/specialAuthorization.ts index 7448be6ca..71472e957 100644 --- a/frontend/src/features/settings/apiManager/specialAuthorization.ts +++ b/frontend/src/features/settings/apiManager/specialAuthorization.ts @@ -1,137 +1,13 @@ import { AxiosResponse } from "axios"; -import { LOADetail, NoFeePermitType, SpecialAuthorizationData } from "../types/SpecialAuthorization"; -import { LOAFormData, serializeLOAFormData } from "../types/LOAFormData"; +import { NoFeePermitType, SpecialAuthorizationData } from "../types/SpecialAuthorization"; import { SPECIAL_AUTH_API_ROUTES } from "./endpoints/endpoints"; -import { streamDownloadFile } from "../../../common/helpers/util"; import { RequiredOrNull } from "../../../common/types/common"; import { - httpDELETERequest, httpGETRequest, - httpGETRequestStream, - httpPOSTRequestWithFile, httpPUTRequest, - httpPUTRequestWithFile, } from "../../../common/apiManager/httpRequestHandler"; -/** - * Get the LOAs for a given company. - * @param companyId Company id of the company to get LOAs for - * @param expired Whether or not to only fetch expired LOAs - * @returns LOAs for the given company - */ -export const getLOAs = async ( - companyId: number | string, - expired: boolean, -): Promise => { - const response = await httpGETRequest( - SPECIAL_AUTH_API_ROUTES.LOA.ALL(companyId, expired), - ); - return response.data; -}; - -/** - * Get the LOA detail for a specific LOA. - * @param companyId Company id of the company to get LOA for - * @param loaId id of the LOA to fetch - * @returns LOA detail for a given LOA - */ -export const getLOADetail = async ( - companyId: number | string, - loaId: number, -): Promise => { - const response = await httpGETRequest( - SPECIAL_AUTH_API_ROUTES.LOA.DETAIL(companyId, loaId), - ); - return response.data; -}; - -/** - * Create an LOA for a company. - * @param LOAData Information about the LOA to be created for the company - * @returns Result of creating the LOA, or error on fail - */ -export const createLOA = async ( - LOAData: { - companyId: number | string; - data: LOAFormData; - }, -): Promise> => { - const { companyId, data } = LOAData; - return await httpPOSTRequestWithFile( - SPECIAL_AUTH_API_ROUTES.LOA.CREATE(companyId), - serializeLOAFormData(data), - ); -}; - -/** - * Update an LOA for a company. - * @param LOAData Information about the LOA to be updated for the company - * @returns Result of updating the LOA, or error on fail - */ -export const updateLOA = async ( - LOAData: { - companyId: number | string; - loaId: number; - data: LOAFormData; - }, -): Promise> => { - const { companyId, loaId, data } = LOAData; - return await httpPUTRequestWithFile( - SPECIAL_AUTH_API_ROUTES.LOA.UPDATE(companyId, loaId), - serializeLOAFormData(data), - ); -}; - -/** - * Remove an LOA for a company. - * @param LOAData LOA id and id of the company to remove it from - * @returns Result of removing the LOA, or error on fail - */ -export const removeLOA = async ( - LOAData: { - companyId: number | string; - loaId: number; - }, -): Promise> => { - const { companyId, loaId } = LOAData; - return await httpDELETERequest( - SPECIAL_AUTH_API_ROUTES.LOA.REMOVE(companyId, loaId), - ); -}; - -/** - * Download LOA. - * @param loaId id of the LOA to download - * @param companyId id of the company that the LOA belongs to - * @returns A Promise containing the dms reference string for the LOA download stream - */ -export const downloadLOA = async ( - loaId: number, - companyId: string | number, -) => { - const url = SPECIAL_AUTH_API_ROUTES.LOA.DOWNLOAD(companyId, loaId); - const response = await httpGETRequestStream(url); - return await streamDownloadFile(response); -}; - -/** - * Remove an LOA document. - * @param LOAData LOA id and id of the company to remove it from - * @returns Result of removing the LOA document, or error on fail - */ -export const removeLOADocument = async ( - LOAData: { - companyId: number | string; - loaId: number; - }, -): Promise> => { - const { companyId, loaId } = LOAData; - return await httpDELETERequest( - SPECIAL_AUTH_API_ROUTES.LOA.REMOVE_DOCUMENT(companyId, loaId), - ); -}; - /** * Get the special authorizations info for a given company. * @param companyId Company id of the company to get special authorizations info for 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 06ec26564..c54381b3f 100644 --- a/frontend/src/features/settings/components/SpecialAuthorizations/LOA/expired/ExpiredLOAModal.tsx +++ b/frontend/src/features/settings/components/SpecialAuthorizations/LOA/expired/ExpiredLOAModal.tsx @@ -4,7 +4,7 @@ import { faClockRotateLeft } from "@fortawesome/free-solid-svg-icons"; import "./ExpiredLOAModal.scss"; import { LOAList } from "../list/LOAList"; -import { LOADetail } from "../../../../types/SpecialAuthorization"; +import { LOADetail } from "../../../../types/LOADetail"; export const ExpiredLOAModal = ({ showModal, 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 666f7a19c..848894506 100644 --- a/frontend/src/features/settings/components/SpecialAuthorizations/LOA/list/LOADownloadCell.tsx +++ b/frontend/src/features/settings/components/SpecialAuthorizations/LOA/list/LOADownloadCell.tsx @@ -1,7 +1,7 @@ import { MRT_Row } from "material-react-table"; import { CustomActionLink } from "../../../../../../common/components/links/CustomActionLink"; -import { LOADetail } from "../../../../types/SpecialAuthorization"; +import { LOADetail } from "../../../../types/LOADetail"; export const LOADownloadCell = ({ onDownload, 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 69d3910e8..97c14e0aa 100644 --- a/frontend/src/features/settings/components/SpecialAuthorizations/LOA/list/LOAList.tsx +++ b/frontend/src/features/settings/components/SpecialAuthorizations/LOA/list/LOAList.tsx @@ -9,7 +9,7 @@ import { } from "material-react-table"; import "./LOAList.scss"; -import { LOADetail } from "../../../../types/SpecialAuthorization"; +import { LOADetail } from "../../../../types/LOADetail"; import { LOAListColumnDef } from "./LOAListColumnDef"; import { defaultTableInitialStateOptions, 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 bec21ad11..ea09f28e4 100644 --- a/frontend/src/features/settings/components/SpecialAuthorizations/LOA/list/LOAListColumnDef.tsx +++ b/frontend/src/features/settings/components/SpecialAuthorizations/LOA/list/LOAListColumnDef.tsx @@ -1,6 +1,6 @@ import { MRT_ColumnDef, MRT_Row } from "material-react-table"; -import { LOADetail } from "../../../../types/SpecialAuthorization"; +import { LOADetail } from "../../../../types/LOADetail"; import { DATE_FORMATS, toLocal } from "../../../../../../common/helpers/formatDate"; import { applyWhenNotNullable } from "../../../../../../common/helpers/util"; import { LOANumberCell } from "./LOANumberCell"; 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 4301b5c81..67a536b67 100644 --- a/frontend/src/features/settings/components/SpecialAuthorizations/LOA/list/LOANumberCell.tsx +++ b/frontend/src/features/settings/components/SpecialAuthorizations/LOA/list/LOANumberCell.tsx @@ -1,6 +1,6 @@ import { MRT_Row } from "material-react-table"; -import { LOADetail } from "../../../../types/SpecialAuthorization"; +import { LOADetail } from "../../../../types/LOADetail"; import { CustomActionLink } from "../../../../../../common/components/links/CustomActionLink"; export const LOANumberCell = ({ diff --git a/frontend/src/features/settings/helpers/permissions.ts b/frontend/src/features/settings/helpers/permissions.ts index 7efdf5822..2a63ee5d6 100644 --- a/frontend/src/features/settings/helpers/permissions.ts +++ b/frontend/src/features/settings/helpers/permissions.ts @@ -82,6 +82,7 @@ export const canUpdateLCVFlag = ( ): boolean => { return ( userRole === USER_ROLE.HQ_ADMINISTRATOR || + userRole === USER_ROLE.SYSTEM_ADMINISTRATOR || Boolean(DoesUserHaveClaim(userClaims, CLAIMS.WRITE_LCV_FLAG)) ); }; diff --git a/frontend/src/features/settings/hooks/LOA.ts b/frontend/src/features/settings/hooks/LOA.ts index bb3d8cb6e..087a8bc6a 100644 --- a/frontend/src/features/settings/hooks/LOA.ts +++ b/frontend/src/features/settings/hooks/LOA.ts @@ -1,4 +1,8 @@ -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { + useMutation, + useQuery, + useQueryClient, +} from "@tanstack/react-query"; import { Nullable } from "../../../common/types/common"; import { @@ -8,7 +12,7 @@ import { removeLOA, removeLOADocument, updateLOA, -} from "../apiManager/specialAuthorization"; +} from "../apiManager/loa"; const QUERY_KEYS = { LOAS: (expired: boolean) => ["loas", expired], diff --git a/frontend/src/features/settings/pages/SpecialAuthorizations/SpecialAuthorizations.tsx b/frontend/src/features/settings/pages/SpecialAuthorizations/SpecialAuthorizations.tsx index 410938068..042ba3f2c 100644 --- a/frontend/src/features/settings/pages/SpecialAuthorizations/SpecialAuthorizations.tsx +++ b/frontend/src/features/settings/pages/SpecialAuthorizations/SpecialAuthorizations.tsx @@ -16,7 +16,7 @@ import { DEFAULT_NO_FEE_PERMIT_TYPE, NoFeePermitType } from "../../types/Special import { NoFeePermitsSection } from "../../components/SpecialAuthorizations/NoFeePermits/NoFeePermitsSection"; import OnRouteBCContext from "../../../../common/authentication/OnRouteBCContext"; import { LCVSection } from "../../components/SpecialAuthorizations/LCV/LCVSection"; -import { downloadLOA } from "../../apiManager/specialAuthorization"; +import { downloadLOA } from "../../apiManager/loa"; import { useFetchSpecialAuthorizations, useUpdateLCV, diff --git a/frontend/src/features/settings/types/LOADetail.ts b/frontend/src/features/settings/types/LOADetail.ts new file mode 100644 index 000000000..e990e678a --- /dev/null +++ b/frontend/src/features/settings/types/LOADetail.ts @@ -0,0 +1,38 @@ +import { Nullable } from "../../../common/types/common"; +import { PermitType } from "../../permits/types/PermitType"; + +export interface LOADetail { + loaId: number; + loaNumber: number; + companyId: number; + startDate: string; + expiryDate?: Nullable; + documentId: string; + fileName: string; + loaPermitType: PermitType[]; + comment?: Nullable; + powerUnits: string[]; + trailers: string[]; + originalLoaId: number; + previousLoaId?: Nullable; +} + +export interface CreateLOARequestData { + startDate: string; + expiryDate?: Nullable; + loaPermitType: PermitType[]; + // document: Buffer; + comment?: Nullable; + powerUnits: string[]; + trailers: string[]; +} + +export interface UpdateLOARequestData { + startDate: string; + expiryDate?: Nullable; + loaPermitType: PermitType[]; + // document?: Buffer; + comment?: Nullable; + powerUnits: string[]; + trailers: string[]; +} diff --git a/frontend/src/features/settings/types/LOAFormData.ts b/frontend/src/features/settings/types/LOAFormData.ts index 5d5d343a1..68011d7b9 100644 --- a/frontend/src/features/settings/types/LOAFormData.ts +++ b/frontend/src/features/settings/types/LOAFormData.ts @@ -3,7 +3,7 @@ import { Dayjs } from "dayjs"; import { Nullable } from "../../../common/types/common"; import { PERMIT_TYPES } from "../../permits/types/PermitType"; import { LOAVehicle } from "./LOAVehicle"; -import { LOADetail } from "./SpecialAuthorization"; +import { LOADetail } from "./LOADetail"; import { applyWhenNotNullable, getDefaultRequiredVal, diff --git a/frontend/src/features/settings/types/SpecialAuthorization.ts b/frontend/src/features/settings/types/SpecialAuthorization.ts index 4392c906c..792e38806 100644 --- a/frontend/src/features/settings/types/SpecialAuthorization.ts +++ b/frontend/src/features/settings/types/SpecialAuthorization.ts @@ -1,6 +1,4 @@ -import { areArraysEqual } from "../../../common/helpers/util"; -import { Nullable, RequiredOrNull } from "../../../common/types/common"; -import { PermitType } from "../../permits/types/PermitType"; +import { RequiredOrNull } from "../../../common/types/common"; export const NO_FEE_PERMIT_TYPES = { CA_GOVT: "CA_GOVT", @@ -29,69 +27,6 @@ export const noFeePermitTypeDescription = (noFeePermitType: NoFeePermitType) => } }; -export interface LOADetail { - loaId: number; - loaNumber: number; - companyId: number; - startDate: string; - expiryDate?: Nullable; - documentId: string; - fileName: string; - loaPermitType: PermitType[]; - comment?: Nullable; - powerUnits: string[]; - trailers: string[]; - originalLoaId: number; - previousLoaId?: Nullable; -} - -export interface CreateLOARequestData { - startDate: string; - expiryDate?: Nullable; - loaPermitType: PermitType[]; - // document: Buffer; - comment?: Nullable; - powerUnits: string[]; - trailers: string[]; -} - -export interface UpdateLOARequestData { - startDate: string; - expiryDate?: Nullable; - loaPermitType: PermitType[]; - // document?: Buffer; - comment?: Nullable; - powerUnits: string[]; - 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;