CASH-464: new copy of time-slot-modal-question without modal

This commit is contained in:
Adam Caouette 2025-04-22 11:10:56 -04:00
parent 4ac5440dcc
commit 8fbe19a7a4

View file

@ -0,0 +1,606 @@
<template>
<div class="time-slots-modal">
<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="selectedRouteCode"
isRequired
validationRules="time-slot-required" />
<div
v-if="hasWaitListExperiment && displayWaitListFeature"
class="mt-5 bg-light rounded waitlist">
<textBlock marginTopSizeOverride="0" />
<checkboxQuestion class="mt-3" v-model="waitListRequested" @click="waitListChecked" />
</div>
<div
v-if="waitListRequested"
class="mt-5 rounded waitlist-success"
ref="waitlistSuccessMessage">
<img :src="waitListSuccessImage" class="success-image" />
<span v-html="waitListSuccessText" class="success-text"></span>
</div>
<div
class="mt-5 mb-2 supplemental-information"
v-if="supplementalInformationBlock"
v-html="supplementalInformationBlock" />
<textBlock
v-show="disclaimerTextBlockCopy"
:customText="disclaimerTextBlockCopy"
justifyText="left"
typeStyle="caption"
class="mb-2" />
</div>
</template>
<script>
// Removed modal import
import textBlock from "@/digital-components/text-block/text-block";
import buttonQuestion from "@/digital-components/button-question/button-question";
import checkboxQuestion from "@/digital-components/checkbox-question/checkbox-question";
import timeSlotModalListButton from "@/layouts/schedule/time-slot-modal-question/time-slot-modal-list-button/time-slot-modal-list-button.vue";
import experimentMixin from "@/mixins/experiment-mixin";
import { experimentSettings } from "@/constants/experiments";
import store from "@/store";
// Helpers
import { deepClone } from "@/helpers/object-helper";
import {
convertDateStringToDate,
militaryToTwelveHourTime,
} from "@/layouts/schedule/helpers/schedule-helper";
import { getDisplayTextForDurationLength } from "@/helpers/duration-length-helper";
// Validation
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,
} from "@/constants/schedule-constants";
const cmsWidgetFieldMappings = {
MODAL_CLOSE_BUTTON: "FooterText",
SUPPLEMENTAL_INFORMATION: "BodyText",
TIME_SLOT_BUTTON: "HeaderText",
DISCLAIMER: "FooterText",
DURATION: "SubheaderText",
};
defineRule("time-slot-required", required(errorMessages.OPTION_REQUIRED));
export default {
name: "time-slot-no-modal-question",
// Removed 'modal' emits
emits: ["update:modelValue", "TimeSlotSelected", "click-event"],
props: {
modelValue: {
type: Object,
default: () => ({
timeSlot: {
routeCode: null,
date: null,
startTime: null,
endTime: null,
jobMaxMinutes: null,
jobMinMinutes: null,
},
isPremiumAppointment: null,
}),
},
cmsWidgetName: String,
mobileCmsWidgetName: String,
mobilePremiumCmsWidgetName: String,
dropoffCmsWidgetName: String,
sameDayDropOffCmsWidgetName: String,
overnightDropOffCmsWidgetName: String,
appointmentType: String,
timeSlotsForSelectedDate: Object,
premiumAppointmentFee: Object,
estimatedServiceMinutesMinimum: Number,
estimatedServiceMinutesMaximum: Number,
validationRules: String,
customComponentId: String,
selectedDate: String,
displayWaitList: Boolean,
},
data() {
return {
selectedRouteCode: this.getSelectedRouteCode(),
timeSlotModalListButton: timeSlotModalListButton,
waitListRequested: this.getWaitListRequestedFromStore(),
};
},
setup(props) {
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,
};
},
computed: {
supplementalInformationBlock() {
let appointmentTypeCmsWidgetName;
if (this.appointmentType === AppointmentTypeStrings.IN_SHOP) {
return null;
} else if (this.appointmentType === AppointmentTypeStrings.MOBILE) {
appointmentTypeCmsWidgetName = this.selectedRouteCode?.includes(
PREMIUM_TIME_SLOT_ID_FLAG
)
? this.mobilePremiumCmsWidgetName
: this.mobileCmsWidgetName;
} else {
if (!this.selectedRouteCode) {
return null;
} else {
appointmentTypeCmsWidgetName =
this.getRelevantDropOffCmsWidgetNameForSelectedTimeSlot(
this.selectedRouteCode,
true
);
}
}
return this.getCmsContent(
appointmentTypeCmsWidgetName,
cmsWidgetFieldMappings.SUPPLEMENTAL_INFORMATION
);
},
footerCloseButtonText() {
return this.getCmsContent(
this.cmsWidgetName,
cmsWidgetFieldMappings.MODAL_CLOSE_BUTTON
);
},
premiumAppointmentButtonText() {
return this.getCmsContent(
this.mobilePremiumCmsWidgetName,
cmsWidgetFieldMappings.TIME_SLOT_BUTTON
);
},
dropoffButtonText() {
return this.getCmsContent(
this.dropoffCmsWidgetName,
cmsWidgetFieldMappings.TIME_SLOT_BUTTON
);
},
sameDayDropoffButtonText() {
return this.getCmsContent(
this.sameDayDropOffCmsWidgetName,
cmsWidgetFieldMappings.TIME_SLOT_BUTTON
);
},
overnightDropoffButtonText() {
return this.getCmsContent(
this.overnightDropOffCmsWidgetName,
cmsWidgetFieldMappings.TIME_SLOT_BUTTON
);
},
dropoffDisclaimerText() {
return this.getCmsContent(this.dropoffCmsWidgetName, cmsWidgetFieldMappings.DISCLAIMER);
},
sameDayDropOffDisclaimerText() {
return this.getCmsContent(
this.sameDayDropOffCmsWidgetName,
cmsWidgetFieldMappings.DISCLAIMER
);
},
overnightDropOffDisclaimerText() {
return this.getCmsContent(
this.overnightDropOffCmsWidgetName,
cmsWidgetFieldMappings.DISCLAIMER
);
},
disclaimerTextBlockCopy() {
if (this.appointmentType === AppointmentTypeStrings.DROP_OFF) {
if (this.selectedRouteCode?.includes(RouteCodeFlags.ALL_DAY_DROP_OFF)) {
if (this.isSameDay) {
return this.sameDayDropOffDisclaimerText;
} else {
return this.dropoffDisclaimerText;
}
} else if (this.selectedRouteCode?.includes(RouteCodeFlags.OVERNIGHT_DROP_OFF)) {
return this.overnightDropOffDisclaimerText;
} else {
return null;
}
} else {
return null;
}
},
dropOffDurationText() {
return this.getCmsContent(this.dropoffCmsWidgetName, cmsWidgetFieldMappings.DURATION);
},
sameDayDropoffDurationText() {
return this.getCmsContent(
this.sameDayDropOffCmsWidgetName,
cmsWidgetFieldMappings.DURATION
);
},
overnightDropoffDurationText() {
return this.getCmsContent(
this.overnightDropOffCmsWidgetName,
cmsWidgetFieldMappings.DURATION
);
},
inshopDurationText() {
const inshopDurationTextWithoutTime = this.getCmsContent(
this.cmsWidgetName,
cmsWidgetFieldMappings.DURATION
);
const inshopDurationTime = getDisplayTextForDurationLength(
this.estimatedServiceMinutesMinimum,
this.estimatedServiceMinutesMaximum
);
return `${inshopDurationTextWithoutTime} ${inshopDurationTime}`;
},
durationTextBlockCopy() {
if (this.appointmentType === AppointmentTypeStrings.MOBILE) {
return null;
} else if (this.appointmentType === AppointmentTypeStrings.IN_SHOP) {
return this.inshopDurationText;
} else {
if (this.selectedRouteCode?.includes(RouteCodeFlags.OVERNIGHT_DROP_OFF)) {
return this.overnightDropoffDurationText;
} else if (this.selectedRouteCode?.includes(RouteCodeFlags.ALL_DAY_DROP_OFF)) {
if (this.isSameDay) {
return this.sameDayDropoffDurationText;
} else {
return this.dropOffDurationText;
}
} else {
return null;
}
}
},
isSameDay() {
console.log("%c TSQ - calculating isSameDay; this.selectedDate ", "color: beige;", this.selectedDate);
console.log("%c TSQ - calculating isSameDay; IS THIS WORKING OK? ", "color: beige;");
debugger; // eslint-disable-line no-debugger
if (!this.timeSlotsForSelectedDate) {
return false;
}
// const selectedDate = this.timeSlotsForSelectedDate.date;
const todaysDate = new Date().toISOString().split("T")[0];
return this.selectedDate === todaysDate;
},
dateSelectedReadableDate() {
if (!this.timeSlotsForSelectedDate) {
return null;
}
// This conversion ensures we don't get get GMT induced date changes
const dateObject = convertDateStringToDate(this.timeSlotsForSelectedDate.date);
// Ex: Tuesday, April 22
return dateObject.toLocaleDateString("en-us", {
weekday: "long",
month: "long",
day: "numeric",
});
},
availableTimeSlots() {
if (!this.timeSlotsForSelectedDate) {
return null;
}
if (this.appointmentType === AppointmentTypeStrings.DROP_OFF) {
return this.getAvailableTimeSlotsForDropOff(
this.timeSlotsForSelectedDate.timeSlots
);
} else if (this.appointmentType === AppointmentTypeStrings.MOBILE) {
return this.getAvailableTimeSlotsForMobile(this.timeSlotsForSelectedDate.timeSlots);
} else {
return this.getAvailableTimeSlotsForInshop(this.timeSlotsForSelectedDate.timeSlots);
}
},
hasWaitListExperiment() {
return experimentMixin.methods.hasSettingEqualTo(
experimentSettings.DISPLAY_WAITLIST,
"true"
);
},
displayWaitListFeature() {
return this.displayWaitList;
},
waitListSuccessImage() {
return this.getCmsContent("WaitListSuccessWidget", "Image");
},
waitListSuccessText() {
return this.getCmsContent("WaitListSuccessWidget", "BodyText");
},
},
methods: {
async setSelectedTimeSlot() {
this.$emit(
"update:modelValue",
this.getSelectedTimeSlotInfoObject(this.selectedRouteCode)
);
this.$emit("TimeSlotSelected");
},
getRelevantDropOffCmsWidgetNameForSelectedTimeSlot(
selectedRouteCode,
isSameDayRelevant = false
) {
if (selectedRouteCode.includes(RouteCodeFlags.OVERNIGHT_DROP_OFF)) {
return this.overnightDropOffCmsWidgetName;
} else if (selectedRouteCode.includes(RouteCodeFlags.ALL_DAY_DROP_OFF)) {
return this.isSameDay && isSameDayRelevant
? this.sameDayDropOffCmsWidgetName
: this.dropoffCmsWidgetName;
}
},
getAvailableTimeSlotsForInshop(timeSlotsForSelectedDate) {
return timeSlotsForSelectedDate.map((timeSlot) => {
const readableTime = militaryToTwelveHourTime(timeSlot.startTime);
return {
value: timeSlot.id,
buttonLabel: readableTime,
};
});
},
getAvailableTimeSlotsForDropOff(timeSlotsForSelectedDate) {
const availableTimeSlots = timeSlotsForSelectedDate.map((timeSlot) => {
let buttonLabelValue;
if (timeSlot.id.includes(RouteCodeFlags.OVERNIGHT_DROP_OFF)) {
buttonLabelValue = this.overnightDropoffButtonText;
} else if (this.isSameDay) {
buttonLabelValue = this.sameDayDropoffButtonText;
} else {
buttonLabelValue = this.dropoffButtonText;
}
return {
value: timeSlot.id,
buttonLabel: buttonLabelValue,
};
});
return availableTimeSlots;
},
getAvailableTimeSlotsForMobile(timeSlotsForSelectedDate) {
const availableTimeSlots = timeSlotsForSelectedDate.map((timeSlot) => {
const readableTime = `${militaryToTwelveHourTime(timeSlot.startTime)} - ${militaryToTwelveHourTime(
timeSlot.endTime
)}`;
return {
value: timeSlot.id,
buttonLabel: readableTime,
};
});
const isPremiumTimeSlot = timeSlotsForSelectedDate[0].offerPremium;
const hasPremiumPartAvailable =
this.premiumAppointmentFee?.partType === PREMIUM_FEE_PART_TYPE;
if (isPremiumTimeSlot && hasPremiumPartAvailable) {
availableTimeSlots.unshift(
this.getPremiumAppointmentTimeSlot(timeSlotsForSelectedDate[0])
);
}
return availableTimeSlots;
},
getPremiumAppointmentTimeSlot(timeSlotData) {
const formattedPrice =
"+$" + this.getTotalLineItemPrice(this.premiumAppointmentFee).toFixed(2);
return {
// Unique value is required for each <input> and the premium appointment shares an id
value: this.addPremiumFlagToInput(timeSlotData.id),
buttonLabel: this.premiumAppointmentButtonText,
buttonLabelSubCopy: formattedPrice,
additionalButtonData: {
isPremiumAppointment: true,
},
};
},
getSelectedRouteCode() {
let selectedRouteCode;
if (!this.modelValue?.timeSlot) {
selectedRouteCode = null;
}
if (this.modelValue?.isPremiumAppointment) {
selectedRouteCode = this.addPremiumFlagToInput(
this.modelValue?.timeSlot?.routeCode
);
} else {
selectedRouteCode = this.modelValue?.timeSlot?.routeCode;
}
return selectedRouteCode;
},
getWaitListRequestedFromStore() {
return store.getters.order.customer?.waitListRequested;
},
autoSelectTimeSlotIfOnlyOneIsAvailable() {
const numberOfOptions = this.availableTimeSlots?.length;
if (numberOfOptions === 1) {
this.selectedRouteCode = this.availableTimeSlots[0].value;
}
},
addPremiumFlagToInput(routeCode) {
return (routeCode += PREMIUM_TIME_SLOT_ID_FLAG);
},
removePremiumFlagFromInput(routeCode) {
return routeCode.replace(PREMIUM_TIME_SLOT_ID_FLAG, "");
},
getSelectedTimeSlotInfoObject(routeCode) {
const routeCodeIncludesPremium = routeCode?.includes(PREMIUM_TIME_SLOT_ID_FLAG);
if (routeCodeIncludesPremium) {
routeCode = this.removePremiumFlagFromInput(routeCode);
}
const timeSlot = this.timeSlotsForSelectedDate?.timeSlots?.find(
(slot) => slot.id == routeCode
);
if (timeSlot) {
return {
timeSlot: {
date: this.timeSlotsForSelectedDate.date,
routeCode: timeSlot.id,
startTime: timeSlot.startTime,
endTime: timeSlot.endTime,
jobMaxMinutes: this.estimatedServiceMinutesMaximum?.toString() || null,
jobMinMinutes: this.estimatedServiceMinutesMinimum?.toString() || null,
},
isPremiumAppointment: routeCodeIncludesPremium ? true : false,
};
}
return {
timeSlot: {
date: null,
routeCode: null,
startTime: null,
endTime: null,
jobMaxMinutes: null,
jobMinMinutes: null,
},
isPremiumAppointment: null,
};
},
waitListChecked(event) {
this.$emit("waitListRequested", event.target.checked);
},
scrollToSuccessMessage() {
const successMessageElement = this.$refs.waitlistSuccessMessage;
if (successMessageElement) {
successMessageElement.scrollIntoView({ behavior: "smooth" });
}
},
},
watch: {
waitListRequested(newVal) {
if (newVal) {
this.$nextTick(() => {
this.scrollToSuccessMessage();
});
}
},
modelValue: {
handler(newValue) {
this.handleChange(newValue);
},
deep: true,
},
selectedDate: {
handler() {
this.selectedRouteCode = null;
this.autoSelectTimeSlotIfOnlyOneIsAvailable();
},
},
selectedRouteCode(newVal) {
if (newVal) {
this.setSelectedTimeSlot();
}
},
},
components: {
// Removed modal
textBlock,
buttonQuestion,
checkboxQuestion,
},
};
</script>
<style lang="scss">
.time-slots-modal {
.modal-header {
padding-bottom: 0;
margin-bottom: 0 !important;
}
.text-block.duration-text-block {
margin-top: 4px !important;
}
.supplemental-information {
line-height: 1.5rem;
font-size: 0.875rem;
li strong {
font-weight: bold;
}
li:not(:last-child) {
margin-bottom: 8px;
}
}
.waitlist {
box-shadow: 0px 1px 4px 0px #00000033;
border-radius: 8px;
font-size: 1rem;
padding: 16px;
margin-top: 16px;
margin-bottom: -8px;
.ui-checkbox {
padding-left: 0px;
input {
border-radius: 4px;
box-shadow: 0px 1px 4px 0px #00000033;
}
}
.form-check-input {
margin-left: 0px;
}
}
.waitlist-success {
background-color: #ecf5e9;
display: flex;
align-items: flex-start;
border: 1px solid #0c7e47;
border-radius: 5px;
font-size: 1.125rem;
padding: 0.75rem 1rem;
margin-bottom: -0.5rem;
.success-text {
margin-left: 0.5rem;
}
.success-image {
margin-top: 4.5px;
width: 1rem;
height: 1rem;
color: #0c7e47;
}
}
}
</style>