DigitalConsumer.ISS/src/layouts/schedule-page/service-location/service-location.vue
Bill Richardson c6d4cf32ca readability
null means car doesnt check recal
2026-03-31 14:13:43 -04:00

660 lines
26 KiB
Vue

<template>
<div class="service-location">
<alert
v-if="displayBigTruckNoShops"
ref="alertBigTruckNoShops"
cmsWidgetName="AlertBigTruckNoShopsWidget"
alertClass="alert-warning" />
<div class="appointment-type">
<div class="appointment-type-question-text d-flex">
<span
v-if="recalibrationRequired"
class="recal-text">{{ scheduleRecalText }}</span>
<span
v-if="!requiresInshopRecalibration">{{ appointmentQuestionText }}</span>
</div>
<alert
v-if="displayRecalibrationWarning"
ref="alertRecalNoMobile"
cmsWidgetName="AlertRecalNoMobileWidget"
alertClass="alert-warning"
@textLinkClicked="openModalAction" />
<alert
v-if="displayServiceableInshopOnly"
ref="alertInshopOnly"
cmsWidgetName="AlertInshopOnlyWidget"
:manualCopy="inShopOnlyCopy"
:isCollapsible="true"
alertClass="alert-warning" />
<alert
v-if="displayServiceableMobileOnly"
ref="alertMobileOnly"
cmsWidgetName="AlertMobileOnlyWidget"
:manualCopy="mobileOnlyCopy"
:isCollapsible="true"
alertClass="alert-warning" />
<appointmentTypeQuestion
v-if="!requiresInshopRecalibration && isServiceableMobile"
ref="appointmentTypeQuestion"
v-model="selectedAppointmentType"
groupName="appointmentTypeQuestion"
cmsWidgetName="AppointmentTypeQuestionWidget"
validationRules="option-required" />
</div>
<div
v-if="isInshop">
<alert
v-if="displayLowAvailabilityInshop"
ref="alertLowAvailabilityInshop"
cmsWidgetName="AlertLowAvailabilityInshopWidget"
:manualHeadlineLineTwo="getLowAvailabilityHeadlineLineTwoText"
:isCollapsible="true"
:startCollapsed="true"
alertClass="alert-warning">
<template #headline-line-two>
<button
class="m-0 alert-header-text d-block looka-likea-link"
type="button"
@click="triggerShopAddressModal">
<span class="m-0">
{{ getLowAvailabilityHeadlineLineTwoText }}
</span>
</button>
</template>
</alert>
<shopAddress
ref="shopAddress"
v-model="selectedProvider"
:serviceZipcode="zipCode"
:selectedAppointmentType="selectedAppointmentType"
modalWidgetName="ChangeLocationModalWidget"
cmsWidgetName="ShopAddressWidget"
@zipUpdated="handleInShopZipUpdated" />
<!-- INTEGRATION TODO: Put this right under the calendar -->
<alert
v-if="selectedProvider === null"
ref="alertNoAvailableShops"
cmsWidgetName="AlertNoAvailableShopsWidget"
:isCollapsible="false"
alertClass="alert-danger" />
</div>
<div
v-if="isMobile">
<div class="expandable-link-container">
<a
v-if="displayMilitaryZipAlert && !militaryBaseWarningExpanded"
href="#"
class="expandable-link"
@click.prevent="militaryBaseWarningExpanded = true">
{{ militaryBaseWarningLinkText }}
</a>
<div
v-if="displayMilitaryZipAlert && militaryBaseWarningExpanded"
class="expandable-link-text">
{{ militaryBaseWarningText }}
</div>
</div>
<serviceZipQuestion
ref="mobileServiceZipCodeQuestion"
customInputId="mobileServiceZipCode"
class="mobile-service-zip-question"
:parentZipCode="mobileZipCode"
:placeholderText="mobileZipPlaceholder"
:serviceZipFormatErrorMessage="errorMessages.MOBILE_SERVICE_ZIP_FORMAT"
:hasError="mobileZipError !== ''"
cmsWidgetName="MobileZipWidget"
@serviceZipCodeUpdated="handleServiceZipCodeChanged" />
<div
v-if="mobileZipError"
ref="errorMessageDiv"
class="row my-1 form-test-error">
<span
class="d-inline-flex mt-0"
role="alert">{{ mobileZipError }}</span>
</div>
<textBlock
v-if="displayMobileFeeDisclaimer"
cmsWidgetName="MobileFeeDisclaimerWidget"
typeStyle="disclaimer" />
</div>
<recalModal
:ref="RECAL_MODAL_REF_NAME"
cmsWidgetName="RecalModal" />
</div>
</template>
<script>
// Import Supporting Files
import { AppointmentTypeStrings } from '@/constants/schedule-constants.js';
import errorMessages from '@/constants/error-messages';
import { useMainStore } from '@/store';
import showIssLoadingModal from '@/helpers/loading-modal-helper.js';
import {
getZipCodeData,
getMobileZipCodeData
} from '@/helpers/service-location-helper';
import { toTitleCase } from '@/helpers/text-helper.js';
// Import Component
import alert from '@/ux-components/alert/alert.vue';
import textBlock from '@/digital-components/text-block/text-block.vue';
import appointmentTypeQuestion from '@/layouts/schedule-page/service-location/appointment-type-question/appointment-type-question.vue';
import baseFormMixin from '@/mixins/base-form-mixin';
import shopAddress from '@/layouts/schedule-page/service-location/shop-address/shop-address.vue';
import serviceZipQuestion from '@/layouts/schedule-page/service-location/service-zip-question/service-zip-question.vue';
import widgetFields from '@/constants/cms-widget-fields';
import recalModal from '@/iss-components/recal-modal/recal-modal.vue';
const RECAL_MODAL_REF_NAME = 'RecalModal';
export default {
name: 'service-location',
components: {
alert,
textBlock,
appointmentTypeQuestion,
shopAddress,
serviceZipQuestion,
recalModal
},
mixins: [baseFormMixin],
emits: ['appointment-type-changed', 'city-updated', 'clear-mobile-data', 'in-shop-zip-updated', 'mobile-zip-updated', 'provider-changed'],
props: {
schedulingInshopAvailabilityRating: {
type: String,
default: 'high'
}
},
setup() {
const mainStore = useMainStore();
return { mainStore };
},
data() {
return {
streetAddress: this.getServiceAddressFromStore(),
streetAddress2: this.getServiceAddress2FromStore(),
city: this.getServiceCityFromStore(),
state: this.getServiceStateFromStore(),
zipCode: this.getServiceZipCodeFromStore(),
initializingData: false,
isGlassServiceableInshop: null,
isRecalibrationServiceableInshop: null,
isGlassServiceableMobile: null,
isRecalibrationServiceableMobile: null,
selectedAppointmentType: this.getSelectedAppointmentType(),
selectedProvider: this.getSelectedProvider(),
mobileFeePart: null,
mobileProviderNumber: null,
zipContainsMilitaryBase: false,
zipCodeCtu: null,
mobileZipCode: '',
mobileZipError: '',
militaryBaseWarningExpanded: false,
availabilityRating: null,
RECAL_MODAL_REF_NAME,
errorMessages
};
},
computed: {
appointmentQuestionText() {
return this.getCmsContent('WhereWouldYouLikeServiceWidget', widgetFields.CONTENT_GROUP_WIDGET.HEADER_TEXT);
},
displayBigTruckNoShops() {
return this.isBigTruck && !this.isServiceableInshop && !this.isServiceableMobile;
},
displayLowAvailabilityInshop() {
return this.availabilityRating === 'low' && this.isInshop && this.selectedProvider !== null;
},
displayMilitaryZipAlert() {
return this.zipContainsMilitaryBase && this.isServiceableMobile;
},
displayMobileFeeDisclaimer() {
return this.mainStore.isNoComp || this.mainStore.isITAC;
},
displayRecalibrationWarning() {
return this.requiresInshopRecalibration;
},
displayServiceableInshopOnly() {
return !this.isServiceableMobile && this.isServiceableInshop && !this.displayRecalibrationWarning;
},
displayServiceableMobileOnly() {
return this.isServiceableMobile && this.recalibrationRequired && !this.isServiceableInshop;
},
getLowAvailabilityHeadlineLineTwoText() {
return 'Try a different location';
},
inShopOnlyCopy() {
let content = this.getCmsContent('AlertInshopOnlyWidget', widgetFields.ALERT_WIDGET.BODY_TEXT);
content = content.replace('{custom:city}', toTitleCase(this.city));
return content;
},
isBigTruck() {
return this.mainStore.order.vehicle.isBigTruck;
},
isInshop() {
return (
this.selectedAppointmentType === AppointmentTypeStrings.IN_SHOP
|| this.selectedAppointmentType === AppointmentTypeStrings.DROP_OFF
);
},
isMobile() {
return this.selectedAppointmentType === AppointmentTypeStrings.MOBILE
|| this.selectedAppointmentType === AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP;
},
isServiceableInshop() {
if (this.isRecalibrationServiceableInshop !== null) {
return (this.isGlassServiceableInshop && this.isRecalibrationServiceableInshop);
}
return this.isGlassServiceableInshop;
},
isServiceableMobile() {
if (this.isBigTruck) {
return false;
}
if (this.isRecalibrationServiceableMobile === true || this.isRecalibrationServiceableMobile === false) {
return (
this.isGlassServiceableMobile
&& this.isRecalibrationServiceableMobile
);
}
return this.isGlassServiceableMobile;
},
militaryBaseWarningLinkText() {
return this.getCmsContent('AlertMilitaryBaseZipWidget', widgetFields.ALERT_WIDGET.HEADLINE_TEXT);
},
militaryBaseWarningText() {
return this.getCmsContent('AlertMilitaryBaseZipWidget', widgetFields.ALERT_WIDGET.BODY_TEXT);
},
mobileOnlyCopy() {
let content = this.getCmsContent('AlertMobileOnlyWidget', widgetFields.ALERT_WIDGET.BODY_TEXT);
content = content.replace('{custom:city}', toTitleCase(this.city));
return content;
},
mobileZipPlaceholder() {
return this.getCmsContent('MobileZipPlaceHolderWidget', widgetFields.TEXT_BLOCK_WIDGET.TEXT);
},
recalibrationRequired() {
return this.mainStore.hasRecalibrationPart;
},
requiresInshopRecalibration() {
// Specifically check for isRecalibrationServiceableMobile === false, not null or true.
return (
this.isServiceableInshop
&& this.isGlassServiceableMobile
&& this.isRecalibrationServiceableMobile === false
);
},
scheduleRecalText() {
if (this.requiresInshopRecalibration) {
return this.getCmsContent('WhereWouldYouLikeServiceWidget', widgetFields.CONTENT_GROUP_WIDGET.BODY_TEXT_2);
}
return this.getCmsContent('WhereWouldYouLikeServiceWidget', widgetFields.CONTENT_GROUP_WIDGET.BODY_TEXT);
}
},
watch: {
city(newCity, oldCity) {
if (newCity !== oldCity) {
this.$emit('city-updated', newCity);
}
},
async mobileZipCode(newZip, oldZip) {
if (newZip !== '' && newZip !== oldZip && !this.initializingData) {
await this.processMobileZipCodeChange();
}
},
schedulingInshopAvailabilityRating(newRating) {
this.availabilityRating = newRating;
},
selectedAppointmentType(newValue, oldValue) {
if (this.isInshop && this.selectedProvider && this.availabilityRating === null) {
const appointmentTypeServiceLocationObj = {
appointmentType: newValue,
mobileProviderNumber: null,
provider: this.selectedProvider,
refreshDatePicker: oldValue !== newValue && oldValue !== null,
resetDatePicker: oldValue !== newValue || oldValue === null,
zipCode: this.zipCode,
zipCodeCtu: this.zipCodeCtu
};
this.$emit('appointment-type-changed', appointmentTypeServiceLocationObj);
} else {
const appointmentTypeServiceLocationObj = {
appointmentType: newValue,
mobileProviderNumber: this.mobileProviderNumber,
provider: null,
refreshDatePicker: false,
resetDatePicker: true,
zipCode: this.zipCode,
zipCodeCtu: this.zipCodeCtu
};
this.$emit('appointment-type-changed', appointmentTypeServiceLocationObj);
}
},
selectedProvider(newProvider, oldProvider) {
if (newProvider?.providerNumber !== oldProvider?.providerNumber || this.availabilityRating === null) {
const appointmentIsInshop = this.isInshop;
const returnedProvider = {
provider: newProvider,
refreshDatePicker: appointmentIsInshop && oldProvider?.providerNumber !== null
};
if (newProvider?.providerNumber !== oldProvider?.providerNumber) {
this.$emit('provider-changed', returnedProvider);
}
}
}
},
methods: {
async initializeComponent(initialData) {
this.initializingData = true;
await this.setData(initialData);
this.initializingData = false;
},
async forwardButtonAction(appointmentType) {
let provider = this.selectedProvider;
this.mainStore.updateMobileFee(null);
if (this.isMobile) {
if (this.mainStore.isNoComp || this.mainStore.isITAC) {
this.mainStore.updateMobileFee(this.mobileFeePart);
}
provider = {
providerNumber: this.mobileProviderNumber,
address: {
streetAddress: null,
city: null,
state: null,
zipCode: null,
zipCodeCtu: null
}
};
}
this.mainStore.updateServiceLocation({
state: this.state,
zipCode: this.zipCode,
zipCodeCtu: this.zipCodeCtu,
appointmentType: appointmentType || this.selectedAppointmentType,
provider
});
},
openModalAction(modalName) {
this.$refs[modalName].openModal();
},
resetDependentState() {},
getServiceAddressFromStore() {
return useMainStore().order.serviceLocation.address;
},
getServiceAddress2FromStore() {
return useMainStore().order.serviceLocation.address2;
},
getServiceCityFromStore() {
return useMainStore().order.serviceLocation.city;
},
getServiceStateFromStore() {
return (
useMainStore().order.serviceLocation.state
|| useMainStore().order.customer.address.state
);
},
getServiceZipCodeFromStore() {
return (
useMainStore().order.serviceLocation.zipCode
|| useMainStore().order.customer.address.zipCode
);
},
getSelectedAppointmentType() {
return useMainStore().order.serviceLocation.appointmentType;
},
getSelectedProvider() {
return useMainStore().order.serviceLocation.provider;
},
async handleInShopZipUpdated(newZip) {
this.zipCode = newZip;
const zipCodeData = await getZipCodeData(this.zipCode);
this.city = zipCodeData.city;
this.state = zipCodeData.state;
this.setCtuForMobile(zipCodeData.zipCodeCtu);
this.setContainsMilitaryBase(zipCodeData.containsMilitaryBase);
const serviceabilityDetailsPromise = this.mainStore.getServiceabilityDetails(this.zipCode);
await serviceabilityDetailsPromise.then((result) => {
const details = result.data;
if (details) {
this.setServiceabilityDetails(details);
}
const inShopZipServiceLocationObj = {
appointmentType: AppointmentTypeStrings.IN_SHOP,
mobileProviderNumber: null,
provider: this.selectedProvider,
refreshDatePicker: false,
resetDatePicker: false,
zipCode: newZip,
zipCodeCtu: zipCodeData.zipCodeCtu
};
this.$emit('in-shop-zip-updated', inShopZipServiceLocationObj);
});
},
async handleServiceZipCodeChanged(serviceZipCodeFromQuestion) {
if (serviceZipCodeFromQuestion !== this.mobileZipCode) {
// Can probs just make this one call but have to think about initialization flow and not wanting to trigger extra calls on load
this.mobileZipCode = serviceZipCodeFromQuestion;
} else {
await this.processMobileZipCodeChange();
}
},
async processMobileZipCodeChange() {
showIssLoadingModal(true);
await this.updateMobileZip();
if (!this.mobileZipError) {
const updateMobileZipServiceLocationObj = {
mobileProviderNumber: this.mobileProviderNumber,
provider: null,
refreshDatePicker: true,
zipCode: this.zipCode,
zipCodeCtu: this.zipCodeCtu
};
this.$emit('mobile-zip-updated', updateMobileZipServiceLocationObj);
} else {
this.$emit('clear-mobile-data');
showIssLoadingModal(false);
}
},
setCtuForMobile(val) {
this.zipCodeCtu = val;
},
async setData(initialData) {
if (initialData.defaultMobileZipCode) {
this.mobileZipCode = initialData.defaultMobileZipCode;
}
if (initialData.zipCodeData) {
this.zipContainsMilitaryBase = initialData.zipCodeData.containsMilitaryBase;
this.zipCodeCtu = initialData.zipCodeData.zipCodeCtu;
this.city = initialData.zipCodeData.city;
this.state = initialData.zipCodeData.state;
}
if (initialData.serviceabilityDetails) {
this.setServiceabilityDetails(initialData.serviceabilityDetails);
}
if (initialData.mobileFeePart) {
this.mobileFeePart = initialData.mobileFeePart;
}
if (initialData.providers) {
this.setMobileProviderNumber(initialData.providers.mobileProviderNumber);
let foundMatch = false;
if (this.selectedProvider && this.selectedProvider.providerNumber) {
const matchedProvider = initialData.providers.shopProviders.find((provider) => provider.providerNumber === this.selectedProvider.providerNumber);
if (matchedProvider) {
foundMatch = true;
this.selectedProvider = matchedProvider;
}
}
if (initialData.providers.shopProviders.length > 0 && !foundMatch) {
this.selectedProvider = initialData.providers.shopProviders[0];
} else if (initialData.providers.shopProviders.length === 0) {
this.selectedProvider = null;
}
}
if (initialData.glassFees) {
useMainStore().getCombinedQuote(initialData.glassFees)
.then((combinedQuote) => {
this.setGlassFeeItems(combinedQuote);
});
}
},
setContainsMilitaryBase(val) {
if (this.zipContainsMilitaryBase !== val) {
this.zipContainsMilitaryBase = val;
}
},
setGlassFeeItems(newGlassFeeItems) {
this.mainStore.updateGlassFees(newGlassFeeItems);
},
setMobileProviderNumber(providerNumber) {
this.mobileProviderNumber = providerNumber;
},
setServiceabilityDetails(serviceabilityDetails) {
this.isGlassServiceableInshop = serviceabilityDetails.isGlassServiceableInshop;
this.isRecalibrationServiceableInshop = serviceabilityDetails.isRecalibrationServiceableInshop;
this.isGlassServiceableMobile = serviceabilityDetails.isGlassServiceableMobile;
this.isRecalibrationServiceableMobile = serviceabilityDetails.isRecalibrationServiceableMobile;
if (!this.isServiceableMobile) {
this.selectedAppointmentType = AppointmentTypeStrings.IN_SHOP;
}
},
triggerShopAddressModal() {
this.$refs.shopAddress.openModal();
},
async updateMobileZip() {
this.mobileZipError = '';
const zipCodeData = await getMobileZipCodeData(this.mobileZipCode);
if (!zipCodeData.isValid) {
this.mobileZipError = errorMessages.INVALID_ZIP;
} else if (!zipCodeData.isServiceable) {
this.mobileZipError = errorMessages.NO_SERVICE_IN_AREA(toTitleCase(zipCodeData.city));
} else {
this.city = zipCodeData.city;
this.state = zipCodeData.state;
this.zipCode = this.mobileZipCode;
this.setCtuForMobile(zipCodeData.zipCodeCtu);
this.setContainsMilitaryBase(zipCodeData.containsMilitaryBase);
const serviceabilityDetailsPromise = this.mainStore.getServiceabilityDetails(this.mobileZipCode);
await serviceabilityDetailsPromise.then((result) => {
const details = result.data;
if (details) {
this.setServiceabilityDetails(details);
}
});
const providersPromise = useMainStore().getSafeliteProviders(this.mobileZipCode);
await providersPromise.then((result) => {
const providers = result.data;
if (providers) {
this.setMobileProviderNumber(providers.mobileProviderNumber);
if (providers.shopProviders.length > 0) {
this.selectedProvider = providers.shopProviders[0];
} else {
this.selectedProvider = null;
}
}
});
}
}
}
};
</script>
<style lang="scss" scoped>
$page-side-padding: 1.5rem;
.service-location {
> div {
margin-top: 1.25rem;
margin-bottom: 0;
}
.form-test-error {
font-weight: $font-weight-bold;
}
.appointment-type-question-text {
color: $black;
font-weight: 600;
font-size: 1rem;
line-height: 1.625rem;
flex-direction: column;
.recal-text {
font-weight: $font-weight-bold;
}
}
.mobile-service-zip-question {
:deep(.input-wrapper.has-text-button) {
button {
background-color: #1574a1;
}
}
:deep(.form-test-error span) {
font-weight: $font-weight-bold;
}
}
.expandable-link-container {
margin-bottom: 1rem;
}
.expandable-link {
font-style: italic;
font-weight: $font-weight-bold;
text-decoration: none;
color: $heritage-blue-primary;
line-height: 1.375rem;
&:hover {
text-decoration: underline;
}
}
div.alert {
margin-top: 1.25rem;
&.widget-name-AlertLowAvailabilityInshopWidget {
@include media-breakpoint-up(xs) {
:deep(.alert-headline-text-container) {
padding-left: 0.5rem;
padding-right: 0.375rem;
letter-spacing: 0.45px;
}
}
@include media-breakpoint-up(xl) {
:deep(.alert-headline-text-container) {
padding: 0rem 0.625rem;
}
}
}
button.looka-likea-link {
background: none;
border: none;
font-weight: 500;
font-size: 1rem;
padding: 0;
color: $heritage-blue-primary;
text-decoration: none;
cursor: pointer;
&:hover {
color: #125b7e;
text-decoration: underline;
}
}
}
:deep(.alert-shop-distance) {
margin-bottom: 0;
}
}
</style>