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 @@ -
+
{{ errorMessage }}
@@ -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' : '', + ]">