-
+
+
+
+
@@ -84,81 +86,45 @@ import serviceZipQuestion from "@/layouts/service-location/service-zip-modal-que
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";
-import loadingModal from "@/fmg-components/loading-modal/loading-modal.vue";
// 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 { nextTick } from "vue";
-import { regex } from "@/helpers/validation-rules";
+import loader from "@/ux-components/loader/loader";
-import { getAvailabilityRating } from "@/layouts/service-location/helpers/service-location-helper/service-location-helper";
-
-import { Provider } from "@/layouts/service-location/classes/provider";
import { AppointmentTypeStrings } from "@/constants/schedule-constants";
import {
- getPricedMobileFeePart,
- getPricedRecycleFeePart,
getServiceabilityDetails,
- getBillToAccountNumber,
getClosestApplicableShops,
getShopProviderData,
- getZipCodeData,
} from "@/layouts/service-location/helpers/service-location-helper/service-location-helper";
defineRule("option-required", required(errorMessages.OPTION_REQUIRED));
-defineRule("zip-format", regex(/(^\d{5}$)|(^\d{5}-\d{4}$)/, errorMessages.SERVICE_ZIP_FORMAT));
export default {
name: "shop-question-popup",
mixins: [baseMixin],
data() {
return {
- isShopQuestionDisplayed: true,
- internalModel: this.copyModel(this.modelValue),
- answers: [],
+ localZipCode: this.zipCodeFromParent,
shopListButton: shopListButton,
- shopIndex: 0,
- displaySeeMoreLocationsLink: false,
- serviceZipCodeTextInputId: "",
- localShopProviderData: this.shopProviderData,
- selectedProviderNumber: null,
- isLoadingShops: false,
- lastSearchedZip: "",
- lastSuccessfulZip: "",
- hasLoadedShopData: false,
- displayInvalidZipAlert: false,
- areShopsAvialable: true,
- displayNoShopsAlert: false,
+ numberOfShopsToDisplay: 0,
+ localShopProviderData: this.shopProviderDataFromParent,
+ localSelectedProviderNumber: null,
+ zipCodeData: null,
+ isLoading: false,
};
},
props: {
- appointmentType: String,
- shopProviderData: Object,
- modelValue: {
- type: Object,
- default: () => ({
- state: "",
- zipCode: "",
- }),
- },
- isDisplayed: Boolean,
- modelWidgetName: {
- type: String,
- required: true,
- },
- isServiceableInshop: {
- type: Boolean,
- default: false,
- },
- zipCode: {
+ shopProviderDataFromParent: Object,
+ zipCodeFromParent: {
type: String,
default: "",
},
- preSelectedProviderNumber: {
+ selectedProviderNumberFromParent: {
type: String,
default: null,
},
@@ -167,14 +133,20 @@ export default {
shopQuestionLinkText() {
return this.getCmsContent("ChangeMyLocationLinkWidget", "Text");
},
- modalName() {
- return this.modelWidgetName;
- },
modalHeaderText() {
return this.getCmsContent("ShopQuestionWidget", "QuestionText");
},
- selectShopText() {
- return this.getCmsContent("YourSafeliteShopWidget", "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;
},
modal() {
return this.$refs.shopQuestionModal;
@@ -182,79 +154,33 @@ export default {
shopProviders() {
return this.localShopProviderData?.shopProviders ?? [];
},
- modalFooterText() {
- return this.getCmsContent("SaveLocationWidget", "Text");
- },
- selectedValue: {
- get: function () {
- return this.modelValue;
- },
- set: function (newValue) {},
- },
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.appointmentType,
+ // 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,
};
},
- showMoreShopsLinkText() {
- return this.getCmsContent("ShowMoreShopsLinkWidget", "Text");
+ displaySeeMoreLocationsLink() {
+ if (!this.shopProviders.length) return false;
+ return this.numberOfShopsToDisplay < this.shopProviders.length;
},
- },
- methods: {
- openModal(event) {
- if (event) event.preventDefault();
- if (
- !this.shopProviderData ||
- !this.shopProviderData.shopProviders ||
- !this.shopProviderData.shopProviders.length
- ) {
- // Optionally show a loading message or disable the link
- return;
- }
- this.$refs.shopQuestionModal.openModal();
+ isModalFooterButtonDisabled() {
+ return (
+ this.isLoading ||
+ this.displayInvalidZipAlert ||
+ this.displayNoShopsAlert ||
+ !this.localSelectedProviderNumber
+ );
},
- getSelectedProviderObject(providerNumber) {
- const provider =
- this.shopProviders?.find((provider) => provider.providerNumber == providerNumber) ??
- new Provider();
-
- return provider;
- },
- async getNextShopsFromList(numberToGet = 3) {
- const logFirstShopsDisplayed = this.shopIndex == 0;
-
- 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) => {
- return str.replace(/\w\S*/g, function (txt) {
- return 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);
+ 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;
@@ -267,354 +193,139 @@ export default {
value: shopProvider.providerNumber,
};
});
-
- var gaAction = this.GaActions.MORE_LOCATIONS_CLICKED;
- if (logFirstShopsDisplayed) {
- gaAction = this.GaActions.SHOPS_FIRST_DISPLAYED;
- }
- if (
- this.appointmentType === AppointmentTypeStrings.IN_SHOP ||
- this.appointmentType === AppointmentTypeStrings.DROP_OFF ||
- this.appointmentType === AppointmentTypeStrings.IN_SHOP_OR_DROP_OFF
- ) {
- var shops = mappedData.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, gaAction, joinedShops, true);
- }
-
- 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();
},
- async handleUpdate(selectedShopIndex = null) {
- this.resetAnswers();
- if (selectedShopIndex >= 3) {
- await this.getNextShopsFromList(selectedShopIndex + 1);
- } else {
- await this.getNextShopsFromList();
- await nextTick();
- }
- },
- copyModel(modelToCopy) {
- return {
- state: modelToCopy.state,
- zipCode: modelToCopy.zipCode,
- };
- },
- resetAnswers() {
- this.answers = [];
- this.shopIndex = 0;
- },
- onModalOpened() {
- const preSelectedIndex = this.shopProviders.findIndex(
- (provider) => provider.providerNumber == this.preSelectedProviderNumber
+ },
+ methods: {
+ openModal() {
+ // Reset local state from parent
+ this.zipCodeData = null;
+ 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 (preSelectedIndex > -1) {
- this.shopIndex = Math.ceil((preSelectedIndex + 1) / 3) * 3;
+ if (selectedProviderNumberFromParentIndex > -1) {
+ this.numberOfShopsToDisplay =
+ Math.ceil((selectedProviderNumberFromParentIndex + 1) / 3) * 3;
} else {
- this.shopIndex = 3;
+ this.numberOfShopsToDisplay = 3;
}
- this.internalModel = this.copyModel(this.modelValue);
- this.internalModel.zipCode = this.zipCode;
- this.localShopProviderData = this.shopProviderData;
- this.answers = [];
- this.onSearchZip();
- this.focusOnZipInput();
- this.selectedProviderNumber = this.preSelectedProviderNumber;
- if (
- (!this.selectedProviderNumber ||
- !this.shopProviders.some(
- (p) => p.providerNumber == this.selectedProviderNumber
- )) &&
- this.shopProviders.length > 0
- ) {
- this.selectedProviderNumber = this.shopProviders[0].providerNumber;
+
+ 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();
},
- onModalClosed() {
- this.internalModel = this.copyModel(this.modelValue);
- this.resetsOnZipInput();
- },
- resetsOnZipInput() {
- this.resetAlerts();
- },
async onSearchZip(event) {
- if (event) {
- event.preventDefault();
- event.stopPropagation();
+ event.preventDefault();
+ if (!this.localZipCode) {
+ this.zipCodeData = {
+ isValid: false,
+ };
+ return;
}
- this.resetsOnZipInput();
- this.displayNoShopsAlert = false;
- const zip = this.internalModel.zipCode;
- const zipCodeData = await this.getZipCodeData(
- this.internalModel.zipCode,
- "service-location"
- );
- if (!zipCodeData.isValid) {
- this.displayInvalidZipAlert = true;
- //this.selectedProviderNumber = null;
- this.focusOnZipInput();
- this.resetModalButtonStyle();
- } else {
+ this.isLoading = true;
+ this.zipCodeData = await this.getZipCodeData(this.localZipCode, "service-location");
+ // Add zipCode to zipCodeData as it does not come back from endpoint
+ this.zipCodeData.zipCode = this.localZipCode;
+ if (this.zipCodeData.isValid) {
if (this.isVehicleHeavyTruck) {
// check the location endpoint to verify this zip can service a heavy truck
const closestShops = await getClosestApplicableShops(
- this.internalModel.zipCode,
+ this.localZipCode,
this.carId,
"service-location"
);
if (!closestShops || closestShops.providers?.length === 0) {
- this.displayNoServiceAlert = true;
- this.focusOnZipInput();
- this.resetModalButtonStyle();
+ // We need an alert for heavy trucks
return null;
}
} else {
- getShopProviderData(zip).then(async (result) => {
- if (this.displayNoShopsAlert === true) {
- this.displayNoShopsAlert = false;
- }
+ getShopProviderData(this.localZipCode).then(async (result) => {
this.localShopProviderData = result.data;
- this.updateShopsForZip(this.internalModel.zipCode);
- if (this.appointmentType === AppointmentTypeStrings.MOBILE) {
- this.selectedProvider = new Provider(
- this.shopProviderData.mobileProviderNumber
- );
- } else {
- this.selectedProvider = new Provider();
- }
+ this.numberOfShopsToDisplay = 3;
+ this.updateShopsForZip();
+ this.isLoading = false;
});
}
+ } else {
+ this.isLoading = false;
}
},
- async updateShopsForZip(zipCode) {
- if (!this.appointmentType) {
- this.answers = [];
- this.isLoadingShops = false;
+ updateShopsForZip() {
+ if (!this.shopProviders.length) {
return;
}
- this.lastSearchedZip = zipCode;
- if (!zipCode) {
- this.isLoadingShops = false;
- this.answers = [];
- return;
- }
- this.isLoadingShops = true;
- if (!zipCode || !this.shopProviders.length) {
- this.answers = [];
- this.displayNoShopsAlert = true;
- this.displaySeeMoreLocationsLink = false;
- this.isLoadingShops = false;
- return;
- }
-
- const selectedIndex = this.shopProviders.findIndex(
- (provider) => provider.providerNumber == this.selectedProviderNumber
- );
-
- // If not found, only show the first 3 shops
- if (selectedIndex === -1) {
- this.shopIndex = 3;
- }
-
- const sortedShops = this.getShopsByZip(zipCode, this.shopProviders, this.shopIndex);
-
- const toTitleCase = (str) => {
- return str.replace(/\w\S*/g, function (txt) {
- return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
- });
- };
-
- const mappedData = sortedShops.map((shopProvider) => {
- const streetAddress = toTitleCase(shopProvider.address.streetAddress);
- const city = toTitleCase(shopProvider.address.city);
- const state = shopProvider.address.state;
- const zipCode = shopProvider.address.zipCode;
- const distanceInMiles = Math.round(shopProvider.distanceInMiles * 2) / 2;
-
- 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 {
- buttonLabel: city,
- buttonLabelSubCopy: `${distanceInMiles} mi`,
- buttonBodyCopy: `${streetAddress}, ${city}, ${state} ${zipCode}`,
- additionalButtonData: {
- availabilityRatingCallback: getAvailabilityRating,
- startDate: formattedStartDate,
- endDate: formattedEndDate,
- shopAppointmentType: this.appointmentType,
- },
- value: shopProvider.providerNumber,
- };
- });
- this.answers = mappedData;
if (
- this.selectedProviderNumber &&
- !this.answers.some((a) => String(a.value) === String(this.selectedProviderNumber))
+ !this.answers.some(
+ (a) => String(a.value) === String(this.localSelectedProviderNumber)
+ )
) {
- this.selectedProviderNumber = null;
+ this.localSelectedProviderNumber = null;
}
- this.shopIndex = mappedData.length;
- this.displaySeeMoreLocationsLink = this.shopIndex < this.shopProviders.length;
- if (mappedData.length > 0) {
- this.lastSuccessfulZip = zipCode;
- }
- this.isLoadingShops = false;
- this.displayNoShopsAlert = this.answers.length === 0;
- },
- resetModalButtonStyle() {
- this.modal.resetButtonStyle();
- },
- onInputIdAssigned(inputId) {
- this.serviceZipCodeTextInputId = inputId;
- },
- focusOnZipInput() {
- const input = document.getElementById(this.serviceZipCodeTextInputId);
- input?.focus();
- },
- resetAlerts() {
- this.displayInvalidZipAlert = false;
- this.displayNoServiceAlert = false;
- this.displayNoShopsAlert = false;
+ // I think we should call our new pushGA Event here with new mapped data
},
async setZipCodeAndShop() {
- if (
- this.displayInvalidZipAlert ||
- this.displayNoShopsAlert ||
- !this.selectedProviderNumber
- ) {
- return;
- }
- const zipToEmit = this.lastSearchedZip;
-
- const selectedProvider = this.shopProviders.find(
- (provider) => provider.providerNumber == this.selectedProviderNumber
- );
- this.resetAlerts();
-
- const zipCodeData = await this.getZipCodeData(zipToEmit, "service-location");
-
- if (!zipCodeData.isValid) {
- this.displayInvalidZipAlert = true;
- this.focusOnZipInput();
- this.resetModalButtonStyle();
- } else {
- if (this.isVehicleHeavyTruck) {
- // check the location endpoint to verify this zip can service a heavy truck
- const closestShops = await getClosestApplicableShops(
- selectedProvider.address.zipCode,
- this.carId,
- "service-location"
- );
-
- if (!closestShops || closestShops.providers?.length === 0) {
- this.displayNoServiceAlert = true;
- this.focusOnZipInput();
- this.resetModalButtonStyle();
- return null;
- }
- }
-
- selectedProvider.address.zipCodeCtu = zipCodeData.zipCodeCtu;
-
- // retrieve mobile fee part
- const serviceZipCode = selectedProvider.address.zipCode;
- const mobileFeePart = await getPricedMobileFeePart(
- serviceZipCode,
- "service-location"
- );
-
- // retrieve recycle fee part
- const recycleFeePart = await getPricedRecycleFeePart(
- serviceZipCode,
- "service-location"
- );
+ 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(
- serviceZipCode,
+ this.zipCodeData.zipCode,
null,
- "service-location"
+ this.pageName
);
- const billToAccountNumber = await getBillToAccountNumber(
- selectedProvider.address.zipCodeCtu
- );
-
- // update content related to service zip code
- this.$emit("updated-mobile-fee-part", mobileFeePart);
- this.$emit("updated-recycle-fee-part", recycleFeePart);
this.$emit("updated-serviceability", serviceabilityDetails.data);
- this.$emit("updated-contains-military-base", zipCodeData.containsMilitaryBase);
- this.$emit("updated-bill-to-account-number", billToAccountNumber);
- this.$emit("updated-zipcode-ctu", selectedProvider.address.zipCodeCtu);
-
- this.$emit("update:modelValue", this.selectedValue);
- this.$emit("update-shop-provider-data", this.localShopProviderData);
- this.internalModel.state = zipCodeData.state;
- this.$emit("shop-selected", {
- providerNumber: selectedProvider?.providerNumber,
- address: selectedProvider?.address,
- searchedZip: zipToEmit,
- searchedState: this.internalModel.state,
- });
-
- this.closeModal();
}
- },
- getShopsByZip(zipCode, shops, numberToGet = 3) {
- return shops
- .filter(
- (shop) => shop.address && shop.address.zipCode && shop.address.zipCode !== ""
- )
- .sort((a, b) => a.distanceInMiles - b.distanceInMiles)
- .slice(0, numberToGet);
- },
- },
- watch: {
- shopProviderData: {
- immediate: true,
- handler(newVal) {
- this.localShopProviderData = newVal;
- this.hasLoadedShopData = true;
- },
+
+ this.closeModal();
},
},
components: {
@@ -623,6 +334,7 @@ export default {
serviceZipQuestion,
buttonQuestion,
alert,
+ loader,
},
};
@@ -750,4 +462,10 @@ export default {
display: block;
width: 100%;
}
+.loader-wrapper {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ max-width: 100%;
+}
diff --git a/src/layouts/service-location/shop-question/shop-question.vue b/src/layouts/service-location/shop-question/shop-question.vue
index 79bda1ef7..542ef69d6 100644
--- a/src/layouts/service-location/shop-question/shop-question.vue
+++ b/src/layouts/service-location/shop-question/shop-question.vue
@@ -49,8 +49,6 @@ import { errorMessages } from "@/constants/error-messages";
import baseMixin from "@/mixins/base-mixin.js";
import { nextTick } from "vue";
-import { getAvailabilityRating } from "@/layouts/service-location/helpers/service-location-helper/service-location-helper";
-
import { Provider } from "@/layouts/service-location/classes/provider";
import { AppointmentTypeStrings } from "@/constants/schedule-constants";
@@ -109,17 +107,7 @@ export default {
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,
};
},
diff --git a/src/layouts/vin-lookup/vin-information/vin-information.vue b/src/layouts/vin-lookup/vin-information/vin-information.vue
index f08196135..912663a44 100644
--- a/src/layouts/vin-lookup/vin-information/vin-information.vue
+++ b/src/layouts/vin-lookup/vin-information/vin-information.vue
@@ -57,6 +57,9 @@ export default {
font-size: 0.875rem;
}
.vin-toggle {
+ display: inline-flex;
+ justify-content: center;
+ align-items: center;
&:after {
content: "";
transition: all 0.5s ease;