Merged from develop

This commit is contained in:
Leah Schumann 2023-05-26 08:30:35 -04:00
commit b8978be3d5
20 changed files with 486 additions and 381 deletions

View file

@ -26,7 +26,7 @@ module.exports = {
testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"], testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"],
coverageThreshold: { coverageThreshold: {
global: { global: {
statements: 80, statements: 78,
// Got the go ahead from Mark to temporarily lower this. Taking out initialize component made the year,make,model and style coverage drop a bit. Once unit tests for license plate lookup, vin lookup and address lookup are in the coverage should go back up to 90 // Got the go ahead from Mark to temporarily lower this. Taking out initialize component made the year,make,model and style coverage drop a bit. Once unit tests for license plate lookup, vin lookup and address lookup are in the coverage should go back up to 90
}, },
}, },

View file

@ -108,12 +108,14 @@ const endpoints = {
}, },
GetShopTimeSlots: { GetShopTimeSlots: {
url: "/schedule/api/v1/schedule/shop-time-slots", url: "/schedule/api/v1/schedule/shop-time-slots",
mockUrl: "https://mockey.qa.sagaws.net/service/shop-time-slots", // TODO: REMOVE MOCKURL method: "POST",
},
GetMobileTimeSlots: {
url: "/schedule/api/v1/schedule/mobile-time-slots",
method: "POST", method: "POST",
}, },
GetMobileEarlyBirdFee: { GetMobileEarlyBirdFee: {
url: "/parts/api/v1/parts/mobile-early-bird-fee", url: "/parts/api/v1/parts/mobile-early-bird-fee",
mockUrl: "https://mockey.qa.sagaws.net/service/mobile-early-bird-fee", // TODO: REMOVE MOCKURL
method: "GET", method: "GET",
}, },
SaveSession: { SaveSession: {

View file

@ -33,6 +33,7 @@ const storeActions = {
GET_MOBILE_FEE_PART: "getMobileFeePart", GET_MOBILE_FEE_PART: "getMobileFeePart",
GET_SERVICEABILITY_DETAILS: "getServiceabilityDetails", GET_SERVICEABILITY_DETAILS: "getServiceabilityDetails",
GET_SHOP_TIME_SLOTS: "getShopTimeSlots", GET_SHOP_TIME_SLOTS: "getShopTimeSlots",
GET_MOBILE_TIME_SLOTS: "getMobileTimeSlots",
GET_PROVIDERS: "getProviders", GET_PROVIDERS: "getProviders",
GET_MOBILE_EARLY_BIRD_FEE: "getMobileEarlyBirdFee", GET_MOBILE_EARLY_BIRD_FEE: "getMobileEarlyBirdFee",
SAVE_SESSION: "saveSession", SAVE_SESSION: "saveSession",

View file

@ -257,7 +257,7 @@ export default {
watch: { watch: {
modelValue(newValue, oldValue) { modelValue(newValue, oldValue) {
this.resetField({ this.resetField({
value: newValue value: newValue,
}); });
}, },
answers() { answers() {

View file

@ -22,7 +22,6 @@
<div class="separator-line"></div> <div class="separator-line"></div>
<div class="nav-back ps-3"><button></button></div> <div class="nav-back ps-3"><button></button></div>
<div class="nav-forward pe-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">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">Monday</span>M</div>
<div class="grid-item caption"><span class="sr-only">Tuesday</span>T</div> <div class="grid-item caption"><span class="sr-only">Tuesday</span>T</div>
@ -84,7 +83,6 @@ export default {
months: null, months: null,
disableViewMoreDatesButton: false, disableViewMoreDatesButton: false,
selectableDatesData: [], // NOTE: uses monthNum (1-based), NOT monthIndex (0-based) selectableDatesData: [], // NOTE: uses monthNum (1-based), NOT monthIndex (0-based)
today: null,
hideSomeDaysForInitialView: null, hideSomeDaysForInitialView: null,
}; };
}, },
@ -112,6 +110,12 @@ export default {
}, },
}, },
computed: { computed: {
today() {
if (this.todayOverrideDateString) {
return new Date(this.todayOverrideDateString);
}
return new Date();
},
todayMonthIndex() { todayMonthIndex() {
return this.today.getMonth() + 1; return this.today.getMonth() + 1;
}, },
@ -151,39 +155,31 @@ export default {
this.$emit("date-clicked"); this.$emit("date-clicked");
}, },
getWeekStartDate(date) { getWeekStartDate(date) {
// Get the day of the week for date const dayOfWeek = date.getDay();
let dayOfWeek = date.getDay();
// Subtract the day of the week from date to get the date of Sunday // 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); sunday.setDate(sunday.getDate() - dayOfWeek);
// Return the date of Sunday
return sunday; return sunday;
}, },
getWeekEndDate(date) { getWeekEndDate(date) {
const currentDay = date.getDay(); // Get the day of the week (0 = Sunday, 1 = Monday, etc.) const dayOfWeek = date.getDay();
const daysUntilSaturday = 6 - currentDay; // Calculate the number of days until Saturday const daysUntilSaturday = 6 - dayOfWeek; // Calculate the number of days until Saturday
// Clone the given date and add the remaining days until Saturday // Clone the given date and add the remaining days until Saturday
const saturday = new Date(date); const saturday = new Date(date);
saturday.setDate(date.getDate() + daysUntilSaturday); saturday.setDate(date.getDate() + daysUntilSaturday);
return saturday; return saturday;
}, },
getNextWeekSunday(date) { getNextWeekSunday(date) {
const currentDay = date.getDay(); // Get the day of the week (0 = Sunday, 1 = Monday, etc.) const dayOfWeek = date.getDay();
const daysUntilNextSunday = currentDay === 0 ? 7 : 7 - currentDay; // Calculate the number of days until the next Sunday const daysUntilNextSunday = 7 - dayOfWeek; // Calculate the number of days until the next Sunday
// Clone the given date and add the remaining days until Sunday // Clone the given date and add the remaining days until Sunday
const nextSunday = new Date(date); const nextSunday = new Date(date);
nextSunday.setDate(date.getDate() + daysUntilNextSunday); nextSunday.setDate(date.getDate() + daysUntilNextSunday);
return nextSunday; return nextSunday;
}, },
getInitialViewWeeks(today, initialViewRowsToShow) { getInitialViewWeeks(today, initialViewRowsToShow) {
// TODO: this only is for future direction; create logic for past direction // TODO: this only is for future direction; need to create logic for past direction
let weeks = []; const weeks = [];
let weekStartDate = this.getWeekStartDate(today); let weekStartDate = this.getWeekStartDate(today);
let weekEndDate = this.getWeekEndDate(today); let weekEndDate = this.getWeekEndDate(today);
for (let i = 0; i < initialViewRowsToShow; i++) { for (let i = 0; i < initialViewRowsToShow; i++) {
@ -200,14 +196,19 @@ export default {
return weeks; return weeks;
}, },
async loadInitialData(config) { async loadInitialData(config) {
// CALLED FROM CONSUMING COMPONENT BEFORE DATE-PICKER APPEARS let todayDate;
const todayDate = config.todayOverrideDateString if (this.today) {
? new Date(config.todayOverrideDateString) todayDate = this.today;
: new Date(); } else if (config.todayOverrideDateString) {
todayDate = new Date(config.todayOverrideDateString);
} else {
todayDate = new Date();
}
let todayMonthIndex = todayDate.getMonth() + 1; const todayMonthIndex = todayDate.getMonth() + 1;
let todayYearNum = todayDate.getFullYear(); const todayYearNum = todayDate.getFullYear();
let currentMonthStart = new Date(todayYearNum, todayMonthIndex - 1, 1); // TODO - set up currentMonthStart if direction is PAST:
// let currentMonthStart = new Date(todayYearNum, todayMonthIndex - 1, 1);
let currentMonthEnd = new Date(todayYearNum, todayMonthIndex, 0); let currentMonthEnd = new Date(todayYearNum, todayMonthIndex, 0);
let calendarViewDirection = "none"; let calendarViewDirection = "none";
@ -219,10 +220,10 @@ export default {
config.initialViewRowsToShow config.initialViewRowsToShow
); );
let initialViewStartDate = todayDate; const initialViewStartDate = todayDate;
let initialViewEndDate = initialViewWeeks[initialViewWeeks.length - 1].weekEndDate; const initialViewEndDate = initialViewWeeks[initialViewWeeks.length - 1].weekEndDate;
let saturday1month = initialViewWeeks[0].weekEndDate.getMonth(); const saturday1month = initialViewWeeks[0].weekEndDate.getMonth();
let sunday5month = const sunday5month =
initialViewWeeks[initialViewWeeks.length - 1].weekStartDate.getMonth(); initialViewWeeks[initialViewWeeks.length - 1].weekStartDate.getMonth();
let hideSomeDaysForInitialView = false; let hideSomeDaysForInitialView = false;
@ -244,9 +245,10 @@ export default {
let myPromise = new Promise((resolve, reject) => { let myPromise = new Promise((resolve, reject) => {
const response = config.customSelectableDatesCallback( const response = config.customSelectableDatesCallback(
initialViewStartDate, initialViewStartDate.toISOString().split("T")[0],
initialViewEndDate, initialViewEndDate.toISOString().split("T")[0],
store.getters.order.serviceLocation.appointmentType store.getters.order.serviceLocation.appointmentType,
store.getters.order.serviceLocation.provider.providerNumber
); );
resolve(response); resolve(response);
}); });
@ -284,9 +286,9 @@ export default {
// Growing from 0 to 1 // Growing from 0 to 1
time = Math.min(1, (timestamp - start) / duration); time = Math.min(1, (timestamp - start) / duration);
let percentageNew = timingFunc(time); const percentageNew = timingFunc(time);
let distanceToGo = targetY; const distanceToGo = targetY;
let thisDistance = percentageNew * distanceToGo; const thisDistance = percentageNew * distanceToGo;
wrapper.scrollTo(0, initY + thisDistance); wrapper.scrollTo(0, initY + thisDistance);
@ -305,13 +307,11 @@ export default {
}, },
async setCalendarData(config = {}) { async setCalendarData(config = {}) {
this.today = config.todayDate;
this.hideSomeDaysForInitialView = config.hideSomeDaysForInitialView; this.hideSomeDaysForInitialView = config.hideSomeDaysForInitialView;
let hideSecondMonth = config.hideSecondMonth; const hideSecondMonth = config.hideSecondMonth;
const direction = config.calendarViewDirection; const direction = config.calendarViewDirection;
const monthsAfterToLoadOffset = 12;
const monthsAfterToLoadOffset = 12; // TO BE MADE "CONSTANTS" const monthsBeforeToLoadOffset = 36;
const monthsBeforeToLoadOffset = 36; // TO BE MADE "CONSTANTS"
config.initialShopTimeSlotsResponse.days.forEach((selectableDate) => { config.initialShopTimeSlotsResponse.days.forEach((selectableDate) => {
this.selectableDatesData.push(selectableDate); this.selectableDatesData.push(selectableDate);
}); });
@ -382,25 +382,25 @@ export default {
} }
} }
const monthEndDate = new Date(yearNum, monthIndex, 0); // BOTH const monthEndDate = new Date(yearNum, monthIndex, 0);
let monthEndDateNum = monthEndDate.getDate(); // BOTH let monthEndDateNum = monthEndDate.getDate();
if ( if (
offset === 0 && offset === 0 &&
calendarViewDirection === "past" && calendarViewDirection === "past" &&
monthEndDateNum > this.currentWeekEndDateNum monthEndDateNum > this.currentWeekEndDateNum
) { ) {
monthEndDateNum = this.currentWeekEndDateNum; // PAST monthEndDateNum = this.currentWeekEndDateNum;
} }
const monthStartDateNum = const monthStartDateNum =
offset === 0 && calendarViewDirection === "future" offset === 0 && calendarViewDirection === "future"
? this.currentWeekStartDateNum ? this.currentWeekStartDateNum
: 1; // FUTURE : 1;
const monthStartDate = new Date(yearNum, monthIndex - 1, monthStartDateNum); // BOTH const monthStartDate = new Date(yearNum, monthIndex - 1, monthStartDateNum);
const startDateDayIndex = monthStartDate.getDay(); // FUTURE const startDateDayIndex = monthStartDate.getDay();
const endDateDayIndex = monthEndDate.getDay(); // PAST const endDateDayIndex = monthEndDate.getDay();
if (Math.abs(offset) === 1 && hideSecondMonth) { if (Math.abs(offset) === 1 && hideSecondMonth) {
monthClass = monthClass + " month-hidden"; monthClass = monthClass + " month-hidden";
@ -424,7 +424,7 @@ export default {
// populate dates array // populate dates array
for (let i = monthStartDateNum; i <= monthEndDateNum; i++) { for (let i = monthStartDateNum; i <= monthEndDateNum; i++) {
let dayClasses = ""; let dayClasses = "";
let dateString = const dateString =
yearNum.toString() + yearNum.toString() +
"-" + "-" +
forceTwoDigitString(monthIndex) + forceTwoDigitString(monthIndex) +
@ -514,7 +514,7 @@ export default {
monthToShow.dates[monthToShow.dates.length - 1].inputValue.dateString monthToShow.dates[monthToShow.dates.length - 1].inputValue.dateString
); );
this.isLoading = false; 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", ""); monthToShow.monthClass = monthToShow.monthClass.replace(" month-hidden", "");
this.scrollToElement(monthToShow.monthString); this.scrollToElement(monthToShow.monthString);
@ -526,7 +526,8 @@ export default {
const moreSelectableDates = await this.customSelectableDatesCallback( const moreSelectableDates = await this.customSelectableDatesCallback(
monthStart, monthStart,
monthEnd, monthEnd,
this.$store.getters.order.serviceLocation.appointmentType this.$store.getters.order.serviceLocation.appointmentType,
this.$store.getters.order.serviceLocation.provider.providerNumber
); );
moreSelectableDates.days.forEach((selectableDate) => { moreSelectableDates.days.forEach((selectableDate) => {
const index = this.selectableDatesData.findIndex( const index = this.selectableDatesData.findIndex(

View file

@ -10,7 +10,7 @@ export default {
name: "textBlock", name: "textBlock",
props: { props: {
customText: String, // used to allow the insert of token values into textblock 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) typeStyle: String, // h1-h6, body, small, label, caption (see Figma or Confluence documentation)
fontWeight: String, // bold=500, default is 400 fontWeight: String, // bold=500, default is 400
cmsWidgetName: String, cmsWidgetName: String,
@ -28,15 +28,11 @@ export default {
<style lang="scss" scoped> <style lang="scss" scoped>
.text-block { .text-block {
display: flex;
&.left {
justify-content: flex-start;
}
&.right { &.right {
justify-content: flex-end; text-align: right;
} }
&.center { &.center {
justify-content: center; text-align: center;
} }
&.bold { &.bold {
font-weight: 500; font-weight: 500;

View file

@ -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 };
}

View file

@ -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>

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

@ -25,17 +25,17 @@
v-model="selectedDate" v-model="selectedDate"
:customSelectableDatesCallback="getAvailableDatesMethod" :customSelectableDatesCallback="getAvailableDatesMethod"
@date-clicked="openInshopTimeSlotsModal" /> @date-clicked="openInshopTimeSlotsModal" />
<!-- todayOverrideDateString="2023-08-06T03:00:00" -->
<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"
@ -73,6 +73,7 @@ import {
getRouterLinkRouteFromCopy, getRouterLinkRouteFromCopy,
getRouterLinkDisplayTextFromCopy, getRouterLinkDisplayTextFromCopy,
} from "@/helpers/cms-content-helper"; } from "@/helpers/cms-content-helper";
import { AppointmentTypeStrings, 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";
@ -80,18 +81,33 @@ import store from "@/store";
defineRule("date-required", required(errorMessages.DATE_REQUIRED)); defineRule("date-required", required(errorMessages.DATE_REQUIRED));
defineRule("time-slot-selection-required", required(errorMessages.DATE_REQUIRED)); defineRule("time-slot-selection-required", required(errorMessages.DATE_REQUIRED));
const getAvailableDates = async (startDate, endDate, appointmentType) => { const getAvailableDates = async (startDate, endDate, appointmentType, providerNumber) => {
// USING DATES PASSED, MAKE AN API CALL // USING DATES PASSED, MAKE AN API CALL
const newShopTimeSlotsResponse = await baseMixin.methods.dispatchStoreAction(
storeActions.GET_SHOP_TIME_SLOTS, let newTimeSlotsResponse;
{ if (appointmentType === AppointmentTypeStrings.MOBILE) {
startDate: startDate, newTimeSlotsResponse = await baseMixin.methods.dispatchStoreAction(
endDate: endDate, storeActions.GET_MOBILE_TIME_SLOTS,
shopAppointmentType: appointmentType, {
}, startDate: startDate,
false endDate: endDate,
); },
const newShopTimeSlots = newShopTimeSlotsResponse.data; false
);
} else {
newTimeSlotsResponse = await baseMixin.methods.dispatchStoreAction(
storeActions.GET_SHOP_TIME_SLOTS,
{
startDate: startDate,
endDate: endDate,
shopAppointmentType: appointmentType,
providerNumber: providerNumber,
},
false
);
}
const newShopTimeSlots = newTimeSlotsResponse.data;
return convertApiResponse(newShopTimeSlots); return convertApiResponse(newShopTimeSlots);
}; };
const convertApiResponse = (responseData) => { const convertApiResponse = (responseData) => {
@ -112,9 +128,12 @@ export default {
data() { data() {
return { return {
selectedDate: null, selectedDate: null,
selectedTimeSlotId: null, selectedTimeSlotData: {
id: null,
isPremiumAppointment: null,
},
selectableDatesData: [], selectableDatesData: [],
mobileEarlyBirdFee: null, mobilePremiumAppointmentFee: null,
}; };
}, },
async beforeRouteEnter(to, from, next) { async beforeRouteEnter(to, from, next) {
@ -125,40 +144,29 @@ export default {
selectableDatesSetting: "custom", selectableDatesSetting: "custom",
initialViewRowsToShow: 5, initialViewRowsToShow: 5,
customSelectableDatesCallback: getAvailableDates, 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 // Price EARLY BIRD pre-emptively to allow for asynchronous call to pricing
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 = locationAlerts.methods.loadInitialData( const alertReasonsPromise = locationAlerts.methods.loadInitialData(
store.getters.order.serviceLocation.zipCodeCtu store.getters.order.serviceLocation.zipCodeCtu
); );
// Settle promises and get results // Settle promises and get results
const promiseResultMap = [ const promiseResultMap = [
{ {
@ -174,8 +182,8 @@ export default {
promise: datePickerInitialDataPromise, promise: datePickerInitialDataPromise,
}, },
{ {
resultKey: "earlyBird", resultKey: "premiumFee",
promise: earlyBirdPromise, promise: premiumFeePromise,
}, },
{ {
resultKey: "pricingResults", resultKey: "pricingResults",
@ -191,8 +199,8 @@ export default {
vm.$refs.datePicker.initializeComponent(resultMap.datePickerInitialData); vm.$refs.datePicker.initializeComponent(resultMap.datePickerInitialData);
vm.$refs.locationAlerts.initializeComponent(resultMap.alertReasons); vm.$refs.locationAlerts.initializeComponent(resultMap.alertReasons);
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];
} }
}); });
}, },
@ -216,18 +224,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;
@ -247,7 +255,8 @@ export default {
const newShopTimeSlots = await getAvailableDates( const newShopTimeSlots = await getAvailableDates(
startDate, startDate,
endDate, endDate,
this.appointmentType this.appointmentType,
this.$store.getters.order.serviceLocation.provider.providerNumber
); );
// ADD API CALL RESULTS TO EXISTING DATE DATA // ADD API CALL RESULTS TO EXISTING DATE DATA
this.selectableDatesData.days = this.selectableDatesData.days.concat( this.selectableDatesData.days = this.selectableDatesData.days.concat(
@ -268,11 +277,63 @@ export default {
return timeSlots.find((timeSlot) => timeSlot.id === timeSlotId); return timeSlots.find((timeSlot) => timeSlot.id === timeSlotId);
}, },
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;
} }
}, },
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`);
// Ex: April 25
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 = hours > 11 ? "PM" : "AM";
if (hours > 12) {
hours -= 12;
}
if (shouldTrimMinutesIfEmpty && minutes === "00") {
return `${hours} ${meridianNotation}`;
} else {
return `${hours}:${minutes} ${meridianNotation}`;
}
},
backButtonAction() { backButtonAction() {
this.$router.navigateWithoutSaving(this.navigationScenarios.CLICKED_BACK, this.$route); this.$router.navigateWithoutSaving(this.navigationScenarios.CLICKED_BACK, this.$route);
}, },
@ -296,9 +357,15 @@ 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,
};
} }
}, },
selectedTimeSlotData() {
this.updateFooterButtonText(this.selectedTimeSlotData);
},
}, },
components: { components: {
funnelHeader, funnelHeader,

View file

@ -6,45 +6,32 @@
<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
<span v-if="buttonLabelSubCopy" class="m-0 small" :class="textPosition"> v-if="buttonLabelSubCopy"
{{ formattedButtonLabelSubCopy }} class="premium-appointment-price"
:class="textPosition">
{{ formattedButtonLabelSubCopy }}
</span>
</span> </span>
<span v-if="screenReaderOnlyText" class="sr-only"> <span v-if="screenReaderOnlyText" class="sr-only">
{{ screenReaderOnlyText }} {{ screenReaderOnlyText }}
</span> </span>
<loader
v-if="isLoaderDisplayed && selectingInitiatesLoad"
:class="[this.loaderColor, this.loaderPosition]" />
</div> </div>
</baseInputButton> </baseInputButton>
</template> </template>
<script> <script>
import loader from "@/ux-components/loader/loader";
import baseInputButton from "@/digital-components/base-input-button/base-input-button"; import baseInputButton from "@/digital-components/base-input-button/base-input-button";
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: {
loaderColor: String,
loaderPosition: {
type: String,
default: "right",
},
},
data() {
return {
isLoaderDisplayed: false,
};
},
computed: { computed: {
formattedButtonLabelSubCopy() { formattedButtonLabelSubCopy() {
return this.buttonLabelSubCopy?.toFixed(2); return this.buttonLabelSubCopy;
}, },
}, },
methods: { methods: {
@ -58,16 +45,12 @@ export default {
}, },
}, },
components: { components: {
loader,
baseInputButton, baseInputButton,
}, },
}; };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.loader {
position: absolute;
}
.list-button { .list-button {
outline: none; outline: none;
input[type="radio"], input[type="radio"],
@ -85,6 +68,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 +95,20 @@ export default {
width: 100%; width: 100%;
outline: none; outline: none;
span { span.premium-appointment-price {
&.small { position: absolute;
font-size: 0.75rem; background: $green-100;
color: $gray-550; 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> </style>

View file

@ -14,12 +14,12 @@
typeStyle="small" typeStyle="small"
class="duration-text-block" /> class="duration-text-block" />
<buttonQuestion <buttonQuestion
buttonTypeString="timeslotModalListButton" buttonTypeString="timeSlotModalListButton"
:buttonTypeObject="timeslotModalListButton" :buttonTypeObject="timeSlotModalListButton"
: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" />
@ -41,7 +41,7 @@
import modal from "@/digital-components/modal/modal"; import modal from "@/digital-components/modal/modal";
import textBlock from "@/digital-components/text-block/text-block"; import textBlock from "@/digital-components/text-block/text-block";
import buttonQuestion from "@/digital-components/button-question/button-question"; import buttonQuestion from "@/digital-components/button-question/button-question";
import timeslotModalListButton from "./timeslot-modal-list-button/timeslot-modal-list-button"; import timeSlotModalListButton from "./time-slot-modal-list-button/time-slot-modal-list-button";
// TODO: Move this somewhere more global // TODO: Move this somewhere more global
import { DAYS_OF_WEEK, MONTHS_OF_YEAR } from "@/digital-components/date-picker/mixins/constants.js"; import { DAYS_OF_WEEK, MONTHS_OF_YEAR } from "@/digital-components/date-picker/mixins/constants.js";
@ -49,15 +49,17 @@ 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,19 +67,21 @@ 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,
timeslotModalListButton: timeslotModalListButton, isSelectedAppointmentPremium: null,
timeSlotModalListButton: timeSlotModalListButton,
}; };
}, },
setup(props) { setup(props) {
@ -89,42 +93,35 @@ 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) {
this.autoSelectTimeSlotIfOnlyOneIsAvailable(newValue);
}, },
// dateAndTimeSlotData(newValue, oldValue) {
// const numberOfOptions = newValue?.timeSlots.length;
// console.log('running');
// if (numberOfOptions === 1) {
// 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
if (this.isSameDay) { ? this.sameDayDropOffCmsWidgetName
cmsFieldName = "BodyText2"; : this.dropoffCmsWidgetName;
}
} }
return this.getCmsContent(appointmentTypeCmsWidgetName, cmsFieldName); return this.getCmsContent(appointmentTypeCmsWidgetName, "BodyText");
}, },
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");
@ -167,53 +164,24 @@ export default {
} }
// This conversion ensures we don't get get GMT induced date changes // This conversion ensures we don't get get GMT induced date changes
const dateObject = new Date(`${this.dateAndTimeSlotData.dateString}T00:00:00`); const dateObject = new Date(`${this.dateAndTimeSlotData.dateString}T00:00:00`);
const weekdayName = DAYS_OF_WEEK[dateObject.getDay()]; // Ex: Tuesday, April 22
const month = MONTHS_OF_YEAR[dateObject.getMonth()]; return dateObject.toLocaleDateString("en-us", {
const numberDayOfMonth = dateObject.getDate(); weekday: "long",
// Ex. Tuesday, April 23 month: "long",
return `${weekdayName}, ${month} ${numberDayOfMonth}`; day: "numeric",
});
}, },
availableTimeSlots() { availableTimeSlots() {
if (this.dateAndTimeSlotData === null) { if (this.dateAndTimeSlotData === null) {
return null; return null;
} }
let availableTimeSlots;
if (this.appointmentType === AppointmentTypeStrings.DROP_OFF) { if (this.appointmentType === AppointmentTypeStrings.DROP_OFF) {
availableTimeSlots = [ return this.getAvailableTimeSlotsForDropOff(this.dateAndTimeSlotData.timeSlots);
{ } else if (this.appointmentType === AppointmentTypeStrings.MOBILE) {
value: this.dateAndTimeSlotData.timeSlots[0], return this.getAvailableTimeSlotsForMobile(this.dateAndTimeSlotData.timeSlots);
buttonLabel: this.dropoffButtonText,
},
];
} else { } else {
availableTimeSlots = this.dateAndTimeSlotData.timeSlots.map((timeslot) => { return this.getAvailableTimeSlotsForInshop(this.dateAndTimeSlotData.timeSlots);
let readableTime;
if (this.appointmentType === AppointmentTypeStrings.IN_SHOP) {
readableTime = this.getDisplayTextForMilitaryTime(timeslot.startTime);
} else if (this.appointmentType === AppointmentTypeStrings.MOBILE) {
readableTime = `${this.getDisplayTextForMilitaryTime(
timeslot.startTime
)} - ${this.getDisplayTextForMilitaryTime(timeslot.endTime)}`;
}
return {
value: timeslot.id,
buttonLabel: readableTime,
};
});
} }
if (this.appointmentType === AppointmentTypeStrings.MOBILE) {
const offerPremium = this.dateAndTimeSlotData.timeSlots[0].offerPremium;
const hasEarlyBird = this.mobileEarlyBirdFee?.partType === "EARLY BIRD";
if (offerPremium && hasEarlyBird) {
availableTimeSlots.unshift({
value: this.dateAndTimeSlotData.timeSlots[0].id + "-earlybird",
buttonLabel: this.earlyBirdButtonText,
buttonLabelSubCopy: this.getTotalLineItemPrice(this.mobileEarlyBirdFee),
});
}
}
return availableTimeSlots;
}, },
}, },
methods: { methods: {
@ -222,22 +190,36 @@ 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"
getDisplayTextForMilitaryTime(militaryTimeInput) { getDisplayTextForMilitaryTime(militaryTimeInput) {
let hours = parseInt(militaryTimeInput.split(":")[0]); let hours = parseInt(militaryTimeInput.split(":")[0]);
const minutes = militaryTimeInput.split(":")[1]; const minutes = militaryTimeInput.split(":")[1];
let meridianNotation = "AM"; let meridianNotation = hours > 11 ? "PM" : "AM";
if (militaryTimeInput.split(":")[0] > 12) { if (hours > 12) {
hours -= 12; hours -= 12;
meridianNotation = "PM";
} }
return `${hours}:${minutes} ${meridianNotation}`; return `${hours}:${minutes} ${meridianNotation}`;
}, },
@ -252,6 +234,77 @@ export default {
} }
return displayTextForDurationLength; return displayTextForDurationLength;
}, },
getAvailableTimeSlotsForInshop(timeSlotsForSelectedDate) {
return timeSlotsForSelectedDate.map((timeSlot) => {
const readableTime = this.getDisplayTextForMilitaryTime(timeSlot.startTime);
return {
value: timeSlot.id,
buttonLabel: readableTime,
};
});
},
getAvailableTimeSlotsForDropOff(timeSlotsForSelectedDate) {
return [
{
value: timeSlotsForSelectedDate[0].id,
buttonLabel: this.dropoffButtonText,
},
];
},
getAvailableTimeSlotsForMobile(timeSlotsForSelectedDate) {
const availableTimeSlots = timeSlotsForSelectedDate.map((timeSlot) => {
const readableTime = `${this.getDisplayTextForMilitaryTime(
timeSlot.startTime
)} - ${this.getDisplayTextForMilitaryTime(timeSlot.endTime)}`;
return {
value: timeSlot.id,
buttonLabel: readableTime,
};
});
const isPremiumTimeSlot = timeSlotsForSelectedDate[0].offerPremium;
const hasPremiumPartAvailable =
this.premiumAppointmentFee?.partType === PREMIUM_FEE_PART_TYPE;
if (isPremiumTimeSlot && hasPremiumPartAvailable) {
availableTimeSlots.unshift(
this.getPremiumAppointmentTimeSlot(timeSlotsForSelectedDate[0])
);
}
return availableTimeSlots;
},
getPremiumAppointmentTimeSlot(timeSlotData) {
const formattedPrice =
"+$" + this.getTotalLineItemPrice(this.premiumAppointmentFee).toFixed(2);
return {
// Unique value is required for each <input> and the premium appoinment shares a timeSlot ID
value: this.addPremiumFlagToInput(timeSlotData.id),
buttonLabel: this.premiumAppointmentButtonText,
buttonLabelSubCopy: formattedPrice,
additionalButtonData: {
isPremiumAppointment: true,
},
};
},
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);
},
removePremiumFlagFromInput(timeSlotId) {
return parseInt(timeSlotId.trim(PREMIUM_TIME_SLOT_ID_FLAG.length));
},
}, },
components: { components: {
modal, modal,

View file

@ -44,7 +44,7 @@ export async function getServiceabilityDetails(serviceZipCode, lineItems) {
return Promise.resolve(serviceabilityDetails); return Promise.resolve(serviceabilityDetails);
} }
export async function getAvailabilityRating(providerNumber, startDate, endDate, appointmentType) { export async function getAvailabilityRating(startDate, endDate, shopAppointmentType, providerNumber) {
// For a given shop provider number and date range, get the appointment time slots available // For a given shop provider number and date range, get the appointment time slots available
const shopTimeSlots = await baseMixin.methods.dispatchStoreAction( const shopTimeSlots = await baseMixin.methods.dispatchStoreAction(
storeActions.GET_SHOP_TIME_SLOTS, storeActions.GET_SHOP_TIME_SLOTS,
@ -52,7 +52,7 @@ export async function getAvailabilityRating(providerNumber, startDate, endDate,
providerNumber: providerNumber, providerNumber: providerNumber,
startDate: startDate, startDate: startDate,
endDate: endDate, endDate: endDate,
shopAppointmentType: appointmentType, shopAppointmentType: shopAppointmentType,
}, },
false false
); );

View file

@ -236,6 +236,17 @@ export default {
}); });
}, },
async setMobileLocation() { async setMobileLocation() {
// START - TEMP CODE FROM A CAOUETTE 5/25/23 TO BE REMOVED BY EOD
let tempTest = false;
let internalModelAddressQuestions = this.internalModel.addressQuestions;
let modelValueAddressQuestions = this.modelValue.addressQuestions;
if (tempTest) {
// THIS WILL NEVER BE TRUE
console.warn(internalModelAddressQuestions);
console.warn(modelValueAddressQuestions);
}
// END - TEMP CODE FROM A CAOUETTE 5/25/23 TO BE REMOVED BY EOD
if ( if (
this.internalModel.addressQuestions.zipCode !== this.internalModel.addressQuestions.zipCode !==
this.modelValue.addressQuestions.zipCode this.modelValue.addressQuestions.zipCode
@ -270,6 +281,13 @@ export default {
this.closeModal(); this.closeModal();
} }
} else {
// START - TEMP CODE FROM A CAOUETTE 5/25/23 TO BE REMOVED BY EOD
if (tempTest) {
// THIS WILL NEVER BE TRUE
console.warn("the zips match and the exception was run");
}
// END - TEMP CODE FROM A CAOUETTE 5/25/23 TO BE REMOVED BY EOD
} }
}, },
}, },

View file

@ -52,9 +52,14 @@ export default {
props: { props: {
loaderColor: String, loaderColor: String,
}, },
async beforeMount() { beforeMount() {
this.displayLoader(); this.displayLoader();
this.additionalButtonData.availabilityRatingCallback(this.modelValue).then((data) => { console.log(this.value)
const startDate = this.additionalButtonData.startDate;
const endDate = this.additionalButtonData.endDate;
const shopAppointmentType = this.additionalButtonData.shopAppointmentType;
this.additionalButtonData.availabilityRatingCallback(startDate, endDate, shopAppointmentType, this.value).then((data) => {
this.availabilityRating = data; this.availabilityRating = data;
this.isLoaderDisplayed = false; this.isLoaderDisplayed = false;
}); });
@ -66,7 +71,7 @@ export default {
availabilityRating: "None", availabilityRating: "None",
availabilityRatingClass: "", availabilityRatingClass: "",
loaderPosition: "left", loaderPosition: "left",
blockUi: "no-block" blockUi: "no-block",
}; };
}, },
methods: { methods: {

View file

@ -106,8 +106,18 @@ export default {
return this.getCmsContent("ShowMoreShopsLinkWidget", "Text"); return this.getCmsContent("ShowMoreShopsLinkWidget", "Text");
}, },
additionalButtonData() { additionalButtonData() {
const startDate = new Date();
const endDate = new Date()
endDate.setDate(startDate.getDate() + 7);
const formattedStartDate = startDate.toISOString().split("T")[0];
const formattedEndDate = endDate.toISOString().split("T")[0];
return { return {
availabilityRatingCallback: getAvailabilityRating, availabilityRatingCallback: getAvailabilityRating,
startDate: formattedStartDate,
endDate: formattedEndDate,
shopAppointmentType: this.selectedAppointmentType,
}; };
}, },
}, },

View file

@ -28,21 +28,7 @@ import analyticsMixin from "@/mixins/analytics-mixin";
import { experimentTriggers } from "../constants/experiments"; import { experimentTriggers } from "../constants/experiments";
import { applicationConfig } from "../constants/application-config"; 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 = [ 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: "/", path: "/",
name: "root", name: "root",

View file

@ -1136,57 +1136,121 @@ export const actions = {
}); });
}, },
getShopTimeSlots( getShopTimeSlots(context, { startDate, endDate, shopAppointmentType, providerNumber }) {
context,
{
providerNumber = null,
startDate = "2023-01-01",
endDate = "2023-05-01",
shopAppointmentType = "",
}
) {
const order = context.state.order; const order = context.state.order;
const mockArray = []; const vehicle = context.state.order.vehicle;
let partNumbers = [
...(order.lineItems.supportingItems ?? []),
...(order.lineItems.vaps ?? []),
...getFlattenedArrayOfLineItemsWithChildParts(order.lineItems.glassParts),
];
partNumbers = partNumbers.map((lineItem) => {
return lineItem.partNumber;
});
const glassPieces = order.damage.glassToReplace ?? [];
var payload = { var payload = {
providerNumber: providerNumber ?? order.providerNumber, providerNumber: providerNumber,
startDate: startDate, startDate: startDate,
endDate: endDate, endDate: endDate,
shopAppointmentType: shopAppointmentType, shopAppointmentType: shopAppointmentType,
applicationName: applicationConfig.APPLICATION_NAME, applicationName: "Safelite.com funnel",
parentAccountNumber: context.getters.payment.parentAccountNumber, parentAccountNumber: context.getters.payment.parentAccountNumber,
carId: context.getters.vehicle.carId, carId: vehicle.carId,
partNumbers: mockArray, // TODO: WHERE DO I GET THIS? partNumbers: partNumbers,
glassPieces: glassPieces,
eon: order.eon, eon: order.eon,
provisionalReasons: mockArray, // TODO: WHERE DO I GET THIS? coverage: {
status: "",
deductible: 0,
additionalAuthFlag: "",
},
partSelection: {
// TODO: Provisional booking will utilize these fields
hasAnsweredPartQuestions: false,
hasAnsweredMoldingQuestions: false,
hasAnsweredCapabilityQuestions: false,
hasManuallySelectedParts: false,
},
vehicle: {
year: vehicle.year,
make: vehicle.make,
model: vehicle.model,
style: vehicle.style,
vin: vehicle.vin,
},
}; };
// TODO: REMOVE MOCK CALL & USE REAL CALL BELOW console.log(payload)
return globalMethods.callMockHttpClient({
return globalMethods.callHttpClient({
method: endpoints.GetShopTimeSlots.method, method: endpoints.GetShopTimeSlots.method,
endpoint: endpoints.GetShopTimeSlots.mockUrl, endpoint: endpoints.GetShopTimeSlots.url,
payload: payload, payload: payload,
logApiCall: false,
});
},
getMobileTimeSlots(context, { startDate, endDate }) {
const order = context.state.order;
const vehicle = context.state.order.vehicle;
let partNumbers = [
...(order.lineItems.supportingItems ?? []),
...(order.lineItems.vaps ?? []),
...getFlattenedArrayOfLineItemsWithChildParts(order.lineItems.glassParts),
];
partNumbers = partNumbers.map((lineItem) => {
return lineItem.partNumber;
});
const glassPieces = order.damage.glassToReplace ?? [];
var payload = {
startDate: startDate,
endDate: endDate,
applicationName: "Safelite.com funnel",
parentAccountNumber: context.getters.payment.parentAccountNumber,
carId: vehicle.carId,
partNumbers: partNumbers,
glassPieces: glassPieces,
eon: order.eon,
coverage: {
status: "",
deductible: 0,
additionalAuthFlag: "",
},
partSelection: {
// TODO: Provisional booking will utilize these fields
hasAnsweredPartQuestions: false,
hasAnsweredMoldingQuestions: false,
hasAnsweredCapabilityQuestions: false,
hasManuallySelectedParts: false,
},
vehicle: {
year: vehicle.year,
make: vehicle.make,
model: vehicle.model,
style: vehicle.style,
vin: vehicle.vin,
},
zipCode: order.serviceLocation.zipCode,
};
return globalMethods.callHttpClient({
method: endpoints.GetMobileTimeSlots.method,
endpoint: endpoints.GetMobileTimeSlots.url,
payload: payload,
logApiCall: false,
}); });
// TODO: RESTORE THIS
// return globalMethods.callHttpClient({
// method: endpoints.GetShopTimeSlots.method,
// endpoint: endpoints.GetShopTimeSlots.url,
// payload: payload,
// logApiCall: false,
// });
}, },
getMobileEarlyBirdFee(context) { getMobileEarlyBirdFee(context) {
const damageType = context.getters.damage.isRepair ? "Repair" : "Replace"; const damageType = context.getters.damage.isRepair ? "Repair" : "Replace";
const paymentType = context.getters.order.payment.isInsurance ? "Insurance" : "Cash"; const paymentType = context.getters.order.payment.isInsurance ? "Insurance" : "Cash";
return globalMethods.callMockHttpClient({
return globalMethods.callHttpClient({
method: endpoints.GetMobileEarlyBirdFee.method, method: endpoints.GetMobileEarlyBirdFee.method,
endpoint: `${endpoints.GetMobileEarlyBirdFee.mockUrl}/Cash/Replace`, endpoint: `${endpoints.GetMobileEarlyBirdFee.url}/${paymentType}/${damageType}`,
}); });
// return globalMethods.callHttpClient({
// method: endpoints.GetMobileEarlyBirdFee.method,
// endpoint: `${endpoints.GetMobileEarlyBirdFee.url}/${paymentType}/${damageType}`,
// });
}, },
// Session API Actions // Session API Actions
@ -1913,7 +1977,7 @@ function addPricesToLineItems(lineItems, pricingLineItems) {
function getFlattenedArrayOfLineItemsWithChildParts(lineItems) { function getFlattenedArrayOfLineItemsWithChildParts(lineItems) {
let flattenedArray = []; let flattenedArray = [];
lineItems.forEach((lineItem) => { lineItems?.forEach((lineItem) => {
flattenedArray.push(lineItem); flattenedArray.push(lineItem);
if (lineItem.childParts) { if (lineItem.childParts) {
flattenedArray = [ flattenedArray = [

View file

@ -21,8 +21,8 @@ export default {
}, },
blockUi: { blockUi: {
type: Boolean, type: Boolean,
default: true default: true,
} },
}, },
}; };
</script> </script>