Merge branch 'develop' into feature/CSR-1144
This commit is contained in:
commit
57c6abf134
14 changed files with 201 additions and 387 deletions
|
|
@ -22,7 +22,6 @@
|
|||
<div class="separator-line"></div>
|
||||
<div class="nav-back ps-3"><button></button></div>
|
||||
<div class="nav-forward pe-3"><button></button></div>
|
||||
<!-- TODO Accessibility: Do the days of the week need to be read? -->
|
||||
<div class="grid-item caption"><span class="sr-only">Sunday</span>S</div>
|
||||
<div class="grid-item caption"><span class="sr-only">Monday</span>M</div>
|
||||
<div class="grid-item caption"><span class="sr-only">Tuesday</span>T</div>
|
||||
|
|
@ -84,7 +83,6 @@ export default {
|
|||
months: null,
|
||||
disableViewMoreDatesButton: false,
|
||||
selectableDatesData: [], // NOTE: uses monthNum (1-based), NOT monthIndex (0-based)
|
||||
today: null,
|
||||
hideSomeDaysForInitialView: null,
|
||||
};
|
||||
},
|
||||
|
|
@ -112,6 +110,12 @@ export default {
|
|||
},
|
||||
},
|
||||
computed: {
|
||||
today() {
|
||||
if (this.todayOverrideDateString) {
|
||||
return new Date(this.todayOverrideDateString);
|
||||
}
|
||||
return new Date();
|
||||
},
|
||||
todayMonthIndex() {
|
||||
return this.today.getMonth() + 1;
|
||||
},
|
||||
|
|
@ -151,39 +155,31 @@ export default {
|
|||
this.$emit("date-clicked");
|
||||
},
|
||||
getWeekStartDate(date) {
|
||||
// Get the day of the week for date
|
||||
let dayOfWeek = date.getDay();
|
||||
|
||||
const dayOfWeek = date.getDay();
|
||||
// Subtract the day of the week from date to get the date of Sunday
|
||||
let sunday = new Date(date);
|
||||
const sunday = new Date(date);
|
||||
sunday.setDate(sunday.getDate() - dayOfWeek);
|
||||
|
||||
// Return the date of Sunday
|
||||
return sunday;
|
||||
},
|
||||
getWeekEndDate(date) {
|
||||
const currentDay = date.getDay(); // Get the day of the week (0 = Sunday, 1 = Monday, etc.)
|
||||
const daysUntilSaturday = 6 - currentDay; // Calculate the number of days until Saturday
|
||||
|
||||
const dayOfWeek = date.getDay();
|
||||
const daysUntilSaturday = 6 - dayOfWeek; // Calculate the number of days until Saturday
|
||||
// Clone the given date and add the remaining days until Saturday
|
||||
const saturday = new Date(date);
|
||||
saturday.setDate(date.getDate() + daysUntilSaturday);
|
||||
|
||||
return saturday;
|
||||
},
|
||||
getNextWeekSunday(date) {
|
||||
const currentDay = date.getDay(); // Get the day of the week (0 = Sunday, 1 = Monday, etc.)
|
||||
const daysUntilNextSunday = currentDay === 0 ? 7 : 7 - currentDay; // Calculate the number of days until the next Sunday
|
||||
|
||||
const dayOfWeek = date.getDay();
|
||||
const daysUntilNextSunday = 7 - dayOfWeek; // Calculate the number of days until the next Sunday
|
||||
// Clone the given date and add the remaining days until Sunday
|
||||
const nextSunday = new Date(date);
|
||||
nextSunday.setDate(date.getDate() + daysUntilNextSunday);
|
||||
|
||||
return nextSunday;
|
||||
},
|
||||
getInitialViewWeeks(today, initialViewRowsToShow) {
|
||||
// TODO: this only is for future direction; create logic for past direction
|
||||
let weeks = [];
|
||||
// TODO: this only is for future direction; need to create logic for past direction
|
||||
const weeks = [];
|
||||
let weekStartDate = this.getWeekStartDate(today);
|
||||
let weekEndDate = this.getWeekEndDate(today);
|
||||
for (let i = 0; i < initialViewRowsToShow; i++) {
|
||||
|
|
@ -200,14 +196,19 @@ export default {
|
|||
return weeks;
|
||||
},
|
||||
async loadInitialData(config) {
|
||||
// CALLED FROM CONSUMING COMPONENT BEFORE DATE-PICKER APPEARS
|
||||
const todayDate = config.todayOverrideDateString
|
||||
? new Date(config.todayOverrideDateString)
|
||||
: new Date();
|
||||
let todayDate;
|
||||
if (this.today) {
|
||||
todayDate = this.today;
|
||||
} else if (config.todayOverrideDateString) {
|
||||
todayDate = new Date(config.todayOverrideDateString);
|
||||
} else {
|
||||
todayDate = new Date();
|
||||
}
|
||||
|
||||
let todayMonthIndex = todayDate.getMonth() + 1;
|
||||
let todayYearNum = todayDate.getFullYear();
|
||||
let currentMonthStart = new Date(todayYearNum, todayMonthIndex - 1, 1);
|
||||
const todayMonthIndex = todayDate.getMonth() + 1;
|
||||
const todayYearNum = todayDate.getFullYear();
|
||||
// TODO - set up currentMonthStart if direction is PAST:
|
||||
// let currentMonthStart = new Date(todayYearNum, todayMonthIndex - 1, 1);
|
||||
let currentMonthEnd = new Date(todayYearNum, todayMonthIndex, 0);
|
||||
|
||||
let calendarViewDirection = "none";
|
||||
|
|
@ -219,10 +220,10 @@ export default {
|
|||
config.initialViewRowsToShow
|
||||
);
|
||||
|
||||
let initialViewStartDate = todayDate;
|
||||
let initialViewEndDate = initialViewWeeks[initialViewWeeks.length - 1].weekEndDate;
|
||||
let saturday1month = initialViewWeeks[0].weekEndDate.getMonth();
|
||||
let sunday5month =
|
||||
const initialViewStartDate = todayDate;
|
||||
const initialViewEndDate = initialViewWeeks[initialViewWeeks.length - 1].weekEndDate;
|
||||
const saturday1month = initialViewWeeks[0].weekEndDate.getMonth();
|
||||
const sunday5month =
|
||||
initialViewWeeks[initialViewWeeks.length - 1].weekStartDate.getMonth();
|
||||
|
||||
let hideSomeDaysForInitialView = false;
|
||||
|
|
@ -284,9 +285,9 @@ export default {
|
|||
// Growing from 0 to 1
|
||||
time = Math.min(1, (timestamp - start) / duration);
|
||||
|
||||
let percentageNew = timingFunc(time);
|
||||
let distanceToGo = targetY;
|
||||
let thisDistance = percentageNew * distanceToGo;
|
||||
const percentageNew = timingFunc(time);
|
||||
const distanceToGo = targetY;
|
||||
const thisDistance = percentageNew * distanceToGo;
|
||||
|
||||
wrapper.scrollTo(0, initY + thisDistance);
|
||||
|
||||
|
|
@ -305,13 +306,12 @@ export default {
|
|||
},
|
||||
|
||||
async setCalendarData(config = {}) {
|
||||
this.today = config.todayDate;
|
||||
this.hideSomeDaysForInitialView = config.hideSomeDaysForInitialView;
|
||||
let hideSecondMonth = config.hideSecondMonth;
|
||||
const hideSecondMonth = config.hideSecondMonth;
|
||||
const direction = config.calendarViewDirection;
|
||||
|
||||
const monthsAfterToLoadOffset = 12; // TO BE MADE "CONSTANTS"
|
||||
const monthsBeforeToLoadOffset = 36; // TO BE MADE "CONSTANTS"
|
||||
const monthsAfterToLoadOffset = 12;
|
||||
const monthsBeforeToLoadOffset = 36;
|
||||
config.initialShopTimeSlotsResponse.days.forEach((selectableDate) => {
|
||||
this.selectableDatesData.push(selectableDate);
|
||||
});
|
||||
|
|
@ -382,25 +382,25 @@ export default {
|
|||
}
|
||||
}
|
||||
|
||||
const monthEndDate = new Date(yearNum, monthIndex, 0); // BOTH
|
||||
let monthEndDateNum = monthEndDate.getDate(); // BOTH
|
||||
const monthEndDate = new Date(yearNum, monthIndex, 0);
|
||||
let monthEndDateNum = monthEndDate.getDate();
|
||||
|
||||
if (
|
||||
offset === 0 &&
|
||||
calendarViewDirection === "past" &&
|
||||
monthEndDateNum > this.currentWeekEndDateNum
|
||||
) {
|
||||
monthEndDateNum = this.currentWeekEndDateNum; // PAST
|
||||
monthEndDateNum = this.currentWeekEndDateNum;
|
||||
}
|
||||
|
||||
const monthStartDateNum =
|
||||
offset === 0 && calendarViewDirection === "future"
|
||||
? this.currentWeekStartDateNum
|
||||
: 1; // FUTURE
|
||||
const monthStartDate = new Date(yearNum, monthIndex - 1, monthStartDateNum); // BOTH
|
||||
: 1;
|
||||
const monthStartDate = new Date(yearNum, monthIndex - 1, monthStartDateNum);
|
||||
|
||||
const startDateDayIndex = monthStartDate.getDay(); // FUTURE
|
||||
const endDateDayIndex = monthEndDate.getDay(); // PAST
|
||||
const startDateDayIndex = monthStartDate.getDay();
|
||||
const endDateDayIndex = monthEndDate.getDay();
|
||||
|
||||
if (Math.abs(offset) === 1 && hideSecondMonth) {
|
||||
monthClass = monthClass + " month-hidden";
|
||||
|
|
@ -424,7 +424,7 @@ export default {
|
|||
// populate dates array
|
||||
for (let i = monthStartDateNum; i <= monthEndDateNum; i++) {
|
||||
let dayClasses = "";
|
||||
let dateString =
|
||||
const dateString =
|
||||
yearNum.toString() +
|
||||
"-" +
|
||||
forceTwoDigitString(monthIndex) +
|
||||
|
|
@ -514,7 +514,7 @@ export default {
|
|||
monthToShow.dates[monthToShow.dates.length - 1].inputValue.dateString
|
||||
);
|
||||
this.isLoading = false;
|
||||
this.hideSomeDaysForInitialView = false; // if hid days on initial partial view, this removes hidden styling on days
|
||||
this.hideSomeDaysForInitialView = false; // if hid days on initial partial view, this will reveal those days
|
||||
monthToShow.monthClass = monthToShow.monthClass.replace(" month-hidden", "");
|
||||
this.scrollToElement(monthToShow.monthString);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div
|
||||
class="text-block w-100"
|
||||
:class="[justifyText, typeStyle, fontWeight, margin]"
|
||||
class="text-block w-100 mt-2"
|
||||
:class="[justifyText, typeStyle, fontWeight]"
|
||||
v-html="this.TextBlockCopy"></div>
|
||||
</template>
|
||||
|
||||
|
|
@ -10,12 +10,7 @@ export default {
|
|||
name: "textBlock",
|
||||
props: {
|
||||
customText: String, // used to allow the insert of token values into textblock
|
||||
justifyText: String, // left, right, center
|
||||
margin: {
|
||||
// bootstrap margin to apply to the block.
|
||||
type: String,
|
||||
default: "mt-2",
|
||||
},
|
||||
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,
|
||||
|
|
@ -33,15 +28,11 @@ export default {
|
|||
|
||||
<style lang="scss" scoped>
|
||||
.text-block {
|
||||
display: flex;
|
||||
&.left {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
&.right {
|
||||
justify-content: flex-end;
|
||||
text-align: right;
|
||||
}
|
||||
&.center {
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
&.bold {
|
||||
font-weight: 500;
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
}
|
||||
|
|
@ -1,90 +0,0 @@
|
|||
<template>
|
||||
<div class="page-container-grouped-styles">
|
||||
<funnelHeader cmsWidgetName="FunnelHeaderWidget" />
|
||||
<div class="">
|
||||
<div class="rounded text-center">
|
||||
<div class="fade-on-route-transition">
|
||||
<date-picker
|
||||
selectableDates="custom"
|
||||
v-model="selectedDate"
|
||||
:customSelectableDatesCallback="getAvailableDates" />
|
||||
<!-- EXAMPLE THAT OVERRIDES TODAY BY PASSING STRING --
|
||||
<date-picker
|
||||
selectableDates="custom"
|
||||
v-model="selectedDate"
|
||||
:customSelectableDatesCallback="getAvailableDates"
|
||||
todayOverrideDateString="2022-12-30T03:00:00" /> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// Components
|
||||
import datePicker from "@/digital-components/date-picker/date-picker";
|
||||
import funnelHeader from "@/fmg-components/funnel-header/funnel-header";
|
||||
import vehicleBanner from "@/fmg-components/vehicle-banner/vehicle-banner";
|
||||
import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header";
|
||||
|
||||
// Supporting files
|
||||
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
|
||||
import { settleAllPromises } from "@/helpers/layout-helper";
|
||||
import { storeActions } from "@/constants/store-actions";
|
||||
import store from "@/store";
|
||||
|
||||
import baseMixin from "@/mixins/base-mixin";
|
||||
|
||||
export default {
|
||||
name: "demo-date-picker",
|
||||
data() {
|
||||
return {
|
||||
selectedDate: null,
|
||||
// selectedDate: { // USE THIS FORMAT FOR A PRE-SELECTED DATE ON LOAD
|
||||
// year: 2023,
|
||||
// month: 3, // use 1-based index for months
|
||||
// date: 21,
|
||||
// },
|
||||
mockSelectableDatesData: [
|
||||
{ year: 2023, month: 3, date: 4 },
|
||||
{ year: 2023, month: 3, date: 5 },
|
||||
{ year: 2023, month: 3, date: 22 },
|
||||
{ year: 2023, month: 4, date: 13 },
|
||||
{ year: 2023, month: 4, date: 7 },
|
||||
{ year: 2023, month: 4, date: 14 },
|
||||
{ year: 2023, month: 4, date: 26 },
|
||||
{ year: 2023, month: 5, date: 21 },
|
||||
{ year: 2023, month: 5, date: 23 },
|
||||
{ year: 2023, month: 5, date: 25 },
|
||||
],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
todayDate() {
|
||||
const today = new Date();
|
||||
return today.toDateString();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
arePagePrerequisitesValid() {
|
||||
return true;
|
||||
},
|
||||
getAvailableDates(startDate, endDate) {
|
||||
this.mockSelectableDatesData.push(endDate);
|
||||
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
// temporary test method that adds endDate to list of selectable dates
|
||||
return this.mockSelectableDatesData;
|
||||
},
|
||||
},
|
||||
components: {
|
||||
datePicker,
|
||||
funnelHeader,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
// .page-container-grouped-styles {
|
||||
// padding: 0 .5rem !important;
|
||||
// }
|
||||
</style>
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
:groupName="groupName"
|
||||
buttonTypeString="listButtonHorizontal"
|
||||
v-model="selectedValues"
|
||||
:additionalButtonData="{ additionalButtonStyling: 'listButtonHorizontalStrong' }"
|
||||
:additionalButtonData="additionalButtonData"
|
||||
isRequired />
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
describe("Review Page", () => {
|
||||
test.todo("Add more tests as specific functionality is added.");
|
||||
});
|
||||
|
|
@ -1,102 +0,0 @@
|
|||
<template>
|
||||
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm">
|
||||
<!-- When customer-details is added: v-slot="{ meta }" -->
|
||||
<div class="page-container-grouped-styles">
|
||||
<funnelHeader cmsWidgetName="FunnelHeaderWidget" />
|
||||
<vehicleBanner
|
||||
cmsWidgetName="VehicleBannerWidget"
|
||||
:displayGenericVehicleImage="false" />
|
||||
|
||||
<textBlock
|
||||
:customText="subHeaderTitle"
|
||||
typeStyle="h5"
|
||||
justifyText="text-center"
|
||||
margin="mt-1"
|
||||
class="dark-header" />
|
||||
<textBlock
|
||||
:customText="subHeaderBody"
|
||||
typeStyle="body"
|
||||
justifyText="left"
|
||||
margin="mt-0 mb-2" />
|
||||
|
||||
<buttonMain
|
||||
ref="buttonMain"
|
||||
isPrimary
|
||||
:buttonText="forwardButtonText"
|
||||
loaderColor="white"
|
||||
@click-event="forwardButtonAction" />
|
||||
|
||||
<funnelFooter
|
||||
cmsWidgetName="FunnelFooterWidget"
|
||||
@back-clicked="backButtonAction"
|
||||
@ForwardClicked="forwardButtonAction" />
|
||||
</div>
|
||||
</Form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import funnelHeader from "@/fmg-components/funnel-header/funnel-header";
|
||||
import funnelFooter from "@/fmg-components/funnel-footer/funnel-footer";
|
||||
import vehicleBanner from "@/fmg-components/vehicle-banner/vehicle-banner";
|
||||
import buttonMain from "@/ux-components/button-main/button-main";
|
||||
import textBlock from "@/digital-components/text-block/text-block";
|
||||
|
||||
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
|
||||
import { settleAllPromises } from "@/helpers/layout-helper";
|
||||
|
||||
export default {
|
||||
name: "review",
|
||||
async beforeRouteEnter(to, from, next) {
|
||||
// Call APIs
|
||||
const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage);
|
||||
|
||||
// Settle promises and get results
|
||||
const promiseResultMap = [
|
||||
{
|
||||
resultKey: "cmsContent",
|
||||
promise: cmsContentPromise,
|
||||
},
|
||||
];
|
||||
|
||||
const resultMap = await settleAllPromises(promiseResultMap);
|
||||
|
||||
next((vm) => {
|
||||
vm.setCmsContent(resultMap.cmsContent);
|
||||
});
|
||||
},
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
methods: {
|
||||
arePagePrerequisitesValid() {
|
||||
return true;
|
||||
},
|
||||
backButtonAction() {},
|
||||
forwardButtonAction() {},
|
||||
},
|
||||
computed: {
|
||||
subHeaderTitle() {
|
||||
return this.getCmsContent("FunnelSubHeaderWidget", "HeaderText");
|
||||
},
|
||||
subHeaderBody() {
|
||||
return this.getCmsContent("FunnelSubHeaderWidget", "BodyText");
|
||||
},
|
||||
forwardButtonText() {
|
||||
return this.getCmsContent("FunnelFooterWidget", "ForwardButtonText");
|
||||
},
|
||||
},
|
||||
components: {
|
||||
funnelHeader,
|
||||
funnelFooter,
|
||||
vehicleBanner,
|
||||
buttonMain,
|
||||
textBlock,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dark-header {
|
||||
color: $black;
|
||||
}
|
||||
</style>
|
||||
11
src/layouts/schedule/constants/schedule-constants.js
Normal file
11
src/layouts/schedule/constants/schedule-constants.js
Normal 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 };
|
||||
|
|
@ -25,17 +25,17 @@
|
|||
v-model="selectedDate"
|
||||
:customSelectableDatesCallback="getAvailableDatesMethod"
|
||||
@date-clicked="openInshopTimeSlotsModal" />
|
||||
<!-- todayOverrideDateString="2023-08-06T03:00:00" -->
|
||||
<time-slot-modal-question
|
||||
ref="timeSlotModalQuestion"
|
||||
cmsWidgetName="TimeSlotModalQuestion"
|
||||
earlyBirdCmsWidgetName="EarlyBirdTimeSlotModal"
|
||||
mobilePremiumCmsWidgetName="EarlyBirdTimeSlotModal"
|
||||
mobileCmsWidgetName="MobileTimeSlotModal"
|
||||
dropoffCmsWidgetName="DropOffTimeSlotModal"
|
||||
v-model="selectedTimeSlotId"
|
||||
sameDayDropOffCmsWidgetName="SameDayDropOffTimeSlotModal"
|
||||
v-model="selectedTimeSlotData"
|
||||
@time-slot-modal-closed="timeSlotModalClosed"
|
||||
:appointmentType="appointmentType"
|
||||
:mobileEarlyBirdFee="mobileEarlyBirdFee"
|
||||
:premiumAppointmentFee="mobilePremiumAppointmentFee"
|
||||
:dateAndTimeSlotData="timeSlotsForSelectedDate"
|
||||
:estimatedServiceMinutesMinimum="selectableDatesData.estimatedServiceMinutesMinimum"
|
||||
:estimatedServiceMinutesMaximum="selectableDatesData.estimatedServiceMinutesMaximum"
|
||||
|
|
@ -73,6 +73,7 @@ import {
|
|||
getRouterLinkRouteFromCopy,
|
||||
getRouterLinkDisplayTextFromCopy,
|
||||
} from "@/helpers/cms-content-helper";
|
||||
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";
|
||||
|
|
@ -112,9 +113,12 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
selectedDate: null,
|
||||
selectedTimeSlotId: null,
|
||||
selectedTimeSlotData: {
|
||||
id: null,
|
||||
isPremiumAppointment: null,
|
||||
},
|
||||
selectableDatesData: [],
|
||||
mobileEarlyBirdFee: null,
|
||||
mobilePremiumAppointmentFee: null,
|
||||
};
|
||||
},
|
||||
async beforeRouteEnter(to, from, next) {
|
||||
|
|
@ -125,34 +129,24 @@ export default {
|
|||
selectableDatesSetting: "custom",
|
||||
initialViewRowsToShow: 5,
|
||||
customSelectableDatesCallback: getAvailableDates,
|
||||
|
||||
/* vvvvv SAVE THESE FOR TESTING PURPOSES FOR NOW vvvvv
|
||||
|
||||
// todayOverrideDateString: "2023-04-29T03:00:00", // show partial
|
||||
// todayOverrideDateString: "2023-04-30T03:00:00", //
|
||||
// todayOverrideDateString: "2023-05-02T03:00:00", // ONE MONTH ONLY
|
||||
// todayOverrideDateString: "2023-05-06T03:00:00", // ONE MONTH ONLY
|
||||
// todayOverrideDateString: "2023-05-07T03:00:00", // show partial
|
||||
// todayOverrideDateString: "2023-05-30T03:00:00", //
|
||||
// todayOverrideDateString: "2023-06-30T03:00:00", //
|
||||
// todayOverrideDateString: "2023-07-01T03:00:00", // show partial && ONE MONTH ONLY
|
||||
// todayOverrideDateString: "2023-07-02T03:00:00", // ONE MONTH ONLY
|
||||
// todayOverrideDateString: "2023-07-12T03:00:00", // show partial
|
||||
// todayOverrideDateString: "2023-08-31T03:00:00",
|
||||
// todayOverrideDateString: "2023-09-30T03:00:00", // show partial
|
||||
*/
|
||||
});
|
||||
|
||||
// Price EARLY BIRD pre-emptively to allow for asynchronous call to pricing
|
||||
const pricingPromise = baseMixin.methods.dispatchStoreAction(
|
||||
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
|
||||
);
|
||||
|
||||
const earlyBirdPromise = baseMixin.methods.dispatchStoreAction(
|
||||
const premiumFeePromise = baseMixin.methods.dispatchStoreAction(
|
||||
storeActions.GET_MOBILE_EARLY_BIRD_FEE
|
||||
);
|
||||
const alertReasonsPromise = locationAlerts.methods.loadInitialData(
|
||||
|
|
@ -174,8 +168,8 @@ export default {
|
|||
promise: datePickerInitialDataPromise,
|
||||
},
|
||||
{
|
||||
resultKey: "earlyBird",
|
||||
promise: earlyBirdPromise,
|
||||
resultKey: "premiumFee",
|
||||
promise: premiumFeePromise,
|
||||
},
|
||||
{
|
||||
resultKey: "pricingResults",
|
||||
|
|
@ -191,8 +185,8 @@ export default {
|
|||
vm.$refs.datePicker.initializeComponent(resultMap.datePickerInitialData);
|
||||
vm.$refs.locationAlerts.initializeComponent(resultMap.alertReasons);
|
||||
vm.selectableDatesData = resultMap.datePickerInitialData.initialShopTimeSlotsResponse;
|
||||
if (resultMap.earlyBird) {
|
||||
vm.mobileEarlyBirdFee = resultMap.pricingResults[0];
|
||||
if (resultMap.premiumFee) {
|
||||
vm.mobilePremiumAppointmentFee = resultMap.pricingResults[0];
|
||||
}
|
||||
});
|
||||
},
|
||||
|
|
@ -216,18 +210,18 @@ export default {
|
|||
);
|
||||
},
|
||||
appointmentDateAndTime() {
|
||||
if (!this.selectedTimeSlotId) {
|
||||
if (!this.selectedTimeSlotData.id) {
|
||||
return null;
|
||||
}
|
||||
const timeSlotSelectedObject = this.getTimeSlotObjectFromTimeSlotId(
|
||||
this.selectedTimeSlotId
|
||||
this.selectedTimeSlotData.id
|
||||
);
|
||||
if (timeSlotSelectedObject) {
|
||||
return {
|
||||
date: this.selectedDate.dateString,
|
||||
startTime: timeSlotSelectedObject.startTime,
|
||||
endTime: timeSlotSelectedObject.endTime,
|
||||
id: this.selectedTimeSlotId,
|
||||
id: this.selectedTimeSlotData.id,
|
||||
};
|
||||
} else {
|
||||
return null;
|
||||
|
|
@ -269,7 +263,7 @@ export default {
|
|||
},
|
||||
timeSlotModalClosed() {
|
||||
// Clear the selectedDate if no timeslot has been selected
|
||||
if (!this.selectedTimeSlotId) {
|
||||
if (!this.selectedTimeSlotData.id) {
|
||||
this.selectedDate = null;
|
||||
}
|
||||
},
|
||||
|
|
@ -296,7 +290,10 @@ export default {
|
|||
selectedDate(newValue, oldValue) {
|
||||
// Clear time slot selection if date selected changes
|
||||
if (newValue !== oldValue) {
|
||||
this.selectedTimeSlotId = null;
|
||||
this.selectedTimeSlotData = {
|
||||
id: null,
|
||||
isPremiumAppointment: null,
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
:answers="availableTimeSlots"
|
||||
groupName="ChooseTimeSlot"
|
||||
textPosition="text-center"
|
||||
v-model="selectedTimeSlot"
|
||||
v-model="selectedTimeSlotId"
|
||||
isRequired
|
||||
validationRules="time-slot-required"
|
||||
class="mt-5" />
|
||||
|
|
@ -49,15 +49,17 @@ 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 +67,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 +93,38 @@ 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;
|
||||
if (numberOfOptions === 1) {
|
||||
this.selectedTimeSlotId = newValue.timeSlots[0].id;
|
||||
}
|
||||
},
|
||||
// dateAndTimeSlotData(newValue, oldValue) {
|
||||
// const numberOfOptions = newValue?.timeSlots.length;
|
||||
// console.log('running');
|
||||
// if (numberOfOptions === 1) {
|
||||
// this.selectedTimeSlot = 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");
|
||||
|
|
@ -203,13 +203,20 @@ 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 <input> 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 +229,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 +274,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,
|
||||
|
|
|
|||
|
|
@ -6,12 +6,16 @@
|
|||
<div
|
||||
:aria-label="buttonLabel"
|
||||
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 }}
|
||||
<span
|
||||
v-if="buttonLabelSubCopy"
|
||||
class="premium-appointment-price"
|
||||
:class="textPosition">
|
||||
{{ formattedButtonLabelSubCopy }}
|
||||
</span>
|
||||
</span>
|
||||
<span v-if="buttonLabelSubCopy" class="m-0 small" :class="textPosition">
|
||||
{{ formattedButtonLabelSubCopy }}
|
||||
</span>
|
||||
|
||||
<span v-if="screenReaderOnlyText" class="sr-only">
|
||||
{{ screenReaderOnlyText }}
|
||||
</span>
|
||||
|
|
@ -28,7 +32,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 +48,7 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
formattedButtonLabelSubCopy() {
|
||||
return this.buttonLabelSubCopy?.toFixed(2);
|
||||
return this.buttonLabelSubCopy;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
|
@ -85,6 +89,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 +116,20 @@ 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;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -179,19 +179,19 @@ describe("shop-question.vue", () => {
|
|||
expect(wrapper.vm.answers).toEqual([
|
||||
{
|
||||
buttonBodyCopy: "4403 Executive Pkwy, Westerville, OH 43081",
|
||||
buttonLabel: "4403 Executive Pkwy",
|
||||
buttonLabel: "Westerville",
|
||||
buttonLabelSubCopy: "5 mi",
|
||||
value: "003335",
|
||||
},
|
||||
{
|
||||
buttonBodyCopy: "760 Dearborn Park Ln, Worthington, OH 43085",
|
||||
buttonLabel: "760 Dearborn Park Ln",
|
||||
buttonLabel: "Worthington",
|
||||
buttonLabelSubCopy: "10.5 mi",
|
||||
value: "001820",
|
||||
},
|
||||
{
|
||||
buttonBodyCopy: "5015 N High St, Columbus, OH 43214",
|
||||
buttonLabel: "5015 N High St",
|
||||
buttonLabel: "Columbus",
|
||||
buttonLabelSubCopy: "11.5 mi",
|
||||
value: "003343",
|
||||
},
|
||||
|
|
@ -320,19 +320,19 @@ describe("shop-question.vue", () => {
|
|||
const displayedAnswers = [
|
||||
{
|
||||
buttonBodyCopy: "4403 Executive Pkwy, Westerville, OH 43081",
|
||||
buttonLabel: "4403 Executive Pkwy",
|
||||
buttonLabel: "Westerville",
|
||||
buttonLabelSubCopy: "5 mi",
|
||||
value: "003335",
|
||||
},
|
||||
{
|
||||
buttonBodyCopy: "760 Dearborn Park Ln, Worthington, OH 43085",
|
||||
buttonLabel: "760 Dearborn Park Ln",
|
||||
buttonLabel: "Worthington",
|
||||
buttonLabelSubCopy: "10.5 mi",
|
||||
value: "001820",
|
||||
},
|
||||
{
|
||||
buttonBodyCopy: "5015 N High St, Columbus, OH 43214",
|
||||
buttonLabel: "5015 N High St",
|
||||
buttonLabel: "Columbus",
|
||||
buttonLabelSubCopy: "11.5 mi",
|
||||
value: "003343",
|
||||
},
|
||||
|
|
@ -388,37 +388,37 @@ describe("shop-question.vue", () => {
|
|||
expect(wrapper.vm.answers).toEqual([
|
||||
{
|
||||
buttonBodyCopy: "4403 Executive Pkwy, Westerville, OH 43081",
|
||||
buttonLabel: "4403 Executive Pkwy",
|
||||
buttonLabel: "Westerville",
|
||||
buttonLabelSubCopy: "5 mi",
|
||||
value: "003335",
|
||||
},
|
||||
{
|
||||
buttonBodyCopy: "760 Dearborn Park Ln, Worthington, OH 43085",
|
||||
buttonLabel: "760 Dearborn Park Ln",
|
||||
buttonLabel: "Worthington",
|
||||
buttonLabelSubCopy: "10.5 mi",
|
||||
value: "001820",
|
||||
},
|
||||
{
|
||||
buttonBodyCopy: "5015 N High St, Columbus, OH 43214",
|
||||
buttonLabel: "5015 N High St",
|
||||
buttonLabel: "Columbus",
|
||||
buttonLabelSubCopy: "11.5 mi",
|
||||
value: "003343",
|
||||
},
|
||||
{
|
||||
buttonBodyCopy: "1670 Harmon Ave, Columbus, OH 43223",
|
||||
buttonLabel: "1670 Harmon Ave",
|
||||
buttonLabel: "Columbus",
|
||||
buttonLabelSubCopy: "16 mi",
|
||||
value: "006747",
|
||||
},
|
||||
{
|
||||
buttonBodyCopy: "3938 Powell Rd, Powell, OH 43065",
|
||||
buttonLabel: "3938 Powell Rd",
|
||||
buttonLabel: "Powell",
|
||||
buttonLabelSubCopy: "16.5 mi",
|
||||
value: "003341",
|
||||
},
|
||||
{
|
||||
buttonBodyCopy: "4580 W Broad St, Columbus, OH 43228",
|
||||
buttonLabel: "4580 W Broad St",
|
||||
buttonLabel: "Columbus",
|
||||
buttonLabelSubCopy: "19.5 mi",
|
||||
value: "003342",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@ export default {
|
|||
const distanceInMiles = Math.round(shopProvider.distanceInMiles * 2) / 2;
|
||||
|
||||
return {
|
||||
buttonLabel: streetAddress,
|
||||
buttonLabel: city,
|
||||
buttonLabelSubCopy: `${distanceInMiles} mi`,
|
||||
buttonBodyCopy: `${streetAddress}, ${city}, ${state} ${zipCode}`,
|
||||
value: shopProvider.providerNumber,
|
||||
|
|
|
|||
|
|
@ -28,27 +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";
|
||||
import review from "@/layouts/review/review";
|
||||
|
||||
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: "/review", // This is a temporary route for testing.
|
||||
name: "review",
|
||||
component: review,
|
||||
},
|
||||
{
|
||||
path: "/",
|
||||
name: "root",
|
||||
|
|
|
|||
Loading…
Reference in a new issue