Merge branch 'develop' into feature/CSR-1392

This commit is contained in:
CarlNation 2023-10-24 09:04:12 -04:00
commit dc4ec8cdd0
63 changed files with 9743 additions and 1181 deletions

View file

@ -31,5 +31,5 @@ module.exports = {
},
},
// Uncomment this to avoid the massive amount of warnings we are getting for onSubmit and onInvalidSubmit
silent: true,
//silent: true,
};

336
public/css/hop-styling.css Normal file
View file

@ -0,0 +1,336 @@
.btn-group-lg > .btn {
padding: 10px 20px;
font-size: 20px;
line-height: 1.3;
border-radius: 5px;
}
.btn-group-sm > .btn {
padding: 3px 6px;
font-size: 13px;
line-height: 1.5;
border-radius: 3px;
}
.btn-group-xs > .btn {
padding: 1px 5px;
font-size: 13px;
line-height: 1.5;
border-radius: 3px;
}
.dl-horizontal dd:before,
.dl-horizontal dd:after,
.container:before,
.container:after,
.container-fluid:before,
.container-fluid:after,
.row:before,
.row:after,
.form-horizontal .form-group:before,
.form-horizontal .form-group:after,
.btn-toolbar:before,
.btn-toolbar:after,
.btn-group-vertical > .btn-group:before,
.btn-group-vertical > .btn-group:after,
.panel-body:before,
.panel-body:after,
.modal-footer:before,
.modal-footer:after {
content: " ";
display: table;
}
.dl-horizontal dd:after,
.container:after,
.container-fluid:after,
.row:after,
.form-horizontal .form-group:after,
.btn-toolbar:after,
.btn-group-vertical > .btn-group:after,
.panel-body:after,
.modal-footer:after {
clear: both;
}
[data-icon]:before,
[data-icon-after]:after,
.ico:before {
speak: none;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-transform: none !important;
font-weight: normal !important;
font-style: normal !important;
font-variant: normal !important;
font-size: 20px !important;
font-family: "safelite-ico" !important;
line-height: 20px !important;
}
html {
position: static !important;
height: auto !important;
}
html,
body {
background-color: #ffffff !important;
}
body {
margin-bottom: 16px !important;
}
.headerlinecontainer {
color: #000000;
font-family: Roboto, Arial, "Helvetica Neue", Helvetica, Tahoma, Verdana, sans-serif;
}
.headerline1 {
display: block;
margin-bottom: 11.5px;
font-weight: 300;
font-size: 20px;
line-height: 30px;
}
.headerline2,
.headerline3,
.headerline4,
.headerline5 {
margin-bottom: 11.5px;
font-size: 16px;
}
.headerline3 {
color: #0c7e47;
}
.headerline5 {
display: block;
padding: 11.5px 0;
color: #167cac;
font-style: italic;
}
.headerline5v2 {
display: block;
padding: 11.5px 0;
color: #da291c;
font-style: italic;
font-size: 12px;
}
.form-group {
margin-bottom: 10px;
}
.form-group .control-label {
display: block;
margin-bottom: 2px;
}
.form-group .form-control {
margin-bottom: 2px;
}
@media (min-width: 500px) {
.form-group {
width: 80%;
}
}
@media (min-width: 800px) {
.form-group {
width: 60%;
}
}
@media (min-width: 576px) {
#cardExpirationContainer .card_expirationMonth,
#cardExpirationContainer .card_expirationYear {
position: relative;
width: 49%;
}
#cardExpirationContainer .card_expirationMonth {
float: left;
}
#cardExpirationContainer .card_expirationYear {
float: right;
}
}
.totalContainer {
margin: 16px 0;
padding: 8px;
border-radius: 5px;
background-color: #0c7e47;
color: #ffffff;
font-size: 16px;
}
.totalContainer em {
font-weight: 500;
}
.totalContainer .strikeThrough {
padding-left: 8px;
text-decoration: line-through;
font-size: 14px;
}
.callerDisplayTextContainer {
margin: 16px 0;
font-style: italic;
}
.disclaimerContainer {
margin: 16px 0;
padding: 8px;
border-radius: 5px;
background-color: #f0f1f2;
}
.disclaimerContainerv2 {
margin: 16px 0;
border-radius: 5px;
}
.buttonContainer #sghopcancelbutton {
float: left;
}
.buttonContainer #sghopsubmitbutton {
float: right;
}
.buttonContainer #sghopsubmitbutton.creditCardSpecific {
float: right;
}
@media (max-width: 500px) {
.buttonContainer #sghopsubmitbutton.creditCardSpecific {
float: left;
width: 100%;
}
}
.buttonContainer #selectPaymentTypeButton {
float: right;
}
@media (max-width: 500px) {
.buttonContainer #selectPaymentTypeButton {
float: left;
width: 100%;
}
}
.globalMessage .message {
padding-left: 0;
line-height: 1.25;
}
.globalMessage .message strong {
color: #da291c;
}
.italicSmall {
font-weight: 300;
font-style: normal;
font-size: 15px;
}
#pageLoadingModal {
background-image: url("data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==");
}
#pageLoadingModal .modal-content {
margin-top: 100px;
}
#paypalParentDiv {
margin-bottom: 20px;
}
#creditCardParentDiv {
margin-bottom: 0;
}
.radio .list-group.paymentSelection,
.radio.list-group.paymentSelection {
margin-top: 0;
margin-bottom: 0;
}
@media (max-width: 33.9em) {
.radio .list-group.paymentSelection label,
.radio.list-group.paymentSelection label,
.radio .list-group.paymentSelection a,
.radio.list-group.paymentSelection a {
max-height: 52px;
}
}
#creditCardParentLink {
margin-bottom: 0;
}
@media (max-width: 33.9em) {
#creditCardParentLink {
min-height: 57px;
}
}
#creditCardImages,
#paypalImageSpan {
padding-right: 0;
}
@media (max-width: 33.9em) {
.paymentOptionText {
padding-left: 5px;
}
}
.extraPaddingTop {
padding-top: 20px;
}
.extraPaddingBottom {
padding-bottom: 20px;
}
@media (min-width: 33.9em) {
#paypalImage {
max-width: 22%;
}
}
@media (max-width: 33.9em) {
#paypalImage {
max-width: 55px;
}
}
label.centerme {
position: inherit;
display: table-cell;
width: 9999px !important;
/*This is kind of a hack*/
vertical-align: middle;
}
input[name="rdioCreditCard"] {
display: block;
}
@media (min-width: 33.9em) {
input[name="rdioCreditCard"] {
margin-top: 10px;
}
}
@media (min-width: 33.9em) {
span.paymentOptionText {
margin-top: 6px;
}
}
.headerline1.safelitehop {
margin-top: 20px;
}
@media (max-width: 499px) {
#buttonContainer {
width: 100%;
}
}
.has-success .control-label {
color: #4d4e53;
}
#thm_fp {
display: block;
}
#paypalParentLink label,
#creditCardParentLink label {
padding-top: 9px;
padding-bottom: 9px;
}
#paypalParentLink label .paymentOptionText,
#creditCardParentLink label .paymentOptionText {
padding-left: 20px;
text-align: left;
}
#paypalParentLink {
margin-top: 12px;
}
html #creditCardParentLink.list-group-item {
border-radius: 48px;
text-align: center;
}
.optional-label span {
display: none;
}
.optional-label::after {
content: " (optional)";
font-weight: 300;
font-style: normal;
font-size: 15px;
}
.creditCardSpecific {
display: none;
}
@media (max-width: 500px) {
.buy-backdrop.afterpay-checkout-wrapper {
-webkit-box-align: start;
-ms-flex-align: start;
align-items: flex-start;
}
}

File diff suppressed because it is too large Load diff

View file

@ -11,16 +11,20 @@ const applicationConfig = {
SITE_ENTRY_TRIGGER_VALUE: "FixMyGlass",
CASH_PARENT_ACCOUNT_NUMBER: 167132,
MY_ACCOUNT: process.env.VUE_APP_MY_ACCOUNT,
PAYPAL_SUCCESS_URL:
PIA_RESPONSE_URL:
location.protocol +
"//" +
location.host +
"/fmg/?fmgPage=payment-pia-return&src=concept-funnel",
PAYPAL_CANCEL_URL:
PIA_CANCEL_URL:
location.protocol +
"//" +
location.host +
"/fmg/?fmgPage=payment-method&src=concept-funnel",
GOOGLE_CALENDAR: "https://www.google.com/calendar/render?action=TEMPLATE",
YAHOO_CALENDAR: "https://calendar.yahoo.com/?v=60",
OUTLOOK_CALENDAR:
"https://outlook.office.com/calendar/deeplink/compose?path=/calendar/action/compose&rru=addevent",
};
export { applicationConfig };

View file

@ -5,9 +5,3 @@ export const paymentMethods = {
PAYPAL: "Paypal",
AFTERPAY: "Afterpay",
};
export const paymentTimes = {
NONE: null,
ADVANCE: "Advance",
LATER: "Later",
};

View file

@ -1,3 +1,4 @@
// Note: querystring-helper will change all keys to lowercase so make sure they are here as well
const queryStrings = {
FMG_PAGE: "fmgPage",
START_TYPE: "start_type",
@ -6,6 +7,18 @@ const queryStrings = {
ERROR: "error",
TOKEN: "token",
PAYERID: "payerid",
SUBSCRIPTIONID: "subscriptionid",
REFERRAL_SEQ_NUM: "referralseqnum",
CARD_EXPIRATION_MONTH: "card_expirationmonth",
CARD_EXPIRATION_YEAR: "card_expirationyear",
CARD_TYPE: "sgcardtype",
BILL_TO_POSTAL_CODE: "billto_postalcode",
BILL_TO_FIRST_NAME: "billto_firstname",
BILL_TO_LAST_NAME: "billto_lastname",
REFERENCE_NUMBER: "req_reference_number",
AUTH_CODE: "auth_code",
TRANSACTION_ID: "transaction_id",
TRANS_REFERENCE_NUMBER: "auth_trans_ref_no",
};
export { queryStrings };

View file

@ -0,0 +1,6 @@
const serviceType = {
REPLACEMENT: "replacement",
REPAIR: "repair",
REPLACEMENT_AND_RECALIBRATION: "replacement and recalibration",
};
export { serviceType };

View file

@ -80,7 +80,7 @@ const storeActions = {
SAVE_MOLDING_QUESTION_ANSWERS: "saveMoldingQuestionAnswers",
SAVE_CAPABILITY_QUESTION_ANSWERS: "saveCapabilityQuestionAnswers",
SAVE_PAYMENT_TYPE: "savePaymentType",
SAVE_PAY_NOW_TYPE: "savePayNowType",
SAVE_PAYMENT_METHOD_CHOICE: "savePaymentMethodChoice",
SAVE_PARENT_ACCOUNT_NUMBER: "saveParentAccountNumber",
SAVE_SUPPORTING_ITEMS: "saveSupportingItems",
SAVE_SUPPORTING_ITEMS_SUPPRESSING_STATE_RESETTING:
@ -92,6 +92,8 @@ const storeActions = {
SAVE_WORK_ORDER_FLAG: "saveWorkOrderFlag",
SAVE_PIA_ERROR_CODE: "savePiaErrorCode",
SAVE_PIA_WORK_ORDER: "savePiaWorkOrder",
SAVE_CCTOKEN: "saveCCToken",
SAVE_PAYPAL_TOKEN: "savePaypalToken",
};
export { storeActions };

View file

@ -1,7 +1,10 @@
const storeMutations = {
// PAYMENT MUTATIONS
UPDATE_WORK_ORDER_FLAG: "updateWorkOrderFlag",
UPDATE_PIA_ERROR_CODE: "updatePiaErrorCode",
UPDATE_PIA_WORK_ORDER: "updatePiaWorkOrder",
UPDATE_CCTOKEN: "updateCCToken",
UPDATE_PAYPAL_TOKEN: "updatePaypalToken",
// VEHICLE MUTATIONS
UPDATE_YEAR: "updateYear",
@ -50,7 +53,8 @@ const storeMutations = {
UPDATE_IS_INSURANCE: "updateIsInsurance",
UPDATE_SAVED_SESSION_ID: "updateSavedSessionId",
UPDATE_CRM_CUSTOMER_ID: "updateCrmCustomerId",
UPDATE_PAY_NOW_TYPE: "updatePayNowType",
UPDATE_IS_PIA: "updateIsPia",
UPDATE_PIA_TYPE: "updatePiaType",
WORK_ORDER_NUMBER: "updateWorkOrderNumber",
// EVENT BUS MUTATIONS
@ -68,6 +72,7 @@ const storeMutations = {
RESET_SERVICE_LOCATION_PROVIDER: "resetServiceLocationProvider",
RESET_SERVICE_LOCATION_MOBILE_ADDRESS: "resetServiceLocationMobileAddress",
RESET_SCHEDULE: "resetSchedule",
RESET_PAYMENT_METHOD_CHOICE: "resetPaymentMethodChoice",
// OTHER MUTATIONS
UPDATE_PAGE_DATA: "updatePageData",

View file

@ -72,7 +72,10 @@ https://safelite.atlassian.net/wiki/spaces/DC/pages/76644418/Button+Question+Com
</div>
</fieldset>
</div>
<div class="row form-test-error">
<div
id="form-test-error"
class="row form-test-error"
:class="isErrorCentered ? 'text-center' : ''">
<error-message
role="comment"
aria-atomic="true"
@ -227,8 +230,18 @@ export default {
classes += " two-list-card-width";
}
if (this.buttonTypeString == "listCard" && this.buttonsInfo.length === 1) {
classes += " one-list-card-width";
}
return classes;
},
isErrorCentered() {
if (this.buttonTypeString == "listCard" && this.buttonsInfo.length <= 2) {
return true;
}
return false;
},
buttonsInfo() {
return (Array.isArray(this.answers) ? this.answers : [])?.map((answer) => ({
buttonLabel: answer.buttonLabel ?? answer.Text ?? answer,

View file

@ -1,16 +1,18 @@
<template>
<!-- Checkbox groups MUST be wrapped in a <fieldset> and <legend> tag -->
<div class="form-check ui-checkbox" :class="[hasError ? 'has-error' : '']">
<input
v-model="value"
class="form-check-input"
type="checkbox"
aria-checked="false"
:name="checkboxName"
:id="buttonID"
:tabindex="tabIndex"
:aria-required="isRequired" />
<div
class="form-check ui-checkbox d-flex align-items-center"
:class="[hasError ? 'has-error' : '']">
<label class="d-flex align-items-start" :for="buttonID">
<input
v-model="value"
class="form-check-input me-2"
type="checkbox"
aria-checked="false"
:name="checkboxName"
:id="buttonID"
:tabindex="tabIndex"
:aria-required="isRequired" />
<p v-html="checkboxLabelCopy" class="m-0"></p>
<span v-if="screenReaderOnlyText" class="sr-only">{{ screenReaderOnlyText }}</span>
</label>

View file

@ -22,7 +22,7 @@
@mousedown="closeModal"
aria-label="Close"></button>
</div>
<div class="modal-body ps-5 pe-5 pb-4 pt-0">
<div class="modal-body ps-5 pe-5 pb-4 pt-1">
<slot></slot>
</div>
<div class="modal-footer px-5 py-4">
@ -179,7 +179,7 @@ export default {
}
}
@include media-breakpoint-up(md) {
overflow: scroll;
overflow: auto;
flex: none;
}
}
@ -198,10 +198,6 @@ export default {
button {
margin: 0;
}
@include media-breakpoint-up(md) {
position: relative;
background-color: transparent;
}
}
}
&.show .modal-dialog {

View file

@ -67,7 +67,7 @@
</div>
<div v-show="errorMessage" class="row form-test-error">
<span
class="d-inline-flex small my-1"
class="d-inline-flex small mt-1"
aria-atomic="true"
aria-live="polite"
:class="[centerErrorMessage ? 'center-error-message' : '']"

View file

@ -44,13 +44,13 @@ export default {
showAddRainDefense() {
// if not found in the cart
return !this.cartItems.some((vap) => {
return vap.partType === "RAIN DEFENSE";
return vap?.partType === "RAIN DEFENSE";
});
},
showAddWipers() {
// if not found in the cart
return !this.cartItems.some((vap) => {
return vap.partType.includes(" WIPER");
return vap?.partType?.includes(" WIPER");
});
},
rainDefensePrice() {
@ -59,7 +59,7 @@ export default {
return item.partType === "RAIN DEFENSE";
});
const price = lineItem.laborAmount + lineItem.sellingPrice + lineItem.kitPrice;
const price = lineItem?.laborAmount + lineItem?.sellingPrice + lineItem?.kitPrice;
return parseFloat(price).toFixed(2);
},
wipersPrice() {
@ -68,7 +68,7 @@ export default {
return item.partType.includes(" WIPER");
});
lineItems?.forEach((item) => {
price += item.laborAmount + item.sellingPrice + item.kitPrice;
price += item?.laborAmount + item?.sellingPrice + item?.kitPrice;
});
return parseFloat(price).toFixed(2);
},
@ -87,12 +87,13 @@ export default {
};
if (this.showAddWipers) {
const answer = getAnswer("ADD WIPER BLADES", this.answersCmsData);
const answer = getAnswer("WipersModal", this.answersCmsData);
answer.SubText = "+$" + this.wipersPrice;
answers.push(answer);
}
if (this.showAddRainDefense) {
const answer = getAnswer("ADD RAIN DEFENSE", this.answersCmsData);
const answer = getAnswer("RainDefenseModal", this.answersCmsData);
answer.SubText = "+$" + this.rainDefensePrice;
answers.push(answer);
}

View file

@ -7,12 +7,14 @@
@click="toggleClass()">
<div class="col d-flex justify-content-between">
<span class="label">Amount Due</span
><span class="label amount-due">{{ amountDue }}</span>
><span class="label amount-due">{{ currencyFormatter.format(amountDue) }}</span>
</div>
</div>
<div class="price-table">
<div class="service-type" :class="{ packageWithVAPS: packageWithVAPS }">
<textBlock :cmsWidgetName="packageNameWidget" /><span>{{ packagePrice }}</span>
<textBlock :cmsWidgetName="packageNameWidget" /><span>{{
currencyFormatter.format(packagePrice)
}}</span>
</div>
<div
v-for="(lineItem, i) in displayedPackageLineItems"
@ -22,7 +24,7 @@
<textLink
ref="removeLink"
linkType="text"
text="showMoreShopsLinkText"
text="removeLinkText"
href="#!"
@click-event="removeItem(lineItem)" />
</div>
@ -35,7 +37,7 @@
<textLink
ref="removeLink"
linkType="text"
text="showMoreShopsLinkText"
text="removeLinkText"
href="#!"
@click-event="removeItem(lineItem)" />
<span>{{ lineItem.price }}</span>
@ -45,9 +47,15 @@
><span>{{ recycleFee }}</span>
</div>
<!-- Sub total, sales tax, total columns -->
<div class="sub-total"><span>Subtotal</span><span>$320</span></div>
<div class="sales-tax"><span>Sales tax</span><span>$1</span></div>
<div class="amount-due"><span>Amount due</span><span>$321</span></div>
<div class="sub-total">
<span>Subtotal</span><span>{{ currencyFormatter.format(subTotal) }}</span>
</div>
<div class="sales-tax">
<span>Sales tax</span><span>{{ currencyFormatter.format(salesTax) }}</span>
</div>
<div class="amount-due">
<span>Amount due</span><span>{{ currencyFormatter.format(amountDue) }}</span>
</div>
</div>
</div>
</div>
@ -76,28 +84,16 @@ export default {
return {
isActive: false,
packageWithVAPS: false,
amountDue: this.getAmountDue(),
currencyFormatter: new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}),
};
},
methods: {
toggleClass: function (event) {
this.isActive = !this.isActive;
},
getPackagePriceString(packageName) {
const formattedPriceFloat = parseFloat(this.getPackagePrice(packageName)).toFixed(2);
return (this.isInsuranceSelected ? "As little as $" : "$") + formattedPriceFloat;
},
getPackagePrice(packageName) {
let priceFloat = this.isInsuranceSelected
? 0
: baseMixin.methods.getTierOnePackagePrice(
baseMixin.methods.filterOutFees(this.availableLineItems)
);
priceFloat += this.getVapsPrice(packageName);
return priceFloat;
},
getVapsPrice(packageName) {
const vapsItems = this.getVapsLineItemsForSelectedPackage(packageName);
@ -157,19 +153,24 @@ export default {
this.isRepair,
this.vaps
);
return tier;
},
packagePrice() {
let packagePriceString;
if (this.packageLevel.length > 0) {
packagePriceString = this.getPackagePriceString(this.packageLevel);
}
return packagePriceString;
// return (packagePriceString.length > 0) ? packagePriceString : "Test String";
let packagePrice = this.isInsuranceSelected
? 0
: baseMixin.methods.getTierOnePackagePrice(
baseMixin.methods.filterOutFees(this.availableLineItems)
);
packagePrice += this.getVapsPrice(this.packageLevel);
return packagePrice;
},
displayedPackageLineItems() {
const displayedPackageLineItems = [];
const packageLineItems = this.getVapsLineItemsForSelectedPackage(this.packageLevel);
// TODO Determine if we could get the same list from this.lineItems?
packageLineItems?.forEach((item) => {
let price = item.sellingPrice + item.kitPrice + item.laborAmount;
@ -181,6 +182,7 @@ export default {
};
displayedPackageLineItems.push(packageLineItem);
});
return displayedPackageLineItems;
},
displayedNonPackageLineItems() {
@ -229,6 +231,9 @@ export default {
const supportingItems = this.lineItems?.supportingItems;
return supportingItems?.length > 0 ? supportingItems : [];
},
removeLinkText() {
return "Remove"; // TODO: get from CMS
},
recycleLabel() {
return "Recycling";
},
@ -244,6 +249,29 @@ export default {
// }
return true;
},
subTotal() {
let subTotal = 0;
this.lineItems.forEach((lineItem) => {
subTotal +=
(lineItem.kitPrice ?? 0) +
(lineItem.laborAmount ?? 0) +
(lineItem.sellingPrice ?? 0);
});
return subTotal;
},
salesTax() {
let salesTax = 0;
this.lineItems.forEach((lineItem) => {
salesTax += lineItem.salesTax;
});
return salesTax;
},
amountDue() {
return this.subTotal + this.salesTax;
},
},
components: {
textBlock,

View file

@ -12,6 +12,7 @@
class="my-4 caption modal-sub-body"
v-if="ModalSubBodyText"
v-html="ModalSubBodyText"></p>
<slot></slot>
</modal>
</template>
@ -22,6 +23,10 @@ export default {
name: "content-group-modal",
props: {
cmsWidgetName: String,
footerButtonActionName: {
type: String,
default: null,
},
},
computed: {
ModalName() {
@ -50,10 +55,16 @@ export default {
openModal() {
this.$refs[this.ModalName].openModal();
},
footerButtonClick() {
closeModal() {
this.$refs[this.ModalName].closeModal();
},
footerButtonClick() {
if (this.footerButtonActionName) {
this.$emit(this.footerButtonActionName, this.ModalName);
} else {
this.closeModal();
}
},
},
components: {
modal,

View file

@ -7,14 +7,17 @@
<funnelHeader cmsWidgetName="FunnelHeaderWidget" />
</div>
</div>
<div class="row justify-content-md-center">
<div class="col-md-6">
<div class="row justify-content-center">
<div class="col-md-6 col-xl-4">
<vehicleBanner
cmsWidgetName="VehicleBannerWidget"
:displayGenericVehicleImage="false" />
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" />
</div>
</div>
<div class="row justify-content-center">
<div class="col-md-6 col-xl-4">
<alert
ref="alertFewMoreQuestions"
cmsWidgetName="alertWidget"
@ -25,7 +28,7 @@
v-bind:isDismissible="false" />
</div>
</div>
<div class="row justify-content-md-center">
<div class="row justify-content-center">
<div class="col-md-6 col-xl-4">
<div v-for="(questionsDatum, i) in questionsData" :key="questionsDatum.key">
<questionChain

View file

@ -16,7 +16,8 @@
@click-event="buttonClick"
data-bs-target="#footerModal"
data-test-id="nav-bar-main-button"
data-bs-dismiss="modal" />
data-bs-dismiss="modal"
v-if="!isSubmitHidden" />
</div>
<div v-if="!isBackButtonHidden" class="col-auto link-col py-1 text-break">
<textLink
@ -44,6 +45,7 @@ export default {
isBackButtonHidden: { type: Boolean, default: false },
cmsWidgetName: String,
buttonSize: { type: Boolean, default: false },
isSubmitHidden: { type: Boolean, default: false },
},
components: {
textLink,

View file

@ -1,5 +1,5 @@
<template>
<div class="vehicle_banner mb-3 text-center">
<div class="vehicle_banner mb-4 text-center">
<img class="vehicle-image img-fluid" :src="vehicleImageToDisplay" alt="" />
</div>
</template>

View file

@ -7,22 +7,6 @@ export function getDateDifferenceInDays(startDate, endDate) {
// Convert milliseconds to days and return the result
return difference / (1000 * 3600 * 24);
}
export function getFullDayName(date) {
// Use a ternary operator to check if the input is a valid date object
return date instanceof Date
? // Use the built-in method toLocaleDateString() to get the full day name in the current locale
date.toLocaleDateString(undefined, { weekday: "long" })
: // Return undefined if the input is not a valid date object
undefined;
}
export function getFullMonthName(date) {
// Use a ternary operator to check if the input is a valid date object
return date instanceof Date
? // Use the built-in method toLocaleDateString() to get the full month name in the current locale
date.toLocaleDateString(undefined, { month: "long" })
: // Return undefined if the input is not a valid date object
undefined;
}
export function get12HourTimeFormat(time) {
// Check correct time format and split into components
time = time.toString().match(/^([01]\d|2[0-3])(:)([0-5]\d)?$/) || [time];
@ -112,7 +96,7 @@ export function shortTimeString(date) {
// Use a ternary operator to check if the input is a valid date object
return date instanceof Date
? // Use the built-in method toLocaleTimeString() to get the short time string in the current locale
date.toLocaleTimeString(undefined, { hour: "numeric", minute: "numeric", hour12: true })
date.toLocaleTimeString("en-us", { hour: "numeric", minute: "numeric", hour12: true })
: // Return undefined if the input is not a valid date object
undefined;
}

View file

@ -1,6 +1,4 @@
import {
getFullDayName,
getFullMonthName,
get12HourTimeFormat,
get12HourTimeMobileFormat,
getDateFormat,
@ -52,33 +50,17 @@ describe("date-helper.js", () => {
}
});
// it("getFullMonthName should return full month format.", () => {
// // Arrange / Act
// const date = new Date("2023-10-01");
// const monthName = getFullMonthName(date);
it("getDateFormat should return date in the given format.", () => {
// Arrange / Act
const dateString = "2023-10-01";
const dateParts = dateString.split("-");
const date = new Date(dateParts[0], parseInt(dateParts[1]) - 1, dateParts[2]);
const format = "yyyy-MM-dd";
const formattedDate = getDateFormat(date, format);
// // Assert
// expect(monthName).toEqual("October");
// });
// it("getFullDayName should return full Day Name format.", () => {
// // Arrange / Act
// const date = new Date("2023-10-01");
// const dayName = getFullDayName(date);
// // Assert
// expect(dayName).toEqual("Sunday");
// });
// it("getDateFormat should return date in the given format.", () => {
// // Arrange / Act
// const date = new Date("2023-10-01");
// const format = "yyyy-MM-dd";
// const formattedDate = getDateFormat(date, format);
// // Assert
// expect(formattedDate).toEqual("2023-10-01");
// });
// Assert
expect(formattedDate).toEqual("2023-10-01");
});
it("should return the correct difference in days", function () {
// Define some sample dates and their expected differences

View file

@ -39,12 +39,14 @@ export function getAvailablePackages(glassToReplace, availableLineItems, isRepai
isRepair,
packageNames.TIER_ONE
);
let tierTwoVaps = getPackageContents(
glassToReplace,
availableLineItems,
isRepair,
packageNames.TIER_TWO
);
let tierThreeVaps = getPackageContents(
glassToReplace,
availableLineItems,
@ -87,9 +89,11 @@ export function getPackageContents(glassToReplace, availableLineItems, isRepair,
if (shouldFrontWipersBeAvailable(glassToReplace, availableLineItems, isRepair, targetTier)) {
vaps.push(partTypeStrings.FRONT_WIPER);
}
if (shouldRearWipersBeAvailable(glassToReplace, availableLineItems, isRepair, targetTier)) {
vaps.push(partTypeStrings.REAR_WIPER);
}
if (shouldRainDefenseBeAvailable(glassToReplace, availableLineItems, isRepair, targetTier)) {
vaps.push(partTypeStrings.RAIN_DEFENSE);
}
@ -107,6 +111,7 @@ export function shouldFrontWipersBeAvailable(
partTypeStrings.FRONT_WIPER,
availableLineItems
);
const isFrontWindshieldTask =
isRepair || containsGlassPieceWithLocation(glassLocations.WINDSHIELD, glassToReplace);

View file

@ -1,250 +0,0 @@
// Components
import addToCalendar from "@/layouts/add-to-calendar/add-to-calendar.vue";
import { calendarOptions } from "@/constants/calendar-options";
import { AppointmentTypeStrings, RouteCodeFlags } from "@/constants/schedule-constants";
// Supporting Files
import { shallowMount } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import store from "@/store";
beforeEach(() => {
jest.restoreAllMocks();
jest.clearAllMocks();
store.getters = {
order: {
schedule: {
date: "2019-01-01",
startTime: "09:00",
endTime: "10:00",
routeCode: "000",
},
lineItems: {
glassParts: [
{
partNumber: "ABC123",
},
],
supportingItems: [],
},
serviceLocation: {
address: "",
address2: null,
city: "",
state: "AZ",
appointmentType: "Inshop",
zipCode: "12345",
zipCodeCtu: "01234",
provider: {
providerNumber: "123",
address: {
streetAddress: "test1",
city: "test",
state: "AZ",
zipCode: "12345",
zipCodeCtu: "01234",
},
},
},
damage: {
isRepair: false,
},
referralNumber: "1234567",
},
payment: {
isInsurance: true,
},
lineItems: {
glassParts: [],
supportingItems: [],
},
};
});
afterEach(() => {
store.getters = {};
jest.restoreAllMocks();
jest.clearAllMocks();
});
describe("Add-to-calendar methods...", () => {
test("Add-to-calendar should trigger openModal method", () => {
//Arrange
const { wrapper } = setupMocks({});
//Act
wrapper.vm.openCalendarModal();
//Assert
expect(wrapper.vm.$refs.calendarModalQuestion.openModal).toBeCalled();
});
test("serviceType should return 'replacement and recalibration' when isRepair and funnelHasRecalibrationPart true.", () => {
//Arrange
store.getters.order.damage.isRepair = true;
store.getters.funnelHasRecalibrationPart = true;
const { wrapper } = setupMocks({});
//Act
const testValue = wrapper.vm.serviceType();
//Assert
expect(testValue).toEqual("replacement and recalibration");
});
test("serviceType should return 'replacement' when isRepair is true and funnelHasRecalibrationPart is false.", () => {
//Arrange
store.getters.order.damage.isRepair = true;
store.getters.funnelHasRecalibrationPart = false;
const { wrapper } = setupMocks({});
//Act
const testValue = wrapper.vm.serviceType();
//Assert
expect(testValue).toEqual("replacement");
});
test("serviceType should return 'repair' when isRepair is false.", () => {
//Arrange
store.getters.order.damage.isRepair = false;
const { wrapper } = setupMocks({});
//Act
const testValue = wrapper.vm.serviceType();
//Assert
expect(testValue).toEqual("repair");
});
test("getCalendarData should return expected model value.", () => {
//Arrange
const type = calendarOptions.OUTLOOKCOM;
const { wrapper } = setupMocks({});
//Act
const testValue = wrapper.vm.getCalendarData(type);
//Assert
expect(testValue.name).toEqual(calendarOptions.OUTLOOKCOM);
});
test("getappointmentData should return expected model value for appointmentType mobile.", () => {
//Arrange
store.getters.order.serviceLocation.appointmentType = AppointmentTypeStrings.MOBILE;
store.getters.order.damage.isRepair = true;
store.getters.funnelHasRecalibrationPart = true;
const { wrapper } = setupMocks({});
//Act
const testValue = wrapper.vm.getappointmentData();
//Assert
expect(testValue.Subject).toEqual("Safelite replacement and recalibration appointment");
});
test("getappointmentData should return expected model value for appointmentType DROP_OFF and OVERNIGHT_DROP_OFF.", () => {
//Arrange
store.getters.order.serviceLocation.appointmentType = AppointmentTypeStrings.DROP_OFF;
store.getters.order.schedule.routeCode = RouteCodeFlags.OVERNIGHT_DROP_OFF;
const { wrapper } = setupMocks({});
//Act
const testValue = wrapper.vm.getappointmentData();
//Assert
expect(testValue.Subject).toEqual("Drop off your vehicle by 5:30 PM");
});
test("getappointmentData should return expected model value for appointmentType DROP_OFF and ALL_DAY_DROP_OFF and isSameDayDropOff.", () => {
//Arrange
store.getters.order.serviceLocation.appointmentType = AppointmentTypeStrings.DROP_OFF;
store.getters.order.schedule.routeCode = RouteCodeFlags.ALL_DAY_DROP_OFF;
store.getters.order.schedule.date = new Date().toISOString().split("T")[0];
const { wrapper } = setupMocks({});
//Act
const testValue = wrapper.vm.getappointmentData();
//Assert
expect(testValue.Subject).toEqual("Drop off your vehicle in the next 30 - 60 minutes");
});
test("getappointmentData should return expected model value for appointmentType DROP_OFF and ALL_DAY_DROP_OFF.", () => {
//Arrange
store.getters.order.serviceLocation.appointmentType = AppointmentTypeStrings.DROP_OFF;
store.getters.order.schedule.routeCode = RouteCodeFlags.ALL_DAY_DROP_OFF;
const { wrapper } = setupMocks({});
//Act
const testValue = wrapper.vm.getappointmentData();
//Assert
expect(testValue.Subject).toEqual("Drop off your vehicle between 9:00 AM - 11:00 AM");
});
test("getappointmentData should return expected model value for appointmentType DROP_OFF.", () => {
//Arrange
store.getters.order.serviceLocation.appointmentType = AppointmentTypeStrings.DROP_OFF;
store.getters.order.damage.isRepair = false;
const { wrapper } = setupMocks({});
//Act
const testValue = wrapper.vm.getappointmentData();
//Assert
expect(testValue.Subject).toEqual("Safelite repair appointment");
});
test("getappointmentData should return expected model value for appointmentType IN_SHOP.", () => {
//Arrange
store.getters.order.serviceLocation.appointmentType = AppointmentTypeStrings.IN_SHOP;
store.getters.order.damage.isRepair = false;
const { wrapper } = setupMocks({});
//Act
const testValue = wrapper.vm.getappointmentData();
//Assert
expect(testValue.Subject).toEqual("Safelite repair appointment");
});
test("isSameDayDropOff should return true for current schedule date.", () => {
//Arrange
store.getters.order.schedule.date = new Date().toISOString().split("T")[0];
const { wrapper } = setupMocks({});
//Act
const testValue = wrapper.vm.isSameDayDropOff();
//Assert
expect(testValue).toEqual(true);
});
test("setCalendarAppointment should call getAppointment method for iCAL and Outlook.", () => {
//Arrange
const newValue = { name: calendarOptions.OUTLOOK };
const { wrapper } = setupMocks({});
wrapper.vm.getAppointment = jest.fn();
//Act
wrapper.vm.setCalendarAppointment(newValue);
//Assert
expect(wrapper.vm.getAppointment).toHaveBeenCalled();
});
test("setCalendarAppointment should call window.open for Google, yahoo and Outlook.com.", () => {
//Arrange
const newValue = { name: calendarOptions.GOOGLE };
const { wrapper } = setupMocks({});
window.open = jest.fn();
//Act
wrapper.vm.setCalendarAppointment(newValue);
//Assert
expect(window.open).toHaveBeenCalledTimes(1);
});
});
function setupMocks({ customMountOptions }) {
const mountOptions = getMountOptions({
...customMountOptions,
});
const wrapper = shallowMount(addToCalendar, mountOptions);
//wrapper.vm.setCmsContent = jest.fn();
//wrapper.vm.$refs.datePicker.initializeComponent = jest.fn();
//wrapper.vm.$refs.locationAlerts.initializeComponent = jest.fn();
//wrapper.vm.$refs.funnelFooter.updateButtonText = jest.fn();
wrapper.vm.$refs.calendarModalQuestion.openModal = jest.fn();
return { wrapper };
}

View file

@ -1,300 +0,0 @@
<template>
<div class="calendar-section">
<div class="add-to-calendar">
<button v-on:click="openCalendarModal">
<span class="add-to-calendar-span">
<img src="@/assets/img/icons/add-to-calendar.svg" class="add-to-calendar-img" />
<span class="add-to-calendar-text">Add to calendar</span></span
>
</button>
</div>
<calendarModalQuestion
ref="calendarModalQuestion"
customComponentId="calendarModalQuestion"
cmsWidgetName="CalendarModalQuestion"
@calendar-modal-closed="calendarModalClosed"
:calendarOptionsData="calendarValues"
v-model="selectedCalendarOption">
</calendarModalQuestion>
</div>
</template>
<script>
import { calendarOptions } from "@/constants/calendar-options";
import { calendarStatus } from "@/constants/calendar-status";
import calendarModalQuestion from "@/layouts/add-to-calendar/calendar-modal-question/calendar-modal-question";
import {
getDateFormat,
calculateDuration,
combineDateAndTime,
shortTimeString,
addMinutes,
} from "@/helpers/date-helper";
import { getCalendarFile, download } from "@/helpers/add-to-calendar-helper";
import { AppointmentTypeStrings, RouteCodeFlags } from "@/constants/schedule-constants";
import store from "@/store";
export default {
name: "add-to-calendar",
components: {
calendarModalQuestion,
},
data() {
return {
appointment: this.getappointmentData(),
selectedCalendarOption: null,
};
},
computed: {
calendarValues: {
get: function () {
var obj = [];
const options = Object.values(calendarOptions);
for (var i = 0; i < options.length; i++) {
obj[i] = this.getCalendarData(options[i]);
}
return obj;
},
},
},
watch: {
selectedCalendarOption(newValue) {
if (newValue) {
this.setCalendarAppointment(newValue);
}
},
},
methods: {
setCalendarAppointment(calendarOption) {
if (
calendarOption.name.includes(calendarOptions.ICAL) ||
calendarOption.name.includes(calendarOptions.OUTLOOK)
) {
this.getAppointment();
} else {
window.open(calendarOption.url);
}
},
calendarModalClosed() {
// Clear the selectedOption if close the modal
this.selectedCalendarOption = null;
},
openCalendarModal() {
this.$refs["calendarModalQuestion"].openModal();
},
scheduleDate() {
return store.getters.order.schedule.date;
},
scheduleStartTime() {
return store.getters.order.schedule.startTime;
},
scheduleEndTime() {
return store.getters.order.schedule.endTime;
},
serviceLocationAddress() {
return store.getters.order.serviceLocation.address;
},
ServiceLocationAddress2() {
return store.getters.order.serviceLocation.address2;
},
serviceLocationCity() {
return store.getters.order.serviceLocation.city;
},
serviceLocationState() {
return store.getters.order.serviceLocation.state;
},
serviceLocationZipCode() {
return store.getters.order.serviceLocation.zipCode;
},
providerAddress() {
return store.getters.order.serviceLocation.provider.address.streetAddress;
},
providerCity() {
return store.getters.order.serviceLocation.provider.address.city;
},
providerState() {
return store.getters.order.serviceLocation.provider.address.state;
},
providerZipCode() {
return store.getters.order.serviceLocation.provider.address.zipCode;
},
appointmentType() {
return store.getters.order.serviceLocation.appointmentType;
},
uniqueId() {
return store.getters.order.referralNumber?.toString();
},
serviceType() {
const isRepair = store.getters.order.damage.isRepair;
const funnelHasRecalibrationPart = store.getters.funnelHasRecalibrationPart;
if (isRepair) {
if (funnelHasRecalibrationPart) {
return "replacement and recalibration";
} else {
return "replacement";
}
}
return "repair";
},
routeCode() {
return store.getters.order.schedule.routeCode;
},
isSameDayDropOff() {
const todaysDate = new Date().toISOString().split("T")[0];
return this.scheduleDate() === todaysDate;
},
getCalendarData(type) {
var URL = "";
const dateFormat = "yyyyMMddTHHmmss";
const outlookComTimeSpanFormat = "yyyy-MM-ddTHH:mm:ss";
if (type == calendarOptions.OUTLOOKCOM) {
URL = `https://outlook.office.com/calendar/deeplink/compose?path=/calendar/action/compose&rru=addevent&startdt=${encodeURIComponent(
getDateFormat(this.appointment.StartDate, outlookComTimeSpanFormat)
)}&enddt=${encodeURIComponent(
getDateFormat(this.appointment.EndDate, outlookComTimeSpanFormat)
)}&subject=${encodeURIComponent(
this.appointment.Subject
)}&body=${encodeURIComponent(this.appointment.Body)}&location=${encodeURIComponent(
this.appointment.Location
)}`;
}
if (type == calendarOptions.GOOGLE) {
URL = `https://www.google.com/calendar/render?action=TEMPLATE&text=${encodeURIComponent(
this.appointment.Subject
)}&dates=${encodeURIComponent(
getDateFormat(this.appointment.StartDate, dateFormat)
)}/${encodeURIComponent(
getDateFormat(this.appointment.EndDate, dateFormat)
)}&details=${encodeURIComponent(
this.appointment.Body
)}&location=${encodeURIComponent(this.appointment.Location)}&sf=true&output=xml`;
}
if (type == calendarOptions.YAHOO) {
URL = `https://calendar.yahoo.com/?v=60&TITLE=${encodeURIComponent(
this.appointment.Subject
)}&DESC=${encodeURIComponent(this.appointment.Body)}&ST=${encodeURIComponent(
getDateFormat(this.appointment.StartDate, dateFormat)
)}&DUR=${this.appointment.Duration}&in_loc=${encodeURIComponent(
this.appointment.Location
)}`;
}
return {
url: URL,
name: type,
icon: require(`@/assets/img/icons/${type}.svg`),
};
},
getappointmentData() {
var subject = "";
var location = "";
var startDateTime = combineDateAndTime(this.scheduleDate(), this.scheduleStartTime());
var endDateTime = combineDateAndTime(this.scheduleDate(), this.scheduleEndTime());
var body = "";
var isHTML = false;
var duration = "";
if (this.appointmentType() === AppointmentTypeStrings.MOBILE) {
location = `${
this.ServiceLocationAddress2() ? this.ServiceLocationAddress2() + ", " : ""
}${this.serviceLocationAddress()}, ${this.serviceLocationCity()}, ${this.serviceLocationState()} ${this.serviceLocationZipCode()}`;
subject = `Safelite ${this.serviceType()} appointment`;
body =
"Your safety is our top priority, so we'll let you know who's coming to you and your tech will reach out to let you know when they're on the way.";
} else {
location = `${this.providerAddress()}, ${this.providerCity()}, ${this.providerState()} ${this.providerZipCode()}`;
if (this.appointmentType() === AppointmentTypeStrings.DROP_OFF) {
if (this.routeCode().includes(RouteCodeFlags.OVERNIGHT_DROP_OFF)) {
subject = "Drop off your vehicle by 5:30 PM";
body =
"Drop off your vehicle by 5:30 PM the night of your scheduled appointment. The shop will work with you on a convenient pick-up time.";
} else if (this.routeCode().includes(RouteCodeFlags.ALL_DAY_DROP_OFF)) {
if (this.isSameDayDropOff()) {
subject = "Drop off your vehicle in the next 30 - 60 minutes";
endDateTime = addMinutes(startDateTime, 120);
body =
"Drop your vehicle off in the next 30 - 60 minutes. We'll work your vehicle into our day's schedule. The shop will work with you on a convenient pick-up time.";
} else {
endDateTime = addMinutes(startDateTime, 120);
subject =
"Drop off your vehicle between " +
shortTimeString(startDateTime) +
" - " +
shortTimeString(endDateTime);
body = `Drop off your vehicle by ${shortTimeString(
endDateTime
)} and pick it up later in the day. When you arrive, park in an open space and follow the key drop instructions. We'll text you when your vehicle is ready.`;
}
} //normal time slot
else {
subject = `Safelite ${this.serviceType()} appointment`;
body = "Just a reminder that you're coming to us at " + location + ".";
}
} else {
subject = `Safelite ${this.serviceType()} appointment`;
body = "Just a reminder that you're coming to us at " + location + ".";
}
duration = calculateDuration(startDateTime, endDateTime);
}
return {
Subject: subject,
Location: location,
StartDate: startDateTime,
EndDate: endDateTime,
RefrrlSeqNum: this.uniqueId(),
Body: body,
IsHTML: isHTML,
Duration: duration,
};
},
getAppointment() {
let body = this.appointment.Body;
body = body.replace("|", "<").replace("~", ">");
const calFile = {
Location: this.appointment.Location,
Body: body,
IsHTML: this.appointment.IsHTML,
UniqueId: this.appointment.RefrrlSeqNum,
Subject: this.appointment.Subject,
StartDate: this.appointment.StartDate,
EndDate: this.appointment.EndDate,
Status: calendarStatus.BUSY,
};
var calBody = getCalendarFile(calFile);
download("SafeliteAppointment.ics", calBody);
},
},
};
</script>
<style scoped>
.add-to-calendar-text {
margin: 0 0 0 7px;
color: #1574a1;
vertical-align: middle;
font-weight: 500;
font-family: "Roboto";
}
.add-to-calendar button {
border: 0;
background-color: #fff;
}
.calendar-section {
display: table;
position: relative;
margin: 0 auto;
}
.calendar-section .add-to-calendar {
padding-top: 15px;
}
.calendar-section .add-to-calendar .add-to-calendar-span {
cursor: pointer;
}
.calendar-section .add-to-calendar .add-to-calendar-img {
display: inline;
width: 20px;
}
</style>

View file

@ -11,7 +11,7 @@
<funnelHeader cmsWidgetName="FunnelHeaderWidget" ref="funnelHeader" />
</div>
</div>
<div class="row justify-content-md-center">
<div class="row justify-content-center">
<div class="col-md-6 col-xl-4">
<vehicleBanner
cmsWidgetName="VehicleBannerWidget"

View file

@ -6,7 +6,7 @@
<funnelHeader cmsWidgetName="FunnelHeaderWidget" />
</div>
</div>
<div class="row justify-content-md-center">
<div class="row justify-content-center">
<div class="col-md-6 col-xl-4">
<vehicleBanner
cmsWidgetName="VehicleBannerWidget"

View file

@ -0,0 +1,196 @@
// Components
import addToCalendar from "@/layouts/confirmation/add-to-calendar/add-to-calendar.vue";
import { calendarOptions } from "@/constants/calendar-options";
import { AppointmentTypeStrings, RouteCodeFlags } from "@/constants/schedule-constants";
// Supporting Files
import { shallowMount } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import store from "@/store";
beforeEach(() => {
jest.restoreAllMocks();
jest.clearAllMocks();
store.getters = {
order: {
schedule: {
date: "2019-01-01",
startTime: "09:00",
endTime: "10:00",
routeCode: "000",
},
lineItems: {
glassParts: [
{
partNumber: "ABC123",
},
],
supportingItems: [],
},
serviceLocation: {
address: "",
address2: null,
city: "",
state: "AZ",
appointmentType: "Inshop",
zipCode: "12345",
zipCodeCtu: "01234",
provider: {
providerNumber: "123",
address: {
streetAddress: "test1",
city: "test",
state: "AZ",
zipCode: "12345",
zipCodeCtu: "01234",
},
},
},
damage: {
isRepair: false,
},
referralNumber: "1234567",
},
payment: {
isInsurance: true,
},
lineItems: {
glassParts: [],
supportingItems: [],
},
};
});
afterEach(() => {
store.getters = {};
jest.restoreAllMocks();
jest.clearAllMocks();
});
describe("Add-to-calendar methods...", () => {
test("Add-to-calendar should trigger openModal method", () => {
//Arrange
const { wrapper } = setupMocks({
customMountOptions: {
propsData: {
appointment: {
RefrrlSeqNum: "123456",
StartDate: new Date(2023, 9, 11, 12, 0, 0),
EndDate: new Date(2023, 9, 11, 13, 0, 0),
Subject: "Meeting with John",
Location: "Conference Room",
Body: "Discuss the project progress",
IsHTML: true,
Duration: "1220",
},
},
},
});
//Act
wrapper.vm.openCalendarModal();
//Assert
expect(wrapper.vm.$refs.calendarModalQuestion.openModal).toBeCalled();
});
test("getCalendarData should return expected model value.", () => {
//Arrange
const type = calendarOptions.OUTLOOKCOM;
const { wrapper } = setupMocks({
customMountOptions: {
propsData: {
appointment: {
RefrrlSeqNum: "123456",
StartDate: new Date(2023, 9, 11, 12, 0, 0),
EndDate: new Date(2023, 9, 11, 13, 0, 0),
Subject: "Meeting with John",
Location: "Conference Room",
Body: "Discuss the project progress",
IsHTML: true,
Duration: "1220",
},
},
},
});
//Act
const testValue = wrapper.vm.getCalendarData(type);
//Assert
expect(testValue.name).toEqual(calendarOptions.OUTLOOKCOM);
});
test("setCalendarAppointment should call getAppointment method for iCAL and Outlook.", () => {
//Arrange
const newValue = { name: calendarOptions.OUTLOOK };
const { wrapper } = setupMocks({
customMountOptions: {
propsData: {
appointment: {
RefrrlSeqNum: "123456",
StartDate: new Date(2023, 9, 11, 12, 0, 0),
EndDate: new Date(2023, 9, 11, 13, 0, 0),
Subject: "Meeting with John",
Location: "Conference Room",
Body: "Discuss the project progress",
IsHTML: true,
Duration: "1220",
},
},
},
});
wrapper.vm.getAppointment = jest.fn();
//Act
wrapper.vm.setCalendarAppointment(newValue);
//Assert
expect(wrapper.vm.getAppointment).toHaveBeenCalled();
});
test("setCalendarAppointment should call window.open for Google, yahoo and Outlook.com.", () => {
//Arrange
const newValue = { name: calendarOptions.GOOGLE };
const { wrapper } = setupMocks({
customMountOptions: {
propsData: {
appointment: {
RefrrlSeqNum: "123456",
StartDate: new Date(2023, 9, 11, 12, 0, 0),
EndDate: new Date(2023, 9, 11, 13, 0, 0),
Subject: "Meeting with John",
Location: "Conference Room",
Body: "Discuss the project progress",
IsHTML: true,
Duration: "1220",
},
},
},
});
window.open = jest.fn();
//Act
wrapper.vm.setCalendarAppointment(newValue);
//Assert
expect(window.open).toHaveBeenCalledTimes(1);
});
});
var appointmentText = "Appointment content";
function setupMocks({ customMountOptions }) {
const mountOptions = getMountOptions({
...customMountOptions,
});
mountOptions.mixins = [
{
methods: {
getCmsContent: jest.fn().mockImplementation(() => {
return appointmentText;
}),
},
},
];
const wrapper = shallowMount(addToCalendar, mountOptions);
wrapper.vm.$refs.calendarModalQuestion.openModal = jest.fn();
return { wrapper };
}

View file

@ -0,0 +1,314 @@
<template>
<div class="calendar-section">
<div class="add-to-calendar">
<button v-on:click="openCalendarModal">
<span class="add-to-calendar-span">
<img src="@/assets/img/icons/add-to-calendar.svg" class="add-to-calendar-img" />
<span class="add-to-calendar-text">Add to calendar</span></span
>
</button>
</div>
<calendarModalQuestion
ref="calendarModalQuestion"
customComponentId="calendarModalQuestion"
cmsWidgetName="CalendarModalQuestion"
@calendar-modal-closed="calendarModalClosed"
:calendarOptionsData="calendarValues"
v-model="selectedCalendarOption">
</calendarModalQuestion>
</div>
</template>
<script>
import { calendarOptions } from "@/constants/calendar-options";
import { calendarStatus } from "@/constants/calendar-status";
import calendarModalQuestion from "@/layouts/confirmation/add-to-calendar/calendar-modal-question/calendar-modal-question";
import { getDateFormat } from "@/helpers/date-helper";
import { AppointmentTypeStrings, RouteCodeFlags } from "@/constants/schedule-constants";
import { getCalendarFile, download } from "@/helpers/add-to-calendar-helper";
import {
calculateDuration,
combineDateAndTime,
shortTimeString,
addMinutes,
} from "@/helpers/date-helper";
import store from "@/store";
import { serviceType } from "@/constants/service-type";
import { applicationConfig } from "@/constants/application-config.js";
export default {
name: "add-to-calendar",
components: {
calendarModalQuestion,
},
props: {
mobileWidgetName: String,
inShopWidgetName: String,
dropOffWidgetName: String,
overnightDropOffWidgetName: String,
allDayDropOffWidgetName: String,
sameDayDropOffWidgetName: String,
serviceLocationFullAddress: String,
providerFullAddress: String,
appointmentType: String,
scheduleDate: String,
scheduleStartTime: String,
scheduleEndTime: String,
},
data() {
return {
selectedCalendarOption: null,
};
},
computed: {
calendarValues: {
get: function () {
var obj = [];
const options = Object.values(calendarOptions);
for (var i = 0; i < options.length; i++) {
obj[i] = this.getCalendarData(options[i]);
}
return obj;
},
},
IsSameDayDropOff() {
const todaysDate = new Date().toISOString().split("T")[0];
return this.scheduleDate === todaysDate;
},
AddToCalendar_Mobile_Subject() {
return this.getCmsContent(this.mobileWidgetName, "HeaderText");
},
AddToCalendar_Mobile_Body() {
return this.getCmsContent(this.mobileWidgetName, "BodyText");
},
AddToCalendar_InShop_Subject() {
return this.getCmsContent(this.inShopWidgetName, "HeaderText");
},
AddToCalendar_InShop_Body() {
return this.getCmsContent(this.inShopWidgetName, "BodyText");
},
AddToCalendar_DropOff_Subject() {
return this.getCmsContent(this.dropOffWidgetName, "HeaderText");
},
AddToCalendar_DropOff_Body() {
return this.getCmsContent(this.dropOffWidgetName, "BodyText");
},
AddToCalendar_OvernightDropOff_Subject() {
return this.getCmsContent(this.overnightDropOffWidgetName, "HeaderText");
},
AddToCalendar_OvernightDropOff_Body() {
return this.getCmsContent(this.overnightDropOffWidgetName, "BodyText");
},
AddToCalendar_AllDayDropOff_Subject() {
return this.getCmsContent(this.allDayDropOffWidgetName, "HeaderText");
},
AddToCalendar_AllDayDropOff_Body() {
return this.getCmsContent(this.allDayDropOffWidgetName, "BodyText");
},
AddToCalendar_SameDayDropOff_Subject() {
return this.getCmsContent(this.sameDayDropOffWidgetName, "HeaderText");
},
AddToCalendar_SameDayDropOff_Body() {
return this.getCmsContent(this.sameDayDropOffWidgetName, "BodyText");
},
UniqueId() {
return store.getters.order.referralNumber?.toString();
},
ServiceType() {
const isRepair = store.getters.order.damage.isRepair;
const funnelHasRecalibrationPart = store.getters.funnelHasRecalibrationPart;
if (!isRepair) {
if (funnelHasRecalibrationPart) {
return serviceType.REPLACEMENT_AND_RECALIBRATION;
} else {
return serviceType.REPLACEMENT;
}
}
return serviceType.REPAIR;
},
RouteCode() {
return store.getters.order.schedule.routeCode;
},
Appointment() {
var subject = "";
var location = "";
var startDateTime = combineDateAndTime(this.scheduleDate, this.scheduleStartTime);
var endDateTime = combineDateAndTime(this.scheduleDate, this.scheduleEndTime);
var body = "";
var isHTML = false;
var duration = "";
if (this.appointmentType === AppointmentTypeStrings.MOBILE) {
location = this.serviceLocationFullAddress?.replace("<br/>", "");
subject = this.AddToCalendar_Mobile_Subject?.replace(
"{custom:SERVICETYPE}",
this.ServiceType
);
body = this.AddToCalendar_Mobile_Body;
} else {
location = this.providerFullAddress?.replace("<br/>", "");
if (this.appointmentType === AppointmentTypeStrings.DROP_OFF) {
if (this.RouteCode.includes(RouteCodeFlags.OVERNIGHT_DROP_OFF)) {
subject = this.AddToCalendar_OvernightDropOff_Subject;
body = this.AddToCalendar_OvernightDropOff_Body;
} else if (this.RouteCode.includes(RouteCodeFlags.ALL_DAY_DROP_OFF)) {
if (this.IsSameDayDropOff) {
subject = this.AddToCalendar_SameDayDropOff_Subject;
endDateTime = addMinutes(startDateTime, 120);
body = this.AddToCalendar_SameDayDropOff_Body;
} else {
endDateTime = addMinutes(startDateTime, 120);
subject = this.AddToCalendar_AllDayDropOff_Subject?.replace(
"{custom:STARTDATETIME}",
shortTimeString(startDateTime)
).replace("{custom:ENDDATETIME}", shortTimeString(endDateTime));
body = this.AddToCalendar_AllDayDropOff_Body?.replace(
"{custom:ENDDATETIME}",
shortTimeString(endDateTime)
);
}
} //normal time slot
else {
subject = this.AddToCalendar_DropOff_Subject?.replace(
"{custom:SERVICETYPE}",
this.ServiceType
);
body = this.AddToCalendar_DropOff_Body?.replace(
"{custom:ADDRESS}",
location
);
}
} else {
subject = this.AddToCalendar_InShop_Subject?.replace(
"{custom:SERVICETYPE}",
this.ServiceType
);
body = this.AddToCalendar_InShop_Body?.replace("{custom:ADDRESS}", location);
}
duration = calculateDuration(startDateTime, endDateTime);
}
return {
Subject: subject,
Location: location,
StartDate: startDateTime,
EndDate: endDateTime,
RefrrlSeqNum: this.UniqueId,
Body: body,
IsHTML: isHTML,
Duration: duration,
};
},
},
watch: {
selectedCalendarOption(newValue) {
if (newValue) {
this.setCalendarAppointment(newValue);
}
},
},
methods: {
setCalendarAppointment(calendarOption) {
if (
calendarOption.name == calendarOptions.ICAL ||
calendarOption.name == calendarOptions.OUTLOOK
) {
this.getAppointment();
} else {
window.open(calendarOption.url);
}
},
calendarModalClosed() {
// Clear the selectedOption if close the modal
this.selectedCalendarOption = null;
},
openCalendarModal() {
this.$refs["calendarModalQuestion"].openModal();
},
getCalendarData(type) {
var URL = "";
const dateFormat = "yyyyMMddTHHmmss";
const outlookComTimeSpanFormat = "yyyy-MM-ddTHH:mm:ss";
if (type == calendarOptions.OUTLOOKCOM) {
URL = `${applicationConfig.OUTLOOK_CALENDAR}&startdt=${encodeURIComponent(
getDateFormat(this.Appointment.StartDate, outlookComTimeSpanFormat)
)}&enddt=${encodeURIComponent(
getDateFormat(this.Appointment.EndDate, outlookComTimeSpanFormat)
)}&subject=${encodeURIComponent(
this.Appointment.Subject
)}&body=${encodeURIComponent(this.Appointment.Body)}&location=${encodeURIComponent(
this.Appointment.Location
)}`;
}
if (type == calendarOptions.GOOGLE) {
URL = `${applicationConfig.GOOGLE_CALENDAR}&text=${encodeURIComponent(
this.Appointment.Subject
)}&dates=${encodeURIComponent(
getDateFormat(this.Appointment.StartDate, dateFormat)
)}/${encodeURIComponent(
getDateFormat(this.Appointment.EndDate, dateFormat)
)}&details=${encodeURIComponent(
this.Appointment.Body
)}&location=${encodeURIComponent(this.Appointment.Location)}&sf=true&output=xml`;
}
if (type == calendarOptions.YAHOO) {
URL = `${applicationConfig.YAHOO_CALENDAR}&TITLE=${encodeURIComponent(
this.Appointment.Subject
)}&DESC=${encodeURIComponent(this.Appointment.Body)}&ST=${encodeURIComponent(
getDateFormat(this.Appointment.StartDate, dateFormat)
)}&DUR=${this.Appointment.Duration}&in_loc=${encodeURIComponent(
this.Appointment.Location
)}`;
}
return {
url: URL,
name: type,
icon: require(`@/assets/img/icons/${type}.svg`),
};
},
getAppointment() {
let body = this.Appointment.Body;
body = body?.replace("|", "<").replace("~", ">");
const calFile = {
Location: this.Appointment.Location,
Body: body,
IsHTML: this.Appointment.IsHTML,
UniqueId: this.Appointment.RefrrlSeqNum,
Subject: this.Appointment.Subject,
StartDate: this.Appointment.StartDate,
EndDate: this.Appointment.EndDate,
Status: calendarStatus.BUSY,
};
var calBody = getCalendarFile(calFile);
download("SafeliteAppointment.ics", calBody);
},
},
};
</script>
<style scoped>
.add-to-calendar-text {
margin: 0 0 0 7px;
color: #1574a1;
vertical-align: middle;
font-weight: 500;
font-family: "Roboto";
}
.add-to-calendar button {
border: 0;
background-color: #fff;
}
.calendar-section {
display: table;
position: relative;
margin: 0 auto;
}
.calendar-section .add-to-calendar {
padding-top: 15px;
}
.calendar-section .add-to-calendar .add-to-calendar-span {
cursor: pointer;
}
.calendar-section .add-to-calendar .add-to-calendar-img {
display: inline;
width: 20px;
}
</style>

View file

@ -1,6 +1,6 @@
import { shallowMount } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import calendarModalQuestion from "@/layouts/add-to-calendar/calendar-modal-question/calendar-modal-question.vue";
import calendarModalQuestion from "@/layouts/confirmation/add-to-calendar/calendar-modal-question/calendar-modal-question.vue";
describe("calendar-modal-question.vue", () => {
test("When selected calendar option is changed, a correctly selectedCalendarOption should be emitted", async () => {

View file

@ -115,16 +115,16 @@ describe("confirmation.vue", () => {
});
});
describe("computed properties...", () => {
// test("ScheduleDateFormatted should return date in expected format.", () => {
// //Arrange
// const { wrapper } = setupMocks({});
test("ScheduleDateFormatted should return date in expected format.", () => {
//Arrange
const { wrapper } = setupMocks({});
// // Act
// const testValue = wrapper.vm.ScheduleDateFormatted;
// Act
const testValue = wrapper.vm.ScheduleDateFormatted;
// // Assert
// expect(testValue).toEqual("Sunday, January 1");
// });
// Assert
expect(testValue).toEqual("Sunday, January 1");
});
test("ScheduleTimeFormatted should return mobile time in expected format.", () => {
//Arrange
store.getters.order.serviceLocation.appointmentType = AppointmentTypeStrings.MOBILE;

View file

@ -4,16 +4,12 @@
<funnelHeader cmsWidgetName="FunnelHeaderWidget" />
<div>
<div class="container-fluid pb-2">
<div class="row justify-content-md-center">
<div class="row justify-content-center">
<div class="col-md-6 col-xl-4">
<div class="header-container">
<img :src="ScheduleConfirmationImage" />
<span v-html="ScheduleConfirmationText"></span>
</div>
<div
class="appoinmenttext"
v-html="this.getAppointmentWordingText"></div>
<div class="main">
<div>
<vehicleBanner
@ -21,16 +17,26 @@
:displayGenericVehicleImage="false" />
</div>
<div class="scheduleText">
<p>{{ this.ScheduleDateFormatted }}</p>
<p>{{ this.ScheduleTimeFormatted }}</p>
<p>{{ ScheduleDateFormatted }}</p>
<p>{{ ScheduleTimeFormatted }}</p>
</div>
<addToCalendar></addToCalendar>
<addToCalendar
mobileWidgetName="AddToCalendar_Mobile"
inShopWidgetName="AddToCalendar_InShop"
dropOffWidgetName="AddToCalendar_DropOff"
overnightDropOffWidgetName="AddToCalendar_OvernightDropOff"
allDayDropOffWidgetName="AddToCalendar_AllDayDropOff"
sameDayDropOffWidgetName="AddToCalendar_SameDayDropOff"
:serviceLocationFullAddress="ServiceLocationFullAddress"
:providerFullAddress="ProviderFullAddress"
:appointmentType="AppointmentType"
:scheduleDate="ScheduleDate"
:scheduleStartTime="ScheduleStartTime"
:scheduleEndTime="ScheduleEndTime" />
<div
class="appoinmenttext"
v-html="this.AppointmentWordingText"></div>
<div class="appoinmenttext" v-html="AppointmentWordingText"></div>
</div>
<div class="emailtext" v-html="this.ConfirmationEmailText"></div>
<div class="emailtext" v-html="ConfirmationEmailText"></div>
<navbar
cmsWidgetName="FunnelFooterWidget"
ref="navbar"
@ -49,21 +55,18 @@
import funnelHeader from "@/fmg-components/funnel-header/funnel-header";
import vehicleBanner from "@/fmg-components/vehicle-banner/vehicle-banner";
import navbar from "@/fmg-components/nav-bar/nav-bar";
import addToCalendar from "@/layouts/add-to-calendar/add-to-calendar";
import addToCalendar from "@/layouts/confirmation/add-to-calendar/add-to-calendar";
//Supporting files
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { AppointmentTypeStrings } from "@/constants/schedule-constants";
import { settleAllPromises } from "@/helpers/layout-helper";
import { applicationConfig } from "@/constants/application-config.js";
import { convertDateStringToDate } from "@/layouts/schedule/helpers/schedule-helper";
import { Form } from "vee-validate";
import store from "@/store";
import {
getFullDayName,
getFullMonthName,
get12HourTimeFormat,
get12HourTimeMobileFormat,
} from "@/helpers/date-helper";
import { get12HourTimeFormat, get12HourTimeMobileFormat } from "@/helpers/date-helper";
export default {
name: "confirmation",
async beforeRouteEnter(to, from, next) {
@ -93,9 +96,9 @@ export default {
},
ConfirmationEmailText() {
return this.getCmsContent("ConfirmationEmailWidget", "BodyText")
.replaceAll("{custom:MY_ACCOUNT_URL}", applicationConfig.MY_ACCOUNT)
.replaceAll("&lt;", "<")
.replaceAll("&gt;", ">");
?.replaceAll("{custom:MY_ACCOUNT_URL}", applicationConfig.MY_ACCOUNT)
?.replaceAll("&lt;", "<")
?.replaceAll("&gt;", ">");
},
ScheduleDate() {
return store.getters.order.schedule.date;
@ -147,17 +150,17 @@ export default {
},
AppointmentWordingText() {
if (this.AppointmentType == AppointmentTypeStrings.MOBILE) {
return this.MobileWordingText.replaceAll(
return this.MobileWordingText?.replaceAll(
"{custom:ADDRESS}",
this.ServiceLocationFullAddress
);
} else if (this.AppointmentType == AppointmentTypeStrings.DROP_OFF) {
return this.DropOffWordingText.replaceAll(
return this.DropOffWordingText?.replaceAll(
"{custom:ADDRESS}",
this.ProviderFullAddress
);
} else {
return this.InShopWordingText.replaceAll(
return this.InShopWordingText?.replaceAll(
"{custom:ADDRESS}",
this.ProviderFullAddress
);
@ -174,10 +177,14 @@ export default {
return `${this.ProviderAddress},<br/> ${this.ProviderCity}, ${this.ProviderState} ${this.ProviderZipCode}`;
},
ScheduleDateFormatted() {
const scheduleDate = new Date(this.ScheduleDate);
return `${getFullDayName(scheduleDate)}, ${getFullMonthName(
scheduleDate
)} ${scheduleDate.getDate()}`;
// This conversion ensures we don't get get GMT induced date changes
const dateObject = convertDateStringToDate(this.ScheduleDate);
// Ex: Tuesday, April 22
return dateObject.toLocaleDateString("en-us", {
weekday: "long",
month: "long",
day: "numeric",
});
},
ScheduleTimeFormatted() {
if (this.AppointmentType == AppointmentTypeStrings.MOBILE) {
@ -191,11 +198,6 @@ export default {
}
},
},
data() {
return {
workOrderNumber: store.getters.order.workOrderNumber,
};
},
methods: {
arePagePrerequisitesValid() {
// Service Location

View file

@ -6,9 +6,9 @@
<funnelHeader cmsWidgetName="FunnelHeaderWidget" />
</div>
</div>
<div class="row justify-content-md-center">
<div class="row justify-content-center">
<div class="col-md-6 col-xl-4">
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" class="my-5" />
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" class="mt-4 mb-5" />
<textboxQuestion
class="mb-4"
cmsWidgetName="FirstNameWidget"

View file

@ -7,7 +7,7 @@
<funnelHeader cmsWidgetName="FunnelHeaderWidget" />
</div>
</div>
<div class="row justify-content-md-center">
<div class="row justify-content-center">
<div class="col-md-6 col-xl-4">
<vehicleBanner cmsWidgetName="VehicleBannerWidget" />
<funnelSubHeader class="pb-4" cmsWidgetName="FunnelSubHeaderWidget" />
@ -33,12 +33,13 @@
<div v-else>
<alert
class="my-4"
class="mt-4 mb-4"
:cmsWidgetName="alertInfo"
alertClass="alert-info"
v-bind:isDismissible="false" />
<textboxQuestion
class="mb-4"
cmsWidgetName="ServiceZipQuestionWidget"
v-model="serviceZipCode"
inputId="serviceZipCode"
@ -47,6 +48,7 @@
validationRules="zip-required|zip-format" />
<textboxQuestion
class="mb-0"
cmsWidgetName="EmailAddressQuestionWidget"
v-model="emailAddress"
inputId="emailAddress"

View file

@ -6,7 +6,7 @@
<funnelHeader cmsWidgetName="FunnelHeaderWidget" />
</div>
</div>
<div class="row justify-content-md-center">
<div class="row justify-content-center">
<div class="col-md-6 col-xl-4">
<vehicleBanner
cmsWidgetName="VehicleBannerWidget"

View file

@ -0,0 +1,164 @@
<template>
<transition name="fade" mode="out-in">
<div class="add-vaps-buttons" aria-live="polite">
<buttonQuestion
ref="buttonQuestion"
:answers="answersToDisplay"
groupName="addVaps"
v-model="answersToDisplay"
buttonTypeString="addVapsButton"
:buttonTypeObject="addVapsButton"
:isWide="answersToDisplay.length > 1 ? false : true"
@update:modelValue="openModal" />
<contentGroupModal
ref="RainDefenseModal"
cmsWidgetName="RainDefenseModal"
v-if="rainDefenseItem"
@footer-button-action="addToCart('RainDefenseModal', rainDefenseItem)"
footerButtonActionName="footer-button-action">
<p class="price">{{ rainDefenseItem.listPrice }}</p>
</contentGroupModal>
<contentGroupModal
ref="WipersModal"
cmsWidgetName="WipersModal"
v-if="wipersItem"
@footer-button-action="addToCart('WipersModal', wipersItem)"
footerButtonActionName="footer-button-action">
<p class="price">{{ wipersItem.listPrice }}</p>
</contentGroupModal>
</div>
</transition>
</template>
<script>
import buttonQuestion from "@/digital-components/button-question/button-question";
import addVapsButton from "./add-vaps-button/add-vaps-button";
import contentGroupModal from "@/fmg-components/content-group-modal/content-group-modal";
export default {
name: "add-vaps-modal-buttons",
props: {
vapsTilesCmsName: String,
availableLineItems: Object,
modelValue: Object,
},
data() {
return {
addVapsButton: addVapsButton,
};
},
methods: {
openModal(modalName) {
this.$refs[modalName].openModal();
},
addToCart(modalName, part) {
const vapsItems = this.currentCartItems.vaps;
const alreadyInVaps = vapsItems.some((item) => {
return item.partType === part.partType;
});
if (!alreadyInVaps) {
if (part.subItems) {
// for wipers, a "combined" part (w/ subItems)
part.subItems.forEach((part) => {
this.currentCartItems.vaps.push(part);
});
} else {
// for rain defense, a "single" part (w/o subItems)
this.currentCartItems.vaps.push(part);
}
}
this.$refs[modalName].closeModal();
},
},
computed: {
currentCartItems() {
return this.modelValue;
},
showAddRainDefense() {
// show if not in cart
return !this.currentCartItems.vaps?.some((vap) => {
return vap?.partType === "RAIN DEFENSE";
});
},
showAddWipers() {
// show if not in cart
if (this.currentCartItems && this.currentCartItems.vaps) {
return !this.currentCartItems.vaps.some((vap) => {
return vap?.partType?.includes(" WIPER");
});
} else {
return false;
}
},
rainDefenseItem() {
const lineItem = this.availableLineItems.find((item) => {
return item.partType === "RAIN DEFENSE";
});
if (lineItem)
lineItem.listPrice = parseFloat(
lineItem?.laborAmount + lineItem?.sellingPrice + lineItem?.kitPrice
).toFixed(2);
return lineItem;
},
wipersItem() {
const wiperLineItems = this.availableLineItems.filter((item) => {
return item.partType.includes("WIPER");
});
let wiperLineItem;
if (wiperLineItems.length === 1) {
wiperLineItem = wiperLineItems[0];
wiperLineItem.listPrice = parseFloat(
wiperLineItem.laborAmount + wiperLineItem.sellingPrice + wiperLineItem.kitPrice
).toFixed(2);
return wiperLineItem;
} else if (wiperLineItems.length > 1) {
let combinedPrice = 0;
wiperLineItems?.forEach((item) => {
combinedPrice += item?.laborAmount + item?.sellingPrice + item?.kitPrice;
});
wiperLineItem = {
description: "COMBINED ITEM",
partType: "WIPERS",
subItems: [],
listPrice: parseFloat(combinedPrice).toFixed(2),
};
wiperLineItems?.forEach((item) => {
wiperLineItem.subItems.push(item);
});
return wiperLineItem;
}
return [];
},
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.wipersItem.listPrice;
answers.push(answer);
}
if (this.showAddRainDefense) {
const answer = getAnswer("RainDefenseModal", this.answersCmsData);
answer.SubText = "+$" + this.rainDefenseItem.listPrice;
answers.push(answer);
}
return answers;
},
},
components: {
buttonQuestion,
contentGroupModal,
},
};
</script>

View file

@ -8,6 +8,7 @@
:questionText="paymentMethodQuestionText"
:answers="paymentMethodAnswerData"
isRequired
:validationRules="validationRules"
v-model="selectedMethod"
textPosition="text-start" />
</div>
@ -27,6 +28,7 @@ export default {
},
props: {
modelValue: String,
validationRules: String,
},
methods: {
getAnswersNullSafe(widgetName) {

View file

@ -7,42 +7,49 @@
<funnelHeader cmsWidgetName="FunnelHeaderWidget" />
</div>
</div>
<div class="row justify-content-md-center">
<div class="row justify-content-center">
<div class="col-md-6 col-xl-4">
<vehicleBanner
cmsWidgetName="VehicleBannerWidget"
:displayGenericVehicleImage="false" />
<funnelSubHeader class="mb-5" cmsWidgetName="FunnelSubHeaderWidget" />
<funnelSubHeader
class="mb-5 mt-4"
cmsWidgetName="FunnelSubHeaderWidget"
leftAlignHeader />
<p v-if="isPiaDisabled" class="fw-normal no-pia-disclaimer">
<span>{{ noPiaDisclaimer }}</span>
</p>
<hr />
<cart
:damage="damageInfo"
:availableLineItems="availableLineItems"
:lineItems="lineItems"
:lineItems="taxedLineItems"
servicePackageOptionsCmsName="ServicePackageTitle"
@cart-remove="cartRemove" />
<div>
<alert
ref="paypalErrorAlert"
ref="piaErrorAlert"
class="my-4"
v-if="displayPaypalAlert"
cmsWidgetName="PaypalErrorAlertWidget"
v-if="displayPiaAlert"
cmsWidgetName="PIAErrorAlertWidget"
alertClass="alert-danger"
v-bind:isDismissible="false" />
</div>
<add-vaps-modal-buttons
:cartItems="cartItems"
v-model="lineItems"
:availableLineItems="availableLineItems"
vapsTilesCmsName="VapsProductTiles"
v-on="{ 'buttonEvent.openModal': openModal }" />
vapsTilesCmsName="VapsProductTiles" />
<button @click="openModal('RainDefenseModal')">OPEN MODAL</button>
<contentGroupModal ref="RainDefenseModal" cmsWidgetName="RainDefenseModal" />
<paymentMethodQuestion
v-if="!isPiaDisabled"
v-model="paymentMethodInternalModel" />
v-model="paymentMethodInternalModel"
validationRules="option-required" />
<navbar
cmsWidgetName="FunnelFooterWidget"
@ -65,11 +72,11 @@ import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-heade
import paymentMethodQuestion from "@/layouts/payment-method/payment-method-question/payment-method-question";
import loadingModal from "@/fmg-components/loading-modal/loading-modal.vue";
import cart from "@/fmg-components/cart/cart";
import addVapsModalButtons from "@/fmg-components/add-vaps-modal-buttons/add-vaps-modal-buttons";
import contentGroupModal from "@/fmg-components/content-group-modal/content-group-modal";
import addVapsModalButtons from "./add-vaps-modal-buttons/add-vaps-modal-buttons";
import { submitWorkOrder } from "@/helpers/heritage-integration/order-helper.js";
import alert from "@/ux-components/alert/alert";
// Supporting Items
import baseMixin from "@/mixins/base-mixin.js";
import { settleAllPromises } from "@/helpers/layout-helper";
import { storeActions } from "@/constants/store-actions";
@ -79,8 +86,13 @@ import { paymentMethods } from "@/constants/payment-method-constants";
import { experimentSettings } from "@/constants/experiments";
import { Form } from "vee-validate";
import { defineRule } from "vee-validate";
import { required } from "@/helpers/validation-rules";
import { errorMessages } from "@/constants/error-messages";
import { AppointmentTypeStrings } from "@/constants/schedule-constants";
defineRule("option-required", required(errorMessages.OPTION_REQUIRED));
export default {
name: "paymentMethod",
async beforeRouteEnter(to, from, next) {
@ -97,7 +109,6 @@ export default {
null,
"payment-method"
);
console.log("rainDefensePromise: ", rainDefensePromise);
const supportingItemsPromise = baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.GET_SUPPORTING_ITEMS,
@ -126,7 +137,7 @@ export default {
const resultMap = await settleAllPromises(promiseResultMap);
const glassParts = (await store.getters.order.lineItems.glassParts) ?? [];
const glassParts = store.getters.order.lineItems.glassParts ?? [];
const availableLineItems = [
resultMap.rainDefense,
@ -143,16 +154,35 @@ export default {
"payment-method",
false
);
const lineItems = store.getters.lineItems;
const cartItems = [...glassParts, ...lineItems.supportingItems, ...lineItems.vaps]; // TOBEREMOVED BY AJC IN CSR-1384
const lineItems = store.getters.order.lineItems;
const combinedLineItems = [];
combinedLineItems.push(lineItems.glassParts ?? []);
combinedLineItems.push(lineItems.supportingItems ?? []);
combinedLineItems.push(lineItems.vaps ?? []);
const taxedLineItems = await baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.TAX_ORDER_ITEMS_AND_SAVE_SERVER_DATA,
{
billToAccountNumber: "87291",
providerNumber: store.getters.order.serviceLocation.provider.providerNumber,
appointmentType: store.getters.order.serviceLocation.appointmentType,
serviceLocationState: store.getters.order.serviceLocation.state,
serviceLocationZipCode: store.getters.order.serviceLocation.zipCode,
pricedLineItems: combinedLineItems,
},
"payment-method",
false
);
// Call the "next" function to complete the transition to this page.
next((vm) => {
vm.cartItems = cartItems; // TOBEREMOVED BY AJC IN CSR-1384
vm.cartItems = combinedLineItems; // TOBEREMOVED BY AJC IN CSR-1384
vm.setCmsContent(resultMap.cmsContent);
vm.availableLineItems = pricedAvailableLineItems;
vm.taxedLineItems = taxedLineItems;
vm.lineItems = lineItems;
});
},
@ -161,9 +191,10 @@ export default {
cartItems: [], // TOBEREMOVED BY AJC IN CSR-1384
lineItems: [],
taxedLineItems: [], // temporary, for use by cart.vue
availableLineItems: [],
displayPaypalAlert: this.getPaypalAlert(),
paymentMethodInternalModel: null,
paymentMethodInternalModel: this.getPaymentMethodFromStore(),
displayPiaAlert: this.getPiaAlert(),
};
},
methods: {
@ -219,19 +250,22 @@ export default {
packageReqs && serviceLocationReqs && isInsuranceSet && scheduleReqs && customerReqs
);
},
openModal(modalName) {
this.$refs[modalName].openModal();
},
getPaypalAlert() {
getPiaAlert() {
return store.getters.order.payment.piaErrorCode ? true : false;
},
vapsItemsSelectedAction(vapsItemsSelected) {
this.cartItems = vapsItemsSelected;
getPaymentMethodFromStore() {
const isPia = store.getters.order.payment.isPia;
if (isPia === null || isPia === undefined) {
return null;
}
if (isPia) {
return store.getters.order.payment.piaType;
} else {
return paymentMethods.LATER;
}
},
// TODO (as part of CSR-1384) enable adding to the cart
// vapsItemsSelectedAction(vapsItemsSelected) {
// this.cartItems = vapsItemsSelected;
// },
cartRemove(item) {
const matchedIndices = [];
this.cartItems.forEach((cartItem, i) => {
@ -249,15 +283,25 @@ export default {
this.$router.navigateWithoutSaving(this.navigationScenarios.CLICKED_BACK, this.$route);
},
async forwardButtonAction() {
await this.dispatchStoreAction(
storeActions.SAVE_PAYMENT_METHOD_CHOICE,
this.paymentMethod,
false
);
await this.dispatchStoreAction(storeActions.SAVE_VAPS, this.lineItems.vaps, false);
await this.dispatchStoreAction(storeActions.SAVE_PROMOS, this.lineItems.promos, false);
switch (this.paymentMethod) {
case paymentMethods.CREDIT_CARD:
this.piaSetup("cc");
this.piaSetup(this.paymentMethod);
break;
case paymentMethods.PAYPAL:
this.piaSetup("pp");
this.piaSetup(this.paymentMethod);
break;
case paymentMethods.AFTERPAY:
this.piaSetup("ap");
this.piaSetup(this.paymentMethod);
break;
default:
this.dispatchStoreAction(storeActions.SAVE_PIA_ERROR_CODE, null, false);
@ -269,21 +313,11 @@ export default {
);
}
},
async ccdPayment() {
this.piaSetup("cc");
},
async paypal() {
this.piaSetup("pp");
},
async afterpay() {
this.piaSetup("ap");
},
async piaSetup(payNowType) {
this.$refs.loadingModal.showModal();
// since we're leaving the site for pia, clear any save session promises that we will not be able to resolve when we return
await this.dispatchStoreAction(storeActions.RESET_SAVE_SESSION_PROMISE);
await this.dispatchStoreAction(storeActions.SAVE_PAY_NOW_TYPE, payNowType, false);
// make sure pia error codes are reset
this.dispatchStoreAction(storeActions.SAVE_PIA_ERROR_CODE, null, false);
@ -303,9 +337,11 @@ export default {
},
computed: {
damageInfo() {
// console.log("this.$store.getters.damage ", this.$store.getters.damage)
return this.$store.getters.damage;
},
noPiaDisclaimer() {
return this.getCmsContent("NoPiaDisclaimerWidget", "Text");
},
customCtaCopy() {
switch (this.paymentMethod) {
case paymentMethods.CREDIT_CARD:
@ -349,8 +385,14 @@ export default {
cart,
alert,
addVapsModalButtons,
contentGroupModal,
paymentMethodQuestion,
},
};
</script>
<style lang="scss" scoped>
.no-pia-disclaimer {
color: $gray-550;
margin: 0;
}
</style>

View file

@ -9,19 +9,20 @@ import { submitWorkOrder } from "@/helpers/heritage-integration/order-helper.js"
import { queryStrings } from "@/constants/query-strings";
import { getQuerystringParameter } from "@/helpers/querystring-helper";
import { storeActions } from "@/constants/store-actions";
import store from "@/store";
import baseMixin from "@/mixins/base-mixin.js";
import loadingModal from "@/fmg-components/loading-modal/loading-modal.vue";
import { Form } from "vee-validate";
import { paymentMethods } from "@/constants/payment-method-constants";
export default {
name: "payment-pia-return",
async mounted() {
this.$refs.loadingModal.showModal();
const piaError = getQuerystringParameter(queryStrings.ERROR);
const token = getQuerystringParameter(queryStrings.TOKEN);
const payerId = getQuerystringParameter(queryStrings.PAYERID);
if (piaError) {
console.log("Error during payment: " + piaError);
baseMixin.methods.dispatchStoreAction(
storeActions.SAVE_PIA_ERROR_CODE,
piaError,
@ -29,33 +30,26 @@ export default {
);
this.$router.navigateWithoutSaving(this.navigationScenarios.PIA_ERROR, this.$route);
} else {
if (token && payerId) {
baseMixin.methods.dispatchStoreAction(
storeActions.SAVE_PIA_ERROR_CODE,
null,
false
);
baseMixin.methods.dispatchStoreAction(
storeActions.SAVE_WORK_ORDER_FLAG,
true,
false
);
// Final work order submit after returning from PIA.
await this.dispatchStoreAction(storeActions.RESET_SAVE_SESSION_PROMISE);
await submitWorkOrder({ pageNameToLog: "payment-pia-return" });
this.$refs.loadingModal.isModalVisible = false;
this.$router.navigateWithoutSaving(
this.navigationScenarios.PIA_SUCCESS,
this.$route
);
} else {
baseMixin.methods.dispatchStoreAction(
storeActions.SAVE_PIA_ERROR_CODE,
piaError,
false
);
this.$router.navigateWithoutSaving(this.navigationScenarios.PIA_ERROR, this.$route);
switch (store.getters.order.payment.piaType) {
case paymentMethods.CREDIT_CARD:
case paymentMethods.AFTERPAY:
await this.processCreditCardResponse();
break;
case paymentMethods.PAYPAL:
await this.processPaypalResponse();
break;
default:
var msg = "Unknown Pia type: " + store.getters.order.payment.piaType;
console.log(msg);
baseMixin.methods.dispatchStoreAction(
storeActions.SAVE_PIA_ERROR_CODE,
msg,
false
);
this.$router.navigateWithoutSaving(
this.navigationScenarios.PIA_ERROR,
this.$route
);
}
}
},
@ -63,6 +57,72 @@ export default {
arePagePrerequisitesValid() {
return true;
},
async processPaypalResponse() {
const token = getQuerystringParameter(queryStrings.TOKEN);
const payerId = getQuerystringParameter(queryStrings.PAYERID);
baseMixin.methods.dispatchStoreAction(storeActions.SAVE_PIA_ERROR_CODE, null, false);
baseMixin.methods.dispatchStoreAction(storeActions.SAVE_WORK_ORDER_FLAG, true, false);
baseMixin.methods.dispatchStoreAction(storeActions.SAVE_PAYPAL_TOKEN, token, false);
await this.saveAndSubmitWorkOrder();
},
async processCreditCardResponse() {
const subscriptionID = getQuerystringParameter(queryStrings.SUBSCRIPTIONID);
baseMixin.methods.dispatchStoreAction(storeActions.SAVE_PIA_ERROR_CODE, null, false);
baseMixin.methods.dispatchStoreAction(storeActions.SAVE_WORK_ORDER_FLAG, true, false);
const referralSeqNum = getQuerystringParameter(queryStrings.REFERRAL_SEQ_NUM);
if (referralSeqNum != store.getters.order.referralSequenceNumber) {
console.log(
"error: unknown ref:" +
referralSeqNum +
" " +
store.getters.order.referralSequenceNumber
);
baseMixin.methods.dispatchStoreAction(
storeActions.SAVE_PIA_ERROR_CODE,
"unknown error",
false
);
this.$router.navigateWithoutSaving(this.navigationScenarios.PIA_ERROR, this.$route);
} else {
const expMonth = getQuerystringParameter(queryStrings.CARD_EXPIRATION_MONTH);
const expYear = getQuerystringParameter(queryStrings.CARD_EXPIRATION_YEAR);
const cardType = getQuerystringParameter(queryStrings.CARD_TYPE);
const billToPostalCode = getQuerystringParameter(queryStrings.BILL_TO_POSTAL_CODE);
const billToFirstName = getQuerystringParameter(queryStrings.BILL_TO_FIRST_NAME);
const billToLastName = getQuerystringParameter(queryStrings.BILL_TO_LAST_NAME);
const referenceNumber = getQuerystringParameter(queryStrings.REFERENCE_NUMBER);
const authCode = getQuerystringParameter(queryStrings.AUTH_CODE);
const transactionId = getQuerystringParameter(queryStrings.TRANSACTION_ID);
const transReferenceNumber = getQuerystringParameter(
queryStrings.TRANS_REFERENCE_NUMBER
);
var ccToken = {
subscriptionId: subscriptionID,
expMonth: expMonth,
expYear: expYear,
cardType: cardType,
billToPostalCode: billToPostalCode,
billToFirstName: billToFirstName,
billToLastName: billToLastName,
referenceNumber: referenceNumber,
authCode: authCode,
transactionId: transactionId,
transReferenceNumber: transReferenceNumber,
};
baseMixin.methods.dispatchStoreAction(storeActions.SAVE_CCTOKEN, ccToken, false);
await this.saveAndSubmitWorkOrder();
}
},
async saveAndSubmitWorkOrder() {
// Final work order submit after returning from PIA.
await this.dispatchStoreAction(storeActions.RESET_SAVE_SESSION_PROMISE);
await submitWorkOrder({ pageNameToLog: "payment-pia-return" });
this.$refs.loadingModal.isModalVisible = false;
this.$router.navigateWithoutSaving(this.navigationScenarios.PIA_SUCCESS, this.$route);
},
forwardButtonAction() {
this.$router.navigateWithoutSaving(this.navigationScenarios.PIA_SUCCESS, this.$route);
},

View file

@ -1,31 +1,35 @@
<template>
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm" v-slot="{ meta }">
<loadingModal notFullScreen ref="loadingModal" />
<div class="page-container-grouped-styles">
<funnelHeader cmsWidgetName="FunnelHeaderWidget" v-if="!isPaypal" />
<funnelSubHeader
class="mb-5"
cmsWidgetName="FunnelSubHeaderWidget"
leftAlignHeader
v-if="!isPaypal" />
<hr class="mt-0" />
<div id="card-container">
<iframe
ref="paymentFrame"
class="hop-iframe payment-iframe"
name="card-frame"
id="card-frame"
seamless
scrolling="no">
</iframe>
<div class="container-fluid page-container-grouped-styles">
<div class="row justify-content-center">
<div class="col-md-6">
<funnelHeader cmsWidgetName="FunnelHeaderWidget" />
</div>
</div>
<div class="fade-on-route-transition sub-container make-tall">
<navbar
cmsWidgetName="FunnelFooterWidget"
:isForwardActionDisabled="!meta.valid"
:isBackButtonHidden="shouldHideBackButton"
@back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction" />
<div class="row justify-content-center">
<div class="col-md-6 col-xl-4">
<div id="card-container">
<iframe
ref="paymentFrame"
class="hop-iframe payment-iframe"
name="card-frame"
id="card-frame"
seamless
scrolling="no">
</iframe>
</div>
<div class="fade-on-route-transition sub-container make-tall">
<navbar
cmsWidgetName="FunnelFooterWidget"
:isForwardActionDisabled="!meta.valid"
:isBackButtonHidden="shouldHideBackButton"
isSubmitHidden="true"
@back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction" />
</div>
</div>
</div>
</div>
</Form>
@ -51,27 +55,12 @@
<input type="hidden" name="sgNoKeystrokeProcessing" value="false" />
<input type="hidden" name="maskCharacter" value="*" />
<input
type="hidden"
name="styleSheetCode"
value="https://fixmyglasstest.safelite.com/FixMyGlass/css/experiments/site-2020Styling.css" />
<input
type="hidden"
name="styleSheetCode2"
value="https://fixmyglasstest.safelite.com/FixMyGlass/css/experiments/SLHop-2020Styling.css" />
<input type="hidden" name="styleSheetCode" :value="dynamicCSSUrl" />
<input type="hidden" name="styleSheetCode2" :value="dynamicHopCSSUrl" />
<input
type="hidden"
name="sgReceiptResponseURL"
value="https://fixmyglasstest.safelite.com/FixMyGlass/Payment/Success.aspx" />
<input
type="hidden"
name="sgDeclineResponseURL"
value="https://fixmyglasstest.safelite.com/FixMyGlass/Payment/Cancel.aspx" />
<input
type="hidden"
name="sgErrorResponseURL"
value="https://fixmyglasstest.safelite.com/FixMyGlass/Payment/Error.aspx" />
<input type="hidden" name="sgReceiptResponseURL" :value="piaResponseUrl" />
<input type="hidden" name="sgDeclineResponseURL" :value="piaResponseUrl" />
<input type="hidden" name="sgErrorResponseURL" :value="piaResponseUrl" />
<input type="hidden" name="sgheaderline1" :value="sgHeaderline1" />
<input type="hidden" name="sgHeader1subtitle" value="" />
@ -139,20 +128,14 @@
<input type="hidden" name="sgWorkOrder" :value="workOrderNumber" />
<input type="hidden" name="sgEmailAddress" :value="emailAddress" />
<input type="hidden" name="paypalInvoiceNumber" :value="invoiceNumber" />
<input type="hidden" name="payPalSuccessUrl" :value="payPalSuccessUrl" />
<input type="hidden" name="paypalCancelUrl" :value="payPalCancelUrl" />
<input type="hidden" name="paypalSuccessUrl" :value="piaResponseUrl" />
<input type="hidden" name="paypalCancelUrl" :value="piaCancelUrl" />
<input
type="hidden"
name="sgCCDeclineURL"
value="https://fixmyglasstest.safelite.com/FixMyGlass/Payment.aspx?error=ccDecline" />
<input
type="hidden"
name="sgCCTimeoutURL"
value="https://fixmyglasstest.safelite.com/FixMyGlass/Payment.aspx?error=ccTimeout" />
<input type="hidden" name="sgCCDeclineURL" :value="piaCancelUrl" />
<input type="hidden" name="sgCCTimeoutURL" :value="piaCancelUrl" />
<input type="hidden" name="sgTransactionType" value="authorization" />
<input type="hidden" name="amount" :value="amountDue" />
<input type="hidden" name="amount" :value="totalAmount" />
<input type="hidden" name="ctu" :value="ctu" />
<input type="hidden" name="orderNumber" :value="workOrderNumber" />
@ -177,7 +160,6 @@
// Components
import funnelHeader from "@/fmg-components/funnel-header/funnel-header";
import navbar from "@/fmg-components/nav-bar/nav-bar";
import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header";
import loadingModal from "@/fmg-components/loading-modal/loading-modal.vue";
import { storeActions } from "@/constants/store-actions";
@ -186,6 +168,7 @@ import { externalUrls } from "@/router/router-constants/externalUrl-values";
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { settleAllPromises } from "@/helpers/layout-helper";
import { applicationConfig } from "@/constants/application-config";
import { paymentMethods } from "@/constants/payment-method-constants";
import baseMixin from "@/mixins/base-mixin.js";
// Validation
@ -196,8 +179,8 @@ export default {
data() {
return {
checkoutUrl: externalUrls.SAFELITE_HOP,
payPalSuccessUrl: applicationConfig.PAYPAL_SUCCESS_URL,
payPalCancelUrl: applicationConfig.PAYPAL_CANCEL_URL,
piaResponseUrl: applicationConfig.PIA_RESPONSE_URL,
piaCancelUrl: applicationConfig.PIA_CANCEL_URL,
paymentType: this.getPaymentType(),
authToken: "",
authSignature: "",
@ -244,6 +227,16 @@ export default {
vm.fetchSignatureInfo();
});
},
computed: {
dynamicCSSUrl() {
const { protocol, hostname, port } = window.location;
return `${protocol}//${hostname}:${port}/fmg/css/site-2020Styling.css`;
},
dynamicHopCSSUrl() {
const { protocol, hostname, port } = window.location;
return `${protocol}//${hostname}:${port}/fmg/css/hop-styling.css`;
},
},
methods: {
arePagePrerequisitesValid() {
// TODO
@ -257,7 +250,7 @@ export default {
// no work order, set pia error and return
this.dispatchStoreAction(storeActions.SAVE_PIA_ERROR_CODE, "NO WORK ORDER", false);
this.$router.navigateWithoutSaving(this.navigationScenarios.CLICKED_BACK, this.$route);
this.backButtonAction();
},
getInvoiceNumber() {
if (store.getters.order.workOrderNumber) {
@ -266,7 +259,7 @@ export default {
// no work order, set pia error and return
this.dispatchStoreAction(storeActions.SAVE_PIA_ERROR_CODE, "NO WORK ORDER", false);
this.$router.navigateWithoutSaving(this.navigationScenarios.CLICKED_BACK, this.$route);
this.backButtonAction();
},
getAddress1() {
return store.getters.order.serviceLocation.address
@ -294,7 +287,16 @@ export default {
: store.getters.order.serviceLocation.provider.address.zipCode;
},
getPaymentType() {
return store.getters.order.payment.payNowType;
switch (store.getters.order.payment.piaType) {
case paymentMethods.AFTERPAY:
return "ap";
case paymentMethods.CREDIT_CARD:
return "cc";
case paymentMethods.PAYPAL:
return "pp";
default:
return store.getters.payment.piaType;
}
},
getHeaderLine1() {
if (!this.isPaypal()) {
@ -324,10 +326,12 @@ export default {
return itemsForPia;
},
getAmountDue() {
return baseMixin.methods.getAmountDue(store.getters.order.lineItems);
return 350.0;
//return baseMixin.methods.getAmountDue(store.getters.order.lineItems);
},
getDisplayAmountDue() {
return baseMixin.methods.getDisplayAmountDue(store.getters.order.lineItems);
return "$350.00";
//return baseMixin.methods.getDisplayAmountDue(store.getters.order.lineItems);
},
isPaypal() {
if (this.getPaymentType() == "pp") {
@ -367,7 +371,6 @@ export default {
components: {
funnelHeader,
navbar,
funnelSubHeader,
Form,
loadingModal,
},
@ -375,13 +378,9 @@ export default {
</script>
<style lang="scss">
.payment-iframe {
height: 100%;
}
.hop-iframe {
position: relative;
min-height: 1300px;
min-height: 1350px;
width: 100%;
border: 0 none;
}

View file

@ -7,7 +7,7 @@
<funnelHeader cmsWidgetName="FunnelHeaderWidget" />
</div>
</div>
<div class="row justify-content-md-center">
<div class="row justify-content-center">
<div class="col-md-6 col-xl-4">
<vehicleBanner
cmsWidgetName="VehicleBannerWidget"

View file

@ -30,7 +30,7 @@
isPrimary
:buttonText="forwardButtonText"
loaderColor="white"
class="mb-2"
class="mb-2 w-100"
@click-event="forwardButtonAction" />
<div>

View file

@ -3,7 +3,7 @@
v-for="alert in prefixedAlertReasons"
:key="alert.cmsWidgetName"
:ref="alert.cmsWidgetName"
class="mt-2 mb-3"
class="mt-5 mb-4"
:cmsWidgetName="alert.cmsWidgetName"
alertClass="alert-warning" />
</template>

View file

@ -7,9 +7,9 @@
<funnelHeader cmsWidgetName="FunnelHeaderWidget" />
</div>
</div>
<div class="row justify-content-md-center">
<div class="row justify-content-center">
<div class="col-md-6 col-xl-4">
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" class="mt-5" />
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" class="mt-4" />
<template v-if="ChangeShopLink.length">
<textBlock
cmsWidgetName="ChangeShopLink"
@ -439,16 +439,16 @@ export default {
this.appointmentType === AppointmentTypeStrings.MOBILE &&
this.selectedTimeSlotInfo?.isPremiumAppointment
) {
const earlyBirdIndex = supportingItems.findIndex(
const premiumFeeIndex = supportingItems.findIndex(
(item) => item.partType == PREMIUM_FEE_PART_TYPE
);
if (earlyBirdIndex >= 0) {
supportingItems[earlyBirdIndex].laborAmount =
if (premiumFeeIndex >= 0) {
supportingItems[premiumFeeIndex].laborAmount =
this.mobilePremiumAppointmentFee.laborAmount;
supportingItems[earlyBirdIndex].selingPrice =
this.mobilePremiumAppointmentFee.selingPrice;
supportingItems[earlyBirdIndex].kitPrice =
supportingItems[premiumFeeIndex].sellingPrice =
this.mobilePremiumAppointmentFee.sellingPrice;
supportingItems[premiumFeeIndex].kitPrice =
this.mobilePremiumAppointmentFee.kitPrice;
} else {
supportingItems.push(this.mobilePremiumAppointmentFee);
@ -461,12 +461,12 @@ export default {
);
} else {
// if it's not a mobile and/or premium early bird, then make sure we remove any that may have been added
const removeEarlyBirdIndex = supportingItems.findIndex(
const removePremiumFeeIndex = supportingItems.findIndex(
(item) => item.partType == PREMIUM_FEE_PART_TYPE
);
if (removeEarlyBirdIndex >= 0) {
supportingItems.splice(removeEarlyBirdIndex, 1);
if (removePremiumFeeIndex >= 0) {
supportingItems.splice(removePremiumFeeIndex, 1);
this.dispatchStoreAction(
this.storeActions.SAVE_SUPPORTING_ITEMS_SUPPRESSING_STATE_RESETTING,
supportingItems,

View file

@ -46,6 +46,18 @@ export async function getServiceabilityDetails(serviceZipCode, lineItems, pageNa
return Promise.resolve(serviceabilityDetails);
}
export async function getShopProviderData(serviceZipCode) {
const shopProviderData = await baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.GET_PROVIDERS,
{
serviceZipCode: serviceZipCode,
},
"service-location"
);
return Promise.resolve(shopProviderData);
}
export async function getAvailabilityRating(
startDate,
endDate,

View file

@ -145,9 +145,6 @@ export default {
alertInvalidZipWidgetName: String,
customComponentId: String,
validationRules: String,
onZipUpdateCallback: {
type: Function,
},
},
computed: {
mobileLocationLinkPromptText() {
@ -256,10 +253,7 @@ export default {
this.$emit("updated-serviceability", serviceabilityDetails.data);
this.$emit("updated-contains-military-base", zipCodeData.containsMilitaryBase);
if (this.onZipUpdateCallback) {
await this.onZipUpdateCallback(serviceZipCode);
}
// Update the page level model
// update the page level model
this.$emit("update:modelValue", this.internalModel);
this.closeModal();

View file

@ -7,10 +7,6 @@ import { getMountOptions } from "@/helpers/unit-test-helper";
import store from "@/store";
import baseMixin from "@/mixins/base-mixin";
import {
getServiceabilityDetails,
Provider,
} from "@/layouts/service-location/helpers/service-location-helper/service-location-helper";
// Define Mocks
jest.mock("@/helpers/cms-content-helper", () => ({
@ -21,7 +17,7 @@ jest.mock("@/helpers/cms-content-helper", () => ({
jest.mock("./classes/provider");
const mockGetPricedMobileFeePart = (mockServiceZipCode) => {
let mockGetPricedMobileFeePart = (mockServiceZipCode) => {
let mobileFeePart = {};
if (mockServiceZipCode === "43235") {
@ -38,7 +34,7 @@ const mockGetPricedMobileFeePart = (mockServiceZipCode) => {
return Promise.resolve(mobileFeePart);
};
const mockGetServiceabilityDetails = (mockServiceZipCode) => {
let mockGetServiceabilityDetails = (mockServiceZipCode) => {
const serviceabilityDetails = {
isGlassServiceableInshop: true,
isRecalibrationServiceableInshop: true,
@ -49,6 +45,17 @@ const mockGetServiceabilityDetails = (mockServiceZipCode) => {
return Promise.resolve(serviceabilityDetails);
};
let mockGetShopProviderData = (mockServiceZipCode) => {
const result = {
data: {
mobileProviderNumber: "001820",
shopProviders: [{}],
},
};
return Promise.resolve(result);
};
jest.mock(
"@/layouts/service-location/helpers/service-location-helper/service-location-helper",
() => ({
@ -58,6 +65,9 @@ jest.mock(
getServiceabilityDetails: jest.fn((mockServiceZipCode) => {
return mockGetServiceabilityDetails(mockServiceZipCode);
}),
getShopProviderData: jest.fn((mockServiceZipCode) => {
return mockGetShopProviderData(mockServiceZipCode);
}),
})
);
@ -401,6 +411,9 @@ describe("service-location.vue", () => {
const { wrapper } = setupMocks({});
await wrapper.setData({
shopProviderData: {
mobileProviderNumber: "001820",
},
selectedAppointmentType: "Mobile",
providerData: { mobileProviderNumber: "01820" },
});
@ -453,6 +466,9 @@ describe("service-location.vue", () => {
const { wrapper } = setupMocks({});
await wrapper.setData({
shopProviderData: {
mobileProviderNumber: "001820",
},
selectedAppointmentType: "Mobile",
providerData: { mobileProviderNumber: "01820" },
});
@ -517,6 +533,9 @@ describe("service-location.vue", () => {
const { wrapper } = setupMocks({});
await wrapper.setData({
shopProviderData: {
mobileProviderNumber: "001820",
},
selectedAppointmentType: "Mobile",
providerData: { mobileProviderNumber: "01820" },
});
@ -554,14 +573,13 @@ describe("service-location.vue", () => {
describe("should be logical AND when recalibration is defined.", () => {
test("T & T => T", async () => {
// Arrange
getServiceabilityDetails.mockImplementation(() =>
mockGetServiceabilityDetails = () =>
Promise.resolve({
isGlassServiceableInshop: true,
isRecalibrationServiceableInshop: true,
isGlassServiceableMobile: true,
isRecalibrationServiceableMobile: true,
})
);
});
const { wrapper } = setupMocks({});
@ -582,14 +600,13 @@ describe("service-location.vue", () => {
test("T & F => F", async () => {
// Arrange
getServiceabilityDetails.mockImplementation(() =>
mockGetServiceabilityDetails = () =>
Promise.resolve({
isGlassServiceableInshop: true,
isRecalibrationServiceableInshop: false,
isGlassServiceableMobile: true,
isRecalibrationServiceableMobile: false,
})
);
});
const { wrapper } = setupMocks({});
@ -610,14 +627,13 @@ describe("service-location.vue", () => {
test("F & T => F", async () => {
// Arrange
getServiceabilityDetails.mockImplementation(() =>
mockGetServiceabilityDetails = () =>
Promise.resolve({
isGlassServiceableInshop: false,
isRecalibrationServiceableInshop: true,
isGlassServiceableMobile: false,
isRecalibrationServiceableMobile: true,
})
);
});
const { wrapper } = setupMocks({});
@ -638,14 +654,13 @@ describe("service-location.vue", () => {
test("F & F => F", async () => {
// Arrange
getServiceabilityDetails.mockImplementation(() =>
mockGetServiceabilityDetails = () =>
Promise.resolve({
isGlassServiceableInshop: false,
isRecalibrationServiceableInshop: false,
isGlassServiceableMobile: false,
isRecalibrationServiceableMobile: false,
})
);
});
const { wrapper } = setupMocks({});
@ -666,14 +681,13 @@ describe("service-location.vue", () => {
test("displayServiceableMobileOnly should be true if mobile is true and inshop is false.", async () => {
// Arrange
getServiceabilityDetails.mockImplementation(() =>
mockGetServiceabilityDetails = () =>
Promise.resolve({
isGlassServiceableInshop: false,
isRecalibrationServiceableInshop: false,
isGlassServiceableMobile: true,
isRecalibrationServiceableMobile: true,
})
);
});
const { wrapper } = setupMocks({});
@ -695,14 +709,13 @@ describe("service-location.vue", () => {
test("displayServiceableMobileOnly should be false if mobile is false.", async () => {
// Arrange
getServiceabilityDetails.mockImplementation(() =>
mockGetServiceabilityDetails = () =>
Promise.resolve({
isGlassServiceableInshop: false,
isRecalibrationServiceableInshop: false,
isGlassServiceableMobile: false,
isRecalibrationServiceableMobile: false,
})
);
});
const { wrapper } = setupMocks({});
@ -724,14 +737,13 @@ describe("service-location.vue", () => {
test("displayServiceableMobileOnly should be false if inShop is true", async () => {
// Arrange
getServiceabilityDetails.mockImplementation(() =>
mockGetServiceabilityDetails = () =>
Promise.resolve({
isGlassServiceableInshop: true,
isRecalibrationServiceableInshop: true,
isGlassServiceableMobile: true,
isRecalibrationServiceableMobile: true,
})
);
});
const { wrapper } = setupMocks({});
@ -753,14 +765,13 @@ describe("service-location.vue", () => {
test("displayNoShopsAlert should be true if inShop is false and mobile is false", async () => {
// Arrange
getServiceabilityDetails.mockImplementation(() =>
mockGetServiceabilityDetails = () =>
Promise.resolve({
isGlassServiceableInshop: false,
isRecalibrationServiceableInshop: false,
isGlassServiceableMobile: false,
isRecalibrationServiceableMobile: false,
})
);
});
const { wrapper } = setupMocks({});
@ -782,14 +793,13 @@ describe("service-location.vue", () => {
test("displayNoShopsAlert should be false if inShop is true and mobile is true", async () => {
// Arrange
getServiceabilityDetails.mockImplementation(() =>
mockGetServiceabilityDetails = () =>
Promise.resolve({
isGlassServiceableInshop: true,
isRecalibrationServiceableInshop: true,
isGlassServiceableMobile: true,
isRecalibrationServiceableMobile: true,
})
);
});
const { wrapper } = setupMocks({});
@ -811,14 +821,13 @@ describe("service-location.vue", () => {
test("displayNoShopsAlert should be false if inShop is true", async () => {
// Arrange
getServiceabilityDetails.mockImplementation(() =>
mockGetServiceabilityDetails = () =>
Promise.resolve({
isGlassServiceableInshop: true,
isRecalibrationServiceableInshop: true,
isGlassServiceableMobile: false,
isRecalibrationServiceableMobile: false,
})
);
});
const { wrapper } = setupMocks({});
@ -840,14 +849,13 @@ describe("service-location.vue", () => {
test("displayNoShopsAlert should be false if mobile is true", async () => {
// Arrange
getServiceabilityDetails.mockImplementation(() =>
mockGetServiceabilityDetails = () =>
Promise.resolve({
isGlassServiceableInshop: false,
isRecalibrationServiceableInshop: false,
isGlassServiceableMobile: true,
isRecalibrationServiceableMobile: true,
})
);
});
const { wrapper } = setupMocks({});
@ -869,14 +877,13 @@ describe("service-location.vue", () => {
test("displayServiceableInshopOnly should be true if inshop is true and mobile is false", async () => {
// Arrange
getServiceabilityDetails.mockImplementation(() =>
mockGetServiceabilityDetails = () =>
Promise.resolve({
isGlassServiceableInshop: true,
isRecalibrationServiceableInshop: true,
isGlassServiceableMobile: false,
isRecalibrationServiceableMobile: false,
})
);
});
const { wrapper } = setupMocks({});
@ -898,14 +905,13 @@ describe("service-location.vue", () => {
test("displayServiceableInshopOnly should be false if inshop is false", async () => {
// Arrange
getServiceabilityDetails.mockImplementation(() =>
mockGetServiceabilityDetails = () =>
Promise.resolve({
isGlassServiceableInshop: false,
isRecalibrationServiceableInshop: false,
isGlassServiceableMobile: false,
isRecalibrationServiceableMobile: false,
})
);
});
const { wrapper } = setupMocks({});
@ -927,14 +933,13 @@ describe("service-location.vue", () => {
test("displayServiceableInshopOnly should be false if mobile is true", async () => {
// Arrange
getServiceabilityDetails.mockImplementation(() =>
mockGetServiceabilityDetails = () =>
Promise.resolve({
isGlassServiceableInshop: true,
isRecalibrationServiceableInshop: true,
isGlassServiceableMobile: true,
isRecalibrationServiceableMobile: true,
})
);
});
const { wrapper } = setupMocks({});
@ -956,14 +961,13 @@ describe("service-location.vue", () => {
test("displayServiceableInshopOnly should be false in the dual/static recalibration scenario", async () => {
// Arrange
getServiceabilityDetails.mockImplementation(() =>
mockGetServiceabilityDetails = () =>
Promise.resolve({
isGlassServiceableInshop: true,
isRecalibrationServiceableInshop: true,
isGlassServiceableMobile: true,
isRecalibrationServiceableMobile: false,
})
);
});
const { wrapper } = setupMocks({});
@ -988,14 +992,13 @@ describe("service-location.vue", () => {
describe("should be based only on glass serviceability if recalibration is not defined.", () => {
test("Should return true", async () => {
// Arrange
getServiceabilityDetails.mockImplementation(() =>
mockGetServiceabilityDetails = () =>
Promise.resolve({
isGlassServiceableInshop: true,
isRecalibrationServiceableInshop: null,
isGlassServiceableMobile: true,
isRecalibrationServiceableMobile: null,
})
);
});
const { wrapper } = setupMocks({});
@ -1016,14 +1019,13 @@ describe("service-location.vue", () => {
test("Should return false", async () => {
// Arrange
getServiceabilityDetails.mockImplementation(() =>
mockGetServiceabilityDetails = () =>
Promise.resolve({
isGlassServiceableInshop: false,
isRecalibrationServiceableInshop: null,
isGlassServiceableMobile: false,
isRecalibrationServiceableMobile: null,
})
);
});
const { wrapper } = setupMocks({});
@ -1044,14 +1046,13 @@ describe("service-location.vue", () => {
test("displayServiceableMobileOnly should be true if mobile is true and inshop is false.", async () => {
// Arrange
getServiceabilityDetails.mockImplementation(() =>
mockGetServiceabilityDetails = () =>
Promise.resolve({
isGlassServiceableInshop: false,
isRecalibrationServiceableInshop: null,
isGlassServiceableMobile: true,
isRecalibrationServiceableMobile: null,
})
);
});
const { wrapper } = setupMocks({});
@ -1073,14 +1074,13 @@ describe("service-location.vue", () => {
test("displayServiceableMobileOnly should be false if mobile is false", async () => {
// Arrange
getServiceabilityDetails.mockImplementation(() =>
mockGetServiceabilityDetails = () =>
Promise.resolve({
isGlassServiceableInshop: false,
isRecalibrationServiceableInshop: null,
isGlassServiceableMobile: false,
isRecalibrationServiceableMobile: null,
})
);
});
const { wrapper } = setupMocks({});
@ -1102,14 +1102,13 @@ describe("service-location.vue", () => {
test("displayServiceableMobileOnly should be false if inShop is true", async () => {
// Arrange
getServiceabilityDetails.mockImplementation(() =>
mockGetServiceabilityDetails = () =>
Promise.resolve({
isGlassServiceableInshop: true,
isRecalibrationServiceableInshop: null,
isGlassServiceableMobile: true,
isRecalibrationServiceableMobile: null,
})
);
});
const { wrapper } = setupMocks({});
@ -1131,14 +1130,13 @@ describe("service-location.vue", () => {
test("requiresInshopRecalibration should not be true if recalibration info is null.", async () => {
// Arrange
getServiceabilityDetails.mockImplementation(() =>
mockGetServiceabilityDetails = () =>
Promise.resolve({
isGlassServiceableInshop: true,
isRecalibrationServiceableInshop: null,
isGlassServiceableMobile: true,
isRecalibrationServiceableMobile: null,
})
);
});
const { wrapper } = setupMocks({});
@ -1162,14 +1160,13 @@ describe("service-location.vue", () => {
describe("should properly display alerts.", () => {
test("Should show mobile-only error if only mobile is available.", async () => {
// Arrange
getServiceabilityDetails.mockImplementation(() =>
mockGetServiceabilityDetails = () =>
Promise.resolve({
isGlassServiceableInshop: false,
isRecalibrationServiceableInshop: false,
isGlassServiceableMobile: true,
isRecalibrationServiceableMobile: true,
})
);
});
const { wrapper } = setupMocks({});
@ -1191,14 +1188,13 @@ describe("service-location.vue", () => {
test("Should not show mobile-only error if not mobile-only.", async () => {
// Arrange
getServiceabilityDetails.mockImplementation(() =>
mockGetServiceabilityDetails = () =>
Promise.resolve({
isGlassServiceableInshop: true,
isRecalibrationServiceableInshop: true,
isGlassServiceableMobile: true,
isRecalibrationServiceableMobile: true,
})
);
});
const { wrapper } = setupMocks({});
@ -1220,14 +1216,13 @@ describe("service-location.vue", () => {
test("Should show no-shop error if no shops are available.", async () => {
// Arrange
getServiceabilityDetails.mockImplementation(() =>
mockGetServiceabilityDetails = () =>
Promise.resolve({
isGlassServiceableInshop: false,
isRecalibrationServiceableInshop: false,
isGlassServiceableMobile: false,
isRecalibrationServiceableMobile: false,
})
);
});
const { wrapper } = setupMocks({});
@ -1249,14 +1244,13 @@ describe("service-location.vue", () => {
test("Should not show no-shop error if shops are available.", async () => {
// Arrange
getServiceabilityDetails.mockImplementation(() =>
mockGetServiceabilityDetails = () =>
Promise.resolve({
isGlassServiceableInshop: true,
isRecalibrationServiceableInshop: true,
isGlassServiceableMobile: true,
isRecalibrationServiceableMobile: true,
})
);
});
const { wrapper } = setupMocks({});
@ -1278,14 +1272,13 @@ describe("service-location.vue", () => {
test("Should show inshop-only error if only inshop is available", async () => {
// Arrange
getServiceabilityDetails.mockImplementation(() =>
mockGetServiceabilityDetails = () =>
Promise.resolve({
isGlassServiceableInshop: true,
isRecalibrationServiceableInshop: true,
isGlassServiceableMobile: false,
isRecalibrationServiceableMobile: false,
})
);
});
const { wrapper } = setupMocks({});
@ -1307,14 +1300,13 @@ describe("service-location.vue", () => {
test("Should not show inshop-only error if not inshop-only", async () => {
// Arrange
getServiceabilityDetails.mockImplementation(() =>
mockGetServiceabilityDetails = () =>
Promise.resolve({
isGlassServiceableInshop: false,
isRecalibrationServiceableInshop: false,
isGlassServiceableMobile: false,
isRecalibrationServiceableMobile: false,
})
);
});
const { wrapper } = setupMocks({});
@ -1336,14 +1328,13 @@ describe("service-location.vue", () => {
test("Should not show inshop-only error if dual or static recalibration, but should show that error instead.", async () => {
// Arrange
getServiceabilityDetails.mockImplementation(() =>
mockGetServiceabilityDetails = () =>
Promise.resolve({
isGlassServiceableInshop: true,
isRecalibrationServiceableInshop: true,
isGlassServiceableMobile: true,
isRecalibrationServiceableMobile: false,
})
);
});
const { wrapper } = setupMocks({});
@ -1367,14 +1358,13 @@ describe("service-location.vue", () => {
test("Should not show dual/static recalibration error if not those recalibration types", async () => {
// Arrange
getServiceabilityDetails.mockImplementation(() =>
mockGetServiceabilityDetails = () =>
Promise.resolve({
isGlassServiceableInshop: true,
isRecalibrationServiceableInshop: true,
isGlassServiceableMobile: true,
isRecalibrationServiceableMobile: true,
})
);
});
const { wrapper } = setupMocks({});

View file

@ -7,7 +7,7 @@
<funnelHeader cmsWidgetName="FunnelHeaderWidget" />
</div>
</div>
<div class="row justify-content-md-center">
<div class="row justify-content-center">
<div class="col-md-6 col-xl-4">
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" class="mt-5" />
@ -19,8 +19,7 @@
@updated-serviceability="setServiceabilityDetails"
@updated-contains-military-base="setContainsMilitaryBase"
linkWidgetName="ServiceZipLinkWidget"
modalWidgetName="ServiceZipModalWidget"
:onZipUpdateCallback="reloadShopData" />
modalWidgetName="ServiceZipModalWidget" />
<alert
ref="alertMilitaryBaseZip"
@ -59,7 +58,7 @@
alertClass="alert-warning" />
</div>
</div>
<div class="row justify-content-md-center appointment-type">
<div class="row justify-content-center appointment-type">
<div class="col-md-6">
<appointmentTypeQuestion
v-model="selectedAppointmentType"
@ -73,7 +72,7 @@
validationRules="option-required" />
</div>
</div>
<div class="row justify-content-md-center">
<div class="row justify-content-center">
<div class="col-md-6 col-xl-4">
<mobileLocationModalQuestions
customComponentId="mobileLocationQuestions"
@ -86,16 +85,15 @@
validationRules="mobile-location-required"
ref="mobileLocationQuestions"
linkWidgetName="MobileLocationLinkWidget"
modalWidgetName="MobileLocationModalWidget"
:onZipUpdateCallback="reloadShopData" />
modalWidgetName="MobileLocationModalWidget" />
<shopQuestion
ref="shopQuestion"
v-show="isShopQuestionDisplayed"
v-model="selectedProvider"
:selectedAppointmentType="selectedAppointmentType"
:shopProviderData="shopProviderData"
:isDisplayed="isShopQuestionDisplayed"
@updated-shop-list="setUpdatedShopList"
cmsWidgetName="ShopQuestionWidget" />
<contentGroupModal ref="RecalModal" cmsWidgetName="RecalModal" />
@ -135,6 +133,7 @@ import { settleAllPromises } from "@/helpers/layout-helper";
import {
getPricedMobileFeePart,
getServiceabilityDetails,
getShopProviderData,
} from "@/layouts/service-location/helpers/service-location-helper/service-location-helper";
import { Provider } from "@/layouts/service-location/classes/provider";
@ -180,7 +179,7 @@ export default {
mobileFeePart: null,
zipContainsMilitaryBase: false,
zipCodeCtu: null,
providerData: null,
shopProviderData: null,
};
},
async beforeRouteEnter(to, from, next) {
@ -188,7 +187,10 @@ export default {
const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage);
const serviceZipCode = store.getters.order.serviceLocation.zipCode;
const getZipCodeData = baseMixin.methods.getZipCodeData(serviceZipCode, "service-location");
const zipCodeDataPromise = baseMixin.methods.getZipCodeData(
serviceZipCode,
"service-location"
);
const serviceabilityDetailsPromise = getServiceabilityDetails(
serviceZipCode,
@ -198,7 +200,7 @@ export default {
const mobileFeePartPromise = getPricedMobileFeePart(serviceZipCode, "service-location");
const shopQuestionInitialDataPromise = shopQuestion.methods.loadInitialData(serviceZipCode);
const shopProviderDataPromise = getShopProviderData(serviceZipCode); // shopQuestion.methods.loadInitialData(serviceZipCode);
// Settle promises and get results
const promiseResultMap = [
@ -206,6 +208,10 @@ export default {
resultKey: "cmsContent",
promise: cmsContentPromise,
},
{
resultKey: "zipCodeData",
promise: zipCodeDataPromise,
},
{
resultKey: "mobileFeePart",
promise: mobileFeePartPromise,
@ -215,12 +221,8 @@ export default {
promise: serviceabilityDetailsPromise,
},
{
resultKey: "zipCodeData",
promise: getZipCodeData,
},
{
resultKey: "shopQuestionInitialData",
promise: shopQuestionInitialDataPromise,
resultKey: "shopProviderData",
promise: shopProviderDataPromise,
},
];
@ -232,14 +234,9 @@ export default {
vm.setData(
resultMap.zipCodeData,
resultMap.serviceabilityDetails,
resultMap.mobileFeePart
resultMap.mobileFeePart,
resultMap.shopProviderData
);
// Initialize the Shop Question component
vm.$refs.shopQuestion.initializeComponent(resultMap.shopQuestionInitialData);
// Initialize the page level shop data
vm.providerData = resultMap.shopQuestionInitialData;
});
},
computed: {
@ -254,7 +251,7 @@ export default {
if (newValue.zipCode !== this.zipCode) {
this.resetMobileLocation();
this.selectedAppointmentType = null;
this.selectedProvider = null;
this.selectedProvider = new Provider();
}
this.state = newValue.state;
@ -277,20 +274,16 @@ export default {
};
},
set: function (newValue) {
this.streetAddress = newValue.addressQuestions.streetAddress;
this.apartmentNumberOrBusinessName =
newValue.addressQuestions.apartmentNumberOrBusinessName;
this.city = newValue.addressQuestions.city;
this.state = newValue.addressQuestions.state;
this.zipCode = newValue.addressQuestions.zipCode;
this.isVehicleProtected = newValue.isVehicleProtected;
if (newValue.zipCode !== this.zipCode) {
if (!this.selectedAppointmentType == "Mobile") {
this.selectedAppointmentType = null;
}
this.selectedProvider = null;
if (newValue.addressQuestions.zipCode !== this.zipCode) {
getShopProviderData(newValue.addressQuestions.zipCode).then((result) => {
this.shopProviderData = result.data;
this.selectedProvider = new Provider(
this.shopProviderData.mobileProviderNumber
);
});
}
this.setMobileLocation(newValue);
},
},
isServiceableMobile() {
@ -355,7 +348,7 @@ export default {
store.getters.payment.isInsurance !== null
);
},
setData(zipCodeData, serviceabilityDetails, mobileFeePart) {
setData(zipCodeData, serviceabilityDetails, mobileFeePart, shopProviderData) {
if (zipCodeData) {
this.zipContainsMilitaryBase = zipCodeData.containsMilitaryBase;
this.zipCodeCtu = zipCodeData.zipCodeCtu;
@ -368,6 +361,10 @@ export default {
if (mobileFeePart) {
this.mobileFeePart = mobileFeePart;
}
if (shopProviderData) {
this.shopProviderData = shopProviderData;
}
},
setContainsMilitaryBase(val) {
if (this.zipContainsMilitaryBase !== val) {
@ -416,12 +413,18 @@ export default {
this.isRecalibrationServiceableMobile =
serviceabilityDetails.isRecalibrationServiceableMobile;
},
setMobileLocation(mobileLocation) {
this.streetAddress = mobileLocation.addressQuestions.streetAddress;
this.apartmentNumberOrBusinessName =
mobileLocation.addressQuestions.apartmentNumberOrBusinessName;
this.city = mobileLocation.addressQuestions.city;
this.state = mobileLocation.addressQuestions.state;
this.zipCode = mobileLocation.addressQuestions.zipCode;
this.isVehicleProtected = mobileLocation.isVehicleProtected;
},
async reloadShopData(zipCode) {
await this.$refs.shopQuestion.reloadShopData(zipCode);
},
setUpdatedShopList(providerData) {
this.providerData = providerData;
},
openRecalibrationInformationModal() {
this.recalibrationInformationModal.openModal();
},
@ -438,7 +441,7 @@ export default {
// If it already exists, update the price with latest data
if (mobileFeeIndex >= 0) {
supportingItems[mobileFeeIndex].laborAmount = this.mobileFeePart.laborAmount;
supportingItems[mobileFeeIndex].selingPrice = this.mobileFeePart.selingPrice;
supportingItems[mobileFeeIndex].sellingPrice = this.mobileFeePart.sellingPrice;
supportingItems[mobileFeeIndex].kitPrice = this.mobileFeePart.kitPrice;
} else {
supportingItems.push(this.mobileFeePart);
@ -494,21 +497,31 @@ export default {
this.updateAndSaveSupportingItems();
this.$router.navigateWithSaving(this.navigationScenarios.CLICKED_FORWARD, this.$route);
},
openModalAction(modalName) {
this.$refs[modalName].openModal();
},
},
watch: {
zipCode: {
handler(newValue) {
getShopProviderData(this.zipCode).then(async (result) => {
this.shopProviderData = result.data;
if (this.selectedAppointmentType === "Mobile") {
this.selectedProvider = new Provider(
this.shopProviderData.mobileProviderNumber
);
} else {
this.selectedProvider = new Provider();
}
});
},
},
selectedAppointmentType: {
handler(newValue) {
if (newValue === "Mobile") {
this.selectedProvider = new Provider(this.providerData.mobileProviderNumber);
} else {
this.selectedProvider = new Provider();
}
},
},
providerData: {
handler(newValue) {
if (newValue === "Mobile") {
this.selectedProvider = new Provider(this.providerData.mobileProviderNumber);
this.selectedProvider = new Provider(
this.shopProviderData.mobileProviderNumber
);
} else {
this.selectedProvider = new Provider();
}
@ -535,7 +548,6 @@ export default {
.appointment-type-question {
.button-question {
.col {
padding: 0 0.5rem;
@include media-breakpoint-up(md) {
padding: 0 0.75rem;
}

View file

@ -68,9 +68,6 @@ export default {
},
linkWidgetName: String,
modalWidgetName: String,
onZipUpdateCallback: {
type: Function,
},
},
computed: {
serviceZipLinkText() {
@ -164,13 +161,9 @@ export default {
this.$emit("updated-serviceability", serviceabilityDetails.data);
this.$emit("updated-contains-military-base", zipCodeData.containsMilitaryBase);
// Update the page level model
// update the page level model
this.$emit("update:modelValue", this.internalModel);
if (this.onZipUpdateCallback) {
await this.onZipUpdateCallback(serviceZipCode);
}
this.closeModal();
}
} else {

View file

@ -61,6 +61,7 @@ const mockCmsContent = {
const cmsWidgetName = "ShopQuestionWidget";
const shopQuestionInitialData = {
mobileProviderNumber: "001820",
shopProviders: [
{
address: {
@ -162,15 +163,18 @@ describe("shop-question.vue", () => {
selectedAppointmentType: "Dropoff",
cmsWidgetName: cmsWidgetName,
isDisplayed: true,
shopProviderData: null,
},
mountOptions: {
attachTo: document.body,
},
});
// Act
wrapper.vm.initializeComponent(shopQuestionInitialData);
await wrapper.setProps({
shopProviderData: shopQuestionInitialData,
});
// Act
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
@ -199,47 +203,6 @@ describe("shop-question.vue", () => {
});
it("Should display the 'Show more locations' link when there are more than three locations to chose from", async () => {
// Arrange
const { wrapper } = setupMocks({
mixins: [mockMixin],
props: {
modelValue: {
address: {
city: "POWELL",
country: "US",
state: "OH",
streetAddress: "3938 POWELL RD",
zipCode: "43065",
},
distanceInMiles: 16.2690495685233,
providerNumber: "003341",
},
serviceZipCode: "43081",
selectedAppointmentType: "Dropoff",
cmsWidgetName: cmsWidgetName,
isDisplayed: true,
},
mountOptions: {
attachTo: document.body,
},
});
// Act
wrapper.vm.initializeComponent(shopQuestionInitialData);
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
const showMoreShopsLink = wrapper.findComponent({ ref: "showMoreShopsLink" });
// Assert
expect(showMoreShopsLink.exists()).toBe(true);
expect(showMoreShopsLink.isVisible()).toBe(true);
});
it("Should not display the 'Show more locations' link when there are fewer than three locations to chose from", async () => {
// Arrange
const alsoShopQuestionInitialData = {
shopProviders: [
@ -276,6 +239,17 @@ describe("shop-question.vue", () => {
distanceInMiles: 11.738869544543,
providerNumber: "003343",
},
{
address: {
city: "COLUMBUS",
country: "US",
state: "OH",
streetAddress: "1670 HARMON AVE",
zipCode: "43223",
},
distanceInMiles: 15.9727889297435,
providerNumber: "006747",
},
],
};
@ -297,6 +271,7 @@ describe("shop-question.vue", () => {
selectedAppointmentType: "Dropoff",
cmsWidgetName: cmsWidgetName,
isDisplayed: true,
shopProviderData: null,
},
mountOptions: {
attachTo: document.body,
@ -304,17 +279,94 @@ describe("shop-question.vue", () => {
});
// Act
wrapper.vm.initializeComponent(alsoShopQuestionInitialData);
await wrapper.setProps({
shopProviderData: alsoShopQuestionInitialData,
});
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
const showMoreShopsLink = wrapper.findComponent({ ref: "showMoreShopsLink" });
// Assert
expect(showMoreShopsLink.exists()).toBe(false);
expect(showMoreShopsLink.exists()).toBeTruthy();
expect(showMoreShopsLink.isVisible()).toBeTruthy();
});
it("Should display the next three shops when the 'Show more location' link is clicked", async () => {
it("Should not display the 'Show more locations' link when there are fewer than three locations to chose from", async () => {
// Arrange
const alsoShopQuestionInitialData = {
shopProviders: [
{
address: {
city: "WESTERVILLE",
country: "US",
state: "OH",
streetAddress: "4403 EXECUTIVE PKWY",
zipCode: "43081",
},
distanceInMiles: 5.16769294095201,
providerNumber: "003335",
},
{
address: {
city: "WORTHINGTON",
country: "US",
state: "OH",
streetAddress: "760 DEARBORN PARK LN",
zipCode: "43085",
},
distanceInMiles: 10.5865432478478,
providerNumber: "001820",
},
],
};
const { wrapper } = setupMocks({
mixins: [mockMixin],
props: {
modelValue: {
address: {
city: "POWELL",
country: "US",
state: "OH",
streetAddress: "3938 POWELL RD",
zipCode: "43065",
},
distanceInMiles: 16.2690495685233,
providerNumber: "003341",
},
serviceZipCode: "43081",
selectedAppointmentType: "Dropoff",
cmsWidgetName: cmsWidgetName,
isDisplayed: true,
shopProviderData: null,
},
mountOptions: {
attachTo: document.body,
},
});
// Act
await wrapper.setProps({
shopProviderData: alsoShopQuestionInitialData,
});
await wrapper.vm.$nextTick();
const showMoreShopsLink = wrapper.findComponent({ ref: "showMoreShopsLink" });
await wrapper.vm.$nextTick();
// Assert
expect(showMoreShopsLink.exists()).toBeFalsy();
});
//TODO: Add test to ensure the "Show more locations" link is not visible when all the existing shops are displayed.
it("Should display the next three shops when the 'Show more locations' link is clicked", async () => {
// Arrange
const displayedAnswers = [
{
@ -355,6 +407,7 @@ describe("shop-question.vue", () => {
selectedAppointmentType: "Dropoff",
cmsWidgetName: cmsWidgetName,
isDisplayed: true,
shopProviderData: null,
},
mountOptions: {
attachTo: document.body,
@ -362,7 +415,9 @@ describe("shop-question.vue", () => {
});
// Act
wrapper.vm.initializeComponent(shopQuestionInitialData);
await wrapper.setProps({
shopProviderData: shopQuestionInitialData,
});
await wrapper.vm.$nextTick();
@ -384,7 +439,7 @@ describe("shop-question.vue", () => {
await wrapper.vm.$nextTick();
showMoreShopsLink.trigger("click");
showMoreShopsLink.trigger("click-event");
await wrapper.vm.$nextTick();
@ -443,6 +498,7 @@ describe("shop-question.vue", () => {
serviceZipCode: "43081",
selectedAppointmentType: "Dropoff",
cmsWidgetName: cmsWidgetName,
shopProviderData: null,
},
mountOptions: {
attachTo: document.body,
@ -450,7 +506,9 @@ describe("shop-question.vue", () => {
});
// Act
wrapper.vm.initializeComponent(shopQuestionInitialData);
await wrapper.setProps({
shopProviderData: shopQuestionInitialData,
});
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
@ -468,15 +526,18 @@ describe("shop-question.vue", () => {
modelValue: null,
selectedAppointmentType: "Dropoff",
cmsWidgetName: cmsWidgetName,
shopProviderData: null,
},
mountOptions: {
attachTo: document.body,
},
});
// Act
await wrapper.vm.initializeComponent(shopQuestionInitialData);
await wrapper.setProps({
shopProviderData: shopQuestionInitialData,
});
// Act
await wrapper.vm.$nextTick();
expect(wrapper.vm.answers.length).toEqual(3);

View file

@ -30,7 +30,7 @@
linkType="text"
:text="showMoreShopsLinkText"
href="#!"
@click-event="getNextShopsFromList"
@click-event="getNextShopsFromList(3)"
:aria-label="showMoreShopsLinkText" />
</div>
</transition>
@ -43,8 +43,6 @@ import buttonQuestion from "@/digital-components/button-question/button-question
import shopListButton from "./shop-list-button/shop-list-button";
import textLink from "@/ux-components/text-link/text-link";
import { storeActions } from "@/constants/store-actions.js";
// Supporting files
import { defineRule } from "vee-validate";
import { required } from "@/helpers/validation-rules";
@ -63,7 +61,6 @@ export default {
mixins: [baseMixin],
data() {
return {
shopProviders: [],
shopListButton: shopListButton,
answers: [],
shopIndex: 0,
@ -76,6 +73,7 @@ export default {
default: () => null,
},
selectedAppointmentType: String,
shopProviderData: Object,
cmsWidgetName: String,
validationRules: String,
isDisplayed: Boolean,
@ -84,6 +82,9 @@ export default {
questionText() {
return this.getCmsContent(this.cmsWidgetName, "QuestionText");
},
shopProviders() {
return this.shopProviderData?.shopProviders ?? [];
},
selectedValue: {
get: function () {
return this.modelValue;
@ -124,22 +125,6 @@ export default {
},
},
methods: {
loadInitialData(serviceZipCode) {
return this.loadData(serviceZipCode);
},
loadData(serviceZipCode) {
return baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.GET_PROVIDERS,
{
serviceZipCode: serviceZipCode,
},
"service-location"
);
},
initializeComponent(shopQuestionInitialData) {
this.shopProviders = shopQuestionInitialData.shopProviders;
this.$emit("updated-shop-list", shopQuestionInitialData);
},
async getNextShopsFromList(numberToGet = 3) {
const shopIterator = (array, n) => {
const l = array.length;
@ -199,15 +184,6 @@ export default {
this.answers = [];
this.shopIndex = 0;
},
async reloadShopData(serviceZipCode) {
const result = await this.loadData(serviceZipCode);
this.initializeComponent(result.data);
this.resetAnswers();
await nextTick();
await this.getNextShopsFromList();
},
getSelectedProviderObject(providerNumber) {
const provider =
this.shopProviders?.find((provider) => provider.providerNumber == providerNumber) ??
@ -238,20 +214,19 @@ export default {
},
shopProviders: {
async handler(newValue) {
this.resetAnswers();
await nextTick();
const selectedShopIndex = this.getSelectedProviderIndex(
newValue,
this.selectedProviderNumber
);
if (this.selectedAppointmentType) {
const selectedShopIndex = this.getSelectedProviderIndex(
newValue,
this.selectedProviderNumber
);
if (selectedShopIndex >= 3) {
await this.getNextShopsFromList(selectedShopIndex + 1);
} else {
await this.getNextShopsFromList();
await nextTick();
}
if (selectedShopIndex >= 3) {
await this.getNextShopsFromList(selectedShopIndex + 1);
} else {
await this.getNextShopsFromList();
await nextTick();
}
},
},

View file

@ -6,7 +6,7 @@
<funnelHeader cmsWidgetName="FunnelHeaderWidget" />
</div>
</div>
<div class="row justify-content-md-center">
<div class="row justify-content-center">
<div class="col-md-6 col-xl-4">
<vehicleBanner
cmsWidgetName="VehicleBannerWidget"
@ -23,7 +23,7 @@
:isDismissible="false" />
</div>
</div>
<div class="row justify-content-md-center">
<div class="row justify-content-center">
<div class="col-md-6">
<damageLocationQuestion
ref="damageLocation"
@ -32,7 +32,7 @@
groupName="DamageLocationQuestion" />
</div>
</div>
<div class="row justify-content-md-center">
<div class="row justify-content-center">
<div class="col-md-6 col-xl-4">
<windshieldOptions
class="windshield-options"
@ -66,7 +66,7 @@
validationRules="replace-options-required" />
</div>
</div>
<div class="row justify-content-md-center">
<div class="row justify-content-center">
<div class="col-md-6 col-xl-4">
<navbar
cmsWidgetName="FunnelFooterWidget"
@ -551,9 +551,19 @@ export default {
<style lang="scss">
.replace-options-question {
.question-text {
margin: 1rem 0 0 0;
@include media-breakpoint-up(md) {
margin: 1rem 0;
}
}
.two-list-card-width {
width: 100%;
flex: 0 auto;
margin-top: 0.5rem;
&:first-child {
margin-top: 1rem;
}
}
}
</style>

View file

@ -8,8 +8,8 @@
<funnelHeader cmsWidgetName="FunnelHeaderWidget" />
</div>
</div>
<div class="row justify-content-md-center">
<div class="col-md-6 col-lg-4">
<div class="row justify-content-center">
<div class="col-md-6 col-xl-4">
<vehicleBanner
ref="vehicleBanner"
cmsWidgetName="VehicleBannerWidget"
@ -17,20 +17,18 @@
<funnelSubHeader
ref="funnelSubHeader"
cmsWidgetName="FunnelSubHeaderWidget" />
<div class="prevent-squish my-5">
<div class="row">
<div class="col pb-0">
<alert
class="rounded border-0 shadow-sm"
alertClass="alert-warning"
cmsWidgetName="AlertWidget"
:isDismissible="false" />
</div>
</div>
</div>
</div>
</div>
<div class="row justify-content-md-center">
<div class="row justify-content-center">
<div class="col-md-6 col-xl-4 my-5">
<alert
class="rounded border-0 shadow-sm"
alertClass="alert-warning"
cmsWidgetName="AlertWidget"
:isDismissible="false" />
</div>
</div>
<div class="row justify-content-center">
<div class="col-md-6 col-xl-4">
<div v-for="(item, i) in PartsOrQuestions" :key="i">
<!-- Render horizontal lines if there is multi-glass (aka if i > 0) -->

View file

@ -5,12 +5,19 @@ import vehicle from "@/layouts/vehicle/vehicle.vue";
import { shallowMount } from "@vue/test-utils";
import { nextTick } from "vue";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import baseMixin from "../../mixins/base-mixin";
// Mock our module for promises.
jest.mock("@/helpers/layout-helper.js", () => ({
settleAllPromises: jest.fn(),
}));
// Mock fetchCmsContentForPage
jest.mock("@/helpers/cms-content-helper", () => ({
fetchCmsContentForPage: jest.fn(),
}));
describe("vehicle.vue", () => {
test("arePagePrerequisitesValid should be true ", async () => {
//Arrange
@ -40,17 +47,24 @@ describe("vehicle.vue", () => {
});
function setupMocks() {
const wrapper = shallowMount(
vehicle,
getMountOptions({
router: {
navigate: jest.fn(),
navigate: jest.fn(),
navigateWithSaving: jest.fn(),
navigateWithoutSaving: jest.fn(),
},
})
);
const mountOptions = getMountOptions({
router: {
navigate: jest.fn(),
navigate: jest.fn(),
navigateWithSaving: jest.fn(),
navigateWithoutSaving: jest.fn(),
},
});
//Mock props
const mockMixin = {
methods: {
getCmsContent: jest.fn(),
},
};
mountOptions.mixins = [mockMixin];
const wrapper = shallowMount(vehicle, mountOptions);
return { wrapper };
}

View file

@ -9,6 +9,10 @@
<div class="row justify-content-center">
<div class="col-md-6 col-xl-4">
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" class="siteSubHeader" />
<div
cmsWidgetName="FindYourVehicleCopyWidget"
v-html="this.findYourVehicleCopy"
class="mt-2 mb-4 small"></div>
<vehicleQuestion
ref="vehicleYearQuestion"
v-model="selectedYear"
@ -294,6 +298,9 @@ export default {
else if (this.imageUrl == null && this.carId !== null) return false;
else return true;
},
findYourVehicleCopy() {
return this.getCmsContent("FindYourVehicleCopyWidget", "QuestionText");
},
},
methods: {
getVehicle(year, make, model, style) {
@ -430,6 +437,6 @@ export default {
}
.siteSubHeader {
margin-top: 1.5rem;
margin-bottom: 1.5rem;
margin-bottom: 0;
}
</style>

View file

@ -7,7 +7,7 @@
<funnelHeader cmsWidgetName="FunnelHeaderWidget" />
</div>
</div>
<div class="row justify-content-md-center">
<div class="row justify-content-center">
<div class="col-md-6 col-xl-4">
<vehicleBanner
cmsWidgetName="VehicleBannerWidget"
@ -16,7 +16,7 @@
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" />
<textboxQuestion
class="mb-4 mt-5"
class="mb-2 mt-5"
cmsWidgetName="VinNumberQuestionWidget"
v-model="vin"
customInputId="vin"

View file

@ -88,6 +88,7 @@ export default {
lineItem.partType != partTypeStrings.RAIN_DEFENSE
);
});
let totalPrice = this.getTotalPriceOfAllLineItemsAndChildParts(lineItemsToPrice);
return totalPrice;
},

View file

@ -22,6 +22,7 @@ import {
militaryToTwelveHourTime,
getDisplayTextForDurationLength,
} from "@/layouts/schedule/helpers/schedule-helper";
import { paymentMethods } from "@/constants/payment-method-constants";
import { getDateDifferenceInDays } from "@/helpers/date-helper";
// Export State
@ -94,9 +95,24 @@ const getDefaultState = () => {
coverageStatus: null,
},
parentAccountNumber: 0,
payNowType: "cc",
isPia: null,
piaType: null,
piaErrorCode: null,
inactivePromos: null,
paypalToken: null,
ccToken: {
subscriptionId: null,
expMonth: null,
expYear: null,
cardType: null,
billToPostalCode: null,
billToFirstName: null,
billToLastName: null,
referenceNumber: null,
authCode: null,
transactionId: null,
transReferenceNumber: null,
},
},
schedule: {
date: null,
@ -227,8 +243,11 @@ export const mutations = {
updateIsInsurance(state, isInsurance) {
state.order.payment.isInsurance = isInsurance;
},
updatePayNowType(state, payNowType) {
state.order.payment.payNowType = payNowType;
updateIsPia(state, isPia) {
state.order.payment.isPia = isPia;
},
updatePiaType(state, piaType) {
state.order.payment.piaType = piaType;
},
updatePiaErrorCode(state, piaErrorCode) {
state.order.payment.piaErrorCode = piaErrorCode;
@ -239,6 +258,22 @@ export const mutations = {
updateWorkOrderNumber(state, workOrderNumber) {
state.order.workOrderNumber = workOrderNumber;
},
updateCCToken(state, ccToken) {
state.order.payment.ccToken.subscriptionId = ccToken.subscriptionId;
state.order.payment.ccToken.expMonth = ccToken.expMonth;
state.order.payment.ccToken.expYear = ccToken.expYear;
state.order.payment.ccToken.cardType = ccToken.cardType;
state.order.payment.ccToken.billToPostalCode = ccToken.billToPostalCode;
state.order.payment.ccToken.billToFirstName = ccToken.billToFirstName;
state.order.payment.ccToken.billToLastName = ccToken.billToLastName;
state.order.payment.ccToken.referenceNumber = ccToken.referenceNumber;
state.order.payment.ccToken.authCode = ccToken.authCode;
state.order.payment.ccToken.transactionId = ccToken.transactionId;
state.order.payment.ccToken.transReferenceNumber = ccToken.transReferenceNumber;
},
updatePaypalToken(state, ppToken) {
state.order.payment.paypalToken = ppToken;
},
updateInsuranceVerifiedStatus(state, isVerified) {
state.order.payment.insuranceCoverage.isVerified = isVerified;
},
@ -418,6 +453,10 @@ export const mutations = {
state.order.serviceLocation.state = null;
state.order.serviceLocation.isVehicleProtected = null;
},
resetPaymentMethodChoice(state) {
state.order.payment.isPia = null;
state.order.payment.piaType = null;
},
// Misc Mutations
updateStateWithOrderInformation(state, sessionInformation) {
state.order.referralNumber = sessionInformation.order.referralNumber;
@ -672,7 +711,10 @@ function getTimeSlotsAdditionalEventData(
) {
var numberOfDays = null;
if (firstAvailableAppointmentDateString)
numberOfDays = getDateDifferenceInDays(new Date(), firstAvailableAppointmentDateString);
numberOfDays = getDateDifferenceInDays(
new Date().toISOString().split("T")[0],
firstAvailableAppointmentDateString
);
if (shopAppointmentType)
return `FirstAvailableAppointment:${numberOfDays},Zip:${zipCode},ShopAppointmentType:${shopAppointmentType},ProvisionalTriggers:${provisionalTriggers.join(
@ -885,6 +927,7 @@ export const actions = {
context.commit(storeMutations.RESET_SERVICE_LOCATION_PROVIDER);
context.commit(storeMutations.RESET_SCHEDULE);
context.commit(storeMutations.RESET_PAYMENT_METHOD_CHOICE);
},
resetState(context) {
@ -1550,6 +1593,7 @@ export const actions = {
endpoint: endpoints.SaveSession.url,
payload: {
submitAfterSave: context.state.submitAfterSave,
userAgent: navigator.userAgent,
applicationUser: {
crmCustomerId: applicationUser.crmCustomerId,
experiments: applicationUser.experiments,
@ -1604,6 +1648,24 @@ export const actions = {
isInsurance: order.payment.isInsurance,
parentAccountNumber: order.payment.parentAccountNumber,
inactivePromos: order.payment.inactivePromos,
isCreditCard:
order.payment.piaType == paymentMethods.CREDIT_CARD ? true : false,
isPaypal: order.payment.piaType == paymentMethods.PAYPAL ? true : false,
isAfterPay: order.payment.piaType == paymentMethods.AFTERPAY ? true : false,
paypalToken: order.payment.paypalToken,
ccToken: {
subscriptionId: order.payment.ccToken.subscriptionId,
expMonth: order.payment.ccToken.expMonth,
expYear: order.payment.ccToken.expYear,
cardType: order.payment.ccToken.cardType,
billToPostalCode: order.payment.ccToken.billToPostalCode,
billToFirstName: order.payment.ccToken.billToFirstName,
billToLastName: order.payment.ccToken.billToLastName,
referenceNumber: order.payment.ccToken.referenceNumber,
authCode: order.payment.ccToken.authCode,
transactionId: order.payment.ccToken.transactionId,
transReferenceNumber: order.payment.ccToken.transReferenceNumber,
},
},
serviceLocation: {
streetAddress: order.serviceLocation.address,
@ -1721,6 +1783,12 @@ export const actions = {
savePiaWorkOrder(context, piaWorkOrder) {
context.commit(storeMutations.UPDATE_PIA_WORK_ORDER, piaWorkOrder);
},
saveCCToken(context, ccToken) {
context.commit(storeMutations.UPDATE_CCTOKEN, ccToken);
},
savePaypalToken(context, ppToken) {
context.commit(storeMutations.UPDATE_PAYPAL_TOKEN, ppToken);
},
// Business domain actions
@ -2007,11 +2075,19 @@ export const actions = {
},
savePaymentType(context, isInsurance) {
if (isInsurance !== context.getters.payment.isInsurance) {
context.commit(storeMutations.RESET_PAYMENT_METHOD_CHOICE);
}
context.commit(storeMutations.UPDATE_IS_INSURANCE, isInsurance);
},
savePayNowType(context, payNowType) {
context.commit(storeMutations.UPDATE_PAY_NOW_TYPE, payNowType);
savePaymentMethodChoice(context, paymentMethod) {
const isPia = paymentMethod !== paymentMethods.LATER;
context.commit(storeMutations.UPDATE_IS_PIA, isPia);
context.commit(storeMutations.UPDATE_PIA_TYPE, isPia ? paymentMethod : null);
},
saveParentAccountNumber(context, parentAccountNumber) {
@ -2111,14 +2187,13 @@ export const actions = {
pageNameToLog,
}
) {
const flattenedLineItemsWithChildParts =
getFlattenedArrayOfLineItemsWithChildParts(pricedLineItems);
const lineItemsWithOnlyPriceInfo = flattenedLineItemsWithChildParts.map((lineItem) => ({
// const flattenedLineItemsWithChildParts =
// getFlattenedArrayOfLineItemsWithChildParts(pricedLineItems);
const lineItemsWithOnlyPriceInfo = pricedLineItems.map((lineItem) => ({
partNumber: lineItem.partNumber,
laborAmount: lineItem.laborAmount,
kitPrice: lineItem.kitPrice,
sellingPrice: lineItem.sellingPrice,
laborAmount: lineItem.laborAmount ?? 0,
kitPrice: lineItem.kitPrice ?? 0,
sellingPrice: lineItem.sellingPrice ?? 0,
}));
const pricedLineItemsFormattedForRequest =
@ -2281,6 +2356,7 @@ export const actions = {
context.state.order.serviceLocation.appointmentType
) {
context.commit(storeMutations.RESET_SCHEDULE);
context.commit(storeMutations.RESET_PAYMENT_METHOD_CHOICE);
}
}
@ -2452,8 +2528,7 @@ function convertGlassPieceNamingFromApi(glassArray) {
}
function addPricesToLineItems(lineItems, pricingLineItems) {
const pricedLineItems = deepClone(lineItems);
pricedLineItems.forEach((lineItem) => {
lineItems.forEach((lineItem) => {
let lineItemIndex = pricingLineItems.findIndex(
(pricingLineItem) => pricingLineItem.partNumber === lineItem.partNumber
);
@ -2466,13 +2541,15 @@ function addPricesToLineItems(lineItems, pricingLineItems) {
lineItem.laborAmount = pricedLineItem.laborAmount;
lineItem.sellingPrice = pricedLineItem.sellingPrice;
lineItem.kitPrice = pricedLineItem.kitPrice;
lineItem.salesTax = pricedLineItem.salesTax;
});
return pricedLineItems;
return lineItems;
}
function addTaxesToPricedLineItems(lineItems, taxingLineItems) {
lineItems.forEach((lineItem) => {
const taxedLineItems = deepClone(lineItems);
taxedLineItems.forEach((lineItem) => {
let lineItemIndex = taxingLineItems.findIndex(
(taxingLineItem) => taxingLineItem.partNumber === lineItem.partNumber
);
@ -2482,10 +2559,10 @@ function addTaxesToPricedLineItems(lineItems, taxingLineItems) {
}
let taxedLineItem = taxingLineItems.splice(lineItemIndex, 1)[0];
lineItem.SalesTax = taxedLineItem.SalesTax;
lineItem.salesTax = taxedLineItem.salesTax;
});
return lineItems;
return taxedLineItems;
}
function getArrayOfAllLineItems(lineItems) {

View file

@ -753,6 +753,19 @@ describe("Actions", () => {
isVerified: false,
},
isInsurance: false,
ccToken: {
subscriptionId: null,
expMonth: null,
expYear: null,
cardType: null,
billToPostalCode: null,
billToFirstName: null,
billToLastName: null,
referenceNumber: null,
authCode: null,
transactionId: null,
transReferenceNumber: null,
},
},
serviceLocation: {},
customer: {},

View file

@ -14,6 +14,30 @@ body {
display: flex;
flex-direction: column;
}
// Set max-width on columns to prevent overly-wide
// components on extra wide screens.
.col-md-6 {
max-width: 472px;
@include media-breakpoint-up(xl) {
max-width: 708px;
}
.col {
max-width: 236px;
&.one-list-card-width {
max-width: 472px;
@include media-breakpoint-up(xl) {
max-width: 66.6666666%;
}
}
}
}
.col-xl-4 {
max-width: 472px;
.col {
max-width: 100%;
}
}
//END set max-width on columns
}
.pointer {
cursor: pointer;

View file

@ -1,6 +1,6 @@
<template>
<div
class="alert fade show text-center mb-0 py-2 px-4"
class="alert fade show text-center mb-0 py-2 px-4 border-0"
role="alert"
:class="[
isDismissible ? 'alert-dismissible' : '',