diff --git a/azure-pipelines.yml b/azure-pipelines.yml index ba2e359dc..e58f51862 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -78,4 +78,5 @@ stages: appDeployVariables: __VUE_APP_CONSUMER_API_GATEWAY__: $(__VUE_APP_CONSUMER_API_GATEWAY__) __VUE_APP_GOOGLE_PLACES_API_KEY__: $(__VUE_APP_GOOGLE_PLACES_API_KEY__) + __VUE_APP_HERITAGE_FUNNEL__: $(__VUE_APP_HERITAGE_FUNNEL__) cfDistributionId: $(cfDistributionId) \ No newline at end of file diff --git a/src/.eslintrc.js b/src/.eslintrc.js new file mode 100644 index 000000000..309a0e4fd --- /dev/null +++ b/src/.eslintrc.js @@ -0,0 +1,6 @@ +module.exports = { + env: { + jest: true + }, +//... +} \ No newline at end of file diff --git a/src/common-components/funnel-header/funnel-header.vue b/src/common-components/funnel-header/funnel-header.vue index efe6bd06b..97c67bd40 100644 --- a/src/common-components/funnel-header/funnel-header.vue +++ b/src/common-components/funnel-header/funnel-header.vue @@ -68,4 +68,7 @@ export default { .logo-image { max-width: 78px; } +.alert { + left: 0; +} diff --git a/src/common-components/button-back/button-back.spec.js b/src/common-components/funnel-sub-header/button-back/button-back.spec.js similarity index 100% rename from src/common-components/button-back/button-back.spec.js rename to src/common-components/funnel-sub-header/button-back/button-back.spec.js diff --git a/src/common-components/button-back/button-back.vue b/src/common-components/funnel-sub-header/button-back/button-back.vue similarity index 100% rename from src/common-components/button-back/button-back.vue rename to src/common-components/funnel-sub-header/button-back/button-back.vue diff --git a/src/common-components/funnel-sub-header/funnel-sub-header.vue b/src/common-components/funnel-sub-header/funnel-sub-header.vue index 155203e65..f8ac3134a 100644 --- a/src/common-components/funnel-sub-header/funnel-sub-header.vue +++ b/src/common-components/funnel-sub-header/funnel-sub-header.vue @@ -23,7 +23,7 @@ diff --git a/src/layouts/button-question-examples/button-question-examples.vue b/src/layouts/button-question-examples/button-question-examples.vue deleted file mode 100644 index 6df20ddf1..000000000 --- a/src/layouts/button-question-examples/button-question-examples.vue +++ /dev/null @@ -1,138 +0,0 @@ - - - \ No newline at end of file diff --git a/src/layouts/component-test/component-test.vue b/src/layouts/component-test/component-test.vue index 106a0c776..526b1e971 100644 --- a/src/layouts/component-test/component-test.vue +++ b/src/layouts/component-test/component-test.vue @@ -1144,7 +1144,7 @@ - - diff --git a/src/layouts/vehicle-damage/replace-options-question/replace-options-question.spec.js b/src/layouts/vehicle-damage/replace-options-question/replace-options-question.spec.js index f8b010c41..8bdace584 100644 --- a/src/layouts/vehicle-damage/replace-options-question/replace-options-question.spec.js +++ b/src/layouts/vehicle-damage/replace-options-question/replace-options-question.spec.js @@ -3,7 +3,7 @@ import replaceOptionsQuestion from "@/layouts/vehicle-damage/replace-options-que import { getMountOptions } from "@/helpers/unit-test-helper.js"; import { nextTick } from "vue"; import store from "@/store"; -jest.mock("@/store", () => { return {}; }, {virtual: true}); +jest.mock("@/store",()=>{return{};},{virtual:true}); describe("replace-options-question.vue", () => { test("Selected damage option is emitted upon selection.", async () => { diff --git a/src/layouts/vehicle-damage/side-door-options/side-door-options.spec.js b/src/layouts/vehicle-damage/side-door-options/side-door-options.spec.js index 03a96e567..eff56bbc9 100644 --- a/src/layouts/vehicle-damage/side-door-options/side-door-options.spec.js +++ b/src/layouts/vehicle-damage/side-door-options/side-door-options.spec.js @@ -3,7 +3,7 @@ import sideDoorOptions from "@/layouts/vehicle-damage/side-door-options/side-doo import { getMountOptions } from "@/helpers/unit-test-helper.js"; import replaceOptionsQuestion from "@/layouts/vehicle-damage/replace-options-question/replace-options-question"; import store from "@/store"; -jest.mock("@/store", () => { return {}; }, {virtual: true}); +jest.mock("@/store",()=>{return{};},{virtual:true}); describe("replace-options-question.vue", () => { test("Selected side door option is emitted upon selection.", async () => { @@ -95,7 +95,7 @@ describe("replace-options-question.vue", () => { }) { //Mock store - store.dispatch = jest.fn(() => dataFromStoreApi); + store.dispatch = jest.fn(() => {}); store.getters = { vehicle: {year: 2019, make: 'honda', model: 'civc', style: '2 Door', category: 'CAR'} }; const mountOptions = getMountOptions({ store: { diff --git a/src/layouts/vehicle-damage/vehicle-damage.vue b/src/layouts/vehicle-damage/vehicle-damage.vue index c508e0d03..893e7d572 100644 --- a/src/layouts/vehicle-damage/vehicle-damage.vue +++ b/src/layouts/vehicle-damage/vehicle-damage.vue @@ -72,13 +72,15 @@ import alert from "@/ux-components/alert/alert"; import { fetchCmsContentForPage } from "@/helpers/cms-content-helper"; import { settleAllPromises } from "@/helpers/layout-helper"; import { storeActions } from "@/constants/store-actions"; -import store from "@/store"; -import baseMixin from "@/mixins/base-mixin"; import { Form, defineRule } from "vee-validate"; import { required } from "@/helpers/validation-rules"; import { errorMessages } from "@/constants/error-messages"; import { damageLocationsCms } from "@/constants/damage-locations-cms.js"; import { damageLocationsSelected } from "@/constants/damage-locations-selected.js"; +import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper"; + +import store from "@/store"; +import baseMixin from "@/mixins/base-mixin"; // DEFINE VALIDATION RULES defineRule("replace-options-required", required(errorMessages.REPLACE_OPTIONS_REQUIRED)); @@ -139,7 +141,10 @@ export default { }, methods: { arePagePrerequisitesValid() { - return store.getters.vehicle.carId !== null; + if(store.getters.vehicle.carId){ + return true; + } + return false; }, resetDependentState() { store.dispatch(storeActions.RESET_PARTS_STATE_AND_DEPENDENCIES); @@ -271,6 +276,12 @@ export default { }, navigateForward(partsData){ + // CSR-98 TEMP + if (store.getters.vehicle.year == 2010) { + navigateToHeritageFunnel(); + return; + } + //found problem questions if (partsData.data.partsOrQuestions.some(pq => pq.partQuestions != null && pq.partQuestions.length > 0)){ this.$router.navigateAfterSave(this.navigationScenarios.SELECTED_DAMAGE_WITH_PART_QUESTIONS, this.$route, {}, {}, partsData.data); @@ -373,15 +384,15 @@ export default { hasSplitSingleConflict() { if (!this.selectedWindshieldOptions.selectedWindshieldReplaceOptions) return false; - return this.selectedWindshieldOptions.selectedWindshieldReplaceOptions.some(selectedSingleWindshield => + return this.selectedWindshieldOptions.selectedWindshieldReplaceOptions.some(selectedSingleWindshield => { return selectedSingleWindshield.toUpperCase() === damageLocationsSelected.SINGLE.toUpperCase(); - }) && - (this.selectedWindshieldOptions.selectedWindshieldReplaceOptions.some(selectedDriverWindshield => + }) && + (this.selectedWindshieldOptions.selectedWindshieldReplaceOptions.some(selectedDriverWindshield => { return selectedDriverWindshield.toUpperCase() === damageLocationsSelected.DRIVER.toUpperCase(); }) || - this.selectedWindshieldOptions.selectedWindshieldReplaceOptions.some(selectedPassengerWindshield => + this.selectedWindshieldOptions.selectedWindshieldReplaceOptions.some(selectedPassengerWindshield => { return selectedPassengerWindshield.toUpperCase() === damageLocationsSelected.PASSENGER.toUpperCase(); }) diff --git a/src/layouts/vehicle-make/vehicle-make.vue b/src/layouts/vehicle-make/vehicle-make.vue index 7ffb28828..4dee11115 100644 --- a/src/layouts/vehicle-make/vehicle-make.vue +++ b/src/layouts/vehicle-make/vehicle-make.vue @@ -75,7 +75,10 @@ export default { ); }, arePagePrerequisitesValid() { - return store.getters.vehicle.year !== null; + if (store.getters.vehicle.year){ + return true; + } + return false; }, resetDependentState() { // Set diff --git a/src/layouts/vehicle-model/vehicle-model.vue b/src/layouts/vehicle-model/vehicle-model.vue index ea83290aa..6f57b5140 100644 --- a/src/layouts/vehicle-model/vehicle-model.vue +++ b/src/layouts/vehicle-model/vehicle-model.vue @@ -76,7 +76,10 @@ export default { ); }, arePagePrerequisitesValid() { - return store.getters.vehicle.make !== null; + if(store.getters.vehicle.make){ + return true; + } + return false; }, resetDependentState() { // Set diff --git a/src/layouts/vehicle-parts/vehicle-parts.spec.js b/src/layouts/vehicle-parts/vehicle-parts.spec.js index efd69e1c5..a663c5396 100644 --- a/src/layouts/vehicle-parts/vehicle-parts.spec.js +++ b/src/layouts/vehicle-parts/vehicle-parts.spec.js @@ -103,7 +103,7 @@ describe("vehicle-parts.vue", () => { test("PageData / isRepair populated in Vuex. arePagePrerequisitesValid should be true ", async () => { //Arrange - store.getters.pageData.mockReturnValueOnce(basePartResponse); + store.getters.pageData.mockReturnValue(basePartResponse); store.getters.lineItems = { glassParts: {} } const { wrapper } = setupMocks({ diff --git a/src/layouts/vehicle-parts/vehicle-parts.vue b/src/layouts/vehicle-parts/vehicle-parts.vue index 87dc02f0d..d4c3e164a 100644 --- a/src/layouts/vehicle-parts/vehicle-parts.vue +++ b/src/layouts/vehicle-parts/vehicle-parts.vue @@ -4,20 +4,32 @@
-
-
- -
+
+
+
+
- -
-
-
+ +
+
+
- +
@@ -32,18 +44,10 @@ import funnelSubHeader from "@/common-components/funnel-sub-header/funnel-sub-he import funnelFooter from "@/common-components/funnel-footer/funnel-footer"; import alert from "@/ux-components/alert/alert"; // Supporting Files -import { - fetchCmsContentForPage -} from "@/helpers/cms-content-helper"; -import { - settleAllPromises -} from "@/helpers/layout-helper"; -import { - fmgPageValues -} from "@/router/router-constants/fmgPage-values"; -import { - storeMutations -} from "@/constants/store-mutations"; +import { fetchCmsContentForPage } from "@/helpers/cms-content-helper"; +import { settleAllPromises } from "@/helpers/layout-helper"; +import { fmgPageValues } from "@/router/router-constants/fmgPage-values"; +import { storeMutations } from "@/constants/store-mutations"; import store from "@/store"; export default { @@ -71,148 +75,151 @@ export default { }) ); - // Set alertData for the page alert. These use props so we don't call - // initializeComponent here. - vm.alertWidgetData = { - copy: resultMap.cmsContent.AlertWidget.BodyText, - headline: resultMap.cmsContent.AlertWidget.HeadlineText, - }; - }); - }, - data() { + // Set alertData for the page alert. These use props so we don't call + // initializeComponent here. + vm.alertWidgetData = { + copy: resultMap.cmsContent.AlertWidget.BodyText, + headline: resultMap.cmsContent.AlertWidget.HeadlineText, + }; + }); + }, + data() { + return { + glassParts: {}, + alertWidgetData: Object, + }; + }, + components: { + glassPartQuestion, + funnelHeader, + vehicleBanner, + funnelSubHeader, + funnelFooter, + alert, + }, + computed: { + PartsForQuestions() { + const partsData = this.PartsFromApi; + + // Map API result data, to vehicle-parts data structure + const mappedData = partsData.partsOrQuestions.map((g) => { return { - glassParts: {}, - alertWidgetData: Object, + glassName: g.glassName, + glassLocation: g.glassLocation, + colorAnswers: g.parts.reduce((arr, p) => { + arr.push({ + ColorAnswerText: p.color, + FeatureAnswers: [ + { + FeatureAnswerText: + p.description === "" ? p.color : p.description, + PartNumber: p.partNumber, + }, + ], + }); + return arr; + }, []), }; + }); + + return mappedData; }, - components: { - glassPartQuestion, - funnelHeader, - vehicleBanner, - funnelSubHeader, - funnelFooter, - alert, + + PartsFromApi() { + return store.getters.pageData(fmgPageValues.VEHICLE_PARTS); }, - computed: { - PartsForQuestions() { - const partsData = this.PartsFromApi; - // Map API result data, to vehicle-parts data structure - const mappedData = partsData.partsOrQuestions.map((g) => { - - return { - glassName: g.glassName, - glassLocation: g.glassLocation, - colorAnswers: g.parts.reduce((arr, p) => { - arr.push({ - ColorAnswerText: p.color, - FeatureAnswers: [{ - FeatureAnswerText: p.description === "" ? p.color : p.description, - PartNumber: p.partNumber, - }, ], - }); - return arr; - }, []), - }; - }); - - return mappedData; - }, - - PartsFromApi() { - return store.getters.pageData(fmgPageValues.VEHICLE_PARTS); - }, - - RefPrefix() { - return "partQuestion"; - }, + RefPrefix() { + return "partQuestion"; }, - methods: { - arePagePrerequisitesValid() { - // Check if isRepair is populated and if the pageData we need is here (Parts data) - if ( - store.getters.damage.isRepair !== null && - Object.keys(store.getters.pageData(fmgPageValues.VEHICLE_PARTS)) - .length !== 0 - ) { - return true; - } + }, + methods: { + arePagePrerequisitesValid() { + // Check if isRepair is populated and if the pageData we need is here (Parts data) + if ( + (store.getters.damage.isRepair != null) && + Object.keys(store.getters.pageData(fmgPageValues.VEHICLE_PARTS)) + .length !== 0 + ) { + return true; + } - return false; - }, - backButtonAction() { - this.$router.navigate(this.navigationScenarios.CLICKED_BACK, this.$route); - }, - forwardButtonAction() { - const selectedGlassPartNumbers = []; - const matchedParts = []; + return false; + }, + backButtonAction() { + this.$router.navigate(this.navigationScenarios.CLICKED_BACK, this.$route); + }, + forwardButtonAction() { + const selectedGlassPartNumbers = []; + const matchedParts = []; - // Compile all selected parts from the page. - for (let [key, value] of Object.entries(this.glassParts)) { - for (let [glassKey, glassValue] of Object.entries(value)) { - selectedGlassPartNumbers.push(glassValue[0]); - } - } - - // Match them to the parts from the API. - for (let [key, value] of Object.entries( - this.PartsFromApi.partsOrQuestions - )) { - for (let [partKey, partValue] of Object.entries(value.parts)) { - const currentPart = - this.PartsFromApi.partsOrQuestions[key].parts[partKey]; - const isMatched = selectedGlassPartNumbers.some( - (p) => p === currentPart.partNumber - ); - - if (isMatched) { - matchedParts.push(currentPart); - } - } - } - - // If no parts could be matched, throw an error. - if (matchedParts.length === 0) { - throw new Error("Could not match any parts to the selected parts"); - } - - // Save parts to the store. - store.commit(storeMutations.UPDATE_PARTS, matchedParts); - - // Navigate to the next page. - this.$router.navigateAfterSave( - this.navigationScenarios.SELECTED_PARTS, - this.$route - ); - }, - - resetDependentState() { - // Nothing additional to reset here: The page save is already fully resetting all the line-items on the order - }, - - LoadInitialPartsData() { - const partsData = this.PartsFromApi; - const alreadyPopulatedPartsData = - this.$store.getters.lineItems.glassParts === null ? {} : - this.$store.getters.lineItems.glassParts; - - partsData.partsOrQuestions.map((g) => { - // If the part is already populated, use the value from the store and populate the v-model. - Object.keys(alreadyPopulatedPartsData).forEach((key) => { - const partNumber = alreadyPopulatedPartsData[key].partNumber; - g.parts.forEach((p) => { - if (p.partNumber === partNumber) { - this.glassParts[g.glassLocation + "-" + g.glassName] = { - [g.glassLocation]: [partNumber], - }; - } - }); - }); - }); + // Compile all selected parts from the page. + for (let [key, value] of Object.entries(this.glassParts)) { + for (let [glassKey, glassValue] of Object.entries(value)) { + selectedGlassPartNumbers.push(glassValue[0]); } + } + + // Match them to the parts from the API. + for (let [key, value] of Object.entries( + this.PartsFromApi.partsOrQuestions + )) { + for (let [partKey, partValue] of Object.entries(value.parts)) { + const currentPart = + this.PartsFromApi.partsOrQuestions[key].parts[partKey]; + const isMatched = selectedGlassPartNumbers.some( + (p) => p === currentPart.partNumber + ); + + if (isMatched) { + matchedParts.push(currentPart); + } + } + } + + // If no parts could be matched, throw an error. + if (matchedParts.length === 0) { + throw new Error("Could not match any parts to the selected parts"); + } + + // Save parts to the store. + store.commit(storeMutations.UPDATE_PARTS, matchedParts); + + // Navigate to the next page. + this.$router.navigateAfterSave( + this.navigationScenarios.SELECTED_PARTS, + this.$route + ); }, - mounted(){ - this.LoadInitialPartsData(); - } + + resetDependentState() { + // Nothing additional to reset here: The page save is already fully resetting all the line-items on the order + }, + + LoadInitialPartsData() { + const partsData = this.PartsFromApi; + const alreadyPopulatedPartsData = + this.$store.getters.lineItems.glassParts === null + ? {} + : this.$store.getters.lineItems.glassParts; + + partsData.partsOrQuestions.map((g) => { + // If the part is already populated, use the value from the store and populate the v-model. + Object.keys(alreadyPopulatedPartsData).forEach((key) => { + const partNumber = alreadyPopulatedPartsData[key].partNumber; + g.parts.forEach((p) => { + if (p.partNumber === partNumber) { + this.glassParts[g.glassLocation + "-" + g.glassName] = { + [g.glassLocation]: [partNumber], + }; + } + }); + }); + }); + }, + }, + mounted() { + this.LoadInitialPartsData(); + }, }; diff --git a/src/layouts/vehicle-style/vehicle-style.vue b/src/layouts/vehicle-style/vehicle-style.vue index a27f32047..e98cc400d 100644 --- a/src/layouts/vehicle-style/vehicle-style.vue +++ b/src/layouts/vehicle-style/vehicle-style.vue @@ -87,7 +87,10 @@ export default { ); }, arePagePrerequisitesValid() { - return store.getters.vehicle.model !== null; + if(store.getters.vehicle.model){ + return true; + } + return false; }, resetDependentState() { // Invokes diff --git a/src/mixins/base-mixin.js b/src/mixins/base-mixin.js index b3b3a19f9..b5a954c43 100644 --- a/src/mixins/base-mixin.js +++ b/src/mixins/base-mixin.js @@ -2,7 +2,6 @@ import store from "@/store"; import { storeActions } from "@/constants/store-actions.js"; import { storeMutations } from "@/constants/store-mutations.js"; import { navigationScenarios } from "@/router/router-constants/navigation-scenarios"; -import { widgetNames } from "@/constants/widget-names.js"; import { vehicleCategories } from "@/constants/vehicle-categories.js"; export default { @@ -52,9 +51,6 @@ export default { navigationScenarios() { return navigationScenarios; }, - widgetNames() { - return widgetNames; - }, vehicleCategories() { return vehicleCategories; }, diff --git a/src/mixins/base-mixin.spec.js b/src/mixins/base-mixin.spec.js index d32163469..48af9a7c0 100644 --- a/src/mixins/base-mixin.spec.js +++ b/src/mixins/base-mixin.spec.js @@ -1,6 +1,5 @@ import baseMixin from "@/mixins/base-mixin"; import { storeActions } from "@/constants/store-actions.js"; -import { widgetNames } from "@/constants/widget-names.js"; import { storeMutations } from "@/constants/store-mutations.js"; import { navigationScenarios } from "@/router/router-constants/navigation-scenarios"; import { vehicleCategories } from "@/constants/vehicle-categories.js"; @@ -67,18 +66,6 @@ describe("baseMixin.js", () => { // Assert expect(navigationScenariosForTest).toEqual(navigationScenarios); }); - - test("computed: widgetNames should be equal to import object", () => { - // Arrange - const mixIn = getMixInInstance({}); - - // Act - let widgetNamesForTest = mixIn.computed.widgetNames(); - - // Assert - expect(widgetNamesForTest).toEqual(widgetNames); - }); - test("computed: vehicleCategories should be equal to import object", () => { // Arrange const mixIn = getMixInInstance({}); @@ -131,7 +118,6 @@ function getMixInInstance({ isDispatchSuccess = true }) { const baseMixIn = baseMixin; baseMixIn.methods.$route = route; baseMixIn.methods.storeActions = storeActions; - baseMixIn.methods.widgetNames = widgetNames; baseMixIn.methods.vehicleCategories = vehicleCategories; store.dispatch = storeDispatch; diff --git a/src/router/index.js b/src/router/index.js index be434e9d8..778c55cfc 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -1,19 +1,23 @@ // Supporting files import { createWebHistory, createRouter } from "vue-router"; import { storeActions } from "@/constants/store-actions"; -import baseMixin from "@/mixins/base-mixin"; import { lazyLoadComponent } from "@/router/dynamic-routing/component-loader.js"; import { routingTable } from "@/router/router-constants/routing-table.js"; import { globalEvents, globalEventTypes } from "@/constants/events"; + +// Heritage integration +import { isSavedSessionStillActive } from "@/helpers/heritage-integration/session-helper"; +import { updateOrCreateFunnelCookie,getFunnelCookie } from "@/helpers/heritage-integration/cookie-helper"; +import { loadOrderIfPresent, saveOrder } from "@/helpers/heritage-integration/order-helper"; +import { getPageToRouteExistingOrderTo, navigateToHeritageFunnel} from "@/helpers/heritage-integration/navigation-helper"; + +import baseMixin from "@/mixins/base-mixin"; import eventBus from "@/helpers/event-bus/event-bus"; import store from "@/store"; // Components import ComponentTest from "@/layouts/component-test/component-test.vue"; -import AddressPOC from "@/layouts/address-poc/address-poc.vue"; import FormTest from "@/layouts/form-test/form-test.vue"; -import NestedRadio from "@/layouts/nested-radio-poc/nested-radio.vue"; -import buttonQuestionExamples from "@/layouts/button-question-examples/button-question-examples.vue" const routes = [ { @@ -26,26 +30,6 @@ const routes = [ name: "FormTest", component: FormTest, }, - { - path: "/address-poc", // This is a temporary route for testing. - name: "AddressPOC", - component: AddressPOC, - }, - { - path: "/form-test", // This is a temporary route for testing. - name: "FormTest", - component: FormTest, - }, - { - path: "/nested-radio", // This is a temporary route for testing. - name: "NestedRadio", - component: NestedRadio, - }, - { - path: "/button-question-examples", // This is a temporary route for testing. - name: "buttonQuestionExamples", - component: buttonQuestionExamples, - }, { path: "/", name: "root", @@ -55,9 +39,35 @@ const routes = [ await GoToFunnelStartOn404(next); } else { try { + + // If the saved session has timed out, clear the session, execute 404 logic. + if (getFunnelCookie() !== null && !isSavedSessionStillActive()) { + await GoToFunnelStartOn404(next); + } + + + + // On entering the funnel "fresh", read cookie information, decide what to do next. + if (from.redirectedFrom === undefined) { + const loadOrderResponse = await loadOrderIfPresent(); + const pageToRedirectTo = await getPageToRouteExistingOrderTo(to, loadOrderResponse); + + // If getPageToRouteExistingOrderTo determines that the return user needs to + // go back to heritage funnel, send them there and stop our current navigation. + if (pageToRedirectTo === 'heritage') { + await navigateToHeritageFunnel(); + return next(false); + } + + // Assign our fmgPage so it will load normally like the other pages. + to.query.fmgPage = pageToRedirectTo; + } + + // Process funnel cookie. + updateOrCreateFunnelCookie(); + // If we already have our route, go to it. if (router.hasRoute(to.query.fmgPage)) { - // Since our route is already in scope, we can grab the component from it and call the arePagePrerequisitesValid function. let component = router.getRoutes().filter((x) => x.name === to.query.fmgPage)[0].components; @@ -123,10 +133,14 @@ router.navigateAfterSave = (scenario, currentRoute, optionalQuery = {}, optional navigate(scenario, currentRoute, true, optionalQuery, optionalParams, optionalPageData); } +router.navigateToExternalUrl = (url, optionalQuery = {}) => { + navigateToUrl(url, optionalQuery); +} + // PRIVATE FUNCTIONS // Navigate to the next route, depending on the scenario. -function navigate(scenario, currentRoute, invalidateOnSave, optionalQuery = {}, optionalParams = {}, optionalPageData = {}) { +async function navigate(scenario, currentRoute, invalidateOnSave, optionalQuery = {}, optionalParams = {}, optionalPageData = {}) { if (!scenario) { console.error("No scenario provided. Please review the routing table."); return; @@ -134,8 +148,9 @@ function navigate(scenario, currentRoute, invalidateOnSave, optionalQuery = {}, // Match our maps up and navigate if we have a destination. const matchingScenarioMap = getNavigationMap(scenario, currentRoute); + const destinationFmgPageValue = matchingScenarioMap.destinationFmgPageValue; - if (matchingScenarioMap.destinationFmgPageValue !== undefined) { + if (destinationFmgPageValue !== undefined) { // We're always pushing the same path, just changing query strings. Make sure our optional query strings get combined with our fmgPage one. // If we need to do invalidation @@ -146,17 +161,22 @@ function navigate(scenario, currentRoute, invalidateOnSave, optionalQuery = {}, } // Append page data to the store for the NEXT page, if any. It will be an empty object if none is provided. - baseMixin.methods.savePageDataToStore(matchingScenarioMap.destinationFmgPageValue, optionalPageData); + baseMixin.methods.savePageDataToStore(destinationFmgPageValue, optionalPageData); + + // if cookie and referralNumber/Date exists + if (getFunnelCookie()?.ReferralNumber && getFunnelCookie()?.ReferralDate) { + await saveOrder(); + } router.push({ name: "root", query: Object.assign(optionalQuery, { - fmgPage: matchingScenarioMap.destinationFmgPageValue, + fmgPage: destinationFmgPageValue, }), - params: optionalParams, + params: optionalParams }); } else if (matchingScenarioMap.destinationUrl !== undefined) { - navigateToUrl(matchingScenarioMap.destinationUrl); + navigateToUrl(matchingScenarioMap.destinationUrl, optionalQuery); } } @@ -177,9 +197,15 @@ function getNavigationMap(scenario, currentRoute) { //---------------------------------------------------------- Private Functions ---------------------------------------------------------- // Navigate to an external url. -function navigateToUrl(url) { +function navigateToUrl(url, optionalQuery = {}) { // possibly show some loading screen in the future here. - window.location.assign(url); + var externalUrl = new URL(url); + + for (const queryKey in optionalQuery) { + externalUrl.searchParams.append(queryKey, optionalQuery[queryKey]); + } + + window.location.assign(externalUrl); } // Get route information by page name. diff --git a/src/router/router-constants/externalUrl-values.js b/src/router/router-constants/externalUrl-values.js new file mode 100644 index 000000000..a5b059480 --- /dev/null +++ b/src/router/router-constants/externalUrl-values.js @@ -0,0 +1,5 @@ +const externalUrls = { + HERITAGE_FUNNEL: process.env.VUE_APP_HERITAGE_FUNNEL, +}; + +export { externalUrls }; \ No newline at end of file diff --git a/src/router/router-constants/navigation-scenarios.js b/src/router/router-constants/navigation-scenarios.js index e55385943..da170da53 100644 --- a/src/router/router-constants/navigation-scenarios.js +++ b/src/router/router-constants/navigation-scenarios.js @@ -4,6 +4,7 @@ const navigationScenarios = { SELECTED_MAKE: "SELECTED_MAKE", SELECTED_STYLE: "SELECTED_STYLE", CLICKED_BACK: "CLICKED_BACK", + CLICKED_FORWARD: "CLICKED_FORWARD", SELECTED_PARTS: "SELECTED_PARTS", SELECTED_DAMAGE_WITH_SINGLE_PART: "SELECTED_DAMAGE_WITH_SINGLE_PART", SELECTED_DAMAGE_WITH_MULTIPLE_PARTS: "SELECTED_DAMAGE_WITH_MULTIPLE_PARTS", diff --git a/src/router/router-constants/router-parameter-keys.js b/src/router/router-constants/router-parameter-keys.js deleted file mode 100644 index 0c75eb738..000000000 --- a/src/router/router-constants/router-parameter-keys.js +++ /dev/null @@ -1,3 +0,0 @@ -const routerParameterKeys = { }; - - export { routerParameterKeys }; \ No newline at end of file diff --git a/src/router/router-constants/routing-table.js b/src/router/router-constants/routing-table.js index c30afcc7e..99583b975 100644 --- a/src/router/router-constants/routing-table.js +++ b/src/router/router-constants/routing-table.js @@ -1,5 +1,6 @@ import { fmgPageValues } from "@/router/router-constants/fmgPage-values"; import { navigationScenarios } from "@/router/router-constants/navigation-scenarios"; +import { applicationConfig } from "@/constants/application-config"; const routingTable = [ { @@ -59,7 +60,7 @@ const routingTable = [ }, { scenario: navigationScenarios.SELECTED_DAMAGE_WITH_SINGLE_PART, - destinationFmgPageValue: fmgPageValues.REVEAL, + destinationFmgPageValue: fmgPageValues.REVEAL }, { scenario: navigationScenarios.SELECTED_DAMAGE_WITH_MULTIPLE_PARTS, @@ -80,6 +81,10 @@ const routingTable = [ }, { scenario: navigationScenarios.SELECTED_PARTS, + destinationFmgPageValue: fmgPageValues.QUOTE, + }, + { + scenario: navigationScenarios.CLICKED_FORWARD, destinationFmgPageValue: fmgPageValues.REVEAL, }, ], diff --git a/src/store/index.js b/src/store/index.js index 18593c897..099e8dbcf 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -1,11 +1,10 @@ import { createStore } from "vuex"; import { endpoints } from "@/constants/endpoints.js"; import { storeMutations } from "@/constants/store-mutations"; +import { getDateForSavedSessionTimeout } from "@/helpers/heritage-integration/session-helper"; import createPersistedState from "vuex-persistedstate"; import globalMethods from "@/global-methods"; - - // Export State export const state = { order: { @@ -29,28 +28,35 @@ export const state = { firstName: null, lastName: null, }, + serviceLocation: { + zip: null, + }, + customer: { + emailAddress: null, + }, + damage: { + isRepair: null, + numberOfChips: null, + glassToReplace: null, + }, + lineItems: { + glassParts: null, + otherParts: null + }, + referralNumber: null, + referralDate: null, + referralCorrelationId: null, + parentAccountNumber: null, }, - serviceLocation: { - zip: null, + applicationUser: { + eventBus: [], + pageData: {}, + savedSessionTimeout: getDateForSavedSessionTimeout() }, - customer: { - emailAddress: null, - }, - damage: { - isRepair: null, - numberOfChips: null, - glassToReplace: null, - }, - lineItems:{ - glassParts: {}, - otherParts: {} - } - }, - applicationUser: { - eventBus: [], - pageData: {} - }, -} + } +}; + +export const state = getDefaultState(); // Export Mutations export const mutations = { @@ -88,46 +94,32 @@ export const mutations = { updateIsRepair(state, isRepair){ state.order.damage.isRepair = isRepair; }, - updateNumberOfChips(state, numberOfChips){ + updateNumberOfChips(state, numberOfChips) { state.order.damage.numberOfChips = numberOfChips; }, - updateGlassToReplace(state, glassToReplace){ + updateGlassToReplace(state, glassToReplace) { state.order.damage.glassToReplace = glassToReplace; }, - updateParts(state, partsData){ + updateParts(state, partsData) { state.order.lineItems.glassParts = partsData; }, - updatePageData(state, pageData){ + updatePageData(state, pageData) { state.applicationUser.pageData[pageData.page] = pageData.data; }, - updateRegistrationAddress(state, registrationAddress){ - state.order.vehicle.registration.address = registrationAddress; + updateReferralCorrelationId(state, referralCorrelationId) { + state.order.referralCorrelationId = referralCorrelationId; }, - updateRegistrationCity(state, registrationCity){ - state.order.vehicle.registration.city = registrationCity; + updateReferralNumber(state, referralNumber) { + state.order.referralNumber = referralNumber; }, - updateRegistrationState(state, registrationState){ - state.order.vehicle.registration.state = registrationState; + updateReferralDate(state, referralDate) { + state.order.referralDate = referralDate; }, - updateRegistrationZipCode(state, registrationZipCode){ - state.order.vehicle.registration.zipCode = registrationZipCode; - }, - updateRegistrationFirstName(state, registrationFirstName){ - state.order.vehicle.registration.firstName = registrationFirstName; - }, - updateRegistrationLastName(state, registrationLastName){ - state.order.vehicle.registration.lastName = registrationLastName; - }, - updateRegistrationLicensePlate(state, registrationLicensePlate){ - state.order.vehicle.registration.licensePlate = registrationLicensePlate; - }, - updateServiceLocationZip(state, serviceLocationZip){ - state.order.vehicle.serviceLocation.zip = serviceLocationZip; - }, - updateCustomerEmailAddress(state, customerEmailAddress){ - state.order.vehicle.customer.emailAddress = customerEmailAddress; + updateParentAcctNumber(state, parentAcctNumber) { + state.order.parentAccountNumber = parentAcctNumber; }, + // EVENT BUS MUTATIONS addEventToBus(state, event) { state.applicationUser.eventBus.push(event); @@ -172,6 +164,33 @@ export const mutations = { resetPartsState(state) { state.order.lineItems.glassParts = null; state.order.lineItems.otherParts = null; + }, + resetState(state) { + Object.assign(state, getDefaultState()); + }, + + // Misc Mutations + updateStateWithOrderInformation(state, orderInformation) { + state.order.referralNumber = orderInformation.referralNumber; + state.order.referralDate = orderInformation.referralDate; + state.order.referralCorrelationId = orderInformation.referralCorrelationId; + state.order.vehicle = { + year: orderInformation.vehicle?.year, + make: orderInformation.vehicle?.make, + model: orderInformation.vehicle?.model, + style: orderInformation.vehicle?.style, + carId: orderInformation.vehicle?.carId, + category: orderInformation.vehicle?.category, + imageUrl: orderInformation.vehicle?.imageUrl, + imageVifNumber: orderInformation.vehicle?.imageVifNumber, + imageColor: orderInformation.vehicle?.imageVifColor + }; + state.order.damage.glassToReplace = orderInformation.glassToReplace; + state.order.damage.isRepair = orderInformation.isRepair; + state.order.damage.numberOfChips = orderInformation.numberOfChips; + state.order.lineItems.glassParts = orderInformation.parts; + state.order.parentAccountNumber = orderInformation.parentAccountNumber; + state.order.serviceLocation.zipCode = orderInformation.zipCode; // TODO CSR-416 Make sure this is correct } } @@ -191,7 +210,9 @@ export const getters = { lineItems: (state) => state.order.lineItems, pageData: (state) => (page) => { return state.applicationUser.pageData[page]; - } + }, + applicationUser: (state) => state.applicationUser, + order: (state) => state.order, } // Export Actions @@ -298,6 +319,9 @@ export const actions = { resetPartsAndDependencies(context) { context.commit(storeMutations.RESET_PARTS_STATE); }, + resetState(context) { + context.commit(storeMutations.RESET_STATE); + }, // Content API Actions getRouteInfo(context, { pageName }) { @@ -330,6 +354,13 @@ export const actions = { }); }, + // Misc Actions + setReferralInformation(context, { referralNumber, referralDate, referralCorrelationId }) { + context.commit(storeMutations.UPDATE_REFERRAL_NUMBER, referralNumber); + context.commit(storeMutations.UPDATE_REFERRAL_DATE, referralDate); + context.commit(storeMutations.UPDATE_REFERRAL_CORRELATION_ID, referralCorrelationId); + }, + // Parts API Actions getPartsOrQuestions(context, { carId, glassArray, zipCode, vin = '' }) { return globalMethods.callHttpClient({ @@ -342,6 +373,46 @@ export const actions = { vin: vin }, }); + }, + + // Order API Actions + saveOrder(context) { + const vehicle = context.getters.vehicle; + const damage = context.getters.damage; + + return globalMethods.callHttpClient({ + method: endpoints.SaveOrder.method, + endpoint: endpoints.SaveOrder.url, + payload: { + vehicle: { + carId: vehicle.carId, + year: vehicle.year, + make: vehicle.make, + model: vehicle.model, + style: vehicle.style, + }, + numberOfChips: damage.numberOfChips, + zipCode: 43215, // TODO CSR-416, should not be hardcoded (state.order.serviceLocation.zipCode) + glassToReplace: damage.glassToReplace, + referralNumber: context.state.order.referralNumber, + referralDate: context.state.order.referralDate + }, + }); + }, + + loadOrder(context, { referralNumber, referralDate, referralCorrelationId }) { + return globalMethods.callHttpClient({ + method: endpoints.LoadOrder.method, + endpoint: endpoints.LoadOrder.url, + payload: { + referralNumber: referralNumber, + referralDate: referralDate, + referralCorrelationId: referralCorrelationId + }, + }).then((response) => { + context.commit(storeMutations.SET_LOAD_FUNNEL_SESSION_INFO, response.data); + return response; + }); } } @@ -357,3 +428,5 @@ export default createStore({ getters, actions, }); + +// Private Functions diff --git a/src/store/store.spec.js b/src/store/store.spec.js index a3f8a5cb5..5c7e0c881 100644 --- a/src/store/store.spec.js +++ b/src/store/store.spec.js @@ -200,6 +200,38 @@ describe("Mutations", () => { expect(storeState.applicationUser.pageData['vehicle-year']).toEqual({}); }); + it("updateStateWithOrderInformation, should set order information in state", () => { + // Arrange + const storeState = state; + + // Act + mutations.updateStateWithOrderInformation(storeState, { + referralNumber: 123, + referralDate: new Date().toUTCString(), + referralCorrelationId: "xxx-xxx-xxx", + vehicle: { + year: "2019", + make: "Acura", + model: "ILX", + style: "4 DOOR SEDAN", + carId: "C0000001", + category: "CAR" + }, + glassToReplace: ["Windshield"], + isRepair: false, + numberOfChips: 0, + parts: [], + parentAccountNumber: "123456789", + }); + + // Assert + expect(storeState.order.referralNumber).toEqual(123); + expect(storeState.order.referralCorrelationId).toEqual("xxx-xxx-xxx"); + expect(storeState.order.vehicle.year).toEqual("2019"); + expect(storeState.order.vehicle.make).toEqual("Acura"); + expect(storeState.order.vehicle.model).toEqual("ILX"); + }); + }); describe("Actions", () => { @@ -466,6 +498,66 @@ describe("Actions", () => { expect(response.data).toEqual({ imageUrl: "https://test.com" }); }); + it("saveOrder action, returns order information", async () => { + // Arrange + const context = state; + + context.getters = { + vehicle: {}, + damage: {}, + }; + context.state = { + order: {} + }; + + globalMethods.callHttpClient.mockImplementation(() => { + return Promise.resolve({ data: { referralNumber: 123 } }); + }); + + // Act + const response = await actions.saveOrder(context); + + // Assert + expect(response.data).toEqual({ referralNumber: 123 }); + }); + + + it("loadOrder action, returns order information, calls mutation", async () => { + // Arrange + const context = state; + + globalMethods.callHttpClient.mockImplementation(() => { + return Promise.resolve({ data: { referralNumber: 123 } }); + }); + + const commit = jest.fn(); + + context.commit = commit; + + // Act + const response = await actions.loadOrder(context, {referralNumber: "123", referralDate: new Date().toUTCString(), referralCorrelationId: "xxx-xxx-xxx"}); + + // Assert + expect(response.data).toEqual({ referralNumber: 123 }); + expect(commit).toBeCalledWith(storeMutations.SET_LOAD_FUNNEL_SESSION_INFO, {"referralNumber": 123}); + }); + + it("setReferralInformation, should call commit three times", () => { + // Arrange + const context = state; + const commit = jest.fn(); + + context.commit = commit; + + // Act + actions.setReferralInformation(context, {referralNumber: "123", referralDate: new Date().toUTCString(), referralCorrelationId: "xxx-xxx-xxx"}); + + // Assert + expect(commit).toBeCalledWith(storeMutations.UPDATE_REFERRAL_NUMBER, "123"); + expect(commit).toBeCalledWith(storeMutations.UPDATE_REFERRAL_DATE, new Date().toUTCString()); + expect(commit).toBeCalledWith(storeMutations.UPDATE_REFERRAL_CORRELATION_ID, "xxx-xxx-xxx"); + }); + }); describe("Getters", () => { diff --git a/src/styles/common-error-styles.scss b/src/styles/common-error-styles.scss index 323430dfe..fe1637e43 100644 --- a/src/styles/common-error-styles.scss +++ b/src/styles/common-error-styles.scss @@ -3,14 +3,19 @@ &.list-card { border: 1px solid $red; color: $red; - label { - box-shadow: 0 0 1px $red !important; - border-radius: .5rem; - } input[type=checkbox]:focus + label, input[type=radio]:focus + label { box-shadow: 0 0 0 2.5px $red; } + input[type=checkbox]:focus + label:hover, + input[type=radio]:focus + label { + box-shadow: 0 0 0 1px $red; + } + &:hover { + box-shadow: 0px 0px 0px 4px $red-200; + border-radius: 10px !important; + border: 1px solid $red; + } } input[type=checkbox]:checked + label, @@ -21,6 +26,9 @@ color: $red; label { border: 1px solid $red; + &:hover { + box-shadow: 0px 0px 0px 4px $red-200; + } } input[type=checkbox]:focus + label, input[type=radio]:focus + label { diff --git a/src/ux-components/button-main/button-main.vue b/src/ux-components/button-main/button-main.vue index 84ed98a1a..df422234d 100644 --- a/src/ux-components/button-main/button-main.vue +++ b/src/ux-components/button-main/button-main.vue @@ -61,6 +61,9 @@ export default { @media (hover: hover) { background: linear-gradient(270deg, $blue-500 0%, $blue-700 100%); } + &:focus { + box-shadow: 0 0 0 3px, 0 0 0 5.5px $blue-700; + } &:focus, // Mouse, touch, stylus focus &:focus-visible { // Keyboard focus for accessibility @@ -91,6 +94,7 @@ export default { &.has-loader { color: $white; background: $blue-700; + box-shadow: 0 0 0 3px, 0 0 0 5.5px $blue-700; } &.delay {// fixes flicker while transitioning between states transition: background 0s 0s ease-in-out; diff --git a/src/ux-components/list-button-horizontal/list-button-horizontal.vue b/src/ux-components/list-button-horizontal/list-button-horizontal.vue index 68aceec51..cef3d765c 100644 --- a/src/ux-components/list-button-horizontal/list-button-horizontal.vue +++ b/src/ux-components/list-button-horizontal/list-button-horizontal.vue @@ -161,6 +161,7 @@ export default { } } label { + outline: none; position: relative; background: $white; transition: all 150ms linear; diff --git a/src/ux-components/list-button/list-button.vue b/src/ux-components/list-button/list-button.vue index caef528de..775b19ff4 100644 --- a/src/ux-components/list-button/list-button.vue +++ b/src/ux-components/list-button/list-button.vue @@ -128,9 +128,9 @@ export default { fieldOptions['initialValue'] = fieldOptions.potentialInitialValue; } - const { - checked, - handleChange, + const { + checked, + handleChange, errors, } = useField(props.groupName, props.validationRules, fieldOptions); @@ -147,6 +147,7 @@ export default {