CSR-1137 | More changes

Added early-bird styling
Added auto select if only one option
Refactored early-bird names
Moved constants to constant file
Changed time-slot-modal-question v-model to reflect premium appointment
Moved same-day dropoff content to new CMS widget
This commit is contained in:
Scott Kiener 2023-05-24 10:37:43 -04:00
parent 991e000c4e
commit 98889512d6
5 changed files with 126 additions and 70 deletions

View file

@ -5,7 +5,7 @@
:groupName="groupName" :groupName="groupName"
buttonTypeString="listButtonHorizontal" buttonTypeString="listButtonHorizontal"
v-model="selectedValues" v-model="selectedValues"
:additionalButtonData="{ additionalButtonStyling: 'listButtonHorizontalStrong' }" :additionalButtonData="additionalButtonData"
isRequired /> isRequired />
</div> </div>
</template> </template>
@ -27,6 +27,11 @@ export default {
answersFromCms() { answersFromCms() {
return this.getCmsContent(this.cmsWidgetName, "Answers"); return this.getCmsContent(this.cmsWidgetName, "Answers");
}, },
additionalButtonData() {
return {
additionalButtonStyling: 'listButtonHorizontalStrong'
};
},
selectedValues: { selectedValues: {
get: function () { get: function () {
// Convert to CMS answer name from bool // Convert to CMS answer name from bool

View file

@ -0,0 +1,11 @@
const AppointmentTypeStrings = {
IN_SHOP: "Inshop",
MOBILE: "Mobile",
DROP_OFF: "Dropoff",
};
const PREMIUM_TIME_SLOT_ID_FLAG = "-premium";
const PREMIUM_FEE_PART_TYPE = "EARLY BIRD";
export { AppointmentTypeStrings, PREMIUM_TIME_SLOT_ID_FLAG, PREMIUM_FEE_PART_TYPE };

View file

@ -39,13 +39,14 @@
<time-slot-modal-question <time-slot-modal-question
ref="timeSlotModalQuestion" ref="timeSlotModalQuestion"
cmsWidgetName="TimeSlotModalQuestion" cmsWidgetName="TimeSlotModalQuestion"
earlyBirdCmsWidgetName="EarlyBirdTimeSlotModal" mobilePremiumCmsWidgetName="EarlyBirdTimeSlotModal"
mobileCmsWidgetName="MobileTimeSlotModal" mobileCmsWidgetName="MobileTimeSlotModal"
dropoffCmsWidgetName="DropOffTimeSlotModal" dropoffCmsWidgetName="DropOffTimeSlotModal"
v-model="selectedTimeSlotId" sameDayDropOffCmsWidgetName="SameDayDropOffTimeSlotModal"
v-model="selectedTimeSlotData"
@time-slot-modal-closed="timeSlotModalClosed" @time-slot-modal-closed="timeSlotModalClosed"
:appointmentType="appointmentType" :appointmentType="appointmentType"
:mobileEarlyBirdFee="mobileEarlyBirdFee" :premiumAppointmentFee="mobilePremiumAppointmentFee"
:dateAndTimeSlotData="timeSlotsForSelectedDate" :dateAndTimeSlotData="timeSlotsForSelectedDate"
:estimatedServiceMinutesMinimum="selectableDatesData.estimatedServiceMinutesMinimum" :estimatedServiceMinutesMinimum="selectableDatesData.estimatedServiceMinutesMinimum"
:estimatedServiceMinutesMaximum="selectableDatesData.estimatedServiceMinutesMaximum" :estimatedServiceMinutesMaximum="selectableDatesData.estimatedServiceMinutesMaximum"
@ -84,6 +85,7 @@ import {
getRouterLinkRouteFromCopy, getRouterLinkRouteFromCopy,
getRouterLinkDisplayTextFromCopy, getRouterLinkDisplayTextFromCopy,
} from "@/helpers/cms-content-helper"; } from "@/helpers/cms-content-helper";
import {PREMIUM_FEE_PART_TYPE} from "./constants/schedule-constants";
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 store from "@/store"; import store from "@/store";
@ -123,10 +125,13 @@ export default {
data() { data() {
return { return {
selectedDate: null, selectedDate: null,
selectedTimeSlotId: null, selectedTimeSlotData: {
id: null,
isPremiumAppointment: null,
},
selectableDatesData: [], selectableDatesData: [],
weatherAlerts: [], weatherAlerts: [],
mobileEarlyBirdFee: null, mobilePremiumAppointmentFee: null,
}; };
}, },
async beforeRouteEnter(to, from, next) { async beforeRouteEnter(to, from, next) {
@ -159,12 +164,12 @@ export default {
const pricingPromise = baseMixin.methods.dispatchStoreAction( const pricingPromise = baseMixin.methods.dispatchStoreAction(
storeActions.PRICE_ORDER_ITEMS_AND_SAVE_SERVER_DATA, storeActions.PRICE_ORDER_ITEMS_AND_SAVE_SERVER_DATA,
{ {
availableLineItems: [{ partNumber: "EARLY BIRD" }], availableLineItems: [{ partNumber: PREMIUM_FEE_PART_TYPE, partType: PREMIUM_FEE_PART_TYPE, description: null }],
}, },
false false
); );
const earlyBirdPromise = baseMixin.methods.dispatchStoreAction( const premiumFeePromise = baseMixin.methods.dispatchStoreAction(
storeActions.GET_MOBILE_EARLY_BIRD_FEE storeActions.GET_MOBILE_EARLY_BIRD_FEE
); );
const alertReasonsPromise = getAlertReasons(store.getters.order.serviceLocation.zipCodeCtu); const alertReasonsPromise = getAlertReasons(store.getters.order.serviceLocation.zipCodeCtu);
@ -184,8 +189,8 @@ export default {
promise: datePickerInitialDataPromise, promise: datePickerInitialDataPromise,
}, },
{ {
resultKey: "earlyBird", resultKey: "premiumFee",
promise: earlyBirdPromise, promise: premiumFeePromise,
}, },
{ {
resultKey: "pricingResults", resultKey: "pricingResults",
@ -201,8 +206,8 @@ export default {
vm.setData(resultMap.alertReasons); vm.setData(resultMap.alertReasons);
vm.$refs.datePicker.initializeComponent(resultMap.datePickerInitialData); vm.$refs.datePicker.initializeComponent(resultMap.datePickerInitialData);
vm.selectableDatesData = resultMap.datePickerInitialData.initialShopTimeSlotsResponse; vm.selectableDatesData = resultMap.datePickerInitialData.initialShopTimeSlotsResponse;
if (resultMap.earlyBird) { if (resultMap.premiumFee) {
vm.mobileEarlyBirdFee = resultMap.pricingResults[0]; vm.mobilePremiumAppointmentFee = resultMap.pricingResults[0];
} }
}); });
}, },
@ -229,18 +234,18 @@ export default {
); );
}, },
appointmentDateAndTime() { appointmentDateAndTime() {
if (!this.selectedTimeSlotId) { if (!this.selectedTimeSlotData.id) {
return null; return null;
} }
const timeSlotSelectedObject = this.getTimeSlotObjectFromTimeSlotId( const timeSlotSelectedObject = this.getTimeSlotObjectFromTimeSlotId(
this.selectedTimeSlotId this.selectedTimeSlotData.id
); );
if (timeSlotSelectedObject) { if (timeSlotSelectedObject) {
return { return {
date: this.selectedDate.dateString, date: this.selectedDate.dateString,
startTime: timeSlotSelectedObject.startTime, startTime: timeSlotSelectedObject.startTime,
endTime: timeSlotSelectedObject.endTime, endTime: timeSlotSelectedObject.endTime,
id: this.selectedTimeSlotId, id: this.selectedTimeSlotData.id,
}; };
} else { } else {
return null; return null;
@ -310,7 +315,7 @@ export default {
}, },
timeSlotModalClosed() { timeSlotModalClosed() {
// Clear the selectedDate if no timeslot has been selected // Clear the selectedDate if no timeslot has been selected
if (!this.selectedTimeSlotId) { if (!this.selectedTimeSlotData.id) {
this.selectedDate = null; this.selectedDate = null;
} }
}, },
@ -337,7 +342,10 @@ export default {
selectedDate(newValue, oldValue) { selectedDate(newValue, oldValue) {
// Clear time slot selection if date selected changes // Clear time slot selection if date selected changes
if (newValue !== oldValue) { if (newValue !== oldValue) {
this.selectedTimeSlotId = null; this.selectedTimeSlotData = {
id: null,
isPremiumAppointment: null
};
} }
}, },
}, },

View file

@ -19,7 +19,7 @@
:answers="availableTimeSlots" :answers="availableTimeSlots"
groupName="ChooseTimeSlot" groupName="ChooseTimeSlot"
textPosition="text-center" textPosition="text-center"
v-model="selectedTimeSlot" v-model="selectedTimeSlotId"
isRequired isRequired
validationRules="time-slot-required" validationRules="time-slot-required"
class="mt-5" /> class="mt-5" />
@ -49,15 +49,13 @@ 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";
// Constants
import { AppointmentTypeStrings, PREMIUM_TIME_SLOT_ID_FLAG, PREMIUM_FEE_PART_TYPE} from "../constants/schedule-constants";
// 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 // Constants
const AppointmentTypeStrings = {
IN_SHOP: "Inshop",
MOBILE: "Mobile",
DROP_OFF: "Dropoff",
};
export default { export default {
name: "timeSlotModalQuestion", name: "timeSlotModalQuestion",
@ -65,18 +63,20 @@ export default {
modelValue: Object, modelValue: Object,
cmsWidgetName: String, cmsWidgetName: String,
mobileCmsWidgetName: String, mobileCmsWidgetName: String,
earlyBirdCmsWidgetName: String, mobilePremiumCmsWidgetName: String,
dropoffCmsWidgetName: String, dropoffCmsWidgetName: String,
sameDayDropOffCmsWidgetName: String,
appointmentType: String, appointmentType: String,
dateAndTimeSlotData: Object, dateAndTimeSlotData: Object,
mobileEarlyBirdFee: Object, premiumAppointmentFee: Object,
estimatedServiceMinutesMinimum: Number, estimatedServiceMinutesMinimum: Number,
estimatedServiceMinutesMaximum: Number, estimatedServiceMinutesMaximum: Number,
validationRules: String, validationRules: String,
}, },
data() { data() {
return { return {
selectedTimeSlot: null, selectedTimeSlotId: null,
isSelectedAppointmentPremium: null,
timeslotModalListButton: timeslotModalListButton, timeslotModalListButton: timeslotModalListButton,
}; };
}, },
@ -89,42 +89,36 @@ export default {
}, },
watch: { watch: {
modelValue() { modelValue() {
this.selectedTimeSlot = this.modelValue;
// Run component validation that is used at parent level // Run component validation that is used at parent level
this.handleChange(this.modelValue); this.handleChange(this.modelValue.id);
}, },
// dateAndTimeSlotData(newValue, oldValue) { dateAndTimeSlotData(newValue, oldValue) {
// const numberOfOptions = newValue?.timeSlots.length; const numberOfOptions = newValue?.timeSlots.length;
// console.log('running'); if (numberOfOptions === 1) {
// if (numberOfOptions === 1) { this.selectedTimeSlotId = newValue.timeSlots[0].id;
// this.selectedTimeSlot = newValue.timeSlots[0].id; }
// } }
// }
}, },
computed: { computed: {
supplementalInformationBlock() { supplementalInformationBlock() {
let appointmentTypeCmsWidgetName; let appointmentTypeCmsWidgetName;
let cmsFieldName = "BodyText";
if (this.appointmentType === AppointmentTypeStrings.IN_SHOP) { if (this.appointmentType === AppointmentTypeStrings.IN_SHOP) {
return null; return null;
} else if (this.appointmentType === AppointmentTypeStrings.MOBILE) { } else if (this.appointmentType === AppointmentTypeStrings.MOBILE) {
appointmentTypeCmsWidgetName = appointmentTypeCmsWidgetName =
this.selectedTimeSlot === this.earlyBirdButtonText this.selectedTimeSlotId === this.premiumAppointmentButtonText
? this.earlyBirdCmsWidgetName ? this.mobilePremiumCmsWidgetName
: this.mobileCmsWidgetName; : this.mobileCmsWidgetName;
} else { } else {
appointmentTypeCmsWidgetName = this.dropoffCmsWidgetName; appointmentTypeCmsWidgetName = this.isSameDay ? this.sameDayDropOffCmsWidgetName : this.dropoffCmsWidgetName;
if (this.isSameDay) {
cmsFieldName = "BodyText2";
} }
} return this.getCmsContent(appointmentTypeCmsWidgetName, "BodyText");
return this.getCmsContent(appointmentTypeCmsWidgetName, cmsFieldName);
}, },
footerCloseButtonText() { footerCloseButtonText() {
return this.getCmsContent(this.cmsWidgetName, "FooterText"); return this.getCmsContent(this.cmsWidgetName, "FooterText");
}, },
earlyBirdButtonText() { premiumAppointmentButtonText() {
return this.getCmsContent(this.earlyBirdCmsWidgetName, "HeaderText"); return this.getCmsContent(this.mobilePremiumCmsWidgetName, "HeaderText");
}, },
dropoffButtonText() { dropoffButtonText() {
return this.getCmsContent(this.dropoffCmsWidgetName, "HeaderText"); return this.getCmsContent(this.dropoffCmsWidgetName, "HeaderText");
@ -136,10 +130,7 @@ export default {
return this.getCmsContent(this.dropoffCmsWidgetName, "SubheaderText"); return this.getCmsContent(this.dropoffCmsWidgetName, "SubheaderText");
}, },
inshopDurationText() { inshopDurationText() {
const inshopDurationTextWithoutTime = this.getCmsContent( const inshopDurationTextWithoutTime = this.getCmsContent(this.cmsWidgetName, "SubheaderText");
this.cmsWidgetName,
"SubheaderText"
);
const inshopDurationTime = this.getDisplayTextForDurationLength( const inshopDurationTime = this.getDisplayTextForDurationLength(
this.estimatedServiceMinutesMinimum, this.estimatedServiceMinutesMinimum,
this.estimatedServiceMinutesMaximum this.estimatedServiceMinutesMaximum
@ -203,13 +194,18 @@ export default {
} }
if (this.appointmentType === AppointmentTypeStrings.MOBILE) { if (this.appointmentType === AppointmentTypeStrings.MOBILE) {
const offerPremium = this.dateAndTimeSlotData.timeSlots[0].offerPremium; const isPremiumTimeSlot = this.dateAndTimeSlotData.timeSlots[0].offerPremium;
const hasEarlyBird = this.mobileEarlyBirdFee?.partType === "EARLY BIRD"; const hasPremiumPartAvailable = this.premiumAppointmentFee?.partType === PREMIUM_FEE_PART_TYPE;
if (offerPremium && hasEarlyBird) { if (isPremiumTimeSlot && hasPremiumPartAvailable) {
const formattedPrice = "+$" + this.getTotalLineItemPrice(this.premiumAppointmentFee).toFixed(2);
availableTimeSlots.unshift({ availableTimeSlots.unshift({
value: this.dateAndTimeSlotData.timeSlots[0].id + "-earlybird", // Unique value is required for each <input> and the premium appoinment shares a timeslot ID
buttonLabel: this.earlyBirdButtonText, value: this.addPremiumFlagToInput(this.dateAndTimeSlotData.timeSlots[0].id),
buttonLabelSubCopy: this.getTotalLineItemPrice(this.mobileEarlyBirdFee), buttonLabel: this.premiumAppointmentButtonText,
buttonLabelSubCopy: formattedPrice,
additionalButtonData: {
isPremiumAppointment: true
}
}); });
} }
} }
@ -222,12 +218,27 @@ export default {
}, },
// fires any time the footer button is used, is fired before "onModalClosed" // fires any time the footer button is used, is fired before "onModalClosed"
closeModal() { closeModal() {
this.$emit("update:modelValue", this.selectedTimeSlot); if (this.selectedTimeSlotId.toString().includes(PREMIUM_TIME_SLOT_ID_FLAG)) {
this.selectedTimeSlotId = this.removePremiumFlagFromInput(this.selectedTimeSlotId);
this.isSelectedAppointmentPremium = true;
} else {
this.isSelectedAppointmentPremium = false;
}
const selectedTimeSlotData = {
id: this.selectedTimeSlotId,
isPremiumAppointment: this.isSelectedAppointmentPremium,
}
this.$emit("update:modelValue", selectedTimeSlotData);
this.$refs["timeSlots"].closeModal(); this.$refs["timeSlots"].closeModal();
}, },
// fires any time the modal is closed, AFTER "closeModal" fires if footer button is used // fires any time the modal is closed, AFTER "closeModal" fires if footer button is used
onModalClosed() { onModalClosed() {
this.selectedTimeSlot = this.modelValue; this.isSelectedAppointmentPremium = this.modelValue.isPremiumAppointment;
if (this.isSelectedAppointmentPremium) {
this.selectedTimeSlotId = this.addPremiumFlagToInput(this.modelValue.id);
} else {
this.selectedTimeSlotId = this.modelValue.id;
}
this.$emit("time-slot-modal-closed"); this.$emit("time-slot-modal-closed");
}, },
// Expected input: "HH:MM:SS" // Expected input: "HH:MM:SS"
@ -252,6 +263,12 @@ export default {
} }
return displayTextForDurationLength; return displayTextForDurationLength;
}, },
addPremiumFlagToInput(timeSlotId) {
return timeSlotId += PREMIUM_TIME_SLOT_ID_FLAG;
},
removePremiumFlagFromInput(timeSlotId) {
return parseInt(timeSlotId.trim(PREMIUM_TIME_SLOT_ID_FLAG.length));
},
}, },
components: { components: {
modal, modal,

View file

@ -6,12 +6,13 @@
<div <div
:aria-label="buttonLabel" :aria-label="buttonLabel"
class="button-content list-button-content d-flex flex-column justify-content-center py-3 px-4"> class="button-content list-button-content d-flex flex-column justify-content-center py-3 px-4">
<span class="m-0" :class="textPosition"> <span class="m-0 position-relative" :class="textPosition">
{{ buttonLabel }} {{ buttonLabel }}
</span> <span v-if="buttonLabelSubCopy" class="premium-appointment-price" :class="textPosition">
<span v-if="buttonLabelSubCopy" class="m-0 small" :class="textPosition">
{{ formattedButtonLabelSubCopy }} {{ formattedButtonLabelSubCopy }}
</span> </span>
</span>
<span v-if="screenReaderOnlyText" class="sr-only"> <span v-if="screenReaderOnlyText" class="sr-only">
{{ screenReaderOnlyText }} {{ screenReaderOnlyText }}
</span> </span>
@ -28,7 +29,7 @@ import baseInputButton from "@/digital-components/base-input-button/base-input-b
import inputButtonWrapperMixin from "@/mixins/input-button-wrapper-mixin"; import inputButtonWrapperMixin from "@/mixins/input-button-wrapper-mixin";
export default { export default {
name: "timeslotMOdalListButton", name: "timeslotModalListButton",
mixins: [inputButtonWrapperMixin], mixins: [inputButtonWrapperMixin],
props: { props: {
loaderColor: String, loaderColor: String,
@ -44,7 +45,7 @@ export default {
}, },
computed: { computed: {
formattedButtonLabelSubCopy() { formattedButtonLabelSubCopy() {
return this.buttonLabelSubCopy?.toFixed(2); return this.buttonLabelSubCopy;
}, },
}, },
methods: { methods: {
@ -85,6 +86,9 @@ export default {
font-weight: 500; font-weight: 500;
background: $blue-100; background: $blue-100;
box-shadow: 0 0 0 1px $blue; box-shadow: 0 0 0 1px $blue;
span.premium-appointment-price {
background: $green-200;
}
} }
&:checked:focus + .list-button-content { &:checked:focus + .list-button-content {
box-shadow: 0 0 0 2.5px $blue; box-shadow: 0 0 0 2.5px $blue;
@ -109,11 +113,22 @@ export default {
width: 100%; width: 100%;
outline: none; outline: none;
span { span.premium-appointment-price {
&.small { position: absolute;
background: $green-100;
border-radius: 4.5rem;
line-height: 1.25rem;
color: $green-700;
font-size: 0.75rem; font-size: 0.75rem;
color: $gray-550; margin-left:4px;
padding: 2px 8px;
font-weight: 500;
} }
} }
.position-relative {
position: relative;
} }
</style> </style>