Merge branch 'develop' into feature/CASH-2032
This commit is contained in:
commit
5549b6befd
73 changed files with 2366 additions and 3549 deletions
|
|
@ -13,6 +13,8 @@ const environmentVariables = {
|
|||
['__VUE_APP_SAFELITE_HOP__']: "https://sv2-safelitehop-dev.safelite.com/fmgCheckoutShared.aspx",
|
||||
['__VUE_APP_SESSION_TIMEOUT_MINUTES__']: 30,
|
||||
['__VUE_APP_SOLARWINDS_MONITORING_SCRIPT__']: "",
|
||||
['__VUE_APP_ADYEN_ENVIRONMENT__']: "test",
|
||||
['__VUE_APP_ADYEN_CLIENT_KEY__']: "test_WYGT4F5ZT5BRRL5MPWHK6QLYOIXZXROD",
|
||||
};
|
||||
|
||||
const keysOnly = Object.keys(environmentVariables);
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ module.exports = {
|
|||
},
|
||||
transformIgnorePatterns: ["'/node_modules/(?!vee-validate)"],
|
||||
moduleFileExtensions: ["js", "vue"],
|
||||
modulePathIgnorePatterns: ["vin-lookup"],
|
||||
collectCoverageFrom: [
|
||||
"src/**/*.{js,vue}",
|
||||
"!src/main.js",
|
||||
|
|
@ -33,11 +32,7 @@ module.exports = {
|
|||
"!src/layouts/payment-pia-return/*.vue", // Temp test exclusion while in development
|
||||
"!src/layouts/insurance/*.vue", // Temp test exclusion while in development
|
||||
"!src/layouts/insurance-company/*.vue", // Temp test exclusion while in development
|
||||
"!src/layouts/schedule/**/*.vue", // Temp test exclusion while in development
|
||||
"!src/digital-components/date-picker/**/*.vue", // Temp test exclusion while in development
|
||||
|
||||
"!src/**/*-june-2025.vue", // Exclude these temporary files for CASH-845 project
|
||||
|
||||
"!src/layouts/insurance-company/insurance-company-question/*.vue", // Temp test exclusion while in development
|
||||
"!src/experiment-components/*.vue",
|
||||
// END
|
||||
|
|
@ -45,7 +40,7 @@ module.exports = {
|
|||
testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
statements: 64,
|
||||
statements: 68,
|
||||
},
|
||||
},
|
||||
// Uncomment this to avoid the massive amount of warnings we are getting for onSubmit and onInvalidSubmit
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en-US">
|
||||
<head>
|
||||
<meta name="robots" content="noindex" />
|
||||
<script>
|
||||
window.dataLayer = [{}];
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -545,7 +545,6 @@
|
|||
{ path: '/parts/parts-or-questions', method: 'POST', body: {"carId":"CR00046193","glassPieces":[{"location":"Windshield","name":"Single"}],"zip":"43015","vin":null,"serviceType":null,"referralSeqNumber":environmentContext.referralNumber,"AppName":"FixMyGlass"} },
|
||||
{ path: '/parts/wipers/CR00046193/43015', method: 'GET' },
|
||||
{ path: '/parts/mobile-premium-fee/Cash/Replace', method: 'GET' },
|
||||
{ path: '/parts/service-package-discount', method: 'POST', body: {"glassPieces":[{"location":"Windshield","name":"Single"}],"damageType":"Replace","AppName":"FixMyGlass"} },
|
||||
{ path: '/parts/mobile-fee/?serviceType=Install&facilityType=Mobile&parentAccountNumber=167132&billToAccountNumber=87291&providerNumber=03357&isItacOptimized=false&zipCode=43015', method: 'GET' },
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ const GaCategories = {
|
|||
EVOX: "Evox",
|
||||
APPOINTMENT: "Appointment",
|
||||
SERVICE_LOCATION: "service-location",
|
||||
CONFIRMATION_CLICKED: "confirmation clicked",
|
||||
RECAL_DISCLAIMER: "Recalibration disclaimer",
|
||||
};
|
||||
|
||||
const GaActions = {
|
||||
|
|
@ -26,6 +28,9 @@ const GaActions = {
|
|||
MOBILE_AVAILABLE: "mobile_available",
|
||||
SHOPS_FIRST_DISPLAYED: "shops_first_displayed",
|
||||
MORE_LOCATIONS_CLICKED: "more_locations_clicked",
|
||||
YES: "yes",
|
||||
NO: "no",
|
||||
ACKNOWLEDGED: "Acknowledged",
|
||||
};
|
||||
|
||||
const GaLabels = {
|
||||
|
|
|
|||
|
|
@ -32,6 +32,8 @@ const applicationConfig = {
|
|||
COOKIE_NAME_LENGTH_MAX_LIMIT: 44,
|
||||
SESSION_TIMEOUT_MINUTES: process.env.__VUE_APP_SESSION_TIMEOUT_MINUTES__,
|
||||
RETURN_USER_PAGE: "/fmg/return-user",
|
||||
ADYEN_CLIENT_KEY: process.env.__VUE_APP_ADYEN_CLIENT_KEY__,
|
||||
ADYEN_ENVIRONMENT: process.env.__VUE_APP_ADYEN_ENVIRONMENT__,
|
||||
};
|
||||
|
||||
export { applicationConfig };
|
||||
|
|
|
|||
|
|
@ -76,10 +76,6 @@ const endpoints = {
|
|||
url: "/parts/api/v1/parts/pricing-by-day-part-number",
|
||||
method: "GET",
|
||||
},
|
||||
GetServicePackageDiscountPart: {
|
||||
url: "/parts/api/v1/parts/service-package-discount",
|
||||
method: "POST",
|
||||
},
|
||||
GetServiceabilityDetails: {
|
||||
url: "/location/api/v1/location/serviceability-details",
|
||||
method: "GET",
|
||||
|
|
@ -162,7 +158,7 @@ const endpoints = {
|
|||
},
|
||||
TaxOrderItems: {
|
||||
url: "/price/api/v1/price/taxed-order-items",
|
||||
method: "GET",
|
||||
method: "POST",
|
||||
},
|
||||
LogExperimentExposureIfAssigned: {
|
||||
url: "/experiments/api/v1/experiments/log-exposure",
|
||||
|
|
@ -216,6 +212,10 @@ const endpoints = {
|
|||
url: "/order/api/v1/order/add-donation-part",
|
||||
method: "POST",
|
||||
},
|
||||
GetOemAfterMarketParts: {
|
||||
url: "/parts/api/v1/parts/oem-after-market-parts",
|
||||
method: "POST",
|
||||
},
|
||||
InitializeAdyenPayment: {
|
||||
url: "/payment/api/v1/payment/payment/session/initialize-session",
|
||||
method: "POST",
|
||||
|
|
|
|||
|
|
@ -12,6 +12,9 @@ const experimentSettings = {
|
|||
SUPPRESS_VIN_CAPTURE: "SuppressVinCapture",
|
||||
DISPLAY_AVAILABILITY_INDICATORS: "DisplayAvailabilityIndicators",
|
||||
PIA_EXPERIENCE: "PIA Experience",
|
||||
CONDITIONAL_PROVIDER_NUMBERS: "ConditionalProviderNumbers",
|
||||
CONDITIONAL_PROVIDER_NUMBERS_2: "ConditionalProviderNumbers2",
|
||||
CONDITIONAL_PROVIDER_SHUTOFF_DATE: "ConditionalProviderShutoffDate",
|
||||
PIA_INSURANCE: "DisplayPIAInsurance",
|
||||
SUBMIT_ORDER_ENABLE_PIA: "SubmitOrder_Enable_PIA",
|
||||
IS_EMAIL_OPTIONAL: "isEmailOptional",
|
||||
|
|
@ -33,6 +36,10 @@ const experimentSettings = {
|
|||
SHOW_PM_MOBILE_DAYS: "Show_PMmobileDays",
|
||||
SHOW_NO_PM_MOBILE_DAYS: "Show_NoPMmobileDays",
|
||||
SHOW_NO_MOBILE_AVAILABLE_DAYS: "Show_NoMobileAvailableDays",
|
||||
SHOW_MOBILE_FIRST_POPUP_REPAIR: "Show_Mobile_First_Popup_Repair",
|
||||
SHOW_MOBILE_FIRST_POPUP_REPLACE_WITHOUT_MSR: "Show_Mobile_First_Popup_Replace_Without_MSR",
|
||||
SHOW_MOBILE_FIRST_POPUP_REPLACE_WITH_MSR: "Show_Mobile_First_Popup_Replace_With_MSR",
|
||||
OFFER_OEM: "OfferOem",
|
||||
USE_ADYEN_PAYMENT: "UseAdyenPayment",
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ const partTypeStrings = {
|
|||
SERVICE_PACKAGE_DISCOUNT: "SERVICE PACKAGE DISCOUNT",
|
||||
QUOTE_PAGE_DISCOUNT: "QUOTE PAGE DISCOUNT",
|
||||
DONATION: "DONATION",
|
||||
WINDSHIELD: "WINDSHIELD",
|
||||
};
|
||||
|
||||
export { partTypeStrings };
|
||||
|
|
|
|||
|
|
@ -48,6 +48,10 @@ export const pageProgressMapper = {
|
|||
percent: 30,
|
||||
step: 1,
|
||||
},
|
||||
"policy-info": {
|
||||
percent: 55,
|
||||
step: 2,
|
||||
},
|
||||
quote: {
|
||||
percent: 60,
|
||||
step: 2,
|
||||
|
|
@ -80,6 +84,10 @@ export const pageProgressMapper = {
|
|||
percent: 98.5,
|
||||
step: 4,
|
||||
},
|
||||
"payment-adyen": {
|
||||
percent: 98.5,
|
||||
step: 4,
|
||||
},
|
||||
confirmation: {
|
||||
percent: 100,
|
||||
step: 4,
|
||||
|
|
|
|||
|
|
@ -77,4 +77,29 @@ export const quotePageDiscountTable = [
|
|||
description: "PREM RCL GEOTARGET DISC",
|
||||
packageLevel: packageNames.TIER_THREE,
|
||||
},
|
||||
// Replace - Curve Tempered
|
||||
{
|
||||
name: "Curve Tempered Glass Only",
|
||||
experimentCode: "Show_GlassOnly_Replace_CT_Discount",
|
||||
partNumber: "GL CT CSH PKG",
|
||||
partType: partTypeStrings.QUOTE_PAGE_DISCOUNT,
|
||||
description: "GLASS CT GEOTARGET DISC",
|
||||
packageLevel: packageNames.TIER_ONE,
|
||||
},
|
||||
{
|
||||
name: "Curve Tempered Standard",
|
||||
experimentCode: "Show_Standard_Replace_CT_Discount",
|
||||
partNumber: "STD CT CSH PKG",
|
||||
partType: partTypeStrings.QUOTE_PAGE_DISCOUNT,
|
||||
description: "STD CT GEOTARGET DISC",
|
||||
packageLevel: packageNames.TIER_TWO,
|
||||
},
|
||||
{
|
||||
name: "Curve Tempered Premium",
|
||||
experimentCode: "Show_Premium_Replace_CT_Discount",
|
||||
partNumber: "PRM CT CSH PKG",
|
||||
partType: partTypeStrings.QUOTE_PAGE_DISCOUNT,
|
||||
description: "PREM CT GEOTARGET DISC",
|
||||
packageLevel: packageNames.TIER_THREE,
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ const PREMIUM_FEE_PART_TYPE = "EARLY BIRD";
|
|||
|
||||
const PRICING_BY_DAY_PART_TYPE = "PREMAPPT GEN";
|
||||
|
||||
const RECAL_ACK_YES = "Yes";
|
||||
|
||||
const RouteCodeFlags = {
|
||||
ALL_DAY_DROP_OFF: "ALL DAY DROP OFF",
|
||||
OVERNIGHT_DROP_OFF: "OVERNIGHT DROP OFF",
|
||||
|
|
@ -31,4 +33,5 @@ export {
|
|||
PRICING_BY_DAY_PART_TYPE,
|
||||
RouteCodeFlags,
|
||||
cmsWidgetFieldMappings,
|
||||
RECAL_ACK_YES,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ const storeActions = {
|
|||
GET_ALERT_REASONS_BY_CTU: "getAlertReasonsByCtu",
|
||||
GET_PARTS_OR_QUESTIONS: "getPartsOrQuestions",
|
||||
GET_PARTS: "getParts",
|
||||
GET_OEM_AFTERMARKET_PARTS: "getOemAfterMarketParts",
|
||||
GET_WIPERS: "getWipers",
|
||||
GET_RAIN_REPEL: "getRainRepel",
|
||||
GET_SUPPORTING_ITEMS: "getSupportingItems",
|
||||
|
|
@ -32,7 +33,6 @@ const storeActions = {
|
|||
GET_MOLDING_QUESTIONS: "getMoldingQuestions",
|
||||
GET_MOBILE_FEE_PART: "getMobileFeePart",
|
||||
GET_PRICING_BY_DAY_PART: "getPricingByDayPart",
|
||||
GET_SERVICE_PACKAGE_DISCOUNT_PART: "getServicePackageDiscountPart",
|
||||
GET_SERVICEABILITY_DETAILS: "getServiceabilityDetails",
|
||||
GET_SHOP_TIME_SLOTS: "getShopTimeSlots",
|
||||
GET_MOBILE_TIME_SLOTS: "getMobileTimeSlots",
|
||||
|
|
@ -111,6 +111,8 @@ const storeActions = {
|
|||
SAVE_PAYPAL_TOKEN: "savePaypalToken",
|
||||
SAVE_NEXTGEN_SETTLED_AMOUNT: "saveNextGenSettledAmount",
|
||||
SAVE_IS_RECAL_ACK_OPT_IN: "saveIsRecalAckOptIn",
|
||||
SAVE_IS_RECAL_ACKNOWLEDGED_FOR_SCHEDULING: "saveIsRecalAcknowledgedForScheduling",
|
||||
SAVE_IS_OEM_GLASS_SELECTED: "saveIsOemGlassSelected",
|
||||
SAVE_IS_MSR_FEE_APPLICABLE: "saveIsMSRFeeApplicable",
|
||||
|
||||
CREATE_SUBMITTED_STATE: "createSubmittedState",
|
||||
|
|
|
|||
|
|
@ -69,6 +69,8 @@ const storeMutations = {
|
|||
LOCK_TOKEN: "updateLockToken",
|
||||
UPDATE_SETTLED_TENDER_AMOUNT: "updateSettledTenderAmount",
|
||||
UPDATE_IS_RECAL_ACK_OPT_IN: "updateIsRecalAckOptIn",
|
||||
UPDATE_IS_RECAL_ACKNOWLEDGED_FOR_SCHEDULING: "updateIsRecalAcknowledgedForScheduling",
|
||||
UPDATE_IS_OEM_GLASS_SELECTED: "updateIsOemGlassSelected",
|
||||
UPDATE_IS_MSR_FEE_APPLICABLE: "updateIsMSRFeeApplicable",
|
||||
UPDATE_CASH_PRICE_SUBTOTAL: "updateCashPriceSubTotal",
|
||||
|
||||
|
|
@ -117,6 +119,7 @@ const storeMutations = {
|
|||
UPDATE_EXTERNAL_PARAMETER_SERVICE_PACKAGE: "updateExternalParameterServicePackage",
|
||||
UPDATE_EXTERNAL_PARAMETER_SOURCE: "updateExternalParameterSource",
|
||||
UPDATE_EXTERNAL_PARAMETER_PHONE_NUMBER: "updateExternalParameterPhoneNumber",
|
||||
UPDATE_EXTERNAL_PARAMETER_QS_STASH: "updateExternalParameterQsStash",
|
||||
|
||||
//RESET EXTERNAL_PARAMETER MUTATIONS
|
||||
RESET_EXTERNAL_PARAMETER_VEHICLE_STATE: "resetExternalParameterVehicleState",
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -1,17 +1,18 @@
|
|||
<template>
|
||||
<div
|
||||
class="date-picker text-center"
|
||||
:class="`${calendarViewDirection} calendar-2025 ${showPricingByDayClass}`">
|
||||
:class="`${calendarViewDirection} calendar-2025 ${showPricingByDayClass} ${loadingStatus === 'reinitial' ? 'loading-reinitial' : ''}`">
|
||||
<fieldset id="date-picker-fieldset" ref="datePickerFieldset">
|
||||
<legend class="sr-only">Select a day and time</legend>
|
||||
<div
|
||||
v-for="month in months"
|
||||
v-for="(month, i) in months"
|
||||
:key="`${month.monthLabel}-${month.yearNum?.toString()}`"
|
||||
:id="`${month.monthLabel}-${month.yearNum?.toString()}`"
|
||||
class="calendar-grid-container"
|
||||
class="calendar-grid-container month"
|
||||
:class="[
|
||||
hideSomeDaysForInitialView ? 'partial-month-initial-view' : '',
|
||||
month.monthClass,
|
||||
i === 0 ? 'first-month-shown' : '',
|
||||
]">
|
||||
<div class="month-year body-small d-flex align-items-center">
|
||||
{{ month.monthLabel }} {{ month.yearNum?.toString() }}
|
||||
|
|
@ -114,7 +115,8 @@
|
|||
</div>
|
||||
</div>
|
||||
<loader
|
||||
:class="[!isLoading ? 'date-picker-hidden' : '']"
|
||||
class="date-picker-loader"
|
||||
:class="[loadingStatus !== 'none' ? 'show-loader' : '']"
|
||||
loaderColor="blue"
|
||||
loaderPosition="center" />
|
||||
<div class="row form-test-error" id="date-of-month-error">
|
||||
|
|
@ -125,7 +127,7 @@
|
|||
type="button"
|
||||
class="btn btn-link view-more-dates"
|
||||
id="viewMoreDates"
|
||||
:disabled="isLoading"
|
||||
:disabled="loadingStatus !== 'none'"
|
||||
@click="showAnotherMonth">
|
||||
View more dates
|
||||
</button>
|
||||
|
|
@ -160,7 +162,6 @@ export default {
|
|||
name: "datePicker",
|
||||
data() {
|
||||
return {
|
||||
isLoading: true,
|
||||
months: null,
|
||||
disableViewMoreDatesButton: false,
|
||||
hideSomeDaysForInitialView: null,
|
||||
|
|
@ -169,7 +170,10 @@ export default {
|
|||
};
|
||||
},
|
||||
props: {
|
||||
customComponentId: String,
|
||||
customComponentId: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
selectableDatesSetting: {
|
||||
type: String,
|
||||
validator(value) {
|
||||
|
|
@ -200,6 +204,10 @@ export default {
|
|||
pricingByDayUpcharge: Number,
|
||||
isPricingByDayExperiment: Boolean,
|
||||
isMobileSelected: Boolean,
|
||||
loadingStatus: {
|
||||
type: String,
|
||||
default: "none",
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const uuid = uuidv4();
|
||||
|
|
@ -281,7 +289,7 @@ export default {
|
|||
if (event.screenX === 0 && event.screenY === 0) {
|
||||
return;
|
||||
}
|
||||
this.$emit("date-clicked", date);
|
||||
this.$emit("date-selected", date);
|
||||
},
|
||||
getWeekStartDate(dateString) {
|
||||
const date = convertDateStringToDate(dateString);
|
||||
|
|
@ -324,9 +332,9 @@ export default {
|
|||
let preSelectedDateMonthEnd = this.getMonthEnd(
|
||||
preSelectedDate.replace("-mobile", "")
|
||||
);
|
||||
let weekIncludesPreSelectedMonthEnd = false;
|
||||
let monthEndsByThisWeek = false;
|
||||
let i = 0;
|
||||
while (!weekIncludesPreSelectedMonthEnd) {
|
||||
while (!monthEndsByThisWeek && i < 52) {
|
||||
if (i > 0) {
|
||||
weekStartDate = this.getNextWeekSunday(weekEndDate);
|
||||
weekEndDate = this.getWeekEndDate(weekStartDate);
|
||||
|
|
@ -338,7 +346,12 @@ export default {
|
|||
preSelectedDateMonthEnd === weekEndDate
|
||||
) {
|
||||
weekEndDate = preSelectedDateMonthEnd;
|
||||
weekIncludesPreSelectedMonthEnd = true;
|
||||
monthEndsByThisWeek = true;
|
||||
} else if (preSelectedDateMonthEnd < weekEndDate) {
|
||||
// Additional case: if month ends before this week (but not necessarily during it),
|
||||
// still cut the loop here, but don't truncate the week.
|
||||
|
||||
monthEndsByThisWeek = true;
|
||||
}
|
||||
}
|
||||
weeks.push({
|
||||
|
|
@ -466,7 +479,7 @@ export default {
|
|||
const response = config.getSelectableDatesCallback({
|
||||
startDateString: initialViewStartDate,
|
||||
endDateString: initialViewEndDate,
|
||||
providerNumber: config.providerNumber,
|
||||
inshopProviderNumber: config.inshopProviderNumber,
|
||||
zipCode: config.zipCode,
|
||||
includeMobileTimeSlots: config.includeMobileTimeSlots,
|
||||
includeInshopTimeSlots: config.includeInshopTimeSlots,
|
||||
|
|
@ -489,7 +502,6 @@ export default {
|
|||
});
|
||||
},
|
||||
async setCalendarData(config = {}) {
|
||||
this.isLoading = true;
|
||||
this.selectableDatesInshop = [];
|
||||
this.selectableDatesMobile = [];
|
||||
this.months = [];
|
||||
|
|
@ -541,7 +553,6 @@ export default {
|
|||
// }
|
||||
}
|
||||
this.months = months;
|
||||
this.isLoading = false;
|
||||
|
||||
if (config.preSelectedDate) {
|
||||
this.$nextTick(() => {
|
||||
|
|
@ -729,7 +740,6 @@ export default {
|
|||
},
|
||||
async showAnotherMonth() {
|
||||
if (!this.months) return;
|
||||
this.isLoading = true;
|
||||
let monthToShow;
|
||||
let monthStartDate;
|
||||
|
||||
|
|
@ -774,7 +784,6 @@ export default {
|
|||
monthToShowDates[monthToShowDates.length - 1].inputValue
|
||||
);
|
||||
|
||||
this.isLoading = false;
|
||||
this.hideSomeDaysForInitialView = false; // if hid days on initial partial view, this will reveal those days
|
||||
monthToShow.monthClass = monthToShow.monthClass.replace(" month-hidden", "");
|
||||
this.scrollToElement(monthToShow.monthString);
|
||||
|
|
@ -787,7 +796,6 @@ export default {
|
|||
},
|
||||
async updateSelectableDates(monthStart, monthEnd) {
|
||||
const moreSelectableDates = await this.getMoreDatesCallback(monthStart, monthEnd);
|
||||
|
||||
moreSelectableDates.inshopTimeSlotsData.days.forEach((selectableDate) => {
|
||||
const index = this.selectableDatesInshop.findIndex(
|
||||
(dateObj) => dateObj.date === selectableDate.date
|
||||
|
|
@ -846,12 +854,6 @@ export default {
|
|||
},
|
||||
},
|
||||
watch: {
|
||||
isLoading(newValue) {
|
||||
// if done loading dates, then set to first available date
|
||||
if (newValue === false && this.firstAvailableSelectableDate) {
|
||||
this.selectedDate = this.firstAvailableSelectableDate;
|
||||
}
|
||||
},
|
||||
modelValue(newValue) {
|
||||
this.resetField({
|
||||
value: newValue,
|
||||
|
|
@ -872,9 +874,14 @@ export default {
|
|||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.date-picker-hidden {
|
||||
.date-picker-loader {
|
||||
opacity: 0;
|
||||
max-height: 0;
|
||||
|
||||
&.show-loader {
|
||||
opacity: 1;
|
||||
max-height: none;
|
||||
}
|
||||
}
|
||||
.date-picker {
|
||||
position: relative;
|
||||
|
|
@ -903,12 +910,12 @@ export default {
|
|||
height: 1.5rem;
|
||||
}
|
||||
}
|
||||
.calendar-grid-container {
|
||||
.month {
|
||||
margin: 0 auto 1rem auto;
|
||||
position: relative;
|
||||
transition:
|
||||
height ease 2s,
|
||||
opacity ease 2s;
|
||||
height ease 1s,
|
||||
max-height ease 1s;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
justify-content: center;
|
||||
|
|
@ -1157,7 +1164,7 @@ export default {
|
|||
}
|
||||
|
||||
.past {
|
||||
.calendar-grid-container {
|
||||
.month {
|
||||
.month-year {
|
||||
grid-area: 1 / 1 / 2 / 6;
|
||||
}
|
||||
|
|
@ -1214,7 +1221,7 @@ export default {
|
|||
}
|
||||
// new calendar styling overrides
|
||||
&.calendar-2025 {
|
||||
.calendar-grid-container .grid-item {
|
||||
.month .grid-item {
|
||||
margin: 0 0 0.15rem 0;
|
||||
}
|
||||
.month-year {
|
||||
|
|
@ -1316,6 +1323,33 @@ export default {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.loading-reinitial {
|
||||
.month {
|
||||
opacity: 0;
|
||||
max-height: 0;
|
||||
}
|
||||
.month.first-month-shown {
|
||||
opacity: 1;
|
||||
max-height: none;
|
||||
|
||||
.grid-item.radio-wrapper.selectable-day {
|
||||
input[type="radio"]:checked + label,
|
||||
label {
|
||||
background-color: $white;
|
||||
border: none;
|
||||
color: initial;
|
||||
cursor: default;
|
||||
&:hover {
|
||||
border: none;
|
||||
}
|
||||
span {
|
||||
font-family: UrbanistRegular;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
:deep(.timeslots .button-inner-wrapper) {
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -160,6 +160,10 @@ export default {
|
|||
return this.staticBackdrop ? "static" : "true";
|
||||
},
|
||||
},
|
||||
unmounted() {
|
||||
// remove any modal effects before leaving page (e.g. user hits browser back button)
|
||||
this.closeModal();
|
||||
},
|
||||
components: {
|
||||
modalButtonMain,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,4 +1,9 @@
|
|||
/* eslint-disable */
|
||||
// ***************************************************************
|
||||
// This file wraps the Salesforce generated code.
|
||||
// See the comment below for where you need to paste in the Salesforce generated code.
|
||||
// You may also need to modify the script tag src located in salesforce-webchat.vue to point to the correct Salesforce instance.
|
||||
// ***************************************************************
|
||||
|
||||
function setupCustomSettings(
|
||||
modifyEmbeddedSvcCallback,
|
||||
|
|
@ -7,90 +12,38 @@ function setupCustomSettings(
|
|||
onWebchatCloseCallback
|
||||
) {
|
||||
modifyEmbeddedSvcCallback();
|
||||
embedded_svc.addEventHandler("onSettingsCallCompleted", finishedLoadingCallback);
|
||||
embedded_svc.addEventHandler("afterMaximize", onWebchatOpenCallback);
|
||||
embedded_svc.addEventHandler("afterDestroy", onWebchatCloseCallback);
|
||||
window.addEventListener("onEmbeddedMessagingButtonCreated", finishedLoadingCallback);
|
||||
window.addEventListener("onEmbeddedMessagingWindowMaximized", onWebchatOpenCallback);
|
||||
window.addEventListener("onEmbeddedMessagingWindowClosed", onWebchatCloseCallback);
|
||||
}
|
||||
|
||||
/*
|
||||
The code below is generated from salesforce but modified in the following ways:
|
||||
* Only the javascript inside the second <script> tag in the provided salesforce code is copy pasted
|
||||
* CSS is not copied in
|
||||
* First <script> tag loading in the file from the CDN is not copied in
|
||||
* Copy the entire contents of the second <script> tag inside the initializeSalesforceWebchatForDev
|
||||
method and add a call to setupCustomSettings before the 'embedded_svc.init()' method is called
|
||||
|
||||
Actual call to paste in:
|
||||
setupCustomSettings(
|
||||
modifyEmbeddedSvcCallback,
|
||||
finishedLoadingCallback,
|
||||
onWebchatOpenCallback,
|
||||
onWebchatCloseCallback
|
||||
);
|
||||
*/
|
||||
|
||||
export function initializeSalesforceWebchatForDev(
|
||||
modifyEmbeddedSvcCallback,
|
||||
finishedLoadingCallback,
|
||||
onWebchatOpenCallback,
|
||||
onWebchatCloseCallback
|
||||
) {
|
||||
var initESW = function (gslbBaseURL) {
|
||||
embedded_svc.settings.displayHelpButton = true; //Or false
|
||||
embedded_svc.settings.language = ""; //For example, enter 'en' or 'en-US'
|
||||
//embedded_svc.settings.defaultMinimizedText = '...'; //(Defaults to Chat with an Expert)
|
||||
//embedded_svc.settings.disabledMinimizedText = '...'; //(Defaults to Agent Offline)
|
||||
//embedded_svc.settings.loadingText = ''; //(Defaults to Loading)
|
||||
//embedded_svc.settings.storageDomain = 'yourdomain.com'; //(Sets the domain for your deployment so that visitors can navigate subdomains during a chat session)
|
||||
// Settings for Chat
|
||||
//embedded_svc.settings.directToButtonRouting = function(prechatFormData) {
|
||||
// Dynamically changes the button ID based on what the visitor enters in the pre-chat form.
|
||||
// Returns a valid button ID.
|
||||
//};
|
||||
//embedded_svc.settings.prepopulatedPrechatFields = {}; //Sets the auto-population of pre-chat form fields
|
||||
//embedded_svc.settings.fallbackRouting = []; //An array of button IDs, user IDs, or userId_buttonId
|
||||
//embedded_svc.settings.offlineSupportMinimizedText = '...'; //(Defaults to Contact Us)
|
||||
setupCustomSettings(
|
||||
modifyEmbeddedSvcCallback,
|
||||
finishedLoadingCallback,
|
||||
onWebchatOpenCallback,
|
||||
onWebchatCloseCallback
|
||||
);
|
||||
// START OF SALESFORCE GENERATED CODE
|
||||
// Copy in the contents of the first <script> tag inside the initEmbeddedMessaging() method
|
||||
try {
|
||||
embeddedservice_bootstrap.settings.language = "en_US"; // For example, enter 'en' or 'en-US'
|
||||
|
||||
embedded_svc.settings.enabledFeatures = ["LiveAgent"];
|
||||
embedded_svc.settings.entryFeature = "LiveAgent";
|
||||
|
||||
setupCustomSettings(
|
||||
modifyEmbeddedSvcCallback,
|
||||
finishedLoadingCallback,
|
||||
onWebchatOpenCallback,
|
||||
onWebchatCloseCallback
|
||||
);
|
||||
|
||||
embedded_svc.init(
|
||||
"https://safelite2--dev.sandbox.my.salesforce.com",
|
||||
"https://safelite2--dev.sandbox.my.salesforce-sites.com/chat",
|
||||
gslbBaseURL,
|
||||
embeddedservice_bootstrap.init(
|
||||
"00D6s0000008r6A",
|
||||
"Sales_Central_Snap_in",
|
||||
"Sales_Central_TFN",
|
||||
"https://safelite2--dev.sandbox.my.site.com/ESWSalesCentralTFN1755660094477",
|
||||
{
|
||||
baseLiveAgentContentURL:
|
||||
"https://c.la12s-core1.sfdc-8tgtt5.salesforceliveagent.com/content",
|
||||
deploymentId: "572Wr0000017xKX",
|
||||
buttonId: "573Wr000000CpgH",
|
||||
baseLiveAgentURL: "https://d.la12s-core1.sfdc-8tgtt5.salesforceliveagent.com/chat",
|
||||
eswLiveAgentDevName:
|
||||
"EmbeddedServiceLiveAgent_Parent04IWr000003yKq9MAE_1961681659a",
|
||||
isOfflineSupportEnabled: false,
|
||||
scrt2URL: "https://safelite2--dev.sandbox.my.salesforce-scrt.com",
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
if (!window.embedded_svc) {
|
||||
var s = document.createElement("script");
|
||||
s.setAttribute(
|
||||
"src",
|
||||
"https://safelite2--dev.sandbox.my.salesforce.com/embeddedservice/5.0/esw.min.js"
|
||||
);
|
||||
s.onload = function () {
|
||||
initESW(null);
|
||||
};
|
||||
document.body.appendChild(s);
|
||||
} else {
|
||||
initESW("https://service.force.com");
|
||||
} catch (err) {
|
||||
console.error("Error loading Embedded Messaging: ", err);
|
||||
}
|
||||
// END OF SALESFORCE GENERATED CODE
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,9 @@
|
|||
/* eslint-disable */
|
||||
// ***************************************************************
|
||||
// This file wraps the Salesforce generated code.
|
||||
// See the comment below for where you need to paste in the Salesforce generated code.
|
||||
// You may also need to modify the script tag src located in salesforce-webchat.vue to point to the correct Salesforce instance.
|
||||
// ***************************************************************
|
||||
|
||||
function setupCustomSettings(
|
||||
modifyEmbeddedSvcCallback,
|
||||
|
|
@ -7,91 +12,39 @@ function setupCustomSettings(
|
|||
onWebchatCloseCallback
|
||||
) {
|
||||
modifyEmbeddedSvcCallback();
|
||||
embedded_svc.addEventHandler("onSettingsCallCompleted", finishedLoadingCallback);
|
||||
embedded_svc.addEventHandler("afterMaximize", onWebchatOpenCallback);
|
||||
embedded_svc.addEventHandler("afterDestroy", onWebchatCloseCallback);
|
||||
window.addEventListener("onEmbeddedMessagingButtonCreated", finishedLoadingCallback);
|
||||
window.addEventListener("onEmbeddedMessagingWindowMaximized", onWebchatOpenCallback);
|
||||
window.addEventListener("onEmbeddedMessagingWindowClosed", onWebchatCloseCallback);
|
||||
}
|
||||
|
||||
/*
|
||||
The code below is generated from salesforce but modified in the following ways:
|
||||
* Only the javascript inside the second <script> tag in the provided salesforce code is copy pasted
|
||||
* CSS is not copied in
|
||||
* First <script> tag loading in the file from the CDN is not copied in
|
||||
* Copy the entire contents of the second <script> tag inside the initializeSalesforceWebchatForDev
|
||||
method and add a call to setupCustomSettings before the 'embedded_svc.init()' method is called
|
||||
|
||||
Actual call to paste in:
|
||||
setupCustomSettings(
|
||||
modifyEmbeddedSvcCallback,
|
||||
finishedLoadingCallback,
|
||||
onWebchatOpenCallback,
|
||||
onWebchatCloseCallback
|
||||
);
|
||||
*/
|
||||
|
||||
export function initializeSalesforceWebchatForProd(
|
||||
modifyEmbeddedSvcCallback,
|
||||
finishedLoadingCallback,
|
||||
onWebchatOpenCallback,
|
||||
onWebchatCloseCallback
|
||||
) {
|
||||
var initESW = function (gslbBaseURL) {
|
||||
let embedded_svc = window.embedded_svc;
|
||||
embedded_svc.settings.displayHelpButton = true; //Or false
|
||||
embedded_svc.settings.language = ""; //For example, enter 'en' or 'en-US'
|
||||
setupCustomSettings(
|
||||
modifyEmbeddedSvcCallback,
|
||||
finishedLoadingCallback,
|
||||
onWebchatOpenCallback,
|
||||
onWebchatCloseCallback
|
||||
);
|
||||
|
||||
//embedded_svc.settings.defaultMinimizedText = '...'; //(Defaults to Chat with an Expert)
|
||||
//embedded_svc.settings.disabledMinimizedText = '...'; //(Defaults to Agent Offline)
|
||||
// START OF SALESFORCE GENERATED CODE
|
||||
// Copy in the contents of the first <script> tag inside the initEmbeddedMessaging() method
|
||||
try {
|
||||
embeddedservice_bootstrap.settings.language = "en_US"; // For example, enter 'en' or 'en-US'
|
||||
|
||||
//embedded_svc.settings.loadingText = ''; //(Defaults to Loading)
|
||||
//embedded_svc.settings.storageDomain = 'yourdomain.com'; //(Sets the domain for your deployment so that visitors can navigate subdomains during a chat session)
|
||||
|
||||
// Settings for Chat
|
||||
//embedded_svc.settings.directToButtonRouting = function(prechatFormData) {
|
||||
// Dynamically changes the button ID based on what the visitor enters in the pre-chat form.
|
||||
// Returns a valid button ID.
|
||||
//};
|
||||
//embedded_svc.settings.prepopulatedPrechatFields = {}; //Sets the auto-population of pre-chat form fields
|
||||
//embedded_svc.settings.fallbackRouting = []; //An array of button IDs, user IDs, or userId_buttonId
|
||||
//embedded_svc.settings.offlineSupportMinimizedText = '...'; //(Defaults to Contact Us)
|
||||
|
||||
embedded_svc.settings.enabledFeatures = ["LiveAgent"];
|
||||
embedded_svc.settings.entryFeature = "LiveAgent";
|
||||
|
||||
setupCustomSettings(
|
||||
modifyEmbeddedSvcCallback,
|
||||
finishedLoadingCallback,
|
||||
onWebchatOpenCallback,
|
||||
onWebchatCloseCallback
|
||||
);
|
||||
|
||||
embedded_svc.init(
|
||||
"https://safelite2.my.salesforce.com",
|
||||
"https://safelite2.my.salesforce-sites.com/chat",
|
||||
gslbBaseURL,
|
||||
embeddedservice_bootstrap.init(
|
||||
"00D3t000004nvsT",
|
||||
"Sales_Central_Snap_in",
|
||||
"Sales_Central_TFN",
|
||||
"https://safelite2.my.site.com/ESWSalesCentralTFN1769108630294",
|
||||
{
|
||||
baseLiveAgentContentURL:
|
||||
"https://c.la13-core2.sfdc-lywfpd.salesforceliveagent.com/content",
|
||||
deploymentId: "5723t0000008wWB",
|
||||
buttonId: "5733t000000D90z",
|
||||
baseLiveAgentURL: "https://d.la13-core2.sfdc-lywfpd.salesforceliveagent.com/chat",
|
||||
eswLiveAgentDevName:
|
||||
"EmbeddedServiceLiveAgent_Parent04I3t000000Cq0SEAS_188de1871ea",
|
||||
isOfflineSupportEnabled: false,
|
||||
scrt2URL: "https://safelite2.my.salesforce-scrt.com",
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
if (!window.embedded_svc) {
|
||||
var s = document.createElement("script");
|
||||
s.setAttribute("src", "https://safelite2.my.salesforce.com/embeddedservice/5.0/esw.min.js");
|
||||
s.onload = function () {
|
||||
initESW(null);
|
||||
};
|
||||
document.body.appendChild(s);
|
||||
} else {
|
||||
initESW("https://service.force.com");
|
||||
} catch (err) {
|
||||
console.error("Error loading Embedded Messaging: ", err);
|
||||
}
|
||||
// END OF SALESFORCE GENERATED CODE
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,9 @@
|
|||
/* eslint-disable */
|
||||
// ***************************************************************
|
||||
// This file wraps the Salesforce generated code.
|
||||
// See the comment below for where you need to paste in the Salesforce generated code.
|
||||
// You may also need to modify the script tag src located in salesforce-webchat.vue to point to the correct Salesforce instance.
|
||||
// ***************************************************************
|
||||
|
||||
function setupCustomSettings(
|
||||
modifyEmbeddedSvcCallback,
|
||||
|
|
@ -7,93 +12,38 @@ function setupCustomSettings(
|
|||
onWebchatCloseCallback
|
||||
) {
|
||||
modifyEmbeddedSvcCallback();
|
||||
embedded_svc.addEventHandler("onSettingsCallCompleted", finishedLoadingCallback);
|
||||
embedded_svc.addEventHandler("afterMaximize", onWebchatOpenCallback);
|
||||
embedded_svc.addEventHandler("afterDestroy", onWebchatCloseCallback);
|
||||
window.addEventListener("onEmbeddedMessagingButtonCreated", finishedLoadingCallback);
|
||||
window.addEventListener("onEmbeddedMessagingWindowMaximized", onWebchatOpenCallback);
|
||||
window.addEventListener("onEmbeddedMessagingWindowClosed", onWebchatCloseCallback);
|
||||
}
|
||||
|
||||
/*
|
||||
The code below is generated from salesforce but modified in the following ways:
|
||||
* Only the javascript inside the second <script> tag in the provided salesforce code is copy pasted
|
||||
* CSS is not copied in
|
||||
* First <script> tag loading in the file from the CDN is not copied in
|
||||
* Copy the entire contents of the second <script> tag inside the initializeSalesforceWebchatForQa
|
||||
method and add a call to setupCustomSettings before the 'embedded_svc.init()' method is called
|
||||
|
||||
Actual call to paste in:
|
||||
setupCustomSettings(
|
||||
modifyEmbeddedSvcCallback,
|
||||
finishedLoadingCallback,
|
||||
onWebchatOpenCallback,
|
||||
onWebchatCloseCallback
|
||||
);
|
||||
*/
|
||||
|
||||
export function initializeSalesforceWebchatForQa(
|
||||
modifyEmbeddedSvcCallback,
|
||||
finishedLoadingCallback,
|
||||
onWebchatOpenCallback,
|
||||
onWebchatCloseCallback
|
||||
) {
|
||||
var initESW = function (gslbBaseURL) {
|
||||
embedded_svc.settings.displayHelpButton = true; //Or false
|
||||
embedded_svc.settings.language = ""; //For example, enter 'en' or 'en-US'
|
||||
setupCustomSettings(
|
||||
modifyEmbeddedSvcCallback,
|
||||
finishedLoadingCallback,
|
||||
onWebchatOpenCallback,
|
||||
onWebchatCloseCallback
|
||||
);
|
||||
// START OF SALESFORCE GENERATED CODE
|
||||
// Copy in the contents of the first <script> tag inside the initEmbeddedMessaging() method
|
||||
try {
|
||||
embeddedservice_bootstrap.settings.language = "en_US"; // For example, enter 'en' or 'en-US'
|
||||
|
||||
//embedded_svc.settings.defaultMinimizedText = '...'; //(Defaults to Chat with an Expert)
|
||||
//embedded_svc.settings.disabledMinimizedText = '...'; //(Defaults to Agent Offline)
|
||||
|
||||
//embedded_svc.settings.loadingText = ''; //(Defaults to Loading)
|
||||
//embedded_svc.settings.storageDomain = 'yourdomain.com'; //(Sets the domain for your deployment so that visitors can navigate subdomains during a chat session)
|
||||
|
||||
// Settings for Chat
|
||||
//embedded_svc.settings.directToButtonRouting = function(prechatFormData) {
|
||||
// Dynamically changes the button ID based on what the visitor enters in the pre-chat form.
|
||||
// Returns a valid button ID.
|
||||
//};
|
||||
//embedded_svc.settings.prepopulatedPrechatFields = {}; //Sets the auto-population of pre-chat form fields
|
||||
//embedded_svc.settings.fallbackRouting = []; //An array of button IDs, user IDs, or userId_buttonId
|
||||
//embedded_svc.settings.offlineSupportMinimizedText = '...'; //(Defaults to Contact Us)
|
||||
|
||||
embedded_svc.settings.enabledFeatures = ["LiveAgent"];
|
||||
embedded_svc.settings.entryFeature = "LiveAgent";
|
||||
|
||||
setupCustomSettings(
|
||||
modifyEmbeddedSvcCallback,
|
||||
finishedLoadingCallback,
|
||||
onWebchatOpenCallback,
|
||||
onWebchatCloseCallback
|
||||
);
|
||||
|
||||
embedded_svc.init(
|
||||
"https://safelite2--safeliteua.sandbox.my.salesforce.com",
|
||||
"https://safelite2--safeliteua.sandbox.my.salesforce-sites.com/chat",
|
||||
gslbBaseURL,
|
||||
"00DDJ000000HRqO",
|
||||
"Sales_Central_Snap_in",
|
||||
embeddedservice_bootstrap.init(
|
||||
"00DTI000005x7c7",
|
||||
"Sales_Central_TFN",
|
||||
"https://safelite2--qa.sandbox.my.site.com/ESWSalesCentralTFN1768832680884",
|
||||
{
|
||||
baseLiveAgentContentURL:
|
||||
"https://c.la12s-core1.sfdc-8tgtt5.salesforceliveagent.com/content",
|
||||
deploymentId: "572DJ00000001j3",
|
||||
buttonId: "573DJ00000001ur",
|
||||
baseLiveAgentURL: "https://d.la12s-core1.sfdc-8tgtt5.salesforceliveagent.com/chat",
|
||||
eswLiveAgentDevName:
|
||||
"EmbeddedServiceLiveAgent_Parent04IDJ0000008OZk2AM_1883057c9a6",
|
||||
isOfflineSupportEnabled: false,
|
||||
scrt2URL: "https://safelite2--qa.sandbox.my.salesforce-scrt.com",
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
if (!window.embedded_svc) {
|
||||
var s = document.createElement("script");
|
||||
s.setAttribute(
|
||||
"src",
|
||||
"https://safelite2--safeliteua.sandbox.my.salesforce.com/embeddedservice/5.0/esw.min.js"
|
||||
);
|
||||
s.onload = function () {
|
||||
initESW(null);
|
||||
};
|
||||
document.body.appendChild(s);
|
||||
} else {
|
||||
initESW("https://service.force.com");
|
||||
} catch (err) {
|
||||
console.error("Error loading Embedded Messaging: ", err);
|
||||
}
|
||||
// END OF SALESFORCE GENERATED CODE
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
/* eslint-disable */
|
||||
// ***************************************************************
|
||||
// This file wraps the Salesforce generated code.
|
||||
// See the comment below for where you need to paste in the Salesforce generated code.
|
||||
// You may also need to modify the script tag src located in salesforce-webchat.vue to point to the correct Salesforce instance.
|
||||
// ***************************************************************
|
||||
|
||||
function setupCustomSettings(
|
||||
modifyEmbeddedSvcCallback,
|
||||
finishedLoadingCallback,
|
||||
onWebchatOpenCallback,
|
||||
onWebchatCloseCallback
|
||||
) {
|
||||
modifyEmbeddedSvcCallback();
|
||||
window.addEventListener("onEmbeddedMessagingButtonCreated", finishedLoadingCallback);
|
||||
window.addEventListener("onEmbeddedMessagingWindowMaximized", onWebchatOpenCallback);
|
||||
window.addEventListener("onEmbeddedMessagingWindowClosed", onWebchatCloseCallback);
|
||||
}
|
||||
|
||||
export function initializeSalesforceWebchatForSys(
|
||||
modifyEmbeddedSvcCallback,
|
||||
finishedLoadingCallback,
|
||||
onWebchatOpenCallback,
|
||||
onWebchatCloseCallback
|
||||
) {
|
||||
setupCustomSettings(
|
||||
modifyEmbeddedSvcCallback,
|
||||
finishedLoadingCallback,
|
||||
onWebchatOpenCallback,
|
||||
onWebchatCloseCallback
|
||||
);
|
||||
|
||||
// START OF SALESFORCE GENERATED CODE
|
||||
// Copy in the contents of the first <script> tag inside the initEmbeddedMessaging() method
|
||||
try {
|
||||
embeddedservice_bootstrap.settings.language = "en_US"; // For example, enter 'en' or 'en-US'
|
||||
|
||||
embeddedservice_bootstrap.init(
|
||||
"00DcY0000067qKz",
|
||||
"Sales_Central_TFN",
|
||||
"https://safelite2--sys.sandbox.my.site.com/ESWSalesCentralTFN1767797265631",
|
||||
{
|
||||
scrt2URL: "https://safelite2--sys.sandbox.my.salesforce-scrt.com",
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("Error loading Embedded Messaging: ", err);
|
||||
}
|
||||
// END OF SALESFORCE GENERATED CODE
|
||||
}
|
||||
|
|
@ -2,10 +2,20 @@ import { shallowMount } from "@vue/test-utils";
|
|||
import salesforceWebchat from "./salesforce-webchat.vue";
|
||||
import * as devHelper from "./salesforce-helper-dev";
|
||||
import * as qaHelper from "./salesforce-helper-qa";
|
||||
import * as sysHelper from "./salesforce-helper-sys";
|
||||
import * as prodHelper from "./salesforce-helper-prod";
|
||||
import { applicationConfig } from "../../constants/application-config";
|
||||
|
||||
describe("salesforceWebchat.vue", () => {
|
||||
beforeEach(() => {
|
||||
window.embeddedservice_bootstrap = {
|
||||
settings: {},
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
delete window.embeddedservice_bootstrap;
|
||||
});
|
||||
it("renders correctly", () => {
|
||||
const wrapper = shallowMount(salesforceWebchat);
|
||||
expect(wrapper.exists()).toBe(true);
|
||||
|
|
@ -14,7 +24,7 @@ describe("salesforceWebchat.vue", () => {
|
|||
it("has the correct default data", () => {
|
||||
const wrapper = shallowMount(salesforceWebchat);
|
||||
expect(wrapper.vm.$data).toEqual({
|
||||
isAgentAvailable: false,
|
||||
isWebchatLoaded: false,
|
||||
isWebchatOpen: false,
|
||||
});
|
||||
});
|
||||
|
|
@ -37,6 +47,12 @@ describe("salesforceWebchat.vue", () => {
|
|||
wrapper.vm.initializeSalesforceWebchat();
|
||||
expect(mockQa).toHaveBeenCalled();
|
||||
|
||||
applicationConfig.CURRENT_ENVIRONMENT = "SysTest";
|
||||
wrapper = shallowMount(salesforceWebchat);
|
||||
const mockSys = jest.spyOn(sysHelper, "initializeSalesforceWebchatForSys");
|
||||
wrapper.vm.initializeSalesforceWebchat();
|
||||
expect(mockSys).toHaveBeenCalled();
|
||||
|
||||
applicationConfig.CURRENT_ENVIRONMENT = "Dev";
|
||||
wrapper = shallowMount(salesforceWebchat);
|
||||
const mockDev = jest.spyOn(devHelper, "initializeSalesforceWebchatForDev");
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
// Supporting files
|
||||
import { initializeSalesforceWebchatForDev } from "./salesforce-helper-dev";
|
||||
import { initializeSalesforceWebchatForQa } from "./salesforce-helper-qa";
|
||||
import { initializeSalesforceWebchatForSys } from "./salesforce-helper-sys";
|
||||
import { initializeSalesforceWebchatForProd } from "./salesforce-helper-prod";
|
||||
import { applicationConfig } from "@/constants/application-config";
|
||||
import { webchatHelper } from "@/helpers/webchat-helper";
|
||||
|
|
@ -16,7 +17,7 @@ export default {
|
|||
mixins: [analyticsMixin],
|
||||
data() {
|
||||
return {
|
||||
isAgentAvailable: false,
|
||||
isWebchatLoaded: false,
|
||||
isWebchatOpen: false,
|
||||
};
|
||||
},
|
||||
|
|
@ -33,32 +34,13 @@ export default {
|
|||
mounted() {
|
||||
// Load in script from salesforce CDN
|
||||
const script = document.createElement("script");
|
||||
script.src = "https://service.force.com/embeddedservice/5.0/esw.min.js";
|
||||
script.src = this.webChatScriptSrc;
|
||||
script.onload = this.initializeSalesforceWebchat;
|
||||
document.body.appendChild(script);
|
||||
|
||||
// Notify funnel-header about Salesforce chat open/close state
|
||||
this._salesforceChatOpenObserver = new MutationObserver(() => {
|
||||
const isOpen = !!document.querySelector(".embeddedServiceSidebar");
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("salesforce-chat-visibility", { detail: { open: isOpen } })
|
||||
);
|
||||
});
|
||||
this._salesforceChatOpenObserver.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
},
|
||||
beforeUnmount() {
|
||||
// Clean up event listeners and observers
|
||||
window.removeEventListener(
|
||||
"salesforce-chat-visibility",
|
||||
this._handleSalesforceChatVisibility
|
||||
);
|
||||
window.removeEventListener("sierra-chat-transfer", this._handleSierraToSalesforceTransfer);
|
||||
if (this._salesforceChatOpenObserver) {
|
||||
this._salesforceChatOpenObserver.disconnect();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
initializeSalesforceWebchat() {
|
||||
|
|
@ -72,7 +54,6 @@ export default {
|
|||
);
|
||||
break;
|
||||
case "QA":
|
||||
case "SysTest":
|
||||
initializeSalesforceWebchatForQa(
|
||||
this.modifyEmbeddedSvcCallback,
|
||||
this.finishedLoadingCallback,
|
||||
|
|
@ -80,8 +61,17 @@ export default {
|
|||
this.onWebchatCloseCallback
|
||||
);
|
||||
break;
|
||||
case "SysTest":
|
||||
initializeSalesforceWebchatForSys(
|
||||
this.modifyEmbeddedSvcCallback,
|
||||
this.finishedLoadingCallback,
|
||||
this.onWebchatOpenCallback,
|
||||
this.onWebchatCloseCallback
|
||||
);
|
||||
break;
|
||||
case "Dev":
|
||||
case "Localhost":
|
||||
default:
|
||||
initializeSalesforceWebchatForDev(
|
||||
this.modifyEmbeddedSvcCallback,
|
||||
this.finishedLoadingCallback,
|
||||
|
|
@ -92,16 +82,13 @@ export default {
|
|||
}
|
||||
},
|
||||
modifyEmbeddedSvcCallback() {
|
||||
window.embedded_svc.settings.avatarImgURL = require("@/assets/img/salesforce-webchat/avatar_webchat.png");
|
||||
window.embedded_svc.settings.prechatBackgroundImgURL = require("@/assets/img/salesforce-webchat/safelite_logo_webchat.png");
|
||||
window.embedded_svc.settings.smallCompanyLogoImgURL = require("@/assets/img/salesforce-webchat/minimized_chat_bubble_webchat.png");
|
||||
window.embedded_svc.settings.displayHelpButton = false;
|
||||
window.embedded_svc.addEventHandler("onChatEstablished", () => {
|
||||
window.embeddedservice_bootstrap.settings.hideChatButtonOnLoad = true;
|
||||
window.addEventListener("onEmbeddedMessagingConversationStarted", () => {
|
||||
this.pushEventForChatsToGA("support_chat", "chat_opened", "Live Agent", true);
|
||||
});
|
||||
},
|
||||
finishedLoadingCallback(data) {
|
||||
this.isAgentAvailable = data.isAgentAvailable;
|
||||
this.isWebchatLoaded = true;
|
||||
this.updateShouldShowWebchatButton();
|
||||
},
|
||||
onWebchatOpenCallback() {
|
||||
|
|
@ -117,16 +104,35 @@ export default {
|
|||
updateShouldShowWebchatButton() {
|
||||
this.webchatGlobalNonpersistedState.showWebchatButton =
|
||||
!this.hideSalesforceWebchatLaunchButton &&
|
||||
this.isAgentAvailable &&
|
||||
this.isWebchatLoaded &&
|
||||
!this.isWebchatOpen;
|
||||
},
|
||||
},
|
||||
computed: {},
|
||||
computed: {
|
||||
webChatScriptSrc() {
|
||||
// These URLs are from the src tag on the script tag in the Salesforce generated code.
|
||||
switch (applicationConfig.CURRENT_ENVIRONMENT) {
|
||||
case "Prod":
|
||||
return "https://safelite2.my.site.com/ESWSalesCentralTFN1769108630294/assets/js/bootstrap.min.js";
|
||||
case "QA":
|
||||
return "https://safelite2--qa.sandbox.my.site.com/ESWSalesCentralTFN1768832680884/assets/js/bootstrap.min.js";
|
||||
case "SysTest":
|
||||
return "https://safelite2--sys.sandbox.my.site.com/ESWSalesCentralTFN1767797265631/assets/js/bootstrap.min.js";
|
||||
case "Dev":
|
||||
case "Localhost":
|
||||
default:
|
||||
return "https://safelite2--dev.sandbox.my.site.com/ESWSalesCentralTFN1755660094477/assets/js/bootstrap.min.js";
|
||||
}
|
||||
},
|
||||
},
|
||||
components: {},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.embedded-messaging > .embeddedMessagingFrame[class~="isMaximized"] {
|
||||
z-index: 9999 !important;
|
||||
}
|
||||
.dockableContainer {
|
||||
&.showDockableContainer {
|
||||
color: $gray-600;
|
||||
|
|
|
|||
|
|
@ -59,14 +59,6 @@ describe("sierraWebchat.vue", () => {
|
|||
expect(window.sierraChat.closeChatModal).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("dispatches sierra-chat-closed on handleSierraOnClose", () => {
|
||||
const eventSpy = jest.spyOn(window, "dispatchEvent");
|
||||
wrapper.vm.createSierraConfig().onClose();
|
||||
expect(eventSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ type: "sierra-chat-closed" })
|
||||
);
|
||||
});
|
||||
|
||||
it("dispatches sierra-chat-transfer on handleSierraOnTransfer", () => {
|
||||
const eventSpy = jest.spyOn(window, "dispatchEvent");
|
||||
const transfer = {
|
||||
|
|
@ -77,25 +69,4 @@ describe("sierraWebchat.vue", () => {
|
|||
expect.objectContaining({ type: "sierra-chat-transfer" })
|
||||
);
|
||||
});
|
||||
|
||||
it("handles sierra-chat-transfer event and starts Salesforce chat with prepopulated fields", () => {
|
||||
const transfer = {
|
||||
data: {
|
||||
first_name: "A",
|
||||
last_name: "B",
|
||||
email: "a@b.com",
|
||||
chat_summary: "summary",
|
||||
},
|
||||
};
|
||||
const event = new CustomEvent("sierra-chat-transfer", { detail: transfer });
|
||||
window.dispatchEvent(event);
|
||||
expect(window.sierraChat.closeChatModal).toHaveBeenCalled();
|
||||
expect(window.embedded_svc.settings.prepopulatedPrechatFields).toEqual({
|
||||
FirstName: "A",
|
||||
LastName: "B",
|
||||
Email: "a@b.com",
|
||||
Subject: "summary",
|
||||
});
|
||||
expect(window.embedded_svc.liveAgentAPI.startChat).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,10 +7,20 @@ import analyticsMixin from "@/mixins/analytics-mixin";
|
|||
import store from "@/store";
|
||||
import { routeData } from "@/router/constants/routes";
|
||||
import { deepClone } from "@/helpers/object-helper";
|
||||
import { webchatHelper } from "@/helpers/webchat-helper";
|
||||
|
||||
export default {
|
||||
name: "sierraWebchat",
|
||||
mixins: [analyticsMixin],
|
||||
data() {
|
||||
return {
|
||||
transferInProgress: false,
|
||||
};
|
||||
},
|
||||
setup() {
|
||||
const { webchatGlobalNonpersistedState, launchWebchat } = webchatHelper();
|
||||
return { webchatGlobalNonpersistedState, launchWebchat };
|
||||
},
|
||||
methods: {
|
||||
openSierraChatModal() {
|
||||
const sierra = window.sierraChat || window.sierra || window.SierraChat;
|
||||
|
|
@ -35,11 +45,18 @@ export default {
|
|||
display: "corner",
|
||||
onLoad: () => this.openSierraChatModal(),
|
||||
onOpen: () => {},
|
||||
onClose: () => window.dispatchEvent(new CustomEvent("sierra-chat-closed")),
|
||||
onTransfer: (transfer) =>
|
||||
onClose: () => {
|
||||
if (!this.transferInProgress) {
|
||||
this.webchatGlobalNonpersistedState.showWebchatButton = true;
|
||||
this.transferInProgress = false;
|
||||
}
|
||||
},
|
||||
onTransfer: (transfer) => {
|
||||
this.transferInProgress = true;
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("sierra-chat-transfer", { detail: transfer })
|
||||
),
|
||||
);
|
||||
},
|
||||
};
|
||||
},
|
||||
getSierraContext() {
|
||||
|
|
@ -138,10 +155,7 @@ export default {
|
|||
}
|
||||
// Pass data to Salesforce chat if available
|
||||
const transfer = event.detail;
|
||||
if (
|
||||
window.embedded_svc &&
|
||||
typeof window.embedded_svc.bootstrapEmbeddedService === "function"
|
||||
) {
|
||||
if (window.embeddedservice_bootstrap && window.embeddedservice_bootstrap.prechatAPI) {
|
||||
if (
|
||||
transfer &&
|
||||
transfer.data &&
|
||||
|
|
@ -149,51 +163,22 @@ export default {
|
|||
transfer.data.last_name &&
|
||||
transfer.data.email
|
||||
) {
|
||||
window.embedded_svc.settings.prepopulatedPrechatFields = {
|
||||
window.embeddedservice_bootstrap.prechatAPI.setHiddenPrechatFields({
|
||||
ScarlettAISummary: transfer.data.chat_summary,
|
||||
FirstName: transfer.data.first_name,
|
||||
LastName: transfer.data.last_name,
|
||||
Email: transfer.data.email,
|
||||
Subject: transfer.data.chat_summary,
|
||||
};
|
||||
window.embedded_svc.settings.extraPrechatFormDetails = [
|
||||
{
|
||||
label: "Scarlett AI Summary",
|
||||
value: transfer.data.chat_summary,
|
||||
transcriptFields: ["Scarlett_AI_Summary__c"],
|
||||
displayToAgent: true,
|
||||
},
|
||||
];
|
||||
if (
|
||||
window.embedded_svc.liveAgentAPI &&
|
||||
typeof window.embedded_svc.liveAgentAPI.startChat === "function"
|
||||
) {
|
||||
window.embedded_svc.liveAgentAPI.startChat();
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
window.embedded_svc.bootstrapEmbeddedService();
|
||||
this.webchatGlobalNonpersistedState.shouldLaunchSierra = false;
|
||||
this.launchWebchat();
|
||||
}
|
||||
};
|
||||
window.addEventListener("sierra-chat-transfer", this._handleSierraTransfer);
|
||||
|
||||
// Notify funnel-header about Sierra chat open/close state
|
||||
this._sierraChatContainerObserver = new MutationObserver(() => {
|
||||
const isOpen = !!document.querySelector("[data-sierra-chat-container]");
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("sierra-chat-visibility", { detail: { open: isOpen } })
|
||||
);
|
||||
});
|
||||
this._sierraChatContainerObserver.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
},
|
||||
beforeUnmount() {
|
||||
window.removeEventListener("launch-sierra-webchat", this.triggerSierraChat);
|
||||
window.removeEventListener("sierra-chat-transfer", this._handleSierraTransfer);
|
||||
if (this._sierraChatContainerObserver) {
|
||||
this._sierraChatContainerObserver.disconnect();
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -59,7 +59,6 @@ export default {
|
|||
return {
|
||||
globalAlertMessages: [],
|
||||
sierraChatOpen: false,
|
||||
salesforceChatOpen: false,
|
||||
isSalesforceTransferInProgress: false,
|
||||
isScrolled: false,
|
||||
};
|
||||
|
|
@ -80,20 +79,7 @@ export default {
|
|||
return this.getCmsContent(this.cmsWidgetName, "LogoImage");
|
||||
},
|
||||
shouldShowWebchatButton() {
|
||||
// Hide the chat icon if Sierra chat is open
|
||||
if (this.sierraChatOpen) {
|
||||
return false;
|
||||
}
|
||||
// Hide the chat icon if Salesforce chat is open or transfer is in progress
|
||||
if (this.salesforceChatOpen || this.isSalesforceTransferInProgress) {
|
||||
return false;
|
||||
}
|
||||
// Show if either Sierra is enabled or Salesforce button is available and not hidden by page
|
||||
return (
|
||||
(this.webchatGlobalNonpersistedState.shouldLaunchSierra ||
|
||||
this.webchatGlobalNonpersistedState.showWebchatButton) &&
|
||||
!this.shouldHideWebchatButtonOnPage
|
||||
);
|
||||
return this.webchatGlobalNonpersistedState.showWebchatButton;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
|
@ -121,10 +107,6 @@ export default {
|
|||
webchatClicked(event) {
|
||||
event.preventDefault();
|
||||
this.launchWebchat();
|
||||
// Only set sierraChatOpen if Sierra experiment is active
|
||||
if (this.webchatGlobalNonpersistedState.shouldLaunchSierra) {
|
||||
this.sierraChatOpen = true;
|
||||
}
|
||||
},
|
||||
syncSierraExperimentFlag() {
|
||||
const experiments = store.getters.applicationUser.experiments;
|
||||
|
|
@ -164,37 +146,6 @@ export default {
|
|||
unknownAlertEvent.displayAlert = true;
|
||||
this.globalAlertMessages.push(unknownAlertEvent);
|
||||
}
|
||||
// Listen for Sierra chat open/close events from sierra-webchat
|
||||
this._handleSierraChatVisibility = (event) => {
|
||||
this.sierraChatOpen = !!(event.detail && event.detail.open);
|
||||
if (this.sierraChatOpen) {
|
||||
this.isSalesforceTransferInProgress = false;
|
||||
}
|
||||
};
|
||||
window.addEventListener("sierra-chat-visibility", this._handleSierraChatVisibility);
|
||||
|
||||
// Listen for Salesforce chat open/close events from salesforce-webchat
|
||||
this._handleSalesforceChatVisibility = (event) => {
|
||||
this.salesforceChatOpen = !!(event.detail && event.detail.open === true);
|
||||
};
|
||||
window.addEventListener("salesforce-chat-visibility", this._handleSalesforceChatVisibility);
|
||||
|
||||
// Listen for transfer event to set transfer-in-progress flag
|
||||
this._handleSierraToSalesforceTransfer = () => {
|
||||
this.isSalesforceTransferInProgress = true;
|
||||
};
|
||||
window.addEventListener("sierra-chat-transfer", this._handleSierraToSalesforceTransfer);
|
||||
|
||||
// Reset transfer flag when Salesforce chat opens
|
||||
this._handleSalesforceChatOpened = (event) => {
|
||||
if (event.detail && event.detail.open === true) {
|
||||
this.isSalesforceTransferInProgress = false;
|
||||
this.salesforceChatOpen = true;
|
||||
} else {
|
||||
this.salesforceChatOpen = false;
|
||||
}
|
||||
};
|
||||
window.addEventListener("salesforce-chat-visibility", this._handleSalesforceChatOpened);
|
||||
|
||||
// Add scroll listener for header shadow effect
|
||||
this._handleScroll = () => {
|
||||
|
|
@ -203,14 +154,7 @@ export default {
|
|||
window.addEventListener("scroll", this._handleScroll);
|
||||
},
|
||||
beforeUnmount() {
|
||||
window.removeEventListener("sierra-chat-closed", this._handleSierraChatClosed);
|
||||
window.removeEventListener("sierra-chat-visibility", this._handleSierraChatVisibility);
|
||||
window.removeEventListener(
|
||||
"salesforce-chat-visibility",
|
||||
this._handleSalesforceChatVisibility
|
||||
);
|
||||
window.removeEventListener("sierra-chat-transfer", this._handleSierraToSalesforceTransfer);
|
||||
window.removeEventListener("salesforce-chat-visibility", this._handleSalesforceChatOpened);
|
||||
window.removeEventListener("scroll", this._handleScroll);
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -57,10 +57,6 @@ export default {
|
|||
modalWidgetName: String,
|
||||
pageName: String,
|
||||
},
|
||||
unmounted() {
|
||||
// remove any modal effects before leaving page (e.g. user hits browser back button)
|
||||
this.modal?.closeModal();
|
||||
},
|
||||
computed: {
|
||||
buttonText() {
|
||||
return this.getCmsContent(this.modalWidgetName, "SubheaderText");
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ defineRule("email-address-required", required(errorMessages.EMAIL_ADDRESS_REQUIR
|
|||
defineRule(
|
||||
"email-address-format",
|
||||
regex(
|
||||
/^([a-zA-Z0-9_\-.+]+)@([a-zA-Z0-9-]+)\.([a-zA-Z]{2,})$/,
|
||||
/^([a-zA-Z0-9_.+]+)@([a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*\.)+([a-zA-Z]{2,})$/,
|
||||
errorMessages.EMAIL_ADDRESS_FORMAT
|
||||
)
|
||||
);
|
||||
|
|
|
|||
|
|
@ -54,10 +54,6 @@ export default {
|
|||
mounted() {
|
||||
if (this.showSaveProgressPopup) this.modal.openModal();
|
||||
},
|
||||
unmounted() {
|
||||
// remove any modal effects before leaving page (e.g. user hits browser back button)
|
||||
this.modal.closeModal();
|
||||
},
|
||||
emits: ["close-save-progress-popup", "save-progress"], // <--- should remove oodles of warnings in dev tools
|
||||
props: {
|
||||
modelValue: Object,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import globalMethods from "@/global-methods";
|
|||
import { endpoints } from "@/constants/endpoints";
|
||||
import { AppointmentTypeStrings } from "@/constants/schedule-constants";
|
||||
import { paymentMethods } from "@/constants/payment-method-constants";
|
||||
import { applicationConfig } from "../constants/application-config";
|
||||
|
||||
export async function getNewSession(sessionConfig) {}
|
||||
|
||||
|
|
@ -42,8 +43,8 @@ export async function createAdyenCheckout({
|
|||
id: id,
|
||||
sessionData: sessionData,
|
||||
},
|
||||
clientKey: "test_WYGT4F5ZT5BRRL5MPWHK6QLYOIXZXROD",
|
||||
environment: "test",
|
||||
clientKey: applicationConfig.ADYEN_CLIENT_KEY,
|
||||
environment: applicationConfig.ADYEN_ENVIRONMENT,
|
||||
locale: "en_US",
|
||||
countryCode: "US",
|
||||
showPayButton: showPayButton,
|
||||
|
|
@ -83,6 +84,21 @@ export function mapAdyenToFmgPaymentMethod(adyenMethod) {
|
|||
return match ?? paymentMethods.CREDIT_CARD;
|
||||
}
|
||||
|
||||
export function mapFmgToAdyenPaymentMethod(fmgMethod) {
|
||||
const paymentMethodMap = {
|
||||
[paymentMethods.CREDIT_CARD]: "scheme",
|
||||
[paymentMethods.AFTERPAY]: "afterpaytouch",
|
||||
[paymentMethods.PAYPAL]: "paypal",
|
||||
[paymentMethods.APPLE_PAY]: "applepay",
|
||||
[paymentMethods.PAY_NOW]: null,
|
||||
[paymentMethods.LATER]: null,
|
||||
};
|
||||
|
||||
const match = paymentMethodMap[fmgMethod];
|
||||
|
||||
return match;
|
||||
}
|
||||
|
||||
export function generateCcToken(adyenSessionInfo) {
|
||||
const order = store.getters.order;
|
||||
const locationInfo = getLocationInfo(order);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,14 @@ import { partTypeStrings } from "@/constants/part-type-strings";
|
|||
|
||||
const recalPartTypes = [partTypeStrings.RECALIBRATION, partTypeStrings.ADAS_RECALIBRATION];
|
||||
|
||||
export function anyPartWithRequiresRecalFlag(lineItems) {
|
||||
if (!lineItems) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return lineItems.glassParts?.some((li) => li.requiresRecalibration === true);
|
||||
}
|
||||
|
||||
export function isRecalPartOrHasChildRecalPart(lineItem) {
|
||||
if (lineItem.childParts && lineItem.childParts.length > 0) {
|
||||
return isRecalPart(lineItem) || containsRecalParts(lineItem.childParts);
|
||||
|
|
|
|||
|
|
@ -254,6 +254,19 @@ export function getDiscountedPackageName(discountPackage) {
|
|||
return package_names[discountPackage] || null;
|
||||
}
|
||||
|
||||
export function getPackageLabel(packageName) {
|
||||
switch (packageName) {
|
||||
case packageNames.TIER_ONE:
|
||||
return externalParamPackageLabels.GLASS_ONLY;
|
||||
case packageNames.TIER_TWO:
|
||||
return externalParamPackageLabels.STANDARD;
|
||||
case packageNames.TIER_THREE:
|
||||
return externalParamPackageLabels.PREMIUM;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function maxTier(tierA, tierB) {
|
||||
if (tierA === packageNames.TIER_THREE || tierB === packageNames.TIER_THREE) {
|
||||
return packageNames.TIER_THREE;
|
||||
|
|
|
|||
|
|
@ -8,10 +8,11 @@ const webchatGlobalNonpersistedState = reactive({
|
|||
|
||||
export const webchatHelper = () => {
|
||||
function launchWebchat() {
|
||||
webchatGlobalNonpersistedState.showWebchatButton = false;
|
||||
if (webchatGlobalNonpersistedState.shouldLaunchSierra) {
|
||||
window.dispatchEvent(new CustomEvent("launch-sierra-webchat")); // launches the sierra chat
|
||||
} else {
|
||||
window.embedded_svc.bootstrapEmbeddedService(); // launches the salesForce prechat form
|
||||
window.embeddedservice_bootstrap.utilAPI.launchChat(); // launches the salesForce prechat form
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ defineRule("email-address-required", required(errorMessages.EMAIL_ADDRESS_REQUIR
|
|||
defineRule(
|
||||
"email-address-format",
|
||||
regex(
|
||||
/^([a-zA-Z0-9_\-.+]+)@([a-zA-Z0-9-]+)\.([a-zA-Z]{2,})$/,
|
||||
/^([a-zA-Z0-9_.+]+)@([a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*\.)+([a-zA-Z]{2,})$/,
|
||||
errorMessages.EMAIL_ADDRESS_FORMAT
|
||||
)
|
||||
);
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ defineRule("email-address-required", required(errorMessages.EMAIL_ADDRESS_REQUIR
|
|||
defineRule(
|
||||
"email-address-format",
|
||||
regex(
|
||||
/^([a-zA-Z0-9_\-.+]+)@([a-zA-Z0-9-]+)\.([a-zA-Z]{2,})$/,
|
||||
/^([a-zA-Z0-9_.+]+)@([a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*\.)+([a-zA-Z]{2,})$/,
|
||||
errorMessages.EMAIL_ADDRESS_FORMAT
|
||||
)
|
||||
);
|
||||
|
|
|
|||
|
|
@ -164,6 +164,8 @@ export default {
|
|||
// await any pending save-sessions. if you don't, we will save the new parent account into vuex and then a pending async save-session response overwrites it
|
||||
await flushSaveSessionQueue();
|
||||
|
||||
await this.removeQPDLineItemIfExists();
|
||||
|
||||
await this.dispatchStoreAction(
|
||||
this.storeActions.SAVE_PARENT_ACCOUNT_NUMBER,
|
||||
this.parentAccountNumber.toString(),
|
||||
|
|
@ -193,6 +195,17 @@ export default {
|
|||
handlePageShow() {
|
||||
window.location.reload();
|
||||
},
|
||||
async removeQPDLineItemIfExists() {
|
||||
const lineItems = deepClone(store.getters.order.lineItems);
|
||||
lineItems.supportingItems = baseMixin.methods.filterOutQuotePageDiscountPart(
|
||||
lineItems.supportingItems
|
||||
);
|
||||
await baseMixin.methods.dispatchStoreAction(
|
||||
storeActions.SAVE_SUPPORTING_ITEMS_SUPPRESSING_STATE_RESETTING,
|
||||
lineItems.supportingItems,
|
||||
false
|
||||
);
|
||||
},
|
||||
},
|
||||
unmounted() {
|
||||
window.removeEventListener("pageshow", this.handlePageShow);
|
||||
|
|
|
|||
|
|
@ -25,6 +25,11 @@
|
|||
cmsWidgetName="PleaseProvideAddressWidget"
|
||||
class="provide-address-header" />
|
||||
<textBlock
|
||||
v-if="isMSRFeeApplicable()"
|
||||
cmsWidgetName="VehicleKeysMSRMessageWidget"
|
||||
class="keys-message" />
|
||||
<textBlock
|
||||
v-else
|
||||
cmsWidgetName="VehicleKeysMessageWidget"
|
||||
class="keys-message" />
|
||||
<mobileAddressQuestions
|
||||
|
|
@ -143,6 +148,9 @@ export default {
|
|||
getIsVehicleProtectedFromStore() {
|
||||
return store.getters.order.serviceLocation.isVehicleProtected;
|
||||
},
|
||||
isMSRFeeApplicable() {
|
||||
return store.getters.order.isMSRFeeApplicable;
|
||||
},
|
||||
async forwardButtonAction() {
|
||||
await this.dispatchStoreAction(
|
||||
this.storeActions.SAVE_MOBILE_DETAILS,
|
||||
|
|
|
|||
|
|
@ -306,6 +306,12 @@ describe("partQuestions.vue...", () => {
|
|||
//Assert
|
||||
expect(spy).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
storeActions.SAVE_IS_OEM_GLASS_SELECTED,
|
||||
false,
|
||||
false
|
||||
);
|
||||
expect(spy).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
"savePartQuestionAnswers",
|
||||
[
|
||||
{
|
||||
|
|
|
|||
|
|
@ -188,6 +188,8 @@ export default {
|
|||
}
|
||||
});
|
||||
|
||||
this.dispatchStoreAction(this.storeActions.SAVE_IS_OEM_GLASS_SELECTED, false, false);
|
||||
|
||||
// save to vuex store as order.damage.partQuestionAnswers (array)
|
||||
// used in GET_PARTS call following this one
|
||||
await this.dispatchStoreAction(
|
||||
|
|
|
|||
|
|
@ -1,18 +1,48 @@
|
|||
<template>
|
||||
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm">
|
||||
<div class="container-fluid">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<funnelHeader cmsWidgetName="FunnelHeaderWidget" ref="funnelHeader" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 col-xl-4">
|
||||
<funnelSubHeader class="pb-4" cmsWidgetName="FunnelSubHeaderWidget" />
|
||||
<!--
|
||||
... Page code goes here.
|
||||
-->
|
||||
<div id="adyen-container"></div>
|
||||
<funnelHeader cmsWidgetName="FunnelHeaderWidget" ref="funnelHeader" />
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div>
|
||||
<alert
|
||||
ref="piaErrorAlert"
|
||||
class="my-4"
|
||||
v-if="hasPaymentFailureError"
|
||||
cmsWidgetName="PIAErrorAlertWidget"
|
||||
alertClass="alert-danger"
|
||||
@textLinkClicked="paymentFailedPayLater"
|
||||
v-bind:isDismissible="false" />
|
||||
</div>
|
||||
<div id="card-container">
|
||||
<div id="payment-container">
|
||||
<funnelSubHeader class="py-5" cmsWidgetName="FunnelSubHeaderWidget" />
|
||||
<div id="adyen-container"></div>
|
||||
</div>
|
||||
|
||||
<div id="cart-container">
|
||||
<cart
|
||||
ref="cart"
|
||||
:damage="damageInfo"
|
||||
:availableVaps="[]"
|
||||
:allowItemRemoval="false"
|
||||
v-model="lineItems"
|
||||
servicePackageOptionsCmsName="ServicePackageTitle"
|
||||
recyclingModalCmsWidgetName="RecycleModal"
|
||||
:isInsurance="isInsurance"
|
||||
:insuranceDeductible="currentDeductible"
|
||||
:insuranceCompanyName="insuranceCompanyName"
|
||||
:showInsuranceCoverageAs="showInsuranceCoverageAs"
|
||||
:isItac="isItac"
|
||||
:isNoComp="isNoComp"
|
||||
:isCollapsible="false" />
|
||||
<textBlock
|
||||
class="payment-disclaimer"
|
||||
marginTopSizeOverride="0"
|
||||
cmsWidgetName="PaymentDisclaimerWidget" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<navbar
|
||||
cmsWidgetName="FunnelFooterWidget"
|
||||
ref="navbar"
|
||||
|
|
@ -29,6 +59,8 @@ import Form from "vee-validate";
|
|||
import funnelHeader from "@/fmg-components/funnel-header/funnel-header";
|
||||
import navbar from "@/fmg-components/nav-bar/nav-bar";
|
||||
import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header";
|
||||
import alert from "@/ux-components/alert/alert";
|
||||
import textBlock from "@/digital-components/text-block/text-block";
|
||||
|
||||
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
|
||||
import { settleAllPromises } from "@/helpers/layout-helper";
|
||||
|
|
@ -42,9 +74,16 @@ import { submitWorkOrder } from "@/helpers/heritage-integration/order-helper.js"
|
|||
import { createAdyenCheckout } from "@/helpers/adyen-helper";
|
||||
import { Dropin } from "@adyen/adyen-web/auto";
|
||||
import { applicationConfig } from "@/constants/application-config";
|
||||
import { paymentMethods } from "@/constants/payment-method-constants";
|
||||
|
||||
import "@adyen/adyen-web/styles/adyen.css";
|
||||
import { mapAdyenToFmgPaymentMethod } from "../../helpers/adyen-helper";
|
||||
import { mapAdyenToFmgPaymentMethod, mapFmgToAdyenPaymentMethod } from "../../helpers/adyen-helper";
|
||||
import { showFmgLoadingModal } from "../../helpers/loading-modal-helper";
|
||||
import cart from "@/fmg-components/cart/cart";
|
||||
import { coverageStatus } from "@/constants/insurance";
|
||||
import { deepClone } from "@/helpers/object-helper";
|
||||
import store from "@/store";
|
||||
import { debugLog } from "@/helpers/debug-log-helper.js";
|
||||
|
||||
export default {
|
||||
name: "payment-adyen",
|
||||
|
|
@ -52,6 +91,8 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
sessionId: String,
|
||||
dropinComponent: null,
|
||||
hasPaymentFailureError: false,
|
||||
};
|
||||
},
|
||||
|
||||
|
|
@ -69,9 +110,15 @@ export default {
|
|||
|
||||
const resultMap = await settleAllPromises(promiseResultMap);
|
||||
|
||||
const hasExistingPiaError = to.query?.piaFailure;
|
||||
|
||||
// Hydrate page with results
|
||||
next(async (vm) => {
|
||||
vm.setCmsContent(resultMap.cmsContent);
|
||||
|
||||
if (hasExistingPiaError) {
|
||||
vm.hasPaymentFailureError = true;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
|
|
@ -89,18 +136,117 @@ export default {
|
|||
},
|
||||
|
||||
arePagePrerequisitesValid() {
|
||||
// Stub, for checking if application is capable of entering this page
|
||||
// And/or, is all information required for this page present?
|
||||
// To prevent sequence-breaking from the end-user.
|
||||
// Service Location
|
||||
const serviceLocation = store.getters.order.serviceLocation;
|
||||
const mobileReqs = !!(
|
||||
serviceLocation.address &&
|
||||
serviceLocation.city &&
|
||||
serviceLocation.state &&
|
||||
serviceLocation.zipCode
|
||||
);
|
||||
|
||||
return true;
|
||||
const providerLocation = serviceLocation.provider.address;
|
||||
const dropOffInshopReqs = !!(
|
||||
providerLocation.streetAddress &&
|
||||
providerLocation.city &&
|
||||
providerLocation.state &&
|
||||
providerLocation.zipCode
|
||||
);
|
||||
|
||||
const isMobile = serviceLocation.appointmentType === AppointmentTypeStrings.MOBILE;
|
||||
|
||||
const serviceLocationReqs =
|
||||
(isMobile && mobileReqs) || (!isMobile && dropOffInshopReqs);
|
||||
|
||||
// Insurance
|
||||
const isInsuranceSet = store.getters.order.payment.isInsurance !== null;
|
||||
|
||||
// Schedule
|
||||
const schedule = store.getters.order.schedule;
|
||||
const scheduleReqs = !!(
|
||||
schedule.date &&
|
||||
schedule.startTime &&
|
||||
schedule.endTime &&
|
||||
schedule.jobMaxMinutes &&
|
||||
schedule.jobMinMinutes
|
||||
);
|
||||
|
||||
// Customer
|
||||
const customer = store.getters.order.customer;
|
||||
const customerReqs = !!(
|
||||
customer.firstName &&
|
||||
customer.lastName &&
|
||||
customer.phoneNumber &&
|
||||
customer.emailAddress
|
||||
);
|
||||
|
||||
const paymentMethodReqs =
|
||||
store.getters.order.payment.isPia !== null &&
|
||||
(store.getters.order.payment.isPia || !!store.getters.order.payment.piaType);
|
||||
|
||||
const preReqResult =
|
||||
serviceLocationReqs &&
|
||||
isInsuranceSet &&
|
||||
scheduleReqs &&
|
||||
customerReqs &&
|
||||
paymentMethodReqs;
|
||||
|
||||
// prettier-ignore
|
||||
{
|
||||
debugLog("--- payment-adyen.vue pagePrereqs start ---", null, !preReqResult);
|
||||
debugLog("mobileReqs:", mobileReqs, !preReqResult);
|
||||
debugLog("serviceLocation.address:", serviceLocation.address, !preReqResult);
|
||||
debugLog("serviceLocation.city:", serviceLocation.city, !preReqResult);
|
||||
debugLog("serviceLocation.state:", serviceLocation.state, !preReqResult);
|
||||
debugLog("serviceLocation.zipCode:", serviceLocation.zipCode, !preReqResult);
|
||||
|
||||
debugLog("", null, !preReqResult);
|
||||
|
||||
debugLog("dropOffInshopReqs:", dropOffInshopReqs, !preReqResult);
|
||||
debugLog("serviceLocationReqs:", serviceLocationReqs, !preReqResult);
|
||||
debugLog("providerLocation.streetAddress:", providerLocation.streetAddress, !preReqResult);
|
||||
debugLog("providerLocation.city:", providerLocation.city, !preReqResult);
|
||||
debugLog("providerLocation.state:", providerLocation.state, !preReqResult);
|
||||
debugLog("providerLocation.zipCode:", providerLocation.zipCode, !preReqResult);
|
||||
|
||||
debugLog("", null, !preReqResult);
|
||||
|
||||
debugLog("isInsuranceSet:", isInsuranceSet, !preReqResult);
|
||||
|
||||
debugLog("", null, !preReqResult);
|
||||
|
||||
debugLog("scheduleReqs:", scheduleReqs, !preReqResult);
|
||||
debugLog("schedule.date:", schedule.date, !preReqResult);
|
||||
debugLog("schedule.startTime:", schedule.startTime, !preReqResult);
|
||||
debugLog("schedule.endTime:", schedule.endTime, !preReqResult);
|
||||
debugLog("schedule.jobMaxMinutes:", schedule.jobMaxMinutes, !preReqResult);
|
||||
debugLog("schedule.jobMinMinutes:", schedule.jobMinMinutes, !preReqResult);
|
||||
|
||||
debugLog("", null, !preReqResult);
|
||||
|
||||
debugLog("customerReqs:", customerReqs, !preReqResult);
|
||||
debugLog("customer.firstName:", customer.firstName, !preReqResult);
|
||||
debugLog("customer.lastName:", customer.lastName, !preReqResult);
|
||||
debugLog("customer.phoneNumber:", customer.phoneNumber, !preReqResult);
|
||||
debugLog("customer.emailAddress:", customer.emailAddress, !preReqResult);
|
||||
|
||||
debugLog("", null, !preReqResult);
|
||||
|
||||
debugLog("paymentMethodReqs:", paymentMethodReqs, !preReqResult);
|
||||
debugLog("store.getters.order.payment.isPia:", store.getters.order?.payment?.isPia, !preReqResult);
|
||||
debugLog("store.getters.order.payment.piaType:", store.getters.order?.payment?.piaType, !preReqResult);
|
||||
|
||||
debugLog("--- payment-adyen.vue pagePrereqs end ---", null, !preReqResult);
|
||||
}
|
||||
|
||||
return preReqResult;
|
||||
},
|
||||
|
||||
async initializeAdyen() {
|
||||
console.log(`Price = ${this.amountDue}`);
|
||||
console.log(`Adyen Price = ${this.adyenPriceTotal}`);
|
||||
|
||||
const requestBody = this.adyenInitRequestInfo;
|
||||
const requestBody = this.getAdyenInitRequestInfo();
|
||||
|
||||
console.log(`Calling with:`);
|
||||
console.log(requestBody);
|
||||
|
|
@ -148,8 +294,16 @@ export default {
|
|||
|
||||
console.log(checkout);
|
||||
|
||||
window.checkout = checkout;
|
||||
const dropin = new Dropin(checkout, {
|
||||
const expiryTime = new Date(checkout.options.expiresAt);
|
||||
const expiryInterval = expiryTime.getTime() - new Date().getTime();
|
||||
|
||||
const handleTimeout = () => {
|
||||
this.resetAdyenDropin();
|
||||
};
|
||||
|
||||
const timeout = setTimeout(handleTimeout, expiryInterval);
|
||||
|
||||
const configuration = {
|
||||
paymentMethodsConfiguration: {
|
||||
ideal: {
|
||||
showImage: true,
|
||||
|
|
@ -159,10 +313,10 @@ export default {
|
|||
value: this.adyenPriceTotal,
|
||||
currency: "USD",
|
||||
},
|
||||
environment: "test", // Change this to "live" when you're ready to accept live PayPal payments
|
||||
environment: applicationConfig.ADYEN_ENVIRONMENT,
|
||||
countryCode: "US", // Only needed for test. This will be automatically retrieved when you are in production.
|
||||
blockPayPalVenmoButton: false,
|
||||
//blockPayPalPayLaterButton: true
|
||||
blockPayPalVenmoButton: true,
|
||||
blockPayPalPayLaterButton: true,
|
||||
},
|
||||
card: {
|
||||
hasHolderName: true,
|
||||
|
|
@ -171,7 +325,21 @@ export default {
|
|||
name: "Credit or debit card",
|
||||
},
|
||||
},
|
||||
}).mount("#adyen-container");
|
||||
};
|
||||
|
||||
const adyenPaymentType = mapFmgToAdyenPaymentMethod(this.piaType);
|
||||
|
||||
if (adyenPaymentType) {
|
||||
configuration.openPaymentMethod = {
|
||||
type: adyenPaymentType,
|
||||
};
|
||||
}
|
||||
|
||||
const dropin = new Dropin(checkout, configuration);
|
||||
|
||||
this.dropinComponent = dropin;
|
||||
|
||||
dropin.mount("#adyen-container");
|
||||
},
|
||||
|
||||
async handleCompletedPayment(result) {
|
||||
|
|
@ -189,6 +357,8 @@ export default {
|
|||
console.log(`Get session payload =`);
|
||||
console.log(paymentSessionRequest);
|
||||
|
||||
showFmgLoadingModal(true);
|
||||
|
||||
const response = await globalMethods.callHttpClient({
|
||||
method: endpoints.GetAdyenSessionResult.method,
|
||||
endpoint: endpoints.GetAdyenSessionResult.url,
|
||||
|
|
@ -233,7 +403,20 @@ export default {
|
|||
false
|
||||
);
|
||||
|
||||
await baseMixin.methods.dispatchStoreAction(storeActions.SAVE_CCTOKEN, ccToken, false);
|
||||
if (paymentMethod === paymentMethods.PAYPAL) {
|
||||
await baseMixin.methods.dispatchStoreAction(
|
||||
storeActions.SAVE_PAYPAL_TOKEN,
|
||||
responseJson?.storedToken,
|
||||
false
|
||||
);
|
||||
} else {
|
||||
await baseMixin.methods.dispatchStoreAction(
|
||||
storeActions.SAVE_CCTOKEN,
|
||||
ccToken,
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
await baseMixin.methods.dispatchStoreAction(
|
||||
storeActions.SAVE_NEXTGEN_SETTLED_AMOUNT,
|
||||
this.amountDue,
|
||||
|
|
@ -246,33 +429,33 @@ export default {
|
|||
console.log(`Result =`);
|
||||
console.log(result);
|
||||
|
||||
// Fetch session info
|
||||
const paymentSessionRequest = {
|
||||
zipCodeCtu: this.locationInfo.zipCodeCtu,
|
||||
workOrderNumber: this.workOrderNumberLastSixDigits,
|
||||
sourceSystem: this.sourceSystem,
|
||||
sessionId: this.sessionId,
|
||||
sessionResult: result?.sessionResult,
|
||||
};
|
||||
const wasPaymentCancelled = result?.resultCode === "Cancelled";
|
||||
|
||||
console.log(`Get session payload =`);
|
||||
console.log(paymentSessionRequest);
|
||||
if (!wasPaymentCancelled) {
|
||||
const stringToLog = `ADYEN PAYMENT NOT AUTHORIZED. Code = ${result?.resultCode}. Id = ${this.sessionId}`;
|
||||
|
||||
try {
|
||||
const response = await globalMethods.callHttpClient({
|
||||
method: endpoints.GetAdyenSessionResult.method,
|
||||
endpoint: endpoints.GetAdyenSessionResult.url,
|
||||
payload: paymentSessionRequest,
|
||||
logApiCall: true,
|
||||
pageNameToLog: "payment-adyen",
|
||||
});
|
||||
await global.$logger.logError(stringToLog);
|
||||
|
||||
console.log(response);
|
||||
} catch (error) {
|
||||
console.log(`Error getting session info after failure.`);
|
||||
this.hasPaymentFailureError = true;
|
||||
}
|
||||
|
||||
this.dropinComponent?.update();
|
||||
},
|
||||
async handleError(error) {
|
||||
console.log(error);
|
||||
|
||||
const wasPaymentCancelled = error?.name === "CANCEL";
|
||||
|
||||
if (!wasPaymentCancelled) {
|
||||
const stringToLog = `ADYEN ERROR. Name = ${error?.name}. Details = ${error?.message}.`;
|
||||
|
||||
await global.$logger.logError(stringToLog);
|
||||
|
||||
this.hasPaymentFailureError = true;
|
||||
}
|
||||
|
||||
this.dropinComponent?.update();
|
||||
},
|
||||
handleError(error) {},
|
||||
|
||||
async saveAndSubmitWorkOrder() {
|
||||
// Work order submission after successful payment.
|
||||
|
|
@ -285,15 +468,12 @@ export default {
|
|||
} catch (error) {
|
||||
console.log(`error: response from submit work order: ${error.message}`);
|
||||
|
||||
// navigate to error page
|
||||
const errorPayload = {
|
||||
cause: `Error while submitting work order after Adyen payment.`,
|
||||
currentPage: this.pageName,
|
||||
sessionId: this.sessionId,
|
||||
idempotencyKey: this.idempotencyKey,
|
||||
};
|
||||
this.$router.navigateWithoutSaving(
|
||||
this.navigationScenarios.PIA_ERROR,
|
||||
this.pageName
|
||||
);
|
||||
|
||||
this.$router.handleSoftError(errorPayload);
|
||||
return;
|
||||
}
|
||||
|
||||
// clear order
|
||||
|
|
@ -303,10 +483,57 @@ export default {
|
|||
this.pageName
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
adyenInitRequestInfo() {
|
||||
async paymentFailedPayLater() {
|
||||
console.log(`Doing final work order submission...`);
|
||||
showFmgLoadingModal(true);
|
||||
|
||||
await this.dispatchStoreAction(
|
||||
storeActions.SAVE_PAYMENT_METHOD_CHOICE,
|
||||
paymentMethods.PAY_LATER,
|
||||
false
|
||||
);
|
||||
|
||||
await this.dispatchStoreAction(storeActions.RESET_SAVE_SESSION_PROMISE);
|
||||
|
||||
try {
|
||||
await submitWorkOrder({
|
||||
pageNameToLog: this.pageName,
|
||||
submitAfterSave: true,
|
||||
});
|
||||
|
||||
await baseMixin.methods.dispatchStoreAction(storeActions.CREATE_SUBMITTED_STATE);
|
||||
|
||||
this.$router.navigateWithoutSaving(
|
||||
this.navigationScenarios.CLICKED_PAY_LATER,
|
||||
this.pageName
|
||||
);
|
||||
} catch (error) {
|
||||
console.log(`Final submission failed after failed payment.`);
|
||||
this.$router.handleSoftError();
|
||||
return;
|
||||
}
|
||||
},
|
||||
|
||||
async resetAdyenDropin() {
|
||||
this?.dropinComponent?.unmount();
|
||||
await this.initializeAdyen();
|
||||
},
|
||||
|
||||
// Now a Method so it is always freshly called and not cached.
|
||||
getIdempotencyKey() {
|
||||
const currentDateTime = new Date();
|
||||
const currentHour = currentDateTime.getUTCHours();
|
||||
const currentDate = currentDateTime.getUTCDate();
|
||||
const id = this?.$store?.getters?.order?.referralCorrelationId;
|
||||
const system = this.sourceSystem;
|
||||
const total = this.adyenPriceTotal;
|
||||
|
||||
return `${id}-${system}-${currentDate}-${currentHour}-${total}`;
|
||||
},
|
||||
|
||||
// Now a Method so it is always freshly called and not cached.
|
||||
getAdyenInitRequestInfo() {
|
||||
return {
|
||||
sourceSystem: this.sourceSystem,
|
||||
referralSequenceNumber: this.$store.getters.order.referralSequenceNumber,
|
||||
|
|
@ -323,9 +550,15 @@ export default {
|
|||
IP: "127.0.0.1", // TODO
|
||||
firstName: this.$store.getters.order.customer.firstName,
|
||||
lastName: this.$store.getters.order.customer.lastName,
|
||||
idempotencyKey: this.idempotencyKey,
|
||||
idempotencyKey: this.getIdempotencyKey(),
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
piaType() {
|
||||
return this.$store.getters.order.payment.piaType;
|
||||
},
|
||||
|
||||
workOrderNumberLastSixDigits() {
|
||||
const workOrderNumber = this.$store.getters.order.workOrderNumber ?? "";
|
||||
|
|
@ -382,10 +615,6 @@ export default {
|
|||
};
|
||||
},
|
||||
|
||||
idempotencyKey() {
|
||||
return `${this.$store.getters.order.referralCorrelationId}-${this.sourceSystem}`;
|
||||
},
|
||||
|
||||
sourceSystem() {
|
||||
return "FMG-2.0";
|
||||
},
|
||||
|
|
@ -395,7 +624,41 @@ export default {
|
|||
},
|
||||
|
||||
adyenPriceTotal() {
|
||||
return this.amountDue * 100;
|
||||
return Math.round(this.amountDue * 100);
|
||||
},
|
||||
|
||||
// Cart info
|
||||
damageInfo() {
|
||||
return this.$store.getters.damage;
|
||||
},
|
||||
isInsurance() {
|
||||
return this.$store.getters.payment.isInsurance;
|
||||
},
|
||||
isNoComp() {
|
||||
return this.$store.getters.policy.isNoComp;
|
||||
},
|
||||
isItac() {
|
||||
return this.$store.getters.policy.isItac;
|
||||
},
|
||||
showInsuranceCoverageAs() {
|
||||
if (!this.isInsurance) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this.isNoComp || this.isItac) {
|
||||
return coverageStatus.NOCOMP;
|
||||
} else {
|
||||
return this.$store.getters.payment.insuranceCoverage.coverageStatus;
|
||||
}
|
||||
},
|
||||
currentDeductible() {
|
||||
return this.$store.getters.policy.currentDeductible;
|
||||
},
|
||||
insuranceCompanyName() {
|
||||
return this.$store.getters.policy.insuranceCompanyName;
|
||||
},
|
||||
lineItems() {
|
||||
return deepClone(this.$store.getters.lineItems);
|
||||
},
|
||||
},
|
||||
|
||||
|
|
@ -408,6 +671,39 @@ export default {
|
|||
funnelHeader,
|
||||
funnelSubHeader,
|
||||
navbar,
|
||||
cart,
|
||||
alert,
|
||||
textBlock,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="scss">
|
||||
#card-container {
|
||||
display: flex;
|
||||
& > #payment-container {
|
||||
flex-basis: 55%;
|
||||
margin-right: 9%;
|
||||
}
|
||||
& > #cart-container {
|
||||
flex-basis: 36%;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(lg) {
|
||||
flex-direction: column;
|
||||
& > #payment-container {
|
||||
flex-basis: auto;
|
||||
margin-right: 0;
|
||||
}
|
||||
& > #cart-container {
|
||||
flex-basis: auto;
|
||||
margin: 0 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.payment-disclaimer p {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.2;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -810,11 +810,51 @@ export default {
|
|||
const piaInsurance = this.getSettingValue(experimentSettings.PIA_INSURANCE);
|
||||
|
||||
if (this.isInsurance) {
|
||||
return piaInsurance === "true" && this.currentDeductible > 0;
|
||||
return (
|
||||
(piaInsurance === "true" || this.isConditionalPIAEnabled) &&
|
||||
this.currentDeductible > 0
|
||||
);
|
||||
} else {
|
||||
return piaExperience === "PIA Optional" || piaExperience === "PIA Required";
|
||||
if (piaExperience?.includes("PIA Disabled")) {
|
||||
return this.isConditionalPIAEnabled;
|
||||
} else {
|
||||
return piaExperience === "PIA Optional" || piaExperience === "PIA Required";
|
||||
}
|
||||
}
|
||||
},
|
||||
isConditionalPIAEnabled() {
|
||||
const conditionalProviderNumbers = this.getSettingValue(
|
||||
experimentSettings.CONDITIONAL_PROVIDER_NUMBERS
|
||||
);
|
||||
const conditionalProviderNumbers2 = this.getSettingValue(
|
||||
experimentSettings.CONDITIONAL_PROVIDER_NUMBERS_2
|
||||
);
|
||||
const conditionalProviderShutoffDate = this.getSettingValue(
|
||||
experimentSettings.CONDITIONAL_PROVIDER_SHUTOFF_DATE
|
||||
);
|
||||
|
||||
const allConditionalProviderNumbers = [
|
||||
conditionalProviderNumbers,
|
||||
conditionalProviderNumbers2,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(",");
|
||||
const providerNumber =
|
||||
this.$store.getters.order.serviceLocation.provider.providerNumber;
|
||||
const appointmentDate = new Date(this.$store.getters.order.schedule.date);
|
||||
const shutoffDate = new Date(conditionalProviderShutoffDate);
|
||||
if (allConditionalProviderNumbers && conditionalProviderShutoffDate) {
|
||||
const providerNumbersArray = allConditionalProviderNumbers
|
||||
.split(",")
|
||||
.map((num) => num.trim());
|
||||
if (!providerNumbersArray.includes(providerNumber)) {
|
||||
return false;
|
||||
} else {
|
||||
return appointmentDate <= shutoffDate;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
paymentMethod() {
|
||||
if (!this.isPiaEnabled) {
|
||||
return paymentMethods.LATER;
|
||||
|
|
|
|||
|
|
@ -745,10 +745,16 @@ export default {
|
|||
}
|
||||
},
|
||||
updatePaymentMethod(paymentMethod) {
|
||||
// if the payment method is not set yet, then set it based on what HOP sends us
|
||||
debugLog(
|
||||
`updatePaymentMethod(${paymentMethod}) and existing store.getters.order.payment.piaType = ${store?.getters?.order?.payment?.piaType}`
|
||||
);
|
||||
|
||||
// if the payment method is not set yet, then set it based on what HOP sends
|
||||
// or if its not already set to AfterPay, always set it based on what HOP sends
|
||||
if (
|
||||
store.getters.order.payment.piaType == paymentMethods.PAY_NOW ||
|
||||
store.getters.order.payment.piaType == paymentMethods.NONE
|
||||
store.getters.order.payment.piaType == paymentMethods.NONE ||
|
||||
store.getters.order.payment.piaType != paymentMethods.AFTERPAY
|
||||
) {
|
||||
console.log(new Date() + " UPDATE PaymentMethod from HOP:" + paymentMethod);
|
||||
this.dispatchStoreAction(
|
||||
|
|
|
|||
70
src/layouts/policy-info/policy-info.spec.js
Normal file
70
src/layouts/policy-info/policy-info.spec.js
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import policyInfo from "@/layouts/policy-info/policy-info.vue";
|
||||
import { shallowMount } from "@vue/test-utils";
|
||||
import { getMountOptions } from "@/helpers/unit-test-helper.js";
|
||||
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
|
||||
import { settleAllPromises } from "@/helpers/layout-helper";
|
||||
|
||||
// Mock our module for promises.
|
||||
jest.mock("@/helpers/layout-helper.js", () => ({
|
||||
settleAllPromises: jest.fn(),
|
||||
}));
|
||||
|
||||
// Mock fetchCmsContentForPage
|
||||
jest.mock("@/helpers/cms-content-helper", () => ({
|
||||
fetchCmsContentForPage: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("policy-info.vue", () => {
|
||||
describe("component rendering", () => {
|
||||
test("renders funnelHeader component", () => {
|
||||
const { wrapper } = setupMocks();
|
||||
expect(wrapper.findComponent({ name: "funnelHeader" }).exists()).toBe(true);
|
||||
});
|
||||
|
||||
test("renders funnelSubHeader component", () => {
|
||||
const { wrapper } = setupMocks();
|
||||
expect(wrapper.findComponent({ name: "funnelSubHeader" }).exists()).toBe(true);
|
||||
});
|
||||
|
||||
test("renders navbar component", () => {
|
||||
const { wrapper } = setupMocks();
|
||||
expect(wrapper.findComponent({ name: "navbar" }).exists()).toBe(true);
|
||||
});
|
||||
|
||||
test("renders Form component", () => {
|
||||
const { wrapper } = setupMocks();
|
||||
expect(wrapper.findComponent({ name: "Form" }).exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("methods", () => {
|
||||
test("arePagePrerequisitesValid returns true", () => {
|
||||
const { wrapper } = setupMocks();
|
||||
expect(wrapper.vm.arePagePrerequisitesValid()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function setupMocks() {
|
||||
const mockCmsContent = {
|
||||
FunnelHeaderWidget: { Text: "Header" },
|
||||
FunnelSubHeaderWidget: { Text: "Subheader" },
|
||||
FunnelFooterWidget: { Text: "Footer" },
|
||||
};
|
||||
|
||||
fetchCmsContentForPage.mockResolvedValue(mockCmsContent);
|
||||
settleAllPromises.mockResolvedValue({ cmsContent: mockCmsContent });
|
||||
|
||||
const mountOptions = getMountOptions({
|
||||
route: { name: "policy-info", query: {}, params: {} },
|
||||
router: {
|
||||
navigate: jest.fn(),
|
||||
navigateWithSaving: jest.fn(),
|
||||
navigateWithoutSaving: jest.fn(),
|
||||
},
|
||||
});
|
||||
|
||||
const wrapper = shallowMount(policyInfo, mountOptions);
|
||||
|
||||
return { wrapper };
|
||||
}
|
||||
72
src/layouts/policy-info/policy-info.vue
Normal file
72
src/layouts/policy-info/policy-info.vue
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
<template>
|
||||
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm" v-slot="{ meta }">
|
||||
<funnelHeader cmsWidgetName="FunnelHeaderWidget" ref="funnelHeader" />
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-10 col-lg-8 col-xl-7">
|
||||
<funnelSubHeader class="pb-4" cmsWidgetName="FunnelSubHeaderWidget" />
|
||||
<!--
|
||||
... Page code goes here.
|
||||
-->
|
||||
<navbar
|
||||
cmsWidgetName="FunnelFooterWidget"
|
||||
ref="navbar"
|
||||
:isForwardActionDisabled="!meta.valid"
|
||||
@back-clicked="backButtonAction"
|
||||
@ForwardClicked="forwardButtonAction" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import funnelHeader from "@/fmg-components/funnel-header/funnel-header";
|
||||
import navbar from "@/fmg-components/nav-bar/nav-bar";
|
||||
import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header";
|
||||
import { Form } from "vee-validate";
|
||||
|
||||
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
|
||||
import { settleAllPromises } from "@/helpers/layout-helper";
|
||||
|
||||
export default {
|
||||
name: "policy-info",
|
||||
mixins: [],
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
components: {
|
||||
Form,
|
||||
funnelHeader,
|
||||
navbar,
|
||||
funnelSubHeader,
|
||||
},
|
||||
// Ensure CMS content is fetched during route navigation without depending on `to`/`from`
|
||||
async beforeRouteEnter(to, from, next) {
|
||||
const pageName = to.name;
|
||||
const cmsContentPromise = fetchCmsContentForPage(pageName);
|
||||
const promiseResultMap = [
|
||||
{
|
||||
resultKey: "cmsContent",
|
||||
promise: cmsContentPromise,
|
||||
},
|
||||
];
|
||||
const resultMap = await settleAllPromises(promiseResultMap);
|
||||
next((vm) => {
|
||||
vm.setCmsContent(resultMap.cmsContent);
|
||||
});
|
||||
},
|
||||
|
||||
methods: {
|
||||
async backButtonAction() {
|
||||
// Stub, for navigating back via nav-bar.
|
||||
},
|
||||
async forwardButtonAction() {
|
||||
// Stub, for navigating forward via nav-bar
|
||||
},
|
||||
arePagePrerequisitesValid() {
|
||||
return true;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -253,9 +253,10 @@ export default {
|
|||
height: 40px;
|
||||
}
|
||||
h3 {
|
||||
font-family: UrbanistSemibold;
|
||||
font-family: UrbanistBold;
|
||||
font-size: 1rem;
|
||||
margin: 0 0 0 0.5rem;
|
||||
color: $black;
|
||||
}
|
||||
}
|
||||
p {
|
||||
|
|
|
|||
167
src/layouts/quote/oem-glass-question/oem-glass-question.vue
Normal file
167
src/layouts/quote/oem-glass-question/oem-glass-question.vue
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
<template>
|
||||
<div
|
||||
class="oem-glass-question-container py-5 mt-3 mb-6"
|
||||
:class="[isExpanded ? 'expanded' : '']">
|
||||
<div class="oem-glass-question-header" @click="toggleIsExpanded">
|
||||
{{ headerText }}
|
||||
</div>
|
||||
<div class="oem-glass-question-body">
|
||||
<div class="oem-glass-question-copy" v-html="bodyText"></div>
|
||||
<div class="oem-glass-question-option">
|
||||
<checkboxQuestion
|
||||
class="mt-3 ms-1"
|
||||
cmsWidgetName="OemGlassInputQuestionWidget"
|
||||
v-model="installOemGlass" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import checkboxQuestion from "@/digital-components/checkbox-question/checkbox-question";
|
||||
|
||||
export default {
|
||||
name: "oem-part-question",
|
||||
props: {
|
||||
cmsWidgetName: String,
|
||||
modelValue: Boolean,
|
||||
oemGlassPartPrice: Number,
|
||||
aftermarketGlassPartPrice: Number,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isExpanded: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
headerText() {
|
||||
return this.getCmsContent(this.cmsWidgetName, "HeaderText");
|
||||
},
|
||||
bodyText() {
|
||||
let priceDifference = this.oemGlassPartPrice - this.aftermarketGlassPartPrice;
|
||||
let bodyText = this.getCmsContent(this.cmsWidgetName, "BodyText");
|
||||
bodyText = bodyText.replace(
|
||||
"{custom:OEMGLASSPRICE}",
|
||||
"<span id='price-dif'>$" + priceDifference.toFixed(2) + "</span>"
|
||||
);
|
||||
return bodyText;
|
||||
},
|
||||
footerText() {
|
||||
return this.getCmsContent(this.cmsWidgetName, "FooterText");
|
||||
},
|
||||
installOemGlass: {
|
||||
get() {
|
||||
return this.modelValue;
|
||||
},
|
||||
set(newValue) {
|
||||
this.$emit("update:modelValue", newValue);
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
toggleIsExpanded() {
|
||||
this.setIsExpanded(!this.isExpanded);
|
||||
},
|
||||
setIsExpanded(newVal) {
|
||||
this.isExpanded = newVal;
|
||||
},
|
||||
},
|
||||
components: {
|
||||
checkboxQuestion,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.oem-glass-question-container {
|
||||
border-top: 1px solid gray;
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
margin: 0 2rem;
|
||||
}
|
||||
|
||||
.oem-glass-question-header {
|
||||
font-family: UrbanistSemibold, Arial, Helvetica, sans-serif;
|
||||
color: $blue;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
cursor: pointer;
|
||||
|
||||
span {
|
||||
flex-grow: 1;
|
||||
color: $black;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
transition: all 0.5s ease;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 9'%3E%3Cpath d='M7.99282 0.117702C7.75716 0.116049 7.53044 0.207797 7.3623 0.37286L0.255553 7.46878C0.0862976 7.63796 -0.00878906 7.86742 -0.00878906 8.10667C-0.00878906 8.34593 0.0862976 8.57539 0.255553 8.74457C0.424808 8.91374 0.654368 9.00879 0.893731 9.00879C1.13309 9.00879 1.36265 8.91374 1.53191 8.74457L7.99282 2.27378L14.4614 8.74457C14.6307 8.91205 14.8595 9.00547 15.0977 9.00428C15.3359 9.00308 15.5638 8.90737 15.7314 8.73819C15.8989 8.56901 15.9924 8.34022 15.9912 8.10216C15.99 7.8641 15.8942 7.63627 15.725 7.46878L8.6259 0.377963C8.45765 0.21088 8.22999 0.117289 7.99282 0.117702Z' fill='%230070d1'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: center center;
|
||||
color: $blue;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: inline-flex;
|
||||
position: relative;
|
||||
margin-left: 0.5rem;
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
.oem-glass-question-body {
|
||||
max-height: 0;
|
||||
transition: all 350ms ease-in;
|
||||
overflow: hidden;
|
||||
visibility: hidden;
|
||||
|
||||
.oem-glass-question-option {
|
||||
margin-top: 0.5em;
|
||||
font-family: UrbanistSemibold, Arial, Helvetica, sans-serif;
|
||||
|
||||
.oem-glass-question-strikethrough-price {
|
||||
font-size: smaller;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.oem-glass-question-free-price {
|
||||
display: inline-block;
|
||||
font-weight: bolder;
|
||||
border-radius: 1em;
|
||||
background-color: $green-300;
|
||||
padding: 0 0.5em;
|
||||
margin-inline-start: 0.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.expanded {
|
||||
.oem-glass-question-header {
|
||||
&::after {
|
||||
transform: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.oem-glass-question-body {
|
||||
visibility: visible;
|
||||
max-height: none;
|
||||
padding-top: 0.5em;
|
||||
margin-top: 0.5em;
|
||||
|
||||
.oem-glass-question-copy {
|
||||
#price-dif {
|
||||
font-weight: $font-weight-700;
|
||||
color: $black;
|
||||
}
|
||||
}
|
||||
|
||||
.oem-glass-question-option {
|
||||
.small {
|
||||
font-weight: $font-weight-700;
|
||||
color: $black;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -118,6 +118,9 @@ jest.mock("@/mixins/experiment-mixin.js", () => ({
|
|||
if (settingName === mockExperimentSettings.DISPLAY_AFTERPAY_BREAKOUT_DISPLAY) {
|
||||
return false;
|
||||
}
|
||||
if (settingName === mockExperimentSettings.OFFER_OEM) {
|
||||
return false;
|
||||
}
|
||||
return "test";
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@
|
|||
@vapsItemsSelected="vapsItemsSelectedAction"
|
||||
@quotePageDiscountSelected="quotePageDiscountSelectedAction"
|
||||
@currentNumberOfPackages="currentNumberOfPackagesAction"
|
||||
@servicePackageSelected="servicePackageSelectedAction"
|
||||
:servicePackage="servicePackage"
|
||||
:activePromos="lineItems.promos"
|
||||
:availableQuotePageDiscounts="quoteDiscountPartsInfo"
|
||||
|
|
@ -85,6 +86,13 @@
|
|||
isInsuranceSelected ? isInsuranceContinueButtonText : ''
|
||||
" />
|
||||
|
||||
<oemGlassQuestion
|
||||
cmsWidgetName="OemGlassQuestionWidget"
|
||||
v-if="offerOem && !isInsuranceSelected"
|
||||
v-model="isOemGlassSelected"
|
||||
:oemGlassPartPrice="oemGlassPartPrice"
|
||||
:aftermarketGlassPartPrice="aftermarketGlassPartPrice" />
|
||||
|
||||
<saveProgressModalQuestion
|
||||
modalWidgetName="SaveProgressModalWidget"
|
||||
modalName="SaveProgressModal"
|
||||
|
|
@ -128,6 +136,7 @@ import afterpayModalBanner from "@/layouts/quote/afterpay-modal-banner/afterpay-
|
|||
import recalDisclaimer from "@/layouts/quote/recal-disclaimer/recal-disclaimer.vue";
|
||||
import saveProgressModalQuestion from "@/fmg-components/save-progress-modal-question/save-progress-modal-question";
|
||||
import saveProgressPopupQuestion from "@/fmg-components/save-progress-popup-question/save-progress-popup-question";
|
||||
import oemGlassQuestion from "./oem-glass-question/oem-glass-question.vue";
|
||||
|
||||
// Supporting files
|
||||
import baseMixin from "@/mixins/base-mixin.js";
|
||||
|
|
@ -217,6 +226,15 @@ export default {
|
|||
null,
|
||||
"quote"
|
||||
);
|
||||
const experimentAllowsOfferOem = experimentMixin.methods.hasSettingEqualTo(
|
||||
experimentSettings.OFFER_OEM,
|
||||
"true"
|
||||
);
|
||||
const oemGlassCheckPromise = baseMixin.methods.dispatchStoreActionWithLogging(
|
||||
storeActions.GET_OEM_AFTERMARKET_PARTS,
|
||||
null,
|
||||
"quote"
|
||||
);
|
||||
|
||||
const shouldExposeSkipQuote = store.getters.isRecalibrationOnOrder;
|
||||
const skipQuoteExperiment = store.getters.applicationUser.experiments.find(
|
||||
|
|
@ -290,6 +308,10 @@ export default {
|
|||
resultKey: "supportingItems",
|
||||
promise: supportingItemsPromise,
|
||||
},
|
||||
{
|
||||
resultKey: "oemGlassCheck",
|
||||
promise: oemGlassCheckPromise,
|
||||
},
|
||||
];
|
||||
|
||||
const resultMap = await settleAllPromises(promiseResultMap);
|
||||
|
|
@ -311,6 +333,18 @@ export default {
|
|||
lineItems.vaps = lineItems.vaps ?? [];
|
||||
const nullSafeGlassParts = lineItems.glassParts ?? [];
|
||||
|
||||
const oemGlassCheck = resultMap.oemGlassCheck;
|
||||
const offerOem = experimentAllowsOfferOem ? oemGlassCheck?.offerOem : false;
|
||||
const oemGlassPieceParts = oemGlassCheck?.glassPieceParts;
|
||||
const oemGlassParts =
|
||||
oemGlassPieceParts && oemGlassPieceParts.length > 0
|
||||
? oemGlassPieceParts[0].parts
|
||||
: null;
|
||||
const isOemGlassSelected = store.getters.order.damage?.installOemGlass ?? false;
|
||||
|
||||
let alternateGlassPartsForOem;
|
||||
if (oemGlassParts) alternateGlassPartsForOem = oemGlassParts;
|
||||
|
||||
const quotePageDiscountPartsInfo = getAvailableQuotePageDiscounts();
|
||||
|
||||
const quotePageDiscountParts = quotePageDiscountPartsInfo.map((partInfo) => ({
|
||||
|
|
@ -328,6 +362,18 @@ export default {
|
|||
...quotePageDiscountParts,
|
||||
];
|
||||
|
||||
let pricedAlternateGlassParts = null;
|
||||
if (alternateGlassPartsForOem && alternateGlassPartsForOem.length > 0) {
|
||||
pricedAlternateGlassParts = await baseMixin.methods.dispatchStoreActionWithLogging(
|
||||
storeActions.PRICE_ORDER_ITEMS_AND_SAVE_SERVER_DATA,
|
||||
{
|
||||
availableLineItems: alternateGlassPartsForOem,
|
||||
},
|
||||
"quote",
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
// When calling pricing from the quote page, always use the cash parent account but save the existing one in case it's unverified insurance navigating backwards
|
||||
var holdParentAccountNumber = store.getters.order.payment?.parentAccountNumber;
|
||||
var holdBillToAccountNumber = store.getters.order.payment?.billToAccountNumber;
|
||||
|
|
@ -389,6 +435,41 @@ export default {
|
|||
vm.holdParentAccountNumber = holdParentAccountNumber;
|
||||
vm.holdBillToAccountNumber = holdBillToAccountNumber;
|
||||
vm.skipToInsurance = shouldSkipToInsurance;
|
||||
vm.offerOem = offerOem;
|
||||
vm.alternateGlassPartsForOem = pricedAlternateGlassParts; // oemParts
|
||||
vm.isOemGlassSelected = isOemGlassSelected;
|
||||
|
||||
const getOEMGlassPartPrice = () => {
|
||||
let price = 0;
|
||||
if (vm.alternateGlassPartsForOem) {
|
||||
let oemGlassPart = vm.isOemGlassSelected
|
||||
? vm.lineItems.glassParts
|
||||
: vm.alternateGlassPartsForOem;
|
||||
if (oemGlassPart) {
|
||||
price =
|
||||
oemGlassPart.find(
|
||||
(altPart) => altPart.partType === partTypeStrings.WINDSHIELD
|
||||
)?.sellingPrice || 0;
|
||||
}
|
||||
}
|
||||
return price;
|
||||
};
|
||||
const getAftermarketGlassPartPrice = () => {
|
||||
let price = 0;
|
||||
if (vm.alternateGlassPartsForOem) {
|
||||
let aftermarketGlassPart = vm.isOemGlassSelected
|
||||
? vm.alternateGlassPartsForOem
|
||||
: vm.lineItems.glassParts;
|
||||
if (aftermarketGlassPart) {
|
||||
price = aftermarketGlassPart.find(
|
||||
(altPart) => altPart.partType === partTypeStrings.WINDSHIELD
|
||||
)?.sellingPrice;
|
||||
}
|
||||
}
|
||||
return price;
|
||||
};
|
||||
vm.oemGlassPartPrice = getOEMGlassPartPrice();
|
||||
vm.aftermarketGlassPartPrice = getAftermarketGlassPartPrice();
|
||||
|
||||
if (revalidatePromoResponse) {
|
||||
const revalidateAlerts = buildToastMessagesFromRevalidateOrValidatePromoResponse(
|
||||
|
|
@ -556,6 +637,8 @@ export default {
|
|||
availableLineItems: null,
|
||||
lineItems: [],
|
||||
addableVaps: [],
|
||||
offerOem: null,
|
||||
alternateGlassPartsForOem: null,
|
||||
servicePackage: null,
|
||||
holdParentAccountNumber: null,
|
||||
holdBillToAccountNumber: null,
|
||||
|
|
@ -564,6 +647,8 @@ export default {
|
|||
isSaveProgressComplete: null,
|
||||
packageNumber: null,
|
||||
skipToInsurance: null,
|
||||
servicePackageSelected: null,
|
||||
isOemGlassSelected: false,
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
|
|
@ -670,6 +755,9 @@ export default {
|
|||
vapsItemsSelectedAction(vapsItemsSelected) {
|
||||
this.lineItems.vaps = vapsItemsSelected;
|
||||
},
|
||||
servicePackageSelectedAction(newValue) {
|
||||
this.servicePackageSelected = newValue;
|
||||
},
|
||||
quotePageDiscountSelectedAction(supportingItemsSelected) {
|
||||
this.lineItems.supportingItems = supportingItemsSelected;
|
||||
},
|
||||
|
|
@ -725,6 +813,20 @@ export default {
|
|||
this.lineItems.supportingItems = this.filterOutFees(this.lineItems.supportingItems);
|
||||
}
|
||||
|
||||
if (
|
||||
this.lineItems.glassParts &&
|
||||
this.alternateGlassPartsForOem &&
|
||||
this.alternateGlassPartsForOem.length > 0
|
||||
) {
|
||||
let isOEMGlassOnOrder = this.isOEMGlassOnOrder(this.lineItems.glassParts);
|
||||
if (
|
||||
(this.isOemGlassSelected && !isOEMGlassOnOrder) ||
|
||||
(!this.isOemGlassSelected && isOEMGlassOnOrder)
|
||||
) {
|
||||
this.swapOEMGlassPartsOnOrder();
|
||||
}
|
||||
}
|
||||
|
||||
if (this.lineItems.glassParts?.length > 0) {
|
||||
this.dispatchStoreAction(
|
||||
this.storeActions.SAVE_GLASS_PARTS_SUPPRESSING_STATE_RESETTING,
|
||||
|
|
@ -757,8 +859,20 @@ export default {
|
|||
},
|
||||
false
|
||||
);
|
||||
this.dispatchStoreAction(
|
||||
this.storeActions.SAVE_IS_OEM_GLASS_SELECTED,
|
||||
this.isOemGlassSelected,
|
||||
false
|
||||
);
|
||||
|
||||
await savePageData(this.pageName, { saveProgressPopupSkipped: true }, true);
|
||||
await savePageData(
|
||||
this.pageName,
|
||||
{
|
||||
saveProgressPopupSkipped: true,
|
||||
servicePackageSelected: this.servicePackageSelected,
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
const payment = this.$store.getters.payment;
|
||||
if (payment.isInsurance) {
|
||||
|
|
@ -868,6 +982,26 @@ export default {
|
|||
const alert = createPromoSuccessAlert(lineItems.promos[0].promoCode);
|
||||
this.$refs.funnelHeader.pushGlobalAlert(alert, alert.shouldAutoFade);
|
||||
},
|
||||
isOEMGlassOnOrder(glassParts) {
|
||||
if (glassParts && glassParts.length > 0) {
|
||||
return glassParts.some((part) => part.partNumber.endsWith("OEM"));
|
||||
}
|
||||
return false;
|
||||
},
|
||||
swapOEMGlassPartsOnOrder() {
|
||||
this.lineItems.glassParts.forEach((part, index) => {
|
||||
if (part.partType === partTypeStrings.WINDSHIELD) {
|
||||
let altPartToUse = this.alternateGlassPartsForOem.find(
|
||||
(altPart) => altPart.partType === partTypeStrings.WINDSHIELD
|
||||
);
|
||||
if (altPartToUse) {
|
||||
let childParts = this.lineItems.glassParts[index].childParts;
|
||||
altPartToUse.childParts = childParts;
|
||||
this.lineItems.glassParts[index] = altPartToUse;
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
components: {
|
||||
funnelHeader,
|
||||
|
|
@ -884,6 +1018,7 @@ export default {
|
|||
recalDisclaimer,
|
||||
saveProgressModalQuestion,
|
||||
saveProgressPopupQuestion,
|
||||
oemGlassQuestion,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import {
|
|||
findLineItemsWithPartType,
|
||||
getPackageNameByType,
|
||||
getDiscountedPackageName,
|
||||
getPackageLabel,
|
||||
} from "@/helpers/service-package-helper";
|
||||
import {
|
||||
getPromosThatMatchLineItemsOnOrder,
|
||||
|
|
@ -45,6 +46,7 @@ import { containsRecalParts, getItemsWithoutRecalParts } from "@/helpers/recal-h
|
|||
import experimentMixin from "@/mixins/experiment-mixin.js";
|
||||
import { experimentSettings } from "@/constants/experiments";
|
||||
import { savePageData } from "@/router/methods/helpers/save-page-data";
|
||||
import { debugLog } from "@/helpers/debug-log-helper";
|
||||
|
||||
export default {
|
||||
name: "servicePackageQuestion",
|
||||
|
|
@ -84,6 +86,12 @@ export default {
|
|||
if (this.activePromos?.length) this.selectDefaultPackage();
|
||||
},
|
||||
selectedPackageName(newValue) {
|
||||
debugLog("selectedPackageName:", newValue);
|
||||
const packageLabel = getPackageLabel(newValue);
|
||||
|
||||
debugLog("getPackageLabel:", packageLabel);
|
||||
this.$emit("servicePackageSelected", `ServicePackage-${packageLabel}`);
|
||||
|
||||
const VapsProductsInSelectedPackage = this.getVapsLineItemsForSelectedPackage(newValue);
|
||||
this.$emit("vapsItemsSelected", VapsProductsInSelectedPackage);
|
||||
const discountPartInSelectedPackage = this.getDiscountPartForSelectedPackage(newValue);
|
||||
|
|
|
|||
|
|
@ -72,10 +72,10 @@ export function isDropOffRouteCode(routeCode) {
|
|||
);
|
||||
}
|
||||
|
||||
export function getTodayDate(routeCode) {
|
||||
export function getTodayDate() {
|
||||
return new Date();
|
||||
}
|
||||
|
||||
export function getTodayDateString(routeCode) {
|
||||
export function getTodayDateString() {
|
||||
return convertDateToDateString(getTodayDate());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
isModalOpen: false,
|
||||
selectedAppontment: {
|
||||
selectedAppointment: {
|
||||
estimatedServiceMinutes: {
|
||||
minimum: null,
|
||||
maximum: null,
|
||||
|
|
@ -58,6 +58,7 @@ export default {
|
|||
timeSlot: null,
|
||||
date: null,
|
||||
},
|
||||
confirmedAppointment: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
|
|
@ -67,22 +68,25 @@ export default {
|
|||
this.modal.openModal();
|
||||
},
|
||||
onModalClosed() {
|
||||
this.pushEvent();
|
||||
this.$emit("close-mobile-first-modal");
|
||||
this.confirmedAppointment = false;
|
||||
},
|
||||
closeModal() {
|
||||
this.modal.closeModal();
|
||||
},
|
||||
confirmAppointment() {
|
||||
let selectedTimeSlot = {
|
||||
timeSlot: this.selectedAppontment.timeSlot,
|
||||
routeCode: this.selectedAppontment.timeSlot.id,
|
||||
date: this.selectedAppontment.date,
|
||||
timeSlot: this.selectedAppointment.timeSlot,
|
||||
routeCode: this.selectedAppointment.timeSlot.id,
|
||||
date: this.selectedAppointment.date,
|
||||
};
|
||||
this.confirmedAppointment = true;
|
||||
this.$emit("confirm-appointment", selectedTimeSlot);
|
||||
this.modal.closeModal();
|
||||
},
|
||||
setSelectedAppointment(appointment) {
|
||||
this.selectedAppontment = appointment;
|
||||
this.selectedAppointment = appointment;
|
||||
},
|
||||
getIsModalOpen() {
|
||||
return this.isModalOpen;
|
||||
|
|
@ -90,6 +94,21 @@ export default {
|
|||
setIsModalOpen(isOpen) {
|
||||
this.isModalOpen = isOpen;
|
||||
},
|
||||
pushEvent() {
|
||||
let gaAction = this.confirmedAppointment ? this.GaActions.YES : this.GaActions.NO;
|
||||
if (this.selectedAppointment && this.selectedAppointment.timeSlot) {
|
||||
this.pushEventToGA(
|
||||
this.GaCategories.CONFIRMATION_CLICKED,
|
||||
gaAction,
|
||||
this.selectedAppointment.timeSlot.startTime +
|
||||
"-" +
|
||||
this.selectedAppointment.timeSlot.endTime +
|
||||
" " +
|
||||
this.selectedAppointment.date,
|
||||
true
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
modalName() {
|
||||
|
|
@ -112,37 +131,37 @@ export default {
|
|||
if (token.includes(INLINE_SERVICETYPE_TOKEN)) {
|
||||
const serviceType = "<strong>" + this.getServiceType + "</strong>";
|
||||
tokens.splice(tokens.indexOf(token), 1, serviceType);
|
||||
} else if (token.includes(INLINE_DAY_TOKEN) && this.selectedAppontment.date) {
|
||||
} else if (token.includes(INLINE_DAY_TOKEN) && this.selectedAppointment.date) {
|
||||
const day =
|
||||
"<strong>" +
|
||||
new Date(this.selectedAppontment.date).toLocaleDateString("en-US", {
|
||||
new Date(this.selectedAppointment.date).toLocaleDateString("en-US", {
|
||||
weekday: "long",
|
||||
timeZone: "UTC",
|
||||
}) +
|
||||
"</strong>";
|
||||
tokens.splice(tokens.indexOf(token), 1, day);
|
||||
} else if (token.includes(INLINE_DATE_TOKEN) && this.selectedAppontment.date) {
|
||||
const date = new Date(this.selectedAppontment.date).toLocaleDateString(
|
||||
} else if (token.includes(INLINE_DATE_TOKEN) && this.selectedAppointment.date) {
|
||||
const date = new Date(this.selectedAppointment.date).toLocaleDateString(
|
||||
"en-US",
|
||||
{ timeZone: "UTC", weekday: "long", month: "long", day: "numeric" }
|
||||
);
|
||||
tokens.splice(tokens.indexOf(token), 1, date);
|
||||
} else if (
|
||||
token.includes(INLINE_TIMESLOT_TOKEN) &&
|
||||
this.selectedAppontment.timeSlot
|
||||
this.selectedAppointment.timeSlot
|
||||
) {
|
||||
const timeslot =
|
||||
this.get12HourTimeFormat(this.selectedAppontment.timeSlot.startTime) +
|
||||
this.get12HourTimeFormat(this.selectedAppointment.timeSlot.startTime) +
|
||||
" - " +
|
||||
this.get12HourTimeFormat(this.selectedAppontment.timeSlot.endTime);
|
||||
this.get12HourTimeFormat(this.selectedAppointment.timeSlot.endTime);
|
||||
tokens.splice(tokens.indexOf(token), 1, timeslot);
|
||||
} else if (
|
||||
token.includes(INLINE_SERVICE_LENGTH_TOKEN) &&
|
||||
this.selectedAppontment.estimatedServiceMinutes
|
||||
this.selectedAppointment.estimatedServiceMinutes
|
||||
) {
|
||||
const inshopDurationTime = getDisplayTextForDurationLength(
|
||||
this.selectedAppontment.estimatedServiceMinutes.minimum,
|
||||
this.selectedAppontment.estimatedServiceMinutes.maximum
|
||||
this.selectedAppointment.estimatedServiceMinutes.minimum,
|
||||
this.selectedAppointment.estimatedServiceMinutes.maximum
|
||||
);
|
||||
tokens.splice(tokens.indexOf(token), 1, inshopDurationTime);
|
||||
}
|
||||
|
|
|
|||
223
src/layouts/schedule/recal-ack-modal/recal-ack-modal.vue
Normal file
223
src/layouts/schedule/recal-ack-modal/recal-ack-modal.vue
Normal file
|
|
@ -0,0 +1,223 @@
|
|||
// This modal is used to acknowledge the recal disclaimer during scheduling. // It will trigger when
|
||||
there is a glass part with requiresRecalibration = true // but there is no recalibration part in
|
||||
lineItems. This mostly occurs when // a zip code that does mobile only is selected but the vehicle
|
||||
requires // recalibration.
|
||||
<template>
|
||||
<div id="recal-ack-modal-container">
|
||||
<modal
|
||||
:ref="modalName"
|
||||
suppressPageScroll
|
||||
:onModalClosedCallback="onModalClosed"
|
||||
:footerButtonText="modalFooterText"
|
||||
:isFooterButtonPrimary="true"
|
||||
@footer-button-event="ackRecalDisclaimer">
|
||||
<p class="modal-body-inner" v-html="modalBodyText"></p>
|
||||
<div>
|
||||
<div class="learn-more">
|
||||
<a href="javascript:;" @click="learnMore = !learnMore">{{ learnMoreLabel }}</a>
|
||||
<div v-if="learnMore">{{ learnMoreText }}</div>
|
||||
</div>
|
||||
<buttonQuestion
|
||||
:questionText="questionText"
|
||||
:answers="answersFromCms"
|
||||
:groupName="groupName"
|
||||
buttonTypeString="listButtonHorizontal"
|
||||
v-model="selectedValues"
|
||||
isRequired />
|
||||
</div>
|
||||
<template v-slot:modal-header-slot>
|
||||
<span>{{ modalHeaderText }}</span>
|
||||
</template>
|
||||
</modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import modal from "@/digital-components/modal/modal";
|
||||
import buttonQuestion from "@/digital-components/button-question/button-question";
|
||||
import { RECAL_ACK_YES } from "@/constants/schedule-constants";
|
||||
|
||||
export default {
|
||||
name: "recal-ack-modal",
|
||||
props: {
|
||||
agreementWidgetName: String,
|
||||
modalWidgetName: String,
|
||||
groupName: String,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
learnMore: false,
|
||||
isModalOpen: true,
|
||||
isRecalAcknowledged: "",
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
openModal() {
|
||||
this.modal.openModal();
|
||||
},
|
||||
onModalClosed() {
|
||||
this.$emit("recal-ack-modal");
|
||||
},
|
||||
closeModal() {
|
||||
this.modal.closeModal();
|
||||
},
|
||||
setIsModalOpen(isOpen) {
|
||||
this.isModalOpen = isOpen;
|
||||
},
|
||||
ackRecalDisclaimer() {
|
||||
this.modal.closeModal();
|
||||
|
||||
if (this.isRecalAcknowledged === RECAL_ACK_YES) {
|
||||
this.pushEventToGA(
|
||||
this.GaCategories.RECAL_DISCLAIMER,
|
||||
this.GaActions.ACKNOWLEDGED,
|
||||
this.isRecalAcknowledged,
|
||||
true
|
||||
);
|
||||
|
||||
this.$emit("recalAcknowledged", this.isRecalAcknowledged);
|
||||
}
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
questionText() {
|
||||
return this.getCmsContent(this.agreementWidgetName, "QuestionText");
|
||||
},
|
||||
answersFromCms() {
|
||||
return this.getCmsContent(this.agreementWidgetName, "Answers");
|
||||
},
|
||||
learnMoreLabel() {
|
||||
return this.getCmsContent(this.modalWidgetName, "SubheaderText");
|
||||
},
|
||||
learnMoreText() {
|
||||
return this.getCmsContent(this.modalWidgetName, "FooterText2");
|
||||
},
|
||||
modalName() {
|
||||
return this.modalWidgetName;
|
||||
},
|
||||
modal() {
|
||||
return this.$refs[this.modalName];
|
||||
},
|
||||
modalHeaderText() {
|
||||
return this.getCmsContent(this.modalWidgetName, "HeaderText");
|
||||
},
|
||||
modalBodyText() {
|
||||
return this.getCmsContent(this.modalWidgetName, "BodyText");
|
||||
},
|
||||
modalFooterText() {
|
||||
return this.getCmsContent(this.modalWidgetName, "FooterText");
|
||||
},
|
||||
selectedValues: {
|
||||
get: function () {
|
||||
return this.isRecalAcknowledged;
|
||||
},
|
||||
set: function (newValue) {
|
||||
this.isRecalAcknowledged = this.answersFromCms.find(
|
||||
(a) => a.Name === newValue
|
||||
)?.Text;
|
||||
},
|
||||
},
|
||||
},
|
||||
components: { modal, buttonQuestion },
|
||||
};
|
||||
</script>
|
||||
<style lang="scss">
|
||||
#recal-ack-modal-container {
|
||||
.modal-component {
|
||||
@include media-breakpoint-up(md) {
|
||||
.modal-dialog {
|
||||
left: 0;
|
||||
align-content: center;
|
||||
flex-wrap: wrap;
|
||||
width: 34.875rem;
|
||||
transform: translate(0, 0);
|
||||
|
||||
.modal-content {
|
||||
border-radius: $border-radius-lg;
|
||||
}
|
||||
}
|
||||
}
|
||||
.modal-dialog {
|
||||
.modal-content {
|
||||
.btn-close {
|
||||
display: none;
|
||||
}
|
||||
.modal-header {
|
||||
width: 75%;
|
||||
padding: 0 1.25rem 0 1.25rem;
|
||||
flex-direction: column;
|
||||
& > span {
|
||||
color: $black;
|
||||
font-size: 1.25rem;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: $font-size-20;
|
||||
font-weight: $font-weight-normal;
|
||||
}
|
||||
}
|
||||
.modal-body {
|
||||
padding: 0.875rem 1.25rem 1.875rem 1.25rem;
|
||||
|
||||
.modal-body-inner {
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 1rem;
|
||||
background-color: #f4f4f4;
|
||||
font-size: $font-size-14;
|
||||
}
|
||||
p {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
|
||||
.button-inner-wrapper > .col:first-child .list-button-horizontal {
|
||||
border-bottom-left-radius: 50rem;
|
||||
border-top-left-radius: 50rem;
|
||||
}
|
||||
.col:first-of-type .list-button-horizontal .list-button-horizontal-content,
|
||||
.list-group:first-of-type
|
||||
.list-button-horizontal
|
||||
.list-button-horizontal-content {
|
||||
border-bottom-left-radius: 50rem;
|
||||
border-top-left-radius: 50rem;
|
||||
}
|
||||
|
||||
.button-inner-wrapper > .col:last-child .list-button-horizontal {
|
||||
border-bottom-right-radius: 50rem;
|
||||
border-top-right-radius: 50rem;
|
||||
}
|
||||
.col:last-of-type .list-button-horizontal .list-button-horizontal-content,
|
||||
.list-group:first-of-type
|
||||
.list-button-horizontal
|
||||
.list-button-horizontal-content {
|
||||
border-bottom-right-radius: 50rem;
|
||||
border-top-right-radius: 50rem;
|
||||
}
|
||||
|
||||
.learn-more {
|
||||
padding-bottom: 0;
|
||||
font-size: 0.875rem;
|
||||
a {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
.question-text {
|
||||
margin-top: 0.875rem;
|
||||
}
|
||||
}
|
||||
.modal-footer {
|
||||
flex-flow: wrap-reverse;
|
||||
padding-top: 0;
|
||||
padding-bottom: 2rem;
|
||||
}
|
||||
.modal-footer .modal-footer-button:hover {
|
||||
background-color: $red-1000;
|
||||
border-color: $red-1100;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
183
src/layouts/schedule/schedule.spec.js
Normal file
183
src/layouts/schedule/schedule.spec.js
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
// Components
|
||||
import schedule from "@/layouts/schedule/schedule.vue";
|
||||
|
||||
// Supporting Files
|
||||
import { shallowMount } from "@vue/test-utils";
|
||||
import { getMountOptions } from "@/helpers/unit-test-helper.js";
|
||||
import { routeData } from "@/router/constants/routes";
|
||||
import store from "@/store";
|
||||
import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
|
||||
|
||||
const storeMocked = {
|
||||
getters: {
|
||||
payment: { isInsurance: false },
|
||||
order: {
|
||||
payment: { isInsurance: false, insuranceCoverage: { isVerified: false } },
|
||||
referralNumber: "",
|
||||
serviceLocation: {
|
||||
address: "",
|
||||
address2: "",
|
||||
city: "",
|
||||
state: "",
|
||||
zipCode: "00000",
|
||||
zipCodeCtu: "000",
|
||||
appointmentType: null,
|
||||
provider: {},
|
||||
isVehicleProtected: false,
|
||||
},
|
||||
schedule: {
|
||||
date: "",
|
||||
routeCode: "",
|
||||
startTime: "",
|
||||
endTime: "",
|
||||
jobMinMinutes: null,
|
||||
jobMaxMinutes: null,
|
||||
},
|
||||
policy: {
|
||||
isItac: false,
|
||||
isNoComp: false,
|
||||
},
|
||||
damage: { isRepair: false },
|
||||
lineItems: { glassParts: [] },
|
||||
},
|
||||
vehicle: {
|
||||
cardId: "",
|
||||
},
|
||||
lineItems: {
|
||||
supportingItems: [],
|
||||
},
|
||||
applicationUser: {
|
||||
experiments: [],
|
||||
},
|
||||
isMobileAppointment: false,
|
||||
},
|
||||
};
|
||||
|
||||
jest.mock("@/helpers/heritage-integration/navigation-helper", () => ({
|
||||
navigateToHeritageFunnel: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("schedule.vue", () => {
|
||||
describe("navigation", () => {
|
||||
test("backButtonAction => non-insurance: navigateWithoutSaving called", () => {
|
||||
// Arrange
|
||||
const { wrapper, mocks } = setupMocks();
|
||||
|
||||
// Act
|
||||
wrapper.vm.backButtonAction();
|
||||
|
||||
// Assert
|
||||
expect(wrapper.vm.$router.navigateWithoutSaving).toHaveBeenCalledWith(
|
||||
mocks.navigationScenarios.CLICKED_BACK,
|
||||
routeData.SCHEDULE.name
|
||||
);
|
||||
});
|
||||
|
||||
test("backButtonAction => insurance verified: navigateToHeritageFunnel called", () => {
|
||||
// Arrange
|
||||
store.getters.order.payment.isInsurance = true;
|
||||
store.getters.order.payment.insuranceCoverage = { isVerified: true };
|
||||
store.getters.order.referralNumber = "";
|
||||
const { wrapper } = setupMocks();
|
||||
|
||||
// Act
|
||||
wrapper.vm.backButtonAction();
|
||||
|
||||
// Assert
|
||||
expect(navigateToHeritageFunnel).toHaveBeenCalledWith({
|
||||
shouldSaveSession: false,
|
||||
pageNameToLog: "schedule",
|
||||
navType: "back",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("computed: isForwardActionDisabled", () => {
|
||||
test("should be true when routeCode missing or appointmentType not set", async () => {
|
||||
const { wrapper } = setupMocks();
|
||||
await wrapper.setData({
|
||||
selectedTimeSlotInfo: { timeSlot: { routeCode: null } },
|
||||
appointmentType: null,
|
||||
});
|
||||
expect(wrapper.vm.isForwardActionDisabled).toBe(true);
|
||||
});
|
||||
|
||||
test("should be false when routeCode present and appointmentType set", async () => {
|
||||
const { wrapper } = setupMocks();
|
||||
await wrapper.setData({
|
||||
selectedTimeSlotInfo: { timeSlot: { routeCode: "RC123" } },
|
||||
appointmentType: "SomeType",
|
||||
});
|
||||
expect(wrapper.vm.isForwardActionDisabled).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("computed: selectedRouteCodeData", () => {
|
||||
test("should return null when routeCode not set", async () => {
|
||||
const { wrapper } = setupMocks();
|
||||
await wrapper.setData({
|
||||
selectedTimeSlotInfo: { timeSlot: { routeCode: null } },
|
||||
});
|
||||
expect(wrapper.vm.selectedRouteCodeData).toBeNull();
|
||||
});
|
||||
|
||||
test("should return routeCode and premium flag when set", async () => {
|
||||
const { wrapper } = setupMocks();
|
||||
await wrapper.setData({
|
||||
selectedTimeSlotInfo: {
|
||||
timeSlot: { routeCode: "RC999" },
|
||||
isPremiumAppointment: true,
|
||||
},
|
||||
});
|
||||
expect(wrapper.vm.selectedRouteCodeData).toEqual({
|
||||
routeCode: "RC999",
|
||||
isPremiumAppointment: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("forwardButtonAction (early exit path)", () => {
|
||||
test("shows recal acknowledgement popup and does not navigate when required", async () => {
|
||||
const { wrapper } = setupMocks();
|
||||
// Force the recal-ack modal path
|
||||
wrapper.vm.showRecalAcknowledgementModal = jest.fn(() => true);
|
||||
await wrapper.setData({ isRecalAcknowledgedForScheduling: null });
|
||||
|
||||
await wrapper.vm.forwardButtonAction();
|
||||
|
||||
expect(wrapper.vm.showRecalAcknowledgementPopup).toBe(true);
|
||||
expect(wrapper.vm.$router.navigateWithSaving).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function setupMocks(mountOptionsMockData = {}) {
|
||||
const route = { name: "schedule" };
|
||||
const defaultMountOptions = {
|
||||
route: route,
|
||||
router: {
|
||||
navigate: jest.fn(),
|
||||
navigateWithSaving: jest.fn(),
|
||||
navigateWithoutSaving: jest.fn(),
|
||||
},
|
||||
store: { ...storeMocked },
|
||||
};
|
||||
const mockBaseMixin = {
|
||||
methods: {
|
||||
getTierOnePackagePrice: jest.fn(),
|
||||
filterOutFees: jest.fn(),
|
||||
getCmsContent: jest.fn(),
|
||||
},
|
||||
};
|
||||
const baseMountOptions = getMountOptions({
|
||||
...defaultMountOptions,
|
||||
...mountOptionsMockData,
|
||||
});
|
||||
|
||||
baseMountOptions.global.mixins = [mockBaseMixin];
|
||||
baseMountOptions.global.mocks.pageName = route.name;
|
||||
const wrapper = shallowMount(schedule, baseMountOptions);
|
||||
wrapper.getCmsContent = jest.fn();
|
||||
|
||||
return { wrapper, mocks: baseMountOptions.global?.mocks || {} };
|
||||
}
|
||||
|
|
@ -6,6 +6,12 @@
|
|||
modalWidgetName="MobileFirstModalWidget"
|
||||
ref="mobileFirstModal"
|
||||
@confirm-appointment="updateTimeSlotandNavigateForward" />
|
||||
<recalAckModal
|
||||
modalWidgetName="RecalAckModalWidget"
|
||||
agreementWidgetName="RecalAgreementQuestionWidget"
|
||||
groupName="recalAckGroup"
|
||||
@recalAcknowledged="updateRecalAcknowledgedAndNavigateForward"
|
||||
ref="recalAckModal" />
|
||||
<div
|
||||
class="container page-container-grouped-styles"
|
||||
:class="[isMobileFirstModalOpen ? 'hidden-background' : '']">
|
||||
|
|
@ -22,6 +28,7 @@
|
|||
cmsWidgetName="ScheduleYourServiceWidget"
|
||||
id="schedule-your-service" />
|
||||
<appointmentTypeQuestion
|
||||
v-if="hasSelectableDatesLoaded"
|
||||
v-model="appointmentTypeFromAppointmentTypeQuestion"
|
||||
v-show="isAppointmentTypeDisplayed"
|
||||
:isServiceableMobile="isServiceableMobile"
|
||||
|
|
@ -91,15 +98,11 @@
|
|||
linkWidgetName="ServiceZipLinkWidget"
|
||||
modalWidgetName="ServiceZipModalWidget" />
|
||||
</div>
|
||||
<div
|
||||
class="row your-shop-location"
|
||||
v-if="appointmentType && appointmentType !== appointmentTypeStrings.MOBILE">
|
||||
<div class="row your-shop-location" v-if="showShopLocationComponent">
|
||||
<div class="col-12 col-md-10 col-lg-8 col-xl-7">
|
||||
<div>
|
||||
<shopLocation
|
||||
v-if="
|
||||
appointmentType && appointmentType !== appointmentTypeStrings.MOBILE
|
||||
"
|
||||
v-if="showShopLocationComponent"
|
||||
:selectedShop="selectedShopAnswer"
|
||||
:selectedProviderNumberFromParent="selectedProvider?.providerNumber"
|
||||
:shopProviderDataFromParent="shopProviderData"
|
||||
|
|
@ -133,8 +136,6 @@
|
|||
:isOvernightDropoff="isOvernightDropoff" />
|
||||
</div>
|
||||
<datePicker
|
||||
:currentZip="zipCode"
|
||||
:currentProviderNumber="selectedProvider?.providerNumber"
|
||||
v-show="appointmentType"
|
||||
customComponentId="dateQuestion"
|
||||
selectableDatesSetting="custom"
|
||||
|
|
@ -144,11 +145,12 @@
|
|||
class="text-link-small"
|
||||
:getMoreDatesCallback="getMoreScheduleData"
|
||||
validationRules="date-required"
|
||||
@date-clicked="handleDateClicked"
|
||||
@date-selected="handleDateSelected"
|
||||
:pricingByDayBasePrice="pricingByDayBasePrice"
|
||||
:pricingByDayUpcharge="pricingByDayUpcharge"
|
||||
:showPricingByDay="showPricingByDay"
|
||||
:isPricingByDayExperiment="isPricingByDayExperiment" />
|
||||
:isPricingByDayExperiment="isPricingByDayExperiment"
|
||||
:loadingStatus="calendarLoadingStatus" />
|
||||
<timeSlotQuestion
|
||||
ref="timeSlotModalQuestion"
|
||||
v-if="showTimeSlotQuestion"
|
||||
|
|
@ -167,7 +169,8 @@
|
|||
:timeSlotsForSelectedDate="timeSlotsForSelectedDate"
|
||||
:isSameDay="isSameDay"
|
||||
:selectedRouteCodeData="selectedRouteCodeData"
|
||||
@waitListRequested="handleWaitListRequested" />
|
||||
@waitListRequested="handleWaitListRequested"
|
||||
:lazyLoadAmountToLoad="10" />
|
||||
<navbar
|
||||
cmsWidgetName="FunnelFooterWidget"
|
||||
ref="navbar"
|
||||
|
|
@ -190,6 +193,7 @@ import shopLocation from "@/layouts/schedule/shop-location/shop-location";
|
|||
import timeSlotQuestion from "@/layouts/schedule/time-slot-question/time-slot-question.vue";
|
||||
import durationTextBlock from "@/layouts/schedule/duration-text-block/duration-text-block.vue";
|
||||
import mobileFirstModal from "@/layouts/schedule/mobile-first-modal/mobile-first-modal.vue";
|
||||
import recalAckModal from "@/layouts/schedule/recal-ack-modal/recal-ack-modal.vue";
|
||||
|
||||
import shopQuestion from "@/layouts/service-location/shop-question/shop-question";
|
||||
import shopQuestionPopup from "@/layouts/service-location/shop-question/shop-question-popup";
|
||||
|
|
@ -216,6 +220,7 @@ import {
|
|||
RouteCodeFlags,
|
||||
PREMIUM_FEE_PART_TYPE,
|
||||
PRICING_BY_DAY_PART_TYPE,
|
||||
RECAL_ACK_YES,
|
||||
} from "@/constants/schedule-constants";
|
||||
|
||||
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
|
||||
|
|
@ -242,6 +247,7 @@ import {
|
|||
isDropOffRouteCode,
|
||||
getTodayDate,
|
||||
} from "@/layouts/schedule/helpers/schedule-helper";
|
||||
import { containsRecalParts, anyPartWithRequiresRecalFlag } from "@/helpers/recal-helper";
|
||||
|
||||
import { DAYS_OF_WEEK } from "@/digital-components/date-picker/mixins/constants";
|
||||
import { required } from "@/helpers/validation-rules";
|
||||
|
|
@ -287,6 +293,7 @@ const getScheduleApiResponse = async ({
|
|||
startDateString,
|
||||
endDateString,
|
||||
providerNumber,
|
||||
inshopProviderNumber,
|
||||
zipCode,
|
||||
includeMobileTimeSlots = true,
|
||||
includeInshopTimeSlots = true,
|
||||
|
|
@ -329,7 +336,7 @@ const getScheduleApiResponse = async ({
|
|||
startDate: apiStartDate,
|
||||
endDate: apiEndDate,
|
||||
shopAppointmentType: "InshopOrDropoff",
|
||||
providerNumber: providerNumber,
|
||||
providerNumber: inshopProviderNumber,
|
||||
},
|
||||
};
|
||||
if (apiStartDate < apiEndDate) {
|
||||
|
|
@ -429,19 +436,19 @@ export default {
|
|||
name: "schedule",
|
||||
data() {
|
||||
return {
|
||||
isRecalAcknowledgedForScheduling: this.getIsRecalAcknowledgedForSchedulingFromStore(),
|
||||
selectedDate: this.getSelectedDateFromStore(),
|
||||
selectedTimeSlotInfo: this.getSelectedTimeSlotInfo(),
|
||||
mobilePremiumAppointmentFee: null,
|
||||
waitListRequested: null,
|
||||
displayWaitList: null,
|
||||
pricingByDayUpchargeLineItem: null,
|
||||
includePricingByDayUpcharge: null,
|
||||
isPricingByDayExperiment: null,
|
||||
pricingByDayBasePrice: null,
|
||||
pricingByDayUpcharge: null,
|
||||
showPricingByDay: null,
|
||||
selectableDatesInshop: [],
|
||||
selectableDatesMobile: [],
|
||||
selectableDatesInshop: {},
|
||||
selectableDatesMobile: {},
|
||||
preSelectedDate: null,
|
||||
streetAddress: this.getServiceAddressFromStore(),
|
||||
apartmentNumberOrBusinessName: this.getServiceAddress2FromStore(),
|
||||
|
|
@ -474,6 +481,7 @@ export default {
|
|||
experimentSettings.SHOW_MOBILE_FIRST_APPT,
|
||||
"true"
|
||||
),
|
||||
calendarLoadingStatus: "none",
|
||||
};
|
||||
},
|
||||
async beforeRouteEnter(to, from, next) {
|
||||
|
|
@ -584,6 +592,8 @@ export default {
|
|||
|
||||
// Call the "next" function to complete the transition to this page.
|
||||
next((vm) => {
|
||||
vm.showLoadingModal();
|
||||
vm.calendarLoadingStatus = "initial";
|
||||
vm.setCmsContent(resultMap.cmsContent);
|
||||
vm.$refs.locationAlerts.initializeComponent(resultMap.alertReasons);
|
||||
vm.mobilePremiumAppointmentFee = pricedPremiumFee;
|
||||
|
|
@ -604,6 +614,9 @@ export default {
|
|||
);
|
||||
vm.initializeDatePicker().then(() => {
|
||||
vm.showMobileFirstModal();
|
||||
vm.hideLoadingModal();
|
||||
vm.calendarLoadingStatus = "none";
|
||||
if (!vm.preSelectedDate) vm.setSelectedDateToFirstAvailable();
|
||||
});
|
||||
});
|
||||
},
|
||||
|
|
@ -620,8 +633,25 @@ export default {
|
|||
this.handleZipCodeChange(newValue);
|
||||
},
|
||||
},
|
||||
showShopLocationComponent() {
|
||||
// If we get no inshop or mobile data, then show the shoplocation component so the user is not
|
||||
// looking at a half blank screen with no way to try another zip/location.
|
||||
if (
|
||||
(this.selectableDatesInshop === undefined &&
|
||||
this.selectableDatesMobile === undefined) ||
|
||||
(this.selectableDatesInshop?.days?.length === 0 &&
|
||||
this.selectableDatesMobile?.days?.length === 0) ||
|
||||
(this.appointmentType && this.appointmentType !== AppointmentTypeStrings.MOBILE)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
isMobileSelected() {
|
||||
return this.appointmentType == AppointmentTypeStrings.MOBILE;
|
||||
return (
|
||||
this.appointmentType == AppointmentTypeStrings.MOBILE ||
|
||||
(this.isServiceableMobile && !this.isServiceableInshop)
|
||||
);
|
||||
},
|
||||
isServiceableMobile() {
|
||||
if (this.isRecalibrationServiceableMobile !== null) {
|
||||
|
|
@ -633,15 +663,18 @@ export default {
|
|||
return this.isGlassServiceableMobile;
|
||||
}
|
||||
},
|
||||
closestInshopProviderNumber() {
|
||||
return this.shopProviderData?.shopProviders[0]?.providerNumber;
|
||||
},
|
||||
showTimeSlotQuestion() {
|
||||
if (
|
||||
this.selectedDate &&
|
||||
this.selectedDate.includes("mobile") &&
|
||||
!this.isMobileSelected
|
||||
this.appointmentTypeFromAppointmentTypeQuestion &&
|
||||
this.timeSlotsForSelectedDate
|
||||
) {
|
||||
this.setSelectedDateToFirstAvailable();
|
||||
return true;
|
||||
}
|
||||
return this.selectedDate && this.appointmentTypeFromAppointmentTypeQuestion;
|
||||
return false;
|
||||
},
|
||||
isMobileStaticRecalibrationApplicable() {
|
||||
return (
|
||||
|
|
@ -869,6 +902,60 @@ export default {
|
|||
? this.$refs.mobileFirstModal?.getIsModalOpen()
|
||||
: false;
|
||||
},
|
||||
displayWaitList() {
|
||||
if (!this.appointmentType) return false;
|
||||
const firstMobileDateString =
|
||||
Array.isArray(this.selectableDatesMobile.days) &&
|
||||
this.selectableDatesMobile.days.length > 0
|
||||
? this.selectableDatesMobile.days[0].date
|
||||
: null;
|
||||
const firstInshopDateString =
|
||||
Array.isArray(this.selectableDatesInshop.days) &&
|
||||
this.selectableDatesInshop.days.length > 0
|
||||
? this.selectableDatesInshop.days[0].date
|
||||
: null;
|
||||
const dateString =
|
||||
this.isMobileSelected && firstMobileDateString
|
||||
? firstMobileDateString
|
||||
: firstInshopDateString;
|
||||
|
||||
if (
|
||||
experimentMixin.methods.hasSettingEqualTo(
|
||||
experimentSettings.DISPLAY_WAITLIST,
|
||||
"true"
|
||||
) &&
|
||||
dateString
|
||||
) {
|
||||
const [year, month, day] = dateString.split("-").map(Number);
|
||||
const targetDate = new Date(year, month - 1, day);
|
||||
const currentDate = getTodayDate();
|
||||
const futureDate = new Date(currentDate);
|
||||
const experimentThresholdDays = experimentMixin.methods.hasSetting(
|
||||
experimentSettings.WAITLIST_THRESHOLD_DAYS
|
||||
)
|
||||
? parseInt(
|
||||
experimentMixin.methods.getSettingValue(
|
||||
experimentSettings.WAITLIST_THRESHOLD_DAYS
|
||||
)
|
||||
)
|
||||
: 0;
|
||||
futureDate.setDate(currentDate.getDate() + experimentThresholdDays);
|
||||
|
||||
if (targetDate >= futureDate) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
hasSelectableDatesLoaded() {
|
||||
if (this.isMobileSelected) {
|
||||
return this.selectableDatesMobile?.days?.length > 0;
|
||||
} else {
|
||||
return this.selectableDatesInshop?.days?.length > 0;
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
splitCopyOnCMSPlaceHolder,
|
||||
|
|
@ -1130,37 +1217,17 @@ export default {
|
|||
);
|
||||
}
|
||||
},
|
||||
getTimeSlotInfo() {
|
||||
// Get the current date
|
||||
const currentDate = getTodayDate();
|
||||
|
||||
// Add 10 days to the current date
|
||||
currentDate.setDate(currentDate.getDate() + 10);
|
||||
|
||||
// Format the date as yyyy-mm-dd
|
||||
const year = currentDate.getFullYear();
|
||||
const month = String(currentDate.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(currentDate.getDate()).padStart(2, "0");
|
||||
|
||||
const formattedDate = `${year}-${month}-${day}`;
|
||||
|
||||
return {
|
||||
date: formattedDate,
|
||||
endTime: "12:00",
|
||||
jobMaxMinutes: "180",
|
||||
jobMinMinutes: "120",
|
||||
routeCode: "3357I-03357-M-I*20988*AM",
|
||||
startTime: "08:00",
|
||||
};
|
||||
},
|
||||
onShopSelected(shopQuestionPopUpData) {
|
||||
this.resetWaitlist();
|
||||
this.preSelectedDate = null;
|
||||
this.calendarLoadingStatus = "reinitial";
|
||||
this.shopProviderData = shopQuestionPopUpData.shopProviderData;
|
||||
const oldProvider = this.selectedProvider;
|
||||
const selectedProvider = this.shopProviderData.shopProviders.find((shopProvider) => {
|
||||
return shopProvider.providerNumber === shopQuestionPopUpData.selectedProviderNumber;
|
||||
});
|
||||
const didProviderNumberChange =
|
||||
oldProvider?.providerNumber !== selectedProvider?.providerNumber;
|
||||
|
||||
// No zipCodeData comes back if zip was not changed
|
||||
if (shopQuestionPopUpData.zipCodeData) {
|
||||
|
|
@ -1169,24 +1236,32 @@ export default {
|
|||
selectedProvider
|
||||
);
|
||||
} else {
|
||||
const didProviderNumberChange =
|
||||
oldProvider?.providerNumber !== selectedProvider.providerNumber;
|
||||
if (didProviderNumberChange) {
|
||||
this.updateSelectedProvider(selectedProvider);
|
||||
this.initializeDatePicker();
|
||||
}
|
||||
}
|
||||
this.initializeDatePicker().then(() => {
|
||||
this.showMobileFirstModal();
|
||||
this.hideLoadingModal();
|
||||
this.calendarLoadingStatus = "none";
|
||||
this.setSelectedDateToFirstAvailable();
|
||||
});
|
||||
},
|
||||
async getMoreScheduleData(startDate, endDate) {
|
||||
// called only when "View more dates" is clicked; not on initial page load
|
||||
this.calendarLoadingStatus = "more";
|
||||
const moreShopTimeSlots = await getScheduleApiResponse({
|
||||
startDateString: startDate,
|
||||
endDateString: endDate,
|
||||
providerNumber: this.selectedProvider?.providerNumber,
|
||||
inshopProviderNumber: this.isMobileSelected
|
||||
? this.closestInshopProviderNumber
|
||||
: this.selectedProvider?.providerNumber,
|
||||
zipCode: this.zipCode,
|
||||
includeMobileTimeSlots: this.isServiceableMobile,
|
||||
includeInshopTimeSlots: this.isServiceableInshop || this.isServiceableDropoff,
|
||||
});
|
||||
this.calendarLoadingStatus = "none";
|
||||
|
||||
// ADD API CALL RESULTS TO EXISTING DATE DATA
|
||||
this.selectableDatesInshop.days = this.selectableDatesInshop.days.concat(
|
||||
|
|
@ -1202,8 +1277,6 @@ export default {
|
|||
async initializeDatePicker() {
|
||||
this.isShowMobileFirstAppt && this.showLoadingModal();
|
||||
|
||||
this.selectedDate = null;
|
||||
|
||||
const includeMobileTimeSlots = this.isServiceableMobile;
|
||||
const includeInshopTimeSlots = this.isServiceableInshop || this.isServiceableDropoff;
|
||||
const datePickerInitialData = await this.$refs.datePicker.loadInitialData({
|
||||
|
|
@ -1213,6 +1286,9 @@ export default {
|
|||
getSelectableDatesCallback: getScheduleApiResponse,
|
||||
preSelectedDate: this.preSelectedDate,
|
||||
providerNumber: this.selectedProvider?.providerNumber,
|
||||
inshopProviderNumber: this.isMobileSelected
|
||||
? this.closestInshopProviderNumber
|
||||
: this.selectedProvider?.providerNumber,
|
||||
zipCode: this.zipCode,
|
||||
includeMobileTimeSlots: includeMobileTimeSlots,
|
||||
includeInshopTimeSlots: includeInshopTimeSlots,
|
||||
|
|
@ -1224,7 +1300,6 @@ export default {
|
|||
await datePickerInitialData.initialShopTimeSlotsResponse.inshopTimeSlotsData;
|
||||
this.selectableDatesMobile =
|
||||
await datePickerInitialData.initialShopTimeSlotsResponse.mobileTimeSlotsData;
|
||||
this.setDisplayWaitList();
|
||||
|
||||
if (this.preSelectedDate) {
|
||||
this.selectedDate = this.preSelectedDate;
|
||||
|
|
@ -1236,11 +1311,11 @@ export default {
|
|||
) {
|
||||
await this.$nextTick();
|
||||
await this.$refs.datePicker.showAnotherMonth();
|
||||
this.setDisplayWaitList();
|
||||
}
|
||||
}
|
||||
|
||||
this.isShowMobileFirstAppt && this.hideLoadingModal();
|
||||
},
|
||||
getIsRecalAcknowledgedForSchedulingFromStore() {
|
||||
return store.getters.order.isRecalAcknowledgedForScheduling;
|
||||
},
|
||||
getScheduleApiResponse,
|
||||
getServiceZipCtuCodeFromStore() {
|
||||
|
|
@ -1251,8 +1326,8 @@ export default {
|
|||
if (isMobileSelected === undefined) {
|
||||
isMobileSelected = this.appointmentType === AppointmentTypeStrings.MOBILE;
|
||||
}
|
||||
let storedDate = store.getters.order.schedule.date;
|
||||
if (isMobileSelected) storedDate += "-mobile";
|
||||
let storedDate = store.getters.order.schedule?.date;
|
||||
if (isMobileSelected && storedDate) storedDate += "-mobile";
|
||||
return storedDate;
|
||||
},
|
||||
getSelectedTimeSlotInfo() {
|
||||
|
|
@ -1360,7 +1435,25 @@ export default {
|
|||
hideLoadingModal() {
|
||||
this.$refs.loadingModal.hideModal();
|
||||
},
|
||||
async closeRecalAcknowledgementPopup() {
|
||||
this.showRecalAcknowledgementPopup = false;
|
||||
},
|
||||
async forwardButtonAction() {
|
||||
const showRecalAcknowledgementModal = this.showRecalAcknowledgementModal();
|
||||
if (
|
||||
showRecalAcknowledgementModal &&
|
||||
this.isRecalAcknowledgedForScheduling !== RECAL_ACK_YES
|
||||
) {
|
||||
this.showRecalAcknowledgementPopup = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this.dispatchStoreAction(
|
||||
storeActions.SAVE_IS_RECAL_ACKNOWLEDGED_FOR_SCHEDULING,
|
||||
this.isRecalAcknowledgedForScheduling,
|
||||
false
|
||||
);
|
||||
|
||||
this.navigatingForward = true;
|
||||
var ctu = this.zipCodeCtu;
|
||||
if (this.appointmentType === AppointmentTypeStrings.MOBILE) {
|
||||
|
|
@ -1489,39 +1582,6 @@ export default {
|
|||
}
|
||||
this.showLoadingModal();
|
||||
},
|
||||
setDisplayWaitList() {
|
||||
const dateString = this.isMobileSelected
|
||||
? this.selectableDatesMobile?.days[0]?.date
|
||||
: this.selectableDatesInshop?.days[0]?.date;
|
||||
if (
|
||||
experimentMixin.methods.hasSettingEqualTo(
|
||||
experimentSettings.DISPLAY_WAITLIST,
|
||||
"true"
|
||||
) &&
|
||||
dateString
|
||||
) {
|
||||
const [year, month, day] = dateString.split("-").map(Number);
|
||||
const targetDate = new Date(year, month - 1, day);
|
||||
const currentDate = getTodayDate();
|
||||
const futureDate = new Date(currentDate);
|
||||
const experimentThresholdDays = experimentMixin.methods.hasSetting(
|
||||
experimentSettings.WAITLIST_THRESHOLD_DAYS
|
||||
)
|
||||
? parseInt(
|
||||
experimentMixin.methods.getSettingValue(
|
||||
experimentSettings.WAITLIST_THRESHOLD_DAYS
|
||||
)
|
||||
)
|
||||
: 0;
|
||||
futureDate.setDate(currentDate.getDate() + experimentThresholdDays);
|
||||
|
||||
if (targetDate >= futureDate) {
|
||||
this.displayWaitList = true;
|
||||
} else {
|
||||
this.displayWaitList = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
updateSupportingItems() {
|
||||
const supportingItems = this.getSupportingItems();
|
||||
|
||||
|
|
@ -1591,28 +1651,39 @@ export default {
|
|||
handleWaitListRequested(value) {
|
||||
this.waitListRequested = value;
|
||||
},
|
||||
handleDateClicked(date) {
|
||||
// do something to mark this as upcharge day or not...
|
||||
if (date.isPricingByDayUpchargeDay) {
|
||||
this.includePricingByDayUpcharge = true;
|
||||
} else {
|
||||
this.includePricingByDayUpcharge = false;
|
||||
handleDateSelected(date) {
|
||||
const previousDate = this.selectedDate;
|
||||
// check if date actually changed
|
||||
if (previousDate !== date.dateString) {
|
||||
this.selectedTimeSlotInfo = this.getEmptyTimeSlot();
|
||||
this.updateFooterButtonText(this.selectedTimeSlotInfo);
|
||||
}
|
||||
|
||||
// TODO: REMOVE AS PART OF CASH-1634
|
||||
// // do something to mark this as upcharge day or not...
|
||||
// if (date.isPricingByDayUpchargeDay) {
|
||||
// this.includePricingByDayUpcharge = true;
|
||||
// } else {
|
||||
// this.includePricingByDayUpcharge = false;
|
||||
// }
|
||||
},
|
||||
getEmptyTimeSlot() {
|
||||
return {
|
||||
timeSlot: {
|
||||
date: null,
|
||||
routeCode: null,
|
||||
startTime: null,
|
||||
endTime: null,
|
||||
jobMaxMinutes: null,
|
||||
jobMinMinutes: null,
|
||||
},
|
||||
isPremiumAppointment: null,
|
||||
};
|
||||
},
|
||||
updateTimeSlot(timeSlotObj) {
|
||||
if (!timeSlotObj?.routeCode) {
|
||||
this.appointmentType = AppointmentTypeStrings.IN_SHOP_OR_DROP_OFF;
|
||||
this.selectedTimeSlotInfo = {
|
||||
timeSlot: {
|
||||
date: null,
|
||||
routeCode: null,
|
||||
startTime: null,
|
||||
endTime: null,
|
||||
jobMaxMinutes: null,
|
||||
jobMinMinutes: null,
|
||||
},
|
||||
isPremiumAppointment: null,
|
||||
};
|
||||
this.selectedTimeSlotInfo = this.getEmptyTimeSlot();
|
||||
return;
|
||||
}
|
||||
const timeSlot = this.timeSlotsForSelectedDate?.timeSlots?.find(
|
||||
|
|
@ -1662,6 +1733,8 @@ export default {
|
|||
this.updateSelectedProvider();
|
||||
},
|
||||
updateSelectedProvider(newShopProvider) {
|
||||
if (!this.appointmentTypeStrings) return;
|
||||
|
||||
if (newShopProvider) {
|
||||
this.selectedProvider = newShopProvider;
|
||||
} else if (this.appointmentType === this.appointmentTypeStrings.MOBILE) {
|
||||
|
|
@ -1682,8 +1755,29 @@ export default {
|
|||
this.selectedProvider = this.shopProviderData.shopProviders[0];
|
||||
}
|
||||
},
|
||||
handleZipCodeChange(newZipCode) {
|
||||
async handleZipCodeChange(newZipCode) {
|
||||
// if we were showing the recal ack modal and now are changing the zip, we need to navigate
|
||||
// to service-zip so we can get call parts again to add the recal part if needed
|
||||
if (this.shouldShowRecalAckModal()) {
|
||||
await baseMixin.methods.dispatchStoreAction(
|
||||
storeActions.SAVE_SERVICE_ZIP_CODE_INFO,
|
||||
{
|
||||
zipCode: newZipCode.zipCode,
|
||||
state: newZipCode.state,
|
||||
zipCodeCtu: newZipCode.zipCodeCtu,
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
this.$router.navigateWithSaving(
|
||||
this.navigationScenarios.ZIP_CODE_CHANGED_RECAL_ACK,
|
||||
this.pageName
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.resetMobileLocation();
|
||||
this.calendarLoadingStatus = "initial";
|
||||
|
||||
getShopProviderData(newZipCode.zipCode, this.pageName).then((result) => {
|
||||
this.shopProviderData = result.data;
|
||||
|
|
@ -1694,6 +1788,9 @@ export default {
|
|||
this.selectedDate = null;
|
||||
this.initializeDatePicker().then(() => {
|
||||
this.showMobileFirstModal();
|
||||
this.hideLoadingModal();
|
||||
this.calendarLoadingStatus = "none";
|
||||
this.setSelectedDateToFirstAvailable();
|
||||
});
|
||||
});
|
||||
},
|
||||
|
|
@ -1705,10 +1802,12 @@ export default {
|
|||
this.zipContainsMilitaryBase = newZipCode.containsMilitaryBase;
|
||||
this.selectedDate = null;
|
||||
this.selectedProvider = newProvider;
|
||||
this.initializeDatePicker();
|
||||
},
|
||||
handleAppointmentTypeChange(newAppointmentType) {
|
||||
this.updateFooterButtonText();
|
||||
// if time appointment type changes, clear any selected time slot
|
||||
this.selectedTimeSlotInfo = this.getEmptyTimeSlot();
|
||||
|
||||
if (newAppointmentType === AppointmentTypeStrings.MOBILE) {
|
||||
// Remember last shop selected if previous selection was inshop/dropoff
|
||||
if (
|
||||
|
|
@ -1717,17 +1816,6 @@ export default {
|
|||
AppointmentTypeStrings.IN_SHOP_OR_DROP_OFF) &&
|
||||
this.selectedProvider
|
||||
) {
|
||||
this.selectedTimeSlotInfo = {
|
||||
timeSlot: {
|
||||
date: null,
|
||||
routeCode: null,
|
||||
startTime: null,
|
||||
endTime: null,
|
||||
jobMaxMinutes: null,
|
||||
jobMinMinutes: null,
|
||||
},
|
||||
isPremiumAppointment: null,
|
||||
};
|
||||
this.lastSelectedInshopOrDropoffProvider = this.selectedProvider;
|
||||
}
|
||||
this.appointmentType = AppointmentTypeStrings.MOBILE;
|
||||
|
|
@ -1746,16 +1834,45 @@ export default {
|
|||
this.appointmentType = AppointmentTypeStrings.IN_SHOP_OR_DROP_OFF;
|
||||
}
|
||||
// make sure a selectedProvider exists
|
||||
this.updateSelectedProvider(this.lastSelectedInshopOrDropoffProvider);
|
||||
if (this.lastSelectedInshopOrDropoffProvider) {
|
||||
this.updateSelectedProvider(this.lastSelectedInshopOrDropoffProvider);
|
||||
} else {
|
||||
this.updateSelectedProvider();
|
||||
}
|
||||
} else {
|
||||
this.appointmentType = null;
|
||||
}
|
||||
this.setSelectedDateToFirstAvailable();
|
||||
this.setDisplayWaitList();
|
||||
this.setSelectedDateToFirstAvailable(); // v-if on appointmentTypeQuestion ensures dates have been loaded by now
|
||||
},
|
||||
setSelectedDateToFirstAvailable() {
|
||||
this.selectedDate = this.getFirstAvailableDate();
|
||||
},
|
||||
showRecalAcknowledgementModal() {
|
||||
if (this.shouldShowRecalAckModal()) {
|
||||
this.$refs.navbar.removeLoader();
|
||||
this.$refs.recalAckModal.openModal();
|
||||
return true;
|
||||
}
|
||||
|
||||
this.closeRecalAcknowledgementPopup();
|
||||
return false;
|
||||
},
|
||||
shouldShowRecalAckModal() {
|
||||
// if any of the glass parts contain an item with the requiresRecalibration flag set to true
|
||||
// and the order does not contain a recalibration part, show the recalibration acknowledgement modal
|
||||
const order = this.$store.getters.order;
|
||||
const containsRecalPart = containsRecalParts(order.lineItems);
|
||||
const hasPartWithRecalRequirement = anyPartWithRequiresRecalFlag(order.lineItems);
|
||||
|
||||
if (
|
||||
!containsRecalPart &&
|
||||
hasPartWithRecalRequirement &&
|
||||
this.isRecalAcknowledgedForScheduling !== RECAL_ACK_YES
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
async showMobileFirstModal() {
|
||||
if (!this.selectedRouteCodeData?.routeCode) {
|
||||
const pmMobileDays = experimentMixin.methods.getSettingValue(
|
||||
|
|
@ -1767,6 +1884,20 @@ export default {
|
|||
const noMobileAvailableDays = experimentMixin.methods.getSettingValue(
|
||||
experimentSettings.SHOW_NO_MOBILE_AVAILABLE_DAYS
|
||||
);
|
||||
const mobileFirstPopupRepairSetting = experimentMixin.methods.getSettingValue(
|
||||
experimentSettings.SHOW_MOBILE_FIRST_POPUP_REPAIR
|
||||
);
|
||||
const mobileFirstPopupReplaceWithoutMsrSetting =
|
||||
experimentMixin.methods.getSettingValue(
|
||||
experimentSettings.SHOW_MOBILE_FIRST_POPUP_REPLACE_WITHOUT_MSR
|
||||
);
|
||||
const mobileFirstPopupReplaceWithMsrSetting =
|
||||
experimentMixin.methods.getSettingValue(
|
||||
experimentSettings.SHOW_MOBILE_FIRST_POPUP_REPLACE_WITH_MSR
|
||||
);
|
||||
|
||||
const order = this.$store.getters.order;
|
||||
const isRepair = order.damage.isRepair;
|
||||
|
||||
let preSelectedMobileAppointment = null;
|
||||
const todaysDate = getTodayDate();
|
||||
|
|
@ -1783,12 +1914,63 @@ export default {
|
|||
(new Date(firstMobilePMDate) - todaysDate) / (1000 * 60 * 60 * 24);
|
||||
|
||||
const shouldExposeMobileFirstAppointment = () => {
|
||||
return (
|
||||
const mobileFirstDaysCheck =
|
||||
(firstMobileAMDate &&
|
||||
numberOfDaysToFirstMobileAMDate <= noMobileAvailableDays) ||
|
||||
(firstMobilePMDate &&
|
||||
numberOfDaysToFirstMobilePMDate <= noMobileAvailableDays)
|
||||
);
|
||||
numberOfDaysToFirstMobilePMDate <= noMobileAvailableDays);
|
||||
debugLog("::: Mobile First Days Check:", mobileFirstDaysCheck);
|
||||
|
||||
if (
|
||||
mobileFirstPopupRepairSetting === "false" &&
|
||||
mobileFirstPopupReplaceWithoutMsrSetting === "false" &&
|
||||
mobileFirstPopupReplaceWithMsrSetting === "false"
|
||||
) {
|
||||
debugLog(
|
||||
"::: Mobile First Damage Popup settings are all false, using default check."
|
||||
);
|
||||
return mobileFirstDaysCheck;
|
||||
}
|
||||
|
||||
if (isRepair) {
|
||||
if (mobileFirstPopupRepairSetting === "true") {
|
||||
debugLog(
|
||||
"::: Mobile First Popup Repair setting is true, showing mobile first appointment for repair."
|
||||
);
|
||||
return mobileFirstDaysCheck;
|
||||
} else {
|
||||
debugLog(
|
||||
"::: Mobile First Popup Repair setting is false, not showing mobile first appointment for repair."
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.isMobileStaticRecalibrationApplicable) {
|
||||
if (mobileFirstPopupReplaceWithMsrSetting === "true") {
|
||||
debugLog(
|
||||
"::: Mobile First Popup Replace with MSR setting is true, showing mobile first appointment for replace with MSR."
|
||||
);
|
||||
return mobileFirstDaysCheck;
|
||||
} else {
|
||||
debugLog(
|
||||
"::: Mobile First Popup Replace with MSR setting is false, not showing mobile first appointment for replace with MSR."
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (mobileFirstPopupReplaceWithoutMsrSetting === "true") {
|
||||
debugLog(
|
||||
"::: Mobile First Popup Replace without MSR setting is true, showing mobile first appointment for replace without MSR."
|
||||
);
|
||||
return mobileFirstDaysCheck;
|
||||
} else {
|
||||
debugLog(
|
||||
"::: Mobile First Popup Replace without MSR setting is false, not showing mobile first appointment for replace without MSR."
|
||||
);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
if (shouldExposeMobileFirstAppointment()) {
|
||||
|
|
@ -1813,23 +1995,34 @@ export default {
|
|||
}
|
||||
|
||||
if (this.isShowMobileFirstAppt) {
|
||||
if (firstMobilePMDate && numberOfDaysToFirstMobilePMDate <= pmMobileDays) {
|
||||
if (pmMobileDays == 0) {
|
||||
// Selects the first available time slot
|
||||
preSelectedMobileAppointment =
|
||||
numberOfDaysToFirstMobileAMDate <= numberOfDaysToFirstMobilePMDate
|
||||
? firstMobileAMAppt
|
||||
: firstMobilePMAppt;
|
||||
} else if (
|
||||
firstMobilePMDate &&
|
||||
numberOfDaysToFirstMobilePMDate <= pmMobileDays
|
||||
) {
|
||||
// Selects the first available PM time slot
|
||||
preSelectedMobileAppointment = firstMobilePMAppt;
|
||||
} else if (
|
||||
firstMobilePMDate &&
|
||||
numberOfDaysToFirstMobilePMDate >= noPMMobileDays
|
||||
) {
|
||||
// Selects the first available AM time slot. If none, selects the first available PM time slot
|
||||
preSelectedMobileAppointment = firstMobileAMAppt
|
||||
? firstMobileAMAppt
|
||||
: null;
|
||||
!preSelectedMobileAppointment &&
|
||||
(preSelectedMobileAppointment = firstMobilePMAppt);
|
||||
: firstMobilePMAppt;
|
||||
}
|
||||
|
||||
this.$refs.mobileFirstModal.setSelectedAppointment(
|
||||
preSelectedMobileAppointment
|
||||
);
|
||||
this.$refs.mobileFirstModal.openModal();
|
||||
if (preSelectedMobileAppointment) {
|
||||
this.$refs.mobileFirstModal.setSelectedAppointment(
|
||||
preSelectedMobileAppointment
|
||||
);
|
||||
this.$refs.mobileFirstModal.openModal();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1868,39 +2061,17 @@ export default {
|
|||
this.updateTimeSlot(timeSlotObj);
|
||||
this.forwardButtonAction();
|
||||
},
|
||||
updateRecalAcknowledgedAndNavigateForward(isAcknowledged) {
|
||||
this.isRecalAcknowledgedForScheduling = isAcknowledged;
|
||||
this.forwardButtonAction();
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
appointmentTypeFromAppointmentTypeQuestion: {
|
||||
handler(newValue, oldValue) {
|
||||
this.handleAppointmentTypeChange(newValue);
|
||||
this.handleAppointmentTypeChange(newValue); // v-if on appointmentTypeQuestion ensures dates have been loaded by now
|
||||
},
|
||||
},
|
||||
selectedDate(newValue, oldValue) {
|
||||
// Clear time slot selection if date selected changes
|
||||
const selectedDate = this.getSelectedDateFromStore();
|
||||
if (oldValue && this.appointmentType === AppointmentTypeStrings.MOBILE) {
|
||||
oldValue = `${oldValue}-mobile`;
|
||||
}
|
||||
const hasValueChanged = newValue !== oldValue;
|
||||
const isDateDifferent = (newValue || oldValue) !== selectedDate;
|
||||
|
||||
if (hasValueChanged && isDateDifferent && !this.selectedMobileFirstAppointment) {
|
||||
this.selectedTimeSlotInfo = {
|
||||
timeSlot: {
|
||||
date: null,
|
||||
routeCode: null,
|
||||
startTime: null,
|
||||
endTime: null,
|
||||
jobMaxMinutes: null,
|
||||
jobMinMinutes: null,
|
||||
},
|
||||
isPremiumAppointment: null,
|
||||
};
|
||||
}
|
||||
},
|
||||
selectedTimeSlotInfo(newValue) {
|
||||
this.updateFooterButtonText(newValue);
|
||||
},
|
||||
},
|
||||
components: {
|
||||
funnelHeader,
|
||||
|
|
@ -1913,7 +2084,7 @@ export default {
|
|||
textBlock,
|
||||
timeSlotQuestion,
|
||||
mobileFirstModal,
|
||||
|
||||
recalAckModal,
|
||||
alert,
|
||||
serviceZipModalQuestion,
|
||||
appointmentTypeQuestion,
|
||||
|
|
|
|||
|
|
@ -548,9 +548,6 @@ export default {
|
|||
}
|
||||
},
|
||||
},
|
||||
unmounted() {
|
||||
if (this.isModalOpened) this.closeModal();
|
||||
},
|
||||
watch: {
|
||||
waitListRequested(newVal) {
|
||||
if (newVal) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<template>
|
||||
<div class="time-slots-question">
|
||||
<buttonQuestion
|
||||
ref="chooseDropOffOrInshop"
|
||||
v-if="isDropOffAppointmentAvailable"
|
||||
v-model="selectedAnswerForDropOffOrInshop"
|
||||
@update:modelValue="dropOffSelectionChanged"
|
||||
|
|
@ -23,8 +24,9 @@
|
|||
</div>
|
||||
</buttonQuestion>
|
||||
<buttonQuestion
|
||||
ref="buttonQuestion"
|
||||
v-if="shouldDisplayTimeSlotQuestion"
|
||||
ref="chooseTimeSlot"
|
||||
v-if="displayTimeSlotQuestion"
|
||||
v-model="selectedAnswerForTimeSlots"
|
||||
@update:modelValue="timeSlotSelectionChanged"
|
||||
buttonTypeString="timeSlotModalListButton"
|
||||
:buttonTypeObject="timeSlotModalListButton"
|
||||
|
|
@ -34,10 +36,12 @@
|
|||
isInshopOrDropOffAppointment ? 'is-inshop-or-drop-off-appointment' : '',
|
||||
]"
|
||||
:answers="availableTimeSlots"
|
||||
:lazyLoad="{ isLazyLoad: !isMobileAppointment, amountToLoad: 10 }"
|
||||
:lazyLoad="{
|
||||
isLazyLoad: isLazyLoad,
|
||||
amountToLoad: lazyLoadAmountToLoad || 10,
|
||||
}"
|
||||
groupName="chooseTimeSlot"
|
||||
textPosition="text-center"
|
||||
v-model="selectedAnswerForTimeSlots"
|
||||
questionText="Available times:"
|
||||
isRequired
|
||||
validationRules="time-slot-required" />
|
||||
|
|
@ -133,6 +137,7 @@ export default {
|
|||
isSameDay: Boolean,
|
||||
displayWaitList: Boolean,
|
||||
selectedRouteCodeData: Object,
|
||||
lazyLoadAmountToLoad: Number,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
@ -389,12 +394,32 @@ export default {
|
|||
this.answersForDropOffQuestion[0].value !== PICK_A_TIME_BUTTON_VALUE
|
||||
);
|
||||
},
|
||||
shouldDisplayTimeSlotQuestion() {
|
||||
displayTimeSlotQuestion() {
|
||||
return (
|
||||
this.selectedAnswerForDropOffOrInshop == PICK_A_TIME_BUTTON_VALUE ||
|
||||
!this.isDropOffAppointmentAvailable
|
||||
);
|
||||
},
|
||||
isLazyLoad() {
|
||||
if (this.availableTimeSlots?.length < this.lazyLoadAmountToLoad + 1) return false;
|
||||
if (this.isMobileAppointment) return false;
|
||||
if (this.selectedAnswerForTimeSlots && !this.isSelectedInshopTimeSlotShowing)
|
||||
return false;
|
||||
return true;
|
||||
},
|
||||
availableTimeSlotsForInshop() {
|
||||
return this.getAvailableTimeSlotsForInshop(this.timeSlotsForSelectedDate?.timeSlots);
|
||||
},
|
||||
isSelectedInshopTimeSlotShowing() {
|
||||
if (!this.selectedAnswerForTimeSlots) return false;
|
||||
const idx = this.availableTimeSlotsForInshop?.findIndex((timeSlot) => {
|
||||
return timeSlot.value === this.selectedAnswerForTimeSlots;
|
||||
});
|
||||
if (idx > -1 && idx < this.lazyLoadAmountToLoad) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
updateDropoffAndTimeSlotAnswersFromSelectedRouteCodeData(selectedRouteCodeData) {
|
||||
|
|
@ -452,6 +477,7 @@ export default {
|
|||
// }
|
||||
// },
|
||||
getAvailableTimeSlotsForInshop(timeSlotsForSelectedDate) {
|
||||
if (!timeSlotsForSelectedDate) return;
|
||||
return timeSlotsForSelectedDate
|
||||
.filter((timeSlot) => {
|
||||
return !isDropOffRouteCode(timeSlot.id);
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@ export default {
|
|||
},
|
||||
set: function (newValue) {
|
||||
this.$emit("update:modelValue", newValue);
|
||||
this.$emit("handle-appointment-type-change", newValue);
|
||||
},
|
||||
},
|
||||
isMobileOnly() {
|
||||
|
|
|
|||
|
|
@ -93,6 +93,7 @@ import { errorMessages } from "@/constants/error-messages";
|
|||
import { damageLocationsCms } from "@/constants/damage-locations-cms.js";
|
||||
import { damageLocationsSelected } from "@/constants/damage-locations-selected.js";
|
||||
import { getFunnelCookie } from "@/helpers/heritage-integration/cookie-helper";
|
||||
import { debugLog } from "@/helpers/debug-log-helper";
|
||||
|
||||
import store from "@/store";
|
||||
import baseMixin from "@/mixins/base-mixin";
|
||||
|
|
@ -144,6 +145,7 @@ export default {
|
|||
if (store.getters.externalParameterState?.isExternalParameter) {
|
||||
await nextTick();
|
||||
const isValid = await baseMixin.methods.isFormValid(vm.$refs.theForm);
|
||||
debugLog(`**** vehicle-damage is form valid: ${isValid} ****`);
|
||||
if (
|
||||
store.getters.vehicle?.vehicleSubType === "MOTOR HOME" &&
|
||||
store.getters.externalParameterDamage.damageType?.toUpperCase() ===
|
||||
|
|
@ -183,7 +185,10 @@ export default {
|
|||
const selectedGlassToReplace =
|
||||
resultMap.damageOptions.windshieldOptions.availableReplacementOptions;
|
||||
|
||||
if (selectedGlassToReplace == damageLocationsSelected.SINGLE) {
|
||||
if (
|
||||
selectedGlassToReplace == damageLocationsSelected.SINGLE &&
|
||||
store.getters.externalParameterDamage.damageType
|
||||
) {
|
||||
vm.selectedWindshieldOptions.selectedWindshieldReplaceOptions.push(
|
||||
damageLocationsSelected.SINGLE
|
||||
);
|
||||
|
|
@ -251,6 +256,8 @@ export default {
|
|||
if (store.getters.vehicle.carId) {
|
||||
return true;
|
||||
}
|
||||
debugLog("**** Vehicle Page Prereq invalid ****");
|
||||
debugLog("store.getters.vehicle.carId:", store.getters.vehicle?.carId, true);
|
||||
return false;
|
||||
},
|
||||
attachCustomEvents() {
|
||||
|
|
|
|||
|
|
@ -198,6 +198,8 @@ export default {
|
|||
false
|
||||
);
|
||||
|
||||
this.dispatchStoreAction(this.storeActions.SAVE_IS_OEM_GLASS_SELECTED, false, false);
|
||||
|
||||
// Navigate to the next page
|
||||
this.navigateForward(matchedParts);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -95,9 +95,8 @@ describe("vin-lookup.vue", () => {
|
|||
it("Should call navigateForward() if the store carId does not match the vin response carId but does match previously enterted carId and forward button is clicked", async () => {
|
||||
// Arrange
|
||||
const { wrapper } = setupMocks({});
|
||||
mockOutPromises({ carId: "C11111" });
|
||||
mockOutPromises({ carId: "C11111" }); //this line in above tests affects this test, so adding to here too till we figure out how to isolate the calls
|
||||
|
||||
wrapper.vm.lookupVehicle = jest.fn().mockImplementation(() => vinPromise);
|
||||
wrapper.vm.navigateForward = jest.fn();
|
||||
wrapper.vm.previouslyEnteredCarId = "C11111";
|
||||
|
||||
|
|
@ -111,6 +110,7 @@ describe("vin-lookup.vue", () => {
|
|||
it("Should not call navigateForward() if zip service returns a non-serviceable flag", async () => {
|
||||
// Arrange
|
||||
const { wrapper } = setupMocks({});
|
||||
mockOutPromises({ carId: "C11111" }); //this line in above tests affects this test, so adding to here too till we figure out how to isolate the calls
|
||||
const zipValidationApiResponse = {
|
||||
data: {
|
||||
isServiceable: false,
|
||||
|
|
@ -135,6 +135,8 @@ describe("vin-lookup.vue", () => {
|
|||
it("Should not call navigateForward() when forward button is clicked but lookupVehicle errors out.", async () => {
|
||||
// Arrange
|
||||
const { wrapper } = setupMocks({});
|
||||
mockOutPromises({ carId: "C11111" }); //this line in above tests affects this test, so adding to here too till we figure out how to isolate the calls
|
||||
|
||||
wrapper.vm.vinTouched = true;
|
||||
wrapper.vm.vin = "foo";
|
||||
wrapper.vm.initialVin = "!foo";
|
||||
|
|
@ -164,6 +166,7 @@ describe("vin-lookup.vue", () => {
|
|||
isCarIdDifferent: true,
|
||||
isSelectedGlassAvailableForVehicle: false,
|
||||
});
|
||||
wrapper.vm.pageName = "vin-lookup";
|
||||
|
||||
// Act
|
||||
await wrapper.vm.navigateForward();
|
||||
|
|
@ -172,9 +175,7 @@ describe("vin-lookup.vue", () => {
|
|||
expect(wrapper.vm.$router.navigateWithSaving).toBeCalledTimes(1);
|
||||
expect(wrapper.vm.$router.navigateWithSaving).toHaveBeenCalledWith(
|
||||
navigationScenarios.SELECTED_VIN_WITH_MISMATCHED_GLASS,
|
||||
wrapper.vm.$route,
|
||||
expect.anything(),
|
||||
expect.anything()
|
||||
"vin-lookup"
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -251,12 +252,14 @@ describe("vin-lookup.vue", () => {
|
|||
const { wrapper } = setupMocks({});
|
||||
|
||||
// Act
|
||||
const vinLookup = wrapper.findComponent('[data-test="vin-lookup-component"]');
|
||||
vinLookup.trigger("imageLookupError");
|
||||
const vinLookup = wrapper.findComponent({ ref: "vinLookupQuestion" });
|
||||
vinLookup.trigger("image-lookup-error");
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
// Assert
|
||||
expect(wrapper.findAllComponents({ name: "alert" }).length).toBe(1);
|
||||
expect(wrapper.vm.displayVinScanFailedAlert).toBe(true);
|
||||
//TODO - Fix original check for alert box but should show if displayVinScanFailedAlert is true which I'm checking
|
||||
//expect(wrapper.findAllComponents({ cmsWidgetName: "AlertVinScanFailed" }).length).toBe(1);
|
||||
});
|
||||
|
||||
test("should hide the AlertNoService when displayNoServiceAlert is false", async () => {
|
||||
|
|
@ -277,6 +280,7 @@ describe("vin-lookup.vue", () => {
|
|||
});
|
||||
});
|
||||
|
||||
/*
|
||||
describe("getVinFromImage", () => {
|
||||
test("GetVinFromImage resolves with first valid VIN when any vins are returned.", async () => {
|
||||
// Arrange
|
||||
|
|
@ -289,7 +293,7 @@ describe("vin-lookup.vue", () => {
|
|||
|
||||
const storeMixin = {
|
||||
methods: {
|
||||
dispatchStoreAction: lookup,
|
||||
dispatchStoreActionWithLogging: lookup,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -354,6 +358,7 @@ describe("vin-lookup.vue", () => {
|
|||
await expect(promise).rejects.toEqual("An error occurred during the lookup.");
|
||||
});
|
||||
});
|
||||
*/
|
||||
});
|
||||
|
||||
function setupMocks({ customMountOptions }) {
|
||||
|
|
|
|||
|
|
@ -393,21 +393,18 @@ export default {
|
|||
|
||||
// Check if Service Zip entered is serviceable then save the ZIP info
|
||||
if (zipCodeData.isServiceable) {
|
||||
//Only save the zipCode, state, and zipCodeCtu if the zip changed or we lack zipCodeCtu
|
||||
if (
|
||||
this.$store.getters.order.serviceLocation.zipCode != this.serviceZipCode ||
|
||||
!this.$store.getters.order.serviceLocation.zipCodeCtu
|
||||
) {
|
||||
await this.dispatchStoreAction(
|
||||
storeActions.SAVE_SERVICE_ZIP_CODE_INFO,
|
||||
{
|
||||
state: zipCodeData.state,
|
||||
zipCode: this.serviceZipCode,
|
||||
zipCodeCtu: zipCodeData.zipCodeCtu,
|
||||
},
|
||||
false
|
||||
);
|
||||
}
|
||||
//Always save the service zip info even if it was not changed;
|
||||
// if it didn't change it doesn't alter other values and this makes it more consistent
|
||||
await this.dispatchStoreAction(
|
||||
storeActions.SAVE_SERVICE_ZIP_CODE_INFO,
|
||||
{
|
||||
state: zipCodeData.state,
|
||||
zipCode: this.serviceZipCode,
|
||||
zipCodeCtu: zipCodeData.zipCodeCtu,
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
// if no value due to field being optional, blank both phone and email address
|
||||
if (!this.emailOrSms) {
|
||||
await this.dispatchStoreAction(storeActions.SAVE_PHONE_NUMBER, "", false);
|
||||
|
|
|
|||
|
|
@ -31,7 +31,6 @@ import { applicationConfig } from "../constants/application-config";
|
|||
import { getFunnelCookie } from "@/helpers/heritage-integration/cookie-helper";
|
||||
import { routeData } from "@/router/constants/routes";
|
||||
import { getQuerystringParameter } from "@/helpers/querystring-helper";
|
||||
import { Variables } from "../constants/analytics";
|
||||
import { containsRecalParts, getRecalPartNumbers } from "@/helpers/recal-helper";
|
||||
import { getAmountDue, getSubTotal, getSalesTax } from "@/helpers/pricing-helper.js";
|
||||
import { partTypeStrings } from "@/constants/part-type-strings";
|
||||
|
|
@ -232,10 +231,41 @@ export default {
|
|||
(part) => `${part.glassLocation}-${part.glassName}`
|
||||
);
|
||||
|
||||
const format = (part) => `${part?.partNumber}-${part?.partType ? part.partType : ""}`;
|
||||
const mapParts = (arr) => (arr ?? []).map(format);
|
||||
|
||||
const glassParts = mapParts(order?.lineItems?.glassParts);
|
||||
const supportingItems = mapParts(order?.lineItems?.supportingItems);
|
||||
const vaps = mapParts(order?.lineItems?.vaps);
|
||||
const childParts = [].concat(
|
||||
...(order?.lineItems?.glassParts ?? []).map((gp) => mapParts(gp?.childParts))
|
||||
);
|
||||
const allParts = [...glassParts, ...supportingItems, ...vaps, ...childParts];
|
||||
|
||||
if (applicationUser?.pageData?.quote?.servicePackageSelected) {
|
||||
allParts.push(applicationUser.pageData.quote.servicePackageSelected);
|
||||
}
|
||||
|
||||
var appointment = `${order?.schedule?.date ?? ""} ${order?.schedule?.startTime ?? ""}`;
|
||||
var currentPageName = getPageNameFromRouter();
|
||||
|
||||
// add query strings to the page name for debugging. on the vehicle page, if from an external link, pull it from the stash
|
||||
if (currentPageName === "vehicle") {
|
||||
if (!window.location.search) {
|
||||
if (store.getters.externalParameterState?.qsStash) {
|
||||
currentPageName += `${store.getters.externalParameterState.qsStash}`;
|
||||
}
|
||||
} else {
|
||||
currentPageName += `${window.location.search}`;
|
||||
}
|
||||
} else {
|
||||
if (window.location.search) {
|
||||
currentPageName += `${window.location.search}`;
|
||||
}
|
||||
}
|
||||
|
||||
var sessionData = {};
|
||||
sessionData.currentPage = getPageNameFromRouter();
|
||||
sessionData.currentPage = currentPageName;
|
||||
sessionData.sid = getSessionIdValue();
|
||||
sessionData.deviceId = getDeviceIdValue();
|
||||
sessionData.fmgSessionId = applicationUser?.savedSessionId;
|
||||
|
|
@ -251,7 +281,7 @@ export default {
|
|||
? "Insurance"
|
||||
: "Cash";
|
||||
sessionData.damageType = order?.damage?.isRepair ? "Repair" : "Replace";
|
||||
sessionData.productType = glassProducts;
|
||||
sessionData.productType = allParts;
|
||||
sessionData.eon = order?.eon;
|
||||
sessionData.referralNumber = order?.referralNumber;
|
||||
sessionData.referralSequenceNumber = order?.referralSequenceNumber;
|
||||
|
|
@ -937,9 +967,6 @@ export default {
|
|||
ValueToLogTypes() {
|
||||
return ValueToLogTypes;
|
||||
},
|
||||
Variables() {
|
||||
return Variables;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ const navigationScenarios = {
|
|||
|
||||
// Schedule
|
||||
CLICKED_CHANGE_LOCATION: "CLICKED_CHANGE_LOCATION",
|
||||
ZIP_CODE_CHANGED_RECAL_ACK: "ZIP_CODE_CHANGED_RECAL_ACK",
|
||||
|
||||
// Payment
|
||||
CLICKED_INSURANCE: "CLICKED_INSURANCE",
|
||||
|
|
|
|||
|
|
@ -51,6 +51,10 @@ export const routeData = {
|
|||
name: "quote",
|
||||
path: "/quote",
|
||||
},
|
||||
POLICY_INFO: {
|
||||
name: "policy-info",
|
||||
path: "/policy-info",
|
||||
},
|
||||
INSURANCE_COMPANY: {
|
||||
name: "insurance-company",
|
||||
path: "/insurance-company",
|
||||
|
|
|
|||
|
|
@ -429,6 +429,10 @@ const routingTable = function () {
|
|||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
pageName: routeData.POLICY_INFO.name,
|
||||
maps: [],
|
||||
},
|
||||
{
|
||||
pageName: routeData.INSURANCE_COMPANY.name,
|
||||
maps: [
|
||||
|
|
@ -486,6 +490,10 @@ const routingTable = function () {
|
|||
scenario: navigationScenarios.CLICKED_FORWARD_WITH_MOBILE_SERVICE,
|
||||
destinationPageData: routeData.MOBILE_DETAILS,
|
||||
},
|
||||
{
|
||||
scenario: navigationScenarios.ZIP_CODE_CHANGED_RECAL_ACK,
|
||||
destinationPageData: routeData.SERVICE_ZIP,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
@ -565,6 +573,10 @@ const routingTable = function () {
|
|||
piaError: "true",
|
||||
},
|
||||
},
|
||||
{
|
||||
scenario: navigationScenarios.CLICKED_PAY_LATER,
|
||||
destinationPageData: routeData.CONFIRMATION,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
@ -578,6 +590,17 @@ const routingTable = function () {
|
|||
scenario: navigationScenarios.CLICKED_FORWARD,
|
||||
destinationPageData: routeData.CONFIRMATION,
|
||||
},
|
||||
{
|
||||
scenario: navigationScenarios.PIA_ERROR,
|
||||
destinationPageData: routeData.PAYMENT_METHOD,
|
||||
params: {
|
||||
piaError: "true",
|
||||
},
|
||||
},
|
||||
{
|
||||
scenario: navigationScenarios.CLICKED_PAY_LATER,
|
||||
destinationPageData: routeData.CONFIRMATION,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { storeActions } from "@/constants/store-actions";
|
|||
import { getQuerystringParameter } from "@/helpers/querystring-helper";
|
||||
import { queryStrings } from "@/constants/query-strings";
|
||||
import { externalParameterStatus } from "@/constants/external-parameters";
|
||||
import { debugLog } from "@/helpers/debug-log-helper";
|
||||
|
||||
export async function initializeFromQueryStrings() {
|
||||
// Get advertiser info
|
||||
|
|
@ -26,12 +27,13 @@ function setupAdvertisersAndAffiliates() {
|
|||
}
|
||||
|
||||
function updateExternalParameterState() {
|
||||
debugLog("--- updateExternalParameterState ---");
|
||||
const externalParameterYear = getQuerystringParameter(queryStrings.VEHICLE_YEAR);
|
||||
const externalParameterMake = getQuerystringParameter(queryStrings.VEHICLE_MAKE);
|
||||
const externalParameterModel = getQuerystringParameter(queryStrings.VEHICLE_MODEL);
|
||||
const externalParameterStyle = getQuerystringParameter(queryStrings.VEHICLE_STYLE);
|
||||
const externalParameterDamage = getQuerystringParameter(queryStrings.VEHICLE_DAMAGE);
|
||||
const externalParameterZipCode = getQuerystringParameter(queryStrings.SERVICE_ZIP);
|
||||
let externalParameterZipCode = getQuerystringParameter(queryStrings.SERVICE_ZIP);
|
||||
const externalParameterEmail = getQuerystringParameter(queryStrings.EMAIL);
|
||||
const externalParameterIsInsurance = getQuerystringParameter(queryStrings.IS_INSURANCE);
|
||||
const externalParameterVinSelection = getQuerystringParameter(queryStrings.VIN_SELECTION);
|
||||
|
|
@ -39,6 +41,27 @@ function updateExternalParameterState() {
|
|||
const externalParameterNumberOfChips = getQuerystringParameter(queryStrings.NUMBER_OF_CHIPS);
|
||||
const externalParameterSource = getQuerystringParameter(queryStrings.EXPERIMENTS);
|
||||
const externalParameterPhoneNumber = getQuerystringParameter(queryStrings.PHONE_NUMBER);
|
||||
|
||||
if (!externalParameterZipCode) {
|
||||
externalParameterZipCode = getQuerystringParameter(queryStrings.ZIP_CODE);
|
||||
}
|
||||
|
||||
if (externalParameterYear || externalParameterZipCode) {
|
||||
debugLog("externalParameterYear:", externalParameterYear);
|
||||
debugLog("externalParameterMake:", externalParameterMake);
|
||||
debugLog("externalParameterModel:", externalParameterModel);
|
||||
debugLog("externalParameterStyle:", externalParameterStyle);
|
||||
debugLog("externalParameterDamage:", externalParameterDamage);
|
||||
debugLog("externalParameterZipCode:", externalParameterZipCode);
|
||||
debugLog("externalParameterEmail:", externalParameterEmail);
|
||||
debugLog("externalParameterIsInsurance:", externalParameterIsInsurance);
|
||||
debugLog("externalParameterVinSelection:", externalParameterVinSelection);
|
||||
debugLog("externalParameterServicePackage:", externalParameterServicePackage);
|
||||
debugLog("externalParameterNumberOfChips:", externalParameterNumberOfChips);
|
||||
debugLog("externalParameterSource:", externalParameterSource);
|
||||
debugLog("externalPhoneNumber:", externalParameterPhoneNumber);
|
||||
}
|
||||
|
||||
if (
|
||||
externalParameterYear &&
|
||||
externalParameterMake &&
|
||||
|
|
@ -51,6 +74,7 @@ function updateExternalParameterState() {
|
|||
store.commit(storeMutations.UPDATE_EXTERNAL_PARAMETER_MAKE, externalParameterMake);
|
||||
store.commit(storeMutations.UPDATE_EXTERNAL_PARAMETER_MODEL, externalParameterModel);
|
||||
store.commit(storeMutations.UPDATE_EXTERNAL_PARAMETER_STYLE, externalParameterStyle);
|
||||
store.commit(storeMutations.UPDATE_EXTERNAL_PARAMETER_QS_STASH, window.location.search);
|
||||
}
|
||||
if (
|
||||
externalParameterDamage &&
|
||||
|
|
@ -100,6 +124,7 @@ function updateExternalParameterState() {
|
|||
externalParameterPhoneNumber
|
||||
);
|
||||
}
|
||||
debugLog("--- updateExternalParameterState END ---");
|
||||
}
|
||||
|
||||
// if there is an existing external parameter state then they have already been through from an external source(LeadGen) and
|
||||
|
|
|
|||
|
|
@ -15,7 +15,30 @@ export async function adyenReturnBeforeEnter(to, from) {
|
|||
const redirectResult = to?.query?.redirectResult;
|
||||
|
||||
if (sessionId && redirectResult) {
|
||||
const result = await finalizeAdyenPayment(sessionId, redirectResult);
|
||||
let result = null;
|
||||
|
||||
try {
|
||||
result = await finalizeAdyenPayment(sessionId, redirectResult);
|
||||
} catch (unexpectedResult) {
|
||||
// If user cancelled payment, handle without error message
|
||||
if (unexpectedResult.code === "Cancelled" || unexpectedResult.code === "CANCEL") {
|
||||
return {
|
||||
name: routeData.PAYMENT_ADYEN.name,
|
||||
replace: true,
|
||||
};
|
||||
}
|
||||
|
||||
// Otherwise, failure scenario.
|
||||
// Payment fails, so return user to payment-adyen screen to try again or pay later.
|
||||
return {
|
||||
name: routeData.PAYMENT_ADYEN.name,
|
||||
query: {
|
||||
piaFailure: true,
|
||||
},
|
||||
replace: true,
|
||||
};
|
||||
}
|
||||
|
||||
console.log(`Out of promise`);
|
||||
console.log(result);
|
||||
|
||||
|
|
@ -29,6 +52,12 @@ export async function adyenReturnBeforeEnter(to, from) {
|
|||
const amountDue = getAmountDue(store.getters.order.lineItems);
|
||||
const ccToken = generateCcToken(sessionInfo);
|
||||
|
||||
ccToken.authCode = "831001";
|
||||
ccToken.cardType = "VS";
|
||||
ccToken.lastFour = "1111";
|
||||
ccToken.expMonth = "03";
|
||||
ccToken.expYear = "2030";
|
||||
|
||||
await baseMixin.methods.dispatchStoreAction(
|
||||
storeActions.SAVE_PAYMENT_METHOD_CHOICE,
|
||||
paymentMethod,
|
||||
|
|
@ -53,7 +82,10 @@ export async function adyenReturnBeforeEnter(to, from) {
|
|||
} catch (error) {
|
||||
console.log(error);
|
||||
return {
|
||||
name: routeData.ERROR.name,
|
||||
name: routeData.PAYMENT_METHOD.name,
|
||||
query: {
|
||||
piaError: "true",
|
||||
},
|
||||
replace: true,
|
||||
};
|
||||
}
|
||||
|
|
@ -87,12 +119,32 @@ function finalizeAdyenPayment(sessionId, redirectResult) {
|
|||
onPaymentFailed: (result, component) => {
|
||||
console.log(`Payment failed`);
|
||||
console.log(result);
|
||||
reject("Payment failed from Adyen");
|
||||
|
||||
if (result?.resultCode !== "Cancelled") {
|
||||
const stringToLog = `ADYEN PAYMENT NOT AUTHORIZED. Code = ${result?.resultCode}. Id = ${sessionId}`;
|
||||
|
||||
global.$logger.logError(stringToLog);
|
||||
}
|
||||
|
||||
reject({
|
||||
code: result?.resultCode,
|
||||
message: "Payment failed from Adyen",
|
||||
});
|
||||
},
|
||||
onError: (error, component) => {
|
||||
console.log(`Error occured`);
|
||||
console.log(error);
|
||||
reject("Error from Adyen");
|
||||
|
||||
if (error?.name !== "CANCEL") {
|
||||
const stringToLog = `ADYEN ERROR. Name = ${error?.name}. Details = ${error?.message}.`;
|
||||
|
||||
global.$logger.logError(stringToLog);
|
||||
}
|
||||
|
||||
reject({
|
||||
code: error?.name,
|
||||
message: "Error from Adyen",
|
||||
});
|
||||
},
|
||||
},
|
||||
options: {},
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ export async function vehicleBeforeEnter(to, from) {
|
|||
if (zipData.isValid) {
|
||||
store.dispatch(storeActions.SAVE_SERVICE_ZIP_CODE_INFO, {
|
||||
zipCode: newZipFromQuerystring,
|
||||
state: zipData.state.state,
|
||||
state: zipData.state,
|
||||
zipCodeCtu: zipData.zipCodeCtu,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ export const routes = [
|
|||
createRoute(routeData.CONFIRMATION),
|
||||
createRoute(routeData.RETURN_USER),
|
||||
createRoute(routeData.MOBILE_DETAILS),
|
||||
createRoute(routeData.POLICY_INFO),
|
||||
|
||||
// Virtual pages (resolve to a non-virtual page.)
|
||||
createVirtualRoute(routeData.LANDING, landingBeforeEnter),
|
||||
|
|
|
|||
|
|
@ -122,6 +122,7 @@ const getDefaultState = () => {
|
|||
capabilityQuestionAnswers: null,
|
||||
dateOfLoss: null,
|
||||
damageCause: null,
|
||||
installOemGlass: null,
|
||||
},
|
||||
lineItems: {
|
||||
glassParts: null,
|
||||
|
|
@ -189,6 +190,7 @@ const getDefaultState = () => {
|
|||
lockToken: null,
|
||||
settledTenderAmount: 0,
|
||||
isRecalAckOptIn: false,
|
||||
isRecalAcknowledgedForScheduling: "",
|
||||
isMSRFeeApplicable: false,
|
||||
},
|
||||
applicationUser: {
|
||||
|
|
@ -342,6 +344,12 @@ export const mutations = {
|
|||
updateIsRecalAckOptIn(state, isRecalAckOptIn) {
|
||||
state.order.isRecalAckOptIn = isRecalAckOptIn;
|
||||
},
|
||||
updateIsRecalAcknowledgedForScheduling(state, isRecalAcknowledgedForScheduling) {
|
||||
state.order.isRecalAcknowledgedForScheduling = isRecalAcknowledgedForScheduling;
|
||||
},
|
||||
updateIsOemGlassSelected(state, isOemGlassSelected) {
|
||||
state.order.damage.installOemGlass = isOemGlassSelected;
|
||||
},
|
||||
updateIsMSRFeeApplicable(state, isMSRFeeApplicable) {
|
||||
state.order.isMSRFeeApplicable = isMSRFeeApplicable;
|
||||
},
|
||||
|
|
@ -508,6 +516,10 @@ export const mutations = {
|
|||
externalParameterState.vehicle.style = style;
|
||||
saveExternalParameterState(externalParameterState);
|
||||
},
|
||||
updateExternalParameterQsStash(state, qsStash) {
|
||||
externalParameterState.qsStash = qsStash;
|
||||
saveExternalParameterState(externalParameterState);
|
||||
},
|
||||
updateExternalParameterIsRepair(state, isRepair) {
|
||||
externalParameterState.vehicleDamage.isRepair = isRepair;
|
||||
saveExternalParameterState(externalParameterState);
|
||||
|
|
@ -712,6 +724,8 @@ export const mutations = {
|
|||
state.order.damage.capabilityQuestionAnswers =
|
||||
sessionInformation.order.damage.capabilityQuestionAnswers;
|
||||
|
||||
state.order.damage.installOemGlass = sessionInformation.order.damage.installOemGlass;
|
||||
|
||||
state.order.lineItems.glassParts = sessionInformation.order.lineItems.glassParts;
|
||||
state.order.lineItems.supportingItems = sessionInformation.order.lineItems.supportingItems;
|
||||
state.order.lineItems.vaps = sessionInformation.order.lineItems.vaps ?? [];
|
||||
|
|
@ -1765,7 +1779,8 @@ export const actions = {
|
|||
const glassArray = damage.glassToReplace;
|
||||
const zipCode = order.serviceLocation.zipCode;
|
||||
const vin = vehicle.vin;
|
||||
const serviceType = order.serviceLocation?.appointmentType;
|
||||
const serviceType = damage.isRepair ? "Repair" : "Install";
|
||||
const parentAccountNumber = context.getters.payment.parentAccountNumber;
|
||||
const referralSeqNumber = order.referralSequenceNumber;
|
||||
|
||||
// create a new array to avoid mutating state
|
||||
|
|
@ -1781,6 +1796,7 @@ export const actions = {
|
|||
vin: vin,
|
||||
serviceType: serviceType,
|
||||
referralSeqNumber: referralSeqNumber,
|
||||
parentAccountNumber: parentAccountNumber,
|
||||
},
|
||||
logApiCall: true,
|
||||
pageNameToLog: pageNameToLog,
|
||||
|
|
@ -1836,6 +1852,55 @@ export const actions = {
|
|||
return response;
|
||||
},
|
||||
|
||||
// Parts API Actions
|
||||
async getOemAfterMarketParts(context, { pageNameToLog }) {
|
||||
const vehicle = context.getters.vehicle;
|
||||
const damage = context.getters.damage;
|
||||
const order = context.state.order;
|
||||
|
||||
const carId = vehicle.carId;
|
||||
const glassArray = damage.glassToReplace;
|
||||
const resultsArray = damage.partQuestionAnswers;
|
||||
const zipCode = order.serviceLocation.zipCode;
|
||||
const vin = vehicle.vin;
|
||||
const make = vehicle.make;
|
||||
const serviceType = order.serviceLocation?.appointmentType;
|
||||
const referralSeqNumber = order.referralSequenceNumber;
|
||||
const parentAccountNumber = applicationConfig.CASH_PARENT_ACCOUNT_NUMBER;
|
||||
const oemEndorsementFlag = order.damage.installOemGlass || false;
|
||||
// create a new array to avoid mutating state
|
||||
const glassArrayForPayload = convertGlassPieceNamingForApi(glassArray);
|
||||
const resultsArrayForPayload = convertResultsForApi(resultsArray);
|
||||
|
||||
const response = await globalMethods.callHttpClient({
|
||||
method: endpoints.GetOemAfterMarketParts.method,
|
||||
endpoint: endpoints.GetOemAfterMarketParts.url,
|
||||
payload: {
|
||||
carId: carId,
|
||||
glassPieces: glassArrayForPayload,
|
||||
answerResults: resultsArrayForPayload,
|
||||
zip: zipCode,
|
||||
vin: vin,
|
||||
serviceType: serviceType,
|
||||
referralSeqNumber: referralSeqNumber,
|
||||
make: make,
|
||||
parentAccountNumber: parentAccountNumber.toString(),
|
||||
oemEndorsementFlag: oemEndorsementFlag,
|
||||
},
|
||||
logApiCall: true,
|
||||
pageNameToLog: pageNameToLog,
|
||||
});
|
||||
|
||||
// Flatten location and name properties
|
||||
if (response?.data?.glassPieceParts) {
|
||||
response.data.glassPieceParts = convertGlassPieceNamingFromApi(
|
||||
response.data.glassPieceParts
|
||||
);
|
||||
}
|
||||
|
||||
return response;
|
||||
},
|
||||
|
||||
async getWipers(context, { payload, pageNameToLog }) {
|
||||
const carId = payload.carId;
|
||||
const serviceZipCode = payload.serviceZipCode;
|
||||
|
|
@ -1936,26 +2001,6 @@ export const actions = {
|
|||
return response;
|
||||
});
|
||||
},
|
||||
getServicePackageDiscountPart(context, { pageNameToLog }) {
|
||||
const damage = context.getters.damage;
|
||||
const damageType = damage.isRepair ? "Repair" : "Replace";
|
||||
const glassPieces = convertGlassPieceNamingForApi(damage.glassToReplace);
|
||||
|
||||
return globalMethods
|
||||
.callHttpClient({
|
||||
method: endpoints.GetServicePackageDiscountPart.method,
|
||||
endpoint: endpoints.GetServicePackageDiscountPart.url,
|
||||
payload: {
|
||||
glassPieces: glassPieces,
|
||||
damageType: damageType,
|
||||
},
|
||||
logApiCall: true,
|
||||
pageNameToLog: pageNameToLog,
|
||||
})
|
||||
.then((response) => {
|
||||
return response;
|
||||
});
|
||||
},
|
||||
|
||||
getServiceabilityDetails(context, { payload: { serviceZipCode }, pageNameToLog }) {
|
||||
const escapeRecalibrationType = (rt) => rt.split("&").join("%26");
|
||||
|
|
@ -2421,6 +2466,7 @@ export const actions = {
|
|||
partQuestionAnswers: order.damage.partQuestionAnswers,
|
||||
moldingQuestionAnswers: order.damage.moldingQuestionAnswers,
|
||||
capabilityQuestionAnswers: order.damage.capabilityQuestionAnswers,
|
||||
installOemGlass: order.damage.installOemGlass,
|
||||
},
|
||||
lineItems: {
|
||||
glassParts: lineItems.glassParts,
|
||||
|
|
@ -2927,6 +2973,17 @@ export const actions = {
|
|||
context.commit(storeMutations.UPDATE_IS_RECAL_ACK_OPT_IN, isRecalAckOptIn);
|
||||
},
|
||||
|
||||
saveIsRecalAcknowledgedForScheduling(context, isRecalAcknowledgedForScheduling) {
|
||||
context.commit(
|
||||
storeMutations.UPDATE_IS_RECAL_ACKNOWLEDGED_FOR_SCHEDULING,
|
||||
isRecalAcknowledgedForScheduling
|
||||
);
|
||||
},
|
||||
|
||||
saveIsOemGlassSelected(context, isOemGlassSelected) {
|
||||
context.commit(storeMutations.UPDATE_IS_OEM_GLASS_SELECTED, isOemGlassSelected);
|
||||
},
|
||||
|
||||
saveIsMSRFeeApplicable(context, isMSRFeeApplicable) {
|
||||
context.commit(storeMutations.UPDATE_IS_MSR_FEE_APPLICABLE, isMSRFeeApplicable);
|
||||
},
|
||||
|
|
@ -3093,40 +3150,24 @@ export const actions = {
|
|||
promoCode: lineItem.promoCode ?? null,
|
||||
}));
|
||||
|
||||
const pricedLineItemsFormattedForRequest =
|
||||
buildQueryStringParameterFromArrayOfComplexObjects(
|
||||
lineItemsWithOnlyPriceInfo,
|
||||
"lineItems"
|
||||
);
|
||||
|
||||
let queryString = "";
|
||||
if (appointmentType == "Mobile") {
|
||||
queryString =
|
||||
`ParentAccountNumber=${context.getters.order.payment.parentAccountNumber}` +
|
||||
`&BillToAccountNumber=${billToAccountNumber}` +
|
||||
`&ProviderNumber=${providerNumber}` +
|
||||
`&AppointmentType=${appointmentType}` +
|
||||
`&ServiceLocation.City=${serviceLocationCity}` +
|
||||
`&ServiceLocation.State=${serviceLocationState}` +
|
||||
`&ServiceLocation.ZipCode=${serviceLocationZipCode}` +
|
||||
`&${pricedLineItemsFormattedForRequest}`;
|
||||
} else {
|
||||
queryString =
|
||||
`ParentAccountNumber=${context.getters.order.payment.parentAccountNumber}` +
|
||||
`&BillToAccountNumber=${billToAccountNumber}` +
|
||||
`&ProviderNumber=${providerNumber}` +
|
||||
`&AppointmentType=${appointmentType}` +
|
||||
`&${pricedLineItemsFormattedForRequest}`;
|
||||
}
|
||||
|
||||
const lineItemServerData = context.getters.order.lineItems.serverData;
|
||||
if (lineItemServerData) {
|
||||
queryString += `&ServerData=${encodeURIComponent(lineItemServerData)}`;
|
||||
}
|
||||
const order = context.getters.order;
|
||||
|
||||
const response = await globalMethods.callHttpClient({
|
||||
method: endpoints.TaxOrderItems.method,
|
||||
endpoint: `${endpoints.TaxOrderItems.url}?${queryString}`,
|
||||
endpoint: endpoints.TaxOrderItems.url,
|
||||
payload: {
|
||||
parentAccountNumber: order.payment.parentAccountNumber,
|
||||
billToAccountNumber: billToAccountNumber,
|
||||
providerNumber: providerNumber,
|
||||
appointmentType: appointmentType,
|
||||
pricedLineItems: lineItemsWithOnlyPriceInfo,
|
||||
serviceLocation: {
|
||||
city: appointmentType == "Mobile" ? serviceLocationCity : null,
|
||||
state: appointmentType == "Mobile" ? serviceLocationState : null,
|
||||
zipCode: appointmentType == "Mobile" ? serviceLocationZipCode : null,
|
||||
},
|
||||
serverData: order.lineItems.serverData ? order.lineItems.serverData : "",
|
||||
},
|
||||
logApiCall: true,
|
||||
pageNameToLog: pageNameToLog,
|
||||
});
|
||||
|
|
@ -4105,6 +4146,7 @@ function createExternalParameterDefaultState() {
|
|||
// create default externalParameter state
|
||||
const externalParameterDefaultState = {
|
||||
isExternalParameter: externalParameterStatus.NOT_SET,
|
||||
qsStash: null,
|
||||
vehicle: {
|
||||
year: null,
|
||||
make: null,
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ $red-600: #ac160b;
|
|||
$red-700: #840900;
|
||||
$red-800: #5b0600;
|
||||
$red-900: #330300;
|
||||
$red-1000: #a80019;
|
||||
$red-1100: #791710;
|
||||
|
||||
// Greens
|
||||
$green-100: #e3f2ea;
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
<!-- prettier-ignore -->
|
||||
<a v-else-if="linkType === 'textSmall'" @click="handleClick" class="small" :href="href">{{ text }}<slot name="after-text"></slot></a>
|
||||
<!-- prettier-ignore -->
|
||||
<a v-else-if="linkType === 'text'" @click="handleClick" :href="href">{{ text }}<slot name="after-text"></slot></a>
|
||||
<a v-else-if="linkType === 'text'" @click="handleClick" class="text-link" :href="href">{{ text }}<slot name="after-text"></slot></a>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
|
@ -89,5 +89,8 @@ a {
|
|||
&.new-window-link {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
&.text-link {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Reference in a new issue