Merge branch 'develop' into feature/CSR-1410

This commit is contained in:
Leah Schumann 2023-06-29 07:03:35 -04:00
commit 27e1e07297
24 changed files with 19254 additions and 197 deletions

View file

@ -26,7 +26,7 @@ module.exports = {
testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"], testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"],
coverageThreshold: { coverageThreshold: {
global: { global: {
statements: 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 // 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
}, },
}, },

File diff suppressed because it is too large Load diff

View file

@ -32,6 +32,7 @@ const storeMutations = {
UPDATE_REGISTRATION_LAST_NAME: "updateRegistrationLastName", UPDATE_REGISTRATION_LAST_NAME: "updateRegistrationLastName",
UPDATE_REGISTRATION: "updateRegistration", UPDATE_REGISTRATION: "updateRegistration",
UPDATE_SERVICE_ZIP: "updateServiceZip",
UPDATE_SERVICE_LOCATION: "updateServiceLocation", UPDATE_SERVICE_LOCATION: "updateServiceLocation",
UPDATE_SCHEDULE: "updateSchedule", UPDATE_SCHEDULE: "updateSchedule",

View file

@ -112,7 +112,7 @@ export default {
computed: { computed: {
today() { today() {
if (this.todayOverrideDateString) { if (this.todayOverrideDateString) {
return new Date(this.todayOverrideDateString); return new Date(this.todayOverrideDateString + "T00:00:00");
} }
return new Date(); return new Date();
}, },
@ -151,6 +151,9 @@ export default {
}, },
}, },
methods: { methods: {
initializeComponent(initialData) {
this.setCalendarData(initialData);
},
fireDateClickedEvent() { fireDateClickedEvent() {
this.$emit("date-clicked"); this.$emit("date-clicked");
}, },
@ -177,9 +180,12 @@ export default {
nextSunday.setDate(date.getDate() + daysUntilNextSunday); nextSunday.setDate(date.getDate() + daysUntilNextSunday);
return nextSunday; return nextSunday;
}, },
getInitialViewWeeks(today, initialViewRowsToShow) { getInitialViewWeeks(today, initialViewRowsToShow, preSelectedDateString) {
// TODO: this only is for future direction; need to create logic for past direction // TODO: this only is for future direction; need to create logic for past direction
const weeks = []; const weeks = [];
if (preSelectedDateString) initialViewRowsToShow = 26;
let weekStartDate = this.getWeekStartDate(today); let weekStartDate = this.getWeekStartDate(today);
let weekEndDate = this.getWeekEndDate(today); let weekEndDate = this.getWeekEndDate(today);
for (let i = 0; i < initialViewRowsToShow; i++) { for (let i = 0; i < initialViewRowsToShow; i++) {
@ -192,15 +198,23 @@ export default {
weekStartDate: weekStartDate, weekStartDate: weekStartDate,
weekEndDate: weekEndDate, weekEndDate: weekEndDate,
}); });
if (
preSelectedDateString &&
new Date(preSelectedDateString + "T00:00:00") < weekEndDate
) {
break;
}
} }
// are any of these weeks split between two months? // are any of these weeks split between two months?
// NOTE: a week split between two months counts as 2 weeks
const hasSplitWeek = (week) => { const hasSplitWeek = (week) => {
return week.weekStartDate.getMonth() !== week.weekEndDate.getMonth() ? true : false; return week.weekStartDate.getMonth() !== week.weekEndDate.getMonth() ? true : false;
}; };
const splitWeekIndex = weeks.findIndex(hasSplitWeek); const splitWeekIndex = weeks.findIndex(hasSplitWeek);
if (splitWeekIndex > -1) { if (!preSelectedDateString && splitWeekIndex > -1) {
// a preSelectedDateString precludes split week logic
const week1 = []; const week1 = [];
const week2 = []; const week2 = [];
let switchToWeek2 = false; let switchToWeek2 = false;
@ -245,7 +259,7 @@ export default {
if (this.today) { if (this.today) {
todayDate = this.today; todayDate = this.today;
} else if (config.todayOverrideDateString) { } else if (config.todayOverrideDateString) {
todayDate = new Date(config.todayOverrideDateString); todayDate = new Date(config.todayOverrideDateString + "T00:00:00");
} else { } else {
todayDate = new Date(); todayDate = new Date();
} }
@ -262,20 +276,19 @@ export default {
const initialViewWeeks = this.getInitialViewWeeks( const initialViewWeeks = this.getInitialViewWeeks(
todayDate, todayDate,
config.initialViewRowsToShow config.initialViewRowsToShow,
config.preSelectedDate
); );
const initialViewStartDate = todayDate; const initialViewStartDate = todayDate;
const initialViewEndDate = initialViewWeeks[initialViewWeeks.length - 1].weekEndDate; const initialViewEndDate = initialViewWeeks[initialViewWeeks.length - 1].weekEndDate;
const firstSaturdayMonth = initialViewWeeks[0].weekEndDate.getMonth(); const firstSaturdayMonth = initialViewWeeks[0].weekEndDate.getMonth();
const lastSundayMonth = const lastSundayMonth =
initialViewWeeks[initialViewWeeks.length - 1].weekStartDate.getMonth(); initialViewWeeks[initialViewWeeks.length - 1].weekStartDate.getMonth();
let hideSomeDaysForInitialView = false; let hideSomeDaysForInitialView = false;
let hideSecondMonth = false; let hideSecondMonth = false;
// TODO - NEED TO CREATE OPPOSITE LOGIC FOR "past" DIRECTION LIKE BELOW vvvvv // TODO - NEED TO CREATE OPPOSITE LOGIC FOR "past" DIRECTION LIKE BELOW v v v
if (calendarViewDirection === "future") { if (calendarViewDirection === "future" && !config.preSelectedDate) {
if (firstSaturdayMonth !== lastSundayMonth) { if (firstSaturdayMonth !== lastSundayMonth) {
hideSomeDaysForInitialView = true; hideSomeDaysForInitialView = true;
} }
@ -288,7 +301,7 @@ export default {
} }
} }
const myPromise = new Promise((resolve, reject) => { const loadInitialDataPromise = new Promise((resolve, reject) => {
const response = config.customSelectableDatesCallback( const response = config.customSelectableDatesCallback(
initialViewStartDate.toISOString().split("T")[0], initialViewStartDate.toISOString().split("T")[0],
initialViewEndDate.toISOString().split("T")[0], initialViewEndDate.toISOString().split("T")[0],
@ -298,7 +311,7 @@ export default {
resolve(response); resolve(response);
}); });
return myPromise.then((response) => { return loadInitialDataPromise.then((response) => {
const initialData = { const initialData = {
todayDate: todayDate, todayDate: todayDate,
initialViewStartDate: initialViewStartDate, initialViewStartDate: initialViewStartDate,
@ -307,50 +320,11 @@ export default {
initialShopTimeSlotsResponse: response, initialShopTimeSlotsResponse: response,
hideSomeDaysForInitialView: hideSomeDaysForInitialView, hideSomeDaysForInitialView: hideSomeDaysForInitialView,
hideSecondMonth: hideSecondMonth, hideSecondMonth: hideSecondMonth,
preSelectedDate: config.preSelectedDate,
}; };
return initialData; 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 = {}) { async setCalendarData(config = {}) {
this.hideSomeDaysForInitialView = config.hideSomeDaysForInitialView; this.hideSomeDaysForInitialView = config.hideSomeDaysForInitialView;
const hideSecondMonth = config.hideSecondMonth; const hideSecondMonth = config.hideSecondMonth;
@ -370,6 +344,7 @@ export default {
initialViewStartDate: config.initialViewStartDate, initialViewStartDate: config.initialViewStartDate,
initialViewEndDate: config.initialViewEndDate, initialViewEndDate: config.initialViewEndDate,
hideSecondMonth: hideSecondMonth, hideSecondMonth: hideSecondMonth,
preSelectedDate: config.preSelectedDate,
}; };
if (direction === "future") { if (direction === "future") {
// first 0, then 1 // first 0, then 1
@ -389,8 +364,17 @@ export default {
} }
this.months = months; this.months = months;
this.isLoading = false; 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) { async getMonthData(offset = requiredParameter(), options) {
/* options will contain: /* options will contain:
calendarViewDirection (string) calendarViewDirection (string)
@ -399,6 +383,7 @@ export default {
monthsBeforeToLoadOffset (number), monthsBeforeToLoadOffset (number),
monthsAfterToLoadOffset (number), monthsAfterToLoadOffset (number),
hideSecondMonth (boolean), hideSecondMonth (boolean),
preSelectedDate (string)
data used: data used:
todayDate (date object) todayDate (date object)
@ -410,7 +395,10 @@ export default {
const calendarViewDirection = options.calendarViewDirection; const calendarViewDirection = options.calendarViewDirection;
const initialViewStartDate = options.initialViewStartDate; const initialViewStartDate = options.initialViewStartDate;
const initialViewEndDate = options.initialViewEndDate; 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 = []; const dates = [];
let monthClass = ""; let monthClass = "";
let isMonthThatHidesSomeDaysForInitialView; let isMonthThatHidesSomeDaysForInitialView;
@ -447,11 +435,23 @@ export default {
const startDateDayIndex = monthStartDate.getDay(); const startDateDayIndex = monthStartDate.getDay();
const endDateDayIndex = monthEndDate.getDay(); const endDateDayIndex = monthEndDate.getDay();
if (Math.abs(offset) === 1 && hideSecondMonth) { if (preSelectedDateObj) {
monthClass = monthClass + " month-hidden"; if (
} else if (Math.abs(offset) > 1) { monthStartDate.getFullYear() === preSelectedDateObj.getFullYear() &&
monthClass = monthClass + " month-hidden"; 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 ( if (
Math.abs(offset) === options.monthsAfterToLoadOffset && Math.abs(offset) === options.monthsAfterToLoadOffset &&
calendarViewDirection === "future" 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: { components: {
loader, loader,

View file

@ -16,8 +16,7 @@
:text="getRouterLinkDisplayTextFromCopy(copy)" :text="getRouterLinkDisplayTextFromCopy(copy)"
href="#!" href="#!"
@click-event="$emit('textLinkClicked', getRouterLinkRouteFromCopy(copy))" @click-event="$emit('textLinkClicked', getRouterLinkRouteFromCopy(copy))"
:data-bs-target="'#' + getRouterLinkRouteFromCopy(copy)" :data-bs-target="'#' + getRouterLinkRouteFromCopy(copy)" />
aria-label="Modal window" />
</span> </span>
<span v-else v-html="copy"></span> <span v-else v-html="copy"></span>
</span> </span>

View file

@ -122,16 +122,16 @@ export default {
}, },
data() { data() {
return { return {
autocomplete: null,
autocompleteListener: null,
showAddressFields: false,
matchFound: null, // null = no attempted match, true = match was found, false = match was not found
displayVerificationWarning: false, displayVerificationWarning: false,
displayNoMatchWarning: false, displayNoMatchWarning: false,
alertHeadlineVerificationWarning: "", alertHeadlineVerificationWarning: "",
alertCopyVerificationWarning: "", alertCopyVerificationWarning: "",
alertHeadlineNoMatchWarning: "", alertHeadlineNoMatchWarning: "",
alertCopyNoMatchWarning: "", alertCopyNoMatchWarning: "",
autocomplete: null,
autocompleteListener: null,
showAddressFields: false,
matchFound: null, // null = no attempted match, true = match was found, false = match was not found
}; };
}, },
computed: { computed: {
@ -218,7 +218,7 @@ export default {
this.$loadScript( this.$loadScript(
`https://maps.googleapis.com/maps/api/js?key=${apiKey}&libraries=places&callback=Function.prototype` `https://maps.googleapis.com/maps/api/js?key=${apiKey}&libraries=places&callback=Function.prototype`
).then(() => { ).then(() => {
// When loaded trigger the setup // When loaded, trigger the setup
this.initializeAutocomplete(); this.initializeAutocomplete();
}); });
}, },
@ -255,6 +255,11 @@ export default {
}); });
this.addressField1.addEventListener("keydown", (e) => { 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"); const event = new Event("place_changed");
// When either of the two enter keys or the tab key are pressed // 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 // After filling in the address fields, disable the address autocomplete
this.unloadAutocomplete(); this.unloadAutocomplete();
// Restore focus to the first address field
this.addressField1.focus();
}); });
} }
}, },

View file

@ -18,13 +18,7 @@ export default {
}) { }) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const cfDistroUrl = applicationConfig.CONSUMER_CF_DISTRO; const cfDistroUrl = applicationConfig.CONSUMER_CF_DISTRO;
let payloadAndAnalyticsData = {}; const payloadAndAnalyticsData = Object.assign({}, payload, { AppName: "FixMyGlass" });
if (isFormData) {
payloadAndAnalyticsData = payload;
payloadAndAnalyticsData.append("AppName", "FixMyGlass");
} else {
Object.assign(payloadAndAnalyticsData, payload, { AppName: "FixMyGlass" });
}
const headers = { const headers = {
[headerKeys.EXPERIMENT]: JSON.stringify(store.getters.experimentSettings), [headerKeys.EXPERIMENT]: JSON.stringify(store.getters.experimentSettings),
}; };
@ -44,7 +38,7 @@ export default {
additionalEventData = "_" + additionalSuccessEventDataHandler(response); additionalEventData = "_" + additionalSuccessEventDataHandler(response);
} }
const pageName = analyticsMixIn.methods.getPageName(); const pageName = analyticsMixIn.methods.getPageName();
const nextPageName = router.getNextPage() || pageName; const nextPageName = router.lastNavigationPage || pageName;
analyticsMixIn.methods.pushEventToGA( analyticsMixIn.methods.pushEventToGA(
GaCategories.API_RESPONSE, GaCategories.API_RESPONSE,
`${nextPageName}_${endpoint}`, `${nextPageName}_${endpoint}`,

View file

@ -59,7 +59,7 @@
cmsWidgetName="ServiceZipQuestionWidget" cmsWidgetName="ServiceZipQuestionWidget"
v-model="serviceZipCode" v-model="serviceZipCode"
ref="serviceZip" ref="serviceZip"
inputId="7add1b26df344f2caf1678de5797803f" customInputId="serviceZip"
aria-haspopup="" aria-haspopup=""
mask="#####" mask="#####"
validationRules="service-zip-required|service-zip-format" /> validationRules="service-zip-required|service-zip-format" />

View file

@ -6,7 +6,7 @@
cmsWidgetName="FirstNameQuestionWidget" cmsWidgetName="FirstNameQuestionWidget"
v-model="customerModel.firstName" v-model="customerModel.firstName"
ref="firstName" ref="firstName"
inputId="08497a2efd9a4a73a70360ab47b4838d" customInputId="firstName"
validationRules="first-name-required" /> validationRules="first-name-required" />
</div> </div>
</div> </div>
@ -16,7 +16,7 @@
cmsWidgetName="LastNameQuestionWidget" cmsWidgetName="LastNameQuestionWidget"
v-model="customerModel.lastName" v-model="customerModel.lastName"
ref="lastName" ref="lastName"
inputId="0030e56a57e74a4ab92de7fb8e97fec5" customInputId="lastName"
validationRules="last-name-required" /> validationRules="last-name-required" />
</div> </div>
</div> </div>
@ -26,8 +26,8 @@
cmsWidgetName="EmailAddressQuestionWidget" cmsWidgetName="EmailAddressQuestionWidget"
v-model="customerModel.emailAddress" v-model="customerModel.emailAddress"
ref="emailAddress" ref="emailAddress"
inputId="00450a91b8964a768ce3992e6feb890f" customInputId="emailAddress"
validationRules="email-address-required|email-address-format" /> validationRules="email-address-format" />
</div> </div>
</div> </div>
<div class="row mb-4"> <div class="row mb-4">
@ -49,7 +49,6 @@ import textBlock from "@/digital-components/text-block/text-block";
// DEFINE VALIDATION RULES // DEFINE VALIDATION RULES
defineRule("first-name-required", required(errorMessages.FIRST_NAME_REQUIRED)); defineRule("first-name-required", required(errorMessages.FIRST_NAME_REQUIRED));
defineRule("last-name-required", required(errorMessages.LAST_NAME_REQUIRED)); defineRule("last-name-required", required(errorMessages.LAST_NAME_REQUIRED));
defineRule("email-address-required", required(errorMessages.EMAIL_ADDRESS_REQUIRED));
defineRule( defineRule(
"email-address-format", "email-address-format",
regex( regex(

View file

@ -151,7 +151,6 @@ export default {
if ( if (
store.getters.order.vehicle.carId && store.getters.order.vehicle.carId &&
store.getters.order.serviceLocation.zipCode && store.getters.order.serviceLocation.zipCode &&
store.getters.order.customer.emailAddress &&
store.getters.pageData(fmgPageValues.ADDRESS_VEHICLES) store.getters.pageData(fmgPageValues.ADDRESS_VEHICLES)
) { ) {
return true; return true;

View file

@ -44,8 +44,8 @@
cmsWidgetName="EmailAddressQuestionWidget" cmsWidgetName="EmailAddressQuestionWidget"
v-model="emailAddress" v-model="emailAddress"
inputId="emailAddress" inputId="emailAddress"
isRequired disableAutoFill
validationRules="email-address-required|email-address-format" /> validationRules="email-address-format" />
</div> </div>
</div> </div>
<div class="row mb-2"> <div class="row mb-2">
@ -117,7 +117,6 @@ import { queryStrings } from "@/constants/query-strings";
// Define Validation Rules // Define Validation Rules
defineRule("zip-required", required(errorMessages.SERVICE_ZIP_REQUIRED)); defineRule("zip-required", required(errorMessages.SERVICE_ZIP_REQUIRED));
defineRule("zip-format", regex(/(^\d{5}$)|(^\d{5}-\d{4}$)/, errorMessages.SERVICE_ZIP_FORMAT)); defineRule("zip-format", regex(/(^\d{5}$)|(^\d{5}-\d{4}$)/, errorMessages.SERVICE_ZIP_FORMAT));
defineRule("email-address-required", required(errorMessages.EMAIL_ADDRESS_REQUIRED));
defineRule( defineRule(
"email-address-format", "email-address-format",
regex( regex(

View file

@ -13,7 +13,7 @@
cmsWidgetName="LicensePlateNumberQuestionWidget" cmsWidgetName="LicensePlateNumberQuestionWidget"
v-model="licensePlate" v-model="licensePlate"
isRequired isRequired
inputId="license_plate" customInputId="licensePlate"
validationRules="license-plate-required" /> validationRules="license-plate-required" />
</div> </div>
</div> </div>
@ -22,9 +22,9 @@
<textboxQuestion <textboxQuestion
cmsWidgetName="RegistrationZipQuestionWidget" cmsWidgetName="RegistrationZipQuestionWidget"
v-model="registrationZipCode" v-model="registrationZipCode"
inputId="zip" customInputId="zip"
mask="#####" mask="#####"
validationRules="zip-required|zip-format" /> validationRules="registration-zip-required|zip-format" />
</div> </div>
</div> </div>
<div class="row mt-0"> <div class="row mt-0">
@ -32,8 +32,8 @@
<textboxQuestion <textboxQuestion
cmsWidgetName="EmailAddressQuestionWidget" cmsWidgetName="EmailAddressQuestionWidget"
v-model="email" v-model="email"
inputId="email" customInputId="email"
validationRules="email-address-required|email-address-format" /> validationRules="email-address-format" />
</div> </div>
</div> </div>
<div class="row mb-2"> <div class="row mb-2">
@ -114,9 +114,8 @@ import vinPagesMixin from "@/mixins/vin-pages-mixin";
// DEFINE VALIDATION RULES // DEFINE VALIDATION RULES
defineRule("license-plate-required", required(errorMessages.LICENSE_PLATE_REQUIRED)); 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("zip-format", regex(/(^\d{5}$)|(^\d{5}-\d{4}$)/, errorMessages.SERVICE_ZIP_FORMAT));
defineRule("email-address-required", required(errorMessages.EMAIL_ADDRESS_REQUIRED));
defineRule( defineRule(
"email-address-format", "email-address-format",
regex( regex(
@ -303,16 +302,11 @@ export default {
await this.dispatchStoreAction(storeActions.SAVE_EMAIL, this.email, false); await this.dispatchStoreAction(storeActions.SAVE_EMAIL, this.email, false);
await this.dispatchStoreAction( await this.dispatchStoreAction(
storeActions.SAVE_SERVICE_LOCATION, storeActions.SAVE_SERVICE_ZIP_CODE_INFO,
{ {
address: "",
address2: "",
city: "",
state: resultMap.serviceZipValidationResponse.state, state: resultMap.serviceZipValidationResponse.state,
zipCode: this.serviceZipCode, zipCode: this.serviceZipCode,
zipCodeCtu: resultMap.serviceZipValidationResponse.zipCodeCtu, zipCodeCtu: resultMap.serviceZipValidationResponse.zipCodeCtu,
appointmentType: "",
isVehicleProtected: null,
}, },
false false
); );

View file

@ -38,8 +38,7 @@
args: getRouterLinkRouteFromCopy(copy), args: getRouterLinkRouteFromCopy(copy),
}) })
" "
:data-bs-target="'#' + getRouterLinkRouteFromCopy(copy)" :data-bs-target="'#' + getRouterLinkRouteFromCopy(copy)" />
aria-label="Modal window" />
</span> </span>
</template> </template>
</li> </li>

View file

@ -11,3 +11,10 @@ export async function getAlertReasons(ctu) {
return Promise.resolve(alertReasons); 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
}

View file

@ -63,6 +63,7 @@ import { storeActions } from "@/constants/store-actions";
import { settleAllPromises } from "@/helpers/layout-helper"; import { settleAllPromises } from "@/helpers/layout-helper";
import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper"; import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
import { splitCopyOnCMSPlaceHolder } 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 { AppointmentTypeStrings, PREMIUM_FEE_PART_TYPE } from "@/constants/schedule-constants";
import { errorMessages } from "@/constants/error-messages"; import { errorMessages } from "@/constants/error-messages";
import { required } from "@/helpers/validation-rules"; import { required } from "@/helpers/validation-rules";
@ -71,33 +72,98 @@ import store from "@/store";
defineRule("date-required", required(errorMessages.DATE_REQUIRED)); defineRule("date-required", required(errorMessages.DATE_REQUIRED));
defineRule("time-slot-selection-required", required(errorMessages.DATE_REQUIRED)); defineRule("time-slot-selection-required", required(errorMessages.DATE_REQUIRED));
const getAvailableDates = async (startDate, endDate, appointmentType, providerNumber) => { // Define constants
// USING DATES PASSED, MAKE AN API CALL const TIME_SLOTS_CALL_DAYS_LIMIT = 34; // needs to be 34 for API limits (35 does not consistently work)
let newTimeSlotsResponse; const getAvailableDates = async (
if (appointmentType === AppointmentTypeStrings.MOBILE) { startDateString,
newTimeSlotsResponse = await baseMixin.methods.dispatchStoreAction( endDateString,
storeActions.GET_MOBILE_TIME_SLOTS, appointmentType,
{ providerNumber
startDate: startDate, ) => {
endDate: endDate, const apiEndDateLimit = new Date(startDateString + "T00:00:00");
}, const endDate = new Date(endDateString + "T00:00:00");
false apiEndDateLimit.setDate(apiEndDateLimit.getDate() + TIME_SLOTS_CALL_DAYS_LIMIT);
);
} else { // how many days are between startDate and endDate?
newTimeSlotsResponse = await baseMixin.methods.dispatchStoreAction( const difference = calcDaysBetweenDates(startDateString, endDateString);
storeActions.GET_SHOP_TIME_SLOTS, const apiCallsCount = Math.ceil(difference / TIME_SLOTS_CALL_DAYS_LIMIT);
{ const storeActionConfigs = [];
startDate: startDate, const timeSlotsData = {};
endDate: endDate, let apiStartDate = new Date(startDateString + "T00:00:00");
shopAppointmentType: appointmentType, let apiEndDate = apiEndDateLimit;
providerNumber: providerNumber, timeSlotsData.days = [];
},
false 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 { export default {
@ -107,7 +173,7 @@ export default {
selectedDate: this.getSelectedDate(), selectedDate: this.getSelectedDate(),
selectedTimeSlotData: { selectedTimeSlotData: {
id: this.getSelectedRouteCode(), id: this.getSelectedRouteCode(),
isPremiumAppointment: null, isPremiumAppointment: this.isMobilePremiumFeeOnOrderInVuex(),
}, },
selectableDatesData: [], selectableDatesData: [],
mobilePremiumAppointmentFee: null, mobilePremiumAppointmentFee: null,
@ -116,11 +182,17 @@ export default {
async beforeRouteEnter(to, from, next) { async beforeRouteEnter(to, from, next) {
// Call APIs // Call APIs
const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage); 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 // setup config options for date-picker
selectableDatesSetting: "custom", selectableDatesSetting: "custom",
initialViewRowsToShow: 5, initialViewRowsToShow: 5,
customSelectableDatesCallback: getAvailableDates, customSelectableDatesCallback: getAvailableDates,
preSelectedDate: preSelectedDate,
}); });
const premiumFeePromise = baseMixin.methods.dispatchStoreAction( const premiumFeePromise = baseMixin.methods.dispatchStoreAction(
@ -268,6 +340,12 @@ export default {
getSelectedRouteCode() { getSelectedRouteCode() {
return store.getters.order.schedule.routeCode; 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() { timeSlotModalClosed() {
// Clear the selectedDate if no timeSlot has been selected // Clear the selectedDate if no timeSlot has been selected
if (!this.selectedTimeSlotData.id) { if (!this.selectedTimeSlotData.id) {

View file

@ -80,7 +80,7 @@ export default {
}, },
data() { data() {
return { return {
selectedTimeSlotId: this.modelValue.id, selectedTimeSlotId: this.getModifiedSelectedTimeSlotId(),
timeSlotModalListButton: timeSlotModalListButton, timeSlotModalListButton: timeSlotModalListButton,
}; };
}, },
@ -94,12 +94,8 @@ export default {
}, },
watch: { watch: {
modelValue() { modelValue() {
this.selectedTimeSlotId = this.getModifiedSelectedTimeSlotId();
// Run component validation that is used at parent level // 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); this.handleChange(this.modelValue.id);
}, },
availableTimeSlots(newValue) { availableTimeSlots(newValue) {
@ -256,6 +252,13 @@ export default {
onModalClosed() { onModalClosed() {
this.$emit("time-slot-modal-closed"); 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" // Expected input: "HH:MM"
getDisplayTextForMilitaryTime(militaryTimeInput) { getDisplayTextForMilitaryTime(militaryTimeInput) {
let hours = parseInt(militaryTimeInput.split(":")[0]); let hours = parseInt(militaryTimeInput.split(":")[0]);

View file

@ -14,8 +14,7 @@
linkType="text" linkType="text"
:text="mobileLocationLinkText" :text="mobileLocationLinkText"
href="#!" href="#!"
@click-event="openModal" @click-event="openModal" />
aria-label="Modal window" />
</div> </div>
<div v-show="errorMessage" class="row my-1 form-test-error"> <div v-show="errorMessage" class="row my-1 form-test-error">
<span class="d-inline-flex small mt-0 center-error-message" role="alert"> <span class="d-inline-flex small mt-0 center-error-message" role="alert">

View file

@ -68,15 +68,13 @@
linkWidgetName="MobileLocationLinkWidget" linkWidgetName="MobileLocationLinkWidget"
modalWidgetName="MobileLocationModalWidget" modalWidgetName="MobileLocationModalWidget"
:onZipUpdateCallback="reloadShopData" /> :onZipUpdateCallback="reloadShopData" />
<Transition name="fade" mode="out-in"> <shopQuestion
<shopQuestion ref="shopQuestion"
ref="shopQuestion" v-show="isShopQuestionDisplayed"
v-show="isShopQuestionDisplayed" v-model="selectedProvider"
v-model="selectedProvider" :selectedAppointmentType="selectedAppointmentType"
:selectedAppointmentType="selectedAppointmentType" :isDisplayed="isShopQuestionDisplayed"
:isDisplayed="isShopQuestionDisplayed" cmsWidgetName="ShopQuestionWidget" />
cmsWidgetName="ShopQuestionWidget" />
</Transition>
<contentGroupModal ref="RecalModal" cmsWidgetName="RecalModal" /> <contentGroupModal ref="RecalModal" cmsWidgetName="RecalModal" />
<funnel-footer <funnel-footer
cmsWidgetName="FunnelFooterWidget" cmsWidgetName="FunnelFooterWidget"
@ -108,7 +106,6 @@ import baseMixin from "@/mixins/base-mixin.js";
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper"; import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { settleAllPromises } from "@/helpers/layout-helper"; import { settleAllPromises } from "@/helpers/layout-helper";
import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
import { import {
getPricedMobileFeePart, getPricedMobileFeePart,
getServiceabilityDetails, getServiceabilityDetails,
@ -122,10 +119,10 @@ import { errorMessages } from "@/constants/error-messages";
// DEFINE VALIDATION RULES // DEFINE VALIDATION RULES
defineRule("mobile-location-required", (value) => { defineRule("mobile-location-required", (value) => {
if ( if (
value.addressQuestions.streetAddress == null || value.addressQuestions.streetAddress == "" ||
value.addressQuestions.city == null || value.addressQuestions.city == "" ||
value.addressQuestions.state == null || value.addressQuestions.state == "" ||
value.addressQuestions.zipCode == null || value.addressQuestions.zipCode == "" ||
value.isVehicleProtected == null value.isVehicleProtected == null
) { ) {
return errorMessages.MOBILE_LOCATION_REQUIRED; return errorMessages.MOBILE_LOCATION_REQUIRED;

View file

@ -6,8 +6,7 @@
linkType="text" linkType="text"
:text="serviceZipLinkText" :text="serviceZipLinkText"
href="#!" href="#!"
@click-event="openModal" @click-event="openModal" />
aria-label="Modal window" />
</div> </div>
</div> </div>
<modal <modal
@ -22,12 +21,12 @@
customInputId="serviceZipCode" customInputId="serviceZipCode"
v-model="internalModel.zipCode" v-model="internalModel.zipCode"
v-on="{ 'textboxQuestionEvent.inputIdAssigned': onInputIdAssigned }" v-on="{ 'textboxQuestionEvent.inputIdAssigned': onInputIdAssigned }"
:cmsWidgetName="textboxQuestionWidgetName" /> cmsWidgetName="ServiceZipQuestionWidget" />
<alert <alert
ref="alertInvalidZip" ref="alertInvalidZip"
v-if="displayInvalidZipAlert" v-if="displayInvalidZipAlert"
class="my-4" class="my-4"
:cmsWidgetName="alertInvalidZipWidgetName" cmsWidgetName="AlertInvalidZipWidget"
alertClass="alert-danger" alertClass="alert-danger"
v-bind:isDismissible="false" /> v-bind:isDismissible="false" />
</modal> </modal>
@ -73,15 +72,6 @@ export default {
type: Function, type: Function,
}, },
}, },
setup() {
const textboxQuestionWidgetName = "ServiceZipQuestionWidget";
const alertInvalidZipWidgetName = "AlertInvalidZipWidget";
return {
textboxQuestionWidgetName,
alertInvalidZipWidgetName,
};
},
computed: { computed: {
serviceZipLinkText() { serviceZipLinkText() {
if (this.modelValue.zipCode && this.modelValue.zipCode.length > 0) { if (this.modelValue.zipCode && this.modelValue.zipCode.length > 0) {
@ -90,7 +80,7 @@ export default {
return this.getCmsContent(this.linkWidgetName, "BodyText"); return this.getCmsContent(this.linkWidgetName, "BodyText");
}, },
modalHeaderText() { modalHeaderText() {
return this.getCmsContent(this.textboxQuestionWidgetName, "QuestionText"); return this.getCmsContent("ServiceZipQuestionWidget", "QuestionText");
}, },
modalFooterText() { modalFooterText() {
return this.getCmsContent(this.modalWidgetName, "FooterText"); return this.getCmsContent(this.modalWidgetName, "FooterText");
@ -98,6 +88,9 @@ export default {
modalName() { modalName() {
return this.modalWidgetName; return this.modalWidgetName;
}, },
modal() {
return this.$refs[this.modalName];
},
}, },
methods: { methods: {
resetAlerts() { resetAlerts() {
@ -117,13 +110,13 @@ export default {
}; };
}, },
openModal() { openModal() {
this.$refs[this.modalName].openModal(); this.modal.openModal();
}, },
closeModal() { closeModal() {
this.$refs[this.modalName].closeModal(); this.modal.closeModal();
}, },
resetModalButtonStyle() { resetModalButtonStyle() {
this.$refs[this.modalName].resetButtonStyle(); this.modal.resetButtonStyle();
}, },
onInputIdAssigned(inputId) { onInputIdAssigned(inputId) {
this.serviceZipCodeTextInputId = inputId; this.serviceZipCodeTextInputId = inputId;

View file

@ -13,7 +13,7 @@
<textboxQuestion <textboxQuestion
cmsWidgetName="VinNumberQuestionWidget" cmsWidgetName="VinNumberQuestionWidget"
v-model="vin" v-model="vin"
inputId="vin" customInputId="vin"
isRequired isRequired
validationRules="vin-required|vin-format" validationRules="vin-required|vin-format"
:isDisabled="vinPopulatedOnPageLoad" :isDisabled="vinPopulatedOnPageLoad"
@ -46,7 +46,7 @@
<textboxQuestion <textboxQuestion
cmsWidgetName="ServiceZipQuestionWidget" cmsWidgetName="ServiceZipQuestionWidget"
v-model="serviceZipCode" v-model="serviceZipCode"
inputId="serviceZipCode" customInputId="serviceZipCode"
mask="#####" mask="#####"
isRequired isRequired
validationRules="zip-required|zip-format" /> validationRules="zip-required|zip-format" />
@ -57,9 +57,9 @@
<textboxQuestion <textboxQuestion
cmsWidgetName="EmailAddressQuestionWidget" cmsWidgetName="EmailAddressQuestionWidget"
v-model="emailAddress" v-model="emailAddress"
inputId="emailAddress" customInputId="emailAddress"
isRequired isRequired
validationRules="email-address-required|email-address-format" /> validationRules="email-address-format" />
</div> </div>
</div> </div>
<div class="row mb-2"> <div class="row mb-2">
@ -152,7 +152,6 @@ import vinPagesMixin from "@/mixins/vin-pages-mixin";
// DEFINE VALIDATION RULES // DEFINE VALIDATION RULES
defineRule("zip-required", required(errorMessages.SERVICE_ZIP_REQUIRED)); defineRule("zip-required", required(errorMessages.SERVICE_ZIP_REQUIRED));
defineRule("zip-format", regex(/(^\d{5}$)|(^\d{5}-\d{4}$)/, errorMessages.SERVICE_ZIP_FORMAT)); defineRule("zip-format", regex(/(^\d{5}$)|(^\d{5}-\d{4}$)/, errorMessages.SERVICE_ZIP_FORMAT));
defineRule("email-address-required", required(errorMessages.EMAIL_ADDRESS_REQUIRED));
defineRule( defineRule(
"email-address-format", "email-address-format",
regex( regex(
@ -325,14 +324,9 @@ export default {
await this.dispatchStoreAction( await this.dispatchStoreAction(
storeActions.SAVE_SERVICE_ZIP_CODE_INFO, storeActions.SAVE_SERVICE_ZIP_CODE_INFO,
{ {
address: "",
address2: "",
city: "",
state: resultMap.zipCodeData.state, state: resultMap.zipCodeData.state,
zipCode: this.serviceZipCode, zipCode: this.serviceZipCode,
zipCodeCtu: resultMap.zipCodeData.zipCodeCtu, zipCodeCtu: resultMap.zipCodeData.zipCodeCtu,
appointmentType: "",
isVehicleProtected: null,
}, },
false false
); );
@ -367,14 +361,9 @@ export default {
await this.dispatchStoreAction( await this.dispatchStoreAction(
storeActions.SAVE_SERVICE_ZIP_CODE_INFO, storeActions.SAVE_SERVICE_ZIP_CODE_INFO,
{ {
address: null,
address2: null,
city: null,
state: zipCodeData.state, state: zipCodeData.state,
zipCode: this.serviceZipCode, zipCode: this.serviceZipCode,
zipCodeCtu: zipCodeData.zipCodeCtu, zipCodeCtu: zipCodeData.zipCodeCtu,
appointmentType: null,
isVehicleProtected: null,
}, },
false false
); );

View file

@ -156,6 +156,14 @@ const router = createRouter({
//---------------------------------------------------------- Router Functions ---------------------------------------------------------- //---------------------------------------------------------- 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) => { router.afterEach(async (to, from) => {
// Update lastPageVisited in the store // Update lastPageVisited in the store
store.commit(storeMutations.UPDATE_LAST_PAGE_VISITED, to.name); store.commit(storeMutations.UPDATE_LAST_PAGE_VISITED, to.name);

View file

@ -8,6 +8,7 @@ import { storeActions } from "@/constants/store-actions";
import { applicationConfig } from "@/constants/application-config"; import { applicationConfig } from "@/constants/application-config";
import { experimentTriggers } from "@/constants/experiments"; import { experimentTriggers } from "@/constants/experiments";
import { damageLocationsSelected } from "@/constants/damage-locations-selected"; import { damageLocationsSelected } from "@/constants/damage-locations-selected";
import { singleWindshieldCarIds } from "@/constants/single-windshield-carids";
import { fmgPageValues } from "@/router/router-constants/fmgPage-values"; import { fmgPageValues } from "@/router/router-constants/fmgPage-values";
import { deleteFunnelCookie } from "@/helpers/heritage-integration/cookie-helper.js"; import { deleteFunnelCookie } from "@/helpers/heritage-integration/cookie-helper.js";
import { deepEqual } from "@/helpers/object-helper"; import { deepEqual } from "@/helpers/object-helper";
@ -243,6 +244,11 @@ export const mutations = {
state.order.vehicle.registration.firstName = registrationInfo?.firstName; state.order.vehicle.registration.firstName = registrationInfo?.firstName;
state.order.vehicle.registration.lastName = registrationInfo?.lastName; 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) { updateServiceLocation(state, serviceLocationInfo) {
state.order.serviceLocation.address = serviceLocationInfo.address; state.order.serviceLocation.address = serviceLocationInfo.address;
state.order.serviceLocation.address2 = serviceLocationInfo.address2; state.order.serviceLocation.address2 = serviceLocationInfo.address2;
@ -642,13 +648,28 @@ export const actions = {
}, },
lookupVinByImage(context, image) { lookupVinByImage(context, image) {
const data = new FormData(); return new Promise((resolve, reject) => {
data.append("vinImage", image); let reader = new FileReader();
return globalMethods.callHttpClient({ reader.onload = (e) => {
method: endpoints.LookupVinByImage.method, resolve(reader.result);
endpoint: endpoints.LookupVinByImage.url, };
payload: data, reader.readAsDataURL(image);
isFormData: true, }).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: { customer: {
emailAddress: order.customer.emailAddress, emailAddress: order.customer.emailAddress || null,
}, },
damage: { damage: {
numberOfChips: damage.numberOfChips, numberOfChips: damage.numberOfChips,
@ -1889,7 +1910,7 @@ export const actions = {
context.commit(storeMutations.RESET_SERVICE_LOCATION_MOBILE_ADDRESS); 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) { saveServiceLocation(context, serviceLocationInfo) {
@ -1943,6 +1964,19 @@ export const actions = {
}, },
isVinOptionalVehicle(context) { 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()) { switch (context.state.order.vehicle.make.toLowerCase()) {
case "mercedes benz": case "mercedes benz":
case "volkswagen": case "volkswagen":
@ -1951,14 +1985,12 @@ export const actions = {
return true; return true;
default: default:
} }
if ( if (
context.state.order.vehicle.make.toLowerCase() === "ford" && context.state.order.vehicle.make.toLowerCase() === "ford" &&
context.state.order.vehicle.year >= 2018 context.state.order.vehicle.year >= 2018
) { ) {
return true; return true;
} }
if ( if (
context.state.order.vehicle.make.toLowerCase() === "bmw" && context.state.order.vehicle.make.toLowerCase() === "bmw" &&
context.state.order.vehicle.year <= 2017 context.state.order.vehicle.year <= 2017

View file

@ -406,14 +406,16 @@ describe("Actions", () => {
it("lookupVinByImage action, should return list of vins", async () => { it("lookupVinByImage action, should return list of vins", async () => {
// Arrange // Arrange
const context = state; const context = state;
const dummyImage = {}; const image = new File([], "test.jpg", {
type: "image/jpeg",
});
globalMethods.callHttpClient.mockImplementation(() => { globalMethods.callHttpClient.mockImplementation(() => {
return Promise.resolve({ data: ["1C6JJTAG3NL134044"] }); return Promise.resolve({ data: ["1C6JJTAG3NL134044"] });
}); });
// Act // Act
const response = await actions.lookupVinByImage(context, dummyImage); const response = await actions.lookupVinByImage(context, image);
// Assert // Assert
expect(response.data).toEqual(["1C6JJTAG3NL134044"]); expect(response.data).toEqual(["1C6JJTAG3NL134044"]);
@ -422,7 +424,9 @@ describe("Actions", () => {
it("lookupVinByImage action, should reject if error in calling API", async () => { it("lookupVinByImage action, should reject if error in calling API", async () => {
// Arrange // Arrange
const context = state; const context = state;
const dummyImage = {}; const image = new File([], "test.jpg", {
type: "image/jpeg",
});
globalMethods.callHttpClient.mockImplementation(() => { globalMethods.callHttpClient.mockImplementation(() => {
return Promise.reject("An error occurred"); return Promise.reject("An error occurred");
@ -431,9 +435,7 @@ describe("Actions", () => {
// Act // Act
// Assert // Assert
await expect(actions.lookupVinByImage(context, dummyImage)).rejects.toEqual( await expect(actions.lookupVinByImage(context, image)).rejects.toEqual("An error occurred");
"An error occurred"
);
}); });
it("getVehicleMakes action, should return makes list", async () => { 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); var vinOptionalResult = actions.isVinOptionalVehicle(context);
expect(vinOptionalResult).toEqual(expectedVinSkip); expect(vinOptionalResult).toEqual(expectedVinSkip);
} }

View file

@ -26,8 +26,7 @@
@click-event=" @click-event="
$emit('textLinkClicked', getRouterLinkRouteFromCopy(copy)) $emit('textLinkClicked', getRouterLinkRouteFromCopy(copy))
" "
:data-bs-target="'#' + getRouterLinkRouteFromCopy(copy)" :data-bs-target="'#' + getRouterLinkRouteFromCopy(copy)" />
aria-label="Modal window" />
</span> </span>
<span v-else v-html="copy"></span> <span v-else v-html="copy"></span>
</template> </template>