Initial Schedule Date Picker check-in ISS
This commit is contained in:
parent
ef72f212f7
commit
4afa129284
12 changed files with 2218 additions and 17 deletions
|
|
@ -15,6 +15,18 @@ const endpoints = Object.freeze({
|
|||
url: '/location/api/v1/location/alert-reasons',
|
||||
method: 'GET'
|
||||
},
|
||||
GetShopTimeSlots: {
|
||||
url: '/schedule/api/v1/schedule/shop-time-slots',
|
||||
method: 'POST'
|
||||
},
|
||||
GetMobileTimeSlots: {
|
||||
url: '/schedule/api/v1/schedule/mobile-time-slots',
|
||||
method: 'POST'
|
||||
},
|
||||
GetMobilePremiumFee: {
|
||||
url: '/parts/api/v1/parts/mobile-premium-fee',
|
||||
method: 'GET'
|
||||
},
|
||||
GetVehicleYears: {
|
||||
url: '/vehicle/api/v1/vehicle/years',
|
||||
method: 'GET'
|
||||
|
|
|
|||
|
|
@ -11,4 +11,14 @@ const RouteCodeFlags = {
|
|||
OVERNIGHT_DROP_OFF: 'OVERNIGHT DROP OFF'
|
||||
};
|
||||
|
||||
export { AppointmentTypeStrings, PREMIUM_TIME_SLOT_ID_FLAG, PREMIUM_FEE_PART_TYPE, RouteCodeFlags };
|
||||
const GET_SHOP_TIME_SLOTS = 'getShopTimeSlots';
|
||||
const GET_MOBILE_TIME_SLOTS = 'getMobileTimeSlots';
|
||||
|
||||
export {
|
||||
AppointmentTypeStrings,
|
||||
PREMIUM_TIME_SLOT_ID_FLAG,
|
||||
PREMIUM_FEE_PART_TYPE,
|
||||
RouteCodeFlags,
|
||||
GET_SHOP_TIME_SLOTS,
|
||||
GET_MOBILE_TIME_SLOTS
|
||||
};
|
||||
|
|
|
|||
1062
src/digital-components/date-picker/date-picker.vue
Normal file
1062
src/digital-components/date-picker/date-picker.vue
Normal file
File diff suppressed because it is too large
Load diff
26
src/digital-components/date-picker/mixins/constants.js
Normal file
26
src/digital-components/date-picker/mixins/constants.js
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
const TIMINGFUNC_MAP = {
|
||||
linear: (t) => t,
|
||||
'ease-in': (t) => t * t,
|
||||
'ease-out': (t) => t * (2 - t),
|
||||
'ease-in-out': (t) => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t)
|
||||
};
|
||||
const BUFFER_OFFSET = 10;
|
||||
|
||||
const MONTHS_OF_YEAR = [
|
||||
'January',
|
||||
'February',
|
||||
'March',
|
||||
'April',
|
||||
'May',
|
||||
'June',
|
||||
'July',
|
||||
'August',
|
||||
'September',
|
||||
'October',
|
||||
'November',
|
||||
'December'
|
||||
];
|
||||
|
||||
const DAYS_OF_WEEK = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
||||
|
||||
export { TIMINGFUNC_MAP, BUFFER_OFFSET, MONTHS_OF_YEAR, DAYS_OF_WEEK };
|
||||
10
src/digital-components/date-picker/mixins/helpers.js
Normal file
10
src/digital-components/date-picker/mixins/helpers.js
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
const selectableDaysOptions = Object.freeze({
|
||||
CUSTOM: 'custom',
|
||||
PAST: 'past'
|
||||
});
|
||||
|
||||
const requiredParameter = () => {
|
||||
throw new Error('parameter is required');
|
||||
};
|
||||
|
||||
export { selectableDaysOptions, requiredParameter };
|
||||
12
src/helpers/date-helper.js
Normal file
12
src/helpers/date-helper.js
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
const getDateDifferenceInDays = (startDate, endDate) => {
|
||||
const date1 = new Date(endDate);
|
||||
date1.setHours(0, 0, 0, 0);
|
||||
const date2 = new Date(startDate);
|
||||
date2.setHours(0, 0, 0, 0);
|
||||
// To calculate the time difference of two dates
|
||||
const DifferenceInTime = date1.getTime() - date2.getTime();
|
||||
// To calculate the no. of days between two dates
|
||||
return DifferenceInTime / (1000 * 3600 * 24);
|
||||
};
|
||||
|
||||
export default getDateDifferenceInDays;
|
||||
|
|
@ -46,15 +46,7 @@ export async function getAvailabilityRating(
|
|||
providerNumber
|
||||
) {
|
||||
// For a given shop provider number and date range, get the appointment time slots available
|
||||
const shopTimeSlots = await useMainStore().getShopTimeSlots(
|
||||
{
|
||||
providerNumber,
|
||||
startDate,
|
||||
endDate,
|
||||
shopAppointmentType
|
||||
},
|
||||
false
|
||||
);
|
||||
const shopTimeSlots = await useMainStore().getShopTimeSlots(startDate, endDate, shopAppointmentType, providerNumber);
|
||||
|
||||
const numberOfDaysToEvaluate = 2;
|
||||
const isGoodAvailability =
|
||||
|
|
|
|||
|
|
@ -27,4 +27,68 @@ export async function mockGetAlertReasons(ctu) {
|
|||
return Promise.resolve(retList);
|
||||
}
|
||||
|
||||
export function calcDaysBetweenDates(dateString1, dateString2) {
|
||||
const date1 = new Date(dateString1);
|
||||
const date2 = new Date(dateString2);
|
||||
const timeDifference = Math.abs(date2 - date1); // Calculate the time difference in milliseconds
|
||||
return Math.ceil(timeDifference / (1000 * 60 * 60 * 24)); // Convert milliseconds to days
|
||||
}
|
||||
|
||||
export function convertDateToDateString(date) {
|
||||
// returns YYYY-MM-DD format
|
||||
if (date instanceof Date !== true) return null;
|
||||
return (
|
||||
`${date.getFullYear()
|
||||
}-${
|
||||
(`0${date.getMonth() + 1}`).slice(-2)
|
||||
}-${
|
||||
(`0${date.getDate()}`).slice(-2)}`
|
||||
);
|
||||
}
|
||||
|
||||
export function convertDateStringToDate(dateString) {
|
||||
// dateString must be YYYY-MM-DD format
|
||||
if (typeof dateString !== 'string') return null;
|
||||
const dateParts = dateString.split('-');
|
||||
return new Date(dateParts[0], parseInt(dateParts[1], 10) - 1, dateParts[2]);
|
||||
}
|
||||
|
||||
export function sumDateString(dateString, daysToAdd) {
|
||||
// dateString must be YYYY-MM-DD format
|
||||
if (typeof dateString !== 'string') return null;
|
||||
const date = convertDateStringToDate(dateString);
|
||||
date.setDate(date.getDate() + daysToAdd);
|
||||
return convertDateToDateString(date);
|
||||
}
|
||||
|
||||
export function militaryToTwelveHourTime(timeString) {
|
||||
// Expected input: "HH:MM"
|
||||
if (typeof timeString !== 'string') return null;
|
||||
let hours = parseInt(timeString.split(':')[0], 10);
|
||||
const minutes = timeString.split(':')[1];
|
||||
const meridianNotation = hours > 11 ? 'PM' : 'AM';
|
||||
|
||||
if (hours > 12) {
|
||||
hours -= 12;
|
||||
}
|
||||
|
||||
return `${hours}:${minutes} ${meridianNotation}`;
|
||||
}
|
||||
|
||||
export function getDisplayTextForDurationLength(durationMinimum, durationMaximum) {
|
||||
const isLongAppointment = durationMaximum >= 120;
|
||||
const isDurationRange = durationMinimum !== durationMaximum;
|
||||
|
||||
const adjustedMinimum = isLongAppointment ? durationMinimum / 60 : durationMinimum;
|
||||
const adjustedMaximum = isLongAppointment ? durationMaximum / 60 : durationMaximum;
|
||||
|
||||
const durationText = isDurationRange
|
||||
? `${adjustedMinimum} - ${adjustedMaximum}`
|
||||
: adjustedMinimum;
|
||||
|
||||
const unitText = isLongAppointment ? 'hours' : 'minutes';
|
||||
|
||||
return `${durationText} ${unitText}`;
|
||||
}
|
||||
|
||||
export default getAlertReasons;
|
||||
|
|
|
|||
|
|
@ -21,6 +21,15 @@
|
|||
<locationAlerts
|
||||
ref="locationAlerts"
|
||||
cmsWidgetPrefix="LocationAlert-" />
|
||||
<datePicker
|
||||
ref="datePicker"
|
||||
v-model="selectedDate"
|
||||
customComponentId="dateQuestion"
|
||||
selectableDatesSetting="custom"
|
||||
class="text-link-small"
|
||||
:customSelectableDatesCallback="getAvailableDatesMethod"
|
||||
validationRules="date-required"
|
||||
@date-clicked="openInshopTimeSlotsModal" />
|
||||
<siteFooter
|
||||
ref="siteFooter"
|
||||
class="mt-5"
|
||||
|
|
@ -37,23 +46,141 @@
|
|||
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 siteFooter from '@/iss-components/site-footer/site-footer.vue';
|
||||
import textBlock from '@/digital-components/text-block/text-block.vue';
|
||||
|
||||
// Supporting files
|
||||
import { AppointmentTypeStrings } from '@/constants/schedule-constants.js';
|
||||
import baseMixin from '@/mixins/base-mixin.js';
|
||||
import {
|
||||
calcDaysBetweenDates,
|
||||
convertDateStringToDate,
|
||||
sumDateString
|
||||
} from '@/layouts/schedule-page/helpers/schedule-helper';
|
||||
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 settleAllPromises from '@/helpers/layout-helper';
|
||||
import { Form } from 'vee-validate';
|
||||
import { Form, defineRule } from 'vee-validate';
|
||||
import BaseFormMixin from '@/mixins/base-form-mixin.js';
|
||||
import errorMessages from '@/constants/error-messages';
|
||||
import { required } from '@/helpers/validation-rules';
|
||||
import { useMainStore } from '@/store';
|
||||
|
||||
// DEFINE VALIDATION RULES
|
||||
defineRule('date-required', required(errorMessages.DATE_REQUIRED));
|
||||
defineRule('time-slot-selection-required', (value) => {
|
||||
if (value?.timeSlot?.routeCode == null) {
|
||||
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 = 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);
|
||||
|
||||
if (i === apiCallsCount) {
|
||||
apiEndDate = endDateString;
|
||||
}
|
||||
} else if (apiEndDate > apiEndDateLimit) {
|
||||
apiEndDate = apiEndDateLimit;
|
||||
}
|
||||
|
||||
if (appointmentType === AppointmentTypeStrings.MOBILE) {
|
||||
storeActionConfig = {
|
||||
storeAction: GET_MOBILE_TIME_SLOTS,
|
||||
payload: {
|
||||
startDate: apiStartDate,
|
||||
endDate: apiEndDate
|
||||
}
|
||||
};
|
||||
} 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) {
|
||||
timeSlotsResponse = await useMainStore().getShopTimeSlots(
|
||||
storeAction.payload.startDate,
|
||||
storeAction.payload.endDate,
|
||||
storeAction.payload.appointmentType,
|
||||
storeAction.payload.providerNumber
|
||||
);
|
||||
} else {
|
||||
timeSlotsResponse = await useMainStore().getMobileTimeSlots(storeAction.payload.startDate, storeAction.payload.endDate);
|
||||
}
|
||||
|
||||
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-page',
|
||||
components: {
|
||||
siteHeader,
|
||||
siteSubHeader,
|
||||
locationAlerts,
|
||||
datePicker,
|
||||
siteFooter,
|
||||
textBlock,
|
||||
// eslint-disable-next-line vue/no-reserved-component-names
|
||||
|
|
@ -62,8 +189,20 @@ export default {
|
|||
mixins: [BaseFormMixin],
|
||||
async beforeRouteEnter(to, from, next) {
|
||||
// Call APIs
|
||||
let preSelectedDate = await useMainStore().order.schedule.date;
|
||||
if (!preSelectedDate || preSelectedDate.startTime === null) {
|
||||
preSelectedDate = null;
|
||||
}
|
||||
|
||||
const cmsContentPromise = fetchCmsContentForPage(to.query.issPage);
|
||||
|
||||
const datePickerInitialDataPromise = await datePicker.methods.loadInitialData({
|
||||
selectableDatesSetting: 'custom',
|
||||
initialViewRowsToShow: 5,
|
||||
customSelectableDatesCallback: getAvailableDates,
|
||||
preSelectedDate
|
||||
});
|
||||
|
||||
const alertReasonsPromise = locationAlerts.methods.loadInitialData(
|
||||
useMainStore().order.serviceLocation.zipCodeCtu,
|
||||
useMainStore().order.serviceLocation.provider?.address?.zipCodeCtu
|
||||
|
|
@ -78,6 +217,10 @@ export default {
|
|||
{
|
||||
resultKey: 'alertReasons',
|
||||
promise: alertReasonsPromise
|
||||
},
|
||||
{
|
||||
resultKey: 'datePickerInitialData',
|
||||
promise: datePickerInitialDataPromise
|
||||
}
|
||||
];
|
||||
|
||||
|
|
@ -85,7 +228,10 @@ export default {
|
|||
const resultMap = await settleAllPromises(promiseResultMap);
|
||||
next((vm) => {
|
||||
vm.setCmsContent(resultMap.cmsContent);
|
||||
vm.$refs.datePicker.initializeComponent(resultMap.datePickerInitialData);
|
||||
vm.$refs.locationAlerts.initializeComponent(resultMap.alertReasons);
|
||||
vm.setData(resultMap.datePickerInitialData.initialShopTimeSlotsResponse);
|
||||
vm.updateFooterButtonText(vm.selectedTimeSlotInfo);
|
||||
});
|
||||
},
|
||||
setup() {
|
||||
|
|
@ -100,8 +246,38 @@ export default {
|
|||
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.mainStore.order.serviceLocation.appointmentType;
|
||||
},
|
||||
timeSlotsForSelectedDate() {
|
||||
if (!this.selectedDate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.selectableDatesData.days?.find((selectableDate) => selectableDate.date === this.selectedDate);
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
selectedDate(newValue, oldValue) {
|
||||
// Clear time slot selection if date selected changes
|
||||
if (newValue !== oldValue) {
|
||||
this.selectedTimeSlotInfo = {
|
||||
timeSlot: {
|
||||
date: null,
|
||||
routeCode: null,
|
||||
startTime: null,
|
||||
endTime: null,
|
||||
jobMaxMinutes: null,
|
||||
jobMinMinutes: null
|
||||
},
|
||||
isPremiumAppointment: null
|
||||
};
|
||||
}
|
||||
},
|
||||
selectedTimeSlotInfo(newValue) {
|
||||
this.updateFooterButtonText(newValue);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
|
@ -121,20 +297,146 @@ export default {
|
|||
&& useMainStore().order.lineItems.glassParts.length > 0);
|
||||
return serviceLocationPreReqs && paymentInfo && supportingItems && damageInfo;
|
||||
},
|
||||
setData(initialShopTimeSlotsResponse) {
|
||||
this.selectableDatesData = initialShopTimeSlotsResponse;
|
||||
},
|
||||
async getAvailableDatesMethod(startDate, endDate) {
|
||||
const newShopTimeSlots = await getAvailableDates(
|
||||
startDate,
|
||||
endDate,
|
||||
this.appointmentType,
|
||||
this.mainStore.order.serviceLocation.provider.providerNumber
|
||||
);
|
||||
// ADD API CALL RESULTS TO EXISTING DATE DATA
|
||||
this.selectableDatesData.days = this.selectableDatesData.days.concat(newShopTimeSlots.days);
|
||||
return newShopTimeSlots;
|
||||
},
|
||||
getAvailableDates,
|
||||
getServiceZipCtuCodeFromStore() {
|
||||
return this.mainStore.order.serviceLocation.zipCodeCtu;
|
||||
},
|
||||
openInshopTimeSlotsModal() {
|
||||
this.$refs.timeSlotModalQuestion.openModal();
|
||||
},
|
||||
getSelectedDate() {
|
||||
return this.mainStore.order.schedule.date;
|
||||
},
|
||||
getSelectedTimeSlotInfo() {
|
||||
const supportingItems = this.getSupportingItems();
|
||||
const isPremiumAppointment =
|
||||
!!supportingItems.filter((lineItem) => lineItem.partType === PREMIUM_FEE_PART_TYPE)
|
||||
.length > 0;
|
||||
|
||||
const selectedTimeSlotInfo = {
|
||||
timeSlot: this.mainStore.order.schedule,
|
||||
isPremiumAppointment
|
||||
};
|
||||
|
||||
return selectedTimeSlotInfo;
|
||||
},
|
||||
getSupportingItems() {
|
||||
return this.mainStore.lineItems.supportingItems;
|
||||
},
|
||||
timeSlotModalClosed() {
|
||||
// Clear the selectedDate if no timeSlot has been selected
|
||||
if (this.selectedTimeSlotInfo.timeSlot.routeCode == null) {
|
||||
this.selectedDate = null;
|
||||
}
|
||||
},
|
||||
updateFooterButtonText(timeSlotInfo) {
|
||||
let navbarButtonText;
|
||||
if (!timeSlotInfo || !timeSlotInfo.timeSlot.date) {
|
||||
navbarButtonText = 'Continue';
|
||||
} else {
|
||||
navbarButtonText = `Select ${this.convertSelectedDateToShortMonthAndDay(timeSlotInfo.timeSlot.date)}`;
|
||||
if (this.appointmentType === AppointmentTypeStrings.IN_SHOP) {
|
||||
navbarButtonText += ` at ${this.getDisplayTextForMilitaryTime(timeSlotInfo.timeSlot.startTime)}`;
|
||||
} else if (
|
||||
this.appointmentType === AppointmentTypeStrings.MOBILE
|
||||
&& !timeSlotInfo.isPremiumAppointment
|
||||
) {
|
||||
navbarButtonText += ` at ${this.getDisplayTextForMilitaryTime(
|
||||
timeSlotInfo.timeSlot.startTime,
|
||||
true
|
||||
)} - ${this.getDisplayTextForMilitaryTime(
|
||||
timeSlotInfo.timeSlot.endTime,
|
||||
true
|
||||
)}`;
|
||||
}
|
||||
}
|
||||
this.$refs.navbar.updateButtonText(navbarButtonText);
|
||||
},
|
||||
convertSelectedDateToShortMonthAndDay(selectedDate) {
|
||||
// This conversion ensures we don't get get GMT induced date changes
|
||||
const dateObject = convertDateStringToDate(selectedDate);
|
||||
return dateObject.toLocaleDateString('en-us', { month: 'short', day: 'numeric' });
|
||||
},
|
||||
getDisplayTextForMilitaryTime(militaryTimeInput, shouldTrimMinutesIfEmpty = false) {
|
||||
// Expected input: "HH:MM"
|
||||
let hours = parseInt(militaryTimeInput.split(':')[0], 10);
|
||||
const minutes = militaryTimeInput.split(':')[1];
|
||||
const meridianNotation = hours > 11 ? 'PM' : 'AM';
|
||||
|
||||
if (hours > 12) {
|
||||
hours -= 12;
|
||||
}
|
||||
|
||||
if (shouldTrimMinutesIfEmpty && minutes === '00') {
|
||||
return `${hours} ${meridianNotation}`;
|
||||
}
|
||||
return `${hours}:${minutes} ${meridianNotation}`;
|
||||
},
|
||||
updateSupportingItems() {
|
||||
const supportingItems = this.getSupportingItems();
|
||||
|
||||
// if we have a premium fee(early bird), then save/update supporting items
|
||||
if (
|
||||
this.appointmentType === AppointmentTypeStrings.MOBILE
|
||||
&& this.selectedTimeSlotInfo?.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
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
backButtonAction() {
|
||||
this.$router.navigate(this.navigationScenarios.CLICKED_BACK, this.$route);
|
||||
},
|
||||
|
||||
forwardButtonAction() {
|
||||
// validate and save here
|
||||
this.$router.navigate(this.navigationScenarios.CLICKED_FORWARD, this.$route);
|
||||
},
|
||||
|
||||
navigateForward() {
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$page-side-padding: 1.5rem;
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,118 @@
|
|||
<template>
|
||||
<baseInputButton
|
||||
v-bind="$props"
|
||||
v-model="selectedValue"
|
||||
buttonWrapperClasses="list-group base-input-button list-button rounded-3 d-flex flex-column w-100 mb-2">
|
||||
<div
|
||||
:aria-label="buttonLabel"
|
||||
class="button-content list-button-content d-flex flex-column justify-content-center py-3 px-4">
|
||||
<span
|
||||
class="m-0 position-relative"
|
||||
:class="textPosition">
|
||||
{{ buttonLabel }}
|
||||
<span
|
||||
v-if="buttonLabelSubCopy"
|
||||
class="premium-appointment-price"
|
||||
:class="textPosition">
|
||||
{{ formattedButtonLabelSubCopy }}
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
v-if="screenReaderOnlyText"
|
||||
class="sr-only">
|
||||
{{ screenReaderOnlyText }}
|
||||
</span>
|
||||
</div>
|
||||
</baseInputButton>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import baseInputButton from '@/digital-components/base-input-button/base-input-button.vue';
|
||||
import inputButtonWrapperMixin from '@/mixins/input-button-wrapper-mixin';
|
||||
|
||||
export default {
|
||||
name: 'time-slot-modal-list-button',
|
||||
components: {
|
||||
baseInputButton
|
||||
},
|
||||
mixins: [inputButtonWrapperMixin],
|
||||
computed: {
|
||||
formattedButtonLabelSubCopy() {
|
||||
return this.buttonLabelSubCopy;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
displayLoader() {
|
||||
this.isLoaderDisplayed = true;
|
||||
},
|
||||
preHandleAnswerChange() {
|
||||
if (this.selectingInitiatesLoad) {
|
||||
this.displayLoader();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.list-button {
|
||||
outline: none;
|
||||
input[type="radio"],
|
||||
input[type="checkbox"] {
|
||||
position: static; //override bootstrap
|
||||
|
||||
&:focus-visible + .list-button-content {
|
||||
box-shadow: 0 0 0 2.5px $blue;
|
||||
}
|
||||
&:focus + .list-button-content {
|
||||
box-shadow: 0 0 0 2.5px $blue;
|
||||
}
|
||||
&:checked + .list-button-content {
|
||||
color: $black;
|
||||
font-weight: 500;
|
||||
background: $blue-100;
|
||||
box-shadow: 0 0 0 1px $blue;
|
||||
span.premium-appointment-price {
|
||||
background: $green-200;
|
||||
}
|
||||
}
|
||||
&:checked:focus + .list-button-content {
|
||||
box-shadow: 0 0 0 2.5px $blue;
|
||||
}
|
||||
&:checked + .list-button-content p,
|
||||
&:checked + .list-button-content span {
|
||||
font-weight: 500;
|
||||
}
|
||||
&:checked + .list-button-content span:nth-child(2) {
|
||||
font-weight: 400;
|
||||
color: $gray-600;
|
||||
}
|
||||
}
|
||||
}
|
||||
.list-button-content {
|
||||
color: $gray-600;
|
||||
position: relative;
|
||||
background: $white;
|
||||
transition: all 150ms linear;
|
||||
border-radius: $border-radius-lg;
|
||||
border: 1px solid $gray-500;
|
||||
width: 100%;
|
||||
outline: none;
|
||||
|
||||
span.premium-appointment-price {
|
||||
position: absolute;
|
||||
background: $green-100;
|
||||
border-radius: 4.5rem;
|
||||
line-height: 1.25rem;
|
||||
color: $green-700;
|
||||
font-size: 0.75rem;
|
||||
margin-left: 4px;
|
||||
padding: 2px 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.position-relative {
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,520 @@
|
|||
<template>
|
||||
<modal
|
||||
:ref="modalName"
|
||||
:headerText="dateSelectedReadableDate"
|
||||
:footerButtonText="footerCloseButtonText"
|
||||
:onModalClosedCallback="onModalClosed"
|
||||
class="time-slots-modal"
|
||||
@isModalOpened="setModalStatus"
|
||||
@footer-button-event="setSelectedTimeSlot">
|
||||
<template v-if="isModalOpened">
|
||||
<textBlock
|
||||
v-show="durationTextBlockCopy"
|
||||
:customText="durationTextBlockCopy"
|
||||
justifyText="center"
|
||||
typeStyle="small"
|
||||
class="duration-text-block" />
|
||||
<buttonQuestion
|
||||
ref="buttonQuestion"
|
||||
v-model="selectedRouteCode"
|
||||
buttonTypeString="timeSlotModalListButton"
|
||||
:buttonTypeObject="timeSlotModalListButton"
|
||||
class="mt-5"
|
||||
:answers="availableTimeSlots"
|
||||
groupName="chooseTimeSlot"
|
||||
textPosition="text-center"
|
||||
isRequired
|
||||
validationRules="time-slot-required" />
|
||||
<div
|
||||
v-if="supplementalInformationBlock"
|
||||
class="mt-1 mb-2 supplemental-information"
|
||||
v-html="supplementalInformationBlock"></div>
|
||||
<textBlock
|
||||
v-show="disclaimerTextBlockCopy"
|
||||
:customText="disclaimerTextBlockCopy"
|
||||
justifyText="left"
|
||||
typeStyle="caption"
|
||||
class="mb-2" />
|
||||
</template>
|
||||
</modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// Components
|
||||
import modal from '@/digital-components/modal/modal.vue';
|
||||
import textBlock from '@/digital-components/text-block/text-block.vue';
|
||||
import buttonQuestion from '@/digital-components/button-question/button-question.vue';
|
||||
|
||||
// Helpers
|
||||
import { deepClone } from '@/helpers/object-helper';
|
||||
|
||||
// Validation
|
||||
import { defineRule, useField } from 'vee-validate';
|
||||
import errorMessages from '@/constants/error-messages';
|
||||
import { required } from '@/helpers/validation-rules';
|
||||
|
||||
// Constants
|
||||
import {
|
||||
AppointmentTypeStrings,
|
||||
RouteCodeFlags,
|
||||
PREMIUM_TIME_SLOT_ID_FLAG,
|
||||
PREMIUM_FEE_PART_TYPE
|
||||
} from '@/constants/schedule-constants';
|
||||
import {
|
||||
convertDateStringToDate,
|
||||
militaryToTwelveHourTime,
|
||||
getDisplayTextForDurationLength
|
||||
} from '@/layouts/schedule-page/helpers/schedule-helper';
|
||||
import timeSlotModalListButton from './time-slot-modal-list-button/time-slot-modal-list-button.vue';
|
||||
|
||||
const cmsWidgetFieldMappings = {
|
||||
MODAL_CLOSE_BUTTON: 'FooterText',
|
||||
SUPPLEMENTAL_INFORMATION: 'BodyText',
|
||||
TIME_SLOT_BUTTON: 'HeaderText',
|
||||
DISCLAIMER: 'FooterText',
|
||||
DURATION: 'SubheaderText'
|
||||
};
|
||||
|
||||
// Validation for the modal button
|
||||
defineRule('time-slot-required', required(errorMessages.OPTION_REQUIRED));
|
||||
|
||||
export default {
|
||||
name: 'time-slot-modal-question',
|
||||
components: {
|
||||
modal,
|
||||
textBlock,
|
||||
buttonQuestion
|
||||
},
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
timeSlot: {
|
||||
routeCode: null,
|
||||
date: null,
|
||||
startTime: null,
|
||||
endTime: null,
|
||||
jobMaxMinutes: null,
|
||||
jobMinMinutes: null
|
||||
},
|
||||
isPremiumAppointment: null
|
||||
})
|
||||
},
|
||||
cmsWidgetName: String,
|
||||
mobileCmsWidgetName: String,
|
||||
mobilePremiumCmsWidgetName: String,
|
||||
dropoffCmsWidgetName: String,
|
||||
sameDayDropOffCmsWidgetName: String,
|
||||
overnightDropOffCmsWidgetName: String,
|
||||
appointmentType: String,
|
||||
timeSlotsForSelectedDate: Object,
|
||||
premiumAppointmentFee: Object,
|
||||
estimatedServiceMinutesMinimum: Number,
|
||||
estimatedServiceMinutesMaximum: Number,
|
||||
validationRules: String,
|
||||
customComponentId: String,
|
||||
selectedDate: String
|
||||
},
|
||||
emits: ['update:modelValue', 'time-slot-modal-closed'],
|
||||
setup(props) {
|
||||
const uuid = crypto.randomUUID();
|
||||
const componentId = !props.customComponentId
|
||||
? `component-${uuid}`
|
||||
: props.customComponentId;
|
||||
|
||||
const { modelValue } = deepClone(props);
|
||||
const initialValue = modelValue;
|
||||
|
||||
const fieldOptions = {
|
||||
value: modelValue,
|
||||
initialValue
|
||||
};
|
||||
|
||||
const { errorMessage, handleChange, meta, validate, errors } = useField(
|
||||
componentId,
|
||||
props.validationRules,
|
||||
fieldOptions
|
||||
);
|
||||
|
||||
return {
|
||||
componentId,
|
||||
errorMessage,
|
||||
handleChange,
|
||||
validate,
|
||||
meta,
|
||||
errors
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isModalOpened: false,
|
||||
selectedRouteCode: this.getSelectedRouteCode(),
|
||||
timeSlotModalListButton
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
modalName() {
|
||||
return 'timeSlots';
|
||||
},
|
||||
modal() {
|
||||
return this.$refs[this.modalName];
|
||||
},
|
||||
supplementalInformationBlock() {
|
||||
let appointmentTypeCmsWidgetName;
|
||||
if (this.appointmentType === AppointmentTypeStrings.IN_SHOP) {
|
||||
return null;
|
||||
} if (this.appointmentType === AppointmentTypeStrings.MOBILE) {
|
||||
appointmentTypeCmsWidgetName = this.selectedRouteCode?.includes(PREMIUM_TIME_SLOT_ID_FLAG)
|
||||
? this.mobilePremiumCmsWidgetName
|
||||
: this.mobileCmsWidgetName;
|
||||
} else {
|
||||
if (!this.selectedRouteCode) {
|
||||
return null;
|
||||
}
|
||||
appointmentTypeCmsWidgetName =
|
||||
this.getRelevantDropOffCmsWidgetNameForSelectedTimeSlot(
|
||||
this.selectedRouteCode,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
return this.getCmsContent(
|
||||
appointmentTypeCmsWidgetName,
|
||||
cmsWidgetFieldMappings.SUPPLEMENTAL_INFORMATION
|
||||
);
|
||||
},
|
||||
footerCloseButtonText() {
|
||||
return this.getCmsContent(
|
||||
this.cmsWidgetName,
|
||||
cmsWidgetFieldMappings.MODAL_CLOSE_BUTTON
|
||||
);
|
||||
},
|
||||
premiumAppointmentButtonText() {
|
||||
return this.getCmsContent(
|
||||
this.mobilePremiumCmsWidgetName,
|
||||
cmsWidgetFieldMappings.TIME_SLOT_BUTTON
|
||||
);
|
||||
},
|
||||
dropoffButtonText() {
|
||||
return this.getCmsContent(
|
||||
this.dropoffCmsWidgetName,
|
||||
cmsWidgetFieldMappings.TIME_SLOT_BUTTON
|
||||
);
|
||||
},
|
||||
sameDayDropoffButtonText() {
|
||||
return this.getCmsContent(
|
||||
this.sameDayDropOffCmsWidgetName,
|
||||
cmsWidgetFieldMappings.TIME_SLOT_BUTTON
|
||||
);
|
||||
},
|
||||
overnightDropoffButtonText() {
|
||||
return this.getCmsContent(
|
||||
this.overnightDropOffCmsWidgetName,
|
||||
cmsWidgetFieldMappings.TIME_SLOT_BUTTON
|
||||
);
|
||||
},
|
||||
dropoffDisclaimerText() {
|
||||
return this.getCmsContent(this.dropoffCmsWidgetName, cmsWidgetFieldMappings.DISCLAIMER);
|
||||
},
|
||||
sameDayDropOffDisclaimerText() {
|
||||
return this.getCmsContent(
|
||||
this.sameDayDropOffCmsWidgetName,
|
||||
cmsWidgetFieldMappings.DISCLAIMER
|
||||
);
|
||||
},
|
||||
overnightDropOffDisclaimerText() {
|
||||
return this.getCmsContent(
|
||||
this.overnightDropOffCmsWidgetName,
|
||||
cmsWidgetFieldMappings.DISCLAIMER
|
||||
);
|
||||
},
|
||||
disclaimerTextBlockCopy() {
|
||||
if (this.appointmentType === AppointmentTypeStrings.DROP_OFF) {
|
||||
if (this.selectedRouteCode?.includes(RouteCodeFlags.ALL_DAY_DROP_OFF)) {
|
||||
if (this.isSameDay) {
|
||||
return this.sameDayDropOffDisclaimerText;
|
||||
}
|
||||
return this.dropoffDisclaimerText;
|
||||
} if (this.selectedRouteCode?.includes(RouteCodeFlags.OVERNIGHT_DROP_OFF)) {
|
||||
return this.overnightDropOffDisclaimerText;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
dropOffDurationText() {
|
||||
return this.getCmsContent(this.dropoffCmsWidgetName, cmsWidgetFieldMappings.DURATION);
|
||||
},
|
||||
sameDayDropoffDurationText() {
|
||||
return this.getCmsContent(
|
||||
this.sameDayDropOffCmsWidgetName,
|
||||
cmsWidgetFieldMappings.DURATION
|
||||
);
|
||||
},
|
||||
overnightDropoffDurationText() {
|
||||
return this.getCmsContent(
|
||||
this.overnightDropOffCmsWidgetName,
|
||||
cmsWidgetFieldMappings.DURATION
|
||||
);
|
||||
},
|
||||
inshopDurationText() {
|
||||
const inshopDurationTextWithoutTime = this.getCmsContent(
|
||||
this.cmsWidgetName,
|
||||
cmsWidgetFieldMappings.DURATION
|
||||
);
|
||||
|
||||
const inshopDurationTime = getDisplayTextForDurationLength(
|
||||
this.estimatedServiceMinutesMinimum,
|
||||
this.estimatedServiceMinutesMaximum
|
||||
);
|
||||
|
||||
return `${inshopDurationTextWithoutTime} ${inshopDurationTime}`;
|
||||
},
|
||||
durationTextBlockCopy() {
|
||||
if (this.appointmentType === AppointmentTypeStrings.MOBILE) {
|
||||
return null;
|
||||
} if (this.appointmentType === AppointmentTypeStrings.IN_SHOP) {
|
||||
return this.inshopDurationText;
|
||||
}
|
||||
if (this.selectedRouteCode?.includes(RouteCodeFlags.OVERNIGHT_DROP_OFF)) {
|
||||
return this.overnightDropoffDurationText;
|
||||
} if (this.selectedRouteCode?.includes(RouteCodeFlags.ALL_DAY_DROP_OFF)) {
|
||||
if (this.isSameDay) {
|
||||
return this.sameDayDropoffDurationText;
|
||||
}
|
||||
return this.dropOffDurationText;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
isSameDay() {
|
||||
if (!this.timeSlotsForSelectedDate) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const selectedDate = this.timeSlotsForSelectedDate.date;
|
||||
const todaysDate = new Date().toISOString().split('T')[0];
|
||||
return selectedDate === todaysDate;
|
||||
},
|
||||
dateSelectedReadableDate() {
|
||||
if (!this.timeSlotsForSelectedDate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// This conversion ensures we don't get get GMT induced date changes
|
||||
const dateObject = convertDateStringToDate(this.timeSlotsForSelectedDate.date);
|
||||
// Ex: Tuesday, April 22
|
||||
return dateObject.toLocaleDateString('en-us', {
|
||||
weekday: 'long',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
});
|
||||
},
|
||||
availableTimeSlots() {
|
||||
if (!this.timeSlotsForSelectedDate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this.appointmentType === AppointmentTypeStrings.DROP_OFF) {
|
||||
return this.getAvailableTimeSlotsForDropOff(this.timeSlotsForSelectedDate.timeSlots);
|
||||
} if (this.appointmentType === AppointmentTypeStrings.MOBILE) {
|
||||
return this.getAvailableTimeSlotsForMobile(this.timeSlotsForSelectedDate.timeSlots);
|
||||
}
|
||||
return this.getAvailableTimeSlotsForInshop(this.timeSlotsForSelectedDate.timeSlots);
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
modelValue: {
|
||||
handler(newValue) {
|
||||
this.handleChange(newValue);
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
selectedDate: {
|
||||
handler() {
|
||||
this.selectedRouteCode = null;
|
||||
this.autoSelectTimeSlotIfOnlyOneIsAvailable();
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openModal() {
|
||||
this.modal.openModal();
|
||||
},
|
||||
setModalStatus(isOpened) {
|
||||
this.isModalOpened = isOpened;
|
||||
},
|
||||
closeModal() {
|
||||
this.modal.closeModal();
|
||||
},
|
||||
onModalClosed() {
|
||||
this.$emit('time-slot-modal-closed');
|
||||
},
|
||||
async setSelectedTimeSlot() {
|
||||
this.$emit(
|
||||
'update:modelValue',
|
||||
this.getSelectedTimeSlotInfoObject(this.selectedRouteCode)
|
||||
);
|
||||
this.closeModal();
|
||||
},
|
||||
getRelevantDropOffCmsWidgetNameForSelectedTimeSlot(
|
||||
selectedRouteCode,
|
||||
isSameDayRelevant = false
|
||||
) {
|
||||
if (selectedRouteCode.includes(RouteCodeFlags.OVERNIGHT_DROP_OFF)) {
|
||||
return this.overnightDropOffCmsWidgetName;
|
||||
}
|
||||
|
||||
if (selectedRouteCode.includes(RouteCodeFlags.ALL_DAY_DROP_OFF)) {
|
||||
return this.isSameDay && isSameDayRelevant
|
||||
? this.sameDayDropOffCmsWidgetName
|
||||
: this.dropoffCmsWidgetName;
|
||||
}
|
||||
|
||||
return '';
|
||||
},
|
||||
getAvailableTimeSlotsForInshop(timeSlotsForSelectedDate) {
|
||||
return timeSlotsForSelectedDate.map((timeSlot) => {
|
||||
const readableTime = militaryToTwelveHourTime(timeSlot.startTime);
|
||||
return {
|
||||
value: timeSlot.id,
|
||||
buttonLabel: readableTime
|
||||
};
|
||||
});
|
||||
},
|
||||
getAvailableTimeSlotsForDropOff(timeSlotsForSelectedDate) {
|
||||
const availableTimeSlots = timeSlotsForSelectedDate.map((timeSlot) => {
|
||||
let buttonLabelValue;
|
||||
if (timeSlot.id.includes(RouteCodeFlags.OVERNIGHT_DROP_OFF)) {
|
||||
buttonLabelValue = this.overnightDropoffButtonText;
|
||||
} else if (this.isSameDay) {
|
||||
buttonLabelValue = this.sameDayDropoffButtonText;
|
||||
} else {
|
||||
buttonLabelValue = this.dropoffButtonText;
|
||||
}
|
||||
|
||||
return {
|
||||
value: timeSlot.id,
|
||||
buttonLabel: buttonLabelValue
|
||||
};
|
||||
});
|
||||
|
||||
return availableTimeSlots;
|
||||
},
|
||||
getAvailableTimeSlotsForMobile(timeSlotsForSelectedDate) {
|
||||
const availableTimeSlots = timeSlotsForSelectedDate.map((timeSlot) => {
|
||||
const readableTime = `${militaryToTwelveHourTime(timeSlot.startTime)} - ${militaryToTwelveHourTime(timeSlot.endTime)}`;
|
||||
return {
|
||||
value: timeSlot.id,
|
||||
buttonLabel: readableTime
|
||||
};
|
||||
});
|
||||
|
||||
const isPremiumTimeSlot = timeSlotsForSelectedDate[0].offerPremium;
|
||||
const hasPremiumPartAvailable =
|
||||
this.premiumAppointmentFee?.partType === PREMIUM_FEE_PART_TYPE;
|
||||
if (isPremiumTimeSlot && hasPremiumPartAvailable) {
|
||||
availableTimeSlots.unshift(this.getPremiumAppointmentTimeSlot(timeSlotsForSelectedDate[0]));
|
||||
}
|
||||
|
||||
return availableTimeSlots;
|
||||
},
|
||||
getPremiumAppointmentTimeSlot(timeSlotData) {
|
||||
const formattedPrice =
|
||||
`+$${this.getTotalLineItemPrice(this.premiumAppointmentFee).toFixed(2)}`;
|
||||
|
||||
return {
|
||||
// Unique value is required for each <input> and the premium appoinment shares an id
|
||||
value: this.addPremiumFlagToInput(timeSlotData.id),
|
||||
buttonLabel: this.premiumAppointmentButtonText,
|
||||
buttonLabelSubCopy: formattedPrice,
|
||||
additionalButtonData: {
|
||||
isPremiumAppointment: true
|
||||
}
|
||||
};
|
||||
},
|
||||
getSelectedRouteCode() {
|
||||
let selectedRouteCode;
|
||||
if (!this.modelValue?.timeSlot) {
|
||||
selectedRouteCode = null;
|
||||
}
|
||||
|
||||
if (this.modelValue?.isPremiumAppointment) {
|
||||
selectedRouteCode = this.addPremiumFlagToInput(this.modelValue?.timeSlot?.routeCode);
|
||||
} else {
|
||||
selectedRouteCode = this.modelValue?.timeSlot?.routeCode;
|
||||
}
|
||||
|
||||
return selectedRouteCode;
|
||||
},
|
||||
autoSelectTimeSlotIfOnlyOneIsAvailable() {
|
||||
const numberOfOptions = this.availableTimeSlots?.length;
|
||||
if (numberOfOptions === 1) {
|
||||
this.selectedRouteCode = this.availableTimeSlots[0].value;
|
||||
}
|
||||
},
|
||||
addPremiumFlagToInput(routeCode) {
|
||||
return (`${routeCode}${PREMIUM_TIME_SLOT_ID_FLAG}`);
|
||||
},
|
||||
removePremiumFlagFromInput(routeCode) {
|
||||
return routeCode.replace(PREMIUM_TIME_SLOT_ID_FLAG, '');
|
||||
},
|
||||
getSelectedTimeSlotInfoObject(routeCode) {
|
||||
let routeCodeToUse = routeCode;
|
||||
const routeCodeIncludesPremium = routeCode?.includes(PREMIUM_TIME_SLOT_ID_FLAG);
|
||||
if (routeCodeIncludesPremium) {
|
||||
routeCodeToUse = this.removePremiumFlagFromInput(routeCode);
|
||||
}
|
||||
|
||||
const timeSlot = this.timeSlotsForSelectedDate?.timeSlots?.find((ts) => ts.id === routeCodeToUse);
|
||||
|
||||
if (timeSlot) {
|
||||
return {
|
||||
timeSlot: {
|
||||
date: this.timeSlotsForSelectedDate.date,
|
||||
routeCode: timeSlot.id,
|
||||
startTime: timeSlot.startTime,
|
||||
endTime: timeSlot.endTime,
|
||||
jobMaxMinutes: this.estimatedServiceMinutesMaximum.toString(),
|
||||
jobMinMinutes: this.estimatedServiceMinutesMinimum.toString()
|
||||
},
|
||||
isPremiumAppointment: !!routeCodeIncludesPremium
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
timeSlot: {
|
||||
date: null,
|
||||
startTime: null,
|
||||
endTime: null,
|
||||
routeCode: null,
|
||||
jobMaxMinutes: null,
|
||||
jobMinMinutes: null
|
||||
},
|
||||
isPremiumAppointment: null
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.time-slots-modal.modal.modal-component {
|
||||
.modal-header {
|
||||
padding-bottom: 0;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
.text-block.duration-text-block {
|
||||
margin-top: 4px !important;
|
||||
}
|
||||
.supplemental-information {
|
||||
line-height: 1.5rem;
|
||||
font-size: 0.875rem;
|
||||
li strong {
|
||||
font-weight: $font-weight-bold;
|
||||
}
|
||||
li:not(:last-child) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -12,9 +12,23 @@ import issPageValues from '@/router/router-constants/issPage-values';
|
|||
import damageLocationsSelected from '@/constants/damage-locations-selected';
|
||||
import coverageStatuses from '@/constants/coverage-statuses';
|
||||
import { PREMIUM_FEE_PART_TYPE } from '@/constants/schedule-constants';
|
||||
import getDateDifferenceInDays from '@/helpers/date-helper';
|
||||
|
||||
const storeId = 'main';
|
||||
|
||||
function getTimeSlotsAdditionalEventData(
|
||||
provisionalTriggers,
|
||||
zipCode,
|
||||
firstAvailableAppointmentDateString,
|
||||
shopAppointmentType
|
||||
) {
|
||||
let numberOfDays = null;
|
||||
if (firstAvailableAppointmentDateString) numberOfDays = getDateDifferenceInDays(new Date(), firstAvailableAppointmentDateString);
|
||||
|
||||
if (shopAppointmentType) return `FirstAvailableAppointment:${numberOfDays},Zip:${zipCode},ShopAppointmentType:${shopAppointmentType},ProvisionalTriggers:${provisionalTriggers.join(',')}`;
|
||||
return `FirstAvailableAppointment:${numberOfDays},Zip:${zipCode},ProvisionalTriggers:${provisionalTriggers.join(',')}`;
|
||||
}
|
||||
|
||||
const getDefaultState = () => ({
|
||||
order: {
|
||||
vehicle: {
|
||||
|
|
@ -600,6 +614,65 @@ export const useMainStore = defineStore({
|
|||
});
|
||||
},
|
||||
|
||||
getMobileTimeSlots(startDate, endDate) {
|
||||
const { order } = this;
|
||||
const { vehicle } = this.order;
|
||||
let lineItems = [
|
||||
...(order.lineItems.supportingItems ?? []),
|
||||
...(order.lineItems.vaps ?? []),
|
||||
...getFlattenedArrayOfLineItemsWithChildParts(order.lineItems.glassParts)
|
||||
];
|
||||
lineItems = lineItems.map((lineItem) => ({
|
||||
partNumber: lineItem.partNumber,
|
||||
partType: lineItem.partType
|
||||
}));
|
||||
const glassPieces = order.damage.glassToReplace
|
||||
? convertGlassPieceToBackEndCompatibleFormat(order.damage.glassToReplace)
|
||||
: [];
|
||||
const payload = {
|
||||
startDate,
|
||||
endDate,
|
||||
applicationName: applicationConfig.APPLICATION_NAME,
|
||||
parentAccountNumber: this.payment.parentAccountNumber,
|
||||
carId: vehicle.carId,
|
||||
lineItems,
|
||||
glassPieces,
|
||||
eon: order.eon,
|
||||
coverage: {
|
||||
status: '',
|
||||
deductible: 0,
|
||||
additionalAuthFlag: ''
|
||||
},
|
||||
partSelection: {
|
||||
hasAnsweredPartQuestions: !!order.damage.partQuestionAnswers?.length,
|
||||
hasAnsweredMoldingQuestions: !!order.damage.moldingQuestionAnswers?.length,
|
||||
hasAnsweredCapabilityQuestions: !!order.damage.capabilityQuestionAnswers?.length,
|
||||
hasManuallySelectedParts:
|
||||
!!this.applicationUser.pageData['vehicle-parts']?.partsOrQuestions
|
||||
.length
|
||||
},
|
||||
vehicle: {
|
||||
year: vehicle.year,
|
||||
make: vehicle.make,
|
||||
model: vehicle.model,
|
||||
style: vehicle.style,
|
||||
vin: vehicle.vin ?? ''
|
||||
},
|
||||
zipCode: order.serviceLocation.zipCode
|
||||
};
|
||||
return globalMethods.callHttpClient({
|
||||
method: endpoints.GetMobileTimeSlots.method,
|
||||
endpoint: endpoints.GetMobileTimeSlots.url,
|
||||
payload,
|
||||
logApiCall: true,
|
||||
additionalSuccessEventDataHandler: (response) =>
|
||||
getTimeSlotsAdditionalEventData(
|
||||
response.data.provisionalTriggers,
|
||||
order.serviceLocation.zipCode,
|
||||
response.data.days?.[0]?.date
|
||||
)
|
||||
});
|
||||
},
|
||||
getShopTimeSlots(startDate, endDate, shopAppointmentType, providerNumber) {
|
||||
const { order } = this;
|
||||
const { vehicle } = this.order;
|
||||
|
|
|
|||
Loading…
Reference in a new issue