Merge branch 'develop' into feature/CSR-1410
This commit is contained in:
commit
27e1e07297
24 changed files with 19254 additions and 197 deletions
|
|
@ -26,7 +26,7 @@ module.exports = {
|
|||
testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
statements: 77,
|
||||
statements: 76,
|
||||
// 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
|
||||
},
|
||||
},
|
||||
|
|
|
|||
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
|
|
@ -32,6 +32,7 @@ const storeMutations = {
|
|||
UPDATE_REGISTRATION_LAST_NAME: "updateRegistrationLastName",
|
||||
UPDATE_REGISTRATION: "updateRegistration",
|
||||
|
||||
UPDATE_SERVICE_ZIP: "updateServiceZip",
|
||||
UPDATE_SERVICE_LOCATION: "updateServiceLocation",
|
||||
UPDATE_SCHEDULE: "updateSchedule",
|
||||
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ export default {
|
|||
computed: {
|
||||
today() {
|
||||
if (this.todayOverrideDateString) {
|
||||
return new Date(this.todayOverrideDateString);
|
||||
return new Date(this.todayOverrideDateString + "T00:00:00");
|
||||
}
|
||||
return new Date();
|
||||
},
|
||||
|
|
@ -151,6 +151,9 @@ export default {
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
initializeComponent(initialData) {
|
||||
this.setCalendarData(initialData);
|
||||
},
|
||||
fireDateClickedEvent() {
|
||||
this.$emit("date-clicked");
|
||||
},
|
||||
|
|
@ -177,9 +180,12 @@ export default {
|
|||
nextSunday.setDate(date.getDate() + daysUntilNextSunday);
|
||||
return nextSunday;
|
||||
},
|
||||
getInitialViewWeeks(today, initialViewRowsToShow) {
|
||||
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++) {
|
||||
|
|
@ -192,15 +198,23 @@ 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 (splitWeekIndex > -1) {
|
||||
if (!preSelectedDateString && splitWeekIndex > -1) {
|
||||
// a preSelectedDateString precludes split week logic
|
||||
const week1 = [];
|
||||
const week2 = [];
|
||||
let switchToWeek2 = false;
|
||||
|
|
@ -245,7 +259,7 @@ export default {
|
|||
if (this.today) {
|
||||
todayDate = this.today;
|
||||
} else if (config.todayOverrideDateString) {
|
||||
todayDate = new Date(config.todayOverrideDateString);
|
||||
todayDate = new Date(config.todayOverrideDateString + "T00:00:00");
|
||||
} else {
|
||||
todayDate = new Date();
|
||||
}
|
||||
|
|
@ -262,20 +276,19 @@ export default {
|
|||
|
||||
const initialViewWeeks = this.getInitialViewWeeks(
|
||||
todayDate,
|
||||
config.initialViewRowsToShow
|
||||
config.initialViewRowsToShow,
|
||||
config.preSelectedDate
|
||||
);
|
||||
|
||||
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") {
|
||||
// TODO - NEED TO CREATE OPPOSITE LOGIC FOR "past" DIRECTION LIKE BELOW v v v
|
||||
if (calendarViewDirection === "future" && !config.preSelectedDate) {
|
||||
if (firstSaturdayMonth !== lastSundayMonth) {
|
||||
hideSomeDaysForInitialView = true;
|
||||
}
|
||||
|
|
@ -288,7 +301,7 @@ export default {
|
|||
}
|
||||
}
|
||||
|
||||
const myPromise = new Promise((resolve, reject) => {
|
||||
const loadInitialDataPromise = new Promise((resolve, reject) => {
|
||||
const response = config.customSelectableDatesCallback(
|
||||
initialViewStartDate.toISOString().split("T")[0],
|
||||
initialViewEndDate.toISOString().split("T")[0],
|
||||
|
|
@ -298,7 +311,7 @@ export default {
|
|||
resolve(response);
|
||||
});
|
||||
|
||||
return myPromise.then((response) => {
|
||||
return loadInitialDataPromise.then((response) => {
|
||||
const initialData = {
|
||||
todayDate: todayDate,
|
||||
initialViewStartDate: initialViewStartDate,
|
||||
|
|
@ -307,50 +320,11 @@ 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);
|
||||
|
||||
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 = this.$refs.datePickerFieldset;
|
||||
const targetMonth = document.getElementById(elementId);
|
||||
|
||||
scrollTopSmooth(wrapper, targetMonth, 800, "ease-in-out");
|
||||
},
|
||||
|
||||
async setCalendarData(config = {}) {
|
||||
this.hideSomeDaysForInitialView = config.hideSomeDaysForInitialView;
|
||||
const hideSecondMonth = config.hideSecondMonth;
|
||||
|
|
@ -370,6 +344,7 @@ export default {
|
|||
initialViewStartDate: config.initialViewStartDate,
|
||||
initialViewEndDate: config.initialViewEndDate,
|
||||
hideSecondMonth: hideSecondMonth,
|
||||
preSelectedDate: config.preSelectedDate,
|
||||
};
|
||||
if (direction === "future") {
|
||||
// first 0, then 1
|
||||
|
|
@ -389,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)
|
||||
|
|
@ -399,6 +383,7 @@ export default {
|
|||
monthsBeforeToLoadOffset (number),
|
||||
monthsAfterToLoadOffset (number),
|
||||
hideSecondMonth (boolean),
|
||||
preSelectedDate (string)
|
||||
|
||||
data used:
|
||||
todayDate (date object)
|
||||
|
|
@ -410,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;
|
||||
|
|
@ -447,11 +435,23 @@ export default {
|
|||
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"
|
||||
|
|
@ -581,6 +581,38 @@ export default {
|
|||
});
|
||||
});
|
||||
},
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -16,8 +16,7 @@
|
|||
:text="getRouterLinkDisplayTextFromCopy(copy)"
|
||||
href="#!"
|
||||
@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>
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -122,16 +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: "",
|
||||
autocomplete: null,
|
||||
autocompleteListener: null,
|
||||
showAddressFields: false,
|
||||
matchFound: null, // null = no attempted match, true = match was found, false = match was not found
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -218,7 +218,7 @@ export default {
|
|||
this.$loadScript(
|
||||
`https://maps.googleapis.com/maps/api/js?key=${apiKey}&libraries=places&callback=Function.prototype`
|
||||
).then(() => {
|
||||
// When loaded trigger the setup
|
||||
// When loaded, trigger the setup
|
||||
this.initializeAutocomplete();
|
||||
});
|
||||
},
|
||||
|
|
@ -255,6 +255,11 @@ export default {
|
|||
});
|
||||
|
||||
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
|
||||
|
|
@ -334,6 +339,9 @@ export default {
|
|||
|
||||
// After filling in the address fields, disable the address autocomplete
|
||||
this.unloadAutocomplete();
|
||||
|
||||
// Restore focus to the first address field
|
||||
this.addressField1.focus();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -18,13 +18,7 @@ export default {
|
|||
}) {
|
||||
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),
|
||||
};
|
||||
|
|
@ -44,7 +38,7 @@ export default {
|
|||
additionalEventData = "_" + additionalSuccessEventDataHandler(response);
|
||||
}
|
||||
const pageName = analyticsMixIn.methods.getPageName();
|
||||
const nextPageName = router.getNextPage() || pageName;
|
||||
const nextPageName = router.lastNavigationPage || pageName;
|
||||
analyticsMixIn.methods.pushEventToGA(
|
||||
GaCategories.API_RESPONSE,
|
||||
`${nextPageName}_${endpoint}`,
|
||||
|
|
|
|||
|
|
@ -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-4">
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -151,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;
|
||||
|
|
|
|||
|
|
@ -44,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">
|
||||
|
|
@ -117,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(
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
@ -114,9 +114,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(
|
||||
|
|
@ -303,16 +302,11 @@ export default {
|
|||
await this.dispatchStoreAction(storeActions.SAVE_EMAIL, this.email, false);
|
||||
|
||||
await this.dispatchStoreAction(
|
||||
storeActions.SAVE_SERVICE_LOCATION,
|
||||
storeActions.SAVE_SERVICE_ZIP_CODE_INFO,
|
||||
{
|
||||
address: "",
|
||||
address2: "",
|
||||
city: "",
|
||||
state: resultMap.serviceZipValidationResponse.state,
|
||||
zipCode: this.serviceZipCode,
|
||||
zipCodeCtu: resultMap.serviceZipValidationResponse.zipCodeCtu,
|
||||
appointmentType: "",
|
||||
isVehicleProtected: null,
|
||||
},
|
||||
false
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ import { storeActions } from "@/constants/store-actions";
|
|||
import { settleAllPromises } from "@/helpers/layout-helper";
|
||||
import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-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";
|
||||
|
|
@ -71,33 +72,98 @@ 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, providerNumber) => {
|
||||
// USING DATES PASSED, MAKE AN API CALL
|
||||
// Define constants
|
||||
const TIME_SLOTS_CALL_DAYS_LIMIT = 34; // needs to be 34 for API limits (35 does not consistently work)
|
||||
|
||||
let newTimeSlotsResponse;
|
||||
if (appointmentType === AppointmentTypeStrings.MOBILE) {
|
||||
newTimeSlotsResponse = await baseMixin.methods.dispatchStoreAction(
|
||||
storeActions.GET_MOBILE_TIME_SLOTS,
|
||||
{
|
||||
startDate: startDate,
|
||||
endDate: endDate,
|
||||
},
|
||||
false
|
||||
);
|
||||
} else {
|
||||
newTimeSlotsResponse = await baseMixin.methods.dispatchStoreAction(
|
||||
storeActions.GET_SHOP_TIME_SLOTS,
|
||||
{
|
||||
startDate: startDate,
|
||||
endDate: endDate,
|
||||
shopAppointmentType: appointmentType,
|
||||
providerNumber: providerNumber,
|
||||
},
|
||||
false
|
||||
);
|
||||
const getAvailableDates = async (
|
||||
startDateString,
|
||||
endDateString,
|
||||
appointmentType,
|
||||
providerNumber
|
||||
) => {
|
||||
const apiEndDateLimit = new Date(startDateString + "T00:00:00");
|
||||
const endDate = new Date(endDateString + "T00:00:00");
|
||||
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 + "T00:00:00");
|
||||
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 + "T00:00:00");
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
return newTimeSlotsResponse.data;
|
||||
// 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;
|
||||
});
|
||||
};
|
||||
|
||||
export default {
|
||||
|
|
@ -107,7 +173,7 @@ export default {
|
|||
selectedDate: this.getSelectedDate(),
|
||||
selectedTimeSlotData: {
|
||||
id: this.getSelectedRouteCode(),
|
||||
isPremiumAppointment: null,
|
||||
isPremiumAppointment: this.isMobilePremiumFeeOnOrderInVuex(),
|
||||
},
|
||||
selectableDatesData: [],
|
||||
mobilePremiumAppointmentFee: null,
|
||||
|
|
@ -116,11 +182,17 @@ export default {
|
|||
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,
|
||||
preSelectedDate: preSelectedDate,
|
||||
});
|
||||
|
||||
const premiumFeePromise = baseMixin.methods.dispatchStoreAction(
|
||||
|
|
@ -268,6 +340,12 @@ export default {
|
|||
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.selectedTimeSlotData.id) {
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
selectedTimeSlotId: this.modelValue.id,
|
||||
selectedTimeSlotId: this.getModifiedSelectedTimeSlotId(),
|
||||
timeSlotModalListButton: timeSlotModalListButton,
|
||||
};
|
||||
},
|
||||
|
|
@ -94,12 +94,8 @@ export default {
|
|||
},
|
||||
watch: {
|
||||
modelValue() {
|
||||
this.selectedTimeSlotId = this.getModifiedSelectedTimeSlotId();
|
||||
// Run component validation that is used at parent level
|
||||
if (this.modelValue.isPremiumAppointment) {
|
||||
this.selectedTimeSlotId = this.addPremiumFlagToInput(this.modelValue.id);
|
||||
} else {
|
||||
this.selectedTimeSlotId = this.modelValue.id;
|
||||
}
|
||||
this.handleChange(this.modelValue.id);
|
||||
},
|
||||
availableTimeSlots(newValue) {
|
||||
|
|
@ -256,6 +252,13 @@ export default {
|
|||
onModalClosed() {
|
||||
this.$emit("time-slot-modal-closed");
|
||||
},
|
||||
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]);
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
@ -122,10 +119,10 @@ import { errorMessages } from "@/constants/error-messages";
|
|||
// DEFINE VALIDATION RULES
|
||||
defineRule("mobile-location-required", (value) => {
|
||||
if (
|
||||
value.addressQuestions.streetAddress == null ||
|
||||
value.addressQuestions.city == null ||
|
||||
value.addressQuestions.state == null ||
|
||||
value.addressQuestions.zipCode == null ||
|
||||
value.addressQuestions.streetAddress == "" ||
|
||||
value.addressQuestions.city == "" ||
|
||||
value.addressQuestions.state == "" ||
|
||||
value.addressQuestions.zipCode == "" ||
|
||||
value.isVehicleProtected == null
|
||||
) {
|
||||
return errorMessages.MOBILE_LOCATION_REQUIRED;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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-2">
|
||||
|
|
@ -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(
|
||||
|
|
@ -325,14 +324,9 @@ export default {
|
|||
await this.dispatchStoreAction(
|
||||
storeActions.SAVE_SERVICE_ZIP_CODE_INFO,
|
||||
{
|
||||
address: "",
|
||||
address2: "",
|
||||
city: "",
|
||||
state: resultMap.zipCodeData.state,
|
||||
zipCode: this.serviceZipCode,
|
||||
zipCodeCtu: resultMap.zipCodeData.zipCodeCtu,
|
||||
appointmentType: "",
|
||||
isVehicleProtected: null,
|
||||
},
|
||||
false
|
||||
);
|
||||
|
|
@ -367,14 +361,9 @@ export default {
|
|||
await this.dispatchStoreAction(
|
||||
storeActions.SAVE_SERVICE_ZIP_CODE_INFO,
|
||||
{
|
||||
address: null,
|
||||
address2: null,
|
||||
city: null,
|
||||
state: zipCodeData.state,
|
||||
zipCode: this.serviceZipCode,
|
||||
zipCodeCtu: zipCodeData.zipCodeCtu,
|
||||
appointmentType: null,
|
||||
isVehicleProtected: null,
|
||||
},
|
||||
false
|
||||
);
|
||||
|
|
|
|||
|
|
@ -156,6 +156,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);
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ 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 { deleteFunnelCookie } from "@/helpers/heritage-integration/cookie-helper.js";
|
||||
import { deepEqual } from "@/helpers/object-helper";
|
||||
|
|
@ -243,6 +244,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;
|
||||
|
|
@ -642,13 +648,28 @@ 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,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
|
|
@ -1346,7 +1367,7 @@ export const actions = {
|
|||
},
|
||||
},
|
||||
customer: {
|
||||
emailAddress: order.customer.emailAddress,
|
||||
emailAddress: order.customer.emailAddress || null,
|
||||
},
|
||||
damage: {
|
||||
numberOfChips: damage.numberOfChips,
|
||||
|
|
@ -1889,7 +1910,7 @@ export const actions = {
|
|||
context.commit(storeMutations.RESET_SERVICE_LOCATION_MOBILE_ADDRESS);
|
||||
}
|
||||
|
||||
context.commit(storeMutations.UPDATE_SERVICE_LOCATION, serviceZipCodeInfo);
|
||||
context.commit(storeMutations.UPDATE_SERVICE_ZIP, serviceZipCodeInfo);
|
||||
},
|
||||
|
||||
saveServiceLocation(context, serviceLocationInfo) {
|
||||
|
|
@ -1943,6 +1964,19 @@ export const actions = {
|
|||
},
|
||||
|
||||
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":
|
||||
|
|
@ -1951,14 +1985,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
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
@ -3209,6 +3211,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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue