diff --git a/src/constants/endpoints.js b/src/constants/endpoints.js index a213e2f4..610b5273 100644 --- a/src/constants/endpoints.js +++ b/src/constants/endpoints.js @@ -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' diff --git a/src/constants/schedule-constants.js b/src/constants/schedule-constants.js new file mode 100644 index 00000000..eaa0d5fa --- /dev/null +++ b/src/constants/schedule-constants.js @@ -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 }; diff --git a/src/digital-components/modal/modal.vue b/src/digital-components/modal/modal.vue index 13c3eb91..4787a6c0 100644 --- a/src/digital-components/modal/modal.vue +++ b/src/digital-components/modal/modal.vue @@ -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 { diff --git a/src/helpers/cms-content-helper.js b/src/helpers/cms-content-helper.js index 861eacec..5120c4db 100644 --- a/src/helpers/cms-content-helper.js +++ b/src/helpers/cms-content-helper.js @@ -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 = `${splitParams[1]}`; @@ -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 = `${splitParams[1]}`; @@ -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: ? syntax stores the captured match like so: match.groups.variableName - const matchStartOfString - = '(?^.+?)' // Match and Capture all characters (lazy), cannot be empty + const matchStartOfString = + '(?^.+?)' // Match and Capture all characters (lazy), cannot be empty + '(?=(?:{if))'; // Looks ahead but does not capture {if - const matchIfOperator - = '(?{if:)' // Match & Capture {if: + const matchIfOperator = + '(?{if:)' // Match & Capture {if: + '(?.*?):' // Match all chars up to and including next ':' - Capture all chars up to ':' + '(?.*?)}' // Match all chars up to and including next '}' - Capture all chars up to '}' + '(?.*?)' // Match and Capture all characters (lazy), can be empty + `(?=${anyLogicOperatorNonCapture})`; // Looks ahead but does not capture the next logic operator - const matchElseOperator - = '(?{else})' // Match & Capture {else} + const matchElseOperator = + '(?{else})' // Match & Capture {else} + '(?.*?)' // Match & Capture all characters (lazy), can be empty + `(?=${anyLogicOperatorNonCapture})`; // Looks ahead but does not capture the next logic operator - const matchEndOperator - = '(?{end})' // Match & Capture {end} + const matchEndOperator = + '(?{end})' // Match & Capture {end} + '(?.*?)' // 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 diff --git a/src/helpers/object-helper.js b/src/helpers/object-helper.js new file mode 100644 index 00000000..5c1cafd6 --- /dev/null +++ b/src/helpers/object-helper.js @@ -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; +} diff --git a/src/helpers/service-location-helper.js b/src/helpers/service-location-helper.js index 242752d3..a8ac5503 100644 --- a/src/helpers/service-location-helper.js +++ b/src/helpers/service-location-helper.js @@ -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); +} diff --git a/src/iss-components/site-footer/site-footer.vue b/src/iss-components/site-footer/site-footer.vue index f83d8372..2c4a4cb0 100644 --- a/src/iss-components/site-footer/site-footer.vue +++ b/src/iss-components/site-footer/site-footer.vue @@ -2,7 +2,7 @@