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. + + + + + + + + +
+ ); +}; 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 ? ( + + ) : 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 ( + + +
+ +
+ +
+ + + Add Power Unit + +
+ + + + + + + + + + +
+
+
+ ); +}; 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 ? ( +
+ + + +
+ ) : null} + + {trailerSubtypeOptions.length > 0 ? ( + + + Add Trailer + + + + + ) : 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

+ + + + +
+ ); +}; 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 } }) => ( +
+ + + {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 && ( - - ) + } {hasToCartButton ? ( @@ -80,7 +78,7 @@ export const ReviewActions = ({ ) : null} - {reviewContext === PERMIT_REVIEW_CONTEXTS.QUEUE && ( + {reviewContext === PERMIT_REVIEW_CONTEXTS.QUEUE ? ( <> +