Merge pull request #3088 from Safelite/rlsmerge/2026.03.12-to-dev
Rlsmerge/2026.03.12 to dev
This commit is contained in:
commit
9731526eb1
18 changed files with 1077 additions and 96 deletions
|
|
@ -1,3 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="21" viewBox="0 0 20 21">
|
|
||||||
<path fill="#E86421" fill-rule="nonzero" d="M10 .5c5.523 0 10 4.477 10 10s-4.477 10-10 10-10-4.477-10-10S4.477.5 10 .5zm.043 13.6a1 1 0 1 0 0 2 1 1 0 0 0 0-2zm.77-9.2h-1.54l-.088.009a.502.502 0 0 0-.299.193.66.66 0 0 0-.125.47l.747 6.806.017.096c.065.249.263.425.495.426l.084-.008c.22-.042.396-.246.427-.51l.793-6.805.004-.102a.651.651 0 0 0-.127-.37.489.489 0 0 0-.388-.205z"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 474 B |
|
|
@ -15,6 +15,8 @@ const GaCategories = {
|
||||||
APPOINTMENT: "Appointment",
|
APPOINTMENT: "Appointment",
|
||||||
SERVICE_LOCATION: "service-location",
|
SERVICE_LOCATION: "service-location",
|
||||||
CONFIRMATION_CLICKED: "confirmation clicked",
|
CONFIRMATION_CLICKED: "confirmation clicked",
|
||||||
|
INSHOP_CONFIRMATION_CLICKED: "inshop confirmation clicked",
|
||||||
|
MOBILE_CONFIRMATION_CLICKED: "mobile confirmation clicked",
|
||||||
RECAL_DISCLAIMER: "Recalibration disclaimer",
|
RECAL_DISCLAIMER: "Recalibration disclaimer",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ const experimentUniverses = {
|
||||||
IGQ_SkipQuote: "NextGen_IGQSkipToInsurance",
|
IGQ_SkipQuote: "NextGen_IGQSkipToInsurance",
|
||||||
AFTERPAY_BREAKOUT_DISPLAY: "AfterpayBreakoutDisplay",
|
AFTERPAY_BREAKOUT_DISPLAY: "AfterpayBreakoutDisplay",
|
||||||
MOBILE_FIRST_APPOINTMENT: "MobileFirstAppointment",
|
MOBILE_FIRST_APPOINTMENT: "MobileFirstAppointment",
|
||||||
|
MULTI_LOCATION_POPUP: "MultiLocationPopup",
|
||||||
};
|
};
|
||||||
|
|
||||||
const experimentSettings = {
|
const experimentSettings = {
|
||||||
|
|
@ -40,6 +41,10 @@ const experimentSettings = {
|
||||||
SHOW_MOBILE_FIRST_POPUP_REPLACE_WITHOUT_MSR: "Show_Mobile_First_Popup_Replace_Without_MSR",
|
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",
|
SHOW_MOBILE_FIRST_POPUP_REPLACE_WITH_MSR: "Show_Mobile_First_Popup_Replace_With_MSR",
|
||||||
OFFER_OEM: "OfferOem",
|
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",
|
USE_ADYEN_PAYMENT: "UseAdyenPayment",
|
||||||
SHOW_UNVERIFIED_DEDUCT_ENTRY: "ShowUnverifiedDeducEntry",
|
SHOW_UNVERIFIED_DEDUCT_ENTRY: "ShowUnverifiedDeducEntry",
|
||||||
SERVICE_PACKAGE_ORDER: "ServicePackageOrder",
|
SERVICE_PACKAGE_ORDER: "ServicePackageOrder",
|
||||||
|
|
|
||||||
|
|
@ -226,6 +226,7 @@ export default {
|
||||||
let classes;
|
let classes;
|
||||||
switch (this.buttonTypeString) {
|
switch (this.buttonTypeString) {
|
||||||
case "timeSlotModalListButton":
|
case "timeSlotModalListButton":
|
||||||
|
case "multiLocationRadioButton":
|
||||||
case "listButton":
|
case "listButton":
|
||||||
classes = "w-100";
|
classes = "w-100";
|
||||||
break;
|
break;
|
||||||
|
|
@ -265,6 +266,8 @@ export default {
|
||||||
classes += " radio-button-container";
|
classes += " radio-button-container";
|
||||||
} else if (this.buttonTypeString == "servicePackageRadio") {
|
} else if (this.buttonTypeString == "servicePackageRadio") {
|
||||||
classes = "package-wrapper col";
|
classes = "package-wrapper col";
|
||||||
|
} else if (this.buttonTypeString == "multiLocationRadioButton") {
|
||||||
|
classes = "multi-location-wrapper col";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.buttonTypeString == "listCard" && this.buttonsInfo.length > 2) {
|
if (this.buttonTypeString == "listCard" && this.buttonsInfo.length > 2) {
|
||||||
|
|
|
||||||
|
|
@ -317,6 +317,10 @@ export default {
|
||||||
serviceLocationZipCode:
|
serviceLocationZipCode:
|
||||||
store.getters.order.serviceLocation.zipCode,
|
store.getters.order.serviceLocation.zipCode,
|
||||||
pricedLineItems: pricedLineItemsToTax,
|
pricedLineItems: pricedLineItemsToTax,
|
||||||
|
serviceLocationAddressLine1:
|
||||||
|
store.getters.order.serviceLocation?.address,
|
||||||
|
serviceLocationAddressLine2:
|
||||||
|
store.getters.order.serviceLocation?.address2,
|
||||||
},
|
},
|
||||||
"payment-method",
|
"payment-method",
|
||||||
false
|
false
|
||||||
|
|
|
||||||
|
|
@ -343,14 +343,3 @@ export function splitCMSCopyOnParagraphTag(copy) {
|
||||||
export function splitCMSCopyOnBR(copy) {
|
export function splitCMSCopyOnBR(copy) {
|
||||||
return copy.split("<br>");
|
return copy.split("<br>");
|
||||||
}
|
}
|
||||||
|
|
||||||
// This function takes in a number and returns it formatted as USD currency, with or without cents depending on if the number is an integer or not.
|
|
||||||
export function formatToUSDollar(amount) {
|
|
||||||
const isInteger = amount % 1 === 0;
|
|
||||||
return new Intl.NumberFormat("en-US", {
|
|
||||||
style: "currency",
|
|
||||||
currency: "USD",
|
|
||||||
minimumFractionDigits: isInteger ? 0 : 2,
|
|
||||||
maximumFractionDigits: 2,
|
|
||||||
}).format(amount);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -322,6 +322,8 @@ export default {
|
||||||
serviceLocationState: store.getters.order.serviceLocation.state,
|
serviceLocationState: store.getters.order.serviceLocation.state,
|
||||||
serviceLocationZipCode: store.getters.order.serviceLocation.zipCode,
|
serviceLocationZipCode: store.getters.order.serviceLocation.zipCode,
|
||||||
pricedLineItems: lineItemsForCart,
|
pricedLineItems: lineItemsForCart,
|
||||||
|
serviceLocationAddressLine1: store.getters.order.serviceLocation?.address,
|
||||||
|
serviceLocationAddressLine2: store.getters.order.serviceLocation?.address2,
|
||||||
},
|
},
|
||||||
"payment-method",
|
"payment-method",
|
||||||
false
|
false
|
||||||
|
|
@ -526,6 +528,10 @@ export default {
|
||||||
serviceLocationState: this.$store.getters.order.serviceLocation.state,
|
serviceLocationState: this.$store.getters.order.serviceLocation.state,
|
||||||
serviceLocationZipCode: this.$store.getters.order.serviceLocation.zipCode,
|
serviceLocationZipCode: this.$store.getters.order.serviceLocation.zipCode,
|
||||||
pricedLineItems: this.lineItems,
|
pricedLineItems: this.lineItems,
|
||||||
|
serviceLocationAddressLine1:
|
||||||
|
this.$store.getters.order.serviceLocation?.address,
|
||||||
|
serviceLocationAddressLine2:
|
||||||
|
this.$store.getters.order.serviceLocation?.address2,
|
||||||
},
|
},
|
||||||
"payment-method",
|
"payment-method",
|
||||||
false
|
false
|
||||||
|
|
@ -650,6 +656,8 @@ export default {
|
||||||
serviceLocationState: store.getters.order.serviceLocation.state,
|
serviceLocationState: store.getters.order.serviceLocation.state,
|
||||||
serviceLocationZipCode: store.getters.order.serviceLocation.zipCode,
|
serviceLocationZipCode: store.getters.order.serviceLocation.zipCode,
|
||||||
pricedLineItems: this.lineItems,
|
pricedLineItems: this.lineItems,
|
||||||
|
serviceLocationAddressLine1: store.getters.order.serviceLocation?.address,
|
||||||
|
serviceLocationAddressLine2: store.getters.order.serviceLocation?.address2,
|
||||||
},
|
},
|
||||||
"payment-method",
|
"payment-method",
|
||||||
false
|
false
|
||||||
|
|
|
||||||
|
|
@ -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
|
<mobileFirstModal
|
||||||
modalWidgetName="MobileFirstModalWidget"
|
modalWidgetName="MobileFirstModalWidget"
|
||||||
ref="mobileFirstModal"
|
ref="mobileFirstModal"
|
||||||
@confirm-appointment="updateTimeSlotandNavigateForward" />
|
@confirm-appointment="updateMobileFirstTimeSlotandNavigateForward" />
|
||||||
|
<multiLocationModal
|
||||||
|
modalWidgetName="MultiLocationModalWidget"
|
||||||
|
buttonWidgetName="MultiLocationRadioButtonWidget"
|
||||||
|
ref="multiLocationModal"
|
||||||
|
@confirm-appointment="updateMultiLocationTimeSlotandNavigateForward" />
|
||||||
<recalAckModal
|
<recalAckModal
|
||||||
modalWidgetName="RecalAckModalWidget"
|
modalWidgetName="RecalAckModalWidget"
|
||||||
agreementWidgetName="RecalAgreementQuestionWidget"
|
agreementWidgetName="RecalAgreementQuestionWidget"
|
||||||
|
|
@ -14,7 +19,9 @@
|
||||||
ref="recalAckModal" />
|
ref="recalAckModal" />
|
||||||
<div
|
<div
|
||||||
class="container page-container-grouped-styles"
|
class="container page-container-grouped-styles"
|
||||||
:class="[isMobileFirstModalOpen ? 'hidden-background' : '']">
|
:class="[
|
||||||
|
isMobileFirstModalOpen || isMultiLocationModalOpen ? 'hidden-background' : '',
|
||||||
|
]">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 col-md-10 col-lg-8 col-xl-7">
|
<div class="col-12 col-md-10 col-lg-8 col-xl-7">
|
||||||
<mobileFeeWaiverAlert
|
<mobileFeeWaiverAlert
|
||||||
|
|
@ -74,16 +81,6 @@
|
||||||
cmsWidgetName="AlertNoShopsWidget"
|
cmsWidgetName="AlertNoShopsWidget"
|
||||||
v-if="displayNoShopsAlert"
|
v-if="displayNoShopsAlert"
|
||||||
alertClass="alert-warning" />
|
alertClass="alert-warning" />
|
||||||
<alert
|
|
||||||
ref="alertMSRNotCoveredByInsurance"
|
|
||||||
class="mt-5 mb-5"
|
|
||||||
v-if="isMSRFeeNotCoveredByInsurance"
|
|
||||||
:manualHeadline="MSRFeeAlertHeadlineText"
|
|
||||||
:manualCopy="MSRFeeAlertBodyText"
|
|
||||||
:showWarningIcon="true"
|
|
||||||
:hasBorder="true"
|
|
||||||
:showHorizontalRow="true"
|
|
||||||
alertClass="alert-warning" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|
@ -201,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 timeSlotQuestion from "@/layouts/schedule/time-slot-question/time-slot-question.vue";
|
||||||
import durationTextBlock from "@/layouts/schedule/duration-text-block/duration-text-block.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 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 recalAckModal from "@/layouts/schedule/recal-ack-modal/recal-ack-modal.vue";
|
||||||
|
|
||||||
import shopQuestion from "@/layouts/service-location/shop-question/shop-question";
|
import shopQuestion from "@/layouts/service-location/shop-question/shop-question";
|
||||||
|
|
@ -231,11 +229,7 @@ import {
|
||||||
RECAL_ACK_YES,
|
RECAL_ACK_YES,
|
||||||
} from "@/constants/schedule-constants";
|
} from "@/constants/schedule-constants";
|
||||||
|
|
||||||
import {
|
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
|
||||||
fetchCmsContentForPage,
|
|
||||||
splitCopyOnCMSPlaceHolder,
|
|
||||||
formatToUSDollar,
|
|
||||||
} from "@/helpers/cms-content-helper";
|
|
||||||
import { settleAllPromises } from "@/helpers/layout-helper";
|
import { settleAllPromises } from "@/helpers/layout-helper";
|
||||||
import {
|
import {
|
||||||
getPricedMobileFeePart,
|
getPricedMobileFeePart,
|
||||||
|
|
@ -251,6 +245,7 @@ import { partNumberStrings } from "@/constants/part-number-strings";
|
||||||
import store from "@/store";
|
import store from "@/store";
|
||||||
|
|
||||||
import { storeActions } from "@/constants/store-actions";
|
import { storeActions } from "@/constants/store-actions";
|
||||||
|
import { splitCopyOnCMSPlaceHolder } from "@/helpers/cms-content-helper";
|
||||||
import {
|
import {
|
||||||
calcDaysBetweenDates,
|
calcDaysBetweenDates,
|
||||||
convertDateStringToDate,
|
convertDateStringToDate,
|
||||||
|
|
@ -488,10 +483,7 @@ export default {
|
||||||
shopListButton: shopListButton,
|
shopListButton: shopListButton,
|
||||||
lastSelectedInshopOrDropoffProvider: null,
|
lastSelectedInshopOrDropoffProvider: null,
|
||||||
selectedMobileFirstAppointment: false,
|
selectedMobileFirstAppointment: false,
|
||||||
isShowMobileFirstAppt: experimentMixin.methods.hasSettingEqualTo(
|
selectedMultiLocationAppointment: false,
|
||||||
experimentSettings.SHOW_MOBILE_FIRST_APPT,
|
|
||||||
"true"
|
|
||||||
),
|
|
||||||
calendarLoadingStatus: "none",
|
calendarLoadingStatus: "none",
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
@ -624,7 +616,8 @@ export default {
|
||||||
shopProviderData.data
|
shopProviderData.data
|
||||||
);
|
);
|
||||||
vm.initializeDatePicker().then(() => {
|
vm.initializeDatePicker().then(() => {
|
||||||
vm.showMobileFirstModal();
|
// vm.showMobileFirstModal(); // KEEP IN CASE WE WANT TO REINSTATE MOBILE FIRST
|
||||||
|
vm.showMultiLocationModal();
|
||||||
vm.hideLoadingModal();
|
vm.hideLoadingModal();
|
||||||
vm.calendarLoadingStatus = "none";
|
vm.calendarLoadingStatus = "none";
|
||||||
if (!vm.preSelectedDate) vm.setSelectedDateToFirstAvailable();
|
if (!vm.preSelectedDate) vm.setSelectedDateToFirstAvailable();
|
||||||
|
|
@ -690,7 +683,8 @@ export default {
|
||||||
isMobileStaticRecalibrationApplicable() {
|
isMobileStaticRecalibrationApplicable() {
|
||||||
return (
|
return (
|
||||||
this.displayMSR &&
|
this.displayMSR &&
|
||||||
this.mobileFeePart?.partNumber == partNumberStrings.MOBILE_STATIC_RECAL_FEE
|
this.mobileFeePart?.partNumber == partNumberStrings.MOBILE_STATIC_RECAL_FEE &&
|
||||||
|
(this.isCashItacNoComp || this.mobileFeePart?.isInsurable)
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
displayMSR() {
|
displayMSR() {
|
||||||
|
|
@ -700,28 +694,6 @@ export default {
|
||||||
?.toLowerCase() === "true"
|
?.toLowerCase() === "true"
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
isMSRFeeNotCoveredByInsurance() {
|
|
||||||
return (
|
|
||||||
this.isMobileSelected &&
|
|
||||||
this.isMobileStaticRecalibrationApplicable &&
|
|
||||||
this.mobileFeeHasPrice &&
|
|
||||||
this.isInsurance &&
|
|
||||||
!this.mobileFeePart?.isInsurable
|
|
||||||
);
|
|
||||||
},
|
|
||||||
MSRFeeAlertHeadlineText() {
|
|
||||||
return this.getCmsContent("AlertMSRNotCoveredByInsuranceWidget", "HeadlineText");
|
|
||||||
},
|
|
||||||
MSRFeeAlertBodyText() {
|
|
||||||
const cmsContentText = this.getCmsContent(
|
|
||||||
"AlertMSRNotCoveredByInsuranceWidget",
|
|
||||||
"BodyText"
|
|
||||||
);
|
|
||||||
return cmsContentText.replaceAll(
|
|
||||||
"{custom:mobileFee}",
|
|
||||||
formatToUSDollar(this.mobileFee)
|
|
||||||
);
|
|
||||||
},
|
|
||||||
isCashItacNoComp() {
|
isCashItacNoComp() {
|
||||||
return !this.isInsurance || this.isITAC || this.isNoComp;
|
return !this.isInsurance || this.isITAC || this.isNoComp;
|
||||||
},
|
},
|
||||||
|
|
@ -934,6 +906,11 @@ export default {
|
||||||
? this.$refs.mobileFirstModal?.getIsModalOpen()
|
? this.$refs.mobileFirstModal?.getIsModalOpen()
|
||||||
: false;
|
: false;
|
||||||
},
|
},
|
||||||
|
isMultiLocationModalOpen() {
|
||||||
|
return this.selectableDatesMobile.days && this.selectableDatesInshop.days
|
||||||
|
? this.$refs.multiLocationModal?.getIsModalOpen()
|
||||||
|
: false;
|
||||||
|
},
|
||||||
displayWaitList() {
|
displayWaitList() {
|
||||||
if (!this.appointmentType) return false;
|
if (!this.appointmentType) return false;
|
||||||
const firstMobileDateString =
|
const firstMobileDateString =
|
||||||
|
|
@ -988,6 +965,18 @@ export default {
|
||||||
return this.selectableDatesInshop?.days?.length > 0;
|
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: {
|
methods: {
|
||||||
splitCopyOnCMSPlaceHolder,
|
splitCopyOnCMSPlaceHolder,
|
||||||
|
|
@ -1273,7 +1262,8 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.initializeDatePicker().then(() => {
|
this.initializeDatePicker().then(() => {
|
||||||
this.showMobileFirstModal();
|
// this.showMobileFirstModal(); // KEEP IN CASE WE WANT TO REINSTATE MOBILE FIRST
|
||||||
|
this.showMultiLocationModal();
|
||||||
this.hideLoadingModal();
|
this.hideLoadingModal();
|
||||||
this.calendarLoadingStatus = "none";
|
this.calendarLoadingStatus = "none";
|
||||||
this.setSelectedDateToFirstAvailable();
|
this.setSelectedDateToFirstAvailable();
|
||||||
|
|
@ -1307,7 +1297,7 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
async initializeDatePicker() {
|
async initializeDatePicker() {
|
||||||
this.isShowMobileFirstAppt && this.showLoadingModal();
|
this.isShowMultiLocationPopup && this.showLoadingModal();
|
||||||
|
|
||||||
const includeMobileTimeSlots = this.isServiceableMobile;
|
const includeMobileTimeSlots = this.isServiceableMobile;
|
||||||
const includeInshopTimeSlots = this.isServiceableInshop || this.isServiceableDropoff;
|
const includeInshopTimeSlots = this.isServiceableInshop || this.isServiceableDropoff;
|
||||||
|
|
@ -1819,7 +1809,8 @@ export default {
|
||||||
this.zipCodeCtu = newZipCode.zipCodeCtu;
|
this.zipCodeCtu = newZipCode.zipCodeCtu;
|
||||||
this.selectedDate = null;
|
this.selectedDate = null;
|
||||||
this.initializeDatePicker().then(() => {
|
this.initializeDatePicker().then(() => {
|
||||||
this.showMobileFirstModal();
|
// this.showMobileFirstModal(); // KEEP IN CASE WE WANT TO REINSTATE MOBILE FIRST
|
||||||
|
this.showMultiLocationModal();
|
||||||
this.hideLoadingModal();
|
this.hideLoadingModal();
|
||||||
this.calendarLoadingStatus = "none";
|
this.calendarLoadingStatus = "none";
|
||||||
this.setSelectedDateToFirstAvailable();
|
this.setSelectedDateToFirstAvailable();
|
||||||
|
|
@ -1934,10 +1925,10 @@ export default {
|
||||||
const order = this.$store.getters.order;
|
const order = this.$store.getters.order;
|
||||||
const isRepair = order.damage.isRepair;
|
const isRepair = order.damage.isRepair;
|
||||||
|
|
||||||
let preSelectedMobileAppointment = null;
|
let promotedMobileAppointment = null;
|
||||||
const todaysDate = getTodayDate();
|
const todaysDate = getTodayDate();
|
||||||
let firstMobileAMAppt = await this.getFirstAvailableMobileApptByTOD("AM");
|
let firstMobileAMAppt = await this.getFirstAvailableApptByTOD("AM", "mobile");
|
||||||
let firstMobilePMAppt = await this.getFirstAvailableMobileApptByTOD("PM");
|
let firstMobilePMAppt = await this.getFirstAvailableApptByTOD("PM", "mobile");
|
||||||
if (!firstMobileAMAppt && !firstMobilePMAppt) {
|
if (!firstMobileAMAppt && !firstMobilePMAppt) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -2032,7 +2023,7 @@ export default {
|
||||||
if (this.isShowMobileFirstAppt) {
|
if (this.isShowMobileFirstAppt) {
|
||||||
if (pmMobileDays == 0) {
|
if (pmMobileDays == 0) {
|
||||||
// Selects the first available time slot
|
// Selects the first available time slot
|
||||||
preSelectedMobileAppointment =
|
promotedMobileAppointment =
|
||||||
numberOfDaysToFirstMobileAMDate <= numberOfDaysToFirstMobilePMDate
|
numberOfDaysToFirstMobileAMDate <= numberOfDaysToFirstMobilePMDate
|
||||||
? firstMobileAMAppt
|
? firstMobileAMAppt
|
||||||
: firstMobilePMAppt;
|
: firstMobilePMAppt;
|
||||||
|
|
@ -2041,20 +2032,20 @@ export default {
|
||||||
numberOfDaysToFirstMobilePMDate <= pmMobileDays
|
numberOfDaysToFirstMobilePMDate <= pmMobileDays
|
||||||
) {
|
) {
|
||||||
// Selects the first available PM time slot
|
// Selects the first available PM time slot
|
||||||
preSelectedMobileAppointment = firstMobilePMAppt;
|
promotedMobileAppointment = firstMobilePMAppt;
|
||||||
} else if (
|
} else if (
|
||||||
firstMobilePMDate &&
|
firstMobilePMDate &&
|
||||||
numberOfDaysToFirstMobilePMDate >= noPMMobileDays
|
numberOfDaysToFirstMobilePMDate >= noPMMobileDays
|
||||||
) {
|
) {
|
||||||
// Selects the first available AM time slot. If none, selects the first available PM time slot
|
// Selects the first available AM time slot. If none, selects the first available PM time slot
|
||||||
preSelectedMobileAppointment = firstMobileAMAppt
|
promotedMobileAppointment = firstMobileAMAppt
|
||||||
? firstMobileAMAppt
|
? firstMobileAMAppt
|
||||||
: firstMobilePMAppt;
|
: firstMobilePMAppt;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (preSelectedMobileAppointment) {
|
if (promotedMobileAppointment) {
|
||||||
this.$refs.mobileFirstModal.setSelectedAppointment(
|
this.$refs.mobileFirstModal.setSelectedAppointment(
|
||||||
preSelectedMobileAppointment
|
promotedMobileAppointment
|
||||||
);
|
);
|
||||||
this.$refs.mobileFirstModal.openModal();
|
this.$refs.mobileFirstModal.openModal();
|
||||||
}
|
}
|
||||||
|
|
@ -2062,11 +2053,176 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async getFirstAvailableMobileApptByTOD(timeOfDay) {
|
async showMultiLocationModal() {
|
||||||
if (!this.selectableDatesMobile?.days?.length) {
|
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;
|
return null;
|
||||||
} else {
|
} else {
|
||||||
for (const dateObj of this.selectableDatesMobile.days) {
|
for (const dateObj of selectableDays) {
|
||||||
let matchingTimeSlot = {
|
let matchingTimeSlot = {
|
||||||
estimatedServiceMinutes: {
|
estimatedServiceMinutes: {
|
||||||
minimum: this.estimatedServiceMinutesMinimum,
|
minimum: this.estimatedServiceMinutesMinimum,
|
||||||
|
|
@ -2074,6 +2230,7 @@ export default {
|
||||||
},
|
},
|
||||||
timeSlot: null,
|
timeSlot: null,
|
||||||
date: null,
|
date: null,
|
||||||
|
addressCopy: null,
|
||||||
};
|
};
|
||||||
const isMatchingApptDay = dateObj.timeSlots.some(
|
const isMatchingApptDay = dateObj.timeSlots.some(
|
||||||
(slot) =>
|
(slot) =>
|
||||||
|
|
@ -2081,6 +2238,16 @@ export default {
|
||||||
(matchingTimeSlot.timeSlot = slot) &&
|
(matchingTimeSlot.timeSlot = slot) &&
|
||||||
(matchingTimeSlot.date = dateObj.date)
|
(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) {
|
if (isMatchingApptDay && matchingTimeSlot) {
|
||||||
return matchingTimeSlot;
|
return matchingTimeSlot;
|
||||||
}
|
}
|
||||||
|
|
@ -2088,7 +2255,7 @@ export default {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
updateTimeSlotandNavigateForward(timeSlotObj) {
|
updateMobileFirstTimeSlotandNavigateForward(timeSlotObj) {
|
||||||
this.selectedMobileFirstAppointment = true;
|
this.selectedMobileFirstAppointment = true;
|
||||||
this.appointmentType = AppointmentTypeStrings.MOBILE;
|
this.appointmentType = AppointmentTypeStrings.MOBILE;
|
||||||
this.selectedDate = timeSlotObj.date;
|
this.selectedDate = timeSlotObj.date;
|
||||||
|
|
@ -2096,6 +2263,18 @@ export default {
|
||||||
this.updateTimeSlot(timeSlotObj);
|
this.updateTimeSlot(timeSlotObj);
|
||||||
this.forwardButtonAction();
|
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) {
|
updateRecalAcknowledgedAndNavigateForward(isAcknowledged) {
|
||||||
this.isRecalAcknowledgedForScheduling = isAcknowledged;
|
this.isRecalAcknowledgedForScheduling = isAcknowledged;
|
||||||
this.forwardButtonAction();
|
this.forwardButtonAction();
|
||||||
|
|
@ -2119,6 +2298,7 @@ export default {
|
||||||
textBlock,
|
textBlock,
|
||||||
timeSlotQuestion,
|
timeSlotQuestion,
|
||||||
mobileFirstModal,
|
mobileFirstModal,
|
||||||
|
multiLocationModal,
|
||||||
recalAckModal,
|
recalAckModal,
|
||||||
alert,
|
alert,
|
||||||
serviceZipModalQuestion,
|
serviceZipModalQuestion,
|
||||||
|
|
|
||||||
|
|
@ -209,6 +209,15 @@ export default {
|
||||||
userAgent.includes("webOS")
|
userAgent.includes("webOS")
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
isAppleBrowser() {
|
||||||
|
const userAgent = navigator.userAgent;
|
||||||
|
return (
|
||||||
|
userAgent.includes("iPod") ||
|
||||||
|
userAgent.includes("iPad") ||
|
||||||
|
userAgent.includes("iPhone") ||
|
||||||
|
userAgent.includes("Mac")
|
||||||
|
);
|
||||||
|
},
|
||||||
getSubmittedOrder() {
|
getSubmittedOrder() {
|
||||||
return JSON.parse(
|
return JSON.parse(
|
||||||
window.sessionStorage.getItem(sessionStorageKeyConstants.SUBMITTED_STATE)
|
window.sessionStorage.getItem(sessionStorageKeyConstants.SUBMITTED_STATE)
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { getBoolFromString } from "@/helpers/boolean-helper";
|
||||||
import { getFunnelCookie } from "@/helpers/heritage-integration/cookie-helper";
|
import { getFunnelCookie } from "@/helpers/heritage-integration/cookie-helper";
|
||||||
import { routeData } from "@/router/constants/routes";
|
import { routeData } from "@/router/constants/routes";
|
||||||
import { isVerifiedInsurance } from "@/router/methods/helpers/requires-verified-redirect";
|
import { isVerifiedInsurance } from "@/router/methods/helpers/requires-verified-redirect";
|
||||||
|
import { hasVehicleInfo } from "@/helpers/vehicle-helper";
|
||||||
import store from "@/store";
|
import store from "@/store";
|
||||||
import { queryStrings } from "@/constants/query-strings";
|
import { queryStrings } from "@/constants/query-strings";
|
||||||
import baseMixin from "@/mixins/base-mixin";
|
import baseMixin from "@/mixins/base-mixin";
|
||||||
|
|
@ -33,7 +34,7 @@ export async function vehicleBeforeEnter(to, from) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isVerifiedInsurance()) {
|
if (isVerifiedInsurance() && hasVehicleInfo()) {
|
||||||
return {
|
return {
|
||||||
name: routeData.VEHICLE_DAMAGE.name,
|
name: routeData.VEHICLE_DAMAGE.name,
|
||||||
replace: true,
|
replace: true,
|
||||||
|
|
|
||||||
|
|
@ -3115,6 +3115,8 @@ export const actions = {
|
||||||
serviceLocationState,
|
serviceLocationState,
|
||||||
serviceLocationZipCode,
|
serviceLocationZipCode,
|
||||||
pricedLineItems,
|
pricedLineItems,
|
||||||
|
serviceLocationAddressLine1,
|
||||||
|
serviceLocationAddressLine2,
|
||||||
},
|
},
|
||||||
pageNameToLog,
|
pageNameToLog,
|
||||||
}
|
}
|
||||||
|
|
@ -3148,6 +3150,8 @@ export const actions = {
|
||||||
appointmentType: appointmentType,
|
appointmentType: appointmentType,
|
||||||
pricedLineItems: lineItemsWithOnlyPriceInfo,
|
pricedLineItems: lineItemsWithOnlyPriceInfo,
|
||||||
serviceLocation: {
|
serviceLocation: {
|
||||||
|
addressLine1: appointmentType == "Mobile" ? serviceLocationAddressLine1 : null,
|
||||||
|
addressLine2: appointmentType == "Mobile" ? serviceLocationAddressLine2 : null,
|
||||||
city: appointmentType == "Mobile" ? serviceLocationCity : null,
|
city: appointmentType == "Mobile" ? serviceLocationCity : null,
|
||||||
state: appointmentType == "Mobile" ? serviceLocationState : null,
|
state: appointmentType == "Mobile" ? serviceLocationState : null,
|
||||||
zipCode: appointmentType == "Mobile" ? serviceLocationZipCode : 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);
|
// 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);
|
// 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-sans-serif: UrbanistRegular, Arial, Helvetica, sans-serif;
|
||||||
$font-family-monospace:
|
$font-family-monospace:
|
||||||
UrbanistRegular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", 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
|
// stylelint-enable value-keyword-case
|
||||||
$font-family-base: $font-family-sans-serif;
|
$font-family-base: $font-family-sans-serif;
|
||||||
$font-family-code: $font-family-monospace;
|
$font-family-code: $font-family-monospace;
|
||||||
|
|
|
||||||
|
|
@ -7,15 +7,9 @@
|
||||||
isDismissible ? 'alert-dismissible' : '',
|
isDismissible ? 'alert-dismissible' : '',
|
||||||
this.alertClass,
|
this.alertClass,
|
||||||
this.cssClassNameForCmsWidget,
|
this.cssClassNameForCmsWidget,
|
||||||
this.hasBorder ? 'bordered' : '',
|
|
||||||
]">
|
]">
|
||||||
<p class="mx-6 my-0 fw-bold alert-heading">
|
<p class="mx-6 my-0 fw-bold alert-heading">
|
||||||
<img v-if="showInfoIcon" :src="infoIcon" alt="Info Icon" class="info-icon" />
|
<img v-if="showInfoIcon" :src="infoIcon" alt="Info Icon" class="info-icon" />
|
||||||
<img
|
|
||||||
v-if="showWarningIcon"
|
|
||||||
:src="warningIcon"
|
|
||||||
alt="Warning Icon"
|
|
||||||
class="warning-icon" />
|
|
||||||
{{ alertHeadline }}
|
{{ alertHeadline }}
|
||||||
</p>
|
</p>
|
||||||
<hr v-if="showHorizontalRow" />
|
<hr v-if="showHorizontalRow" />
|
||||||
|
|
@ -69,9 +63,7 @@ export default {
|
||||||
name: "alert",
|
name: "alert",
|
||||||
props: {
|
props: {
|
||||||
showInfoIcon: Boolean,
|
showInfoIcon: Boolean,
|
||||||
showWarningIcon: Boolean,
|
|
||||||
showHorizontalRow: Boolean,
|
showHorizontalRow: Boolean,
|
||||||
hasBorder: Boolean,
|
|
||||||
isDismissible: Boolean,
|
isDismissible: Boolean,
|
||||||
alertClass: String,
|
alertClass: String,
|
||||||
/*
|
/*
|
||||||
|
|
@ -104,9 +96,6 @@ export default {
|
||||||
infoIcon() {
|
infoIcon() {
|
||||||
return require(`@/assets/img/icons/info-circle-blue.svg`);
|
return require(`@/assets/img/icons/info-circle-blue.svg`);
|
||||||
},
|
},
|
||||||
warningIcon() {
|
|
||||||
return require(`@/assets/img/icons/alert-circle-yellow.svg`);
|
|
||||||
},
|
|
||||||
pageQueryString() {
|
pageQueryString() {
|
||||||
return applicationConfig.PAGE_QUERYSTRING;
|
return applicationConfig.PAGE_QUERYSTRING;
|
||||||
},
|
},
|
||||||
|
|
@ -222,19 +211,12 @@ export default {
|
||||||
background-color: $yellow-100;
|
background-color: $yellow-100;
|
||||||
.alert-heading {
|
.alert-heading {
|
||||||
color: $yellow-600;
|
color: $yellow-600;
|
||||||
|
|
||||||
.warning-icon {
|
|
||||||
height: 1rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
svg {
|
svg {
|
||||||
fill: $yellow-600;
|
fill: $yellow-600;
|
||||||
width: 1rem;
|
width: 1rem;
|
||||||
height: 1rem;
|
height: 1rem;
|
||||||
}
|
}
|
||||||
hr {
|
|
||||||
border-color: $yellow-800;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
&.alert-success {
|
&.alert-success {
|
||||||
background-color: $green-100;
|
background-color: $green-100;
|
||||||
|
|
@ -247,9 +229,6 @@ export default {
|
||||||
height: 1rem;
|
height: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&.bordered {
|
|
||||||
border: 1px solid;
|
|
||||||
}
|
|
||||||
& p {
|
& p {
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue