Merge pull request #2658 from Safelite/feature/skiener/CASH-816-refactor

Feature/skiener/cash 816 refactor
This commit is contained in:
scottkiener-at-safelite 2025-07-17 15:42:30 -04:00 committed by GitHub
commit 3fd176559e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 190 additions and 605 deletions

View file

@ -182,11 +182,11 @@ export default {
};
},
methods: {
focusSearchInput() {
focusSearchInput(event) {
// Focus cursor in input when search icon is clicked
const field = document.querySelector("input");
field.focus();
this.$emit("search-icon-click");
this.$emit("search-icon-click", event);
},
async imageChanged(e) {
let file = e.target.files[0];

View file

@ -105,23 +105,11 @@
class="ps-4 pe-4 mt-4" />
</div>
<shopQuestionPopup
modalWidgetName="ShopQuestionPopupWidget"
:appointmentType="appointmentType"
:modelValue="{
zipCode: zipCode,
state: state,
}"
:preSelectedProviderNumber="selectedProvider?.providerNumber"
:shopProviderData="shopProviderData"
:isServiceableInshop="isServiceableInshop"
@updated-serviceability="setServiceabilityDetails"
v-if="isShopQuestionDisplayed"
:selectedProviderNumberFromParent="selectedProvider?.providerNumber"
:shopProviderDataFromParent="shopProviderData"
@shop-selected="onShopSelected"
@update:modelValue="onShopModelUpdated"
@updated-zipcode-ctu="setZipcodeCtu"
@update-shop-provider-data="shopProviderData = $event"
@updated-contains-military-base="setContainsMilitaryBase"
:zipCode="zipCode"
:isDisplayed="isShopQuestionDisplayed"
:zipCodeFromParent="zipCode"
cmsWidgetName="YourSafeliteShopWidget" />
<contentGroupModal ref="RecalModal" cmsWidgetName="RecalModal" />
@ -174,7 +162,6 @@ import shopQuestion from "@/layouts/service-location/shop-question/shop-question
import shopQuestionPopup from "@/layouts/service-location/shop-question/shop-question-popup";
import buttonQuestion from "@/digital-components/button-question/button-question.vue";
import shopListButton from "@/layouts/service-location/shop-question/shop-list-button/shop-list-button";
import { getAvailabilityRating } from "@/layouts/service-location/helpers/service-location-helper/service-location-helper";
import funnelHeader from "@/fmg-components/funnel-header/funnel-header";
import navbar from "@/fmg-components/nav-bar/nav-bar";
@ -813,17 +800,7 @@ export default {
return this.displayNoShopsAlert || !this.isMobileAddressValid;
},
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,
};
},
@ -1168,43 +1145,20 @@ export default {
startTime: "08:00",
};
},
onShopSelected(providerObject) {
const current = this.shopProviderData?.shopProviders?.find(
(p) => p.providerNumber === this.selectedProvider?.providerNumber
);
if (current) {
this.selectedProvider = current;
}
onShopSelected(shopQuestionPopUpData) {
this.shopProviderData = shopQuestionPopUpData.shopProviderData;
this.setServiceabilityDetails(shopQuestionPopUpData.serviceabilityDetails);
this.selectedProvider = this.shopProviderData.shopProviders.find((shopProvider) => {
return shopProvider.providerNumber === shopQuestionPopUpData.selectedProviderNumber;
});
let provider = this.shopProviderData?.shopProviders?.find(
(p) => p.providerNumber === providerObject?.providerNumber
);
this.selectedProvider = provider;
if (providerObject && providerObject.address) {
this.zipCode = providerObject.searchedZip;
this.state = providerObject.searchedState;
}
},
setZipcodeCtu(zipcodeCtu) {
this.zipCodeCtu = zipcodeCtu;
},
onShopModelUpdated(newModel) {
if (newModel.zipCode) {
this.zipCode = newModel.zipCode;
}
if (newModel.zipCodeCtu) {
this.zipCodeCtu = newModel.zipCodeCtu;
}
if (newModel.state) {
this.state = newModel.state;
}
if (this.shopProviderData && newModel.selectedProviderNumber) {
const provider = this.shopProviderData.shopProviders.find(
(p) => p.providerNumber === newModel.selectedProviderNumber
);
if (provider) {
this.selectedProvider = provider;
}
// No zipCodeData comes back if zip was not changed
if (shopQuestionPopUpData.zipCodeData) {
this.zipCode = shopQuestionPopUpData.zipCodeData.zipCode;
this.state = shopQuestionPopUpData.zipCodeData.state;
this.zipCodeCtu = shopQuestionPopUpData.zipCodeData.zipCodeCtu;
this.zipContainsMilitaryBase =
shopQuestionPopUpData.zipCodeData.containsMilitaryBase;
}
},
async getMoreScheduleData(startDate, endDate) {

View file

@ -122,35 +122,6 @@ export async function getShopProviderData(serviceZipCode) {
);
}
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 baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.GET_SHOP_TIME_SLOTS,
{
providerNumber: providerNumber,
startDate: startDate,
endDate: endDate,
shopAppointmentType: shopAppointmentType,
},
"service-location",
false
);
const numberOfDaysToEvaluate = 2;
const isGoodAvailability =
shopTimeSlots.data.days.filter((x) => x.timeSlots.length > 0).length >=
numberOfDaysToEvaluate;
const shopStatus = isGoodAvailability ? "high" : "low";
return shopStatus;
}
export async function getClosestApplicableShops(serviceZipCode, carId, pageNameToLog) {
if (!serviceZipCode) {
return null;

View file

@ -1,8 +1,4 @@
import {
getPricedMobileFeePart,
getServiceabilityDetails,
getAvailabilityRating,
} from "./service-location-helper";
import { getPricedMobileFeePart, getServiceabilityDetails } from "./service-location-helper";
import { storeActions } from "@/constants/store-actions";
jest.mock("@/store", () => ({
@ -320,38 +316,4 @@ describe("service-location-helper.js", () => {
expect(result).toEqual(expected);
});
});
describe("getAvailabilityRating", () => {
it("Should return a 'high' rating", async () => {
// Arrange
const providerNumber = "0000001";
const expected = "high";
// Act
const result = await getAvailabilityRating(
"2023-06-30",
"2023-07-06",
"Inshop",
providerNumber
);
// Assert
expect(result).toEqual(expected);
});
it("Should return a 'low' rating", async () => {
// Arrange
const providerNumber = "0000000";
const expected = "low";
// Act
const result = await getAvailabilityRating(
"2023-06-30",
"2023-07-06",
"Inshop",
providerNumber
);
// Assert
expect(result).toEqual(expected);
});
});
});

View file

@ -148,11 +148,9 @@
import alert from "@/ux-components/alert/alert";
import serviceZipModalQuestion from "@/layouts/service-location/service-zip-modal-question/service-zip-modal-question";
import appointmentTypeQuestion from "@/layouts/service-location/appointment-type-question/appointment-type-question";
import shopQuestion from "@/layouts/service-location/shop-question/shop-question";
import shopQuestionPopup from "@/layouts/service-location/shop-question/shop-question-popup";
import buttonQuestion from "@/digital-components/button-question/button-question.vue";
import shopListButton from "@/layouts/service-location/shop-question/shop-list-button/shop-list-button";
import { getAvailabilityRating } from "@/layouts/service-location/helpers/service-location-helper/service-location-helper";
import funnelHeader from "@/fmg-components/funnel-header/funnel-header";
import navbar from "@/fmg-components/nav-bar/nav-bar";
@ -163,7 +161,6 @@ import contentGroupModal from "@/fmg-components/content-group-modal/content-grou
import textBlock from "@/digital-components/text-block/text-block";
// Supporting files
import baseMixin from "@/mixins/base-mixin.js";
import experimentMixin from "@/mixins/experiment-mixin.js";
import { experimentSettings } from "@/constants/experiments";
import { AppointmentTypeStrings } from "@/constants/schedule-constants";
@ -450,17 +447,7 @@ export default {
return this.displayNoShopsAlert || !this.isMobileAddressValid;
},
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,
};
},

View file

@ -8,7 +8,6 @@
cornerStyle="rounded"
mask="#####"
:includeSearchIcon="includeSearchIcon"
@search-icon-click="$emit('search-icon-click')"
:displayQuestionText="false"
:isRequired="isRequired"
:validationRules="computedValidationRules" />

View file

@ -58,15 +58,22 @@ export default {
beforeMount() {
if (this.displayAvailabilityIndicators) {
this.displayLoader();
const startDate = this.additionalButtonData.startDate;
const endDate = this.additionalButtonData.endDate;
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];
const shopAppointmentType = this.additionalButtonData.shopAppointmentType;
this.additionalButtonData
.availabilityRatingCallback(startDate, endDate, shopAppointmentType, this.value)
.then((data) => {
this.availabilityRating = data;
});
this.getAvailabilityRating(
formattedStartDate,
formattedEndDate,
shopAppointmentType,
this.value
).then((data) => {
this.availabilityRating = data;
});
}
},
data() {
@ -103,6 +110,29 @@ export default {
displayLoader() {
this.isLoaderDisplayed = true;
},
async getAvailabilityRating(startDate, endDate, shopAppointmentType, providerNumber) {
// For a given shop provider number and date range, get the appointment time slots available
const shopTimeSlots = await this.dispatchStoreActionWithLogging(
this.storeActions.GET_SHOP_TIME_SLOTS,
{
providerNumber: providerNumber,
startDate: startDate,
endDate: endDate,
shopAppointmentType: shopAppointmentType,
},
"service-location",
false
);
const numberOfDaysToEvaluate = 2;
const isGoodAvailability =
shopTimeSlots.data.days.filter((x) => x.timeSlots.length > 0).length >=
numberOfDaysToEvaluate;
const shopStatus = isGoodAvailability ? "high" : "low";
return shopStatus;
},
},
components: {
loader,

View file

@ -1,6 +1,6 @@
<template>
<transition name="fade" mode="out-in">
<div v-if="isDisplayed" class="shop-question" aria-live="polite">
<div class="shop-question" aria-live="polite">
<div class="modal-link">
<text-link
linkType="text"
@ -13,20 +13,17 @@
<modal
ref="shopQuestionModal"
:headerText="modalHeaderText"
:onModalOpenedCallback="onModalOpened"
:onModalClosedCallback="onModalClosed"
:onModalOpenedCallback="updateSelectedAnswer"
:onModalClosedCallback="resetZipCodeData"
:footerButtonText="modalFooterText"
:footerButtonDisabled="
displayInvalidZipAlert || displayNoShopsAlert || !selectedProviderNumber
"
:footerButtonDisabled="isModalFooterButtonDisabled"
@footer-button-event="setZipCodeAndShop">
<form @submit.prevent>
<serviceZipQuestion
ref="serviceZipQuestion"
class="shop-question-zipcode"
customInputId="serviceZipCode"
v-model="internalModel.zipCode"
v-on="{ 'textboxQuestionEvent.inputIdAssigned': onInputIdAssigned }"
v-model="localZipCode"
cmsWidgetName="ServiceZipQuestionWidget"
@search-icon-click="onSearchZip"
@keydown.enter="onSearchZip"
@ -57,9 +54,9 @@
:answers="answers"
groupName="chooseShop"
textPosition="text-start"
v-model="selectedProviderNumber"
v-model="localSelectedProviderNumber"
isRequired
:validationRules="this.selectedProviderNumber ? '' : 'option-required'" />
:validationRules="this.localSelectedProviderNumber ? '' : 'option-required'" />
<div class="show-more-shops-link">
<textLink
v-if="displaySeeMoreLocationsLink"
@ -69,7 +66,7 @@
linkType="text"
:text="showMoreShopsLinkText"
href="#!"
@click-event="getNextShopsFromList(3)"
@click-event="updateShopListWithNextShops()"
:aria-label="showMoreShopsLinkText" />
</div>
</div>
@ -84,81 +81,43 @@ 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 { 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,
};
},
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 +126,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 +147,32 @@ 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.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 +185,130 @@ 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();
}
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 {
event.preventDefault();
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();
});
}
}
},
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
(provider) => provider.providerNumber == this.localSelectedProviderNumber
);
this.resetAlerts();
const serviceZipCode = selectedProvider.address.zipCode;
const zipCodeData = await this.getZipCodeData(zipToEmit, "service-location");
// retrieve serviceability details
const serviceabilityDetails = await getServiceabilityDetails(
serviceZipCode,
null,
"service-location"
);
const shopQuestionPopUpData = {
serviceabilityDetails: serviceabilityDetails.data,
zipCodeData: this.zipCodeData,
shopProviderData: this.localShopProviderData,
selectedProviderNumber: this.localSelectedProviderNumber,
};
this.$emit("shop-selected", shopQuestionPopUpData);
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"
);
// retrieve serviceability details
const serviceabilityDetails = await getServiceabilityDetails(
serviceZipCode,
null,
"service-location"
);
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: {

View file

@ -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,
};
},