Another pass for change location modal updates

This commit is contained in:
Alex Humphries 2026-01-09 12:17:04 -05:00
parent 144f04ce38
commit 6eab5c2040
9 changed files with 244 additions and 164 deletions

View file

@ -257,6 +257,9 @@ export default {
border-radius: 1.5rem;
overflow: auto;
}
.modal-header {
justify-content: center;
}
}
}
&.show .modal-dialog {

View file

@ -16,7 +16,8 @@
class="input-wrapper"
:class="[
includeSearchIcon ? 'has-search-icon' : '',
includeSelectIcon ? 'has-select-icon' : ''
includeSelectIcon ? 'has-select-icon' : '',
hasTextButton ? 'has-text-button' : ''
]">
<input
:id="inputId"
@ -61,6 +62,13 @@
data-bs-toggle="modal"
:data-bs-target="'#' + cmsWidgetName"
aria-label="Select button" />
<buttonMain
v-if="hasTextButton"
ref="buttonMain"
variant="primary"
:buttonText="buttonText"
@clickEvent="buttonCallback">
</buttonMain>
</div>
<div
v-show="errorMessage"
@ -76,9 +84,13 @@
<script>
import { useField, validate } from 'vee-validate';
import { extractSpanText } from '@/helpers/text-helper';
import buttonMain from '@/ux-components/button-main/button-main.vue';
export default {
name: 'textbox-question',
components: {
buttonMain
},
props: {
type: {
type: String,
@ -118,7 +130,11 @@ export default {
includeSelectIcon: Boolean,
min: String,
max: String,
disableAutoFill: Boolean
disableAutoFill: Boolean,
buttonCallback: {
type: Function,
default: () => {}
}
},
emits: ['focus', 'update:modelValue', 'textboxQuestionEvent.inputIdAssigned', 'click-event'],
setup(props) {
@ -161,6 +177,12 @@ export default {
questionTextForAria() {
return extractSpanText(this.getCmsContent(this.cmsWidgetName, 'QuestionText'));
},
buttonText() {
return this.getCmsContent(this.cmsWidgetName, 'ButtonText');
},
hasTextButton() {
return this.buttonText && this.buttonText.length > 0;
},
value: {
get() {
return this.modelValue;
@ -318,6 +340,12 @@ input::-webkit-date-and-time-value {
display: flex;
}
}
&.has-text-button {
display: flex;
input[type='text'] {
border-radius: 1.5rem;
}
}
}
input {
&.has-icon {

View file

@ -5,7 +5,7 @@
<baseInputButton
v-bind="$props"
v-model="selectedValue"
buttonWrapperClasses="list-group base-input-button list-button rounded-3 d-flex flex-column w-100 mb-2">
buttonWrapperClasses="list-group base-input-button list-button rounded-3 d-flex flex-column w-100 no-hover">
<div
:aria-label="buttonLabel"
class="button-content list-button-content d-flex flex-column justify-content-center py-3 px-4">
@ -15,9 +15,10 @@
class="m-0 button-label-copy"
:class="textPosition">{{ buttonLabel }}
</span>
<span class="separator"></span>
<span
id="buttonLabelSubCopySpan"
class="m-0 caption ms-2"
class="m-0 button-label-sub-copy"
:class="textPosition">{{ buttonLabelSubCopy }}
</span>
<div
@ -41,7 +42,7 @@
<span
v-if="buttonBodyCopy"
id="buttonBodyCopy"
class="m-0 button-label-sub-copy small"
class="m-0 button-body-copy"
v-html="buttonBodyCopy"></span>
</div>
<span
@ -103,116 +104,112 @@ export default {
<style lang="scss" scoped>
@import "@/styles/ux-variables-svg-strings.scss";
$heritage-border-radius: 60px;
$heritage-border-color: #CACBCC;
$heritage-box-shadow: 0 1px 5px rgba($black, 0.2);
$heritage-checked-background-color: #e7f1f6;
$heritage-checked-border-color: #0070d1;
.list-button {
outline: none;
input[type="radio"],
input[type="checkbox"] {
position: static; //override bootstrap
&:focus-visible + .list-button-content {
box-shadow: 0 0 0 2.5px $blue;
}
&:focus + .list-button-content {
box-shadow: 0 0 0 2.5px $blue;
}
&:checked + .list-button-content {
color: $black;
font-weight: 500;
background: $blue-100;
box-shadow: 0 0 0 1px $blue;
}
&:checked:focus + .list-button-content {
box-shadow: 0 0 0 2.5px $blue;
}
&:checked + .list-button-content p,
&:checked + .list-button-content span {
font-weight: 500;
}
}
outline: none;
input[type="radio"],
input[type="checkbox"] {
position: static; //override bootstrap
&:checked + .list-button-content {
background: $heritage-checked-background-color;
border-color: $heritage-checked-border-color;
.button-label-copy {
font-weight: 500;
color: $black;
}
}
&:focus:checked + .list-button-content {
border-width: 2.5px;
}
}
}
.list-button-content {
color: $gray-600;
position: relative;
background: $white;
transition: all 150ms linear;
border-radius: $border-radius-lg;
border: 1px solid $gray-500;
width: 100%;
outline: none;
span {
&.small {
font-size: 0.75rem;
color: $gray-550;
}
}
color: $darker-gray;
font-weight: $font-weight-normal;
position: relative;
background: $white;
transition: all 150ms linear;
border-radius: $heritage-border-radius;
border: 1px solid $heritage-border-color;
box-shadow: $heritage-box-shadow;
width: 100%;
outline: none;
margin-bottom: .625rem;
height: 4.375rem;
}
.button-content {
row-gap: 0.25rem;
row-gap: 0.25rem;
.row-one {
display: flex;
align-items: center;
line-height: 1.5rem;
.row-one {
display: flex;
align-items: center;
justify-content: center;
line-height: 1.5rem;
.button-label-copy {
font-weight: 500;
}
.separator {
height: 1.25rem;
margin: 0rem .3125rem;
border-left: 1px solid $heritage-border-color;
}
.availability-indicator {
background-repeat: no-repeat;
display: flex;
align-items: center;
margin-left: auto;
padding: 0.125rem 0.5rem;
.availability-indicator {
background-repeat: no-repeat;
display: flex;
align-items: center;
margin-left: auto;
padding: 0.125rem 0.5rem;
.availability-badge {
display: inline-flex;
width: 13px;
height: 12px;
background-position: center;
background-repeat: no-repeat;
margin: 0 0.25rem 0 0;
.availability-badge {
display: inline-flex;
width: 13px;
height: 12px;
background-position: center;
background-repeat: no-repeat;
margin: 0 0.25rem 0 0;
&.green {
background-image: url($svg-shop-list-button-green-availability);
}
&.green {
background-image: url($svg-shop-list-button-green-availability);
}
&.orange {
background-image: url($svg-shop-list-button-orange-availability);
}
}
.button-auxillary-copy {
box-sizing: border-box;
justify-content: right;
line-height: 1.25rem;
font-weight: 500;
font-size: 0.75rem;
align-items: center;
}
&.orange {
background-image: url($svg-shop-list-button-orange-availability);
}
}
.button-auxillary-copy {
box-sizing: border-box;
justify-content: right;
line-height: 1.25rem;
font-weight: 500;
font-size: 0.75rem;
align-items: center;
}
&.green {
color: $green-700;
background-color: $green-100;
}
&.green {
color: $green-700;
background-color: $green-100;
}
&.orange {
color: $orange-600;
background-color: $orange-100;
}
&.orange {
color: $orange-600;
background-color: $orange-100;
}
&.gray {
color: $gray-600;
background-color: $gray-100;
padding-left: 0.125rem;
padding-right: 0.125rem;
}
}
}
&.gray {
color: $gray-600;
background-color: $gray-100;
padding-left: 0.125rem;
padding-right: 0.125rem;
}
}
}
.row-two {
text-align: left;
}
.row-two {
text-align: center;
}
}
</style>

View file

@ -77,6 +77,7 @@
@updated-mobile-ctu="setCtuForMobile" />
<shopAddress
v-model="selectedProvider"
:serviceZipcode="zipCode"
:isDisplayed="isInshop"
:selectAppointmentType="selectedAppointmentType"
modalWidgetName="ChangeLocationModalWidget"
@ -168,6 +169,7 @@ export default {
const mobileFeePartPromise = getPricedMobileFeePart(serviceZipCode);
const serviceabilityDetailsPromise = getServiceabilityDetails(serviceZipCode);
const getGlassFeesPromise = useMainStore().getGlassFees();
console.log('Fetching providers for zip code:', serviceZipCode);
const providersPromise = useMainStore().getProviders(serviceZipCode);
// Settle promises and get results
@ -191,7 +193,7 @@ export default {
{
resultKey: 'zipCodeData',
promise: zipCodeData
},
},
{
resultKey: 'providers',
promise: providersPromise
@ -361,7 +363,7 @@ export default {
async forwardButtonAction() {
let provider = this.selectedProvider;
this.mainStore.updateMobileFee(null);
if (this.isMobileLocationDisplayed) {
if (this.isMobile) {
if (this.mainStore.isNoComp || this.mainStore.isITAC) {
this.mainStore.updateMobileFee(this.mobileFeePart);
}
@ -415,6 +417,9 @@ export default {
);
},
getServiceZipCodeFromStore() {
console.log('Getting service zip code from store');
console.log(useMainStore().order.serviceLocation.zipCode);
console.log(useMainStore().order.customer.address.zipCode);
return (
useMainStore().order.serviceLocation.zipCode
|| useMainStore().order.customer.address.zipCode
@ -454,7 +459,17 @@ export default {
if (providers) {
this.setMobileProviderNumber(providers.mobileProviderNumber);
if(providers.shopProviders.length > 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];
}
}

View file

@ -1,14 +1,13 @@
<template>
<textboxQuestion
ref="zipInputTextQuestion"
v-model="value"
v-model="internalZipcode"
:cmsWidgetName="cmsWidgetName"
inputId="serviceZipCode"
questionAlignment="center"
cornerStyle="rounded"
mask="#####"
:displayQuestionText="false"
isRequired
:buttonCallback="updateModelValue"
validationRules="zip-required|zip-format" />
</template>
@ -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;
}
}
};
</script>
<style scoped lang="scss">
.textbox-question {
:deep(.form-label) {
font-weight: $font-weight-normal;
color: $darker-gray;
}
}
</style>

View file

@ -17,11 +17,11 @@
<div class="current-address">
<div class="address-line">
<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>
</div>
<div class="address-line">{{ toTitleCase(modelValue.address.streetAddress) }}</div>
<div class="address-line">{{ toTitleCase(modelValue.address?.streetAddress) }}</div>
<div class="address-line">
{{ toTitleCase(modelValue.address.city) }}, {{ modelValue.address.state }} {{ modelValue.address.zipCode }}
{{ toTitleCase(modelValue.address?.city) }}, {{ modelValue.address?.state }} {{ modelValue.address?.zipCode }}
</div>
</div>
<buttonMain
@ -34,6 +34,7 @@
:onModalOpenedCallback="onModalOpened"
:onModalClosedCallback="onModalClosed"
:footerButtonText="modalFooterText"
:modalPosition="'center'"
@footerButtonEvent="updateSelectedProvider">
<buttonQuestion
ref="buttonQuestion"
@ -41,13 +42,15 @@
buttonTypeString="shopListButton"
:buttonTypeObject="shopListButton"
class="radioQuestion"
:questionText="questionText"
:answers="nearbyShops"
:answers="nearbyShopsData"
groupName="chooseShop"
textPosition="text-start"
isRequired
validationRules="option-required"
:additionalButtonData="additionalButtonData" />
<span class="service-zip-prompt">
{{ serviceZipPrompt }}
</span>
<serviceZipQuestion
ref="serviceZipCodeQuestion"
v-model="internalZipcode"
@ -98,6 +101,7 @@ export default {
type: Object,
default: () => 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;
}
}
</style>

View file

@ -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();
}
}

View file

@ -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;
$border-input-focus: 2.5px solid $heritage-blue-primary;

View file

@ -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 {