diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index e40dd13ba..6045d3a4c 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -11,24 +11,24 @@ class="mb-3 text-link-small" marginTopSizeOverride="1" /> - - + - { + if (value.date == null || + value.startTime == null || + value.endTime == null || + value.routeCode == null || + value.estimatedServiceMinutesMaximum) { + return errorMessages.DATE_REQUIRED; + } + return true; +}); // Define constants const TIME_SLOTS_CALL_DAYS_LIMIT = 34; // needs to be 34 for API limits (35 does not consistently work) @@ -171,10 +182,7 @@ export default { data() { return { selectedDate: this.getSelectedDate(), - selectedTimeSlotData: { - id: this.getSelectedRouteCode(), - isPremiumAppointment: this.isMobilePremiumFeeOnOrderInVuex(), - }, + selectedTimeSlot: this.getSelectedTimeSlot(), selectableDatesData: [], mobilePremiumAppointmentFee: null, }; @@ -217,6 +225,7 @@ export default { store.getters.order.serviceLocation.zipCodeCtu, store.getters.order.serviceLocation.provider?.address?.zipCodeCtu ); + // Settle promises and get results const promiseResultMap = [ { @@ -248,7 +257,7 @@ export default { vm.mobilePremiumAppointmentFee = resultMap.premiumFeeWithPrice ? resultMap.premiumFeeWithPrice[0] : null; - vm.updateFooterButtonText(vm.selectedTimeSlotData); + vm.updateFooterButtonText(vm.selectedTimeSlot); }); }, computed: { @@ -266,30 +275,11 @@ export default { if (!this.selectedDate) { return null; } + return this.selectableDatesData.days?.find( (selectableDate) => selectableDate.date === this.selectedDate ); }, - appointmentDateAndTime() { - if (!this.selectedTimeSlotData.id) { - return null; - } - const timeSlotSelectedObject = this.getTimeSlotObjectFromTimeSlotId( - this.selectedTimeSlotData.id - ); - if (timeSlotSelectedObject) { - return { - date: this.selectedDate, - startTime: timeSlotSelectedObject.startTime, - endTime: timeSlotSelectedObject.endTime, - routeCode: this.selectedTimeSlotData.id, - jobMaxMinutes: - this.selectableDatesData.estimatedServiceMinutesMaximum.toString(), - }; - } else { - return null; - } - }, }, methods: { splitCopyOnCMSPlaceHolder, @@ -328,11 +318,8 @@ export default { openInshopTimeSlotsModal() { this.$refs["timeSlotModalQuestion"].openModal(); }, - getTimeSlotObjectFromTimeSlotId(timeSlotId) { - const timeSlots = this.selectableDatesData.days.find( - (selectableDate) => selectableDate.date === this.selectedDate - )?.timeSlots; - if (timeSlots) return timeSlots.find((timeSlot) => timeSlot.id === timeSlotId); + getSelectedTimeSlot() { + return store.getters.order.schedule; }, getSelectedDate() { return store.getters.order.schedule.date; @@ -340,45 +327,39 @@ export default { getSelectedRouteCode() { return store.getters.order.schedule.routeCode; }, + getSupportingItems() { + return store.getters.lineItems.supportingItems; + }, isMobilePremiumFeeOnOrderInVuex() { const supportingItemsFromVuex = store.getters.lineItems.supportingItems; return !!supportingItemsFromVuex.filter( (lineItem) => lineItem.partType === PREMIUM_FEE_PART_TYPE ).length; }, - timeSlotModalClosed() { - // Clear the selectedDate if no timeSlot has been selected - if (!this.selectedTimeSlotData.id) { - this.selectedDate = null; - } - }, - updateFooterButtonText(timeSlotData) { + updateFooterButtonText(timeSlot) { let funnelFooterButtonText; - if (!timeSlotData.id || !this.appointmentDateAndTime) { + if (!timeSlot || !timeSlot.date) { funnelFooterButtonText = "Continue"; } else { - funnelFooterButtonText = - "Select " + this.convertSelectedDateToShortMonthAndDay(this.selectedDate); + funnelFooterButtonText = `Select ${this.convertSelectedDateToShortMonthAndDay( + timeSlot.date + )}`; if (this.appointmentType === AppointmentTypeStrings.IN_SHOP) { - funnelFooterButtonText += - " at " + - this.getDisplayTextForMilitaryTime(this.appointmentDateAndTime.startTime); + funnelFooterButtonText += ` at ${this.getDisplayTextForMilitaryTime( + timeSlot.startTime + )}`; } else if ( this.appointmentType === AppointmentTypeStrings.MOBILE && - !timeSlotData.isPremiumAppointment + !timeSlot.isPremiumAppointment ) { - funnelFooterButtonText += - " at " + - this.getDisplayTextForMilitaryTime( - this.appointmentDateAndTime.startTime, - true - ) + - " - " + - this.getDisplayTextForMilitaryTime( - this.appointmentDateAndTime.endTime, - true - ); + funnelFooterButtonText += ` at ${this.getDisplayTextForMilitaryTime( + timeSlot.startTime, + true + )} - ${this.getDisplayTextForMilitaryTime( + timeSlot.endTime, + true + )}`; } } this.$refs.funnelFooter.updateButtonText(funnelFooterButtonText); @@ -408,21 +389,16 @@ export default { }, forwardButtonAction() { this.updateSupportingItems(); - this.dispatchStoreAction( - this.storeActions.SAVE_SCHEDULE, - this.appointmentDateAndTime, - false - ); + this.dispatchStoreAction(this.storeActions.SAVE_SCHEDULE, this.selectedTimeSlot, false); navigateToHeritageFunnel({ loadingModal: this.$refs.loadingModal }); }, - updateSupportingItems() { - const supportingItems = store.getters.lineItems.supportingItems; + const supportingItems = this.getSupportingItems(); // if we have a premium fee(early bird), then save/update supporting items if ( this.appointmentType === AppointmentTypeStrings.MOBILE && - this.selectedTimeSlotData?.isPremiumAppointment + this.selectedTimeSlot?.isPremiumAppointment ) { const earlyBirdIndex = supportingItems.findIndex( (item) => item.partType == PREMIUM_FEE_PART_TYPE @@ -462,17 +438,8 @@ export default { }, }, watch: { - selectedDate(newValue, oldValue) { - // Clear time slot selection if date selected changes - if (newValue !== oldValue) { - this.selectedTimeSlotData = { - id: null, - isPremiumAppointment: null, - }; - } - }, - selectedTimeSlotData(newValue) { - this.updateFooterButtonText(this.selectedTimeSlotData); + selectedTimeSlot(newValue) { + this.updateFooterButtonText(newValue); }, }, components: { 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 eff2358f0..077372bf2 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 @@ -6,33 +6,37 @@ :footerButtonText="footerCloseButtonText" :onModalOpenedCallback="onModalOpened" :onModalClosedCallback="onModalClosed" - @footer-button-event="closeModal"> - - -
- + @isModalOpened="setModalStatus" + @footer-button-event="setTimeSlot"> + @@ -41,21 +45,27 @@ 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 +// Helpers +import { convertDateStringToDate } from "@/layouts/schedule/helpers/schedule-helper"; +import { deepClone } from "@/helpers/object-helper"; + +// Validation - TODO: Move this somewhere more global? import { defineRule, useField } from "vee-validate"; import { errorMessages } from "@/constants/error-messages"; import { required } from "@/helpers/validation-rules"; +import { v4 as uuidv4 } from "uuid"; + // Constants import { AppointmentTypeStrings, + RouteCodeFlags, PREMIUM_TIME_SLOT_ID_FLAG, PREMIUM_FEE_PART_TYPE, - RouteCodeFlags, } from "@/constants/schedule-constants"; + const cmsWidgetFieldMappings = { MODAL_CLOSE_BUTTON: "FooterText", SUPPLEMENTAL_INFORMATION: "BodyText", @@ -67,12 +77,18 @@ const cmsWidgetFieldMappings = { // Validation for the modal button defineRule("time-slot-required", required(errorMessages.OPTION_REQUIRED)); -// Constants - export default { - name: "timeSlotModalQuestion", + name: "time-slot-modal-question", + emits: ["update:modelValue"], props: { - modelValue: Object, + modelValue: { + type: Object, + default: () => ({ + id: null, + isPremiumAppointment: null, + }), + }, + cmsWidgetName: String, mobileCmsWidgetName: String, mobilePremiumCmsWidgetName: String, @@ -85,32 +101,76 @@ export default { estimatedServiceMinutesMinimum: Number, estimatedServiceMinutesMaximum: Number, validationRules: String, + customComponentId: String, }, data() { return { - selectedTimeSlotId: this.getModifiedSelectedTimeSlotId(), + internalModel: deepClone(this.modelValue), + isModalOpened: false, timeSlotModalListButton: timeSlotModalListButton, }; }, setup(props) { - const { handleChange } = useField("time-slot-modal-question", props.validationRules); - // Run validation on component load - handleChange(props.modelValue.id); + const uuid = uuidv4(); + const componentId = !props.customComponentId + ? `component-${uuid}` + : props.customComponentId; + + const modelValue = deepClone(props).modelValue; + const initialValue = modelValue; + + const fieldOptions = { + value: modelValue, + initialValue: initialValue, + }; + + const { errorMessage, handleChange, meta, validate, errors } = useField( + componentId, + props.validationRules, + fieldOptions + ); + return { + componentId, + errorMessage, handleChange, + validate, + meta, + errors, }; }, watch: { - modelValue() { - this.selectedTimeSlotId = this.getModifiedSelectedTimeSlotId(); - // Run component validation that is used at parent level - this.handleChange(this.modelValue.id); + modelValue: { + handler(newValue) { + this.internalModel = deepClone(newValue); + this.handleChange(newValue); + }, }, availableTimeSlots(newValue) { this.autoSelectTimeSlotIfOnlyOneIsAvailable(newValue); }, }, computed: { + selectedValue: { + get: function () { + return this.modelValue; + }, + set: function (newValue) { + this.$emit("update:modelValue", newValue); + }, + }, + selectedTimeSlotId: { + get: function () { + return this.selectedValue?.id; + }, + set: function (newValue) { + // Button Question only supports Number, or String data types so we must get the full object to emit + this.selectedValue = this.getSelectedTimeSlotObject(newValue); + }, + }, + modal() { + return this.$refs["timeSlots"]; + }, supplementalInformationBlock() { let appointmentTypeCmsWidgetName; if (this.appointmentType === AppointmentTypeStrings.IN_SHOP) { @@ -281,35 +341,26 @@ export default { }, methods: { openModal() { - this.$refs["timeSlots"].openModal(); + this.modal.openModal(); + }, + onModalOpened() { + this.internalModel = deepClone(this.modelValue); + }, + setModalStatus(isOpened) { + this.isModalOpened = isOpened; }, - // fires any time the footer button is used, is fired before "onModalClosed" closeModal() { - let isSelectedAppointmentPremium = false; - if (this.selectedTimeSlotId.includes(PREMIUM_TIME_SLOT_ID_FLAG)) { - this.selectedTimeSlotId = this.removePremiumFlagFromInput(this.selectedTimeSlotId); - isSelectedAppointmentPremium = true; - } - const selectedTimeSlotData = { - id: this.selectedTimeSlotId, - isPremiumAppointment: isSelectedAppointmentPremium, - }; - this.$emit("update:modelValue", selectedTimeSlotData); - this.$refs["timeSlots"].closeModal(); + this.modal.closeModal(); }, - // fires any time the modal is closed, AFTER "closeModal" fires if footer button is used onModalClosed() { - this.$emit("time-slot-modal-closed"); + this.internalModel = deepClone(this.modelValue); }, - getModifiedSelectedTimeSlotId() { - if (this.modelValue.isPremiumAppointment) { - return this.addPremiumFlagToInput(this.modelValue.id); - } else { - return this.modelValue.id; - } + setTimeSlot() { + this.$emit("update:modelValue", this.internalModel); + this.closeModal(); }, - // Expected input: "HH:MM" getDisplayTextForMilitaryTime(militaryTimeInput) { + // Expected input: "HH:MM" let hours = parseInt(militaryTimeInput.split(":")[0]); const minutes = militaryTimeInput.split(":")[1]; const meridianNotation = hours > 11 ? "PM" : "AM"; @@ -414,6 +465,19 @@ export default { removePremiumFlagFromInput(timeSlotId) { return timeSlotId.substring(0, timeSlotId.length - PREMIUM_TIME_SLOT_ID_FLAG.length); }, + getSelectedTimeSlotObject(timeSlotId) { + const timeSlot = this.dateAndTimeSlotData.timeSlots?.find( + (timeSlot) => timeSlot.id == timeSlotId + ); + + return { + date: this.dateAndTimeSlotData.date, + startTime: timeSlot.startTime, + endTime: timeSlot.endTime, + routeCode: timeSlot.id, + jobMaxMinutes: this.estimatedServiceMinutesMaximum.toString(), + }; + }, }, components: { modal, 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 8c442a052..07c6535dd 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 @@ -265,7 +265,6 @@ export default { modelValue: { handler(newValue) { this.internalModel = deepClone(newValue); - this.handleChange(newValue); }, deep: true,