Merge branch 'develop' into feature/CSR-1370
This commit is contained in:
commit
f614e795a6
35 changed files with 1185 additions and 434 deletions
|
|
@ -29,6 +29,10 @@ const errorMessages = {
|
||||||
DATE_REQUIRED: "Please select a date",
|
DATE_REQUIRED: "Please select a date",
|
||||||
PHONE_REQUIRED: "Please enter your phone number",
|
PHONE_REQUIRED: "Please enter your phone number",
|
||||||
PHONE_FORMAT: "Phone number must be 10 digits",
|
PHONE_FORMAT: "Phone number must be 10 digits",
|
||||||
|
YEAR_REQUIRED: "Please select your vehicle year",
|
||||||
|
MAKE_REQUIRED: "Please select your vehicle make",
|
||||||
|
MODEL_REQUIRED: "Please select your vehicle model",
|
||||||
|
STYLE_REQUIRED: "Please select your vehicle style",
|
||||||
};
|
};
|
||||||
|
|
||||||
export { errorMessages };
|
export { errorMessages };
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,7 @@ https://safelite.atlassian.net/wiki/spaces/DC/pages/76644418/Button+Question+Com
|
||||||
</div>
|
</div>
|
||||||
<div class="row form-test-error">
|
<div class="row form-test-error">
|
||||||
<error-message
|
<error-message
|
||||||
class="small"
|
class="small mt-1"
|
||||||
:name="formatString(groupName)"
|
:name="formatString(groupName)"
|
||||||
v-if="!suppressError"></error-message>
|
v-if="!suppressError"></error-message>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
v-for="month in months"
|
v-for="month in months"
|
||||||
:key="`${month.monthLabel}-${month.yearNum?.toString()}`"
|
:key="`${month.monthLabel}-${month.yearNum?.toString()}`"
|
||||||
:id="`${month.monthLabel}-${month.yearNum?.toString()}`"
|
:id="`${month.monthLabel}-${month.yearNum?.toString()}`"
|
||||||
class="calendar-grid-container position-relative"
|
class="calendar-grid-container"
|
||||||
:class="[
|
:class="[
|
||||||
hideSomeDaysForInitialView ? 'partial-month-initial-view' : '',
|
hideSomeDaysForInitialView ? 'partial-month-initial-view' : '',
|
||||||
month.monthClass,
|
month.monthClass,
|
||||||
|
|
@ -56,15 +56,15 @@
|
||||||
:class="[!isLoading ? 'date-picker-hidden' : '']"
|
:class="[!isLoading ? 'date-picker-hidden' : '']"
|
||||||
loaderColor="blue"
|
loaderColor="blue"
|
||||||
loaderPosition="center" />
|
loaderPosition="center" />
|
||||||
|
<button
|
||||||
|
v-if="calendarViewDirection === 'future' && !disableViewMoreDatesButton"
|
||||||
|
type="button"
|
||||||
|
class="btn btn-link"
|
||||||
|
:disabled="isLoading"
|
||||||
|
@click="showAnotherMonth">
|
||||||
|
View more dates
|
||||||
|
</button>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<button
|
|
||||||
v-if="calendarViewDirection === 'future' && !disableViewMoreDatesButton"
|
|
||||||
type="button"
|
|
||||||
class="btn btn-link"
|
|
||||||
:disabled="isLoading"
|
|
||||||
@click="showAnotherMonth">
|
|
||||||
View more dates
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -74,6 +74,10 @@ import loader from "@/ux-components/loader/loader";
|
||||||
import store from "@/store";
|
import store from "@/store";
|
||||||
import { TIMINGFUNC_MAP, BUFFER_OFFSET, MONTHS_OF_YEAR } from "./mixins/constants";
|
import { TIMINGFUNC_MAP, BUFFER_OFFSET, MONTHS_OF_YEAR } from "./mixins/constants";
|
||||||
import { selectableDaysOptions, requiredParameter, forceTwoDigitString } from "./mixins/helpers";
|
import { selectableDaysOptions, requiredParameter, forceTwoDigitString } from "./mixins/helpers";
|
||||||
|
import {
|
||||||
|
convertDateToDateString,
|
||||||
|
convertDateStringToDate,
|
||||||
|
} from "@/layouts/schedule/helpers/schedule-helper";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "datePicker",
|
name: "datePicker",
|
||||||
|
|
@ -110,23 +114,14 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
today() {
|
todayString() {
|
||||||
if (this.todayOverrideDateString) {
|
return this.todayOverrideDateString || convertDateToDateString(new Date());
|
||||||
return new Date(this.todayOverrideDateString + "T00:00:00");
|
|
||||||
}
|
|
||||||
return new Date();
|
|
||||||
},
|
|
||||||
todayMonthIndex() {
|
|
||||||
return this.today.getMonth() + 1;
|
|
||||||
},
|
|
||||||
todayYearNum() {
|
|
||||||
return this.today.getFullYear();
|
|
||||||
},
|
},
|
||||||
todayDayIndex() {
|
todayDayIndex() {
|
||||||
return this.today.getDay();
|
return convertDateStringToDate(this.todayString).getDay();
|
||||||
},
|
},
|
||||||
todayDateNum() {
|
todayDateNum() {
|
||||||
return this.today.getDate();
|
return convertDateStringToDate(this.todayString).getDate();
|
||||||
},
|
},
|
||||||
currentWeekStartDateNum() {
|
currentWeekStartDateNum() {
|
||||||
return this.todayDayIndex >= this.todayDateNum
|
return this.todayDayIndex >= this.todayDateNum
|
||||||
|
|
@ -157,154 +152,187 @@ export default {
|
||||||
fireDateClickedEvent() {
|
fireDateClickedEvent() {
|
||||||
this.$emit("date-clicked");
|
this.$emit("date-clicked");
|
||||||
},
|
},
|
||||||
getWeekStartDate(date) {
|
getWeekStartDate(dateString) {
|
||||||
|
const date = convertDateStringToDate(dateString);
|
||||||
const dayOfWeek = date.getDay();
|
const dayOfWeek = date.getDay();
|
||||||
// Subtract the day of the week from date to get the date of Sunday
|
// Subtract the day of the week from date to get the date of Sunday
|
||||||
const sunday = new Date(date);
|
const sunday = new Date(date);
|
||||||
sunday.setDate(sunday.getDate() - dayOfWeek);
|
sunday.setDate(sunday.getDate() - dayOfWeek);
|
||||||
return sunday;
|
return convertDateToDateString(sunday);
|
||||||
},
|
},
|
||||||
getWeekEndDate(date) {
|
getWeekEndDate(dateString) {
|
||||||
|
const date = convertDateStringToDate(dateString);
|
||||||
const dayOfWeek = date.getDay();
|
const dayOfWeek = date.getDay();
|
||||||
const daysUntilSaturday = 6 - dayOfWeek; // Calculate the number of days until Saturday
|
const daysUntilSaturday = 6 - dayOfWeek; // Calculate the number of days until Saturday
|
||||||
// Clone the given date and add the remaining days until Saturday
|
// Clone the given date and add the remaining days until Saturday
|
||||||
const saturday = new Date(date);
|
const saturday = new Date(date);
|
||||||
saturday.setDate(date.getDate() + daysUntilSaturday);
|
saturday.setDate(date.getDate() + daysUntilSaturday);
|
||||||
return saturday;
|
return convertDateToDateString(saturday);
|
||||||
},
|
},
|
||||||
getNextWeekSunday(date) {
|
getNextWeekSunday(dateString) {
|
||||||
|
const date = convertDateStringToDate(dateString);
|
||||||
const dayOfWeek = date.getDay();
|
const dayOfWeek = date.getDay();
|
||||||
const daysUntilNextSunday = 7 - dayOfWeek; // Calculate the number of days until the next Sunday
|
const daysUntilNextSunday = 7 - dayOfWeek; // Calculate the number of days until the next Sunday
|
||||||
// Clone the given date and add the remaining days until Sunday
|
// Clone the given date and add the remaining days until Sunday
|
||||||
const nextSunday = new Date(date);
|
const nextSunday = new Date(date);
|
||||||
nextSunday.setDate(date.getDate() + daysUntilNextSunday);
|
nextSunday.setDate(date.getDate() + daysUntilNextSunday);
|
||||||
return nextSunday;
|
return convertDateToDateString(nextSunday);
|
||||||
},
|
},
|
||||||
getInitialViewWeeks(today, initialViewRowsToShow, preSelectedDateString) {
|
getMonthEnd(dateStr) {
|
||||||
|
// convert string to date, do date calc, then return a string back
|
||||||
|
const date = new Date(dateStr.split("-")[0], parseInt(dateStr.split("-")[1]), 0);
|
||||||
|
return convertDateToDateString(date);
|
||||||
|
},
|
||||||
|
getInitialViewWeeks(todayString, initialViewRowsToShow, preSelectedDateString) {
|
||||||
// TODO: this only is for future direction; need to create logic for past direction
|
// TODO: this only is for future direction; need to create logic for past direction
|
||||||
const weeks = [];
|
const weeks = [];
|
||||||
|
let weekStartDate = this.getWeekStartDate(todayString);
|
||||||
|
let weekEndDate = this.getWeekEndDate(todayString);
|
||||||
|
|
||||||
if (preSelectedDateString) initialViewRowsToShow = 26;
|
if (preSelectedDateString) {
|
||||||
|
let preSelectedDateMonthEnd = this.getMonthEnd(preSelectedDateString);
|
||||||
|
let weekIncludesPreSelectedMonthEnd = false;
|
||||||
|
let i = 0;
|
||||||
|
while (!weekIncludesPreSelectedMonthEnd) {
|
||||||
|
if (i > 0) {
|
||||||
|
weekStartDate = this.getNextWeekSunday(weekEndDate);
|
||||||
|
weekEndDate = this.getWeekEndDate(weekStartDate);
|
||||||
|
|
||||||
let weekStartDate = this.getWeekStartDate(today);
|
if (
|
||||||
let weekEndDate = this.getWeekEndDate(today);
|
(preSelectedDateMonthEnd > weekStartDate &&
|
||||||
for (let i = 0; i < initialViewRowsToShow; i++) {
|
preSelectedDateMonthEnd < weekEndDate) ||
|
||||||
if (i > 0) {
|
preSelectedDateMonthEnd === weekStartDate ||
|
||||||
weekStartDate = this.getNextWeekSunday(weekEndDate);
|
preSelectedDateMonthEnd === weekEndDate
|
||||||
weekEndDate = this.getWeekEndDate(weekStartDate);
|
) {
|
||||||
}
|
weekEndDate = preSelectedDateMonthEnd;
|
||||||
weeks.push({
|
weekIncludesPreSelectedMonthEnd = true;
|
||||||
weekNum: i + 1,
|
|
||||||
weekStartDate: weekStartDate,
|
|
||||||
weekEndDate: weekEndDate,
|
|
||||||
});
|
|
||||||
if (
|
|
||||||
preSelectedDateString &&
|
|
||||||
new Date(preSelectedDateString + "T00:00:00") < weekEndDate
|
|
||||||
) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// are any of these weeks split between two months?
|
|
||||||
// NOTE: a week split between two months counts as 2 weeks
|
|
||||||
const hasSplitWeek = (week) => {
|
|
||||||
return week.weekStartDate.getMonth() !== week.weekEndDate.getMonth() ? true : false;
|
|
||||||
};
|
|
||||||
const splitWeekIndex = weeks.findIndex(hasSplitWeek);
|
|
||||||
|
|
||||||
if (!preSelectedDateString && splitWeekIndex > -1) {
|
|
||||||
// a preSelectedDateString precludes split week logic
|
|
||||||
const week1 = [];
|
|
||||||
const week2 = [];
|
|
||||||
let switchToWeek2 = false;
|
|
||||||
|
|
||||||
for (let j = 0; j < 7; j++) {
|
|
||||||
const newDate = new Date(weeks[splitWeekIndex].weekStartDate);
|
|
||||||
newDate.setDate(newDate.getDate() + j);
|
|
||||||
if (newDate.getDate() === 1) switchToWeek2 = true;
|
|
||||||
if (switchToWeek2) {
|
|
||||||
week2.push(newDate);
|
|
||||||
} else {
|
|
||||||
week1.push(newDate);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const week1EndDate = week1[week1.length - 1];
|
|
||||||
const week2StartDate = week2[0];
|
|
||||||
|
|
||||||
if (week1EndDate < today) {
|
|
||||||
// replace week 1 with week 2
|
|
||||||
weeks[splitWeekIndex].weekStartDate = week2StartDate;
|
|
||||||
} else {
|
|
||||||
const newWeek = {
|
|
||||||
weekNum: weeks[splitWeekIndex].weekNum,
|
|
||||||
weekStartDate: week2StartDate,
|
|
||||||
weekEndDate: weeks[splitWeekIndex].weekEndDate,
|
|
||||||
};
|
|
||||||
weeks[splitWeekIndex].weekEndDate = week1EndDate;
|
|
||||||
weeks.splice(splitWeekIndex + 1, 0, newWeek);
|
|
||||||
weeks.pop();
|
|
||||||
weeks.forEach((item, index) => {
|
|
||||||
if (index > splitWeekIndex) {
|
|
||||||
item.weekNum = item.weekNum + 1;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
weeks.push({
|
||||||
|
weekNum: i + 1,
|
||||||
|
weekStartDate: weekStartDate,
|
||||||
|
weekEndDate: weekEndDate,
|
||||||
});
|
});
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// If any of these weeks is split between two months, then make them 2 separate "weeks"
|
||||||
|
// (a week split between two months is considered 2 weeks per business requirements)
|
||||||
|
const hasSplitWeek = (week) => {
|
||||||
|
return week.weekStartDate.split("-")[1] !== week.weekEndDate.split("-")[1]
|
||||||
|
? true
|
||||||
|
: false;
|
||||||
|
};
|
||||||
|
const splitWeekIndex = weeks.findIndex(hasSplitWeek);
|
||||||
|
|
||||||
|
if (splitWeekIndex > -1) {
|
||||||
|
const week1 = [];
|
||||||
|
const week2 = [];
|
||||||
|
let switchToWeek2 = false;
|
||||||
|
|
||||||
|
for (let j = 0; j < 7; j++) {
|
||||||
|
const newDate = convertDateStringToDate(
|
||||||
|
weeks[splitWeekIndex].weekStartDate
|
||||||
|
);
|
||||||
|
newDate.setDate(newDate.getDate() + j);
|
||||||
|
if (newDate.getDate() === 1) switchToWeek2 = true;
|
||||||
|
if (switchToWeek2) {
|
||||||
|
week2.push(convertDateToDateString(newDate));
|
||||||
|
} else {
|
||||||
|
week1.push(convertDateToDateString(newDate));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const week1EndDate = week1[week1.length - 1];
|
||||||
|
const week2StartDate = week2[0];
|
||||||
|
|
||||||
|
if (week1EndDate < todayString) {
|
||||||
|
// replace week 1 with week 2
|
||||||
|
weeks[splitWeekIndex].weekStartDate = week2StartDate;
|
||||||
|
} else {
|
||||||
|
const newWeek = {
|
||||||
|
weekNum: weeks[splitWeekIndex].weekNum,
|
||||||
|
weekStartDate: week2StartDate,
|
||||||
|
weekEndDate: weeks[splitWeekIndex].weekEndDate,
|
||||||
|
};
|
||||||
|
weeks[splitWeekIndex].weekEndDate = week1EndDate;
|
||||||
|
weeks.splice(splitWeekIndex + 1, 0, newWeek);
|
||||||
|
weeks.pop();
|
||||||
|
weeks.forEach((item, index) => {
|
||||||
|
if (index > splitWeekIndex) {
|
||||||
|
item.weekNum = item.weekNum + 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return weeks;
|
return weeks;
|
||||||
},
|
},
|
||||||
async loadInitialData(config) {
|
async loadInitialData(config) {
|
||||||
let todayDate;
|
/*
|
||||||
if (this.today) {
|
** NOTE: this _could_ be called by a parent before fully loaded, so data or computeds might not be available
|
||||||
todayDate = this.today;
|
*/
|
||||||
} else if (config.todayOverrideDateString) {
|
let todayDateString;
|
||||||
todayDate = new Date(config.todayOverrideDateString + "T00:00:00");
|
|
||||||
} else {
|
|
||||||
todayDate = new Date();
|
|
||||||
}
|
|
||||||
|
|
||||||
const todayMonthIndex = todayDate.getMonth() + 1;
|
|
||||||
const todayYearNum = todayDate.getFullYear();
|
|
||||||
// TODO - set up currentMonthStart if direction is PAST:
|
|
||||||
// let currentMonthStart = new Date(todayYearNum, todayMonthIndex - 1, 1);
|
|
||||||
const currentMonthEnd = new Date(todayYearNum, todayMonthIndex, 0);
|
|
||||||
|
|
||||||
let calendarViewDirection = "none";
|
let calendarViewDirection = "none";
|
||||||
if (config.selectableDatesSetting === "past") calendarViewDirection = "past";
|
|
||||||
if (config.selectableDatesSetting === "custom") calendarViewDirection = "future";
|
|
||||||
|
|
||||||
const initialViewWeeks = this.getInitialViewWeeks(
|
|
||||||
todayDate,
|
|
||||||
config.initialViewRowsToShow,
|
|
||||||
config.preSelectedDate
|
|
||||||
);
|
|
||||||
const initialViewStartDate = todayDate;
|
|
||||||
const initialViewEndDate = initialViewWeeks[initialViewWeeks.length - 1].weekEndDate;
|
|
||||||
const firstSaturdayMonth = initialViewWeeks[0].weekEndDate.getMonth();
|
|
||||||
const lastSundayMonth =
|
|
||||||
initialViewWeeks[initialViewWeeks.length - 1].weekStartDate.getMonth();
|
|
||||||
let hideSomeDaysForInitialView = false;
|
let hideSomeDaysForInitialView = false;
|
||||||
let hideSecondMonth = false;
|
let hideSecondMonth = false;
|
||||||
|
|
||||||
// TODO - NEED TO CREATE OPPOSITE LOGIC FOR "past" DIRECTION LIKE BELOW v v v
|
if (this.todayString) {
|
||||||
if (calendarViewDirection === "future" && !config.preSelectedDate) {
|
todayDateString = this.todayString;
|
||||||
if (firstSaturdayMonth !== lastSundayMonth) {
|
} else if (config.todayOverrideDateString) {
|
||||||
hideSomeDaysForInitialView = true;
|
todayDateString = config.todayOverrideDateString;
|
||||||
}
|
} else {
|
||||||
if (initialViewStartDate.getMonth() === lastSundayMonth) {
|
todayDateString = convertDateToDateString(new Date());
|
||||||
hideSecondMonth = true;
|
}
|
||||||
if (currentMonthEnd > initialViewEndDate) {
|
if (config.selectableDatesSetting === "past") calendarViewDirection = "past";
|
||||||
// should part of 1st month be hidden?
|
if (config.selectableDatesSetting === "custom") calendarViewDirection = "future";
|
||||||
|
|
||||||
|
const currentMonthEnd = this.getMonthEnd(todayDateString);
|
||||||
|
// TODO - set up currentMonthStart if direction is PAST:
|
||||||
|
// let currentMonthStart = new Date(todayYearNum, todayMonthIndex - 1, 1);
|
||||||
|
const initialViewWeeks = this.getInitialViewWeeks(
|
||||||
|
todayDateString,
|
||||||
|
config.initialViewRowsToShow,
|
||||||
|
config.preSelectedDate
|
||||||
|
);
|
||||||
|
const initialViewStartDate = todayDateString;
|
||||||
|
const initialViewEndDate = initialViewWeeks[initialViewWeeks.length - 1].weekEndDate;
|
||||||
|
|
||||||
|
if (!config.preSelectedDate) {
|
||||||
|
const firstSaturdayMonth = initialViewWeeks[0].weekEndDate.split("-")[1];
|
||||||
|
const lastSundayMonth =
|
||||||
|
initialViewWeeks[initialViewWeeks.length - 1].weekStartDate.split("-")[1];
|
||||||
|
|
||||||
|
if (calendarViewDirection === "future") {
|
||||||
|
if (firstSaturdayMonth !== lastSundayMonth) {
|
||||||
hideSomeDaysForInitialView = true;
|
hideSomeDaysForInitialView = true;
|
||||||
}
|
}
|
||||||
|
if (initialViewStartDate.split("-")[1] === lastSundayMonth) {
|
||||||
|
hideSecondMonth = true;
|
||||||
|
if (currentMonthEnd > initialViewEndDate) {
|
||||||
|
// should part of 1st month be hidden?
|
||||||
|
hideSomeDaysForInitialView = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// TODO - NEED TO CREATE OPPOSITE LOGIC FOR "past" DIRECTION LIKE THE ABOVE ^ ^ ^
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadInitialDataPromise = new Promise((resolve, reject) => {
|
const loadInitialDataPromise = new Promise((resolve, reject) => {
|
||||||
const response = config.customSelectableDatesCallback(
|
const response = config.customSelectableDatesCallback(
|
||||||
initialViewStartDate.toISOString().split("T")[0],
|
initialViewStartDate,
|
||||||
initialViewEndDate.toISOString().split("T")[0],
|
initialViewEndDate,
|
||||||
store.getters.order.serviceLocation.appointmentType,
|
store.getters.order.serviceLocation.appointmentType,
|
||||||
store.getters.order.serviceLocation.provider.providerNumber
|
store.getters.order.serviceLocation.provider.providerNumber
|
||||||
);
|
);
|
||||||
|
|
@ -313,7 +341,7 @@ export default {
|
||||||
|
|
||||||
return loadInitialDataPromise.then((response) => {
|
return loadInitialDataPromise.then((response) => {
|
||||||
const initialData = {
|
const initialData = {
|
||||||
todayDate: todayDate,
|
todayDate: todayDateString,
|
||||||
initialViewStartDate: initialViewStartDate,
|
initialViewStartDate: initialViewStartDate,
|
||||||
initialViewEndDate: initialViewEndDate,
|
initialViewEndDate: initialViewEndDate,
|
||||||
calendarViewDirection: calendarViewDirection,
|
calendarViewDirection: calendarViewDirection,
|
||||||
|
|
@ -329,7 +357,7 @@ export default {
|
||||||
this.hideSomeDaysForInitialView = config.hideSomeDaysForInitialView;
|
this.hideSomeDaysForInitialView = config.hideSomeDaysForInitialView;
|
||||||
const hideSecondMonth = config.hideSecondMonth;
|
const hideSecondMonth = config.hideSecondMonth;
|
||||||
const direction = config.calendarViewDirection;
|
const direction = config.calendarViewDirection;
|
||||||
const monthsAfterToLoadOffset = 12;
|
const monthsAfterToLoadOffset = 6;
|
||||||
const monthsBeforeToLoadOffset = 36;
|
const monthsBeforeToLoadOffset = 36;
|
||||||
config.initialShopTimeSlotsResponse.days.forEach((selectableDate) => {
|
config.initialShopTimeSlotsResponse.days.forEach((selectableDate) => {
|
||||||
this.selectableDatesData.push(selectableDate);
|
this.selectableDatesData.push(selectableDate);
|
||||||
|
|
@ -371,6 +399,13 @@ export default {
|
||||||
const monthToShow = this.months.find((month) =>
|
const monthToShow = this.months.find((month) =>
|
||||||
month.monthClass.includes("month-preselected")
|
month.monthClass.includes("month-preselected")
|
||||||
);
|
);
|
||||||
|
if (
|
||||||
|
monthToShow.monthClass.includes("month-preselected") &&
|
||||||
|
monthToShow.monthClass.includes("last-available-month")
|
||||||
|
) {
|
||||||
|
// disable View More dates button if the preSelectedDate in the last available month
|
||||||
|
this.disableViewMoreDatesButton = true;
|
||||||
|
}
|
||||||
this.scrollToElement(monthToShow.monthString);
|
this.scrollToElement(monthToShow.monthString);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -390,32 +425,37 @@ export default {
|
||||||
this.hideSomeDaysForInitialView (string)
|
this.hideSomeDaysForInitialView (string)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
let monthIndex = this.todayMonthIndex + offset; // mutable
|
let monthNum = convertDateStringToDate(this.todayString).getMonth() + offset + 1;
|
||||||
let yearNum = this.todayYearNum; // mutable
|
let yearNum = convertDateStringToDate(this.todayString).getFullYear();
|
||||||
const calendarViewDirection = options.calendarViewDirection;
|
const calendarViewDirection = options.calendarViewDirection;
|
||||||
const initialViewStartDate = options.initialViewStartDate;
|
|
||||||
const initialViewEndDate = options.initialViewEndDate;
|
|
||||||
const hideSecondMonth = options.hideSecondMonth;
|
|
||||||
const preSelectedDateObj = options.preSelectedDate
|
|
||||||
? new Date(options.preSelectedDate + "T00:00:00")
|
|
||||||
: null;
|
|
||||||
const dates = [];
|
|
||||||
let monthClass = "";
|
|
||||||
let isMonthThatHidesSomeDaysForInitialView;
|
|
||||||
|
|
||||||
if (calendarViewDirection === "future" && offset > 0) {
|
if (calendarViewDirection === "future" && offset > 0) {
|
||||||
while (monthIndex > 12) {
|
while (monthNum > 12) {
|
||||||
monthIndex = monthIndex - 12;
|
monthNum = monthNum - 12;
|
||||||
yearNum++;
|
yearNum++;
|
||||||
}
|
}
|
||||||
} else if (calendarViewDirection === "past" && offset < 0) {
|
} else if (calendarViewDirection === "past" && offset < 0) {
|
||||||
while (monthIndex < 1) {
|
while (monthNum < 1) {
|
||||||
monthIndex = 12 + monthIndex;
|
monthNum = 12 + monthNum;
|
||||||
yearNum--;
|
yearNum--;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const initialViewEndDate = options.initialViewEndDate;
|
||||||
|
// const initialViewStartDate = options.initialViewStartDate; // TODO: to be used for past calendarViewDirection
|
||||||
|
const hideSecondMonth = options.hideSecondMonth;
|
||||||
|
const preSelectedDateObj = options.preSelectedDate
|
||||||
|
? convertDateStringToDate(options.preSelectedDate)
|
||||||
|
: null;
|
||||||
|
const dates = [];
|
||||||
|
const monthEndDate = new Date(yearNum, monthNum, 0);
|
||||||
|
const monthStartDateNum =
|
||||||
|
offset === 0 && calendarViewDirection === "future"
|
||||||
|
? this.currentWeekStartDateNum
|
||||||
|
: 1;
|
||||||
|
const monthStartDate = new Date(yearNum, monthNum - 1, monthStartDateNum);
|
||||||
|
const startDateDayIndex = monthStartDate.getDay();
|
||||||
|
|
||||||
const monthEndDate = new Date(yearNum, monthIndex, 0);
|
let monthClass = "";
|
||||||
|
let isMonthThatHidesSomeDaysForInitialView;
|
||||||
let monthEndDateNum = monthEndDate.getDate();
|
let monthEndDateNum = monthEndDate.getDate();
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|
@ -426,16 +466,7 @@ export default {
|
||||||
monthEndDateNum = this.currentWeekEndDateNum;
|
monthEndDateNum = this.currentWeekEndDateNum;
|
||||||
}
|
}
|
||||||
|
|
||||||
const monthStartDateNum =
|
if (options.preSelectedDate) {
|
||||||
offset === 0 && calendarViewDirection === "future"
|
|
||||||
? this.currentWeekStartDateNum
|
|
||||||
: 1;
|
|
||||||
const monthStartDate = new Date(yearNum, monthIndex - 1, monthStartDateNum);
|
|
||||||
|
|
||||||
const startDateDayIndex = monthStartDate.getDay();
|
|
||||||
const endDateDayIndex = monthEndDate.getDay();
|
|
||||||
|
|
||||||
if (preSelectedDateObj) {
|
|
||||||
if (
|
if (
|
||||||
monthStartDate.getFullYear() === preSelectedDateObj.getFullYear() &&
|
monthStartDate.getFullYear() === preSelectedDateObj.getFullYear() &&
|
||||||
monthStartDate.getMonth() === preSelectedDateObj.getMonth()
|
monthStartDate.getMonth() === preSelectedDateObj.getMonth()
|
||||||
|
|
@ -472,25 +503,28 @@ export default {
|
||||||
const dateString =
|
const dateString =
|
||||||
yearNum.toString() +
|
yearNum.toString() +
|
||||||
"-" +
|
"-" +
|
||||||
forceTwoDigitString(monthIndex) +
|
("0" + monthNum).slice(-2) +
|
||||||
"-" +
|
"-" +
|
||||||
forceTwoDigitString(i);
|
("0" + i).slice(-2);
|
||||||
|
|
||||||
if (offset === 0 && i === this.todayDateNum) {
|
if (offset === 0 && i === this.todayDateNum) {
|
||||||
dayClasses += "current-day";
|
dayClasses += " current-day";
|
||||||
}
|
}
|
||||||
if (offset === 0 && i < this.todayDateNum && calendarViewDirection === "future") {
|
if (offset === 0 && i < this.todayDateNum && calendarViewDirection === "future") {
|
||||||
dayClasses += "unavailable-day";
|
dayClasses += " unavailable-day";
|
||||||
}
|
}
|
||||||
if (offset === 0 && i > this.todayDateNum && calendarViewDirection === "past") {
|
if (offset === 0 && i > this.todayDateNum && calendarViewDirection === "past") {
|
||||||
dayClasses += "unavailable-day";
|
dayClasses += " unavailable-day";
|
||||||
|
}
|
||||||
|
if (convertDateStringToDate(dateString).getDay() === 0) {
|
||||||
|
dayClasses += " sunday";
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
this.hideSomeDaysForInitialView &&
|
this.hideSomeDaysForInitialView &&
|
||||||
initialViewEndDate.getMonth() + 1 === monthIndex &&
|
convertDateStringToDate(initialViewEndDate).getMonth() + 1 === monthNum &&
|
||||||
initialViewEndDate.getDate() < i
|
convertDateStringToDate(initialViewEndDate).getDate() < i
|
||||||
) {
|
) {
|
||||||
dayClasses += "day-hidden";
|
dayClasses += " day-hidden";
|
||||||
isMonthThatHidesSomeDaysForInitialView = true;
|
isMonthThatHidesSomeDaysForInitialView = true;
|
||||||
}
|
}
|
||||||
const dateObject = {
|
const dateObject = {
|
||||||
|
|
@ -506,9 +540,9 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
const monthToAdd = {
|
const monthToAdd = {
|
||||||
monthLabel: MONTHS_OF_YEAR[monthIndex - 1],
|
monthLabel: MONTHS_OF_YEAR[monthNum - 1],
|
||||||
monthIndex: monthIndex,
|
monthIndex: monthNum,
|
||||||
monthString: MONTHS_OF_YEAR[monthIndex - 1] + "-" + yearNum?.toString(),
|
monthString: MONTHS_OF_YEAR[monthNum - 1] + "-" + yearNum?.toString(),
|
||||||
yearNum: yearNum,
|
yearNum: yearNum,
|
||||||
dates: dates,
|
dates: dates,
|
||||||
startDateDayIndex: startDateDayIndex,
|
startDateDayIndex: startDateDayIndex,
|
||||||
|
|
@ -626,28 +660,28 @@ export default {
|
||||||
max-height: 0;
|
max-height: 0;
|
||||||
}
|
}
|
||||||
.date-picker {
|
.date-picker {
|
||||||
overflow: hidden;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 100%;
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
fieldset {
|
fieldset {
|
||||||
overflow-y: auto;
|
flex-grow: 1;
|
||||||
height: 88%;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
.loader {
|
.loader {
|
||||||
position: absolute;
|
|
||||||
height: 2rem;
|
height: 2rem;
|
||||||
width: 2rem;
|
width: calc(100% - 1.5rem);
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
width: 100%;
|
width: 1.5rem;
|
||||||
height: 100%;
|
height: 1.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.calendar-grid-container {
|
.calendar-grid-container {
|
||||||
margin: 0 auto 2rem auto;
|
margin: 0 auto 2rem auto;
|
||||||
max-width: 414px;
|
max-width: 414px;
|
||||||
|
position: relative;
|
||||||
transition: height ease 2s, opacity ease 2s;
|
transition: height ease 2s, opacity ease 2s;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(7, 1fr);
|
grid-template-columns: repeat(7, 1fr);
|
||||||
|
|
@ -659,6 +693,8 @@ export default {
|
||||||
.grid-item {
|
.grid-item {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin: 10px 3px;
|
margin: 10px 3px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
|
||||||
&.first-day-,
|
&.first-day-,
|
||||||
&.first-day-0 {
|
&.first-day-0 {
|
||||||
|
|
@ -687,7 +723,7 @@ export default {
|
||||||
.separator-line {
|
.separator-line {
|
||||||
grid-area: 2/1/2/8;
|
grid-area: 2/1/2/8;
|
||||||
border-top: 1px solid $gray-500;
|
border-top: 1px solid $gray-500;
|
||||||
margin: 0.75rem 0;
|
margin: 0.5rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.month-year {
|
.month-year {
|
||||||
|
|
@ -717,7 +753,7 @@ export default {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
outline: none;
|
outline: none;
|
||||||
height: 1.35rem;
|
height: 1.5rem;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transition: height ease 250ms, opacity ease 250ms;
|
transition: height ease 250ms, opacity ease 250ms;
|
||||||
|
|
||||||
|
|
@ -735,12 +771,6 @@ export default {
|
||||||
box-shadow: 0 0 0 3px #fff, 0 0 0 5.5px #1574a1;
|
box-shadow: 0 0 0 3px #fff, 0 0 0 5.5px #1574a1;
|
||||||
background-color: $blue;
|
background-color: $blue;
|
||||||
color: $white;
|
color: $white;
|
||||||
&.past-day {
|
|
||||||
box-shadow: none;
|
|
||||||
background-color: transparent;
|
|
||||||
color: $gray-500;
|
|
||||||
font-weight: normal;
|
|
||||||
}
|
|
||||||
&.current-day {
|
&.current-day {
|
||||||
&:after {
|
&:after {
|
||||||
background-color: $white;
|
background-color: $white;
|
||||||
|
|
@ -754,7 +784,6 @@ export default {
|
||||||
&:checked + label {
|
&:checked + label {
|
||||||
color: $white;
|
color: $white;
|
||||||
background: $blue;
|
background: $blue;
|
||||||
box-shadow: 0 0 0 3px $white, 0 0 0 5.5px $blue;
|
|
||||||
&:after {
|
&:after {
|
||||||
background-color: $white;
|
background-color: $white;
|
||||||
}
|
}
|
||||||
|
|
@ -779,10 +808,9 @@ export default {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 36px;
|
min-width: 2.5rem;
|
||||||
height: 36px;
|
width: 2.5rem;
|
||||||
min-width: 36px;
|
height: 2.5rem;
|
||||||
border-radius: 50%;
|
|
||||||
|
|
||||||
span {
|
span {
|
||||||
&.small {
|
&.small {
|
||||||
|
|
@ -821,26 +849,43 @@ export default {
|
||||||
color: $blue;
|
color: $blue;
|
||||||
background-color: $blue-100;
|
background-color: $blue-100;
|
||||||
border: 1px solid $blue;
|
border: 1px solid $blue;
|
||||||
|
min-width: 2.5rem;
|
||||||
|
width: 2.5rem;
|
||||||
|
border-radius: 50%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&.past-day,
|
|
||||||
&.future-day,
|
|
||||||
&.unavailable-day {
|
&.unavailable-day {
|
||||||
label {
|
label {
|
||||||
color: $gray-500;
|
color: $gray-500;
|
||||||
background-color: transparent;
|
background-color: $gray-100;
|
||||||
border: 1px solid transparent;
|
border: none;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
&.unavailable-day:not(&.sunday) {
|
||||||
|
label {
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
display: inline-block;
|
||||||
|
position: absolute;
|
||||||
|
left: -1rem;
|
||||||
|
width: 1rem;
|
||||||
|
height: 2.5rem;
|
||||||
|
background: $gray-100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
&.current-day {
|
&.current-day {
|
||||||
label {
|
label {
|
||||||
|
font-weight: 500;
|
||||||
|
color: $black;
|
||||||
|
|
||||||
&:after {
|
&:after {
|
||||||
content: "";
|
content: "";
|
||||||
width: 0.25rem;
|
width: 0.25rem;
|
||||||
height: 0.25rem;
|
height: 0.25rem;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background-color: $blue;
|
background-color: $black;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 28px;
|
top: 28px;
|
||||||
}
|
}
|
||||||
|
|
@ -864,15 +909,12 @@ export default {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
max-height: 0;
|
max-height: 0;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
&.last-available-month:not(&.month-hidden) {
|
&.last-available-month:not(&.month-hidden) {
|
||||||
margin-bottom: 14rem;
|
margin-bottom: 14rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#bottom-spacer {
|
|
||||||
height: 20rem;
|
|
||||||
background: lightblue;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-link {
|
.btn-link {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,4 @@ const requiredParameter = () => {
|
||||||
throw new Error("parameter is required");
|
throw new Error("parameter is required");
|
||||||
};
|
};
|
||||||
|
|
||||||
const forceTwoDigitString = (monthNum) => {
|
export { selectableDaysOptions, requiredParameter };
|
||||||
const newString = monthNum.toString();
|
|
||||||
return newString.length === 1 ? "0" + newString : newString;
|
|
||||||
};
|
|
||||||
|
|
||||||
export { selectableDaysOptions, requiredParameter, forceTwoDigitString };
|
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ export default {
|
||||||
isDisabled: Boolean,
|
isDisabled: Boolean,
|
||||||
isRequired: Boolean,
|
isRequired: Boolean,
|
||||||
validationRules: String,
|
validationRules: String,
|
||||||
|
placeHolderText: String,
|
||||||
cmsWidgetName: String,
|
cmsWidgetName: String,
|
||||||
hasError: Boolean,
|
hasError: Boolean,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -139,6 +139,13 @@ export default {
|
||||||
color: rgb(0, 0, 0);
|
color: rgb(0, 0, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.modal-footer {
|
||||||
|
position: sticky;
|
||||||
|
width: 100%;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 5;
|
||||||
|
background-color: $gray-100;
|
||||||
|
}
|
||||||
&.modal-component {
|
&.modal-component {
|
||||||
.modal-dialog {
|
.modal-dialog {
|
||||||
max-width: 576px;
|
max-width: 576px;
|
||||||
|
|
|
||||||
|
|
@ -65,9 +65,9 @@
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="errorMessage" class="row my-1 form-test-error">
|
<div v-show="errorMessage" class="row form-test-error">
|
||||||
<span
|
<span
|
||||||
class="d-inline-flex small mt-0"
|
class="d-inline-flex small my-1"
|
||||||
role="alert"
|
role="alert"
|
||||||
:class="[centerErrorMessage ? 'center-error-message' : '']"
|
:class="[centerErrorMessage ? 'center-error-message' : '']"
|
||||||
>{{ errorMessage }}</span
|
>{{ errorMessage }}</span
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
:isDisabled="isForwardActionDisabled"
|
:isDisabled="isForwardActionDisabled"
|
||||||
@click-event="buttonClick"
|
@click-event="buttonClick"
|
||||||
data-bs-target="#footerModal"
|
data-bs-target="#footerModal"
|
||||||
|
data-test-id="funnel-footer-main-button"
|
||||||
data-bs-dismiss="modal" />
|
data-bs-dismiss="modal" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!isBackButtonHidden" class="col-auto link-col py-1 text-break">
|
<div v-if="!isBackButtonHidden" class="col-auto link-col py-1 text-break">
|
||||||
|
|
|
||||||
|
|
@ -39,9 +39,11 @@ export default {
|
||||||
}
|
}
|
||||||
const pageName = analyticsMixIn.methods.getPageName();
|
const pageName = analyticsMixIn.methods.getPageName();
|
||||||
const nextPageName = router.lastNavigationPage || pageName;
|
const nextPageName = router.lastNavigationPage || pageName;
|
||||||
|
const endpointWithoutParams =
|
||||||
|
analyticsMixIn.methods.removeParamsFromEndpoint(endpoint);
|
||||||
analyticsMixIn.methods.pushEventToGA(
|
analyticsMixIn.methods.pushEventToGA(
|
||||||
GaCategories.API_RESPONSE,
|
GaCategories.API_RESPONSE,
|
||||||
`${nextPageName}_${endpoint}`,
|
`${nextPageName}_${endpointWithoutParams}`,
|
||||||
`${GaLabels.SUCCESS}${additionalEventData}`,
|
`${GaLabels.SUCCESS}${additionalEventData}`,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -296,7 +296,7 @@ export function doesCopyContainTextLink(copy) {
|
||||||
* @returns array of strings
|
* @returns array of strings
|
||||||
*/
|
*/
|
||||||
export function splitCopyOnCMSPlaceHolder(copy) {
|
export function splitCopyOnCMSPlaceHolder(copy) {
|
||||||
return copy.split(/{(.*?)}/g);
|
return copy.split(/{(.*?)}/g).filter((str) => str.length > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@
|
||||||
v-model="customerModel.emailAddress"
|
v-model="customerModel.emailAddress"
|
||||||
ref="emailAddress"
|
ref="emailAddress"
|
||||||
customInputId="emailAddress"
|
customInputId="emailAddress"
|
||||||
validationRules="email-address-format" />
|
validationRules="email-address-required|email-address-format" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row mb-0">
|
<div class="row mb-0">
|
||||||
|
|
@ -49,6 +49,7 @@ import textBlock from "@/digital-components/text-block/text-block";
|
||||||
// DEFINE VALIDATION RULES
|
// DEFINE VALIDATION RULES
|
||||||
defineRule("first-name-required", required(errorMessages.FIRST_NAME_REQUIRED));
|
defineRule("first-name-required", required(errorMessages.FIRST_NAME_REQUIRED));
|
||||||
defineRule("last-name-required", required(errorMessages.LAST_NAME_REQUIRED));
|
defineRule("last-name-required", required(errorMessages.LAST_NAME_REQUIRED));
|
||||||
|
defineRule("email-address-required", required(errorMessages.EMAIL_ADDRESS_REQUIRED));
|
||||||
defineRule(
|
defineRule(
|
||||||
"email-address-format",
|
"email-address-format",
|
||||||
regex(
|
regex(
|
||||||
|
|
|
||||||
|
|
@ -151,6 +151,7 @@ export default {
|
||||||
if (
|
if (
|
||||||
store.getters.order.vehicle.carId &&
|
store.getters.order.vehicle.carId &&
|
||||||
store.getters.order.serviceLocation.zipCode &&
|
store.getters.order.serviceLocation.zipCode &&
|
||||||
|
store.getters.order.customer.emailAddress &&
|
||||||
store.getters.pageData(fmgPageValues.ADDRESS_VEHICLES)
|
store.getters.pageData(fmgPageValues.ADDRESS_VEHICLES)
|
||||||
) {
|
) {
|
||||||
return true;
|
return true;
|
||||||
|
|
|
||||||
|
|
@ -44,8 +44,9 @@
|
||||||
cmsWidgetName="EmailAddressQuestionWidget"
|
cmsWidgetName="EmailAddressQuestionWidget"
|
||||||
v-model="emailAddress"
|
v-model="emailAddress"
|
||||||
inputId="emailAddress"
|
inputId="emailAddress"
|
||||||
|
isRequired
|
||||||
disableAutoFill
|
disableAutoFill
|
||||||
validationRules="email-address-format" />
|
validationRules="email-address-required|email-address-format" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row mb-2">
|
<div class="row mb-2">
|
||||||
|
|
@ -117,6 +118,7 @@ import { queryStrings } from "@/constants/query-strings";
|
||||||
// Define Validation Rules
|
// Define Validation Rules
|
||||||
defineRule("zip-required", required(errorMessages.SERVICE_ZIP_REQUIRED));
|
defineRule("zip-required", required(errorMessages.SERVICE_ZIP_REQUIRED));
|
||||||
defineRule("zip-format", regex(/(^\d{5}$)|(^\d{5}-\d{4}$)/, errorMessages.SERVICE_ZIP_FORMAT));
|
defineRule("zip-format", regex(/(^\d{5}$)|(^\d{5}-\d{4}$)/, errorMessages.SERVICE_ZIP_FORMAT));
|
||||||
|
defineRule("email-address-required", required(errorMessages.EMAIL_ADDRESS_REQUIRED));
|
||||||
defineRule(
|
defineRule(
|
||||||
"email-address-format",
|
"email-address-format",
|
||||||
regex(
|
regex(
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@
|
||||||
cmsWidgetName="EmailAddressQuestionWidget"
|
cmsWidgetName="EmailAddressQuestionWidget"
|
||||||
v-model="email"
|
v-model="email"
|
||||||
customInputId="email"
|
customInputId="email"
|
||||||
validationRules="email-address-format" />
|
validationRules="email-address-required|email-address-format" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row mb-2">
|
<div class="row mb-2">
|
||||||
|
|
@ -115,6 +115,7 @@ import vinPagesMixin from "@/mixins/vin-pages-mixin";
|
||||||
defineRule("license-plate-required", required(errorMessages.LICENSE_PLATE_REQUIRED));
|
defineRule("license-plate-required", required(errorMessages.LICENSE_PLATE_REQUIRED));
|
||||||
defineRule("registration-zip-required", required(errorMessages.REGISTRATION_ZIP_REQUIRED));
|
defineRule("registration-zip-required", required(errorMessages.REGISTRATION_ZIP_REQUIRED));
|
||||||
defineRule("zip-format", regex(/(^\d{5}$)|(^\d{5}-\d{4}$)/, errorMessages.SERVICE_ZIP_FORMAT));
|
defineRule("zip-format", regex(/(^\d{5}$)|(^\d{5}-\d{4}$)/, errorMessages.SERVICE_ZIP_FORMAT));
|
||||||
|
defineRule("email-address-required", required(errorMessages.EMAIL_ADDRESS_REQUIRED));
|
||||||
defineRule(
|
defineRule(
|
||||||
"email-address-format",
|
"email-address-format",
|
||||||
regex(
|
regex(
|
||||||
|
|
|
||||||
|
|
@ -18,3 +18,30 @@ export function calcDaysBetweenDates(dateString1, dateString2) {
|
||||||
const timeDifference = Math.abs(date2 - date1); // Calculate the time difference in milliseconds
|
const timeDifference = Math.abs(date2 - date1); // Calculate the time difference in milliseconds
|
||||||
return Math.ceil(timeDifference / (1000 * 60 * 60 * 24)); // Convert milliseconds to days
|
return Math.ceil(timeDifference / (1000 * 60 * 60 * 24)); // Convert milliseconds to days
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function convertDateToDateString(date) {
|
||||||
|
// returns YYYY-MM-DD format
|
||||||
|
if (date instanceof Date !== true) return;
|
||||||
|
return (
|
||||||
|
date.getFullYear() +
|
||||||
|
"-" +
|
||||||
|
("0" + (date.getMonth() + 1)).slice(-2) +
|
||||||
|
"-" +
|
||||||
|
("0" + date.getDate()).slice(-2)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convertDateStringToDate(dateString) {
|
||||||
|
// dateString must be YYYY-MM-DD format
|
||||||
|
if (typeof dateString !== "string") return;
|
||||||
|
const dateParts = dateString.split("-");
|
||||||
|
return new Date(dateParts[0], parseInt(dateParts[1]) - 1, dateParts[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sumDateString(dateString, daysToAdd) {
|
||||||
|
// dateString must be YYYY-MM-DD format
|
||||||
|
if (typeof dateString !== "string") return;
|
||||||
|
const date = convertDateStringToDate(dateString);
|
||||||
|
date.setDate(date.getDate() + daysToAdd);
|
||||||
|
return convertDateToDateString(date);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
<textBlock
|
<textBlock
|
||||||
cmsWidgetName="ChangeShopLink"
|
cmsWidgetName="ChangeShopLink"
|
||||||
justifyText="center"
|
justifyText="center"
|
||||||
class="mb-3"
|
class="mb-3 text-link-small"
|
||||||
marginTopSizeOverride="1" />
|
marginTopSizeOverride="1" />
|
||||||
</template>
|
</template>
|
||||||
<location-alerts cmsWidgetPrefix="LocationAlert-" ref="locationAlerts" />
|
<location-alerts cmsWidgetPrefix="LocationAlert-" ref="locationAlerts" />
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
selectableDatesSetting="custom"
|
selectableDatesSetting="custom"
|
||||||
ref="datePicker"
|
ref="datePicker"
|
||||||
v-model="selectedDate"
|
v-model="selectedDate"
|
||||||
|
class="text-link-small"
|
||||||
:customSelectableDatesCallback="getAvailableDatesMethod"
|
:customSelectableDatesCallback="getAvailableDatesMethod"
|
||||||
@date-clicked="openInshopTimeSlotsModal" />
|
@date-clicked="openInshopTimeSlotsModal" />
|
||||||
<time-slot-modal-question
|
<time-slot-modal-question
|
||||||
|
|
@ -63,7 +64,11 @@ import { storeActions } from "@/constants/store-actions";
|
||||||
import { settleAllPromises } from "@/helpers/layout-helper";
|
import { settleAllPromises } from "@/helpers/layout-helper";
|
||||||
import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
|
import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
|
||||||
import { splitCopyOnCMSPlaceHolder } from "@/helpers/cms-content-helper";
|
import { splitCopyOnCMSPlaceHolder } from "@/helpers/cms-content-helper";
|
||||||
import { calcDaysBetweenDates } from "@/layouts/schedule/helpers/schedule-helper";
|
import {
|
||||||
|
calcDaysBetweenDates,
|
||||||
|
convertDateStringToDate,
|
||||||
|
sumDateString,
|
||||||
|
} from "@/layouts/schedule/helpers/schedule-helper";
|
||||||
import { AppointmentTypeStrings, PREMIUM_FEE_PART_TYPE } from "@/constants/schedule-constants";
|
import { AppointmentTypeStrings, PREMIUM_FEE_PART_TYPE } from "@/constants/schedule-constants";
|
||||||
import { errorMessages } from "@/constants/error-messages";
|
import { errorMessages } from "@/constants/error-messages";
|
||||||
import { required } from "@/helpers/validation-rules";
|
import { required } from "@/helpers/validation-rules";
|
||||||
|
|
@ -81,49 +86,41 @@ const getAvailableDates = async (
|
||||||
appointmentType,
|
appointmentType,
|
||||||
providerNumber
|
providerNumber
|
||||||
) => {
|
) => {
|
||||||
var today = new Date();
|
const apiEndDateLimit = sumDateString(startDateString, TIME_SLOTS_CALL_DAYS_LIMIT);
|
||||||
var currentTime = today.getHours() + ":" + today.getMinutes() + ":" + today.getSeconds();
|
|
||||||
|
|
||||||
const apiEndDateLimit = new Date(startDateString + "T" + currentTime);
|
|
||||||
const endDate = new Date(endDateString + "T" + currentTime);
|
|
||||||
apiEndDateLimit.setDate(apiEndDateLimit.getDate() + TIME_SLOTS_CALL_DAYS_LIMIT);
|
|
||||||
|
|
||||||
// how many days are between startDate and endDate?
|
|
||||||
const difference = calcDaysBetweenDates(startDateString, endDateString);
|
const difference = calcDaysBetweenDates(startDateString, endDateString);
|
||||||
const apiCallsCount = Math.ceil(difference / TIME_SLOTS_CALL_DAYS_LIMIT);
|
const apiCallsCount = Math.ceil(difference / TIME_SLOTS_CALL_DAYS_LIMIT);
|
||||||
const storeActionConfigs = [];
|
const storeActionConfigs = [];
|
||||||
const timeSlotsData = {};
|
const timeSlotsData = {};
|
||||||
let apiStartDate = new Date(startDateString + "T" + currentTime);
|
|
||||||
let apiEndDate = apiEndDateLimit;
|
|
||||||
timeSlotsData.days = [];
|
timeSlotsData.days = [];
|
||||||
|
let apiStartDate = startDateString;
|
||||||
|
let apiEndDate = apiEndDateLimit;
|
||||||
|
|
||||||
for (let i = 1; i <= apiCallsCount; i++) {
|
for (let i = 1; i <= apiCallsCount; i++) {
|
||||||
let storeActionConfig;
|
let storeActionConfig;
|
||||||
|
|
||||||
if (i > 1) {
|
if (i > 1) {
|
||||||
apiStartDate = new Date(apiEndDate);
|
apiStartDate = sumDateString(apiEndDate, 1);
|
||||||
apiStartDate.setDate(apiStartDate.getDate() + 1);
|
apiEndDate = sumDateString(apiStartDate, TIME_SLOTS_CALL_DAYS_LIMIT);
|
||||||
apiEndDate = new Date(apiStartDate);
|
|
||||||
apiEndDate.setDate(apiEndDate.getDate() + TIME_SLOTS_CALL_DAYS_LIMIT);
|
if (i === apiCallsCount) {
|
||||||
}
|
apiEndDate = endDateString;
|
||||||
if (i === apiCallsCount) {
|
}
|
||||||
apiEndDate = new Date(endDateString + "T" + currentTime);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (appointmentType === AppointmentTypeStrings.MOBILE) {
|
if (appointmentType === AppointmentTypeStrings.MOBILE) {
|
||||||
storeActionConfig = {
|
storeActionConfig = {
|
||||||
storeAction: storeActions.GET_MOBILE_TIME_SLOTS,
|
storeAction: storeActions.GET_MOBILE_TIME_SLOTS,
|
||||||
payload: {
|
payload: {
|
||||||
startDate: apiStartDate.toISOString().split("T")[0],
|
startDate: apiStartDate,
|
||||||
endDate: apiEndDate.toISOString().split("T")[0],
|
endDate: apiEndDate,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
storeActionConfig = {
|
storeActionConfig = {
|
||||||
storeAction: storeActions.GET_SHOP_TIME_SLOTS,
|
storeAction: storeActions.GET_SHOP_TIME_SLOTS,
|
||||||
payload: {
|
payload: {
|
||||||
startDate: apiStartDate.toISOString().split("T")[0],
|
startDate: apiStartDate,
|
||||||
endDate: apiEndDate.toISOString().split("T")[0],
|
endDate: apiEndDate,
|
||||||
shopAppointmentType: appointmentType,
|
shopAppointmentType: appointmentType,
|
||||||
providerNumber: providerNumber,
|
providerNumber: providerNumber,
|
||||||
},
|
},
|
||||||
|
|
@ -384,12 +381,11 @@ export default {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$refs.funnelFooter.updateButtonText(funnelFooterButtonText);
|
this.$refs.funnelFooter.updateButtonText(funnelFooterButtonText);
|
||||||
},
|
},
|
||||||
convertSelectedDateToShortMonthAndDay(selectedDate) {
|
convertSelectedDateToShortMonthAndDay(selectedDate) {
|
||||||
// This conversion ensures we don't get get GMT induced date changes
|
// This conversion ensures we don't get get GMT induced date changes
|
||||||
const dateObject = new Date(`${selectedDate}T00:00:00`);
|
const dateObject = convertDateStringToDate(selectedDate);
|
||||||
// Ex: April 25
|
// Ex: April 25
|
||||||
return dateObject.toLocaleDateString("en-us", { month: "short", day: "numeric" });
|
return dateObject.toLocaleDateString("en-us", { month: "short", day: "numeric" });
|
||||||
},
|
},
|
||||||
|
|
@ -496,3 +492,18 @@ export default {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.text-link-small {
|
||||||
|
a,
|
||||||
|
.btn-link {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.funnel-sub-header {
|
||||||
|
h5.dark-header {
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@
|
||||||
import modal from "@/digital-components/modal/modal";
|
import modal from "@/digital-components/modal/modal";
|
||||||
import textBlock from "@/digital-components/text-block/text-block";
|
import textBlock from "@/digital-components/text-block/text-block";
|
||||||
import buttonQuestion from "@/digital-components/button-question/button-question";
|
import buttonQuestion from "@/digital-components/button-question/button-question";
|
||||||
|
import { convertDateStringToDate } from "@/layouts/schedule/helpers/schedule-helper";
|
||||||
import timeSlotModalListButton from "./time-slot-modal-list-button/time-slot-modal-list-button";
|
import timeSlotModalListButton from "./time-slot-modal-list-button/time-slot-modal-list-button";
|
||||||
|
|
||||||
// TODO: Move this somewhere more global
|
// TODO: Move this somewhere more global
|
||||||
|
|
@ -55,6 +56,13 @@ import {
|
||||||
PREMIUM_FEE_PART_TYPE,
|
PREMIUM_FEE_PART_TYPE,
|
||||||
RouteCodeFlags,
|
RouteCodeFlags,
|
||||||
} from "@/constants/schedule-constants";
|
} from "@/constants/schedule-constants";
|
||||||
|
const cmsWidgetFieldMappings = {
|
||||||
|
MODAL_CLOSE_BUTTON: "FooterText",
|
||||||
|
SUPPLEMENTAL_INFORMATION: "BodyText",
|
||||||
|
TIME_SLOT_BUTTON: "HeaderText",
|
||||||
|
DISCLAIMER: "FooterText",
|
||||||
|
DURATION: "SubheaderText",
|
||||||
|
};
|
||||||
|
|
||||||
// Validation for the modal button
|
// Validation for the modal button
|
||||||
defineRule("time-slot-required", required(errorMessages.OPTION_REQUIRED));
|
defineRule("time-slot-required", required(errorMessages.OPTION_REQUIRED));
|
||||||
|
|
@ -124,32 +132,64 @@ export default {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return this.getCmsContent(appointmentTypeCmsWidgetName, "BodyText");
|
return this.getCmsContent(
|
||||||
|
appointmentTypeCmsWidgetName,
|
||||||
|
cmsWidgetFieldMappings.SUPPLEMENTAL_INFORMATION
|
||||||
|
);
|
||||||
},
|
},
|
||||||
footerCloseButtonText() {
|
footerCloseButtonText() {
|
||||||
return this.getCmsContent(this.cmsWidgetName, "FooterText");
|
return this.getCmsContent(
|
||||||
|
this.cmsWidgetName,
|
||||||
|
cmsWidgetFieldMappings.MODAL_CLOSE_BUTTON
|
||||||
|
);
|
||||||
},
|
},
|
||||||
premiumAppointmentButtonText() {
|
premiumAppointmentButtonText() {
|
||||||
return this.getCmsContent(this.mobilePremiumCmsWidgetName, "HeaderText");
|
return this.getCmsContent(
|
||||||
|
this.mobilePremiumCmsWidgetName,
|
||||||
|
cmsWidgetFieldMappings.TIME_SLOT_BUTTON
|
||||||
|
);
|
||||||
},
|
},
|
||||||
dropoffButtonText() {
|
dropoffButtonText() {
|
||||||
return this.getCmsContent(this.dropoffCmsWidgetName, "HeaderText");
|
return this.getCmsContent(
|
||||||
|
this.dropoffCmsWidgetName,
|
||||||
|
cmsWidgetFieldMappings.TIME_SLOT_BUTTON
|
||||||
|
);
|
||||||
|
},
|
||||||
|
sameDayDropoffButtonText() {
|
||||||
|
return this.getCmsContent(
|
||||||
|
this.sameDayDropOffCmsWidgetName,
|
||||||
|
cmsWidgetFieldMappings.TIME_SLOT_BUTTON
|
||||||
|
);
|
||||||
},
|
},
|
||||||
overnightDropoffButtonText() {
|
overnightDropoffButtonText() {
|
||||||
return this.getCmsContent(this.overnightDropOffCmsWidgetName, "HeaderText");
|
return this.getCmsContent(
|
||||||
|
this.overnightDropOffCmsWidgetName,
|
||||||
|
cmsWidgetFieldMappings.TIME_SLOT_BUTTON
|
||||||
|
);
|
||||||
},
|
},
|
||||||
dropoffDisclaimerText() {
|
dropoffDisclaimerText() {
|
||||||
return this.getCmsContent(this.dropoffCmsWidgetName, "FooterText");
|
return this.getCmsContent(this.dropoffCmsWidgetName, cmsWidgetFieldMappings.DISCLAIMER);
|
||||||
|
},
|
||||||
|
sameDayDropOffDisclaimerText() {
|
||||||
|
return this.getCmsContent(
|
||||||
|
this.sameDayDropOffCmsWidgetName,
|
||||||
|
cmsWidgetFieldMappings.DISCLAIMER
|
||||||
|
);
|
||||||
},
|
},
|
||||||
overnightDropOffDisclaimerText() {
|
overnightDropOffDisclaimerText() {
|
||||||
return this.getCmsContent(this.overnightDropOffCmsWidgetName, "FooterText");
|
return this.getCmsContent(
|
||||||
|
this.overnightDropOffCmsWidgetName,
|
||||||
|
cmsWidgetFieldMappings.DISCLAIMER
|
||||||
|
);
|
||||||
},
|
},
|
||||||
disclaimerTextBlockCopy() {
|
disclaimerTextBlockCopy() {
|
||||||
if (this.appointmentType === AppointmentTypeStrings.DROP_OFF) {
|
if (this.appointmentType === AppointmentTypeStrings.DROP_OFF) {
|
||||||
if (this.isSameDay) {
|
if (this.selectedTimeSlotId?.includes(RouteCodeFlags.ALL_DAY_DROP_OFF)) {
|
||||||
return null;
|
if (this.isSameDay) {
|
||||||
} else if (this.selectedTimeSlotId?.includes(RouteCodeFlags.ALL_DAY_DROP_OFF)) {
|
return this.sameDayDropOffDisclaimerText;
|
||||||
return this.dropoffDisclaimerText;
|
} else {
|
||||||
|
return this.dropoffDisclaimerText;
|
||||||
|
}
|
||||||
} else if (this.selectedTimeSlotId?.includes(RouteCodeFlags.OVERNIGHT_DROP_OFF)) {
|
} else if (this.selectedTimeSlotId?.includes(RouteCodeFlags.OVERNIGHT_DROP_OFF)) {
|
||||||
return this.overnightDropOffDisclaimerText;
|
return this.overnightDropOffDisclaimerText;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -160,15 +200,24 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
dropOffDurationText() {
|
dropOffDurationText() {
|
||||||
return this.getCmsContent(this.dropoffCmsWidgetName, "SubheaderText");
|
return this.getCmsContent(this.dropoffCmsWidgetName, cmsWidgetFieldMappings.DURATION);
|
||||||
|
},
|
||||||
|
sameDayDropoffDurationText() {
|
||||||
|
return this.getCmsContent(
|
||||||
|
this.sameDayDropOffCmsWidgetName,
|
||||||
|
cmsWidgetFieldMappings.DURATION
|
||||||
|
);
|
||||||
},
|
},
|
||||||
overnightDropoffDurationText() {
|
overnightDropoffDurationText() {
|
||||||
return this.getCmsContent(this.overnightDropOffCmsWidgetName, "SubheaderText");
|
return this.getCmsContent(
|
||||||
|
this.overnightDropOffCmsWidgetName,
|
||||||
|
cmsWidgetFieldMappings.DURATION
|
||||||
|
);
|
||||||
},
|
},
|
||||||
inshopDurationText() {
|
inshopDurationText() {
|
||||||
const inshopDurationTextWithoutTime = this.getCmsContent(
|
const inshopDurationTextWithoutTime = this.getCmsContent(
|
||||||
this.cmsWidgetName,
|
this.cmsWidgetName,
|
||||||
"SubheaderText"
|
cmsWidgetFieldMappings.DURATION
|
||||||
);
|
);
|
||||||
const inshopDurationTime = this.getDisplayTextForDurationLength(
|
const inshopDurationTime = this.getDisplayTextForDurationLength(
|
||||||
this.estimatedServiceMinutesMinimum,
|
this.estimatedServiceMinutesMinimum,
|
||||||
|
|
@ -185,15 +234,16 @@ export default {
|
||||||
if (this.selectedTimeSlotId?.includes(RouteCodeFlags.OVERNIGHT_DROP_OFF)) {
|
if (this.selectedTimeSlotId?.includes(RouteCodeFlags.OVERNIGHT_DROP_OFF)) {
|
||||||
return this.overnightDropoffDurationText;
|
return this.overnightDropoffDurationText;
|
||||||
} else if (this.selectedTimeSlotId?.includes(RouteCodeFlags.ALL_DAY_DROP_OFF)) {
|
} else if (this.selectedTimeSlotId?.includes(RouteCodeFlags.ALL_DAY_DROP_OFF)) {
|
||||||
return this.dropOffDurationText;
|
if (this.isSameDay) {
|
||||||
|
return this.sameDayDropoffDurationText;
|
||||||
|
} else {
|
||||||
|
return this.dropOffDurationText;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
shouldShowDropoffDisclaimerText() {
|
|
||||||
return this.appointmentType === AppointmentTypeStrings.DROP_OFF && !this.isSameDay;
|
|
||||||
},
|
|
||||||
isSameDay() {
|
isSameDay() {
|
||||||
if (!this.dateAndTimeSlotData) {
|
if (!this.dateAndTimeSlotData) {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -202,14 +252,13 @@ export default {
|
||||||
const todaysDate = new Date().toISOString().split("T")[0];
|
const todaysDate = new Date().toISOString().split("T")[0];
|
||||||
return selectedDate === todaysDate;
|
return selectedDate === todaysDate;
|
||||||
},
|
},
|
||||||
|
|
||||||
dateSelectedReadableDate() {
|
dateSelectedReadableDate() {
|
||||||
if (!this.dateAndTimeSlotData) {
|
if (!this.dateAndTimeSlotData) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// This conversion ensures we don't get get GMT induced date changes
|
// This conversion ensures we don't get get GMT induced date changes
|
||||||
const dateObject = new Date(`${this.dateAndTimeSlotData.date}T00:00:00`);
|
const dateObject = convertDateStringToDate(this.dateAndTimeSlotData.date);
|
||||||
// Ex: Tuesday, April 22
|
// Ex: Tuesday, April 22
|
||||||
return dateObject.toLocaleDateString("en-us", {
|
return dateObject.toLocaleDateString("en-us", {
|
||||||
weekday: "long",
|
weekday: "long",
|
||||||
|
|
@ -303,9 +352,14 @@ export default {
|
||||||
},
|
},
|
||||||
getAvailableTimeSlotsForDropOff(timeSlotsForSelectedDate) {
|
getAvailableTimeSlotsForDropOff(timeSlotsForSelectedDate) {
|
||||||
const availableTimeSlots = timeSlotsForSelectedDate.map((timeSlot) => {
|
const availableTimeSlots = timeSlotsForSelectedDate.map((timeSlot) => {
|
||||||
const buttonLabelValue = timeSlot.id.includes(RouteCodeFlags.OVERNIGHT_DROP_OFF)
|
let buttonLabelValue;
|
||||||
? this.overnightDropoffButtonText
|
if (timeSlot.id.includes(RouteCodeFlags.OVERNIGHT_DROP_OFF)) {
|
||||||
: this.dropoffButtonText;
|
buttonLabelValue = this.overnightDropoffButtonText;
|
||||||
|
} else if (this.isSameDay) {
|
||||||
|
buttonLabelValue = this.sameDayDropoffButtonText;
|
||||||
|
} else {
|
||||||
|
buttonLabelValue = this.dropoffButtonText;
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
value: timeSlot.id,
|
value: timeSlot.id,
|
||||||
buttonLabel: buttonLabelValue,
|
buttonLabel: buttonLabelValue,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,13 @@
|
||||||
import { shallowMount } from "@vue/test-utils";
|
import { shallowMount } from "@vue/test-utils";
|
||||||
import { getMountOptions } from "@/helpers/unit-test-helper.js";
|
import { getMountOptions } from "@/helpers/unit-test-helper.js";
|
||||||
import appointmentTypeQuestion from "./appointment-type-question";
|
import appointmentTypeQuestion from "./appointment-type-question";
|
||||||
|
import store from "@/store";
|
||||||
|
|
||||||
|
store.getters = {
|
||||||
|
damage: {
|
||||||
|
isRepair: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const mockCmsContent = {
|
const mockCmsContent = {
|
||||||
QuestionText: "Choose a service option:",
|
QuestionText: "Choose a service option:",
|
||||||
|
|
@ -42,6 +49,15 @@ const mockMixin = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// reset store after each test
|
||||||
|
store.getters = {
|
||||||
|
damage: {
|
||||||
|
isRepair: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
describe("appointment-type-question.vue", () => {
|
describe("appointment-type-question.vue", () => {
|
||||||
it("Should display all options if both in-shop and mobile are available", async () => {
|
it("Should display all options if both in-shop and mobile are available", async () => {
|
||||||
// Arrange/Act
|
// Arrange/Act
|
||||||
|
|
@ -141,6 +157,45 @@ describe("appointment-type-question.vue", () => {
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
it("Should not display Drop off answer when it is repair order", async () => {
|
||||||
|
// Arrange/Act
|
||||||
|
|
||||||
|
store.getters = {
|
||||||
|
damage: {
|
||||||
|
isRepair: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const { wrapper } = setupMocks({
|
||||||
|
mixins: [mockMixin],
|
||||||
|
props: {
|
||||||
|
cmsWidgetName: cmsWidgetName,
|
||||||
|
isServiceableInshop: true,
|
||||||
|
isServiceableMobile: true,
|
||||||
|
},
|
||||||
|
mountOptions: {
|
||||||
|
attachTo: document.body,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(wrapper.vm.answersToDisplay).toEqual([
|
||||||
|
{
|
||||||
|
AnswerImageUrl: "",
|
||||||
|
Name: "Mobile",
|
||||||
|
SubText: "",
|
||||||
|
SubWidgetName: "",
|
||||||
|
Text: "Mobile",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
AnswerImageUrl: "",
|
||||||
|
Name: "Inshop",
|
||||||
|
SubText: "",
|
||||||
|
SubWidgetName: "",
|
||||||
|
Text: "In-shop",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it("Should display no answers if neither in-shop nor mobile service are available", async () => {
|
it("Should display no answers if neither in-shop nor mobile service are available", async () => {
|
||||||
// Arrange/Act
|
// Arrange/Act
|
||||||
|
|
@ -169,6 +224,8 @@ function setupMocks({ mountOptions, mixins, props, isShallowMount = true }) {
|
||||||
|
|
||||||
if (props) resultingMountOptions.propsData = props;
|
if (props) resultingMountOptions.propsData = props;
|
||||||
|
|
||||||
|
resultingMountOptions.global.mocks["$store"] = store;
|
||||||
|
|
||||||
const wrapper = isShallowMount
|
const wrapper = isShallowMount
|
||||||
? shallowMount(appointmentTypeQuestion, resultingMountOptions)
|
? shallowMount(appointmentTypeQuestion, resultingMountOptions)
|
||||||
: mount(appointmentTypeQuestion, resultingMountOptions);
|
: mount(appointmentTypeQuestion, resultingMountOptions);
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import buttonQuestion from "@/digital-components/button-question/button-question";
|
import buttonQuestion from "@/digital-components/button-question/button-question";
|
||||||
|
import { AppointmentTypeStrings } from "@/constants/schedule-constants";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "appointment-type-question",
|
name: "appointment-type-question",
|
||||||
|
|
@ -40,20 +41,19 @@ export default {
|
||||||
return this.getCmsContent(this.cmsWidgetName, "Answers");
|
return this.getCmsContent(this.cmsWidgetName, "Answers");
|
||||||
},
|
},
|
||||||
answersToDisplay() {
|
answersToDisplay() {
|
||||||
let filteredAnswers;
|
const shouldShowMobile = this.isServiceableMobile;
|
||||||
if (this.isServiceableMobile && this.isServiceableInshop) {
|
const shouldShowInshop = this.isServiceableInshop;
|
||||||
filteredAnswers = this.answersFromCms;
|
const shouldShowDropoff =
|
||||||
} else if (this.isServiceableMobile) {
|
this.isServiceableInshop && !this.$store.getters.damage.isRepair;
|
||||||
filteredAnswers = this.answersFromCms.filter((answer) => answer.Name == "Mobile");
|
return this.answersFromCms
|
||||||
} else if (this.isServiceableInshop) {
|
? this.answersFromCms.filter((answer) => {
|
||||||
filteredAnswers = this.answersFromCms.filter(
|
return (
|
||||||
(answer) => answer.Name == "Inshop" || answer.Name == "Dropoff"
|
(answer.Name == AppointmentTypeStrings.IN_SHOP && shouldShowInshop) ||
|
||||||
);
|
(answer.Name == AppointmentTypeStrings.MOBILE && shouldShowMobile) ||
|
||||||
} else {
|
(answer.Name == AppointmentTypeStrings.DROP_OFF && shouldShowDropoff)
|
||||||
filteredAnswers = [];
|
);
|
||||||
}
|
})
|
||||||
|
: [];
|
||||||
return filteredAnswers;
|
|
||||||
},
|
},
|
||||||
selectedValues: {
|
selectedValues: {
|
||||||
get: function () {
|
get: function () {
|
||||||
|
|
|
||||||
|
|
@ -62,27 +62,10 @@ export async function getAvailabilityRating(
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
|
|
||||||
// Rate the availability for the shop
|
|
||||||
let numberOfAppointmentsPerDay = [];
|
|
||||||
for (let i = 0; i < shopTimeSlots.data.days.length; i++) {
|
|
||||||
numberOfAppointmentsPerDay.push(shopTimeSlots.data.days[i].timeSlots.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
const dateRange = 7;
|
|
||||||
const minimumNumberOfAppointmentsPerDay = 1;
|
|
||||||
const numberOfDaysToEvaluate = 2;
|
const numberOfDaysToEvaluate = 2;
|
||||||
|
const isGoodAvailability =
|
||||||
let daysWithMinimalAppointmentsCount = 0;
|
shopTimeSlots.data.days.filter((x) => x.timeSlots.length > 0).length >=
|
||||||
for (let i = 0; i < dateRange; i++) {
|
numberOfDaysToEvaluate;
|
||||||
if (numberOfAppointmentsPerDay[i] >= minimumNumberOfAppointmentsPerDay) {
|
|
||||||
daysWithMinimalAppointmentsCount++;
|
|
||||||
if (daysWithMinimalAppointmentsCount >= numberOfDaysToEvaluate) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const isGoodAvailability = daysWithMinimalAppointmentsCount >= numberOfDaysToEvaluate;
|
|
||||||
|
|
||||||
const shopStatus = isGoodAvailability ? "high" : "low";
|
const shopStatus = isGoodAvailability ? "high" : "low";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -93,94 +93,54 @@ const mockStoreActionGetServiceabilityDetails = storeActions.GET_SERVICEABILITY_
|
||||||
const mockStoreActionGetShopTimeSlots = storeActions.GET_SHOP_TIME_SLOTS;
|
const mockStoreActionGetShopTimeSlots = storeActions.GET_SHOP_TIME_SLOTS;
|
||||||
|
|
||||||
const mockGetShopTimeSlotsGoodAvailability = {
|
const mockGetShopTimeSlotsGoodAvailability = {
|
||||||
estimatedServiceMinutesMinimum: 0,
|
data: {
|
||||||
estimatedServiceMinutesMaximimum: 0,
|
estimatedServiceMinutesMinimum: 0,
|
||||||
days: [
|
estimatedServiceMinutesMaximimum: 0,
|
||||||
{
|
days: [
|
||||||
date: "string",
|
{
|
||||||
timeSlots: [
|
date: "string",
|
||||||
{
|
timeSlots: [
|
||||||
id: "string",
|
{
|
||||||
startTime: "",
|
id: "string",
|
||||||
endTime: "",
|
startTime: "",
|
||||||
offerPremium: true,
|
endTime: "",
|
||||||
},
|
offerPremium: true,
|
||||||
],
|
},
|
||||||
},
|
],
|
||||||
{
|
},
|
||||||
date: "string",
|
{
|
||||||
timeSlots: [],
|
date: "string",
|
||||||
},
|
timeSlots: [
|
||||||
{
|
{
|
||||||
date: "string",
|
id: "string",
|
||||||
timeSlots: [],
|
startTime: "",
|
||||||
},
|
endTime: "",
|
||||||
{
|
offerPremium: true,
|
||||||
date: "string",
|
},
|
||||||
timeSlots: [],
|
],
|
||||||
},
|
},
|
||||||
{
|
],
|
||||||
date: "string",
|
},
|
||||||
timeSlots: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
date: "string",
|
|
||||||
timeSlots: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
date: "string",
|
|
||||||
timeSlots: [
|
|
||||||
{
|
|
||||||
id: "string",
|
|
||||||
startTime: "",
|
|
||||||
endTime: "",
|
|
||||||
offerPremium: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockGetShopTimeSlotsLowAvailability = {
|
const mockGetShopTimeSlotsLowAvailability = {
|
||||||
estimatedServiceMinutesMinimum: 0,
|
data: {
|
||||||
estimatedServiceMinutesMaximimum: 0,
|
estimatedServiceMinutesMinimum: 0,
|
||||||
days: [
|
estimatedServiceMinutesMaximimum: 0,
|
||||||
{
|
days: [
|
||||||
date: "string",
|
{
|
||||||
timeSlots: [],
|
date: "string",
|
||||||
},
|
timeSlots: [
|
||||||
{
|
{
|
||||||
date: "string",
|
id: "string",
|
||||||
timeSlots: [],
|
startTime: "",
|
||||||
},
|
endTime: "",
|
||||||
{
|
offerPremium: true,
|
||||||
date: "string",
|
},
|
||||||
timeSlots: [],
|
],
|
||||||
},
|
},
|
||||||
{
|
],
|
||||||
date: "string",
|
},
|
||||||
timeSlots: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
date: "string",
|
|
||||||
timeSlots: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
date: "string",
|
|
||||||
timeSlots: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
date: "string",
|
|
||||||
timeSlots: [
|
|
||||||
{
|
|
||||||
id: "string",
|
|
||||||
startTime: "",
|
|
||||||
endTime: "",
|
|
||||||
offerPremium: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
jest.mock("@/mixins/base-mixin.js", () => ({
|
jest.mock("@/mixins/base-mixin.js", () => ({
|
||||||
|
|
@ -311,23 +271,36 @@ describe("service-location-helper.js", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getAvailabilityRating", () => {
|
describe("getAvailabilityRating", () => {
|
||||||
// it("Should return a 'Good' rating", async () => {
|
it("Should return a 'high' rating", async () => {
|
||||||
// // Arrange
|
// Arrange
|
||||||
// const providerNumber = "0000001";
|
const providerNumber = "0000001";
|
||||||
// const expected = "Good";
|
const expected = "high";
|
||||||
// // Act
|
// Act
|
||||||
// const result = await getAvailabilityRating(providerNumber);
|
const result = await getAvailabilityRating(
|
||||||
// // Assert
|
"2023-06-30",
|
||||||
// expect(result).toEqual(expected);
|
"2023-07-06",
|
||||||
// });
|
"Inshop",
|
||||||
// it("Should return a 'Low' rating", async () => {
|
providerNumber
|
||||||
// // Arrange
|
);
|
||||||
// const providerNumber = "0000000";
|
|
||||||
// const expected = "Low";
|
// Assert
|
||||||
// // Act
|
expect(result).toEqual(expected);
|
||||||
// const result = await getAvailabilityRating(providerNumber);
|
});
|
||||||
// // Assert
|
|
||||||
// expect(result).toEqual(expected);
|
it("Should return a 'low' rating", async () => {
|
||||||
// });
|
// Arrange
|
||||||
|
const providerNumber = "0000000";
|
||||||
|
const expected = "low";
|
||||||
|
// Act
|
||||||
|
const result = await getAvailabilityRating(
|
||||||
|
"2023-06-30",
|
||||||
|
"2023-07-06",
|
||||||
|
"Inshop",
|
||||||
|
providerNumber
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,10 @@
|
||||||
<span v-if="!isLoaderDisplayed" class="m-0 button-auxillary-copy">{{
|
<span v-if="!isLoaderDisplayed" class="m-0 button-auxillary-copy">{{
|
||||||
badgeText
|
badgeText
|
||||||
}}</span>
|
}}</span>
|
||||||
<loader v-if="isLoaderDisplayed" :class="['left', 'no-block']" />
|
<loader
|
||||||
|
v-if="isLoaderDisplayed"
|
||||||
|
:loaderPosition="left"
|
||||||
|
:allowPageInteraction="true" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row-two">
|
<div class="row-two">
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,7 @@ export default {
|
||||||
additionalButtonData() {
|
additionalButtonData() {
|
||||||
const startDate = new Date();
|
const startDate = new Date();
|
||||||
const endDate = new Date();
|
const endDate = new Date();
|
||||||
endDate.setDate(startDate.getDate() + 7);
|
endDate.setDate(startDate.getDate() + 6);
|
||||||
|
|
||||||
const formattedStartDate = startDate.toISOString().split("T")[0];
|
const formattedStartDate = startDate.toISOString().split("T")[0];
|
||||||
const formattedEndDate = endDate.toISOString().split("T")[0];
|
const formattedEndDate = endDate.toISOString().split("T")[0];
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
import vehicleQuestion from "@/layouts/vehicle/vehicle-question/vehicle-question";
|
||||||
|
import { shallowMount } from "@vue/test-utils";
|
||||||
|
import { getMountOptions } from "@/helpers/unit-test-helper.js";
|
||||||
|
import store from "@/store";
|
||||||
|
|
||||||
|
jest.mock(
|
||||||
|
"@/store",
|
||||||
|
() => {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
{ virtual: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
describe("vehicle-question.vue", () => {
|
||||||
|
test("Selected value is emitted upon selection.", async () => {
|
||||||
|
//Arrange
|
||||||
|
const { wrapper } = setupMocks({ modelValueProp: "value" });
|
||||||
|
const valueToSelect = "newvalue";
|
||||||
|
|
||||||
|
//Act
|
||||||
|
wrapper.setValue({ selectedValue: valueToSelect });
|
||||||
|
await wrapper.vm.$nextTick();
|
||||||
|
|
||||||
|
//Assert
|
||||||
|
expect(wrapper.emitted()["update:modelValue"][0]).toEqual([{ selectedValue: "newvalue" }]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function setupMocks({ modelValueProp = "", dataFromStoreApi = [] }) {
|
||||||
|
//Mock store
|
||||||
|
store.dispatch = jest.fn(() => dataFromStoreApi);
|
||||||
|
store.getters = { vehicle: { year: 2019, make: "honda", model: "civic", style: "4 Door" } };
|
||||||
|
|
||||||
|
const mountOptions = getMountOptions({
|
||||||
|
store: {
|
||||||
|
dispatch: store.dispatch,
|
||||||
|
getters: store.getters,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
//Mock props
|
||||||
|
const mockMixin = {
|
||||||
|
methods: {
|
||||||
|
getCmsContent: jest.fn(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
mountOptions.propsData = {
|
||||||
|
modelValue: modelValueProp,
|
||||||
|
};
|
||||||
|
|
||||||
|
mountOptions.mixins = [mockMixin];
|
||||||
|
const wrapper = shallowMount(vehicleQuestion, mountOptions);
|
||||||
|
|
||||||
|
return { wrapper };
|
||||||
|
}
|
||||||
62
src/layouts/vehicle/vehicle-question/vehicle-question.vue
Normal file
62
src/layouts/vehicle/vehicle-question/vehicle-question.vue
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
<template>
|
||||||
|
<dropdownQuestion
|
||||||
|
:options="values"
|
||||||
|
disableAutoFill
|
||||||
|
v-model="selectedValue"
|
||||||
|
:isDisabled="!values.length" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import dropdownQuestion from "@/digital-components/dropdown-question/dropdown-question";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "vehicle-question",
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
values: [],
|
||||||
|
selectedIndex: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
modelValue: String,
|
||||||
|
updateValues: Function,
|
||||||
|
},
|
||||||
|
|
||||||
|
components: {
|
||||||
|
dropdownQuestion,
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
selectedValue: {
|
||||||
|
get() {
|
||||||
|
return this.selectedIndex?.toString();
|
||||||
|
},
|
||||||
|
set(newValue) {
|
||||||
|
this.selectedIndex = newValue;
|
||||||
|
newValue = newValue != null && newValue > -1 ? this.values[newValue] : null;
|
||||||
|
this.$emit("update:modelValue", newValue);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async getNewValues(selectedYMMS) {
|
||||||
|
const results = await this.updateValues();
|
||||||
|
this.values = results?.data;
|
||||||
|
if (this.values.length == 1) {
|
||||||
|
this.selectedValue = 0;
|
||||||
|
} else {
|
||||||
|
this.selectedValue =
|
||||||
|
selectedYMMS != null ? this.values.indexOf(selectedYMMS) : null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
clearValues() {
|
||||||
|
this.values = [];
|
||||||
|
this.selectedValue = null;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
91
src/layouts/vehicle/vehicle.spec.js
Normal file
91
src/layouts/vehicle/vehicle.spec.js
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
// Components
|
||||||
|
import vehicle from "@/layouts/vehicle/vehicle.vue";
|
||||||
|
|
||||||
|
// Supporting files
|
||||||
|
import { nextTick } from "vue";
|
||||||
|
import { mount } from "@vue/test-utils";
|
||||||
|
|
||||||
|
describe("vehicle.vue", () => {
|
||||||
|
test('"Continue" button is enabled after YMMS is selected.', async () => {
|
||||||
|
const { wrapper } = setupMocks();
|
||||||
|
|
||||||
|
const continueButton = wrapper.get('[data-test-id="funnel-footer-main-button"]');
|
||||||
|
|
||||||
|
expect(continueButton.attributes()["aria-disabled"]).toBe("false");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("arePagePrerequisitesValid should be true ", async () => {
|
||||||
|
//Arrange
|
||||||
|
const { wrapper } = setupMocks();
|
||||||
|
|
||||||
|
//Act
|
||||||
|
let arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid();
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
//Assert
|
||||||
|
expect(arePagePrerequisitesValid).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("navigation", () => {
|
||||||
|
test("forwardButtonAction should trigger navigateForward", async () => {
|
||||||
|
// Arrange
|
||||||
|
const { wrapper } = setupMocks();
|
||||||
|
wrapper.vm.navigateForward = jest.fn();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await wrapper.vm.forwardButtonAction();
|
||||||
|
|
||||||
|
//Assert
|
||||||
|
expect(wrapper.vm.navigateForward).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const FunnelFooterWidgetMockData = {
|
||||||
|
ForwardButtonText: "Continue",
|
||||||
|
};
|
||||||
|
|
||||||
|
function setupMocks() {
|
||||||
|
const mockRoute = {
|
||||||
|
query: {
|
||||||
|
fmgPage: "vehicle",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockRouter = {
|
||||||
|
navigate: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const wrapper = mount(vehicle, {
|
||||||
|
global: {
|
||||||
|
mixins: [
|
||||||
|
{
|
||||||
|
methods: {
|
||||||
|
getCmsContent: jest.fn((cmsWidgetName, fieldName) => {
|
||||||
|
if (cmsWidgetName === "FunnelFooterWidget") {
|
||||||
|
if (fieldName === "ForwardButtonText") {
|
||||||
|
return FunnelFooterWidgetMockData.ForwardButtonText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}),
|
||||||
|
getFooterInfoBoxHeight: jest.fn(() => 80),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
mocks: {
|
||||||
|
$route: mockRoute,
|
||||||
|
$router: mockRouter,
|
||||||
|
},
|
||||||
|
|
||||||
|
stubs: {
|
||||||
|
FunnelHeader: true,
|
||||||
|
FunnelSubHeader: true,
|
||||||
|
VehicleBanner: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return { mockRoute, mockRouter, wrapper };
|
||||||
|
}
|
||||||
278
src/layouts/vehicle/vehicle.vue
Normal file
278
src/layouts/vehicle/vehicle.vue
Normal file
|
|
@ -0,0 +1,278 @@
|
||||||
|
<template>
|
||||||
|
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm" v-slot="{ meta }">
|
||||||
|
<div class="page-container-grouped-styles position-relative">
|
||||||
|
<funnelHeader cmsWidgetName="FunnelHeaderWidget" />
|
||||||
|
<div class="select-car">
|
||||||
|
<div class="container-fluid pb-2">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<div class="select-car-form rounded">
|
||||||
|
<funnelSubHeader
|
||||||
|
cmsWidgetName="FunnelSubHeaderWidget"
|
||||||
|
class="siteSubHeader" />
|
||||||
|
|
||||||
|
<vehicleQuestion
|
||||||
|
ref="vehicleYearQuestion"
|
||||||
|
class="mb-2 mt-4"
|
||||||
|
v-model="selectedYear"
|
||||||
|
cmsWidgetName="VehicleYearQuestion"
|
||||||
|
:updateValues="updateYearValues"
|
||||||
|
validationRules="year-required"
|
||||||
|
placeHolderText="Select year"
|
||||||
|
inputId="yearQuestionField" />
|
||||||
|
|
||||||
|
<vehicleQuestion
|
||||||
|
ref="vehicleMakeQuestion"
|
||||||
|
class="mb-2 mt-4"
|
||||||
|
v-model="selectedMake"
|
||||||
|
cmsWidgetName="VehicleMakeQuestion"
|
||||||
|
:updateValues="updateMakeValues"
|
||||||
|
validationRules="make-required"
|
||||||
|
placeHolderText="Select make"
|
||||||
|
inputId="makeQuestionField" />
|
||||||
|
|
||||||
|
<vehicleQuestion
|
||||||
|
ref="vehicleModelQuestion"
|
||||||
|
class="mb-2 mt-4"
|
||||||
|
v-model="selectedModel"
|
||||||
|
cmsWidgetName="VehicleModelQuestion"
|
||||||
|
:updateValues="updateModelValues"
|
||||||
|
validationRules="model-required"
|
||||||
|
placeHolderText="Select model"
|
||||||
|
inputId="modelQuestionField" />
|
||||||
|
|
||||||
|
<vehicleQuestion
|
||||||
|
ref="vehicleStyleQuestion"
|
||||||
|
class="mb-2 mt-4"
|
||||||
|
v-model="selectedStyle"
|
||||||
|
cmsWidgetName="VehicleStyleQuestion"
|
||||||
|
:updateValues="updateStyleValues"
|
||||||
|
validationRules="style-required"
|
||||||
|
placeHolderText="Select style"
|
||||||
|
inputId="styleQuestionField" />
|
||||||
|
|
||||||
|
<vehicleBanner
|
||||||
|
cmsWidgetName="VehicleBannerWidget"
|
||||||
|
:displayGenericVehicleImage="displayGeneric"
|
||||||
|
class="mt-5 mb-3"
|
||||||
|
ref="banner" />
|
||||||
|
|
||||||
|
<funnelFooter
|
||||||
|
cmsWidgetName="FunnelFooterWidget"
|
||||||
|
@ForwardClicked="forwardButtonAction"
|
||||||
|
:isForwardActionDisabled="!meta.valid"
|
||||||
|
ref="funnelFooter" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Components
|
||||||
|
import funnelHeader from "@/fmg-components/funnel-header/funnel-header";
|
||||||
|
import funnelFooter from "@/fmg-components/funnel-footer/funnel-footer";
|
||||||
|
import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header";
|
||||||
|
import vehicleBanner from "@/fmg-components/vehicle-banner/vehicle-banner";
|
||||||
|
import vehicleQuestion from "@/layouts/vehicle/vehicle-question/vehicle-question";
|
||||||
|
|
||||||
|
// Supporting files
|
||||||
|
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
|
||||||
|
import { experimentUniverses } from "@/constants/experiments";
|
||||||
|
import { getDeviceIdValue, getSessionKeyValue } from "@/helpers/heritage-integration/cookie-helper";
|
||||||
|
import { settleAllPromises } from "@/helpers/layout-helper";
|
||||||
|
import { Form, defineRule } from "vee-validate";
|
||||||
|
import { required } from "@/helpers/validation-rules";
|
||||||
|
import { errorMessages } from "@/constants/error-messages";
|
||||||
|
import store from "@/store";
|
||||||
|
import { storeActions } from "@/constants/store-actions.js";
|
||||||
|
import baseMixin from "@/mixins/base-mixin.js";
|
||||||
|
|
||||||
|
//define validation rules
|
||||||
|
defineRule("year-required", required(errorMessages.YEAR_REQUIRED));
|
||||||
|
defineRule("make-required", required(errorMessages.MAKE_REQUIRED));
|
||||||
|
defineRule("model-required", required(errorMessages.MODEL_REQUIRED));
|
||||||
|
defineRule("style-required", required(errorMessages.STYLE_REQUIRED));
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "vehicle",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
selectedYear: null,
|
||||||
|
selectedMake: null,
|
||||||
|
selectedModel: null,
|
||||||
|
selectedStyle: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
cmsWidgetName: String,
|
||||||
|
validationRules: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.$refs["vehicleYearQuestion"].getNewValues(this.selectedYearfromStore);
|
||||||
|
},
|
||||||
|
|
||||||
|
async beforeRouteEnter(to, from, next) {
|
||||||
|
// Call APIs
|
||||||
|
|
||||||
|
const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage);
|
||||||
|
const experimentForLogging = store.getters.applicationUser.experiments.find(
|
||||||
|
(e) => e.universeName === experimentUniverses.CONCEPT_FUNNEL
|
||||||
|
);
|
||||||
|
|
||||||
|
// If the concept funnel experiment is found, as it should be when coming from safelite.com, then log the experiment exposure.
|
||||||
|
if (experimentForLogging !== undefined) {
|
||||||
|
// Log experiment exposure
|
||||||
|
baseMixin.methods.dispatchStoreAction(
|
||||||
|
storeActions.LOG_EXPERIMENT_EXPOSURE,
|
||||||
|
{
|
||||||
|
userId: getDeviceIdValue(),
|
||||||
|
sessionKey: getSessionKeyValue(),
|
||||||
|
pageName: to.query.fmgPage,
|
||||||
|
experiment: experimentForLogging,
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Settle promises and get results
|
||||||
|
const promiseResultMap = [
|
||||||
|
{
|
||||||
|
resultKey: "cmsContent",
|
||||||
|
promise: cmsContentPromise,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
let resultMap = await settleAllPromises(promiseResultMap);
|
||||||
|
|
||||||
|
// Call the "next" function to complete the transition to this page.
|
||||||
|
next((vm) => {
|
||||||
|
vm.setCmsContent(resultMap.cmsContent);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
selectedYear(year) {
|
||||||
|
const parsedYear = parseInt(year);
|
||||||
|
this.dispatchStoreAction(storeActions.SAVE_VEHICLE_YEAR, parsedYear);
|
||||||
|
if (year) {
|
||||||
|
this.$refs["vehicleMakeQuestion"].getNewValues(this.selectedMakefromStore);
|
||||||
|
} else {
|
||||||
|
this.$refs["vehicleMakeQuestion"].clearValues();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
selectedMake(make) {
|
||||||
|
this.dispatchStoreAction(storeActions.SAVE_VEHICLE_MAKE, make, false);
|
||||||
|
if (make) {
|
||||||
|
this.$refs["vehicleModelQuestion"].getNewValues(this.selectedModelfromStore);
|
||||||
|
} else {
|
||||||
|
this.$refs["vehicleModelQuestion"].clearValues();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
selectedModel(model) {
|
||||||
|
this.dispatchStoreAction(storeActions.SAVE_VEHICLE_MODEL, model, false);
|
||||||
|
if (model) {
|
||||||
|
this.$refs["vehicleStyleQuestion"].getNewValues(this.selectedStylefromStore);
|
||||||
|
} else {
|
||||||
|
this.$refs["vehicleStyleQuestion"].clearValues();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
selectedStyle(style) {
|
||||||
|
this.dispatchStoreAction(storeActions.SAVE_VEHICLE_STYLE, style, false);
|
||||||
|
this.setVehicle();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
setVehicle() {
|
||||||
|
return this.dispatchStoreAction(this.storeActions.SET_VEHICLE, {
|
||||||
|
year: this.$store.getters.vehicle.year,
|
||||||
|
make: this.$store.getters.vehicle.make,
|
||||||
|
model: this.$store.getters.vehicle.model,
|
||||||
|
style: this.$store.getters.vehicle.style,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
arePagePrerequisitesValid() {
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
async forwardButtonAction() {
|
||||||
|
return this.navigateForward();
|
||||||
|
},
|
||||||
|
|
||||||
|
navigateForward() {
|
||||||
|
this.$router.navigateWithSaving(this.navigationScenarios.CLICKED_FORWARD, this.$route);
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateYearValues() {
|
||||||
|
return await baseMixin.methods.dispatchStoreAction(storeActions.GET_VEHICLE_YEARS, {});
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateMakeValues() {
|
||||||
|
return await baseMixin.methods.dispatchStoreAction(storeActions.GET_VEHICLE_MAKES, {
|
||||||
|
year: store.getters.vehicle.year,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateModelValues() {
|
||||||
|
return await baseMixin.methods.dispatchStoreAction(storeActions.GET_VEHICLE_MODELS, {
|
||||||
|
year: store.getters.vehicle.year,
|
||||||
|
|
||||||
|
make: store.getters.vehicle.make,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateStyleValues() {
|
||||||
|
return baseMixin.methods.dispatchStoreAction(storeActions.GET_VEHICLE_STYLES, {
|
||||||
|
year: store.getters.vehicle.year,
|
||||||
|
make: store.getters.vehicle.make,
|
||||||
|
model: store.getters.vehicle.model,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
displayGeneric() {
|
||||||
|
return !this.selectedStyle;
|
||||||
|
},
|
||||||
|
selectedYearfromStore() {
|
||||||
|
return store.getters.vehicle.year;
|
||||||
|
},
|
||||||
|
selectedMakefromStore() {
|
||||||
|
return store.getters.vehicle.make;
|
||||||
|
},
|
||||||
|
selectedModelfromStore() {
|
||||||
|
return store.getters.vehicle.model;
|
||||||
|
},
|
||||||
|
selectedStylefromStore() {
|
||||||
|
return store.getters.vehicle.style;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
components: {
|
||||||
|
funnelHeader,
|
||||||
|
funnelFooter,
|
||||||
|
funnelSubHeader,
|
||||||
|
vehicleBanner,
|
||||||
|
Form,
|
||||||
|
vehicleQuestion,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.select-car-form {
|
||||||
|
margin-left: 0.75rem;
|
||||||
|
margin-right: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.siteSubHeader {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -59,7 +59,7 @@
|
||||||
v-model="emailAddress"
|
v-model="emailAddress"
|
||||||
customInputId="emailAddress"
|
customInputId="emailAddress"
|
||||||
isRequired
|
isRequired
|
||||||
validationRules="email-address-format" />
|
validationRules="email-address-required|email-address-format" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row mb-0">
|
<div class="row mb-0">
|
||||||
|
|
@ -79,7 +79,12 @@
|
||||||
:manualHeadline="AlertPerfectMatchInsuranceVerifiedHeader"
|
:manualHeadline="AlertPerfectMatchInsuranceVerifiedHeader"
|
||||||
:manualCopy="AlertPerfectMatchInsuranceVerifiedBody"
|
:manualCopy="AlertPerfectMatchInsuranceVerifiedBody"
|
||||||
v-model="customAlertData"
|
v-model="customAlertData"
|
||||||
v-if="vinPopulatedOnPageLoad && isInsuranceVerified"
|
v-if="
|
||||||
|
vinPopulatedOnPageLoad &&
|
||||||
|
isInsuranceVerified &&
|
||||||
|
!displayInvalidZipAlert &&
|
||||||
|
!displayNonServiceableZipAlert
|
||||||
|
"
|
||||||
alertClass="alert-success" />
|
alertClass="alert-success" />
|
||||||
<alert
|
<alert
|
||||||
class="my-4"
|
class="my-4"
|
||||||
|
|
@ -106,7 +111,12 @@
|
||||||
:manualHeadline="AlertPerfectMatchInsuranceNotVerifiedHeader"
|
:manualHeadline="AlertPerfectMatchInsuranceNotVerifiedHeader"
|
||||||
:manualCopy="AlertPerfectMatchInsuranceNotVerifiedBody"
|
:manualCopy="AlertPerfectMatchInsuranceNotVerifiedBody"
|
||||||
v-model="customAlertData"
|
v-model="customAlertData"
|
||||||
v-if="vinPopulatedOnPageLoad && !isInsuranceVerified"
|
v-if="
|
||||||
|
vinPopulatedOnPageLoad &&
|
||||||
|
!isInsuranceVerified &&
|
||||||
|
!displayInvalidZipAlert &&
|
||||||
|
!displayNonServiceableZipAlert
|
||||||
|
"
|
||||||
alertClass="alert-success" />
|
alertClass="alert-success" />
|
||||||
<funnelFooter
|
<funnelFooter
|
||||||
cmsWidgetName="FunnelFooterWidget"
|
cmsWidgetName="FunnelFooterWidget"
|
||||||
|
|
@ -152,6 +162,7 @@ import vinPagesMixin from "@/mixins/vin-pages-mixin";
|
||||||
// DEFINE VALIDATION RULES
|
// DEFINE VALIDATION RULES
|
||||||
defineRule("zip-required", required(errorMessages.SERVICE_ZIP_REQUIRED));
|
defineRule("zip-required", required(errorMessages.SERVICE_ZIP_REQUIRED));
|
||||||
defineRule("zip-format", regex(/(^\d{5}$)|(^\d{5}-\d{4}$)/, errorMessages.SERVICE_ZIP_FORMAT));
|
defineRule("zip-format", regex(/(^\d{5}$)|(^\d{5}-\d{4}$)/, errorMessages.SERVICE_ZIP_FORMAT));
|
||||||
|
defineRule("email-address-required", required(errorMessages.EMAIL_ADDRESS_REQUIRED));
|
||||||
defineRule(
|
defineRule(
|
||||||
"email-address-format",
|
"email-address-format",
|
||||||
regex(
|
regex(
|
||||||
|
|
|
||||||
|
|
@ -176,6 +176,18 @@ export default {
|
||||||
getSessionIdValue() === "00000000-0000-0000-0000-000000000000"
|
getSessionIdValue() === "00000000-0000-0000-0000-000000000000"
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
removeParamsFromEndpoint(endpoint) {
|
||||||
|
const endpointWithoutParams = endpoint.split("?")[0];
|
||||||
|
const numSlashesBeforeParams = 6;
|
||||||
|
let splitString = endpointWithoutParams.split("/");
|
||||||
|
if (splitString.length > numSlashesBeforeParams) {
|
||||||
|
splitString = splitString.slice(0, numSlashesBeforeParams);
|
||||||
|
return splitString.join("/");
|
||||||
|
} else {
|
||||||
|
return endpointWithoutParams;
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
analyticsPageEvents() {
|
analyticsPageEvents() {
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ const fmgPageValues = {
|
||||||
VEHICLE_MAKE: "vehicle-make",
|
VEHICLE_MAKE: "vehicle-make",
|
||||||
VEHICLE_MODEL: "vehicle-model",
|
VEHICLE_MODEL: "vehicle-model",
|
||||||
VEHICLE_STYLE: "vehicle-style",
|
VEHICLE_STYLE: "vehicle-style",
|
||||||
|
VEHICLE: "vehicle",
|
||||||
VEHICLE_DAMAGE: "vehicle-damage",
|
VEHICLE_DAMAGE: "vehicle-damage",
|
||||||
ADDRESS_LOOKUP: "address-lookup",
|
ADDRESS_LOOKUP: "address-lookup",
|
||||||
VIN_LOOKUP: "vin-lookup",
|
VIN_LOOKUP: "vin-lookup",
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,15 @@ import { navigationScenarios } from "@/router/router-constants/navigation-scenar
|
||||||
// Get store from router/index.js instead of importing it here to get updated values
|
// Get store from router/index.js instead of importing it here to get updated values
|
||||||
const routingTable = function (store) {
|
const routingTable = function (store) {
|
||||||
return [
|
return [
|
||||||
|
{
|
||||||
|
fmgPageValue: fmgPageValues.VEHICLE,
|
||||||
|
maps: [
|
||||||
|
{
|
||||||
|
scenario: navigationScenarios.CLICKED_FORWARD,
|
||||||
|
destinationFmgPageValue: fmgPageValues.VEHICLE_DAMAGE,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
fmgPageValue: fmgPageValues.VEHICLE_YEAR,
|
fmgPageValue: fmgPageValues.VEHICLE_YEAR,
|
||||||
maps: [
|
maps: [
|
||||||
|
|
@ -57,7 +66,7 @@ const routingTable = function (store) {
|
||||||
maps: [
|
maps: [
|
||||||
{
|
{
|
||||||
scenario: navigationScenarios.CLICKED_BACK,
|
scenario: navigationScenarios.CLICKED_BACK,
|
||||||
destinationFmgPageValue: fmgPageValues.VEHICLE_STYLE,
|
destinationFmgPageValue: fmgPageValues.VEHICLE,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scenario: navigationScenarios.CLICKED_FORWARD_WITH_VIN,
|
scenario: navigationScenarios.CLICKED_FORWARD_WITH_VIN,
|
||||||
|
|
@ -457,7 +466,7 @@ const routingTable = function (store) {
|
||||||
maps: [
|
maps: [
|
||||||
{
|
{
|
||||||
scenario: navigationScenarios.CLICKED_VEHICLE_EDIT,
|
scenario: navigationScenarios.CLICKED_VEHICLE_EDIT,
|
||||||
destinationFmgPageValue: fmgPageValues.VEHICLE_YEAR,
|
destinationFmgPageValue: fmgPageValues.VEHICLE,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scenario: navigationScenarios.CLICKED_DAMAGE_EDIT,
|
scenario: navigationScenarios.CLICKED_DAMAGE_EDIT,
|
||||||
|
|
|
||||||
|
|
@ -534,6 +534,9 @@ export const getters = {
|
||||||
);
|
);
|
||||||
return !!nonWindshieldItems?.length;
|
return !!nonWindshieldItems?.length;
|
||||||
},
|
},
|
||||||
|
isMobileAppointment: (state) => {
|
||||||
|
return state.order.serviceLocation.appointmentType === AppointmentTypeStrings.MOBILE;
|
||||||
|
},
|
||||||
isRecalibrationOnOrder: (state) => {
|
isRecalibrationOnOrder: (state) => {
|
||||||
return getHasRecalibrationPart(state);
|
return getHasRecalibrationPart(state);
|
||||||
},
|
},
|
||||||
|
|
@ -615,6 +618,10 @@ function getNonFalseValuesOfPropertyInArrayOfObjects(array, propertyName) {
|
||||||
return (array ?? []).map((x) => x[propertyName]).filter((x) => x);
|
return (array ?? []).map((x) => x[propertyName]).filter((x) => x);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function provisionalTriggersToString(provisionalTriggers) {
|
||||||
|
return "ProvisionalTriggers:" + provisionalTriggers.join(",");
|
||||||
|
}
|
||||||
|
|
||||||
// Export Actions
|
// Export Actions
|
||||||
export const actions = {
|
export const actions = {
|
||||||
// Vehicle API Actions
|
// Vehicle API Actions
|
||||||
|
|
@ -1239,13 +1246,16 @@ export const actions = {
|
||||||
const order = context.state.order;
|
const order = context.state.order;
|
||||||
const vehicle = context.state.order.vehicle;
|
const vehicle = context.state.order.vehicle;
|
||||||
|
|
||||||
let partNumbers = [
|
let lineItems = [
|
||||||
...(order.lineItems.supportingItems ?? []),
|
...(order.lineItems.supportingItems ?? []),
|
||||||
...(order.lineItems.vaps ?? []),
|
...(order.lineItems.vaps ?? []),
|
||||||
...getFlattenedArrayOfLineItemsWithChildParts(order.lineItems.glassParts),
|
...getFlattenedArrayOfLineItemsWithChildParts(order.lineItems.glassParts),
|
||||||
];
|
];
|
||||||
partNumbers = partNumbers.map((lineItem) => {
|
lineItems = lineItems.map((lineItem) => {
|
||||||
return lineItem.partNumber;
|
return {
|
||||||
|
partNumber: lineItem.partNumber,
|
||||||
|
partType: lineItem.partType,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
const glassPieces = order.damage.glassToReplace
|
const glassPieces = order.damage.glassToReplace
|
||||||
? convertGlassPieceToBackEndCompatibleFormat(order.damage.glassToReplace)
|
? convertGlassPieceToBackEndCompatibleFormat(order.damage.glassToReplace)
|
||||||
|
|
@ -1258,7 +1268,7 @@ export const actions = {
|
||||||
applicationName: applicationConfig.APPLICATION_NAME,
|
applicationName: applicationConfig.APPLICATION_NAME,
|
||||||
parentAccountNumber: context.getters.payment.parentAccountNumber,
|
parentAccountNumber: context.getters.payment.parentAccountNumber,
|
||||||
carId: vehicle.carId,
|
carId: vehicle.carId,
|
||||||
partNumbers: partNumbers,
|
lineItems: lineItems,
|
||||||
glassPieces: glassPieces,
|
glassPieces: glassPieces,
|
||||||
eon: order.eon,
|
eon: order.eon,
|
||||||
coverage: {
|
coverage: {
|
||||||
|
|
@ -1267,11 +1277,12 @@ export const actions = {
|
||||||
additionalAuthFlag: "",
|
additionalAuthFlag: "",
|
||||||
},
|
},
|
||||||
partSelection: {
|
partSelection: {
|
||||||
// TODO: Provisional booking will utilize these fields
|
hasAnsweredPartQuestions: !!order.damage.partQuestionAnswers?.length,
|
||||||
hasAnsweredPartQuestions: false,
|
hasAnsweredMoldingQuestions: !!order.damage.moldingQuestionAnswers?.length,
|
||||||
hasAnsweredMoldingQuestions: false,
|
hasAnsweredCapabilityQuestions: !!order.damage.capabilityQuestionAnswers?.length,
|
||||||
hasAnsweredCapabilityQuestions: false,
|
hasManuallySelectedParts:
|
||||||
hasManuallySelectedParts: false,
|
!!context.state.applicationUser.pageData["vehicle-parts"]?.partsOrQuestions
|
||||||
|
.length,
|
||||||
},
|
},
|
||||||
vehicle: {
|
vehicle: {
|
||||||
year: vehicle.year,
|
year: vehicle.year,
|
||||||
|
|
@ -1286,19 +1297,24 @@ export const actions = {
|
||||||
method: endpoints.GetShopTimeSlots.method,
|
method: endpoints.GetShopTimeSlots.method,
|
||||||
endpoint: endpoints.GetShopTimeSlots.url,
|
endpoint: endpoints.GetShopTimeSlots.url,
|
||||||
payload: payload,
|
payload: payload,
|
||||||
|
additionalSuccessEventDataHandler: (response) =>
|
||||||
|
provisionalTriggersToString(response.data.provisionalTriggers),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
getMobileTimeSlots(context, { startDate, endDate }) {
|
getMobileTimeSlots(context, { startDate, endDate }) {
|
||||||
const order = context.state.order;
|
const order = context.state.order;
|
||||||
const vehicle = context.state.order.vehicle;
|
const vehicle = context.state.order.vehicle;
|
||||||
let partNumbers = [
|
let lineItems = [
|
||||||
...(order.lineItems.supportingItems ?? []),
|
...(order.lineItems.supportingItems ?? []),
|
||||||
...(order.lineItems.vaps ?? []),
|
...(order.lineItems.vaps ?? []),
|
||||||
...getFlattenedArrayOfLineItemsWithChildParts(order.lineItems.glassParts),
|
...getFlattenedArrayOfLineItemsWithChildParts(order.lineItems.glassParts),
|
||||||
];
|
];
|
||||||
partNumbers = partNumbers.map((lineItem) => {
|
lineItems = lineItems.map((lineItem) => {
|
||||||
return lineItem.partNumber;
|
return {
|
||||||
|
partNumber: lineItem.partNumber,
|
||||||
|
partType: lineItem.partType,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
const glassPieces = order.damage.glassToReplace
|
const glassPieces = order.damage.glassToReplace
|
||||||
? convertGlassPieceToBackEndCompatibleFormat(order.damage.glassToReplace)
|
? convertGlassPieceToBackEndCompatibleFormat(order.damage.glassToReplace)
|
||||||
|
|
@ -1309,7 +1325,7 @@ export const actions = {
|
||||||
applicationName: applicationConfig.APPLICATION_NAME,
|
applicationName: applicationConfig.APPLICATION_NAME,
|
||||||
parentAccountNumber: context.getters.payment.parentAccountNumber,
|
parentAccountNumber: context.getters.payment.parentAccountNumber,
|
||||||
carId: vehicle.carId,
|
carId: vehicle.carId,
|
||||||
partNumbers: partNumbers,
|
lineItems: lineItems,
|
||||||
glassPieces: glassPieces,
|
glassPieces: glassPieces,
|
||||||
eon: order.eon,
|
eon: order.eon,
|
||||||
coverage: {
|
coverage: {
|
||||||
|
|
@ -1318,11 +1334,12 @@ export const actions = {
|
||||||
additionalAuthFlag: "",
|
additionalAuthFlag: "",
|
||||||
},
|
},
|
||||||
partSelection: {
|
partSelection: {
|
||||||
// TODO: Provisional booking will utilize these fields
|
hasAnsweredPartQuestions: !!order.damage.partQuestionAnswers?.length,
|
||||||
hasAnsweredPartQuestions: false,
|
hasAnsweredMoldingQuestions: !!order.damage.moldingQuestionAnswers?.length,
|
||||||
hasAnsweredMoldingQuestions: false,
|
hasAnsweredCapabilityQuestions: !!order.damage.capabilityQuestionAnswers?.length,
|
||||||
hasAnsweredCapabilityQuestions: false,
|
hasManuallySelectedParts:
|
||||||
hasManuallySelectedParts: false,
|
!!context.state.applicationUser.pageData["vehicle-parts"]?.partsOrQuestions
|
||||||
|
.length,
|
||||||
},
|
},
|
||||||
vehicle: {
|
vehicle: {
|
||||||
year: vehicle.year,
|
year: vehicle.year,
|
||||||
|
|
@ -1337,6 +1354,8 @@ export const actions = {
|
||||||
method: endpoints.GetMobileTimeSlots.method,
|
method: endpoints.GetMobileTimeSlots.method,
|
||||||
endpoint: endpoints.GetMobileTimeSlots.url,
|
endpoint: endpoints.GetMobileTimeSlots.url,
|
||||||
payload: payload,
|
payload: payload,
|
||||||
|
additionalSuccessEventDataHandler: (response) =>
|
||||||
|
provisionalTriggersToString(response.data.provisionalTriggers),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -1391,7 +1410,7 @@ export const actions = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
customer: {
|
customer: {
|
||||||
emailAddress: order.customer.emailAddress || null,
|
emailAddress: order.customer.emailAddress,
|
||||||
},
|
},
|
||||||
damage: {
|
damage: {
|
||||||
numberOfChips: damage.numberOfChips,
|
numberOfChips: damage.numberOfChips,
|
||||||
|
|
@ -1960,7 +1979,7 @@ export const actions = {
|
||||||
},
|
},
|
||||||
|
|
||||||
saveEmail(context, email) {
|
saveEmail(context, email) {
|
||||||
context.commit(storeMutations.UPDATE_CUSTOMER_EMAIL_ADDRESS, email);
|
context.commit(storeMutations.UPDATE_CUSTOMER_EMAIL_ADDRESS, email === "" ? null : email);
|
||||||
},
|
},
|
||||||
|
|
||||||
saveVin(context, { isSelectedGlassAvailableForVehicle, vehicleInfo }) {
|
saveVin(context, { isSelectedGlassAvailableForVehicle, vehicleInfo }) {
|
||||||
|
|
@ -2199,6 +2218,7 @@ async function resetScheduleIfUnavailable(context, order) {
|
||||||
// If you put any kind of time stamp on the date string with dashes, then it IS parsed as local time.
|
// If you put any kind of time stamp on the date string with dashes, then it IS parsed as local time.
|
||||||
var aptDate = new Date(order.schedule.date + "T00:00:00");
|
var aptDate = new Date(order.schedule.date + "T00:00:00");
|
||||||
var curDate = new Date();
|
var curDate = new Date();
|
||||||
|
curDate.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
// if appointment date is in the past, clear schedule
|
// if appointment date is in the past, clear schedule
|
||||||
if (aptDate.getTime() < curDate.getTime()) {
|
if (aptDate.getTime() < curDate.getTime()) {
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { storeMutations } from "@/constants/store-mutations";
|
||||||
import { storeActions } from "@/constants/store-actions";
|
import { storeActions } from "@/constants/store-actions";
|
||||||
import { experimentTriggers } from "@/constants/experiments";
|
import { experimentTriggers } from "@/constants/experiments";
|
||||||
import { fmgPageValues } from "@/router/router-constants/fmgPage-values";
|
import { fmgPageValues } from "@/router/router-constants/fmgPage-values";
|
||||||
|
import { AppointmentTypeStrings } from "@/constants/schedule-constants";
|
||||||
|
|
||||||
// Mock global method
|
// Mock global method
|
||||||
globalMethods.callHttpClient = jest.fn();
|
globalMethods.callHttpClient = jest.fn();
|
||||||
|
|
@ -2869,6 +2870,45 @@ describe("Getters", () => {
|
||||||
expect(getters.payment(storeState).insuranceCoverage.isVerified).toEqual(true);
|
expect(getters.payment(storeState).insuranceCoverage.isVerified).toEqual(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("isMobileAppointment", () => {
|
||||||
|
it("Should return true for mobile appointments", () => {
|
||||||
|
// Arrange
|
||||||
|
const storeState = state;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
mutations.updateServiceLocation(storeState, {
|
||||||
|
appointmentType: AppointmentTypeStrings.MOBILE,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(getters.isMobileAppointment(storeState)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should return false for non-mobile appointments", () => {
|
||||||
|
// Arrange
|
||||||
|
const storeState = state;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
mutations.updateServiceLocation(storeState, {
|
||||||
|
appointmentType: AppointmentTypeStrings.IN_SHOP,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(getters.isMobileAppointment(storeState)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should return false for null appointments", () => {
|
||||||
|
// Arrange
|
||||||
|
const storeState = state;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
mutations.updateServiceLocation(storeState, { appointmentType: null });
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(getters.isMobileAppointment(storeState)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("experimentOrder", () => {
|
describe("experimentOrder", () => {
|
||||||
test("glassToReplace, glassParts, and otherParts are null > return correct experimentOrder values", () => {
|
test("glassToReplace, glassParts, and otherParts are null > return correct experimentOrder values", () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,7 @@ export default {
|
||||||
color: $white;
|
color: $white;
|
||||||
background: $blue-700;
|
background: $blue-700;
|
||||||
box-shadow: 0 0 0 3px, 0 0 0 5.5px $blue-700;
|
box-shadow: 0 0 0 3px, 0 0 0 5.5px $blue-700;
|
||||||
pointer-events: none;
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
&.delay {
|
&.delay {
|
||||||
// fixes flicker while transitioning between states
|
// fixes flicker while transitioning between states
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,11 @@
|
||||||
class="loader"
|
class="loader"
|
||||||
role="alert"
|
role="alert"
|
||||||
aria-label="Loading new page"
|
aria-label="Loading new page"
|
||||||
v-bind:class="[this.loaderColor, this.loaderPosition, this.blockUi]"></div>
|
v-bind:class="[
|
||||||
|
this.loaderColor,
|
||||||
|
this.loaderPosition,
|
||||||
|
this.allowPageInteraction ? 'allow-ui-interaction' : '',
|
||||||
|
]"></div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
@ -19,10 +23,11 @@ export default {
|
||||||
loaderPosition: {
|
loaderPosition: {
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
/* Controls whether we block the UI when the loader is active or now, defaults to true*/
|
/* Set to true for loaders where UI interaction must be allowed to continue while the loader is active
|
||||||
blockUi: {
|
(See example shop-list-button) */
|
||||||
|
allowPageInteraction: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -31,6 +36,7 @@ export default {
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.loader {
|
.loader {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
pointer-events: all;
|
||||||
|
|
||||||
//Open an overlay to prevent page interaction
|
//Open an overlay to prevent page interaction
|
||||||
&:before {
|
&:before {
|
||||||
|
|
@ -87,8 +93,8 @@ export default {
|
||||||
&.black:after {
|
&.black:after {
|
||||||
background-color: $black;
|
background-color: $black;
|
||||||
}
|
}
|
||||||
|
&.allow-ui-interaction::before {
|
||||||
&.no-block::before {
|
//no-block to enable clicking on certain buttons with loaders while the loader is actives
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue