Merge branch 'develop' into feature/CASH-1947
This commit is contained in:
commit
f86a06a466
27 changed files with 1202 additions and 90 deletions
9
jsconfig.json
Normal file
9
jsconfig.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
5
src/assets/img/icons/info-circle-blue.svg
Normal file
5
src/assets/img/icons/info-circle-blue.svg
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 0C6.41775 0 4.87104 0.469192 3.55544 1.34824C2.23985 2.22729 1.21447 3.47672 0.608967 4.93853C0.00346626 6.40034 -0.15496 8.00887 0.153721 9.56072C0.462403 11.1126 1.22433 12.538 2.34315 13.6569C3.46197 14.7757 4.88743 15.5376 6.43928 15.8463C7.99113 16.155 9.59966 15.9965 11.0615 15.391C12.5233 14.7855 13.7727 13.7602 14.6518 12.4446C15.5308 11.129 16 9.58225 16 8C16 5.87827 15.1571 3.84344 13.6569 2.34315C12.1566 0.842855 10.1217 0 8 0V0ZM8.4672 11.4C8.4672 11.5485 8.4082 11.691 8.30318 11.796C8.19816 11.901 8.05572 11.96 7.9072 11.96C7.75868 11.96 7.61624 11.901 7.51122 11.796C7.4062 11.691 7.3472 11.5485 7.3472 11.4V7.08C7.3472 6.93148 7.4062 6.78904 7.51122 6.68402C7.61624 6.579 7.75868 6.52 7.9072 6.52C8.05572 6.52 8.19816 6.579 8.30318 6.68402C8.4082 6.78904 8.4672 6.93148 8.4672 7.08V11.4ZM7.9072 5.4C7.79645 5.4 7.68818 5.36716 7.59608 5.30562C7.50399 5.24409 7.43222 5.15663 7.38983 5.0543C7.34745 4.95198 7.33636 4.83938 7.35796 4.73075C7.37957 4.62212 7.43291 4.52234 7.51122 4.44402C7.58954 4.3657 7.68932 4.31237 7.79795 4.29076C7.90658 4.26915 8.01918 4.28024 8.12151 4.32263C8.22383 4.36501 8.31129 4.43679 8.37283 4.52888C8.43436 4.62097 8.4672 4.72924 8.4672 4.84C8.46741 4.9136 8.45307 4.98651 8.42501 5.05455C8.39694 5.12259 8.3557 5.18441 8.30365 5.23645C8.25161 5.28849 8.18979 5.32973 8.12176 5.3578C8.05372 5.38587 7.9808 5.40021 7.9072 5.4Z" fill="#0070D1"/>
|
||||
</svg>
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
|
|
@ -15,6 +15,8 @@ const GaCategories = {
|
|||
APPOINTMENT: "Appointment",
|
||||
SERVICE_LOCATION: "service-location",
|
||||
CONFIRMATION_CLICKED: "confirmation clicked",
|
||||
INSHOP_CONFIRMATION_CLICKED: "inshop confirmation clicked",
|
||||
MOBILE_CONFIRMATION_CLICKED: "mobile confirmation clicked",
|
||||
RECAL_DISCLAIMER: "Recalibration disclaimer",
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ const experimentUniverses = {
|
|||
IGQ_SkipQuote: "NextGen_IGQSkipToInsurance",
|
||||
AFTERPAY_BREAKOUT_DISPLAY: "AfterpayBreakoutDisplay",
|
||||
MOBILE_FIRST_APPOINTMENT: "MobileFirstAppointment",
|
||||
MULTI_LOCATION_POPUP: "MultiLocationPopup",
|
||||
};
|
||||
|
||||
const experimentSettings = {
|
||||
|
|
@ -40,7 +41,13 @@ const experimentSettings = {
|
|||
SHOW_MOBILE_FIRST_POPUP_REPLACE_WITHOUT_MSR: "Show_Mobile_First_Popup_Replace_Without_MSR",
|
||||
SHOW_MOBILE_FIRST_POPUP_REPLACE_WITH_MSR: "Show_Mobile_First_Popup_Replace_With_MSR",
|
||||
OFFER_OEM: "OfferOem",
|
||||
SHOW_MULTI_LOCATION_APPT: "ShowMultiLocationAppt",
|
||||
SHOW_NO_AVAILABILE_DAYS_MULTI_LOCATION: "Show_NoAvailableDaysMultiLocation",
|
||||
SHOW_NO_PM_DAYS_MULTI_LOCATION: "Show_NoPMDaysMultiLocation",
|
||||
SHOW_PM_DAYS_MULTI_LOCATION: "Show_PMDaysMultiLocation",
|
||||
USE_ADYEN_PAYMENT: "UseAdyenPayment",
|
||||
SHOW_UNVERIFIED_DEDUCT_ENTRY: "ShowUnverifiedDeducEntry",
|
||||
SERVICE_PACKAGE_ORDER: "ServicePackageOrder",
|
||||
};
|
||||
|
||||
const experimentTriggers = {
|
||||
|
|
|
|||
|
|
@ -226,6 +226,7 @@ export default {
|
|||
let classes;
|
||||
switch (this.buttonTypeString) {
|
||||
case "timeSlotModalListButton":
|
||||
case "multiLocationRadioButton":
|
||||
case "listButton":
|
||||
classes = "w-100";
|
||||
break;
|
||||
|
|
@ -265,6 +266,8 @@ export default {
|
|||
classes += " radio-button-container";
|
||||
} else if (this.buttonTypeString == "servicePackageRadio") {
|
||||
classes = "package-wrapper col";
|
||||
} else if (this.buttonTypeString == "multiLocationRadioButton") {
|
||||
classes = "multi-location-wrapper col";
|
||||
}
|
||||
|
||||
if (this.buttonTypeString == "listCard" && this.buttonsInfo.length > 2) {
|
||||
|
|
|
|||
|
|
@ -31,23 +31,15 @@
|
|||
<div class="donation-slider my-4">
|
||||
<input
|
||||
name="amount"
|
||||
id="one"
|
||||
id="zero"
|
||||
type="radio"
|
||||
:value="donationValues[0]"
|
||||
checked />
|
||||
<label for="one" class="label">$1</label>
|
||||
<input
|
||||
name="amount"
|
||||
id="three"
|
||||
type="radio"
|
||||
:value="donationValues[1]" />
|
||||
<label for="three" class="label">$3</label>
|
||||
<input
|
||||
name="amount"
|
||||
id="five"
|
||||
type="radio"
|
||||
:value="donationValues[2]" />
|
||||
<label for="five" class="label">$5</label>
|
||||
<label for="zero" class="label">${{ donationValues[0] }}</label>
|
||||
<input name="amount" id="one" type="radio" :value="donationValues[1]" />
|
||||
<label for="one" class="label">${{ donationValues[1] }}</label>
|
||||
<input name="amount" id="two" type="radio" :value="donationValues[2]" />
|
||||
<label for="two" class="label">${{ donationValues[2] }}</label>
|
||||
<div class="indicator-bar"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -264,17 +256,17 @@ export default {
|
|||
background: $blue;
|
||||
}
|
||||
input {
|
||||
&#one:checked {
|
||||
&#zero:checked {
|
||||
~ .indicator-bar {
|
||||
transform: translateX(2rem);
|
||||
}
|
||||
}
|
||||
&#three:checked {
|
||||
&#one:checked {
|
||||
~ .indicator-bar {
|
||||
transform: translateX(calc(100% + 2rem));
|
||||
}
|
||||
}
|
||||
&#five:checked {
|
||||
&#two:checked {
|
||||
~ .indicator-bar {
|
||||
transform: translateX(calc(200% + 2rem));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -317,6 +317,10 @@ export default {
|
|||
serviceLocationZipCode:
|
||||
store.getters.order.serviceLocation.zipCode,
|
||||
pricedLineItems: pricedLineItemsToTax,
|
||||
serviceLocationAddressLine1:
|
||||
store.getters.order.serviceLocation?.address,
|
||||
serviceLocationAddressLine2:
|
||||
store.getters.order.serviceLocation?.address2,
|
||||
},
|
||||
"payment-method",
|
||||
false
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ export async function loadSessionIfPresent(isConceptInsurance, pageNameToLog) {
|
|||
// Do nothing if there is no cookie or session to use for loading.
|
||||
if (
|
||||
funnelCookie == null ||
|
||||
funnelCookie.SavedSessionId == null ||
|
||||
funnelCookie.ReferralCorrelationId == null ||
|
||||
!funnelCookie.ReferralNumber
|
||||
) {
|
||||
|
|
@ -128,7 +127,7 @@ async function loadSession(
|
|||
const response = await baseMixin.methods.dispatchStoreActionWithLogging(
|
||||
storeActions.LOAD_SESSION,
|
||||
{
|
||||
savedSessionId: savedSessionId?.toString(),
|
||||
savedSessionId: savedSessionId != null ? savedSessionId.toString() : null,
|
||||
referralNumber,
|
||||
referralDate,
|
||||
referralCorrelationId,
|
||||
|
|
|
|||
11
src/helpers/vehicle-helper.js
Normal file
11
src/helpers/vehicle-helper.js
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import store from "@/store";
|
||||
|
||||
export function hasVehicleInfo() {
|
||||
return (
|
||||
store.getters.order?.vehicle.year &&
|
||||
store.getters.order?.vehicle?.make &&
|
||||
store.getters.order?.vehicle?.model &&
|
||||
store.getters.order?.vehicle?.style &&
|
||||
store.getters.order?.vehicle?.carId
|
||||
);
|
||||
}
|
||||
|
|
@ -470,7 +470,8 @@ export default {
|
|||
}
|
||||
},
|
||||
donationValues() {
|
||||
return [1, 3, 5];
|
||||
var donationAmounts = this.getCmsContent("DonationAmountsWidget", "Text");
|
||||
return donationAmounts.split(",").map(Number);
|
||||
},
|
||||
lineItemsWithoutDonation() {
|
||||
const lineItemsClone = deepClone(this.lineItems);
|
||||
|
|
@ -531,10 +532,11 @@ export default {
|
|||
window.location.assign(location.protocol + "//" + location.host);
|
||||
},
|
||||
async addDonationToOrder() {
|
||||
const selectedItemNumber = this.donationValues.indexOf(Number(this.donationAmount));
|
||||
const donationResponse = await baseMixin.methods.dispatchStoreActionWithLogging(
|
||||
storeActions.SUBMIT_DONATION_PART_TO_SV2,
|
||||
{
|
||||
donationAmount: parseInt(this.donationAmount),
|
||||
selectedItemNumber: selectedItemNumber,
|
||||
submittedOrder: this.submittedOrder,
|
||||
},
|
||||
false
|
||||
|
|
|
|||
|
|
@ -94,6 +94,9 @@ function setupMocks() {
|
|||
vehicle: { year: 2020, make: "Toyota", model: "Camry", carId: "12345" },
|
||||
damage: { isRepair: false },
|
||||
},
|
||||
applicationUser: {
|
||||
experiments: [],
|
||||
},
|
||||
policy: {
|
||||
currentDeductible: 123,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -53,6 +53,17 @@
|
|||
v-bind:isDismissible="false" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<alert
|
||||
ref="UnverifiedInsuranceAlert"
|
||||
class="mt-5 mb-5"
|
||||
cmsWidgetName="UnverifiedInsuranceAlertWidget"
|
||||
v-if="shouldDisplayUnverifiedInsuranceAlert"
|
||||
:showInfoIcon="true"
|
||||
:showHorizontalRow="true"
|
||||
alertClass="alert-info" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="isRecalibrationOnOrder && shouldHideRecalibration"
|
||||
class="questions-about-service mt-5">
|
||||
|
|
@ -311,6 +322,8 @@ export default {
|
|||
serviceLocationState: store.getters.order.serviceLocation.state,
|
||||
serviceLocationZipCode: store.getters.order.serviceLocation.zipCode,
|
||||
pricedLineItems: lineItemsForCart,
|
||||
serviceLocationAddressLine1: store.getters.order.serviceLocation?.address,
|
||||
serviceLocationAddressLine2: store.getters.order.serviceLocation?.address2,
|
||||
},
|
||||
"payment-method",
|
||||
false
|
||||
|
|
@ -515,6 +528,10 @@ export default {
|
|||
serviceLocationState: this.$store.getters.order.serviceLocation.state,
|
||||
serviceLocationZipCode: this.$store.getters.order.serviceLocation.zipCode,
|
||||
pricedLineItems: this.lineItems,
|
||||
serviceLocationAddressLine1:
|
||||
this.$store.getters.order.serviceLocation?.address,
|
||||
serviceLocationAddressLine2:
|
||||
this.$store.getters.order.serviceLocation?.address2,
|
||||
},
|
||||
"payment-method",
|
||||
false
|
||||
|
|
@ -639,6 +656,8 @@ export default {
|
|||
serviceLocationState: store.getters.order.serviceLocation.state,
|
||||
serviceLocationZipCode: store.getters.order.serviceLocation.zipCode,
|
||||
pricedLineItems: this.lineItems,
|
||||
serviceLocationAddressLine1: store.getters.order.serviceLocation?.address,
|
||||
serviceLocationAddressLine2: store.getters.order.serviceLocation?.address2,
|
||||
},
|
||||
"payment-method",
|
||||
false
|
||||
|
|
@ -865,6 +884,13 @@ export default {
|
|||
shouldDisplayPiaAlert() {
|
||||
return getBoolFromString(this.$route?.query?.piaError);
|
||||
},
|
||||
shouldDisplayUnverifiedInsuranceAlert() {
|
||||
const shouldDisplayUnverifiedDeduct = experimentMixin.methods.hasSettingEqualTo(
|
||||
experimentSettings.SHOW_UNVERIFIED_DEDUCT_ENTRY,
|
||||
"true"
|
||||
);
|
||||
return shouldDisplayUnverifiedDeduct;
|
||||
},
|
||||
// Necessary to make the watcher of lineItems work
|
||||
// JavaScript does not keep a record of the old value, only a reference to it's location in the memory.
|
||||
// This creates a separate location for oldValue/newValue so they can be compared
|
||||
|
|
@ -1033,7 +1059,15 @@ export default {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.alert.alert-info.widget-name-UnverifiedInsuranceAlertWidget {
|
||||
:deep(.alert-heading) {
|
||||
color: $black;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(span.label),
|
||||
:deep(span) {
|
||||
font-family: UrbanistSemibold, Arial, Helvetica, sans-serif;
|
||||
|
|
|
|||
|
|
@ -35,6 +35,11 @@ jest.mock("@/mixins/experiment-mixin.js", () => ({
|
|||
return true;
|
||||
}
|
||||
},
|
||||
hasSetting(settingName) {
|
||||
if (settingName === mockExperimentSettings.PROMO_ON_PACKAGE) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
hasSettingEqualTo(settingName, settingValue) {
|
||||
return false;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -162,6 +162,16 @@ export default {
|
|||
}));
|
||||
|
||||
this.$emit("currentNumberOfPackages", modifiedAnswers.length);
|
||||
|
||||
if (this.experimentPackageOrder()) {
|
||||
const orderedKeys = this.experimentPackageOrder()
|
||||
.split(",")
|
||||
.map((key) => key.trim());
|
||||
modifiedAnswers.sort(
|
||||
(a, b) => orderedKeys.indexOf(a.value) - orderedKeys.indexOf(b.value)
|
||||
);
|
||||
}
|
||||
|
||||
return modifiedAnswers;
|
||||
},
|
||||
isDiscountOnOrder() {
|
||||
|
|
@ -234,6 +244,11 @@ export default {
|
|||
)
|
||||
);
|
||||
},
|
||||
experimentPackageOrder() {
|
||||
return experimentMixin.methods.hasSetting(experimentSettings.SERVICE_PACKAGE_ORDER)
|
||||
? experimentMixin.methods.getSettingValue(experimentSettings.SERVICE_PACKAGE_ORDER)
|
||||
: false;
|
||||
},
|
||||
getButtonTypeObject() {
|
||||
if (this.isAfterpayBreakoutDisplay()) {
|
||||
return this.servicePackageRadioAfterpayExperiment;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,345 @@
|
|||
<template>
|
||||
<div id="multi-location-modal-container">
|
||||
<modal
|
||||
:ref="modalName"
|
||||
:headerText="modalHeaderText"
|
||||
suppressPageScroll
|
||||
:onModalClosedCallback="onModalClosed"
|
||||
:footerButtonText="modalFooterText"
|
||||
:isFooterButtonPrimary="true"
|
||||
@footer-button-event="confirmAppointment"
|
||||
@isModalOpened="setIsModalOpen">
|
||||
<buttonQuestion
|
||||
ref="buttonQuestion"
|
||||
buttonTypeString="multiLocationRadioButton"
|
||||
:buttonTypeObject="multiLocationRadioButton"
|
||||
class="mt-5"
|
||||
:answers="availableAppointments"
|
||||
groupName="multiLocationQuestion"
|
||||
textPosition="text-center"
|
||||
v-model="selectedValue"
|
||||
isRequired
|
||||
validationRules="time-slot-required" />
|
||||
|
||||
<template v-slot:modal-header-slot>
|
||||
<span>{{ modalSubHeaderText }}</span>
|
||||
</template>
|
||||
<template v-slot:modal-footer-slot>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-link"
|
||||
id="see-more-options"
|
||||
:disabled="isLoading"
|
||||
@click="closeModal">
|
||||
{{ buttonText }}
|
||||
</button>
|
||||
</template>
|
||||
</modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// Supporting files
|
||||
import store from "@/store";
|
||||
import modal from "@/digital-components/modal/modal";
|
||||
import { splitCopyOnCMSPlaceHolder } from "@/helpers/cms-content-helper";
|
||||
import { get12HourTimeFormat } from "@/helpers/date-helper";
|
||||
import { getDisplayTextForDurationLength } from "@/helpers/duration-length-helper";
|
||||
import buttonQuestion from "@/digital-components/button-question/button-question";
|
||||
import multiLocationRadioButton from "./multi-location-radio/multi-location-radio";
|
||||
|
||||
// Validation
|
||||
import { defineRule, useField } from "vee-validate";
|
||||
import { errorMessages } from "@/constants/error-messages";
|
||||
import { required } from "@/helpers/validation-rules";
|
||||
|
||||
const NULL_APPOINTMENT = {
|
||||
estimatedServiceMinutes: {
|
||||
minimum: null,
|
||||
maximum: null,
|
||||
},
|
||||
timeSlot: null,
|
||||
date: null,
|
||||
};
|
||||
|
||||
// Validation for the modal button
|
||||
defineRule("time-slot-required", required(errorMessages.OPTION_REQUIRED));
|
||||
|
||||
export default {
|
||||
name: "multi-location-modal",
|
||||
props: {
|
||||
cmsWidgetName: String,
|
||||
modalWidgetName: String,
|
||||
buttonWidgetName: String,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isModalOpen: false,
|
||||
mobileAppointmentOption: NULL_APPOINTMENT,
|
||||
inshopAppointmentOption: NULL_APPOINTMENT,
|
||||
confirmedAppointment: false,
|
||||
multiLocationRadioButton: multiLocationRadioButton,
|
||||
selectedValue: null,
|
||||
experimentSettings: "",
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
splitCopyOnCMSPlaceHolder,
|
||||
get12HourTimeFormat,
|
||||
openModal() {
|
||||
this.modal.openModal();
|
||||
},
|
||||
onModalClosed() {
|
||||
this.pushEvent();
|
||||
this.confirmedAppointment = false;
|
||||
},
|
||||
closeModal() {
|
||||
this.modal.closeModal();
|
||||
},
|
||||
confirmAppointment() {
|
||||
if (!this.selectedAppointment.timeSlot.id) return;
|
||||
|
||||
const selectedTimeSlot = {
|
||||
timeSlot: this.selectedAppointment.timeSlot,
|
||||
routeCode: this.selectedAppointment.timeSlot.id,
|
||||
date: this.selectedAppointment.date,
|
||||
};
|
||||
this.confirmedAppointment = true;
|
||||
this.$emit("confirm-appointment", {
|
||||
selectedTimeSlot: selectedTimeSlot,
|
||||
appointmentType: this.selectedValue,
|
||||
});
|
||||
this.modal.closeModal();
|
||||
},
|
||||
setPromotedMobileAppointment(appointment) {
|
||||
this.mobileAppointmentOption = appointment;
|
||||
},
|
||||
setPromotedInshopAppointment(appointment) {
|
||||
this.inshopAppointmentOption = appointment;
|
||||
},
|
||||
getIsModalOpen() {
|
||||
return this.isModalOpen;
|
||||
},
|
||||
setIsModalOpen(isOpen) {
|
||||
this.isModalOpen = isOpen;
|
||||
},
|
||||
captureExperimentSettings(settingsString) {
|
||||
this.experimentSettings = settingsString;
|
||||
},
|
||||
pushEvent() {
|
||||
let gaAction = this.confirmedAppointment ? this.GaActions.YES : this.GaActions.NO;
|
||||
if (
|
||||
this.selectedValue?.toLowerCase() === "mobile" &&
|
||||
this.mobileAppointmentOption?.timeSlot
|
||||
) {
|
||||
this.pushEventToGA(
|
||||
this.GaCategories.MOBILE_CONFIRMATION_CLICKED,
|
||||
gaAction,
|
||||
this.mobileAppointmentOption.timeSlot.startTime +
|
||||
"-" +
|
||||
this.mobileAppointmentOption.timeSlot.endTime +
|
||||
" " +
|
||||
this.mobileAppointmentOption.date,
|
||||
true,
|
||||
null,
|
||||
this.experimentSettings
|
||||
);
|
||||
} else if (
|
||||
this.selectedValue?.toLowerCase() === "inshop" &&
|
||||
this.inshopAppointmentOption?.timeSlot
|
||||
) {
|
||||
this.pushEventToGA(
|
||||
this.GaCategories.INSHOP_CONFIRMATION_CLICKED,
|
||||
gaAction,
|
||||
this.inshopAppointmentOption.timeSlot.startTime +
|
||||
"-" +
|
||||
this.inshopAppointmentOption.date,
|
||||
true,
|
||||
null,
|
||||
this.experimentSettings
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
selectedAppointment() {
|
||||
if (this.selectedValue?.toLowerCase() === "mobile") {
|
||||
return this.mobileAppointmentOption;
|
||||
} else if (this.selectedValue?.toLowerCase() === "inshop") {
|
||||
return this.inshopAppointmentOption;
|
||||
} else {
|
||||
return NULL_APPOINTMENT;
|
||||
}
|
||||
},
|
||||
modalName() {
|
||||
return this.modalWidgetName;
|
||||
},
|
||||
modal() {
|
||||
return this.$refs[this.modalName];
|
||||
},
|
||||
modalSubHeaderText() {
|
||||
return this.getCmsContent(this.modalWidgetName, "SubheaderText");
|
||||
},
|
||||
modalHeaderText() {
|
||||
return this.getCmsContent(this.modalWidgetName, "HeaderText");
|
||||
},
|
||||
modalFooterText() {
|
||||
return this.getCmsContent(this.modalWidgetName, "FooterText");
|
||||
},
|
||||
buttonText() {
|
||||
return this.getCmsContent(this.modalWidgetName, "FooterText2");
|
||||
},
|
||||
mobileButtonLabel() {
|
||||
return this.getCmsContent(this.buttonWidgetName, "HeaderText");
|
||||
},
|
||||
inshopButtonLabel() {
|
||||
return this.getCmsContent(this.buttonWidgetName, "SubheaderText");
|
||||
},
|
||||
mobileButtonDate() {
|
||||
if (!this.mobileAppointmentOption.date) return "";
|
||||
return new Date(this.mobileAppointmentOption.date).toLocaleDateString("en-US", {
|
||||
timeZone: "UTC",
|
||||
weekday: "short",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
});
|
||||
},
|
||||
inshopButtonDate() {
|
||||
if (!this.inshopAppointmentOption.date) return "";
|
||||
return new Date(this.inshopAppointmentOption.date).toLocaleDateString("en-US", {
|
||||
timeZone: "UTC",
|
||||
weekday: "short",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
});
|
||||
},
|
||||
mobileButtonTime() {
|
||||
const timeslot =
|
||||
this.get12HourTimeFormat(this.mobileAppointmentOption.timeSlot?.startTime) +
|
||||
" - " +
|
||||
this.get12HourTimeFormat(this.mobileAppointmentOption.timeSlot?.endTime);
|
||||
return timeslot;
|
||||
},
|
||||
inshopButtonTime() {
|
||||
const timeslot = this.get12HourTimeFormat(
|
||||
this.inshopAppointmentOption.timeSlot?.startTime
|
||||
);
|
||||
return timeslot;
|
||||
},
|
||||
mobileLocationLabel() {
|
||||
return this.getCmsContent(this.buttonWidgetName, "BodyText");
|
||||
},
|
||||
inshopLocationLabel() {
|
||||
return this.inshopAppointmentOption?.addressCopy;
|
||||
},
|
||||
mobileServiceDuration() {
|
||||
const durationTime = getDisplayTextForDurationLength(
|
||||
this.mobileAppointmentOption.estimatedServiceMinutes.minimum,
|
||||
this.mobileAppointmentOption.estimatedServiceMinutes.maximum
|
||||
);
|
||||
const footerText = this.getCmsContent(this.buttonWidgetName, "FooterText").replaceAll(
|
||||
"{custom:serviceLength}",
|
||||
durationTime
|
||||
);
|
||||
return footerText;
|
||||
},
|
||||
inshopServiceDuration() {
|
||||
const durationTime = getDisplayTextForDurationLength(
|
||||
this.inshopAppointmentOption.estimatedServiceMinutes.minimum,
|
||||
this.inshopAppointmentOption.estimatedServiceMinutes.maximum
|
||||
);
|
||||
const footerText = this.getCmsContent(this.buttonWidgetName, "FooterText").replaceAll(
|
||||
"{custom:serviceLength}",
|
||||
durationTime
|
||||
);
|
||||
return footerText;
|
||||
},
|
||||
availableAppointments() {
|
||||
return [
|
||||
{
|
||||
value: "mobile",
|
||||
buttonLabel: this.mobileButtonDate,
|
||||
buttonLabelSubCopy: this.mobileButtonTime,
|
||||
buttonBodyCopy: this.mobileLocationLabel,
|
||||
buttonAuxillaryCopy: this.mobileButtonLabel,
|
||||
buttonFooterCopy: this.mobileServiceDuration,
|
||||
},
|
||||
{
|
||||
value: "inshop",
|
||||
buttonLabel: this.inshopButtonDate,
|
||||
buttonLabelSubCopy: this.inshopButtonTime,
|
||||
buttonBodyCopy: this.inshopLocationLabel,
|
||||
buttonAuxillaryCopy: this.inshopButtonLabel,
|
||||
buttonFooterCopy: this.inshopServiceDuration,
|
||||
},
|
||||
];
|
||||
},
|
||||
},
|
||||
components: { modal, buttonQuestion },
|
||||
};
|
||||
</script>
|
||||
<style lang="scss">
|
||||
#multi-location-modal-container {
|
||||
.modal-component {
|
||||
@include media-breakpoint-up(md) {
|
||||
.modal-dialog {
|
||||
left: 0;
|
||||
align-content: center;
|
||||
flex-wrap: wrap;
|
||||
width: 22.063rem;
|
||||
transform: translate(0, 0);
|
||||
|
||||
.modal-content {
|
||||
border-radius: $border-radius-lg;
|
||||
}
|
||||
}
|
||||
}
|
||||
.modal-dialog {
|
||||
.modal-content {
|
||||
.modal-header {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
padding: 0 1rem;
|
||||
|
||||
& > span {
|
||||
color: $red;
|
||||
font-weight: $font-weight-600;
|
||||
text-transform: uppercase;
|
||||
font-family: $font-family-sans-serif-semibold;
|
||||
}
|
||||
.modal-title {
|
||||
font-size: $font-size-20;
|
||||
font-weight: $font-weight-normal;
|
||||
}
|
||||
}
|
||||
.modal-body {
|
||||
padding: 0.5rem 1rem;
|
||||
|
||||
.button-question {
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.modal-body-inner {
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 1rem;
|
||||
background-color: #f4f4f4;
|
||||
font-size: $font-size-14;
|
||||
}
|
||||
}
|
||||
}
|
||||
.modal-footer {
|
||||
flex-flow: wrap-reverse;
|
||||
|
||||
#see-more-options {
|
||||
width: 100%;
|
||||
text-decoration: none;
|
||||
margin-top: 1.5rem;
|
||||
font-weight: $font-weight-600;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,121 @@
|
|||
import { mount } from "@vue/test-utils";
|
||||
import multiLocationRadio from "./multi-location-radio";
|
||||
import inputButtonWrapperMixin from "@/mixins/input-button-wrapper-mixin";
|
||||
|
||||
describe("multi-location-radio.vue", () => {
|
||||
it("Should include buttonLabel in html", async () => {
|
||||
// Arrange
|
||||
let { wrapper } = setupMocks({
|
||||
mountOptionsMockData: {
|
||||
propsData: mockProps,
|
||||
},
|
||||
});
|
||||
|
||||
// Act
|
||||
const outputHtml = wrapper.html();
|
||||
|
||||
// Assert
|
||||
expect(outputHtml).toEqual(expect.stringContaining(mockProps["buttonLabel"]));
|
||||
});
|
||||
|
||||
it("Should include buttonLabelSubCopy in html", async () => {
|
||||
// Arrange
|
||||
let { wrapper } = setupMocks({
|
||||
mountOptionsMockData: {
|
||||
propsData: mockProps,
|
||||
},
|
||||
});
|
||||
|
||||
// Act
|
||||
const outputHtml = wrapper.html();
|
||||
|
||||
// Assert
|
||||
expect(outputHtml).toEqual(expect.stringContaining(mockProps["buttonLabelSubCopy"]));
|
||||
});
|
||||
it("Should include buttonFooterCopy in html", async () => {
|
||||
// Arrange
|
||||
let { wrapper } = setupMocks({
|
||||
mountOptionsMockData: {
|
||||
propsData: mockProps,
|
||||
},
|
||||
});
|
||||
|
||||
// Act
|
||||
const outputHtml = wrapper.html();
|
||||
|
||||
// Assert
|
||||
expect(outputHtml).toEqual(expect.stringContaining(mockProps["buttonFooterCopy"]));
|
||||
});
|
||||
it("Should get 5 strings from getArrayOfListItemsFromRawCmsCopy when provided with a <ul><li>...</li>(x5)</ul> buttonBodyCopy", async () => {
|
||||
// Arrange
|
||||
let { wrapper } = setupMocks({
|
||||
mountOptionsMockData: {
|
||||
propsData: mockProps,
|
||||
},
|
||||
});
|
||||
// Act
|
||||
const results = wrapper.vm.getArrayOfListItemsFromRawCmsCopy(wrapper.vm.buttonBodyCopy);
|
||||
|
||||
// Assert
|
||||
expect(results.length).toBe(5);
|
||||
});
|
||||
it("Should get strings from getArrayOfListItemsFromRawCmsCopy without <ul> or <li> tags when provided with a <ul><li>...</li></ul> buttonBodyCopy", async () => {
|
||||
// Arrange
|
||||
let { wrapper } = setupMocks({
|
||||
mountOptionsMockData: {
|
||||
propsData: mockProps,
|
||||
},
|
||||
});
|
||||
|
||||
// Act
|
||||
const results = wrapper.vm.getArrayOfListItemsFromRawCmsCopy(wrapper.vm.buttonBodyCopy);
|
||||
|
||||
// Assert
|
||||
const fileredResults = results.filter((result) => {
|
||||
return (
|
||||
result.includes("<ul>") ||
|
||||
result.includes("</ul>") ||
|
||||
result.includes("<li>") ||
|
||||
result.includes("</li>")
|
||||
);
|
||||
});
|
||||
expect(fileredResults.length).toBe(0);
|
||||
});
|
||||
it("Should get 5 strings from getArrayOfListItemsFromRawCmsCopy when buttonBodyCopy has 6 total <li>...</li>, but one is empty ", async () => {
|
||||
// Arrange
|
||||
const moddedProps = mockProps;
|
||||
moddedProps["buttonBodyCopy"] =
|
||||
"<ul><li>buttonBodyCopy test copy</li><li>2</li><li>3</li><li>4</li><li>5</li><li></li></ul>";
|
||||
let { wrapper } = setupMocks({
|
||||
mountOptionsMockData: {
|
||||
propsData: moddedProps,
|
||||
},
|
||||
});
|
||||
|
||||
// Act
|
||||
const results = wrapper.vm.getArrayOfListItemsFromRawCmsCopy(wrapper.vm.buttonBodyCopy);
|
||||
|
||||
// Assert
|
||||
expect(results.length).toBe(5);
|
||||
});
|
||||
});
|
||||
|
||||
const mockProps = {
|
||||
groupName: "group-name",
|
||||
buttonLabel: "buttonLabel test copy",
|
||||
buttonLabelAuxillaryCopy: "buttonLabelAuxillaryCopy test copy",
|
||||
buttonLabelSubCopy: "buttonLabelSubCopy test copy",
|
||||
buttonBodyCopy:
|
||||
"<ul><li>buttonBodyCopy test copy</li><li>2</li><li>3</li><li>4</li><li>5</li></ul>",
|
||||
buttonFooterCopy: "buttonFooterCopy test copy",
|
||||
buttonAuxillaryCopy: "buttonAuxillaryCopy test copy",
|
||||
};
|
||||
|
||||
function setupMocks({ mountOptionsMockData = {} }) {
|
||||
const wrapper = mount(multiLocationRadio, {
|
||||
...mountOptionsMockData,
|
||||
mixins: [inputButtonWrapperMixin],
|
||||
});
|
||||
|
||||
return { wrapper };
|
||||
}
|
||||
|
|
@ -0,0 +1,311 @@
|
|||
<template>
|
||||
<baseInputButton v-bind="$props" @buttonClicked="handleAnswerChange" v-model="selectedValue">
|
||||
<div class="radio-label" for="testradio">
|
||||
<div class="location-specs">
|
||||
<div class="row">
|
||||
<div class="col md-6">
|
||||
<p class="m-0">
|
||||
<span v-html="this.buttonLabel"></span>
|
||||
</p>
|
||||
<span class="service-tag" v-html="this.buttonAuxillaryCopy"></span>
|
||||
<p
|
||||
class="sub-label m-0"
|
||||
v-if="this.buttonLabelSubCopy"
|
||||
v-html="this.buttonLabelSubCopy"></p>
|
||||
</div>
|
||||
<div class="col md-6 location-info">
|
||||
<div
|
||||
class="location-info-label"
|
||||
v-if="this.buttonBodyCopy && this.value === 'mobile'"
|
||||
v-html="this.buttonBodyCopy"></div>
|
||||
<div
|
||||
class="location-info-label"
|
||||
v-if="this.buttonBodyCopy && this.value === 'inshop'">
|
||||
<textLink
|
||||
linkType="newWindowLink"
|
||||
:text="this.buttonBodyCopy"
|
||||
:href="inshopHref"
|
||||
target="_blank" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col md-6 time-info" v-html="this.buttonFooterCopy"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</baseInputButton>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import baseInputButton from "@/digital-components/base-input-button/base-input-button";
|
||||
import textLink from "@/ux-components/text-link/text-link";
|
||||
import inputButtonWrapperMixin from "@/mixins/input-button-wrapper-mixin";
|
||||
import {
|
||||
getRouterLinkDisplayTextFromCopy,
|
||||
getRouterLinkRouteFromCopy,
|
||||
splitCopyOnCMSPlaceHolder,
|
||||
doesCopyContainTextLink,
|
||||
} from "@/helpers/cms-content-helper";
|
||||
import baseMixin from "@/mixins/base-mixin.js";
|
||||
|
||||
export default {
|
||||
name: "multiLocationRadioButton",
|
||||
mixins: [inputButtonWrapperMixin],
|
||||
components: {
|
||||
baseInputButton,
|
||||
textLink,
|
||||
},
|
||||
computed: {
|
||||
arrayOfListItemsFromBodyText() {
|
||||
return this.getArrayOfListItemsFromRawCmsCopy(this.buttonBodyCopy);
|
||||
},
|
||||
shouldDisplayStrikeThroughPrice() {
|
||||
const displayPrice = this.buttonAuxillaryCopy?.substring(
|
||||
this.buttonAuxillaryCopy.indexOf("$")
|
||||
);
|
||||
return displayPrice != this.additionalButtonData?.strikeThroughPrice;
|
||||
},
|
||||
isAppleBrowser() {
|
||||
return baseMixin.methods.isAppleBrowser();
|
||||
},
|
||||
inshopHref() {
|
||||
if (this.isAppleBrowser) {
|
||||
return "https://maps.apple.com/?q=" + encodeURIComponent(this.buttonBodyCopy) + '"';
|
||||
}
|
||||
return (
|
||||
"https://www.google.com/maps/search/?api=1&query=" +
|
||||
encodeURIComponent(this.buttonBodyCopy) +
|
||||
'"'
|
||||
);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getRouterLinkDisplayTextFromCopy,
|
||||
getRouterLinkRouteFromCopy,
|
||||
splitCopyOnCMSPlaceHolder,
|
||||
doesCopyContainTextLink,
|
||||
stripUlTagFromCopy(copy) {
|
||||
const regex = /(?:<ul(?:.*?)>)|(?:<\/ul>)/g;
|
||||
return copy?.replace(regex, "");
|
||||
},
|
||||
getArrayOfListItemsFromRawCmsCopy(copy) {
|
||||
const withoutUlTags = this.stripUlTagFromCopy(copy);
|
||||
return withoutUlTags
|
||||
?.split(/(?:<li(?:.*?)>)|(?:<\/li>)/g)
|
||||
?.filter((lineItem) => lineItem);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.multi-location-wrapper {
|
||||
margin: 1rem 0;
|
||||
&:first-child {
|
||||
margin: 1rem 0 0 0;
|
||||
}
|
||||
&:last-child {
|
||||
margin: 1rem 0 0 0;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
@include media-breakpoint-up(md) {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
input[type="radio"] {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
left: -9999px;
|
||||
|
||||
+ .radio-label {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid $gray-300;
|
||||
box-shadow:
|
||||
0px 4px 8px -4px rgba(0, 0, 0, 0.15),
|
||||
0px 4px 24px -8px rgba(0, 0, 0, 0.2);
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
min-height: 52px;
|
||||
max-height: 126px;
|
||||
@include media-breakpoint-up(md) {
|
||||
padding-bottom: 3rem;
|
||||
}
|
||||
transition: max-height 0.25s ease-in;
|
||||
|
||||
&.has-subheader {
|
||||
min-height: 72px;
|
||||
}
|
||||
|
||||
&:before {
|
||||
content: "";
|
||||
position: relative;
|
||||
top: 5px;
|
||||
margin-right: 0.5rem;
|
||||
border-radius: 50%;
|
||||
border: 1px solid $gray-500;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
min-width: 16px;
|
||||
}
|
||||
|
||||
&:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 15px;
|
||||
top: 20px;
|
||||
border-radius: 50%;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
min-width: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
+ .radio-label {
|
||||
&:before {
|
||||
border: 1px solid #8e9292;
|
||||
box-shadow:
|
||||
0px 0px 0px 4px #9fcee6,
|
||||
0px 1px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:checked {
|
||||
+ .radio-label {
|
||||
.location-specs {
|
||||
.hide-when-closed {
|
||||
display: block;
|
||||
}
|
||||
.service-tag {
|
||||
background: $blue;
|
||||
color: $blue-150;
|
||||
}
|
||||
}
|
||||
}
|
||||
+ .radio-label {
|
||||
border: 1px solid $blue;
|
||||
background-color: $blue-100;
|
||||
}
|
||||
+ .radio-label {
|
||||
&:before {
|
||||
box-shadow: 0px 0px 0px 1px $blue;
|
||||
}
|
||||
}
|
||||
+ .radio-label {
|
||||
&:after {
|
||||
background: $blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:focus {
|
||||
+ .radio-label {
|
||||
&:before {
|
||||
border: 2px solid $blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.location-specs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
transition: all 0.5s ease;
|
||||
|
||||
.row {
|
||||
align-items: center;
|
||||
|
||||
.col {
|
||||
flex: 1 0 100%;
|
||||
}
|
||||
|
||||
.service-tag {
|
||||
font-size: $font-size-12;
|
||||
font-family: $font-family-sans-serif-semibold;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
background: $blue-150;
|
||||
color: $blue;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 1rem;
|
||||
margin: 0.5rem;
|
||||
}
|
||||
p {
|
||||
font-size: $font-size-14;
|
||||
|
||||
& > span {
|
||||
color: $black;
|
||||
font-family: $font-family-sans-serif-semibold;
|
||||
font-weight: $font-weight-bold;
|
||||
font-size: $font-size-base;
|
||||
}
|
||||
}
|
||||
.sub-label {
|
||||
margin-bottom: 0.5rem !important;
|
||||
border-bottom: 1px solid $gray-1000;
|
||||
}
|
||||
}
|
||||
|
||||
.location-info-label,
|
||||
.time-info {
|
||||
font-family: $font-family-sans-serif;
|
||||
font-size: $font-size-12;
|
||||
}
|
||||
.location-info-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&:before {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
top: 0;
|
||||
margin-right: 0.375rem;
|
||||
width: 0.75rem;
|
||||
height: 1rem;
|
||||
background-image: url(~@/assets/img/icons/location-pin.svg);
|
||||
background-repeat: no-repeat;
|
||||
background-position: 0 50%;
|
||||
background-size: 0.75rem auto;
|
||||
padding: 0 0.75rem 0 0;
|
||||
@include blue-filter();
|
||||
}
|
||||
& > a.new-window-link {
|
||||
margin-bottom: 0;
|
||||
font-family: $font-family-sans-serif;
|
||||
font-size: $font-size-12;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideaway {
|
||||
from {
|
||||
display: block;
|
||||
}
|
||||
to {
|
||||
transform: translateY(40px);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.hide-when-closed {
|
||||
display: none;
|
||||
@include media-breakpoint-up(md) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -5,7 +5,12 @@
|
|||
<mobileFirstModal
|
||||
modalWidgetName="MobileFirstModalWidget"
|
||||
ref="mobileFirstModal"
|
||||
@confirm-appointment="updateTimeSlotandNavigateForward" />
|
||||
@confirm-appointment="updateMobileFirstTimeSlotandNavigateForward" />
|
||||
<multiLocationModal
|
||||
modalWidgetName="MultiLocationModalWidget"
|
||||
buttonWidgetName="MultiLocationRadioButtonWidget"
|
||||
ref="multiLocationModal"
|
||||
@confirm-appointment="updateMultiLocationTimeSlotandNavigateForward" />
|
||||
<recalAckModal
|
||||
modalWidgetName="RecalAckModalWidget"
|
||||
agreementWidgetName="RecalAgreementQuestionWidget"
|
||||
|
|
@ -14,7 +19,9 @@
|
|||
ref="recalAckModal" />
|
||||
<div
|
||||
class="container page-container-grouped-styles"
|
||||
:class="[isMobileFirstModalOpen ? 'hidden-background' : '']">
|
||||
:class="[
|
||||
isMobileFirstModalOpen || isMultiLocationModalOpen ? 'hidden-background' : '',
|
||||
]">
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-10 col-lg-8 col-xl-7">
|
||||
<mobileFeeWaiverAlert
|
||||
|
|
@ -28,7 +35,6 @@
|
|||
cmsWidgetName="ScheduleYourServiceWidget"
|
||||
id="schedule-your-service" />
|
||||
<appointmentTypeQuestion
|
||||
v-if="hasSelectableDatesLoaded"
|
||||
v-model="appointmentTypeFromAppointmentTypeQuestion"
|
||||
v-show="isAppointmentTypeDisplayed"
|
||||
:isServiceableMobile="isServiceableMobile"
|
||||
|
|
@ -36,7 +42,6 @@
|
|||
:isDisplayed="isAppointmentTypeDisplayed"
|
||||
:mobileFeeApplies="mobileFeeApplies"
|
||||
:zipCode="zipCode"
|
||||
ref="appointmentTypeQuestion"
|
||||
groupName="appointmentTypeQuestion"
|
||||
cmsWidgetName="AppointmentTypeQuestionWidget"
|
||||
validationRules="option-required"
|
||||
|
|
@ -193,6 +198,7 @@ import shopLocation from "@/layouts/schedule/shop-location/shop-location";
|
|||
import timeSlotQuestion from "@/layouts/schedule/time-slot-question/time-slot-question.vue";
|
||||
import durationTextBlock from "@/layouts/schedule/duration-text-block/duration-text-block.vue";
|
||||
import mobileFirstModal from "@/layouts/schedule/mobile-first-modal/mobile-first-modal.vue";
|
||||
import multiLocationModal from "@/layouts/schedule/multi-location-modal/multi-location-modal.vue";
|
||||
import recalAckModal from "@/layouts/schedule/recal-ack-modal/recal-ack-modal.vue";
|
||||
|
||||
import shopQuestion from "@/layouts/service-location/shop-question/shop-question";
|
||||
|
|
@ -479,10 +485,7 @@ export default {
|
|||
shopListButton: shopListButton,
|
||||
lastSelectedInshopOrDropoffProvider: null,
|
||||
selectedMobileFirstAppointment: false,
|
||||
isShowMobileFirstAppt: experimentMixin.methods.hasSettingEqualTo(
|
||||
experimentSettings.SHOW_MOBILE_FIRST_APPT,
|
||||
"true"
|
||||
),
|
||||
selectedMultiLocationAppointment: false,
|
||||
calendarLoadingStatus: "none",
|
||||
};
|
||||
},
|
||||
|
|
@ -615,7 +618,8 @@ export default {
|
|||
shopProviderData.data
|
||||
);
|
||||
vm.initializeDatePicker().then(() => {
|
||||
vm.showMobileFirstModal();
|
||||
// vm.showMobileFirstModal(); // KEEP IN CASE WE WANT TO REINSTATE MOBILE FIRST
|
||||
vm.showMultiLocationModal();
|
||||
vm.hideLoadingModal();
|
||||
vm.calendarLoadingStatus = "none";
|
||||
if (!vm.preSelectedDate) vm.setSelectedDateToFirstAvailable();
|
||||
|
|
@ -904,6 +908,11 @@ export default {
|
|||
? this.$refs.mobileFirstModal?.getIsModalOpen()
|
||||
: false;
|
||||
},
|
||||
isMultiLocationModalOpen() {
|
||||
return this.selectableDatesMobile.days && this.selectableDatesInshop.days
|
||||
? this.$refs.multiLocationModal?.getIsModalOpen()
|
||||
: false;
|
||||
},
|
||||
displayWaitList() {
|
||||
if (!this.appointmentType) return false;
|
||||
const firstMobileDateString =
|
||||
|
|
@ -952,12 +961,24 @@ export default {
|
|||
return false;
|
||||
},
|
||||
hasSelectableDatesLoaded() {
|
||||
if (this.isMobileSelected) {
|
||||
if (this.isServiceableMobile) {
|
||||
return this.selectableDatesMobile?.days?.length > 0;
|
||||
} else {
|
||||
return this.selectableDatesInshop?.days?.length > 0;
|
||||
}
|
||||
},
|
||||
isShowMobileFirstAppt() {
|
||||
return experimentMixin.methods.hasSettingEqualTo(
|
||||
experimentSettings.SHOW_MOBILE_FIRST_APPT,
|
||||
"true"
|
||||
);
|
||||
},
|
||||
isShowMultiLocationPopup() {
|
||||
return experimentMixin.methods.hasSettingEqualTo(
|
||||
experimentSettings.SHOW_MULTI_LOCATION_APPT,
|
||||
"true"
|
||||
);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
splitCopyOnCMSPlaceHolder,
|
||||
|
|
@ -1243,7 +1264,8 @@ export default {
|
|||
}
|
||||
}
|
||||
this.initializeDatePicker().then(() => {
|
||||
this.showMobileFirstModal();
|
||||
// this.showMobileFirstModal(); // KEEP IN CASE WE WANT TO REINSTATE MOBILE FIRST
|
||||
this.showMultiLocationModal();
|
||||
this.hideLoadingModal();
|
||||
this.calendarLoadingStatus = "none";
|
||||
this.setSelectedDateToFirstAvailable();
|
||||
|
|
@ -1277,7 +1299,7 @@ export default {
|
|||
},
|
||||
|
||||
async initializeDatePicker() {
|
||||
this.isShowMobileFirstAppt && this.showLoadingModal();
|
||||
this.isShowMultiLocationPopup && this.showLoadingModal();
|
||||
|
||||
const includeMobileTimeSlots = this.isServiceableMobile;
|
||||
const includeInshopTimeSlots = this.isServiceableInshop || this.isServiceableDropoff;
|
||||
|
|
@ -1802,7 +1824,8 @@ export default {
|
|||
this.zipCodeCtu = newZipCode.zipCodeCtu;
|
||||
this.selectedDate = null;
|
||||
this.initializeDatePicker().then(() => {
|
||||
this.showMobileFirstModal();
|
||||
// this.showMobileFirstModal(); // KEEP IN CASE WE WANT TO REINSTATE MOBILE FIRST
|
||||
this.showMultiLocationModal();
|
||||
this.hideLoadingModal();
|
||||
this.calendarLoadingStatus = "none";
|
||||
this.setSelectedDateToFirstAvailable();
|
||||
|
|
@ -1857,10 +1880,13 @@ export default {
|
|||
} else {
|
||||
this.appointmentType = null;
|
||||
}
|
||||
this.setSelectedDateToFirstAvailable(); // v-if on appointmentTypeQuestion ensures dates have been loaded by now
|
||||
this.setSelectedDateToFirstAvailable();
|
||||
},
|
||||
setSelectedDateToFirstAvailable() {
|
||||
this.selectedDate = this.getFirstAvailableDate();
|
||||
if (this.hasSelectableDatesLoaded) {
|
||||
// ensure dates have been loaded by now
|
||||
this.selectedDate = this.getFirstAvailableDate();
|
||||
}
|
||||
},
|
||||
showRecalAcknowledgementModal() {
|
||||
if (this.shouldShowRecalAckModal()) {
|
||||
|
|
@ -1914,10 +1940,10 @@ export default {
|
|||
const order = this.$store.getters.order;
|
||||
const isRepair = order.damage.isRepair;
|
||||
|
||||
let preSelectedMobileAppointment = null;
|
||||
let promotedMobileAppointment = null;
|
||||
const todaysDate = getTodayDate();
|
||||
let firstMobileAMAppt = await this.getFirstAvailableMobileApptByTOD("AM");
|
||||
let firstMobilePMAppt = await this.getFirstAvailableMobileApptByTOD("PM");
|
||||
let firstMobileAMAppt = await this.getFirstAvailableApptByTOD("AM", "mobile");
|
||||
let firstMobilePMAppt = await this.getFirstAvailableApptByTOD("PM", "mobile");
|
||||
if (!firstMobileAMAppt && !firstMobilePMAppt) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -2012,7 +2038,7 @@ export default {
|
|||
if (this.isShowMobileFirstAppt) {
|
||||
if (pmMobileDays == 0) {
|
||||
// Selects the first available time slot
|
||||
preSelectedMobileAppointment =
|
||||
promotedMobileAppointment =
|
||||
numberOfDaysToFirstMobileAMDate <= numberOfDaysToFirstMobilePMDate
|
||||
? firstMobileAMAppt
|
||||
: firstMobilePMAppt;
|
||||
|
|
@ -2021,20 +2047,20 @@ export default {
|
|||
numberOfDaysToFirstMobilePMDate <= pmMobileDays
|
||||
) {
|
||||
// Selects the first available PM time slot
|
||||
preSelectedMobileAppointment = firstMobilePMAppt;
|
||||
promotedMobileAppointment = firstMobilePMAppt;
|
||||
} else if (
|
||||
firstMobilePMDate &&
|
||||
numberOfDaysToFirstMobilePMDate >= noPMMobileDays
|
||||
) {
|
||||
// Selects the first available AM time slot. If none, selects the first available PM time slot
|
||||
preSelectedMobileAppointment = firstMobileAMAppt
|
||||
promotedMobileAppointment = firstMobileAMAppt
|
||||
? firstMobileAMAppt
|
||||
: firstMobilePMAppt;
|
||||
}
|
||||
|
||||
if (preSelectedMobileAppointment) {
|
||||
if (promotedMobileAppointment) {
|
||||
this.$refs.mobileFirstModal.setSelectedAppointment(
|
||||
preSelectedMobileAppointment
|
||||
promotedMobileAppointment
|
||||
);
|
||||
this.$refs.mobileFirstModal.openModal();
|
||||
}
|
||||
|
|
@ -2042,11 +2068,176 @@ export default {
|
|||
}
|
||||
}
|
||||
},
|
||||
async getFirstAvailableMobileApptByTOD(timeOfDay) {
|
||||
if (!this.selectableDatesMobile?.days?.length) {
|
||||
async showMultiLocationModal() {
|
||||
if (!this.selectedRouteCodeData?.routeCode) {
|
||||
let maxDayRangeToShowPmTimeslot = experimentMixin.methods.getSettingValue(
|
||||
experimentSettings.SHOW_PM_DAYS_MULTI_LOCATION
|
||||
);
|
||||
let maxDayRangeToShowNoPmTimeslot = experimentMixin.methods.getSettingValue(
|
||||
experimentSettings.SHOW_NO_PM_DAYS_MULTI_LOCATION
|
||||
);
|
||||
let maxDayRangeToShowAnyTimeslot = experimentMixin.methods.getSettingValue(
|
||||
experimentSettings.SHOW_NO_AVAILABILE_DAYS_MULTI_LOCATION
|
||||
);
|
||||
let showMultiLocationAppointment = experimentMixin.methods.getSettingValue(
|
||||
experimentSettings.SHOW_MULTI_LOCATION_APPT
|
||||
);
|
||||
let experimentSettingsString =
|
||||
"ShowPm:" +
|
||||
maxDayRangeToShowPmTimeslot +
|
||||
"|" +
|
||||
"NoPm:" +
|
||||
maxDayRangeToShowNoPmTimeslot +
|
||||
"|" +
|
||||
"NoAvail:" +
|
||||
maxDayRangeToShowAnyTimeslot;
|
||||
let promotedMobileAppointment = null;
|
||||
let promotedInshopAppointment = null;
|
||||
const todaysDate = getTodayDate();
|
||||
|
||||
// Helper function to calculate days until appointment (handles null/undefined)
|
||||
const calculateDaysUntilDate = (appointmentObj) => {
|
||||
const dateString = appointmentObj?.date;
|
||||
return dateString
|
||||
? (new Date(dateString) - todaysDate) / (1000 * 60 * 60 * 24)
|
||||
: null;
|
||||
};
|
||||
|
||||
// Fetch all appointments
|
||||
const [firstMobileAMAppt, firstMobilePMAppt, firstInshopAMAppt, firstInshopPMAppt] =
|
||||
await Promise.all([
|
||||
this.getFirstAvailableApptByTOD("AM", "mobile"),
|
||||
this.getFirstAvailableApptByTOD("PM", "mobile"),
|
||||
this.getFirstAvailableApptByTOD("AM", "inshop"),
|
||||
this.getFirstAvailableApptByTOD("PM", "inshop"),
|
||||
]);
|
||||
|
||||
// Extract dates
|
||||
const firstMobileAMDate = firstMobileAMAppt?.date;
|
||||
const firstMobilePMDate = firstMobilePMAppt?.date;
|
||||
const firstInshopAMDate = firstInshopAMAppt?.date;
|
||||
const firstInshopPMDate = firstInshopPMAppt?.date;
|
||||
|
||||
// Calculate days until appointments
|
||||
const numberOfDaysToFirstMobileAMDate = calculateDaysUntilDate(firstMobileAMAppt);
|
||||
const numberOfDaysToFirstMobilePMDate = calculateDaysUntilDate(firstMobilePMAppt);
|
||||
const numberOfDaysToFirstInshopAMDate = calculateDaysUntilDate(firstInshopAMAppt);
|
||||
const numberOfDaysToFirstInshopPMDate = calculateDaysUntilDate(firstInshopPMAppt);
|
||||
|
||||
const shouldExposeMultiLocationModal = () => {
|
||||
const isMobileAppointmentInDateRange =
|
||||
(firstMobileAMDate &&
|
||||
numberOfDaysToFirstMobileAMDate <= maxDayRangeToShowAnyTimeslot) ||
|
||||
(firstMobilePMDate &&
|
||||
numberOfDaysToFirstMobilePMDate <= maxDayRangeToShowAnyTimeslot);
|
||||
const isInshopAppointmentInDateRange =
|
||||
(firstInshopAMDate &&
|
||||
numberOfDaysToFirstInshopAMDate <= maxDayRangeToShowAnyTimeslot) ||
|
||||
(firstInshopPMDate &&
|
||||
numberOfDaysToFirstInshopPMDate <= maxDayRangeToShowAnyTimeslot);
|
||||
|
||||
if (showMultiLocationAppointment?.toLowerCase() === "true") {
|
||||
return isMobileAppointmentInDateRange && isInshopAppointmentInDateRange;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
if (shouldExposeMultiLocationModal()) {
|
||||
const multiLocationPopupExperiment =
|
||||
store.getters.applicationUser.experiments.find(
|
||||
(e) => e.universeName === experimentUniverses.MULTI_LOCATION_POPUP
|
||||
);
|
||||
const hasExposedMultiLocationPopup = multiLocationPopupExperiment?.isExposed;
|
||||
|
||||
if (!hasExposedMultiLocationPopup && multiLocationPopupExperiment) {
|
||||
baseMixin.methods.dispatchStoreActionWithLogging(
|
||||
storeActions.LOG_EXPERIMENT_EXPOSURE_AND_UPDATE_STORE,
|
||||
{
|
||||
userId: getUserIdValue(),
|
||||
deviceId: getDeviceIdValue(),
|
||||
sessionKey: getSessionKeyValue(),
|
||||
pageName: "schedule",
|
||||
experiment: multiLocationPopupExperiment,
|
||||
},
|
||||
"schedule",
|
||||
false
|
||||
);
|
||||
}
|
||||
if (this.isShowMultiLocationPopup) {
|
||||
if (maxDayRangeToShowPmTimeslot == 0) {
|
||||
// Selects the first available mobile time slot
|
||||
promotedMobileAppointment =
|
||||
numberOfDaysToFirstMobileAMDate <= numberOfDaysToFirstMobilePMDate
|
||||
? firstMobileAMAppt
|
||||
: firstMobilePMAppt;
|
||||
// Selects the first available inshop time slot
|
||||
promotedInshopAppointment =
|
||||
numberOfDaysToFirstInshopAMDate <= numberOfDaysToFirstInshopPMDate
|
||||
? firstInshopAMAppt
|
||||
: firstInshopPMAppt;
|
||||
} else {
|
||||
if (
|
||||
firstMobilePMDate &&
|
||||
numberOfDaysToFirstMobilePMDate <= maxDayRangeToShowPmTimeslot
|
||||
) {
|
||||
// Selects the first available PM time slot
|
||||
promotedMobileAppointment = firstMobilePMAppt;
|
||||
} else if (
|
||||
firstMobilePMDate &&
|
||||
numberOfDaysToFirstMobilePMDate >= maxDayRangeToShowNoPmTimeslot
|
||||
) {
|
||||
// Selects the first available AM time slot. If none, selects the first available PM time slot
|
||||
promotedMobileAppointment = firstMobileAMAppt
|
||||
? firstMobileAMAppt
|
||||
: firstMobilePMAppt;
|
||||
}
|
||||
|
||||
if (
|
||||
firstInshopPMDate &&
|
||||
numberOfDaysToFirstInshopPMDate <= maxDayRangeToShowPmTimeslot
|
||||
) {
|
||||
// Selects the first available PM time slot
|
||||
promotedInshopAppointment = firstInshopPMAppt;
|
||||
} else if (
|
||||
firstInshopPMDate &&
|
||||
numberOfDaysToFirstInshopPMDate >= maxDayRangeToShowNoPmTimeslot
|
||||
) {
|
||||
// Selects the first available AM time slot. If none, selects the first available PM time slot
|
||||
promotedInshopAppointment = firstInshopAMAppt
|
||||
? firstInshopAMAppt
|
||||
: firstInshopPMAppt;
|
||||
}
|
||||
}
|
||||
|
||||
if (promotedMobileAppointment && promotedInshopAppointment) {
|
||||
this.$refs.multiLocationModal.setPromotedMobileAppointment(
|
||||
promotedMobileAppointment
|
||||
);
|
||||
this.$refs.multiLocationModal.setPromotedInshopAppointment(
|
||||
promotedInshopAppointment
|
||||
);
|
||||
this.$refs.multiLocationModal.openModal();
|
||||
this.$refs.multiLocationModal.captureExperimentSettings(
|
||||
experimentSettingsString
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
async getFirstAvailableApptByTOD(timeOfDay, appointmentType) {
|
||||
let selectableDays;
|
||||
if (appointmentType?.toLowerCase() == "mobile") {
|
||||
selectableDays = this.selectableDatesMobile?.days;
|
||||
} else if (appointmentType?.toLowerCase() == "inshop") {
|
||||
selectableDays = this.selectableDatesInshop?.days;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
if (!selectableDays?.length) {
|
||||
return null;
|
||||
} else {
|
||||
for (const dateObj of this.selectableDatesMobile.days) {
|
||||
for (const dateObj of selectableDays) {
|
||||
let matchingTimeSlot = {
|
||||
estimatedServiceMinutes: {
|
||||
minimum: this.estimatedServiceMinutesMinimum,
|
||||
|
|
@ -2054,6 +2245,7 @@ export default {
|
|||
},
|
||||
timeSlot: null,
|
||||
date: null,
|
||||
addressCopy: null,
|
||||
};
|
||||
const isMatchingApptDay = dateObj.timeSlots.some(
|
||||
(slot) =>
|
||||
|
|
@ -2061,6 +2253,16 @@ export default {
|
|||
(matchingTimeSlot.timeSlot = slot) &&
|
||||
(matchingTimeSlot.date = dateObj.date)
|
||||
);
|
||||
if (
|
||||
appointmentType?.toLowerCase() == "inshop" &&
|
||||
this.selectedShopAnswer?.address1 &&
|
||||
this.selectedShopAnswer?.address2
|
||||
) {
|
||||
matchingTimeSlot.addressCopy =
|
||||
this.selectedShopAnswer?.address1 +
|
||||
", " +
|
||||
this.selectedShopAnswer?.address2;
|
||||
}
|
||||
if (isMatchingApptDay && matchingTimeSlot) {
|
||||
return matchingTimeSlot;
|
||||
}
|
||||
|
|
@ -2068,7 +2270,7 @@ export default {
|
|||
return null;
|
||||
}
|
||||
},
|
||||
updateTimeSlotandNavigateForward(timeSlotObj) {
|
||||
updateMobileFirstTimeSlotandNavigateForward(timeSlotObj) {
|
||||
this.selectedMobileFirstAppointment = true;
|
||||
this.appointmentType = AppointmentTypeStrings.MOBILE;
|
||||
this.selectedDate = timeSlotObj.date;
|
||||
|
|
@ -2076,6 +2278,18 @@ export default {
|
|||
this.updateTimeSlot(timeSlotObj);
|
||||
this.forwardButtonAction();
|
||||
},
|
||||
updateMultiLocationTimeSlotandNavigateForward({ selectedTimeSlot, appointmentType }) {
|
||||
if (appointmentType?.toLowerCase() == "mobile") {
|
||||
this.appointmentType = AppointmentTypeStrings.MOBILE;
|
||||
} else if (appointmentType?.toLowerCase() == "inshop") {
|
||||
this.appointmentType = AppointmentTypeStrings.IN_SHOP;
|
||||
}
|
||||
this.selectedMultiLocationAppointment = true;
|
||||
this.selectedDate = selectedTimeSlot.date;
|
||||
this.updateSelectedProvider();
|
||||
this.updateTimeSlot(selectedTimeSlot);
|
||||
this.forwardButtonAction();
|
||||
},
|
||||
updateRecalAcknowledgedAndNavigateForward(isAcknowledged) {
|
||||
this.isRecalAcknowledgedForScheduling = isAcknowledged;
|
||||
this.forwardButtonAction();
|
||||
|
|
@ -2084,7 +2298,7 @@ export default {
|
|||
watch: {
|
||||
appointmentTypeFromAppointmentTypeQuestion: {
|
||||
handler(newValue, oldValue) {
|
||||
this.handleAppointmentTypeChange(newValue); // v-if on appointmentTypeQuestion ensures dates have been loaded by now
|
||||
this.handleAppointmentTypeChange(newValue);
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -2099,6 +2313,7 @@ export default {
|
|||
textBlock,
|
||||
timeSlotQuestion,
|
||||
mobileFirstModal,
|
||||
multiLocationModal,
|
||||
recalAckModal,
|
||||
alert,
|
||||
serviceZipModalQuestion,
|
||||
|
|
|
|||
|
|
@ -200,6 +200,13 @@ export default {
|
|||
}
|
||||
}
|
||||
baseMixin.methods.ResetExternalParamsAndHideModal();
|
||||
|
||||
//clear any validation errors for external parameter flow
|
||||
const form = vm.$refs.theForm;
|
||||
if (form) {
|
||||
form.setFieldError("DamageLocationQuestion", undefined);
|
||||
form.setFieldTouched("DamageLocationQuestion", false, false);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
baseMixin.methods.ResetExternalParamsAndHideModal();
|
||||
|
|
|
|||
|
|
@ -291,6 +291,7 @@ export default {
|
|||
sessionData.isPia = order?.payment?.isPia ?? false;
|
||||
sessionData.piaType = order?.payment?.piaType;
|
||||
sessionData.parentAccountNumber = order?.payment?.parentAccountNumber;
|
||||
sessionData.billToAccountNumber = order?.payment?.billToAccountNumber;
|
||||
sessionData.settledTenderAmount = order?.settledTenderAmount;
|
||||
sessionData.recalRequired = containsRecalParts(order?.lineItems);
|
||||
sessionData.recalType = getRecalPartNumbers(order?.lineItems?.glassParts);
|
||||
|
|
|
|||
|
|
@ -218,6 +218,15 @@ export default {
|
|||
userAgent.includes("webOS")
|
||||
);
|
||||
},
|
||||
isAppleBrowser() {
|
||||
const userAgent = navigator.userAgent;
|
||||
return (
|
||||
userAgent.includes("iPod") ||
|
||||
userAgent.includes("iPad") ||
|
||||
userAgent.includes("iPhone") ||
|
||||
userAgent.includes("Mac")
|
||||
);
|
||||
},
|
||||
getSubmittedOrder() {
|
||||
return JSON.parse(
|
||||
window.sessionStorage.getItem(sessionStorageKeyConstants.SUBMITTED_STATE)
|
||||
|
|
|
|||
|
|
@ -15,11 +15,14 @@ export async function consumeReferralQuerystrings() {
|
|||
// If no referral number in querystring, check if heritage funnel updated last.
|
||||
// This would indicate a customer that went to heritage but did not return through the
|
||||
// normal route. ie: left the site in search of a discount and returned without querystrings.
|
||||
|
||||
//or if a referral number is in query string check if heritage funnel updated last
|
||||
// This would indicate a customer that returned through normal route like from sfa email
|
||||
const didHeritageFunnelUpdateLast = getFunnelCookie()?.DidHeritageFunnelUpdateLast;
|
||||
if (didHeritageFunnelUpdateLast && !referralNumber) {
|
||||
referralNumber = getFunnelCookie().ReferralNumber;
|
||||
parentAccount = getFunnelCookie().ReferralParentAccountNumber;
|
||||
correlationId = getFunnelCookie().ReferralCorrelationId;
|
||||
if (didHeritageFunnelUpdateLast) {
|
||||
referralNumber = referralNumber ?? getFunnelCookie().ReferralNumber;
|
||||
parentAccount = parentAccount ?? getFunnelCookie().ReferralParentAccountNumber;
|
||||
correlationId = correlationId ?? getFunnelCookie().ReferralCorrelationId;
|
||||
referralDate = getFunnelCookie().ReferralDate;
|
||||
savedSessionId = getFunnelCookie().SavedSessionId;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { getBoolFromString } from "@/helpers/boolean-helper";
|
|||
import { getFunnelCookie } from "@/helpers/heritage-integration/cookie-helper";
|
||||
import { routeData } from "@/router/constants/routes";
|
||||
import { isVerifiedInsurance } from "@/router/methods/helpers/requires-verified-redirect";
|
||||
import { hasVehicleInfo } from "@/helpers/vehicle-helper";
|
||||
import store from "@/store";
|
||||
import { queryStrings } from "@/constants/query-strings";
|
||||
import baseMixin from "@/mixins/base-mixin";
|
||||
|
|
@ -33,7 +34,7 @@ export async function vehicleBeforeEnter(to, from) {
|
|||
}
|
||||
}
|
||||
|
||||
if (isVerifiedInsurance()) {
|
||||
if (isVerifiedInsurance() && hasVehicleInfo()) {
|
||||
return {
|
||||
name: routeData.VEHICLE_DAMAGE.name,
|
||||
replace: true,
|
||||
|
|
|
|||
|
|
@ -1645,6 +1645,7 @@ export const actions = {
|
|||
isPia,
|
||||
piaType,
|
||||
parentAccountNumber,
|
||||
billToAccountNumber,
|
||||
settledTenderAmount,
|
||||
recalRequired,
|
||||
recalType,
|
||||
|
|
@ -1718,6 +1719,7 @@ export const actions = {
|
|||
totalPrice: totalPrice,
|
||||
userAgent: userAgent,
|
||||
cashPriceSubTotal: cashPriceSubTotal,
|
||||
billToAccountNumber: billToAccountNumber,
|
||||
};
|
||||
|
||||
return globalMethods.callHttpClient({
|
||||
|
|
@ -2012,52 +2014,27 @@ export const actions = {
|
|||
},
|
||||
|
||||
getServiceabilityDetails(context, { payload: { serviceZipCode }, pageNameToLog }) {
|
||||
const escapeRecalibrationType = (rt) => rt.split("&").join("%26");
|
||||
|
||||
const partialLineItemsObjects = context.getters.order.lineItems.glassParts?.map(
|
||||
(lineItem) => ({
|
||||
partNumber: lineItem.partNumber,
|
||||
recalibrationType:
|
||||
lineItem.recalibrationType && isRecalPartOrHasChildRecalPart(lineItem)
|
||||
? escapeRecalibrationType(lineItem.recalibrationType)
|
||||
: undefined,
|
||||
})
|
||||
const flattenedLineItemsWithChildParts = getFlattenedArrayOfLineItemsWithChildParts(
|
||||
context.getters.order.lineItems.glassParts
|
||||
);
|
||||
|
||||
var lineItems = null;
|
||||
if (partialLineItemsObjects) {
|
||||
lineItems = buildQueryStringParameterFromArrayOfComplexObjects(
|
||||
partialLineItemsObjects,
|
||||
"lineItems"
|
||||
);
|
||||
}
|
||||
const lineItemsList = flattenedLineItemsWithChildParts
|
||||
.map((item) => item.partNumber)
|
||||
.join(",");
|
||||
|
||||
const vehicle = context.getters.vehicle;
|
||||
const carId = vehicle.carId;
|
||||
const damage = context.getters.damage;
|
||||
const glassArray = convertGlassPieceNamingForApi(damage.glassToReplace);
|
||||
|
||||
const glassPieces = buildQueryStringParameterFromArrayOfComplexObjects(
|
||||
glassArray,
|
||||
"glassPieces"
|
||||
);
|
||||
|
||||
const parentAccountNumber =
|
||||
context.getters.order.payment.parentAccountNumber ??
|
||||
applicationConfig.CASH_PARENT_ACCOUNT_NUMBER;
|
||||
const referralSequenceNumber = context.getters.order.referralSequenceNumber;
|
||||
|
||||
var endPoint = `${endpoints.GetServiceabilityDetails.url}?zip=${serviceZipCode}&carId=${carId}&parentAccountNumber=${parentAccountNumber}&referralSequenceNumber=${referralSequenceNumber}&applicationName=${applicationConfig.ANALYTICS_APPLICATION_NAME}`;
|
||||
if (lineItems) {
|
||||
endPoint += `&${lineItems}`;
|
||||
var endPoint = `${endpoints.GetServiceabilityDetails.url}?zip=${serviceZipCode}&carId=${carId}&parentAccountNumber=${parentAccountNumber}&isRepair=${damage.isRepair}`;
|
||||
if (lineItemsList) {
|
||||
endPoint += `&lineItems=${lineItemsList}`;
|
||||
}
|
||||
|
||||
if (glassPieces) {
|
||||
endPoint += `&${glassPieces}`;
|
||||
}
|
||||
|
||||
endPoint += `&isHeavyTruckVehicle=${context.getters.order.vehicle.isBigTruck}`;
|
||||
|
||||
return globalMethods.callHttpClient({
|
||||
method: endpoints.GetServiceabilityDetails.method,
|
||||
endpoint: endPoint,
|
||||
|
|
@ -2126,14 +2103,14 @@ export const actions = {
|
|||
},
|
||||
async submitDonationPartToSV2(
|
||||
context,
|
||||
{ payload: { donationAmount, submittedOrder }, pageNameToLog }
|
||||
{ payload: { selectedItemNumber, submittedOrder }, pageNameToLog }
|
||||
) {
|
||||
const state = JSON.parse(
|
||||
window.sessionStorage.getItem(sessionStorageKeyConstants.SUBMITTED_STATE)
|
||||
);
|
||||
const order = state.order;
|
||||
const payload = {
|
||||
donationAmount: parseInt(donationAmount),
|
||||
selectedItemNumber: selectedItemNumber,
|
||||
savedSessionId: state.applicationUser.savedSessionId,
|
||||
paymentInfo: {
|
||||
parentAccountNumber: order.payment.parentAccountNumber,
|
||||
|
|
@ -3138,6 +3115,8 @@ export const actions = {
|
|||
serviceLocationState,
|
||||
serviceLocationZipCode,
|
||||
pricedLineItems,
|
||||
serviceLocationAddressLine1,
|
||||
serviceLocationAddressLine2,
|
||||
},
|
||||
pageNameToLog,
|
||||
}
|
||||
|
|
@ -3171,6 +3150,8 @@ export const actions = {
|
|||
appointmentType: appointmentType,
|
||||
pricedLineItems: lineItemsWithOnlyPriceInfo,
|
||||
serviceLocation: {
|
||||
addressLine1: appointmentType == "Mobile" ? serviceLocationAddressLine1 : null,
|
||||
addressLine2: appointmentType == "Mobile" ? serviceLocationAddressLine2 : null,
|
||||
city: appointmentType == "Mobile" ? serviceLocationCity : null,
|
||||
state: appointmentType == "Mobile" ? serviceLocationState : null,
|
||||
zipCode: appointmentType == "Mobile" ? serviceLocationZipCode : null,
|
||||
|
|
|
|||
|
|
@ -20,3 +20,9 @@
|
|||
}
|
||||
// Usage: @include responsive-font-size-md(mobile-font-size-in-rem, desktop-font-size-in-rem);
|
||||
// Example: @include responsive-font-size-md(0.875rem, 1rem);
|
||||
|
||||
// Filter to use on SVG elements that are background
|
||||
@mixin blue-filter {
|
||||
filter: brightness(0) saturate(100%) invert(31%) sepia(68%) saturate(1676%) hue-rotate(186deg)
|
||||
brightness(93%) contrast(110%);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -127,6 +127,12 @@ $body-color: $gray-600;
|
|||
$font-family-sans-serif: UrbanistRegular, Arial, Helvetica, sans-serif;
|
||||
$font-family-monospace:
|
||||
UrbanistRegular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
$font-family-sans-serif-semibold: UrbanistSemibold, Arial, Helvetica, sans-serif;
|
||||
$font-family-sans-serif-bold: UrbanistBold, Arial, Helvetica, sans-serif;
|
||||
$font-family-sans-serif-extrabold: UrbanistExtraBold, Arial, Helvetica, sans-serif;
|
||||
$font-family-sans-serif-medium-italic: UrbanistMedium_Italic, Arial, Helvetica, sans-serif;
|
||||
$font-family-sans-serif-bold-italic: UrbanistBold_Italic, Arial, Helvetica, sans-serif;
|
||||
|
||||
// stylelint-enable value-keyword-case
|
||||
$font-family-base: $font-family-sans-serif;
|
||||
$font-family-code: $font-family-monospace;
|
||||
|
|
|
|||
|
|
@ -8,7 +8,11 @@
|
|||
this.alertClass,
|
||||
this.cssClassNameForCmsWidget,
|
||||
]">
|
||||
<p class="mx-6 my-0 fw-bold alert-heading">{{ alertHeadline }}</p>
|
||||
<p class="mx-6 my-0 fw-bold alert-heading">
|
||||
<img v-if="showInfoIcon" :src="infoIcon" alt="Info Icon" class="info-icon" />
|
||||
{{ alertHeadline }}
|
||||
</p>
|
||||
<hr v-if="showHorizontalRow" />
|
||||
<template v-for="paragraph in splitAlertCopyForParagraphTag" :key="paragraph">
|
||||
<p
|
||||
class="mx-4 mt-1 mb-0 text-body small"
|
||||
|
|
@ -58,6 +62,8 @@ import textBlock from "@/digital-components/text-block/text-block";
|
|||
export default {
|
||||
name: "alert",
|
||||
props: {
|
||||
showInfoIcon: Boolean,
|
||||
showHorizontalRow: Boolean,
|
||||
isDismissible: Boolean,
|
||||
alertClass: String,
|
||||
/*
|
||||
|
|
@ -87,6 +93,9 @@ export default {
|
|||
},
|
||||
},
|
||||
computed: {
|
||||
infoIcon() {
|
||||
return require(`@/assets/img/icons/info-circle-blue.svg`);
|
||||
},
|
||||
pageQueryString() {
|
||||
return applicationConfig.PAGE_QUERYSTRING;
|
||||
},
|
||||
|
|
@ -148,6 +157,12 @@ export default {
|
|||
border-radius: 0.5rem;
|
||||
border-width: 0px;
|
||||
|
||||
hr {
|
||||
border: 1px solid $blue;
|
||||
margin-left: 1rem;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
button {
|
||||
display: none;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue