DigitalConsumer.FixMyGlass/src/layouts/scheduling/scheduling.vue
2026-08-03 10:24:07 -04:00

947 lines
38 KiB
Vue

<template>
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm">
<interceptOverlay v-if="isLoadingDates || isLoadingMoreShops" />
<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">
<funnelSubHeader
cmsWidgetName="FunnelSubHeaderWidget"
:overrideHeaderSubText="estimatedTimeText"
alignLeft />
<schedulingZipSearch
ref="schedulingZipSearch"
v-model="zipSearchCode"
:disabled="isLoadingDates"
:pageNameToLog="pageName"
@zip-searched="onZipSearched" />
<datePicker
:key="datePickerKey"
class="mt-5"
v-model="selectedDate"
:startDate="datePickerStartDate"
:endDate="datePickerEndDate"
:availableDates="availableDates"
:isLoadingDates="isLoadingDates"
@requestMoreDates="handleRequestMoreDates" />
<Transition name="card-slide" mode="out-in">
<div :key="selectedDate">
<mobileSchedulingCard
v-if="showMobileSchedulingCard"
class="mt-4"
v-model="selectedScheduling"
:providerNumber="mobileProviderAndTimeSlot.providerNumber"
:timeSlots="mobileTimeSlotsForSelectedDate"
:premiumTimeSlotPrice="premiumTimeSlotPrice"
:zipCode="serviceZipCode"
:showFreeFlag="showMobileFreeFlag"
:radioGroupName="schedulingRadioGroupName"
:isLoading="isLoadingDates"
@zip-code-clicked="onMobileZipCodeClicked" />
<inshopSchedulingCard
v-for="entry in inshopProvidersAndTimeSlots"
v-show="showInshopSchedulingCards"
:key="entry.provider.providerNumber"
class="mt-4"
v-model="selectedScheduling"
:provider="entry.provider"
:timeSlots="
getInshopTimeSlotsForSelectedDate(entry.provider.providerNumber)
"
:radioGroupName="schedulingRadioGroupName"
:isLoading="isLoadingDates || Boolean(entry.isLoadingTimeSlots)"
@address-clicked="onInshopAddressClicked(entry.provider)" />
</div>
</Transition>
<div class="d-flex justify-content-center mt-4">
<textLink
v-if="hasMoreShopsAvailable"
id="viewMoreShopsLinkId"
linkType="text"
:text="viewMoreShopsText"
href="javascript:void(0)"
@click-event="onViewMoreShopsClick">
<template #after-text>
<span class="spacing-gap"></span>
<img
class="chevron-icon"
src="@/assets/img/icons/chevron-no-background.svg"
alt=""
aria-hidden="true" />
</template>
</textLink>
</div>
<waitlistQuestion
class="mt-5"
v-model="isWaitlistRequested"
:availableDates="availableDates" />
<navbar
cmsWidgetName="FunnelFooterWidget"
ref="navbar"
@back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction" />
</div>
</div>
</div>
<recalAckModal
modalWidgetName="RecalAckModalWidget"
agreementWidgetName="RecalAgreementQuestionWidget"
groupName="recalAckGroup"
@recalAcknowledged="onRecalAcknowledged"
ref="recalAckModal" />
</Form>
</template>
<script>
import funnelHeader from "@/fmg-components/funnel-header/funnel-header";
import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header";
import navbar from "@/fmg-components/nav-bar/nav-bar";
import { Form } from "vee-validate";
import datePicker from "@/layouts/scheduling/date-picker/date-picker";
import mobileSchedulingCard from "@/layouts/scheduling/mobile-scheduling-card/mobile-scheduling-card";
import inshopSchedulingCard from "@/layouts/scheduling/inshop-scheduling-card/inshop-scheduling-card";
import interceptOverlay from "@/ux-components/intercept-overlay/intercept-overlay";
import waitlistQuestion from "@/layouts/scheduling/waitlist-question/waitlist-question";
import schedulingZipSearch from "@/layouts/scheduling/scheduling-zip-search/scheduling-zip-search";
import recalAckModal from "@/layouts/schedule/recal-ack-modal/recal-ack-modal.vue";
import textLink from "@/ux-components/text-link/text-link";
import store from "@/store";
import { storeActions } from "@/constants/store-actions";
import {
AppointmentTypeStrings,
PREMIUM_FEE_PART_TYPE,
RECAL_ACK_YES,
} from "@/constants/schedule-constants";
import { isDropOffRouteCode } from "@/layouts/schedule/helpers/schedule-helper";
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { settleAllPromises } from "@/helpers/layout-helper";
import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
import { getDisplayTextForDurationLength } from "@/helpers/duration-length-helper";
import { partTypeStrings } from "@/constants/part-type-strings";
import {
flushPagePrereqsLogs,
hasServiceZipInfo,
hasGlassPartsOrRepairInfo,
hasInsuranceInfo,
} from "@/helpers/page-prerequisites-helper.js";
/**
* Returns a YYYY-MM-DD date string offset by the given number of days from a base date.
* @param {number} offsetDays - Number of days to offset (positive or negative).
* @param {Date} base - The starting date (defaults to today).
*/
function toDateString(offsetDays, base = new Date()) {
// Adding T00:00:00 ensures the date is treated as a local date in the browser's timezone.
const d = typeof base === "string" ? new Date(base + "T00:00:00") : new Date(base);
d.setDate(d.getDate() + offsetDays);
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
}
const SCHEDULE_FETCH_DAYS = 15;
const SCHEDULING_RADIO_GROUP_NAME = "schedulingTimeSlot";
const INITIAL_INSHOP_PROVIDER_COUNT = 3;
const MAX_INSHOP_PROVIDERS = 9;
const VIEW_MORE_SHOPS_BATCH_SIZE = 3;
const INSHOP_MAPS_PLACE_NAME = "safelite,Safelite Autoglass";
/**
* Appends days from newSlots into entry.timeSlots, initializing it if absent.
* @param {{ timeSlots: { days: any[] } | null }} entry
* @param {{ days: any[] } | null | undefined} newSlots
*/
function appendDays(entry, newSlots) {
if (!newSlots) return;
if (!entry.timeSlots) {
entry.timeSlots = newSlots;
} else {
entry.timeSlots.days = [...(entry.timeSlots.days ?? []), ...(newSlots.days ?? [])];
}
}
/**
* Assigns time slots from a v2 multi-provider response onto in-shop provider entries.
* @param {Array<{ provider: { providerNumber: string }, timeSlots: any }>} entries
* @param {{ providerTimeSlots?: Array<{ providerNumber: string, days: any[], provisionalTriggers?: string[] }>, estimatedServiceMinutesMinimum?: number, estimatedServiceMinutesMaximum?: number } | null | undefined} multiProviderResponse
*/
function assignInshopTimeSlotsFromV2Response(entries, multiProviderResponse) {
const providerTimeSlots = multiProviderResponse?.providerTimeSlots ?? [];
entries.forEach((entry) => {
const matchedSlots =
providerTimeSlots.find((pts) => pts.providerNumber === entry.provider.providerNumber) ??
null;
if (matchedSlots) {
matchedSlots.estimatedServiceMinutesMinimum =
matchedSlots.estimatedServiceMinutesMinimum ??
multiProviderResponse?.estimatedServiceMinutesMinimum;
matchedSlots.estimatedServiceMinutesMaximum =
matchedSlots.estimatedServiceMinutesMaximum ??
multiProviderResponse?.estimatedServiceMinutesMaximum;
}
entry.timeSlots = matchedSlots;
});
}
/**
* Appends days from a v2 multi-provider response onto existing in-shop provider entries.
* @param {Array<{ provider: { providerNumber: string }, timeSlots: any }>} entries
* @param {{ providerTimeSlots?: Array<{ providerNumber: string, days: any[] }> } | null | undefined} multiProviderResponse
*/
function appendInshopTimeSlotsFromV2Response(entries, multiProviderResponse) {
const providerTimeSlots = multiProviderResponse?.providerTimeSlots ?? [];
entries.forEach((entry) => {
const newSlots = providerTimeSlots.find(
(pts) => pts.providerNumber === entry.provider.providerNumber
);
appendDays(entry, newSlots);
});
}
/**
* Returns a promise for inshop time slots for multiple providers.
* @param {{ startDate: string, endDate: string, providerNumbers: string[], pageNameToLog: string }} params
*/
function fetchInshopTimeSlots({ startDate, endDate, providerNumbers, pageNameToLog }) {
return store.dispatch("getShopTimeSlotsV2", {
payload: {
startDate,
endDate,
shopAppointmentType: "InshopOrDropoff",
providerNumbers,
},
pageNameToLog,
});
}
/**
* Returns a promise for mobile time slots.
* @param {{ startDate: string, endDate: string, zipCode: string, pageNameToLog: string }} params
*/
function fetchMobileTimeSlots({ startDate, endDate, zipCode, pageNameToLog }) {
return store.dispatch("getMobileTimeSlotsV2", {
payload: {
startDate,
endDate,
zipCode,
},
pageNameToLog,
});
}
/**
* Fetches inshop time slots (when providers are given) and mobile time slots (when requested)
* for a date range, settling both requests together. Either request is omitted entirely when
* it has no applicable providers/zip, rather than firing an empty/unnecessary call.
* @param {{
* startDate: string,
* endDate: string,
* providerNumbers: string[],
* zipCode?: string,
* includeMobile?: boolean,
* pageNameToLog: string,
* }} params
* @returns {Promise<{ inshopTimeSlots?: any, mobileTimeSlots?: any }>}
*/
function fetchTimeSlotsBatch({
startDate,
endDate,
providerNumbers,
zipCode,
includeMobile,
pageNameToLog,
}) {
return settleAllPromises([
...(providerNumbers.length
? [
{
resultKey: "inshopTimeSlots",
promise: fetchInshopTimeSlots({
startDate,
endDate,
providerNumbers,
pageNameToLog,
}),
},
]
: []),
...(includeMobile
? [
{
resultKey: "mobileTimeSlots",
promise: fetchMobileTimeSlots({
startDate,
endDate,
zipCode,
pageNameToLog,
}),
},
]
: []),
]);
}
export default {
name: "scheduling",
async beforeRouteEnter(to, from, next) {
const serviceZipCode = store.getters.order.serviceLocation.zipCode;
const promiseResultMap = [
{
resultKey: "cmsContent",
promise: fetchCmsContentForPage(to.name),
},
{
resultKey: "mobilePremiumFee",
promise: store.dispatch("getMobilePremiumFee", {
pageNameToLog: to.name,
}),
},
{
resultKey: "providers",
promise: store.dispatch("getProviders", {
payload: { serviceZipCode },
pageNameToLog: to.name,
}),
},
];
const resultMap = await settleAllPromises(promiseResultMap);
next(async (vm) => {
vm.setCmsContent(resultMap.cmsContent);
await vm.loadSchedulingData(serviceZipCode, {
providersResult: resultMap.providers,
pageNameToLog: to.name,
});
vm.mobilePremiumAppointmentFee = resultMap.mobilePremiumFee ?? null;
vm.isLoadingDates = false;
});
},
watch: {
selectedDate() {
this.selectedScheduling = null;
},
},
computed: {
schedulingRadioGroupName() {
return SCHEDULING_RADIO_GROUP_NAME;
},
serviceLocationText() {
return this.getCmsContent("ServiceLocationText", "Text");
},
viewMoreShopsText() {
return this.getCmsContent("ViewMoreShopsWidget", "Text");
},
estimatedTimeText() {
if (!this.estimatedServiceMinutesMinimum || !this.estimatedServiceMinutesMaximum) {
return " ";
}
const durationText = getDisplayTextForDurationLength(
this.estimatedServiceMinutesMinimum,
this.estimatedServiceMinutesMaximum
);
return this.getCmsContent("FunnelSubHeaderWidget", "HeaderSubText").replace(
"{custom:DURATION}",
durationText
);
},
serviceZipCode() {
return store.getters.order.serviceLocation.zipCode;
},
showMobileSchedulingCard() {
return Boolean(
this.mobileProviderAndTimeSlot && (this.selectedDate || this.isLoadingDates)
);
},
showInshopSchedulingCards() {
return this.selectedDate || this.isLoadingDates;
},
mobileTimeSlotsForSelectedDate() {
if (!this.selectedDate) {
return [];
}
const day = this.mobileProviderAndTimeSlot?.timeSlots?.days?.find(
(d) => d.date === this.selectedDate
);
return day?.timeSlots ?? [];
},
premiumTimeSlotPrice() {
if (this.mobilePremiumAppointmentFee?.partType !== PREMIUM_FEE_PART_TYPE) {
return null;
}
return this.getTotalLineItemPrice(this.mobilePremiumAppointmentFee);
},
showMobileFreeFlag() {
return true;
},
availableDates() {
if (!this.datesLoaded) return null;
const inshopDates = this.inshopProvidersAndTimeSlots.flatMap(({ timeSlots }) =>
(timeSlots?.days ?? []).map((d) => d.date)
);
const mobileDates = (this.mobileProviderAndTimeSlot?.timeSlots?.days ?? []).map(
(d) => d.date
);
return [...new Set([...inshopDates, ...mobileDates])].sort();
},
hasMoreShopsAvailable() {
const maxVisible = Math.min(MAX_INSHOP_PROVIDERS, this.allShopProviders.length);
return this.inshopProvidersAndTimeSlots.length < maxVisible;
},
},
data() {
return {
selectedDate: null,
isLoadingDates: true,
datesLoaded: false,
datePickerStartDate: toDateString(0),
datePickerEndDate: toDateString(SCHEDULE_FETCH_DAYS - 1),
estimatedServiceMinutesMinimum: null,
estimatedServiceMinutesMaximum: null,
inshopProvidersAndTimeSlots: [],
allShopProviders: [],
mobileProviderAndTimeSlot: null,
mobilePremiumAppointmentFee: null,
selectedScheduling: null,
isWaitlistRequested: false,
isLoadingMoreShops: false,
zipSearchCode: store.getters.order.serviceLocation.zipCode ?? "",
datePickerKey: 0,
billToAccountNumber: store.getters.order.payment?.billToAccountNumber ?? null,
isRecalAcknowledgedForScheduling:
store.getters.order.isRecalAcknowledgedForScheduling ?? "",
};
},
methods: {
async loadSchedulingData(
serviceZipCode,
{ providersResult = null, pageNameToLog = this.pageName } = {}
) {
let providersData = providersResult;
if (!providersData) {
providersData = await store.dispatch("getProviders", {
payload: { serviceZipCode },
pageNameToLog,
});
}
if (providersData?.shopProviders === undefined) {
providersData = providersData?.data ?? {};
}
const allShopProviders = providersData.shopProviders ?? [];
const providers = allShopProviders.slice(0, INITIAL_INSHOP_PROVIDER_COUNT);
const mobileProviderNumber = providersData?.mobileProviderNumber ?? null;
this.allShopProviders = allShopProviders;
this.inshopProvidersAndTimeSlots = providers.map((provider) => ({
provider,
timeSlots: null,
}));
this.mobileProviderAndTimeSlot = mobileProviderNumber
? { providerNumber: mobileProviderNumber, timeSlots: null }
: null;
var gaLabel = this.GaLabels.NO;
if (this.mobileProviderAndTimeSlot) {
gaLabel = this.GaLabels.YES;
}
this.pushEventToGA(
this.GaCategories.APPOINTMENT,
this.GaActions.MOBILE_AVAILABLE,
gaLabel,
true
);
const startDate = toDateString(0);
const endDate = toDateString(SCHEDULE_FETCH_DAYS - 1);
const providerNumbers = providers.map((provider) => provider.providerNumber);
const timeSlotsResultMap = await fetchTimeSlotsBatch({
startDate,
endDate,
providerNumbers,
zipCode: serviceZipCode,
includeMobile: Boolean(mobileProviderNumber),
pageNameToLog,
});
if (timeSlotsResultMap.inshopTimeSlots) {
this.estimatedServiceMinutesMinimum =
timeSlotsResultMap.inshopTimeSlots.estimatedServiceMinutesMinimum;
this.estimatedServiceMinutesMaximum =
timeSlotsResultMap.inshopTimeSlots.estimatedServiceMinutesMaximum;
} else if (timeSlotsResultMap.mobileTimeSlots) {
this.estimatedServiceMinutesMinimum =
timeSlotsResultMap.mobileTimeSlots.estimatedServiceMinutesMinimum;
this.estimatedServiceMinutesMaximum =
timeSlotsResultMap.mobileTimeSlots.estimatedServiceMinutesMaximum;
} else {
this.estimatedServiceMinutesMinimum = null;
this.estimatedServiceMinutesMaximum = null;
}
assignInshopTimeSlotsFromV2Response(
this.inshopProvidersAndTimeSlots,
timeSlotsResultMap.inshopTimeSlots
);
if (this.mobileProviderAndTimeSlot) {
this.mobileProviderAndTimeSlot.timeSlots =
timeSlotsResultMap.mobileTimeSlots ?? null;
}
this.datesLoaded = true;
},
getInshopTimeSlotsForSelectedDate(providerNumber) {
if (!this.selectedDate) {
return [];
}
const providerEntry = this.inshopProvidersAndTimeSlots.find(
({ provider }) => provider.providerNumber === providerNumber
);
const day = providerEntry?.timeSlots?.days?.find((d) => d.date === this.selectedDate);
return day?.timeSlots ?? [];
},
buildInshopMapsQuery(address) {
const streetAddress = address?.streetAddress?.trim();
if (!streetAddress) {
return null;
}
const street = [streetAddress, address.streetAddress2?.trim()]
.filter(Boolean)
.join(" ");
return `${INSHOP_MAPS_PLACE_NAME}, ${street}, ${address.city}, ${address.state} ${address.zipCode}`;
},
buildInshopMapsUrl(address) {
const query = this.buildInshopMapsQuery(address);
if (!query) {
return null;
}
if (this.isAppleBrowser()) {
return `https://maps.apple.com/?q=${encodeURIComponent(query)}`;
}
return `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(query)}`;
},
onInshopAddressClicked(provider) {
const url = this.buildInshopMapsUrl(provider?.address);
if (!url) {
return;
}
window.open(url, "_blank", "noopener,noreferrer");
},
onMobileZipCodeClicked() {
this.$refs.schedulingZipSearch?.focusZipInput();
},
async onZipSearched({ zipCode, billToAccountNumber }) {
// When recal ack applies, changing zip must re-run parts selection via service-zip.
if (this.shouldShowRecalAckModal()) {
this.$router.navigateWithSaving(
this.navigationScenarios.ZIP_CODE_CHANGED_RECAL_ACK,
this.pageName
);
return;
}
this.billToAccountNumber = billToAccountNumber;
this.isLoadingDates = true;
try {
this.selectedDate = null;
this.selectedScheduling = null;
this.isWaitlistRequested = false;
this.isLoadingMoreShops = false;
this.datesLoaded = false;
this.datePickerKey += 1;
this.datePickerStartDate = toDateString(0);
this.datePickerEndDate = toDateString(SCHEDULE_FETCH_DAYS - 1);
await this.loadSchedulingData(zipCode);
} finally {
this.isLoadingDates = false;
}
},
async onViewMoreShopsClick() {
if (this.isLoadingDates || this.isLoadingMoreShops) return;
const maxVisible = Math.min(MAX_INSHOP_PROVIDERS, this.allShopProviders.length);
const remainingCapacity = maxVisible - this.inshopProvidersAndTimeSlots.length;
if (remainingCapacity <= 0) return;
const displayedProviderNumbers = new Set(
this.inshopProvidersAndTimeSlots.map(({ provider }) => provider.providerNumber)
);
const newProviders = this.allShopProviders
.filter((provider) => !displayedProviderNumbers.has(provider.providerNumber))
.slice(0, Math.min(VIEW_MORE_SHOPS_BATCH_SIZE, remainingCapacity));
if (!newProviders.length) return;
const newEntries = newProviders.map((provider) => ({
provider,
timeSlots: null,
isLoadingTimeSlots: true,
}));
this.inshopProvidersAndTimeSlots = [...this.inshopProvidersAndTimeSlots, ...newEntries];
this.isLoadingMoreShops = true;
try {
const resultMap = await fetchTimeSlotsBatch({
startDate: this.datePickerStartDate,
endDate: this.datePickerEndDate,
providerNumbers: newProviders.map((provider) => provider.providerNumber),
pageNameToLog: this.pageName,
});
assignInshopTimeSlotsFromV2Response(newEntries, resultMap.inshopTimeSlots);
} finally {
newEntries.forEach((entry) => {
entry.isLoadingTimeSlots = false;
});
this.isLoadingMoreShops = false;
}
},
arePagePrerequisitesValid() {
const order = store.getters.order;
const logQueue = [];
const serviceZip = hasServiceZipInfo(order, logQueue);
const glassPartsOrRepair = hasGlassPartsOrRepairInfo(order, logQueue);
const insuranceInfo = hasInsuranceInfo(order, logQueue);
const preReqResult = serviceZip && insuranceInfo && glassPartsOrRepair;
flushPagePrereqsLogs("scheduling.vue", preReqResult, logQueue);
return preReqResult;
},
backButtonAction() {
const payment = store.getters.order.payment;
if (
payment?.isInsurance &&
(payment?.insuranceCoverage?.isVerified ||
store.getters.order.referralNumber.length === 6)
) {
navigateToHeritageFunnel({
shouldSaveSession: false,
pageNameToLog: this.pageName,
navType: "back",
});
} else {
this.$router.navigateWithoutSaving(
this.navigationScenarios.CLICKED_BACK,
this.pageName
);
}
},
async handleRequestMoreDates() {
this.isLoadingDates = true;
const newStartDate = toDateString(1, this.datePickerEndDate);
const newEndDate = toDateString(SCHEDULE_FETCH_DAYS, this.datePickerEndDate);
const providerNumbers = this.inshopProvidersAndTimeSlots.map(
({ provider }) => provider.providerNumber
);
const resultMap = await fetchTimeSlotsBatch({
startDate: newStartDate,
endDate: newEndDate,
providerNumbers,
zipCode: this.serviceZipCode,
includeMobile: Boolean(this.mobileProviderAndTimeSlot),
pageNameToLog: this.pageName,
});
appendInshopTimeSlotsFromV2Response(
this.inshopProvidersAndTimeSlots,
resultMap.inshopTimeSlots
);
if (this.mobileProviderAndTimeSlot) {
appendDays(this.mobileProviderAndTimeSlot, resultMap.mobileTimeSlots);
}
this.datePickerEndDate = newEndDate;
this.isLoadingDates = false;
},
getInShopOrDropOffApptType(selectedScheduling = this.selectedScheduling) {
if (selectedScheduling?.appointmentType === AppointmentTypeStrings.MOBILE) {
return AppointmentTypeStrings.MOBILE;
}
return isDropOffRouteCode(selectedScheduling?.routeCodeId)
? AppointmentTypeStrings.DROP_OFF
: AppointmentTypeStrings.IN_SHOP;
},
getSelectedProvider(appointmentType, selectedScheduling = this.selectedScheduling) {
if (appointmentType === AppointmentTypeStrings.MOBILE) {
return {
providerNumber: selectedScheduling?.providerNumber,
address: {
streetAddress: null,
city: null,
state: null,
zipCode: null,
zipCodeCtu: null,
},
};
}
return (
this.inshopProvidersAndTimeSlots.find(
({ provider }) => provider.providerNumber === selectedScheduling?.providerNumber
)?.provider ?? null
);
},
getSelectedTimeSlot(
appointmentType,
selectedScheduling = this.selectedScheduling,
selectedDate = this.selectedDate
) {
const routeCodeId = selectedScheduling?.routeCodeId;
if (!routeCodeId || !selectedDate) {
return null;
}
let timeSlots;
if (appointmentType === AppointmentTypeStrings.MOBILE) {
const day = this.mobileProviderAndTimeSlot?.timeSlots?.days?.find(
(d) => d.date === selectedDate
);
timeSlots = day?.timeSlots ?? [];
} else {
const providerEntry = this.inshopProvidersAndTimeSlots.find(
({ provider }) => provider.providerNumber === selectedScheduling.providerNumber
);
const day = providerEntry?.timeSlots?.days?.find((d) => d.date === selectedDate);
timeSlots = day?.timeSlots ?? [];
}
return timeSlots.find((timeSlot) => timeSlot.id === routeCodeId) ?? null;
},
getEstimatedServiceMinutes(appointmentType, selectedScheduling = this.selectedScheduling) {
if (appointmentType === AppointmentTypeStrings.MOBILE) {
return {
minimum:
this.mobileProviderAndTimeSlot?.timeSlots?.estimatedServiceMinutesMinimum,
maximum:
this.mobileProviderAndTimeSlot?.timeSlots?.estimatedServiceMinutesMaximum,
};
}
const providerEntry = this.inshopProvidersAndTimeSlots.find(
({ provider }) => provider.providerNumber === selectedScheduling?.providerNumber
);
return {
minimum: providerEntry?.timeSlots?.estimatedServiceMinutesMinimum,
maximum: providerEntry?.timeSlots?.estimatedServiceMinutesMaximum,
};
},
updateSupportingItems(selectedScheduling = this.selectedScheduling) {
const supportingItems = store.getters.lineItems?.supportingItems;
if (!supportingItems) {
return;
}
const isPremiumMobile =
selectedScheduling?.appointmentType === AppointmentTypeStrings.MOBILE &&
selectedScheduling?.isPremiumAppointment;
if (isPremiumMobile && this.mobilePremiumAppointmentFee) {
const premiumFeeIndex = supportingItems.findIndex(
(item) => item.partType == PREMIUM_FEE_PART_TYPE
);
if (premiumFeeIndex > -1) {
supportingItems[premiumFeeIndex].laborAmount =
this.mobilePremiumAppointmentFee.laborAmount;
supportingItems[premiumFeeIndex].sellingPrice =
this.mobilePremiumAppointmentFee.sellingPrice;
supportingItems[premiumFeeIndex].kitPrice =
this.mobilePremiumAppointmentFee.kitPrice;
} else {
supportingItems.push(this.mobilePremiumAppointmentFee);
}
} else {
const removePremiumFeeIndex = supportingItems.findIndex(
(item) => item.partType == PREMIUM_FEE_PART_TYPE
);
if (removePremiumFeeIndex >= 0) {
supportingItems.splice(removePremiumFeeIndex, 1);
}
}
this.dispatchStoreAction(
this.storeActions.SAVE_SUPPORTING_ITEMS_SUPPRESSING_STATE_RESETTING,
supportingItems,
false
);
},
async forwardButtonAction() {
if (this.shouldShowRecalAckModal()) {
this.$refs.navbar.removeLoader();
this.$refs.recalAckModal.openModal();
return;
}
const selectedScheduling = this.selectedScheduling;
const selectedDate = this.selectedDate;
await store.dispatch(
storeActions.SAVE_IS_RECAL_ACKNOWLEDGED_FOR_SCHEDULING,
this.isRecalAcknowledgedForScheduling
);
const appointmentType = this.getInShopOrDropOffApptType(selectedScheduling);
const selectedProvider = this.getSelectedProvider(appointmentType, selectedScheduling);
const serviceLocation = store.getters.order.serviceLocation;
const isMobile = appointmentType === AppointmentTypeStrings.MOBILE;
let zipCodeCtu = serviceLocation.zipCodeCtu;
if (
!isMobile &&
selectedProvider?.address?.zipCodeCtu &&
zipCodeCtu !== selectedProvider.address.zipCodeCtu
) {
zipCodeCtu = selectedProvider.address.zipCodeCtu;
}
await this.dispatchStoreAction(
this.storeActions.SAVE_SERVICE_LOCATION,
{
address: isMobile ? serviceLocation.address : "",
address2: isMobile ? serviceLocation.address2 : "",
city: isMobile ? serviceLocation.city : "",
state: serviceLocation.state,
zipCode: serviceLocation.zipCode,
zipCodeCtu: zipCodeCtu,
appointmentType,
isVehicleProtected: isMobile ? serviceLocation.isVehicleProtected : null,
provider: {
providerNumber: selectedProvider?.providerNumber,
address: {
streetAddress: selectedProvider?.address?.streetAddress ?? null,
city: selectedProvider?.address?.city ?? null,
state: selectedProvider?.address?.state ?? null,
zipCode: selectedProvider?.address?.zipCode ?? null,
zipCodeCtu: selectedProvider?.address?.zipCodeCtu ?? null,
},
},
},
false
);
if (this.billToAccountNumber) {
this.dispatchStoreAction(
this.storeActions.SAVE_BILL_TO_ACCOUNT_NUMBER,
this.billToAccountNumber,
false
);
}
this.updateSupportingItems(selectedScheduling);
const selectedTimeSlot = this.getSelectedTimeSlot(
appointmentType,
selectedScheduling,
selectedDate
);
const estimatedServiceMinutes = this.getEstimatedServiceMinutes(
appointmentType,
selectedScheduling
);
this.dispatchStoreAction(
this.storeActions.SAVE_SCHEDULE,
{
date: selectedDate,
routeCode: selectedScheduling?.routeCodeId ?? null,
startTime: selectedTimeSlot?.startTime ?? null,
endTime: selectedTimeSlot?.endTime ?? null,
jobMinMinutes: estimatedServiceMinutes.minimum?.toString() ?? null,
jobMaxMinutes: estimatedServiceMinutes.maximum?.toString() ?? null,
},
false
);
if (this.isWaitlistRequested !== null && this.isWaitlistRequested !== undefined) {
this.dispatchStoreAction(
this.storeActions.SAVE_WAITLIST_REQUESTED,
this.isWaitlistRequested,
false
);
}
if (isMobile) {
this.$router.navigateWithSaving(
this.navigationScenarios.CLICKED_FORWARD_WITH_MOBILE_SERVICE,
this.pageName
);
} else {
this.$router.navigateWithSaving(
this.navigationScenarios.CLICKED_FORWARD,
this.pageName
);
}
},
shouldShowRecalAckModal() {
if (this.isRecalAcknowledgedForScheduling === RECAL_ACK_YES) {
return false;
}
const glassParts = store.getters.order.lineItems?.glassParts ?? [];
return glassParts.some(
(part) =>
part.partType === partTypeStrings.WINDSHIELD &&
part.requiresRecalibration === true &&
part.canSafeliteRecalibrate === false
);
},
onRecalAcknowledged(isAcknowledged) {
this.isRecalAcknowledgedForScheduling = isAcknowledged;
this.forwardButtonAction();
},
},
components: {
funnelHeader,
funnelSubHeader,
navbar,
Form,
datePicker,
mobileSchedulingCard,
inshopSchedulingCard,
interceptOverlay,
waitlistQuestion,
schedulingZipSearch,
recalAckModal,
textLink,
},
};
</script>
<style lang="scss" scoped>
.dark-header {
color: $black;
}
h5 {
line-height: 32px;
font-size: 1.25rem;
}
.card-slide-enter-active {
transition:
opacity 0.3s ease-out,
transform 0.3s ease-out;
}
.card-slide-leave-active {
transition: opacity 0.32s ease-in;
}
.card-slide-enter-from {
opacity: 0;
transform: translateX(24px);
}
.card-slide-leave-to {
opacity: 0;
}
.chevron-icon {
width: 16px;
height: 16px;
flex-shrink: 0;
transform: rotate(180deg);
transition: transform 150ms linear;
vertical-align: baseline;
}
.spacing-gap {
margin-right: 6.5px;
}
:deep(.text-link) {
font-weight: 600;
}
</style>