initial check in for schedule page
This commit is contained in:
parent
41052ab241
commit
6b9ed59cfc
8 changed files with 572 additions and 761 deletions
BIN
src/assets/img/icons/icon-ablue-next.png
Normal file
BIN
src/assets/img/icons/icon-ablue-next.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 494 B |
BIN
src/assets/img/icons/icon-ablue-previous.png
Normal file
BIN
src/assets/img/icons/icon-ablue-previous.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 510 B |
|
|
@ -58,7 +58,8 @@ const errorMessages = Object.freeze({
|
||||||
MODEL_REQUIRED: 'Vehicle model is required.',
|
MODEL_REQUIRED: 'Vehicle model is required.',
|
||||||
STYLE_REQUIRED: 'Vehicle style is required.',
|
STYLE_REQUIRED: 'Vehicle style is required.',
|
||||||
MOBILE_LOCATION_REQUIRED: 'Please enter your service address',
|
MOBILE_LOCATION_REQUIRED: 'Please enter your service address',
|
||||||
DATE_REQUIRED: 'Please select a date'
|
DATE_REQUIRED: 'Please select a date',
|
||||||
|
TIME_REQUIRED: 'Please select an appointment time.'
|
||||||
});
|
});
|
||||||
|
|
||||||
export default errorMessages;
|
export default errorMessages;
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -17,6 +17,21 @@ export function convertDateToDateString(date) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function convertDateToTwoDigitDay(date) {
|
||||||
|
if (date instanceof Date !== true) return null;
|
||||||
|
return (`0${date.getDate()}`).slice(-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convertDateToTwoDigitMonth(date) {
|
||||||
|
if (date instanceof Date !== true) return null;
|
||||||
|
return (`0${date.getMonth() + 1}`).slice(-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convertDateToShortMonth(date) {
|
||||||
|
if (date instanceof Date !== true) return null;
|
||||||
|
return date.toLocaleString('en-US', { month: 'short' });
|
||||||
|
}
|
||||||
|
|
||||||
export function convertDateStringToDate(dateString) {
|
export function convertDateStringToDate(dateString) {
|
||||||
// dateString must be YYYY-MM-DD format
|
// dateString must be YYYY-MM-DD format
|
||||||
if (typeof dateString !== 'string') return null;
|
if (typeof dateString !== 'string') return null;
|
||||||
|
|
@ -163,9 +178,11 @@ export function combineDateAndTime(date, time) {
|
||||||
// Return the new date object
|
// Return the new date object
|
||||||
return newDate;
|
return newDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addMinutes(date, minutes) {
|
export function addMinutes(date, minutes) {
|
||||||
return new Date(date.getTime() + minutes * 60000);
|
return new Date(date.getTime() + minutes * 60000);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function shortTimeString(date) {
|
export function shortTimeString(date) {
|
||||||
// Use a ternary operator to check if the input is a valid date object
|
// Use a ternary operator to check if the input is a valid date object
|
||||||
return date instanceof Date
|
return date instanceof Date
|
||||||
|
|
|
||||||
|
|
@ -290,27 +290,6 @@ describe('schedule-page.vue', () => {
|
||||||
// Assert
|
// Assert
|
||||||
expect(testValue).toStrictEqual('01234');
|
expect(testValue).toStrictEqual('01234');
|
||||||
});
|
});
|
||||||
test('getDisplayTextForMilitaryTime should return the correctly formatted string', () => {
|
|
||||||
// Arrange
|
|
||||||
const { wrapper } = getShallowMountedComponent();
|
|
||||||
wrapper.vm.selectableDatesData = {
|
|
||||||
days: []
|
|
||||||
};
|
|
||||||
const timeInput1 = '15:00';
|
|
||||||
const timeInput2 = '15:30';
|
|
||||||
|
|
||||||
// Act
|
|
||||||
const testOutput1 = wrapper.vm.getDisplayTextForMilitaryTime(timeInput1);
|
|
||||||
const testOutput2 = wrapper.vm.getDisplayTextForMilitaryTime(timeInput2);
|
|
||||||
const testOutput3 = wrapper.vm.getDisplayTextForMilitaryTime(timeInput1, true);
|
|
||||||
const testOutput4 = wrapper.vm.getDisplayTextForMilitaryTime(timeInput2, true);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(testOutput1).toBe('3:00 PM');
|
|
||||||
expect(testOutput2).toBe('3:30 PM');
|
|
||||||
expect(testOutput3).toBe('3 PM');
|
|
||||||
expect(testOutput4).toBe('3:30 PM');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
test('forwardButtonAction should call route method navigateWithoutSaving', async () => {
|
test('forwardButtonAction should call route method navigateWithoutSaving', async () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<Form
|
<Form
|
||||||
ref="theForm"
|
ref="theForm"
|
||||||
v-slot="{ meta }"
|
|
||||||
@submit="onSubmit"
|
@submit="onSubmit"
|
||||||
@invalidSubmit="onInvalidSubmit">
|
@invalidSubmit="onInvalidSubmit">
|
||||||
<div class="fade-on-route-transition">
|
<div class="fade-on-route-transition">
|
||||||
|
|
@ -16,56 +15,27 @@
|
||||||
cmsWidgetName="ScheduleSubHeaderWidget"
|
cmsWidgetName="ScheduleSubHeaderWidget"
|
||||||
secondaryTextClasses="text-center small sub-text"
|
secondaryTextClasses="text-center small sub-text"
|
||||||
class="mt-4" />
|
class="mt-4" />
|
||||||
<template v-if="ChangeShopLink.length">
|
|
||||||
<textBlock
|
|
||||||
cmsWidgetName="ChangeShopLink"
|
|
||||||
justifyText="center"
|
|
||||||
class="mb-5 text-link-small change-shop-link"
|
|
||||||
:marginTopSizeOverride="1" />
|
|
||||||
</template>
|
|
||||||
<div class="main-content-container">
|
<div class="main-content-container">
|
||||||
<locationAlerts
|
<locationAlerts
|
||||||
ref="locationAlerts"
|
ref="locationAlerts"
|
||||||
cmsWidgetPrefix="LocationAlert-" />
|
cmsWidgetPrefix="LocationAlert-" />
|
||||||
<datePicker
|
<datePicker
|
||||||
ref="datePicker"
|
ref="datePicker"
|
||||||
v-model="selectedDate"
|
v-model="selectedTimeSlotInfo"
|
||||||
customComponentId="dateQuestion"
|
customComponentId="dateQuestion"
|
||||||
selectableDatesSetting="custom"
|
selectableDatesSetting="custom"
|
||||||
class="text-link-small"
|
class="text-link-small"
|
||||||
|
:showTimeSlotError="showDatePickerError"
|
||||||
:customSelectableDatesCallback="
|
:customSelectableDatesCallback="
|
||||||
getAvailableDatesMethod
|
getAvailableDatesMethod
|
||||||
"
|
"
|
||||||
validationRules="date-required"
|
@dateSelected="dateSelectedFromPicker"
|
||||||
@dateClicked="openInshopTimeSlotsModal" />
|
@timeSlotSelected="timeSlotSelectedFromPicker" />
|
||||||
<timeSlotModalQuestion
|
|
||||||
ref="timeSlotModalQuestion"
|
|
||||||
v-model="selectedTimeSlotInfo"
|
|
||||||
customComponentId="timeSlotModalQuestion"
|
|
||||||
cmsWidgetName="TimeSlotModalQuestion"
|
|
||||||
mobilePremiumCmsWidgetName="MobilePremiumTimeSlotModal"
|
|
||||||
mobileCmsWidgetName="MobileTimeSlotModal"
|
|
||||||
dropoffCmsWidgetName="DropOffTimeSlotModal"
|
|
||||||
sameDayDropOffCmsWidgetName="SameDayDropOffTimeSlotModal"
|
|
||||||
overnightDropOffCmsWidgetName="OvernightDropOffTimeSlotModal"
|
|
||||||
:selectedDate="selectedDate"
|
|
||||||
:appointmentType="appointmentType"
|
|
||||||
:premiumAppointmentFee="mobilePremiumAppointmentFee"
|
|
||||||
:timeSlotsForSelectedDate="timeSlotsForSelectedDate"
|
|
||||||
:estimatedServiceMinutesMinimum="
|
|
||||||
selectableDatesData.estimatedServiceMinutesMinimum
|
|
||||||
"
|
|
||||||
:estimatedServiceMinutesMaximum="
|
|
||||||
selectableDatesData.estimatedServiceMinutesMaximum
|
|
||||||
"
|
|
||||||
validationRules="time-slot-selection-required"
|
|
||||||
@timeSlotModalClosed="timeSlotModalClosed"
|
|
||||||
@timeSlotSelected="forwardButtonAction" />
|
|
||||||
<siteFooter
|
<siteFooter
|
||||||
ref="navbar"
|
ref="navbar"
|
||||||
class="mt-5"
|
class="mt-5"
|
||||||
cmsWidgetName="SiteFooterWidget"
|
cmsWidgetName="SiteFooterWidget"
|
||||||
:isForwardActionDisabled="!meta.valid"
|
:isForwardButtonNavigationDisabled="!isFormValid"
|
||||||
@backClicked="navigateBack"
|
@backClicked="navigateBack"
|
||||||
@forwardClicked="forwardButtonAction" />
|
@forwardClicked="forwardButtonAction" />
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -80,9 +50,7 @@ import siteHeader from '@/iss-components/site-header/site-header.vue';
|
||||||
import siteSubHeader from '@/iss-components/site-sub-header/site-sub-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 locationAlerts from '@/layouts/schedule-page/location-alerts/location-alerts.vue';
|
||||||
import datePicker from '@/digital-components/date-picker/date-picker.vue';
|
import datePicker from '@/digital-components/date-picker/date-picker.vue';
|
||||||
import timeSlotModalQuestion from '@/layouts/schedule-page/time-slot-modal-question/time-slot-modal-question.vue';
|
|
||||||
import siteFooter from '@/iss-components/site-footer/site-footer.vue';
|
import siteFooter from '@/iss-components/site-footer/site-footer.vue';
|
||||||
import textBlock from '@/digital-components/text-block/text-block.vue';
|
|
||||||
|
|
||||||
// Supporting files
|
// Supporting files
|
||||||
import {
|
import {
|
||||||
|
|
@ -97,25 +65,13 @@ import {
|
||||||
} from '@/helpers/cms-content-helper';
|
} from '@/helpers/cms-content-helper';
|
||||||
import {
|
import {
|
||||||
calcDaysBetweenDates,
|
calcDaysBetweenDates,
|
||||||
convertDateStringToDate,
|
|
||||||
sumDateString
|
sumDateString
|
||||||
} from '@/helpers/date-helper';
|
} from '@/helpers/date-helper';
|
||||||
import settleAllPromises from '@/helpers/layout-helper';
|
import settleAllPromises from '@/helpers/layout-helper';
|
||||||
import { Form, defineRule } from 'vee-validate';
|
import { Form } from 'vee-validate';
|
||||||
import BaseFormMixin from '@/mixins/base-form-mixin.js';
|
import BaseFormMixin from '@/mixins/base-form-mixin.js';
|
||||||
import errorMessages from '@/constants/error-messages';
|
|
||||||
import { required } from '@/helpers/validation-rules';
|
|
||||||
import { useMainStore } from '@/store';
|
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
|
// Define constants
|
||||||
const TIME_SLOTS_CALL_DAYS_LIMIT = 34; // needs to be 34 for API limits (35 does not consistently work)
|
const TIME_SLOTS_CALL_DAYS_LIMIT = 34; // needs to be 34 for API limits (35 does not consistently work)
|
||||||
|
|
||||||
|
|
@ -132,8 +88,6 @@ const getAvailableDates = async (
|
||||||
const difference = calcDaysBetweenDates(startDateString, endDateString);
|
const difference = calcDaysBetweenDates(startDateString, endDateString);
|
||||||
const apiCallsCount = Math.ceil(difference / TIME_SLOTS_CALL_DAYS_LIMIT);
|
const apiCallsCount = Math.ceil(difference / TIME_SLOTS_CALL_DAYS_LIMIT);
|
||||||
const storeActionConfigs = [];
|
const storeActionConfigs = [];
|
||||||
const timeSlotsData = {};
|
|
||||||
timeSlotsData.days = [];
|
|
||||||
let apiStartDate = startDateString;
|
let apiStartDate = startDateString;
|
||||||
let apiEndDate = endDateString;
|
let apiEndDate = endDateString;
|
||||||
|
|
||||||
|
|
@ -232,9 +186,7 @@ export default {
|
||||||
siteSubHeader,
|
siteSubHeader,
|
||||||
locationAlerts,
|
locationAlerts,
|
||||||
datePicker,
|
datePicker,
|
||||||
timeSlotModalQuestion,
|
|
||||||
siteFooter,
|
siteFooter,
|
||||||
textBlock,
|
|
||||||
// eslint-disable-next-line vue/no-reserved-component-names
|
// eslint-disable-next-line vue/no-reserved-component-names
|
||||||
Form
|
Form
|
||||||
},
|
},
|
||||||
|
|
@ -300,7 +252,6 @@ export default {
|
||||||
resultMap.datePickerInitialData.initialShopTimeSlotsResponse,
|
resultMap.datePickerInitialData.initialShopTimeSlotsResponse,
|
||||||
resultMap.premiumFeeWithPrice
|
resultMap.premiumFeeWithPrice
|
||||||
);
|
);
|
||||||
vm.updateFooterButtonText(vm.selectedTimeSlotInfo);
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
|
|
@ -312,6 +263,7 @@ export default {
|
||||||
selectedDate: this.getSelectedDate(),
|
selectedDate: this.getSelectedDate(),
|
||||||
selectedTimeSlotInfo: this.getSelectedTimeSlotInfo(),
|
selectedTimeSlotInfo: this.getSelectedTimeSlotInfo(),
|
||||||
selectableDatesData: [],
|
selectableDatesData: [],
|
||||||
|
showDatePickerError: false,
|
||||||
mobilePremiumAppointmentFee: null
|
mobilePremiumAppointmentFee: null
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
@ -325,38 +277,16 @@ export default {
|
||||||
appointmentType() {
|
appointmentType() {
|
||||||
return useMainStore().order.serviceLocation.appointmentType;
|
return useMainStore().order.serviceLocation.appointmentType;
|
||||||
},
|
},
|
||||||
timeSlotsForSelectedDate() {
|
isFormValid() {
|
||||||
if (!this.selectedDate) {
|
console.log('isFormValid called');
|
||||||
return null;
|
console.log(this.selectedTimeSlotInfo);
|
||||||
}
|
const hasTimeSlotSelected = this.selectedTimeSlotInfo?.id != null;
|
||||||
|
return hasTimeSlotSelected;
|
||||||
return this.selectableDatesData.days?.find((selectableDate) => selectableDate.date === this.selectedDate);
|
|
||||||
},
|
},
|
||||||
supportingItems() {
|
supportingItems() {
|
||||||
return useMainStore().lineItems.supportingItems;
|
return useMainStore().lineItems.supportingItems;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
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: {
|
methods: {
|
||||||
splitCopyOnCMSPlaceHolder,
|
splitCopyOnCMSPlaceHolder,
|
||||||
arePagePrerequisitesValid() {
|
arePagePrerequisitesValid() {
|
||||||
|
|
@ -400,8 +330,8 @@ export default {
|
||||||
getServiceZipCtuCodeFromStore() {
|
getServiceZipCtuCodeFromStore() {
|
||||||
return this.mainStore.order.serviceLocation.zipCodeCtu;
|
return this.mainStore.order.serviceLocation.zipCodeCtu;
|
||||||
},
|
},
|
||||||
openInshopTimeSlotsModal() {
|
dateTimeSelected() {
|
||||||
this.$refs.timeSlotModalQuestion.openModal();
|
console.log('dateTimeSelected called');
|
||||||
},
|
},
|
||||||
getSelectedDate() {
|
getSelectedDate() {
|
||||||
return this.mainStore.order.schedule.date;
|
return this.mainStore.order.schedule.date;
|
||||||
|
|
@ -420,62 +350,24 @@ export default {
|
||||||
|
|
||||||
return selectedTimeSlotInfo;
|
return selectedTimeSlotInfo;
|
||||||
},
|
},
|
||||||
timeSlotModalClosed() {
|
dateSelectedFromPicker(date) {
|
||||||
// Clear the selectedDate if no timeSlot has been selected
|
console.log('schedule-page: dateSelectedFromPicker called');
|
||||||
if (this.selectedTimeSlotInfo.timeSlot.routeCode == null) {
|
console.log(date);
|
||||||
this.selectedDate = null;
|
this.selectedDate = date;
|
||||||
}
|
this.showDatePickerError = false;
|
||||||
},
|
},
|
||||||
updateFooterButtonText(timeSlotInfo) {
|
timeSlotSelectedFromPicker(timeSlot) {
|
||||||
let navbarButtonText;
|
console.log('schedule-page: timeSlotSelectedFromPicker called');
|
||||||
if (!timeSlotInfo || !timeSlotInfo.timeSlot.date) {
|
console.log(timeSlot);
|
||||||
navbarButtonText = 'Continue';
|
this.selectedTimeSlotInfo = timeSlot;
|
||||||
} else {
|
this.showDatePickerError = false;
|
||||||
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}`;
|
|
||||||
},
|
},
|
||||||
forwardButtonAction() {
|
forwardButtonAction() {
|
||||||
|
console.log('forwardButtonAction called');
|
||||||
|
if (!this.isFormValid) {
|
||||||
|
this.showDatePickerError = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.mainStore.saveSchedule(this.selectedTimeSlotInfo.timeSlot);
|
this.mainStore.saveSchedule(this.selectedTimeSlotInfo.timeSlot);
|
||||||
|
|
||||||
this.$router.navigate(
|
this.$router.navigate(
|
||||||
|
|
|
||||||
|
|
@ -136,6 +136,7 @@ const router = createRouter({
|
||||||
router.beforeEach(async (to, from) => {
|
router.beforeEach(async (to, from) => {
|
||||||
const fromQueryPage = from.query?.issPage;
|
const fromQueryPage = from.query?.issPage;
|
||||||
if (fromQueryPage === undefined) {
|
if (fromQueryPage === undefined) {
|
||||||
|
console.log('router.beforeEach: showing loading modal');
|
||||||
showIssLoadingModal(true);
|
showIssLoadingModal(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -288,6 +289,7 @@ function navigate(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (forceSpinner) {
|
if (forceSpinner) {
|
||||||
|
console.log('navigate: showing loading modal');
|
||||||
showIssLoadingModal(true);
|
showIssLoadingModal(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue