Merge branch 'develop' into feature/CSR-1144

This commit is contained in:
CarlNation 2023-05-19 08:25:24 -04:00
commit 4bf8f844d8
22 changed files with 894 additions and 542 deletions

View file

@ -15,10 +15,12 @@ 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
"!src/layouts/schedule/helpers/schedule-helper.js", // Temp test exclusion while in development
"!src/layouts/review/*.vue", // Temp test exclusion while in development
// END
], // ! means exclude from coverage.
testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"],

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

@ -121,7 +121,7 @@ export default {
isOverflowScrollable: Boolean,
isWide: Boolean,
isCashOrInsurance: Boolean,
modelValue: [Array, Number, String, Object],
modelValue: [Array, Number, String],
value: [Number, String],
validationRules: String,
suppressError: Boolean,
@ -141,7 +141,7 @@ export default {
const fieldOptions = {
value: modelValue,
initialValue: modelValue,
initialValue: null,
};
const { errorMessage, handleBlur, handleChange, meta, validate, errors, resetField } =
@ -255,10 +255,8 @@ export default {
},
},
watch: {
modelValue(newValue) {
this.resetField({
value: newValue,
});
modelValue(newValue, oldValue) {
this.resetField();
},
answers() {
//once we get the answers to display from parent, see if we need a GA event to log what we showed

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

@ -1,6 +1,6 @@
<template>
<transition name="fade" mode="out-in">
<div class="appointment-type-question" aria-live="polite">
<div class="appointment-type-question" aria-live="polite" v-if="isDisplayed">
<buttonQuestion
ref="buttonQuestion"
customButtonQuestionId="appointmentTypeQuestion"
@ -25,6 +25,7 @@ export default {
modelValue: String,
groupName: String,
isAvailable: Boolean,
isDisplayed: Boolean,
suppressError: Boolean,
validationRules: String,
cmsWidgetName: String,

View file

@ -45,4 +45,18 @@ describe("object-cloning-helper.js", () => {
// Assert
expect(result).toStrictEqual(expected);
});
it("Should return a copy of the array", async () => {
// Arrange
const array = [9, 8, 7, 6, 5, 4, 3, 2, 1];
const expected = [9, 8, 7, 6, 5, 4, 3, 2, 1];
// Act
const result = deepClone(array);
// Assert
expect(result).toStrictEqual(expected);
});
});

View file

@ -1,9 +1,10 @@
import { getPricedMobileFeePart } from "./service-location-helper";
import { getPricedMobileFeePart, getServiceabilityDetails } from "./service-location-helper";
import { storeActions } from "@/constants/store-actions";
const mockStoreActionGetMobileFeePart = storeActions.GET_MOBILE_FEE_PART;
const mockStoreActionPriceOrderItemsAndSaveServerData =
storeActions.PRICE_ORDER_ITEMS_AND_SAVE_SERVER_DATA;
const mockStoreActionGetServiceabilityDetails = storeActions.GET_SERVICEABILITY_DETAILS;
jest.mock("@/mixins/base-mixin.js", () => ({
methods: {
@ -40,56 +41,87 @@ jest.mock("@/mixins/base-mixin.js", () => ({
},
]);
}
if (actionName === mockStoreActionGetServiceabilityDetails) {
return Promise.resolve({
isGlassServiceableInshop: true,
isRecalibrationServiceableInshop: true,
isGlassServiceableMobile: true,
isRecalibrationServiceableMobile: true,
});
}
}),
},
}));
describe("service-location-helper.js", () => {
it("Should return null if no service zip code is passed in", async () => {
// Arrange
const serviceZipCode = null;
const damageType = "Replace";
const parentAccountNumber = 167132;
const billToAccountNumber = 1234;
const expected = null;
describe("getPricedMobileFeePart", () => {
it("Should return null if no service zip code is passed in", async () => {
// Arrange
const serviceZipCode = null;
const damageType = "Replace";
const parentAccountNumber = 167132;
const billToAccountNumber = 1234;
const expected = null;
// Act
const result = await getPricedMobileFeePart(
serviceZipCode,
damageType,
parentAccountNumber,
billToAccountNumber
);
// Act
const result = await getPricedMobileFeePart(
serviceZipCode,
damageType,
parentAccountNumber,
billToAccountNumber
);
// Assert
expect(result).toEqual(expected);
// Assert
expect(result).toEqual(expected);
});
it("Should return the priced mobile fee part", async () => {
// Arrange
const serviceZipCode = "43235";
const damageType = "Replace";
const parentAccountNumber = 167132;
const billToAccountNumber = 1234;
const expected = {
partNumber: "MOBILE FEE",
description: "MOBILE FEE",
partType: "FEE",
laborAmount: 49.99,
sellingPrice: 0,
kitPrice: 0,
};
// Act
const result = await getPricedMobileFeePart(
serviceZipCode,
damageType,
parentAccountNumber,
billToAccountNumber
);
// Assert
expect(result).toEqual(expected);
});
});
it("Should return the priced mobile fee part", async () => {
// Arrange
const serviceZipCode = "43235";
const damageType = "Replace";
const parentAccountNumber = 167132;
const billToAccountNumber = 1234;
describe("getServiceabilityDetails", () => {
it("Should return the serviceability details", async () => {
// Arrange
const serviceZipCode = "43235";
const expected = {
partNumber: "MOBILE FEE",
description: "MOBILE FEE",
partType: "FEE",
laborAmount: 49.99,
sellingPrice: 0,
kitPrice: 0,
};
const expected = {
isGlassServiceableInshop: true,
isRecalibrationServiceableInshop: true,
isGlassServiceableMobile: true,
isRecalibrationServiceableMobile: true,
};
// Act
const result = await getPricedMobileFeePart(
serviceZipCode,
damageType,
parentAccountNumber,
billToAccountNumber
);
// Act
const result = await getServiceabilityDetails(serviceZipCode);
// Assert
expect(result).toEqual(expected);
// Assert
expect(result).toEqual(expected);
});
});
});

View file

@ -236,35 +236,40 @@ export default {
});
},
async setMobileLocation() {
// Validate the Zip Code
const zipCodeData = await this.getZipCodeData(
this.internalModel.addressQuestions.zipCode
);
if (
this.internalModel.addressQuestions.zipCode !==
this.modelValue.addressQuestions.zipCode
) {
// Validate the Zip Code
const zipCodeData = await this.getZipCodeData(
this.internalModel.addressQuestions.zipCode
);
if (!zipCodeData.isValid) {
this.displayInvalidZipAlert = true;
this.resetModalButtonStyle();
} else {
// retrieve mobile fee part
const serviceZipCode = this.internalModel.addressQuestions.zipCode;
const mobileFeePart = await getPricedMobileFeePart(serviceZipCode);
if (!zipCodeData.isValid) {
this.displayInvalidZipAlert = true;
this.resetModalButtonStyle();
} else {
// retrieve mobile fee part
const serviceZipCode = this.internalModel.addressQuestions.zipCode;
const mobileFeePart = await getPricedMobileFeePart(serviceZipCode);
// retrieve serviceability details
const serviceabilityDetails = await getServiceabilityDetails(serviceZipCode);
// retrieve serviceability details
const serviceabilityDetails = await getServiceabilityDetails(serviceZipCode);
// update content related to service zip code
this.$emit("updated-mobile-fee-part", mobileFeePart);
this.$emit("updated-serviceability", serviceabilityDetails.data);
this.$emit("updated-contains-military-base", zipCodeData.containsMilitaryBase);
// update content related to service zip code
this.$emit("updated-mobile-fee-part", mobileFeePart);
this.$emit("updated-serviceability", serviceabilityDetails.data);
this.$emit("updated-contains-military-base", zipCodeData.containsMilitaryBase);
// Update the page level model
this.$emit("update:modelValue", this.internalModel);
// Update the page level model
this.$emit("update:modelValue", this.internalModel);
if (this.onZipUpdateCallback) {
await this.onZipUpdateCallback(serviceZipCode);
if (this.onZipUpdateCallback) {
await this.onZipUpdateCallback(serviceZipCode);
}
this.closeModal();
}
this.closeModal();
}
},
},

View file

@ -505,36 +505,6 @@ describe("service-location.vue", () => {
expect(wrapper.vm.serviceZipCodeQuestion).toStrictEqual(newServiceZipCodeInfo);
});
test("resets appointment type selection when service zip code is updated by mobile location modal when Mobile is not selected", async () => {
// Arrange
const { wrapper } = setupMocks({});
await wrapper.setData({
selectedAppointmentType: "Dropoff",
});
const mobileLocationQuestionsComponent = wrapper.findComponent({
ref: "mobileLocationQuestions",
});
mobileLocationQuestionsComponent.resetComponent = jest.fn();
const serviceZipCodeComponent = wrapper.findComponent({
ref: "serviceZipCodeQuestion",
});
serviceZipCodeComponent.resetMobileFeePart = jest.fn();
const newServiceZipCodeQuestion = {
zipCode: "61606",
state: "IL",
};
// Act
serviceZipCodeComponent.vm.$emit("update:modelValue", newServiceZipCodeQuestion);
// Assert
expect(wrapper.vm.selectedAppointmentType).toStrictEqual(null);
});
test("does not reset appointment type selection when service zip code is updated by mobile location modal when Mobile is selected", async () => {
// Arrange
const { wrapper } = setupMocks({});

View file

@ -47,9 +47,10 @@
alertClass="alert-warning" />
<appointmentTypeQuestion
v-model="selectedAppointmentType"
v-show="!displayNoShopsAlert"
v-show="isAppointmentTypeDisplayed"
:isServiceableMobile="isServiceableMobile"
:isServiceableInshop="isServiceableInshop"
:isDisplayed="isAppointmentTypeDisplayed"
ref="appointmentTypeQuestion"
groupName="appointmentTypeQuestion"
cmsWidgetName="AppointmentTypeQuestionWidget"
@ -71,9 +72,7 @@
<shopQuestion
ref="shopQuestion"
v-show="isShopQuestionDisplayed"
v-model="selectedProviderNumber"
@providerSelected="onProviderSelected"
:serviceZipCode="zipCode"
v-model="selectedProvider"
:selectedAppointmentType="selectedAppointmentType"
:isDisplayed="isShopQuestionDisplayed"
cmsWidgetName="ShopQuestionWidget" />
@ -134,16 +133,6 @@ defineRule("mobile-location-required", (value) => {
return true;
});
const defaultProvider = {
providerNumber: null,
address: {
streetAddress: null,
city: null,
state: null,
zip: null,
},
};
export default {
name: "service-location",
data() {
@ -160,7 +149,6 @@ export default {
isRecalibrationServiceableMobile: null,
selectedAppointmentType: this.getSelectedAppointmentType(),
selectedProvider: this.getSelectedProvider(),
selectedProviderNumber: this.getSelectedProvider().providerNumber,
mobileFeePart: null,
zipContainsMilitaryBase: false,
zipCodeCtu: null,
@ -228,7 +216,7 @@ export default {
if (newValue.zipCode !== this.zipCode) {
this.resetMobileLocation();
this.selectedAppointmentType = null;
this.selectedProvider = defaultProvider;
this.selectedProvider = null;
}
this.state = newValue.state;
@ -263,7 +251,7 @@ export default {
if (!this.selectedAppointmentType == "Mobile") {
this.selectedAppointmentType = null;
}
this.selectedProvider = defaultProvider;
this.selectedProvider = null;
}
},
},
@ -287,8 +275,11 @@ export default {
this.selectedAppointmentType === "Dropoff"
);
},
// Specifically check for isRecalibrationServiceableMobile === false, not null or true.
isAppointmentTypeDisplayed() {
return this.zipCode && !this.displayNoShopsAlert;
},
requiresInshopRecalibration() {
// Specifically check for isRecalibrationServiceableMobile === false, not null or true.
return (
this.isServiceableInshop &&
this.isGlassServiceableMobile &&
@ -342,6 +333,9 @@ export default {
this.zipContainsMilitaryBase = val;
}
},
setMobileFeePart(mobileFeePart) {
this.mobileFeePart = mobileFeePart;
},
getServiceAddressFromStore() {
return store.getters.order.serviceLocation.address;
},
@ -358,10 +352,7 @@ export default {
return store.getters.order.serviceLocation.appointmentType;
},
getSelectedProvider() {
return store.getters.order.serviceLocation.provider ?? defaultProvider;
},
setMobileFeePart(mobileFeePart) {
this.mobileFeePart = mobileFeePart;
return store.getters.order.serviceLocation.provider;
},
resetMobileLocation() {
this.streetAddress = "";
@ -417,9 +408,6 @@ export default {
async reloadShopData() {
await this.$refs.shopQuestion.reloadShopData(this.zipCode);
},
onProviderSelected(selectedProvider) {
this.selectedProvider = selectedProvider;
},
},
components: {
alert,

View file

@ -434,7 +434,7 @@ describe("shop-question.vue", () => {
const { wrapper } = setupMocks({
mixins: [mockMixin],
props: {
modelValue: selectedProvider.providerNumber,
modelValue: selectedProvider,
serviceZipCode: "43081",
selectedAppointmentType: "Dropoff",
cmsWidgetName: cmsWidgetName,
@ -460,18 +460,7 @@ describe("shop-question.vue", () => {
const { wrapper } = setupMocks({
mixins: [mockMixin],
props: {
modelValue: {
address: {
city: "POWELL",
country: "US",
state: "OH",
streetAddress: "3938 POWELL RD",
zipCode: "43065",
},
distanceInMiles: 16.2690495685233,
providerNumber: "003341",
},
serviceZipCode: "43081",
modelValue: null,
selectedAppointmentType: "Dropoff",
cmsWidgetName: cmsWidgetName,
},
@ -481,99 +470,22 @@ describe("shop-question.vue", () => {
});
// Act
wrapper.vm.initializeComponent(shopQuestionInitialData);
await wrapper.vm.initializeComponent(shopQuestionInitialData);
await wrapper.vm.$nextTick();
expect(wrapper.vm.answers.length).toEqual(3);
await wrapper.setProps({
selectedAppointmentType: "Inshop",
});
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.answers.length).toEqual(3);
});
// it("Should reload the shops when the service zip code changes", async () => {
// // Arrange
// const { wrapper } = setupMocks({
// mixins: [mockMixin],
// props: {
// modelValue: {
// address: {
// city: "POWELL",
// country: "US",
// state: "OH",
// streetAddress: "3938 POWELL RD",
// zipCode: "43065",
// },
// distanceInMiles: 16.2690495685233,
// providerNumber: "003341",
// },
// serviceZipCode: "43081",
// selectedAppointmentType: "Dropoff",
// cmsWidgetName: cmsWidgetName,
// isDisplayed: true
// },
// mountOptions: {
// attachTo: document.body,
// },
// });
// wrapper.vm.$options.methods.loadInitialData = jest.fn().mockImplementation(() => {
// return new Promise((resolve) => {
// resolve(mockNewShopList);
// });
// });
// // Act
// wrapper.vm.initializeComponent(shopQuestionInitialData);
// await wrapper.vm.$options.watch.serviceZipCode.handler.call(wrapper.vm, "43054");
// // Assert
// expect(wrapper.vm.shopProviders.length).toEqual(3);
// expect(wrapper.vm.shopProviders).toEqual(mockNewShopList.shopProviders);
// });
// it("Should clear the existing answers when the service zip code changes", async () => {
// // Arrange
// const { wrapper } = setupMocks({
// mixins: [mockMixin],
// props: {
// modelValue: {
// address: {
// city: "POWELL",
// country: "US",
// state: "OH",
// streetAddress: "3938 POWELL RD",
// zipCode: "43065",
// },
// distanceInMiles: 16.2690495685233,
// providerNumber: "003341",
// },
// serviceZipCode: "43081",
// selectedAppointmentType: "Dropoff",
// cmsWidgetName: cmsWidgetName,
// },
// mountOptions: {
// attachTo: document.body,
// },
// });
// //Act
// wrapper.vm.initializeComponent(shopQuestionInitialData);
// wrapper.setProps({
// selectedAppointmentType: "Inshop",
// });
// await wrapper.vm.$nextTick();
// // Assert
// expect(wrapper.vm.answers.length).toEqual(3);
// });
});
function setupMocks({ mountOptions, mixins, props, isShallowMount = true }) {

View file

@ -68,8 +68,8 @@ export default {
props: {
modelValue: {
type: Object,
default: () => null,
},
serviceZipCode: String,
selectedAppointmentType: String,
cmsWidgetName: String,
validationRules: String,
@ -81,16 +81,12 @@ export default {
},
selectedValue: {
get: function () {
return this.modelValue;
return this.modelValue?.providerNumber;
},
set: function (newValue) {
// Button Question only supports primitive values so we must get the full object to emit to the page
const provider = this.shopProviders?.find(
(provider) => provider.providerNumber == newValue
);
this.$emit("update:modelValue", newValue);
this.$emit("providerSelected", provider);
// Button Question only supports Number, String data types so we must get the full object to emit
const provider = this.getSelectedProviderObject(newValue);
this.$emit("update:modelValue", provider);
},
},
displayDropoffInformation() {
@ -170,11 +166,6 @@ export default {
resetAnswers() {
this.answers = [];
this.shopIndex = 0;
this.selectedValue = "";
if (this.$refs.buttonQuestion) {
this.$refs.buttonQuestion.resetField();
}
},
async reloadShopData(serviceZipCode) {
const result = await this.loadData(serviceZipCode);
@ -185,6 +176,20 @@ export default {
await nextTick();
await this.getNextShopsFromList();
},
getSelectedProviderObject(providerNumber) {
const provider = this.shopProviders?.find(
(provider) => provider.providerNumber == providerNumber
);
return provider;
},
getSelectedProviderIndex(providers, selectedProviderNumber) {
const index = providers.findIndex(
(provider) => provider.providerNumber == selectedProviderNumber
);
return index;
},
},
watch: {
selectedAppointmentType: {
@ -193,6 +198,12 @@ export default {
await this.$nextTick();
this.selectedValue = null;
this.$refs.buttonQuestion?.resetField();
await this.$nextTick();
if (newValue !== "Mobile") {
await this.getNextShopsFromList();
}
@ -200,19 +211,19 @@ export default {
},
shopProviders: {
async handler(newValue) {
//this.resetAnswers();
await this.$nextTick();
if (this.selectedAppointmentType) {
const selectedShopIndex = newValue.findIndex(
(provider) => provider.providerNumber == this.modelValue
const selectedShopIndex = this.getSelectedProviderIndex(
newValue,
this.modelValue?.providerNumber
);
if (selectedShopIndex >= 3) {
await this.getNextShopsFromList(selectedShopIndex + 1);
} else {
await this.getNextShopsFromList();
await this.$nextTick();
}
}
},

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;