From 5b6e68b8c52ded15dbb392f0a2b8bae09e54d4fe Mon Sep 17 00:00:00 2001 From: Adam Caouette Date: Tue, 23 May 2023 11:10:31 -0400 Subject: [PATCH 01/18] CSR-886: remove demo pages / code --- .../demo-date-picker/demo-date-picker.spec.js | 19 ---- .../demo-date-picker/demo-date-picker.vue | 90 ------------------- src/router/index.js | 14 --- 3 files changed, 123 deletions(-) delete mode 100644 src/layouts/demo-date-picker/demo-date-picker.spec.js delete mode 100644 src/layouts/demo-date-picker/demo-date-picker.vue diff --git a/src/layouts/demo-date-picker/demo-date-picker.spec.js b/src/layouts/demo-date-picker/demo-date-picker.spec.js deleted file mode 100644 index 9f7fa378b..000000000 --- a/src/layouts/demo-date-picker/demo-date-picker.spec.js +++ /dev/null @@ -1,19 +0,0 @@ -import demoDatePicker from "./demo-date-picker"; - -// Supporting Files -import { shallowMount } from "@vue/test-utils"; -import { getMountOptions } from "@/helpers/unit-test-helper.js"; - -describe("demo-date-picker.vue", () => { - test.only("test TK...", () => {}); -}); - -function setupMocks({ mountOptionsMockData = {} }) { - const mountOptions = getMountOptions({ - ...mountOptionsMockData, - }); - - const wrapper = shallowMount(demoDatePicker, mountOptions); - - return { wrapper }; -} diff --git a/src/layouts/demo-date-picker/demo-date-picker.vue b/src/layouts/demo-date-picker/demo-date-picker.vue deleted file mode 100644 index 20416b1d4..000000000 --- a/src/layouts/demo-date-picker/demo-date-picker.vue +++ /dev/null @@ -1,90 +0,0 @@ - - - - - diff --git a/src/router/index.js b/src/router/index.js index 3abfa058d..15ba52647 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -28,21 +28,7 @@ import analyticsMixin from "@/mixins/analytics-mixin"; import { experimentTriggers } from "../constants/experiments"; import { applicationConfig } from "../constants/application-config"; -// Components -import datePicker from "@/digital-components/date-picker/date-picker.vue"; -import demoDatePicker from "@/layouts/demo-date-picker/demo-date-picker.vue"; - const routes = [ - { - path: "/demo-date-picker", // This is a temporary route for testing. - name: "demo-date-picker", - component: demoDatePicker, - }, - { - path: "/date-picker", // This is a temporary route for testing. - name: "date-picker", - component: datePicker, - }, { path: "/", name: "root", From 98889512d6a856536e22e249f45fed147f754e34 Mon Sep 17 00:00:00 2001 From: Scott Kiener Date: Wed, 24 May 2023 10:37:43 -0400 Subject: [PATCH 02/18] 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 389b11ac2d59ac0cc0089d4f961bba91491498c6 Mon Sep 17 00:00:00 2001 From: Bryan Mauger Date: Wed, 24 May 2023 11:00:00 -0400 Subject: [PATCH 03/18] CSR-1420 fix misalignment of disclaimer copy Update code for text-block to not use flex. Use text-align accordingly. --- src/digital-components/text-block/text-block.vue | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/digital-components/text-block/text-block.vue b/src/digital-components/text-block/text-block.vue index 4f9c41599..2deb126e5 100644 --- a/src/digital-components/text-block/text-block.vue +++ b/src/digital-components/text-block/text-block.vue @@ -10,7 +10,7 @@ export default { name: "textBlock", props: { customText: String, // used to allow the insert of token values into textblock - justifyText: String, // left, right, center + justifyText: String, // right, center (left is default) typeStyle: String, // h1-h6, body, small, label, caption (see Figma or Confluence documentation) fontWeight: String, // bold=500, default is 400 cmsWidgetName: String, @@ -28,15 +28,11 @@ export default { From 27133a6f560712fa7df10e53627f7a7e3fa1c7a0 Mon Sep 17 00:00:00 2001 From: Scott Kiener Date: Thu, 25 May 2023 10:23:55 -0400 Subject: [PATCH 08/18] CSR-1149 | Implement continue button changes Cleaned up timeslot-modal-list-button Fixed bug with auto-selecting appointments when only one is available --- src/layouts/schedule/schedule.vue | 45 ++++++++++++++++++- .../time-slot-modal-question.vue | 13 +++--- .../timeslot-modal-list-button.vue | 17 ------- 3 files changed, 52 insertions(+), 23 deletions(-) diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index 654f35a15..540e7a955 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 { AppointmentTypeStrings, PREMIUM_FEE_PART_TYPE } from "./constants/schedule-constants"; import { errorMessages } from "@/constants/error-messages"; import { required } from "@/helpers/validation-rules"; import store from "@/store"; @@ -267,6 +267,46 @@ export default { this.selectedDate = null; } }, + updateFooterButtonText(timeSlotData) { + let funnelFooterButtonText; + if (!timeSlotData.id) { + funnelFooterButtonText = "Continue"; + } else { + funnelFooterButtonText = "Select " + this.convertSelectedDateToShortMonthAndDay(this.selectedDate); + + if (this.appointmentType === AppointmentTypeStrings.IN_SHOP) { + funnelFooterButtonText += " at " + this.getDisplayTextForMilitaryTime(this.appointmentDateAndTime.startTime); + } else if (this.appointmentType === AppointmentTypeStrings.MOBILE && !timeSlotData.isPremiumAppointment) { + funnelFooterButtonText += + " at " + + this.getDisplayTextForMilitaryTime(this.appointmentDateAndTime.startTime, true) + + " - " + + this.getDisplayTextForMilitaryTime(this.appointmentDateAndTime.endTime, true); + } + } + + this.$refs.funnelFooter.updateButtonText(funnelFooterButtonText); + }, + convertSelectedDateToShortMonthAndDay(selectedDate) { + // This conversion ensures we don't get get GMT induced date changes + const dateObject = new Date(`${selectedDate.dateString}T00:00:00`); + return dateObject.toLocaleDateString("en-us", {month:"short", day:"numeric"}); + }, + // Expected input: "HH:MM:SS" + getDisplayTextForMilitaryTime(militaryTimeInput, shouldTrimMinutesIfEmpty = false) { + let hours = parseInt(militaryTimeInput.split(":")[0]); + const minutes = militaryTimeInput.split(":")[1]; + let meridianNotation = "AM"; + if (militaryTimeInput.split(":")[0] > 12) { + hours -= 12; + meridianNotation = "PM"; + } + if (shouldTrimMinutesIfEmpty && minutes === "00") { + return `${hours} ${meridianNotation}`; + } else { + return `${hours}:${minutes} ${meridianNotation}`; + } + }, backButtonAction() { this.$router.navigateWithoutSaving(this.navigationScenarios.CLICKED_BACK, this.$route); }, @@ -296,6 +336,9 @@ export default { }; } }, + selectedTimeSlotData() { + this.updateFooterButtonText(this.selectedTimeSlotData); + } }, components: { funnelHeader, 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 e812e7a67..f6f85ccae 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 @@ -97,10 +97,7 @@ export default { this.handleChange(this.modelValue.id); }, dateAndTimeSlotData(newValue, oldValue) { - const numberOfOptions = newValue?.timeSlots.length; - if (numberOfOptions === 1) { - this.selectedTimeSlotId = newValue.timeSlots[0].id; - } + this.autoSelectTimeSlotIfOnlyOneIsAvailable(newValue); }, }, computed: { @@ -181,7 +178,7 @@ export default { if (this.appointmentType === AppointmentTypeStrings.DROP_OFF) { availableTimeSlots = [ { - value: this.dateAndTimeSlotData.timeSlots[0], + value: this.dateAndTimeSlotData.timeSlots[0].id, buttonLabel: this.dropoffButtonText, }, ]; @@ -274,6 +271,12 @@ export default { } return displayTextForDurationLength; }, + autoSelectTimeSlotIfOnlyOneIsAvailable(newdateAndTimeSlotDataValue) { + const numberOfOptions = newdateAndTimeSlotDataValue?.timeSlots.length; + if (numberOfOptions === 1 && !(this.appointmentType === AppointmentTypeStrings.MOBILE && this.premiumAppointmentFee && newdateAndTimeSlotDataValue.timeSlots[0].offerPremium)) { + this.selectedTimeSlotId = newdateAndTimeSlotDataValue.timeSlots[0].id; + } + }, addPremiumFlagToInput(timeSlotId) { return (timeSlotId += PREMIUM_TIME_SLOT_ID_FLAG); }, 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 4f5b17461..8c0f3db0a 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 @@ -19,33 +19,17 @@ {{ screenReaderOnlyText }} -