Merge pull request #1105 from Safelite/feature/CSR-886

Feature/csr 886
This commit is contained in:
AdamCaouetteSafelite 2023-05-18 12:50:24 -04:00 committed by GitHub
commit 25607e8f19
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 724 additions and 305 deletions

View file

@ -15,7 +15,7 @@ module.exports = {
"!src/layouts/vehicle-damage/windshield-options/windshield-options.vue",
"!src/layouts/reveal/**/*.vue",
"!src/ux-components/text-link/**/*.vue",
"!src/common-components/date-picker/**/*.vue", // Temp until unit tests completed
"!src/digital-components/date-picker/**/*.vue", // Temp until unit tests completed
"!src/layouts/vin-lookup/**/*.vue", //Temporary for Quote page testing
"!src/common-components/funnel-header/menu-modal/**/*.vue",
"!src/layouts/schedule/*.vue", // Temp test exclusion while in development

View file

@ -106,6 +106,11 @@ const endpoints = {
url: "/parts/api/v1/parts/part-from-capability-answer",
method: "POST",
},
GetShopTimeSlots: {
url: "/schedule/api/v1/schedule/shop-time-slots",
mockUrl: "https://mockey.qa.sagaws.net/service/shop-time-slots", // TODO: REMOVE MOCKURL
method: "POST",
},
SaveSession: {
url: "/order/api/v1/order/save-session",
method: "POST",

View file

@ -1,16 +0,0 @@
const monthsOfYear = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
export { monthsOfYear };

View file

@ -32,6 +32,7 @@ const storeActions = {
GET_MOLDING_QUESTIONS: "getMoldingQuestions",
GET_MOBILE_FEE_PART: "getMobileFeePart",
GET_SERVICEABILITY_DETAILS: "getServiceabilityDetails",
GET_SHOP_TIME_SLOTS: "getShopTimeSlots",
GET_PROVIDERS: "getProviders",
SAVE_SESSION: "saveSession",
LOAD_SESSION: "loadSession",

View file

@ -1 +0,0 @@
test.todo("some test to be written in the future");

View file

@ -6,15 +6,55 @@ import { getMountOptions } from "@/helpers/unit-test-helper.js";
describe("date-picker.vue", () => {
describe("initial setup", () => {
test("Creates a date object from today", () => {
test("Creates a date object from today", async () => {
// Arrange
const { wrapper } = setupMocks({});
const { wrapper } = await setupMocks({
propsData: {
selectableDatesSetting: "custom"
},
});
// Act
const testResult = wrapper.vm.todayDate instanceof Date;
wrapper.vm.loadInitialData = jest.fn().mockImplementation(
() =>
new Promise((resolve, reject) => {
resolve({
data: [responseValue],
});
})
);
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
expect(wrapper.vm.loadInitialData).toHaveBeenCalled();
// console.log("wrapper.vm.today: ", wrapper.vm.today)
// // Act
// const testResult = wrapper.vm.today instanceof Date;
// console.log("testResult: ", testResult)
// Assert
expect(testResult).toEqual(true);
// // Assert
// expect(testResult).toEqual(true);
wrapper.unmount();
});

View file

@ -1,58 +1,68 @@
<template>
<div class="date-picker text-center" :class="calendarViewDirection">
<fieldset v-if="months">
<legend class="sr-only">Date Picker</legend>
<fieldset id="date-picker-fieldset" ref="datePickerFieldset">
<legend class="sr-only">Select a day and time</legend>
<div
v-for="month in months"
:key="`${month.monthLabel}-${month.yearNum.toString()}-${months.length}`">
:key="`${month.monthLabel}-${month.yearNum?.toString()}`"
:id="`${month.monthLabel}-${month.yearNum?.toString()}`"
class="calendar-grid-container position-relative"
:class="[
hideSomeDaysForInitialView ? 'partial-month-initial-view' : '',
month.monthClass,
]">
<div class="month-year body-small d-flex align-items-center small">
{{ month.monthLabel }} {{ month.yearNum?.toString() }}
</div>
<div
class="calendar-grid-container"
:class="[isInitialView === true ? 'initial-view' : '', month.monthClass]">
<div class="month-year body-small d-flex align-items-center ps-3 small">
{{ month.monthLabel }} {{ month.yearNum.toString() }}
</div>
<div
v-if="calendarViewDirection === 'future'"
class="legend caption d-flex align-items-center justify-content-end">
<span class="legend-circle me-1"></span> &equals; Available
</div>
<div class="nav-back ps-3"><button></button></div>
<div class="nav-forward pe-3"><button></button></div>
<!-- Do the days of the weeek need to be read? -->
<div class="grid-item caption"><span class="sr-only">Sunday</span>S</div>
<div class="grid-item caption"><span class="sr-only">Monday</span>M</div>
<div class="grid-item caption"><span class="sr-only">Tuesday</span>T</div>
<div class="grid-item caption"><span class="sr-only">Wednesday</span>W</div>
<div class="grid-item caption"><span class="sr-only">Thursday</span>T</div>
<div class="grid-item caption"><span class="sr-only">Friday</span>F</div>
<div class="grid-item caption"><span class="sr-only">Saturday</span>S</div>
<div
v-for="date in month.dates"
:key="`${month.monthLabel}-${date.dateNum.toString()}-${months.length}`"
class="grid-item radio-wrapper"
:class="[
date.dateNum === 1 ? 'first-day-' + month.startDateDayIndex : '',
date.dayClasses,
]">
<input
:disabled="!isSelectableDate(date.inputValue)"
type="radio"
name="day-of-month"
v-model="selectedDate"
:value="date.inputValue"
:id="`${month.monthLabel}-${date.dateNum.toString()}`" />
<label :for="`${month.monthLabel}-${date.dateNum.toString()}`">
<span>{{ date.dateNum.toString() }}</span>
</label>
</div>
v-if="calendarViewDirection === 'future'"
class="legend caption d-flex align-items-center justify-content-end">
<span class="legend-circle me-1"></span> &equals; Available
</div>
<div class="separator-line"></div>
<div class="nav-back ps-3"><button></button></div>
<div class="nav-forward pe-3"><button></button></div>
<!-- TODO Accessibility: Do the days of the week need to be read? -->
<div class="grid-item caption"><span class="sr-only">Sunday</span>S</div>
<div class="grid-item caption"><span class="sr-only">Monday</span>M</div>
<div class="grid-item caption"><span class="sr-only">Tuesday</span>T</div>
<div class="grid-item caption"><span class="sr-only">Wednesday</span>W</div>
<div class="grid-item caption"><span class="sr-only">Thursday</span>T</div>
<div class="grid-item caption"><span class="sr-only">Friday</span>F</div>
<div class="grid-item caption"><span class="sr-only">Saturday</span>S</div>
<div
v-for="date in month.dates"
:key="date.inputValue.dateString"
:id="date.inputValue.dateString"
class="grid-item radio-wrapper"
:class="[
date.dateNum === 1 ? 'first-day-' + month.startDateDayIndex : '',
date.dayClasses,
date.isSelectable ? 'selectable-day' : '',
]">
<input
:disabled="!date.isSelectable"
type="radio"
name="day-of-month"
v-model="selectedDate"
:value="date.inputValue"
:id="`${month.monthLabel}-${date.dateNum.toString()}`" />
<label :for="`${month.monthLabel}-${date.dateNum.toString()}`">
<span>{{ date.dateNum.toString() }}</span>
</label>
</div>
</div>
<loader
:class="[!isLoading ? 'date-picker-hidden' : '']"
loaderColor="blue"
loaderPosition="center" />
</fieldset>
<button
v-if="calendarViewDirection === 'future'"
v-if="calendarViewDirection === 'future' && !disableViewMoreDatesButton"
type="button"
class="btn btn-link"
@click="goForward">
:disabled="isLoading"
@click="showAnotherMonth">
View more dates
</button>
</div>
@ -60,39 +70,35 @@
<script>
// Supporting files
import { monthsOfYear } from "@/constants/scheduling.js";
const SelectableDaysOptions = Object.freeze({
CUSTOM: "custom",
PAST: "past",
});
import loader from "@/ux-components/loader/loader";
import { TIMINGFUNC_MAP, BUFFER_OFFSET, MONTHS_OF_YEAR } from "./mixins/constants";
import { selectableDaysOptions, requiredParameter, forceTwoDigitString } from "./mixins/helpers";
export default {
name: "datePicker",
data() {
return {
calendarData: [],
monthsBeforeToLoadOffset: 0,
monthsAfterToLoadOffset: 1,
isLoading: true,
months: null,
disableViewMoreDatesButton: false,
selectableDatesData: [], // NOTE: uses monthNum (1-based), NOT monthIndex (0-based)
isInitialView: true,
initialViewRowsToShow: 5,
initialViewRowsTally: 0,
today: null,
hideSomeDaysForInitialView: null,
};
},
props: {
selectableDates: {
selectableDatesSetting: {
type: String,
validator(value) {
return Object.values(SelectableDaysOptions).includes(value);
return Object.values(selectableDaysOptions).includes(value);
},
default: SelectableDaysOptions.PAST,
default: selectableDaysOptions.PAST,
},
modelValue: {
type: Object,
},
todayOverrideDateString: {
// only used for unit tests to override today's date
// keep for use in unit tests to override today's date
type: String,
default: null,
},
@ -104,20 +110,29 @@ export default {
},
},
computed: {
todayDate() {
return this.todayOverrideDateString
? new Date(this.todayOverrideDateString)
: new Date();
todayMonthIndex() {
return this.today.getMonth() + 1;
},
months() {
if (this.calendarData?.length < 1) {
this.setCalendarData();
}
return this.calendarData;
todayYearNum() {
return this.today.getFullYear();
},
todayDayIndex() {
return this.today.getDay();
},
todayDateNum() {
return this.today.getDate();
},
currentWeekStartDateNum() {
return this.todayDayIndex >= this.todayDateNum
? 1
: this.todayDateNum - this.todayDayIndex;
},
currentWeekEndDateNum() {
return this.todayDateNum + (6 - this.todayDayIndex);
},
calendarViewDirection() {
if (this.selectableDates === "past") return "past";
if (this.selectableDates === "custom") return "future";
if (this.selectableDatesSetting === "past") return "past";
if (this.selectableDatesSetting === "custom") return "future";
return "none";
},
selectedDate: {
@ -130,260 +145,444 @@ export default {
},
},
methods: {
goForward() {
if (this.isInitialView) {
this.isInitialView = false; // 1st click removes hidden styling
} else {
this.addMonthData(); // 2nd click adds 1 month data
}
getWeekStartDate(date) {
// Get the day of the week for date
let dayOfWeek = date.getDay();
// Subtract the day of the week from date to get the date of Sunday
let sunday = new Date(date);
sunday.setDate(sunday.getDate() - dayOfWeek);
// Return the date of Sunday
return sunday;
},
setCalendarData() {
getWeekEndDate(date) {
const currentDay = date.getDay(); // Get the day of the week (0 = Sunday, 1 = Monday, etc.)
const daysUntilSaturday = 6 - currentDay; // Calculate the number of days until Saturday
// Clone the given date and add the remaining days until Saturday
const saturday = new Date(date);
saturday.setDate(date.getDate() + daysUntilSaturday);
return saturday;
},
getNextWeekSunday(date) {
const currentDay = date.getDay(); // Get the day of the week (0 = Sunday, 1 = Monday, etc.)
const daysUntilNextSunday = currentDay === 0 ? 7 : 7 - currentDay; // Calculate the number of days until the next Sunday
// Clone the given date and add the remaining days until Sunday
const nextSunday = new Date(date);
nextSunday.setDate(date.getDate() + daysUntilNextSunday);
return nextSunday;
},
getInitialViewWeeks(today, initialViewRowsToShow) {
// TODO: this only is for future direction; create logic for past direction
let weeks = [];
let weekStartDate = this.getWeekStartDate(today);
let weekEndDate = this.getWeekEndDate(today);
for (let i = 0; i < initialViewRowsToShow; i++) {
if (i > 0) {
weekStartDate = this.getNextWeekSunday(weekEndDate);
weekEndDate = this.getWeekEndDate(weekStartDate);
}
weeks.push({
weekNum: i + 1,
weekStartDate: weekStartDate,
weekEndDate: weekEndDate,
});
}
return weeks;
},
async loadInitialData(config) {
// CALLED FROM CONSUMING COMPONENT BEFORE DATE-PICKER APPEARS
const todayDate = config.todayOverrideDateString
? new Date(config.todayOverrideDateString)
: new Date();
let todayMonthIndex = todayDate.getMonth() + 1;
let todayYearNum = todayDate.getFullYear();
let currentMonthStart = new Date(todayYearNum, todayMonthIndex - 1, 1);
let currentMonthEnd = new Date(todayYearNum, todayMonthIndex, 0);
let calendarViewDirection = "none";
if (config.selectableDatesSetting === "past") calendarViewDirection = "past";
if (config.selectableDatesSetting === "custom") calendarViewDirection = "future";
const initialViewWeeks = this.getInitialViewWeeks(
todayDate,
config.initialViewRowsToShow
);
let initialViewStartDate = todayDate;
let initialViewEndDate = initialViewWeeks[initialViewWeeks.length - 1].weekEndDate;
let saturday1month = initialViewWeeks[0].weekEndDate.getMonth();
let sunday5month =
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") {
if (saturday1month !== sunday5month) {
hideSomeDaysForInitialView = true;
}
if (initialViewStartDate.getMonth() === sunday5month) {
hideSecondMonth = true;
if (currentMonthEnd > initialViewEndDate) {
// should part of 1st month be hidden?
hideSomeDaysForInitialView = true;
}
}
}
let myPromise = new Promise((resolve, reject) => {
const response = config.customSelectableDatesCallback(
initialViewStartDate,
initialViewEndDate
);
resolve(response);
});
return myPromise.then((response) => {
const initialData = {
todayDate: todayDate,
initialViewStartDate: initialViewStartDate,
initialViewEndDate: initialViewEndDate,
calendarViewDirection: calendarViewDirection,
initialShopTimeSlotsResponse: response,
hideSomeDaysForInitialView: hideSomeDaysForInitialView,
hideSecondMonth: hideSecondMonth,
};
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);
let percentageNew = timingFunc(time);
let distanceToGo = targetY;
let 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.today = config.todayDate;
this.hideSomeDaysForInitialView = config.hideSomeDaysForInitialView;
let hideSecondMonth = config.hideSecondMonth;
const direction = config.calendarViewDirection;
const monthsAfterToLoadOffset = 12; // TO BE MADE "CONSTANTS"
const monthsBeforeToLoadOffset = 36; // TO BE MADE "CONSTANTS"
config.initialShopTimeSlotsResponse.forEach((selectableDate) => {
this.selectableDatesData.push(selectableDate);
});
// GENERATE MONTHS AND PUSH THEM INTO ARRAY
const months = [];
if (this.calendarViewDirection === "future") {
const options = {
calendarViewDirection: direction,
monthsBeforeToLoadOffset: monthsBeforeToLoadOffset,
monthsAfterToLoadOffset: monthsAfterToLoadOffset,
initialViewStartDate: config.initialViewStartDate,
initialViewEndDate: config.initialViewEndDate,
hideSecondMonth: hideSecondMonth,
};
if (direction === "future") {
// first 0, then 1
for (let i = 0; i <= this.monthsAfterToLoadOffset; i++) {
months.push(this.getMonthData(i));
for (let i = 0; i <= monthsAfterToLoadOffset; i++) {
months.push(await this.getMonthData(i, options));
}
} else if (this.calendarViewDirection === "past") {
} else if (direction === "past") {
// first 0, then -1
for (let i = 0; i >= 0 - this.monthsAfterToLoadOffset; i--) {
months.unshift(this.getMonthData(i));
for (let i = 0; i >= 0 - monthsBeforeToLoadOffset; i--) {
months.unshift(this.getMonthData(i, options));
}
} else {
// TK - IF A CALENDAR WITH BOTH PAST AND FUTURE WAS EVER NEEDED
// for (let i = this.monthsAfterToLoadOffset; i >= this.monthsBeforeToLoadOffset; i--) {
// TODO - IF A CALENDAR WITH BOTH PAST AND FUTURE WAS EVER NEEDED
// for (let i = monthsAfterToLoadOffset; i >= monthsBeforeToLoadOffset; i--) {
// months.push(this.getMonthDataPAST(i));
// }
}
this.calendarData = months;
this.months = months;
this.isLoading = false;
},
getMonthData(offset) {
const direction = this.calendarViewDirection; // "past" or "future"
let yearNum;
let monthIndex;
async getMonthData(offset = requiredParameter(), options) {
/* options will contain:
calendarViewDirection (string)
initialViewStartDateNum (number)
initialViewEndDateNum (number)
monthsBeforeToLoadOffset (number),
monthsAfterToLoadOffset (number),
hideSecondMonth (boolean),
data used:
todayDate (date object)
this.hideSomeDaysForInitialView (string)
*/
let monthIndex = this.todayMonthIndex + offset; // mutable
let yearNum = this.todayYearNum; // mutable
const calendarViewDirection = options.calendarViewDirection;
const initialViewStartDate = options.initialViewStartDate;
const initialViewEndDate = options.initialViewEndDate;
const hideSecondMonth = options.hideSecondMonth; // <<<<<<<<<<<<<
const dates = [];
let monthClass = "";
let initialViewEndDate; // only used for setCalendarData FUTURE
let initialViewStartDate; // only used for setCalendarData PAST
let currentWeekEndDateNum; // only used for setCalendarData PAST
let currentWeekStartDateNum; // only used for setCalendarData FUTURE
let firstMonthDayTally; // only used for setCalendarData on 1st month generated
const datesArray = [];
const todayDateNum = this.todayDate.getDate();
let isMonthThatHidesSomeDaysForInitialView;
if (typeof offset !== "undefined") {
// offset only passed during initial setup with setCalendarData
yearNum = this.todayDate.getFullYear();
monthIndex = this.todayDate.getMonth() + offset;
if (direction === "future" && offset > 0) {
while (monthIndex > 11) {
monthIndex = monthIndex - 12;
yearNum++;
}
} else if (direction === "past" && offset < 0) {
while (monthIndex < 0) {
monthIndex = 12 + monthIndex;
yearNum--;
}
if (calendarViewDirection === "future" && offset > 0) {
while (monthIndex > 12) {
monthIndex = monthIndex - 12;
yearNum++;
}
} else {
// used only when adding an additional month
if (direction === "future") {
// get last day of current calendar data
const lastMonthInCalendarData = this.calendarData[this.calendarData.length - 1];
if (lastMonthInCalendarData.monthIndex === 11) {
monthIndex = 0;
yearNum = lastMonthInCalendarData.yearNum + 1;
} else {
monthIndex = lastMonthInCalendarData.monthIndex + 1;
yearNum = lastMonthInCalendarData.yearNum;
}
}
if (direction === "past") {
// get last day of current calendar data
const firstMonthInCalendarData = this.calendarData[0];
if (firstMonthInCalendarData.monthIndex === 0) {
monthIndex = 11;
yearNum = firstMonthInCalendarData.yearNum - 1;
} else {
monthIndex = firstMonthInCalendarData.monthIndex - 1;
yearNum = firstMonthInCalendarData.yearNum;
}
} else if (calendarViewDirection === "past" && offset < 0) {
while (monthIndex < 1) {
monthIndex = 12 + monthIndex;
yearNum--;
}
}
const todayDayIndex = this.todayDate.getDay(); // get day of week index of today (0-6)
currentWeekStartDateNum =
todayDayIndex > todayDateNum ? 1 : todayDateNum - todayDayIndex; // FUTURE
const monthEndDate = new Date(yearNum, monthIndex + 1, 0); // BOTH
const monthEndDate = new Date(yearNum, monthIndex, 0); // BOTH
let monthEndDateNum = monthEndDate.getDate(); // BOTH
currentWeekEndDateNum = todayDateNum + 6 - todayDayIndex; // PAST, aka Sat. (ok if it's larger than the month end?)
if (offset === 0 && direction === "past" && monthEndDateNum > currentWeekEndDateNum)
monthEndDateNum = currentWeekEndDateNum; // PAST
if (
offset === 0 &&
calendarViewDirection === "past" &&
monthEndDateNum > this.currentWeekEndDateNum
) {
monthEndDateNum = this.currentWeekEndDateNum; // PAST
}
const monthStartDateNum =
offset === 0 && direction === "future" ? currentWeekStartDateNum : 1; // FUTURE
const monthStartDate = new Date(yearNum, monthIndex, monthStartDateNum); // BOTH
offset === 0 && calendarViewDirection === "future"
? this.currentWeekStartDateNum
: 1; // FUTURE
const monthStartDate = new Date(yearNum, monthIndex - 1, monthStartDateNum); // BOTH
const startDateDayIndex = monthStartDate.getDay(); // FUTURE
const endDateDayIndex = monthEndDate.getDay(); // PAST
if (typeof offset !== "undefined") {
if (offset === 0 && direction === "future")
firstMonthDayTally = monthStartDateNum - startDateDayIndex; // FUTURE (starts low, counts up)
if (offset === 0 && direction === "past")
firstMonthDayTally = currentWeekEndDateNum; // PAST (starts high, counts down)
while (
direction === "future" &&
offset === 0 &&
firstMonthDayTally < monthEndDateNum
) {
// FUTURE
firstMonthDayTally = firstMonthDayTally + 7;
this.initialViewRowsTally++;
}
while (
direction === "past" &&
offset === 0 &&
firstMonthDayTally > monthStartDateNum
) {
// PAST
firstMonthDayTally = firstMonthDayTally - 7;
this.initialViewRowsTally++;
}
if (Math.abs(offset) === 1) {
if (this.initialViewRowsTally < this.initialViewRowsToShow) {
initialViewEndDate = 6 - startDateDayIndex + monthStartDateNum; // FUTURE
initialViewStartDate = monthEndDateNum - endDateDayIndex; // PAST
this.initialViewRowsTally++;
} else {
monthClass = monthClass + " month-hidden";
}
while (this.initialViewRowsTally < this.initialViewRowsToShow) {
initialViewEndDate = initialViewEndDate + 7;
initialViewStartDate = initialViewStartDate - 7;
this.initialViewRowsTally++;
}
}
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"
) {
monthClass = monthClass + " last-available-month";
}
if (
Math.abs(offset) === options.monthsBeforeToLoadOffset &&
calendarViewDirection === "past"
) {
// TODO - re-check this logic if past direction
monthClass = monthClass + " last-available-month";
}
if (this.selectableDates === "custom") {
const monthStart = {
year: yearNum,
month: monthIndex + 1,
date: offset === 0 ? todayDateNum : monthStartDateNum,
};
const monthEnd = {
year: monthEndDate.getFullYear(),
month: monthEndDate.getMonth() + 1,
date: monthEndDateNum,
};
// get available dates
this.updateSelectableDates(monthStart, monthEnd);
}
// populate datesArray
// populate dates array
for (let i = monthStartDateNum; i <= monthEndDateNum; i++) {
let dayClasses = "";
let dateString =
yearNum.toString() +
"-" +
forceTwoDigitString(monthIndex) +
"-" +
forceTwoDigitString(i);
const thisDate = {
// NOTE: uses monthNum (1-based), NOT monthIndex (0-based)
year: yearNum,
month: monthIndex + 1,
month: monthIndex,
date: i,
dateString: dateString,
};
if (offset === 0 && i === todayDateNum) {
if (offset === 0 && i === this.todayDateNum) {
dayClasses += "current-day";
}
if (offset === 0 && i < todayDateNum && direction === "future") {
if (offset === 0 && i < this.todayDateNum && calendarViewDirection === "future") {
dayClasses += "unavailable-day";
}
if (offset === 0 && i > todayDateNum && direction === "past") {
if (offset === 0 && i > this.todayDateNum && calendarViewDirection === "past") {
dayClasses += "unavailable-day";
}
if (Math.abs(offset) === 1 && direction === "future" && i > initialViewEndDate) {
if (
this.hideSomeDaysForInitialView &&
initialViewEndDate.getMonth() + 1 === monthIndex &&
initialViewEndDate.getDate() < i
) {
dayClasses += "day-hidden";
}
if (Math.abs(offset) === 1 && direction === "past" && i < initialViewStartDate) {
dayClasses += "day-hidden";
}
if (this.selectableDates === "custom" && this.isSelectableDate(thisDate)) {
dayClasses += " selectable-day";
isMonthThatHidesSomeDaysForInitialView = true;
}
const dateObject = {
dateNum: i,
dayClasses: dayClasses,
inputValue: thisDate,
isSelectable:
this.selectableDatesData.findIndex(
(date) => date.dateString === dateString
) > -1
? true
: false,
};
datesArray.push(dateObject);
dates.push(dateObject);
}
const monthToAdd = {
monthLabel: monthsOfYear[monthIndex],
monthLabel: MONTHS_OF_YEAR[monthIndex - 1],
monthIndex: monthIndex,
monthString: MONTHS_OF_YEAR[monthIndex - 1] + "-" + yearNum?.toString(),
yearNum: yearNum,
dates: datesArray,
dates: dates,
startDateDayIndex: startDateDayIndex,
monthClass: monthClass,
isMonthThatHidesSomeDaysForInitialView: isMonthThatHidesSomeDaysForInitialView,
};
return monthToAdd;
},
addMonthData() {
const monthToAdd = this.getMonthData();
if (this.calendarViewDirection === "future") {
this.calendarData.push(monthToAdd);
}
if (this.calendarViewDirection === "past") {
this.calendarData.unshift(monthToAdd);
}
},
isSelectableDate(thisDate) {
const testForDate = (dateInArray) => {
return (
dateInArray.date === thisDate.date &&
dateInArray.month === thisDate.month &&
dateInArray.year === thisDate.year
async showAnotherMonth() {
this.isLoading = true;
let monthToShow;
let monthStartDateNum = 0;
if (this.hideSomeDaysForInitialView) {
monthToShow = this.months.find(
({ isMonthThatHidesSomeDaysForInitialView }) =>
isMonthThatHidesSomeDaysForInitialView
);
};
const isSelectable =
this.selectableDatesData.findIndex(testForDate) > -1 ? true : false;
return isSelectable;
// find the first day-hidden to become the next api call start date
monthStartDateNum =
monthToShow.dates.find(({ dayClasses }) => dayClasses.includes("day-hidden"))
.dateNum - 1; // TRY 2
} else {
if (this.calendarViewDirection === "future") {
monthToShow = this.months.find((month) =>
month.monthClass.includes("month-hidden")
);
}
if (this.calendarViewDirection === "past") {
// TODO: UPDATE THIS WITH CORRECT PAST LOOKING LOGIC
monthToShow = this.months.find((month) =>
month.monthClass.includes("month-hidden")
);
}
}
if (monthToShow) {
// make new API call with this month's start and end dates
await this.updateSelectableDates(
monthToShow.dates[monthStartDateNum].inputValue.dateString,
monthToShow.dates[monthToShow.dates.length - 1].inputValue.dateString
);
this.isLoading = false;
this.hideSomeDaysForInitialView = false; // if hid days on initial partial view, this removes hidden styling on days
monthToShow.monthClass = monthToShow.monthClass.replace(" month-hidden", "");
this.scrollToElement(monthToShow.monthString);
if (monthToShow.monthClass.includes("last-available-month"))
this.disableViewMoreDatesButton = true;
}
},
updateSelectableDates(monthStart, monthEnd) {
this.customSelectableDatesCallback(monthStart, monthEnd)?.forEach((newObj) => {
async updateSelectableDates(monthStart, monthEnd) {
const moreSelectableDates = await this.customSelectableDatesCallback(
monthStart,
monthEnd
);
moreSelectableDates.forEach((selectableDate) => {
const index = this.selectableDatesData.findIndex(
(obj) =>
obj.year === newObj.year &&
obj.month === newObj.month &&
obj.date === newObj.date
(obj) => obj.dateString === selectableDate.dateString
);
if (index === -1) this.selectableDatesData.push(newObj);
if (index === -1) this.selectableDatesData.push(selectableDate);
this.months.forEach((month) => {
// TODO: avoid checking all calendar date; maybe only ones between monthStart and monthEnd as defined above?
month.dates.forEach((date) => {
if (date.inputValue.dateString === selectableDate.dateString) {
date["isSelectable"] = true;
}
});
});
});
},
},
components: {
loader,
},
};
</script>
<style lang="scss" scoped>
.date-picker-hidden {
opacity: 0;
max-height: 0;
}
.date-picker {
overflow: hidden;
position: relative;
height: 100%;
fieldset {
overflow-y: auto;
height: 88%;
position: relative;
}
.loader {
position: absolute;
height: 2rem;
width: 2rem;
&::after {
width: 100%;
height: 100%;
}
}
.calendar-grid-container {
margin: 0 auto;
margin: 0 auto 2rem auto;
max-width: 414px;
transition: height ease 2s, opacity ease 2s;
display: grid;
grid-template-columns: repeat(7, 1fr);
justify-content: center;
align-items: center;
padding: 4px;
padding: 0 0.75rem;
opacity: 1;
.grid-item {
text-align: center;
margin: 10%;
margin: 10px 3px;
&.first-day-,
&.first-day-0 {
@ -409,6 +608,12 @@ export default {
}
}
.separator-line {
grid-area: 2/1/2/8;
border-top: 1px solid $gray-500;
margin: 0.75rem 0;
}
.month-year {
grid-area: 1 / 1 / 2 / 5;
text-transform: uppercase;
@ -436,6 +641,9 @@ export default {
justify-content: center;
align-items: center;
outline: none;
height: 1.35rem;
opacity: 1;
transition: height ease 250ms, opacity ease 250ms;
input[type="radio"] {
position: absolute; //override bootstrap
@ -495,9 +703,9 @@ export default {
display: flex;
justify-content: center;
align-items: center;
width: 40px;
height: 40px;
min-width: 40px;
width: 36px;
height: 36px;
min-width: 36px;
border-radius: 50%;
span {
@ -567,19 +775,37 @@ export default {
}
}
&.initial-view {
&.month-hidden {
display: none;
}
&.partial-month-initial-view {
.day-hidden {
display: none;
opacity: 0;
overflow: hidden;
display: flex;
margin: 0;
max-height: 0;
}
}
&.month-hidden {
opacity: 0;
max-height: 0;
margin-bottom: 0;
}
&.last-available-month:not(&.month-hidden) {
margin-bottom: 14rem;
}
}
#bottom-spacer {
height: 20rem;
background: lightblue;
}
.btn-link {
font-weight: 500;
text-underline-offset: 4px;
box-shadow: none !important; // TODO: FIX THIS
position: absolute;
top: 90%;
}
.past {
.calendar-grid-container {
.month-year {
@ -637,4 +863,16 @@ export default {
}
}
}
.btn-link {
display: block;
position: relative;
height: 3rem;
width: 100%;
justify-content: center;
background: transparent;
border: none;
font-weight: 500;
text-underline-offset: 4px;
z-index: 2;
}
</style>

View file

@ -0,0 +1,24 @@
const TIMINGFUNC_MAP = {
linear: (t) => t,
"ease-in": (t) => t * t,
"ease-out": (t) => t * (2 - t),
"ease-in-out": (t) => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t),
};
const BUFFER_OFFSET = 10;
const MONTHS_OF_YEAR = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
export { TIMINGFUNC_MAP, BUFFER_OFFSET, MONTHS_OF_YEAR };

View file

@ -0,0 +1,15 @@
const selectableDaysOptions = Object.freeze({
CUSTOM: "custom",
PAST: "past",
});
const requiredParameter = () => {
throw new Error("parameter is required");
};
const forceTwoDigitString = (monthNum) => {
const newString = monthNum.toString();
return newString.length === 1 ? "0" + newString : newString;
};
export { selectableDaysOptions, requiredParameter, forceTwoDigitString };

View file

@ -60,17 +60,24 @@ export default {
},
/* istanbul ignore next */
callMockHttpClient({ method, endpoint }) {
callMockHttpClient({ method, endpoint, payload }) {
// For Mock use only!
return new Promise((resolve, reject) => {
axios({
method: method,
url: endpoint,
crossDomain: true,
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
responseType: {},
data: payload,
}).then(
(response) => {
resolve(response);
// simulate a delayed response
setTimeout(() => {
resolve(response);
}, 2000);
},
(error) => {
return reject(error.response);

View file

@ -1,8 +1,8 @@
<template>
<div class="page-container-grouped-styles">
<funnelHeader cmsWidgetName="FunnelHeaderWidget" />
<div class="select-car">
<div class="select-car-form rounded text-center">
<div class="">
<div class="rounded text-center">
<div class="fade-on-route-transition">
<date-picker
selectableDates="custom"
@ -50,6 +50,7 @@ export default {
{ year: 2023, month: 3, date: 5 },
{ year: 2023, month: 3, date: 22 },
{ year: 2023, month: 4, date: 13 },
{ year: 2023, month: 4, date: 7 },
{ year: 2023, month: 4, date: 14 },
{ year: 2023, month: 4, date: 26 },
{ year: 2023, month: 5, date: 21 },
@ -81,3 +82,9 @@ export default {
},
};
</script>
<style lang="scss">
// .page-container-grouped-styles {
// padding: 0 .5rem !important;
// }
</style>

View file

@ -30,9 +30,10 @@
</template>
<date-picker
selectableDates="custom"
selectableDatesSetting="custom"
ref="datePicker"
v-model="selectedDate"
:customSelectableDatesCallback="getAvailableDates" />
:customSelectableDatesCallback="getAvailableDatesMethod" />
<funnel-footer
cmsWidgetName="FunnelFooterWidget"
ref="funnelFooter"
@ -55,6 +56,8 @@ import datePicker from "@/digital-components/date-picker/date-picker";
// Supporting files
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import baseMixin from "@/mixins/base-mixin.js";
import { storeActions } from "@/constants/store-actions";
import { settleAllPromises } from "@/helpers/layout-helper";
import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
import { getAlertReasons } from "@/layouts/schedule/helpers/schedule-helper";
@ -70,29 +73,67 @@ import store from "@/store";
defineRule("date-required", required(errorMessages.DATE_REQUIRED));
const getAvailableDates = async (startDate, endDate) => {
// USING DATES PASSED, MAKE AN API CALL
const newShopTimeSlotsResponse = await baseMixin.methods.dispatchStoreAction(
storeActions.GET_SHOP_TIME_SLOTS,
{
startDate: startDate,
endDate: endDate,
shopAppointmentType: "mobile", // TODO - PASS THIS IN FROM PREVIOUS PAGE?
},
false
);
const newShopTimeSlots = newShopTimeSlotsResponse.data;
return convertApiResponse(newShopTimeSlots.days);
};
const convertApiResponse = (responseData) => {
// DATA CONVERSION
responseData?.forEach((date) => {
const dateString = date.date;
date.dateString = dateString;
const dateStringPieces = dateString.split("-");
date.year = Number(dateStringPieces[0]);
date.month = Number(dateStringPieces[1]);
date.date = Number(dateStringPieces[2]);
});
return responseData;
};
export default {
name: "schedule",
data() {
return {
selectedDate: null,
selectableDatesData: [],
weatherAlerts: [],
mockSelectableDatesData: [
{ year: 2023, month: 4, date: 13 },
{ year: 2023, month: 4, date: 14 },
{ year: 2023, month: 4, date: 26 },
{ year: 2023, month: 5, date: 4 },
{ year: 2023, month: 5, date: 5 },
{ year: 2023, month: 5, date: 14 },
{ year: 2023, month: 5, date: 21 },
{ year: 2023, month: 5, date: 23 },
{ year: 2023, month: 5, date: 25 },
{ year: 2023, month: 6, date: 11 },
],
};
},
async beforeRouteEnter(to, from, next) {
// Call APIs
const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage);
const datePickerInitialDataPromise = datePicker.methods.loadInitialData({
// setup config options for date-picker
selectableDatesSetting: "custom",
initialViewRowsToShow: 5,
customSelectableDatesCallback: getAvailableDates,
/* vvvvv SAVE THESE FOR TESTING PURPOSES FOR NOW vvvvv
// todayOverrideDateString: "2023-04-29T03:00:00", // show partial
// todayOverrideDateString: "2023-04-30T03:00:00", //
// todayOverrideDateString: "2023-05-02T03:00:00", // ONE MONTH ONLY
// todayOverrideDateString: "2023-05-06T03:00:00", // ONE MONTH ONLY
// todayOverrideDateString: "2023-05-07T03:00:00", // show partial
// todayOverrideDateString: "2023-05-30T03:00:00", //
// todayOverrideDateString: "2023-06-30T03:00:00", //
// todayOverrideDateString: "2023-07-01T03:00:00", // show partial && ONE MONTH ONLY
// todayOverrideDateString: "2023-07-02T03:00:00", // ONE MONTH ONLY
// todayOverrideDateString: "2023-07-12T03:00:00", // show partial
// todayOverrideDateString: "2023-08-31T03:00:00",
// todayOverrideDateString: "2023-09-30T03:00:00", // show partial
*/
});
const alertReasonsPromise = getAlertReasons(store.getters.order.serviceLocation.zipCodeCtu);
@ -106,6 +147,10 @@ export default {
resultKey: "alertReasons",
promise: alertReasonsPromise,
},
{
resultKey: "datePickerInitialData",
promise: datePickerInitialDataPromise,
},
];
const resultMap = await settleAllPromises(promiseResultMap);
@ -114,6 +159,7 @@ export default {
next((vm) => {
vm.setCmsContent(resultMap.cmsContent);
vm.setData(resultMap.alertReasons);
vm.$refs.datePicker.initializeComponent(resultMap.datePickerInitialData);
});
},
computed: {
@ -137,6 +183,22 @@ export default {
return true;
// NEED TODO - WHAT ARE PAGE REQ'S FOR THIS PAGE?
},
async getAvailableDatesMethod(startDate, endDate) {
/* TODO - DO WE NEED TO KEEP AN AGGREGATE OF ALL DATES RETURNED
FOR TIMESLOTS in this.selectableDatesData? IS selectableDatesData EVEN NEEDED?
// // ADD API CALL RESULTS TO EXISTING DATE DATA
// this.selectableDatesData = this.selectableDatesData.concat(
// this.convertApiResponse(newShopTimeSlots.days)
// );
// // console.log("this.selectableDatesData is now: ", this.selectableDatesData);
// // RETURN AGGREGATE DATE DATA
// return this.selectableDatesData;
*/
return await getAvailableDates(startDate, endDate);
},
setData(alertReasonsData) {
if (alertReasonsData) {
this.convertReasonsToCmsAlerts(alertReasonsData);
@ -165,9 +227,6 @@ export default {
console.log("error fetching alert reasons..");
});
},
getAvailableDates(startDate, endDate) {
return this.mockSelectableDatesData;
},
getServiceZipCtuCodeFromStore() {
return store.getters.order.serviceLocation.zipCodeCtu;
},
@ -190,6 +249,13 @@ export default {
navigateToHeritageFunnel({ loadingModal: this.$refs.loadingModal });
},
},
watch: {
selectedDate(date) {
/* EXAMPLE RETURNED date OBJECT:
{ "year": 2023, "month": 5, "date": 25, "dateString": "2023-05-25" }
*/
},
},
components: {
alert,
funnelHeader,
@ -202,5 +268,3 @@ export default {
mounted() {},
};
</script>
<style lang="scss"></style>

View file

@ -1116,6 +1116,41 @@ export const actions = {
});
},
getShopTimeSlots(
context,
{ startDate = "2023-01-01", endDate = "2023-05-01", shopAppointmentType = "" }
) {
const order = context.state.order;
const mockArray = [];
var payload = {
providerNumber: order.providerNumber,
startDate: startDate,
endDate: endDate,
shopAppointmentType: shopAppointmentType,
applicationName: applicationConfig.APPLICATION_NAME,
parentAccountNumber: context.getters.payment.parentAccountNumber,
carId: context.getters.vehicle.carId,
partNumbers: mockArray, // TODO: WHERE DO I GET THIS?
eon: order.eon,
provisionalReasons: mockArray, // TODO: WHERE DO I GET THIS?
};
// TODO: REMOVE MOCK CALL & USE REAL CALL BELOW
return globalMethods.callMockHttpClient({
method: endpoints.GetShopTimeSlots.method,
endpoint: endpoints.GetShopTimeSlots.mockUrl,
payload: payload,
});
// TODO: RESTORE THIS
// return globalMethods.callHttpClient({
// method: endpoints.GetShopTimeSlots.method,
// endpoint: endpoints.GetShopTimeSlots.url,
// payload: payload,
// logApiCall: false,
// });
},
// Session API Actions
saveSession(context) {
const vehicle = context.getters.vehicle;