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/vehicle-damage/windshield-options/windshield-options.vue",
"!src/layouts/reveal/**/*.vue", "!src/layouts/reveal/**/*.vue",
"!src/ux-components/text-link/**/*.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/layouts/vin-lookup/**/*.vue", //Temporary for Quote page testing
"!src/common-components/funnel-header/menu-modal/**/*.vue", "!src/common-components/funnel-header/menu-modal/**/*.vue",
"!src/layouts/schedule/*.vue", // Temp test exclusion while in development "!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", url: "/parts/api/v1/parts/part-from-capability-answer",
method: "POST", 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: { SaveSession: {
url: "/order/api/v1/order/save-session", url: "/order/api/v1/order/save-session",
method: "POST", 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_MOLDING_QUESTIONS: "getMoldingQuestions",
GET_MOBILE_FEE_PART: "getMobileFeePart", GET_MOBILE_FEE_PART: "getMobileFeePart",
GET_SERVICEABILITY_DETAILS: "getServiceabilityDetails", GET_SERVICEABILITY_DETAILS: "getServiceabilityDetails",
GET_SHOP_TIME_SLOTS: "getShopTimeSlots",
GET_PROVIDERS: "getProviders", GET_PROVIDERS: "getProviders",
SAVE_SESSION: "saveSession", SAVE_SESSION: "saveSession",
LOAD_SESSION: "loadSession", 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("date-picker.vue", () => {
describe("initial setup", () => { describe("initial setup", () => {
test("Creates a date object from today", () => { test("Creates a date object from today", async () => {
// Arrange // Arrange
const { wrapper } = setupMocks({}); const { wrapper } = await setupMocks({
propsData: {
selectableDatesSetting: "custom"
},
});
// Act wrapper.vm.loadInitialData = jest.fn().mockImplementation(
const testResult = wrapper.vm.todayDate instanceof Date; () =>
new Promise((resolve, reject) => {
resolve({
data: [responseValue],
});
})
);
// Assert await wrapper.vm.$nextTick();
expect(testResult).toEqual(true); 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);
wrapper.unmount(); wrapper.unmount();
}); });

View file

@ -1,58 +1,68 @@
<template> <template>
<div class="date-picker text-center" :class="calendarViewDirection"> <div class="date-picker text-center" :class="calendarViewDirection">
<fieldset v-if="months"> <fieldset id="date-picker-fieldset" ref="datePickerFieldset">
<legend class="sr-only">Date Picker</legend> <legend class="sr-only">Select a day and time</legend>
<div <div
v-for="month in months" 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 <div
class="calendar-grid-container" v-if="calendarViewDirection === 'future'"
:class="[isInitialView === true ? 'initial-view' : '', month.monthClass]"> class="legend caption d-flex align-items-center justify-content-end">
<div class="month-year body-small d-flex align-items-center ps-3 small"> <span class="legend-circle me-1"></span> &equals; Available
{{ month.monthLabel }} {{ month.yearNum.toString() }} </div>
</div> <div class="separator-line"></div>
<div <div class="nav-back ps-3"><button></button></div>
v-if="calendarViewDirection === 'future'" <div class="nav-forward pe-3"><button></button></div>
class="legend caption d-flex align-items-center justify-content-end"> <!-- TODO Accessibility: Do the days of the week need to be read? -->
<span class="legend-circle me-1"></span> &equals; Available <div class="grid-item caption"><span class="sr-only">Sunday</span>S</div>
</div> <div class="grid-item caption"><span class="sr-only">Monday</span>M</div>
<div class="nav-back ps-3"><button></button></div> <div class="grid-item caption"><span class="sr-only">Tuesday</span>T</div>
<div class="nav-forward pe-3"><button></button></div> <div class="grid-item caption"><span class="sr-only">Wednesday</span>W</div>
<!-- Do the days of the weeek need to be read? --> <div class="grid-item caption"><span class="sr-only">Thursday</span>T</div>
<div class="grid-item caption"><span class="sr-only">Sunday</span>S</div> <div class="grid-item caption"><span class="sr-only">Friday</span>F</div>
<div class="grid-item caption"><span class="sr-only">Monday</span>M</div> <div class="grid-item caption"><span class="sr-only">Saturday</span>S</div>
<div class="grid-item caption"><span class="sr-only">Tuesday</span>T</div> <div
<div class="grid-item caption"><span class="sr-only">Wednesday</span>W</div> v-for="date in month.dates"
<div class="grid-item caption"><span class="sr-only">Thursday</span>T</div> :key="date.inputValue.dateString"
<div class="grid-item caption"><span class="sr-only">Friday</span>F</div> :id="date.inputValue.dateString"
<div class="grid-item caption"><span class="sr-only">Saturday</span>S</div> class="grid-item radio-wrapper"
<div :class="[
v-for="date in month.dates" date.dateNum === 1 ? 'first-day-' + month.startDateDayIndex : '',
:key="`${month.monthLabel}-${date.dateNum.toString()}-${months.length}`" date.dayClasses,
class="grid-item radio-wrapper" date.isSelectable ? 'selectable-day' : '',
:class="[ ]">
date.dateNum === 1 ? 'first-day-' + month.startDateDayIndex : '', <input
date.dayClasses, :disabled="!date.isSelectable"
]"> type="radio"
<input name="day-of-month"
:disabled="!isSelectableDate(date.inputValue)" v-model="selectedDate"
type="radio" :value="date.inputValue"
name="day-of-month" :id="`${month.monthLabel}-${date.dateNum.toString()}`" />
v-model="selectedDate" <label :for="`${month.monthLabel}-${date.dateNum.toString()}`">
:value="date.inputValue" <span>{{ date.dateNum.toString() }}</span>
:id="`${month.monthLabel}-${date.dateNum.toString()}`" /> </label>
<label :for="`${month.monthLabel}-${date.dateNum.toString()}`">
<span>{{ date.dateNum.toString() }}</span>
</label>
</div>
</div> </div>
</div> </div>
<loader
:class="[!isLoading ? 'date-picker-hidden' : '']"
loaderColor="blue"
loaderPosition="center" />
</fieldset> </fieldset>
<button <button
v-if="calendarViewDirection === 'future'" v-if="calendarViewDirection === 'future' && !disableViewMoreDatesButton"
type="button" type="button"
class="btn btn-link" class="btn btn-link"
@click="goForward"> :disabled="isLoading"
@click="showAnotherMonth">
View more dates View more dates
</button> </button>
</div> </div>
@ -60,39 +70,35 @@
<script> <script>
// Supporting files // Supporting files
import { monthsOfYear } from "@/constants/scheduling.js"; import loader from "@/ux-components/loader/loader";
import { TIMINGFUNC_MAP, BUFFER_OFFSET, MONTHS_OF_YEAR } from "./mixins/constants";
const SelectableDaysOptions = Object.freeze({ import { selectableDaysOptions, requiredParameter, forceTwoDigitString } from "./mixins/helpers";
CUSTOM: "custom",
PAST: "past",
});
export default { export default {
name: "datePicker", name: "datePicker",
data() { data() {
return { return {
calendarData: [], isLoading: true,
monthsBeforeToLoadOffset: 0, months: null,
monthsAfterToLoadOffset: 1, disableViewMoreDatesButton: false,
selectableDatesData: [], // NOTE: uses monthNum (1-based), NOT monthIndex (0-based) selectableDatesData: [], // NOTE: uses monthNum (1-based), NOT monthIndex (0-based)
isInitialView: true, today: null,
initialViewRowsToShow: 5, hideSomeDaysForInitialView: null,
initialViewRowsTally: 0,
}; };
}, },
props: { props: {
selectableDates: { selectableDatesSetting: {
type: String, type: String,
validator(value) { validator(value) {
return Object.values(SelectableDaysOptions).includes(value); return Object.values(selectableDaysOptions).includes(value);
}, },
default: SelectableDaysOptions.PAST, default: selectableDaysOptions.PAST,
}, },
modelValue: { modelValue: {
type: Object, type: Object,
}, },
todayOverrideDateString: { todayOverrideDateString: {
// only used for unit tests to override today's date // keep for use in unit tests to override today's date
type: String, type: String,
default: null, default: null,
}, },
@ -104,20 +110,29 @@ export default {
}, },
}, },
computed: { computed: {
todayDate() { todayMonthIndex() {
return this.todayOverrideDateString return this.today.getMonth() + 1;
? new Date(this.todayOverrideDateString)
: new Date();
}, },
months() { todayYearNum() {
if (this.calendarData?.length < 1) { return this.today.getFullYear();
this.setCalendarData(); },
} todayDayIndex() {
return this.calendarData; 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() { calendarViewDirection() {
if (this.selectableDates === "past") return "past"; if (this.selectableDatesSetting === "past") return "past";
if (this.selectableDates === "custom") return "future"; if (this.selectableDatesSetting === "custom") return "future";
return "none"; return "none";
}, },
selectedDate: { selectedDate: {
@ -130,260 +145,444 @@ export default {
}, },
}, },
methods: { methods: {
goForward() { getWeekStartDate(date) {
if (this.isInitialView) { // Get the day of the week for date
this.isInitialView = false; // 1st click removes hidden styling let dayOfWeek = date.getDay();
} else {
this.addMonthData(); // 2nd click adds 1 month data // 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 // GENERATE MONTHS AND PUSH THEM INTO ARRAY
const months = []; 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 // first 0, then 1
for (let i = 0; i <= this.monthsAfterToLoadOffset; i++) { for (let i = 0; i <= monthsAfterToLoadOffset; i++) {
months.push(this.getMonthData(i)); months.push(await this.getMonthData(i, options));
} }
} else if (this.calendarViewDirection === "past") { } else if (direction === "past") {
// first 0, then -1 // first 0, then -1
for (let i = 0; i >= 0 - this.monthsAfterToLoadOffset; i--) { for (let i = 0; i >= 0 - monthsBeforeToLoadOffset; i--) {
months.unshift(this.getMonthData(i)); months.unshift(this.getMonthData(i, options));
} }
} else { } else {
// TK - IF A CALENDAR WITH BOTH PAST AND FUTURE WAS EVER NEEDED // TODO - IF A CALENDAR WITH BOTH PAST AND FUTURE WAS EVER NEEDED
// for (let i = this.monthsAfterToLoadOffset; i >= this.monthsBeforeToLoadOffset; i--) { // for (let i = monthsAfterToLoadOffset; i >= monthsBeforeToLoadOffset; i--) {
// months.push(this.getMonthDataPAST(i)); // months.push(this.getMonthDataPAST(i));
// } // }
} }
this.calendarData = months; this.months = months;
this.isLoading = false;
}, },
getMonthData(offset) {
const direction = this.calendarViewDirection; // "past" or "future" async getMonthData(offset = requiredParameter(), options) {
let yearNum; /* options will contain:
let monthIndex; 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 monthClass = "";
let initialViewEndDate; // only used for setCalendarData FUTURE let isMonthThatHidesSomeDaysForInitialView;
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();
if (typeof offset !== "undefined") { if (calendarViewDirection === "future" && offset > 0) {
// offset only passed during initial setup with setCalendarData while (monthIndex > 12) {
yearNum = this.todayDate.getFullYear(); monthIndex = monthIndex - 12;
monthIndex = this.todayDate.getMonth() + offset; yearNum++;
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--;
}
} }
} else { } else if (calendarViewDirection === "past" && offset < 0) {
// used only when adding an additional month while (monthIndex < 1) {
monthIndex = 12 + monthIndex;
if (direction === "future") { yearNum--;
// 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;
}
} }
} }
const todayDayIndex = this.todayDate.getDay(); // get day of week index of today (0-6) const monthEndDate = new Date(yearNum, monthIndex, 0); // BOTH
currentWeekStartDateNum =
todayDayIndex > todayDateNum ? 1 : todayDateNum - todayDayIndex; // FUTURE
const monthEndDate = new Date(yearNum, monthIndex + 1, 0); // BOTH
let monthEndDateNum = monthEndDate.getDate(); // 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) if (
monthEndDateNum = currentWeekEndDateNum; // PAST offset === 0 &&
calendarViewDirection === "past" &&
monthEndDateNum > this.currentWeekEndDateNum
) {
monthEndDateNum = this.currentWeekEndDateNum; // PAST
}
const monthStartDateNum = const monthStartDateNum =
offset === 0 && direction === "future" ? currentWeekStartDateNum : 1; // FUTURE offset === 0 && calendarViewDirection === "future"
const monthStartDate = new Date(yearNum, monthIndex, monthStartDateNum); // BOTH ? this.currentWeekStartDateNum
: 1; // FUTURE
const monthStartDate = new Date(yearNum, monthIndex - 1, monthStartDateNum); // BOTH
const startDateDayIndex = monthStartDate.getDay(); // FUTURE const startDateDayIndex = monthStartDate.getDay(); // FUTURE
const endDateDayIndex = monthEndDate.getDay(); // PAST const endDateDayIndex = monthEndDate.getDay(); // PAST
if (typeof offset !== "undefined") { if (Math.abs(offset) === 1 && hideSecondMonth) {
if (offset === 0 && direction === "future") monthClass = monthClass + " month-hidden";
firstMonthDayTally = monthStartDateNum - startDateDayIndex; // FUTURE (starts low, counts up) } else if (Math.abs(offset) > 1) {
if (offset === 0 && direction === "past") monthClass = monthClass + " month-hidden";
firstMonthDayTally = currentWeekEndDateNum; // PAST (starts high, counts down) }
if (
while ( Math.abs(offset) === options.monthsAfterToLoadOffset &&
direction === "future" && calendarViewDirection === "future"
offset === 0 && ) {
firstMonthDayTally < monthEndDateNum monthClass = monthClass + " last-available-month";
) { }
// FUTURE if (
firstMonthDayTally = firstMonthDayTally + 7; Math.abs(offset) === options.monthsBeforeToLoadOffset &&
this.initialViewRowsTally++; calendarViewDirection === "past"
} ) {
// TODO - re-check this logic if past direction
while ( monthClass = monthClass + " last-available-month";
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 (this.selectableDates === "custom") { // populate dates array
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
for (let i = monthStartDateNum; i <= monthEndDateNum; i++) { for (let i = monthStartDateNum; i <= monthEndDateNum; i++) {
let dayClasses = ""; let dayClasses = "";
let dateString =
yearNum.toString() +
"-" +
forceTwoDigitString(monthIndex) +
"-" +
forceTwoDigitString(i);
const thisDate = { const thisDate = {
// NOTE: uses monthNum (1-based), NOT monthIndex (0-based)
year: yearNum, year: yearNum,
month: monthIndex + 1, month: monthIndex,
date: i, date: i,
dateString: dateString,
}; };
if (offset === 0 && i === todayDateNum) { if (offset === 0 && i === this.todayDateNum) {
dayClasses += "current-day"; dayClasses += "current-day";
} }
if (offset === 0 && i < todayDateNum && direction === "future") { if (offset === 0 && i < this.todayDateNum && calendarViewDirection === "future") {
dayClasses += "unavailable-day"; dayClasses += "unavailable-day";
} }
if (offset === 0 && i > todayDateNum && direction === "past") { if (offset === 0 && i > this.todayDateNum && calendarViewDirection === "past") {
dayClasses += "unavailable-day"; 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"; dayClasses += "day-hidden";
} isMonthThatHidesSomeDaysForInitialView = true;
if (Math.abs(offset) === 1 && direction === "past" && i < initialViewStartDate) {
dayClasses += "day-hidden";
}
if (this.selectableDates === "custom" && this.isSelectableDate(thisDate)) {
dayClasses += " selectable-day";
} }
const dateObject = { const dateObject = {
dateNum: i, dateNum: i,
dayClasses: dayClasses, dayClasses: dayClasses,
inputValue: thisDate, inputValue: thisDate,
isSelectable:
this.selectableDatesData.findIndex(
(date) => date.dateString === dateString
) > -1
? true
: false,
}; };
datesArray.push(dateObject); dates.push(dateObject);
} }
const monthToAdd = { const monthToAdd = {
monthLabel: monthsOfYear[monthIndex], monthLabel: MONTHS_OF_YEAR[monthIndex - 1],
monthIndex: monthIndex, monthIndex: monthIndex,
monthString: MONTHS_OF_YEAR[monthIndex - 1] + "-" + yearNum?.toString(),
yearNum: yearNum, yearNum: yearNum,
dates: datesArray, dates: dates,
startDateDayIndex: startDateDayIndex, startDateDayIndex: startDateDayIndex,
monthClass: monthClass, monthClass: monthClass,
isMonthThatHidesSomeDaysForInitialView: isMonthThatHidesSomeDaysForInitialView,
}; };
return monthToAdd; return monthToAdd;
}, },
addMonthData() { async showAnotherMonth() {
const monthToAdd = this.getMonthData(); this.isLoading = true;
if (this.calendarViewDirection === "future") { let monthToShow;
this.calendarData.push(monthToAdd); let monthStartDateNum = 0;
}
if (this.calendarViewDirection === "past") { if (this.hideSomeDaysForInitialView) {
this.calendarData.unshift(monthToAdd); monthToShow = this.months.find(
} ({ isMonthThatHidesSomeDaysForInitialView }) =>
}, isMonthThatHidesSomeDaysForInitialView
isSelectableDate(thisDate) {
const testForDate = (dateInArray) => {
return (
dateInArray.date === thisDate.date &&
dateInArray.month === thisDate.month &&
dateInArray.year === thisDate.year
); );
}; // find the first day-hidden to become the next api call start date
const isSelectable = monthStartDateNum =
this.selectableDatesData.findIndex(testForDate) > -1 ? true : false; monthToShow.dates.find(({ dayClasses }) => dayClasses.includes("day-hidden"))
return isSelectable; .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) { async updateSelectableDates(monthStart, monthEnd) {
this.customSelectableDatesCallback(monthStart, monthEnd)?.forEach((newObj) => { const moreSelectableDates = await this.customSelectableDatesCallback(
monthStart,
monthEnd
);
moreSelectableDates.forEach((selectableDate) => {
const index = this.selectableDatesData.findIndex( const index = this.selectableDatesData.findIndex(
(obj) => (obj) => obj.dateString === selectableDate.dateString
obj.year === newObj.year &&
obj.month === newObj.month &&
obj.date === newObj.date
); );
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> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.date-picker-hidden {
opacity: 0;
max-height: 0;
}
.date-picker { .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 { .calendar-grid-container {
margin: 0 auto; margin: 0 auto 2rem auto;
max-width: 414px; max-width: 414px;
transition: height ease 2s, opacity ease 2s;
display: grid; display: grid;
grid-template-columns: repeat(7, 1fr); grid-template-columns: repeat(7, 1fr);
justify-content: center; justify-content: center;
align-items: center; align-items: center;
padding: 4px; padding: 0 0.75rem;
opacity: 1;
.grid-item { .grid-item {
text-align: center; text-align: center;
margin: 10%; margin: 10px 3px;
&.first-day-, &.first-day-,
&.first-day-0 { &.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 { .month-year {
grid-area: 1 / 1 / 2 / 5; grid-area: 1 / 1 / 2 / 5;
text-transform: uppercase; text-transform: uppercase;
@ -436,6 +641,9 @@ export default {
justify-content: center; justify-content: center;
align-items: center; align-items: center;
outline: none; outline: none;
height: 1.35rem;
opacity: 1;
transition: height ease 250ms, opacity ease 250ms;
input[type="radio"] { input[type="radio"] {
position: absolute; //override bootstrap position: absolute; //override bootstrap
@ -495,9 +703,9 @@ export default {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
width: 40px; width: 36px;
height: 40px; height: 36px;
min-width: 40px; min-width: 36px;
border-radius: 50%; border-radius: 50%;
span { span {
@ -567,19 +775,37 @@ export default {
} }
} }
&.initial-view { &.partial-month-initial-view {
&.month-hidden {
display: none;
}
.day-hidden { .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 { .btn-link {
font-weight: 500; font-weight: 500;
text-underline-offset: 4px; text-underline-offset: 4px;
box-shadow: none !important; // TODO: FIX THIS
position: absolute;
top: 90%;
} }
.past { .past {
.calendar-grid-container { .calendar-grid-container {
.month-year { .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> </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 */ /* istanbul ignore next */
callMockHttpClient({ method, endpoint }) { callMockHttpClient({ method, endpoint, payload }) {
// For Mock use only! // For Mock use only!
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
axios({ axios({
method: method, method: method,
url: endpoint, url: endpoint,
crossDomain: true, crossDomain: true,
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
responseType: {}, responseType: {},
data: payload,
}).then( }).then(
(response) => { (response) => {
resolve(response); // simulate a delayed response
setTimeout(() => {
resolve(response);
}, 2000);
}, },
(error) => { (error) => {
return reject(error.response); return reject(error.response);

View file

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

View file

@ -30,9 +30,10 @@
</template> </template>
<date-picker <date-picker
selectableDates="custom" selectableDatesSetting="custom"
ref="datePicker"
v-model="selectedDate" v-model="selectedDate"
:customSelectableDatesCallback="getAvailableDates" /> :customSelectableDatesCallback="getAvailableDatesMethod" />
<funnel-footer <funnel-footer
cmsWidgetName="FunnelFooterWidget" cmsWidgetName="FunnelFooterWidget"
ref="funnelFooter" ref="funnelFooter"
@ -55,6 +56,8 @@ import datePicker from "@/digital-components/date-picker/date-picker";
// Supporting files // Supporting files
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper"; 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 { settleAllPromises } from "@/helpers/layout-helper";
import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper"; import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
import { getAlertReasons } from "@/layouts/schedule/helpers/schedule-helper"; import { getAlertReasons } from "@/layouts/schedule/helpers/schedule-helper";
@ -70,29 +73,67 @@ import store from "@/store";
defineRule("date-required", required(errorMessages.DATE_REQUIRED)); 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 { export default {
name: "schedule", name: "schedule",
data() { data() {
return { return {
selectedDate: null, selectedDate: null,
selectableDatesData: [],
weatherAlerts: [], 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) { 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({
// 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); const alertReasonsPromise = getAlertReasons(store.getters.order.serviceLocation.zipCodeCtu);
@ -106,6 +147,10 @@ export default {
resultKey: "alertReasons", resultKey: "alertReasons",
promise: alertReasonsPromise, promise: alertReasonsPromise,
}, },
{
resultKey: "datePickerInitialData",
promise: datePickerInitialDataPromise,
},
]; ];
const resultMap = await settleAllPromises(promiseResultMap); const resultMap = await settleAllPromises(promiseResultMap);
@ -114,6 +159,7 @@ export default {
next((vm) => { next((vm) => {
vm.setCmsContent(resultMap.cmsContent); vm.setCmsContent(resultMap.cmsContent);
vm.setData(resultMap.alertReasons); vm.setData(resultMap.alertReasons);
vm.$refs.datePicker.initializeComponent(resultMap.datePickerInitialData);
}); });
}, },
computed: { computed: {
@ -137,6 +183,22 @@ export default {
return true; return true;
// NEED TODO - WHAT ARE PAGE REQ'S FOR THIS PAGE? // 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) { setData(alertReasonsData) {
if (alertReasonsData) { if (alertReasonsData) {
this.convertReasonsToCmsAlerts(alertReasonsData); this.convertReasonsToCmsAlerts(alertReasonsData);
@ -165,9 +227,6 @@ export default {
console.log("error fetching alert reasons.."); console.log("error fetching alert reasons..");
}); });
}, },
getAvailableDates(startDate, endDate) {
return this.mockSelectableDatesData;
},
getServiceZipCtuCodeFromStore() { getServiceZipCtuCodeFromStore() {
return store.getters.order.serviceLocation.zipCodeCtu; return store.getters.order.serviceLocation.zipCodeCtu;
}, },
@ -190,6 +249,13 @@ export default {
navigateToHeritageFunnel({ loadingModal: this.$refs.loadingModal }); navigateToHeritageFunnel({ loadingModal: this.$refs.loadingModal });
}, },
}, },
watch: {
selectedDate(date) {
/* EXAMPLE RETURNED date OBJECT:
{ "year": 2023, "month": 5, "date": 25, "dateString": "2023-05-25" }
*/
},
},
components: { components: {
alert, alert,
funnelHeader, funnelHeader,
@ -202,5 +268,3 @@ export default {
mounted() {}, mounted() {},
}; };
</script> </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 // Session API Actions
saveSession(context) { saveSession(context) {
const vehicle = context.getters.vehicle; const vehicle = context.getters.vehicle;