Merge branch 'develop' into feature/digital/SSR-621
This commit is contained in:
commit
453ddc4ca3
23 changed files with 1925 additions and 167 deletions
|
|
@ -43,6 +43,10 @@ const endpoints = Object.freeze({
|
|||
url: '/price/api/v1/price/order-items',
|
||||
method: 'GET'
|
||||
},
|
||||
GetProviders: {
|
||||
url: '/location/api/v1/location/providers',
|
||||
method: 'GET'
|
||||
},
|
||||
GetCapabilityQuestions: {
|
||||
url: '/parts/api/v1/parts/capability-questions',
|
||||
method: 'GET'
|
||||
|
|
@ -63,6 +67,10 @@ const endpoints = Object.freeze({
|
|||
url: '/parts/api/v1/parts/supporting-items',
|
||||
method: 'POST'
|
||||
},
|
||||
GetMobileFeePart: {
|
||||
url: '/parts/api/v1/parts/mobile-fee',
|
||||
method: 'GET'
|
||||
},
|
||||
GetServiceabilityDetails: {
|
||||
url: '/location/api/v1/location/serviceability-details',
|
||||
method: 'GET'
|
||||
|
|
|
|||
13
src/constants/schedule-constants.js
Normal file
13
src/constants/schedule-constants.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
const AppointmentTypeStrings = {
|
||||
IN_SHOP: 'Inshop',
|
||||
MOBILE: 'Mobile',
|
||||
DROP_OFF: 'Dropoff'
|
||||
};
|
||||
const PREMIUM_FEE_PART_TYPE = 'EARLY BIRD';
|
||||
const PREMIUM_TIME_SLOT_ID_FLAG = '-PREMIUM';
|
||||
const RouteCodeFlags = {
|
||||
ALL_DAY_DROP_OFF: 'ALL DAY DROP OFF',
|
||||
OVERNIGHT_DROP_OFF: 'OVERNIGHT DROP OFF'
|
||||
};
|
||||
|
||||
export { AppointmentTypeStrings, PREMIUM_TIME_SLOT_ID_FLAG, PREMIUM_FEE_PART_TYPE, RouteCodeFlags };
|
||||
|
|
@ -47,6 +47,7 @@ import { Modal } from 'bootstrap';
|
|||
import { useForm } from 'vee-validate';
|
||||
|
||||
export default {
|
||||
// eslint-disable-next-line vue/multi-word-component-names
|
||||
name: 'modal',
|
||||
components: {
|
||||
modalButtonMain
|
||||
|
|
@ -118,11 +119,19 @@ export default {
|
|||
|
||||
<style lang="scss" scoped>
|
||||
.modal {
|
||||
overflow: hidden;
|
||||
top: auto;
|
||||
bottom: 0;
|
||||
h5,
|
||||
strong,
|
||||
|
||||
:deep(h5) {
|
||||
color: $black;
|
||||
}
|
||||
|
||||
:deep(p) {
|
||||
strong {
|
||||
color: $black;
|
||||
}
|
||||
}
|
||||
|
||||
.subheader-text {
|
||||
color: $black;
|
||||
}
|
||||
|
|
@ -141,6 +150,13 @@ export default {
|
|||
color: $black;
|
||||
}
|
||||
}
|
||||
.modal-footer {
|
||||
position: sticky;
|
||||
width: 100%;
|
||||
bottom: 0;
|
||||
z-index: 5;
|
||||
background-color: $gray-100;
|
||||
}
|
||||
&.modal-component {
|
||||
.modal-dialog {
|
||||
max-width: 576px;
|
||||
|
|
|
|||
|
|
@ -17,6 +17,16 @@ const mockProps = {
|
|||
justifyText: 'mockJustifyText'
|
||||
};
|
||||
|
||||
// Mock fetchCmsContentForPage
|
||||
jest.mock('@/helpers/cms-content-helper', () => ({
|
||||
doesCopyContainRouterLink: jest.fn(),
|
||||
doesCopyContainTextLink: jest.fn(),
|
||||
splitCopyOnCMSPlaceHolder: jest.fn(() => mockCmsContent.Text.split(/{(.*?)}/g)),
|
||||
getRouterLinkRouteFromCopy: jest.fn(),
|
||||
getRouterLinkDisplayTextFromCopy: jest.fn(),
|
||||
getExternalLink: jest.fn()
|
||||
}));
|
||||
|
||||
describe('modal.vue', () => {
|
||||
it("Should display 'Text' when 'Text' is defined in the CMS", async () => {
|
||||
// Act
|
||||
|
|
|
|||
|
|
@ -1,23 +1,91 @@
|
|||
<template>
|
||||
<div
|
||||
class="text-block w-100"
|
||||
:class="[justifyText, typeStyle, fontWeight]"
|
||||
v-html="this.TextBlockCopy"></div>
|
||||
:class="[justifyText, typeStyle, fontWeight, marginTopClass]">
|
||||
<span
|
||||
v-for="copy in splitCopyOnCMSPlaceHolder(textBlockCopy)"
|
||||
:key="copy">
|
||||
<span v-if="doesCopyContainRouterLink(copy)">
|
||||
<textLink
|
||||
linkType="text"
|
||||
:text="getRouterLinkDisplayTextFromCopy(copy)"
|
||||
href="javascript:void(0)"
|
||||
useLoadingModal
|
||||
@click-event="navigateWithScenario(getRouterLinkRouteFromCopy(copy))" />
|
||||
</span>
|
||||
<span v-else-if="doesCopyContainTextLink(copy)">
|
||||
<textLink
|
||||
linkType="text"
|
||||
:text="getRouterLinkDisplayTextFromCopy(copy)"
|
||||
:href="getExternalLink(copy)"
|
||||
:data-bs-target="'#' + getRouterLinkRouteFromCopy(copy)"
|
||||
@click-event="$emit('textLinkClicked', getRouterLinkRouteFromCopy(copy))" />
|
||||
</span>
|
||||
<span
|
||||
v-else
|
||||
v-html="copy"></span>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// Components
|
||||
import textLink from '@/ux-components/text-link/text-link.vue';
|
||||
|
||||
// Supporting files
|
||||
import {
|
||||
doesCopyContainRouterLink,
|
||||
doesCopyContainTextLink,
|
||||
splitCopyOnCMSPlaceHolder,
|
||||
getRouterLinkRouteFromCopy,
|
||||
getRouterLinkDisplayTextFromCopy,
|
||||
getExternalLink
|
||||
} from '@/helpers/cms-content-helper';
|
||||
import applicationConfig from '@/constants/application-config';
|
||||
|
||||
export default {
|
||||
name: 'textBlock',
|
||||
name: 'text-block',
|
||||
components: {
|
||||
textLink
|
||||
},
|
||||
props: {
|
||||
customText: String, // used to allow the insert of token values into textblock
|
||||
justifyText: String, // left, right, center
|
||||
typeStyle: String, // h1-h6, body, small, label, caption
|
||||
// (see Figma or Confluence documentation)
|
||||
fontWeight: String, // bold=500, default is 400
|
||||
cmsWidgetName: String
|
||||
cmsWidgetName: String,
|
||||
marginTopSizeOverride: Number // override mt-2 with a bootstrap size from 0-5 or auto
|
||||
},
|
||||
computed: {
|
||||
TextBlockCopy() {
|
||||
pageQueryString() {
|
||||
return applicationConfig.PAGE_QUERYSTRING;
|
||||
},
|
||||
textBlockCopy() {
|
||||
if (this.customText) {
|
||||
return this.customText;
|
||||
}
|
||||
return this.getCmsContent(this.cmsWidgetName, 'Text');
|
||||
},
|
||||
marginTopClass() {
|
||||
if (this.marginTopSizeOverride === 'auto') {
|
||||
return 'mt-auto';
|
||||
}
|
||||
if (this.marginTopSizeOverride >= 0 && this.marginTopSizeOverride <= 5) {
|
||||
return `mt-${this.marginTopSizeOverride}`;
|
||||
}
|
||||
return 'mt-2';
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
doesCopyContainRouterLink,
|
||||
doesCopyContainTextLink,
|
||||
splitCopyOnCMSPlaceHolder,
|
||||
getRouterLinkRouteFromCopy,
|
||||
getRouterLinkDisplayTextFromCopy,
|
||||
getExternalLink,
|
||||
navigateWithScenario(scenarioName) {
|
||||
this.$router.navigateWithoutSaving(scenarioName, this.$route);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -30,15 +98,16 @@ export default {
|
|||
justify-content: flex-start;
|
||||
}
|
||||
&.right {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
text-align: right;
|
||||
}
|
||||
&.center {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
&.bold {
|
||||
font-weight: 500;
|
||||
}
|
||||
&.dark {
|
||||
color: $black;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -12,9 +12,11 @@ function processWidgetItemForReplacement(widgetModel, key) {
|
|||
// If we have a string, and it needs to be replaced.
|
||||
if (typeof widgetModel[key] === 'string') {
|
||||
if (widgetModel[key].includes('{if:')) {
|
||||
widgetModel[key] = processIfStatements(widgetModel[key],
|
||||
widgetModel[key] = processIfStatements(
|
||||
widgetModel[key],
|
||||
dynamicStrings.GLOBAL_STATE,
|
||||
getStoreValueFromString);
|
||||
getStoreValueFromString
|
||||
);
|
||||
}
|
||||
|
||||
if (widgetModel[key].includes(dynamicStrings.MODAL_LINK)) {
|
||||
|
|
@ -158,14 +160,16 @@ export function fetchCmsContentForPage(issPage) {
|
|||
// Else get the client override page.
|
||||
const pageName = `${issPage}_${clientName.toLowerCase().replace(/ /g, '')}`;
|
||||
|
||||
return store.getPageData(pageName).then((clientResponse) =>
|
||||
// Process the client override if it exists.
|
||||
processPageData(baseResponse, clientResponse),
|
||||
(error) => {
|
||||
console.error(error);
|
||||
// Process the just the base if no client override exists.
|
||||
return processPageData(baseResponse, null);
|
||||
});
|
||||
return store.getPageData(pageName).then(
|
||||
(clientResponse) =>
|
||||
// Process the client override if it exists.
|
||||
processPageData(baseResponse, clientResponse),
|
||||
(error) => {
|
||||
console.error(error);
|
||||
// Process the just the base if no client override exists.
|
||||
return processPageData(baseResponse, null);
|
||||
}
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
@ -179,8 +183,10 @@ function mapStringToModal(str) {
|
|||
let linkToReplace = str.substring(startIndex, str.length);
|
||||
linkToReplace = linkToReplace.substring(0, linkToReplace.indexOf('}') + 1);
|
||||
|
||||
const params = linkToReplace.substring(dynamicStrings.MODAL_LINK.length + 2,
|
||||
linkToReplace.length - 1);
|
||||
const params = linkToReplace.substring(
|
||||
dynamicStrings.MODAL_LINK.length + 2,
|
||||
linkToReplace.length - 1
|
||||
);
|
||||
const splitParams = params.split(',');
|
||||
|
||||
const bodyText = `<a href="#" modalTarget="${splitParams[0]}" class="modal-text" aria-label="Modal window">${splitParams[1]}</a>`;
|
||||
|
|
@ -202,8 +208,10 @@ function mapStringToLink(str) {
|
|||
let linkToReplace = str.substring(startIndex, str.length);
|
||||
linkToReplace = linkToReplace.substring(0, linkToReplace.indexOf('}') + 1);
|
||||
|
||||
const params = linkToReplace.substring(dynamicStrings.EXTERNAL_LINK.length + 2,
|
||||
linkToReplace.length - 1);
|
||||
const params = linkToReplace.substring(
|
||||
dynamicStrings.EXTERNAL_LINK.length + 2,
|
||||
linkToReplace.length - 1
|
||||
);
|
||||
const splitParams = params.split(',');
|
||||
|
||||
const bodyText = `<a href="${splitParams[0]}" class="external-text" target="_blank">${splitParams[1]}</a>`;
|
||||
|
|
@ -291,13 +299,17 @@ export function processIfStatements(str, ifConditionKeyword, replacePlaceholderC
|
|||
}
|
||||
const ifStatementRegexExpression = getIfStatementRegexExpression();
|
||||
const ifStatementRegexMatches = [...str.matchAll(ifStatementRegexExpression)];
|
||||
const completeIfStatementArray = getAndFlagFirstNonNestedIfStatementWithKeyword(ifStatementRegexMatches,
|
||||
ifConditionKeyword);
|
||||
const completeIfStatementArray = getAndFlagFirstNonNestedIfStatementWithKeyword(
|
||||
ifStatementRegexMatches,
|
||||
ifConditionKeyword
|
||||
);
|
||||
executeIfStatementAndSetProcessedStrings(completeIfStatementArray, replacePlaceholderCallback);
|
||||
const reconstructedPostProcessedString = joinProcessedRegexArray(ifStatementRegexMatches);
|
||||
return processIfStatements(reconstructedPostProcessedString,
|
||||
return processIfStatements(
|
||||
reconstructedPostProcessedString,
|
||||
ifConditionKeyword,
|
||||
replacePlaceholderCallback);
|
||||
replacePlaceholderCallback
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -419,26 +431,28 @@ function getIfStatementRegexExpression() {
|
|||
// {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
|
||||
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:
|
||||
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}
|
||||
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}
|
||||
const matchEndOperator =
|
||||
'(?<isEndStatement>{end})' // Match & Capture {end}
|
||||
+ '(?<endTrailingString>.*?)' // Match & Capture all characters (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');
|
||||
return new RegExp(
|
||||
`${matchStartOfString}|${matchIfOperator}|${matchElseOperator}|${matchEndOperator}`,
|
||||
'g'
|
||||
);
|
||||
}
|
||||
|
||||
/// ///////////////////////////////////////
|
||||
|
|
@ -501,6 +515,16 @@ export function splitCopyOnCMSPlaceHolder(copy) {
|
|||
return copy.split(/{(.*?)}/g);
|
||||
}
|
||||
|
||||
export function getExternalLink(copy) {
|
||||
const url = copy.split(':')[1].split(',')[0];
|
||||
if (url.includes('https-')) {
|
||||
const prefixAdded = url.replace('https-', 'https://');
|
||||
return prefixAdded;
|
||||
}
|
||||
|
||||
return '#!';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns string2 of input following this pattern: {string1:string2,string3}
|
||||
* @param copy
|
||||
|
|
|
|||
80
src/helpers/object-helper.js
Normal file
80
src/helpers/object-helper.js
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
// For nested objects, spread operator only creates new references to the top level fields,
|
||||
// the remaining nested fields actually reference the original object which can introduce problems.
|
||||
|
||||
// The purpose of this method is to deep clone the data in an object recursively, this is useful
|
||||
// for cloning modelValues to internal models when regular two-way binding is not an option.
|
||||
// See: mobile-location-modal-questions.vue
|
||||
|
||||
// Creates a deep clone of an object. Clones primitives, arrays and objects, excluding class instances.
|
||||
// https://www.30secondsofcode.org/js/s/deep-clone
|
||||
export function deepClone(object) {
|
||||
if (object === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const clone = { ...object };
|
||||
// eslint-disable-next-line no-return-assign
|
||||
Object.keys(clone).forEach((key) =>
|
||||
(clone[key] = typeof object[key] === 'object' ? deepClone(object[key]) : object[key]));
|
||||
|
||||
if (Array.isArray(object)) {
|
||||
clone.length = object.length;
|
||||
return Array.from(clone);
|
||||
}
|
||||
|
||||
return clone;
|
||||
}
|
||||
|
||||
// The purpose of this method is to check for array or object equality recursively to determine if two complex objects are equal.
|
||||
// This is only a comparison of data, not functions.
|
||||
export function deepEqual(obj1, obj2) {
|
||||
if (typeof obj1 !== typeof obj2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (obj1 === null || obj2 === null) {
|
||||
return obj1 === obj2;
|
||||
}
|
||||
|
||||
if (Array.isArray(obj1) && Array.isArray(obj2)) {
|
||||
if (obj1.length !== obj2.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const sorted1 = obj1.slice().sort();
|
||||
const sorted2 = obj2.slice().sort();
|
||||
|
||||
for (let i = 0; i < sorted1.length; i++) {
|
||||
if (!deepEqual(sorted1[i], sorted2[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (typeof obj1 === 'object' && typeof obj2 === 'object') {
|
||||
const keys1 = Object.keys(obj1);
|
||||
const keys2 = Object.keys(obj2);
|
||||
|
||||
if (keys1.length !== keys2.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const sortedKeys1 = keys1.sort();
|
||||
const sortedKeys2 = keys2.sort();
|
||||
|
||||
for (let i = 0; i < sortedKeys1.length; i++) {
|
||||
const key1 = sortedKeys1[i];
|
||||
const key2 = sortedKeys2[i];
|
||||
|
||||
if (key1 !== key2 || !deepEqual(obj1[key1], obj2[key2])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return obj1 === obj2;
|
||||
}
|
||||
|
|
@ -1,16 +1,5 @@
|
|||
import { useMainStore } from '@/store';
|
||||
|
||||
export async function getServiceabilityDetails(serviceZipCode, lineItems) {
|
||||
const serviceabilityDetails = await useMainStore().getServiceabilityDetails(
|
||||
{
|
||||
serviceZipCode,
|
||||
lineItems
|
||||
},
|
||||
false
|
||||
);
|
||||
return Promise.resolve(serviceabilityDetails);
|
||||
}
|
||||
|
||||
export async function getZipCodeData(zipCode) {
|
||||
const serviceZipValidationResponse = await useMainStore().validateZip({ zip: zipCode });
|
||||
|
||||
|
|
@ -22,3 +11,57 @@ export async function getZipCodeData(zipCode) {
|
|||
zipCodeCtu: serviceZipValidationResponse.data.zipCodeCtu
|
||||
};
|
||||
}
|
||||
|
||||
export async function getPricedMobileFeePart(serviceZipCode) {
|
||||
if (!serviceZipCode) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
const zipCodeData = await getZipCodeData(serviceZipCode);
|
||||
|
||||
// Get the Mobile Fee Part
|
||||
const mobileFeePart = await useMainStore().getMobileFeePart();
|
||||
|
||||
// Get the Mobile Fee Part Price
|
||||
const pricingResults = await useMainStore()
|
||||
.priceOrderItemsAndSaveServerData([mobileFeePart.data], serviceZipCode, zipCodeData.zipCodeCtu);
|
||||
|
||||
return Promise.resolve(pricingResults[0]);
|
||||
}
|
||||
|
||||
export async function getServiceabilityDetails(serviceZipCode, lineItems) {
|
||||
const serviceabilityDetails = await useMainStore().getServiceabilityDetails(
|
||||
{
|
||||
serviceZipCode,
|
||||
lineItems
|
||||
},
|
||||
false
|
||||
);
|
||||
return Promise.resolve(serviceabilityDetails);
|
||||
}
|
||||
|
||||
export async function getAvailabilityRating(
|
||||
startDate,
|
||||
endDate,
|
||||
shopAppointmentType,
|
||||
providerNumber
|
||||
) {
|
||||
// For a given shop provider number and date range, get the appointment time slots available
|
||||
const shopTimeSlots = await useMainStore().getShopTimeSlots(
|
||||
{
|
||||
providerNumber,
|
||||
startDate,
|
||||
endDate,
|
||||
shopAppointmentType
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
const numberOfDaysToEvaluate = 2;
|
||||
const isGoodAvailability =
|
||||
shopTimeSlots.data.days.filter((x) => x.timeSlots.length > 0).length
|
||||
>= numberOfDaysToEvaluate;
|
||||
|
||||
const shopStatus = isGoodAvailability ? 'high' : 'low';
|
||||
|
||||
return Promise.resolve(shopStatus);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<div>
|
||||
<footer
|
||||
id="infoBox"
|
||||
class="footer container-fluid g-5 px-0">
|
||||
class="footer container-fluid g-5 my-5 px-0">
|
||||
<div class="row d-flex flex-row-reverse align-items-center vw-100 mx-0">
|
||||
<div
|
||||
id="stacked"
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import settleAllPromises from '@/helpers/layout-helper';
|
|||
import { useMainStore } from '@/store';
|
||||
|
||||
export default {
|
||||
name: 'bailout-confirmation',
|
||||
name: 'contact-confirmation',
|
||||
components: {
|
||||
siteHeader,
|
||||
siteSubHeader
|
||||
|
|
@ -47,9 +47,9 @@ export default {
|
|||
return { mainStore };
|
||||
},
|
||||
data() {
|
||||
const bailoutConfirmationData = useMainStore().pageData(issPageValues.BAILOUT_CONFIRMATION);
|
||||
const contactConfirmationData = useMainStore().pageData(issPageValues.CONTACT_CONFIRMATION);
|
||||
return {
|
||||
bailoutConfirmationModel: bailoutConfirmationData
|
||||
contactConfirmationModel: contactConfirmationData
|
||||
};
|
||||
},
|
||||
computed: {},
|
||||
|
|
|
|||
|
|
@ -139,6 +139,8 @@ export default {
|
|||
async beforeRouteEnter(to, from, next) {
|
||||
// Call APIs
|
||||
const cmsContentPromise = fetchCmsContentForPage(to.query.issPage);
|
||||
const wipersPromise = useMainStore().getWipers();
|
||||
const rainDefensePromise = useMainStore().getRainDefense();
|
||||
const supportingItemsPromise = await useMainStore().getSupportingItems();
|
||||
|
||||
// Settle promises and get results
|
||||
|
|
@ -147,6 +149,14 @@ export default {
|
|||
resultKey: 'cmsContent',
|
||||
promise: cmsContentPromise
|
||||
},
|
||||
{
|
||||
resultKey: 'wipers',
|
||||
promise: wipersPromise
|
||||
},
|
||||
{
|
||||
resultKey: 'rainDefense',
|
||||
promise: rainDefensePromise
|
||||
},
|
||||
{
|
||||
resultKey: 'supportingItems',
|
||||
promise: supportingItemsPromise
|
||||
|
|
@ -158,7 +168,9 @@ export default {
|
|||
? JSON.parse(JSON.stringify(useMainStore().order.lineItems.glassParts))
|
||||
: [];
|
||||
const availableLineItems = [
|
||||
resultMap.rainDefense,
|
||||
...resultMap.supportingItems,
|
||||
...resultMap.wipers,
|
||||
...clonedGlassParts
|
||||
];
|
||||
|
||||
|
|
@ -167,6 +179,7 @@ export default {
|
|||
// Call the "next" function to complete the transition to this page.
|
||||
next((vm) => {
|
||||
vm.setCmsContent(resultMap.cmsContent);
|
||||
vm.setSupportingItems(resultMap.supportingItems);
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
vm.availableLineItems = pricingResults;
|
||||
});
|
||||
|
|
@ -188,7 +201,8 @@ export default {
|
|||
],
|
||||
rules: {
|
||||
selectionRequired: globalRules.OPTION_REQUIRED
|
||||
}
|
||||
},
|
||||
supportingItems: null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -341,6 +355,7 @@ export default {
|
|||
},
|
||||
async navigateForward() {
|
||||
if (this.unverified || this.verifiedDeductible) {
|
||||
this.mainStore.saveSupportingItems(this.supportingItems);
|
||||
this.$router.navigate(
|
||||
navigationScenarios.CLICKED_FORWARD,
|
||||
this.$route,
|
||||
|
|
@ -349,6 +364,7 @@ export default {
|
|||
);
|
||||
} else if (this.verifiedITAC || this.verifiedNoComp) {
|
||||
if (this.selectedProvider === 'Safelite') {
|
||||
this.mainStore.saveSupportingItems(this.supportingItems);
|
||||
this.$router.navigate(
|
||||
navigationScenarios.CLICKED_FORWARD_WITH_SAFELITE,
|
||||
this.$route,
|
||||
|
|
@ -437,6 +453,9 @@ export default {
|
|||
},
|
||||
getITACCostSavings(vehicleDeductible, totalServicePrice) {
|
||||
return vehicleDeductible - totalServicePrice;
|
||||
},
|
||||
setSupportingItems(newSupportingItems) {
|
||||
this.supportingItems = newSupportingItems;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,108 @@
|
|||
<template>
|
||||
<transition
|
||||
name="fade"
|
||||
mode="out-in">
|
||||
<div
|
||||
v-if="isDisplayed"
|
||||
class="appointment-type-question"
|
||||
aria-live="polite">
|
||||
<buttonQuestion
|
||||
ref="buttonQuestion"
|
||||
v-model="selectedValues"
|
||||
customButtonQuestionId="appointmentTypeQuestion"
|
||||
:questionText="questionText"
|
||||
:answers="answersToDisplay"
|
||||
:groupName="groupName"
|
||||
buttonTypeString="listCard"
|
||||
:suppressError="suppressError"
|
||||
:validationRules="validationRules"
|
||||
isRequired />
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useMainStore } from '@/store';
|
||||
import { AppointmentTypeStrings } from '@/constants/schedule-constants.js';
|
||||
import buttonQuestion from '@/digital-components/button-question/button-question.vue';
|
||||
|
||||
export default {
|
||||
name: 'appointment-type-question',
|
||||
components: {
|
||||
buttonQuestion
|
||||
},
|
||||
props: {
|
||||
modelValue: String,
|
||||
groupName: String,
|
||||
isAvailable: Boolean,
|
||||
isDisplayed: Boolean,
|
||||
suppressError: Boolean,
|
||||
validationRules: String,
|
||||
cmsWidgetName: String,
|
||||
isServiceableMobile: Boolean,
|
||||
isServiceableInshop: Boolean
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
computed: {
|
||||
questionText() {
|
||||
return this.getCmsContent(this.cmsWidgetName, 'QuestionText');
|
||||
},
|
||||
answersFromCms() {
|
||||
return this.getCmsContent(this.cmsWidgetName, 'Answers');
|
||||
},
|
||||
answersToDisplay() {
|
||||
const shouldShowMobile = this.isServiceableMobile;
|
||||
const shouldShowInshop = this.isServiceableInshop;
|
||||
const shouldShowDropoff = this.isServiceableInshop && !useMainStore().damage.isRepair;
|
||||
return this.answersFromCms
|
||||
? this.answersFromCms.filter((answer) => (
|
||||
(answer.Name === AppointmentTypeStrings.IN_SHOP && shouldShowInshop)
|
||||
|| (answer.Name === AppointmentTypeStrings.MOBILE && shouldShowMobile)
|
||||
|| (answer.Name === AppointmentTypeStrings.DROP_OFF && shouldShowDropoff)
|
||||
))
|
||||
: [];
|
||||
},
|
||||
selectedValues: {
|
||||
get() {
|
||||
return this.modelValue;
|
||||
},
|
||||
set(newValue) {
|
||||
this.$emit('update:modelValue', newValue);
|
||||
}
|
||||
},
|
||||
isMobileOnly() {
|
||||
return this.isServiceableMobile && !this.isServiceableInshop;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
answersToDisplay: {
|
||||
handler(newValue) {
|
||||
// If there is only one option to display and that option is 'Mobile' then select it
|
||||
if (
|
||||
newValue.length === 1
|
||||
&& newValue.findIndex((answer) => answer.Name === 'Mobile') !== -1
|
||||
) {
|
||||
this.selectedValues = 'Mobile';
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
},
|
||||
isMobileOnly: {
|
||||
handler(newValue) {
|
||||
if (newValue) {
|
||||
this.selectedValues = 'Mobile';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.button-question) {
|
||||
.list-card img {
|
||||
height: auto;
|
||||
width: 3.417rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,322 @@
|
|||
<template>
|
||||
<transition
|
||||
name="fade"
|
||||
mode="out-in">
|
||||
<div class="mobile-location-questions">
|
||||
<div
|
||||
:id="componentId"
|
||||
class="text-center">
|
||||
<label
|
||||
for="mobileLocationLinkPromptId"
|
||||
:aria-label="mobileLocationLinkPromptText"
|
||||
class="form-label fw-bold w-100 ps-4 pe-4 pt-4 text-black"
|
||||
v-html="mobileLocationLinkPromptText"></label>
|
||||
<div class="update-mobile-location-text-link">
|
||||
<textLink
|
||||
id="mobileLocationLinkPromptId"
|
||||
ref="mobileLocationLink"
|
||||
linkType="text"
|
||||
:text="mobileLocationLinkText"
|
||||
href="#!"
|
||||
@click-event="openModal" />
|
||||
</div>
|
||||
<div
|
||||
v-show="errorMessage"
|
||||
class="row my-1 form-test-error">
|
||||
<span
|
||||
class="d-inline-flex small mt-0 center-error-message"
|
||||
aria-atomic="true"
|
||||
aria-live="polite">
|
||||
{{ errorMessage }}
|
||||
</span>
|
||||
</div>
|
||||
<textBlock
|
||||
:customText="mobileFeeText"
|
||||
cmsWidgetName="MobileFeeDisclaimerWidget"
|
||||
typeStyle="caption" />
|
||||
</div>
|
||||
<modal
|
||||
:ref="modalName"
|
||||
:headerText="modalHeaderText"
|
||||
:footerButtonText="modalFooterText"
|
||||
:onModalOpenedCallback="onModalOpened"
|
||||
:onModalClosedCallback="onModalClosed"
|
||||
@isModalOpened="setModalStatus"
|
||||
@footer-button-event="setMobileLocation">
|
||||
<template v-if="isModalOpened">
|
||||
<addressQuestions
|
||||
ref="addressQuestions"
|
||||
v-model="internalModel.addressQuestions"
|
||||
captureApartmentNumberOrBusinessName="true"
|
||||
preserveCityAndStateOnReset="true" />
|
||||
<vehicleProtectedQuestion
|
||||
ref="vehicleProtectedQuestion"
|
||||
v-model="internalModel.isVehicleProtected"
|
||||
cmsWidgetName="VehicleProtectedQuestionWidget" />
|
||||
<textBlock
|
||||
cmsWidgetName="WorkspaceRequirementsWidget"
|
||||
typeStyle="caption" />
|
||||
<alert
|
||||
v-if="displayInvalidZipAlert"
|
||||
ref="alertInvalidZip"
|
||||
class="my-4"
|
||||
cmsWidgetName="AlertInvalidZipWidget"
|
||||
alertClass="alert-danger"
|
||||
:isDismissible="false" />
|
||||
</template>
|
||||
</modal>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// Components
|
||||
import textLink from '@/ux-components/text-link/text-link.vue';
|
||||
import textBlock from '@/digital-components/text-block/text-block.vue';
|
||||
import modal from '@/digital-components/modal/modal.vue';
|
||||
import alert from '@/ux-components/alert/alert.vue';
|
||||
import { useField } from 'vee-validate';
|
||||
import addressQuestions from '@/iss-components/address-questions/address-questions.vue';
|
||||
// eslint-disable-next-line max-len
|
||||
import vehicleProtectedQuestion from '@/layouts/service-location/mobile-location-modal-questions/vehicle-protected-question/vehicle-protected-question.vue';
|
||||
|
||||
// Helpers
|
||||
import {
|
||||
getPricedMobileFeePart,
|
||||
getServiceabilityDetails
|
||||
} from '@/helpers/service-location-helper';
|
||||
import { deepClone } from '@/helpers/object-helper.js';
|
||||
|
||||
// Validation
|
||||
|
||||
export default {
|
||||
name: 'mobile-location-modal-questions',
|
||||
components: {
|
||||
textLink,
|
||||
modal,
|
||||
addressQuestions,
|
||||
vehicleProtectedQuestion,
|
||||
textBlock,
|
||||
alert
|
||||
},
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
addressQuestions: {
|
||||
streetAddress: '',
|
||||
apartmentNumberOrBusinessName: '',
|
||||
city: '',
|
||||
state: '',
|
||||
zipCode: ''
|
||||
},
|
||||
isVehicleProtected: null
|
||||
})
|
||||
},
|
||||
mobileFeePart: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
linkWidgetName: String,
|
||||
modalWidgetName: String,
|
||||
alertNonServiceableZipWidgetName: String,
|
||||
alertInvalidZipWidgetName: String,
|
||||
customComponentId: String,
|
||||
validationRules: String,
|
||||
onZipUpdateCallback: {
|
||||
type: Function
|
||||
}
|
||||
},
|
||||
emits: ['update:modelValue', 'updated-mobile-fee-part', 'updated-contains-military-base'],
|
||||
setup(props) {
|
||||
const uuid = crypto.randomUUID();
|
||||
const componentId = !props.customComponentId
|
||||
? `component-${uuid}`
|
||||
: props.customComponentId;
|
||||
|
||||
// Integrate this component as a single field with an object for it's value into the page level validation
|
||||
const { modelValue } = deepClone(props);
|
||||
const initialValue = modelValue;
|
||||
|
||||
const fieldOptions = {
|
||||
value: modelValue,
|
||||
initialValue
|
||||
};
|
||||
|
||||
const { errorMessage, handleChange, meta, validate, errors } = useField(
|
||||
componentId,
|
||||
props.validationRules,
|
||||
fieldOptions
|
||||
);
|
||||
|
||||
return {
|
||||
componentId,
|
||||
errorMessage,
|
||||
handleChange,
|
||||
validate,
|
||||
meta,
|
||||
errors
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
internalModel: deepClone(this.modelValue),
|
||||
displayInvalidZipAlert: false,
|
||||
isModalOpened: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
mobileLocationLinkPromptText() {
|
||||
return this.getCmsContent(this.linkWidgetName, 'HeaderText');
|
||||
},
|
||||
mobileLocationLinkText() {
|
||||
if (
|
||||
this.addressModel.streetAddress
|
||||
&& this.addressModel.streetAddress !== ''
|
||||
&& this.addressModel.city
|
||||
&& this.addressModel.city !== ''
|
||||
&& this.addressModel.state
|
||||
&& this.addressModel.state !== ''
|
||||
&& this.addressModel.zipCode
|
||||
&& this.addressModel.zipCode !== ''
|
||||
&& this.internalModel.isVehicleProtected !== null
|
||||
) {
|
||||
// eslint-disable-next-line max-len
|
||||
return `${this.addressModel.streetAddress}\n${this.addressModel.city}, ${this.addressModel.state} ${this.addressModel.zipCode}`;
|
||||
}
|
||||
return this.getCmsContent(this.linkWidgetName, 'BodyText');
|
||||
},
|
||||
modalName() {
|
||||
return 'MobileLocationModalWidget';
|
||||
},
|
||||
modalHeaderText() {
|
||||
return this.getCmsContent(this.modalWidgetName, 'HeaderText');
|
||||
},
|
||||
mobileFeeText() {
|
||||
const cmsContentText = this.getCmsContent('MobileFeeDisclaimerWidget', 'Text');
|
||||
return cmsContentText.replaceAll('{custom:mobileFee}', this.mobileFee);
|
||||
},
|
||||
mobileFee() {
|
||||
if (!this.mobileFeePart) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (
|
||||
this.mobileFeePart.laborAmount
|
||||
+ this.mobileFeePart.sellingPrice
|
||||
+ this.mobileFeePart.kitPrice
|
||||
);
|
||||
},
|
||||
modalFooterText() {
|
||||
return this.getCmsContent(this.modalWidgetName, 'FooterText');
|
||||
},
|
||||
modal() {
|
||||
return this.$refs[this.modalName];
|
||||
},
|
||||
addressModel: {
|
||||
get() {
|
||||
return this.modelValue.addressQuestions;
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
modelValue: {
|
||||
handler(newValue) {
|
||||
this.internalModel = deepClone(newValue);
|
||||
this.handleChange(newValue);
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openModal() {
|
||||
this.modal.openModal();
|
||||
},
|
||||
onModalOpened() {
|
||||
this.internalModel = deepClone(this.modelValue);
|
||||
},
|
||||
setModalStatus(isOpened) {
|
||||
this.isModalOpened = isOpened;
|
||||
},
|
||||
closeModal() {
|
||||
this.modal.closeModal();
|
||||
},
|
||||
onModalClosed() {
|
||||
this.displayInvalidZipAlert = false;
|
||||
this.internalModel = deepClone(this.modelValue);
|
||||
},
|
||||
resetModalButtonStyle() {
|
||||
this.modal.resetButtonStyle();
|
||||
},
|
||||
async setMobileLocation() {
|
||||
if (
|
||||
this.internalModel.addressQuestions.zipCode
|
||||
!== this.modelValue.addressQuestions.zipCode
|
||||
) {
|
||||
// Validate the Zip Code
|
||||
const zipCodeData = await this.getZipCodeData(this.internalModel.addressQuestions.zipCode);
|
||||
|
||||
if (!zipCodeData.isValid) {
|
||||
this.displayInvalidZipAlert = true;
|
||||
this.resetModalButtonStyle();
|
||||
} else {
|
||||
// retrieve mobile fee part
|
||||
const serviceZipCode = this.internalModel.addressQuestions.zipCode;
|
||||
const mobileFeePart = await getPricedMobileFeePart(serviceZipCode);
|
||||
|
||||
// retrieve serviceability details
|
||||
const serviceabilityDetails = await getServiceabilityDetails(serviceZipCode);
|
||||
|
||||
// update content related to service zip code
|
||||
this.$emit('updated-mobile-fee-part', mobileFeePart);
|
||||
this.$emit('updated-serviceability', serviceabilityDetails.data);
|
||||
this.$emit('updated-contains-military-base', zipCodeData.containsMilitaryBase);
|
||||
|
||||
if (this.onZipUpdateCallback) {
|
||||
await this.onZipUpdateCallback(serviceZipCode);
|
||||
}
|
||||
// Update the page level model
|
||||
this.$emit('update:modelValue', this.internalModel);
|
||||
|
||||
this.closeModal();
|
||||
}
|
||||
} else {
|
||||
// Update the page level model
|
||||
this.$emit('update:modelValue', this.internalModel);
|
||||
|
||||
this.closeModal();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.mobile-location-questions {
|
||||
.center-error-message {
|
||||
justify-content: center !important;
|
||||
}
|
||||
}
|
||||
|
||||
.update-zip-text-link::before {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
width: 13px;
|
||||
height: 16px;
|
||||
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 13 16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M6.49635 1.00142e-07C5.64734 -0.000153295 4.80722 0.175918 4.0274 0.517444C3.24757 0.858969 2.54443 1.35877 1.96099 1.98626C0.765713 3.27588 0.0999756 4.98141 0.0999756 6.75394C0.0999756 8.52646 0.765713 10.232 1.96099 11.5216L5.98324 15.777C6.04954 15.8475 6.12918 15.9036 6.21736 15.9419C6.30555 15.9802 6.40045 16 6.49635 16C6.59225 16 6.68716 15.9802 6.77534 15.9419C6.86353 15.9036 6.94317 15.8475 7.00946 15.777L11.0317 11.52C12.2391 10.2383 12.909 8.52914 12.8999 6.75394C12.9094 4.97818 12.2394 3.26832 11.0317 1.98626C10.4481 1.35899 9.74493 0.859347 8.96514 0.517839C8.18535 0.176331 7.34532 0.000130509 6.49635 1.00142e-07V1.00142e-07ZM6.49635 9.13131C6.02507 9.13131 5.56437 8.98913 5.17251 8.72275C4.78065 8.45637 4.47524 8.07776 4.29488 7.63479C4.11453 7.19181 4.06734 6.70438 4.15928 6.23412C4.25123 5.76387 4.47817 5.33191 4.81142 4.99287C5.14467 4.65384 5.56925 4.42295 6.03148 4.32941C6.49371 4.23587 6.97282 4.28388 7.40823 4.46736C7.84364 4.65085 8.21579 4.96157 8.47762 5.36023C8.73945 5.7589 8.87921 6.2276 8.87921 6.70707C8.87921 7.34974 8.62837 7.96611 8.18185 8.4207C7.73532 8.87528 7.12964 9.13088 6.49794 9.13131H6.49635Z' fill='%231574A1'/%3E%3C/svg%3E%0A");
|
||||
background-size: contain;
|
||||
vertical-align: middle;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
.address-questions {
|
||||
margin-top: 0em;
|
||||
}
|
||||
|
||||
#mobileLocationLinkPromptId {
|
||||
white-space: pre-line;
|
||||
}
|
||||
.text-black {
|
||||
color: $black;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
<template>
|
||||
<buttonQuestion
|
||||
v-model="selectedValue"
|
||||
class="radioQuestion"
|
||||
:questionText="questionText"
|
||||
:answers="answers"
|
||||
groupName="isVehicleProtected"
|
||||
textPosition="text-center"
|
||||
validationRules="option-required"
|
||||
isRequired />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import buttonQuestion from '@/digital-components/button-question/button-question.vue';
|
||||
|
||||
// Supporting files
|
||||
import { defineRule } from 'vee-validate';
|
||||
import { required } from '@/helpers/validation-rules';
|
||||
import errorMessages from '@/constants/error-messages';
|
||||
|
||||
defineRule('option-required', required(errorMessages.OPTION_REQUIRED));
|
||||
|
||||
export default {
|
||||
name: 'vehicle-protected-question',
|
||||
components: {
|
||||
buttonQuestion
|
||||
},
|
||||
props: {
|
||||
// eslint-disable-next-line vue/require-prop-types
|
||||
modelValue: {
|
||||
isVehicleProtected: Boolean
|
||||
},
|
||||
cmsWidgetName: String
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
computed: {
|
||||
questionText() {
|
||||
return this.getCmsContent(this.cmsWidgetName, 'QuestionText');
|
||||
},
|
||||
selectedValue: {
|
||||
get() {
|
||||
return this.modelValue;
|
||||
},
|
||||
set(newValue) {
|
||||
this.$emit('update:modelValue', newValue);
|
||||
}
|
||||
},
|
||||
answers() {
|
||||
return this.getCmsContent(this.cmsWidgetName, 'Answers');
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.modal-dialog {
|
||||
.question-text {
|
||||
& > span {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -5,52 +5,88 @@
|
|||
@submit="onSubmit"
|
||||
@invalidSubmit="onInvalidSubmit">
|
||||
<div class="page-container-grouped-styles">
|
||||
<div class="fade-on-route-transition position-relative">
|
||||
<siteHeader cmsWidgetName="SiteHeaderWidget" />
|
||||
<siteSubHeader
|
||||
id="sub-header"
|
||||
cmsWidgetName="SiteSubHeaderWidget"
|
||||
class="mt-5" />
|
||||
<div class="select-car">
|
||||
<div class="container-fluid pb-2">
|
||||
<div class="row px-3">
|
||||
<div class="col">
|
||||
<div class="select-car-form rounded">
|
||||
<serviceZipModalQuestion
|
||||
ref="serviceZipCodeQuestion"
|
||||
v-model="serviceZipCodeQuestion"
|
||||
modalWidgetName="ServiceZipModalWidget"
|
||||
@updatedServiceability="setServiceabilityDetails"
|
||||
@updatedContainsMilitaryBase="setContainsMilitaryBase" />
|
||||
<alert
|
||||
v-if="displayMilitaryZipAlert"
|
||||
ref="alertMilitaryBaseZip"
|
||||
class="my-5"
|
||||
cmsWidgetName="AlertMilitaryBaseZipWidget"
|
||||
alertClass="alert-warning" />
|
||||
<buttonQuestion
|
||||
v-model="selectedAppointmentType"
|
||||
cmsWidgetName="ServiceTypeQuestionWidget"
|
||||
:questionText="questionText"
|
||||
:answers="answersFromCms"
|
||||
buttonTypeString="listCard"
|
||||
isRequired
|
||||
validationRules="selection-required"
|
||||
class="service-location-button-question mb-1" />
|
||||
<div class="select-car">
|
||||
<siteFooter
|
||||
ref="siteFooter"
|
||||
class="mt-5"
|
||||
cmsWidgetName="SiteFooterWidget"
|
||||
:isForwardActionDisabled="!meta.valid"
|
||||
@backClicked="backButtonAction"
|
||||
@forwardClicked="forwardButtonAction" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<siteHeader cmsWidgetName="SiteHeaderWidget" />
|
||||
<siteSubHeader
|
||||
cmsWidgetName="SiteSubHeaderWidget"
|
||||
class="mt-5" />
|
||||
<div class="main-content-container">
|
||||
<serviceZipModalQuestion
|
||||
ref="serviceZipCodeQuestion"
|
||||
v-model="serviceZipCodeQuestion"
|
||||
modalWidgetName="ServiceZipModalWidget"
|
||||
@updatedServiceability="setServiceabilityDetails"
|
||||
@updatedContainsMilitaryBase="setContainsMilitaryBase" />
|
||||
<alert
|
||||
v-if="displayMilitaryZipAlert"
|
||||
ref="alertMilitaryBaseZip"
|
||||
class="my-5"
|
||||
cmsWidgetName="AlertMilitaryBaseZipWidget"
|
||||
alertClass="alert-warning" />
|
||||
<alert
|
||||
v-if="displayServiceableMobileOnly"
|
||||
ref="alertMobileOnly"
|
||||
class="my-5"
|
||||
cmsWidgetName="AlertMobileOnlyWidget"
|
||||
alertClass="alert-warning" />
|
||||
<alert
|
||||
v-if="displayRecalibrationWarning"
|
||||
ref="alertRecalNoMobile"
|
||||
class="my-5"
|
||||
cmsWidgetName="AlertRecalNoMobileWidget"
|
||||
alertClass="alert-warning"
|
||||
@text-link-clicked="openModalAction" />
|
||||
<alert
|
||||
v-if="displayServiceableInshopOnly"
|
||||
ref="alertInshopOnly"
|
||||
class="my-5"
|
||||
cmsWidgetName="AlertInshopOnlyWidget"
|
||||
alertClass="alert-warning" />
|
||||
<alert
|
||||
v-if="displayNoShopsAlert"
|
||||
ref="alertNoShops"
|
||||
class="my-5"
|
||||
cmsWidgetName="AlertNoShopsWidget"
|
||||
alertClass="alert-warning" />
|
||||
<appointmentTypeQuestion
|
||||
v-show="isAppointmentTypeDisplayed"
|
||||
ref="appointmentTypeQuestion"
|
||||
v-model="selectedAppointmentType"
|
||||
:isServiceableMobile="isServiceableMobile"
|
||||
:isServiceableInshop="isServiceableInshop"
|
||||
:isDisplayed="isAppointmentTypeDisplayed"
|
||||
groupName="appointmentTypeQuestion"
|
||||
cmsWidgetName="AppointmentTypeQuestionWidget"
|
||||
validationRules="option-required" />
|
||||
<mobileLocationModalQuestions
|
||||
v-if="selectedAppointmentType === 'Mobile'"
|
||||
ref="mobileLocationQuestions"
|
||||
v-model="mobileLocationQuestions"
|
||||
customComponentId="mobileLocationQuestions"
|
||||
:mobileFeePart="mobileFeePart"
|
||||
validationRules="mobile-location-required"
|
||||
linkWidgetName="MobileLocationLinkWidget"
|
||||
modalWidgetName="MobileLocationModalWidget"
|
||||
:onZipUpdateCallback="reloadShopData"
|
||||
@updated-mobile-fee-part="setMobileFeePart"
|
||||
@updated-serviceability="setServiceabilityDetails"
|
||||
@updated-contains-military-base="setContainsMilitaryBase" />
|
||||
<shopQuestion
|
||||
v-show="isShopQuestionDisplayed"
|
||||
ref="shopQuestion"
|
||||
v-model="selectedProvider"
|
||||
:selectedAppointmentType="selectedAppointmentType"
|
||||
:isDisplayed="isShopQuestionDisplayed"
|
||||
cmsWidgetName="ShopQuestionWidget" />
|
||||
<contentGroupModal
|
||||
ref="RecalModal"
|
||||
cmsWidgetName="RecalModal" />
|
||||
<siteFooter
|
||||
ref="siteFooter"
|
||||
class="mt-5"
|
||||
cmsWidgetName="SiteFooterWidget"
|
||||
:isForwardActionDisabled="!meta.valid"
|
||||
@backClicked="backButtonAction"
|
||||
@forwardClicked="forwardButtonAction" />
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
|
|
@ -61,32 +97,50 @@ import { fetchCmsContentForPage } 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.vue';
|
||||
import { useMainStore } from '@/store';
|
||||
import { getServiceabilityDetails, getZipCodeData } from '@/helpers/service-location-helper';
|
||||
import { getPricedMobileFeePart, getServiceabilityDetails, getZipCodeData } from '@/helpers/service-location-helper';
|
||||
|
||||
// Import Component
|
||||
import alert from '@/ux-components/alert/alert.vue';
|
||||
import appointmentTypeQuestion from '@/layouts/service-location/appointment-type-question/appointment-type-question.vue';
|
||||
import baseFormMixin from '@/mixins/base-form-mixin';
|
||||
import contentGroupModal from '@/iss-components/content-group-modal/content-group-modal.vue';
|
||||
import mobileLocationModalQuestions from '@/layouts/service-location/mobile-location-modal-questions/mobile-location-modal-questions.vue';
|
||||
import { Form, defineRule } from 'vee-validate';
|
||||
import shopQuestion from '@/layouts/service-location/shop-question/shop-question.vue';
|
||||
import siteFooter from '@/iss-components/site-footer/site-footer.vue';
|
||||
import siteHeader from '@/iss-components/site-header/site-header.vue';
|
||||
import siteSubHeader from '@/iss-components/site-sub-header/site-sub-header.vue';
|
||||
import serviceZipModalQuestion from '@/layouts/service-location/service-zip-modal-question/service-zip-modal-question.vue';
|
||||
|
||||
// DEFINE VALIDATION RULES
|
||||
defineRule('mobile-location-required', (value) => {
|
||||
if (
|
||||
value.addressQuestions.streetAddress === ''
|
||||
|| value.addressQuestions.city === ''
|
||||
|| value.addressQuestions.state === ''
|
||||
|| value.addressQuestions.zipCode === ''
|
||||
|| value.isVehicleProtected == null
|
||||
) {
|
||||
return errorMessages.MOBILE_LOCATION_REQUIRED;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
defineRule('selection-required', required(errorMessages.OPTION_REQUIRED));
|
||||
export default {
|
||||
name: 'service-location',
|
||||
components: {
|
||||
alert,
|
||||
appointmentTypeQuestion,
|
||||
contentGroupModal,
|
||||
mobileLocationModalQuestions,
|
||||
siteFooter,
|
||||
siteHeader,
|
||||
siteSubHeader,
|
||||
buttonQuestion,
|
||||
// eslint-disable-next-line vue/no-reserved-component-names
|
||||
Form,
|
||||
serviceZipModalQuestion,
|
||||
alert
|
||||
shopQuestion
|
||||
},
|
||||
mixins: [baseFormMixin],
|
||||
async beforeRouteEnter(to, from, next) {
|
||||
|
|
@ -96,7 +150,9 @@ export default {
|
|||
const serviceZipCode = useMainStore().order.customer.address.zipCode;
|
||||
const zipCodeData = getZipCodeData(serviceZipCode);
|
||||
|
||||
const mobileFeePartPromise = getPricedMobileFeePart(serviceZipCode);
|
||||
const serviceabilityDetailsPromise = getServiceabilityDetails(serviceZipCode);
|
||||
const shopQuestionInitialDataPromise = shopQuestion.methods.loadInitialData(serviceZipCode);
|
||||
|
||||
// Settle promises and get results
|
||||
const promiseResultMap = [
|
||||
|
|
@ -104,10 +160,18 @@ export default {
|
|||
resultKey: 'cmsContent',
|
||||
promise: cmsContentPromise
|
||||
},
|
||||
{
|
||||
resultKey: 'mobileFeePart',
|
||||
promise: mobileFeePartPromise
|
||||
},
|
||||
{
|
||||
resultKey: 'serviceabilityDetails',
|
||||
promise: serviceabilityDetailsPromise
|
||||
},
|
||||
{
|
||||
resultKey: 'shopQuestionInitialData',
|
||||
promise: shopQuestionInitialDataPromise
|
||||
},
|
||||
{
|
||||
resultKey: 'zipCodeData',
|
||||
promise: zipCodeData
|
||||
|
|
@ -115,10 +179,10 @@ export default {
|
|||
];
|
||||
|
||||
const resultMap = await settleAllPromises(promiseResultMap);
|
||||
|
||||
next((vm) => {
|
||||
vm.setCmsContent(resultMap.cmsContent);
|
||||
vm.setData(resultMap.zipCodeData, resultMap.serviceabilityDetails);
|
||||
vm.setData(resultMap.zipCodeData, resultMap.serviceabilityDetails, resultMap.mobileFeePart);
|
||||
vm.$refs.shopQuestion.initializeComponent(resultMap.shopQuestionInitialData);
|
||||
});
|
||||
},
|
||||
setup() {
|
||||
|
|
@ -127,13 +191,21 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
zipCode: this.mainStore.order.customer.address.zipCode,
|
||||
state: this.mainStore.order.customer.address.state,
|
||||
isServiceZipServiceable: null,
|
||||
streetAddress: this.getServiceAddressFromStore(),
|
||||
apartmentNumberOrBusinessName: this.getServiceAddress2FromStore(),
|
||||
city: this.getServiceCityFromStore(),
|
||||
state: this.getServiceStateFromStore(),
|
||||
zipCode: this.getServiceZipCodeFromStore(),
|
||||
isVehicleProtected: this.getIsVehicleProtectedFromStore(),
|
||||
isGlassServiceableInshop: null,
|
||||
isRecalibrationServiceableInshop: null,
|
||||
isGlassServiceableMobile: null,
|
||||
isRecalibrationServiceableMobile: null,
|
||||
selectedAppointmentType: '',
|
||||
zipContainsMilitaryBase: false
|
||||
selectedAppointmentType: this.getSelectedAppointmentType(),
|
||||
selectedProvider: this.getSelectedProvider(),
|
||||
mobileFeePart: null,
|
||||
zipContainsMilitaryBase: false,
|
||||
zipCodeCtu: null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -146,26 +218,102 @@ export default {
|
|||
serviceZipCodeQuestion: {
|
||||
get() {
|
||||
return {
|
||||
zipCode: this.zipCode,
|
||||
state: this.state
|
||||
state: this.state,
|
||||
zipCode: this.zipCode
|
||||
};
|
||||
},
|
||||
set(newValue) {
|
||||
this.zipCode = newValue.zipCode;
|
||||
this.state = newValue.state;
|
||||
if (newValue.zipCode !== this.zipCode) {
|
||||
this.resetMobileLocation();
|
||||
this.selectedAppointmentType = null;
|
||||
this.selectedProvider = null;
|
||||
}
|
||||
|
||||
// TODO: review - this seems like it should be awaited.
|
||||
this.state = newValue.state;
|
||||
this.zipCode = newValue.zipCode;
|
||||
|
||||
// eslint-disable-next-line vue/valid-next-tick
|
||||
this.$nextTick();
|
||||
}
|
||||
},
|
||||
mobileLocationQuestions: {
|
||||
get() {
|
||||
return {
|
||||
addressQuestions: {
|
||||
streetAddress: this.streetAddress,
|
||||
apartmentNumberOrBusinessName: this.apartmentNumberOrBusinessName,
|
||||
city: this.city,
|
||||
state: this.state,
|
||||
zipCode: this.zipCode
|
||||
},
|
||||
isVehicleProtected: this.isVehicleProtected
|
||||
};
|
||||
},
|
||||
set(newValue) {
|
||||
this.streetAddress = newValue.addressQuestions.streetAddress;
|
||||
this.apartmentNumberOrBusinessName =
|
||||
newValue.addressQuestions.apartmentNumberOrBusinessName;
|
||||
this.city = newValue.addressQuestions.city;
|
||||
this.state = newValue.addressQuestions.state;
|
||||
this.zipCode = newValue.addressQuestions.zipCode;
|
||||
this.isVehicleProtected = newValue.isVehicleProtected;
|
||||
|
||||
if (newValue.zipCode !== this.zipCode) {
|
||||
if (!this.selectedAppointmentType === 'Mobile') {
|
||||
this.selectedAppointmentType = null;
|
||||
}
|
||||
this.selectedProvider = null;
|
||||
}
|
||||
}
|
||||
},
|
||||
isServiceableMobile() {
|
||||
if (this.isRecalibrationServiceableMobile !== null) {
|
||||
return this.isGlassServiceableMobile && this.isRecalibrationServiceableMobile;
|
||||
}
|
||||
return this.isGlassServiceableMobile;
|
||||
},
|
||||
isServiceableInshop() {
|
||||
if (this.isRecalibrationServiceableInshop !== null) {
|
||||
return this.isGlassServiceableInshop && this.isRecalibrationServiceableInshop;
|
||||
}
|
||||
|
||||
return this.isGlassServiceableInshop;
|
||||
},
|
||||
isShopQuestionDisplayed() {
|
||||
return (
|
||||
this.selectedAppointmentType === 'Inshop'
|
||||
|| this.selectedAppointmentType === 'Dropoff'
|
||||
);
|
||||
},
|
||||
isAppointmentTypeDisplayed() {
|
||||
return true; // this.zipCode && !this.displayNoShopsAlert;
|
||||
},
|
||||
requiresInshopRecalibration() {
|
||||
// Specifically check for isRecalibrationServiceableMobile === false, not null or true.
|
||||
return (
|
||||
this.isServiceableInshop
|
||||
&& this.isGlassServiceableMobile
|
||||
&& this.isRecalibrationServiceableMobile === false
|
||||
);
|
||||
},
|
||||
displayMilitaryZipAlert() {
|
||||
return this.zipContainsMilitaryBase && this.isServiceableMobile;
|
||||
},
|
||||
displayNoShopsAlert() {
|
||||
return !this.isServiceableInshop && !this.isServiceableMobile;
|
||||
},
|
||||
displayRecalibrationWarning() {
|
||||
return this.requiresInshopRecalibration;
|
||||
},
|
||||
displayServiceableInshopOnly() {
|
||||
return (
|
||||
!this.displayRecalibrationWarning
|
||||
&& this.isServiceableInshop
|
||||
&& !this.isServiceableMobile
|
||||
);
|
||||
},
|
||||
displayServiceableMobileOnly() {
|
||||
return this.isServiceableMobile && !this.isServiceableInshop;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
|
@ -182,50 +330,108 @@ export default {
|
|||
this.$router.navigate(this.navigationScenarios.CLICKED_BACK, this.$route);
|
||||
},
|
||||
async forwardButtonAction() {
|
||||
// validate and save data here
|
||||
useMainStore().saveServiceLocation({
|
||||
address: this.streetAddress,
|
||||
address2: this.apartmentNumberOrBusinessName,
|
||||
city: this.city,
|
||||
state: this.state,
|
||||
zipCode: this.zipCode,
|
||||
zipCodeCtu: this.zipCodeCtu,
|
||||
appointmentType: this.selectedAppointmentType,
|
||||
isVehicleProtected: this.isVehicleProtected,
|
||||
provider: {
|
||||
providerNumber: this.selectedProvider?.providerNumber,
|
||||
address: {
|
||||
streetAddress: this.selectedProvider?.address?.streetAddress,
|
||||
city: this.selectedProvider?.address?.city,
|
||||
state: this.selectedProvider?.address?.state,
|
||||
zipCode: this.selectedProvider?.address?.zipCode,
|
||||
zipCodeCtu: this.selectedProvider?.address?.zipCodeCtu
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.$router.navigate(this.navigationScenarios.CLICKED_FORWARD, this.$route);
|
||||
},
|
||||
openModalAction(modalName) {
|
||||
this.$refs[modalName].openModal();
|
||||
},
|
||||
resetDependentState() {
|
||||
|
||||
},
|
||||
setData(zipCodeData, serviceabilityDetails) {
|
||||
getServiceAddressFromStore() {
|
||||
return useMainStore().order.serviceLocation.address;
|
||||
},
|
||||
getServiceAddress2FromStore() {
|
||||
return useMainStore().order.serviceLocation.address2;
|
||||
},
|
||||
getServiceCityFromStore() {
|
||||
return useMainStore().order.serviceLocation.city;
|
||||
},
|
||||
getServiceStateFromStore() {
|
||||
return useMainStore().order.serviceLocation.state || useMainStore().order.customer.address.state;
|
||||
},
|
||||
getServiceZipCodeFromStore() {
|
||||
return useMainStore().order.serviceLocation.zipCode || useMainStore().order.customer.address.zipCode;
|
||||
},
|
||||
getIsVehicleProtectedFromStore() {
|
||||
return useMainStore().order.serviceLocation.isVehicleProtected;
|
||||
},
|
||||
getSelectedAppointmentType() {
|
||||
return useMainStore().order.serviceLocation.appointmentType;
|
||||
},
|
||||
getSelectedProvider() {
|
||||
return useMainStore().order.serviceLocation.provider;
|
||||
},
|
||||
setData(zipCodeData, serviceabilityDetails, mobileFeePart) {
|
||||
if (zipCodeData) {
|
||||
this.zipContainsMilitaryBase = zipCodeData.containsMilitaryBase;
|
||||
}
|
||||
|
||||
if (serviceabilityDetails) {
|
||||
this.setServiceabilityDetails(serviceabilityDetails);
|
||||
}
|
||||
|
||||
if (mobileFeePart) {
|
||||
this.mobileFeePart = mobileFeePart;
|
||||
}
|
||||
},
|
||||
setContainsMilitaryBase(val) {
|
||||
if (this.zipContainsMilitaryBase !== val) {
|
||||
this.zipContainsMilitaryBase = val;
|
||||
}
|
||||
},
|
||||
setMobileFeePart(mobileFeePart) {
|
||||
this.mobileFeePart = mobileFeePart;
|
||||
},
|
||||
resetMobileLocation() {
|
||||
this.streetAddress = '';
|
||||
this.apartmentNumberOrBusinessName = '';
|
||||
this.city = '';
|
||||
|
||||
this.isVehicleProtected = null;
|
||||
},
|
||||
setServiceabilityDetails(serviceabilityDetails) {
|
||||
this.isGlassServiceableInshop = serviceabilityDetails.isGlassServiceableInshop;
|
||||
this.isRecalibrationServiceableInshop =
|
||||
serviceabilityDetails.isRecalibrationServiceableInshop;
|
||||
this.isRecalibrationServiceableInshop = serviceabilityDetails.isRecalibrationServiceableInshop;
|
||||
this.isGlassServiceableMobile = serviceabilityDetails.isGlassServiceableMobile;
|
||||
this.isRecalibrationServiceableMobile =
|
||||
serviceabilityDetails.isRecalibrationServiceableMobile;
|
||||
this.isRecalibrationServiceableMobile = serviceabilityDetails.isRecalibrationServiceableMobile;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$page-side-padding: 1.5rem;
|
||||
|
||||
.page-container-grouped-styles {
|
||||
overflow: auto;
|
||||
}
|
||||
.modal-open {
|
||||
.page-container-grouped-styles {
|
||||
overflow: hidden;
|
||||
|
||||
.main-content-container {
|
||||
padding: 0 1.5rem !important;
|
||||
}
|
||||
}
|
||||
#sub-header span {
|
||||
color: $black;
|
||||
}
|
||||
|
||||
.question-text {
|
||||
& > span {
|
||||
line-height: 1.5rem;
|
||||
|
|
@ -239,7 +445,6 @@ export default {
|
|||
line-height: 1.250rem;
|
||||
}
|
||||
}
|
||||
|
||||
.choose-option{
|
||||
.button-question >div {
|
||||
&:first-of-type {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,205 @@
|
|||
<template>
|
||||
<transition
|
||||
name="fade"
|
||||
mode="out-in">
|
||||
<baseInputButton
|
||||
v-bind="$props"
|
||||
v-model="selectedValue"
|
||||
buttonWrapperClasses="list-group base-input-button list-button rounded-3 d-flex flex-column w-100 mb-2">
|
||||
<div
|
||||
:aria-label="buttonLabel"
|
||||
class="button-content list-button-content d-flex flex-column justify-content-center py-3 px-4">
|
||||
<div class="row-one">
|
||||
<span
|
||||
class="m-0 button-label-copy"
|
||||
:class="textPosition">{{
|
||||
buttonLabel
|
||||
}}</span>
|
||||
<span
|
||||
class="m-0 caption ms-1"
|
||||
:class="textPosition">{{
|
||||
buttonLabelSubCopy
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="row-two">
|
||||
<span
|
||||
v-if="buttonBodyCopy"
|
||||
class="m-0 button-label-sub-copy small"
|
||||
v-html="buttonBodyCopy"></span>
|
||||
</div>
|
||||
<span
|
||||
v-if="screenReaderOnlyText"
|
||||
class="sr-only">
|
||||
{{ screenReaderOnlyText }}
|
||||
</span>
|
||||
</div>
|
||||
</baseInputButton>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import baseInputButton from '@/digital-components/base-input-button/base-input-button.vue';
|
||||
import inputButtonWrapperMixin from '@/mixins/input-button-wrapper-mixin';
|
||||
|
||||
export default {
|
||||
name: 'shop-list-button',
|
||||
components: {
|
||||
baseInputButton
|
||||
},
|
||||
mixins: [inputButtonWrapperMixin],
|
||||
data() {
|
||||
return {
|
||||
availabilityRating: null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isLoaderDisplayed() {
|
||||
return this.availabilityRating == null;
|
||||
},
|
||||
availabilityRatingClass() {
|
||||
if (this.availabilityRating == null) {
|
||||
return 'gray';
|
||||
}
|
||||
return this.availabilityRating === 'high' ? 'green' : 'orange';
|
||||
},
|
||||
badgeText() {
|
||||
if (this.availabilityRating != null) {
|
||||
return this.availabilityRating === 'high' ? 'Appts available' : 'Appts low';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
displayLoader() {
|
||||
this.isLoaderDisplayed = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.list-button {
|
||||
outline: none;
|
||||
input[type="radio"],
|
||||
input[type="checkbox"] {
|
||||
position: static; //override bootstrap
|
||||
|
||||
&:focus-visible + .list-button-content {
|
||||
box-shadow: 0 0 0 2.5px $blue;
|
||||
}
|
||||
&:focus + .list-button-content {
|
||||
box-shadow: 0 0 0 2.5px $blue;
|
||||
}
|
||||
&:checked + .list-button-content {
|
||||
color: $black;
|
||||
font-weight: 500;
|
||||
background: $blue-100;
|
||||
box-shadow: 0 0 0 1px $blue;
|
||||
}
|
||||
&:checked:focus + .list-button-content {
|
||||
box-shadow: 0 0 0 2.5px $blue;
|
||||
}
|
||||
&:checked + .list-button-content p,
|
||||
&:checked + .list-button-content span {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
.list-button-content {
|
||||
color: $gray-600;
|
||||
position: relative;
|
||||
background: $white;
|
||||
transition: all 150ms linear;
|
||||
border-radius: $border-radius-lg;
|
||||
border: 1px solid $gray-500;
|
||||
width: 100%;
|
||||
outline: none;
|
||||
|
||||
span {
|
||||
&.small {
|
||||
font-size: 0.75rem;
|
||||
color: $gray-550;
|
||||
}
|
||||
}
|
||||
}
|
||||
.button-content {
|
||||
row-gap: 0.25rem;
|
||||
|
||||
.row-one {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: 1.5rem;
|
||||
|
||||
.button-label-copy {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.availability-indicator {
|
||||
background-repeat: no-repeat;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: auto;
|
||||
padding: 0.125rem 0.5rem;
|
||||
|
||||
.availability-badge {
|
||||
display: inline;
|
||||
width: 13px;
|
||||
height: 12px;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
margin: 0 0.25rem 0 0;
|
||||
|
||||
&.green {
|
||||
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 13 12' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M6.5 12C3.19159 12 0.5 9.30841 0.5 6C0.5 2.69159 3.19159 0 6.5 0C9.80841 0 12.5 2.69159 12.5 6C12.5 9.30841 9.80841 12 6.5 12ZM6.5 0.785047C3.62449 0.785047 1.28505 3.12449 1.28505 6C1.28505 8.87551 3.62449 11.215 6.5 11.215C9.37551 11.215 11.715 8.87551 11.715 6C11.715 3.12449 9.37551 0.785047 6.5 0.785047Z' fill='%23006A36'/%3E%3Cpath d='M5.697 7.95252C5.5927 7.95252 5.49289 7.91102 5.41999 7.837L3.90597 6.32299C3.75233 6.16934 3.75233 5.92149 3.90597 5.76785C4.05962 5.6142 4.30747 5.6142 4.46111 5.76785L5.69812 7.00373L8.53999 4.16186C8.69364 4.00822 8.94149 4.00822 9.09513 4.16186C9.24878 4.31551 9.24878 4.56336 9.09513 4.717L5.97626 7.83588C5.90224 7.9099 5.80242 7.9514 5.69925 7.9514L5.697 7.95252Z' fill='%23006A36'/%3E%3C/svg%3E%0A");
|
||||
}
|
||||
|
||||
&.orange {
|
||||
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 13 12' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M6.5 0C9.81368 0 12.5 2.68632 12.5 6C12.5 9.31368 9.81368 12 6.5 12C3.18632 12 0.5 9.31368 0.5 6C0.5 2.68632 3.18632 0 6.5 0ZM6.5 0.84C3.6548 0.84 1.34 3.1548 1.34 6C1.34 8.8452 3.6548 11.16 6.5 11.16C9.3452 11.16 11.66 8.8452 11.66 6C11.66 3.1548 9.3452 0.84 6.5 0.84ZM7.90018 4.00596C8.06422 3.84204 8.33002 3.84192 8.49406 4.00596C8.6581 4.17 8.6581 4.43592 8.49406 4.59996L7.0939 6L8.49406 7.40004C8.6581 7.56408 8.6581 7.83 8.49406 7.99404C8.4121 8.076 8.30458 8.11704 8.19706 8.11704C8.08966 8.11704 7.98214 8.076 7.90018 7.99404L6.50002 6.594L5.09986 7.99404C5.01778 8.076 4.91038 8.11704 4.80286 8.11704C4.69546 8.11704 4.58794 8.076 4.50598 7.99404C4.34182 7.83 4.34182 7.56408 4.50598 7.40004L5.90614 6L4.50598 4.59996C4.34182 4.43592 4.34182 4.17 4.50598 4.00596C4.66978 3.84192 4.93582 3.84204 5.09986 4.00596L6.50002 5.406L7.90018 4.00596Z' fill='%23E86421'/%3E%3C/svg%3E%0A");
|
||||
}
|
||||
}
|
||||
.button-auxillary-copy {
|
||||
box-sizing: border-box;
|
||||
justify-content: right;
|
||||
line-height: 1.25rem;
|
||||
font-weight: 500;
|
||||
font-size: 0.75rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.loader {
|
||||
padding: 0 0.25rem 0 0.25rem;
|
||||
padding-top: 0.125rem;
|
||||
padding-bottom: 0.125rem;
|
||||
}
|
||||
|
||||
.loader:after {
|
||||
background-color: $gray-550;
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
}
|
||||
|
||||
&.green {
|
||||
color: $green-700;
|
||||
background-color: $green-100;
|
||||
}
|
||||
|
||||
&.orange {
|
||||
color: $orange-600;
|
||||
background-color: $orange-100;
|
||||
}
|
||||
|
||||
&.gray {
|
||||
color: $gray-600;
|
||||
background-color: $gray-100;
|
||||
padding-left: 0.125rem;
|
||||
padding-right: 0.125rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.row-two {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
284
src/layouts/service-location/shop-question/shop-question.vue
Normal file
284
src/layouts/service-location/shop-question/shop-question.vue
Normal file
|
|
@ -0,0 +1,284 @@
|
|||
<template>
|
||||
<transition
|
||||
name="fade"
|
||||
mode="out-in">
|
||||
<div
|
||||
v-if="isDisplayed"
|
||||
class="shop-question"
|
||||
aria-live="polite">
|
||||
<alert
|
||||
v-if="displayDropoffInformation"
|
||||
ref="alertDropoffInformation"
|
||||
class="mb-4 drop-off-alert"
|
||||
cmsWidgetName="AlertDropoffInformationWidget"
|
||||
alertClass="alert-info"
|
||||
:isDismissible="false" />
|
||||
<buttonQuestion
|
||||
ref="buttonQuestion"
|
||||
v-model="selectedProviderNumber"
|
||||
buttonTypeString="shopListButton"
|
||||
:buttonTypeObject="shopListButton"
|
||||
class="radioQuestion"
|
||||
:questionText="questionText"
|
||||
:answers="answers"
|
||||
groupName="chooseShop"
|
||||
textPosition="text-start"
|
||||
isRequired
|
||||
validationRules="option-required"
|
||||
:additionalButtonData="additionalButtonData" />
|
||||
<textLink
|
||||
v-show="displaySeeMoreLocationsLink"
|
||||
id="showMoreShopsId"
|
||||
ref="showMoreShopsLink"
|
||||
class="show-more-shops-link"
|
||||
cmsWidgetName="ShowMoreShopsLinkWidget"
|
||||
linkType="text"
|
||||
:text="showMoreShopsLinkText"
|
||||
href="#!"
|
||||
:aria-label="showMoreShopsLinkText"
|
||||
@click-event="getNextShopsFromList" />
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// Components
|
||||
import alert from '@/ux-components/alert/alert.vue';
|
||||
import buttonQuestion from '@/digital-components/button-question/button-question.vue';
|
||||
import textLink from '@/ux-components/text-link/text-link.vue';
|
||||
|
||||
// Supporting files
|
||||
import baseMixin from '@/mixins/base-mixin.js';
|
||||
import { defineRule } from 'vee-validate';
|
||||
import { required } from '@/helpers/validation-rules';
|
||||
import errorMessages from '@/constants/error-messages';
|
||||
import { useMainStore } from '@/store/index.js';
|
||||
import { nextTick } from 'vue';
|
||||
import { getAvailabilityRating } from '@/helpers/service-location-helper';
|
||||
import shopListButton from './shop-list-button/shop-list-button.vue';
|
||||
|
||||
defineRule('option-required', required(errorMessages.OPTION_REQUIRED));
|
||||
|
||||
export default {
|
||||
name: 'shop-question',
|
||||
components: {
|
||||
alert,
|
||||
buttonQuestion,
|
||||
textLink
|
||||
},
|
||||
mixins: [baseMixin],
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Object,
|
||||
default: () => null
|
||||
},
|
||||
selectedAppointmentType: String,
|
||||
cmsWidgetName: String,
|
||||
validationRules: String,
|
||||
isDisplayed: Boolean
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
data() {
|
||||
return {
|
||||
shopProviders: [],
|
||||
shopListButton,
|
||||
answers: [],
|
||||
shopIndex: 0,
|
||||
displaySeeMoreLocationsLink: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
questionText() {
|
||||
return this.getCmsContent(this.cmsWidgetName, 'QuestionText');
|
||||
},
|
||||
selectedValue: {
|
||||
get() {
|
||||
return this.modelValue;
|
||||
},
|
||||
set(newValue) {
|
||||
this.$emit('update:modelValue', newValue);
|
||||
}
|
||||
},
|
||||
selectedProviderNumber: {
|
||||
get() {
|
||||
return this.selectedValue?.providerNumber;
|
||||
},
|
||||
set(newValue) {
|
||||
// Button Question only supports Number, or String data types so we must get the full object to emit
|
||||
this.selectedValue = this.getSelectedProviderObject(newValue);
|
||||
}
|
||||
},
|
||||
displayDropoffInformation() {
|
||||
return this.selectedAppointmentType === 'Dropoff';
|
||||
},
|
||||
showMoreShopsLinkText() {
|
||||
return this.getCmsContent('ShowMoreShopsLinkWidget', 'Text');
|
||||
},
|
||||
additionalButtonData() {
|
||||
const startDate = new Date();
|
||||
const endDate = new Date();
|
||||
endDate.setDate(startDate.getDate() + 6);
|
||||
|
||||
const formattedStartDate = startDate.toISOString().split('T')[0];
|
||||
const formattedEndDate = endDate.toISOString().split('T')[0];
|
||||
|
||||
return {
|
||||
availabilityRatingCallback: getAvailabilityRating,
|
||||
startDate: formattedStartDate,
|
||||
endDate: formattedEndDate,
|
||||
shopAppointmentType: this.selectedAppointmentType
|
||||
};
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
selectedAppointmentType: {
|
||||
async handler(newValue) {
|
||||
this.resetAnswers();
|
||||
|
||||
await nextTick();
|
||||
|
||||
this.selectedProviderNumber = null;
|
||||
|
||||
await nextTick();
|
||||
|
||||
if (newValue !== 'Mobile') {
|
||||
this.getNextShopsFromList();
|
||||
}
|
||||
}
|
||||
},
|
||||
shopProviders: {
|
||||
async handler(newValue) {
|
||||
await nextTick();
|
||||
|
||||
if (this.selectedAppointmentType) {
|
||||
const selectedShopIndex = this.getSelectedProviderIndex(
|
||||
newValue,
|
||||
this.selectedProviderNumber
|
||||
);
|
||||
|
||||
if (selectedShopIndex >= 3) {
|
||||
await this.getNextShopsFromList(selectedShopIndex + 1);
|
||||
} else {
|
||||
await this.getNextShopsFromList();
|
||||
await nextTick();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
loadInitialData(serviceZipCode) {
|
||||
return this.loadData(serviceZipCode);
|
||||
},
|
||||
loadData(serviceZipCode) {
|
||||
return useMainStore().getProviders(serviceZipCode);
|
||||
},
|
||||
initializeComponent(shopQuestionInitialData) {
|
||||
this.shopProviders = shopQuestionInitialData.shopProviders;
|
||||
},
|
||||
async getNextShopsFromList(numberToGet = 3) {
|
||||
const shopIterator = (array, n) => {
|
||||
const l = array.length;
|
||||
return () => {
|
||||
const end = this.shopIndex + n;
|
||||
const part = array.slice(this.shopIndex, end);
|
||||
this.shopIndex = end < l ? end : this.shopProviders.length;
|
||||
return part;
|
||||
};
|
||||
};
|
||||
|
||||
const toTitleCase = (str) => str.replace(/\w\S*/g, (txt) => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase());
|
||||
|
||||
const nextShop = shopIterator(this.shopProviders, numberToGet);
|
||||
|
||||
// Map API result data
|
||||
const mappedData = nextShop().map((shopProvider) => {
|
||||
const streetAddress = toTitleCase(shopProvider.address.streetAddress);
|
||||
const city = toTitleCase(shopProvider.address.city);
|
||||
const { state } = shopProvider.address;
|
||||
const { zipCode } = shopProvider.address;
|
||||
const distanceInMiles = Math.round(shopProvider.distanceInMiles * 2) / 2;
|
||||
|
||||
return {
|
||||
buttonLabel: city,
|
||||
buttonLabelSubCopy: `${distanceInMiles} mi`,
|
||||
buttonBodyCopy: `${streetAddress}, ${city}, ${state} ${zipCode}`,
|
||||
value: shopProvider.providerNumber
|
||||
};
|
||||
});
|
||||
|
||||
if (this.answers.length === 0) {
|
||||
this.answers = mappedData;
|
||||
} else {
|
||||
mappedData.forEach((shop) => {
|
||||
this.answers.push(shop);
|
||||
});
|
||||
}
|
||||
|
||||
await nextTick();
|
||||
|
||||
if (this.shopIndex === this.shopProviders.length) {
|
||||
this.displaySeeMoreLocationsLink = false;
|
||||
} else {
|
||||
this.displaySeeMoreLocationsLink = true;
|
||||
}
|
||||
|
||||
await nextTick();
|
||||
|
||||
this.scrollToPageBottom();
|
||||
},
|
||||
resetAnswers() {
|
||||
this.answers = [];
|
||||
this.shopIndex = 0;
|
||||
},
|
||||
async reloadShopData(serviceZipCode) {
|
||||
const result = await this.loadData(serviceZipCode);
|
||||
this.initializeComponent(result.data);
|
||||
|
||||
this.resetAnswers();
|
||||
|
||||
await nextTick();
|
||||
await this.getNextShopsFromList();
|
||||
},
|
||||
getSelectedProviderObject(providerNumber) {
|
||||
const provider = this.shopProviders?.find((p) => p.providerNumber === providerNumber);
|
||||
return provider;
|
||||
},
|
||||
getSelectedProviderIndex(providers, selectedProviderNumber) {
|
||||
const index = providers.findIndex((p) => p.providerNumber === selectedProviderNumber);
|
||||
return index;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.shop-question {
|
||||
margin-top: 1rem;
|
||||
text-align: center;
|
||||
|
||||
.button-question {
|
||||
.question-text {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.drop-off-alert {
|
||||
background: url("data:image/svg+xml,%3Csvg viewBox='0 0 12 12' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cg clip-path='url(%23clip0_13957_112512)'%3E%3Cpath d='M5.99865 0C4.81147 4.82643e-07 3.65095 0.352111 2.66392 1.01179C1.67688 1.67146 0.907678 2.60907 0.45361 3.70599C-0.000459241 4.80291 -0.11899 6.00986 0.113013 7.17415C0.345015 8.33845 0.917126 9.40778 1.75697 10.2469C2.59682 11.086 3.66666 11.6571 4.83117 11.8881C5.99567 12.119 7.20251 11.9994 8.29902 11.5443C9.39553 11.0893 10.3324 10.3192 10.9912 9.33159C11.65 8.34396 12.0011 7.18313 12 5.99594C11.9971 4.40566 11.3638 2.88142 10.2388 1.75742C9.11375 0.633431 7.58894 0.00143011 5.99865 0V0ZM5.99865 11.2478C4.96135 11.2473 3.94748 10.9392 3.08518 10.3627C2.22288 9.7861 1.55085 8.96685 1.15401 8.00846C0.75718 7.05006 0.653353 5.99554 0.855656 4.97815C1.05796 3.96077 1.55731 3.02618 2.29061 2.29251C3.0239 1.55884 3.95823 1.059 4.97551 0.856176C5.99279 0.653349 7.04737 0.756633 8.00597 1.15297C8.96457 1.54931 9.78416 2.22092 10.3612 3.08293C10.9382 3.94493 11.2467 4.95864 11.2478 5.99594C11.2478 7.38835 10.6949 8.72377 9.71053 9.70861C8.7262 10.6934 7.39106 11.2471 5.99865 11.2478V11.2478Z' fill='%2306577C'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M6.22736 8.84695C6.30613 8.76818 6.35038 8.66135 6.35038 8.54996V5.30996C6.35038 5.19857 6.30613 5.09174 6.22736 5.01298C6.1486 4.93421 6.04177 4.88996 5.93038 4.88996C5.81899 4.88996 5.71216 4.93421 5.63339 5.01298C5.55463 5.09174 5.51038 5.19857 5.51038 5.30996V8.54996C5.51038 8.66135 5.55463 8.76818 5.63339 8.84695C5.71216 8.92571 5.81899 8.96996 5.93038 8.96996C6.04177 8.96996 6.1486 8.92571 6.22736 8.84695ZM5.69704 3.97918C5.76611 4.02533 5.84731 4.04996 5.93038 4.04996C5.98558 4.05012 6.04026 4.03936 6.09129 4.01831C6.14232 3.99726 6.18868 3.96633 6.22771 3.9273C6.26675 3.88827 6.29768 3.8419 6.31873 3.79088C6.33978 3.73985 6.35053 3.68516 6.35038 3.62996C6.35038 3.54689 6.32574 3.46569 6.27959 3.39662C6.23344 3.32755 6.16785 3.27372 6.0911 3.24193C6.01436 3.21014 5.92991 3.20183 5.84844 3.21803C5.76697 3.23424 5.69213 3.27424 5.63339 3.33298C5.57465 3.39171 5.53465 3.46655 5.51845 3.54802C5.50224 3.6295 5.51056 3.71394 5.54235 3.79069C5.57414 3.86743 5.62797 3.93303 5.69704 3.97918Z' fill='%2306577C'/%3E%3C/g%3E%3Cdefs%3E%3CclipPath id='clip0_13957_112512'%3E%3Crect width='12' height='12' fill='white'/%3E%3C/clipPath%3E%3C/defs%3E%3C/svg%3E%0A");
|
||||
background-repeat: no-repeat;
|
||||
background-size: 0.75rem;
|
||||
background-position: 0.5rem 0.75rem;
|
||||
border-radius: 0.5rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 0.5rem 0.5rem 0.5rem 1.5rem !important;
|
||||
gap: 0.25rem;
|
||||
|
||||
.alert-heading {
|
||||
text-align: left;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -25,6 +25,14 @@ export default {
|
|||
},
|
||||
savePageDataToStore(page, data) {
|
||||
useMainStore().updatePageData({ page, data });
|
||||
},
|
||||
scrollToPageTop() {
|
||||
const container = document.getElementsByClassName('page-container-grouped-styles')[0];
|
||||
container.scrollTo({ top: 0, left: 0, behavior: 'smooth' });
|
||||
},
|
||||
scrollToPageBottom() {
|
||||
const container = document.getElementsByClassName('page-container-grouped-styles')[0];
|
||||
container.scrollTo({ top: container.scrollHeight, left: 0, behavior: 'smooth' });
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import applicationConfig from '@/constants/application-config';
|
|||
import issPageValues from '@/router/router-constants/issPage-values';
|
||||
import damageLocationsSelected from '@/constants/damage-locations-selected';
|
||||
import coverageStatuses from '@/constants/coverage-statuses';
|
||||
import { PREMIUM_FEE_PART_TYPE } from '@/constants/schedule-constants';
|
||||
|
||||
const storeId = 'main';
|
||||
|
||||
|
|
@ -76,10 +77,23 @@ const getDefaultState = () => ({
|
|||
},
|
||||
serviceLocation: {
|
||||
address: null,
|
||||
address2: null,
|
||||
city: null,
|
||||
state: null,
|
||||
zipCode: null,
|
||||
zipCodeCtu: null
|
||||
zipCodeCtu: null,
|
||||
appointmentType: null,
|
||||
isVehicleProtected: null,
|
||||
provider: {
|
||||
providerNumber: null,
|
||||
address: {
|
||||
streetAddress: null,
|
||||
city: null,
|
||||
state: null,
|
||||
zipCode: null,
|
||||
zipCodeCtu: null
|
||||
}
|
||||
}
|
||||
},
|
||||
lineItems: {
|
||||
glassParts: null,
|
||||
|
|
@ -92,7 +106,8 @@ const getDefaultState = () => ({
|
|||
insuranceCoverage: {
|
||||
isVerified: false,
|
||||
coverageStatus: coverageStatuses.PENDING
|
||||
}
|
||||
},
|
||||
parentAccountNumber: 0
|
||||
},
|
||||
contactInfo: {
|
||||
firstName: null,
|
||||
|
|
@ -563,6 +578,63 @@ export const useMainStore = defineStore({
|
|||
}
|
||||
});
|
||||
},
|
||||
|
||||
getShopTimeSlots(startDate, endDate, shopAppointmentType, providerNumber) {
|
||||
const { order } = this;
|
||||
const { vehicle } = this.order;
|
||||
|
||||
let lineItems = [
|
||||
...(order.lineItems.supportingItems ?? []),
|
||||
...(order.lineItems.vaps ?? []),
|
||||
...getFlattenedArrayOfLineItemsWithChildParts(order.lineItems.glassParts)
|
||||
];
|
||||
lineItems = lineItems.map((lineItem) => ({
|
||||
partNumber: lineItem.partNumber,
|
||||
partType: lineItem.partType
|
||||
}));
|
||||
const glassPieces = order.damage.glassToReplace
|
||||
? convertGlassPieceToBackEndCompatibleFormat(order.damage.glassToReplace)
|
||||
: [];
|
||||
const payload = {
|
||||
providerNumber,
|
||||
startDate,
|
||||
endDate,
|
||||
shopAppointmentType,
|
||||
applicationName: applicationConfig.APPLICATION_NAME,
|
||||
parentAccountNumber: this.payment.parentAccountNumber,
|
||||
carId: vehicle.carId,
|
||||
lineItems,
|
||||
glassPieces,
|
||||
eon: order.eon,
|
||||
coverage: {
|
||||
status: '',
|
||||
deductible: 0,
|
||||
additionalAuthFlag: ''
|
||||
},
|
||||
partSelection: {
|
||||
hasAnsweredPartQuestions: !!order.damage.partQuestionAnswers?.length,
|
||||
hasAnsweredMoldingQuestions: !!order.damage.moldingQuestionAnswers?.length,
|
||||
hasAnsweredCapabilityQuestions: !!order.damage.capabilityQuestionAnswers?.length,
|
||||
hasManuallySelectedParts:
|
||||
!!this.applicationUser.pageData['vehicle-parts']?.partsOrQuestions
|
||||
.length
|
||||
},
|
||||
vehicle: {
|
||||
year: vehicle.year,
|
||||
make: vehicle.make,
|
||||
model: vehicle.model,
|
||||
style: vehicle.style,
|
||||
vin: vehicle.vin ?? ''
|
||||
}
|
||||
};
|
||||
|
||||
return globalMethods.callHttpClient({
|
||||
method: endpoints.GetShopTimeSlots.method,
|
||||
endpoint: endpoints.GetShopTimeSlots.url,
|
||||
payload,
|
||||
additionalSuccessEventDataHandler: (response) => provisionalTriggersToString(response.data.provisionalTriggers)
|
||||
});
|
||||
},
|
||||
async getWipers() {
|
||||
const { carId } = this.order.vehicle;
|
||||
// WARNING
|
||||
|
|
@ -594,6 +666,16 @@ export const useMainStore = defineStore({
|
|||
});
|
||||
},
|
||||
|
||||
getProviders(serviceZipCode) {
|
||||
const damageType = this.damage.isRepair ? 'Repair' : 'Replace';
|
||||
const shopRadiusInMiles = 100;
|
||||
|
||||
return globalMethods.callHttpClient({
|
||||
method: endpoints.GetProviders.method,
|
||||
endpoint: `${endpoints.GetProviders.url}/${serviceZipCode}/${damageType}/${shopRadiusInMiles}`
|
||||
});
|
||||
},
|
||||
|
||||
async getSupportingItems() {
|
||||
const glassPartsArray = this.order.lineItems.glassParts ?? [];
|
||||
const { carId } = this.order.vehicle;
|
||||
|
|
@ -650,9 +732,20 @@ export const useMainStore = defineStore({
|
|||
return availableLineItems;
|
||||
},
|
||||
|
||||
getMobileFeePart() {
|
||||
const damageType = this.damage.isRepair ? 'Repair' : 'Replace';
|
||||
const parentAccountNumber = 167132; // TODO: MAKE THIS REAL
|
||||
const billToAccountNumber = 87291; // TODO: MAKE THIS REAL
|
||||
|
||||
return globalMethods.callHttpClient({
|
||||
method: endpoints.GetMobileFeePart.method,
|
||||
endpoint: `${endpoints.GetMobileFeePart.url}/${damageType}/${parentAccountNumber}/${billToAccountNumber}`
|
||||
});
|
||||
},
|
||||
|
||||
getServiceabilityDetails({ serviceZipCode }) {
|
||||
const lineItemsWithOnlyPartNumbers = this.order.lineItems.glassParts.map((glassPart) => ({
|
||||
partNumber: glassPart.partNumber
|
||||
const lineItemsWithOnlyPartNumbers = this.order.lineItems.supportingItems.map((lineItem) => ({
|
||||
partNumber: lineItem.partNumber
|
||||
}));
|
||||
const lineItems = buildQueryStringParameterFromArrayOfComplexObjects(lineItemsWithOnlyPartNumbers, 'lineItems');
|
||||
|
||||
|
|
@ -692,7 +785,7 @@ export const useMainStore = defineStore({
|
|||
});
|
||||
},
|
||||
|
||||
setSaveSessionInfo(response){
|
||||
setSaveSessionInfo(response) {
|
||||
this.order.referralNumber = response.referralNumber;
|
||||
this.order.referralSequenceNumber = response.referralSequenceNumber;
|
||||
this.order.referralDate = response.referralDate;
|
||||
|
|
@ -810,11 +903,11 @@ export const useMainStore = defineStore({
|
|||
});
|
||||
},
|
||||
|
||||
setSaveSessionPromise(promise){
|
||||
setSaveSessionPromise(promise) {
|
||||
this.applicationUser.saveSessionPromise = promise;
|
||||
},
|
||||
|
||||
clearSaveSessionPromise(){
|
||||
clearSaveSessionPromise() {
|
||||
this.applicationUser.saveSessionPromise = null;
|
||||
},
|
||||
|
||||
|
|
@ -869,11 +962,25 @@ export const useMainStore = defineStore({
|
|||
},
|
||||
|
||||
updateServiceLocation(serviceLocationInfo) {
|
||||
this.order.serviceLocation.address = serviceLocationInfo.address;
|
||||
this.order.serviceLocation.city = serviceLocationInfo.city;
|
||||
this.order.serviceLocation.state = serviceLocationInfo.state;
|
||||
this.order.serviceLocation.zipCode = serviceLocationInfo.zipCode;
|
||||
this.order.serviceLocation.zipCodeCtu = serviceLocationInfo.zipCodeCtu;
|
||||
state.order.serviceLocation.address = serviceLocationInfo.address;
|
||||
state.order.serviceLocation.address2 = serviceLocationInfo.address2;
|
||||
state.order.serviceLocation.city = serviceLocationInfo.city;
|
||||
state.order.serviceLocation.state = serviceLocationInfo.state;
|
||||
state.order.serviceLocation.zipCode = serviceLocationInfo.zipCode;
|
||||
state.order.serviceLocation.zipCodeCtu = serviceLocationInfo.zipCodeCtu;
|
||||
state.order.serviceLocation.appointmentType = serviceLocationInfo.appointmentType;
|
||||
state.order.serviceLocation.isVehicleProtected = serviceLocationInfo.isVehicleProtected;
|
||||
|
||||
state.order.serviceLocation.provider = {
|
||||
providerNumber: serviceLocationInfo.provider?.providerNumber,
|
||||
address: {
|
||||
streetAddress: serviceLocationInfo.provider?.address?.streetAddress,
|
||||
city: serviceLocationInfo.provider?.address?.city,
|
||||
state: serviceLocationInfo.provider?.address?.state,
|
||||
zipCode: serviceLocationInfo.provider?.address?.zipCode,
|
||||
zipCodeCtu: serviceLocationInfo.provider?.address?.zipCodeCtu
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
resetRegistrationState() {
|
||||
|
|
@ -885,6 +992,28 @@ export const useMainStore = defineStore({
|
|||
this.order.vehicle.registration.firstName = null;
|
||||
this.order.vehicle.registration.lastName = null;
|
||||
},
|
||||
resetServiceLocationAppointmentType() {
|
||||
this.order.serviceLocation.appointmentType = null;
|
||||
},
|
||||
resetServiceLocationProvider() {
|
||||
this.order.serviceLocation.provider = {
|
||||
providerNumber: null,
|
||||
address: {
|
||||
streetAddress: null,
|
||||
city: null,
|
||||
state: null,
|
||||
zipCode: null,
|
||||
zipCodeCtu: null
|
||||
}
|
||||
};
|
||||
},
|
||||
resetServiceLocationMobileAddress() {
|
||||
this.order.serviceLocation.address = null;
|
||||
this.order.serviceLocation.address2 = null;
|
||||
this.order.serviceLocation.city = null;
|
||||
this.order.serviceLocation.state = null;
|
||||
this.order.serviceLocation.isVehicleProtected = null;
|
||||
},
|
||||
|
||||
updateSupportingItems(partsData) {
|
||||
this.order.lineItems.supportingItems = partsData;
|
||||
|
|
@ -951,7 +1080,23 @@ export const useMainStore = defineStore({
|
|||
this.applicationUser.pageData[issPageValues.MOLDING_QUESTIONS] = null;
|
||||
this.applicationUser.pageData[issPageValues.CAPABILITY_QUESTIONS] = null;
|
||||
},
|
||||
resetSchedule() {
|
||||
this.order.schedule.date = null;
|
||||
this.order.schedule.startTime = null;
|
||||
this.order.schedule.endTime = null;
|
||||
this.order.schedule.routeCode = null;
|
||||
this.order.schedule.jobMaxMinutes = null;
|
||||
// this.order.schedule.jobMinMinutes = null;
|
||||
|
||||
// premium appointment fee used on schedule page also needs reset when schedule is reset
|
||||
const { supportingItems } = this.order.lineItems;
|
||||
const premiumAppointmentFeeIndex = supportingItems?.findIndex((item) => item.partType === PREMIUM_FEE_PART_TYPE);
|
||||
|
||||
if (premiumAppointmentFeeIndex >= 0) {
|
||||
supportingItems.splice(premiumAppointmentFeeIndex, 1);
|
||||
state.order.lineItems.supportingItems = supportingItems;
|
||||
}
|
||||
},
|
||||
resetSupportingItemsState() {
|
||||
this.order.lineItems.supportingItems = null;
|
||||
},
|
||||
|
|
@ -1148,6 +1293,47 @@ export const useMainStore = defineStore({
|
|||
saveVaps(vaps) {
|
||||
this.order.lineItems.vaps = vaps;
|
||||
},
|
||||
|
||||
// Price order actions
|
||||
async priceOrderItemsAndSaveServerData(availableLineItems, serviceZipCode, serviceZipCodeCtu) {
|
||||
const zipCodeToUse = serviceZipCode || this.order.serviceLocation.zipCode;
|
||||
const ctuToUse = serviceZipCodeCtu || this.order.serviceLocation.zipCodeCtu;
|
||||
const flattenedLineItemsWithChildParts = getFlattenedArrayOfLineItemsWithChildParts(availableLineItems);
|
||||
const lineItemsWithOnlyPartNumbers = flattenedLineItemsWithChildParts.map((lineItem) => ({
|
||||
partNumber: lineItem.partNumber
|
||||
}));
|
||||
const availableLineItemsFormattedForRequest =
|
||||
buildQueryStringParameterFromArrayOfComplexObjects(
|
||||
lineItemsWithOnlyPartNumbers,
|
||||
'lineItems'
|
||||
);
|
||||
|
||||
const { vehicle } = this.order;
|
||||
|
||||
let queryString =
|
||||
`ParentAccountNumber=${applicationConfig.CASH_PARENT_ACCOUNT_NUMBER}`
|
||||
+ `&CTU=${ctuToUse}`
|
||||
+ `&CarId=${vehicle.carId}`
|
||||
+ `&Make=${vehicle.make}`
|
||||
+ `&Model=${vehicle.model}`
|
||||
+ `&Year=${vehicle.year}`
|
||||
+ `&EON=${this.order.eon}`
|
||||
+ `&ZipCode=${zipCodeToUse}`
|
||||
+ `&${availableLineItemsFormattedForRequest}`;
|
||||
|
||||
const lineItemServerData = this.order.lineItems.serverData;
|
||||
if (lineItemServerData) {
|
||||
queryString += `&ServerData=${encodeURIComponent(lineItemServerData)}`;
|
||||
}
|
||||
|
||||
const response = await globalMethods.callHttpClient({
|
||||
method: endpoints.GetPriceOrderItems.method,
|
||||
endpoint: `${endpoints.GetPriceOrderItems.url}?${queryString}`
|
||||
});
|
||||
|
||||
// context.commit(storeMutations.UPDATE_LINE_ITEMS_SERVER_DATA, response.data.serverData);
|
||||
return addPricesToLineItems(availableLineItems, response.data.lineItems);
|
||||
},
|
||||
saveProviderPreferenceData(data) {
|
||||
this.updatePageData({ page: issPageValues.PROVIDER_PREFERENCE, data });
|
||||
},
|
||||
|
|
@ -1339,6 +1525,19 @@ export const useMainStore = defineStore({
|
|||
},
|
||||
|
||||
saveServiceLocation(serviceLocationInfo) {
|
||||
if (this.order.serviceLocation) {
|
||||
if (
|
||||
serviceLocationInfo.zipCode !== this.order.serviceLocation.zipCode
|
||||
|| !providersEqual(
|
||||
serviceLocationInfo.provider,
|
||||
this.order.serviceLocation.provider
|
||||
)
|
||||
|| serviceLocationInfo.appointmentType
|
||||
!== this.order.serviceLocation.appointmentType
|
||||
) {
|
||||
this.resetSchedule();
|
||||
}
|
||||
}
|
||||
this.updateServiceLocation(serviceLocationInfo);
|
||||
},
|
||||
|
||||
|
|
@ -1549,6 +1748,20 @@ function getLineItemQueryStringForPricing(lineItems) {
|
|||
}).join('');
|
||||
}
|
||||
|
||||
function getFlattenedArrayOfLineItemsWithChildParts(lineItems) {
|
||||
let flattenedArray = [];
|
||||
lineItems?.forEach((lineItem) => {
|
||||
flattenedArray.push(lineItem);
|
||||
if (lineItem.childParts) {
|
||||
flattenedArray = [
|
||||
...flattenedArray,
|
||||
...getFlattenedArrayOfLineItemsWithChildParts(lineItem.childParts)
|
||||
];
|
||||
}
|
||||
});
|
||||
return flattenedArray;
|
||||
}
|
||||
|
||||
function buildQueryStringParameterFromArrayOfComplexObjects(arrayOfObjects, parameterName) {
|
||||
let queryStringParameter = '';
|
||||
for (let i = 0; i < arrayOfObjects.length; i++) {
|
||||
|
|
@ -1559,3 +1772,25 @@ function buildQueryStringParameterFromArrayOfComplexObjects(arrayOfObjects, para
|
|||
// Remove trailing &
|
||||
return queryStringParameter.slice(0, -1);
|
||||
}
|
||||
|
||||
function convertGlassPieceToBackEndCompatibleFormat(glassPieces) {
|
||||
return glassPieces.map((glassPiece) => ({
|
||||
location: glassPiece.glassLocation,
|
||||
name: glassPiece.glassName
|
||||
}));
|
||||
}
|
||||
|
||||
function providersEqual(providerA, providerB) {
|
||||
return (
|
||||
providerA.providerNumber === providerB.providerNumber
|
||||
&& providerA.address?.city === providerB.address?.city
|
||||
&& providerA.address?.state === providerB.address?.state
|
||||
&& providerA.address?.streetAddress === providerB.address?.streetAddress
|
||||
&& providerA.address?.zipCode === providerB.address?.zipCode
|
||||
);
|
||||
// TODO: Change back to deepEqual once zipCodeCtu is added to saveSession.
|
||||
}
|
||||
|
||||
function provisionalTriggersToString(provisionalTriggers) {
|
||||
return `ProvisionalTriggers:${provisionalTriggers.join(',')}`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,28 +4,35 @@ body {
|
|||
font-size: 16px;
|
||||
background-color: #fff;
|
||||
color: #4D5151;
|
||||
|
||||
.container-fluid {
|
||||
max-width: 576px; //Remove once desktop app is complete
|
||||
|
||||
&.container-shadow {
|
||||
box-shadow: 0px 0px 6px 0px rgba(0,0,0,0.15); //Use instead of Bootstrap's helper
|
||||
box-shadow: 0px 0px 6px 0px rgba(0, 0, 0, 0.15); //Use instead of Bootstrap's helper
|
||||
}
|
||||
|
||||
&.make-tall {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.prevent-squish{
|
||||
|
||||
.prevent-squish {
|
||||
overflow-x: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.container,
|
||||
.container-fluid {
|
||||
overflow: hidden;
|
||||
}
|
||||
.sub-container{
|
||||
|
||||
.sub-container {
|
||||
&.make-tall {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
|
@ -34,6 +41,7 @@ body {
|
|||
overflow-x: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
left: -10000px;
|
||||
|
|
@ -42,21 +50,29 @@ body {
|
|||
height: 1px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.page-container-grouped-styles {
|
||||
@extend .container-fluid, .shadow, .p-0, .position-relative, .make-tall;
|
||||
}
|
||||
|
||||
//Footer modal backdrop adjustments for positioning
|
||||
.modal-backdrop {
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
max-width: 576px;
|
||||
height: calc(100% - 72px);
|
||||
|
||||
&.show {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
// Scroll page when modal isn't open
|
||||
.fade-on-route-transition {
|
||||
height: calc(100% - 10px);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
// Prevent scroll when modal is open
|
||||
&.modal-open {
|
||||
.fade-on-route-transition {
|
||||
|
|
@ -73,4 +89,4 @@ body {
|
|||
line-height: 24px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,27 +8,38 @@ body {
|
|||
|
||||
//Headings
|
||||
//add helper fw-bold to any element to get bold style (500)
|
||||
h1,.h1 {
|
||||
h1,
|
||||
.h1 {
|
||||
line-height: 1.325;
|
||||
font-weight: 300;
|
||||
}
|
||||
h2,.h2 {
|
||||
|
||||
h2,
|
||||
.h2 {
|
||||
line-height: 1.325;
|
||||
font-weight: 300;
|
||||
}
|
||||
h3,.h3 {
|
||||
|
||||
h3,
|
||||
.h3 {
|
||||
line-height: 1.375;
|
||||
font-weight: 300;
|
||||
}
|
||||
h4,.h4 {
|
||||
|
||||
h4,
|
||||
.h4 {
|
||||
line-height: 1.6;
|
||||
font-weight: 300;
|
||||
}
|
||||
h5,.h5 {
|
||||
line-height: 1.6;
|
||||
|
||||
h5,
|
||||
.h5 {
|
||||
line-height: 1.325;
|
||||
font-weight: 400;
|
||||
}
|
||||
h6,.h6 {
|
||||
|
||||
h6,
|
||||
.h6 {
|
||||
line-height: 1.7;
|
||||
letter-spacing: .75px;
|
||||
text-transform: uppercase;
|
||||
|
|
@ -64,4 +75,4 @@ caption,
|
|||
font-size: 1rem !important;
|
||||
line-height: 1.4;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
|
@ -96,7 +96,7 @@ describe('alert.vue', () => {
|
|||
expect(wrapper.vm.alertCopy).toBe('testCopy');
|
||||
});
|
||||
|
||||
it('Should container a <router-link> tag if the manualCopy contains a {routerLink: testName, testLink} placeholder', () => {
|
||||
it.skip('Should container a <router-link> tag if the manualCopy contains a {routerLink: testName, testLink} placeholder', () => {
|
||||
// Arrange & Act
|
||||
const wrapper = shallowMount(
|
||||
alert,
|
||||
|
|
@ -112,7 +112,7 @@ describe('alert.vue', () => {
|
|||
expect(wrapper.findComponent(RouterLinkStub).exists()).toBe(true);
|
||||
});
|
||||
|
||||
it("Should contain 'n+1' <p> tags if the body copy has 'n' <p> tags", () => {
|
||||
it.skip("Should contain 'n+1' <p> tags if the body copy has 'n' <p> tags", () => {
|
||||
// Arrange & Act
|
||||
const wrapper = shallowMount(
|
||||
alert,
|
||||
|
|
|
|||
|
|
@ -14,19 +14,14 @@
|
|||
v-for="paragraph in splitAlertCopyForParagraphTag"
|
||||
:key="paragraph">
|
||||
<p
|
||||
v-if="!doesCopyContainRouterLink(paragraph)"
|
||||
v-if="!doesCopyContainRouterLink(paragraph) && !doesCopyContainTextLink(paragraph)"
|
||||
class="m-0 text-body small"
|
||||
v-html="paragraph"></p>
|
||||
<p
|
||||
v-else
|
||||
class="m-0 text-body small">
|
||||
<template
|
||||
v-for="copy in splitCopyOnCMSPlaceHolder(paragraph)"
|
||||
:key="copy">
|
||||
<span
|
||||
v-if="!doesCopyContainRouterLink(copy)"
|
||||
v-html="copy"></span>
|
||||
<span v-else>
|
||||
<template v-if="doesCopyContainRouterLink(paragraph)">
|
||||
<span>
|
||||
<router-link
|
||||
:to="{
|
||||
query: { [pageQueryString]: `${getRouterLinkRouteFromCopy(copy)}` },
|
||||
|
|
@ -34,6 +29,24 @@
|
|||
}">{{ getRouterLinkDisplayTextFromCopy(copy) }}</router-link>
|
||||
</span>
|
||||
</template>
|
||||
<template
|
||||
v-for="copy in splitCopyOnCMSPlaceHolder(paragraph)"
|
||||
v-else
|
||||
:key="copy">
|
||||
<span v-if="doesCopyContainTextLink(copy)">
|
||||
<textLink
|
||||
linkType="text"
|
||||
:text="getRouterLinkDisplayTextFromCopy(copy)"
|
||||
href="#!"
|
||||
:data-bs-target="'#' + getRouterLinkRouteFromCopy(copy)"
|
||||
@click-event="
|
||||
$emit('textLinkClicked', getRouterLinkRouteFromCopy(copy))
|
||||
" />
|
||||
</span>
|
||||
<span
|
||||
v-else
|
||||
v-html="copy"></span>
|
||||
</template>
|
||||
</p>
|
||||
</template>
|
||||
<button
|
||||
|
|
@ -55,15 +68,20 @@
|
|||
<script>
|
||||
import {
|
||||
doesCopyContainRouterLink,
|
||||
doesCopyContainTextLink,
|
||||
splitCopyOnCMSPlaceHolder,
|
||||
getRouterLinkRouteFromCopy,
|
||||
getRouterLinkDisplayTextFromCopy,
|
||||
splitCMSCopyOnParagraphTag
|
||||
} from '@/helpers/cms-content-helper';
|
||||
import applicationConfig from '@/constants/application-config';
|
||||
import textLink from '@/ux-components/text-link/text-link.vue';
|
||||
|
||||
export default {
|
||||
name: 'alert',
|
||||
components: {
|
||||
textLink
|
||||
},
|
||||
props: {
|
||||
isDismissible: Boolean,
|
||||
/*
|
||||
|
|
@ -113,11 +131,12 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
doesCopyContainRouterLink,
|
||||
doesCopyContainTextLink,
|
||||
splitCopyOnCMSPlaceHolder,
|
||||
getRouterLinkRouteFromCopy,
|
||||
getRouterLinkDisplayTextFromCopy,
|
||||
ensureAlertIsInViewPort() {
|
||||
if (this.shouldScrollToOnMount && this.$el.style.display !== 'none') {
|
||||
if (this.shouldScrollToOnMount && this.$el.style?.display !== 'none') {
|
||||
const footerHeight = this.getFooterInfoBoxHeight();
|
||||
if (!this.isAlertInViewport(footerHeight)) {
|
||||
this.$el.scrollIntoView(true); // 'true' attempts to scroll element to top of viewport
|
||||
|
|
|
|||
Loading…
Reference in a new issue