Merge remote-tracking branch 'origin/feature/CASH-1077' into feature/CASH-1077

This commit is contained in:
Johnny shultz 2025-07-11 09:24:18 -04:00
commit 5ca35375a1
4 changed files with 112 additions and 69 deletions

View file

@ -16,10 +16,19 @@ const RouteCodeFlags = {
OVERNIGHT_DROP_OFF: "OVERNIGHT DROP OFF", OVERNIGHT_DROP_OFF: "OVERNIGHT DROP OFF",
}; };
const cmsWidgetFieldMappings = {
MODAL_CLOSE_BUTTON: "FooterText",
SUPPLEMENTAL_INFORMATION: "BodyText",
TIME_SLOT_BUTTON: "HeaderText",
DISCLAIMER: "FooterText",
DURATION: "SubheaderText",
};
export { export {
AppointmentTypeStrings, AppointmentTypeStrings,
PREMIUM_TIME_SLOT_ID_FLAG, PREMIUM_TIME_SLOT_ID_FLAG,
PREMIUM_FEE_PART_TYPE, PREMIUM_FEE_PART_TYPE,
PRICING_BY_DAY_PART_TYPE, PRICING_BY_DAY_PART_TYPE,
RouteCodeFlags, RouteCodeFlags,
cmsWidgetFieldMappings,
}; };

View file

@ -2,6 +2,17 @@
<div <div
class="date-picker text-center" class="date-picker text-center"
:class="`${calendarViewDirection} calendar-2025 ${showPricingByDayClass}`"> :class="`${calendarViewDirection} calendar-2025 ${showPricingByDayClass}`">
<div class="date-picker-header">
<funnelSubHeader cmsWidgetName="DatePickerSubHeaderWidget" class="mt-5" />
<textBlock
v-show="durationTextBlockCopy"
:customText="durationTextBlockCopy"
justifyText="center"
typeStyle="small"
marginTopSizeOverride="0"
class="duration-text-block" />
</div>
<fieldset id="date-picker-fieldset" ref="datePickerFieldset"> <fieldset id="date-picker-fieldset" ref="datePickerFieldset">
<legend class="sr-only">Select a day and time</legend> <legend class="sr-only">Select a day and time</legend>
<div <div
@ -142,7 +153,7 @@
overnightDropOffCmsWidgetName="OvernightDropOffTimeSlotModal" overnightDropOffCmsWidgetName="OvernightDropOffTimeSlotModal"
dropOffOrPickATimeQuestionCmsWidgetName="DropOffOrPickATimeQuestionWidget" dropOffOrPickATimeQuestionCmsWidgetName="DropOffOrPickATimeQuestionWidget"
:selectedDate="selectedDate" :selectedDate="selectedDate"
:appointmentType="appointmentTypeForTimeSlotQuestion" :appointmentType="appointmentType"
:premiumAppointmentFee="premiumAppointmentFee" :premiumAppointmentFee="premiumAppointmentFee"
:displayWaitList="displayWaitList" :displayWaitList="displayWaitList"
:timeSlotsForSelectedDate="timeSlotsForSelectedDate" :timeSlotsForSelectedDate="timeSlotsForSelectedDate"
@ -156,6 +167,8 @@
<script> <script>
import timeSlotQuestion from "@/layouts/schedule/time-slot-question/time-slot-question.vue"; import timeSlotQuestion from "@/layouts/schedule/time-slot-question/time-slot-question.vue";
import textBlock from "@/digital-components/text-block/text-block";
import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header";
// Supporting files // Supporting files
import loader from "@/ux-components/loader/loader"; import loader from "@/ux-components/loader/loader";
@ -166,7 +179,12 @@ import {
MONTHS_OF_YEAR, MONTHS_OF_YEAR,
DAYS_OF_WEEK, DAYS_OF_WEEK,
} from "@/digital-components/date-picker/mixins/constants"; } from "@/digital-components/date-picker/mixins/constants";
import { AppointmentTypeStrings, PREMIUM_FEE_PART_TYPE } from "@/constants/schedule-constants"; import {
AppointmentTypeStrings,
RouteCodeFlags,
PREMIUM_FEE_PART_TYPE,
cmsWidgetFieldMappings,
} from "@/constants/schedule-constants";
import { import {
selectableDaysOptions, selectableDaysOptions,
requiredParameter, requiredParameter,
@ -178,6 +196,7 @@ import {
import { useField, ErrorMessage } from "vee-validate"; import { useField, ErrorMessage } from "vee-validate";
import { deepClone } from "@/helpers/object-helper"; import { deepClone } from "@/helpers/object-helper";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
import { getDisplayTextForDurationLength } from "@/helpers/duration-length-helper";
export default { export default {
name: "datePicker", name: "datePicker",
@ -190,6 +209,7 @@ export default {
selectedTimeSlotInfo: this.getSelectedTimeSlotInfo(), selectedTimeSlotInfo: this.getSelectedTimeSlotInfo(),
selectableDatesInshop: [], // NOTE: this and the mobile version below use monthNum (1-based), NOT monthIndex (0-based) selectableDatesInshop: [], // NOTE: this and the mobile version below use monthNum (1-based), NOT monthIndex (0-based)
selectableDatesMobile: [], selectableDatesMobile: [],
durationTextBlockCopyForInshopOrDropoff: null,
}; };
}, },
props: { props: {
@ -224,7 +244,7 @@ export default {
pricingByDayUpcharge: Number, pricingByDayUpcharge: Number,
isPricingByDayExperiment: Boolean, isPricingByDayExperiment: Boolean,
timeSlotsForSelectedDate: Object, timeSlotsForSelectedDate: Object,
appointmentTypeForTimeSlotQuestion: String, appointmentType: String,
premiumAppointmentFee: Object, premiumAppointmentFee: Object,
estimatedServiceMinutesMinimum: Number, estimatedServiceMinutesMinimum: Number,
estimatedServiceMinutesMaximum: Number, estimatedServiceMinutesMaximum: Number,
@ -289,6 +309,54 @@ export default {
this.$emit("update:modelValue", newSelectedDate); this.$emit("update:modelValue", newSelectedDate);
}, },
}, },
dropOffDurationText() {
return this.getCmsContent("DropOffTimeSlotModal", cmsWidgetFieldMappings.DURATION);
},
sameDayDropoffDurationText() {
return this.getCmsContent(
"SameDayDropOffTimeSlotModal",
cmsWidgetFieldMappings.DURATION
);
},
overnightDropoffDurationText() {
return this.getCmsContent(
"OvernightDropOffTimeSlotModal",
cmsWidgetFieldMappings.DURATION
);
},
inshopDurationText() {
const inshopDurationTextWithoutTime = this.getCmsContent(
"TimeSlotModalQuestion",
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.appointmentType === AppointmentTypeStrings.IN_SHOP_OR_DROP_OFF ||
this.appointmentType === AppointmentTypeStrings.DROP_OFF
) {
return this.durationTextBlockCopyForInshopOrDropoff ?? this.inshopDurationText;
}
return null;
},
isSameDay() {
if (!this.timeSlotsForSelectedDate) {
return false;
}
const todaysDate = new Date().toISOString().split("T")[0];
return this.selectedDate === todaysDate;
},
}, },
methods: { methods: {
initializeComponent(initialData) { initializeComponent(initialData) {
@ -877,6 +945,18 @@ export default {
handleWaitListRequested(value) { handleWaitListRequested(value) {
this.$emit("waitListRequested", value); this.$emit("waitListRequested", value);
}, },
getDurationTextBlockCopyForInshopOrDropoff(selectedRouteCode) {
if (selectedRouteCode?.includes(RouteCodeFlags.OVERNIGHT_DROP_OFF)) {
return this.overnightDropoffDurationText;
} else if (selectedRouteCode?.includes(RouteCodeFlags.ALL_DAY_DROP_OFF)) {
if (this.isSameDay) {
return this.sameDayDropoffDurationText;
} else {
return this.dropOffDurationText;
}
}
return this.inshopDurationText;
},
}, },
watch: { watch: {
modelValue(newValue) { modelValue(newValue) {
@ -891,13 +971,18 @@ export default {
} }
}, },
selectedTimeSlotInfo(newValue) { selectedTimeSlotInfo(newValue) {
this.$emit("TimeSlotSelected", newValue); // NEEDED ON SCHEDULE - to update footer button text and to save to Store correctly const routeCode = newValue?.timeSlot?.routeCode;
this.durationTextBlockCopyForInshopOrDropoff =
this.getDurationTextBlockCopyForInshopOrDropoff(routeCode);
this.$emit("TimeSlotSelected", newValue); // needed to update footer button text on Schedule page and to save to Store correctly
}, },
}, },
components: { components: {
loader, loader,
ErrorMessage, ErrorMessage,
timeSlotQuestion, timeSlotQuestion,
textBlock,
funnelSubHeader,
}, },
}; };
</script> </script>
@ -913,6 +998,10 @@ export default {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
.date-picker-header {
margin-bottom: 1.5rem;
}
fieldset { fieldset {
flex-grow: 1; flex-grow: 1;
position: relative; position: relative;

View file

@ -130,7 +130,6 @@
<div class="row justify-content-center date-picker-wrapper"> <div class="row justify-content-center date-picker-wrapper">
<div class="col-md-6 col-xl-4"> <div class="col-md-6 col-xl-4">
<locationAlerts cmsWidgetPrefix="LocationAlert-" ref="locationAlerts" /> <locationAlerts cmsWidgetPrefix="LocationAlert-" ref="locationAlerts" />
<datePicker <datePicker
v-show="appointmentType" v-show="appointmentType"
customComponentId="dateQuestion" customComponentId="dateQuestion"
@ -147,7 +146,7 @@
:showPricingByDay="showPricingByDay" :showPricingByDay="showPricingByDay"
:isPricingByDayExperiment="isPricingByDayExperiment" :isPricingByDayExperiment="isPricingByDayExperiment"
:timeSlotsForSelectedDate="timeSlotsForSelectedDate" :timeSlotsForSelectedDate="timeSlotsForSelectedDate"
:appointmentTypeForTimeSlotQuestion="appointmentType" :appointmentType="appointmentType"
:premiumAppointmentFee="mobilePremiumAppointmentFee" :premiumAppointmentFee="mobilePremiumAppointmentFee"
:estimatedServiceMinutesMinimum="getServiceMinutesMin" :estimatedServiceMinutesMinimum="getServiceMinutesMin"
:estimatedServiceMinutesMaximum="getServiceMinutesMax" :estimatedServiceMinutesMaximum="getServiceMinutesMax"
@ -865,7 +864,6 @@ export default {
appointmentTypeStrings() { appointmentTypeStrings() {
return AppointmentTypeStrings; return AppointmentTypeStrings;
}, },
ChangeShopLinkText() { ChangeShopLinkText() {
return this.getCmsContent("ChangeShopLink", "Text"); return this.getCmsContent("ChangeShopLink", "Text");
}, },
@ -1704,7 +1702,7 @@ export default {
.change-location a { .change-location a {
font-family: UrbanistSemibold; font-family: UrbanistSemibold;
} }
.date-picker-wrapper { .date-picker {
border-top: 1px solid $gray-500; border-top: 1px solid $gray-500;
margin-top: 1.5rem; margin-top: 1.5rem;
} }

View file

@ -1,11 +1,5 @@
<template> <template>
<div class="time-slots-question"> <div class="time-slots-question">
<textBlock
v-show="durationTextBlockCopy"
:customText="durationTextBlockCopy"
justifyText="center"
typeStyle="small"
class="duration-text-block" />
<buttonQuestion <buttonQuestion
v-if="isDropOffAppointmentAvailable" v-if="isDropOffAppointmentAvailable"
v-model="selectedAnswerForDropOffOrInshop" v-model="selectedAnswerForDropOffOrInshop"
@ -38,6 +32,7 @@
groupName="chooseTimeSlot" groupName="chooseTimeSlot"
textPosition="text-center" textPosition="text-center"
v-model="selectedAnswerForTimeSlots" v-model="selectedAnswerForTimeSlots"
questionText="Available times:"
isRequired isRequired
validationRules="time-slot-required" /> validationRules="time-slot-required" />
<div <div
@ -103,16 +98,9 @@ import {
RouteCodeFlags, RouteCodeFlags,
PREMIUM_TIME_SLOT_ID_FLAG, PREMIUM_TIME_SLOT_ID_FLAG,
PREMIUM_FEE_PART_TYPE, PREMIUM_FEE_PART_TYPE,
cmsWidgetFieldMappings,
} from "@/constants/schedule-constants"; } from "@/constants/schedule-constants";
const cmsWidgetFieldMappings = {
MODAL_CLOSE_BUTTON: "FooterText",
SUPPLEMENTAL_INFORMATION: "BodyText",
TIME_SLOT_BUTTON: "HeaderText",
DISCLAIMER: "FooterText",
DURATION: "SubheaderText",
};
const PICK_A_TIME_BUTTON_VALUE = "pick-a-time-selected"; const PICK_A_TIME_BUTTON_VALUE = "pick-a-time-selected";
defineRule("time-slot-required", required(errorMessages.OPTION_REQUIRED)); defineRule("time-slot-required", required(errorMessages.OPTION_REQUIRED));
@ -194,7 +182,11 @@ export default {
supplementalInformationBlock() { supplementalInformationBlock() {
let appointmentTypeCmsWidgetName; let appointmentTypeCmsWidgetName;
if (this.appointmentType === AppointmentTypeStrings.IN_SHOP_OR_DROP_OFF) { if (
this.appointmentType === AppointmentTypeStrings.IN_SHOP_OR_DROP_OFF ||
this.appointmentType === AppointmentTypeStrings.IN_SHOP ||
this.appointmentType === AppointmentTypeStrings.DROP_OFF
) {
return null; return null;
// This is planned to be used again for drop-off // This is planned to be used again for drop-off
// Wrote this to be ready for that eventuality, is untested and no HTML work done yet // Wrote this to be ready for that eventuality, is untested and no HTML work done yet
@ -286,51 +278,6 @@ export default {
// return null; // 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_OR_DROP_OFF) {
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;
}
}
return this.inshopDurationText;
}
return null;
},
isSameDay() { isSameDay() {
if (!this.timeSlotsForSelectedDate) { if (!this.timeSlotsForSelectedDate) {
return false; return false;
@ -609,6 +556,7 @@ export default {
selectedAnswerForDropOffOrInshop(newValue) { selectedAnswerForDropOffOrInshop(newValue) {
if (newValue == PICK_A_TIME_BUTTON_VALUE) { if (newValue == PICK_A_TIME_BUTTON_VALUE) {
this.selectedRouteCode = null; this.selectedRouteCode = null;
this.setSelectedTimeSlot();
} else { } else {
this.selectedAnswerForTimeSlots = null; this.selectedAnswerForTimeSlots = null;
this.selectedRouteCode = newValue; this.selectedRouteCode = newValue;
@ -631,7 +579,6 @@ export default {
<style lang="scss"> <style lang="scss">
.time-slots-question { .time-slots-question {
border-top: 1px solid $gray-500;
margin-top: 1.5rem; margin-top: 1.5rem;
.time-slot-header { .time-slot-header {