From ffa59712e2cba88184ad994b9a609365d1701a15 Mon Sep 17 00:00:00 2001
From: zgong-gov <123983557+zgong-gov@users.noreply.github.com>
Date: Thu, 28 Nov 2024 16:29:28 -0800
Subject: [PATCH 1/3] ORV2-2755 - STOS application form and review (#1674)
---
docker-compose.yml | 1 +
frontend/Dockerfile | 2 +
frontend/README.md | 1 +
frontend/package-lock.json | 93 ++++
frontend/package.json | 1 +
frontend/public/config/config.js | 3 +-
.../common/apiManager/endpoints/endpoints.ts | 3 +
.../banners/PermitExpiryDateBanner.scss | 20 +-
.../banners/PermitExpiryDateBanner.tsx | 22 +-
.../components/form/CountryAndProvince.tsx | 36 +-
.../components/form/CustomFormComponents.scss | 16 +
.../components/form/CustomFormComponents.tsx | 35 +-
.../form/subFormComponents/Autocomplete.scss | 37 ++
.../form/subFormComponents/Autocomplete.tsx | 127 +++++
.../form/subFormComponents/NumberInput.scss | 38 +-
.../form/subFormComponents/NumberInput.tsx | 160 +++++--
.../form/subFormComponents/PhoneExtInput.scss | 3 +
.../form/subFormComponents/PhoneExtInput.tsx | 45 ++
.../src/common/constants/bannerMessages.ts | 16 +-
frontend/src/common/constants/countries.ts | 283 ++++++++++-
.../constants/countries_and_states.json | 280 -----------
.../common/constants/validation_messages.json | 31 +-
.../countries/countrySupportsProvinces.ts | 15 +
.../helpers/countries/getCountryFullName.ts | 17 +
.../helpers/countries/getProvinceFullName.ts | 26 ++
frontend/src/common/helpers/equality.ts | 19 +
.../common/helpers/formatCountryProvince.ts | 37 --
.../helpers/numeric/convertToNumberIfValid.ts | 24 +
frontend/src/common/helpers/util.ts | 19 -
.../src/common/helpers/validationMessages.ts | 35 ++
frontend/src/common/types/Country.ts | 7 +
frontend/src/common/types/Province.ts | 4 +
frontend/src/common/types/common.ts | 8 +
.../table/CompanySearchResultColumnDef.tsx | 6 +-
.../apiManager/manageProfileAPI.tsx | 13 +-
.../forms/common/ReusableUserInfoForm.tsx | 4 +-
.../subForms/CompanyContactDetailsForm.tsx | 2 +-
.../subForms/CompanyPrimaryContactForm.tsx | 7 +-
.../pages/DisplayCompanyInfo.tsx | 122 +++--
.../manageProfile/pages/DisplayMyInfo.tsx | 36 +-
.../components/form/PowerUnitForm.tsx | 42 +-
.../components/form/TrailerForm.tsx | 29 +-
.../components/form/tests/helpers/access.ts | 2 +-
.../manageVehicles/helpers/vehicleSubtypes.ts | 3 +-
.../features/manageVehicles/types/Vehicle.ts | 2 +-
.../src/features/permits/ApplicationSteps.tsx | 15 +-
.../features/permits/apiManager/permitsAPI.ts | 32 +-
.../dashboard/ApplicationStepPage.tsx | 77 +--
.../integration/fixtures/getVehicleInfo.ts | 18 +-
.../tests/integration/helpers/prepare.tsx | 24 +-
.../components/form/CompanyInformation.scss | 2 +
.../components/form/CompanyInformation.tsx | 52 ++-
.../components/form/ContactDetails.scss | 45 +-
.../components/form/ContactDetails.tsx | 36 +-
.../helpers/CompanyInformation/prepare.tsx | 11 +-
.../components/permit-list/Columns.tsx | 2 +-
.../features/permits/constants/constants.ts | 49 +-
.../src/features/permits/constants/stos.ts | 68 +++
.../src/features/permits/constants/tros.json | 158 -------
.../src/features/permits/constants/tros.ts | 110 ++++-
.../src/features/permits/constants/trow.json | 326 -------------
.../src/features/permits/constants/trow.ts | 57 ++-
.../permits/context/ApplicationFormContext.ts | 40 +-
.../features/permits/helpers/conditions.ts | 5 +
.../features/permits/helpers/dateSelection.ts | 48 +-
.../src/features/permits/helpers/equality.ts | 118 ++++-
.../features/permits/helpers/feeSummary.ts | 19 +-
.../helpers/getDefaultApplicationFormData.ts | 44 +-
.../src/features/permits/helpers/mappers.ts | 99 ++--
.../src/features/permits/helpers/permitLCV.ts | 2 +-
.../src/features/permits/helpers/permitLOA.ts | 37 +-
.../permits/helpers/permitVehicles.ts | 221 ---------
.../permits/helpers/permittedCommodity.ts | 58 +++
.../permits/helpers/permittedRoute.ts | 26 ++
.../{ => serialize}/deserializeApplication.ts | 12 +-
.../{ => serialize}/deserializePermit.ts | 2 +-
.../{ => serialize}/serializeApplication.ts | 46 +-
.../helpers/serialize/serializePermitData.ts | 46 ++
.../serializePermitVehicleConfiguration.ts | 36 ++
.../serializePermitVehicleDetails.ts | 32 ++
.../src/features/permits/helpers/sorter.ts | 87 ----
.../getDefaultVehicleConfiguration.ts | 20 +
.../helpers/vehicles/getAllowedVehicles.ts | 56 +++
.../vehicles/getDefaultVehicleDetails.ts | 25 +
.../permits/helpers/vehicles/rules/gvw.ts | 37 ++
.../permits/helpers/vehicles/sortVehicles.ts | 73 +++
.../subtypes/getEligibleSubtypeOptions.ts | 72 +++
.../subtypes/getEligibleVehicleSubtypes.ts | 60 +++
.../vehicles/subtypes/sortVehicleSubtypes.ts | 20 +
.../{ => form}/useApplicationFormContext.ts | 132 ++++--
.../form/useApplicationFormUpdateMethods.ts | 113 +++++
.../hooks/form/useInitApplicationFormData.ts | 107 +++++
frontend/src/features/permits/hooks/hooks.ts | 52 +--
.../permits/hooks/useCommodityOptions.ts | 19 +
.../hooks/useInitApplicationFormData.ts | 134 ------
.../permits/hooks/usePermitVehicleForLOAs.ts | 137 ------
.../hooks/usePermitVehicleManagement.ts | 35 +-
.../permits/hooks/usePermitVehicles.ts | 185 ++++++++
.../permits/hooks/usePermittedCommodity.ts | 35 ++
.../permits/hooks/useVehicleConfiguration.ts | 84 ++++
.../Amend/components/AmendPermitForm.tsx | 167 ++++---
.../Amend/components/AmendPermitReview.tsx | 24 +-
.../pages/Amend/hooks/useAmendPermitForm.ts | 108 ++---
.../pages/Application/ApplicationForm.tsx | 250 +++++-----
.../pages/Application/ApplicationReview.tsx | 157 +++++--
.../common/PowerUnitInfoDisplay.scss | 25 +
.../common/PowerUnitInfoDisplay.tsx | 102 ++++
.../common/SelectedVehicleSubtypeList.scss | 23 +
.../common/SelectedVehicleSubtypeList.tsx | 52 +++
.../dashboard/StartApplicationAction.tsx | 39 +-
.../form/ApplicationNotesSection.scss | 31 ++
.../form/ApplicationNotesSection.tsx | 57 +++
.../ChangeCommodityTypeDialog.scss | 78 ++++
.../ChangeCommodityTypeDialog.tsx | 65 +++
.../CommodityDetailsSection.scss | 20 +
.../CommodityDetailsSection.tsx | 135 ++++++
.../LoadedDimensionsSection.scss | 25 +
.../LoadedDimensionsSection.tsx | 106 +++++
.../components/LoadedDimensionInput.tsx | 66 +++
.../components/form/PermitDetails.scss | 26 +-
.../components/form/PermitDetails.tsx | 16 +-
.../components/form/PermitForm.tsx | 55 ++-
.../components/form/PermitLOASection.scss | 8 +-
.../components/form/PermitLOASection.tsx | 12 +-
.../TripDetailsSection/HighwaySequences.scss | 88 ++++
.../TripDetailsSection/HighwaySequences.tsx | 196 ++++++++
.../SpecificRouteDetails.scss | 16 +
.../SpecificRouteDetails.tsx | 30 ++
.../TripDetailsSection.scss | 21 +
.../TripDetailsSection/TripDetailsSection.tsx | 40 ++
.../TripOriginDestination.scss | 15 +
.../TripOriginDestination.tsx | 39 ++
.../components/HighwayNumberInput.tsx | 32 ++
.../form/VehicleDetails/VehicleDetails.scss | 59 ---
.../form/VehicleDetails/VehicleDetails.tsx | 426 -----------------
.../AddPowerUnitDialog.scss | 89 ++++
.../AddPowerUnitDialog.tsx | 144 ++++++
.../VehicleInformationSection/AddTrailer.scss | 65 +++
.../VehicleInformationSection/AddTrailer.tsx | 151 ++++++
.../PowerUnitInfo.scss | 29 ++
.../PowerUnitInfo.tsx | 38 ++
.../VehicleDetails.scss | 46 ++
.../VehicleDetails.tsx | 439 ++++++++++++++++++
.../VehicleInformationSection.scss | 79 ++++
.../VehicleInformationSection.tsx | 197 ++++++++
.../components}/SelectUnitOrPlate.scss | 0
.../components}/SelectUnitOrPlate.tsx | 0
.../components}/SelectVehicleDropdown.scss | 0
.../components}/SelectVehicleDropdown.tsx | 10 +-
.../components/review/ApplicationNotes.scss | 14 +
.../components/review/ApplicationNotes.tsx | 29 ++
.../components/review/CommodityDetails.scss | 28 ++
.../components/review/CommodityDetails.tsx | 87 ++++
.../components/review/LoadedDimensions.scss | 28 ++
.../components/review/LoadedDimensions.tsx | 136 ++++++
.../components/review/PermitReview.scss | 9 +-
.../components/review/PermitReview.tsx | 90 +++-
.../components/review/ReviewActions.scss | 1 -
.../components/review/ReviewActions.tsx | 39 +-
.../review/ReviewContactDetails.scss | 3 -
.../review/ReviewContactDetails.tsx | 17 +
.../components/review/ReviewFeeSummary.scss | 9 +-
.../review/ReviewPermitDetails.scss | 20 +-
.../components/review/ReviewPermitDetails.tsx | 100 ++--
.../components/review/ReviewVehicleInfo.scss | 42 +-
.../components/review/ReviewVehicleInfo.tsx | 350 ++++++++------
.../components/review/TripDetails.scss | 52 +++
.../components/review/TripDetails.tsx | 146 ++++++
.../tests/ApplicationReview.test.tsx | 19 +-
.../helpers/ApplicationReview/prepare.tsx | 2 +-
.../pages/ShoppingCart/ShoppingCartPage.tsx | 3 -
.../src/features/permits/types/PermitData.ts | 7 +
.../types/PermitVehicleConfiguration.ts | 14 +
.../permits/types/PermitVehicleDetails.ts | 2 +
.../permits/types/PermittedCommodity.ts | 4 +
.../features/permits/types/PermittedRoute.ts | 14 +
.../policy/apiManager/endpoints/endpoints.ts | 9 +
.../src/features/policy/apiManager/policy.ts | 21 +
.../hooks/usePolicyConfigurationQuery.ts | 23 +
.../features/policy/hooks/usePolicyEngine.ts | 24 +
.../policy/types/PolicyConfiguration.ts | 9 +
.../queue/apiManager/endpoints/endpoints.ts | 2 +
.../src/features/queue/apiManager/queueAPI.ts | 21 +-
.../components/ApplicationInQueueReview.tsx | 35 +-
frontend/src/features/queue/hooks/hooks.ts | 98 ++--
.../queue/pages/ReviewApplicationInQueue.tsx | 2 +-
.../UserInformationWizardForm.tsx | 4 +-
frontend/src/routes/Routes.tsx | 27 +-
frontend/src/routes/constants.ts | 12 +
frontend/src/setupTests.ts | 1 +
frontend/src/themes/orbcStyles.scss | 25 +-
191 files changed, 7466 insertions(+), 3196 deletions(-)
create mode 100644 frontend/src/common/components/form/subFormComponents/Autocomplete.scss
create mode 100644 frontend/src/common/components/form/subFormComponents/Autocomplete.tsx
create mode 100644 frontend/src/common/components/form/subFormComponents/PhoneExtInput.scss
create mode 100644 frontend/src/common/components/form/subFormComponents/PhoneExtInput.tsx
delete mode 100644 frontend/src/common/constants/countries_and_states.json
create mode 100644 frontend/src/common/helpers/countries/countrySupportsProvinces.ts
create mode 100644 frontend/src/common/helpers/countries/getCountryFullName.ts
create mode 100644 frontend/src/common/helpers/countries/getProvinceFullName.ts
delete mode 100644 frontend/src/common/helpers/formatCountryProvince.ts
create mode 100644 frontend/src/common/helpers/numeric/convertToNumberIfValid.ts
create mode 100644 frontend/src/common/types/Country.ts
create mode 100644 frontend/src/common/types/Province.ts
create mode 100644 frontend/src/features/permits/constants/stos.ts
delete mode 100644 frontend/src/features/permits/constants/tros.json
delete mode 100644 frontend/src/features/permits/constants/trow.json
delete mode 100644 frontend/src/features/permits/helpers/permitVehicles.ts
create mode 100644 frontend/src/features/permits/helpers/permittedCommodity.ts
create mode 100644 frontend/src/features/permits/helpers/permittedRoute.ts
rename frontend/src/features/permits/helpers/{ => serialize}/deserializeApplication.ts (80%)
rename frontend/src/features/permits/helpers/{ => serialize}/deserializePermit.ts (87%)
rename frontend/src/features/permits/helpers/{ => serialize}/serializeApplication.ts (54%)
create mode 100644 frontend/src/features/permits/helpers/serialize/serializePermitData.ts
create mode 100644 frontend/src/features/permits/helpers/serialize/serializePermitVehicleConfiguration.ts
create mode 100644 frontend/src/features/permits/helpers/serialize/serializePermitVehicleDetails.ts
delete mode 100644 frontend/src/features/permits/helpers/sorter.ts
create mode 100644 frontend/src/features/permits/helpers/vehicles/configuration/getDefaultVehicleConfiguration.ts
create mode 100644 frontend/src/features/permits/helpers/vehicles/getAllowedVehicles.ts
create mode 100644 frontend/src/features/permits/helpers/vehicles/getDefaultVehicleDetails.ts
create mode 100644 frontend/src/features/permits/helpers/vehicles/rules/gvw.ts
create mode 100644 frontend/src/features/permits/helpers/vehicles/sortVehicles.ts
create mode 100644 frontend/src/features/permits/helpers/vehicles/subtypes/getEligibleSubtypeOptions.ts
create mode 100644 frontend/src/features/permits/helpers/vehicles/subtypes/getEligibleVehicleSubtypes.ts
create mode 100644 frontend/src/features/permits/helpers/vehicles/subtypes/sortVehicleSubtypes.ts
rename frontend/src/features/permits/hooks/{ => form}/useApplicationFormContext.ts (52%)
create mode 100644 frontend/src/features/permits/hooks/form/useApplicationFormUpdateMethods.ts
create mode 100644 frontend/src/features/permits/hooks/form/useInitApplicationFormData.ts
create mode 100644 frontend/src/features/permits/hooks/useCommodityOptions.ts
delete mode 100644 frontend/src/features/permits/hooks/useInitApplicationFormData.ts
delete mode 100644 frontend/src/features/permits/hooks/usePermitVehicleForLOAs.ts
create mode 100644 frontend/src/features/permits/hooks/usePermitVehicles.ts
create mode 100644 frontend/src/features/permits/hooks/usePermittedCommodity.ts
create mode 100644 frontend/src/features/permits/hooks/useVehicleConfiguration.ts
create mode 100644 frontend/src/features/permits/pages/Application/components/common/PowerUnitInfoDisplay.scss
create mode 100644 frontend/src/features/permits/pages/Application/components/common/PowerUnitInfoDisplay.tsx
create mode 100644 frontend/src/features/permits/pages/Application/components/common/SelectedVehicleSubtypeList.scss
create mode 100644 frontend/src/features/permits/pages/Application/components/common/SelectedVehicleSubtypeList.tsx
create mode 100644 frontend/src/features/permits/pages/Application/components/form/ApplicationNotesSection.scss
create mode 100644 frontend/src/features/permits/pages/Application/components/form/ApplicationNotesSection.tsx
create mode 100644 frontend/src/features/permits/pages/Application/components/form/CommodityDetailsSection/ChangeCommodityTypeDialog.scss
create mode 100644 frontend/src/features/permits/pages/Application/components/form/CommodityDetailsSection/ChangeCommodityTypeDialog.tsx
create mode 100644 frontend/src/features/permits/pages/Application/components/form/CommodityDetailsSection/CommodityDetailsSection.scss
create mode 100644 frontend/src/features/permits/pages/Application/components/form/CommodityDetailsSection/CommodityDetailsSection.tsx
create mode 100644 frontend/src/features/permits/pages/Application/components/form/LoadedDimensionsSection/LoadedDimensionsSection.scss
create mode 100644 frontend/src/features/permits/pages/Application/components/form/LoadedDimensionsSection/LoadedDimensionsSection.tsx
create mode 100644 frontend/src/features/permits/pages/Application/components/form/LoadedDimensionsSection/components/LoadedDimensionInput.tsx
create mode 100644 frontend/src/features/permits/pages/Application/components/form/TripDetailsSection/HighwaySequences.scss
create mode 100644 frontend/src/features/permits/pages/Application/components/form/TripDetailsSection/HighwaySequences.tsx
create mode 100644 frontend/src/features/permits/pages/Application/components/form/TripDetailsSection/SpecificRouteDetails.scss
create mode 100644 frontend/src/features/permits/pages/Application/components/form/TripDetailsSection/SpecificRouteDetails.tsx
create mode 100644 frontend/src/features/permits/pages/Application/components/form/TripDetailsSection/TripDetailsSection.scss
create mode 100644 frontend/src/features/permits/pages/Application/components/form/TripDetailsSection/TripDetailsSection.tsx
create mode 100644 frontend/src/features/permits/pages/Application/components/form/TripDetailsSection/TripOriginDestination.scss
create mode 100644 frontend/src/features/permits/pages/Application/components/form/TripDetailsSection/TripOriginDestination.tsx
create mode 100644 frontend/src/features/permits/pages/Application/components/form/TripDetailsSection/components/HighwayNumberInput.tsx
delete mode 100644 frontend/src/features/permits/pages/Application/components/form/VehicleDetails/VehicleDetails.scss
delete mode 100644 frontend/src/features/permits/pages/Application/components/form/VehicleDetails/VehicleDetails.tsx
create mode 100644 frontend/src/features/permits/pages/Application/components/form/VehicleInformationSection/AddPowerUnitDialog.scss
create mode 100644 frontend/src/features/permits/pages/Application/components/form/VehicleInformationSection/AddPowerUnitDialog.tsx
create mode 100644 frontend/src/features/permits/pages/Application/components/form/VehicleInformationSection/AddTrailer.scss
create mode 100644 frontend/src/features/permits/pages/Application/components/form/VehicleInformationSection/AddTrailer.tsx
create mode 100644 frontend/src/features/permits/pages/Application/components/form/VehicleInformationSection/PowerUnitInfo.scss
create mode 100644 frontend/src/features/permits/pages/Application/components/form/VehicleInformationSection/PowerUnitInfo.tsx
create mode 100644 frontend/src/features/permits/pages/Application/components/form/VehicleInformationSection/VehicleDetails.scss
create mode 100644 frontend/src/features/permits/pages/Application/components/form/VehicleInformationSection/VehicleDetails.tsx
create mode 100644 frontend/src/features/permits/pages/Application/components/form/VehicleInformationSection/VehicleInformationSection.scss
create mode 100644 frontend/src/features/permits/pages/Application/components/form/VehicleInformationSection/VehicleInformationSection.tsx
rename frontend/src/features/permits/pages/Application/components/form/{VehicleDetails/customFields => VehicleInformationSection/components}/SelectUnitOrPlate.scss (100%)
rename frontend/src/features/permits/pages/Application/components/form/{VehicleDetails/customFields => VehicleInformationSection/components}/SelectUnitOrPlate.tsx (100%)
rename frontend/src/features/permits/pages/Application/components/form/{VehicleDetails/customFields => VehicleInformationSection/components}/SelectVehicleDropdown.scss (100%)
rename frontend/src/features/permits/pages/Application/components/form/{VehicleDetails/customFields => VehicleInformationSection/components}/SelectVehicleDropdown.tsx (93%)
create mode 100644 frontend/src/features/permits/pages/Application/components/review/ApplicationNotes.scss
create mode 100644 frontend/src/features/permits/pages/Application/components/review/ApplicationNotes.tsx
create mode 100644 frontend/src/features/permits/pages/Application/components/review/CommodityDetails.scss
create mode 100644 frontend/src/features/permits/pages/Application/components/review/CommodityDetails.tsx
create mode 100644 frontend/src/features/permits/pages/Application/components/review/LoadedDimensions.scss
create mode 100644 frontend/src/features/permits/pages/Application/components/review/LoadedDimensions.tsx
create mode 100644 frontend/src/features/permits/pages/Application/components/review/TripDetails.scss
create mode 100644 frontend/src/features/permits/pages/Application/components/review/TripDetails.tsx
create mode 100644 frontend/src/features/permits/types/PermitVehicleConfiguration.ts
create mode 100644 frontend/src/features/permits/types/PermittedCommodity.ts
create mode 100644 frontend/src/features/permits/types/PermittedRoute.ts
create mode 100644 frontend/src/features/policy/apiManager/endpoints/endpoints.ts
create mode 100644 frontend/src/features/policy/apiManager/policy.ts
create mode 100644 frontend/src/features/policy/hooks/usePolicyConfigurationQuery.ts
create mode 100644 frontend/src/features/policy/hooks/usePolicyEngine.ts
create mode 100644 frontend/src/features/policy/types/PolicyConfiguration.ts
diff --git a/docker-compose.yml b/docker-compose.yml
index 46f68d2e2..44b408ad0 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -202,6 +202,7 @@ services:
args:
VITE_DEPLOY_ENVIRONMENT: ${VITE_DEPLOY_ENVIRONMENT}
VITE_API_VEHICLE_URL: ${VITE_API_VEHICLE_URL}
+ VITE_POLICY_URL: ${VITE_POLICY_URL}
VITE_KEYCLOAK_ISSUER_URL: ${VITE_KEYCLOAK_ISSUER_URL}
VITE_KEYCLOAK_AUDIENCE: ${VITE_KEYCLOAK_AUDIENCE}
VITE_SITEMINDER_LOG_OFF_URL: ${VITE_SITEMINDER_LOG_OFF_URL}
diff --git a/frontend/Dockerfile b/frontend/Dockerfile
index 94c8c5baf..885754bea 100644
--- a/frontend/Dockerfile
+++ b/frontend/Dockerfile
@@ -7,12 +7,14 @@ WORKDIR /app
#ENV NODE_ENV production
ARG VITE_DEPLOY_ENVIRONMENT
ARG VITE_API_VEHICLE_URL
+ARG VITE_POLICY_URL
ARG VITE_KEYCLOAK_ISSUER_URL
ARG VITE_KEYCLOAK_AUDIENCE
ARG VITE_SITEMINDER_LOG_OFF_URL
ENV VITE_DEPLOY_ENVIRONMENT $VITE_DEPLOY_ENVIRONMENT
ENV VITE_API_VEHICLE_URL $VITE_API_VEHICLE_URL
+ENV VITE_POLICY_URL $VITE_POLICY_URL
ENV VITE_KEYCLOAK_ISSUER_URL $VITE_KEYCLOAK_ISSUER_URL
ENV VITE_KEYCLOAK_AUDIENCE $VITE_KEYCLOAK_AUDIENCE
ENV VITE_SITEMINDER_LOG_OFF_URL $VITE_SITEMINDER_LOG_OFF_URL
diff --git a/frontend/README.md b/frontend/README.md
index 51639dbd5..a773a935c 100644
--- a/frontend/README.md
+++ b/frontend/README.md
@@ -19,6 +19,7 @@ Create a .env file in the root directory of onRouteBC and add the following vari
```conf
VITE_DEPLOY_ENVIRONMENT=local
VITE_API_VEHICLE_URL=http://localhost:5000
+VITE_POLICY_URL=http://localhost:5002
VITE_KEYCLOAK_ISSUER_URL=
VITE_KEYCLOAK_AUDIENCE=
VITE_SITEMINDER_LOG_OFF_URL=
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index fbb2494de..acb2c624f 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -26,6 +26,7 @@
"material-react-table": "^2.13.1",
"mui-nested-menu": "^3.4.0",
"oidc-client-ts": "^3.0.1",
+ "onroute-policy-engine": "^0.4.6",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-error-boundary": "^4.0.13",
@@ -4054,6 +4055,15 @@
"node": ">= 12"
}
},
+ "node_modules/clone": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
+ "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
@@ -5187,6 +5197,12 @@
"node": ">=0.10.0"
}
},
+ "node_modules/eventemitter2": {
+ "version": "6.4.9",
+ "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.9.tgz",
+ "integrity": "sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==",
+ "license": "MIT"
+ },
"node_modules/execa": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz",
@@ -5750,6 +5766,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/hash-it": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/hash-it/-/hash-it-6.0.0.tgz",
+ "integrity": "sha512-KHzmSFx1KwyMPw0kXeeUD752q/Kfbzhy6dAZrjXV9kAIXGqzGvv8vhkUqj+2MGZldTo0IBpw6v7iWE7uxsvH0w==",
+ "license": "MIT"
+ },
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
@@ -6530,6 +6552,18 @@
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="
},
+ "node_modules/json-rules-engine": {
+ "version": "6.6.0",
+ "resolved": "https://registry.npmjs.org/json-rules-engine/-/json-rules-engine-6.6.0.tgz",
+ "integrity": "sha512-jJ4eVCPnItetPiU3fTIzrrl3d2zeIXCcCy11dwWhN72YXBR2mByV1Vfbrvt6y2n+VFmxc6rtL/XhDqLKIwBx6g==",
+ "license": "ISC",
+ "dependencies": {
+ "clone": "^2.1.2",
+ "eventemitter2": "^6.4.4",
+ "hash-it": "^6.0.0",
+ "jsonpath-plus": "^7.2.0"
+ }
+ },
"node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@@ -6560,6 +6594,15 @@
"integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==",
"dev": true
},
+ "node_modules/jsonpath-plus": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-7.2.0.tgz",
+ "integrity": "sha512-zBfiUPM5nD0YZSBT/o/fbCUlCcepMIdP0CJZxM1+KgA4f2T206f6VAg9e7mX35+KlMaIc5qXW34f3BnwJ3w+RA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
"node_modules/jsx-ast-utils": {
"version": "3.3.5",
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
@@ -7381,6 +7424,16 @@
"wrappy": "1"
}
},
+ "node_modules/onroute-policy-engine": {
+ "version": "0.4.6",
+ "resolved": "https://registry.npmjs.org/onroute-policy-engine/-/onroute-policy-engine-0.4.6.tgz",
+ "integrity": "sha512-BmuZu7hP6tjreva2orZPUM6WSRxoZjheYrxQBjRBEC/oHaVXk1wUIANP0zDQAdAj2Q/Tb6yB3q+gWinofkYudQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "dayjs": "^1.11.10",
+ "json-rules-engine": "^6.5.0"
+ }
+ },
"node_modules/optionator": {
"version": "0.9.3",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
@@ -12075,6 +12128,11 @@
"integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==",
"dev": true
},
+ "clone": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
+ "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w=="
+ },
"clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
@@ -12946,6 +13004,11 @@
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
"dev": true
},
+ "eventemitter2": {
+ "version": "6.4.9",
+ "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.9.tgz",
+ "integrity": "sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg=="
+ },
"execa": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz",
@@ -13340,6 +13403,11 @@
"has-symbols": "^1.0.3"
}
},
+ "hash-it": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/hash-it/-/hash-it-6.0.0.tgz",
+ "integrity": "sha512-KHzmSFx1KwyMPw0kXeeUD752q/Kfbzhy6dAZrjXV9kAIXGqzGvv8vhkUqj+2MGZldTo0IBpw6v7iWE7uxsvH0w=="
+ },
"hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
@@ -13898,6 +13966,17 @@
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="
},
+ "json-rules-engine": {
+ "version": "6.6.0",
+ "resolved": "https://registry.npmjs.org/json-rules-engine/-/json-rules-engine-6.6.0.tgz",
+ "integrity": "sha512-jJ4eVCPnItetPiU3fTIzrrl3d2zeIXCcCy11dwWhN72YXBR2mByV1Vfbrvt6y2n+VFmxc6rtL/XhDqLKIwBx6g==",
+ "requires": {
+ "clone": "^2.1.2",
+ "eventemitter2": "^6.4.4",
+ "hash-it": "^6.0.0",
+ "jsonpath-plus": "^7.2.0"
+ }
+ },
"json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@@ -13922,6 +14001,11 @@
"integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==",
"dev": true
},
+ "jsonpath-plus": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-7.2.0.tgz",
+ "integrity": "sha512-zBfiUPM5nD0YZSBT/o/fbCUlCcepMIdP0CJZxM1+KgA4f2T206f6VAg9e7mX35+KlMaIc5qXW34f3BnwJ3w+RA=="
+ },
"jsx-ast-utils": {
"version": "3.3.5",
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
@@ -14518,6 +14602,15 @@
"wrappy": "1"
}
},
+ "onroute-policy-engine": {
+ "version": "0.4.6",
+ "resolved": "https://registry.npmjs.org/onroute-policy-engine/-/onroute-policy-engine-0.4.6.tgz",
+ "integrity": "sha512-BmuZu7hP6tjreva2orZPUM6WSRxoZjheYrxQBjRBEC/oHaVXk1wUIANP0zDQAdAj2Q/Tb6yB3q+gWinofkYudQ==",
+ "requires": {
+ "dayjs": "^1.11.10",
+ "json-rules-engine": "^6.5.0"
+ }
+ },
"optionator": {
"version": "0.9.3",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index b068b137f..6e2a5b0fb 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -21,6 +21,7 @@
"material-react-table": "^2.13.1",
"mui-nested-menu": "^3.4.0",
"oidc-client-ts": "^3.0.1",
+ "onroute-policy-engine": "^0.4.6",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-error-boundary": "^4.0.13",
diff --git a/frontend/public/config/config.js b/frontend/public/config/config.js
index 2501a3134..ef8ccfa88 100644
--- a/frontend/public/config/config.js
+++ b/frontend/public/config/config.js
@@ -21,6 +21,7 @@ const envConfig = (() => {
VITE_KEYCLOAK_ISSUER_URL: "",
VITE_KEYCLOAK_AUDIENCE: "",
VITE_SITEMINDER_LOG_OFF_URL: "",
- VITE_FRONTEND_PR_NUM: ""
+ VITE_FRONTEND_PR_NUM: "",
+ VITE_POLICY_URL: "",
};
})();
diff --git a/frontend/src/common/apiManager/endpoints/endpoints.ts b/frontend/src/common/apiManager/endpoints/endpoints.ts
index 4eec5510e..75bdafc62 100644
--- a/frontend/src/common/apiManager/endpoints/endpoints.ts
+++ b/frontend/src/common/apiManager/endpoints/endpoints.ts
@@ -1,2 +1,5 @@
export const VEHICLES_URL =
import.meta.env.VITE_API_VEHICLE_URL || envConfig.VITE_API_VEHICLE_URL;
+
+export const POLICY_URL =
+ import.meta.env.VITE_POLICY_URL || envConfig.VITE_POLICY_URL;
diff --git a/frontend/src/common/components/banners/PermitExpiryDateBanner.scss b/frontend/src/common/components/banners/PermitExpiryDateBanner.scss
index 56bd21a08..a40391770 100644
--- a/frontend/src/common/components/banners/PermitExpiryDateBanner.scss
+++ b/frontend/src/common/components/banners/PermitExpiryDateBanner.scss
@@ -3,10 +3,18 @@
.permit-expiry-date-banner {
background-color: $banner-grey;
color: $bc-primary-blue;
- margin-top: 1.25rem;
- padding: 1.5em;
- display: flex;
- align-items: center;
- justify-content: space-between;
- max-width: 270px;
+ padding: 1.5rem;
+ width: fit-content;
+ width: -moz-fit-content; /* For Firefox, Firefox Android */
+
+ & &__label {
+ font-size: 0.875rem;
+ margin: 0;
+ }
+
+ & &__expiry-date {
+ margin: 0.25rem 0 0 0;
+ font-size: 1.5rem;
+ font-weight: bold;
+ }
}
diff --git a/frontend/src/common/components/banners/PermitExpiryDateBanner.tsx b/frontend/src/common/components/banners/PermitExpiryDateBanner.tsx
index 3f975e607..48df51b80 100644
--- a/frontend/src/common/components/banners/PermitExpiryDateBanner.tsx
+++ b/frontend/src/common/components/banners/PermitExpiryDateBanner.tsx
@@ -1,5 +1,3 @@
-import { Box, Typography } from "@mui/material";
-
import "./PermitExpiryDateBanner.scss";
export const PermitExpiryDateBanner = ({
@@ -8,13 +6,17 @@ export const PermitExpiryDateBanner = ({
expiryDate: string;
}) => {
return (
-
-
- PERMIT EXPIRY DATE
-
- {expiryDate}
-
-
-
+
+
+ PERMIT EXPIRY DATE
+
+
+
+ {expiryDate}
+
+
);
};
diff --git a/frontend/src/common/components/form/CountryAndProvince.tsx b/frontend/src/common/components/form/CountryAndProvince.tsx
index 398e55681..93dd563d6 100644
--- a/frontend/src/common/components/form/CountryAndProvince.tsx
+++ b/frontend/src/common/components/form/CountryAndProvince.tsx
@@ -2,9 +2,9 @@ import { useFormContext, FieldPath } from "react-hook-form";
import { useCallback, useEffect, useState } from "react";
import { SelectChangeEvent } from "@mui/material/Select";
import MenuItem from "@mui/material/MenuItem";
-import { COUNTRIES_THAT_SUPPORT_PROVINCE } from "../../constants/countries";
-import CountriesAndStates from "../../constants/countries_and_states.json";
+import { COUNTRIES } from "../../constants/countries";
+import { countrySupportsProvinces } from "../../helpers/countries/countrySupportsProvinces";
import { DEFAULT_WIDTH } from "../../../themes/bcGovStyles";
import { CustomFormComponent } from "./CustomFormComponents";
import { Nullable, ORBC_FormTypes } from "../../types/common";
@@ -14,32 +14,11 @@ import {
requiredMessage,
} from "../../helpers/validationMessages";
-/**
- * The props that can be passed to the country and provinces subsection of a form.
- */
interface CountryAndProvinceProps {
- /**
- * Name of the feature that the field belongs to.
- * This name is used for Id's and keys.
- * Example: feature={"profile"}
- */
feature: string;
-
- /**
- * The value for the width of the select box
- */
width?: string;
-
- /**
- * Name used for the API call. Example: countryField="primaryContact.countryCode"
- */
countryField: string;
provinceField: string;
-
- /**
- * Boolean for react hook form rules. Example-> rules: { required: isCountryRequired }
- * Should default to true
- */
isCountryRequired?: boolean;
isProvinceRequired?: boolean;
countryClassName?: string;
@@ -50,9 +29,6 @@ interface CountryAndProvinceProps {
/**
* The CountryAndProvince component provides form fields for country and province.
- * This implementation uses {@link COUNTRIES_THAT_SUPPORT_PROVINCE} to display or hide
- * the province field.
- *
* @returns A react component with the country and province fields.
*/
export const CountryAndProvince = ({
@@ -68,10 +44,6 @@ export const CountryAndProvince = ({
readOnly,
}: CountryAndProvinceProps): JSX.Element => {
const { resetField, watch, setValue } = useFormContext();
-
- const countrySupportsProvinces = (country: string) =>
- COUNTRIES_THAT_SUPPORT_PROVINCE.includes(country);
-
const countrySelected = watch(countryField);
const provinceSelected = watch(provinceField);
@@ -130,7 +102,7 @@ export const CountryAndProvince = ({
* @param selectedCountry string representing the country
*/
const getProvinces = useCallback(function (selectedCountry: string) {
- return CountriesAndStates.filter(
+ return COUNTRIES.filter(
(country) => country.code === selectedCountry,
).flatMap((country) => country.states);
}, []);
@@ -173,7 +145,7 @@ export const CountryAndProvince = ({
label: "Country",
width: width,
}}
- menuOptions={CountriesAndStates.map((country) => (
+ menuOptions={COUNTRIES.map((country) => (
{country.name}
diff --git a/frontend/src/common/components/form/CustomFormComponents.scss b/frontend/src/common/components/form/CustomFormComponents.scss
index 1e2ead4f5..03ff93237 100644
--- a/frontend/src/common/components/form/CustomFormComponents.scss
+++ b/frontend/src/common/components/form/CustomFormComponents.scss
@@ -1,5 +1,21 @@
@import "../../../themes/orbcStyles";
+.custom-form-control {
+ .custom-form-components & {
+ width: 100%;
+ }
+
+ & &__label {
+ color: $bc-black;
+ font-weight: bold;
+ margin-bottom: 0.5rem;
+
+ .custom-form-components &--error {
+ color: $bc-black;
+ }
+ }
+}
+
@mixin custom-form-component($component) {
#{$component} {
{&}--disabled {
diff --git a/frontend/src/common/components/form/CustomFormComponents.tsx b/frontend/src/common/components/form/CustomFormComponents.tsx
index 93d003719..6070e84ef 100644
--- a/frontend/src/common/components/form/CustomFormComponents.tsx
+++ b/frontend/src/common/components/form/CustomFormComponents.tsx
@@ -7,18 +7,19 @@ import {
useFormContext,
} from "react-hook-form";
+import "./CustomFormComponents.scss";
import { ORBC_FormTypes } from "../../types/common";
import { CustomOutlinedInput } from "./subFormComponents/CustomOutlinedInput";
import { CustomSelect } from "./subFormComponents/CustomSelect";
import { PhoneNumberInput } from "./subFormComponents/PhoneNumberInput";
import { CustomTextArea } from "./subFormComponents/CustomTextArea";
-import { NumberInput } from "./subFormComponents/NumberInput";
+import { PhoneExtInput } from "./subFormComponents/PhoneExtInput";
/**
* Properties of onRouteBC custom form components
*/
export interface CustomFormComponentProps {
- type: "input" | "select" | "phone" | "textarea" | "number";
+ type: "input" | "select" | "phone" | "textarea" | "ext";
feature: string;
options: CustomFormOptionsProps;
menuOptions?: JSX.Element[];
@@ -158,9 +159,9 @@ export const CustomFormComponent = ({
readOnly={readOnly}
/>
);
- case "number":
+ case "ext":
return (
- ({
};
return (
-
+
({
className="custom-form-control"
margin="normal"
error={invalid}
- sx={{ width: "100%" }}
>
{label}
- {showOptionalLabel() && (
+ {showOptionalLabel() ? (
(optional)
- )}
- {customHelperText && (
+ ) : null}
+ {customHelperText ? (
{` (${customHelperText})`}
- )}
+ ) : null}
+
{renderSubFormComponent(invalid)}
- {invalid && (
+
+ {invalid ? (
({
>
{getErrorMessage(errors, name)}
- )}
+ ) : null}
)}
/>
diff --git a/frontend/src/common/components/form/subFormComponents/Autocomplete.scss b/frontend/src/common/components/form/subFormComponents/Autocomplete.scss
new file mode 100644
index 000000000..92cf93338
--- /dev/null
+++ b/frontend/src/common/components/form/subFormComponents/Autocomplete.scss
@@ -0,0 +1,37 @@
+@use "../../../../themes/orbcStyles";
+
+.autocomplete {
+ & &__label {
+ color: orbcStyles.$bc-black;
+ font-weight: bold;
+ margin: 0 0 0.5rem 0;
+ }
+
+ {&}--error &__label {
+ color: orbcStyles.$bc-black;
+ }
+
+ {&}--error &__autocomplete {
+ &:hover {
+ fieldset {
+ border: 2px solid orbcStyles.$bc-red;
+ }
+ }
+
+ fieldset {
+ border: 2px solid orbcStyles.$bc-red;
+ }
+ }
+
+ & &__autocomplete {
+ fieldset {
+ border: 2px solid orbcStyles.$bc-text-box-border-grey;
+ }
+
+ &:focus-within {
+ fieldset {
+ border: 2px solid orbcStyles.$focus-blue;
+ }
+ }
+ }
+}
diff --git a/frontend/src/common/components/form/subFormComponents/Autocomplete.tsx b/frontend/src/common/components/form/subFormComponents/Autocomplete.tsx
new file mode 100644
index 000000000..7e058eff1
--- /dev/null
+++ b/frontend/src/common/components/form/subFormComponents/Autocomplete.tsx
@@ -0,0 +1,127 @@
+import {
+ Autocomplete as MuiAutocomplete,
+ AutocompleteProps as MuiAutocompleteProps,
+ FormControl,
+ FormHelperText,
+ FormLabel,
+ TextField,
+} from "@mui/material";
+
+import "./Autocomplete.scss";
+import { getDefaultRequiredVal } from "../../../helpers/util";
+
+type AutocompleteClassKey =
+ "root" | "label";
+
+export interface AutocompleteProps<
+ Value,
+ Multiple extends boolean | undefined,
+ DisableClearable extends boolean | undefined,
+ FreeSolo extends boolean | undefined,
+ ChipComponent extends React.ElementType,
+> {
+ classes?: Partial>;
+ label?: {
+ id: string;
+ component: React.ReactNode;
+ };
+ helperText?: {
+ messages?: string[];
+ errors?: string[];
+ };
+ autocompleteProps: Omit<
+ MuiAutocompleteProps,
+ "renderInput"
+ >;
+}
+
+export const Autocomplete = <
+ Value,
+ Multiple extends boolean | undefined = false,
+ DisableClearable extends boolean | undefined = false,
+ FreeSolo extends boolean | undefined = false,
+ ChipComponent extends React.ElementType = "div",
+>(props: AutocompleteProps<
+ Value,
+ Multiple,
+ DisableClearable,
+ FreeSolo,
+ ChipComponent
+>) => {
+ const helperMessages = getDefaultRequiredVal([], props.helperText?.messages);
+ const errorMessages = getDefaultRequiredVal([], props.helperText?.errors);
+ const helperTexts = [
+ ...helperMessages.map(message => ({
+ type: "message",
+ message,
+ })),
+ ...errorMessages.map(message => ({
+ type: "error",
+ message,
+ })),
+ ];
+
+ return (
+ 0 ? "autocomplete--error" : ""
+ }
+ `}
+ error={errorMessages.length > 0}
+ >
+ {props.label ? (
+
+ {props.label.component}
+
+ ) : null}
+
+ (
+
+ )}
+ />
+
+ {helperTexts.length > 0 ? (
+
+ {helperTexts.map(({ message, type }) => (
+
+ {message}
+
+ ))}
+
+ ) : null}
+
+ );
+};
diff --git a/frontend/src/common/components/form/subFormComponents/NumberInput.scss b/frontend/src/common/components/form/subFormComponents/NumberInput.scss
index dd3db6702..66e1fd6d8 100644
--- a/frontend/src/common/components/form/subFormComponents/NumberInput.scss
+++ b/frontend/src/common/components/form/subFormComponents/NumberInput.scss
@@ -1,3 +1,37 @@
-@use "../CustomFormComponents";
+@use "../../../../themes/orbcStyles";
-@include CustomFormComponents.custom-form-component(".custom-number-input");
+.number-input {
+ & &__label {
+ color: orbcStyles.$bc-black;
+ font-weight: bold;
+ margin: 0 0 0.5rem 0;
+ }
+
+ {&}--error &__label {
+ color: orbcStyles.$bc-black;
+ }
+
+ {&}--error &__input {
+ &:hover {
+ fieldset {
+ border: 2px solid orbcStyles.$bc-red;
+ }
+ }
+
+ fieldset {
+ border: 2px solid orbcStyles.$bc-red;
+ }
+ }
+
+ & &__input {
+ fieldset {
+ border: 2px solid orbcStyles.$bc-text-box-border-grey;
+ }
+
+ &:focus-within {
+ fieldset {
+ border: 2px solid orbcStyles.$focus-blue;
+ }
+ }
+ }
+}
diff --git a/frontend/src/common/components/form/subFormComponents/NumberInput.tsx b/frontend/src/common/components/form/subFormComponents/NumberInput.tsx
index 819f2284d..73275e861 100644
--- a/frontend/src/common/components/form/subFormComponents/NumberInput.tsx
+++ b/frontend/src/common/components/form/subFormComponents/NumberInput.tsx
@@ -1,42 +1,140 @@
-import { OutlinedInput } from "@mui/material";
-import { useFormContext } from "react-hook-form";
-import { ORBC_FormTypes } from "../../../types/common";
-import { CustomOutlinedInputProps } from "./CustomOutlinedInput";
+import { useState } from "react";
+import {
+ OutlinedInput,
+ OutlinedInputProps,
+ FormControl,
+ FormHelperText,
+ FormLabel,
+} from "@mui/material";
+
import "./NumberInput.scss";
+import { applyWhenNotNullable, getDefaultRequiredVal } from "../../../helpers/util";
+import { convertToNumberIfValid } from "../../../helpers/numeric/convertToNumberIfValid";
+import { isNull, RequiredOrNull } from "../../../types/common";
+
+type NumberInputClassKey =
+ "root" | "label";
-/**
- * An onRouteBC customized MUI OutlineInput component
- * that automatically filters out non-numeric values as the user types
- */
-export const NumberInput = (
- props: CustomOutlinedInputProps,
-): JSX.Element => {
- const { register, setValue } = useFormContext();
- /**
- * Function to prevent non-numeric input as the user types
- */
- const filterNonNumericValue = (input?: string) => {
- if (!input) return "";
- // only allows 0-9 inputs
- return input.replace(/[^\d]/g, "");
+export interface NumberInputProps {
+ classes?: Partial>;
+ label?: {
+ id: string;
+ component: React.ReactNode;
+ };
+ helperText?: {
+ messages?: string[];
+ errors?: string[];
+ };
+ inputProps: Omit & {
+ value: RequiredOrNull;
+ maskFn?: (numericVal: number) => string;
};
+}
+
+export const NumberInput = (props: NumberInputProps) => {
+ const helperMessages = getDefaultRequiredVal([], props.helperText?.messages);
+ const errorMessages = getDefaultRequiredVal([], props.helperText?.errors);
+ const helperTexts = [
+ ...helperMessages.map(message => ({
+ type: "message",
+ message,
+ })),
+ ...errorMessages.map(message => ({
+ type: "error",
+ message,
+ })),
+ ];
+
+ const { maskFn, onChange, onBlur, ...inputProps} = props.inputProps;
+ const inputSlotProps = inputProps.slotProps?.input;
+ const initialValueDisplay = applyWhenNotNullable(
+ (num) => maskFn ? maskFn(num) : `${num}`,
+ inputProps.value,
+ "",
+ );
+
+ const [valueDisplay, setValueDisplay] = useState(initialValueDisplay);
- // Everytime the user types, update the format of the users input
const handleChange = (e: React.ChangeEvent) => {
- const formattedValue = filterNonNumericValue(e.target.value);
- setValue(props.name, formattedValue, { shouldValidate: true });
+ const updatedVal = e.target.value;
+ const numericVal = convertToNumberIfValid(updatedVal, null);
+
+ // If an invalid numeric string was inputted, do nothing
+ if (isNull(numericVal)) return;
+
+ // Otherwise display it without formatting it immediately (as that affects user's ability to input)
+ setValueDisplay(updatedVal);
+ onChange?.(e);
};
- const className = `custom-phone-input ${props.disabled ? "custom-phone-input--disabled" : ""} ${props.invalid ? "custom-phone-input--invalid" : ""}`;
+ // The user is free to enter numbers into the input field,
+ // but as they leave the input will be formatted if a maskFn is available
+ const handleBlur = (e: React.FocusEvent) => {
+ const num = convertToNumberIfValid(valueDisplay, null);
+ if (maskFn && !isNull(num)) {
+ setValueDisplay(maskFn(num));
+ }
+ onBlur?.(e);
+ };
return (
-
+ 0 ? "number-input--error" : ""
+ }
+ `}
+ error={errorMessages.length > 0}
+ >
+ {props.label ? (
+
+ {props.label.component}
+
+ ) : null}
+
+
+
+ {helperTexts.length > 0 ? (
+
+ {helperTexts.map(({ message, type }) => (
+
+ {message}
+
+ ))}
+
+ ) : null}
+
);
};
diff --git a/frontend/src/common/components/form/subFormComponents/PhoneExtInput.scss b/frontend/src/common/components/form/subFormComponents/PhoneExtInput.scss
new file mode 100644
index 000000000..f21a8204b
--- /dev/null
+++ b/frontend/src/common/components/form/subFormComponents/PhoneExtInput.scss
@@ -0,0 +1,3 @@
+@use "../CustomFormComponents";
+
+@include CustomFormComponents.custom-form-component(".phone-ext-input");
diff --git a/frontend/src/common/components/form/subFormComponents/PhoneExtInput.tsx b/frontend/src/common/components/form/subFormComponents/PhoneExtInput.tsx
new file mode 100644
index 000000000..79d10f6ad
--- /dev/null
+++ b/frontend/src/common/components/form/subFormComponents/PhoneExtInput.tsx
@@ -0,0 +1,45 @@
+import { OutlinedInput } from "@mui/material";
+import { useFormContext } from "react-hook-form";
+
+import "./PhoneExtInput.scss";
+import { ORBC_FormTypes } from "../../../types/common";
+import { CustomOutlinedInputProps } from "./CustomOutlinedInput";
+
+/**
+ * Input component used for phone number extensions.
+ */
+export const PhoneExtInput = (
+ props: CustomOutlinedInputProps,
+): JSX.Element => {
+ const { register, setValue } = useFormContext();
+
+ // Automatically prevent non-digit input as the user types
+ const filterNonNumericValue = (input?: string) => {
+ if (!input) return "";
+
+ // Only allows 0-9 inputs
+ return input.replace(/[^\d]/g, "");
+ };
+
+ // Everytime the user types, update the format of the users input
+ const handleChange = (e: React.ChangeEvent) => {
+ const formattedValue = filterNonNumericValue(e.target.value);
+ setValue(props.name, formattedValue, { shouldValidate: true });
+ };
+
+ const className = `
+ phone-ext-input ${props.disabled ? "phone-ext-input--disabled" : ""}
+ ${props.invalid ? "phone-ext-input--invalid" : ""}
+ `;
+
+ return (
+
+ );
+};
diff --git a/frontend/src/common/constants/bannerMessages.ts b/frontend/src/common/constants/bannerMessages.ts
index c4e187dfd..aed2abb7a 100644
--- a/frontend/src/common/constants/bannerMessages.ts
+++ b/frontend/src/common/constants/bannerMessages.ts
@@ -10,7 +10,11 @@ export const BANNER_MESSAGES = {
PERMIT_REFUND_REQUEST: `Refunds and amendments can be requested over the phone by calling the Provincial Permit Centre at Toll-free: ${TOLL_FREE_NUMBER}. Please have your permit number ready.`,
POLICY_REMINDER:
"The applicant is responsible for ensuring they are following Legislation, policies, standards and guidelines in the operation of a commercial transportation business in British Columbia.",
- CANNOT_FIND_VEHICLE: "Can't find a vehicle from your inventory?",
+ CANNOT_FIND_VEHICLE: {
+ TITLE: "Can't find a vehicle from your inventory?",
+ DETAIL: "Your vehicle may not be available in a permit application because it cannot be used for the type of permit you are applying for.",
+ INELIGIBLE_SUBTYPES: "If you are creating a new vehicle, a desired Vehicle Sub-Type may not be available because it is not eligible for the permit application you are currently in.",
+ },
ISSUED_PERMIT_NUMBER_7_YEARS:
"Enter any Permit No. issued to the above Client No. in the last 7 years",
SELECT_VEHICLES_LOA:
@@ -23,4 +27,14 @@ export const BANNER_MESSAGES = {
"Vehicle details cannot be edited in the permit application if you are using an LOA.",
REJECTED_APPLICATIONS:
"Rejected applications appear in Applications in Progress.",
+ APPLICATION_NOTES:
+ "Application notes can provide additional details to the Provincial Permit Centre when submitting a permit application for review.",
+ APPLICATION_NOTES_EXAMPLE:
+ "e.g. Use the credit account for payment.",
+ APPLICATION_NOTES_INFO:
+ "Application notes will not appear on the permit document.",
+ HIGHWAY_SEQUENCES: {
+ TITLE: "The sequence of highways should be in order of travel.",
+ EXAMPLE: "e.g. If the origin is Victoria, BC and the destination is Hope, BC, the sequence of highways travelled in order will be 17 1 3.",
+ },
};
diff --git a/frontend/src/common/constants/countries.ts b/frontend/src/common/constants/countries.ts
index 7b8f34541..726a0889d 100644
--- a/frontend/src/common/constants/countries.ts
+++ b/frontend/src/common/constants/countries.ts
@@ -1 +1,282 @@
-export const COUNTRIES_THAT_SUPPORT_PROVINCE = ["US", "CA"];
+import { Country } from "../types/Country";
+
+export const COUNTRIES: Country[] = [
+ {
+ name: "Canada",
+ code: "CA",
+ states: [
+ {
+ name: "Alberta",
+ code: "AB",
+ },
+ {
+ name: "British Columbia",
+ code: "BC",
+ },
+ {
+ name: "Manitoba",
+ code: "MB",
+ },
+ {
+ name: "New Brunswick",
+ code: "NB",
+ },
+ {
+ name: "Newfoundland and Labrador",
+ code: "NL",
+ },
+ {
+ name: "Northwest Territories",
+ code: "NT",
+ },
+ {
+ name: "Nova Scotia",
+ code: "NS",
+ },
+ {
+ name: "Nunavut",
+ code: "NU",
+ },
+ {
+ name: "Ontario",
+ code: "ON",
+ },
+ {
+ name: "Prince Edward Island",
+ code: "PE",
+ },
+ {
+ name: "Quebec",
+ code: "QC",
+ },
+ {
+ name: "Saskatchewan",
+ code: "SK",
+ },
+ {
+ name: "Yukon",
+ code: "YT",
+ },
+ ],
+ },
+ {
+ name: "United States",
+ code: "US",
+ states: [
+ {
+ name: "Alabama",
+ code: "AL",
+ },
+ {
+ name: "Alaska",
+ code: "AK",
+ },
+ {
+ name: "Arizona",
+ code: "AZ",
+ },
+ {
+ name: "Arkansas",
+ code: "AR",
+ },
+ {
+ name: "California",
+ code: "CA",
+ },
+ {
+ name: "Colorado",
+ code: "CO",
+ },
+ {
+ name: "Connecticut",
+ code: "CT",
+ },
+ {
+ name: "Delaware",
+ code: "DE",
+ },
+ {
+ name: "District Of Columbia",
+ code: "DC",
+ },
+ {
+ name: "Florida",
+ code: "FL",
+ },
+ {
+ name: "Georgia",
+ code: "GA",
+ },
+ {
+ name: "Hawaii",
+ code: "HI",
+ },
+ {
+ name: "Idaho",
+ code: "ID",
+ },
+ {
+ name: "Illinois",
+ code: "IL",
+ },
+ {
+ name: "Indiana",
+ code: "IN",
+ },
+ {
+ name: "Iowa",
+ code: "IA",
+ },
+ {
+ name: "Kansas",
+ code: "KS",
+ },
+ {
+ name: "Kentucky",
+ code: "KY",
+ },
+ {
+ name: "Louisiana",
+ code: "LA",
+ },
+ {
+ name: "Maine",
+ code: "ME",
+ },
+ {
+ name: "Maryland",
+ code: "MD",
+ },
+ {
+ name: "Massachusetts",
+ code: "MA",
+ },
+ {
+ name: "Michigan",
+ code: "MI",
+ },
+ {
+ name: "Minnesota",
+ code: "MN",
+ },
+ {
+ name: "Mississippi",
+ code: "MS",
+ },
+ {
+ name: "Missouri",
+ code: "MO",
+ },
+ {
+ name: "Montana",
+ code: "MT",
+ },
+ {
+ name: "Nebraska",
+ code: "NE",
+ },
+ {
+ name: "Nevada",
+ code: "NV",
+ },
+ {
+ name: "New Hampshire",
+ code: "NH",
+ },
+ {
+ name: "New Jersey",
+ code: "NJ",
+ },
+ {
+ name: "New Mexico",
+ code: "NM",
+ },
+ {
+ name: "New York",
+ code: "NY",
+ },
+ {
+ name: "North Carolina",
+ code: "NC",
+ },
+ {
+ name: "North Dakota",
+ code: "ND",
+ },
+ {
+ name: "Ohio",
+ code: "OH",
+ },
+ {
+ name: "Oklahoma",
+ code: "OK",
+ },
+ {
+ name: "Oregon",
+ code: "OR",
+ },
+ {
+ name: "Pennsylvania",
+ code: "PA",
+ },
+ {
+ name: "Rhode Island",
+ code: "RI",
+ },
+ {
+ name: "South Carolina",
+ code: "SC",
+ },
+ {
+ name: "South Dakota",
+ code: "SD",
+ },
+ {
+ name: "Tennessee",
+ code: "TN",
+ },
+ {
+ name: "Texas",
+ code: "TX",
+ },
+ {
+ name: "Utah",
+ code: "UT",
+ },
+ {
+ name: "Vermont",
+ code: "VT",
+ },
+ {
+ name: "Virginia",
+ code: "VA",
+ },
+ {
+ name: "Washington",
+ code: "WA",
+ },
+ {
+ name: "West Virginia",
+ code: "WV",
+ },
+ {
+ name: "Wisconsin",
+ code: "WI",
+ },
+ {
+ name: "Wyoming",
+ code: "WY",
+ },
+ ],
+ },
+ {
+ name: "Mexico",
+ code: "MX",
+ states: [],
+ },
+ {
+ name: "Other",
+ code: "XX",
+ states: [],
+ },
+];
diff --git a/frontend/src/common/constants/countries_and_states.json b/frontend/src/common/constants/countries_and_states.json
deleted file mode 100644
index a20558590..000000000
--- a/frontend/src/common/constants/countries_and_states.json
+++ /dev/null
@@ -1,280 +0,0 @@
-[
- {
- "name": "Canada",
- "code": "CA",
- "states": [
- {
- "name": "Alberta",
- "code": "AB"
- },
- {
- "name": "British Columbia",
- "code": "BC"
- },
- {
- "name": "Manitoba",
- "code": "MB"
- },
- {
- "name": "New Brunswick",
- "code": "NB"
- },
- {
- "name": "Newfoundland and Labrador",
- "code": "NL"
- },
- {
- "name": "Northwest Territories",
- "code": "NT"
- },
- {
- "name": "Nova Scotia",
- "code": "NS"
- },
- {
- "name": "Nunavut",
- "code": "NU"
- },
- {
- "name": "Ontario",
- "code": "ON"
- },
- {
- "name": "Prince Edward Island",
- "code": "PE"
- },
- {
- "name": "Quebec",
- "code": "QC"
- },
- {
- "name": "Saskatchewan",
- "code": "SK"
- },
- {
- "name": "Yukon",
- "code": "YT"
- }
- ]
- },
- {
- "name": "United States",
- "code": "US",
- "states": [
- {
- "name": "Alabama",
- "code": "AL"
- },
- {
- "name": "Alaska",
- "code": "AK"
- },
- {
- "name": "Arizona",
- "code": "AZ"
- },
- {
- "name": "Arkansas",
- "code": "AR"
- },
- {
- "name": "California",
- "code": "CA"
- },
- {
- "name": "Colorado",
- "code": "CO"
- },
- {
- "name": "Connecticut",
- "code": "CT"
- },
- {
- "name": "Delaware",
- "code": "DE"
- },
- {
- "name": "District Of Columbia",
- "abbreviation": "DC"
- },
- {
- "name": "Florida",
- "code": "FL"
- },
- {
- "name": "Georgia",
- "code": "GA"
- },
- {
- "name": "Hawaii",
- "code": "HI"
- },
- {
- "name": "Idaho",
- "code": "ID"
- },
- {
- "name": "Illinois",
- "code": "IL"
- },
- {
- "name": "Indiana",
- "code": "IN"
- },
- {
- "name": "Iowa",
- "code": "IA"
- },
- {
- "name": "Kansas",
- "code": "KS"
- },
- {
- "name": "Kentucky",
- "code": "KY"
- },
- {
- "name": "Louisiana",
- "code": "LA"
- },
- {
- "name": "Maine",
- "code": "ME"
- },
- {
- "name": "Maryland",
- "code": "MD"
- },
- {
- "name": "Massachusetts",
- "code": "MA"
- },
- {
- "name": "Michigan",
- "code": "MI"
- },
- {
- "name": "Minnesota",
- "code": "MN"
- },
- {
- "name": "Mississippi",
- "code": "MS"
- },
- {
- "name": "Missouri",
- "code": "MO"
- },
- {
- "name": "Montana",
- "code": "MT"
- },
- {
- "name": "Nebraska",
- "code": "NE"
- },
- {
- "name": "Nevada",
- "code": "NV"
- },
- {
- "name": "New Hampshire",
- "code": "NH"
- },
- {
- "name": "New Jersey",
- "code": "NJ"
- },
- {
- "name": "New Mexico",
- "code": "NM"
- },
- {
- "name": "New York",
- "code": "NY"
- },
- {
- "name": "North Carolina",
- "code": "NC"
- },
- {
- "name": "North Dakota",
- "code": "ND"
- },
- {
- "name": "Ohio",
- "code": "OH"
- },
- {
- "name": "Oklahoma",
- "code": "OK"
- },
- {
- "name": "Oregon",
- "code": "OR"
- },
- {
- "name": "Pennsylvania",
- "code": "PA"
- },
- {
- "name": "Rhode Island",
- "code": "RI"
- },
- {
- "name": "South Carolina",
- "code": "SC"
- },
- {
- "name": "South Dakota",
- "code": "SD"
- },
- {
- "name": "Tennessee",
- "code": "TN"
- },
- {
- "name": "Texas",
- "code": "TX"
- },
- {
- "name": "Utah",
- "code": "UT"
- },
- {
- "name": "Vermont",
- "code": "VT"
- },
- {
- "name": "Virginia",
- "code": "VA"
- },
- {
- "name": "Washington",
- "code": "WA"
- },
- {
- "name": "West Virginia",
- "code": "WV"
- },
- {
- "name": "Wisconsin",
- "code": "WI"
- },
- {
- "name": "Wyoming",
- "code": "WY"
- }
- ]
- },
- {
- "name": "Mexico",
- "code": "MX",
- "states": []
- },
- {
- "name": "Other",
- "code": "XX",
- "states": []
- }
-]
diff --git a/frontend/src/common/constants/validation_messages.json b/frontend/src/common/constants/validation_messages.json
index d5b16f7fa..c3843863a 100644
--- a/frontend/src/common/constants/validation_messages.json
+++ b/frontend/src/common/constants/validation_messages.json
@@ -10,6 +10,22 @@
"NaN": {
"defaultMessage": "Must be a number"
},
+ "greaterThan": {
+ "messageTemplate": "Must be greater than :val.",
+ "placeholders": [":val"]
+ },
+ "lessThan": {
+ "messageTemplate": "Must be less than :val.",
+ "placeholders": [":val"]
+ },
+ "greaterThanOrEq": {
+ "messageTemplate": "Must be greater than or equal to :val.",
+ "placeholders": [":val"]
+ },
+ "lessThanOrEq": {
+ "messageTemplate": "Must be less than or equal to :val.",
+ "placeholders": [":val"]
+ },
"country": {
"defaultMessage": "Invalid country code"
},
@@ -60,7 +76,8 @@
"length": {
"messageTemplate": "Address length must be between :min-:max characters",
"placeholders": [":min", ":max"]
- }
+ },
+ "invalid": "You must enter a valid address."
},
"dba": {
"defaultMessage": "Invalid DBA format",
@@ -118,6 +135,12 @@
}
}
},
+ "licensedGVW": {
+ "max": {
+ "messageTemplate": "Can't Exceed :max",
+ "placeholders": [":max"]
+ }
+ },
"upload": {
"fileSize": {
"exceeded": "File exceeds maximum size"
@@ -129,5 +152,11 @@
"messageTemplate": "The :item must be uploaded",
"placeholders": [":item"]
}
+ },
+ "highway": {
+ "missing": "You must enter at least one highway."
+ },
+ "powerUnit": {
+ "required": "A Power Unit must be added."
}
}
\ No newline at end of file
diff --git a/frontend/src/common/helpers/countries/countrySupportsProvinces.ts b/frontend/src/common/helpers/countries/countrySupportsProvinces.ts
new file mode 100644
index 000000000..478eb00b3
--- /dev/null
+++ b/frontend/src/common/helpers/countries/countrySupportsProvinces.ts
@@ -0,0 +1,15 @@
+import { COUNTRIES } from "../../constants/countries";
+import { Nullable } from "../../types/common";
+import { getDefaultRequiredVal } from "../util";
+
+/**
+ * Determines whether or not the country provided by the code supports provinces.
+ * @param countryCode Country code
+ * @returns Whether or not the country supports provinces
+ */
+export const countrySupportsProvinces = (countryCode?: Nullable) => {
+ return getDefaultRequiredVal(
+ [],
+ COUNTRIES.find(country => country.code === countryCode)?.states,
+ ).length > 0;
+};
diff --git a/frontend/src/common/helpers/countries/getCountryFullName.ts b/frontend/src/common/helpers/countries/getCountryFullName.ts
new file mode 100644
index 000000000..bc4292d9b
--- /dev/null
+++ b/frontend/src/common/helpers/countries/getCountryFullName.ts
@@ -0,0 +1,17 @@
+import { COUNTRIES } from "../../constants/countries";
+import { Nullable } from "../../types/common";
+import { getDefaultRequiredVal } from "../util";
+
+/**
+ * Gets the full name of a country.
+ * @param countryCode Country code
+ * @returns Full name of the country
+ */
+export const getCountryFullName = (countryCode?: Nullable) => {
+ if (!countryCode) return "";
+
+ return getDefaultRequiredVal(
+ "",
+ COUNTRIES.find(c => c.code === countryCode)?.name,
+ );
+};
diff --git a/frontend/src/common/helpers/countries/getProvinceFullName.ts b/frontend/src/common/helpers/countries/getProvinceFullName.ts
new file mode 100644
index 000000000..5791dae25
--- /dev/null
+++ b/frontend/src/common/helpers/countries/getProvinceFullName.ts
@@ -0,0 +1,26 @@
+import { COUNTRIES } from "../../constants/countries";
+import { Nullable } from "../../types/common";
+import { getDefaultRequiredVal } from "../util";
+
+/**
+ * Gets the full name of a province.
+ * @param countryCode Country code
+ * @param provinceCode Province code
+ * @returns Full name of the province
+ */
+export const getProvinceFullName = (
+ countryCode?: Nullable,
+ provinceCode?: Nullable,
+) => {
+ if (!countryCode || !provinceCode) return "";
+
+ const provincesOfCountry = getDefaultRequiredVal(
+ [],
+ COUNTRIES.find(country => country.code === countryCode)?.states,
+ );
+
+ return getDefaultRequiredVal(
+ "",
+ provincesOfCountry.find(province => province.code === provinceCode)?.name,
+ );
+};
diff --git a/frontend/src/common/helpers/equality.ts b/frontend/src/common/helpers/equality.ts
index 2a128acc3..90036f86d 100644
--- a/frontend/src/common/helpers/equality.ts
+++ b/frontend/src/common/helpers/equality.ts
@@ -1,4 +1,5 @@
import { Nullable } from "../types/common";
+import { getDefaultRequiredVal } from "./util";
/**
* Check if two nullable values are different.
@@ -75,3 +76,21 @@ export const doUniqueArraysHaveSameObjects = (
return true;
};
+
+/**
+ * Compare whether or not two ordered sequences are equal (ie. having same items in the same order).
+ * @param sequence1 First array of sequential items
+ * @param sequence2 Second array of sequential items
+ * @returns Whether or not the two sequences have the same items in the same order
+ */
+export const areOrderedSequencesEqual = (
+ sequence1: Nullable,
+ sequence2: Nullable,
+ equalFn: (item1: T, item2: T) => boolean,
+) => {
+ const seq1 = getDefaultRequiredVal([], sequence1);
+ const seq2 = getDefaultRequiredVal([], sequence2);
+
+ if (seq1.length !== seq2.length) return false;
+ return seq1.every((seqNumber, index) => equalFn(seqNumber, seq2[index]));
+};
diff --git a/frontend/src/common/helpers/formatCountryProvince.ts b/frontend/src/common/helpers/formatCountryProvince.ts
deleted file mode 100644
index 611e2bce2..000000000
--- a/frontend/src/common/helpers/formatCountryProvince.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import CountriesAndStates from "../constants/countries_and_states.json";
-
-/**
- * Converts CountryCode to Country Name using the countries_and_states.json file
- * @param countryCode
- * @returns Full name of the country
- */
-export const formatCountry = (countryCode?: string) => {
- if (!countryCode) return "";
-
- const countryName = CountriesAndStates.filter(
- (country: any) => country.code === countryCode,
- );
- return countryName[0].name;
-};
-
-/**
- * Converts provinceCode to Province Name using the countries_and_states.json file
- * @param countryCode
- * @param provinceCode
- * @returns Full name of the province
- */
-export const formatProvince = (countryCode?: string, provinceCode?: string) => {
- if (!countryCode || !provinceCode) return "";
-
- const countries = CountriesAndStates.filter(
- (country: any) => country.code === countryCode,
- ).flatMap((country: any) => country.states);
-
- const provinceName = countries.filter(
- (province: any) => province.code === provinceCode,
- );
-
- if (!provinceName[0]) return "";
-
- return provinceName[0].name;
-};
diff --git a/frontend/src/common/helpers/numeric/convertToNumberIfValid.ts b/frontend/src/common/helpers/numeric/convertToNumberIfValid.ts
new file mode 100644
index 000000000..7cc4ae3fa
--- /dev/null
+++ b/frontend/src/common/helpers/numeric/convertToNumberIfValid.ts
@@ -0,0 +1,24 @@
+import { isNull, isUndefined, Nullable } from "../../types/common";
+
+/**
+ * Converts a numeric value to a number if possible.
+ * @param numericVal The numeric value (can be number or string)
+ * @param fallbackWhenInvalid The value to return if invalid.
+ * @returns The converted number value, or fallback value when invalid
+ */
+export const convertToNumberIfValid = >(
+ numericVal?: Nullable,
+ fallbackWhenInvalid?: T,
+) => {
+ const isNullable = isNull(numericVal) || isUndefined(numericVal);
+ const isNumberButInvalid = (typeof numericVal === "number") && isNaN(numericVal);
+ const isStringButInvalid = (typeof numericVal === "string")
+ && (numericVal.trim() === "" || isNaN(Number(numericVal.trim())));
+
+ const isInvalid = isNullable
+ || ((typeof numericVal !== "number") && (typeof numericVal !== "string"))
+ || isNumberButInvalid
+ || isStringButInvalid;
+
+ return !isInvalid ? Number(numericVal) : fallbackWhenInvalid as T;
+};
diff --git a/frontend/src/common/helpers/util.ts b/frontend/src/common/helpers/util.ts
index 5fea00c4c..0ffff21af 100644
--- a/frontend/src/common/helpers/util.ts
+++ b/frontend/src/common/helpers/util.ts
@@ -175,25 +175,6 @@ export const streamDownloadFile = async (response: Response) => {
return { blobObj, filename };
};
-/**
- * Convers a string to a number.
- * (Applicable for number fields in forms).
- *
- * @param str The string value.
- * @param valueToReturnWhenInvalid The value to return if invalid.
- * @returns A number or valueToReturnWhenInvalid.
- */
-export const convertToNumberIfValid = (
- str?: Nullable,
- valueToReturnWhenInvalid?: 0 | Nullable | Nullable,
-) => {
- // return input as a number if it's a valid number value,
- // or original value if invalid number
- return str != null && str !== "" && !isNaN(Number(str))
- ? Number(str)
- : valueToReturnWhenInvalid;
-};
-
/**
* Returns a label for the userRole.
* @param userRole The userRole the user belongs to.
diff --git a/frontend/src/common/helpers/validationMessages.ts b/frontend/src/common/helpers/validationMessages.ts
index a820c02f3..bd00e02ee 100644
--- a/frontend/src/common/helpers/validationMessages.ts
+++ b/frontend/src/common/helpers/validationMessages.ts
@@ -16,6 +16,26 @@ export const requiredMessage = () => validationMessages.required.defaultMessage;
export const selectionRequired = () =>
validationMessages.selectionRequired.defaultMessage;
export const invalidNumber = () => validationMessages.NaN.defaultMessage;
+export const mustBeGreaterThan = (val: number) => {
+ const { messageTemplate, placeholders } = validationMessages.greaterThan;
+ return replacePlaceholders(messageTemplate, placeholders, val);
+};
+
+export const mustBeLessThan = (val: number) => {
+ const { messageTemplate, placeholders } = validationMessages.lessThan;
+ return replacePlaceholders(messageTemplate, placeholders, val);
+};
+
+export const mustBeGreaterThanOrEqualTo = (val: number) => {
+ const { messageTemplate, placeholders } = validationMessages.greaterThanOrEq;
+ return replacePlaceholders(messageTemplate, placeholders, val);
+};
+
+export const mustBeLessThanOrEqualTo = (val: number) => {
+ const { messageTemplate, placeholders } = validationMessages.lessThanOrEq;
+ return replacePlaceholders(messageTemplate, placeholders, val);
+};
+
export const invalidCountryCode = () =>
validationMessages.country.defaultMessage;
export const invalidProvinceCode = () =>
@@ -53,6 +73,8 @@ export const invalidAddressLength = (min: number, max: number) => {
return replacePlaceholders(messageTemplate, placeholders, min, max);
};
+export const invalidAddress = () => validationMessages.address.invalid;
+
export const invalidCityLength = (min: number, max: number) => {
const { messageTemplate, placeholders } = validationMessages.city.length;
return replacePlaceholders(messageTemplate, placeholders, min, max);
@@ -86,6 +108,15 @@ export const invalidPlateLength = (max: number) => {
return replacePlaceholders(messageTemplate, placeholders, max);
};
+export const licensedGVWExceeded = (max: number, localizeNumber?: boolean) => {
+ const { messageTemplate, placeholders } = validationMessages.licensedGVW.max;
+ return replacePlaceholders(
+ messageTemplate,
+ placeholders,
+ localizeNumber ? max.toLocaleString() : max,
+ );
+};
+
export const invalidDBALength = (min: number, max: number) => {
const { messageTemplate, placeholders } = validationMessages.dba.length;
return replacePlaceholders(messageTemplate, placeholders, min, max);
@@ -104,6 +135,10 @@ export const requiredUpload = (uploadItem: string) => {
return replacePlaceholders(messageTemplate, placeholders, uploadItem);
};
+export const requiredHighway = () => validationMessages.highway.missing;
+
+export const requiredPowerUnit = () => validationMessages.powerUnit.required;
+
/**
* Checks if a given string is
* null, empty or conforms to length requirements if it has a value.
diff --git a/frontend/src/common/types/Country.ts b/frontend/src/common/types/Country.ts
new file mode 100644
index 000000000..a9736b8c7
--- /dev/null
+++ b/frontend/src/common/types/Country.ts
@@ -0,0 +1,7 @@
+import { Province } from "./Province";
+
+export interface Country {
+ name: string;
+ code: string;
+ states: Province[];
+}
diff --git a/frontend/src/common/types/Province.ts b/frontend/src/common/types/Province.ts
new file mode 100644
index 000000000..30a7f7c72
--- /dev/null
+++ b/frontend/src/common/types/Province.ts
@@ -0,0 +1,4 @@
+export interface Province {
+ name: string;
+ code: string;
+}
diff --git a/frontend/src/common/types/common.ts b/frontend/src/common/types/common.ts
index 5e90d2636..0223e35c8 100644
--- a/frontend/src/common/types/common.ts
+++ b/frontend/src/common/types/common.ts
@@ -146,3 +146,11 @@ export type Nullable = Optional>;
export type NullableFields = {
[P in keyof T]?: Nullable;
};
+
+export const isNull = (val?: Nullable) => {
+ return !val && (typeof val !== "undefined") && val == null;
+};
+
+export const isUndefined = (val?: Nullable) => {
+ return (typeof val === "undefined");
+};
diff --git a/frontend/src/features/idir/search/table/CompanySearchResultColumnDef.tsx b/frontend/src/features/idir/search/table/CompanySearchResultColumnDef.tsx
index dfb17ab28..1b09a744b 100644
--- a/frontend/src/features/idir/search/table/CompanySearchResultColumnDef.tsx
+++ b/frontend/src/features/idir/search/table/CompanySearchResultColumnDef.tsx
@@ -1,7 +1,7 @@
import { MRT_ColumnDef } from "material-react-table";
-import CountriesAndStates from "../../../../common/constants/countries_and_states.json";
-import { CompanyProfile } from "../../../manageProfile/types/manageProfile";
+import { COUNTRIES } from "../../../../common/constants/countries";
+import { CompanyProfile } from "../../../manageProfile/types/manageProfile";
import { getDefaultNullableVal } from "../../../../common/helpers/util";
/*
@@ -31,7 +31,7 @@ export const CompanySearchResultColumnDef: MRT_ColumnDef[] = [
sortingFn: "alphanumeric",
Cell: (props: { row: any }) => {
const mailingAddress = props.row?.original?.mailingAddress;
- const country = CountriesAndStates.filter((country) => {
+ const country = COUNTRIES.filter((country) => {
return country?.code === mailingAddress?.countryCode;
});
diff --git a/frontend/src/features/manageProfile/apiManager/manageProfileAPI.tsx b/frontend/src/features/manageProfile/apiManager/manageProfileAPI.tsx
index 9709e40dc..936be1506 100644
--- a/frontend/src/features/manageProfile/apiManager/manageProfileAPI.tsx
+++ b/frontend/src/features/manageProfile/apiManager/manageProfileAPI.tsx
@@ -32,15 +32,10 @@ export const getCompanyInfo = async (): Promise => {
export const getCompanyInfoById = async (
companyId: number,
): Promise> => {
- try {
- const response = await httpGETRequest(
- `${MANAGE_PROFILE_API.COMPANIES}/${companyId}`,
- );
- return response.data;
- } catch (err) {
- console.error(err);
- return null;
- }
+ const response = await httpGETRequest(
+ `${MANAGE_PROFILE_API.COMPANIES}/${companyId}`,
+ );
+ return response.data;
};
export const getMyInfo = async (): Promise => {
diff --git a/frontend/src/features/manageProfile/components/forms/common/ReusableUserInfoForm.tsx b/frontend/src/features/manageProfile/components/forms/common/ReusableUserInfoForm.tsx
index f4a601a58..498a6f7be 100644
--- a/frontend/src/features/manageProfile/components/forms/common/ReusableUserInfoForm.tsx
+++ b/frontend/src/features/manageProfile/components/forms/common/ReusableUserInfoForm.tsx
@@ -94,7 +94,7 @@ export const ReusableUserInfoForm = ({
className="my-info-form__input my-info-form__input--left"
/>
(
}}
className="company-primary-contact-form__input company-primary-contact-form__input--left"
/>
+
(
className="company-primary-contact-form__input company-primary-contact-form__input--right"
/>
+
(
}}
className="company-primary-contact-form__input company-primary-contact-form__input--left"
/>
+
>;
}) => {
- if (!companyInfo) return <>>;
-
const userEmail = getUserEmailFromSession();
- return (
+ const mailingCountry = getCountryFullName(companyInfo?.mailingAddress?.countryCode);
+ const mailingProvince = getProvinceFullName(
+ companyInfo?.mailingAddress?.countryCode,
+ companyInfo?.mailingAddress?.provinceCode,
+ );
+
+ const primaryContactCountry = getCountryFullName(companyInfo?.primaryContact?.countryCode);
+ const primaryContactProvince = getProvinceFullName(
+ companyInfo?.primaryContact?.countryCode,
+ companyInfo?.primaryContact?.provinceCode,
+ );
+
+ const phoneDisplay = (phone: string, ext?: Nullable) => {
+ const extDisplay = ext ? `Ext: ${ext}` : "";
+ return `${formatPhoneNumber(
+ phone,
+ )} ${extDisplay}`;
+ };
+
+ return companyInfo ? (
- {companyInfo?.alternateName && (
+ {companyInfo.alternateName ? (
<>
Doing Business As (DBA)
- {companyInfo?.alternateName}
+ {companyInfo.alternateName}
>
- )}
+ ) : null}
+
Company Mailing Address
- {companyInfo?.mailingAddress.addressLine1}
+
+ {companyInfo.mailingAddress.addressLine1}
+
- {formatCountry(companyInfo?.mailingAddress.countryCode)}
+ {mailingCountry}
+
+ {mailingProvince ? (
+
+ {mailingProvince}
+
+ ) : null}
+
- {formatProvince(
- companyInfo?.mailingAddress.countryCode,
- companyInfo?.mailingAddress.provinceCode,
- )}
+ {`${companyInfo.mailingAddress.city} ${companyInfo.mailingAddress.postalCode}`}
- {`${companyInfo?.mailingAddress.city} ${companyInfo?.mailingAddress.postalCode}`}
Company Contact Details
+
- Email: {getDefaultRequiredVal("", companyInfo?.email, userEmail)}
+ Email: {getDefaultRequiredVal("", companyInfo.email, userEmail)}
- {`Phone: ${formatPhoneNumber(companyInfo?.phone)} ${
- companyInfo?.extension ? `Ext: ${companyInfo?.extension}` : ""
- }`}
+
+
+ {`Phone: ${phoneDisplay(companyInfo.phone, companyInfo.extension)}`}
+
+
{companyInfo?.fax ? (
- Fax: {formatPhoneNumber(companyInfo?.fax)}
- ) : (
- ""
- )}
+
+ Fax: {formatPhoneNumber(companyInfo.fax)}
+
+ ) : null}
Company Primary Contact
- {`${companyInfo?.primaryContact.firstName} ${companyInfo?.primaryContact.lastName}`}
+
- Email:{" "}
- {getDefaultRequiredVal("", companyInfo?.primaryContact?.email)}
+ {`${companyInfo.primaryContact.firstName} ${companyInfo.primaryContact.lastName}`}
- {`Primary Phone: ${formatPhoneNumber(
- companyInfo?.primaryContact.phone1,
- )} ${
- companyInfo?.primaryContact.phone1Extension
- ? `Ext: ${companyInfo?.primaryContact.phone1Extension}`
- : ""
- }`}
+
- {formatCountry(companyInfo?.primaryContact.countryCode)}
+ Email: {getDefaultRequiredVal("", companyInfo.primaryContact.email)}
+
- {formatProvince(
- companyInfo?.primaryContact.countryCode,
- companyInfo?.primaryContact.provinceCode,
- )}
+ {`Primary Phone: ${phoneDisplay(
+ companyInfo.primaryContact.phone1,
+ companyInfo.primaryContact.phone1Extension,
+ )}`}
- {companyInfo?.primaryContact.city}
+
+
+ {primaryContactCountry}
+
+
+ {primaryContactProvince ? (
+
+ {primaryContactProvince}
+
+ ) : null}
+
+ {companyInfo.primaryContact.city}
- {DoesUserHaveClaimWithContext(CLAIMS.WRITE_ORG) && (
+
+ {DoesUserHaveClaimWithContext(CLAIMS.WRITE_ORG) ? (
- )}
+ ) : null}
- );
+ ) : null;
},
);
diff --git a/frontend/src/features/manageProfile/pages/DisplayMyInfo.tsx b/frontend/src/features/manageProfile/pages/DisplayMyInfo.tsx
index e7c06e500..2fc19e223 100644
--- a/frontend/src/features/manageProfile/pages/DisplayMyInfo.tsx
+++ b/frontend/src/features/manageProfile/pages/DisplayMyInfo.tsx
@@ -6,10 +6,8 @@ import { faPencil } from "@fortawesome/free-solid-svg-icons";
import "./DisplayMyInfo.scss";
import { ReadUserInformationResponse } from "../types/manageProfile";
import { formatPhoneNumber } from "../../../common/components/form/subFormComponents/PhoneNumberInput";
-import {
- formatProvince,
- formatCountry,
-} from "../../../common/helpers/formatCountryProvince";
+import { getProvinceFullName } from "../../../common/helpers/countries/getProvinceFullName";
+import { getCountryFullName } from "../../../common/helpers/countries/getCountryFullName";
export const DisplayMyInfo = memo(
({
@@ -19,33 +17,49 @@ export const DisplayMyInfo = memo(
myInfo?: ReadUserInformationResponse;
setIsEditing: React.Dispatch>;
}) => {
- if (!myInfo) return <>>;
- return (
+ const countryFullName = getCountryFullName(myInfo?.countryCode);
+ const provinceFullName = getProvinceFullName(myInfo?.countryCode, myInfo?.provinceCode);
+
+ return myInfo ? (
{`${myInfo.firstName} ${myInfo.lastName}`}
+
Email: {myInfo.email}
+
Primary Phone: {formatPhoneNumber(myInfo.phone1)}{" "}
{myInfo.phone1Extension ? `Ext: ${myInfo.phone1Extension}` : ""}
+
{myInfo.phone2 ? (
Alternate Phone: {formatPhoneNumber(myInfo.phone2)}{" "}
{myInfo.phone2Extension ? `Ext: ${myInfo.phone2Extension}` : ""}
) : null}
+
{myInfo.fax ? (
Fax: {formatPhoneNumber(myInfo.fax)}
) : null}
- {formatCountry(myInfo.countryCode)}
-
- {formatProvince(myInfo.countryCode, myInfo.provinceCode)}
-
+
+ {countryFullName ? (
+
+ {countryFullName}
+
+ ) : null}
+
+ {provinceFullName ? (
+
+ {provinceFullName}
+
+ ) : null}
+
{myInfo.city}
+
- );
+ ) : null;
},
);
diff --git a/frontend/src/features/manageVehicles/components/form/PowerUnitForm.tsx b/frontend/src/features/manageVehicles/components/form/PowerUnitForm.tsx
index 62b1216f9..337aac044 100644
--- a/frontend/src/features/manageVehicles/components/form/PowerUnitForm.tsx
+++ b/frontend/src/features/manageVehicles/components/form/PowerUnitForm.tsx
@@ -9,19 +9,19 @@ import { CountryAndProvince } from "../../../../common/components/form/CountryAn
import { CustomFormComponent } from "../../../../common/components/form/CustomFormComponents";
import { SnackBarContext } from "../../../../App";
import { VEHICLES_ROUTES } from "../../../../routes/constants";
-import { Nullable } from "../../../../common/types/common";
+import { now } from "../../../../common/helpers/formatDate";
+import { getDefaultRequiredVal } from "../../../../common/helpers/util";
+import { convertToNumberIfValid } from "../../../../common/helpers/numeric/convertToNumberIfValid";
+import {
+ disableMouseWheelInputOnNumberField,
+} from "../../../../common/helpers/disableMouseWheelInputOnNumberField";
+
import {
usePowerUnitSubTypesQuery,
useAddPowerUnitMutation,
useUpdatePowerUnitMutation,
} from "../../hooks/powerUnits";
-import {
- getDefaultRequiredVal,
- getDefaultNullableVal,
- convertToNumberIfValid,
-} from "../../../../common/helpers/util";
-
import {
invalidNumber,
invalidPlateLength,
@@ -29,7 +29,6 @@ import {
invalidYearMin,
requiredMessage,
} from "../../../../common/helpers/validationMessages";
-import { disableMouseWheelInputOnNumberField } from "../../../../common/helpers/disableMouseWheelInputOnNumberField";
const FEATURE = "power-unit";
@@ -41,19 +40,21 @@ export const PowerUnitForm = ({
powerUnit?: PowerUnit;
}) => {
const isEditMode = Boolean(powerUnit?.powerUnitId);
+ const getDefaultYear = () => now().year();
+ const defaultYear = getDefaultYear();
// If data was passed to this component, then use that data, otherwise set fields to empty
const powerUnitDefaultValues = {
provinceCode: getDefaultRequiredVal("", powerUnit?.provinceCode),
countryCode: getDefaultRequiredVal("", powerUnit?.countryCode),
unitNumber: getDefaultRequiredVal("", powerUnit?.unitNumber),
- licensedGvw: getDefaultNullableVal(powerUnit?.licensedGvw),
+ licensedGvw: convertToNumberIfValid(powerUnit?.licensedGvw, null),
make: getDefaultRequiredVal("", powerUnit?.make),
plate: getDefaultRequiredVal("", powerUnit?.plate),
powerUnitTypeCode: getDefaultRequiredVal("", powerUnit?.powerUnitTypeCode),
- steerAxleTireSize: getDefaultNullableVal(powerUnit?.steerAxleTireSize),
+ steerAxleTireSize: convertToNumberIfValid(powerUnit?.steerAxleTireSize, null),
vin: getDefaultRequiredVal("", powerUnit?.vin),
- year: getDefaultNullableVal(powerUnit?.year),
+ year: convertToNumberIfValid(powerUnit?.year, defaultYear),
};
const formMethods = useForm({
@@ -79,15 +80,15 @@ export const PowerUnitForm = ({
powerUnit: {
...powerUnitToBeUpdated,
// need to explicitly convert form values to number here (since we can't use valueAsNumber prop)
- year: convertToNumberIfValid(data.year, data.year as string) as any,
+ year: convertToNumberIfValid(data.year, defaultYear),
licensedGvw: convertToNumberIfValid(
data.licensedGvw,
- data.licensedGvw as string,
- ) as any,
+ null,
+ ),
steerAxleTireSize: convertToNumberIfValid(
data.steerAxleTireSize,
null,
- ) as Nullable,
+ ),
},
});
@@ -108,17 +109,12 @@ export const PowerUnitForm = ({
powerUnit: {
...powerUnitToBeAdded,
// need to explicitly convert form values to number here (since we can't use valueAsNumber prop)
- year: !isNaN(Number(data.year)) ? Number(data.year) : data.year,
- licensedGvw:
- data.licensedGvw != null &&
- data.licensedGvw !== "" &&
- !isNaN(Number(data.licensedGvw))
- ? Number(data.licensedGvw)
- : data.licensedGvw,
+ year: convertToNumberIfValid(data.year, defaultYear),
+ licensedGvw: convertToNumberIfValid(data.licensedGvw, null),
steerAxleTireSize: convertToNumberIfValid(
data.steerAxleTireSize,
null,
- ) as Nullable,
+ ),
},
});
diff --git a/frontend/src/features/manageVehicles/components/form/TrailerForm.tsx b/frontend/src/features/manageVehicles/components/form/TrailerForm.tsx
index bfb86ec07..5367389f8 100644
--- a/frontend/src/features/manageVehicles/components/form/TrailerForm.tsx
+++ b/frontend/src/features/manageVehicles/components/form/TrailerForm.tsx
@@ -9,19 +9,19 @@ import { CountryAndProvince } from "../../../../common/components/form/CountryAn
import { CustomFormComponent } from "../../../../common/components/form/CustomFormComponents";
import { SnackBarContext } from "../../../../App";
import { VEHICLES_ROUTES } from "../../../../routes/constants";
-import { Nullable } from "../../../../common/types/common";
+import { now } from "../../../../common/helpers/formatDate";
+import { getDefaultRequiredVal } from "../../../../common/helpers/util";
+import { convertToNumberIfValid } from "../../../../common/helpers/numeric/convertToNumberIfValid";
+import {
+ disableMouseWheelInputOnNumberField,
+} from "../../../../common/helpers/disableMouseWheelInputOnNumberField";
+
import {
useTrailerSubTypesQuery,
useAddTrailerMutation,
useUpdateTrailerMutation,
} from "../../hooks/trailers";
-import {
- getDefaultRequiredVal,
- getDefaultNullableVal,
- convertToNumberIfValid,
-} from "../../../../common/helpers/util";
-
import {
invalidNumber,
invalidPlateLength,
@@ -29,7 +29,6 @@ import {
invalidYearMin,
requiredMessage,
} from "../../../../common/helpers/validationMessages";
-import { disableMouseWheelInputOnNumberField } from "../../../../common/helpers/disableMouseWheelInputOnNumberField";
const FEATURE = "trailer";
@@ -41,6 +40,8 @@ export const TrailerForm = ({
trailer?: Trailer;
}) => {
const isEditMode = Boolean(trailer?.trailerId);
+ const getDefaultYear = () => now().year();
+ const defaultYear = getDefaultYear();
// If data was passed to this component, then use that data, otherwise set fields to empty
const trailerDefaultValues = {
@@ -51,8 +52,8 @@ export const TrailerForm = ({
plate: getDefaultRequiredVal("", trailer?.plate),
trailerTypeCode: getDefaultRequiredVal("", trailer?.trailerTypeCode),
vin: getDefaultRequiredVal("", trailer?.vin),
- year: getDefaultNullableVal(trailer?.year),
- emptyTrailerWidth: getDefaultNullableVal(trailer?.emptyTrailerWidth),
+ year: convertToNumberIfValid(trailer?.year, defaultYear),
+ emptyTrailerWidth: convertToNumberIfValid(trailer?.emptyTrailerWidth, null),
};
const formMethods = useForm({
@@ -77,11 +78,11 @@ export const TrailerForm = ({
trailer: {
...trailerToBeUpdated,
// need to explicitly convert form values to number here (since we can't use valueAsNumber prop)
- year: !isNaN(Number(data.year)) ? Number(data.year) : data.year,
+ year: convertToNumberIfValid(data.year, defaultYear),
emptyTrailerWidth: convertToNumberIfValid(
data.emptyTrailerWidth,
null,
- ) as Nullable,
+ ),
},
});
if (result.status === 200) {
@@ -100,11 +101,11 @@ export const TrailerForm = ({
trailer: {
...trailerToBeAdded,
// need to explicitly convert form values to number here (since we can't use valueAsNumber prop)
- year: !isNaN(Number(data.year)) ? Number(data.year) : data.year,
+ year: convertToNumberIfValid(data.year, defaultYear),
emptyTrailerWidth: convertToNumberIfValid(
data.emptyTrailerWidth,
null,
- ) as Nullable,
+ ),
},
});
diff --git a/frontend/src/features/manageVehicles/components/form/tests/helpers/access.ts b/frontend/src/features/manageVehicles/components/form/tests/helpers/access.ts
index 282318eb9..d2161b720 100644
--- a/frontend/src/features/manageVehicles/components/form/tests/helpers/access.ts
+++ b/frontend/src/features/manageVehicles/components/form/tests/helpers/access.ts
@@ -153,7 +153,7 @@ export const submitVehicleForm = async (
await replaceValueForInput(user, unitNumber, 0, details.newUnitNumber);
await replaceValueForInput(user, make, 0, details.newMake);
- await replaceValueForInput(user, year, 1, `${details.newYear}`);
+ await replaceValueForInput(user, year, 4, `${details.newYear}`);
await replaceValueForInput(user, vin, 0, details.newVin);
await replaceValueForInput(user, plate, 0, details.newPlate);
diff --git a/frontend/src/features/manageVehicles/helpers/vehicleSubtypes.ts b/frontend/src/features/manageVehicles/helpers/vehicleSubtypes.ts
index 144302cb1..ff5b1d3ae 100644
--- a/frontend/src/features/manageVehicles/helpers/vehicleSubtypes.ts
+++ b/frontend/src/features/manageVehicles/helpers/vehicleSubtypes.ts
@@ -1,3 +1,4 @@
+import { LCV_VEHICLE_SUBTYPES } from "../../permits/constants/constants";
import {
BaseVehicle,
PowerUnit,
@@ -11,7 +12,7 @@ import {
* @returns If the subtype is considered to be LCV vehicle subtype
*/
export const isVehicleSubtypeLCV = (subtype: string) => {
- return ["LCVRMDB", "LCVTPDB"].includes(subtype);
+ return LCV_VEHICLE_SUBTYPES.map(({ typeCode }) => typeCode).includes(subtype);
};
export const EMPTY_VEHICLE_SUBTYPE = {
diff --git a/frontend/src/features/manageVehicles/types/Vehicle.ts b/frontend/src/features/manageVehicles/types/Vehicle.ts
index b08325a87..dc1a2164a 100644
--- a/frontend/src/features/manageVehicles/types/Vehicle.ts
+++ b/frontend/src/features/manageVehicles/types/Vehicle.ts
@@ -27,7 +27,7 @@ export interface BaseVehicle {
export interface PowerUnit extends BaseVehicle {
powerUnitId?: string;
- licensedGvw?: number;
+ licensedGvw?: Nullable;
steerAxleTireSize?: Nullable;
powerUnitTypeCode: string;
}
diff --git a/frontend/src/features/permits/ApplicationSteps.tsx b/frontend/src/features/permits/ApplicationSteps.tsx
index 4a7d5653b..27259c032 100644
--- a/frontend/src/features/permits/ApplicationSteps.tsx
+++ b/frontend/src/features/permits/ApplicationSteps.tsx
@@ -3,13 +3,22 @@ import { ErrorBoundary } from "react-error-boundary";
import { ApplicationStepPage } from "./components/dashboard/ApplicationStepPage";
import { ErrorFallback } from "../../common/pages/ErrorFallback";
-import { ApplicationStep } from "../../routes/constants";
+import { ApplicationStep, ApplicationStepContext } from "../../routes/constants";
export const ApplicationSteps = React.memo(
- ({ applicationStep }: { applicationStep: ApplicationStep }) => {
+ ({
+ applicationStep,
+ applicationStepContext,
+ }: {
+ applicationStep: ApplicationStep;
+ applicationStepContext: ApplicationStepContext;
+ }) => {
return (
-
+
);
},
diff --git a/frontend/src/features/permits/apiManager/permitsAPI.ts b/frontend/src/features/permits/apiManager/permitsAPI.ts
index 3131adfea..5cb146595 100644
--- a/frontend/src/features/permits/apiManager/permitsAPI.ts
+++ b/frontend/src/features/permits/apiManager/permitsAPI.ts
@@ -22,7 +22,7 @@ import {
import {
serializeForCreateApplication,
serializeForUpdateApplication,
-} from "../helpers/serializeApplication";
+} from "../helpers/serialize/serializeApplication";
import {
CompleteTransactionRequestData,
@@ -49,8 +49,8 @@ import {
import {
ApplicationResponseData,
ApplicationListItem,
- ApplicationFormData,
ApplicationFilters,
+ ApplicationFormData,
} from "../types/application";
import {
@@ -68,28 +68,27 @@ import {
/**
* Create a new application.
- * @param application application data to be submitted
- * @returns response with created application data, or error if failed
+ * @param applicationFormData Application form data to be submitted
+ * @returns Response with created application, or error if failed
*/
export const createApplication = async (
- application: ApplicationFormData,
+ applicationFormData: ApplicationFormData,
companyId: number,
): Promise> => {
return await httpPOSTRequest(
APPLICATIONS_API_ROUTES.CREATE(companyId),
- replaceEmptyValuesWithNull({
- // must convert application to ApplicationRequestData (dayjs fields to strings)
- ...serializeForCreateApplication(application),
- }),
+ replaceEmptyValuesWithNull(
+ serializeForCreateApplication(applicationFormData),
+ ),
);
};
/**
* Update an existing application.
- * @param application application data
- * @param applicationId application number for the application to update
- * @param companyId id of the company that the application belongs to
- * @returns response with updated application data, or error if failed
+ * @param applicationFormData Application form data
+ * @param applicationId Application id for the application to update
+ * @param companyId Company id that the application belongs to
+ * @returns Response with updated application, or error if failed
*/
export const updateApplication = async (
application: ApplicationFormData,
@@ -98,10 +97,9 @@ export const updateApplication = async (
): Promise> => {
return await httpPUTRequest(
APPLICATIONS_API_ROUTES.UPDATE(companyId, applicationId),
- replaceEmptyValuesWithNull({
- // must convert application to ApplicationRequestData (dayjs fields to strings)
- ...serializeForUpdateApplication(application),
- }),
+ replaceEmptyValuesWithNull(
+ serializeForUpdateApplication(application),
+ ),
);
};
diff --git a/frontend/src/features/permits/components/dashboard/ApplicationStepPage.tsx b/frontend/src/features/permits/components/dashboard/ApplicationStepPage.tsx
index 960f85e65..2d1a59d8d 100644
--- a/frontend/src/features/permits/components/dashboard/ApplicationStepPage.tsx
+++ b/frontend/src/features/permits/components/dashboard/ApplicationStepPage.tsx
@@ -1,5 +1,4 @@
import { Box } from "@mui/material";
-import { AxiosError } from "axios";
import { Navigate, useParams } from "react-router-dom";
import { useMemo } from "react";
@@ -8,23 +7,27 @@ import { Banner } from "../../../../common/components/dashboard/components/banne
import { ApplicationForm } from "../../pages/Application/ApplicationForm";
import { ApplicationContext } from "../../context/ApplicationContext";
import { ApplicationReview } from "../../pages/Application/ApplicationReview";
-import { useCompanyInfoQuery } from "../../../manageProfile/apiManager/hooks";
+import { getCompanyIdFromSession } from "../../../../common/apiManager/httpRequestHandler";
import { Loading } from "../../../../common/pages/Loading";
-import { ErrorFallback } from "../../../../common/pages/ErrorFallback";
+import { ApplicationInQueueReview } from "../../../queue/components/ApplicationInQueueReview";
import { useApplicationForStepsQuery } from "../../hooks/hooks";
import { PERMIT_STATUSES } from "../../types/PermitStatus";
import { applyWhenNotNullable, getDefaultRequiredVal } from "../../../../common/helpers/util";
+import { useFeatureFlagsQuery } from "../../../../common/hooks/hooks";
import {
DEFAULT_PERMIT_TYPE,
+ PERMIT_TYPES,
PermitType,
isPermitTypeValid,
} from "../../types/PermitType";
+
import {
+ APPLICATION_STEP_CONTEXTS,
APPLICATION_STEPS,
ApplicationStep,
+ ApplicationStepContext,
ERROR_ROUTES,
} from "../../../../routes/constants";
-import { getCompanyIdFromSession } from "../../../../common/apiManager/httpRequestHandler";
const displayHeaderText = (stepKey: ApplicationStep) => {
switch (stepKey) {
@@ -40,19 +43,23 @@ const displayHeaderText = (stepKey: ApplicationStep) => {
export const ApplicationStepPage = ({
applicationStep,
+ applicationStepContext,
}: {
applicationStep: ApplicationStep;
+ applicationStepContext: ApplicationStepContext;
}) => {
- const companyInfoQuery = useCompanyInfoQuery();
+ // Get application number from route, if there is one (for edit applications)
+ // or get the permit type for creating a new application
+ const { permitId, permitType, companyId: companyIdParam } = useParams();
+
const companyId: number = getDefaultRequiredVal(
0,
+ applyWhenNotNullable(id => Number(id), companyIdParam),
applyWhenNotNullable(id => Number(id), getCompanyIdFromSession()),
- companyInfoQuery.data?.companyId,
);
- // Get application number from route, if there is one (for edit applications)
- // or get the permit type for creating a new application
- const { permitId, permitType } = useParams();
+ const { data: featureFlags } = useFeatureFlagsQuery();
+ const enableSTOS = featureFlags?.["STOS"] === "ENABLED";
// Query for the application data whenever this page is rendered
const {
@@ -85,45 +92,59 @@ export const ApplicationStepPage = ({
DEFAULT_PERMIT_TYPE,
isPermitTypeValid(permitType)
? (permitType?.toUpperCase() as PermitType)
- : null,
+ : null, // when permitType in the url param is empty or not a valid permit type
applicationData?.permitType,
);
+ // Currently onRouteBC only handles TROS and TROW permits, and STOS only if feature flag is enabled
+ const isPermitTypeAllowed = () => {
+ const allowedPermitTypes: string[] = enableSTOS ? [
+ PERMIT_TYPES.TROS,
+ PERMIT_TYPES.TROW,
+ PERMIT_TYPES.STOS,
+ ] : [
+ PERMIT_TYPES.TROS,
+ PERMIT_TYPES.TROW,
+ ];
+
+ return allowedPermitTypes.includes(applicationPermitType);
+ };
+
// Permit must be an application in progress in order to allow application-related edit/review/add to cart steps
- // (ie. empty status for new application, or in progress)
+ // (ie. empty status for new application, or in progress and in queue)
const isValidApplicationStatus = () => {
return (
!isInvalidApplication &&
(!applicationData?.permitStatus ||
- applicationData?.permitStatus === PERMIT_STATUSES.IN_PROGRESS)
+ applicationData?.permitStatus === PERMIT_STATUSES.IN_PROGRESS ||
+ applicationData?.permitStatus === PERMIT_STATUSES.IN_QUEUE
+ )
);
};
const renderApplicationStep = () => {
if (applicationStep === APPLICATION_STEPS.REVIEW) {
- return ;
+ return applicationStepContext === APPLICATION_STEP_CONTEXTS.QUEUE ? (
+
+ ) : (
+
+ );
}
- return ;
+ return (
+
+ );
};
- if (isInvalidApplication || !isValidApplicationStatus()) {
+ if (isInvalidApplication || !isValidApplicationStatus() || !companyId || !isPermitTypeAllowed()) {
return ;
}
- if (companyInfoQuery.isPending) {
- return ;
- }
-
- if (companyInfoQuery.isError) {
- if (companyInfoQuery.error instanceof AxiosError) {
- if (companyInfoQuery.error.response?.status === 401) {
- return ;
- }
- return ;
- }
- }
-
if (isLoading) {
return ;
}
diff --git a/frontend/src/features/permits/components/dashboard/tests/integration/fixtures/getVehicleInfo.ts b/frontend/src/features/permits/components/dashboard/tests/integration/fixtures/getVehicleInfo.ts
index 3a5beda79..3c6ae5573 100644
--- a/frontend/src/features/permits/components/dashboard/tests/integration/fixtures/getVehicleInfo.ts
+++ b/frontend/src/features/permits/components/dashboard/tests/integration/fixtures/getVehicleInfo.ts
@@ -1,17 +1,11 @@
import { factory, nullable, primaryKey } from "@mswjs/data";
-import { DEFAULT_PERMIT_TYPE } from "../../../../../types/PermitType";
import {
PowerUnit,
Trailer,
VEHICLE_TYPES,
} from "../../../../../../manageVehicles/types/Vehicle";
-import {
- getIneligiblePowerUnitSubtypes,
- getIneligibleTrailerSubtypes,
-} from "../../../../../helpers/permitVehicles";
-
let powerUnitId = 1;
let trailerId = 1;
@@ -112,7 +106,11 @@ export const getDefaultPowerUnitSubTypes = () => [
type: "Power Unit Type C",
description: "Power Unit Type C.",
},
- { ...getIneligiblePowerUnitSubtypes(DEFAULT_PERMIT_TYPE)[0] },
+ {
+ typeCode: "BUSCRUM",
+ type: "Buses/Crummies",
+ description: "A motor vehicle used to transport persons, when such transportation is not undertaken for compensation or gain.",
+ },
];
export const getDefaultTrailerSubTypes = () => [
@@ -131,7 +129,11 @@ export const getDefaultTrailerSubTypes = () => [
type: "Trailer Type C",
description: "Trailer Type C.",
},
- { ...getIneligibleTrailerSubtypes(DEFAULT_PERMIT_TYPE)[0] },
+ {
+ typeCode: "DBTRBTR",
+ type: "Tandem/Tridem Drive B-Train (Super B-Train)",
+ description: "B-trains for wood chip residual.",
+ },
];
export const createPowerUnit = (powerUnit: PowerUnit) => {
diff --git a/frontend/src/features/permits/components/dashboard/tests/integration/helpers/prepare.tsx b/frontend/src/features/permits/components/dashboard/tests/integration/helpers/prepare.tsx
index cfef07572..4f3549a32 100644
--- a/frontend/src/features/permits/components/dashboard/tests/integration/helpers/prepare.tsx
+++ b/frontend/src/features/permits/components/dashboard/tests/integration/helpers/prepare.tsx
@@ -13,10 +13,12 @@ import { MANAGE_PROFILE_API } from "../../../../../../manageProfile/apiManager/e
import { getDefaultCompanyInfo } from "../fixtures/getCompanyInfo";
import { getDefaultUserDetails } from "../fixtures/getUserDetails";
import { getDefaultRequiredVal } from "../../../../../../../common/helpers/util";
-import { APPLICATION_STEPS } from "../../../../../../../routes/constants";
-import { Nullable, Optional } from "../../../../../../../common/types/common";
+import { APPLICATION_STEP_CONTEXTS, APPLICATION_STEPS } from "../../../../../../../routes/constants";
+import { Nullable } from "../../../../../../../common/types/common";
import { PERMIT_STATUSES } from "../../../../../types/PermitStatus";
import { SPECIAL_AUTH_API_ROUTES } from "../../../../../../settings/apiManager/endpoints/endpoints";
+import { getCountryFullName } from "../../../../../../../common/helpers/countries/getCountryFullName";
+import { getProvinceFullName } from "../../../../../../../common/helpers/countries/getProvinceFullName";
import {
PowerUnit,
Trailer,
@@ -38,11 +40,6 @@ import {
updateApplication,
} from "../fixtures/getActiveApplication";
-import {
- formatCountry,
- formatProvince,
-} from "../../../../../../../common/helpers/formatCountryProvince";
-
import OnRouteBCContext, {
OnRouteBCContextType,
} from "../../../../../../../common/authentication/OnRouteBCContext";
@@ -283,7 +280,10 @@ export const ComponentWithWrapper = (userDetails: OnRouteBCContextType) => {
return (
-
+
);
@@ -330,10 +330,10 @@ export const getVehicleDetails = (
plate: vehicle.plate,
make: vehicle.make,
year: getDefaultRequiredVal(0, vehicle.year as Nullable),
- country: formatCountry(vehicle.countryCode as Optional),
- province: formatProvince(
- vehicle.countryCode as Optional,
- vehicle.provinceCode as Optional,
+ country: getCountryFullName(vehicle.countryCode),
+ province: getProvinceFullName(
+ vehicle.countryCode,
+ vehicle.provinceCode,
),
vehicleType: "Power Unit",
vehicleSubtype,
diff --git a/frontend/src/features/permits/components/form/CompanyInformation.scss b/frontend/src/features/permits/components/form/CompanyInformation.scss
index 272386451..100fc14f9 100644
--- a/frontend/src/features/permits/components/form/CompanyInformation.scss
+++ b/frontend/src/features/permits/components/form/CompanyInformation.scss
@@ -5,6 +5,8 @@
@include orbcStyles.permit-right-box-style(".company-info__body");
.company-info {
+ border-top: none;
+
& &__info-msg {
width: 320px;
}
diff --git a/frontend/src/features/permits/components/form/CompanyInformation.tsx b/frontend/src/features/permits/components/form/CompanyInformation.tsx
index a99a29463..493257cd9 100644
--- a/frontend/src/features/permits/components/form/CompanyInformation.tsx
+++ b/frontend/src/features/permits/components/form/CompanyInformation.tsx
@@ -3,10 +3,8 @@ import { Box, Typography } from "@mui/material";
import "./CompanyInformation.scss";
import { CompanyProfile } from "../../../manageProfile/types/manageProfile";
import { Nullable } from "../../../../common/types/common";
-import {
- formatCountry,
- formatProvince,
-} from "../../../../common/helpers/formatCountryProvince";
+import { getProvinceFullName } from "../../../../common/helpers/countries/getProvinceFullName";
+import { getCountryFullName } from "../../../../common/helpers/countries/getCountryFullName";
export const CompanyInformation = ({
companyInfo,
@@ -15,15 +13,19 @@ export const CompanyInformation = ({
companyInfo?: Nullable;
doingBusinessAs?: Nullable;
}) => {
+ const countryFullName = getCountryFullName(companyInfo?.mailingAddress?.countryCode);
+ const provinceFullName = getProvinceFullName(
+ companyInfo?.mailingAddress?.countryCode,
+ companyInfo?.mailingAddress?.provinceCode,
+ );
+
return (
-
+
Company Information
-
+
+
{doingBusinessAs ? (
-
+
Doing Business As
-
+
+
{doingBusinessAs}
@@ -49,22 +52,27 @@ export const CompanyInformation = ({
{companyInfo?.mailingAddress ? (
-
+
Company Mailing Address
-
+
+
{companyInfo.mailingAddress.addressLine1}
-
- {formatCountry(companyInfo.mailingAddress.countryCode)}
-
-
- {formatProvince(
- companyInfo.mailingAddress.countryCode,
- companyInfo.mailingAddress.provinceCode,
- )}
-
+
+ {countryFullName ? (
+
+ {countryFullName}
+
+ ) : null}
+
+ {provinceFullName ? (
+
+ {provinceFullName}
+
+ ) : null}
+
{`${companyInfo.mailingAddress.city} ${companyInfo.mailingAddress.postalCode}`}
diff --git a/frontend/src/features/permits/components/form/ContactDetails.scss b/frontend/src/features/permits/components/form/ContactDetails.scss
index ec5757c8f..a0b7061d7 100644
--- a/frontend/src/features/permits/components/form/ContactDetails.scss
+++ b/frontend/src/features/permits/components/form/ContactDetails.scss
@@ -5,10 +5,47 @@
@include orbcStyles.permit-right-box-style(".contact-details-form__body");
.contact-details-form {
- & &__body {
- .bc-gov-alertbanner {
- margin-top: 1.5rem;
- margin-bottom: 1.5rem;
+ & &__input {
+ max-width: 30.625rem;
+
+ .custom-form-control {
+ margin: 1.5rem 0 0 0;
+ }
+
+ &--first-name, &--company-email {
+ .custom-form-control {
+ margin-top: 0;
+ }
+ }
+
+ &--fax {
+ max-width: 19.125rem;
}
}
+
+ .side-by-side-inputs {
+ max-width: 30.625rem;
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ margin: 1.5rem 0 0 0;
+
+ .custom-form-control {
+ margin: 0;
+ }
+
+ &__left-input {
+ max-width: 19.125rem;
+ width: 100%;
+ }
+
+ &__right-input {
+ max-width: 9rem;
+ }
+ }
+
+ & &__info {
+ margin-top: 1.5rem;
+ margin-bottom: 1.5rem;
+ }
}
diff --git a/frontend/src/features/permits/components/form/ContactDetails.tsx b/frontend/src/features/permits/components/form/ContactDetails.tsx
index f8a3259e2..fb9fdf611 100644
--- a/frontend/src/features/permits/components/form/ContactDetails.tsx
+++ b/frontend/src/features/permits/components/form/ContactDetails.tsx
@@ -1,27 +1,27 @@
-import { Box, Typography } from "@mui/material";
+import { Box } from "@mui/material";
+import isEmail from "validator/lib/isEmail";
import "./ContactDetails.scss";
import { InfoBcGovBanner } from "../../../../common/components/banners/InfoBcGovBanner";
import { CustomFormComponent } from "../../../../common/components/form/CustomFormComponents";
+import { BANNER_MESSAGES } from "../../../../common/constants/bannerMessages";
import {
invalidEmail,
invalidExtensionLength,
invalidPhoneLength,
requiredMessage,
} from "../../../../common/helpers/validationMessages";
-import { PHONE_WIDTH, EXT_WIDTH } from "../../../../themes/orbcStyles";
-import { BANNER_MESSAGES } from "../../../../common/constants/bannerMessages";
-import isEmail from "validator/lib/isEmail";
export const ContactDetails = ({ feature }: { feature: string }) => {
return (
- Contact Information
+ Contact Information
{
/>
{
}}
/>
-
+
{
},
label: "Phone Number",
- width: PHONE_WIDTH,
}}
/>
{
},
},
label: "Ext",
- width: EXT_WIDTH,
}}
/>
-
+
{
},
},
label: "Alternate Number",
- width: PHONE_WIDTH,
}}
/>
{
},
},
label: "Ext",
- width: EXT_WIDTH,
}}
/>
-
+
{
/>
{
/>
diff --git a/frontend/src/features/permits/components/form/tests/helpers/CompanyInformation/prepare.tsx b/frontend/src/features/permits/components/form/tests/helpers/CompanyInformation/prepare.tsx
index 072bdb9c7..41b040ab5 100644
--- a/frontend/src/features/permits/components/form/tests/helpers/CompanyInformation/prepare.tsx
+++ b/frontend/src/features/permits/components/form/tests/helpers/CompanyInformation/prepare.tsx
@@ -1,10 +1,8 @@
import { render } from "@testing-library/react";
import { CompanyProfile } from "../../../../../../manageProfile/types/manageProfile";
import { CompanyInformation } from "../../../CompanyInformation";
-import {
- formatCountry,
- formatProvince,
-} from "../../../../../../../common/helpers/formatCountryProvince";
+import { getProvinceFullName } from "../../../../../../../common/helpers/countries/getProvinceFullName";
+import { getCountryFullName } from "../../../../../../../common/helpers/countries/getCountryFullName";
export const defaultCompanyInfo = {
companyId: 74,
@@ -32,10 +30,11 @@ export const defaultCompanyInfo = {
isSuspended: false,
};
-export const country = formatCountry(
+export const country = getCountryFullName(
defaultCompanyInfo.mailingAddress.countryCode,
);
-export const province = formatProvince(
+
+export const province = getProvinceFullName(
defaultCompanyInfo.mailingAddress.countryCode,
defaultCompanyInfo.mailingAddress.provinceCode,
);
diff --git a/frontend/src/features/permits/components/permit-list/Columns.tsx b/frontend/src/features/permits/components/permit-list/Columns.tsx
index 177cb6699..310966ed5 100644
--- a/frontend/src/features/permits/components/permit-list/Columns.tsx
+++ b/frontend/src/features/permits/components/permit-list/Columns.tsx
@@ -88,7 +88,7 @@ export const PermitsColumnDefinition = (
{
accessorKey: "issuer",
id: "issuer",
- header: "Applicant",
+ header: "Issued By",
enableSorting: true,
},
];
\ No newline at end of file
diff --git a/frontend/src/features/permits/constants/constants.ts b/frontend/src/features/permits/constants/constants.ts
index ef343f2a0..ed6a2d0b5 100644
--- a/frontend/src/features/permits/constants/constants.ts
+++ b/frontend/src/features/permits/constants/constants.ts
@@ -4,7 +4,9 @@ import {
PERMIT_CATEGORIES,
PermitCategory,
} from "../types/PermitCategory";
+
import {
+ PERMIT_TYPES,
PermitType,
TERM_PERMIT_LIST,
getPermitTypeShortName,
@@ -37,16 +39,22 @@ export const ALL_PERMIT_TYPE_CHOOSE_FROM_OPTIONS: PermitTypeChooseFromItem[] = [
label: getPermitTypeShortName(permitType),
})),
},
+ {
+ value: PERMIT_CATEGORIES.SINGLE_TRIP,
+ label: getPermitCategoryName(PERMIT_CATEGORIES.SINGLE_TRIP),
+ items: [
+ {
+ value: PERMIT_TYPES.STOS,
+ label: getPermitTypeShortName(PERMIT_TYPES.STOS),
+ },
+ ],
+ // items: SINGLE_TRIP_PERMIT_LIST.map((permitType: PermitType) => ({
+ // value: permitType,
+ // label: getPermitTypeShortName(permitType),
+ // })),
+ },
/* TODO uncomment these when required */
// {
- // value: PERMIT_CATEGORIES.SINGLE_TRIP,
- // label: getPermitCategoryName(PERMIT_CATEGORIES.SINGLE_TRIP),
- // items: SINGLE_TRIP_PERMIT_LIST.map((permitType: PermitType) => ({
- // value: permitType,
- // label: getPermitTypeShortName(permitType),
- // })),
- // },
- // {
// value: PERMIT_CATEGORIES.NON_RESIDENT,
// label: getPermitCategoryName(PERMIT_CATEGORIES.NON_RESIDENT),
// items: NON_RESIDENT_PERMIT_LIST.map((permitType: PermitType) => ({
@@ -68,11 +76,11 @@ export interface PermitTypeChooseFromItem {
}
export const BASE_DAYS_IN_YEAR = 365;
-export const COMMON_MIN_DURATION = 30;
+export const TERM_PERMIT_MIN_DURATION = 30;
export const TERM_DURATION_INTERVAL_DAYS = 30;
-export const COMMON_DURATION_OPTIONS = [
- { value: COMMON_MIN_DURATION, label: "30 Days" },
+export const TERM_PERMIT_DURATION_OPTIONS = [
+ { value: TERM_PERMIT_MIN_DURATION, label: "30 Days" },
{ value: 60, label: "60 Days" },
{ value: 90, label: "90 Days" },
{ value: 120, label: "120 Days" },
@@ -93,3 +101,22 @@ export const LCV_CONDITION = {
checked: true,
disabled: true,
};
+
+export const LCV_VEHICLE_SUBTYPES = [
+ {
+ typeCode: "LCVRMDB",
+ type: "Long Combination Vehicles (LCV) - Rocky Mountain Doubles",
+ description: "LCV vehicles for approved carriers and routes only."
+ },
+ {
+ typeCode: "LCVTPDB",
+ type: "Long Combination Vehicles (LCV) - Turnpike Doubles",
+ description: "LCV vehicles for approved carriers and routes only."
+ },
+];
+
+export const DEFAULT_COMMODITY_SELECT_VALUE = "-";
+export const DEFAULT_COMMODITY_SELECT_OPTION = {
+ value: DEFAULT_COMMODITY_SELECT_VALUE,
+ label: "Select",
+};
diff --git a/frontend/src/features/permits/constants/stos.ts b/frontend/src/features/permits/constants/stos.ts
new file mode 100644
index 000000000..a47944da3
--- /dev/null
+++ b/frontend/src/features/permits/constants/stos.ts
@@ -0,0 +1,68 @@
+import { PermitCondition } from "../types/PermitCondition";
+
+export const STOS_CONDITIONS: PermitCondition[] = [
+ {
+ description: "General Permit Conditions",
+ condition: "CVSE-1000",
+ conditionLink: "https://www.th.gov.bc.ca/forms/getForm.aspx?formId=1251",
+ checked: true,
+ disabled: true
+ },
+ {
+ description: "Permit Scope and Limitation",
+ condition: "CVSE-1070",
+ conditionLink: "https://www.th.gov.bc.ca/forms/getForm.aspx?formId=1261",
+ checked: true,
+ disabled: true
+ },
+ {
+ description: "Routes Pre-Approved for 5.0 m OAW",
+ condition: "CVSE-1001",
+ conditionLink: "",
+ checked: false
+ },
+ {
+ description: "General Permit Conditions to 6.1 m in the Peace River Area",
+ condition: "CVSE-1002",
+ conditionLink: "",
+ checked: false
+ },
+ {
+ description: "East-West Overheight Corridors in the Lower Mainland",
+ condition: "CVSE-1010",
+ conditionLink: "",
+ checked: false
+ },
+ {
+ description: "Routes - Woods Chips & Residual",
+ condition: "CVSE-1012",
+ conditionLink: "https://www.th.gov.bc.ca/forms/getForm.aspx?formId=1259",
+ checked: false
+ },
+ {
+ description: "Restricted Routes for Hauling Wood on Wide Bunks",
+ condition: "CVSE-1013",
+ conditionLink: "https://www.th.gov.bc.ca/forms/getForm.aspx?formId=1254",
+ checked: false
+ },
+];
+
+export const MANDATORY_STOS_CONDITIONS: PermitCondition[] =
+ STOS_CONDITIONS.filter(
+ ({ condition }: PermitCondition) =>
+ condition === "CVSE-1000" || condition === "CVSE-1070"
+ );
+
+export const MIN_STOS_DURATION = 1;
+export const MAX_STOS_DURATION = 7;
+export const STOS_DURATION_OPTIONS = [
+ { value: MIN_STOS_DURATION, label: "1 Day" },
+ { value: 2, label: "2 Days" },
+ { value: 3, label: "3 Days" },
+ { value: 4, label: "4 Days" },
+ { value: 5, label: "5 Days" },
+ { value: 6, label: "6 Days" },
+ { value: MAX_STOS_DURATION, label: "7 Days" },
+];
+
+export const STOS_DURATION_INTERVAL_DAYS = 1;
diff --git a/frontend/src/features/permits/constants/tros.json b/frontend/src/features/permits/constants/tros.json
deleted file mode 100644
index e940d9ad6..000000000
--- a/frontend/src/features/permits/constants/tros.json
+++ /dev/null
@@ -1,158 +0,0 @@
-{
- "tros": {
- "conditions": [
- {
- "description": "General Permit Conditions",
- "condition": "CVSE-1000",
- "conditionLink": "https://www.th.gov.bc.ca/forms/getForm.aspx?formId=1251",
- "checked": true,
- "disabled": true
- },
- {
- "description": "Permit Scope and Limitation",
- "condition": "CVSE-1070",
- "conditionLink": "https://www.th.gov.bc.ca/forms/getForm.aspx?formId=1261",
- "checked": true,
- "disabled": true
- },
- {
- "description": "Supplement for Structures",
- "condition": "CVSE-1000S",
- "conditionLink": "https://www.th.gov.bc.ca/forms/getForm.aspx?formId=1255",
- "checked": false
- },
- {
- "description": "Log Permit Conditions",
- "condition": "CVSE-1000L",
- "conditionLink": "https://www.th.gov.bc.ca/forms/getForm.aspx?formId=1250",
- "checked": false
- },
- {
- "description": "Routes - Woods Chips & Residual",
- "condition": "CVSE-1012",
- "conditionLink": "https://www.th.gov.bc.ca/forms/getForm.aspx?formId=1259",
- "checked": false
- },
- {
- "description": "Restricted Routes for Hauling Wood on Wide Bunks",
- "condition": "CVSE-1013",
- "conditionLink": "https://www.th.gov.bc.ca/forms/getForm.aspx?formId=1254",
- "checked": false
- }
- ],
- "ineligiblePowerUnitSubtypes": [
- {
- "typeCode": "BUSCRUM",
- "type": "Buses/Crummies",
- "description": "A motor vehicle used to transport persons, when such transportation is not undertaken for compensation or gain."
- },
- {
- "typeCode": "CRAFTAT",
- "type": "Cranes, Rubber-Tired Loaders, Firetrucks - All Terrain",
- "description": "Industrial vehicles not designed to transport load on highways."
- },
- {
- "typeCode": "CRAFTMB",
- "type": "Cranes, Rubber-Tired Loaders, Firetrucks - Mobile",
- "description": "Industrial vehicles not designed to transport load on highways."
- },
- {
- "typeCode": "FARMVEH",
- "type": "Farm Vehicles",
- "description": "A Farm Vehicle is a commercial vehicle owned and operated by a farmer, rancher or market gardener, the use of which is confined to purposes connected with his farm, ranch or market garden, including use for pleasure and is not used in connection with any other business in which the owner may be engaged."
- },
- {
- "typeCode": "LCVRMDB",
- "type": "Long Combination Vehicles (LCV) - Rocky Mountain Doubles",
- "description": "LCV vehicles for approved carriers and routes only."
- },
- {
- "typeCode": "LCVTPDB",
- "type": "Long Combination Vehicles (LCV) - Turnpike Doubles",
- "description": "LCV vehicles for approved carriers and routes only."
- },
- {
- "typeCode": "MUNFITR",
- "type": "Municipal Fire Trucks",
- "description": ""
- },
- {
- "typeCode": "OGSERVC",
- "type": "Oil and Gas - Service Rigs",
- "description": "Oil and Gas - Service Rigs are fixed equipment oilfield service trucks and includes rathole augers only equiped with heavy front projected crane (must exceed 14,000 kg tare weight)."
- },
- {
- "typeCode": "OGSRRAH",
- "type": "Oil and Gas - Service Rigs and Rathole Augers Only Equipped with Heavy Front Projected Crane (must exceed 14,000 kg tare weight)",
- "description": "Oil and Gas - Service Rigs are fixed equipment oilfield service trucks and includes rathole augers only equiped with heavy front projected crane (must exceed 14,000 kg tare weight)."
- },
- {
- "typeCode": "SCRAPER",
- "type": "Scrapers",
- "description": "A Scraper is a vehicle that is designed and used primarily for grading of highways, earth moving and other construction work on highways and that is not designed or used primarily for the transportation of persons or property, and that is only incidentally operated or moved over a highway."
- },
- {
- "typeCode": "PUTAXIS",
- "type": "Taxis",
- "description": ""
- }
- ],
- "ineligibleTrailerSubtypes": [
- {
- "typeCode": "DBTRBTR",
- "type": "Tandem/Tridem Drive B-Train (Super B-Train)",
- "description": "B-trains for wood chip residual."
- },
- {
- "typeCode": "SPAUTHV",
- "type": "Specially Authorized Vehicles",
- "description": ""
- },
- {
- "typeCode": "STACTRN",
- "type": "Semi-Trailers - A-Trains and C-Trains",
- "description": "A-Train means a combination of vehicles composed of a truck tractor, a semi-trailer and either, (a) an A dolly and a semi-trailer, or (b) a full trailer. C-Train means a combination of vehicles composed of a truck tractor and a semi-trailer, followed by another semi-trailer attached to the first semi-trailer by the means of a C dolly or C converter dolly."
- },
- {
- "typeCode": "STROPRT",
- "type": "Steering Trailers - Manned",
- "description": "Treated as a semi-trailer axle group and a booster in the Heavy Haul Quick Reference Chart, but with a requirement that the number of axles in the frst axle group cannot exceed the number of axles in the second axle group"
- },
- {
- "typeCode": "STRSELF",
- "type": "Steering Trailers - Self/Remote",
- "description": "Treated as a semi-trailer axle group and a booster in the Heavy Haul Quick Reference Chart, but with a requirement that the number of axles in the first axle group cannot exceed the number of axles in the second axle group."
- },
- {
- "typeCode": "STSTEER",
- "type": "Semi-Trailers - Steering Trailers",
- "description": "See Commercial Transport Procedures Manual chapter 5.3 for details."
- },
- {
- "typeCode": "STWDTAN",
- "type": "Semi-Trailers - Spread Tandems",
- "description": ""
- },
- {
- "typeCode": "XXXXXXX",
- "type": "None",
- "description": "Select \"None\" if no trailer is required and only the power unit is being permitted."
- },
- {
- "typeCode": "MHMBSHL",
- "type": "Manufactured Homes, Modular Buildings, Structures and Houseboats (<= 5.0 m OAW) with Attached Axles",
- "description": ""
- },
- {
- "typeCode": "MHMBSHG",
- "type": "Manufactured Homes, Modular Buildings, Structures and Houseboats (> 5.0 m OAW) with Attached Axles",
- "description": ""
- },
- {
- "typeCode": "PMHWAAX",
- "type": "Park Model Homes with Attached Axles",
- "description": ""
- }
- ]
- }
-}
\ No newline at end of file
diff --git a/frontend/src/features/permits/constants/tros.ts b/frontend/src/features/permits/constants/tros.ts
index df75b6cc9..c589b903a 100644
--- a/frontend/src/features/permits/constants/tros.ts
+++ b/frontend/src/features/permits/constants/tros.ts
@@ -1,22 +1,116 @@
-import { tros } from "./tros.json";
import { PermitCondition } from "../types/PermitCondition";
import {
BASE_DAYS_IN_YEAR,
- COMMON_DURATION_OPTIONS,
- COMMON_MIN_DURATION,
+ TERM_PERMIT_DURATION_OPTIONS,
+ TERM_PERMIT_MIN_DURATION,
TERM_DURATION_INTERVAL_DAYS,
} from "./constants";
-export const TROS_INELIGIBLE_POWERUNITS = [...tros.ineligiblePowerUnitSubtypes];
-export const TROS_INELIGIBLE_TRAILERS = [...tros.ineligibleTrailerSubtypes];
-export const TROS_CONDITIONS: PermitCondition[] = [...tros.conditions];
+export const TROS_ELIGIBLE_VEHICLE_SUBTYPES = [
+ "BOOSTER",
+ "DOLLIES",
+ "EXPANDO",
+ "FEBGHSE",
+ "FECVYER",
+ "FEDRMMX",
+ "FEPNYTR",
+ "FESEMTR",
+ "FEWHELR",
+ "FLOATTR",
+ "FULLLTL",
+ "HIBOEXP",
+ "HIBOFLT",
+ "JEEPSRG",
+ "LOGDGLG",
+ "LOGFULL",
+ "LOGNTAC",
+ "LOGOWBK",
+ "LOGSMEM",
+ "LOGTNDM",
+ "LOGTRIX",
+ "ODTRLEX",
+ "OGOSFDT",
+ "PLATFRM",
+ "POLETRL",
+ "PONYTRL",
+ "REDIMIX",
+ "SEMITRL",
+ "STBTRAN",
+ "STCHIPS",
+ "STCRANE",
+ "STINGAT",
+ "STLOGNG",
+ "STNTSHC",
+ "STREEFR",
+ "STSDBDK",
+ "STSTNGR",
+ "STWHELR",
+ "STWIDWH",
+ "BUSTRLR",
+ "CONCRET",
+ "DDCKBUS",
+ "GRADERS",
+ "LOGGING",
+ "LOGOFFH",
+ "LWBTRCT",
+ "OGBEDTK",
+ "OGOILSW",
+ "PICKRTT",
+ "PLOWBLD",
+ "REGTRCK",
+ "STINGER",
+ "TOWVEHC",
+ "TRKTRAC",
+];
+
+export const TROS_CONDITIONS: PermitCondition[] = [
+ {
+ description: "General Permit Conditions",
+ condition: "CVSE-1000",
+ conditionLink: "https://www.th.gov.bc.ca/forms/getForm.aspx?formId=1251",
+ checked: true,
+ disabled: true,
+ },
+ {
+ description: "Permit Scope and Limitation",
+ condition: "CVSE-1070",
+ conditionLink: "https://www.th.gov.bc.ca/forms/getForm.aspx?formId=1261",
+ checked: true,
+ disabled: true,
+ },
+ {
+ description: "Supplement for Structures",
+ condition: "CVSE-1000S",
+ conditionLink: "https://www.th.gov.bc.ca/forms/getForm.aspx?formId=1255",
+ checked: false,
+ },
+ {
+ description: "Log Permit Conditions",
+ condition: "CVSE-1000L",
+ conditionLink: "https://www.th.gov.bc.ca/forms/getForm.aspx?formId=1250",
+ checked: false,
+ },
+ {
+ description: "Routes - Woods Chips & Residual",
+ condition: "CVSE-1012",
+ conditionLink: "https://www.th.gov.bc.ca/forms/getForm.aspx?formId=1259",
+ checked: false,
+ },
+ {
+ description: "Restricted Routes for Hauling Wood on Wide Bunks",
+ condition: "CVSE-1013",
+ conditionLink: "https://www.th.gov.bc.ca/forms/getForm.aspx?formId=1254",
+ checked: false,
+ }
+];
+
export const MANDATORY_TROS_CONDITIONS: PermitCondition[] =
TROS_CONDITIONS.filter(
({ condition }: PermitCondition) =>
condition === "CVSE-1000" || condition === "CVSE-1070"
);
-export const MIN_TROS_DURATION = COMMON_MIN_DURATION;
+export const MIN_TROS_DURATION = TERM_PERMIT_MIN_DURATION;
export const MAX_TROS_DURATION = BASE_DAYS_IN_YEAR;
-export const TROS_DURATION_OPTIONS = [...COMMON_DURATION_OPTIONS];
+export const TROS_DURATION_OPTIONS = [...TERM_PERMIT_DURATION_OPTIONS];
export const TROS_DURATION_INTERVAL_DAYS = TERM_DURATION_INTERVAL_DAYS;
diff --git a/frontend/src/features/permits/constants/trow.json b/frontend/src/features/permits/constants/trow.json
deleted file mode 100644
index cd66f9aed..000000000
--- a/frontend/src/features/permits/constants/trow.json
+++ /dev/null
@@ -1,326 +0,0 @@
-{
- "trow": {
- "conditions": [
- {
- "description": "General Permit Conditions",
- "condition": "CVSE-1000",
- "conditionLink": "https://www.th.gov.bc.ca/forms/getForm.aspx?formId=1251",
- "checked": true,
- "disabled": true
- },
- {
- "description": "Permit Scope and Limitation",
- "condition": "CVSE-1070",
- "conditionLink": "https://www.th.gov.bc.ca/forms/getForm.aspx?formId=1261",
- "checked": true,
- "disabled": true
- },
- {
- "description": "Highways and Restrictive Load Limits",
- "condition": "CVSE-1011",
- "conditionLink": "https://www.th.gov.bc.ca/forms/getForm.aspx?formId=1258",
- "checked": true,
- "disabled": true
- }
- ],
- "ineligiblePowerUnitSubtypes": [
- {
- "typeCode": "BUSCRUM",
- "type": "Buses/Crummies",
- "description": "A motor vehicle used to transport persons, when such transportation is not undertaken for compensation or gain."
- },
- {
- "typeCode": "BUSTRLR",
- "type": "Intercity Buses (Pulling Pony Trailers)",
- "description": "Intercity Buses are vehicles designed to carry more than 15 passengers and equipped with facilities to allow extended travel without stopping."
- },
- {
- "typeCode": "DDCKBUS",
- "type": "Double Decker Buses",
- "description": "Double Decker buses are used primarily by the tourism industry."
- },
- {
- "typeCode": "FARMVEH",
- "type": "Farm Vehicles",
- "description": "A Farm Vehicle is a commercial vehicle owned and operated by a farmer, rancher or market gardener, the use of which is confined to purposes connected with his farm, ranch or market garden, including use for pleasure and is not used in connection with any other business in which the owner may be engaged."
- },
- {
- "typeCode": "LCVRMDB",
- "type": "Long Combination Vehicles (LCV) - Rocky Mountain Doubles",
- "description": "LCV vehicles for approved carriers and routes only."
- },
- {
- "typeCode": "LCVTPDB",
- "type": "Long Combination Vehicles (LCV) - Turnpike Doubles",
- "description": "LCV vehicles for approved carriers and routes only."
- },
- {
- "typeCode": "LOGGING",
- "type": "Logging Trucks",
- "description": "Logging Truck is a truck or truck and trailer combination used to haul, in their natural state, green felled or bucked logs or poles."
- },
- {
- "typeCode": "LOGOFFH",
- "type": "Logging Trucks - Off-Highway",
- "description": "Logging Trucks - Off-Highway are permitted on an occasional basis from manufacturers to/from customers and to/from the bush for repairs etc., at maximum dimensions noted in Policy."
- },
- {
- "typeCode": "LWBTRCT",
- "type": "Long Wheelbase Truck Tractors Exceeding 6.2 m up to 7.25 m",
- "description": "Long Wheelbase Truck Tractors Exceeding 6.2 m up to 7.25 are vehicles that do not comply with wheelbase requirements indicated in CTR Appendix B"
- },
- {
- "typeCode": "OGBEDTK",
- "type": "Oil and Gas - Bed Trucks",
- "description": "Bed Truck means a truck tractor equipped with a cargo carrying deck and a winch that is used for self loading and that is located behind the cab."
- },
- {
- "typeCode": "PLOWBLD",
- "type": "Trucks Equipped with Front or Underbody Plow Blades",
- "description": "Vehicles not covered by the Highway Maintenance Agreement may be permitted to a width of 3.2 m."
- },
- {
- "typeCode": "PUTAXIS",
- "type": "Taxis",
- "description": ""
- },
- {
- "typeCode": "REGTRCK",
- "type": "Trucks",
- "description": "A Truck is a motor vehicle, other than a bus, that is either permanently fitted with special equipment or is designed to and normally used to carry a load, and that may operate as a single unit or may pull a full trailer or pony trailer."
- },
- {
- "typeCode": "SCRAPER",
- "type": "Scrapers",
- "description": "A Scraper is a vehicle that is designed and used primarily for grading of highways, earth moving and other construction work on highways and that is not designed or used primarily for the transportation of persons or property, and that is only incidentally operated or moved over a highway."
- },
- {
- "typeCode": "SPAUTHV",
- "type": "Specially Authorized Vehicles",
- "description": ""
- },
- {
- "typeCode": "STINGER",
- "type": "Truck Tractors - Stinger Steered",
- "description": "Truck Tractors - Stinger Steered are vehicles equipped with a rear mounted underslung fifth wheel and are considered a truck tractor similar to an auto carrier;"
- },
- {
- "typeCode": "TRKTRAC",
- "type": "Truck Tractors",
- "description": "Truck Tractor is a motor vehicle, having a net weight of more than 4,000 kg, that is equipped with a fifth-wheel coupler or a centre rotatable log bunk mounted on a bolster affixed to the vehicles chassis, and includes an auto carrier with an underslung fifth wheel coupler and a truck tractor with a load box."
- }
- ],
- "ineligibleTrailerSubtypes": [
- {
- "typeCode": "BOOSTER",
- "type": "Boosters",
- "description": "A Booster is similar to a jeep, but it is used behind a load."
- },
- {
- "typeCode": "DBTRBTR",
- "type": "Tandem/Tridem Drive B-Train (Super B-Train)",
- "description": "B-trains for wood chip residual."
- },
- {
- "typeCode": "EXPANDO",
- "type": "Expando Semi-Trailers",
- "description": "For use with Oil Field Bed Truck as power unit only. For other applications use Semi Trailer - Highboy or step deck, lowbed, expandos."
- },
- {
- "typeCode": "FLOATTR",
- "type": "Float Trailers",
- "description": "See Commercial Transport Procedures Manual chapter 4.3 for details."
- },
- {
- "typeCode": "FULLLTL",
- "type": "Full Trailers",
- "description": "See Commercial Transport Procedures Manual chapter 5.3 for details."
- },
- {
- "typeCode": "HIBOEXP",
- "type": "Semi-Trailers - Hiboys/Expandos",
- "description": "See Commercial Transport Procedures Manual chapter 5.3 for details."
- },
- {
- "typeCode": "HIBOFLT",
- "type": "Semi-Trailers - Hiboys/Flat Decks",
- "description": "See Commercial Transport Procedures Manual chapter 5.3 for details."
- },
- {
- "typeCode": "JEEPSRG",
- "type": "Jeeps",
- "description": "Jeep means a semi-trailer that is designed to be attached between a truck tractor and another semi-trailer, so as to distribute the load of the other semi-trailer between the axles of the jeep and axles of the truck tractor."
- },
- {
- "typeCode": "LOGDGLG",
- "type": "Legacy Logging Trailer Combinations - Tandem Pole Trailers, Dogloggers",
- "description": "See Commercial Transport Procedures Manual chapter 5.3 for details."
- },
- {
- "typeCode": "LOGFULL",
- "type": "Logging Trailers - Full Trailers, Tri Axle, Quad Axle",
- "description": "See Commercial Transport Procedures Manual chapter 5.3 for details."
- },
- {
- "typeCode": "LOGNTAC",
- "type": "Legacy Logging Trailer Combinations - Non-TAC B-Trains",
- "description": "See Commercial Transport Procedures Manual chapter 5.3 for details."
- },
- {
- "typeCode": "LOGOWBK",
- "type": "Logging Trailers - Overwidth Bunks",
- "description": "See Commercial Transport Procedures Manual chapter 5.3 for details."
- },
- {
- "typeCode": "LOGSMEM",
- "type": "Logging Semi-Trailer - Empty, 3.2 m Bunks",
- "description": "See Commercial Transport Procedures Manual chapter 5.3 for details."
- },
- {
- "typeCode": "LOGTNDM",
- "type": "Legacy Logging Trailer Combinations - Single Axle Jeeps, Tandem Axle Pole Trailers, Dogloggers",
- "description": "See Commercial Transport Procedures Manual chapter 5.3 for details."
- },
- {
- "typeCode": "LOGTRIX",
- "type": "Legacy Logging Trailer Combinations - Single Axle Jeeps, Tri Axle Trailers",
- "description": "See Commercial Transport Procedures Manual chapter 5.3 for details."
- },
- {
- "typeCode": "MHMBSHG",
- "type": "Manufactured Homes, Modular Buildings, Structures and Houseboats (> 5.0 m OAW) with Attached Axles",
- "description": ""
- },
- {
- "typeCode": "MHMBSHL",
- "type": "Manufactured Homes, Modular Buildings, Structures and Houseboats (<= 5.0 m OAW) with Attached Axles",
- "description": ""
- },
- {
- "typeCode": "ODTRLEX",
- "type": "Overdimensional Trailers and Semi-Trailers (For Export)",
- "description": "See Commercial Transport Procedures Manual chapter 5.3 for details."
- },
- {
- "typeCode": "OGOSFDT",
- "type": "Oil and Gas - Oversize Oilfield Flat Deck Semi-Trailers",
- "description": "See Commercial Transport Procedures Manual chapter 5.3 for details."
- },
- {
- "typeCode": "PLATFRM",
- "type": "Platform Trailers",
- "description": "Extraordinary Loads Only (currently platform trailers require extraordinary load approval)."
- },
- {
- "typeCode": "PMHWAAX",
- "type": "Park Model Homes with Attached Axles",
- "description": ""
- },
- {
- "typeCode": "POLETRL",
- "type": "Pole Trailers",
- "description": "CTR Appendices H and I – plus legacy vehicles from 5.3.7.C including doglogger and sjostrom trailer."
- },
- {
- "typeCode": "PONYTRL",
- "type": "Pony Trailers",
- "description": "See Commercial Transport Procedures Manual chapter 5.3 for details."
- },
- {
- "typeCode": "SEMITRL",
- "type": "Semi-Trailers",
- "description": "See Commercial Transport Procedures Manual chapter 5.3 for details."
- },
- {
- "typeCode": "SPAUTHV",
- "type": "Specially Authorized Vehicles",
- "description": ""
- },
- {
- "typeCode": "STACTRN",
- "type": "Semi-Trailers - A-Trains and C-Trains",
- "description": "A-Train means a combination of vehicles composed of a truck tractor, a semi-trailer and either, (a) an A dolly and a semi-trailer, or (b) a full trailer. C-Train means a combination of vehicles composed of a truck tractor and a semi-trailer, followed by another semi-trailer attached to the first semi-trailer by the means of a C dolly or C converter dolly."
- },
- {
- "typeCode": "STBTRAN",
- "type": "Semi-Trailers - B-Trains",
- "description": "B-trains for transporting reducible or non reducible loads other than wood chip residual."
- },
- {
- "typeCode": "STCHIPS",
- "type": "Semi-Trailers - Walled B-Trains (Chip Trucks)",
- "description": "See Commercial Transport Procedures Manual chapter 4.5.1 for details"
- },
- {
- "typeCode": "STCRANE",
- "type": "Semi-Trailers with Crane",
- "description": "See commercial transport procedures manual chapter 5.3 for details."
- },
- {
- "typeCode": "STINGAT",
- "type": "Stinger Steered Automobile Transporters",
- "description": "See Commercial Transport Procedures Manual chapter 5.3 for details."
- },
- {
- "typeCode": "STLOGNG",
- "type": "Semi-Trailers - Logging",
- "description": "See Commercial Transport Procedures Manual chapter 4.5 for details."
- },
- {
- "typeCode": "STNTSHC",
- "type": "Semi-Trailers - Non-Tac Short Chassis",
- "description": "See Commercial Transport Procedures Manual chapter 5.3 for details."
- },
- {
- "typeCode": "STREEFR",
- "type": "Semi-Trailers - Insulated Vans with Reefer/Refrigeration Units",
- "description": "See commercial transport procedures manual chapter 5.1 for details."
- },
- {
- "typeCode": "STROPRT",
- "type": "Steering Trailers - Manned",
- "description": "Treated as a semi-trailer axle group and a booster in the Heavy Haul Quick Reference Chart, but with a requirement that the number of axles in the frst axle group cannot exceed the number of axles in the second axle group"
- },
- {
- "typeCode": "STRSELF",
- "type": "Steering Trailers - Self/Remote",
- "description": "Treated as a semi-trailer axle group and a booster in the Heavy Haul Quick Reference Chart, but with a requirement that the number of axles in the first axle group cannot exceed the number of axles in the second axle group."
- },
- {
- "typeCode": "STSDBDK",
- "type": "Semi-Trailers - Single Drop, Double Drop, Step Decks, Lowbed, Expandos, etc.",
- "description": "See Commercial Transport Procedures Manual chapter 5.3 for details."
- },
- {
- "typeCode": "STSTEER",
- "type": "Semi-Trailers - Steering Trailers",
- "description": "See Commercial Transport Procedures Manual chapter 5.3 for details."
- },
- {
- "typeCode": "STSTNGR",
- "type": "Semi-Trailers - Stinger Steered Automobile Transporters",
- "description": "See Commercial Transport Procedures Manual chapter 5.3 for details."
- },
- {
- "typeCode": "STWDTAN",
- "type": "Semi-Trailers - Spread Tandems",
- "description": ""
- },
- {
- "typeCode": "STWHELR",
- "type": "Semi-Trailers - Wheelers",
- "description": "Note that at this time, wheeler semi-trailers need extraordinary load approvals except on the pre-approved wheeler 85 tonne routes from 6.3.3.D (different from the other 85 tonne routes))."
- },
- {
- "typeCode": "STWIDWH",
- "type": "Semi-Trailers - Wide Wheelers",
- "description": "Extraordinary Loads Only (currently wide wheelers require extraordinary load approval)."
- },
- {
- "typeCode": "XXXXXXX",
- "type": "None",
- "description": "Select \"None\" if no trailer is required and only the power unit is being permitted."
- }
- ]
- }
-}
diff --git a/frontend/src/features/permits/constants/trow.ts b/frontend/src/features/permits/constants/trow.ts
index c8a73db19..e60ecb2d2 100644
--- a/frontend/src/features/permits/constants/trow.ts
+++ b/frontend/src/features/permits/constants/trow.ts
@@ -1,17 +1,58 @@
-import { trow } from "./trow.json";
import { PermitCondition } from "../types/PermitCondition";
import {
BASE_DAYS_IN_YEAR,
- COMMON_DURATION_OPTIONS,
- COMMON_MIN_DURATION,
+ TERM_PERMIT_DURATION_OPTIONS,
+ TERM_PERMIT_MIN_DURATION,
TERM_DURATION_INTERVAL_DAYS,
} from "./constants";
-export const TROW_INELIGIBLE_POWERUNITS = [...trow.ineligiblePowerUnitSubtypes];
-export const TROW_INELIGIBLE_TRAILERS = [...trow.ineligibleTrailerSubtypes];
-export const TROW_CONDITIONS: PermitCondition[] = [...trow.conditions];
+export const TROW_ELIGIBLE_VEHICLE_SUBTYPES = [
+ "DOLLIES",
+ "FEBGHSE",
+ "FECVYER",
+ "FEDRMMX",
+ "FEPNYTR",
+ "FESEMTR",
+ "FEWHELR",
+ "REDIMIX",
+ "CONCRET",
+ "CRAFTAT",
+ "CRAFTMB",
+ "GRADERS",
+ "MUNFITR",
+ "OGOILSW",
+ "OGSERVC",
+ "OGSRRAH",
+ "PICKRTT",
+ "TOWVEHC",
+];
+
+export const TROW_CONDITIONS: PermitCondition[] = [
+ {
+ description: "General Permit Conditions",
+ condition: "CVSE-1000",
+ conditionLink: "https://www.th.gov.bc.ca/forms/getForm.aspx?formId=1251",
+ checked: true,
+ disabled: true,
+ },
+ {
+ description: "Permit Scope and Limitation",
+ condition: "CVSE-1070",
+ conditionLink: "https://www.th.gov.bc.ca/forms/getForm.aspx?formId=1261",
+ checked: true,
+ disabled: true,
+ },
+ {
+ description: "Highways and Restrictive Load Limits",
+ condition: "CVSE-1011",
+ conditionLink: "https://www.th.gov.bc.ca/forms/getForm.aspx?formId=1258",
+ checked: true,
+ disabled: true,
+ },
+];
+
export const MANDATORY_TROW_CONDITIONS: PermitCondition[] = [...TROW_CONDITIONS];
-export const MIN_TROW_DURATION = COMMON_MIN_DURATION;
+export const MIN_TROW_DURATION = TERM_PERMIT_MIN_DURATION;
export const MAX_TROW_DURATION = BASE_DAYS_IN_YEAR;
-export const TROW_DURATION_OPTIONS = [...COMMON_DURATION_OPTIONS];
+export const TROW_DURATION_OPTIONS = [...TERM_PERMIT_DURATION_OPTIONS];
export const TROW_DURATION_INTERVAL_DAYS = TERM_DURATION_INTERVAL_DAYS;
diff --git a/frontend/src/features/permits/context/ApplicationFormContext.ts b/frontend/src/features/permits/context/ApplicationFormContext.ts
index 42a74b64e..ee9867753 100644
--- a/frontend/src/features/permits/context/ApplicationFormContext.ts
+++ b/frontend/src/features/permits/context/ApplicationFormContext.ts
@@ -1,16 +1,14 @@
import { createContext } from "react";
import { Dayjs } from "dayjs";
+import { Policy } from "onroute-policy-engine";
-import { PermitVehicleDetails } from "../types/PermitVehicleDetails";
import { LOADetail } from "../../settings/types/LOADetail";
import { ApplicationFormData } from "../types/application";
import { getDefaultValues } from "../helpers/getDefaultApplicationFormData";
import { DEFAULT_PERMIT_TYPE } from "../types/PermitType";
-import { PermitCondition } from "../types/PermitCondition";
-import { PowerUnit, Trailer, VehicleSubType } from "../../manageVehicles/types/Vehicle";
+import { PowerUnit, Trailer } 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,
@@ -19,13 +17,14 @@ import {
interface ApplicationFormContextType {
initialFormData: ApplicationFormData;
formData: ApplicationFormData;
+ policyEngine: Nullable;
durationOptions: {
value: number;
label: string;
}[];
- vehicleOptions: (PowerUnit | Trailer)[];
- powerUnitSubtypes: VehicleSubType[];
- trailerSubtypes: VehicleSubType[];
+ allVehiclesFromInventory: (PowerUnit | Trailer)[];
+ powerUnitSubtypeNamesMap: Map;
+ trailerSubtypeNamesMap: Map;
isLcvDesignated: boolean;
feature: string;
companyInfo?: Nullable;
@@ -40,26 +39,23 @@ interface ApplicationFormContextType {
revisionDateTime: string;
comment: string;
}[];
+ policyViolations: Record;
+ clearViolation: (fieldReference: string) => void;
+ triggerPolicyValidation: () => Promise>;
onLeave?: () => void;
onSave?: () => Promise;
onCancel?: () => void;
onContinue: () => Promise;
- onSetDuration: (duration: number) => void;
- onSetExpiryDate: (expiry: Dayjs) => void;
- onSetConditions: (conditions: PermitCondition[]) => void;
- onToggleSaveVehicle: (saveVehicle: boolean) => void;
- onSetVehicle: (vehicleDetails: PermitVehicleDetails) => void;
- onClearVehicle: (saveVehicle: boolean) => void;
- onUpdateLOAs: (updatedLOAs: PermitLOA[]) => void;
}
export const ApplicationFormContext = createContext({
initialFormData: getDefaultValues(DEFAULT_PERMIT_TYPE, undefined),
formData: getDefaultValues(DEFAULT_PERMIT_TYPE, undefined),
+ policyEngine: undefined,
durationOptions: [],
- vehicleOptions: [],
- powerUnitSubtypes: [],
- trailerSubtypes: [],
+ allVehiclesFromInventory: [],
+ powerUnitSubtypeNamesMap: new Map(),
+ trailerSubtypeNamesMap: new Map(),
isLcvDesignated: false,
feature: "",
companyInfo: undefined,
@@ -69,15 +65,11 @@ export const ApplicationFormContext = createContext(
pastStartDateStatus: PAST_START_DATE_STATUSES.ALLOWED,
companyLOAs: [],
revisionHistory: [],
+ policyViolations: {},
+ clearViolation: () => undefined,
+ triggerPolicyValidation: async () => ({}),
onLeave: undefined,
onSave: undefined,
onCancel: undefined,
onContinue: async () => undefined,
- onSetDuration: () => undefined,
- onSetExpiryDate: () => undefined,
- onSetConditions: () => undefined,
- onToggleSaveVehicle: () => undefined,
- onSetVehicle: () => undefined,
- onClearVehicle: () => undefined,
- onUpdateLOAs: () => undefined,
});
diff --git a/frontend/src/features/permits/helpers/conditions.ts b/frontend/src/features/permits/helpers/conditions.ts
index 681c5d263..581a30769 100644
--- a/frontend/src/features/permits/helpers/conditions.ts
+++ b/frontend/src/features/permits/helpers/conditions.ts
@@ -1,5 +1,6 @@
import { isVehicleSubtypeLCV } from "../../manageVehicles/helpers/vehicleSubtypes";
import { LCV_CONDITION } from "../constants/constants";
+import { MANDATORY_STOS_CONDITIONS, STOS_CONDITIONS } from "../constants/stos";
import { MANDATORY_TROS_CONDITIONS, TROS_CONDITIONS } from "../constants/tros";
import { MANDATORY_TROW_CONDITIONS, TROW_CONDITIONS } from "../constants/trow";
import { PermitCondition } from "../types/PermitCondition";
@@ -17,6 +18,8 @@ export const getMandatoryConditions = (
) => {
const additionalConditions = includeLcvCondition ? [LCV_CONDITION] : [];
switch (permitType) {
+ case PERMIT_TYPES.STOS:
+ return MANDATORY_STOS_CONDITIONS.concat(additionalConditions);
case PERMIT_TYPES.TROW:
return MANDATORY_TROW_CONDITIONS.concat(additionalConditions);
case PERMIT_TYPES.TROS:
@@ -32,6 +35,8 @@ const getConditionsByPermitType = (
) => {
const additionalConditions = includeLcvCondition ? [LCV_CONDITION] : [];
switch (permitType) {
+ case PERMIT_TYPES.STOS:
+ return STOS_CONDITIONS.concat(additionalConditions);
case PERMIT_TYPES.TROW:
return TROW_CONDITIONS.concat(additionalConditions);
case PERMIT_TYPES.TROS:
diff --git a/frontend/src/features/permits/helpers/dateSelection.ts b/frontend/src/features/permits/helpers/dateSelection.ts
index 0664b8671..5c2dafb8e 100644
--- a/frontend/src/features/permits/helpers/dateSelection.ts
+++ b/frontend/src/features/permits/helpers/dateSelection.ts
@@ -22,15 +22,29 @@ import {
TROW_DURATION_OPTIONS,
} from "../constants/trow";
+import {
+ MAX_STOS_DURATION,
+ MIN_STOS_DURATION,
+ STOS_DURATION_INTERVAL_DAYS,
+ STOS_DURATION_OPTIONS,
+} from "../constants/stos";
+
/**
* Get list of selectable duration options for a given permit type.
* @param permitType Permit type to get duration options for
* @returns List of selectable duration options for the given permit type
*/
export const durationOptionsForPermitType = (permitType: PermitType) => {
- if (permitType === PERMIT_TYPES.TROS) return TROS_DURATION_OPTIONS;
- if (permitType === PERMIT_TYPES.TROW) return TROW_DURATION_OPTIONS;
- return [];
+ switch (permitType) {
+ case PERMIT_TYPES.STOS:
+ return STOS_DURATION_OPTIONS;
+ case PERMIT_TYPES.TROW:
+ return TROW_DURATION_OPTIONS;
+ case PERMIT_TYPES.TROS:
+ return TROS_DURATION_OPTIONS;
+ default:
+ return [];
+ }
};
/**
@@ -39,9 +53,16 @@ export const durationOptionsForPermitType = (permitType: PermitType) => {
* @returns Mininum allowable duration for the permit type
*/
export const minDurationForPermitType = (permitType: PermitType) => {
- if (permitType === PERMIT_TYPES.TROS) return MIN_TROS_DURATION;
- if (permitType === PERMIT_TYPES.TROW) return MIN_TROW_DURATION;
- return 0;
+ switch (permitType) {
+ case PERMIT_TYPES.STOS:
+ return MIN_STOS_DURATION;
+ case PERMIT_TYPES.TROW:
+ return MIN_TROW_DURATION;
+ case PERMIT_TYPES.TROS:
+ return MIN_TROS_DURATION;
+ default:
+ return 0;
+ }
};
/**
@@ -50,9 +71,16 @@ export const minDurationForPermitType = (permitType: PermitType) => {
* @returns Maxinum allowable duration for the permit type
*/
export const maxDurationForPermitType = (permitType: PermitType) => {
- if (permitType === PERMIT_TYPES.TROS) return MAX_TROS_DURATION;
- if (permitType === PERMIT_TYPES.TROW) return MAX_TROW_DURATION;
- return BASE_DAYS_IN_YEAR;
+ switch (permitType) {
+ case PERMIT_TYPES.STOS:
+ return MAX_STOS_DURATION;
+ case PERMIT_TYPES.TROW:
+ return MAX_TROW_DURATION;
+ case PERMIT_TYPES.TROS:
+ return MAX_TROS_DURATION;
+ default:
+ return BASE_DAYS_IN_YEAR;
+ }
};
/**
@@ -62,6 +90,8 @@ export const maxDurationForPermitType = (permitType: PermitType) => {
*/
export const getDurationIntervalDays = (permitType: PermitType) => {
switch (permitType) {
+ case PERMIT_TYPES.STOS:
+ return STOS_DURATION_INTERVAL_DAYS;
case PERMIT_TYPES.TROW:
return TROW_DURATION_INTERVAL_DAYS;
case PERMIT_TYPES.TROS:
diff --git a/frontend/src/features/permits/helpers/equality.ts b/frontend/src/features/permits/helpers/equality.ts
index 1987ec74a..ada7f2b4a 100644
--- a/frontend/src/features/permits/helpers/equality.ts
+++ b/frontend/src/features/permits/helpers/equality.ts
@@ -6,11 +6,11 @@ import { PermitVehicleDetails } from "../types/PermitVehicleDetails";
import { PermitData } from "../types/PermitData";
import { PermitCondition } from "../types/PermitCondition";
import { arePermitLOADetailsEqual, PermitLOA } from "../types/PermitLOA";
-import { doUniqueArraysHaveSameObjects } from "../../../common/helpers/equality";
-import {
- DATE_FORMATS,
- dayjsToLocalStr,
-} from "../../../common/helpers/formatDate";
+import { areOrderedSequencesEqual, doUniqueArraysHaveSameObjects } from "../../../common/helpers/equality";
+import { ReplaceDayjsWithString } from "../types/utility";
+import { PermittedCommodity } from "../types/PermittedCommodity";
+import { PermittedRoute } from "../types/PermittedRoute";
+import { PermitVehicleConfiguration } from "../types/PermitVehicleConfiguration";
/**
* Compare whether or not two mailing addresses are equal.
@@ -141,26 +141,112 @@ export const arePermitLOAsEqual = (
};
/**
- * Compare whether or not two application data info are equal.
- * @param data1 first application data info
- * @param data2 second application data info
- * @returns true when application data are equivalent, false otherwise
+ * Compare whether or not the permitted commodities for two permits are equal.
+ * @param permittedCommodity1 Permitted commodity belonging to the first permit
+ * @param permittedCommodity2 Permitted commodity belonging to the second permit
+ * @returns true when the two permitted commodities are equivalent, false otherwise
*/
-export const areApplicationDataEqual = (
- data1: PermitData,
- data2: PermitData,
+export const arePermittedCommoditiesEqual = (
+ permittedCommodity1?: Nullable,
+ permittedCommodity2?: Nullable,
+) => {
+ return (
+ getDefaultRequiredVal("", permittedCommodity1?.commodityType)
+ === getDefaultRequiredVal("", permittedCommodity2?.commodityType)
+ ) && (
+ getDefaultRequiredVal("", permittedCommodity1?.loadDescription)
+ === getDefaultRequiredVal("", permittedCommodity2?.loadDescription)
+ );
+};
+
+/**
+ * Compare whether or not the permitted route details for two permits are equal.
+ * @param permittedRoute1 Permitted route details belonging to the first permit
+ * @param permittedRoute2 Permitted route details belonging to the second permit
+ * @returns true when the permitted route details are considered equivalent, false otherwise
+ */
+export const areVehicleConfigurationsEqual = (
+ vehicleConfig1?: Nullable,
+ vehicleConfig2?: Nullable,
+) => {
+ return (
+ getDefaultRequiredVal(0, vehicleConfig1?.overallWidth)
+ === getDefaultRequiredVal(0, vehicleConfig2?.overallWidth)
+ ) && (
+ getDefaultRequiredVal(0, vehicleConfig1?.overallHeight)
+ === getDefaultRequiredVal(0, vehicleConfig2?.overallHeight)
+ ) && (
+ getDefaultRequiredVal(0, vehicleConfig1?.overallLength)
+ === getDefaultRequiredVal(0, vehicleConfig2?.overallLength)
+ ) && (
+ getDefaultRequiredVal(0, vehicleConfig1?.frontProjection)
+ === getDefaultRequiredVal(0, vehicleConfig2?.frontProjection)
+ ) && (
+ getDefaultRequiredVal(0, vehicleConfig1?.rearProjection)
+ === getDefaultRequiredVal(0, vehicleConfig2?.rearProjection)
+ ) && areOrderedSequencesEqual(
+ vehicleConfig1?.trailers,
+ vehicleConfig2?.trailers,
+ (trailer1, trailer2) => trailer1.vehicleSubType === trailer2.vehicleSubType,
+ );
+};
+
+/**
+ * Compare whether or not the permitted route details for two permits are equal.
+ * @param permittedRoute1 Permitted route details belonging to the first permit
+ * @param permittedRoute2 Permitted route details belonging to the second permit
+ * @returns true when the permitted route details are considered equivalent, false otherwise
+ */
+export const arePermittedRoutesEqual = (
+ permittedRoute1?: Nullable,
+ permittedRoute2?: Nullable,
+) => {
+ return (
+ getDefaultRequiredVal("", permittedRoute1?.manualRoute?.origin)
+ === getDefaultRequiredVal("", permittedRoute2?.manualRoute?.origin)
+ ) && (
+ getDefaultRequiredVal("", permittedRoute1?.manualRoute?.destination)
+ === getDefaultRequiredVal("", permittedRoute2?.manualRoute?.destination)
+ ) && (
+ getDefaultRequiredVal("", permittedRoute1?.manualRoute?.exitPoint)
+ === getDefaultRequiredVal("", permittedRoute2?.manualRoute?.exitPoint)
+ ) && (
+ getDefaultRequiredVal(0, permittedRoute1?.manualRoute?.totalDistance)
+ === getDefaultRequiredVal(0, permittedRoute2?.manualRoute?.totalDistance)
+ ) && (
+ getDefaultRequiredVal("", permittedRoute1?.routeDetails)
+ === getDefaultRequiredVal("", permittedRoute2?.routeDetails)
+ ) && areOrderedSequencesEqual(
+ permittedRoute1?.manualRoute?.highwaySequence,
+ permittedRoute2?.manualRoute?.highwaySequence,
+ (seqNumber1, seqNumber2) => seqNumber1 === seqNumber2,
+ );
+};
+
+/**
+ * Compare whether or not the permit data belonging to two applications are equal.
+ * @param data1 Permit data belonging to first application
+ * @param data2 Permit data belonging to second application
+ * @returns true when permit data are equivalent, false otherwise
+ */
+export const areApplicationPermitDataEqual = (
+ data1: ReplaceDayjsWithString,
+ data2: ReplaceDayjsWithString,
) => {
return (
data1.permitDuration === data2.permitDuration &&
- dayjsToLocalStr(data1.startDate, DATE_FORMATS.DATEONLY) ===
- dayjsToLocalStr(data2.startDate, DATE_FORMATS.DATEONLY) &&
- dayjsToLocalStr(data1.expiryDate, DATE_FORMATS.DATEONLY) ===
- dayjsToLocalStr(data2.expiryDate, DATE_FORMATS.DATEONLY) &&
+ data1.startDate === data2.startDate &&
+ data1.expiryDate === data2.expiryDate &&
areContactDetailsEqual(data1.contactDetails, data2.contactDetails) &&
areVehicleDetailsEqual(data1.vehicleDetails, data2.vehicleDetails) &&
areConditionsEqual(data1.commodities, data2.commodities) &&
areMailingAddressesEqual(data1.mailingAddress, data2.mailingAddress) &&
arePermitLOAsEqual(data1.loas, data2.loas) &&
+ arePermittedCommoditiesEqual(data1.permittedCommodity, data2.permittedCommodity) &&
+ areVehicleConfigurationsEqual(data1.vehicleConfiguration, data2.vehicleConfiguration) &&
+ arePermittedRoutesEqual(data1.permittedRoute, data2.permittedRoute) &&
+ (getDefaultRequiredVal("", data1.applicationNotes)
+ === getDefaultRequiredVal("", data2.applicationNotes)) &&
((!data1.companyName && !data2.companyName) ||
data1.companyName === data2.companyName) &&
((!data1.doingBusinessAs && !data2.doingBusinessAs) ||
diff --git a/frontend/src/features/permits/helpers/feeSummary.ts b/frontend/src/features/permits/helpers/feeSummary.ts
index 560f96c85..943da6b06 100644
--- a/frontend/src/features/permits/helpers/feeSummary.ts
+++ b/frontend/src/features/permits/helpers/feeSummary.ts
@@ -30,14 +30,19 @@ export const calculateFeeByDuration = (permitType: PermitType, duration: number)
const intervalPeriodsToPay = safeDuration > 360
? Math.ceil(360 / intervalDays) : Math.ceil(safeDuration / intervalDays);
- if (permitType === PERMIT_TYPES.TROW) {
- // Only for TROW, $100 per interval (30 days)
- return intervalPeriodsToPay * 100;
+ switch (permitType) {
+ // Add more conditions for other permit types if needed
+ case PERMIT_TYPES.STOS:
+ // STOS have constant fee of $15 (regardless of duration)
+ return 15;
+ case PERMIT_TYPES.TROW:
+ // Only for TROW, $100 per interval (30 days)
+ return intervalPeriodsToPay * 100;
+ case PERMIT_TYPES.TROS:
+ default:
+ // For TROS, $30 per interval (30 days)
+ return intervalPeriodsToPay * 30;
}
- // Add more conditions for other permit types if needed
-
- // For TROS, $30 per interval (30 days)
- return intervalPeriodsToPay * 30;
};
/**
diff --git a/frontend/src/features/permits/helpers/getDefaultApplicationFormData.ts b/frontend/src/features/permits/helpers/getDefaultApplicationFormData.ts
index e08a2a6ce..6d79cd660 100644
--- a/frontend/src/features/permits/helpers/getDefaultApplicationFormData.ts
+++ b/frontend/src/features/permits/helpers/getDefaultApplicationFormData.ts
@@ -5,13 +5,19 @@ import { getMandatoryConditions } from "./conditions";
import { Nullable } from "../../../common/types/common";
import { PERMIT_STATUSES } from "../types/PermitStatus";
import { calculateFeeByDuration } from "./feeSummary";
-import { PermitType } from "../types/PermitType";
+import { PERMIT_TYPES, PermitType } from "../types/PermitType";
import { getExpiryDate } from "./permitState";
import { PermitMailingAddress } from "../types/PermitMailingAddress";
import { PermitContactDetails } from "../types/PermitContactDetails";
import { Application, ApplicationFormData } from "../types/application";
import { minDurationForPermitType } from "./dateSelection";
-import { getDefaultVehicleDetails } from "./permitVehicles";
+import { getDefaultVehicleDetails } from "./vehicles/getDefaultVehicleDetails";
+import { getDefaultPermittedRoute } from "./permittedRoute";
+import { getDefaultPermittedCommodity } from "./permittedCommodity";
+import {
+ getDefaultVehicleConfiguration
+} from "./vehicles/configuration/getDefaultVehicleConfiguration";
+
import {
getEndOfDate,
getStartOfDate,
@@ -133,11 +139,11 @@ export const getExpiryDateOrDefault = (
/**
* Gets default values for the application data, or populate with values from existing application and relevant data.
- * @param permitType permit type for the application
- * @param companyInfo data from company profile information (can be undefined, but must be passed as param)
- * @param applicationData existing application data, if any
- * @param userDetails user details of current user, if any
- * @returns default values for the application data
+ * @param permitType Permit type for the application
+ * @param companyInfo Company profile information (can be undefined, but must be passed as param)
+ * @param applicationData Existing application data, if already exists
+ * @param userDetails User details of current user, if any
+ * @returns Default values for the application data
*/
export const getDefaultValues = (
permitType: PermitType,
@@ -166,16 +172,18 @@ export const getDefaultValues = (
applicationData?.permitType,
);
+ const defaultApplicationNumber = getDefaultRequiredVal(
+ "",
+ applicationData?.applicationNumber,
+ );
+
return {
originalPermitId: getDefaultRequiredVal(
"",
applicationData?.originalPermitId,
),
comment: getDefaultRequiredVal("", applicationData?.comment),
- applicationNumber: getDefaultRequiredVal(
- "",
- applicationData?.applicationNumber,
- ),
+ applicationNumber: defaultApplicationNumber,
permitId: getDefaultRequiredVal("", applicationData?.permitId),
permitNumber: getDefaultRequiredVal("", applicationData?.permitNumber),
permitType: defaultPermitType,
@@ -209,8 +217,7 @@ export const getDefaultValues = (
),
),
contactDetails: getDefaultContactDetails(
- getDefaultRequiredVal("", applicationData?.applicationNumber).trim() ===
- "",
+ defaultApplicationNumber.trim() === "",
applicationData?.permitData?.contactDetails,
userDetails,
companyInfo?.email,
@@ -225,6 +232,17 @@ export const getDefaultValues = (
),
feeSummary: `${calculateFeeByDuration(defaultPermitType, durationOrDefault)}`,
loas: getDefaultRequiredVal([], applicationData?.permitData?.loas),
+ permittedRoute: getDefaultPermittedRoute(permitType, applicationData?.permitData?.permittedRoute),
+ applicationNotes: permitType !== PERMIT_TYPES.STOS
+ ? null : getDefaultRequiredVal("", applicationData?.permitData?.applicationNotes),
+ permittedCommodity: getDefaultPermittedCommodity(
+ permitType,
+ applicationData?.permitData?.permittedCommodity,
+ ),
+ vehicleConfiguration: getDefaultVehicleConfiguration(
+ permitType,
+ applicationData?.permitData?.vehicleConfiguration,
+ ),
},
};
};
diff --git a/frontend/src/features/permits/helpers/mappers.ts b/frontend/src/features/permits/helpers/mappers.ts
index e60bc999b..3f82f0587 100644
--- a/frontend/src/features/permits/helpers/mappers.ts
+++ b/frontend/src/features/permits/helpers/mappers.ts
@@ -1,16 +1,17 @@
-import { Permit, PermitsActionResponse } from "../types/permit";
+import { PermitsActionResponse } from "../types/permit";
import { Nullable, Optional } from "../../../common/types/common";
import { getDefaultRequiredVal } from "../../../common/helpers/util";
import {
PERMIT_APPLICATION_ORIGINS,
PermitApplicationOrigin,
} from "../types/PermitApplicationOrigin";
+
import {
IDIR_USER_ROLE,
UserRoleType,
} from "../../../common/authentication/types";
+
import {
- VehicleSubType,
Vehicle,
VehicleType,
VEHICLE_TYPES,
@@ -19,61 +20,50 @@ import {
} from "../../manageVehicles/types/Vehicle";
/**
- * This helper function is used to get the vehicle object that matches the vehicleType and id.
- * @param vehicles List of existing vehicles
- * @param vehicleType Type of vehicle
- * @param id string used as a key to find the existing vehicle
- * @returns The found Vehicle object in the provided list, or undefined if not found
+ * Find a vehicle from a list of existing vehicles that matches the vehicle type and id.
+ * @param existingVehicles List of existing vehicles
+ * @param vehicleType Vehicle type
+ * @param vehicleId Vehicle id
+ * @returns The vehicle found in the provided list, or undefined if not found
*/
-export const mapToVehicleObjectById = (
- vehicles: Optional,
+export const findFromExistingVehicles = (
+ existingVehicles: Vehicle[],
vehicleType: VehicleType,
- id: Nullable,
+ vehicleId?: Nullable,
): Optional => {
- if (!vehicles) return undefined;
-
- return vehicles.find((item) => {
- return vehicleType === VEHICLE_TYPES.POWER_UNIT
- ? item.vehicleType === VEHICLE_TYPES.POWER_UNIT &&
- (item as PowerUnit).powerUnitId === id
- : item.vehicleType === VEHICLE_TYPES.TRAILER &&
- (item as Trailer).trailerId === id;
+ return existingVehicles.find((existingVehicle) => {
+ if (existingVehicle.vehicleType !== vehicleType) return false;
+ return vehicleType === VEHICLE_TYPES.TRAILER
+ ? (existingVehicle as Trailer).trailerId === vehicleId
+ : (existingVehicle as PowerUnit).powerUnitId === vehicleId;
});
};
/**
- * Maps the typeCode (Example: GRADERS) to the corresponding Trailer or PowerUnit subtype object, then return that object
- * @param typeCode
- * @param vehicleType
- * @param powerUnitSubTypes
- * @param trailerSubTypes
- * @returns A Vehicle Sub type object
+ * Get full vehicle subtype name by type code.
+ * @param powerUnitSubtypeNamesMap Map of power unit subtypes (typeCode to subtype name)
+ * @param trailerSubtypeNamesMap Map of trailer subtypes (typeCode to subtype name)
+ * @param vehicleType Vehicle type
+ * @param typeCode Type code for a subtype
+ * @returns The found vehicle subtype name, or undefined if not found
*/
-export const mapTypeCodeToObject = (
- typeCode: string,
+export const getSubtypeNameByCode = (
+ powerUnitSubtypeNamesMap: Map,
+ trailerSubtypeNamesMap: Map,
vehicleType: string,
- powerUnitSubTypes: Nullable,
- trailerSubTypes: Nullable,
+ typeCode: string,
) => {
- let typeObject = undefined;
-
- if (powerUnitSubTypes && vehicleType === VEHICLE_TYPES.POWER_UNIT) {
- typeObject = powerUnitSubTypes.find((v) => {
- return v.typeCode == typeCode;
- });
- } else if (trailerSubTypes && vehicleType === VEHICLE_TYPES.TRAILER) {
- typeObject = trailerSubTypes.find((v) => {
- return v.typeCode == typeCode;
- });
+ if (vehicleType === VEHICLE_TYPES.TRAILER) {
+ return trailerSubtypeNamesMap.get(typeCode);
}
-
- return typeObject;
+
+ return powerUnitSubtypeNamesMap.get(typeCode);
};
/**
* Gets display text for vehicle type.
- * @param vehicleType Vehicle type (powerUnit or trailer)
- * @returns display text for the vehicle type
+ * @param vehicleType Vehicle type (power unit or trailer)
+ * @returns Display text for the vehicle type
*/
export const vehicleTypeDisplayText = (vehicleType: VehicleType) => {
if (vehicleType === VEHICLE_TYPES.TRAILER) {
@@ -82,31 +72,6 @@ export const vehicleTypeDisplayText = (vehicleType: VehicleType) => {
return "Power Unit";
};
-/**
- * Get a cloned permit.
- * @param permit Permit to clone
- * @returns Cloned permit with same fields and nested fields as old permit, but different reference
- */
-export const clonePermit = (permit: Permit): Permit => {
- return {
- ...permit,
- permitData: {
- ...permit.permitData,
- contactDetails: {
- ...permit.permitData.contactDetails,
- },
- vehicleDetails: {
- ...permit.permitData.vehicleDetails,
- },
- commodities: [...permit.permitData.commodities],
- loas: [...getDefaultRequiredVal([], permit.permitData.loas)],
- mailingAddress: {
- ...permit.permitData.mailingAddress,
- },
- },
- };
-};
-
/**
* Remove empty values from permits action response
* @param res Permits action response received from backend
diff --git a/frontend/src/features/permits/helpers/permitLCV.ts b/frontend/src/features/permits/helpers/permitLCV.ts
index 60237460e..f35a5b3f2 100644
--- a/frontend/src/features/permits/helpers/permitLCV.ts
+++ b/frontend/src/features/permits/helpers/permitLCV.ts
@@ -3,7 +3,7 @@ import { isVehicleSubtypeLCV } from "../../manageVehicles/helpers/vehicleSubtype
import { Application, ApplicationFormData } from "../types/application";
import { PermitVehicleDetails } from "../types/PermitVehicleDetails";
import { getPermitConditionSelectionState } from "./conditions";
-import { getDefaultVehicleDetails } from "./permitVehicles";
+import { getDefaultVehicleDetails } from "./vehicles/getDefaultVehicleDetails";
/**
* Get updated vehicle details based on LCV designation.
diff --git a/frontend/src/features/permits/helpers/permitLOA.ts b/frontend/src/features/permits/helpers/permitLOA.ts
index c24c11568..97049954d 100644
--- a/frontend/src/features/permits/helpers/permitLOA.ts
+++ b/frontend/src/features/permits/helpers/permitLOA.ts
@@ -6,10 +6,12 @@ import { getEndOfDate, toLocalDayjs } from "../../../common/helpers/formatDate";
import { Nullable } from "../../../common/types/common";
import { Application, ApplicationFormData } from "../types/application";
import { getDefaultRequiredVal } from "../../../common/helpers/util";
-import { PowerUnit, Trailer, VEHICLE_TYPES } from "../../manageVehicles/types/Vehicle";
+import { PowerUnit, Trailer, Vehicle, VEHICLE_TYPES } from "../../manageVehicles/types/Vehicle";
import { PermitVehicleDetails } from "../types/PermitVehicleDetails";
-import { filterVehicles, getDefaultVehicleDetails } from "./permitVehicles";
+import { getAllowedVehicles } from "./vehicles/getAllowedVehicles";
+import { getDefaultVehicleDetails } from "./vehicles/getDefaultVehicleDetails";
import { PermitLOA } from "../types/PermitLOA";
+import { isPermitVehicleWithinGvwLimit } from "./vehicles/rules/gvw";
import {
durationOptionsForPermitType,
getAvailableDurationOptions,
@@ -118,22 +120,22 @@ export const getUpdatedLOASelection = (
* @param selectedLOAs LOAs that are selected for the permit
* @param vehicleOptions Provided vehicle options for selection
* @param prevSelectedVehicle Previously selected vehicle details in the permit form
- * @param ineligiblePowerUnitSubtypes Ineligible power unit subtypes
- * @param ineligibleTrailerSubtypes Ineligible trailer subtypes
+ * @param eligibleSubtypes Set of eligible vehicle subtypes
+ * @param vehicleRestrictions Restriction rules that each vehicle must meet
* @returns Updated vehicle details and filtered vehicle options
*/
export const getUpdatedVehicleDetailsForLOAs = (
selectedLOAs: PermitLOA[],
vehicleOptions: (PowerUnit | Trailer)[],
prevSelectedVehicle: PermitVehicleDetails,
- ineligiblePowerUnitSubtypes: string[],
- ineligibleTrailerSubtypes: string[],
+ eligibleSubtypes: Set,
+ vehicleRestrictions: ((vehicle: Vehicle) => boolean)[],
) => {
- const filteredVehicles = filterVehicles(
+ const filteredVehicles = getAllowedVehicles(
vehicleOptions,
- ineligiblePowerUnitSubtypes,
- ineligibleTrailerSubtypes,
+ eligibleSubtypes,
selectedLOAs,
+ vehicleRestrictions,
);
const filteredVehicleIds = filteredVehicles.map(filteredVehicle => ({
@@ -167,16 +169,14 @@ export const getUpdatedVehicleDetailsForLOAs = (
* @param applicationData Existing application data
* @param upToDateLOAs Most recent up-to-date company LOAs
* @param inventoryVehicles Vehicle options from the inventory
- * @param ineligiblePowerUnitSubtypes Ineligible power unit subtypes that cannot be used for vehicles
- * @param ineligibleTrailerSubtypes Ineligible trailer subtypes that cannot be used for vehicles
+ * @param eligibleVehicleSubtypes Set of eligible vehicle subtypes that can be used for vehicles
* @returns Application data after applying the up-to-date LOAs
*/
export const applyUpToDateLOAsToApplication = >(
applicationData: T,
upToDateLOAs: LOADetail[],
inventoryVehicles: (PowerUnit | Trailer)[],
- ineligiblePowerUnitSubtypes: string[],
- ineligibleTrailerSubtypes: string[],
+ eligibleVehicleSubtypes: Set,
): T => {
// If application doesn't exist, no need to apply LOAs to it at all
if (!applicationData) return applicationData;
@@ -225,8 +225,15 @@ export const applyUpToDateLOAsToApplication = v.vehicleType !== VEHICLE_TYPES.POWER_UNIT
+ || isPermitVehicleWithinGvwLimit(
+ applicationData.permitType,
+ VEHICLE_TYPES.POWER_UNIT,
+ (v as PowerUnit).licensedGvw,
+ ),
+ ],
);
return {
diff --git a/frontend/src/features/permits/helpers/permitVehicles.ts b/frontend/src/features/permits/helpers/permitVehicles.ts
deleted file mode 100644
index e250c87e0..000000000
--- a/frontend/src/features/permits/helpers/permitVehicles.ts
+++ /dev/null
@@ -1,221 +0,0 @@
-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 { PermitLOA } from "../types/PermitLOA";
-import { applyWhenNotNullable, getDefaultRequiredVal } from "../../../common/helpers/util";
-import { Nullable } from "../../../common/types/common";
-import { PermitVehicleDetails } from "../types/PermitVehicleDetails";
-import { EMPTY_VEHICLE_SUBTYPE, isVehicleSubtypeLCV } from "../../manageVehicles/helpers/vehicleSubtypes";
-import {
- PowerUnit,
- Trailer,
- VehicleSubType,
- VehicleType,
- VEHICLE_TYPES,
- Vehicle,
-} from "../../manageVehicles/types/Vehicle";
-import { sortVehicleSubTypes } from "./sorter";
-
-export const getIneligiblePowerUnitSubtypes = (permitType: PermitType) => {
- switch (permitType) {
- case PERMIT_TYPES.TROW:
- return TROW_INELIGIBLE_POWERUNITS;
- case PERMIT_TYPES.TROS:
- return TROS_INELIGIBLE_POWERUNITS;
- default:
- return [];
- }
-};
-
-export const getIneligibleTrailerSubtypes = (permitType: PermitType) => {
- switch (permitType) {
- case PERMIT_TYPES.TROW:
- return TROW_INELIGIBLE_TRAILERS;
- case PERMIT_TYPES.TROS:
- return TROS_INELIGIBLE_TRAILERS;
- default:
- return [];
- }
-};
-
-/**
- * Get all ineligible power unit and trailer subtypes based on LCV designation and permit type.
- * @param permitType Permit type
- * @param isLcvDesignated Whether or not the LCV designation is used
- * @returns All ineligible power unit and trailer subtypes
- */
-export const getIneligibleSubtypes = (
- permitType: PermitType,
- isLcvDesignated: boolean,
-) => {
- return {
- ineligibleTrailerSubtypes: getIneligibleTrailerSubtypes(permitType),
- ineligiblePowerUnitSubtypes: getIneligiblePowerUnitSubtypes(permitType)
- .filter(subtype => !isLcvDesignated || !isVehicleSubtypeLCV(subtype.typeCode)),
- };
-};
-
-/**
- * Gets default values for vehicle details, or populate with values from existing vehicle details.
- * @param vehicleDetails existing vehicle details, if any
- * @returns default values for vehicle details
- */
-export const getDefaultVehicleDetails = (
- vehicleDetails?: Nullable,
-) => ({
- vehicleId: getDefaultRequiredVal("", vehicleDetails?.vehicleId),
- unitNumber: getDefaultRequiredVal("", vehicleDetails?.unitNumber),
- vin: getDefaultRequiredVal("", vehicleDetails?.vin),
- plate: getDefaultRequiredVal("", vehicleDetails?.plate),
- make: getDefaultRequiredVal("", vehicleDetails?.make),
- year: applyWhenNotNullable((year) => year, vehicleDetails?.year, null),
- countryCode: getDefaultRequiredVal("", vehicleDetails?.countryCode),
- provinceCode: getDefaultRequiredVal("", vehicleDetails?.provinceCode),
- vehicleType: getDefaultRequiredVal("", vehicleDetails?.vehicleType),
- vehicleSubType: getDefaultRequiredVal("", vehicleDetails?.vehicleSubType),
- saveVehicle: false,
-});
-
-/**
- * A helper method that filters eligible power unit or trailer subtypes for dropdown lists.
- * @param allVehicleSubtypes List of both eligible and ineligible vehicle subtypes
- * @param vehicleType Type of vehicle
- * @param ineligiblePowerUnitSubtypes List of provided ineligible power unit subtypes
- * @param ineligibleTrailerSubtypes List of provided ineligible trailer subtypes
- * @param allowedPowerUnitSubtypes List of provided allowed power unit subtypes
- * @param allowedTrailerSubtypes List of provided allowed trailer subtypes
- * @returns List of only eligible power unit or trailer subtypes
- */
-export const filterVehicleSubtypes = (
- allVehicleSubtypes: VehicleSubType[],
- vehicleType: VehicleType,
- ineligiblePowerUnitSubtypes: VehicleSubType[],
- ineligibleTrailerSubtypes: VehicleSubType[],
- allowedPowerUnitSubtypes: string[],
- allowedTrailerSubtypes: string[],
-) => {
- const ineligibleSubtypes = vehicleType === VEHICLE_TYPES.TRAILER
- ? ineligibleTrailerSubtypes : ineligiblePowerUnitSubtypes;
-
- const allowedSubtypes = vehicleType === VEHICLE_TYPES.TRAILER
- ? allowedTrailerSubtypes : allowedPowerUnitSubtypes;
-
- return allVehicleSubtypes.filter((vehicleSubtype) => {
- return allowedSubtypes.some(
- allowedSubtype => vehicleSubtype.typeCode === allowedSubtype
- ) || !ineligibleSubtypes.some(
- (ineligibleSubtype) => vehicleSubtype.typeCode === ineligibleSubtype.typeCode,
- );
- });
-};
-
-/**
- * A helper method that filters power unit or trailer vehicles from dropdown lists.
- * @param vehicles List of both eligible and ineligible vehicles
- * @param ineligiblePowerUnitSubtypes List of ineligible power unit subtypes
- * @param ineligibleTrailerSubtypes List of ineligible trailer subtypes
- * @param loas LOAs that potentially bypass ineligible vehicle restrictions
- * @returns List of only eligible vehicles
- */
-export const filterVehicles = (
- vehicles: Vehicle[],
- ineligiblePowerUnitSubtypes: string[],
- ineligibleTrailerSubtypes: string[],
- loas: PermitLOA[],
-) => {
- const permittedPowerUnitIds = new Set([
- ...loas.map(loa => loa.powerUnits)
- .reduce((prevPowerUnits, currPowerUnits) => [
- ...prevPowerUnits,
- ...currPowerUnits,
- ], []),
- ]);
-
- const permittedTrailerIds = new Set([
- ...loas.map(loa => loa.trailers)
- .reduce((prevTrailers, currTrailers) => [
- ...prevTrailers,
- ...currTrailers,
- ], []),
- ]);
-
- return vehicles.filter((vehicle) => {
- if (vehicle.vehicleType === VEHICLE_TYPES.TRAILER) {
- const trailer = vehicle as Trailer;
- return permittedTrailerIds.has(trailer.trailerId as string)
- || !ineligibleTrailerSubtypes.some((ineligibleSubtype) => {
- return trailer.trailerTypeCode === ineligibleSubtype;
- });
- }
-
- const powerUnit = vehicle as PowerUnit;
- return permittedPowerUnitIds.has(powerUnit.powerUnitId as string)
- || !ineligiblePowerUnitSubtypes.some((ineligibleSubtype) => {
- return powerUnit.powerUnitTypeCode === ineligibleSubtype;
- });
- });
-};
-
-/**
- * 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/helpers/permittedCommodity.ts b/frontend/src/features/permits/helpers/permittedCommodity.ts
new file mode 100644
index 000000000..f945e2abf
--- /dev/null
+++ b/frontend/src/features/permits/helpers/permittedCommodity.ts
@@ -0,0 +1,58 @@
+import { Policy } from "onroute-policy-engine";
+
+import { getDefaultRequiredVal } from "../../../common/helpers/util";
+import { Nullable, RequiredOrNull } from "../../../common/types/common";
+import { PermittedCommodity } from "../types/PermittedCommodity";
+import { PERMIT_TYPES, PermitType } from "../types/PermitType";
+import {
+ DEFAULT_COMMODITY_SELECT_OPTION,
+ DEFAULT_COMMODITY_SELECT_VALUE,
+} from "../constants/constants";
+
+/**
+ * Get default permitted commodity data for an application/permit, or null if not applicable.
+ * @param permitType Permit type
+ * @param permittedCommodity Permitted commodity data if it already exists
+ * @returns Default permitted commodity data, or null
+ */
+export const getDefaultPermittedCommodity = (
+ permitType: PermitType,
+ permittedCommodity?: Nullable,
+): RequiredOrNull => {
+ if (permitType !== PERMIT_TYPES.STOS) return null;
+
+ return {
+ commodityType: getDefaultRequiredVal(
+ DEFAULT_COMMODITY_SELECT_VALUE,
+ permittedCommodity?.commodityType,
+ ),
+ loadDescription: getDefaultRequiredVal(
+ "",
+ permittedCommodity?.loadDescription,
+ ),
+ };
+};
+
+/**
+ * Get list of permitted commodity options for a given permit type.
+ * @param permitType Permit type
+ * @param policyEngine Instance of the policy engine, if it exists
+ * @returns List of permitted commodity options
+ */
+export const getPermittedCommodityOptions = (
+ permitType: PermitType,
+ policyEngine?: Nullable,
+) => {
+ const commodities = getDefaultRequiredVal(
+ new Map(),
+ policyEngine?.getCommodities(permitType),
+ );
+
+ return [DEFAULT_COMMODITY_SELECT_OPTION].concat(
+ [...commodities.entries()]
+ .map(([commodityType, commodityDescription]) => ({
+ value: commodityType,
+ label: commodityDescription,
+ })),
+ );
+};
diff --git a/frontend/src/features/permits/helpers/permittedRoute.ts b/frontend/src/features/permits/helpers/permittedRoute.ts
new file mode 100644
index 000000000..2033de2f8
--- /dev/null
+++ b/frontend/src/features/permits/helpers/permittedRoute.ts
@@ -0,0 +1,26 @@
+import { getDefaultRequiredVal } from "../../../common/helpers/util";
+import { Nullable, RequiredOrNull } from "../../../common/types/common";
+import { PermittedRoute } from "../types/PermittedRoute";
+import { PERMIT_TYPES, PermitType } from "../types/PermitType";
+
+export const getDefaultPermittedRoute = (
+ permitType: PermitType,
+ permittedRoute?: Nullable,
+): RequiredOrNull => {
+ if (permitType !== PERMIT_TYPES.STOS) return null;
+
+ return {
+ manualRoute: {
+ origin: getDefaultRequiredVal("", permittedRoute?.manualRoute?.origin),
+ destination: getDefaultRequiredVal("", permittedRoute?.manualRoute?.destination),
+ highwaySequence: getDefaultRequiredVal(
+ [],
+ permittedRoute?.manualRoute?.highwaySequence)
+ .filter(highwayNumber => Boolean(highwayNumber.trim()),
+ ),
+ exitPoint: getDefaultRequiredVal(null, permittedRoute?.manualRoute?.exitPoint),
+ totalDistance: getDefaultRequiredVal(null, permittedRoute?.manualRoute?.totalDistance),
+ },
+ routeDetails: getDefaultRequiredVal("", permittedRoute?.routeDetails),
+ };
+};
diff --git a/frontend/src/features/permits/helpers/deserializeApplication.ts b/frontend/src/features/permits/helpers/serialize/deserializeApplication.ts
similarity index 80%
rename from frontend/src/features/permits/helpers/deserializeApplication.ts
rename to frontend/src/features/permits/helpers/serialize/deserializeApplication.ts
index 08f9f9e0d..f68797767 100644
--- a/frontend/src/features/permits/helpers/deserializeApplication.ts
+++ b/frontend/src/features/permits/helpers/serialize/deserializeApplication.ts
@@ -1,17 +1,17 @@
import { Dayjs } from "dayjs";
-import { applyWhenNotNullable } from "../../../common/helpers/util";
-import { Application, ApplicationResponseData } from "../types/application";
+import { applyWhenNotNullable } from "../../../../common/helpers/util";
+import { Application, ApplicationResponseData } from "../../types/application";
+import { getDurationOrDefault } from "../getDefaultApplicationFormData";
+import { getExpiryDate } from "../permitState";
+import { minDurationForPermitType } from "../dateSelection";
import {
getEndOfDate,
getStartOfDate,
now,
toLocalDayjs,
utcToLocalDayjs,
-} from "../../../common/helpers/formatDate";
-import { getDurationOrDefault } from "./getDefaultApplicationFormData";
-import { getExpiryDate } from "./permitState";
-import { minDurationForPermitType } from "./dateSelection";
+} from "../../../../common/helpers/formatDate";
/**
* Deserializes an ApplicationResponseData object (received from backend) to an Application object
diff --git a/frontend/src/features/permits/helpers/deserializePermit.ts b/frontend/src/features/permits/helpers/serialize/deserializePermit.ts
similarity index 87%
rename from frontend/src/features/permits/helpers/deserializePermit.ts
rename to frontend/src/features/permits/helpers/serialize/deserializePermit.ts
index 18aa84cd8..8ae487da9 100644
--- a/frontend/src/features/permits/helpers/deserializePermit.ts
+++ b/frontend/src/features/permits/helpers/serialize/deserializePermit.ts
@@ -1,4 +1,4 @@
-import { Permit, PermitResponseData } from "../types/permit";
+import { Permit, PermitResponseData } from "../../types/permit";
/**
* Deserialize a PermitResponseData object (received from backend) to a Permit object.
diff --git a/frontend/src/features/permits/helpers/serializeApplication.ts b/frontend/src/features/permits/helpers/serialize/serializeApplication.ts
similarity index 54%
rename from frontend/src/features/permits/helpers/serializeApplication.ts
rename to frontend/src/features/permits/helpers/serialize/serializeApplication.ts
index c43e2ea38..b70dc65c5 100644
--- a/frontend/src/features/permits/helpers/serializeApplication.ts
+++ b/frontend/src/features/permits/helpers/serialize/serializeApplication.ts
@@ -1,12 +1,13 @@
-import { DATE_FORMATS, dayjsToLocalStr } from "../../../common/helpers/formatDate";
+import { serializePermitData } from "./serializePermitData";
import {
ApplicationFormData,
CreateApplicationRequestData,
UpdateApplicationRequestData,
-} from "../types/application";
+} from "../../types/application";
/**
- * Serializes Application form data to CreateApplicationRequestData so it can be used as payload for create application requests.
+ * Serializes Application form data to CreateApplicationRequestData.
+ * The result can be used as payload for create application requests.
* @param application Application form data
* @returns CreateApplicationRequestData object used for payload data to request to backend
*/
@@ -18,11 +19,7 @@ export const serializeForCreateApplication = (
originalPermitId,
applicationNumber,
permitType,
- permitData: {
- startDate,
- expiryDate,
- ...restOfPermitData
- },
+ permitData,
comment,
} = application;
@@ -32,22 +29,13 @@ export const serializeForCreateApplication = (
applicationNumber,
permitType,
comment,
- permitData: {
- ...restOfPermitData,
- startDate: dayjsToLocalStr(
- startDate,
- DATE_FORMATS.DATEONLY,
- ),
- expiryDate: dayjsToLocalStr(
- expiryDate,
- DATE_FORMATS.DATEONLY,
- ),
- },
+ permitData: serializePermitData(permitData),
};
};
/**
- * Serializes Application form data to UpdateApplicationRequestData so it can be used as payload for update application requests
+ * Serializes Application form data to UpdateApplicationRequestData.
+ * The result can be used as payload for update application requests.
* @param application Application form data
* @returns UpdateApplicationRequestData object used for payload to request to backend
*/
@@ -57,26 +45,12 @@ export const serializeForUpdateApplication = (
const {
permitType,
comment,
- permitData: {
- startDate,
- expiryDate,
- ...restOfPermitData
- },
+ permitData,
} = application;
return {
permitType,
comment,
- permitData: {
- ...restOfPermitData,
- startDate: dayjsToLocalStr(
- startDate,
- DATE_FORMATS.DATEONLY,
- ),
- expiryDate: dayjsToLocalStr(
- expiryDate,
- DATE_FORMATS.DATEONLY,
- ),
- },
+ permitData: serializePermitData(permitData),
};
};
diff --git a/frontend/src/features/permits/helpers/serialize/serializePermitData.ts b/frontend/src/features/permits/helpers/serialize/serializePermitData.ts
new file mode 100644
index 000000000..8cf1a8d89
--- /dev/null
+++ b/frontend/src/features/permits/helpers/serialize/serializePermitData.ts
@@ -0,0 +1,46 @@
+import { DATE_FORMATS, dayjsToLocalStr } from "../../../../common/helpers/formatDate";
+import { PermitData } from "../../types/PermitData";
+import { ReplaceDayjsWithString } from "../../types/utility";
+import { serializePermitVehicleConfiguration } from "./serializePermitVehicleConfiguration";
+import { serializePermitVehicleDetails } from "./serializePermitVehicleDetails";
+
+export const serializePermitData = (
+ permitData: PermitData,
+): ReplaceDayjsWithString => {
+ const {
+ startDate,
+ expiryDate,
+ vehicleDetails,
+ vehicleConfiguration,
+ permittedRoute,
+ ...restOfPermitData
+ } = permitData;
+
+ const serializedPermittedRoute = permittedRoute
+ ? {
+ manualRoute: permittedRoute.manualRoute
+ ? {
+ ...permittedRoute.manualRoute,
+ highwaySequence: permittedRoute.manualRoute.highwaySequence
+ .filter(highwayNumber => Boolean(highwayNumber.trim())),
+ }
+ : null,
+ routeDetails: permittedRoute.routeDetails,
+ }
+ : null;
+
+ return {
+ ...restOfPermitData,
+ startDate: dayjsToLocalStr(
+ startDate,
+ DATE_FORMATS.DATEONLY,
+ ),
+ expiryDate: dayjsToLocalStr(
+ expiryDate,
+ DATE_FORMATS.DATEONLY,
+ ),
+ vehicleDetails: serializePermitVehicleDetails(vehicleDetails),
+ vehicleConfiguration: serializePermitVehicleConfiguration(vehicleConfiguration),
+ permittedRoute: serializedPermittedRoute,
+ };
+};
\ No newline at end of file
diff --git a/frontend/src/features/permits/helpers/serialize/serializePermitVehicleConfiguration.ts b/frontend/src/features/permits/helpers/serialize/serializePermitVehicleConfiguration.ts
new file mode 100644
index 000000000..1ee1f8efb
--- /dev/null
+++ b/frontend/src/features/permits/helpers/serialize/serializePermitVehicleConfiguration.ts
@@ -0,0 +1,36 @@
+import { convertToNumberIfValid } from "../../../../common/helpers/numeric/convertToNumberIfValid";
+import { Nullable, RequiredOrNull } from "../../../../common/types/common";
+import { PermitVehicleConfiguration } from "../../types/PermitVehicleConfiguration";
+
+/**
+ * Serialize permit vehicle configuration values as request payload.
+ * @param vehicleConfiguration Permit vehicle configuration
+ * @returns Serialized permit vehicle configuration values
+ */
+export const serializePermitVehicleConfiguration = (
+ vehicleConfiguration?: Nullable,
+): RequiredOrNull => {
+ return vehicleConfiguration ? {
+ trailers: vehicleConfiguration.trailers,
+ frontProjection: convertToNumberIfValid(
+ vehicleConfiguration.frontProjection,
+ null,
+ ),
+ rearProjection: convertToNumberIfValid(
+ vehicleConfiguration.rearProjection,
+ null,
+ ),
+ overallWidth: convertToNumberIfValid(
+ vehicleConfiguration.overallWidth,
+ null,
+ ),
+ overallHeight: convertToNumberIfValid(
+ vehicleConfiguration.overallHeight,
+ null,
+ ),
+ overallLength: convertToNumberIfValid(
+ vehicleConfiguration.overallLength,
+ null,
+ ),
+ } : null;
+};
diff --git a/frontend/src/features/permits/helpers/serialize/serializePermitVehicleDetails.ts b/frontend/src/features/permits/helpers/serialize/serializePermitVehicleDetails.ts
new file mode 100644
index 000000000..425cb340e
--- /dev/null
+++ b/frontend/src/features/permits/helpers/serialize/serializePermitVehicleDetails.ts
@@ -0,0 +1,32 @@
+import { PermitVehicleDetails } from "../../types/PermitVehicleDetails";
+import { convertToNumberIfValid } from "../../../../common/helpers/numeric/convertToNumberIfValid";
+
+/**
+ * Serialize permit vehicles details data to be used as request payload.
+ * @param vehicleDetails Permit vehicle details info
+ * @param defaultVehicleYear Default vehicle year to fallback to if vehicle details year is invalid
+ * @returns Serialized permit vehicles details data to be used as request payload
+ */
+export const serializePermitVehicleDetails = (
+ vehicleDetails: PermitVehicleDetails,
+): PermitVehicleDetails => {
+ return {
+ vin: vehicleDetails.vin,
+ plate: vehicleDetails.plate,
+ make: vehicleDetails.make,
+ // Convert year to number here, as React doesn't accept valueAsNumber prop for input component
+ year: convertToNumberIfValid(vehicleDetails.year, null),
+ countryCode: vehicleDetails.countryCode,
+ provinceCode: vehicleDetails.provinceCode,
+ vehicleType: vehicleDetails.vehicleType,
+ vehicleSubType: vehicleDetails.vehicleSubType,
+ saveVehicle: vehicleDetails.saveVehicle,
+ unitNumber: vehicleDetails.unitNumber,
+ // Either powerUnitId or trailerId, depending on vehicleType
+ vehicleId: vehicleDetails.vehicleId,
+ licensedGVW: convertToNumberIfValid(
+ vehicleDetails.licensedGVW,
+ null,
+ ),
+ };
+};
\ No newline at end of file
diff --git a/frontend/src/features/permits/helpers/sorter.ts b/frontend/src/features/permits/helpers/sorter.ts
deleted file mode 100644
index 4ce07bc8c..000000000
--- a/frontend/src/features/permits/helpers/sorter.ts
+++ /dev/null
@@ -1,87 +0,0 @@
-import { Optional } from "../../../common/types/common";
-import {
- VehicleSubType,
- BaseVehicle,
- Vehicle,
-} from "../../manageVehicles/types/Vehicle";
-
-/**
- * Sort Power Unit or Trailer Types alphabetically and immutably
- * @param vehicleType string, either powerUnit or trailer
- * @param options array of Vehicle Types
- * @returns an array of sorted vehicle types alphabetically
- */
-export const sortVehicleSubTypes = (
- vehicleType: string,
- options: Optional,
-) => {
- if (!vehicleType || !options) return [];
- const sorted = [...options]; // make copy of original array (original shouldn't be changed)
- sorted.sort((a, b) => {
- if (a.type?.toLowerCase() === b.type?.toLowerCase()) {
- return a.typeCode > b.typeCode ? 1 : -1;
- }
- if (a.type && b.type) return a.type > b.type ? 1 : -1;
- return 0;
- });
- return sorted;
-};
-
-/**
- * @param a Vehicle a
- * @param b Vehicle b
- * @returns 1 or -1 depending on whether a's plate > b's plate
- */
-const sortByPlate = (a: BaseVehicle, b: BaseVehicle) => {
- return a.plate > b.plate ? 1 : -1;
-};
-
-/**
- * @param a Vehicle a
- * @param b Vehicle b
- * @returns 1 or -1 depending on whether a's unitNumber > b's unitNumber
- */
-const sortByUnitNumber = (a: BaseVehicle, b: BaseVehicle) => {
- return (a.unitNumber || -1) > (b.unitNumber || -1) ? 1 : -1;
-};
-
-/**
- * @param a Vehicle a
- * @param b Vehicle b
- * @returns 1 or -1 depending on whether a's vehicleType > b's vehicleType
- */
-const sortByVehicleType = (a: BaseVehicle, b: BaseVehicle) => {
- if (a.vehicleType && b.vehicleType) {
- return a.vehicleType > b.vehicleType ? 1 : -1;
- }
- return 0;
-};
-
-/**
- * Sort Vehicles by Plates and Unit Number alphabetically and immutably
- * @param vehicleType string, either plate or unitNumber
- * @param options array of Vehicles (Power Units and Trailers)
- * @returns an array of sorted vehicles alphabetically
- */
-export const sortVehicles = (
- chooseFrom: string,
- options: Optional,
-) => {
- if (!chooseFrom || !options) return [];
-
- // We shouldn't change original array, but make an copy and sort on that instead
- const sortedVehicles = [...options];
- sortedVehicles.sort((a, b) => {
- // If the vehicle types (powerUnit | trailer) are the same, sort by plate or unitnumber
- if (a.vehicleType?.toLowerCase() === b.vehicleType?.toLowerCase()) {
- if (chooseFrom === "plate") {
- return sortByPlate(a, b);
- }
- return sortByUnitNumber(a, b);
- }
- // else sort by vehicle type
- return sortByVehicleType(a, b);
- });
-
- return sortedVehicles;
-};
diff --git a/frontend/src/features/permits/helpers/vehicles/configuration/getDefaultVehicleConfiguration.ts b/frontend/src/features/permits/helpers/vehicles/configuration/getDefaultVehicleConfiguration.ts
new file mode 100644
index 000000000..59796eb32
--- /dev/null
+++ b/frontend/src/features/permits/helpers/vehicles/configuration/getDefaultVehicleConfiguration.ts
@@ -0,0 +1,20 @@
+import { getDefaultRequiredVal } from "../../../../../common/helpers/util";
+import { Nullable } from "../../../../../common/types/common";
+import { PERMIT_TYPES, PermitType } from "../../../types/PermitType";
+import { PermitVehicleConfiguration } from "../../../types/PermitVehicleConfiguration";
+
+export const getDefaultVehicleConfiguration = (
+ permitType: PermitType,
+ vehicleConfiguration?: Nullable,
+) => {
+ if (permitType !== PERMIT_TYPES.STOS) return null;
+
+ return {
+ frontProjection: getDefaultRequiredVal(null, vehicleConfiguration?.frontProjection),
+ rearProjection: getDefaultRequiredVal(null, vehicleConfiguration?.rearProjection),
+ overallWidth: getDefaultRequiredVal(null, vehicleConfiguration?.overallWidth),
+ overallHeight: getDefaultRequiredVal(null, vehicleConfiguration?.overallHeight),
+ overallLength: getDefaultRequiredVal(null, vehicleConfiguration?.overallLength),
+ trailers: getDefaultRequiredVal([], vehicleConfiguration?.trailers),
+ };
+};
\ No newline at end of file
diff --git a/frontend/src/features/permits/helpers/vehicles/getAllowedVehicles.ts b/frontend/src/features/permits/helpers/vehicles/getAllowedVehicles.ts
new file mode 100644
index 000000000..f74a9f9cf
--- /dev/null
+++ b/frontend/src/features/permits/helpers/vehicles/getAllowedVehicles.ts
@@ -0,0 +1,56 @@
+import { PermitLOA } from "../../types/PermitLOA";
+import {
+ PowerUnit,
+ Trailer,
+ VEHICLE_TYPES,
+ Vehicle,
+} from "../../../manageVehicles/types/Vehicle";
+
+/**
+ * A helper method that filters only allowed vehicles from dropdown lists.
+ * @param vehicles List of all vehicles (both eligible and ineligible)
+ * @param eligibleSubtypes Set of eligible vehicle subtypes
+ * @param loas LOAs that potentially allow certain non-allowed vehicles to be used
+ * @param restrictions Restriction rules that each vehicle must meet
+ * @returns List of only allowed vehicles
+ */
+export const getAllowedVehicles = (
+ vehicles: Vehicle[],
+ eligibleSubtypes: Set,
+ loas: PermitLOA[],
+ restrictions: ((vehicle: Vehicle) => boolean)[],
+) => {
+ const allowedLOAPowerUnitIds = new Set([
+ ...loas.map(loa => loa.powerUnits)
+ .reduce((prevPowerUnits, currPowerUnits) => [
+ ...prevPowerUnits,
+ ...currPowerUnits,
+ ], []),
+ ]);
+
+ const allowedLOATrailerIds = new Set([
+ ...loas.map(loa => loa.trailers)
+ .reduce((prevTrailers, currTrailers) => [
+ ...prevTrailers,
+ ...currTrailers,
+ ], []),
+ ]);
+
+ return vehicles.filter((vehicle) => {
+ if (vehicle.vehicleType === VEHICLE_TYPES.TRAILER) {
+ const trailer = vehicle as Trailer;
+ return allowedLOATrailerIds.has(trailer.trailerId as string)
+ || (
+ eligibleSubtypes.has(trailer.trailerTypeCode)
+ && restrictions.every(restriction => restriction(trailer))
+ );
+ }
+
+ const powerUnit = vehicle as PowerUnit;
+ return allowedLOAPowerUnitIds.has(powerUnit.powerUnitId as string)
+ || (
+ eligibleSubtypes.has(powerUnit.powerUnitTypeCode)
+ && restrictions.every(restriction => restriction(powerUnit))
+ );
+ });
+};
diff --git a/frontend/src/features/permits/helpers/vehicles/getDefaultVehicleDetails.ts b/frontend/src/features/permits/helpers/vehicles/getDefaultVehicleDetails.ts
new file mode 100644
index 000000000..e60fb75a2
--- /dev/null
+++ b/frontend/src/features/permits/helpers/vehicles/getDefaultVehicleDetails.ts
@@ -0,0 +1,25 @@
+import { applyWhenNotNullable, getDefaultRequiredVal } from "../../../../common/helpers/util";
+import { Nullable } from "../../../../common/types/common";
+import { DEFAULT_VEHICLE_TYPE, PermitVehicleDetails } from "../../types/PermitVehicleDetails";
+
+/**
+ * Gets default values for vehicle details, or populate with values from existing vehicle details.
+ * @param vehicleDetails existing vehicle details, if any
+ * @returns default values for vehicle details
+ */
+export const getDefaultVehicleDetails = (
+ vehicleDetails?: Nullable,
+) => ({
+ vehicleId: getDefaultRequiredVal("", vehicleDetails?.vehicleId),
+ unitNumber: getDefaultRequiredVal("", vehicleDetails?.unitNumber),
+ vin: getDefaultRequiredVal("", vehicleDetails?.vin),
+ plate: getDefaultRequiredVal("", vehicleDetails?.plate),
+ make: getDefaultRequiredVal("", vehicleDetails?.make),
+ year: applyWhenNotNullable((year) => year, vehicleDetails?.year, null),
+ countryCode: getDefaultRequiredVal("", vehicleDetails?.countryCode),
+ provinceCode: getDefaultRequiredVal("", vehicleDetails?.provinceCode),
+ vehicleType: getDefaultRequiredVal(DEFAULT_VEHICLE_TYPE, vehicleDetails?.vehicleType),
+ vehicleSubType: getDefaultRequiredVal("", vehicleDetails?.vehicleSubType),
+ licensedGVW: getDefaultRequiredVal(null, vehicleDetails?.licensedGVW),
+ saveVehicle: false,
+});
diff --git a/frontend/src/features/permits/helpers/vehicles/rules/gvw.ts b/frontend/src/features/permits/helpers/vehicles/rules/gvw.ts
new file mode 100644
index 000000000..e5a5dfa33
--- /dev/null
+++ b/frontend/src/features/permits/helpers/vehicles/rules/gvw.ts
@@ -0,0 +1,37 @@
+import { VEHICLE_TYPES, VehicleType } from "../../../../manageVehicles/types/Vehicle";
+import { PERMIT_TYPES, PermitType } from "../../../types/PermitType";
+import {
+ isNull,
+ isUndefined,
+ Nullable,
+ Optional,
+} from "../../../../../common/types/common";
+
+export const gvwLimit = (permitType: PermitType) => {
+ if (permitType === PERMIT_TYPES.STOS) {
+ return 63500;
+ }
+
+ // For term permits the gvw limit is 64000 kg, but this is not yet implemented
+ // In the future, this may change
+ return undefined;
+};
+
+export const isWithinGvwLimit = (
+ gvw?: Nullable,
+ limit?: Optional,
+) => {
+ if (isUndefined(gvw) || isNull(gvw) || isUndefined(limit)) return true;
+ return gvw <= limit;
+};
+
+export const isPermitVehicleWithinGvwLimit = (
+ permitType: PermitType,
+ vehicleType: VehicleType,
+ gvw?: Nullable,
+) => {
+ if (vehicleType !== VEHICLE_TYPES.POWER_UNIT) return true;
+
+ const limit = gvwLimit(permitType);
+ return isWithinGvwLimit(gvw, limit);
+};
diff --git a/frontend/src/features/permits/helpers/vehicles/sortVehicles.ts b/frontend/src/features/permits/helpers/vehicles/sortVehicles.ts
new file mode 100644
index 000000000..0b10f83c6
--- /dev/null
+++ b/frontend/src/features/permits/helpers/vehicles/sortVehicles.ts
@@ -0,0 +1,73 @@
+import { VEHICLE_CHOOSE_FROM, VehicleChooseFrom } from "../../constants/constants";
+import {
+ BaseVehicle,
+ Vehicle,
+ VEHICLE_TYPES,
+} from "../../../manageVehicles/types/Vehicle";
+
+/**
+ * Compare two vehicles by plate.
+ * @param vehicle1 First vehicle
+ * @param vehicle2 Second vehicle
+ * @returns Result of the compare operation between the two vehicles' plates
+ */
+const compareVehiclesByPlate = (vehicle1: BaseVehicle, vehicle2: BaseVehicle) => {
+ return vehicle1.plate.localeCompare(vehicle2.plate);
+};
+
+/**
+ * Compare two vehicles by unit number.
+ * @param vehicle1 First vehicle
+ * @param vehicle2 Second vehicle
+ * @returns Result of the compare operation between the two vehicles' unit numbers
+ */
+const compareVehiclesByUnitNumber = (vehicle1: BaseVehicle, vehicle2: BaseVehicle) => {
+ if (!vehicle1.unitNumber && !vehicle2.unitNumber)
+ return 0;
+
+ if (!vehicle1.unitNumber) return 1;
+ if (!vehicle2.unitNumber) return -1;
+ return vehicle1.unitNumber.localeCompare(vehicle2.unitNumber);
+};
+
+/**
+ * Compare two vehicles by vehicle type.
+ * @param vehicle1 First vehicle
+ * @param vehicle2 Second vehicle
+ * @returns Result of the compare operation between the two vehicles' types
+ */
+const compareVehiclesByVehicleType = (vehicle1: BaseVehicle, vehicle2: BaseVehicle) => {
+ if (!vehicle1.vehicleType && !vehicle2.vehicleType)
+ return 0;
+
+ if (!vehicle1.vehicleType) return 1;
+ if (!vehicle2.vehicleType) return -1;
+ if (vehicle1.vehicleType === vehicle2.vehicleType) return 0;
+
+ return vehicle1.vehicleType === VEHICLE_TYPES.POWER_UNIT ? -1 : 1;
+};
+
+/**
+ * Sort vehicles (by plate or unit number) alphabetically.
+ * @param vehicles Vehicles to sort
+ * @param sortBy Sort key (by plate or unit number)
+ * @returns Sorted vehicles
+ */
+export const sortVehicles = (
+ vehicles: Vehicle[],
+ sortBy: VehicleChooseFrom,
+) => {
+ // We shouldn't change original array, but make an copy and sort on that instead
+ const sortedVehicles = [...vehicles];
+ sortedVehicles.sort((vehicle1, vehicle2) => {
+ const compareByVehicleTypeResult = compareVehiclesByVehicleType(vehicle1, vehicle2);
+ if (compareByVehicleTypeResult !== 0) return compareByVehicleTypeResult;
+
+ if (sortBy === VEHICLE_CHOOSE_FROM.PLATE) {
+ return compareVehiclesByPlate(vehicle1, vehicle2);
+ }
+ return compareVehiclesByUnitNumber(vehicle1, vehicle2);
+ });
+
+ return sortedVehicles;
+};
diff --git a/frontend/src/features/permits/helpers/vehicles/subtypes/getEligibleSubtypeOptions.ts b/frontend/src/features/permits/helpers/vehicles/subtypes/getEligibleSubtypeOptions.ts
new file mode 100644
index 000000000..efccda390
--- /dev/null
+++ b/frontend/src/features/permits/helpers/vehicles/subtypes/getEligibleSubtypeOptions.ts
@@ -0,0 +1,72 @@
+import { EMPTY_VEHICLE_SUBTYPE } from "../../../../manageVehicles/helpers/vehicleSubtypes";
+import { VEHICLE_TYPES, VehicleSubType } from "../../../../manageVehicles/types/Vehicle";
+import { sortVehicleSubtypes } from "./sortVehicleSubtypes";
+
+/**
+ * A helper method that filters allowed vehicle subtypes for dropdown lists.
+ * @param allVehicleSubtypes List of all vehicle subtypes
+ * @param allowedSubtypeCodes Set of allowed vehicle subtype codes
+ * @returns List of only allowed vehicle subtypes
+ */
+const getAllowedVehicleSubtypes = (
+ allVehicleSubtypes: VehicleSubType[],
+ allowedSubtypeCodes: Set,
+) => {
+ return allVehicleSubtypes.filter(({ typeCode }) => allowedSubtypeCodes.has(typeCode));
+};
+
+/**
+ * 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
+ */
+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 eligibleSubtypeCodes Set of eligible vehicle subtype codes by default
+ * @param allowedLOASubtypeCodes Set of vehicle subtypes allowed by selected LOAs
+ * @param vehicleType Vehicle type
+ * @returns Eligible subset of vehicle subtype options
+ */
+export const getEligibleSubtypeOptions = (
+ powerUnitSubtypes: VehicleSubType[],
+ trailerSubtypes: VehicleSubType[],
+ eligibleSubtypesCodes: Set,
+ allowedLOASubtypeCodes: Set,
+ 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 getAllowedVehicleSubtypes(
+ sortedVehicleSubtypes,
+ new Set([...eligibleSubtypesCodes, ...allowedLOASubtypeCodes]),
+ );
+};
diff --git a/frontend/src/features/permits/helpers/vehicles/subtypes/getEligibleVehicleSubtypes.ts b/frontend/src/features/permits/helpers/vehicles/subtypes/getEligibleVehicleSubtypes.ts
new file mode 100644
index 000000000..5ffb1dbc2
--- /dev/null
+++ b/frontend/src/features/permits/helpers/vehicles/subtypes/getEligibleVehicleSubtypes.ts
@@ -0,0 +1,60 @@
+import { Policy } from "onroute-policy-engine";
+
+import { Nullable } from "../../../../../common/types/common";
+import { PERMIT_TYPES, PermitType } from "../../../types/PermitType";
+import { getDefaultRequiredVal } from "../../../../../common/helpers/util";
+import { TROW_ELIGIBLE_VEHICLE_SUBTYPES } from "../../../constants/trow";
+import { TROS_ELIGIBLE_VEHICLE_SUBTYPES } from "../../../constants/tros";
+import {
+ DEFAULT_COMMODITY_SELECT_VALUE,
+ LCV_VEHICLE_SUBTYPES,
+} from "../../../constants/constants";
+
+/**
+ * Get eligible vehicle subtypes based on given criteria.
+ * @param permitType Permit type
+ * @param isLcvDesignated Whether or not LCV flag is designated
+ * @param selectedCommodity The selected commodity, if applicable for this permit type
+ * @param policyEngine The policy engine used to find vehicle subtypes
+ * @returns List of eligible vehicle subtypes that can be used
+ */
+export const getEligibleVehicleSubtypes = (
+ permitType: PermitType,
+ isLcvDesignated: boolean,
+ selectedCommodity?: Nullable,
+ policyEngine?: Nullable,
+) => {
+ const lcvSubtypes = LCV_VEHICLE_SUBTYPES.map(({ typeCode }) => typeCode);
+ switch (permitType) {
+ case PERMIT_TYPES.STOS: {
+ if (!selectedCommodity || !policyEngine || (selectedCommodity === DEFAULT_COMMODITY_SELECT_VALUE))
+ return new Set();
+
+ const subtypesMap = policyEngine.getPermittableVehicleTypes(permitType, selectedCommodity);
+ return new Set(
+ [
+ ...getDefaultRequiredVal(
+ new Map(),
+ subtypesMap.get("powerUnits"),
+ ).keys(),
+ ...getDefaultRequiredVal(
+ new Map(),
+ subtypesMap.get("trailers"),
+ ).keys(),
+ ].concat(isLcvDesignated ? lcvSubtypes : []),
+ );
+ }
+ // Policy engine currently doesn't return vehicle subtypes unless a commodity is provided
+ // which TROW and TROS doesn't have, thus here the hardcoded subtypes are being used
+ case PERMIT_TYPES.TROW:
+ return new Set(
+ TROW_ELIGIBLE_VEHICLE_SUBTYPES.concat(isLcvDesignated ? lcvSubtypes : []),
+ );
+ case PERMIT_TYPES.TROS:
+ return new Set(
+ TROS_ELIGIBLE_VEHICLE_SUBTYPES.concat(isLcvDesignated ? lcvSubtypes : []),
+ );
+ default:
+ return new Set();
+ }
+};
diff --git a/frontend/src/features/permits/helpers/vehicles/subtypes/sortVehicleSubtypes.ts b/frontend/src/features/permits/helpers/vehicles/subtypes/sortVehicleSubtypes.ts
new file mode 100644
index 000000000..6d4501b63
--- /dev/null
+++ b/frontend/src/features/permits/helpers/vehicles/subtypes/sortVehicleSubtypes.ts
@@ -0,0 +1,20 @@
+import { VehicleSubType } from "../../../../manageVehicles/types/Vehicle";
+
+/**
+ * Sort vehicle subtypes alphabetically.
+ * @param vehicleType Vehicle type
+ * @param subtypeOptions Vehicle subtype options
+ * @returns Sorted list of vehicle subtypes
+ */
+export const sortVehicleSubtypes = (
+ vehicleType: string,
+ subtypeOptions: VehicleSubType[],
+) => {
+ if (!vehicleType) return [];
+
+ // Make copy of original array (original shouldn't be changed)
+ const sorted = [...subtypeOptions];
+ sorted.sort((subtype1, subtype2) => subtype1.type.localeCompare(subtype2.type));
+
+ return sorted;
+};
diff --git a/frontend/src/features/permits/hooks/useApplicationFormContext.ts b/frontend/src/features/permits/hooks/form/useApplicationFormContext.ts
similarity index 52%
rename from frontend/src/features/permits/hooks/useApplicationFormContext.ts
rename to frontend/src/features/permits/hooks/form/useApplicationFormContext.ts
index 6d3ec01a0..9fb0070fa 100644
--- a/frontend/src/features/permits/hooks/useApplicationFormContext.ts
+++ b/frontend/src/features/permits/hooks/form/useApplicationFormContext.ts
@@ -1,25 +1,30 @@
import { useContext } from "react";
+import { Policy } from "onroute-policy-engine";
-import { ApplicationFormContext } from "../context/ApplicationFormContext";
-import { usePermitDateSelection } from "./usePermitDateSelection";
-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";
+import { ApplicationFormContext } from "../../context/ApplicationFormContext";
+import { usePermitDateSelection } from "../usePermitDateSelection";
+import { usePermitConditions } from "../usePermitConditions";
+import { getStartOfDate } from "../../../../common/helpers/formatDate";
+import { usePermitVehicles } from "../usePermitVehicles";
+import { arePermitLOADetailsEqual } 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";
+import { useVehicleConfiguration } from "../useVehicleConfiguration";
+import { useApplicationFormUpdateMethods } from "./useApplicationFormUpdateMethods";
+import { usePermittedCommodity } from "../usePermittedCommodity";
+import { DEFAULT_COMMODITY_SELECT_VALUE } from "../../constants/constants";
export const useApplicationFormContext = () => {
+ const applicationFormContextData = useContext(ApplicationFormContext);
const {
initialFormData,
formData,
durationOptions,
- vehicleOptions,
- powerUnitSubtypes,
- trailerSubtypes,
+ allVehiclesFromInventory,
+ powerUnitSubtypeNamesMap,
+ trailerSubtypeNamesMap,
isLcvDesignated,
feature,
companyInfo,
@@ -33,6 +38,13 @@ export const useApplicationFormContext = () => {
onSave,
onCancel,
onContinue,
+ } = applicationFormContextData;
+
+ // This assignment is type-safe since the parent component ensured that
+ // the loading page or error page is rendered when policy engine is null/undefined
+ const policyEngine = applicationFormContextData.policyEngine as Policy;
+
+ const {
onSetDuration,
onSetExpiryDate,
onSetConditions,
@@ -40,7 +52,12 @@ export const useApplicationFormContext = () => {
onSetVehicle,
onClearVehicle,
onUpdateLOAs,
- } = useContext(ApplicationFormContext);
+ onUpdateHighwaySequence,
+ onUpdateVehicleConfigTrailers,
+ onSetCommodityType,
+ onUpdateVehicleConfig,
+ onClearVehicleConfig,
+ } = useApplicationFormUpdateMethods();
const {
permitType,
@@ -55,6 +72,9 @@ export const useApplicationFormContext = () => {
startDate: permitStartDate,
commodities,
vehicleDetails: vehicleFormData,
+ permittedRoute,
+ permittedCommodity,
+ vehicleConfiguration,
} = formData.permitData;
const createdAt = useMemoizedObject(
@@ -98,7 +118,7 @@ export const useApplicationFormContext = () => {
permitType,
startDate,
durationOptions,
- currentSelectedLOAs as PermitLOA[],
+ currentSelectedLOAs,
permitDuration,
onSetDuration,
onSetExpiryDate,
@@ -113,18 +133,16 @@ export const useApplicationFormContext = () => {
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,
+ const {
+ commodityOptions,
+ onChangeCommodityType,
+ } = usePermittedCommodity(
+ policyEngine,
+ permitType,
+ onSetCommodityType,
+ () => onClearVehicle(Boolean(vehicleFormData.saveVehicle)),
+ onClearVehicleConfig,
+ permittedCommodity?.commodityType,
);
// Check to see if vehicle details is still valid after LOA has been deselected
@@ -133,15 +151,38 @@ export const useApplicationFormContext = () => {
filteredVehicleOptions,
subtypeOptions,
isSelectedLOAVehicle,
- } = usePermitVehicleForLOAs(
+ } = usePermitVehicles({
+ policyEngine,
+ permitType,
+ isLcvDesignated,
vehicleFormData,
- vehicleOptions,
- currentSelectedLOAs as PermitLOA[],
- powerUnitSubtypes,
- trailerSubtypes,
- ineligiblePowerUnitSubtypes,
- ineligibleTrailerSubtypes,
- () => onClearVehicle(Boolean(vehicleFormData.saveVehicle)),
+ allVehiclesFromInventory,
+ selectedLOAs: currentSelectedLOAs,
+ powerUnitSubtypeNamesMap,
+ trailerSubtypeNamesMap,
+ onClearVehicle: () => onClearVehicle(Boolean(vehicleFormData.saveVehicle)),
+ selectedCommodity: permittedCommodity?.commodityType,
+ });
+
+ const selectedVehicleConfigSubtypes = useMemoizedArray(
+ getDefaultRequiredVal(
+ [],
+ vehicleConfiguration?.trailers?.map(({ vehicleSubType }) => vehicleSubType),
+ ),
+ (subtype) => subtype,
+ (subtype1, subtype2) => subtype1 === subtype2,
+ );
+
+ const { nextAllowedSubtypes } = useVehicleConfiguration(
+ policyEngine,
+ permitType,
+ getDefaultRequiredVal(
+ DEFAULT_COMMODITY_SELECT_VALUE,
+ permittedCommodity?.commodityType,
+ ),
+ selectedVehicleConfigSubtypes,
+ vehicleFormData.vehicleSubType,
+ onUpdateVehicleConfigTrailers,
);
const memoizedCompanyLOAs = useMemoizedArray(
@@ -160,6 +201,13 @@ export const useApplicationFormContext = () => {
&& historyItem1.comment === historyItem2.comment,
);
+ const highwaySequence = useMemoizedObject(
+ getDefaultRequiredVal([], permittedRoute?.manualRoute?.highwaySequence),
+ (seq1, seq2) =>
+ seq1.length === seq2.length
+ && seq1.every((num, index) => num === seq2[index]),
+ );
+
return {
initialFormData,
permitType,
@@ -182,6 +230,13 @@ export const useApplicationFormContext = () => {
pastStartDateStatus,
companyLOAs: memoizedCompanyLOAs,
revisionHistory: memoizedRevisionHistory,
+ commodityOptions,
+ highwaySequence,
+ nextAllowedSubtypes,
+ powerUnitSubtypeNamesMap,
+ trailerSubtypeNamesMap,
+ selectedVehicleConfigSubtypes,
+ vehicleConfiguration,
onLeave,
onSave,
onCancel,
@@ -193,5 +248,10 @@ export const useApplicationFormContext = () => {
onSetVehicle,
onClearVehicle,
onUpdateLOAs,
+ onUpdateHighwaySequence,
+ onUpdateVehicleConfigTrailers,
+ commodityType: permittedCommodity?.commodityType,
+ onChangeCommodityType,
+ onUpdateVehicleConfig,
};
};
\ No newline at end of file
diff --git a/frontend/src/features/permits/hooks/form/useApplicationFormUpdateMethods.ts b/frontend/src/features/permits/hooks/form/useApplicationFormUpdateMethods.ts
new file mode 100644
index 000000000..6485e7725
--- /dev/null
+++ b/frontend/src/features/permits/hooks/form/useApplicationFormUpdateMethods.ts
@@ -0,0 +1,113 @@
+import dayjs, { Dayjs } from "dayjs";
+import { useCallback } from "react";
+import { useFormContext } from "react-hook-form";
+
+import { PermitCondition } from "../../types/PermitCondition";
+import { PermitLOA } from "../../types/PermitLOA";
+import { PermitVehicleConfiguration, VehicleInConfiguration } from "../../types/PermitVehicleConfiguration";
+import { EMPTY_VEHICLE_DETAILS, PermitVehicleDetails } from "../../types/PermitVehicleDetails";
+import { ApplicationFormData } from "../../types/application";
+import { getDefaultVehicleConfiguration } from "../../helpers/vehicles/configuration/getDefaultVehicleConfiguration";
+import { PermitType } from "../../types/PermitType";
+import { RequiredOrNull } from "../../../../common/types/common";
+
+/**
+ * Hook that returns custom methods that update specific values in the application form.
+ * This allows a degree of control over encapsulation of the form methods (eg. without leaking/allowing form methods
+ * like setValue to be called directly everywhere throughout the child components).
+ *
+ * NOTE: This hook must be used inside a component/hook that is a child of an application FormProvider.
+ * @returns Custom methods to update specific values in the application form
+ */
+export const useApplicationFormUpdateMethods = () => {
+ const { setValue } = useFormContext();
+
+ const onSetDuration = useCallback((duration: number) => {
+ setValue("permitData.permitDuration", duration);
+ }, [setValue]);
+
+ const onSetExpiryDate = useCallback((expiry: Dayjs) => {
+ setValue("permitData.expiryDate", dayjs(expiry));
+ }, [setValue]);
+
+ const onSetConditions = useCallback((conditions: PermitCondition[]) => {
+ setValue("permitData.commodities", [...conditions]);
+ }, [setValue]);
+
+ const onToggleSaveVehicle = useCallback((saveVehicle: boolean) => {
+ setValue("permitData.vehicleDetails.saveVehicle", saveVehicle);
+ }, [setValue]);
+
+ const onSetVehicle = useCallback((vehicleDetails: PermitVehicleDetails) => {
+ setValue("permitData.vehicleDetails", {
+ ...vehicleDetails,
+ });
+ }, [setValue]);
+
+ const onClearVehicle = useCallback((saveVehicle: boolean) => {
+ setValue("permitData.vehicleDetails", {
+ ...EMPTY_VEHICLE_DETAILS,
+ saveVehicle,
+ });
+ }, [setValue]);
+
+ const onSetCommodityType = useCallback((commodityType: string) => {
+ setValue("permitData.permittedCommodity.commodityType", commodityType);
+ }, [setValue]);
+
+ const onUpdateLOAs = useCallback((updatedLOAs: PermitLOA[]) => {
+ setValue("permitData.loas", updatedLOAs);
+ }, [setValue]);
+
+ const onUpdateHighwaySequence = useCallback((updatedHighwaySequence: string[]) => {
+ setValue(
+ "permitData.permittedRoute.manualRoute.highwaySequence",
+ updatedHighwaySequence,
+ );
+ }, [setValue]);
+
+ const onUpdateVehicleConfigTrailers = useCallback(
+ (updatedTrailerSubtypes: VehicleInConfiguration[]) => {
+ setValue(
+ "permitData.vehicleConfiguration.trailers",
+ updatedTrailerSubtypes,
+ );
+ },
+ [setValue],
+ );
+
+ const onUpdateVehicleConfig = useCallback(
+ (updatedVehicleConfig: RequiredOrNull) => {
+ setValue(
+ "permitData.vehicleConfiguration",
+ updatedVehicleConfig,
+ );
+ },
+ [setValue],
+ );
+
+ const onClearVehicleConfig = useCallback(
+ (permitType: PermitType) => {
+ setValue(
+ "permitData.vehicleConfiguration",
+ getDefaultVehicleConfiguration(permitType),
+ );
+ },
+ [setValue],
+ );
+
+ return {
+ onSetDuration,
+ onSetExpiryDate,
+ onSetConditions,
+ onToggleSaveVehicle,
+ onSetVehicle,
+ onClearVehicle,
+ onUpdateLOAs,
+ onUpdateHighwaySequence,
+ onUpdateVehicleConfigTrailers,
+ onSetCommodityType,
+ onUpdateVehicleConfig,
+ onClearVehicleConfig,
+ };
+};
diff --git a/frontend/src/features/permits/hooks/form/useInitApplicationFormData.ts b/frontend/src/features/permits/hooks/form/useInitApplicationFormData.ts
new file mode 100644
index 000000000..01a3b08de
--- /dev/null
+++ b/frontend/src/features/permits/hooks/form/useInitApplicationFormData.ts
@@ -0,0 +1,107 @@
+import { useEffect, useMemo } from "react";
+import { useForm } from "react-hook-form";
+import { Policy } from "onroute-policy-engine";
+
+import { Application, ApplicationFormData } from "../../types/application";
+import { BCeIDUserDetailContext } from "../../../../common/authentication/OnRouteBCContext";
+import { CompanyProfile } from "../../../manageProfile/types/manageProfile";
+import { Nullable } from "../../../../common/types/common";
+import { PermitType } from "../../types/PermitType";
+import { LOADetail } from "../../../settings/types/LOADetail";
+import { applyUpToDateLOAsToApplication } from "../../helpers/permitLOA";
+import { getDefaultValues } from "../../helpers/getDefaultApplicationFormData";
+import { applyLCVToApplicationData } from "../../helpers/permitLCV";
+import { PowerUnit, Trailer } from "../../../manageVehicles/types/Vehicle";
+import { getEligibleVehicleSubtypes } from "../../helpers/vehicles/subtypes/getEligibleVehicleSubtypes";
+
+/**
+ * Custom hook for populating the form using fetched application data, as well as current company id and user details.
+ * This also involves resetting certain form values whenever new/updated application data is fetched.
+ * @param permitType Permit type for the application
+ * @param isLcvDesignated Whether or not the company is designated to use LCV for permits
+ * @param companyLOAs Most up-to-date LOAs belonging to the company
+ * @param inventoryVehicles Vehicles in the inventory for the company
+ * @param companyInfo Company information for filling out the form
+ * @param applicationData Application data received to fill out the form, preferrably from ApplicationContext/backend
+ * @param userDetails User details for filling out the form
+ * @param policyEngine Instance of the policy engine, if it's available
+ * @returns Current application form data, methods to manage the form, and selectable input options
+ */
+export const useInitApplicationFormData = (
+ data: {
+ permitType: PermitType;
+ isLcvDesignated: boolean;
+ companyLOAs: LOADetail[];
+ inventoryVehicles: (PowerUnit | Trailer)[];
+ companyInfo: Nullable;
+ applicationData?: Nullable;
+ userDetails?: BCeIDUserDetailContext;
+ policyEngine?: Nullable;
+ },
+) => {
+ const {
+ permitType,
+ isLcvDesignated,
+ companyLOAs,
+ inventoryVehicles,
+ companyInfo,
+ applicationData,
+ userDetails,
+ policyEngine,
+ } = data;
+
+ // Used to populate/initialize the form with
+ // This will be updated whenever new application, company, and user data is fetched
+ const initialFormData = useMemo(() => {
+ const eligibleVehicleSubtypes = getEligibleVehicleSubtypes(
+ permitType,
+ isLcvDesignated,
+ applicationData?.permitData?.permittedCommodity?.commodityType,
+ policyEngine,
+ );
+
+ return applyUpToDateLOAsToApplication(
+ applyLCVToApplicationData(
+ getDefaultValues(
+ permitType,
+ companyInfo,
+ applicationData,
+ userDetails,
+ ),
+ isLcvDesignated,
+ ),
+ companyLOAs,
+ inventoryVehicles,
+ eligibleVehicleSubtypes,
+ );
+ }, [
+ permitType,
+ companyInfo,
+ applicationData,
+ userDetails,
+ isLcvDesignated,
+ companyLOAs,
+ inventoryVehicles,
+ policyEngine,
+ ]);
+
+ // Register default values with react-hook-form
+ const formMethods = useForm({
+ defaultValues: initialFormData,
+ reValidateMode: "onChange",
+ });
+
+ const { watch, reset } = formMethods;
+ const currentFormData = watch();
+
+ // Reset the form with updated default form data whenever fetched data changes
+ useEffect(() => {
+ reset(initialFormData);
+ }, [initialFormData]);
+
+ return {
+ initialFormData,
+ currentFormData,
+ formMethods,
+ };
+};
diff --git a/frontend/src/features/permits/hooks/hooks.ts b/frontend/src/features/permits/hooks/hooks.ts
index 541d1a037..8719b8a84 100644
--- a/frontend/src/features/permits/hooks/hooks.ts
+++ b/frontend/src/features/permits/hooks/hooks.ts
@@ -1,6 +1,7 @@
import { useState, useEffect } from "react";
import { AxiosError } from "axios";
import { MRT_PaginationState } from "material-react-table";
+import { useNavigate } from "react-router-dom";
import {
useQueryClient,
useMutation,
@@ -8,21 +9,27 @@ import {
keepPreviousData,
} from "@tanstack/react-query";
-import { Application, ApplicationFormData } from "../types/application";
import { IssuePermitsResponse } from "../types/permit";
import { StartTransactionResponseData } from "../types/payment";
-import {
- APPLICATION_STEPS,
- ApplicationStep,
- ERROR_ROUTES,
-} from "../../../routes/constants";
import { isPermitTypeValid } from "../types/PermitType";
import { isPermitIdNumeric } from "../helpers/permitState";
-import { deserializeApplicationResponse } from "../helpers/deserializeApplication";
-import { deserializePermitResponse } from "../helpers/deserializePermit";
+import { deserializeApplicationResponse } from "../helpers/serialize/deserializeApplication";
+import { deserializePermitResponse } from "../helpers/serialize/deserializePermit";
import { AmendPermitFormData } from "../pages/Amend/types/AmendPermitFormData";
import { Nullable, Optional } from "../../../common/types/common";
import { useTableControls } from "./useTableControls";
+import { getDefaultRequiredVal } from "../../../common/helpers/util";
+import {
+ Application,
+ ApplicationFormData,
+} from "../types/application";
+
+import {
+ APPLICATION_STEPS,
+ ApplicationStep,
+ ERROR_ROUTES,
+} from "../../../routes/constants";
+
import {
getApplication,
getPermit,
@@ -39,8 +46,6 @@ import {
resendPermit,
getPendingPermits,
} from "../apiManager/permitsAPI";
-import { getDefaultRequiredVal } from "../../../common/helpers/util";
-import { useNavigate } from "react-router-dom";
const QUERY_KEYS = {
PERMIT_DETAIL: (
@@ -63,7 +68,7 @@ const QUERY_KEYS = {
*/
export const useSaveApplicationMutation = () => {
const queryClient = useQueryClient();
- const navigate = useNavigate();
+
return useMutation({
mutationFn: async ({
data,
@@ -72,34 +77,17 @@ export const useSaveApplicationMutation = () => {
data: ApplicationFormData;
companyId: number;
}) => {
- const res = data.permitId
+ return data.permitId
? await updateApplication(data, data.permitId, companyId)
- : await createApplication(data, companyId);
-
+ : await createApplication(data, companyId);
+ },
+ onSuccess: (res) => {
if (res.status === 200 || res.status === 201) {
queryClient.invalidateQueries({
queryKey: ["application"],
});
-
- return {
- application: deserializeApplicationResponse(res.data),
- status: res.status,
- };
- } else {
- return {
- application: null,
- status: res.status,
- };
}
},
- onError: (error: AxiosError) => {
- console.error(error);
- navigate(ERROR_ROUTES.UNEXPECTED, {
- state: {
- correlationId: error?.response?.headers["x-correlation-id"],
- },
- });
- },
});
};
diff --git a/frontend/src/features/permits/hooks/useCommodityOptions.ts b/frontend/src/features/permits/hooks/useCommodityOptions.ts
new file mode 100644
index 000000000..c843d364a
--- /dev/null
+++ b/frontend/src/features/permits/hooks/useCommodityOptions.ts
@@ -0,0 +1,19 @@
+import { useMemo } from "react";
+import { Policy } from "onroute-policy-engine";
+
+import { getPermittedCommodityOptions } from "../helpers/permittedCommodity";
+import { Nullable } from "../../../common/types/common";
+import { PermitType } from "../types/PermitType";
+
+export const useCommodityOptions = (
+ policyEngine: Nullable,
+ permitType: PermitType,
+) => {
+ const commodityOptions = useMemo(() => {
+ return getPermittedCommodityOptions(permitType, policyEngine);
+ }, [policyEngine, permitType]);
+
+ return {
+ commodityOptions,
+ };
+};
diff --git a/frontend/src/features/permits/hooks/useInitApplicationFormData.ts b/frontend/src/features/permits/hooks/useInitApplicationFormData.ts
deleted file mode 100644
index 1b410ff0b..000000000
--- a/frontend/src/features/permits/hooks/useInitApplicationFormData.ts
+++ /dev/null
@@ -1,134 +0,0 @@
-import { useCallback, useEffect, useMemo } from "react";
-import { useForm } from "react-hook-form";
-import dayjs, { Dayjs } from "dayjs";
-
-import { Application, ApplicationFormData } from "../types/application";
-import { BCeIDUserDetailContext } from "../../../common/authentication/OnRouteBCContext";
-import { CompanyProfile } from "../../manageProfile/types/manageProfile";
-import { Nullable } from "../../../common/types/common";
-import { PermitType } from "../types/PermitType";
-import { LOADetail } from "../../settings/types/LOADetail";
-import { applyUpToDateLOAsToApplication } from "../helpers/permitLOA";
-import { getDefaultValues } from "../helpers/getDefaultApplicationFormData";
-import { applyLCVToApplicationData } from "../helpers/permitLCV";
-import { PowerUnit, Trailer } from "../../manageVehicles/types/Vehicle";
-import { getIneligibleSubtypes } from "../helpers/permitVehicles";
-import { PermitCondition } from "../types/PermitCondition";
-import { EMPTY_VEHICLE_DETAILS, PermitVehicleDetails } from "../types/PermitVehicleDetails";
-import { PermitLOA } from "../types/PermitLOA";
-
-/**
- * Custom hook for populating the form using fetched application data, as well as current company id and user details.
- * This also involves resetting certain form values whenever new/updated application data is fetched.
- * @param permitType Permit type for the application
- * @param isLcvDesignated Whether or not the company is designated to use LCV for permits
- * @param loas Most up-to-date LOAs belonging to the company
- * @param companyInfo Company information for filling out the form
- * @param applicationData Application data received to fill out the form, preferrably from ApplicationContext/backend
- * @param userDetails User details for filling out the form
- * @returns Current application form data, methods to manage the form, and selectable input options
- */
-export const useInitApplicationFormData = (
- permitType: PermitType,
- isLcvDesignated: boolean,
- companyLOAs: LOADetail[],
- inventoryVehicles: (PowerUnit | Trailer)[],
- companyInfo: Nullable,
- applicationData?: Nullable,
- userDetails?: BCeIDUserDetailContext,
-) => {
- // Used to populate/initialize the form with
- // This will be updated whenever new application, company, and user data is fetched
- const initialFormData = useMemo(() => {
- const ineligibleSubtypes = getIneligibleSubtypes(permitType, isLcvDesignated);
- const ineligiblePowerUnitSubtypes= ineligibleSubtypes.ineligiblePowerUnitSubtypes
- .map(({ typeCode }) => typeCode);
-
- const ineligibleTrailerSubtypes = ineligibleSubtypes.ineligibleTrailerSubtypes
- .map(({ typeCode }) => typeCode);
-
- return applyUpToDateLOAsToApplication(
- applyLCVToApplicationData(
- getDefaultValues(
- permitType,
- companyInfo,
- applicationData,
- userDetails,
- ),
- isLcvDesignated,
- ),
- companyLOAs,
- inventoryVehicles,
- ineligiblePowerUnitSubtypes,
- ineligibleTrailerSubtypes,
- );
- }, [
- permitType,
- companyInfo,
- applicationData,
- userDetails,
- isLcvDesignated,
- companyLOAs,
- inventoryVehicles,
- ]);
-
- // Register default values with react-hook-form
- const formMethods = useForm({
- defaultValues: initialFormData,
- reValidateMode: "onBlur",
- });
-
- const { watch, reset, setValue } = formMethods;
- const currentFormData = watch();
-
- // Reset the form with updated default form data whenever fetched data changes
- useEffect(() => {
- reset(initialFormData);
- }, [initialFormData]);
-
- const onSetDuration = useCallback((duration: number) => {
- setValue("permitData.permitDuration", duration);
- }, [setValue]);
-
- const onSetExpiryDate = useCallback((expiry: Dayjs) => {
- setValue("permitData.expiryDate", dayjs(expiry));
- }, [setValue]);
-
- const onSetConditions = useCallback((conditions: PermitCondition[]) => {
- setValue("permitData.commodities", [...conditions]);
- }, [setValue]);
-
- const onToggleSaveVehicle = useCallback((saveVehicle: boolean) => {
- setValue("permitData.vehicleDetails.saveVehicle", saveVehicle);
- }, [setValue]);
-
- const onSetVehicle = useCallback((vehicleDetails: PermitVehicleDetails) => {
- setValue("permitData.vehicleDetails", {
- ...vehicleDetails,
- });
- }, [setValue]);
-
- const onClearVehicle = useCallback((saveVehicle: boolean) => {
- setValue("permitData.vehicleDetails", {
- ...EMPTY_VEHICLE_DETAILS,
- saveVehicle,
- });
- }, [setValue]);
-
- const onUpdateLOAs = useCallback((updatedLOAs: PermitLOA[]) => {
- setValue("permitData.loas", updatedLOAs);
- }, [setValue]);
-
- return {
- initialFormData,
- currentFormData,
- formMethods,
- onSetDuration,
- onSetExpiryDate,
- onSetConditions,
- onToggleSaveVehicle,
- onSetVehicle,
- onClearVehicle,
- onUpdateLOAs,
- };
-};
diff --git a/frontend/src/features/permits/hooks/usePermitVehicleForLOAs.ts b/frontend/src/features/permits/hooks/usePermitVehicleForLOAs.ts
deleted file mode 100644
index a6ac81b82..000000000
--- a/frontend/src/features/permits/hooks/usePermitVehicleForLOAs.ts
+++ /dev/null
@@ -1,137 +0,0 @@
-import { useEffect, useMemo } from "react";
-
-import { PermitVehicleDetails } from "../types/PermitVehicleDetails";
-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: 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 {
- updatedVehicle,
- 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;
- useEffect(() => {
- // If vehicle originally selected exists but the updated vehicle is cleared, clear the vehicle
- if (vehicleIdInForm && !updatedVehicleId) {
- onClearVehicle();
- }
- }, [
- vehicleIdInForm,
- updatedVehicleId,
- ]);
-
- // 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/hooks/usePermitVehicleManagement.ts b/frontend/src/features/permits/hooks/usePermitVehicleManagement.ts
index 2eb6c3262..ca5cc14de 100644
--- a/frontend/src/features/permits/hooks/usePermitVehicleManagement.ts
+++ b/frontend/src/features/permits/hooks/usePermitVehicleManagement.ts
@@ -1,9 +1,9 @@
import { useCallback, useMemo } from "react";
import { getDefaultRequiredVal } from "../../../common/helpers/util";
-import { mapToVehicleObjectById } from "../helpers/mappers";
+import { findFromExistingVehicles } from "../helpers/mappers";
import { Nullable } from "../../../common/types/common";
-import { getDefaultVehicleDetails } from "../helpers/permitVehicles";
+import { getDefaultVehicleDetails } from "../helpers/vehicles/getDefaultVehicleDetails";
import { PermitVehicleDetails } from "../types/PermitVehicleDetails";
import {
usePowerUnitSubTypesQuery,
@@ -41,6 +41,7 @@ const transformByVehicleType = (
countryCode: vehicleFormData.countryCode,
provinceCode: vehicleFormData.provinceCode,
powerUnitTypeCode: vehicleFormData.vehicleSubType,
+ licensedGvw: vehicleFormData.licensedGVW,
};
const defaultTrailer: Trailer = {
@@ -102,7 +103,7 @@ export const usePermitVehicleManagement = (companyId: number) => {
const { data: powerUnitSubtypesData } = usePowerUnitSubTypesQuery();
const { data: trailerSubtypesData } = useTrailerSubTypesQuery();
- const fetchedVehicles = useMemo(() => [
+ const allVehiclesFromInventory = useMemo(() => [
...getDefaultRequiredVal(
[],
powerUnitsData,
@@ -119,8 +120,15 @@ export const usePermitVehicleManagement = (companyId: number) => {
})),
], [powerUnitsData, trailersData]);
- const powerUnitSubTypes = getDefaultRequiredVal([], powerUnitSubtypesData);
- const trailerSubTypes = getDefaultRequiredVal([], trailerSubtypesData);
+ const powerUnitSubtypeNamesMap = useMemo(() => new Map(
+ getDefaultRequiredVal([], powerUnitSubtypesData)
+ .map(({ typeCode, type }) => [typeCode, type]),
+ ), [powerUnitSubtypesData]);
+
+ const trailerSubtypeNamesMap = useMemo(() => new Map(
+ getDefaultRequiredVal([], trailerSubtypesData)
+ .map(({ typeCode, type }) => [typeCode, type]),
+ ), [trailerSubtypesData]);
const handleSaveVehicle = useCallback(async (
vehicleData?: Nullable,
@@ -134,8 +142,8 @@ export const usePermitVehicleManagement = (companyId: number) => {
// Check if the vehicle that is to be saved was created from an existing vehicle
const vehicleId = vehicle.vehicleId;
- const existingVehicle = mapToVehicleObjectById(
- fetchedVehicles,
+ const existingVehicle = findFromExistingVehicles(
+ allVehiclesFromInventory,
vehicle.vehicleType as VehicleType,
vehicleId,
);
@@ -164,12 +172,13 @@ export const usePermitVehicleManagement = (companyId: number) => {
if (!modifyVehicleSuccess(res.status)) return null;
- const { powerUnitId, powerUnitTypeCode, ...restOfPowerUnit } = res.data;
+ const { powerUnitId, powerUnitTypeCode, licensedGvw, ...restOfPowerUnit } = res.data;
return getDefaultVehicleDetails({
...restOfPowerUnit,
vehicleId: powerUnitId,
vehicleSubType: powerUnitTypeCode,
vehicleType: VEHICLE_TYPES.POWER_UNIT,
+ licensedGVW: licensedGvw,
});
}
@@ -197,7 +206,7 @@ export const usePermitVehicleManagement = (companyId: number) => {
if (!modifyVehicleSuccess(res.status)) return null;
const { trailerId, trailerTypeCode, ...restOfTrailer } = res.data;
- return getDefaultRequiredVal({
+ return getDefaultVehicleDetails({
...restOfTrailer,
vehicleId: trailerId,
vehicleSubType: trailerTypeCode,
@@ -206,12 +215,12 @@ export const usePermitVehicleManagement = (companyId: number) => {
}
return undefined;
- }, [fetchedVehicles]);
+ }, [allVehiclesFromInventory]);
return {
handleSaveVehicle,
- powerUnitSubTypes,
- trailerSubTypes,
- vehicleOptions: fetchedVehicles,
+ powerUnitSubtypeNamesMap,
+ trailerSubtypeNamesMap,
+ allVehiclesFromInventory,
};
};
diff --git a/frontend/src/features/permits/hooks/usePermitVehicles.ts b/frontend/src/features/permits/hooks/usePermitVehicles.ts
new file mode 100644
index 000000000..151c26836
--- /dev/null
+++ b/frontend/src/features/permits/hooks/usePermitVehicles.ts
@@ -0,0 +1,185 @@
+import { useEffect, useMemo } from "react";
+import { Policy } from "onroute-policy-engine";
+
+import { PermitType } from "../types/PermitType";
+import { PermitVehicleDetails } from "../types/PermitVehicleDetails";
+import { getUpdatedVehicleDetailsForLOAs } from "../helpers/permitLOA";
+import { PermitLOA } from "../types/PermitLOA";
+import { getEligibleSubtypeOptions } from "../helpers/vehicles/subtypes/getEligibleSubtypeOptions";
+import { Nullable } from "../../../common/types/common";
+import { getEligibleVehicleSubtypes } from "../helpers/vehicles/subtypes/getEligibleVehicleSubtypes";
+import { isPermitVehicleWithinGvwLimit } from "../helpers/vehicles/rules/gvw";
+import {
+ PowerUnit,
+ Trailer,
+ VEHICLE_TYPES,
+} from "../../manageVehicles/types/Vehicle";
+
+export const usePermitVehicles = (
+ data: {
+ policyEngine: Policy;
+ permitType: PermitType;
+ isLcvDesignated: boolean;
+ vehicleFormData: PermitVehicleDetails;
+ allVehiclesFromInventory: (PowerUnit | Trailer)[];
+ selectedLOAs: PermitLOA[];
+ powerUnitSubtypeNamesMap: Map;
+ trailerSubtypeNamesMap: Map;
+ onClearVehicle: () => void;
+ selectedCommodity?: Nullable;
+ },
+) => {
+ const {
+ policyEngine,
+ permitType,
+ isLcvDesignated,
+ vehicleFormData,
+ allVehiclesFromInventory,
+ selectedLOAs,
+ powerUnitSubtypeNamesMap,
+ trailerSubtypeNamesMap,
+ onClearVehicle,
+ selectedCommodity,
+ } = data;
+
+ const eligibleVehicleSubtypes = useMemo(() => {
+ return getEligibleVehicleSubtypes(
+ permitType,
+ isLcvDesignated,
+ selectedCommodity,
+ policyEngine,
+ );
+ }, [
+ policyEngine,
+ permitType,
+ isLcvDesignated,
+ selectedCommodity,
+ ]);
+
+ // Check to see if vehicle details is still valid after LOA has been deselected
+ const {
+ updatedVehicle,
+ filteredVehicleOptions,
+ } = useMemo(() => {
+ return getUpdatedVehicleDetailsForLOAs(
+ selectedLOAs,
+ allVehiclesFromInventory,
+ vehicleFormData,
+ eligibleVehicleSubtypes,
+ [
+ (v) => v.vehicleType !== VEHICLE_TYPES.POWER_UNIT
+ || isPermitVehicleWithinGvwLimit(
+ permitType,
+ VEHICLE_TYPES.POWER_UNIT,
+ (v as PowerUnit).licensedGvw,
+ ),
+ ],
+ );
+ }, [
+ selectedLOAs,
+ allVehiclesFromInventory,
+ vehicleFormData,
+ eligibleVehicleSubtypes,
+ permitType,
+ ]);
+
+ const vehicleIdInForm = vehicleFormData.vehicleId;
+ const updatedVehicleId = updatedVehicle.vehicleId;
+ useEffect(() => {
+ // If vehicle originally selected exists but the updated vehicle is cleared, clear the vehicle
+ if (vehicleIdInForm && !updatedVehicleId) {
+ onClearVehicle();
+ }
+ }, [
+ vehicleIdInForm,
+ updatedVehicleId,
+ ]);
+
+ // Get vehicle subtypes that are allowed by LOAs
+ const vehicleType = vehicleFormData.vehicleType;
+ const {
+ subtypeOptions,
+ isSelectedLOAVehicle,
+ } = useMemo(() => {
+ const allowedLOAPowerUnitIds = new Set([
+ ...selectedLOAs.map(loa => loa.powerUnits)
+ .reduce((prevPowerUnits, currPowerUnits) => [
+ ...prevPowerUnits,
+ ...currPowerUnits,
+ ], []),
+ ]);
+
+ const allowedLOATrailerIds = 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 = allVehiclesFromInventory
+ .filter(vehicle => vehicle.vehicleType === VEHICLE_TYPES.POWER_UNIT) as PowerUnit[];
+
+ const trailersInInventory = allVehiclesFromInventory
+ .filter(vehicle => vehicle.vehicleType === VEHICLE_TYPES.TRAILER) as Trailer[];
+
+ const allowedLOASubtypes = new Set([
+ ...powerUnitsInInventory
+ .filter(powerUnit => allowedLOAPowerUnitIds.has(powerUnit.powerUnitId as string))
+ .map(powerUnit => powerUnit.powerUnitTypeCode),
+ ...trailersInInventory
+ .filter(trailer => allowedLOATrailerIds.has(trailer.trailerId as string))
+ .map(trailer => trailer.trailerTypeCode),
+ ]);
+
+ // Check if selected vehicle is an LOA vehicle
+ const isSelectedLOAVehicle = Boolean(vehicleIdInForm)
+ && (
+ allowedLOAPowerUnitIds.has(vehicleIdInForm as string)
+ || allowedLOATrailerIds.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(
+ [...powerUnitSubtypeNamesMap.entries()].map(([typeCode, type]) => ({
+ type,
+ typeCode,
+ description: "",
+ })),
+ [...trailerSubtypeNamesMap.entries()].map(([typeCode, type]) => ({
+ type,
+ typeCode,
+ description: "",
+ })),
+ eligibleVehicleSubtypes,
+ allowedLOASubtypes,
+ vehicleType,
+ );
+
+ return {
+ subtypeOptions,
+ isSelectedLOAVehicle,
+ };
+ }, [
+ selectedLOAs,
+ allVehiclesFromInventory,
+ vehicleType,
+ vehicleIdInForm,
+ powerUnitSubtypeNamesMap,
+ trailerSubtypeNamesMap,
+ eligibleVehicleSubtypes,
+ ]);
+
+ return {
+ filteredVehicleOptions,
+ subtypeOptions,
+ isSelectedLOAVehicle,
+ };
+};
diff --git a/frontend/src/features/permits/hooks/usePermittedCommodity.ts b/frontend/src/features/permits/hooks/usePermittedCommodity.ts
new file mode 100644
index 000000000..a749bb6a7
--- /dev/null
+++ b/frontend/src/features/permits/hooks/usePermittedCommodity.ts
@@ -0,0 +1,35 @@
+import { useCallback } from "react";
+import { Policy } from "onroute-policy-engine";
+
+import { PermitType } from "../types/PermitType";
+import { Nullable } from "../../../common/types/common";
+import { useCommodityOptions } from "./useCommodityOptions";
+
+export const usePermittedCommodity = (
+ policyEngine: Policy,
+ permitType: PermitType,
+ onSetCommodityType: (commodityType: string) => void,
+ onClearVehicle: () => void,
+ onClearVehicleConfig: (permitType: PermitType) => void,
+ selectedCommodityType?: Nullable,
+) => {
+ const { commodityOptions } = useCommodityOptions(policyEngine, permitType);
+
+ const onChangeCommodityType = useCallback((commodityType: string) => {
+ if (selectedCommodityType !== commodityType) {
+ onSetCommodityType(commodityType);
+ onClearVehicle();
+ onClearVehicleConfig(permitType);
+ }
+ }, [
+ onSetCommodityType,
+ onClearVehicleConfig,
+ selectedCommodityType,
+ permitType,
+ ]);
+
+ return {
+ commodityOptions,
+ onChangeCommodityType,
+ };
+};
diff --git a/frontend/src/features/permits/hooks/useVehicleConfiguration.ts b/frontend/src/features/permits/hooks/useVehicleConfiguration.ts
new file mode 100644
index 000000000..74e2dd903
--- /dev/null
+++ b/frontend/src/features/permits/hooks/useVehicleConfiguration.ts
@@ -0,0 +1,84 @@
+import { useCallback, useEffect, useMemo } from "react";
+import { Policy } from "onroute-policy-engine";
+
+import { PERMIT_TYPES, PermitType } from "../types/PermitType";
+import { VehicleInConfiguration } from "../types/PermitVehicleConfiguration";
+import { DEFAULT_COMMODITY_SELECT_VALUE } from "../constants/constants";
+
+export const useVehicleConfiguration = (
+ policyEngine: Policy,
+ permitType: PermitType,
+ selectedCommodity: string,
+ selectedSubtypes: string[],
+ selectedPowerUnitSubtype: string,
+ onUpdateVehicleConfigTrailers:
+ (updatedTrailerSubtypes: VehicleInConfiguration[]) => void,
+) => {
+ const getNextAllowedVehicleSubtypes = useCallback(
+ (selectedCommodity: string, selectedSubtypes: string[]) => {
+ const nextAllowedSubtypes = policyEngine.getNextPermittableVehicles(
+ permitType,
+ selectedCommodity,
+ selectedSubtypes,
+ );
+
+ return [...nextAllowedSubtypes.entries()]
+ .map(([subtypeCode, subtypeFullName]) => ({
+ value: subtypeCode,
+ label: subtypeFullName,
+ }));
+ },
+ [policyEngine, permitType],
+ );
+
+ useEffect(() => {
+ if (permitType === PERMIT_TYPES.STOS && selectedSubtypes.length > 0) {
+ if ((selectedCommodity === DEFAULT_COMMODITY_SELECT_VALUE) || !policyEngine.isConfigurationValid(
+ permitType,
+ selectedCommodity,
+ [selectedPowerUnitSubtype, ...selectedSubtypes],
+ true,
+ )) {
+ onUpdateVehicleConfigTrailers([]);
+ }
+ }
+ }, [
+ permitType,
+ policyEngine,
+ selectedCommodity,
+ selectedPowerUnitSubtype,
+ selectedSubtypes,
+ onUpdateVehicleConfigTrailers,
+ ]);
+
+ const nextAllowedSubtypes = useMemo(() => {
+ if ((permitType !== PERMIT_TYPES.STOS)
+ || !selectedCommodity
+ || !selectedPowerUnitSubtype
+ || (selectedCommodity === DEFAULT_COMMODITY_SELECT_VALUE)
+ ) {
+ return [];
+ }
+
+ const nextAllowed = getNextAllowedVehicleSubtypes(
+ selectedCommodity,
+ [selectedPowerUnitSubtype, ...selectedSubtypes],
+ ).filter(({ value }) => !selectedSubtypes.includes(value));
+
+ // Sort next allowed subtypes so that if the option "None" is present, it appears at the very beginning
+ const hasNoneOption = nextAllowed.find(subtypeOption => subtypeOption.value === "NONEXXX");
+ return hasNoneOption ? [
+ hasNoneOption,
+ ...nextAllowed.filter(({ value }) => value !== hasNoneOption.value),
+ ] : nextAllowed;
+ }, [
+ selectedCommodity,
+ selectedSubtypes,
+ selectedPowerUnitSubtype,
+ getNextAllowedVehicleSubtypes,
+ ]);
+
+ return {
+ nextAllowedSubtypes,
+ };
+};
diff --git a/frontend/src/features/permits/pages/Amend/components/AmendPermitForm.tsx b/frontend/src/features/permits/pages/Amend/components/AmendPermitForm.tsx
index 326c1e1d0..52677ecb1 100644
--- a/frontend/src/features/permits/pages/Amend/components/AmendPermitForm.tsx
+++ b/frontend/src/features/permits/pages/Amend/components/AmendPermitForm.tsx
@@ -1,6 +1,6 @@
-import { useContext, useMemo } from "react";
+import { useContext, useMemo, useState } from "react";
import { FieldValues, FormProvider } from "react-hook-form";
-import { useNavigate, useParams } from "react-router-dom";
+import { Navigate, useNavigate, useParams } from "react-router-dom";
import "./AmendPermitForm.scss";
import { usePermitVehicleManagement } from "../../../hooks/usePermitVehicleManagement";
@@ -12,16 +12,21 @@ import { Application } from "../../../types/application";
import { useCompanyInfoDetailsQuery } from "../../../../manageProfile/apiManager/hooks";
import { Breadcrumb } from "../../../../../common/components/breadcrumb/Breadcrumb";
import { ApplicationFormContext } from "../../../context/ApplicationFormContext";
-import { Nullable } from "../../../../../common/types/common";
+import { isNull, isUndefined, Nullable } from "../../../../../common/types/common";
import { ERROR_ROUTES } from "../../../../../routes/constants";
import { applyWhenNotNullable, getDefaultRequiredVal } from "../../../../../common/helpers/util";
import { PermitVehicleDetails } from "../../../types/PermitVehicleDetails";
-import { AmendPermitFormData } from "../types/AmendPermitFormData";
import { getDatetimes } from "./helpers/getDatetimes";
import { PAST_START_DATE_STATUSES } from "../../../../../common/components/form/subFormComponents/CustomDatePicker";
import { useFetchLOAs } from "../../../../settings/hooks/LOA";
import { useFetchSpecialAuthorizations } from "../../../../settings/hooks/specialAuthorizations";
import { filterLOAsForPermitType, filterNonExpiredLOAs } from "../../../helpers/permitLOA";
+import { usePolicyEngine } from "../../../../policy/hooks/usePolicyEngine";
+import { Loading } from "../../../../../common/pages/Loading";
+import { serializePermitVehicleDetails } from "../../../helpers/serialize/serializePermitVehicleDetails";
+import { serializeForUpdateApplication } from "../../../helpers/serialize/serializeApplication";
+import { requiredPowerUnit } from "../../../../../common/helpers/validationMessages";
+import { PERMIT_TYPES } from "../../../types/PermitType";
import {
dayjsToUtcStr,
now,
@@ -67,31 +72,27 @@ export const AmendPermitForm = () => {
const {
handleSaveVehicle,
- vehicleOptions,
- powerUnitSubTypes,
- trailerSubTypes,
+ allVehiclesFromInventory,
+ powerUnitSubtypeNamesMap,
+ trailerSubtypeNamesMap,
} = usePermitVehicleManagement(companyId);
+ const policyEngine = usePolicyEngine();
+
const {
initialFormData,
formData,
formMethods,
- onSetDuration,
- onSetExpiryDate,
- onSetConditions,
- onToggleSaveVehicle,
- onSetVehicle,
- onClearVehicle,
- onUpdateLOAs,
- } = useAmendPermitForm(
- currentStepIndex === 0,
+ } = useAmendPermitForm({
+ repopulateFormData: currentStepIndex === 0,
isLcvDesignated,
companyLOAs,
- vehicleOptions,
+ inventoryVehicles: allVehiclesFromInventory,
companyInfo,
permit,
amendmentApplication,
- );
+ policyEngine,
+ });
const { createdDateTime, updatedDateTime } = getDatetimes(
amendmentApplication,
@@ -109,33 +110,56 @@ export const AmendPermitForm = () => {
formData.permitData.startDate,
);
- const amendPermitMutation = useAmendPermit(companyId);
- const modifyAmendmentMutation = useModifyAmendmentApplication();
+ const { mutateAsync: createAmendment } = useAmendPermit(companyId);
+ const { mutateAsync: modifyAmendment } = useModifyAmendmentApplication();
const snackBar = useContext(SnackBarContext);
const { handleSubmit } = formMethods;
- // Helper method to return form field values as an Permit object
- const transformPermitFormData = (data: FieldValues) => {
- return {
- ...data,
- permitData: {
- ...data.permitData,
- vehicleDetails: {
- ...data.permitData.vehicleDetails,
- // Convert year to number here, as React doesn't accept valueAsNumber prop for input component
- year: !isNaN(Number(data.permitData.vehicleDetails.year))
- ? Number(data.permitData.vehicleDetails.year)
- : data.permitData.vehicleDetails.year,
- },
- },
- } as AmendPermitFormData;
+ const [policyViolations, setPolicyViolations] = useState>({});
+
+ const clearViolation = (fieldReference: string) => {
+ if (fieldReference in policyViolations) {
+ const otherViolations = Object.entries(policyViolations)
+ .filter(([fieldRef]) => fieldRef !== fieldReference);
+
+ setPolicyViolations(Object.fromEntries(otherViolations));
+ }
+ };
+
+ const triggerPolicyValidation = async () => {
+ const validationResults = await policyEngine?.validate(
+ serializeForUpdateApplication(formData),
+ );
+
+ const violations = getDefaultRequiredVal(
+ [],
+ validationResults?.violations
+ .filter(({ fieldReference }) => Boolean(fieldReference))
+ .map(violation => ({
+ fieldReference: violation.fieldReference as string,
+ message: violation.message,
+ })),
+ ).concat(formData.permitType === PERMIT_TYPES.STOS && !formData.permitData.vehicleDetails.vin ? [
+ { fieldReference: "permitData.vehicleDetails", message: requiredPowerUnit() },
+ ] : []);
+
+ const updatedViolations = Object.fromEntries(
+ violations.map(({ fieldReference, message }) => [fieldReference, message]),
+ );
+
+ setPolicyViolations(updatedViolations);
+ return updatedViolations;
};
// When "Continue" button is clicked
const onContinue = async (data: FieldValues) => {
- const permitToBeAmended = transformPermitFormData(data);
- const vehicleData = permitToBeAmended.permitData.vehicleDetails;
+ const updatedViolations = await triggerPolicyValidation();
+ if (Object.keys(updatedViolations).length > 0) {
+ return;
+ }
+
+ const vehicleData = serializePermitVehicleDetails(data.permitData.vehicleDetails);
const savedVehicle = await handleSaveVehicle(vehicleData);
// Save application before continuing
@@ -161,34 +185,28 @@ export const AmendPermitForm = () => {
additionalSuccessAction?: () => void,
savedVehicleInventoryDetails?: Nullable,
) => {
- if (
- !savedVehicleInventoryDetails &&
- typeof savedVehicleInventoryDetails !== "undefined"
- ) {
- // save vehicle to inventory failed (result is null), go to unexpected error page
+ if (isNull(savedVehicleInventoryDetails)) {
return onSaveFailure();
}
- const permitToBeAmended = transformPermitFormData(
- !savedVehicleInventoryDetails
- ? formData
- : {
- ...formData,
- permitData: {
- ...formData.permitData,
- vehicleDetails: {
- ...savedVehicleInventoryDetails,
- saveVehicle: true,
- },
+ const permitToBeAmended = !savedVehicleInventoryDetails
+ ? formData
+ : {
+ ...formData,
+ permitData: {
+ ...formData.permitData,
+ vehicleDetails: {
+ ...savedVehicleInventoryDetails,
+ saveVehicle: true,
},
},
- );
+ };
const shouldUpdateApplication =
permitToBeAmended.permitId !== permit?.permitId;
const response = shouldUpdateApplication
- ? await modifyAmendmentMutation.mutateAsync({
+ ? await modifyAmendment({
applicationId: getDefaultRequiredVal(
"",
permitToBeAmended.permitId,
@@ -196,7 +214,7 @@ export const AmendPermitForm = () => {
application: permitToBeAmended,
companyId,
})
- : await amendPermitMutation.mutateAsync(permitToBeAmended);
+ : await createAmendment(permitToBeAmended);
if (response.application) {
onSaveSuccess(response.application);
@@ -230,10 +248,11 @@ export const AmendPermitForm = () => {
const applicationFormContextData = useMemo(() => ({
initialFormData,
formData,
+ policyEngine,
durationOptions,
- vehicleOptions,
- powerUnitSubtypes: powerUnitSubTypes,
- trailerSubtypes: trailerSubTypes,
+ allVehiclesFromInventory,
+ powerUnitSubtypeNamesMap,
+ trailerSubtypeNamesMap,
isLcvDesignated,
feature: FEATURE,
companyInfo,
@@ -243,41 +262,37 @@ export const AmendPermitForm = () => {
pastStartDateStatus: PAST_START_DATE_STATUSES.WARNING,
companyLOAs: applicableLOAs,
revisionHistory,
+ policyViolations,
onLeave: undefined,
onSave: undefined,
onCancel: goHome,
onContinue: handleSubmit(onContinue),
- onSetDuration,
- onSetExpiryDate,
- onSetConditions,
- onToggleSaveVehicle,
- onSetVehicle,
- onClearVehicle,
- onUpdateLOAs,
+ triggerPolicyValidation,
+ clearViolation,
}), [
initialFormData,
formData,
+ policyEngine,
durationOptions,
- vehicleOptions,
- powerUnitSubTypes,
- trailerSubTypes,
+ allVehiclesFromInventory,
+ powerUnitSubtypeNamesMap,
+ trailerSubtypeNamesMap,
isLcvDesignated,
companyInfo,
createdDateTime,
updatedDateTime,
applicableLOAs,
revisionHistory,
+ policyViolations,
goHome,
onContinue,
- onSetDuration,
- onSetExpiryDate,
- onSetConditions,
- onToggleSaveVehicle,
- onSetVehicle,
- onClearVehicle,
- onUpdateLOAs,
+ triggerPolicyValidation,
+ clearViolation,
]);
+ if (isUndefined(policyEngine)) return ;
+ if (isNull(policyEngine)) return ;
+
return (
diff --git a/frontend/src/features/permits/pages/Amend/components/AmendPermitReview.tsx b/frontend/src/features/permits/pages/Amend/components/AmendPermitReview.tsx
index 91a08b3b3..4ec5c4dad 100644
--- a/frontend/src/features/permits/pages/Amend/components/AmendPermitReview.tsx
+++ b/frontend/src/features/permits/pages/Amend/components/AmendPermitReview.tsx
@@ -17,6 +17,8 @@ import { DEFAULT_PERMIT_TYPE } from "../../../types/PermitType";
import { usePowerUnitSubTypesQuery } from "../../../../manageVehicles/hooks/powerUnits";
import { useTrailerSubTypesQuery } from "../../../../manageVehicles/hooks/trailers";
import { PERMIT_REVIEW_CONTEXTS } from "../../../types/PermitReviewContext";
+import { usePolicyEngine } from "../../../../policy/hooks/usePolicyEngine";
+import { useCommodityOptions } from "../../../hooks/useCommodityOptions";
import {
applyWhenNotNullable,
getDefaultRequiredVal,
@@ -52,6 +54,14 @@ export const AmendPermitReview = () => {
const { data: companyInfo } = useCompanyInfoDetailsQuery(companyId);
const doingBusinessAs = companyInfo?.alternateName;
+ const permitType = getDefaultRequiredVal(
+ DEFAULT_PERMIT_TYPE,
+ amendmentApplication?.permitType,
+ permit?.permitType,
+ );
+
+ const policyEngine = usePolicyEngine();
+ const { commodityOptions } = useCommodityOptions(policyEngine, permitType);
const powerUnitSubTypesQuery = usePowerUnitSubTypesQuery();
const trailerSubTypesQuery = useTrailerSubTypesQuery();
@@ -105,11 +115,7 @@ export const AmendPermitReview = () => {
0,
amendmentApplication?.permitData?.permitDuration,
),
- getDefaultRequiredVal(
- DEFAULT_PERMIT_TYPE,
- amendmentApplication?.permitType,
- permit?.permitType,
- ),
+ permitType,
);
return (
@@ -118,7 +124,7 @@ export const AmendPermitReview = () => {
{
permitDuration={amendmentApplication?.permitData?.permitDuration}
permitExpiryDate={amendmentApplication?.permitData?.expiryDate}
permitConditions={amendmentApplication?.permitData?.commodities}
+ permittedCommodity={amendmentApplication?.permitData?.permittedCommodity}
+ commodityOptions={commodityOptions}
createdDateTime={createdDateTime}
updatedDateTime={updatedDateTime}
companyInfo={companyInfo}
@@ -142,6 +150,9 @@ export const AmendPermitReview = () => {
vehicleWasSaved={
amendmentApplication?.permitData?.vehicleDetails?.saveVehicle
}
+ vehicleConfiguration={amendmentApplication?.permitData?.vehicleConfiguration}
+ route={amendmentApplication?.permitData?.permittedRoute}
+ applicationNotes={amendmentApplication?.permitData?.applicationNotes}
showChangedFields={true}
oldFields={{
...oldFields,
@@ -161,6 +172,7 @@ export const AmendPermitReview = () => {
calculatedFee={`${amountToRefund}`}
doingBusinessAs={doingBusinessAs}
loas={amendmentApplication?.permitData?.loas}
+ isStaffUser={true}
>
{amendmentApplication?.comment ? (
diff --git a/frontend/src/features/permits/pages/Amend/hooks/useAmendPermitForm.ts b/frontend/src/features/permits/pages/Amend/hooks/useAmendPermitForm.ts
index 6cb6abc82..ed3346e01 100644
--- a/frontend/src/features/permits/pages/Amend/hooks/useAmendPermitForm.ts
+++ b/frontend/src/features/permits/pages/Amend/hooks/useAmendPermitForm.ts
@@ -1,6 +1,6 @@
-import { useCallback, useEffect, useMemo } from "react";
+import { useEffect, useMemo } from "react";
import { useForm } from "react-hook-form";
-import dayjs, { Dayjs } from "dayjs";
+import { Policy } from "onroute-policy-engine";
import { Nullable } from "../../../../../common/types/common";
import { Permit } from "../../../types/permit";
@@ -8,13 +8,10 @@ import { Application } from "../../../types/application";
import { applyWhenNotNullable } from "../../../../../common/helpers/util";
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/LOADetail";
-import { getIneligibleSubtypes } from "../../../helpers/permitVehicles";
+import { getEligibleVehicleSubtypes } from "../../../helpers/vehicles/subtypes/getEligibleVehicleSubtypes";
import { applyUpToDateLOAsToApplication } from "../../../helpers/permitLOA";
import { PowerUnit, Trailer } from "../../../../manageVehicles/types/Vehicle";
-import { PermitLOA } from "../../../types/PermitLOA";
import {
AmendPermitFormData,
getDefaultFormDataFromApplication,
@@ -22,28 +19,38 @@ import {
} from "../types/AmendPermitFormData";
export const useAmendPermitForm = (
- repopulateFormData: boolean,
- isLcvDesignated: boolean,
- companyLOAs: LOADetail[],
- inventoryVehicles: (PowerUnit | Trailer)[],
- companyInfo: Nullable,
- permit?: Nullable,
- amendmentApplication?: Nullable,
+ data: {
+ repopulateFormData: boolean;
+ isLcvDesignated: boolean;
+ companyLOAs: LOADetail[];
+ inventoryVehicles: (PowerUnit | Trailer)[];
+ companyInfo: Nullable;
+ permit?: Nullable;
+ amendmentApplication?: Nullable;
+ policyEngine?: Nullable;
+ },
) => {
+ const {
+ repopulateFormData,
+ isLcvDesignated,
+ companyLOAs,
+ inventoryVehicles,
+ companyInfo,
+ permit,
+ amendmentApplication,
+ policyEngine,
+ } = data;
+
// Default form data values to populate the amend form with
const defaultFormData = useMemo(() => {
if (amendmentApplication) {
- const ineligibleSubtypes = getIneligibleSubtypes(
+ const eligibleSubtypes = getEligibleVehicleSubtypes(
amendmentApplication.permitType,
isLcvDesignated,
+ amendmentApplication.permitData.permittedCommodity?.commodityType,
+ policyEngine,
);
- const ineligiblePowerUnitSubtypes= ineligibleSubtypes.ineligiblePowerUnitSubtypes
- .map(({ typeCode }) => typeCode);
-
- const ineligibleTrailerSubtypes = ineligibleSubtypes.ineligibleTrailerSubtypes
- .map(({ typeCode }) => typeCode);
-
return applyUpToDateLOAsToApplication(
applyLCVToApplicationData(
getDefaultFormDataFromApplication(
@@ -54,8 +61,7 @@ export const useAmendPermitForm = (
),
companyLOAs,
inventoryVehicles,
- ineligiblePowerUnitSubtypes,
- ineligibleTrailerSubtypes,
+ eligibleSubtypes,
);
}
@@ -72,16 +78,12 @@ export const useAmendPermitForm = (
),
);
- const ineligibleSubtypes = getIneligibleSubtypes(
+ const eligibleSubtypes = getEligibleVehicleSubtypes(
defaultPermitFormData.permitType,
isLcvDesignated,
+ defaultPermitFormData.permitData.permittedCommodity?.commodityType,
+ policyEngine,
);
-
- const ineligiblePowerUnitSubtypes= ineligibleSubtypes.ineligiblePowerUnitSubtypes
- .map(({ typeCode }) => typeCode);
-
- const ineligibleTrailerSubtypes = ineligibleSubtypes.ineligibleTrailerSubtypes
- .map(({ typeCode }) => typeCode);
return applyUpToDateLOAsToApplication(
applyLCVToApplicationData(
@@ -90,8 +92,7 @@ export const useAmendPermitForm = (
),
companyLOAs,
inventoryVehicles,
- ineligiblePowerUnitSubtypes,
- ineligibleTrailerSubtypes,
+ eligibleSubtypes,
);
}, [
amendmentApplication,
@@ -101,64 +102,25 @@ export const useAmendPermitForm = (
isLcvDesignated,
companyLOAs,
inventoryVehicles,
+ policyEngine,
]);
// Register default values with react-hook-form
const formMethods = useForm({
defaultValues: defaultFormData,
- reValidateMode: "onBlur",
+ reValidateMode: "onChange",
});
- const { reset, watch, setValue } = formMethods;
+ const { reset, watch } = formMethods;
const formData = watch();
useEffect(() => {
reset(defaultFormData);
}, [defaultFormData]);
- const onSetDuration = useCallback((duration: number) => {
- setValue("permitData.permitDuration", duration);
- }, [setValue]);
-
- const onSetExpiryDate = useCallback((expiry: Dayjs) => {
- setValue("permitData.expiryDate", dayjs(expiry));
- }, [setValue]);
-
- const onSetConditions = useCallback((conditions: PermitCondition[]) => {
- setValue("permitData.commodities", [...conditions]);
- }, [setValue]);
-
- const onToggleSaveVehicle = useCallback((saveVehicle: boolean) => {
- setValue("permitData.vehicleDetails.saveVehicle", saveVehicle);
- }, [setValue]);
-
- const onSetVehicle = useCallback((vehicleDetails: PermitVehicleDetails) => {
- setValue("permitData.vehicleDetails", {
- ...vehicleDetails,
- });
- }, [setValue]);
-
- const onClearVehicle = useCallback((saveVehicle: boolean) => {
- setValue("permitData.vehicleDetails", {
- ...EMPTY_VEHICLE_DETAILS,
- saveVehicle,
- });
- }, [setValue]);
-
- const onUpdateLOAs = useCallback((updatedLOAs: PermitLOA[]) => {
- setValue("permitData.loas", updatedLOAs);
- }, [setValue]);
-
return {
initialFormData: defaultFormData,
formData,
formMethods,
- onSetDuration,
- onSetExpiryDate,
- onSetConditions,
- onToggleSaveVehicle,
- onSetVehicle,
- onClearVehicle,
- onUpdateLOAs,
};
};
diff --git a/frontend/src/features/permits/pages/Application/ApplicationForm.tsx b/frontend/src/features/permits/pages/Application/ApplicationForm.tsx
index 4e91f7dad..57f994343 100644
--- a/frontend/src/features/permits/pages/Application/ApplicationForm.tsx
+++ b/frontend/src/features/permits/pages/Application/ApplicationForm.tsx
@@ -1,7 +1,8 @@
-import { FieldValues, FormProvider } from "react-hook-form";
-import { useNavigate } from "react-router-dom";
+import { FormProvider } from "react-hook-form";
+import { Navigate, useNavigate } from "react-router-dom";
import { useContext, useMemo, useState } from "react";
import dayjs from "dayjs";
+import { isAxiosError } from "axios";
import "./ApplicationForm.scss";
import { Application, ApplicationFormData } from "../../types/application";
@@ -10,22 +11,31 @@ import { ApplicationBreadcrumb } from "../../components/application-breadcrumb/A
import { useSaveApplicationMutation } from "../../hooks/hooks";
import { SnackBarContext } from "../../../../App";
import { LeaveApplicationDialog } from "../../components/dialog/LeaveApplicationDialog";
-import { areApplicationDataEqual } from "../../helpers/equality";
-import { useInitApplicationFormData } from "../../hooks/useInitApplicationFormData";
+import { areApplicationPermitDataEqual } from "../../helpers/equality";
+import { useInitApplicationFormData } from "../../hooks/form/useInitApplicationFormData";
import OnRouteBCContext from "../../../../common/authentication/OnRouteBCContext";
import { PermitForm } from "./components/form/PermitForm";
import { usePermitVehicleManagement } from "../../hooks/usePermitVehicleManagement";
-import { useCompanyInfoQuery } from "../../../manageProfile/apiManager/hooks";
-import { Nullable } from "../../../../common/types/common";
+import { useCompanyInfoDetailsQuery } from "../../../manageProfile/apiManager/hooks";
+import { isNull, isUndefined, Nullable } from "../../../../common/types/common";
import { PermitType } from "../../types/PermitType";
import { PermitVehicleDetails } from "../../types/PermitVehicleDetails";
import { durationOptionsForPermitType } from "../../helpers/dateSelection";
-import { getCompanyIdFromSession } from "../../../../common/apiManager/httpRequestHandler";
import { PAST_START_DATE_STATUSES } from "../../../../common/components/form/subFormComponents/CustomDatePicker";
import { useFetchLOAs } from "../../../settings/hooks/LOA";
import { useFetchSpecialAuthorizations } from "../../../settings/hooks/specialAuthorizations";
import { ApplicationFormContext } from "../../context/ApplicationFormContext";
import { filterLOAsForPermitType, filterNonExpiredLOAs } from "../../helpers/permitLOA";
+import { usePolicyEngine } from "../../../policy/hooks/usePolicyEngine";
+import { Loading } from "../../../../common/pages/Loading";
+import { serializePermitVehicleDetails } from "../../helpers/serialize/serializePermitVehicleDetails";
+import { serializePermitData } from "../../helpers/serialize/serializePermitData";
+import { deserializeApplicationResponse } from "../../helpers/serialize/deserializeApplication";
+import {
+ serializeForCreateApplication,
+ serializeForUpdateApplication,
+} from "../../helpers/serialize/serializeApplication";
+
import {
applyWhenNotNullable,
getDefaultRequiredVal,
@@ -43,29 +53,23 @@ const FEATURE = "application";
* The first step in creating or saving an Application.
* @returns A form component for users to save an Application
*/
-export const ApplicationForm = ({ permitType }: { permitType: PermitType }) => {
+export const ApplicationForm = ({
+ permitType,
+ companyId,
+}: {
+ permitType: PermitType;
+ companyId: number;
+}) => {
// Context to hold all of the application data related to the application
const applicationContext = useContext(ApplicationContext);
const {
- companyId: companyIdFromContext,
- companyLegalName,
userDetails,
- onRouteBCClientNumber,
idirUserDetails,
} = useContext(OnRouteBCContext);
const isStaffUser = Boolean(idirUserDetails?.userRole);
- const companyInfoQuery = useCompanyInfoQuery();
- const companyInfo = companyInfoQuery.data;
-
- // Company id should be set by context, otherwise default to companyId in session and then the fetched companyId
- const companyId: number = getDefaultRequiredVal(
- 0,
- companyIdFromContext,
- applyWhenNotNullable(id => Number(id), getCompanyIdFromSession()),
- companyInfo?.companyId,
- );
+ const { data: companyInfo } = useCompanyInfoDetailsQuery(companyId);
const { data: activeLOAs } = useFetchLOAs(companyId, false);
const companyLOAs = useMemo(() => getDefaultRequiredVal(
@@ -78,11 +82,13 @@ export const ApplicationForm = ({ permitType }: { permitType: PermitType }) => {
const {
handleSaveVehicle,
- vehicleOptions,
- powerUnitSubTypes,
- trailerSubTypes,
+ allVehiclesFromInventory,
+ powerUnitSubtypeNamesMap,
+ trailerSubtypeNamesMap,
} = usePermitVehicleManagement(companyId);
+ const policyEngine = usePolicyEngine();
+
// Use a custom hook that performs the following whenever page is rendered (or when application context is updated/changed):
// 1. Get all data needed to initialize the application form (from application context, company, user details)
// 2. Generate those default values and register them to the form
@@ -92,22 +98,16 @@ export const ApplicationForm = ({ permitType }: { permitType: PermitType }) => {
initialFormData,
currentFormData,
formMethods,
- onSetDuration,
- onSetExpiryDate,
- onSetConditions,
- onToggleSaveVehicle,
- onSetVehicle,
- onClearVehicle,
- onUpdateLOAs,
- } = useInitApplicationFormData(
+ } = useInitApplicationFormData({
permitType,
isLcvDesignated,
companyLOAs,
- vehicleOptions,
+ inventoryVehicles: allVehiclesFromInventory,
companyInfo,
- applicationContext?.applicationData,
+ applicationData: applicationContext?.applicationData,
userDetails,
- );
+ policyEngine,
+ });
// Applicable LOAs must be:
// 1. Applicable for the current permit type
@@ -130,7 +130,7 @@ export const ApplicationForm = ({ permitType }: { permitType: PermitType }) => {
applicationContext?.applicationData?.updatedDateTime,
);
- const saveApplicationMutation = useSaveApplicationMutation();
+ const { mutateAsync: saveApplication } = useSaveApplicationMutation();
const snackBar = useContext(SnackBarContext);
// Show leave application dialog
@@ -141,42 +141,59 @@ export const ApplicationForm = ({ permitType }: { permitType: PermitType }) => {
const navigate = useNavigate();
- // Helper method to format form values to Application objects before saving them
- const formattedFormData = (data: FieldValues) => {
- return {
- ...data,
- applicationNumber: applicationContext.applicationData?.applicationNumber,
- permitData: {
- ...data.permitData,
- companyName: companyLegalName,
- clientNumber: onRouteBCClientNumber,
- vehicleDetails: {
- ...data.permitData.vehicleDetails,
- // Convert year to number here, as React doesn't accept valueAsNumber prop for input component
- year: !isNaN(Number(data.permitData.vehicleDetails.year))
- ? Number(data.permitData.vehicleDetails.year)
- : data.permitData.vehicleDetails.year,
- },
- },
- } as ApplicationFormData;
+ const [policyViolations, setPolicyViolations] = useState>({});
+
+ const clearViolation = (fieldReference: string) => {
+ if (fieldReference in policyViolations) {
+ const otherViolations = Object.entries(policyViolations)
+ .filter(([fieldRef]) => fieldRef !== fieldReference);
+
+ setPolicyViolations(Object.fromEntries(otherViolations));
+ }
+ };
+
+ const triggerPolicyValidation = async () => {
+ const validationResults = await policyEngine?.validate(
+ currentFormData.permitId
+ ? serializeForUpdateApplication(currentFormData)
+ : serializeForCreateApplication(currentFormData),
+ );
+
+ const violations = getDefaultRequiredVal(
+ [],
+ validationResults?.violations
+ .filter(({ fieldReference }) => Boolean(fieldReference))
+ .map(violation => ({
+ fieldReference: violation.fieldReference as string,
+ message: violation.message,
+ })),
+ );
+
+ const updatedViolations = Object.fromEntries(
+ violations.map(({ fieldReference, message }) => [fieldReference, message]),
+ );
+
+ setPolicyViolations(updatedViolations);
+ return updatedViolations;
};
// Check to see if all application values were already saved
const isApplicationSaved = () => {
- const currentFormattedFormData = formattedFormData(currentFormData);
- const savedData = formattedFormData(initialFormData);
-
// Check if all current form field values match field values already saved in application context
- return areApplicationDataEqual(
- currentFormattedFormData.permitData,
- savedData.permitData,
+ return areApplicationPermitDataEqual(
+ serializePermitData(currentFormData.permitData),
+ serializePermitData(initialFormData.permitData),
);
};
// When "Continue" button is clicked
- const onContinue = async (data: FieldValues) => {
- const applicationToBeSaved = formattedFormData(data);
- const vehicleData = applicationToBeSaved.permitData.vehicleDetails;
+ const onContinue = async (data: ApplicationFormData) => {
+ const updatedViolations = await triggerPolicyValidation();
+ if (Object.keys(updatedViolations).length > 0) {
+ return;
+ }
+
+ const vehicleData = serializePermitVehicleDetails(data.permitData.vehicleDetails);
const savedVehicleDetails = await handleSaveVehicle(vehicleData);
// Save application before continuing
@@ -200,50 +217,50 @@ export const ApplicationForm = ({ permitType }: { permitType: PermitType }) => {
return getDefaultRequiredVal("", savedApplication.permitId);
};
- const onSaveFailure = () => {
- navigate(ERROR_ROUTES.UNEXPECTED);
- };
-
// Whenever application is to be saved (either through "Save" or "Continue")
const onSaveApplication = async (
additionalSuccessAction?: (permitId: string) => void,
savedVehicleInventoryDetails?: Nullable,
) => {
- if (
- !savedVehicleInventoryDetails &&
- typeof savedVehicleInventoryDetails !== "undefined"
- ) {
- // save vehicle to inventory failed (result is null), go to unexpected error page
- return onSaveFailure();
+ if (isNull(savedVehicleInventoryDetails)) {
+ return navigate(ERROR_ROUTES.UNEXPECTED);
}
- const applicationToBeSaved = formattedFormData(
- !savedVehicleInventoryDetails
- ? currentFormData
- : {
- ...currentFormData,
- permitData: {
- ...currentFormData.permitData,
- vehicleDetails: {
- ...savedVehicleInventoryDetails,
- saveVehicle: true,
- },
+ const applicationToBeSaved = !savedVehicleInventoryDetails
+ ? currentFormData
+ : {
+ ...currentFormData,
+ permitData: {
+ ...currentFormData.permitData,
+ vehicleDetails: {
+ ...savedVehicleInventoryDetails,
+ saveVehicle: true,
},
},
- );
-
- const { application: savedApplication, status } =
- await saveApplicationMutation.mutateAsync({
- data: applicationToBeSaved,
- companyId,
- });
-
- if (savedApplication) {
- const savedPermitId = onSaveSuccess(savedApplication, status);
- additionalSuccessAction?.(savedPermitId);
- } else {
- onSaveFailure();
- }
+ };
+
+ await saveApplication({
+ data: applicationToBeSaved,
+ companyId,
+ }, {
+ onSuccess: ({ data, status }) => {
+ const savedApplication = deserializeApplicationResponse(data);
+ const savedPermitId = onSaveSuccess(savedApplication, status);
+ additionalSuccessAction?.(savedPermitId);
+ },
+ onError: (e) => {
+ console.error(e);
+ if (isAxiosError(e)) {
+ navigate(ERROR_ROUTES.UNEXPECTED, {
+ state: {
+ correlationId: e?.response?.headers["x-correlation-id"],
+ },
+ });
+ } else {
+ navigate(ERROR_ROUTES.UNEXPECTED);
+ }
+ },
+ });
};
const onSave = async () => {
@@ -277,10 +294,11 @@ export const ApplicationForm = ({ permitType }: { permitType: PermitType }) => {
const applicationFormContextData = useMemo(() => ({
initialFormData,
formData: currentFormData,
+ policyEngine,
durationOptions,
- vehicleOptions,
- powerUnitSubtypes: powerUnitSubTypes,
- trailerSubtypes: trailerSubTypes,
+ allVehiclesFromInventory,
+ powerUnitSubtypeNamesMap,
+ trailerSubtypeNamesMap,
isLcvDesignated,
feature: FEATURE,
companyInfo,
@@ -290,42 +308,38 @@ export const ApplicationForm = ({ permitType }: { permitType: PermitType }) => {
pastStartDateStatus,
companyLOAs: applicableLOAs,
revisionHistory: [],
+ policyViolations,
+ clearViolation,
+ triggerPolicyValidation,
onLeave: handleLeaveApplication,
onSave,
onCancel: undefined,
onContinue: handleSubmit(onContinue),
- onSetDuration,
- onSetExpiryDate,
- onSetConditions,
- onToggleSaveVehicle,
- onSetVehicle,
- onClearVehicle,
- onUpdateLOAs,
}), [
initialFormData,
currentFormData,
+ policyEngine,
durationOptions,
- vehicleOptions,
- powerUnitSubTypes,
- trailerSubTypes,
+ allVehiclesFromInventory,
+ powerUnitSubtypeNamesMap,
+ trailerSubtypeNamesMap,
isLcvDesignated,
companyInfo,
createdDateTime,
updatedDateTime,
pastStartDateStatus,
applicableLOAs,
+ policyViolations,
+ clearViolation,
+ triggerPolicyValidation,
handleLeaveApplication,
onSave,
onContinue,
- onSetDuration,
- onSetExpiryDate,
- onSetConditions,
- onToggleSaveVehicle,
- onSetVehicle,
- onClearVehicle,
- onUpdateLOAs,
]);
+ if (isUndefined(policyEngine)) return ;
+ if (isNull(policyEngine)) return ;
+
return (
diff --git a/frontend/src/features/permits/pages/Application/ApplicationReview.tsx b/frontend/src/features/permits/pages/Application/ApplicationReview.tsx
index 1c7f78249..715c490ee 100644
--- a/frontend/src/features/permits/pages/Application/ApplicationReview.tsx
+++ b/frontend/src/features/permits/pages/Application/ApplicationReview.tsx
@@ -1,13 +1,14 @@
import { useContext, useEffect, useState } from "react";
import { FormProvider, useForm } from "react-hook-form";
import { useNavigate, useParams } from "react-router-dom";
+import { isAxiosError } from "axios";
import "./ApplicationReview.scss";
import { ApplicationContext } from "../../context/ApplicationContext";
import { Application } from "../../types/application";
import { useSaveApplicationMutation } from "../../hooks/hooks";
import { ApplicationBreadcrumb } from "../../components/application-breadcrumb/ApplicationBreadcrumb";
-import { useCompanyInfoQuery } from "../../../manageProfile/apiManager/hooks";
+import { useCompanyInfoDetailsQuery } from "../../../manageProfile/apiManager/hooks";
import { PermitReview } from "./components/review/PermitReview";
import { getDefaultRequiredVal } from "../../../../common/helpers/util";
import { SnackBarContext } from "../../../../App";
@@ -18,30 +19,41 @@ import { usePowerUnitSubTypesQuery } from "../../../manageVehicles/hooks/powerUn
import { useTrailerSubTypesQuery } from "../../../manageVehicles/hooks/trailers";
import { useFetchSpecialAuthorizations } from "../../../settings/hooks/specialAuthorizations";
import { calculateFeeByDuration } from "../../helpers/feeSummary";
-import { DEFAULT_PERMIT_TYPE } from "../../types/PermitType";
+import { DEFAULT_PERMIT_TYPE, PERMIT_TYPES } from "../../types/PermitType";
import { PERMIT_REVIEW_CONTEXTS } from "../../types/PermitReviewContext";
+import { usePolicyEngine } from "../../../policy/hooks/usePolicyEngine";
+import { useCommodityOptions } from "../../hooks/useCommodityOptions";
+import { useSubmitApplicationForReview } from "../../../queue/hooks/hooks";
+import { deserializeApplicationResponse } from "../../helpers/serialize/deserializeApplication";
+import OnRouteBCContext from "../../../../common/authentication/OnRouteBCContext";
import {
APPLICATIONS_ROUTES,
APPLICATION_STEPS,
ERROR_ROUTES,
} from "../../../../routes/constants";
-export const ApplicationReview = () => {
+export const ApplicationReview = ({
+ companyId,
+}: {
+ companyId: number;
+}) => {
const { applicationData, setApplicationData: setApplicationContextData } =
useContext(ApplicationContext);
- const companyId = getDefaultRequiredVal(0, applicationData?.companyId);
+ const { idirUserDetails } = useContext(OnRouteBCContext);
+ const isStaffUser = Boolean(idirUserDetails?.userRole);
const { data: specialAuth } = useFetchSpecialAuthorizations(companyId);
const isNoFeePermitType = Boolean(specialAuth?.noFeeType);
- const { data: companyInfo } = useCompanyInfoQuery();
+ const { data: companyInfo } = useCompanyInfoDetailsQuery(companyId);
const doingBusinessAs = companyInfo?.alternateName;
+ const permitType = getDefaultRequiredVal(DEFAULT_PERMIT_TYPE, applicationData?.permitType);
const fee = isNoFeePermitType
? "0"
: `${calculateFeeByDuration(
- getDefaultRequiredVal(DEFAULT_PERMIT_TYPE, applicationData?.permitType),
+ permitType,
getDefaultRequiredVal(0, applicationData?.permitData?.permitDuration),
)}`;
@@ -53,6 +65,8 @@ export const ApplicationReview = () => {
const navigate = useNavigate();
+ const policyEngine = usePolicyEngine();
+ const { commodityOptions } = useCommodityOptions(policyEngine, permitType);
const powerUnitSubTypesQuery = usePowerUnitSubTypesQuery();
const trailerSubTypesQuery = useTrailerSubTypesQuery();
const methods = useForm
();
@@ -61,14 +75,67 @@ export const ApplicationReview = () => {
const [allConfirmed, setAllConfirmed] = useState(false);
const [hasAttemptedSubmission, setHasAttemptedSubmission] = useState(false);
- // Send data to the backend API
- const saveApplicationMutation = useSaveApplicationMutation();
+ const { mutateAsync: saveApplication } = useSaveApplicationMutation();
const addToCartMutation = useAddToCart();
+ // Submit for review (if applicable)
+ const {
+ mutateAsync: submitForReview,
+ } = useSubmitApplicationForReview();
+
const back = () => {
navigate(APPLICATIONS_ROUTES.DETAILS(permitId), { replace: true });
};
+ const handleSaveApplication = async (
+ followUpAction: (
+ companyId: number,
+ permitId: string,
+ applicationNumber: string,
+ ) => Promise,
+ ) => {
+ setHasAttemptedSubmission(true);
+
+ if (!allConfirmed) return;
+
+ const companyId = applicationData?.companyId;
+ const permitId = applicationData?.permitId;
+ const applicationNumber = applicationData?.applicationNumber;
+ if (!companyId || !permitId || !applicationNumber) {
+ return navigate(ERROR_ROUTES.UNEXPECTED);
+ }
+
+ await saveApplication({
+ data: {
+ ...applicationData,
+ permitData: {
+ ...applicationData.permitData,
+ doingBusinessAs, // always set most recent DBA from company info
+ },
+ },
+ companyId,
+ }, {
+ onSuccess: ({ data: savedApplication }) => {
+ setApplicationContextData(
+ deserializeApplicationResponse(savedApplication),
+ );
+ followUpAction(companyId, permitId, applicationNumber);
+ },
+ onError: (e) => {
+ console.error(e);
+ if (isAxiosError(e)) {
+ navigate(ERROR_ROUTES.UNEXPECTED, {
+ state: {
+ correlationId: e?.response?.headers["x-correlation-id"],
+ },
+ });
+ } else {
+ navigate(ERROR_ROUTES.UNEXPECTED);
+ }
+ },
+ });
+ };
+
const proceedWithAddToCart = async (
companyId: number,
applicationIds: string[],
@@ -86,37 +153,14 @@ export const ApplicationReview = () => {
}
};
- const handleAddToCart = async () => {
- setHasAttemptedSubmission(true);
-
- if (!allConfirmed) return;
-
- const companyId = applicationData?.companyId;
- const permitId = applicationData?.permitId;
- const applicationNumber = applicationData?.applicationNumber;
- if (!companyId || !permitId || !applicationNumber) {
- return navigate(ERROR_ROUTES.UNEXPECTED);
- }
-
- const { application: savedApplication } =
- await saveApplicationMutation.mutateAsync({
- data: {
- ...applicationData,
- permitData: {
- ...applicationData.permitData,
- doingBusinessAs, // always set most recent DBA from company info
- },
- },
- companyId,
- });
-
- if (savedApplication) {
- setApplicationContextData(savedApplication);
+ const setShowSnackbar = () => true;
+ const handleAddToCart = async () => {
+ await handleSaveApplication(async (companyId, permitId, applicationNumber) => {
await proceedWithAddToCart(companyId, [permitId], () => {
setSnackBar({
showSnackbar: true,
- setShowSnackbar: () => true,
+ setShowSnackbar,
message: `Application ${applicationNumber} added to cart`,
alertType: "success",
});
@@ -124,9 +168,36 @@ export const ApplicationReview = () => {
refetchCartCount();
navigate(APPLICATIONS_ROUTES.BASE);
});
- } else {
- navigate(ERROR_ROUTES.UNEXPECTED);
- }
+ });
+ };
+
+ const continueBtnText = permitType === PERMIT_TYPES.STOS && !isStaffUser
+ ? "Submit for Review" : undefined;
+
+ const handleSubmitForReview = async () => {
+ if (permitType !== PERMIT_TYPES.STOS) return;
+ if (isStaffUser) return;
+
+ await handleSaveApplication(async (companyId, permitId, applicationNumber) => {
+ await submitForReview({
+ companyId,
+ applicationId: permitId,
+ }, {
+ onSuccess: () => {
+ setSnackBar({
+ showSnackbar: true,
+ setShowSnackbar,
+ message: `Application ${applicationNumber} submitted for review`,
+ alertType: "success",
+ });
+
+ navigate(APPLICATIONS_ROUTES.BASE);
+ },
+ onError: () => {
+ navigate(ERROR_ROUTES.UNEXPECTED);
+ },
+ });
+ });
};
useEffect(() => {
@@ -143,7 +214,7 @@ export const ApplicationReview = () => {
{
permitDuration={applicationData?.permitData?.permitDuration}
permitExpiryDate={applicationData?.permitData?.expiryDate}
permitConditions={applicationData?.permitData?.commodities}
+ permittedCommodity={applicationData?.permitData?.permittedCommodity}
+ commodityOptions={commodityOptions}
createdDateTime={applicationData?.createdDateTime}
updatedDateTime={applicationData?.updatedDateTime}
companyInfo={companyInfo}
contactDetails={applicationData?.permitData?.contactDetails}
onEdit={back}
+ continueBtnText={continueBtnText}
+ onContinue={handleSubmitForReview}
onAddToCart={handleAddToCart}
allConfirmed={allConfirmed}
setAllConfirmed={setAllConfirmed}
@@ -166,10 +241,14 @@ export const ApplicationReview = () => {
vehicleWasSaved={
applicationData?.permitData?.vehicleDetails?.saveVehicle
}
+ vehicleConfiguration={applicationData?.permitData?.vehicleConfiguration}
+ route={applicationData?.permitData?.permittedRoute}
+ applicationNotes={applicationData?.permitData?.applicationNotes}
doingBusinessAs={doingBusinessAs}
calculatedFee={fee}
loas={applicationData?.permitData?.loas}
applicationRejectionHistory={applicationData?.rejectionHistory}
+ isStaffUser={isStaffUser}
/>
diff --git a/frontend/src/features/permits/pages/Application/components/common/PowerUnitInfoDisplay.scss b/frontend/src/features/permits/pages/Application/components/common/PowerUnitInfoDisplay.scss
new file mode 100644
index 000000000..67da064fc
--- /dev/null
+++ b/frontend/src/features/permits/pages/Application/components/common/PowerUnitInfoDisplay.scss
@@ -0,0 +1,25 @@
+@use "../../../../../../themes/orbcStyles";
+
+.power-unit-info-display {
+ display: grid;
+ grid-template-columns: repeat(4, [col] 1fr);
+ row-gap: 1rem;
+
+ & &__data {
+ max-width: 15.75rem;
+ margin-left: 1.5rem;
+
+ &--unit, &--year {
+ margin-left: 0;
+ }
+ }
+
+ & &__label {
+ font-weight: bold;
+ color: orbcStyles.$bc-black;
+ }
+
+ & &__value {
+ color: orbcStyles.$bc-black;
+ }
+}
diff --git a/frontend/src/features/permits/pages/Application/components/common/PowerUnitInfoDisplay.tsx b/frontend/src/features/permits/pages/Application/components/common/PowerUnitInfoDisplay.tsx
new file mode 100644
index 000000000..7d71ff553
--- /dev/null
+++ b/frontend/src/features/permits/pages/Application/components/common/PowerUnitInfoDisplay.tsx
@@ -0,0 +1,102 @@
+import "./PowerUnitInfoDisplay.scss";
+import { countrySupportsProvinces } from "../../../../../../common/helpers/countries/countrySupportsProvinces";
+import { getCountryFullName } from "../../../../../../common/helpers/countries/getCountryFullName";
+import { getProvinceFullName } from "../../../../../../common/helpers/countries/getProvinceFullName";
+import { getDefaultRequiredVal } from "../../../../../../common/helpers/util";
+import { PermitVehicleDetails } from "../../../../types/PermitVehicleDetails";
+
+export const PowerUnitInfoDisplay = ({
+ powerUnitInfo,
+ powerUnitSubtypeNamesMap,
+}: {
+ powerUnitInfo: PermitVehicleDetails;
+ powerUnitSubtypeNamesMap: Map;
+}) => {
+ const provinceDisplay = countrySupportsProvinces(powerUnitInfo.countryCode)
+ ? getProvinceFullName(powerUnitInfo.countryCode, powerUnitInfo.provinceCode)
+ : getCountryFullName(powerUnitInfo.countryCode);
+
+ return (
+
+
+
+ Unit #
+
+
+
+ {powerUnitInfo.unitNumber ? powerUnitInfo.unitNumber : "-"}
+
+
+
+
+
+ VIN (last 6 digits)
+
+
+
+ {powerUnitInfo.vin}
+
+
+
+
+
+ Plate
+
+
+
+ {powerUnitInfo.plate}
+
+
+
+
+
+ Make
+
+
+
+ {powerUnitInfo.make}
+
+
+
+
+
+ Year
+
+
+
+ {powerUnitInfo.year}
+
+
+
+
+
+ Province / State
+
+
+
+ {provinceDisplay}
+
+
+
+
+
+ Vehicle Sub-type
+
+
+
+ {powerUnitSubtypeNamesMap.get(powerUnitInfo.vehicleSubType)}
+
+
+
+
+
+ Licensed GVW (kg)
+
+
+
+ {getDefaultRequiredVal(0, powerUnitInfo.licensedGVW).toLocaleString()}
+
+
+
+ );
+};
diff --git a/frontend/src/features/permits/pages/Application/components/common/SelectedVehicleSubtypeList.scss b/frontend/src/features/permits/pages/Application/components/common/SelectedVehicleSubtypeList.scss
new file mode 100644
index 000000000..86aeda055
--- /dev/null
+++ b/frontend/src/features/permits/pages/Application/components/common/SelectedVehicleSubtypeList.scss
@@ -0,0 +1,23 @@
+@use "../../../../../../themes/orbcStyles";
+
+.selected-vehicle-subtype-list {
+ overflow: visible;
+ box-shadow: none;
+ border-radius: 0;
+
+ & &__table {
+ width: 100%;
+ border: 1px solid orbcStyles.$bc-border-grey;
+ table-layout: fixed;
+ }
+
+ & &__header {
+ background-color: orbcStyles.$bc-background-light-grey;
+ font-weight: bold;
+ color: orbcStyles.$bc-black;
+ }
+
+ & &__cell {
+ color: orbcStyles.$bc-black;
+ }
+}
diff --git a/frontend/src/features/permits/pages/Application/components/common/SelectedVehicleSubtypeList.tsx b/frontend/src/features/permits/pages/Application/components/common/SelectedVehicleSubtypeList.tsx
new file mode 100644
index 000000000..9cdb8ec48
--- /dev/null
+++ b/frontend/src/features/permits/pages/Application/components/common/SelectedVehicleSubtypeList.tsx
@@ -0,0 +1,52 @@
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableContainer,
+ TableHead,
+ TableRow,
+} from "@mui/material";
+
+import "./SelectedVehicleSubtypeList.scss";
+
+export const SelectedVehicleSubtypeList = ({
+ selectedSubtypesDisplay,
+}: {
+ selectedSubtypesDisplay: string[];
+}) => {
+ return (
+
+
+
+
+
+ Vehicle Sub-type
+
+
+
+
+
+ {selectedSubtypesDisplay.map((subtype) => (
+
+
+ {subtype}
+
+
+ ))}
+
+
+
+ );
+};
diff --git a/frontend/src/features/permits/pages/Application/components/dashboard/StartApplicationAction.tsx b/frontend/src/features/permits/pages/Application/components/dashboard/StartApplicationAction.tsx
index c5de21fe7..3b6c68749 100644
--- a/frontend/src/features/permits/pages/Application/components/dashboard/StartApplicationAction.tsx
+++ b/frontend/src/features/permits/pages/Application/components/dashboard/StartApplicationAction.tsx
@@ -2,19 +2,23 @@ import { Box, Button, FormLabel, Menu, MenuItem, Tooltip } from "@mui/material";
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { NestedMenuItem } from "mui-nested-menu";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { faChevronDown } from "@fortawesome/free-solid-svg-icons";
+
+import "./StartApplicationAction.scss";
+import { useFeatureFlagsQuery } from "../../../../../../common/hooks/hooks";
import { APPLICATIONS_ROUTES } from "../../../../../../routes/constants";
+import { PERMIT_CATEGORIES } from "../../../../types/PermitCategory";
import {
ALL_PERMIT_TYPE_CHOOSE_FROM_OPTIONS,
PermitTypeChooseFromItem,
} from "../../../../constants/constants";
+
import {
EMPTY_PERMIT_TYPE_SELECT,
PermitType,
getFormattedPermitTypeName,
} from "../../../../types/PermitType";
-import "./StartApplicationAction.scss";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { faChevronDown } from "@fortawesome/free-solid-svg-icons";
export const StartApplicationAction = () => {
const navigate = useNavigate();
@@ -24,6 +28,9 @@ export const StartApplicationAction = () => {
const [isError, setIsError] = useState(false);
+ const { data: featureFlags } = useFeatureFlagsQuery();
+ const enableSTOS = featureFlags?.["STOS"] === "ENABLED";
+
const handleChooseFrom = (
_event: React.MouseEvent,
item: PermitTypeChooseFromItem,
@@ -42,19 +49,21 @@ export const StartApplicationAction = () => {
};
// Update the structure of menuItems to ensure the callback is applied correctly
- const menuItems = ALL_PERMIT_TYPE_CHOOSE_FROM_OPTIONS.map(
- (item: PermitTypeChooseFromItem) => ({
- ...item,
- callback: (event: React.MouseEvent) =>
- handleChooseFrom(event, item),
- // Correctly set the nested item's callback
- items: item?.items?.map((nestedItem) => ({
- ...nestedItem,
+ const menuItems = ALL_PERMIT_TYPE_CHOOSE_FROM_OPTIONS
+ .filter(option => enableSTOS ? true : option.value !== PERMIT_CATEGORIES.SINGLE_TRIP)
+ .map(
+ (item: PermitTypeChooseFromItem) => ({
+ ...item,
callback: (event: React.MouseEvent) =>
- handleChooseFrom(event, nestedItem),
- })),
- }),
- );
+ handleChooseFrom(event, item),
+ // Correctly set the nested item's callback
+ items: item?.items?.map((nestedItem) => ({
+ ...nestedItem,
+ callback: (event: React.MouseEvent) =>
+ handleChooseFrom(event, nestedItem),
+ })),
+ }),
+ );
const [anchorEl, setAnchorEl] = useState();
const open = Boolean(anchorEl);
diff --git a/frontend/src/features/permits/pages/Application/components/form/ApplicationNotesSection.scss b/frontend/src/features/permits/pages/Application/components/form/ApplicationNotesSection.scss
new file mode 100644
index 000000000..5ccccf078
--- /dev/null
+++ b/frontend/src/features/permits/pages/Application/components/form/ApplicationNotesSection.scss
@@ -0,0 +1,31 @@
+@use "../../../../../../themes/orbcStyles";
+
+@include orbcStyles.permit-main-box-style(".application-notes-section");
+@include orbcStyles.permit-left-box-style(".application-notes-section__header");
+@include orbcStyles.permit-right-box-style(".application-notes-section__body");
+
+.application-notes-section {
+ & &__input {
+ max-width: 43.125rem;
+
+ .custom-form-control {
+ margin: 1.5rem 0 0 0;
+ }
+ }
+
+ & &__info {
+ color: orbcStyles.$bc-black;
+
+ .application-notes-info {
+ &__details {
+ font-weight: normal;
+ font-size: 1rem;
+ margin: 0;
+
+ &--info {
+ margin-top: 0.5rem;
+ }
+ }
+ }
+ }
+}
diff --git a/frontend/src/features/permits/pages/Application/components/form/ApplicationNotesSection.tsx b/frontend/src/features/permits/pages/Application/components/form/ApplicationNotesSection.tsx
new file mode 100644
index 000000000..03bc32b59
--- /dev/null
+++ b/frontend/src/features/permits/pages/Application/components/form/ApplicationNotesSection.tsx
@@ -0,0 +1,57 @@
+import { Box } from "@mui/material"
+
+import "./ApplicationNotesSection.scss";
+import { CustomFormComponent } from "../../../../../../common/components/form/CustomFormComponents";
+import { PERMIT_TYPES, PermitType } from "../../../../types/PermitType";
+import { InfoBcGovBanner } from "../../../../../../common/components/banners/InfoBcGovBanner";
+import { BANNER_MESSAGES } from "../../../../../../common/constants/bannerMessages";
+
+export const ApplicationNotesSection = ({
+ feature,
+ permitType,
+}: {
+ feature: string;
+ permitType: PermitType;
+}) => {
+ return permitType === PERMIT_TYPES.STOS ? (
+
+
+
+ Application Notes
+
+
+
+
+
+
+ {BANNER_MESSAGES.APPLICATION_NOTES_EXAMPLE}
+
+
+
+ {BANNER_MESSAGES.APPLICATION_NOTES_INFO}
+
+
+ }
+ />
+
+
+
+
+ ) : null;
+};
+
diff --git a/frontend/src/features/permits/pages/Application/components/form/CommodityDetailsSection/ChangeCommodityTypeDialog.scss b/frontend/src/features/permits/pages/Application/components/form/CommodityDetailsSection/ChangeCommodityTypeDialog.scss
new file mode 100644
index 000000000..fb17b0c04
--- /dev/null
+++ b/frontend/src/features/permits/pages/Application/components/form/CommodityDetailsSection/ChangeCommodityTypeDialog.scss
@@ -0,0 +1,78 @@
+@use "../../../../../../../themes/orbcStyles";
+
+.change-commodity-type-dialog {
+ & &__title-section {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ padding: 1.5rem;
+ color: orbcStyles.$bc-black;
+ background-color: orbcStyles.$bc-background-light-grey;
+ }
+
+ & &__icon {
+ width: 36px;
+ height: 36px;
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ align-items: center;
+ border-radius: 50%;
+ background-color: orbcStyles.$bc-messages-gold-text;
+ margin-right: 1rem;
+
+ .warning-icon {
+ color: orbcStyles.$white;
+ }
+ }
+
+ & &__title {
+ font-size: 1.5rem;
+ font-weight: bold;
+ color: orbcStyles.$bc-messages-gold-text;
+ }
+
+ & &__content {
+ padding: 0;
+ }
+
+ & &__warning-msg {
+ padding: 1.5rem;
+ color: orbcStyles.$bc-black;
+ font-size: 1rem;
+ font-weight: normal;
+ }
+
+ & &__actions {
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-end;
+ padding: 0 1.5rem 1.5rem;
+ }
+
+ & &__action-btn {
+ &--cancel {
+ color: orbcStyles.$black;
+ background-color: orbcStyles.$bc-background-light-grey;
+ padding: 0.875rem 1rem;
+
+ &:hover {
+ color: orbcStyles.$black;
+ background-color: orbcStyles.$bc-background-light-grey;
+ border: 2px solid orbcStyles.$bc-text-box-border-grey;
+ }
+ }
+
+ &--continue {
+ color: orbcStyles.$white;
+ background-color: orbcStyles.$bc-primary-blue;
+ font-weight: bold;
+ margin-left: 1.5rem;
+ padding: 0.875rem 2rem;
+
+ &:hover {
+ background-color: orbcStyles.$button-hover;
+ }
+ }
+ }
+}
diff --git a/frontend/src/features/permits/pages/Application/components/form/CommodityDetailsSection/ChangeCommodityTypeDialog.tsx b/frontend/src/features/permits/pages/Application/components/form/CommodityDetailsSection/ChangeCommodityTypeDialog.tsx
new file mode 100644
index 000000000..96fa0bc70
--- /dev/null
+++ b/frontend/src/features/permits/pages/Application/components/form/CommodityDetailsSection/ChangeCommodityTypeDialog.tsx
@@ -0,0 +1,65 @@
+import { faTriangleExclamation } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import {
+ Button,
+ Dialog,
+ DialogActions,
+ DialogContent,
+ DialogTitle,
+ Typography,
+} from "@mui/material";
+
+import "./ChangeCommodityTypeDialog.scss";
+
+export const ChangeCommodityTypeDialog = ({
+ newCommodityType,
+ onClose,
+ onConfirm,
+}: {
+ newCommodityType: string | undefined;
+ onClose: () => void;
+ onConfirm: (updatedCommodityType: string) => void;
+}) => {
+ return (
+
+
+
+
+
+
+
+ Change Commodity Type
+
+
+
+
+
+ Changing your commodity will require you to re-enter your vehicle information.
+
+
+
+
+
+ Cancel
+
+
+ onConfirm(newCommodityType as string)}
+ >
+ Continue
+
+
+
+ );
+};
diff --git a/frontend/src/features/permits/pages/Application/components/form/CommodityDetailsSection/CommodityDetailsSection.scss b/frontend/src/features/permits/pages/Application/components/form/CommodityDetailsSection/CommodityDetailsSection.scss
new file mode 100644
index 000000000..35404ae36
--- /dev/null
+++ b/frontend/src/features/permits/pages/Application/components/form/CommodityDetailsSection/CommodityDetailsSection.scss
@@ -0,0 +1,20 @@
+@use "../../../../../../../themes/orbcStyles";
+
+@include orbcStyles.permit-main-box-style(".commodity-details-section");
+@include orbcStyles.permit-left-box-style(".commodity-details-section__header");
+@include orbcStyles.permit-right-box-style(".commodity-details-section__body");
+
+.commodity-details-section {
+ & &__input {
+ max-width: 30.625rem;
+
+ .custom-form-control {
+ margin: 1.5rem 0 0 0;
+ }
+
+ &--commodity-type {
+ margin: 0;
+ width: 100%;
+ }
+ }
+}
diff --git a/frontend/src/features/permits/pages/Application/components/form/CommodityDetailsSection/CommodityDetailsSection.tsx b/frontend/src/features/permits/pages/Application/components/form/CommodityDetailsSection/CommodityDetailsSection.tsx
new file mode 100644
index 000000000..2ed069ce3
--- /dev/null
+++ b/frontend/src/features/permits/pages/Application/components/form/CommodityDetailsSection/CommodityDetailsSection.tsx
@@ -0,0 +1,135 @@
+import { Box, MenuItem } from "@mui/material"
+import { useCallback, useMemo, useState } from "react";
+
+import "./CommodityDetailsSection.scss";
+import { CustomFormComponent } from "../../../../../../../common/components/form/CustomFormComponents";
+import { requiredMessage } from "../../../../../../../common/helpers/validationMessages";
+import { PERMIT_TYPES, PermitType } from "../../../../../types/PermitType";
+import { Autocomplete } from "../../../../../../../common/components/form/subFormComponents/Autocomplete";
+import { Controller, useFormContext } from "react-hook-form";
+import { Nullable } from "../../../../../../../common/types/common";
+import { DEFAULT_COMMODITY_SELECT_OPTION, DEFAULT_COMMODITY_SELECT_VALUE } from "../../../../../constants/constants";
+import { getDefaultRequiredVal } from "../../../../../../../common/helpers/util";
+import { ApplicationFormData } from "../../../../../types/application";
+import { ChangeCommodityTypeDialog } from "./ChangeCommodityTypeDialog";
+
+export const CommodityDetailsSection = ({
+ feature,
+ permitType,
+ commodityOptions,
+ selectedCommodityType,
+ onChangeCommodityType,
+}: {
+ feature: string;
+ permitType: PermitType;
+ commodityOptions: {
+ value: string;
+ label: string;
+ }[];
+ selectedCommodityType?: Nullable;
+ onChangeCommodityType: (commodityType: string) => void;
+}) => {
+ const [newCommodityType, setNewCommodityType] = useState();
+
+ const selectedCommodityOption = useMemo(() => {
+ return getDefaultRequiredVal(
+ DEFAULT_COMMODITY_SELECT_OPTION,
+ commodityOptions.find(({ value }) => value === selectedCommodityType),
+ );
+ }, [selectedCommodityType, commodityOptions]);
+
+ const { trigger } = useFormContext();
+
+ const handleCloseDialog = () => {
+ setNewCommodityType(undefined);
+ };
+
+ const handleConfirmChangeCommodityType = (updatedCommodityType: string) => {
+ onChangeCommodityType(updatedCommodityType);
+ setNewCommodityType(undefined);
+ trigger("permitData.permittedCommodity.commodityType");
+ };
+
+ const handleCommodityTypeChange = useCallback((updatedCommodityType: string) => {
+ if (selectedCommodityType === updatedCommodityType) return;
+
+ if (selectedCommodityType !== DEFAULT_COMMODITY_SELECT_VALUE) {
+ setNewCommodityType(updatedCommodityType);
+ return;
+ }
+
+ handleConfirmChangeCommodityType(updatedCommodityType);
+ }, [selectedCommodityType]);
+
+ return permitType === PERMIT_TYPES.STOS ? (
+
+
+ Commodity Details
+
+
+
+ value !== DEFAULT_COMMODITY_SELECT_VALUE || requiredMessage(),
+ },
+ }}
+ render={({ fieldState: {error} }) => (
+ {
+ if (!value) {
+ handleCommodityTypeChange(DEFAULT_COMMODITY_SELECT_VALUE);
+ } else {
+ handleCommodityTypeChange(value.value);
+ }
+ },
+ renderOption: (props, option) => (
+
+ {option.label}
+
+ ),
+ isOptionEqualToValue: (option, value) => option.value === value.value && option.label === value.label,
+ }}
+ helperText={error?.message ? {
+ errors: [error.message],
+ } : undefined}
+ />
+ )}
+ />
+
+
+
+
+ {newCommodityType ? (
+
+ ) : null}
+
+ ) : null;
+};
diff --git a/frontend/src/features/permits/pages/Application/components/form/LoadedDimensionsSection/LoadedDimensionsSection.scss b/frontend/src/features/permits/pages/Application/components/form/LoadedDimensionsSection/LoadedDimensionsSection.scss
new file mode 100644
index 000000000..8dea11ad2
--- /dev/null
+++ b/frontend/src/features/permits/pages/Application/components/form/LoadedDimensionsSection/LoadedDimensionsSection.scss
@@ -0,0 +1,25 @@
+@use "../../../../../../../themes/orbcStyles";
+
+@include orbcStyles.permit-main-box-style(".loaded-dimensions-section");
+@include orbcStyles.permit-left-box-style(".loaded-dimensions-section__header");
+@include orbcStyles.permit-right-box-style(".loaded-dimensions-section__body");
+
+.loaded-dimensions-section {
+ & &__input-row {
+ display: flex;
+ margin-top: 1.5rem;
+
+ &--first {
+ margin-top: 0;
+ }
+ }
+
+ & &__input {
+ max-width: 14.0625rem;
+ margin: 0 0 0 2.5rem;
+
+ &--first {
+ margin: 0;
+ }
+ }
+}
diff --git a/frontend/src/features/permits/pages/Application/components/form/LoadedDimensionsSection/LoadedDimensionsSection.tsx b/frontend/src/features/permits/pages/Application/components/form/LoadedDimensionsSection/LoadedDimensionsSection.tsx
new file mode 100644
index 000000000..bf8104b34
--- /dev/null
+++ b/frontend/src/features/permits/pages/Application/components/form/LoadedDimensionsSection/LoadedDimensionsSection.tsx
@@ -0,0 +1,106 @@
+import { Box } from "@mui/material";
+
+import "./LoadedDimensionsSection.scss";
+import { PERMIT_TYPES, PermitType } from "../../../../../types/PermitType";
+import { Nullable, RequiredOrNull } from "../../../../../../../common/types/common";
+import { PermitVehicleConfiguration } from "../../../../../types/PermitVehicleConfiguration";
+import { LoadedDimensionInput } from "./components/LoadedDimensionInput";
+
+export const LoadedDimensionsSection = ({
+ permitType,
+ feature,
+ vehicleConfiguration,
+ onUpdateVehicleConfiguration,
+}: {
+ permitType: PermitType;
+ feature: string;
+ vehicleConfiguration?: Nullable;
+ onUpdateVehicleConfiguration:
+ (updatedVehicleConfig: RequiredOrNull) => void;
+}) => {
+ return permitType === PERMIT_TYPES.STOS ? (
+
+
+
+ Loaded Dimensions (Metres)
+
+
+
+
+
+ onUpdateVehicleConfiguration({
+ ...vehicleConfiguration,
+ overallWidth: updatedValue,
+ })}
+ />
+
+ onUpdateVehicleConfiguration({
+ ...vehicleConfiguration,
+ overallHeight: updatedValue,
+ })}
+ />
+
+ onUpdateVehicleConfiguration({
+ ...vehicleConfiguration,
+ overallLength: updatedValue,
+ })}
+ />
+
+
+
+ onUpdateVehicleConfiguration({
+ ...vehicleConfiguration,
+ frontProjection: updatedValue,
+ })}
+ />
+
+ onUpdateVehicleConfiguration({
+ ...vehicleConfiguration,
+ rearProjection: updatedValue,
+ })}
+ />
+
+
+
+ ) : null;
+};
diff --git a/frontend/src/features/permits/pages/Application/components/form/LoadedDimensionsSection/components/LoadedDimensionInput.tsx b/frontend/src/features/permits/pages/Application/components/form/LoadedDimensionsSection/components/LoadedDimensionInput.tsx
new file mode 100644
index 000000000..391484781
--- /dev/null
+++ b/frontend/src/features/permits/pages/Application/components/form/LoadedDimensionsSection/components/LoadedDimensionInput.tsx
@@ -0,0 +1,66 @@
+import { Controller } from "react-hook-form";
+
+import { NumberInput } from "../../../../../../../../common/components/form/subFormComponents/NumberInput";
+import { getDefaultRequiredVal } from "../../../../../../../../common/helpers/util";
+import { convertToNumberIfValid } from "../../../../../../../../common/helpers/numeric/convertToNumberIfValid";
+import { Nullable, RequiredOrNull } from "../../../../../../../../common/types/common";
+import {
+ mustBeGreaterThanOrEqualTo,
+ requiredMessage,
+} from "../../../../../../../../common/helpers/validationMessages";
+
+export const LoadedDimensionInput = ({
+ name,
+ label,
+ className,
+ value,
+ onUpdateValue,
+}: {
+ name: string;
+ label: {
+ id: string;
+ component: React.ReactNode;
+ };
+ className: string;
+ value?: Nullable;
+ onUpdateValue: (updateValue: RequiredOrNull) => void;
+}) => {
+ return (
+ (
+ numericVal.toFixed(2),
+ onBlur: (e) => {
+ onUpdateValue(
+ getDefaultRequiredVal(
+ null,
+ convertToNumberIfValid(e.target.value, null),
+ ),
+ );
+ },
+ slotProps: {
+ input: {
+ min: 0,
+ step: 0.01,
+ },
+ },
+ }}
+ helperText={error?.message ? {
+ errors: [error.message],
+ } : undefined}
+ />
+ )}
+ />
+ );
+};
diff --git a/frontend/src/features/permits/pages/Application/components/form/PermitDetails.scss b/frontend/src/features/permits/pages/Application/components/form/PermitDetails.scss
index f287ec271..2b416d786 100644
--- a/frontend/src/features/permits/pages/Application/components/form/PermitDetails.scss
+++ b/frontend/src/features/permits/pages/Application/components/form/PermitDetails.scss
@@ -5,15 +5,33 @@
@include orbcStyles.permit-right-box-style(".permit-details__body");
.permit-details {
- & &__input-section {
+ & &__date-selection {
display: flex;
- gap: 2.5rem;
+ flex-direction: row;
+ justify-content: space-between;
+ max-width: 30.625rem;
+
+ .custom-form-control, .custom-date-picker__form-control {
+ margin: 0;
+ width: 100%;
+ }
+ }
+
+ & &__input {
+ &--start-date {
+ max-width: 16.125rem;
+ }
- .custom-date-picker {
- width: 344px;
+ &--duration {
+ max-width: 12rem;
+ width: 100%;
}
}
+ .permit-expiry-date-banner {
+ margin-top: 1.5rem;
+ }
+
& &__conditions {
margin-top: 1.5rem;
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 b161b6b8c..1a37f9329 100644
--- a/frontend/src/features/permits/pages/Application/components/form/PermitDetails.tsx
+++ b/frontend/src/features/permits/pages/Application/components/form/PermitDetails.tsx
@@ -1,11 +1,10 @@
-import { Box, MenuItem, Typography } from "@mui/material";
+import { Box, MenuItem } from "@mui/material";
import dayjs, { Dayjs } from "dayjs";
import "./PermitDetails.scss";
import { InfoBcGovBanner } from "../../../../../../common/components/banners/InfoBcGovBanner";
import { PermitExpiryDateBanner } from "../../../../../../common/components/banners/PermitExpiryDateBanner";
import { CustomFormComponent } from "../../../../../../common/components/form/CustomFormComponents";
-import { PHONE_WIDTH } from "../../../../../../themes/bcGovStyles";
import { ConditionsTable } from "./ConditionsTable";
import { requiredMessage } from "../../../../../../common/helpers/validationMessages";
import { ONROUTE_WEBPAGE_LINKS } from "../../../../../../routes/constants";
@@ -48,12 +47,13 @@ export const PermitDetails = ({
return (
- Permit Details
+ Permit Details
-
+
(
@@ -88,9 +88,9 @@ export const PermitDetails = ({
-
- Select the commodities below and their respective CVSE forms.
-
+
+ The following CVSE forms will be included in your permit
+
{
const {
@@ -33,6 +37,14 @@ export const PermitForm = () => {
pastStartDateStatus,
companyLOAs,
revisionHistory,
+ commodityOptions,
+ highwaySequence,
+ nextAllowedSubtypes,
+ powerUnitSubtypeNamesMap,
+ trailerSubtypeNamesMap,
+ selectedVehicleConfigSubtypes,
+ commodityType,
+ vehicleConfiguration,
onLeave,
onSave,
onCancel,
@@ -42,6 +54,10 @@ export const PermitForm = () => {
onSetVehicle,
onClearVehicle,
onUpdateLOAs,
+ onUpdateHighwaySequence,
+ onUpdateVehicleConfigTrailers,
+ onChangeCommodityType,
+ onUpdateVehicleConfig,
} = useApplicationFormContext();
return (
@@ -79,16 +95,49 @@ export const PermitForm = () => {
pastStartDateStatus={pastStartDateStatus}
onSetConditions={onSetConditions}
/>
+
+
-
+
+
+
+
+
+
{isAmendAction ? (
diff --git a/frontend/src/features/permits/pages/Application/components/form/PermitLOASection.scss b/frontend/src/features/permits/pages/Application/components/form/PermitLOASection.scss
index f5aa24d5a..8f708dc58 100644
--- a/frontend/src/features/permits/pages/Application/components/form/PermitLOASection.scss
+++ b/frontend/src/features/permits/pages/Application/components/form/PermitLOASection.scss
@@ -5,15 +5,9 @@
@include orbcStyles.permit-right-box-style(".permit-loa-section__body");
.permit-loa-section {
- & &__header {
- h3 {
- padding-top: 1rem;
- }
- }
-
.loa-title {
color: orbcStyles.$bc-black;
- padding-top: 1rem;
+ display: flex;
&__title {
font-weight: bold;
diff --git a/frontend/src/features/permits/pages/Application/components/form/PermitLOASection.tsx b/frontend/src/features/permits/pages/Application/components/form/PermitLOASection.tsx
index b99c1177e..b47bf5373 100644
--- a/frontend/src/features/permits/pages/Application/components/form/PermitLOASection.tsx
+++ b/frontend/src/features/permits/pages/Application/components/form/PermitLOASection.tsx
@@ -1,6 +1,6 @@
import { useEffect, useMemo } from "react";
import { Dayjs } from "dayjs";
-import { Box, Typography } from "@mui/material";
+import { Box } from "@mui/material";
import "./PermitLOASection.scss";
import { InfoBcGovBanner } from "../../../../../../common/components/banners/InfoBcGovBanner";
@@ -71,17 +71,17 @@ export const PermitLOASection = ({
}
};
- return (
+ return loasForTable.length > 0 ? (
-
+
Letter of Authorization (LOA)
-
+
- Select the relevant LOA(s)
+
Select the relevant LOA(s)
(optional)
@@ -101,5 +101,5 @@ export const PermitLOASection = ({
/>
- );
+ ) : null;
};
diff --git a/frontend/src/features/permits/pages/Application/components/form/TripDetailsSection/HighwaySequences.scss b/frontend/src/features/permits/pages/Application/components/form/TripDetailsSection/HighwaySequences.scss
new file mode 100644
index 000000000..7a1c58dcd
--- /dev/null
+++ b/frontend/src/features/permits/pages/Application/components/form/TripDetailsSection/HighwaySequences.scss
@@ -0,0 +1,88 @@
+@use "../../../../../../../themes/orbcStyles";
+
+.highway-sequences {
+ & &__info {
+ color: orbcStyles.$bc-black;
+ margin: 1.5rem 0;
+
+ .highway-sequences-info {
+ &__example {
+ font-weight: normal;
+ font-size: 1rem;
+ margin: 0;
+ }
+
+ &__links {
+ font-weight: normal;
+ font-size: 1rem;
+ margin: 0.5rem 0 0 0;
+
+ .highways-link {
+ margin: 0 0.25rem;
+ }
+ }
+ }
+ }
+
+ & &__inputs {
+ .highway-sequence-rows {
+ &__row {
+ width: 100%;
+ display: flex;
+ justify-content: space-between;
+ margin-top: 1.5rem;
+
+ &--first {
+ margin-top: 0rem;
+ }
+ }
+
+ &__cell {
+ max-width: 6.25rem;
+ margin: 0;
+ }
+
+ &__error {
+ margin-top: 0.5rem;
+ color: orbcStyles.$bc-red;
+ }
+
+ .highway-number-form-control {
+ &__label {
+ font-size: 1rem;
+ font-weight: bold;
+ color: orbcStyles.$bc-black;
+ margin-bottom: 0.5rem;
+ }
+
+ &__input {
+ fieldset {
+ border: 2px solid orbcStyles.$bc-text-box-border-grey;
+ }
+
+ &--focus {
+ fieldset {
+ border: 2px solid orbcStyles.$focus-blue;
+ }
+ }
+
+ &--error {
+ fieldset {
+ border: 2px solid orbcStyles.$bc-red;
+ }
+ }
+ }
+ }
+ }
+
+ .add-highways-row-btn {
+ display: flex;
+ align-items: center;
+ margin-top: 1.5rem;
+
+ &__icon {
+ margin-right: 0.5rem;
+ }
+ }
+ }
+}
diff --git a/frontend/src/features/permits/pages/Application/components/form/TripDetailsSection/HighwaySequences.tsx b/frontend/src/features/permits/pages/Application/components/form/TripDetailsSection/HighwaySequences.tsx
new file mode 100644
index 000000000..62235ca65
--- /dev/null
+++ b/frontend/src/features/permits/pages/Application/components/form/TripDetailsSection/HighwaySequences.tsx
@@ -0,0 +1,196 @@
+import { useMemo } from "react";
+import { Controller, useFormContext } from "react-hook-form";
+import { Button, FormControl, FormLabel } from "@mui/material";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { faPlus } from "@fortawesome/free-solid-svg-icons";
+
+import "./HighwaySequences.scss";
+import { InfoBcGovBanner } from "../../../../../../../common/components/banners/InfoBcGovBanner";
+import { CustomExternalLink } from "../../../../../../../common/components/links/CustomExternalLink";
+import { BANNER_MESSAGES } from "../../../../../../../common/constants/bannerMessages";
+import { ONROUTE_WEBPAGE_LINKS } from "../../../../../../../routes/constants";
+import { requiredHighway } from "../../../../../../../common/helpers/validationMessages";
+import { ApplicationFormData } from "../../../../../types/application";
+import { HighwayNumberInput } from "./components/HighwayNumberInput";
+
+const toHighwayRows = (highwaySequence: string[]) => {
+ return [
+ highwaySequence.slice(0, 8),
+ highwaySequence.slice(8, 16),
+ highwaySequence.slice(16, 24),
+ highwaySequence.slice(24, 32),
+ ].filter((row, index) => index === 0 || row.length > 0)
+ .map(row => {
+ if (row.length === 8) return row;
+
+ // The row has less than 8 highway numbers, pad the rest with empty strings
+ return [...row, ...(new Array(8 - row.length).fill(""))];
+ });
+};
+
+const highwaySequenceRules = {
+ validate: {
+ requiredHighwaySequence: (
+ value: string[],
+ ) => {
+ return (
+ (
+ value.length > 0 &&
+ value.some(highwayNumber => Boolean(highwayNumber.trim()))
+ ) ||
+ requiredHighway()
+ )
+ },
+ },
+};
+
+export const HighwaySequences = ({
+ highwaySequence,
+ onUpdateHighwaySequence,
+}: {
+ highwaySequence: string[];
+ onUpdateHighwaySequence: (highwaySequence: string[]) => void;
+}) => {
+ const highwayRows = useMemo(() => toHighwayRows(highwaySequence), [
+ highwaySequence,
+ ]);
+
+ const maxHighwayRowsReached = highwayRows.length === 4;
+
+ const handleAddHighwayRow = () => {
+ if (maxHighwayRowsReached) return;
+
+ onUpdateHighwaySequence([
+ ...highwayRows.flat(),
+ ...(new Array(8).fill("")),
+ ]);
+ };
+
+ const {
+ control,
+ formState: { errors },
+ trigger,
+ } = useFormContext();
+
+ const handleHighwayInput = (
+ updatedHighwayNumber: string,
+ rowIndex: number,
+ colIndex: number,
+ ) => {
+ const indexToUpdate = rowIndex * 8 + colIndex;
+ onUpdateHighwaySequence(
+ highwayRows.flat().map((highwayNumber, index) =>
+ indexToUpdate === index ? updatedHighwayNumber : highwayNumber)
+ );
+ trigger("permitData.permittedRoute.manualRoute.highwaySequence");
+ };
+
+ return (
+
+
+ Sequence of highways to be travelled
+
+
+
+
+ {BANNER_MESSAGES.HIGHWAY_SEQUENCES.EXAMPLE}
+
+
+
+ Please refer to the
+
+
+ List of Highways in British Columbia
+
+
+ and the
+
+
+ Height Clearance Tool
+
+
+ to build your sequence.
+
+
+ }
+ />
+
+
+
(
+
+ {highwayRows.map((highwayRow, rowIndex) => (
+
+ {highwayRow.map((highwayNumber, colIndex) => (
+
+
+ {rowIndex * 8 + colIndex + 1}
+
+
+
+
+ ))}
+
+ ))}
+
+ {errors.permitData?.permittedRoute?.manualRoute?.highwaySequence ? (
+
+ {errors.permitData.permittedRoute.manualRoute.highwaySequence.message}
+
+ ) : null}
+
+ )}
+ />
+
+ {!maxHighwayRowsReached ? (
+
+
+ Add Highways
+
+ ) : null}
+
+
+ );
+};
diff --git a/frontend/src/features/permits/pages/Application/components/form/TripDetailsSection/SpecificRouteDetails.scss b/frontend/src/features/permits/pages/Application/components/form/TripDetailsSection/SpecificRouteDetails.scss
new file mode 100644
index 000000000..263f6badc
--- /dev/null
+++ b/frontend/src/features/permits/pages/Application/components/form/TripDetailsSection/SpecificRouteDetails.scss
@@ -0,0 +1,16 @@
+@import "../../../../../../../themes/orbcStyles";
+
+.specific-route-details {
+ & &__input {
+ max-width: 43.125rem;
+
+ .custom-form-control {
+ margin: 0;
+ }
+ }
+
+ & &__helper-text {
+ color: $bc-black;
+ margin: 0.25rem 0 0 0;
+ }
+}
diff --git a/frontend/src/features/permits/pages/Application/components/form/TripDetailsSection/SpecificRouteDetails.tsx b/frontend/src/features/permits/pages/Application/components/form/TripDetailsSection/SpecificRouteDetails.tsx
new file mode 100644
index 000000000..41a46a3fb
--- /dev/null
+++ b/frontend/src/features/permits/pages/Application/components/form/TripDetailsSection/SpecificRouteDetails.tsx
@@ -0,0 +1,30 @@
+import "./SpecificRouteDetails.scss";
+import { CustomFormComponent } from "../../../../../../../common/components/form/CustomFormComponents";
+import { requiredMessage } from "../../../../../../../common/helpers/validationMessages";
+
+export const SpecificRouteDetails = ({
+ feature,
+}: {
+ feature: string;
+}) => {
+ return (
+
+
+
+
+ e.g. From the Alberta/BC border to 10 km north on Highway 23 near Revelstoke.
+
+
+ );
+};
\ No newline at end of file
diff --git a/frontend/src/features/permits/pages/Application/components/form/TripDetailsSection/TripDetailsSection.scss b/frontend/src/features/permits/pages/Application/components/form/TripDetailsSection/TripDetailsSection.scss
new file mode 100644
index 000000000..e57460c33
--- /dev/null
+++ b/frontend/src/features/permits/pages/Application/components/form/TripDetailsSection/TripDetailsSection.scss
@@ -0,0 +1,21 @@
+@use "../../../../../../../themes/orbcStyles";
+
+@include orbcStyles.permit-main-box-style(".trip-details-section");
+@include orbcStyles.permit-left-box-style(".trip-details-section__header");
+@include orbcStyles.permit-right-box-style(".trip-details-section__body");
+
+.trip-details-section {
+ .trip-origin-destination {
+ padding-bottom: 1.5rem;
+ }
+
+ .highway-sequences {
+ border-top: 1px solid orbcStyles.$bc-text-box-border-grey;
+ padding: 1.5rem 0;
+ }
+
+ .specific-route-details {
+ border-top: 1px solid orbcStyles.$bc-text-box-border-grey;
+ padding-top: 1.5rem;
+ }
+}
diff --git a/frontend/src/features/permits/pages/Application/components/form/TripDetailsSection/TripDetailsSection.tsx b/frontend/src/features/permits/pages/Application/components/form/TripDetailsSection/TripDetailsSection.tsx
new file mode 100644
index 000000000..90b002b34
--- /dev/null
+++ b/frontend/src/features/permits/pages/Application/components/form/TripDetailsSection/TripDetailsSection.tsx
@@ -0,0 +1,40 @@
+import { Box } from "@mui/material";
+
+import "./TripDetailsSection.scss";
+import { PERMIT_TYPES, PermitType } from "../../../../../types/PermitType";
+import { TripOriginDestination } from "./TripOriginDestination";
+import { SpecificRouteDetails } from "./SpecificRouteDetails";
+import { HighwaySequences } from "./HighwaySequences";
+
+export const TripDetailsSection = ({
+ feature,
+ permitType,
+ highwaySequence,
+ onUpdateHighwaySequence,
+}: {
+ feature: string;
+ permitType: PermitType;
+ highwaySequence: string[];
+ onUpdateHighwaySequence: (updatedHighwaySequence: string[]) => void;
+}) => {
+ return permitType === PERMIT_TYPES.STOS ? (
+
+
+
+ Trip Details
+
+
+
+
+
+
+
+
+
+
+
+ ) : null;
+};
diff --git a/frontend/src/features/permits/pages/Application/components/form/TripDetailsSection/TripOriginDestination.scss b/frontend/src/features/permits/pages/Application/components/form/TripDetailsSection/TripOriginDestination.scss
new file mode 100644
index 000000000..f597cbe7d
--- /dev/null
+++ b/frontend/src/features/permits/pages/Application/components/form/TripDetailsSection/TripOriginDestination.scss
@@ -0,0 +1,15 @@
+.trip-origin-destination {
+ & &__input {
+ max-width: 30.625rem;
+
+ .custom-form-control {
+ margin: 1.5rem 0 0 0;
+ }
+
+ &--origin {
+ .custom-form-control {
+ margin: 0;
+ }
+ }
+ }
+}
diff --git a/frontend/src/features/permits/pages/Application/components/form/TripDetailsSection/TripOriginDestination.tsx b/frontend/src/features/permits/pages/Application/components/form/TripDetailsSection/TripOriginDestination.tsx
new file mode 100644
index 000000000..d04a029aa
--- /dev/null
+++ b/frontend/src/features/permits/pages/Application/components/form/TripDetailsSection/TripOriginDestination.tsx
@@ -0,0 +1,39 @@
+import "./TripOriginDestination.scss";
+import { CustomFormComponent } from "../../../../../../../common/components/form/CustomFormComponents";
+import { invalidAddress } from "../../../../../../../common/helpers/validationMessages";
+
+export const TripOriginDestination = ({
+ feature,
+}: {
+ feature: string;
+}) => {
+ return (
+
+
+
+
+
+ );
+};
diff --git a/frontend/src/features/permits/pages/Application/components/form/TripDetailsSection/components/HighwayNumberInput.tsx b/frontend/src/features/permits/pages/Application/components/form/TripDetailsSection/components/HighwayNumberInput.tsx
new file mode 100644
index 000000000..7ce072b15
--- /dev/null
+++ b/frontend/src/features/permits/pages/Application/components/form/TripDetailsSection/components/HighwayNumberInput.tsx
@@ -0,0 +1,32 @@
+import { OutlinedInput } from "@mui/material";
+
+export const HighwayNumberInput = ({
+ className,
+ highwayNumber,
+ onHighwayInputChange,
+ rowIndex,
+ colIndex,
+}: {
+ className: string;
+ highwayNumber: string;
+ onHighwayInputChange: (
+ updatedHighwayNumber: string,
+ rowIndex: number,
+ colIndex: number,
+ ) => void;
+ rowIndex: number;
+ colIndex: number;
+}) => {
+ return (
+
onHighwayInputChange(e.target.value, rowIndex, colIndex)}
+ />
+ );
+};
diff --git a/frontend/src/features/permits/pages/Application/components/form/VehicleDetails/VehicleDetails.scss b/frontend/src/features/permits/pages/Application/components/form/VehicleDetails/VehicleDetails.scss
deleted file mode 100644
index c12eeb801..000000000
--- a/frontend/src/features/permits/pages/Application/components/form/VehicleDetails/VehicleDetails.scss
+++ /dev/null
@@ -1,59 +0,0 @@
-@use "../../../../../../../themes/orbcStyles";
-
-@include orbcStyles.permit-main-box-style(".vehicle-details");
-@include orbcStyles.permit-left-box-style(".vehicle-details__header");
-@include orbcStyles.permit-right-box-style(".vehicle-details__body");
-
-.vehicle-details {
- border-bottom: none;
-
- & &__info {
- display: flex;
- flex-direction: row-reverse;
- margin-top: 1.5rem;
-
- .bc-gov-alertbanner {
- margin-left: 2.5rem;
- height: fit-content;
- height: -moz-fit-content; /* For Firefox, Firefox Android */
-
- .vehicle-inventory-info {
- font-weight: normal;
- }
- }
- }
-
- & &__input-section {
- display: flex;
- flex-direction: column;
-
- .vehicle-selection {
- display: flex;
- gap: 2.5rem;
-
- .select-unit-or-plate {
- &__select {
- width: 180px;
- }
- }
-
- .select-vehicle-dropdown {
- &__autocomplete {
- width: 268px;
- }
- }
- }
- }
-}
-
-@media (width < 1370px) {
- .vehicle-details {
- & &__info {
- flex-direction: column;
-
- .bc-gov-alertbanner {
- margin-left: 0;
- }
- }
- }
-}
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
deleted file mode 100644
index c97489dbb..000000000
--- a/frontend/src/features/permits/pages/Application/components/form/VehicleDetails/VehicleDetails.tsx
+++ /dev/null
@@ -1,426 +0,0 @@
-import { useEffect, useState } from "react";
-import {
- Box,
- FormControl,
- FormControlLabel,
- FormLabel,
- MenuItem,
- Radio,
- RadioGroup,
- SelectChangeEvent,
- Typography,
-} from "@mui/material";
-
-import "./VehicleDetails.scss";
-import { CountryAndProvince } from "../../../../../../../common/components/form/CountryAndProvince";
-import { CustomFormComponent } from "../../../../../../../common/components/form/CustomFormComponents";
-import { InfoBcGovBanner } from "../../../../../../../common/components/banners/InfoBcGovBanner";
-import { mapToVehicleObjectById } from "../../../../../helpers/mappers";
-import { getDefaultRequiredVal } from "../../../../../../../common/helpers/util";
-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 { selectedVehicleSubtype } from "../../../../../../manageVehicles/helpers/vehicleSubtypes";
-import { disableMouseWheelInputOnNumberField } from "../../../../../../../common/helpers/disableMouseWheelInputOnNumberField";
-import {
- PowerUnit,
- Trailer,
- VehicleSubType,
- VEHICLE_TYPES,
- Vehicle,
- VehicleType,
-} from "../../../../../../manageVehicles/types/Vehicle";
-
-import {
- CHOOSE_FROM_OPTIONS,
- VEHICLE_CHOOSE_FROM,
- VEHICLE_TYPE_OPTIONS,
- VehicleChooseFrom,
-} from "../../../../../constants/constants";
-
-import {
- invalidNumber,
- invalidPlateLength,
- invalidVINLength,
- invalidYearMin,
- requiredMessage,
-} from "../../../../../../../common/helpers/validationMessages";
-
-export const VehicleDetails = ({
- feature,
- vehicleFormData,
- vehicleOptions,
- subtypeOptions,
- isSelectedLOAVehicle,
- onSetSaveVehicle,
- onSetVehicle,
- onClearVehicle,
-}: {
- feature: string;
- vehicleFormData: PermitVehicleDetails;
- vehicleOptions: Vehicle[];
- subtypeOptions: VehicleSubType[];
- isSelectedLOAVehicle: boolean;
- onSetSaveVehicle: (saveVehicle: boolean) => void;
- onSetVehicle: (vehicleDetails: PermitVehicleDetails) => void;
- onClearVehicle: (saveVehicle: boolean) => void;
-}) => {
- const formFieldStyle = {
- fontWeight: "bold",
- width: "490px",
- marginLeft: "8px",
- };
-
- const vehicleType = vehicleFormData.vehicleType;
-
- // Choose vehicle based on either Unit Number or Plate
- const [chooseFrom, setChooseFrom] = useState(
- VEHICLE_CHOOSE_FROM.UNIT_NUMBER,
- );
-
- // Radio button value to decide if the user wants to save the vehicle in inventory
- // Reset to false every reload
- const [saveVehicle, setSaveVehicle] = useState(false);
-
- // Disable vehicle type selection when a vehicle has been selected from dropdown
- // Enable only when user chooses to manually enter new vehicle info by clearing the vehicle details
- const shouldDisableVehicleTypeSelect = () => {
- const existingVehicle = vehicleType
- ? mapToVehicleObjectById(
- vehicleOptions,
- vehicleType as VehicleType,
- vehicleFormData.vehicleId,
- )
- : undefined;
-
- return Boolean(existingVehicle);
- };
-
- const disableVehicleTypeSelect = shouldDisableVehicleTypeSelect();
-
- // Set the "Save to Inventory" radio button to false on render
- useEffect(() => {
- onSetSaveVehicle(saveVehicle);
- }, [saveVehicle]);
-
- // Whenever a new vehicle is selected
- const onSelectVehicle = (selectedVehicle: Vehicle) => {
- const vehicleType =
- selectedVehicle.vehicleType === VEHICLE_TYPES.TRAILER
- ? VEHICLE_TYPES.TRAILER
- : VEHICLE_TYPES.POWER_UNIT;
-
- const vehicleId =
- vehicleType === VEHICLE_TYPES.POWER_UNIT
- ? (selectedVehicle as PowerUnit).powerUnitId
- : (selectedVehicle as Trailer).trailerId;
-
- const vehicle = mapToVehicleObjectById(
- vehicleOptions,
- vehicleType,
- vehicleId,
- );
-
- if (!vehicle) {
- // vehicle selection is invalid
- onClearVehicle(saveVehicle);
- return;
- }
-
- // Prepare form fields with values from selected vehicle
- const vehicleDetails = {
- vehicleId:
- vehicle.vehicleType === VEHICLE_TYPES.POWER_UNIT
- ? (vehicle as PowerUnit).powerUnitId
- : (vehicle as Trailer).trailerId,
- unitNumber: vehicle.unitNumber,
- vin: vehicle.vin,
- plate: vehicle.plate,
- make: vehicle.make,
- year: vehicle.year,
- countryCode: vehicle.countryCode,
- provinceCode: vehicle.provinceCode,
- vehicleType: getDefaultRequiredVal("", vehicle.vehicleType),
- vehicleSubType: selectedVehicleSubtype(vehicle),
- };
-
- onSetVehicle({
- ...vehicleDetails,
- saveVehicle,
- });
- };
-
- const handleChooseFrom = (event: SelectChangeEvent) => {
- setChooseFrom(event.target.value as VehicleChooseFrom);
- };
-
- const handleSaveVehicleRadioBtns = (saveToInventory: string) => {
- setSaveVehicle(saveToInventory === "true");
- };
-
- // Reset the vehicle subtype field whenever a different vehicle type is selected
- const handleChangeVehicleType = (event: SelectChangeEvent) => {
- const updatedVehicleType = event.target.value;
- if (updatedVehicleType !== vehicleType) {
- onSetVehicle({
- ...vehicleFormData,
- vehicleType: updatedVehicleType,
- vehicleSubType: "",
- saveVehicle,
- });
- }
- };
-
- // If the selected vehicle is an LOA vehicle, it should not be edited/saved to inventory
- useEffect(() => {
- if (isSelectedLOAVehicle) {
- setSaveVehicle(false);
- }
- }, [isSelectedLOAVehicle]);
-
- return (
-
-
- Vehicle Details
-
-
-
-
- Choose a saved vehicle from your inventory or enter new vehicle
- information below.
-
-
-
-
- Your vehicle may not be available in a permit application
- because it cannot be used for the type of permit you are
- applying for.
-
- If you are creating a new vehicle, a desired Vehicle Sub-Type
- may not be available because it is not eligible for the permit
- application you are currently in.
-
- }
- />
-
-
-
- (
-
- {data.label}
-
- ))}
- />
-
- onClearVehicle(saveVehicle)}
- handleSelectVehicle={onSelectVehicle}
- />
-
-
-
-
-
-
-
-
- !isNaN(v) || invalidNumber(),
- lessThan1950: (v) =>
- parseInt(v) > 1950 || invalidYearMin(1950),
- },
- },
- inputType: "number",
- label: "Year",
- width: formFieldStyle.width,
- }}
- readOnly={isSelectedLOAVehicle}
- disabled={isSelectedLOAVehicle}
- />
-
-
-
- (
-
- {data.label}
-
- ))}
- />
-
- (
-
- {subtype.type}
-
- ))}
- readOnly={isSelectedLOAVehicle}
- disabled={isSelectedLOAVehicle}
- />
-
-
-
- Would you like to add/update this vehicle to your Vehicle
- Inventory?
-
-
- handleSaveVehicleRadioBtns(e.target.value)}
- >
-
-
- }
- label="Yes"
- />
-
- }
- label="No"
- />
-
-
-
-
-
-
-
- );
-};
diff --git a/frontend/src/features/permits/pages/Application/components/form/VehicleInformationSection/AddPowerUnitDialog.scss b/frontend/src/features/permits/pages/Application/components/form/VehicleInformationSection/AddPowerUnitDialog.scss
new file mode 100644
index 000000000..d308a24c9
--- /dev/null
+++ b/frontend/src/features/permits/pages/Application/components/form/VehicleInformationSection/AddPowerUnitDialog.scss
@@ -0,0 +1,89 @@
+@import "../../../../../../../themes/orbcStyles";
+
+.add-power-unit-dialog {
+ & &__container {
+ max-width: 66.75rem;
+ }
+
+ & &__header {
+ padding: 1.5rem;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ background-color: $bc-background-light-grey;
+ }
+
+ & &__icon {
+ width: 3rem;
+ height: 3rem;
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ align-items: center;
+ border-radius: 50%;
+ background-color: $bc-black;
+
+ .icon {
+ color: $bc-white;
+ height: 1.5rem;
+ }
+ }
+
+ & &__title {
+ font-weight: bold;
+ font-size: 1.5rem;
+ margin-left: 1rem;
+ color: $bc-black;
+ }
+
+ & form &__body {
+ padding: 1.5rem;
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ }
+
+ .vehicle-details {
+ &__inputs {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-between;
+ }
+
+ &__input {
+ width: 100%;
+ }
+ }
+
+ & &__btns {
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-end;
+ padding: 1.5rem;
+ }
+
+ & &__btn {
+ &--cancel {
+ cursor: pointer;
+ background-color: $bc-background-light-grey;
+ color: $bc-black;
+ border: none;
+
+ &:hover {
+ border: 2px solid $bc-text-box-border-grey;
+ }
+ }
+
+ &--add {
+ margin-left: 1.5rem;
+ color: $white;
+ background-color: $bc-primary-blue;
+ font-weight: bold;
+ cursor: pointer;
+
+ &:hover {
+ background-color: $button-hover;
+ }
+ }
+ }
+}
diff --git a/frontend/src/features/permits/pages/Application/components/form/VehicleInformationSection/AddPowerUnitDialog.tsx b/frontend/src/features/permits/pages/Application/components/form/VehicleInformationSection/AddPowerUnitDialog.tsx
new file mode 100644
index 000000000..1bf4ed6f4
--- /dev/null
+++ b/frontend/src/features/permits/pages/Application/components/form/VehicleInformationSection/AddPowerUnitDialog.tsx
@@ -0,0 +1,144 @@
+import { useCallback } from "react";
+import { FormProvider, useForm } from "react-hook-form";
+import { faPlus } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import {
+ Button,
+ Dialog,
+ DialogActions,
+ DialogContent,
+ DialogTitle,
+} from "@mui/material";
+
+import "./AddPowerUnitDialog.scss";
+import { VehicleDetails } from "./VehicleDetails";
+import { EMPTY_VEHICLE_DETAILS, PermitVehicleDetails } from "../../../../../types/PermitVehicleDetails";
+import { Vehicle, VehicleSubType } from "../../../../../../manageVehicles/types/Vehicle";
+import { PermitType } from "../../../../../types/PermitType";
+import { serializePermitVehicleDetails } from "../../../../../helpers/serialize/serializePermitVehicleDetails";
+
+export const AddPowerUnitDialog = ({
+ open,
+ feature,
+ vehicleFormData,
+ vehicleOptions,
+ subtypeOptions,
+ isSelectedLOAVehicle,
+ permitType,
+ onCancel,
+ onAddPowerUnit,
+}: {
+ open: boolean;
+ feature: string;
+ vehicleFormData: PermitVehicleDetails;
+ vehicleOptions: Vehicle[];
+ subtypeOptions: VehicleSubType[];
+ isSelectedLOAVehicle: boolean;
+ permitType: PermitType;
+ onCancel: () => void;
+ onAddPowerUnit: (powerUnit: PermitVehicleDetails) => void;
+}) => {
+ const formMethods = useForm<{
+ permitData: {
+ vehicleDetails: PermitVehicleDetails;
+ };
+ }>({
+ defaultValues: {
+ permitData: {
+ vehicleDetails: vehicleFormData,
+ },
+ },
+ reValidateMode: "onChange",
+ });
+
+ const { handleSubmit, setValue, watch } = formMethods;
+ const selectedVehicle = watch("permitData.vehicleDetails");
+
+ const onToggleSaveVehicle = useCallback((saveVehicle: boolean) => {
+ setValue("permitData.vehicleDetails.saveVehicle", saveVehicle);
+ }, [setValue]);
+
+ const onSetVehicle = useCallback((vehicleDetails: PermitVehicleDetails) => {
+ setValue("permitData.vehicleDetails", {
+ ...vehicleDetails,
+ });
+ }, [setValue]);
+
+ const onClearVehicle = useCallback((saveVehicle: boolean) => {
+ setValue("permitData.vehicleDetails", {
+ ...EMPTY_VEHICLE_DETAILS,
+ saveVehicle,
+ });
+ }, [setValue]);
+
+ const handleAdd = () => {
+ const powerUnit = serializePermitVehicleDetails(selectedVehicle);
+ onAddPowerUnit(powerUnit);
+ };
+
+ return (
+
+
+
+
+
+ );
+};
diff --git a/frontend/src/features/permits/pages/Application/components/form/VehicleInformationSection/AddTrailer.scss b/frontend/src/features/permits/pages/Application/components/form/VehicleInformationSection/AddTrailer.scss
new file mode 100644
index 000000000..beece314b
--- /dev/null
+++ b/frontend/src/features/permits/pages/Application/components/form/VehicleInformationSection/AddTrailer.scss
@@ -0,0 +1,65 @@
+@import "../../../../../../../themes/orbcStyles";
+
+.add-trailer {
+ padding: 1.5rem 0 0 0;
+ border-top: 1px solid $bc-text-box-border-grey;
+
+ & &__trailer-list {
+ margin-top: 1rem;
+ }
+
+ & &__input {
+ width: 100%;
+ max-width: 30.625rem;
+ margin: 1.5rem 0 0 0;
+
+ .form-control {
+ &__label {
+ color: $bc-black;
+ font-weight: bold;
+ margin-bottom: 0.5rem;
+
+ &--focused {
+ color: $bc-black;
+ }
+ }
+
+ &__select {
+ fieldset {
+ border: 2px solid $bc-text-box-border-grey;
+ }
+
+ &:focus-within {
+ fieldset {
+ border: 2px solid $focus-blue;
+ }
+ }
+ }
+
+ &__menu {
+ width: calc(100% - 10px);
+ }
+ }
+ }
+
+ & &__reset-btn {
+ margin-top: 1.5rem;
+ background-color: $bc-background-light-grey;
+ padding: 0.875rem 1rem;
+ box-shadow: none;
+ line-height: normal;
+ min-height: inherit;
+ border: none;
+
+ &:hover {
+ background-color: $bc-background-light-grey;
+ border: 2px solid $bc-text-box-border-grey;
+ box-shadow: none;
+ }
+ }
+
+ & &__error {
+ color: $bc-red;
+ margin: 0.5rem 0 0 0;
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/features/permits/pages/Application/components/form/VehicleInformationSection/AddTrailer.tsx b/frontend/src/features/permits/pages/Application/components/form/VehicleInformationSection/AddTrailer.tsx
new file mode 100644
index 000000000..6b5df7a74
--- /dev/null
+++ b/frontend/src/features/permits/pages/Application/components/form/VehicleInformationSection/AddTrailer.tsx
@@ -0,0 +1,151 @@
+import { useContext, useState } from "react";
+import {
+ Button,
+ FormControl,
+ FormLabel,
+ MenuItem,
+ Select,
+} from "@mui/material";
+
+import "./AddTrailer.scss";
+import { CustomSelectDisplayProps } from "../../../../../../../common/types/formElements";
+import { useMemoizedArray } from "../../../../../../../common/hooks/useMemoizedArray";
+import { VehicleInConfiguration } from "../../../../../types/PermitVehicleConfiguration";
+import { getDefaultRequiredVal } from "../../../../../../../common/helpers/util";
+import { ApplicationFormContext } from "../../../../../context/ApplicationFormContext";
+import { SelectedVehicleSubtypeList } from "../../common/SelectedVehicleSubtypeList";
+
+const DEFAULT_EMPTY_SUBTYPE = "-";
+
+export const AddTrailer = ({
+ selectedTrailerSubtypes,
+ trailerSubtypeOptions,
+ trailerSubtypeNamesMap,
+ onUpdateVehicleConfigTrailers,
+}: {
+ selectedTrailerSubtypes: string[];
+ trailerSubtypeOptions: {
+ value: string;
+ label: string;
+ }[];
+ trailerSubtypeNamesMap: Map;
+ onUpdateVehicleConfigTrailers: (updatedTrailerSubtypes: VehicleInConfiguration[]) => void;
+}) => {
+ const [trailerSelection, setTrailerSelection] = useState(DEFAULT_EMPTY_SUBTYPE);
+
+ const trailersFieldRef = "permitData.vehicleConfiguration.trailers";
+ const { policyViolations, clearViolation } = useContext(ApplicationFormContext);
+
+ const subtypeOptions = useMemoizedArray(
+ [{ value: DEFAULT_EMPTY_SUBTYPE, label: "Select" }].concat(trailerSubtypeOptions),
+ (option) => option.value,
+ (option1, option2) => option1.value === option2.value && option1.label === option2.label,
+ );
+
+ const selectedSubtypesDisplay = useMemoizedArray(
+ selectedTrailerSubtypes.map(subtype => {
+ if (subtype === "NONEXXX") return "None";
+ return getDefaultRequiredVal(
+ subtype,
+ trailerSubtypeNamesMap.get(subtype),
+ );
+ }),
+ (selectedSubtype) => selectedSubtype,
+ (subtype1, subtype2) => subtype1 === subtype2,
+ );
+
+ const handleAddTrailerSubtype = (subtype: string) => {
+ if (subtype !== DEFAULT_EMPTY_SUBTYPE) {
+ onUpdateVehicleConfigTrailers(selectedTrailerSubtypes.map(addedSubtype => ({
+ vehicleSubType: addedSubtype,
+ })).concat([{ vehicleSubType: subtype }]));
+
+ setTrailerSelection(DEFAULT_EMPTY_SUBTYPE);
+ clearViolation(trailersFieldRef);
+ }
+ };
+
+ const handleResetTrailerConfig = () => {
+ onUpdateVehicleConfigTrailers([]);
+ clearViolation(trailersFieldRef);
+ };
+
+ return (selectedSubtypesDisplay.length > 0 || trailerSubtypeOptions.length > 0) ? (
+
+
Add Trailer(s)
+
+ {selectedSubtypesDisplay.length > 0 ? (
+
+
+
+
+ Reset Trailer Configuration
+
+
+ ) : null}
+
+ {trailerSubtypeOptions.length > 0 ? (
+
+
+ Add Trailer
+
+
+ handleAddTrailerSubtype(e.target.value)}
+ value={trailerSelection}
+ MenuProps={{
+ className: "form-control__menu",
+ disablePortal: true,
+ }}
+ SelectDisplayProps={
+ {
+ "data-testid": `trailer-subtype-input-container`,
+ className: "form-control__input-container",
+ } as CustomSelectDisplayProps
+ }
+ >
+ {subtypeOptions.map(({ value, label }) => (
+
+ {label}
+
+ ))}
+
+
+ ) : null}
+
+ {trailersFieldRef in policyViolations ? (
+
+ {policyViolations[trailersFieldRef]}
+
+ ) : null}
+
+ ) : null;
+};
diff --git a/frontend/src/features/permits/pages/Application/components/form/VehicleInformationSection/PowerUnitInfo.scss b/frontend/src/features/permits/pages/Application/components/form/VehicleInformationSection/PowerUnitInfo.scss
new file mode 100644
index 000000000..ab67da3b8
--- /dev/null
+++ b/frontend/src/features/permits/pages/Application/components/form/VehicleInformationSection/PowerUnitInfo.scss
@@ -0,0 +1,29 @@
+@use "../../../../../../../themes/orbcStyles";
+
+.power-unit-info {
+ padding: 1.5rem 0;
+ border-top: 1px solid orbcStyles.$bc-text-box-border-grey;
+
+ & &__title {
+ margin-bottom: 0;
+ }
+
+ .power-unit-info-display {
+ padding: 1.5rem 0;
+ }
+
+ & &__remove-btn {
+ background-color: orbcStyles.$bc-background-light-grey;
+ padding: 0.875rem 1rem;
+ box-shadow: none;
+ line-height: normal;
+ min-height: inherit;
+ border: none;
+
+ &:hover {
+ background-color: orbcStyles.$bc-background-light-grey;
+ border: 2px solid orbcStyles.$bc-text-box-border-grey;
+ box-shadow: none;
+ }
+ }
+}
diff --git a/frontend/src/features/permits/pages/Application/components/form/VehicleInformationSection/PowerUnitInfo.tsx b/frontend/src/features/permits/pages/Application/components/form/VehicleInformationSection/PowerUnitInfo.tsx
new file mode 100644
index 000000000..7dac3c806
--- /dev/null
+++ b/frontend/src/features/permits/pages/Application/components/form/VehicleInformationSection/PowerUnitInfo.tsx
@@ -0,0 +1,38 @@
+import { Button } from "@mui/material";
+
+import "./PowerUnitInfo.scss";
+import { PermitVehicleDetails } from "../../../../../types/PermitVehicleDetails";
+import { PowerUnitInfoDisplay } from "../../common/PowerUnitInfoDisplay";
+
+export const PowerUnitInfo = ({
+ powerUnitInfo,
+ powerUnitSubtypeNamesMap,
+ onRemovePowerUnit,
+}: {
+ powerUnitInfo: PermitVehicleDetails;
+ powerUnitSubtypeNamesMap: Map;
+ onRemovePowerUnit: () => void,
+}) => {
+ return (
+
+
Power Unit
+
+
+
+
+ Remove
+
+
+ );
+};
diff --git a/frontend/src/features/permits/pages/Application/components/form/VehicleInformationSection/VehicleDetails.scss b/frontend/src/features/permits/pages/Application/components/form/VehicleInformationSection/VehicleDetails.scss
new file mode 100644
index 000000000..b01851f40
--- /dev/null
+++ b/frontend/src/features/permits/pages/Application/components/form/VehicleInformationSection/VehicleDetails.scss
@@ -0,0 +1,46 @@
+@use "../../../../../../../themes/orbcStyles";
+
+.vehicle-details {
+ display: flex;
+ flex-direction: column;
+
+ & &__vehicle-selection {
+ display: flex;
+ justify-content: space-between;
+ max-width: 30.625rem;
+
+ .select-unit-or-plate {
+ margin: 0;
+ max-width: 11.375rem;
+ width: 100%;
+ }
+
+ .select-vehicle-dropdown {
+ margin: 0;
+ max-width: 16.75rem;
+ width: 100%;
+ }
+ }
+
+ & &__input {
+ max-width: 30.625rem;
+
+ .custom-form-control {
+ margin: 1.5rem 0 0 0;
+ }
+ }
+
+ .save-to-inventory {
+ margin-top: 1.5rem;
+
+ &__label {
+ font-size: 1rem;
+ font-weight: bold;
+ margin: 0;
+ }
+
+ &__radio-btns {
+ display: flex;
+ }
+ }
+}
diff --git a/frontend/src/features/permits/pages/Application/components/form/VehicleInformationSection/VehicleDetails.tsx b/frontend/src/features/permits/pages/Application/components/form/VehicleInformationSection/VehicleDetails.tsx
new file mode 100644
index 000000000..d3f4a68c9
--- /dev/null
+++ b/frontend/src/features/permits/pages/Application/components/form/VehicleInformationSection/VehicleDetails.tsx
@@ -0,0 +1,439 @@
+import { useEffect, useState } from "react";
+import {
+ Box,
+ FormControl,
+ FormControlLabel,
+ FormLabel,
+ MenuItem,
+ Radio,
+ RadioGroup,
+ SelectChangeEvent,
+} from "@mui/material";
+
+import "./VehicleDetails.scss";
+import { CountryAndProvince } from "../../../../../../../common/components/form/CountryAndProvince";
+import { CustomFormComponent } from "../../../../../../../common/components/form/CustomFormComponents";
+import { findFromExistingVehicles } from "../../../../../helpers/mappers";
+import { getDefaultRequiredVal } from "../../../../../../../common/helpers/util";
+import { CustomInputHTMLAttributes } from "../../../../../../../common/types/formElements";
+import { SelectUnitOrPlate } from "./components/SelectUnitOrPlate";
+import { SelectVehicleDropdown } from "./components/SelectVehicleDropdown";
+import { PermitVehicleDetails } from "../../../../../types/PermitVehicleDetails";
+import { selectedVehicleSubtype } from "../../../../../../manageVehicles/helpers/vehicleSubtypes";
+import { PERMIT_TYPES, PermitType } from "../../../../../types/PermitType";
+import { isUndefined } from "../../../../../../../common/types/common";
+import { gvwLimit, isPermitVehicleWithinGvwLimit } from "../../../../../helpers/vehicles/rules/gvw";
+import {
+ disableMouseWheelInputOnNumberField,
+} from "../../../../../../../common/helpers/disableMouseWheelInputOnNumberField";
+
+import {
+ PowerUnit,
+ Trailer,
+ VehicleSubType,
+ VEHICLE_TYPES,
+ Vehicle,
+ VehicleType,
+} from "../../../../../../manageVehicles/types/Vehicle";
+
+import {
+ CHOOSE_FROM_OPTIONS,
+ VEHICLE_CHOOSE_FROM,
+ VEHICLE_TYPE_OPTIONS,
+ VehicleChooseFrom,
+} from "../../../../../constants/constants";
+
+import {
+ invalidNumber,
+ invalidPlateLength,
+ invalidVINLength,
+ invalidYearMin,
+ licensedGVWExceeded,
+ requiredMessage,
+} from "../../../../../../../common/helpers/validationMessages";
+
+export const VehicleDetails = ({
+ feature,
+ vehicleFormData,
+ vehicleOptions,
+ subtypeOptions,
+ isSelectedLOAVehicle,
+ permitType,
+ onSetSaveVehicle,
+ onSetVehicle,
+ onClearVehicle,
+}: {
+ feature: string;
+ vehicleFormData: PermitVehicleDetails;
+ vehicleOptions: Vehicle[];
+ subtypeOptions: VehicleSubType[];
+ isSelectedLOAVehicle: boolean;
+ permitType: PermitType;
+ onSetSaveVehicle: (saveVehicle: boolean) => void;
+ onSetVehicle: (vehicleDetails: PermitVehicleDetails) => void;
+ onClearVehicle: (saveVehicle: boolean) => void;
+}) => {
+ const powerUnitsOnly = permitType === PERMIT_TYPES.STOS;
+ const vehicleType = vehicleFormData.vehicleType;
+
+ // Choose vehicle based on either Unit Number or Plate
+ const [chooseFrom, setChooseFrom] = useState(
+ VEHICLE_CHOOSE_FROM.UNIT_NUMBER,
+ );
+
+ // Radio button value to decide if the user wants to save the vehicle in inventory
+ // Reset to false every reload
+ const [saveVehicle, setSaveVehicle] = useState(false);
+
+ // Disable vehicle type selection when a vehicle has been selected from dropdown
+ // Enable only when user chooses to manually enter new vehicle info by clearing the vehicle details
+ const shouldDisableVehicleTypeSelect = () => {
+ const existingVehicle = vehicleType
+ ? findFromExistingVehicles(
+ vehicleOptions,
+ vehicleType as VehicleType,
+ vehicleFormData.vehicleId,
+ )
+ : undefined;
+
+ return Boolean(existingVehicle);
+ };
+
+ const disableVehicleTypeSelect = shouldDisableVehicleTypeSelect();
+
+ // Set the "Save to Inventory" radio button to false on render
+ useEffect(() => {
+ onSetSaveVehicle(saveVehicle);
+ }, [saveVehicle]);
+
+ // Whenever a new vehicle is selected
+ const onSelectVehicle = (selectedVehicle: Vehicle) => {
+ const vehicleType =
+ selectedVehicle.vehicleType === VEHICLE_TYPES.TRAILER
+ ? VEHICLE_TYPES.TRAILER
+ : VEHICLE_TYPES.POWER_UNIT;
+
+ const vehicleId =
+ vehicleType === VEHICLE_TYPES.POWER_UNIT
+ ? (selectedVehicle as PowerUnit).powerUnitId
+ : (selectedVehicle as Trailer).trailerId;
+
+ const vehicle = findFromExistingVehicles(
+ vehicleOptions,
+ vehicleType,
+ vehicleId,
+ );
+
+ if (!vehicle) {
+ // vehicle selection is invalid
+ onClearVehicle(saveVehicle);
+ return;
+ }
+
+ // Prepare form fields with values from selected vehicle
+ const vehicleDetails = {
+ vehicleId:
+ vehicle.vehicleType === VEHICLE_TYPES.POWER_UNIT
+ ? (vehicle as PowerUnit).powerUnitId
+ : (vehicle as Trailer).trailerId,
+ unitNumber: vehicle.unitNumber,
+ vin: vehicle.vin,
+ plate: vehicle.plate,
+ make: vehicle.make,
+ year: vehicle.year,
+ countryCode: vehicle.countryCode,
+ provinceCode: vehicle.provinceCode,
+ vehicleType: getDefaultRequiredVal("", vehicle.vehicleType),
+ vehicleSubType: selectedVehicleSubtype(vehicle),
+ licensedGVW:
+ vehicle.vehicleType === VEHICLE_TYPES.POWER_UNIT
+ ? getDefaultRequiredVal(null, (vehicle as PowerUnit).licensedGvw)
+ : null,
+ };
+
+ onSetVehicle({
+ ...vehicleDetails,
+ saveVehicle,
+ });
+ };
+
+ const handleChooseFrom = (event: SelectChangeEvent) => {
+ setChooseFrom(event.target.value as VehicleChooseFrom);
+ };
+
+ const handleSaveVehicleRadioBtns = (saveToInventory: string) => {
+ setSaveVehicle(saveToInventory === "true");
+ };
+
+ // Reset the vehicle subtype field whenever a different vehicle type is selected
+ const handleChangeVehicleType = (event: SelectChangeEvent) => {
+ const updatedVehicleType = event.target.value;
+ if (updatedVehicleType !== vehicleType) {
+ onSetVehicle({
+ ...vehicleFormData,
+ vehicleType: updatedVehicleType,
+ vehicleSubType: "",
+ saveVehicle,
+ });
+ }
+ };
+
+ // If the selected vehicle is an LOA vehicle, it should not be edited/saved to inventory
+ useEffect(() => {
+ if (isSelectedLOAVehicle) {
+ setSaveVehicle(false);
+ }
+ }, [isSelectedLOAVehicle]);
+
+ return (
+
+
+ (
+
+ {data.label}
+
+ ))}
+ />
+
+ onClearVehicle(saveVehicle)}
+ handleSelectVehicle={onSelectVehicle}
+ />
+
+
+
+
+
+
+
+
+
+ !isNaN(v) || invalidNumber(),
+ lessThan1950: (v) =>
+ parseInt(v) > 1950 || invalidYearMin(1950),
+ },
+ },
+ inputType: "number",
+ label: "Year",
+ }}
+ readOnly={isSelectedLOAVehicle}
+ disabled={isSelectedLOAVehicle}
+ />
+
+
+
+ {!powerUnitsOnly ? (
+ (
+
+ {data.label}
+
+ ))}
+ />
+ ) : null}
+
+ (
+
+ {subtype.type}
+
+ ))}
+ readOnly={isSelectedLOAVehicle}
+ disabled={isSelectedLOAVehicle}
+ />
+
+ {powerUnitsOnly ? (
+ !isNaN(v) || invalidNumber(),
+ exceededGvw: (v) => {
+ const maxAllowedGvw = gvwLimit(permitType);
+ return isSelectedLOAVehicle
+ || isUndefined(maxAllowedGvw)
+ || isPermitVehicleWithinGvwLimit(
+ permitType,
+ vehicleType as VehicleType,
+ parseInt(v),
+ )
+ || licensedGVWExceeded(maxAllowedGvw, true);
+ },
+ },
+ },
+ inputType: "number",
+ label: "Licensed GVW (kg)",
+ }}
+ readOnly={isSelectedLOAVehicle}
+ disabled={isSelectedLOAVehicle}
+ />
+ ) : null}
+
+
+
+
+ Would you like to add/update this vehicle to your Vehicle
+ Inventory?
+
+
+ handleSaveVehicleRadioBtns(e.target.value)}
+ >
+
+
+ }
+ label="Yes"
+ />
+
+
+ }
+ label="No"
+ />
+
+
+
+
+ );
+};
diff --git a/frontend/src/features/permits/pages/Application/components/form/VehicleInformationSection/VehicleInformationSection.scss b/frontend/src/features/permits/pages/Application/components/form/VehicleInformationSection/VehicleInformationSection.scss
new file mode 100644
index 000000000..187dff169
--- /dev/null
+++ b/frontend/src/features/permits/pages/Application/components/form/VehicleInformationSection/VehicleInformationSection.scss
@@ -0,0 +1,79 @@
+@use "../../../../../../../themes/orbcStyles";
+
+@include orbcStyles.permit-main-box-style(".vehicle-information-section");
+@include orbcStyles.permit-left-box-style(".vehicle-information-section__header");
+@include orbcStyles.permit-right-box-style(".vehicle-information-section__body");
+
+.vehicle-information-section {
+ border-bottom: none;
+
+ & &__info {
+ display: flex;
+ flex-direction: row-reverse;
+ margin-top: 1.5rem;
+
+ &--single-trip {
+ flex-direction: column;
+ align-items: flex-start;
+ }
+
+ .add-power-unit {
+ margin-top: 1.5rem;
+
+ .add-power-unit-btn {
+ display: flex;
+ align-items: center;
+ color: orbcStyles.$bc-black;
+
+ &--disabled {
+ color: orbcStyles.$disabled-colour;
+ border-color: orbcStyles.$disabled-colour;
+ background-color: orbcStyles.$white;
+ }
+
+ &__icon {
+ margin-right: 0.5rem;
+ }
+ }
+
+ &__error {
+ color: orbcStyles.$bc-red;
+ margin: 0.5rem 0 0 0;
+ }
+ }
+
+ .vehicle-details {
+ width: 100%;
+ }
+ }
+
+ & &__info-banner {
+ margin-left: 2.5rem;
+ height: fit-content;
+ height: -moz-fit-content; /* For Firefox, Firefox Android */
+
+ &--single-trip {
+ margin: 0;
+ }
+
+ .vehicle-inventory-info {
+ font-weight: normal;
+ }
+ }
+
+ .power-unit-info {
+ margin-top: 1.5rem;
+ }
+}
+
+@media (width < 1370px) {
+ .vehicle-information-section {
+ & &__info {
+ flex-direction: column;
+ }
+
+ & &__info-banner {
+ margin-left: 0;
+ }
+ }
+}
diff --git a/frontend/src/features/permits/pages/Application/components/form/VehicleInformationSection/VehicleInformationSection.tsx b/frontend/src/features/permits/pages/Application/components/form/VehicleInformationSection/VehicleInformationSection.tsx
new file mode 100644
index 000000000..66f36e616
--- /dev/null
+++ b/frontend/src/features/permits/pages/Application/components/form/VehicleInformationSection/VehicleInformationSection.tsx
@@ -0,0 +1,197 @@
+import { useState } from "react";
+import { Controller, useFormContext } from "react-hook-form";
+import { Box, Button } from "@mui/material";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { faPlus } from "@fortawesome/free-solid-svg-icons";
+
+import "./VehicleInformationSection.scss";
+import { PERMIT_TYPES, PermitType } from "../../../../../types/PermitType";
+import { InfoBcGovBanner } from "../../../../../../../common/components/banners/InfoBcGovBanner";
+import { BANNER_MESSAGES } from "../../../../../../../common/constants/bannerMessages";
+import { PermitVehicleDetails } from "../../../../../types/PermitVehicleDetails";
+import { Vehicle, VehicleSubType } from "../../../../../../manageVehicles/types/Vehicle";
+import { VehicleDetails } from "./VehicleDetails";
+import { PowerUnitInfo } from "./PowerUnitInfo";
+import { AddPowerUnitDialog } from "./AddPowerUnitDialog";
+import { AddTrailer } from "./AddTrailer";
+import { VehicleInConfiguration } from "../../../../../types/PermitVehicleConfiguration";
+import { requiredPowerUnit } from "../../../../../../../common/helpers/validationMessages";
+import { ApplicationFormData } from "../../../../../types/application";
+
+export const VehicleInformationSection = ({
+ permitType,
+ feature,
+ vehicleFormData,
+ vehicleOptions,
+ subtypeOptions,
+ isSelectedLOAVehicle,
+ nextAllowedSubtypes,
+ powerUnitSubtypeNamesMap,
+ trailerSubtypeNamesMap,
+ selectedConfigSubtypes,
+ onSetSaveVehicle,
+ onSetVehicle,
+ onClearVehicle,
+ onUpdateVehicleConfigTrailers,
+}: {
+ permitType: PermitType;
+ feature: string;
+ vehicleFormData: PermitVehicleDetails;
+ vehicleOptions: Vehicle[];
+ subtypeOptions: VehicleSubType[];
+ isSelectedLOAVehicle: boolean;
+ nextAllowedSubtypes: {
+ value: string;
+ label: string;
+ }[];
+ powerUnitSubtypeNamesMap: Map;
+ trailerSubtypeNamesMap: Map;
+ selectedConfigSubtypes: string[];
+ onSetSaveVehicle: (saveVehicle: boolean) => void;
+ onSetVehicle: (vehicleDetails: PermitVehicleDetails) => void;
+ onClearVehicle: (saveVehicle: boolean) => void;
+ onUpdateVehicleConfigTrailers: (updatedTrailerSubtypes: VehicleInConfiguration[]) => void;
+}) => {
+ const isSingleTrip = permitType === PERMIT_TYPES.STOS;
+ const infoSectionClassName = `vehicle-information-section__info`
+ + `${isSingleTrip ? " vehicle-information-section__info--single-trip" : ""}`;
+
+ const infoBannerClassName = `vehicle-information-section__info-banner`
+ + `${isSingleTrip ? " vehicle-information-section__info-banner--single-trip" : ""}`;
+
+ const isPowerUnitSelectedForSingleTrip = isSingleTrip && Boolean(vehicleFormData.vin);
+
+ const [showAddPowerUnitDialog, setShowAddPowerUnitDialog] = useState(false);
+
+ const powerUnitFieldRef = "permitData.vehicleDetails";
+ const { clearErrors } = useFormContext();
+
+ const handleClickAddPowerUnit = () => {
+ if (isPowerUnitSelectedForSingleTrip) return;
+ setShowAddPowerUnitDialog(true);
+ };
+
+ const handleRemovePowerUnit = () => {
+ onClearVehicle(false);
+ onUpdateVehicleConfigTrailers([]);
+ };
+
+ const handleClosePowerUnitDialog = () => {
+ handleRemovePowerUnit();
+ setShowAddPowerUnitDialog(false);
+ };
+
+ const handleAddPowerUnit = (powerUnit: PermitVehicleDetails) => {
+ clearErrors(powerUnitFieldRef);
+ onSetVehicle(powerUnit);
+ setShowAddPowerUnitDialog(false);
+ };
+
+ return (
+
+
+ Vehicle Information
+
+
+
+
+ Choose a saved vehicle from your inventory or enter new vehicle
+ information below.
+
+
+
+
+ {BANNER_MESSAGES.CANNOT_FIND_VEHICLE.DETAIL}
+
+
+ {BANNER_MESSAGES.CANNOT_FIND_VEHICLE.INELIGIBLE_SUBTYPES}
+
+ }
+ />
+
+ {isSingleTrip ? (
+ (v.vin.trim() !== "") || requiredPowerUnit(),
+ }}
+ render={({ fieldState: { error } }) => (
+
+
+
+ Add Power Unit
+
+
+ {error?.message ? (
+
+ {error.message}
+
+ ) : null}
+
+ )}
+ />
+ ) : (
+
+ )}
+
+
+ {isPowerUnitSelectedForSingleTrip ? (
+
+ ) : null}
+
+ {isPowerUnitSelectedForSingleTrip ? (
+
+ ) : null}
+
+
+ {showAddPowerUnitDialog ? (
+
+ ) : null}
+
+ );
+};
+
diff --git a/frontend/src/features/permits/pages/Application/components/form/VehicleDetails/customFields/SelectUnitOrPlate.scss b/frontend/src/features/permits/pages/Application/components/form/VehicleInformationSection/components/SelectUnitOrPlate.scss
similarity index 100%
rename from frontend/src/features/permits/pages/Application/components/form/VehicleDetails/customFields/SelectUnitOrPlate.scss
rename to frontend/src/features/permits/pages/Application/components/form/VehicleInformationSection/components/SelectUnitOrPlate.scss
diff --git a/frontend/src/features/permits/pages/Application/components/form/VehicleDetails/customFields/SelectUnitOrPlate.tsx b/frontend/src/features/permits/pages/Application/components/form/VehicleInformationSection/components/SelectUnitOrPlate.tsx
similarity index 100%
rename from frontend/src/features/permits/pages/Application/components/form/VehicleDetails/customFields/SelectUnitOrPlate.tsx
rename to frontend/src/features/permits/pages/Application/components/form/VehicleInformationSection/components/SelectUnitOrPlate.tsx
diff --git a/frontend/src/features/permits/pages/Application/components/form/VehicleDetails/customFields/SelectVehicleDropdown.scss b/frontend/src/features/permits/pages/Application/components/form/VehicleInformationSection/components/SelectVehicleDropdown.scss
similarity index 100%
rename from frontend/src/features/permits/pages/Application/components/form/VehicleDetails/customFields/SelectVehicleDropdown.scss
rename to frontend/src/features/permits/pages/Application/components/form/VehicleInformationSection/components/SelectVehicleDropdown.scss
diff --git a/frontend/src/features/permits/pages/Application/components/form/VehicleDetails/customFields/SelectVehicleDropdown.tsx b/frontend/src/features/permits/pages/Application/components/form/VehicleInformationSection/components/SelectVehicleDropdown.tsx
similarity index 93%
rename from frontend/src/features/permits/pages/Application/components/form/VehicleDetails/customFields/SelectVehicleDropdown.tsx
rename to frontend/src/features/permits/pages/Application/components/form/VehicleInformationSection/components/SelectVehicleDropdown.tsx
index feeab5e40..8bede4b65 100644
--- a/frontend/src/features/permits/pages/Application/components/form/VehicleDetails/customFields/SelectVehicleDropdown.tsx
+++ b/frontend/src/features/permits/pages/Application/components/form/VehicleInformationSection/components/SelectVehicleDropdown.tsx
@@ -11,8 +11,8 @@ import {
import "./SelectVehicleDropdown.scss";
import { getDefaultRequiredVal } from "../../../../../../../../common/helpers/util";
-import { sortVehicles } from "../../../../../../helpers/sorter";
-import { VEHICLE_CHOOSE_FROM } from "../../../../../../constants/constants";
+import { sortVehicles } from "../../../../../../helpers/vehicles/sortVehicles";
+import { VEHICLE_CHOOSE_FROM, VehicleChooseFrom } from "../../../../../../constants/constants";
import { EMPTY_VEHICLE_UNIT_NUMBER } from "../../../../../../../../common/constants/constants";
import { Nullable } from "../../../../../../../../common/types/common";
import { PermitVehicleDetails } from "../../../../../../types/PermitVehicleDetails";
@@ -49,7 +49,7 @@ export const SelectVehicleDropdown = ({
handleSelectVehicle,
handleClearVehicle,
}: {
- chooseFrom: string;
+ chooseFrom: VehicleChooseFrom;
selectedVehicle: Nullable;
label: string;
vehicleOptions: Vehicle[];
@@ -57,8 +57,8 @@ export const SelectVehicleDropdown = ({
handleClearVehicle: () => void;
}) => {
const eligibleVehicles = useMemo(() => sortVehicles(
- chooseFrom,
vehicleOptions,
+ chooseFrom,
), [chooseFrom, vehicleOptions]);
const selectedOption = selectedVehicle
@@ -107,7 +107,7 @@ export const SelectVehicleDropdown = ({
getOptionLabel={(option) => {
if (!option) return "";
if (!option.unitNumber) option.unitNumber = EMPTY_VEHICLE_UNIT_NUMBER;
- return chooseFrom == "plate" ? option.plate : option.unitNumber;
+ return chooseFrom == VEHICLE_CHOOSE_FROM.PLATE ? option.plate : option.unitNumber;
}}
className="select-vehicle-dropdown__autocomplete"
renderOption={(props, option) => {
diff --git a/frontend/src/features/permits/pages/Application/components/review/ApplicationNotes.scss b/frontend/src/features/permits/pages/Application/components/review/ApplicationNotes.scss
new file mode 100644
index 000000000..ab38874e5
--- /dev/null
+++ b/frontend/src/features/permits/pages/Application/components/review/ApplicationNotes.scss
@@ -0,0 +1,14 @@
+@use "../../../../../../themes/orbcStyles";
+
+@include orbcStyles.permit-main-box-style(".review-application-notes");
+@include orbcStyles.permit-left-box-style(".review-application-notes__header");
+@include orbcStyles.permit-right-box-style(".review-application-notes__body");
+
+@media (width < 370px) {
+ .review-application-notes {
+ &__body {
+ max-width: 100%;
+ min-width: 100%;
+ }
+ }
+}
diff --git a/frontend/src/features/permits/pages/Application/components/review/ApplicationNotes.tsx b/frontend/src/features/permits/pages/Application/components/review/ApplicationNotes.tsx
new file mode 100644
index 000000000..8155ab7de
--- /dev/null
+++ b/frontend/src/features/permits/pages/Application/components/review/ApplicationNotes.tsx
@@ -0,0 +1,29 @@
+import { Box, Typography } from "@mui/material";
+
+import "./ApplicationNotes.scss";
+import { Nullable } from "../../../../../../common/types/common";
+
+export const ApplicationNotes = ({
+ applicationNotes,
+}: {
+ applicationNotes?: Nullable;
+}) => {
+ return applicationNotes ? (
+
+
+
+ Application Notes
+
+
+
+
+
+ {applicationNotes}
+
+
+
+ ) : null;
+};
diff --git a/frontend/src/features/permits/pages/Application/components/review/CommodityDetails.scss b/frontend/src/features/permits/pages/Application/components/review/CommodityDetails.scss
new file mode 100644
index 000000000..6750b9d29
--- /dev/null
+++ b/frontend/src/features/permits/pages/Application/components/review/CommodityDetails.scss
@@ -0,0 +1,28 @@
+@use "../../../../../../themes/orbcStyles";
+
+@include orbcStyles.permit-main-box-style(".review-commodity-details");
+@include orbcStyles.permit-left-box-style(".review-commodity-details__header");
+@include orbcStyles.permit-right-box-style(".review-commodity-details__body");
+
+.review-commodity-details {
+ &__body {
+ .commodity-info {
+ &__label {
+ font-weight: bold;
+ }
+
+ &--load-description {
+ margin-top: 1.5rem;
+ }
+ }
+ }
+}
+
+@media (width < 370px) {
+ .review-commodity-details {
+ &__body {
+ max-width: 100%;
+ min-width: 100%;
+ }
+ }
+}
diff --git a/frontend/src/features/permits/pages/Application/components/review/CommodityDetails.tsx b/frontend/src/features/permits/pages/Application/components/review/CommodityDetails.tsx
new file mode 100644
index 000000000..642389ce1
--- /dev/null
+++ b/frontend/src/features/permits/pages/Application/components/review/CommodityDetails.tsx
@@ -0,0 +1,87 @@
+import { Box, Typography } from "@mui/material";
+
+import "./CommodityDetails.scss";
+import { areValuesDifferent } from "../../../../../../common/helpers/equality";
+import { Nullable } from "../../../../../../common/types/common";
+import { PermittedCommodity } from "../../../../types/PermittedCommodity";
+import { DiffChip } from "./DiffChip";
+import { getDefaultRequiredVal } from "../../../../../../common/helpers/util";
+
+export const CommodityDetails = ({
+ commodity,
+ oldCommodity,
+ showChangedFields = false,
+ commodityOptions,
+}: {
+ commodity?: Nullable;
+ oldCommodity?: Nullable;
+ showChangedFields?: boolean;
+ commodityOptions: {
+ label: string;
+ value: string;
+ }[];
+}) => {
+ const changedFields = showChangedFields
+ ? {
+ commodityType: areValuesDifferent(
+ commodity?.commodityType,
+ oldCommodity?.commodityType,
+ ),
+ loadDescription: areValuesDifferent(
+ commodity?.loadDescription,
+ oldCommodity?.loadDescription,
+ ),
+ }
+ : {
+ commodityType: false,
+ loadDescription: false,
+ };
+
+ const commodityTypeDisplay = getDefaultRequiredVal(
+ "",
+ commodityOptions.find(({ value }) => value === commodity?.commodityType)?.label,
+ commodity?.commodityType,
+ );
+
+ return commodity ? (
+
+
+
+ Commodity Details
+
+
+
+
+
+
+ Commodity Type
+
+ {changedFields.commodityType ? : null}
+
+
+
+ {commodityTypeDisplay}
+
+
+
+
+
+ Load Description
+
+ {changedFields.loadDescription ? : null}
+
+
+
+ {commodity.loadDescription}
+
+
+
+
+ ) : null;
+};
diff --git a/frontend/src/features/permits/pages/Application/components/review/LoadedDimensions.scss b/frontend/src/features/permits/pages/Application/components/review/LoadedDimensions.scss
new file mode 100644
index 000000000..1b98bbf38
--- /dev/null
+++ b/frontend/src/features/permits/pages/Application/components/review/LoadedDimensions.scss
@@ -0,0 +1,28 @@
+@use "../../../../../../themes/orbcStyles";
+
+@include orbcStyles.permit-main-box-style(".review-loaded-dimensions");
+@include orbcStyles.permit-left-box-style(".review-loaded-dimensions__header");
+@include orbcStyles.permit-right-box-style(".review-loaded-dimensions__body");
+
+.review-loaded-dimensions {
+ &__body {
+ display: grid;
+ grid-template-columns: repeat(3, [col] 1fr);
+ row-gap: 1rem;
+
+ .loaded-dimension {
+ &__label {
+ font-weight: bold;
+ }
+ }
+ }
+}
+
+@media (width < 370px) {
+ .review-loaded-dimensions {
+ &__body {
+ max-width: 100%;
+ min-width: 100%;
+ }
+ }
+}
diff --git a/frontend/src/features/permits/pages/Application/components/review/LoadedDimensions.tsx b/frontend/src/features/permits/pages/Application/components/review/LoadedDimensions.tsx
new file mode 100644
index 000000000..fe2539305
--- /dev/null
+++ b/frontend/src/features/permits/pages/Application/components/review/LoadedDimensions.tsx
@@ -0,0 +1,136 @@
+import { Box, Typography } from "@mui/material";
+
+import "./LoadedDimensions.scss";
+import { areValuesDifferent } from "../../../../../../common/helpers/equality";
+import { Nullable } from "../../../../../../common/types/common";
+import { DiffChip } from "./DiffChip";
+import { PermitVehicleConfiguration } from "../../../../types/PermitVehicleConfiguration";
+import { getDefaultRequiredVal } from "../../../../../../common/helpers/util";
+
+export const LoadedDimensions = ({
+ vehicleConfiguration,
+ oldVehicleConfiguration,
+ showChangedFields = false,
+}: {
+ vehicleConfiguration?: Nullable;
+ oldVehicleConfiguration?: Nullable;
+ showChangedFields?: boolean;
+}) => {
+ const changedFields = showChangedFields
+ ? {
+ overallWidth: areValuesDifferent(
+ vehicleConfiguration?.overallWidth,
+ oldVehicleConfiguration?.overallWidth,
+ ),
+ overallHeight: areValuesDifferent(
+ vehicleConfiguration?.overallHeight,
+ oldVehicleConfiguration?.overallHeight,
+ ),
+ overallLength: areValuesDifferent(
+ vehicleConfiguration?.overallLength,
+ oldVehicleConfiguration?.overallLength,
+ ),
+ frontProjection: areValuesDifferent(
+ vehicleConfiguration?.frontProjection,
+ oldVehicleConfiguration?.frontProjection,
+ ),
+ rearProjection: areValuesDifferent(
+ vehicleConfiguration?.rearProjection,
+ oldVehicleConfiguration?.rearProjection,
+ ),
+ }
+ : {
+ overallWidth: false,
+ overallHeight: false,
+ overallLength: false,
+ frontProjection: false,
+ rearProjection: false,
+ };
+
+ return vehicleConfiguration ? (
+
+
+
+ Loaded Dimensions (Metres)
+
+
+
+
+
+
+ Overall Width
+
+ {changedFields.overallWidth ? : null}
+
+
+
+ {getDefaultRequiredVal(0, vehicleConfiguration?.overallWidth).toFixed(2)}
+
+
+
+
+
+ Overall Height
+
+ {changedFields.overallHeight ? : null}
+
+
+
+ {getDefaultRequiredVal(0, vehicleConfiguration?.overallHeight).toFixed(2)}
+
+
+
+
+
+ Overall Length
+
+ {changedFields.overallLength ? : null}
+
+
+
+ {getDefaultRequiredVal(0, vehicleConfiguration?.overallLength).toFixed(2)}
+
+
+
+
+
+ Front Projection
+
+ {changedFields.frontProjection ? : null}
+
+
+
+ {getDefaultRequiredVal(0, vehicleConfiguration?.frontProjection).toFixed(2)}
+
+
+
+
+
+ Rear Projection
+
+ {changedFields.rearProjection ? : null}
+
+
+
+ {getDefaultRequiredVal(0, vehicleConfiguration?.rearProjection).toFixed(2)}
+
+
+
+
+ ) : null;
+};
diff --git a/frontend/src/features/permits/pages/Application/components/review/PermitReview.scss b/frontend/src/features/permits/pages/Application/components/review/PermitReview.scss
index fa8401702..5afae0637 100644
--- a/frontend/src/features/permits/pages/Application/components/review/PermitReview.scss
+++ b/frontend/src/features/permits/pages/Application/components/review/PermitReview.scss
@@ -1,8 +1,8 @@
-@import "../../../../../../themes/orbcStyles";
+@use "../../../../../../themes/orbcStyles";
.permit-review {
padding-top: 1.5rem;
- background-color: $bc-white;
+ background-color: orbcStyles.$bc-white;
&__container {
padding-bottom: 5rem;
@@ -11,4 +11,9 @@
margin-bottom: 1.5rem;
}
}
+
+ .review-actions {
+ border-top: 1px solid orbcStyles.$bc-text-box-border-grey;
+ padding-top: 2.5rem;
+ }
}
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 5b1c3934a..0f7f00046 100644
--- a/frontend/src/features/permits/pages/Application/components/review/PermitReview.tsx
+++ b/frontend/src/features/permits/pages/Application/components/review/PermitReview.tsx
@@ -1,5 +1,16 @@
import { Box } from "@mui/material";
import { Dayjs } from "dayjs";
+import { useMemo } from "react";
+
+import "./PermitReview.scss";
+import { ReviewActions } from "./ReviewActions";
+import { ReviewContactDetails } from "./ReviewContactDetails";
+import { ReviewFeeSummary } from "./ReviewFeeSummary";
+import { ReviewPermitDetails } from "./ReviewPermitDetails";
+import { ReviewPermitLOAs } from "./ReviewPermitLOAs";
+import { ReviewVehicleInfo } from "./ReviewVehicleInfo";
+import { PERMIT_TYPES, PermitType } from "../../../../types/PermitType";
+import { PermitVehicleDetails } from "../../../../types/PermitVehicleDetails";
import { WarningBcGovBanner } from "../../../../../../common/components/banners/WarningBcGovBanner";
import { Nullable } from "../../../../../../common/types/common";
import { CompanyProfile } from "../../../../../manageProfile/types/manageProfile";
@@ -9,22 +20,21 @@ import { Application } from "../../../../types/application";
import { PermitCondition } from "../../../../types/PermitCondition";
import { PermitContactDetails } from "../../../../types/PermitContactDetails";
import { PermitLOA } from "../../../../types/PermitLOA";
+import { getDefaultRequiredVal } from "../../../../../../common/helpers/util";
+import { ApplicationRejectionHistory } from "../../../../types/ApplicationRejectionHistory";
+import { ReviewApplicationRejectionHistory } from "./ReviewApplicationRejectionHistory";
+import { isPermitStartOrExpiryDateInPast } from "../../../../helpers/dateSelection";
+import { CommodityDetails } from "./CommodityDetails";
+import { PermittedCommodity } from "../../../../types/PermittedCommodity";
+import { PermitVehicleConfiguration } from "../../../../types/PermitVehicleConfiguration";
+import { PermittedRoute } from "../../../../types/PermittedRoute";
+import { LoadedDimensions } from "./LoadedDimensions";
+import { ApplicationNotes } from "./ApplicationNotes";
+import { TripDetails } from "./TripDetails";
import {
PERMIT_REVIEW_CONTEXTS,
PermitReviewContext,
} from "../../../../types/PermitReviewContext";
-import { PermitType } from "../../../../types/PermitType";
-import { PermitVehicleDetails } from "../../../../types/PermitVehicleDetails";
-import "./PermitReview.scss";
-import { ReviewActions } from "./ReviewActions";
-import { ReviewContactDetails } from "./ReviewContactDetails";
-import { ReviewFeeSummary } from "./ReviewFeeSummary";
-import { ReviewPermitDetails } from "./ReviewPermitDetails";
-import { ReviewPermitLOAs } from "./ReviewPermitLOAs";
-import { ReviewVehicleInfo } from "./ReviewVehicleInfo";
-import { ApplicationRejectionHistory } from "../../../../types/ApplicationRejectionHistory";
-import { ReviewApplicationRejectionHistory } from "./ReviewApplicationRejectionHistory";
-import { isPermitStartOrExpiryDateInPast } from "../../../../helpers/dateSelection";
interface PermitReviewProps {
reviewContext: PermitReviewContext;
@@ -39,6 +49,11 @@ interface PermitReviewProps {
permitDuration?: Nullable;
permitExpiryDate?: Nullable;
permitConditions?: Nullable;
+ permittedCommodity?: Nullable;
+ commodityOptions: {
+ label: string;
+ value: string;
+ }[];
continueBtnText?: string;
isAmendAction: boolean;
children?: React.ReactNode;
@@ -49,6 +64,9 @@ interface PermitReviewProps {
trailerSubTypes?: Nullable;
vehicleDetails?: Nullable;
vehicleWasSaved?: Nullable;
+ vehicleConfiguration?: Nullable;
+ route?: Nullable;
+ applicationNotes?: Nullable;
onEdit: () => void;
onContinue?: () => Promise;
onAddToCart?: () => Promise;
@@ -61,9 +79,21 @@ interface PermitReviewProps {
doingBusinessAs?: Nullable;
loas?: Nullable;
applicationRejectionHistory?: Nullable;
+ isStaffUser: boolean;
}
export const PermitReview = (props: PermitReviewProps) => {
+ const { powerUnitSubTypes, trailerSubTypes } = props;
+ const powerUnitSubtypeNamesMap = useMemo(() => new Map(
+ getDefaultRequiredVal([], powerUnitSubTypes)
+ .map(({ typeCode, type }) => [typeCode, type]),
+ ), [powerUnitSubTypes]);
+
+ const trailerSubtypeNamesMap = useMemo(() => new Map(
+ getDefaultRequiredVal([], trailerSubTypes)
+ .map(({ typeCode, type }) => [typeCode, type]),
+ ), [trailerSubTypes]);
+
const shouldShowRejectionHistory =
(props.reviewContext === PERMIT_REVIEW_CONTEXTS.QUEUE ||
props.reviewContext === PERMIT_REVIEW_CONTEXTS.APPLY) &&
@@ -78,6 +108,9 @@ export const PermitReview = (props: PermitReviewProps) => {
)
: false;
+ const hasToCartButton = props.reviewContext === PERMIT_REVIEW_CONTEXTS.APPLY
+ && (props.permitType !== PERMIT_TYPES.STOS || props.isStaffUser);
+
return (
@@ -115,20 +148,43 @@ export const PermitReview = (props: PermitReviewProps) => {
showDateErrorBanner={invalidPermitDates}
/>
+
+
+
+
- {shouldShowRejectionHistory && props.applicationRejectionHistory && (
+
+
+
+
+ {shouldShowRejectionHistory && props.applicationRejectionHistory ? (
- )}
+ ) : null}
{
onEdit={props.onEdit}
continueBtnText={props.continueBtnText}
onContinue={props.onContinue}
- hasToCartButton={props.reviewContext === PERMIT_REVIEW_CONTEXTS.APPLY}
+ hasToCartButton={hasToCartButton}
onAddToCart={props.onAddToCart}
handleApproveButton={props.handleApproveButton}
handleRejectButton={props.handleRejectButton}
diff --git a/frontend/src/features/permits/pages/Application/components/review/ReviewActions.scss b/frontend/src/features/permits/pages/Application/components/review/ReviewActions.scss
index c6363f277..8f22a434b 100644
--- a/frontend/src/features/permits/pages/Application/components/review/ReviewActions.scss
+++ b/frontend/src/features/permits/pages/Application/components/review/ReviewActions.scss
@@ -2,7 +2,6 @@
.review-actions {
background-color: $bc-white;
- padding-top: 1.5rem;
display: flex;
align-items: center;
justify-content: flex-end;
diff --git a/frontend/src/features/permits/pages/Application/components/review/ReviewActions.tsx b/frontend/src/features/permits/pages/Application/components/review/ReviewActions.tsx
index e7e51cb0c..4e9d069f7 100644
--- a/frontend/src/features/permits/pages/Application/components/review/ReviewActions.tsx
+++ b/frontend/src/features/permits/pages/Application/components/review/ReviewActions.tsx
@@ -1,11 +1,12 @@
import { faPencil } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Box, Button } from "@mui/material";
+
+import "./ReviewActions.scss";
import {
PERMIT_REVIEW_CONTEXTS,
PermitReviewContext,
} from "../../../../types/PermitReviewContext";
-import "./ReviewActions.scss";
export const ReviewActions = ({
reviewContext,
@@ -33,23 +34,20 @@ export const ReviewActions = ({
return (
{
- // hide edit button until edit application in queue feature is complete
- reviewContext !== PERMIT_REVIEW_CONTEXTS.QUEUE && (
-
-
- Edit
-
- )
+
+
+ Edit
+
}
{hasToCartButton ? (
@@ -80,7 +78,7 @@ export const ReviewActions = ({
) : null}
- {reviewContext === PERMIT_REVIEW_CONTEXTS.QUEUE && (
+ {reviewContext === PERMIT_REVIEW_CONTEXTS.QUEUE ? (
<>
Reject
+
>
- )}
+ ) : null}
);
};
diff --git a/frontend/src/features/permits/pages/Application/components/review/ReviewContactDetails.scss b/frontend/src/features/permits/pages/Application/components/review/ReviewContactDetails.scss
index dfbf97372..d28bfb102 100644
--- a/frontend/src/features/permits/pages/Application/components/review/ReviewContactDetails.scss
+++ b/frontend/src/features/permits/pages/Application/components/review/ReviewContactDetails.scss
@@ -7,9 +7,6 @@
.review-contact-details {
&__body {
.contact-details {
- gap: 2.5rem;
- padding-top: 1.5rem;
-
&__label {
margin-right: 0.5em;
}
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 473a5d03e..53b4f2d32 100644
--- a/frontend/src/features/permits/pages/Application/components/review/ReviewContactDetails.tsx
+++ b/frontend/src/features/permits/pages/Application/components/review/ReviewContactDetails.tsx
@@ -72,6 +72,7 @@ export const ReviewContactDetails = ({
Contact Information
+
@@ -81,10 +82,13 @@ export const ReviewContactDetails = ({
>
{nameDisplay(contactDetails?.firstName, contactDetails?.lastName)}
+
{changedFields.name ? : null}
+
Primary Phone:
+
+
{changedFields.phone1 ? : null}
+
{contactDetails?.phone2 ? (
Alternate Phone:
+
+
{changedFields.phone2 ? : null}
) : null}
+
Company Email:
+
{contactDetails?.email}
+
{changedFields.email ? : null}
+
{contactDetails?.additionalEmail ? (
Additional Email:
+
{contactDetails?.additionalEmail}
+
{changedFields.additionalEmail ? : null}
) : null}
+
{contactDetails?.fax ? (
Fax:
+
{phoneDisplay(contactDetails?.fax)}
+
{changedFields.fax ? : null}
) : null}
diff --git a/frontend/src/features/permits/pages/Application/components/review/ReviewFeeSummary.scss b/frontend/src/features/permits/pages/Application/components/review/ReviewFeeSummary.scss
index 447e8dd16..b5a1fd1f6 100644
--- a/frontend/src/features/permits/pages/Application/components/review/ReviewFeeSummary.scss
+++ b/frontend/src/features/permits/pages/Application/components/review/ReviewFeeSummary.scss
@@ -7,12 +7,17 @@
.review-fee-summary {
&__body {
.fee-summary-wrapper {
- gap: 2.5rem;
- padding-top: 1.5rem;
+ .fee-summary {
+ margin-top: 0;
+ }
.fee-summary-banner {
max-width: 690px;
}
+
+ .confirmation-checkboxes {
+ margin-top: 1.5rem;
+ }
}
}
}
diff --git a/frontend/src/features/permits/pages/Application/components/review/ReviewPermitDetails.scss b/frontend/src/features/permits/pages/Application/components/review/ReviewPermitDetails.scss
index e114b1327..9f5051b03 100644
--- a/frontend/src/features/permits/pages/Application/components/review/ReviewPermitDetails.scss
+++ b/frontend/src/features/permits/pages/Application/components/review/ReviewPermitDetails.scss
@@ -1,5 +1,4 @@
@use "../../../../../../themes/orbcStyles";
-@import "../../../../../../themes/orbcStyles";
@include orbcStyles.permit-main-box-style(".review-permit-details");
@include orbcStyles.permit-left-box-style(".review-permit-details__header");
@@ -8,27 +7,32 @@
.review-permit-details {
&__body {
.permit-dates {
- gap: 40px;
- padding-top: 24px;
-
&__label {
- font-weight: 600;
+ font-weight: bold;
+ }
+
+ &__duration {
+ margin-top: 1.5rem;
}
}
.permit-expiry-banner {
- width: 90%;
+ margin-top: 1.5rem;
}
.permit-error-banner {
padding-top: 1.5rem;
.bc-gov-alertbanner {
- background-color: $bc-messages-red-background;
- color: $bc-messages-red-text;
+ background-color: orbcStyles.$bc-messages-red-background;
+ color: orbcStyles.$bc-messages-red-text;
margin-bottom: 0;
}
}
+
+ .permit-conditions {
+ margin-top: 1.5rem;
+ }
}
}
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 bc5620188..bb2144360 100644
--- a/frontend/src/features/permits/pages/Application/components/review/ReviewPermitDetails.tsx
+++ b/frontend/src/features/permits/pages/Application/components/review/ReviewPermitDetails.tsx
@@ -1,20 +1,21 @@
import { Box, Typography } from "@mui/material";
import { Dayjs } from "dayjs";
-import { ErrorBcGovBanner } from "../../../../../../common/components/banners/ErrorBcGovBanner";
-import { PermitExpiryDateBanner } from "../../../../../../common/components/banners/PermitExpiryDateBanner";
-import { areValuesDifferent } from "../../../../../../common/helpers/equality";
-import {
- DATE_FORMATS,
- dayjsToLocalStr,
-} from "../../../../../../common/helpers/formatDate";
+
+import "./ReviewPermitDetails.scss";
import { applyWhenNotNullable } from "../../../../../../common/helpers/util";
import { Nullable } from "../../../../../../common/types/common";
import { BASE_DAYS_IN_YEAR } from "../../../../constants/constants";
import { PermitCondition } from "../../../../types/PermitCondition";
import { DiffChip } from "./DiffChip";
import { ReviewConditionsTable } from "./ReviewConditionsTable";
-import "./ReviewPermitDetails.scss";
import { pastStartOrExpiryDate } from "../../../../../../common/helpers/validationMessages";
+import { ErrorBcGovBanner } from "../../../../../../common/components/banners/ErrorBcGovBanner";
+import { PermitExpiryDateBanner } from "../../../../../../common/components/banners/PermitExpiryDateBanner";
+import { areValuesDifferent } from "../../../../../../common/helpers/equality";
+import {
+ DATE_FORMATS,
+ dayjsToLocalStr,
+} from "../../../../../../common/helpers/formatDate";
export const ReviewPermitDetails = ({
startDate,
@@ -57,6 +58,13 @@ export const ReviewPermitDetails = ({
duration: false,
};
+ const displayDuration = (duration: number) => {
+ const measurementUnit = duration !== 1 ? "Days" : "Day";
+ return duration === BASE_DAYS_IN_YEAR
+ ? "1 Year"
+ : `${duration} ${measurementUnit}`;
+ };
+
return (
@@ -64,39 +72,47 @@ export const ReviewPermitDetails = ({
Permit Details
+
-
- Start Date
- {changedFields.startDate ? : null}
-
-
- {applyWhenNotNullable(
- (dayJsObject) =>
- dayjsToLocalStr(dayJsObject, DATE_FORMATS.DATEONLY_SLASH),
- startDate,
- "",
- )}
-
-
- Permit Duration
- {changedFields.duration ? : null}
-
-
- {applyWhenNotNullable(
- (duration) =>
- duration === BASE_DAYS_IN_YEAR ? "1 Year" : `${duration} Days`,
- permitDuration,
- "",
- )}
-
+
+
+ Start Date
+ {changedFields.startDate ? : null}
+
+
+
+ {applyWhenNotNullable(
+ (dayJsObject) =>
+ dayjsToLocalStr(dayJsObject, DATE_FORMATS.DATEONLY_SLASH),
+ startDate,
+ "",
+ )}
+
+
+
+
+
+ Permit Duration
+ {changedFields.duration ? : null}
+
+
+
+ {applyWhenNotNullable(
+ displayDuration,
+ permitDuration,
+ "",
+ )}
+
+
+
- {showDateErrorBanner && (
+ {showDateErrorBanner ? (
- )}
+ ) : null}
+
-
+
Selected commodities and their respective CVSE forms.
+
diff --git a/frontend/src/features/permits/pages/Application/components/review/ReviewVehicleInfo.scss b/frontend/src/features/permits/pages/Application/components/review/ReviewVehicleInfo.scss
index d962248dc..d99cf4f1b 100644
--- a/frontend/src/features/permits/pages/Application/components/review/ReviewVehicleInfo.scss
+++ b/frontend/src/features/permits/pages/Application/components/review/ReviewVehicleInfo.scss
@@ -7,23 +7,51 @@
.review-vehicle-info {
&__body {
.info-section {
- gap: 2.5rem;
- padding-top: 1.5rem;
-
&__label {
- font-weight: 600;
+ font-weight: bold;
&--indicator {
- font-weight: 500;
+ font-weight: normal;
}
}
&__msg {
- font-weight: 600;
+ font-weight: bold;
+ color: orbcStyles.$bc-green;
+
+ .icon {
+ margin-right: 0.5rem;
+ }
+ }
+ }
+
+ .selected-power-unit-and-trailers {
+ .selected-power-unit {
+ padding: 0 0 1.5rem 0;
+
+ .power-unit-info-display {
+ padding: 1rem 0 0 0;
+ }
+ }
+
+ .selected-trailers {
+ border-top: 1px solid orbcStyles.$bc-text-box-border-grey;
+ padding: 1.5rem 0 0 0;
+
+ .selected-vehicle-subtype-list {
+ margin: 1rem 0 0 0;
+ box-shadow: none;
+ border-radius: 0;
+ }
+ }
+
+ .vehicle-saved {
+ margin-top: 1rem;
+ font-weight: bold;
color: orbcStyles.$bc-green;
.icon {
- margin-right: 0.5em;
+ margin-right: 0.5rem;
}
}
}
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 284727a32..0fdaee09a 100644
--- a/frontend/src/features/permits/pages/Application/components/review/ReviewVehicleInfo.tsx
+++ b/frontend/src/features/permits/pages/Application/components/review/ReviewVehicleInfo.tsx
@@ -6,58 +6,51 @@ import "./ReviewVehicleInfo.scss";
import { DiffChip } from "./DiffChip";
import { areValuesDifferent } from "../../../../../../common/helpers/equality";
import { Nullable } from "../../../../../../common/types/common";
-import { PermitVehicleDetails } from "../../../../types/PermitVehicleDetails";
+import { VehicleType } from "../../../../../manageVehicles/types/Vehicle";
+import { getDefaultRequiredVal } from "../../../../../../common/helpers/util";
+import { DEFAULT_VEHICLE_TYPE, PermitVehicleDetails } from "../../../../types/PermitVehicleDetails";
+import { getCountryFullName } from "../../../../../../common/helpers/countries/getCountryFullName";
+import { getProvinceFullName } from "../../../../../../common/helpers/countries/getProvinceFullName";
+import { PERMIT_TYPES, PermitType } from "../../../../types/PermitType";
+import { PowerUnitInfoDisplay } from "../common/PowerUnitInfoDisplay";
+import { SelectedVehicleSubtypeList } from "../common/SelectedVehicleSubtypeList";
+import { useMemoizedArray } from "../../../../../../common/hooks/useMemoizedArray";
+import { VehicleInConfiguration } from "../../../../types/PermitVehicleConfiguration";
import {
- mapTypeCodeToObject,
+ getSubtypeNameByCode,
vehicleTypeDisplayText,
} from "../../../../helpers/mappers";
-import {
- VehicleSubType,
- VehicleType,
-} from "../../../../../manageVehicles/types/Vehicle";
-
-import {
- formatCountry,
- formatProvince,
-} from "../../../../../../common/helpers/formatCountryProvince";
-
export const ReviewVehicleInfo = ({
+ permitType,
vehicleDetails,
vehicleWasSaved,
- powerUnitSubTypes,
- trailerSubTypes,
+ powerUnitSubtypeNamesMap,
+ trailerSubtypeNamesMap,
showChangedFields = false,
oldFields,
+ selectedVehicleConfigSubtypes,
}: {
+ permitType?: Nullable;
vehicleDetails?: Nullable;
vehicleWasSaved?: Nullable;
- powerUnitSubTypes?: Nullable;
- trailerSubTypes?: Nullable;
+ powerUnitSubtypeNamesMap: Map;
+ trailerSubtypeNamesMap: Map;
showChangedFields?: boolean;
oldFields?: Nullable;
+ selectedVehicleConfigSubtypes?: Nullable;
}) => {
- const DisplayVehicleType = () => {
- const vehicleTypeCode = vehicleDetails?.vehicleType;
- if (!vehicleTypeCode) return "";
- return vehicleTypeDisplayText(vehicleTypeCode as VehicleType);
- };
-
- const DisplayVehicleSubType = () => {
- const code = vehicleDetails?.vehicleSubType;
- const vehicleTypeCode = vehicleDetails?.vehicleType;
-
- if (!code || !vehicleTypeCode) return "";
+ const vehicleType = getDefaultRequiredVal(
+ DEFAULT_VEHICLE_TYPE,
+ vehicleDetails?.vehicleType,
+ ) as VehicleType;
- const typeObject = mapTypeCodeToObject(
- code,
- vehicleTypeCode,
- powerUnitSubTypes,
- trailerSubTypes,
- );
-
- return typeObject?.type;
- };
+ const vehicleSubtype = getSubtypeNameByCode(
+ powerUnitSubtypeNamesMap,
+ trailerSubtypeNamesMap,
+ vehicleType,
+ getDefaultRequiredVal("", vehicleDetails?.vehicleSubType),
+ );
const changedFields = showChangedFields
? {
@@ -98,118 +91,195 @@ export const ReviewVehicleInfo = ({
subtype: false,
};
+ const provinceDisplay = getProvinceFullName(
+ vehicleDetails?.countryCode,
+ vehicleDetails?.provinceCode,
+ );
+
+ const selectedSubtypesDisplay = useMemoizedArray(
+ getDefaultRequiredVal(
+ [],
+ selectedVehicleConfigSubtypes,
+ ).map(({ vehicleSubType }) => {
+ if (vehicleSubType === "NONEXXX") return "None";
+ return getDefaultRequiredVal(
+ vehicleSubType,
+ trailerSubtypeNamesMap.get(vehicleSubType),
+ powerUnitSubtypeNamesMap.get(vehicleSubType),
+ );
+ }),
+ (selectedSubtype) => selectedSubtype,
+ (subtype1, subtype2) => subtype1 === subtype2,
+ );
+
+ const showDiffChip = (show: boolean) => {
+ return show ? : null;
+ };
+
return (
Vehicle Information
+
-
-
- Unit #
- {changedFields.unit ? : null}
-
-
- {vehicleDetails?.unitNumber}
-
-
- VIN{" "}
-
- (last 6 digits)
-
- {changedFields.vin ? : null}
-
-
- {vehicleDetails?.vin}
-
-
- Plate
- {changedFields.plate ? : null}
-
-
- {vehicleDetails?.plate}
-
-
- Make
- {changedFields.make ? : null}
-
-
- {vehicleDetails?.make}
-
-
- Year
- {changedFields.year ? : null}
-
-
- {vehicleDetails?.year}
-
-
- Country
- {changedFields.country ? : null}
-
-
- {formatCountry(vehicleDetails?.countryCode)}
-
-
- Province / State
- {changedFields.province ? : null}
-
-
- {formatProvince(
- vehicleDetails?.countryCode,
- vehicleDetails?.provinceCode,
- )}
-
-
- Vehicle Type
- {changedFields.type ? : null}
-
-
- {DisplayVehicleType()}
-
-
- Vehicle Sub-type
- {changedFields.subtype ? : null}
-
-
- {DisplayVehicleSubType()}
-
- {vehicleWasSaved && (
-
-
-
- This vehicle has been added/updated to your Vehicle Inventory.
+ {permitType !== PERMIT_TYPES.STOS ? (
+
+
+ Unit #
+ {showDiffChip(changedFields.unit)}
+
+
+
+ {vehicleDetails?.unitNumber}
+
+
+
+ VIN{" "}
+
+ (last 6 digits)
+ {showDiffChip(changedFields.vin)}
+
+
+
+ {vehicleDetails?.vin}
+
+
+
+ Plate
+ {showDiffChip(changedFields.plate)}
+
+
+
+ {vehicleDetails?.plate}
- )}
-
+
+
+ Make
+ {showDiffChip(changedFields.make)}
+
+
+
+ {vehicleDetails?.make}
+
+
+
+ Year
+ {showDiffChip(changedFields.year)}
+
+
+
+ {vehicleDetails?.year}
+
+
+
+ Country
+ {showDiffChip(changedFields.country)}
+
+
+
+ {getCountryFullName(vehicleDetails?.countryCode)}
+
+
+ {provinceDisplay ? (
+ <>
+
+ Province / State
+ {showDiffChip(changedFields.province)}
+
+
+
+ {provinceDisplay}
+
+ >
+ ) : null}
+
+
+ Vehicle Type
+ {showDiffChip(changedFields.type)}
+
+
+
+ {vehicleTypeDisplayText(vehicleType)}
+
+
+
+ Vehicle Sub-type
+ {showDiffChip(changedFields.subtype)}
+
+
+
+ {vehicleSubtype}
+
+
+ {vehicleWasSaved ? (
+
+
+
+ This vehicle has been added/updated to your Vehicle Inventory.
+
+
+ ) : null}
+
+ ) : (
+
+ {vehicleDetails ? (
+
+ Power Unit
+
+
+
+ ) : null}
+
+
+ Trailer(s)
+
+
+
+
+ {vehicleWasSaved ? (
+
+
+
+ This vehicle has been added/updated to your Vehicle Inventory.
+
+
+ ) : null}
+
+ )}
);
diff --git a/frontend/src/features/permits/pages/Application/components/review/TripDetails.scss b/frontend/src/features/permits/pages/Application/components/review/TripDetails.scss
new file mode 100644
index 000000000..d7d180982
--- /dev/null
+++ b/frontend/src/features/permits/pages/Application/components/review/TripDetails.scss
@@ -0,0 +1,52 @@
+@use "../../../../../../themes/orbcStyles";
+
+@include orbcStyles.permit-main-box-style(".review-trip-details");
+@include orbcStyles.permit-left-box-style(".review-trip-details__header");
+@include orbcStyles.permit-right-box-style(".review-trip-details__body");
+
+.review-trip-details {
+ &__body {
+ width: 100%;
+
+ .origin-destination {
+ &__label-text {
+ font-weight: bold;
+ }
+
+ &__destination {
+ margin-top: 1.5rem;
+ }
+ }
+
+ .review-highway-sequences {
+ margin-top: 1.5rem;
+ border-top: 1px solid orbcStyles.$bc-text-box-border-grey;
+ padding: 1.5rem 0;
+
+ &__sequences {
+ margin-top: 1.5rem;
+ display: grid;
+ grid-template-columns: repeat(8, [col] 1fr);
+ row-gap: 1rem;
+ }
+ }
+
+ .specific-route {
+ padding-top: 1.5rem;
+ border-top: 1px solid orbcStyles.$bc-text-box-border-grey;
+
+ &__label-text {
+ font-weight: bold;
+ }
+ }
+ }
+}
+
+@media (width < 370px) {
+ .review-trip-details {
+ &__body {
+ max-width: 100%;
+ min-width: 100%;
+ }
+ }
+}
diff --git a/frontend/src/features/permits/pages/Application/components/review/TripDetails.tsx b/frontend/src/features/permits/pages/Application/components/review/TripDetails.tsx
new file mode 100644
index 000000000..4ecf3b879
--- /dev/null
+++ b/frontend/src/features/permits/pages/Application/components/review/TripDetails.tsx
@@ -0,0 +1,146 @@
+import { Box, Typography } from "@mui/material";
+
+import "./TripDetails.scss";
+import { Nullable } from "../../../../../../common/types/common";
+import { DiffChip } from "./DiffChip";
+import { PermittedRoute } from "../../../../types/PermittedRoute";
+import { getDefaultRequiredVal } from "../../../../../../common/helpers/util";
+import { areOrderedSequencesEqual, areValuesDifferent } from "../../../../../../common/helpers/equality";
+
+export const TripDetails = ({
+ routeDetails,
+ oldRouteDetails,
+ showChangedFields = false,
+}: {
+ routeDetails?: Nullable;
+ oldRouteDetails?: Nullable;
+ showChangedFields?: boolean;
+}) => {
+ const origin = getDefaultRequiredVal("", routeDetails?.manualRoute?.origin);
+ const destination = getDefaultRequiredVal("", routeDetails?.manualRoute?.destination);
+ const highwaySequence = getDefaultRequiredVal([], routeDetails?.manualRoute?.highwaySequence);
+ const details = getDefaultRequiredVal("", routeDetails?.routeDetails);
+
+ const changedFields = showChangedFields
+ ? {
+ origin: areValuesDifferent(
+ routeDetails?.manualRoute?.origin,
+ oldRouteDetails?.manualRoute?.origin,
+ ),
+ destination: areValuesDifferent(
+ routeDetails?.manualRoute?.destination,
+ oldRouteDetails?.manualRoute?.destination,
+ ),
+ highwaySequences: areOrderedSequencesEqual(
+ routeDetails?.manualRoute?.highwaySequence,
+ oldRouteDetails?.manualRoute?.highwaySequence,
+ (seqNum, oldSeqNum) => seqNum === oldSeqNum,
+ ),
+ routeDetails: areValuesDifferent(
+ routeDetails?.routeDetails,
+ oldRouteDetails?.routeDetails,
+ ),
+ }
+ : {
+ origin: false,
+ destination: false,
+ highwaySequences: false,
+ routeDetails: false,
+ };
+
+ const showDiffChip = (show: boolean) => {
+ return show ? : null;
+ };
+
+ return routeDetails ? (
+
+
+
+ Trip Details
+
+
+
+
+ {origin || destination ? (
+
+ {origin ? (
+
+
+ Origin
+
+ {showDiffChip(changedFields.origin)}
+
+
+
+ {origin}
+
+
+ ) : null}
+
+ {destination ? (
+
+
+ Destination
+
+ {showDiffChip(changedFields.destination)}
+
+
+
+ {destination}
+
+
+ ) : null}
+
+ ) : null}
+
+ {highwaySequence.length > 0 ? (
+
+
+
+ Sequences of highways to be travelled
+
+
+ {showDiffChip(changedFields.highwaySequences)}
+
+
+
+ {highwaySequence.map((highwaySequence, index) => (
+
+ {highwaySequence}
+
+ ))}
+
+
+ ) : null}
+
+ {details ? (
+
+
+
+ Specific Route Details
+
+
+ {showDiffChip(changedFields.routeDetails)}
+
+
+
+ {details}
+
+
+ ) : null}
+
+
+ ) : null;
+};
diff --git a/frontend/src/features/permits/pages/Application/tests/ApplicationReview.test.tsx b/frontend/src/features/permits/pages/Application/tests/ApplicationReview.test.tsx
index 960f2b9b3..8cf0cd30d 100644
--- a/frontend/src/features/permits/pages/Application/tests/ApplicationReview.test.tsx
+++ b/frontend/src/features/permits/pages/Application/tests/ApplicationReview.test.tsx
@@ -7,16 +7,13 @@ import { VehicleType } from "../../../../manageVehicles/types/Vehicle";
import { getDefaultRequiredVal } from "../../../../../common/helpers/util";
import { calculateFeeByDuration } from "../../../helpers/feeSummary";
import { getPermitTypeName } from "../../../types/PermitType";
+import { getCountryFullName } from "../../../../../common/helpers/countries/getCountryFullName";
+import { getProvinceFullName } from "../../../../../common/helpers/countries/getProvinceFullName";
import {
DATE_FORMATS,
dayjsToLocalStr,
} from "../../../../../common/helpers/formatDate";
-import {
- formatCountry,
- formatProvince,
-} from "../../../../../common/helpers/formatCountryProvince";
-
import {
applicationCreatedDate,
applicationHeaderTitle,
@@ -167,8 +164,10 @@ describe("Review and Confirm Application Details", () => {
// Assert
const { addressLine1, city, countryCode, postalCode, provinceCode } =
companyInfo.mailingAddress;
- const country = formatCountry(countryCode);
- const province = formatProvince(countryCode, provinceCode);
+
+ const country = getCountryFullName(countryCode);
+ const province = getProvinceFullName(countryCode, provinceCode);
+
expect(await companyMailAddrHeaderTitle()).toHaveTextContent(
companyMailAddrTitle,
);
@@ -345,15 +344,17 @@ describe("Review and Confirm Application Details", () => {
vehicleSubType,
} = defaultApplicationData.permitData
.vehicleDetails as PermitVehicleDetails;
+
const unit = getDefaultRequiredVal("", unitNumber);
- const country = formatCountry(countryCode);
- const province = formatProvince(countryCode, provinceCode);
+ const country = getCountryFullName(countryCode);
+ const province = getProvinceFullName(countryCode, provinceCode);
const vehicleTypeStr = vehicleTypeDisplayText(vehicleType as VehicleType);
const vehicleSubtypeStr = getDefaultRequiredVal(
"",
vehicleSubtypes.find((subtype) => subtype.typeCode === vehicleSubType)
?.type,
);
+
expect(await vehicleUnitNumber()).toHaveTextContent(unit);
expect(await vehicleVIN()).toHaveTextContent(vin);
expect(await vehiclePlate()).toHaveTextContent(plate);
diff --git a/frontend/src/features/permits/pages/Application/tests/helpers/ApplicationReview/prepare.tsx b/frontend/src/features/permits/pages/Application/tests/helpers/ApplicationReview/prepare.tsx
index 63f36255d..58930e28c 100644
--- a/frontend/src/features/permits/pages/Application/tests/helpers/ApplicationReview/prepare.tsx
+++ b/frontend/src/features/permits/pages/Application/tests/helpers/ApplicationReview/prepare.tsx
@@ -209,7 +209,7 @@ const ComponentWithWrapper = ({
[testApplicationData],
)}
>
-
+
);
diff --git a/frontend/src/features/permits/pages/ShoppingCart/ShoppingCartPage.tsx b/frontend/src/features/permits/pages/ShoppingCart/ShoppingCartPage.tsx
index 2cc8aac35..dda5e1813 100644
--- a/frontend/src/features/permits/pages/ShoppingCart/ShoppingCartPage.tsx
+++ b/frontend/src/features/permits/pages/ShoppingCart/ShoppingCartPage.tsx
@@ -4,7 +4,6 @@ import { useSearchParams, useNavigate, Navigate } from "react-router-dom";
import { FormProvider, useForm } from "react-hook-form";
import "./ShoppingCartPage.scss";
-import { ApplicationContext } from "../../context/ApplicationContext";
import { isZeroAmount } from "../../helpers/feeSummary";
import { PermitPayFeeSummary } from "../Application/components/pay/PermitPayFeeSummary";
import OnRouteBCContext from "../../../../common/authentication/OnRouteBCContext";
@@ -66,7 +65,6 @@ const AVAILABLE_CV_PAYMENT_METHODS = [PAYMENT_METHOD_TYPE_CODE.WEB];
export const ShoppingCartPage = () => {
const navigate = useNavigate();
- const { applicationData } = useContext(ApplicationContext);
const { idirUserDetails, userDetails } = useContext(OnRouteBCContext);
const companyId: number = applyWhenNotNullable(id => Number(id), getCompanyIdFromSession(), 0);
const isStaffActingAsCompany = Boolean(idirUserDetails?.userRole);
@@ -418,7 +416,6 @@ export const ShoppingCartPage = () => {
;
clientNumber?: Nullable;
loas?: Nullable;
+ permittedCommodity?: Nullable;
+ vehicleConfiguration?: Nullable;
+ permittedRoute?: Nullable;
+ applicationNotes?: Nullable;
}
diff --git a/frontend/src/features/permits/types/PermitVehicleConfiguration.ts b/frontend/src/features/permits/types/PermitVehicleConfiguration.ts
new file mode 100644
index 000000000..79eb3d63c
--- /dev/null
+++ b/frontend/src/features/permits/types/PermitVehicleConfiguration.ts
@@ -0,0 +1,14 @@
+import { Nullable } from "../../../common/types/common";
+
+export interface VehicleInConfiguration {
+ vehicleSubType: string;
+}
+
+export interface PermitVehicleConfiguration {
+ overallLength?: Nullable;
+ overallWidth?: Nullable;
+ overallHeight?: Nullable;
+ frontProjection?: Nullable;
+ rearProjection?: Nullable;
+ trailers?: Nullable;
+};
diff --git a/frontend/src/features/permits/types/PermitVehicleDetails.ts b/frontend/src/features/permits/types/PermitVehicleDetails.ts
index 7b9fc2ddb..083624671 100644
--- a/frontend/src/features/permits/types/PermitVehicleDetails.ts
+++ b/frontend/src/features/permits/types/PermitVehicleDetails.ts
@@ -13,6 +13,7 @@ export interface PermitVehicleDetails {
saveVehicle?: Nullable;
unitNumber?: Nullable;
vehicleId: Nullable; // either powerUnitId or trailerId, depending on vehicleType
+ licensedGVW?: Nullable;
}
export const DEFAULT_VEHICLE_TYPE = VEHICLE_TYPES.POWER_UNIT;
@@ -28,4 +29,5 @@ export const EMPTY_VEHICLE_DETAILS = {
provinceCode: "",
vehicleType: DEFAULT_VEHICLE_TYPE,
vehicleSubType: "",
+ licensedGVW: null,
};
diff --git a/frontend/src/features/permits/types/PermittedCommodity.ts b/frontend/src/features/permits/types/PermittedCommodity.ts
new file mode 100644
index 000000000..13ff9af39
--- /dev/null
+++ b/frontend/src/features/permits/types/PermittedCommodity.ts
@@ -0,0 +1,4 @@
+export interface PermittedCommodity {
+ commodityType: string;
+ loadDescription: string;
+};
diff --git a/frontend/src/features/permits/types/PermittedRoute.ts b/frontend/src/features/permits/types/PermittedRoute.ts
new file mode 100644
index 000000000..2410f951b
--- /dev/null
+++ b/frontend/src/features/permits/types/PermittedRoute.ts
@@ -0,0 +1,14 @@
+import { Nullable } from "../../../common/types/common";
+
+interface ManualRoute {
+ highwaySequence: string[];
+ origin: string;
+ destination: string;
+ exitPoint?: Nullable;
+ totalDistance?: Nullable;
+}
+
+export interface PermittedRoute {
+ manualRoute?: Nullable;
+ routeDetails?: Nullable;
+};
diff --git a/frontend/src/features/policy/apiManager/endpoints/endpoints.ts b/frontend/src/features/policy/apiManager/endpoints/endpoints.ts
new file mode 100644
index 000000000..48c2e776f
--- /dev/null
+++ b/frontend/src/features/policy/apiManager/endpoints/endpoints.ts
@@ -0,0 +1,9 @@
+import { POLICY_URL } from "../../../../common/apiManager/endpoints/endpoints";
+
+const POLICY_CONFIG_API_BASE = `${POLICY_URL}/policy-configurations`;
+
+export const POLICY_CONFIG_API_ROUTES = {
+ GET_ALL: (isCurrent: boolean) => `${POLICY_CONFIG_API_BASE}?isCurrent=${isCurrent}`,
+ GET: (policyConfigId: number | string) => `${POLICY_CONFIG_API_BASE}/${policyConfigId}`,
+ GET_DRAFT: () => `${POLICY_CONFIG_API_BASE}/draft`,
+};
diff --git a/frontend/src/features/policy/apiManager/policy.ts b/frontend/src/features/policy/apiManager/policy.ts
new file mode 100644
index 000000000..d60b27e2e
--- /dev/null
+++ b/frontend/src/features/policy/apiManager/policy.ts
@@ -0,0 +1,21 @@
+import { httpGETRequest } from "../../../common/apiManager/httpRequestHandler";
+import { POLICY_CONFIG_API_ROUTES } from "./endpoints/endpoints";
+import { RequiredOrNull } from "../../../common/types/common";
+import { PolicyConfiguration } from "../types/PolicyConfiguration";
+
+/**
+ * Get the policy configuration.
+ * @param isCurrent Whether or not to get the most current policy configuration
+ * @returns Policy configuration
+ */
+export const getPolicyConfiguration = async (
+ isCurrent: boolean,
+): Promise> => {
+ const response = await httpGETRequest(
+ POLICY_CONFIG_API_ROUTES.GET_ALL(isCurrent),
+ );
+
+ const policyConfigurations = response.data as PolicyConfiguration[];
+ return policyConfigurations.length > 0 ? policyConfigurations[0] : null;
+};
+
diff --git a/frontend/src/features/policy/hooks/usePolicyConfigurationQuery.ts b/frontend/src/features/policy/hooks/usePolicyConfigurationQuery.ts
new file mode 100644
index 000000000..a63f6467d
--- /dev/null
+++ b/frontend/src/features/policy/hooks/usePolicyConfigurationQuery.ts
@@ -0,0 +1,23 @@
+import { useQuery } from "@tanstack/react-query";
+
+import { getPolicyConfiguration } from "../apiManager/policy";
+
+const QUERY_KEYS = {
+ POLICY_CONFIG: () => ["policy-config"],
+};
+
+/**
+ * Hook to fetch the policy configuration.
+ * @returns Query result of the policy configuration
+ */
+export const usePolicyConfigurationQuery = () => {
+ return useQuery({
+ queryKey: QUERY_KEYS.POLICY_CONFIG(),
+ queryFn: () => {
+ return getPolicyConfiguration(true);
+ },
+ retry: false,
+ refetchOnMount: "always",
+ refetchOnWindowFocus: false,
+ });
+};
diff --git a/frontend/src/features/policy/hooks/usePolicyEngine.ts b/frontend/src/features/policy/hooks/usePolicyEngine.ts
new file mode 100644
index 000000000..d5cb9c529
--- /dev/null
+++ b/frontend/src/features/policy/hooks/usePolicyEngine.ts
@@ -0,0 +1,24 @@
+import { useMemo } from "react";
+import { Policy } from "onroute-policy-engine";
+
+import { usePolicyConfigurationQuery } from "./usePolicyConfigurationQuery";
+import { isNull } from "../../../common/types/common";
+
+/**
+ * Hook that instantiates the policy engine instance.
+ * The hook will return undefined when policy configuration is still loading,
+ * and null when there's a problem getting the policy configuration.
+ * @returns The instantiated policy engine, or undefined when loading, and null on error
+ */
+export const usePolicyEngine = () => {
+ const { data: policyConfiguration } = usePolicyConfigurationQuery();
+
+ const policyEngine = useMemo(() => {
+ if (isNull(policyConfiguration)) return null;
+ if (!policyConfiguration) return undefined;
+
+ return new Policy(policyConfiguration.policy);
+ }, [policyConfiguration]);
+
+ return policyEngine;
+};
diff --git a/frontend/src/features/policy/types/PolicyConfiguration.ts b/frontend/src/features/policy/types/PolicyConfiguration.ts
new file mode 100644
index 000000000..cccdb8e87
--- /dev/null
+++ b/frontend/src/features/policy/types/PolicyConfiguration.ts
@@ -0,0 +1,9 @@
+import { PolicyDefinition } from "onroute-policy-engine/types";
+
+export interface PolicyConfiguration {
+ policyConfigId: number;
+ effectiveDate: string;
+ isDraft: boolean;
+ changeDescription: string;
+ policy: PolicyDefinition;
+}
diff --git a/frontend/src/features/queue/apiManager/endpoints/endpoints.ts b/frontend/src/features/queue/apiManager/endpoints/endpoints.ts
index 06be84c57..a12b47868 100644
--- a/frontend/src/features/queue/apiManager/endpoints/endpoints.ts
+++ b/frontend/src/features/queue/apiManager/endpoints/endpoints.ts
@@ -8,4 +8,6 @@ export const APPLICATION_QUEUE_API_ROUTES = {
`${APPLICATIONS_API_BASE(companyId)}/${applicationId}/queue/status`,
CLAIM: (companyId: number, applicationId: string) =>
`${APPLICATIONS_API_BASE(companyId)}/${applicationId}/queue/assign`,
+ SUBMIT_FOR_REVIEW: (companyId: number, applicationId: string) =>
+ `${APPLICATIONS_API_BASE(companyId)}/${applicationId}/queue`,
};
diff --git a/frontend/src/features/queue/apiManager/queueAPI.ts b/frontend/src/features/queue/apiManager/queueAPI.ts
index a45a0c5e0..9bd13bd03 100644
--- a/frontend/src/features/queue/apiManager/queueAPI.ts
+++ b/frontend/src/features/queue/apiManager/queueAPI.ts
@@ -1,17 +1,18 @@
+import { getDefaultRequiredVal } from "../../../common/helpers/util";
+import { getApplications } from "../../permits/apiManager/permitsAPI";
+import { ApplicationListItem } from "../../permits/types/application";
+import { CaseActivityType } from "../types/CaseActivityType";
+import { APPLICATION_QUEUE_API_ROUTES } from "./endpoints/endpoints";
import {
getCompanyIdFromSession,
httpPOSTRequest,
} from "../../../common/apiManager/httpRequestHandler";
-import { getDefaultRequiredVal } from "../../../common/helpers/util";
+
import {
Nullable,
PaginatedResponse,
PaginationAndFilters,
} from "../../../common/types/common";
-import { getApplications } from "../../permits/apiManager/permitsAPI";
-import { ApplicationListItem } from "../../permits/types/application";
-import { CaseActivityType } from "../types/CaseActivityType";
-import { APPLICATION_QUEUE_API_ROUTES } from "./endpoints/endpoints";
/**
* Fetch all applications in queue.
@@ -104,3 +105,13 @@ export const claimApplicationInQueue = async (
);
return response;
};
+
+export const submitApplicationForReview = async (
+ companyId: number,
+ applicationId: string,
+) => {
+ return await httpPOSTRequest(
+ APPLICATION_QUEUE_API_ROUTES.SUBMIT_FOR_REVIEW(companyId, applicationId),
+ {},
+ );
+};
diff --git a/frontend/src/features/queue/components/ApplicationInQueueReview.tsx b/frontend/src/features/queue/components/ApplicationInQueueReview.tsx
index 8ef5e6b0c..e6b2a8dcb 100644
--- a/frontend/src/features/queue/components/ApplicationInQueueReview.tsx
+++ b/frontend/src/features/queue/components/ApplicationInQueueReview.tsx
@@ -1,9 +1,11 @@
import { useEffect, useState } from "react";
import { FormProvider, useForm } from "react-hook-form";
import { useNavigate } from "react-router-dom";
+
+import "./ApplicationInQueueReview.scss";
import { getDefaultRequiredVal } from "../../../common/helpers/util";
import { Nullable } from "../../../common/types/common";
-import { APPLICATION_STEPS, IDIR_ROUTES } from "../../../routes/constants";
+import { APPLICATION_QUEUE_ROUTES, APPLICATION_STEPS, IDIR_ROUTES } from "../../../routes/constants";
import { useCompanyInfoDetailsQuery } from "../../manageProfile/apiManager/hooks";
import { usePowerUnitSubTypesQuery } from "../../manageVehicles/hooks/powerUnits";
import { useTrailerSubTypesQuery } from "../../manageVehicles/hooks/trailers";
@@ -14,10 +16,11 @@ import { PERMIT_REVIEW_CONTEXTS } from "../../permits/types/PermitReviewContext"
import { DEFAULT_PERMIT_TYPE } from "../../permits/types/PermitType";
import { useFetchSpecialAuthorizations } from "../../settings/hooks/specialAuthorizations";
import { CASE_ACTIVITY_TYPES } from "../types/CaseActivityType";
-import "./ApplicationInQueueReview.scss";
import { QueueBreadcrumb } from "./QueueBreadcrumb";
import { RejectApplicationModal } from "./RejectApplicationModal";
import { useUpdateApplicationInQueueStatus } from "../hooks/hooks";
+import { usePolicyEngine } from "../../policy/hooks/usePolicyEngine";
+import { useCommodityOptions } from "../../permits/hooks/useCommodityOptions";
export const ApplicationInQueueReview = ({
applicationData,
@@ -33,15 +36,18 @@ export const ApplicationInQueueReview = ({
const { data: companyInfo } = useCompanyInfoDetailsQuery(companyId);
const doingBusinessAs = companyInfo?.alternateName;
+ const permitType = getDefaultRequiredVal(DEFAULT_PERMIT_TYPE, applicationData?.permitType);
const fee = isNoFeePermitType
? "0"
: `${calculateFeeByDuration(
- getDefaultRequiredVal(DEFAULT_PERMIT_TYPE, applicationData?.permitType),
+ permitType,
getDefaultRequiredVal(0, applicationData?.permitData?.permitDuration),
)}`;
const navigate = useNavigate();
+ const policyEngine = usePolicyEngine();
+ const { commodityOptions } = useCommodityOptions(policyEngine, permitType);
const powerUnitSubTypesQuery = usePowerUnitSubTypesQuery();
const trailerSubTypesQuery = useTrailerSubTypesQuery();
const methods = useForm();
@@ -52,7 +58,7 @@ export const ApplicationInQueueReview = ({
const [hasAttemptedSubmission, setHasAttemptedSubmission] = useState(false);
const handleEdit = () => {
- return;
+ navigate(APPLICATION_QUEUE_ROUTES.EDIT(companyId, applicationId), { replace: true });
};
const isSuccess = (status?: number) => status === 201;
@@ -113,7 +119,7 @@ export const ApplicationInQueueReview = ({
- {showRejectApplicationModal && (
- setShowRejectApplicationModal(false)}
- onConfirm={handleReject}
- isPending={updateApplicationMutationPending}
- />
- )}
- {showRejectApplicationModal && (
+
+ {showRejectApplicationModal ? (
setShowRejectApplicationModal(false)}
onConfirm={handleReject}
isPending={updateApplicationMutationPending}
/>
- )}
+ ) : null}
);
};
diff --git a/frontend/src/features/queue/hooks/hooks.ts b/frontend/src/features/queue/hooks/hooks.ts
index 776ddc585..08c843a5b 100644
--- a/frontend/src/features/queue/hooks/hooks.ts
+++ b/frontend/src/features/queue/hooks/hooks.ts
@@ -1,50 +1,61 @@
+import { useContext } from "react";
+import { useNavigate } from "react-router-dom";
+import { AxiosError } from "axios";
+import { MRT_PaginationState, MRT_SortingState } from "material-react-table";
import {
keepPreviousData,
useMutation,
useQuery,
useQueryClient,
} from "@tanstack/react-query";
-import { AxiosError } from "axios";
-import { MRT_PaginationState, MRT_SortingState } from "material-react-table";
-import { useContext } from "react";
+
+import { canViewApplicationQueue } from "../helpers/canViewApplicationQueue";
+import { CaseActivityType } from "../types/CaseActivityType";
+import { ERROR_ROUTES } from "../../../routes/constants";
import OnRouteBCContext from "../../../common/authentication/OnRouteBCContext";
import { IDIRUserRoleType } from "../../../common/authentication/types";
import { Nullable } from "../../../common/types/common";
import { useTableControls } from "../../permits/hooks/useTableControls";
+import { APPLICATION_QUEUE_STATUSES, ApplicationQueueStatus } from "../types/ApplicationQueueStatus";
import {
claimApplicationInQueue,
getApplicationsInQueue,
getClaimedApplicationsInQueue,
getUnclaimedApplicationsInQueue,
+ submitApplicationForReview,
updateApplicationQueueStatus,
} from "../apiManager/queueAPI";
-import { canViewApplicationQueue } from "../helpers/canViewApplicationQueue";
-import { CaseActivityType } from "../types/CaseActivityType";
-import { useNavigate } from "react-router-dom";
-import { ERROR_ROUTES } from "../../../routes/constants";
const QUEUE_QUERY_KEYS_BASE = "queue";
+/*
+ * The queryKey structure is: ["queue", status?, { pagination, sorting }]
+ *
+ * eg. ["queue"] and ["queue", undefined] refers to all (ApplicationQueueStatus) queue items
+ * (regardless of pagination and sorting)
+ *
+ * eg. ["queue", "IN_REVIEW"] only refers to "IN_REVIEW" queue items
+ */
const QUEUE_QUERY_KEYS = {
- ALL: (pagination: MRT_PaginationState, sorting: MRT_SortingState) => [
- [QUEUE_QUERY_KEYS_BASE, pagination, sorting],
- ],
- CLAIMED: (pagination: MRT_PaginationState, sorting: MRT_SortingState) => [
- [QUEUE_QUERY_KEYS_BASE, pagination, sorting],
- ],
- UNCLAIMED: (pagination: MRT_PaginationState, sorting: MRT_SortingState) => [
- [QUEUE_QUERY_KEYS_BASE, pagination, sorting],
- ],
- DETAIL: (applicationNumber: string) => [
- QUEUE_QUERY_KEYS_BASE,
- { applicationNumber },
- ],
+ ALL_ITEMS: [QUEUE_QUERY_KEYS_BASE] as const,
+ WITH_STATUS: (status?: ApplicationQueueStatus) => [
+ ...QUEUE_QUERY_KEYS.ALL_ITEMS,
+ status,
+ ] as const,
+ WITH_PAGINATION: (
+ pagination: MRT_PaginationState,
+ sorting: MRT_SortingState,
+ status?: ApplicationQueueStatus,
+ ) => [
+ ...QUEUE_QUERY_KEYS.WITH_STATUS(status),
+ { pagination, sorting },
+ ] as const,
};
/**
- * Hook that fetches all applications in queue (PENDING_REVIEW, IN_REVIEW) for staff and manages its pagination state.
+ * Hook that fetches all applications in queue (PENDING_REVIEW and IN_REVIEW) for staff and manages its pagination state.
* This is the data that is consumed by the ApplicationsInReviewList component.
- * @returns All applications in queue(PENDING_REVIEW, IN_REVIEW) along with pagination state and setter
+ * @returns All applications in queue (PENDING_REVIEW and IN_REVIEW) along with pagination state and setter
*/
export const useApplicationsInQueueQuery = () => {
const { idirUserDetails, companyId } = useContext(OnRouteBCContext);
@@ -58,7 +69,7 @@ export const useApplicationsInQueueQuery = () => {
useTableControls();
const applicationsInQueueQuery = useQuery({
- queryKey: QUEUE_QUERY_KEYS.ALL(pagination, sorting),
+ queryKey: QUEUE_QUERY_KEYS.WITH_PAGINATION(pagination, sorting),
queryFn: () =>
getApplicationsInQueue(
{
@@ -93,7 +104,11 @@ export const useClaimedApplicationsInQueueQuery = () => {
useTableControls({ pageSize: 25 });
const claimedApplicationsInQueueQuery = useQuery({
- queryKey: QUEUE_QUERY_KEYS.CLAIMED(pagination, sorting),
+ queryKey: QUEUE_QUERY_KEYS.WITH_PAGINATION(
+ pagination,
+ sorting,
+ APPLICATION_QUEUE_STATUSES.IN_REVIEW,
+ ),
queryFn: () =>
getClaimedApplicationsInQueue({
page: pagination.pageIndex,
@@ -125,7 +140,11 @@ export const useUnclaimedApplicationsInQueueQuery = () => {
useTableControls({ pageSize: 25 });
const unclaimedApplicationsInQueueQuery = useQuery({
- queryKey: QUEUE_QUERY_KEYS.UNCLAIMED(pagination, sorting),
+ queryKey: QUEUE_QUERY_KEYS.WITH_PAGINATION(
+ pagination,
+ sorting,
+ APPLICATION_QUEUE_STATUSES.PENDING_REVIEW,
+ ),
queryFn: () =>
getUnclaimedApplicationsInQueue({
page: pagination.pageIndex,
@@ -188,13 +207,40 @@ export const useUpdateApplicationInQueueStatus = () => {
});
};
+/**
+ * Hook for submitting an application for review by adding it to the queue.
+ * @returns Mutation object that allows the application to be submitted for review
+ */
+export const useSubmitApplicationForReview = () => {
+ const { invalidate } = useInvalidateApplicationsInQueue();
+
+ return useMutation({
+ mutationFn: async (data: {
+ companyId: number;
+ applicationId: string;
+ }) => {
+ return submitApplicationForReview(
+ data.companyId,
+ data.applicationId,
+ );
+ },
+ onSuccess: () => {
+ invalidate();
+ },
+ });
+};
+
+/**
+ * Hook that allows all queue query keys to be invalidated.
+ * @returns Method that invalidates the query keys
+ */
export const useInvalidateApplicationsInQueue = () => {
const queryClient = useQueryClient();
return {
invalidate: () => {
queryClient.invalidateQueries({
- queryKey: [QUEUE_QUERY_KEYS_BASE],
+ queryKey: QUEUE_QUERY_KEYS.ALL_ITEMS,
});
},
};
diff --git a/frontend/src/features/queue/pages/ReviewApplicationInQueue.tsx b/frontend/src/features/queue/pages/ReviewApplicationInQueue.tsx
index ec18dc1d0..b09ada4c1 100644
--- a/frontend/src/features/queue/pages/ReviewApplicationInQueue.tsx
+++ b/frontend/src/features/queue/pages/ReviewApplicationInQueue.tsx
@@ -9,7 +9,7 @@ import {
getDefaultRequiredVal,
} from "../../../common/helpers/util";
import { ERROR_ROUTES } from "../../../routes/constants";
-import { deserializeApplicationResponse } from "../../permits/helpers/deserializeApplication";
+import { deserializeApplicationResponse } from "../../permits/helpers/serialize/deserializeApplication";
import { UniversalUnexpected } from "../../../common/pages/UniversalUnexpected";
export const ReviewApplicationInQueue = () => {
diff --git a/frontend/src/features/wizard/subcomponents/UserInformationWizardForm.tsx b/frontend/src/features/wizard/subcomponents/UserInformationWizardForm.tsx
index 143cc48bf..daf61e73e 100644
--- a/frontend/src/features/wizard/subcomponents/UserInformationWizardForm.tsx
+++ b/frontend/src/features/wizard/subcomponents/UserInformationWizardForm.tsx
@@ -98,7 +98,7 @@ export const UserInformationWizardForm = memo(() => {
className="user-info-wizard-form__input user-info-wizard-form__input--left"
/>
{
className="user-info-wizard-form__input user-info-wizard-form__input--left"
/>
{
return (
@@ -130,15 +130,23 @@ export const AppRoutes = () => {
}
+ element={
+
+ }
/>
- {
- // TODO: placeholder route for edit step
- /* }
- /> */
- }
+ element={
+
+ }
+ />
{
element={
}
/>
@@ -348,6 +357,7 @@ export const AppRoutes = () => {
element={
}
/>
@@ -356,6 +366,7 @@ export const AppRoutes = () => {
element={
}
/>
diff --git a/frontend/src/routes/constants.ts b/frontend/src/routes/constants.ts
index 08e9dec68..69b5d7e87 100644
--- a/frontend/src/routes/constants.ts
+++ b/frontend/src/routes/constants.ts
@@ -112,6 +112,14 @@ export const APPLICATION_STEPS = {
export type ApplicationStep =
(typeof APPLICATION_STEPS)[keyof typeof APPLICATION_STEPS];
+export const APPLICATION_STEP_CONTEXTS = {
+ APPLY: 0,
+ QUEUE: 1,
+} as const;
+
+export type ApplicationStepContext =
+ (typeof APPLICATION_STEP_CONTEXTS)[keyof typeof APPLICATION_STEP_CONTEXTS];
+
export const NEW_APPLICATION_SEGMENT = "new";
export const APPLICATIONS_ROUTES = {
BASE: APPLICATIONS_ROUTE_BASE,
@@ -219,4 +227,8 @@ export const ONROUTE_WEBPAGE_LINKS = {
CONTACT_US: "https://onroutebc.gov.bc.ca",
SERVICE_BC_OFFICE_LOCATIONS:
"https://www2.gov.bc.ca/gov/content/governments/organizational-structure/ministries-organizations/ministries/citizens-services/servicebc#locations",
+ LIST_OF_BC_HIGHWAYS:
+ "https://www2.gov.bc.ca/gov/content/transportation/transportation-reports-and-reference/reference-information/numbered-routes",
+ HEIGHT_CLEARANCE_TOOL:
+ "https://www.drivebc.ca/cvrp/?c=hct",
};
diff --git a/frontend/src/setupTests.ts b/frontend/src/setupTests.ts
index 22a8fb4c8..99c8ffd29 100644
--- a/frontend/src/setupTests.ts
+++ b/frontend/src/setupTests.ts
@@ -9,5 +9,6 @@ import "vitest-canvas-mock";
return {
VITE_DEPLOY_ENVIRONMENT: "docker",
VITE_API_VEHICLE_URL: "http://localhost:5000",
+ VITE_POLICY_URL: "http://localhost:5002",
};
})();
diff --git a/frontend/src/themes/orbcStyles.scss b/frontend/src/themes/orbcStyles.scss
index fe51cdc8a..82e57dce2 100644
--- a/frontend/src/themes/orbcStyles.scss
+++ b/frontend/src/themes/orbcStyles.scss
@@ -93,26 +93,35 @@ $bc-shadow: 0 0 0.5rem #00000029;
display: flex;
flex-wrap: wrap;
background-color: $white;
- border-bottom: 1px solid $bc-text-box-border-grey;
- padding-bottom: 1.5rem;
+ border-top: 1px solid $bc-text-box-border-grey;
+ padding-top: 2.5rem;
+ padding-bottom: 2.5rem;
+
+ h3 {
+ color: $bc-black;
+ font-size: 1.5rem;
+ padding: 0;
+ margin: 0 0 0.5rem 0;
+ }
+
+ h4 {
+ color: $bc-black;
+ font-size: 1.25rem;
+ padding: 0;
+ margin: 0 0 0.5rem 0;
+ }
}
}
@mixin permit-left-box-style($permit-left-box) {
#{$permit-left-box} {
- padding-top: 1.5rem;
min-width: $permit-left-column-width;
max-width: $permit-left-column-width;
-
- h3 {
- font-size: 1.5rem;
- }
}
}
@mixin permit-right-box-style($permit-right-box) {
#{$permit-right-box} {
- padding-top: 1.5rem;
min-width: 600px;
max-width: calc(100% - $permit-left-column-width);
}
From d1d68429ab6bab037ecb6d02831d0d8e4b27753d Mon Sep 17 00:00:00 2001
From: Krishnan Subramanian <84348052+krishnan-aot@users.noreply.github.com>
Date: Fri, 29 Nov 2024 12:54:00 -0800
Subject: [PATCH 2/3] ORV2-3069 Special Authorization Feature flag split
(#1677)
---
.../dbo.ORBC_FEATURE_FLAG.Table.sql | 46 ++++++
.../common/authentication/PermissionMatrix.ts | 8 +-
.../dashboard/ManageSettingsDashboard.tsx | 20 ++-
.../settings/hooks/specialAuthorizations.ts | 3 +-
.../SpecialAuthorizations.tsx | 134 ++++++++++--------
.../special-auth/special-auth.controller.ts | 3 +-
6 files changed, 136 insertions(+), 78 deletions(-)
diff --git a/database/mssql/scripts/sampledata/dbo.ORBC_FEATURE_FLAG.Table.sql b/database/mssql/scripts/sampledata/dbo.ORBC_FEATURE_FLAG.Table.sql
index df88332e4..f37300398 100644
--- a/database/mssql/scripts/sampledata/dbo.ORBC_FEATURE_FLAG.Table.sql
+++ b/database/mssql/scripts/sampledata/dbo.ORBC_FEATURE_FLAG.Table.sql
@@ -211,6 +211,52 @@ VALUES
GETUTCDATE()
);
+INSERT INTO
+ [dbo].[ORBC_FEATURE_FLAG] (
+ [FEATURE_ID],
+ [FEATURE_KEY],
+ [FEATURE_VALUE],
+ [CONCURRENCY_CONTROL_NUMBER],
+ [DB_CREATE_USERID],
+ [DB_CREATE_TIMESTAMP],
+ [DB_LAST_UPDATE_USERID],
+ [DB_LAST_UPDATE_TIMESTAMP]
+ )
+VALUES
+ (
+ '10',
+ 'LCV',
+ 'ENABLED',
+ NULL,
+ N'dbo',
+ GETUTCDATE(),
+ N'dbo',
+ GETUTCDATE()
+ );
+
+INSERT INTO
+ [dbo].[ORBC_FEATURE_FLAG] (
+ [FEATURE_ID],
+ [FEATURE_KEY],
+ [FEATURE_VALUE],
+ [CONCURRENCY_CONTROL_NUMBER],
+ [DB_CREATE_USERID],
+ [DB_CREATE_TIMESTAMP],
+ [DB_LAST_UPDATE_USERID],
+ [DB_LAST_UPDATE_TIMESTAMP]
+ )
+VALUES
+ (
+ '11',
+ 'NO-FEE',
+ 'ENABLED',
+ NULL,
+ N'dbo',
+ GETUTCDATE(),
+ N'dbo',
+ GETUTCDATE()
+ );
+
SET
IDENTITY_INSERT [dbo].[ORBC_FEATURE_FLAG] OFF
GO
\ No newline at end of file
diff --git a/frontend/src/common/authentication/PermissionMatrix.ts b/frontend/src/common/authentication/PermissionMatrix.ts
index 093895385..55f29b652 100644
--- a/frontend/src/common/authentication/PermissionMatrix.ts
+++ b/frontend/src/common/authentication/PermissionMatrix.ts
@@ -237,10 +237,10 @@ const MANAGE_SETTINGS = {
* Special Authorizations Tab
*/
VIEW_SPECIAL_AUTHORIZATIONS: { allowedIDIRRoles: ALL_IDIR_ROLES },
- ADD_NO_FEE_FLAG: { allowedIDIRRoles: [SA, FIN, HQA] },
- UPDATE_NO_FEE_FLAG: { allowedIDIRRoles: [SA, FIN, HQA] },
- ADD_LCV_FLAG: { allowedIDIRRoles: [HQA] },
- REMOVE_LCV_FLAG: { allowedIDIRRoles: [HQA] },
+ ADD_NO_FEE_FLAG: { allowedIDIRRoles: [SA, HQA] },
+ UPDATE_NO_FEE_FLAG: { allowedIDIRRoles: [SA, HQA] },
+ ADD_LCV_FLAG: { allowedIDIRRoles: [SA, HQA] },
+ REMOVE_LCV_FLAG: { allowedIDIRRoles: [SA, HQA] },
ADD_AN_LOA: { allowedIDIRRoles: [SA, HQA] },
EDIT_AN_LOA: { allowedIDIRRoles: [SA, HQA] },
VIEW_LOA: { allowedIDIRRoles: ALL_IDIR_ROLES },
diff --git a/frontend/src/features/settings/components/dashboard/ManageSettingsDashboard.tsx b/frontend/src/features/settings/components/dashboard/ManageSettingsDashboard.tsx
index 6612c3e58..40cb94dcb 100644
--- a/frontend/src/features/settings/components/dashboard/ManageSettingsDashboard.tsx
+++ b/frontend/src/features/settings/components/dashboard/ManageSettingsDashboard.tsx
@@ -6,11 +6,7 @@ import { SETTINGS_TABS, SettingsTab } from "../../types/tabs";
import OnRouteBCContext from "../../../../common/authentication/OnRouteBCContext";
import { ERROR_ROUTES } from "../../../../routes/constants";
import { SpecialAuthorizations } from "../../pages/SpecialAuthorizations/SpecialAuthorizations";
-import { useFeatureFlagsQuery } from "../../../../common/hooks/hooks";
-import {
- canViewSpecialAuthorizations,
- canViewSuspend,
-} from "../../helpers/permissions";
+import { canViewSuspend } from "../../helpers/permissions";
import { CreditAccountMetadataComponent } from "../../pages/CreditAccountMetadataComponent";
import { usePermissionMatrix } from "../../../../common/authentication/PermissionMatrix";
import { useGetCreditAccountMetadataQuery } from "../../hooks/creditAccount";
@@ -20,8 +16,6 @@ import { CREDIT_ACCOUNT_USER_TYPE } from "../../types/creditAccount";
export const ManageSettingsDashboard = React.memo(() => {
const { userClaims, companyId, idirUserDetails } =
useContext(OnRouteBCContext);
-
- const { data: featureFlags } = useFeatureFlagsQuery();
const { data: creditAccountMetadata, isPending } =
useGetCreditAccountMetadataQuery(companyId as number);
@@ -41,15 +35,17 @@ export const ManageSettingsDashboard = React.memo(() => {
}
};
- const isStaffActingAsCompany = Boolean(idirUserDetails?.userRole);
const isFinanceUser = idirUserDetails?.userRole === IDIR_USER_ROLE.FINANCE;
const [hideSuspendTab, setHideSuspendTab] = useState(false);
const showSuspendTab = canViewSuspend(userClaims) && !hideSuspendTab;
- const showSpecialAuth =
- isStaffActingAsCompany &&
- canViewSpecialAuthorizations(userClaims, idirUserDetails?.userRole) &&
- featureFlags?.["LOA"] === "ENABLED";
+
+ const showSpecialAuth = usePermissionMatrix({
+ permissionMatrixKeys: {
+ permissionMatrixFeatureKey: "MANAGE_SETTINGS",
+ permissionMatrixFunctionKey: "VIEW_SPECIAL_AUTHORIZATIONS",
+ },
+ });
const showCreditAccountTab = usePermissionMatrix({
featureFlag: "CREDIT-ACCOUNT",
diff --git a/frontend/src/features/settings/hooks/specialAuthorizations.ts b/frontend/src/features/settings/hooks/specialAuthorizations.ts
index 98a8fe207..404486ce6 100644
--- a/frontend/src/features/settings/hooks/specialAuthorizations.ts
+++ b/frontend/src/features/settings/hooks/specialAuthorizations.ts
@@ -13,6 +13,7 @@ const QUERY_KEYS = {
*/
export const useFetchSpecialAuthorizations = (
companyId: number | string,
+ enabled: boolean = true,
) => {
return useQuery({
queryKey: QUERY_KEYS.SPECIAL_AUTH(companyId),
@@ -22,7 +23,7 @@ export const useFetchSpecialAuthorizations = (
retry: false,
refetchOnMount: "always",
refetchOnWindowFocus: false,
- enabled: Boolean(companyId),
+ enabled,
});
};
diff --git a/frontend/src/features/settings/pages/SpecialAuthorizations/SpecialAuthorizations.tsx b/frontend/src/features/settings/pages/SpecialAuthorizations/SpecialAuthorizations.tsx
index 042ba3f2c..959e2d811 100644
--- a/frontend/src/features/settings/pages/SpecialAuthorizations/SpecialAuthorizations.tsx
+++ b/frontend/src/features/settings/pages/SpecialAuthorizations/SpecialAuthorizations.tsx
@@ -1,4 +1,4 @@
-import { useContext, useState } from "react";
+import { useState } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { Button } from "@mui/material";
@@ -11,10 +11,12 @@ import { ExpiredLOAModal } from "../../components/SpecialAuthorizations/LOA/expi
import { DeleteConfirmationDialog } from "../../../../common/components/dialog/DeleteConfirmationDialog";
import { LOASteps } from "./LOA/LOASteps";
import { useFetchLOAs, useRemoveLOAMutation } from "../../hooks/LOA";
-import { getDefaultNullableVal, getDefaultRequiredVal } from "../../../../common/helpers/util";
-import { DEFAULT_NO_FEE_PERMIT_TYPE, NoFeePermitType } from "../../types/SpecialAuthorization";
+import { getDefaultRequiredVal } from "../../../../common/helpers/util";
+import {
+ DEFAULT_NO_FEE_PERMIT_TYPE,
+ NoFeePermitType,
+} from "../../types/SpecialAuthorization";
import { NoFeePermitsSection } from "../../components/SpecialAuthorizations/NoFeePermits/NoFeePermitsSection";
-import OnRouteBCContext from "../../../../common/authentication/OnRouteBCContext";
import { LCVSection } from "../../components/SpecialAuthorizations/LCV/LCVSection";
import { downloadLOA } from "../../apiManager/loa";
import {
@@ -22,69 +24,81 @@ import {
useUpdateLCV,
useUpdateNoFee,
} from "../../hooks/specialAuthorizations";
+import { usePermissionMatrix } from "../../../../common/authentication/PermissionMatrix";
+import { useFeatureFlagsQuery } from "../../../../common/hooks/hooks";
-import {
- canUpdateLCVFlag,
- canUpdateLOA,
- canUpdateNoFeePermitsFlag,
- canViewLCVFlag,
- canViewLOA,
- canViewNoFeePermitsFlag,
-} from "../../helpers/permissions";
-
-export const SpecialAuthorizations = ({
- companyId,
-}: {
- companyId: number;
-}) => {
- const {
- data: specialAuthorizations,
- refetch: refetchSpecialAuth,
- } = useFetchSpecialAuthorizations(companyId);
-
- const noFeeType = getDefaultRequiredVal(null, specialAuthorizations?.noFeeType);
- const isLcvAllowed = getDefaultRequiredVal(false, specialAuthorizations?.isLcvAllowed);
+export const SpecialAuthorizations = ({ companyId }: { companyId: number }) => {
+ const { data: featureFlags } = useFeatureFlagsQuery();
+ const { data: specialAuthorizations, refetch: refetchSpecialAuth } =
+ useFetchSpecialAuthorizations(
+ companyId,
+ // At least one of the special auth feature flags must be enabled
+ // to decide whether to enable the query.
+ featureFlags?.["NO-FEE"] === "ENABLED" ||
+ featureFlags?.["LCV"] === "ENABLED",
+ );
+
+ const noFeeType = getDefaultRequiredVal(
+ null,
+ specialAuthorizations?.noFeeType,
+ );
+ const isLcvAllowed = getDefaultRequiredVal(
+ false,
+ specialAuthorizations?.isLcvAllowed,
+ );
const [showExpiredLOAs, setShowExpiredLOAs] = useState(false);
const [loaToDelete, setLoaToDelete] = useState>(null);
const [showLOASteps, setShowLOASteps] = useState(false);
const [loaToEdit, setLoaToEdit] = useState>(null);
- const {
- userClaims,
- idirUserDetails,
- userDetails,
- } = useContext(OnRouteBCContext);
-
- const canEditNoFeePermits = canUpdateNoFeePermitsFlag(
- userClaims,
- getDefaultNullableVal(idirUserDetails?.userRole, userDetails?.userRole),
- );
-
- const canViewNoFeePermits = canViewNoFeePermitsFlag(
- userClaims,
- getDefaultNullableVal(idirUserDetails?.userRole, userDetails?.userRole),
- );
-
- const canUpdateLCV = canUpdateLCVFlag(
- userClaims,
- getDefaultNullableVal(idirUserDetails?.userRole, userDetails?.userRole),
- );
-
- const canViewLCV = canViewLCVFlag(
- userClaims,
- getDefaultNullableVal(idirUserDetails?.userRole, userDetails?.userRole),
- );
-
- const canWriteLOA = canUpdateLOA(
- userClaims,
- getDefaultNullableVal(idirUserDetails?.userRole, userDetails?.userRole),
- );
-
- const canReadLOA = canViewLOA(
- userClaims,
- getDefaultNullableVal(idirUserDetails?.userRole, userDetails?.userRole),
- );
+ const canEditNoFeePermits = usePermissionMatrix({
+ featureFlag: "NO-FEE",
+ permissionMatrixKeys: {
+ permissionMatrixFeatureKey: "MANAGE_SETTINGS",
+ permissionMatrixFunctionKey: "UPDATE_NO_FEE_FLAG",
+ },
+ });
+
+ const canViewNoFeePermits = usePermissionMatrix({
+ featureFlag: "NO-FEE",
+ permissionMatrixKeys: {
+ permissionMatrixFeatureKey: "MANAGE_SETTINGS",
+ permissionMatrixFunctionKey: "VIEW_SPECIAL_AUTHORIZATIONS",
+ },
+ });
+
+ const canUpdateLCV = usePermissionMatrix({
+ featureFlag: "LCV",
+ permissionMatrixKeys: {
+ permissionMatrixFeatureKey: "MANAGE_SETTINGS",
+ permissionMatrixFunctionKey: "REMOVE_LCV_FLAG",
+ },
+ });
+
+ const canViewLCV = usePermissionMatrix({
+ featureFlag: "LCV",
+ permissionMatrixKeys: {
+ permissionMatrixFeatureKey: "MANAGE_SETTINGS",
+ permissionMatrixFunctionKey: "VIEW_SPECIAL_AUTHORIZATIONS",
+ },
+ });
+
+ const canReadLOA = usePermissionMatrix({
+ featureFlag: "LOA",
+ permissionMatrixKeys: {
+ permissionMatrixFeatureKey: "MANAGE_SETTINGS",
+ permissionMatrixFunctionKey: "VIEW_LOA",
+ },
+ });
+
+ const canWriteLOA = usePermissionMatrix({
+ featureFlag: "LOA",
+ permissionMatrixKeys: {
+ permissionMatrixFeatureKey: "MANAGE_SETTINGS",
+ permissionMatrixFunctionKey: "EDIT_AN_LOA",
+ },
+ });
const updateNoFeeMutation = useUpdateNoFee();
const updateLCVMutation = useUpdateLCV();
diff --git a/vehicles/src/modules/special-auth/special-auth.controller.ts b/vehicles/src/modules/special-auth/special-auth.controller.ts
index 9bd7e4b23..f4197ebc9 100644
--- a/vehicles/src/modules/special-auth/special-auth.controller.ts
+++ b/vehicles/src/modules/special-auth/special-auth.controller.ts
@@ -27,7 +27,6 @@ import {
@ApiBearerAuth()
@ApiTags('Special Authorization')
-@IsFeatureFlagEnabled('LOA')
@Controller('companies/:companyId/special-auths')
@ApiMethodNotAllowedResponse({
description: 'The Special Authorizaion Api Method Not Allowed Response',
@@ -79,6 +78,7 @@ export class SpecialAuthController {
],
})
@Put('/lcv')
+ @IsFeatureFlagEnabled('LCV')
async updateLcv(
@Req() request: Request,
@Param() { companyId }: CompanyIdPathParamDto,
@@ -107,6 +107,7 @@ export class SpecialAuthController {
IDIRUserRole.SYSTEM_ADMINISTRATOR,
],
})
+ @IsFeatureFlagEnabled('NO-FEE')
@Put('/no-fee')
async updateNoFee(
@Req() request: Request,
From 04f0d502f48484f0c8a1ee1a8625de35ad73b8a7 Mon Sep 17 00:00:00 2001
From: John Fletcher <113134542+john-fletcher-aot@users.noreply.github.com>
Date: Fri, 29 Nov 2024 14:50:09 -0800
Subject: [PATCH 3/3] feat: Add indexes for all foreign keys and permit
search/sort (#1678)
---
.../versions/revert/v_50_ddl_revert.sql | 131 ++++++++++++++++++
database/mssql/scripts/versions/v_50_ddl.sql | 130 +++++++++++++++++
2 files changed, 261 insertions(+)
create mode 100644 database/mssql/scripts/versions/revert/v_50_ddl_revert.sql
create mode 100644 database/mssql/scripts/versions/v_50_ddl.sql
diff --git a/database/mssql/scripts/versions/revert/v_50_ddl_revert.sql b/database/mssql/scripts/versions/revert/v_50_ddl_revert.sql
new file mode 100644
index 000000000..5ad103cfd
--- /dev/null
+++ b/database/mssql/scripts/versions/revert/v_50_ddl_revert.sql
@@ -0,0 +1,131 @@
+SET ANSI_NULLS ON
+GO
+SET QUOTED_IDENTIFIER ON
+GO
+SET NOCOUNT ON
+GO
+
+SET XACT_ABORT ON
+GO
+SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
+GO
+BEGIN TRANSACTION
+GO
+
+-- Foreign key indexes on all tables
+DROP INDEX [tps].[ETL_PROCESSES].[IX_FK_ETL_PROCESSES_ETL_PROCESS_TYPE];
+DROP INDEX [dbo].[ORBC_ADDRESS].[IX_FK_ORBC_ADDRESS_PROVINCE];
+DROP INDEX [case].[ORBC_CASE_ACTIVITY].[IX_FK_ORBC_CASE_ACTIVITY_CASE_ACTIVITY_TYPE];
+DROP INDEX [case].[ORBC_CASE_ACTIVITY].[IX_FK_ORBC_CASE_ACTIVITY_CASE_EVENT_ID];
+DROP INDEX [case].[ORBC_CASE_ACTIVITY].[IX_FK_ORBC_CASE_ACTIVITY_CASE_ID];
+DROP INDEX [case].[ORBC_CASE].[IX_FK_ORBC_CASE_CASE_STATUS_TYPE];
+DROP INDEX [case].[ORBC_CASE].[IX_FK_ORBC_CASE_CASE_TYPE];
+DROP INDEX [case].[ORBC_CASE_DOCUMENT].[IX_FK_ORBC_CASE_DOCUMENT_CASE_EVENT_ID];
+DROP INDEX [case].[ORBC_CASE_DOCUMENT].[IX_FK_ORBC_CASE_DOCUMENT_CASE_ID];
+DROP INDEX [case].[ORBC_CASE_EVENT].[IX_FK_ORBC_CASE_EVENT_CASE_EVENT_TYPE];
+DROP INDEX [case].[ORBC_CASE_NOTES].[IX_FK_ORBC_CASE_NOTES_CASE_EVENT_ID];
+DROP INDEX [case].[ORBC_CASE_NOTES].[IX_FK_ORBC_CASE_NOTES_CASE_ID];
+DROP INDEX [case].[ORBC_CASE].[IX_FK_ORBC_CASE_ORIGINAL_CASE_ID];
+DROP INDEX [case].[ORBC_CASE].[IX_FK_ORBC_CASE_PERMIT_ID];
+DROP INDEX [case].[ORBC_CASE].[IX_FK_ORBC_CASE_PREVIOUS_CASE_ID];
+DROP INDEX [dbo].[ORBC_COMPANY].[IX_FK_ORBC_COMPANY_DIRECTORY];
+DROP INDEX [dbo].[ORBC_COMPANY].[IX_FK_ORBC_COMPANY_MAILING_ADDRESS];
+DROP INDEX [dbo].[ORBC_COMPANY].[IX_FK_ORBC_COMPANY_PRIMARY_CONTACT];
+DROP INDEX [dbo].[ORBC_COMPANY_SUSPEND_ACTIVITY].[IX_FK_ORBC_COMPANY_SUSPEND_ACTIVITY_ORBC_COMPANY];
+DROP INDEX [dbo].[ORBC_COMPANY_SUSPEND_ACTIVITY].[IX_FK_ORBC_COMPANY_SUSPEND_ACTIVITY_SUSPEND_ACTIVITY_TYPE];
+DROP INDEX [dbo].[ORBC_COMPANY_USER].[IX_FK_ORBC_COMPANY_USER_COMPANY];
+DROP INDEX [dbo].[ORBC_COMPANY_USER].[IX_FK_ORBC_COMPANY_USER_USER];
+DROP INDEX [dbo].[ORBC_COMPANY_USER].[IX_FK_ORBC_COMPANY_USER_USER_AUTH_GROUP];
+DROP INDEX [dbo].[ORBC_COMPANY_USER].[IX_FK_ORBC_COMPANY_USER_USER_STATUS];
+DROP INDEX [dbo].[ORBC_CONTACT].[IX_FK_ORBC_CONTACT_PROVINCE];
+DROP INDEX [permit].[ORBC_CREDIT_ACCOUNT_ACTIVITY].[IX_FK_ORBC_CREDIT_ACCOUNT_ACTIVITY_CREDIT_ACCOUNT_ACTIVITY_TYPE];
+DROP INDEX [permit].[ORBC_CREDIT_ACCOUNT_ACTIVITY].[IX_FK_ORBC_CREDIT_ACCOUNT_ACTIVITY_CREDIT_ACCOUNT_ID];
+DROP INDEX [permit].[ORBC_CREDIT_ACCOUNT].[IX_FK_ORBC_CREDIT_ACCOUNT_COMPANY_ID];
+DROP INDEX [permit].[ORBC_CREDIT_ACCOUNT].[IX_FK_ORBC_CREDIT_ACCOUNT_CREDIT_ACCOUNT_STATUS_TYPE];
+DROP INDEX [permit].[ORBC_CREDIT_ACCOUNT].[IX_FK_ORBC_CREDIT_ACCOUNT_CREDIT_ACCOUNT_TYPE];
+DROP INDEX [permit].[ORBC_CREDIT_ACCOUNT_USER].[IX_FK_ORBC_CREDIT_ACCOUNT_USER_COMPANY_ID];
+DROP INDEX [permit].[ORBC_CREDIT_ACCOUNT_USER].[IX_FK_ORBC_CREDIT_ACCOUNT_USER_CREDIT_ACCOUNT_ID];
+DROP INDEX [permit].[ORBC_GL_CODE_TYPE].[IX_FK_ORBC_GL_CODE_TYPE_GL_TYPE];
+DROP INDEX [permit].[ORBC_GL_CODE_TYPE].[IX_FK_ORBC_GL_CODE_TYPE_PAYMENT_CARD_TYPE];
+DROP INDEX [permit].[ORBC_GL_CODE_TYPE].[IX_FK_ORBC_GL_CODE_TYPE_PAYMENT_METHOD_TYPE];
+DROP INDEX [permit].[ORBC_GL_CODE_TYPE].[IX_FK_ORBC_GL_CODE_TYPE_PERMIT_TYPE];
+DROP INDEX [access].[ORBC_GROUP_ROLE].[IX_FK_ORBC_GROUP_ROLE_ROLE];
+DROP INDEX [access].[ORBC_GROUP_ROLE].[IX_FK_ORBC_GROUP_ROLE_USER_AUTH_GROUP];
+DROP INDEX [permit].[ORBC_LOA_DETAILS].[IX_FK_ORBC_LOA_DETAILS_COMPANY];
+DROP INDEX [permit].[ORBC_LOA_DETAILS].[IX_FK_ORBC_LOA_DETAILS_ORIGINAL_LOA_ID];
+DROP INDEX [permit].[ORBC_LOA_DETAILS].[IX_FK_ORBC_LOA_DETAILS_PREVIOUS_LOA_ID];
+DROP INDEX [permit].[ORBC_LOA_PERMIT_TYPE_DETAILS].[IX_FK_ORBC_LOA_PERMIT_TYPE_LOA_ID];
+DROP INDEX [permit].[ORBC_LOA_PERMIT_TYPE_DETAILS].[IX_FK_ORBC_LOA_PERMIT_TYPES_PERMIT_TYPE];
+DROP INDEX [permit].[ORBC_LOA_VEHICLES].[IX_FK_ORBC_LOA_VEHICLES_LOA_ID];
+DROP INDEX [dbo].[ORBC_PENDING_IDIR_USER].[IX_FK_ORBC_PENDING_IDIR_USER_AUTH_GROUP];
+DROP INDEX [dbo].[ORBC_PENDING_USER].[IX_FK_ORBC_PENDING_USER_AUTH_GROUP];
+DROP INDEX [dbo].[ORBC_PENDING_USER].[IX_FK_ORBC_PENDING_USER_COMPANY];
+DROP INDEX [permit].[ORBC_PERMIT_COMMENTS].[IX_FK_ORBC_PERMIT_COMMENTS_PERMIT];
+DROP INDEX [permit].[ORBC_PERMIT_DATA].[IX_FK_ORBC_PERMIT_ID];
+DROP INDEX [permit].[ORBC_PERMIT].[IX_FK_ORBC_PERMIT_PARENT_PERMIT];
+DROP INDEX [permit].[ORBC_PERMIT].[IX_FK_ORBC_PERMIT_PERMIT_APPLICATION_ORIGIN];
+DROP INDEX [permit].[ORBC_PERMIT].[IX_FK_ORBC_PERMIT_PERMIT_APPROVAL_SOURCE];
+DROP INDEX [permit].[ORBC_PERMIT].[IX_FK_ORBC_PERMIT_PERMIT_ISSUED_BY];
+DROP INDEX [permit].[ORBC_PERMIT].[IX_FK_ORBC_PERMIT_PERMIT_STATUS_TYPE];
+DROP INDEX [permit].[ORBC_PERMIT].[IX_FK_ORBC_PERMIT_PERMIT_TYPE];
+DROP INDEX [permit].[ORBC_PERMIT_STATE].[IX_FK_ORBC_PERMIT_STATE_PERMIT];
+DROP INDEX [permit].[ORBC_PERMIT_STATE].[IX_FK_ORBC_PERMIT_STATE_PERMIT_STATUS];
+DROP INDEX [dbo].[ORBC_POWER_UNIT].[IX_FK_ORBC_POWER_UNIT_COMPANY];
+DROP INDEX [dbo].[ORBC_POWER_UNIT].[IX_FK_ORBC_POWER_UNIT_POWER_UNIT_TYPE];
+DROP INDEX [dbo].[ORBC_POWER_UNIT].[IX_FK_ORBC_POWER_UNIT_PROVINCE];
+DROP INDEX [permit].[ORBC_SPECIAL_AUTH].[IX_FK_ORBC_SPECIAL_AUTH_COMPANY_ID];
+DROP INDEX [permit].[ORBC_SPECIAL_AUTH].[IX_FK_ORBC_SPECIAL_AUTH_NO_FEE_TYPE];
+DROP INDEX [dbo].[ORBC_TRAILER].[IX_FK_ORBC_TRAILER_COMPANY];
+DROP INDEX [dbo].[ORBC_TRAILER].[IX_FK_ORBC_TRAILER_PROVINCE];
+DROP INDEX [dbo].[ORBC_TRAILER].[IX_FK_ORBC_TRAILER_TRAILER_TYPE];
+DROP INDEX [dbo].[ORBC_USER].[IX_FK_ORBC_USER_CONTACT];
+DROP INDEX [dbo].[ORBC_USER].[IX_FK_ORBC_USER_DIRECTORY];
+DROP INDEX [dbo].[ORBC_USER].[IX_FK_ORBC_USER_USER_AUTH_GROUP];
+DROP INDEX [dbo].[ORBC_USER].[IX_FK_ORBC_USER_USER_STATUS];
+DROP INDEX [permit].[ORBC_CFS_TRANSACTION_DETAIL].[IX_ORBC_CFS_DETAILS_TRANSACTION_ID_FK];
+DROP INDEX [permit].[ORBC_CFS_TRANSACTION_DETAIL].[IX_ORBC_CFS_TRANSACTION_DETAIL_FILE_STATUS_FK];
+DROP INDEX [dops].[ORBC_DOCUMENT].[IX_ORBC_DOCUMENT_COMPANY_ID_FK];
+DROP INDEX [permit].[ORBC_LOA_DETAILS].[IX_ORBC_LOA_DETAILS_DOCUMENT_ID_FK];
+DROP INDEX [permit].[ORBC_LOA_VEHICLES].[IX_ORBC_LOA_VEHICLES_POWER_UNIT_ID_FK];
+DROP INDEX [permit].[ORBC_LOA_VEHICLES].[IX_ORBC_LOA_VEHICLES_TRAILER_ID_FK];
+DROP INDEX [permit].[ORBC_PERMIT_TRANSACTION].[IX_ORBC_PERMIT_TRANSACTION_PERMIT_ID_FK];
+DROP INDEX [permit].[ORBC_PERMIT_TRANSACTION].[IX_ORBC_PERMIT_TRANSACTION_TRANSACTION_ID_FK];
+DROP INDEX [permit].[ORBC_RECEIPT].[IX_ORBC_RECEIPT_TRANSACTION_ID_FK];
+DROP INDEX [permit].[ORBC_TRANSACTION].[IX_ORBC_TRANSACTION_CARD_TYPE_FK];
+DROP INDEX [permit].[ORBC_TRANSACTION].[IX_ORBC_TRANSACTION_PAYMENT_METHOD_FK];
+DROP INDEX [permit].[ORBC_TRANSACTION].[IX_ORBC_TRANSACTION_TYPE_FK];
+GO
+
+-- Targeted indexes for permit search and sort
+DROP INDEX [permit].[ORBC_PERMIT].[IX_PERMIT_NUMBER];
+DROP INDEX [permit].[ORBC_PERMIT_DATA].[IX_START_DATE];
+DROP INDEX [permit].[ORBC_PERMIT_DATA].[IX_EXPIRY_DATE];
+DROP INDEX [permit].[ORBC_PERMIT_DATA].[IX_UNIT_NUMBER];
+DROP INDEX [permit].[ORBC_PERMIT_DATA].[IX_PLATE];
+DROP INDEX [permit].[ORBC_PERMIT_DATA].[IX_VIN];
+GO
+
+IF @@ERROR <> 0 SET NOEXEC ON
+GO
+
+DECLARE @VersionDescription VARCHAR(255)
+SET @VersionDescription = 'Reverting adding indexes on foreign keys and permit sort columns'
+
+INSERT [dbo].[ORBC_SYS_VERSION] ([VERSION_ID], [DESCRIPTION], [RELEASE_DATE]) VALUES (49, @VersionDescription, getutcdate())
+
+IF @@ERROR <> 0 SET NOEXEC ON
+GO
+
+COMMIT TRANSACTION
+GO
+IF @@ERROR <> 0 SET NOEXEC ON
+GO
+DECLARE @Success AS BIT
+SET @Success = 1
+SET NOEXEC OFF
+IF (@Success = 1) PRINT 'The database revert succeeded'
+ELSE BEGIN
+ IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION
+ PRINT 'The database revert failed'
+END
+GO
\ No newline at end of file
diff --git a/database/mssql/scripts/versions/v_50_ddl.sql b/database/mssql/scripts/versions/v_50_ddl.sql
new file mode 100644
index 000000000..f5ac2f00a
--- /dev/null
+++ b/database/mssql/scripts/versions/v_50_ddl.sql
@@ -0,0 +1,130 @@
+SET ANSI_NULLS ON
+GO
+SET QUOTED_IDENTIFIER ON
+GO
+SET NOCOUNT ON
+GO
+
+SET XACT_ABORT ON
+GO
+SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
+GO
+BEGIN TRANSACTION
+GO
+
+-- Foreign key indexes on all tables
+CREATE NONCLUSTERED INDEX IX_FK_ETL_PROCESSES_ETL_PROCESS_TYPE ON [tps].[ETL_PROCESSES] ([PROCESS_TYPE]);
+CREATE NONCLUSTERED INDEX IX_FK_ORBC_ADDRESS_PROVINCE ON [dbo].[ORBC_ADDRESS] ([PROVINCE_TYPE]);
+CREATE NONCLUSTERED INDEX IX_FK_ORBC_CASE_ACTIVITY_CASE_ACTIVITY_TYPE ON [case].[ORBC_CASE_ACTIVITY] ([CASE_ACTIVITY_TYPE]);
+CREATE NONCLUSTERED INDEX IX_FK_ORBC_CASE_ACTIVITY_CASE_EVENT_ID ON [case].[ORBC_CASE_ACTIVITY] ([CASE_EVENT_ID]);
+CREATE NONCLUSTERED INDEX IX_FK_ORBC_CASE_ACTIVITY_CASE_ID ON [case].[ORBC_CASE_ACTIVITY] ([CASE_ID]);
+CREATE NONCLUSTERED INDEX IX_FK_ORBC_CASE_CASE_STATUS_TYPE ON [case].[ORBC_CASE] ([CASE_STATUS_TYPE]);
+CREATE NONCLUSTERED INDEX IX_FK_ORBC_CASE_CASE_TYPE ON [case].[ORBC_CASE] ([CASE_TYPE]);
+CREATE NONCLUSTERED INDEX IX_FK_ORBC_CASE_DOCUMENT_CASE_EVENT_ID ON [case].[ORBC_CASE_DOCUMENT] ([CASE_EVENT_ID]);
+CREATE NONCLUSTERED INDEX IX_FK_ORBC_CASE_DOCUMENT_CASE_ID ON [case].[ORBC_CASE_DOCUMENT] ([CASE_ID]);
+CREATE NONCLUSTERED INDEX IX_FK_ORBC_CASE_EVENT_CASE_EVENT_TYPE ON [case].[ORBC_CASE_EVENT] ([CASE_EVENT_TYPE]);
+CREATE NONCLUSTERED INDEX IX_FK_ORBC_CASE_NOTES_CASE_EVENT_ID ON [case].[ORBC_CASE_NOTES] ([CASE_EVENT_ID]);
+CREATE NONCLUSTERED INDEX IX_FK_ORBC_CASE_NOTES_CASE_ID ON [case].[ORBC_CASE_NOTES] ([CASE_ID]);
+CREATE NONCLUSTERED INDEX IX_FK_ORBC_CASE_ORIGINAL_CASE_ID ON [case].[ORBC_CASE] ([ORIGINAL_CASE_ID]);
+CREATE NONCLUSTERED INDEX IX_FK_ORBC_CASE_PERMIT_ID ON [case].[ORBC_CASE] ([PERMIT_ID]);
+CREATE NONCLUSTERED INDEX IX_FK_ORBC_CASE_PREVIOUS_CASE_ID ON [case].[ORBC_CASE] ([PREVIOUS_CASE_ID]);
+CREATE NONCLUSTERED INDEX IX_FK_ORBC_COMPANY_DIRECTORY ON [dbo].[ORBC_COMPANY] ([COMPANY_DIRECTORY]);
+CREATE NONCLUSTERED INDEX IX_FK_ORBC_COMPANY_MAILING_ADDRESS ON [dbo].[ORBC_COMPANY] ([MAILING_ADDRESS_ID]);
+CREATE NONCLUSTERED INDEX IX_FK_ORBC_COMPANY_PRIMARY_CONTACT ON [dbo].[ORBC_COMPANY] ([PRIMARY_CONTACT_ID]);
+CREATE NONCLUSTERED INDEX IX_FK_ORBC_COMPANY_SUSPEND_ACTIVITY_ORBC_COMPANY ON [dbo].[ORBC_COMPANY_SUSPEND_ACTIVITY] ([COMPANY_ID]);
+CREATE NONCLUSTERED INDEX IX_FK_ORBC_COMPANY_SUSPEND_ACTIVITY_SUSPEND_ACTIVITY_TYPE ON [dbo].[ORBC_COMPANY_SUSPEND_ACTIVITY] ([SUSPEND_ACTIVITY_TYPE]);
+CREATE NONCLUSTERED INDEX IX_FK_ORBC_COMPANY_USER_COMPANY ON [dbo].[ORBC_COMPANY_USER] ([COMPANY_ID]);
+CREATE NONCLUSTERED INDEX IX_FK_ORBC_COMPANY_USER_USER ON [dbo].[ORBC_COMPANY_USER] ([USER_GUID]);
+CREATE NONCLUSTERED INDEX IX_FK_ORBC_COMPANY_USER_USER_AUTH_GROUP ON [dbo].[ORBC_COMPANY_USER] ([USER_AUTH_GROUP_TYPE]);
+CREATE NONCLUSTERED INDEX IX_FK_ORBC_COMPANY_USER_USER_STATUS ON [dbo].[ORBC_COMPANY_USER] ([USER_STATUS_TYPE]);
+CREATE NONCLUSTERED INDEX IX_FK_ORBC_CONTACT_PROVINCE ON [dbo].[ORBC_CONTACT] ([PROVINCE_TYPE]);
+CREATE NONCLUSTERED INDEX IX_FK_ORBC_CREDIT_ACCOUNT_ACTIVITY_CREDIT_ACCOUNT_ACTIVITY_TYPE ON [permit].[ORBC_CREDIT_ACCOUNT_ACTIVITY] ([CREDIT_ACCOUNT_ACTIVITY_TYPE]);
+CREATE NONCLUSTERED INDEX IX_FK_ORBC_CREDIT_ACCOUNT_ACTIVITY_CREDIT_ACCOUNT_ID ON [permit].[ORBC_CREDIT_ACCOUNT_ACTIVITY] ([CREDIT_ACCOUNT_ID]);
+CREATE NONCLUSTERED INDEX IX_FK_ORBC_CREDIT_ACCOUNT_COMPANY_ID ON [permit].[ORBC_CREDIT_ACCOUNT] ([COMPANY_ID]);
+CREATE NONCLUSTERED INDEX IX_FK_ORBC_CREDIT_ACCOUNT_CREDIT_ACCOUNT_STATUS_TYPE ON [permit].[ORBC_CREDIT_ACCOUNT] ([CREDIT_ACCOUNT_STATUS_TYPE]);
+CREATE NONCLUSTERED INDEX IX_FK_ORBC_CREDIT_ACCOUNT_CREDIT_ACCOUNT_TYPE ON [permit].[ORBC_CREDIT_ACCOUNT] ([CREDIT_ACCOUNT_TYPE]);
+CREATE NONCLUSTERED INDEX IX_FK_ORBC_CREDIT_ACCOUNT_USER_COMPANY_ID ON [permit].[ORBC_CREDIT_ACCOUNT_USER] ([COMPANY_ID]);
+CREATE NONCLUSTERED INDEX IX_FK_ORBC_CREDIT_ACCOUNT_USER_CREDIT_ACCOUNT_ID ON [permit].[ORBC_CREDIT_ACCOUNT_USER] ([CREDIT_ACCOUNT_ID]);
+CREATE NONCLUSTERED INDEX IX_FK_ORBC_GL_CODE_TYPE_GL_TYPE ON [permit].[ORBC_GL_CODE_TYPE] ([GL_TYPE]);
+CREATE NONCLUSTERED INDEX IX_FK_ORBC_GL_CODE_TYPE_PAYMENT_CARD_TYPE ON [permit].[ORBC_GL_CODE_TYPE] ([PAYMENT_CARD_TYPE]);
+CREATE NONCLUSTERED INDEX IX_FK_ORBC_GL_CODE_TYPE_PAYMENT_METHOD_TYPE ON [permit].[ORBC_GL_CODE_TYPE] ([PAYMENT_METHOD_TYPE]);
+CREATE NONCLUSTERED INDEX IX_FK_ORBC_GL_CODE_TYPE_PERMIT_TYPE ON [permit].[ORBC_GL_CODE_TYPE] ([PERMIT_TYPE]);
+CREATE NONCLUSTERED INDEX IX_FK_ORBC_GROUP_ROLE_ROLE ON [access].[ORBC_GROUP_ROLE] ([ROLE_TYPE]);
+CREATE NONCLUSTERED INDEX IX_FK_ORBC_GROUP_ROLE_USER_AUTH_GROUP ON [access].[ORBC_GROUP_ROLE] ([USER_AUTH_GROUP_TYPE]);
+CREATE NONCLUSTERED INDEX IX_FK_ORBC_LOA_DETAILS_COMPANY ON [permit].[ORBC_LOA_DETAILS] ([COMPANY_ID]);
+CREATE NONCLUSTERED INDEX IX_FK_ORBC_LOA_DETAILS_ORIGINAL_LOA_ID ON [permit].[ORBC_LOA_DETAILS] ([ORIGINAL_LOA_ID]);
+CREATE NONCLUSTERED INDEX IX_FK_ORBC_LOA_DETAILS_PREVIOUS_LOA_ID ON [permit].[ORBC_LOA_DETAILS] ([PREVIOUS_LOA_ID]);
+CREATE NONCLUSTERED INDEX IX_FK_ORBC_LOA_PERMIT_TYPE_LOA_ID ON [permit].[ORBC_LOA_PERMIT_TYPE_DETAILS] ([LOA_ID]);
+CREATE NONCLUSTERED INDEX IX_FK_ORBC_LOA_PERMIT_TYPES_PERMIT_TYPE ON [permit].[ORBC_LOA_PERMIT_TYPE_DETAILS] ([PERMIT_TYPE]);
+CREATE NONCLUSTERED INDEX IX_FK_ORBC_LOA_VEHICLES_LOA_ID ON [permit].[ORBC_LOA_VEHICLES] ([LOA_ID]);
+CREATE NONCLUSTERED INDEX IX_FK_ORBC_PENDING_IDIR_USER_AUTH_GROUP ON [dbo].[ORBC_PENDING_IDIR_USER] ([USER_AUTH_GROUP_TYPE]);
+CREATE NONCLUSTERED INDEX IX_FK_ORBC_PENDING_USER_AUTH_GROUP ON [dbo].[ORBC_PENDING_USER] ([USER_AUTH_GROUP_TYPE]);
+CREATE NONCLUSTERED INDEX IX_FK_ORBC_PENDING_USER_COMPANY ON [dbo].[ORBC_PENDING_USER] ([COMPANY_ID]);
+CREATE NONCLUSTERED INDEX IX_FK_ORBC_PERMIT_COMMENTS_PERMIT ON [permit].[ORBC_PERMIT_COMMENTS] ([PERMIT_ID]);
+CREATE NONCLUSTERED INDEX IX_FK_ORBC_PERMIT_ID ON [permit].[ORBC_PERMIT_DATA] ([PERMIT_ID]);
+CREATE NONCLUSTERED INDEX IX_FK_ORBC_PERMIT_PARENT_PERMIT ON [permit].[ORBC_PERMIT] ([PREVIOUS_REV_ID]);
+CREATE NONCLUSTERED INDEX IX_FK_ORBC_PERMIT_PERMIT_APPLICATION_ORIGIN ON [permit].[ORBC_PERMIT] ([PERMIT_APPLICATION_ORIGIN_TYPE]);
+CREATE NONCLUSTERED INDEX IX_FK_ORBC_PERMIT_PERMIT_APPROVAL_SOURCE ON [permit].[ORBC_PERMIT] ([PERMIT_APPROVAL_SOURCE_TYPE]);
+CREATE NONCLUSTERED INDEX IX_FK_ORBC_PERMIT_PERMIT_ISSUED_BY ON [permit].[ORBC_PERMIT] ([PERMIT_ISSUED_BY_TYPE]);
+CREATE NONCLUSTERED INDEX IX_FK_ORBC_PERMIT_PERMIT_STATUS_TYPE ON [permit].[ORBC_PERMIT] ([PERMIT_STATUS_TYPE]);
+CREATE NONCLUSTERED INDEX IX_FK_ORBC_PERMIT_PERMIT_TYPE ON [permit].[ORBC_PERMIT] ([PERMIT_TYPE]);
+CREATE NONCLUSTERED INDEX IX_FK_ORBC_PERMIT_STATE_PERMIT ON [permit].[ORBC_PERMIT_STATE] ([PERMIT_ID]);
+CREATE NONCLUSTERED INDEX IX_FK_ORBC_PERMIT_STATE_PERMIT_STATUS ON [permit].[ORBC_PERMIT_STATE] ([PERMIT_STATUS_TYPE]);
+CREATE NONCLUSTERED INDEX IX_FK_ORBC_POWER_UNIT_COMPANY ON [dbo].[ORBC_POWER_UNIT] ([COMPANY_ID]);
+CREATE NONCLUSTERED INDEX IX_FK_ORBC_POWER_UNIT_POWER_UNIT_TYPE ON [dbo].[ORBC_POWER_UNIT] ([POWER_UNIT_TYPE]);
+CREATE NONCLUSTERED INDEX IX_FK_ORBC_POWER_UNIT_PROVINCE ON [dbo].[ORBC_POWER_UNIT] ([PROVINCE_TYPE]);
+CREATE NONCLUSTERED INDEX IX_FK_ORBC_SPECIAL_AUTH_COMPANY_ID ON [permit].[ORBC_SPECIAL_AUTH] ([COMPANY_ID]);
+CREATE NONCLUSTERED INDEX IX_FK_ORBC_SPECIAL_AUTH_NO_FEE_TYPE ON [permit].[ORBC_SPECIAL_AUTH] ([NO_FEE_TYPE]);
+CREATE NONCLUSTERED INDEX IX_FK_ORBC_TRAILER_COMPANY ON [dbo].[ORBC_TRAILER] ([COMPANY_ID]);
+CREATE NONCLUSTERED INDEX IX_FK_ORBC_TRAILER_PROVINCE ON [dbo].[ORBC_TRAILER] ([PROVINCE_TYPE]);
+CREATE NONCLUSTERED INDEX IX_FK_ORBC_TRAILER_TRAILER_TYPE ON [dbo].[ORBC_TRAILER] ([TRAILER_TYPE]);
+CREATE NONCLUSTERED INDEX IX_FK_ORBC_USER_CONTACT ON [dbo].[ORBC_USER] ([CONTACT_ID]);
+CREATE NONCLUSTERED INDEX IX_FK_ORBC_USER_DIRECTORY ON [dbo].[ORBC_USER] ([USER_DIRECTORY]);
+CREATE NONCLUSTERED INDEX IX_FK_ORBC_USER_USER_AUTH_GROUP ON [dbo].[ORBC_USER] ([USER_AUTH_GROUP_TYPE]);
+CREATE NONCLUSTERED INDEX IX_FK_ORBC_USER_USER_STATUS ON [dbo].[ORBC_USER] ([USER_STATUS_TYPE]);
+CREATE NONCLUSTERED INDEX IX_ORBC_CFS_DETAILS_TRANSACTION_ID_FK ON [permit].[ORBC_CFS_TRANSACTION_DETAIL] ([TRANSACTION_ID]);
+CREATE NONCLUSTERED INDEX IX_ORBC_CFS_TRANSACTION_DETAIL_FILE_STATUS_FK ON [permit].[ORBC_CFS_TRANSACTION_DETAIL] ([CFS_FILE_STATUS_TYPE]);
+CREATE NONCLUSTERED INDEX IX_ORBC_DOCUMENT_COMPANY_ID_FK ON [dops].[ORBC_DOCUMENT] ([COMPANY_ID]);
+CREATE NONCLUSTERED INDEX IX_ORBC_LOA_DETAILS_DOCUMENT_ID_FK ON [permit].[ORBC_LOA_DETAILS] ([DOCUMENT_ID]);
+CREATE NONCLUSTERED INDEX IX_ORBC_LOA_VEHICLES_POWER_UNIT_ID_FK ON [permit].[ORBC_LOA_VEHICLES] ([POWER_UNIT_ID]);
+CREATE NONCLUSTERED INDEX IX_ORBC_LOA_VEHICLES_TRAILER_ID_FK ON [permit].[ORBC_LOA_VEHICLES] ([TRAILER_ID]);
+CREATE NONCLUSTERED INDEX IX_ORBC_PERMIT_TRANSACTION_PERMIT_ID_FK ON [permit].[ORBC_PERMIT_TRANSACTION] ([PERMIT_ID]);
+CREATE NONCLUSTERED INDEX IX_ORBC_PERMIT_TRANSACTION_TRANSACTION_ID_FK ON [permit].[ORBC_PERMIT_TRANSACTION] ([TRANSACTION_ID]);
+CREATE NONCLUSTERED INDEX IX_ORBC_RECEIPT_TRANSACTION_ID_FK ON [permit].[ORBC_RECEIPT] ([TRANSACTION_ID]);
+CREATE NONCLUSTERED INDEX IX_ORBC_TRANSACTION_CARD_TYPE_FK ON [permit].[ORBC_TRANSACTION] ([PAYMENT_CARD_TYPE]);
+CREATE NONCLUSTERED INDEX IX_ORBC_TRANSACTION_PAYMENT_METHOD_FK ON [permit].[ORBC_TRANSACTION] ([PAYMENT_METHOD_TYPE]);
+CREATE NONCLUSTERED INDEX IX_ORBC_TRANSACTION_TYPE_FK ON [permit].[ORBC_TRANSACTION] ([TRANSACTION_TYPE]);
+GO
+
+-- Targeted indexes for permit search and sort
+CREATE NONCLUSTERED INDEX [IX_PERMIT_NUMBER] ON [permit].[ORBC_PERMIT] ([PERMIT_NUMBER] ASC);
+CREATE NONCLUSTERED INDEX [IX_START_DATE] ON [permit].[ORBC_PERMIT_DATA] ([START_DATE] ASC);
+CREATE NONCLUSTERED INDEX [IX_EXPIRY_DATE] ON [permit].[ORBC_PERMIT_DATA] ([EXPIRY_DATE] ASC);
+CREATE NONCLUSTERED INDEX [IX_UNIT_NUMBER] ON [permit].[ORBC_PERMIT_DATA] ([UNIT_NUMBER] ASC);
+CREATE NONCLUSTERED INDEX [IX_PLATE] ON [permit].[ORBC_PERMIT_DATA] ([PLATE] ASC);
+CREATE NONCLUSTERED INDEX [IX_VIN] ON [permit].[ORBC_PERMIT_DATA] ([VIN] ASC);
+GO
+
+IF @@ERROR <> 0 SET NOEXEC ON
+GO
+
+DECLARE @VersionDescription VARCHAR(255)
+SET @VersionDescription = 'Add indexes to all foreign key columns and permit sort columns'
+
+INSERT [dbo].[ORBC_SYS_VERSION] ([VERSION_ID], [DESCRIPTION], [UPDATE_SCRIPT], [REVERT_SCRIPT], [RELEASE_DATE]) VALUES (50, @VersionDescription, '$(UPDATE_SCRIPT)', '$(REVERT_SCRIPT)', getutcdate())
+IF @@ERROR <> 0 SET NOEXEC ON
+GO
+
+COMMIT TRANSACTION
+GO
+IF @@ERROR <> 0 SET NOEXEC ON
+GO
+DECLARE @Success AS BIT
+SET @Success = 1
+SET NOEXEC OFF
+IF (@Success = 1) PRINT 'The database update succeeded'
+ELSE BEGIN
+ IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION
+ PRINT 'The database update failed'
+END
+GO