DigitalConsumer.ISS/src/layouts/schedule-page/schedule-page.vue
Bill Richardson 68d9101f53 Show All Fields and Skip Autocomplete if cannot reach google api
Some Auto formatting on save for the index page
Add global auth handler on Index page.
remove a left in console.warn on schedule.
2026-05-26 15:18:53 -04:00

1056 lines
48 KiB
Vue

<template>
<Form
ref="theForm"
@submit="onSubmit"
@invalidSubmit="onInvalidSubmit">
<div class="fade-on-route-transition">
<div class="justify-content-center">
<siteHeader
cmsWidgetName="SiteHeaderWidget" />
</div>
<div class="iss-heritage-container-width">
<div class="schedule-page-container iss-heritage-content-container-width">
<siteSubHeader
cmsWidgetName="ScheduleSubHeaderWidget"
secondaryTextClasses="text-center small sub-text"
class="mt-4" />
<div class="service-location-content-container">
<serviceLocation
ref="serviceLocation"
:schedulingInshopAvailabilityRating="inShopAvailabilityRating"
@appointmentTypeChanged="appointmentTypeChangedFromServiceLocation"
@cityUpdated="cityUpdatedFromServiceLocation"
@clearMobileData="clearMobileDataFromServiceLocation"
@inShopZipUpdated="inShopZipUpdatedFromServiceLocation"
@mobileZipUpdated="mobileZipUpdatedFromServiceLocation"
@providerChanged="providerChangedFromServiceLocation" />
</div>
</div>
<div
v-show="hasNeededServiceLocationData"
class="date-picker-container iss-heritage-content-container-width">
<div class="date-picker-content-container">
<locationAlerts
ref="locationAlerts"
cmsWidgetPrefix="LocationAlert-"
:city="selectedServiceLocationCity" />
<datePicker
ref="datePicker"
v-model="selectedTimeSlotInfo"
customComponentId="dateQuestion"
selectableDatesSetting="custom"
class="text-link-small"
:activeAppointmentType="selectedAppointmentType"
:isMobileView="isMobileView"
:showTimeSlotError="showDatePickerError"
:customSelectableDatesCallback="getAvailableDatesMethod"
@dateSelected="dateSelectedFromPicker"
@timeSlotSelected="timeSlotSelectedFromPicker" />
</div>
</div>
<div class="site-footer-container iss-heritage-content-container-width">
<siteFooter
ref="navbar"
class="mt-5"
cmsWidgetName="SiteFooterWidget"
:isForwardButtonHidden="!hasNeededServiceLocationData"
:isForwardButtonNavigationDisabled="!isFormValid"
@backClicked="navigateBack(this, navigateBackScenario)"
@forwardClicked="forwardButtonAction" />
</div>
</div>
</div>
<suggestTimeslotModal
:ref="SUGGEST_TIMESLOT_MODAL_REF_NAME"
cmsWidgetName="SuggestTimeslotModal"
@confirmAppointmentClicked="autoSelectTimeslotConfirmed" />
<recalAckModal
:ref="RECAL_ACK_MODAL_REF_NAME"
cmsWidgetName="RecalAckModalWidget"
:ackError="ackError"
@recalAcknowledged="updateIsRecalAcknowledged"/>
</Form>
</template>
<script>
// Components
import siteHeader from '@/iss-components/site-header/site-header.vue';
import siteSubHeader from '@/iss-components/site-sub-header/site-sub-header.vue';
import locationAlerts from '@/layouts/schedule-page/location-alerts/location-alerts.vue';
import datePicker from '@/digital-components/date-picker/date-picker.vue';
import serviceLocation from '@/layouts/schedule-page/service-location/service-location.vue';
import siteFooter from '@/iss-components/site-footer/site-footer.vue';
import suggestTimeslotModal from '@/layouts/schedule-page/suggest-timeslot-modal/suggest-timeslot-modal.vue';
import recalAckModal from '@/layouts/schedule-page/recal-ack-modal/recal-ack-modal.vue';
// Supporting files
import { experimentSettings, experimentTest, experimentVariation, experimentUniverses } from '@/constants/experiments';
import {
AppointmentTypeStrings,
GET_MOBILE_TIME_SLOTS,
GET_SHOP_TIME_SLOTS,
PREMIUM_FEE_PART_TYPE
} from '@/constants/schedule-constants.js';
import {
fetchCmsContentForPage,
splitCopyOnCMSPlaceHolder
} from '@/helpers/cms-content-helper';
import {
calcDaysBetweenDates,
convertDateToDateString,
sumDateString
} from '@/helpers/date-helper';
import settleAllPromises from '@/helpers/layout-helper';
import showIssLoadingModal from '@/helpers/loading-modal-helper.js';
import {
getPricedMobileFeePart,
getMobileZipCodeData,
getZipCodeData,
isServiceableMobileWithData,
mapTimeSlot
} from '@/helpers/service-location-helper';
import { Form } from 'vee-validate';
import BaseFormMixin from '@/mixins/base-form-mixin.js';
import { useMainStore } from '@/store';
import { containsRecalParts, anyPartWithRequiresRecalFlag } from '@/helpers/recal-helper';
import errorMessages from '@/constants/error-messages';
// Define constants
const SUGGEST_TIMESLOT_MODAL_REF_NAME = 'SuggestTimeslotModal';
const TIME_SLOTS_CALL_DAYS_LIMIT = 15;
const RECAL_ACK_MODAL_REF_NAME = 'recalAckModal';
const getAvailableDates = async (
startDateString,
endDateString,
appointmentType,
providerNumber,
mobileZipCodeOverride = null
) => {
const apiEndDateLimit = sumDateString(
startDateString,
TIME_SLOTS_CALL_DAYS_LIMIT - 1
);
const difference = calcDaysBetweenDates(startDateString, endDateString);
const apiCallsCount = Math.ceil(difference / TIME_SLOTS_CALL_DAYS_LIMIT);
const storeActionConfigs = [];
let apiStartDate = startDateString;
let apiEndDate = endDateString;
for (let i = 1; i <= apiCallsCount; i++) {
let storeActionConfig;
if (i > 1) {
apiStartDate = sumDateString(apiEndDate, 1);
apiEndDate = sumDateString(
apiStartDate,
TIME_SLOTS_CALL_DAYS_LIMIT - 1
);
if (i === apiCallsCount) {
apiEndDate = endDateString;
}
} else if (apiEndDate > apiEndDateLimit) {
apiEndDate = apiEndDateLimit;
}
if (appointmentType === AppointmentTypeStrings.MOBILE || appointmentType === AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP) {
storeActionConfig = {
storeAction: GET_MOBILE_TIME_SLOTS,
payload: {
startDate: apiStartDate,
endDate: apiEndDate,
zipCodeOverride: mobileZipCodeOverride
}
};
} else {
storeActionConfig = {
storeAction: GET_SHOP_TIME_SLOTS,
payload: {
startDate: apiStartDate,
endDate: apiEndDate,
shopAppointmentType: appointmentType,
providerNumber
}
};
}
if (apiStartDate < apiEndDate) storeActionConfigs.push(storeActionConfig);
}
const timeSlotsResponsesData = {
days: []
};
function compareDayStrings(a, b) {
if (a.date < b.date) return -1;
if (a.date > b.date) return 1;
return 0;
}
const makeParallelCalls = async () => {
await Promise.all(storeActionConfigs.map(async (storeAction) => {
let timeSlotsResponse = null;
if (storeAction.storeAction === GET_SHOP_TIME_SLOTS) {
if (storeAction.payload.providerNumber) {
timeSlotsResponse = await useMainStore().getShopTimeSlots(
storeAction.payload.startDate,
storeAction.payload.endDate,
storeAction.payload.shopAppointmentType,
storeAction.payload.providerNumber
);
}
} else if (storeAction.payload?.zipCodeOverride) {
timeSlotsResponse = await useMainStore().getMobileTimeSlots(
storeAction.payload.startDate,
storeAction.payload.endDate,
storeAction.payload.zipCodeOverride
);
}
if (!timeSlotsResponse || !timeSlotsResponse.data) {
return;
}
timeSlotsResponsesData.estimatedServiceMinutesMinimum = timeSlotsResponse.data.estimatedServiceMinutesMinimum;
timeSlotsResponsesData.estimatedServiceMinutesMaximum = timeSlotsResponse.data.estimatedServiceMinutesMaximum;
timeSlotsResponsesData.days = [
...timeSlotsResponsesData.days,
...timeSlotsResponse.data.days
];
}));
};
return makeParallelCalls().then(() => {
// sort days chronologically
timeSlotsResponsesData.days.sort(compareDayStrings);
return timeSlotsResponsesData;
})
.catch(() => {
return timeSlotsResponsesData;
});
};
export default {
name: 'schedule-page',
components: {
siteHeader,
siteSubHeader,
locationAlerts,
datePicker,
serviceLocation,
siteFooter,
suggestTimeslotModal,
Form,
recalAckModal
},
mixins: [BaseFormMixin],
async beforeRouteEnter(to, from, next) {
// Call APIs
const cmsContentPromise = fetchCmsContentForPage(to.query.issPage);
let storeServiceLocation = useMainStore().order.serviceLocation;
let storeSelectedProvider = storeServiceLocation?.provider;
const storeSelectedSchedule = useMainStore().order.schedule;
if (storeSelectedProvider?.providerNumber && !storeSelectedSchedule?.routeCode) {
useMainStore().resetServiceLocationProvider();
storeServiceLocation = useMainStore().order.serviceLocation;
storeSelectedProvider = storeServiceLocation?.provider;
}
const storeSelectedAppointmentType = storeServiceLocation?.appointmentType;
const isMobileAppointment = storeSelectedAppointmentType === AppointmentTypeStrings.MOBILE
|| storeSelectedAppointmentType === AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP;
const serviceZipCode = storeServiceLocation?.zipCode || useMainStore().order.customer.address.zipCode;
const zipCodeData = getZipCodeData(serviceZipCode);
const mobileFeePartPromise = getPricedMobileFeePart(serviceZipCode);
const serviceabilityDetailsPromise = useMainStore().getServiceabilityDetails(serviceZipCode);
const getGlassFeesPromise = useMainStore().getGlassFees();
const providersPromise = useMainStore().getSafeliteProviders(serviceZipCode, 150);
const premiumFeePromise = useMainStore().getMobilePremiumFee();
const premiumFeeWithPricePromise = premiumFeePromise.then((result) => {
if (result.data) {
return useMainStore().getCombinedQuote(result.data);
}
return result.data;
});
// Settle promises and get results
const promiseResultMap = [
{
resultKey: 'cmsContent',
promise: cmsContentPromise
},
{
resultKey: 'mobileFeePart',
promise: mobileFeePartPromise
},
{
resultKey: 'glassFees',
promise: getGlassFeesPromise
},
{
resultKey: 'serviceabilityDetails',
promise: serviceabilityDetailsPromise
},
{
resultKey: 'zipCodeData',
promise: zipCodeData
},
{
resultKey: 'providers',
promise: providersPromise
},
{
resultKey: 'premiumFeeWithPrice',
promise: premiumFeeWithPricePromise
}
];
// use resultMap to populate layout content.
const resultMap = await settleAllPromises(promiseResultMap);
const providersUnderOneHundredMiles = resultMap.providers?.shopProviders?.filter((provider) => provider.distanceInMiles <= 100);
const serviceLocationData = {
defaultMobileZipCode: isMobileAppointment
? storeServiceLocation?.zipCode
: '',
glassFees: resultMap.glassFees,
mobileFeePart: resultMap.mobileFeePart,
providers: {
...resultMap.providers,
shopProviders: providersUnderOneHundredMiles || []
},
serviceabilityDetails: resultMap.serviceabilityDetails,
zipCodeData: resultMap.zipCodeData
};
useMainStore().updateIsSafeliteProvider(true);
next(async (vm) => {
const foundProviderInFullList = resultMap.providers?.shopProviders
.find((provider) => provider.providerNumber === storeSelectedProvider?.providerNumber) || null;
const foundProviderInHundredMiles = providersUnderOneHundredMiles
.find((provider) => provider.providerNumber === storeSelectedProvider?.providerNumber) || null;
if (!foundProviderInHundredMiles && !foundProviderInFullList) {
if (storeSelectedProvider?.providerNumber && resultMap.providers?.shopProviders) {
serviceLocationData.providers?.shopProviders.push(storeSelectedProvider);
}
} else if (!foundProviderInHundredMiles && foundProviderInFullList) {
serviceLocationData.providers?.shopProviders.push(foundProviderInFullList);
}
const initialServiceLocationObj = {
mobileProviderNumber: isMobileAppointment
? storeSelectedProvider?.providerNumber
: null,
provider: isMobileAppointment ? null : foundProviderInFullList || resultMap.providers?.shopProviders[0] || null,
zipCode: useMainStore().order.serviceLocation.zipCode || useMainStore().order.customer.address.zipCode,
zipCodeCtu: resultMap.zipCodeData?.zipCodeCtu
};
const mobileFirstObject = {
mobileProviderNumber: resultMap.providers?.mobileProviderNumber,
serviceabilityDetails: resultMap.serviceabilityDetails,
zipCode: initialServiceLocationObj.zipCode
};
vm.setCmsContent(resultMap.cmsContent);
vm.$refs.serviceLocation.initializeComponent(serviceLocationData);
await vm.setData(resultMap.premiumFeeWithPrice, initialServiceLocationObj);
await vm.checkMobileFirstExperience(mobileFirstObject);
showIssLoadingModal(false);
});
},
setup() {
const mainStore = useMainStore();
return { mainStore };
},
data() {
return {
inShopAvailabilityRating: 'high',
inShopDatesData: [],
isDatePickerRefreshing: false,
isMobileView: false,
mobileDatesData: [],
mobileFeePart: null,
mobilePremiumAppointmentFee: null,
mobileProviderNumber: null,
selectableDatesData: [],
selectedAppointmentType: this.getAppointmentType(),
selectedDate: this.getSelectedDate(),
selectedMobileZipCode: null,
selectedProvider: this.getSelectedProvider(),
selectedServiceLocation: this.getServiceLocation(),
selectedServiceLocationCity: this.getServiceLocationCity(),
selectedTimeSlotInfo: this.getSelectedTimeSlotInfo(),
showDatePickerError: false,
SUGGEST_TIMESLOT_MODAL_REF_NAME,
RECAL_ACK_MODAL_REF_NAME,
isRecalAcknowledged: this.getIsRecalAcknowledgedForScheduling()
};
},
computed: {
hasNeededServiceLocationData() {
const serviceLocationToUse = this.selectedServiceLocation;
if (!serviceLocationToUse || !this.selectedAppointmentType) {
return false;
}
const validMobileAppointmentType = ((this.selectedAppointmentType === AppointmentTypeStrings.MOBILE
|| this.selectedAppointmentType === AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP)
&& serviceLocationToUse.mobileProviderNumber);
const validInShopAppointmentType = ((this.selectedAppointmentType === AppointmentTypeStrings.IN_SHOP
|| this.selectedAppointmentType === AppointmentTypeStrings.DROP_OFF)
&& (this.selectedProvider?.providerNumber || serviceLocationToUse.provider?.providerNumber));
const serviceLocationPreReqs = serviceLocationToUse.zipCode !== null
&& serviceLocationToUse.zipCodeCtu !== null
&& serviceLocationToUse.appointmentType !== null
&& (validMobileAppointmentType || validInShopAppointmentType)
&& !this.isDatePickerRefreshing;
const damageInfo = useMainStore().order.damage.isRepair
|| (useMainStore().order.lineItems?.glassParts != null && useMainStore().order.lineItems.glassParts.length > 0);
const supportingItems = this.supportingItems !== null;
return (serviceLocationPreReqs && supportingItems && damageInfo);
},
isFormValid() {
const hasTimeSlotSelected = this.selectedTimeSlotInfo?.timeSlot?.routeCode != null;
return hasTimeSlotSelected;
},
navigateBackScenario() {
const { isNoComp, isITAC } = useMainStore();
return isNoComp || isITAC
? this.navigationScenarios.CLICKED_BACK_CANNOT_REACH_TPA_FLOW
: this.navigationScenarios.CLICKED_BACK_CAN_REACH_TPA_FLOW;
},
supportingItems() {
return useMainStore().lineItems.supportingItems;
},
displayRecalAckModal() {
// if any of the glass parts contain an item with the requiresRecalibration flag set to true
// and the order does not contain a recalibration part, show the recalibration acknowledgement modal
const { glassParts } = useMainStore().order.lineItems;
const containsRecalPart = containsRecalParts(glassParts);
const hasPartWithRecalRequirement = anyPartWithRequiresRecalFlag(glassParts);
if (!containsRecalPart && hasPartWithRecalRequirement) {
return true;
}
return false;
},
ackError() {
return errorMessages.ACKNOWLEDGEMENT_REQUIRED;
}
},
watch: {
inShopDatesData(newData) {
if (newData?.initialShopTimeSlotsResponse?.days?.length > 0) {
const numberOfDaysNeededToBeHighAvailability = 2;
const availabilityEndDate = new Date();
availabilityEndDate.setDate(availabilityEndDate.getDate() + 6);
const datesInAvailabilityRange = newData.initialShopTimeSlotsResponse.days.filter((day) => {
const dayDate = new Date(`${day.date}T00:00:00`);
return dayDate <= availabilityEndDate;
});
const isGoodAvailability =
datesInAvailabilityRange.filter((x) => x.timeSlots.length > 0).length
>= numberOfDaysNeededToBeHighAvailability;
this.inShopAvailabilityRating = isGoodAvailability ? 'high' : 'low';
} else {
this.inShopAvailabilityRating = 'low';
}
}
},
mounted() {
showIssLoadingModal(true);
this.mql = window.matchMedia('(min-width: 1200px)');
this.isMobileView = !this.mql.matches;
this.mql.addEventListener('change', this.handleMqlChange);
this.mainStore.order.isRecalAcknowledgedForScheduling = false;
this.isRecalAcknowledged = false;
},
unmounted() {
if (this.mql) {
this.mql.removeEventListener('change', this.handleMqlChange);
}
},
methods: {
splitCopyOnCMSPlaceHolder,
arePagePrerequisitesValid() {
return useMainStore().order.serviceLocation.zipCode !== null;
},
async autoSelectTimeslotConfirmed(emittedValue) {
if (this.mainStore.isNoComp || this.mainStore.isITAC) {
console.log('User is No Comp or ITAC, skipping mobile fee update on auto select timeslot confirmed');
// this.mainStore.updateMobileFee(this.mobileFeePart);
}
const providerToUse = {
providerNumber: emittedValue.mobileProviderNumber,
address: {
streetAddress: null,
city: null,
state: null,
zipCode: null,
zipCodeCtu: null
}
};
this.mainStore.updateServiceLocation({
state: emittedValue.zipCodeData.state,
zipCode: emittedValue.zipCodeData.zipCode,
zipCodeCtu: emittedValue.zipCodeData.zipCodeCtu,
appointmentType: AppointmentTypeStrings.MOBILE,
provider: providerToUse
});
const timeSlotToUse = {
date: emittedValue.date,
routeCode: emittedValue.timeSlot.id,
startTime: emittedValue.timeSlot.startTime,
endTime: emittedValue.timeSlot.endTime,
jobMaxMinutes: emittedValue.estimatedServiceMinutesMax.toString(),
jobMinMinutes: emittedValue.estimatedServiceMinutesMin.toString(),
};
this.selectedAppointmentType = AppointmentTypeStrings.MOBILE;
this.pushServiceTypeEvent();
this.mainStore.saveSchedule(timeSlotToUse);
showIssLoadingModal(true);
this.$router.navigate(
this.navigationScenarios.CLICKED_FORWARD,
this.$route
);
},
async checkMobileFirstExperience(mobileFirstObject = {}) {
const mobileFirstExperimentVariation = this.getExperimentVariationNameByTestName(experimentTest.ISS_MOBILE_FIRST_POPUP_V3);
const isMobileFirstTestVariation = mobileFirstExperimentVariation === experimentVariation.ISS_MOBILE_FIRST_TEST_V3;
const showMobileFirst = this.getSettingValue(experimentSettings.ISS_MOBILE_FIRST_SHOW_FIRST_MOBILE_APPOINTMENT);
if (showMobileFirst) {
const pmMobileDaysLimit = this.getSettingValue(experimentSettings.ISS_MOBILE_FIRST_MAX_PM_MOBILE_DAYS);
const mobileDaysLimit = this.getSettingValue(experimentSettings.ISS_MOBILE_FIRST_MAX_MOBILE_DAYS);
const hasSelectedDate = !!this.getSelectedDate();
const isServiceableMobile = isServiceableMobileWithData(
mobileFirstObject.serviceabilityDetails?.isGlassServiceableMobile ?? null,
mobileFirstObject.serviceabilityDetails?.isRecalibrationServiceableMobile ?? null
);
if (!hasSelectedDate && isServiceableMobile && mobileFirstObject.mobileProviderNumber && mobileFirstObject.zipCode) {
const todayDateObject = new Date();
const todayDateString = convertDateToDateString(todayDateObject);
const calendarViewDirection = 'future';
const initialDays = 15;
const initialViewStartDate = todayDateString;
const initialEndDate = new Date();
initialEndDate.setDate(initialEndDate.getDate() + (initialDays - 1));
const initialViewEndDate = convertDateToDateString(initialEndDate);
const preSelectedDate = this.selectedDate;
const initialData = await getAvailableDates(
initialViewStartDate,
initialViewEndDate,
AppointmentTypeStrings.MOBILE,
mobileFirstObject.mobileProviderNumber,
mobileFirstObject.zipCode
).then((response) => ({
todayDate: todayDateString,
initialViewStartDate,
initialViewEndDate,
calendarViewDirection,
initialShopTimeSlotsResponse: response,
preSelectedDate,
initialDaysLoaded: initialDays,
daysFromStart: null
}))
.catch(() => {
return {
todayDate: todayDateString,
initialViewStartDate,
initialViewEndDate,
calendarViewDirection,
initialShopTimeSlotsResponse: { days: [] },
preSelectedDate,
initialDaysLoaded: initialDays,
daysFromStart: null
};
});
this.mobileDatesData = initialData;
const estimatedServiceMinutesMax = initialData.initialShopTimeSlotsResponse?.estimatedServiceMinutesMaximum || 120;
const estimatedServiceMinutesMin = initialData.initialShopTimeSlotsResponse?.estimatedServiceMinutesMinimum || 90;
const retTimeSlotsResponse = [];
initialData.initialShopTimeSlotsResponse.days.forEach((selectableDate) => {
const dateObjectToPush = {
date: selectableDate.date,
timeSlots: selectableDate.timeSlots,
morningTimeSlots: selectableDate.timeSlots.filter((timeSlot) => {
const timeHour = parseInt(timeSlot.startTime.split(':')[0], 10);
return timeHour < 12;
}).map((timeSlot) => mapTimeSlot(timeSlot, 'morning')),
afternoonTimeSlots: selectableDate.timeSlots.filter((timeSlot) => {
const timeHour = parseInt(timeSlot.startTime.split(':')[0], 10);
return timeHour >= 12;
}).map((timeSlot) => mapTimeSlot(timeSlot, 'afternoon')),
isSelected: false
};
if (dateObjectToPush.morningTimeSlots.length > 0) {
const findIndex = dateObjectToPush.morningTimeSlots.findIndex((timeSlot) => timeSlot.id.endsWith('DROP OFF'));
if (findIndex !== -1) {
const dropOffSlot = dateObjectToPush.morningTimeSlots.splice(findIndex, 1)[0];
dateObjectToPush.morningTimeSlots.unshift(dropOffSlot);
}
}
if (dateObjectToPush.afternoonTimeSlots.length > 0) {
const findIndex = dateObjectToPush.afternoonTimeSlots.findIndex((timeSlot) => timeSlot.id.endsWith('DROP OFF'));
if (findIndex !== -1) {
const dropOffSlot = dateObjectToPush.afternoonTimeSlots.splice(findIndex, 1)[0];
dateObjectToPush.afternoonTimeSlots.push(dropOffSlot);
}
}
retTimeSlotsResponse.push(dateObjectToPush);
});
let dateDifferenceForPmMobile = null;
const firstPmMobileDateWithTimeSlots = retTimeSlotsResponse.find((date) => {
const hasPmMobileTimeSlots = date.afternoonTimeSlots?.length > 0;
if (hasPmMobileTimeSlots && pmMobileDaysLimit) {
dateDifferenceForPmMobile = calcDaysBetweenDates(todayDateString, date.date);
return dateDifferenceForPmMobile <= pmMobileDaysLimit;
}
return hasPmMobileTimeSlots;
});
let dateDifferenceForMobile = null;
const firstMobileDateWithTimeSlots = retTimeSlotsResponse.find((date) => {
const hasMobileTimeSlots = date.morningTimeSlots?.length > 0;
const hasPmMobileTimeSlots = date.afternoonTimeSlots?.length > 0;
if (hasMobileTimeSlots || hasPmMobileTimeSlots) {
if (mobileDaysLimit) {
dateDifferenceForMobile = calcDaysBetweenDates(todayDateString, date.date);
return mobileDaysLimit && dateDifferenceForMobile <= mobileDaysLimit;
}
return true;
}
return hasPmMobileTimeSlots;
});
const zipCodeData = await getMobileZipCodeData(mobileFirstObject.zipCode);
if ((firstPmMobileDateWithTimeSlots || firstMobileDateWithTimeSlots) && zipCodeData?.isValid) {
zipCodeData.zipCode = mobileFirstObject.zipCode;
const timeSlotForAutoSelectionMobile = {
date: firstPmMobileDateWithTimeSlots ? firstPmMobileDateWithTimeSlots.date : firstMobileDateWithTimeSlots.date,
estimatedServiceMinutesMax,
estimatedServiceMinutesMin,
isRepair: this.mainStore.order.damage.isRepair,
mobileProviderNumber: mobileFirstObject.mobileProviderNumber,
timeSlot: firstPmMobileDateWithTimeSlots ? firstPmMobileDateWithTimeSlots.afternoonTimeSlots[0]
: (firstMobileDateWithTimeSlots.morningTimeSlots[0] || firstMobileDateWithTimeSlots.afternoonTimeSlots[0]),
zipCodeData
};
const dateObjectToUse = new Date(`${timeSlotForAutoSelectionMobile.date}T${timeSlotForAutoSelectionMobile.timeSlot.startTime}:00`);
timeSlotForAutoSelectionMobile.dateObject = dateObjectToUse;
timeSlotForAutoSelectionMobile.dayOfWeek = dateObjectToUse.toLocaleDateString('en-US', { weekday: 'long' });
const experiment = this.getExperimentByName(experimentUniverses.ISS_MOBILE_FIRST_APPOINTMENTS);
if (experiment && !experiment.isExposed) {
this.mainStore.logMobileFirstExperimentExposure();
}
if (isMobileFirstTestVariation) {
this.$refs[SUGGEST_TIMESLOT_MODAL_REF_NAME].openModal(timeSlotForAutoSelectionMobile);
} else {
console.log('In Control group or experiment not found, skip showing popup');
}
}
}
}
},
async setData(premiumFeeWithPriceResponse, initialServiceLocationObj) {
this.selectedServiceLocation = initialServiceLocationObj;
this.mobilePremiumAppointmentFee = premiumFeeWithPriceResponse
? premiumFeeWithPriceResponse[0]
: null;
if (initialServiceLocationObj?.zipCodeCtu) {
this.$refs.locationAlerts.loadInitialData(
initialServiceLocationObj.zipCodeCtu,
initialServiceLocationObj.provider?.ctu
).then((alertReasons) => {
this.$refs.locationAlerts.initializeComponent(alertReasons.data);
});
}
if (this.selectedAppointmentType !== AppointmentTypeStrings.MOBILE
&& this.selectedAppointmentType !== AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP) {
await this.getDatePickerInitialData(initialServiceLocationObj.provider?.providerNumber).then((initialData) => {
this.selectableDatesData = initialData.initialShopTimeSlotsResponse;
this.$refs.datePicker.setCalendarData(initialData);
});
} else {
this.mobileProviderNumber = initialServiceLocationObj.mobileProviderNumber;
this.selectedMobileZipCode = initialServiceLocationObj.zipCode;
await this.getDatePickerInitialData().then((initialData) => {
this.selectableDatesData = initialData.initialShopTimeSlotsResponse;
this.$refs.datePicker.setCalendarData(initialData);
});
}
},
appointmentTypeChangedFromServiceLocation(newAppointmentType) {
this.selectedAppointmentType = newAppointmentType.appointmentType;
if (newAppointmentType.appointmentType === AppointmentTypeStrings.IN_SHOP) {
if (this.inShopDatesData?.initialShopTimeSlotsResponse?.days?.length > 0) {
this.selectableDatesData = this.inShopDatesData.initialShopTimeSlotsResponse;
this.$refs.datePicker.initializeComponent(this.inShopDatesData);
}
} else if (this.mobileDatesData?.initialShopTimeSlotsResponse?.days?.length > 0) {
this.selectableDatesData = this.mobileDatesData.initialShopTimeSlotsResponse;
this.$refs.datePicker.initializeComponent(this.mobileDatesData);
}
},
cityUpdatedFromServiceLocation(newCity) {
this.selectedServiceLocationCity = newCity;
},
clearMobileDataFromServiceLocation() {
if (this.selectedServiceLocation) {
this.selectedServiceLocation.mobileProviderNumber = null;
}
this.mobileDatesData = [];
this.mobileProviderNumber = null;
this.selectedMobileZipCode = null;
},
dateSelectedFromPicker(date) {
this.selectedDate = date;
this.showDatePickerError = false;
},
getAppointmentType() {
return this.mainStore.order?.serviceLocation?.appointmentType;
},
async getAvailableDatesMethod(startDate, endDate) {
let mobileProviderNumberToUse = this.mobileProviderNumber;
if (this.selectedAppointmentType === AppointmentTypeStrings.MOBILE
|| this.selectedAppointmentType === AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP) {
mobileProviderNumberToUse = this.selectedServiceLocation.mobileProviderNumber;
}
const newShopTimeSlots = await getAvailableDates(
startDate,
endDate,
this.selectedAppointmentType,
this.selectedProvider?.providerNumber || mobileProviderNumberToUse,
this.selectedMobileZipCode
);
// ADD API CALL RESULTS TO EXISTING DATE DATA
this.selectableDatesData.days =
this.selectableDatesData.days.concat(newShopTimeSlots.days);
return newShopTimeSlots;
},
getAvailableDates,
async getDatePickerInitialData(defaultProviderNumber = null) {
let todayDateString;
const todayDateObject = new Date();
const calendarViewDirection = 'future';
const initialDays = 15;
if (this.todayString) {
todayDateString = this.todayString;
} else {
todayDateString = convertDateToDateString(todayDateObject);
}
const initialViewStartDate = todayDateString;
const initialEndDate = new Date();
initialEndDate.setDate(initialEndDate.getDate() + (initialDays - 1));
let initialViewEndDate = convertDateToDateString(initialEndDate);
const preSelectedDate = this.selectedDate;
const endDateObject = this.getEndDateForExistingDate(
todayDateObject,
preSelectedDate,
initialDays
);
if (endDateObject.endDateString !== initialViewEndDate) {
initialViewEndDate = endDateObject.endDateString;
}
let mobileProviderNumberToUse = defaultProviderNumber;
if (this.selectedAppointmentType === AppointmentTypeStrings.MOBILE
|| this.selectedAppointmentType === AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP) {
mobileProviderNumberToUse = this.selectedServiceLocation.mobileProviderNumber;
}
const initialData = await getAvailableDates(
initialViewStartDate,
initialViewEndDate,
this.selectedAppointmentType || AppointmentTypeStrings.IN_SHOP,
this.selectedProvider?.providerNumber || mobileProviderNumberToUse,
this.selectedServiceLocation.zipCode
).then((response) => ({
todayDate: todayDateString,
initialViewStartDate,
initialViewEndDate,
calendarViewDirection,
initialShopTimeSlotsResponse: response,
preSelectedDate,
initialDaysLoaded: endDateObject.newDaysLoaded || initialDays,
daysFromStart: endDateObject.daysFromStart || null
}))
.catch(() => {
return {
todayDate: todayDateString,
initialViewStartDate,
initialViewEndDate,
calendarViewDirection,
initialShopTimeSlotsResponse: { days: [] },
preSelectedDate,
initialDaysLoaded: endDateObject.newDaysLoaded || initialDays,
daysFromStart: endDateObject.daysFromStart || null
};
});
if (this.selectedAppointmentType === AppointmentTypeStrings.MOBILE
|| this.selectedAppointmentType === AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP) {
this.pushMobileFirstDateEvent(initialData);
this.mobileDatesData = initialData;
if (this.selectedProvider?.providerNumber) {
const inShopData = await getAvailableDates(
initialViewStartDate,
initialViewEndDate,
AppointmentTypeStrings.IN_SHOP,
this.selectedProvider?.providerNumber || defaultProviderNumber,
this.selectedServiceLocation.zipCode
).then((response) => ({
todayDate: todayDateString,
initialViewStartDate,
initialViewEndDate,
calendarViewDirection,
initialShopTimeSlotsResponse: response,
preSelectedDate,
initialDaysLoaded: endDateObject.newDaysLoaded || initialDays,
daysFromStart: endDateObject.daysFromStart || null
}));
this.pushInshopFirstDateEvent(inShopData);
this.inShopDatesData = inShopData;
}
} else {
this.pushInshopFirstDateEvent(initialData);
this.inShopDatesData = initialData;
this.mobileDatesData = [];
}
return initialData;
},
getEndDateForExistingDate(startDate, selectedDateString, daysInView) {
const endDate = new Date(startDate);
endDate.setDate(endDate.getDate() + (daysInView - 1));
if (selectedDateString) {
const selectedDate = new Date(`${selectedDateString}T00:00:00`);
const daysDifferenceComparedToStartDate = Math.ceil((selectedDate - startDate) / (1000 * 60 * 60 * 24));
if (selectedDate > endDate) {
const daysDifferenceComparedToStartDateAsPages = Math.ceil(daysDifferenceComparedToStartDate / daysInView);
const newEndDate = new Date(startDate);
newEndDate.setDate(newEndDate.getDate() + (daysDifferenceComparedToStartDateAsPages * daysInView) - 1);
return {
endDateString: convertDateToDateString(newEndDate),
newDaysLoaded: (daysDifferenceComparedToStartDateAsPages * daysInView),
daysFromStart: daysDifferenceComparedToStartDate
};
}
return {
endDateString: convertDateToDateString(endDate),
newDaysLoaded: null,
daysFromStart: daysDifferenceComparedToStartDate
};
}
return {
endDateString: convertDateToDateString(endDate),
newDaysLoaded: null,
daysFromStart: null
};
},
getServiceZipCtuCodeFromStore() {
return this.mainStore.order.serviceLocation.zipCodeCtu;
},
getSelectedDate() {
return this.mainStore.order.schedule.date;
},
getSelectedProvider() {
return this.mainStore.order.serviceLocation.provider;
},
getSelectedTimeSlotInfo() {
const isPremiumAppointment =
!!(
this.supportingItems?.filter((lineItem) =>
lineItem.partType === PREMIUM_FEE_PART_TYPE) ?? []
).length > 0;
const selectedTimeSlotInfo = {
timeSlot: this.mainStore.order.schedule,
isPremiumAppointment
};
return selectedTimeSlotInfo;
},
getServiceLocation() {
return this.mainStore.order.serviceLocation;
},
getServiceLocationCity() {
return this.mainStore.order.serviceLocation?.city;
},
handleMqlChange(e) {
this.isMobileView = !e.matches;
},
inShopZipUpdatedFromServiceLocation(inShopZipServiceLocation) {
if (inShopZipServiceLocation) {
this.mobileProviderNumber = null;
this.selectedMobileZipCode = null;
this.selectedServiceLocation = inShopZipServiceLocation;
}
},
mobileZipUpdatedFromServiceLocation(mobileZipServiceLocation) {
if (mobileZipServiceLocation) {
this.mobileProviderNumber = mobileZipServiceLocation.mobileProviderNumber;
this.selectedMobileZipCode = mobileZipServiceLocation.zipCode;
this.selectedServiceLocation = mobileZipServiceLocation;
if (mobileZipServiceLocation.refreshDatePicker) {
this.refreshDatePicker();
}
}
},
async providerChangedFromServiceLocation(newProvider) {
this.selectedProvider = newProvider?.provider;
if (newProvider?.refreshDatePicker && newProvider.provider) {
this.mainStore.updateServiceLocationProvider(newProvider.provider);
showIssLoadingModal(true);
await this.mainStore.getBillToInfo(newProvider.provider?.providerNumber)
.then(() => {
this.refreshDatePicker();
})
.catch(() => {
// Error fetching billTo info, close loading modal
showIssLoadingModal(false);
});
}
},
pushInshopFirstDateEvent(inshopData) {
let dateDiffString = '';
if (inshopData?.initialShopTimeSlotsResponse?.days?.length > 0) {
const todayDateString = convertDateToDateString(new Date());
dateDiffString = calcDaysBetweenDates(todayDateString, inshopData.initialShopTimeSlotsResponse.days[0].date).toString();
}
this.pushEventToGA('appointment', 'availability_inshop', `days_out_${dateDiffString}`, true, null, null);
},
pushMobileFirstDateEvent(mobileData) {
let dateDiffString = '';
if (mobileData?.initialShopTimeSlotsResponse?.days?.length > 0) {
const todayDateString = convertDateToDateString(new Date());
dateDiffString = calcDaysBetweenDates(todayDateString, mobileData.initialShopTimeSlotsResponse.days[0].date).toString();
}
this.pushEventToGA('appointment', 'availability_mobile', `days_out_${dateDiffString}`, true, null, null);
},
pushServiceTypeEvent() {
if (this.selectedAppointmentType) {
this.pushEventToGA('appointment', 'service_type', this.selectedAppointmentType.toLowerCase(), true, null, null);
}
},
async refreshDatePicker() {
this.resetLocalFlags();
this.isDatePickerRefreshing = true;
await this.getDatePickerInitialData().then((data) => {
this.selectableDatesData = data.initialShopTimeSlotsResponse;
this.$refs.datePicker.initializeComponent(data);
this.isDatePickerRefreshing = false;
showIssLoadingModal(false);
});
},
resetLocalFlags() {
this.selectedDate = null;
this.selectedTimeSlotInfo = null;
this.showDatePickerError = false;
},
timeSlotSelectedFromPicker(timeSlot) {
this.selectedTimeSlotInfo = timeSlot;
this.showDatePickerError = false;
},
async forwardButtonAction() {
if (this.displayRecalAckModal && this.isRecalAcknowledged === false) {
showIssLoadingModal(false);
this.$refs[RECAL_ACK_MODAL_REF_NAME].openModal();
return;
}
if (!this.isFormValid) {
this.showDatePickerError = true;
return;
}
let appointmentTypeToUse = this.selectedAppointmentType;
if (appointmentTypeToUse === AppointmentTypeStrings.IN_SHOP && this.selectedTimeSlotInfo.timeSlot?.isDropoff === true) {
appointmentTypeToUse = AppointmentTypeStrings.DROP_OFF;
}
await this.$refs.serviceLocation.forwardButtonAction(appointmentTypeToUse);
this.pushServiceTypeEvent();
this.mainStore.saveSchedule(this.selectedTimeSlotInfo.timeSlot);
this.$router.navigate(
this.navigationScenarios.CLICKED_FORWARD,
this.$route
);
},
updateIsRecalAcknowledged(isAcknowledged) {
this.isRecalAcknowledged = isAcknowledged;
this.mainStore.order.isRecalAcknowledgedForScheduling = isAcknowledged;
},
getIsRecalAcknowledgedForScheduling() {
return this.mainStore.order.isRecalAcknowledgedForScheduling;
},
}
};
</script>
<style lang="scss" scoped>
$page-side-padding: 1.5rem;
.iss-heritage-container-width {
padding-right: 0.9375rem;
padding-left: 0.9375rem;
.schedule-page-container {
position: relative;
min-height: 1px;
}
}
:deep(.text-link-small) {
a,
.btn-link {
font-size: 0.875rem;
line-height: 1.5;
}
}
:deep(.change-shop-link) {
a {
font-weight: 500;
}
}
:deep(.subheader-primary) {
h5.dark-header {
margin-bottom: 0.25rem !important;
}
}
</style>