From 34c6a1651e69a824971c2c8a88c4df5eb189e6e9 Mon Sep 17 00:00:00 2001 From: Alex Humphries Date: Mon, 5 Jan 2026 10:52:25 -0500 Subject: [PATCH 01/16] Update styles/layout for service-location parity - Updated subheader on service-location - Update list-button styles so that inshop/mobile buttons match heritage --- .../button-question/button-question.vue | 2 +- .../appointment-type-question.vue | 46 +++++++++---------- .../service-location/service-location.vue | 32 ++++--------- src/styles/shared-input-button-styles.scss | 12 +---- src/styles/ux-variables.scss | 1 + src/ux-components/list-button/list-button.vue | 25 +++++----- 6 files changed, 46 insertions(+), 72 deletions(-) diff --git a/src/digital-components/button-question/button-question.vue b/src/digital-components/button-question/button-question.vue index 5c380612..2df23f7b 100644 --- a/src/digital-components/button-question/button-question.vue +++ b/src/digital-components/button-question/button-question.vue @@ -329,7 +329,7 @@ export default { } .question-text { - margin-bottom: 1rem; + margin-bottom: .625rem; font-size: 1rem; line-height: 1.625rem; diff --git a/src/layouts/service-location/appointment-type-question/appointment-type-question.vue b/src/layouts/service-location/appointment-type-question/appointment-type-question.vue index ad3bf61d..41c0353d 100644 --- a/src/layouts/service-location/appointment-type-question/appointment-type-question.vue +++ b/src/layouts/service-location/appointment-type-question/appointment-type-question.vue @@ -1,24 +1,23 @@ diff --git a/src/layouts/service-location/service-location.vue b/src/layouts/service-location/service-location.vue index e4843e92..bed11abc 100644 --- a/src/layouts/service-location/service-location.vue +++ b/src/layouts/service-location/service-location.vue @@ -12,8 +12,8 @@
- + - diff --git a/src/store/index.js b/src/store/index.js index 14106bf2..ef1b7f52 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -1011,14 +1011,13 @@ export const useMainStore = defineStore({ }); }, - getProviders(serviceZipCode) { + getProviders(serviceZipCode, shopRadiusInMiles = 100) { const { carId, isBigTruck } = this.order.vehicle; const damageType = this.damage.isRepair ? 'Repair' : 'Replace'; const { parentAccountNumber } = this.order; const partsWithRecal = getTopLevelGlassPartsWithRecal(this.order.lineItems.glassParts); const windshieldPartWithRecal = partsWithRecal?.length > 0 ? partsWithRecal[0] : null; const safeliteOnly = true; - const shopRadiusInMiles = 100; let url = `${endpoints.GetProviders.url}/${serviceZipCode}/${damageType}/${shopRadiusInMiles}/${parentAccountNumber}/${safeliteOnly}/${carId}`; if (windshieldPartWithRecal) { diff --git a/src/styles/ux-variables.scss b/src/styles/ux-variables.scss index 47914dbd..56bd5bad 100644 --- a/src/styles/ux-variables.scss +++ b/src/styles/ux-variables.scss @@ -145,7 +145,7 @@ $font-weight-lighter: lighter; $font-weight-light: 300; $font-weight-normal: 400; $font-weight-bold: 500; -$font-weight-bolder: bolder; +$font-weight-bolder: 600; //Border Radius diff --git a/src/ux-components/list-button/list-button.vue b/src/ux-components/list-button/list-button.vue index 2c22bc70..24a46018 100644 --- a/src/ux-components/list-button/list-button.vue +++ b/src/ux-components/list-button/list-button.vue @@ -85,7 +85,7 @@ export default { color: $black; font-weight: 500; background: #e7f1f6; - border-color: #0070d1; + border-color: $heritage-blue-primary; } &:checked + .list-button-content p, &:checked + .list-button-content span { diff --git a/src/ux-components/text-link/text-link.vue b/src/ux-components/text-link/text-link.vue index 31f989c9..7b65c45e 100644 --- a/src/ux-components/text-link/text-link.vue +++ b/src/ux-components/text-link/text-link.vue @@ -62,7 +62,7 @@ export default { diff --git a/src/layouts/service-location/service-location.vue b/src/layouts/service-location/service-location.vue index deddc2a1..5fe675da 100644 --- a/src/layouts/service-location/service-location.vue +++ b/src/layouts/service-location/service-location.vue @@ -77,6 +77,7 @@ @updated-mobile-ctu="setCtuForMobile" /> 0) { + let foundMatch = false; + if(this.selectedProvider && this.selectedProvider.providerNumber) { + const matchedProvider = providers.shopProviders.find( + (provider) => provider.providerNumber === this.selectedProvider.providerNumber + ); + if (matchedProvider) { + foundMatch = true; + this.selectedProvider = matchedProvider; + } + } + if(providers.shopProviders.length > 0 && !foundMatch) { this.selectedProvider = providers.shopProviders[0]; } } diff --git a/src/layouts/service-location/service-zip-question/service-zip-question.vue b/src/layouts/service-location/service-zip-question/service-zip-question.vue index 478ab789..dc5224a3 100644 --- a/src/layouts/service-location/service-zip-question/service-zip-question.vue +++ b/src/layouts/service-location/service-zip-question/service-zip-question.vue @@ -1,14 +1,13 @@ @@ -31,21 +30,32 @@ export default { }, // TODO: Correct prop def. props: { - modelValue: { - serviceZipCode: String - }, + modelValue: String, cmsWidgetName: String }, + data() { + return { + internalZipcode: this.modelValue + } + }, emits: ['update:modelValue'], - computed: { - value: { - get() { - return this.modelValue; - }, - set(newValue) { - this.$emit('update:modelValue', newValue); - } + methods: { + updateModelValue() { + this.$emit('update:modelValue', this.internalZipcode); + }, + }, + watch: { + modelValue(newValue) { + this.internalZipcode = newValue; } } }; + diff --git a/src/layouts/service-location/shop-address/shop-address.vue b/src/layouts/service-location/shop-address/shop-address.vue index 2989e0da..a44e6f62 100644 --- a/src/layouts/service-location/shop-address/shop-address.vue +++ b/src/layouts/service-location/shop-address/shop-address.vue @@ -17,11 +17,11 @@
{{ displayedShopName }} - {{ modelValue.distanceInMiles.toFixed(2) }} mi + {{ modelValue.distanceInMiles?.toFixed(2) }} mi
-
{{ toTitleCase(modelValue.address.streetAddress) }}
+
{{ toTitleCase(modelValue.address?.streetAddress) }}
- {{ toTitleCase(modelValue.address.city) }}, {{ modelValue.address.state }} {{ modelValue.address.zipCode }} + {{ toTitleCase(modelValue.address?.city) }}, {{ modelValue.address?.state }} {{ modelValue.address?.zipCode }}
+ + {{ serviceZipPrompt }} + null }, + serviceZipcode: String, cmsWidgetName: String, validationRules: String, isDisplayed: Boolean, @@ -109,13 +113,13 @@ export default { toTitleCase, serviceZipCodeTextInputId: '', displayInvalidZipAlert: false, - internalZipcode: this.modelValue.address.zipCode, + internalZipcode: this.serviceZipcode, internalProviderNumber: this.modelValue.providerNumber, textboxQuestionWidgetName: 'ChangeLocationZipQuestionWidget', alertInvalidZipWidgetName: 'AlertInvalidZipWidget', shopListButton: markRaw(shopListButton), searchRadiusInMiles: 25, - nearbyShops: this.getNearbyShops(25) + nearbyShops: [] }; }, emits: ['update:modelValue'], @@ -150,18 +154,37 @@ export default { const formattedEndDate = endDate.toISOString().split('T')[0]; return { - displayAvailabilityIndicators: true, + displayAvailabilityIndicators: false, availabilityRatingCallback: getAvailabilityRating, startDate: formattedStartDate, endDate: formattedEndDate, shopAppointmentType: this.selectedAppointmentType }; + }, + serviceZipPrompt() { + return this.getCmsContent(this.modalWidgetName, 'BodyText2'); + }, + nearbyShopsData() { + return this.nearbyShops.map((shopProvider) => { + const streetAddress = toTitleCase(shopProvider.address.streetAddress); + const city = toTitleCase(shopProvider.address.city); + const { state } = shopProvider.address; + const { zipCode } = shopProvider.address; + const distanceInMiles = shopProvider.distanceInMiles.toFixed(2); + + return { + buttonLabel: city, + buttonLabelSubCopy: `${distanceInMiles} mi`, + buttonBodyCopy: `${streetAddress}, ${city}, ${state} ${zipCode}`, + value: shopProvider.providerNumber + }; + }); } }, watch: { - internalZipcode(newZipcode) { + internalZipcode() { this.resetsOnZipInput(); - }, + } }, methods: { resetAlerts() { @@ -171,10 +194,6 @@ export default { this.resetAlerts(); this.nearbyShops = await this.getNearbyShops(this.searchRadiusInMiles); }, - focusOnZipInput() { - const input = document.getElementById(this.serviceZipCodeTextInputId); - input?.focus(); - }, openModal() { this.$refs[this.modalName].openModal(); }, @@ -185,39 +204,29 @@ export default { this.serviceZipCodeTextInputId = inputId; }, onModalOpened() { - this.internalZipcode = this.modelValue.address.zipCode; - this.internalProviderNumber = this.modelValue.providerNumber; - this.focusOnZipInput(); + this.internalZipcode = this.serviceZipcode; + this.resetsOnZipInput().then(() => { + this.internalProviderNumber = this.nearbyShops[0]?.providerNumber; + }); }, onModalClosed() { - this.internalZipcode = this.modelValue.address.zipCode; - this.internalProviderNumber = this.modelValue.providerNumber; - this.resetsOnZipInput(); + this.internalZipcode = this.serviceZipcode; + this.resetsOnZipInput().then(() => { + this.internalProviderNumber = this.nearbyShops[0]?.providerNumber; + }); }, async updateSelectedProvider() { - // TODO: Do something with the zipcode? + if (this.internalProviderNumber !== this.modelValue.providerNumber) { + const selectedShop = this.nearbyShops.find(shop => shop.providerNumber === this.internalProviderNumber); + if (selectedShop) { + this.$emit('update:modelValue', selectedShop); + } + } + this.closeModal(); }, async getNearbyShops(radiusInMiles) { const result = await useMainStore().getProviders(this.internalZipcode, radiusInMiles); - - const mappedData = result.data.shopProviders.map((shopProvider) => { - const streetAddress = toTitleCase(shopProvider.address.streetAddress); - const city = toTitleCase(shopProvider.address.city); - const { state } = shopProvider.address; - const { zipCode } = shopProvider.address; - const distanceInMiles = Math.round(shopProvider.distanceInMiles * 2) / 2; - - return { - buttonLabel: city, - buttonLabelSubCopy: `${distanceInMiles} mi`, - buttonBodyCopy: `${streetAddress}, ${city}, ${state} ${zipCode}`, - value: shopProvider.providerNumber - }; - }); - - console.log('Internal provider number after getting shops:', this.internalProviderNumber); - - return mappedData; + return result.data.shopProviders; } } }; @@ -227,6 +236,7 @@ export default { @import "@/styles/ux-variables-svg-strings.scss"; .shop-address { .address-header { + color: $black; font-weight: 600; margin-bottom: 0.625rem; } @@ -242,6 +252,14 @@ export default { :deep(.modal-title) { font-weight: 400; display: block; + width: fit-content; + } + .radioQuestion { + margin-bottom: .625rem; + } + .service-zip-prompt { + color: $black; + font-weight: 600; } } diff --git a/src/store/index.js b/src/store/index.js index ef1b7f52..00995f29 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -1798,6 +1798,7 @@ export const useMainStore = defineStore({ this.order.vehicle.registration.lastName = registrationInfo?.lastName; }, updateServiceLocation(serviceLocationInfo) { + console.log('Updating service location info:', serviceLocationInfo); this.order.serviceLocation.address = serviceLocationInfo.address; this.order.serviceLocation.address2 = serviceLocationInfo.address2; this.order.serviceLocation.city = serviceLocationInfo.city; @@ -2707,6 +2708,7 @@ export const useMainStore = defineStore({ }, saveServiceLocation(serviceLocationInfo) { + console.log('Saving service location info:', serviceLocationInfo); if (this.order.serviceLocation) { if ( serviceLocationInfo.zipCode !== this.order.serviceLocation.zipCode @@ -2717,6 +2719,7 @@ export const useMainStore = defineStore({ || serviceLocationInfo.appointmentType !== this.order.serviceLocation.appointmentType ) { + console.log('Service location info has changed, resetting dependent state.'); this.resetSchedule(); } } diff --git a/src/styles/ux-variables.scss b/src/styles/ux-variables.scss index 56bd5bad..206716b6 100644 --- a/src/styles/ux-variables.scss +++ b/src/styles/ux-variables.scss @@ -70,6 +70,10 @@ $orange: #fd7e14; $teal: #20c997; $cyan: #0dcaf0; +//Heritage +$heritage-blue-primary: #0070D1; +$heritage-blue-secondary: #167cac; + // Darker gray $darker-gray: #525656; @@ -145,7 +149,7 @@ $font-weight-lighter: lighter; $font-weight-light: 300; $font-weight-normal: 400; $font-weight-bold: 500; -$font-weight-bolder: 600; +$font-weight-bolder: bolder; //Border Radius @@ -217,8 +221,4 @@ $enable-important-utilities: false; $letter-spacing-primary: 0.03em; // Borders $border-input: 1px solid rgba(179, 180, 181); -$border-input-focus: 2.5px solid $blue; - -//Heritage -$heritage-blue-primary: #0070D1; -$heritage-blue-secondary: #167cac; \ No newline at end of file +$border-input-focus: 2.5px solid $heritage-blue-primary; diff --git a/src/ux-components/list-button/list-button.vue b/src/ux-components/list-button/list-button.vue index 942d2154..c3566b68 100644 --- a/src/ux-components/list-button/list-button.vue +++ b/src/ux-components/list-button/list-button.vue @@ -87,15 +87,18 @@ $heritage-checked-border-color: #0070d1; &:checked + .list-button-content { background: $heritage-checked-background-color; border-color: $heritage-checked-border-color; - box-shadow: 0 0 0 1px $blue; &:not(.has-sub-copy) { font-weight: 500; + color: $black; } } &:checked + .list-button-content span:nth-child(2) { font-weight: 400; color: $gray-600; } + &:focus:checked + .list-button-content { + border-width: 2.5px; + } } } .list-button-content { @@ -110,9 +113,12 @@ $heritage-checked-border-color: #0070d1; width: 100%; outline: none; margin-bottom: .625rem; + height: 4.375rem; &:not(.has-sub-copy) { font-weight: 400; + height: 2.8125rem; + color: $darker-gray; } span { From bcc156ec293beff8b172ec9ffcede75a8899d9a0 Mon Sep 17 00:00:00 2001 From: Alex Humphries Date: Fri, 9 Jan 2026 16:00:54 -0500 Subject: [PATCH 05/16] Add search radius to change location modal + related style updates --- src/constants/button-variants.js | 9 ---- src/constants/component-variants.js | 15 ++++++ src/constants/error-messages.js | 4 +- .../dropdown-question/dropdown-question.vue | 39 ++++++++++++-- src/digital-components/modal/modal.vue | 10 ++-- .../textbox-question/textbox-question.vue | 14 ++++- .../policy-vehicles/policy-vehicles.vue | 2 +- .../service-location/service-location.vue | 6 --- .../service-zip-question.spec.js | 1 - .../service-zip-question.vue | 2 +- .../shop-address/shop-address.vue | 54 +++++++++++++------ .../button-main/button-main.spec.js | 2 +- src/ux-components/button-main/button-main.vue | 2 +- 13 files changed, 113 insertions(+), 47 deletions(-) delete mode 100644 src/constants/button-variants.js create mode 100644 src/constants/component-variants.js diff --git a/src/constants/button-variants.js b/src/constants/button-variants.js deleted file mode 100644 index cf73718a..00000000 --- a/src/constants/button-variants.js +++ /dev/null @@ -1,9 +0,0 @@ -const buttonVariants = Object.freeze({ - primary: 'primary', - secondary: 'secondary', - success: 'success', - link: 'link', - navigation: 'navigation' -}); - -export default buttonVariants; diff --git a/src/constants/component-variants.js b/src/constants/component-variants.js new file mode 100644 index 00000000..7fd84311 --- /dev/null +++ b/src/constants/component-variants.js @@ -0,0 +1,15 @@ +export const buttonVariants = Object.freeze({ + primary: 'primary', + secondary: 'secondary', + success: 'success', + link: 'link', + navigation: 'navigation' +}); +export const dropdownVariants = Object.freeze({ + primary: 'primary', + compact: 'compact' +}); +export const modalPositions = Object.freeze({ + center: 'center', + edge: 'edge' +}); diff --git a/src/constants/error-messages.js b/src/constants/error-messages.js index 90493221..65acc9ae 100644 --- a/src/constants/error-messages.js +++ b/src/constants/error-messages.js @@ -23,8 +23,8 @@ const errorMessages = Object.freeze({ LAST_NAME_REQUIRED: 'Please enter your last name', EMAIL_ADDRESS_REQUIRED: 'Email is required.', EMAIL_ADDRESS_FORMAT: 'Please enter a valid email address.', - SERVICE_ZIP_REQUIRED: 'Please enter your service ZIP', - SERVICE_ZIP_FORMAT: 'Please enter a valid service ZIP', + SERVICE_ZIP_REQUIRED: 'ZIP code is required', + SERVICE_ZIP_FORMAT: 'Invalid ZIP, please enter a new ZIP (5 characters)', VIN_REQUIRED: 'Please enter your VIN', VIN_FORMAT: // eslint-disable-next-line max-len diff --git a/src/digital-components/dropdown-question/dropdown-question.vue b/src/digital-components/dropdown-question/dropdown-question.vue index 69b8a530..536ed4c9 100644 --- a/src/digital-components/dropdown-question/dropdown-question.vue +++ b/src/digital-components/dropdown-question/dropdown-question.vue @@ -2,8 +2,9 @@ -
{{ toTitleCase(modelValue.address?.streetAddress) }}
+
{{ getProviderAddress(modelValue) }}
- {{ toTitleCase(modelValue.address?.city) }}, {{ modelValue.address?.state }} {{ modelValue.address?.zipCode }} + {{ getProviderCityZipState(modelValue) }}
+ { - const streetAddress = toTitleCase(shopProvider.address.streetAddress); const city = toTitleCase(shopProvider.address.city); - const { state } = shopProvider.address; - const { zipCode } = shopProvider.address; const distanceInMiles = shopProvider.distanceInMiles.toFixed(2); return { buttonLabel: city, buttonLabelSubCopy: `${distanceInMiles} mi`, - buttonBodyCopy: `${streetAddress}, ${city}, ${state} ${zipCode}`, + buttonBodyCopy: this.getFullProviderAddress(shopProvider), value: shopProvider.providerNumber }; }); @@ -200,11 +206,18 @@ export default { optionsObj[option.Name] = option.Text; }); return optionsObj; - } + }, + providerAddresses() { + return this.nearbyShops?.map((provider) => ({ + title: toTitleCase(provider.companyName), + fullAddress: this.getFullProviderAddress(provider), + addressLines: [this.getProviderAddress(provider), this.getProviderCityZipState(provider)] + })) ?? []; + }, }, watch: { - internalZipcode() { - this.refreshShops(); + async internalZipcode() { + await this.resetsOnZipInput(); }, async searchRadiusInMiles() { this.nearbyShops = await this.getNearbyShops(this.searchRadiusInMiles); @@ -230,6 +243,7 @@ export default { this.resetsOnZipInput().then(() => { this.internalProviderNumber = this.nearbyShops[0]?.providerNumber; }); + this.forceServiceZipRerender(); }, onModalClosed() { this.internalZipcode = this.serviceZipcode; @@ -249,6 +263,42 @@ export default { async getNearbyShops(radiusInMiles) { const result = await useMainStore().getProviders(this.internalZipcode, radiusInMiles); return result.data.shopProviders; + }, + getFullProviderAddress(provider) { + const addressLine1 = this.getProviderAddress(provider); + const addressLine2 = this.getProviderCityZipState(provider); + const joinString = + addressLine1.length > 0 && addressLine2.length > 0 ? ', ' : ''; + return [addressLine1, addressLine2].join(joinString); + }, + getProviderAddress(provider) { + return toTitleCase(provider?.address?.streetAddress); + }, + getProviderCityZipState(provider) { + const city = toTitleCase(provider?.address?.city); + const state = provider?.address?.state ?? ''; + const zipCode = provider?.address?.zipCode ?? ''; + let addressLine2 = ''; + if (city) { + addressLine2 += city; + if (state || zipCode) { + addressLine2 += state ? ', ' : ' '; + } + } + if (state) { + addressLine2 += state; + addressLine2 += zipCode ? ' ' : ''; + } + if (zipCode) { + addressLine2 += zipCode; + } + return addressLine2; + }, + forceServiceZipRerender() { + this.renderServiceZip = false; + this.$nextTick(() => { + this.renderServiceZip = true; + }); } } }; @@ -283,5 +333,8 @@ export default { color: $black; font-weight: 600; } + :deep(#map) { + height: 27rem; + } } From 21e9127b5062d1c0f7d81a1179c49b7c5acab57f Mon Sep 17 00:00:00 2001 From: Alex Humphries Date: Tue, 13 Jan 2026 12:20:05 -0500 Subject: [PATCH 07/16] Fix availability indicators for shop address --- .../shop-list-button/shop-list-button.vue | 44 ++++++++++++++----- .../service-location/service-location.vue | 2 +- .../shop-address/shop-address.vue | 4 +- 3 files changed, 35 insertions(+), 15 deletions(-) diff --git a/src/iss-components/shop-list-button/shop-list-button.vue b/src/iss-components/shop-list-button/shop-list-button.vue index 44812a90..42b89ba3 100644 --- a/src/iss-components/shop-list-button/shop-list-button.vue +++ b/src/iss-components/shop-list-button/shop-list-button.vue @@ -10,6 +10,7 @@ :aria-label="buttonLabel" class="button-content list-button-content d-flex flex-column justify-content-center py-3 px-4">
+
{{ buttonLabelSubCopy }} -
+
+ v-if="displayAvailabilityIndicators" + id="availabilityIndicatorBlock" + class="availability-indicator rounded-pill" + :class="availabilityRatingClass">
+ id="availabilityIndicator" + class="d-flex align-items-center"> +
+
+ {{ badgeText }}
- {{ badgeText }}
@@ -157,11 +160,23 @@ $heritage-checked-border-color: #0070d1; border-left: 1px solid $heritage-border-color; } + .availability-indicator-spacer { + width: 8rem; + margin-right: auto; + } + + .availability-indicator-container { + display: flex; + align-items: center; + justify-content: flex-end; + margin-left: auto; + width: 8rem; + } + .availability-indicator { background-repeat: no-repeat; display: flex; align-items: center; - margin-left: auto; padding: 0.125rem 0.5rem; .availability-badge { @@ -179,6 +194,11 @@ $heritage-checked-border-color: #0070d1; &.orange { background-image: url($svg-shop-list-button-orange-availability); } + + &.gray { + background-image: url(~@/assets/img/heritage-loader-blue.gif); + background-size: contain; + } } .button-auxillary-copy { box-sizing: border-box; diff --git a/src/layouts/service-location/service-location.vue b/src/layouts/service-location/service-location.vue index b7b5fcee..92f3ea7e 100644 --- a/src/layouts/service-location/service-location.vue +++ b/src/layouts/service-location/service-location.vue @@ -79,7 +79,7 @@ v-model="selectedProvider" :serviceZipcode="zipCode" :isDisplayed="isInshop" - :selectAppointmentType="selectedAppointmentType" + :selectedAppointmentType="selectedAppointmentType" modalWidgetName="ChangeLocationModalWidget" cmsWidgetName="ShopAddressWidget" /> Date: Thu, 15 Jan 2026 13:18:12 -0500 Subject: [PATCH 08/16] Some updates for validation on zip entry/shop search --- src/constants/error-messages.js | 5 + .../button-question/button-question.vue | 5 + src/digital-components/modal/modal.vue | 6 +- .../text-block/text-block.vue | 9 +- .../textbox-question/textbox-question.vue | 10 +- src/iss-components/google-map/google-map.vue | 2 +- .../shop-list-button.spec.js.snap | 4 +- .../appointment-type-question.vue | 21 +- .../service-location/service-location.vue | 223 ++++++------- .../service-zip-question.spec.js | 8 +- .../service-zip-question.vue | 32 +- .../shop-address/shop-address.vue | 124 +++++--- .../shop-question/shop-question.vue | 294 ------------------ src/store/index.js | 3 - src/styles/common-typography-styles.scss | 18 -- src/styles/ux-variables.scss | 3 +- 16 files changed, 255 insertions(+), 512 deletions(-) delete mode 100644 src/layouts/service-location/shop-question/shop-question.vue diff --git a/src/constants/error-messages.js b/src/constants/error-messages.js index 65acc9ae..ace5e564 100644 --- a/src/constants/error-messages.js +++ b/src/constants/error-messages.js @@ -25,6 +25,11 @@ const errorMessages = Object.freeze({ EMAIL_ADDRESS_FORMAT: 'Please enter a valid email address.', SERVICE_ZIP_REQUIRED: 'ZIP code is required', SERVICE_ZIP_FORMAT: 'Invalid ZIP, please enter a new ZIP (5 characters)', + 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.`, + PROVIDER_REQUIRED: 'Please select a provider.', + 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.', VIN_REQUIRED: 'Please enter your VIN', VIN_FORMAT: // eslint-disable-next-line max-len diff --git a/src/digital-components/button-question/button-question.vue b/src/digital-components/button-question/button-question.vue index 5c380612..da7eb5c7 100644 --- a/src/digital-components/button-question/button-question.vue +++ b/src/digital-components/button-question/button-question.vue @@ -294,6 +294,11 @@ export default { this.resetField({ value: newValue }); + }, + answers() { + this.resetField({ + value: this.modelValue + }); } }, beforeMount() { diff --git a/src/digital-components/modal/modal.vue b/src/digital-components/modal/modal.vue index 3875cf0a..8f952d6b 100644 --- a/src/digital-components/modal/modal.vue +++ b/src/digital-components/modal/modal.vue @@ -86,6 +86,10 @@ export default { modalPosition: { type: String, default: modalPositions.edge + }, + allowInvalidSubmit: { + type: Boolean, + default: false } }, emits: ['footer-button-event', 'isModalOpened'], @@ -123,7 +127,7 @@ export default { methods: { async validateAndEmit() { const validationResult = await this.validate(); - if (validationResult.valid) { + if (validationResult.valid || this.allowInvalidSubmit) { this.$emit('footer-button-event'); } }, diff --git a/src/digital-components/text-block/text-block.vue b/src/digital-components/text-block/text-block.vue index f6d0d591..f6db4887 100644 --- a/src/digital-components/text-block/text-block.vue +++ b/src/digital-components/text-block/text-block.vue @@ -51,7 +51,7 @@ export default { props: { customText: String, // used to allow the insert of token values into textblock justifyText: String, // left, right, center - typeStyle: String, // h1-h6, body, small, label, caption + typeStyle: String, // h1-h6, body, small, label, caption, disclaimer // (see Figma or Confluence documentation) fontWeight: String, // bold=500, default is 400 cmsWidgetName: String, @@ -109,5 +109,12 @@ export default { &.dark { color: $black; } + &.disclaimer { + color: $lighter-gray; + font-weight: $font-weight-light; + font-size: .8125rem; + font-style: italic; + margin-top: .625rem; + } } diff --git a/src/digital-components/textbox-question/textbox-question.vue b/src/digital-components/textbox-question/textbox-question.vue index 7992abb7..028617e7 100644 --- a/src/digital-components/textbox-question/textbox-question.vue +++ b/src/digital-components/textbox-question/textbox-question.vue @@ -250,8 +250,10 @@ export default { this.$emit('click-event'); } }, - clickedTextButton() { - if (this.meta.valid) { + async clickedTextButton() { + const result = await validate(this.value, this.validationRules); + if (result.valid) { + this.handleChange(this.value); this.$emit('click-event'); } } @@ -378,8 +380,8 @@ input::-webkit-date-and-time-value { .form-control { border: $border-input; border-radius: $border-radius; - min-height: 3rem; - max-height: 3rem; + min-height: 2.8125rem; + max-height: 2.8125rem; padding: 0.75rem 1rem; letter-spacing: inherit; box-shadow: $box-shadow-input; diff --git a/src/iss-components/google-map/google-map.vue b/src/iss-components/google-map/google-map.vue index 4f2fa023..686cf5ec 100644 --- a/src/iss-components/google-map/google-map.vue +++ b/src/iss-components/google-map/google-map.vue @@ -55,7 +55,7 @@ export default { const { AdvancedMarkerElement, PinElement } = await window.google.maps.importLibrary('marker'); markers?.forEach((marker, index) => { const pinElement = new PinElement({ - glyph: String.fromCharCode('A'.charCodeAt(0) + index), + glyphText: String.fromCharCode('A'.charCodeAt(0) + index), glyphColor: '#000000', borderColor: '#000000' }); diff --git a/src/iss-components/shop-list-button/__snapshots__/shop-list-button.spec.js.snap b/src/iss-components/shop-list-button/__snapshots__/shop-list-button.spec.js.snap index 200d67d6..209c7610 100644 --- a/src/iss-components/shop-list-button/__snapshots__/shop-list-button.spec.js.snap +++ b/src/iss-components/shop-list-button/__snapshots__/shop-list-button.spec.js.snap @@ -2,13 +2,13 @@ exports[`Shop list button should render correctly with all relevant props 1`] = ` - + `; exports[`Shop list button should render correctly with required props 1`] = ` - + `; diff --git a/src/layouts/service-location/appointment-type-question/appointment-type-question.vue b/src/layouts/service-location/appointment-type-question/appointment-type-question.vue index 41c0353d..32dda198 100644 --- a/src/layouts/service-location/appointment-type-question/appointment-type-question.vue +++ b/src/layouts/service-location/appointment-type-question/appointment-type-question.vue @@ -57,16 +57,12 @@ export default { return retCms; }, answersToDisplay() { - const shouldShowMobile = this.isServiceableMobile && this.isItacOrNoComp; - const shouldShowMobileNotITACNotNoComp = this.isServiceableMobile && !this.isItacOrNoComp; + const shouldShowMobile = this.isServiceableMobile; const shouldShowInshop = this.isServiceableInshop; - const shouldShowDropoff = this.isServiceableInshop && !useMainStore().damage.isRepair; return this.answersFromCms ? this.answersFromCms.filter((answer) => ( (answer.Name === AppointmentTypeStrings.IN_SHOP && shouldShowInshop) || (answer.Name === AppointmentTypeStrings.MOBILE && shouldShowMobile) - || (answer.Name === AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP && shouldShowMobileNotITACNotNoComp) - || (answer.Name === AppointmentTypeStrings.DROP_OFF && shouldShowDropoff) )) : []; }, @@ -97,14 +93,9 @@ export default { // If there is only one option to display and that option is 'Mobile' then select it if ( newValue.length === 1 - && newValue.findIndex((answer) => (answer.Name === AppointmentTypeStrings.MOBILE - || answer.Name === AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP)) !== -1 + && newValue.findIndex((answer) => (answer.Name === AppointmentTypeStrings.MOBILE)) !== -1 ) { - if (this.isItacOrNoComp) { - this.selectedValues = AppointmentTypeStrings.MOBILE; - } else { - this.selectedValues = AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP; - } + this.selectedValues = AppointmentTypeStrings.MOBILE; } }, immediate: true @@ -112,11 +103,7 @@ export default { isMobileOnly: { handler(newValue) { if (newValue) { - if (this.isItacOrNoComp) { - this.selectedValues = AppointmentTypeStrings.MOBILE; - } else { - this.selectedValues = AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP; - } + this.selectedValues = AppointmentTypeStrings.MOBILE; } } } diff --git a/src/layouts/service-location/service-location.vue b/src/layouts/service-location/service-location.vue index 92f3ea7e..e38bc1cd 100644 --- a/src/layouts/service-location/service-location.vue +++ b/src/layouts/service-location/service-location.vue @@ -62,26 +62,39 @@ cmsWidgetName="AppointmentTypeQuestionWidget" validationRules="option-required" />
- + cmsWidgetName="ShopAddressWidget" + @zip-updated="handleInShopZipUpdated" /> +
+ +
+ {{ mobileZipError }} +
+ +
{ - if ( - value.addressQuestions.streetAddress === '' - || value.addressQuestions.city === '' - || value.addressQuestions.state === '' - || value.addressQuestions.zipCode === '' - || value.isVehicleProtected == null - ) { - return errorMessages.MOBILE_LOCATION_REQUIRED; - } - return true; -}); -defineRule('selection-required', required(errorMessages.OPTION_REQUIRED)); +import shopAddress from '@/layouts/service-location/shop-address/shop-address.vue'; +import serviceZipQuestion from '@/layouts/service-location/service-zip-question/service-zip-question.vue'; +import widgetFields from '@/constants/cms-widget-fields'; const RECAL_MODAL_REF_NAME = 'RecalModal'; const SITE_FOOTER_REF_NAME = 'siteFooter'; @@ -148,15 +149,16 @@ export default { name: 'service-location', components: { alert, + textBlock, appointmentTypeQuestion, contentGroupModal, - mobileLocationModalQuestions, siteFooter, siteHeader, siteSubHeader, // eslint-disable-next-line vue/no-reserved-component-names Form, - shopAddress + shopAddress, + serviceZipQuestion }, mixins: [baseFormMixin], async beforeRouteEnter(to, from, next) { @@ -232,8 +234,12 @@ export default { mobileProviderNumber: null, zipContainsMilitaryBase: false, zipCodeCtu: null, + mobileZipCode: '', + mobileZipError: '', + showMobileOption: false, RECAL_MODAL_REF_NAME, - SITE_FOOTER_REF_NAME + SITE_FOOTER_REF_NAME, + errorMessages }; }, computed: { @@ -246,35 +252,6 @@ export default { answersFromCms() { return this.getCmsContent('ServiceTypeQuestionWidget', 'Answers'); }, - mobileLocationQuestions: { - get() { - return { - addressQuestions: { - streetAddress: this.streetAddress, - streetAddress2: this.streetAddress2, - city: this.city, - state: this.state, - zipCode: this.zipCode - }, - isVehicleProtected: this.isVehicleProtected - }; - }, - set(newValue) { - this.streetAddress = newValue.addressQuestions.streetAddress; - this.streetAddress2 = newValue.addressQuestions.streetAddress2; - this.city = newValue.addressQuestions.city; - this.state = newValue.addressQuestions.state; - this.zipCode = newValue.addressQuestions.zipCode; - this.isVehicleProtected = newValue.isVehicleProtected; - - if (newValue.zipCode !== this.zipCode) { - if (!this.isMobile) { - this.selectedAppointmentType = null; - } - this.selectedProvider = null; - } - } - }, isServiceableMobile() { if (this.isRecalibrationServiceableMobile !== null) { return ( @@ -301,15 +278,10 @@ export default { ); }, isAppointmentTypeDisplayed() { - return this.zipCode && !this.displayNoShopsAlert; + return this.zipCode && !this.displayNoShopsAlert && this.showMobileOption; }, isMobile() { - return ( - this.selectedAppointmentType - === AppointmentTypeStrings.MOBILE - || this.selectedAppointmentType - === AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP - ); + return this.selectedAppointmentType === AppointmentTypeStrings.MOBILE }, requiresInshopRecalibration() { // Specifically check for isRecalibrationServiceableMobile === false, not null or true. @@ -341,6 +313,9 @@ export default { displayServiceableMobileOnly() { return this.isServiceableMobile && !this.isServiceableInshop; }, + displayMobileFeeDisclaimer() { + return this.mainStore.isNoComp || this.mainStore.isITAC; + }, navigateBackScenario() { const { isNoComp, isITAC } = useMainStore(); return isNoComp || isITAC @@ -349,7 +324,10 @@ export default { }, isBigTruck() { return this.mainStore.order.vehicle.isBigTruck; - } + }, + mobileZipPlaceholder() { + return this.getCmsContent('MobileZipPlaceHolderWidget', widgetFields.TEXT_BLOCK_WIDGET.TEXT); + }, }, methods: { arePagePrerequisitesValid() { @@ -445,6 +423,7 @@ export default { if (serviceabilityDetails) { this.setServiceabilityDetails(serviceabilityDetails); + this.showMobileOption = this.isServiceableMobile; } if (mobileFeePart) { @@ -503,6 +482,65 @@ export default { serviceabilityDetails.isGlassServiceableMobile; this.isRecalibrationServiceableMobile = serviceabilityDetails.isRecalibrationServiceableMobile; + }, + 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_ZIP(this.mobileZipCode); + } else { + this.zipCode = this.mobileZipCode; + this.setCtuForMobile(zipCodeData.zipCodeCtu); + this.setContainsMilitaryBase(zipCodeData.containsMilitaryBase); + + const serviceabilityDetailsPromise = getServiceabilityDetails(this.mobileZipCode); + serviceabilityDetailsPromise.then((result) => { + const details = result.data; + if (details) { + this.setServiceabilityDetails(details); + } + }); + + const providersPromise = useMainStore().getProviders(this.mobileZipCode); + providersPromise.then((result) => { + const providers = result.data; + if (providers) { + this.setMobileProviderNumber(providers.mobileProviderNumber); + if(providers.shopProviders.length > 0) { + this.selectedProvider = providers.shopProviders[0]; + } + } + }); + } + }, + async handleInShopZipUpdated(newZip) { + this.zipCode = newZip; + const zipCodeData = await getZipCodeData(this.zipCode); + this.setCtuForMobile(zipCodeData.zipCodeCtu); + this.setContainsMilitaryBase(zipCodeData.containsMilitaryBase); + + const serviceabilityDetailsPromise = getServiceabilityDetails(this.zipCode); + serviceabilityDetailsPromise.then((result) => { + const details = result.data; + if (details) { + this.setServiceabilityDetails(details); + this.showMobileOption = this.isServiceableMobile; + } + }); + } + }, + watch: { + mobileZipCode(newZip) { + if(newZip !== '') { + this.updateMobileZip(); + } + }, + selectedAppointmentType() { + this.mobileZipCode = ''; } } }; @@ -510,7 +548,6 @@ export default { diff --git a/src/layouts/service-location/shop-address/shop-address.vue b/src/layouts/service-location/shop-address/shop-address.vue index ce3e3deb..2417a52a 100644 --- a/src/layouts/service-location/shop-address/shop-address.vue +++ b/src/layouts/service-location/shop-address/shop-address.vue @@ -6,13 +6,6 @@ v-if="isDisplayed" class="shop-address" aria-live="polite"> -
{{ questionText }}
@@ -32,9 +25,9 @@ :ref="modalName" :headerText="modalHeaderText" :onModalOpenedCallback="onModalOpened" - :onModalClosedCallback="onModalClosed" :footerButtonText="modalFooterText" :modalPosition="modalPositions.center" + :allowInvalidSubmit="true" @footerButtonEvent="updateSelectedProvider"> - +
+ {{ errorMessage }} +
@@ -96,11 +90,17 @@ import googleMap from '@/iss-components/google-map/google-map.vue'; import baseMixin from '@/mixins/base-mixin.js'; import { toTitleCase } from '@/helpers/text-helper.js'; import { markRaw } from 'vue'; -import { getServiceabilityDetails, getZipCodeData,getAvailabilityRating } from '@/helpers/service-location-helper'; +import { getAvailabilityRating } from '@/helpers/service-location-helper'; import { useMainStore } from '@/store'; import widgetFields from '@/constants/cms-widget-fields'; import { dropdownVariants, modalPositions } from '@/constants/component-variants'; +import errorMessages from '@/constants/error-messages'; +import { defineRule } from 'vee-validate'; +import { required } from '@/helpers/validation-rules'; +defineRule('option-required', required(errorMessages.PROVIDER_REQUIRED)); + +const SERVICE_ZIP_QUESTION_REF_NAME = 'serviceZipCodeQuestion'; export default { name: 'shop-address', @@ -130,21 +130,21 @@ export default { return { toTitleCase, serviceZipCodeTextInputId: '', - displayInvalidZipAlert: false, internalZipcode: this.serviceZipcode, internalProviderNumber: this.modelValue.providerNumber, searchRadiusQuestionWidgetName: 'SearchRadiusQuestionWidget', textboxQuestionWidgetName: 'ChangeLocationZipQuestionWidget', - alertInvalidZipWidgetName: 'AlertInvalidZipWidget', shopListButton: markRaw(shopListButton), - searchRadiusInMiles: 100, + searchRadiusInMiles: "25", nearbyShops: [], modalPositions, dropdownVariants, - renderServiceZip: true + renderServiceZip: true, + errorMessage: '', + SERVICE_ZIP_QUESTION_REF_NAME }; }, - emits: ['update:modelValue'], + emits: ['update:modelValue', 'zip-updated'], computed: { questionText() { return this.getCmsContent(this.cmsWidgetName, widgetFields.INPUT_QUESTION_WIDGET.QUESTION_TEXT); @@ -199,12 +199,17 @@ export default { }; }); }, - searchRadiusOptions() { + searchRadiusArray() { const options = this.getCmsContent(this.searchRadiusQuestionWidgetName, widgetFields.INPUT_QUESTION_WIDGET.ANSWERS); + return options; + }, + searchRadiusOptions() { const optionsObj = {}; - options.forEach(option => { - optionsObj[option.Name] = option.Text; - }); + if(this.searchRadiusArray) { + this.searchRadiusArray.forEach(option => { + optionsObj[option.Name] = option.Text; + }); + } return optionsObj; }, providerAddresses() { @@ -217,19 +222,30 @@ export default { }, watch: { async internalZipcode() { - await this.resetsOnZipInput(); + const shopsUpdated = await this.updateShops(); + if (shopsUpdated) { + this.$refs[SERVICE_ZIP_QUESTION_REF_NAME].internalZipcode = ''; + } }, async searchRadiusInMiles() { - this.nearbyShops = await this.getNearbyShops(this.searchRadiusInMiles); + await this.updateShops(); + }, + nearbyShops(newShops) { + if (!newShops.find(shop => shop.providerNumber === this.internalProviderNumber)) { + this.internalProviderNumber = ''; + } } }, methods: { - resetAlerts() { - this.displayInvalidZipAlert = false; - }, - async resetsOnZipInput() { - this.resetAlerts(); - this.nearbyShops = await this.getNearbyShops(this.searchRadiusInMiles); + async updateShops() { + this.errorMessage = ''; + const result = await this.getNearbyShops(this.searchRadiusInMiles); + if (result.length === 0) { + this.errorMessage = errorMessages.ZIP_CODE_NOT_SERVICED_FOR_VEHICLE; + } + else { + this.nearbyShops = await this.getNearbyShops(this.searchRadiusInMiles); + } }, openModal() { this.$refs[this.modalName].openModal(); @@ -239,26 +255,19 @@ export default { }, onModalOpened() { this.internalZipcode = this.serviceZipcode; - this.searchRadiusInMiles = 25; - this.resetsOnZipInput().then(() => { + this.searchRadiusInMiles = "25"; + this.updateShops().then(() => { this.internalProviderNumber = this.nearbyShops[0]?.providerNumber; }); this.forceServiceZipRerender(); }, - onModalClosed() { - this.internalZipcode = this.serviceZipcode; - this.resetsOnZipInput().then(() => { - this.internalProviderNumber = this.nearbyShops[0]?.providerNumber; - }); - }, async updateSelectedProvider() { - if (this.internalProviderNumber !== this.modelValue.providerNumber) { - const selectedShop = this.nearbyShops.find(shop => shop.providerNumber === this.internalProviderNumber); - if (selectedShop) { - this.$emit('update:modelValue', selectedShop); - } + const selectedShop = this.nearbyShops.find(shop => shop.providerNumber === this.internalProviderNumber); + if (selectedShop) { + this.$emit('update:modelValue', selectedShop); + this.$emit('zip-updated', this.internalZipcode); + this.closeModal(); } - this.closeModal(); }, async getNearbyShops(radiusInMiles) { const result = await useMainStore().getProviders(this.internalZipcode, radiusInMiles); @@ -336,5 +345,22 @@ export default { :deep(#map) { height: 27rem; } + :deep(.textbox-question) { + &.has-error { + input.form-control { + border: $border-input; + &:focus { + border: $border-input-focus; + } + } + } + .form-label { + font-weight: $font-weight-normal; + color: $darker-gray; + } + .btn-primary { + margin-bottom: 1.25rem; + } + } } diff --git a/src/layouts/service-location/shop-question/shop-question.vue b/src/layouts/service-location/shop-question/shop-question.vue deleted file mode 100644 index fdbbdd79..00000000 --- a/src/layouts/service-location/shop-question/shop-question.vue +++ /dev/null @@ -1,294 +0,0 @@ - - - - - diff --git a/src/store/index.js b/src/store/index.js index 00995f29..ef1b7f52 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -1798,7 +1798,6 @@ export const useMainStore = defineStore({ this.order.vehicle.registration.lastName = registrationInfo?.lastName; }, updateServiceLocation(serviceLocationInfo) { - console.log('Updating service location info:', serviceLocationInfo); this.order.serviceLocation.address = serviceLocationInfo.address; this.order.serviceLocation.address2 = serviceLocationInfo.address2; this.order.serviceLocation.city = serviceLocationInfo.city; @@ -2708,7 +2707,6 @@ export const useMainStore = defineStore({ }, saveServiceLocation(serviceLocationInfo) { - console.log('Saving service location info:', serviceLocationInfo); if (this.order.serviceLocation) { if ( serviceLocationInfo.zipCode !== this.order.serviceLocation.zipCode @@ -2719,7 +2717,6 @@ export const useMainStore = defineStore({ || serviceLocationInfo.appointmentType !== this.order.serviceLocation.appointmentType ) { - console.log('Service location info has changed, resetting dependent state.'); this.resetSchedule(); } } diff --git a/src/styles/common-typography-styles.scss b/src/styles/common-typography-styles.scss index cdaf2ebf..a745b5d8 100644 --- a/src/styles/common-typography-styles.scss +++ b/src/styles/common-typography-styles.scss @@ -115,21 +115,3 @@ caption, font-weight: 800; font-style: normal; } - -@font-face { - font-family: "Urbanist"; - src: - url("@/assets/fonts/Urbanist-MediumItalic.woff2") format("woff2"), - url("@/assets/fonts/Urbanist-MediumItalic.woff") format("woff"); - font-weight: 500; - font-style: italic; -} - -@font-face { - font-family: "Urbanist"; - src: - url("@/assets/fonts/Urbanist-BoldItalic.woff2") format("woff2"), - url("@/assets/fonts/Urbanist-BoldItalic.woff") format("woff"); - font-weight: 700; - font-style: italic; -} \ No newline at end of file diff --git a/src/styles/ux-variables.scss b/src/styles/ux-variables.scss index 206716b6..b571d788 100644 --- a/src/styles/ux-variables.scss +++ b/src/styles/ux-variables.scss @@ -76,6 +76,7 @@ $heritage-blue-secondary: #167cac; // Darker gray $darker-gray: #525656; +$lighter-gray: #9aa1a3; // scss-docs-start colors-map $colors: ( @@ -220,5 +221,5 @@ $enable-important-utilities: false; // Letter Spacing $letter-spacing-primary: 0.03em; // Borders -$border-input: 1px solid rgba(179, 180, 181); +$border-input: 1px solid $gray-350; $border-input-focus: 2.5px solid $heritage-blue-primary; From b1ec2a298d3f0ff03b6d416410bfc86bb320beb2 Mon Sep 17 00:00:00 2001 From: Alex Humphries Date: Thu, 15 Jan 2026 14:51:50 -0500 Subject: [PATCH 09/16] Auto expand shop search until shop is found --- .../shop-address/shop-address.vue | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/layouts/service-location/shop-address/shop-address.vue b/src/layouts/service-location/shop-address/shop-address.vue index 2417a52a..88fb545d 100644 --- a/src/layouts/service-location/shop-address/shop-address.vue +++ b/src/layouts/service-location/shop-address/shop-address.vue @@ -237,14 +237,21 @@ export default { } }, methods: { - async updateShops() { + async updateShops(autoExpand = false) { this.errorMessage = ''; - const result = await this.getNearbyShops(this.searchRadiusInMiles); + let result = await this.getNearbyShops(this.searchRadiusInMiles); + let currentSearchIndex = this.searchRadiusArray.findIndex(option => option.Name === this.searchRadiusInMiles); + while (autoExpand && result.length === 0 && currentSearchIndex < this.searchRadiusArray.length - 1) { + const newSearchRadius = this.searchRadiusArray[currentSearchIndex + 1].Name; + result = await this.getNearbyShops(newSearchRadius); + currentSearchIndex++; + } if (result.length === 0) { this.errorMessage = errorMessages.ZIP_CODE_NOT_SERVICED_FOR_VEHICLE; } else { - this.nearbyShops = await this.getNearbyShops(this.searchRadiusInMiles); + this.nearbyShops = result; + this.searchRadiusInMiles = this.searchRadiusArray[currentSearchIndex].Name; } }, openModal() { @@ -256,7 +263,8 @@ export default { onModalOpened() { this.internalZipcode = this.serviceZipcode; this.searchRadiusInMiles = "25"; - this.updateShops().then(() => { + this.nearbyShops = []; + this.updateShops(true).then(() => { this.internalProviderNumber = this.nearbyShops[0]?.providerNumber; }); this.forceServiceZipRerender(); @@ -308,7 +316,7 @@ export default { this.$nextTick(() => { this.renderServiceZip = true; }); - } + }, } }; From 253044d502feb4b6d69886ae442f5079ea6605da Mon Sep 17 00:00:00 2001 From: Alex Humphries Date: Fri, 16 Jan 2026 09:00:04 -0500 Subject: [PATCH 10/16] Some random tweaks, plus alert component updates --- src/assets/img/icons/alert-circle.svg | 3 + src/assets/img/icons/chevron-up.svg | 3 + src/helpers/service-location-helper.js | 1 + .../appointment-type-question.vue | 59 +---- .../service-location/service-location.vue | 99 ++++--- .../shop-address/shop-address.vue | 11 +- src/styles/ux-variables.scss | 3 + src/ux-components/alert/alert.vue | 249 ++++++++++++------ 8 files changed, 250 insertions(+), 178 deletions(-) create mode 100644 src/assets/img/icons/alert-circle.svg create mode 100644 src/assets/img/icons/chevron-up.svg diff --git a/src/assets/img/icons/alert-circle.svg b/src/assets/img/icons/alert-circle.svg new file mode 100644 index 00000000..904aefb9 --- /dev/null +++ b/src/assets/img/icons/alert-circle.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/img/icons/chevron-up.svg b/src/assets/img/icons/chevron-up.svg new file mode 100644 index 00000000..f235d47f --- /dev/null +++ b/src/assets/img/icons/chevron-up.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/helpers/service-location-helper.js b/src/helpers/service-location-helper.js index 37127226..7e10ccd2 100644 --- a/src/helpers/service-location-helper.js +++ b/src/helpers/service-location-helper.js @@ -5,6 +5,7 @@ function processZipCodeResults(request) { containsMilitaryBase: serviceZipValidationResponse.containsMilitaryBase, isValid: serviceZipValidationResponse.isValid, isServiceable: serviceZipValidationResponse.isServiceable, + city: serviceZipValidationResponse.city, state: serviceZipValidationResponse.state, zipCodeCtu: serviceZipValidationResponse.zipCodeCtu })).catch(() => ({ diff --git a/src/layouts/service-location/appointment-type-question/appointment-type-question.vue b/src/layouts/service-location/appointment-type-question/appointment-type-question.vue index 32dda198..d192a27f 100644 --- a/src/layouts/service-location/appointment-type-question/appointment-type-question.vue +++ b/src/layouts/service-location/appointment-type-question/appointment-type-question.vue @@ -3,15 +3,13 @@ name="fade" mode="out-in">
0 && this.isMobileFeeHidden) { - return retCms.map((x) => (x.Name === AppointmentTypeStrings.MOBILE ? { ...x, SubText: x.SubText.replace('*', '') } : x)); - } return retCms; }, - answersToDisplay() { - const shouldShowMobile = this.isServiceableMobile; - const shouldShowInshop = this.isServiceableInshop; - return this.answersFromCms - ? this.answersFromCms.filter((answer) => ( - (answer.Name === AppointmentTypeStrings.IN_SHOP && shouldShowInshop) - || (answer.Name === AppointmentTypeStrings.MOBILE && shouldShowMobile) - )) - : []; - }, selectedValues: { get() { return this.modelValue; @@ -73,46 +55,13 @@ export default { set(newValue) { this.$emit('update:modelValue', newValue); } - }, - isITAC() { - return useMainStore().isITAC; - }, - isItacOrNoComp() { - return this.isITAC || useMainStore().isNoComp; - }, - isMobileFeeHidden() { - return this.getSettingValue(experimentSettings.ISS_FEATURE_TOGGLE_IS_MOBILE_FEE_HIDDEN) === 'true'; - }, - isMobileOnly() { - return this.isServiceableMobile && !this.isServiceableInshop; } }, - watch: { - answersToDisplay: { - handler(newValue) { - // If there is only one option to display and that option is 'Mobile' then select it - if ( - newValue.length === 1 - && newValue.findIndex((answer) => (answer.Name === AppointmentTypeStrings.MOBILE)) !== -1 - ) { - this.selectedValues = AppointmentTypeStrings.MOBILE; - } - }, - immediate: true - }, - isMobileOnly: { - handler(newValue) { - if (newValue) { - this.selectedValues = AppointmentTypeStrings.MOBILE; - } - } - } - } }; diff --git a/src/layouts/service-location/service-location.vue b/src/layouts/service-location/service-location.vue index e38bc1cd..9cc5f595 100644 --- a/src/layouts/service-location/service-location.vue +++ b/src/layouts/service-location/service-location.vue @@ -13,18 +13,18 @@ - + -
+
+ {{ appointmentQuestionText }} +
- +
+ +
+ @@ -118,7 +128,6 @@ import { AppointmentTypeStrings } from '@/constants/schedule-constants.js'; import { fetchCmsContentForPage } from '@/helpers/cms-content-helper'; import settleAllPromises from '@/helpers/layout-helper'; -import { required } from '@/helpers/validation-rules'; import errorMessages from '@/constants/error-messages'; import { useMainStore } from '@/store'; import { @@ -237,6 +246,7 @@ export default { mobileZipCode: '', mobileZipError: '', showMobileOption: false, + militaryBaseWarningExpanded: false, RECAL_MODAL_REF_NAME, SITE_FOOTER_REF_NAME, errorMessages @@ -277,9 +287,6 @@ export default { || this.selectedAppointmentType === 'Dropoff' ); }, - isAppointmentTypeDisplayed() { - return this.zipCode && !this.displayNoShopsAlert && this.showMobileOption; - }, isMobile() { return this.selectedAppointmentType === AppointmentTypeStrings.MOBILE }, @@ -328,6 +335,15 @@ export default { mobileZipPlaceholder() { return this.getCmsContent('MobileZipPlaceHolderWidget', widgetFields.TEXT_BLOCK_WIDGET.TEXT); }, + militaryBaseWarningLinkText() { + return this.getCmsContent('AlertMilitaryBaseZipWidget', widgetFields.ALERT_WIDGET.HEADLINE_TEXT); + }, + militaryBaseWarningText() { + return this.getCmsContent('AlertMilitaryBaseZipWidget', widgetFields.ALERT_WIDGET.BODY_TEXT); + }, + appointmentQuestionText() { + return this.getCmsContent('AppointmentTypeQuestionWidget', widgetFields.INPUT_QUESTION_WIDGET.QUESTION_TEXT); + } }, methods: { arePagePrerequisitesValid() { @@ -424,6 +440,9 @@ export default { if (serviceabilityDetails) { this.setServiceabilityDetails(serviceabilityDetails); this.showMobileOption = this.isServiceableMobile; + if (!this.showMobileOption) { + this.selectedAppointmentType = AppointmentTypeStrings.IN_SHOP; + } } if (mobileFeePart) { @@ -564,10 +583,30 @@ $page-side-padding: 1.5rem; .form-test-error { font-weight: $font-weight-bold; } + .appointment-type-question-text { + color: $black; + font-weight: 600; + margin-bottom: 1rem; + font-size: 1rem; + line-height: 1.625rem; + } .mobile-service-zip-question { :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; + } + } } diff --git a/src/layouts/service-location/shop-address/shop-address.vue b/src/layouts/service-location/shop-address/shop-address.vue index 88fb545d..cf7239f6 100644 --- a/src/layouts/service-location/shop-address/shop-address.vue +++ b/src/layouts/service-location/shop-address/shop-address.vue @@ -3,7 +3,6 @@ name="fade" mode="out-in">
{{ questionText }}
@@ -122,7 +121,6 @@ export default { serviceZipcode: String, cmsWidgetName: String, validationRules: String, - isDisplayed: Boolean, modalWidgetName: String, selectedAppointmentType: String, }, @@ -229,11 +227,6 @@ export default { }, async searchRadiusInMiles() { await this.updateShops(); - }, - nearbyShops(newShops) { - if (!newShops.find(shop => shop.providerNumber === this.internalProviderNumber)) { - this.internalProviderNumber = ''; - } } }, methods: { @@ -250,6 +243,10 @@ export default { this.errorMessage = errorMessages.ZIP_CODE_NOT_SERVICED_FOR_VEHICLE; } else { + if (!result.find(shop => shop.providerNumber === this.internalProviderNumber)) { + this.internalProviderNumber = ''; + await this.$nextTick(); + } this.nearbyShops = result; this.searchRadiusInMiles = this.searchRadiusArray[currentSearchIndex].Name; } diff --git a/src/styles/ux-variables.scss b/src/styles/ux-variables.scss index b571d788..e8ef6a64 100644 --- a/src/styles/ux-variables.scss +++ b/src/styles/ux-variables.scss @@ -210,6 +210,9 @@ $alert-bg-scale: -90%; $alert-border-scale: -100%; $alert-color-scale: 40%; +$alert-yellow-bg: #fef6e8; +$alert-yellow-color: #e86421; + //Modal animation // This affects all [Bootstrap] modals $modal-fade-transform: translate(0, 100%); diff --git a/src/ux-components/alert/alert.vue b/src/ux-components/alert/alert.vue index ef633fdc..faaa01b5 100644 --- a/src/ux-components/alert/alert.vue +++ b/src/ux-components/alert/alert.vue @@ -1,64 +1,84 @@ @@ -74,6 +94,7 @@ import { import applicationConfig from '@/constants/application-config'; import textBlock from '@/digital-components/text-block/text-block.vue'; import textLink from '@/ux-components/text-link/text-link.vue'; +import { Collapse } from 'bootstrap'; export default { // eslint-disable-next-line vue/multi-word-component-names @@ -84,13 +105,14 @@ export default { }, props: { isDismissible: Boolean, + isCollapsible: Boolean, /* - alertClass class names: - alert-success (green) - alert-danger (red) - alert-warning (yellow) - alert-info (blue) - */ + alertClass class names: + alert-success (green) + alert-danger (red) + alert-warning (yellow) + alert-info (blue) + */ alertClass: String, cmsWidgetName: { type: String, @@ -106,7 +128,14 @@ export default { shouldScrollToOnMount: { type: Boolean, default: true - } + }, + startCollapsed: Boolean + }, + data() { + return { + collapsed: this.startCollapsed || false, + collapseElement: null, + }; }, computed: { pageQueryString() { @@ -124,10 +153,17 @@ export default { }, splitAlertCopyForParagraphTag() { return splitCMSCopyOnParagraphTag(this.alertCopy); - } + }, + collapseId() { + return this.isCollapsible + ? `${this.cmsWidgetName}-collapse` + : null; + }, }, mounted() { this.ensureAlertIsInViewPort(); + this.collapseElement = Collapse.getOrCreateInstance(document.getElementById(this.collapseId)); + }, methods: { doesCopyContainRouterLink, @@ -152,37 +188,81 @@ export default { <= (window.innerHeight - footerHeight || document.documentElement.clientHeight - footerHeight) ); - } + }, + toggleCollapse() { + this.collapsed = !this.collapsed; + if (this.collapsed) { + this.collapseElement.hide(); + } else { + this.collapseElement.show(); + } + }, } }; From d6762e85a85521c4c4f222ac91075558dae09b54 Mon Sep 17 00:00:00 2001 From: Alex Humphries Date: Fri, 16 Jan 2026 12:20:52 -0500 Subject: [PATCH 11/16] More alert/error/etc updates --- src/constants/error-messages.js | 2 +- .../appointment-type-question.vue | 13 ++- .../service-location/service-location.vue | 110 +++++++++++------- .../shop-address/shop-address.vue | 30 ++++- src/styles/ux-variables.scss | 2 + src/ux-components/alert/alert.vue | 19 +-- 6 files changed, 119 insertions(+), 57 deletions(-) diff --git a/src/constants/error-messages.js b/src/constants/error-messages.js index ace5e564..98d19bf2 100644 --- a/src/constants/error-messages.js +++ b/src/constants/error-messages.js @@ -26,7 +26,7 @@ const errorMessages = Object.freeze({ SERVICE_ZIP_REQUIRED: 'ZIP code is required', SERVICE_ZIP_FORMAT: 'Invalid ZIP, please enter a new ZIP (5 characters)', 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.', 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.', diff --git a/src/layouts/service-location/appointment-type-question/appointment-type-question.vue b/src/layouts/service-location/appointment-type-question/appointment-type-question.vue index d192a27f..f14c51c4 100644 --- a/src/layouts/service-location/appointment-type-question/appointment-type-question.vue +++ b/src/layouts/service-location/appointment-type-question/appointment-type-question.vue @@ -9,7 +9,7 @@ ref="buttonQuestion" v-model="selectedValues" customButtonQuestionId="appointmentTypeQuestion" - :answers="answersFromCms" + :answers="answersToDisplay" :groupName="groupName" :suppressError="suppressError" :validationRules="validationRules" @@ -45,8 +45,15 @@ export default { return this.getCmsContent(this.cmsWidgetName, 'QuestionText'); }, answersFromCms() { - const retCms = this.getCmsContent(this.cmsWidgetName, 'Answers'); - return retCms; + return this.getCmsContent(this.cmsWidgetName, 'Answers'); + }, + answersToDisplay() { + return this.answersFromCms + ? this.answersFromCms.filter((answer) => ( + (answer.Name === AppointmentTypeStrings.IN_SHOP) + || (answer.Name === AppointmentTypeStrings.MOBILE) + )) + : []; }, selectedValues: { get() { diff --git a/src/layouts/service-location/service-location.vue b/src/layouts/service-location/service-location.vue index 9cc5f595..ab684447 100644 --- a/src/layouts/service-location/service-location.vue +++ b/src/layouts/service-location/service-location.vue @@ -13,18 +13,6 @@ - - -
- {{ appointmentQuestionText }} + {{ scheduleRecalText }} + {{ appointmentQuestionText }}
+ + +
@@ -105,6 +107,7 @@ cmsWidgetName="MobileFeeDisclaimerWidget" typeStyle="disclaimer" />
+ 0 && !foundMatch) { this.selectedProvider = providers.shopProviders[0]; } + else { + this.selectedProvider = null; + } } if (glassFees) { @@ -485,13 +502,6 @@ export default { setMobileProviderNumber(providerNumber) { this.mobileProviderNumber = providerNumber; }, - resetMobileLocation() { - this.streetAddress = ''; - this.streetAddress2 = ''; - this.city = ''; - - this.isVehicleProtected = null; - }, setServiceabilityDetails(serviceabilityDetails) { this.isGlassServiceableInshop = serviceabilityDetails.isGlassServiceableInshop; @@ -501,6 +511,9 @@ export default { serviceabilityDetails.isGlassServiceableMobile; this.isRecalibrationServiceableMobile = serviceabilityDetails.isRecalibrationServiceableMobile; + if (!this.isServiceableMobile) { + this.selectedAppointmentType = AppointmentTypeStrings.IN_SHOP; + } }, async updateMobileZip() { this.mobileZipError = ''; @@ -510,8 +523,10 @@ export default { if (!zipCodeData.isValid) { this.mobileZipError = errorMessages.INVALID_ZIP; } else if (!zipCodeData.isServiceable) { - this.mobileZipError = errorMessages.NO_SERVICE_IN_ZIP(this.mobileZipCode); + this.mobileZipError = errorMessages.NO_SERVICE_IN_AREA(toTitleCase(zipCodeData.city)); } else { + console.log('zipCodeData:', zipCodeData); + this.city = zipCodeData.city; this.zipCode = this.mobileZipCode; this.setCtuForMobile(zipCodeData.zipCodeCtu); this.setContainsMilitaryBase(zipCodeData.containsMilitaryBase); @@ -532,6 +547,9 @@ export default { if(providers.shopProviders.length > 0) { this.selectedProvider = providers.shopProviders[0]; } + else { + this.selectedProvider = null; + } } }); } @@ -539,6 +557,8 @@ export default { async handleInShopZipUpdated(newZip) { this.zipCode = newZip; const zipCodeData = await getZipCodeData(this.zipCode); + console.log('zipCodeData:', zipCodeData); + this.city = zipCodeData.city; this.setCtuForMobile(zipCodeData.zipCodeCtu); this.setContainsMilitaryBase(zipCodeData.containsMilitaryBase); @@ -547,7 +567,6 @@ export default { const details = result.data; if (details) { this.setServiceabilityDetails(details); - this.showMobileOption = this.isServiceableMobile; } }); } @@ -589,6 +608,11 @@ $page-side-padding: 1.5rem; margin-bottom: 1rem; font-size: 1rem; line-height: 1.625rem; + flex-direction: column; + .recal-text { + margin-bottom: 1.25rem; + font-weight: $font-weight-bold; + } } .mobile-service-zip-question { :deep(.form-test-error span) { diff --git a/src/layouts/service-location/shop-address/shop-address.vue b/src/layouts/service-location/shop-address/shop-address.vue index cf7239f6..6b4d7402 100644 --- a/src/layouts/service-location/shop-address/shop-address.vue +++ b/src/layouts/service-location/shop-address/shop-address.vue @@ -6,7 +6,9 @@ class="shop-address" aria-live="polite">
{{ questionText }}
-
+
{{ displayedShopName }} {{ modelValue.distanceInMiles?.toFixed(2) }} mi @@ -16,10 +18,22 @@ {{ getProviderCityZipState(modelValue) }}
+
+ {{ `No shops found within 100 miles of ${internalZipcode}` }} +
+