Merge branch 'release/2025.07.31' into feature/CASH-845-stage-6

This commit is contained in:
scottkiener-at-safelite 2025-07-17 15:49:56 -04:00 committed by GitHub
commit 5a8bc3ef56
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: { methods: {
focusSearchInput() { focusSearchInput(event) {
// Focus cursor in input when search icon is clicked // Focus cursor in input when search icon is clicked
const field = document.querySelector("input"); const field = document.querySelector("input");
field.focus(); field.focus();
this.$emit("search-icon-click"); this.$emit("search-icon-click", event);
}, },
async imageChanged(e) { async imageChanged(e) {
let file = e.target.files[0]; let file = e.target.files[0];

View file

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

View file

@ -1,8 +1,4 @@
import { import { getPricedMobileFeePart, getServiceabilityDetails } from "./service-location-helper";
getPricedMobileFeePart,
getServiceabilityDetails,
getAvailabilityRating,
} from "./service-location-helper";
import { storeActions } from "@/constants/store-actions"; import { storeActions } from "@/constants/store-actions";
jest.mock("@/store", () => ({ jest.mock("@/store", () => ({
@ -320,38 +316,4 @@ describe("service-location-helper.js", () => {
expect(result).toEqual(expected); 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 alert from "@/ux-components/alert/alert";
import serviceZipModalQuestion from "@/layouts/service-location/service-zip-modal-question/service-zip-modal-question"; 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 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 shopQuestionPopup from "@/layouts/service-location/shop-question/shop-question-popup";
import buttonQuestion from "@/digital-components/button-question/button-question.vue"; 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 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 funnelHeader from "@/fmg-components/funnel-header/funnel-header";
import navbar from "@/fmg-components/nav-bar/nav-bar"; 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"; import textBlock from "@/digital-components/text-block/text-block";
// Supporting files // Supporting files
import baseMixin from "@/mixins/base-mixin.js";
import experimentMixin from "@/mixins/experiment-mixin.js"; import experimentMixin from "@/mixins/experiment-mixin.js";
import { experimentSettings } from "@/constants/experiments"; import { experimentSettings } from "@/constants/experiments";
import { AppointmentTypeStrings } from "@/constants/schedule-constants"; import { AppointmentTypeStrings } from "@/constants/schedule-constants";
@ -450,17 +447,7 @@ export default {
return this.displayNoShopsAlert || !this.isMobileAddressValid; return this.displayNoShopsAlert || !this.isMobileAddressValid;
}, },
additionalButtonData() { 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 { return {
availabilityRatingCallback: getAvailabilityRating,
startDate: formattedStartDate,
endDate: formattedEndDate,
shopAppointmentType: this.selectedAppointmentType, shopAppointmentType: this.selectedAppointmentType,
}; };
}, },

View file

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

View file

@ -58,15 +58,22 @@ export default {
beforeMount() { beforeMount() {
if (this.displayAvailabilityIndicators) { if (this.displayAvailabilityIndicators) {
this.displayLoader(); this.displayLoader();
const startDate = this.additionalButtonData.startDate; const startDate = new Date();
const endDate = this.additionalButtonData.endDate; 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; const shopAppointmentType = this.additionalButtonData.shopAppointmentType;
this.additionalButtonData this.getAvailabilityRating(
.availabilityRatingCallback(startDate, endDate, shopAppointmentType, this.value) formattedStartDate,
.then((data) => { formattedEndDate,
this.availabilityRating = data; shopAppointmentType,
}); this.value
).then((data) => {
this.availabilityRating = data;
});
} }
}, },
data() { data() {
@ -103,6 +110,29 @@ export default {
displayLoader() { displayLoader() {
this.isLoaderDisplayed = true; 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: { components: {
loader, loader,

View file

@ -1,6 +1,6 @@
<template> <template>
<transition name="fade" mode="out-in"> <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"> <div class="modal-link">
<text-link <text-link
linkType="text" linkType="text"
@ -13,20 +13,17 @@
<modal <modal
ref="shopQuestionModal" ref="shopQuestionModal"
:headerText="modalHeaderText" :headerText="modalHeaderText"
:onModalOpenedCallback="onModalOpened" :onModalOpenedCallback="updateSelectedAnswer"
:onModalClosedCallback="onModalClosed" :onModalClosedCallback="resetZipCodeData"
:footerButtonText="modalFooterText" :footerButtonText="modalFooterText"
:footerButtonDisabled=" :footerButtonDisabled="isModalFooterButtonDisabled"
displayInvalidZipAlert || displayNoShopsAlert || !selectedProviderNumber
"
@footer-button-event="setZipCodeAndShop"> @footer-button-event="setZipCodeAndShop">
<form @submit.prevent> <form @submit.prevent>
<serviceZipQuestion <serviceZipQuestion
ref="serviceZipQuestion" ref="serviceZipQuestion"
class="shop-question-zipcode" class="shop-question-zipcode"
customInputId="serviceZipCode" customInputId="serviceZipCode"
v-model="internalModel.zipCode" v-model="localZipCode"
v-on="{ 'textboxQuestionEvent.inputIdAssigned': onInputIdAssigned }"
cmsWidgetName="ServiceZipQuestionWidget" cmsWidgetName="ServiceZipQuestionWidget"
@search-icon-click="onSearchZip" @search-icon-click="onSearchZip"
@keydown.enter="onSearchZip" @keydown.enter="onSearchZip"
@ -57,9 +54,9 @@
:answers="answers" :answers="answers"
groupName="chooseShop" groupName="chooseShop"
textPosition="text-start" textPosition="text-start"
v-model="selectedProviderNumber" v-model="localSelectedProviderNumber"
isRequired isRequired
:validationRules="this.selectedProviderNumber ? '' : 'option-required'" /> :validationRules="this.localSelectedProviderNumber ? '' : 'option-required'" />
<div class="show-more-shops-link"> <div class="show-more-shops-link">
<textLink <textLink
v-if="displaySeeMoreLocationsLink" v-if="displaySeeMoreLocationsLink"
@ -69,7 +66,7 @@
linkType="text" linkType="text"
:text="showMoreShopsLinkText" :text="showMoreShopsLinkText"
href="#!" href="#!"
@click-event="getNextShopsFromList(3)" @click-event="updateShopListWithNextShops()"
:aria-label="showMoreShopsLinkText" /> :aria-label="showMoreShopsLinkText" />
</div> </div>
</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 buttonQuestion from "@/digital-components/button-question/button-question.vue";
import alert from "@/ux-components/alert/alert.vue"; import alert from "@/ux-components/alert/alert.vue";
import shopListButton from "./shop-list-button/shop-list-button"; import shopListButton from "./shop-list-button/shop-list-button";
import loadingModal from "@/fmg-components/loading-modal/loading-modal.vue";
// Supporting files // Supporting files
import { defineRule } from "vee-validate"; import { defineRule } from "vee-validate";
import { required } from "@/helpers/validation-rules"; import { required } from "@/helpers/validation-rules";
import { errorMessages } from "@/constants/error-messages"; import { errorMessages } from "@/constants/error-messages";
import baseMixin from "@/mixins/base-mixin.js"; 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 { AppointmentTypeStrings } from "@/constants/schedule-constants";
import { import {
getPricedMobileFeePart,
getPricedRecycleFeePart,
getServiceabilityDetails, getServiceabilityDetails,
getBillToAccountNumber,
getClosestApplicableShops, getClosestApplicableShops,
getShopProviderData, getShopProviderData,
getZipCodeData,
} from "@/layouts/service-location/helpers/service-location-helper/service-location-helper"; } from "@/layouts/service-location/helpers/service-location-helper/service-location-helper";
defineRule("option-required", required(errorMessages.OPTION_REQUIRED)); defineRule("option-required", required(errorMessages.OPTION_REQUIRED));
defineRule("zip-format", regex(/(^\d{5}$)|(^\d{5}-\d{4}$)/, errorMessages.SERVICE_ZIP_FORMAT));
export default { export default {
name: "shop-question-popup", name: "shop-question-popup",
mixins: [baseMixin], mixins: [baseMixin],
data() { data() {
return { return {
isShopQuestionDisplayed: true, localZipCode: this.zipCodeFromParent,
internalModel: this.copyModel(this.modelValue),
answers: [],
shopListButton: shopListButton, shopListButton: shopListButton,
shopIndex: 0, numberOfShopsToDisplay: 0,
displaySeeMoreLocationsLink: false, localShopProviderData: this.shopProviderDataFromParent,
serviceZipCodeTextInputId: "", localSelectedProviderNumber: null,
localShopProviderData: this.shopProviderData, zipCodeData: null,
selectedProviderNumber: null,
isLoadingShops: false,
lastSearchedZip: "",
lastSuccessfulZip: "",
hasLoadedShopData: false,
displayInvalidZipAlert: false,
areShopsAvialable: true,
displayNoShopsAlert: false,
}; };
}, },
props: { props: {
appointmentType: String, shopProviderDataFromParent: Object,
shopProviderData: Object, zipCodeFromParent: {
modelValue: {
type: Object,
default: () => ({
state: "",
zipCode: "",
}),
},
isDisplayed: Boolean,
modelWidgetName: {
type: String,
required: true,
},
isServiceableInshop: {
type: Boolean,
default: false,
},
zipCode: {
type: String, type: String,
default: "", default: "",
}, },
preSelectedProviderNumber: { selectedProviderNumberFromParent: {
type: String, type: String,
default: null, default: null,
}, },
@ -167,14 +126,20 @@ export default {
shopQuestionLinkText() { shopQuestionLinkText() {
return this.getCmsContent("ChangeMyLocationLinkWidget", "Text"); return this.getCmsContent("ChangeMyLocationLinkWidget", "Text");
}, },
modalName() {
return this.modelWidgetName;
},
modalHeaderText() { modalHeaderText() {
return this.getCmsContent("ShopQuestionWidget", "QuestionText"); return this.getCmsContent("ShopQuestionWidget", "QuestionText");
}, },
selectShopText() { modalFooterText() {
return this.getCmsContent("YourSafeliteShopWidget", "QuestionText"); 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() { modal() {
return this.$refs.shopQuestionModal; return this.$refs.shopQuestionModal;
@ -182,79 +147,32 @@ export default {
shopProviders() { shopProviders() {
return this.localShopProviderData?.shopProviders ?? []; return this.localShopProviderData?.shopProviders ?? [];
}, },
modalFooterText() {
return this.getCmsContent("SaveLocationWidget", "Text");
},
selectedValue: {
get: function () {
return this.modelValue;
},
set: function (newValue) {},
},
additionalButtonData() { 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 { return {
availabilityRatingCallback: getAvailabilityRating, // Used for availability indicators, could be refactored out of any availability indicator
startDate: formattedStartDate, // logic if/when we are sure we'll never have separate drop off/ in shop logic
endDate: formattedEndDate, shopAppointmentType: AppointmentTypeStrings.IN_SHOP_OR_DROP_OFF,
shopAppointmentType: this.appointmentType,
}; };
}, },
showMoreShopsLinkText() { displaySeeMoreLocationsLink() {
return this.getCmsContent("ShowMoreShopsLinkWidget", "Text"); if (!this.shopProviders.length) return false;
return this.numberOfShopsToDisplay < this.shopProviders.length;
}, },
}, isModalFooterButtonDisabled() {
methods: { return (
openModal(event) { this.displayInvalidZipAlert ||
if (event) event.preventDefault(); this.displayNoShopsAlert ||
if ( !this.localSelectedProviderNumber
!this.shopProviderData || );
!this.shopProviderData.shopProviders ||
!this.shopProviderData.shopProviders.length
) {
// Optionally show a loading message or disable the link
return;
}
this.$refs.shopQuestionModal.openModal();
}, },
getSelectedProviderObject(providerNumber) { answers() {
const provider = const updatedProvidersToDisplay = this.shopProviders.slice(
this.shopProviders?.find((provider) => provider.providerNumber == providerNumber) ?? 0,
new Provider(); this.numberOfShopsToDisplay
);
return provider; return updatedProvidersToDisplay.map((shopProvider) => {
}, const streetAddress = this.toTitleCase(shopProvider.address.streetAddress);
async getNextShopsFromList(numberToGet = 3) { const city = this.toTitleCase(shopProvider.address.city);
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);
const state = shopProvider.address.state; const state = shopProvider.address.state;
const zipCode = shopProvider.address.zipCode; const zipCode = shopProvider.address.zipCode;
const distanceInMiles = Math.round(shopProvider.distanceInMiles * 2) / 2; const distanceInMiles = Math.round(shopProvider.distanceInMiles * 2) / 2;
@ -267,354 +185,130 @@ export default {
value: shopProvider.providerNumber, 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(); methods: {
if (selectedShopIndex >= 3) { openModal() {
await this.getNextShopsFromList(selectedShopIndex + 1); // Reset local state from parent
} else { this.zipCodeData = null;
await this.getNextShopsFromList(); this.localShopProviderData = this.shopProviderDataFromParent;
await nextTick(); this.localZipCode = this.zipCodeFromParent;
} // This will be set once the modal is open (updateSelectedAnswer()) to ensure that the
}, // correct answer is selected
copyModel(modelToCopy) { this.localSelectedProviderNumber = null;
return {
state: modelToCopy.state, const selectedProviderNumberFromParentIndex = this.shopProviders.findIndex(
zipCode: modelToCopy.zipCode, (provider) => provider.providerNumber == this.selectedProviderNumberFromParent
};
},
resetAnswers() {
this.answers = [];
this.shopIndex = 0;
},
onModalOpened() {
const preSelectedIndex = this.shopProviders.findIndex(
(provider) => provider.providerNumber == this.preSelectedProviderNumber
); );
if (preSelectedIndex > -1) { if (selectedProviderNumberFromParentIndex > -1) {
this.shopIndex = Math.ceil((preSelectedIndex + 1) / 3) * 3; this.numberOfShopsToDisplay =
Math.ceil((selectedProviderNumberFromParentIndex + 1) / 3) * 3;
} else { } else {
this.shopIndex = 3; this.numberOfShopsToDisplay = 3;
} }
this.internalModel = this.copyModel(this.modelValue);
this.internalModel.zipCode = this.zipCode; this.updateShopsForZip();
this.localShopProviderData = this.shopProviderData; this.$refs.shopQuestionModal.openModal();
this.answers = []; },
this.onSearchZip(); updateSelectedAnswer() {
this.focusOnZipInput(); this.localSelectedProviderNumber = this.selectedProviderNumberFromParent;
this.selectedProviderNumber = this.preSelectedProviderNumber; },
if ( toTitleCase(str) {
(!this.selectedProviderNumber || return str.replace(/\w\S*/g, function (txt) {
!this.shopProviders.some( return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
(p) => p.providerNumber == this.selectedProviderNumber });
)) && },
this.shopProviders.length > 0 updateShopListWithNextShops() {
) { //gaAction = this.GaActions.SHOPS_FIRST_DISPLAYED; --This needs to actually fire on initial display, can never happen here
this.selectedProviderNumber = this.shopProviders[0].providerNumber; 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() { closeModal() {
this.modal.closeModal(); this.modal.closeModal();
}, },
onModalClosed() {
this.internalModel = this.copyModel(this.modelValue);
this.resetsOnZipInput();
},
resetsOnZipInput() {
this.resetAlerts();
},
async onSearchZip(event) { async onSearchZip(event) {
if (event) { event.preventDefault();
event.preventDefault(); this.zipCodeData = await this.getZipCodeData(this.localZipCode, "service-location");
event.stopPropagation(); // Add zipCode to zipCodeData as it does not come back from endpoint
} this.zipCodeData.zipCode = this.localZipCode;
this.resetsOnZipInput(); if (this.zipCodeData.isValid) {
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 {
if (this.isVehicleHeavyTruck) { if (this.isVehicleHeavyTruck) {
// check the location endpoint to verify this zip can service a heavy truck // check the location endpoint to verify this zip can service a heavy truck
const closestShops = await getClosestApplicableShops( const closestShops = await getClosestApplicableShops(
this.internalModel.zipCode, this.localZipCode,
this.carId, this.carId,
"service-location" "service-location"
); );
if (!closestShops || closestShops.providers?.length === 0) { if (!closestShops || closestShops.providers?.length === 0) {
this.displayNoServiceAlert = true; // We need an alert for heavy trucks
this.focusOnZipInput();
this.resetModalButtonStyle();
return null; return null;
} }
} else { } else {
getShopProviderData(zip).then(async (result) => { getShopProviderData(this.localZipCode).then(async (result) => {
if (this.displayNoShopsAlert === true) {
this.displayNoShopsAlert = false;
}
this.localShopProviderData = result.data; this.localShopProviderData = result.data;
this.updateShopsForZip(this.internalModel.zipCode); this.numberOfShopsToDisplay = 3;
if (this.appointmentType === AppointmentTypeStrings.MOBILE) { this.updateShopsForZip();
this.selectedProvider = new Provider(
this.shopProviderData.mobileProviderNumber
);
} else {
this.selectedProvider = new Provider();
}
}); });
} }
} }
}, },
async updateShopsForZip(zipCode) { updateShopsForZip() {
if (!this.appointmentType) { if (!this.shopProviders.length) {
this.answers = [];
this.isLoadingShops = false;
return; 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 ( if (
this.selectedProviderNumber && !this.answers.some(
!this.answers.some((a) => String(a.value) === String(this.selectedProviderNumber)) (a) => String(a.value) === String(this.localSelectedProviderNumber)
)
) { ) {
this.selectedProviderNumber = null; this.localSelectedProviderNumber = null;
} }
this.shopIndex = mappedData.length; // I think we should call our new pushGA Event here with new mapped data
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;
}, },
async setZipCodeAndShop() { async setZipCodeAndShop() {
if (
this.displayInvalidZipAlert ||
this.displayNoShopsAlert ||
!this.selectedProviderNumber
) {
return;
}
const zipToEmit = this.lastSearchedZip;
const selectedProvider = this.shopProviders.find( 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.closeModal();
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;
},
}, },
}, },
components: { components: {

View file

@ -49,8 +49,6 @@ import { errorMessages } from "@/constants/error-messages";
import baseMixin from "@/mixins/base-mixin.js"; import baseMixin from "@/mixins/base-mixin.js";
import { nextTick } from "vue"; 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 { Provider } from "@/layouts/service-location/classes/provider";
import { AppointmentTypeStrings } from "@/constants/schedule-constants"; import { AppointmentTypeStrings } from "@/constants/schedule-constants";
@ -109,17 +107,7 @@ export default {
return this.getCmsContent("ShowMoreShopsLinkWidget", "Text"); return this.getCmsContent("ShowMoreShopsLinkWidget", "Text");
}, },
additionalButtonData() { 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 { return {
availabilityRatingCallback: getAvailabilityRating,
startDate: formattedStartDate,
endDate: formattedEndDate,
shopAppointmentType: this.selectedAppointmentType, shopAppointmentType: this.selectedAppointmentType,
}; };
}, },