More alert/error/etc updates

This commit is contained in:
Alex Humphries 2026-01-16 12:20:52 -05:00
parent 253044d502
commit d6762e85a8
6 changed files with 119 additions and 57 deletions

View file

@ -26,7 +26,7 @@ const errorMessages = Object.freeze({
SERVICE_ZIP_REQUIRED: 'ZIP code is required', SERVICE_ZIP_REQUIRED: 'ZIP code is required',
SERVICE_ZIP_FORMAT: 'Invalid ZIP, please enter a new ZIP (5 characters)', SERVICE_ZIP_FORMAT: 'Invalid ZIP, please enter a new ZIP (5 characters)',
MOBILE_SERVICE_ZIP_FORMAT: 'Invalid ZIP code', MOBILE_SERVICE_ZIP_FORMAT: 'Invalid ZIP code',
NO_SERVICE_IN_ZIP: (zip) => `We're sorry, but we don't offer service in ${zip} at this time.`, NO_SERVICE_IN_AREA: (area) => `We're sorry, but we don't offer service in ${area} at this time.`,
PROVIDER_REQUIRED: 'Please select a provider.', PROVIDER_REQUIRED: 'Please select a provider.',
INVALID_ZIP: 'The ZIP you entered was invalid. Please enter a valid ZIP.', INVALID_ZIP: 'The ZIP you entered was invalid. Please enter a valid ZIP.',
ZIP_CODE_NOT_SERVICED_FOR_VEHICLE: 'We do not currently offer glass service for your vehicle in this ZIP code. Please try another ZIP code.', ZIP_CODE_NOT_SERVICED_FOR_VEHICLE: 'We do not currently offer glass service for your vehicle in this ZIP code. Please try another ZIP code.',

View file

@ -9,7 +9,7 @@
ref="buttonQuestion" ref="buttonQuestion"
v-model="selectedValues" v-model="selectedValues"
customButtonQuestionId="appointmentTypeQuestion" customButtonQuestionId="appointmentTypeQuestion"
:answers="answersFromCms" :answers="answersToDisplay"
:groupName="groupName" :groupName="groupName"
:suppressError="suppressError" :suppressError="suppressError"
:validationRules="validationRules" :validationRules="validationRules"
@ -45,8 +45,15 @@ export default {
return this.getCmsContent(this.cmsWidgetName, 'QuestionText'); return this.getCmsContent(this.cmsWidgetName, 'QuestionText');
}, },
answersFromCms() { answersFromCms() {
const retCms = this.getCmsContent(this.cmsWidgetName, 'Answers'); return this.getCmsContent(this.cmsWidgetName, 'Answers');
return retCms; },
answersToDisplay() {
return this.answersFromCms
? this.answersFromCms.filter((answer) => (
(answer.Name === AppointmentTypeStrings.IN_SHOP)
|| (answer.Name === AppointmentTypeStrings.MOBILE)
))
: [];
}, },
selectedValues: { selectedValues: {
get() { get() {

View file

@ -13,18 +13,6 @@
<siteSubHeader <siteSubHeader
cmsWidgetName="SiteSubHeaderWidget" cmsWidgetName="SiteSubHeaderWidget"
class="subheader" /> class="subheader" />
<alert
v-if="displayServiceableMobileOnly"
ref="alertMobileOnly"
class="my-5"
cmsWidgetName="AlertMobileOnlyWidget"
alertClass="alert-warning" />
<alert
v-if="displayServiceableInshopOnly"
ref="alertInshopOnly"
cmsWidgetName="AlertInshopOnlyWidget"
:isCollapsible="true"
alertClass="alert-warning" />
<alert <alert
v-if="displayRecalibrationWarning" v-if="displayRecalibrationWarning"
ref="alertRecalNoMobile" ref="alertRecalNoMobile"
@ -32,13 +20,6 @@
cmsWidgetName="AlertRecalNoMobileWidget" cmsWidgetName="AlertRecalNoMobileWidget"
alertClass="alert-warning" alertClass="alert-warning"
@text-link-clicked="openModalAction" /> @text-link-clicked="openModalAction" />
<alert
v-if="displayNoShopsAlert"
ref="alertNoShops"
class="my-5"
:isCollapsible="true"
cmsWidgetName="AlertNoShopsWidget"
alertClass="alert-warning" />
<alert <alert
v-if="displayBigTruckNoShops" v-if="displayBigTruckNoShops"
ref="alertBigTruckNoShops" ref="alertBigTruckNoShops"
@ -47,10 +28,25 @@
alertClass="alert-warning" /> alertClass="alert-warning" />
<div class="appointment-type"> <div class="appointment-type">
<div class="appointment-type-question-text d-flex"> <div class="appointment-type-question-text d-flex">
<span class="w-100">{{ appointmentQuestionText }}</span> <span class="w-100 recal-text" v-if="recalibrationRequired">{{ scheduleRecalText }}</span>
<span class="w-100" v-if="!requiresInshopRecalibration">{{ appointmentQuestionText }}</span>
</div> </div>
<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 <appointmentTypeQuestion
v-if="showMobileOption" v-if="!requiresInshopRecalibration && isServiceableMobile"
ref="appointmentTypeQuestion" ref="appointmentTypeQuestion"
v-model="selectedAppointmentType" v-model="selectedAppointmentType"
groupName="appointmentTypeQuestion" groupName="appointmentTypeQuestion"
@ -66,6 +62,12 @@
modalWidgetName="ChangeLocationModalWidget" modalWidgetName="ChangeLocationModalWidget"
cmsWidgetName="ShopAddressWidget" cmsWidgetName="ShopAddressWidget"
@zip-updated="handleInShopZipUpdated" /> @zip-updated="handleInShopZipUpdated" />
<alert
v-if="selectedProvider === null"
ref="alertNoAvailableShops"
cmsWidgetName="AlertNoAvailableShopsWidget"
:isCollapsible="true"
alertClass="alert-danger" />
</div> </div>
<div <div
v-if="isMobile"> v-if="isMobile">
@ -105,6 +107,7 @@
cmsWidgetName="MobileFeeDisclaimerWidget" cmsWidgetName="MobileFeeDisclaimerWidget"
typeStyle="disclaimer" /> typeStyle="disclaimer" />
</div> </div>
<!-- INTEGRATION TODO: Put this right under the calendar -->
<contentGroupModal <contentGroupModal
:ref="RECAL_MODAL_REF_NAME" :ref="RECAL_MODAL_REF_NAME"
cssModalHeadlineClass="text-center" cssModalHeadlineClass="text-center"
@ -136,6 +139,7 @@ import {
getZipCodeData, getZipCodeData,
getMobileZipCodeData getMobileZipCodeData
} from '@/helpers/service-location-helper'; } from '@/helpers/service-location-helper';
import { toTitleCase } from '@/helpers/text-helper.js';
// Import Component // Import Component
import alert from '@/ux-components/alert/alert.vue'; import alert from '@/ux-components/alert/alert.vue';
@ -232,7 +236,6 @@ export default {
city: this.getServiceCityFromStore(), city: this.getServiceCityFromStore(),
state: this.getServiceStateFromStore(), state: this.getServiceStateFromStore(),
zipCode: this.getServiceZipCodeFromStore(), zipCode: this.getServiceZipCodeFromStore(),
isVehicleProtected: this.getIsVehicleProtectedFromStore(),
isGlassServiceableInshop: null, isGlassServiceableInshop: null,
isRecalibrationServiceableInshop: null, isRecalibrationServiceableInshop: null,
isGlassServiceableMobile: null, isGlassServiceableMobile: null,
@ -245,7 +248,6 @@ export default {
zipCodeCtu: null, zipCodeCtu: null,
mobileZipCode: '', mobileZipCode: '',
mobileZipError: '', mobileZipError: '',
showMobileOption: false,
militaryBaseWarningExpanded: false, militaryBaseWarningExpanded: false,
RECAL_MODAL_REF_NAME, RECAL_MODAL_REF_NAME,
SITE_FOOTER_REF_NAME, SITE_FOOTER_REF_NAME,
@ -313,12 +315,11 @@ export default {
displayServiceableInshopOnly() { displayServiceableInshopOnly() {
return ( return (
!this.displayRecalibrationWarning !this.displayRecalibrationWarning
&& this.isServiceableInshop
&& !this.isServiceableMobile && !this.isServiceableMobile
); );
}, },
displayServiceableMobileOnly() { displayServiceableMobileOnly() {
return this.isServiceableMobile && !this.isServiceableInshop; return this.isServiceableMobile && !this.isServiceableInshop && this.displayRecalibrationWarning;
}, },
displayMobileFeeDisclaimer() { displayMobileFeeDisclaimer() {
return this.mainStore.isNoComp || this.mainStore.isITAC; return this.mainStore.isNoComp || this.mainStore.isITAC;
@ -342,7 +343,27 @@ export default {
return this.getCmsContent('AlertMilitaryBaseZipWidget', widgetFields.ALERT_WIDGET.BODY_TEXT); return this.getCmsContent('AlertMilitaryBaseZipWidget', widgetFields.ALERT_WIDGET.BODY_TEXT);
}, },
appointmentQuestionText() { appointmentQuestionText() {
return this.getCmsContent('AppointmentTypeQuestionWidget', widgetFields.INPUT_QUESTION_WIDGET.QUESTION_TEXT); return this.getCmsContent('WhereWouldYouLikeServiceWidget', widgetFields.CONTENT_GROUP_WIDGET.HEADER_TEXT);
},
scheduleRecalText() {
if(this.requiresInshopRecalibration) {
return this.getCmsContent('WhereWouldYouLikeServiceWidget', widgetFields.CONTENT_GROUP_WIDGET.BODY_TEXT_2);
} else {
return this.getCmsContent('WhereWouldYouLikeServiceWidget', widgetFields.CONTENT_GROUP_WIDGET.BODY_TEXT);
}
},
inShopOnlyCopy() {
let content = this.getCmsContent('AlertInshopOnlyWidget', widgetFields.ALERT_WIDGET.BODY_TEXT);
content = content.replace('{custom:city}', toTitleCase(this.city));
return content;
},
mobileOnlyCopy() {
let content = this.getCmsContent('AlertMobileOnlyWidget', widgetFields.ALERT_WIDGET.BODY_TEXT);
content = content.replace('{custom:city}', toTitleCase(this.city));
return content;
},
recalibrationRequired() {
return this.isRecalibrationServiceableMobile !== null;
} }
}, },
methods: { methods: {
@ -379,7 +400,6 @@ export default {
zipCode: this.zipCode, zipCode: this.zipCode,
zipCodeCtu: this.zipCodeCtu, zipCodeCtu: this.zipCodeCtu,
appointmentType: this.selectedAppointmentType, appointmentType: this.selectedAppointmentType,
isVehicleProtected: this.isVehicleProtected,
provider provider
}); });
@ -413,9 +433,6 @@ export default {
|| useMainStore().order.customer.address.zipCode || useMainStore().order.customer.address.zipCode
); );
}, },
getIsVehicleProtectedFromStore() {
return useMainStore().order.serviceLocation.isVehicleProtected;
},
getSelectedAppointmentType() { getSelectedAppointmentType() {
return useMainStore().order.serviceLocation.appointmentType; return useMainStore().order.serviceLocation.appointmentType;
}, },
@ -435,14 +452,11 @@ export default {
if (zipCodeData) { if (zipCodeData) {
this.zipContainsMilitaryBase = zipCodeData.containsMilitaryBase; this.zipContainsMilitaryBase = zipCodeData.containsMilitaryBase;
this.zipCodeCtu = zipCodeData.zipCodeCtu; this.zipCodeCtu = zipCodeData.zipCodeCtu;
this.city = zipCodeData.city;
} }
if (serviceabilityDetails) { if (serviceabilityDetails) {
this.setServiceabilityDetails(serviceabilityDetails); this.setServiceabilityDetails(serviceabilityDetails);
this.showMobileOption = this.isServiceableMobile;
if (!this.showMobileOption) {
this.selectedAppointmentType = AppointmentTypeStrings.IN_SHOP;
}
} }
if (mobileFeePart) { if (mobileFeePart) {
@ -464,6 +478,9 @@ export default {
if(providers.shopProviders.length > 0 && !foundMatch) { if(providers.shopProviders.length > 0 && !foundMatch) {
this.selectedProvider = providers.shopProviders[0]; this.selectedProvider = providers.shopProviders[0];
} }
else {
this.selectedProvider = null;
}
} }
if (glassFees) { if (glassFees) {
@ -485,13 +502,6 @@ export default {
setMobileProviderNumber(providerNumber) { setMobileProviderNumber(providerNumber) {
this.mobileProviderNumber = providerNumber; this.mobileProviderNumber = providerNumber;
}, },
resetMobileLocation() {
this.streetAddress = '';
this.streetAddress2 = '';
this.city = '';
this.isVehicleProtected = null;
},
setServiceabilityDetails(serviceabilityDetails) { setServiceabilityDetails(serviceabilityDetails) {
this.isGlassServiceableInshop = this.isGlassServiceableInshop =
serviceabilityDetails.isGlassServiceableInshop; serviceabilityDetails.isGlassServiceableInshop;
@ -501,6 +511,9 @@ export default {
serviceabilityDetails.isGlassServiceableMobile; serviceabilityDetails.isGlassServiceableMobile;
this.isRecalibrationServiceableMobile = this.isRecalibrationServiceableMobile =
serviceabilityDetails.isRecalibrationServiceableMobile; serviceabilityDetails.isRecalibrationServiceableMobile;
if (!this.isServiceableMobile) {
this.selectedAppointmentType = AppointmentTypeStrings.IN_SHOP;
}
}, },
async updateMobileZip() { async updateMobileZip() {
this.mobileZipError = ''; this.mobileZipError = '';
@ -510,8 +523,10 @@ export default {
if (!zipCodeData.isValid) { if (!zipCodeData.isValid) {
this.mobileZipError = errorMessages.INVALID_ZIP; this.mobileZipError = errorMessages.INVALID_ZIP;
} else if (!zipCodeData.isServiceable) { } else if (!zipCodeData.isServiceable) {
this.mobileZipError = errorMessages.NO_SERVICE_IN_ZIP(this.mobileZipCode); this.mobileZipError = errorMessages.NO_SERVICE_IN_AREA(toTitleCase(zipCodeData.city));
} else { } else {
console.log('zipCodeData:', zipCodeData);
this.city = zipCodeData.city;
this.zipCode = this.mobileZipCode; this.zipCode = this.mobileZipCode;
this.setCtuForMobile(zipCodeData.zipCodeCtu); this.setCtuForMobile(zipCodeData.zipCodeCtu);
this.setContainsMilitaryBase(zipCodeData.containsMilitaryBase); this.setContainsMilitaryBase(zipCodeData.containsMilitaryBase);
@ -532,6 +547,9 @@ export default {
if(providers.shopProviders.length > 0) { if(providers.shopProviders.length > 0) {
this.selectedProvider = providers.shopProviders[0]; this.selectedProvider = providers.shopProviders[0];
} }
else {
this.selectedProvider = null;
}
} }
}); });
} }
@ -539,6 +557,8 @@ export default {
async handleInShopZipUpdated(newZip) { async handleInShopZipUpdated(newZip) {
this.zipCode = newZip; this.zipCode = newZip;
const zipCodeData = await getZipCodeData(this.zipCode); const zipCodeData = await getZipCodeData(this.zipCode);
console.log('zipCodeData:', zipCodeData);
this.city = zipCodeData.city;
this.setCtuForMobile(zipCodeData.zipCodeCtu); this.setCtuForMobile(zipCodeData.zipCodeCtu);
this.setContainsMilitaryBase(zipCodeData.containsMilitaryBase); this.setContainsMilitaryBase(zipCodeData.containsMilitaryBase);
@ -547,7 +567,6 @@ export default {
const details = result.data; const details = result.data;
if (details) { if (details) {
this.setServiceabilityDetails(details); this.setServiceabilityDetails(details);
this.showMobileOption = this.isServiceableMobile;
} }
}); });
} }
@ -589,6 +608,11 @@ $page-side-padding: 1.5rem;
margin-bottom: 1rem; margin-bottom: 1rem;
font-size: 1rem; font-size: 1rem;
line-height: 1.625rem; line-height: 1.625rem;
flex-direction: column;
.recal-text {
margin-bottom: 1.25rem;
font-weight: $font-weight-bold;
}
} }
.mobile-service-zip-question { .mobile-service-zip-question {
:deep(.form-test-error span) { :deep(.form-test-error span) {

View file

@ -6,7 +6,9 @@
class="shop-address" class="shop-address"
aria-live="polite"> aria-live="polite">
<div class="address-header">{{ questionText }}</div> <div class="address-header">{{ questionText }}</div>
<div class="current-address"> <div
v-if="modelValue"
class="current-address">
<div class="address-line"> <div class="address-line">
<span class="shop-name">{{ displayedShopName }}</span> <span class="shop-name">{{ displayedShopName }}</span>
<span class="shop-distance">{{ modelValue.distanceInMiles?.toFixed(2) }} mi</span> <span class="shop-distance">{{ modelValue.distanceInMiles?.toFixed(2) }} mi</span>
@ -16,10 +18,22 @@
{{ getProviderCityZipState(modelValue) }} {{ getProviderCityZipState(modelValue) }}
</div> </div>
</div> </div>
<div
v-else
class="no-shops-found">
{{ `No shops found within 100 miles of ${internalZipcode}` }}
</div>
<buttonMain <buttonMain
class="more-shops-button"
:buttonText="showMoreShopsLinkText" :buttonText="showMoreShopsLinkText"
:variant="'primary'" :variant="'primary'"
@clickEvent="openModal" /> @clickEvent="openModal" />
<alert
class="alert-shop-distance"
v-if="modelValue?.distanceInMiles > 30"
ref="alertShopDistance"
cmsWidgetName="AlertShopDistanceWidget"
alertClass="alert-warning" />
<modal <modal
:ref="modalName" :ref="modalName"
:headerText="modalHeaderText" :headerText="modalHeaderText"
@ -129,7 +143,7 @@ export default {
toTitleCase, toTitleCase,
serviceZipCodeTextInputId: '', serviceZipCodeTextInputId: '',
internalZipcode: this.serviceZipcode, internalZipcode: this.serviceZipcode,
internalProviderNumber: this.modelValue.providerNumber, internalProviderNumber: this.modelValue?.providerNumber,
searchRadiusQuestionWidgetName: 'SearchRadiusQuestionWidget', searchRadiusQuestionWidgetName: 'SearchRadiusQuestionWidget',
textboxQuestionWidgetName: 'ChangeLocationZipQuestionWidget', textboxQuestionWidgetName: 'ChangeLocationZipQuestionWidget',
shopListButton: markRaw(shopListButton), shopListButton: markRaw(shopListButton),
@ -335,6 +349,18 @@ export default {
.current-address { .current-address {
margin: .625rem 0; margin: .625rem 0;
} }
.no-shops-found {
font-size: .75rem;
margin: .75rem 0rem;
font-weight: $font-weight-bold;
line-height: 1.375rem;
}
.more-shops-button {
margin-bottom: 1.25rem;
}
.alert-shop-distance {
margin-top: 2.5rem;
}
:deep(.modal-title) { :deep(.modal-title) {
font-weight: 400; font-weight: 400;
display: block; display: block;

View file

@ -212,6 +212,8 @@ $alert-color-scale: 40%;
$alert-yellow-bg: #fef6e8; $alert-yellow-bg: #fef6e8;
$alert-yellow-color: #e86421; $alert-yellow-color: #e86421;
$alert-red-bg: #fbe9e8;
$alert-red-color: $red;
//Modal animation //Modal animation
// This affects all [Bootstrap] modals // This affects all [Bootstrap] modals

View file

@ -41,17 +41,18 @@
</button> </button>
</div> </div>
<div class="alert-body" <div class="alert-body"
v-show="alertCopy"
:id="collapseId"> :id="collapseId">
<template <template
v-for="paragraph in splitAlertCopyForParagraphTag" v-for="paragraph in splitAlertCopyForParagraphTag"
:key="paragraph"> :key="paragraph">
<p <p
v-if="!doesCopyContainRouterLink(paragraph) && !doesCopyContainTextLink(paragraph)" v-if="!doesCopyContainRouterLink(paragraph) && !doesCopyContainTextLink(paragraph)"
class="m-0 text-body small" class="m-0 text-body"
v-html="paragraph"></p> v-html="paragraph"></p>
<p <p
v-else v-else
class="m-0 text-body small"> class="m-0 text-body">
<template v-if="doesCopyContainRouterLink(paragraph)"> <template v-if="doesCopyContainRouterLink(paragraph)">
<textBlock <textBlock
:customText="paragraph" :customText="paragraph"
@ -162,8 +163,9 @@ export default {
}, },
mounted() { mounted() {
this.ensureAlertIsInViewPort(); this.ensureAlertIsInViewPort();
this.collapseElement = Collapse.getOrCreateInstance(document.getElementById(this.collapseId)); if (this.isCollapsible) {
this.collapseElement = Collapse.getOrCreateInstance(document.getElementById(this.collapseId));
}
}, },
methods: { methods: {
doesCopyContainRouterLink, doesCopyContainRouterLink,
@ -234,8 +236,9 @@ export default {
height: 100%; height: 100%;
width: 100%; width: 100%;
transition: transform 0.3s; transition: transform 0.3s;
transform: rotate(180deg);
&.rotated { &.rotated {
transform: rotate(180deg); transform: none;
} }
} }
a { a {
@ -271,10 +274,10 @@ export default {
} }
} }
&.alert-danger { &.alert-danger {
background-color: $red-100; background-color: $alert-red-bg;
border-color: $red-200; border-color: $alert-red-color;
.alert-body { .alert-body {
border-top-color: $red-200; border-top-color: $alert-red-color;
} }
svg { svg {
fill: $red-600; fill: $red-600;