Merge branch 'develop' into feature/CSR-1392

This commit is contained in:
CarlNation 2023-11-01 10:22:00 -04:00
commit 18ce344af3
18 changed files with 415 additions and 206 deletions

View file

@ -22,6 +22,8 @@ module.exports = {
"!src/common-components/funnel-header/menu-modal/**/*.vue", "!src/common-components/funnel-header/menu-modal/**/*.vue",
"!src/layouts/schedule/helpers/schedule-helper.js", // Temp test exclusion while in development "!src/layouts/schedule/helpers/schedule-helper.js", // Temp test exclusion while in development
"!src/layouts/review/*.vue", // Temp test exclusion while in development "!src/layouts/review/*.vue", // Temp test exclusion while in development
"!src/layouts/payment/*.vue", // Temp test exclusion while in development
"!src/layouts/payment-pia-return/*.vue", // Temp test exclusion while in development
// END // END
], // ! means exclude from coverage. ], // ! means exclude from coverage.
testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"], testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"],

18
package-lock.json generated
View file

@ -14,6 +14,7 @@
"canvas-confetti": "^1.4.0", "canvas-confetti": "^1.4.0",
"core-js": "^3.6.5", "core-js": "^3.6.5",
"http-status-codes": "^2.1.4", "http-status-codes": "^2.1.4",
"iframe-resizer": "^4.3.7",
"jest-junit": "^13.0.0", "jest-junit": "^13.0.0",
"maska": "^1.5.0", "maska": "^1.5.0",
"vee-validate": "^4.5.7", "vee-validate": "^4.5.7",
@ -11813,6 +11814,18 @@
"integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=", "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=",
"dev": true "dev": true
}, },
"node_modules/iframe-resizer": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/iframe-resizer/-/iframe-resizer-4.3.7.tgz",
"integrity": "sha512-a3EGVScU9NtUpj6lWvGhVw3EfOw5AopRs5xGsQU385kWdgQt++OsD6PCnTV+8YkgBu/g28rLIh0EztFg9UQr1Q==",
"engines": {
"node": ">=0.8.0"
},
"funding": {
"type": "individual",
"url": "https://github.com/davidjbradshaw/iframe-resizer/blob/master/FUNDING.md"
}
},
"node_modules/ignore": { "node_modules/ignore": {
"version": "4.0.6", "version": "4.0.6",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz",
@ -33845,6 +33858,11 @@
"integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=", "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=",
"dev": true "dev": true
}, },
"iframe-resizer": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/iframe-resizer/-/iframe-resizer-4.3.7.tgz",
"integrity": "sha512-a3EGVScU9NtUpj6lWvGhVw3EfOw5AopRs5xGsQU385kWdgQt++OsD6PCnTV+8YkgBu/g28rLIh0EztFg9UQr1Q=="
},
"ignore": { "ignore": {
"version": "4.0.6", "version": "4.0.6",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz",

View file

@ -17,6 +17,7 @@
"canvas-confetti": "^1.4.0", "canvas-confetti": "^1.4.0",
"core-js": "^3.6.5", "core-js": "^3.6.5",
"http-status-codes": "^2.1.4", "http-status-codes": "^2.1.4",
"iframe-resizer": "^4.3.7",
"jest-junit": "^13.0.0", "jest-junit": "^13.0.0",
"maska": "^1.5.0", "maska": "^1.5.0",
"vee-validate": "^4.5.7", "vee-validate": "^4.5.7",

View file

@ -75,14 +75,11 @@ body .creditCardSpecific div {
padding: 8px 0; padding: 8px 0;
} }
body .creditCardSpecific div .headerlinecontainer { body .creditCardSpecific div .headerlinecontainer {
padding: 0; display: none;
} }
body .creditCardSpecific div label { body .creditCardSpecific div label {
font-weight: 500; font-weight: 500;
} }
body .creditCardSpecific div:first-child {
display: none;
}
body .creditCardSpecific #infoRow2 { body .creditCardSpecific #infoRow2 {
margin-bottom: 0; margin-bottom: 0;
} }

View file

@ -88,14 +88,11 @@ body {
div { div {
padding: 8px 0; padding: 8px 0;
.headerlinecontainer { .headerlinecontainer {
padding: 0; display: none;
} }
label { label {
font-weight: 500; font-weight: 500;
} }
&:first-child {
display: none;
}
} }
#infoRow2 { #infoRow2 {
margin-bottom: 0; margin-bottom: 0;

View file

@ -5,15 +5,33 @@
<component :is="Component" @focusin="handleAnyComponentFocus" /> <component :is="Component" @focusin="handleAnyComponentFocus" />
</transition> </transition>
</router-view> </router-view>
<loadingModal :showLoader="shouldShowLoader" :showTextCarousel="shouldShowTextCarousel" />
</template> </template>
<script> <script>
import { handleAnyComponentFocus } from "@/helpers/button-question-focus-helper"; import { handleAnyComponentFocus } from "@/helpers/button-question-focus-helper";
import loadingModal from "@/fmg-components/loading-modal/loading-modal.vue";
import { showFmgLoadingModal } from "@/helpers/loading-modal-helper";
export default { export default {
name: "app", name: "app",
setup() {
const { globalNonpersistedState } = showFmgLoadingModal();
return { globalNonpersistedState };
},
methods: { methods: {
handleAnyComponentFocus: handleAnyComponentFocus, handleAnyComponentFocus: handleAnyComponentFocus,
}, },
computed: {
shouldShowLoader() {
return this.globalNonpersistedState.showLoadingModal;
},
shouldShowTextCarousel() {
return this.globalNonpersistedState.showTextCarousel;
},
},
components: {
loadingModal,
},
}; };
</script> </script>

View file

@ -92,6 +92,7 @@ import listButton from "@/ux-components/list-button/list-button";
import listButtonHorizontal from "@/ux-components/list-button-horizontal/list-button-horizontal"; import listButtonHorizontal from "@/ux-components/list-button-horizontal/list-button-horizontal";
import listCard from "@/ux-components/list-card/list-card"; import listCard from "@/ux-components/list-card/list-card";
import radio from "@/ux-components/radio/radio"; import radio from "@/ux-components/radio/radio";
import checkbox from "@/ux-components/checkbox/checkbox";
import { useField, ErrorMessage } from "vee-validate"; import { useField, ErrorMessage } from "vee-validate";
import { queryStrings } from "@/constants/query-strings"; import { queryStrings } from "@/constants/query-strings";
@ -132,7 +133,6 @@ export default {
suppressError: Boolean, suppressError: Boolean,
useTextForValue: Boolean, useTextForValue: Boolean,
valueToLogType: String, valueToLogType: String,
additionalButtonData: Object,
isSmallQuestionText: Boolean, isSmallQuestionText: Boolean,
customButtonQuestionId: String, customButtonQuestionId: String,
logDisplayedValuesEvent: { logDisplayedValuesEvent: {
@ -209,6 +209,9 @@ export default {
case "radio": case "radio":
classes = "ui-radio d-flex"; classes = "ui-radio d-flex";
break; break;
case "checkbox":
classes = "ui-checkbox d-flex";
break;
case "servicePackageRadio": case "servicePackageRadio":
classes = "package-main"; classes = "package-main";
break; break;
@ -252,7 +255,7 @@ export default {
buttonFooterCopy: answer.buttonFooterCopy ?? answer.buttonFooterCopy, buttonFooterCopy: answer.buttonFooterCopy ?? answer.buttonFooterCopy,
buttonImage: answer.buttonImage ?? answer.AnswerImageUrl, buttonImage: answer.buttonImage ?? answer.AnswerImageUrl,
buttonImageId: answer.buttonImageId ?? answer.ImageId, buttonImageId: answer.buttonImageId ?? answer.ImageId,
additionalButtonData: answer.additionalButtonData ?? answer.additionalButtonData, additionalButtonData: answer.additionalButtonData,
groupName: this.formatString(this.groupName), groupName: this.formatString(this.groupName),
value: value:
answer.value ?? answer.value ??
@ -317,6 +320,7 @@ export default {
listCard, listCard,
ErrorMessage, ErrorMessage,
radio, radio,
checkbox,
}, },
}; };
</script> </script>

View file

@ -1,108 +0,0 @@
<template>
<transition name="fade" mode="out-in">
<div class="add-vaps-buttons" aria-live="polite" v-if="isDisplayed">
<buttonQuestion
ref="buttonQuestion"
:answers="answersToDisplay"
groupName="addVaps"
v-model="selectedValues"
buttonTypeString="addVapsButton"
:buttonTypeObject="addVapsButton"
:isWide="answersToDisplay.length > 1 ? false : true" />
</div>
</transition>
</template>
<script>
import buttonQuestion from "@/digital-components/button-question/button-question";
import addVapsButton from "./add-vaps-button/add-vaps-button";
export default {
name: "add-vaps-modal-buttons",
props: {
cartItems: Object,
vapsTilesCmsName: String,
availableLineItems: Object,
},
data() {
return {
addVapsButton: addVapsButton,
};
},
computed: {
selectedValues: {
get: function () {
return this.answersToDisplay;
},
set: function (newValue) {
// TODO - emit event to open modal
},
},
isDisplayed() {
return this.showAddRainDefense || this.showAddWipers;
},
showAddRainDefense() {
// if not found in the cart
return !this.cartItems.some((vap) => {
return vap?.partType === "RAIN DEFENSE";
});
},
showAddWipers() {
// if not found in the cart
return !this.cartItems.some((vap) => {
return vap?.partType?.includes(" WIPER");
});
},
rainDefensePrice() {
// const lineItem = this.rainDefenseItem;
const lineItem = this.availableLineItems.find((item) => {
return item.partType === "RAIN DEFENSE";
});
const price = lineItem?.laborAmount + lineItem?.sellingPrice + lineItem?.kitPrice;
return parseFloat(price).toFixed(2);
},
wipersPrice() {
let price = 0;
const lineItems = this.availableLineItems.filter((item) => {
return item.partType.includes(" WIPER");
});
lineItems?.forEach((item) => {
price += item?.laborAmount + item?.sellingPrice + item?.kitPrice;
});
return parseFloat(price).toFixed(2);
},
answersCmsData() {
return this.getCmsContent(this.vapsTilesCmsName, "Answers");
},
answersToDisplay() {
const answers = [];
if (!this.answersCmsData) return answers;
const getAnswer = (name, answers) => {
const answerIndex = answers.findIndex((answer) => {
return answer.Name === name;
});
return answers[answerIndex];
};
if (this.showAddWipers) {
const answer = getAnswer("WipersModal", this.answersCmsData);
answer.SubText = "+$" + this.wipersPrice;
answers.push(answer);
}
// if (this.showAddRainDefense) {
// const answer = getAnswer("RainDefenseModal", this.answersCmsData);
// answer.SubText = "+$" + this.rainDefensePrice;
// answers.push(answer);
// }
return answers;
},
},
components: {
buttonQuestion,
},
};
</script>

View file

@ -1,5 +1,5 @@
<template> <template>
<div v-if="isModalVisible"> <div v-if="showModalContent">
<div v-if="notFullScreen" class="dimmer-overlay"></div> <div v-if="notFullScreen" class="dimmer-overlay"></div>
<div class="modal-loader" :class="layoutClass"> <div class="modal-loader" :class="layoutClass">
<div class="row g-2 h-100 d-flex align-items-center"> <div class="row g-2 h-100 d-flex align-items-center">
@ -11,7 +11,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="row"> <div v-if="showTextCarouselContent" class="row">
<div class="col"> <div class="col">
<div class="text-container slide"> <div class="text-container slide">
<p> <p>
@ -59,6 +59,7 @@ export default {
data() { data() {
return { return {
isModalVisible: false, isModalVisible: false,
isModalTextCarouselVisible: false,
}; };
}, },
props: { props: {
@ -66,15 +67,30 @@ export default {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
showLoader: {
type: Boolean,
default: false,
},
showTextCarousel: {
type: Boolean,
default: false,
},
}, },
computed: { computed: {
layoutClass() { layoutClass() {
return this.notFullScreen ? "card-layout" : "full-screen container-fluid"; return this.notFullScreen ? "card-layout" : "full-screen container-fluid";
}, },
showModalContent() {
return this.isModalVisible || this.showLoader;
},
showTextCarouselContent() {
return this.isModalTextCarouselVisible || this.showTextCarousel;
},
}, },
methods: { methods: {
showModal() { showModal(showTextCarousel = true) {
//Display modal //Display modal
this.isModalTextCarouselVisible = showTextCarousel;
this.isModalVisible = true; this.isModalVisible = true;
//Force page reload on back button //Force page reload on back button
window.addEventListener( window.addEventListener(

View file

@ -0,0 +1,18 @@
import { reactive } from "vue";
// state encapsulated and managed by the composable
const globalNonpersistedState = reactive({
showLoadingModal: true,
showTextCarousel: false,
});
export const showFmgLoadingModal = (newValue, showTextCarousel = false) => {
if (newValue !== undefined) {
globalNonpersistedState.showLoadingModal = newValue;
}
if (showTextCarousel !== undefined) {
globalNonpersistedState.showTextCarousel = showTextCarousel;
}
return { globalNonpersistedState };
};

View file

@ -26,30 +26,23 @@
v-if="wipersModal" v-if="wipersModal"
@footer-button-action="addWipersToCart('WipersModal', wipersModal)" @footer-button-action="addWipersToCart('WipersModal', wipersModal)"
footerButtonActionName="footer-button-action"> footerButtonActionName="footer-button-action">
<div v-if="wipersToDisplay === wipersToDisplayStrings.BOTH" class="wiper-options"> <div v-if="wipersOffered === wipersOfferedStrings.BOTH" class="wiper-options">
<div> <buttonQuestion
<checkboxQuestion ref="buttonQuestion"
class="mb-5" buttonTypeString="checkbox"
cmsWidgetName="AddFrontBladesQuestionWidget" class="mt-5"
v-model="selectedFrontWipers" /> :answers="wipersOptions"
<p class="price">+${{ wipersModal.totalFrontPrice }}</p> groupName="wipers-options"
</div> textPosition="text-center"
<div> v-model="selectedWipers"
<checkboxQuestion isMultiSelect
class="mb-5" isRequired
cmsWidgetName="AddRearBladesQuestionWidget" validationRules="checkbox-required" />
v-model="selectedRearWipers" />
<p class="price">+${{ wipersModal.totalRearPrice }}</p>
</div>
</div> </div>
<div <div v-else-if="wipersOffered === wipersOfferedStrings.FRONT" class="wiper-options">
v-else-if="wipersToDisplay === wipersToDisplayStrings.FRONT"
class="wiper-options">
<p class="price">+${{ wipersModal.totalFrontPrice }}</p> <p class="price">+${{ wipersModal.totalFrontPrice }}</p>
</div> </div>
<div <div v-else-if="wipersOffered === wipersOfferedStrings.REAR" class="wiper-options">
v-else-if="wipersToDisplay === wipersToDisplayStrings.REAR"
class="wiper-options">
<p class="price">+${{ wipersModal.totalRearPrice }}</p> <p class="price">+${{ wipersModal.totalRearPrice }}</p>
</div> </div>
</contentGroupModal> </contentGroupModal>
@ -62,12 +55,17 @@
import buttonQuestion from "@/digital-components/button-question/button-question"; import buttonQuestion from "@/digital-components/button-question/button-question";
import addVapsButton from "./add-vaps-button/add-vaps-button"; import addVapsButton from "./add-vaps-button/add-vaps-button";
import contentGroupModal from "@/fmg-components/content-group-modal/content-group-modal"; import contentGroupModal from "@/fmg-components/content-group-modal/content-group-modal";
import checkboxQuestion from "@/digital-components/checkbox-question/checkbox-question";
// SUPPORTING // SUPPORTING
import { partTypeStrings } from "@/constants/part-type-strings"; import { partTypeStrings } from "@/constants/part-type-strings";
import { defineRule } from "vee-validate";
import { required } from "@/helpers/validation-rules";
import { errorMessages } from "@/constants/error-messages";
const wipersToDisplayStrings = { // DEFINE VALIDATION RULES
defineRule("checkbox-required", required(errorMessages.OPTION_REQUIRED));
const wipersOfferedStrings = {
FRONT: "FRONT", FRONT: "FRONT",
REAR: "REAR", REAR: "REAR",
BOTH: "BOTH", BOTH: "BOTH",
@ -83,15 +81,18 @@ export default {
data() { data() {
return { return {
addVapsButton: addVapsButton, addVapsButton: addVapsButton,
selectedFrontWipers: false, wipersOffered: null,
selectedRearWipers: false, isModalOpened: false,
wipersToDisplay: null, selectedWipers: this.getSelectedWipers(),
}; };
}, },
methods: { methods: {
openModal(modalName) { openModal(modalName) {
this.$refs[modalName].openModal(); this.$refs[modalName].openModal();
}, },
closeModal(modalName) {
this.$refs[modalName].closeModal();
},
addRainDefenseToCart(modalName, part) { addRainDefenseToCart(modalName, part) {
const alreadyInCart = this.currentCartItems.vaps.some((item) => { const alreadyInCart = this.currentCartItems.vaps.some((item) => {
return item.partType === part.partType; return item.partType === part.partType;
@ -99,9 +100,11 @@ export default {
if (!alreadyInCart) { if (!alreadyInCart) {
this.currentCartItems.vaps.push(part); this.currentCartItems.vaps.push(part);
} }
this.$refs[modalName].closeModal(); this.closeModal(modalName);
}, },
addWipersToCart(modalName, part) { addWipersToCart(modalName, part) {
const itemsForCart = [];
const frontWipersAlreadyInCart = this.currentCartItems.vaps.some((item) => { const frontWipersAlreadyInCart = this.currentCartItems.vaps.some((item) => {
return item.partType === part.frontWiperLineItems[0].partType; return item.partType === part.frontWiperLineItems[0].partType;
}); });
@ -109,36 +112,43 @@ export default {
return item.partType === part.rearWiperLineItems[0]?.partType; return item.partType === part.rearWiperLineItems[0]?.partType;
}); });
if (this.wipersToDisplay === wipersToDisplayStrings.FRONT) { if (this.wipersOffered === wipersOfferedStrings.BOTH) {
this.selectedFrontWipers = true; // set as true if front wipers is only option this.selectedWipers.forEach((wiper) => {
itemsForCart.push(wiper);
});
} else {
if (
!frontWipersAlreadyInCart &&
this.wipersOffered === wipersOfferedStrings.FRONT
) {
itemsForCart.push(wipersOfferedStrings.FRONT);
}
if (!rearWipersAlreadyInCart && this.wipersOffered === wipersOfferedStrings.REAR) {
itemsForCart.push(wipersOfferedStrings.REAR);
}
} }
if (this.wipersToDisplay === wipersToDisplayStrings.REAR) {
this.selectedRearWipers = true; // set as true if rear wipers is only option itemsForCart.forEach((type) => {
} if (type === wipersOfferedStrings.FRONT) {
if (!frontWipersAlreadyInCart && this.selectedFrontWipers) {
if (part.frontWiperLineItems) {
// for wipers, a "combined" part (frontWiperLineItems)
part.frontWiperLineItems.forEach((part) => { part.frontWiperLineItems.forEach((part) => {
this.currentCartItems.vaps.push(part); this.currentCartItems.vaps.push(part);
}); });
} }
} if (type === wipersOfferedStrings.REAR) {
if (!rearWipersAlreadyInCart && this.selectedRearWipers) {
if (part.rearWiperLineItems) {
part.rearWiperLineItems.forEach((part) => { part.rearWiperLineItems.forEach((part) => {
this.currentCartItems.vaps.push(part); this.currentCartItems.vaps.push(part);
}); });
} }
} });
this.$refs[modalName].closeModal();
if (this.wipersToDisplay === wipersToDisplayStrings.BOTH) { this.closeModal(modalName);
this.selectedFrontWipers = false; // set as false if both options are given this.selectedWipers;
this.selectedRearWipers = false; // set as false if both options are given
}
}, },
updateWipersToDisplay(value) { updateWipersOffered(value) {
this.wipersToDisplay = value; this.wipersOffered = value;
},
getSelectedWipers() {
return null;
}, },
}, },
computed: { computed: {
@ -229,31 +239,72 @@ export default {
]; ];
prices.sort((a, b) => a - b); prices.sort((a, b) => a - b);
modalData.SubText = "+$" + prices[0] + " - $" + prices[prices.length - 1]; modalData.SubText = "+$" + prices[0] + " - $" + prices[prices.length - 1];
this.updateWipersToDisplay(wipersToDisplayStrings.BOTH); this.updateWipersOffered(wipersOfferedStrings.BOTH);
buttons.push(modalData); buttons.push(modalData);
} else { } else {
// NO FRONT WIPERS IN CART; SHOW ONLY FRONT WIPERS BUTTON // NO FRONT WIPERS IN CART; SHOW ONLY FRONT WIPERS BUTTON
modalData.SubText = "+$" + this.wipersModal?.totalFrontPrice; modalData.SubText = "+$" + this.wipersModal?.totalFrontPrice;
this.updateWipersToDisplay(wipersToDisplayStrings.FRONT); this.updateWipersOffered(wipersOfferedStrings.FRONT);
buttons.push(modalData); buttons.push(modalData);
} }
} else if (!rearWipersInCart && this.wipersModal?.rearWiperLineItems.length > 0) { } else if (!rearWipersInCart && this.wipersModal?.rearWiperLineItems.length > 0) {
// NO REAR WIPERS IN CART; SHOW ONLY REAR WIPERS BUTTON // NO REAR WIPERS IN CART; SHOW ONLY REAR WIPERS BUTTON
modalData.SubText = "+$" + this.wipersModal?.totalRearPrice; modalData.SubText = "+$" + this.wipersModal?.totalRearPrice;
this.updateWipersToDisplay(wipersToDisplayStrings.REAR); this.updateWipersOffered(wipersOfferedStrings.REAR);
buttons.push(modalData); buttons.push(modalData);
} }
return buttons; return buttons;
}, },
wipersToDisplayStrings() { wipersOfferedStrings() {
return wipersToDisplayStrings; return wipersOfferedStrings;
},
wipersOptions() {
const wipersOptions = [];
const wipersFrontCopy = this.getCmsContent(
"AddFrontBladesQuestionWidget",
"QuestionText"
);
const wipersRearCopy = this.getCmsContent(
"AddRearBladesQuestionWidget",
"QuestionText"
);
const frontWipersLineItems = this.wipersModal.frontWiperLineItems;
const rearWipersLineItems = this.wipersModal.rearWiperLineItems;
if (frontWipersLineItems) {
const wipersOption = {
buttonLabel: wipersFrontCopy,
groupName: "wipers-options",
value: wipersOfferedStrings.FRONT,
buttonLabelSubCopy: "buttonLabelSubCopy",
buttonBodyCopy: "buttonBodyCopy",
buttonAuxillaryCopy: "buttonAuxillaryCopy",
buttonFooterCopy: "buttonFooterCopy",
additionalButtonData: "+$" + this.wipersModal.totalFrontPrice,
};
wipersOptions.push(wipersOption);
}
if (rearWipersLineItems) {
const wipersOption = {
buttonLabel: wipersRearCopy,
groupName: "wipers-options",
value: wipersOfferedStrings.REAR,
buttonLabelSubCopy: "buttonLabelSubCopy",
buttonBodyCopy: "buttonBodyCopy",
buttonAuxillaryCopy: "buttonAuxillaryCopy",
buttonFooterCopy: "buttonFooterCopy",
additionalButtonData: "+$" + this.wipersModal.totalRearPrice,
};
wipersOptions.push(wipersOption);
}
return wipersOptions;
}, },
}, },
components: { components: {
buttonQuestion, buttonQuestion,
contentGroupModal, contentGroupModal,
checkboxQuestion,
}, },
}; };
</script> </script>
@ -286,26 +337,31 @@ export default {
text-align: center; text-align: center;
} }
.wiper-options { .wiper-options {
& > div { .ui-checkbox {
display: flex; display: block;
justify-content: space-between;
margin-bottom: 1rem;
&:last-child { & > div.col {
margin-bottom: 0; margin-bottom: 1rem;
&:last-child {
margin-bottom: 0;
}
} }
.checkbox {
display: flex;
justify-content: space-between;
.form-check { .additional-button-data {
margin-bottom: 0; font-size: 0.875rem;
line-height: 1.75;
padding-left: 0.5rem;
color: $green;
}
} }
} }
p { p {
margin-bottom: 0; margin-bottom: 0;
} line-height: 1.75;
p.price {
font-size: 0.875rem;
padding-left: 0.5rem;
color: $green;
} }
} }
} }

View file

@ -12,6 +12,7 @@
<div class="col-md-6 col-xl-4 p-0"> <div class="col-md-6 col-xl-4 p-0">
<div id="card-container"> <div id="card-container">
<iframe <iframe
v-resize
ref="paymentFrame" ref="paymentFrame"
class="hop-iframe payment-iframe" class="hop-iframe payment-iframe"
name="card-frame" name="card-frame"
@ -202,7 +203,7 @@ import { paymentMethods } from "@/constants/payment-method-constants";
import { AppointmentTypeStrings } from "@/constants/schedule-constants"; import { AppointmentTypeStrings } from "@/constants/schedule-constants";
import baseMixin from "@/mixins/base-mixin.js"; import baseMixin from "@/mixins/base-mixin.js";
// Validation import iframeResize from "../../../node_modules/iframe-resizer/js/iframeResizer.js";
export default { export default {
name: "payment", name: "payment",
@ -235,6 +236,18 @@ export default {
paypalPayment: this.isPaypal(), paypalPayment: this.isPaypal(),
}; };
}, },
directives: {
resize: {
mounted: function (el) {
el.addEventListener("load", () => {
iframeResize({ checkOrigin: false }, el);
});
},
unmounted: function (el) {
el.iFrameResizer.removeListeners();
},
},
},
async beforeRouteEnter(to, from, next) { async beforeRouteEnter(to, from, next) {
// Call APIs // Call APIs
const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage); const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage);
@ -487,7 +500,7 @@ export default {
<style lang="scss"> <style lang="scss">
.hop-iframe { .hop-iframe {
position: relative; position: relative;
min-height: 1320px; min-height: 1120px;
width: 100%; width: 100%;
border: 0 none; border: 0 none;
} }

View file

@ -186,18 +186,21 @@ describe("shop-question.vue", () => {
buttonLabel: "Westerville", buttonLabel: "Westerville",
buttonLabelSubCopy: "5 mi", buttonLabelSubCopy: "5 mi",
value: "003335", value: "003335",
additionalButtonData: wrapper.vm.additionalButtonData,
}, },
{ {
buttonBodyCopy: "760 Dearborn Park Ln, Worthington, OH 43085", buttonBodyCopy: "760 Dearborn Park Ln, Worthington, OH 43085",
buttonLabel: "Worthington", buttonLabel: "Worthington",
buttonLabelSubCopy: "10.5 mi", buttonLabelSubCopy: "10.5 mi",
value: "001820", value: "001820",
additionalButtonData: wrapper.vm.additionalButtonData,
}, },
{ {
buttonBodyCopy: "5015 N High St, Columbus, OH 43214", buttonBodyCopy: "5015 N High St, Columbus, OH 43214",
buttonLabel: "Columbus", buttonLabel: "Columbus",
buttonLabelSubCopy: "11.5 mi", buttonLabelSubCopy: "11.5 mi",
value: "003343", value: "003343",
additionalButtonData: wrapper.vm.additionalButtonData,
}, },
]); ]);
}); });
@ -414,6 +417,10 @@ describe("shop-question.vue", () => {
}, },
}); });
displayedAnswers.forEach(
(answer) => (answer.additionalButtonData = wrapper.vm.additionalButtonData)
);
// Act // Act
await wrapper.setProps({ await wrapper.setProps({
shopProviderData: shopQuestionInitialData, shopProviderData: shopQuestionInitialData,
@ -445,42 +452,49 @@ describe("shop-question.vue", () => {
// Assert // Assert
expect(wrapper.vm.answers.length).toBe(6); expect(wrapper.vm.answers.length).toBe(6);
console.log(wrapper.vm.answers);
expect(wrapper.vm.answers).toEqual([ expect(wrapper.vm.answers).toEqual([
{ {
buttonBodyCopy: "4403 Executive Pkwy, Westerville, OH 43081", buttonBodyCopy: "4403 Executive Pkwy, Westerville, OH 43081",
buttonLabel: "Westerville", buttonLabel: "Westerville",
buttonLabelSubCopy: "5 mi", buttonLabelSubCopy: "5 mi",
value: "003335", value: "003335",
additionalButtonData: wrapper.vm.additionalButtonData,
}, },
{ {
buttonBodyCopy: "760 Dearborn Park Ln, Worthington, OH 43085", buttonBodyCopy: "760 Dearborn Park Ln, Worthington, OH 43085",
buttonLabel: "Worthington", buttonLabel: "Worthington",
buttonLabelSubCopy: "10.5 mi", buttonLabelSubCopy: "10.5 mi",
value: "001820", value: "001820",
additionalButtonData: wrapper.vm.additionalButtonData,
}, },
{ {
buttonBodyCopy: "5015 N High St, Columbus, OH 43214", buttonBodyCopy: "5015 N High St, Columbus, OH 43214",
buttonLabel: "Columbus", buttonLabel: "Columbus",
buttonLabelSubCopy: "11.5 mi", buttonLabelSubCopy: "11.5 mi",
value: "003343", value: "003343",
additionalButtonData: wrapper.vm.additionalButtonData,
}, },
{ {
buttonBodyCopy: "1670 Harmon Ave, Columbus, OH 43223", buttonBodyCopy: "1670 Harmon Ave, Columbus, OH 43223",
buttonLabel: "Columbus", buttonLabel: "Columbus",
buttonLabelSubCopy: "16 mi", buttonLabelSubCopy: "16 mi",
value: "006747", value: "006747",
additionalButtonData: wrapper.vm.additionalButtonData,
}, },
{ {
buttonBodyCopy: "3938 Powell Rd, Powell, OH 43065", buttonBodyCopy: "3938 Powell Rd, Powell, OH 43065",
buttonLabel: "Powell", buttonLabel: "Powell",
buttonLabelSubCopy: "16.5 mi", buttonLabelSubCopy: "16.5 mi",
value: "003341", value: "003341",
additionalButtonData: wrapper.vm.additionalButtonData,
}, },
{ {
buttonBodyCopy: "4580 W Broad St, Columbus, OH 43228", buttonBodyCopy: "4580 W Broad St, Columbus, OH 43228",
buttonLabel: "Columbus", buttonLabel: "Columbus",
buttonLabelSubCopy: "19.5 mi", buttonLabelSubCopy: "19.5 mi",
value: "003342", value: "003342",
additionalButtonData: wrapper.vm.additionalButtonData,
}, },
]); ]);
}); });

View file

@ -19,8 +19,7 @@
textPosition="text-start" textPosition="text-start"
v-model="selectedProviderNumber" v-model="selectedProviderNumber"
isRequired isRequired
validationRules="option-required" validationRules="option-required" />
:additionalButtonData="additionalButtonData" />
<textLink <textLink
v-if="displaySeeMoreLocationsLink" v-if="displaySeeMoreLocationsLink"
ref="showMoreShopsLink" ref="showMoreShopsLink"
@ -156,6 +155,7 @@ export default {
buttonLabel: city, buttonLabel: city,
buttonLabelSubCopy: `${distanceInMiles} mi`, buttonLabelSubCopy: `${distanceInMiles} mi`,
buttonBodyCopy: `${streetAddress}, ${city}, ${state} ${zipCode}`, buttonBodyCopy: `${streetAddress}, ${city}, ${state} ${zipCode}`,
additionalButtonData: this.additionalButtonData,
value: shopProvider.providerNumber, value: shopProvider.providerNumber,
}; };
}); });

View file

@ -8,6 +8,7 @@ import { globalEvents, globalEventTypes } from "@/constants/events";
import { queryStrings } from "@/constants/query-strings"; import { queryStrings } from "@/constants/query-strings";
import { getQuerystringParameter } from "@/helpers/querystring-helper"; import { getQuerystringParameter } from "@/helpers/querystring-helper";
import { getDeviceIdValue } from "@/helpers/heritage-integration/cookie-helper"; import { getDeviceIdValue } from "@/helpers/heritage-integration/cookie-helper";
import { showFmgLoadingModal } from "@/helpers/loading-modal-helper";
// Heritage integration // Heritage integration
import { isSavedSessionStillActive } from "@/helpers/heritage-integration/session-helper"; import { isSavedSessionStillActive } from "@/helpers/heritage-integration/session-helper";
@ -154,6 +155,11 @@ router.beforeEach(async (to, from, next) => {
// use current page url query string name when to.name is "root" (due to unresolved navigation in beforeEach) // use current page url query string name when to.name is "root" (due to unresolved navigation in beforeEach)
router.lastNavigationPage = to.name == "root" ? analyticsMixin.methods.getPageName() : to.name; router.lastNavigationPage = to.name == "root" ? analyticsMixin.methods.getPageName() : to.name;
const fromQueryPage = from.query?.fmgPage;
if (fromQueryPage === undefined) {
showFmgLoadingModal(true);
}
next(); next();
}); });
@ -171,6 +177,8 @@ router.afterEach(async (to, from) => {
} }
} }
showFmgLoadingModal(false);
// Push page view to GA // Push page view to GA
analyticsMixin.methods.pushPageViewToGA(); analyticsMixin.methods.pushPageViewToGA();

View file

@ -2196,15 +2196,25 @@ export const actions = {
"lineItems" "lineItems"
); );
let queryString = let queryString = "";
`ParentAccountNumber=${applicationConfig.CASH_PARENT_ACCOUNT_NUMBER}` + if (appointmentType == "Mobile") {
`&BillToAccountNumber=${billToAccountNumber}` + queryString =
`&ProviderNumber=${providerNumber}` + `ParentAccountNumber=${applicationConfig.CASH_PARENT_ACCOUNT_NUMBER}` +
`&AppointmentType=${appointmentType}` + `&BillToAccountNumber=${billToAccountNumber}` +
`&ServiceLocation.City=${serviceLocationCity}` + `&ProviderNumber=${providerNumber}` +
`&ServiceLocation.State=${serviceLocationState}` + `&AppointmentType=${appointmentType}` +
`&ServiceLocation.ZipCode=${serviceLocationZipCode}` + `&ServiceLocation.City=${serviceLocationCity}` +
`&${pricedLineItemsFormattedForRequest}`; `&ServiceLocation.State=${serviceLocationState}` +
`&ServiceLocation.ZipCode=${serviceLocationZipCode}` +
`&${pricedLineItemsFormattedForRequest}`;
} else {
queryString =
`ParentAccountNumber=${applicationConfig.CASH_PARENT_ACCOUNT_NUMBER}` +
`&BillToAccountNumber=${billToAccountNumber}` +
`&ProviderNumber=${providerNumber}` +
`&AppointmentType=${appointmentType}` +
`&${pricedLineItemsFormattedForRequest}`;
}
const lineItemServerData = context.getters.order.lineItems.serverData; const lineItemServerData = context.getters.order.lineItems.serverData;
if (lineItemServerData) { if (lineItemServerData) {
@ -2257,13 +2267,18 @@ export const actions = {
}, },
}; };
const validateResponse = await globalMethods.callHttpClient({ const validateResponse = await globalMethods
method: endpoints.ValidatePromo.method, .callHttpClient({
endpoint: endpoints.ValidatePromo.url, method: endpoints.ValidatePromo.method,
payload: requestObject, endpoint: endpoints.ValidatePromo.url,
logApiCall: true, payload: requestObject,
pageNameToLog: pageNameToLog, logApiCall: true,
}); pageNameToLog: pageNameToLog,
})
.catch((error) => {
// Validate promo will return 4xx errors when an invalid promo
return error;
});
if (validateResponse?.data?.serverData) { if (validateResponse?.data?.serverData) {
context.commit( context.commit(

View file

@ -0,0 +1,66 @@
import { mount } from "@vue/test-utils";
import checkbox from "./checkbox";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import inputButtonWrapperMixin from "@/mixins/input-button-wrapper-mixin";
describe("checkbox.vue", () => {
it("Should have correct group name", async () => {
// Arrange
let { wrapper } = setupMocks({
mountOptionsMockData: {
propsData: {
groupName: "checkbox-button-test",
value: "test value",
},
},
});
// Act
const input = wrapper.find("input");
// Assert
expect(input.attributes().name).toEqual("checkbox-button-test");
});
it("Should have correct label text", async () => {
// Act
let { wrapper } = setupMocks({
mountOptionsMockData: {
propsData: {
buttonLabel: "label text",
value: "test value",
},
},
});
// Arrange
const paragraph = wrapper.find("p");
// Assert
expect(paragraph.text()).toEqual("label text");
});
it("Should have correct screenreader-only text", async () => {
// Act
let { wrapper } = setupMocks({});
// Arrange
await wrapper.setProps({
screenReaderOnlyText: "screenreader text",
value: "test value",
});
const paragraph = wrapper.find(".sr-only");
// Assert
expect(paragraph.text()).toEqual("screenreader text");
});
});
function setupMocks({ mountOptionsMockData = {} }) {
const wrapper = mount(checkbox, {
...mountOptionsMockData,
mixins: [inputButtonWrapperMixin],
});
return { wrapper };
}

View file

@ -0,0 +1,74 @@
<template>
<baseInputButton
v-bind="$props"
buttonWrapperClasses="form-check base-input-button checkbox"
inputClasses="form-check-input"
v-model="selectedValue">
<div class="d-flex align-items-start form-check-label">
<p v-if="buttonLabel" class="m-0">{{ buttonLabel }}</p>
<span v-if="screenReaderOnlyText" class="sr-only">{{ screenReaderOnlyText }}</span>
</div>
<div
v-if="additionalButtonData"
v-html="additionalButtonData"
class="additional-button-data"></div>
</baseInputButton>
</template>
<script>
import baseInputButton from "@/digital-components/base-input-button/base-input-button";
import inputButtonWrapperMixin from "@/mixins/input-button-wrapper-mixin";
export default {
name: "checkbox",
mixins: [inputButtonWrapperMixin],
components: {
baseInputButton,
},
};
</script>
<style lang="scss">
.form-check {
position: relative;
.form-check-input {
border: 1px solid $gray-500; //
border-radius: 2px; //
margin-right: 0.5rem;
opacity: 1;
height: 1em;
width: 1em;
&:checked {
background-color: $blue; //
background-size: contain;
background-position: center;
border: 1px solid $blue;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23ffffff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10l3 3l6-6'/%3e%3c/svg%3e");
+ .form-check-label {
p {
font-weight: 400;
font-size: 0.875rem;
color: $gray-600;
}
}
}
&:focus {
box-shadow: 0 0 0 2.5px $blue;
}
}
&:hover {
.form-check-input:not(:checked) {
box-shadow: 0 0 0 4px $blue-300;
}
}
p {
font-weight: 400;
font-size: 0.875rem;
color: $gray-600;
}
}
</style>