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..50ecfeb88 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, @@ -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, @@ -628,12 +662,16 @@ export default { .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; + padding-bottom: 6rem; // arbitrary amount to provide enough spacing for scroll animation + max-height: 21.25rem; // 340px... arbitrary amount to not push View More link out of view on iphones } .loader { position: absolute; @@ -648,6 +686,7 @@ export default { .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 +698,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 +728,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 +758,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 +776,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 +789,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 +813,9 @@ export default { display: flex; justify-content: center; align-items: center; - width: 36px; - height: 36px; - min-width: 36px; - border-radius: 50%; + width: 3rem; + height: 2.5rem; + min-width: 3rem; span { &.small { @@ -821,26 +854,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 +914,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/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 }} str.length > 0); } /** diff --git a/src/layouts/address-lookup/customer-questions/customer-questions.vue b/src/layouts/address-lookup/customer-questions/customer-questions.vue index ad1ba34c5..489ec5f40 100644 --- a/src/layouts/address-lookup/customer-questions/customer-questions.vue +++ b/src/layouts/address-lookup/customer-questions/customer-questions.vue @@ -27,7 +27,7 @@ v-model="customerModel.emailAddress" ref="emailAddress" customInputId="emailAddress" - validationRules="email-address-format" /> + validationRules="email-address-required|email-address-format" />
@@ -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 0e41105d4..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" /> { - const apiEndDateLimit = new Date(startDateString + "T00:00:00"); - const endDate = new Date(endDateString + "T00:00:00"); - 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 + "T00:00:00"); - 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 + "T00:00:00"); + 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, }, @@ -381,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" }); }, @@ -493,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 @@ @@ -31,6 +26,7 @@ export default {