Merge branch 'release/submit-cash' into feature/CSR-1370
This commit is contained in:
commit
5c54e975c3
86 changed files with 22509 additions and 1226 deletions
|
|
@ -26,7 +26,7 @@ module.exports = {
|
|||
testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
statements: 80,
|
||||
statements: 75,
|
||||
// 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: {
|
||||
url: "/schedule/api/v1/schedule/shop-time-slots",
|
||||
mockUrl: "https://mockey.qa.sagaws.net/service/shop-time-slots", // TODO: REMOVE MOCKURL
|
||||
method: "POST",
|
||||
},
|
||||
GetMobileEarlyBirdFee: {
|
||||
url: "/parts/api/v1/parts/mobile-early-bird-fee",
|
||||
mockUrl: "https://mockey.qa.sagaws.net/service/mobile-early-bird-fee", // TODO: REMOVE MOCKURL
|
||||
GetMobileTimeSlots: {
|
||||
url: "/schedule/api/v1/schedule/mobile-time-slots",
|
||||
method: "POST",
|
||||
},
|
||||
GetMobilePremiumFee: {
|
||||
url: "/parts/api/v1/parts/mobile-premium-fee",
|
||||
method: "GET",
|
||||
},
|
||||
SaveSession: {
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ const errorMessages = {
|
|||
VEHICLE_REQUIRED: "Please select a vehicle",
|
||||
MOBILE_LOCATION_REQUIRED: "Please enter your service address",
|
||||
DATE_REQUIRED: "Please select a date",
|
||||
PHONE_REQUIRED: "Please enter your phone number",
|
||||
PHONE_FORMAT: "Phone number must be 10 digits",
|
||||
};
|
||||
|
||||
export { errorMessages };
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ const experimentUniverses = {
|
|||
const experimentSettings = {
|
||||
GOOGLE_CUSTOM_DIMENSION_INDEX: "Google Custom Dimension Index",
|
||||
SUPPRESS_VIN_CAPTURE: "SuppressVinCapture",
|
||||
DISPLAY_AVAILABILITY_INDICATORS: "DisplayAvailabilityIndicators",
|
||||
};
|
||||
|
||||
const experimentTriggers = {
|
||||
|
|
|
|||
16
src/constants/schedule-constants.js
Normal file
16
src/constants/schedule-constants.js
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
const AppointmentTypeStrings = {
|
||||
IN_SHOP: "Inshop",
|
||||
MOBILE: "Mobile",
|
||||
DROP_OFF: "Dropoff",
|
||||
};
|
||||
|
||||
const PREMIUM_TIME_SLOT_ID_FLAG = "-premium";
|
||||
|
||||
const PREMIUM_FEE_PART_TYPE = "EARLY BIRD";
|
||||
|
||||
const RouteCodeFlags = {
|
||||
ALL_DAY_DROP_OFF: "ALL DAY DROP OFF",
|
||||
OVERNIGHT_DROP_OFF: "OVERNIGHT DROP OFF",
|
||||
};
|
||||
|
||||
export { AppointmentTypeStrings, PREMIUM_TIME_SLOT_ID_FLAG, PREMIUM_FEE_PART_TYPE, RouteCodeFlags };
|
||||
18896
src/constants/single-windshield-carids.js
Normal file
18896
src/constants/single-windshield-carids.js
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -33,8 +33,9 @@ const storeActions = {
|
|||
GET_MOBILE_FEE_PART: "getMobileFeePart",
|
||||
GET_SERVICEABILITY_DETAILS: "getServiceabilityDetails",
|
||||
GET_SHOP_TIME_SLOTS: "getShopTimeSlots",
|
||||
GET_MOBILE_TIME_SLOTS: "getMobileTimeSlots",
|
||||
GET_PROVIDERS: "getProviders",
|
||||
GET_MOBILE_EARLY_BIRD_FEE: "getMobileEarlyBirdFee",
|
||||
GET_MOBILE_PREMIUM_FEE: "getMobilePremiumFee",
|
||||
SAVE_SESSION: "saveSession",
|
||||
LOAD_SESSION: "loadSession",
|
||||
UPDATE_STORE_WITH_SAVE_SESSION_RESPONSE: "updateStoreWithSaveSessionResponse",
|
||||
|
|
@ -53,6 +54,7 @@ const storeActions = {
|
|||
RESET_DAMAGE_STATE_AND_DEPENDENCIES: "resetDamageAndDependencies",
|
||||
RESET_REGISTRATION_STATE_AND_DEPENDENCIES: "resetRegistrationAndDependencies",
|
||||
RESET_PARTS_STATE_AND_DEPENDENCIES: "resetPartsAndDependencies",
|
||||
RESET_SERVICE_LOCATION_STATE_AND_DEPENDENCIES: "resetServiceLocationAndDependencies",
|
||||
RESET_STATE: "resetState",
|
||||
|
||||
// SAVE COMPONENT STATE
|
||||
|
|
@ -62,6 +64,7 @@ const storeActions = {
|
|||
SAVE_VEHICLE_STYLE: "saveVehicleStyle",
|
||||
SAVE_VEHICLE_DAMAGE: "saveVehicleDamage",
|
||||
SAVE_VIN_LOOKUP: "saveVinLookup",
|
||||
SAVE_SERVICE_ZIP_CODE_INFO: "saveServiceZipCodeInfo",
|
||||
SAVE_SERVICE_LOCATION: "saveServiceLocation",
|
||||
SAVE_SCHEDULE: "saveSchedule",
|
||||
SAVE_EMAIL: "saveEmail",
|
||||
|
|
@ -69,6 +72,7 @@ const storeActions = {
|
|||
SAVE_VIN: "saveVin",
|
||||
SAVE_REGISTRATION_ADDRESS_LOOKUP: "saveRegistrationAddressLookup",
|
||||
SAVE_GLASS_PARTS: "saveGlassParts",
|
||||
SAVE_GLASS_PART_PRICES: "saveGlassPartPrices",
|
||||
SAVE_PART_QUESTION_ANSWERS: "savePartQuestionAnswers",
|
||||
RESET_MOLDING_AND_CAPABILITY_QUESTIONS_IF_NEEDED:
|
||||
"resetMoldingAndCapabilityQuestionAnswersIfNeeded",
|
||||
|
|
@ -77,7 +81,10 @@ const storeActions = {
|
|||
SAVE_PAYMENT_TYPE: "savePaymentType",
|
||||
SAVE_PARENT_ACCOUNT_NUMBER: "saveParentAccountNumber",
|
||||
SAVE_SUPPORTING_ITEMS: "saveSupportingItems",
|
||||
SAVE_SUPPORTING_ITEMS_SUPPRESSING_STATE_RESETTING:
|
||||
"saveSupportingItemsSuppressingStateResetting",
|
||||
SAVE_VAPS: "saveVaps",
|
||||
SAVE_CUSTOMER_DETAILS: "saveCustomerDetails",
|
||||
};
|
||||
|
||||
export { storeActions };
|
||||
|
|
|
|||
|
|
@ -32,11 +32,15 @@ const storeMutations = {
|
|||
UPDATE_REGISTRATION_LAST_NAME: "updateRegistrationLastName",
|
||||
UPDATE_REGISTRATION: "updateRegistration",
|
||||
|
||||
UPDATE_SERVICE_ZIP: "updateServiceZip",
|
||||
UPDATE_SERVICE_LOCATION: "updateServiceLocation",
|
||||
UPDATE_SCHEDULE: "updateSchedule",
|
||||
|
||||
UPDATE_CUSTOMER_EMAIL_ADDRESS: "updateCustomerEmailAddress",
|
||||
|
||||
//CUSTOMER MUTATIONS
|
||||
UPDATE_CUSTOMER_DETAILS: "updateCustomerDetails",
|
||||
|
||||
// ORDER MUTATIONS
|
||||
UPDATE_REFERRAL_NUMBER: "updateReferralNumber",
|
||||
UPDATE_REFERRAL_DATE: "updateReferralDate",
|
||||
|
|
@ -58,6 +62,10 @@ const storeMutations = {
|
|||
RESET_GLASS_PARTS_STATE: "resetGlassPartsState",
|
||||
RESET_STATE: "resetState",
|
||||
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
|
||||
UPDATE_PAGE_DATA: "updatePageData",
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
import { shallowMount, mount } from "@vue/test-utils";
|
||||
import buttonQuestion from "./button-question";
|
||||
import { getMountOptions } from "@/helpers/unit-test-helper.js";
|
||||
import crypto from "crypto";
|
||||
|
||||
global.crypto = crypto;
|
||||
|
||||
describe("buttonQuestion.vue", () => {
|
||||
it("Should show overflow classes on fieldset if isOverflowScrollable is true", () => {
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ https://safelite.atlassian.net/wiki/spaces/DC/pages/76644418/Button+Question+Com
|
|||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="row form-test-error mt-1">
|
||||
<div class="row form-test-error">
|
||||
<error-message
|
||||
class="small"
|
||||
:name="formatString(groupName)"
|
||||
|
|
@ -189,7 +189,7 @@ export default {
|
|||
classes = "d-flex flex-row p-0";
|
||||
break;
|
||||
case "listCard":
|
||||
classes = "row g-2 justify-content-center";
|
||||
classes = "row g-2 justify-content-center mb-1";
|
||||
if (this.isWide) {
|
||||
classes += " flex-column";
|
||||
}
|
||||
|
|
@ -256,7 +256,9 @@ export default {
|
|||
},
|
||||
watch: {
|
||||
modelValue(newValue, oldValue) {
|
||||
this.resetField();
|
||||
this.resetField({
|
||||
value: newValue,
|
||||
});
|
||||
},
|
||||
answers() {
|
||||
//once we get the answers to display from parent, see if we need a GA event to log what we showed
|
||||
|
|
|
|||
|
|
@ -1,14 +1,25 @@
|
|||
import { shallowMount } from "@vue/test-utils";
|
||||
import checkbox from "./checkbox";
|
||||
import checkboxQuestion from "./checkbox-question";
|
||||
import { nextTick } from "vue";
|
||||
|
||||
describe("checkbox.vue", () => {
|
||||
// Mock CMS content
|
||||
const questionText = "Question Text";
|
||||
const mockMixin = {
|
||||
methods: {
|
||||
getCmsContent: jest.fn().mockImplementation(() => {
|
||||
return questionText;
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
describe("checkbox-question.vue", () => {
|
||||
it("Should return checkbox name", async () => {
|
||||
// Act
|
||||
const wrapper = shallowMount(checkbox, {
|
||||
const wrapper = shallowMount(checkboxQuestion, {
|
||||
propsData: {
|
||||
checkboxName: "Checkbox",
|
||||
},
|
||||
mixins: [mockMixin],
|
||||
});
|
||||
|
||||
// Assert
|
||||
|
|
@ -20,10 +31,11 @@ describe("checkbox.vue", () => {
|
|||
|
||||
it("Should return checkbox id", async () => {
|
||||
// Act
|
||||
const wrapper = shallowMount(checkbox, {
|
||||
const wrapper = shallowMount(checkboxQuestion, {
|
||||
propsData: {
|
||||
buttonID: "Checkbox ID",
|
||||
},
|
||||
mixins: [mockMixin],
|
||||
});
|
||||
|
||||
// Assert
|
||||
|
|
@ -35,10 +47,11 @@ describe("checkbox.vue", () => {
|
|||
|
||||
it("Should return tabindex value", async () => {
|
||||
// Act
|
||||
const wrapper = shallowMount(checkbox, {
|
||||
const wrapper = shallowMount(checkboxQuestion, {
|
||||
propsData: {
|
||||
tabIndex: "1",
|
||||
},
|
||||
mixins: [mockMixin],
|
||||
});
|
||||
|
||||
// Assert
|
||||
|
|
@ -50,24 +63,11 @@ describe("checkbox.vue", () => {
|
|||
|
||||
it("Should return label text", async () => {
|
||||
// Act
|
||||
const wrapper = shallowMount(checkbox, {
|
||||
propsData: {
|
||||
checkboxLabel: "label text",
|
||||
},
|
||||
});
|
||||
|
||||
// Assert
|
||||
const paragraph = wrapper.find("p");
|
||||
|
||||
expect(paragraph.text()).toEqual("label text");
|
||||
});
|
||||
|
||||
it("Should return label text", async () => {
|
||||
// Act
|
||||
const wrapper = shallowMount(checkbox, {
|
||||
const wrapper = shallowMount(checkboxQuestion, {
|
||||
propsData: {
|
||||
screenReaderOnlyText: "screenreader text",
|
||||
},
|
||||
mixins: [mockMixin],
|
||||
});
|
||||
|
||||
// Assert
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
<!-- Checkbox groups MUST be wrapped in a <fieldset> and <legend> tag -->
|
||||
<div class="form-check ui-checkbox" :class="[hasError ? 'has-error' : '']">
|
||||
<input
|
||||
v-model="value"
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
aria-checked="false"
|
||||
|
|
@ -10,7 +11,7 @@
|
|||
:tabindex="tabIndex"
|
||||
:aria-required="isRequired" />
|
||||
<label class="d-flex align-items-start" :for="buttonID">
|
||||
<p v-if="checkboxLabel" class="m-0">{{ checkboxLabel }}</p>
|
||||
<p v-html="checkboxLabelCopy" class="m-0"></p>
|
||||
<span v-if="screenReaderOnlyText" class="sr-only">{{ screenReaderOnlyText }}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
|
@ -18,15 +19,32 @@
|
|||
|
||||
<script>
|
||||
export default {
|
||||
name: "checkbox",
|
||||
name: "checkboxQuestion",
|
||||
computed: {
|
||||
checkboxLabelCopy() {
|
||||
return this.getCmsContent(this.cmsWidgetName, "QuestionText");
|
||||
},
|
||||
value: {
|
||||
get: function () {
|
||||
return this.modelValue;
|
||||
},
|
||||
set: function (newValue) {
|
||||
this.$emit("update:modelValue", newValue);
|
||||
},
|
||||
},
|
||||
},
|
||||
props: {
|
||||
cmsWidgetName: String,
|
||||
checkboxName: String,
|
||||
buttonID: String,
|
||||
tabIndex: Number,
|
||||
checkboxLabel: String,
|
||||
screenReaderOnlyText: String,
|
||||
isRequired: Boolean,
|
||||
hasError: Boolean,
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -22,7 +22,6 @@
|
|||
<div class="separator-line"></div>
|
||||
<div class="nav-back ps-3"><button></button></div>
|
||||
<div class="nav-forward pe-3"><button></button></div>
|
||||
<!-- TODO Accessibility: Do the days of the week need to be read? -->
|
||||
<div class="grid-item caption"><span class="sr-only">Sunday</span>S</div>
|
||||
<div class="grid-item caption"><span class="sr-only">Monday</span>M</div>
|
||||
<div class="grid-item caption"><span class="sr-only">Tuesday</span>T</div>
|
||||
|
|
@ -32,8 +31,8 @@
|
|||
<div class="grid-item caption"><span class="sr-only">Saturday</span>S</div>
|
||||
<div
|
||||
v-for="date in month.dates"
|
||||
:key="date.inputValue.dateString"
|
||||
:id="date.inputValue.dateString"
|
||||
:key="date.inputValue"
|
||||
:id="date.inputValue"
|
||||
class="grid-item radio-wrapper"
|
||||
:class="[
|
||||
date.dateNum === 1 ? 'first-day-' + month.startDateDayIndex : '',
|
||||
|
|
@ -84,7 +83,6 @@ export default {
|
|||
months: null,
|
||||
disableViewMoreDatesButton: false,
|
||||
selectableDatesData: [], // NOTE: uses monthNum (1-based), NOT monthIndex (0-based)
|
||||
today: null,
|
||||
hideSomeDaysForInitialView: null,
|
||||
};
|
||||
},
|
||||
|
|
@ -112,6 +110,12 @@ export default {
|
|||
},
|
||||
},
|
||||
computed: {
|
||||
today() {
|
||||
if (this.todayOverrideDateString) {
|
||||
return new Date(this.todayOverrideDateString + "T00:00:00");
|
||||
}
|
||||
return new Date();
|
||||
},
|
||||
todayMonthIndex() {
|
||||
return this.today.getMonth() + 1;
|
||||
},
|
||||
|
|
@ -147,43 +151,41 @@ export default {
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
initializeComponent(initialData) {
|
||||
this.setCalendarData(initialData);
|
||||
},
|
||||
fireDateClickedEvent() {
|
||||
this.$emit("date-clicked");
|
||||
},
|
||||
getWeekStartDate(date) {
|
||||
// Get the day of the week for date
|
||||
let dayOfWeek = date.getDay();
|
||||
|
||||
const dayOfWeek = date.getDay();
|
||||
// Subtract the day of the week from date to get the date of Sunday
|
||||
let sunday = new Date(date);
|
||||
const sunday = new Date(date);
|
||||
sunday.setDate(sunday.getDate() - dayOfWeek);
|
||||
|
||||
// Return the date of Sunday
|
||||
return sunday;
|
||||
},
|
||||
getWeekEndDate(date) {
|
||||
const currentDay = date.getDay(); // Get the day of the week (0 = Sunday, 1 = Monday, etc.)
|
||||
const daysUntilSaturday = 6 - currentDay; // Calculate the number of days until Saturday
|
||||
|
||||
const dayOfWeek = date.getDay();
|
||||
const daysUntilSaturday = 6 - dayOfWeek; // Calculate the number of days until Saturday
|
||||
// Clone the given date and add the remaining days until Saturday
|
||||
const saturday = new Date(date);
|
||||
saturday.setDate(date.getDate() + daysUntilSaturday);
|
||||
|
||||
return saturday;
|
||||
},
|
||||
getNextWeekSunday(date) {
|
||||
const currentDay = date.getDay(); // Get the day of the week (0 = Sunday, 1 = Monday, etc.)
|
||||
const daysUntilNextSunday = currentDay === 0 ? 7 : 7 - currentDay; // Calculate the number of days until the next Sunday
|
||||
|
||||
const dayOfWeek = date.getDay();
|
||||
const daysUntilNextSunday = 7 - dayOfWeek; // Calculate the number of days until the next Sunday
|
||||
// Clone the given date and add the remaining days until Sunday
|
||||
const nextSunday = new Date(date);
|
||||
nextSunday.setDate(date.getDate() + daysUntilNextSunday);
|
||||
|
||||
return nextSunday;
|
||||
},
|
||||
getInitialViewWeeks(today, initialViewRowsToShow) {
|
||||
// TODO: this only is for future direction; create logic for past direction
|
||||
let weeks = [];
|
||||
getInitialViewWeeks(today, initialViewRowsToShow, preSelectedDateString) {
|
||||
// TODO: this only is for future direction; need to create logic for past direction
|
||||
const weeks = [];
|
||||
|
||||
if (preSelectedDateString) initialViewRowsToShow = 26;
|
||||
|
||||
let weekStartDate = this.getWeekStartDate(today);
|
||||
let weekEndDate = this.getWeekEndDate(today);
|
||||
for (let i = 0; i < initialViewRowsToShow; i++) {
|
||||
|
|
@ -196,19 +198,77 @@ export default {
|
|||
weekStartDate: weekStartDate,
|
||||
weekEndDate: weekEndDate,
|
||||
});
|
||||
if (
|
||||
preSelectedDateString &&
|
||||
new Date(preSelectedDateString + "T00:00:00") < weekEndDate
|
||||
) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// are any of these weeks split between two months?
|
||||
// NOTE: a week split between two months counts as 2 weeks
|
||||
const hasSplitWeek = (week) => {
|
||||
return week.weekStartDate.getMonth() !== week.weekEndDate.getMonth() ? true : false;
|
||||
};
|
||||
const splitWeekIndex = weeks.findIndex(hasSplitWeek);
|
||||
|
||||
if (!preSelectedDateString && splitWeekIndex > -1) {
|
||||
// a preSelectedDateString precludes split week logic
|
||||
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;
|
||||
},
|
||||
async loadInitialData(config) {
|
||||
// CALLED FROM CONSUMING COMPONENT BEFORE DATE-PICKER APPEARS
|
||||
const todayDate = config.todayOverrideDateString
|
||||
? new Date(config.todayOverrideDateString)
|
||||
: new Date();
|
||||
let todayDate;
|
||||
if (this.today) {
|
||||
todayDate = this.today;
|
||||
} else if (config.todayOverrideDateString) {
|
||||
todayDate = new Date(config.todayOverrideDateString + "T00:00:00");
|
||||
} else {
|
||||
todayDate = new Date();
|
||||
}
|
||||
|
||||
let todayMonthIndex = todayDate.getMonth() + 1;
|
||||
let todayYearNum = todayDate.getFullYear();
|
||||
let currentMonthStart = new Date(todayYearNum, todayMonthIndex - 1, 1);
|
||||
let currentMonthEnd = new Date(todayYearNum, todayMonthIndex, 0);
|
||||
const todayMonthIndex = todayDate.getMonth() + 1;
|
||||
const todayYearNum = todayDate.getFullYear();
|
||||
// TODO - set up currentMonthStart if direction is PAST:
|
||||
// let currentMonthStart = new Date(todayYearNum, todayMonthIndex - 1, 1);
|
||||
const currentMonthEnd = new Date(todayYearNum, todayMonthIndex, 0);
|
||||
|
||||
let calendarViewDirection = "none";
|
||||
if (config.selectableDatesSetting === "past") calendarViewDirection = "past";
|
||||
|
|
@ -216,24 +276,23 @@ export default {
|
|||
|
||||
const initialViewWeeks = this.getInitialViewWeeks(
|
||||
todayDate,
|
||||
config.initialViewRowsToShow
|
||||
config.initialViewRowsToShow,
|
||||
config.preSelectedDate
|
||||
);
|
||||
|
||||
let initialViewStartDate = todayDate;
|
||||
let initialViewEndDate = initialViewWeeks[initialViewWeeks.length - 1].weekEndDate;
|
||||
let saturday1month = initialViewWeeks[0].weekEndDate.getMonth();
|
||||
let sunday5month =
|
||||
const initialViewStartDate = todayDate;
|
||||
const initialViewEndDate = initialViewWeeks[initialViewWeeks.length - 1].weekEndDate;
|
||||
const firstSaturdayMonth = initialViewWeeks[0].weekEndDate.getMonth();
|
||||
const lastSundayMonth =
|
||||
initialViewWeeks[initialViewWeeks.length - 1].weekStartDate.getMonth();
|
||||
|
||||
let hideSomeDaysForInitialView = false;
|
||||
let hideSecondMonth = false;
|
||||
|
||||
// TODO - NEED TO CREATE OPPOSITE LOGIC FOR "past" DIRECTION LIKE BELOW vvvvv
|
||||
if (calendarViewDirection === "future") {
|
||||
if (saturday1month !== sunday5month) {
|
||||
// TODO - NEED TO CREATE OPPOSITE LOGIC FOR "past" DIRECTION LIKE BELOW v v v
|
||||
if (calendarViewDirection === "future" && !config.preSelectedDate) {
|
||||
if (firstSaturdayMonth !== lastSundayMonth) {
|
||||
hideSomeDaysForInitialView = true;
|
||||
}
|
||||
if (initialViewStartDate.getMonth() === sunday5month) {
|
||||
if (initialViewStartDate.getMonth() === lastSundayMonth) {
|
||||
hideSecondMonth = true;
|
||||
if (currentMonthEnd > initialViewEndDate) {
|
||||
// should part of 1st month be hidden?
|
||||
|
|
@ -242,16 +301,17 @@ export default {
|
|||
}
|
||||
}
|
||||
|
||||
let myPromise = new Promise((resolve, reject) => {
|
||||
const loadInitialDataPromise = new Promise((resolve, reject) => {
|
||||
const response = config.customSelectableDatesCallback(
|
||||
initialViewStartDate,
|
||||
initialViewEndDate,
|
||||
store.getters.order.serviceLocation.appointmentType
|
||||
initialViewStartDate.toISOString().split("T")[0],
|
||||
initialViewEndDate.toISOString().split("T")[0],
|
||||
store.getters.order.serviceLocation.appointmentType,
|
||||
store.getters.order.serviceLocation.provider.providerNumber
|
||||
);
|
||||
resolve(response);
|
||||
});
|
||||
|
||||
return myPromise.then((response) => {
|
||||
return loadInitialDataPromise.then((response) => {
|
||||
const initialData = {
|
||||
todayDate: todayDate,
|
||||
initialViewStartDate: initialViewStartDate,
|
||||
|
|
@ -260,58 +320,17 @@ export default {
|
|||
initialShopTimeSlotsResponse: response,
|
||||
hideSomeDaysForInitialView: hideSomeDaysForInitialView,
|
||||
hideSecondMonth: hideSecondMonth,
|
||||
preSelectedDate: config.preSelectedDate,
|
||||
};
|
||||
return initialData;
|
||||
});
|
||||
},
|
||||
initializeComponent(initialData) {
|
||||
this.setCalendarData(initialData);
|
||||
},
|
||||
scrollToElement(elementId, speed, easing) {
|
||||
// TODO - needs to be cleaned up & refactored
|
||||
function scrollTopSmooth(wrapper, target, duration = 300, timingName = "linear") {
|
||||
const initY = wrapper.scrollTop;
|
||||
const wrapperRect = wrapper.getBoundingClientRect();
|
||||
const targetRect = target.getBoundingClientRect();
|
||||
const targetY = targetRect.top - wrapperRect.top - BUFFER_OFFSET;
|
||||
const timingFunc = TIMINGFUNC_MAP[timingName];
|
||||
|
||||
let start = null;
|
||||
|
||||
const step = (timestamp) => {
|
||||
start = start || timestamp;
|
||||
const progress = timestamp - start,
|
||||
// Growing from 0 to 1
|
||||
time = Math.min(1, (timestamp - start) / duration);
|
||||
|
||||
let percentageNew = timingFunc(time);
|
||||
let distanceToGo = targetY;
|
||||
let thisDistance = percentageNew * distanceToGo;
|
||||
|
||||
wrapper.scrollTo(0, initY + thisDistance);
|
||||
|
||||
if (percentageNew < 1) {
|
||||
window.requestAnimationFrame(step);
|
||||
}
|
||||
};
|
||||
|
||||
window.requestAnimationFrame(step);
|
||||
}
|
||||
|
||||
const wrapper = this.$refs.datePickerFieldset;
|
||||
const targetMonth = document.getElementById(elementId);
|
||||
|
||||
scrollTopSmooth(wrapper, targetMonth, 800, "ease-in-out");
|
||||
},
|
||||
|
||||
async setCalendarData(config = {}) {
|
||||
this.today = config.todayDate;
|
||||
this.hideSomeDaysForInitialView = config.hideSomeDaysForInitialView;
|
||||
let hideSecondMonth = config.hideSecondMonth;
|
||||
const hideSecondMonth = config.hideSecondMonth;
|
||||
const direction = config.calendarViewDirection;
|
||||
|
||||
const monthsAfterToLoadOffset = 12; // TO BE MADE "CONSTANTS"
|
||||
const monthsBeforeToLoadOffset = 36; // TO BE MADE "CONSTANTS"
|
||||
const monthsAfterToLoadOffset = 12;
|
||||
const monthsBeforeToLoadOffset = 36;
|
||||
config.initialShopTimeSlotsResponse.days.forEach((selectableDate) => {
|
||||
this.selectableDatesData.push(selectableDate);
|
||||
});
|
||||
|
|
@ -325,6 +344,7 @@ export default {
|
|||
initialViewStartDate: config.initialViewStartDate,
|
||||
initialViewEndDate: config.initialViewEndDate,
|
||||
hideSecondMonth: hideSecondMonth,
|
||||
preSelectedDate: config.preSelectedDate,
|
||||
};
|
||||
if (direction === "future") {
|
||||
// first 0, then 1
|
||||
|
|
@ -344,8 +364,17 @@ export default {
|
|||
}
|
||||
this.months = months;
|
||||
this.isLoading = false;
|
||||
},
|
||||
|
||||
if (config.preSelectedDate) {
|
||||
this.$nextTick(() => {
|
||||
//Advance to month
|
||||
const monthToShow = this.months.find((month) =>
|
||||
month.monthClass.includes("month-preselected")
|
||||
);
|
||||
this.scrollToElement(monthToShow.monthString);
|
||||
});
|
||||
}
|
||||
},
|
||||
async getMonthData(offset = requiredParameter(), options) {
|
||||
/* options will contain:
|
||||
calendarViewDirection (string)
|
||||
|
|
@ -354,6 +383,7 @@ export default {
|
|||
monthsBeforeToLoadOffset (number),
|
||||
monthsAfterToLoadOffset (number),
|
||||
hideSecondMonth (boolean),
|
||||
preSelectedDate (string)
|
||||
|
||||
data used:
|
||||
todayDate (date object)
|
||||
|
|
@ -365,7 +395,10 @@ export default {
|
|||
const calendarViewDirection = options.calendarViewDirection;
|
||||
const initialViewStartDate = options.initialViewStartDate;
|
||||
const initialViewEndDate = options.initialViewEndDate;
|
||||
const hideSecondMonth = options.hideSecondMonth; // <<<<<<<<<<<<<
|
||||
const hideSecondMonth = options.hideSecondMonth;
|
||||
const preSelectedDateObj = options.preSelectedDate
|
||||
? new Date(options.preSelectedDate + "T00:00:00")
|
||||
: null;
|
||||
const dates = [];
|
||||
let monthClass = "";
|
||||
let isMonthThatHidesSomeDaysForInitialView;
|
||||
|
|
@ -382,31 +415,43 @@ export default {
|
|||
}
|
||||
}
|
||||
|
||||
const monthEndDate = new Date(yearNum, monthIndex, 0); // BOTH
|
||||
let monthEndDateNum = monthEndDate.getDate(); // BOTH
|
||||
const monthEndDate = new Date(yearNum, monthIndex, 0);
|
||||
let monthEndDateNum = monthEndDate.getDate();
|
||||
|
||||
if (
|
||||
offset === 0 &&
|
||||
calendarViewDirection === "past" &&
|
||||
monthEndDateNum > this.currentWeekEndDateNum
|
||||
) {
|
||||
monthEndDateNum = this.currentWeekEndDateNum; // PAST
|
||||
monthEndDateNum = this.currentWeekEndDateNum;
|
||||
}
|
||||
|
||||
const monthStartDateNum =
|
||||
offset === 0 && calendarViewDirection === "future"
|
||||
? this.currentWeekStartDateNum
|
||||
: 1; // FUTURE
|
||||
const monthStartDate = new Date(yearNum, monthIndex - 1, monthStartDateNum); // BOTH
|
||||
: 1;
|
||||
const monthStartDate = new Date(yearNum, monthIndex - 1, monthStartDateNum);
|
||||
|
||||
const startDateDayIndex = monthStartDate.getDay(); // FUTURE
|
||||
const endDateDayIndex = monthEndDate.getDay(); // PAST
|
||||
const startDateDayIndex = monthStartDate.getDay();
|
||||
const endDateDayIndex = monthEndDate.getDay();
|
||||
|
||||
if (Math.abs(offset) === 1 && hideSecondMonth) {
|
||||
monthClass = monthClass + " month-hidden";
|
||||
} else if (Math.abs(offset) > 1) {
|
||||
monthClass = monthClass + " month-hidden";
|
||||
if (preSelectedDateObj) {
|
||||
if (
|
||||
monthStartDate.getFullYear() === preSelectedDateObj.getFullYear() &&
|
||||
monthStartDate.getMonth() === preSelectedDateObj.getMonth()
|
||||
) {
|
||||
monthClass = monthClass + " month-preselected";
|
||||
} else if (monthStartDate > preSelectedDateObj) {
|
||||
monthClass = monthClass + " month-hidden";
|
||||
}
|
||||
} else {
|
||||
if (Math.abs(offset) === 1 && hideSecondMonth) {
|
||||
monthClass = monthClass + " month-hidden";
|
||||
} else if (Math.abs(offset) > 1) {
|
||||
monthClass = monthClass + " month-hidden";
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
Math.abs(offset) === options.monthsAfterToLoadOffset &&
|
||||
calendarViewDirection === "future"
|
||||
|
|
@ -424,19 +469,13 @@ export default {
|
|||
// populate dates array
|
||||
for (let i = monthStartDateNum; i <= monthEndDateNum; i++) {
|
||||
let dayClasses = "";
|
||||
let dateString =
|
||||
const dateString =
|
||||
yearNum.toString() +
|
||||
"-" +
|
||||
forceTwoDigitString(monthIndex) +
|
||||
"-" +
|
||||
forceTwoDigitString(i);
|
||||
|
||||
const thisDate = {
|
||||
year: yearNum,
|
||||
month: monthIndex,
|
||||
date: i,
|
||||
dateString: dateString,
|
||||
};
|
||||
if (offset === 0 && i === this.todayDateNum) {
|
||||
dayClasses += "current-day";
|
||||
}
|
||||
|
|
@ -457,11 +496,9 @@ export default {
|
|||
const dateObject = {
|
||||
dateNum: i,
|
||||
dayClasses: dayClasses,
|
||||
inputValue: thisDate,
|
||||
inputValue: dateString,
|
||||
isSelectable:
|
||||
this.selectableDatesData.findIndex(
|
||||
(date) => date.dateString === dateString
|
||||
) > -1
|
||||
this.selectableDatesData.findIndex((date) => date.date === dateString) > -1
|
||||
? true
|
||||
: false,
|
||||
};
|
||||
|
|
@ -510,11 +547,11 @@ export default {
|
|||
if (monthToShow) {
|
||||
// make new API call with this month's start and end dates
|
||||
await this.updateSelectableDates(
|
||||
monthToShow.dates[monthStartDateNum].inputValue.dateString,
|
||||
monthToShow.dates[monthToShow.dates.length - 1].inputValue.dateString
|
||||
monthToShow.dates[monthStartDateNum].inputValue,
|
||||
monthToShow.dates[monthToShow.dates.length - 1].inputValue
|
||||
);
|
||||
this.isLoading = false;
|
||||
this.hideSomeDaysForInitialView = false; // if hid days on initial partial view, this removes hidden styling on days
|
||||
this.hideSomeDaysForInitialView = false; // if hid days on initial partial view, this will reveal those days
|
||||
monthToShow.monthClass = monthToShow.monthClass.replace(" month-hidden", "");
|
||||
this.scrollToElement(monthToShow.monthString);
|
||||
|
||||
|
|
@ -526,23 +563,56 @@ export default {
|
|||
const moreSelectableDates = await this.customSelectableDatesCallback(
|
||||
monthStart,
|
||||
monthEnd,
|
||||
this.$store.getters.order.serviceLocation.appointmentType
|
||||
this.$store.getters.order.serviceLocation.appointmentType,
|
||||
this.$store.getters.order.serviceLocation.provider.providerNumber
|
||||
);
|
||||
moreSelectableDates.days.forEach((selectableDate) => {
|
||||
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) => {
|
||||
// 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) => {
|
||||
if (date.inputValue.dateString === selectableDate.dateString) {
|
||||
if (date.inputValue === selectableDate.date) {
|
||||
date["isSelectable"] = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
scrollToElement(elementId, speed, easing) {
|
||||
// TODO - needs to be cleaned up & refactored
|
||||
function scrollTopSmooth(wrapper, target, duration = 300, timingName = "linear") {
|
||||
const initY = wrapper.scrollTop;
|
||||
const wrapperRect = wrapper.getBoundingClientRect();
|
||||
const targetRect = target.getBoundingClientRect();
|
||||
const targetY = targetRect.top - wrapperRect.top - BUFFER_OFFSET;
|
||||
const timingFunc = TIMINGFUNC_MAP[timingName];
|
||||
let start = null;
|
||||
|
||||
const step = (timestamp) => {
|
||||
start = start || timestamp;
|
||||
const progress = timestamp - start,
|
||||
// Growing from 0 to 1
|
||||
time = Math.min(1, (timestamp - start) / duration);
|
||||
const percentageNew = timingFunc(time);
|
||||
const distanceToGo = targetY;
|
||||
const thisDistance = percentageNew * distanceToGo;
|
||||
|
||||
wrapper.scrollTo(0, initY + thisDistance);
|
||||
if (percentageNew < 1) {
|
||||
window.requestAnimationFrame(step);
|
||||
}
|
||||
};
|
||||
window.requestAnimationFrame(step);
|
||||
}
|
||||
|
||||
const wrapper = document.getElementById("date-picker-fieldset");
|
||||
const targetMonth = document.getElementById(elementId);
|
||||
|
||||
scrollTopSmooth(wrapper, targetMonth, 800, "ease-in-out");
|
||||
},
|
||||
},
|
||||
components: {
|
||||
loader,
|
||||
|
|
@ -807,9 +877,8 @@ export default {
|
|||
.btn-link {
|
||||
font-weight: 500;
|
||||
text-underline-offset: 4px;
|
||||
box-shadow: none !important; // TODO: FIX THIS
|
||||
position: absolute;
|
||||
top: 95.4%;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.past {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { shallowMount } from "@vue/test-utils";
|
||||
import dropdownQuestion from "./dropdown-question";
|
||||
import crypto from "crypto";
|
||||
|
||||
// Mock CMS content
|
||||
const questionText = "Question Text";
|
||||
|
|
@ -12,8 +11,6 @@ const mockMixin = {
|
|||
},
|
||||
};
|
||||
|
||||
global.crypto = crypto;
|
||||
|
||||
// TODO: Remove the following from dropdown-question.vue -> :class="(errors && errors.length) || hasError ? 'has-error' : ''"
|
||||
// It is not being used.
|
||||
describe("dropdownQuestion.vue", () => {
|
||||
|
|
|
|||
|
|
@ -30,28 +30,26 @@
|
|||
|
||||
<script>
|
||||
import { useField } from "vee-validate";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
export default {
|
||||
name: "dropdown-question",
|
||||
props: {
|
||||
modelValue: String,
|
||||
customInputId: String,
|
||||
customDropdownId: String,
|
||||
options: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
modelValue: String,
|
||||
isDisabled: Boolean,
|
||||
isRequired: Boolean,
|
||||
validationRules: String,
|
||||
cmsWidgetName: String,
|
||||
hasError: Boolean,
|
||||
placeHolderText: String,
|
||||
customDropdownId: String,
|
||||
},
|
||||
setup(props) {
|
||||
const dropdownId = !props.customDropdownId
|
||||
? `dropdown-${crypto.randomUUID()}`
|
||||
: props.customDropdownId;
|
||||
const uuid = uuidv4();
|
||||
const dropdownId = !props.customDropdownId ? `dropdown-${uuid}` : props.customDropdownId;
|
||||
|
||||
const propsClone = Object.assign({}, props);
|
||||
const modelValue = propsClone.modelValue;
|
||||
|
|
|
|||
|
|
@ -7,15 +7,13 @@ const mockMeta = (returnValue) => jest.fn(async () => Promise.resolve(returnValu
|
|||
|
||||
import { shallowMount } from "@vue/test-utils";
|
||||
import modal from "./modal";
|
||||
import crypto from "crypto";
|
||||
|
||||
import { useForm } from "vee-validate";
|
||||
import { Modal } from "bootstrap";
|
||||
|
||||
const footerButtonText = "Sample footer text here.";
|
||||
const headerText = "Sample header text here.";
|
||||
|
||||
global.crypto = crypto;
|
||||
|
||||
describe("modal.vue", () => {
|
||||
it("Should display modal header text when headerText is defined", async () => {
|
||||
// Arrange / Act
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@
|
|||
import modalButtonMain from "@/digital-components/modal/ux-components/modal-button-main/modal-button-main";
|
||||
import { Modal } from "bootstrap";
|
||||
import { useForm } from "vee-validate";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
export default {
|
||||
name: "modal",
|
||||
|
|
@ -58,7 +59,9 @@ export default {
|
|||
},
|
||||
},
|
||||
setup() {
|
||||
const modalId = `modal-${crypto.randomUUID()}`;
|
||||
const uuid = uuidv4();
|
||||
const modalId = `modal-${uuid}`;
|
||||
|
||||
const { meta, validate, resetForm } = useForm();
|
||||
|
||||
return {
|
||||
|
|
@ -90,10 +93,12 @@ export default {
|
|||
openModal() {
|
||||
const modal = Modal.getOrCreateInstance(document.getElementById(this.modalId));
|
||||
modal.show();
|
||||
this.$emit("isModalOpened", true);
|
||||
},
|
||||
closeModal() {
|
||||
const modal = Modal.getInstance(document.getElementById(this.modalId));
|
||||
modal.hide();
|
||||
this.$emit("isModalOpened", false);
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -163,8 +168,7 @@ export default {
|
|||
}
|
||||
.modal-footer {
|
||||
border-top: none;
|
||||
background-color: $gray-100;
|
||||
box-shadow: 0px -1px 0px rgba(179, 180, 181, 0.3);
|
||||
|
||||
button {
|
||||
margin: 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,64 @@
|
|||
// Components
|
||||
import phoneNumberQuestion from "./phone-number-question";
|
||||
|
||||
// Supporting Files
|
||||
import { shallowMount } from "@vue/test-utils";
|
||||
|
||||
describe("phone-number-question.vue", () => {
|
||||
it("Should render phoneNumberQuestion sub-component (textbox-question)", async () => {
|
||||
// Arrange
|
||||
const wrapper = shallowMount(phoneNumberQuestion, {});
|
||||
|
||||
wrapper.getCmsContent = jest.fn();
|
||||
|
||||
// Act
|
||||
const phoneNumber = wrapper.findComponent({ ref: "phoneNumber" });
|
||||
|
||||
// Assert
|
||||
expect(phoneNumber.exists()).toBe(true);
|
||||
});
|
||||
|
||||
it("Should emit new value when modelValue is changed", async () => {
|
||||
// Act
|
||||
const wrapper = shallowMount(phoneNumberQuestion, {
|
||||
propsData: {
|
||||
modelValue: "val",
|
||||
},
|
||||
});
|
||||
|
||||
const phoneNumber = wrapper.findComponent({ ref: "phoneNumber" });
|
||||
await phoneNumber.setValue("val2");
|
||||
|
||||
// Assert
|
||||
expect(wrapper.emitted("update:modelValue")).toEqual([["val2"]]);
|
||||
});
|
||||
|
||||
it("Should emit new value when modelValue is changed", async () => {
|
||||
// Act
|
||||
const wrapper = shallowMount(phoneNumberQuestion, {
|
||||
propsData: {
|
||||
modelValue: "val",
|
||||
},
|
||||
});
|
||||
|
||||
const phoneNumber = wrapper.findComponent({ ref: "phoneNumber" });
|
||||
await phoneNumber.setValue("val2");
|
||||
|
||||
// Assert
|
||||
expect(wrapper.emitted("update:modelValue")).toEqual([["val2"]]);
|
||||
});
|
||||
|
||||
it("Should combine external and internal validation rules to pass to textbox-question", async () => {
|
||||
// Act
|
||||
const wrapper = shallowMount(phoneNumberQuestion, {
|
||||
propsData: {
|
||||
validationRules: "outsideValidation",
|
||||
},
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(wrapper.vm.validationRulesForTextBoxQuestion).toEqual(
|
||||
"outsideValidation|phone-number-format"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
<template>
|
||||
<div class="phone-number-question d-flex flex-column">
|
||||
<textboxQuestion
|
||||
ref="phoneNumber"
|
||||
type="text"
|
||||
:max-length="12"
|
||||
v-model="selectedValue"
|
||||
:mask="mask"
|
||||
:isRequired="isRequired"
|
||||
:validationRules="validationRulesForTextBoxQuestion"
|
||||
cmsWidgetName="PhoneNumberQuestionWidget" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import textboxQuestion from "@/digital-components/textbox-question/textbox-question";
|
||||
|
||||
// Supporting files
|
||||
import { errorMessages } from "@/constants/error-messages";
|
||||
import { regex } from "@/helpers/validation-rules";
|
||||
import { defineRule } from "vee-validate";
|
||||
|
||||
// Validation
|
||||
defineRule(
|
||||
"phone-number-format",
|
||||
regex(/^(?=(?:.*\d){10})(?=(?:.*-){2})[\d-]{12}$/, errorMessages.PHONE_FORMAT)
|
||||
);
|
||||
|
||||
export default {
|
||||
name: "phoneNumberQuestion",
|
||||
props: {
|
||||
cmsWidgetName: String,
|
||||
isRequired: Boolean,
|
||||
validationRules: String,
|
||||
hasError: Boolean,
|
||||
centerErrorMessage: Boolean,
|
||||
modelValue: String,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
phoneNumber: "",
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
selectedValue: {
|
||||
get: function () {
|
||||
return this.modelValue;
|
||||
},
|
||||
set: function (newValue) {
|
||||
this.$emit("update:modelValue", newValue);
|
||||
},
|
||||
},
|
||||
validationRulesForTextBoxQuestion() {
|
||||
if (!this.validationRules || this.validationRules.length === 0) {
|
||||
return "phone-number-format";
|
||||
} else {
|
||||
return this.validationRules + "|phone-number-format";
|
||||
}
|
||||
},
|
||||
mask() {
|
||||
return {
|
||||
mask: "x##-###-####",
|
||||
tokens: {
|
||||
x: {
|
||||
pattern: /[2-9]/,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
components: {
|
||||
textboxQuestion,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.phone-number-question {
|
||||
label {
|
||||
color: $black;
|
||||
}
|
||||
input {
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid $gray-500;
|
||||
height: 48px;
|
||||
&:focus {
|
||||
box-shadow: 0 0 0 2.5px $blue;
|
||||
outline: none;
|
||||
}
|
||||
&:hover {
|
||||
box-shadow: 0 0 0 4px $blue-300;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { shallowMount } from "@vue/test-utils";
|
||||
import TextBlock from "./text-block";
|
||||
|
||||
describe("modal.vue", () => {
|
||||
describe("text-block.vue", () => {
|
||||
it("Should display 'Text' when 'Text' is defined in the CMS", async () => {
|
||||
// Act
|
||||
const wrapper = shallowMount(TextBlock, {
|
||||
|
|
|
|||
|
|
@ -1,47 +1,92 @@
|
|||
<template>
|
||||
<div
|
||||
class="text-block w-100"
|
||||
:class="[justifyText, typeStyle, fontWeight, margin]"
|
||||
v-html="this.TextBlockCopy"></div>
|
||||
<div class="text-block w-100" :class="[justifyText, typeStyle, fontWeight, marginTopClass]">
|
||||
<span v-for="copy in splitCopyOnCMSPlaceHolder(this.textBlockCopy)" :key="copy">
|
||||
<span v-if="doesCopyContainRouterLink(copy)">
|
||||
<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)" />
|
||||
</span>
|
||||
<span v-else v-html="copy"></span>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<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 {
|
||||
name: "textBlock",
|
||||
props: {
|
||||
customText: String, // used to allow the insert of token values into textblock
|
||||
justifyText: String, // left, right, center
|
||||
margin: {
|
||||
// bootstrap margin to apply to the block.
|
||||
type: String,
|
||||
default: "mt-2",
|
||||
},
|
||||
justifyText: String, // right, center (left is default)
|
||||
typeStyle: String, // h1-h6, body, small, label, caption (see Figma or Confluence documentation)
|
||||
fontWeight: String, // bold=500, default is 400
|
||||
cmsWidgetName: String,
|
||||
marginTopSizeOverride: Number, // override mt-2 with a bootstrap size from 0-5 or auto
|
||||
},
|
||||
methods: {
|
||||
doesCopyContainRouterLink,
|
||||
doesCopyContainTextLink,
|
||||
splitCopyOnCMSPlaceHolder,
|
||||
getRouterLinkRouteFromCopy,
|
||||
getRouterLinkDisplayTextFromCopy,
|
||||
},
|
||||
computed: {
|
||||
TextBlockCopy() {
|
||||
pageQueryString() {
|
||||
return applicationConfig.PAGE_QUERYSTRING;
|
||||
},
|
||||
textBlockCopy() {
|
||||
if (this.customText) {
|
||||
return this.customText;
|
||||
}
|
||||
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>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.text-block {
|
||||
display: flex;
|
||||
&.left {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
&.right {
|
||||
justify-content: flex-end;
|
||||
text-align: right;
|
||||
}
|
||||
&.center {
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
&.bold {
|
||||
font-weight: 500;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,61 @@
|
|||
// Components
|
||||
import textareaQuestion from "./textarea-question";
|
||||
|
||||
// Supporting Files
|
||||
import { shallowMount } from "@vue/test-utils";
|
||||
|
||||
const maska = jest.fn();
|
||||
|
||||
const questionText = "textareaQuestionText";
|
||||
const mockMixin = {
|
||||
methods: {
|
||||
getCmsContent: jest.fn().mockImplementation(() => {
|
||||
return questionText;
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
describe("textarea-question.vue", () => {
|
||||
it("Should render a textarea", async () => {
|
||||
// Arrange
|
||||
const wrapper = shallowMount(textareaQuestion, {
|
||||
global: {
|
||||
directives: {
|
||||
maska: maska,
|
||||
},
|
||||
},
|
||||
propsData: {
|
||||
modelValue: "",
|
||||
},
|
||||
mixins: [mockMixin],
|
||||
});
|
||||
|
||||
wrapper.getCmsContent = jest.fn();
|
||||
|
||||
// Act
|
||||
const textarea = wrapper.find("textarea");
|
||||
|
||||
// Assert
|
||||
expect(textarea.exists()).toBe(true);
|
||||
});
|
||||
|
||||
it("Should emit new value when modelValue is changed", async () => {
|
||||
// Act
|
||||
const wrapper = shallowMount(textareaQuestion, {
|
||||
global: {
|
||||
directives: {
|
||||
maska: maska,
|
||||
},
|
||||
},
|
||||
propsData: {
|
||||
modelValue: "val",
|
||||
},
|
||||
mixins: [mockMixin],
|
||||
});
|
||||
|
||||
await wrapper.find("textarea").setValue("val2");
|
||||
|
||||
// Assert
|
||||
expect(wrapper.emitted("update:modelValue")).toEqual([["val2"]]);
|
||||
});
|
||||
});
|
||||
112
src/digital-components/textarea-question/textarea-question.vue
Normal file
112
src/digital-components/textarea-question/textarea-question.vue
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
<template>
|
||||
<div class="textarea-question">
|
||||
<div class="label-wrapper mb-1" :aria-label="questionText">
|
||||
<!-- Wrap label and span because v-html prevents v-if from displaying if v-if <span> is inside <label>-->
|
||||
<label for="textarea-question" class="fw-bold" v-html="questionText"></label>
|
||||
<span v-if="!isRequired" class="fw-normal ms-1">(Optional)</span>
|
||||
</div>
|
||||
<textarea
|
||||
id="textareaQuestion"
|
||||
ref="textarea"
|
||||
v-model="value"
|
||||
v-maska="mask"
|
||||
@keyup="updateCount"
|
||||
class="p-4"
|
||||
:maxlength="maxLength"
|
||||
role="textbox"
|
||||
aria-multiline="true"
|
||||
:aria-required="isRequired">
|
||||
</textarea>
|
||||
<p
|
||||
tabindex="0"
|
||||
class="caption mt-2 mb-0"
|
||||
id="charactersRemaining"
|
||||
:class="[urgentCountdown ? 'urgent-countdown' : '']">
|
||||
{{ remainingCount }}/{{ maxLength }} characters remaining
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "textareaQuestion",
|
||||
props: {
|
||||
cmsWidgetName: String,
|
||||
isRequired: Boolean,
|
||||
maxLength: {
|
||||
type: Number,
|
||||
default: 250,
|
||||
},
|
||||
modelValue: String,
|
||||
},
|
||||
// TODO: At some point in the future we should probably add the tie in to validation here in case the field must be populated for some other use cases
|
||||
setup() {},
|
||||
computed: {
|
||||
questionText() {
|
||||
return this.getCmsContent(this.cmsWidgetName, "QuestionText");
|
||||
},
|
||||
value: {
|
||||
get: function () {
|
||||
return this.modelValue;
|
||||
},
|
||||
set: function (newValue) {
|
||||
this.$emit("update:modelValue", newValue);
|
||||
},
|
||||
},
|
||||
remainingCount() {
|
||||
return this.maxLength - this.value.length;
|
||||
},
|
||||
urgentCountdown() {
|
||||
return this.remainingCount <= this.maxLength * 0.1 ? true : false;
|
||||
},
|
||||
mask() {
|
||||
// Allow any character but only the max length number of times.
|
||||
return {
|
||||
mask: `x*${this.maxLength}`,
|
||||
tokens: {
|
||||
x: {
|
||||
pattern: /.|\n|\r/,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.textarea-question {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
label {
|
||||
color: $black;
|
||||
}
|
||||
.label-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
label {
|
||||
span {
|
||||
color: $gray-500;
|
||||
}
|
||||
}
|
||||
}
|
||||
textarea {
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid $gray-500;
|
||||
height: 88px;
|
||||
&:focus {
|
||||
box-shadow: 0 0 0 2.5px $blue;
|
||||
outline: none;
|
||||
}
|
||||
&:hover {
|
||||
box-shadow: 0 0 0 4px $blue-300;
|
||||
}
|
||||
}
|
||||
p {
|
||||
color: $gray-500;
|
||||
&.urgent-countdown {
|
||||
color: $red;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
import { shallowMount } from "@vue/test-utils";
|
||||
import textboxQuestion from "./textbox-question";
|
||||
import crypto from "crypto";
|
||||
|
||||
// Mock CMS content
|
||||
const questionText = "Question Text";
|
||||
|
|
@ -13,8 +12,6 @@ const mockMixin = {
|
|||
};
|
||||
const maska = jest.fn();
|
||||
|
||||
global.crypto = crypto;
|
||||
|
||||
describe("textboxQuestion.vue", () => {
|
||||
it("Should render a text input", async () => {
|
||||
// Arrange
|
||||
|
|
|
|||
|
|
@ -44,7 +44,8 @@
|
|||
@change="handleChange"
|
||||
@blur="handleChange"
|
||||
:maxlength="maxLength ? maxLength : '999'"
|
||||
@focus="$emit('focus', $event.target.value)" />
|
||||
@focus="$emit('focus', $event.target.value)"
|
||||
@keydown="keyDownHandler" />
|
||||
<button v-if="includeSearchIcon" type="submit" aria-label="Search button" />
|
||||
<template v-if="includeImageQuestion">
|
||||
<template v-if="!isDisabled">
|
||||
|
|
@ -77,13 +78,14 @@
|
|||
|
||||
<script>
|
||||
import { useField, validate } from "vee-validate";
|
||||
import { storeActions } from "@/constants/store-actions";
|
||||
import loader from "@/ux-components/loader/loader.vue";
|
||||
import { ref } from "vue";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
export default {
|
||||
name: "textbox-question",
|
||||
props: {
|
||||
customInputId: String,
|
||||
type: {
|
||||
type: String,
|
||||
default: "text",
|
||||
|
|
@ -97,7 +99,6 @@ export default {
|
|||
default: true,
|
||||
},
|
||||
modelValue: String,
|
||||
customInputId: String,
|
||||
isDisabled: Boolean,
|
||||
isRequired: Boolean,
|
||||
hasIcon: Boolean, // If input has an icon
|
||||
|
|
@ -118,9 +119,11 @@ export default {
|
|||
maxFileSize: Number,
|
||||
hideInput: Boolean,
|
||||
centerErrorMessage: Boolean,
|
||||
keyDownHandler: Function,
|
||||
},
|
||||
setup(props) {
|
||||
const inputId = !props.customInputId ? `input-${crypto.randomUUID()}` : props.customInputId;
|
||||
const uuid = uuidv4();
|
||||
const inputId = !props.customInputId ? `input-${uuid}` : props.customInputId;
|
||||
|
||||
const propsClone = Object.assign({}, props);
|
||||
const modelValue = propsClone.modelValue;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// Components
|
||||
import addressQuestions from "@/layouts/address-lookup/customer-questions/address-questions/address-questions";
|
||||
import addressQuestions from "@/fmg-components/address-questions/address-questions";
|
||||
import alert from "@/ux-components/alert/alert";
|
||||
|
||||
// Supporting Files
|
||||
|
|
@ -7,7 +7,6 @@ import { mount, shallowMount } from "@vue/test-utils";
|
|||
import { getMountOptions } from "@/helpers/unit-test-helper.js";
|
||||
import { storeMutations } from "@/constants/store-mutations";
|
||||
import store from "@/store";
|
||||
import { createImportSpecifier } from "typescript";
|
||||
|
||||
let autocompleteElement;
|
||||
describe("address-questions.vue", () => {
|
||||
|
|
@ -134,22 +133,19 @@ describe("address-questions.vue", () => {
|
|||
});
|
||||
|
||||
test("Should set this.showAddressFields to true when the model is prepopulated", async () => {
|
||||
// Arrange
|
||||
// Act
|
||||
const newAddressModel = {
|
||||
streetAddress: "foo",
|
||||
city: "foo",
|
||||
state: "foo",
|
||||
zipCode: "55555",
|
||||
};
|
||||
const wrapper = shallowMount(addressQuestions, {
|
||||
propsData: {
|
||||
modelValue: newAddressModel,
|
||||
// Arrange / Act
|
||||
const { wrapper } = setupMocks({
|
||||
props: {
|
||||
modelValue: {
|
||||
streetAddress: "foo",
|
||||
city: "foo",
|
||||
state: "foo",
|
||||
zipCode: "55555",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Act
|
||||
wrapper.vm.setupAddressLookup();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
// Assert
|
||||
expect(wrapper.vm.showAddressFields).toBe(true);
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div class="address-questions" role="application">
|
||||
<div class="row mb-4">
|
||||
<div class="address-questions">
|
||||
<div class="row mb-4" aria-live="polite">
|
||||
<div class="col">
|
||||
<textboxQuestion
|
||||
id="streetAddressField"
|
||||
|
|
@ -122,14 +122,16 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
autocomplete: null,
|
||||
autocompleteListener: null,
|
||||
showAddressFields: false,
|
||||
matchFound: null, // null = no attempted match, true = match was found, false = match was not found
|
||||
displayVerificationWarning: false,
|
||||
displayNoMatchWarning: false,
|
||||
alertHeadlineVerificationWarning: "",
|
||||
alertCopyVerificationWarning: "",
|
||||
alertHeadlineNoMatchWarning: "",
|
||||
alertCopyNoMatchWarning: "",
|
||||
matchFound: null, // null = no attempted match, true = match was found, false = match was not found
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -203,182 +205,202 @@ export default {
|
|||
return this.captureApartmentNumberOrBusinessName;
|
||||
},
|
||||
},
|
||||
addressField1: {
|
||||
get: function () {
|
||||
return document.getElementById("autocomplete");
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
setupAddressLookup() {
|
||||
this.showAddressFields = false;
|
||||
if (
|
||||
this.addressModel.streetAddress &&
|
||||
this.addressModel.city &&
|
||||
this.addressModel.state &&
|
||||
this.addressModel.zipCode
|
||||
) {
|
||||
this.showAddressFields = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const addressField1 = document.getElementById("autocomplete");
|
||||
const self = this;
|
||||
|
||||
loadGooglePlacesAutocompleteScript() {
|
||||
// Load the Google Places Autocomplete script
|
||||
const apiKey = applicationConfig.GOOGLE_PLACES_API_KEY;
|
||||
|
||||
this.$loadScript(
|
||||
`https://maps.googleapis.com/maps/api/js?key=${apiKey}&libraries=places&callback=Function.prototype`
|
||||
)
|
||||
.then(() => {
|
||||
// Script is loaded, initialize the autocomplete textbox
|
||||
const autocomplete = new window.google.maps.places.Autocomplete(addressField1, {
|
||||
componentRestrictions: { country: ["us"] },
|
||||
fields: ["address_components"],
|
||||
types: ["geocode"],
|
||||
});
|
||||
).then(() => {
|
||||
// When loaded, trigger the setup
|
||||
this.initializeAutocomplete();
|
||||
});
|
||||
},
|
||||
initializeAutocomplete() {
|
||||
// Initialize the Google Places Autocomplete
|
||||
this.autocomplete = new window.google.maps.places.Autocomplete(this.addressField1, {
|
||||
componentRestrictions: { country: ["us"] },
|
||||
fields: ["address_components"],
|
||||
types: ["geocode"],
|
||||
});
|
||||
|
||||
// Standard place_changed event handling
|
||||
const autocompleteListener = window.google.maps.event.addListener(
|
||||
autocomplete,
|
||||
"place_changed",
|
||||
fillInAddress
|
||||
// Set up the Autocomplete place_changed event to call our method to fill in the address
|
||||
this.autocompleteListener = window.google.maps.event.addListener(
|
||||
this.autocomplete,
|
||||
"place_changed",
|
||||
this.fillInAddress
|
||||
);
|
||||
|
||||
// When the Street Address textbox receives focus,
|
||||
// append the search results list container to the bottom of the textbox
|
||||
// and disable browser autofill
|
||||
this.addressField1.addEventListener("focus", (e) => {
|
||||
// Make place results box stick to the input on scroll
|
||||
const streetAddressField = document.getElementById("streetAddressField");
|
||||
const autocompleteResultsContainer =
|
||||
document.getElementsByClassName("pac-container")[0];
|
||||
if (autocompleteResultsContainer) {
|
||||
streetAddressField.appendChild(autocompleteResultsContainer);
|
||||
}
|
||||
|
||||
// Unfortunately this is the only place we can set the autocomplete attribute without the
|
||||
// Google Places object resetting it to "off" which does nothing to prevent browser autofill
|
||||
this.addressField1.setAttribute("autocomplete", "do-not-autofill");
|
||||
});
|
||||
|
||||
this.addressField1.addEventListener("keydown", (e) => {
|
||||
// If a match has been previously attempted then do nothing
|
||||
if (this.matchFound !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const event = new Event("place_changed");
|
||||
|
||||
// When either of the two enter keys or the tab key are pressed
|
||||
if (e.code === "Enter" || e.code === "NumpadEnter" || e.code === "Tab") {
|
||||
// Grab the selected item
|
||||
const selectedItem = document.querySelector(
|
||||
".pac-container .pac-item-selected"
|
||||
);
|
||||
|
||||
addressField1.addEventListener("focus", () => {
|
||||
// Wrapping the addressField1 element in the Google Address Autocomplete object
|
||||
// will cause "autocomplete='off'" which Chrome completely ignores. This event
|
||||
// handler will set the value to something arbitrary so autofill doesn't work.
|
||||
// https://stackoverflow.com/a/30976223
|
||||
addressField1.setAttribute("autocomplete", "do-not-autofill");
|
||||
if (selectedItem !== null) {
|
||||
// If an item was selected then fill in the address with the selected item
|
||||
// by triggering the "place_changed" event of the Autocomplete object
|
||||
this.autocomplete.dispatchEvent(event);
|
||||
} else {
|
||||
// Otherwise fill-in the address using first item from the list.
|
||||
this.fillInAddressUsingFirstItem();
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
// Make place results box stick to the input on scroll
|
||||
const streetAddressField = document.getElementById("streetAddressField");
|
||||
const autocompleteResultsContainer =
|
||||
document.getElementsByClassName("pac-container")[0];
|
||||
if (autocompleteResultsContainer) {
|
||||
streetAddressField.appendChild(autocompleteResultsContainer);
|
||||
}
|
||||
});
|
||||
this.addressField1.addEventListener("change", () => {
|
||||
// If a match has been previously attempted then do nothing
|
||||
if (this.matchFound !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
addressField1.addEventListener("keydown", (e) => {
|
||||
const autocomplete = document.getElementById("autocomplete");
|
||||
const event = new Event("place_changed");
|
||||
// Get the address that the user clicked on (if any)
|
||||
const clickedAddress = document.querySelector(".pac-container .pac-item:hover");
|
||||
|
||||
if (e.code === "Enter" || e.code === "NumpadEnter" || e.code === "Tab") {
|
||||
const selectedItem = document.querySelector(
|
||||
".pac-container .pac-item-selected"
|
||||
);
|
||||
if (selectedItem !== null) {
|
||||
// Fill-in the address using selected item in the list.
|
||||
autocomplete.dispatchEvent(event);
|
||||
//fillInAddress(selectedItem.textContent);
|
||||
} else {
|
||||
// Fill-in the address using first item in the list.
|
||||
fillInAddressUsingFirstItem();
|
||||
// If the Street Address field changed without clicking (i.e. by pressing Tab, or clicking outside the field)
|
||||
if (clickedAddress === null) {
|
||||
// Fill-in the address using first item in the list.
|
||||
this.fillInAddressUsingFirstItem();
|
||||
}
|
||||
});
|
||||
},
|
||||
fillInAddress(place) {
|
||||
if (!place) {
|
||||
place = this.autocomplete.getPlace();
|
||||
}
|
||||
|
||||
if (place && place.address_components) {
|
||||
this.matchFound = true;
|
||||
|
||||
const self = this;
|
||||
this.$nextTick(function () {
|
||||
self.showAddressFields = true;
|
||||
|
||||
for (const component of place.address_components) {
|
||||
const componentType = component.types[0];
|
||||
|
||||
switch (componentType) {
|
||||
case "street_number": {
|
||||
self.addressModel.streetAddress = component.long_name;
|
||||
break;
|
||||
}
|
||||
case "route": {
|
||||
self.addressModel.streetAddress += " " + component.short_name;
|
||||
break;
|
||||
}
|
||||
case "locality": {
|
||||
self.addressModel.city = component.long_name;
|
||||
break;
|
||||
}
|
||||
case "administrative_area_level_1": {
|
||||
self.addressModel.state = component.short_name;
|
||||
break;
|
||||
}
|
||||
case "postal_code": {
|
||||
self.addressModel.zipCode = component.long_name;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
addressField1.addEventListener("change", () => {
|
||||
// If a match has been previously attempted then do nothing
|
||||
if (self.matchFound !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the address that the user clicked on (if any)
|
||||
const clickedAddress = document.querySelector(
|
||||
".pac-container .pac-item:hover"
|
||||
);
|
||||
|
||||
// If the Street Address field changed without clicking (i.e. by pressing Tab, or clicking outside the field)
|
||||
if (clickedAddress === null) {
|
||||
// Fill-in the address using first item in the list.
|
||||
fillInAddressUsingFirstItem();
|
||||
}
|
||||
});
|
||||
|
||||
function fillInAddressUsingFirstItem() {
|
||||
// Fill-in the address using first item in the list.
|
||||
const item = document.querySelector(".pac-container .pac-item");
|
||||
if (item != null) {
|
||||
const firstResult = item.textContent;
|
||||
const geocoder = new window.google.maps.Geocoder();
|
||||
geocoder.geocode(
|
||||
{
|
||||
address: firstResult,
|
||||
},
|
||||
function (results, status) {
|
||||
if (status === window.google.maps.GeocoderStatus.OK) {
|
||||
fillInAddress(results[0]);
|
||||
self.displayVerificationWarning = true;
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
self.matchFound = false;
|
||||
}
|
||||
}
|
||||
|
||||
function fillInAddress(place) {
|
||||
if (!place) {
|
||||
place = autocomplete.getPlace();
|
||||
}
|
||||
// After filling in the address fields, disable the address autocomplete
|
||||
this.unloadAutocomplete();
|
||||
|
||||
if (place && place.address_components) {
|
||||
self.matchFound = true;
|
||||
|
||||
self.$nextTick(function () {
|
||||
self.showAddressFields = true;
|
||||
|
||||
for (const component of place.address_components) {
|
||||
const componentType = component.types[0];
|
||||
|
||||
switch (componentType) {
|
||||
case "street_number": {
|
||||
self.addressModel.streetAddress = component.long_name;
|
||||
break;
|
||||
}
|
||||
case "route": {
|
||||
self.addressModel.streetAddress +=
|
||||
" " + component.short_name;
|
||||
break;
|
||||
}
|
||||
case "locality": {
|
||||
self.addressModel.city = component.long_name;
|
||||
break;
|
||||
}
|
||||
case "administrative_area_level_1": {
|
||||
self.addressModel.state = component.short_name;
|
||||
break;
|
||||
}
|
||||
case "postal_code": {
|
||||
self.addressModel.zipCode = component.long_name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// after showing the address fields, disable the address autocomplete
|
||||
window.google.maps.event.removeListener(autocompleteListener);
|
||||
window.google.maps.event.clearInstanceListeners(autocomplete);
|
||||
addressField1.onchange = null;
|
||||
const pacContainer = document.querySelector(".pac-container");
|
||||
if (pacContainer) {
|
||||
pacContainer.remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// Failed to fetch script
|
||||
console.log("Unable to load Google Places API script");
|
||||
// Restore focus to the first address field
|
||||
this.addressField1.focus();
|
||||
});
|
||||
}
|
||||
},
|
||||
fillInAddressUsingFirstItem() {
|
||||
// Fill-in the address using first item in the list.
|
||||
const item = document.querySelector(".pac-container .pac-item");
|
||||
if (item != null) {
|
||||
const firstResult = item.textContent;
|
||||
const geocoder = new window.google.maps.Geocoder();
|
||||
const self = this;
|
||||
geocoder.geocode(
|
||||
{
|
||||
address: firstResult,
|
||||
},
|
||||
function (results, status) {
|
||||
if (status === window.google.maps.GeocoderStatus.OK) {
|
||||
self.fillInAddress(results[0]);
|
||||
self.displayVerificationWarning = true;
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
this.matchFound = false;
|
||||
}
|
||||
},
|
||||
resetAlerts() {
|
||||
this.displayVerificationWarning = false;
|
||||
},
|
||||
unloadAutocomplete() {
|
||||
if (this.autocompleteListener && this.autocomplete) {
|
||||
window.google.maps.event.removeListener(this.autocompleteListener);
|
||||
this.autocompleteListener = null;
|
||||
|
||||
window.google.maps.event.clearInstanceListeners(this.autocomplete);
|
||||
this.autocomplete = null;
|
||||
|
||||
this.addressField1.onchange = null;
|
||||
|
||||
const pacContainer = document.querySelector(".pac-container");
|
||||
if (pacContainer) {
|
||||
pacContainer.remove();
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.setupAddressLookup();
|
||||
// If we already have a full address, show it
|
||||
this.showAddressFields =
|
||||
(this.addressModel.streetAddress ?? "") !== "" &&
|
||||
(this.addressModel.city ?? "") !== "" &&
|
||||
(this.addressModel.state ?? "") !== "" &&
|
||||
(this.addressModel.zipCode ?? "") !== "";
|
||||
|
||||
if (!this.showAddressFields) {
|
||||
this.loadGooglePlacesAutocompleteScript();
|
||||
}
|
||||
},
|
||||
unmounted() {
|
||||
this.unloadAutocomplete();
|
||||
},
|
||||
watch: {
|
||||
matchFound: {
|
||||
|
|
@ -398,7 +420,7 @@ export default {
|
|||
this.addressModel.state = "";
|
||||
this.addressModel.zipCode = "";
|
||||
}
|
||||
this.showAddressFields = true;
|
||||
|
||||
this.displayVerificationWarning = false;
|
||||
|
||||
// Only deep watch the Address Model after a failed match
|
||||
|
|
@ -414,11 +436,6 @@ export default {
|
|||
}
|
||||
},
|
||||
},
|
||||
modelValue: {
|
||||
handler() {
|
||||
this.setupAddressLookup();
|
||||
},
|
||||
},
|
||||
},
|
||||
components: {
|
||||
textboxQuestion,
|
||||
|
|
@ -1,8 +1,5 @@
|
|||
import { mount } from "@vue/test-utils";
|
||||
import contentGroupModal from "./content-group-modal";
|
||||
import crypto from "crypto";
|
||||
|
||||
global.crypto = crypto;
|
||||
|
||||
describe("content-group-modal.vue", () => {
|
||||
it("Should display header text when HeaderText is defined in the CMS", async () => {
|
||||
|
|
|
|||
|
|
@ -46,19 +46,10 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
paddingHeight: 0,
|
||||
customButtontext: "",
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.paddingHeight = this.getFooterInfoBoxHeight() + 24;
|
||||
this.$nextTick(() => {
|
||||
window.addEventListener("resize", this.onResize);
|
||||
});
|
||||
},
|
||||
beforeUnmount() {
|
||||
window.removeEventListener("resize", this.onResize);
|
||||
},
|
||||
|
||||
unmounted() {
|
||||
document.onkeydown = null;
|
||||
},
|
||||
|
|
@ -73,9 +64,6 @@ export default {
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
onResize() {
|
||||
this.paddingHeight = this.getFooterInfoBoxHeight();
|
||||
},
|
||||
updateButtonText(newText) {
|
||||
this.customButtontext = newText;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -21,6 +21,19 @@
|
|||
aria-hidden="true"
|
||||
v-on="{ 'show.bs.modal': show, 'hide.bs.modal': hide }"
|
||||
:style="`height: calc(100% - ${currentFooterAndHeaderHeight}px);`">
|
||||
<div class="menu-modal-container">
|
||||
<button
|
||||
class="menu-button"
|
||||
type="button"
|
||||
:class="[isActive ? 'active' : '']"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#footerModal"
|
||||
aria-label="Hamburger Menu (modal window)">
|
||||
<div class="bar1"></div>
|
||||
<div class="bar2"></div>
|
||||
<div class="bar3"></div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-dialog modal-fullscreen">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header visually-hidden">
|
||||
|
|
|
|||
|
|
@ -13,19 +13,19 @@
|
|||
<div class="col">
|
||||
<div class="text-container slide">
|
||||
<p>
|
||||
Finding shops near you
|
||||
Tidying up the shop
|
||||
<span class="dot-1">.</span>
|
||||
<span class="dot-2">.</span>
|
||||
<span class="dot-3">.</span>
|
||||
</p>
|
||||
<p>
|
||||
Looking for dates
|
||||
Getting all the glass shined up
|
||||
<span class="dot-1">.</span>
|
||||
<span class="dot-2">.</span>
|
||||
<span class="dot-3">.</span>
|
||||
</p>
|
||||
<p>
|
||||
Searching for times
|
||||
Planning your new view of the road
|
||||
<span class="dot-1">.</span>
|
||||
<span class="dot-2">.</span>
|
||||
<span class="dot-3">.</span>
|
||||
|
|
@ -164,19 +164,19 @@ export default {
|
|||
transform: translateX(-600px);
|
||||
}
|
||||
55% {
|
||||
transform: translateX(-1110px);
|
||||
transform: translateX(-1195px);
|
||||
}
|
||||
66% {
|
||||
transform: translateX(-1110px);
|
||||
transform: translateX(-1195px);
|
||||
}
|
||||
77% {
|
||||
transform: translateX(-1600px);
|
||||
transform: translateX(-1750px);
|
||||
}
|
||||
88% {
|
||||
transform: translateX(-1600px);
|
||||
transform: translateX(-1750px);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(-2050px);
|
||||
transform: translateX(-2200px);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,22 +1,24 @@
|
|||
import axios from "axios";
|
||||
import analyticsMixIn from "@/mixins/analytics-mixin.js";
|
||||
import store from "@/store";
|
||||
import router from "@/router";
|
||||
|
||||
import { applicationConfig } from "@/constants/application-config.js";
|
||||
import { GaCategories, GaActions, GaLabels } from "@/constants/analytics";
|
||||
import { headerKeys } from "@/constants/header-keys";
|
||||
|
||||
export default {
|
||||
callHttpClient({ method, endpoint, payload, logApiCall = true, isFormData = false }) {
|
||||
callHttpClient({
|
||||
method,
|
||||
endpoint,
|
||||
payload,
|
||||
logApiCall = true,
|
||||
isFormData = false,
|
||||
additionalSuccessEventDataHandler,
|
||||
}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const cfDistroUrl = applicationConfig.CONSUMER_CF_DISTRO;
|
||||
let payloadAndAnalyticsData = {};
|
||||
if (isFormData) {
|
||||
payloadAndAnalyticsData = payload;
|
||||
payloadAndAnalyticsData.append("AppName", "FixMyGlass");
|
||||
} else {
|
||||
Object.assign(payloadAndAnalyticsData, payload, { AppName: "FixMyGlass" });
|
||||
}
|
||||
const payloadAndAnalyticsData = Object.assign({}, payload, { AppName: "FixMyGlass" });
|
||||
const headers = {
|
||||
[headerKeys.EXPERIMENT]: JSON.stringify(store.getters.experimentSettings),
|
||||
};
|
||||
|
|
@ -31,10 +33,16 @@ export default {
|
|||
}).then(
|
||||
(response) => {
|
||||
if (logApiCall) {
|
||||
let additionalEventData = "";
|
||||
if (additionalSuccessEventDataHandler) {
|
||||
additionalEventData = "_" + additionalSuccessEventDataHandler(response);
|
||||
}
|
||||
const pageName = analyticsMixIn.methods.getPageName();
|
||||
const nextPageName = router.lastNavigationPage || pageName;
|
||||
analyticsMixIn.methods.pushEventToGA(
|
||||
GaCategories.API_RESPONSE,
|
||||
GaActions.RESULT,
|
||||
`${GaLabels.SUCCESS}_${endpoint}`,
|
||||
`${nextPageName}_${endpoint}`,
|
||||
`${GaLabels.SUCCESS}${additionalEventData}`,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { storeActions } from "@/constants/store-actions.js";
|
||||
import store from "@/store";
|
||||
import { dynamicStrings } from "../constants/dynamic-strings";
|
||||
import { dynamicStrings } from "@/constants/dynamic-strings";
|
||||
|
||||
export function fetchCmsContentForPage(fmgPage) {
|
||||
return store.dispatch(storeActions.GET_PAGE_DATA, { pageName: fmgPage }).then((response) => {
|
||||
|
|
@ -284,7 +284,7 @@ function getIfStatementRegexExpression() {
|
|||
//////////////////////////////////////////
|
||||
|
||||
export function doesCopyContainRouterLink(copy) {
|
||||
return copy.includes(this.dynamicStrings.ROUTER_LINK);
|
||||
return copy.includes(dynamicStrings.ROUTER_LINK);
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -59,7 +59,7 @@
|
|||
cmsWidgetName="ServiceZipQuestionWidget"
|
||||
v-model="serviceZipCode"
|
||||
ref="serviceZip"
|
||||
inputId="7add1b26df344f2caf1678de5797803f"
|
||||
customInputId="serviceZip"
|
||||
aria-haspopup=""
|
||||
mask="#####"
|
||||
validationRules="service-zip-required|service-zip-format" />
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
cmsWidgetName="FirstNameQuestionWidget"
|
||||
v-model="customerModel.firstName"
|
||||
ref="firstName"
|
||||
inputId="08497a2efd9a4a73a70360ab47b4838d"
|
||||
customInputId="firstName"
|
||||
validationRules="first-name-required" />
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -16,7 +16,7 @@
|
|||
cmsWidgetName="LastNameQuestionWidget"
|
||||
v-model="customerModel.lastName"
|
||||
ref="lastName"
|
||||
inputId="0030e56a57e74a4ab92de7fb8e97fec5"
|
||||
customInputId="lastName"
|
||||
validationRules="last-name-required" />
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -26,8 +26,8 @@
|
|||
cmsWidgetName="EmailAddressQuestionWidget"
|
||||
v-model="customerModel.emailAddress"
|
||||
ref="emailAddress"
|
||||
inputId="00450a91b8964a768ce3992e6feb890f"
|
||||
validationRules="email-address-required|email-address-format" />
|
||||
customInputId="emailAddress"
|
||||
validationRules="email-address-format" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-0">
|
||||
|
|
@ -38,7 +38,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import addressQuestions from "@/layouts/address-lookup/customer-questions/address-questions/address-questions";
|
||||
import addressQuestions from "@/fmg-components/address-questions/address-questions";
|
||||
import textboxQuestion from "@/digital-components/textbox-question/textbox-question";
|
||||
import { defineRule } from "vee-validate";
|
||||
import { required } from "@/helpers/validation-rules";
|
||||
|
|
@ -49,7 +49,6 @@ import textBlock from "@/digital-components/text-block/text-block";
|
|||
// DEFINE VALIDATION RULES
|
||||
defineRule("first-name-required", required(errorMessages.FIRST_NAME_REQUIRED));
|
||||
defineRule("last-name-required", required(errorMessages.LAST_NAME_REQUIRED));
|
||||
defineRule("email-address-required", required(errorMessages.EMAIL_ADDRESS_REQUIRED));
|
||||
defineRule(
|
||||
"email-address-format",
|
||||
regex(
|
||||
|
|
|
|||
|
|
@ -20,19 +20,12 @@
|
|||
validationRules="vehicle-required"
|
||||
v-model="selectedVehicleVin"
|
||||
:isCarIdDifferent="isCarIdDifferent" />
|
||||
<div class="alert-provide-vin my-3" v-if="splitAlertProvideVinBodyForLink.length">
|
||||
<span v-for="copy in splitAlertProvideVinBodyForLink" :key="copy">
|
||||
<span v-if="doesCopyContainRouterLink(copy)" class="text-body">
|
||||
<router-link
|
||||
:to="{
|
||||
query: { fmgPage: `${getRouterLinkRouteFromCopy(copy)}` },
|
||||
name: 'root',
|
||||
}"
|
||||
>{{ getRouterLinkDisplayTextFromCopy(copy) }}</router-link
|
||||
>
|
||||
</span>
|
||||
<span v-else class="m-0 text-body" v-html="copy"></span>
|
||||
</span>
|
||||
<div class="alert-provide-vin" v-if="splitAlertProvideVinBodyForLink.length">
|
||||
<textBlock
|
||||
:customText="AlertProvideVinBody"
|
||||
justifyText="left"
|
||||
class="mb-3"
|
||||
marginTopSizeOverride="3" />
|
||||
</div>
|
||||
<funnelFooter
|
||||
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 alert from "@/ux-components/alert/alert";
|
||||
import addressVehiclesQuestion from "@/layouts/address-vehicles/address-vehicles-question/address-vehicles-question";
|
||||
import textBlock from "@/digital-components/text-block/text-block";
|
||||
|
||||
// Supporting files
|
||||
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
|
||||
|
|
@ -157,7 +151,6 @@ export default {
|
|||
if (
|
||||
store.getters.order.vehicle.carId &&
|
||||
store.getters.order.serviceLocation.zipCode &&
|
||||
store.getters.order.customer.emailAddress &&
|
||||
store.getters.pageData(fmgPageValues.ADDRESS_VEHICLES)
|
||||
) {
|
||||
return true;
|
||||
|
|
@ -237,6 +230,7 @@ export default {
|
|||
alert,
|
||||
funnelFooter,
|
||||
addressVehiclesQuestion,
|
||||
textBlock,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
58
src/layouts/customer-details/customer-details.spec.js
Normal file
58
src/layouts/customer-details/customer-details.spec.js
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
// Components
|
||||
import customerDetails from "@/layouts/customer-details/customer-details.vue";
|
||||
|
||||
// Supporting Files
|
||||
import { shallowMount } from "@vue/test-utils";
|
||||
import { getMountOptions } from "@/helpers/unit-test-helper.js";
|
||||
import * as navigateToHeritage from "@/helpers/heritage-integration/navigation-helper";
|
||||
|
||||
jest.mock("@/helpers/heritage-integration/navigation-helper", () => ({
|
||||
navigateToHeritageFunnel: jest.fn(),
|
||||
}));
|
||||
|
||||
// Mock our module for promises.
|
||||
jest.mock("@/helpers/layout-helper.js", () => ({
|
||||
settleAllPromises: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("customer-details.vue", () => {
|
||||
describe("navigation", () => {
|
||||
test("if the back button is clicked, navigate back", async () => {
|
||||
// Arrange
|
||||
const { wrapper } = setupMocks();
|
||||
|
||||
// Act
|
||||
await wrapper.vm.backButtonAction();
|
||||
|
||||
// Assert
|
||||
expect(wrapper.vm.$router.navigateWithoutSaving).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("if the continue button is clicked, navigate forward", async () => {
|
||||
// Arrange
|
||||
const { wrapper } = setupMocks();
|
||||
|
||||
// Act
|
||||
await wrapper.vm.forwardButtonAction();
|
||||
|
||||
// Assert
|
||||
expect(navigateToHeritage.navigateToHeritageFunnel).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function setupMocks() {
|
||||
const wrapper = shallowMount(
|
||||
customerDetails,
|
||||
getMountOptions({
|
||||
router: {
|
||||
navigate: jest.fn(),
|
||||
navigate: jest.fn(),
|
||||
navigateWithSaving: jest.fn(),
|
||||
navigateWithoutSaving: jest.fn(),
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
return { wrapper };
|
||||
}
|
||||
144
src/layouts/customer-details/customer-details.vue
Normal file
144
src/layouts/customer-details/customer-details.vue
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
<template>
|
||||
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm" v-slot="{ meta }">
|
||||
<div class="page-container-grouped-styles">
|
||||
<funnelHeader cmsWidgetName="FunnelHeaderWidget" />
|
||||
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" class="my-5" />
|
||||
<textboxQuestion
|
||||
class="mb-4"
|
||||
cmsWidgetName="FirstNameWidget"
|
||||
v-model="firstName"
|
||||
ref="firstName"
|
||||
customInputId="firstName"
|
||||
validationRules="first-name-required" />
|
||||
<textboxQuestion
|
||||
class="mb-4"
|
||||
cmsWidgetName="LastNameWidget"
|
||||
v-model="lastName"
|
||||
ref="lastName"
|
||||
customInputId="lastName"
|
||||
validationRules="last-name-required" />
|
||||
<textboxQuestion
|
||||
class="mb-4"
|
||||
cmsWidgetName="emailQuestionWidget"
|
||||
v-model="emailAddress"
|
||||
inputId="email"
|
||||
validationRules="email-address-required|email-address-format" />
|
||||
<phoneNumberQuestion
|
||||
class="mb-4"
|
||||
cmsWidgetName="phoneNumberQuestionWidget"
|
||||
v-model="phoneNumber"
|
||||
isRequired
|
||||
validationRules="phone-number-required" />
|
||||
<checkboxQuestion
|
||||
class="mb-5"
|
||||
cmsWidgetName="TextMeQuestionWidget"
|
||||
v-model="textMeUpdates" />
|
||||
<textareaQuestion
|
||||
class="mb-4"
|
||||
v-model="techNotes"
|
||||
cmsWidgetName="TextAreaContentWidget"
|
||||
maxLength="250" />
|
||||
<textBlock cmsWidgetName="DisclaimerCopyWidget" typeStyle="caption" />
|
||||
<funnel-footer
|
||||
cmsWidgetName="FunnelFooterWidget"
|
||||
ref="funnelFooter"
|
||||
:isForwardActionDisabled="!meta.valid"
|
||||
@back-clicked="backButtonAction"
|
||||
@ForwardClicked="forwardButtonAction" />
|
||||
</div>
|
||||
</Form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import funnelHeader from "@/fmg-components/funnel-header/funnel-header";
|
||||
import funnelFooter from "@/fmg-components/funnel-footer/funnel-footer";
|
||||
import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header";
|
||||
import textareaQuestion from "@/digital-components/textarea-question/textarea-question";
|
||||
import textboxQuestion from "@/digital-components/textbox-question/textbox-question";
|
||||
import phoneNumberQuestion from "@/digital-components/phone-number-question/phone-number-question";
|
||||
import textBlock from "@/digital-components/text-block/text-block";
|
||||
import checkboxQuestion from "@/digital-components/checkbox-question/checkbox-question";
|
||||
//Supporting files
|
||||
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
|
||||
import { settleAllPromises } from "@/helpers/layout-helper";
|
||||
import { storeActions } from "@/constants/store-actions";
|
||||
import { errorMessages } from "@/constants/error-messages";
|
||||
import { routerParams } from "@/router/router-constants/router-params";
|
||||
import { required, regex } from "@/helpers/validation-rules";
|
||||
import { Form, defineRule } from "vee-validate";
|
||||
import { useField, validate } from "vee-validate";
|
||||
import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
|
||||
|
||||
// DEFINE VALIDATION RULES
|
||||
defineRule("first-name-required", required(errorMessages.FIRST_NAME_REQUIRED));
|
||||
defineRule("last-name-required", required(errorMessages.LAST_NAME_REQUIRED));
|
||||
defineRule("phone-number-required", required(errorMessages.PHONE_REQUIRED));
|
||||
defineRule("email-address-required", required(errorMessages.EMAIL_ADDRESS_REQUIRED));
|
||||
defineRule(
|
||||
"email-address-format",
|
||||
regex(
|
||||
/^([a-zA-Z0-9_\-.+]+)@([a-zA-Z0-9-]+)\.([a-zA-Z]{2,})$/,
|
||||
errorMessages.EMAIL_ADDRESS_FORMAT
|
||||
)
|
||||
);
|
||||
|
||||
export default {
|
||||
name: "customer-details",
|
||||
async beforeRouteEnter(to, from, next) {
|
||||
// Call APIs
|
||||
const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage);
|
||||
|
||||
// Settle promises and get results
|
||||
const promiseResultMap = [
|
||||
{
|
||||
resultKey: "cmsContent",
|
||||
promise: cmsContentPromise,
|
||||
},
|
||||
];
|
||||
|
||||
const resultMap = await settleAllPromises(promiseResultMap);
|
||||
|
||||
// Call the "next" function to complete the transition to this page.
|
||||
next((vm) => {
|
||||
vm.setCmsContent(resultMap.cmsContent);
|
||||
});
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
techNotes: "",
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
emailAddress: "",
|
||||
phoneNumber: "",
|
||||
textMeUpdates: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
textAreaLabelCopy() {
|
||||
return this.getCmsContent("TextAreaContentWidget", "QuestionText");
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
arePagePrerequisitesValid() {
|
||||
return true;
|
||||
},
|
||||
backButtonAction() {
|
||||
this.$router.navigateWithoutSaving(this.navigationScenarios.CLICKED_BACK, this.$route);
|
||||
},
|
||||
forwardButtonAction() {
|
||||
navigateToHeritageFunnel({ loadingModal: this.$refs.loadingModal });
|
||||
},
|
||||
},
|
||||
components: {
|
||||
funnelHeader,
|
||||
funnelSubHeader,
|
||||
textareaQuestion,
|
||||
textboxQuestion,
|
||||
funnelFooter,
|
||||
Form,
|
||||
textBlock,
|
||||
phoneNumberQuestion,
|
||||
checkboxQuestion,
|
||||
},
|
||||
};
|
||||
</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>
|
||||
|
|
@ -12,7 +12,6 @@
|
|||
cmsWidgetName="AlertVinLookupQuestion"
|
||||
alertClass="" />
|
||||
<buttonQuestion
|
||||
class="minus"
|
||||
cmsWidgetName="VinLookupMethod"
|
||||
:answers="answersFromCms"
|
||||
groupName="vinLookupMethodOption"
|
||||
|
|
@ -45,8 +44,8 @@
|
|||
cmsWidgetName="EmailAddressQuestionWidget"
|
||||
v-model="emailAddress"
|
||||
inputId="emailAddress"
|
||||
isRequired
|
||||
validationRules="email-address-required|email-address-format" />
|
||||
disableAutoFill
|
||||
validationRules="email-address-format" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
|
|
@ -118,7 +117,6 @@ import { queryStrings } from "@/constants/query-strings";
|
|||
// Define Validation Rules
|
||||
defineRule("zip-required", required(errorMessages.SERVICE_ZIP_REQUIRED));
|
||||
defineRule("zip-format", regex(/(^\d{5}$)|(^\d{5}-\d{4}$)/, errorMessages.SERVICE_ZIP_FORMAT));
|
||||
defineRule("email-address-required", required(errorMessages.EMAIL_ADDRESS_REQUIRED));
|
||||
defineRule(
|
||||
"email-address-format",
|
||||
regex(
|
||||
|
|
@ -230,7 +228,7 @@ export default {
|
|||
const zipCodeData = await this.getZipCodeData(this.serviceZipCode);
|
||||
await this.dispatchStoreAction(storeActions.SAVE_EMAIL, this.emailAddress, false);
|
||||
await this.dispatchStoreAction(
|
||||
storeActions.SAVE_SERVICE_LOCATION,
|
||||
storeActions.SAVE_SERVICE_ZIP_CODE_INFO,
|
||||
{
|
||||
zipCode: this.serviceZipCode,
|
||||
state: zipCodeData.state,
|
||||
|
|
@ -360,7 +358,4 @@ export default {
|
|||
font-weight: 500;
|
||||
color: $black;
|
||||
}
|
||||
.minus {
|
||||
margin-bottom: -4px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
cmsWidgetName="LicensePlateNumberQuestionWidget"
|
||||
v-model="licensePlate"
|
||||
isRequired
|
||||
inputId="license_plate"
|
||||
customInputId="licensePlate"
|
||||
validationRules="license-plate-required" />
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -22,9 +22,9 @@
|
|||
<textboxQuestion
|
||||
cmsWidgetName="RegistrationZipQuestionWidget"
|
||||
v-model="registrationZipCode"
|
||||
inputId="zip"
|
||||
customInputId="zip"
|
||||
mask="#####"
|
||||
validationRules="zip-required|zip-format" />
|
||||
validationRules="registration-zip-required|zip-format" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-0">
|
||||
|
|
@ -32,8 +32,8 @@
|
|||
<textboxQuestion
|
||||
cmsWidgetName="EmailAddressQuestionWidget"
|
||||
v-model="email"
|
||||
inputId="email"
|
||||
validationRules="email-address-required|email-address-format" />
|
||||
customInputId="email"
|
||||
validationRules="email-address-format" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
|
|
@ -113,9 +113,8 @@ import vinPagesMixin from "@/mixins/vin-pages-mixin";
|
|||
|
||||
// DEFINE VALIDATION RULES
|
||||
defineRule("license-plate-required", required(errorMessages.LICENSE_PLATE_REQUIRED));
|
||||
defineRule("zip-required", required(errorMessages.REGISTRATION_ZIP_REQUIRED));
|
||||
defineRule("registration-zip-required", required(errorMessages.REGISTRATION_ZIP_REQUIRED));
|
||||
defineRule("zip-format", regex(/(^\d{5}$)|(^\d{5}-\d{4}$)/, errorMessages.SERVICE_ZIP_FORMAT));
|
||||
defineRule("email-address-required", required(errorMessages.EMAIL_ADDRESS_REQUIRED));
|
||||
defineRule(
|
||||
"email-address-format",
|
||||
regex(
|
||||
|
|
@ -300,11 +299,12 @@ export default {
|
|||
);
|
||||
|
||||
await this.dispatchStoreAction(storeActions.SAVE_EMAIL, this.email, false);
|
||||
|
||||
await this.dispatchStoreAction(
|
||||
storeActions.SAVE_SERVICE_LOCATION,
|
||||
storeActions.SAVE_SERVICE_ZIP_CODE_INFO,
|
||||
{
|
||||
zipCode: this.serviceZipCode,
|
||||
state: resultMap.serviceZipValidationResponse.state,
|
||||
zipCode: this.serviceZipCode,
|
||||
zipCodeCtu: resultMap.serviceZipValidationResponse.zipCodeCtu,
|
||||
},
|
||||
false
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ store.getters = {
|
|||
damage: baseStoreGettersDamage,
|
||||
order: {},
|
||||
};
|
||||
store.commit = jest.fn();
|
||||
store.dispatch = jest.fn();
|
||||
|
||||
afterEach(() => {
|
||||
// reset store after each test
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
:groupName="groupName"
|
||||
buttonTypeString="listButtonHorizontal"
|
||||
v-model="selectedValues"
|
||||
:additionalButtonData="{ additionalButtonStyling: 'listButtonHorizontalStrong' }"
|
||||
:additionalButtonData="additionalButtonData"
|
||||
isRequired />
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -27,6 +27,11 @@ export default {
|
|||
answersFromCms() {
|
||||
return this.getCmsContent(this.cmsWidgetName, "Answers");
|
||||
},
|
||||
additionalButtonData() {
|
||||
return {
|
||||
additionalButtonStyling: "listButtonHorizontalStrong",
|
||||
};
|
||||
},
|
||||
selectedValues: {
|
||||
get: function () {
|
||||
// Convert to CMS answer name from bool
|
||||
|
|
|
|||
|
|
@ -182,6 +182,7 @@ export default {
|
|||
this.isInsuranceSelected,
|
||||
false
|
||||
);
|
||||
|
||||
if (!this.isInsuranceSelected) {
|
||||
this.dispatchStoreAction(
|
||||
this.storeActions.SAVE_PARENT_ACCOUNT_NUMBER,
|
||||
|
|
@ -196,18 +197,21 @@ export default {
|
|||
) {
|
||||
this.supportingItems = this.filterOutFees(this.supportingItems);
|
||||
}
|
||||
|
||||
if (this.pricedGlassParts.length > 0) {
|
||||
this.dispatchStoreAction(
|
||||
this.storeActions.SAVE_GLASS_PARTS,
|
||||
this.storeActions.SAVE_GLASS_PART_PRICES,
|
||||
this.pricedGlassParts,
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
this.dispatchStoreAction(
|
||||
this.storeActions.SAVE_SUPPORTING_ITEMS,
|
||||
this.supportingItems,
|
||||
false
|
||||
);
|
||||
|
||||
this.dispatchStoreAction(this.storeActions.SAVE_VAPS, this.selectedVaps, false);
|
||||
|
||||
const payment = this.$store.getters.payment;
|
||||
|
|
@ -235,3 +239,8 @@ export default {
|
|||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
.text-block {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -38,8 +38,7 @@
|
|||
args: getRouterLinkRouteFromCopy(copy),
|
||||
})
|
||||
"
|
||||
:data-bs-target="'#' + getRouterLinkRouteFromCopy(copy)"
|
||||
aria-label="Modal window" />
|
||||
:data-bs-target="'#' + getRouterLinkRouteFromCopy(copy)" />
|
||||
</span>
|
||||
</template>
|
||||
</li>
|
||||
|
|
|
|||
|
|
@ -11,3 +11,10 @@ export async function getAlertReasons(ctu) {
|
|||
|
||||
return Promise.resolve(alertReasons);
|
||||
}
|
||||
|
||||
export function calcDaysBetweenDates(dateString1, dateString2) {
|
||||
const date1 = new Date(dateString1);
|
||||
const date2 = new Date(dateString2);
|
||||
const timeDifference = Math.abs(date2 - date1); // Calculate the time difference in milliseconds
|
||||
return Math.ceil(timeDifference / (1000 * 60 * 60 * 24)); // Convert milliseconds to days
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,8 +36,12 @@ export default {
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
loadInitialData(zipCodeCtu) {
|
||||
return getAlertReasons(zipCodeCtu);
|
||||
loadInitialData(serviceLocationCtu, providerCtu) {
|
||||
let ctuToUse = serviceLocationCtu;
|
||||
if (providerCtu) {
|
||||
ctuToUse = providerCtu;
|
||||
}
|
||||
return getAlertReasons(ctuToUse);
|
||||
},
|
||||
initializeComponent(initialData) {
|
||||
this.alertReasons = initialData;
|
||||
|
|
|
|||
|
|
@ -4,20 +4,13 @@
|
|||
<loadingModal ref="loadingModal" />
|
||||
<funnelHeader cmsWidgetName="FunnelHeaderWidget" />
|
||||
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" class="mt-5" />
|
||||
<div class="text-center mt-1 mb-3" v-if="ChangeShopLink.length">
|
||||
<span v-for="copy in ChangeShopLink" :key="copy">
|
||||
<span v-if="doesCopyContainRouterLink(copy)" class="text-body">
|
||||
<router-link
|
||||
:to="{
|
||||
query: { fmgPage: `${getRouterLinkRouteFromCopy(copy)}` },
|
||||
name: 'root',
|
||||
}"
|
||||
>{{ getRouterLinkDisplayTextFromCopy(copy) }}</router-link
|
||||
>
|
||||
</span>
|
||||
<span v-else class="m-0 text-body" v-html="copy"></span>
|
||||
</span>
|
||||
</div>
|
||||
<template v-if="ChangeShopLink.length">
|
||||
<textBlock
|
||||
cmsWidgetName="ChangeShopLink"
|
||||
justifyText="center"
|
||||
class="mb-3"
|
||||
marginTopSizeOverride="1" />
|
||||
</template>
|
||||
<location-alerts cmsWidgetPrefix="LocationAlert-" ref="locationAlerts" />
|
||||
<date-picker
|
||||
selectableDatesSetting="custom"
|
||||
|
|
@ -25,17 +18,18 @@
|
|||
v-model="selectedDate"
|
||||
:customSelectableDatesCallback="getAvailableDatesMethod"
|
||||
@date-clicked="openInshopTimeSlotsModal" />
|
||||
<!-- todayOverrideDateString="2023-08-06T03:00:00" -->
|
||||
<time-slot-modal-question
|
||||
ref="timeSlotModalQuestion"
|
||||
cmsWidgetName="TimeSlotModalQuestion"
|
||||
earlyBirdCmsWidgetName="EarlyBirdTimeSlotModal"
|
||||
mobilePremiumCmsWidgetName="MobilePremiumTimeSlotModal"
|
||||
mobileCmsWidgetName="MobileTimeSlotModal"
|
||||
dropoffCmsWidgetName="DropOffTimeSlotModal"
|
||||
v-model="selectedTimeSlotId"
|
||||
sameDayDropOffCmsWidgetName="SameDayDropOffTimeSlotModal"
|
||||
overnightDropOffCmsWidgetName="OvernightDropOffTimeSlotModal"
|
||||
v-model="selectedTimeSlotData"
|
||||
@time-slot-modal-closed="timeSlotModalClosed"
|
||||
:appointmentType="appointmentType"
|
||||
:mobileEarlyBirdFee="mobileEarlyBirdFee"
|
||||
:premiumAppointmentFee="mobilePremiumAppointmentFee"
|
||||
:dateAndTimeSlotData="timeSlotsForSelectedDate"
|
||||
:estimatedServiceMinutesMinimum="selectableDatesData.estimatedServiceMinutesMinimum"
|
||||
:estimatedServiceMinutesMaximum="selectableDatesData.estimatedServiceMinutesMaximum"
|
||||
|
|
@ -60,6 +54,7 @@ import { Form, defineRule } from "vee-validate";
|
|||
import datePicker from "@/digital-components/date-picker/date-picker";
|
||||
import locationAlerts from "@/layouts/schedule/location-alerts/location-alerts";
|
||||
import timeSlotModalQuestion from "./time-slot-modal-question/time-slot-modal-question";
|
||||
import textBlock from "@/digital-components/text-block/text-block";
|
||||
|
||||
// Supporting files
|
||||
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
|
||||
|
|
@ -67,12 +62,9 @@ import baseMixin from "@/mixins/base-mixin.js";
|
|||
import { storeActions } from "@/constants/store-actions";
|
||||
import { settleAllPromises } from "@/helpers/layout-helper";
|
||||
import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
|
||||
import {
|
||||
doesCopyContainRouterLink,
|
||||
splitCopyOnCMSPlaceHolder,
|
||||
getRouterLinkRouteFromCopy,
|
||||
getRouterLinkDisplayTextFromCopy,
|
||||
} from "@/helpers/cms-content-helper";
|
||||
import { splitCopyOnCMSPlaceHolder } from "@/helpers/cms-content-helper";
|
||||
import { calcDaysBetweenDates } from "@/layouts/schedule/helpers/schedule-helper";
|
||||
import { AppointmentTypeStrings, PREMIUM_FEE_PART_TYPE } from "@/constants/schedule-constants";
|
||||
import { errorMessages } from "@/constants/error-messages";
|
||||
import { required } from "@/helpers/validation-rules";
|
||||
import store from "@/store";
|
||||
|
|
@ -80,85 +72,154 @@ import store from "@/store";
|
|||
defineRule("date-required", required(errorMessages.DATE_REQUIRED));
|
||||
defineRule("time-slot-selection-required", required(errorMessages.DATE_REQUIRED));
|
||||
|
||||
const getAvailableDates = async (startDate, endDate, appointmentType) => {
|
||||
// USING DATES PASSED, MAKE AN API CALL
|
||||
const newShopTimeSlotsResponse = await baseMixin.methods.dispatchStoreAction(
|
||||
storeActions.GET_SHOP_TIME_SLOTS,
|
||||
{
|
||||
startDate: startDate,
|
||||
endDate: endDate,
|
||||
shopAppointmentType: appointmentType,
|
||||
},
|
||||
false
|
||||
);
|
||||
const newShopTimeSlots = newShopTimeSlotsResponse.data;
|
||||
return convertApiResponse(newShopTimeSlots);
|
||||
};
|
||||
const convertApiResponse = (responseData) => {
|
||||
// DATA CONVERSION
|
||||
responseData?.days.forEach((date) => {
|
||||
const dateString = date.date;
|
||||
date.dateString = dateString;
|
||||
const dateStringPieces = dateString.split("-");
|
||||
date.year = Number(dateStringPieces[0]);
|
||||
date.month = Number(dateStringPieces[1]);
|
||||
date.date = Number(dateStringPieces[2]);
|
||||
// Define constants
|
||||
const TIME_SLOTS_CALL_DAYS_LIMIT = 34; // needs to be 34 for API limits (35 does not consistently work)
|
||||
|
||||
const getAvailableDates = async (
|
||||
startDateString,
|
||||
endDateString,
|
||||
appointmentType,
|
||||
providerNumber
|
||||
) => {
|
||||
var today = new Date();
|
||||
var currentTime = today.getHours() + ":" + today.getMinutes() + ":" + today.getSeconds();
|
||||
|
||||
const apiEndDateLimit = new Date(startDateString + "T" + currentTime);
|
||||
const endDate = new Date(endDateString + "T" + currentTime);
|
||||
apiEndDateLimit.setDate(apiEndDateLimit.getDate() + TIME_SLOTS_CALL_DAYS_LIMIT);
|
||||
|
||||
// how many days are between startDate and endDate?
|
||||
const difference = calcDaysBetweenDates(startDateString, endDateString);
|
||||
const apiCallsCount = Math.ceil(difference / TIME_SLOTS_CALL_DAYS_LIMIT);
|
||||
const storeActionConfigs = [];
|
||||
const timeSlotsData = {};
|
||||
let apiStartDate = new Date(startDateString + "T" + currentTime);
|
||||
let apiEndDate = apiEndDateLimit;
|
||||
timeSlotsData.days = [];
|
||||
|
||||
for (let i = 1; i <= apiCallsCount; i++) {
|
||||
let storeActionConfig;
|
||||
|
||||
if (i > 1) {
|
||||
apiStartDate = new Date(apiEndDate);
|
||||
apiStartDate.setDate(apiStartDate.getDate() + 1);
|
||||
apiEndDate = new Date(apiStartDate);
|
||||
apiEndDate.setDate(apiEndDate.getDate() + TIME_SLOTS_CALL_DAYS_LIMIT);
|
||||
}
|
||||
if (i === apiCallsCount) {
|
||||
apiEndDate = new Date(endDateString + "T" + currentTime);
|
||||
}
|
||||
|
||||
if (appointmentType === AppointmentTypeStrings.MOBILE) {
|
||||
storeActionConfig = {
|
||||
storeAction: storeActions.GET_MOBILE_TIME_SLOTS,
|
||||
payload: {
|
||||
startDate: apiStartDate.toISOString().split("T")[0],
|
||||
endDate: apiEndDate.toISOString().split("T")[0],
|
||||
},
|
||||
};
|
||||
} else {
|
||||
storeActionConfig = {
|
||||
storeAction: storeActions.GET_SHOP_TIME_SLOTS,
|
||||
payload: {
|
||||
startDate: apiStartDate.toISOString().split("T")[0],
|
||||
endDate: apiEndDate.toISOString().split("T")[0],
|
||||
shopAppointmentType: appointmentType,
|
||||
providerNumber: providerNumber,
|
||||
},
|
||||
};
|
||||
}
|
||||
storeActionConfigs.push(storeActionConfig);
|
||||
}
|
||||
|
||||
// ASYNC METHOD
|
||||
const timeSlotsResponsesData = {
|
||||
days: [],
|
||||
};
|
||||
function compareDayStrings(a, b) {
|
||||
if (a.date < b.date) return -1;
|
||||
if (a.date > b.date) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
const makeParallelCalls = async () => {
|
||||
await Promise.all(
|
||||
storeActionConfigs.map(async (storeAction) => {
|
||||
const timeSlotsResponse = await baseMixin.methods.dispatchStoreAction(
|
||||
storeAction.storeAction,
|
||||
storeAction.payload,
|
||||
false
|
||||
);
|
||||
timeSlotsResponsesData.estimatedServiceMinutesMinimum =
|
||||
timeSlotsResponse.data.estimatedServiceMinutesMinimum;
|
||||
timeSlotsResponsesData.estimatedServiceMinutesMaximum =
|
||||
timeSlotsResponse.data.estimatedServiceMinutesMaximum;
|
||||
timeSlotsResponsesData.days = [
|
||||
...timeSlotsResponsesData.days,
|
||||
...timeSlotsResponse.data.days,
|
||||
];
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return makeParallelCalls().then(() => {
|
||||
// sort days chronologically
|
||||
timeSlotsResponsesData.days.sort(compareDayStrings);
|
||||
return timeSlotsResponsesData;
|
||||
});
|
||||
return responseData;
|
||||
};
|
||||
|
||||
export default {
|
||||
name: "schedule",
|
||||
data() {
|
||||
return {
|
||||
selectedDate: null,
|
||||
selectedTimeSlotId: null,
|
||||
selectedDate: this.getSelectedDate(),
|
||||
selectedTimeSlotData: {
|
||||
id: this.getSelectedRouteCode(),
|
||||
isPremiumAppointment: this.isMobilePremiumFeeOnOrderInVuex(),
|
||||
},
|
||||
selectableDatesData: [],
|
||||
mobileEarlyBirdFee: null,
|
||||
mobilePremiumAppointmentFee: null,
|
||||
};
|
||||
},
|
||||
async beforeRouteEnter(to, from, next) {
|
||||
// Call APIs
|
||||
const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage);
|
||||
const datePickerInitialDataPromise = datePicker.methods.loadInitialData({
|
||||
let preSelectedDate = await store.getters.order.schedule.date;
|
||||
if (!preSelectedDate || preSelectedDate.startTime === null) {
|
||||
preSelectedDate = null;
|
||||
}
|
||||
|
||||
const datePickerInitialDataPromise = await datePicker.methods.loadInitialData({
|
||||
// setup config options for date-picker
|
||||
selectableDatesSetting: "custom",
|
||||
initialViewRowsToShow: 5,
|
||||
customSelectableDatesCallback: getAvailableDates,
|
||||
|
||||
/* vvvvv SAVE THESE FOR TESTING PURPOSES FOR NOW vvvvv
|
||||
|
||||
// todayOverrideDateString: "2023-04-29T03:00:00", // show partial
|
||||
// todayOverrideDateString: "2023-04-30T03:00:00", //
|
||||
// todayOverrideDateString: "2023-05-02T03:00:00", // ONE MONTH ONLY
|
||||
// todayOverrideDateString: "2023-05-06T03:00:00", // ONE MONTH ONLY
|
||||
// todayOverrideDateString: "2023-05-07T03:00:00", // show partial
|
||||
// todayOverrideDateString: "2023-05-30T03:00:00", //
|
||||
// todayOverrideDateString: "2023-06-30T03:00:00", //
|
||||
// todayOverrideDateString: "2023-07-01T03:00:00", // show partial && ONE MONTH ONLY
|
||||
// todayOverrideDateString: "2023-07-02T03:00:00", // ONE MONTH ONLY
|
||||
// todayOverrideDateString: "2023-07-12T03:00:00", // show partial
|
||||
// todayOverrideDateString: "2023-08-31T03:00:00",
|
||||
// todayOverrideDateString: "2023-09-30T03:00:00", // show partial
|
||||
*/
|
||||
preSelectedDate: preSelectedDate,
|
||||
});
|
||||
|
||||
// Price EARLY BIRD pre-emptively to allow for asynchronous call to pricing
|
||||
const pricingPromise = baseMixin.methods.dispatchStoreAction(
|
||||
storeActions.PRICE_ORDER_ITEMS_AND_SAVE_SERVER_DATA,
|
||||
{
|
||||
availableLineItems: [{ partNumber: "EARLY BIRD" }],
|
||||
},
|
||||
false
|
||||
const premiumFeePromise = baseMixin.methods.dispatchStoreAction(
|
||||
storeActions.GET_MOBILE_PREMIUM_FEE
|
||||
);
|
||||
|
||||
const earlyBirdPromise = baseMixin.methods.dispatchStoreAction(
|
||||
storeActions.GET_MOBILE_EARLY_BIRD_FEE
|
||||
);
|
||||
const premiumFeeWithPricePromise = premiumFeePromise.then((result) => {
|
||||
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(
|
||||
store.getters.order.serviceLocation.zipCodeCtu
|
||||
store.getters.order.serviceLocation.zipCodeCtu,
|
||||
store.getters.order.serviceLocation.provider?.address?.zipCodeCtu
|
||||
);
|
||||
|
||||
// Settle promises and get results
|
||||
const promiseResultMap = [
|
||||
{
|
||||
|
|
@ -174,12 +235,8 @@ export default {
|
|||
promise: datePickerInitialDataPromise,
|
||||
},
|
||||
{
|
||||
resultKey: "earlyBird",
|
||||
promise: earlyBirdPromise,
|
||||
},
|
||||
{
|
||||
resultKey: "pricingResults",
|
||||
promise: pricingPromise,
|
||||
resultKey: "premiumFeeWithPrice",
|
||||
promise: premiumFeeWithPricePromise,
|
||||
},
|
||||
];
|
||||
|
||||
|
|
@ -191,9 +248,10 @@ export default {
|
|||
vm.$refs.datePicker.initializeComponent(resultMap.datePickerInitialData);
|
||||
vm.$refs.locationAlerts.initializeComponent(resultMap.alertReasons);
|
||||
vm.selectableDatesData = resultMap.datePickerInitialData.initialShopTimeSlotsResponse;
|
||||
if (resultMap.earlyBird) {
|
||||
vm.mobileEarlyBirdFee = resultMap.pricingResults[0];
|
||||
}
|
||||
vm.mobilePremiumAppointmentFee = resultMap.premiumFeeWithPrice
|
||||
? resultMap.premiumFeeWithPrice[0]
|
||||
: null;
|
||||
vm.updateFooterButtonText(vm.selectedTimeSlotData);
|
||||
});
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -208,26 +266,28 @@ export default {
|
|||
return this.$store.getters.order.serviceLocation.appointmentType;
|
||||
},
|
||||
timeSlotsForSelectedDate() {
|
||||
if (this.selectedDate === null) {
|
||||
if (!this.selectedDate) {
|
||||
return null;
|
||||
}
|
||||
return this.selectableDatesData.days.find(
|
||||
(selectableDate) => selectableDate.dateString === this.selectedDate.dateString
|
||||
return this.selectableDatesData.days?.find(
|
||||
(selectableDate) => selectableDate.date === this.selectedDate
|
||||
);
|
||||
},
|
||||
appointmentDateAndTime() {
|
||||
if (!this.selectedTimeSlotId) {
|
||||
if (!this.selectedTimeSlotData.id) {
|
||||
return null;
|
||||
}
|
||||
const timeSlotSelectedObject = this.getTimeSlotObjectFromTimeSlotId(
|
||||
this.selectedTimeSlotId
|
||||
this.selectedTimeSlotData.id
|
||||
);
|
||||
if (timeSlotSelectedObject) {
|
||||
return {
|
||||
date: this.selectedDate.dateString,
|
||||
date: this.selectedDate,
|
||||
startTime: timeSlotSelectedObject.startTime,
|
||||
endTime: timeSlotSelectedObject.endTime,
|
||||
id: this.selectedTimeSlotId,
|
||||
routeCode: this.selectedTimeSlotData.id,
|
||||
jobMaxMinutes:
|
||||
this.selectableDatesData.estimatedServiceMinutesMaximum.toString(),
|
||||
};
|
||||
} else {
|
||||
return null;
|
||||
|
|
@ -235,19 +295,29 @@ export default {
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
doesCopyContainRouterLink,
|
||||
splitCopyOnCMSPlaceHolder,
|
||||
getRouterLinkRouteFromCopy,
|
||||
getRouterLinkDisplayTextFromCopy,
|
||||
arePagePrerequisitesValid() {
|
||||
return true;
|
||||
// NEED TODO - WHAT ARE PAGE REQ'S FOR THIS PAGE?
|
||||
const serviceLocation = store.getters.order.serviceLocation;
|
||||
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) {
|
||||
const newShopTimeSlots = await getAvailableDates(
|
||||
startDate,
|
||||
endDate,
|
||||
this.appointmentType
|
||||
this.appointmentType,
|
||||
this.$store.getters.order.serviceLocation.provider.providerNumber
|
||||
);
|
||||
// ADD API CALL RESULTS TO EXISTING DATE DATA
|
||||
this.selectableDatesData.days = this.selectableDatesData.days.concat(
|
||||
|
|
@ -263,42 +333,155 @@ export default {
|
|||
},
|
||||
getTimeSlotObjectFromTimeSlotId(timeSlotId) {
|
||||
const timeSlots = this.selectableDatesData.days.find(
|
||||
(selectableDate) => selectableDate.dateString === this.selectedDate.dateString
|
||||
).timeSlots;
|
||||
return timeSlots.find((timeSlot) => timeSlot.id === timeSlotId);
|
||||
(selectableDate) => selectableDate.date === this.selectedDate
|
||||
)?.timeSlots;
|
||||
if (timeSlots) return timeSlots.find((timeSlot) => timeSlot.id === timeSlotId);
|
||||
},
|
||||
getSelectedDate() {
|
||||
return store.getters.order.schedule.date;
|
||||
},
|
||||
getSelectedRouteCode() {
|
||||
return store.getters.order.schedule.routeCode;
|
||||
},
|
||||
isMobilePremiumFeeOnOrderInVuex() {
|
||||
const supportingItemsFromVuex = store.getters.lineItems.supportingItems;
|
||||
return !!supportingItemsFromVuex.filter(
|
||||
(lineItem) => lineItem.partType === PREMIUM_FEE_PART_TYPE
|
||||
).length;
|
||||
},
|
||||
timeSlotModalClosed() {
|
||||
// Clear the selectedDate if no timeslot has been selected
|
||||
if (!this.selectedTimeSlotId) {
|
||||
// Clear the selectedDate if no timeSlot has been selected
|
||||
if (!this.selectedTimeSlotData.id) {
|
||||
this.selectedDate = null;
|
||||
}
|
||||
},
|
||||
updateFooterButtonText(timeSlotData) {
|
||||
let funnelFooterButtonText;
|
||||
if (!timeSlotData.id || !this.appointmentDateAndTime) {
|
||||
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() {
|
||||
this.$router.navigateWithoutSaving(this.navigationScenarios.CLICKED_BACK, this.$route);
|
||||
},
|
||||
async forwardButtonAction() {
|
||||
//TODO: replace properties with real values once they are available
|
||||
await this.dispatchStoreAction(
|
||||
forwardButtonAction() {
|
||||
this.updateSupportingItems();
|
||||
this.dispatchStoreAction(
|
||||
this.storeActions.SAVE_SCHEDULE,
|
||||
{
|
||||
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",
|
||||
},
|
||||
this.appointmentDateAndTime,
|
||||
false
|
||||
);
|
||||
//Temporary, still need to determine what needs to be saved before continuing.
|
||||
this.$router.navigateWithoutSaving(
|
||||
this.navigationScenarios.CLICKED_FORWARD,
|
||||
this.$route
|
||||
);
|
||||
},
|
||||
|
||||
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: {
|
||||
selectedDate(newValue, oldValue) {
|
||||
// Clear time slot selection if date selected changes
|
||||
if (newValue !== oldValue) {
|
||||
this.selectedTimeSlotId = null;
|
||||
this.selectedTimeSlotData = {
|
||||
id: null,
|
||||
isPremiumAppointment: null,
|
||||
};
|
||||
}
|
||||
},
|
||||
selectedTimeSlotData(newValue) {
|
||||
this.updateFooterButtonText(this.selectedTimeSlotData);
|
||||
},
|
||||
},
|
||||
components: {
|
||||
funnelHeader,
|
||||
|
|
@ -309,6 +492,7 @@ export default {
|
|||
datePicker,
|
||||
locationAlerts,
|
||||
timeSlotModalQuestion,
|
||||
textBlock,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -6,45 +6,32 @@
|
|||
<div
|
||||
:aria-label="buttonLabel"
|
||||
class="button-content list-button-content d-flex flex-column justify-content-center py-3 px-4">
|
||||
<span class="m-0" :class="textPosition">
|
||||
<span class="m-0 position-relative" :class="textPosition">
|
||||
{{ buttonLabel }}
|
||||
</span>
|
||||
<span v-if="buttonLabelSubCopy" class="m-0 small" :class="textPosition">
|
||||
{{ formattedButtonLabelSubCopy }}
|
||||
<span
|
||||
v-if="buttonLabelSubCopy"
|
||||
class="premium-appointment-price"
|
||||
:class="textPosition">
|
||||
{{ formattedButtonLabelSubCopy }}
|
||||
</span>
|
||||
</span>
|
||||
<span v-if="screenReaderOnlyText" class="sr-only">
|
||||
{{ screenReaderOnlyText }}
|
||||
</span>
|
||||
<loader
|
||||
v-if="isLoaderDisplayed && selectingInitiatesLoad"
|
||||
:class="[this.loaderColor, this.loaderPosition]" />
|
||||
</div>
|
||||
</baseInputButton>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import loader from "@/ux-components/loader/loader";
|
||||
import baseInputButton from "@/digital-components/base-input-button/base-input-button";
|
||||
import inputButtonWrapperMixin from "@/mixins/input-button-wrapper-mixin";
|
||||
|
||||
export default {
|
||||
name: "timeslotMOdalListButton",
|
||||
name: "timeSlotModalListButton",
|
||||
mixins: [inputButtonWrapperMixin],
|
||||
props: {
|
||||
loaderColor: String,
|
||||
loaderPosition: {
|
||||
type: String,
|
||||
default: "right",
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isLoaderDisplayed: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
formattedButtonLabelSubCopy() {
|
||||
return this.buttonLabelSubCopy?.toFixed(2);
|
||||
return this.buttonLabelSubCopy;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
|
@ -58,16 +45,12 @@ export default {
|
|||
},
|
||||
},
|
||||
components: {
|
||||
loader,
|
||||
baseInputButton,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.loader {
|
||||
position: absolute;
|
||||
}
|
||||
.list-button {
|
||||
outline: none;
|
||||
input[type="radio"],
|
||||
|
|
@ -85,6 +68,9 @@ export default {
|
|||
font-weight: 500;
|
||||
background: $blue-100;
|
||||
box-shadow: 0 0 0 1px $blue;
|
||||
span.premium-appointment-price {
|
||||
background: $green-200;
|
||||
}
|
||||
}
|
||||
&:checked:focus + .list-button-content {
|
||||
box-shadow: 0 0 0 2.5px $blue;
|
||||
|
|
@ -109,11 +95,20 @@ export default {
|
|||
width: 100%;
|
||||
outline: none;
|
||||
|
||||
span {
|
||||
&.small {
|
||||
font-size: 0.75rem;
|
||||
color: $gray-550;
|
||||
}
|
||||
span.premium-appointment-price {
|
||||
position: absolute;
|
||||
background: $green-100;
|
||||
border-radius: 4.5rem;
|
||||
line-height: 1.25rem;
|
||||
color: $green-700;
|
||||
font-size: 0.75rem;
|
||||
margin-left: 4px;
|
||||
padding: 2px 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.position-relative {
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -14,22 +14,22 @@
|
|||
typeStyle="small"
|
||||
class="duration-text-block" />
|
||||
<buttonQuestion
|
||||
buttonTypeString="timeslotModalListButton"
|
||||
:buttonTypeObject="timeslotModalListButton"
|
||||
buttonTypeString="timeSlotModalListButton"
|
||||
:buttonTypeObject="timeSlotModalListButton"
|
||||
:answers="availableTimeSlots"
|
||||
groupName="ChooseTimeSlot"
|
||||
textPosition="text-center"
|
||||
v-model="selectedTimeSlot"
|
||||
v-model="selectedTimeSlotId"
|
||||
isRequired
|
||||
validationRules="time-slot-required"
|
||||
class="mt-5" />
|
||||
<div
|
||||
class="mt-1 mb-2"
|
||||
class="mt-1 mb-2 supplemental-information"
|
||||
v-if="supplementalInformationBlock"
|
||||
v-html="supplementalInformationBlock"></div>
|
||||
<textBlock
|
||||
v-show="shouldShowDropoffDisclaimerText"
|
||||
:customText="dropoffDisclaimerText"
|
||||
v-show="disclaimerTextBlockCopy"
|
||||
:customText="disclaimerTextBlockCopy"
|
||||
justifyText="left"
|
||||
typeStyle="caption"
|
||||
class="mb-2" />
|
||||
|
|
@ -41,23 +41,25 @@
|
|||
import modal from "@/digital-components/modal/modal";
|
||||
import textBlock from "@/digital-components/text-block/text-block";
|
||||
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
|
||||
import { DAYS_OF_WEEK, MONTHS_OF_YEAR } from "@/digital-components/date-picker/mixins/constants.js";
|
||||
import { defineRule, useField } from "vee-validate";
|
||||
import { errorMessages } from "@/constants/error-messages";
|
||||
import { required } from "@/helpers/validation-rules";
|
||||
|
||||
// Constants
|
||||
import {
|
||||
AppointmentTypeStrings,
|
||||
PREMIUM_TIME_SLOT_ID_FLAG,
|
||||
PREMIUM_FEE_PART_TYPE,
|
||||
RouteCodeFlags,
|
||||
} from "@/constants/schedule-constants";
|
||||
|
||||
// Validation for the modal button
|
||||
defineRule("time-slot-required", required(errorMessages.OPTION_REQUIRED));
|
||||
|
||||
// Constants
|
||||
const AppointmentTypeStrings = {
|
||||
IN_SHOP: "Inshop",
|
||||
MOBILE: "Mobile",
|
||||
DROP_OFF: "Dropoff",
|
||||
};
|
||||
|
||||
export default {
|
||||
name: "timeSlotModalQuestion",
|
||||
|
|
@ -65,76 +67,104 @@ export default {
|
|||
modelValue: Object,
|
||||
cmsWidgetName: String,
|
||||
mobileCmsWidgetName: String,
|
||||
earlyBirdCmsWidgetName: String,
|
||||
mobilePremiumCmsWidgetName: String,
|
||||
dropoffCmsWidgetName: String,
|
||||
sameDayDropOffCmsWidgetName: String,
|
||||
overnightDropOffCmsWidgetName: String,
|
||||
appointmentType: String,
|
||||
dateAndTimeSlotData: Object,
|
||||
mobileEarlyBirdFee: Object,
|
||||
premiumAppointmentFee: Object,
|
||||
estimatedServiceMinutesMinimum: Number,
|
||||
estimatedServiceMinutesMaximum: Number,
|
||||
validationRules: String,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedTimeSlot: null,
|
||||
timeslotModalListButton: timeslotModalListButton,
|
||||
selectedTimeSlotId: this.getModifiedSelectedTimeSlotId(),
|
||||
timeSlotModalListButton: timeSlotModalListButton,
|
||||
};
|
||||
},
|
||||
setup(props) {
|
||||
const { handleChange } = useField("time-slot-modal-question", props.validationRules);
|
||||
|
||||
// Run validation on component load
|
||||
handleChange(props.modelValue.id);
|
||||
return {
|
||||
handleChange,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
modelValue() {
|
||||
this.selectedTimeSlot = this.modelValue;
|
||||
this.selectedTimeSlotId = this.getModifiedSelectedTimeSlotId();
|
||||
// Run component validation that is used at parent level
|
||||
this.handleChange(this.modelValue);
|
||||
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: {
|
||||
supplementalInformationBlock() {
|
||||
let appointmentTypeCmsWidgetName;
|
||||
let cmsFieldName = "BodyText";
|
||||
if (this.appointmentType === AppointmentTypeStrings.IN_SHOP) {
|
||||
return null;
|
||||
} else if (this.appointmentType === AppointmentTypeStrings.MOBILE) {
|
||||
appointmentTypeCmsWidgetName =
|
||||
this.selectedTimeSlot === this.earlyBirdButtonText
|
||||
? this.earlyBirdCmsWidgetName
|
||||
: this.mobileCmsWidgetName;
|
||||
appointmentTypeCmsWidgetName = this.selectedTimeSlotId?.includes(
|
||||
PREMIUM_TIME_SLOT_ID_FLAG
|
||||
)
|
||||
? this.mobilePremiumCmsWidgetName
|
||||
: this.mobileCmsWidgetName;
|
||||
} else {
|
||||
appointmentTypeCmsWidgetName = this.dropoffCmsWidgetName;
|
||||
if (this.isSameDay) {
|
||||
cmsFieldName = "BodyText2";
|
||||
if (!this.selectedTimeSlotId) {
|
||||
return null;
|
||||
} else {
|
||||
appointmentTypeCmsWidgetName =
|
||||
this.getRelevantDropOffCmsWidgetNameForSelectedTimeSlot(
|
||||
this.selectedTimeSlotId,
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
return this.getCmsContent(appointmentTypeCmsWidgetName, cmsFieldName);
|
||||
return this.getCmsContent(appointmentTypeCmsWidgetName, "BodyText");
|
||||
},
|
||||
footerCloseButtonText() {
|
||||
return this.getCmsContent(this.cmsWidgetName, "FooterText");
|
||||
},
|
||||
earlyBirdButtonText() {
|
||||
return this.getCmsContent(this.earlyBirdCmsWidgetName, "HeaderText");
|
||||
premiumAppointmentButtonText() {
|
||||
return this.getCmsContent(this.mobilePremiumCmsWidgetName, "HeaderText");
|
||||
},
|
||||
dropoffButtonText() {
|
||||
return this.getCmsContent(this.dropoffCmsWidgetName, "HeaderText");
|
||||
},
|
||||
overnightDropoffButtonText() {
|
||||
return this.getCmsContent(this.overnightDropOffCmsWidgetName, "HeaderText");
|
||||
},
|
||||
dropoffDisclaimerText() {
|
||||
return this.getCmsContent(this.dropoffCmsWidgetName, "FooterText");
|
||||
},
|
||||
overnightDropOffDisclaimerText() {
|
||||
return this.getCmsContent(this.overnightDropOffCmsWidgetName, "FooterText");
|
||||
},
|
||||
disclaimerTextBlockCopy() {
|
||||
if (this.appointmentType === AppointmentTypeStrings.DROP_OFF) {
|
||||
if (this.isSameDay) {
|
||||
return null;
|
||||
} else if (this.selectedTimeSlotId?.includes(RouteCodeFlags.ALL_DAY_DROP_OFF)) {
|
||||
return this.dropoffDisclaimerText;
|
||||
} else if (this.selectedTimeSlotId?.includes(RouteCodeFlags.OVERNIGHT_DROP_OFF)) {
|
||||
return this.overnightDropOffDisclaimerText;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
dropOffDurationText() {
|
||||
return this.getCmsContent(this.dropoffCmsWidgetName, "SubheaderText");
|
||||
},
|
||||
overnightDropoffDurationText() {
|
||||
return this.getCmsContent(this.overnightDropOffCmsWidgetName, "SubheaderText");
|
||||
},
|
||||
inshopDurationText() {
|
||||
const inshopDurationTextWithoutTime = this.getCmsContent(
|
||||
this.cmsWidgetName,
|
||||
|
|
@ -152,68 +182,52 @@ export default {
|
|||
} else if (this.appointmentType === AppointmentTypeStrings.IN_SHOP) {
|
||||
return this.inshopDurationText;
|
||||
} else {
|
||||
return this.dropOffDurationText;
|
||||
if (this.selectedTimeSlotId?.includes(RouteCodeFlags.OVERNIGHT_DROP_OFF)) {
|
||||
return this.overnightDropoffDurationText;
|
||||
} else if (this.selectedTimeSlotId?.includes(RouteCodeFlags.ALL_DAY_DROP_OFF)) {
|
||||
return this.dropOffDurationText;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
},
|
||||
shouldShowDropoffDisclaimerText() {
|
||||
return this.appointmentType === AppointmentTypeStrings.DROP_OFF && !this.isSameDay;
|
||||
},
|
||||
isSameDay() {
|
||||
return false;
|
||||
if (!this.dateAndTimeSlotData) {
|
||||
return false;
|
||||
}
|
||||
const selectedDate = this.dateAndTimeSlotData.date;
|
||||
const todaysDate = new Date().toISOString().split("T")[0];
|
||||
return selectedDate === todaysDate;
|
||||
},
|
||||
|
||||
dateSelectedReadableDate() {
|
||||
if (this.dateAndTimeSlotData === null) {
|
||||
if (!this.dateAndTimeSlotData) {
|
||||
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) {
|
||||
const offerPremium = this.dateAndTimeSlotData.timeSlots[0].offerPremium;
|
||||
const hasEarlyBird = this.mobileEarlyBirdFee?.partType === "EARLY BIRD";
|
||||
if (offerPremium && hasEarlyBird) {
|
||||
availableTimeSlots.unshift({
|
||||
value: this.dateAndTimeSlotData.timeSlots[0].id + "-earlybird",
|
||||
buttonLabel: this.earlyBirdButtonText,
|
||||
buttonLabelSubCopy: this.getTotalLineItemPrice(this.mobileEarlyBirdFee),
|
||||
});
|
||||
}
|
||||
// This conversion ensures we don't get get GMT induced date changes
|
||||
const dateObject = new Date(`${this.dateAndTimeSlotData.date}T00:00:00`);
|
||||
// Ex: Tuesday, April 22
|
||||
return dateObject.toLocaleDateString("en-us", {
|
||||
weekday: "long",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
});
|
||||
},
|
||||
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: {
|
||||
|
|
@ -222,22 +236,36 @@ export default {
|
|||
},
|
||||
// fires any time the footer button is used, is fired before "onModalClosed"
|
||||
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();
|
||||
},
|
||||
// fires any time the modal is closed, AFTER "closeModal" fires if footer button is used
|
||||
onModalClosed() {
|
||||
this.selectedTimeSlot = this.modelValue;
|
||||
this.$emit("time-slot-modal-closed");
|
||||
},
|
||||
// Expected input: "HH:MM:SS"
|
||||
getModifiedSelectedTimeSlotId() {
|
||||
if (this.modelValue.isPremiumAppointment) {
|
||||
return this.addPremiumFlagToInput(this.modelValue.id);
|
||||
} else {
|
||||
return this.modelValue.id;
|
||||
}
|
||||
},
|
||||
// Expected input: "HH:MM"
|
||||
getDisplayTextForMilitaryTime(militaryTimeInput) {
|
||||
let hours = parseInt(militaryTimeInput.split(":")[0]);
|
||||
const minutes = militaryTimeInput.split(":")[1];
|
||||
let meridianNotation = "AM";
|
||||
if (militaryTimeInput.split(":")[0] > 12) {
|
||||
const meridianNotation = hours > 11 ? "PM" : "AM";
|
||||
if (hours > 12) {
|
||||
hours -= 12;
|
||||
meridianNotation = "PM";
|
||||
}
|
||||
return `${hours}:${minutes} ${meridianNotation}`;
|
||||
},
|
||||
|
|
@ -252,6 +280,86 @@ export default {
|
|||
}
|
||||
return displayTextForDurationLength;
|
||||
},
|
||||
getRelevantDropOffCmsWidgetNameForSelectedTimeSlot(
|
||||
selectedTimeSlotId,
|
||||
isSameDayRelevant = false
|
||||
) {
|
||||
if (selectedTimeSlotId.includes(RouteCodeFlags.OVERNIGHT_DROP_OFF)) {
|
||||
return this.overnightDropOffCmsWidgetName;
|
||||
} else if (selectedTimeSlotId.includes(RouteCodeFlags.ALL_DAY_DROP_OFF)) {
|
||||
return this.isSameDay && isSameDayRelevant
|
||||
? this.sameDayDropOffCmsWidgetName
|
||||
: this.dropoffCmsWidgetName;
|
||||
}
|
||||
},
|
||||
getAvailableTimeSlotsForInshop(timeSlotsForSelectedDate) {
|
||||
return timeSlotsForSelectedDate.map((timeSlot) => {
|
||||
const readableTime = this.getDisplayTextForMilitaryTime(timeSlot.startTime);
|
||||
return {
|
||||
value: timeSlot.id,
|
||||
buttonLabel: readableTime,
|
||||
};
|
||||
});
|
||||
},
|
||||
getAvailableTimeSlotsForDropOff(timeSlotsForSelectedDate) {
|
||||
const availableTimeSlots = timeSlotsForSelectedDate.map((timeSlot) => {
|
||||
const buttonLabelValue = timeSlot.id.includes(RouteCodeFlags.OVERNIGHT_DROP_OFF)
|
||||
? this.overnightDropoffButtonText
|
||||
: this.dropoffButtonText;
|
||||
return {
|
||||
value: timeSlot.id,
|
||||
buttonLabel: buttonLabelValue,
|
||||
};
|
||||
});
|
||||
return availableTimeSlots;
|
||||
},
|
||||
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: {
|
||||
modal,
|
||||
|
|
@ -271,7 +379,17 @@ export default {
|
|||
margin-bottom: 0 !important;
|
||||
}
|
||||
.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>
|
||||
|
|
|
|||
|
|
@ -95,8 +95,10 @@ export default {
|
|||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.list-card img {
|
||||
height: auto;
|
||||
width: 3.417rem;
|
||||
.appointment-type-question {
|
||||
.list-card img {
|
||||
height: auto;
|
||||
width: 3.417rem;
|
||||
}
|
||||
}
|
||||
</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 store from "@/store";
|
||||
import baseMixin from "@/mixins/base-mixin.js";
|
||||
|
||||
export async function getPricedMobileFeePart(serviceZipCode) {
|
||||
|
|
@ -42,3 +43,48 @@ export async function getServiceabilityDetails(serviceZipCode, lineItems) {
|
|||
|
||||
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,187 @@
|
|||
import { getPricedMobileFeePart, getServiceabilityDetails } from "./service-location-helper";
|
||||
import {
|
||||
getPricedMobileFeePart,
|
||||
getServiceabilityDetails,
|
||||
getAvailabilityRating,
|
||||
} from "./service-location-helper";
|
||||
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,
|
||||
zipCode: null,
|
||||
zipCodeCtu: 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 mockStoreActionPriceOrderItemsAndSaveServerData =
|
||||
storeActions.PRICE_ORDER_ITEMS_AND_SAVE_SERVER_DATA;
|
||||
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", () => ({
|
||||
methods: {
|
||||
|
|
@ -18,7 +195,7 @@ jest.mock("@/mixins/base-mixin.js", () => ({
|
|||
});
|
||||
}),
|
||||
|
||||
dispatchStoreAction: jest.fn().mockImplementation((actionName) => {
|
||||
dispatchStoreAction: jest.fn().mockImplementation((actionName, request) => {
|
||||
if (actionName === mockStoreActionGetMobileFeePart) {
|
||||
return Promise.resolve({
|
||||
data: {
|
||||
|
|
@ -50,6 +227,14 @@ jest.mock("@/mixins/base-mixin.js", () => ({
|
|||
isRecalibrationServiceableMobile: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (actionName === mockStoreActionGetShopTimeSlots) {
|
||||
if (request.providerNumber == "0000001") {
|
||||
return Promise.resolve(mockGetShopTimeSlotsGoodAvailability);
|
||||
}
|
||||
|
||||
return Promise.resolve(mockGetShopTimeSlotsLowAvailability);
|
||||
}
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
|
@ -124,4 +309,25 @@ describe("service-location-helper.js", () => {
|
|||
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);
|
||||
// });
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,11 +2,6 @@ import mobileLocationModalQuestions from "./mobile-location-modal-questions";
|
|||
import { mount, shallowMount } from "@vue/test-utils";
|
||||
import { storeActions } from "@/constants/store-actions";
|
||||
import { getMountOptions } from "@/helpers/unit-test-helper.js";
|
||||
import modal from "@/digital-components/modal/modal";
|
||||
|
||||
import crypto from "crypto";
|
||||
|
||||
global.crypto = crypto;
|
||||
|
||||
const linkWidgetName = "linkWidgetName";
|
||||
const modalWidgetName = "modalWidgetName";
|
||||
|
|
|
|||
|
|
@ -14,8 +14,7 @@
|
|||
linkType="text"
|
||||
:text="mobileLocationLinkText"
|
||||
href="#!"
|
||||
@click-event="openModal"
|
||||
aria-label="Modal window" />
|
||||
@click-event="openModal" />
|
||||
</div>
|
||||
<div v-show="errorMessage" class="row my-1 form-test-error">
|
||||
<span class="d-inline-flex small mt-0 center-error-message" role="alert">
|
||||
|
|
@ -33,24 +32,27 @@
|
|||
:footerButtonText="modalFooterText"
|
||||
:onModalOpenedCallback="onModalOpened"
|
||||
:onModalClosedCallback="onModalClosed"
|
||||
@isModalOpened="setModalStatus"
|
||||
@footer-button-event="setMobileLocation">
|
||||
<addressQuestions
|
||||
ref="addressQuestions"
|
||||
v-model="internalModel.addressQuestions"
|
||||
captureApartmentNumberOrBusinessName="true"
|
||||
preserveCityAndStateOnReset="true" />
|
||||
<vehicleProtectedQuestion
|
||||
ref="vehicleProtectedQuestion"
|
||||
v-model="internalModel.isVehicleProtected"
|
||||
cmsWidgetName="VehicleProtectedQuestionWidget" />
|
||||
<textBlock cmsWidgetName="WorkspaceRequirementsWidget" typeStyle="caption" />
|
||||
<alert
|
||||
ref="alertInvalidZip"
|
||||
v-if="displayInvalidZipAlert"
|
||||
class="my-4"
|
||||
cmsWidgetName="AlertInvalidZipWidget"
|
||||
alertClass="alert-danger"
|
||||
v-bind:isDismissible="false" />
|
||||
<template v-if="isModalOpened">
|
||||
<addressQuestions
|
||||
ref="addressQuestions"
|
||||
v-model="internalModel.addressQuestions"
|
||||
captureApartmentNumberOrBusinessName="true"
|
||||
preserveCityAndStateOnReset="true" />
|
||||
<vehicleProtectedQuestion
|
||||
ref="vehicleProtectedQuestion"
|
||||
v-model="internalModel.isVehicleProtected"
|
||||
cmsWidgetName="VehicleProtectedQuestionWidget" />
|
||||
<textBlock cmsWidgetName="WorkspaceRequirementsWidget" typeStyle="caption" />
|
||||
<alert
|
||||
ref="alertInvalidZip"
|
||||
v-if="displayInvalidZipAlert"
|
||||
class="my-4"
|
||||
cmsWidgetName="AlertInvalidZipWidget"
|
||||
alertClass="alert-danger"
|
||||
v-bind:isDismissible="false" />
|
||||
</template>
|
||||
</modal>
|
||||
</div>
|
||||
</transition>
|
||||
|
|
@ -62,11 +64,11 @@ import textLink from "@/ux-components/text-link/text-link";
|
|||
import textBlock from "@/digital-components/text-block/text-block";
|
||||
import modal from "@/digital-components/modal/modal";
|
||||
import alert from "@/ux-components/alert/alert";
|
||||
import addressQuestions from "@/layouts/address-lookup/customer-questions/address-questions/address-questions";
|
||||
import addressQuestions from "@/fmg-components/address-questions/address-questions";
|
||||
import vehicleProtectedQuestion from "@/layouts/service-location/mobile-location-modal-questions/vehicle-protected-question/vehicle-protected-question";
|
||||
|
||||
// Helpers
|
||||
import { deepClone } from "@/layouts/service-location/helpers/object-cloning-helper/object-cloning-helper";
|
||||
import { deepClone } from "@/helpers/object-helper";
|
||||
import {
|
||||
getPricedMobileFeePart,
|
||||
getServiceabilityDetails,
|
||||
|
|
@ -74,6 +76,7 @@ import {
|
|||
|
||||
// Validation
|
||||
import { useField } from "vee-validate";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
export default {
|
||||
name: "mobile-location-modal-questions",
|
||||
|
|
@ -82,11 +85,13 @@ export default {
|
|||
return {
|
||||
internalModel: deepClone(this.modelValue),
|
||||
displayInvalidZipAlert: false,
|
||||
isModalOpened: false,
|
||||
};
|
||||
},
|
||||
setup(props) {
|
||||
const uuid = uuidv4();
|
||||
const componentId = !props.customComponentId
|
||||
? `component-${crypto.randomUUID()}`
|
||||
? `component-${uuid}`
|
||||
: props.customComponentId;
|
||||
|
||||
// Integrate this component as a single field with an object for it's value into the page level validation
|
||||
|
|
@ -185,6 +190,9 @@ export default {
|
|||
modalFooterText() {
|
||||
return this.getCmsContent(this.modalWidgetName, "FooterText");
|
||||
},
|
||||
modal() {
|
||||
return this.$refs[this.modalName];
|
||||
},
|
||||
addressModel: {
|
||||
get: function () {
|
||||
return this.modelValue.addressQuestions;
|
||||
|
|
@ -193,47 +201,23 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
openModal() {
|
||||
this.$refs[this.modalName].openModal();
|
||||
},
|
||||
closeModal() {
|
||||
this.$refs[this.modalName].closeModal();
|
||||
this.modal.openModal();
|
||||
},
|
||||
onModalOpened() {
|
||||
this.internalModel = deepClone(this.modelValue);
|
||||
},
|
||||
onModalClosed() {
|
||||
this.internalModel = deepClone(this.modelValue);
|
||||
this.resetValidation();
|
||||
setModalStatus(isOpened) {
|
||||
this.isModalOpened = isOpened;
|
||||
},
|
||||
resetComponent(updatedServiceZipCodeInfo) {
|
||||
// Reset the validation form, setting the initial values
|
||||
// for the state and zipCode to those that were entered
|
||||
// on the service-zip-modal-question component
|
||||
this.$refs[this.modalName].resetForm({
|
||||
values: {
|
||||
autocomplete: updatedServiceZipCodeInfo.streetAddress,
|
||||
city: updatedServiceZipCodeInfo.city,
|
||||
state: updatedServiceZipCodeInfo.state,
|
||||
zipCode: updatedServiceZipCodeInfo.zipCode,
|
||||
isVehicleProtected: updatedServiceZipCodeInfo.isVehicleProtected,
|
||||
},
|
||||
});
|
||||
closeModal() {
|
||||
this.modal.closeModal();
|
||||
},
|
||||
onModalClosed() {
|
||||
this.displayInvalidZipAlert = false;
|
||||
this.internalModel = deepClone(this.modelValue);
|
||||
},
|
||||
resetModalButtonStyle() {
|
||||
this.$refs[this.modalName].resetButtonStyle();
|
||||
},
|
||||
resetValidation() {
|
||||
this.$refs.addressQuestions.resetAlerts();
|
||||
|
||||
this.$refs[this.modalName].resetForm({
|
||||
values: {
|
||||
autocomplete: this.internalModel.addressQuestions.streetAddress,
|
||||
city: this.internalModel.addressQuestions.city,
|
||||
state: this.internalModel.addressQuestions.state,
|
||||
zipCode: this.internalModel.addressQuestions.zipCode,
|
||||
isVehicleProtected: this.internalModel.isVehicleProtected,
|
||||
},
|
||||
});
|
||||
this.modal.resetButtonStyle();
|
||||
},
|
||||
async setMobileLocation() {
|
||||
if (
|
||||
|
|
@ -261,15 +245,19 @@ export default {
|
|||
this.$emit("updated-serviceability", serviceabilityDetails.data);
|
||||
this.$emit("updated-contains-military-base", zipCodeData.containsMilitaryBase);
|
||||
|
||||
// Update the page level model
|
||||
this.$emit("update:modelValue", this.internalModel);
|
||||
|
||||
if (this.onZipUpdateCallback) {
|
||||
await this.onZipUpdateCallback(serviceZipCode);
|
||||
}
|
||||
// Update the page level model
|
||||
this.$emit("update:modelValue", this.internalModel);
|
||||
|
||||
this.closeModal();
|
||||
}
|
||||
} else {
|
||||
// Update the page level model
|
||||
this.$emit("update:modelValue", this.internalModel);
|
||||
|
||||
this.closeModal();
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
@ -279,14 +267,6 @@ export default {
|
|||
this.internalModel = deepClone(newValue);
|
||||
|
||||
this.handleChange(newValue);
|
||||
|
||||
this.resetComponent({
|
||||
streetAddress: newValue.addressQuestions.streetAddress,
|
||||
city: newValue.addressQuestions.city,
|
||||
state: newValue.addressQuestions.state,
|
||||
zipCode: newValue.addressQuestions.zipCode,
|
||||
isVehicleProtected: newValue.isVehicleProtected,
|
||||
});
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -68,15 +68,13 @@
|
|||
linkWidgetName="MobileLocationLinkWidget"
|
||||
modalWidgetName="MobileLocationModalWidget"
|
||||
:onZipUpdateCallback="reloadShopData" />
|
||||
<Transition name="fade" mode="out-in">
|
||||
<shopQuestion
|
||||
ref="shopQuestion"
|
||||
v-show="isShopQuestionDisplayed"
|
||||
v-model="selectedProvider"
|
||||
:selectedAppointmentType="selectedAppointmentType"
|
||||
:isDisplayed="isShopQuestionDisplayed"
|
||||
cmsWidgetName="ShopQuestionWidget" />
|
||||
</Transition>
|
||||
<shopQuestion
|
||||
ref="shopQuestion"
|
||||
v-show="isShopQuestionDisplayed"
|
||||
v-model="selectedProvider"
|
||||
:selectedAppointmentType="selectedAppointmentType"
|
||||
:isDisplayed="isShopQuestionDisplayed"
|
||||
cmsWidgetName="ShopQuestionWidget" />
|
||||
<contentGroupModal ref="RecalModal" cmsWidgetName="RecalModal" />
|
||||
<funnel-footer
|
||||
cmsWidgetName="FunnelFooterWidget"
|
||||
|
|
@ -108,7 +106,6 @@ import baseMixin from "@/mixins/base-mixin.js";
|
|||
|
||||
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
|
||||
import { settleAllPromises } from "@/helpers/layout-helper";
|
||||
import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
|
||||
import {
|
||||
getPricedMobileFeePart,
|
||||
getServiceabilityDetails,
|
||||
|
|
@ -138,11 +135,11 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
streetAddress: this.getServiceAddressFromStore(),
|
||||
apartmentNumberOrBusinessName: "",
|
||||
apartmentNumberOrBusinessName: this.getServiceAddress2FromStore(),
|
||||
city: this.getServiceCityFromStore(),
|
||||
state: this.getServiceStateFromStore(),
|
||||
zipCode: this.getServiceZipCodeFromStore(),
|
||||
isVehicleProtected: null,
|
||||
isVehicleProtected: this.getIsVehicleProtectedFromStore(),
|
||||
isGlassServiceableInshop: null,
|
||||
isRecalibrationServiceableInshop: null,
|
||||
isGlassServiceableMobile: null,
|
||||
|
|
@ -339,6 +336,9 @@ export default {
|
|||
getServiceAddressFromStore() {
|
||||
return store.getters.order.serviceLocation.address;
|
||||
},
|
||||
getServiceAddress2FromStore() {
|
||||
return store.getters.order.serviceLocation.address2;
|
||||
},
|
||||
getServiceCityFromStore() {
|
||||
return store.getters.order.serviceLocation.city;
|
||||
},
|
||||
|
|
@ -348,6 +348,9 @@ export default {
|
|||
getServiceZipCodeFromStore() {
|
||||
return store.getters.order.serviceLocation.zipCode;
|
||||
},
|
||||
getIsVehicleProtectedFromStore() {
|
||||
return store.getters.order.serviceLocation.isVehicleProtected;
|
||||
},
|
||||
getSelectedAppointmentType() {
|
||||
return store.getters.order.serviceLocation.appointmentType;
|
||||
},
|
||||
|
|
@ -390,17 +393,15 @@ export default {
|
|||
streetAddress: this.selectedProvider?.address?.streetAddress,
|
||||
city: this.selectedProvider?.address?.city,
|
||||
state: this.selectedProvider?.address?.state,
|
||||
zip: this.selectedProvider?.address?.zipCode,
|
||||
zipCode: this.selectedProvider?.address?.zipCode,
|
||||
zipCodeCtu: this.selectedProvider?.address?.zipCodeCtu,
|
||||
},
|
||||
},
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
this.$router.navigateWithSaving(
|
||||
this.navigationScenarios.SELECTED_LOCATION,
|
||||
this.$route
|
||||
);
|
||||
this.$router.navigateWithSaving(this.navigationScenarios.CLICKED_FORWARD, this.$route);
|
||||
},
|
||||
openModalAction(modalName) {
|
||||
this.$refs[modalName].openModal();
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
import { mount, shallowMount } from "@vue/test-utils";
|
||||
import serviceZipModalQuestion from "./service-zip-modal-question";
|
||||
import crypto from "crypto";
|
||||
global.crypto = crypto;
|
||||
|
||||
jest.mock("@/digital-components/textbox-question/textbox-question", () => ({
|
||||
getCmsContent: jest.fn((widgetName, cmsFieldName) => {
|
||||
|
|
|
|||
|
|
@ -6,8 +6,7 @@
|
|||
linkType="text"
|
||||
:text="serviceZipLinkText"
|
||||
href="#!"
|
||||
@click-event="openModal"
|
||||
aria-label="Modal window" />
|
||||
@click-event="openModal" />
|
||||
</div>
|
||||
</div>
|
||||
<modal
|
||||
|
|
@ -22,12 +21,12 @@
|
|||
customInputId="serviceZipCode"
|
||||
v-model="internalModel.zipCode"
|
||||
v-on="{ 'textboxQuestionEvent.inputIdAssigned': onInputIdAssigned }"
|
||||
:cmsWidgetName="textboxQuestionWidgetName" />
|
||||
cmsWidgetName="ServiceZipQuestionWidget" />
|
||||
<alert
|
||||
ref="alertInvalidZip"
|
||||
v-if="displayInvalidZipAlert"
|
||||
class="my-4"
|
||||
:cmsWidgetName="alertInvalidZipWidgetName"
|
||||
cmsWidgetName="AlertInvalidZipWidget"
|
||||
alertClass="alert-danger"
|
||||
v-bind:isDismissible="false" />
|
||||
</modal>
|
||||
|
|
@ -73,15 +72,6 @@ export default {
|
|||
type: Function,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const textboxQuestionWidgetName = "ServiceZipQuestionWidget";
|
||||
const alertInvalidZipWidgetName = "AlertInvalidZipWidget";
|
||||
|
||||
return {
|
||||
textboxQuestionWidgetName,
|
||||
alertInvalidZipWidgetName,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
serviceZipLinkText() {
|
||||
if (this.modelValue.zipCode && this.modelValue.zipCode.length > 0) {
|
||||
|
|
@ -90,7 +80,7 @@ export default {
|
|||
return this.getCmsContent(this.linkWidgetName, "BodyText");
|
||||
},
|
||||
modalHeaderText() {
|
||||
return this.getCmsContent(this.textboxQuestionWidgetName, "QuestionText");
|
||||
return this.getCmsContent("ServiceZipQuestionWidget", "QuestionText");
|
||||
},
|
||||
modalFooterText() {
|
||||
return this.getCmsContent(this.modalWidgetName, "FooterText");
|
||||
|
|
@ -98,6 +88,9 @@ export default {
|
|||
modalName() {
|
||||
return this.modalWidgetName;
|
||||
},
|
||||
modal() {
|
||||
return this.$refs[this.modalName];
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
resetAlerts() {
|
||||
|
|
@ -117,13 +110,13 @@ export default {
|
|||
};
|
||||
},
|
||||
openModal() {
|
||||
this.$refs[this.modalName].openModal();
|
||||
this.modal.openModal();
|
||||
},
|
||||
closeModal() {
|
||||
this.$refs[this.modalName].closeModal();
|
||||
this.modal.closeModal();
|
||||
},
|
||||
resetModalButtonStyle() {
|
||||
this.$refs[this.modalName].resetButtonStyle();
|
||||
this.modal.resetButtonStyle();
|
||||
},
|
||||
onInputIdAssigned(inputId) {
|
||||
this.serviceZipCodeTextInputId = inputId;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { mount } from "@vue/test-utils";
|
|||
import shopListButton from "./shop-list-button";
|
||||
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 () => {
|
||||
// Arrange
|
||||
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 = {
|
||||
buttonLabel: "buttonLabel test copy",
|
||||
buttonLabelSubCopy: "buttonLabelSubCopy test copy",
|
||||
|
|
@ -58,6 +70,12 @@ const mockProps = {
|
|||
value: 0,
|
||||
modelValue: 0,
|
||||
groupName: "mockGroup",
|
||||
additionalButtonData: {
|
||||
availabilityRatingCallback: getAvailabilityRating,
|
||||
startDate: formattedStartDate,
|
||||
endDate: formattedEndDate,
|
||||
shopAppointmentType: "Dropoff",
|
||||
},
|
||||
};
|
||||
|
||||
function setupMocks({ mountOptionsMockData = {} }) {
|
||||
|
|
|
|||
|
|
@ -11,26 +11,32 @@
|
|||
<span class="m-0 button-label-copy" :class="textPosition">{{
|
||||
buttonLabel
|
||||
}}</span>
|
||||
<span class="m-0 button-label-sub-copy" :class="textPosition">{{
|
||||
<span class="m-0 caption ms-1" :class="textPosition">{{
|
||||
buttonLabelSubCopy
|
||||
}}</span>
|
||||
<div
|
||||
class="availability-indicator"
|
||||
:class="availability === 'high' ? 'green' : 'red'">
|
||||
<span class="m-0 button-auxillary-copy">{{ buttonAuxillaryCopy }}</span>
|
||||
v-if="displayAvailabilityIndicators"
|
||||
class="availability-indicator rounded-pill"
|
||||
: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>
|
||||
<span
|
||||
v-if="buttonBodyCopy"
|
||||
class="m-0 button-label-sub-copy small"
|
||||
:class="textPosition"
|
||||
v-html="buttonBodyCopy"></span>
|
||||
<div class="row-two">
|
||||
<span
|
||||
v-if="buttonBodyCopy"
|
||||
class="m-0 button-label-sub-copy small"
|
||||
v-html="buttonBodyCopy"></span>
|
||||
</div>
|
||||
<span v-if="screenReaderOnlyText" class="sr-only">
|
||||
{{ screenReaderOnlyText }}
|
||||
</span>
|
||||
<loader
|
||||
v-if="isLoaderDisplayed && selectingInitiatesLoad"
|
||||
:class="[this.loaderColor, this.loaderPosition]" />
|
||||
</div>
|
||||
</baseInputButton>
|
||||
</transition>
|
||||
|
|
@ -40,32 +46,60 @@
|
|||
import loader from "@/ux-components/loader/loader";
|
||||
import baseInputButton from "@/digital-components/base-input-button/base-input-button";
|
||||
import inputButtonWrapperMixin from "@/mixins/input-button-wrapper-mixin";
|
||||
import experimentMixin from "@/mixins/experiment-mixin";
|
||||
import { experimentSettings } from "@/constants/experiments";
|
||||
|
||||
export default {
|
||||
name: "shopListButton",
|
||||
mixins: [inputButtonWrapperMixin],
|
||||
props: {
|
||||
loaderColor: String,
|
||||
loaderPosition: {
|
||||
type: String,
|
||||
default: "right",
|
||||
},
|
||||
beforeMount() {
|
||||
if (this.displayAvailabilityIndicators) {
|
||||
this.displayLoader();
|
||||
const startDate = this.additionalButtonData.startDate;
|
||||
const endDate = this.additionalButtonData.endDate;
|
||||
const shopAppointmentType = this.additionalButtonData.shopAppointmentType;
|
||||
|
||||
this.additionalButtonData
|
||||
.availabilityRatingCallback(startDate, endDate, shopAppointmentType, this.value)
|
||||
.then((data) => {
|
||||
this.availabilityRating = data;
|
||||
});
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isLoaderDisplayed: false,
|
||||
availability: "low",
|
||||
availabilityRating: null,
|
||||
};
|
||||
},
|
||||
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: {
|
||||
displayLoader() {
|
||||
this.isLoaderDisplayed = true;
|
||||
},
|
||||
preHandleAnswerChange() {
|
||||
if (this.selectingInitiatesLoad) {
|
||||
this.displayLoader();
|
||||
}
|
||||
},
|
||||
},
|
||||
components: {
|
||||
loader,
|
||||
|
|
@ -75,9 +109,6 @@ export default {
|
|||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.loader {
|
||||
position: absolute;
|
||||
}
|
||||
.list-button {
|
||||
outline: none;
|
||||
input[type="radio"],
|
||||
|
|
@ -103,10 +134,6 @@ export default {
|
|||
&:checked + .list-button-content span {
|
||||
font-weight: 500;
|
||||
}
|
||||
&:checked + .list-button-content span:nth-child(2) {
|
||||
font-weight: 400;
|
||||
color: $gray-600;
|
||||
}
|
||||
}
|
||||
}
|
||||
.list-button-content {
|
||||
|
|
@ -127,53 +154,83 @@ export default {
|
|||
}
|
||||
}
|
||||
.button-content {
|
||||
row-gap: 0.25rem;
|
||||
|
||||
.row-one {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 0.25rem !important;
|
||||
line-height: 1.5rem;
|
||||
|
||||
.button-label-copy {
|
||||
flex-grow: 0;
|
||||
line-height: 1.5rem;
|
||||
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 {
|
||||
display: none;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
background-repeat: no-repeat;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.125rem 1.5rem;
|
||||
gap: 0.25rem;
|
||||
background: #e3f2ea;
|
||||
border-radius: 4.5rem;
|
||||
margin-left: auto;
|
||||
padding: 0.125rem 0.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 {
|
||||
box-sizing: border-box;
|
||||
justify-content: right;
|
||||
line-height: 1.25rem !important;
|
||||
font-weight: 400;
|
||||
line-height: 1.25rem;
|
||||
font-weight: 500;
|
||||
font-size: 0.75rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.green {
|
||||
color: #006a36;
|
||||
background: #e3f2ea;
|
||||
.loader {
|
||||
padding: 0 0.25rem 0 0.25rem;
|
||||
padding-top: 0.125rem;
|
||||
padding-bottom: 0.125rem;
|
||||
}
|
||||
|
||||
.red {
|
||||
color: #ac160b;
|
||||
background: #e3f2ea;
|
||||
.loader:after {
|
||||
background-color: $gray-550;
|
||||
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>
|
||||
|
|
|
|||
|
|
@ -19,7 +19,8 @@
|
|||
textPosition="text-start"
|
||||
v-model="selectedProviderNumber"
|
||||
isRequired
|
||||
validationRules="option-required" />
|
||||
validationRules="option-required"
|
||||
:additionalButtonData="additionalButtonData" />
|
||||
<textLink
|
||||
v-show="displaySeeMoreLocationsLink"
|
||||
ref="showMoreShopsLink"
|
||||
|
|
@ -51,6 +52,8 @@ import { errorMessages } from "@/constants/error-messages";
|
|||
import baseMixin from "@/mixins/base-mixin.js";
|
||||
import { nextTick } from "vue";
|
||||
|
||||
import { getAvailabilityRating } from "@/layouts/service-location/helpers/service-location-helper/service-location-helper";
|
||||
|
||||
defineRule("option-required", required(errorMessages.OPTION_REQUIRED));
|
||||
|
||||
export default {
|
||||
|
|
@ -102,6 +105,21 @@ export default {
|
|||
showMoreShopsLinkText() {
|
||||
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: {
|
||||
loadInitialData(serviceZipCode) {
|
||||
|
|
@ -158,7 +176,7 @@ export default {
|
|||
});
|
||||
}
|
||||
|
||||
await this.$nextTick();
|
||||
await nextTick();
|
||||
|
||||
if (this.shopIndex == this.shopProviders.length) {
|
||||
this.displaySeeMoreLocationsLink = false;
|
||||
|
|
@ -166,7 +184,7 @@ export default {
|
|||
this.displaySeeMoreLocationsLink = true;
|
||||
}
|
||||
|
||||
await this.$nextTick();
|
||||
await nextTick();
|
||||
|
||||
this.scrollToPageBottom();
|
||||
},
|
||||
|
|
@ -203,22 +221,20 @@ export default {
|
|||
async handler(newValue) {
|
||||
this.resetAnswers();
|
||||
|
||||
await this.$nextTick();
|
||||
await nextTick();
|
||||
|
||||
this.selectedProviderNumber = null;
|
||||
|
||||
this.$refs.buttonQuestion?.resetField();
|
||||
|
||||
await this.$nextTick();
|
||||
await nextTick();
|
||||
|
||||
if (newValue !== "Mobile") {
|
||||
await this.getNextShopsFromList();
|
||||
this.getNextShopsFromList();
|
||||
}
|
||||
},
|
||||
},
|
||||
shopProviders: {
|
||||
async handler(newValue) {
|
||||
await this.$nextTick();
|
||||
await nextTick();
|
||||
|
||||
if (this.selectedAppointmentType) {
|
||||
const selectedShopIndex = this.getSelectedProviderIndex(
|
||||
|
|
@ -230,7 +246,7 @@ export default {
|
|||
await this.getNextShopsFromList(selectedShopIndex + 1);
|
||||
} else {
|
||||
await this.getNextShopsFromList();
|
||||
await this.$nextTick();
|
||||
await nextTick();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -261,14 +277,16 @@ export default {
|
|||
background-repeat: no-repeat;
|
||||
background-size: 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 {
|
||||
text-align: left;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
p {
|
||||
padding-left: 0.5rem;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
fmg
|
||||
<template>
|
||||
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm" v-slot="{ meta }">
|
||||
<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({
|
||||
selectedGlassParts: { "Rear-Stationary": { partNumber: "DB12209GTYN" } },
|
||||
|
|
@ -498,7 +498,7 @@ describe("vehicle-parts.vue", () => {
|
|||
await wrapper.vm.forwardButtonAction();
|
||||
|
||||
//Assert
|
||||
expect(wrapper.vm.$store.commit).toHaveBeenCalled();
|
||||
expect(wrapper.vm.dispatchStoreAction).toHaveBeenCalled();
|
||||
|
||||
// TODO KO UNCOMMENT FOR QUOTE PAGES RELEASE
|
||||
// expect(wrapper.vm.$router.navigateWithSaving).toHaveBeenCalledWith(
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
<textboxQuestion
|
||||
cmsWidgetName="VinNumberQuestionWidget"
|
||||
v-model="vin"
|
||||
inputId="vin"
|
||||
customInputId="vin"
|
||||
isRequired
|
||||
validationRules="vin-required|vin-format"
|
||||
:isDisabled="vinPopulatedOnPageLoad"
|
||||
|
|
@ -46,7 +46,7 @@
|
|||
<textboxQuestion
|
||||
cmsWidgetName="ServiceZipQuestionWidget"
|
||||
v-model="serviceZipCode"
|
||||
inputId="serviceZipCode"
|
||||
customInputId="serviceZipCode"
|
||||
mask="#####"
|
||||
isRequired
|
||||
validationRules="zip-required|zip-format" />
|
||||
|
|
@ -57,9 +57,9 @@
|
|||
<textboxQuestion
|
||||
cmsWidgetName="EmailAddressQuestionWidget"
|
||||
v-model="emailAddress"
|
||||
inputId="emailAddress"
|
||||
customInputId="emailAddress"
|
||||
isRequired
|
||||
validationRules="email-address-required|email-address-format" />
|
||||
validationRules="email-address-format" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-0">
|
||||
|
|
@ -152,7 +152,6 @@ import vinPagesMixin from "@/mixins/vin-pages-mixin";
|
|||
// DEFINE VALIDATION RULES
|
||||
defineRule("zip-required", required(errorMessages.SERVICE_ZIP_REQUIRED));
|
||||
defineRule("zip-format", regex(/(^\d{5}$)|(^\d{5}-\d{4}$)/, errorMessages.SERVICE_ZIP_FORMAT));
|
||||
defineRule("email-address-required", required(errorMessages.EMAIL_ADDRESS_REQUIRED));
|
||||
defineRule(
|
||||
"email-address-format",
|
||||
regex(
|
||||
|
|
@ -323,10 +322,10 @@ export default {
|
|||
|
||||
await this.dispatchStoreAction(storeActions.SAVE_EMAIL, this.emailAddress, false);
|
||||
await this.dispatchStoreAction(
|
||||
storeActions.SAVE_SERVICE_LOCATION,
|
||||
storeActions.SAVE_SERVICE_ZIP_CODE_INFO,
|
||||
{
|
||||
zipCode: this.serviceZipCode,
|
||||
state: resultMap.zipCodeData.state,
|
||||
zipCode: this.serviceZipCode,
|
||||
zipCodeCtu: resultMap.zipCodeData.zipCodeCtu,
|
||||
},
|
||||
false
|
||||
|
|
@ -352,18 +351,18 @@ export default {
|
|||
{
|
||||
address: vehicleRegistrationInfo.address,
|
||||
city: vehicleRegistrationInfo.city,
|
||||
zipCode: vehicleRegistrationInfo.zipCode,
|
||||
state: vehicleRegistrationInfo.state,
|
||||
zipCode: vehicleRegistrationInfo.zipCode,
|
||||
zipCodeCtu: zipCodeData.zipCodeCtu,
|
||||
},
|
||||
false
|
||||
);
|
||||
} else {
|
||||
await this.dispatchStoreAction(
|
||||
storeActions.SAVE_SERVICE_LOCATION,
|
||||
storeActions.SAVE_SERVICE_ZIP_CODE_INFO,
|
||||
{
|
||||
zipCode: this.serviceZipCode,
|
||||
state: zipCodeData.state,
|
||||
zipCode: this.serviceZipCode,
|
||||
zipCodeCtu: zipCodeData.zipCodeCtu,
|
||||
},
|
||||
false
|
||||
|
|
|
|||
|
|
@ -23,6 +23,10 @@ import { applicationConfig } from "../constants/application-config";
|
|||
|
||||
export default {
|
||||
methods: {
|
||||
getPageName() {
|
||||
return getPageNameByQueryString();
|
||||
},
|
||||
|
||||
logPageView(pageEvent) {
|
||||
const currentPageName = getPageNameByQueryString();
|
||||
var payload = {
|
||||
|
|
|
|||
|
|
@ -442,7 +442,7 @@ export default {
|
|||
const collectedGlassParts = this.reducedGlassPartsArray(partsOrQuestions);
|
||||
|
||||
// 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;
|
||||
|
||||
|
|
|
|||
|
|
@ -2240,15 +2240,17 @@ describe("vehicle-questions-mixin", () => {
|
|||
|
||||
// Assert
|
||||
expect(wrapper.vm.$store.commit).toHaveBeenCalledTimes(1);
|
||||
expect(wrapper.vm.$store.commit).toHaveBeenCalledWith(
|
||||
storeMutations.UPDATE_GLASS_PARTS,
|
||||
expect(wrapper.vm.$store.dispatch).toHaveBeenCalledWith(
|
||||
storeActions.SAVE_GLASS_PARTS,
|
||||
collectedGlassParts
|
||||
);
|
||||
|
||||
expect(wrapper.vm.$router.navigateWithSaving).toHaveBeenCalledTimes(1);
|
||||
expect(wrapper.vm.$router.navigateWithSaving).toHaveBeenCalledWith(
|
||||
navigationScenarios.CLICKED_FORWARD_WITH_NO_MORE_QUESTIONS,
|
||||
{ query: { fmgPage: "vin-lookup" } }
|
||||
);
|
||||
|
||||
expect(wrapper.vm.$router.navigateWithoutSaving).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -28,22 +28,8 @@ import analyticsMixin from "@/mixins/analytics-mixin";
|
|||
import { experimentTriggers } from "../constants/experiments";
|
||||
import { applicationConfig } from "../constants/application-config";
|
||||
|
||||
// Components
|
||||
import datePicker from "@/digital-components/date-picker/date-picker.vue";
|
||||
import demoDatePicker from "@/layouts/demo-date-picker/demo-date-picker.vue";
|
||||
import review from "@/layouts/review/review";
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: "/demo-date-picker", // This is a temporary route for testing.
|
||||
name: "demo-date-picker",
|
||||
component: demoDatePicker,
|
||||
},
|
||||
{
|
||||
path: "/date-picker", // This is a temporary route for testing.
|
||||
name: "date-picker",
|
||||
component: datePicker,
|
||||
},
|
||||
{
|
||||
path: "/review", // This is a temporary route for testing.
|
||||
name: "review",
|
||||
|
|
@ -176,6 +162,14 @@ const router = createRouter({
|
|||
|
||||
//---------------------------------------------------------- Router Functions ----------------------------------------------------------
|
||||
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
// set lastNavigationPage here to capture state before API calls for analytics.
|
||||
// use current page url query string name when to.name is "root" (due to unresolved navigation in beforeEach)
|
||||
router.lastNavigationPage = to.name == "root" ? analyticsMixin.methods.getPageName() : to.name;
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
router.afterEach(async (to, from) => {
|
||||
// Update lastPageVisited in the store
|
||||
store.commit(storeMutations.UPDATE_LAST_PAGE_VISITED, to.name);
|
||||
|
|
@ -242,6 +236,12 @@ router.overrideNavigation = (
|
|||
next();
|
||||
};
|
||||
|
||||
router.getNextPage = () => nextPageName;
|
||||
|
||||
// PRIVATE VARIABLES
|
||||
|
||||
var nextPageName;
|
||||
|
||||
// PRIVATE FUNCTIONS
|
||||
|
||||
// Navigate to the next route, depending on the scenario.
|
||||
|
|
@ -265,6 +265,8 @@ async function navigate(
|
|||
if (destinationFmgPageValue !== undefined) {
|
||||
// We're always pushing the same path, just changing query strings. Make sure our optional query strings get combined with our fmgPage one.
|
||||
|
||||
nextPageName = destinationFmgPageValue;
|
||||
|
||||
// Update page data to the store for next page if provided. Otherwise, keep existing page data or set to empty object
|
||||
const existingPageDataForPage = store.getters.pageData(destinationFmgPageValue);
|
||||
baseMixin.methods.savePageDataToStore(
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ const fmgPageValues = {
|
|||
SERVICE_LOCATION: "service-location",
|
||||
HERITAGE: "heritage",
|
||||
SCHEDULE: "schedule",
|
||||
CUSTOMER_DETAILS: "customer-details",
|
||||
REVIEW: "review",
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -46,9 +46,6 @@ const navigationScenarios = {
|
|||
CLICKED_BACK_WITH_VIN_AND_NO_MORE_QUESTIONS: "CLICKED_BACK_WITH_VIN_AND_NO_MORE_QUESTIONS",
|
||||
CLICKED_BACK_WITH_NO_VIN_NOR_QUESTIONS: "CLICKED_BACK_WITH_NO_VIN_NOR_QUESTIONS",
|
||||
|
||||
// Scheduling
|
||||
SELECTED_LOCATION: "SELECTED_LOCATION",
|
||||
|
||||
// Review
|
||||
CLICKED_VEHICLE_EDIT: "CLICKED_VEHICLE_EDIT",
|
||||
CLICKED_DAMAGE_EDIT: "CLICKED_DAMAGE_EDIT",
|
||||
|
|
|
|||
|
|
@ -421,7 +421,7 @@ const routingTable = function (store) {
|
|||
destinationFmgPageValue: fmgPageValues.QUOTE,
|
||||
},
|
||||
{
|
||||
scenario: navigationScenarios.SELECTED_LOCATION,
|
||||
scenario: navigationScenarios.CLICKED_FORWARD,
|
||||
destinationFmgPageValue: fmgPageValues.SCHEDULE,
|
||||
},
|
||||
],
|
||||
|
|
@ -433,6 +433,23 @@ const routingTable = function (store) {
|
|||
scenario: navigationScenarios.CLICKED_BACK,
|
||||
destinationFmgPageValue: fmgPageValues.SERVICE_LOCATION,
|
||||
},
|
||||
{
|
||||
scenario: navigationScenarios.CLICKED_FORWARD,
|
||||
destinationFmgPageValue: fmgPageValues.CUSTOMER_DETAILS,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
fmgPageValue: fmgPageValues.CUSTOMER_DETAILS,
|
||||
maps: [
|
||||
{
|
||||
scenario: navigationScenarios.CLICKED_BACK,
|
||||
destinationFmgPageValue: fmgPageValues.SCHEDULE,
|
||||
},
|
||||
{
|
||||
scenario: navigationScenarios.CLICKED_FORWARD,
|
||||
destinationFmgPageValue: fmgPageValues.REVIEW,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
@ -446,6 +463,10 @@ const routingTable = function (store) {
|
|||
scenario: navigationScenarios.CLICKED_DAMAGE_EDIT,
|
||||
destinationFmgPageValue: fmgPageValues.VEHICLE_DAMAGE,
|
||||
},
|
||||
{
|
||||
scenario: navigationScenarios.CLICKED_BACK,
|
||||
destinationFmgPageValue: fmgPageValues.CUSTOMER_DETAILS,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { createStore, Store } from "vuex";
|
||||
import { createStore } from "vuex";
|
||||
import { endpoints } from "@/constants/endpoints.js";
|
||||
import { storeMutations } from "@/constants/store-mutations";
|
||||
import { getDateForSavedSessionTimeout } from "@/helpers/heritage-integration/session-helper";
|
||||
|
|
@ -8,9 +8,11 @@ import { storeActions } from "@/constants/store-actions";
|
|||
import { applicationConfig } from "@/constants/application-config";
|
||||
import { experimentTriggers } from "@/constants/experiments";
|
||||
import { damageLocationsSelected } from "@/constants/damage-locations-selected";
|
||||
import { singleWindshieldCarIds } from "@/constants/single-windshield-carids";
|
||||
import { fmgPageValues } from "@/router/router-constants/fmgPage-values";
|
||||
import router from "@/router";
|
||||
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
|
||||
const getDefaultState = () => {
|
||||
|
|
@ -39,18 +41,21 @@ const getDefaultState = () => {
|
|||
},
|
||||
serviceLocation: {
|
||||
address: null,
|
||||
address2: null,
|
||||
city: null,
|
||||
state: null,
|
||||
zipCode: null,
|
||||
zipCodeCtu: null,
|
||||
appointmentType: null,
|
||||
isVehicleProtected: null,
|
||||
provider: {
|
||||
providerNumber: null,
|
||||
address: {
|
||||
streetAddress: null,
|
||||
city: null,
|
||||
state: null,
|
||||
zip: null,
|
||||
zipCode: null,
|
||||
zipCodeCtu: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -84,6 +89,7 @@ const getDefaultState = () => {
|
|||
startTime: null,
|
||||
endTime: null,
|
||||
routeCode: null,
|
||||
jobMaxMinutes: null,
|
||||
},
|
||||
referralNumber: null,
|
||||
referralDate: null,
|
||||
|
|
@ -239,6 +245,11 @@ export const mutations = {
|
|||
state.order.vehicle.registration.firstName = registrationInfo?.firstName;
|
||||
state.order.vehicle.registration.lastName = registrationInfo?.lastName;
|
||||
},
|
||||
updateServiceZip(state, serviceZipInfo) {
|
||||
state.order.serviceLocation.state = serviceZipInfo.state;
|
||||
state.order.serviceLocation.zipCode = serviceZipInfo.zipCode;
|
||||
state.order.serviceLocation.zipCodeCtu = serviceZipInfo.zipCodeCtu;
|
||||
},
|
||||
updateServiceLocation(state, serviceLocationInfo) {
|
||||
state.order.serviceLocation.address = serviceLocationInfo.address;
|
||||
state.order.serviceLocation.address2 = serviceLocationInfo.address2;
|
||||
|
|
@ -249,18 +260,16 @@ export const mutations = {
|
|||
state.order.serviceLocation.appointmentType = serviceLocationInfo.appointmentType;
|
||||
state.order.serviceLocation.isVehicleProtected = serviceLocationInfo.isVehicleProtected;
|
||||
|
||||
if (serviceLocationInfo.provider) {
|
||||
state.order.serviceLocation.provider.providerNumber =
|
||||
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;
|
||||
}
|
||||
state.order.serviceLocation.provider = {
|
||||
providerNumber: serviceLocationInfo.provider?.providerNumber,
|
||||
address: {
|
||||
streetAddress: serviceLocationInfo.provider?.address?.streetAddress,
|
||||
city: serviceLocationInfo.provider?.address?.city,
|
||||
state: serviceLocationInfo.provider?.address?.state,
|
||||
zipCode: serviceLocationInfo.provider?.address?.zipCode,
|
||||
zipCodeCtu: serviceLocationInfo.provider?.address?.zipCodeCtu,
|
||||
},
|
||||
};
|
||||
},
|
||||
updateSchedule(state, scheduleInfo) {
|
||||
if (scheduleInfo) {
|
||||
|
|
@ -268,6 +277,17 @@ export const mutations = {
|
|||
state.order.schedule.startTime = scheduleInfo.startTime;
|
||||
state.order.schedule.endTime = scheduleInfo.endTime;
|
||||
state.order.schedule.routeCode = scheduleInfo.routeCode;
|
||||
state.order.schedule.jobMaxMinutes = scheduleInfo.jobMaxMinutes;
|
||||
}
|
||||
},
|
||||
updateCustomerDetails(state, detailsInfo) {
|
||||
if (detailsInfo) {
|
||||
state.order.customerDetails.firstName = detailsInfo.firstName;
|
||||
state.order.customerDetails.lastName = detailsInfo.lastName;
|
||||
state.order.customerDetails.email = detailsInfo.email;
|
||||
state.order.customerDetails.telephone = detailsInfo.telephone;
|
||||
state.order.customerDetails.textUpdates = detailsInfo.textUpdates;
|
||||
state.order.customerDetails.techNotes = detailsInfo.techNotes;
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -338,12 +358,52 @@ export const mutations = {
|
|||
state.applicationUser.pageData[fmgPageValues.MOLDING_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;
|
||||
|
||||
//premium appointment fee used on schedule page also needs reset when schedule is reset
|
||||
const supportingItems = state.order.lineItems.supportingItems;
|
||||
const premiumAppointmentFeeIndex = supportingItems?.findIndex(
|
||||
(item) => item.partType == PREMIUM_FEE_PART_TYPE
|
||||
);
|
||||
|
||||
if (premiumAppointmentFeeIndex >= 0) {
|
||||
supportingItems.splice(premiumAppointmentFeeIndex, 1);
|
||||
state.order.lineItems.supportingItems = supportingItems;
|
||||
}
|
||||
},
|
||||
resetState(state) {
|
||||
Object.assign(state, getDefaultState());
|
||||
},
|
||||
resetSaveSessionPromise(state) {
|
||||
state.applicationUser.saveSessionPromise = null;
|
||||
},
|
||||
resetServiceLocationAppointmentType(state) {
|
||||
state.order.serviceLocation.appointmentType = null;
|
||||
},
|
||||
resetServiceLocationProvider(state) {
|
||||
state.order.serviceLocation.provider = {
|
||||
providerNumber: null,
|
||||
address: {
|
||||
streetAddress: null,
|
||||
city: null,
|
||||
state: null,
|
||||
zipCode: null,
|
||||
zipCodeCtu: 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.isVehicleProtected = null;
|
||||
},
|
||||
// Misc Mutations
|
||||
updateStateWithOrderInformation(state, sessionInformation) {
|
||||
state.order.referralNumber = sessionInformation.order.referralNumber;
|
||||
|
|
@ -395,18 +455,20 @@ export const mutations = {
|
|||
state.order.lineItems.supportingItems = sessionInformation.order.lineItems.supportingItems;
|
||||
state.order.lineItems.vaps = sessionInformation.order.lineItems.vaps;
|
||||
state.order.lineItems.serverData = sessionInformation.order.lineItems.serverData;
|
||||
|
||||
state.order.payment.parentAccountNumber =
|
||||
sessionInformation.order.payment.parentAccountNumber;
|
||||
(state.order.serviceLocation.address =
|
||||
sessionInformation.order.serviceLocation.streetAddress),
|
||||
(state.order.serviceLocation.address2 =
|
||||
sessionInformation.order.serviceLocation.address2),
|
||||
(state.order.serviceLocation.city = sessionInformation.order.serviceLocation.city),
|
||||
(state.order.serviceLocation.state = sessionInformation.order.serviceLocation.state),
|
||||
(state.order.serviceLocation.zipCode =
|
||||
sessionInformation.order.serviceLocation.zipCode),
|
||||
(state.order.serviceLocation.zipCodeCtu =
|
||||
sessionInformation.order.serviceLocation.zipCodeCtu);
|
||||
|
||||
state.order.serviceLocation.address =
|
||||
sessionInformation.order.serviceLocation.streetAddress;
|
||||
state.order.serviceLocation.address2 =
|
||||
sessionInformation.order.serviceLocation.streetAddress2;
|
||||
state.order.serviceLocation.city = sessionInformation.order.serviceLocation.city;
|
||||
state.order.serviceLocation.state = sessionInformation.order.serviceLocation.state;
|
||||
state.order.serviceLocation.zipCode = sessionInformation.order.serviceLocation.zipCode;
|
||||
state.order.serviceLocation.zipCodeCtu =
|
||||
sessionInformation.order.serviceLocation.zipCodeCtu;
|
||||
|
||||
state.order.serviceLocation.appointmentType =
|
||||
sessionInformation.order.serviceLocation.appointmentType;
|
||||
state.order.serviceLocation.isVehicleProtected =
|
||||
|
|
@ -420,8 +482,10 @@ export const mutations = {
|
|||
sessionInformation.order.serviceLocation.provider?.address?.city;
|
||||
state.order.serviceLocation.provider.address.state =
|
||||
sessionInformation.order.serviceLocation.provider?.address?.state;
|
||||
state.order.serviceLocation.provider.address.zip =
|
||||
sessionInformation.order.serviceLocation.provider?.address?.zip;
|
||||
state.order.serviceLocation.provider.address.zipCode =
|
||||
sessionInformation.order.serviceLocation.provider?.address?.zipCode;
|
||||
state.order.serviceLocation.provider.address.zipCodeCtu =
|
||||
sessionInformation.order.serviceLocation.provider?.address?.zipCodeCtu;
|
||||
|
||||
state.order.payment.isInsurance = sessionInformation.order.payment.isInsurance;
|
||||
state.order.payment.insuranceCoverage.isVerified =
|
||||
|
|
@ -440,6 +504,7 @@ export const mutations = {
|
|||
state.order.schedule.startTime = sessionInformation.order.schedule?.startTime;
|
||||
state.order.schedule.endTime = sessionInformation.order.schedule?.endTime;
|
||||
state.order.schedule.routeCode = sessionInformation.order.schedule?.routeCode;
|
||||
state.order.schedule.jobMaxMinutes = sessionInformation.order.schedule?.jobMaxMinutes;
|
||||
},
|
||||
updateExperiments(state, experiments) {
|
||||
state.applicationUser.experiments = experiments;
|
||||
|
|
@ -551,6 +616,7 @@ export const actions = {
|
|||
payload: {},
|
||||
});
|
||||
},
|
||||
|
||||
lookupVehicleByYmms(context, { year, make, model, style }) {
|
||||
return globalMethods.callHttpClient({
|
||||
method: endpoints.LookupVehicleByYmms.method,
|
||||
|
|
@ -558,6 +624,7 @@ export const actions = {
|
|||
payload: {},
|
||||
});
|
||||
},
|
||||
|
||||
lookupVehicleByVin(context, { vin }) {
|
||||
return globalMethods.callHttpClient({
|
||||
method: endpoints.LookupVehicleByVin.method,
|
||||
|
|
@ -567,6 +634,7 @@ export const actions = {
|
|||
},
|
||||
});
|
||||
},
|
||||
|
||||
lookupVinByPlate(context, { licensePlate, licenseState }) {
|
||||
return globalMethods.callHttpClient({
|
||||
method: endpoints.LookupVinByPlate.method,
|
||||
|
|
@ -577,6 +645,7 @@ export const actions = {
|
|||
},
|
||||
});
|
||||
},
|
||||
|
||||
lookupVinByAddress(
|
||||
context,
|
||||
{ licenseLastName, licenseStreetAddress, licenseZip, licenseState }
|
||||
|
|
@ -592,16 +661,33 @@ export const actions = {
|
|||
},
|
||||
});
|
||||
},
|
||||
|
||||
lookupVinByImage(context, image) {
|
||||
const data = new FormData();
|
||||
data.append("vinImage", image);
|
||||
return globalMethods.callHttpClient({
|
||||
method: endpoints.LookupVinByImage.method,
|
||||
endpoint: endpoints.LookupVinByImage.url,
|
||||
payload: data,
|
||||
isFormData: true,
|
||||
return new Promise((resolve, reject) => {
|
||||
let reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
resolve(reader.result);
|
||||
};
|
||||
reader.readAsDataURL(image);
|
||||
}).then((result) => {
|
||||
const components = result.split(",");
|
||||
const contentType = image.type;
|
||||
const imageBase64 = components[1];
|
||||
|
||||
const data = {
|
||||
imageData: imageBase64,
|
||||
contentType: contentType,
|
||||
fileName: image.name,
|
||||
};
|
||||
|
||||
return globalMethods.callHttpClient({
|
||||
method: endpoints.LookupVinByImage.method,
|
||||
endpoint: endpoints.LookupVinByImage.url,
|
||||
payload: data,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
isVinByAddressPermissible(context, zip) {
|
||||
return globalMethods.callHttpClient({
|
||||
method: endpoints.IsVinByAddressPermissible.method,
|
||||
|
|
@ -609,6 +695,7 @@ export const actions = {
|
|||
payload: {},
|
||||
});
|
||||
},
|
||||
|
||||
getVehicleMakes(context, { year }) {
|
||||
return globalMethods.callHttpClient({
|
||||
method: endpoints.GetVehicleMakes.method,
|
||||
|
|
@ -616,6 +703,7 @@ export const actions = {
|
|||
payload: {},
|
||||
});
|
||||
},
|
||||
|
||||
getVehicleModels(context, { year, make }) {
|
||||
return globalMethods.callHttpClient({
|
||||
method: endpoints.GetVehicleModels.method,
|
||||
|
|
@ -623,6 +711,7 @@ export const actions = {
|
|||
payload: {},
|
||||
});
|
||||
},
|
||||
|
||||
getVehicleStyles(context, { year, make, model }) {
|
||||
return globalMethods.callHttpClient({
|
||||
method: endpoints.GetVehicleStyles.method,
|
||||
|
|
@ -630,6 +719,7 @@ export const actions = {
|
|||
payload: {},
|
||||
});
|
||||
},
|
||||
|
||||
setVehicle(context, { year, make, model, style }) {
|
||||
return globalMethods
|
||||
.callHttpClient({
|
||||
|
|
@ -652,6 +742,7 @@ export const actions = {
|
|||
return response;
|
||||
});
|
||||
},
|
||||
|
||||
getDamageOptions(context, { carId }) {
|
||||
return globalMethods.callHttpClient({
|
||||
methods: endpoints.GetDamageOptions.method,
|
||||
|
|
@ -659,6 +750,7 @@ export const actions = {
|
|||
payload: {},
|
||||
});
|
||||
},
|
||||
|
||||
validateZip(context, { zip }) {
|
||||
return globalMethods.callHttpClient({
|
||||
methods: endpoints.ValidateZip.method,
|
||||
|
|
@ -669,22 +761,36 @@ export const actions = {
|
|||
// Dependency Actions
|
||||
resetDamageAndDependencies(context) {
|
||||
context.commit(storeMutations.RESET_DAMAGE_STATE);
|
||||
context.commit(storeMutations.RESET_GLASS_PARTS_STATE);
|
||||
context.commit(storeMutations.UPDATE_SUPPORTING_ITEMS, null);
|
||||
|
||||
context.dispatch(storeActions.RESET_PARTS_STATE_AND_DEPENDENCIES);
|
||||
|
||||
context.commit(storeMutations.UPDATE_VAPS, null);
|
||||
},
|
||||
|
||||
resetRegistrationAndDependencies(context) {
|
||||
context.commit(storeMutations.RESET_REGISTRATION_STATE);
|
||||
context.commit(storeMutations.RESET_GLASS_PARTS_STATE);
|
||||
context.commit(storeMutations.UPDATE_SUPPORTING_ITEMS, null);
|
||||
|
||||
context.dispatch(storeActions.RESET_PARTS_STATE_AND_DEPENDENCIES);
|
||||
},
|
||||
|
||||
resetPartsAndDependencies(context) {
|
||||
context.commit(storeMutations.RESET_GLASS_PARTS_STATE);
|
||||
context.commit(storeMutations.UPDATE_SUPPORTING_ITEMS, null);
|
||||
|
||||
context.dispatch(storeActions.RESET_SERVICE_LOCATION_STATE_AND_DEPENDENCIES);
|
||||
},
|
||||
|
||||
resetServiceLocationAndDependencies(context) {
|
||||
context.commit(storeMutations.RESET_SERVICE_LOCATION_APPOINTMENT_TYPE);
|
||||
context.commit(storeMutations.RESET_SERVICE_LOCATION_PROVIDER);
|
||||
|
||||
context.commit(storeMutations.RESET_SCHEDULE);
|
||||
},
|
||||
|
||||
resetState(context) {
|
||||
context.commit(storeMutations.RESET_STATE);
|
||||
},
|
||||
|
||||
resetSaveSessionPromise(context) {
|
||||
context.commit(storeMutations.RESET_SAVE_SESSION_PROMISE);
|
||||
},
|
||||
|
|
@ -699,12 +805,14 @@ export const actions = {
|
|||
},
|
||||
});
|
||||
},
|
||||
|
||||
getHomepageName(context) {
|
||||
return globalMethods.callHttpClient({
|
||||
method: endpoints.GetHomepageInfo.method,
|
||||
endpoint: endpoints.GetHomepageInfo.url(applicationConfig.APPLICATION_ABBREVIATION),
|
||||
});
|
||||
},
|
||||
|
||||
getPageData(context, { pageName }) {
|
||||
return globalMethods.callHttpClient({
|
||||
method: endpoints.GetPageData.method,
|
||||
|
|
@ -771,6 +879,7 @@ export const actions = {
|
|||
context.commit(storeMutations.UPDATE_SAVED_SESSION_ID, savedSessionId);
|
||||
context.commit(storeMutations.UPDATE_CRM_CUSTOMER_ID, crmCustomerId);
|
||||
},
|
||||
|
||||
logPageView(
|
||||
context,
|
||||
{
|
||||
|
|
@ -812,6 +921,7 @@ export const actions = {
|
|||
}
|
||||
);
|
||||
},
|
||||
|
||||
logCustomEvent(
|
||||
context,
|
||||
{
|
||||
|
|
@ -1116,52 +1226,121 @@ export const actions = {
|
|||
});
|
||||
},
|
||||
|
||||
getShopTimeSlots(
|
||||
context,
|
||||
{ startDate = "2023-01-01", endDate = "2023-05-01", shopAppointmentType = "" }
|
||||
) {
|
||||
getShopTimeSlots(context, { startDate, endDate, shopAppointmentType, providerNumber }) {
|
||||
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 = {
|
||||
providerNumber: order.providerNumber,
|
||||
providerNumber: providerNumber,
|
||||
startDate: startDate,
|
||||
endDate: endDate,
|
||||
shopAppointmentType: shopAppointmentType,
|
||||
applicationName: applicationConfig.APPLICATION_NAME,
|
||||
parentAccountNumber: context.getters.payment.parentAccountNumber,
|
||||
carId: context.getters.vehicle.carId,
|
||||
partNumbers: mockArray, // TODO: WHERE DO I GET THIS?
|
||||
carId: vehicle.carId,
|
||||
partNumbers: partNumbers,
|
||||
glassPieces: glassPieces,
|
||||
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.callMockHttpClient({
|
||||
return globalMethods.callHttpClient({
|
||||
method: endpoints.GetShopTimeSlots.method,
|
||||
endpoint: endpoints.GetShopTimeSlots.mockUrl,
|
||||
endpoint: endpoints.GetShopTimeSlots.url,
|
||||
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 paymentType = context.getters.order.payment.isInsurance ? "Insurance" : "Cash";
|
||||
return globalMethods.callMockHttpClient({
|
||||
method: endpoints.GetMobileEarlyBirdFee.method,
|
||||
endpoint: `${endpoints.GetMobileEarlyBirdFee.mockUrl}/Cash/Replace`,
|
||||
|
||||
return globalMethods.callHttpClient({
|
||||
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
|
||||
saveSession(context) {
|
||||
const vehicle = context.getters.vehicle;
|
||||
|
|
@ -1203,7 +1382,7 @@ export const actions = {
|
|||
},
|
||||
},
|
||||
customer: {
|
||||
emailAddress: order.customer.emailAddress,
|
||||
emailAddress: order.customer.emailAddress || null,
|
||||
},
|
||||
damage: {
|
||||
numberOfChips: damage.numberOfChips,
|
||||
|
|
@ -1242,7 +1421,8 @@ export const actions = {
|
|||
order.serviceLocation.provider?.address?.streetAddress,
|
||||
city: order.serviceLocation.provider?.address?.city,
|
||||
state: order.serviceLocation.provider?.address?.state,
|
||||
zip: order.serviceLocation.provider?.address?.zip,
|
||||
zipCode: order.serviceLocation.provider?.address?.zipCode,
|
||||
zipCodeCtu: order.serviceLocation.provider?.address?.zipCodeCtu,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -1251,6 +1431,7 @@ export const actions = {
|
|||
startTime: order.schedule?.startTime,
|
||||
endTime: order.schedule?.endTime,
|
||||
routeCode: order.schedule?.routeCode,
|
||||
jobMaxMinutes: order.schedule?.jobMaxMinutes,
|
||||
},
|
||||
existingPromoCode: null,
|
||||
referralCorrelationId: order.referralCorrelationId,
|
||||
|
|
@ -1260,8 +1441,11 @@ export const actions = {
|
|||
eon: order.eon,
|
||||
},
|
||||
},
|
||||
additionalSuccessEventDataHandler: (response) =>
|
||||
"Email provided: " + (order.customer.emailAddress ? "true" : "false"),
|
||||
});
|
||||
},
|
||||
|
||||
loadSession(
|
||||
context,
|
||||
{
|
||||
|
|
@ -1288,7 +1472,7 @@ export const actions = {
|
|||
},
|
||||
})
|
||||
.then(
|
||||
(response) => {
|
||||
async (response) => {
|
||||
// Flatten location and name properties
|
||||
response.data.order.damage?.glassToReplace?.map((glass) => {
|
||||
glass.glassLocation = glass.location;
|
||||
|
|
@ -1302,10 +1486,13 @@ export const actions = {
|
|||
if (context.state.order.eon && context.state.order.eon != response.data.eon) {
|
||||
context.commit(storeMutations.RESET_STATE);
|
||||
}
|
||||
|
||||
context.commit(
|
||||
storeMutations.UPDATE_STATE_WITH_ORDER_INFORMATION,
|
||||
response.data
|
||||
);
|
||||
|
||||
await resetScheduleIfUnavailable(context, response.data.order);
|
||||
return response;
|
||||
},
|
||||
(error) => {
|
||||
|
|
@ -1339,6 +1526,7 @@ export const actions = {
|
|||
context.commit(storeMutations.UPDATE_YEAR, year);
|
||||
}
|
||||
},
|
||||
|
||||
saveVehicleMake(context, make) {
|
||||
//Reset dependent state when changing
|
||||
if (context.state.order.vehicle.make !== make) {
|
||||
|
|
@ -1359,6 +1547,7 @@ export const actions = {
|
|||
context.commit(storeMutations.UPDATE_MAKE, make);
|
||||
}
|
||||
},
|
||||
|
||||
saveVehicleModel(context, model) {
|
||||
//Reset dependent state when changing
|
||||
if (context.state.order.vehicle.model !== model) {
|
||||
|
|
@ -1378,6 +1567,7 @@ export const actions = {
|
|||
context.commit(storeMutations.UPDATE_MODEL, model);
|
||||
}
|
||||
},
|
||||
|
||||
saveVehicleStyle(context, style) {
|
||||
//Reset dependent state when changing
|
||||
if (context.state.order.vehicle.style !== style) {
|
||||
|
|
@ -1396,6 +1586,7 @@ export const actions = {
|
|||
context.commit(storeMutations.UPDATE_STYLE, style);
|
||||
}
|
||||
},
|
||||
|
||||
saveVehicleDamage(
|
||||
context,
|
||||
{ isWindshieldRepair, selectedGlassToReplace, selectedWindshieldChipCount }
|
||||
|
|
@ -1453,6 +1644,7 @@ export const actions = {
|
|||
context.commit(storeMutations.UPDATE_REGISTRATION, registrationInfo);
|
||||
}
|
||||
},
|
||||
|
||||
saveRegistrationLicensePlateLookup(
|
||||
context,
|
||||
{ isSelectedGlassAvailableForVehicle, vehicleInfo, registrationInfo }
|
||||
|
|
@ -1466,7 +1658,6 @@ export const actions = {
|
|||
|
||||
if (!isSelectedGlassAvailableForVehicle) {
|
||||
context.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES);
|
||||
context.dispatch(storeActions.RESET_PARTS_STATE_AND_DEPENDENCIES);
|
||||
}
|
||||
|
||||
//Save new values
|
||||
|
|
@ -1474,6 +1665,7 @@ export const actions = {
|
|||
context.commit(storeMutations.UPDATE_REGISTRATION, registrationInfo);
|
||||
}
|
||||
},
|
||||
|
||||
saveRegistrationAddressLookup(
|
||||
context,
|
||||
{ isSelectedGlassAvailableForVehicle, vehicleInfo, registrationInfo }
|
||||
|
|
@ -1491,7 +1683,6 @@ export const actions = {
|
|||
|
||||
if (!isSelectedGlassAvailableForVehicle) {
|
||||
context.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES);
|
||||
context.dispatch(storeActions.RESET_PARTS_STATE_AND_DEPENDENCIES);
|
||||
}
|
||||
|
||||
//Save new values
|
||||
|
|
@ -1499,6 +1690,7 @@ export const actions = {
|
|||
context.commit(storeMutations.UPDATE_REGISTRATION, registrationInfo);
|
||||
}
|
||||
},
|
||||
|
||||
savePartQuestionAnswers(context, partQuestionAnswersArray) {
|
||||
// if part question answers have changed, reset subsequent question answers
|
||||
const sortedPreviousResultsArray = sortArrayOfObjectsByPropertyValue(
|
||||
|
|
@ -1516,6 +1708,8 @@ export const actions = {
|
|||
);
|
||||
|
||||
if (havePartQuestionAnswersChanged) {
|
||||
context.dispatch(storeActions.RESET_SERVICE_LOCATION_STATE_AND_DEPENDENCIES);
|
||||
|
||||
context.commit(storeMutations.UPDATE_GLASS_PARTS, null);
|
||||
context.commit(storeMutations.UPDATE_SUPPORTING_ITEMS, null);
|
||||
context.commit(storeMutations.UPDATE_MOLDING_QUESTION_ANSWERS, null);
|
||||
|
|
@ -1537,6 +1731,7 @@ export const actions = {
|
|||
//Save new values
|
||||
context.commit(storeMutations.UPDATE_PART_QUESTION_ANSWERS, partQuestionAnswersArray);
|
||||
},
|
||||
|
||||
resetMoldingAndCapabilityQuestionAnswersIfNeeded(context, matchedParts) {
|
||||
const partsOrQuestionsDataToCompareWith =
|
||||
context.getters.pageData(fmgPageValues.MOLDING_QUESTIONS)?.partsOrQuestions ??
|
||||
|
|
@ -1562,6 +1757,8 @@ export const actions = {
|
|||
previouslySelectedPartNumbers !== currentlySelectedPartNumbers;
|
||||
|
||||
if (haveSelectedVehiclePartsChanged) {
|
||||
context.dispatch(storeActions.RESET_SERVICE_LOCATION_STATE_AND_DEPENDENCIES);
|
||||
|
||||
context.commit(storeMutations.UPDATE_GLASS_PARTS, null);
|
||||
context.commit(storeMutations.UPDATE_SUPPORTING_ITEMS, null);
|
||||
context.commit(storeMutations.UPDATE_MOLDING_QUESTION_ANSWERS, null);
|
||||
|
|
@ -1576,6 +1773,7 @@ export const actions = {
|
|||
});
|
||||
}
|
||||
},
|
||||
|
||||
saveMoldingQuestionAnswers(context, moldingQuestionAnswers) {
|
||||
const sortedPreviousResultsArray = sortArrayOfObjectsByPropertyValue(
|
||||
context.getters.damage.moldingQuestionAnswers,
|
||||
|
|
@ -1592,6 +1790,8 @@ export const actions = {
|
|||
);
|
||||
|
||||
if (haveMoldingQuestionAnswersChanged) {
|
||||
context.dispatch(storeActions.RESET_SERVICE_LOCATION_STATE_AND_DEPENDENCIES);
|
||||
|
||||
context.commit(storeMutations.UPDATE_GLASS_PARTS, null);
|
||||
context.commit(storeMutations.UPDATE_SUPPORTING_ITEMS, null);
|
||||
context.commit(storeMutations.UPDATE_CAPABILITY_QUESTION_ANSWERS, null);
|
||||
|
|
@ -1604,6 +1804,7 @@ export const actions = {
|
|||
//Save new values
|
||||
context.commit(storeMutations.UPDATE_MOLDING_QUESTION_ANSWERS, moldingQuestionAnswers);
|
||||
},
|
||||
|
||||
saveCapabilityQuestionAnswers(context, capabilityQuestionAnswers) {
|
||||
const sortedPreviousResultsArray = sortArrayOfObjectsByPropertyValue(
|
||||
context.getters.damage.capabilityQuestionAnswers,
|
||||
|
|
@ -1620,6 +1821,8 @@ export const actions = {
|
|||
);
|
||||
|
||||
if (haveCapabilityQuestionAnswersChanged) {
|
||||
context.dispatch(storeActions.RESET_SERVICE_LOCATION_STATE_AND_DEPENDENCIES);
|
||||
|
||||
context.commit(storeMutations.UPDATE_GLASS_PARTS, null);
|
||||
context.commit(storeMutations.UPDATE_SUPPORTING_ITEMS, null);
|
||||
}
|
||||
|
|
@ -1630,18 +1833,31 @@ export const actions = {
|
|||
capabilityQuestionAnswers
|
||||
);
|
||||
},
|
||||
|
||||
savePaymentType(context, isInsurance) {
|
||||
context.commit(storeMutations.UPDATE_IS_INSURANCE, isInsurance);
|
||||
},
|
||||
|
||||
saveParentAccountNumber(context, parentAccountNumber) {
|
||||
context.commit(storeMutations.UPDATE_PARENT_ACCT_NUMBER, parentAccountNumber);
|
||||
},
|
||||
|
||||
saveSupportingItems(context, supportingItems) {
|
||||
if (!deepEqual(supportingItems, context.state.order.lineItems.supportingItems)) {
|
||||
context.dispatch(storeActions.RESET_SERVICE_LOCATION_STATE_AND_DEPENDENCIES);
|
||||
}
|
||||
|
||||
context.commit(storeMutations.UPDATE_SUPPORTING_ITEMS, supportingItems);
|
||||
},
|
||||
|
||||
saveSupportingItemsSuppressingStateResetting(context, supportingItems) {
|
||||
context.commit(storeMutations.UPDATE_SUPPORTING_ITEMS, supportingItems);
|
||||
},
|
||||
|
||||
saveVaps(context, vaps) {
|
||||
context.commit(storeMutations.UPDATE_VAPS, vaps);
|
||||
},
|
||||
|
||||
// Price order actions
|
||||
async priceOrderItemsAndSaveServerData(
|
||||
context,
|
||||
|
|
@ -1678,7 +1894,6 @@ export const actions = {
|
|||
`&${availableLineItemsFormattedForRequest}`;
|
||||
|
||||
const lineItemServerData = context.getters.order.lineItems.serverData;
|
||||
|
||||
if (lineItemServerData) {
|
||||
queryString += `&ServerData=${encodeURIComponent(lineItemServerData)}`;
|
||||
}
|
||||
|
|
@ -1694,35 +1909,93 @@ export const actions = {
|
|||
|
||||
return availableLineItems;
|
||||
},
|
||||
|
||||
// Misc order actions
|
||||
saveSchedule(context, scheduleInfo) {
|
||||
context.commit(storeMutations.UPDATE_SCHEDULE, scheduleInfo);
|
||||
},
|
||||
|
||||
saveDetails(context, scheduleInfo) {
|
||||
context.commit(storeMutations.UPDATE_DETAILS, scheduleInfo);
|
||||
},
|
||||
|
||||
saveServiceZipCodeInfo(context, serviceZipCodeInfo) {
|
||||
if (
|
||||
context.state.order.serviceLocation &&
|
||||
serviceZipCodeInfo.zipCode !== context.state.order.serviceLocation.zipCode
|
||||
) {
|
||||
context.dispatch(storeActions.RESET_SERVICE_LOCATION_STATE_AND_DEPENDENCIES);
|
||||
|
||||
context.commit(storeMutations.RESET_SERVICE_LOCATION_MOBILE_ADDRESS);
|
||||
}
|
||||
|
||||
context.commit(storeMutations.UPDATE_SERVICE_ZIP, serviceZipCodeInfo);
|
||||
},
|
||||
|
||||
saveServiceLocation(context, serviceLocationInfo) {
|
||||
if (context.state.order.serviceLocation) {
|
||||
if (
|
||||
serviceLocationInfo.zipCode !== context.state.order.serviceLocation.zipCode ||
|
||||
!providersEqual(
|
||||
serviceLocationInfo.provider,
|
||||
context.state.order.serviceLocation.provider
|
||||
) ||
|
||||
serviceLocationInfo.appointmentType !==
|
||||
context.state.order.serviceLocation.appointmentType
|
||||
) {
|
||||
context.commit(storeMutations.RESET_SCHEDULE);
|
||||
}
|
||||
}
|
||||
|
||||
context.commit(storeMutations.UPDATE_SERVICE_LOCATION, serviceLocationInfo);
|
||||
},
|
||||
|
||||
saveEmail(context, email) {
|
||||
context.commit(storeMutations.UPDATE_CUSTOMER_EMAIL_ADDRESS, email);
|
||||
},
|
||||
|
||||
saveVin(context, { isSelectedGlassAvailableForVehicle, vehicleInfo }) {
|
||||
//Reset dependent state when changing
|
||||
if (vehicleInfo.vin !== context.state.order.vehicle.vin) {
|
||||
if (!isSelectedGlassAvailableForVehicle) {
|
||||
context.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES);
|
||||
context.dispatch(storeActions.RESET_PARTS_STATE_AND_DEPENDENCIES);
|
||||
}
|
||||
|
||||
//Save new values
|
||||
context.commit(storeMutations.UPDATE_VEHICLE, vehicleInfo);
|
||||
}
|
||||
},
|
||||
|
||||
saveGlassParts(context, parts) {
|
||||
if (!deepEqual(parts, context.state.order.lineItems.glassParts)) {
|
||||
context.dispatch(storeActions.RESET_SERVICE_LOCATION_STATE_AND_DEPENDENCIES);
|
||||
}
|
||||
|
||||
context.commit(storeMutations.UPDATE_GLASS_PARTS, parts);
|
||||
},
|
||||
|
||||
saveGlassPartPrices(context, parts) {
|
||||
context.commit(storeMutations.UPDATE_GLASS_PARTS, parts);
|
||||
},
|
||||
|
||||
clearVin(context) {
|
||||
context.commit(storeMutations.UPDATE_VEHICLE_VIN, null);
|
||||
},
|
||||
|
||||
isVinOptionalVehicle(context) {
|
||||
//Optional for carIds with only a single windshield
|
||||
if (
|
||||
singleWindshieldCarIds.find((item) => item === context.state.order.vehicle.carId) &&
|
||||
context.state.order.damage.glassToReplace.length == 1 &&
|
||||
context.state.order.damage.glassToReplace.find(
|
||||
(glassToReplace) =>
|
||||
glassToReplace.glassLocation.toLowerCase() ===
|
||||
damageLocationsSelected.WINDSHIELD.toLowerCase()
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
//Optional for specific YMMSs
|
||||
switch (context.state.order.vehicle.make.toLowerCase()) {
|
||||
case "mercedes benz":
|
||||
case "volkswagen":
|
||||
|
|
@ -1731,14 +2004,12 @@ export const actions = {
|
|||
return true;
|
||||
default:
|
||||
}
|
||||
|
||||
if (
|
||||
context.state.order.vehicle.make.toLowerCase() === "ford" &&
|
||||
context.state.order.vehicle.year >= 2018
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
context.state.order.vehicle.make.toLowerCase() === "bmw" &&
|
||||
context.state.order.vehicle.year <= 2017
|
||||
|
|
@ -1863,7 +2134,7 @@ function addPricesToLineItems(lineItems, pricingLineItems) {
|
|||
|
||||
function getFlattenedArrayOfLineItemsWithChildParts(lineItems) {
|
||||
let flattenedArray = [];
|
||||
lineItems.forEach((lineItem) => {
|
||||
lineItems?.forEach((lineItem) => {
|
||||
flattenedArray.push(lineItem);
|
||||
if (lineItem.childParts) {
|
||||
flattenedArray = [
|
||||
|
|
@ -1885,3 +2156,137 @@ function buildQueryStringParameterFromArrayOfComplexObjects(arrayOfObjects, para
|
|||
// Remove trailing &
|
||||
return queryStringParameter.slice(0, -1);
|
||||
}
|
||||
|
||||
function convertGlassPieceToBackEndCompatibleFormat(glassPieces) {
|
||||
return glassPieces.map((glassPiece) => {
|
||||
return {
|
||||
location: glassPiece.glassLocation,
|
||||
name: glassPiece.glassName,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function providersEqual(providerA, providerB) {
|
||||
return (
|
||||
providerA.providerNumber === providerB.providerNumber &&
|
||||
providerA.address?.city === providerB.address?.city &&
|
||||
providerA.address?.state === providerB.address?.state &&
|
||||
providerA.address?.streetAddress === providerB.address?.streetAddress &&
|
||||
providerA.address?.zipCode === providerB.address?.zipCode
|
||||
);
|
||||
//TODO: Change back to deepEqual once zipCodeCtu is added to saveSession.
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -406,14 +406,16 @@ describe("Actions", () => {
|
|||
it("lookupVinByImage action, should return list of vins", async () => {
|
||||
// Arrange
|
||||
const context = state;
|
||||
const dummyImage = {};
|
||||
const image = new File([], "test.jpg", {
|
||||
type: "image/jpeg",
|
||||
});
|
||||
|
||||
globalMethods.callHttpClient.mockImplementation(() => {
|
||||
return Promise.resolve({ data: ["1C6JJTAG3NL134044"] });
|
||||
});
|
||||
|
||||
// Act
|
||||
const response = await actions.lookupVinByImage(context, dummyImage);
|
||||
const response = await actions.lookupVinByImage(context, image);
|
||||
|
||||
// Assert
|
||||
expect(response.data).toEqual(["1C6JJTAG3NL134044"]);
|
||||
|
|
@ -422,7 +424,9 @@ describe("Actions", () => {
|
|||
it("lookupVinByImage action, should reject if error in calling API", async () => {
|
||||
// Arrange
|
||||
const context = state;
|
||||
const dummyImage = {};
|
||||
const image = new File([], "test.jpg", {
|
||||
type: "image/jpeg",
|
||||
});
|
||||
|
||||
globalMethods.callHttpClient.mockImplementation(() => {
|
||||
return Promise.reject("An error occurred");
|
||||
|
|
@ -431,9 +435,7 @@ describe("Actions", () => {
|
|||
// Act
|
||||
|
||||
// Assert
|
||||
await expect(actions.lookupVinByImage(context, dummyImage)).rejects.toEqual(
|
||||
"An error occurred"
|
||||
);
|
||||
await expect(actions.lookupVinByImage(context, image)).rejects.toEqual("An error occurred");
|
||||
});
|
||||
|
||||
it("getVehicleMakes action, should return makes list", async () => {
|
||||
|
|
@ -547,41 +549,48 @@ describe("Actions", () => {
|
|||
// Arrange
|
||||
const context = state;
|
||||
const commit = jest.fn();
|
||||
const dispatch = jest.fn();
|
||||
|
||||
context.commit = commit;
|
||||
context.dispatch = dispatch;
|
||||
|
||||
// Act
|
||||
await actions.resetDamageAndDependencies(context);
|
||||
|
||||
expect(commit).toBeCalledWith(storeMutations.RESET_DAMAGE_STATE);
|
||||
expect(commit).toBeCalledWith(storeMutations.RESET_GLASS_PARTS_STATE);
|
||||
expect(dispatch).toBeCalledWith(storeActions.RESET_PARTS_STATE_AND_DEPENDENCIES);
|
||||
});
|
||||
|
||||
it("resetRegistrationAndDependencies action", async () => {
|
||||
// Arrange
|
||||
const context = state;
|
||||
const commit = jest.fn();
|
||||
const dispatch = jest.fn();
|
||||
|
||||
context.commit = commit;
|
||||
context.dispatch = dispatch;
|
||||
|
||||
// Act
|
||||
await actions.resetRegistrationAndDependencies(context);
|
||||
|
||||
expect(commit).toBeCalledWith(storeMutations.RESET_REGISTRATION_STATE);
|
||||
expect(commit).toBeCalledWith(storeMutations.RESET_GLASS_PARTS_STATE);
|
||||
expect(dispatch).toBeCalledWith(storeActions.RESET_PARTS_STATE_AND_DEPENDENCIES);
|
||||
});
|
||||
|
||||
it("resetPartsAndDependencies action", async () => {
|
||||
// Arrange
|
||||
const context = state;
|
||||
const commit = jest.fn();
|
||||
const dispatch = jest.fn();
|
||||
|
||||
context.commit = commit;
|
||||
context.dispatch = dispatch;
|
||||
|
||||
// Act
|
||||
await actions.resetPartsAndDependencies(context);
|
||||
|
||||
expect(commit).toBeCalledWith(storeMutations.RESET_GLASS_PARTS_STATE);
|
||||
expect(dispatch).toBeCalledWith(storeActions.RESET_SERVICE_LOCATION_STATE_AND_DEPENDENCIES);
|
||||
});
|
||||
|
||||
it("resetState action", async () => {
|
||||
|
|
@ -933,6 +942,90 @@ describe("Actions", () => {
|
|||
);
|
||||
});
|
||||
|
||||
it("saveServiceZipCodeInfo, should call mutation and save zip code to state", () => {
|
||||
// Arrange
|
||||
const context = state;
|
||||
const commit = jest.fn();
|
||||
context.commit = commit;
|
||||
|
||||
const serviceZipCodeInfo = {
|
||||
zipCode: "43212",
|
||||
};
|
||||
|
||||
// Act
|
||||
actions.saveServiceLocation(context, serviceZipCodeInfo);
|
||||
|
||||
// Assert
|
||||
expect(commit).toBeCalledWith(storeMutations.UPDATE_SERVICE_LOCATION, serviceZipCodeInfo);
|
||||
expect(state.order.serviceLocation.zipCode).toEqual("43212");
|
||||
});
|
||||
|
||||
it("saveServiceZipCodeInfo, should reset if zip code is different", () => {
|
||||
// Arrange
|
||||
const context = {
|
||||
state: {
|
||||
order: {
|
||||
serviceLocation: {
|
||||
zipCode: "43212",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const commit = jest.fn();
|
||||
const dispatch = jest.fn();
|
||||
|
||||
context.commit = commit;
|
||||
context.dispatch = dispatch;
|
||||
|
||||
const serviceZipCodeInfo = {
|
||||
zipCode: "43202",
|
||||
};
|
||||
|
||||
// Act
|
||||
actions.saveServiceZipCodeInfo(context, serviceZipCodeInfo);
|
||||
|
||||
// Assert
|
||||
expect(dispatch).toHaveBeenCalledWith(
|
||||
storeActions.RESET_SERVICE_LOCATION_STATE_AND_DEPENDENCIES
|
||||
);
|
||||
expect(commit).toHaveBeenCalledWith(storeMutations.RESET_SERVICE_LOCATION_MOBILE_ADDRESS);
|
||||
});
|
||||
|
||||
it("saveServiceZipCodeInfo, should not reset if zip code is the same", () => {
|
||||
// Arrange
|
||||
const context = {
|
||||
state: {
|
||||
order: {
|
||||
serviceLocation: {
|
||||
zipCode: "43212",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const commit = jest.fn();
|
||||
const dispatch = jest.fn();
|
||||
|
||||
context.commit = commit;
|
||||
context.dispatch = dispatch;
|
||||
|
||||
const serviceZipCodeInfo = {
|
||||
zipCode: "43212",
|
||||
};
|
||||
|
||||
// Act
|
||||
actions.saveServiceZipCodeInfo(context, serviceZipCodeInfo);
|
||||
|
||||
// Assert
|
||||
expect(dispatch).not.toHaveBeenCalledWith(
|
||||
storeActions.RESET_SERVICE_LOCATION_STATE_AND_DEPENDENCIES
|
||||
);
|
||||
expect(commit).not.toHaveBeenCalledWith(
|
||||
storeMutations.RESET_SERVICE_LOCATION_MOBILE_ADDRESS
|
||||
);
|
||||
});
|
||||
|
||||
it("saveServiceLocation, should call mutation and save service address to state", () => {
|
||||
// Arrange
|
||||
const context = state;
|
||||
|
|
@ -959,18 +1052,339 @@ describe("Actions", () => {
|
|||
expect(state.order.serviceLocation.zipCodeCtu).toEqual("01820");
|
||||
});
|
||||
|
||||
it("saveGlassParts, should call mutation", () => {
|
||||
it("saveServiceLocation, should reset if zipcode is different", () => {
|
||||
// Arrange
|
||||
const context = state;
|
||||
const context = {
|
||||
state: {
|
||||
order: {
|
||||
serviceLocation: {
|
||||
address: "123 Test Lane",
|
||||
city: "Columbus",
|
||||
zipCode: "43212",
|
||||
state: "OH",
|
||||
zipCodeCtu: "01820",
|
||||
appointmentType: "Mobile",
|
||||
isVehicleProtected: true,
|
||||
provider: {
|
||||
providerNumber: "11111",
|
||||
address: {
|
||||
streetAddress: "123 Test Lane",
|
||||
city: "Columbus",
|
||||
state: "OH",
|
||||
zipCode: "43212",
|
||||
zipCodeCtu: "01820",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const commit = jest.fn();
|
||||
const dispatch = jest.fn();
|
||||
|
||||
context.commit = commit;
|
||||
context.dispatch = dispatch;
|
||||
|
||||
const serviceLocation = {
|
||||
address: "123 Test Lane",
|
||||
city: "Columbus",
|
||||
zipCode: "43202",
|
||||
state: "OH",
|
||||
zipCodeCtu: "01820",
|
||||
appointmentType: "Mobile",
|
||||
isVehicleProtected: true,
|
||||
provider: {
|
||||
providerNumber: "11111",
|
||||
address: {
|
||||
streetAddress: "123 Test Lane",
|
||||
city: "Columbus",
|
||||
state: "OH",
|
||||
zipCode: "43212",
|
||||
zipCodeCtu: "01820",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Act
|
||||
actions.saveServiceLocation(context, serviceLocation);
|
||||
|
||||
// Assert
|
||||
expect(commit).toHaveBeenCalledWith(storeMutations.RESET_SCHEDULE);
|
||||
});
|
||||
|
||||
it("saveServiceLocation, should reset if provider is different", () => {
|
||||
// Arrange
|
||||
const context = {
|
||||
state: {
|
||||
order: {
|
||||
serviceLocation: {
|
||||
address: "123 Test Lane",
|
||||
city: "Columbus",
|
||||
zipCode: "43212",
|
||||
state: "OH",
|
||||
zipCodeCtu: "01820",
|
||||
appointmentType: "Mobile",
|
||||
isVehicleProtected: true,
|
||||
provider: {
|
||||
providerNumber: "11111",
|
||||
address: {
|
||||
streetAddress: "123 Test Lane",
|
||||
city: "Columbus",
|
||||
state: "OH",
|
||||
zipCode: "43212",
|
||||
zipCodeCtu: "01820",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const commit = jest.fn();
|
||||
const dispatch = jest.fn();
|
||||
|
||||
context.commit = commit;
|
||||
context.dispatch = dispatch;
|
||||
|
||||
const serviceLocation = {
|
||||
address: "123 Test Lane",
|
||||
city: "Columbus",
|
||||
zipCode: "43212",
|
||||
state: "OH",
|
||||
zipCodeCtu: "01820",
|
||||
appointmentType: "Mobile",
|
||||
isVehicleProtected: true,
|
||||
provider: {
|
||||
providerNumber: "22222",
|
||||
address: {
|
||||
streetAddress: "321 Test Lane",
|
||||
city: "Columbus",
|
||||
state: "OH",
|
||||
zipCode: "43212",
|
||||
zipCodeCtu: "01820",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Act
|
||||
actions.saveServiceLocation(context, serviceLocation);
|
||||
|
||||
// Assert
|
||||
expect(commit).toHaveBeenCalledWith(storeMutations.RESET_SCHEDULE);
|
||||
});
|
||||
|
||||
it("saveServiceLocation, should reset if appointment type is different", () => {
|
||||
// Arrange
|
||||
const context = {
|
||||
state: {
|
||||
order: {
|
||||
serviceLocation: {
|
||||
address: "123 Test Lane",
|
||||
city: "Columbus",
|
||||
zipCode: "43212",
|
||||
state: "OH",
|
||||
zipCodeCtu: "01820",
|
||||
appointmentType: "Mobile",
|
||||
isVehicleProtected: true,
|
||||
provider: {
|
||||
providerNumber: "11111",
|
||||
address: {
|
||||
streetAddress: "123 Test Lane",
|
||||
city: "Columbus",
|
||||
state: "OH",
|
||||
zipCode: "43212",
|
||||
zipCodeCtu: "01820",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const commit = jest.fn();
|
||||
const dispatch = jest.fn();
|
||||
|
||||
context.commit = commit;
|
||||
context.dispatch = dispatch;
|
||||
|
||||
const serviceLocation = {
|
||||
address: "123 Test Lane",
|
||||
city: "Columbus",
|
||||
zipCode: "43212",
|
||||
state: "OH",
|
||||
zipCodeCtu: "01820",
|
||||
appointmentType: "Inshop",
|
||||
isVehicleProtected: true,
|
||||
provider: {
|
||||
providerNumber: "11111",
|
||||
address: {
|
||||
streetAddress: "123 Test Lane",
|
||||
city: "Columbus",
|
||||
state: "OH",
|
||||
zipCode: "43212",
|
||||
zipCodeCtu: "01820",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Act
|
||||
actions.saveServiceLocation(context, serviceLocation);
|
||||
|
||||
// Assert
|
||||
expect(commit).toHaveBeenCalledWith(storeMutations.RESET_SCHEDULE);
|
||||
});
|
||||
|
||||
it("saveServiceLocation, should not reset if parameters are the same", () => {
|
||||
// Arrange
|
||||
const context = {
|
||||
state: {
|
||||
order: {
|
||||
serviceLocation: {
|
||||
address: "123 Test Lane",
|
||||
city: "Columbus",
|
||||
zipCode: "43212",
|
||||
state: "OH",
|
||||
zipCodeCtu: "01820",
|
||||
appointmentType: "Mobile",
|
||||
isVehicleProtected: true,
|
||||
provider: {
|
||||
providerNumber: "11111",
|
||||
address: {
|
||||
streetAddress: "123 Test Lane",
|
||||
city: "Columbus",
|
||||
state: "OH",
|
||||
zipCode: "43212",
|
||||
zipCodeCtu: "01820",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const commit = jest.fn();
|
||||
const dispatch = jest.fn();
|
||||
|
||||
context.commit = commit;
|
||||
context.dispatch = dispatch;
|
||||
|
||||
const serviceLocation = {
|
||||
address: "123 Test Lane",
|
||||
city: "Columbus",
|
||||
zipCode: "43212",
|
||||
state: "OH",
|
||||
zipCodeCtu: "01820",
|
||||
appointmentType: "Mobile",
|
||||
isVehicleProtected: true,
|
||||
provider: {
|
||||
providerNumber: "11111",
|
||||
address: {
|
||||
streetAddress: "123 Test Lane",
|
||||
city: "Columbus",
|
||||
state: "OH",
|
||||
zipCode: "43212",
|
||||
zipCodeCtu: "01820",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Act
|
||||
actions.saveServiceLocation(context, serviceLocation);
|
||||
|
||||
// Assert
|
||||
expect(commit).not.toHaveBeenCalledWith(storeMutations.RESET_SCHEDULE);
|
||||
});
|
||||
|
||||
it("saveGlassParts, should call mutation", () => {
|
||||
// Arrange
|
||||
const context = {
|
||||
state: state,
|
||||
};
|
||||
|
||||
const commit = jest.fn();
|
||||
const dispatch = jest.fn();
|
||||
|
||||
context.commit = commit;
|
||||
context.dispatch = dispatch;
|
||||
|
||||
// Act
|
||||
actions.saveGlassParts(context, { glassParts: {} });
|
||||
|
||||
// Assert
|
||||
expect(commit).toBeCalledWith(storeMutations.UPDATE_GLASS_PARTS, { glassParts: {} });
|
||||
expect(dispatch).toBeCalledWith(storeActions.RESET_SERVICE_LOCATION_STATE_AND_DEPENDENCIES);
|
||||
});
|
||||
|
||||
it("saveSupportingItems, should call mutations", () => {
|
||||
// Arrange
|
||||
const context = {
|
||||
state: state,
|
||||
};
|
||||
|
||||
const commit = jest.fn();
|
||||
const dispatch = jest.fn();
|
||||
|
||||
context.commit = commit;
|
||||
context.dispatch = dispatch;
|
||||
|
||||
// Act
|
||||
actions.saveSupportingItems(context, []);
|
||||
|
||||
// Assert
|
||||
expect(commit).toBeCalledWith(storeMutations.UPDATE_SUPPORTING_ITEMS, []);
|
||||
});
|
||||
|
||||
it("saveSupportingItems, should call reset logic when value is new", () => {
|
||||
// Arrange
|
||||
const context = {
|
||||
state: {
|
||||
order: {
|
||||
lineItems: {
|
||||
supportingItems: ["TestValue1", "TestValue2"],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const commit = jest.fn();
|
||||
const dispatch = jest.fn();
|
||||
|
||||
context.commit = commit;
|
||||
context.dispatch = dispatch;
|
||||
|
||||
// Act
|
||||
actions.saveSupportingItems(context, ["TestValue3", "TestValue4", "TestValue5"]);
|
||||
|
||||
// Assert
|
||||
expect(dispatch).toBeCalledWith(storeActions.RESET_SERVICE_LOCATION_STATE_AND_DEPENDENCIES);
|
||||
});
|
||||
|
||||
it("saveSupportingItems, should not call reset logic when value is the same", () => {
|
||||
// Arrange
|
||||
const context = {
|
||||
state: {
|
||||
order: {
|
||||
lineItems: {
|
||||
supportingItems: ["TestValue1", "TestValue2"],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const commit = jest.fn();
|
||||
const dispatch = jest.fn();
|
||||
|
||||
context.commit = commit;
|
||||
context.dispatch = dispatch;
|
||||
|
||||
// Act
|
||||
actions.saveSupportingItems(context, ["TestValue1", "TestValue2"]);
|
||||
|
||||
// Assert
|
||||
expect(dispatch).not.toBeCalledWith(
|
||||
storeActions.RESET_SERVICE_LOCATION_STATE_AND_DEPENDENCIES
|
||||
);
|
||||
});
|
||||
|
||||
it("clearVin, should call mutation", () => {
|
||||
|
|
@ -2805,6 +3219,36 @@ describe("isVinOptionalVehicle", () => {
|
|||
},
|
||||
};
|
||||
|
||||
var vinOptionalResult = actions.isVinOptionalVehicle(context);
|
||||
expect(vinOptionalResult).toEqual(expectedVinSkip);
|
||||
}
|
||||
);
|
||||
const testcarID = [
|
||||
["CR00000100", "make", [{ glassLocation: "driver" }], false],
|
||||
["CR00067899", "make2", [{ glassLocation: "windshield" }], true],
|
||||
[
|
||||
"CR00062396",
|
||||
"make3",
|
||||
[{ glassLocation: "windshield" }, { glassLocation: "driver" }],
|
||||
false,
|
||||
],
|
||||
["CR00066428", "make4", [{ glassLocation: "rear" }], false],
|
||||
];
|
||||
|
||||
test.each(testcarID)(
|
||||
"%s %s %o should skip vin lookup is %s",
|
||||
async (carId, make, glassLocation, expectedVinSkip) => {
|
||||
const context = state;
|
||||
|
||||
context.state = {
|
||||
order: {
|
||||
vehicle: { make: make, carId: carId },
|
||||
damage: {
|
||||
glassToReplace: glassLocation,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
var vinOptionalResult = actions.isVinOptionalVehicle(context);
|
||||
expect(vinOptionalResult).toEqual(expectedVinSkip);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -102,7 +102,8 @@ html {
|
|||
}
|
||||
}
|
||||
&.textbox-question,
|
||||
&.dropdown-question {
|
||||
&.dropdown-question,
|
||||
&.phone-number-question {
|
||||
p {
|
||||
color: $red;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,3 +56,9 @@ body {
|
|||
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;
|
||||
z-index: 5;
|
||||
@include box-shadow-hover($blue-300);
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -190,3 +190,6 @@ $alert-color-scale: 40%;
|
|||
// This affects all [Bootstrap] modals
|
||||
$modal-fade-transform: translate(0, 100%);
|
||||
$modal-backdrop-opacity: 0;
|
||||
|
||||
//Disable default !important behavior
|
||||
$enable-important-utilities: false;
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ describe("alert.vue", () => {
|
|||
},
|
||||
})
|
||||
);
|
||||
|
||||
const wrapperDiv = wrapper.find("div");
|
||||
|
||||
// Assert
|
||||
|
|
@ -49,7 +48,7 @@ describe("alert.vue", () => {
|
|||
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
|
||||
const wrapper = shallowMount(
|
||||
alert,
|
||||
|
|
@ -58,11 +57,12 @@ describe("alert.vue", () => {
|
|||
manualHeadline: "testHeader",
|
||||
manualCopy: "testCopy with a {routerLink: testName, testLink} inside of it",
|
||||
},
|
||||
stubs: ["router-link"],
|
||||
stubs: ["textBlock"],
|
||||
})
|
||||
);
|
||||
|
||||
// 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", () => {
|
||||
|
|
|
|||
|
|
@ -14,17 +14,11 @@
|
|||
v-if="!doesCopyContainRouterLink(paragraph) && !doesCopyContainTextLink(paragraph)"
|
||||
v-html="paragraph"></p>
|
||||
<p class="m-0 text-body small" v-else>
|
||||
<template v-for="copy in splitCopyOnCMSPlaceHolder(paragraph)" :key="copy">
|
||||
<span v-if="doesCopyContainRouterLink(copy)">
|
||||
<router-link
|
||||
:to="{
|
||||
query: { [pageQueryString]: `${getRouterLinkRouteFromCopy(copy)}` },
|
||||
name: 'root',
|
||||
}"
|
||||
>{{ getRouterLinkDisplayTextFromCopy(copy) }}</router-link
|
||||
>
|
||||
</span>
|
||||
<span v-else-if="doesCopyContainTextLink(copy)">
|
||||
<template v-if="doesCopyContainRouterLink(paragraph)">
|
||||
<textBlock :customText="paragraph" class="mb-1" marginTopSizeOverride="0" />
|
||||
</template>
|
||||
<template v-else v-for="copy in splitCopyOnCMSPlaceHolder(paragraph)" :key="copy">
|
||||
<span v-if="doesCopyContainTextLink(copy)">
|
||||
<textLink
|
||||
linkType="text"
|
||||
:text="getRouterLinkDisplayTextFromCopy(copy)"
|
||||
|
|
@ -32,8 +26,7 @@
|
|||
@click-event="
|
||||
$emit('textLinkClicked', getRouterLinkRouteFromCopy(copy))
|
||||
"
|
||||
:data-bs-target="'#' + getRouterLinkRouteFromCopy(copy)"
|
||||
aria-label="Modal window" />
|
||||
:data-bs-target="'#' + getRouterLinkRouteFromCopy(copy)" />
|
||||
</span>
|
||||
<span v-else v-html="copy"></span>
|
||||
</template>
|
||||
|
|
@ -59,6 +52,7 @@ import {
|
|||
} from "@/helpers/cms-content-helper";
|
||||
import textLink from "@/ux-components/text-link/text-link";
|
||||
import { applicationConfig } from "@/constants/application-config";
|
||||
import textBlock from "@/digital-components/text-block/text-block";
|
||||
|
||||
export default {
|
||||
name: "alert",
|
||||
|
|
@ -136,6 +130,7 @@ export default {
|
|||
},
|
||||
components: {
|
||||
textLink,
|
||||
textBlock,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ export default {
|
|||
|
||||
+ .list-card-content {
|
||||
outline: none;
|
||||
display: block;
|
||||
display: flex;
|
||||
position: relative;
|
||||
|
||||
p {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
class="loader"
|
||||
role="alert"
|
||||
aria-label="Loading new page"
|
||||
v-bind:class="[this.loaderColor, this.loaderPosition]"></div>
|
||||
v-bind:class="[this.loaderColor, this.loaderPosition, this.blockUi]"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
|
@ -19,6 +19,11 @@ export default {
|
|||
loaderPosition: {
|
||||
type: String,
|
||||
},
|
||||
/* Controls whether we block the UI when the loader is active or now, defaults to true*/
|
||||
blockUi: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -82,5 +87,9 @@ export default {
|
|||
&.black:after {
|
||||
background-color: $black;
|
||||
}
|
||||
|
||||
&.no-block::before {
|
||||
z-index: -1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Reference in a new issue