Merge pull request #1005 from Safelite/feature/richardson/INSR-7767
Bunch o crap
This commit is contained in:
commit
100d0ff854
9 changed files with 791 additions and 1072 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 |
|
|
@ -59,7 +59,8 @@ const errorMessages = Object.freeze({
|
|||
MODEL_REQUIRED: 'Vehicle model is required.',
|
||||
STYLE_REQUIRED: 'Vehicle style is required.',
|
||||
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;
|
||||
|
|
|
|||
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) {
|
||||
// dateString must be YYYY-MM-DD format
|
||||
if (typeof dateString !== 'string') return null;
|
||||
|
|
@ -51,6 +66,12 @@ export function getDisplayTextForDurationLength(durationMinimum, durationMaximum
|
|||
return `${durationText} ${unitText}`;
|
||||
}
|
||||
|
||||
export function isAfternoon(timeString) {
|
||||
if (typeof timeString !== 'string') return false;
|
||||
const hours = parseInt(timeString.split(':')[0], 10);
|
||||
return hours >= 12;
|
||||
}
|
||||
|
||||
export function militaryToTwelveHourTime(timeString) {
|
||||
// Expected input: "HH:MM"
|
||||
if (typeof timeString !== 'string') return null;
|
||||
|
|
@ -163,9 +184,11 @@ export function combineDateAndTime(date, time) {
|
|||
// Return the new date object
|
||||
return newDate;
|
||||
}
|
||||
|
||||
export function addMinutes(date, minutes) {
|
||||
return new Date(date.getTime() + minutes * 60000);
|
||||
}
|
||||
|
||||
export function shortTimeString(date) {
|
||||
// Use a ternary operator to check if the input is a valid date object
|
||||
return date instanceof Date
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import { createTestingPinia } from '@pinia/testing';
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { getMountOptions } from '@/helpers/unit-test-helper.js';
|
||||
import { useMainStore } from '@/store/index.js';
|
||||
import { AppointmentTypeStrings } from '@/constants/schedule-constants';
|
||||
|
||||
// Mock fetchCmsContentForPage
|
||||
jest.mock('@/helpers/cms-content-helper', () => ({
|
||||
|
|
@ -214,7 +213,7 @@ describe('schedule-page.vue', () => {
|
|||
// Act
|
||||
const newShopTimeSlots = await wrapper.vm.getAvailableDatesMethod(
|
||||
'2023-01-01',
|
||||
'2023-01-31'
|
||||
'2023-01-15'
|
||||
);
|
||||
|
||||
// Assert
|
||||
|
|
@ -236,7 +235,7 @@ describe('schedule-page.vue', () => {
|
|||
estimatedServiceMinutesMaximum: 120
|
||||
});
|
||||
});
|
||||
test('Should call API service in days of 34 or less when getAvailableDatesMethod is called with large date ranges', async () => {
|
||||
test('Should call API service in days of 15 or less when getAvailableDatesMethod is called with large date ranges', async () => {
|
||||
// Arrange
|
||||
const { wrapper } = getShallowMountedComponent();
|
||||
wrapper.vm.selectableDatesData = {
|
||||
|
|
@ -264,7 +263,7 @@ describe('schedule-page.vue', () => {
|
|||
// 2023-01-01 --> 2023-02-05
|
||||
// 2023-02-06 --> 2023-03-12
|
||||
// 2023-03-13 --> 2023-03-31
|
||||
expect(store.getShopTimeSlots).toHaveBeenCalledTimes(3);
|
||||
expect(store.getShopTimeSlots).toHaveBeenCalledTimes(6);
|
||||
});
|
||||
});
|
||||
describe('Rendering', () => {
|
||||
|
|
@ -290,32 +289,16 @@ describe('schedule-page.vue', () => {
|
|||
// Assert
|
||||
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 () => {
|
||||
// Arrange
|
||||
const { wrapper } = getShallowMountedComponent();
|
||||
wrapper.vm.$router.navigate = jest.fn(() => ({}));
|
||||
wrapper.vm.selectedTimeSlotInfo = {
|
||||
timeSlot: {
|
||||
routeCode: 'test-id'
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
await wrapper.vm.forwardButtonAction();
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
<template>
|
||||
<Form
|
||||
ref="theForm"
|
||||
v-slot="{ meta }"
|
||||
@submit="onSubmit"
|
||||
@invalidSubmit="onInvalidSubmit">
|
||||
<div class="fade-on-route-transition">
|
||||
|
|
@ -16,56 +15,27 @@
|
|||
cmsWidgetName="ScheduleSubHeaderWidget"
|
||||
secondaryTextClasses="text-center small sub-text"
|
||||
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">
|
||||
<locationAlerts
|
||||
ref="locationAlerts"
|
||||
cmsWidgetPrefix="LocationAlert-" />
|
||||
<datePicker
|
||||
ref="datePicker"
|
||||
v-model="selectedDate"
|
||||
v-model="selectedTimeSlotInfo"
|
||||
customComponentId="dateQuestion"
|
||||
selectableDatesSetting="custom"
|
||||
class="text-link-small"
|
||||
:showTimeSlotError="showDatePickerError"
|
||||
:customSelectableDatesCallback="
|
||||
getAvailableDatesMethod
|
||||
"
|
||||
validationRules="date-required"
|
||||
@dateClicked="openInshopTimeSlotsModal" />
|
||||
<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" />
|
||||
@dateSelected="dateSelectedFromPicker"
|
||||
@timeSlotSelected="timeSlotSelectedFromPicker" />
|
||||
<siteFooter
|
||||
ref="navbar"
|
||||
class="mt-5"
|
||||
cmsWidgetName="SiteFooterWidget"
|
||||
:isForwardActionDisabled="!meta.valid"
|
||||
:isForwardButtonNavigationDisabled="!isFormValid"
|
||||
@backClicked="navigateBack"
|
||||
@forwardClicked="forwardButtonAction" />
|
||||
</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 locationAlerts from '@/layouts/schedule-page/location-alerts/location-alerts.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 textBlock from '@/digital-components/text-block/text-block.vue';
|
||||
|
||||
// Supporting files
|
||||
import {
|
||||
|
|
@ -97,27 +65,15 @@ import {
|
|||
} from '@/helpers/cms-content-helper';
|
||||
import {
|
||||
calcDaysBetweenDates,
|
||||
convertDateStringToDate,
|
||||
sumDateString
|
||||
} from '@/helpers/date-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 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 TIME_SLOTS_CALL_DAYS_LIMIT = 15;
|
||||
|
||||
const getAvailableDates = async (
|
||||
startDateString,
|
||||
|
|
@ -127,13 +83,11 @@ const getAvailableDates = async (
|
|||
) => {
|
||||
const apiEndDateLimit = sumDateString(
|
||||
startDateString,
|
||||
TIME_SLOTS_CALL_DAYS_LIMIT
|
||||
TIME_SLOTS_CALL_DAYS_LIMIT - 1
|
||||
);
|
||||
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;
|
||||
|
||||
|
|
@ -144,7 +98,7 @@ const getAvailableDates = async (
|
|||
apiStartDate = sumDateString(apiEndDate, 1);
|
||||
apiEndDate = sumDateString(
|
||||
apiStartDate,
|
||||
TIME_SLOTS_CALL_DAYS_LIMIT
|
||||
TIME_SLOTS_CALL_DAYS_LIMIT - 1
|
||||
);
|
||||
|
||||
if (i === apiCallsCount) {
|
||||
|
|
@ -154,11 +108,7 @@ const getAvailableDates = async (
|
|||
apiEndDate = apiEndDateLimit;
|
||||
}
|
||||
|
||||
if (
|
||||
appointmentType === AppointmentTypeStrings.MOBILE
|
||||
|| appointmentType
|
||||
=== AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP
|
||||
) {
|
||||
if (appointmentType === AppointmentTypeStrings.MOBILE || appointmentType === AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP) {
|
||||
storeActionConfig = {
|
||||
storeAction: GET_MOBILE_TIME_SLOTS,
|
||||
payload: {
|
||||
|
|
@ -232,9 +182,7 @@ export default {
|
|||
siteSubHeader,
|
||||
locationAlerts,
|
||||
datePicker,
|
||||
timeSlotModalQuestion,
|
||||
siteFooter,
|
||||
textBlock,
|
||||
// eslint-disable-next-line vue/no-reserved-component-names
|
||||
Form
|
||||
},
|
||||
|
|
@ -300,7 +248,6 @@ export default {
|
|||
resultMap.datePickerInitialData.initialShopTimeSlotsResponse,
|
||||
resultMap.premiumFeeWithPrice
|
||||
);
|
||||
vm.updateFooterButtonText(vm.selectedTimeSlotInfo);
|
||||
});
|
||||
},
|
||||
setup() {
|
||||
|
|
@ -312,6 +259,7 @@ export default {
|
|||
selectedDate: this.getSelectedDate(),
|
||||
selectedTimeSlotInfo: this.getSelectedTimeSlotInfo(),
|
||||
selectableDatesData: [],
|
||||
showDatePickerError: false,
|
||||
mobilePremiumAppointmentFee: null
|
||||
};
|
||||
},
|
||||
|
|
@ -325,38 +273,14 @@ export default {
|
|||
appointmentType() {
|
||||
return useMainStore().order.serviceLocation.appointmentType;
|
||||
},
|
||||
timeSlotsForSelectedDate() {
|
||||
if (!this.selectedDate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.selectableDatesData.days?.find((selectableDate) => selectableDate.date === this.selectedDate);
|
||||
isFormValid() {
|
||||
const hasTimeSlotSelected = this.selectedTimeSlotInfo?.timeSlot?.routeCode != null;
|
||||
return hasTimeSlotSelected;
|
||||
},
|
||||
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: {
|
||||
splitCopyOnCMSPlaceHolder,
|
||||
arePagePrerequisitesValid() {
|
||||
|
|
@ -400,9 +324,6 @@ export default {
|
|||
getServiceZipCtuCodeFromStore() {
|
||||
return this.mainStore.order.serviceLocation.zipCodeCtu;
|
||||
},
|
||||
openInshopTimeSlotsModal() {
|
||||
this.$refs.timeSlotModalQuestion.openModal();
|
||||
},
|
||||
getSelectedDate() {
|
||||
return this.mainStore.order.schedule.date;
|
||||
},
|
||||
|
|
@ -420,64 +341,21 @@ export default {
|
|||
|
||||
return selectedTimeSlotInfo;
|
||||
},
|
||||
timeSlotModalClosed() {
|
||||
// Clear the selectedDate if no timeSlot has been selected
|
||||
if (this.selectedTimeSlotInfo.timeSlot.routeCode == null) {
|
||||
this.selectedDate = null;
|
||||
}
|
||||
dateSelectedFromPicker(date) {
|
||||
this.selectedDate = date;
|
||||
this.showDatePickerError = false;
|
||||
},
|
||||
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}`;
|
||||
timeSlotSelectedFromPicker(timeSlot) {
|
||||
this.selectedTimeSlotInfo = timeSlot;
|
||||
this.showDatePickerError = false;
|
||||
},
|
||||
forwardButtonAction() {
|
||||
this.mainStore.saveSchedule(this.selectedTimeSlotInfo.timeSlot);
|
||||
if (!this.isFormValid) {
|
||||
this.showDatePickerError = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this.mainStore.saveSchedule(this.selectedTimeSlotInfo.timeSlot);
|
||||
this.$router.navigate(
|
||||
this.navigationScenarios.CLICKED_FORWARD,
|
||||
this.$route
|
||||
|
|
|
|||
|
|
@ -2369,23 +2369,23 @@ export const useMainStore = defineStore({
|
|||
|
||||
const isMobileApt = appointmentType === AppointmentTypeStrings.MOBILE
|
||||
|| appointmentType === AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP;
|
||||
|
||||
|
||||
const retPricedLineItems = await globalMethods.callHttpClient({
|
||||
method: endpoints.TaxOrderItems.method,
|
||||
endpoint: endpoints.TaxOrderItems.url,
|
||||
payload: {
|
||||
ParentAccountNumber: this.order.parentAccountNumber,
|
||||
BillToAccountNumber: this.billToAccountNumber,
|
||||
ProviderNumber: this.providerNumber,
|
||||
AppointmentType: appointmentType,
|
||||
PricedLineItems: getLineItemsFlattened(pricedLineItems),
|
||||
ServiceLocation: {
|
||||
City: isMobileApt ? serviceLocationCity : null,
|
||||
State: isMobileApt ? serviceLocationState : null,
|
||||
ZipCode: isMobileApt ? serviceLocationZipCode : null,
|
||||
},
|
||||
ServerData: lineItemServerData ? lineItemServerData : "",
|
||||
},
|
||||
ParentAccountNumber: this.order.parentAccountNumber,
|
||||
BillToAccountNumber: this.billToAccountNumber,
|
||||
ProviderNumber: this.providerNumber,
|
||||
AppointmentType: appointmentType,
|
||||
PricedLineItems: getLineItemsFlattened(pricedLineItems),
|
||||
ServiceLocation: {
|
||||
City: isMobileApt ? serviceLocationCity : null,
|
||||
State: isMobileApt ? serviceLocationState : null,
|
||||
ZipCode: isMobileApt ? serviceLocationZipCode : null
|
||||
},
|
||||
ServerData: lineItemServerData || ''
|
||||
}
|
||||
}).then((response) => {
|
||||
this.order.lineItems.serverData = response.data.serverData;
|
||||
return addTaxesToPricedLineItems(pricedLineItems, response.data.taxedLineItems);
|
||||
|
|
|
|||
|
|
@ -154,6 +154,7 @@ $border-radius: 0.25rem;
|
|||
$border-radius-sm: 0.2rem;
|
||||
$border-radius-lg: 0.5rem; //Used for buttons. Can be used for other things, of course.
|
||||
$border-radius-pill: 50rem;
|
||||
$border-radius-list-button: 1.375rem; // Used for list buttons
|
||||
|
||||
//Progress Bar Styling
|
||||
$progress-bar-success-color: $green;
|
||||
|
|
|
|||
Loading…
Reference in a new issue