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 @@
-
-
-
-
+ @isModalOpened="setModalStatus"
+ @footer-button-event="setSelectedTimeSlot"
+ class="time-slots-modal">
+
+
+
+
+
+
@@ -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,