514 lines
19 KiB
Vue
514 lines
19 KiB
Vue
<template>
|
|
<transition name="fade" mode="out-in">
|
|
<div class="shop-question" aria-live="polite">
|
|
<div class="modal-link">
|
|
<text-link
|
|
linkType="text"
|
|
:text="shopQuestionLinkText"
|
|
href="#!"
|
|
@click-event="openModal"></text-link>
|
|
</div>
|
|
</div>
|
|
</transition>
|
|
<modal
|
|
ref="shopQuestionModal"
|
|
class="shop-question-popup"
|
|
:headerText="modalHeaderText"
|
|
:onModalOpenedCallback="updateSelectedAnswer"
|
|
:onModalClosedCallback="resetZipCodeData"
|
|
:footerButtonText="modalFooterText"
|
|
:footerButtonDisabled="isModalFooterButtonDisabled"
|
|
@footer-button-event="setZipCodeAndShop">
|
|
<form @submit.prevent>
|
|
<serviceZipQuestion
|
|
ref="serviceZipQuestion"
|
|
class="shop-question-zipcode"
|
|
customInputId="serviceZipCode"
|
|
v-model="localZipCode"
|
|
cmsWidgetName="ServiceZipQuestionWidget"
|
|
@search-icon-click="onSearchZip"
|
|
@keydown.enter="onSearchZip"
|
|
includeSearchIcon
|
|
:isRequired="false"
|
|
:validationRules="''" />
|
|
</form>
|
|
<div v-if="isLoading" class="loader-wrapper">
|
|
<loader loaderColor="blue" loaderPosition="center" />
|
|
</div>
|
|
<div v-if="!isLoading">
|
|
<alert
|
|
ref="alertInvalidZip"
|
|
v-if="displayInvalidZipAlert"
|
|
class="mb-4"
|
|
cmsWidgetName="AlertInvalidZipWidget"
|
|
alertClass="alert-danger"
|
|
v-bind:isDismissible="false" />
|
|
<alert
|
|
ref="alertNoShops"
|
|
class="my-5"
|
|
cmsWidgetName="AlertNoShopsWidget"
|
|
v-if="displayNoShopsAlert"
|
|
alertClass="alert-warning" />
|
|
<alert
|
|
ref="alertNoShops"
|
|
class="my-5"
|
|
cmsWidgetName="AlertNoServiceWidget"
|
|
v-if="displayNoServiceAlert"
|
|
alertClass="alert-danger" />
|
|
<div v-if="displayShopsList">
|
|
<buttonQuestion
|
|
ref="buttonQuestion"
|
|
buttonTypeString="shopListButton"
|
|
:buttonTypeObject="shopListButton"
|
|
class="radioQuestion"
|
|
:questionText="questionText"
|
|
:answers="answers"
|
|
groupName="chooseShop"
|
|
textPosition="text-start"
|
|
v-model="localSelectedProviderNumber"
|
|
isRequired
|
|
:validationRules="this.localSelectedProviderNumber ? '' : 'option-required'" />
|
|
<div class="show-more-shops-link">
|
|
<textLink
|
|
v-if="displaySeeMoreLocationsLink"
|
|
ref="showMoreShopsLink"
|
|
id="showMoreShopsId2"
|
|
cmsWidgetName="ShowMoreShopsLinkWidget"
|
|
linkType="text"
|
|
:text="showMoreShopsLinkText"
|
|
href="#!"
|
|
@click-event="updateShopListWithNextShops()"
|
|
:aria-label="showMoreShopsLinkText" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</modal>
|
|
</template>
|
|
|
|
<script>
|
|
// Components
|
|
import modal from "@/digital-components/modal/modal";
|
|
import textLink from "@/ux-components/text-link/text-link.vue";
|
|
import serviceZipQuestion from "@/layouts/service-location/service-zip-modal-question/service-zip-question/service-zip-question.vue";
|
|
import buttonQuestion from "@/digital-components/button-question/button-question.vue";
|
|
import alert from "@/ux-components/alert/alert.vue";
|
|
import shopListButton from "./shop-list-button/shop-list-button";
|
|
|
|
// Supporting files
|
|
import { defineRule } from "vee-validate";
|
|
import { required } from "@/helpers/validation-rules";
|
|
import { errorMessages } from "@/constants/error-messages";
|
|
import baseMixin from "@/mixins/base-mixin.js";
|
|
import loader from "@/ux-components/loader/loader";
|
|
|
|
import { AppointmentTypeStrings } from "@/constants/schedule-constants";
|
|
|
|
import {
|
|
getServiceabilityDetails,
|
|
getClosestApplicableShops,
|
|
getShopProviderData,
|
|
getPricedMobileFeePart,
|
|
} from "@/layouts/service-location/helpers/service-location-helper/service-location-helper";
|
|
|
|
defineRule("option-required", required(errorMessages.OPTION_REQUIRED));
|
|
|
|
export default {
|
|
name: "shop-question-popup",
|
|
mixins: [baseMixin],
|
|
data() {
|
|
return {
|
|
localZipCode: this.zipCodeFromParent,
|
|
shopListButton: shopListButton,
|
|
numberOfShopsToDisplay: 0,
|
|
localShopProviderData: this.shopProviderDataFromParent,
|
|
localSelectedProviderNumber: null,
|
|
zipCodeData: null,
|
|
isLoading: false,
|
|
displayNoServiceAlert: false,
|
|
};
|
|
},
|
|
props: {
|
|
shopProviderDataFromParent: Object,
|
|
zipCodeFromParent: {
|
|
type: String,
|
|
default: "",
|
|
},
|
|
selectedProviderNumberFromParent: {
|
|
type: String,
|
|
default: null,
|
|
},
|
|
},
|
|
computed: {
|
|
shopQuestionLinkText() {
|
|
return this.getCmsContent("ChangeMyLocationLinkWidget", "Text");
|
|
},
|
|
modalHeaderText() {
|
|
return this.getCmsContent("ShopQuestionWidget", "QuestionText");
|
|
},
|
|
modalFooterText() {
|
|
return this.getCmsContent("SaveLocationWidget", "Text");
|
|
},
|
|
showMoreShopsLinkText() {
|
|
return this.getCmsContent("ShowMoreShopsLinkWidget", "Text");
|
|
},
|
|
displayInvalidZipAlert() {
|
|
return this.zipCodeData && !this.zipCodeData.isValid;
|
|
},
|
|
displayNoShopsAlert() {
|
|
return !this.shopProviders.length && !this.displayInvalidZipAlert;
|
|
},
|
|
displayShopsList() {
|
|
return !this.displayInvalidZipAlert && !this.displayNoServiceAlert;
|
|
},
|
|
modal() {
|
|
return this.$refs.shopQuestionModal;
|
|
},
|
|
shopProviders() {
|
|
return this.localShopProviderData?.shopProviders ?? [];
|
|
},
|
|
additionalButtonData() {
|
|
return {
|
|
// Used for availability indicators, could be refactored out of any availability indicator
|
|
// logic if/when we are sure we'll never have separate drop off/ in shop logic
|
|
shopAppointmentType: AppointmentTypeStrings.IN_SHOP_OR_DROP_OFF,
|
|
};
|
|
},
|
|
displaySeeMoreLocationsLink() {
|
|
if (!this.shopProviders.length) return false;
|
|
return this.numberOfShopsToDisplay < this.shopProviders.length;
|
|
},
|
|
isModalFooterButtonDisabled() {
|
|
return (
|
|
this.isLoading ||
|
|
this.displayInvalidZipAlert ||
|
|
this.displayNoShopsAlert ||
|
|
this.displayNoServiceAlert ||
|
|
!this.localSelectedProviderNumber
|
|
);
|
|
},
|
|
answers() {
|
|
const updatedProvidersToDisplay = this.shopProviders.slice(
|
|
0,
|
|
this.numberOfShopsToDisplay
|
|
);
|
|
return updatedProvidersToDisplay.map((shopProvider) => {
|
|
const streetAddress = this.toTitleCase(shopProvider.address.streetAddress);
|
|
const city = this.toTitleCase(shopProvider.address.city);
|
|
const state = shopProvider.address.state;
|
|
const zipCode = shopProvider.address.zipCode;
|
|
const distanceInMiles = Math.round(shopProvider.distanceInMiles * 2) / 2;
|
|
|
|
return {
|
|
buttonLabel: city,
|
|
buttonLabelSubCopy: `${distanceInMiles} mi`,
|
|
buttonBodyCopy: `${streetAddress}, ${city}, ${state} ${zipCode}`,
|
|
additionalButtonData: this.additionalButtonData,
|
|
value: shopProvider.providerNumber,
|
|
};
|
|
});
|
|
},
|
|
},
|
|
methods: {
|
|
openModal() {
|
|
// Reset local state from parent
|
|
this.zipCodeData = null;
|
|
this.displayNoServiceAlert = false;
|
|
this.localShopProviderData = this.shopProviderDataFromParent;
|
|
this.localZipCode = this.zipCodeFromParent;
|
|
// This will be set once the modal is open (updateSelectedAnswer()) to ensure that the
|
|
// correct answer is selected
|
|
this.localSelectedProviderNumber = null;
|
|
|
|
const selectedProviderNumberFromParentIndex = this.shopProviders.findIndex(
|
|
(provider) => provider.providerNumber == this.selectedProviderNumberFromParent
|
|
);
|
|
|
|
if (selectedProviderNumberFromParentIndex > -1) {
|
|
this.numberOfShopsToDisplay =
|
|
Math.ceil((selectedProviderNumberFromParentIndex + 1) / 3) * 3;
|
|
} else {
|
|
this.numberOfShopsToDisplay = 3;
|
|
}
|
|
|
|
this.updateShopsForZip();
|
|
this.$refs.shopQuestionModal.openModal();
|
|
},
|
|
updateSelectedAnswer() {
|
|
this.localSelectedProviderNumber = this.selectedProviderNumberFromParent;
|
|
},
|
|
toTitleCase(str) {
|
|
return str.replace(/\w\S*/g, function (txt) {
|
|
return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
|
|
});
|
|
},
|
|
updateShopListWithNextShops() {
|
|
//gaAction = this.GaActions.SHOPS_FIRST_DISPLAYED; --This needs to actually fire on initial display, can never happen here
|
|
this.numberOfShopsToDisplay += 3;
|
|
this.pushShopsDisplayedGAEvent(this.answers);
|
|
},
|
|
pushShopsDisplayedGAEvent(nextShopListAnswers) {
|
|
var shops = nextShopListAnswers.map((shop) => {
|
|
if (shop.value.length > 5 && shop.value.startsWith("00")) {
|
|
return shop.value.substring(1);
|
|
} else {
|
|
return shop.value;
|
|
}
|
|
});
|
|
|
|
var joinedShops = shops.join(",");
|
|
if (!joinedShops) {
|
|
joinedShops = "no-shops";
|
|
}
|
|
|
|
this.pushEventToGA(
|
|
this.GaCategories.SERVICE_LOCATION,
|
|
this.GaActions.MORE_LOCATIONS_CLICKED,
|
|
joinedShops,
|
|
true
|
|
);
|
|
},
|
|
closeModal() {
|
|
this.modal.closeModal();
|
|
},
|
|
async onSearchZip(event) {
|
|
event.preventDefault();
|
|
this.displayNoServiceAlert = false;
|
|
if (!this.localZipCode) {
|
|
this.zipCodeData = {
|
|
isValid: false,
|
|
};
|
|
return;
|
|
}
|
|
this.isLoading = true;
|
|
|
|
this.zipCodeData = await this.getZipCodeData(this.localZipCode, this.pageName);
|
|
// Add zipCode to zipCodeData as it does not come back from endpoint
|
|
this.zipCodeData.zipCode = this.localZipCode;
|
|
if (this.zipCodeData.isValid) {
|
|
if (this.$store.getters.order.vehicle?.isBigTruck) {
|
|
// check the location endpoint to verify this zip can service a heavy truck
|
|
const closestShops = await getClosestApplicableShops(
|
|
this.localZipCode,
|
|
this.$store.getters.order.vehicle.carId,
|
|
this.pageName
|
|
);
|
|
if (!closestShops || closestShops.inShopProviders?.length === 0) {
|
|
this.isLoading = false;
|
|
this.displayNoServiceAlert = true;
|
|
return null;
|
|
}
|
|
}
|
|
|
|
getShopProviderData(this.localZipCode).then(async (result) => {
|
|
this.localShopProviderData = result.data;
|
|
this.numberOfShopsToDisplay = 3;
|
|
this.updateShopsForZip();
|
|
this.isLoading = false;
|
|
});
|
|
} else {
|
|
this.isLoading = false;
|
|
}
|
|
},
|
|
updateShopsForZip() {
|
|
if (!this.shopProviders.length) {
|
|
return;
|
|
}
|
|
if (
|
|
!this.answers.some(
|
|
(a) => String(a.value) === String(this.localSelectedProviderNumber)
|
|
)
|
|
) {
|
|
this.localSelectedProviderNumber = null;
|
|
}
|
|
// I think we should call our new pushGA Event here with new mapped data
|
|
},
|
|
async setZipCodeAndShop() {
|
|
const shopQuestionPopUpData = {
|
|
zipCodeData: this.zipCodeData,
|
|
shopProviderData: this.localShopProviderData,
|
|
selectedProviderNumber: this.localSelectedProviderNumber,
|
|
};
|
|
this.$emit("shop-selected", shopQuestionPopUpData);
|
|
|
|
if (this.zipCodeData) {
|
|
// retrieve serviceability details
|
|
const serviceabilityDetails = await getServiceabilityDetails(
|
|
this.zipCodeData.zipCode,
|
|
null,
|
|
this.pageName
|
|
);
|
|
|
|
this.$emit("updated-serviceability", serviceabilityDetails.data);
|
|
|
|
// retrieve mobile fee part
|
|
const mobileFeePart = await getPricedMobileFeePart(
|
|
this.zipCodeData.zipCode,
|
|
this.pageName
|
|
);
|
|
|
|
this.$emit("updated-mobile-fee-part", mobileFeePart);
|
|
}
|
|
|
|
this.closeModal();
|
|
},
|
|
},
|
|
components: {
|
|
modal,
|
|
textLink,
|
|
serviceZipQuestion,
|
|
buttonQuestion,
|
|
alert,
|
|
loader,
|
|
},
|
|
};
|
|
</script>
|
|
|
|
<style lang="scss">
|
|
.shop-question {
|
|
margin-top: 1rem;
|
|
text-align: center;
|
|
|
|
a {
|
|
@include responsive-font-size-md(0.875rem, 1rem);
|
|
}
|
|
|
|
.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;
|
|
gap: 0.25rem;
|
|
|
|
.alert-heading {
|
|
text-align: left;
|
|
font-size: 0.75rem;
|
|
line-height: 1.25rem;
|
|
}
|
|
}
|
|
.modal-link {
|
|
text-align: center;
|
|
}
|
|
.modal.modal-component .modal-dialog .modal-content .modal-body .textbox-question {
|
|
padding: 0;
|
|
}
|
|
.modal.shop-question-popup .btn.btn-primary {
|
|
background: $blue;
|
|
|
|
&:focus,
|
|
&:focus-visible {
|
|
box-shadow:
|
|
0 0 0 3px,
|
|
0 0 0 5.5px $blue;
|
|
}
|
|
|
|
&.form-test-invalid {
|
|
background: #d4d6d8;
|
|
|
|
&:focus,
|
|
&:focus-visible {
|
|
box-shadow: none;
|
|
}
|
|
}
|
|
}
|
|
.shop-question-zipcode {
|
|
display: flex;
|
|
flex-direction: row;
|
|
align-items: stretch;
|
|
width: 100%;
|
|
height: 48px;
|
|
border-radius: 48px;
|
|
padding-left: 16px;
|
|
border: 1px solid #ccc;
|
|
background: #fff;
|
|
margin-bottom: 20px;
|
|
@include media-breakpoint-up(md) {
|
|
height: auto;
|
|
padding-left: 0;
|
|
}
|
|
.input-wrapper.has-search-icon {
|
|
display: flex;
|
|
align-items: stretch;
|
|
flex: 1 1 0;
|
|
height: 100%;
|
|
margin-bottom: 0;
|
|
input,
|
|
.form-control {
|
|
width: 100%;
|
|
height: 48px;
|
|
min-height: 48px;
|
|
max-height: 48px;
|
|
border: none;
|
|
padding: 0 1.5rem 0 1.5rem;
|
|
outline: none;
|
|
font-size: 1rem;
|
|
background: transparent;
|
|
box-sizing: border-box;
|
|
border-radius: 2.5rem 0 0 2.5rem;
|
|
}
|
|
}
|
|
.search-icon-button {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background-color: #e5f1fa !important;
|
|
width: 2.5rem;
|
|
height: 48px;
|
|
border: none;
|
|
max-height: 48px;
|
|
border-radius: 0 2.5rem 2.5rem 0;
|
|
cursor: pointer;
|
|
}
|
|
}
|
|
.show-more-shops-link a {
|
|
text-align: center;
|
|
display: block;
|
|
margin-left: auto;
|
|
margin-right: auto;
|
|
}
|
|
.shop-question-zipcode .search-icon-button {
|
|
background-color: #e5f1fa !important;
|
|
border-radius: 0 2.5rem 2.5rem 0;
|
|
}
|
|
.shop-question-zipcode .search-icon-button:hover,
|
|
.shop-question-zipcode .search-icon-button:focus {
|
|
border: 1px solid $gray-500;
|
|
box-shadow: 0 0 0 4px $blue-300;
|
|
background-color: #e5f1fa;
|
|
outline: none;
|
|
}
|
|
.shop-question-zipcode .input-wrapper.has-search-icon .form-control {
|
|
border-radius: 2.5rem;
|
|
padding-left: 1.5rem;
|
|
}
|
|
|
|
.shop-question-zipcode .form-test-error {
|
|
display: block;
|
|
width: 100%;
|
|
margin-top: 0.25rem;
|
|
margin-left: 0;
|
|
color: #d32f2f; // or your error color
|
|
position: static !important;
|
|
font-size: 0.875rem;
|
|
clear: both;
|
|
float: none;
|
|
display: block;
|
|
width: 100%;
|
|
}
|
|
.loader-wrapper {
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
max-width: 100%;
|
|
}
|
|
</style>
|