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"
marginTopSizeOverride="1" />
</template>
<location-alerts cmsWidgetPrefix="LocationAlert-" ref="locationAlerts" />
<date-picker
<locationAlerts cmsWidgetPrefix="LocationAlert-" ref="locationAlerts" />
<datePicker
selectableDatesSetting="custom"
ref="datePicker"
v-model="selectedDate"
class="text-link-small"
:customSelectableDatesCallback="getAvailableDatesMethod"
@date-clicked="openInshopTimeSlotsModal" />
<time-slot-modal-question
<timeSlotModalQuestion
ref="timeSlotModalQuestion"
customComponentId="timeSlotModalQuestion"
cmsWidgetName="TimeSlotModalQuestion"
mobilePremiumCmsWidgetName="MobilePremiumTimeSlotModal"
mobileCmsWidgetName="MobileTimeSlotModal"
dropoffCmsWidgetName="DropOffTimeSlotModal"
sameDayDropOffCmsWidgetName="SameDayDropOffTimeSlotModal"
overnightDropOffCmsWidgetName="OvernightDropOffTimeSlotModal"
v-model="selectedTimeSlotData"
@time-slot-modal-closed="timeSlotModalClosed"
v-model="selectedTimeSlot"
:appointmentType="appointmentType"
:premiumAppointmentFee="mobilePremiumAppointmentFee"
:dateAndTimeSlotData="timeSlotsForSelectedDate"
@ -75,7 +75,18 @@ import { required } from "@/helpers/validation-rules";
import store from "@/store";
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
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: {

View file

@ -6,33 +6,37 @@
:footerButtonText="footerCloseButtonText"
:onModalOpenedCallback="onModalOpened"
:onModalClosedCallback="onModalClosed"
@footer-button-event="closeModal">
<textBlock
v-show="durationTextBlockCopy"
:customText="durationTextBlockCopy"
justifyText="center"
typeStyle="small"
class="duration-text-block" />
<buttonQuestion
buttonTypeString="timeSlotModalListButton"
:buttonTypeObject="timeSlotModalListButton"
:answers="availableTimeSlots"
groupName="ChooseTimeSlot"
textPosition="text-center"
v-model="selectedTimeSlotId"
isRequired
validationRules="time-slot-required"
class="mt-5" />
<div
class="mt-1 mb-2 supplemental-information"
v-if="supplementalInformationBlock"
v-html="supplementalInformationBlock"></div>
<textBlock
v-show="disclaimerTextBlockCopy"
:customText="disclaimerTextBlockCopy"
justifyText="left"
typeStyle="caption"
class="mb-2" />
@isModalOpened="setModalStatus"
@footer-button-event="setTimeSlot">
<template v-if="isModalOpened">
<textBlock
v-show="durationTextBlockCopy"
:customText="durationTextBlockCopy"
justifyText="center"
typeStyle="small"
class="duration-text-block" />
<buttonQuestion
ref="buttonQuestion"
buttonTypeString="timeSlotModalListButton"
:buttonTypeObject="timeSlotModalListButton"
class="mt-5"
:answers="availableTimeSlots"
groupName="chooseTimeSlot"
textPosition="text-center"
v-model="selectedTimeSlotId"
isRequired
validationRules="time-slot-required" />
<div
class="mt-1 mb-2 supplemental-information"
v-if="supplementalInformationBlock"
v-html="supplementalInformationBlock"></div>
<textBlock
v-show="disclaimerTextBlockCopy"
:customText="disclaimerTextBlockCopy"
justifyText="left"
typeStyle="caption"
class="mb-2" />
</template>
</modal>
</template>
@ -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,

View file

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