310 lines
12 KiB
Vue
310 lines
12 KiB
Vue
<template>
|
|
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm">
|
|
<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">
|
|
<h5 class="fw-normal mb-0 dark-header" :class="headerColor">
|
|
<span>
|
|
{{ serviceLocationText }}
|
|
</span>
|
|
</h5>
|
|
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" alignLeft />
|
|
<datePicker
|
|
class="mt-5"
|
|
v-model="selectedDate"
|
|
:startDate="datePickerStartDate"
|
|
:endDate="datePickerEndDate"
|
|
:availableDates="availableDates"
|
|
:isLoadingDates="isLoadingDates"
|
|
@requestMoreDates="handleRequestMoreDates" />
|
|
<mobileSchedulingCard
|
|
v-if="showMobileSchedulingCard"
|
|
class="mt-4"
|
|
v-model="selectedMobileScheduling"
|
|
:timeSlots="mobileTimeSlotsForSelectedDate"
|
|
:premiumTimeSlotPrice="premiumTimeSlotPrice"
|
|
:zipCode="serviceZipCode"
|
|
:showFreeFlag="showMobileFreeFlag"
|
|
@zip-code-clicked="onMobileZipCodeClicked" />
|
|
<inshopSchedulingCard
|
|
v-for="{ provider } in inShopProvidersAndTimeslots"
|
|
v-show="selectedDate"
|
|
:key="provider.providerNumber"
|
|
class="mt-4"
|
|
v-model="selectedInshopSchedulingByProvider[provider.providerNumber]"
|
|
:provider="provider"
|
|
:timeSlots="getInshopTimeSlotsForSelectedDate(provider.providerNumber)"
|
|
:radioGroupName="`inshopSchedulingTimeSlot-${provider.providerNumber}`"
|
|
@address-clicked="onInshopAddressClicked(provider)" />
|
|
<navbar
|
|
cmsWidgetName="FunnelFooterWidget"
|
|
ref="navbar"
|
|
@back-clicked="backButtonAction"
|
|
@ForwardClicked="forwardButtonAction" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</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 loadingModal from "@/fmg-components/loading-modal/loading-modal.vue";
|
|
import store from "@/store";
|
|
import { AppointmentTypeStrings, PREMIUM_FEE_PART_TYPE } from "@/constants/schedule-constants";
|
|
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
|
|
import { settleAllPromises } from "@/helpers/layout-helper";
|
|
import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
|
|
import {
|
|
flushPagePrereqsLogs,
|
|
hasServiceZipInfo,
|
|
hasGlassPartsOrRepairInfo,
|
|
hasInsuranceInfo,
|
|
} from "@/helpers/page-prerequisites-helper.js";
|
|
import { debugLog } from "@/helpers/debug-log-helper";
|
|
|
|
/**
|
|
* 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()) {
|
|
const d = 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;
|
|
|
|
export default {
|
|
name: "scheduling",
|
|
async beforeRouteEnter(to, from, next) {
|
|
const serviceZipCode = store.getters.order.serviceLocation.zipCode;
|
|
|
|
const shopProviderData = await store.dispatch("getProviders", {
|
|
payload: { serviceZipCode },
|
|
pageNameToLog: to.name,
|
|
});
|
|
// Get the first 3 providers from the shopProviderData
|
|
const providers = shopProviderData?.data?.shopProviders?.slice(0, 3) ?? [];
|
|
const mobileProviderNumber = shopProviderData?.data?.mobileProviderNumber ?? null;
|
|
|
|
const startDate = toDateString(0);
|
|
const endDate = toDateString(SCHEDULE_FETCH_DAYS - 1);
|
|
|
|
const promiseResultMap = [
|
|
{
|
|
resultKey: "cmsContent",
|
|
promise: fetchCmsContentForPage(to.name),
|
|
},
|
|
{
|
|
resultKey: "mobilePremiumFee",
|
|
promise: store.dispatch("getMobilePremiumFee", {
|
|
pageNameToLog: to.name,
|
|
}),
|
|
},
|
|
...providers.map((provider, i) => ({
|
|
resultKey: `inshopTimeSlots_${i}`,
|
|
promise: store.dispatch("getShopTimeSlots", {
|
|
payload: {
|
|
startDate,
|
|
endDate,
|
|
shopAppointmentType: "InshopOrDropoff",
|
|
providerNumber: provider.providerNumber,
|
|
},
|
|
pageNameToLog: to.name,
|
|
}),
|
|
})),
|
|
// Get the mobile time slots if a mobile provider number is available
|
|
...(mobileProviderNumber
|
|
? [
|
|
{
|
|
resultKey: "mobileTimeSlots",
|
|
promise: store.dispatch("getMobileTimeSlots", {
|
|
payload: { startDate, endDate, zipCode: serviceZipCode },
|
|
pageNameToLog: to.name,
|
|
}),
|
|
},
|
|
]
|
|
: []),
|
|
];
|
|
const resultMap = await settleAllPromises(promiseResultMap);
|
|
next((vm) => {
|
|
vm.setCmsContent(resultMap.cmsContent);
|
|
vm.inShopProvidersAndTimeslots = providers.map((provider, i) => ({
|
|
provider,
|
|
timeSlots: resultMap[`inshopTimeSlots_${i}`] ?? null,
|
|
}));
|
|
vm.mobileProviderAndTimeSlot = mobileProviderNumber
|
|
? {
|
|
providerNumber: mobileProviderNumber,
|
|
timeSlots: resultMap.mobileTimeSlots ?? null,
|
|
}
|
|
: null;
|
|
vm.datesLoaded = true;
|
|
vm.mobilePremiumAppointmentFee = resultMap.mobilePremiumFee ?? null;
|
|
});
|
|
},
|
|
watch: {
|
|
selectedDate() {
|
|
this.selectedMobileScheduling = null;
|
|
this.selectedInshopSchedulingByProvider = {};
|
|
},
|
|
},
|
|
computed: {
|
|
serviceLocationText() {
|
|
return this.getCmsContent("ServiceLocationText", "Text");
|
|
},
|
|
serviceZipCode() {
|
|
return store.getters.order.serviceLocation.zipCode;
|
|
},
|
|
showMobileSchedulingCard() {
|
|
return Boolean(this.mobileProviderAndTimeSlot && this.selectedDate);
|
|
},
|
|
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();
|
|
},
|
|
},
|
|
data() {
|
|
return {
|
|
selectedDate: null,
|
|
isLoadingDates: false,
|
|
datesLoaded: false,
|
|
datePickerStartDate: toDateString(0),
|
|
datePickerEndDate: toDateString(SCHEDULE_FETCH_DAYS - 1),
|
|
inShopProvidersAndTimeslots: [],
|
|
mobileProviderAndTimeSlot: null,
|
|
mobilePremiumAppointmentFee: null,
|
|
selectedMobileScheduling: null,
|
|
selectedInshopSchedulingByProvider: {},
|
|
};
|
|
},
|
|
methods: {
|
|
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 ?? [];
|
|
},
|
|
onInshopAddressClicked(provider) {
|
|
// TODO: open Google Maps when address link functionality is implemented
|
|
console.log("onInshopAddressClicked", provider?.providerNumber);
|
|
},
|
|
onMobileZipCodeClicked() {
|
|
// TODO: open service zip modal when zip edit is implemented for scheduling page
|
|
},
|
|
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;
|
|
|
|
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;
|
|
this.$refs.loadingModal.showModal();
|
|
|
|
// TODO: call getShopTimeSlots with the next date range and append results
|
|
// to this.availableDates and advance this.datePickerEndDate
|
|
console.log("handleRequestMoreDates");
|
|
|
|
this.isLoadingDates = false;
|
|
this.$refs.loadingModal.hideModal();
|
|
},
|
|
forwardButtonAction() {
|
|
const appointmentType = store.getters.order.serviceLocation.appointmentType; // TODO: remove this once we have a proper appointment type
|
|
if (appointmentType === AppointmentTypeStrings.MOBILE) {
|
|
this.$router.navigateWithSaving(
|
|
this.navigationScenarios.CLICKED_FORWARD_WITH_MOBILE_SERVICE,
|
|
this.pageName
|
|
);
|
|
} else {
|
|
this.$router.navigateWithSaving(
|
|
this.navigationScenarios.CLICKED_FORWARD,
|
|
this.pageName
|
|
);
|
|
}
|
|
},
|
|
},
|
|
components: {
|
|
funnelHeader,
|
|
funnelSubHeader,
|
|
navbar,
|
|
Form,
|
|
datePicker,
|
|
mobileSchedulingCard,
|
|
inshopSchedulingCard,
|
|
loadingModal,
|
|
},
|
|
};
|
|
</script>
|
|
<style lang="scss" scoped>
|
|
.dark-header {
|
|
color: $black;
|
|
}
|
|
h5 {
|
|
line-height: 32px;
|
|
font-size: 1.25rem;
|
|
}
|
|
</style>
|