Merge pull request #1060 from Safelite/feature/INSR-7771
INSR-7771 | TPA Search Parity
This commit is contained in:
commit
9b772d33a5
11 changed files with 1165 additions and 1771 deletions
|
|
@ -93,6 +93,10 @@ const endpoints = Object.freeze({
|
|||
) => `${LOCATION_BASE_URL}/providers/${zipCode}/${damageType}/${radius}/${parentAccountNumber}/${safeliteOnly}/${carId}/${recalPartNumber != null ? `${recalPartNumber}/` : ''}${isBigTruck}`,
|
||||
method: 'GET'
|
||||
},
|
||||
GetTpaAndSafeliteProviders: {
|
||||
url: `${LOCATION_BASE_URL}/tpa-and-safelite-providers`,
|
||||
method: 'GET'
|
||||
},
|
||||
GetCapabilityQuestions: {
|
||||
url: `${PARTS_BASE_URL}/capability-questions`,
|
||||
method: 'GET'
|
||||
|
|
|
|||
|
|
@ -67,7 +67,10 @@ const errorMessages = Object.freeze({
|
|||
DATE_REQUIRED: 'Please select a date',
|
||||
TIME_REQUIRED: 'Please select an appointment time.',
|
||||
PRIMARY_PHONE_REQUIRED: 'Primary phone number is required.',
|
||||
BAILOUT_EMAIL_ADDRESS_REQUIRED: 'Email address is required.'
|
||||
BAILOUT_EMAIL_ADDRESS_REQUIRED: 'Email address is required.',
|
||||
TPA_SEARCH_ZIP_FORMAT: 'Please enter a valid Zip code.',
|
||||
TPA_SEARCH_SHOP_FORMAT: 'Please enter a valid shop name.',
|
||||
TPA_SEARCH_REQUIRED: 'Please enter a shop name or ZIP code.'
|
||||
});
|
||||
|
||||
export default errorMessages;
|
||||
|
|
|
|||
|
|
@ -28,7 +28,8 @@ const globalRules = Object.freeze({
|
|||
ZIP_CODE_SEARCH_FORMAT: 'zip-code-search-format',
|
||||
OPTION_REQUIRED: 'option-required',
|
||||
PRIMARY_PHONE_REQUIRED: 'primary-phone-required',
|
||||
BAILOUT_EMAIL_ADDRESS_REQUIRED: 'bailout-email-address-required'
|
||||
BAILOUT_EMAIL_ADDRESS_REQUIRED: 'bailout-email-address-required',
|
||||
TPA_SEARCH_FORMAT: 'tpa-search-format'
|
||||
});
|
||||
|
||||
export default globalRules;
|
||||
|
|
|
|||
|
|
@ -310,22 +310,31 @@ input::-webkit-date-and-time-value {
|
|||
.input-wrapper {
|
||||
position: relative;
|
||||
&.has-search-icon {
|
||||
display:flex;
|
||||
.form-control {
|
||||
&:focus, &:focus-within {
|
||||
border: 1px solid #0070d1;
|
||||
}
|
||||
max-height: 3rem;
|
||||
}
|
||||
input[type='text'] {
|
||||
border-radius: $border-radius;
|
||||
border-color: #0070d1;
|
||||
box-shadow: rgba(0, 0, 0, 0.2) 0rem 0.0625rem .25rem 0rem;
|
||||
border-right: none;
|
||||
border-radius: 4.5rem 0 0 4.5rem;
|
||||
height: 3rem;
|
||||
&:focus, &:focus-within {
|
||||
border-right:none;
|
||||
}
|
||||
}
|
||||
button[type='submit'] {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
right: 0;
|
||||
background-image: url($svg-search-icon);
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
border-radius: 0 $border-radius-lg $border-radius-lg 0;
|
||||
border-radius: 0 4.5rem 4.5rem 0;
|
||||
background-color: $blue-100;
|
||||
width: 2.75rem;
|
||||
height: 100%;
|
||||
border: 1px solid $gray-500;
|
||||
border: 1px solid #0070d1;
|
||||
border-left: none;
|
||||
display: flex;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -125,6 +125,28 @@ function defineGlobalZipCodeRules() {
|
|||
defineRule(globalRules.ZIP_CODE_SEARCH_FORMAT, regex(/^\d{5}$/, errorMessages.ZIP_FORMAT));
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Define global rules related to the zip code/shop name search on TPA Search page
|
||||
*/
|
||||
function defineGlobalTpaSearchRules() {
|
||||
defineRule(globalRules.TPA_SEARCH_FORMAT, (value) => {
|
||||
if (value.length === 0) {
|
||||
return errorMessages.TPA_SEARCH_REQUIRED;
|
||||
}
|
||||
// Non Numeric
|
||||
if (isNaN(value)) {
|
||||
if (value.length < 3) {
|
||||
return errorMessages.TPA_SEARCH_SHOP_FORMAT;
|
||||
}
|
||||
} else {
|
||||
if (value.length != 5) {
|
||||
return errorMessages.TPA_SEARCH_ZIP_FORMAT;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @function defineGlobalRules
|
||||
* @summary Define all global rules
|
||||
|
|
@ -138,6 +160,6 @@ export default function defineGlobalRules() {
|
|||
defineGlobalPolicyNumberRules();
|
||||
defineGlobalPolicyZipCodeRules();
|
||||
defineGlobalZipCodeRules();
|
||||
|
||||
defineGlobalTpaSearchRules();
|
||||
defineRule(globalRules.OPTION_REQUIRED, required(errorMessages.OPTION_REQUIRED));
|
||||
}
|
||||
|
|
|
|||
119
src/iss-components/tpa-shop-list-button/tpa-shop-list-button.vue
Normal file
119
src/iss-components/tpa-shop-list-button/tpa-shop-list-button.vue
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
<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 label-margin-bottom no-hover">
|
||||
<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
|
||||
id="buttonLabelSpan"
|
||||
class="m-0 button-label-copy"
|
||||
:class="textPosition">{{ buttonLabel }}
|
||||
</span>
|
||||
<span
|
||||
id="buttonLabelSubCopySpan"
|
||||
class="m-0 caption ms-2"
|
||||
:class="textPosition">{{ buttonLabelSubCopy }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="row-two">
|
||||
<span
|
||||
v-if="buttonBodyCopy"
|
||||
id="buttonBodyCopy"
|
||||
class="m-0 button-label-sub-copy"
|
||||
v-html="buttonBodyCopy"></span>
|
||||
</div>
|
||||
<span
|
||||
v-if="screenReaderOnlyText"
|
||||
id="screenReaderOnlyTextSpan"
|
||||
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';
|
||||
import loader from '@/ux-components/loader/loader.vue';
|
||||
|
||||
export default {
|
||||
name: 'tpa-shop-list-button',
|
||||
components: {
|
||||
baseInputButton,
|
||||
loader
|
||||
},
|
||||
mixins: [inputButtonWrapperMixin],
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "@/styles/ux-variables-svg-strings.scss";
|
||||
|
||||
.list-button {
|
||||
outline: none;
|
||||
input[type="radio"],
|
||||
input[type="checkbox"] {
|
||||
position: static; //override bootstrap
|
||||
|
||||
&:checked + .list-button-content {
|
||||
background: #e7f1f6;
|
||||
border: 1px solid #0070d1;
|
||||
}
|
||||
}
|
||||
}
|
||||
.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;
|
||||
color: #4d4e53;
|
||||
font-size: .9375rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.625rem;
|
||||
|
||||
.row-one {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: 1.5rem;
|
||||
|
||||
.button-label-copy {
|
||||
font-weight: 600;
|
||||
line-height: 1.5rem;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
#buttonLabelSubCopySpan {
|
||||
font-size: .75rem;
|
||||
line-height: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.row-two {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
div.col:not(:last-child) >.label-margin-bottom{
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,214 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`TPA search page returns the initial data 1`] = `
|
||||
Object {
|
||||
"additionalButtonData": Object {
|
||||
"displayAvailabilityIndicators": false,
|
||||
},
|
||||
"dataLoaded": false,
|
||||
"filter": "",
|
||||
"mapZipCode": null,
|
||||
"providers": Array [],
|
||||
"reloadingProviders": false,
|
||||
"rules": Object {
|
||||
"filter": "option-required",
|
||||
"provider": "option-required",
|
||||
"zipCode": "zip-code-required|zip-code-search-format",
|
||||
},
|
||||
"selectedProviderNumber": "",
|
||||
"shopListButton": Object {
|
||||
"beforeMount": [Function],
|
||||
"components": Object {
|
||||
"baseInputButton": Object {
|
||||
"computed": Object {
|
||||
"buttonId": [Function],
|
||||
"eventTypes": [Function],
|
||||
"inputType": [Function],
|
||||
"isChecked": [Function],
|
||||
"isValueSelectedOnClick": [Function],
|
||||
},
|
||||
"data": [Function],
|
||||
"emits": Array [
|
||||
"update:modelValue",
|
||||
],
|
||||
"methods": Object {
|
||||
"handleBlur": [Function],
|
||||
"handleClick": [Function],
|
||||
"handleEventAction": [Function],
|
||||
"handleFocus": [Function],
|
||||
"handleSelectionChange": [Function],
|
||||
},
|
||||
"mounted": [Function],
|
||||
"name": "base-input-button",
|
||||
"props": Object {
|
||||
"buttonWrapperClasses": Array [
|
||||
[Function],
|
||||
[Function],
|
||||
[Function],
|
||||
],
|
||||
"groupName": Object {
|
||||
"required": true,
|
||||
"type": [Function],
|
||||
},
|
||||
"inputClasses": Array [
|
||||
[Function],
|
||||
[Function],
|
||||
[Function],
|
||||
],
|
||||
"isMultiSelect": [Function],
|
||||
"isRequired": Object {
|
||||
"default": true,
|
||||
"type": [Function],
|
||||
},
|
||||
"lastValuePushedToGa": Array [
|
||||
[Function],
|
||||
[Function],
|
||||
],
|
||||
"modelValue": Object {
|
||||
"required": true,
|
||||
"validator": [Function],
|
||||
},
|
||||
"selectingInitiatesLoad": Object {
|
||||
"default": false,
|
||||
"type": [Function],
|
||||
},
|
||||
"setLastValuePushedToGa": [Function],
|
||||
"suppressError": [Function],
|
||||
"validationRules": Object {
|
||||
"default": "",
|
||||
"type": [Function],
|
||||
},
|
||||
"value": Object {
|
||||
"required": true,
|
||||
"type": Array [
|
||||
[Function],
|
||||
[Function],
|
||||
],
|
||||
},
|
||||
"valueToLogType": [Function],
|
||||
},
|
||||
"render": [Function],
|
||||
"setup": [Function],
|
||||
},
|
||||
"loader": Object {
|
||||
"computed": Object {
|
||||
"cssProps": [Function],
|
||||
},
|
||||
"methods": Object {
|
||||
"captureClick": [Function],
|
||||
},
|
||||
"name": "loader",
|
||||
"props": Object {
|
||||
"allowPageInteraction": Object {
|
||||
"default": false
|
||||
"type": [Function],
|
||||
},
|
||||
"height": Object {
|
||||
"default": 1,
|
||||
"type": [Function],
|
||||
},
|
||||
"loaderColor": Object {
|
||||
"type": [Function],
|
||||
},
|
||||
"loaderPosition": Object {
|
||||
"type": [Function],
|
||||
},
|
||||
"width": Object {
|
||||
"default": 1,
|
||||
"type": [Function],
|
||||
},
|
||||
},
|
||||
"render": [Function],
|
||||
},
|
||||
},
|
||||
"computed": Object {
|
||||
"availabilityRatingClass": [Function],
|
||||
"badgeText": [Function],
|
||||
"isLoaderDisplayed": [Function],
|
||||
},
|
||||
"data": [Function],
|
||||
"mixins": Array [
|
||||
Object {
|
||||
"computed": Object {
|
||||
"selectedValue": Object {
|
||||
"get": [Function],
|
||||
"set": [Function],
|
||||
},
|
||||
},
|
||||
"model": Object {
|
||||
"event": "change",
|
||||
"prop": "modelValue",
|
||||
},
|
||||
"props": Object {
|
||||
"additionalButtonData": [Function],
|
||||
"additionalButtonStyling": [Function],
|
||||
"altText": Object {
|
||||
"default": "",
|
||||
"type": [Function],
|
||||
},
|
||||
"buttonAuxiliaryCopy": [Function],
|
||||
"buttonBodyCopy": [Function],
|
||||
"buttonFooterCopy": [Function],
|
||||
"buttonImage": [Function],
|
||||
"buttonImageId": [Function],
|
||||
"buttonLabel": Array [
|
||||
[Function],
|
||||
[Function],
|
||||
],
|
||||
"buttonLabelSubCopy": [Function],
|
||||
"groupName": Object {
|
||||
"required": true,
|
||||
"type": [Function],
|
||||
},
|
||||
"isMultiSelect": [Function],
|
||||
"isRequired": Object {
|
||||
"default": true,
|
||||
"type": [Function],
|
||||
},
|
||||
"isWide": [Function],
|
||||
"lastValuePushedToGa": Array [
|
||||
[Function],
|
||||
[Function],
|
||||
],
|
||||
"modelValue": Object {
|
||||
"required": true,
|
||||
"validator": [Function],
|
||||
},
|
||||
"screenReaderOnlyText": [Function],
|
||||
"selectingInitiatesLoad": Object {
|
||||
"default": false,
|
||||
"type": [Function],
|
||||
},
|
||||
"setLastValuePushedToGa": [Function],
|
||||
"suppressError": [Function],
|
||||
"textPosition": [Function],
|
||||
"validationRules": Object {
|
||||
"default": "",
|
||||
"type": [Function],
|
||||
},
|
||||
"value": Object {
|
||||
"required": true,
|
||||
"type": Array [
|
||||
[Function],
|
||||
[Function],
|
||||
],
|
||||
},
|
||||
"valueToLogType": [Function],
|
||||
},
|
||||
},
|
||||
],
|
||||
"name": "shop-list-button",
|
||||
"render": [Function],
|
||||
},
|
||||
"widget": Object {
|
||||
"filterByQuestion": "FilterByQuestion",
|
||||
"noNetworkShopsAlert": "NoNetworkShopsAlertWidget",
|
||||
"searchInstructions": "SearchInstructions",
|
||||
"shopNotListedLink": "ShopNotListedLink",
|
||||
"siteFooter": "SiteFooterWidget",
|
||||
"siteHeader": "SiteHeaderWidget",
|
||||
"tpaSearchQuestion": "TPASearchQuestion",
|
||||
},
|
||||
"zipCode": null,
|
||||
}
|
||||
`;
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -15,26 +15,26 @@
|
|||
<label
|
||||
id="tpaSearchQuestionLabel"
|
||||
for="tpaSearchQuestionField"
|
||||
class="text-center fs-5 mt-5 mb-0 text-black w-100">
|
||||
class="fs-5 mt-5 text-black w-100">
|
||||
{{ tpaSearchQuestionLabel }}
|
||||
</label>
|
||||
<label
|
||||
id="searchInstructions"
|
||||
for="tpaSearchQuestionField"
|
||||
class="text-center small darker-gray w-100 mb-4">
|
||||
class="darker-gray w-100 mb-4">
|
||||
{{ searchInstructionsText }}
|
||||
</label>
|
||||
<div class="mb-5">
|
||||
<textboxQuestion
|
||||
id="tpaSearchQuestionField"
|
||||
v-model="zipCode"
|
||||
v-model="tpaSearchValue"
|
||||
inputId="tpaSearchQuestionFieldInput"
|
||||
:cmsWidgetName="widget.tpaSearchQuestion"
|
||||
:includeSearchIcon="true"
|
||||
:displayQuestionText="false"
|
||||
isRequired
|
||||
:isDisabled="reloadingProviders"
|
||||
:validationRules="rules.zipCode"
|
||||
:validationRules="rules.tpaSearch"
|
||||
@clickEvent="searchClick" />
|
||||
</div>
|
||||
</Form>
|
||||
|
|
@ -52,10 +52,10 @@
|
|||
@invalidSubmit="onInvalidSubmit">
|
||||
<dropdownQuestion
|
||||
id="searchRadiusFilter"
|
||||
v-model="filter"
|
||||
v-model="radiusFilter"
|
||||
:cmsWidgetName="widget.filterByQuestion"
|
||||
inputId="searchRadiusFilterInput"
|
||||
:options="filterOptions"
|
||||
:options="radiusFilterOptions"
|
||||
disableAutoFill
|
||||
:isDisabled="reloadingProviders"
|
||||
:validationRules="rules.filter" />
|
||||
|
|
@ -73,7 +73,7 @@
|
|||
id="selectProviderQuestion"
|
||||
v-model="selectedProviderNumber"
|
||||
buttonTypeString="shopListButton"
|
||||
:buttonTypeObject="shopListButton"
|
||||
:buttonTypeObject="tpaShopListButton"
|
||||
class="radioQuestion"
|
||||
:answers="providerButtonData"
|
||||
groupName="chooseShop"
|
||||
|
|
@ -99,12 +99,23 @@
|
|||
:text="shopNotListedModalLink"
|
||||
@clickEvent="doNotSeeMyShopLinkClick" />
|
||||
</div>
|
||||
<hr>
|
||||
<siteFooter
|
||||
class="mt-6"
|
||||
ref="siteFooter"
|
||||
:cmsWidgetName="widget.siteFooter"
|
||||
:isForwardActionDisabled="!meta.valid"
|
||||
@ForwardClicked="forwardButtonAction"
|
||||
@backClicked="navigateBack" />
|
||||
<div class="need-help-link-container">
|
||||
<textLink
|
||||
id="needHelpLink"
|
||||
v-if="needHelpLinkText"
|
||||
linkType="navigation"
|
||||
:text="needHelpLinkText"
|
||||
href="#"
|
||||
@clickEvent="needHelpLinkClick" />
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
|
|
@ -123,7 +134,7 @@ import textLink from '@/ux-components/text-link/text-link.vue';
|
|||
import alert from '@/ux-components/alert/alert.vue';
|
||||
import siteFooter from '@/iss-components/site-footer/site-footer.vue';
|
||||
import googleMap from '@/iss-components/google-map/google-map.vue';
|
||||
import shopListButton from '@/iss-components/shop-list-button/shop-list-button.vue';
|
||||
import tpaShopListButton from '@/iss-components/tpa-shop-list-button/tpa-shop-list-button.vue';
|
||||
import loader from '@/ux-components/loader/loader.vue';
|
||||
|
||||
// Supporting files
|
||||
|
|
@ -135,53 +146,9 @@ import widgetFields from '@/constants/cms-widget-fields.js';
|
|||
import { shallowRef } from 'vue';
|
||||
import { toTitleCase, toDisplayPhoneNumber } from '@/helpers/text-helper.js';
|
||||
import bailoutMessage from '@/constants/bailoutMessage';
|
||||
import settleAllPromises from '@/helpers/layout-helper';
|
||||
import issPageValues from '@/router/router-constants/issPage-values';
|
||||
|
||||
const radiusFilterPairs = [
|
||||
{ radius: 15, filter: '15 miles' },
|
||||
{ radius: 25, filter: '25 miles' },
|
||||
{ radius: 50, filter: '50 miles' },
|
||||
{ radius: 100, filter: '100 miles' }
|
||||
];
|
||||
|
||||
function convertRadiusFilterToInteger(filter) {
|
||||
const pair = radiusFilterPairs.find((p) => p.filter === filter);
|
||||
return pair?.radius ?? 0;
|
||||
}
|
||||
|
||||
async function getInitialSearchData() {
|
||||
const customerZipCode = useMainStore().order.customer.address.zipCode;
|
||||
const serviceLocationZipCode = useMainStore().order.serviceLocation.zipCode;
|
||||
const zipCode = serviceLocationZipCode ?? customerZipCode;
|
||||
|
||||
const { searchFilter } = useMainStore().order.serviceLocation;
|
||||
let { providerNumber } = useMainStore().order.serviceLocation.provider;
|
||||
|
||||
let providers = [];
|
||||
let radius = 0;
|
||||
if (searchFilter) {
|
||||
radius = convertRadiusFilterToInteger(searchFilter);
|
||||
const result = await useMainStore().getTpaProviders(zipCode, radius);
|
||||
providers = result?.data?.shopProviders ?? [];
|
||||
|
||||
return { zipCode, filter: searchFilter, providers, providerNumber };
|
||||
}
|
||||
|
||||
providerNumber = null;
|
||||
let pairIndex = 0;
|
||||
let filter = '';
|
||||
while (providers.length === 0 && pairIndex < radiusFilterPairs.length) {
|
||||
const pair = radiusFilterPairs[pairIndex];
|
||||
radius = pair.radius;
|
||||
filter = pair.filter;
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const result = await useMainStore().getTpaProviders(zipCode, radius);
|
||||
providers = result?.data?.shopProviders ?? [];
|
||||
|
||||
pairIndex += 1;
|
||||
}
|
||||
|
||||
return { zipCode, filter, providers, providerNumber };
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'tpa-search',
|
||||
|
|
@ -200,23 +167,40 @@ export default {
|
|||
},
|
||||
mixins: [BaseFormMixin],
|
||||
async beforeRouteEnter(to, from, next) {
|
||||
const { zipCode, filter, providers, providerNumber } =
|
||||
await getInitialSearchData();
|
||||
const cmsContent = await fetchCmsContentForPage(to.query.issPage);
|
||||
|
||||
const zipCode = useMainStore().order.customer.address.zipCode;
|
||||
const pageData = useMainStore().pageData(to.query.issPage);
|
||||
const cmsContentPromise = fetchCmsContentForPage(to.query.issPage);
|
||||
const providersPromise = useMainStore().getTpaAndSafeliteProviders(zipCode, pageData?.tpaSearchValue ?? '');
|
||||
const promiseResultMap = [
|
||||
{
|
||||
resultKey: 'cmsContent',
|
||||
promise: cmsContentPromise
|
||||
},
|
||||
{
|
||||
resultKey: 'providers',
|
||||
promise: providersPromise
|
||||
}
|
||||
];
|
||||
const resultMap = await settleAllPromises(promiseResultMap);
|
||||
next(async (vm) => {
|
||||
vm.setCmsContent(cmsContent);
|
||||
vm.setInitialSearchData(zipCode, filter, providers, providerNumber);
|
||||
vm.setCmsContent(resultMap.cmsContent);
|
||||
vm.mapZipCode = zipCode;
|
||||
vm.providers = resultMap.providers;
|
||||
if (pageData) {
|
||||
vm.tpaSearchValue = pageData?.tpaSearchValue ?? vm.mapZipCode;
|
||||
vm.radiusFilter = pageData?.radiusFilter ?? 25;
|
||||
}
|
||||
});
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dataLoaded: false,
|
||||
zipCode: null,
|
||||
pageData: null,
|
||||
tpaSearchValue: null,
|
||||
mapZipCode: null,
|
||||
filter: '',
|
||||
radiusFilter: null,
|
||||
providers: [],
|
||||
selectedProviderNumber: '',
|
||||
selectedProviderNumber: useMainStore().order.serviceLocation.provider.providerNumber,
|
||||
reloadingProviders: false,
|
||||
additionalButtonData: {
|
||||
displayAvailabilityIndicators: false
|
||||
|
|
@ -228,28 +212,28 @@ export default {
|
|||
filterByQuestion: 'FilterByQuestion',
|
||||
noNetworkShopsAlert: 'NoNetworkShopsAlertWidget',
|
||||
shopNotListedLink: 'ShopNotListedLink',
|
||||
siteFooter: 'SiteFooterWidget'
|
||||
siteFooter: 'SiteFooterWidget',
|
||||
needHelpLink: 'NeedHelpLink'
|
||||
},
|
||||
rules: {
|
||||
zipCode: `${globalRules.ZIP_CODE_REQUIRED}|${globalRules.ZIP_CODE_SEARCH_FORMAT}`,
|
||||
tpaSearch: globalRules.TPA_SEARCH_FORMAT,
|
||||
filter: globalRules.OPTION_REQUIRED, // TODO do we even need this?
|
||||
provider: globalRules.OPTION_REQUIRED
|
||||
},
|
||||
shopListButton: shallowRef(shopListButton)
|
||||
tpaShopListButton: shallowRef(tpaShopListButton)
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
filterOptions() {
|
||||
const filterByAnswers =
|
||||
this.getCmsContent(
|
||||
radiusFilterOptions() {
|
||||
const radiusFilterOptions = this.getCmsContent(
|
||||
this.widget.filterByQuestion,
|
||||
widgetFields.INPUT_QUESTION_WIDGET.ANSWERS
|
||||
) ?? [];
|
||||
const filterByAnswersObj = {};
|
||||
[...filterByAnswers].forEach((answer) => {
|
||||
filterByAnswersObj[answer.Name] = answer.Name;
|
||||
});
|
||||
return filterByAnswersObj;
|
||||
const options = {};
|
||||
for (const option of radiusFilterOptions) {
|
||||
options[option.Name] = option.Text;
|
||||
}
|
||||
return options;
|
||||
},
|
||||
tpaSearchQuestionLabel() {
|
||||
return this.getCmsContent(
|
||||
|
|
@ -257,6 +241,9 @@ export default {
|
|||
widgetFields.INPUT_QUESTION_WIDGET.QUESTION_TEXT
|
||||
);
|
||||
},
|
||||
needHelpLinkText() {
|
||||
return this.getCmsContent(this.widget.needHelpLink, widgetFields.TEXT_BLOCK_WIDGET.TEXT);
|
||||
},
|
||||
searchInstructionsText() {
|
||||
return this.getCmsContent(
|
||||
this.widget.searchInstructions,
|
||||
|
|
@ -269,25 +256,25 @@ export default {
|
|||
widgetFields.TEXT_BLOCK_WIDGET.TEXT
|
||||
);
|
||||
},
|
||||
radiusInMiles() {
|
||||
return convertRadiusFilterToInteger(this.filter);
|
||||
},
|
||||
noNetworkShopsAlertHeaderText() {
|
||||
return this.getCmsContent(
|
||||
this.widget.noNetworkShopsAlert,
|
||||
widgetFields.ALERT_WIDGET.HEADLINE_TEXT
|
||||
)?.replaceAll('{custom:radiusInMiles}', this.radiusInMiles);
|
||||
)?.replaceAll('{custom:radiusInMiles}', this.radiusFilter + " miles");
|
||||
},
|
||||
radiusFilteredAndCappedProviders() {
|
||||
return this.providers.filter((provider) => provider.distanceInMiles <= this.radiusFilter).slice(0, 5);
|
||||
},
|
||||
providerAddresses() {
|
||||
return this.providers?.map((provider) => ({
|
||||
title: provider.companyName,
|
||||
return this.radiusFilteredAndCappedProviders?.map((provider) => ({
|
||||
title: provider.companyName.toLowerCase(),
|
||||
fullAddress: this.getFullProviderAddress(provider),
|
||||
addressLines: [this.getProviderAddress(provider), this.getProviderCityZipState(provider)]
|
||||
})) ?? [];
|
||||
},
|
||||
providerButtonData() {
|
||||
return (
|
||||
this.providers?.map((provider) =>
|
||||
this.radiusFilteredAndCappedProviders?.map((provider) =>
|
||||
this.getShopButtonDataFromProvider(provider)) ?? []
|
||||
);
|
||||
},
|
||||
|
|
@ -298,14 +285,6 @@ export default {
|
|||
}
|
||||
},
|
||||
watch: {
|
||||
async filter() {
|
||||
if (this.dataLoaded) {
|
||||
await this.getProviderButtonData().then((providers) => {
|
||||
this.providers = providers;
|
||||
});
|
||||
this.mapZipCode = this.zipCode;
|
||||
}
|
||||
},
|
||||
providers(newProviders) {
|
||||
if (this.dataLoaded) {
|
||||
this.selectedProviderNumber =
|
||||
|
|
@ -318,7 +297,6 @@ export default {
|
|||
const provider = this.providers?.find((p) => p.providerNumber === newNumber);
|
||||
if (provider && this.dataLoaded) {
|
||||
useMainStore().updateServiceLocation({
|
||||
searchFilter: this.filter,
|
||||
zipCode: this.zipCode,
|
||||
provider: {
|
||||
providerNumber: provider?.providerNumber,
|
||||
|
|
@ -342,13 +320,6 @@ export default {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
setInitialSearchData(zipCode, filter, providers, providerNumber) {
|
||||
this.zipCode = zipCode;
|
||||
this.mapZipCode = zipCode;
|
||||
this.filter = filter;
|
||||
this.providers = providers;
|
||||
this.selectedProviderNumber = providerNumber;
|
||||
},
|
||||
getFullProviderAddress(provider) {
|
||||
const addressLine1 = this.getProviderAddress(provider);
|
||||
const addressLine2 = this.getProviderCityZipState(provider);
|
||||
|
|
@ -379,16 +350,16 @@ export default {
|
|||
}
|
||||
return addressLine2;
|
||||
},
|
||||
async getProviderButtonData() {
|
||||
async getProviderButtonData(shopName = "") {
|
||||
this.reloadingProviders = true;
|
||||
const getTpaProvidersResult = await useMainStore().getTpaProviders(
|
||||
this.zipCode,
|
||||
this.radiusInMiles
|
||||
const getTpaProvidersResult = await useMainStore().getTpaAndSafeliteProviders(
|
||||
this.mapZipCode,
|
||||
shopName
|
||||
);
|
||||
this.reloadingProviders = false;
|
||||
return getTpaProvidersResult?.data?.shopProviders ?? [];
|
||||
return getTpaProvidersResult?.data ?? [];
|
||||
},
|
||||
doNotSeeMyShopLinkClick() {
|
||||
needHelpLinkClick() {
|
||||
this.mainStore.setBailout(bailoutMessage.RequestCallback());
|
||||
this.$router.navigate(
|
||||
this.navigationScenarios.CLICKED_NEED_HELP,
|
||||
|
|
@ -396,8 +367,12 @@ export default {
|
|||
);
|
||||
},
|
||||
async searchClick() {
|
||||
this.providers = await this.getProviderButtonData();
|
||||
this.mapZipCode = this.zipCode;
|
||||
if (isNaN(this.tpaSearchValue)) {
|
||||
this.providers = await this.getProviderButtonData(this.tpaSearchValue);
|
||||
} else {
|
||||
this.mapZipCode = this.tpaSearchValue;
|
||||
this.providers = await this.getProviderButtonData();
|
||||
}
|
||||
},
|
||||
forwardButtonAction() {
|
||||
if (this.selectedProviderIsSafeliteShop) {
|
||||
|
|
@ -407,16 +382,12 @@ export default {
|
|||
? this.navigationScenarios.CLICKED_FORWARD_WITH_SAFELITE_SHOP
|
||||
: this.navigationScenarios
|
||||
.CLICKED_FORWARD_WITH_NON_SAFELITE_SHOP;
|
||||
// Do not save the zip code as the tpa search value, prevents an outdated zipcode from being used
|
||||
// as the default search value if customer goes back and changes zip earlier in the flow
|
||||
const tpaSearchValueToSave = this.tpaSearchValue === this.mapZipCode ? '' : this.tpaSearchValue;
|
||||
this.savePageDataToStore(issPageValues.TPA_SEARCH, {tpaSearchValue: tpaSearchValueToSave, radiusFilter: this.radiusFilter});
|
||||
this.$router.navigate(scenario, this.$route);
|
||||
},
|
||||
getCustomValueFromString(str) {
|
||||
switch (str) {
|
||||
case 'radiusInMiles':
|
||||
return this.radiusInMiles;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
},
|
||||
getShopButtonDataFromProvider(provider) {
|
||||
const cellNumber = toDisplayPhoneNumber(provider?.phoneNumber);
|
||||
const distance =
|
||||
|
|
@ -426,7 +397,7 @@ export default {
|
|||
: null;
|
||||
|
||||
return {
|
||||
buttonLabel: provider?.companyName ?? '',
|
||||
buttonLabel: provider?.companyName.toLowerCase() ?? '',
|
||||
buttonLabelSubCopy: distance === null ? '' : `${distance} mi`,
|
||||
buttonBodyCopy: `${this.getFullProviderAddress(provider)}<br>${
|
||||
cellNumber ?? ''
|
||||
|
|
@ -442,4 +413,30 @@ export default {
|
|||
.darker-gray {
|
||||
color: map-get($colors, 'darker-gray');
|
||||
}
|
||||
|
||||
#tpaSearchQuestionLabel {
|
||||
margin-bottom: .625rem;
|
||||
}
|
||||
|
||||
#map {
|
||||
height: 27.1875rem;
|
||||
}
|
||||
|
||||
#providerSelectionForm {
|
||||
#alertNoNetworkProviders {
|
||||
:deep(.alert) {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 1.375rem 0;
|
||||
}
|
||||
|
||||
.need-help-link-container {
|
||||
margin-top:1.875rem;
|
||||
margin-bottom: 2.1875rem;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -142,7 +142,7 @@ export const getDefaultState = () => ({
|
|||
appointmentType: null,
|
||||
isVehicleProtected: null,
|
||||
IsSafeliteProvider: null,
|
||||
searchFilter: null,
|
||||
tpaSearchRadius: null,
|
||||
provider: {
|
||||
providerNumber: null,
|
||||
address: {
|
||||
|
|
@ -1038,8 +1038,24 @@ export const useMainStore = defineStore({
|
|||
return this.getProviders(zipCode, radius, true)
|
||||
},
|
||||
|
||||
getTpaProviders(zipCode, radius) {
|
||||
return this.getProviders(zipCode, radius, false)
|
||||
getTpaAndSafeliteProviders(zipCode, provderNameSearchString = "") {
|
||||
const { parentAccountNumber } = this.order;
|
||||
const carId = this.order.vehicle.carId;
|
||||
const damageType = this.damage.isRepair ? 'Repair' : 'Install';
|
||||
|
||||
const parts = getLineItemsFlattened(this.order.lineItems.glassParts);
|
||||
const partNumbers = parts.map((part) => part.partNumber).join(',');
|
||||
|
||||
let url = `${endpoints.GetTpaAndSafeliteProviders.url}/${zipCode}?accountNumber=${parentAccountNumber}&carId=${carId}&damageType=${damageType}&partNumbers=${partNumbers}`;
|
||||
if (provderNameSearchString) {
|
||||
url += `&providerNameSearchString=${provderNameSearchString}`;
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
globalMethods.callHttpClient({
|
||||
method: endpoints.GetTpaAndSafeliteProviders.method,
|
||||
endpoint: url
|
||||
}).then((response) => resolve(response), (error) => reject(error));
|
||||
});
|
||||
},
|
||||
|
||||
getProviders(zipcode, radius, safeliteOnly) {
|
||||
|
|
@ -1844,7 +1860,7 @@ export const useMainStore = defineStore({
|
|||
phoneNumber: serviceLocationInfo.provider?.phoneNumber
|
||||
};
|
||||
}
|
||||
this.order.serviceLocation.searchFilter = serviceLocationInfo.searchFilter;
|
||||
this.order.serviceLocation.tpaSearchRadius = serviceLocationInfo.tpaSearchRadius;
|
||||
},
|
||||
|
||||
resetState() {
|
||||
|
|
|
|||
|
|
@ -1966,53 +1966,65 @@ describe('Store', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('getTpaProviders method', () => {
|
||||
describe('getTpaAndSafeliteProviders method', () => {
|
||||
describe('successful method call', () => {
|
||||
it.each([[true, 'Repair'], [false, 'Replace']])(
|
||||
'calls getProviders api endpoint',
|
||||
it.each([[true, 'Repair'], [false, 'Install']])(
|
||||
'calls getProviders api endpoint with damageType %s',
|
||||
async (isRepair, damageType) => {
|
||||
// Arrange
|
||||
globalMethods.callHttpClient = jest.fn().mockReturnValue(Promise.resolve({}));
|
||||
|
||||
const carId = getRandomString(10, 14);
|
||||
const parentAccountNumber = getRandomString(6, 6);
|
||||
const partNumber = getRandomString(6, 6);
|
||||
store.order.parentAccountNumber = parentAccountNumber;
|
||||
store.order.damage.isRepair = isRepair;
|
||||
store.order.vehicle.carId = carId;
|
||||
const zipCode = getRandomString(6, 6);
|
||||
const radius = getRandomInt(5, 100);
|
||||
const isBigTruck = false;
|
||||
store.order.vehicle.isBigTruck = isBigTruck;
|
||||
const expectedEndpoint = endpoints.GetProviders.url(
|
||||
zipCode,
|
||||
damageType,
|
||||
radius,
|
||||
parentAccountNumber,
|
||||
'false',
|
||||
carId,
|
||||
null,
|
||||
isBigTruck
|
||||
);
|
||||
store.order.lineItems.glassParts = [{ partNumber }];
|
||||
const zipCode = getRandomString(5, 5);
|
||||
|
||||
const expectedEndpoint = `${endpoints.GetTpaAndSafeliteProviders.url}/${zipCode}?accountNumber=${parentAccountNumber}&carId=${carId}&damageType=${damageType}&partNumbers=${partNumber}`;
|
||||
|
||||
// Act
|
||||
await store.getTpaProviders(zipCode, radius);
|
||||
await store.getTpaAndSafeliteProviders(zipCode);
|
||||
|
||||
// Asserts
|
||||
expect(globalMethods.callHttpClient).toHaveBeenCalledWith(expect.objectContaining({
|
||||
method: endpoints.GetProviders.method,
|
||||
method: endpoints.GetTpaAndSafeliteProviders.method,
|
||||
endpoint: expectedEndpoint
|
||||
}));
|
||||
}
|
||||
);
|
||||
it('includes providerNameSearchString in endpoint when provided', async () => {
|
||||
// Arrange
|
||||
globalMethods.callHttpClient = jest.fn().mockReturnValue(Promise.resolve({}));
|
||||
|
||||
const carId = getRandomString(10, 14);
|
||||
const parentAccountNumber = getRandomString(6, 6);
|
||||
const partNumber = getRandomString(6, 6);
|
||||
store.order.parentAccountNumber = parentAccountNumber;
|
||||
store.order.damage.isRepair = true;
|
||||
store.order.vehicle.carId = carId;
|
||||
store.order.lineItems.glassParts = [{ partNumber }];
|
||||
const zipCode = getRandomString(5, 5);
|
||||
const providerNameSearchString = 'Test Shop';
|
||||
|
||||
// Act
|
||||
await store.getTpaAndSafeliteProviders(zipCode, providerNameSearchString);
|
||||
|
||||
// Asserts
|
||||
expect(globalMethods.callHttpClient).toHaveBeenCalledWith(expect.objectContaining({
|
||||
endpoint: expect.stringContaining(`&providerNameSearchString=${providerNameSearchString}`)
|
||||
}));
|
||||
});
|
||||
it('Returns expected response object', async () => {
|
||||
// Arrange
|
||||
const response = { ReferralNumber: getRandomString(6, 6) };
|
||||
globalMethods.callHttpClient = jest.fn().mockReturnValue(Promise.resolve(response));
|
||||
const zipCode = getRandomString(6, 6);
|
||||
const radius = getRandomInt(5, 100);
|
||||
const zipCode = getRandomString(5, 5);
|
||||
|
||||
// Act
|
||||
const result = store.getTpaProviders(zipCode, radius);
|
||||
const result = store.getTpaAndSafeliteProviders(zipCode);
|
||||
|
||||
// Asserts
|
||||
await expect(result).resolves.toBe(response);
|
||||
|
|
@ -2020,11 +2032,10 @@ describe('Store', () => {
|
|||
it('null response by api => no exceptions thrown', async () => {
|
||||
// Arrange
|
||||
globalMethods.callHttpClient = jest.fn().mockReturnValue(Promise.resolve(null));
|
||||
const zipCode = getRandomString(6, 6);
|
||||
const radius = getRandomInt(5, 100);
|
||||
const zipCode = getRandomString(5, 5);
|
||||
|
||||
// Act
|
||||
await store.getTpaProviders(zipCode, radius);
|
||||
await store.getTpaAndSafeliteProviders(zipCode);
|
||||
});
|
||||
it('no providers returned by api => promise resolves to empty list', async () => {
|
||||
// Arrange
|
||||
|
|
@ -2034,11 +2045,10 @@ describe('Store', () => {
|
|||
}
|
||||
};
|
||||
globalMethods.callHttpClient = jest.fn().mockReturnValue(Promise.resolve(responseNoProviders));
|
||||
const zipCode = getRandomString(6, 6);
|
||||
const radius = getRandomInt(5, 100);
|
||||
const zipCode = getRandomString(5, 5);
|
||||
|
||||
// Act
|
||||
const result = await store.getTpaProviders(zipCode, radius);
|
||||
const result = await store.getTpaAndSafeliteProviders(zipCode);
|
||||
|
||||
// Asserts
|
||||
expect(result.data.shopProviders).toHaveLength(0);
|
||||
|
|
@ -2060,7 +2070,7 @@ describe('Store', () => {
|
|||
globalMethods.callHttpClient = jest.fn().mockReturnValue(Promise.resolve(responseProviders));
|
||||
|
||||
// Act
|
||||
const result = await store.getTpaProviders();
|
||||
const result = await store.getTpaAndSafeliteProviders();
|
||||
|
||||
// Asserts
|
||||
expect(result.data.shopProviders).toHaveLength(2);
|
||||
|
|
|
|||
Loading…
Reference in a new issue