DigitalConsumer.FixMyGlass/src/layouts/schedule/schedule.vue

473 lines
18 KiB
Vue

<template>
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm" v-slot="{ meta }">
<div class="page-container-grouped-styles">
<loadingModal ref="loadingModal" />
<funnelHeader cmsWidgetName="FunnelHeaderWidget" />
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" class="mt-5" />
<template v-if="ChangeShopLink.length">
<textBlock
cmsWidgetName="ChangeShopLink"
justifyText="center"
class="mb-3 text-link-small"
marginTopSizeOverride="1" />
</template>
<locationAlerts cmsWidgetPrefix="LocationAlert-" ref="locationAlerts" />
<datePicker
selectableDatesSetting="custom"
ref="datePicker"
v-model="selectedDate"
class="text-link-small"
:customSelectableDatesCallback="getAvailableDatesMethod"
validationRules="date-required"
@date-clicked="openInshopTimeSlotsModal" />
<timeSlotModalQuestion
ref="timeSlotModalQuestion"
customComponentId="timeSlotModalQuestion"
cmsWidgetName="TimeSlotModalQuestion"
mobilePremiumCmsWidgetName="MobilePremiumTimeSlotModal"
mobileCmsWidgetName="MobileTimeSlotModal"
dropoffCmsWidgetName="DropOffTimeSlotModal"
sameDayDropOffCmsWidgetName="SameDayDropOffTimeSlotModal"
overnightDropOffCmsWidgetName="OvernightDropOffTimeSlotModal"
v-model="selectedTimeSlot"
:appointmentType="appointmentType"
:premiumAppointmentFee="mobilePremiumAppointmentFee"
:dateAndTimeSlotData="timeSlotsForSelectedDate"
:estimatedServiceMinutesMinimum="selectableDatesData.estimatedServiceMinutesMinimum"
:estimatedServiceMinutesMaximum="selectableDatesData.estimatedServiceMinutesMaximum"
validationRules="time-slot-selection-required" />
<funnel-footer
cmsWidgetName="FunnelFooterWidget"
ref="funnelFooter"
:isForwardActionDisabled="!meta.valid"
@back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction" />
</div>
</Form>
</template>
<script>
// Components
import funnelHeader from "@/fmg-components/funnel-header/funnel-header";
import funnelFooter from "@/fmg-components/funnel-footer/funnel-footer";
import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header";
import loadingModal from "@/fmg-components/loading-modal/loading-modal.vue";
import { Form, defineRule } from "vee-validate";
import datePicker from "@/digital-components/date-picker/date-picker";
import locationAlerts from "@/layouts/schedule/location-alerts/location-alerts";
import timeSlotModalQuestion from "./time-slot-modal-question/time-slot-modal-question";
import textBlock from "@/digital-components/text-block/text-block";
// Supporting files
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import baseMixin from "@/mixins/base-mixin.js";
import { storeActions } from "@/constants/store-actions";
import { settleAllPromises } from "@/helpers/layout-helper";
import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
import { splitCopyOnCMSPlaceHolder } from "@/helpers/cms-content-helper";
import {
calcDaysBetweenDates,
convertDateStringToDate,
sumDateString,
} from "@/layouts/schedule/helpers/schedule-helper";
import { AppointmentTypeStrings, PREMIUM_FEE_PART_TYPE } from "@/constants/schedule-constants";
import { errorMessages } from "@/constants/error-messages";
import { required } from "@/helpers/validation-rules";
import store from "@/store";
defineRule("date-required", required(errorMessages.DATE_REQUIRED));
// DEFINE VALIDATION RULES
defineRule("time-slot-selection-required", (value) => {
if (value.date == null ||
value.startTime == null ||
value.endTime == null ||
value.routeCode == null ||
value.estimatedServiceMinutesMaximum) {
return errorMessages.DATE_REQUIRED;
}
return true;
});
// Define constants
const TIME_SLOTS_CALL_DAYS_LIMIT = 34; // needs to be 34 for API limits (35 does not consistently work)
const getAvailableDates = async (
startDateString,
endDateString,
appointmentType,
providerNumber
) => {
const apiEndDateLimit = sumDateString(startDateString, TIME_SLOTS_CALL_DAYS_LIMIT);
const difference = calcDaysBetweenDates(startDateString, endDateString);
const apiCallsCount = Math.ceil(difference / TIME_SLOTS_CALL_DAYS_LIMIT);
const storeActionConfigs = [];
const timeSlotsData = {};
timeSlotsData.days = [];
let apiStartDate = startDateString;
let apiEndDate = apiEndDateLimit;
for (let i = 1; i <= apiCallsCount; i++) {
let storeActionConfig;
if (i > 1) {
apiStartDate = sumDateString(apiEndDate, 1);
apiEndDate = sumDateString(apiStartDate, TIME_SLOTS_CALL_DAYS_LIMIT);
if (i === apiCallsCount) {
apiEndDate = endDateString;
}
}
if (appointmentType === AppointmentTypeStrings.MOBILE) {
storeActionConfig = {
storeAction: storeActions.GET_MOBILE_TIME_SLOTS,
payload: {
startDate: apiStartDate,
endDate: apiEndDate,
},
};
} else {
storeActionConfig = {
storeAction: storeActions.GET_SHOP_TIME_SLOTS,
payload: {
startDate: apiStartDate,
endDate: apiEndDate,
shopAppointmentType: appointmentType,
providerNumber: providerNumber,
},
};
}
storeActionConfigs.push(storeActionConfig);
}
// ASYNC METHOD
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) => {
const timeSlotsResponse = await baseMixin.methods.dispatchStoreAction(
storeAction.storeAction,
storeAction.payload,
false
);
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;
});
};
export default {
name: "schedule",
data() {
return {
selectedDate: this.getSelectedDate(),
selectedTimeSlot: this.getSelectedTimeSlot(),
selectableDatesData: [],
mobilePremiumAppointmentFee: null,
};
},
async beforeRouteEnter(to, from, next) {
// Call APIs
const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage);
let preSelectedDate = await store.getters.order.schedule.date;
if (!preSelectedDate || preSelectedDate.startTime === null) {
preSelectedDate = null;
}
const datePickerInitialDataPromise = await datePicker.methods.loadInitialData({
// setup config options for date-picker
selectableDatesSetting: "custom",
initialViewRowsToShow: 5,
customSelectableDatesCallback: getAvailableDates,
preSelectedDate: preSelectedDate,
});
const premiumFeePromise = baseMixin.methods.dispatchStoreAction(
storeActions.GET_MOBILE_PREMIUM_FEE
);
const premiumFeeWithPricePromise = premiumFeePromise.then((result) => {
if (result.data) {
return baseMixin.methods.dispatchStoreAction(
storeActions.PRICE_ORDER_ITEMS_AND_SAVE_SERVER_DATA,
{
availableLineItems: [result.data],
},
false
);
} else {
return result.data;
}
});
const alertReasonsPromise = locationAlerts.methods.loadInitialData(
store.getters.order.serviceLocation.zipCodeCtu,
store.getters.order.serviceLocation.provider?.address?.zipCodeCtu
);
// Settle promises and get results
const promiseResultMap = [
{
resultKey: "cmsContent",
promise: cmsContentPromise,
},
{
resultKey: "alertReasons",
promise: alertReasonsPromise,
},
{
resultKey: "datePickerInitialData",
promise: datePickerInitialDataPromise,
},
{
resultKey: "premiumFeeWithPrice",
promise: premiumFeeWithPricePromise,
},
];
const resultMap = await settleAllPromises(promiseResultMap);
// Call the "next" function to complete the transition to this page.
next((vm) => {
vm.setCmsContent(resultMap.cmsContent);
vm.$refs.datePicker.initializeComponent(resultMap.datePickerInitialData);
vm.$refs.locationAlerts.initializeComponent(resultMap.alertReasons);
vm.selectableDatesData = resultMap.datePickerInitialData.initialShopTimeSlotsResponse;
vm.mobilePremiumAppointmentFee = resultMap.premiumFeeWithPrice
? resultMap.premiumFeeWithPrice[0]
: null;
vm.updateFooterButtonText(vm.selectedTimeSlot);
});
},
computed: {
ChangeShopLinkText() {
return this.getCmsContent("ChangeShopLink", "Text");
},
ChangeShopLink() {
// Splits content when brackets are found in text so that text can be looped through and router-link can be injected when needed
return this.splitCopyOnCMSPlaceHolder(this.ChangeShopLinkText);
},
appointmentType() {
return this.$store.getters.order.serviceLocation.appointmentType;
},
timeSlotsForSelectedDate() {
if (!this.selectedDate) {
return null;
}
return this.selectableDatesData.days?.find(
(selectableDate) => selectableDate.date === this.selectedDate
);
},
},
methods: {
splitCopyOnCMSPlaceHolder,
arePagePrerequisitesValid() {
const serviceLocation = store.getters.order.serviceLocation;
const serviceLocationPreReqs =
serviceLocation.zipCode &&
serviceLocation.zipCodeCtu &&
serviceLocation.appointmentType &&
(serviceLocation.appointmentType === AppointmentTypeStrings.MOBILE ||
serviceLocation.provider.providerNumber);
const paymentInfo = store.getters.payment.isInsurance !== null;
const supportingItems = store.getters.lineItems.supportingItems !== null;
const damageInfo =
store.getters.order.damage.isRepair ||
(store.getters.order.lineItems?.glassParts != null &&
store.getters.order.lineItems.glassParts.length > 0);
return serviceLocationPreReqs && paymentInfo && supportingItems && damageInfo;
},
async getAvailableDatesMethod(startDate, endDate) {
const newShopTimeSlots = await getAvailableDates(
startDate,
endDate,
this.appointmentType,
this.$store.getters.order.serviceLocation.provider.providerNumber
);
// ADD API CALL RESULTS TO EXISTING DATE DATA
this.selectableDatesData.days = this.selectableDatesData.days.concat(
newShopTimeSlots.days
);
return newShopTimeSlots;
},
getServiceZipCtuCodeFromStore() {
return store.getters.order.serviceLocation.zipCodeCtu;
},
openInshopTimeSlotsModal() {
this.$refs["timeSlotModalQuestion"].openModal();
},
getSelectedTimeSlot() {
return store.getters.order.schedule;
},
getSelectedDate() {
return store.getters.order.schedule.date;
},
getSelectedRouteCode() {
return store.getters.order.schedule.routeCode;
},
getSupportingItems() {
return store.getters.lineItems.supportingItems;
},
isMobilePremiumFeeOnOrderInVuex() {
const supportingItemsFromVuex = store.getters.lineItems.supportingItems;
return !!supportingItemsFromVuex.filter(
(lineItem) => lineItem.partType === PREMIUM_FEE_PART_TYPE
).length;
},
updateFooterButtonText(timeSlot) {
let funnelFooterButtonText;
if (!timeSlot || !timeSlot.date) {
funnelFooterButtonText = "Continue";
} else {
funnelFooterButtonText = `Select ${this.convertSelectedDateToShortMonthAndDay(
timeSlot.date
)}`;
if (this.appointmentType === AppointmentTypeStrings.IN_SHOP) {
funnelFooterButtonText += ` at ${this.getDisplayTextForMilitaryTime(
timeSlot.startTime
)}`;
} else if (
this.appointmentType === AppointmentTypeStrings.MOBILE &&
!timeSlot.isPremiumAppointment
) {
funnelFooterButtonText += ` at ${this.getDisplayTextForMilitaryTime(
timeSlot.startTime,
true
)} - ${this.getDisplayTextForMilitaryTime(
timeSlot.endTime,
true
)}`;
}
}
this.$refs.funnelFooter.updateButtonText(funnelFooterButtonText);
},
convertSelectedDateToShortMonthAndDay(selectedDate) {
// This conversion ensures we don't get get GMT induced date changes
const dateObject = convertDateStringToDate(selectedDate);
// Ex: April 25
return dateObject.toLocaleDateString("en-us", { month: "short", day: "numeric" });
},
// Expected input: "HH:MM"
getDisplayTextForMilitaryTime(militaryTimeInput, shouldTrimMinutesIfEmpty = false) {
let hours = parseInt(militaryTimeInput.split(":")[0]);
const minutes = militaryTimeInput.split(":")[1];
const meridianNotation = hours > 11 ? "PM" : "AM";
if (hours > 12) {
hours -= 12;
}
if (shouldTrimMinutesIfEmpty && minutes === "00") {
return `${hours} ${meridianNotation}`;
} else {
return `${hours}:${minutes} ${meridianNotation}`;
}
},
backButtonAction() {
this.$router.navigateWithoutSaving(this.navigationScenarios.CLICKED_BACK, this.$route);
},
forwardButtonAction() {
this.updateSupportingItems();
this.dispatchStoreAction(this.storeActions.SAVE_SCHEDULE, this.selectedTimeSlot, false);
navigateToHeritageFunnel({ loadingModal: this.$refs.loadingModal });
},
updateSupportingItems() {
const supportingItems = this.getSupportingItems();
// if we have a premium fee(early bird), then save/update supporting items
if (
this.appointmentType === AppointmentTypeStrings.MOBILE &&
this.selectedTimeSlot?.isPremiumAppointment
) {
const earlyBirdIndex = supportingItems.findIndex(
(item) => item.partType == PREMIUM_FEE_PART_TYPE
);
if (earlyBirdIndex >= 0) {
supportingItems[earlyBirdIndex].laborAmount =
this.mobilePremiumAppointmentFee.laborAmount;
supportingItems[earlyBirdIndex].selingPrice =
this.mobilePremiumAppointmentFee.selingPrice;
supportingItems[earlyBirdIndex].kitPrice =
this.mobilePremiumAppointmentFee.kitPrice;
} else {
supportingItems.push(this.mobilePremiumAppointmentFee);
}
this.dispatchStoreAction(
this.storeActions.SAVE_SUPPORTING_ITEMS_SUPPRESSING_STATE_RESETTING,
supportingItems,
false
);
} else {
// if it's not a mobile and/or premium early bird, then make sure we remove any that may have been added
const removeEarlyBirdIndex = supportingItems.findIndex(
(item) => item.partType == PREMIUM_FEE_PART_TYPE
);
if (removeEarlyBirdIndex >= 0) {
supportingItems.splice(removeEarlyBirdIndex, 1);
this.dispatchStoreAction(
this.storeActions.SAVE_SUPPORTING_ITEMS_SUPPRESSING_STATE_RESETTING,
supportingItems,
false
);
}
}
},
},
watch: {
selectedTimeSlot(newValue) {
this.updateFooterButtonText(newValue);
},
},
components: {
funnelHeader,
funnelFooter,
funnelSubHeader,
Form,
loadingModal,
datePicker,
locationAlerts,
timeSlotModalQuestion,
textBlock,
},
};
</script>
<style lang="scss">
.text-link-small {
a,
.btn-link {
font-size: 0.875rem;
line-height: 1.5;
}
}
.funnel-sub-header {
h5.dark-header {
margin-bottom: 0.25rem;
}
}
</style>