First roll of refactoring to fix bug and make component more consistent with other modal components

This commit is contained in:
Leah Schumann 2023-07-18 09:58:01 -04:00
parent 5a9ab3b565
commit f3ae844989
3 changed files with 175 additions and 145 deletions

View file

@ -11,24 +11,24 @@
class="mb-3 text-link-small" class="mb-3 text-link-small"
marginTopSizeOverride="1" /> marginTopSizeOverride="1" />
</template> </template>
<location-alerts cmsWidgetPrefix="LocationAlert-" ref="locationAlerts" /> <locationAlerts cmsWidgetPrefix="LocationAlert-" ref="locationAlerts" />
<date-picker <datePicker
selectableDatesSetting="custom" selectableDatesSetting="custom"
ref="datePicker" ref="datePicker"
v-model="selectedDate" v-model="selectedDate"
class="text-link-small" class="text-link-small"
:customSelectableDatesCallback="getAvailableDatesMethod" :customSelectableDatesCallback="getAvailableDatesMethod"
@date-clicked="openInshopTimeSlotsModal" /> @date-clicked="openInshopTimeSlotsModal" />
<time-slot-modal-question <timeSlotModalQuestion
ref="timeSlotModalQuestion" ref="timeSlotModalQuestion"
customComponentId="timeSlotModalQuestion"
cmsWidgetName="TimeSlotModalQuestion" cmsWidgetName="TimeSlotModalQuestion"
mobilePremiumCmsWidgetName="MobilePremiumTimeSlotModal" mobilePremiumCmsWidgetName="MobilePremiumTimeSlotModal"
mobileCmsWidgetName="MobileTimeSlotModal" mobileCmsWidgetName="MobileTimeSlotModal"
dropoffCmsWidgetName="DropOffTimeSlotModal" dropoffCmsWidgetName="DropOffTimeSlotModal"
sameDayDropOffCmsWidgetName="SameDayDropOffTimeSlotModal" sameDayDropOffCmsWidgetName="SameDayDropOffTimeSlotModal"
overnightDropOffCmsWidgetName="OvernightDropOffTimeSlotModal" overnightDropOffCmsWidgetName="OvernightDropOffTimeSlotModal"
v-model="selectedTimeSlotData" v-model="selectedTimeSlot"
@time-slot-modal-closed="timeSlotModalClosed"
:appointmentType="appointmentType" :appointmentType="appointmentType"
:premiumAppointmentFee="mobilePremiumAppointmentFee" :premiumAppointmentFee="mobilePremiumAppointmentFee"
:dateAndTimeSlotData="timeSlotsForSelectedDate" :dateAndTimeSlotData="timeSlotsForSelectedDate"
@ -75,7 +75,18 @@ import { required } from "@/helpers/validation-rules";
import store from "@/store"; import store from "@/store";
defineRule("date-required", required(errorMessages.DATE_REQUIRED)); defineRule("date-required", required(errorMessages.DATE_REQUIRED));
defineRule("time-slot-selection-required", required(errorMessages.DATE_REQUIRED));
// DEFINE VALIDATION RULES
defineRule("time-slot-selection-required", (value) => {
if (value.date == null ||
value.startTime == null ||
value.endTime == null ||
value.routeCode == null ||
value.estimatedServiceMinutesMaximum) {
return errorMessages.DATE_REQUIRED;
}
return true;
});
// Define constants // Define constants
const TIME_SLOTS_CALL_DAYS_LIMIT = 34; // needs to be 34 for API limits (35 does not consistently work) 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() { data() {
return { return {
selectedDate: this.getSelectedDate(), selectedDate: this.getSelectedDate(),
selectedTimeSlotData: { selectedTimeSlot: this.getSelectedTimeSlot(),
id: this.getSelectedRouteCode(),
isPremiumAppointment: this.isMobilePremiumFeeOnOrderInVuex(),
},
selectableDatesData: [], selectableDatesData: [],
mobilePremiumAppointmentFee: null, mobilePremiumAppointmentFee: null,
}; };
@ -217,6 +225,7 @@ export default {
store.getters.order.serviceLocation.zipCodeCtu, store.getters.order.serviceLocation.zipCodeCtu,
store.getters.order.serviceLocation.provider?.address?.zipCodeCtu store.getters.order.serviceLocation.provider?.address?.zipCodeCtu
); );
// Settle promises and get results // Settle promises and get results
const promiseResultMap = [ const promiseResultMap = [
{ {
@ -248,7 +257,7 @@ export default {
vm.mobilePremiumAppointmentFee = resultMap.premiumFeeWithPrice vm.mobilePremiumAppointmentFee = resultMap.premiumFeeWithPrice
? resultMap.premiumFeeWithPrice[0] ? resultMap.premiumFeeWithPrice[0]
: null; : null;
vm.updateFooterButtonText(vm.selectedTimeSlotData); vm.updateFooterButtonText(vm.selectedTimeSlot);
}); });
}, },
computed: { computed: {
@ -266,30 +275,11 @@ export default {
if (!this.selectedDate) { if (!this.selectedDate) {
return null; return null;
} }
return this.selectableDatesData.days?.find( return this.selectableDatesData.days?.find(
(selectableDate) => selectableDate.date === this.selectedDate (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: { methods: {
splitCopyOnCMSPlaceHolder, splitCopyOnCMSPlaceHolder,
@ -328,11 +318,8 @@ export default {
openInshopTimeSlotsModal() { openInshopTimeSlotsModal() {
this.$refs["timeSlotModalQuestion"].openModal(); this.$refs["timeSlotModalQuestion"].openModal();
}, },
getTimeSlotObjectFromTimeSlotId(timeSlotId) { getSelectedTimeSlot() {
const timeSlots = this.selectableDatesData.days.find( return store.getters.order.schedule;
(selectableDate) => selectableDate.date === this.selectedDate
)?.timeSlots;
if (timeSlots) return timeSlots.find((timeSlot) => timeSlot.id === timeSlotId);
}, },
getSelectedDate() { getSelectedDate() {
return store.getters.order.schedule.date; return store.getters.order.schedule.date;
@ -340,45 +327,39 @@ export default {
getSelectedRouteCode() { getSelectedRouteCode() {
return store.getters.order.schedule.routeCode; return store.getters.order.schedule.routeCode;
}, },
getSupportingItems() {
return store.getters.lineItems.supportingItems;
},
isMobilePremiumFeeOnOrderInVuex() { isMobilePremiumFeeOnOrderInVuex() {
const supportingItemsFromVuex = store.getters.lineItems.supportingItems; const supportingItemsFromVuex = store.getters.lineItems.supportingItems;
return !!supportingItemsFromVuex.filter( return !!supportingItemsFromVuex.filter(
(lineItem) => lineItem.partType === PREMIUM_FEE_PART_TYPE (lineItem) => lineItem.partType === PREMIUM_FEE_PART_TYPE
).length; ).length;
}, },
timeSlotModalClosed() { updateFooterButtonText(timeSlot) {
// Clear the selectedDate if no timeSlot has been selected
if (!this.selectedTimeSlotData.id) {
this.selectedDate = null;
}
},
updateFooterButtonText(timeSlotData) {
let funnelFooterButtonText; let funnelFooterButtonText;
if (!timeSlotData.id || !this.appointmentDateAndTime) { if (!timeSlot || !timeSlot.date) {
funnelFooterButtonText = "Continue"; funnelFooterButtonText = "Continue";
} else { } else {
funnelFooterButtonText = funnelFooterButtonText = `Select ${this.convertSelectedDateToShortMonthAndDay(
"Select " + this.convertSelectedDateToShortMonthAndDay(this.selectedDate); timeSlot.date
)}`;
if (this.appointmentType === AppointmentTypeStrings.IN_SHOP) { if (this.appointmentType === AppointmentTypeStrings.IN_SHOP) {
funnelFooterButtonText += funnelFooterButtonText += ` at ${this.getDisplayTextForMilitaryTime(
" at " + timeSlot.startTime
this.getDisplayTextForMilitaryTime(this.appointmentDateAndTime.startTime); )}`;
} else if ( } else if (
this.appointmentType === AppointmentTypeStrings.MOBILE && this.appointmentType === AppointmentTypeStrings.MOBILE &&
!timeSlotData.isPremiumAppointment !timeSlot.isPremiumAppointment
) { ) {
funnelFooterButtonText += funnelFooterButtonText += ` at ${this.getDisplayTextForMilitaryTime(
" at " + timeSlot.startTime,
this.getDisplayTextForMilitaryTime( true
this.appointmentDateAndTime.startTime, )} - ${this.getDisplayTextForMilitaryTime(
true timeSlot.endTime,
) + true
" - " + )}`;
this.getDisplayTextForMilitaryTime(
this.appointmentDateAndTime.endTime,
true
);
} }
} }
this.$refs.funnelFooter.updateButtonText(funnelFooterButtonText); this.$refs.funnelFooter.updateButtonText(funnelFooterButtonText);
@ -408,21 +389,16 @@ export default {
}, },
forwardButtonAction() { forwardButtonAction() {
this.updateSupportingItems(); this.updateSupportingItems();
this.dispatchStoreAction( this.dispatchStoreAction(this.storeActions.SAVE_SCHEDULE, this.selectedTimeSlot, false);
this.storeActions.SAVE_SCHEDULE,
this.appointmentDateAndTime,
false
);
navigateToHeritageFunnel({ loadingModal: this.$refs.loadingModal }); navigateToHeritageFunnel({ loadingModal: this.$refs.loadingModal });
}, },
updateSupportingItems() { 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 we have a premium fee(early bird), then save/update supporting items
if ( if (
this.appointmentType === AppointmentTypeStrings.MOBILE && this.appointmentType === AppointmentTypeStrings.MOBILE &&
this.selectedTimeSlotData?.isPremiumAppointment this.selectedTimeSlot?.isPremiumAppointment
) { ) {
const earlyBirdIndex = supportingItems.findIndex( const earlyBirdIndex = supportingItems.findIndex(
(item) => item.partType == PREMIUM_FEE_PART_TYPE (item) => item.partType == PREMIUM_FEE_PART_TYPE
@ -462,17 +438,8 @@ export default {
}, },
}, },
watch: { watch: {
selectedDate(newValue, oldValue) { selectedTimeSlot(newValue) {
// Clear time slot selection if date selected changes this.updateFooterButtonText(newValue);
if (newValue !== oldValue) {
this.selectedTimeSlotData = {
id: null,
isPremiumAppointment: null,
};
}
},
selectedTimeSlotData(newValue) {
this.updateFooterButtonText(this.selectedTimeSlotData);
}, },
}, },
components: { components: {

View file

@ -6,33 +6,37 @@
:footerButtonText="footerCloseButtonText" :footerButtonText="footerCloseButtonText"
:onModalOpenedCallback="onModalOpened" :onModalOpenedCallback="onModalOpened"
:onModalClosedCallback="onModalClosed" :onModalClosedCallback="onModalClosed"
@footer-button-event="closeModal"> @isModalOpened="setModalStatus"
<textBlock @footer-button-event="setTimeSlot">
v-show="durationTextBlockCopy" <template v-if="isModalOpened">
:customText="durationTextBlockCopy" <textBlock
justifyText="center" v-show="durationTextBlockCopy"
typeStyle="small" :customText="durationTextBlockCopy"
class="duration-text-block" /> justifyText="center"
<buttonQuestion typeStyle="small"
buttonTypeString="timeSlotModalListButton" class="duration-text-block" />
:buttonTypeObject="timeSlotModalListButton" <buttonQuestion
:answers="availableTimeSlots" ref="buttonQuestion"
groupName="ChooseTimeSlot" buttonTypeString="timeSlotModalListButton"
textPosition="text-center" :buttonTypeObject="timeSlotModalListButton"
v-model="selectedTimeSlotId" class="mt-5"
isRequired :answers="availableTimeSlots"
validationRules="time-slot-required" groupName="chooseTimeSlot"
class="mt-5" /> textPosition="text-center"
<div v-model="selectedTimeSlotId"
class="mt-1 mb-2 supplemental-information" isRequired
v-if="supplementalInformationBlock" validationRules="time-slot-required" />
v-html="supplementalInformationBlock"></div> <div
<textBlock class="mt-1 mb-2 supplemental-information"
v-show="disclaimerTextBlockCopy" v-if="supplementalInformationBlock"
:customText="disclaimerTextBlockCopy" v-html="supplementalInformationBlock"></div>
justifyText="left" <textBlock
typeStyle="caption" v-show="disclaimerTextBlockCopy"
class="mb-2" /> :customText="disclaimerTextBlockCopy"
justifyText="left"
typeStyle="caption"
class="mb-2" />
</template>
</modal> </modal>
</template> </template>
@ -41,21 +45,27 @@
import modal from "@/digital-components/modal/modal"; import modal from "@/digital-components/modal/modal";
import textBlock from "@/digital-components/text-block/text-block"; import textBlock from "@/digital-components/text-block/text-block";
import buttonQuestion from "@/digital-components/button-question/button-question"; 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"; 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 { defineRule, useField } from "vee-validate";
import { errorMessages } from "@/constants/error-messages"; import { errorMessages } from "@/constants/error-messages";
import { required } from "@/helpers/validation-rules"; import { required } from "@/helpers/validation-rules";
import { v4 as uuidv4 } from "uuid";
// Constants // Constants
import { import {
AppointmentTypeStrings, AppointmentTypeStrings,
RouteCodeFlags,
PREMIUM_TIME_SLOT_ID_FLAG, PREMIUM_TIME_SLOT_ID_FLAG,
PREMIUM_FEE_PART_TYPE, PREMIUM_FEE_PART_TYPE,
RouteCodeFlags,
} from "@/constants/schedule-constants"; } from "@/constants/schedule-constants";
const cmsWidgetFieldMappings = { const cmsWidgetFieldMappings = {
MODAL_CLOSE_BUTTON: "FooterText", MODAL_CLOSE_BUTTON: "FooterText",
SUPPLEMENTAL_INFORMATION: "BodyText", SUPPLEMENTAL_INFORMATION: "BodyText",
@ -67,12 +77,18 @@ const cmsWidgetFieldMappings = {
// Validation for the modal button // Validation for the modal button
defineRule("time-slot-required", required(errorMessages.OPTION_REQUIRED)); defineRule("time-slot-required", required(errorMessages.OPTION_REQUIRED));
// Constants
export default { export default {
name: "timeSlotModalQuestion", name: "time-slot-modal-question",
emits: ["update:modelValue"],
props: { props: {
modelValue: Object, modelValue: {
type: Object,
default: () => ({
id: null,
isPremiumAppointment: null,
}),
},
cmsWidgetName: String, cmsWidgetName: String,
mobileCmsWidgetName: String, mobileCmsWidgetName: String,
mobilePremiumCmsWidgetName: String, mobilePremiumCmsWidgetName: String,
@ -85,32 +101,76 @@ export default {
estimatedServiceMinutesMinimum: Number, estimatedServiceMinutesMinimum: Number,
estimatedServiceMinutesMaximum: Number, estimatedServiceMinutesMaximum: Number,
validationRules: String, validationRules: String,
customComponentId: String,
}, },
data() { data() {
return { return {
selectedTimeSlotId: this.getModifiedSelectedTimeSlotId(), internalModel: deepClone(this.modelValue),
isModalOpened: false,
timeSlotModalListButton: timeSlotModalListButton, timeSlotModalListButton: timeSlotModalListButton,
}; };
}, },
setup(props) { setup(props) {
const { handleChange } = useField("time-slot-modal-question", props.validationRules); const uuid = uuidv4();
// Run validation on component load const componentId = !props.customComponentId
handleChange(props.modelValue.id); ? `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 { return {
componentId,
errorMessage,
handleChange, handleChange,
validate,
meta,
errors,
}; };
}, },
watch: { watch: {
modelValue() { modelValue: {
this.selectedTimeSlotId = this.getModifiedSelectedTimeSlotId(); handler(newValue) {
// Run component validation that is used at parent level this.internalModel = deepClone(newValue);
this.handleChange(this.modelValue.id); this.handleChange(newValue);
},
}, },
availableTimeSlots(newValue) { availableTimeSlots(newValue) {
this.autoSelectTimeSlotIfOnlyOneIsAvailable(newValue); this.autoSelectTimeSlotIfOnlyOneIsAvailable(newValue);
}, },
}, },
computed: { 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() { supplementalInformationBlock() {
let appointmentTypeCmsWidgetName; let appointmentTypeCmsWidgetName;
if (this.appointmentType === AppointmentTypeStrings.IN_SHOP) { if (this.appointmentType === AppointmentTypeStrings.IN_SHOP) {
@ -281,35 +341,26 @@ export default {
}, },
methods: { methods: {
openModal() { 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() { closeModal() {
let isSelectedAppointmentPremium = false; this.modal.closeModal();
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();
}, },
// fires any time the modal is closed, AFTER "closeModal" fires if footer button is used
onModalClosed() { onModalClosed() {
this.$emit("time-slot-modal-closed"); this.internalModel = deepClone(this.modelValue);
}, },
getModifiedSelectedTimeSlotId() { setTimeSlot() {
if (this.modelValue.isPremiumAppointment) { this.$emit("update:modelValue", this.internalModel);
return this.addPremiumFlagToInput(this.modelValue.id); this.closeModal();
} else {
return this.modelValue.id;
}
}, },
// Expected input: "HH:MM"
getDisplayTextForMilitaryTime(militaryTimeInput) { getDisplayTextForMilitaryTime(militaryTimeInput) {
// Expected input: "HH:MM"
let hours = parseInt(militaryTimeInput.split(":")[0]); let hours = parseInt(militaryTimeInput.split(":")[0]);
const minutes = militaryTimeInput.split(":")[1]; const minutes = militaryTimeInput.split(":")[1];
const meridianNotation = hours > 11 ? "PM" : "AM"; const meridianNotation = hours > 11 ? "PM" : "AM";
@ -414,6 +465,19 @@ export default {
removePremiumFlagFromInput(timeSlotId) { removePremiumFlagFromInput(timeSlotId) {
return timeSlotId.substring(0, timeSlotId.length - PREMIUM_TIME_SLOT_ID_FLAG.length); 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: { components: {
modal, modal,

View file

@ -265,7 +265,6 @@ export default {
modelValue: { modelValue: {
handler(newValue) { handler(newValue) {
this.internalModel = deepClone(newValue); this.internalModel = deepClone(newValue);
this.handleChange(newValue); this.handleChange(newValue);
}, },
deep: true, deep: true,