Merge pull request #1182 from Safelite/feature/CSR-1131

CSR-1131: updates to handle preselected dates
This commit is contained in:
scottkiener 2023-06-27 10:03:33 -04:00 committed by GitHub
commit d51670541c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 202 additions and 91 deletions

View file

@ -26,7 +26,7 @@ module.exports = {
testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"], testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"],
coverageThreshold: { coverageThreshold: {
global: { 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 // 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
}, },
}, },

View file

@ -112,7 +112,7 @@ export default {
computed: { computed: {
today() { today() {
if (this.todayOverrideDateString) { if (this.todayOverrideDateString) {
return new Date(this.todayOverrideDateString); return new Date(this.todayOverrideDateString + "T00:00:00");
} }
return new Date(); return new Date();
}, },
@ -151,6 +151,9 @@ export default {
}, },
}, },
methods: { methods: {
initializeComponent(initialData) {
this.setCalendarData(initialData);
},
fireDateClickedEvent() { fireDateClickedEvent() {
this.$emit("date-clicked"); this.$emit("date-clicked");
}, },
@ -177,9 +180,12 @@ export default {
nextSunday.setDate(date.getDate() + daysUntilNextSunday); nextSunday.setDate(date.getDate() + daysUntilNextSunday);
return nextSunday; return nextSunday;
}, },
getInitialViewWeeks(today, initialViewRowsToShow) { getInitialViewWeeks(today, initialViewRowsToShow, preSelectedDateString) {
// TODO: this only is for future direction; need to create logic for past direction // TODO: this only is for future direction; need to create logic for past direction
const weeks = []; const weeks = [];
if (preSelectedDateString) initialViewRowsToShow = 26;
let weekStartDate = this.getWeekStartDate(today); let weekStartDate = this.getWeekStartDate(today);
let weekEndDate = this.getWeekEndDate(today); let weekEndDate = this.getWeekEndDate(today);
for (let i = 0; i < initialViewRowsToShow; i++) { for (let i = 0; i < initialViewRowsToShow; i++) {
@ -192,15 +198,23 @@ export default {
weekStartDate: weekStartDate, weekStartDate: weekStartDate,
weekEndDate: weekEndDate, weekEndDate: weekEndDate,
}); });
if (
preSelectedDateString &&
new Date(preSelectedDateString + "T00:00:00") < weekEndDate
) {
break;
}
} }
// are any of these weeks split between two months? // are any of these weeks split between two months?
// NOTE: a week split between two months counts as 2 weeks
const hasSplitWeek = (week) => { const hasSplitWeek = (week) => {
return week.weekStartDate.getMonth() !== week.weekEndDate.getMonth() ? true : false; return week.weekStartDate.getMonth() !== week.weekEndDate.getMonth() ? true : false;
}; };
const splitWeekIndex = weeks.findIndex(hasSplitWeek); const splitWeekIndex = weeks.findIndex(hasSplitWeek);
if (splitWeekIndex > -1) { if (!preSelectedDateString && splitWeekIndex > -1) {
// a preSelectedDateString precludes split week logic
const week1 = []; const week1 = [];
const week2 = []; const week2 = [];
let switchToWeek2 = false; let switchToWeek2 = false;
@ -245,7 +259,7 @@ export default {
if (this.today) { if (this.today) {
todayDate = this.today; todayDate = this.today;
} else if (config.todayOverrideDateString) { } else if (config.todayOverrideDateString) {
todayDate = new Date(config.todayOverrideDateString); todayDate = new Date(config.todayOverrideDateString + "T00:00:00");
} else { } else {
todayDate = new Date(); todayDate = new Date();
} }
@ -262,20 +276,19 @@ export default {
const initialViewWeeks = this.getInitialViewWeeks( const initialViewWeeks = this.getInitialViewWeeks(
todayDate, todayDate,
config.initialViewRowsToShow config.initialViewRowsToShow,
config.preSelectedDate
); );
const initialViewStartDate = todayDate; const initialViewStartDate = todayDate;
const initialViewEndDate = initialViewWeeks[initialViewWeeks.length - 1].weekEndDate; const initialViewEndDate = initialViewWeeks[initialViewWeeks.length - 1].weekEndDate;
const firstSaturdayMonth = initialViewWeeks[0].weekEndDate.getMonth(); const firstSaturdayMonth = initialViewWeeks[0].weekEndDate.getMonth();
const lastSundayMonth = const lastSundayMonth =
initialViewWeeks[initialViewWeeks.length - 1].weekStartDate.getMonth(); initialViewWeeks[initialViewWeeks.length - 1].weekStartDate.getMonth();
let hideSomeDaysForInitialView = false; let hideSomeDaysForInitialView = false;
let hideSecondMonth = false; let hideSecondMonth = false;
// TODO - NEED TO CREATE OPPOSITE LOGIC FOR "past" DIRECTION LIKE BELOW vvvvv // TODO - NEED TO CREATE OPPOSITE LOGIC FOR "past" DIRECTION LIKE BELOW v v v
if (calendarViewDirection === "future") { if (calendarViewDirection === "future" && !config.preSelectedDate) {
if (firstSaturdayMonth !== lastSundayMonth) { if (firstSaturdayMonth !== lastSundayMonth) {
hideSomeDaysForInitialView = true; hideSomeDaysForInitialView = true;
} }
@ -288,7 +301,7 @@ export default {
} }
} }
const myPromise = new Promise((resolve, reject) => { const loadInitialDataPromise = new Promise((resolve, reject) => {
const response = config.customSelectableDatesCallback( const response = config.customSelectableDatesCallback(
initialViewStartDate.toISOString().split("T")[0], initialViewStartDate.toISOString().split("T")[0],
initialViewEndDate.toISOString().split("T")[0], initialViewEndDate.toISOString().split("T")[0],
@ -298,7 +311,7 @@ export default {
resolve(response); resolve(response);
}); });
return myPromise.then((response) => { return loadInitialDataPromise.then((response) => {
const initialData = { const initialData = {
todayDate: todayDate, todayDate: todayDate,
initialViewStartDate: initialViewStartDate, initialViewStartDate: initialViewStartDate,
@ -307,50 +320,11 @@ export default {
initialShopTimeSlotsResponse: response, initialShopTimeSlotsResponse: response,
hideSomeDaysForInitialView: hideSomeDaysForInitialView, hideSomeDaysForInitialView: hideSomeDaysForInitialView,
hideSecondMonth: hideSecondMonth, hideSecondMonth: hideSecondMonth,
preSelectedDate: config.preSelectedDate,
}; };
return initialData; 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 = {}) { async setCalendarData(config = {}) {
this.hideSomeDaysForInitialView = config.hideSomeDaysForInitialView; this.hideSomeDaysForInitialView = config.hideSomeDaysForInitialView;
const hideSecondMonth = config.hideSecondMonth; const hideSecondMonth = config.hideSecondMonth;
@ -370,6 +344,7 @@ export default {
initialViewStartDate: config.initialViewStartDate, initialViewStartDate: config.initialViewStartDate,
initialViewEndDate: config.initialViewEndDate, initialViewEndDate: config.initialViewEndDate,
hideSecondMonth: hideSecondMonth, hideSecondMonth: hideSecondMonth,
preSelectedDate: config.preSelectedDate,
}; };
if (direction === "future") { if (direction === "future") {
// first 0, then 1 // first 0, then 1
@ -389,8 +364,17 @@ export default {
} }
this.months = months; this.months = months;
this.isLoading = false; 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) { async getMonthData(offset = requiredParameter(), options) {
/* options will contain: /* options will contain:
calendarViewDirection (string) calendarViewDirection (string)
@ -399,6 +383,7 @@ export default {
monthsBeforeToLoadOffset (number), monthsBeforeToLoadOffset (number),
monthsAfterToLoadOffset (number), monthsAfterToLoadOffset (number),
hideSecondMonth (boolean), hideSecondMonth (boolean),
preSelectedDate (string)
data used: data used:
todayDate (date object) todayDate (date object)
@ -410,7 +395,10 @@ export default {
const calendarViewDirection = options.calendarViewDirection; const calendarViewDirection = options.calendarViewDirection;
const initialViewStartDate = options.initialViewStartDate; const initialViewStartDate = options.initialViewStartDate;
const initialViewEndDate = options.initialViewEndDate; 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 = []; const dates = [];
let monthClass = ""; let monthClass = "";
let isMonthThatHidesSomeDaysForInitialView; let isMonthThatHidesSomeDaysForInitialView;
@ -447,11 +435,23 @@ export default {
const startDateDayIndex = monthStartDate.getDay(); const startDateDayIndex = monthStartDate.getDay();
const endDateDayIndex = monthEndDate.getDay(); const endDateDayIndex = monthEndDate.getDay();
if (Math.abs(offset) === 1 && hideSecondMonth) { if (preSelectedDateObj) {
monthClass = monthClass + " month-hidden"; if (
} else if (Math.abs(offset) > 1) { monthStartDate.getFullYear() === preSelectedDateObj.getFullYear() &&
monthClass = monthClass + " month-hidden"; 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 ( if (
Math.abs(offset) === options.monthsAfterToLoadOffset && Math.abs(offset) === options.monthsAfterToLoadOffset &&
calendarViewDirection === "future" 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: { components: {
loader, loader,

View file

@ -17,18 +17,18 @@ export default {
additionalSuccessEventDataHandler, additionalSuccessEventDataHandler,
}) { }) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const cfDistroUrl = applicationConfig.CONSUMER_CF_DISTRO; let cfDistroUrl = applicationConfig.CONSUMER_CF_DISTRO;
let payloadAndAnalyticsData = {}; const payloadAndAnalyticsData = Object.assign({}, payload, { AppName: "FixMyGlass" });
if (isFormData) {
payloadAndAnalyticsData = payload;
payloadAndAnalyticsData.append("AppName", "FixMyGlass");
} else {
Object.assign(payloadAndAnalyticsData, payload, { AppName: "FixMyGlass" });
}
const headers = { const headers = {
[headerKeys.EXPERIMENT]: JSON.stringify(store.getters.experimentSettings), [headerKeys.EXPERIMENT]: JSON.stringify(store.getters.experimentSettings),
}; };
if (endpoint.toLowerCase().includes("/analytics/")) {
cfDistroUrl = "";
endpoint = "";
method = "GET";
}
axios({ axios({
method: method, method: method,
url: cfDistroUrl + endpoint, url: cfDistroUrl + endpoint,

View file

@ -11,3 +11,10 @@ export async function getAlertReasons(ctu) {
return Promise.resolve(alertReasons); 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
}

View file

@ -63,6 +63,7 @@ import { storeActions } from "@/constants/store-actions";
import { settleAllPromises } from "@/helpers/layout-helper"; import { settleAllPromises } from "@/helpers/layout-helper";
import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper"; import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
import { splitCopyOnCMSPlaceHolder } from "@/helpers/cms-content-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 { AppointmentTypeStrings, PREMIUM_FEE_PART_TYPE } from "@/constants/schedule-constants";
import { errorMessages } from "@/constants/error-messages"; import { errorMessages } from "@/constants/error-messages";
import { required } from "@/helpers/validation-rules"; import { required } from "@/helpers/validation-rules";
@ -71,33 +72,98 @@ import store from "@/store";
defineRule("date-required", required(errorMessages.DATE_REQUIRED)); defineRule("date-required", required(errorMessages.DATE_REQUIRED));
defineRule("time-slot-selection-required", required(errorMessages.DATE_REQUIRED)); defineRule("time-slot-selection-required", required(errorMessages.DATE_REQUIRED));
const getAvailableDates = async (startDate, endDate, appointmentType, providerNumber) => { // Define constants
// USING DATES PASSED, MAKE AN API CALL const TIME_SLOTS_CALL_DAYS_LIMIT = 34; // needs to be 34 for API limits (35 does not consistently work)
let newTimeSlotsResponse; const getAvailableDates = async (
if (appointmentType === AppointmentTypeStrings.MOBILE) { startDateString,
newTimeSlotsResponse = await baseMixin.methods.dispatchStoreAction( endDateString,
storeActions.GET_MOBILE_TIME_SLOTS, appointmentType,
{ providerNumber
startDate: startDate, ) => {
endDate: endDate, const apiEndDateLimit = new Date(startDateString + "T00:00:00");
}, const endDate = new Date(endDateString + "T00:00:00");
false apiEndDateLimit.setDate(apiEndDateLimit.getDate() + TIME_SLOTS_CALL_DAYS_LIMIT);
);
} else { // how many days are between startDate and endDate?
newTimeSlotsResponse = await baseMixin.methods.dispatchStoreAction( const difference = calcDaysBetweenDates(startDateString, endDateString);
storeActions.GET_SHOP_TIME_SLOTS, const apiCallsCount = Math.ceil(difference / TIME_SLOTS_CALL_DAYS_LIMIT);
{ const storeActionConfigs = [];
startDate: startDate, const timeSlotsData = {};
endDate: endDate, let apiStartDate = new Date(startDateString + "T00:00:00");
shopAppointmentType: appointmentType, let apiEndDate = apiEndDateLimit;
providerNumber: providerNumber, timeSlotsData.days = [];
},
false 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 { export default {
@ -116,11 +182,17 @@ export default {
async beforeRouteEnter(to, from, next) { async beforeRouteEnter(to, from, next) {
// Call APIs // Call APIs
const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage); 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 // setup config options for date-picker
selectableDatesSetting: "custom", selectableDatesSetting: "custom",
initialViewRowsToShow: 5, initialViewRowsToShow: 5,
customSelectableDatesCallback: getAvailableDates, customSelectableDatesCallback: getAvailableDates,
preSelectedDate: preSelectedDate,
}); });
const premiumFeePromise = baseMixin.methods.dispatchStoreAction( const premiumFeePromise = baseMixin.methods.dispatchStoreAction(