diff --git a/src/constants/error-messages.js b/src/constants/error-messages.js
index 415647879..ef5459395 100644
--- a/src/constants/error-messages.js
+++ b/src/constants/error-messages.js
@@ -29,6 +29,10 @@ const errorMessages = {
DATE_REQUIRED: "Please select a date",
PHONE_REQUIRED: "Please enter your phone number",
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 };
diff --git a/src/digital-components/button-question/button-question.vue b/src/digital-components/button-question/button-question.vue
index 2c2c662dc..a53958dd5 100644
--- a/src/digital-components/button-question/button-question.vue
+++ b/src/digital-components/button-question/button-question.vue
@@ -74,7 +74,7 @@ https://safelite.atlassian.net/wiki/spaces/DC/pages/76644418/Button+Question+Com
diff --git a/src/digital-components/date-picker/date-picker.vue b/src/digital-components/date-picker/date-picker.vue
index 368aca135..30cc297b8 100644
--- a/src/digital-components/date-picker/date-picker.vue
+++ b/src/digital-components/date-picker/date-picker.vue
@@ -6,7 +6,7 @@
v-for="month in months"
:key="`${month.monthLabel}-${month.yearNum?.toString()}`"
:id="`${month.monthLabel}-${month.yearNum?.toString()}`"
- class="calendar-grid-container position-relative"
+ class="calendar-grid-container"
:class="[
hideSomeDaysForInitialView ? 'partial-month-initial-view' : '',
month.monthClass,
@@ -56,15 +56,15 @@
:class="[!isLoading ? 'date-picker-hidden' : '']"
loaderColor="blue"
loaderPosition="center" />
+
-
@@ -74,6 +74,10 @@ import loader from "@/ux-components/loader/loader";
import store from "@/store";
import { TIMINGFUNC_MAP, BUFFER_OFFSET, MONTHS_OF_YEAR } from "./mixins/constants";
import { selectableDaysOptions, requiredParameter, forceTwoDigitString } from "./mixins/helpers";
+import {
+ convertDateToDateString,
+ convertDateStringToDate,
+} from "@/layouts/schedule/helpers/schedule-helper";
export default {
name: "datePicker",
@@ -110,23 +114,14 @@ export default {
},
},
computed: {
- today() {
- if (this.todayOverrideDateString) {
- return new Date(this.todayOverrideDateString + "T00:00:00");
- }
- return new Date();
- },
- todayMonthIndex() {
- return this.today.getMonth() + 1;
- },
- todayYearNum() {
- return this.today.getFullYear();
+ todayString() {
+ return this.todayOverrideDateString || convertDateToDateString(new Date());
},
todayDayIndex() {
- return this.today.getDay();
+ return convertDateStringToDate(this.todayString).getDay();
},
todayDateNum() {
- return this.today.getDate();
+ return convertDateStringToDate(this.todayString).getDate();
},
currentWeekStartDateNum() {
return this.todayDayIndex >= this.todayDateNum
@@ -157,154 +152,187 @@ export default {
fireDateClickedEvent() {
this.$emit("date-clicked");
},
- getWeekStartDate(date) {
+ getWeekStartDate(dateString) {
+ const date = convertDateStringToDate(dateString);
const dayOfWeek = date.getDay();
// Subtract the day of the week from date to get the date of Sunday
const sunday = new Date(date);
sunday.setDate(sunday.getDate() - dayOfWeek);
- return sunday;
+ return convertDateToDateString(sunday);
},
- getWeekEndDate(date) {
+ getWeekEndDate(dateString) {
+ const date = convertDateStringToDate(dateString);
const dayOfWeek = date.getDay();
const daysUntilSaturday = 6 - dayOfWeek; // Calculate the number of days until Saturday
// Clone the given date and add the remaining days until Saturday
const saturday = new Date(date);
saturday.setDate(date.getDate() + daysUntilSaturday);
- return saturday;
+ return convertDateToDateString(saturday);
},
- getNextWeekSunday(date) {
+ getNextWeekSunday(dateString) {
+ const date = convertDateStringToDate(dateString);
const dayOfWeek = date.getDay();
const daysUntilNextSunday = 7 - dayOfWeek; // Calculate the number of days until the next Sunday
// Clone the given date and add the remaining days until Sunday
const nextSunday = new Date(date);
nextSunday.setDate(date.getDate() + daysUntilNextSunday);
- return nextSunday;
+ 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
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);
- let weekEndDate = this.getWeekEndDate(today);
- for (let i = 0; i < initialViewRowsToShow; i++) {
- if (i > 0) {
- weekStartDate = this.getNextWeekSunday(weekEndDate);
- weekEndDate = this.getWeekEndDate(weekStartDate);
- }
- weeks.push({
- weekNum: i + 1,
- weekStartDate: weekStartDate,
- weekEndDate: weekEndDate,
- });
- 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;
+ if (
+ (preSelectedDateMonthEnd > weekStartDate &&
+ preSelectedDateMonthEnd < weekEndDate) ||
+ preSelectedDateMonthEnd === weekStartDate ||
+ preSelectedDateMonthEnd === weekEndDate
+ ) {
+ weekEndDate = preSelectedDateMonthEnd;
+ weekIncludesPreSelectedMonthEnd = true;
}
+ }
+ 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;
},
async loadInitialData(config) {
- let todayDate;
- if (this.today) {
- todayDate = this.today;
- } else if (config.todayOverrideDateString) {
- 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);
-
+ /*
+ ** NOTE: this _could_ be called by a parent before fully loaded, so data or computeds might not be available
+ */
+ let todayDateString;
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 hideSecondMonth = false;
- // TODO - NEED TO CREATE OPPOSITE LOGIC FOR "past" DIRECTION LIKE BELOW v v v
- if (calendarViewDirection === "future" && !config.preSelectedDate) {
- if (firstSaturdayMonth !== lastSundayMonth) {
- hideSomeDaysForInitialView = true;
- }
- if (initialViewStartDate.getMonth() === lastSundayMonth) {
- hideSecondMonth = true;
- if (currentMonthEnd > initialViewEndDate) {
- // should part of 1st month be hidden?
+ if (this.todayString) {
+ todayDateString = this.todayString;
+ } else if (config.todayOverrideDateString) {
+ todayDateString = config.todayOverrideDateString;
+ } else {
+ todayDateString = convertDateToDateString(new Date());
+ }
+ if (config.selectableDatesSetting === "past") calendarViewDirection = "past";
+ 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;
}
+ 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 response = config.customSelectableDatesCallback(
- initialViewStartDate.toISOString().split("T")[0],
- initialViewEndDate.toISOString().split("T")[0],
+ initialViewStartDate,
+ initialViewEndDate,
store.getters.order.serviceLocation.appointmentType,
store.getters.order.serviceLocation.provider.providerNumber
);
@@ -313,7 +341,7 @@ export default {
return loadInitialDataPromise.then((response) => {
const initialData = {
- todayDate: todayDate,
+ todayDate: todayDateString,
initialViewStartDate: initialViewStartDate,
initialViewEndDate: initialViewEndDate,
calendarViewDirection: calendarViewDirection,
@@ -329,7 +357,7 @@ export default {
this.hideSomeDaysForInitialView = config.hideSomeDaysForInitialView;
const hideSecondMonth = config.hideSecondMonth;
const direction = config.calendarViewDirection;
- const monthsAfterToLoadOffset = 12;
+ const monthsAfterToLoadOffset = 6;
const monthsBeforeToLoadOffset = 36;
config.initialShopTimeSlotsResponse.days.forEach((selectableDate) => {
this.selectableDatesData.push(selectableDate);
@@ -371,6 +399,13 @@ export default {
const monthToShow = this.months.find((month) =>
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);
});
}
@@ -390,32 +425,37 @@ export default {
this.hideSomeDaysForInitialView (string)
*/
- let monthIndex = this.todayMonthIndex + offset; // mutable
- let yearNum = this.todayYearNum; // mutable
+ let monthNum = convertDateStringToDate(this.todayString).getMonth() + offset + 1;
+ let yearNum = convertDateStringToDate(this.todayString).getFullYear();
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) {
- while (monthIndex > 12) {
- monthIndex = monthIndex - 12;
+ while (monthNum > 12) {
+ monthNum = monthNum - 12;
yearNum++;
}
} else if (calendarViewDirection === "past" && offset < 0) {
- while (monthIndex < 1) {
- monthIndex = 12 + monthIndex;
+ while (monthNum < 1) {
+ monthNum = 12 + monthNum;
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();
if (
@@ -426,16 +466,7 @@ export default {
monthEndDateNum = this.currentWeekEndDateNum;
}
- const monthStartDateNum =
- 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 (options.preSelectedDate) {
if (
monthStartDate.getFullYear() === preSelectedDateObj.getFullYear() &&
monthStartDate.getMonth() === preSelectedDateObj.getMonth()
@@ -472,25 +503,28 @@ export default {
const dateString =
yearNum.toString() +
"-" +
- forceTwoDigitString(monthIndex) +
+ ("0" + monthNum).slice(-2) +
"-" +
- forceTwoDigitString(i);
+ ("0" + i).slice(-2);
if (offset === 0 && i === this.todayDateNum) {
- dayClasses += "current-day";
+ dayClasses += " current-day";
}
if (offset === 0 && i < this.todayDateNum && calendarViewDirection === "future") {
- dayClasses += "unavailable-day";
+ dayClasses += " unavailable-day";
}
if (offset === 0 && i > this.todayDateNum && calendarViewDirection === "past") {
- dayClasses += "unavailable-day";
+ dayClasses += " unavailable-day";
+ }
+ if (convertDateStringToDate(dateString).getDay() === 0) {
+ dayClasses += " sunday";
}
if (
this.hideSomeDaysForInitialView &&
- initialViewEndDate.getMonth() + 1 === monthIndex &&
- initialViewEndDate.getDate() < i
+ convertDateStringToDate(initialViewEndDate).getMonth() + 1 === monthNum &&
+ convertDateStringToDate(initialViewEndDate).getDate() < i
) {
- dayClasses += "day-hidden";
+ dayClasses += " day-hidden";
isMonthThatHidesSomeDaysForInitialView = true;
}
const dateObject = {
@@ -506,9 +540,9 @@ export default {
}
const monthToAdd = {
- monthLabel: MONTHS_OF_YEAR[monthIndex - 1],
- monthIndex: monthIndex,
- monthString: MONTHS_OF_YEAR[monthIndex - 1] + "-" + yearNum?.toString(),
+ monthLabel: MONTHS_OF_YEAR[monthNum - 1],
+ monthIndex: monthNum,
+ monthString: MONTHS_OF_YEAR[monthNum - 1] + "-" + yearNum?.toString(),
yearNum: yearNum,
dates: dates,
startDateDayIndex: startDateDayIndex,
@@ -626,28 +660,28 @@ export default {
max-height: 0;
}
.date-picker {
- overflow: hidden;
position: relative;
- height: 100%;
+ flex-grow: 1;
+ display: flex;
+ flex-direction: column;
fieldset {
- overflow-y: auto;
- height: 88%;
+ flex-grow: 1;
position: relative;
}
.loader {
- position: absolute;
height: 2rem;
- width: 2rem;
+ width: calc(100% - 1.5rem);
&::after {
- width: 100%;
- height: 100%;
+ width: 1.5rem;
+ height: 1.5rem;
}
}
.calendar-grid-container {
margin: 0 auto 2rem auto;
max-width: 414px;
+ position: relative;
transition: height ease 2s, opacity ease 2s;
display: grid;
grid-template-columns: repeat(7, 1fr);
@@ -659,6 +693,8 @@ export default {
.grid-item {
text-align: center;
margin: 10px 3px;
+ font-size: 0.875rem;
+ line-height: 1.5;
&.first-day-,
&.first-day-0 {
@@ -687,7 +723,7 @@ export default {
.separator-line {
grid-area: 2/1/2/8;
border-top: 1px solid $gray-500;
- margin: 0.75rem 0;
+ margin: 0.5rem 0;
}
.month-year {
@@ -717,7 +753,7 @@ export default {
justify-content: center;
align-items: center;
outline: none;
- height: 1.35rem;
+ height: 1.5rem;
opacity: 1;
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;
background-color: $blue;
color: $white;
- &.past-day {
- box-shadow: none;
- background-color: transparent;
- color: $gray-500;
- font-weight: normal;
- }
&.current-day {
&:after {
background-color: $white;
@@ -754,7 +784,6 @@ export default {
&:checked + label {
color: $white;
background: $blue;
- box-shadow: 0 0 0 3px $white, 0 0 0 5.5px $blue;
&:after {
background-color: $white;
}
@@ -779,10 +808,9 @@ export default {
display: flex;
justify-content: center;
align-items: center;
- width: 36px;
- height: 36px;
- min-width: 36px;
- border-radius: 50%;
+ min-width: 2.5rem;
+ width: 2.5rem;
+ height: 2.5rem;
span {
&.small {
@@ -821,26 +849,43 @@ export default {
color: $blue;
background-color: $blue-100;
border: 1px solid $blue;
+ min-width: 2.5rem;
+ width: 2.5rem;
+ border-radius: 50%;
}
}
- &.past-day,
- &.future-day,
&.unavailable-day {
label {
color: $gray-500;
- background-color: transparent;
- border: 1px solid transparent;
+ background-color: $gray-100;
+ border: 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 {
label {
+ font-weight: 500;
+ color: $black;
+
&:after {
content: "";
width: 0.25rem;
height: 0.25rem;
border-radius: 50%;
- background-color: $blue;
+ background-color: $black;
position: absolute;
top: 28px;
}
@@ -864,15 +909,12 @@ export default {
opacity: 0;
max-height: 0;
margin-bottom: 0;
+ overflow: hidden;
}
&.last-available-month:not(&.month-hidden) {
margin-bottom: 14rem;
}
}
- #bottom-spacer {
- height: 20rem;
- background: lightblue;
- }
.btn-link {
font-weight: 500;
diff --git a/src/digital-components/date-picker/mixins/helpers.js b/src/digital-components/date-picker/mixins/helpers.js
index 6670bb60d..3141a7a68 100644
--- a/src/digital-components/date-picker/mixins/helpers.js
+++ b/src/digital-components/date-picker/mixins/helpers.js
@@ -7,9 +7,4 @@ const requiredParameter = () => {
throw new Error("parameter is required");
};
-const forceTwoDigitString = (monthNum) => {
- const newString = monthNum.toString();
- return newString.length === 1 ? "0" + newString : newString;
-};
-
-export { selectableDaysOptions, requiredParameter, forceTwoDigitString };
+export { selectableDaysOptions, requiredParameter };
diff --git a/src/digital-components/dropdown-question/dropdown-question.vue b/src/digital-components/dropdown-question/dropdown-question.vue
index 0fc4a977c..1d8fe22cd 100644
--- a/src/digital-components/dropdown-question/dropdown-question.vue
+++ b/src/digital-components/dropdown-question/dropdown-question.vue
@@ -44,6 +44,7 @@ export default {
isDisabled: Boolean,
isRequired: Boolean,
validationRules: String,
+ placeHolderText: String,
cmsWidgetName: String,
hasError: Boolean,
},
diff --git a/src/digital-components/modal/modal.vue b/src/digital-components/modal/modal.vue
index 090887c75..899a63b8f 100644
--- a/src/digital-components/modal/modal.vue
+++ b/src/digital-components/modal/modal.vue
@@ -139,6 +139,13 @@ export default {
color: rgb(0, 0, 0);
}
}
+ .modal-footer {
+ position: sticky;
+ width: 100%;
+ bottom: 0;
+ z-index: 5;
+ background-color: $gray-100;
+ }
&.modal-component {
.modal-dialog {
max-width: 576px;
diff --git a/src/digital-components/textbox-question/textbox-question.vue b/src/digital-components/textbox-question/textbox-question.vue
index b6934a2ab..b82153895 100644
--- a/src/digital-components/textbox-question/textbox-question.vue
+++ b/src/digital-components/textbox-question/textbox-question.vue
@@ -65,9 +65,9 @@
-
@@ -49,6 +49,7 @@ import textBlock from "@/digital-components/text-block/text-block";
// DEFINE VALIDATION RULES
defineRule("first-name-required", required(errorMessages.FIRST_NAME_REQUIRED));
defineRule("last-name-required", required(errorMessages.LAST_NAME_REQUIRED));
+defineRule("email-address-required", required(errorMessages.EMAIL_ADDRESS_REQUIRED));
defineRule(
"email-address-format",
regex(
diff --git a/src/layouts/address-vehicles/address-vehicles.vue b/src/layouts/address-vehicles/address-vehicles.vue
index 1f62097b8..de1fa9999 100644
--- a/src/layouts/address-vehicles/address-vehicles.vue
+++ b/src/layouts/address-vehicles/address-vehicles.vue
@@ -151,6 +151,7 @@ export default {
if (
store.getters.order.vehicle.carId &&
store.getters.order.serviceLocation.zipCode &&
+ store.getters.order.customer.emailAddress &&
store.getters.pageData(fmgPageValues.ADDRESS_VEHICLES)
) {
return true;
diff --git a/src/layouts/estimate/estimate.vue b/src/layouts/estimate/estimate.vue
index 1300b11ba..d18af242c 100644
--- a/src/layouts/estimate/estimate.vue
+++ b/src/layouts/estimate/estimate.vue
@@ -44,8 +44,9 @@
cmsWidgetName="EmailAddressQuestionWidget"
v-model="emailAddress"
inputId="emailAddress"
+ isRequired
disableAutoFill
- validationRules="email-address-format" />
+ validationRules="email-address-required|email-address-format" />
@@ -117,6 +118,7 @@ import { queryStrings } from "@/constants/query-strings";
// Define Validation Rules
defineRule("zip-required", required(errorMessages.SERVICE_ZIP_REQUIRED));
defineRule("zip-format", regex(/(^\d{5}$)|(^\d{5}-\d{4}$)/, errorMessages.SERVICE_ZIP_FORMAT));
+defineRule("email-address-required", required(errorMessages.EMAIL_ADDRESS_REQUIRED));
defineRule(
"email-address-format",
regex(
diff --git a/src/layouts/license-plate-lookup/license-plate-lookup.vue b/src/layouts/license-plate-lookup/license-plate-lookup.vue
index 787c2711e..49ff8ff26 100644
--- a/src/layouts/license-plate-lookup/license-plate-lookup.vue
+++ b/src/layouts/license-plate-lookup/license-plate-lookup.vue
@@ -33,7 +33,7 @@
cmsWidgetName="EmailAddressQuestionWidget"
v-model="email"
customInputId="email"
- validationRules="email-address-format" />
+ validationRules="email-address-required|email-address-format" />
@@ -115,6 +115,7 @@ import vinPagesMixin from "@/mixins/vin-pages-mixin";
defineRule("license-plate-required", required(errorMessages.LICENSE_PLATE_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("email-address-required", required(errorMessages.EMAIL_ADDRESS_REQUIRED));
defineRule(
"email-address-format",
regex(
diff --git a/src/layouts/schedule/helpers/schedule-helper.js b/src/layouts/schedule/helpers/schedule-helper.js
index 6f8a09373..82c756b5b 100644
--- a/src/layouts/schedule/helpers/schedule-helper.js
+++ b/src/layouts/schedule/helpers/schedule-helper.js
@@ -18,3 +18,30 @@ export function calcDaysBetweenDates(dateString1, dateString2) {
const timeDifference = Math.abs(date2 - date1); // Calculate the time difference in milliseconds
return Math.ceil(timeDifference / (1000 * 60 * 60 * 24)); // Convert milliseconds to days
}
+
+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);
+}
diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue
index 6bac741af..39abffdf0 100644
--- a/src/layouts/schedule/schedule.vue
+++ b/src/layouts/schedule/schedule.vue
@@ -8,7 +8,7 @@
@@ -16,6 +16,7 @@
selectableDatesSetting="custom"
ref="datePicker"
v-model="selectedDate"
+ class="text-link-small"
:customSelectableDatesCallback="getAvailableDatesMethod"
@date-clicked="openInshopTimeSlotsModal" />
{
- var today = new Date();
- 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 apiEndDateLimit = sumDateString(startDateString, TIME_SLOTS_CALL_DAYS_LIMIT);
const difference = calcDaysBetweenDates(startDateString, endDateString);
const apiCallsCount = Math.ceil(difference / TIME_SLOTS_CALL_DAYS_LIMIT);
const storeActionConfigs = [];
const timeSlotsData = {};
- let apiStartDate = new Date(startDateString + "T" + currentTime);
- let apiEndDate = apiEndDateLimit;
timeSlotsData.days = [];
+ let apiStartDate = startDateString;
+ let apiEndDate = apiEndDateLimit;
for (let i = 1; i <= apiCallsCount; i++) {
let storeActionConfig;
if (i > 1) {
- apiStartDate = new Date(apiEndDate);
- apiStartDate.setDate(apiStartDate.getDate() + 1);
- apiEndDate = new Date(apiStartDate);
- apiEndDate.setDate(apiEndDate.getDate() + TIME_SLOTS_CALL_DAYS_LIMIT);
- }
- if (i === apiCallsCount) {
- apiEndDate = new Date(endDateString + "T" + currentTime);
+ apiStartDate = sumDateString(apiEndDate, 1);
+ apiEndDate = sumDateString(apiStartDate, TIME_SLOTS_CALL_DAYS_LIMIT);
+
+ if (i === apiCallsCount) {
+ apiEndDate = endDateString;
+ }
}
if (appointmentType === AppointmentTypeStrings.MOBILE) {
storeActionConfig = {
storeAction: storeActions.GET_MOBILE_TIME_SLOTS,
payload: {
- startDate: apiStartDate.toISOString().split("T")[0],
- endDate: apiEndDate.toISOString().split("T")[0],
+ startDate: apiStartDate,
+ endDate: apiEndDate,
},
};
} else {
storeActionConfig = {
storeAction: storeActions.GET_SHOP_TIME_SLOTS,
payload: {
- startDate: apiStartDate.toISOString().split("T")[0],
- endDate: apiEndDate.toISOString().split("T")[0],
+ startDate: apiStartDate,
+ endDate: apiEndDate,
shopAppointmentType: appointmentType,
providerNumber: providerNumber,
},
@@ -384,12 +381,11 @@ export default {
);
}
}
-
this.$refs.funnelFooter.updateButtonText(funnelFooterButtonText);
},
convertSelectedDateToShortMonthAndDay(selectedDate) {
// 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
return dateObject.toLocaleDateString("en-us", { month: "short", day: "numeric" });
},
@@ -496,3 +492,18 @@ export default {
},
};
+
+
diff --git a/src/layouts/schedule/time-slot-modal-question/time-slot-modal-question.vue b/src/layouts/schedule/time-slot-modal-question/time-slot-modal-question.vue
index 159994079..eff2358f0 100644
--- a/src/layouts/schedule/time-slot-modal-question/time-slot-modal-question.vue
+++ b/src/layouts/schedule/time-slot-modal-question/time-slot-modal-question.vue
@@ -41,6 +41,7 @@
import modal from "@/digital-components/modal/modal";
import textBlock from "@/digital-components/text-block/text-block";
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";
// TODO: Move this somewhere more global
@@ -55,6 +56,13 @@ import {
PREMIUM_FEE_PART_TYPE,
RouteCodeFlags,
} 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
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() {
- return this.getCmsContent(this.cmsWidgetName, "FooterText");
+ return this.getCmsContent(
+ this.cmsWidgetName,
+ cmsWidgetFieldMappings.MODAL_CLOSE_BUTTON
+ );
},
premiumAppointmentButtonText() {
- return this.getCmsContent(this.mobilePremiumCmsWidgetName, "HeaderText");
+ return this.getCmsContent(
+ this.mobilePremiumCmsWidgetName,
+ cmsWidgetFieldMappings.TIME_SLOT_BUTTON
+ );
},
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() {
- return this.getCmsContent(this.overnightDropOffCmsWidgetName, "HeaderText");
+ return this.getCmsContent(
+ this.overnightDropOffCmsWidgetName,
+ cmsWidgetFieldMappings.TIME_SLOT_BUTTON
+ );
},
dropoffDisclaimerText() {
- return this.getCmsContent(this.dropoffCmsWidgetName, "FooterText");
+ return this.getCmsContent(this.dropoffCmsWidgetName, cmsWidgetFieldMappings.DISCLAIMER);
+ },
+ sameDayDropOffDisclaimerText() {
+ return this.getCmsContent(
+ this.sameDayDropOffCmsWidgetName,
+ cmsWidgetFieldMappings.DISCLAIMER
+ );
},
overnightDropOffDisclaimerText() {
- return this.getCmsContent(this.overnightDropOffCmsWidgetName, "FooterText");
+ return this.getCmsContent(
+ this.overnightDropOffCmsWidgetName,
+ cmsWidgetFieldMappings.DISCLAIMER
+ );
},
disclaimerTextBlockCopy() {
if (this.appointmentType === AppointmentTypeStrings.DROP_OFF) {
- if (this.isSameDay) {
- return null;
- } else if (this.selectedTimeSlotId?.includes(RouteCodeFlags.ALL_DAY_DROP_OFF)) {
- return this.dropoffDisclaimerText;
+ if (this.selectedTimeSlotId?.includes(RouteCodeFlags.ALL_DAY_DROP_OFF)) {
+ if (this.isSameDay) {
+ return this.sameDayDropOffDisclaimerText;
+ } else {
+ return this.dropoffDisclaimerText;
+ }
} else if (this.selectedTimeSlotId?.includes(RouteCodeFlags.OVERNIGHT_DROP_OFF)) {
return this.overnightDropOffDisclaimerText;
} else {
@@ -160,15 +200,24 @@ export default {
}
},
dropOffDurationText() {
- return this.getCmsContent(this.dropoffCmsWidgetName, "SubheaderText");
+ return this.getCmsContent(this.dropoffCmsWidgetName, cmsWidgetFieldMappings.DURATION);
+ },
+ sameDayDropoffDurationText() {
+ return this.getCmsContent(
+ this.sameDayDropOffCmsWidgetName,
+ cmsWidgetFieldMappings.DURATION
+ );
},
overnightDropoffDurationText() {
- return this.getCmsContent(this.overnightDropOffCmsWidgetName, "SubheaderText");
+ return this.getCmsContent(
+ this.overnightDropOffCmsWidgetName,
+ cmsWidgetFieldMappings.DURATION
+ );
},
inshopDurationText() {
const inshopDurationTextWithoutTime = this.getCmsContent(
this.cmsWidgetName,
- "SubheaderText"
+ cmsWidgetFieldMappings.DURATION
);
const inshopDurationTime = this.getDisplayTextForDurationLength(
this.estimatedServiceMinutesMinimum,
@@ -185,15 +234,16 @@ export default {
if (this.selectedTimeSlotId?.includes(RouteCodeFlags.OVERNIGHT_DROP_OFF)) {
return this.overnightDropoffDurationText;
} else if (this.selectedTimeSlotId?.includes(RouteCodeFlags.ALL_DAY_DROP_OFF)) {
- return this.dropOffDurationText;
+ if (this.isSameDay) {
+ return this.sameDayDropoffDurationText;
+ } else {
+ return this.dropOffDurationText;
+ }
} else {
return null;
}
}
},
- shouldShowDropoffDisclaimerText() {
- return this.appointmentType === AppointmentTypeStrings.DROP_OFF && !this.isSameDay;
- },
isSameDay() {
if (!this.dateAndTimeSlotData) {
return false;
@@ -202,14 +252,13 @@ export default {
const todaysDate = new Date().toISOString().split("T")[0];
return selectedDate === todaysDate;
},
-
dateSelectedReadableDate() {
if (!this.dateAndTimeSlotData) {
return null;
}
// 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
return dateObject.toLocaleDateString("en-us", {
weekday: "long",
@@ -303,9 +352,14 @@ export default {
},
getAvailableTimeSlotsForDropOff(timeSlotsForSelectedDate) {
const availableTimeSlots = timeSlotsForSelectedDate.map((timeSlot) => {
- const buttonLabelValue = timeSlot.id.includes(RouteCodeFlags.OVERNIGHT_DROP_OFF)
- ? this.overnightDropoffButtonText
- : this.dropoffButtonText;
+ let buttonLabelValue;
+ if (timeSlot.id.includes(RouteCodeFlags.OVERNIGHT_DROP_OFF)) {
+ buttonLabelValue = this.overnightDropoffButtonText;
+ } else if (this.isSameDay) {
+ buttonLabelValue = this.sameDayDropoffButtonText;
+ } else {
+ buttonLabelValue = this.dropoffButtonText;
+ }
return {
value: timeSlot.id,
buttonLabel: buttonLabelValue,
diff --git a/src/layouts/service-location/appointment-type-question/appointment-type-question.spec.js b/src/layouts/service-location/appointment-type-question/appointment-type-question.spec.js
index 65fba87f2..63242df22 100644
--- a/src/layouts/service-location/appointment-type-question/appointment-type-question.spec.js
+++ b/src/layouts/service-location/appointment-type-question/appointment-type-question.spec.js
@@ -1,6 +1,13 @@
import { shallowMount } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import appointmentTypeQuestion from "./appointment-type-question";
+import store from "@/store";
+
+store.getters = {
+ damage: {
+ isRepair: false,
+ },
+};
const mockCmsContent = {
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", () => {
it("Should display all options if both in-shop and mobile are available", async () => {
// 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 () => {
// Arrange/Act
@@ -169,6 +224,8 @@ function setupMocks({ mountOptions, mixins, props, isShallowMount = true }) {
if (props) resultingMountOptions.propsData = props;
+ resultingMountOptions.global.mocks["$store"] = store;
+
const wrapper = isShallowMount
? shallowMount(appointmentTypeQuestion, resultingMountOptions)
: mount(appointmentTypeQuestion, resultingMountOptions);
diff --git a/src/layouts/service-location/appointment-type-question/appointment-type-question.vue b/src/layouts/service-location/appointment-type-question/appointment-type-question.vue
index cce1a4d5b..732382b1a 100644
--- a/src/layouts/service-location/appointment-type-question/appointment-type-question.vue
+++ b/src/layouts/service-location/appointment-type-question/appointment-type-question.vue
@@ -18,6 +18,7 @@
diff --git a/src/layouts/vehicle/vehicle.spec.js b/src/layouts/vehicle/vehicle.spec.js
new file mode 100644
index 000000000..210281423
--- /dev/null
+++ b/src/layouts/vehicle/vehicle.spec.js
@@ -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 };
+}
diff --git a/src/layouts/vehicle/vehicle.vue b/src/layouts/vehicle/vehicle.vue
new file mode 100644
index 000000000..bea1fdd8e
--- /dev/null
+++ b/src/layouts/vehicle/vehicle.vue
@@ -0,0 +1,278 @@
+
+
+
+
+
+
+
diff --git a/src/layouts/vin-lookup/vin-lookup.vue b/src/layouts/vin-lookup/vin-lookup.vue
index d8f79df61..ba1764bd3 100644
--- a/src/layouts/vin-lookup/vin-lookup.vue
+++ b/src/layouts/vin-lookup/vin-lookup.vue
@@ -59,7 +59,7 @@
v-model="emailAddress"
customInputId="emailAddress"
isRequired
- validationRules="email-address-format" />
+ validationRules="email-address-required|email-address-format" />
@@ -79,7 +79,12 @@
:manualHeadline="AlertPerfectMatchInsuranceVerifiedHeader"
:manualCopy="AlertPerfectMatchInsuranceVerifiedBody"
v-model="customAlertData"
- v-if="vinPopulatedOnPageLoad && isInsuranceVerified"
+ v-if="
+ vinPopulatedOnPageLoad &&
+ isInsuranceVerified &&
+ !displayInvalidZipAlert &&
+ !displayNonServiceableZipAlert
+ "
alertClass="alert-success" />
numSlashesBeforeParams) {
+ splitString = splitString.slice(0, numSlashesBeforeParams);
+ return splitString.join("/");
+ } else {
+ return endpointWithoutParams;
+ }
+ },
},
computed: {
analyticsPageEvents() {
diff --git a/src/router/router-constants/fmgPage-values.js b/src/router/router-constants/fmgPage-values.js
index a83d03ecb..1ce76d5e8 100644
--- a/src/router/router-constants/fmgPage-values.js
+++ b/src/router/router-constants/fmgPage-values.js
@@ -3,6 +3,7 @@ const fmgPageValues = {
VEHICLE_MAKE: "vehicle-make",
VEHICLE_MODEL: "vehicle-model",
VEHICLE_STYLE: "vehicle-style",
+ VEHICLE: "vehicle",
VEHICLE_DAMAGE: "vehicle-damage",
ADDRESS_LOOKUP: "address-lookup",
VIN_LOOKUP: "vin-lookup",
diff --git a/src/router/router-constants/routing-table.js b/src/router/router-constants/routing-table.js
index 688967933..4238b659a 100644
--- a/src/router/router-constants/routing-table.js
+++ b/src/router/router-constants/routing-table.js
@@ -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
const routingTable = function (store) {
return [
+ {
+ fmgPageValue: fmgPageValues.VEHICLE,
+ maps: [
+ {
+ scenario: navigationScenarios.CLICKED_FORWARD,
+ destinationFmgPageValue: fmgPageValues.VEHICLE_DAMAGE,
+ },
+ ],
+ },
{
fmgPageValue: fmgPageValues.VEHICLE_YEAR,
maps: [
@@ -57,7 +66,7 @@ const routingTable = function (store) {
maps: [
{
scenario: navigationScenarios.CLICKED_BACK,
- destinationFmgPageValue: fmgPageValues.VEHICLE_STYLE,
+ destinationFmgPageValue: fmgPageValues.VEHICLE,
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_VIN,
@@ -457,7 +466,7 @@ const routingTable = function (store) {
maps: [
{
scenario: navigationScenarios.CLICKED_VEHICLE_EDIT,
- destinationFmgPageValue: fmgPageValues.VEHICLE_YEAR,
+ destinationFmgPageValue: fmgPageValues.VEHICLE,
},
{
scenario: navigationScenarios.CLICKED_DAMAGE_EDIT,
diff --git a/src/store/index.js b/src/store/index.js
index 18c74a486..fa2906387 100644
--- a/src/store/index.js
+++ b/src/store/index.js
@@ -534,6 +534,9 @@ export const getters = {
);
return !!nonWindshieldItems?.length;
},
+ isMobileAppointment: (state) => {
+ return state.order.serviceLocation.appointmentType === AppointmentTypeStrings.MOBILE;
+ },
isRecalibrationOnOrder: (state) => {
return getHasRecalibrationPart(state);
},
@@ -615,6 +618,10 @@ function getNonFalseValuesOfPropertyInArrayOfObjects(array, propertyName) {
return (array ?? []).map((x) => x[propertyName]).filter((x) => x);
}
+function provisionalTriggersToString(provisionalTriggers) {
+ return "ProvisionalTriggers:" + provisionalTriggers.join(",");
+}
+
// Export Actions
export const actions = {
// Vehicle API Actions
@@ -1239,13 +1246,16 @@ export const actions = {
const order = context.state.order;
const vehicle = context.state.order.vehicle;
- let partNumbers = [
+ let lineItems = [
...(order.lineItems.supportingItems ?? []),
...(order.lineItems.vaps ?? []),
...getFlattenedArrayOfLineItemsWithChildParts(order.lineItems.glassParts),
];
- partNumbers = partNumbers.map((lineItem) => {
- return lineItem.partNumber;
+ lineItems = lineItems.map((lineItem) => {
+ return {
+ partNumber: lineItem.partNumber,
+ partType: lineItem.partType,
+ };
});
const glassPieces = order.damage.glassToReplace
? convertGlassPieceToBackEndCompatibleFormat(order.damage.glassToReplace)
@@ -1258,7 +1268,7 @@ export const actions = {
applicationName: applicationConfig.APPLICATION_NAME,
parentAccountNumber: context.getters.payment.parentAccountNumber,
carId: vehicle.carId,
- partNumbers: partNumbers,
+ lineItems: lineItems,
glassPieces: glassPieces,
eon: order.eon,
coverage: {
@@ -1267,11 +1277,12 @@ export const actions = {
additionalAuthFlag: "",
},
partSelection: {
- // TODO: Provisional booking will utilize these fields
- hasAnsweredPartQuestions: false,
- hasAnsweredMoldingQuestions: false,
- hasAnsweredCapabilityQuestions: false,
- hasManuallySelectedParts: false,
+ hasAnsweredPartQuestions: !!order.damage.partQuestionAnswers?.length,
+ hasAnsweredMoldingQuestions: !!order.damage.moldingQuestionAnswers?.length,
+ hasAnsweredCapabilityQuestions: !!order.damage.capabilityQuestionAnswers?.length,
+ hasManuallySelectedParts:
+ !!context.state.applicationUser.pageData["vehicle-parts"]?.partsOrQuestions
+ .length,
},
vehicle: {
year: vehicle.year,
@@ -1286,19 +1297,24 @@ export const actions = {
method: endpoints.GetShopTimeSlots.method,
endpoint: endpoints.GetShopTimeSlots.url,
payload: payload,
+ additionalSuccessEventDataHandler: (response) =>
+ provisionalTriggersToString(response.data.provisionalTriggers),
});
},
getMobileTimeSlots(context, { startDate, endDate }) {
const order = context.state.order;
const vehicle = context.state.order.vehicle;
- let partNumbers = [
+ let lineItems = [
...(order.lineItems.supportingItems ?? []),
...(order.lineItems.vaps ?? []),
...getFlattenedArrayOfLineItemsWithChildParts(order.lineItems.glassParts),
];
- partNumbers = partNumbers.map((lineItem) => {
- return lineItem.partNumber;
+ lineItems = lineItems.map((lineItem) => {
+ return {
+ partNumber: lineItem.partNumber,
+ partType: lineItem.partType,
+ };
});
const glassPieces = order.damage.glassToReplace
? convertGlassPieceToBackEndCompatibleFormat(order.damage.glassToReplace)
@@ -1309,7 +1325,7 @@ export const actions = {
applicationName: applicationConfig.APPLICATION_NAME,
parentAccountNumber: context.getters.payment.parentAccountNumber,
carId: vehicle.carId,
- partNumbers: partNumbers,
+ lineItems: lineItems,
glassPieces: glassPieces,
eon: order.eon,
coverage: {
@@ -1318,11 +1334,12 @@ export const actions = {
additionalAuthFlag: "",
},
partSelection: {
- // TODO: Provisional booking will utilize these fields
- hasAnsweredPartQuestions: false,
- hasAnsweredMoldingQuestions: false,
- hasAnsweredCapabilityQuestions: false,
- hasManuallySelectedParts: false,
+ hasAnsweredPartQuestions: !!order.damage.partQuestionAnswers?.length,
+ hasAnsweredMoldingQuestions: !!order.damage.moldingQuestionAnswers?.length,
+ hasAnsweredCapabilityQuestions: !!order.damage.capabilityQuestionAnswers?.length,
+ hasManuallySelectedParts:
+ !!context.state.applicationUser.pageData["vehicle-parts"]?.partsOrQuestions
+ .length,
},
vehicle: {
year: vehicle.year,
@@ -1337,6 +1354,8 @@ export const actions = {
method: endpoints.GetMobileTimeSlots.method,
endpoint: endpoints.GetMobileTimeSlots.url,
payload: payload,
+ additionalSuccessEventDataHandler: (response) =>
+ provisionalTriggersToString(response.data.provisionalTriggers),
});
},
@@ -1391,7 +1410,7 @@ export const actions = {
},
},
customer: {
- emailAddress: order.customer.emailAddress || null,
+ emailAddress: order.customer.emailAddress,
},
damage: {
numberOfChips: damage.numberOfChips,
@@ -1960,7 +1979,7 @@ export const actions = {
},
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 }) {
@@ -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.
var aptDate = new Date(order.schedule.date + "T00:00:00");
var curDate = new Date();
+ curDate.setHours(0, 0, 0, 0);
// if appointment date is in the past, clear schedule
if (aptDate.getTime() < curDate.getTime()) {
diff --git a/src/store/store.spec.js b/src/store/store.spec.js
index 1ea0125b0..4a6788433 100644
--- a/src/store/store.spec.js
+++ b/src/store/store.spec.js
@@ -4,6 +4,7 @@ import { storeMutations } from "@/constants/store-mutations";
import { storeActions } from "@/constants/store-actions";
import { experimentTriggers } from "@/constants/experiments";
import { fmgPageValues } from "@/router/router-constants/fmgPage-values";
+import { AppointmentTypeStrings } from "@/constants/schedule-constants";
// Mock global method
globalMethods.callHttpClient = jest.fn();
@@ -2869,6 +2870,45 @@ describe("Getters", () => {
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", () => {
test("glassToReplace, glassParts, and otherParts are null > return correct experimentOrder values", () => {
// Arrange
diff --git a/src/ux-components/button-main/button-main.vue b/src/ux-components/button-main/button-main.vue
index 18d7427c2..f1099111b 100644
--- a/src/ux-components/button-main/button-main.vue
+++ b/src/ux-components/button-main/button-main.vue
@@ -100,7 +100,7 @@ export default {
color: $white;
background: $blue-700;
box-shadow: 0 0 0 3px, 0 0 0 5.5px $blue-700;
- pointer-events: none;
+ pointer-events: auto;
}
&.delay {
// fixes flicker while transitioning between states
diff --git a/src/ux-components/loader/loader.vue b/src/ux-components/loader/loader.vue
index 0635614b8..eb244c796 100644
--- a/src/ux-components/loader/loader.vue
+++ b/src/ux-components/loader/loader.vue
@@ -3,7 +3,11 @@
class="loader"
role="alert"
aria-label="Loading new page"
- v-bind:class="[this.loaderColor, this.loaderPosition, this.blockUi]">
+ v-bind:class="[
+ this.loaderColor,
+ this.loaderPosition,
+ this.allowPageInteraction ? 'allow-ui-interaction' : '',
+ ]">