Merge pull request #1005 from Safelite/feature/richardson/INSR-7767

Bunch o crap
This commit is contained in:
brich1212safe 2026-01-15 12:49:30 -05:00 committed by GitHub
commit 100d0ff854
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 791 additions and 1072 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 494 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 510 B

View file

@ -59,7 +59,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

View file

@ -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;
@ -51,6 +66,12 @@ export function getDisplayTextForDurationLength(durationMinimum, durationMaximum
return `${durationText} ${unitText}`; 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) { export function militaryToTwelveHourTime(timeString) {
// Expected input: "HH:MM" // Expected input: "HH:MM"
if (typeof timeString !== 'string') return null; if (typeof timeString !== 'string') return null;
@ -163,9 +184,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

View file

@ -6,7 +6,6 @@ import { createTestingPinia } from '@pinia/testing';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { getMountOptions } from '@/helpers/unit-test-helper.js'; import { getMountOptions } from '@/helpers/unit-test-helper.js';
import { useMainStore } from '@/store/index.js'; import { useMainStore } from '@/store/index.js';
import { AppointmentTypeStrings } from '@/constants/schedule-constants';
// Mock fetchCmsContentForPage // Mock fetchCmsContentForPage
jest.mock('@/helpers/cms-content-helper', () => ({ jest.mock('@/helpers/cms-content-helper', () => ({
@ -214,7 +213,7 @@ describe('schedule-page.vue', () => {
// Act // Act
const newShopTimeSlots = await wrapper.vm.getAvailableDatesMethod( const newShopTimeSlots = await wrapper.vm.getAvailableDatesMethod(
'2023-01-01', '2023-01-01',
'2023-01-31' '2023-01-15'
); );
// Assert // Assert
@ -236,7 +235,7 @@ describe('schedule-page.vue', () => {
estimatedServiceMinutesMaximum: 120 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 // Arrange
const { wrapper } = getShallowMountedComponent(); const { wrapper } = getShallowMountedComponent();
wrapper.vm.selectableDatesData = { wrapper.vm.selectableDatesData = {
@ -264,7 +263,7 @@ describe('schedule-page.vue', () => {
// 2023-01-01 --> 2023-02-05 // 2023-01-01 --> 2023-02-05
// 2023-02-06 --> 2023-03-12 // 2023-02-06 --> 2023-03-12
// 2023-03-13 --> 2023-03-31 // 2023-03-13 --> 2023-03-31
expect(store.getShopTimeSlots).toHaveBeenCalledTimes(3); expect(store.getShopTimeSlots).toHaveBeenCalledTimes(6);
}); });
}); });
describe('Rendering', () => { describe('Rendering', () => {
@ -290,32 +289,16 @@ 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
const { wrapper } = getShallowMountedComponent(); const { wrapper } = getShallowMountedComponent();
wrapper.vm.$router.navigate = jest.fn(() => ({})); wrapper.vm.$router.navigate = jest.fn(() => ({}));
wrapper.vm.selectedTimeSlotInfo = {
timeSlot: {
routeCode: 'test-id'
}
};
// Act // Act
await wrapper.vm.forwardButtonAction(); await wrapper.vm.forwardButtonAction();

View file

@ -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,27 +65,15 @@ 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 = 15;
const getAvailableDates = async ( const getAvailableDates = async (
startDateString, startDateString,
@ -127,13 +83,11 @@ const getAvailableDates = async (
) => { ) => {
const apiEndDateLimit = sumDateString( const apiEndDateLimit = sumDateString(
startDateString, startDateString,
TIME_SLOTS_CALL_DAYS_LIMIT TIME_SLOTS_CALL_DAYS_LIMIT - 1
); );
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;
@ -144,7 +98,7 @@ const getAvailableDates = async (
apiStartDate = sumDateString(apiEndDate, 1); apiStartDate = sumDateString(apiEndDate, 1);
apiEndDate = sumDateString( apiEndDate = sumDateString(
apiStartDate, apiStartDate,
TIME_SLOTS_CALL_DAYS_LIMIT TIME_SLOTS_CALL_DAYS_LIMIT - 1
); );
if (i === apiCallsCount) { if (i === apiCallsCount) {
@ -154,11 +108,7 @@ const getAvailableDates = async (
apiEndDate = apiEndDateLimit; apiEndDate = apiEndDateLimit;
} }
if ( if (appointmentType === AppointmentTypeStrings.MOBILE || appointmentType === AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP) {
appointmentType === AppointmentTypeStrings.MOBILE
|| appointmentType
=== AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP
) {
storeActionConfig = { storeActionConfig = {
storeAction: GET_MOBILE_TIME_SLOTS, storeAction: GET_MOBILE_TIME_SLOTS,
payload: { payload: {
@ -232,9 +182,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 +248,6 @@ export default {
resultMap.datePickerInitialData.initialShopTimeSlotsResponse, resultMap.datePickerInitialData.initialShopTimeSlotsResponse,
resultMap.premiumFeeWithPrice resultMap.premiumFeeWithPrice
); );
vm.updateFooterButtonText(vm.selectedTimeSlotInfo);
}); });
}, },
setup() { setup() {
@ -312,6 +259,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 +273,14 @@ export default {
appointmentType() { appointmentType() {
return useMainStore().order.serviceLocation.appointmentType; return useMainStore().order.serviceLocation.appointmentType;
}, },
timeSlotsForSelectedDate() { isFormValid() {
if (!this.selectedDate) { const hasTimeSlotSelected = this.selectedTimeSlotInfo?.timeSlot?.routeCode != null;
return 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,9 +324,6 @@ export default {
getServiceZipCtuCodeFromStore() { getServiceZipCtuCodeFromStore() {
return this.mainStore.order.serviceLocation.zipCodeCtu; return this.mainStore.order.serviceLocation.zipCodeCtu;
}, },
openInshopTimeSlotsModal() {
this.$refs.timeSlotModalQuestion.openModal();
},
getSelectedDate() { getSelectedDate() {
return this.mainStore.order.schedule.date; return this.mainStore.order.schedule.date;
}, },
@ -420,64 +341,21 @@ export default {
return selectedTimeSlotInfo; return selectedTimeSlotInfo;
}, },
timeSlotModalClosed() { dateSelectedFromPicker(date) {
// Clear the selectedDate if no timeSlot has been selected this.selectedDate = date;
if (this.selectedTimeSlotInfo.timeSlot.routeCode == null) { this.showDatePickerError = false;
this.selectedDate = null;
}
}, },
updateFooterButtonText(timeSlotInfo) { timeSlotSelectedFromPicker(timeSlot) {
let navbarButtonText; this.selectedTimeSlotInfo = timeSlot;
if (!timeSlotInfo || !timeSlotInfo.timeSlot.date) { this.showDatePickerError = false;
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}`;
}, },
forwardButtonAction() { forwardButtonAction() {
this.mainStore.saveSchedule(this.selectedTimeSlotInfo.timeSlot); if (!this.isFormValid) {
this.showDatePickerError = true;
return;
}
this.mainStore.saveSchedule(this.selectedTimeSlotInfo.timeSlot);
this.$router.navigate( this.$router.navigate(
this.navigationScenarios.CLICKED_FORWARD, this.navigationScenarios.CLICKED_FORWARD,
this.$route this.$route

View file

@ -2369,23 +2369,23 @@ export const useMainStore = defineStore({
const isMobileApt = appointmentType === AppointmentTypeStrings.MOBILE const isMobileApt = appointmentType === AppointmentTypeStrings.MOBILE
|| appointmentType === AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP; || appointmentType === AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP;
const retPricedLineItems = await globalMethods.callHttpClient({ const retPricedLineItems = await globalMethods.callHttpClient({
method: endpoints.TaxOrderItems.method, method: endpoints.TaxOrderItems.method,
endpoint: endpoints.TaxOrderItems.url, endpoint: endpoints.TaxOrderItems.url,
payload: { payload: {
ParentAccountNumber: this.order.parentAccountNumber, ParentAccountNumber: this.order.parentAccountNumber,
BillToAccountNumber: this.billToAccountNumber, BillToAccountNumber: this.billToAccountNumber,
ProviderNumber: this.providerNumber, ProviderNumber: this.providerNumber,
AppointmentType: appointmentType, AppointmentType: appointmentType,
PricedLineItems: getLineItemsFlattened(pricedLineItems), PricedLineItems: getLineItemsFlattened(pricedLineItems),
ServiceLocation: { ServiceLocation: {
City: isMobileApt ? serviceLocationCity : null, City: isMobileApt ? serviceLocationCity : null,
State: isMobileApt ? serviceLocationState : null, State: isMobileApt ? serviceLocationState : null,
ZipCode: isMobileApt ? serviceLocationZipCode : null, ZipCode: isMobileApt ? serviceLocationZipCode : null
}, },
ServerData: lineItemServerData ? lineItemServerData : "", ServerData: lineItemServerData || ''
}, }
}).then((response) => { }).then((response) => {
this.order.lineItems.serverData = response.data.serverData; this.order.lineItems.serverData = response.data.serverData;
return addTaxesToPricedLineItems(pricedLineItems, response.data.taxedLineItems); return addTaxesToPricedLineItems(pricedLineItems, response.data.taxedLineItems);

View file

@ -154,6 +154,7 @@ $border-radius: 0.25rem;
$border-radius-sm: 0.2rem; $border-radius-sm: 0.2rem;
$border-radius-lg: 0.5rem; //Used for buttons. Can be used for other things, of course. $border-radius-lg: 0.5rem; //Used for buttons. Can be used for other things, of course.
$border-radius-pill: 50rem; $border-radius-pill: 50rem;
$border-radius-list-button: 1.375rem; // Used for list buttons
//Progress Bar Styling //Progress Bar Styling
$progress-bar-success-color: $green; $progress-bar-success-color: $green;