Merge 'develop' into feature/digital/SSR-351

This commit is contained in:
katie 2023-04-20 14:42:38 -04:00
commit 6aaacdef8c
46 changed files with 2366 additions and 607 deletions

3
.gitignore vendored
View file

@ -24,4 +24,5 @@ pnpm-debug.log*
# Misc
coverage/*
junit.xml
junit.xml
/.vs

View file

@ -14,5 +14,5 @@
@import "@/styles/common-error-styles.scss";
@import "@/styles/common-animations.scss";
@import "@/styles/shared-input-button-styles.scss";
@import "@/styles/client-customizations.scss"
@import "@/styles/client-customizations.scss";
</style>

View file

@ -11,6 +11,7 @@ const applicationConfig = {
CLIENTTAG_QUERYSTRING: 'CientTag',
GOOGLE_PLACES_API_KEY: process.env.VUE_APP_GOOGLE_PLACES_API_KEY,
ISS_DEV_CMS_DOMAIN: "https://digitalisscms.dev.safelite.io",
CASH_PARENT_ACCOUNT_NUMBER: 167132
};
export { applicationConfig };

View file

@ -2,7 +2,7 @@ const damageCustomLabels = Object.freeze({
MATCH: 'match',
REAR_WINDOW: 'rear window',
SIDE_WINDOW: 'side window',
WINDSHIELD: 'windshield',
WINDSHIELD: 'windshield'
});
export default damageCustomLabels;

View file

@ -3,7 +3,7 @@ const damageLocationsCms = {
SIDEDOOR: "SIDEDOOR",
REARWINDOW: "REARWINDOW",
DRIVERSIDE: "DRIVERSIDE",
PASSENGERSIDE: "PASSENGERSIDE",
PASSENGERSIDE: "PASSENGERSIDE"
};
export { damageLocationsCms };

View file

@ -15,7 +15,7 @@ const damageLocationsSelected = {
DRIVERSIDE: "DriverSide",
PASSENGERSIDE: "PassengerSide",
STATIONARY: "Stationary",
SLIDER: "Slider",
SLIDER: "Slider"
};
export { damageLocationsSelected };

View file

@ -2,7 +2,8 @@ const dynamicStrings = {
GLOBAL_STATE: "globalState",
CUSTOM: "custom",
ROUTER_LINK: "routerLink:",
MODAL_LINK: "modalLink"
MODAL_LINK: "modalLink",
TEXT_LINK: 'textLink'
};
export { dynamicStrings };

View file

@ -18,8 +18,8 @@ const customMappings = {
{ key: "Passenger Back", transformedValue: "passenger side back door" },
{ key: "Passenger Vent", transformedValue: "passenger side vent glass" },
{ key: "Passenger Quarter", transformedValue: "passenger side quarter panel" },
{ key: "Passenger SlideDoor", transformedValue: "passenger side sliding door" },
],
{ key: "Passenger SlideDoor", transformedValue: "passenger side sliding door" }
]
};
// Gets an instance of a string where the dynamic portion of the text {custom:KeyName}

View file

@ -1,110 +1,126 @@
const endpoints = {
GetRouteInfo: {
url: (applicationAbbreviation) => `/content/api/v1/content/${applicationAbbreviation}/RouteInfo`,
method: "POST",
method: 'POST'
},
GetHomepageInfo: {
url: (applicationAbbreviation) => `/content/api/v1/content/${applicationAbbreviation}/HomepageInfo`,
method: "GET",
method: 'GET'
},
GetPageData: {
url: (applicationAbbreviation, pageName) => `/content/api/v1/content/${applicationAbbreviation}/${pageName}`,
method: "GET",
method: 'GET'
},
GetVehicleYears: {
url: "/vehicle/api/v1/vehicle/years",
method: "GET",
url: '/vehicle/api/v1/vehicle/years',
method: 'GET'
},
GetVehicleMakes: {
url: "/vehicle/api/v1/vehicle/makes/",
method: "GET",
url: '/vehicle/api/v1/vehicle/makes/',
method: 'GET'
},
GetVehicleModels: {
url: "/vehicle/api/v1/vehicle/models",
method: "GET",
url: '/vehicle/api/v1/vehicle/models',
method: 'GET'
},
GetVehicleStyles: {
url: "/vehicle/api/v1/vehicle/styles",
method: "GET",
url: '/vehicle/api/v1/vehicle/styles',
method: 'GET'
},
GetDamageOptions: {
url: "/parts/api/v1/parts/damage-options",
method: "GET",
url: '/parts/api/v1/parts/damage-options',
method: 'GET'
},
GetPartsOrQuestions: {
url: "/parts/api/v1/parts/parts-or-questions",
method: "POST",
url: '/parts/api/v1/parts/parts-or-questions',
method: 'POST'
},
GetParts: {
url: "/parts/api/v1/parts/parts",
method: "POST",
url: '/parts/api/v1/parts/parts',
method: 'POST'
},
GetPriceOrderItems: {
url: '/price/api/v1/price/order-items',
method: 'GET'
},
GetCapabilityQuestions: {
url: "/parts/api/v1/parts/capability-questions",
method: "GET",
url: '/parts/api/v1/parts/capability-questions',
method: 'GET'
},
GetPartFromCapabilityAnswer: {
url: "/parts/api/v1/parts/part-from-capability-answer",
method: "POST",
url: '/parts/api/v1/parts/part-from-capability-answer',
method: 'POST'
},
GetWipers: {
url: '/parts/api/v1/parts/wipers',
method: 'GET'
},
GetRainDefense: {
url: '/parts/api/v1/parts/rain-defense',
method: 'GET'
},
GetSupportingItems: {
url: '/parts/api/v1/parts/supporting-items',
method: 'POST'
},
GetServiceabilityDetails: {
url: "/location/api/v1/location/serviceability-details",
method: "GET",
},
GetVehicle: {
url: "/vehicle/api/v1/vehicle/lookup",
method: "GET",
url: '/vehicle/api/v1/vehicle/lookup',
method: 'GET'
},
LogExperimentExposureIfAssigned: {
url: "/experiments/api/v1/experiments/log-exposure",
method: "POST",
url: '/experiments/api/v1/experiments/log-exposure',
method: 'POST'
},
LogPageView: {
url: "/analytics/api/v1/analytics/log-page-view",
method: "POST",
url: '/analytics/api/v1/analytics/log-page-view',
method: 'POST'
},
LogCustomEvent: {
url: "/analytics/api/v1/analytics/log-custom-event",
method: "POST",
url: '/analytics/api/v1/analytics/log-custom-event',
method: 'POST'
},
LookupVehicleByVin: {
url: "/vehicle/api/v1/vehicle/lookup",
method: "POST",
url: '/vehicle/api/v1/vehicle/lookup',
method: 'POST'
},
LookupVinByAddress: {
url: "/vehicle/api/v1/vehicle/lookup-vin-by-address",
method: "POST",
url: '/vehicle/api/v1/vehicle/lookup-vin-by-address',
method: 'POST'
},
LookupVinByPlate: {
url: "/vehicle/api/v1/vehicle/lookup-vin-by-plate",
method: "POST",
url: '/vehicle/api/v1/vehicle/lookup-vin-by-plate',
method: 'POST'
},
InitializeSession: {
url: "/analytics/api/v1/analytics/initialize",
method: "POST",
url: '/analytics/api/v1/analytics/initialize',
method: 'POST'
},
GetExperimentsByUser: {
url: "/analytics/api/v1/analytics/get-experiments",
method: "GET",
url: '/analytics/api/v1/analytics/get-experiments',
method: 'GET'
},
RunExperimentsForTrigger: {
url: "/experiments/api/v1/experiments/run",
method: "POST",
url: '/experiments/api/v1/experiments/run',
method: 'POST'
},
ValidateZip: {
url: "/location/api/v1/location/zip",
method: "GET",
url: '/location/api/v1/location/zip',
method: 'GET'
},
GooglePlaces: {
url: "https://maps.googleapis.com/maps/api/js?key={apiKey}&libraries=places"
url: 'https://maps.googleapis.com/maps/api/js?key={apiKey}&libraries=places'
},
ValidateClientTag: {
url: "/clientauth/api/v1/clientauth/validate-client-tag",
method: "GET",
url: '/clientauth/api/v1/clientauth/validate-client-tag',
method: 'GET'
},
IsVinbyAddressPermissible:{
url:"/vehicle/api/v1/vehicle/is-vin-by-address-permissible",
method:"Get",
url:'/vehicle/api/v1/vehicle/is-vin-by-address-permissible',
method:'Get'
}
};

View file

@ -36,7 +36,7 @@ const errorMessages = {
DAMAGE_OPTION_REQUIRED: "Please select an option",
POLICYHOLDER_FIRST_NAME_REQUIRED: "Please enter the policyholder first name",
POLICYHOLDER_LAST_NAME_REQUIRED: "Please enter the policyholder last name",
POLICYHOLDER_LAST_NAME_REQUIRED: "Please enter the policyholder last name"
};

View file

@ -1,17 +1,17 @@
const globalEvents = {
Categories: {
GLOBAL_ALERT: "GLOBAL_ALERT",
GLOBAL_ALERT: "GLOBAL_ALERT"
},
SubCategories: {
PAGE_NOT_FOUND: "PAGE_NOT_FOUND",
},
PAGE_NOT_FOUND: "PAGE_NOT_FOUND"
}
};
const globalEventTypes = {
Success: "alert-success",
Warning: "alert-warning",
Info: "alert-info",
Danger: "alert-danger",
Danger: "alert-danger"
};
export { globalEvents, globalEventTypes };

View file

@ -1,14 +1,14 @@
const experimentUniverses = {
ISS_FUNNEL: "ISSFunnel",
ISS_FUNNEL: "ISSFunnel"
};
const experimentSettings = {
GOOGLE_CUSTOM_DIMENSION_INDEX: "Google Custom Dimension Index",
GOOGLE_CUSTOM_DIMENSION_INDEX: "Google Custom Dimension Index"
};
const experimentTriggers = {
SITE_ENTRY: "SiteEntry",
PAGE_ENTRY: "PageEntry",
PAGE_ENTRY: "PageEntry"
};
export { experimentUniverses, experimentSettings, experimentTriggers };

View file

@ -1,3 +1,3 @@
export const headerKeys = {
EXPERIMENT: "X-Experiment-Data",
EXPERIMENT: "X-Experiment-Data"
};

View file

@ -3,8 +3,8 @@
const endpoints = {
GetRouteInfo: {
url: "https://mockey.qa.sagaws.net/service/ISS/Content/RouteInfo",
method: "GET",
},
method: "GET"
}
};

View file

@ -0,0 +1,8 @@
const partTypeStrings = {
FRONT_WIPER: 'FRONT WIPER',
REAR_WIPER: 'REAR WIPER',
RAIN_DEFENSE: 'RAIN DEFENSE',
RECALIBRATION: 'RECALIBRATION'
};
export { partTypeStrings };

View file

@ -49,5 +49,5 @@ export const states = {
WA: "Washington",
WV: "West Virginia",
WI: "Wisconsin",
WY: "Wyoming",
WY: "Wyoming"
};

View file

@ -34,7 +34,7 @@ const tintMap = {
{ name: "gray tint privacy", src: "Glass-NoShade-GrayTint.svg" },
// No shade or tint
{ name: "clear", src: "Glass-NoShade-NoTint.svg" },
{ name: "clear", src: "Glass-NoShade-NoTint.svg" }
],
windshield: [
@ -69,8 +69,8 @@ const tintMap = {
{ name: "gray tint privacy", src: "Windshield-NoShade-GrayTint.svg" },
// No shade or tint
{ name: "clear", src: "Windshield-NoShade-NoTint.svg" },
],
{ name: "clear", src: "Windshield-NoShade-NoTint.svg" }
]
};
// Gets the tint image source string given the glass location, and the tint description (like 'Green Tint')

View file

@ -5,7 +5,7 @@ const vehicleCategories = {
COMMERCIALVAN: "COMMERCIAL VAN",
SUV: "SUV",
MOTORHOME: "MOTOR HOME",
SEMI: "SEMI",
SEMI: "SEMI"
};
export { vehicleCategories };

View file

@ -1,7 +1,7 @@
const vinLookupMethodSelections = Object.freeze({
MANUALVIN: 'ManualVin',
LICENSEPLATE: 'LicensePlate',
HOMEADDRESS: 'HomeAddress',
HOMEADDRESS: 'HomeAddress'
});
export {vinLookupMethodSelections};

View file

@ -37,7 +37,7 @@
:buttonLabel="answer.buttonLabel"
:buttonLabelSubCopy="answer.buttonLabelSubCopy"
:buttonBodyCopy="answer.buttonBodyCopy"
:buttonAuxillaryCopy="answer.buttonAuxillaryCopy"
:buttonAuxiliaryCopy="answer.buttonAuxiliaryCopy"
:buttonFooterCopy="answer.buttonFooterCopy"
:buttonImage="answer.buttonImage"
:buttonImageId="answer.buttonImageId"
@ -194,7 +194,7 @@ export default {
altText: answer.altText ?? (answer.Name ? answer.Name : answer),
buttonLabelSubCopy: answer.buttonLabelSubCopy ?? answer.SubText,
buttonBodyCopy: answer.buttonBodyCopy ?? answer.buttonBodyCopy,
buttonAuxillaryCopy: answer.buttonAuxillaryCopy ?? answer.buttonAuxillaryCopy,
buttonAuxiliaryCopy: answer.buttonAuxiliaryCopy ?? answer.buttonAuxiliaryCopy,
buttonFooterCopy: answer.buttonFooterCopy ?? answer.buttonFooterCopy,
buttonImage: answer.buttonImage ?? answer.AnswerImageUrl,
buttonImageId: answer.buttonImageId ?? answer.ImageId,

View file

@ -1,8 +1,8 @@
import { dynamicStrings } from "@/constants/dynamic-strings";
import { dynamicStrings } from '@/constants/dynamic-strings';
import { useMainStore } from '@/store';
export function fetchCmsContentForPage(issPage) {
const store = useMainStore()
const store = useMainStore();
const clientName = store.issConfig.clientName;
const accountNumber = store.issConfig.accountNumber;
const clientOverride = (clientName.length > 0 && accountNumber > 0);
@ -18,7 +18,7 @@ export function fetchCmsContentForPage(issPage) {
}
else {
// Else get the client override page.
const pageName = issPage + "_" + clientName.toLowerCase().replace(/ /g, "");
const pageName = issPage + '_' + clientName.toLowerCase().replace(/ /g, '');
return store.getPageData(pageName)
.then(
@ -26,7 +26,8 @@ export function fetchCmsContentForPage(issPage) {
// Process the client override if it exists.
return processPageData(baseResponse, clientResponse);
},
(error) => {
(error) => {
console.error(error);
// Process the just the base if no client override exists.
return processPageData(baseResponse, null);
}
@ -44,8 +45,11 @@ function processPageData(baseResponse, clientResponse) {
const pageDataFromCms = {};
let widgets = [];
if ( clientResponse === null )
{
if (!baseResponse?.data?.Result) {
console.error('No result data found'); // Something has gone terribly wrong.
return {}
}
if (!clientResponse?.data?.Result) {
widgets = baseResponse.data.Result;
}
else
@ -55,7 +59,7 @@ function processPageData(baseResponse, clientResponse) {
let found = false;
clientResponse.data.Result.forEach((clientWidget) => {
if ( clientWidget.Name == baseWidget.Name )
if ( clientWidget.Name === baseWidget.Name )
{
widgets.push(clientWidget);
found = true;
@ -74,7 +78,7 @@ function processPageData(baseResponse, clientResponse) {
let found = false;
widgets.forEach((currentWidget) => {
if ( clientWidget.Name == currentWidget.Name )
if ( clientWidget.Name === currentWidget.Name )
{
found = true;
}
@ -89,7 +93,7 @@ function processPageData(baseResponse, clientResponse) {
widgets.forEach((widget) => {
// Global state value replacement.
let widgetWithReplacements = findAndReplaceGlobalStateValues(
const widgetWithReplacements = findAndReplaceGlobalStateValues(
widget.Model,
widget.Name
);
@ -103,7 +107,7 @@ function processPageData(baseResponse, clientResponse) {
}
pageDataFromCms[widgetWithReplacements.Name] = [
widgetWithReplacements.Model,
widgetWithReplacements.Model
];
});
@ -121,7 +125,7 @@ function processPageData(baseResponse, clientResponse) {
function findAndReplaceGlobalStateValues(widgetModel, widgetName) {
const objWithReplacements = {
Name: widgetName,
Model: {},
Model: {}
};
Object.keys(widgetModel).forEach((key) => {
@ -140,7 +144,13 @@ function findAndReplaceGlobalStateValues(widgetModel, widgetName) {
// This is a recursive function, it will call itself until it runs out of items to iterate on given the object.
function processWidgetItemForReplacement(widgetModel, key) {
// If we have a string, and it needs to be replaced.
if (typeof widgetModel[key] === "string") {
if (typeof widgetModel[key] === 'string') {
widgetModel[key] = processIfStatements(
widgetModel[key],
dynamicStrings.GLOBAL_STATE,
getStoreValueFromString
);
if (widgetModel[key].includes(dynamicStrings.GLOBAL_STATE)) {
widgetModel[key] = mapStringToState(widgetModel[key]);
}
@ -152,7 +162,7 @@ function processWidgetItemForReplacement(widgetModel, key) {
// If we have an object. array, etc
if (
typeof widgetModel[key] === "object" &&
typeof widgetModel[key] === 'object' &&
Object.keys(widgetModel[key]).length
) {
Object.keys(widgetModel[key]).forEach((item) => {
@ -167,15 +177,16 @@ function processWidgetItemForReplacement(widgetModel, key) {
}
function mapStringToModal(str) {
let startIndex = str.indexOf("{" + dynamicStrings.MODAL_LINK);
const startIndex = str.indexOf('{' + dynamicStrings.MODAL_LINK);
let linkToReplace = str.substring(startIndex, str.length);
linkToReplace = linkToReplace.substring(0, linkToReplace.indexOf("}") + 1);
let params = linkToReplace.substring((dynamicStrings.MODAL_LINK).length + 2, linkToReplace.length -1)
let splitParams = params.split(",");
linkToReplace = linkToReplace.substring(0, linkToReplace.indexOf('}') + 1);
let bodyText = '<a modalTarget="' + splitParams[0] + '" class="modal-text" aria-label="Modal window">' + splitParams[1] + '</a>'
let returnVal = str.replace(linkToReplace, bodyText)
const params = linkToReplace.substring((dynamicStrings.MODAL_LINK).length + 2, linkToReplace.length - 1);
const splitParams = params.split(',');
const bodyText = '<a modalTarget="' + splitParams[0] + '" class="modal-text" aria-label="Modal window">' + splitParams[1] + '</a>';
let returnVal = str.replace(linkToReplace, bodyText);
if (returnVal.includes(dynamicStrings.MODAL_LINK)) {
returnVal = mapStringToModal(returnVal);
@ -186,28 +197,23 @@ function mapStringToModal(str) {
// Function to convert a string, into a matching global state item.
function mapStringToState(str) {
// Pull all matches out of the string.
const regexExp = new RegExp("{(.*?):(.*?)}", "g");
const regexExp = new RegExp('{([^{}]*?):([^{}]*?)}', 'g');
const regexMatches = [...str.matchAll(regexExp)];
const globalStateMatches = regexMatches.filter(match => {
return match[1] === dynamicStrings.GLOBAL_STATE;
});
// Our final string value that will be built from the matches.
let stringBuilder = "";
let stringBuilder = '';
for (const match of globalStateMatches) {
// Reset store state for each match.
let storeState = useMainStore();
for (const s of match[2].split(".")) {
if (storeState[s] != undefined) {
storeState = storeState[s];
} else {
return ""; // if we can't map our string to state data, return an empty string.
}
const valueFromStore = getStoreValueFromString(match[2]);
if (!valueFromStore) {
console.warning('Unable to resolve global state data.');
return '' // if we can't map our string to state data, return an empty string.
}
const stringWithReplacement = str.replace(match[0], storeState);
const stringWithReplacement = str.replace(match[0], valueFromStore);
// If we still have values we need to substitute, call this function again.
if (stringWithReplacement.includes(dynamicStrings.GLOBAL_STATE)) {
@ -221,9 +227,199 @@ function mapStringToState(str) {
return stringBuilder.trimStart();
}
function getStoreValueFromString(str) {
if (!str) return '';
let storeOrStateObject = useMainStore();
for (const s of str.split('.')) {
if (s === 'getters') continue; //For backward compatibility
if (storeOrStateObject[s] != undefined) {
storeOrStateObject = storeOrStateObject[s];
} else {
break;
}
}
return storeOrStateObject ?? '';
}
///////////////////////////////////
// If Statement Processing Logic //
///////////////////////////////////
/**
* Recursive function - replaces all instances of if statements from the CMS that utilize the specified ifConditionKeyword
* @param {*} str string - Input string to be processed
* @param {*} ifConditionKeyword string - Defines which if statements to process ex: 'globalState'
* @param {*} replacePlaceholderCallback function - Callback to replace CMS placeholder values
* @returns The processed string
*/
export function processIfStatements(str, ifConditionKeyword, replacePlaceholderCallback) {
const containsRelevantIfStatement = new RegExp('{if:' + ifConditionKeyword + ':.+?}', 'g').test(
str
);
const hasEmbeddedCrLf = /\r?\n|\r/g.test(str);
if (!containsRelevantIfStatement) {
return str;
} else {
const ifStatementRegexExpression = getIfStatementRegexExpression();
const ifStatementRegexMatches = [...str.matchAll(ifStatementRegexExpression)];
const completeIfStatementArray = getAndFlagFirstNonNestedIfStatementWithKeyword(
ifStatementRegexMatches,
ifConditionKeyword
);
executeIfStatementAndSetProcessedStrings(
completeIfStatementArray,
replacePlaceholderCallback
);
const reconstructedPostProcessedString = joinProcessedRegexArray(ifStatementRegexMatches);
return processIfStatements(
reconstructedPostProcessedString,
ifConditionKeyword,
replacePlaceholderCallback
);
}
}
function getAndFlagFirstNonNestedIfStatementWithKeyword(matches, ifConditionKeyword) {
let index = 0;
for (const match of matches) {
if (match.groups.isIfStatement && match.groups.ifConditionType === ifConditionKeyword) {
let interiorIndex = 0;
let nestedLevel = 0;
let elseStatementIndex = null;
for (const interiorMatch of matches.slice(index + 1)) {
if (interiorMatch.groups.isIfStatement) {
if (interiorMatch.groups.ifConditionType === ifConditionKeyword) {
break;
} else {
nestedLevel++;
}
} else if (interiorMatch.groups.isElseStatement) {
if (!nestedLevel) {
elseStatementIndex = interiorIndex + 1;
}
} else if (interiorMatch.groups.isEndStatement) {
if (nestedLevel) {
nestedLevel--;
} else {
const ifStatementArray = matches.slice(index, index + interiorIndex + 2);
flagMatchesForProcessing(ifStatementArray, elseStatementIndex);
return ifStatementArray;
}
}
interiorIndex++;
}
}
index++;
}
console.error('Did not find end in conditional logic');
return matches;
}
function flagMatchesForProcessing(matches, elseStatementIndex) {
matches[0].isFlaggedForProcessing = true;
matches[matches.length - 1].isFlaggedForProcessing = true;
if (elseStatementIndex) {
matches[elseStatementIndex].isFlaggedForProcessing = true;
}
}
function joinProcessedRegexArray(regexMatches) {
let processedString = '';
regexMatches.forEach((match) => {
const rawString = match[0];
processedString += match.groups.processedString ?? rawString;
});
return processedString;
}
function executeIfStatementAndSetProcessedStrings(ifStatementArray, replacePlaceholderCallback) {
const ifCondition = replacePlaceholderCallback(ifStatementArray[0].groups.ifCondition);
let isInsideDesiredBlock = ifCondition;
ifStatementArray.forEach((entry) => {
if (entry.groups.isElseStatement && entry.isFlaggedForProcessing) {
isInsideDesiredBlock = !ifCondition;
} else if (entry.groups.isEndStatement && entry.isFlaggedForProcessing) {
isInsideDesiredBlock = true;
}
setProcessedStringOnEntry(entry, isInsideDesiredBlock);
});
}
function setProcessedStringOnEntry(entry, isInsideDesiredBlock) {
if (!isInsideDesiredBlock) {
entry.groups.processedString = '';
} else {
if (entry.groups.isIfStatement) {
entry.groups.processedString = entry.isFlaggedForProcessing
? entry.groups.ifTrailingString
: entry[0];
} else if (entry.groups.isElseStatement) {
entry.groups.processedString = entry.isFlaggedForProcessing
? entry.groups.elseTrailingString
: entry[0];
} else {
entry.groups.processedString = entry.isFlaggedForProcessing
? entry.groups.endTrailingString
: entry[0];
}
}
}
function getIfStatementRegexExpression() {
// Matches but does not capture:
// {if:...} or {else} or {end}
const anyLogicOperatorNonCapture = '(?:{(?:end|else|if:.*?)})';
// NOTE: ?<variableName> syntax stores the captured match like so: match.groups.variableName
const matchStartOfString =
'(?<processedString>^.+?)' + // Match and Capture all characters (lazy), cannot be empty
'(?=(?:{if))'; // Looks ahead but does not capture {if
const matchIfOperator =
'(?<isIfStatement>{if:)' + // Match & Capture {if:
'(?<ifConditionType>.*?):' + // Match all chars up to and including next ':' - Capture all chars up to ':'
'(?<ifCondition>.*?)}' + // Match all chars up to and including next '}' - Capture all chars up to '}'
'(?<ifTrailingString>.*?)' + // Match and Capture all characters (lazy), can be empty
'(?=' +
anyLogicOperatorNonCapture +
')'; // Looks ahead but does not capture the next logic operator
const matchElseOperator =
'(?<isElseStatement>{else})' + // Match & Capture {else}
'(?<elseTrailingString>.*?)' + // Match & Capture all characters (lazy), can be empty
'(?=' +
anyLogicOperatorNonCapture +
')'; // Looks ahead but does not capture the next logic operator
const matchEndOperator =
'(?<isEndStatement>{end})' + // Match & Capture {end}
'(?<endTrailingString>.*?)' + // Match & Capture all chracters (lazy), can be empty
'(?=' +
anyLogicOperatorNonCapture +
'|$)'; // Looks ahead but does not capture the next logic operator
// Combine all matching patterns, separated by 'or' pipes
return new RegExp(
matchStartOfString +
'|' +
matchIfOperator +
'|' +
matchElseOperator +
'|' +
matchEndOperator,
'g'
);
}
//////////////////////////////////////////
// End of If Statement Processing Logic //
//////////////////////////////////////////
export function doesCopyContainTextLink(copy) {
return copy.includes(dynamicStrings.TEXT_LINK);
}
export function setupModalLinks(context) {
context.$nextTick(() => {
const elements = document.getElementsByClassName("modal-text")
const elements = document.getElementsByClassName("modal-text");
for(let element of elements){
const target = element.getAttribute("modalTarget");
if(target)
@ -238,23 +434,35 @@ export function doesCopyContainRouterLink(copy) {
return copy.includes(this.dynamicStrings.ROUTER_LINK);
}
/**
* splits copy on { ... } such as {routerlink: ...}
* @returns array of strings
*/
export function splitCopyOnCMSPlaceHolder(copy) {
// splits copy on { ... } such as {routerlink: ...}
return copy.split(/{(.*?)}/g);
}
/**
* Returns string2 of input following this pattern: {string1:string2,string3}
* @returns string
*/
export function getRouterLinkRouteFromCopy(copy) {
// sample input: {routerLink:estimate,provide your VIN}
// first split would return 'estimate,provide your VIN'
// second split would return 'estimate'
return copy.split(":")[1].split(",")[0];
return copy.split(':')[1].split(',')[0];
}
/**
* Returns string3 of input following this pattern: { string1: string2, string3 }
* @returns string
*/
export function getRouterLinkDisplayTextFromCopy(copy) {
// sample input: {routerLink:estimate,provide your VIN}
// first split would return 'estimate,provide your VIN'
// second split would return 'provide your VIN'
return copy.split(":")[1].split(",")[1];
return copy.split(':')[1].split(',')[1];
}
// Copy returned from the CMS that has newlines will return blocks wrapped in
@ -263,5 +471,5 @@ export function getRouterLinkDisplayTextFromCopy(copy) {
// attributes present
export function splitCMSCopyOnParagraphTag(copy) {
// filter removes empty strings that are a result of string.split with regex
return copy.split(/(?:<p(?:.*?)>)|(?:<\/p>)/g).filter((paragraph) => paragraph !== "");
return copy.split(/(?:<p(?:.*?)>)|(?:<\/p>)/g).filter((paragraph) => paragraph !== '');
}

View file

@ -3,7 +3,7 @@
<img id="siteFooterImage" :src="footerImageURL" />
</div>
<footer class="footer container-fluid fixed-bottom g-5 bg-light py-4" id="infoBox">
<div class="row d-flex flex-row-reverse align-items-center vw-100">
<div class="row d-flex flex-row-reverse align-items-center vw-100 mx-0">
<div class="col button-col d-flex" id="stacked" >
<buttonMain
v-if="!isForwardButtonHidden"

View file

@ -30,13 +30,13 @@ export default({
isDismissible: false,
messageCopy: "",
messageHeadline: "",
type: "",
type: ""
},
headerAnswers: null
};
},
props: {
cmsWidgetName: String,
cmsWidgetName: String
},
computed: {
imageSrc()
@ -51,7 +51,7 @@ export default({
return {
"background-color": this.headerAnswers ? this.headerAnswers.HexCode : "#FFFFFF"
};
},
}
},
mounted() {
const answers = this.getCmsContent(this.cmsWidgetName, "Answers");

View file

@ -13,11 +13,14 @@ describe("site sub header", () => {
};
it("should contain the cms content", () => {
const wrapper = shallowMount(siteSubHeader, {
const wrapper = shallowMount(siteSubHeader, {
propsData: {
justification: '',
issContainingPage: ''
},
mixins: [mockMixin]
});
wrapper.getCmsContent = jest.fn();
const actual = wrapper.find("span");
expect(actual.html()).toContain(subHeaderText);
});

View file

@ -12,8 +12,8 @@
/>
</h5>
</div>
<div class="d-flex align-items-center justify-content-center container-fluid overflow-hidden">
<p class="text-center small fw-normal mb-0 subheader-secondary">
<div class="d-flex align-items-center container-fluid overflow-hidden" :class="justifySubheader">
<p class="text-center fw-normal mb-0 subheader-secondary" :class="alternateFormatting">
<span>
{{ subText }}
</span>
@ -31,23 +31,33 @@ import buttonBack from "@/iss-components/site-sub-header/button-back/button-back
props: {
cmsWidgetName: String,
hasBackButton: Boolean,
justification: String,
issContainingPage: String
},
computed: {
content()
{
content() {
return this.getCmsContent(this.cmsWidgetName, "SubHeaderText")
},
subText()
{
return this.getCmsContent(this.cmsWidgetName, "SecondaryText")
subText() {
const subText = this.getCmsContent(this.cmsWidgetName, "SecondaryText");
return subText ?? '';
},
backButtonAccessibleText()
{
return this.getCmsContent(this.cmsWidgetName, "BackButtonAccessibleText")
backButtonAccessibleText() {
return this.getCmsContent(this.cmsWidgetName, "BackButtonAccessibleText")
},
headerColor() {
return this.subText ? "dark-header" : "light-header";
},
justifySubheader() {
return (this.justification?.toLowerCase() === 'left') ?
'justify-content-left' :
'justify-content-center';
},
alternateFormatting() {
return (this.issContainingPage?.toLowerCase() === 'service-packages') ?
'service-packages-subtext mt-4 mb-2 px-5' :
'small';
}
},
methods: {
clickEvent() {
@ -60,22 +70,27 @@ import buttonBack from "@/iss-components/site-sub-header/button-back/button-back
<style lang="scss" scoped>
.dark-header {
color: $black;
}
.dark-header {
color: $black;
}
.light-header {
color: $gray-550;
}
.light-header {
color: $gray-550;
}
h5 {
line-height: 32px;
h5 {
line-height: 32px;
button {
color: inherit;
}
}
p.small {
color: $gray-550;
}
p.service-packages-subtext {
font-weight: 500 !important;
line-height: 24px;
}
button {
color: inherit;
}
}
p.small {
color: $gray-550;
}
</style>

View file

@ -76,6 +76,11 @@
this.mainStore.issConfig.clientName = data.accountName;
this.mainStore.issConfig.accountNumber = data.accountNumber;
this.mainStore.issConfig.styleSheet = data.styleSheet;
this.mainStore.issConfig.isCoverageEnabled = data.coverageEnabled;
// NOTE: This is only here for testing purposes. Will be removed and replaced by actual token/signature validation when work is completed.
if ( data.authentication == "RSAToken")
this.mainStore.issConfig.isAuthenticated = true;
try
{

View file

@ -0,0 +1,81 @@
<template>
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm" v-slot="{ meta }" >
<div class="page-container-grouped-styles">
<div class="fade-on-route-transition position-relative">
<siteHeader cmsWidgetName="SiteHeaderWidget"/>
<div class="container-fluid pb-2">
<p>Placeholder for payment page</p>
<siteFooter
cmsWidgetName="SiteFooterWidget"
ref="siteFooter"
:isForwardActionDisabled="!meta.valid"
@ForwardClicked="forwardButtonAction"
@back-clicked="backButtonAction"
/>
</div>
</div>
</div>
</Form>
</template>
<script>
// Components
import siteHeader from '@/iss-components/site-header/site-header';
import siteFooter from "@/iss-components/site-footer/site-footer";
// Supporting files
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { settleAllPromises } from "@/helpers/layout-helper";
import { Form } from "vee-validate";
import BaseFormMixin from '@/mixins/base-form-mixin.js';
import { useMainStore } from '@/store';
export default {
name: "payment-page",
mixins: [BaseFormMixin],
data() {
},
setup() {
const mainStore = useMainStore();
return { mainStore };
},
async beforeRouteEnter(to, from, next)
{
// Call APIs
const cmsContentPromise = fetchCmsContentForPage(to.query.issPage);
// Settle promises and get results
const promiseResultMap = [
{
resultKey: "cmsContent",
promise: cmsContentPromise,
},];
//use resultMap to populate layout content.
let resultMap = await settleAllPromises(promiseResultMap);
next((vm) => {
vm.setCmsContent(resultMap.cmsContent);
});
},
methods:
{
backButtonAction() {
this.$router.navigate(this.navigationScenarios.CLICKED_BACK, this.$route);
},
forwardButtonAction() {
},
navigateForward() {
},
},
components: {
siteHeader,
siteFooter,
Form,
},
}
</script>
<style lang="scss">
</style>

View file

@ -0,0 +1,127 @@
import { mount } from "@vue/test-utils"
import providerPrefRadio from "./provider-pref-radio"
import inputButtonWrapperMixin from "@/mixins/input-button-wrapper-mixin"
describe.skip("provider-pref-radio.vue", () => {
it("Should include buttonLabel in html", async () => {
// Arrange
let { wrapper } = setupMocks({
mountOptionsMockData: {
propsData: mockProps,
},
})
// Act
const outputHtml = wrapper.html()
console.log(outputHtml);
// Assert
expect(outputHtml).toEqual(expect.stringContaining(mockProps["buttonLabel"]))
})
it("Should include buttonLabelAuxillaryCopy in html", async () => {
// Arrange
let { wrapper } = setupMocks({
mountOptionsMockData: {
propsData: mockProps,
},
})
// Act
const outputHtml = wrapper.html()
// Assert
expect(outputHtml).toEqual(expect.stringContaining(mockProps["buttonLabelAuxillaryCopy"]))
})
it("Should include buttonLabelSubCopy in html", async () => {
// Arrange
let { wrapper } = setupMocks({
mountOptionsMockData: {
propsData: mockProps,
},
})
// Act
const outputHtml = wrapper.html()
// Assert
expect(outputHtml).toEqual(expect.stringContaining(mockProps["buttonLabelSubCopy"]))
})
it("Should include buttonFooterCopy in html", async () => {
// Arrange
let { wrapper } = setupMocks({
mountOptionsMockData: {
propsData: mockProps,
},
})
// Act
const outputHtml = wrapper.html()
// Assert
expect(outputHtml).toEqual(expect.stringContaining(mockProps["buttonFooterCopy"]))
})
it("Should get 5 strings from getArrayOfListItemsFromRawCmsCopy when provided with a dashed (x5) buttonBodyCopy", async () => {
// Arrange
let { wrapper } = setupMocks({
mountOptionsMockData: {
propsData: mockProps,
},
})
// Act
const results = wrapper.vm.getArrayOfListItemsFromRawCmsCopy(wrapper.vm.buttonBodyCopy)
// Assert
expect(results.length).toBe(5)
})
it("Should get strings from getArrayOfListItemsFromRawCmsCopy without dashes when provided with a dashed buttonBodyCopy", async () => {
// Arrange
let { wrapper } = setupMocks({
mountOptionsMockData: {
propsData: mockProps,
},
})
// Act
const results = wrapper.vm.getArrayOfListItemsFromRawCmsCopy(wrapper.vm.buttonBodyCopy)
// Assert
const fileredResults = results.filter((result) => {
return result.includes(" -");
})
expect(fileredResults.length).toBe(0)
})
it("Should get 5 strings from getArrayOfListItemsFromRawCmsCopy when buttonBodyCopy has 6 total dashes, but one is empty ", async () => {
// Arrange
const moddedProps = mockProps
moddedProps["buttonBodyCopy"] = "- buttonBodyCopy test copy - 2 - 3 - 4 - 5 -"
let { wrapper } = setupMocks({
mountOptionsMockData: {
propsData: moddedProps,
},
})
// Act
const results = wrapper.vm.getArrayOfListItemsFromRawCmsCopy(wrapper.vm.buttonBodyCopy)
// Assert
expect(results.length).toBe(5)
})
})
const mockProps = {
buttonLabel: 'buttonLabel test copy',
buttonLabelAuxillaryCopy: 'buttonLabelAuxillaryCopy test copy',
buttonLabelSubCopy: 'buttonLabelSubCopy test copy',
buttonBodyCopy: '- buttonBodyCopy test copy - 2 - 3 - 4 - 5',
buttonFooterCopy: 'buttonFooterCopy test copy'
}
function setupMocks({ mountOptionsMockData = {} }) {
const wrapper = mount(providerPrefRadio, {
...mountOptionsMockData,
mixins: [inputButtonWrapperMixin]
})
return { wrapper }
}

View file

@ -0,0 +1,265 @@
<template>
<baseInputButton v-bind="$props" @buttonClicked="handleAnswerChange" v-model="selectedValue">
<div class="package-label mb-4"
:class="[this.buttonLabelSubCopy ? 'has-subheader' : '']"
for="testradio">
<div class="package-specs">
<p class="m-0">
<span v-html="this.buttonLabel"></span>
<span class="pricing-info" v-html="this.buttonAuxiliaryCopy"></span>
</p>
<p class="sub-label m-0"
v-if="this.buttonLabelSubCopy"
v-html="this.buttonLabelSubCopy"></p>
<div>
<ul >
<li v-for="listItem in this.arrayOfListItemsFromBodyText" :key="listItem" class="mb-0">
<!-- no textLink -->
<span v-if="!doesCopyContainTextLink(listItem)"
v-html="listItem"></span>
<!-- with textLink -->
<template v-else
v-for="copy in splitCopyOnCMSPlaceHolder(listItem)"
:key="copy">
<span v-if="!doesCopyContainTextLink(copy)" v-html="copy"></span>
<span v-else>
<textLink linkType="text"
:text="getRouterLinkDisplayTextFromCopy(copy)"
href="#!"
@click-event="
$emit('buttonEvent', {
eventName: 'openModal',
args: getRouterLinkRouteFromCopy(copy),
})
"
:data-bs-target="'#' + getRouterLinkRouteFromCopy(copy)"
aria-label="Modal window" />
</span>
</template>
</li>
</ul>
<!-- End of body text parsing -->
<!--ms-n6-->
<div class="package-footer fw-bold caption mt-4 ml-n4 mr-3"
v-if="this.buttonFooterCopy"
v-html="this.buttonFooterCopy"></div>
</div>
</div>
</div>
</baseInputButton>
</template>
<script>
import baseInputButton from '@/digital-components/base-input-button/base-input-button';
import textLink from '@/ux-components/text-link/text-link';
import inputButtonWrapperMixin from '@/mixins/input-button-wrapper-mixin';
import {
getRouterLinkDisplayTextFromCopy,
getRouterLinkRouteFromCopy,
splitCopyOnCMSPlaceHolder,
doesCopyContainTextLink,
} from '@/helpers/cms-content-helper';
export default {
name: 'servicePackageRadio',
mixins: [inputButtonWrapperMixin],
components: {
baseInputButton,
textLink,
},
computed: {
arrayOfListItemsFromBodyText() {
return this.getArrayOfListItemsFromRawCmsCopy(this.buttonBodyCopy);
},
},
methods: {
getRouterLinkDisplayTextFromCopy,
getRouterLinkRouteFromCopy,
splitCopyOnCMSPlaceHolder,
doesCopyContainTextLink,
stripUlTagFromCopy(copy) {
return copy;
},
getArrayOfListItemsFromRawCmsCopy(copy) {
if (copy) {
return copy
.split('- ') //At some point we'll want a better delimiter
.filter((lineItem) => lineItem);
}
}
},
};
</script>
<style lang="scss" scoped>
.ml-n4 {
margin-left: -$spacer * 2;
}
.mr-3 {
margin-right: $spacer * 1.5;
}
.package-main {
.package-wrapper {
margin: 0.5rem 0;
label {
display: block;
}
input[type="radio"] {
opacity: 0;
position: absolute;
left: -9999px;
+ .package-label {
display: flex;
align-items: flex-start;
position: relative;
cursor: pointer;
width: 100%;
padding: 1rem;
border: 1px solid $gray-300;
box-shadow: 0px 4px 8px -4px rgba(0, 0, 0, 0.15), 0px 4px 24px -8px rgba(0, 0, 0, 0.2);
border-radius: 0.5rem;
overflow: hidden;
min-height: 60px;
&.has-subheader {
min-height: 80px;
}
&:before {
content: "";
position: relative;
top: 5px;
margin-right: 1rem;
border-radius: 50%;
border: 1px solid $gray-500;
width: 16px;
height: 16px;
min-width: 16px;
}
&:after {
content: "";
position: absolute;
left: 19px;
top: 24px;
border-radius: 50%;
width: 10px;
height: 10px;
min-width: 10px;
}
}
&:hover {
+ .package-label {
&:before {
border: 1px solid #8e9292;
box-shadow: 0px 0px 0px 4px #9fcee6, 0px 1px 4px rgba(0, 0, 0, 0.2);
}
}
}
&:checked {
+ .package-label {
.package-specs {
max-height: 1000px;
}
}
+ .package-label {
.package-specs {
.hide-when-closed {
display: block;
}
}
}
+ .package-label {
background-color: $blue-100;
border: 1px solid $blue;
max-height: 500px;
}
+ .package-label {
&:before {
box-shadow: 0px 0px 0px 1px $blue;
}
}
+ .package-label {
&:after {
background: $blue;
}
}
}
&:focus {
+ .package-label {
&:before {
border: 2px solid $blue;
}
}
}
}
.package-footer {
color: $red;
}
.package-specs {
display: flex;
flex-direction: column;
width: 100%;
max-height: 1000px;
transition: all 0.5s ease;
p {
font-weight: 500;
display: flex;
justify-content: space-between;
span {
&.pricing-info {
color: $green;
font-size: 0.875rem;
}
}
&.sub-label {
color: $green;
text-transform: uppercase;
font-size: 0.75rem;
}
}
ul {
margin: 1rem 0 0 -.6rem;
padding: 0;
li {
margin-bottom: 0.5rem;
font-size: 0.875rem;
line-height: 1.714;
a {
line-height: 1.714;
padding: 0;
}
}
}
@keyframes slideaway {
from {
display: block;
}
to {
transform: translateY(40px);
opacity: 0;
}
}
}
}
}
</style>

View file

@ -74,6 +74,12 @@ const ProviderPreferenceMockData = {
return siteFooterWidgetMockData.ForwardButtonText;
}
}
if (cmsWidgetName === 'StateSteeringModal') {
if (fieldName === 'BodyText') {
return "{if:custom:OH}ohioText{end}{if:custom:CA}cali's Text{end}";
}
}
if (cmsWidgetName === 'ProviderPreference') {
if (fieldName === 'QuestionText') {
@ -106,13 +112,48 @@ const ProviderPreferenceMockData = {
return { mockRoute, mockRouter, wrapper };
}
describe('provider-preference.vue', () => {
test('"Continue" button is disabled when no Shop Location is selected.', () => {
test("getStateSpecificText returns true when values match", () => {
const { wrapper } = setupMocks();
useMainStore().order.customer.address.state = "ohio";
const actual = wrapper.vm.getStateSpecificText("Ohio")
expect(actual).toBeTruthy();
});
test("getStateSpecificText returns false when values do not match", () => {
const { wrapper } = setupMocks();
useMainStore().order.customer.address.state = "delaware";
const actual = wrapper.vm.getStateSpecificText("Ohio")
expect(actual).toBeFalsy();
});
test("getStateSpecificText should return correct value for state", () => {
const { wrapper } = setupMocks();
useMainStore().order.customer.address.state = "ca";
let actual = wrapper.vm.steeringModalBody;
expect(actual).toBe("cali's Text");
useMainStore().order.customer.address.state = "oH";
actual = wrapper.vm.steeringModalBody;
expect(actual).toBe("ohioText");
});
test.skip('"Continue" button is disabled when no Shop Location is selected.', () => {
const { wrapper } = setupMocks();
const continueButton = wrapper.get('[data-test-id="site-footer-main-button"]');
expect(continueButton.attributes()['aria-disabled']).toBe('true');
});
test('"Continue" button is enabled after a Shop Location is selected.', async () => {
test.skip('"Continue" button is enabled after a Shop Location is selected.', async () => {
const { wrapper } = setupMocks();
const ProviderPreferenceWrapper = wrapper.findComponent({ name: 'provider-preference' });
@ -127,7 +168,9 @@ describe('provider-preference.vue', () => {
const continueButton = wrapper.get('[data-test-id="site-footer-main-button"]');
expect(continueButton.attributes()['aria-disabled']).toBe('false');
});
test('Select "Schedule with Safelite" then click "Continue". Navigates to "service-location" page.', async () => {
test.skip('Select "Schedule with Safelite" then click "Continue". Navigates to "service-location" page.', async () => {
const { mockRoute, mockRouter, wrapper } = setupMocks();
const ProviderPreferenceWrapper = wrapper.findComponent({ name: 'provider-preference' });
@ -150,7 +193,7 @@ describe('provider-preference.vue', () => {
*/
});
test('Click the back button, trigger navigate function from Vue Router with CLICKED_BACK parameter.', async () => {
test.skip('Click the back button, trigger navigate function from Vue Router with CLICKED_BACK parameter.', async () => {
const { mockRoute, mockRouter, wrapper } = setupMocks();
await wrapper.get('[data-test-id="site-footer-back-button"]').trigger('click');

View file

@ -4,53 +4,44 @@
<div class="fade-on-route-transition position-relative">
<siteHeader cmsWidgetName="SiteHeaderWidget" />
<siteSubHeader cmsWidgetName="SiteSubHeader" id="sub-header" />
<div class="select-car">
<div class="container-fluid pb-2">
<div class="row px-3">
<div class="col">
<div class="select-car-form rounded">
<div v-html="ProviderPreferenceHeaderText" class="text-center mb-5 modal-link" ></div>
<div
v-html="ProviderPreferenceBodyText"
class="mt-0 body-text"
></div>
<buttonQuestion
cmsWidgetName="ServiceLocationQuestion"
:questionText="questionText"
:answers="answersFromCms"
buttonTypeString="listButton"
isRequired
v-model="SelectedshopLocation"
validationRules="questions-required" />
<siteFooter
<div>
<siteFooter
cmsWidgetName="SiteFooterWidget"
:isForwardActionDisabled="isForwardActionDisabled"
@backClicked="backButtonAction"
@forwardClicked="forwardButtonAction"
ref="siteFooter"
/>
</div>
</div>
</div>
</div>
ref="siteFooter" />
</div>
</div>
</div>
<contentGroupModal cmsWidgetName="ShopPreferenceDrawer"
ref="ShopPreferenceDrawer"
/>
<modal
ref="SteeringModal"
modalId="SteeringModal"
@footer-button-event="closeSteeringModal"
:footerButtonText="steeringModalFooter">
<h5 class="text-center">{{steeringModalHeader}}</h5>
<div class="steeringModalBody">
<div class="mb-4" >{{steeringModalBody}}</div>
<div v-if="steeringModalBody2">{{steeringModalBody2}}</div>
</div>
</modal>
</Form>
</template>
<script>
// Import Supporting Files
import { fetchCmsContentForPage, setupModalLinks } from '@/helpers/cms-content-helper';
import { fetchCmsContentForPage, setupModalLinks, processIfStatements } from '@/helpers/cms-content-helper';
import { settleAllPromises } from "@/helpers/layout-helper";
import { required } from "@/helpers/validation-rules";
import { errorMessages } from "@/constants/error-messages";
import buttonQuestion from "@/digital-components/button-question/button-question";
import contentGroupModal from "@/iss-components/content-group-modal/content-group-modal"
import contentGroupModal from "@/iss-components/content-group-modal/content-group-modal";
import modal from "@/digital-components/modal/modal.vue"
import { states } from "@/constants/states"
// Import Component
import baseFormMixin from "@/mixins/base-form-mixin";
@ -71,7 +62,8 @@ export default {
siteSubHeader,
Form,
buttonQuestion,
contentGroupModal
contentGroupModal,
modal
},
data() {
return {
@ -99,17 +91,29 @@ export default {
ProviderPreferenceHeaderText(){
return this.getCmsContent("ProviderPreference", "HeaderText");
},
ProviderPreferenceBodyText(){
return this.getCmsContent("ProviderPreference", "BodyText");
},
questionText() {
return this.getCmsContent("ServiceLocationQuestion", "QuestionText");
return this.getCmsContent("ProviderPreference", "SubHeaderText");
},
answersFromCms() {
return this.getCmsContent("ServiceLocationQuestion", "Answers");
steeringModalHeader() {
let header = this.getCmsContent("StateSteeringModal", "HeaderText");
return header.replace("{custom:state}", states[this.mainStore.order.customer.address.state])
},
steeringModalBody() {
const bodyText = this.getCmsContent("StateSteeringModal", "BodyText");
 return processIfStatements(bodyText, "custom", this.getStateSpecificText);
},
steeringModalBody2() {
const bodyText = this.getCmsContent("StateSteeringModal", "BodyText2");
 return processIfStatements(bodyText, "custom", this.getStateSpecificText);
},
steeringModalFooter() {
return this.getCmsContent("StateSteeringModal", "FooterText");
}
},
methods: {
getStateSpecificText(value) {
return this.mainStore.order.customer.address.state?.toLowerCase() == value?.toLowerCase();
},
arePagePrerequisiteValid() {
return true;
},
@ -125,10 +129,15 @@ export default {
this.$router.navigate(this.navigationScenarios.CLICKED_FORWARD_WITH_SAFELITE, this.$route);
}
},
resetDependentState() {},
closeSteeringModal() {
this.$refs["SteeringModal"].closeModal();
}
},
mounted() {
setupModalLinks(this);
if(this.steeringModalBody) {
this.$refs["SteeringModal"].openModal();
}
}
};
</script>
@ -136,12 +145,6 @@ export default {
#sub-header span{
color: $black;
}
.body-text {
color: $darker-gray;
p, li {
margin-bottom: 0.5rem;
}
}
.modal-link a{
color: $blue-700;
font-size: 14px;
@ -154,4 +157,5 @@ export default {
text-align: left;
}
}
</style>

View file

@ -0,0 +1,80 @@
<template>
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm" v-slot="{ meta }" >
<div class="page-container-grouped-styles">
<div class="fade-on-route-transition position-relative">
<siteHeader cmsWidgetName="SiteHeaderWidget"/>
<div class="container-fluid pb-2">
<p>Placeholder for review page</p>
<siteFooter
cmsWidgetName="SiteFooterWidget"
ref="siteFooter"
:isForwardActionDisabled="!meta.valid"
@ForwardClicked="forwardButtonAction"
@back-clicked="backButtonAction"
/>
</div>
</div>
</div>
</Form>
</template>
<script>
// Components
import siteHeader from '@/iss-components/site-header/site-header';
import siteFooter from "@/iss-components/site-footer/site-footer";
// Supporting files
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { settleAllPromises } from "@/helpers/layout-helper";
import { Form } from "vee-validate";
import BaseFormMixin from '@/mixins/base-form-mixin.js';
import { useMainStore } from '@/store';
export default {
name: "review-page",
mixins: [BaseFormMixin],
data() {
},
setup() {
const mainStore = useMainStore();
return { mainStore };
},
async beforeRouteEnter(to, from, next)
{
// Call APIs
const cmsContentPromise = fetchCmsContentForPage(to.query.issPage);
// Settle promises and get results
const promiseResultMap = [
{
resultKey: "cmsContent",
promise: cmsContentPromise,
},];
//use resultMap to populate layout content.
let resultMap = await settleAllPromises(promiseResultMap);
next((vm) => {
vm.setCmsContent(resultMap.cmsContent);
});
},
methods:
{
backButtonAction() {
this.$router.navigate(this.navigationScenarios.CLICKED_BACK, this.$route);
},
async forwardButtonAction() {
return this.navigateForward();
},
navigateForward() {
this.$router.navigate(
this.navigationScenarios.CLICKED_FORWARD_WITH_REVIEW,
this.$route
);
},
},
components: {
siteHeader,
siteFooter,
Form,
},
}
</script>
<style lang="scss">
</style>

View file

@ -0,0 +1,355 @@
<template>
<buttonQuestion ref="buttonQuestion"
:answers="servicePackageAnswers"
:groupName="groupName"
buttonTypeString="servicePackageRadio"
:buttonTypeObject="servicePackageRadio"
v-model="selectedPackageName"
:validationRules="validationRules"
:isRequired="isRequired" />
</template>
<script>
import buttonQuestion from '@/digital-components/button-question/button-question';
import { processIfStatements } from '@/helpers/cms-content-helper';
import { damageLocationsSelected as glassLocations } from '@/constants/damage-locations-selected';
import servicePackageRadio from './service-package-radio/service-package-radio';
import { partTypeStrings } from '@/constants/part-type-strings';
import { useMainStore } from '@/store';
const packageNames = {
TIER_ONE: 'TierOne',
TIER_TWO: 'TierTwo',
TIER_THREE: 'TierThree',
};
export default {
name: 'servicePackageQuestion',
props: {
cmsWidgetName: String,
groupName: String,
validationRules: String,
isRequired: Boolean,
availableLineItems: []
},
data() {
return {
servicePackageRadio: servicePackageRadio,
selectedPackageName: null
};
},
watch: {
availableLineItems() {
const store = useMainStore();
if (this.allGlassPartsAndSupportingItemsHavePrices(store.order.lineItems)) {
this.selectDefaultPackage();
}
},
selectedPackageName(newValue) {
const VapsProductsInSelectedPackage = this.getVapsLineItemsForSelectedPackage(newValue);
this.$emit('vapsItemsSelected', VapsProductsInSelectedPackage);
}
},
computed: {
nullSafeAvailableLineItems() {
return this.availableLineItems ?? [];
},
servicePackageAnswers() {
if (!this.cmsWidgetName) return {};
const cmsAnswersContent = [
{
Name: 'TierOne',
cmsWidgetName: 'EconomyServicePackage'
},
{
Name: 'TierTwo',
cmsWidgetName: 'StandardServicePackage'
},
{
Name: 'TierThree',
cmsWidgetName: 'PremiumServicePackage'
}
];
//if cms content has not yet loaded, skip
if (this.getCmsContent(cmsAnswersContent[0].cmsWidgetName, 'HeaderText') == '') {
return {};
}
const modifiedAnswers = cmsAnswersContent.map((answer) => ({
value: answer.Name,
buttonLabel: this.getHeaderTextFromCms(answer.cmsWidgetName),
buttonLabelSubCopy: this.getSubheaderTextFromCms(answer.cmsWidgetName),
buttonBodyCopy: this.getBodyTextFromCms(answer.cmsWidgetName),
buttonAuxiliaryCopy: this.getPackagePriceString(answer.Name),
buttonFooterCopy: this.getFooterTextFromCms(answer.cmsWidgetName)
}
));
return modifiedAnswers;
},
frontWipersApplicableForTierTwo() {
const store = useMainStore();
const frontWipersAreAvailable = this.lineItemsContainsPartType(
partTypeStrings.FRONT_WIPER
);
const isRepair = store.order.damage.isRepair;
const glassToReplaceContainsWindshield = this.glassToReplaceContainsGlassLocation(
glassLocations.WINDSHIELD
);
if (frontWipersAreAvailable) {
if (isRepair) {
return true;
} else {
if (glassToReplaceContainsWindshield) {
return true;
} else {
return false;
}
}
} else {
return false;
}
},
rearWiperApplicableForTierTwo() {
const rearWiperIsAvailable = this.lineItemsContainsPartType(partTypeStrings.REAR_WIPER);
return (
this.glassToReplaceContainsGlassLocation(glassLocations.REAR) &&
rearWiperIsAvailable
);
},
frontWipersApplicableForTierThree() {
const frontWipersAreAvailable = this.lineItemsContainsPartType(partTypeStrings.FRONT_WIPER);
return frontWipersAreAvailable;
},
rearWiperApplicableForTierThree() {
const rearWiperIsAvailable = this.lineItemsContainsPartType(partTypeStrings.REAR_WIPER);
const frontWipersAreAvailable = this.lineItemsContainsPartType(partTypeStrings.FRONT_WIPER);
return (
rearWiperIsAvailable &&
(this.glassToReplaceContainsGlassLocation(glassLocations.REAR) ||
!frontWipersAreAvailable)
);
},
rainDefenseApplicableForTierThree() {
if (
this.rearWiperApplicableForTierTwo &&
!this.frontWipersApplicableForTierTwo &&
this.frontWipersApplicableForTierThree
) {
return false;
} else {
return true;
}
},
shouldDisplayTierTwoPackage() {
return this.frontWipersApplicableForTierTwo || this.rearWiperApplicableForTierTwo;
}
},
methods: {
processIfStatements,
getHeaderTextFromCms(cmsWidgetName) {
return this.getCmsContent(cmsWidgetName, 'HeaderText');
},
getSubheaderTextFromCms(cmsWidgetName) {
return this.getCmsContent(cmsWidgetName, 'SubheaderText');
},
getBodyTextFromCms(cmsWidgetName) {
const bodyText = this.getCmsContent(cmsWidgetName, "BodyText");
return this.processIfStatements(bodyText, "custom", this.getCustomValueFromString);
},
getFooterTextFromCms(cmsWidgetName) {
const footerText = this.getCmsContent(cmsWidgetName, "FooterText");
return this.processIfStatements(footerText, "custom", this.getCustomValueFromString);
},
getPackagePriceString(packageName) {
const formattedPriceFloat = parseFloat(this.getPackagePrice(packageName)).toFixed(2);
return '$' + formattedPriceFloat;
},
getPackagePrice(packageName) {
let priceFloat = 0;
if (packageName === packageNames.TIER_TWO) {
priceFloat += this.getTierTwoPackageVapsPrice();
} else if (packageName === packageNames.TIER_THREE) {
priceFloat += this.getTierThreePackageVapsPrice();
}
return priceFloat;
},
getTierTwoPackageVapsPrice() {
let vapsPrice = 0;
const priceFrontWipers = this.frontWipersApplicableForTierTwo;
const priceRearWipers = this.rearWiperApplicableForTierTwo;
this.nullSafeAvailableLineItems.forEach((item) => {
if (
(priceFrontWipers &&
item.partType.toUpperCase() === partTypeStrings.FRONT_WIPER) ||
(priceRearWipers && item.partType.toUpperCase() === partTypeStrings.REAR_WIPER)
) {
vapsPrice += this.getTotalLineItemPrice(item);
}
});
return vapsPrice;
},
getTierThreePackageVapsPrice() {
let vapsPrice = 0;
const priceFrontWipers = this.frontWipersApplicableForTierThree;
const priceRearWipers = this.rearWiperApplicableForTierThree;
const priceRainDefense = this.rainDefenseApplicableForTierThree;
this.nullSafeAvailableLineItems.forEach((item) => {
if (
(priceFrontWipers &&
item.partType.toUpperCase() === partTypeStrings.FRONT_WIPER) ||
(priceRearWipers &&
item.partType.toUpperCase() === partTypeStrings.REAR_WIPER) ||
(priceRainDefense &&
item.partType.toUpperCase() === partTypeStrings.RAIN_DEFENSE)
) {
vapsPrice += this.getTotalLineItemPrice(item);
}
});
return vapsPrice;
},
selectDefaultPackage() {
const store = useMainStore();
const vapsFromStore = store.lineItems.vaps;
let lowestTierForPackage = packageNames.TIER_ONE;
if (vapsFromStore?.length > 0) {
vapsFromStore.every((vapsItem) => {
let lowestTierForThisItem = this.getLowestTierForThisItem(vapsItem);
if (lowestTierForThisItem === packageNames.TIER_THREE) {
lowestTierForPackage = packageNames.TIER_THREE;
return false;
} else if (lowestTierForThisItem === packageNames.TIER_TWO) {
lowestTierForPackage = packageNames.TIER_TWO;
return true;
} else {
return true;
}
});
}
this.selectedPackageName = lowestTierForPackage;
},
allGlassPartsAndSupportingItemsHavePrices(lineItems) {
if (lineItems?.glassParts) {
for (let i = 0; i < lineItems.glassParts.length; i++) {
if (this.priceIsNullOrZero(lineItems.glassParts[i])) {
return false;
}
}
}
if (lineItems?.supportingItems) {
for (let i = 0; i < lineItems.supportingItems.length; i++) {
if (this.priceIsNullOrZero(lineItems.supportingItems[i])) {
return false;
}
}
}
return true;
},
priceIsNullOrZero(lineItem) {
return (
(lineItem.kitPrice == null || lineItem.kitPrice == 0) &&
(lineItem.laborAmount == null || lineItem.laborAmount == 0) &&
(lineItem.sellingPrice == null || lineItem.sellingPrice == 0)
);
},
getLowestTierForThisItem(vapsItem) {
let lowestTierForThisItem = null;
switch (vapsItem.partType) {
case partTypeStrings.FRONT_WIPER:
if (this.frontWipersApplicableForTierThree) {
lowestTierForThisItem = packageNames.TIER_THREE;
}
if (this.frontWipersApplicableForTierTwo) {
lowestTierForThisItem = packageNames.TIER_TWO;
}
break;
case partTypeStrings.REAR_WIPER:
if (this.rearWiperApplicableForTierThree) {
lowestTierForThisItem = packageNames.TIER_THREE;
}
if (this.rearWiperApplicableForTierTwo) {
lowestTierForThisItem = packageNames.TIER_TWO;
}
break;
case partTypeStrings.RAIN_DEFENSE:
if (this.rainDefenseApplicableForTierThree) {
lowestTierForThisItem = packageNames.TIER_THREE;
}
break;
}
return lowestTierForThisItem;
},
getVapsLineItemsForSelectedPackage(packageName) {
const vapsLineItemsForSelectedPackage = [];
if (packageName === packageNames.TIER_TWO) {
if (this.frontWipersApplicableForTierTwo) {
vapsLineItemsForSelectedPackage.push(
...this.getLineItemsContainingPartType(partTypeStrings.FRONT_WIPER)
);
}
if (this.rearWiperApplicableForTierTwo) {
vapsLineItemsForSelectedPackage.push(
...this.getLineItemsContainingPartType(partTypeStrings.REAR_WIPER)
);
}
} else if (packageName === packageNames.TIER_THREE) {
if (this.frontWipersApplicableForTierThree) {
vapsLineItemsForSelectedPackage.push(
...this.getLineItemsContainingPartType(partTypeStrings.FRONT_WIPER)
);
}
if (this.rearWiperApplicableForTierThree) {
vapsLineItemsForSelectedPackage.push(
...this.getLineItemsContainingPartType(partTypeStrings.REAR_WIPER)
);
}
if (this.rainDefenseApplicableForTierThree) {
vapsLineItemsForSelectedPackage.push(
...this.getLineItemsContainingPartType(partTypeStrings.RAIN_DEFENSE)
);
}
}
return vapsLineItemsForSelectedPackage;
},
getCustomValueFromString(str) {
switch (str) {
case 'isRecalibrationOnOrder':
return this.isRecalibrationOnOrder;
case 'frontWipersApplicableForTierTwo':
return this.frontWipersApplicableForTierTwo;
case 'rearWiperApplicableForTierTwo':
return this.rearWiperApplicableForTierTwo;
case 'frontWipersApplicableForTierThree':
return this.frontWipersApplicableForTierThree;
case 'rearWiperApplicableForTierThree':
return this.rearWiperApplicableForTierThree;
case 'rainDefenseApplicableForTierThree':
return this.rainDefenseApplicableForTierThree;
default:
return null;
}
},
getLineItemsContainingPartType(partType) {
const partTypeMatches = this.nullSafeAvailableLineItems.filter(
(lineItem) => lineItem.partType.toUpperCase() === partType
);
return partTypeMatches;
},
lineItemsContainsPartType(partType) {
const partTypeMatches = this.getLineItemsContainingPartType(partType);
return !!partTypeMatches.length;
},
glassToReplaceContainsGlassLocation(glassLocation) {
const store = useMainStore();
const glassLocationMatches =
store.order.damage.glassToReplace?.filter(
(glassToReplace) => glassToReplace.glassLocation === glassLocation
) ?? [];
return !!glassLocationMatches.length;
},
getTotalLineItemPrice(lineItem) {
return lineItem.kitPrice + lineItem.laborAmount + lineItem.sellingPrice
}
},
components: {
buttonQuestion
}
};
</script>

View file

@ -0,0 +1,127 @@
import { mount } from "@vue/test-utils"
import servicePackageRadio from "./service-package-radio"
import inputButtonWrapperMixin from "@/mixins/input-button-wrapper-mixin"
describe("service-package-radio.vue", () => {
it("Should include buttonLabel in html", async () => {
// Arrange
let { wrapper } = setupMocks({
mountOptionsMockData: {
propsData: mockProps,
},
})
// Act
const outputHtml = wrapper.html()
// Assert
expect(outputHtml).toEqual(expect.stringContaining(mockProps["buttonLabel"]))
})
it("Should include buttonLabelAuxillaryCopy in html", async () => {
// Arrange
let { wrapper } = setupMocks({
mountOptionsMockData: {
propsData: mockProps,
},
})
// Act
const outputHtml = wrapper.html()
// Assert
expect(outputHtml).toEqual(expect.stringContaining(mockProps["buttonLabelAuxillaryCopy"]))
})
it("Should include buttonLabelSubCopy in html", async () => {
// Arrange
let { wrapper } = setupMocks({
mountOptionsMockData: {
propsData: mockProps,
},
})
// Act
const outputHtml = wrapper.html()
// Assert
expect(outputHtml).toEqual(expect.stringContaining(mockProps["buttonLabelSubCopy"]))
})
it("Should include buttonFooterCopy in html", async () => {
// Arrange
let { wrapper } = setupMocks({
mountOptionsMockData: {
propsData: mockProps,
},
})
// Act
const outputHtml = wrapper.html()
// Assert
expect(outputHtml).toEqual(expect.stringContaining(mockProps["buttonFooterCopy"]))
})
it("Should get 5 strings from getArrayOfListItemsFromRawCmsCopy when provided with a dashed (x5) buttonBodyCopy", async () => {
// Arrange
let { wrapper } = setupMocks({
mountOptionsMockData: {
propsData: mockProps,
},
})
// Act
const results = wrapper.vm.getArrayOfListItemsFromRawCmsCopy(wrapper.vm.buttonBodyCopy)
// Assert
expect(results.length).toBe(5)
})
it("Should get strings from getArrayOfListItemsFromRawCmsCopy without dashes when provided with a dashed buttonBodyCopy", async () => {
// Arrange
let { wrapper } = setupMocks({
mountOptionsMockData: {
propsData: mockProps,
},
})
// Act
const results = wrapper.vm.getArrayOfListItemsFromRawCmsCopy(wrapper.vm.buttonBodyCopy)
// Assert
const fileredResults = results.filter((result) => {
return result.includes(" -");
})
expect(fileredResults.length).toBe(0)
})
it("Should get 5 strings from getArrayOfListItemsFromRawCmsCopy when buttonBodyCopy has 6 total dashes, but one is empty ", async () => {
// Arrange
const moddedProps = mockProps
moddedProps["buttonBodyCopy"] = "- buttonBodyCopy test copy - 2 - 3 - 4 - 5 -"
let { wrapper } = setupMocks({
mountOptionsMockData: {
propsData: moddedProps,
},
})
// Act
const results = wrapper.vm.getArrayOfListItemsFromRawCmsCopy(wrapper.vm.buttonBodyCopy)
// Assert
expect(results.length).toBe(5)
})
})
const mockProps = {
buttonLabel: 'buttonLabel test copy',
buttonLabelAuxillaryCopy: 'buttonLabelAuxillaryCopy test copy',
buttonLabelSubCopy: 'buttonLabelSubCopy test copy',
buttonBodyCopy: '- buttonBodyCopy test copy - 2 - 3 - 4 - 5',
buttonFooterCopy: 'buttonFooterCopy test copy'
}
function setupMocks({ mountOptionsMockData = {} }) {
const wrapper = mount(servicePackageRadio, {
...mountOptionsMockData,
mixins: [inputButtonWrapperMixin]
})
return { wrapper }
}

View file

@ -0,0 +1,265 @@
<template>
<baseInputButton v-bind="$props" @buttonClicked="handleAnswerChange" v-model="selectedValue">
<div class="package-label mb-4"
:class="[this.buttonLabelSubCopy ? 'has-subheader' : '']"
for="testradio">
<div class="package-specs">
<p class="m-0">
<span v-html="this.buttonLabel"></span>
<span class="pricing-info" v-html="this.buttonAuxiliaryCopy"></span>
</p>
<p class="sub-label m-0"
v-if="this.buttonLabelSubCopy"
v-html="this.buttonLabelSubCopy"></p>
<div>
<ul >
<li v-for="listItem in this.arrayOfListItemsFromBodyText" :key="listItem" class="mb-0">
<!-- no textLink -->
<span v-if="!doesCopyContainTextLink(listItem)"
v-html="listItem"></span>
<!-- with textLink -->
<template v-else
v-for="copy in splitCopyOnCMSPlaceHolder(listItem)"
:key="copy">
<span v-if="!doesCopyContainTextLink(copy)" v-html="copy"></span>
<span v-else>
<textLink linkType="text"
:text="getRouterLinkDisplayTextFromCopy(copy)"
href="#!"
@click-event="
$emit('buttonEvent', {
eventName: 'openModal',
args: getRouterLinkRouteFromCopy(copy),
})
"
:data-bs-target="'#' + getRouterLinkRouteFromCopy(copy)"
aria-label="Modal window" />
</span>
</template>
</li>
</ul>
<!-- End of body text parsing -->
<!--ms-n6-->
<div class="package-footer fw-bold caption mt-4 ml-n4 mr-3"
v-if="this.buttonFooterCopy"
v-html="this.buttonFooterCopy"></div>
</div>
</div>
</div>
</baseInputButton>
</template>
<script>
import baseInputButton from '@/digital-components/base-input-button/base-input-button';
import textLink from '@/ux-components/text-link/text-link';
import inputButtonWrapperMixin from '@/mixins/input-button-wrapper-mixin';
import {
getRouterLinkDisplayTextFromCopy,
getRouterLinkRouteFromCopy,
splitCopyOnCMSPlaceHolder,
doesCopyContainTextLink,
} from '@/helpers/cms-content-helper';
export default {
name: 'servicePackageRadio',
mixins: [inputButtonWrapperMixin],
components: {
baseInputButton,
textLink,
},
computed: {
arrayOfListItemsFromBodyText() {
return this.getArrayOfListItemsFromRawCmsCopy(this.buttonBodyCopy);
},
},
methods: {
getRouterLinkDisplayTextFromCopy,
getRouterLinkRouteFromCopy,
splitCopyOnCMSPlaceHolder,
doesCopyContainTextLink,
stripUlTagFromCopy(copy) {
return copy;
},
getArrayOfListItemsFromRawCmsCopy(copy) {
if (copy) {
return copy
.split('- ') //At some point we'll want a better delimiter
.filter((lineItem) => lineItem);
}
}
},
};
</script>
<style lang="scss" scoped>
.ml-n4 {
margin-left: -$spacer * 2;
}
.mr-3 {
margin-right: $spacer * 1.5;
}
.package-main {
.package-wrapper {
margin: 0.5rem 0;
label {
display: block;
}
input[type="radio"] {
opacity: 0;
position: absolute;
left: -9999px;
+ .package-label {
display: flex;
align-items: flex-start;
position: relative;
cursor: pointer;
width: 100%;
padding: 1rem;
border: 1px solid $gray-300;
box-shadow: 0px 4px 8px -4px rgba(0, 0, 0, 0.15), 0px 4px 24px -8px rgba(0, 0, 0, 0.2);
border-radius: 0.5rem;
overflow: hidden;
min-height: 60px;
&.has-subheader {
min-height: 80px;
}
&:before {
content: "";
position: relative;
top: 5px;
margin-right: 1rem;
border-radius: 50%;
border: 1px solid $gray-500;
width: 16px;
height: 16px;
min-width: 16px;
}
&:after {
content: "";
position: absolute;
left: 19px;
top: 24px;
border-radius: 50%;
width: 10px;
height: 10px;
min-width: 10px;
}
}
&:hover {
+ .package-label {
&:before {
border: 1px solid #8e9292;
box-shadow: 0px 0px 0px 4px #9fcee6, 0px 1px 4px rgba(0, 0, 0, 0.2);
}
}
}
&:checked {
+ .package-label {
.package-specs {
max-height: 1000px;
}
}
+ .package-label {
.package-specs {
.hide-when-closed {
display: block;
}
}
}
+ .package-label {
background-color: $blue-100;
border: 1px solid $blue;
max-height: 500px;
}
+ .package-label {
&:before {
box-shadow: 0px 0px 0px 1px $blue;
}
}
+ .package-label {
&:after {
background: $blue;
}
}
}
&:focus {
+ .package-label {
&:before {
border: 2px solid $blue;
}
}
}
}
.package-footer {
color: $red;
}
.package-specs {
display: flex;
flex-direction: column;
width: 100%;
max-height: 1000px;
transition: all 0.5s ease;
p {
font-weight: 500;
display: flex;
justify-content: space-between;
span {
&.pricing-info {
color: $green;
font-size: 0.875rem;
}
}
&.sub-label {
color: $green;
text-transform: uppercase;
font-size: 0.75rem;
}
}
ul {
margin: 1rem 0 0 -.6rem;
padding: 0;
li {
margin-bottom: 0.5rem;
font-size: 0.875rem;
line-height: 1.714;
a {
line-height: 1.714;
padding: 0;
}
}
}
@keyframes slideaway {
from {
display: block;
}
to {
transform: translateY(40px);
opacity: 0;
}
}
}
}
}
</style>

View file

@ -0,0 +1,150 @@
<template>
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm" v-slot="{ meta }">
<div class="page-container-grouped-styles">
<loadingModal ref="loadingModal" />
<siteHeader cmsWidgetName="SiteHeaderWidget" />
<siteSubHeader cmsWidgetName="SiteSubHeaderWidget"
justification="left"
issContainingPage="service-packages"/>
<div class="fade-on-route-transition sub-container make-tall">
<servicePackageQuestion ref="servicePackage"
cmsWidgetName="ServicePackage"
groupName="ServicePackageQuestion"
:availableLineItems="availableLineItems"
@vapsItemsSelected="vapsItemsSelectedAction"
v-on="{ 'buttonEvent.openModal': openModalAction }"
validationRules="option-required"
isRequired />
<textBlock cmsWidgetName="PriceDisclaimerWidget"
justifyText="left"
typeStyle="caption"
style="margin-bottom: 6rem;"
class="mt-2 mx-6" />
<contentGroupModal ref="RainDefenseModal" cmsWidgetName="RainDefenseModal" />
<contentGroupModal ref="FrontWiperModal" cmsWidgetName="FrontWiperModal" />
<contentGroupModal ref="RearWiperModal" cmsWidgetName="RearWiperModal" />
<contentGroupModal ref="RecalModal" cmsWidgetName="RecalModal" />
<siteFooter cmsWidgetName="SiteFooterWidget"
:isForwardActionDisabled="!meta.valid"
@back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction" />
</div>
</div>
</Form>
</template>
<script>
// Components
import siteHeader from '@/iss-components/site-header/site-header';
import siteFooter from '@/iss-components/site-footer/site-footer';
import siteSubHeader from '@/iss-components/site-sub-header/site-sub-header';
import servicePackageQuestion from './service-package-question/service-package-question';
import textBlock from '@/digital-components/text-block/text-block';
import contentGroupModal from '@/iss-components/content-group-modal/content-group-modal';
import loadingModal from '@/iss-components/loading-modal/loading-modal.vue';
import { settleAllPromises } from '@/helpers/layout-helper';
import { useMainStore } from "@/store";
import { fetchCmsContentForPage } from '@/helpers/cms-content-helper';
import { required } from '@/helpers/validation-rules';
import { errorMessages } from '@/constants/error-messages';
import { Form, defineRule } from 'vee-validate';
defineRule('option-required', required(errorMessages.OPTION_REQUIRED));
export default {
name: 'service-packages',
async beforeRouteEnter(to, from, next) {
const store = useMainStore();
// Call APIs
const cmsContentPromise = fetchCmsContentForPage(to.query.issPage);
const wipersPromise = await store.getWipers();
const rainDefensePromise = await store.getRainDefense();
const supportingItemsPromise = await store.getSupportingItems();
const promiseResultMap = [
{
resultKey: 'cmsContent',
promise: cmsContentPromise,
},
{
resultKey: 'wipers',
promise: wipersPromise,
},
{
resultKey: 'rainDefense',
promise: rainDefensePromise,
},
{
resultKey: 'supportingItems',
promise: supportingItemsPromise,
},
];
const resultMap = await settleAllPromises(promiseResultMap);
const clonedGlassParts = store.order.lineItems.glassParts
? JSON.parse(JSON.stringify(store.order.lineItems.glassParts))
: [];
const availableLineItems = [
resultMap.rainDefense,
...resultMap.supportingItems,
...resultMap.wipers,
...clonedGlassParts,
];
const pricingResults = await store.getPriceOrderItems(availableLineItems);
// Call the "next" function to complete the transition to this page.
next((vm) => {
vm.setCmsContent(resultMap.cmsContent);
vm.pricedGlassParts = clonedGlassParts;
vm.supportingItems = resultMap.supportingItems;
vm.availableLineItems = pricingResults;
});
},
data() {
return {
selectedVaps: null,
availableLineItems: null,
supportingItems: null,
pricedGlassParts: null
};
},
methods: {
openModalAction(modalName) {
this.$refs[modalName].openModal();
},
arePagePrerequisitesValid() {
const store = useMainStore();
return (
store.order.serviceLocation.zipCode &&
store.order.serviceLocation.zipCodeCtu &&
(store.order.damage.isRepair ||
(store.order.lineItems?.glassParts != null &&
store.order.lineItems.glassParts.length > 0)) &&
store.order.referralNumber?.length !== 6
);
},
vapsItemsSelectedAction(vapsItemsSelected) {
this.selectedVaps = vapsItemsSelected;
},
backButtonAction() {
this.$router.navigate(this.navigationScenarios.CLICKED_BACK, this.$route);
},
forwardButtonAction() {
//TODO: save items
this.$router.navigate(this.navigationScenarios.CLICKED_FORWARD, this.$route);
}
},
components: {
siteHeader,
siteFooter,
siteSubHeader,
Form, /* eslint-disable-line */
textBlock,
servicePackageQuestion,
loadingModal,
contentGroupModal
}
};
</script>

View file

@ -17,6 +17,15 @@
validationRules="policy-number-required" />
</div>
</div>
<div class="row mt-4 px-3" v-if="this.displayPolicyZip">
<div class="col">
<textboxQuestion
inputId="policyZipCode"
cmsWidgetName="PolicyZipQuestion"
isRequired
ref="policyZip" />
</div>
</div>
<div class="row mt-4 px-3">
<div class="col">
<textboxQuestion
@ -168,6 +177,8 @@ defineRule("loss-city-required", required(errorMessages.LOSS_CITY_REQUIRED));
defineRule("phone-number-required", required(errorMessages.PHONE_NUMBER_FORMAT));
defineRule("email-address-required", required(errorMessages.EMAIL_ADDRESS_REQUIRED));
defineRule("damage-option-required", required(errorMessages.DAMAGE_OPTION_REQUIRED));
defineRule("policy-zip-required", required(errorMessages.POLICY_ZIP_REQUIRED));
defineRule("policy-zip-format", regex(/(^\d{5}$)|(^\d{5}-\d{4}$)/, errorMessages.POLICY_ZIP_FORMAT));
defineRule("email-address-format",
regex(
@ -332,6 +343,14 @@ export default {
},
displayGlassOnlyQuestion(){
return !!this.getCmsContent("GlassOnlyQuestion","QuestionText");
},
displayPolicyZip(){
if(this.mainStore.issConfig.isAuthenticated){
return false;
}
else{
return true;
}
}
},
components: {

View file

@ -22,6 +22,12 @@ const vueApp = createApp(App);
*/
vueApp.config.unwrapInjectedRef = true;
vueApp.config.compilerOptions.isCustomElement = (tag) => {
return (tag === 'siteSubHeader' ||
tag === 'ServicePackages' ||
tag === 'servicePackageQuestion');
}
// Pinia
const pinia = createPinia();
vueApp.use(pinia);

View file

@ -10,7 +10,7 @@ export default {
buttonLabel: [Number, String],
buttonLabelSubCopy: String,
buttonBodyCopy: String,
buttonAuxillaryCopy: String,
buttonAuxiliaryCopy: String,
buttonFooterCopy: String,
buttonImage: String,
buttonImageId: String,

View file

@ -285,8 +285,7 @@ describe("vehicle-questions-mixin", () => {
const testCases = [
[issPageValues.PART_QUESTIONS, issPageValues.VEHICLE_PARTS, true],
[issPageValues.VEHICLE_PARTS, issPageValues.VEHICLE_PARTS, false],
[issPageValues.QUOTE, issPageValues.VEHICLE_PARTS, false],
[issPageValues.QUOTE, issPageValues.QUOTE, false],
[issPageValues.VEHICLE_PARTS, issPageValues.REVEAL, false],
[issPageValues.VEHICLE_PARTS, issPageValues.PART_QUESTIONS, false],
[issPageValues.PART_QUESTIONS, issPageValues.CAPABILITY_QUESTIONS, true],
[issPageValues.MOLDING_QUESTIONS, issPageValues.CAPABILITY_QUESTIONS, true],
@ -310,8 +309,7 @@ describe("vehicle-questions-mixin", () => {
const testCases = [
[issPageValues.PART_QUESTIONS, issPageValues.VEHICLE_PARTS, false],
[issPageValues.VEHICLE_PARTS, issPageValues.VEHICLE_PARTS, false],
[issPageValues.QUOTE, issPageValues.VEHICLE_PARTS, true],
[issPageValues.QUOTE, issPageValues.QUOTE, false],
[issPageValues.VEHICLE_PARTS, issPageValues.SERVICE_LOCATION, true],
[issPageValues.VEHICLE_PARTS, issPageValues.PART_QUESTIONS, true],
[issPageValues.PART_QUESTIONS, issPageValues.CAPABILITY_QUESTIONS, false],
[issPageValues.MOLDING_QUESTIONS, issPageValues.CAPABILITY_QUESTIONS, false],
@ -2079,188 +2077,9 @@ describe("vehicle-questions-mixin", () => {
);
});
});
// TODO KO UNSKIP FOR QUOTE MVP
describe.skip("should go to quote page", () => {
test("single glass location selected, has no part questions and has one part => go to heritage funnel", async () => {
// Arrange
const partsOrQuestions = [
{
glassName: "Single",
glassLocation: "Windshield",
parts: [
{
partNumber: "FW04186GTYN",
description: "solar, soundproofing, lane keep assist",
color: "Green Tint",
requiresRecalibration: true,
requiresCapabilityQuestions: false,
childParts: [
{
partNumber: "GGG 3563 KIT",
partType: "MOULDING",
description: "Kit, Top & Sides ",
},
],
},
],
partQuestions: null,
},
];
const { wrapper } = setupMocks({
partsOrQuestions: partsOrQuestions,
});
// Act
await wrapper.vm.navigateForward(partsOrQuestions);
// Assert
expect(wrapper.vm.$router.navigate).toHaveBeenCalledTimes(1);
expect(wrapper.vm.$router.navigate).toHaveBeenCalledWith(
navigationScenarios.CLICKED_FORWARD_WITH_NO_MORE_QUESTIONS,
{ query: { issPage: "vin-lookup" } }
);
});
test("multiple glass locations selected, each has one part and no part questions => go to heritage funnel", async () => {
// Arrange
const partsOrQuestions = [
{
glassName: "Single",
glassLocation: "Windshield",
parts: [
{
partNumber: "FW04186GTYN",
description: "solar, soundproofing, lane keep assist",
color: "Green Tint",
requiresRecalibration: true,
requiresCapabilityQuestions: false,
childParts: [
{
partNumber: "GGG 3563 KIT",
partType: "MOULDING",
description: "Kit, Top & Sides ",
},
],
},
],
partQuestions: null,
},
{
glassName: "Back",
glassLocation: "Driver",
parts: [
{
partNumber: "FD25457GTYN",
description: "solar, driver side, rear",
color: "Green Tint",
requiresRecalibration: false,
requiresCapabilityQuestions: false,
childParts: null,
},
],
partQuestions: null,
},
{
glassName: "Front",
glassLocation: "Driver",
parts: [
{
partNumber: "FD27090GTYN",
description: "solar, driver side, front",
color: "Green Tint",
requiresRecalibration: false,
requiresCapabilityQuestions: false,
childParts: null,
},
],
partQuestions: null,
},
{
glassName: "Vent",
glassLocation: "Driver",
parts: [
{
partNumber: "FV25459GTNN",
description: "solar, driver side, rear",
color: "Green Tint",
requiresRecalibration: false,
requiresCapabilityQuestions: false,
childParts: null,
},
],
partQuestions: null,
},
{
glassName: "Stationary",
glassLocation: "Rear",
parts: [
{
partNumber: "FB25460GTYN",
description: "heated glass, solar",
color: "Green Tint",
requiresRecalibration: false,
requiresCapabilityQuestions: false,
childParts: null,
},
],
partQuestions: null,
},
];
const { wrapper } = setupMocks({
partsOrQuestions: partsOrQuestions,
});
// Act
await wrapper.vm.navigateForward(partsOrQuestions);
// Assert
expect(useMainStore.updateGlassParts).toHaveBeenCalledTimes(1);
expect(wrapper.vm.$router.navigate).toHaveBeenCalledTimes(1);
expect(wrapper.vm.$router.navigate).toHaveBeenCalledWith(
navigationScenarios.CLICKED_FORWARD_WITH_NO_MORE_QUESTIONS,
{ query: { issPage: "vin-lookup" } }
);
});
});
});
describe("navigateBack", () => {
test("current page is quote and there are capability questions => go to capability questions", () => {
// Arrange
const { wrapper } = setupMocks({ issPage: issPageValues.QUOTE });
wrapper.vm.hasCapabilityQuestions = jest.fn().mockReturnValue(true);
// Act
wrapper.vm.navigateBack();
// Assert
expect(wrapper.vm.$router.navigate).toHaveBeenCalledWith(
navigationScenarios.CLICKED_BACK_WITH_CAPABILITY_QUESTIONS,
{ query: { issPage: issPageValues.QUOTE } }
);
});
test("current page is quote and there are part questions and molding questions => go to molding questions", () => {
// Arrange
const { wrapper } = setupMocks({ issPage: issPageValues.QUOTE });
wrapper.vm.hasPartQuestions = jest.fn().mockReturnValue(true);
wrapper.vm.hasChildPartQuestions = jest.fn().mockReturnValue(true);
// Act
wrapper.vm.navigateBack();
// Assert
expect(wrapper.vm.$router.navigate).toHaveBeenCalledWith(
navigationScenarios.CLICKED_BACK_WITH_MOLDING_QUESTIONS,
{ query: { issPage: issPageValues.QUOTE } }
);
});
test("current page is molding questions and there are part questions, multiple parts to choose, and capability questions => go to vehicle-parts", () => {
// Arrange
const { wrapper } = setupMocks({ issPage: issPageValues.MOLDING_QUESTIONS });

View file

@ -68,6 +68,7 @@ const routes = [
console.log(error);
GoToStartOn404(next);
}
return null;
}
}];
@ -77,10 +78,10 @@ const router = createRouter({
scrollBehavior(to, from, savedPosition) {
// always scroll to top
return { top: 0 }
},
}
});
router.afterEach((to, from) => {
router.afterEach((to, from) => { /*eslint-disable-line*/
const store = useMainStore();
// Update lastPageVisited in the store
@ -110,12 +111,12 @@ async function GetRouteInfoFromPageName(pageName) {
routeData.push({
path: "/",
name: `${key}`,
component: lazyLoadComponent(jsonFromResponse[key].LayoutName),
component: lazyLoadComponent(jsonFromResponse[key].LayoutName)
});
});
return routeData;
};
}
//Use this navigation when you need to call next() explicitly. beforeRouteEnter is a good example.
router.overrideNavigation = (
@ -145,7 +146,7 @@ router.navigate = (scenario, currentRoute, optionalQuery = {}, optionalParams =
}
// Navigate to the next route, depending on the scenario.
function navigate (scenario, currentRoute, optionalQuery = {}, optionalParams = {}, optionalPageData = {}) {
function navigate (scenario, currentRoute, optionalQuery = {}, optionalParams = {}, optionalPageData = {}) { /*eslint-disable-line*/
if (!scenario) {
console.error("No scenario provided. Please review the routing table.");
return;
@ -174,7 +175,7 @@ function navigate (scenario, currentRoute, optionalQuery = {}, optionalParams =
router.push({
name: "root",
query: Object.assign(optionalQuery, {
issPage: matchingScenarioMap.destinationIssPageValue,
issPage: matchingScenarioMap.destinationIssPageValue
}),
params: optionalParams
});
@ -186,8 +187,8 @@ function navigate (scenario, currentRoute, optionalQuery = {}, optionalParams =
// Navigate to an external url.
function navigateToUrl(url, optionalQuery = {}) {
// possibly show some loading screen in the future here.
let externalUrl = new URL(url);
const externalUrl = new URL(url);
for (const queryKey in optionalQuery) {
externalUrl.searchParams.append(queryKey, optionalQuery[queryKey]);
}
@ -223,7 +224,7 @@ function GoToStartOn404(next) {
router.addRoute({
path: "/",
name: errorPageName,
component: lazyLoadComponent(errorPageName),
component: lazyLoadComponent(errorPageName)
});
// Put item on the bus
@ -234,13 +235,13 @@ function GoToStartOn404(next) {
isDismissible: true,
messageCopy: "You can get a quote by starting on this page.",
messageHeadline: "We're sorry, something went wrong.",
type: globalEventTypes.Danger,
type: globalEventTypes.Danger
}
);
next({
name: errorPageName,
query: {issPage: errorPageName },
query: {issPage: errorPageName }
});
};
@ -253,14 +254,14 @@ async function runExperiments(nextPage) {
await store.runExperimentsForTrigger({
userId: getDeviceIdValue(),
triggerEvent: experimentTriggers.SITE_ENTRY,
triggerValue: applicationConfig.SITE_ENTRY_TRIGGER_VALUE,
triggerValue: applicationConfig.SITE_ENTRY_TRIGGER_VALUE
});
}
await store.runExperimentsForTrigger({
userId: getDeviceIdValue(),
triggerEvent: experimentTriggers.PAGE_ENTRY,
triggerValue: nextPage,
triggerValue: nextPage
});
}

View file

@ -1,30 +1,29 @@
export const issPageValues = {
ENTRY_PAGE: "entry-page",
WELCOME_PAGE: "welcome-page",
POLICY_HOLDER_DETAILS: "policy-holder-details",
VEHICLE_MAKE: "vehicle-make",
VEHICLE_YEAR: "vehicle-year",
VEHICLE_MODEL: "vehicle-model",
VEHICLE_STYLE: "vehicle-style",
VEHICLE_DAMAGE: "vehicle-damage",
VEHICLE_LOOKUP: "vehicle-lookup",
ADDRESS_LOOKUP: "address-lookup",
ADDRESS_VEHICLES: "address-vehicles",
LICENSE_PLATE_LOOKUP: "license-plate-lookup",
VIN_LOOKUP: "vin-lookup",
VEHICLE_PARTS: "vehicle-parts",
PART_QUESTIONS: "part-questions",
MOLDING_QUESTIONS: "molding-questions",
CAPABILITY_QUESTIONS: "capability-questions",
COVERAGE_STATEMENT: "coverage-statement",
PROVIDER_PREFERENCE: "provider-preference",
SERVICE_LOCATION: "service-location",
SCHEDULE_PAGE: "schedule-page",
CONTACT_DETAILS: "contact-details",
REVEAL: "reveal",
ESTIMATE: "estimate",
ADDRESS_VEHICLES: "address-vehicles",
QUOTE: "quote",
HERITAGE: "heritage",
ENTRY_PAGE: 'entry-page',
WELCOME_PAGE: 'welcome-page',
ADDRESS_LOOKUP: 'address-lookup',
ADDRESS_VEHICLES: 'address-vehicles',
CAPABILITY_QUESTIONS: 'capability-questions',
ESTIMATE: 'estimate',
COVERAGE_STATEMENT: 'coverage-statement',
LICENSE_PLATE_LOOKUP: 'license-plate-lookup',
MOLDING_QUESTIONS: 'molding-questions',
PART_QUESTIONS: 'part-questions',
POLICY_HOLDER_DETAILS: 'policy-holder-details',
PROVIDER_PREFERENCE: 'provider-preference',
REVIEW_PAGE: 'review-page',
REVEAL: 'reveal',
REVIEW_ORDER: 'review-order',
SERVICE_LOCATION: 'service-location',
SERVICE_PACKAGE: 'service-package',
VEHICLE_DAMAGE: 'vehicle-damage',
VEHICLE_LOOKUP: 'vehicle-lookup',
VEHICLE_MAKE: 'vehicle-make',
VEHICLE_MODEL: 'vehicle-model',
VEHICLE_PARTS: 'vehicle-parts',
VEHICLE_STYLE: 'vehicle-style',
VEHICLE_YEAR: 'vehicle-year',
VIN_LOOKUP: 'vin-lookup'
};

View file

@ -44,6 +44,9 @@ const navigationScenarios = {
CLICKED_FORWARD_COVERAGE_STATEMENT: "CLICKED_FORWARD_COVERAGE_STATEMENT",
CLICKED_FORWARD_WITH_SAFELITE: "CLICKED_FORWARD_WITH_SAFELITE",
CLICKED_FORWARD_WITH_REVIEW:"CLICKED_FORWARD_WITH_REVIEW",
};
export { navigationScenarios };

View file

@ -1,5 +1,5 @@
import { issPageValues } from "@/router/router-constants/issPage-values";
import { navigationScenarios } from "@/router/router-constants/navigation-scenarios";
import { issPageValues } from '@/router/router-constants/issPage-values';
import { navigationScenarios } from '@/router/router-constants/navigation-scenarios';
// Get store from router/index.js instead of importing it here to get updated values
const routingTable = function(store) {
@ -14,8 +14,8 @@ const routingTable = function(store) {
{
scenario: navigationScenarios.SELECTED_YEAR,
destinationIssPageValue: issPageValues.VEHICLE_MAKE
},
],
}
]
},
{
issPageValue: issPageValues.VEHICLE_MAKE,
@ -27,19 +27,19 @@ const routingTable = function(store) {
{
scenario: navigationScenarios.SELECTED_MAKE,
destinationIssPageValue: issPageValues.VEHICLE_MODEL
},
],
}
]
},
{
issPageValue: issPageValues.VEHICLE_MODEL,
maps: [
{
scenario: navigationScenarios.CLICKED_BACK,
destinationIssPageValue: issPageValues.VEHICLE_MAKE,
destinationIssPageValue: issPageValues.VEHICLE_MAKE
},
{
scenario: navigationScenarios.SELECTED_MODEL,
destinationIssPageValue: issPageValues.VEHICLE_STYLE,
destinationIssPageValue: issPageValues.VEHICLE_STYLE
}
]
},
@ -48,11 +48,11 @@ const routingTable = function(store) {
maps: [
{
scenario: navigationScenarios.CLICKED_BACK,
destinationIssPageValue: issPageValues.VEHICLE_MODEL,
destinationIssPageValue: issPageValues.VEHICLE_MODEL
},
{
scenario: navigationScenarios.SELECTED_STYLE,
destinationIssPageValue: issPageValues.VEHICLE_DAMAGE,
destinationIssPageValue: issPageValues.VEHICLE_DAMAGE
}
]
},
@ -61,298 +61,298 @@ const routingTable = function(store) {
maps: [
{
scenario: navigationScenarios.CLICKED_BACK,
destinationIssPageValue: issPageValues.VEHICLE_STYLE,
destinationIssPageValue: issPageValues.VEHICLE_STYLE
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_REPAIR,
destinationIssPageValue: issPageValues.COVERAGE_STATEMENT,
destinationIssPageValue: issPageValues.COVERAGE_STATEMENT
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_VIN,
destinationIssPageValue: issPageValues.VEHICLE_LOOKUP,
destinationIssPageValue: issPageValues.VEHICLE_LOOKUP
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITHOUT_VIN,
destinationIssPageValue: issPageValues.VEHICLE_LOOKUP,
},
],
destinationIssPageValue: issPageValues.VEHICLE_LOOKUP
}
]
},
{
issPageValue: issPageValues.VEHICLE_LOOKUP,
maps: [
{
scenario: navigationScenarios.CLICKED_BACK,
destinationIssPageValue: issPageValues.VEHICLE_DAMAGE,
destinationIssPageValue: issPageValues.VEHICLE_DAMAGE
},
{
scenario: navigationScenarios.SELECTED_MANUAL_VIN,
destinationIssPageValue: issPageValues.VIN_LOOKUP,
destinationIssPageValue: issPageValues.VIN_LOOKUP
},
{
scenario: navigationScenarios.SELECTED_LICENSE_PLATE,
destinationIssPageValue: issPageValues.LICENSE_PLATE_LOOKUP,
destinationIssPageValue: issPageValues.LICENSE_PLATE_LOOKUP
},
{
scenario: navigationScenarios.SELECTED_HOME_ADDRESS,
destinationIssPageValue: issPageValues.ADDRESS_LOOKUP,
},
],
destinationIssPageValue: issPageValues.ADDRESS_LOOKUP
}
]
},
{
issPageValue: issPageValues.VIN_LOOKUP,
maps: [
{
scenario: navigationScenarios.CLICKED_BACK,
destinationIssPageValue: issPageValues.VEHICLE_LOOKUP,
destinationIssPageValue: issPageValues.VEHICLE_LOOKUP
},
{
scenario: navigationScenarios.SELECTED_VIN_WITH_MISMATCHED_GLASS,
destinationIssPageValue: issPageValues.VEHICLE_DAMAGE,
destinationIssPageValue: issPageValues.VEHICLE_DAMAGE
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_PART_QUESTIONS,
destinationIssPageValue: issPageValues.PART_QUESTIONS,
destinationIssPageValue: issPageValues.PART_QUESTIONS
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_MULTIPLE_PARTS_TO_CHOOSE,
destinationIssPageValue: issPageValues.VEHICLE_PARTS,
destinationIssPageValue: issPageValues.VEHICLE_PARTS
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_MOLDING_QUESTIONS,
destinationIssPageValue: issPageValues.MOLDING_QUESTIONS,
destinationIssPageValue: issPageValues.MOLDING_QUESTIONS
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_CAPABILITY_QUESTIONS,
destinationIssPageValue: issPageValues.CAPABILITY_QUESTIONS,
destinationIssPageValue: issPageValues.CAPABILITY_QUESTIONS
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_NO_MORE_QUESTIONS,
destinationIssPageValue: issPageValues.COVERAGE_STATEMENT,
},
],
destinationIssPageValue: issPageValues.COVERAGE_STATEMENT
}
]
},
{
issPageValue: issPageValues.LICENSE_PLATE_LOOKUP,
maps: [
{
scenario: navigationScenarios.CLICKED_BACK,
destinationIssPageValue: issPageValues.VEHICLE_LOOKUP,
destinationIssPageValue: issPageValues.VEHICLE_LOOKUP
},
{
scenario: navigationScenarios.SELECTED_VIN_WITH_MISMATCHED_GLASS,
destinationIssPageValue: issPageValues.VEHICLE_DAMAGE,
destinationIssPageValue: issPageValues.VEHICLE_DAMAGE
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_NO_MORE_QUESTIONS,
destinationIssPageValue: issPageValues.COVERAGE_STATEMENT,
destinationIssPageValue: issPageValues.COVERAGE_STATEMENT
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_PART_QUESTIONS,
destinationIssPageValue: issPageValues.PART_QUESTIONS,
destinationIssPageValue: issPageValues.PART_QUESTIONS
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_MULTIPLE_PARTS_TO_CHOOSE,
destinationIssPageValue: issPageValues.VEHICLE_PARTS,
destinationIssPageValue: issPageValues.VEHICLE_PARTS
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_MOLDING_QUESTIONS,
destinationIssPageValue: issPageValues.MOLDING_QUESTIONS,
destinationIssPageValue: issPageValues.MOLDING_QUESTIONS
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_CAPABILITY_QUESTIONS,
destinationIssPageValue: issPageValues.CAPABILITY_QUESTIONS,
},
],
destinationIssPageValue: issPageValues.CAPABILITY_QUESTIONS
}
]
},
{
issPageValue: issPageValues.PART_QUESTIONS,
maps: [
{
scenario: navigationScenarios.CLICKED_BACK_WITH_VIN_AND_NO_MORE_QUESTIONS,
destinationIssPageValue: issPageValues.VIN_LOOKUP,
destinationIssPageValue: issPageValues.VIN_LOOKUP
},
{
scenario: navigationScenarios.CLICKED_BACK_WITH_NO_VIN_NOR_QUESTIONS,
destinationIssPageValue: issPageValues.VEHICLE_LOOKUP,
destinationIssPageValue: issPageValues.VEHICLE_LOOKUP
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_MULTIPLE_PARTS_TO_CHOOSE,
destinationIssPageValue: issPageValues.VEHICLE_PARTS,
destinationIssPageValue: issPageValues.VEHICLE_PARTS
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_MOLDING_QUESTIONS,
destinationIssPageValue: issPageValues.MOLDING_QUESTIONS,
destinationIssPageValue: issPageValues.MOLDING_QUESTIONS
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_CAPABILITY_QUESTIONS,
destinationIssPageValue: issPageValues.CAPABILITY_QUESTIONS,
destinationIssPageValue: issPageValues.CAPABILITY_QUESTIONS
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_NO_MORE_QUESTIONS,
destinationIssPageValue: issPageValues.COVERAGE_STATEMENT,
},
],
destinationIssPageValue: issPageValues.COVERAGE_STATEMENT
}
]
},
{
issPageValue: issPageValues.VEHICLE_PARTS,
maps: [
{
scenario: navigationScenarios.CLICKED_FORWARD,
destinationIssPageValue: issPageValues.REVEAL,
destinationIssPageValue: issPageValues.REVEAL
},
{
scenario: navigationScenarios.CLICKED_BACK_WITH_PART_QUESTIONS,
destinationIssPageValue: issPageValues.PART_QUESTIONS,
destinationIssPageValue: issPageValues.PART_QUESTIONS
},
{
scenario: navigationScenarios.CLICKED_BACK_WITH_VIN_AND_NO_MORE_QUESTIONS,
destinationIssPageValue: issPageValues.VIN_LOOKUP,
destinationIssPageValue: issPageValues.VIN_LOOKUP
},
{
scenario: navigationScenarios.CLICKED_BACK_WITH_NO_VIN_NOR_QUESTIONS,
destinationIssPageValue: issPageValues.VEHICLE_LOOKUP,
destinationIssPageValue: issPageValues.VEHICLE_LOOKUP
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_MOLDING_QUESTIONS,
destinationIssPageValue: issPageValues.MOLDING_QUESTIONS,
destinationIssPageValue: issPageValues.MOLDING_QUESTIONS
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_CAPABILITY_QUESTIONS,
destinationIssPageValue: issPageValues.CAPABILITY_QUESTIONS,
destinationIssPageValue: issPageValues.CAPABILITY_QUESTIONS
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_NO_MORE_QUESTIONS,
destinationIssPageValue: issPageValues.COVERAGE_STATEMENT,
},
],
destinationIssPageValue: issPageValues.COVERAGE_STATEMENT
}
]
},
{
issPageValue: issPageValues.MOLDING_QUESTIONS,
maps: [
{
scenario: navigationScenarios.CLICKED_BACK_WITH_VIN_AND_NO_MORE_QUESTIONS,
destinationIssPageValue: issPageValues.VIN_LOOKUP,
destinationIssPageValue: issPageValues.VIN_LOOKUP
},
{
scenario: navigationScenarios.CLICKED_BACK_WITH_NO_VIN_NOR_QUESTIONS,
destinationIssPageValue: issPageValues.VEHICLE_LOOKUP,
destinationIssPageValue: issPageValues.VEHICLE_LOOKUP
},
{
scenario: navigationScenarios.CLICKED_BACK_WITH_PART_QUESTIONS,
destinationIssPageValue: issPageValues.PART_QUESTIONS,
destinationIssPageValue: issPageValues.PART_QUESTIONS
},
{
scenario: navigationScenarios.CLICKED_BACK_WITH_MULTIPLE_PARTS_TO_CHOOSE,
destinationIssPageValue: issPageValues.VEHICLE_PARTS,
destinationIssPageValue: issPageValues.VEHICLE_PARTS
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_CAPABILITY_QUESTIONS,
destinationIssPageValue: issPageValues.CAPABILITY_QUESTIONS,
destinationIssPageValue: issPageValues.CAPABILITY_QUESTIONS
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_NO_MORE_QUESTIONS,
destinationIssPageValue: issPageValues.COVERAGE_STATEMENT,
},
],
destinationIssPageValue: issPageValues.COVERAGE_STATEMENT
}
]
},
{
issPageValue: issPageValues.CAPABILITY_QUESTIONS,
maps: [
{
scenario: navigationScenarios.CLICKED_BACK_WITH_VIN_AND_NO_MORE_QUESTIONS,
destinationIssPageValue: issPageValues.VIN_LOOKUP,
destinationIssPageValue: issPageValues.VIN_LOOKUP
},
{
scenario: navigationScenarios.CLICKED_BACK_WITH_NO_VIN_NOR_QUESTIONS,
destinationIssPageValue: issPageValues.VEHICLE_LOOKUP,
destinationIssPageValue: issPageValues.VEHICLE_LOOKUP
},
{
scenario: navigationScenarios.CLICKED_BACK_WITH_PART_QUESTIONS,
destinationIssPageValue: issPageValues.PART_QUESTIONS,
destinationIssPageValue: issPageValues.PART_QUESTIONS
},
{
scenario: navigationScenarios.CLICKED_BACK_WITH_MULTIPLE_PARTS_TO_CHOOSE,
destinationIssPageValue: issPageValues.VEHICLE_PARTS,
destinationIssPageValue: issPageValues.VEHICLE_PARTS
},
{
scenario: navigationScenarios.CLICKED_BACK_WITH_MOLDING_QUESTIONS,
destinationIssPageValue: issPageValues.MOLDING_QUESTIONS,
destinationIssPageValue: issPageValues.MOLDING_QUESTIONS
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_NO_MORE_QUESTIONS,
destinationIssPageValue: issPageValues.COVERAGE_STATEMENT,
},
],
destinationIssPageValue: issPageValues.COVERAGE_STATEMENT
}
]
},
{
issPageValue: issPageValues.ADDRESS_LOOKUP,
maps: [
{
scenario: navigationScenarios.CLICKED_BACK,
destinationIssPageValue: issPageValues.VEHICLE_LOOKUP,
destinationIssPageValue: issPageValues.VEHICLE_LOOKUP
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_MULTIPLE_VEHICLES,
destinationIssPageValue: issPageValues.ADDRESS_VEHICLES,
destinationIssPageValue: issPageValues.ADDRESS_VEHICLES
},
{
scenario: navigationScenarios.SELECTED_VIN_WITH_MISMATCHED_GLASS,
destinationIssPageValue: issPageValues.VEHICLE_DAMAGE,
destinationIssPageValue: issPageValues.VEHICLE_DAMAGE
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_PART_QUESTIONS,
destinationIssPageValue: issPageValues.PART_QUESTIONS,
destinationIssPageValue: issPageValues.PART_QUESTIONS
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_MULTIPLE_PARTS_TO_CHOOSE,
destinationIssPageValue: issPageValues.VEHICLE_PARTS,
destinationIssPageValue: issPageValues.VEHICLE_PARTS
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_MOLDING_QUESTIONS,
destinationIssPageValue: issPageValues.MOLDING_QUESTIONS,
destinationIssPageValue: issPageValues.MOLDING_QUESTIONS
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_CAPABILITY_QUESTIONS,
destinationIssPageValue: issPageValues.CAPABILITY_QUESTIONS,
destinationIssPageValue: issPageValues.CAPABILITY_QUESTIONS
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_NO_MORE_QUESTIONS,
destinationIssPageValue: issPageValues.COVERAGE_STATEMENT,
},
],
destinationIssPageValue: issPageValues.COVERAGE_STATEMENT
}
]
},
{
issPageValue: issPageValues.ADDRESS_VEHICLES,
maps: [
{
scenario: navigationScenarios.CLICKED_BACK,
destinationIssPageValue: issPageValues.ADDRESS_LOOKUP,
destinationIssPageValue: issPageValues.ADDRESS_LOOKUP
},
{
scenario: navigationScenarios.SELECTED_VIN_WITH_MISMATCHED_GLASS,
destinationIssPageValue: issPageValues.VEHICLE_DAMAGE,
destinationIssPageValue: issPageValues.VEHICLE_DAMAGE
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_PART_QUESTIONS,
destinationIssPageValue: issPageValues.PART_QUESTIONS,
destinationIssPageValue: issPageValues.PART_QUESTIONS
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_MULTIPLE_PARTS_TO_CHOOSE,
destinationIssPageValue: issPageValues.VEHICLE_PARTS,
destinationIssPageValue: issPageValues.VEHICLE_PARTS
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_MOLDING_QUESTIONS,
destinationIssPageValue: issPageValues.MOLDING_QUESTIONS,
destinationIssPageValue: issPageValues.MOLDING_QUESTIONS
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_CAPABILITY_QUESTIONS,
destinationIssPageValue: issPageValues.CAPABILITY_QUESTIONS,
destinationIssPageValue: issPageValues.CAPABILITY_QUESTIONS
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_NO_MORE_QUESTIONS,
destinationIssPageValue: issPageValues.COVERAGE_STATEMENT,
},
],
destinationIssPageValue: issPageValues.COVERAGE_STATEMENT
}
]
},
{
issPageValue: issPageValues.ENTRY_PAGE,
@ -364,8 +364,8 @@ const routingTable = function(store) {
{
scenario: navigationScenarios.MOVE_FORWARD_ENTRY_PAGE,
destinationIssPageValue: issPageValues.WELCOME_PAGE
},
],
}
]
},
{
issPageValue: issPageValues.WELCOME_PAGE,
@ -377,8 +377,8 @@ const routingTable = function(store) {
{
scenario: navigationScenarios.CLICKED_FORWARD_WELCOME_PAGE,
destinationIssPageValue: issPageValues.POLICY_HOLDER_DETAILS
},
],
}
]
},
{
issPageValue: issPageValues.POLICY_HOLDER_DETAILS,
@ -390,55 +390,54 @@ const routingTable = function(store) {
{
scenario: navigationScenarios.CLICKED_FORWARD_POLICY_HOLDER_DETAILS,
destinationIssPageValue: issPageValues.VEHICLE_YEAR
},
],
}
]
},
{
issPageValue: issPageValues.COVERAGE_STATEMENT,
maps: [
{
scenario: navigationScenarios.CLICKED_BACK_WITH_VIN_AND_NO_MORE_QUESTIONS,
destinationIssPageValue: issPageValues.VIN_LOOKUP,
destinationIssPageValue: issPageValues.VIN_LOOKUP
},
{
scenario: navigationScenarios.CLICKED_BACK_WITH_NO_VIN_NOR_QUESTIONS,
destinationIssPageValue: issPageValues.VEHICLE_LOOKUP,
destinationIssPageValue: issPageValues.VEHICLE_LOOKUP
},
{
scenario: navigationScenarios.CLICKED_BACK_WITH_PART_QUESTIONS,
destinationIssPageValue: issPageValues.PART_QUESTIONS,
destinationIssPageValue: issPageValues.PART_QUESTIONS
},
{
scenario: navigationScenarios.CLICKED_BACK_WITH_MULTIPLE_PARTS_TO_CHOOSE,
destinationIssPageValue: issPageValues.VEHICLE_PARTS,
destinationIssPageValue: issPageValues.VEHICLE_PARTS
},
{
scenario: navigationScenarios.CLICKED_BACK_WITH_MOLDING_QUESTIONS,
destinationIssPageValue: issPageValues.MOLDING_QUESTIONS,
destinationIssPageValue: issPageValues.MOLDING_QUESTIONS
},
{
scenario: navigationScenarios.CLICKED_BACK_WITH_CAPABILITY_QUESTIONS,
destinationIssPageValue: issPageValues.CAPABILITY_QUESTIONS,
destinationIssPageValue: issPageValues.CAPABILITY_QUESTIONS
},
{
scenario: navigationScenarios.CLICKED_FORWARD_COVERAGE_STATEMENT,
destinationIssPageValue: issPageValues.PROVIDER_PREFERENCE,
},
],
destinationIssPageValue: issPageValues.PROVIDER_PREFERENCE
}
]
},
{
issPageValue: issPageValues.PROVIDER_PREFERENCE,
maps: [
{
scenario: navigationScenarios.CLICKED_BACK,
destinationIssPageValue: issPageValues.COVERAGE_STATEMENT,
destinationIssPageValue: issPageValues.COVERAGE_STATEMENT
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_SAFELITE,
destinationIssPageValue: issPageValues.SERVICE_LOCATION,
},
],
destinationIssPageValue: issPageValues.SERVICE_LOCATION
}
]
},
{
issPageValue: issPageValues.SERVICE_LOCATION,
@ -447,30 +446,68 @@ const routingTable = function(store) {
scenario: navigationScenarios.CLICKED_BACK,
destinationIssPageValue: issPageValues.PROVIDER_PREFERENCE,
},
],
{
scenario: navigationScenarios.CLICKED_FORWARD,
destinationIssPageValue: issPageValues.SERVICE_PACKAGES
}
]
},
{
issPageValue: issPageValues.SCHEDULE_PAGE,
maps: [
{
scenario: navigationScenarios.CLICKED_BACK,
destinationIssPageValue: issPageValues.SERVICE_LOCATION,
},
],
destinationIssPageValue: issPageValues.SERVICE_LOCATION
}
]
},
{
issPageValue: issPageValues.CONTACT_DETAILS,
maps: [
{
scenario: navigationScenarios.CLICKED_BACK,
destinationIssPageValue: issPageValues.SCHEDULE_PAGE,
destinationIssPageValue: issPageValues.SCHEDULE_PAGE
}
]
},
{
issPageValue: issPageValues.SERVICE_PACKAGE,
maps: [
{
scenario: navigationScenarios.CLICKED_BACK,
destinationIssPageValue: issPageValues.SERVICE_LOCATION
},
{
scenario: navigationScenarios.CLICKED_FORWARD,
destinationIssPageValue: issPageValues.REVIEW_ORDER
}
]
},
{
issPageValue: issPageValues.REVIEW_PAGE,
maps: [
{
scenario: navigationScenarios.CLICKED_BACK,
destinationIssPageValue: issPageValues.SERVICE_PACKAGE,
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_REVIEW,
destinationIssPageValue: issPageValues.PAYMENT_PAGE,
},
],
},
{
issPageValue: issPageValues.PAYMENT_PAGE,
maps: [
{
scenario: navigationScenarios.CLICKED_BACK,
destinationIssPageValue: issPageValues.REVIEW_PAGE
}
]
}
];
};

View file

@ -30,8 +30,8 @@ const getDefaultState = () => {
state: null,
zipCode: null,
firstName: null,
lastName: null,
},
lastName: null
}
},
damage: {
isRepair: null,
@ -39,7 +39,7 @@ const getDefaultState = () => {
glassToReplace: null,
partQuestionAnswers: null,
moldingQuestionAnswers: null,
capabilityQuestionAnswers: null,
capabilityQuestionAnswers: null
},
policy: {
policyNumber: null,
@ -47,7 +47,7 @@ const getDefaultState = () => {
damageCause: null,
damageState: null,
damageCity: null,
isDamageGlassOnly: null,
isDamageGlassOnly: null
},
customer: {
address: {
@ -55,12 +55,12 @@ const getDefaultState = () => {
streetAddress2: null,
city: null,
state: null,
zipCode: null,
zipCode: null
},
firstName: null,
lastName: null,
emailAddress: null,
phoneNumber: null,
phoneNumber: null
},
serviceLocation: {
address: null,
@ -78,16 +78,14 @@ const getDefaultState = () => {
payment: {
isInsurance: true,
insuranceCoverage: {
isVerified: false,
},
isVerified: false
}
},
referralNumber: null,
referralDate: null,
referralDate: null
},
applicationUser: {
lastPageVisited: null,
experiments: [],
triggeredSiteEntry: false,
eventBus: [],
pageData: {},
savedSessionTimeout: getDateForSavedSessionTimeout(),
@ -95,16 +93,18 @@ const getDefaultState = () => {
savedSessionId: null,
crmCustomerId: null,
lastPageVisited: null,
triggeredSiteEntry: false,
triggeredSiteEntry: false
},
issConfig: {
clientName: "Generic Insurance", // this is the default and will be overriden by the client's name
clientDisplayName: "Generic Insurance",
styleSheet: "",
accountNumber: 0,
enableTPAFlow: false,
isCoverageEnabled: false, // Indicates if we should be calling coverage on this flow.
isAuthenticated: false, // Indicates if user is authenticated or not.
enableTPAFlow: false, // Indicates if the TPA flow is supported for this client.
returnURL: null,
returnURL2: null,
returnURL2: null
}
};
};
@ -118,6 +118,7 @@ export const useMainStore = defineStore({
vehicle: (state) => state.order.vehicle,
damage: (state) => state.order.damage,
lineItems: (state) => state.order.lineItems,
hasAnyNonWindshieldGlassParts: (state) => !state.order.policy.isDamageGlassOnly,
eventBusItem: (state) => ( eventCategory, eventSubCategory) => {
const matchedEvent = state.applicationUser.eventBus.find(
@ -233,8 +234,8 @@ export const useMainStore = defineStore({
licenseLastName: licenseLastName,
licenseStreetAddress: licenseStreetAddress,
licenseZip: licenseZip,
licenseState: licenseState,
},
licenseState: licenseState
}
});
},
@ -243,14 +244,14 @@ export const useMainStore = defineStore({
method: endpoints.GetRouteInfo.method,
endpoint: endpoints.GetRouteInfo.url(applicationConfig.APPLICATION_ABBREVIATION),
payload: {
pageName: pageName,
pageName: pageName
},
});
},
getHomepageName() {
return globalMethods.callHttpClient({
method: endpoints.GetHomepageInfo.method,
endpoint: endpoints.GetHomepageInfo.url(applicationConfig.APPLICATION_ABBREVIATION),
endpoint: endpoints.GetHomepageInfo.url(applicationConfig.APPLICATION_ABBREVIATION)
});
},
@ -258,7 +259,7 @@ export const useMainStore = defineStore({
return globalMethods.callHttpClient({
method: endpoints.GetPageData.method,
endpoint: endpoints.GetPageData.url(applicationConfig.APPLICATION_ABBREVIATION, pageName),
payload: {},
payload: {}
});
},
@ -267,7 +268,7 @@ export const useMainStore = defineStore({
return globalMethods.callHttpClient({
method: endpoints.GetVehicleYears.method,
endpoint: endpoints.GetVehicleYears.url,
payload: {},
payload: {}
});
},
@ -275,7 +276,7 @@ export const useMainStore = defineStore({
return globalMethods.callHttpClient({
method: endpoints.GetVehicleMakes.method,
endpoint: endpoints.GetVehicleMakes.url + this.order.vehicle.year,
payload: {},
payload: {}
});
},
@ -283,7 +284,7 @@ export const useMainStore = defineStore({
return globalMethods.callHttpClient({
method: endpoints.GetVehicleModels.method,
endpoint: `${endpoints.GetVehicleModels.url}/${this.order.vehicle.year}/${this.order.vehicle.make}`,
payload: {},
payload: {}
});
},
@ -291,7 +292,7 @@ export const useMainStore = defineStore({
return globalMethods.callHttpClient({
method: endpoints.GetVehicleStyles.method,
endpoint: `${endpoints.GetVehicleStyles.url}/${this.order.vehicle.year}/${this.order.vehicle.make}/${this.order.vehicle.model}`,
payload: {},
payload: {}
});
},
@ -299,7 +300,7 @@ export const useMainStore = defineStore({
return globalMethods.callHttpClient({
methods: endpoints.GetDamageOptions.method,
endpoint: `${endpoints.GetDamageOptions.url}/${carId}`,
payload: {},
payload: {}
});
},
getIsVinbyAddressPermissible(){
@ -307,36 +308,36 @@ export const useMainStore = defineStore({
const response = globalMethods.callHttpClient({
method:endpoints.IsVinbyAddressPermissible.method,
endpoint:`${endpoints.IsVinbyAddressPermissible.url}?state=${this.order.customer.address.state}`,
payload: {},
payload: {}
});
return response;
} catch (responseError) {
return {
error: {
status: responseError.status,
},
status: responseError.status
}
};
}
},
async lookupVinByPlate(licensePlate, licenseState) {
try {
const response = await globalMethods.callHttpClient({
method: endpoints.LookupVinByPlate.method,
endpoint: endpoints.LookupVinByPlate.url,
payload: {
licensePlate: licensePlate,
licenseState: licenseState,
},
});
try {
const response = await globalMethods.callHttpClient({
method: endpoints.LookupVinByPlate.method,
endpoint: endpoints.LookupVinByPlate.url,
payload: {
licensePlate: licensePlate,
licenseState: licenseState
}
});
return response;
} catch (responseError) {
return {
error: {
status: responseError.status,
},
};
}
return response;
} catch (responseError) {
return {
error: {
status: responseError.status
}
};
}
},
// PartsOrQuestions API Actions
@ -361,8 +362,8 @@ export const useMainStore = defineStore({
carId: carId,
glassPieces: glassArrayForPayload,
zip: zipCode,
vin: vin,
},
vin: vin
}
});
// Flatten location and name properties
@ -396,8 +397,8 @@ export const useMainStore = defineStore({
glassPieces: glassArrayForPayload,
answerResults: resultsArrayForPayload,
zip: zipCode,
vin: vin,
},
vin: vin
}
});
// Flatten location and name properties
@ -411,7 +412,7 @@ export const useMainStore = defineStore({
getCapabilityQuestions(carId, partNumber) {
return globalMethods.callHttpClient({
method: endpoints.GetCapabilityQuestions.method,
endpoint: `${endpoints.GetCapabilityQuestions.url}/${carId}/${partNumber}`,
endpoint: `${endpoints.GetCapabilityQuestions.url}/${carId}/${partNumber}`
});
},
@ -429,11 +430,101 @@ export const useMainStore = defineStore({
method: endpoints.GetPartFromCapabilityAnswer.method,
endpoint: endpoints.GetPartFromCapabilityAnswer.url,
payload: {
part,
capabilityAnswerResults: capabilityQuestionAnswersForPart,
},
part,
capabilityAnswerResults: capabilityQuestionAnswersForPart
}
});
},
async getWipers() {
const carId = this.order.vehicle.carId;
///WARNING
///TODO: this is temp test code until serviceLocation is complete.
//const serviceZipCode = this.order.serviceLocation.zipCode;
const serviceZipCode = '44902';
return globalMethods
.callHttpClient({
method: endpoints.GetWipers.method,
endpoint: `${endpoints.GetWipers.url}/${carId}/${serviceZipCode}`
})
.catch((error) => {
// The wiper service sometimes returns 500s on legitimate carId/zipCode combination - return empty array instead of breaking flow
console.error(error);
return [];
});
},
async getRainDefense() {
return globalMethods
.callHttpClient({
method: endpoints.GetRainDefense.method,
endpoint: `${endpoints.GetRainDefense.url}`
})
.catch((error) => {
// The wiper service sometimes returns 500s on legitimate carId/zipCode combination - return empty array instead of breaking flow
console.error(error);
return [];
});
},
async getSupportingItems() {
const glassPartsArray = this.order.lineItems.glassParts ?? [];
const carId = this.order.vehicle.carId;
const isRepair = this.order.damage.isRepair;
const numberOfChips = this.order.damage.numberOfChips;
return globalMethods
.callHttpClient({
method: endpoints.GetSupportingItems.method,
endpoint: endpoints.GetSupportingItems.url,
payload: {
carId: carId,
serviceType: isRepair ? 'Repair' : 'Replace',
parentAccountNumber: applicationConfig.CASH_PARENT_ACCOUNT_NUMBER,
parts: glassPartsArray,
numberOfRepairChips: isRepair ? numberOfChips : 0
}
});
},
async getPriceOrderItems(availableLineItems) {
let zipCodeToUse = this.order.serviceLocation.zipCode;
let ctuToUse = this.order.serviceLocation.zipCodeCtu;
const availableLineItemsFormattedForRequest =
getLineItemQueryStringForPricing(availableLineItems);
const vehicle = this.order.vehicle;
///WARNING
///TODO: this is temp test code until serviceLocation is complete.
/// and ctu is available. Also, EON may need to be implemented.
zipCodeToUse = "44902"
ctuToUse = "01820"
let queryString =
`ParentAccountNumber=${applicationConfig.CASH_PARENT_ACCOUNT_NUMBER}` +
`&CTU=${ctuToUse}` +
`&CarId=${vehicle.carId}` +
`&Make=${vehicle.make}` +
`&Model=${vehicle.model}` +
`&Year=${vehicle.year}` +
`&EON=0` +
`&ZipCode=${zipCodeToUse}` +
`${availableLineItemsFormattedForRequest}`;
const response = await globalMethods
.callHttpClient({
method: endpoints.GetPriceOrderItems.method,
endpoint: `${endpoints.GetPriceOrderItems.url}?${queryString}`
}).catch((error) => {
console.error(error);
return [];
});
availableLineItems = addPricesToLineItems(availableLineItems, response.data.lineItems)
return availableLineItems;
},
getServiceabilityDetails({ serviceZipCode }) {
const lineItemsToSend = this.order.lineItems.supportingItems;
@ -459,7 +550,7 @@ export const useMainStore = defineStore({
.callHttpClient({
methods: endpoints.GetVehicle.method,
endpoint: `${endpoints.GetVehicle.url}/${this.order.vehicle.year}/${this.order.vehicle.make}/${this.order.vehicle.model}/${this.order.vehicle.style}`,
payload: {},
payload: {}
})
.then((response) => {
this.updateVehicle(response.data);
@ -620,6 +711,8 @@ export const useMainStore = defineStore({
this.issConfig.clientDisplayName = "Generic Insurance";
this.issConfig.accountNumber = 0;
this.issConfig.styleSheet = "";
this.issConfig.isCoverageEnabled = false;
this.issConfig.isAuthenticated = false;
this.issConfig.enableTPAFlow = false;
this.issConfig.returnURL = null;
this.issConfig.returnURL2 = null;
@ -781,7 +874,7 @@ export const useMainStore = defineStore({
this.updateCapabilityQuestionAnswers(null);
this.updatePageData({
page: issPageValues.CAPABILITY_QUESTIONS,
data: null,
data: null
});
}
@ -856,9 +949,9 @@ export const useMainStore = defineStore({
userPartitionNumber: experiment.userPartitionNumber,
assignmentId: experiment.assignmentId,
sessionKey: sessionKey,
pageName: pageName,
},
},
pageName: pageName
}
}
});
},
logPageView({ userId, sessionKey, pageName, sessionId, action, event, shouldUseSessionId, experimentsForUser })
@ -879,7 +972,7 @@ export const useMainStore = defineStore({
method: endpoints.LogPageView.method,
endpoint: endpoints.LogPageView.url,
payload: payload,
logApiCall: false,
logApiCall: false
});
},
logCustomEvent({ userId, sessionKey, pageName, sessionId, category, action, label, value, shouldUseSessionId, experimentsForUser})
@ -898,14 +991,14 @@ export const useMainStore = defineStore({
label: label,
value: value,
shouldUseSessionId: shouldUseSessionId,
experimentsForUser: experimentsForUser,
experimentsForUser: experimentsForUser
};
return globalMethods.callHttpClient({
method: endpoints.LogCustomEvent.method,
endpoint: endpoints.LogCustomEvent.url,
payload: payload,
logApiCall: false,
logApiCall: false
});
},
initializeSession({ userId, sessionId, userAgent, referrer }) {
@ -917,14 +1010,14 @@ export const useMainStore = defineStore({
userAgent: userAgent,
operatorId: "WEB",
userName: "SafeliteISS",
referrer: referrer,
referrer: referrer
};
return globalMethods.callHttpClient({
method: endpoints.InitializeSession.method,
endpoint: endpoints.InitializeSession.url,
payload: payload,
logApiCall: false,
logApiCall: false
});
},
@ -943,7 +1036,7 @@ export const useMainStore = defineStore({
return globalMethods.callHttpClient({
method: endpoints.GetExperimentsByUser.method,
endpoint: `${endpoints.GetExperimentsByUser.url}/${userId}`,
payload: {},
payload: {}
});
},
@ -957,13 +1050,13 @@ export const useMainStore = defineStore({
userId: userId,
triggerEvent: triggerEvent,
triggerValue: triggerValue,
experimentOrder: this.experimentOrder,
experimentOrder: this.experimentOrder
};
const response = await globalMethods.callHttpClient({
method: endpoints.RunExperimentsForTrigger.method,
endpoint: endpoints.RunExperimentsForTrigger.url,
payload: payload,
payload: payload
});
this.updateExperiments(response.data.experiments);
@ -972,14 +1065,14 @@ export const useMainStore = defineStore({
async validateZip({ zip }) {
return await globalMethods.callHttpClient({
methods: endpoints.ValidateZip.method,
endpoint: `${endpoints.ValidateZip.url}/${zip}`,
endpoint: `${endpoints.ValidateZip.url}/${zip}`
});
},
async validateClientTag(clientTag) {
return await globalMethods.callHttpClient({
methods: endpoints.ValidateClientTag.method,
endpoint: `${endpoints.ValidateClientTag.url}/${clientTag}`,
endpoint: `${endpoints.ValidateClientTag.url}/${clientTag}`
});
},
@ -1130,7 +1223,7 @@ function convertGlassPieceNamingForApi(glassArray) {
glassArray.forEach((glass) => {
converted.push({
location: glass.glassLocation,
name: glass.glassName,
name: glass.glassName
});
});
return converted;
@ -1143,7 +1236,7 @@ function convertResultsForApi(resultsArray) {
converted.push({
location: answer.glassLocation,
name: answer.glassName,
result: answer.result,
result: answer.result
});
});
return converted;
@ -1170,3 +1263,31 @@ function getAllPartNumbers(partsOrQuestions) {
.join(",")
: [];
}
function addPricesToLineItems(lineItems, pricingLineItems) {
lineItems.forEach((lineItem) => {
const lineItemIndex = pricingLineItems.findIndex(
(pricingLineItem) => pricingLineItem.partNumber === lineItem.partNumber
)
if (lineItem.childParts) {
addPricesToLineItems(lineItem.childParts, pricingLineItems)
}
const pricedLineItem = pricingLineItems.splice(lineItemIndex, 1)[0]
lineItem.laborAmount = pricedLineItem.laborAmount
lineItem.sellingPrice = pricedLineItem.sellingPrice
lineItem.kitPrice = pricedLineItem.kitPrice
});
return lineItems;
}
function getLineItemQueryStringForPricing(lineItems) {
return lineItems
.map((lineItem) => {
let queryStringSnippet = `&LineItems=${lineItem.partNumber}`;
if (lineItem.childParts) {
queryStringSnippet += getLineItemQueryStringForPricing(lineItem.childParts);
}
return queryStringSnippet;
})
.join('');
}

View file

@ -3,7 +3,6 @@ process.env.VUE_APP_CURRENT_ENVIRONMENT = "Localhost";
process.env.VUE_APP_GOOGLE_PLACES_API_KEY =
"AIzaSyCuLhQcDdZTTb4JzpUFms1OCch2dk5lHF0";
// GA & GTM
// NOTE: Using the old ISS site GTM Cotnainer ID for now, will create a new one soon.
process.env.VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY = "(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);})(window,document,'script','dataLayer','GTM-KKNWZ3');";
@ -22,11 +21,11 @@ module.exports = {
@import "./node_modules/bootstrap/scss/variables";
@import "./node_modules/bootstrap/scss/mixins";
@import "@/styles/mixins/customMixins";
`,
},
},
},
configureWebpack: {
devtool: 'source-map'
`
}
}
},
configureWebpack: {
devtool: 'source-map'
}
};