Merge branch 'release/2026.03.12' into rlsmerge/2026.03.12-to-dev
This commit is contained in:
commit
c785ebede4
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",
|
||||
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,6 +41,10 @@ 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",
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -343,14 +343,3 @@ export function splitCMSCopyOnParagraphTag(copy) {
|
|||
export function splitCMSCopyOnBR(copy) {
|
||||
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,
|
||||
serviceLocationZipCode: store.getters.order.serviceLocation.zipCode,
|
||||
pricedLineItems: lineItemsForCart,
|
||||
serviceLocationAddressLine1: store.getters.order.serviceLocation?.address,
|
||||
serviceLocationAddressLine2: store.getters.order.serviceLocation?.address2,
|
||||
},
|
||||
"payment-method",
|
||||
false
|
||||
|
|
@ -526,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
|
||||
|
|
@ -650,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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -74,16 +81,6 @@
|
|||
cmsWidgetName="AlertNoShopsWidget"
|
||||
v-if="displayNoShopsAlert"
|
||||
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
|
||||
|
|
@ -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 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";
|
||||
|
|
@ -231,11 +229,7 @@ import {
|
|||
RECAL_ACK_YES,
|
||||
} from "@/constants/schedule-constants";
|
||||
|
||||
import {
|
||||
fetchCmsContentForPage,
|
||||
splitCopyOnCMSPlaceHolder,
|
||||
formatToUSDollar,
|
||||
} from "@/helpers/cms-content-helper";
|
||||
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
|
||||
import { settleAllPromises } from "@/helpers/layout-helper";
|
||||
import {
|
||||
getPricedMobileFeePart,
|
||||
|
|
@ -251,6 +245,7 @@ import { partNumberStrings } from "@/constants/part-number-strings";
|
|||
import store from "@/store";
|
||||
|
||||
import { storeActions } from "@/constants/store-actions";
|
||||
import { splitCopyOnCMSPlaceHolder } from "@/helpers/cms-content-helper";
|
||||
import {
|
||||
calcDaysBetweenDates,
|
||||
convertDateStringToDate,
|
||||
|
|
@ -488,10 +483,7 @@ export default {
|
|||
shopListButton: shopListButton,
|
||||
lastSelectedInshopOrDropoffProvider: null,
|
||||
selectedMobileFirstAppointment: false,
|
||||
isShowMobileFirstAppt: experimentMixin.methods.hasSettingEqualTo(
|
||||
experimentSettings.SHOW_MOBILE_FIRST_APPT,
|
||||
"true"
|
||||
),
|
||||
selectedMultiLocationAppointment: false,
|
||||
calendarLoadingStatus: "none",
|
||||
};
|
||||
},
|
||||
|
|
@ -624,7 +616,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();
|
||||
|
|
@ -690,7 +683,8 @@ export default {
|
|||
isMobileStaticRecalibrationApplicable() {
|
||||
return (
|
||||
this.displayMSR &&
|
||||
this.mobileFeePart?.partNumber == partNumberStrings.MOBILE_STATIC_RECAL_FEE
|
||||
this.mobileFeePart?.partNumber == partNumberStrings.MOBILE_STATIC_RECAL_FEE &&
|
||||
(this.isCashItacNoComp || this.mobileFeePart?.isInsurable)
|
||||
);
|
||||
},
|
||||
displayMSR() {
|
||||
|
|
@ -700,28 +694,6 @@ export default {
|
|||
?.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() {
|
||||
return !this.isInsurance || this.isITAC || this.isNoComp;
|
||||
},
|
||||
|
|
@ -934,6 +906,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 =
|
||||
|
|
@ -988,6 +965,18 @@ export default {
|
|||
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,
|
||||
|
|
@ -1273,7 +1262,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();
|
||||
|
|
@ -1307,7 +1297,7 @@ export default {
|
|||
},
|
||||
|
||||
async initializeDatePicker() {
|
||||
this.isShowMobileFirstAppt && this.showLoadingModal();
|
||||
this.isShowMultiLocationPopup && this.showLoadingModal();
|
||||
|
||||
const includeMobileTimeSlots = this.isServiceableMobile;
|
||||
const includeInshopTimeSlots = this.isServiceableInshop || this.isServiceableDropoff;
|
||||
|
|
@ -1819,7 +1809,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();
|
||||
|
|
@ -1934,10 +1925,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;
|
||||
}
|
||||
|
|
@ -2032,7 +2023,7 @@ export default {
|
|||
if (this.isShowMobileFirstAppt) {
|
||||
if (pmMobileDays == 0) {
|
||||
// Selects the first available time slot
|
||||
preSelectedMobileAppointment =
|
||||
promotedMobileAppointment =
|
||||
numberOfDaysToFirstMobileAMDate <= numberOfDaysToFirstMobilePMDate
|
||||
? firstMobileAMAppt
|
||||
: firstMobilePMAppt;
|
||||
|
|
@ -2041,20 +2032,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();
|
||||
}
|
||||
|
|
@ -2062,11 +2053,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,
|
||||
|
|
@ -2074,6 +2230,7 @@ export default {
|
|||
},
|
||||
timeSlot: null,
|
||||
date: null,
|
||||
addressCopy: null,
|
||||
};
|
||||
const isMatchingApptDay = dateObj.timeSlots.some(
|
||||
(slot) =>
|
||||
|
|
@ -2081,6 +2238,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;
|
||||
}
|
||||
|
|
@ -2088,7 +2255,7 @@ export default {
|
|||
return null;
|
||||
}
|
||||
},
|
||||
updateTimeSlotandNavigateForward(timeSlotObj) {
|
||||
updateMobileFirstTimeSlotandNavigateForward(timeSlotObj) {
|
||||
this.selectedMobileFirstAppointment = true;
|
||||
this.appointmentType = AppointmentTypeStrings.MOBILE;
|
||||
this.selectedDate = timeSlotObj.date;
|
||||
|
|
@ -2096,6 +2263,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();
|
||||
|
|
@ -2119,6 +2298,7 @@ export default {
|
|||
textBlock,
|
||||
timeSlotQuestion,
|
||||
mobileFirstModal,
|
||||
multiLocationModal,
|
||||
recalAckModal,
|
||||
alert,
|
||||
serviceZipModalQuestion,
|
||||
|
|
|
|||
|
|
@ -209,6 +209,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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -3115,6 +3115,8 @@ export const actions = {
|
|||
serviceLocationState,
|
||||
serviceLocationZipCode,
|
||||
pricedLineItems,
|
||||
serviceLocationAddressLine1,
|
||||
serviceLocationAddressLine2,
|
||||
},
|
||||
pageNameToLog,
|
||||
}
|
||||
|
|
@ -3148,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;
|
||||
|
|
|
|||
|
|
@ -7,15 +7,9 @@
|
|||
isDismissible ? 'alert-dismissible' : '',
|
||||
this.alertClass,
|
||||
this.cssClassNameForCmsWidget,
|
||||
this.hasBorder ? 'bordered' : '',
|
||||
]">
|
||||
<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="showWarningIcon"
|
||||
:src="warningIcon"
|
||||
alt="Warning Icon"
|
||||
class="warning-icon" />
|
||||
{{ alertHeadline }}
|
||||
</p>
|
||||
<hr v-if="showHorizontalRow" />
|
||||
|
|
@ -69,9 +63,7 @@ export default {
|
|||
name: "alert",
|
||||
props: {
|
||||
showInfoIcon: Boolean,
|
||||
showWarningIcon: Boolean,
|
||||
showHorizontalRow: Boolean,
|
||||
hasBorder: Boolean,
|
||||
isDismissible: Boolean,
|
||||
alertClass: String,
|
||||
/*
|
||||
|
|
@ -104,9 +96,6 @@ export default {
|
|||
infoIcon() {
|
||||
return require(`@/assets/img/icons/info-circle-blue.svg`);
|
||||
},
|
||||
warningIcon() {
|
||||
return require(`@/assets/img/icons/alert-circle-yellow.svg`);
|
||||
},
|
||||
pageQueryString() {
|
||||
return applicationConfig.PAGE_QUERYSTRING;
|
||||
},
|
||||
|
|
@ -222,19 +211,12 @@ export default {
|
|||
background-color: $yellow-100;
|
||||
.alert-heading {
|
||||
color: $yellow-600;
|
||||
|
||||
.warning-icon {
|
||||
height: 1rem;
|
||||
}
|
||||
}
|
||||
svg {
|
||||
fill: $yellow-600;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
hr {
|
||||
border-color: $yellow-800;
|
||||
}
|
||||
}
|
||||
&.alert-success {
|
||||
background-color: $green-100;
|
||||
|
|
@ -247,9 +229,6 @@ export default {
|
|||
height: 1rem;
|
||||
}
|
||||
}
|
||||
&.bordered {
|
||||
border: 1px solid;
|
||||
}
|
||||
& p {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue