Merge pull request #3080 from Safelite/feature/CASH-2378
Feature/CASH-2378 - implement multi location popup
This commit is contained in:
commit
baa13e9b40
9 changed files with 1057 additions and 26 deletions
3
src/assets/img/icons/alert-circle-yellow.svg
Normal file
3
src/assets/img/icons/alert-circle-yellow.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<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>
|
||||
|
After Width: | Height: | Size: 474 B |
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -343,3 +343,14 @@ 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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,316 @@
|
|||
<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,
|
||||
};
|
||||
},
|
||||
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;
|
||||
},
|
||||
pushEvent() {
|
||||
let gaAction = this.confirmedAppointment ? this.GaActions.YES : this.GaActions.NO;
|
||||
if (this.mobileAppointmentOption && this.mobileAppointmentOption.timeSlot) {
|
||||
this.pushEventToGA(
|
||||
this.GaCategories.CONFIRMATION_CLICKED,
|
||||
gaAction,
|
||||
this.mobileAppointmentOption.timeSlot.startTime +
|
||||
"-" +
|
||||
this.mobileAppointmentOption.timeSlot.endTime +
|
||||
" " +
|
||||
this.mobileAppointmentOption.date,
|
||||
true
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
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;
|
||||
& > span {
|
||||
color: $red;
|
||||
font-size: $font-size-14;
|
||||
font-weight: $font-weight-600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: $font-size-20;
|
||||
font-weight: $font-weight-normal;
|
||||
}
|
||||
}
|
||||
.modal-body {
|
||||
padding: 0.5rem 1rem;
|
||||
|
||||
.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,309 @@
|
|||
<template>
|
||||
<baseInputButton v-bind="$props" @buttonClicked="handleAnswerChange" v-model="selectedValue">
|
||||
<div class="location-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 v-html="this.buttonAuxillaryCopy"></span>
|
||||
<p
|
||||
class="sub-label m-0"
|
||||
v-if="this.buttonLabelSubCopy"
|
||||
v-html="this.buttonLabelSubCopy"></p>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="col md-6 location-info">
|
||||
<div
|
||||
class="location-info-label"
|
||||
v-if="this.buttonBodyCopy"
|
||||
v-html="this.buttonBodyCopy"></div>
|
||||
</div>
|
||||
<div class="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";
|
||||
|
||||
export default {
|
||||
name: "multiLocationRadioButton",
|
||||
mixins: [inputButtonWrapperMixin],
|
||||
components: {
|
||||
baseInputButton,
|
||||
},
|
||||
computed: {
|
||||
arrayOfListItemsFromBodyText() {
|
||||
return this.getArrayOfListItemsFromRawCmsCopy(this.buttonBodyCopy);
|
||||
},
|
||||
shouldDisplayStrikeThroughPrice() {
|
||||
const displayPrice = this.buttonAuxillaryCopy?.substring(
|
||||
this.buttonAuxillaryCopy.indexOf("$")
|
||||
);
|
||||
return displayPrice != this.additionalButtonData?.strikeThroughPrice;
|
||||
},
|
||||
},
|
||||
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;
|
||||
}
|
||||
@include media-breakpoint-up(md) {
|
||||
margin: 1rem 0.5rem 0 0.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
&:first-child {
|
||||
margin: 1rem 0.5rem 0 0;
|
||||
}
|
||||
&:last-child {
|
||||
margin: 1rem 0 0 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
@include media-breakpoint-up(md) {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
input[type="radio"] {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
left: -9999px;
|
||||
|
||||
+ .location-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) {
|
||||
max-height: 500px;
|
||||
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 {
|
||||
+ .location-label {
|
||||
&:before {
|
||||
border: 1px solid #8e9292;
|
||||
box-shadow:
|
||||
0px 0px 0px 4px #9fcee6,
|
||||
0px 1px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:checked {
|
||||
+ .location-label {
|
||||
.location-specs {
|
||||
max-height: 1000px;
|
||||
}
|
||||
}
|
||||
+ .location-label {
|
||||
.location-specs {
|
||||
.hide-when-closed {
|
||||
display: block;
|
||||
}
|
||||
p:first-child {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
+ .location-label {
|
||||
border: 1px solid $blue;
|
||||
max-height: 500px;
|
||||
background-color: $blue-100;
|
||||
.special-save-box {
|
||||
background-color: $green-200;
|
||||
}
|
||||
}
|
||||
+ .location-label {
|
||||
&:before {
|
||||
box-shadow: 0px 0px 0px 1px $blue;
|
||||
}
|
||||
}
|
||||
+ .location-label {
|
||||
&:after {
|
||||
background: $blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:focus {
|
||||
+ .location-label {
|
||||
&:before {
|
||||
border: 2px solid $blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.package-footer {
|
||||
color: $red;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.location-specs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
transition: all 0.5s ease;
|
||||
@include media-breakpoint-up(md) {
|
||||
max-height: 500px;
|
||||
}
|
||||
|
||||
.row {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.pricing-info .insurance-pricing span {
|
||||
font-size: 0.875rem;
|
||||
font-family: UrbanistSemibold, Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
p {
|
||||
font-family: UrbanistSemibold, Arial, Helvetica, sans-serif;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
&:first-child {
|
||||
font-weight: 400;
|
||||
}
|
||||
span {
|
||||
&.pricing-info {
|
||||
color: $green;
|
||||
font-size: 0.875rem;
|
||||
font-family: UrbanistSemibold, Arial, Helvetica, sans-serif;
|
||||
@include media-breakpoint-up(md) {
|
||||
position: absolute;
|
||||
bottom: 0.75rem;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
margin-left: 1.25rem;
|
||||
}
|
||||
span.strikethrough-price {
|
||||
text-decoration: line-through;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 400;
|
||||
color: $gray-550;
|
||||
padding-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.sub-label {
|
||||
color: $green;
|
||||
text-transform: uppercase;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 1rem 0 0 -5px;
|
||||
padding: 0;
|
||||
li {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.714;
|
||||
color: $gray-550;
|
||||
a {
|
||||
line-height: 1.714;
|
||||
padding: 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,6 +81,16 @@
|
|||
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
|
||||
|
|
@ -191,6 +208,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";
|
||||
|
|
@ -221,7 +239,11 @@ import {
|
|||
RECAL_ACK_YES,
|
||||
} from "@/constants/schedule-constants";
|
||||
|
||||
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
|
||||
import {
|
||||
fetchCmsContentForPage,
|
||||
splitCopyOnCMSPlaceHolder,
|
||||
formatToUSDollar,
|
||||
} from "@/helpers/cms-content-helper";
|
||||
import { settleAllPromises } from "@/helpers/layout-helper";
|
||||
import {
|
||||
getPricedMobileFeePart,
|
||||
|
|
@ -237,7 +259,6 @@ 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,
|
||||
|
|
@ -475,10 +496,7 @@ export default {
|
|||
shopListButton: shopListButton,
|
||||
lastSelectedInshopOrDropoffProvider: null,
|
||||
selectedMobileFirstAppointment: false,
|
||||
isShowMobileFirstAppt: experimentMixin.methods.hasSettingEqualTo(
|
||||
experimentSettings.SHOW_MOBILE_FIRST_APPT,
|
||||
"true"
|
||||
),
|
||||
selectedMultiLocationAppointment: false,
|
||||
calendarLoadingStatus: "none",
|
||||
};
|
||||
},
|
||||
|
|
@ -611,7 +629,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();
|
||||
|
|
@ -677,8 +696,7 @@ export default {
|
|||
isMobileStaticRecalibrationApplicable() {
|
||||
return (
|
||||
this.displayMSR &&
|
||||
this.mobileFeePart?.partNumber == partNumberStrings.MOBILE_STATIC_RECAL_FEE &&
|
||||
(this.isCashItacNoComp || this.mobileFeePart?.isInsurable)
|
||||
this.mobileFeePart?.partNumber == partNumberStrings.MOBILE_STATIC_RECAL_FEE
|
||||
);
|
||||
},
|
||||
displayMSR() {
|
||||
|
|
@ -688,6 +706,28 @@ 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;
|
||||
},
|
||||
|
|
@ -900,6 +940,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 =
|
||||
|
|
@ -954,6 +999,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,
|
||||
|
|
@ -1239,7 +1296,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();
|
||||
|
|
@ -1273,7 +1331,7 @@ export default {
|
|||
},
|
||||
|
||||
async initializeDatePicker() {
|
||||
this.isShowMobileFirstAppt && this.showLoadingModal();
|
||||
this.isShowMultiLocationPopup && this.showLoadingModal();
|
||||
|
||||
const includeMobileTimeSlots = this.isServiceableMobile;
|
||||
const includeInshopTimeSlots = this.isServiceableInshop || this.isServiceableDropoff;
|
||||
|
|
@ -1785,7 +1843,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();
|
||||
|
|
@ -1900,10 +1959,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;
|
||||
}
|
||||
|
|
@ -1998,7 +2057,7 @@ export default {
|
|||
if (this.isShowMobileFirstAppt) {
|
||||
if (pmMobileDays == 0) {
|
||||
// Selects the first available time slot
|
||||
preSelectedMobileAppointment =
|
||||
promotedMobileAppointment =
|
||||
numberOfDaysToFirstMobileAMDate <= numberOfDaysToFirstMobilePMDate
|
||||
? firstMobileAMAppt
|
||||
: firstMobilePMAppt;
|
||||
|
|
@ -2007,20 +2066,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();
|
||||
}
|
||||
|
|
@ -2028,11 +2087,170 @@ 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
|
||||
);
|
||||
|
||||
// temp vars to use for testing; TO BE REMOVED
|
||||
maxDayRangeToShowPmTimeslot = 2;
|
||||
maxDayRangeToShowNoPmTimeslot = 2;
|
||||
maxDayRangeToShowAnyTimeslot = 5;
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
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,
|
||||
|
|
@ -2040,6 +2258,7 @@ export default {
|
|||
},
|
||||
timeSlot: null,
|
||||
date: null,
|
||||
addressCopy: null,
|
||||
};
|
||||
const isMatchingApptDay = dateObj.timeSlots.some(
|
||||
(slot) =>
|
||||
|
|
@ -2047,6 +2266,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;
|
||||
}
|
||||
|
|
@ -2054,7 +2283,7 @@ export default {
|
|||
return null;
|
||||
}
|
||||
},
|
||||
updateTimeSlotandNavigateForward(timeSlotObj) {
|
||||
updateMobileFirstTimeSlotandNavigateForward(timeSlotObj) {
|
||||
this.selectedMobileFirstAppointment = true;
|
||||
this.appointmentType = AppointmentTypeStrings.MOBILE;
|
||||
this.selectedDate = timeSlotObj.date;
|
||||
|
|
@ -2062,6 +2291,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();
|
||||
|
|
@ -2085,6 +2326,7 @@ export default {
|
|||
textBlock,
|
||||
timeSlotQuestion,
|
||||
mobileFirstModal,
|
||||
multiLocationModal,
|
||||
recalAckModal,
|
||||
alert,
|
||||
serviceZipModalQuestion,
|
||||
|
|
|
|||
|
|
@ -7,9 +7,15 @@
|
|||
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" />
|
||||
|
|
@ -63,7 +69,9 @@ export default {
|
|||
name: "alert",
|
||||
props: {
|
||||
showInfoIcon: Boolean,
|
||||
showWarningIcon: Boolean,
|
||||
showHorizontalRow: Boolean,
|
||||
hasBorder: Boolean,
|
||||
isDismissible: Boolean,
|
||||
alertClass: String,
|
||||
/*
|
||||
|
|
@ -96,6 +104,9 @@ 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;
|
||||
},
|
||||
|
|
@ -211,12 +222,19 @@ 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;
|
||||
|
|
@ -229,6 +247,9 @@ export default {
|
|||
height: 1rem;
|
||||
}
|
||||
}
|
||||
&.bordered {
|
||||
border: 1px solid;
|
||||
}
|
||||
& p {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue