Merge branch 'develop' into CASH-1193

This commit is contained in:
maguire-arman 2025-07-30 09:51:23 -04:00 committed by GitHub
commit 7bd75f0609
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 853 additions and 990 deletions

View file

@ -31,6 +31,8 @@ module.exports = {
"!src/layouts/payment-pia-return/*.vue", // Temp test exclusion while in development
"!src/layouts/insurance/*.vue", // Temp test exclusion while in development
"!src/layouts/insurance-company/*.vue", // Temp test exclusion while in development
"!src/layouts/schedule/**/*.vue", // Temp test exclusion while in development
"!src/digital-components/date-picker/**/*.vue", // Temp test exclusion while in development
"!src/**/*-june-2025.vue", // Exclude these temporary files for CASH-845 project
@ -41,7 +43,7 @@ module.exports = {
testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"],
coverageThreshold: {
global: {
statements: 68,
statements: 66,
},
},
// Uncomment this to avoid the massive amount of warnings we are getting for onSubmit and onInvalidSubmit

View file

@ -1,35 +1,35 @@
export const pageProgressMapper = {
//Combine progress bar slide percent and button steps
vehicle: {
percent: 7,
step: 1,
},
"vehicle-damage": {
percent: 10,
step: 1,
},
estimate: {
"vehicle-damage": {
percent: 14,
step: 1,
},
"service-zip": {
estimate: {
percent: 18,
step: 1,
},
"service-zip": {
percent: 22,
step: 1,
},
"vin-lookup": {
percent: 18,
percent: 22,
step: 1,
},
"license-plate-lookup": {
percent: 18,
percent: 22,
step: 1,
},
"address-lookup": {
percent: 18,
percent: 22,
step: 1,
},
"address-vehicles": {
percent: 25,
percent: 26,
step: 1,
},
"part-questions": {
@ -49,7 +49,7 @@ export const pageProgressMapper = {
step: 1,
},
quote: {
percent: 39, //Must be 39
percent: 39, //The first Step 2 must be 39 so the animated bar lines up with the dot
step: 2,
},
"insurance-company": {
@ -61,7 +61,7 @@ export const pageProgressMapper = {
step: 2,
},
schedule: {
percent: 70.5, //Must be 70.5
percent: 70.5, //The first Step 3 must be 70.5 so the animated bar lines up with the dot
step: 3,
},
"mobile-details": {
@ -73,7 +73,7 @@ export const pageProgressMapper = {
step: 3,
},
"payment-method": {
percent: 92, //Must be 92
percent: 92, //The first Step 4 must be 92 so the animated bar lines up with the dot
step: 3,
},
payment: {

View file

@ -674,140 +674,140 @@ describe("date-picker.vue", () => {
wrapper.unmount();
});
describe("when loadInitialData is run...", () => {
test("and this.todayString exists, expect it should have a todayDate matching today", async () => {
// Arrange
const { wrapper } = setupMocks({});
const todayDate = new Date();
const todayDateString =
todayDate.getFullYear() +
"-" +
("0" + (todayDate.getMonth() + 1)).slice(-2) +
"-" +
("0" + todayDate.getDate()).slice(-2);
// describe("when loadInitialData is run...", () => {
// test("and this.todayString exists, expect it should have a todayDate matching today", async () => {
// // Arrange
// const { wrapper } = setupMocks({});
// const todayDate = new Date();
// const todayDateString =
// todayDate.getFullYear() +
// "-" +
// ("0" + (todayDate.getMonth() + 1)).slice(-2) +
// "-" +
// ("0" + todayDate.getDate()).slice(-2);
// Act
const testResult = await wrapper.vm.loadInitialData({
selectableDatesSetting: "custom",
initialViewRowsToShow: 5,
getSelectableDatesCallback: mockPromiseResolve,
preSelectedDate: null,
});
// // Act
// const testResult = await wrapper.vm.loadInitialData({
// selectableDatesSetting: "custom",
// initialViewRowsToShow: 5,
// getSelectableDatesCallback: mockPromiseResolve,
// preSelectedDate: null,
// });
// Assert
expect(testResult).toMatchObject({
todayDate: todayDateString,
});
// // Assert
// expect(testResult).toMatchObject({
// todayDate: todayDateString,
// });
wrapper.unmount();
});
// wrapper.unmount();
// });
test("and overrride date exists, expect it should have a todayDate matching the override", async () => {
// Arrange
const overrideDate = mockValuesForSetDate01.overrideDate;
const { wrapper } = setupMocks({
propsData: {
selectableDatesSetting: "custom",
todayOverrideDateString: overrideDate,
},
});
// test("and overrride date exists, expect it should have a todayDate matching the override", async () => {
// // Arrange
// const overrideDate = mockValuesForSetDate01.overrideDate;
// const { wrapper } = setupMocks({
// propsData: {
// selectableDatesSetting: "custom",
// todayOverrideDateString: overrideDate,
// },
// });
// Act
const localThis = {
todayString: null,
todayOverrideDateString: overrideDate,
getMonthEnd: () => mockValuesForSetDate01.monthEnd,
getInitialViewWeeks: () => mockValuesForSetDate01.initialViewWeeks,
};
const testResult = await datePicker.methods.loadInitialData.call(localThis, {
selectableDatesSetting: "custom",
initialViewRowsToShow: 5,
getSelectableDatesCallback: mockPromiseResolve,
preSelectedDate: null,
// // Act
// const localThis = {
// todayString: null,
// todayOverrideDateString: overrideDate,
// getMonthEnd: () => mockValuesForSetDate01.monthEnd,
// getInitialViewWeeks: () => mockValuesForSetDate01.initialViewWeeks,
// };
// const testResult = await datePicker.methods.loadInitialData.call(localThis, {
// selectableDatesSetting: "custom",
// initialViewRowsToShow: 5,
// getSelectableDatesCallback: mockPromiseResolve,
// preSelectedDate: null,
todayOverrideDateString: overrideDate,
});
// todayOverrideDateString: overrideDate,
// });
// Assert
expect(testResult).toMatchObject({
todayDate: overrideDate,
});
// // Assert
// expect(testResult).toMatchObject({
// todayDate: overrideDate,
// });
wrapper.unmount();
});
// wrapper.unmount();
// });
test("with no this.todayString or overrride date, expect it should have a todayDate matching today", async () => {
// Arrange
const { wrapper } = setupMocks({});
const todayDate = new Date();
const todayDateString =
todayDate.getFullYear() +
"-" +
("0" + (todayDate.getMonth() + 1)).slice(-2) +
"-" +
("0" + todayDate.getDate()).slice(-2);
// test("with no this.todayString or overrride date, expect it should have a todayDate matching today", async () => {
// // Arrange
// const { wrapper } = setupMocks({});
// const todayDate = new Date();
// const todayDateString =
// todayDate.getFullYear() +
// "-" +
// ("0" + (todayDate.getMonth() + 1)).slice(-2) +
// "-" +
// ("0" + todayDate.getDate()).slice(-2);
// Act
const localThis = {
todayString: null,
getMonthEnd: () => mockValuesForSetDate01.monthEnd,
getInitialViewWeeks: () => mockValuesForSetDate01.initialViewWeeks,
};
// // Act
// const localThis = {
// todayString: null,
// getMonthEnd: () => mockValuesForSetDate01.monthEnd,
// getInitialViewWeeks: () => mockValuesForSetDate01.initialViewWeeks,
// };
//Act
const testResult = await datePicker.methods.loadInitialData.call(localThis, {
selectableDatesSetting: "custom",
initialViewRowsToShow: 5,
getSelectableDatesCallback: mockPromiseResolve,
preSelectedDate: null,
});
// //Act
// const testResult = await datePicker.methods.loadInitialData.call(localThis, {
// selectableDatesSetting: "custom",
// initialViewRowsToShow: 5,
// getSelectableDatesCallback: mockPromiseResolve,
// preSelectedDate: null,
// });
// Assert
expect(testResult).toMatchObject({
todayDate: todayDateString,
});
// // Assert
// expect(testResult).toMatchObject({
// todayDate: todayDateString,
// });
wrapper.unmount();
});
// wrapper.unmount();
// });
test("and config.selectableDatesSetting is PAST, expect it should have a calendarViewDirection of PAST", async () => {
// Arrange
const { wrapper } = setupMocks({});
// test("and config.selectableDatesSetting is PAST, expect it should have a calendarViewDirection of PAST", async () => {
// // Arrange
// const { wrapper } = setupMocks({});
// Act
const testResult = await wrapper.vm.loadInitialData({
selectableDatesSetting: "past",
initialViewRowsToShow: 5,
getSelectableDatesCallback: mockPromiseResolve,
});
// // Act
// const testResult = await wrapper.vm.loadInitialData({
// selectableDatesSetting: "past",
// initialViewRowsToShow: 5,
// getSelectableDatesCallback: mockPromiseResolve,
// });
// Assert
expect(testResult).toMatchObject({
calendarViewDirection: "past",
});
// // Assert
// expect(testResult).toMatchObject({
// calendarViewDirection: "past",
// });
wrapper.unmount();
});
// wrapper.unmount();
// });
test("and config.selectableDatesSetting is CUSTOM, expect it should have a calendarViewDirection of FUTURE", async () => {
// Arrange
const { wrapper } = setupMocks({});
// test("and config.selectableDatesSetting is CUSTOM, expect it should have a calendarViewDirection of FUTURE", async () => {
// // Arrange
// const { wrapper } = setupMocks({});
// Act
const testResult = await wrapper.vm.loadInitialData({
selectableDatesSetting: "custom",
initialViewRowsToShow: 5,
getSelectableDatesCallback: mockPromiseResolve,
});
// // Act
// const testResult = await wrapper.vm.loadInitialData({
// selectableDatesSetting: "custom",
// initialViewRowsToShow: 5,
// getSelectableDatesCallback: mockPromiseResolve,
// });
// Assert
expect(testResult).toMatchObject({
calendarViewDirection: "future",
});
// // Assert
// expect(testResult).toMatchObject({
// calendarViewDirection: "future",
// });
wrapper.unmount();
});
});
// wrapper.unmount();
// });
// });
describe("when setCalendarData is run...", () => {
test("expect that data elements are updated properly", async () => {

View file

@ -307,6 +307,7 @@ export default {
},
set(newSelectedDate) {
this.$emit("update:modelValue", newSelectedDate);
this.dispatchStoreAction(this.storeActions.SAVE_WAITLIST_REQUESTED, false, false);
},
},
dropOffDurationText() {
@ -335,11 +336,32 @@ export default {
this.estimatedServiceMinutesMaximum
);
return `${inshopDurationTextWithoutTime} ${inshopDurationTime}`;
if (this.estimatedServiceMinutesMinimum && this.estimatedServiceMinutesMaximum) {
return `${inshopDurationTextWithoutTime} ${inshopDurationTime}`;
}
return null;
},
mobileDurationText() {
const mobileDurationTextWithoutTime = this.getCmsContent(
"TimeSlotModalQuestion",
cmsWidgetFieldMappings.DURATION
);
const mobileDurationTime = getDisplayTextForDurationLength(
this.estimatedServiceMinutesMinimum,
this.estimatedServiceMinutesMaximum
);
if (this.estimatedServiceMinutesMinimum && this.estimatedServiceMinutesMaximum) {
return `${mobileDurationTextWithoutTime} ${mobileDurationTime}`;
}
return null;
},
durationTextBlockCopy() {
if (this.appointmentType === AppointmentTypeStrings.MOBILE) {
return null;
return this.mobileDurationText;
} else if (this.appointmentType === AppointmentTypeStrings.IN_SHOP) {
return this.inshopDurationText;
} else if (
@ -359,8 +381,8 @@ export default {
},
},
methods: {
initializeComponent(initialData) {
this.setCalendarData(initialData);
async initializeComponent(initialData) {
await this.setCalendarData(initialData);
this.$refs.timeSlotModalQuestion.initializeComponent();
},
fireDateSelectedEvent(event, date) {
@ -555,6 +577,8 @@ export default {
endDateString: initialViewEndDate,
providerNumber: config.providerNumber,
zipCode: config.zipCode,
includeMobileTimeSlots: config.includeMobileTimeSlots,
includeInshopTimeSlots: config.includeInshopTimeSlots,
});
resolve(response);
});
@ -574,6 +598,11 @@ export default {
});
},
async setCalendarData(config = {}) {
this.isLoading = true;
this.selectableDatesInshop = [];
this.selectableDatesMobile = [];
this.months = [];
this.hideSomeDaysForInitialView = config.hideSomeDaysForInitialView;
const hideSecondMonth = config.hideSecondMonth;
const direction = config.calendarViewDirection;

View file

@ -182,11 +182,11 @@ export default {
};
},
methods: {
focusSearchInput() {
focusSearchInput(event) {
// Focus cursor in input when search icon is clicked
const field = document.querySelector("input");
field.focus();
this.$emit("search-icon-click");
this.$emit("search-icon-click", event);
},
async imageChanged(e) {
let file = e.target.files[0];

View file

@ -317,10 +317,18 @@ export default {
}
}
.custom-secondary {
&:hover {
border-radius: 0.5rem;
color: $blue;
border: 1px solid $blue;
&:hover,
&:focus,
&:active {
color: $blue;
background: transparent;
border: 1px solid $blue;
box-shadow:
0 0 0 3px #ffffff,
0 0 0 5.5px $blue;
}
}
}

View file

@ -19,10 +19,10 @@
v-if="showThisQuestionChain(questionsDatum, i)"
:answerKey="questionsDatum.answerKey"
:validationRules="validationRules" />
<!-- Save your progress slot -->
<slot></slot>
</div>
<slot></slot>
<navbar
ref="navbar"
cmsWidgetName="FunnelFooterWidget"
@ -88,6 +88,12 @@ export default {
</script>
<style lang="scss">
.nav-bar {
margin-top: 1rem;
}
.save-progress-modal-question {
margin: 2rem 0;
}
.questions-page {
.question-text {
margin-bottom: 1.5rem;

View file

@ -28,7 +28,7 @@
<div
v-if="!isBackButtonHidden"
class="col-auto link-col py-1 text-break"
:class="[buttonSize ? 'back-centered-below mt-5' : '']">
:class="[buttonSize ? 'mt-5' : '']">
<textLink
linkType="navigation"
:text="backLink"
@ -134,11 +134,6 @@ export default {
width: 100%;
}
.back-centered-below {
display: flex;
justify-content: center;
}
@media only screen and (min-width: 340px) {
.col,
.col-auto,
@ -147,11 +142,6 @@ export default {
justify-content: flex-end;
}
.back-centered-below {
display: flex;
justify-content: center;
}
.btn-primary {
width: auto;
}

View file

@ -1,15 +1,13 @@
<template>
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm">
<div class="page-container-grouped-styles position-relative">
<div>
<div class="container-fluid pb-2">
<div class="row justify-content-center">
<div class="col-md-6">
<funnelHeader
cmsWidgetName="FunnelHeaderWidget"
ref="funnelHeader"
hideSalesforceWebchatLaunchButton />
</div>
<loadingModal notFullScreen ref="loadingModal" />
<funnelHeader cmsWidgetName="FunnelHeaderWidget" ref="funnelHeader" />
<div class="container page-container-grouped-styles">
<div class="row">
<div class="col-12 col-md-10 col-lg-8 col-xl-7">
<div class="header-container">
<img :src="ScheduleConfirmationImage" />
<span v-html="ScheduleConfirmationText"></span>
</div>
<div class="row justify-content-center">
<div class="col-md-6 col-xl-4">
@ -85,7 +83,63 @@
<hr class="mb-5" />
</div>
<addToCalendar
mobileWidgetName="AddToCalendar_Mobile"
inShopWidgetName="AddToCalendar_InShop"
dropOffWidgetName="AddToCalendar_DropOff"
overnightDropOffWidgetName="AddToCalendar_OvernightDropOff"
allDayDropOffWidgetName="AddToCalendar_AllDayDropOff"
sameDayDropOffWidgetName="AddToCalendar_SameDayDropOff"
:serviceLocationFullAddress="ServiceLocationFullAddress"
:providerFullAddress="ProviderFullAddress"
:appointmentType="AppointmentType"
:scheduleDate="ScheduleDate"
:scheduleStartTime="ScheduleStartTime"
:scheduleEndTime="ScheduleEndTime" />
<div class="appointment-text" v-html="AppointmentWordingText"></div>
<textBlock
:customText="AppointmentDuration"
justifyText="center"
typeStyle="medium"
class="duration-text-block" />
</div>
<div class="mt-3" v-if="shouldDisplayFosterLove">
<donationBlock
cmsWidgetName="DonationWidget"
v-model="donationAmount"
:donationValues="donationValues"
:showDonationSuccess="showDonationSuccess"
:showDonationError="showDonationError"
@DonationAddedEvent="addDonationToOrder"
ref="donationBlock" />
</div>
<hr class="mt-5 mb-0" />
<cart
v-if="ShowCart"
:damage="damageInfo"
:availableVaps="vaps"
:allowItemRemoval="false"
v-model="lineItemsWithoutDonation"
:donationCartItem="donationLineItem"
:showAsPaid="isPia"
servicePackageOptionsCmsName="ServicePackageTitle"
:isInsurance="isInsurance"
:insuranceDeductible="currentDeductible"
:insuranceCompanyName="insuranceCompanyName"
:showInsuranceCoverageAs="showInsuranceCoverageAs"
:shouldHideRecalibration="shouldHideRecalibration"
:isExpandedOnLoad="false"
:isItac="isItac"
:isNoComp="isNoComp"
:isMSRFeeApplicable="isMSRFeeApplicable" />
<hr class="mt-4 mb-5" />
</div>
</div>
</div>
@ -94,7 +148,6 @@
<script>
// Components
import funnelHeader from "@/fmg-components/funnel-header/funnel-header";
import navbar from "@/fmg-components/nav-bar/nav-bar";
import addToCalendar from "@/layouts/confirmation/add-to-calendar/add-to-calendar";
import cart from "@/fmg-components/cart/cart";
import textBlock from "@/digital-components/text-block/text-block";
@ -620,7 +673,8 @@ export default {
.header-container {
display: flex;
align-items: center;
padding: 1rem 0;
justify-content: flex-start;
padding: 1.5rem 0 0.5rem 0;
p {
display: inline-block;

View file

@ -51,7 +51,7 @@
@ForwardClicked="forwardButtonAction" />
<textBlock
class="mb-5 disclaimer-block"
class="mb-5"
cmsWidgetName="DisclaimerCopyWidget"
typeStyle="caption" />
</div>
@ -215,10 +215,3 @@ export default {
},
};
</script>
<style lang="scss" scoped>
.disclaimer-block {
position: relative;
z-index: 2;
}
</style>

View file

@ -1,15 +1,15 @@
<template>
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm" v-slot="{ meta }">
<loadingModal ref="loadingModal" />
<div class="container-fluid page-container-grouped-styles">
<div class="row justify-content-center">
<div class="col-md-6">
<funnelHeader cmsWidgetName="FunnelHeaderWidget" ref="funnelHeader" />
</div>
</div>
<div class="row justify-content-center">
<div class="col-md-6 col-xl-4 mt-4">
<funnelHeader cmsWidgetName="FunnelHeaderWidget" ref="funnelHeader" />
<div class="container position-relative">
<div class="row">
<div class="col-12 col-md-10 col-lg-8 col-xl-7">
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" />
<hr class="my-3" />
<textBlock cmsWidgetName="AppointmentTimeOfService" class="time-header" />
<textBlock cmsWidgetName="AppointmentDateAndTime" />
<hr class="my-3" />
<div class="mobile-location-questions">
<div class="address-questions-container">
<mobileAddressQuestions
@ -150,8 +150,11 @@ export default {
},
},
computed: {
headerText() {
return this.getCmsContent("MobileLocationModalWidget", "HeaderText");
AppointmentDateAndTime() {
return this.getCmsContent("AppointmentDateAndTime", "Text");
},
AppointmentTimeOfService() {
return this.getCmsContent("AppointmentDateAndTime", "Text");
},
},
components: {
@ -166,3 +169,11 @@ export default {
},
};
</script>
<style lang="scss" scoped>
.time-header {
font-family: UrbanistSemibold, Arial, Helvetica, sans-serif;
font-weight: 600;
color: $black;
}
</style>

View file

@ -6,9 +6,13 @@
async></component>
<div>
<span v-for="token in headerCopyTokens" :key="token" v-html="token"></span>
<span
class="callout"
v-for="token in headerCopyTokens"
:key="token"
v-html="token"></span>
</div>
<div :id="showAfterpayExtendedPayOption ? 'extended-pay-option' : ''">
<div>
<span v-for="token in afterpayCopyTokens" :key="token">
<span v-if="isAfterpayPriceToken(token)">{{ afterpayPrice }}</span>
<span v-else-if="isInlineImageToken(token)">
@ -78,15 +82,7 @@ export default {
return splitCMSCopyOnBR(this.getCmsContent(this.cmsWidgetName, "HeaderText"));
},
afterpayCopyTokens() {
if (this.showAfterpayExtendedPayOption) {
return splitCopyOnCMSPlaceHolder(
this.getCmsContent(this.cmsWidgetName, "BodyText")
);
} else {
return splitCopyOnCMSPlaceHolder(
this.getCmsContent(this.cmsWidgetName, "BodyText2")
);
}
return splitCopyOnCMSPlaceHolder(this.getCmsContent(this.cmsWidgetName, "BodyText"));
},
modalCopy() {
return this.getCmsContent(this.cmsWidgetName, "FooterText");
@ -114,12 +110,6 @@ export default {
return price;
},
showAfterpayExtendedPayOption() {
return (
this.afterpayExtendedPayOptionThreshold &&
this.getTierOnePackagePrice >= this.afterpayExtendedPayOptionThreshold
);
},
},
components: {},
};
@ -138,11 +128,17 @@ export default {
flex-direction: row;
}
.callout {
font-family:
UrbanistSemibold Arial,
Helvetica,
sans-serif;
}
> div:first-of-type {
display: flex;
color: $black;
font-size: $font-size-20;
font-weight: $font-weight-600;
line-height: 2rem;
@include media-breakpoint-down(md) {
border-bottom: 1px solid;
@ -170,7 +166,7 @@ export default {
}
@include media-breakpoint-up(md) {
padding-left: 1rem;
max-width: 26rem;
max-width: 32rem;
}
}
& .alert-heading {

View file

@ -927,6 +927,9 @@ export default {
}
.funnel-sub-header {
:deep(h5) {
text-align: center;
}
p {
font-size: 1rem;
}

View file

@ -117,4 +117,10 @@ export default {
margin-top: 14rem;
}
}
:deep(.menu-modal-container) {
display: none;
}
:deep(.progress-bar-outer) {
opacity: 0;
}
</style>

View file

@ -608,6 +608,7 @@ function setupMocks({ customMountOptions }) {
wrapper.vm.setCmsContent = jest.fn();
wrapper.vm.$refs.datePicker.initializeComponent = jest.fn();
wrapper.vm.$refs.datePicker.loadInitialData = jest.fn();
wrapper.vm.$refs.locationAlerts.initializeComponent = jest.fn();
wrapper.vm.$refs.navbar.updateButtonText = jest.fn();

View file

@ -1,5 +1,5 @@
<template>
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm" v-slot="{ meta }">
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm">
<loadingModal ref="loadingModal" />
<funnelHeader cmsWidgetName="FunnelHeaderWidget" ref="funnelHeader" />
<div class="container page-container-grouped-styles">
@ -18,6 +18,7 @@
@updated-serviceability="setServiceabilityDetails"
@updated-contains-military-base="setContainsMilitaryBase"
@updated-bill-to-account-number="setBillToAccountNumber"
@service-zip-changed-from-modal="onServiceZipChanged"
linkWidgetName="ServiceZipLinkWidget"
modalWidgetName="ServiceZipModalWidget" />
<alert
@ -101,23 +102,12 @@
class="ps-4 pe-4 mt-4" />
</div>
<shopQuestionPopup
modalWidgetName="ShopQuestionPopupWidget"
:appointmentType="appointmentType"
:modelValue="{
zipCode: zipCode,
state: state,
}"
:preSelectedProviderNumber="selectedProvider?.providerNumber"
:shopProviderData="shopProviderData"
:isServiceableInshop="isServiceableInshop"
@updated-serviceability="setServiceabilityDetails"
v-if="isShopQuestionDisplayed"
:selectedProviderNumberFromParent="selectedProvider?.providerNumber"
:shopProviderDataFromParent="shopProviderData"
@shop-selected="onShopSelected"
@update:modelValue="onShopModelUpdated"
@updated-zipcode-ctu="setZipcodeCtu"
@update-shop-provider-data="shopProviderData = $event"
@updated-contains-military-base="setContainsMilitaryBase"
:zipCode="zipCode"
:isDisplayed="isShopQuestionDisplayed"
@updated-serviceability="setServiceabilityDetails"
:zipCodeFromParent="zipCode"
cmsWidgetName="YourSafeliteShopWidget" />
<contentGroupModal ref="RecalModal" cmsWidgetName="RecalModal" />
@ -127,6 +117,8 @@
<div class="col-md-6 col-xl-4">
<locationAlerts cmsWidgetPrefix="LocationAlert-" ref="locationAlerts" />
<datePicker
:currentZip="zipCode"
:currentProviderNumber="selectedProvider?.providerNumber"
v-show="appointmentType"
customComponentId="dateQuestion"
selectableDatesSetting="custom"
@ -152,7 +144,7 @@
<navbar
cmsWidgetName="FunnelFooterWidget"
ref="navbar"
:isForwardActionDisabled="!meta.valid"
:isForwardActionDisabled="isForwardActionDisabled"
@back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction" />
</div>
@ -170,7 +162,6 @@ import shopQuestion from "@/layouts/service-location/shop-question/shop-question
import shopQuestionPopup from "@/layouts/service-location/shop-question/shop-question-popup";
import buttonQuestion from "@/digital-components/button-question/button-question.vue";
import shopListButton from "@/layouts/service-location/shop-question/shop-list-button/shop-list-button";
import { getAvailabilityRating } from "@/layouts/service-location/helpers/service-location-helper/service-location-helper";
import funnelHeader from "@/fmg-components/funnel-header/funnel-header";
import navbar from "@/fmg-components/nav-bar/nav-bar";
@ -258,6 +249,8 @@ const getScheduleApiResponse = async ({
endDateString,
providerNumber,
zipCode,
includeMobileTimeSlots = true,
includeInshopTimeSlots = true,
}) => {
const apiEndDateLimit = sumDateString(startDateString, TIME_SLOTS_CALL_DAYS_LIMIT);
const difference = calcDaysBetweenDates(startDateString, endDateString);
@ -301,8 +294,8 @@ const getScheduleApiResponse = async ({
},
};
if (apiStartDate < apiEndDate) {
storeActionConfigs.push(storeActionConfigInshop);
storeActionConfigs.push(storeActionConfigMobile);
if (includeInshopTimeSlots) storeActionConfigs.push(storeActionConfigInshop);
if (includeMobileTimeSlots) storeActionConfigs.push(storeActionConfigMobile);
}
}
@ -338,6 +331,8 @@ const getScheduleApiResponse = async ({
...mobileTimeSlotsData.days,
...mobileTimeSlotsResponse.data.days,
];
mobileTimeSlotsData.type = mobileTimeSlotsResponse.data.type;
mobileTimeSlotsData.zipCode = mobileTimeSlotsResponse.data.zipCode;
} else {
const inshopTimeSlotsResponse =
await baseMixin.methods.dispatchStoreActionWithLogging(
@ -355,6 +350,9 @@ const getScheduleApiResponse = async ({
...inshopTimeSlotsData.days,
...inshopTimeSlotsResponse.data.days,
];
inshopTimeSlotsData.type = inshopTimeSlotsResponse.data.type;
inshopTimeSlotsData.providerNumber =
inshopTimeSlotsResponse.data.providerNumber;
}
})
);
@ -384,7 +382,7 @@ export default {
name: "schedule",
data() {
return {
selectedDate: this.getSelectedDate(),
selectedDate: this.getSelectedDateFromStore(),
selectedTimeSlotInfo: this.getSelectedTimeSlotInfo(),
mobilePremiumAppointmentFee: null,
waitListRequested: null,
@ -397,6 +395,7 @@ export default {
showPricingByDay: null,
selectableDatesInshop: [],
selectableDatesMobile: [],
preSelectedDate: null,
streetAddress: this.getServiceAddressFromStore(),
apartmentNumberOrBusinessName: this.getServiceAddress2FromStore(),
@ -412,9 +411,9 @@ export default {
isRecalibrationServiceableDropoff: null,
isGlassServiceableMobile: null,
isRecalibrationServiceableMobile: null,
appointmentType: this.getAppointmentType(),
appointmentTypeFromAppointmentTypeQuestion: this.getAppointmentType(),
selectedProvider: this.getSelectedProvider(),
appointmentType: this.getAppointmentTypeFromStore(),
appointmentTypeFromAppointmentTypeQuestion: this.getAppointmentTypeFromStore(),
selectedProvider: this.getSelectedProviderFromStore(),
mobileFeePart: null,
recycleFeePart: null,
zipContainsMilitaryBase: false,
@ -422,8 +421,8 @@ export default {
billToAccountNumber: null,
shopProviderData: null,
navigatingForward: false,
isMobileAddressValid: true,
shopListButton: shopListButton,
lastSelectedInshopOrDropoffProvider: null,
};
},
async beforeRouteEnter(to, from, next) {
@ -467,11 +466,11 @@ export default {
let appointmentType = store.getters.order.serviceLocation.appointmentType;
// Check to see if date should be pre-selected
let preSelectedSlot = await store.getters.order.schedule;
let scheduleFromStore = await store.getters.order.schedule;
let preSelectedDate;
if (preSelectedSlot.date && preSelectedSlot.date.length > 0) {
preSelectedDate = preSelectedSlot.date;
if (scheduleFromStore.date && scheduleFromStore.date.length > 0) {
preSelectedDate = scheduleFromStore.date;
if (appointmentType === AppointmentTypeStrings.MOBILE) {
preSelectedDate += "-mobile";
@ -480,7 +479,7 @@ export default {
// Check to see if pre-selected date should have pricing by day upcharge
if (showPricingByDay) {
// is this preSelectedDate a higher priced pricingByDay day?
const dayIndex = convertDateStringToDate(preSelectedSlot?.date).getDay();
const dayIndex = convertDateStringToDate(scheduleFromStore?.date).getDay();
const dayObject = DAYS_OF_WEEK[dayIndex];
if (dayObject.isPricingByDayUpchargeDay) {
includePricingByDayUpcharge = true;
@ -504,18 +503,9 @@ export default {
"service-location"
);
const mobileFeePartPromise = getPricedMobileFeePart(serviceZipCode, "service-location");
const shopProviderData = await getShopProviderData(serviceZipCode); // shopQuestion.methods.loadInitialData(serviceZipCode);
const shopProviderData = await getShopProviderData(serviceZipCode);
const providerNumber = shopProviderData?.data?.shopProviders[0]?.providerNumber;
const datePickerInitialDataPromise = await datePicker.methods.loadInitialData({
// setup config options for date-picker
selectableDatesSetting: "custom",
initialViewRowsToShow: NUMBER_OF_CALENDAR_ROWS_TO_SHOW_FOR_INITIAL_VIEW,
getSelectableDatesCallback: getScheduleApiResponse,
preSelectedDate: preSelectedDate,
providerNumber: providerNumber,
});
// Get pricingByDayUpcharge needed for Pricing By Day
const pricingByDayUpchargePartPromise = showPricingByDay
? getPricingByDayPartWithPrice()
@ -551,10 +541,6 @@ export default {
resultKey: "alertReasons",
promise: alertReasonsPromise,
},
{
resultKey: "datePickerInitialData",
promise: datePickerInitialDataPromise,
},
{
resultKey: "pricingByDayUpchargePart",
promise: pricingByDayUpchargePartPromise,
@ -579,6 +565,7 @@ export default {
const resultMap = await settleAllPromises(promiseResultMap);
// add pricing by day data
const pricingByDayUpcharge =
showPricingByDay && resultMap.pricingByDayUpchargePart
? await baseMixin.methods.getTotalLineItemPrice(
@ -587,31 +574,21 @@ export default {
)
: null;
const datePickerInitialData = resultMap.datePickerInitialData;
datePickerInitialData.pricingByDayBasePrice = pricingByDayBasePrice;
datePickerInitialData.pricingByDayUpcharge = pricingByDayUpcharge;
// Call the "next" function to complete the transition to this page.
next((vm) => {
vm.setCmsContent(resultMap.cmsContent);
vm.$refs.datePicker.initializeComponent(datePickerInitialData);
vm.$refs.locationAlerts.initializeComponent(resultMap.alertReasons);
vm.selectableDatesInshop =
datePickerInitialData.initialShopTimeSlotsResponse.inshopTimeSlotsData;
vm.selectableDatesMobile =
datePickerInitialData.initialShopTimeSlotsResponse.mobileTimeSlotsData;
vm.mobilePremiumAppointmentFee = resultMap.premiumFeeWithPrice
? resultMap.premiumFeeWithPrice[0]
: null;
vm.updateFooterButtonText(vm.selectedTimeSlotInfo);
vm.setDisplayWaitList();
vm.pricingByDayUpchargeLineItem = resultMap.pricingByDayUpchargePart;
vm.includePricingByDayUpcharge = includePricingByDayUpcharge;
vm.isPricingByDayExperiment = isPricingByDayExperiment;
vm.pricingByDayBasePrice = pricingByDayBasePrice;
vm.pricingByDayUpcharge = pricingByDayUpcharge;
vm.showPricingByDay = showPricingByDay;
vm.preSelectedDate = preSelectedDate;
vm.appointmentType = appointmentType;
vm.setData(
resultMap.zipCodeData,
@ -619,39 +596,7 @@ export default {
resultMap.mobileFeePart,
shopProviderData.data
);
});
},
mounted() {
this.$nextTick(() => {
if (!this.selectedDate) {
// if no date is preselected on load, then select the first available
if (this.isMobileSelected) {
this.selectedDate = returnFirstDate(this.selectableDatesMobile);
} else {
this.selectedDate = returnFirstDate(this.selectableDatesInshop);
}
// if there is still no selected date, then load more and try again
if (!this.selectedDate) {
setTimeout(() => {
this.$refs.datePicker.showAnotherMonth().then((moreSelectableDates) => {
// update global
this.selectableDatesInshop.days =
moreSelectableDates.inshopTimeSlotsData.days;
this.selectableDatesMobile.days =
moreSelectableDates.mobileTimeSlotsData.days;
if (this.isMobileSelected) {
this.selectedDate = returnFirstDate(this.selectableDatesMobile);
} else {
this.selectedDate = returnFirstDate(this.selectableDatesInshop);
}
this.setDisplayWaitList();
});
}, 50);
}
}
vm.initializeDatePicker();
});
},
computed: {
@ -664,17 +609,7 @@ export default {
};
},
set: function (newValue) {
if (newValue.zipCode !== this.zipCode) {
this.resetMobileLocation();
this.appointmentType = null;
this.selectedProvider = new Provider();
}
this.state = newValue.state;
this.zipCode = newValue.zipCode;
this.zipCodeCtu = newValue.zipCodeCtu;
this.$nextTick();
this.handleZipCodeChange(newValue);
},
},
isMobileSelected() {
@ -806,20 +741,10 @@ export default {
return store.getters.order.policy.isNoComp;
},
isForwardActionDisabled() {
return this.displayNoShopsAlert || !this.isMobileAddressValid;
return !this.selectedTimeSlotInfo?.timeSlot?.routeCode || !this.appointmentType;
},
additionalButtonData() {
const startDate = new Date();
const endDate = new Date();
endDate.setDate(startDate.getDate() + 6);
const formattedStartDate = startDate.toISOString().split("T")[0];
const formattedEndDate = endDate.toISOString().split("T")[0];
return {
availabilityRatingCallback: getAvailabilityRating,
startDate: formattedStartDate,
endDate: formattedEndDate,
shopAppointmentType: this.appointmentType,
};
},
@ -831,7 +756,11 @@ export default {
});
};
if (this.selectedProvider && this.selectedProvider.address) {
if (
this.selectedProvider &&
this.selectedProvider.address &&
this.selectedProvider?.providerNumber
) {
const provider = this.shopProviderData?.shopProviders?.find(
(p) => p.providerNumber === this.selectedProvider?.providerNumber
);
@ -983,13 +912,13 @@ export default {
if (shopProviderData) {
this.shopProviderData = shopProviderData;
this.updateSelectedProvider(); // ensure that page loads with non-null selectedProvider
}
var gaLabel = this.GaLabels.NO;
if (this.isServiceableMobile) {
gaLabel = this.GaLabels.YES;
}
this.pushEventToGA(
this.GaCategories.APPOINTMENT,
this.GaActions.MOBILE_AVAILABLE,
@ -1038,10 +967,10 @@ export default {
getIsVehicleProtectedFromStore() {
return store.getters.order.serviceLocation.isVehicleProtected;
},
getAppointmentType() {
getAppointmentTypeFromStore() {
return store.getters.order.serviceLocation.appointmentType;
},
getSelectedProvider() {
getSelectedProviderFromStore() {
return store.getters.order.serviceLocation.provider;
},
resetMobileLocation() {
@ -1051,6 +980,17 @@ export default {
this.isVehicleProtected = null;
},
resetWaitlist() {
this.dispatchStoreAction(this.storeActions.SAVE_WAITLIST_REQUESTED, false, false);
this.waitListRequested = false;
},
onServiceZipChanged() {
this.resetWaitlist();
this.appointmentType = null;
this.selectedProvider = null;
this.appointmentTypeFromAppointmentTypeQuestion = null;
this.preSelectedDate = null;
},
setServiceabilityDetails(serviceabilityDetails) {
this.isGlassServiceableInshop = serviceabilityDetails.isGlassServiceableInshop;
this.isRecalibrationServiceableInshop =
@ -1164,43 +1104,22 @@ export default {
startTime: "08:00",
};
},
onShopSelected(providerObject) {
const current = this.shopProviderData?.shopProviders?.find(
(p) => p.providerNumber === this.selectedProvider?.providerNumber
);
if (current) {
this.selectedProvider = current;
}
onShopSelected(shopQuestionPopUpData) {
this.resetWaitlist();
this.preSelectedDate = null;
this.shopProviderData = shopQuestionPopUpData.shopProviderData;
const selectedProvider = this.shopProviderData.shopProviders.find((shopProvider) => {
return shopProvider.providerNumber === shopQuestionPopUpData.selectedProviderNumber;
});
let provider = this.shopProviderData?.shopProviders?.find(
(p) => p.providerNumber === providerObject?.providerNumber
);
this.selectedProvider = provider;
if (providerObject && providerObject.address) {
this.zipCode = providerObject.searchedZip;
this.state = providerObject.searchedState;
}
},
setZipcodeCtu(zipcodeCtu) {
this.zipCodeCtu = zipcodeCtu;
},
onShopModelUpdated(newModel) {
if (newModel.zipCode) {
this.zipCode = newModel.zipCode;
}
if (newModel.zipCodeCtu) {
this.zipCodeCtu = newModel.zipCodeCtu;
}
if (newModel.state) {
this.state = newModel.state;
}
if (this.shopProviderData && newModel.selectedProviderNumber) {
const provider = this.shopProviderData.shopProviders.find(
(p) => p.providerNumber === newModel.selectedProviderNumber
// No zipCodeData comes back if zip was not changed
if (shopQuestionPopUpData.zipCodeData) {
this.updateSelectedProviderAndZipCode(
shopQuestionPopUpData.zipCodeData,
selectedProvider
);
if (provider) {
this.selectedProvider = provider;
}
} else {
this.updateSelectedProvider(selectedProvider);
}
},
async getMoreScheduleData(startDate, endDate) {
@ -1208,8 +1127,10 @@ export default {
const moreShopTimeSlots = await getScheduleApiResponse({
startDateString: startDate,
endDateString: endDate,
providerNumber: this.selectedProvider.providerNumber,
providerNumber: this.selectedProvider?.providerNumber,
zipCode: this.zipCode,
includeMobileTimeSlots: this.isServiceableMobile,
includeInshopTimeSlots: this.isServiceableInshop || this.isServiceableDropoff,
});
// ADD API CALL RESULTS TO EXISTING DATE DATA
@ -1222,11 +1143,71 @@ export default {
return moreShopTimeSlots;
},
async initializeDatePicker() {
this.selectedDate = null;
const datePickerInitialData = await this.$refs.datePicker.loadInitialData({
// setup config options for date-picker
selectableDatesSetting: "custom",
initialViewRowsToShow: NUMBER_OF_CALENDAR_ROWS_TO_SHOW_FOR_INITIAL_VIEW,
getSelectableDatesCallback: getScheduleApiResponse,
preSelectedDate: this.preSelectedDate,
providerNumber: this.selectedProvider?.providerNumber,
zipCode: this.zipCode,
includeMobileTimeSlots: this.isServiceableMobile,
includeInshopTimeSlots: this.isServiceableInshop || this.isServiceableDropoff,
});
datePickerInitialData.pricingByDayBasePrice = this.pricingByDayBasePrice;
datePickerInitialData.pricingByDayUpcharge = this.pricingByDayUpcharge;
await this.$refs.datePicker.initializeComponent(datePickerInitialData);
this.selectableDatesInshop =
datePickerInitialData.initialShopTimeSlotsResponse.inshopTimeSlotsData;
this.selectableDatesMobile =
datePickerInitialData.initialShopTimeSlotsResponse.mobileTimeSlotsData;
this.setDisplayWaitList();
if (this.preSelectedDate) this.selectedDate = this.preSelectedDate;
if (!this.selectedDate) {
// if no date is preselected on load, then select the first available
let selectedDateMobile = this.getSelectedDateForMobile();
let selectedDateInshop = this.getSelectedDateForInshop();
// if there is still no selected date, then load more and try again
if (
(this.isServiceableMobile && !selectedDateMobile) ||
(this.isServiceableInshop && !selectedDateInshop) ||
(this.isServiceableDropoff && !selectedDateInshop)
) {
await this.$nextTick();
await this.$refs.datePicker.showAnotherMonth();
// update all dates
// > CHLOE HERD 7/22 -- CASH-1207
// > Do not update the available dates again here;
// > they have already been updated by `showAnotherMonth`.
// > Doing so will likely add or remove dates,
// > desyncing the schedule page and the date-picker.
this.setDisplayWaitList();
}
await this.$nextTick();
if (this.isMobileSelected) {
this.selectedDate = this.getSelectedDateForMobile();
} else {
this.selectedDate = this.getSelectedDateForInshop();
}
}
},
getScheduleApiResponse,
getServiceZipCtuCodeFromStore() {
return store.getters.order.serviceLocation.zipCodeCtu;
},
getSelectedDate() {
getSelectedDateFromStore() {
let isMobileSelected = this.isMobileSelected;
if (isMobileSelected === undefined) {
isMobileSelected = this.appointmentType === AppointmentTypeStrings.MOBILE;
@ -1270,7 +1251,10 @@ export default {
this.appointmentType === AppointmentTypeStrings.DROP_OFF
) {
if (
!timeSlotInfo.timeSlot.routeCode.includes(RouteCodeFlags.ALL_DAY_DROP_OFF)
!timeSlotInfo.timeSlot.routeCode.includes(
RouteCodeFlags.ALL_DAY_DROP_OFF
) &&
!timeSlotInfo.timeSlot.routeCode.includes(RouteCodeFlags.OVERNIGHT_DROP_OFF)
) {
navbarButtonText += ` at ${this.getDisplayTextForMilitaryTime(
timeSlotInfo.timeSlot.startTime
@ -1328,19 +1312,20 @@ export default {
);
}
},
showLoadingModal() {
this.$refs.loadingModal.showModal();
},
async forwardButtonAction() {
this.navigatingForward = true;
var ctu = this.zipCodeCtu;
if (
this.appointmentType == AppointmentTypeStrings.MOBILE &&
this.selectedProvider &&
this.selectedProvider.address
) {
this.selectedProvider.address.streetAddress = null;
this.selectedProvider.address.city = null;
this.selectedProvider.address.state = null;
this.selectedProvider.address.zipCode = null;
this.selectedProvider.address.zipCodeCtu = null;
if (this.appointmentType === AppointmentTypeStrings.MOBILE) {
if (this.selectedProvider && this.selectedProvider.address) {
this.selectedProvider.address.streetAddress = null;
this.selectedProvider.address.city = null;
this.selectedProvider.address.state = null;
this.selectedProvider.address.zipCode = null;
this.selectedProvider.address.zipCodeCtu = null;
}
} else {
if (this.zipCodeCtu != this.selectedProvider.address.zipCodeCtu) {
ctu = this.selectedProvider.address.zipCodeCtu;
@ -1420,12 +1405,15 @@ export default {
}
if (this.selectedTimeSlotInfo.timeSlot.jobMinMinutes == null) {
this.selectedTimeSlotInfo.timeSlot.jobMinMinutes = this.isMobileSelected
? this.selectableDatesMobile.estimatedServiceMinutesMinimum?.toString()
: this.selectableDatesInshop.estimatedServiceMinutesMinimum?.toString();
this.selectedTimeSlotInfo.timeSlot.jobMaxMinutes = this.isMobileSelected
? this.selectableDatesMobile.estimatedServiceMinutesMaximum?.toString()
: this.selectableDatesInshop.estimatedServiceMinutesMaximum?.toString();
this.selectedTimeSlotInfo = this.getSelectedTimeSlotInfo();
if (this.selectedTimeSlotInfo.timeSlot.jobMinMinutes == null) {
this.selectedTimeSlotInfo.timeSlot.jobMinMinutes = this.isMobileSelected
? this.selectableDatesMobile.estimatedServiceMinutesMinimum?.toString()
: this.selectableDatesInshop.estimatedServiceMinutesMinimum?.toString();
this.selectedTimeSlotInfo.timeSlot.jobMaxMinutes = this.isMobileSelected
? this.selectableDatesMobile.estimatedServiceMinutesMaximum?.toString()
: this.selectableDatesInshop.estimatedServiceMinutesMaximum?.toString();
}
}
this.dispatchStoreAction(
@ -1454,19 +1442,19 @@ export default {
this.pageName
);
}
this.showLoadingModal();
},
setDisplayWaitList() {
const dateString = this.isMobileSelected
? this.selectableDatesMobile?.days[0]?.date
: this.selectableDatesInshop?.days[0]?.date;
if (
experimentMixin.methods.hasSettingEqualTo(
experimentSettings.DISPLAY_WAITLIST,
"true"
) &&
this.selectableDatesInshop.days[0] &&
this.selectableDatesMobile.days[0]
dateString
) {
const dateString = this.isMobileSelected
? this.selectableDatesMobile.days[0].date
: this.selectableDatesInshop.days[0].date;
const [year, month, day] = dateString.split("-").map(Number);
const targetDate = new Date(year, month - 1, day);
const currentDate = new Date();
@ -1568,8 +1556,17 @@ export default {
},
updateTimeSlot(timeSlotObj) {
this.selectedTimeSlotInfo = timeSlotObj;
if (this.appointmentType !== AppointmentTypeStrings.MOBILE) {
if (
this.appointmentType !== AppointmentTypeStrings.MOBILE &&
this.appointmentType !== null
) {
// update apptType based on routeCode to determine if it should be dropoff or inshop
debugLog(`Updating appointment type based on route code.`);
debugLog(`Route code =`, timeSlotObj.timeSlot.routeCode);
debugLog(
`Parsed type = `,
this.getInShopOrDropOffApptType(timeSlotObj.timeSlot.routeCode)
);
this.appointmentType = this.getInShopOrDropOffApptType(
timeSlotObj.timeSlot.routeCode
);
@ -1580,56 +1577,128 @@ export default {
? AppointmentTypeStrings.DROP_OFF
: AppointmentTypeStrings.IN_SHOP;
},
getSelectedDate() {
let dateToSelect;
if (this.isMobileSelected) {
dateToSelect = returnFirstDate(this.selectableDatesMobile);
} else {
dateToSelect = returnFirstDate(this.selectableDatesInshop);
}
if (!dateToSelect) return null;
return this.isMobileSelected ? dateToSelect + "-mobile" : dateToSelect;
},
getSelectedDateForMobile() {
let dateToSelect = returnFirstDate(this.selectableDatesMobile);
if (!dateToSelect) return null;
return dateToSelect + "-mobile";
},
getSelectedDateForInshop() {
let dateToSelect = returnFirstDate(this.selectableDatesInshop);
if (!dateToSelect) return null;
return dateToSelect;
},
resetSelectedProvider() {
this.selectedProvider = new Provider();
this.updateSelectedProvider();
},
updateSelectedProvider(newProvider) {
if (newProvider) {
this.selectedProvider = newProvider;
this.initializeDatePicker();
} else if (this.appointmentType === this.appointmentTypeStrings.MOBILE) {
this.selectedProvider = {
providerNumber: this.shopProviderData.mobileProviderNumber.toString(),
address: {
streetAddress: null,
city: null,
state: null,
zipCode: null,
zipCodeCtu: null,
},
};
} else if (
!this.selectedProvider?.address?.streetAddress &&
this.shopProviderData?.shopProviders?.length
) {
this.selectedProvider = this.shopProviderData.shopProviders[0];
}
},
handleZipCodeChange(newZipCode) {
this.resetMobileLocation();
getShopProviderData(newZipCode.zipCode).then((result) => {
this.shopProviderData = result.data;
this.resetSelectedProvider();
this.state = newZipCode.state;
this.zipCode = newZipCode.zipCode;
this.zipCodeCtu = newZipCode.zipCodeCtu;
this.selectedDate = null;
this.initializeDatePicker();
});
},
updateSelectedProviderAndZipCode(newZipCode, newProvider) {
this.resetMobileLocation();
this.state = newZipCode.state;
this.zipCode = newZipCode.zipCode;
this.zipCodeCtu = newZipCode.zipCodeCtu;
this.zipContainsMilitaryBase = newZipCode.containsMilitaryBase;
this.selectedDate = null;
this.selectedProvider = newProvider;
this.initializeDatePicker();
},
handleAppointmentTypeChange(newAppointmentType) {
this.updateFooterButtonText();
if (newAppointmentType === AppointmentTypeStrings.MOBILE) {
// Remember last shop selected if previous selection was inshop/dropoff
if (
(this.appointmentType == AppointmentTypeStrings.IN_SHOP ||
AppointmentTypeStrings.DROP_OFF ||
AppointmentTypeStrings.IN_SHOP_OR_DROP_OFF) &&
this.selectedProvider
) {
this.lastSelectedInshopOrDropoffProvider = this.selectedProvider;
}
this.appointmentType = AppointmentTypeStrings.MOBILE;
this.updateSelectedProvider();
} else if (newAppointmentType) {
if (this.appointmentType != AppointmentTypeStrings.MOBILE) {
// Clear last shop selected if appointment type was changed in any manner other than from Mobile
this.lastSelectedInshopOrDropoffProvider = null;
}
if (this.selectedTimeSlotInfo?.timeSlot?.routeCode) {
// update appointmentType based on routeCode to determine if it should be dropoff or inshop
this.appointmentType = this.getInShopOrDropOffApptType(
this.selectedTimeSlotInfo.timeSlot.routeCode
);
} else {
this.appointmentType = AppointmentTypeStrings.IN_SHOP_OR_DROP_OFF;
}
// make sure a selectedProvider exists
this.updateSelectedProvider(this.lastSelectedInshopOrDropoffProvider);
} else {
this.appointmentType = null;
}
this.selectedDate = this.getSelectedDate();
this.setDisplayWaitList();
},
},
watch: {
zipCode: {
handler(newValue) {
if (!this.navigatingForward) {
getShopProviderData(this.zipCode).then(async (result) => {
this.shopProviderData = result.data;
if (this.appointmentType === "Mobile") {
this.selectedProvider = new Provider(
this.shopProviderData.mobileProviderNumber
);
} else {
this.isMobileAddressValid = true;
}
});
}
},
},
appointmentType: {
handler(newValue, oldValue) {
if (newValue === AppointmentTypeStrings.MOBILE) {
this.selectedProvider = new Provider(
this.shopProviderData.mobileProviderNumber
);
this.isMobileSelected = true;
} else {
this.selectedProvider = this.shopProviderData.shopProviders[0];
}
},
},
appointmentTypeFromAppointmentTypeQuestion: {
handler(newValue, oldValue) {
if (newValue === AppointmentTypeStrings.MOBILE) {
this.appointmentType = AppointmentTypeStrings.MOBILE;
} else {
if (this.selectedTimeSlotInfo?.timeSlot?.routeCode) {
// update appointmentType based on routeCode to determine if it should be dropoff or inshop
this.appointmentType = this.getInShopOrDropOffApptType(
this.selectedTimeSlotInfo.timeSlot.routeCode
);
} else {
this.appointmentType = AppointmentTypeStrings.IN_SHOP_OR_DROP_OFF;
}
}
this.handleAppointmentTypeChange(newValue);
},
},
selectedDate(newValue, oldValue) {
// Clear time slot selection if date selected changes
if (newValue !== oldValue) {
const selectedDate = this.getSelectedDateFromStore();
if (oldValue && this.appointmentType === AppointmentTypeStrings.MOBILE) {
oldValue = `${oldValue}-mobile`;
}
const hasValueChanged = newValue !== oldValue;
const isDateDifferent = (newValue || oldValue) !== selectedDate;
if (hasValueChanged && isDateDifferent) {
this.selectedTimeSlotInfo = {
timeSlot: {
date: null,

View file

@ -7,7 +7,8 @@
:answers="answersForDropOffQuestion"
isRequired
insertOrLabelBeforeFinalOption
groupName="chooseDropOffOrInshop">
groupName="chooseDropOffOrInshop"
validationRules="time-slot-required">
<div>
<alert
v-if="shouldDisplayDropOffAlert"
@ -40,7 +41,7 @@
v-if="supplementalInformationBlock"
v-html="supplementalInformationBlock" />
<div
v-if="hasWaitListExperiment && displayWaitListFeature"
v-if="hasWaitListExperiment && displayWaitListFeature && selectedDate"
class="mt-5 bg-light rounded waitlist">
<textBlock
cmsWidgetName="WaitListLabelWidget"
@ -53,7 +54,10 @@
v-model="waitListRequested"
@click="waitListChecked" />
</div>
<div v-if="waitListRequested" class="rounded waitlist-success" ref="waitlistSuccessMessage">
<div
v-if="waitListRequested && displayWaitList && selectedDate"
class="rounded waitlist-success"
ref="waitlistSuccessMessage">
<img :src="waitListSuccessImage" class="success-image" />
<span v-html="waitListSuccessText" class="success-text"></span>
</div>
@ -76,6 +80,7 @@ import alert from "@/ux-components/alert/alert.vue";
import experimentMixin from "@/mixins/experiment-mixin";
import { experimentSettings } from "@/constants/experiments";
import store from "@/store";
import { storeActions } from "@/constants/store-actions";
// Helpers
import { deepClone } from "@/helpers/object-helper";
@ -144,7 +149,6 @@ export default {
return {
selectedRouteCode: null,
timeSlotModalListButton: timeSlotModalListButton,
waitListRequested: this.getWaitListRequestedFromStore(),
selectedAnswerForDropOffOrInshop: null,
selectedAnswerForTimeSlots: null,
};
@ -222,7 +226,7 @@ export default {
},
premiumAppointmentButtonText() {
return this.getCmsContent(
this.dropOffOrPickATimeQuestionCmsWidgetName,
this.mobilePremiumCmsWidgetName,
cmsWidgetFieldMappings.TIME_SLOT_BUTTON
);
},
@ -345,6 +349,14 @@ export default {
waitListSuccessText() {
return this.getCmsContent("WaitListSuccessWidget", "BodyText");
},
waitListRequested: {
get() {
return this.$store.getters.order.customer?.waitListRequested;
},
set(value) {
this.dispatchStoreAction(storeActions.SAVE_WAITLIST_REQUESTED, value);
},
},
isMobileAppointment() {
return this.appointmentType === AppointmentTypeStrings.MOBILE;
},
@ -432,6 +444,8 @@ export default {
);
}
this.resetSelectedTimeSlotIfDateChange(timeSlotsForSelectedDate);
return availableTimeSlots;
},
getPremiumAppointmentTimeSlot(timeSlotData) {
@ -468,7 +482,7 @@ export default {
return store.getters.order.customer?.waitListRequested;
},
autoSelectTimeSlotIfOnlyOneIsAvailable() {
const numberOfOptions = this.timeSlotsForSelectedDate.timeSlots?.length;
const numberOfOptions = this.timeSlotsForSelectedDate?.timeSlots?.length;
if (numberOfOptions === 1) {
if (this.availableTimeSlots.length > 0) {
this.selectedAnswerForTimeSlots = this.availableTimeSlots[0].value;
@ -527,6 +541,23 @@ export default {
successMessageElement.scrollIntoView({ behavior: "smooth" });
}
},
resetSelectedTimeSlotIfDateChange(timeSlotsForSelectedDate) {
if (this.selectedRouteCode) {
const selectedRouteCodeString = this.selectedRouteCode.replace(
PREMIUM_TIME_SLOT_ID_FLAG,
""
);
const matchingTimeSlot = timeSlotsForSelectedDate.find((slot) => {
return slot.id === selectedRouteCodeString;
});
if (!matchingTimeSlot) {
this.resetSelectedTimeSlot();
}
}
},
resetSelectedTimeSlot() {
this.selectedRouteCode = null;
},
},
watch: {
waitListRequested(newVal) {
@ -544,7 +575,9 @@ export default {
},
selectedDate: {
handler() {
this.selectedRouteCode = null;
this.resetSelectedTimeSlot();
this.selectedAnswerForDropOffOrInshop = null;
this.selectedAnswerForTimeSlots = null;
this.autoSelectTimeSlotIfOnlyOneIsAvailable();
},
},

View file

@ -73,7 +73,11 @@ export default {
},
selectedValue: {
get: function () {
return this.modelValue;
if (this.modelValue === AppointmentTypeStrings.DROP_OFF) {
return AppointmentTypeStrings.IN_SHOP;
} else {
return this.modelValue;
}
},
set: function (newValue) {
this.$emit("update:modelValue", newValue);

View file

@ -122,35 +122,6 @@ export async function getShopProviderData(serviceZipCode) {
);
}
export async function getAvailabilityRating(
startDate,
endDate,
shopAppointmentType,
providerNumber
) {
// For a given shop provider number and date range, get the appointment time slots available
const shopTimeSlots = await baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.GET_SHOP_TIME_SLOTS,
{
providerNumber: providerNumber,
startDate: startDate,
endDate: endDate,
shopAppointmentType: shopAppointmentType,
},
"service-location",
false
);
const numberOfDaysToEvaluate = 2;
const isGoodAvailability =
shopTimeSlots.data.days.filter((x) => x.timeSlots.length > 0).length >=
numberOfDaysToEvaluate;
const shopStatus = isGoodAvailability ? "high" : "low";
return shopStatus;
}
export async function getClosestApplicableShops(serviceZipCode, carId, pageNameToLog) {
if (!serviceZipCode) {
return null;

View file

@ -1,8 +1,4 @@
import {
getPricedMobileFeePart,
getServiceabilityDetails,
getAvailabilityRating,
} from "./service-location-helper";
import { getPricedMobileFeePart, getServiceabilityDetails } from "./service-location-helper";
import { storeActions } from "@/constants/store-actions";
jest.mock("@/store", () => ({
@ -320,38 +316,4 @@ describe("service-location-helper.js", () => {
expect(result).toEqual(expected);
});
});
describe("getAvailabilityRating", () => {
it("Should return a 'high' rating", async () => {
// Arrange
const providerNumber = "0000001";
const expected = "high";
// Act
const result = await getAvailabilityRating(
"2023-06-30",
"2023-07-06",
"Inshop",
providerNumber
);
// Assert
expect(result).toEqual(expected);
});
it("Should return a 'low' rating", async () => {
// Arrange
const providerNumber = "0000000";
const expected = "low";
// Act
const result = await getAvailabilityRating(
"2023-06-30",
"2023-07-06",
"Inshop",
providerNumber
);
// Assert
expect(result).toEqual(expected);
});
});
});

View file

@ -131,10 +131,8 @@
import alert from "@/ux-components/alert/alert";
import serviceZipModalQuestion from "@/layouts/service-location/service-zip-modal-question/service-zip-modal-question";
import appointmentTypeQuestion from "@/layouts/service-location/appointment-type-question/appointment-type-question";
import shopQuestion from "@/layouts/service-location/shop-question/shop-question";
import shopQuestionPopup from "@/layouts/service-location/shop-question/shop-question-popup";
import shopListButton from "@/layouts/service-location/shop-question/shop-list-button/shop-list-button";
import { getAvailabilityRating } from "@/layouts/service-location/helpers/service-location-helper/service-location-helper";
import funnelHeader from "@/fmg-components/funnel-header/funnel-header";
import navbar from "@/fmg-components/nav-bar/nav-bar";
@ -145,7 +143,6 @@ import contentGroupModal from "@/fmg-components/content-group-modal/content-grou
import textBlock from "@/digital-components/text-block/text-block";
// Supporting files
import baseMixin from "@/mixins/base-mixin.js";
import experimentMixin from "@/mixins/experiment-mixin.js";
import { experimentSettings } from "@/constants/experiments";
import { AppointmentTypeStrings } from "@/constants/schedule-constants";
@ -432,17 +429,7 @@ export default {
return this.displayNoShopsAlert || !this.isMobileAddressValid;
},
additionalButtonData() {
const startDate = new Date();
const endDate = new Date();
endDate.setDate(startDate.getDate() + 6);
const formattedStartDate = startDate.toISOString().split("T")[0];
const formattedEndDate = endDate.toISOString().split("T")[0];
return {
availabilityRatingCallback: getAvailabilityRating,
startDate: formattedStartDate,
endDate: formattedEndDate,
shopAppointmentType: this.selectedAppointmentType,
};
},

View file

@ -219,7 +219,7 @@ export default {
this.$emit("updated-serviceability", serviceabilityDetails.data);
this.$emit("updated-contains-military-base", zipCodeData.containsMilitaryBase);
this.$emit("updated-bill-to-account-number", billToAccountNumber);
this.$emit("service-zip-changed-from-modal");
// update the page level model
this.$emit("update:modelValue", this.internalModel);

View file

@ -8,7 +8,6 @@
cornerStyle="rounded"
mask="#####"
:includeSearchIcon="includeSearchIcon"
@search-icon-click="$emit('search-icon-click')"
:displayQuestionText="false"
:isRequired="isRequired"
:validationRules="computedValidationRules" />

View file

@ -58,15 +58,22 @@ export default {
beforeMount() {
if (this.displayAvailabilityIndicators) {
this.displayLoader();
const startDate = this.additionalButtonData.startDate;
const endDate = this.additionalButtonData.endDate;
const startDate = new Date();
const endDate = new Date();
endDate.setDate(startDate.getDate() + 6);
const formattedStartDate = startDate.toISOString().split("T")[0];
const formattedEndDate = endDate.toISOString().split("T")[0];
const shopAppointmentType = this.additionalButtonData.shopAppointmentType;
this.additionalButtonData
.availabilityRatingCallback(startDate, endDate, shopAppointmentType, this.value)
.then((data) => {
this.availabilityRating = data;
});
this.getAvailabilityRating(
formattedStartDate,
formattedEndDate,
shopAppointmentType,
this.value
).then((data) => {
this.availabilityRating = data;
});
}
},
data() {
@ -103,6 +110,29 @@ export default {
displayLoader() {
this.isLoaderDisplayed = true;
},
async getAvailabilityRating(startDate, endDate, shopAppointmentType, providerNumber) {
// For a given shop provider number and date range, get the appointment time slots available
const shopTimeSlots = await this.dispatchStoreActionWithLogging(
this.storeActions.GET_SHOP_TIME_SLOTS,
{
providerNumber: providerNumber,
startDate: startDate,
endDate: endDate,
shopAppointmentType: shopAppointmentType,
},
"service-location",
false
);
const numberOfDaysToEvaluate = 2;
const isGoodAvailability =
shopTimeSlots.data.days.filter((x) => x.timeSlots.length > 0).length >=
numberOfDaysToEvaluate;
const shopStatus = isGoodAvailability ? "high" : "low";
return shopStatus;
},
},
components: {
loader,

View file

@ -1,6 +1,6 @@
<template>
<transition name="fade" mode="out-in">
<div v-if="isDisplayed" class="shop-question" aria-live="polite">
<div class="shop-question" aria-live="polite">
<div class="modal-link">
<text-link
linkType="text"
@ -13,20 +13,17 @@
<modal
ref="shopQuestionModal"
:headerText="modalHeaderText"
:onModalOpenedCallback="onModalOpened"
:onModalClosedCallback="onModalClosed"
:onModalOpenedCallback="updateSelectedAnswer"
:onModalClosedCallback="resetZipCodeData"
:footerButtonText="modalFooterText"
:footerButtonDisabled="
displayInvalidZipAlert || displayNoShopsAlert || !selectedProviderNumber
"
:footerButtonDisabled="isModalFooterButtonDisabled"
@footer-button-event="setZipCodeAndShop">
<form @submit.prevent>
<serviceZipQuestion
ref="serviceZipQuestion"
class="shop-question-zipcode"
customInputId="serviceZipCode"
v-model="internalModel.zipCode"
v-on="{ 'textboxQuestionEvent.inputIdAssigned': onInputIdAssigned }"
v-model="localZipCode"
cmsWidgetName="ServiceZipQuestionWidget"
@search-icon-click="onSearchZip"
@keydown.enter="onSearchZip"
@ -34,43 +31,48 @@
:isRequired="false"
:validationRules="''" />
</form>
<alert
ref="alertInvalidZip"
v-if="displayInvalidZipAlert"
class="mb-4"
cmsWidgetName="AlertInvalidZipWidget"
alertClass="alert-danger"
v-bind:isDismissible="false" />
<alert
ref="alertNoShops"
class="my-5"
cmsWidgetName="AlertNoShopsWidget"
v-if="displayNoShopsAlert"
alertClass="alert-warning" />
<div v-if="!displayInvalidZipAlert">
<buttonQuestion
ref="buttonQuestion"
buttonTypeString="shopListButton"
:buttonTypeObject="shopListButton"
class="radioQuestion"
:questionText="questionText"
:answers="answers"
groupName="chooseShop"
textPosition="text-start"
v-model="selectedProviderNumber"
isRequired
:validationRules="this.selectedProviderNumber ? '' : 'option-required'" />
<div class="show-more-shops-link">
<textLink
v-if="displaySeeMoreLocationsLink"
ref="showMoreShopsLink"
id="showMoreShopsId2"
cmsWidgetName="ShowMoreShopsLinkWidget"
linkType="text"
:text="showMoreShopsLinkText"
href="#!"
@click-event="getNextShopsFromList(3)"
:aria-label="showMoreShopsLinkText" />
<div v-show="isLoading" class="loader-wrapper">
<loader loaderColor="blue" loaderPosition="center" />
</div>
<div v-show="!isLoading">
<alert
ref="alertInvalidZip"
v-if="displayInvalidZipAlert"
class="mb-4"
cmsWidgetName="AlertInvalidZipWidget"
alertClass="alert-danger"
v-bind:isDismissible="false" />
<alert
ref="alertNoShops"
class="my-5"
cmsWidgetName="AlertNoShopsWidget"
v-if="displayNoShopsAlert"
alertClass="alert-warning" />
<div v-if="!displayInvalidZipAlert">
<buttonQuestion
ref="buttonQuestion"
buttonTypeString="shopListButton"
:buttonTypeObject="shopListButton"
class="radioQuestion"
:questionText="questionText"
:answers="answers"
groupName="chooseShop"
textPosition="text-start"
v-model="localSelectedProviderNumber"
isRequired
:validationRules="this.localSelectedProviderNumber ? '' : 'option-required'" />
<div class="show-more-shops-link">
<textLink
v-if="displaySeeMoreLocationsLink"
ref="showMoreShopsLink"
id="showMoreShopsId2"
cmsWidgetName="ShowMoreShopsLinkWidget"
linkType="text"
:text="showMoreShopsLinkText"
href="#!"
@click-event="updateShopListWithNextShops()"
:aria-label="showMoreShopsLinkText" />
</div>
</div>
</div>
</modal>
@ -84,81 +86,45 @@ import serviceZipQuestion from "@/layouts/service-location/service-zip-modal-que
import buttonQuestion from "@/digital-components/button-question/button-question.vue";
import alert from "@/ux-components/alert/alert.vue";
import shopListButton from "./shop-list-button/shop-list-button";
import loadingModal from "@/fmg-components/loading-modal/loading-modal.vue";
// Supporting files
import { defineRule } from "vee-validate";
import { required } from "@/helpers/validation-rules";
import { errorMessages } from "@/constants/error-messages";
import baseMixin from "@/mixins/base-mixin.js";
import { nextTick } from "vue";
import { regex } from "@/helpers/validation-rules";
import loader from "@/ux-components/loader/loader";
import { getAvailabilityRating } from "@/layouts/service-location/helpers/service-location-helper/service-location-helper";
import { Provider } from "@/layouts/service-location/classes/provider";
import { AppointmentTypeStrings } from "@/constants/schedule-constants";
import {
getPricedMobileFeePart,
getPricedRecycleFeePart,
getServiceabilityDetails,
getBillToAccountNumber,
getClosestApplicableShops,
getShopProviderData,
getZipCodeData,
} from "@/layouts/service-location/helpers/service-location-helper/service-location-helper";
defineRule("option-required", required(errorMessages.OPTION_REQUIRED));
defineRule("zip-format", regex(/(^\d{5}$)|(^\d{5}-\d{4}$)/, errorMessages.SERVICE_ZIP_FORMAT));
export default {
name: "shop-question-popup",
mixins: [baseMixin],
data() {
return {
isShopQuestionDisplayed: true,
internalModel: this.copyModel(this.modelValue),
answers: [],
localZipCode: this.zipCodeFromParent,
shopListButton: shopListButton,
shopIndex: 0,
displaySeeMoreLocationsLink: false,
serviceZipCodeTextInputId: "",
localShopProviderData: this.shopProviderData,
selectedProviderNumber: null,
isLoadingShops: false,
lastSearchedZip: "",
lastSuccessfulZip: "",
hasLoadedShopData: false,
displayInvalidZipAlert: false,
areShopsAvialable: true,
displayNoShopsAlert: false,
numberOfShopsToDisplay: 0,
localShopProviderData: this.shopProviderDataFromParent,
localSelectedProviderNumber: null,
zipCodeData: null,
isLoading: false,
};
},
props: {
appointmentType: String,
shopProviderData: Object,
modelValue: {
type: Object,
default: () => ({
state: "",
zipCode: "",
}),
},
isDisplayed: Boolean,
modelWidgetName: {
type: String,
required: true,
},
isServiceableInshop: {
type: Boolean,
default: false,
},
zipCode: {
shopProviderDataFromParent: Object,
zipCodeFromParent: {
type: String,
default: "",
},
preSelectedProviderNumber: {
selectedProviderNumberFromParent: {
type: String,
default: null,
},
@ -167,14 +133,20 @@ export default {
shopQuestionLinkText() {
return this.getCmsContent("ChangeMyLocationLinkWidget", "Text");
},
modalName() {
return this.modelWidgetName;
},
modalHeaderText() {
return this.getCmsContent("ShopQuestionWidget", "QuestionText");
},
selectShopText() {
return this.getCmsContent("YourSafeliteShopWidget", "QuestionText");
modalFooterText() {
return this.getCmsContent("SaveLocationWidget", "Text");
},
showMoreShopsLinkText() {
return this.getCmsContent("ShowMoreShopsLinkWidget", "Text");
},
displayInvalidZipAlert() {
return this.zipCodeData && !this.zipCodeData.isValid;
},
displayNoShopsAlert() {
return !this.shopProviders.length && !this.displayInvalidZipAlert;
},
modal() {
return this.$refs.shopQuestionModal;
@ -182,79 +154,33 @@ export default {
shopProviders() {
return this.localShopProviderData?.shopProviders ?? [];
},
modalFooterText() {
return this.getCmsContent("SaveLocationWidget", "Text");
},
selectedValue: {
get: function () {
return this.modelValue;
},
set: function (newValue) {},
},
additionalButtonData() {
const startDate = new Date();
const endDate = new Date();
endDate.setDate(startDate.getDate() + 6);
const formattedStartDate = startDate.toISOString().split("T")[0];
const formattedEndDate = endDate.toISOString().split("T")[0];
return {
availabilityRatingCallback: getAvailabilityRating,
startDate: formattedStartDate,
endDate: formattedEndDate,
shopAppointmentType: this.appointmentType,
// Used for availability indicators, could be refactored out of any availability indicator
// logic if/when we are sure we'll never have separate drop off/ in shop logic
shopAppointmentType: AppointmentTypeStrings.IN_SHOP_OR_DROP_OFF,
};
},
showMoreShopsLinkText() {
return this.getCmsContent("ShowMoreShopsLinkWidget", "Text");
displaySeeMoreLocationsLink() {
if (!this.shopProviders.length) return false;
return this.numberOfShopsToDisplay < this.shopProviders.length;
},
},
methods: {
openModal(event) {
if (event) event.preventDefault();
if (
!this.shopProviderData ||
!this.shopProviderData.shopProviders ||
!this.shopProviderData.shopProviders.length
) {
// Optionally show a loading message or disable the link
return;
}
this.$refs.shopQuestionModal.openModal();
isModalFooterButtonDisabled() {
return (
this.isLoading ||
this.displayInvalidZipAlert ||
this.displayNoShopsAlert ||
!this.localSelectedProviderNumber
);
},
getSelectedProviderObject(providerNumber) {
const provider =
this.shopProviders?.find((provider) => provider.providerNumber == providerNumber) ??
new Provider();
return provider;
},
async getNextShopsFromList(numberToGet = 3) {
const logFirstShopsDisplayed = this.shopIndex == 0;
const shopIterator = (array, n) => {
const l = array.length;
return () => {
const end = this.shopIndex + n;
const part = array.slice(this.shopIndex, end);
this.shopIndex = end < l ? end : this.shopProviders.length;
return part;
};
};
const toTitleCase = (str) => {
return str.replace(/\w\S*/g, function (txt) {
return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
});
};
const nextShop = shopIterator(this.shopProviders, numberToGet);
// Map API result data
const mappedData = nextShop().map((shopProvider) => {
const streetAddress = toTitleCase(shopProvider.address.streetAddress);
const city = toTitleCase(shopProvider.address.city);
answers() {
const updatedProvidersToDisplay = this.shopProviders.slice(
0,
this.numberOfShopsToDisplay
);
return updatedProvidersToDisplay.map((shopProvider) => {
const streetAddress = this.toTitleCase(shopProvider.address.streetAddress);
const city = this.toTitleCase(shopProvider.address.city);
const state = shopProvider.address.state;
const zipCode = shopProvider.address.zipCode;
const distanceInMiles = Math.round(shopProvider.distanceInMiles * 2) / 2;
@ -267,354 +193,139 @@ export default {
value: shopProvider.providerNumber,
};
});
var gaAction = this.GaActions.MORE_LOCATIONS_CLICKED;
if (logFirstShopsDisplayed) {
gaAction = this.GaActions.SHOPS_FIRST_DISPLAYED;
}
if (
this.appointmentType === AppointmentTypeStrings.IN_SHOP ||
this.appointmentType === AppointmentTypeStrings.DROP_OFF ||
this.appointmentType === AppointmentTypeStrings.IN_SHOP_OR_DROP_OFF
) {
var shops = mappedData.map((shop) => {
if (shop.value.length > 5 && shop.value.startsWith("00")) {
return shop.value.substring(1);
} else {
return shop.value;
}
});
var joinedShops = shops.join(",");
if (!joinedShops) {
joinedShops = "no-shops";
}
this.pushEventToGA(this.GaCategories.SERVICE_LOCATION, gaAction, joinedShops, true);
}
if (this.answers.length === 0) {
this.answers = mappedData;
} else {
mappedData.forEach((shop) => {
this.answers.push(shop);
});
}
await nextTick();
if (this.shopIndex == this.shopProviders.length) {
this.displaySeeMoreLocationsLink = false;
} else {
this.displaySeeMoreLocationsLink = true;
}
await nextTick();
this.scrollToPageBottom();
},
async handleUpdate(selectedShopIndex = null) {
this.resetAnswers();
if (selectedShopIndex >= 3) {
await this.getNextShopsFromList(selectedShopIndex + 1);
} else {
await this.getNextShopsFromList();
await nextTick();
}
},
copyModel(modelToCopy) {
return {
state: modelToCopy.state,
zipCode: modelToCopy.zipCode,
};
},
resetAnswers() {
this.answers = [];
this.shopIndex = 0;
},
onModalOpened() {
const preSelectedIndex = this.shopProviders.findIndex(
(provider) => provider.providerNumber == this.preSelectedProviderNumber
},
methods: {
openModal() {
// Reset local state from parent
this.zipCodeData = null;
this.localShopProviderData = this.shopProviderDataFromParent;
this.localZipCode = this.zipCodeFromParent;
// This will be set once the modal is open (updateSelectedAnswer()) to ensure that the
// correct answer is selected
this.localSelectedProviderNumber = null;
const selectedProviderNumberFromParentIndex = this.shopProviders.findIndex(
(provider) => provider.providerNumber == this.selectedProviderNumberFromParent
);
if (preSelectedIndex > -1) {
this.shopIndex = Math.ceil((preSelectedIndex + 1) / 3) * 3;
if (selectedProviderNumberFromParentIndex > -1) {
this.numberOfShopsToDisplay =
Math.ceil((selectedProviderNumberFromParentIndex + 1) / 3) * 3;
} else {
this.shopIndex = 3;
this.numberOfShopsToDisplay = 3;
}
this.internalModel = this.copyModel(this.modelValue);
this.internalModel.zipCode = this.zipCode;
this.localShopProviderData = this.shopProviderData;
this.answers = [];
this.onSearchZip();
this.focusOnZipInput();
this.selectedProviderNumber = this.preSelectedProviderNumber;
if (
(!this.selectedProviderNumber ||
!this.shopProviders.some(
(p) => p.providerNumber == this.selectedProviderNumber
)) &&
this.shopProviders.length > 0
) {
this.selectedProviderNumber = this.shopProviders[0].providerNumber;
this.updateShopsForZip();
this.$refs.shopQuestionModal.openModal();
},
updateSelectedAnswer() {
this.localSelectedProviderNumber = this.selectedProviderNumberFromParent;
},
toTitleCase(str) {
return str.replace(/\w\S*/g, function (txt) {
return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
});
},
updateShopListWithNextShops() {
//gaAction = this.GaActions.SHOPS_FIRST_DISPLAYED; --This needs to actually fire on initial display, can never happen here
this.numberOfShopsToDisplay += 3;
this.pushShopsDisplayedGAEvent(this.answers);
},
pushShopsDisplayedGAEvent(nextShopListAnswers) {
var shops = nextShopListAnswers.map((shop) => {
if (shop.value.length > 5 && shop.value.startsWith("00")) {
return shop.value.substring(1);
} else {
return shop.value;
}
});
var joinedShops = shops.join(",");
if (!joinedShops) {
joinedShops = "no-shops";
}
this.pushEventToGA(
this.GaCategories.SERVICE_LOCATION,
this.GaActions.MORE_LOCATIONS_CLICKED,
joinedShops,
true
);
},
closeModal() {
this.modal.closeModal();
},
onModalClosed() {
this.internalModel = this.copyModel(this.modelValue);
this.resetsOnZipInput();
},
resetsOnZipInput() {
this.resetAlerts();
},
async onSearchZip(event) {
if (event) {
event.preventDefault();
event.stopPropagation();
event.preventDefault();
if (!this.localZipCode) {
this.zipCodeData = {
isValid: false,
};
return;
}
this.resetsOnZipInput();
this.displayNoShopsAlert = false;
const zip = this.internalModel.zipCode;
const zipCodeData = await this.getZipCodeData(
this.internalModel.zipCode,
"service-location"
);
if (!zipCodeData.isValid) {
this.displayInvalidZipAlert = true;
//this.selectedProviderNumber = null;
this.focusOnZipInput();
this.resetModalButtonStyle();
} else {
this.isLoading = true;
this.zipCodeData = await this.getZipCodeData(this.localZipCode, "service-location");
// Add zipCode to zipCodeData as it does not come back from endpoint
this.zipCodeData.zipCode = this.localZipCode;
if (this.zipCodeData.isValid) {
if (this.isVehicleHeavyTruck) {
// check the location endpoint to verify this zip can service a heavy truck
const closestShops = await getClosestApplicableShops(
this.internalModel.zipCode,
this.localZipCode,
this.carId,
"service-location"
);
if (!closestShops || closestShops.providers?.length === 0) {
this.displayNoServiceAlert = true;
this.focusOnZipInput();
this.resetModalButtonStyle();
// We need an alert for heavy trucks
return null;
}
} else {
getShopProviderData(zip).then(async (result) => {
if (this.displayNoShopsAlert === true) {
this.displayNoShopsAlert = false;
}
getShopProviderData(this.localZipCode).then(async (result) => {
this.localShopProviderData = result.data;
this.updateShopsForZip(this.internalModel.zipCode);
if (this.appointmentType === AppointmentTypeStrings.MOBILE) {
this.selectedProvider = new Provider(
this.shopProviderData.mobileProviderNumber
);
} else {
this.selectedProvider = new Provider();
}
this.numberOfShopsToDisplay = 3;
this.updateShopsForZip();
this.isLoading = false;
});
}
} else {
this.isLoading = false;
}
},
async updateShopsForZip(zipCode) {
if (!this.appointmentType) {
this.answers = [];
this.isLoadingShops = false;
updateShopsForZip() {
if (!this.shopProviders.length) {
return;
}
this.lastSearchedZip = zipCode;
if (!zipCode) {
this.isLoadingShops = false;
this.answers = [];
return;
}
this.isLoadingShops = true;
if (!zipCode || !this.shopProviders.length) {
this.answers = [];
this.displayNoShopsAlert = true;
this.displaySeeMoreLocationsLink = false;
this.isLoadingShops = false;
return;
}
const selectedIndex = this.shopProviders.findIndex(
(provider) => provider.providerNumber == this.selectedProviderNumber
);
// If not found, only show the first 3 shops
if (selectedIndex === -1) {
this.shopIndex = 3;
}
const sortedShops = this.getShopsByZip(zipCode, this.shopProviders, this.shopIndex);
const toTitleCase = (str) => {
return str.replace(/\w\S*/g, function (txt) {
return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
});
};
const mappedData = sortedShops.map((shopProvider) => {
const streetAddress = toTitleCase(shopProvider.address.streetAddress);
const city = toTitleCase(shopProvider.address.city);
const state = shopProvider.address.state;
const zipCode = shopProvider.address.zipCode;
const distanceInMiles = Math.round(shopProvider.distanceInMiles * 2) / 2;
const startDate = new Date();
const endDate = new Date();
endDate.setDate(startDate.getDate() + 6);
const formattedStartDate = startDate.toISOString().split("T")[0];
const formattedEndDate = endDate.toISOString().split("T")[0];
return {
buttonLabel: city,
buttonLabelSubCopy: `${distanceInMiles} mi`,
buttonBodyCopy: `${streetAddress}, ${city}, ${state} ${zipCode}`,
additionalButtonData: {
availabilityRatingCallback: getAvailabilityRating,
startDate: formattedStartDate,
endDate: formattedEndDate,
shopAppointmentType: this.appointmentType,
},
value: shopProvider.providerNumber,
};
});
this.answers = mappedData;
if (
this.selectedProviderNumber &&
!this.answers.some((a) => String(a.value) === String(this.selectedProviderNumber))
!this.answers.some(
(a) => String(a.value) === String(this.localSelectedProviderNumber)
)
) {
this.selectedProviderNumber = null;
this.localSelectedProviderNumber = null;
}
this.shopIndex = mappedData.length;
this.displaySeeMoreLocationsLink = this.shopIndex < this.shopProviders.length;
if (mappedData.length > 0) {
this.lastSuccessfulZip = zipCode;
}
this.isLoadingShops = false;
this.displayNoShopsAlert = this.answers.length === 0;
},
resetModalButtonStyle() {
this.modal.resetButtonStyle();
},
onInputIdAssigned(inputId) {
this.serviceZipCodeTextInputId = inputId;
},
focusOnZipInput() {
const input = document.getElementById(this.serviceZipCodeTextInputId);
input?.focus();
},
resetAlerts() {
this.displayInvalidZipAlert = false;
this.displayNoServiceAlert = false;
this.displayNoShopsAlert = false;
// I think we should call our new pushGA Event here with new mapped data
},
async setZipCodeAndShop() {
if (
this.displayInvalidZipAlert ||
this.displayNoShopsAlert ||
!this.selectedProviderNumber
) {
return;
}
const zipToEmit = this.lastSearchedZip;
const selectedProvider = this.shopProviders.find(
(provider) => provider.providerNumber == this.selectedProviderNumber
);
this.resetAlerts();
const zipCodeData = await this.getZipCodeData(zipToEmit, "service-location");
if (!zipCodeData.isValid) {
this.displayInvalidZipAlert = true;
this.focusOnZipInput();
this.resetModalButtonStyle();
} else {
if (this.isVehicleHeavyTruck) {
// check the location endpoint to verify this zip can service a heavy truck
const closestShops = await getClosestApplicableShops(
selectedProvider.address.zipCode,
this.carId,
"service-location"
);
if (!closestShops || closestShops.providers?.length === 0) {
this.displayNoServiceAlert = true;
this.focusOnZipInput();
this.resetModalButtonStyle();
return null;
}
}
selectedProvider.address.zipCodeCtu = zipCodeData.zipCodeCtu;
// retrieve mobile fee part
const serviceZipCode = selectedProvider.address.zipCode;
const mobileFeePart = await getPricedMobileFeePart(
serviceZipCode,
"service-location"
);
// retrieve recycle fee part
const recycleFeePart = await getPricedRecycleFeePart(
serviceZipCode,
"service-location"
);
const shopQuestionPopUpData = {
zipCodeData: this.zipCodeData,
shopProviderData: this.localShopProviderData,
selectedProviderNumber: this.localSelectedProviderNumber,
};
this.$emit("shop-selected", shopQuestionPopUpData);
if (this.zipCodeData) {
// retrieve serviceability details
const serviceabilityDetails = await getServiceabilityDetails(
serviceZipCode,
this.zipCodeData.zipCode,
null,
"service-location"
this.pageName
);
const billToAccountNumber = await getBillToAccountNumber(
selectedProvider.address.zipCodeCtu
);
// update content related to service zip code
this.$emit("updated-mobile-fee-part", mobileFeePart);
this.$emit("updated-recycle-fee-part", recycleFeePart);
this.$emit("updated-serviceability", serviceabilityDetails.data);
this.$emit("updated-contains-military-base", zipCodeData.containsMilitaryBase);
this.$emit("updated-bill-to-account-number", billToAccountNumber);
this.$emit("updated-zipcode-ctu", selectedProvider.address.zipCodeCtu);
this.$emit("update:modelValue", this.selectedValue);
this.$emit("update-shop-provider-data", this.localShopProviderData);
this.internalModel.state = zipCodeData.state;
this.$emit("shop-selected", {
providerNumber: selectedProvider?.providerNumber,
address: selectedProvider?.address,
searchedZip: zipToEmit,
searchedState: this.internalModel.state,
});
this.closeModal();
}
},
getShopsByZip(zipCode, shops, numberToGet = 3) {
return shops
.filter(
(shop) => shop.address && shop.address.zipCode && shop.address.zipCode !== ""
)
.sort((a, b) => a.distanceInMiles - b.distanceInMiles)
.slice(0, numberToGet);
},
},
watch: {
shopProviderData: {
immediate: true,
handler(newVal) {
this.localShopProviderData = newVal;
this.hasLoadedShopData = true;
},
this.closeModal();
},
},
components: {
@ -623,6 +334,7 @@ export default {
serviceZipQuestion,
buttonQuestion,
alert,
loader,
},
};
</script>
@ -750,4 +462,10 @@ export default {
display: block;
width: 100%;
}
.loader-wrapper {
display: flex;
justify-content: center;
align-items: center;
max-width: 100%;
}
</style>

View file

@ -49,8 +49,6 @@ import { errorMessages } from "@/constants/error-messages";
import baseMixin from "@/mixins/base-mixin.js";
import { nextTick } from "vue";
import { getAvailabilityRating } from "@/layouts/service-location/helpers/service-location-helper/service-location-helper";
import { Provider } from "@/layouts/service-location/classes/provider";
import { AppointmentTypeStrings } from "@/constants/schedule-constants";
@ -109,17 +107,7 @@ export default {
return this.getCmsContent("ShowMoreShopsLinkWidget", "Text");
},
additionalButtonData() {
const startDate = new Date();
const endDate = new Date();
endDate.setDate(startDate.getDate() + 6);
const formattedStartDate = startDate.toISOString().split("T")[0];
const formattedEndDate = endDate.toISOString().split("T")[0];
return {
availabilityRatingCallback: getAvailabilityRating,
startDate: formattedStartDate,
endDate: formattedEndDate,
shopAppointmentType: this.selectedAppointmentType,
};
},

View file

@ -57,6 +57,9 @@ export default {
font-size: 0.875rem;
}
.vin-toggle {
display: inline-flex;
justify-content: center;
align-items: center;
&:after {
content: "";
transition: all 0.5s ease;