Merge remote-tracking branch 'origin/develop' into release/submit-cash
This commit is contained in:
commit
fb6e825aa3
48 changed files with 1813 additions and 729 deletions
|
|
@ -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: 77,
|
||||||
// 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
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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",
|
method: "POST",
|
||||||
},
|
},
|
||||||
GetMobileEarlyBirdFee: {
|
GetMobileTimeSlots: {
|
||||||
url: "/parts/api/v1/parts/mobile-early-bird-fee",
|
url: "/schedule/api/v1/schedule/mobile-time-slots",
|
||||||
mockUrl: "https://mockey.qa.sagaws.net/service/mobile-early-bird-fee", // TODO: REMOVE MOCKURL
|
method: "POST",
|
||||||
|
},
|
||||||
|
GetMobilePremiumFee: {
|
||||||
|
url: "/parts/api/v1/parts/mobile-premium-fee",
|
||||||
method: "GET",
|
method: "GET",
|
||||||
},
|
},
|
||||||
SaveSession: {
|
SaveSession: {
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ const experimentUniverses = {
|
||||||
const experimentSettings = {
|
const experimentSettings = {
|
||||||
GOOGLE_CUSTOM_DIMENSION_INDEX: "Google Custom Dimension Index",
|
GOOGLE_CUSTOM_DIMENSION_INDEX: "Google Custom Dimension Index",
|
||||||
SUPPRESS_VIN_CAPTURE: "SuppressVinCapture",
|
SUPPRESS_VIN_CAPTURE: "SuppressVinCapture",
|
||||||
|
DISPLAY_AVAILABILITY_INDICATORS: "DisplayAvailabilityIndicators",
|
||||||
};
|
};
|
||||||
|
|
||||||
const experimentTriggers = {
|
const experimentTriggers = {
|
||||||
|
|
|
||||||
11
src/constants/schedule-constants.js
Normal file
11
src/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 };
|
||||||
|
|
@ -33,8 +33,9 @@ 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_PREMIUM_FEE: "getMobilePremiumFee",
|
||||||
SAVE_SESSION: "saveSession",
|
SAVE_SESSION: "saveSession",
|
||||||
LOAD_SESSION: "loadSession",
|
LOAD_SESSION: "loadSession",
|
||||||
UPDATE_STORE_WITH_SAVE_SESSION_RESPONSE: "updateStoreWithSaveSessionResponse",
|
UPDATE_STORE_WITH_SAVE_SESSION_RESPONSE: "updateStoreWithSaveSessionResponse",
|
||||||
|
|
@ -62,6 +63,7 @@ const storeActions = {
|
||||||
SAVE_VEHICLE_STYLE: "saveVehicleStyle",
|
SAVE_VEHICLE_STYLE: "saveVehicleStyle",
|
||||||
SAVE_VEHICLE_DAMAGE: "saveVehicleDamage",
|
SAVE_VEHICLE_DAMAGE: "saveVehicleDamage",
|
||||||
SAVE_VIN_LOOKUP: "saveVinLookup",
|
SAVE_VIN_LOOKUP: "saveVinLookup",
|
||||||
|
SAVE_SERVICE_ZIP_CODE_INFO: "saveServiceZipCodeInfo",
|
||||||
SAVE_SERVICE_LOCATION: "saveServiceLocation",
|
SAVE_SERVICE_LOCATION: "saveServiceLocation",
|
||||||
SAVE_SCHEDULE: "saveSchedule",
|
SAVE_SCHEDULE: "saveSchedule",
|
||||||
SAVE_EMAIL: "saveEmail",
|
SAVE_EMAIL: "saveEmail",
|
||||||
|
|
@ -69,6 +71,7 @@ const storeActions = {
|
||||||
SAVE_VIN: "saveVin",
|
SAVE_VIN: "saveVin",
|
||||||
SAVE_REGISTRATION_ADDRESS_LOOKUP: "saveRegistrationAddressLookup",
|
SAVE_REGISTRATION_ADDRESS_LOOKUP: "saveRegistrationAddressLookup",
|
||||||
SAVE_GLASS_PARTS: "saveGlassParts",
|
SAVE_GLASS_PARTS: "saveGlassParts",
|
||||||
|
SAVE_GLASS_PART_PRICES: "saveGlassPartPrices",
|
||||||
SAVE_PART_QUESTION_ANSWERS: "savePartQuestionAnswers",
|
SAVE_PART_QUESTION_ANSWERS: "savePartQuestionAnswers",
|
||||||
RESET_MOLDING_AND_CAPABILITY_QUESTIONS_IF_NEEDED:
|
RESET_MOLDING_AND_CAPABILITY_QUESTIONS_IF_NEEDED:
|
||||||
"resetMoldingAndCapabilityQuestionAnswersIfNeeded",
|
"resetMoldingAndCapabilityQuestionAnswersIfNeeded",
|
||||||
|
|
@ -77,6 +80,8 @@ const storeActions = {
|
||||||
SAVE_PAYMENT_TYPE: "savePaymentType",
|
SAVE_PAYMENT_TYPE: "savePaymentType",
|
||||||
SAVE_PARENT_ACCOUNT_NUMBER: "saveParentAccountNumber",
|
SAVE_PARENT_ACCOUNT_NUMBER: "saveParentAccountNumber",
|
||||||
SAVE_SUPPORTING_ITEMS: "saveSupportingItems",
|
SAVE_SUPPORTING_ITEMS: "saveSupportingItems",
|
||||||
|
SAVE_SUPPORTING_ITEMS_SUPPRESSING_STATE_RESETTING:
|
||||||
|
"saveSupportingItemsSuppressingStateResetting",
|
||||||
SAVE_VAPS: "saveVaps",
|
SAVE_VAPS: "saveVaps",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,10 @@ const storeMutations = {
|
||||||
RESET_GLASS_PARTS_STATE: "resetGlassPartsState",
|
RESET_GLASS_PARTS_STATE: "resetGlassPartsState",
|
||||||
RESET_STATE: "resetState",
|
RESET_STATE: "resetState",
|
||||||
RESET_SAVE_SESSION_PROMISE: "resetSaveSessionPromise",
|
RESET_SAVE_SESSION_PROMISE: "resetSaveSessionPromise",
|
||||||
|
RESET_SERVICE_LOCATION_APPOINTMENT_TYPE: "resetServiceLocationAppointmentType",
|
||||||
|
RESET_SERVICE_LOCATION_PROVIDER: "resetServiceLocationProvider",
|
||||||
|
RESET_SERVICE_LOCATION_MOBILE_ADDRESS: "resetServiceLocationMobileAddress",
|
||||||
|
RESET_SCHEDULE: "resetSchedule",
|
||||||
|
|
||||||
// OTHER MUTATIONS
|
// OTHER MUTATIONS
|
||||||
UPDATE_PAGE_DATA: "updatePageData",
|
UPDATE_PAGE_DATA: "updatePageData",
|
||||||
|
|
|
||||||
|
|
@ -256,7 +256,9 @@ export default {
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
modelValue(newValue, oldValue) {
|
modelValue(newValue, oldValue) {
|
||||||
this.resetField();
|
this.resetField({
|
||||||
|
value: newValue,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
answers() {
|
answers() {
|
||||||
//once we get the answers to display from parent, see if we need a GA event to log what we showed
|
//once we get the answers to display from parent, see if we need a GA event to log what we showed
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -32,8 +31,8 @@
|
||||||
<div class="grid-item caption"><span class="sr-only">Saturday</span>S</div>
|
<div class="grid-item caption"><span class="sr-only">Saturday</span>S</div>
|
||||||
<div
|
<div
|
||||||
v-for="date in month.dates"
|
v-for="date in month.dates"
|
||||||
:key="date.inputValue.dateString"
|
:key="date.inputValue"
|
||||||
:id="date.inputValue.dateString"
|
:id="date.inputValue"
|
||||||
class="grid-item radio-wrapper"
|
class="grid-item radio-wrapper"
|
||||||
:class="[
|
:class="[
|
||||||
date.dateNum === 1 ? 'first-day-' + month.startDateDayIndex : '',
|
date.dateNum === 1 ? 'first-day-' + month.startDateDayIndex : '',
|
||||||
|
|
@ -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++) {
|
||||||
|
|
@ -197,18 +193,68 @@ export default {
|
||||||
weekEndDate: weekEndDate,
|
weekEndDate: weekEndDate,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// are any of these weeks split between two months?
|
||||||
|
const hasSplitWeek = (week) => {
|
||||||
|
return week.weekStartDate.getMonth() !== week.weekEndDate.getMonth() ? true : false;
|
||||||
|
};
|
||||||
|
const splitWeekIndex = weeks.findIndex(hasSplitWeek);
|
||||||
|
|
||||||
|
if (splitWeekIndex > -1) {
|
||||||
|
const week1 = [];
|
||||||
|
const week2 = [];
|
||||||
|
let switchToWeek2 = false;
|
||||||
|
|
||||||
|
for (let j = 0; j < 7; j++) {
|
||||||
|
const newDate = new Date(weeks[splitWeekIndex].weekStartDate);
|
||||||
|
newDate.setDate(newDate.getDate() + j);
|
||||||
|
if (newDate.getDate() === 1) switchToWeek2 = true;
|
||||||
|
if (switchToWeek2) {
|
||||||
|
week2.push(newDate);
|
||||||
|
} else {
|
||||||
|
week1.push(newDate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const week1EndDate = week1[week1.length - 1];
|
||||||
|
const week2StartDate = week2[0];
|
||||||
|
|
||||||
|
if (week1EndDate < today) {
|
||||||
|
// replace week 1 with week 2
|
||||||
|
weeks[splitWeekIndex].weekStartDate = week2StartDate;
|
||||||
|
} else {
|
||||||
|
const newWeek = {
|
||||||
|
weekNum: weeks[splitWeekIndex].weekNum,
|
||||||
|
weekStartDate: week2StartDate,
|
||||||
|
weekEndDate: weeks[splitWeekIndex].weekEndDate,
|
||||||
|
};
|
||||||
|
weeks[splitWeekIndex].weekEndDate = week1EndDate;
|
||||||
|
weeks.splice(splitWeekIndex + 1, 0, newWeek);
|
||||||
|
weeks.pop();
|
||||||
|
weeks.forEach((item, index) => {
|
||||||
|
if (index > splitWeekIndex) {
|
||||||
|
item.weekNum = item.weekNum + 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
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 currentMonthEnd = new Date(todayYearNum, todayMonthIndex, 0);
|
// let currentMonthStart = new Date(todayYearNum, todayMonthIndex - 1, 1);
|
||||||
|
const currentMonthEnd = new Date(todayYearNum, todayMonthIndex, 0);
|
||||||
|
|
||||||
let calendarViewDirection = "none";
|
let calendarViewDirection = "none";
|
||||||
if (config.selectableDatesSetting === "past") calendarViewDirection = "past";
|
if (config.selectableDatesSetting === "past") calendarViewDirection = "past";
|
||||||
|
|
@ -219,10 +265,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 firstSaturdayMonth = initialViewWeeks[0].weekEndDate.getMonth();
|
||||||
let sunday5month =
|
const lastSundayMonth =
|
||||||
initialViewWeeks[initialViewWeeks.length - 1].weekStartDate.getMonth();
|
initialViewWeeks[initialViewWeeks.length - 1].weekStartDate.getMonth();
|
||||||
|
|
||||||
let hideSomeDaysForInitialView = false;
|
let hideSomeDaysForInitialView = false;
|
||||||
|
|
@ -230,10 +276,10 @@ export default {
|
||||||
|
|
||||||
// TODO - NEED TO CREATE OPPOSITE LOGIC FOR "past" DIRECTION LIKE BELOW vvvvv
|
// TODO - NEED TO CREATE OPPOSITE LOGIC FOR "past" DIRECTION LIKE BELOW vvvvv
|
||||||
if (calendarViewDirection === "future") {
|
if (calendarViewDirection === "future") {
|
||||||
if (saturday1month !== sunday5month) {
|
if (firstSaturdayMonth !== lastSundayMonth) {
|
||||||
hideSomeDaysForInitialView = true;
|
hideSomeDaysForInitialView = true;
|
||||||
}
|
}
|
||||||
if (initialViewStartDate.getMonth() === sunday5month) {
|
if (initialViewStartDate.getMonth() === lastSundayMonth) {
|
||||||
hideSecondMonth = true;
|
hideSecondMonth = true;
|
||||||
if (currentMonthEnd > initialViewEndDate) {
|
if (currentMonthEnd > initialViewEndDate) {
|
||||||
// should part of 1st month be hidden?
|
// should part of 1st month be hidden?
|
||||||
|
|
@ -242,11 +288,12 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let myPromise = new Promise((resolve, reject) => {
|
const 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 +331,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 +352,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 +427,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,19 +469,13 @@ 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) +
|
||||||
"-" +
|
"-" +
|
||||||
forceTwoDigitString(i);
|
forceTwoDigitString(i);
|
||||||
|
|
||||||
const thisDate = {
|
|
||||||
year: yearNum,
|
|
||||||
month: monthIndex,
|
|
||||||
date: i,
|
|
||||||
dateString: dateString,
|
|
||||||
};
|
|
||||||
if (offset === 0 && i === this.todayDateNum) {
|
if (offset === 0 && i === this.todayDateNum) {
|
||||||
dayClasses += "current-day";
|
dayClasses += "current-day";
|
||||||
}
|
}
|
||||||
|
|
@ -457,11 +496,9 @@ export default {
|
||||||
const dateObject = {
|
const dateObject = {
|
||||||
dateNum: i,
|
dateNum: i,
|
||||||
dayClasses: dayClasses,
|
dayClasses: dayClasses,
|
||||||
inputValue: thisDate,
|
inputValue: dateString,
|
||||||
isSelectable:
|
isSelectable:
|
||||||
this.selectableDatesData.findIndex(
|
this.selectableDatesData.findIndex((date) => date.date === dateString) > -1
|
||||||
(date) => date.dateString === dateString
|
|
||||||
) > -1
|
|
||||||
? true
|
? true
|
||||||
: false,
|
: false,
|
||||||
};
|
};
|
||||||
|
|
@ -510,11 +547,11 @@ export default {
|
||||||
if (monthToShow) {
|
if (monthToShow) {
|
||||||
// make new API call with this month's start and end dates
|
// make new API call with this month's start and end dates
|
||||||
await this.updateSelectableDates(
|
await this.updateSelectableDates(
|
||||||
monthToShow.dates[monthStartDateNum].inputValue.dateString,
|
monthToShow.dates[monthStartDateNum].inputValue,
|
||||||
monthToShow.dates[monthToShow.dates.length - 1].inputValue.dateString
|
monthToShow.dates[monthToShow.dates.length - 1].inputValue
|
||||||
);
|
);
|
||||||
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,17 +563,18 @@ 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(
|
||||||
(obj) => obj.dateString === selectableDate.dateString
|
(dateObj) => dateObj.date === selectableDate.date
|
||||||
);
|
);
|
||||||
if (index === -1) this.selectableDatesData.push(selectableDate);
|
if (index === -1) this.selectableDatesData.push(selectableDate.date);
|
||||||
this.months.forEach((month) => {
|
this.months.forEach((month) => {
|
||||||
// TODO: avoid checking all calendar date; maybe only ones between monthStart and monthEnd as defined above?
|
// TODO: avoid checking all calendar dates; maybe only ones between monthStart and monthEnd as defined above?
|
||||||
month.dates.forEach((date) => {
|
month.dates.forEach((date) => {
|
||||||
if (date.inputValue.dateString === selectableDate.dateString) {
|
if (date.inputValue === selectableDate.date) {
|
||||||
date["isSelectable"] = true;
|
date["isSelectable"] = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -807,7 +845,6 @@ export default {
|
||||||
.btn-link {
|
.btn-link {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
text-underline-offset: 4px;
|
text-underline-offset: 4px;
|
||||||
box-shadow: none !important; // TODO: FIX THIS
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { shallowMount } from "@vue/test-utils";
|
import { shallowMount } from "@vue/test-utils";
|
||||||
import TextBlock from "./text-block";
|
import TextBlock from "./text-block";
|
||||||
|
|
||||||
describe("modal.vue", () => {
|
describe("text-block.vue", () => {
|
||||||
it("Should display 'Text' when 'Text' is defined in the CMS", async () => {
|
it("Should display 'Text' when 'Text' is defined in the CMS", async () => {
|
||||||
// Act
|
// Act
|
||||||
const wrapper = shallowMount(TextBlock, {
|
const wrapper = shallowMount(TextBlock, {
|
||||||
|
|
|
||||||
|
|
@ -1,47 +1,93 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div class="text-block w-100" :class="[justifyText, typeStyle, fontWeight, marginTopClass]">
|
||||||
class="text-block w-100"
|
<span v-for="copy in splitCopyOnCMSPlaceHolder(this.textBlockCopy)" :key="copy">
|
||||||
:class="[justifyText, typeStyle, fontWeight, margin]"
|
<span v-if="doesCopyContainRouterLink(copy)">
|
||||||
v-html="this.TextBlockCopy"></div>
|
<router-link
|
||||||
|
:to="{
|
||||||
|
query: { [pageQueryString]: `${getRouterLinkRouteFromCopy(copy)}` },
|
||||||
|
name: 'root',
|
||||||
|
}"
|
||||||
|
>{{ getRouterLinkDisplayTextFromCopy(copy) }}</router-link
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
<span v-else-if="doesCopyContainTextLink(copy)">
|
||||||
|
<textLink
|
||||||
|
linkType="text"
|
||||||
|
:text="getRouterLinkDisplayTextFromCopy(copy)"
|
||||||
|
href="#!"
|
||||||
|
@click-event="$emit('textLinkClicked', getRouterLinkRouteFromCopy(copy))"
|
||||||
|
:data-bs-target="'#' + getRouterLinkRouteFromCopy(copy)"
|
||||||
|
aria-label="Modal window" />
|
||||||
|
</span>
|
||||||
|
<span v-else v-html="copy"></span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
// Components
|
||||||
|
import textLink from "@/ux-components/text-link/text-link";
|
||||||
|
|
||||||
|
// Supporting files
|
||||||
|
import {
|
||||||
|
doesCopyContainRouterLink,
|
||||||
|
doesCopyContainTextLink,
|
||||||
|
splitCopyOnCMSPlaceHolder,
|
||||||
|
getRouterLinkRouteFromCopy,
|
||||||
|
getRouterLinkDisplayTextFromCopy,
|
||||||
|
} from "@/helpers/cms-content-helper";
|
||||||
|
import { applicationConfig } from "@/constants/application-config";
|
||||||
|
|
||||||
export default {
|
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)
|
||||||
margin: {
|
|
||||||
// bootstrap margin to apply to the block.
|
|
||||||
type: String,
|
|
||||||
default: "mt-2",
|
|
||||||
},
|
|
||||||
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,
|
||||||
|
marginTopSizeOverride: Number, // override mt-2 with a bootstrap size from 0-5 or auto
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
doesCopyContainRouterLink,
|
||||||
|
doesCopyContainTextLink,
|
||||||
|
splitCopyOnCMSPlaceHolder,
|
||||||
|
getRouterLinkRouteFromCopy,
|
||||||
|
getRouterLinkDisplayTextFromCopy,
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
TextBlockCopy() {
|
pageQueryString() {
|
||||||
|
return applicationConfig.PAGE_QUERYSTRING;
|
||||||
|
},
|
||||||
|
textBlockCopy() {
|
||||||
if (this.customText) {
|
if (this.customText) {
|
||||||
return this.customText;
|
return this.customText;
|
||||||
}
|
}
|
||||||
return this.getCmsContent(this.cmsWidgetName, "Text");
|
return this.getCmsContent(this.cmsWidgetName, "Text");
|
||||||
},
|
},
|
||||||
|
marginTopClass() {
|
||||||
|
if (this.marginTopSizeOverride === "auto") {
|
||||||
|
return "mt-auto";
|
||||||
|
}
|
||||||
|
if (this.marginTopSizeOverride >= 0 && this.marginTopSizeOverride <= 5) {
|
||||||
|
return "mt-" + this.marginTopSizeOverride;
|
||||||
|
}
|
||||||
|
return "mt-2";
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
textLink,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<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;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { storeActions } from "@/constants/store-actions.js";
|
import { storeActions } from "@/constants/store-actions.js";
|
||||||
import store from "@/store";
|
import store from "@/store";
|
||||||
import { dynamicStrings } from "../constants/dynamic-strings";
|
import { dynamicStrings } from "@/constants/dynamic-strings";
|
||||||
|
|
||||||
export function fetchCmsContentForPage(fmgPage) {
|
export function fetchCmsContentForPage(fmgPage) {
|
||||||
return store.dispatch(storeActions.GET_PAGE_DATA, { pageName: fmgPage }).then((response) => {
|
return store.dispatch(storeActions.GET_PAGE_DATA, { pageName: fmgPage }).then((response) => {
|
||||||
|
|
@ -284,7 +284,7 @@ function getIfStatementRegexExpression() {
|
||||||
//////////////////////////////////////////
|
//////////////////////////////////////////
|
||||||
|
|
||||||
export function doesCopyContainRouterLink(copy) {
|
export function doesCopyContainRouterLink(copy) {
|
||||||
return copy.includes(this.dynamicStrings.ROUTER_LINK);
|
return copy.includes(dynamicStrings.ROUTER_LINK);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function doesCopyContainTextLink(copy) {
|
export function doesCopyContainTextLink(copy) {
|
||||||
|
|
|
||||||
81
src/helpers/object-helper.js
Normal file
81
src/helpers/object-helper.js
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
// For nested objects, spread operator only creates new references to the top level fields,
|
||||||
|
// the remaining nested fields actually reference the original object which can introduce problems.
|
||||||
|
|
||||||
|
// The purpose of this method is to deep clone the data in an object recursively, this is useful
|
||||||
|
// for cloning modelValues to internal models when regular two-way binding is not an option.
|
||||||
|
// See: mobile-location-modal-questions.vue
|
||||||
|
|
||||||
|
// Creates a deep clone of an object. Clones primitives, arrays and objects, excluding class instances.
|
||||||
|
// https://www.30secondsofcode.org/js/s/deep-clone
|
||||||
|
export function deepClone(object) {
|
||||||
|
if (object === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let clone = Object.assign({}, object);
|
||||||
|
Object.keys(clone).forEach(
|
||||||
|
(key) =>
|
||||||
|
(clone[key] = typeof object[key] === "object" ? deepClone(object[key]) : object[key])
|
||||||
|
);
|
||||||
|
|
||||||
|
if (Array.isArray(object)) {
|
||||||
|
clone.length = object.length;
|
||||||
|
return Array.from(clone);
|
||||||
|
}
|
||||||
|
|
||||||
|
return clone;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The purpose of this method is to check for array or object equality recursively to determine if two complex objects are equal.
|
||||||
|
// This is only a comparison of data, not functions.
|
||||||
|
export function deepEqual(obj1, obj2) {
|
||||||
|
if (typeof obj1 !== typeof obj2) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj1 === null || obj2 === null) {
|
||||||
|
return obj1 === obj2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(obj1) && Array.isArray(obj2)) {
|
||||||
|
if (obj1.length !== obj2.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sorted1 = obj1.slice().sort();
|
||||||
|
const sorted2 = obj2.slice().sort();
|
||||||
|
|
||||||
|
for (let i = 0; i < sorted1.length; i++) {
|
||||||
|
if (!deepEqual(sorted1[i], sorted2[i])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof obj1 === "object" && typeof obj2 === "object") {
|
||||||
|
const keys1 = Object.keys(obj1);
|
||||||
|
const keys2 = Object.keys(obj2);
|
||||||
|
|
||||||
|
if (keys1.length !== keys2.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortedKeys1 = keys1.sort();
|
||||||
|
const sortedKeys2 = keys2.sort();
|
||||||
|
|
||||||
|
for (let i = 0; i < sortedKeys1.length; i++) {
|
||||||
|
const key1 = sortedKeys1[i];
|
||||||
|
const key2 = sortedKeys2[i];
|
||||||
|
|
||||||
|
if (key1 !== key2 || !deepEqual(obj1[key1], obj2[key2])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj1 === obj2;
|
||||||
|
}
|
||||||
275
src/helpers/object-helper.spec.js
Normal file
275
src/helpers/object-helper.spec.js
Normal file
|
|
@ -0,0 +1,275 @@
|
||||||
|
import { deepClone, deepEqual } from "./object-helper";
|
||||||
|
|
||||||
|
describe("object-cloning-helper.js", () => {
|
||||||
|
describe("deepClone", () => {
|
||||||
|
it("Should return null if no object is passed in", async () => {
|
||||||
|
// Arrange
|
||||||
|
const expected = null;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = deepClone(null);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should return a deep copy of the object", async () => {
|
||||||
|
// Arrange
|
||||||
|
|
||||||
|
const object = {
|
||||||
|
addressQuestions: {
|
||||||
|
streetAddress: "555 Some St",
|
||||||
|
apartmentNumberOrBusinessName: "Apt 1",
|
||||||
|
city: "Funkytown",
|
||||||
|
state: "OH",
|
||||||
|
zipCode: "55555",
|
||||||
|
},
|
||||||
|
isVehicleProtected: true,
|
||||||
|
serviceZipCode: "55555",
|
||||||
|
};
|
||||||
|
|
||||||
|
const expected = {
|
||||||
|
addressQuestions: {
|
||||||
|
streetAddress: "555 Some St",
|
||||||
|
apartmentNumberOrBusinessName: "Apt 1",
|
||||||
|
city: "Funkytown",
|
||||||
|
state: "OH",
|
||||||
|
zipCode: "55555",
|
||||||
|
},
|
||||||
|
isVehicleProtected: true,
|
||||||
|
serviceZipCode: "55555",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = deepClone(object);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toStrictEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should return a copy of the array", async () => {
|
||||||
|
// Arrange
|
||||||
|
|
||||||
|
const array = [9, 8, 7, 6, 5, 4, 3, 2, 1];
|
||||||
|
|
||||||
|
const expected = [9, 8, 7, 6, 5, 4, 3, 2, 1];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = deepClone(array);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toStrictEqual(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("deepEqual", () => {
|
||||||
|
it("Should return false if the items being compared are not the same type", async () => {
|
||||||
|
// Arrange
|
||||||
|
const obj1 = ""; // String
|
||||||
|
const obj2 = 3; // Number
|
||||||
|
const expected = false;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = deepEqual(obj1, obj2);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should return false if one the items being compared is null", async () => {
|
||||||
|
// Arrange
|
||||||
|
const obj1 = {}; // Object
|
||||||
|
const obj2 = null; // Array
|
||||||
|
const expected = false;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = deepEqual(obj1, obj2);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Both items being compared are arrays", () => {
|
||||||
|
it("Should return true if the two arrays have the same elements in the same order", async () => {
|
||||||
|
// Arrange
|
||||||
|
const obj1 = [1, 2, 3]; // Array
|
||||||
|
const obj2 = [1, 2, 3]; // Array
|
||||||
|
const expected = true;
|
||||||
|
|
||||||
|
// Acts
|
||||||
|
const result = deepEqual(obj1, obj2);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should return true if the two arrays have the same elements in a different order", async () => {
|
||||||
|
// Arrange
|
||||||
|
const obj1 = [1, 2, 3]; // Array
|
||||||
|
const obj2 = [3, 1, 2]; // Array
|
||||||
|
const expected = true;
|
||||||
|
|
||||||
|
// Acts
|
||||||
|
const result = deepEqual(obj1, obj2);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should return false if the two arrays are not the same length", async () => {
|
||||||
|
// Arrange
|
||||||
|
const obj1 = [1, 2, 3]; // Array
|
||||||
|
const obj2 = [1, 2]; // Array
|
||||||
|
const expected = false;
|
||||||
|
|
||||||
|
// Acts
|
||||||
|
const result = deepEqual(obj1, obj2);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should return false if they have the same elements but their the array elements are different", async () => {
|
||||||
|
// Arrange
|
||||||
|
const obj1 = [1, 2, 3]; // Array
|
||||||
|
const obj2 = [4, 5, 6]; // Array
|
||||||
|
const expected = false;
|
||||||
|
|
||||||
|
// Acts
|
||||||
|
const result = deepEqual(obj1, obj2);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Both items being compared are objects", () => {
|
||||||
|
it("Should return false if the two objects *do not* have the same number of keys", async () => {
|
||||||
|
// Arrange
|
||||||
|
const obj1 = {
|
||||||
|
prop1: {},
|
||||||
|
}; // Object
|
||||||
|
|
||||||
|
const obj2 = {
|
||||||
|
prop1: {},
|
||||||
|
prop2: {},
|
||||||
|
}; // Object
|
||||||
|
const expected = false;
|
||||||
|
|
||||||
|
// Acts
|
||||||
|
const result = deepEqual(obj1, obj2);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should return true if the two objects have the same keys in the same order", async () => {
|
||||||
|
// Arrange
|
||||||
|
const obj1 = {
|
||||||
|
prop1: {},
|
||||||
|
prop2: {},
|
||||||
|
}; // Object
|
||||||
|
|
||||||
|
const obj2 = {
|
||||||
|
prop1: {},
|
||||||
|
prop2: {},
|
||||||
|
}; // Object
|
||||||
|
|
||||||
|
const expected = true;
|
||||||
|
|
||||||
|
// Acts
|
||||||
|
const result = deepEqual(obj1, obj2);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should return true if the two objects have the same keys in a different order", async () => {
|
||||||
|
// Arrange
|
||||||
|
const obj1 = {
|
||||||
|
prop1: {},
|
||||||
|
prop2: {},
|
||||||
|
}; // Object
|
||||||
|
|
||||||
|
const obj2 = {
|
||||||
|
prop2: {},
|
||||||
|
prop1: {},
|
||||||
|
}; // Object
|
||||||
|
|
||||||
|
const expected = true;
|
||||||
|
|
||||||
|
// Acts
|
||||||
|
const result = deepEqual(obj1, obj2);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should return false if the two objects have the same keys but in a nested object comparison one of the two values is null", async () => {
|
||||||
|
// Arrange
|
||||||
|
const obj1 = {
|
||||||
|
prop1: {
|
||||||
|
subProp1: {
|
||||||
|
foo: "",
|
||||||
|
bar: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
prop2: {
|
||||||
|
subProp1: [1, 2, 3],
|
||||||
|
},
|
||||||
|
}; // Object
|
||||||
|
|
||||||
|
const obj2 = {
|
||||||
|
prop1: {},
|
||||||
|
prop2: {},
|
||||||
|
}; // Object
|
||||||
|
|
||||||
|
const expected = false;
|
||||||
|
|
||||||
|
// Acts
|
||||||
|
const result = deepEqual(obj1, obj2);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should return false if the two objects have the same elements but a nested object comparison is of two different types", async () => {
|
||||||
|
// Arrange
|
||||||
|
const obj1 = {
|
||||||
|
prop1: {
|
||||||
|
subProp1: {
|
||||||
|
foo: "",
|
||||||
|
bar: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
prop2: {
|
||||||
|
subProp1: [1, 2, 3],
|
||||||
|
},
|
||||||
|
}; // Object
|
||||||
|
|
||||||
|
const obj2 = {
|
||||||
|
prop1: {
|
||||||
|
subProp1: {
|
||||||
|
foo: "",
|
||||||
|
bar: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
prop2: {
|
||||||
|
subProp1: {
|
||||||
|
foo1: "",
|
||||||
|
bar1: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}; // Object
|
||||||
|
|
||||||
|
const expected = false;
|
||||||
|
|
||||||
|
// Acts
|
||||||
|
const result = deepEqual(obj1, obj2);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -20,19 +20,12 @@
|
||||||
validationRules="vehicle-required"
|
validationRules="vehicle-required"
|
||||||
v-model="selectedVehicleVin"
|
v-model="selectedVehicleVin"
|
||||||
:isCarIdDifferent="isCarIdDifferent" />
|
:isCarIdDifferent="isCarIdDifferent" />
|
||||||
<div class="alert-provide-vin my-3" v-if="splitAlertProvideVinBodyForLink.length">
|
<div class="alert-provide-vin" v-if="splitAlertProvideVinBodyForLink.length">
|
||||||
<span v-for="copy in splitAlertProvideVinBodyForLink" :key="copy">
|
<textBlock
|
||||||
<span v-if="doesCopyContainRouterLink(copy)" class="text-body">
|
:customText="AlertProvideVinBody"
|
||||||
<router-link
|
justifyText="left"
|
||||||
:to="{
|
class="mb-3"
|
||||||
query: { fmgPage: `${getRouterLinkRouteFromCopy(copy)}` },
|
marginTopSizeOverride="3" />
|
||||||
name: 'root',
|
|
||||||
}"
|
|
||||||
>{{ getRouterLinkDisplayTextFromCopy(copy) }}</router-link
|
|
||||||
>
|
|
||||||
</span>
|
|
||||||
<span v-else class="m-0 text-body" v-html="copy"></span>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<funnelFooter
|
<funnelFooter
|
||||||
cmsWidgetName="FunnelFooterWidget"
|
cmsWidgetName="FunnelFooterWidget"
|
||||||
|
|
@ -52,6 +45,7 @@ import vehicleBanner from "@/fmg-components/vehicle-banner/vehicle-banner";
|
||||||
import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header";
|
import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header";
|
||||||
import alert from "@/ux-components/alert/alert";
|
import alert from "@/ux-components/alert/alert";
|
||||||
import addressVehiclesQuestion from "@/layouts/address-vehicles/address-vehicles-question/address-vehicles-question";
|
import addressVehiclesQuestion from "@/layouts/address-vehicles/address-vehicles-question/address-vehicles-question";
|
||||||
|
import textBlock from "@/digital-components/text-block/text-block";
|
||||||
|
|
||||||
// Supporting files
|
// Supporting files
|
||||||
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
|
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
|
||||||
|
|
@ -237,6 +231,7 @@ export default {
|
||||||
alert,
|
alert,
|
||||||
funnelFooter,
|
funnelFooter,
|
||||||
addressVehiclesQuestion,
|
addressVehiclesQuestion,
|
||||||
|
textBlock,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -229,7 +229,7 @@ export default {
|
||||||
const zipCodeData = await this.getZipCodeData(this.serviceZipCode);
|
const zipCodeData = await this.getZipCodeData(this.serviceZipCode);
|
||||||
await this.dispatchStoreAction(storeActions.SAVE_EMAIL, this.emailAddress, false);
|
await this.dispatchStoreAction(storeActions.SAVE_EMAIL, this.emailAddress, false);
|
||||||
await this.dispatchStoreAction(
|
await this.dispatchStoreAction(
|
||||||
storeActions.SAVE_SERVICE_LOCATION,
|
storeActions.SAVE_SERVICE_ZIP_CODE_INFO,
|
||||||
{
|
{
|
||||||
zipCode: this.serviceZipCode,
|
zipCode: this.serviceZipCode,
|
||||||
state: zipCodeData.state,
|
state: zipCodeData.state,
|
||||||
|
|
|
||||||
|
|
@ -109,7 +109,7 @@ store.getters = {
|
||||||
damage: baseStoreGettersDamage,
|
damage: baseStoreGettersDamage,
|
||||||
order: {},
|
order: {},
|
||||||
};
|
};
|
||||||
store.commit = jest.fn();
|
store.dispatch = jest.fn();
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
// reset store after each test
|
// reset store after each test
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -182,6 +182,7 @@ export default {
|
||||||
this.isInsuranceSelected,
|
this.isInsuranceSelected,
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!this.isInsuranceSelected) {
|
if (!this.isInsuranceSelected) {
|
||||||
this.dispatchStoreAction(
|
this.dispatchStoreAction(
|
||||||
this.storeActions.SAVE_PARENT_ACCOUNT_NUMBER,
|
this.storeActions.SAVE_PARENT_ACCOUNT_NUMBER,
|
||||||
|
|
@ -196,18 +197,21 @@ export default {
|
||||||
) {
|
) {
|
||||||
this.supportingItems = this.filterOutFees(this.supportingItems);
|
this.supportingItems = this.filterOutFees(this.supportingItems);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.pricedGlassParts.length > 0) {
|
if (this.pricedGlassParts.length > 0) {
|
||||||
this.dispatchStoreAction(
|
this.dispatchStoreAction(
|
||||||
this.storeActions.SAVE_GLASS_PARTS,
|
this.storeActions.SAVE_GLASS_PART_PRICES,
|
||||||
this.pricedGlassParts,
|
this.pricedGlassParts,
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.dispatchStoreAction(
|
this.dispatchStoreAction(
|
||||||
this.storeActions.SAVE_SUPPORTING_ITEMS,
|
this.storeActions.SAVE_SUPPORTING_ITEMS,
|
||||||
this.supportingItems,
|
this.supportingItems,
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
|
|
||||||
this.dispatchStoreAction(this.storeActions.SAVE_VAPS, this.selectedVaps, false);
|
this.dispatchStoreAction(this.storeActions.SAVE_VAPS, this.selectedVaps, false);
|
||||||
|
|
||||||
const payment = this.$store.getters.payment;
|
const payment = this.$store.getters.payment;
|
||||||
|
|
|
||||||
|
|
@ -36,8 +36,12 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
loadInitialData(zipCodeCtu) {
|
loadInitialData(serviceLocationCtu, providerCtu) {
|
||||||
return getAlertReasons(zipCodeCtu);
|
let ctuToUse = serviceLocationCtu;
|
||||||
|
if (providerCtu) {
|
||||||
|
ctuToUse = providerCtu;
|
||||||
|
}
|
||||||
|
return getAlertReasons(ctuToUse);
|
||||||
},
|
},
|
||||||
initializeComponent(initialData) {
|
initializeComponent(initialData) {
|
||||||
this.alertReasons = initialData;
|
this.alertReasons = initialData;
|
||||||
|
|
|
||||||
|
|
@ -4,20 +4,13 @@
|
||||||
<loadingModal ref="loadingModal" />
|
<loadingModal ref="loadingModal" />
|
||||||
<funnelHeader cmsWidgetName="FunnelHeaderWidget" />
|
<funnelHeader cmsWidgetName="FunnelHeaderWidget" />
|
||||||
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" class="mt-5" />
|
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" class="mt-5" />
|
||||||
<div class="text-center mt-1 mb-3" v-if="ChangeShopLink.length">
|
<template v-if="ChangeShopLink.length">
|
||||||
<span v-for="copy in ChangeShopLink" :key="copy">
|
<textBlock
|
||||||
<span v-if="doesCopyContainRouterLink(copy)" class="text-body">
|
cmsWidgetName="ChangeShopLink"
|
||||||
<router-link
|
justifyText="center"
|
||||||
:to="{
|
class="mb-3"
|
||||||
query: { fmgPage: `${getRouterLinkRouteFromCopy(copy)}` },
|
marginTopSizeOverride="1" />
|
||||||
name: 'root',
|
</template>
|
||||||
}"
|
|
||||||
>{{ getRouterLinkDisplayTextFromCopy(copy) }}</router-link
|
|
||||||
>
|
|
||||||
</span>
|
|
||||||
<span v-else class="m-0 text-body" v-html="copy"></span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<location-alerts cmsWidgetPrefix="LocationAlert-" ref="locationAlerts" />
|
<location-alerts cmsWidgetPrefix="LocationAlert-" ref="locationAlerts" />
|
||||||
<date-picker
|
<date-picker
|
||||||
selectableDatesSetting="custom"
|
selectableDatesSetting="custom"
|
||||||
|
|
@ -25,17 +18,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="MobilePremiumTimeSlotModal"
|
||||||
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"
|
||||||
|
|
@ -60,6 +53,7 @@ import { Form, defineRule } from "vee-validate";
|
||||||
import datePicker from "@/digital-components/date-picker/date-picker";
|
import datePicker from "@/digital-components/date-picker/date-picker";
|
||||||
import locationAlerts from "@/layouts/schedule/location-alerts/location-alerts";
|
import locationAlerts from "@/layouts/schedule/location-alerts/location-alerts";
|
||||||
import timeSlotModalQuestion from "./time-slot-modal-question/time-slot-modal-question";
|
import timeSlotModalQuestion from "./time-slot-modal-question/time-slot-modal-question";
|
||||||
|
import textBlock from "@/digital-components/text-block/text-block";
|
||||||
|
|
||||||
// Supporting files
|
// Supporting files
|
||||||
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
|
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
|
||||||
|
|
@ -67,12 +61,8 @@ import baseMixin from "@/mixins/base-mixin.js";
|
||||||
import { storeActions } from "@/constants/store-actions";
|
import { storeActions } from "@/constants/store-actions";
|
||||||
import { settleAllPromises } from "@/helpers/layout-helper";
|
import { settleAllPromises } from "@/helpers/layout-helper";
|
||||||
import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
|
import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
|
||||||
import {
|
import { splitCopyOnCMSPlaceHolder } from "@/helpers/cms-content-helper";
|
||||||
doesCopyContainRouterLink,
|
import { AppointmentTypeStrings, PREMIUM_FEE_PART_TYPE } from "@/constants/schedule-constants";
|
||||||
splitCopyOnCMSPlaceHolder,
|
|
||||||
getRouterLinkRouteFromCopy,
|
|
||||||
getRouterLinkDisplayTextFromCopy,
|
|
||||||
} from "@/helpers/cms-content-helper";
|
|
||||||
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,41 +70,46 @@ 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
|
||||||
return convertApiResponse(newShopTimeSlots);
|
);
|
||||||
};
|
} else {
|
||||||
const convertApiResponse = (responseData) => {
|
newTimeSlotsResponse = await baseMixin.methods.dispatchStoreAction(
|
||||||
// DATA CONVERSION
|
storeActions.GET_SHOP_TIME_SLOTS,
|
||||||
responseData?.days.forEach((date) => {
|
{
|
||||||
const dateString = date.date;
|
startDate: startDate,
|
||||||
date.dateString = dateString;
|
endDate: endDate,
|
||||||
const dateStringPieces = dateString.split("-");
|
shopAppointmentType: appointmentType,
|
||||||
date.year = Number(dateStringPieces[0]);
|
providerNumber: providerNumber,
|
||||||
date.month = Number(dateStringPieces[1]);
|
},
|
||||||
date.date = Number(dateStringPieces[2]);
|
false
|
||||||
});
|
);
|
||||||
return responseData;
|
}
|
||||||
|
|
||||||
|
return newTimeSlotsResponse.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "schedule",
|
name: "schedule",
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
selectedDate: null,
|
selectedDate: this.getSelectedDate(),
|
||||||
selectedTimeSlotId: null,
|
selectedTimeSlotData: {
|
||||||
|
id: this.getSelectedRouteCode(),
|
||||||
|
isPremiumAppointment: null,
|
||||||
|
},
|
||||||
selectableDatesData: [],
|
selectableDatesData: [],
|
||||||
mobileEarlyBirdFee: null,
|
mobilePremiumAppointmentFee: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
async beforeRouteEnter(to, from, next) {
|
async beforeRouteEnter(to, from, next) {
|
||||||
|
|
@ -125,40 +120,30 @@ 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
|
const premiumFeePromise = baseMixin.methods.dispatchStoreAction(
|
||||||
const pricingPromise = baseMixin.methods.dispatchStoreAction(
|
storeActions.GET_MOBILE_PREMIUM_FEE
|
||||||
storeActions.PRICE_ORDER_ITEMS_AND_SAVE_SERVER_DATA,
|
|
||||||
{
|
|
||||||
availableLineItems: [{ partNumber: "EARLY BIRD" }],
|
|
||||||
},
|
|
||||||
false
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const earlyBirdPromise = baseMixin.methods.dispatchStoreAction(
|
const premiumFeeWithPricePromise = premiumFeePromise.then((result) => {
|
||||||
storeActions.GET_MOBILE_EARLY_BIRD_FEE
|
if (result.data) {
|
||||||
);
|
return baseMixin.methods.dispatchStoreAction(
|
||||||
|
storeActions.PRICE_ORDER_ITEMS_AND_SAVE_SERVER_DATA,
|
||||||
|
{
|
||||||
|
availableLineItems: [result.data],
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return result.data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const alertReasonsPromise = locationAlerts.methods.loadInitialData(
|
const alertReasonsPromise = locationAlerts.methods.loadInitialData(
|
||||||
store.getters.order.serviceLocation.zipCodeCtu
|
store.getters.order.serviceLocation.zipCodeCtu,
|
||||||
|
store.getters.order.serviceLocation.provider?.address?.zipCtu
|
||||||
);
|
);
|
||||||
|
|
||||||
// Settle promises and get results
|
// Settle promises and get results
|
||||||
const promiseResultMap = [
|
const promiseResultMap = [
|
||||||
{
|
{
|
||||||
|
|
@ -174,12 +159,8 @@ export default {
|
||||||
promise: datePickerInitialDataPromise,
|
promise: datePickerInitialDataPromise,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
resultKey: "earlyBird",
|
resultKey: "premiumFeeWithPrice",
|
||||||
promise: earlyBirdPromise,
|
promise: premiumFeeWithPricePromise,
|
||||||
},
|
|
||||||
{
|
|
||||||
resultKey: "pricingResults",
|
|
||||||
promise: pricingPromise,
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -191,9 +172,10 @@ 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) {
|
vm.mobilePremiumAppointmentFee = resultMap.premiumFeeWithPrice
|
||||||
vm.mobileEarlyBirdFee = resultMap.pricingResults[0];
|
? resultMap.premiumFeeWithPrice[0]
|
||||||
}
|
: null;
|
||||||
|
vm.updateFooterButtonText(vm.selectedTimeSlotData);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
@ -208,26 +190,28 @@ export default {
|
||||||
return this.$store.getters.order.serviceLocation.appointmentType;
|
return this.$store.getters.order.serviceLocation.appointmentType;
|
||||||
},
|
},
|
||||||
timeSlotsForSelectedDate() {
|
timeSlotsForSelectedDate() {
|
||||||
if (this.selectedDate === null) {
|
if (!this.selectedDate) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return this.selectableDatesData.days.find(
|
return this.selectableDatesData.days?.find(
|
||||||
(selectableDate) => selectableDate.dateString === this.selectedDate.dateString
|
(selectableDate) => selectableDate.date === this.selectedDate
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
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,
|
||||||
startTime: timeSlotSelectedObject.startTime,
|
startTime: timeSlotSelectedObject.startTime,
|
||||||
endTime: timeSlotSelectedObject.endTime,
|
endTime: timeSlotSelectedObject.endTime,
|
||||||
id: this.selectedTimeSlotId,
|
routeCode: this.selectedTimeSlotData.id,
|
||||||
|
jobMaxMinutes:
|
||||||
|
this.selectableDatesData.estimatedServiceMinutesMaximum.toString(),
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -235,19 +219,29 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
doesCopyContainRouterLink,
|
|
||||||
splitCopyOnCMSPlaceHolder,
|
splitCopyOnCMSPlaceHolder,
|
||||||
getRouterLinkRouteFromCopy,
|
|
||||||
getRouterLinkDisplayTextFromCopy,
|
|
||||||
arePagePrerequisitesValid() {
|
arePagePrerequisitesValid() {
|
||||||
return true;
|
const serviceLocation = store.getters.order.serviceLocation;
|
||||||
// NEED TODO - WHAT ARE PAGE REQ'S FOR THIS PAGE?
|
const serviceLocationPreReqs =
|
||||||
|
serviceLocation.zipCode &&
|
||||||
|
serviceLocation.zipCodeCtu &&
|
||||||
|
serviceLocation.appointmentType &&
|
||||||
|
(serviceLocation.appointmentType === AppointmentTypeStrings.MOBILE ||
|
||||||
|
serviceLocation.provider.providerNumber);
|
||||||
|
const paymentInfo = store.getters.payment.isInsurance !== null;
|
||||||
|
const supportingItems = store.getters.lineItems.supportingItems !== null;
|
||||||
|
const damageInfo =
|
||||||
|
store.getters.order.damage.isRepair ||
|
||||||
|
(store.getters.order.lineItems?.glassParts != null &&
|
||||||
|
store.getters.order.lineItems.glassParts.length > 0);
|
||||||
|
return serviceLocationPreReqs && paymentInfo && supportingItems && damageInfo;
|
||||||
},
|
},
|
||||||
async getAvailableDatesMethod(startDate, endDate) {
|
async getAvailableDatesMethod(startDate, endDate) {
|
||||||
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(
|
||||||
|
|
@ -263,42 +257,145 @@ export default {
|
||||||
},
|
},
|
||||||
getTimeSlotObjectFromTimeSlotId(timeSlotId) {
|
getTimeSlotObjectFromTimeSlotId(timeSlotId) {
|
||||||
const timeSlots = this.selectableDatesData.days.find(
|
const timeSlots = this.selectableDatesData.days.find(
|
||||||
(selectableDate) => selectableDate.dateString === this.selectedDate.dateString
|
(selectableDate) => selectableDate.date === this.selectedDate
|
||||||
).timeSlots;
|
).timeSlots;
|
||||||
return timeSlots.find((timeSlot) => timeSlot.id === timeSlotId);
|
return timeSlots.find((timeSlot) => timeSlot.id === timeSlotId);
|
||||||
},
|
},
|
||||||
|
getSelectedDate() {
|
||||||
|
return store.getters.order.schedule.date;
|
||||||
|
},
|
||||||
|
getSelectedRouteCode() {
|
||||||
|
return store.getters.order.schedule.routeCode;
|
||||||
|
},
|
||||||
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}T00:00:00`);
|
||||||
|
// Ex: April 25
|
||||||
|
return dateObject.toLocaleDateString("en-us", { month: "short", day: "numeric" });
|
||||||
|
},
|
||||||
|
// Expected input: "HH:MM"
|
||||||
|
getDisplayTextForMilitaryTime(militaryTimeInput, shouldTrimMinutesIfEmpty = false) {
|
||||||
|
let hours = parseInt(militaryTimeInput.split(":")[0]);
|
||||||
|
const minutes = militaryTimeInput.split(":")[1];
|
||||||
|
const 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);
|
||||||
},
|
},
|
||||||
async forwardButtonAction() {
|
forwardButtonAction() {
|
||||||
//TODO: replace properties with real values once they are available
|
this.updateSupportingItems();
|
||||||
await this.dispatchStoreAction(
|
this.dispatchStoreAction(
|
||||||
this.storeActions.SAVE_SCHEDULE,
|
this.storeActions.SAVE_SCHEDULE,
|
||||||
{
|
this.appointmentDateAndTime,
|
||||||
date: "2023-07-04T00:00:00",
|
|
||||||
startTime: "2023-07-04T12:00:00",
|
|
||||||
endTime: "2023-07-04T17:00:00",
|
|
||||||
routeCode: "03341-01820-S-B*20232*11 AM",
|
|
||||||
},
|
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
|
|
||||||
navigateToHeritageFunnel({ loadingModal: this.$refs.loadingModal });
|
navigateToHeritageFunnel({ loadingModal: this.$refs.loadingModal });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
updateSupportingItems() {
|
||||||
|
const supportingItems = store.getters.lineItems.supportingItems;
|
||||||
|
|
||||||
|
// if we have a premium fee(early bird), then save/update supporting items
|
||||||
|
if (
|
||||||
|
this.appointmentType === AppointmentTypeStrings.MOBILE &&
|
||||||
|
this.selectedTimeSlotData?.isPremiumAppointment
|
||||||
|
) {
|
||||||
|
const earlyBirdIndex = supportingItems.findIndex(
|
||||||
|
(item) => item.partType == PREMIUM_FEE_PART_TYPE
|
||||||
|
);
|
||||||
|
|
||||||
|
if (earlyBirdIndex >= 0) {
|
||||||
|
supportingItems[earlyBirdIndex].laborAmount =
|
||||||
|
this.mobilePremiumAppointmentFee.laborAmount;
|
||||||
|
supportingItems[earlyBirdIndex].selingPrice =
|
||||||
|
this.mobilePremiumAppointmentFee.selingPrice;
|
||||||
|
supportingItems[earlyBirdIndex].kitPrice =
|
||||||
|
this.mobilePremiumAppointmentFee.kitPrice;
|
||||||
|
} else {
|
||||||
|
supportingItems.push(this.mobilePremiumAppointmentFee);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dispatchStoreAction(
|
||||||
|
this.storeActions.SAVE_SUPPORTING_ITEMS_SUPPRESSING_STATE_RESETTING,
|
||||||
|
supportingItems,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// if it's not a mobile and/or premium early bird, then make sure we remove any that may have been added
|
||||||
|
const removeEarlyBirdIndex = supportingItems.findIndex(
|
||||||
|
(item) => item.partType == PREMIUM_FEE_PART_TYPE
|
||||||
|
);
|
||||||
|
|
||||||
|
if (removeEarlyBirdIndex >= 0) {
|
||||||
|
supportingItems.splice(removeEarlyBirdIndex, 1);
|
||||||
|
this.dispatchStoreAction(
|
||||||
|
this.storeActions.SAVE_SUPPORTING_ITEMS_SUPPRESSING_STATE_RESETTING,
|
||||||
|
supportingItems,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
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(newValue) {
|
||||||
|
this.updateFooterButtonText(this.selectedTimeSlotData);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
funnelHeader,
|
funnelHeader,
|
||||||
|
|
@ -309,6 +406,7 @@ export default {
|
||||||
datePicker,
|
datePicker,
|
||||||
locationAlerts,
|
locationAlerts,
|
||||||
timeSlotModalQuestion,
|
timeSlotModalQuestion,
|
||||||
|
textBlock,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -14,17 +14,17 @@
|
||||||
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" />
|
||||||
<div
|
<div
|
||||||
class="mt-1 mb-2"
|
class="mt-1 mb-2 supplemental-information"
|
||||||
v-if="supplementalInformationBlock"
|
v-if="supplementalInformationBlock"
|
||||||
v-html="supplementalInformationBlock"></div>
|
v-html="supplementalInformationBlock"></div>
|
||||||
<textBlock
|
<textBlock
|
||||||
|
|
@ -41,23 +41,24 @@
|
||||||
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 { defineRule, useField } from "vee-validate";
|
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,66 +66,67 @@ 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: this.modelValue.id,
|
||||||
timeslotModalListButton: timeslotModalListButton,
|
timeSlotModalListButton: timeSlotModalListButton,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
setup(props) {
|
setup(props) {
|
||||||
const { handleChange } = useField("time-slot-modal-question", props.validationRules);
|
const { handleChange } = useField("time-slot-modal-question", props.validationRules);
|
||||||
|
// Run validation on component load
|
||||||
|
handleChange(props.modelValue.id);
|
||||||
return {
|
return {
|
||||||
handleChange,
|
handleChange,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
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);
|
if (this.modelValue.isPremiumAppointment) {
|
||||||
|
this.selectedTimeSlotId = this.addPremiumFlagToInput(this.modelValue.id);
|
||||||
|
} else {
|
||||||
|
this.selectedTimeSlotId = this.modelValue.id;
|
||||||
|
}
|
||||||
|
this.handleChange(this.modelValue.id);
|
||||||
|
},
|
||||||
|
availableTimeSlots(newValue) {
|
||||||
|
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.selectedTimeSlotId?.includes(
|
||||||
this.selectedTimeSlot === this.earlyBirdButtonText
|
PREMIUM_TIME_SLOT_ID_FLAG
|
||||||
? this.earlyBirdCmsWidgetName
|
)
|
||||||
: this.mobileCmsWidgetName;
|
? this.mobilePremiumCmsWidgetName
|
||||||
|
: 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");
|
||||||
|
|
@ -162,58 +164,30 @@ export default {
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
dateSelectedReadableDate() {
|
dateSelectedReadableDate() {
|
||||||
if (this.dateAndTimeSlotData === null) {
|
if (!this.dateAndTimeSlotData) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
// This conversion ensures we don't get get GMT induced date changes
|
|
||||||
const dateObject = new Date(`${this.dateAndTimeSlotData.dateString}T00:00:00`);
|
|
||||||
const weekdayName = DAYS_OF_WEEK[dateObject.getDay()];
|
|
||||||
const month = MONTHS_OF_YEAR[dateObject.getMonth()];
|
|
||||||
const numberDayOfMonth = dateObject.getDate();
|
|
||||||
// Ex. Tuesday, April 23
|
|
||||||
return `${weekdayName}, ${month} ${numberDayOfMonth}`;
|
|
||||||
},
|
|
||||||
availableTimeSlots() {
|
|
||||||
if (this.dateAndTimeSlotData === null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
let availableTimeSlots;
|
|
||||||
if (this.appointmentType === AppointmentTypeStrings.DROP_OFF) {
|
|
||||||
availableTimeSlots = [
|
|
||||||
{
|
|
||||||
value: this.dateAndTimeSlotData.timeSlots[0],
|
|
||||||
buttonLabel: this.dropoffButtonText,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
} else {
|
|
||||||
availableTimeSlots = this.dateAndTimeSlotData.timeSlots.map((timeslot) => {
|
|
||||||
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) {
|
// This conversion ensures we don't get get GMT induced date changes
|
||||||
const offerPremium = this.dateAndTimeSlotData.timeSlots[0].offerPremium;
|
const dateObject = new Date(`${this.dateAndTimeSlotData.date}T00:00:00`);
|
||||||
const hasEarlyBird = this.mobileEarlyBirdFee?.partType === "EARLY BIRD";
|
// Ex: Tuesday, April 22
|
||||||
if (offerPremium && hasEarlyBird) {
|
return dateObject.toLocaleDateString("en-us", {
|
||||||
availableTimeSlots.unshift({
|
weekday: "long",
|
||||||
value: this.dateAndTimeSlotData.timeSlots[0].id + "-earlybird",
|
month: "long",
|
||||||
buttonLabel: this.earlyBirdButtonText,
|
day: "numeric",
|
||||||
buttonLabelSubCopy: this.getTotalLineItemPrice(this.mobileEarlyBirdFee),
|
});
|
||||||
});
|
},
|
||||||
}
|
availableTimeSlots() {
|
||||||
|
if (!this.dateAndTimeSlotData) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (this.appointmentType === AppointmentTypeStrings.DROP_OFF) {
|
||||||
|
return this.getAvailableTimeSlotsForDropOff(this.dateAndTimeSlotData.timeSlots);
|
||||||
|
} else if (this.appointmentType === AppointmentTypeStrings.MOBILE) {
|
||||||
|
return this.getAvailableTimeSlotsForMobile(this.dateAndTimeSlotData.timeSlots);
|
||||||
|
} else {
|
||||||
|
return this.getAvailableTimeSlotsForInshop(this.dateAndTimeSlotData.timeSlots);
|
||||||
}
|
}
|
||||||
return availableTimeSlots;
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
@ -222,22 +196,29 @@ 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);
|
let isSelectedAppointmentPremium = false;
|
||||||
|
if (this.selectedTimeSlotId.includes(PREMIUM_TIME_SLOT_ID_FLAG)) {
|
||||||
|
this.selectedTimeSlotId = this.removePremiumFlagFromInput(this.selectedTimeSlotId);
|
||||||
|
isSelectedAppointmentPremium = true;
|
||||||
|
}
|
||||||
|
const selectedTimeSlotData = {
|
||||||
|
id: this.selectedTimeSlotId,
|
||||||
|
isPremiumAppointment: 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.$emit("time-slot-modal-closed");
|
this.$emit("time-slot-modal-closed");
|
||||||
},
|
},
|
||||||
// Expected input: "HH:MM:SS"
|
// Expected input: "HH:MM"
|
||||||
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";
|
const 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 +233,70 @@ 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(newAvailableTimeSlotsValue) {
|
||||||
|
const numberOfOptions = newAvailableTimeSlotsValue?.length;
|
||||||
|
if (numberOfOptions === 1) {
|
||||||
|
this.selectedTimeSlotId = newAvailableTimeSlotsValue[0].value;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addPremiumFlagToInput(timeSlotId) {
|
||||||
|
return (timeSlotId += PREMIUM_TIME_SLOT_ID_FLAG);
|
||||||
|
},
|
||||||
|
removePremiumFlagFromInput(timeSlotId) {
|
||||||
|
return timeSlotId.substring(0, timeSlotId.length - PREMIUM_TIME_SLOT_ID_FLAG.length);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
modal,
|
modal,
|
||||||
|
|
@ -271,7 +316,17 @@ export default {
|
||||||
margin-bottom: 0 !important;
|
margin-bottom: 0 !important;
|
||||||
}
|
}
|
||||||
.text-block.duration-text-block {
|
.text-block.duration-text-block {
|
||||||
margin-top: 0 !important;
|
margin-top: 4px !important;
|
||||||
|
}
|
||||||
|
.supplemental-information {
|
||||||
|
line-height: 1.5rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
li strong {
|
||||||
|
font-weight: $font-weight-bold;
|
||||||
|
}
|
||||||
|
li:not(:last-child) {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
// For nested objects, spread operator only creates new references to the top level fields,
|
|
||||||
// the remaining nested fields actually reference the original object which can introduce problems.
|
|
||||||
|
|
||||||
// The purpose of this method is to deep clone the data in an object recursively, this is useful
|
|
||||||
// for cloning modelValues to internal models when regular two-way binding is not an option.
|
|
||||||
// See: mobile-location-modal-questions.vue
|
|
||||||
|
|
||||||
// Creates a deep clone of an object. Clones primitives, arrays and objects, excluding class instances.
|
|
||||||
// https://www.30secondsofcode.org/js/s/deep-clone
|
|
||||||
export function deepClone(object) {
|
|
||||||
if (object === null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
let clone = Object.assign({}, object);
|
|
||||||
Object.keys(clone).forEach(
|
|
||||||
(key) =>
|
|
||||||
(clone[key] = typeof object[key] === "object" ? deepClone(object[key]) : object[key])
|
|
||||||
);
|
|
||||||
|
|
||||||
if (Array.isArray(object)) {
|
|
||||||
clone.length = object.length;
|
|
||||||
return Array.from(clone);
|
|
||||||
}
|
|
||||||
|
|
||||||
return clone;
|
|
||||||
}
|
|
||||||
|
|
@ -1,62 +0,0 @@
|
||||||
import { deepClone } from "./object-cloning-helper";
|
|
||||||
|
|
||||||
describe("object-cloning-helper.js", () => {
|
|
||||||
it("Should return null if no object is passed in", async () => {
|
|
||||||
// Arrange
|
|
||||||
const expected = null;
|
|
||||||
|
|
||||||
// Act
|
|
||||||
const result = deepClone(null);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(result).toEqual(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should return a deep copy of the object", async () => {
|
|
||||||
// Arrange
|
|
||||||
|
|
||||||
const object = {
|
|
||||||
addressQuestions: {
|
|
||||||
streetAddress: "555 Some St",
|
|
||||||
apartmentNumberOrBusinessName: "Apt 1",
|
|
||||||
city: "Funkytown",
|
|
||||||
state: "OH",
|
|
||||||
zipCode: "55555",
|
|
||||||
},
|
|
||||||
isVehicleProtected: true,
|
|
||||||
serviceZipCode: "55555",
|
|
||||||
};
|
|
||||||
|
|
||||||
const expected = {
|
|
||||||
addressQuestions: {
|
|
||||||
streetAddress: "555 Some St",
|
|
||||||
apartmentNumberOrBusinessName: "Apt 1",
|
|
||||||
city: "Funkytown",
|
|
||||||
state: "OH",
|
|
||||||
zipCode: "55555",
|
|
||||||
},
|
|
||||||
isVehicleProtected: true,
|
|
||||||
serviceZipCode: "55555",
|
|
||||||
};
|
|
||||||
|
|
||||||
// Act
|
|
||||||
const result = deepClone(object);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(result).toStrictEqual(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should return a copy of the array", async () => {
|
|
||||||
// Arrange
|
|
||||||
|
|
||||||
const array = [9, 8, 7, 6, 5, 4, 3, 2, 1];
|
|
||||||
|
|
||||||
const expected = [9, 8, 7, 6, 5, 4, 3, 2, 1];
|
|
||||||
|
|
||||||
// Act
|
|
||||||
const result = deepClone(array);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(result).toStrictEqual(expected);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { storeActions } from "@/constants/store-actions";
|
import { storeActions } from "@/constants/store-actions";
|
||||||
|
import store from "@/store";
|
||||||
import baseMixin from "@/mixins/base-mixin.js";
|
import baseMixin from "@/mixins/base-mixin.js";
|
||||||
|
|
||||||
export async function getPricedMobileFeePart(serviceZipCode) {
|
export async function getPricedMobileFeePart(serviceZipCode) {
|
||||||
|
|
@ -42,3 +43,48 @@ export async function getServiceabilityDetails(serviceZipCode, lineItems) {
|
||||||
|
|
||||||
return Promise.resolve(serviceabilityDetails);
|
return Promise.resolve(serviceabilityDetails);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getAvailabilityRating(
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
shopAppointmentType,
|
||||||
|
providerNumber
|
||||||
|
) {
|
||||||
|
// For a given shop provider number and date range, get the appointment time slots available
|
||||||
|
const shopTimeSlots = await baseMixin.methods.dispatchStoreAction(
|
||||||
|
storeActions.GET_SHOP_TIME_SLOTS,
|
||||||
|
{
|
||||||
|
providerNumber: providerNumber,
|
||||||
|
startDate: startDate,
|
||||||
|
endDate: endDate,
|
||||||
|
shopAppointmentType: shopAppointmentType,
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
// Rate the availability for the shop
|
||||||
|
let numberOfAppointmentsPerDay = [];
|
||||||
|
for (let i = 0; i < shopTimeSlots.data.days.length; i++) {
|
||||||
|
numberOfAppointmentsPerDay.push(shopTimeSlots.data.days[i].timeSlots.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
const dateRange = 7;
|
||||||
|
const minimumNumberOfAppointmentsPerDay = 1;
|
||||||
|
const numberOfDaysToEvaluate = 2;
|
||||||
|
|
||||||
|
let daysWithMinimalAppointmentsCount = 0;
|
||||||
|
for (let i = 0; i < dateRange; i++) {
|
||||||
|
if (numberOfAppointmentsPerDay[i] >= minimumNumberOfAppointmentsPerDay) {
|
||||||
|
daysWithMinimalAppointmentsCount++;
|
||||||
|
if (daysWithMinimalAppointmentsCount >= numberOfDaysToEvaluate) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isGoodAvailability = daysWithMinimalAppointmentsCount >= numberOfDaysToEvaluate;
|
||||||
|
|
||||||
|
const shopStatus = isGoodAvailability ? "high" : "low";
|
||||||
|
|
||||||
|
return Promise.resolve(shopStatus);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,186 @@
|
||||||
import { getPricedMobileFeePart, getServiceabilityDetails } from "./service-location-helper";
|
import {
|
||||||
|
getPricedMobileFeePart,
|
||||||
|
getServiceabilityDetails,
|
||||||
|
getAvailabilityRating,
|
||||||
|
} from "./service-location-helper";
|
||||||
import { storeActions } from "@/constants/store-actions";
|
import { storeActions } from "@/constants/store-actions";
|
||||||
|
|
||||||
|
jest.mock("@/store", () => ({
|
||||||
|
getters: {
|
||||||
|
order: {
|
||||||
|
vehicle: {
|
||||||
|
year: null,
|
||||||
|
make: null,
|
||||||
|
model: null,
|
||||||
|
style: null,
|
||||||
|
carId: null,
|
||||||
|
category: null,
|
||||||
|
vin: null,
|
||||||
|
imageUrl: null,
|
||||||
|
imageVifNumber: null,
|
||||||
|
imageColor: null,
|
||||||
|
registration: {
|
||||||
|
licensePlate: null,
|
||||||
|
address: null,
|
||||||
|
city: null,
|
||||||
|
state: null,
|
||||||
|
zipCode: null,
|
||||||
|
firstName: null,
|
||||||
|
lastName: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
serviceLocation: {
|
||||||
|
address: null,
|
||||||
|
city: null,
|
||||||
|
state: null,
|
||||||
|
zipCode: null,
|
||||||
|
zipCodeCtu: null,
|
||||||
|
appointmentType: null,
|
||||||
|
provider: {
|
||||||
|
providerNumber: null,
|
||||||
|
address: {
|
||||||
|
streetAddress: null,
|
||||||
|
city: null,
|
||||||
|
state: null,
|
||||||
|
zip: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
customer: {
|
||||||
|
emailAddress: null,
|
||||||
|
},
|
||||||
|
damage: {
|
||||||
|
isRepair: null,
|
||||||
|
numberOfChips: null,
|
||||||
|
glassToReplace: null,
|
||||||
|
partQuestionAnswers: null,
|
||||||
|
moldingQuestionAnswers: null,
|
||||||
|
capabilityQuestionAnswers: null,
|
||||||
|
},
|
||||||
|
lineItems: {
|
||||||
|
glassParts: null,
|
||||||
|
supportingItems: null,
|
||||||
|
vaps: null,
|
||||||
|
serverData: null,
|
||||||
|
},
|
||||||
|
payment: {
|
||||||
|
isInsurance: null,
|
||||||
|
insuranceCoverage: {
|
||||||
|
isVerified: null,
|
||||||
|
coverageStatus: null,
|
||||||
|
},
|
||||||
|
parentAccountNumber: 0,
|
||||||
|
},
|
||||||
|
schedule: {
|
||||||
|
date: null,
|
||||||
|
startTime: null,
|
||||||
|
endTime: null,
|
||||||
|
routeCode: null,
|
||||||
|
},
|
||||||
|
referralNumber: null,
|
||||||
|
referralDate: null,
|
||||||
|
referralCorrelationId: null,
|
||||||
|
eon: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
const mockStoreActionGetMobileFeePart = storeActions.GET_MOBILE_FEE_PART;
|
const mockStoreActionGetMobileFeePart = storeActions.GET_MOBILE_FEE_PART;
|
||||||
const mockStoreActionPriceOrderItemsAndSaveServerData =
|
const mockStoreActionPriceOrderItemsAndSaveServerData =
|
||||||
storeActions.PRICE_ORDER_ITEMS_AND_SAVE_SERVER_DATA;
|
storeActions.PRICE_ORDER_ITEMS_AND_SAVE_SERVER_DATA;
|
||||||
const mockStoreActionGetServiceabilityDetails = storeActions.GET_SERVICEABILITY_DETAILS;
|
const mockStoreActionGetServiceabilityDetails = storeActions.GET_SERVICEABILITY_DETAILS;
|
||||||
|
const mockStoreActionGetShopTimeSlots = storeActions.GET_SHOP_TIME_SLOTS;
|
||||||
|
|
||||||
|
const mockGetShopTimeSlotsGoodAvailability = {
|
||||||
|
estimatedServiceMinutesMinimum: 0,
|
||||||
|
estimatedServiceMinutesMaximimum: 0,
|
||||||
|
days: [
|
||||||
|
{
|
||||||
|
date: "string",
|
||||||
|
timeSlots: [
|
||||||
|
{
|
||||||
|
id: "string",
|
||||||
|
startTime: "",
|
||||||
|
endTime: "",
|
||||||
|
offerPremium: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: "string",
|
||||||
|
timeSlots: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: "string",
|
||||||
|
timeSlots: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: "string",
|
||||||
|
timeSlots: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: "string",
|
||||||
|
timeSlots: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: "string",
|
||||||
|
timeSlots: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: "string",
|
||||||
|
timeSlots: [
|
||||||
|
{
|
||||||
|
id: "string",
|
||||||
|
startTime: "",
|
||||||
|
endTime: "",
|
||||||
|
offerPremium: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockGetShopTimeSlotsLowAvailability = {
|
||||||
|
estimatedServiceMinutesMinimum: 0,
|
||||||
|
estimatedServiceMinutesMaximimum: 0,
|
||||||
|
days: [
|
||||||
|
{
|
||||||
|
date: "string",
|
||||||
|
timeSlots: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: "string",
|
||||||
|
timeSlots: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: "string",
|
||||||
|
timeSlots: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: "string",
|
||||||
|
timeSlots: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: "string",
|
||||||
|
timeSlots: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: "string",
|
||||||
|
timeSlots: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: "string",
|
||||||
|
timeSlots: [
|
||||||
|
{
|
||||||
|
id: "string",
|
||||||
|
startTime: "",
|
||||||
|
endTime: "",
|
||||||
|
offerPremium: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
jest.mock("@/mixins/base-mixin.js", () => ({
|
jest.mock("@/mixins/base-mixin.js", () => ({
|
||||||
methods: {
|
methods: {
|
||||||
|
|
@ -18,7 +194,7 @@ jest.mock("@/mixins/base-mixin.js", () => ({
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
|
|
||||||
dispatchStoreAction: jest.fn().mockImplementation((actionName) => {
|
dispatchStoreAction: jest.fn().mockImplementation((actionName, request) => {
|
||||||
if (actionName === mockStoreActionGetMobileFeePart) {
|
if (actionName === mockStoreActionGetMobileFeePart) {
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
data: {
|
data: {
|
||||||
|
|
@ -50,6 +226,14 @@ jest.mock("@/mixins/base-mixin.js", () => ({
|
||||||
isRecalibrationServiceableMobile: true,
|
isRecalibrationServiceableMobile: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (actionName === mockStoreActionGetShopTimeSlots) {
|
||||||
|
if (request.providerNumber == "0000001") {
|
||||||
|
return Promise.resolve(mockGetShopTimeSlotsGoodAvailability);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve(mockGetShopTimeSlotsLowAvailability);
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
@ -124,4 +308,25 @@ describe("service-location-helper.js", () => {
|
||||||
expect(result).toEqual(expected);
|
expect(result).toEqual(expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("getAvailabilityRating", () => {
|
||||||
|
// it("Should return a 'Good' rating", async () => {
|
||||||
|
// // Arrange
|
||||||
|
// const providerNumber = "0000001";
|
||||||
|
// const expected = "Good";
|
||||||
|
// // Act
|
||||||
|
// const result = await getAvailabilityRating(providerNumber);
|
||||||
|
// // Assert
|
||||||
|
// expect(result).toEqual(expected);
|
||||||
|
// });
|
||||||
|
// it("Should return a 'Low' rating", async () => {
|
||||||
|
// // Arrange
|
||||||
|
// const providerNumber = "0000000";
|
||||||
|
// const expected = "Low";
|
||||||
|
// // Act
|
||||||
|
// const result = await getAvailabilityRating(providerNumber);
|
||||||
|
// // Assert
|
||||||
|
// expect(result).toEqual(expected);
|
||||||
|
// });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@ import addressQuestions from "@/layouts/address-lookup/customer-questions/addres
|
||||||
import vehicleProtectedQuestion from "@/layouts/service-location/mobile-location-modal-questions/vehicle-protected-question/vehicle-protected-question";
|
import vehicleProtectedQuestion from "@/layouts/service-location/mobile-location-modal-questions/vehicle-protected-question/vehicle-protected-question";
|
||||||
|
|
||||||
// Helpers
|
// Helpers
|
||||||
import { deepClone } from "@/layouts/service-location/helpers/object-cloning-helper/object-cloning-helper";
|
import { deepClone } from "@/helpers/object-helper";
|
||||||
import {
|
import {
|
||||||
getPricedMobileFeePart,
|
getPricedMobileFeePart,
|
||||||
getServiceabilityDetails,
|
getServiceabilityDetails,
|
||||||
|
|
@ -202,6 +202,7 @@ export default {
|
||||||
this.internalModel = deepClone(this.modelValue);
|
this.internalModel = deepClone(this.modelValue);
|
||||||
},
|
},
|
||||||
onModalClosed() {
|
onModalClosed() {
|
||||||
|
this.displayInvalidZipAlert = false;
|
||||||
this.internalModel = deepClone(this.modelValue);
|
this.internalModel = deepClone(this.modelValue);
|
||||||
this.resetValidation();
|
this.resetValidation();
|
||||||
},
|
},
|
||||||
|
|
@ -261,15 +262,19 @@ export default {
|
||||||
this.$emit("updated-serviceability", serviceabilityDetails.data);
|
this.$emit("updated-serviceability", serviceabilityDetails.data);
|
||||||
this.$emit("updated-contains-military-base", zipCodeData.containsMilitaryBase);
|
this.$emit("updated-contains-military-base", zipCodeData.containsMilitaryBase);
|
||||||
|
|
||||||
// Update the page level model
|
|
||||||
this.$emit("update:modelValue", this.internalModel);
|
|
||||||
|
|
||||||
if (this.onZipUpdateCallback) {
|
if (this.onZipUpdateCallback) {
|
||||||
await this.onZipUpdateCallback(serviceZipCode);
|
await this.onZipUpdateCallback(serviceZipCode);
|
||||||
}
|
}
|
||||||
|
// Update the page level model
|
||||||
|
this.$emit("update:modelValue", this.internalModel);
|
||||||
|
|
||||||
this.closeModal();
|
this.closeModal();
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Update the page level model
|
||||||
|
this.$emit("update:modelValue", this.internalModel);
|
||||||
|
|
||||||
|
this.closeModal();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -138,11 +138,11 @@ export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
streetAddress: this.getServiceAddressFromStore(),
|
streetAddress: this.getServiceAddressFromStore(),
|
||||||
apartmentNumberOrBusinessName: "",
|
apartmentNumberOrBusinessName: this.getServiceAddress2FromStore(),
|
||||||
city: this.getServiceCityFromStore(),
|
city: this.getServiceCityFromStore(),
|
||||||
state: this.getServiceStateFromStore(),
|
state: this.getServiceStateFromStore(),
|
||||||
zipCode: this.getServiceZipCodeFromStore(),
|
zipCode: this.getServiceZipCodeFromStore(),
|
||||||
isVehicleProtected: null,
|
isVehicleProtected: this.getIsVehicleProtectedFromStore(),
|
||||||
isGlassServiceableInshop: null,
|
isGlassServiceableInshop: null,
|
||||||
isRecalibrationServiceableInshop: null,
|
isRecalibrationServiceableInshop: null,
|
||||||
isGlassServiceableMobile: null,
|
isGlassServiceableMobile: null,
|
||||||
|
|
@ -339,6 +339,9 @@ export default {
|
||||||
getServiceAddressFromStore() {
|
getServiceAddressFromStore() {
|
||||||
return store.getters.order.serviceLocation.address;
|
return store.getters.order.serviceLocation.address;
|
||||||
},
|
},
|
||||||
|
getServiceAddress2FromStore() {
|
||||||
|
return store.getters.order.serviceLocation.address2;
|
||||||
|
},
|
||||||
getServiceCityFromStore() {
|
getServiceCityFromStore() {
|
||||||
return store.getters.order.serviceLocation.city;
|
return store.getters.order.serviceLocation.city;
|
||||||
},
|
},
|
||||||
|
|
@ -348,6 +351,9 @@ export default {
|
||||||
getServiceZipCodeFromStore() {
|
getServiceZipCodeFromStore() {
|
||||||
return store.getters.order.serviceLocation.zipCode;
|
return store.getters.order.serviceLocation.zipCode;
|
||||||
},
|
},
|
||||||
|
getIsVehicleProtectedFromStore() {
|
||||||
|
return store.getters.order.serviceLocation.isVehicleProtected;
|
||||||
|
},
|
||||||
getSelectedAppointmentType() {
|
getSelectedAppointmentType() {
|
||||||
return store.getters.order.serviceLocation.appointmentType;
|
return store.getters.order.serviceLocation.appointmentType;
|
||||||
},
|
},
|
||||||
|
|
@ -391,6 +397,7 @@ export default {
|
||||||
city: this.selectedProvider?.address?.city,
|
city: this.selectedProvider?.address?.city,
|
||||||
state: this.selectedProvider?.address?.state,
|
state: this.selectedProvider?.address?.state,
|
||||||
zip: this.selectedProvider?.address?.zipCode,
|
zip: this.selectedProvider?.address?.zipCode,
|
||||||
|
zipCtu: this.selectedProvider?.address?.zipCodeCtu,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { mount } from "@vue/test-utils";
|
||||||
import shopListButton from "./shop-list-button";
|
import shopListButton from "./shop-list-button";
|
||||||
import inputButtonWrapperMixin from "@/mixins/input-button-wrapper-mixin";
|
import inputButtonWrapperMixin from "@/mixins/input-button-wrapper-mixin";
|
||||||
|
|
||||||
describe("service-package-radio.vue", () => {
|
describe("shop-list-button.vue", () => {
|
||||||
it("Should include buttonLabel in html", async () => {
|
it("Should include buttonLabel in html", async () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
let { wrapper } = setupMocks({
|
let { wrapper } = setupMocks({
|
||||||
|
|
@ -49,6 +49,18 @@ describe("service-package-radio.vue", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const startDate = new Date();
|
||||||
|
const endDate = new Date();
|
||||||
|
endDate.setDate(startDate.getDate() + 7);
|
||||||
|
const getAvailabilityRating = jest.fn().mockImplementation((actionName, request) => {
|
||||||
|
return Promise.resolve({
|
||||||
|
data: {},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const formattedStartDate = startDate.toISOString().split("T")[0];
|
||||||
|
const formattedEndDate = endDate.toISOString().split("T")[0];
|
||||||
|
|
||||||
const mockProps = {
|
const mockProps = {
|
||||||
buttonLabel: "buttonLabel test copy",
|
buttonLabel: "buttonLabel test copy",
|
||||||
buttonLabelSubCopy: "buttonLabelSubCopy test copy",
|
buttonLabelSubCopy: "buttonLabelSubCopy test copy",
|
||||||
|
|
@ -58,6 +70,12 @@ const mockProps = {
|
||||||
value: 0,
|
value: 0,
|
||||||
modelValue: 0,
|
modelValue: 0,
|
||||||
groupName: "mockGroup",
|
groupName: "mockGroup",
|
||||||
|
additionalButtonData: {
|
||||||
|
availabilityRatingCallback: getAvailabilityRating,
|
||||||
|
startDate: formattedStartDate,
|
||||||
|
endDate: formattedEndDate,
|
||||||
|
shopAppointmentType: "Dropoff",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function setupMocks({ mountOptionsMockData = {} }) {
|
function setupMocks({ mountOptionsMockData = {} }) {
|
||||||
|
|
|
||||||
|
|
@ -11,26 +11,32 @@
|
||||||
<span class="m-0 button-label-copy" :class="textPosition">{{
|
<span class="m-0 button-label-copy" :class="textPosition">{{
|
||||||
buttonLabel
|
buttonLabel
|
||||||
}}</span>
|
}}</span>
|
||||||
<span class="m-0 button-label-sub-copy" :class="textPosition">{{
|
<span class="m-0 caption ms-1" :class="textPosition">{{
|
||||||
buttonLabelSubCopy
|
buttonLabelSubCopy
|
||||||
}}</span>
|
}}</span>
|
||||||
<div
|
<div
|
||||||
class="availability-indicator"
|
v-if="displayAvailabilityIndicators"
|
||||||
:class="availability === 'high' ? 'green' : 'red'">
|
class="availability-indicator rounded-pill"
|
||||||
<span class="m-0 button-auxillary-copy">{{ buttonAuxillaryCopy }}</span>
|
:class="availabilityRatingClass">
|
||||||
|
<div
|
||||||
|
v-if="!isLoaderDisplayed"
|
||||||
|
class="availability-badge"
|
||||||
|
:class="availabilityRating == 'high' ? 'green' : 'red'"></div>
|
||||||
|
<span v-if="!isLoaderDisplayed" class="m-0 button-auxillary-copy">{{
|
||||||
|
badgeText
|
||||||
|
}}</span>
|
||||||
|
<loader v-if="isLoaderDisplayed" :class="['left', 'no-block']" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span
|
<div class="row-two">
|
||||||
v-if="buttonBodyCopy"
|
<span
|
||||||
class="m-0 button-label-sub-copy small"
|
v-if="buttonBodyCopy"
|
||||||
:class="textPosition"
|
class="m-0 button-label-sub-copy small"
|
||||||
v-html="buttonBodyCopy"></span>
|
v-html="buttonBodyCopy"></span>
|
||||||
|
</div>
|
||||||
<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>
|
||||||
</transition>
|
</transition>
|
||||||
|
|
@ -40,32 +46,60 @@
|
||||||
import loader from "@/ux-components/loader/loader";
|
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";
|
||||||
|
import experimentMixin from "@/mixins/experiment-mixin";
|
||||||
|
import { experimentSettings } from "@/constants/experiments";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "shopListButton",
|
name: "shopListButton",
|
||||||
mixins: [inputButtonWrapperMixin],
|
mixins: [inputButtonWrapperMixin],
|
||||||
props: {
|
beforeMount() {
|
||||||
loaderColor: String,
|
if (this.displayAvailabilityIndicators) {
|
||||||
loaderPosition: {
|
this.displayLoader();
|
||||||
type: String,
|
const startDate = this.additionalButtonData.startDate;
|
||||||
default: "right",
|
const endDate = this.additionalButtonData.endDate;
|
||||||
},
|
const shopAppointmentType = this.additionalButtonData.shopAppointmentType;
|
||||||
|
|
||||||
|
this.additionalButtonData
|
||||||
|
.availabilityRatingCallback(startDate, endDate, shopAppointmentType, this.value)
|
||||||
|
.then((data) => {
|
||||||
|
this.availabilityRating = data;
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
isLoaderDisplayed: false,
|
availabilityRating: null,
|
||||||
availability: "low",
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
displayAvailabilityIndicators() {
|
||||||
|
return experimentMixin.methods.hasSettingEqualTo(
|
||||||
|
experimentSettings.DISPLAY_AVAILABILITY_INDICATORS,
|
||||||
|
"true"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
isLoaderDisplayed() {
|
||||||
|
return this.availabilityRating == null;
|
||||||
|
},
|
||||||
|
availabilityRatingClass() {
|
||||||
|
if (this.availabilityRating == null) {
|
||||||
|
return "gray";
|
||||||
|
} else {
|
||||||
|
return this.availabilityRating == "high" ? "green" : "red";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
badgeText() {
|
||||||
|
if (this.availabilityRating != null) {
|
||||||
|
return this.availabilityRating == "high" ? "Appts available" : "Appts low";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
},
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
displayLoader() {
|
displayLoader() {
|
||||||
this.isLoaderDisplayed = true;
|
this.isLoaderDisplayed = true;
|
||||||
},
|
},
|
||||||
preHandleAnswerChange() {
|
|
||||||
if (this.selectingInitiatesLoad) {
|
|
||||||
this.displayLoader();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
loader,
|
loader,
|
||||||
|
|
@ -75,9 +109,6 @@ export default {
|
||||||
</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"],
|
||||||
|
|
@ -103,10 +134,6 @@ export default {
|
||||||
&:checked + .list-button-content span {
|
&:checked + .list-button-content span {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
&:checked + .list-button-content span:nth-child(2) {
|
|
||||||
font-weight: 400;
|
|
||||||
color: $gray-600;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.list-button-content {
|
.list-button-content {
|
||||||
|
|
@ -127,53 +154,83 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.button-content {
|
.button-content {
|
||||||
|
row-gap: 0.25rem;
|
||||||
|
|
||||||
.row-one {
|
.row-one {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 0.25rem !important;
|
line-height: 1.5rem;
|
||||||
|
|
||||||
.button-label-copy {
|
.button-label-copy {
|
||||||
flex-grow: 0;
|
|
||||||
line-height: 1.5rem;
|
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-label-sub-copy {
|
|
||||||
flex-grow: 1;
|
|
||||||
line-height: 1.25rem !important;
|
|
||||||
font-weight: 400;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: #727676;
|
|
||||||
padding-left: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.availability-indicator {
|
.availability-indicator {
|
||||||
display: none;
|
background-repeat: no-repeat;
|
||||||
flex-direction: row;
|
display: flex;
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0.125rem 1.5rem;
|
margin-left: auto;
|
||||||
gap: 0.25rem;
|
padding: 0.125rem 0.5rem;
|
||||||
background: #e3f2ea;
|
|
||||||
border-radius: 4.5rem;
|
.availability-badge {
|
||||||
|
display: inline;
|
||||||
|
width: 13px;
|
||||||
|
height: 12px;
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
margin: 0 0.25rem 0 0;
|
||||||
|
|
||||||
|
&.green {
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 13 12' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M6.5 12C3.19159 12 0.5 9.30841 0.5 6C0.5 2.69159 3.19159 0 6.5 0C9.80841 0 12.5 2.69159 12.5 6C12.5 9.30841 9.80841 12 6.5 12ZM6.5 0.785047C3.62449 0.785047 1.28505 3.12449 1.28505 6C1.28505 8.87551 3.62449 11.215 6.5 11.215C9.37551 11.215 11.715 8.87551 11.715 6C11.715 3.12449 9.37551 0.785047 6.5 0.785047Z' fill='%23006A36'/%3E%3Cpath d='M5.697 7.95252C5.5927 7.95252 5.49289 7.91102 5.41999 7.837L3.90597 6.32299C3.75233 6.16934 3.75233 5.92149 3.90597 5.76785C4.05962 5.6142 4.30747 5.6142 4.46111 5.76785L5.69812 7.00373L8.53999 4.16186C8.69364 4.00822 8.94149 4.00822 9.09513 4.16186C9.24878 4.31551 9.24878 4.56336 9.09513 4.717L5.97626 7.83588C5.90224 7.9099 5.80242 7.9514 5.69925 7.9514L5.697 7.95252Z' fill='%23006A36'/%3E%3C/svg%3E%0A");
|
||||||
|
}
|
||||||
|
|
||||||
|
&.red {
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg width='13' height='12' viewBox='0 0 13 12' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M6.5 12C3.19159 12 0.5 9.30841 0.5 6C0.5 2.69159 3.19159 0 6.5 0C9.80841 0 12.5 2.69159 12.5 6C12.5 9.30841 9.80841 12 6.5 12ZM6.5 0.785047C3.62449 0.785047 1.28505 3.12449 1.28505 6C1.28505 8.87551 3.62449 11.215 6.5 11.215C9.37551 11.215 11.715 8.87551 11.715 6C11.715 3.12449 9.37551 0.785047 6.5 0.785047Z' fill='%23006A36'/%3E%3Csvg width='13' height='12' viewBox='0 0 13 12' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M6.5 0C9.81368 0 12.5 2.68632 12.5 6C12.5 9.31368 9.81368 12 6.5 12C3.18632 12 0.5 9.31368 0.5 6C0.5 2.68632 3.18632 0 6.5 0ZM6.5 0.84C3.6548 0.84 1.34 3.1548 1.34 6C1.34 8.8452 3.6548 11.16 6.5 11.16C9.3452 11.16 11.66 8.8452 11.66 6C11.66 3.1548 9.3452 0.84 6.5 0.84ZM7.90018 4.00596C8.06422 3.84204 8.33002 3.84192 8.49406 4.00596C8.6581 4.17 8.6581 4.43592 8.49406 4.59996L7.0939 6L8.49406 7.40004C8.6581 7.56408 8.6581 7.83 8.49406 7.99404C8.4121 8.076 8.30458 8.11704 8.19706 8.11704C8.08966 8.11704 7.98214 8.076 7.90018 7.99404L6.50002 6.594L5.09986 7.99404C5.01778 8.076 4.91038 8.11704 4.80286 8.11704C4.69546 8.11704 4.58794 8.076 4.50598 7.99404C4.34182 7.83 4.34182 7.56408 4.50598 7.40004L5.90614 6L4.50598 4.59996C4.34182 4.43592 4.34182 4.17 4.50598 4.00596C4.66978 3.84192 4.93582 3.84204 5.09986 4.00596L6.50002 5.406L7.90018 4.00596Z' fill='%23AC160B'/%3E%3C/svg%3E%3C/svg%3E%0A");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.button-auxillary-copy {
|
.button-auxillary-copy {
|
||||||
|
box-sizing: border-box;
|
||||||
justify-content: right;
|
justify-content: right;
|
||||||
line-height: 1.25rem !important;
|
line-height: 1.25rem;
|
||||||
font-weight: 400;
|
font-weight: 500;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.green {
|
.loader {
|
||||||
color: #006a36;
|
padding: 0 0.25rem 0 0.25rem;
|
||||||
background: #e3f2ea;
|
padding-top: 0.125rem;
|
||||||
|
padding-bottom: 0.125rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.red {
|
.loader:after {
|
||||||
color: #ac160b;
|
background-color: $gray-550;
|
||||||
background: #e3f2ea;
|
height: 1rem;
|
||||||
|
width: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.green {
|
||||||
|
color: $green-700;
|
||||||
|
background-color: $green-100;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.red {
|
||||||
|
color: $red-600;
|
||||||
|
background-color: $red-100;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.gray {
|
||||||
|
color: $gray-600;
|
||||||
|
background-color: $gray-100;
|
||||||
|
padding-left: 0.125rem;
|
||||||
|
padding-right: 0.125rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.row-two {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,8 @@
|
||||||
textPosition="text-start"
|
textPosition="text-start"
|
||||||
v-model="selectedProviderNumber"
|
v-model="selectedProviderNumber"
|
||||||
isRequired
|
isRequired
|
||||||
validationRules="option-required" />
|
validationRules="option-required"
|
||||||
|
:additionalButtonData="additionalButtonData" />
|
||||||
<textLink
|
<textLink
|
||||||
v-show="displaySeeMoreLocationsLink"
|
v-show="displaySeeMoreLocationsLink"
|
||||||
ref="showMoreShopsLink"
|
ref="showMoreShopsLink"
|
||||||
|
|
@ -51,6 +52,8 @@ import { errorMessages } from "@/constants/error-messages";
|
||||||
import baseMixin from "@/mixins/base-mixin.js";
|
import baseMixin from "@/mixins/base-mixin.js";
|
||||||
import { nextTick } from "vue";
|
import { nextTick } from "vue";
|
||||||
|
|
||||||
|
import { getAvailabilityRating } from "@/layouts/service-location/helpers/service-location-helper/service-location-helper";
|
||||||
|
|
||||||
defineRule("option-required", required(errorMessages.OPTION_REQUIRED));
|
defineRule("option-required", required(errorMessages.OPTION_REQUIRED));
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
@ -102,6 +105,21 @@ export default {
|
||||||
showMoreShopsLinkText() {
|
showMoreShopsLinkText() {
|
||||||
return this.getCmsContent("ShowMoreShopsLinkWidget", "Text");
|
return this.getCmsContent("ShowMoreShopsLinkWidget", "Text");
|
||||||
},
|
},
|
||||||
|
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 {
|
||||||
|
availabilityRatingCallback: getAvailabilityRating,
|
||||||
|
startDate: formattedStartDate,
|
||||||
|
endDate: formattedEndDate,
|
||||||
|
shopAppointmentType: this.selectedAppointmentType,
|
||||||
|
};
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
loadInitialData(serviceZipCode) {
|
loadInitialData(serviceZipCode) {
|
||||||
|
|
@ -158,7 +176,7 @@ export default {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.$nextTick();
|
await nextTick();
|
||||||
|
|
||||||
if (this.shopIndex == this.shopProviders.length) {
|
if (this.shopIndex == this.shopProviders.length) {
|
||||||
this.displaySeeMoreLocationsLink = false;
|
this.displaySeeMoreLocationsLink = false;
|
||||||
|
|
@ -166,7 +184,7 @@ export default {
|
||||||
this.displaySeeMoreLocationsLink = true;
|
this.displaySeeMoreLocationsLink = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.$nextTick();
|
await nextTick();
|
||||||
|
|
||||||
this.scrollToPageBottom();
|
this.scrollToPageBottom();
|
||||||
},
|
},
|
||||||
|
|
@ -203,22 +221,20 @@ export default {
|
||||||
async handler(newValue) {
|
async handler(newValue) {
|
||||||
this.resetAnswers();
|
this.resetAnswers();
|
||||||
|
|
||||||
await this.$nextTick();
|
await nextTick();
|
||||||
|
|
||||||
this.selectedProviderNumber = null;
|
this.selectedProviderNumber = null;
|
||||||
|
|
||||||
this.$refs.buttonQuestion?.resetField();
|
await nextTick();
|
||||||
|
|
||||||
await this.$nextTick();
|
|
||||||
|
|
||||||
if (newValue !== "Mobile") {
|
if (newValue !== "Mobile") {
|
||||||
await this.getNextShopsFromList();
|
this.getNextShopsFromList();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
shopProviders: {
|
shopProviders: {
|
||||||
async handler(newValue) {
|
async handler(newValue) {
|
||||||
await this.$nextTick();
|
await nextTick();
|
||||||
|
|
||||||
if (this.selectedAppointmentType) {
|
if (this.selectedAppointmentType) {
|
||||||
const selectedShopIndex = this.getSelectedProviderIndex(
|
const selectedShopIndex = this.getSelectedProviderIndex(
|
||||||
|
|
@ -230,7 +246,7 @@ export default {
|
||||||
await this.getNextShopsFromList(selectedShopIndex + 1);
|
await this.getNextShopsFromList(selectedShopIndex + 1);
|
||||||
} else {
|
} else {
|
||||||
await this.getNextShopsFromList();
|
await this.getNextShopsFromList();
|
||||||
await this.$nextTick();
|
await nextTick();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -261,14 +277,16 @@ export default {
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-size: 0.75rem;
|
background-size: 0.75rem;
|
||||||
background-position: 0.5rem 0.75rem;
|
background-position: 0.5rem 0.75rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
padding: 0.5rem 0.5rem 0.5rem 1.5rem;
|
||||||
|
gap: 0.25rem;
|
||||||
|
|
||||||
.alert-heading {
|
.alert-heading {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
line-height: 1.25rem;
|
line-height: 1.25rem;
|
||||||
}
|
}
|
||||||
p {
|
|
||||||
padding-left: 0.5rem;
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
fmg
|
|
||||||
<template>
|
<template>
|
||||||
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm" v-slot="{ meta }">
|
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm" v-slot="{ meta }">
|
||||||
<div class="page-container-grouped-styles">
|
<div class="page-container-grouped-styles">
|
||||||
|
|
|
||||||
|
|
@ -481,7 +481,7 @@ describe("vehicle-parts.vue", () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
wrapper.vm.$store.commit = jest.fn();
|
wrapper.vm.dispatchStoreAction = jest.fn();
|
||||||
|
|
||||||
wrapper.setData({
|
wrapper.setData({
|
||||||
selectedGlassParts: { "Rear-Stationary": { partNumber: "DB12209GTYN" } },
|
selectedGlassParts: { "Rear-Stationary": { partNumber: "DB12209GTYN" } },
|
||||||
|
|
@ -498,7 +498,7 @@ describe("vehicle-parts.vue", () => {
|
||||||
await wrapper.vm.forwardButtonAction();
|
await wrapper.vm.forwardButtonAction();
|
||||||
|
|
||||||
//Assert
|
//Assert
|
||||||
expect(wrapper.vm.$store.commit).toHaveBeenCalled();
|
expect(wrapper.vm.dispatchStoreAction).toHaveBeenCalled();
|
||||||
|
|
||||||
// TODO KO UNCOMMENT FOR QUOTE PAGES RELEASE
|
// TODO KO UNCOMMENT FOR QUOTE PAGES RELEASE
|
||||||
// expect(wrapper.vm.$router.navigateWithSaving).toHaveBeenCalledWith(
|
// expect(wrapper.vm.$router.navigateWithSaving).toHaveBeenCalledWith(
|
||||||
|
|
|
||||||
|
|
@ -323,7 +323,7 @@ export default {
|
||||||
|
|
||||||
await this.dispatchStoreAction(storeActions.SAVE_EMAIL, this.emailAddress, false);
|
await this.dispatchStoreAction(storeActions.SAVE_EMAIL, this.emailAddress, false);
|
||||||
await this.dispatchStoreAction(
|
await this.dispatchStoreAction(
|
||||||
storeActions.SAVE_SERVICE_LOCATION,
|
storeActions.SAVE_SERVICE_ZIP_CODE_INFO,
|
||||||
{
|
{
|
||||||
zipCode: this.serviceZipCode,
|
zipCode: this.serviceZipCode,
|
||||||
state: resultMap.zipCodeData.state,
|
state: resultMap.zipCodeData.state,
|
||||||
|
|
@ -360,7 +360,7 @@ export default {
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
await this.dispatchStoreAction(
|
await this.dispatchStoreAction(
|
||||||
storeActions.SAVE_SERVICE_LOCATION,
|
storeActions.SAVE_SERVICE_ZIP_CODE_INFO,
|
||||||
{
|
{
|
||||||
zipCode: this.serviceZipCode,
|
zipCode: this.serviceZipCode,
|
||||||
state: zipCodeData.state,
|
state: zipCodeData.state,
|
||||||
|
|
|
||||||
|
|
@ -442,7 +442,7 @@ export default {
|
||||||
const collectedGlassParts = this.reducedGlassPartsArray(partsOrQuestions);
|
const collectedGlassParts = this.reducedGlassPartsArray(partsOrQuestions);
|
||||||
|
|
||||||
// save to store lineItems.glassParts
|
// save to store lineItems.glassParts
|
||||||
self.$store.commit(storeMutations.UPDATE_GLASS_PARTS, collectedGlassParts);
|
self.dispatchStoreAction(storeActions.SAVE_GLASS_PARTS, collectedGlassParts, false);
|
||||||
|
|
||||||
const payment = store.getters.payment;
|
const payment = store.getters.payment;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2240,15 +2240,17 @@ describe("vehicle-questions-mixin", () => {
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(wrapper.vm.$store.commit).toHaveBeenCalledTimes(1);
|
expect(wrapper.vm.$store.commit).toHaveBeenCalledTimes(1);
|
||||||
expect(wrapper.vm.$store.commit).toHaveBeenCalledWith(
|
expect(wrapper.vm.$store.dispatch).toHaveBeenCalledWith(
|
||||||
storeMutations.UPDATE_GLASS_PARTS,
|
storeActions.SAVE_GLASS_PARTS,
|
||||||
collectedGlassParts
|
collectedGlassParts
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(wrapper.vm.$router.navigateWithSaving).toHaveBeenCalledTimes(1);
|
expect(wrapper.vm.$router.navigateWithSaving).toHaveBeenCalledTimes(1);
|
||||||
expect(wrapper.vm.$router.navigateWithSaving).toHaveBeenCalledWith(
|
expect(wrapper.vm.$router.navigateWithSaving).toHaveBeenCalledWith(
|
||||||
navigationScenarios.CLICKED_FORWARD_WITH_NO_MORE_QUESTIONS,
|
navigationScenarios.CLICKED_FORWARD_WITH_NO_MORE_QUESTIONS,
|
||||||
{ query: { fmgPage: "vin-lookup" } }
|
{ query: { fmgPage: "vin-lookup" } }
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(wrapper.vm.$router.navigateWithoutSaving).not.toHaveBeenCalled();
|
expect(wrapper.vm.$router.navigateWithoutSaving).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -28,28 +28,14 @@ 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";
|
|
||||||
import review from "@/layouts/review/review";
|
import review from "@/layouts/review/review";
|
||||||
|
|
||||||
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: "/review", // This is a temporary route for testing.
|
path: "/review", // This is a temporary route for testing.
|
||||||
name: "review",
|
name: "review",
|
||||||
component: review,
|
component: review,
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "/",
|
path: "/",
|
||||||
name: "root",
|
name: "root",
|
||||||
async beforeEnter(to, from, next) {
|
async beforeEnter(to, from, next) {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { createStore, Store } from "vuex";
|
import { createStore } from "vuex";
|
||||||
import { endpoints } from "@/constants/endpoints.js";
|
import { endpoints } from "@/constants/endpoints.js";
|
||||||
import { storeMutations } from "@/constants/store-mutations";
|
import { storeMutations } from "@/constants/store-mutations";
|
||||||
import { getDateForSavedSessionTimeout } from "@/helpers/heritage-integration/session-helper";
|
import { getDateForSavedSessionTimeout } from "@/helpers/heritage-integration/session-helper";
|
||||||
|
|
@ -9,8 +9,9 @@ import { applicationConfig } from "@/constants/application-config";
|
||||||
import { experimentTriggers } from "@/constants/experiments";
|
import { experimentTriggers } from "@/constants/experiments";
|
||||||
import { damageLocationsSelected } from "@/constants/damage-locations-selected";
|
import { damageLocationsSelected } from "@/constants/damage-locations-selected";
|
||||||
import { fmgPageValues } from "@/router/router-constants/fmgPage-values";
|
import { fmgPageValues } from "@/router/router-constants/fmgPage-values";
|
||||||
import router from "@/router";
|
|
||||||
import { deleteFunnelCookie } from "@/helpers/heritage-integration/cookie-helper.js";
|
import { deleteFunnelCookie } from "@/helpers/heritage-integration/cookie-helper.js";
|
||||||
|
import { deepEqual } from "@/helpers/object-helper";
|
||||||
|
import { AppointmentTypeStrings, PREMIUM_FEE_PART_TYPE } from "@/constants/schedule-constants";
|
||||||
|
|
||||||
// Export State
|
// Export State
|
||||||
const getDefaultState = () => {
|
const getDefaultState = () => {
|
||||||
|
|
@ -39,11 +40,13 @@ const getDefaultState = () => {
|
||||||
},
|
},
|
||||||
serviceLocation: {
|
serviceLocation: {
|
||||||
address: null,
|
address: null,
|
||||||
|
address2: null,
|
||||||
city: null,
|
city: null,
|
||||||
state: null,
|
state: null,
|
||||||
zipCode: null,
|
zipCode: null,
|
||||||
zipCodeCtu: null,
|
zipCodeCtu: null,
|
||||||
appointmentType: null,
|
appointmentType: null,
|
||||||
|
isVehicleProtected: null,
|
||||||
provider: {
|
provider: {
|
||||||
providerNumber: null,
|
providerNumber: null,
|
||||||
address: {
|
address: {
|
||||||
|
|
@ -84,6 +87,7 @@ const getDefaultState = () => {
|
||||||
startTime: null,
|
startTime: null,
|
||||||
endTime: null,
|
endTime: null,
|
||||||
routeCode: null,
|
routeCode: null,
|
||||||
|
jobMaxMinutes: null,
|
||||||
},
|
},
|
||||||
referralNumber: null,
|
referralNumber: null,
|
||||||
referralDate: null,
|
referralDate: null,
|
||||||
|
|
@ -250,16 +254,7 @@ export const mutations = {
|
||||||
state.order.serviceLocation.isVehicleProtected = serviceLocationInfo.isVehicleProtected;
|
state.order.serviceLocation.isVehicleProtected = serviceLocationInfo.isVehicleProtected;
|
||||||
|
|
||||||
if (serviceLocationInfo.provider) {
|
if (serviceLocationInfo.provider) {
|
||||||
state.order.serviceLocation.provider.providerNumber =
|
state.order.serviceLocation.provider = serviceLocationInfo.provider;
|
||||||
serviceLocationInfo.provider?.providerNumber;
|
|
||||||
state.order.serviceLocation.provider.address.streetAddress =
|
|
||||||
serviceLocationInfo.provider?.address?.streetAddress;
|
|
||||||
state.order.serviceLocation.provider.address.city =
|
|
||||||
serviceLocationInfo.provider?.address?.city;
|
|
||||||
state.order.serviceLocation.provider.address.state =
|
|
||||||
serviceLocationInfo.provider?.address?.state;
|
|
||||||
state.order.serviceLocation.provider.address.zip =
|
|
||||||
serviceLocationInfo.provider?.address?.zip;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
updateSchedule(state, scheduleInfo) {
|
updateSchedule(state, scheduleInfo) {
|
||||||
|
|
@ -268,6 +263,7 @@ export const mutations = {
|
||||||
state.order.schedule.startTime = scheduleInfo.startTime;
|
state.order.schedule.startTime = scheduleInfo.startTime;
|
||||||
state.order.schedule.endTime = scheduleInfo.endTime;
|
state.order.schedule.endTime = scheduleInfo.endTime;
|
||||||
state.order.schedule.routeCode = scheduleInfo.routeCode;
|
state.order.schedule.routeCode = scheduleInfo.routeCode;
|
||||||
|
state.order.schedule.jobMaxMinutes = scheduleInfo.jobMaxMinutes;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -338,12 +334,44 @@ export const mutations = {
|
||||||
state.applicationUser.pageData[fmgPageValues.MOLDING_QUESTIONS] = null;
|
state.applicationUser.pageData[fmgPageValues.MOLDING_QUESTIONS] = null;
|
||||||
state.applicationUser.pageData[fmgPageValues.CAPABILITY_QUESTIONS] = null;
|
state.applicationUser.pageData[fmgPageValues.CAPABILITY_QUESTIONS] = null;
|
||||||
},
|
},
|
||||||
|
resetSchedule(state) {
|
||||||
|
state.order.schedule.date = null;
|
||||||
|
state.order.schedule.startTime = null;
|
||||||
|
state.order.schedule.endTime = null;
|
||||||
|
state.order.schedule.routeCode = null;
|
||||||
|
state.order.schedule.jobMaxMinutes = null;
|
||||||
|
|
||||||
|
//early bird fee used on schedule page also needs reset when schedule is reset
|
||||||
|
const supportingItems = state.order.lineItems.supportingItems;
|
||||||
|
const removeEarlyBirdIndex = supportingItems?.findIndex(
|
||||||
|
(item) => item.partType == PREMIUM_FEE_PART_TYPE
|
||||||
|
);
|
||||||
|
|
||||||
|
if (removeEarlyBirdIndex >= 0) {
|
||||||
|
supportingItems.splice(removeEarlyBirdIndex, 1);
|
||||||
|
state.order.lineItems.supportingItems = supportingItems;
|
||||||
|
}
|
||||||
|
},
|
||||||
resetState(state) {
|
resetState(state) {
|
||||||
Object.assign(state, getDefaultState());
|
Object.assign(state, getDefaultState());
|
||||||
},
|
},
|
||||||
resetSaveSessionPromise(state) {
|
resetSaveSessionPromise(state) {
|
||||||
state.applicationUser.saveSessionPromise = null;
|
state.applicationUser.saveSessionPromise = null;
|
||||||
},
|
},
|
||||||
|
resetServiceLocationAppointmentType(state) {
|
||||||
|
state.order.serviceLocation.appointmentType = null;
|
||||||
|
},
|
||||||
|
resetServiceLocationProvider(state) {
|
||||||
|
state.order.serviceLocation.provider = null;
|
||||||
|
},
|
||||||
|
resetServiceLocationMobileAddress(state) {
|
||||||
|
state.order.serviceLocation.address = null;
|
||||||
|
state.order.serviceLocation.address2 = null;
|
||||||
|
state.order.serviceLocation.city = null;
|
||||||
|
state.order.serviceLocation.state = null;
|
||||||
|
state.order.serviceLocation.zipCode = null;
|
||||||
|
state.order.serviceLocation.isVehicleProtected = null;
|
||||||
|
},
|
||||||
// Misc Mutations
|
// Misc Mutations
|
||||||
updateStateWithOrderInformation(state, sessionInformation) {
|
updateStateWithOrderInformation(state, sessionInformation) {
|
||||||
state.order.referralNumber = sessionInformation.order.referralNumber;
|
state.order.referralNumber = sessionInformation.order.referralNumber;
|
||||||
|
|
@ -395,18 +423,20 @@ export const mutations = {
|
||||||
state.order.lineItems.supportingItems = sessionInformation.order.lineItems.supportingItems;
|
state.order.lineItems.supportingItems = sessionInformation.order.lineItems.supportingItems;
|
||||||
state.order.lineItems.vaps = sessionInformation.order.lineItems.vaps;
|
state.order.lineItems.vaps = sessionInformation.order.lineItems.vaps;
|
||||||
state.order.lineItems.serverData = sessionInformation.order.lineItems.serverData;
|
state.order.lineItems.serverData = sessionInformation.order.lineItems.serverData;
|
||||||
|
|
||||||
state.order.payment.parentAccountNumber =
|
state.order.payment.parentAccountNumber =
|
||||||
sessionInformation.order.payment.parentAccountNumber;
|
sessionInformation.order.payment.parentAccountNumber;
|
||||||
(state.order.serviceLocation.address =
|
|
||||||
sessionInformation.order.serviceLocation.streetAddress),
|
state.order.serviceLocation.address =
|
||||||
(state.order.serviceLocation.address2 =
|
sessionInformation.order.serviceLocation.streetAddress;
|
||||||
sessionInformation.order.serviceLocation.address2),
|
state.order.serviceLocation.address2 =
|
||||||
(state.order.serviceLocation.city = sessionInformation.order.serviceLocation.city),
|
sessionInformation.order.serviceLocation.streetAddress2;
|
||||||
(state.order.serviceLocation.state = sessionInformation.order.serviceLocation.state),
|
state.order.serviceLocation.city = sessionInformation.order.serviceLocation.city;
|
||||||
(state.order.serviceLocation.zipCode =
|
state.order.serviceLocation.state = sessionInformation.order.serviceLocation.state;
|
||||||
sessionInformation.order.serviceLocation.zipCode),
|
state.order.serviceLocation.zipCode = sessionInformation.order.serviceLocation.zipCode;
|
||||||
(state.order.serviceLocation.zipCodeCtu =
|
state.order.serviceLocation.zipCodeCtu =
|
||||||
sessionInformation.order.serviceLocation.zipCodeCtu);
|
sessionInformation.order.serviceLocation.zipCodeCtu;
|
||||||
|
|
||||||
state.order.serviceLocation.appointmentType =
|
state.order.serviceLocation.appointmentType =
|
||||||
sessionInformation.order.serviceLocation.appointmentType;
|
sessionInformation.order.serviceLocation.appointmentType;
|
||||||
state.order.serviceLocation.isVehicleProtected =
|
state.order.serviceLocation.isVehicleProtected =
|
||||||
|
|
@ -440,6 +470,7 @@ export const mutations = {
|
||||||
state.order.schedule.startTime = sessionInformation.order.schedule?.startTime;
|
state.order.schedule.startTime = sessionInformation.order.schedule?.startTime;
|
||||||
state.order.schedule.endTime = sessionInformation.order.schedule?.endTime;
|
state.order.schedule.endTime = sessionInformation.order.schedule?.endTime;
|
||||||
state.order.schedule.routeCode = sessionInformation.order.schedule?.routeCode;
|
state.order.schedule.routeCode = sessionInformation.order.schedule?.routeCode;
|
||||||
|
state.order.schedule.jobMaxMinutes = sessionInformation.order.schedule?.jobMaxMinutes;
|
||||||
},
|
},
|
||||||
updateExperiments(state, experiments) {
|
updateExperiments(state, experiments) {
|
||||||
state.applicationUser.experiments = experiments;
|
state.applicationUser.experiments = experiments;
|
||||||
|
|
@ -551,6 +582,7 @@ export const actions = {
|
||||||
payload: {},
|
payload: {},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
lookupVehicleByYmms(context, { year, make, model, style }) {
|
lookupVehicleByYmms(context, { year, make, model, style }) {
|
||||||
return globalMethods.callHttpClient({
|
return globalMethods.callHttpClient({
|
||||||
method: endpoints.LookupVehicleByYmms.method,
|
method: endpoints.LookupVehicleByYmms.method,
|
||||||
|
|
@ -558,6 +590,7 @@ export const actions = {
|
||||||
payload: {},
|
payload: {},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
lookupVehicleByVin(context, { vin }) {
|
lookupVehicleByVin(context, { vin }) {
|
||||||
return globalMethods.callHttpClient({
|
return globalMethods.callHttpClient({
|
||||||
method: endpoints.LookupVehicleByVin.method,
|
method: endpoints.LookupVehicleByVin.method,
|
||||||
|
|
@ -567,6 +600,7 @@ export const actions = {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
lookupVinByPlate(context, { licensePlate, licenseState }) {
|
lookupVinByPlate(context, { licensePlate, licenseState }) {
|
||||||
return globalMethods.callHttpClient({
|
return globalMethods.callHttpClient({
|
||||||
method: endpoints.LookupVinByPlate.method,
|
method: endpoints.LookupVinByPlate.method,
|
||||||
|
|
@ -577,6 +611,7 @@ export const actions = {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
lookupVinByAddress(
|
lookupVinByAddress(
|
||||||
context,
|
context,
|
||||||
{ licenseLastName, licenseStreetAddress, licenseZip, licenseState }
|
{ licenseLastName, licenseStreetAddress, licenseZip, licenseState }
|
||||||
|
|
@ -592,6 +627,7 @@ export const actions = {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
lookupVinByImage(context, image) {
|
lookupVinByImage(context, image) {
|
||||||
const data = new FormData();
|
const data = new FormData();
|
||||||
data.append("vinImage", image);
|
data.append("vinImage", image);
|
||||||
|
|
@ -602,6 +638,7 @@ export const actions = {
|
||||||
isFormData: true,
|
isFormData: true,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
isVinByAddressPermissible(context, zip) {
|
isVinByAddressPermissible(context, zip) {
|
||||||
return globalMethods.callHttpClient({
|
return globalMethods.callHttpClient({
|
||||||
method: endpoints.IsVinByAddressPermissible.method,
|
method: endpoints.IsVinByAddressPermissible.method,
|
||||||
|
|
@ -609,6 +646,7 @@ export const actions = {
|
||||||
payload: {},
|
payload: {},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
getVehicleMakes(context, { year }) {
|
getVehicleMakes(context, { year }) {
|
||||||
return globalMethods.callHttpClient({
|
return globalMethods.callHttpClient({
|
||||||
method: endpoints.GetVehicleMakes.method,
|
method: endpoints.GetVehicleMakes.method,
|
||||||
|
|
@ -616,6 +654,7 @@ export const actions = {
|
||||||
payload: {},
|
payload: {},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
getVehicleModels(context, { year, make }) {
|
getVehicleModels(context, { year, make }) {
|
||||||
return globalMethods.callHttpClient({
|
return globalMethods.callHttpClient({
|
||||||
method: endpoints.GetVehicleModels.method,
|
method: endpoints.GetVehicleModels.method,
|
||||||
|
|
@ -623,6 +662,7 @@ export const actions = {
|
||||||
payload: {},
|
payload: {},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
getVehicleStyles(context, { year, make, model }) {
|
getVehicleStyles(context, { year, make, model }) {
|
||||||
return globalMethods.callHttpClient({
|
return globalMethods.callHttpClient({
|
||||||
method: endpoints.GetVehicleStyles.method,
|
method: endpoints.GetVehicleStyles.method,
|
||||||
|
|
@ -630,6 +670,7 @@ export const actions = {
|
||||||
payload: {},
|
payload: {},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
setVehicle(context, { year, make, model, style }) {
|
setVehicle(context, { year, make, model, style }) {
|
||||||
return globalMethods
|
return globalMethods
|
||||||
.callHttpClient({
|
.callHttpClient({
|
||||||
|
|
@ -652,6 +693,7 @@ export const actions = {
|
||||||
return response;
|
return response;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
getDamageOptions(context, { carId }) {
|
getDamageOptions(context, { carId }) {
|
||||||
return globalMethods.callHttpClient({
|
return globalMethods.callHttpClient({
|
||||||
methods: endpoints.GetDamageOptions.method,
|
methods: endpoints.GetDamageOptions.method,
|
||||||
|
|
@ -659,6 +701,7 @@ export const actions = {
|
||||||
payload: {},
|
payload: {},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
validateZip(context, { zip }) {
|
validateZip(context, { zip }) {
|
||||||
return globalMethods.callHttpClient({
|
return globalMethods.callHttpClient({
|
||||||
methods: endpoints.ValidateZip.method,
|
methods: endpoints.ValidateZip.method,
|
||||||
|
|
@ -673,18 +716,22 @@ export const actions = {
|
||||||
context.commit(storeMutations.UPDATE_SUPPORTING_ITEMS, null);
|
context.commit(storeMutations.UPDATE_SUPPORTING_ITEMS, null);
|
||||||
context.commit(storeMutations.UPDATE_VAPS, null);
|
context.commit(storeMutations.UPDATE_VAPS, null);
|
||||||
},
|
},
|
||||||
|
|
||||||
resetRegistrationAndDependencies(context) {
|
resetRegistrationAndDependencies(context) {
|
||||||
context.commit(storeMutations.RESET_REGISTRATION_STATE);
|
context.commit(storeMutations.RESET_REGISTRATION_STATE);
|
||||||
context.commit(storeMutations.RESET_GLASS_PARTS_STATE);
|
context.commit(storeMutations.RESET_GLASS_PARTS_STATE);
|
||||||
context.commit(storeMutations.UPDATE_SUPPORTING_ITEMS, null);
|
context.commit(storeMutations.UPDATE_SUPPORTING_ITEMS, null);
|
||||||
},
|
},
|
||||||
|
|
||||||
resetPartsAndDependencies(context) {
|
resetPartsAndDependencies(context) {
|
||||||
context.commit(storeMutations.RESET_GLASS_PARTS_STATE);
|
context.commit(storeMutations.RESET_GLASS_PARTS_STATE);
|
||||||
context.commit(storeMutations.UPDATE_SUPPORTING_ITEMS, null);
|
context.commit(storeMutations.UPDATE_SUPPORTING_ITEMS, null);
|
||||||
},
|
},
|
||||||
|
|
||||||
resetState(context) {
|
resetState(context) {
|
||||||
context.commit(storeMutations.RESET_STATE);
|
context.commit(storeMutations.RESET_STATE);
|
||||||
},
|
},
|
||||||
|
|
||||||
resetSaveSessionPromise(context) {
|
resetSaveSessionPromise(context) {
|
||||||
context.commit(storeMutations.RESET_SAVE_SESSION_PROMISE);
|
context.commit(storeMutations.RESET_SAVE_SESSION_PROMISE);
|
||||||
},
|
},
|
||||||
|
|
@ -699,12 +746,14 @@ export const actions = {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
getHomepageName(context) {
|
getHomepageName(context) {
|
||||||
return globalMethods.callHttpClient({
|
return globalMethods.callHttpClient({
|
||||||
method: endpoints.GetHomepageInfo.method,
|
method: endpoints.GetHomepageInfo.method,
|
||||||
endpoint: endpoints.GetHomepageInfo.url(applicationConfig.APPLICATION_ABBREVIATION),
|
endpoint: endpoints.GetHomepageInfo.url(applicationConfig.APPLICATION_ABBREVIATION),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
getPageData(context, { pageName }) {
|
getPageData(context, { pageName }) {
|
||||||
return globalMethods.callHttpClient({
|
return globalMethods.callHttpClient({
|
||||||
method: endpoints.GetPageData.method,
|
method: endpoints.GetPageData.method,
|
||||||
|
|
@ -771,6 +820,7 @@ export const actions = {
|
||||||
context.commit(storeMutations.UPDATE_SAVED_SESSION_ID, savedSessionId);
|
context.commit(storeMutations.UPDATE_SAVED_SESSION_ID, savedSessionId);
|
||||||
context.commit(storeMutations.UPDATE_CRM_CUSTOMER_ID, crmCustomerId);
|
context.commit(storeMutations.UPDATE_CRM_CUSTOMER_ID, crmCustomerId);
|
||||||
},
|
},
|
||||||
|
|
||||||
logPageView(
|
logPageView(
|
||||||
context,
|
context,
|
||||||
{
|
{
|
||||||
|
|
@ -812,6 +862,7 @@ export const actions = {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
logCustomEvent(
|
logCustomEvent(
|
||||||
context,
|
context,
|
||||||
{
|
{
|
||||||
|
|
@ -1116,52 +1167,121 @@ export const actions = {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
getShopTimeSlots(
|
getShopTimeSlots(context, { startDate, endDate, shopAppointmentType, providerNumber }) {
|
||||||
context,
|
|
||||||
{ 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
|
||||||
|
? convertGlassPieceToBackEndCompatibleFormat(order.damage.glassToReplace)
|
||||||
|
: [];
|
||||||
var payload = {
|
var payload = {
|
||||||
providerNumber: order.providerNumber,
|
providerNumber: providerNumber,
|
||||||
startDate: startDate,
|
startDate: startDate,
|
||||||
endDate: endDate,
|
endDate: endDate,
|
||||||
shopAppointmentType: shopAppointmentType,
|
shopAppointmentType: shopAppointmentType,
|
||||||
applicationName: applicationConfig.APPLICATION_NAME,
|
applicationName: applicationConfig.APPLICATION_NAME,
|
||||||
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
|
return globalMethods.callHttpClient({
|
||||||
return globalMethods.callMockHttpClient({
|
|
||||||
method: endpoints.GetShopTimeSlots.method,
|
method: endpoints.GetShopTimeSlots.method,
|
||||||
endpoint: endpoints.GetShopTimeSlots.mockUrl,
|
endpoint: endpoints.GetShopTimeSlots.url,
|
||||||
payload: payload,
|
payload: payload,
|
||||||
});
|
});
|
||||||
// TODO: RESTORE THIS
|
|
||||||
// return globalMethods.callHttpClient({
|
|
||||||
// method: endpoints.GetShopTimeSlots.method,
|
|
||||||
// endpoint: endpoints.GetShopTimeSlots.url,
|
|
||||||
// payload: payload,
|
|
||||||
// logApiCall: false,
|
|
||||||
// });
|
|
||||||
},
|
},
|
||||||
getMobileEarlyBirdFee(context) {
|
|
||||||
|
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
|
||||||
|
? convertGlassPieceToBackEndCompatibleFormat(order.damage.glassToReplace)
|
||||||
|
: [];
|
||||||
|
var payload = {
|
||||||
|
startDate: startDate,
|
||||||
|
endDate: endDate,
|
||||||
|
applicationName: applicationConfig.APPLICATION_NAME,
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
getMobilePremiumFee(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({
|
|
||||||
method: endpoints.GetMobileEarlyBirdFee.method,
|
return globalMethods.callHttpClient({
|
||||||
endpoint: `${endpoints.GetMobileEarlyBirdFee.mockUrl}/Cash/Replace`,
|
method: endpoints.GetMobilePremiumFee.method,
|
||||||
|
endpoint: `${endpoints.GetMobilePremiumFee.url}/${paymentType}/${damageType}`,
|
||||||
});
|
});
|
||||||
// return globalMethods.callHttpClient({
|
|
||||||
// method: endpoints.GetMobileEarlyBirdFee.method,
|
|
||||||
// endpoint: `${endpoints.GetMobileEarlyBirdFee.url}/${paymentType}/${damageType}`,
|
|
||||||
// });
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// Session API Actions
|
// Session API Actions
|
||||||
saveSession(context) {
|
saveSession(context) {
|
||||||
const vehicle = context.getters.vehicle;
|
const vehicle = context.getters.vehicle;
|
||||||
|
|
@ -1243,6 +1363,7 @@ export const actions = {
|
||||||
city: order.serviceLocation.provider?.address?.city,
|
city: order.serviceLocation.provider?.address?.city,
|
||||||
state: order.serviceLocation.provider?.address?.state,
|
state: order.serviceLocation.provider?.address?.state,
|
||||||
zip: order.serviceLocation.provider?.address?.zip,
|
zip: order.serviceLocation.provider?.address?.zip,
|
||||||
|
zipCtu: order.serviceLocation.provider?.address?.zipCtu,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -1251,6 +1372,7 @@ export const actions = {
|
||||||
startTime: order.schedule?.startTime,
|
startTime: order.schedule?.startTime,
|
||||||
endTime: order.schedule?.endTime,
|
endTime: order.schedule?.endTime,
|
||||||
routeCode: order.schedule?.routeCode,
|
routeCode: order.schedule?.routeCode,
|
||||||
|
jobMaxMinutes: order.schedule?.jobMaxMinutes,
|
||||||
},
|
},
|
||||||
existingPromoCode: null,
|
existingPromoCode: null,
|
||||||
referralCorrelationId: order.referralCorrelationId,
|
referralCorrelationId: order.referralCorrelationId,
|
||||||
|
|
@ -1262,6 +1384,7 @@ export const actions = {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
loadSession(
|
loadSession(
|
||||||
context,
|
context,
|
||||||
{
|
{
|
||||||
|
|
@ -1288,7 +1411,7 @@ export const actions = {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then(
|
.then(
|
||||||
(response) => {
|
async (response) => {
|
||||||
// Flatten location and name properties
|
// Flatten location and name properties
|
||||||
response.data.order.damage?.glassToReplace?.map((glass) => {
|
response.data.order.damage?.glassToReplace?.map((glass) => {
|
||||||
glass.glassLocation = glass.location;
|
glass.glassLocation = glass.location;
|
||||||
|
|
@ -1302,10 +1425,13 @@ export const actions = {
|
||||||
if (context.state.order.eon && context.state.order.eon != response.data.eon) {
|
if (context.state.order.eon && context.state.order.eon != response.data.eon) {
|
||||||
context.commit(storeMutations.RESET_STATE);
|
context.commit(storeMutations.RESET_STATE);
|
||||||
}
|
}
|
||||||
|
|
||||||
context.commit(
|
context.commit(
|
||||||
storeMutations.UPDATE_STATE_WITH_ORDER_INFORMATION,
|
storeMutations.UPDATE_STATE_WITH_ORDER_INFORMATION,
|
||||||
response.data
|
response.data
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await resetScheduleIfUnavailable(context, response.data.order);
|
||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
|
|
@ -1339,6 +1465,7 @@ export const actions = {
|
||||||
context.commit(storeMutations.UPDATE_YEAR, year);
|
context.commit(storeMutations.UPDATE_YEAR, year);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
saveVehicleMake(context, make) {
|
saveVehicleMake(context, make) {
|
||||||
//Reset dependent state when changing
|
//Reset dependent state when changing
|
||||||
if (context.state.order.vehicle.make !== make) {
|
if (context.state.order.vehicle.make !== make) {
|
||||||
|
|
@ -1359,6 +1486,7 @@ export const actions = {
|
||||||
context.commit(storeMutations.UPDATE_MAKE, make);
|
context.commit(storeMutations.UPDATE_MAKE, make);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
saveVehicleModel(context, model) {
|
saveVehicleModel(context, model) {
|
||||||
//Reset dependent state when changing
|
//Reset dependent state when changing
|
||||||
if (context.state.order.vehicle.model !== model) {
|
if (context.state.order.vehicle.model !== model) {
|
||||||
|
|
@ -1378,6 +1506,7 @@ export const actions = {
|
||||||
context.commit(storeMutations.UPDATE_MODEL, model);
|
context.commit(storeMutations.UPDATE_MODEL, model);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
saveVehicleStyle(context, style) {
|
saveVehicleStyle(context, style) {
|
||||||
//Reset dependent state when changing
|
//Reset dependent state when changing
|
||||||
if (context.state.order.vehicle.style !== style) {
|
if (context.state.order.vehicle.style !== style) {
|
||||||
|
|
@ -1396,6 +1525,7 @@ export const actions = {
|
||||||
context.commit(storeMutations.UPDATE_STYLE, style);
|
context.commit(storeMutations.UPDATE_STYLE, style);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
saveVehicleDamage(
|
saveVehicleDamage(
|
||||||
context,
|
context,
|
||||||
{ isWindshieldRepair, selectedGlassToReplace, selectedWindshieldChipCount }
|
{ isWindshieldRepair, selectedGlassToReplace, selectedWindshieldChipCount }
|
||||||
|
|
@ -1453,6 +1583,7 @@ export const actions = {
|
||||||
context.commit(storeMutations.UPDATE_REGISTRATION, registrationInfo);
|
context.commit(storeMutations.UPDATE_REGISTRATION, registrationInfo);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
saveRegistrationLicensePlateLookup(
|
saveRegistrationLicensePlateLookup(
|
||||||
context,
|
context,
|
||||||
{ isSelectedGlassAvailableForVehicle, vehicleInfo, registrationInfo }
|
{ isSelectedGlassAvailableForVehicle, vehicleInfo, registrationInfo }
|
||||||
|
|
@ -1474,6 +1605,7 @@ export const actions = {
|
||||||
context.commit(storeMutations.UPDATE_REGISTRATION, registrationInfo);
|
context.commit(storeMutations.UPDATE_REGISTRATION, registrationInfo);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
saveRegistrationAddressLookup(
|
saveRegistrationAddressLookup(
|
||||||
context,
|
context,
|
||||||
{ isSelectedGlassAvailableForVehicle, vehicleInfo, registrationInfo }
|
{ isSelectedGlassAvailableForVehicle, vehicleInfo, registrationInfo }
|
||||||
|
|
@ -1499,6 +1631,7 @@ export const actions = {
|
||||||
context.commit(storeMutations.UPDATE_REGISTRATION, registrationInfo);
|
context.commit(storeMutations.UPDATE_REGISTRATION, registrationInfo);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
savePartQuestionAnswers(context, partQuestionAnswersArray) {
|
savePartQuestionAnswers(context, partQuestionAnswersArray) {
|
||||||
// if part question answers have changed, reset subsequent question answers
|
// if part question answers have changed, reset subsequent question answers
|
||||||
const sortedPreviousResultsArray = sortArrayOfObjectsByPropertyValue(
|
const sortedPreviousResultsArray = sortArrayOfObjectsByPropertyValue(
|
||||||
|
|
@ -1537,6 +1670,7 @@ export const actions = {
|
||||||
//Save new values
|
//Save new values
|
||||||
context.commit(storeMutations.UPDATE_PART_QUESTION_ANSWERS, partQuestionAnswersArray);
|
context.commit(storeMutations.UPDATE_PART_QUESTION_ANSWERS, partQuestionAnswersArray);
|
||||||
},
|
},
|
||||||
|
|
||||||
resetMoldingAndCapabilityQuestionAnswersIfNeeded(context, matchedParts) {
|
resetMoldingAndCapabilityQuestionAnswersIfNeeded(context, matchedParts) {
|
||||||
const partsOrQuestionsDataToCompareWith =
|
const partsOrQuestionsDataToCompareWith =
|
||||||
context.getters.pageData(fmgPageValues.MOLDING_QUESTIONS)?.partsOrQuestions ??
|
context.getters.pageData(fmgPageValues.MOLDING_QUESTIONS)?.partsOrQuestions ??
|
||||||
|
|
@ -1576,6 +1710,7 @@ export const actions = {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
saveMoldingQuestionAnswers(context, moldingQuestionAnswers) {
|
saveMoldingQuestionAnswers(context, moldingQuestionAnswers) {
|
||||||
const sortedPreviousResultsArray = sortArrayOfObjectsByPropertyValue(
|
const sortedPreviousResultsArray = sortArrayOfObjectsByPropertyValue(
|
||||||
context.getters.damage.moldingQuestionAnswers,
|
context.getters.damage.moldingQuestionAnswers,
|
||||||
|
|
@ -1604,6 +1739,7 @@ export const actions = {
|
||||||
//Save new values
|
//Save new values
|
||||||
context.commit(storeMutations.UPDATE_MOLDING_QUESTION_ANSWERS, moldingQuestionAnswers);
|
context.commit(storeMutations.UPDATE_MOLDING_QUESTION_ANSWERS, moldingQuestionAnswers);
|
||||||
},
|
},
|
||||||
|
|
||||||
saveCapabilityQuestionAnswers(context, capabilityQuestionAnswers) {
|
saveCapabilityQuestionAnswers(context, capabilityQuestionAnswers) {
|
||||||
const sortedPreviousResultsArray = sortArrayOfObjectsByPropertyValue(
|
const sortedPreviousResultsArray = sortArrayOfObjectsByPropertyValue(
|
||||||
context.getters.damage.capabilityQuestionAnswers,
|
context.getters.damage.capabilityQuestionAnswers,
|
||||||
|
|
@ -1630,18 +1766,32 @@ export const actions = {
|
||||||
capabilityQuestionAnswers
|
capabilityQuestionAnswers
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
savePaymentType(context, isInsurance) {
|
savePaymentType(context, isInsurance) {
|
||||||
context.commit(storeMutations.UPDATE_IS_INSURANCE, isInsurance);
|
context.commit(storeMutations.UPDATE_IS_INSURANCE, isInsurance);
|
||||||
},
|
},
|
||||||
|
|
||||||
saveParentAccountNumber(context, parentAccountNumber) {
|
saveParentAccountNumber(context, parentAccountNumber) {
|
||||||
context.commit(storeMutations.UPDATE_PARENT_ACCT_NUMBER, parentAccountNumber);
|
context.commit(storeMutations.UPDATE_PARENT_ACCT_NUMBER, parentAccountNumber);
|
||||||
},
|
},
|
||||||
|
|
||||||
saveSupportingItems(context, supportingItems) {
|
saveSupportingItems(context, supportingItems) {
|
||||||
|
if (!deepEqual(supportingItems, context.state.order.lineItems.supportingItems)) {
|
||||||
|
context.commit(storeMutations.RESET_SERVICE_LOCATION_APPOINTMENT_TYPE);
|
||||||
|
context.commit(storeMutations.RESET_SERVICE_LOCATION_PROVIDER);
|
||||||
|
}
|
||||||
|
|
||||||
context.commit(storeMutations.UPDATE_SUPPORTING_ITEMS, supportingItems);
|
context.commit(storeMutations.UPDATE_SUPPORTING_ITEMS, supportingItems);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
saveSupportingItemsSuppressingStateResetting(context, supportingItems) {
|
||||||
|
context.commit(storeMutations.UPDATE_SUPPORTING_ITEMS, supportingItems);
|
||||||
|
},
|
||||||
|
|
||||||
saveVaps(context, vaps) {
|
saveVaps(context, vaps) {
|
||||||
context.commit(storeMutations.UPDATE_VAPS, vaps);
|
context.commit(storeMutations.UPDATE_VAPS, vaps);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Price order actions
|
// Price order actions
|
||||||
async priceOrderItemsAndSaveServerData(
|
async priceOrderItemsAndSaveServerData(
|
||||||
context,
|
context,
|
||||||
|
|
@ -1678,7 +1828,6 @@ export const actions = {
|
||||||
`&${availableLineItemsFormattedForRequest}`;
|
`&${availableLineItemsFormattedForRequest}`;
|
||||||
|
|
||||||
const lineItemServerData = context.getters.order.lineItems.serverData;
|
const lineItemServerData = context.getters.order.lineItems.serverData;
|
||||||
|
|
||||||
if (lineItemServerData) {
|
if (lineItemServerData) {
|
||||||
queryString += `&ServerData=${encodeURIComponent(lineItemServerData)}`;
|
queryString += `&ServerData=${encodeURIComponent(lineItemServerData)}`;
|
||||||
}
|
}
|
||||||
|
|
@ -1694,16 +1843,33 @@ export const actions = {
|
||||||
|
|
||||||
return availableLineItems;
|
return availableLineItems;
|
||||||
},
|
},
|
||||||
|
|
||||||
// Misc order actions
|
// Misc order actions
|
||||||
saveSchedule(context, scheduleInfo) {
|
saveSchedule(context, scheduleInfo) {
|
||||||
context.commit(storeMutations.UPDATE_SCHEDULE, scheduleInfo);
|
context.commit(storeMutations.UPDATE_SCHEDULE, scheduleInfo);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
saveServiceZipCodeInfo(context, serviceZipCodeInfo) {
|
||||||
|
if (
|
||||||
|
context.state.order.serviceLocation &&
|
||||||
|
serviceZipCodeInfo.zipCode !== context.state.order.serviceLocation.zipCode
|
||||||
|
) {
|
||||||
|
context.commit(storeMutations.RESET_SERVICE_LOCATION_APPOINTMENT_TYPE);
|
||||||
|
context.commit(storeMutations.RESET_SERVICE_LOCATION_PROVIDER);
|
||||||
|
context.commit(storeMutations.RESET_SERVICE_LOCATION_MOBILE_ADDRESS);
|
||||||
|
}
|
||||||
|
|
||||||
|
context.commit(storeMutations.UPDATE_SERVICE_LOCATION, serviceZipCodeInfo);
|
||||||
|
},
|
||||||
|
|
||||||
saveServiceLocation(context, serviceLocationInfo) {
|
saveServiceLocation(context, serviceLocationInfo) {
|
||||||
context.commit(storeMutations.UPDATE_SERVICE_LOCATION, serviceLocationInfo);
|
context.commit(storeMutations.UPDATE_SERVICE_LOCATION, serviceLocationInfo);
|
||||||
},
|
},
|
||||||
|
|
||||||
saveEmail(context, email) {
|
saveEmail(context, email) {
|
||||||
context.commit(storeMutations.UPDATE_CUSTOMER_EMAIL_ADDRESS, email);
|
context.commit(storeMutations.UPDATE_CUSTOMER_EMAIL_ADDRESS, email);
|
||||||
},
|
},
|
||||||
|
|
||||||
saveVin(context, { isSelectedGlassAvailableForVehicle, vehicleInfo }) {
|
saveVin(context, { isSelectedGlassAvailableForVehicle, vehicleInfo }) {
|
||||||
//Reset dependent state when changing
|
//Reset dependent state when changing
|
||||||
if (vehicleInfo.vin !== context.state.order.vehicle.vin) {
|
if (vehicleInfo.vin !== context.state.order.vehicle.vin) {
|
||||||
|
|
@ -1716,12 +1882,24 @@ export const actions = {
|
||||||
context.commit(storeMutations.UPDATE_VEHICLE, vehicleInfo);
|
context.commit(storeMutations.UPDATE_VEHICLE, vehicleInfo);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
saveGlassParts(context, parts) {
|
saveGlassParts(context, parts) {
|
||||||
|
if (!deepEqual(parts, context.state.order.lineItems.glassParts)) {
|
||||||
|
context.commit(storeMutations.RESET_SERVICE_LOCATION_APPOINTMENT_TYPE);
|
||||||
|
context.commit(storeMutations.RESET_SERVICE_LOCATION_PROVIDER);
|
||||||
|
}
|
||||||
|
|
||||||
context.commit(storeMutations.UPDATE_GLASS_PARTS, parts);
|
context.commit(storeMutations.UPDATE_GLASS_PARTS, parts);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
saveGlassPartPrices(context, parts) {
|
||||||
|
context.commit(storeMutations.UPDATE_GLASS_PARTS, parts);
|
||||||
|
},
|
||||||
|
|
||||||
clearVin(context) {
|
clearVin(context) {
|
||||||
context.commit(storeMutations.UPDATE_VEHICLE_VIN, null);
|
context.commit(storeMutations.UPDATE_VEHICLE_VIN, null);
|
||||||
},
|
},
|
||||||
|
|
||||||
isVinOptionalVehicle(context) {
|
isVinOptionalVehicle(context) {
|
||||||
switch (context.state.order.vehicle.make.toLowerCase()) {
|
switch (context.state.order.vehicle.make.toLowerCase()) {
|
||||||
case "mercedes benz":
|
case "mercedes benz":
|
||||||
|
|
@ -1863,7 +2041,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 = [
|
||||||
|
|
@ -1885,3 +2063,126 @@ function buildQueryStringParameterFromArrayOfComplexObjects(arrayOfObjects, para
|
||||||
// Remove trailing &
|
// Remove trailing &
|
||||||
return queryStringParameter.slice(0, -1);
|
return queryStringParameter.slice(0, -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function convertGlassPieceToBackEndCompatibleFormat(glassPieces) {
|
||||||
|
return glassPieces.map((glassPiece) => {
|
||||||
|
return {
|
||||||
|
location: glassPiece.glassLocation,
|
||||||
|
name: glassPiece.glassName,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// This function will verify schedule info is still valid.
|
||||||
|
// check to see if we have an appointment date on the order object.
|
||||||
|
// if so, make sure it's not in the past. if in the past, clear schedule info in store.
|
||||||
|
// if date not in past, then call schedule service to verify appointment is still available.
|
||||||
|
async function resetScheduleIfUnavailable(context, order) {
|
||||||
|
if (!order.schedule?.date) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Date string with slashes is parsed as local time, not UTC. Our date has dashes, '-'.
|
||||||
|
// If you put any kind of time stamp on the date string with dashes, then it IS parsed as local time.
|
||||||
|
var aptDate = new Date(order.schedule.date + "T00:00:00");
|
||||||
|
var curDate = new Date();
|
||||||
|
|
||||||
|
// if appointment date is in the past, clear schedule
|
||||||
|
if (aptDate.getTime() < curDate.getTime()) {
|
||||||
|
context.commit(storeMutations.RESET_SCHEDULE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// create date range to pass to the schedule service to see if our appointment is still available.
|
||||||
|
var endRange = new Date(aptDate);
|
||||||
|
endRange.setDate(aptDate.getDate() + 1);
|
||||||
|
|
||||||
|
var endDay = "" + endRange.getDate();
|
||||||
|
var endMonth = "" + (endRange.getMonth() + 1); // 0 based so add 1
|
||||||
|
const endYear = endRange.getFullYear();
|
||||||
|
|
||||||
|
if (endMonth.length < 2) {
|
||||||
|
endMonth = "0" + endMonth;
|
||||||
|
}
|
||||||
|
if (endDay.length < 2) {
|
||||||
|
endDay = "0" + endDay;
|
||||||
|
}
|
||||||
|
|
||||||
|
const endDate = [endYear, endMonth, endDay].join("-");
|
||||||
|
|
||||||
|
let newTimeSlotsResponse;
|
||||||
|
if (order.serviceLocation.appointmentType === AppointmentTypeStrings.MOBILE) {
|
||||||
|
newTimeSlotsResponse = await context.dispatch(
|
||||||
|
storeActions.GET_MOBILE_TIME_SLOTS,
|
||||||
|
{
|
||||||
|
startDate: order.schedule.date,
|
||||||
|
endDate: endDate,
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
if (newTimeSlotsResponse?.data.days?.length === 0) {
|
||||||
|
context.commit(storeMutations.RESET_SCHEDULE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var mobileRouteCodeFound = false;
|
||||||
|
// if early bird fee is in supporting items then need to check the timeslot to see if offer premium is also still available
|
||||||
|
if (
|
||||||
|
order.lineItem?.supportingItems?.findIndex(
|
||||||
|
(item) => item.partType == PREMIUM_FEE_PART_TYPE
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
newTimeSlotsResponse.data.days?.forEach((day) => {
|
||||||
|
day.timeSlots.forEach((ts) => {
|
||||||
|
if (ts.id === order.schedule.routeCode && ts.offerPremium) {
|
||||||
|
mobileRouteCodeFound = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
newTimeSlotsResponse.data.days?.forEach((day) => {
|
||||||
|
day.timeSlots.forEach((ts) => {
|
||||||
|
if (ts.id === order.schedule.routeCode) {
|
||||||
|
mobileRouteCodeFound = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mobileRouteCodeFound) {
|
||||||
|
context.commit(storeMutations.RESET_SCHEDULE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
newTimeSlotsResponse = await context.dispatch(
|
||||||
|
storeActions.GET_SHOP_TIME_SLOTS,
|
||||||
|
{
|
||||||
|
startDate: order.schedule.date,
|
||||||
|
endDate: endDate,
|
||||||
|
shopAppointmentType: order.serviceLocation.appointmentType,
|
||||||
|
providerNumber: order.serviceLocation.provider.providerNumber,
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
if (newTimeSlotsResponse?.data.days?.length === 0) {
|
||||||
|
context.commit(storeMutations.RESET_SCHEDULE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var routeCodeFound = false;
|
||||||
|
newTimeSlotsResponse.data.days?.forEach((day) => {
|
||||||
|
day.timeSlots.forEach((ts) => {
|
||||||
|
if (ts.id === order.schedule.routeCode) {
|
||||||
|
routeCodeFound = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!routeCodeFound) {
|
||||||
|
context.commit(storeMutations.RESET_SCHEDULE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -961,7 +961,10 @@ describe("Actions", () => {
|
||||||
|
|
||||||
it("saveGlassParts, should call mutation", () => {
|
it("saveGlassParts, should call mutation", () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
const context = state;
|
const context = {
|
||||||
|
state: state,
|
||||||
|
};
|
||||||
|
|
||||||
const commit = jest.fn();
|
const commit = jest.fn();
|
||||||
|
|
||||||
context.commit = commit;
|
context.commit = commit;
|
||||||
|
|
|
||||||
|
|
@ -56,3 +56,9 @@ body {
|
||||||
height: calc(100% - 72px);
|
height: calc(100% - 72px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
//Disable scroll of main container when modal is open
|
||||||
|
.modal-open {
|
||||||
|
.page-container-grouped-styles {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 5;
|
z-index: 5;
|
||||||
@include box-shadow-hover($blue-300);
|
@include box-shadow-hover($blue-300);
|
||||||
|
border-radius: 0.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -190,3 +190,6 @@ $alert-color-scale: 40%;
|
||||||
// This affects all [Bootstrap] modals
|
// This affects all [Bootstrap] modals
|
||||||
$modal-fade-transform: translate(0, 100%);
|
$modal-fade-transform: translate(0, 100%);
|
||||||
$modal-backdrop-opacity: 0;
|
$modal-backdrop-opacity: 0;
|
||||||
|
|
||||||
|
//Disable default !important behavior
|
||||||
|
$enable-important-utilities: false;
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@ describe("alert.vue", () => {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const wrapperDiv = wrapper.find("div");
|
const wrapperDiv = wrapper.find("div");
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
|
@ -49,7 +48,7 @@ describe("alert.vue", () => {
|
||||||
expect(wrapper.vm.alertCopy).toBe("testCopy");
|
expect(wrapper.vm.alertCopy).toBe("testCopy");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should container a <router-link> tag if the manualCopy contains a {routerLink: testName, testLink} placeholder", () => {
|
it("Should container a <textBlock> tag if the manualCopy contains a {routerLink: testName, testLink} placeholder", () => {
|
||||||
// Arrange & Act
|
// Arrange & Act
|
||||||
const wrapper = shallowMount(
|
const wrapper = shallowMount(
|
||||||
alert,
|
alert,
|
||||||
|
|
@ -58,11 +57,12 @@ describe("alert.vue", () => {
|
||||||
manualHeadline: "testHeader",
|
manualHeadline: "testHeader",
|
||||||
manualCopy: "testCopy with a {routerLink: testName, testLink} inside of it",
|
manualCopy: "testCopy with a {routerLink: testName, testLink} inside of it",
|
||||||
},
|
},
|
||||||
stubs: ["router-link"],
|
stubs: ["textBlock"],
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(wrapper.find("router-link").exists()).toBe(true);
|
expect(wrapper.html()).toEqual(expect.stringContaining("text-block-stub"));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should contain 'n+1' <p> tags if the body copy has 'n' <p> tags", () => {
|
it("Should contain 'n+1' <p> tags if the body copy has 'n' <p> tags", () => {
|
||||||
|
|
|
||||||
|
|
@ -14,17 +14,11 @@
|
||||||
v-if="!doesCopyContainRouterLink(paragraph) && !doesCopyContainTextLink(paragraph)"
|
v-if="!doesCopyContainRouterLink(paragraph) && !doesCopyContainTextLink(paragraph)"
|
||||||
v-html="paragraph"></p>
|
v-html="paragraph"></p>
|
||||||
<p class="m-0 text-body small" v-else>
|
<p class="m-0 text-body small" v-else>
|
||||||
<template v-for="copy in splitCopyOnCMSPlaceHolder(paragraph)" :key="copy">
|
<template v-if="doesCopyContainRouterLink(paragraph)">
|
||||||
<span v-if="doesCopyContainRouterLink(copy)">
|
<textBlock :customText="paragraph" class="mb-1" marginTopSizeOverride="0" />
|
||||||
<router-link
|
</template>
|
||||||
:to="{
|
<template v-else v-for="copy in splitCopyOnCMSPlaceHolder(paragraph)" :key="copy">
|
||||||
query: { [pageQueryString]: `${getRouterLinkRouteFromCopy(copy)}` },
|
<span v-if="doesCopyContainTextLink(copy)">
|
||||||
name: 'root',
|
|
||||||
}"
|
|
||||||
>{{ getRouterLinkDisplayTextFromCopy(copy) }}</router-link
|
|
||||||
>
|
|
||||||
</span>
|
|
||||||
<span v-else-if="doesCopyContainTextLink(copy)">
|
|
||||||
<textLink
|
<textLink
|
||||||
linkType="text"
|
linkType="text"
|
||||||
:text="getRouterLinkDisplayTextFromCopy(copy)"
|
:text="getRouterLinkDisplayTextFromCopy(copy)"
|
||||||
|
|
@ -59,6 +53,7 @@ import {
|
||||||
} from "@/helpers/cms-content-helper";
|
} from "@/helpers/cms-content-helper";
|
||||||
import textLink from "@/ux-components/text-link/text-link";
|
import textLink from "@/ux-components/text-link/text-link";
|
||||||
import { applicationConfig } from "@/constants/application-config";
|
import { applicationConfig } from "@/constants/application-config";
|
||||||
|
import textBlock from "@/digital-components/text-block/text-block";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "alert",
|
name: "alert",
|
||||||
|
|
@ -136,6 +131,7 @@ export default {
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
textLink,
|
textLink,
|
||||||
|
textBlock,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -94,7 +94,7 @@ export default {
|
||||||
|
|
||||||
+ .list-card-content {
|
+ .list-card-content {
|
||||||
outline: none;
|
outline: none;
|
||||||
display: block;
|
display: flex;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
p {
|
p {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
class="loader"
|
class="loader"
|
||||||
role="alert"
|
role="alert"
|
||||||
aria-label="Loading new page"
|
aria-label="Loading new page"
|
||||||
v-bind:class="[this.loaderColor, this.loaderPosition]"></div>
|
v-bind:class="[this.loaderColor, this.loaderPosition, this.blockUi]"></div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
@ -19,6 +19,11 @@ export default {
|
||||||
loaderPosition: {
|
loaderPosition: {
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
|
/* Controls whether we block the UI when the loader is active or now, defaults to true*/
|
||||||
|
blockUi: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -82,5 +87,9 @@ export default {
|
||||||
&.black:after {
|
&.black:after {
|
||||||
background-color: $black;
|
background-color: $black;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.no-block::before {
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue