diff --git a/src/digital-components/date-picker/date-picker.vue b/src/digital-components/date-picker/date-picker.vue index e6b44e130..1c38a72c2 100644 --- a/src/digital-components/date-picker/date-picker.vue +++ b/src/digital-components/date-picker/date-picker.vue @@ -84,6 +84,9 @@ import { } from "@/layouts/schedule/helpers/schedule-helper"; import { useField, ErrorMessage } from "vee-validate"; +import { deepClone } from "@/helpers/object-helper"; +import { v4 as uuidv4 } from "uuid"; + export default { name: "datePicker", data() { @@ -96,6 +99,7 @@ export default { }; }, props: { + customComponentId: String, selectableDatesSetting: { type: String, validator(value) { @@ -123,18 +127,24 @@ export default { }, }, setup(props) { - const propsClone = Object.assign({}, props); - const modelValue = propsClone.modelValue; + const uuid = uuidv4(); + const componentId = !props.customComponentId + ? `component-${uuid}` + : props.customComponentId; + + const modelValue = deepClone(props).modelValue; + const initialValue = modelValue; const fieldOptions = { value: modelValue, - initialValue: modelValue, + initialValue: initialValue, }; const { errorMessage, handleBlur, handleChange, meta, validate, errors, resetField } = - useField("date-of-month", props.validationRules, fieldOptions); + useField(componentId, props.validationRules, fieldOptions); return { + componentId, errorMessage, handleBlur, handleChange, diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index 4bb947199..d15e9acec 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -11,8 +11,9 @@ class="mb-3 text-link-small" marginTopSizeOverride="1" /> - - + - { + if ( + value.date == null || + value.startTime == 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) @@ -176,10 +190,7 @@ export default { data() { return { selectedDate: this.getSelectedDate(), - selectedTimeSlotData: { - id: this.getSelectedRouteCode(), - isPremiumAppointment: this.isMobilePremiumFeeOnOrderInVuex(), - }, + selectedTimeSlot: this.getSelectedTimeSlot(), selectableDatesData: [], mobilePremiumAppointmentFee: null, }; @@ -222,6 +233,7 @@ export default { store.getters.order.serviceLocation.zipCodeCtu, store.getters.order.serviceLocation.provider?.address?.zipCodeCtu ); + // Settle promises and get results const promiseResultMap = [ { @@ -253,7 +265,7 @@ export default { vm.mobilePremiumAppointmentFee = resultMap.premiumFeeWithPrice ? resultMap.premiumFeeWithPrice[0] : null; - vm.updateFooterButtonText(vm.selectedTimeSlotData); + vm.updateFooterButtonText(vm.selectedTimeSlot); }); }, computed: { @@ -271,30 +283,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, @@ -333,11 +326,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; @@ -345,6 +335,9 @@ 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( @@ -353,37 +346,31 @@ export default { }, timeSlotModalClosed() { // Clear the selectedDate if no timeSlot has been selected - if (!this.selectedTimeSlotData.id) { + if (this.selectedTimeSlot.routeCode == null) { 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); @@ -413,25 +400,21 @@ export default { }, forwardButtonAction() { this.updateSupportingItems(); - this.dispatchStoreAction( - this.storeActions.SAVE_SCHEDULE, - this.appointmentDateAndTime, - false - ); + this.dispatchStoreAction(this.storeActions.SAVE_SCHEDULE, this.selectedTimeSlot, false); + //Temporary, still need to determine what needs to be saved before continuing. this.$router.navigateWithoutSaving( this.navigationScenarios.CLICKED_FORWARD, this.$route ); }, - 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 @@ -474,14 +457,17 @@ export default { selectedDate(newValue, oldValue) { // Clear time slot selection if date selected changes if (newValue !== oldValue) { - this.selectedTimeSlotData = { - id: null, - isPremiumAppointment: null, + this.selectedTimeSlot = { + date: null, + routeCode: null, + startTime: null, + endTime: null, + jobMaxMinutes: 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 b278578be..13f33ae05 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 @@ -1,38 +1,41 @@ @@ -41,21 +44,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 +76,20 @@ 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: () => ({ + routeCode: null, + date: null, + startTime: null, + endTime: null, + jobMaxMinutes: null, + }), + }, cmsWidgetName: String, mobileCmsWidgetName: String, mobilePremiumCmsWidgetName: String, @@ -85,32 +102,58 @@ export default { estimatedServiceMinutesMinimum: Number, estimatedServiceMinutesMaximum: Number, validationRules: String, + customComponentId: String, + selectedDate: String, }, data() { return { - selectedTimeSlotId: this.getModifiedSelectedTimeSlotId(), + isModalOpened: false, + selectedValue: null, 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); - }, - availableTimeSlots(newValue) { - this.autoSelectTimeSlotIfOnlyOneIsAvailable(newValue); - }, - }, computed: { + modalName() { + return "timeSlots"; + }, + selectedTimeSlotId: { + get: function () { + return this.modelValue?.routeCode; + }, + 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); + }, + }, supplementalInformationBlock() { let appointmentTypeCmsWidgetName; if (this.appointmentType === AppointmentTypeStrings.IN_SHOP) { @@ -219,10 +262,12 @@ export default { this.cmsWidgetName, cmsWidgetFieldMappings.DURATION ); + const inshopDurationTime = this.getDisplayTextForDurationLength( this.estimatedServiceMinutesMinimum, this.estimatedServiceMinutesMaximum ); + return `${inshopDurationTextWithoutTime} ${inshopDurationTime}`; }, durationTextBlockCopy() { @@ -248,6 +293,7 @@ export default { if (!this.dateAndTimeSlotData) { return false; } + const selectedDate = this.dateAndTimeSlotData.date; const todaysDate = new Date().toISOString().split("T")[0]; return selectedDate === todaysDate; @@ -270,6 +316,7 @@ export default { if (!this.dateAndTimeSlotData) { return null; } + if (this.appointmentType === AppointmentTypeStrings.DROP_OFF) { return this.getAvailableTimeSlotsForDropOff(this.dateAndTimeSlotData.timeSlots); } else if (this.appointmentType === AppointmentTypeStrings.MOBILE) { @@ -278,44 +325,37 @@ export default { return this.getAvailableTimeSlotsForInshop(this.dateAndTimeSlotData.timeSlots); } }, + modal() { + return this.$refs["timeSlots"]; + }, }, methods: { openModal() { - this.$refs["timeSlots"].openModal(); + this.modal.openModal(); + }, + 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"); }, - getModifiedSelectedTimeSlotId() { - if (this.modelValue.isPremiumAppointment) { - return this.addPremiumFlagToInput(this.modelValue.id); - } else { - return this.modelValue.id; - } + async setSelectedTimeSlot() { + this.$emit("update:modelValue", this.selectedValue); + 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"; + if (hours > 12) { hours -= 12; } + return `${hours}:${minutes} ${meridianNotation}`; }, getDisplayTextForDurationLength(durationMinimum, durationMaximum) { @@ -364,11 +404,13 @@ export default { } else { buttonLabelValue = this.dropoffButtonText; } + return { value: timeSlot.id, buttonLabel: buttonLabelValue, }; }); + return availableTimeSlots; }, getAvailableTimeSlotsForMobile(timeSlotsForSelectedDate) { @@ -418,6 +460,45 @@ 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 + ); + + if (timeSlot) { + return { + date: this.dateAndTimeSlotData.date, + routeCode: timeSlot.id, + startTime: timeSlot.startTime, + endTime: timeSlot.endTime, + jobMaxMinutes: this.estimatedServiceMinutesMaximum.toString(), + }; + } + + return { + date: null, + startTime: null, + endTime: null, + routeCode: null, + jobMaxMinutes: null, + }; + }, + }, + watch: { + modelValue: { + handler(newValue) { + this.handleChange(newValue); + }, + deep: true, + }, + selectedDate: { + handler() { + this.selectedTimeSlotId = null; + }, + }, + availableTimeSlots(newValue) { + this.autoSelectTimeSlotIfOnlyOneIsAvailable(newValue); + }, }, 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,