From b15ea179b942943a6640d27e3f8eb24382cb81b9 Mon Sep 17 00:00:00 2001 From: Bill Richardson Date: Wed, 14 Jan 2026 13:29:52 -0500 Subject: [PATCH] updates for schedule --- .../date-picker/date-picker.vue | 214 ++++++++++++++++-- src/helpers/date-helper.js | 6 + .../schedule-page/schedule-page.spec.js | 11 +- src/layouts/schedule-page/schedule-page.vue | 17 +- src/store/index.js | 26 +-- src/styles/ux-variables.scss | 1 + 6 files changed, 226 insertions(+), 49 deletions(-) diff --git a/src/digital-components/date-picker/date-picker.vue b/src/digital-components/date-picker/date-picker.vue index e369f11f..7c9bacc2 100644 --- a/src/digital-components/date-picker/date-picker.vue +++ b/src/digital-components/date-picker/date-picker.vue @@ -16,7 +16,7 @@ class="appointment-date-picker-container" :class="isMobileView ? 'mobile' : 'desktop'">
- @@ -25,13 +25,13 @@ src="~@/assets/img/icons/icon-ablue-previous.png" alt="Left arrow icon" /> Previous - +
{{ headerDateString }}
- More @@ -39,7 +39,7 @@ class="arrow-icon" src="~@/assets/img/icons/icon-ablue-next.png" alt="Right arrow icon" /> - +
@@ -121,8 +134,12 @@ import { convertDateToTwoDigitDay, convertDateToTwoDigitMonth, getDisplayTextForDurationLength, + isAfternoon, militaryToTwelveHourTime } from '@/helpers/date-helper'; +import { + PREMIUM_TIME_SLOT_ID_FLAG +} from '@/constants/schedule-constants'; import showIssLoadingModal from '@/helpers/loading-modal-helper.js'; import errorMessages from '@/constants/error-messages.js'; import { selectableDaysOptions } from './mixins/helpers'; @@ -207,6 +224,7 @@ export default { selectableTimeSlotsData: [], selectedTimeOfDayGrouping: null, selectedDate: null, + selectedTime: null, selectedTimeSlot: null, activeDate: new Date(), activePageIndex: 0, @@ -262,7 +280,8 @@ export default { watch: { selectedTimeSlot(newValue, oldValue) { if (newValue !== oldValue) { - this.$emit('timeSlotSelected', newValue); + const testObj = this.getSelectedTimeSlotInfoObject(newValue); + this.$emit('timeSlotSelected', testObj); } }, selectedDate(newValue, oldValue) { @@ -285,11 +304,54 @@ export default { initializeComponent(initialData) { this.setCalendarData(initialData); }, + addPremiumFlagToInput(routeCode) { + return (`${routeCode}${PREMIUM_TIME_SLOT_ID_FLAG}`); + }, + removePremiumFlagFromInput(routeCode) { + return routeCode.replace(PREMIUM_TIME_SLOT_ID_FLAG, ''); + }, + getSelectedTimeSlotInfoObject(timeSlot) { + const routeCode = timeSlot?.id; + let routeCodeToUse = routeCode; + const routeCodeIncludesPremium = routeCode?.includes(PREMIUM_TIME_SLOT_ID_FLAG); + if (routeCodeIncludesPremium) { + routeCodeToUse = this.removePremiumFlagFromInput(routeCode); + } + + const fullTimeSlot = this.selectableTimeSlotsData?.find((ts) => ts.id === routeCodeToUse); + + if (fullTimeSlot) { + return { + timeSlot: { + date: this.selectedDate, + routeCode: fullTimeSlot.id, + startTime: fullTimeSlot.startTime, + endTime: fullTimeSlot.endTime, + jobMaxMinutes: this.appointmentDurationMinutesMaximum.toString(), + jobMinMinutes: this.appointmentDurationMinutesMinimum.toString() + }, + isPremiumAppointment: !!routeCodeIncludesPremium + }; + } + + return { + timeSlot: { + date: null, + startTime: null, + endTime: null, + routeCode: null, + jobMaxMinutes: null, + jobMinMinutes: null + }, + isPremiumAppointment: null + }; + }, handleMqlChange(e) { this.isMobileView = !e.matches; this.activePageIndex = 0; this.selectedDate = null; this.selectedTimeOfDayGrouping = null; + this.selectedTime = null; this.selectedTimeSlot = null; this.findFirstAvailableDateInView(); }, @@ -311,6 +373,46 @@ export default { } } }, + findInitialPageIndex(daysFromStart) { + if (daysFromStart !== null && daysFromStart !== undefined) { + return { + initialPageIndex: Math.floor(daysFromStart / this.daysToAdd), + initialDayIndex: daysFromStart % this.daysToAdd + }; + } + return { + initialPageIndex: 0, + initialDayIndex: null + }; + }, + findInitialDate(myDateString, initialDayIndex) { + const dateInView = this.getDateToShow(initialDayIndex); + const dateInViewString = convertDateToDateString(dateInView); + if (dateInViewString === myDateString) { + const isAfternoonSlot = isAfternoon(myDateString); + const selectableDateObj = this.selectableDatesData.find((dateObj) => dateObj.date === dateInViewString); + if (selectableDateObj) { + if (isAfternoonSlot) { + this.showAfternoonTimes(initialDayIndex); + this.findInitialTimeByStore(selectableDateObj.afternoonTimeSlots); + } else { + this.showMorningTimes(initialDayIndex); + this.findInitialTimeByStore(selectableDateObj.morningTimeSlots); + } + } + } + }, + findInitialTimeByStore(timeSlots) { + const initialTimeSlot = this.mainStore.order.schedule; + if (!initialTimeSlot || !initialTimeSlot.startTime) { + return; + } + const initialStartTime = initialTimeSlot.startTime; + const foundTimeSlot = timeSlots.find((ts) => ts.startTime === initialStartTime); + if (foundTimeSlot) { + this.selectTimeSlotForDay(foundTimeSlot); + } + }, getDateToShow(index) { const dayOffset = this.activePageIndex * this.daysToAdd; const dateToShow = new Date(this.activeDate); @@ -321,6 +423,38 @@ export default { const dateToShow = this.getDateToShow(index); return convertDateToDateString(dateToShow); }, + getEndDateForExistingDate(startDate, selectedDateString, daysInView) { + const endDate = new Date(startDate); + endDate.setDate(endDate.getDate() + (daysInView - 1)); + + if (selectedDateString) { + const selectedDate = new Date(`${selectedDateString}T00:00:00`); + const daysDifferenceComparedToStartDate = Math.ceil((selectedDate - startDate) / (1000 * 60 * 60 * 24)); + if (selectedDate > endDate) { + const daysDifferenceComparedToStartDateAsPages = Math.ceil(daysDifferenceComparedToStartDate / daysInView); + const newEndDate = new Date(startDate); + newEndDate.setDate(newEndDate.getDate() + (daysDifferenceComparedToStartDateAsPages * daysInView) - 1); + + return { + endDateString: convertDateToDateString(newEndDate), + newDaysLoaded: (daysDifferenceComparedToStartDateAsPages * daysInView), + daysFromStart: daysDifferenceComparedToStartDate + }; + } + + return { + endDateString: convertDateToDateString(endDate), + newDaysLoaded: null, + daysFromStart: daysDifferenceComparedToStartDate + }; + } + + return { + endDateString: convertDateToDateString(endDate), + newDaysLoaded: null, + daysFromStart: null + }; + }, gotoNextPage() { const maxPageIndex = Math.floor((this.daysLoaded - 1) / this.daysToAdd); if (this.activePageIndex >= maxPageIndex) { @@ -329,6 +463,7 @@ export default { this.daysLoaded += this.initialDaysToLoad; this.activePageIndex += 1; this.selectedDate = null; + this.selectedTime = null; this.selectedTimeSlot = null; this.findFirstAvailableDateInView(); }); @@ -336,12 +471,14 @@ export default { } this.activePageIndex += 1; this.selectedDate = null; + this.selectedTIme = null; this.selectedTimeSlot = null; this.findFirstAvailableDateInView(); }, gotoPreviousPage() { this.activePageIndex -= 1; this.selectedDate = null; + this.selectedTime = null; this.selectedTimeSlot = null; this.findFirstAvailableDateInView(); }, @@ -357,6 +494,7 @@ export default { }, selectTimeSlotForDay(timeSlot) { if (timeSlot) { + this.selectedTime = timeSlot.startTime; this.selectedTimeSlot = timeSlot; } }, @@ -387,6 +525,7 @@ export default { this.selectedDate = morningDateString; this.selectedTimeOfDayGrouping = 'morning'; this.selectableTimeSlotsData = selectableDateObj.morningTimeSlots; + this.selectedTime = null; this.selectedTimeSlot = null; } }, @@ -411,13 +550,18 @@ export default { this.selectedTimeOfDayGrouping = 'afternoon'; this.selectableTimeSlotsData = selectableDateObj.afternoonTimeSlots; this.selectedTimeSlot = null; + this.selectedTime = null; } }, + timeSlotInputChanged(timeSlot) { + this.selectTimeSlotForDay(timeSlot); + }, async loadInitialData(config) { /* ** NOTE: this _could_ be called by a parent before fully loaded, so data or computeds might not be available */ let todayDateString; + const todayDateObject = new Date(); const calendarViewDirection = 'future'; const initialDays = 15; @@ -426,13 +570,23 @@ export default { } else if (config.todayOverrideDateString) { todayDateString = config.todayOverrideDateString; } else { - todayDateString = convertDateToDateString(new Date()); + todayDateString = convertDateToDateString(todayDateObject); } const initialViewStartDate = todayDateString; const initialEndDate = new Date(); initialEndDate.setDate(initialEndDate.getDate() + (initialDays - 1)); - const initialViewEndDate = convertDateToDateString(initialEndDate); + let initialViewEndDate = convertDateToDateString(initialEndDate); + + const endDateObject = this.getEndDateForExistingDate( + todayDateObject, + config.preSelectedDate, + initialDays + ); + + if (endDateObject.endDateString !== initialViewEndDate) { + initialViewEndDate = endDateObject.endDateString; + } const loadInitialDataPromise = new Promise((resolve) => { const response = config.customSelectableDatesCallback( @@ -452,7 +606,8 @@ export default { calendarViewDirection, initialShopTimeSlotsResponse: response, preSelectedDate: config.preSelectedDate, - initialDaysLoaded: initialDays + initialDaysLoaded: endDateObject.newDaysLoaded || initialDays, + daysFromStart: endDateObject.daysFromStart || null }; return initialData; @@ -480,7 +635,18 @@ export default { this.appointmentDurationMinutesMaximum = config.initialShopTimeSlotsResponse.estimatedServiceMinutesMaximum; this.appointmentDurationMinutesMinimum = config.initialShopTimeSlotsResponse.estimatedServiceMinutesMinimum; - this.findFirstAvailableDateInView(); + let pageIndexObj = { initialPageIndex: 0, initialDayIndex: null }; + if (config.daysFromStart !== null && config.daysFromStart !== undefined) { + pageIndexObj = this.findInitialPageIndex(config.daysFromStart); + this.activePageIndex = pageIndexObj.initialPageIndex; + } + + if (config.preSelectedDate) { + this.findInitialDate(config.preSelectedDate, pageIndexObj.initialDayIndex); + } else { + this.findFirstAvailableDateInView(); + } + this.isLoading = false; }, async loadMoreDays(maxPageIndex, daysInView, daysToFetch) { @@ -590,6 +756,9 @@ export default { .span-block-flex { display: flex; align-items: center; + border: none; + background-color: #fff; + color: #525656; cursor: pointer; &:hover { @@ -601,6 +770,7 @@ export default { text-align: end; .span-block-flex { + justify-self: flex-end; justify-content: flex-end; img { @@ -610,6 +780,7 @@ export default { } .header-middle { font-weight: 600; + color: #000; @include media-breakpoint-up(xs) { grid-column: 2 / span 1; } @@ -623,6 +794,7 @@ export default { text-align: start; .span-block-flex { + justify-self: flex-start; justify-content: flex-start; img { @@ -677,6 +849,10 @@ export default { border: solid 1px #0070d1; } + &:focus, &:focus-visible { + outline: none; + border: solid 2px #0070d1; + } } .span-block { display: block; @@ -697,7 +873,7 @@ export default { .time-slot-button { width: 100%; border: 1px solid #b3b4b5; - border-radius: 1.375rem; + border-radius: $border-radius-list-button; box-shadow: 0 1px 5px 0 rgba(0, 0, 0, .2); color: #525656; padding: 0.5rem 0; diff --git a/src/helpers/date-helper.js b/src/helpers/date-helper.js index bedf9ca2..24a90760 100644 --- a/src/helpers/date-helper.js +++ b/src/helpers/date-helper.js @@ -66,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; diff --git a/src/layouts/schedule-page/schedule-page.spec.js b/src/layouts/schedule-page/schedule-page.spec.js index 49ef7264..133654da 100644 --- a/src/layouts/schedule-page/schedule-page.spec.js +++ b/src/layouts/schedule-page/schedule-page.spec.js @@ -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', () => { @@ -296,7 +295,9 @@ describe('schedule-page.vue', () => { const { wrapper } = getShallowMountedComponent(); wrapper.vm.$router.navigate = jest.fn(() => ({})); wrapper.vm.selectedTimeSlotInfo = { - id: 'test-id' + timeSlot: { + routeCode: 'test-id' + } }; // Act diff --git a/src/layouts/schedule-page/schedule-page.vue b/src/layouts/schedule-page/schedule-page.vue index 8a5667b0..aa08e79a 100644 --- a/src/layouts/schedule-page/schedule-page.vue +++ b/src/layouts/schedule-page/schedule-page.vue @@ -73,7 +73,7 @@ import BaseFormMixin from '@/mixins/base-form-mixin.js'; import { useMainStore } from '@/store'; // 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, @@ -83,7 +83,7 @@ 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); @@ -98,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) { @@ -108,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: { @@ -278,7 +274,7 @@ export default { return useMainStore().order.serviceLocation.appointmentType; }, isFormValid() { - const hasTimeSlotSelected = this.selectedTimeSlotInfo?.id != null; + const hasTimeSlotSelected = this.selectedTimeSlotInfo?.timeSlot?.routeCode != null; return hasTimeSlotSelected; }, supportingItems() { @@ -328,9 +324,6 @@ export default { getServiceZipCtuCodeFromStore() { return this.mainStore.order.serviceLocation.zipCodeCtu; }, - dateTimeSelected() { - console.log('dateTimeSelected called'); - }, getSelectedDate() { return this.mainStore.order.schedule.date; }, diff --git a/src/store/index.js b/src/store/index.js index 1d6e44b7..5d2c3c15 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -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); diff --git a/src/styles/ux-variables.scss b/src/styles/ux-variables.scss index 41cd8269..280b9a24 100644 --- a/src/styles/ux-variables.scss +++ b/src/styles/ux-variables.scss @@ -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;