Merge pull request #2208 from Safelite/feature/CASH-77

Feature/cash-77 - all the syp reverted code readded plus new bug fixes
This commit is contained in:
AdamCaouetteSafelite 2025-01-16 09:20:02 -05:00 committed by GitHub
commit 8f8965d808
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 941 additions and 24 deletions

View file

@ -124,6 +124,10 @@ const endpoints = {
url: "/order/api/v1/order/load-session",
method: "POST",
},
SaveQuote: {
url: "/order/api/v1/order/initiate-saved-progress-email",
method: "POST",
},
GetSignature: {
url: "/order/api/v1/order/sign",
method: "POST",

View file

@ -37,6 +37,7 @@ const storeActions = {
GET_MOBILE_PREMIUM_FEE: "getMobilePremiumFee",
SAVE_SESSION: "saveSession",
LOAD_SESSION: "loadSession",
SAVE_QUOTE: "saveQuote",
UPDATE_STORE_WITH_SAVE_SESSION_RESPONSE: "updateStoreWithSaveSessionResponse",
VALIDATE_ZIP: "validateZip",
PRICE_ORDER_ITEMS_AND_SAVE_SERVER_DATA: "priceOrderItemsAndSaveServerData",

View file

@ -2,7 +2,9 @@
<!-- Modal -->
<div
class="modal fade modal-component"
:class="modalId"
v-on="{ 'hidden.bs.modal': onModalClosed, 'shown.bs.modal': onModalOpened }"
:data-bs-backdrop="backdropSetting"
:id="modalId"
tabindex="-1"
aria-labelledby="ModalComponentLabel"
@ -25,17 +27,17 @@
</div>
<div class="modal-body ps-5 pe-5 pb-4 pt-1">
<slot></slot>
</div>
<div class="modal-footer px-5 py-4">
<modalButtonMain
isPrimary
class="w-100"
id="modalbtn"
ref="modalButtonMain"
loaderColor="white"
:buttonText="footerButtonText"
@click-event="validateAndEmit"
:class="isFooterButtonDisabled && 'form-test-invalid'" />
<div class="modal-footer">
<modalButtonMain
isPrimary
class="w-100"
:id="modalId + '-modalbtn'"
ref="modalButtonMain"
loaderColor="white"
:buttonText="footerButtonText"
@click-event="validateAndEmit"
:class="isFooterButtonDisabled && 'form-test-invalid'" />
</div>
</div>
</div>
</div>
@ -53,6 +55,8 @@ export default {
props: {
headerText: String,
footerButtonText: String,
suppressPageScroll: Boolean,
staticBackdrop: Boolean,
onModalOpenedCallback: {
type: Function,
},
@ -91,7 +95,7 @@ export default {
e.preventDefault();
if (this.isEnterClicked) return;
this.isEnterClicked = true;
document.getElementById("modalbtn")?.focus();
document.getElementById(this.modalId + "-modalbtn")?.focus();
this.$refs.modalButtonMain.clicked();
document.getElementById(this.modalId)?.focus();
setTimeout(() => {
@ -107,8 +111,12 @@ export default {
onModalClosed() {
this.resetButtonStyle();
this.onModalClosedCallback?.();
if (this.suppressPageScroll)
document.querySelector("body").classList.remove("prevent-modal-scroll");
},
openModal() {
if (this.suppressPageScroll)
document.querySelector("body").classList.add("prevent-modal-scroll");
const modal = Modal.getOrCreateInstance(document.getElementById(this.modalId));
modal.show();
this.$emit("isModalOpened", true);
@ -126,6 +134,9 @@ export default {
}
return !this.meta.dirty || !this.meta.valid;
},
backdropSetting() {
return this.staticBackdrop ? "static" : "true";
},
},
components: {
modalButtonMain,
@ -162,6 +173,7 @@ export default {
}
}
.modal-footer {
padding: 1rem 0;
position: sticky;
width: 100%;
bottom: 0;
@ -231,6 +243,57 @@ export default {
}
}
}
.save-progress-modal-question {
p.modal-body {
padding: 0;
}
.modal-body {
display: flex;
flex-direction: column;
.textbox-question {
padding: 0;
margin: 0 0 1.5rem 0;
}
}
}
.save-progress-modal-question,
.save-progress-popup-question {
p.modal-body {
padding: 0;
}
.modal-body {
display: flex;
flex-direction: column;
.textbox-question {
padding: 0;
label {
text-align: left;
font-weight: 900;
}
}
}
.modal-body-inner {
text-align: center;
margin-bottom: 1.5rem;
}
.modal-disclaimer {
font-size: 0.75rem;
order: 2;
a {
text-decoration: none;
}
}
.modal-footer {
margin: 0 0 1.5rem 0;
padding-top: 0;
padding-bottom: 0;
}
}
body {
.modal-backdrop {
height: 100%;

View file

@ -25,7 +25,7 @@
</div>
</div>
<div class="row justify-content-center">
<div class="col-md-6 col-xl-4">
<div class="col-md-6 col-xl-4 question-chain-wrapper">
<div v-for="(questionsDatum, i) in questionsData" :key="questionsDatum.key">
<questionChain
ref="questionChain"
@ -37,6 +37,8 @@
:validationRules="validationRules" />
</div>
<slot></slot>
<navbar
ref="navbar"
cmsWidgetName="FunnelFooterWidget"
@ -112,5 +114,13 @@ export default {
text-align: left;
}
}
.question-chain-wrapper {
display: flex;
flex-direction: column;
.nav-bar {
order: 1;
}
}
}
</style>

View file

@ -0,0 +1,51 @@
import { mount, shallowMount } from "@vue/test-utils";
import saveProgressModalQuestion from "./save-progress-modal-question";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
jest.mock("@/digital-components/modal/modal", () => ({
methods: {
openModal: jest.fn(),
resetButtonStyle: jest.fn(),
},
}));
describe("save-progress-modal-question ", () => {
describe("when openModal is run ", () => {
test("the modal should open ", () => {
// Arrange
const { wrapper } = setupMocks({
props: {
modelValue: "",
modalWidgetName: "testModal",
},
});
// Act
wrapper.vm.openModal();
// Assert
expect(wrapper.vm.modal).not.toBeNull();
});
});
});
function setupMocks({ options, props }) {
const mountOptions = getMountOptions({
...options,
});
const mockBaseMixin = {
methods: {
getCmsContent: jest.fn(),
dispatchStoreAction: jest.fn(),
},
};
if (props) mountOptions.propsData = props;
mountOptions.global.mixins = [mockBaseMixin];
const wrapper = mount(saveProgressModalQuestion, mountOptions);
return { wrapper };
}

View file

@ -0,0 +1,197 @@
<template>
<div class="save-progress-modal-question" :class="isProgressSaved ? 'progress-saved' : ''">
<buttonMain
ref="buttonMain"
type="button"
v-if="!isProgressSaved"
:buttonText="buttonText"
loaderColor="white"
:suppressLoader="true"
class="save-progress-button"
@click-event="openModal" />
<alert
class="my-4"
v-else
cmsWidgetName="SaveProgressModalAlertWidget"
alertClass="alert-success" />
<modal
:ref="modalName"
:headerText="modalHeaderText"
suppressPageScroll
:onModalClosedCallback="onModalClosed"
:footerButtonText="modalButtonText"
@footer-button-event="saveProgress">
<p class="modal-body-inner">{{ modalBodyText }}</p>
<saveProgressQuestion
ref="saveProgressQuestion"
v-model="userInput"
cmsWidgetName="SaveProgressQuestionWidget" />
<p class="modal-disclaimer" v-html="modalDisclaimerText"></p>
</modal>
</div>
</template>
<script>
import saveProgressQuestion from "@/fmg-components/save-progress-modal-question/save-progress-question/save-progress-question";
import modal from "@/digital-components/modal/modal";
import { storeActions } from "@/constants/store-actions.js";
import buttonMain from "@/ux-components/button-main/button-main";
import alert from "@/ux-components/alert/alert";
export default {
name: "save-progress-modal-question",
data() {
return {
userInput: "",
isProgressSaved: false,
};
},
props: {
modelValue: Object,
modalWidgetName: String,
pageName: String,
},
computed: {
buttonText() {
return this.getCmsContent(this.modalWidgetName, "SubheaderText");
},
modalHeaderText() {
return this.getCmsContent(this.modalWidgetName, "HeaderText");
},
modalBodyText() {
return this.getCmsContent(this.modalWidgetName, "BodyText");
},
modalButtonText() {
return this.getCmsContent(this.modalWidgetName, "FooterText");
},
modalDisclaimerText() {
return this.getCmsContent(this.modalWidgetName, "FooterText2");
},
modalName() {
return this.modalWidgetName;
},
modal() {
return this.$refs[this.modalName];
},
},
methods: {
openModal() {
this.modal.openModal();
this.modal.resetButtonStyle();
},
onModalClosed() {
this.userInput = "";
this.modal.resetForm();
},
async saveProgress() {
// save email address to store
await this.dispatchStoreAction(storeActions.SAVE_EMAIL, this.userInput, false);
// hide save progress button; show success message alert
this.isProgressSaved = true;
this.modal.closeModal();
// send store call to send to new API (that triggers an email send)
this.$emit("save-progress");
},
},
components: {
modal,
buttonMain,
saveProgressQuestion,
alert,
},
};
</script>
<style lang="scss" scoped>
.save-progress-modal-question {
margin-bottom: 1.5rem;
order: 2;
text-align: center;
background: $blue-100;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
border-radius: 0.5rem;
&.progress-saved {
background: none;
}
.save-progress-button {
order: 4;
position: relative;
color: $blue;
border: none;
font-weight: 900;
font-size: 0.875rem;
text-align: center;
display: inline-block;
width: auto;
height: auto;
text-decoration: underline;
text-underline-offset: 0.25rem;
line-height: 1rem;
padding: 1rem;
margin: 0 auto;
transition: all 150ms linear;
background: none;
&:hover,
&:active,
&:focus,
&:focus-visible {
color: $blue-700;
background: none;
box-shadow: none;
}
&.delay {
// fixes flicker while transitioning between states
transition: background 0s 0s ease-in-out;
}
}
:deep(.alert) {
border-radius: $border-radius-lg;
border: 1px solid $green;
margin-top: 0;
}
:deep(.alert-heading) {
position: relative;
font-weight: 600;
font-size: 1rem;
color: $black;
text-align: left;
padding-left: 1.5rem;
&::before {
position: absolute;
content: "";
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3E%3Cpath fill='%230C7E47' d='M8 0a8 8 0 1 0 0 16A8 8 0 0 0 8 0Zm3.7 6.171-4.449 4.45a.559.559 0 0 1-.8 0l-2.16-2.16a.563.563 0 0 1 .792-.8l1.76 1.76 4.066-4.042a.563.563 0 1 1 .792.8v-.008Z'/%3E%3C/svg%3E");
width: 1rem;
height: 1rem;
top: 6px;
left: 0;
}
}
}
.applied-promo {
font-size: 0.875rem;
font-weight: 500;
line-height: 24px;
color: $green;
.promo-code {
text-transform: uppercase;
}
}
.alert {
color: $red;
font-size: 0.875rem;
font-weight: 500;
line-height: 24px;
}
</style>

View file

@ -0,0 +1,40 @@
import { shallowMount } from "@vue/test-utils";
import saveProgressQuestion from "./save-progress-question";
describe("save-progress-question.vue", () => {
it("Should get the modelValue", async () => {
// Arrange
const text = "test";
const wrapper = shallowMount(saveProgressQuestion, {
props: {
modelValue: text,
},
attachTo: document.body,
});
// Act
const modelValueText = wrapper.vm.value;
wrapper.vm.value = "test also";
// Assert
expect(modelValueText).toEqual("test");
});
it("Should emit to set value", async () => {
// Arrange
const text = "test";
const wrapper = shallowMount(saveProgressQuestion, {
props: {
modelValue: text,
},
attachTo: document.body,
});
// Act
const modelValueText = wrapper.vm.value;
wrapper.vm.value = "test also";
// Assert
expect(wrapper.emitted("update:modelValue")).toEqual([["test also"]]);
});
});

View file

@ -0,0 +1,62 @@
<template>
<textboxQuestion
ref="saveProgressInputTextQuestion"
:cmsWidgetName="cmsWidgetName"
v-model="value"
inputId="saveProgressUserInput"
questionAlignment="center"
cornerStyle="rounded"
:displayQuestionText="true"
isRequired
@input="validateEmail"
validationRules="email-address-required|email-address-format" />
</template>
<script>
import textboxQuestion from "@/digital-components/textbox-question/textbox-question";
//Supporting Files
import { defineRule } from "vee-validate";
import { required, regex } from "@/helpers/validation-rules";
import { errorMessages } from "@/constants/error-messages";
// Define Validation Rules
defineRule("email-address-required", required(errorMessages.EMAIL_ADDRESS_REQUIRED));
defineRule(
"email-address-format",
regex(
/^([a-zA-Z0-9_\-.+]+)@([a-zA-Z0-9-]+)\.([a-zA-Z]{2,})$/,
errorMessages.EMAIL_ADDRESS_FORMAT
)
);
const EMAIL_REGEX = /[^a-zA-Z0-9_\-.+@]/g;
export default {
name: "save-progress-question",
props: {
modelValue: String,
cmsWidgetName: String,
},
computed: {
value: {
get: function () {
return this.modelValue;
},
set: function (newValue) {
this.$emit("update:modelValue", newValue);
},
},
},
methods: {
validateEmail() {
setTimeout(() => {
this.value = this.value.replace(EMAIL_REGEX, "");
});
},
},
components: {
textboxQuestion,
},
};
</script>

View file

@ -0,0 +1,51 @@
import { mount, shallowMount } from "@vue/test-utils";
import saveProgressPopupQuestion from "./save-progress-popup-question";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
jest.mock("@/digital-components/modal/modal", () => ({
methods: {
closeModal: jest.fn(),
resetButtonStyle: jest.fn(),
},
}));
describe("save-progress-popup-question ", () => {
describe("when closeModal is run ", () => {
test("the modal should close ", () => {
// Arrange
const { wrapper } = setupMocks({
props: {
modelValue: "",
modalWidgetName: "testModal",
},
});
// Act
wrapper.vm.closeModal();
// Assert
expect(wrapper.vm.modal).not.toBeNull();
});
});
});
function setupMocks({ options, props }) {
const mountOptions = getMountOptions({
...options,
});
const mockBaseMixin = {
methods: {
getCmsContent: jest.fn(),
dispatchStoreAction: jest.fn(),
},
};
if (props) mountOptions.propsData = props;
mountOptions.global.mixins = [mockBaseMixin];
const wrapper = mount(saveProgressPopupQuestion, mountOptions);
return { wrapper };
}

View file

@ -0,0 +1,228 @@
<template>
<div class="save-progress-popup-question" :class="[isProgressSaved ? 'progress-saved' : '']">
<modal
:ref="modalName"
:headerText="modalHeaderText"
suppressPageScroll
:staticBackdrop="true"
:onModalClosedCallback="onModalClosed"
:footerButtonText="modalButtonText"
@footer-button-event="saveProgress">
<p class="modal-body-inner">{{ modalBodyText }}</p>
<saveProgressQuestion
ref="saveProgressQuestion"
v-model="userInput"
cmsWidgetName="SaveProgressPopupQuestionWidget"
class="save-progress-popup-question" />
<buttonMain
ref="skipButton"
:buttonText="buttonText"
loaderColor="white"
:suppressLoader="true"
class="skip-button"
@click-event="closeModal" />
<alert
class="my-4"
v-if="isProgressSaved"
cmsWidgetName="SaveProgressModalAlertWidget"
alertClass="alert-success" />
<p class="modal-disclaimer" v-html="modalDisclaimerText"></p>
</modal>
</div>
</template>
<script>
import saveProgressQuestion from "@/fmg-components/save-progress-modal-question/save-progress-question/save-progress-question";
import modal from "@/digital-components/modal/modal";
import { storeActions } from "@/constants/store-actions.js";
import buttonMain from "@/ux-components/button-main/button-main";
import alert from "@/ux-components/alert/alert";
export default {
name: "save-progress-popup-question",
data() {
return {
userInput: "",
isProgressSaved: false,
};
},
mounted() {
if (this.showSaveProgressPopup) this.modal.openModal();
},
emits: ["close-save-progress-popup", "save-progress"], // <--- should remove oodles of warnings in dev tools
props: {
modelValue: Object,
modalWidgetName: String,
pageName: String,
showSaveProgressPopup: Boolean,
},
computed: {
buttonText() {
return this.isProgressSaved
? "Close"
: this.getCmsContent(this.modalWidgetName, "BodyText2");
},
modalHeaderText() {
return this.getCmsContent(this.modalWidgetName, "HeaderText");
},
modalBodyText() {
return this.getCmsContent(this.modalWidgetName, "BodyText");
},
modalButtonText() {
return this.getCmsContent(this.modalWidgetName, "FooterText");
},
modalDisclaimerText() {
return this.getCmsContent(this.modalWidgetName, "FooterText2");
},
modalName() {
return this.modalWidgetName;
},
modal() {
return this.$refs[this.modalName];
},
},
methods: {
closeModal() {
this.modal.closeModal();
},
onModalClosed() {
// send call to close popup
this.$emit("close-save-progress-popup");
},
async saveProgress() {
// save email address to store
await this.dispatchStoreAction(storeActions.SAVE_EMAIL, this.userInput, false);
// hide save progress button; show success message alert
this.isProgressSaved = true;
// send store call to send to new API (that triggers an email send)
this.$emit("save-progress");
// send call to close popup
this.closeModal();
this.$emit("close-save-progress-popup");
},
},
components: {
modal,
buttonMain,
saveProgressQuestion,
alert,
},
};
</script>
<style lang="scss">
.save-progress-popup-question {
order: 2;
margin-bottom: 1.5rem;
.modal-header {
padding-top: 1rem;
.modal-title {
padding-top: 4.25rem;
}
.modal-title::before {
position: absolute;
content: "";
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 63 62'%3E%3Cpath fill='%23B3B4B5' d='M31.5 62c17.052 0 30.876-1.503 30.876-3.358 0-1.854-13.824-3.358-30.876-3.358S.624 56.788.624 58.642C.624 60.497 14.448 62 31.5 62Z' opacity='.4'/%3E%3Cpath fill='%23DA291C' stroke='%23000' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m33.358 52.715-4.574 4.573-24.192-24.2 4.491-4.491 24.275 24.118Z'/%3E%3Cpath fill='%23fff' stroke='%23000' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M58.408 13.402 55.61 30.469 33.356 52.715 9.083 28.597 31.411 6.269l17.067-2.797 9.93 9.93Z'/%3E%3Cpath fill='%23fff' stroke='%23000' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M45.586 19.907a3.618 3.618 0 1 0 0-7.237 3.618 3.618 0 0 0 0 7.237Z'/%3E%3C/svg%3E");
width: 3.875rem;
height: 3.875rem;
top: 2.5rem;
left: calc(50% - 3.875rem / 2);
}
}
.skip-button {
order: 4;
position: relative;
color: $black;
border: none;
font-weight: 900;
font-size: 0.875rem;
text-align: center;
display: inline-block;
width: auto;
height: auto;
text-decoration: underline;
text-underline-offset: 0.25rem;
line-height: 1rem;
padding: 0 0 4px 0;
margin: 0 auto 1.5rem auto;
transition: all 150ms linear;
background: none;
&:hover,
&:active,
&:focus,
&:focus-visible {
color: $blue-700;
background: none;
box-shadow: none;
}
&.delay {
// fixes flicker while transitioning between states
transition: background 0s 0s ease-in-out;
}
}
.alert {
border-radius: $border-radius-lg;
border: 1px solid $green;
order: 2;
margin: 0 0 1.5rem 0;
}
.alert-heading {
position: relative;
font-weight: 600;
font-size: 1rem;
color: $black;
text-align: left;
padding-left: 1.5rem;
&::before {
position: absolute;
content: "";
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3E%3Cpath fill='%230C7E47' d='M8 0a8 8 0 1 0 0 16A8 8 0 0 0 8 0Zm3.7 6.171-4.449 4.45a.559.559 0 0 1-.8 0l-2.16-2.16a.563.563 0 0 1 .792-.8l1.76 1.76 4.066-4.042a.563.563 0 1 1 .792.8v-.008Z'/%3E%3C/svg%3E");
width: 1rem;
height: 1rem;
top: 6px;
left: 0;
}
}
.modal-footer {
order: 3;
padding: 0;
}
.modal-disclaimer {
order: 4;
}
&.progress-saved {
.modal-footer,
.save-progress-popup-question {
display: none;
}
}
}
.applied-promo {
font-size: 0.875rem;
font-weight: 500;
line-height: 24px;
color: $green;
.promo-code {
text-transform: uppercase;
}
}
.alert {
color: $red;
font-size: 0.875rem;
font-weight: 500;
line-height: 24px;
}
</style>

View file

@ -98,6 +98,25 @@ export async function submitWorkOrder({
});
}
export async function saveQuote({ pageNameToLog }) {
await baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.SAVE_QUOTE,
{},
pageNameToLog,
false
);
// save session back to sv2 so the email gets saved just in case they refresh or step away and come back later
await saveSession({
pageNameToLog: pageNameToLog,
shouldAwaitSaveSessionQueue: true,
submitAfterSave: false,
createUnscheduledStatusWorkOrderForPIA: false,
});
return;
}
// PRIVATE FUNCTIONS //
/*

View file

@ -12,13 +12,20 @@
@forwardButtonAction="forwardButtonAction"
@back-click="navigateBack"
:key="currentGlassIndex"
:index="currentGlassIndex" />
:index="currentGlassIndex">
<!-- save progress goes into <slot> on questions-page-layout -->
<saveProgressModalQuestion
modalWidgetName="SaveProgressModalWidget"
v-if="!isEmailInStoreOnPageLoad"
@save-progress="saveProgress" />
</questions-page-layout>
</Form>
</template>
<script>
// Components
import questionsPageLayout from "@/fmg-components/layouts/questions-page-layout/questions-page-layout";
import saveProgressModalQuestion from "@/fmg-components/save-progress-modal-question/save-progress-modal-question";
// Supporting Files
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
@ -31,6 +38,7 @@ import { required } from "@/helpers/validation-rules";
import { errorMessages } from "@/constants/error-messages";
import vehicleQuestionsMixin from "@/mixins/vehicle-questions-mixin";
import baseMixin from "@/mixins/base-mixin.js";
import { saveQuote } from "@/helpers/heritage-integration/order-helper.js";
// DEFINE VALIDATION RULES
defineRule("questions-required", required(errorMessages.OPTION_REQUIRED));
@ -52,9 +60,12 @@ export default {
const resultMap = await settleAllPromises(promiseResultMap);
const emailFromStore = store.getters.order.customer.emailAddress;
// Call the "next" function to complete the transition to this page.
next(async (vm) => {
vm.setCmsContent(resultMap.cmsContent);
vm.isEmailInStoreOnPageLoad = emailFromStore?.length > 0;
if (store.getters.externalParameterState?.isExternalParameter) {
if (store.getters.externalParameterServiceZip.zipCode) {
await baseMixin.methods.dispatchStoreAction(
@ -76,6 +87,7 @@ export default {
questionsData: null,
selectedAnswers: {},
currentGlassIndex: 0,
isEmailInStoreOnPageLoad: null,
};
},
computed: {
@ -145,6 +157,9 @@ export default {
this.questionsData
);
},
saveProgress() {
saveQuote({ pageNameToLog: "capability-questions" });
},
async forwardButtonAction() {
const questionAnswersArray = this.questionsData.map((glass) => {
// get answerResult2 of returned answer
@ -205,6 +220,7 @@ export default {
components: {
Form,
questionsPageLayout,
saveProgressModalQuestion,
},
};
</script>

View file

@ -12,13 +12,20 @@
@forwardButtonAction="forwardButtonAction"
@back-click="navigateBack"
:key="currentGlassIndex"
:index="currentGlassIndex" />
:index="currentGlassIndex">
<!-- save progress goes into <slot> on questions-page-layout -->
<saveProgressModalQuestion
modalWidgetName="SaveProgressModalWidget"
v-if="!isEmailInStoreOnPageLoad"
@save-progress="saveProgress" />
</questions-page-layout>
</Form>
</template>
<script>
// Components
import questionsPageLayout from "@/fmg-components/layouts/questions-page-layout/questions-page-layout";
import saveProgressModalQuestion from "@/fmg-components/save-progress-modal-question/save-progress-modal-question";
// Supporting Files
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
@ -31,6 +38,7 @@ import { required } from "@/helpers/validation-rules";
import { errorMessages } from "@/constants/error-messages";
import vehicleQuestionsMixin from "@/mixins/vehicle-questions-mixin";
import baseMixin from "@/mixins/base-mixin.js";
import { saveQuote } from "@/helpers/heritage-integration/order-helper.js";
// DEFINE VALIDATION RULES
defineRule("questions-required", required(errorMessages.OPTION_REQUIRED));
@ -52,9 +60,12 @@ export default {
const resultMap = await settleAllPromises(promiseResultMap);
const emailFromStore = store.getters.order.customer.emailAddress;
// Call the "next" function to complete the transition to this page.
next(async (vm) => {
vm.setCmsContent(resultMap.cmsContent);
vm.isEmailInStoreOnPageLoad = emailFromStore?.length > 0;
if (store.getters.externalParameterState?.isExternalParameter) {
if (store.getters.externalParameterServiceZip.zipCode) {
await baseMixin.methods.dispatchStoreAction(
@ -77,6 +88,7 @@ export default {
questionsData: null,
selectedAnswers: {},
currentGlassIndex: 0,
isEmailInStoreOnPageLoad: null,
};
},
computed: {
@ -145,6 +157,9 @@ export default {
this.questionsData
);
},
saveProgress() {
saveQuote({ pageNameToLog: "molding-questions" });
},
async forwardButtonAction() {
const questionAnswersArray = this.questionsData.map((glass) => {
return {
@ -189,6 +204,7 @@ export default {
components: {
Form,
questionsPageLayout,
saveProgressModalQuestion,
},
};
</script>

View file

@ -12,13 +12,20 @@
@forwardButtonAction="forwardButtonAction"
@back-click="navigateBack"
:key="currentGlassIndex"
:index="currentGlassIndex" />
:index="currentGlassIndex">
<!-- save progress goes into <slot> on questions-page-layout -->
<saveProgressModalQuestion
modalWidgetName="SaveProgressModalWidget"
v-if="!isEmailInStoreOnPageLoad"
@save-progress="saveProgress" />
</questions-page-layout>
</Form>
</template>
<script>
// Components
import questionsPageLayout from "@/fmg-components/layouts/questions-page-layout/questions-page-layout";
import saveProgressModalQuestion from "@/fmg-components/save-progress-modal-question/save-progress-modal-question";
// Supporting Files
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
@ -31,6 +38,7 @@ import { required } from "@/helpers/validation-rules";
import { errorMessages } from "@/constants/error-messages";
import vehicleQuestionsMixin from "@/mixins/vehicle-questions-mixin";
import baseMixin from "@/mixins/base-mixin.js";
import { saveQuote } from "@/helpers/heritage-integration/order-helper.js";
// DEFINE VALIDATION RULES
defineRule("questions-required", required(errorMessages.OPTION_REQUIRED));
@ -52,9 +60,12 @@ export default {
const resultMap = await settleAllPromises(promiseResultMap);
const emailFromStore = store.getters.order.customer.emailAddress;
// Call the "next" function to complete the transition to this page.
next(async (vm) => {
vm.setCmsContent(resultMap.cmsContent);
vm.isEmailInStoreOnPageLoad = emailFromStore?.length > 0;
if (store.getters.externalParameterState?.isExternalParameter) {
if (store.getters.externalParameterServiceZip.zipCode) {
const serviceState = store.getters.order.serviceLocation.state;
@ -78,6 +89,7 @@ export default {
questionsData: null,
selectedAnswers: {},
currentGlassIndex: 0,
isEmailInStoreOnPageLoad: null,
};
},
computed: {
@ -144,6 +156,9 @@ export default {
this.questionsData
);
},
saveProgress() {
saveQuote({ pageNameToLog: "part-questions" });
},
async forwardButtonAction() {
const questionAnswersArray = this.questionsData.map((glass) => {
return {
@ -185,6 +200,7 @@ export default {
components: {
Form,
questionsPageLayout,
saveProgressModalQuestion,
},
};
</script>

View file

@ -121,6 +121,9 @@ store.getters = {
isInsurance: false,
parentAccountNumber: applicationConfig.CASH_PARENT_ACCOUNT_NUMBER,
},
customer: {
emailAddress: "test@test.com",
},
},
};
@ -136,6 +139,9 @@ afterEach(() => {
insuranceCoverage: {},
isInsurance: false,
},
customer: {
emailAddress: "test@test.com",
},
};
});
@ -263,6 +269,9 @@ describe("quote.vue", () => {
glassParts: ["item", "item2"],
},
payment: {},
customer: {
emailAddress: "test@test.com",
},
serviceLocation: {
zipCode: "12345",
zipCodeCtu: "value",
@ -309,6 +318,9 @@ describe("quote.vue", () => {
glassParts: ["item", "item2"],
},
payment: {},
customer: {
emailAddress: "test@test.com",
},
serviceLocation: {
zipCode: "12345",
zipCodeCtu: "value",
@ -355,6 +367,9 @@ describe("quote.vue", () => {
payment: {
isInsurance: null,
},
customer: {
emailAddress: "test@test.com",
},
serviceLocation: {
state: null,
},
@ -396,6 +411,9 @@ describe("quote.vue", () => {
payment: {
isInsurance: null,
},
customer: {
emailAddress: "test@test.com",
},
serviceLocation: {
state: null,
},
@ -438,6 +456,9 @@ describe("quote.vue", () => {
payment: {
isInsurance: true,
},
customer: {
emailAddress: "test@test.com",
},
serviceLocation: {
state: null,
},
@ -480,6 +501,9 @@ describe("quote.vue", () => {
payment: {
isInsurance: false,
},
customer: {
emailAddress: "test@test.com",
},
serviceLocation: {
state: null,
},
@ -523,6 +547,9 @@ describe("quote.vue", () => {
payment: {
isInsurance: null, // Ensure that previous selection isn't overriding selection
},
customer: {
emailAddress: "test@test.com",
},
serviceLocation: {
state: null,
},
@ -568,6 +595,9 @@ describe("quote.vue", () => {
payment: {
isInsurance: null, // Ensure that previous selection isn't overriding selection
},
customer: {
emailAddress: "test@test.com",
},
serviceLocation: {
state: null,
},
@ -612,6 +642,9 @@ describe("quote.vue", () => {
payment: {
isInsurance: null,
},
customer: {
emailAddress: "test@test.com",
},
serviceLocation: {
state: "AZ",
},
@ -717,6 +750,9 @@ describe("quote.vue", () => {
isInsurance: true,
inactivePromos: [],
},
customer: {
emailAddress: "test@test.com",
},
},
externalParameterState: { isExternalParameter: 1 },
externalParameterQuote: {

View file

@ -1,8 +1,10 @@
<template>
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm" v-slot="{ meta }">
<loadingModal ref="loadingModal" />
<div class="container-fluid page-container-grouped-styles">
<div class="row justify-content-center">
<div
class="container-fluid page-container-grouped-styles quote-page"
:class="showSaveProgressPopup ? 'show-save-progress-popup' : ''">
<div class="row justify-content-center funnel-header-wrapper">
<div class="col-md-6">
<funnelHeader cmsWidgetName="FunnelHeaderWidget" ref="funnelHeader" />
</div>
@ -77,6 +79,12 @@
@back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction" />
<saveProgressModalQuestion
modalWidgetName="SaveProgressModalWidget"
modalName="SaveProgressModal"
v-if="!isEmailInStoreOnPageLoad"
@save-progress="saveProgress" />
<textBlock
cmsWidgetName="quoteDisclaimer"
justifyText="center"
@ -84,6 +92,18 @@
class="mb-5 quote-disclaimer" />
</div>
</div>
<div class="row justify-content-center save-progress-popup">
<div class="col-md-6 col-xl-4">
<saveProgressPopupQuestion
modalWidgetName="SaveProgressPopupWidget"
modalName="SaveProgressPopup"
v-if="!isEmailInStoreOnPageLoad && showSaveProgressPopup"
:showSaveProgressPopup="showSaveProgressPopup"
@save-progress="saveProgress"
@close-save-progress-popup="closeSaveProgressPopup" />
</div>
</div>
</div>
</Form>
</template>
@ -100,6 +120,8 @@ import contentGroupModal from "@/fmg-components/content-group-modal/content-grou
import loadingModal from "@/fmg-components/loading-modal/loading-modal.vue";
import afterpayModalBanner from "@/layouts/quote/afterpay-modal-banner/afterpay-modal-banner";
import recalDisclaimer from "@/layouts/quote/recal-disclaimer/recal-disclaimer.vue";
import saveProgressModalQuestion from "@/fmg-components/save-progress-modal-question/save-progress-modal-question";
import saveProgressPopupQuestion from "@/fmg-components/save-progress-popup-question/save-progress-popup-question";
// Supporting files
import baseMixin from "@/mixins/base-mixin.js";
@ -134,6 +156,7 @@ import { packageNames } from "@/constants/package-names";
import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
import { containsRecalParts } from "@/helpers/recal-helper";
import { externalParameterStatus } from "@/constants/external-parameters";
import { saveQuote } from "@/helpers/heritage-integration/order-helper.js";
defineRule("option-required", required(errorMessages.OPTION_REQUIRED));
@ -183,6 +206,10 @@ export default {
];
const resultMap = await settleAllPromises(promiseResultMap);
const emailFromStore = store.getters.order.customer.emailAddress;
const isEmailInStoreOnPageLoad = emailFromStore?.length > 0;
const showSaveProgressPopup = isEmailInStoreOnPageLoad ? false : true;
const lineItems = deepClone(store.getters.order.lineItems);
lineItems.vaps = lineItems.vaps ?? [];
const nullSafeGlassParts = lineItems.glassParts ?? [];
@ -263,6 +290,8 @@ export default {
// Call the "next" function to complete the transition to this page.
next(async (vm) => {
vm.setCmsContent(resultMap.cmsContent);
vm.isEmailInStoreOnPageLoad = isEmailInStoreOnPageLoad;
vm.showSaveProgressPopup = showSaveProgressPopup;
vm.addableVaps = addableVaps;
vm.lineItems = lineItems;
vm.availableLineItems = pricingResults;
@ -409,6 +438,8 @@ export default {
servicePackage: null,
holdParentAccountNumber: null,
holdBillToAccountNumber: null,
isEmailInStoreOnPageLoad: null,
showSaveProgressPopup: null,
};
},
computed: {
@ -495,6 +526,14 @@ export default {
backButtonAction() {
vehicleQuestionsMixin.methods.navigateBack(this);
},
saveProgress() {
this.isEmailInStoreOnPageLoad = true;
this.showSaveProgressPopup = false;
saveQuote({ pageNameToLog: "quote" });
},
closeSaveProgressPopup() {
this.showSaveProgressPopup = false;
},
forwardButtonAction() {
this.dispatchStoreAction(
this.storeActions.SAVE_PAYMENT_TYPE,
@ -680,10 +719,22 @@ export default {
afterpayModalBanner,
promoModalQuestion,
recalDisclaimer,
saveProgressModalQuestion,
saveProgressPopupQuestion,
},
};
</script>
<style>
.quote-page.show-save-progress-popup .row.justify-content-center {
display: none;
&.funnel-header-wrapper,
&.save-progress-popup {
display: block;
}
}
.funnel-sub-header {
p {
font-size: 1rem;

View file

@ -45,6 +45,11 @@
:isForwardActionDisabled="isForwardActionDisabled"
@back-clicked="navigateBack"
@ForwardClicked="forwardButtonAction" />
<saveProgressModalQuestion
modalWidgetName="SaveProgressModalWidget"
v-if="!isEmailInStoreOnPageLoad"
@save-progress="saveProgress" />
</div>
</div>
</div>
@ -60,6 +65,7 @@ import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-heade
import navbar from "@/fmg-components/nav-bar/nav-bar";
import alert from "@/ux-components/alert/alert";
import loadingModal from "@/fmg-components/loading-modal/loading-modal.vue";
import saveProgressModalQuestion from "@/fmg-components/save-progress-modal-question/save-progress-modal-question";
// Supporting Files
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { settleAllPromises } from "@/helpers/layout-helper";
@ -69,6 +75,7 @@ import store from "@/store";
import vehicleQuestionsMixin from "@/mixins/vehicle-questions-mixin";
import baseMixin from "@/mixins/base-mixin.js";
import { partTypeStrings } from "@/constants/part-type-strings";
import { saveQuote } from "@/helpers/heritage-integration/order-helper.js";
export default {
name: "vehicle-parts",
@ -84,10 +91,11 @@ export default {
},
];
const resultMap = await settleAllPromises(promiseResultMap);
const emailFromStore = store.getters.order?.customer?.emailAddress;
// Call the "next" function to complete the transition to this page.
next((vm) => {
vm.setCmsContent(resultMap.cmsContent);
vm.isEmailInStoreOnPageLoad = emailFromStore?.length > 0;
// Glass Part Question dynamic component
Object.keys(vm.$refs)
.filter((r) => r.includes(vm.RefPrefix) && vm.$refs[r][0] !== undefined)
@ -107,6 +115,7 @@ export default {
selectedGlassParts: {},
alertWidgetData: Object,
alreadyPopulatedPartsData: [],
isEmailInStoreOnPageLoad: null,
};
},
computed: {
@ -225,6 +234,9 @@ export default {
});
});
},
saveProgress() {
saveQuote({ pageNameToLog: "vehicle-parts" });
},
},
mounted() {
this.LoadInitialPartsData();
@ -237,6 +249,7 @@ export default {
navbar,
alert,
loadingModal,
saveProgressModalQuestion,
},
};
</script>

View file

@ -1049,6 +1049,7 @@ export const actions = {
payload: {
licensePlate: licensePlate,
licenseState: licenseState,
logEnabled: context.getters.applicationUser.loggingOption,
},
logApiCall: true,
pageNameToLog: pageNameToLog,
@ -1931,6 +1932,49 @@ export const actions = {
},
// Session API Actions
saveQuote(context, { pageNameToLog, payload }) {
const vehicle = context.getters.vehicle;
const damage = context.getters.damage;
const order = context.state.order;
const applicationUser = context.getters.applicationUser;
const lineItems = context.state.order.lineItems;
const excludeRecalInquote = context.getters.shouldHideRecalibration;
return globalMethods.callHttpClient({
method: endpoints.SaveQuote.method,
endpoint: endpoints.SaveQuote.url,
payload: {
referralCorrelationId: order.referralCorrelationId,
referralNumber: order.referralNumber?.toString(),
referralSequenceNumber: order.referralSequenceNumber,
emailAddress: order.customer.emailAddress,
lastPage: pageNameToLog,
isRepair: damage.isRepair,
firstName: order.customer.firstName,
lastName: order.customer.lastName,
savedSessionId: applicationUser.savedSessionId,
serviceState: order.serviceLocation.state,
serviceZipCode: order.serviceLocation.zipCode,
parentAccountNumber: order.payment.parentAccountNumber,
excludeRecalInquote: excludeRecalInquote,
year: vehicle.year,
make: vehicle.make,
model: vehicle.model,
isInsurance: order.payment.isInsurance ?? false,
isVerified: order.payment.insuranceCoverage.isVerified ?? false,
lineItems: {
glassParts: lineItems.glassParts,
supportingItems: lineItems.supportingItems,
vaps: lineItems.vaps,
serverData: lineItems.serverData,
promos: lineItems.promos,
},
},
logApiCall: true,
pageNameToLog: pageNameToLog,
});
},
saveSession(context, { pageNameToLog, payload }) {
const vehicle = context.getters.vehicle;
const damage = context.getters.damage;
@ -2008,8 +2052,7 @@ export const actions = {
order.payment.insuranceCoverage.coverageStatus?.toString(),
coverageVerificationType:
order.payment.insuranceCoverage.coverageVerificationType,
coverageSubStatus:
order.payment.insuranceCoverage.coverageSubStatus,
coverageSubStatus: order.payment.insuranceCoverage.coverageSubStatus,
},
isInsurance: order.payment.isInsurance,
parentAccountNumber: order.payment.parentAccountNumber,

View file

@ -77,7 +77,7 @@ body {
}
}
}
.modal-open {
.modal-open:not(.prevent-modal-scroll) {
.container-fluid {
&.page-container-grouped-styles {
overflow: hidden;

View file

@ -5,7 +5,7 @@
:class="[
isPrimary ? 'btn-primary' : 'btn-secondary',
isFloat ? 'float-end' : '',
isLoaderDisplayed ? 'has-loader' : '',
(isLoaderDisplayed && !suppressLoader) ? 'has-loader' : '',
]"
@click="clicked">
<span class="m-0">{{ this.buttonText }}</span>
@ -47,7 +47,7 @@ export default {
true
);
if (!this.isDisabled) {
this.isLoaderDisplayed = true;
if (!this.suppressLoader) this.isLoaderDisplayed = true;
this.$emit("click-event");
}
},