diff --git a/src/layouts/schedule/multi-location-modal/multi-location-modal.vue b/src/layouts/schedule/multi-location-modal/multi-location-modal.vue index effa3c91b..68f8d4f7c 100644 --- a/src/layouts/schedule/multi-location-modal/multi-location-modal.vue +++ b/src/layouts/schedule/multi-location-modal/multi-location-modal.vue @@ -103,6 +103,7 @@ export default { timeSlot: this.selectedAppointment.timeSlot, routeCode: this.selectedAppointment.timeSlot.id, date: this.selectedAppointment.date, + provider: this.selectedAppointment.provider, }; this.confirmedAppointment = true; this.$emit("confirm-appointment", { diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index c5a5f9ac6..77f79f197 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -252,6 +252,8 @@ import { sumDateString, isDropOffRouteCode, getTodayDate, + getTodayDateString, + getInitialViewWeeks, } from "@/layouts/schedule/helpers/schedule-helper"; import { containsRecalParts, anyPartWithRequiresRecalFlag } from "@/helpers/recal-helper"; @@ -809,13 +811,6 @@ export default { }; }, selectedShopAnswer() { - const toTitleCase = (str) => { - if (!str) return ""; - return str.replace(/\w\S*/g, function (txt) { - return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(); - }); - }; - if ( this.selectedProvider && this.selectedProvider.address && @@ -824,18 +819,11 @@ export default { const provider = this.shopProviderData?.shopProviders?.find( (p) => p.providerNumber === this.selectedProvider?.providerNumber ); - const streetAddress = toTitleCase(this.selectedProvider.address.streetAddress); - const city = toTitleCase(this.selectedProvider.address.city); - const state = this.selectedProvider.address.state; - const zipCode = this.selectedProvider.address.zipCode; - const distanceInMiles = provider ? Math.round(provider.distanceInMiles * 2) / 2 : 0; + const distanceInMiles = provider?.distanceInMiles + ? Math.round(provider.distanceInMiles * 2) / 2 + : 0; - return { - city: `${city}`, - distance: `${distanceInMiles} mi`, - address1: `${streetAddress}`, - address2: `${city}, ${state} ${zipCode}`, - }; + return this.getFormattedShopAddress(provider?.address, distanceInMiles); } return []; }, @@ -977,6 +965,17 @@ export default { "true" ); }, + initialViewStartDate() { + return getTodayDateString(); + }, + initialViewEndDate() { + const initialViewWeeks = getInitialViewWeeks( + this.initialViewStartDate, + NUMBER_OF_CALENDAR_ROWS_TO_SHOW_FOR_INITIAL_VIEW, + this.preSelectedDate + ); + return initialViewWeeks[initialViewWeeks.length - 1].weekEndDate; + }, }, methods: { splitCopyOnCMSPlaceHolder, @@ -2054,7 +2053,14 @@ export default { } }, async showMultiLocationModal() { - if (!this.selectedRouteCodeData?.routeCode) { + const showMultiLocationAppointment = experimentMixin.methods.getSettingValue( + experimentSettings.SHOW_MULTI_LOCATION_APPT + ); + + if ( + !this.selectedRouteCodeData?.routeCode && + showMultiLocationAppointment?.toLowerCase() === "true" + ) { let maxDayRangeToShowPmTimeslot = experimentMixin.methods.getSettingValue( experimentSettings.SHOW_PM_DAYS_MULTI_LOCATION ); @@ -2064,9 +2070,6 @@ export default { let maxDayRangeToShowAnyTimeslot = experimentMixin.methods.getSettingValue( experimentSettings.SHOW_NO_AVAILABILE_DAYS_MULTI_LOCATION ); - let showMultiLocationAppointment = experimentMixin.methods.getSettingValue( - experimentSettings.SHOW_MULTI_LOCATION_APPT - ); let experimentSettingsString = "ShowPm:" + maxDayRangeToShowPmTimeslot + @@ -2089,25 +2092,23 @@ export default { }; // Fetch all appointments - const [firstMobileAMAppt, firstMobilePMAppt, firstInshopAMAppt, firstInshopPMAppt] = - await Promise.all([ - this.getFirstAvailableMobileApptByTOD("AM"), - this.getFirstAvailableMobileApptByTOD("PM"), - this.getFirstAvailableInshopApptByTOD("AM"), - this.getFirstAvailableInshopApptByTOD("PM"), - ]); + const [firstMobileAMAppt, firstMobilePMAppt, firstInshopAppts] = await Promise.all([ + this.getFirstAvailableMobileApptByTOD("AM"), + this.getFirstAvailableMobileApptByTOD("PM"), + this.getFirstAvailableInshopApptsByTOD(["AM", "PM"]), + ]); // Extract dates const firstMobileAMDate = firstMobileAMAppt?.date; const firstMobilePMDate = firstMobilePMAppt?.date; - const firstInshopAMDate = firstInshopAMAppt?.date; - const firstInshopPMDate = firstInshopPMAppt?.date; + const firstInshopAMDate = firstInshopAppts[0]?.date; + const firstInshopPMDate = firstInshopAppts[1]?.date; // Calculate days until appointments const numberOfDaysToFirstMobileAMDate = calculateDaysUntilDate(firstMobileAMAppt); const numberOfDaysToFirstMobilePMDate = calculateDaysUntilDate(firstMobilePMAppt); - const numberOfDaysToFirstInshopAMDate = calculateDaysUntilDate(firstInshopAMAppt); - const numberOfDaysToFirstInshopPMDate = calculateDaysUntilDate(firstInshopPMAppt); + const numberOfDaysToFirstInshopAMDate = calculateDaysUntilDate(firstInshopAppts[0]); + const numberOfDaysToFirstInshopPMDate = calculateDaysUntilDate(firstInshopAppts[1]); const shouldExposeMultiLocationModal = () => { const isMobileAppointmentInDateRange = @@ -2121,10 +2122,7 @@ export default { (firstInshopPMDate && numberOfDaysToFirstInshopPMDate <= maxDayRangeToShowAnyTimeslot); - if (showMultiLocationAppointment?.toLowerCase() === "true") { - return isMobileAppointmentInDateRange && isInshopAppointmentInDateRange; - } - return false; + return isMobileAppointmentInDateRange && isInshopAppointmentInDateRange; }; if (shouldExposeMultiLocationModal()) { @@ -2158,8 +2156,8 @@ export default { // Selects the first available inshop time slot promotedInshopAppointment = numberOfDaysToFirstInshopAMDate <= numberOfDaysToFirstInshopPMDate - ? firstInshopAMAppt - : firstInshopPMAppt; + ? firstInshopAppts[0] + : firstInshopAppts[1]; } else { if ( firstMobilePMDate && @@ -2182,15 +2180,15 @@ export default { numberOfDaysToFirstInshopPMDate <= maxDayRangeToShowPmTimeslot ) { // Selects the first available PM time slot - promotedInshopAppointment = firstInshopPMAppt; + promotedInshopAppointment = firstInshopAppts[1]; } else if ( firstInshopPMDate && numberOfDaysToFirstInshopPMDate >= maxDayRangeToShowNoPmTimeslot ) { // Selects the first available AM time slot. If none, selects the first available PM time slot - promotedInshopAppointment = firstInshopAMAppt - ? firstInshopAMAppt - : firstInshopPMAppt; + promotedInshopAppointment = firstInshopAppts[0] + ? firstInshopAppts[0] + : firstInshopAppts[1]; } } @@ -2210,6 +2208,7 @@ export default { } } }, + async getFirstAvailableMobileApptByTOD(timeOfDay) { const selectableDays = this.selectableDatesMobile?.days; if (!selectableDays?.length) { @@ -2237,42 +2236,135 @@ export default { return null; } }, - async getFirstAvailableInshopApptByTOD(timeOfDay) { - const selectableDays = this.selectableDatesInshop?.days; - if (!selectableDays?.length) { - return null; - } else { - for (const dateObj of selectableDays) { - let matchingTimeSlot = { - estimatedServiceMinutes: { - minimum: this.estimatedServiceMinutesMinimum, - maximum: this.estimatedServiceMinutesMaximum, - }, - timeSlot: null, - date: null, - addressCopy: null, - }; - const isMatchingApptDay = dateObj.timeSlots.some( - (slot) => - slot.id.includes(timeOfDay) && - (matchingTimeSlot.timeSlot = slot) && - (matchingTimeSlot.date = dateObj.date) - ); - if ( - this.selectedShopAnswer?.address1 && - this.selectedShopAnswer?.address2 - ) { - matchingTimeSlot.addressCopy = - this.selectedShopAnswer?.address1 + - ", " + - this.selectedShopAnswer?.address2; - } - if (isMatchingApptDay && matchingTimeSlot) { - return matchingTimeSlot; - } + + findMatchingInshopAppt(selectableDays, timeOfDay, provider) { + if (!selectableDays?.length) return null; + for (const dateObj of selectableDays) { + const distanceInMiles = provider?.distanceInMiles + ? Math.round(provider.distanceInMiles * 2) / 2 + : 0; + const providerAddress = this.getFormattedShopAddress( + provider?.address, + distanceInMiles + ); + let matchingTimeSlot = { + estimatedServiceMinutes: { + minimum: this.estimatedServiceMinutesMinimum, + maximum: this.estimatedServiceMinutesMaximum, + }, + timeSlot: null, + date: null, + addressCopy: providerAddress.address1 + ", " + providerAddress.address2, + provider: provider, + }; + const isMatchingApptDay = dateObj.timeSlots.some( + (slot) => + slot.id.includes(timeOfDay) && + (matchingTimeSlot.timeSlot = slot) && + (matchingTimeSlot.date = dateObj.date) + ); + if (isMatchingApptDay && matchingTimeSlot) { + return matchingTimeSlot; } - return null; } + return null; + }, + + async getFirstAvailableInshopApptsByTOD(timeOfDayArray) { + let matchingApptNearestAM1 = this.findMatchingInshopAppt( + this.selectableDatesInshop?.days, + timeOfDayArray[0], + this.selectedProvider + ); + let matchingApptNearestPM1 = this.findMatchingInshopAppt( + this.selectableDatesInshop?.days, + timeOfDayArray[1], + this.selectedProvider + ); + let matchingApptNearestAM2; + let matchingApptNearestPM2; + let matchingApptNearestAM3; + let matchingApptNearestPM3; + + const providerNearest2 = this.shopProviderData.shopProviders[1]; + const providerNearest3 = this.shopProviderData.shopProviders[2]; + + if (providerNearest2?.distanceInMiles && providerNearest2.distanceInMiles < 25) { + this.calendarLoadingStatus = "more"; + const selectableDaysFromProviderNearest2 = await getScheduleApiResponse({ + startDateString: this.initialViewStartDate, + endDateString: this.initialViewEndDate, + inshopProviderNumber: providerNearest2.providerNumber, + zipCode: this.zipCode, + includeMobileTimeSlots: false, + includeInshopTimeSlots: true, + }); + matchingApptNearestAM2 = this.findMatchingInshopAppt( + selectableDaysFromProviderNearest2?.inshopTimeSlotsData?.days, + "AM", + providerNearest2 + ); + matchingApptNearestPM2 = this.findMatchingInshopAppt( + selectableDaysFromProviderNearest2?.inshopTimeSlotsData?.days, + "PM", + providerNearest2 + ); + } + if (providerNearest3?.distanceInMiles && providerNearest3?.distanceInMiles < 25) { + this.calendarLoadingStatus = "more"; + const selectableDaysFromProviderNearest3 = await getScheduleApiResponse({ + startDateString: this.initialViewStartDate, + endDateString: this.initialViewEndDate, + inshopProviderNumber: providerNearest3.providerNumber, + zipCode: this.zipCode, + includeMobileTimeSlots: false, + includeInshopTimeSlots: true, + }); + matchingApptNearestAM3 = this.findMatchingInshopAppt( + selectableDaysFromProviderNearest3?.inshopTimeSlotsData?.days, + "AM", + providerNearest3 + ); + matchingApptNearestPM3 = this.findMatchingInshopAppt( + selectableDaysFromProviderNearest3?.inshopTimeSlotsData?.days, + "PM", + providerNearest3 + ); + } + this.calendarLoadingStatus = "none"; + + // WHICH OF THE 3 INSHOP APPTS IS THE EARLIEST? + const compareAppointments = (appt1, appt2) => { + // Handle null/undefined appointments + if (!appt1?.date) return 1; // appt1 is later (or invalid) + if (!appt2?.date) return -1; // appt2 is later (or invalid) + + // Compare dates first (YYYY-MM-DD format allows string comparison) + if (appt1.date < appt2.date) return -1; // appt1 is earlier + if (appt1.date > appt2.date) return 1; // appt2 is earlier + + // Dates are equal, compare times (HH:MM format allows string comparison) + const time1 = appt1.timeSlot?.startTime || "23:59"; + const time2 = appt2.timeSlot?.startTime || "23:59"; + + if (time1 < time2) return -1; // appt1 is earlier + if (time1 > time2) return 1; // appt2 is earlier + + return 0; // Completely equal + }; + // Find earliest AM appointment + const preferredApptAM = + [matchingApptNearestAM1, matchingApptNearestAM2, matchingApptNearestAM3] + .filter((appt) => appt?.date) // Remove null/undefined + .sort(compareAppointments)[0] || null; + + // Find earliest PM appointment + const preferredApptPM = + [matchingApptNearestPM1, matchingApptNearestPM2, matchingApptNearestPM3] + .filter((appt) => appt?.date) + .sort(compareAppointments)[0] || null; + + return [preferredApptAM, preferredApptPM]; }, updateMobileFirstTimeSlotandNavigateForward(timeSlotObj) { this.selectedMobileFirstAppointment = true; @@ -2283,21 +2375,62 @@ export default { this.forwardButtonAction(); }, updateMultiLocationTimeSlotandNavigateForward({ selectedTimeSlot, appointmentType }) { - if (appointmentType?.toLowerCase() == "mobile") { - this.appointmentType = AppointmentTypeStrings.MOBILE; - } else if (appointmentType?.toLowerCase() == "inshop") { - this.appointmentType = AppointmentTypeStrings.IN_SHOP; - } + if (!selectedTimeSlot) return; this.selectedMultiLocationAppointment = true; this.selectedDate = selectedTimeSlot.date; - this.updateSelectedProvider(); - this.updateTimeSlot(selectedTimeSlot); + if (appointmentType?.toLowerCase() == "mobile") { + this.appointmentType = AppointmentTypeStrings.MOBILE; + this.updateSelectedProvider(); + this.updateTimeSlot(selectedTimeSlot); + } else if (appointmentType?.toLowerCase() == "inshop") { + this.appointmentType = AppointmentTypeStrings.IN_SHOP; + + this.updateSelectedProvider(selectedTimeSlot.provider); + + // UPDATE SELECTED TIME SLOT INFO + this.selectedTimeSlotInfo = { + timeSlot: { + date: selectedTimeSlot.date, + routeCode: selectedTimeSlot.routeCode, + startTime: selectedTimeSlot.timeSlot.startTime, + endTime: selectedTimeSlot.timeSlot.endTime, + jobMaxMinutes: this.estimatedServiceMinutesMaximum.toString(), + jobMinMinutes: this.estimatedServiceMinutesMinimum.toString(), + }, + isPremiumAppointment: selectedTimeSlot.isPremiumAppointment ? true : false, + }; + + // UPDATE APPT TYPE + this.appointmentType = this.getInShopOrDropOffApptType(selectedTimeSlot.routeCode); + } this.forwardButtonAction(); }, updateRecalAcknowledgedAndNavigateForward(isAcknowledged) { this.isRecalAcknowledgedForScheduling = isAcknowledged; this.forwardButtonAction(); }, + getFormattedShopAddress(providerAddress, distanceInMiles) { + const toTitleCase = (str) => { + if (!str) return ""; + return str.replace(/\w\S*/g, function (txt) { + return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(); + }); + }; + + if (!providerAddress) return {}; + + const streetAddress = toTitleCase(providerAddress.streetAddress); + const city = toTitleCase(providerAddress.city); + const state = providerAddress.state; + const zipCode = providerAddress.zipCode; + + return { + city: city, + distance: `${distanceInMiles} mi`, + address1: streetAddress, + address2: `${city}, ${state} ${zipCode}`, + }; + }, }, watch: { appointmentTypeFromAppointmentTypeQuestion: {