From 98889512d6a856536e22e249f45fed147f754e34 Mon Sep 17 00:00:00 2001 From: Scott Kiener Date: Wed, 24 May 2023 10:37:43 -0400 Subject: [PATCH 1/2] 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 --- .../cash-or-insurance-question.vue | 7 +- .../schedule/constants/schedule-constants.js | 11 ++ src/layouts/schedule/schedule.vue | 40 ++++--- .../time-slot-modal-question.vue | 103 ++++++++++-------- .../timeslot-modal-list-button.vue | 35 ++++-- 5 files changed, 126 insertions(+), 70 deletions(-) create mode 100644 src/layouts/schedule/constants/schedule-constants.js diff --git a/src/layouts/quote/cash-or-insurance-question/cash-or-insurance-question.vue b/src/layouts/quote/cash-or-insurance-question/cash-or-insurance-question.vue index 45fef683a..f5fec2255 100644 --- a/src/layouts/quote/cash-or-insurance-question/cash-or-insurance-question.vue +++ b/src/layouts/quote/cash-or-insurance-question/cash-or-insurance-question.vue @@ -5,7 +5,7 @@ :groupName="groupName" buttonTypeString="listButtonHorizontal" v-model="selectedValues" - :additionalButtonData="{ additionalButtonStyling: 'listButtonHorizontalStrong' }" + :additionalButtonData="additionalButtonData" isRequired /> @@ -27,6 +27,11 @@ export default { answersFromCms() { return this.getCmsContent(this.cmsWidgetName, "Answers"); }, + additionalButtonData() { + return { + additionalButtonStyling: 'listButtonHorizontalStrong' + }; + }, selectedValues: { get: function () { // Convert to CMS answer name from bool diff --git a/src/layouts/schedule/constants/schedule-constants.js b/src/layouts/schedule/constants/schedule-constants.js new file mode 100644 index 000000000..aae307675 --- /dev/null +++ b/src/layouts/schedule/constants/schedule-constants.js @@ -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 }; \ No newline at end of file diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index 60fb90b75..6c859c68b 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -39,13 +39,14 @@ @@ -49,15 +49,13 @@ import { defineRule, useField } from "vee-validate"; import { errorMessages } from "@/constants/error-messages"; 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 defineRule("time-slot-required", required(errorMessages.OPTION_REQUIRED)); // Constants -const AppointmentTypeStrings = { - IN_SHOP: "Inshop", - MOBILE: "Mobile", - DROP_OFF: "Dropoff", -}; export default { name: "timeSlotModalQuestion", @@ -65,18 +63,20 @@ export default { modelValue: Object, cmsWidgetName: String, mobileCmsWidgetName: String, - earlyBirdCmsWidgetName: String, + mobilePremiumCmsWidgetName: String, dropoffCmsWidgetName: String, + sameDayDropOffCmsWidgetName: String, appointmentType: String, dateAndTimeSlotData: Object, - mobileEarlyBirdFee: Object, + premiumAppointmentFee: Object, estimatedServiceMinutesMinimum: Number, estimatedServiceMinutesMaximum: Number, validationRules: String, }, data() { return { - selectedTimeSlot: null, + selectedTimeSlotId: null, + isSelectedAppointmentPremium: null, timeslotModalListButton: timeslotModalListButton, }; }, @@ -89,42 +89,36 @@ export default { }, watch: { modelValue() { - this.selectedTimeSlot = this.modelValue; // Run component validation that is used at parent level - this.handleChange(this.modelValue); + this.handleChange(this.modelValue.id); }, - // dateAndTimeSlotData(newValue, oldValue) { - // const numberOfOptions = newValue?.timeSlots.length; - // console.log('running'); - // if (numberOfOptions === 1) { - // this.selectedTimeSlot = newValue.timeSlots[0].id; - // } - // } + dateAndTimeSlotData(newValue, oldValue) { + const numberOfOptions = newValue?.timeSlots.length; + if (numberOfOptions === 1) { + this.selectedTimeSlotId = newValue.timeSlots[0].id; + } + } }, computed: { supplementalInformationBlock() { let appointmentTypeCmsWidgetName; - let cmsFieldName = "BodyText"; if (this.appointmentType === AppointmentTypeStrings.IN_SHOP) { return null; } else if (this.appointmentType === AppointmentTypeStrings.MOBILE) { appointmentTypeCmsWidgetName = - this.selectedTimeSlot === this.earlyBirdButtonText - ? this.earlyBirdCmsWidgetName + this.selectedTimeSlotId === this.premiumAppointmentButtonText + ? this.mobilePremiumCmsWidgetName : this.mobileCmsWidgetName; } else { - appointmentTypeCmsWidgetName = this.dropoffCmsWidgetName; - if (this.isSameDay) { - cmsFieldName = "BodyText2"; - } + appointmentTypeCmsWidgetName = this.isSameDay ? this.sameDayDropOffCmsWidgetName : this.dropoffCmsWidgetName; } - return this.getCmsContent(appointmentTypeCmsWidgetName, cmsFieldName); + return this.getCmsContent(appointmentTypeCmsWidgetName, "BodyText"); }, footerCloseButtonText() { return this.getCmsContent(this.cmsWidgetName, "FooterText"); }, - earlyBirdButtonText() { - return this.getCmsContent(this.earlyBirdCmsWidgetName, "HeaderText"); + premiumAppointmentButtonText() { + return this.getCmsContent(this.mobilePremiumCmsWidgetName, "HeaderText"); }, dropoffButtonText() { return this.getCmsContent(this.dropoffCmsWidgetName, "HeaderText"); @@ -136,14 +130,11 @@ export default { return this.getCmsContent(this.dropoffCmsWidgetName, "SubheaderText"); }, inshopDurationText() { - const inshopDurationTextWithoutTime = this.getCmsContent( - this.cmsWidgetName, - "SubheaderText" - ); + const inshopDurationTextWithoutTime = this.getCmsContent(this.cmsWidgetName, "SubheaderText"); const inshopDurationTime = this.getDisplayTextForDurationLength( - this.estimatedServiceMinutesMinimum, - this.estimatedServiceMinutesMaximum - ); + this.estimatedServiceMinutesMinimum, + this.estimatedServiceMinutesMaximum + ); return `${inshopDurationTextWithoutTime} ${inshopDurationTime}`; }, durationTextBlockCopy() { @@ -203,13 +194,18 @@ export default { } if (this.appointmentType === AppointmentTypeStrings.MOBILE) { - const offerPremium = this.dateAndTimeSlotData.timeSlots[0].offerPremium; - const hasEarlyBird = this.mobileEarlyBirdFee?.partType === "EARLY BIRD"; - if (offerPremium && hasEarlyBird) { + const isPremiumTimeSlot = this.dateAndTimeSlotData.timeSlots[0].offerPremium; + const hasPremiumPartAvailable = this.premiumAppointmentFee?.partType === PREMIUM_FEE_PART_TYPE; + if (isPremiumTimeSlot && hasPremiumPartAvailable) { + const formattedPrice = "+$" + this.getTotalLineItemPrice(this.premiumAppointmentFee).toFixed(2); availableTimeSlots.unshift({ - value: this.dateAndTimeSlotData.timeSlots[0].id + "-earlybird", - buttonLabel: this.earlyBirdButtonText, - buttonLabelSubCopy: this.getTotalLineItemPrice(this.mobileEarlyBirdFee), + // Unique value is required for each and the premium appoinment shares a timeslot ID + value: this.addPremiumFlagToInput(this.dateAndTimeSlotData.timeSlots[0].id), + 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" 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(); }, // fires any time the modal is closed, AFTER "closeModal" fires if footer button is used 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"); }, // Expected input: "HH:MM:SS" @@ -252,6 +263,12 @@ export default { } return displayTextForDurationLength; }, + addPremiumFlagToInput(timeSlotId) { + return timeSlotId += PREMIUM_TIME_SLOT_ID_FLAG; + }, + removePremiumFlagFromInput(timeSlotId) { + return parseInt(timeSlotId.trim(PREMIUM_TIME_SLOT_ID_FLAG.length)); + }, }, components: { modal, diff --git a/src/layouts/schedule/time-slot-modal-question/timeslot-modal-list-button/timeslot-modal-list-button.vue b/src/layouts/schedule/time-slot-modal-question/timeslot-modal-list-button/timeslot-modal-list-button.vue index 60b2f9e81..8c71a9bb3 100644 --- a/src/layouts/schedule/time-slot-modal-question/timeslot-modal-list-button/timeslot-modal-list-button.vue +++ b/src/layouts/schedule/time-slot-modal-question/timeslot-modal-list-button/timeslot-modal-list-button.vue @@ -6,12 +6,13 @@
- + {{ buttonLabel }} - - + {{ formattedButtonLabelSubCopy }} + + {{ screenReaderOnlyText }} @@ -28,7 +29,7 @@ import baseInputButton from "@/digital-components/base-input-button/base-input-b import inputButtonWrapperMixin from "@/mixins/input-button-wrapper-mixin"; export default { - name: "timeslotMOdalListButton", + name: "timeslotModalListButton", mixins: [inputButtonWrapperMixin], props: { loaderColor: String, @@ -44,7 +45,7 @@ export default { }, computed: { formattedButtonLabelSubCopy() { - return this.buttonLabelSubCopy?.toFixed(2); + return this.buttonLabelSubCopy; }, }, methods: { @@ -85,6 +86,9 @@ export default { font-weight: 500; background: $blue-100; box-shadow: 0 0 0 1px $blue; + span.premium-appointment-price { + background: $green-200; + } } &:checked:focus + .list-button-content { box-shadow: 0 0 0 2.5px $blue; @@ -109,11 +113,22 @@ export default { width: 100%; outline: none; - span { - &.small { - font-size: 0.75rem; - color: $gray-550; - } + span.premium-appointment-price { + position: absolute; + background: $green-100; + border-radius: 4.5rem; + line-height: 1.25rem; + color: $green-700; + font-size: 0.75rem; + margin-left:4px; + padding: 2px 8px; + font-weight: 500; } } + +.position-relative { + position: relative; +} + + From 28e6ff191d69afa247eebee1cb30273d1490e95a Mon Sep 17 00:00:00 2001 From: Scott Kiener Date: Thu, 25 May 2023 08:16:10 -0400 Subject: [PATCH 2/2] CSR-1137 | Formatting --- .../cash-or-insurance-question.vue | 4 +- .../schedule/constants/schedule-constants.js | 2 +- src/layouts/schedule/schedule.vue | 12 ++++-- .../time-slot-modal-question.vue | 37 ++++++++++++------- .../timeslot-modal-list-button.vue | 21 ++++++----- 5 files changed, 47 insertions(+), 29 deletions(-) diff --git a/src/layouts/quote/cash-or-insurance-question/cash-or-insurance-question.vue b/src/layouts/quote/cash-or-insurance-question/cash-or-insurance-question.vue index f5fec2255..a2afb1a7c 100644 --- a/src/layouts/quote/cash-or-insurance-question/cash-or-insurance-question.vue +++ b/src/layouts/quote/cash-or-insurance-question/cash-or-insurance-question.vue @@ -28,8 +28,8 @@ export default { return this.getCmsContent(this.cmsWidgetName, "Answers"); }, additionalButtonData() { - return { - additionalButtonStyling: 'listButtonHorizontalStrong' + return { + additionalButtonStyling: "listButtonHorizontalStrong", }; }, selectedValues: { diff --git a/src/layouts/schedule/constants/schedule-constants.js b/src/layouts/schedule/constants/schedule-constants.js index aae307675..89b4a4c1a 100644 --- a/src/layouts/schedule/constants/schedule-constants.js +++ b/src/layouts/schedule/constants/schedule-constants.js @@ -8,4 +8,4 @@ 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 }; \ No newline at end of file +export { AppointmentTypeStrings, PREMIUM_TIME_SLOT_ID_FLAG, PREMIUM_FEE_PART_TYPE }; diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index 91e685dbf..654f35a15 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -73,7 +73,7 @@ import { getRouterLinkRouteFromCopy, getRouterLinkDisplayTextFromCopy, } from "@/helpers/cms-content-helper"; -import {PREMIUM_FEE_PART_TYPE} from "./constants/schedule-constants"; +import { PREMIUM_FEE_PART_TYPE } from "./constants/schedule-constants"; import { errorMessages } from "@/constants/error-messages"; import { required } from "@/helpers/validation-rules"; import store from "@/store"; @@ -135,7 +135,13 @@ export default { const pricingPromise = baseMixin.methods.dispatchStoreAction( storeActions.PRICE_ORDER_ITEMS_AND_SAVE_SERVER_DATA, { - availableLineItems: [{ partNumber: PREMIUM_FEE_PART_TYPE, partType: PREMIUM_FEE_PART_TYPE, description: null }], + availableLineItems: [ + { + partNumber: PREMIUM_FEE_PART_TYPE, + partType: PREMIUM_FEE_PART_TYPE, + description: null, + }, + ], }, false ); @@ -286,7 +292,7 @@ export default { if (newValue !== oldValue) { this.selectedTimeSlotData = { id: null, - isPremiumAppointment: null + isPremiumAppointment: null, }; } }, diff --git a/src/layouts/schedule/time-slot-modal-question/time-slot-modal-question.vue b/src/layouts/schedule/time-slot-modal-question/time-slot-modal-question.vue index 54cef6182..e812e7a67 100644 --- a/src/layouts/schedule/time-slot-modal-question/time-slot-modal-question.vue +++ b/src/layouts/schedule/time-slot-modal-question/time-slot-modal-question.vue @@ -50,7 +50,11 @@ import { errorMessages } from "@/constants/error-messages"; import { required } from "@/helpers/validation-rules"; // Constants -import { AppointmentTypeStrings, PREMIUM_TIME_SLOT_ID_FLAG, PREMIUM_FEE_PART_TYPE} from "../constants/schedule-constants"; +import { + AppointmentTypeStrings, + PREMIUM_TIME_SLOT_ID_FLAG, + PREMIUM_FEE_PART_TYPE, +} from "../constants/schedule-constants"; // Validation for the modal button defineRule("time-slot-required", required(errorMessages.OPTION_REQUIRED)); @@ -97,7 +101,7 @@ export default { if (numberOfOptions === 1) { this.selectedTimeSlotId = newValue.timeSlots[0].id; } - } + }, }, computed: { supplementalInformationBlock() { @@ -110,7 +114,9 @@ export default { ? this.mobilePremiumCmsWidgetName : this.mobileCmsWidgetName; } else { - appointmentTypeCmsWidgetName = this.isSameDay ? this.sameDayDropOffCmsWidgetName : this.dropoffCmsWidgetName; + appointmentTypeCmsWidgetName = this.isSameDay + ? this.sameDayDropOffCmsWidgetName + : this.dropoffCmsWidgetName; } return this.getCmsContent(appointmentTypeCmsWidgetName, "BodyText"); }, @@ -130,11 +136,14 @@ export default { return this.getCmsContent(this.dropoffCmsWidgetName, "SubheaderText"); }, inshopDurationText() { - const inshopDurationTextWithoutTime = this.getCmsContent(this.cmsWidgetName, "SubheaderText"); + const inshopDurationTextWithoutTime = this.getCmsContent( + this.cmsWidgetName, + "SubheaderText" + ); const inshopDurationTime = this.getDisplayTextForDurationLength( - this.estimatedServiceMinutesMinimum, - this.estimatedServiceMinutesMaximum - ); + this.estimatedServiceMinutesMinimum, + this.estimatedServiceMinutesMaximum + ); return `${inshopDurationTextWithoutTime} ${inshopDurationTime}`; }, durationTextBlockCopy() { @@ -195,17 +204,19 @@ export default { if (this.appointmentType === AppointmentTypeStrings.MOBILE) { const isPremiumTimeSlot = this.dateAndTimeSlotData.timeSlots[0].offerPremium; - const hasPremiumPartAvailable = this.premiumAppointmentFee?.partType === PREMIUM_FEE_PART_TYPE; + const hasPremiumPartAvailable = + this.premiumAppointmentFee?.partType === PREMIUM_FEE_PART_TYPE; if (isPremiumTimeSlot && hasPremiumPartAvailable) { - const formattedPrice = "+$" + this.getTotalLineItemPrice(this.premiumAppointmentFee).toFixed(2); + const formattedPrice = + "+$" + this.getTotalLineItemPrice(this.premiumAppointmentFee).toFixed(2); availableTimeSlots.unshift({ // Unique value is required for each and the premium appoinment shares a timeslot ID value: this.addPremiumFlagToInput(this.dateAndTimeSlotData.timeSlots[0].id), buttonLabel: this.premiumAppointmentButtonText, buttonLabelSubCopy: formattedPrice, additionalButtonData: { - isPremiumAppointment: true - } + isPremiumAppointment: true, + }, }); } } @@ -227,7 +238,7 @@ export default { const selectedTimeSlotData = { id: this.selectedTimeSlotId, isPremiumAppointment: this.isSelectedAppointmentPremium, - } + }; this.$emit("update:modelValue", selectedTimeSlotData); this.$refs["timeSlots"].closeModal(); }, @@ -264,7 +275,7 @@ export default { return displayTextForDurationLength; }, addPremiumFlagToInput(timeSlotId) { - return timeSlotId += PREMIUM_TIME_SLOT_ID_FLAG; + return (timeSlotId += PREMIUM_TIME_SLOT_ID_FLAG); }, removePremiumFlagFromInput(timeSlotId) { return parseInt(timeSlotId.trim(PREMIUM_TIME_SLOT_ID_FLAG.length)); diff --git a/src/layouts/schedule/time-slot-modal-question/timeslot-modal-list-button/timeslot-modal-list-button.vue b/src/layouts/schedule/time-slot-modal-question/timeslot-modal-list-button/timeslot-modal-list-button.vue index 8c71a9bb3..4f5b17461 100644 --- a/src/layouts/schedule/time-slot-modal-question/timeslot-modal-list-button/timeslot-modal-list-button.vue +++ b/src/layouts/schedule/time-slot-modal-question/timeslot-modal-list-button/timeslot-modal-list-button.vue @@ -8,11 +8,14 @@ class="button-content list-button-content d-flex flex-column justify-content-center py-3 px-4"> {{ buttonLabel }} - - {{ formattedButtonLabelSubCopy }} + + {{ formattedButtonLabelSubCopy }} + - - + {{ screenReaderOnlyText }} @@ -113,14 +116,14 @@ export default { width: 100%; outline: none; - span.premium-appointment-price { + span.premium-appointment-price { position: absolute; - background: $green-100; - border-radius: 4.5rem; + background: $green-100; + border-radius: 4.5rem; line-height: 1.25rem; color: $green-700; font-size: 0.75rem; - margin-left:4px; + margin-left: 4px; padding: 2px 8px; font-weight: 500; } @@ -129,6 +132,4 @@ export default { .position-relative { position: relative; } - -