diff --git a/jest.config.js b/jest.config.js index c712c24da..40025f122 100644 --- a/jest.config.js +++ b/jest.config.js @@ -26,7 +26,7 @@ module.exports = { testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"], coverageThreshold: { global: { - statements: 77, + statements: 76, // Got the go ahead from Mark to temporarily lower this. Taking out initialize component made the year,make,model and style coverage drop a bit. Once unit tests for license plate lookup, vin lookup and address lookup are in the coverage should go back up to 90 }, }, diff --git a/src/digital-components/date-picker/date-picker.vue b/src/digital-components/date-picker/date-picker.vue index bc4d6318e..8b2cdc9ce 100644 --- a/src/digital-components/date-picker/date-picker.vue +++ b/src/digital-components/date-picker/date-picker.vue @@ -112,7 +112,7 @@ export default { computed: { today() { if (this.todayOverrideDateString) { - return new Date(this.todayOverrideDateString); + return new Date(this.todayOverrideDateString + "T00:00:00"); } return new Date(); }, @@ -151,6 +151,9 @@ export default { }, }, methods: { + initializeComponent(initialData) { + this.setCalendarData(initialData); + }, fireDateClickedEvent() { this.$emit("date-clicked"); }, @@ -177,9 +180,12 @@ export default { nextSunday.setDate(date.getDate() + daysUntilNextSunday); return nextSunday; }, - getInitialViewWeeks(today, initialViewRowsToShow) { + getInitialViewWeeks(today, initialViewRowsToShow, preSelectedDateString) { // TODO: this only is for future direction; need to create logic for past direction const weeks = []; + + if (preSelectedDateString) initialViewRowsToShow = 26; + let weekStartDate = this.getWeekStartDate(today); let weekEndDate = this.getWeekEndDate(today); for (let i = 0; i < initialViewRowsToShow; i++) { @@ -192,15 +198,23 @@ export default { weekStartDate: weekStartDate, weekEndDate: weekEndDate, }); + if ( + preSelectedDateString && + new Date(preSelectedDateString + "T00:00:00") < weekEndDate + ) { + break; + } } // are any of these weeks split between two months? + // NOTE: a week split between two months counts as 2 weeks const hasSplitWeek = (week) => { return week.weekStartDate.getMonth() !== week.weekEndDate.getMonth() ? true : false; }; const splitWeekIndex = weeks.findIndex(hasSplitWeek); - if (splitWeekIndex > -1) { + if (!preSelectedDateString && splitWeekIndex > -1) { + // a preSelectedDateString precludes split week logic const week1 = []; const week2 = []; let switchToWeek2 = false; @@ -245,7 +259,7 @@ export default { if (this.today) { todayDate = this.today; } else if (config.todayOverrideDateString) { - todayDate = new Date(config.todayOverrideDateString); + todayDate = new Date(config.todayOverrideDateString + "T00:00:00"); } else { todayDate = new Date(); } @@ -262,20 +276,19 @@ export default { const initialViewWeeks = this.getInitialViewWeeks( todayDate, - config.initialViewRowsToShow + config.initialViewRowsToShow, + config.preSelectedDate ); - const initialViewStartDate = todayDate; const initialViewEndDate = initialViewWeeks[initialViewWeeks.length - 1].weekEndDate; const firstSaturdayMonth = initialViewWeeks[0].weekEndDate.getMonth(); const lastSundayMonth = initialViewWeeks[initialViewWeeks.length - 1].weekStartDate.getMonth(); - let hideSomeDaysForInitialView = false; let hideSecondMonth = false; - // TODO - NEED TO CREATE OPPOSITE LOGIC FOR "past" DIRECTION LIKE BELOW vvvvv - if (calendarViewDirection === "future") { + // TODO - NEED TO CREATE OPPOSITE LOGIC FOR "past" DIRECTION LIKE BELOW v v v + if (calendarViewDirection === "future" && !config.preSelectedDate) { if (firstSaturdayMonth !== lastSundayMonth) { hideSomeDaysForInitialView = true; } @@ -288,7 +301,7 @@ export default { } } - const myPromise = new Promise((resolve, reject) => { + const loadInitialDataPromise = new Promise((resolve, reject) => { const response = config.customSelectableDatesCallback( initialViewStartDate.toISOString().split("T")[0], initialViewEndDate.toISOString().split("T")[0], @@ -298,7 +311,7 @@ export default { resolve(response); }); - return myPromise.then((response) => { + return loadInitialDataPromise.then((response) => { const initialData = { todayDate: todayDate, initialViewStartDate: initialViewStartDate, @@ -307,50 +320,11 @@ export default { initialShopTimeSlotsResponse: response, hideSomeDaysForInitialView: hideSomeDaysForInitialView, hideSecondMonth: hideSecondMonth, + preSelectedDate: config.preSelectedDate, }; return initialData; }); }, - initializeComponent(initialData) { - this.setCalendarData(initialData); - }, - scrollToElement(elementId, speed, easing) { - // TODO - needs to be cleaned up & refactored - function scrollTopSmooth(wrapper, target, duration = 300, timingName = "linear") { - const initY = wrapper.scrollTop; - const wrapperRect = wrapper.getBoundingClientRect(); - const targetRect = target.getBoundingClientRect(); - const targetY = targetRect.top - wrapperRect.top - BUFFER_OFFSET; - const timingFunc = TIMINGFUNC_MAP[timingName]; - - let start = null; - - const step = (timestamp) => { - start = start || timestamp; - const progress = timestamp - start, - // Growing from 0 to 1 - time = Math.min(1, (timestamp - start) / duration); - - const percentageNew = timingFunc(time); - const distanceToGo = targetY; - const thisDistance = percentageNew * distanceToGo; - - wrapper.scrollTo(0, initY + thisDistance); - - if (percentageNew < 1) { - window.requestAnimationFrame(step); - } - }; - - window.requestAnimationFrame(step); - } - - const wrapper = this.$refs.datePickerFieldset; - const targetMonth = document.getElementById(elementId); - - scrollTopSmooth(wrapper, targetMonth, 800, "ease-in-out"); - }, - async setCalendarData(config = {}) { this.hideSomeDaysForInitialView = config.hideSomeDaysForInitialView; const hideSecondMonth = config.hideSecondMonth; @@ -370,6 +344,7 @@ export default { initialViewStartDate: config.initialViewStartDate, initialViewEndDate: config.initialViewEndDate, hideSecondMonth: hideSecondMonth, + preSelectedDate: config.preSelectedDate, }; if (direction === "future") { // first 0, then 1 @@ -389,8 +364,17 @@ export default { } this.months = months; this.isLoading = false; - }, + if (config.preSelectedDate) { + this.$nextTick(() => { + //Advance to month + const monthToShow = this.months.find((month) => + month.monthClass.includes("month-preselected") + ); + this.scrollToElement(monthToShow.monthString); + }); + } + }, async getMonthData(offset = requiredParameter(), options) { /* options will contain: calendarViewDirection (string) @@ -399,6 +383,7 @@ export default { monthsBeforeToLoadOffset (number), monthsAfterToLoadOffset (number), hideSecondMonth (boolean), + preSelectedDate (string) data used: todayDate (date object) @@ -410,7 +395,10 @@ export default { const calendarViewDirection = options.calendarViewDirection; const initialViewStartDate = options.initialViewStartDate; const initialViewEndDate = options.initialViewEndDate; - const hideSecondMonth = options.hideSecondMonth; // <<<<<<<<<<<<< + const hideSecondMonth = options.hideSecondMonth; + const preSelectedDateObj = options.preSelectedDate + ? new Date(options.preSelectedDate + "T00:00:00") + : null; const dates = []; let monthClass = ""; let isMonthThatHidesSomeDaysForInitialView; @@ -447,11 +435,23 @@ export default { const startDateDayIndex = monthStartDate.getDay(); const endDateDayIndex = monthEndDate.getDay(); - if (Math.abs(offset) === 1 && hideSecondMonth) { - monthClass = monthClass + " month-hidden"; - } else if (Math.abs(offset) > 1) { - monthClass = monthClass + " month-hidden"; + if (preSelectedDateObj) { + if ( + monthStartDate.getFullYear() === preSelectedDateObj.getFullYear() && + monthStartDate.getMonth() === preSelectedDateObj.getMonth() + ) { + monthClass = monthClass + " month-preselected"; + } else if (monthStartDate > preSelectedDateObj) { + monthClass = monthClass + " month-hidden"; + } + } else { + if (Math.abs(offset) === 1 && hideSecondMonth) { + monthClass = monthClass + " month-hidden"; + } else if (Math.abs(offset) > 1) { + monthClass = monthClass + " month-hidden"; + } } + if ( Math.abs(offset) === options.monthsAfterToLoadOffset && calendarViewDirection === "future" @@ -581,6 +581,38 @@ export default { }); }); }, + scrollToElement(elementId, speed, easing) { + // TODO - needs to be cleaned up & refactored + function scrollTopSmooth(wrapper, target, duration = 300, timingName = "linear") { + const initY = wrapper.scrollTop; + const wrapperRect = wrapper.getBoundingClientRect(); + const targetRect = target.getBoundingClientRect(); + const targetY = targetRect.top - wrapperRect.top - BUFFER_OFFSET; + const timingFunc = TIMINGFUNC_MAP[timingName]; + let start = null; + + const step = (timestamp) => { + start = start || timestamp; + const progress = timestamp - start, + // Growing from 0 to 1 + time = Math.min(1, (timestamp - start) / duration); + const percentageNew = timingFunc(time); + const distanceToGo = targetY; + const thisDistance = percentageNew * distanceToGo; + + wrapper.scrollTo(0, initY + thisDistance); + if (percentageNew < 1) { + window.requestAnimationFrame(step); + } + }; + window.requestAnimationFrame(step); + } + + const wrapper = document.getElementById("date-picker-fieldset"); + const targetMonth = document.getElementById(elementId); + + scrollTopSmooth(wrapper, targetMonth, 800, "ease-in-out"); + }, }, components: { loader, diff --git a/src/global-methods.js b/src/global-methods.js index 756b58dc8..546e3c7c0 100644 --- a/src/global-methods.js +++ b/src/global-methods.js @@ -17,18 +17,18 @@ export default { additionalSuccessEventDataHandler, }) { return new Promise((resolve, reject) => { - const cfDistroUrl = applicationConfig.CONSUMER_CF_DISTRO; - let payloadAndAnalyticsData = {}; - if (isFormData) { - payloadAndAnalyticsData = payload; - payloadAndAnalyticsData.append("AppName", "FixMyGlass"); - } else { - Object.assign(payloadAndAnalyticsData, payload, { AppName: "FixMyGlass" }); - } + let cfDistroUrl = applicationConfig.CONSUMER_CF_DISTRO; + const payloadAndAnalyticsData = Object.assign({}, payload, { AppName: "FixMyGlass" }); const headers = { [headerKeys.EXPERIMENT]: JSON.stringify(store.getters.experimentSettings), }; + if (endpoint.toLowerCase().includes("/analytics/")) { + cfDistroUrl = ""; + endpoint = ""; + method = "GET"; + } + axios({ method: method, url: cfDistroUrl + endpoint, diff --git a/src/layouts/schedule/helpers/schedule-helper.js b/src/layouts/schedule/helpers/schedule-helper.js index 005bd4b19..6f8a09373 100644 --- a/src/layouts/schedule/helpers/schedule-helper.js +++ b/src/layouts/schedule/helpers/schedule-helper.js @@ -11,3 +11,10 @@ export async function getAlertReasons(ctu) { return Promise.resolve(alertReasons); } + +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 +} diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index a364319ee..ddda457ca 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -63,6 +63,7 @@ import { storeActions } from "@/constants/store-actions"; import { settleAllPromises } from "@/helpers/layout-helper"; import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper"; import { splitCopyOnCMSPlaceHolder } from "@/helpers/cms-content-helper"; +import { calcDaysBetweenDates } from "@/layouts/schedule/helpers/schedule-helper"; import { AppointmentTypeStrings, PREMIUM_FEE_PART_TYPE } from "@/constants/schedule-constants"; import { errorMessages } from "@/constants/error-messages"; import { required } from "@/helpers/validation-rules"; @@ -71,33 +72,98 @@ import store from "@/store"; defineRule("date-required", required(errorMessages.DATE_REQUIRED)); defineRule("time-slot-selection-required", required(errorMessages.DATE_REQUIRED)); -const getAvailableDates = async (startDate, endDate, appointmentType, providerNumber) => { - // USING DATES PASSED, MAKE AN API CALL +// Define constants +const TIME_SLOTS_CALL_DAYS_LIMIT = 34; // needs to be 34 for API limits (35 does not consistently work) - let newTimeSlotsResponse; - if (appointmentType === AppointmentTypeStrings.MOBILE) { - newTimeSlotsResponse = await baseMixin.methods.dispatchStoreAction( - storeActions.GET_MOBILE_TIME_SLOTS, - { - startDate: startDate, - endDate: endDate, - }, - false - ); - } else { - newTimeSlotsResponse = await baseMixin.methods.dispatchStoreAction( - storeActions.GET_SHOP_TIME_SLOTS, - { - startDate: startDate, - endDate: endDate, - shopAppointmentType: appointmentType, - providerNumber: providerNumber, - }, - false - ); +const getAvailableDates = async ( + startDateString, + endDateString, + appointmentType, + providerNumber +) => { + const apiEndDateLimit = new Date(startDateString + "T00:00:00"); + const endDate = new Date(endDateString + "T00:00:00"); + apiEndDateLimit.setDate(apiEndDateLimit.getDate() + TIME_SLOTS_CALL_DAYS_LIMIT); + + // how many days are between startDate and endDate? + const difference = calcDaysBetweenDates(startDateString, endDateString); + const apiCallsCount = Math.ceil(difference / TIME_SLOTS_CALL_DAYS_LIMIT); + const storeActionConfigs = []; + const timeSlotsData = {}; + let apiStartDate = new Date(startDateString + "T00:00:00"); + let apiEndDate = apiEndDateLimit; + timeSlotsData.days = []; + + for (let i = 1; i <= apiCallsCount; i++) { + let storeActionConfig; + + if (i > 1) { + apiStartDate = new Date(apiEndDate); + apiStartDate.setDate(apiStartDate.getDate() + 1); + apiEndDate = new Date(apiStartDate); + apiEndDate.setDate(apiEndDate.getDate() + TIME_SLOTS_CALL_DAYS_LIMIT); + } + if (i === apiCallsCount) { + apiEndDate = new Date(endDateString + "T00:00:00"); + } + + if (appointmentType === AppointmentTypeStrings.MOBILE) { + storeActionConfig = { + storeAction: storeActions.GET_MOBILE_TIME_SLOTS, + payload: { + startDate: apiStartDate.toISOString().split("T")[0], + endDate: apiEndDate.toISOString().split("T")[0], + }, + }; + } else { + storeActionConfig = { + storeAction: storeActions.GET_SHOP_TIME_SLOTS, + payload: { + startDate: apiStartDate.toISOString().split("T")[0], + endDate: apiEndDate.toISOString().split("T")[0], + shopAppointmentType: appointmentType, + providerNumber: providerNumber, + }, + }; + } + storeActionConfigs.push(storeActionConfig); } - return newTimeSlotsResponse.data; + // ASYNC METHOD + 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) => { + const timeSlotsResponse = await baseMixin.methods.dispatchStoreAction( + storeAction.storeAction, + storeAction.payload, + false + ); + 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 { @@ -116,11 +182,17 @@ export default { async beforeRouteEnter(to, from, next) { // Call APIs const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage); - const datePickerInitialDataPromise = datePicker.methods.loadInitialData({ + let preSelectedDate = await store.getters.order.schedule.date; + if (!preSelectedDate || preSelectedDate.startTime === null) { + preSelectedDate = null; + } + + const datePickerInitialDataPromise = await datePicker.methods.loadInitialData({ // setup config options for date-picker selectableDatesSetting: "custom", initialViewRowsToShow: 5, customSelectableDatesCallback: getAvailableDates, + preSelectedDate: preSelectedDate, }); const premiumFeePromise = baseMixin.methods.dispatchStoreAction(