diff --git a/jest.config.js b/jest.config.js index a757d5ada..28c5557cb 100644 --- a/jest.config.js +++ b/jest.config.js @@ -26,7 +26,7 @@ module.exports = { testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"], coverageThreshold: { global: { - statements: 80, + statements: 78, // Got the go ahead from Mark to temporarily lower this. Taking out initialize component made the year,make,model and style coverage drop a bit. Once unit tests for license plate lookup, vin lookup and address lookup are in the coverage should go back up to 90 }, }, diff --git a/src/constants/endpoints.js b/src/constants/endpoints.js index 7d83642a6..53f981d8a 100644 --- a/src/constants/endpoints.js +++ b/src/constants/endpoints.js @@ -108,12 +108,14 @@ const endpoints = { }, GetShopTimeSlots: { url: "/schedule/api/v1/schedule/shop-time-slots", - mockUrl: "https://mockey.qa.sagaws.net/service/shop-time-slots", // TODO: REMOVE MOCKURL + method: "POST", + }, + GetMobileTimeSlots: { + url: "/schedule/api/v1/schedule/mobile-time-slots", method: "POST", }, GetMobileEarlyBirdFee: { url: "/parts/api/v1/parts/mobile-early-bird-fee", - mockUrl: "https://mockey.qa.sagaws.net/service/mobile-early-bird-fee", // TODO: REMOVE MOCKURL method: "GET", }, SaveSession: { diff --git a/src/constants/store-actions.js b/src/constants/store-actions.js index 341fcd487..5ff98fd9d 100644 --- a/src/constants/store-actions.js +++ b/src/constants/store-actions.js @@ -33,6 +33,7 @@ const storeActions = { GET_MOBILE_FEE_PART: "getMobileFeePart", GET_SERVICEABILITY_DETAILS: "getServiceabilityDetails", GET_SHOP_TIME_SLOTS: "getShopTimeSlots", + GET_MOBILE_TIME_SLOTS: "getMobileTimeSlots", GET_PROVIDERS: "getProviders", GET_MOBILE_EARLY_BIRD_FEE: "getMobileEarlyBirdFee", SAVE_SESSION: "saveSession", diff --git a/src/digital-components/button-question/button-question.vue b/src/digital-components/button-question/button-question.vue index e8be5b145..7441ab422 100644 --- a/src/digital-components/button-question/button-question.vue +++ b/src/digital-components/button-question/button-question.vue @@ -257,7 +257,7 @@ export default { watch: { modelValue(newValue, oldValue) { this.resetField({ - value: newValue + value: newValue, }); }, answers() { diff --git a/src/digital-components/date-picker/date-picker.vue b/src/digital-components/date-picker/date-picker.vue index 9154383cd..fefeb30ff 100644 --- a/src/digital-components/date-picker/date-picker.vue +++ b/src/digital-components/date-picker/date-picker.vue @@ -22,7 +22,6 @@
-
SundayS
MondayM
TuesdayT
@@ -84,7 +83,6 @@ export default { months: null, disableViewMoreDatesButton: false, selectableDatesData: [], // NOTE: uses monthNum (1-based), NOT monthIndex (0-based) - today: null, hideSomeDaysForInitialView: null, }; }, @@ -112,6 +110,12 @@ export default { }, }, computed: { + today() { + if (this.todayOverrideDateString) { + return new Date(this.todayOverrideDateString); + } + return new Date(); + }, todayMonthIndex() { return this.today.getMonth() + 1; }, @@ -151,39 +155,31 @@ export default { this.$emit("date-clicked"); }, getWeekStartDate(date) { - // Get the day of the week for date - let dayOfWeek = date.getDay(); - + const dayOfWeek = date.getDay(); // Subtract the day of the week from date to get the date of Sunday - let sunday = new Date(date); + const sunday = new Date(date); sunday.setDate(sunday.getDate() - dayOfWeek); - - // Return the date of Sunday return sunday; }, getWeekEndDate(date) { - const currentDay = date.getDay(); // Get the day of the week (0 = Sunday, 1 = Monday, etc.) - const daysUntilSaturday = 6 - currentDay; // Calculate the number of days until Saturday - + 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; }, getNextWeekSunday(date) { - const currentDay = date.getDay(); // Get the day of the week (0 = Sunday, 1 = Monday, etc.) - const daysUntilNextSunday = currentDay === 0 ? 7 : 7 - currentDay; // Calculate the number of days until the next Sunday - + 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; }, getInitialViewWeeks(today, initialViewRowsToShow) { - // TODO: this only is for future direction; create logic for past direction - let weeks = []; + // TODO: this only is for future direction; need to create logic for past direction + const weeks = []; let weekStartDate = this.getWeekStartDate(today); let weekEndDate = this.getWeekEndDate(today); for (let i = 0; i < initialViewRowsToShow; i++) { @@ -200,14 +196,19 @@ export default { return weeks; }, async loadInitialData(config) { - // CALLED FROM CONSUMING COMPONENT BEFORE DATE-PICKER APPEARS - const todayDate = config.todayOverrideDateString - ? new Date(config.todayOverrideDateString) - : new Date(); + let todayDate; + if (this.today) { + todayDate = this.today; + } else if (config.todayOverrideDateString) { + todayDate = new Date(config.todayOverrideDateString); + } else { + todayDate = new Date(); + } - let todayMonthIndex = todayDate.getMonth() + 1; - let todayYearNum = todayDate.getFullYear(); - let currentMonthStart = new Date(todayYearNum, todayMonthIndex - 1, 1); + 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); let currentMonthEnd = new Date(todayYearNum, todayMonthIndex, 0); let calendarViewDirection = "none"; @@ -219,10 +220,10 @@ export default { config.initialViewRowsToShow ); - let initialViewStartDate = todayDate; - let initialViewEndDate = initialViewWeeks[initialViewWeeks.length - 1].weekEndDate; - let saturday1month = initialViewWeeks[0].weekEndDate.getMonth(); - let sunday5month = + const initialViewStartDate = todayDate; + const initialViewEndDate = initialViewWeeks[initialViewWeeks.length - 1].weekEndDate; + const saturday1month = initialViewWeeks[0].weekEndDate.getMonth(); + const sunday5month = initialViewWeeks[initialViewWeeks.length - 1].weekStartDate.getMonth(); let hideSomeDaysForInitialView = false; @@ -244,9 +245,10 @@ export default { let myPromise = new Promise((resolve, reject) => { const response = config.customSelectableDatesCallback( - initialViewStartDate, - initialViewEndDate, - store.getters.order.serviceLocation.appointmentType + initialViewStartDate.toISOString().split("T")[0], + initialViewEndDate.toISOString().split("T")[0], + store.getters.order.serviceLocation.appointmentType, + store.getters.order.serviceLocation.provider.providerNumber ); resolve(response); }); @@ -284,9 +286,9 @@ export default { // Growing from 0 to 1 time = Math.min(1, (timestamp - start) / duration); - let percentageNew = timingFunc(time); - let distanceToGo = targetY; - let thisDistance = percentageNew * distanceToGo; + const percentageNew = timingFunc(time); + const distanceToGo = targetY; + const thisDistance = percentageNew * distanceToGo; wrapper.scrollTo(0, initY + thisDistance); @@ -305,13 +307,11 @@ export default { }, async setCalendarData(config = {}) { - this.today = config.todayDate; this.hideSomeDaysForInitialView = config.hideSomeDaysForInitialView; - let hideSecondMonth = config.hideSecondMonth; + const hideSecondMonth = config.hideSecondMonth; const direction = config.calendarViewDirection; - - const monthsAfterToLoadOffset = 12; // TO BE MADE "CONSTANTS" - const monthsBeforeToLoadOffset = 36; // TO BE MADE "CONSTANTS" + const monthsAfterToLoadOffset = 12; + const monthsBeforeToLoadOffset = 36; config.initialShopTimeSlotsResponse.days.forEach((selectableDate) => { this.selectableDatesData.push(selectableDate); }); @@ -382,25 +382,25 @@ export default { } } - const monthEndDate = new Date(yearNum, monthIndex, 0); // BOTH - let monthEndDateNum = monthEndDate.getDate(); // BOTH + const monthEndDate = new Date(yearNum, monthIndex, 0); + let monthEndDateNum = monthEndDate.getDate(); if ( offset === 0 && calendarViewDirection === "past" && monthEndDateNum > this.currentWeekEndDateNum ) { - monthEndDateNum = this.currentWeekEndDateNum; // PAST + monthEndDateNum = this.currentWeekEndDateNum; } const monthStartDateNum = offset === 0 && calendarViewDirection === "future" ? this.currentWeekStartDateNum - : 1; // FUTURE - const monthStartDate = new Date(yearNum, monthIndex - 1, monthStartDateNum); // BOTH + : 1; + const monthStartDate = new Date(yearNum, monthIndex - 1, monthStartDateNum); - const startDateDayIndex = monthStartDate.getDay(); // FUTURE - const endDateDayIndex = monthEndDate.getDay(); // PAST + const startDateDayIndex = monthStartDate.getDay(); + const endDateDayIndex = monthEndDate.getDay(); if (Math.abs(offset) === 1 && hideSecondMonth) { monthClass = monthClass + " month-hidden"; @@ -424,7 +424,7 @@ export default { // populate dates array for (let i = monthStartDateNum; i <= monthEndDateNum; i++) { let dayClasses = ""; - let dateString = + const dateString = yearNum.toString() + "-" + forceTwoDigitString(monthIndex) + @@ -514,7 +514,7 @@ export default { monthToShow.dates[monthToShow.dates.length - 1].inputValue.dateString ); this.isLoading = false; - this.hideSomeDaysForInitialView = false; // if hid days on initial partial view, this removes hidden styling on days + this.hideSomeDaysForInitialView = false; // if hid days on initial partial view, this will reveal those days monthToShow.monthClass = monthToShow.monthClass.replace(" month-hidden", ""); this.scrollToElement(monthToShow.monthString); @@ -526,7 +526,8 @@ export default { const moreSelectableDates = await this.customSelectableDatesCallback( monthStart, monthEnd, - this.$store.getters.order.serviceLocation.appointmentType + this.$store.getters.order.serviceLocation.appointmentType, + this.$store.getters.order.serviceLocation.provider.providerNumber ); moreSelectableDates.days.forEach((selectableDate) => { const index = this.selectableDatesData.findIndex( diff --git a/src/digital-components/text-block/text-block.vue b/src/digital-components/text-block/text-block.vue index 4f9c41599..2deb126e5 100644 --- a/src/digital-components/text-block/text-block.vue +++ b/src/digital-components/text-block/text-block.vue @@ -10,7 +10,7 @@ export default { name: "textBlock", props: { customText: String, // used to allow the insert of token values into textblock - justifyText: String, // left, right, center + justifyText: String, // right, center (left is default) typeStyle: String, // h1-h6, body, small, label, caption (see Figma or Confluence documentation) fontWeight: String, // bold=500, default is 400 cmsWidgetName: String, @@ -28,15 +28,11 @@ export default { diff --git a/src/layouts/quote/cash-or-insurance-question/cash-or-insurance-question.vue b/src/layouts/quote/cash-or-insurance-question/cash-or-insurance-question.vue index 45fef683a..a2afb1a7c 100644 --- a/src/layouts/quote/cash-or-insurance-question/cash-or-insurance-question.vue +++ b/src/layouts/quote/cash-or-insurance-question/cash-or-insurance-question.vue @@ -5,7 +5,7 @@ :groupName="groupName" buttonTypeString="listButtonHorizontal" v-model="selectedValues" - :additionalButtonData="{ additionalButtonStyling: 'listButtonHorizontalStrong' }" + :additionalButtonData="additionalButtonData" isRequired /> @@ -27,6 +27,11 @@ export default { answersFromCms() { return this.getCmsContent(this.cmsWidgetName, "Answers"); }, + additionalButtonData() { + return { + additionalButtonStyling: "listButtonHorizontalStrong", + }; + }, selectedValues: { get: function () { // Convert to CMS answer name from bool diff --git a/src/layouts/schedule/constants/schedule-constants.js b/src/layouts/schedule/constants/schedule-constants.js new file mode 100644 index 000000000..89b4a4c1a --- /dev/null +++ b/src/layouts/schedule/constants/schedule-constants.js @@ -0,0 +1,11 @@ +const AppointmentTypeStrings = { + IN_SHOP: "Inshop", + MOBILE: "Mobile", + DROP_OFF: "Dropoff", +}; + +const PREMIUM_TIME_SLOT_ID_FLAG = "-premium"; + +const PREMIUM_FEE_PART_TYPE = "EARLY BIRD"; + +export { AppointmentTypeStrings, PREMIUM_TIME_SLOT_ID_FLAG, PREMIUM_FEE_PART_TYPE }; diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index d5e3b4c28..37bdc8622 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -25,17 +25,17 @@ v-model="selectedDate" :customSelectableDatesCallback="getAvailableDatesMethod" @date-clicked="openInshopTimeSlotsModal" /> - { +const getAvailableDates = async (startDate, endDate, appointmentType, providerNumber) => { // USING DATES PASSED, MAKE AN API CALL - const newShopTimeSlotsResponse = await baseMixin.methods.dispatchStoreAction( - storeActions.GET_SHOP_TIME_SLOTS, - { - startDate: startDate, - endDate: endDate, - shopAppointmentType: appointmentType, - }, - false - ); - const newShopTimeSlots = newShopTimeSlotsResponse.data; + + let newTimeSlotsResponse; + if (appointmentType === AppointmentTypeStrings.MOBILE) { + newTimeSlotsResponse = await baseMixin.methods.dispatchStoreAction( + storeActions.GET_MOBILE_TIME_SLOTS, + { + startDate: startDate, + endDate: endDate, + }, + false + ); + } else { + newTimeSlotsResponse = await baseMixin.methods.dispatchStoreAction( + storeActions.GET_SHOP_TIME_SLOTS, + { + startDate: startDate, + endDate: endDate, + shopAppointmentType: appointmentType, + providerNumber: providerNumber, + }, + false + ); + } + + const newShopTimeSlots = newTimeSlotsResponse.data; return convertApiResponse(newShopTimeSlots); }; const convertApiResponse = (responseData) => { @@ -112,9 +128,12 @@ export default { data() { return { selectedDate: null, - selectedTimeSlotId: null, + selectedTimeSlotData: { + id: null, + isPremiumAppointment: null, + }, selectableDatesData: [], - mobileEarlyBirdFee: null, + mobilePremiumAppointmentFee: null, }; }, async beforeRouteEnter(to, from, next) { @@ -125,40 +144,29 @@ export default { selectableDatesSetting: "custom", initialViewRowsToShow: 5, customSelectableDatesCallback: getAvailableDates, - - /* vvvvv SAVE THESE FOR TESTING PURPOSES FOR NOW vvvvv - - // todayOverrideDateString: "2023-04-29T03:00:00", // show partial - // todayOverrideDateString: "2023-04-30T03:00:00", // - // todayOverrideDateString: "2023-05-02T03:00:00", // ONE MONTH ONLY - // todayOverrideDateString: "2023-05-06T03:00:00", // ONE MONTH ONLY - // todayOverrideDateString: "2023-05-07T03:00:00", // show partial - // todayOverrideDateString: "2023-05-30T03:00:00", // - // todayOverrideDateString: "2023-06-30T03:00:00", // - // todayOverrideDateString: "2023-07-01T03:00:00", // show partial && ONE MONTH ONLY - // todayOverrideDateString: "2023-07-02T03:00:00", // ONE MONTH ONLY - // todayOverrideDateString: "2023-07-12T03:00:00", // show partial - // todayOverrideDateString: "2023-08-31T03:00:00", - // todayOverrideDateString: "2023-09-30T03:00:00", // show partial - */ }); // Price EARLY BIRD pre-emptively to allow for asynchronous call to pricing const pricingPromise = baseMixin.methods.dispatchStoreAction( storeActions.PRICE_ORDER_ITEMS_AND_SAVE_SERVER_DATA, { - availableLineItems: [{ partNumber: "EARLY BIRD" }], + availableLineItems: [ + { + partNumber: PREMIUM_FEE_PART_TYPE, + partType: PREMIUM_FEE_PART_TYPE, + description: null, + }, + ], }, false ); - const earlyBirdPromise = baseMixin.methods.dispatchStoreAction( + const premiumFeePromise = baseMixin.methods.dispatchStoreAction( storeActions.GET_MOBILE_EARLY_BIRD_FEE ); const alertReasonsPromise = locationAlerts.methods.loadInitialData( store.getters.order.serviceLocation.zipCodeCtu ); - // Settle promises and get results const promiseResultMap = [ { @@ -174,8 +182,8 @@ export default { promise: datePickerInitialDataPromise, }, { - resultKey: "earlyBird", - promise: earlyBirdPromise, + resultKey: "premiumFee", + promise: premiumFeePromise, }, { resultKey: "pricingResults", @@ -191,8 +199,8 @@ export default { vm.$refs.datePicker.initializeComponent(resultMap.datePickerInitialData); vm.$refs.locationAlerts.initializeComponent(resultMap.alertReasons); vm.selectableDatesData = resultMap.datePickerInitialData.initialShopTimeSlotsResponse; - if (resultMap.earlyBird) { - vm.mobileEarlyBirdFee = resultMap.pricingResults[0]; + if (resultMap.premiumFee) { + vm.mobilePremiumAppointmentFee = resultMap.pricingResults[0]; } }); }, @@ -216,18 +224,18 @@ export default { ); }, appointmentDateAndTime() { - if (!this.selectedTimeSlotId) { + if (!this.selectedTimeSlotData.id) { return null; } const timeSlotSelectedObject = this.getTimeSlotObjectFromTimeSlotId( - this.selectedTimeSlotId + this.selectedTimeSlotData.id ); if (timeSlotSelectedObject) { return { date: this.selectedDate.dateString, startTime: timeSlotSelectedObject.startTime, endTime: timeSlotSelectedObject.endTime, - id: this.selectedTimeSlotId, + id: this.selectedTimeSlotData.id, }; } else { return null; @@ -247,7 +255,8 @@ export default { const newShopTimeSlots = await getAvailableDates( startDate, endDate, - this.appointmentType + this.appointmentType, + this.$store.getters.order.serviceLocation.provider.providerNumber ); // ADD API CALL RESULTS TO EXISTING DATE DATA this.selectableDatesData.days = this.selectableDatesData.days.concat( @@ -268,11 +277,63 @@ export default { return timeSlots.find((timeSlot) => timeSlot.id === timeSlotId); }, timeSlotModalClosed() { - // Clear the selectedDate if no timeslot has been selected - if (!this.selectedTimeSlotId) { + // Clear the selectedDate if no timeSlot has been selected + if (!this.selectedTimeSlotData.id) { this.selectedDate = null; } }, + updateFooterButtonText(timeSlotData) { + let funnelFooterButtonText; + if (!timeSlotData.id) { + funnelFooterButtonText = "Continue"; + } else { + funnelFooterButtonText = + "Select " + this.convertSelectedDateToShortMonthAndDay(this.selectedDate); + + if (this.appointmentType === AppointmentTypeStrings.IN_SHOP) { + funnelFooterButtonText += + " at " + + this.getDisplayTextForMilitaryTime(this.appointmentDateAndTime.startTime); + } else if ( + this.appointmentType === AppointmentTypeStrings.MOBILE && + !timeSlotData.isPremiumAppointment + ) { + funnelFooterButtonText += + " at " + + this.getDisplayTextForMilitaryTime( + this.appointmentDateAndTime.startTime, + true + ) + + " - " + + this.getDisplayTextForMilitaryTime( + this.appointmentDateAndTime.endTime, + true + ); + } + } + + this.$refs.funnelFooter.updateButtonText(funnelFooterButtonText); + }, + convertSelectedDateToShortMonthAndDay(selectedDate) { + // This conversion ensures we don't get get GMT induced date changes + const dateObject = new Date(`${selectedDate.dateString}T00:00:00`); + // Ex: April 25 + return dateObject.toLocaleDateString("en-us", { month: "short", day: "numeric" }); + }, + // Expected input: "HH:MM:SS" + getDisplayTextForMilitaryTime(militaryTimeInput, shouldTrimMinutesIfEmpty = false) { + let hours = parseInt(militaryTimeInput.split(":")[0]); + const minutes = militaryTimeInput.split(":")[1]; + let meridianNotation = hours > 11 ? "PM" : "AM"; + if (hours > 12) { + hours -= 12; + } + if (shouldTrimMinutesIfEmpty && minutes === "00") { + return `${hours} ${meridianNotation}`; + } else { + return `${hours}:${minutes} ${meridianNotation}`; + } + }, backButtonAction() { this.$router.navigateWithoutSaving(this.navigationScenarios.CLICKED_BACK, this.$route); }, @@ -296,9 +357,15 @@ export default { selectedDate(newValue, oldValue) { // Clear time slot selection if date selected changes if (newValue !== oldValue) { - this.selectedTimeSlotId = null; + this.selectedTimeSlotData = { + id: null, + isPremiumAppointment: null, + }; } }, + selectedTimeSlotData() { + this.updateFooterButtonText(this.selectedTimeSlotData); + }, }, components: { funnelHeader, diff --git a/src/layouts/schedule/time-slot-modal-question/timeslot-modal-list-button/timeslot-modal-list-button.vue b/src/layouts/schedule/time-slot-modal-question/time-slot-modal-list-button/time-slot-modal-list-button.vue similarity index 72% rename from src/layouts/schedule/time-slot-modal-question/timeslot-modal-list-button/timeslot-modal-list-button.vue rename to src/layouts/schedule/time-slot-modal-question/time-slot-modal-list-button/time-slot-modal-list-button.vue index 60b2f9e81..44ac0d9e8 100644 --- a/src/layouts/schedule/time-slot-modal-question/timeslot-modal-list-button/timeslot-modal-list-button.vue +++ b/src/layouts/schedule/time-slot-modal-question/time-slot-modal-list-button/time-slot-modal-list-button.vue @@ -6,45 +6,32 @@
- + {{ buttonLabel }} - - - {{ formattedButtonLabelSubCopy }} + + {{ formattedButtonLabelSubCopy }} + {{ screenReaderOnlyText }} -
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 a73ab712d..079df757d 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 @@ -14,12 +14,12 @@ typeStyle="small" class="duration-text-block" /> @@ -41,7 +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 timeslotModalListButton from "./timeslot-modal-list-button/timeslot-modal-list-button"; +import timeSlotModalListButton from "./time-slot-modal-list-button/time-slot-modal-list-button"; // TODO: Move this somewhere more global import { DAYS_OF_WEEK, MONTHS_OF_YEAR } from "@/digital-components/date-picker/mixins/constants.js"; @@ -49,15 +49,17 @@ import { defineRule, useField } from "vee-validate"; import { errorMessages } from "@/constants/error-messages"; import { required } from "@/helpers/validation-rules"; +// Constants +import { + AppointmentTypeStrings, + PREMIUM_TIME_SLOT_ID_FLAG, + PREMIUM_FEE_PART_TYPE, +} from "../constants/schedule-constants"; + // Validation for the modal button defineRule("time-slot-required", required(errorMessages.OPTION_REQUIRED)); // Constants -const AppointmentTypeStrings = { - IN_SHOP: "Inshop", - MOBILE: "Mobile", - DROP_OFF: "Dropoff", -}; export default { name: "timeSlotModalQuestion", @@ -65,19 +67,21 @@ export default { modelValue: Object, cmsWidgetName: String, mobileCmsWidgetName: String, - earlyBirdCmsWidgetName: String, + mobilePremiumCmsWidgetName: String, dropoffCmsWidgetName: String, + sameDayDropOffCmsWidgetName: String, appointmentType: String, dateAndTimeSlotData: Object, - mobileEarlyBirdFee: Object, + premiumAppointmentFee: Object, estimatedServiceMinutesMinimum: Number, estimatedServiceMinutesMaximum: Number, validationRules: String, }, data() { return { - selectedTimeSlot: null, - timeslotModalListButton: timeslotModalListButton, + selectedTimeSlotId: null, + isSelectedAppointmentPremium: null, + timeSlotModalListButton: timeSlotModalListButton, }; }, setup(props) { @@ -89,42 +93,35 @@ export default { }, watch: { modelValue() { - this.selectedTimeSlot = this.modelValue; // Run component validation that is used at parent level - this.handleChange(this.modelValue); + this.handleChange(this.modelValue.id); + }, + dateAndTimeSlotData(newValue, oldValue) { + this.autoSelectTimeSlotIfOnlyOneIsAvailable(newValue); }, - // dateAndTimeSlotData(newValue, oldValue) { - // const numberOfOptions = newValue?.timeSlots.length; - // console.log('running'); - // if (numberOfOptions === 1) { - // this.selectedTimeSlot = newValue.timeSlots[0].id; - // } - // } }, computed: { supplementalInformationBlock() { let appointmentTypeCmsWidgetName; - let cmsFieldName = "BodyText"; if (this.appointmentType === AppointmentTypeStrings.IN_SHOP) { return null; } else if (this.appointmentType === AppointmentTypeStrings.MOBILE) { appointmentTypeCmsWidgetName = - this.selectedTimeSlot === this.earlyBirdButtonText - ? this.earlyBirdCmsWidgetName + this.selectedTimeSlotId === this.premiumAppointmentButtonText + ? this.mobilePremiumCmsWidgetName : this.mobileCmsWidgetName; } else { - appointmentTypeCmsWidgetName = this.dropoffCmsWidgetName; - if (this.isSameDay) { - cmsFieldName = "BodyText2"; - } + appointmentTypeCmsWidgetName = this.isSameDay + ? this.sameDayDropOffCmsWidgetName + : this.dropoffCmsWidgetName; } - return this.getCmsContent(appointmentTypeCmsWidgetName, cmsFieldName); + return this.getCmsContent(appointmentTypeCmsWidgetName, "BodyText"); }, footerCloseButtonText() { return this.getCmsContent(this.cmsWidgetName, "FooterText"); }, - earlyBirdButtonText() { - return this.getCmsContent(this.earlyBirdCmsWidgetName, "HeaderText"); + premiumAppointmentButtonText() { + return this.getCmsContent(this.mobilePremiumCmsWidgetName, "HeaderText"); }, dropoffButtonText() { return this.getCmsContent(this.dropoffCmsWidgetName, "HeaderText"); @@ -167,53 +164,24 @@ export default { } // This conversion ensures we don't get get GMT induced date changes const dateObject = new Date(`${this.dateAndTimeSlotData.dateString}T00:00:00`); - const weekdayName = DAYS_OF_WEEK[dateObject.getDay()]; - const month = MONTHS_OF_YEAR[dateObject.getMonth()]; - const numberDayOfMonth = dateObject.getDate(); - // Ex. Tuesday, April 23 - return `${weekdayName}, ${month} ${numberDayOfMonth}`; + // Ex: Tuesday, April 22 + return dateObject.toLocaleDateString("en-us", { + weekday: "long", + month: "long", + day: "numeric", + }); }, availableTimeSlots() { if (this.dateAndTimeSlotData === null) { return null; } - let availableTimeSlots; if (this.appointmentType === AppointmentTypeStrings.DROP_OFF) { - availableTimeSlots = [ - { - value: this.dateAndTimeSlotData.timeSlots[0], - buttonLabel: this.dropoffButtonText, - }, - ]; + return this.getAvailableTimeSlotsForDropOff(this.dateAndTimeSlotData.timeSlots); + } else if (this.appointmentType === AppointmentTypeStrings.MOBILE) { + return this.getAvailableTimeSlotsForMobile(this.dateAndTimeSlotData.timeSlots); } else { - availableTimeSlots = this.dateAndTimeSlotData.timeSlots.map((timeslot) => { - let readableTime; - if (this.appointmentType === AppointmentTypeStrings.IN_SHOP) { - readableTime = this.getDisplayTextForMilitaryTime(timeslot.startTime); - } else if (this.appointmentType === AppointmentTypeStrings.MOBILE) { - readableTime = `${this.getDisplayTextForMilitaryTime( - timeslot.startTime - )} - ${this.getDisplayTextForMilitaryTime(timeslot.endTime)}`; - } - return { - value: timeslot.id, - buttonLabel: readableTime, - }; - }); + return this.getAvailableTimeSlotsForInshop(this.dateAndTimeSlotData.timeSlots); } - - if (this.appointmentType === AppointmentTypeStrings.MOBILE) { - const offerPremium = this.dateAndTimeSlotData.timeSlots[0].offerPremium; - const hasEarlyBird = this.mobileEarlyBirdFee?.partType === "EARLY BIRD"; - if (offerPremium && hasEarlyBird) { - availableTimeSlots.unshift({ - value: this.dateAndTimeSlotData.timeSlots[0].id + "-earlybird", - buttonLabel: this.earlyBirdButtonText, - buttonLabelSubCopy: this.getTotalLineItemPrice(this.mobileEarlyBirdFee), - }); - } - } - return availableTimeSlots; }, }, methods: { @@ -222,22 +190,36 @@ export default { }, // fires any time the footer button is used, is fired before "onModalClosed" closeModal() { - this.$emit("update:modelValue", this.selectedTimeSlot); + if (this.selectedTimeSlotId.toString().includes(PREMIUM_TIME_SLOT_ID_FLAG)) { + this.selectedTimeSlotId = this.removePremiumFlagFromInput(this.selectedTimeSlotId); + this.isSelectedAppointmentPremium = true; + } else { + this.isSelectedAppointmentPremium = false; + } + const selectedTimeSlotData = { + id: this.selectedTimeSlotId, + isPremiumAppointment: this.isSelectedAppointmentPremium, + }; + this.$emit("update:modelValue", selectedTimeSlotData); this.$refs["timeSlots"].closeModal(); }, // fires any time the modal is closed, AFTER "closeModal" fires if footer button is used onModalClosed() { - this.selectedTimeSlot = this.modelValue; + this.isSelectedAppointmentPremium = this.modelValue.isPremiumAppointment; + if (this.isSelectedAppointmentPremium) { + this.selectedTimeSlotId = this.addPremiumFlagToInput(this.modelValue.id); + } else { + this.selectedTimeSlotId = this.modelValue.id; + } this.$emit("time-slot-modal-closed"); }, // Expected input: "HH:MM:SS" getDisplayTextForMilitaryTime(militaryTimeInput) { let hours = parseInt(militaryTimeInput.split(":")[0]); const minutes = militaryTimeInput.split(":")[1]; - let meridianNotation = "AM"; - if (militaryTimeInput.split(":")[0] > 12) { + let meridianNotation = hours > 11 ? "PM" : "AM"; + if (hours > 12) { hours -= 12; - meridianNotation = "PM"; } return `${hours}:${minutes} ${meridianNotation}`; }, @@ -252,6 +234,77 @@ export default { } return displayTextForDurationLength; }, + getAvailableTimeSlotsForInshop(timeSlotsForSelectedDate) { + return timeSlotsForSelectedDate.map((timeSlot) => { + const readableTime = this.getDisplayTextForMilitaryTime(timeSlot.startTime); + return { + value: timeSlot.id, + buttonLabel: readableTime, + }; + }); + }, + getAvailableTimeSlotsForDropOff(timeSlotsForSelectedDate) { + return [ + { + value: timeSlotsForSelectedDate[0].id, + buttonLabel: this.dropoffButtonText, + }, + ]; + }, + getAvailableTimeSlotsForMobile(timeSlotsForSelectedDate) { + const availableTimeSlots = timeSlotsForSelectedDate.map((timeSlot) => { + const readableTime = `${this.getDisplayTextForMilitaryTime( + timeSlot.startTime + )} - ${this.getDisplayTextForMilitaryTime(timeSlot.endTime)}`; + return { + value: timeSlot.id, + buttonLabel: readableTime, + }; + }); + + const isPremiumTimeSlot = timeSlotsForSelectedDate[0].offerPremium; + const hasPremiumPartAvailable = + this.premiumAppointmentFee?.partType === PREMIUM_FEE_PART_TYPE; + if (isPremiumTimeSlot && hasPremiumPartAvailable) { + availableTimeSlots.unshift( + this.getPremiumAppointmentTimeSlot(timeSlotsForSelectedDate[0]) + ); + } + + return availableTimeSlots; + }, + getPremiumAppointmentTimeSlot(timeSlotData) { + const formattedPrice = + "+$" + this.getTotalLineItemPrice(this.premiumAppointmentFee).toFixed(2); + return { + // Unique value is required for each and the premium appoinment shares a timeSlot ID + value: this.addPremiumFlagToInput(timeSlotData.id), + buttonLabel: this.premiumAppointmentButtonText, + buttonLabelSubCopy: formattedPrice, + additionalButtonData: { + isPremiumAppointment: true, + }, + }; + }, + autoSelectTimeSlotIfOnlyOneIsAvailable(newdateAndTimeSlotDataValue) { + const numberOfOptions = newdateAndTimeSlotDataValue?.timeSlots.length; + if ( + numberOfOptions === 1 && + !( + this.appointmentType === AppointmentTypeStrings.MOBILE && + this.premiumAppointmentFee && + newdateAndTimeSlotDataValue.timeSlots[0].offerPremium + ) + ) { + this.selectedTimeSlotId = newdateAndTimeSlotDataValue.timeSlots[0].id; + } + }, + addPremiumFlagToInput(timeSlotId) { + return (timeSlotId += PREMIUM_TIME_SLOT_ID_FLAG); + }, + removePremiumFlagFromInput(timeSlotId) { + return parseInt(timeSlotId.trim(PREMIUM_TIME_SLOT_ID_FLAG.length)); + }, }, components: { modal, diff --git a/src/layouts/service-location/helpers/service-location-helper/service-location-helper.js b/src/layouts/service-location/helpers/service-location-helper/service-location-helper.js index 53f4e5797..f998b3188 100644 --- a/src/layouts/service-location/helpers/service-location-helper/service-location-helper.js +++ b/src/layouts/service-location/helpers/service-location-helper/service-location-helper.js @@ -44,7 +44,7 @@ export async function getServiceabilityDetails(serviceZipCode, lineItems) { return Promise.resolve(serviceabilityDetails); } -export async function getAvailabilityRating(providerNumber, startDate, endDate, appointmentType) { +export async function getAvailabilityRating(startDate, endDate, shopAppointmentType, providerNumber) { // For a given shop provider number and date range, get the appointment time slots available const shopTimeSlots = await baseMixin.methods.dispatchStoreAction( storeActions.GET_SHOP_TIME_SLOTS, @@ -52,7 +52,7 @@ export async function getAvailabilityRating(providerNumber, startDate, endDate, providerNumber: providerNumber, startDate: startDate, endDate: endDate, - shopAppointmentType: appointmentType, + shopAppointmentType: shopAppointmentType, }, false ); diff --git a/src/layouts/service-location/mobile-location-modal-questions/mobile-location-modal-questions.vue b/src/layouts/service-location/mobile-location-modal-questions/mobile-location-modal-questions.vue index 48642101c..fa5c1eb62 100644 --- a/src/layouts/service-location/mobile-location-modal-questions/mobile-location-modal-questions.vue +++ b/src/layouts/service-location/mobile-location-modal-questions/mobile-location-modal-questions.vue @@ -236,6 +236,17 @@ export default { }); }, async setMobileLocation() { + // START - TEMP CODE FROM A CAOUETTE 5/25/23 TO BE REMOVED BY EOD + let tempTest = false; + let internalModelAddressQuestions = this.internalModel.addressQuestions; + let modelValueAddressQuestions = this.modelValue.addressQuestions; + if (tempTest) { + // THIS WILL NEVER BE TRUE + console.warn(internalModelAddressQuestions); + console.warn(modelValueAddressQuestions); + } + // END - TEMP CODE FROM A CAOUETTE 5/25/23 TO BE REMOVED BY EOD + if ( this.internalModel.addressQuestions.zipCode !== this.modelValue.addressQuestions.zipCode @@ -270,6 +281,13 @@ export default { this.closeModal(); } + } else { + // START - TEMP CODE FROM A CAOUETTE 5/25/23 TO BE REMOVED BY EOD + if (tempTest) { + // THIS WILL NEVER BE TRUE + console.warn("the zips match and the exception was run"); + } + // END - TEMP CODE FROM A CAOUETTE 5/25/23 TO BE REMOVED BY EOD } }, }, diff --git a/src/layouts/service-location/shop-question/shop-list-button/shop-list-button.vue b/src/layouts/service-location/shop-question/shop-list-button/shop-list-button.vue index b430f3af7..32573651f 100644 --- a/src/layouts/service-location/shop-question/shop-list-button/shop-list-button.vue +++ b/src/layouts/service-location/shop-question/shop-list-button/shop-list-button.vue @@ -52,9 +52,14 @@ export default { props: { loaderColor: String, }, - async beforeMount() { + beforeMount() { this.displayLoader(); - this.additionalButtonData.availabilityRatingCallback(this.modelValue).then((data) => { + console.log(this.value) + const startDate = this.additionalButtonData.startDate; + const endDate = this.additionalButtonData.endDate; + const shopAppointmentType = this.additionalButtonData.shopAppointmentType; + + this.additionalButtonData.availabilityRatingCallback(startDate, endDate, shopAppointmentType, this.value).then((data) => { this.availabilityRating = data; this.isLoaderDisplayed = false; }); @@ -66,7 +71,7 @@ export default { availabilityRating: "None", availabilityRatingClass: "", loaderPosition: "left", - blockUi: "no-block" + blockUi: "no-block", }; }, methods: { diff --git a/src/layouts/service-location/shop-question/shop-question.vue b/src/layouts/service-location/shop-question/shop-question.vue index 0bb9e4237..5da8598a4 100644 --- a/src/layouts/service-location/shop-question/shop-question.vue +++ b/src/layouts/service-location/shop-question/shop-question.vue @@ -106,8 +106,18 @@ export default { return this.getCmsContent("ShowMoreShopsLinkWidget", "Text"); }, additionalButtonData() { + const startDate = new Date(); + const endDate = new Date() + endDate.setDate(startDate.getDate() + 7); + + const formattedStartDate = startDate.toISOString().split("T")[0]; + const formattedEndDate = endDate.toISOString().split("T")[0]; + return { availabilityRatingCallback: getAvailabilityRating, + startDate: formattedStartDate, + endDate: formattedEndDate, + shopAppointmentType: this.selectedAppointmentType, }; }, }, @@ -208,11 +218,11 @@ export default { }, watch: { selectedAppointmentType: { - async handler(newValue) { + async handler(newValue) { this.resetAnswers(); await nextTick(); - + this.selectedProviderNumber = null; await nextTick(); @@ -225,7 +235,7 @@ export default { shopProviders: { async handler(newValue) { await nextTick(); - + if (this.selectedAppointmentType) { const selectedShopIndex = this.getSelectedProviderIndex( newValue, diff --git a/src/router/index.js b/src/router/index.js index 3abfa058d..15ba52647 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -28,21 +28,7 @@ import analyticsMixin from "@/mixins/analytics-mixin"; import { experimentTriggers } from "../constants/experiments"; import { applicationConfig } from "../constants/application-config"; -// Components -import datePicker from "@/digital-components/date-picker/date-picker.vue"; -import demoDatePicker from "@/layouts/demo-date-picker/demo-date-picker.vue"; - const routes = [ - { - path: "/demo-date-picker", // This is a temporary route for testing. - name: "demo-date-picker", - component: demoDatePicker, - }, - { - path: "/date-picker", // This is a temporary route for testing. - name: "date-picker", - component: datePicker, - }, { path: "/", name: "root", diff --git a/src/store/index.js b/src/store/index.js index a119d6978..b97c826c7 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -1136,57 +1136,121 @@ export const actions = { }); }, - getShopTimeSlots( - context, - { - providerNumber = null, - startDate = "2023-01-01", - endDate = "2023-05-01", - shopAppointmentType = "", - } - ) { + getShopTimeSlots(context, { startDate, endDate, shopAppointmentType, providerNumber }) { const order = context.state.order; - const mockArray = []; + const vehicle = context.state.order.vehicle; + + let partNumbers = [ + ...(order.lineItems.supportingItems ?? []), + ...(order.lineItems.vaps ?? []), + ...getFlattenedArrayOfLineItemsWithChildParts(order.lineItems.glassParts), + ]; + partNumbers = partNumbers.map((lineItem) => { + return lineItem.partNumber; + }); + + const glassPieces = order.damage.glassToReplace ?? []; var payload = { - providerNumber: providerNumber ?? order.providerNumber, + providerNumber: providerNumber, startDate: startDate, endDate: endDate, shopAppointmentType: shopAppointmentType, - applicationName: applicationConfig.APPLICATION_NAME, + applicationName: "Safelite.com funnel", parentAccountNumber: context.getters.payment.parentAccountNumber, - carId: context.getters.vehicle.carId, - partNumbers: mockArray, // TODO: WHERE DO I GET THIS? + carId: vehicle.carId, + partNumbers: partNumbers, + glassPieces: glassPieces, eon: order.eon, - provisionalReasons: mockArray, // TODO: WHERE DO I GET THIS? + coverage: { + status: "", + deductible: 0, + additionalAuthFlag: "", + }, + partSelection: { + // TODO: Provisional booking will utilize these fields + hasAnsweredPartQuestions: false, + hasAnsweredMoldingQuestions: false, + hasAnsweredCapabilityQuestions: false, + hasManuallySelectedParts: false, + }, + vehicle: { + year: vehicle.year, + make: vehicle.make, + model: vehicle.model, + style: vehicle.style, + vin: vehicle.vin, + }, }; - // TODO: REMOVE MOCK CALL & USE REAL CALL BELOW - return globalMethods.callMockHttpClient({ + console.log(payload) + + return globalMethods.callHttpClient({ method: endpoints.GetShopTimeSlots.method, - endpoint: endpoints.GetShopTimeSlots.mockUrl, + endpoint: endpoints.GetShopTimeSlots.url, payload: payload, + logApiCall: false, + }); + }, + + getMobileTimeSlots(context, { startDate, endDate }) { + const order = context.state.order; + const vehicle = context.state.order.vehicle; + let partNumbers = [ + ...(order.lineItems.supportingItems ?? []), + ...(order.lineItems.vaps ?? []), + ...getFlattenedArrayOfLineItemsWithChildParts(order.lineItems.glassParts), + ]; + partNumbers = partNumbers.map((lineItem) => { + return lineItem.partNumber; + }); + const glassPieces = order.damage.glassToReplace ?? []; + var payload = { + startDate: startDate, + endDate: endDate, + applicationName: "Safelite.com funnel", + parentAccountNumber: context.getters.payment.parentAccountNumber, + carId: vehicle.carId, + partNumbers: partNumbers, + glassPieces: glassPieces, + eon: order.eon, + coverage: { + status: "", + deductible: 0, + additionalAuthFlag: "", + }, + partSelection: { + // TODO: Provisional booking will utilize these fields + hasAnsweredPartQuestions: false, + hasAnsweredMoldingQuestions: false, + hasAnsweredCapabilityQuestions: false, + hasManuallySelectedParts: false, + }, + vehicle: { + year: vehicle.year, + make: vehicle.make, + model: vehicle.model, + style: vehicle.style, + vin: vehicle.vin, + }, + zipCode: order.serviceLocation.zipCode, + }; + return globalMethods.callHttpClient({ + method: endpoints.GetMobileTimeSlots.method, + endpoint: endpoints.GetMobileTimeSlots.url, + payload: payload, + logApiCall: false, }); - // TODO: RESTORE THIS - // return globalMethods.callHttpClient({ - // method: endpoints.GetShopTimeSlots.method, - // endpoint: endpoints.GetShopTimeSlots.url, - // payload: payload, - // logApiCall: false, - // }); }, getMobileEarlyBirdFee(context) { const damageType = context.getters.damage.isRepair ? "Repair" : "Replace"; const paymentType = context.getters.order.payment.isInsurance ? "Insurance" : "Cash"; - return globalMethods.callMockHttpClient({ + + return globalMethods.callHttpClient({ method: endpoints.GetMobileEarlyBirdFee.method, - endpoint: `${endpoints.GetMobileEarlyBirdFee.mockUrl}/Cash/Replace`, + endpoint: `${endpoints.GetMobileEarlyBirdFee.url}/${paymentType}/${damageType}`, }); - // return globalMethods.callHttpClient({ - // method: endpoints.GetMobileEarlyBirdFee.method, - // endpoint: `${endpoints.GetMobileEarlyBirdFee.url}/${paymentType}/${damageType}`, - // }); }, // Session API Actions @@ -1913,7 +1977,7 @@ function addPricesToLineItems(lineItems, pricingLineItems) { function getFlattenedArrayOfLineItemsWithChildParts(lineItems) { let flattenedArray = []; - lineItems.forEach((lineItem) => { + lineItems?.forEach((lineItem) => { flattenedArray.push(lineItem); if (lineItem.childParts) { flattenedArray = [ diff --git a/src/ux-components/loader/loader.vue b/src/ux-components/loader/loader.vue index d890fdbfe..f304b79fa 100644 --- a/src/ux-components/loader/loader.vue +++ b/src/ux-components/loader/loader.vue @@ -21,8 +21,8 @@ export default { }, blockUi: { type: Boolean, - default: true - } + default: true, + }, }, };