Merge branch 'develop' into dependabot/npm_and_yarn/eslint-9.26.0

This commit is contained in:
Jeremy-Z 2026-02-03 13:30:51 -05:00
commit e74f83cc66
38 changed files with 1839 additions and 2815 deletions

1688
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -66,9 +66,9 @@ const bailoutMessage = Object.freeze({
message: 'User selected to continue a referral where a TPA shop was previously selected.'
}),
vehicleYMMSLookupError: (year, make, model, style, error) => {
const baseMessage = "An error occurred looking up vehicle";
const baseMessage = 'An error occurred looking up vehicle';
const errorMessage = `Error: ${getItemData(error)}`;
const getMessage = (year, make, model, style) => {
if (!year) return `${baseMessage} years. ${errorMessage}`;
if (!make) return `${baseMessage} makes for Year: ${year}. ${errorMessage}`;
@ -76,12 +76,12 @@ const bailoutMessage = Object.freeze({
if (!style) return `${baseMessage} styles for Year: ${year}, Make: ${make}, Model: ${model}. ${errorMessage}`;
return `${baseMessage} with Year: ${year}, Make: ${make}, Model: ${model}, Style: ${style}. ${errorMessage}`;
};
return {
code: bailoutCode.VehicleYMMSLookupError,
message: getMessage(year, make, model, style)
};
},
}
});
export default bailoutMessage;

View file

@ -50,9 +50,6 @@ export const pageProgressMapper = {
'provider-preference': {
percent: 60
},
'service-location': {
percent: 65
},
'schedule-page': {
percent: 70
},

View file

@ -153,6 +153,10 @@ import { selectableDaysOptions } from './mixins/helpers';
export default {
name: 'date-picker',
props: {
activeAppointmentType: {
type: String,
default: null
},
customComponentId: String,
selectableDatesSetting: {
type: String,
@ -161,6 +165,10 @@ export default {
},
default: selectableDaysOptions.PAST
},
isMobileView: {
type: Boolean,
default: false
},
modelValue: {
type: Object
},
@ -241,8 +249,7 @@ export default {
daysLoaded: 0,
appointmentDurationMinutesMinimum: 90,
appointmentDurationMinutesMaximum: 120,
initialDaysToLoad: 15,
isMobileView: false
initialDaysToLoad: 15
};
},
computed: {
@ -284,6 +291,16 @@ export default {
}
},
watch: {
isMobileView(newValue, oldValue) {
if (newValue !== oldValue) {
this.activePageIndex = 0;
this.selectedDate = null;
this.selectedTimeOfDayGrouping = null;
this.selectedTime = null;
this.selectedTimeSlot = null;
this.findFirstAvailableDateInView();
}
},
selectedTimeSlot(newValue, oldValue) {
if (newValue !== oldValue) {
const testObj = this.getSelectedTimeSlotInfoObject(newValue);
@ -296,20 +313,30 @@ export default {
}
}
},
mounted() {
this.mql = window.matchMedia('(min-width: 1200px)');
this.isMobileView = !this.mql.matches;
this.mql.addEventListener('change', this.handleMqlChange);
},
unmounted() {
if (this.mql) {
this.mql.removeEventListener('change', this.handleMqlChange);
}
},
methods: {
initializeComponent(initialData) {
this.resetComponent();
this.setCalendarData(initialData);
},
resetComponent() {
this.isLoading = true;
this.selectableDatesData = []; // NOTE: uses monthNum (1-based), NOT monthIndex (0-based)
this.selectableTimeSlotsData = [];
this.selectedTimeOfDayGrouping = null;
this.selectedDate = null;
this.selectedTime = null;
this.selectedTimeSlot = null;
this.activeDate = new Date();
this.activePageIndex = 0;
this.todaysDate = new Date();
this.mql = null;
this.daysInViewMobile = 3;
this.daysInViewStandard = 5;
this.daysLoaded = 0;
this.appointmentDurationMinutesMinimum = 90;
this.appointmentDurationMinutesMaximum = 120;
this.initialDaysToLoad = 15;
},
addPremiumFlagToInput(routeCode) {
return (`${routeCode}${PREMIUM_TIME_SLOT_ID_FLAG}`);
},
@ -352,17 +379,8 @@ export default {
isPremiumAppointment: null
};
},
handleMqlChange(e) {
this.isMobileView = !e.matches;
this.activePageIndex = 0;
this.selectedDate = null;
this.selectedTimeOfDayGrouping = null;
this.selectedTime = null;
this.selectedTimeSlot = null;
this.findFirstAvailableDateInView();
},
displayTimeSlotTime(timeSlot) {
const { appointmentType } = this.mainStore.order.serviceLocation;
const appointmentType = this.activeAppointmentType || AppointmentTypeStrings.IN_SHOP;
if (appointmentType === AppointmentTypeStrings.MOBILE || appointmentType === AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP) {
return `${militaryToTwelveHourTime(timeSlot.startTime)} - ${militaryToTwelveHourTime(timeSlot.endTime)}`;
}
@ -434,38 +452,6 @@ export default {
const dateToShow = this.getDateToShow(index);
return convertDateToDateString(dateToShow);
},
getEndDateForExistingDate(startDate, selectedDateString, daysInView) {
const endDate = new Date(startDate);
endDate.setDate(endDate.getDate() + (daysInView - 1));
if (selectedDateString) {
const selectedDate = new Date(`${selectedDateString}T00:00:00`);
const daysDifferenceComparedToStartDate = Math.ceil((selectedDate - startDate) / (1000 * 60 * 60 * 24));
if (selectedDate > endDate) {
const daysDifferenceComparedToStartDateAsPages = Math.ceil(daysDifferenceComparedToStartDate / daysInView);
const newEndDate = new Date(startDate);
newEndDate.setDate(newEndDate.getDate() + (daysDifferenceComparedToStartDateAsPages * daysInView) - 1);
return {
endDateString: convertDateToDateString(newEndDate),
newDaysLoaded: (daysDifferenceComparedToStartDateAsPages * daysInView),
daysFromStart: daysDifferenceComparedToStartDate
};
}
return {
endDateString: convertDateToDateString(endDate),
newDaysLoaded: null,
daysFromStart: daysDifferenceComparedToStartDate
};
}
return {
endDateString: convertDateToDateString(endDate),
newDaysLoaded: null,
daysFromStart: null
};
},
async gotoNextPage() {
const maxPageIndex = Math.floor((this.daysLoaded - 1) / this.daysToAdd);
if (this.activePageIndex >= maxPageIndex) {
@ -572,66 +558,7 @@ export default {
timeSlotInputChanged(timeSlot) {
this.selectTimeSlotForDay(timeSlot);
},
async loadInitialData(config) {
/*
** NOTE: this _could_ be called by a parent before fully loaded, so data or computeds might not be available
*/
let todayDateString;
const todayDateObject = new Date();
const calendarViewDirection = 'future';
const initialDays = 15;
if (this.todayString) {
todayDateString = this.todayString;
} else if (config.todayOverrideDateString) {
todayDateString = config.todayOverrideDateString;
} else {
todayDateString = convertDateToDateString(todayDateObject);
}
const initialViewStartDate = todayDateString;
const initialEndDate = new Date();
initialEndDate.setDate(initialEndDate.getDate() + (initialDays - 1));
let initialViewEndDate = convertDateToDateString(initialEndDate);
const endDateObject = this.getEndDateForExistingDate(
todayDateObject,
config.preSelectedDate,
initialDays
);
if (endDateObject.endDateString !== initialViewEndDate) {
initialViewEndDate = endDateObject.endDateString;
}
const loadInitialDataPromise = new Promise((resolve) => {
const response = config.customSelectableDatesCallback(
initialViewStartDate,
initialViewEndDate,
useMainStore().order.serviceLocation.appointmentType,
useMainStore().order.serviceLocation.provider.providerNumber
);
resolve(response);
});
return loadInitialDataPromise.then((response) => {
const initialData = {
todayDate: todayDateString,
initialViewStartDate,
initialViewEndDate,
calendarViewDirection,
initialShopTimeSlotsResponse: response,
preSelectedDate: config.preSelectedDate,
initialDaysLoaded: endDateObject.newDaysLoaded || initialDays,
daysFromStart: endDateObject.daysFromStart || null
};
return initialData;
});
},
async setCalendarData(config = {}) {
const initialMql = window.matchMedia('(min-width: 1200px)');
this.isMobileView = !initialMql.matches;
config.initialShopTimeSlotsResponse.days.forEach((selectableDate) => {
const dateObjectToPush = {
date: selectableDate.date,
@ -663,8 +590,11 @@ export default {
this.findInitialDate(config.preSelectedDate, pageIndexObj.initialDayIndex);
} else {
this.findFirstAvailableDateInView();
while (!this.selectedDate) {
this.gotoNextPage();
let attempts = 0;
while (!this.selectedDate && attempts < 5) {
// eslint-disable-next-line no-await-in-loop
await this.gotoNextPage();
attempts += 1;
}
}
@ -693,9 +623,7 @@ export default {
const moreSelectableDates =
await this.customSelectableDatesCallback(
dateStart,
dateEnd,
this.mainStore.order.serviceLocation.appointmentType,
this.mainStore.order.serviceLocation.provider.providerNumber
dateEnd
);
moreSelectableDates.days.forEach((selectableDate) => {
@ -899,7 +827,7 @@ export default {
border-radius: $border-radius-list-button;
box-shadow: 0 1px 5px 0 rgba(0, 0, 0, .2);
color: #525656;
padding: 0.5rem 0;
padding: 0.625rem 0;
margin-top: 0.625rem;
text-align: center;
cursor: pointer;

View file

@ -76,7 +76,7 @@ export function getMobileFeeLineItem(order) {
* @returns {number|undefined|null} current deductible
*/
export function getDeductible(order) {
return order.currentDeductible;
return order.damage.isRepair ? order.currentDeductible.repair : order.currentDeductible.replace;
}
/**

View file

@ -239,8 +239,13 @@ describe('cart-helper', () => {
describe('getDeductible', () => {
test('Returns deductible on the order', () => {
// Arrange
const deductible = 100;
const deductible = {
replace: 100
};
const order = {
damage: {
isRepair: false
},
currentDeductible: deductible
};
@ -249,7 +254,7 @@ describe('cart-helper', () => {
// Assert
expect(result).not.toBeNull();
expect(result).toBe(deductible);
expect(result).toBe(deductible.replace);
});
});
@ -349,7 +354,12 @@ describe('cart-helper', () => {
coverageStatus: coverageStatuses.VERIFIED,
coverageType: type
},
currentDeductible: 100,
damage: {
isRepair: false
},
currentDeductible: {
replace: 100
},
lineItems: nullLineItems
};
@ -373,7 +383,12 @@ describe('cart-helper', () => {
coverageStatus: coverageStatuses.VERIFIED,
coverageType: type
},
currentDeductible: 100,
damage: {
isRepair: false
},
currentDeductible: {
replace: 100
},
lineItems: emptyLineItems
};
@ -397,7 +412,12 @@ describe('cart-helper', () => {
coverageStatus: coverageStatuses.VERIFIED,
coverageType: type
},
currentDeductible: 100,
damage: {
isRepair: false
},
currentDeductible: {
replace: 100
},
lineItems: defaultLineItems
};
@ -495,7 +515,12 @@ describe('cart-helper', () => {
coverageStatus: coverageStatuses.VERIFIED,
coverageType: type
},
currentDeductible: 100,
damage: {
isRepair: false
},
currentDeductible: {
replace: 100
},
lineItems: nullLineItems
};
@ -520,7 +545,12 @@ describe('cart-helper', () => {
coverageStatus: coverageStatuses.VERIFIED,
coverageType: type
},
currentDeductible: 100,
damage: {
isRepair: false
},
currentDeductible: {
replace: 100
},
lineItems: emptyLineItems
};
@ -545,7 +575,12 @@ describe('cart-helper', () => {
coverageStatus: coverageStatuses.VERIFIED,
coverageType: type
},
currentDeductible: 100,
damage: {
isRepair: false
},
currentDeductible: {
replace: 100
},
lineItems: defaultLineItems
};

View file

@ -306,6 +306,7 @@ function mapStringToState(str) {
const valueFromStore = getStoreValueFromString(match[2]);
if (!valueFromStore) {
console.warn('Unable to resolve global state data.');
console.warn(`Tried to resolve: ${match[2]}`);
return ''; // if we can't map our string to state data, return an empty string.
}
const stringWithReplacement = str.replace(match[0], valueFromStore);

View file

@ -27,6 +27,17 @@ export function createOrderedListFromStringOfParagraphs(stringOfParagraphs) {
return `<ol>${stringOfParagraphs.replaceAll('<p></p>', '').replaceAll('<p>', '<li>').replaceAll('</p>', '</li>')}</ol>`;
}
/**
* @function createUnorderedListFromStringOfParagraphs
* @summary
* Returns string with <p> tags changed to <li> and wrapped in an <ul>
* @param {string} stringOfParagraphs single string with 1 to N <p> tags
* @returns {string} converted string wrapped in an <ul>
*/
export function createUnorderedListFromStringOfParagraphs(stringOfParagraphs) {
return `<ul>${stringOfParagraphs.replaceAll('<p></p>', '').replaceAll('<p>', '<li>').replaceAll('</p>', '</li>')}</ul>`;
}
/**
* @function toTitleCase
* @summary Returns title cased string version of 'text'

View file

@ -2,23 +2,13 @@
exports[`coverageStatement.vue returns the initial data 1`] = `
Object {
"CANCEL_CLAIM_REF_NAME": "CancelClaimModal",
"DEDUCTIBLE_MODAL_REF_NAME": "DeductibleModal",
"RECAL_MODAL_REF_NAME": "RecalModal",
"SITE_FOOTER_REF_NAME": "siteFooter",
"baseServiceLineItems": Array [],
"deductibleText": "Your deductible is",
"rules": Object {
"selectionRequired": "option-required",
},
"selectedProvider": "",
"widget": Object {
"disclaimerText": "DisclaimerWidget",
"explanatoryText": "ExplanatoryTextWidget",
"nextStep": "NextStepsWidget",
"serviceProviderQuestion": "ServiceProviderQuestion",
"subheader": "SiteSubHeaderWidget",
"verifiedItacAlert": "VerifiedITACAlert",
},
}
`;

View file

@ -29,6 +29,7 @@ jest.mock('@/helpers/cms-content-helper', () => ({
jest.mock('@/helpers/text-helper', () => ({
createOrderedListFromStringOfParagraphs: jest.fn(),
createUnorderedListFromStringOfParagraphs: jest.fn(),
formatAmountInDollars: jest.fn()
}));
@ -98,9 +99,6 @@ function getMountedComponent(mainInitialState = {}, initialData = {}, methodToRu
fetchCmsContentForPage.mockImplementation(() => Promise.resolve());
const wrapper = shallowMount(coverageStatement, mountOptions);
wrapper.vm.$refs[CANCEL_CLAIM_REF_NAME].openModal = jest.fn();
wrapper.vm.$refs.siteFooter.disableForwardButton = jest.fn();
wrapper.vm.$refs.siteFooter.enableForwardAction = jest.fn();
return { wrapper };
}
@ -165,16 +163,6 @@ describe('coverageStatement.vue', () => {
// Assert
expect(secondaryText.exists()).toBe(true);
});
test('Should render site footer', () => {
// Arrange
const { wrapper } = getMountedComponent({});
// Act
const footer = wrapper.findComponent({ ref: 'siteFooter' });
// Assert
expect(footer.exists()).toBe(true);
});
});
describe('Computed', () => {
describe.each([
@ -373,7 +361,7 @@ describe('coverageStatement.vue', () => {
[false, coverageStatuses.PENDING, coverageType.NO_COMP],
[false, coverageStatuses.PENDING, coverageType.Deductible],
[true, coverageStatuses.VERIFIED, coverageType.NO_COMP],
[true, coverageStatuses.VERIFIED, coverageType.ITAC],
[false, coverageStatuses.VERIFIED, coverageType.ITAC],
[false, coverageStatuses.VERIFIED, coverageType.Deductible],
[false, coverageStatuses.NO_COVERAGE, coverageType.NO_COMP],
[false, coverageStatuses.NO_COVERAGE, coverageType.ITAC],
@ -512,7 +500,10 @@ describe('coverageStatement.vue', () => {
coverageStatus: coverageStatuses.PENDING,
coverageType: coverageType.NONE
},
currentDeductible: null
currentDeductible: {
repair: null,
replace: null
}
}
};
const { wrapper } = getMountedComponent(mainInitialState);
@ -539,7 +530,9 @@ describe('coverageStatement.vue', () => {
coverageStatus: coverageStatuses.VERIFIED,
coverageType: coverageType.Deductible
},
currentDeductible: deductible
currentDeductible: {
replace: deductible
}
}
};
const { wrapper } = getMountedComponent(mainInitialState);
@ -566,7 +559,9 @@ describe('coverageStatement.vue', () => {
coverageStatus: coverageStatuses.VERIFIED,
coverageType: coverageType.ITAC
},
currentDeductible: deductible
currentDeductible: {
replace: deductible
}
}
};
const { wrapper } = getMountedComponent(mainInitialState);
@ -584,7 +579,7 @@ describe('coverageStatement.vue', () => {
undefined
);
});
test('If Verified ITAC, selected other shop, navigate forward w/ CLICKED_FORWARD_WITH_NON_SAFELITE_SHOP', () => {
test('If Verified ITAC, selected Cancel, navigate forward w/ CLICKED_FORWARD_WITH_NON_SAFELITE_SHOP', () => {
// Arrange
const deductible = servicePrice + 1;
const mainInitialState = {
@ -596,7 +591,9 @@ describe('coverageStatement.vue', () => {
coverageStatus: coverageStatuses.VERIFIED,
coverageType: coverageType.ITAC
},
currentDeductible: deductible
currentDeductible: {
replace: deductible
}
}
};
const { wrapper } = getMountedComponent(mainInitialState);
@ -605,7 +602,7 @@ describe('coverageStatement.vue', () => {
});
// Act
wrapper.vm.navigateForward();
wrapper.vm.cancelClaim();
// Assert
expect(wrapper.vm.$router.navigate)
@ -667,7 +664,7 @@ describe('coverageStatement.vue', () => {
});
// Act
wrapper.vm.navigateForward();
wrapper.vm.cancelClaim();
// Assert
expect(wrapper.vm.$router.navigate)
@ -776,7 +773,9 @@ describe('coverageStatement.vue', () => {
vehicle: {
policyVehicleId: 1
},
currentDeductible: deductible
currentDeductible: {
replace: deductible
}
},
issConfig: {
isClaimRegistrationRequired: true

View file

@ -15,72 +15,87 @@
<div class="coverage-statement-container iss-heritage-content-container-width">
<h5
ref="siteSubHeader"
class="sub-header text-black mt-4"
class="sub-header text-black"
v-html="coverageStatementSubHeader"></h5>
<div
ref="explanatoryText"
class="body-text mt-2"
class="body-text"
v-html="explanatoryText"></div>
<div
ref="explanatoryText2"
class="body-text mt-3"
v-html="explanatoryText2"></div>
<div
ref="secondaryText"
class="mt-4 mb-1 fw-bold text-black"
v-html="secondaryText"></div>
<div
v-if="isDeductibleVisible"
class="d-flex justify-content-center cost">
v-if="isITACQuoteVisible"
class="itac-price-container">
<div class="itac-deductible-value-container">
<div class="itac-deductible-value-text">
Your deductible:
</div>
<div class="itac-deductible-value">
{{ formatAmountInDollars(deductibleValue) }}
</div>
</div>
<div class="separator"></div>
<div class="itac-cost-container">
<div class="itac-cost-text">
Safelite price*:
</div>
<div class="cost">
{{ formatAmountInDollars(totalServicePrice) }}
</div>
</div>
</div>
<div
v-if="showDeductibleOnly"
class="d-flex cost cost-underline">
{{ formatAmountInDollars(deductibleValue) }}
</div>
<div
v-if="isQuoteDisplayed"
class="d-flex justify-content-center cost mb-0">
{{ formatAmountInDollars(totalServicePrice) }}
</div>
<div
v-if="isITACQuoteVisible"
class="d-flex justify-content-center mb-4 deductible-text">
{{ deductibleText }}&nbsp;
<span class="text-success fw-bold">{{
formatAmountInDollars(deductibleValue)
}}</span>
</div>
<alert
v-if="isITACQuoteVisible"
ref="verifiedITACAlert"
class="mb-5"
cmsWidgetName="VerifiedITACAlert"
:manualHeadline="verifiedItacAlertHeader"
:manualCopy="verifiedItacAlertBody"
alertClass="alert-success"
:isDismissible="false">
</alert>
<div
class="fw-bold text-black mt-5 mb-2"
class="fw-bold text-black mt-5"
v-html="nextStepsHeader"></div>
<div
class="body-text"
v-html="nextStepsBody"></div>
<buttonQuestion
<div
v-if="isNoCompQuoteVisible"
class="no-comp-price-text">
Safelite price*:
</div>
<div
v-if="isQuoteDisplayed"
v-model="selectedProvider"
cmsWidgetName="ServiceProviderQuestion"
:questionText="serviceProviderQuestionText"
:answers="serviceProviderQuestionAnswers"
groupName="ServiceProviderQuestionOption"
buttonTypeString="listButton"
isRequired
:validationRules="rules.selectionRequired">
</buttonQuestion>
<siteFooter
:ref="SITE_FOOTER_REF_NAME"
class="mt-5"
cmsWidgetName="SiteFooterWidget"
:isForwardActionDisabled="!meta.valid"
@backClicked="navigateBackByVehicleQuestions"
@forwardClicked="navigateForward" />
class="d-flex justify-content-center cost mb-0 cost-underline">
{{ formatAmountInDollars(totalServicePrice) }}
</div>
<buttonMain
ref="buttonMain"
class="full-width-button mt-5 mb-5"
variant="navigation"
:buttonText="buttonText"
@clickEvent="navigateForward" />
<textLink
v-if="isNoCompQuoteVisible || isITACQuoteVisible"
class="underlined-text cancel-link mt-5"
linkType="text"
text="No, I want to cancel"
href="#"
@clickEvent="cancelClaim" />
<textBlock
v-if="isQuoteDisplayed"
v-if="isDisclaimerVisible"
class="mt-5 mb-5"
:customText="disclaimerText"
typeStyle="caption" />
<div :class="getVariant + ' mb-5'">
<textLink
linkType="navigation"
text="Back"
href="#"
@clickEvent="navigateBackByVehicleQuestions" />
</div>
</div>
</div>
</div>
@ -88,28 +103,17 @@
:ref="RECAL_MODAL_REF_NAME"
cssModalHeadlineClass="text-center"
cmsWidgetName="RecalModal" />
<contentGroupModal
:ref="DEDUCTIBLE_MODAL_REF_NAME"
cmsWidgetName="DeductibleModal"
class="deductible-modal" />
<cancelClaimModal
:ref="CANCEL_CLAIM_REF_NAME"
@cancelClaimConfirmation="cancelClaim"
@returnToClaim="continueClaim" />
</Form>
</template>
<script>
// Import Component
import { Form } from 'vee-validate';
import siteFooter from '@/iss-components/site-footer/site-footer.vue';
import siteHeader from '@/iss-components/site-header/site-header.vue';
import alert from '@/ux-components/alert/alert.vue';
import cancelClaimModal from '@/layouts/coverage-statement/cancel-claim-modal/cancel-claim-modal.vue';
import contentGroupModal from '@/iss-components/content-group-modal/content-group-modal.vue';
import buttonQuestion from '@/digital-components/button-question/button-question.vue';
import textBlock from '@/digital-components/text-block/text-block.vue';
import textLink from '@/ux-components/text-link/text-link.vue';
import buttonMain from '@/ux-components/button-main/button-main.vue';
// Import Supporting Files
import {
fetchCmsContentForPage,
@ -121,35 +125,28 @@ import settleAllPromises from '@/helpers/layout-helper.js';
import { getDamageString } from '@/helpers/damage-helper.js';
import { useMainStore } from '@/store/index.js';
import vehicleQuestionsMixin from '@/mixins/vehicle-questions-mixin.js';
import globalRules from '@/constants/global-rules.js';
import baseFormMixin from '@/mixins/base-form-mixin.js';
import navigationScenarios from '@/router/router-constants/navigation-scenarios.js';
import bailoutMessage from '@/constants/bailoutMessage';
import widgetFields from '@/constants/cms-widget-fields.js';
import { formatAmountInDollars, createOrderedListFromStringOfParagraphs } from '@/helpers/text-helper.js';
import { formatAmountInDollars, createUnorderedListFromStringOfParagraphs, createOrderedListFromStringOfParagraphs } from '@/helpers/text-helper.js';
import showIssLoadingModal from '@/helpers/loading-modal-helper';
import { getPriceOfLineItems } from '@/helpers/price-calculator';
import coverageStatuses from '@/constants/coverage-statuses';
import coverageType from '@/constants/coverage-type';
const CANCEL_CLAIM_REF_NAME = 'CancelClaimModal';
const SAFELITE_PROVIDER = 'Safelite';
const RECAL_MODAL_REF_NAME = 'RecalModal';
const DEDUCTIBLE_MODAL_REF_NAME = 'DeductibleModal';
const SITE_FOOTER_REF_NAME = 'siteFooter';
export default {
name: 'coverage-statement',
components: {
siteFooter,
siteHeader,
// eslint-disable-next-line vue/no-reserved-component-names
Form,
alert,
cancelClaimModal,
contentGroupModal,
buttonQuestion,
textBlock
buttonMain,
textBlock,
textLink
},
mixins: [baseFormMixin, vehicleQuestionsMixin],
async beforeRouteEnter(to, from, next) {
@ -204,24 +201,13 @@ export default {
data() {
return {
baseServiceLineItems: [],
selectedProvider: '',
deductibleText: 'Your deductible is',
// TODO update when design team gives appropriate text
rules: {
selectionRequired: globalRules.OPTION_REQUIRED
},
widget: {
disclaimerText: 'DisclaimerWidget',
subheader: 'SiteSubHeaderWidget',
verifiedItacAlert: 'VerifiedITACAlert',
explanatoryText: 'ExplanatoryTextWidget',
nextStep: 'NextStepsWidget',
serviceProviderQuestion: 'ServiceProviderQuestion'
nextStep: 'NextStepsWidget'
},
CANCEL_CLAIM_REF_NAME,
RECAL_MODAL_REF_NAME,
DEDUCTIBLE_MODAL_REF_NAME,
SITE_FOOTER_REF_NAME
RECAL_MODAL_REF_NAME
};
},
computed: {
@ -231,27 +217,15 @@ export default {
widgetFields.SUB_HEADER_WIDGET.SUB_HEADER_TEXT
);
},
verifiedItacAlertHeader() {
return this.getCmsContent(
this.widget.verifiedItacAlert,
widgetFields.ALERT_WIDGET.HEADLINE_TEXT
);
},
verifiedItacAlertBody() {
secondaryText() {
const itacCostSavings =
this.deductibleValue - this.totalServicePrice;
return this.getCmsContent(
this.widget.verifiedItacAlert,
widgetFields.ALERT_WIDGET.BODY_TEXT
)?.replaceAll(
'{custom:costSavings}',
formatAmountInDollars(itacCostSavings)
);
},
secondaryText() {
return this.getTextFromCmsWithCustomIfStatements(
this.widget.subheader,
widgetFields.SUB_HEADER_WIDGET.SECONDARY_TEXT
)?.replaceAll(
'{custom:costSavings}',
formatAmountInDollars(itacCostSavings)
);
},
disclaimerText() {
@ -266,6 +240,12 @@ export default {
widgetFields.CONTENT_GROUP_WIDGET.BODY_TEXT
);
},
explanatoryText2() {
return this.getTextFromCmsWithCustomIfStatements(
this.widget.explanatoryText,
widgetFields.CONTENT_GROUP_WIDGET.BODY_TEXT_2
)?.replaceAll('{custom:currentReplaceDeductible}', formatAmountInDollars(this.mainStore.order.currentDeductible.replace));
},
nextStepsHeader() {
return this.getTextFromCmsWithCustomIfStatements(
this.widget.nextStep,
@ -278,6 +258,9 @@ export default {
widgetFields.CONTENT_GROUP_WIDGET.BODY_TEXT
)?.replaceAll('{custom:damage}', this.damageText);
if (this.isITACQuoteVisible || this.isNoCompQuoteVisible) {
return createUnorderedListFromStringOfParagraphs(cmsText);
}
return createOrderedListFromStringOfParagraphs(cmsText);
},
damageText() {
@ -285,7 +268,7 @@ export default {
return damageString === 'match' ? '' : damageString;
},
deductibleValue() {
return useMainStore().order.currentDeductible;
return this.mainStore.currentDeductible;
},
isNoCompQuoteVisible() {
return this.mainStore.isVerified && this.mainStore.isNoComp;
@ -309,21 +292,15 @@ export default {
totalServicePrice() {
return getPriceOfLineItems(this.baseServiceLineItems);
},
serviceProviderQuestionText() {
return this.getCmsContent(
this.widget.serviceProviderQuestion,
widgetFields.INPUT_QUESTION_WIDGET.QUESTION_TEXT
);
},
serviceProviderQuestionAnswers() {
return this.getCmsContent(
this.widget.serviceProviderQuestion,
widgetFields.INPUT_QUESTION_WIDGET.ANSWERS
);
},
isQuoteDisplayed() {
return this.isNoCompQuoteVisible;
},
isDisclaimerVisible() {
return this.isITACQuoteVisible || this.isNoCompQuoteVisible;
},
showDeductibleOnly() {
return this.isDeductibleVisible && !this.isRepairZeroDeductible;
},
shouldRegisterClaim() {
const {
vehicle,
@ -342,29 +319,24 @@ export default {
},
isRepair() {
return this.mainStore.order.damage.isRepair;
},
isRepairZeroDeductible() {
return this.isRepair && this.isDeductibleVisible && this.deductibleValue <= 0;
},
buttonText() {
return (this.isITACQuoteVisible || this.isNoCompQuoteVisible) ? 'Continue scheduling' : 'Continue';
},
getVariant() {
if (this.isITACQuoteVisible || this.isNoCompQuoteVisible) {
return 'text-center';
}
return 'text-left';
}
},
watch: {
selectedProvider() {
if (this.selectedProvider) {
if (this.selectedProvider !== SAFELITE_PROVIDER) {
this.$refs[this.CANCEL_CLAIM_REF_NAME].openModal();
this.selectedProvider = '';
this.$refs.siteFooter.disableForwardButton();
} else {
this.$refs.siteFooter.enableForwardAction();
}
}
},
nextStepsBody(newValue, oldValue) {
if (newValue !== oldValue) {
setupModalLink(this, RECAL_MODAL_REF_NAME);
setupModalLink(this, DEDUCTIBLE_MODAL_REF_NAME);
}
},
verifiedItacAlertBody(newValue, oldValue) {
if (newValue !== oldValue) {
setupModalLink(this, DEDUCTIBLE_MODAL_REF_NAME);
}
}
},
@ -421,15 +393,10 @@ export default {
if (this.isUnverifiedVisible || this.isDeductibleVisible) {
this.navigateWithScenario(navigationScenarios.CLICKED_FORWARD);
} else if (this.isITACQuoteVisible || this.isNoCompQuoteVisible) {
useMainStore().updateIsSafeliteProvider(this.selectedProvider === SAFELITE_PROVIDER);
if (this.selectedProvider === SAFELITE_PROVIDER) {
this.navigateWithScenario(navigationScenarios.CLICKED_FORWARD_WITH_SAFELITE);
} else {
useMainStore().setBailout(bailoutMessage.RequestCallback());
this.navigateWithScenario(navigationScenarios.CLICKED_FORWARD_WITH_NON_SAFELITE_SHOP);
}
this.mainStore.updateIsSafeliteProvider(true);
this.navigateWithScenario(navigationScenarios.CLICKED_FORWARD_WITH_SAFELITE);
} else {
useMainStore().setBailout(bailoutMessage.coverageStatementInvalidState());
this.mainStore.setBailout(bailoutMessage.coverageStatementInvalidState());
this.navigateWithScenario(navigationScenarios.CLICKED_FORWARD_WITH_INVALID_STATE);
}
},
@ -449,28 +416,24 @@ export default {
switch (str) {
case 'coverageUnverified':
return this.isUnverifiedVisible;
case 'verifiedDeductible':
return this.isDeductibleVisible;
case 'verifiedITAC':
return this.isITACQuoteVisible;
case 'verifiedNoComp':
return this.isNoCompQuoteVisible;
case 'ADASReplace':
return !isRepair && this.isADAS;
return !isRepair && this.isADAS && !this.isNoCompQuoteVisible && !this.isITACQuoteVisible;
case 'nonADASReplace':
return !isRepair && !this.isADAS;
case 'nonADASRepair':
return isRepair;
case 'deductibleOverZero':
return (
this.isDeductibleVisible && this.deductibleValue > 0
);
case 'isDeductibleZero':
return (
this.isDeductibleVisible && this.deductibleValue <= 0
);
case 'verifiedRepair':
return !isRepair && !this.isADAS && !this.isNoCompQuoteVisible && !this.isITACQuoteVisible;
case 'verifiedRepairDeductible':
return isRepair && this.isDeductibleVisible;
case 'verifiedRepairZeroDeductible':
return isRepair && this.isDeductibleVisible && this.deductibleValue <= 0;
case 'verifiedRepairZeroDeductibleReplaceDeductibleOverZero':
return isRepair && this.isDeductibleVisible && this.deductibleValue <= 0 && this.mainStore.order.currentDeductible.replace > 0;
case 'verifiedRepairDeductibleOverZero':
return isRepair && this.isDeductibleVisible && this.deductibleValue > 0;
case 'verifiedReplaceDeductible':
return !isRepair && this.isDeductibleVisible;
case 'verifiedReplaceZeroDeductible':
return !isRepair && this.isDeductibleVisible && this.deductibleValue <= 0;
case 'verifiedReplaceDeductibleOverZero':
@ -484,13 +447,9 @@ export default {
},
formatAmountInDollars,
cancelClaim() {
useMainStore().updateIsSafeliteProvider(false);
useMainStore().setBailout(bailoutMessage.RequestCallback());
this.mainStore.updateIsSafeliteProvider(false);
this.mainStore.setBailout(bailoutMessage.RequestCallback());
this.navigateWithScenario(navigationScenarios.CLICKED_FORWARD_WITH_NON_SAFELITE_SHOP);
},
continueClaim() {
useMainStore().updateIsSafeliteProvider(true);
this.navigateWithScenario(navigationScenarios.CLICKED_FORWARD_WITH_SAFELITE);
}
}
};
@ -501,46 +460,82 @@ export default {
.coverage-statement-container {
position: relative;
min-height: 1px;
padding-left: .9375rem;
padding-right: .9375rem;
padding-left: .75rem;
padding-right: .75rem;
}
}
.coverage-statement {
.cost {
color: $green;
font-size: 2rem;
font-weight: $font-weight-light;
line-height: 2.75rem;
font-size: 1.675rem;
font-weight: $font-weight-bold;
line-height: 2.5rem;
width: fit-content;
}
.deductible-text {
line-height: 1.5rem;
.cost-underline {
border-bottom: 4px solid #0c7e47;
}
.sub-header {
line-height: 2rem;
line-height: 1.5rem;
margin-top: 20px;
margin-bottom: 20px;
}
.full-width-button {
width: 100%;
}
.underlined-text {
text-decoration: underline;
}
.cancel-link {
display: block;
margin: 24px auto 0 auto;
}
.back-link {
display: inline-block;
align-items: center;
}
:deep(.body-text > ol > ul) {
list-style-position: outside;
list-style-type: disc;
padding-left: 0;
}
:deep(ol) {
padding-left: 1.25rem;
}
:deep(li) {
margin-top: 1px;
padding-left: 5px;
}
:deep(ul) {
padding: 0;
padding-left: 1.125rem;
padding-right: 1.125rem;
line-height: 1.5rem;
font-size: .875rem;
padding-left: 1.25rem;
padding-right: 1.25rem;
line-height: 1.25rem;
li {
margin-bottom: .5rem;
margin-top: .5rem;
padding-left: 5px;
}
}
:deep(p) {
line-height: 1.5rem;
font-size: 0.875rem;
margin-bottom: 0.5rem;
strong {
color: $black;
}
}
:deep(.body-text) {
font-size: 0.875rem;
line-height: 1.5rem;
color: $darker-gray;
strong {
color: $black;
font-weight: 500;
}
margin-top: 6px;
a {
text-decoration: none;
}
}
:deep(.question-text) {
margin-top: 1.5rem;
@ -560,20 +555,45 @@ export default {
}
}
}
}
.itac-price-container {
display: flex;
justify-content: space-around;
border: solid 1px #cacbcc;
border-radius: 5px;
align-items: center;
font-weight: $font-weight-bold;
color: $black;
padding: 10px;
margin-top: 20px;
:deep(.deductible-modal) {
p {
margin-bottom: 0 !important;
.itac-deductible-value-container {
display: flex;
flex-direction: column;
align-items: center;
}
.itac-cost-container {
display: flex;
flex-direction: column;
align-items: center;
}
.itac-deductible-value {
font-size: 1.675rem;
font-weight: $font-weight-normal;
line-height: 2.5rem;
}
.separator {
width: 1px;
height: 60px;
background-color: #cacbcc;
}
}
img.mb-4 {
margin: 0 !important;
.no-comp-price-text {
font-weight: 500;
color: $black;
}
h5 {
color: black;
}
p:last-child {
margin-top: 0.5rem;
:deep(.line-two) {
margin-top: 20px;
display: block;
}
}

View file

@ -84,6 +84,13 @@ const initialStore = {
lastName: 'Test',
servicePhone: '111-111-1111',
emailAddress: 'test@email.com'
},
damage: {
isRepair: false
},
currentDeductible: {
replace: 100,
repair: 0
}
}
};
@ -123,14 +130,20 @@ const sessionStorage = {
vaps: []
},
policy: {},
damage: {},
damage: {
isRepair: false
},
vehicle: {
year: 2000,
make: 'Honda',
model: 'Civic'
},
customer: {},
customerPortalLoginToken: 'token'
customerPortalLoginToken: 'token',
currentDeductible: {
replace: 100,
repair: 0
}
};
function getMountedComponent(mainInitialState = {}, initialData = {}, methodToRun = () => {}, mixin = mockMixin) {

View file

@ -232,7 +232,9 @@ describe('payment-method.vue', () => {
coverageStatus: coverageStatuses.VERIFIED,
coverageType: coverageType.Deductible
},
currentDeductible: 123
currentDeductible: {
replace: 123
}
},
issConfig: {
isClaimRegistrationRequired: true,
@ -286,7 +288,7 @@ describe('payment-method.vue', () => {
test('returns true when coverageStatus is verified and deductibleTotal = 0', () => {
// Arrange
store.order.insuranceCoverage.coverageStatus = coverageStatuses.VERIFIED;
store.order.currentDeductible = 0;
store.order.currentDeductible.replace = 0;
const wrapper = setupMocks({}, store, mixin);
// Act

View file

@ -153,7 +153,7 @@ export default {
isPayInAdvanceDisabled() {
const piaExperience = this.getSettingValue(experimentSettings.ISS_DISPLAY_PAY_IN_ADVANCE);
const isEnabled = piaExperience === 'true';
const isDeductibleTotalEqualToZero = useMainStore().order.currentDeductible === 0;
const isDeductibleTotalEqualToZero = useMainStore().currentDeductible === 0;
return !isEnabled || useMainStore().isUnverified || (useMainStore().isDeductible && isDeductibleTotalEqualToZero);
},

View file

@ -4,11 +4,14 @@
:key="alert.cmsWidgetName"
:ref="alert.cmsWidgetName"
class="mt-2 mb-3"
:manualHeadline="alert.manualHeadline"
:cmsWidgetName="alert.cmsWidgetName"
alertClass="alert-warning" />
</template>
<script>
import alert from '@/ux-components/alert/alert.vue';
import widgetFields from '@/constants/cms-widget-fields';
import { toTitleCase } from '@/helpers/text-helper.js';
import getAlertReasons from '@/layouts/schedule-page/helpers/schedule-helper';
export default {
@ -20,6 +23,10 @@ export default {
cmsWidgetPrefix: {
type: String,
default: ''
},
city: {
type: String,
default: ''
}
},
data() {
@ -29,9 +36,14 @@ export default {
},
computed: {
prefixedAlertReasons() {
if (!this.alertReasons || this.alertReasons.length === 0) {
return [];
}
return this.alertReasons.reduce((newObj, alertReason) => {
newObj.push({
cmsWidgetName: `${this.cmsWidgetPrefix}${alertReason}`,
manualHeadline: this.getCmsCopyForAlertReason(alertReason),
alertReason
});
return newObj;
@ -49,8 +61,11 @@ export default {
initializeComponent(initialData) {
this.alertReasons = initialData;
},
cmsHeadlineTextFound(widgetName) {
return this.getCmsContent(widgetName, 'HeadlineText') !== '';
getCmsCopyForAlertReason(alertReason) {
const widgetName = `${this.cmsWidgetPrefix}${alertReason}`;
let content = this.getCmsContent(widgetName, widgetFields.ALERT_WIDGET.HEADLINE_TEXT);
content = content.replace('{custom:city}', toTitleCase(this.city));
return content;
}
}
};

View file

@ -127,20 +127,19 @@ afterEach(() => {
});
describe('schedule-page.vue', () => {
beforeAll(() => {
Object.defineProperty(window, 'matchMedia', { value: jest.fn().mockImplementation((query) => ({
matches: false,
media: query,
onchange: null,
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn()
})),
writable: true });
});
describe('Initial Load', () => {
test('Should pass arePagePrerequisitesValid with a mobile order and no providerNumber', () => {
// Arrange
const { wrapper } = getShallowMountedComponent();
wrapper.vm.mainStore.order.serviceLocation.appointmentType = 'Mobile';
wrapper.vm.mainStore.order.serviceLocation.provider = null;
// Act
const arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid();
// Assert
expect(arePagePrerequisitesValid).toBe(true);
});
test('Should pass arePagePrerequisitesValid with an inshop order and providerNumber', () => {
test('Should pass arePagePrerequisitesValid with a serviceLocation zipcode [in beforeEach]', () => {
// Arrange
const { wrapper } = getShallowMountedComponent();
@ -150,10 +149,10 @@ describe('schedule-page.vue', () => {
// Assert
expect(arePagePrerequisitesValid).toBe(true);
});
test('Should fail arePagePrerequisitesValid with an inshop order and no providerNumber', () => {
test('Should fail arePagePrerequisitesValid with no serviceLocation zipcode', () => {
// Arrange
const { wrapper } = getShallowMountedComponent();
wrapper.vm.mainStore.order.serviceLocation.provider.providerNumber = null;
wrapper.vm.mainStore.order.serviceLocation.zipCode = null;
// Act
const arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid();
@ -161,28 +160,6 @@ describe('schedule-page.vue', () => {
// Assert
expect(arePagePrerequisitesValid).toBeFalsy();
});
test('Should fail arePagePrerequisitesValid if supportingItems is null', async () => {
// Arrange
const { wrapper } = getShallowMountedComponent();
wrapper.vm.mainStore.order.lineItems.supportingItems = null;
// Act
const arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid();
// Assert
expect(arePagePrerequisitesValid).toBe(false);
});
test('Should fail arePagePrerequisitesValid with an replace with no glass parts', async () => {
// Arrange
const { wrapper } = getShallowMountedComponent();
wrapper.vm.mainStore.order.lineItems.glassParts = [];
// Act
const arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid();
// Assert
expect(arePagePrerequisitesValid).toBe(false);
});
test('should return newShopTimeSlots when getAvailableDatesMethod is called', async () => {
// Arrange
const { wrapper } = getShallowMountedComponent();
@ -290,7 +267,7 @@ describe('schedule-page.vue', () => {
expect(testValue).toStrictEqual('01234');
});
});
test('forwardButtonAction should call route method navigateWithoutSaving', async () => {
test.skip('forwardButtonAction should call route method navigateWithoutSaving', async () => {
// Arrange
const { wrapper } = getShallowMountedComponent();
wrapper.vm.$router.navigate = jest.fn(() => ({}));

View file

@ -15,31 +15,47 @@
cmsWidgetName="ScheduleSubHeaderWidget"
secondaryTextClasses="text-center small sub-text"
class="mt-4" />
<div class="main-content-container">
<div class="service-location-content-container">
<serviceLocation
ref="serviceLocation"
@appointmentTypeChanged="appointmentTypeChangedFromServiceLocation"
@cityUpdated="cityUpdatedFromServiceLocation"
@mobileZipUpdated="mobileZipUpdatedFromServiceLocation"
@providerChanged="providerChangedFromServiceLocation" />
</div>
</div>
<div
v-show="hasNeededServiceLocationData"
class="date-picker-container iss-heritage-content-container-width">
<div class="date-picker-content-container">
<locationAlerts
ref="locationAlerts"
cmsWidgetPrefix="LocationAlert-" />
cmsWidgetPrefix="LocationAlert-"
:city="selectedServiceLocationCity" />
<datePicker
ref="datePicker"
v-model="selectedTimeSlotInfo"
customComponentId="dateQuestion"
selectableDatesSetting="custom"
class="text-link-small"
:activeAppointmentType="selectedAppointmentType"
:isMobileView="isMobileView"
:showTimeSlotError="showDatePickerError"
:customSelectableDatesCallback="
getAvailableDatesMethod
"
:customSelectableDatesCallback="getAvailableDatesMethod"
@dateSelected="dateSelectedFromPicker"
@timeSlotSelected="timeSlotSelectedFromPicker" />
<siteFooter
ref="navbar"
class="mt-5"
cmsWidgetName="SiteFooterWidget"
:isForwardButtonNavigationDisabled="!isFormValid"
@backClicked="navigateBack"
@forwardClicked="forwardButtonAction" />
</div>
</div>
<div class="site-footer-container iss-heritage-content-container-width">
<siteFooter
ref="navbar"
class="mt-5"
cmsWidgetName="SiteFooterWidget"
:isForwardButtonHidden="!hasNeededServiceLocationData"
:isForwardButtonNavigationDisabled="!isFormValid"
@backClicked="navigateBack(this, navigateBackScenario)"
@forwardClicked="forwardButtonAction" />
</div>
</div>
</div>
</Form>
@ -50,6 +66,7 @@ import siteHeader from '@/iss-components/site-header/site-header.vue';
import siteSubHeader from '@/iss-components/site-sub-header/site-sub-header.vue';
import locationAlerts from '@/layouts/schedule-page/location-alerts/location-alerts.vue';
import datePicker from '@/digital-components/date-picker/date-picker.vue';
import serviceLocation from '@/layouts/schedule-page/service-location/service-location.vue';
import siteFooter from '@/iss-components/site-footer/site-footer.vue';
// Supporting files
@ -65,9 +82,16 @@ import {
} from '@/helpers/cms-content-helper';
import {
calcDaysBetweenDates,
convertDateToDateString,
sumDateString
} from '@/helpers/date-helper';
import settleAllPromises from '@/helpers/layout-helper';
import showIssLoadingModal from '@/helpers/loading-modal-helper.js';
import {
getPricedMobileFeePart,
getServiceabilityDetails,
getZipCodeData
} from '@/helpers/service-location-helper';
import { Form } from 'vee-validate';
import BaseFormMixin from '@/mixins/base-form-mixin.js';
import { useMainStore } from '@/store';
@ -79,7 +103,8 @@ const getAvailableDates = async (
startDateString,
endDateString,
appointmentType,
providerNumber
providerNumber,
mobileZipCodeOverride = null
) => {
const apiEndDateLimit = sumDateString(
startDateString,
@ -113,7 +138,8 @@ const getAvailableDates = async (
storeAction: GET_MOBILE_TIME_SLOTS,
payload: {
startDate: apiStartDate,
endDate: apiEndDate
endDate: apiEndDate,
zipCodeOverride: mobileZipCodeOverride
}
};
} else {
@ -153,14 +179,19 @@ const getAvailableDates = async (
} else {
timeSlotsResponse = await useMainStore().getMobileTimeSlots(
storeAction.payload.startDate,
storeAction.payload.endDate
);
storeAction.payload.endDate,
storeAction.payload.zipCodeOverride
).catch((error) => {
console.log('Error fetching mobile time slots:', error);
});
}
timeSlotsResponsesData.estimatedServiceMinutesMinimum =
timeSlotsResponse.data.estimatedServiceMinutesMinimum;
timeSlotsResponsesData.estimatedServiceMinutesMaximum =
timeSlotsResponse.data.estimatedServiceMinutesMaximum;
if (!timeSlotsResponse || !timeSlotsResponse.data) {
return;
}
timeSlotsResponsesData.estimatedServiceMinutesMinimum = timeSlotsResponse.data.estimatedServiceMinutesMinimum;
timeSlotsResponsesData.estimatedServiceMinutesMaximum = timeSlotsResponse.data.estimatedServiceMinutesMaximum;
timeSlotsResponsesData.days = [
...timeSlotsResponsesData.days,
...timeSlotsResponse.data.days
@ -182,6 +213,7 @@ export default {
siteSubHeader,
locationAlerts,
datePicker,
serviceLocation,
siteFooter,
// eslint-disable-next-line vue/no-reserved-component-names
Form
@ -189,23 +221,18 @@ export default {
mixins: [BaseFormMixin],
async beforeRouteEnter(to, from, next) {
// Call APIs
let preSelectedDate = await useMainStore().order.schedule.date;
if (!preSelectedDate || preSelectedDate.startTime === null) {
preSelectedDate = null;
}
const cmsContentPromise = fetchCmsContentForPage(to.query.issPage);
const datePickerInitialDataPromise =
await datePicker.methods.loadInitialData({
selectableDatesSetting: 'custom',
initialViewRowsToShow: 5,
customSelectableDatesCallback: getAvailableDates,
preSelectedDate
});
const storeServiceLocation = useMainStore().order.serviceLocation;
const storeSelectedAppointmentType = storeServiceLocation?.appointmentType;
const storeSelectedProvider = storeServiceLocation?.provider;
const serviceZipCode = storeServiceLocation?.zipCode || useMainStore().order.customer.address.zipCode;
const zipCodeData = getZipCodeData(serviceZipCode);
const mobileFeePartPromise = getPricedMobileFeePart(serviceZipCode);
const serviceabilityDetailsPromise = getServiceabilityDetails(serviceZipCode);
const getGlassFeesPromise = useMainStore().getGlassFees();
const providersPromise = useMainStore().getProviders(serviceZipCode);
const premiumFeePromise = useMainStore().getMobilePremiumFee();
const premiumFeeWithPricePromise = premiumFeePromise.then((result) => {
if (result.data) {
return useMainStore().getCombinedQuote(result.data);
@ -213,11 +240,6 @@ export default {
return result.data;
});
const alertReasonsPromise = locationAlerts.methods.loadInitialData(
useMainStore().order.serviceLocation.zipCodeCtu,
useMainStore().order.serviceLocation.provider?.address?.zipCodeCtu
);
// Settle promises and get results
const promiseResultMap = [
{
@ -225,12 +247,24 @@ export default {
promise: cmsContentPromise
},
{
resultKey: 'alertReasons',
promise: alertReasonsPromise
resultKey: 'mobileFeePart',
promise: mobileFeePartPromise
},
{
resultKey: 'datePickerInitialData',
promise: datePickerInitialDataPromise
resultKey: 'glassFees',
promise: getGlassFeesPromise
},
{
resultKey: 'serviceabilityDetails',
promise: serviceabilityDetailsPromise
},
{
resultKey: 'zipCodeData',
promise: zipCodeData
},
{
resultKey: 'providers',
promise: providersPromise
},
{
resultKey: 'premiumFeeWithPrice',
@ -240,14 +274,32 @@ export default {
// use resultMap to populate layout content.
const resultMap = await settleAllPromises(promiseResultMap);
next((vm) => {
const serviceLocationData = {
glassFees: resultMap.glassFees,
mobileFeePart: resultMap.mobileFeePart,
providers: resultMap.providers,
serviceabilityDetails: resultMap.serviceabilityDetails,
zipCodeData: resultMap.zipCodeData
};
useMainStore().updateIsSafeliteProvider(true);
next(async (vm) => {
const providerToUse = resultMap.providers?.shopProviders
.find((provider) => provider.providerNumber === storeSelectedProvider?.providerNumber) || resultMap.providers?.shopProviders[0];
const isMobileAppointment = storeSelectedAppointmentType === AppointmentTypeStrings.MOBILE
|| storeSelectedAppointmentType === AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP;
const initialServiceLocationObj = {
mobileProviderNumber: isMobileAppointment
? storeSelectedProvider?.providerNumber
: null,
provider: isMobileAppointment ? null : providerToUse,
zipCode: useMainStore().order.serviceLocation.zipCode || useMainStore().order.customer.address.zipCode,
zipCodeCtu: resultMap.zipCodeData?.zipCodeCtu
};
vm.setCmsContent(resultMap.cmsContent);
vm.$refs.datePicker.initializeComponent(resultMap.datePickerInitialData);
vm.$refs.locationAlerts.initializeComponent(resultMap.alertReasons);
vm.setData(
resultMap.datePickerInitialData.initialShopTimeSlotsResponse,
resultMap.premiumFeeWithPrice
);
vm.$refs.serviceLocation.initializeComponent(serviceLocationData);
await vm.setData(resultMap.premiumFeeWithPrice, initialServiceLocationObj);
showIssLoadingModal(false);
});
},
setup() {
@ -256,64 +308,133 @@ export default {
},
data() {
return {
selectedDate: this.getSelectedDate(),
selectedTimeSlotInfo: this.getSelectedTimeSlotInfo(),
inShopDatesData: [],
isMobileView: false,
mobileDatesData: [],
mobileFeePart: null,
mobilePremiumAppointmentFee: null,
mobileProviderNumber: null,
mobileZipCodeOverride: null,
selectableDatesData: [],
showDatePickerError: false,
mobilePremiumAppointmentFee: null
selectedAppointmentType: this.getAppointmentType(),
selectedDate: this.getSelectedDate(),
selectedProvider: this.getSelectedProvider(),
selectedServiceLocation: this.getServiceLocation(),
selectedServiceLocationCity: this.getServiceLocationCity(),
selectedTimeSlotInfo: this.getSelectedTimeSlotInfo(),
showDatePickerError: false
};
},
computed: {
ChangeShopLinkText() {
return this.getCmsContent('ChangeShopLink', 'Text');
},
ChangeShopLink() {
return this.splitCopyOnCMSPlaceHolder(this.ChangeShopLinkText);
},
appointmentType() {
return useMainStore().order.serviceLocation.appointmentType;
hasNeededServiceLocationData() {
const serviceLocationToUse = this.selectedServiceLocation;
if (!serviceLocationToUse || !this.selectedAppointmentType) {
return false;
}
const serviceLocationPreReqs = serviceLocationToUse.zipCode !== null
&& serviceLocationToUse.zipCodeCtu !== null
&& serviceLocationToUse.appointmentType !== null
&& (((this.selectedAppointmentType === AppointmentTypeStrings.MOBILE
|| this.selectedAppointmentType === AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP)
&& serviceLocationToUse.mobileProviderNumber)
|| (this.selectedAppointmentType === AppointmentTypeStrings.IN_SHOP
&& (this.selectedProvider?.providerNumber || serviceLocationToUse.provider?.providerNumber)));
const damageInfo = useMainStore().order.damage.isRepair
|| (useMainStore().order.lineItems?.glassParts != null && useMainStore().order.lineItems.glassParts.length > 0);
const supportingItems = this.supportingItems !== null;
return (serviceLocationPreReqs && supportingItems && damageInfo);
},
isFormValid() {
const hasTimeSlotSelected = this.selectedTimeSlotInfo?.timeSlot?.routeCode != null;
return hasTimeSlotSelected;
},
navigateBackScenario() {
const { isNoComp, isITAC } = useMainStore();
return isNoComp || isITAC
? this.navigationScenarios.CLICKED_BACK_CANNOT_REACH_TPA_FLOW
: this.navigationScenarios.CLICKED_BACK_CAN_REACH_TPA_FLOW;
},
supportingItems() {
return useMainStore().lineItems.supportingItems;
}
},
mounted() {
showIssLoadingModal(true);
this.mql = window.matchMedia('(min-width: 1200px)');
this.isMobileView = !this.mql.matches;
this.mql.addEventListener('change', this.handleMqlChange);
},
unmounted() {
if (this.mql) {
this.mql.removeEventListener('change', this.handleMqlChange);
}
},
methods: {
splitCopyOnCMSPlaceHolder,
arePagePrerequisitesValid() {
const { serviceLocation } = useMainStore().order;
const serviceLocationPreReqs =
serviceLocation.zipCode
&& serviceLocation.zipCodeCtu
&& serviceLocation.appointmentType
&& (serviceLocation.appointmentType
=== AppointmentTypeStrings.MOBILE
|| serviceLocation.appointmentType
=== AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP
|| serviceLocation.provider.providerNumber);
const supportingItems = this.supportingItems !== null;
const damageInfo =
useMainStore().order.damage.isRepair
|| (useMainStore().order.lineItems?.glassParts != null
&& useMainStore().order.lineItems.glassParts.length > 0);
return serviceLocationPreReqs && supportingItems && damageInfo;
return useMainStore().order.serviceLocation.zipCode !== null;
},
setData(initialShopTimeSlotsResponse, premiumFeeWithPriceResponse) {
this.selectableDatesData = initialShopTimeSlotsResponse;
async setData(premiumFeeWithPriceResponse, initialServiceLocationObj) {
this.selectedServiceLocation = initialServiceLocationObj;
this.mobilePremiumAppointmentFee = premiumFeeWithPriceResponse
? premiumFeeWithPriceResponse[0]
: null;
if (initialServiceLocationObj?.zipCodeCtu) {
this.$refs.locationAlerts.loadInitialData(
initialServiceLocationObj.zipCodeCtu,
initialServiceLocationObj.provider?.ctu
).then((alertReasons) => {
this.$refs.locationAlerts.initializeComponent(alertReasons.data);
});
}
if (this.selectedAppointmentType !== AppointmentTypeStrings.MOBILE
&& this.selectedAppointmentType !== AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP) {
await this.getDatePickerInitialData(initialServiceLocationObj.provider?.providerNumber).then((initialData) => {
this.selectableDatesData = initialData.initialShopTimeSlotsResponse;
this.$refs.datePicker.setCalendarData(initialData);
});
} else {
this.mobileProviderNumber = initialServiceLocationObj.mobileProviderNumber;
await this.getDatePickerInitialData().then((initialData) => {
this.selectableDatesData = initialData.initialShopTimeSlotsResponse;
this.$refs.datePicker.setCalendarData(initialData);
});
}
},
appointmentTypeChangedFromServiceLocation(newAppointmentType) {
this.selectedAppointmentType = newAppointmentType.appointmentType;
if (newAppointmentType.appointmentType === AppointmentTypeStrings.IN_SHOP) {
if (this.inShopDatesData?.initialShopTimeSlotsResponse?.days?.length > 0) {
this.selectableDatesData = this.inShopDatesData.initialShopTimeSlotsResponse;
this.$refs.datePicker.initializeComponent(this.inShopDatesData);
}
} else if (this.mobileDatesData?.initialShopTimeSlotsResponse?.days?.length > 0) {
this.selectableDatesData = this.mobileDatesData.initialShopTimeSlotsResponse;
this.$refs.datePicker.initializeComponent(this.mobileDatesData);
}
},
cityUpdatedFromServiceLocation(newCity) {
this.selectedServiceLocationCity = newCity;
},
dateSelectedFromPicker(date) {
this.selectedDate = date;
this.showDatePickerError = false;
},
getAppointmentType() {
return this.mainStore.order?.serviceLocation?.appointmentType;
},
async getAvailableDatesMethod(startDate, endDate) {
const newShopTimeSlots = await getAvailableDates(
startDate,
endDate,
this.appointmentType,
this.mainStore.order.serviceLocation.provider.providerNumber
this.selectedAppointmentType,
this.selectedProvider.providerNumber
);
// ADD API CALL RESULTS TO EXISTING DATE DATA
this.selectableDatesData.days =
@ -321,12 +442,131 @@ export default {
return newShopTimeSlots;
},
getAvailableDates,
async getDatePickerInitialData(defaultProviderNumber = null) {
this.inShopDatesData = [];
this.mobileDatesData = [];
let todayDateString;
const todayDateObject = new Date();
const calendarViewDirection = 'future';
const initialDays = 15;
if (this.todayString) {
todayDateString = this.todayString;
} else {
todayDateString = convertDateToDateString(todayDateObject);
}
const initialViewStartDate = todayDateString;
const initialEndDate = new Date();
initialEndDate.setDate(initialEndDate.getDate() + (initialDays - 1));
let initialViewEndDate = convertDateToDateString(initialEndDate);
const preSelectedDate = this.mainStore.order.schedule.date;
const endDateObject = this.getEndDateForExistingDate(
todayDateObject,
preSelectedDate,
initialDays
);
if (endDateObject.endDateString !== initialViewEndDate) {
initialViewEndDate = endDateObject.endDateString;
}
let mobileProviderNumberToUse = defaultProviderNumber;
if (this.selectedAppointmentType === AppointmentTypeStrings.MOBILE
|| this.selectedAppointmentType === AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP) {
mobileProviderNumberToUse = this.selectedServiceLocation.mobileProviderNumber;
}
const initialData = await getAvailableDates(
initialViewStartDate,
initialViewEndDate,
this.selectedAppointmentType || AppointmentTypeStrings.IN_SHOP,
this.selectedProvider?.providerNumber || mobileProviderNumberToUse,
this.selectedServiceLocation.zipCode
).then((response) => ({
todayDate: todayDateString,
initialViewStartDate,
initialViewEndDate,
calendarViewDirection,
initialShopTimeSlotsResponse: response,
preSelectedDate,
initialDaysLoaded: endDateObject.newDaysLoaded || initialDays,
daysFromStart: endDateObject.daysFromStart || null
}));
if (this.selectedAppointmentType === AppointmentTypeStrings.MOBILE
|| this.selectedAppointmentType === AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP) {
this.mobileDatesData = initialData;
if (this.selectedProvider?.providerNumber) {
const inShopData = await getAvailableDates(
initialViewStartDate,
initialViewEndDate,
AppointmentTypeStrings.IN_SHOP,
this.selectedProvider?.providerNumber || defaultProviderNumber,
this.selectedServiceLocation.zipCode
).then((response) => ({
todayDate: todayDateString,
initialViewStartDate,
initialViewEndDate,
calendarViewDirection,
initialShopTimeSlotsResponse: response,
preSelectedDate,
initialDaysLoaded: endDateObject.newDaysLoaded || initialDays,
daysFromStart: endDateObject.daysFromStart || null
}));
this.inShopDatesData = inShopData;
}
} else {
this.inShopDatesData = initialData;
}
return initialData;
},
getEndDateForExistingDate(startDate, selectedDateString, daysInView) {
const endDate = new Date(startDate);
endDate.setDate(endDate.getDate() + (daysInView - 1));
if (selectedDateString) {
const selectedDate = new Date(`${selectedDateString}T00:00:00`);
const daysDifferenceComparedToStartDate = Math.ceil((selectedDate - startDate) / (1000 * 60 * 60 * 24));
if (selectedDate > endDate) {
const daysDifferenceComparedToStartDateAsPages = Math.ceil(daysDifferenceComparedToStartDate / daysInView);
const newEndDate = new Date(startDate);
newEndDate.setDate(newEndDate.getDate() + (daysDifferenceComparedToStartDateAsPages * daysInView) - 1);
return {
endDateString: convertDateToDateString(newEndDate),
newDaysLoaded: (daysDifferenceComparedToStartDateAsPages * daysInView),
daysFromStart: daysDifferenceComparedToStartDate
};
}
return {
endDateString: convertDateToDateString(endDate),
newDaysLoaded: null,
daysFromStart: daysDifferenceComparedToStartDate
};
}
return {
endDateString: convertDateToDateString(endDate),
newDaysLoaded: null,
daysFromStart: null
};
},
getServiceZipCtuCodeFromStore() {
return this.mainStore.order.serviceLocation.zipCodeCtu;
},
getSelectedDate() {
return this.mainStore.order.schedule.date;
},
getSelectedProvider() {
return this.mainStore.order.serviceLocation.provider;
},
getSelectedTimeSlotInfo() {
const isPremiumAppointment =
!!(
@ -341,20 +581,52 @@ export default {
return selectedTimeSlotInfo;
},
dateSelectedFromPicker(date) {
this.selectedDate = date;
this.showDatePickerError = false;
getServiceLocation() {
return this.mainStore.order.serviceLocation;
},
getServiceLocationCity() {
return this.mainStore.order.serviceLocation?.city;
},
handleMqlChange(e) {
this.isMobileView = !e.matches;
},
mobileZipUpdatedFromServiceLocation(mobileZipServiceLocation) {
if (mobileZipServiceLocation) {
this.mobileProviderNumber = mobileZipServiceLocation.mobileProviderNumber;
this.selectedServiceLocation = mobileZipServiceLocation;
if (mobileZipServiceLocation.refreshDatePicker) {
this.refreshDatePicker();
}
}
},
providerChangedFromServiceLocation(newProvider) {
this.selectedProvider = newProvider?.provider;
if (newProvider?.refreshDatePicker) {
showIssLoadingModal(true);
this.refreshDatePicker();
}
},
async refreshDatePicker() {
await this.getDatePickerInitialData().then((data) => {
this.selectableDatesData = data.initialShopTimeSlotsResponse;
this.$refs.datePicker.initializeComponent(data);
showIssLoadingModal(false);
});
},
timeSlotSelectedFromPicker(timeSlot) {
this.selectedTimeSlotInfo = timeSlot;
this.showDatePickerError = false;
},
forwardButtonAction() {
async forwardButtonAction() {
if (!this.isFormValid) {
this.showDatePickerError = true;
return;
}
// Save service location then schedule and navigate forward
await this.$refs.serviceLocation.forwardButtonAction();
this.mainStore.saveSchedule(this.selectedTimeSlotInfo.timeSlot);
this.$router.navigate(
this.navigationScenarios.CLICKED_FORWARD,
@ -369,11 +641,12 @@ export default {
$page-side-padding: 1.5rem;
.iss-heritage-container-width {
padding-right: 0.9375rem;
padding-left: 0.9375rem;
.schedule-page-container {
position: relative;
min-height: 1px;
padding-left: .9375rem;
padding-right: .9375rem;
}
}

View file

@ -19,12 +19,10 @@
</template>
<script>
import { useMainStore } from '@/store';
import buttonQuestion from '@/digital-components/button-question/button-question.vue';
// Constants
import { AppointmentTypeStrings } from '@/constants/schedule-constants.js';
import { experimentSettings } from '@/constants/experiments';
export default {
name: 'appointment-type-question',
@ -63,12 +61,17 @@ export default {
this.$emit('update:modelValue', newValue);
}
}
},
}
};
</script>
<style lang="scss" scoped>
.appointment-type-question {
margin-bottom: 1.25rem;
margin-top: .625rem;
:deep(.list-button-content) {
padding-top: .625rem;
padding-bottom: .625rem;
}
}
</style>

View file

@ -79,8 +79,6 @@ import modal from '@/digital-components/modal/modal.vue';
import alert from '@/ux-components/alert/alert.vue';
import { useField } from 'vee-validate';
import addressQuestions from '@/iss-components/address-questions/address-questions.vue';
// eslint-disable-next-line max-len
import vehicleProtectedQuestion from '@/layouts/service-location/mobile-location-modal-questions/vehicle-protected-question/vehicle-protected-question.vue';
// Constants
import { experimentSettings } from '@/constants/experiments';
@ -90,10 +88,11 @@ import { useMainStore } from '@/store';
import {
getMobileZipCodeData,
getPricedMobileFeePart,
getServiceabilityDetails,
getZipCodeData
getServiceabilityDetails
} from '@/helpers/service-location-helper';
import { deepClone } from '@/helpers/object-helper.js';
// eslint-disable-next-line max-len
import vehicleProtectedQuestion from '@/layouts/schedule-page/service-location/mobile-location-modal-question/vehicle-protected-question/vehicle-protected-question.vue';
export default {
name: 'mobile-location-modal-questions',

View file

@ -1,13 +1,12 @@
/* eslint-env jest */
import baseMixin from '@/mixins/base-mixin';
import { mount, flushPromises } from '@vue/test-utils';
import { mount } from '@vue/test-utils';
import { createTestingPinia } from '@pinia/testing';
import { AppointmentTypeStrings } from '@/constants/schedule-constants';
import navigationScenarios from '@/router/router-constants/navigation-scenarios';
import routerParams from '@/router/router-constants/router-params';
import { useMainStore } from '@/store';
import serviceLocation from '@/layouts/service-location/service-location.vue';
import { getZipCodeData } from '@/helpers/service-location-helper';
import serviceLocation from '@/layouts/schedule-page/service-location/service-location.vue';
const mockGetServiceabilityDetails = () => {
const serviceabilityDetails = {
@ -50,43 +49,41 @@ const mockZipcodeData = (zip) => {
state: null,
zipCodeCtu: null
});
}
const mockProviders = () => {
return Promise.resolve([
{
"address": {
"city": "COLUMBUS",
"country": "US",
"state": "OH",
"streetAddress": "6826 Sawmill Rd",
"streetAddress2": "",
"zipCode": "43235",
"zipCodeCtu": "03357"
},
"distanceInMiles": 4.136335989015438,
"providerNumber": "003357",
"companyName": "SAFELITE AUTOGLASS - COLUMBUS, OH",
"phoneNumber": "6142336400",
"isSafeliteShop": true
};
const mockProviders = () => Promise.resolve([
{
address: {
city: 'COLUMBUS',
country: 'US',
state: 'OH',
streetAddress: '6826 Sawmill Rd',
streetAddress2: '',
zipCode: '43235',
zipCodeCtu: '03357'
},
{
"address": {
"city": "Lewis Center",
"country": "US",
"state": "OH",
"streetAddress": "1343 Cameron Ave",
"streetAddress2": "",
"zipCode": "43035",
"zipCodeCtu": "03357"
},
"distanceInMiles": 8.193072412262042,
"providerNumber": "003417",
"companyName": "SAFELITE AUTOGLASS - LEWIS CENTER, OH",
"phoneNumber": "6147815433",
"isSafeliteShop": true
}
])
}
distanceInMiles: 4.136335989015438,
providerNumber: '003357',
companyName: 'SAFELITE AUTOGLASS - COLUMBUS, OH',
phoneNumber: '6142336400',
isSafeliteShop: true
},
{
address: {
city: 'Lewis Center',
country: 'US',
state: 'OH',
streetAddress: '1343 Cameron Ave',
streetAddress2: '',
zipCode: '43035',
zipCodeCtu: '03357'
},
distanceInMiles: 8.193072412262042,
providerNumber: '003417',
companyName: 'SAFELITE AUTOGLASS - LEWIS CENTER, OH',
phoneNumber: '6147815433',
isSafeliteShop: true
}
]);
jest.mock(
'@/helpers/service-location-helper',
() => ({
@ -138,13 +135,13 @@ const mountOptions = {
}
}
};
function setupMocks({ appointmentType = AppointmentTypeStrings.IN_SHOP }) {
function setupMocks({ appointmentType = AppointmentTypeStrings.IN_SHOP }) {
mountOptions.global.plugins = [createTestingPinia({
initialState: {
main: {
order: {
serviceLocation: {
appointmentType: appointmentType
appointmentType
}
}
}
@ -204,7 +201,7 @@ describe('service-location.vue', () => {
expect(wrapper.vm.zipContainsMilitaryBase).toBe(false);
const newMobileServiceZipCode = '45433'
const newMobileServiceZipCode = '45433';
// Act
mobileServiceZipCodeQuestion.vm.$emit('update:modelValue', newMobileServiceZipCode);

View file

@ -0,0 +1,581 @@
<template>
<div class="service-location">
<alert
v-if="displayRecalibrationWarning"
ref="alertRecalNoMobile"
cmsWidgetName="AlertRecalNoMobileWidget"
alertClass="alert-warning"
@textLinkClicked="openModalAction" />
<alert
v-if="displayBigTruckNoShops"
ref="alertBigTruckNoShops"
cmsWidgetName="AlertBigTruckNoShopsWidget"
alertClass="alert-warning" />
<div class="appointment-type">
<div class="appointment-type-question-text d-flex">
<span
v-if="recalibrationRequired"
class="recal-text">{{ scheduleRecalText }}</span>
<span
v-if="!requiresInshopRecalibration">{{ appointmentQuestionText }}</span>
</div>
<alert
v-if="displayServiceableInshopOnly"
ref="alertInshopOnly"
cmsWidgetName="AlertInshopOnlyWidget"
:manualCopy="inShopOnlyCopy"
:isCollapsible="true"
alertClass="alert-warning" />
<alert
v-if="displayServiceableMobileOnly"
ref="alertMobileOnly"
cmsWidgetName="AlertMobileOnlyWidget"
:manualCopy="mobileOnlyCopy"
:isCollapsible="true"
alertClass="alert-warning" />
<appointmentTypeQuestion
v-if="!requiresInshopRecalibration && isServiceableMobile"
ref="appointmentTypeQuestion"
v-model="selectedAppointmentType"
groupName="appointmentTypeQuestion"
cmsWidgetName="AppointmentTypeQuestionWidget"
validationRules="option-required" />
</div>
<div
v-if="isInshop">
<alert
v-if="displayLowAvailabilityInshop"
ref="alertLowAvailabilityInshop"
cmsWidgetName="AlertLowAvailabilityInshopWidget"
:isCollapsible="true"
alertClass="alert-warning" />
<shopAddress
ref="shopAddress"
v-model="selectedProvider"
:serviceZipcode="zipCode"
:selectedAppointmentType="selectedAppointmentType"
modalWidgetName="ChangeLocationModalWidget"
cmsWidgetName="ShopAddressWidget"
@zipUpdated="handleInShopZipUpdated" />
<!-- INTEGRATION TODO: Put this right under the calendar -->
<alert
v-if="selectedProvider === null"
ref="alertNoAvailableShops"
cmsWidgetName="AlertNoAvailableShopsWidget"
:isCollapsible="false"
alertClass="alert-danger" />
</div>
<div
v-if="isMobile">
<div class="expandable-link-container">
<a
v-if="displayMilitaryZipAlert && !militaryBaseWarningExpanded"
href="#"
class="expandable-link"
@click.prevent="militaryBaseWarningExpanded = true">
{{ militaryBaseWarningLinkText }}
</a>
<div
v-if="displayMilitaryZipAlert && militaryBaseWarningExpanded"
class="expandable-link-text">
{{ militaryBaseWarningText }}
</div>
</div>
<serviceZipQuestion
ref="mobileServiceZipCodeQuestion"
v-model="mobileZipCode"
customInputId="mobileServiceZipCode"
class="mobile-service-zip-question"
:placeholderText="mobileZipPlaceholder"
:serviceZipFormatErrorMessage="errorMessages.MOBILE_SERVICE_ZIP_FORMAT"
:hasError="mobileZipError !== ''"
cmsWidgetName="MobileZipWidget" />
<div
v-if="mobileZipError"
ref="errorMessageDiv"
class="row my-1 form-test-error">
<span
class="d-inline-flex mt-0"
role="alert">{{ mobileZipError }}</span>
</div>
<textBlock
v-if="displayMobileFeeDisclaimer"
cmsWidgetName="MobileFeeDisclaimerWidget"
typeStyle="disclaimer" />
</div>
<contentGroupModal
:ref="RECAL_MODAL_REF_NAME"
cssModalHeadlineClass="text-center"
cmsWidgetName="RecalModal" />
</div>
</template>
<script>
// Import Supporting Files
import { AppointmentTypeStrings } from '@/constants/schedule-constants.js';
import errorMessages from '@/constants/error-messages';
import { useMainStore } from '@/store';
import showIssLoadingModal from '@/helpers/loading-modal-helper.js';
import {
getAvailabilityRating,
getServiceabilityDetails,
getZipCodeData,
getMobileZipCodeData
} from '@/helpers/service-location-helper';
import { toTitleCase } from '@/helpers/text-helper.js';
// Import Component
import alert from '@/ux-components/alert/alert.vue';
import textBlock from '@/digital-components/text-block/text-block.vue';
import appointmentTypeQuestion from '@/layouts/schedule-page/service-location/appointment-type-question/appointment-type-question.vue';
import baseFormMixin from '@/mixins/base-form-mixin';
import contentGroupModal from '@/iss-components/content-group-modal/content-group-modal.vue';
import shopAddress from '@/layouts/schedule-page/service-location/shop-address/shop-address.vue';
import serviceZipQuestion from '@/layouts/schedule-page/service-location/service-zip-question/service-zip-question.vue';
import widgetFields from '@/constants/cms-widget-fields';
const RECAL_MODAL_REF_NAME = 'RecalModal';
export default {
name: 'service-location',
components: {
alert,
textBlock,
appointmentTypeQuestion,
contentGroupModal,
shopAddress,
serviceZipQuestion
},
mixins: [baseFormMixin],
emits: ['appointment-type-changed', 'city-updated', 'mobile-zip-updated', 'provider-changed'],
setup() {
const mainStore = useMainStore();
return { mainStore };
},
data() {
return {
streetAddress: this.getServiceAddressFromStore(),
streetAddress2: this.getServiceAddress2FromStore(),
city: this.getServiceCityFromStore(),
state: this.getServiceStateFromStore(),
zipCode: this.getServiceZipCodeFromStore(),
isGlassServiceableInshop: null,
isRecalibrationServiceableInshop: null,
isGlassServiceableMobile: null,
isRecalibrationServiceableMobile: null,
selectedAppointmentType: this.getSelectedAppointmentType(),
selectedProvider: this.getSelectedProvider(),
mobileFeePart: null,
mobileProviderNumber: null,
zipContainsMilitaryBase: false,
zipCodeCtu: null,
mobileZipCode: '',
mobileZipError: '',
militaryBaseWarningExpanded: false,
availabilityRating: null,
RECAL_MODAL_REF_NAME,
errorMessages
};
},
computed: {
appointmentQuestionText() {
return this.getCmsContent('WhereWouldYouLikeServiceWidget', widgetFields.CONTENT_GROUP_WIDGET.HEADER_TEXT);
},
displayBigTruckNoShops() {
return this.isBigTruck && !this.isServiceableInshop && !this.isServiceableMobile;
},
displayLowAvailabilityInshop() {
return this.availabilityRating === 'low' && this.isInshop;
},
displayMilitaryZipAlert() {
return this.zipContainsMilitaryBase && this.isServiceableMobile;
},
displayMobileFeeDisclaimer() {
return this.mainStore.isNoComp || this.mainStore.isITAC;
},
displayRecalibrationWarning() {
return this.requiresInshopRecalibration;
},
displayServiceableInshopOnly() {
return !this.isServiceableMobile && this.isServiceableInshop && !this.displayRecalibrationWarning;
},
displayServiceableMobileOnly() {
return this.isServiceableMobile && this.recalibrationRequired && !this.isServiceableInshop;
},
inShopOnlyCopy() {
let content = this.getCmsContent('AlertInshopOnlyWidget', widgetFields.ALERT_WIDGET.BODY_TEXT);
content = content.replace('{custom:city}', toTitleCase(this.city));
return content;
},
isBigTruck() {
return this.mainStore.order.vehicle.isBigTruck;
},
isInshop() {
return (
this.selectedAppointmentType === 'Inshop'
|| this.selectedAppointmentType === 'Dropoff'
);
},
isMobile() {
return this.selectedAppointmentType === AppointmentTypeStrings.MOBILE;
},
isServiceableInshop() {
if (this.isRecalibrationServiceableInshop !== null) {
return (this.isGlassServiceableInshop && this.isRecalibrationServiceableInshop);
}
return this.isGlassServiceableInshop;
},
isServiceableMobile() {
if (this.isRecalibrationServiceableMobile !== null) {
return (
this.isGlassServiceableMobile
&& this.isRecalibrationServiceableMobile
);
}
return this.isGlassServiceableMobile;
},
militaryBaseWarningLinkText() {
return this.getCmsContent('AlertMilitaryBaseZipWidget', widgetFields.ALERT_WIDGET.HEADLINE_TEXT);
},
militaryBaseWarningText() {
return this.getCmsContent('AlertMilitaryBaseZipWidget', widgetFields.ALERT_WIDGET.BODY_TEXT);
},
mobileOnlyCopy() {
let content = this.getCmsContent('AlertMobileOnlyWidget', widgetFields.ALERT_WIDGET.BODY_TEXT);
content = content.replace('{custom:city}', toTitleCase(this.city));
return content;
},
mobileZipPlaceholder() {
return this.getCmsContent('MobileZipPlaceHolderWidget', widgetFields.TEXT_BLOCK_WIDGET.TEXT);
},
recalibrationRequired() {
return this.mainStore.hasRecalibrationPart;
},
requiresInshopRecalibration() {
// Specifically check for isRecalibrationServiceableMobile === false, not null or true.
return (
this.isServiceableInshop
&& this.isGlassServiceableMobile
&& this.isRecalibrationServiceableMobile === false
);
},
scheduleRecalText() {
if (this.requiresInshopRecalibration) {
return this.getCmsContent('WhereWouldYouLikeServiceWidget', widgetFields.CONTENT_GROUP_WIDGET.BODY_TEXT_2);
}
return this.getCmsContent('WhereWouldYouLikeServiceWidget', widgetFields.CONTENT_GROUP_WIDGET.BODY_TEXT);
}
},
watch: {
city(newCity, oldCity) {
if (newCity !== oldCity) {
this.$emit('city-updated', newCity);
}
},
async mobileZipCode(newZip, oldZip) {
if (newZip !== '' && newZip !== oldZip) {
showIssLoadingModal(true);
await this.updateMobileZip();
const updateMobileZipServiceLocationObj = {
mobileProviderNumber: this.mobileProviderNumber,
provider: null,
refreshDatePicker: true,
zipCode: this.zipCode,
zipCodeCtu: this.zipCodeCtu
};
this.$emit('mobile-zip-updated', updateMobileZipServiceLocationObj);
}
},
selectedAppointmentType(newValue, oldValue) {
if (this.selectedAppointmentType === AppointmentTypeStrings.IN_SHOP && this.selectedProvider && this.availabilityRating === null) {
this.refreshAvailabilityRating();
const appointmenTypeServiceLocationObj = {
appointmentType: newValue,
mobileProviderNumber: null,
provider: this.selectedProvider,
refreshDatePicker: oldValue !== newValue && oldValue !== null,
resetDatePicker: oldValue !== newValue || oldValue === null,
zipCode: this.zipCode,
zipCodeCtu: this.zipCodeCtu
};
this.$emit('appointment-type-changed', appointmenTypeServiceLocationObj);
} else {
const appointmenTypeServiceLocationObj = {
appointmentType: newValue,
mobileProviderNumber: this.mobileProviderNumber,
provider: null,
refreshDatePicker: false,
resetDatePicker: true,
zipCode: this.zipCode,
zipCodeCtu: this.zipCodeCtu
};
this.$emit('appointment-type-changed', appointmenTypeServiceLocationObj);
}
},
selectedProvider(newProvider, oldProvider) {
if (newProvider?.providerNumber !== oldProvider?.providerNumber) {
const appointmentIsInshop = this.selectedAppointmentType === AppointmentTypeStrings.IN_SHOP;
const returnedProvider = {
provider: newProvider,
refreshDatePicker: appointmentIsInshop && oldProvider?.providerNumber !== null
};
if (appointmentIsInshop && newProvider?.providerNumber != null) {
this.refreshAvailabilityRating();
}
this.$emit('provider-changed', returnedProvider);
}
}
},
methods: {
initializeComponent(initialData) {
this.setData(initialData);
},
async forwardButtonAction() {
let provider = this.selectedProvider;
this.mainStore.updateMobileFee(null);
if (this.isMobile) {
if (this.mainStore.isNoComp || this.mainStore.isITAC) {
this.mainStore.updateMobileFee(this.mobileFeePart);
}
provider = {
providerNumber: this.mobileProviderNumber,
address: {
streetAddress: null,
city: null,
state: null,
zipCode: null,
zipCodeCtu: null
}
};
}
this.mainStore.saveServiceLocation({
address: this.streetAddress,
address2: this.streetAddress2,
city: this.city,
state: this.state,
zipCode: this.zipCode,
zipCodeCtu: this.zipCodeCtu,
appointmentType: this.selectedAppointmentType,
provider
});
},
openModalAction(modalName) {
this.$refs[modalName].openModal();
},
resetDependentState() {},
getServiceAddressFromStore() {
return useMainStore().order.serviceLocation.address;
},
getServiceAddress2FromStore() {
return useMainStore().order.serviceLocation.address2;
},
getServiceCityFromStore() {
return useMainStore().order.serviceLocation.city;
},
getServiceStateFromStore() {
return (
useMainStore().order.serviceLocation.state
|| useMainStore().order.customer.address.state
);
},
getServiceZipCodeFromStore() {
return (
useMainStore().order.serviceLocation.zipCode
|| useMainStore().order.customer.address.zipCode
);
},
getSelectedAppointmentType() {
return useMainStore().order.serviceLocation.appointmentType;
},
getSelectedProvider() {
return useMainStore().order.serviceLocation.provider;
},
async handleInShopZipUpdated(newZip) {
this.zipCode = newZip;
const zipCodeData = await getZipCodeData(this.zipCode);
this.city = zipCodeData.city;
this.setCtuForMobile(zipCodeData.zipCodeCtu);
this.setContainsMilitaryBase(zipCodeData.containsMilitaryBase);
const serviceabilityDetailsPromise = getServiceabilityDetails(this.zipCode);
await serviceabilityDetailsPromise.then((result) => {
const details = result.data;
if (details) {
this.setServiceabilityDetails(details);
}
});
},
refreshAvailabilityRating() {
const startDate = new Date();
const endDate = new Date();
endDate.setDate(startDate.getDate() + 6);
const formattedStartDate = startDate.toISOString().split('T')[0];
const formattedEndDate = endDate.toISOString().split('T')[0];
getAvailabilityRating(
formattedStartDate,
formattedEndDate,
AppointmentTypeStrings.IN_SHOP,
this.selectedProvider ? this.selectedProvider.providerNumber : null
).then((rating) => {
this.availabilityRating = rating;
}).catch(() => {
this.availabilityRating = null;
});
},
setCtuForMobile(val) {
this.zipCodeCtu = val;
},
setData(initialData) {
if (initialData.zipCodeData) {
this.zipContainsMilitaryBase = initialData.zipCodeData.containsMilitaryBase;
this.zipCodeCtu = initialData.zipCodeData.zipCodeCtu;
this.city = initialData.zipCodeData.city;
}
if (initialData.serviceabilityDetails) {
this.setServiceabilityDetails(initialData.serviceabilityDetails);
}
if (initialData.mobileFeePart) {
this.mobileFeePart = initialData.mobileFeePart;
}
if (initialData.providers) {
this.setMobileProviderNumber(initialData.providers.mobileProviderNumber);
let foundMatch = false;
if (this.selectedProvider && this.selectedProvider.providerNumber) {
// eslint-disable-next-line max-len
const matchedProvider = initialData.providers.shopProviders.find((provider) => provider.providerNumber === this.selectedProvider.providerNumber);
if (matchedProvider) {
foundMatch = true;
this.selectedProvider = matchedProvider;
}
}
if (initialData.providers.shopProviders.length > 0 && !foundMatch) {
// eslint-disable-next-line prefer-destructuring
this.selectedProvider = initialData.providers.shopProviders[0];
} else if (initialData.providers.shopProviders.length === 0) {
this.selectedProvider = null;
}
}
if (initialData.glassFees) {
useMainStore().getCombinedQuote(initialData.glassFees)
.then((combinedQuote) => {
this.setGlassFeeItems(combinedQuote);
});
}
},
setContainsMilitaryBase(val) {
if (this.zipContainsMilitaryBase !== val) {
this.zipContainsMilitaryBase = val;
}
},
setGlassFeeItems(newGlassFeeItems) {
this.mainStore.updateGlassFees(newGlassFeeItems);
},
setMobileProviderNumber(providerNumber) {
this.mobileProviderNumber = providerNumber;
},
setServiceabilityDetails(serviceabilityDetails) {
this.isGlassServiceableInshop = serviceabilityDetails.isGlassServiceableInshop;
this.isRecalibrationServiceableInshop = serviceabilityDetails.isRecalibrationServiceableInshop;
this.isGlassServiceableMobile = serviceabilityDetails.isGlassServiceableMobile;
this.isRecalibrationServiceableMobile = serviceabilityDetails.isRecalibrationServiceableMobile;
if (!this.isServiceableMobile) {
this.selectedAppointmentType = AppointmentTypeStrings.IN_SHOP;
}
},
async updateMobileZip() {
this.mobileZipError = '';
const zipCodeData = await getMobileZipCodeData(this.mobileZipCode);
if (!zipCodeData.isValid) {
this.mobileZipError = errorMessages.INVALID_ZIP;
} else if (!zipCodeData.isServiceable) {
this.mobileZipError = errorMessages.NO_SERVICE_IN_AREA(toTitleCase(zipCodeData.city));
} else {
this.city = zipCodeData.city;
this.zipCode = this.mobileZipCode;
this.setCtuForMobile(zipCodeData.zipCodeCtu);
this.setContainsMilitaryBase(zipCodeData.containsMilitaryBase);
const serviceabilityDetailsPromise = getServiceabilityDetails(this.mobileZipCode);
await serviceabilityDetailsPromise.then((result) => {
const details = result.data;
if (details) {
this.setServiceabilityDetails(details);
}
});
const providersPromise = useMainStore().getProviders(this.mobileZipCode);
await providersPromise.then((result) => {
const providers = result.data;
if (providers) {
this.setMobileProviderNumber(providers.mobileProviderNumber);
if (providers.shopProviders.length > 0) {
// eslint-disable-next-line prefer-destructuring
this.selectedProvider = providers.shopProviders[0];
} else {
this.selectedProvider = null;
}
}
});
}
}
}
};
</script>
<style lang="scss" scoped>
$page-side-padding: 1.5rem;
.service-location {
> div {
margin-top: 1.25rem;
margin-bottom: 0;
}
.form-test-error {
font-weight: $font-weight-bold;
}
.appointment-type-question-text {
color: $black;
font-weight: 600;
font-size: 1rem;
line-height: 1.625rem;
flex-direction: column;
.recal-text {
font-weight: $font-weight-bold;
}
}
.mobile-service-zip-question {
:deep(.input-wrapper.has-text-button) {
button {
background-color: #1574a1;
}
}
:deep(.form-test-error span) {
font-weight: $font-weight-bold;
}
}
.expandable-link-container {
margin-bottom: 1rem;
}
.expandable-link {
font-style: italic;
font-weight: $font-weight-bold;
text-decoration: none;
color: $heritage-blue-primary;
line-height: 1.375rem;
&:hover {
text-decoration: underline;
}
}
}
</style>

View file

@ -1,6 +1,6 @@
import { mount } from '@vue/test-utils';
import crypto from 'crypto';
import serviceZipModalQuestion from '@/layouts/service-location/service-zip-modal-question/service-zip-modal-question.vue';
import serviceZipModalQuestion from '@/layouts/schedule-page/service-location/service-zip-modal-question/service-zip-modal-question.vue';
global.crypto = crypto;

View file

@ -35,7 +35,7 @@
<script>
import textLink from '@/ux-components/text-link/text-link.vue';
import serviceZipQuestion from '@/layouts/service-location/service-zip-question/service-zip-question.vue';
import serviceZipQuestion from '@/layouts/schedule-page/service-location/service-zip-question/service-zip-question.vue';
import modal from '@/digital-components/modal/modal.vue';
import alert from '@/ux-components/alert/alert.vue';
@ -177,6 +177,7 @@ export default {
</script>
<style lang="scss" scoped>
@import "@/styles/ux-variables-svg-strings.scss";
a#serviceZipLinkPromptId {
line-height: 26px;
}
@ -186,7 +187,7 @@ a#serviceZipLinkPromptId {
display: inline-block;
width: 13px;
height: 16px;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 13 16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M6.49635 1.00142e-07C5.64734 -0.000153295 4.80722 0.175918 4.0274 0.517444C3.24757 0.858969 2.54443 1.35877 1.96099 1.98626C0.765713 3.27588 0.0999756 4.98141 0.0999756 6.75394C0.0999756 8.52646 0.765713 10.232 1.96099 11.5216L5.98324 15.777C6.04954 15.8475 6.12918 15.9036 6.21736 15.9419C6.30555 15.9802 6.40045 16 6.49635 16C6.59225 16 6.68716 15.9802 6.77534 15.9419C6.86353 15.9036 6.94317 15.8475 7.00946 15.777L11.0317 11.52C12.2391 10.2383 12.909 8.52914 12.8999 6.75394C12.9094 4.97818 12.2394 3.26832 11.0317 1.98626C10.4481 1.35899 9.74493 0.859347 8.96514 0.517839C8.18535 0.176331 7.34532 0.000130509 6.49635 1.00142e-07V1.00142e-07ZM6.49635 9.13131C6.02507 9.13131 5.56437 8.98913 5.17251 8.72275C4.78065 8.45637 4.47524 8.07776 4.29488 7.63479C4.11453 7.19181 4.06734 6.70438 4.15928 6.23412C4.25123 5.76387 4.47817 5.33191 4.81142 4.99287C5.14467 4.65384 5.56925 4.42295 6.03148 4.32941C6.49371 4.23587 6.97282 4.28388 7.40823 4.46736C7.84364 4.65085 8.21579 4.96157 8.47762 5.36023C8.73945 5.7589 8.87921 6.2276 8.87921 6.70707C8.87921 7.34974 8.62837 7.96611 8.18185 8.4207C7.73532 8.87528 7.12964 9.13088 6.49794 9.13131H6.49635Z' fill='%231574A1'/%3E%3C/svg%3E%0A");
background-image: url($svg-update-zip-text-link);
background-size: contain;
vertical-align: middle;
margin-right: 0.5em;

View file

@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
import serviceZipQuestion from '@/layouts/service-location/service-zip-question/service-zip-question.vue';
import serviceZipQuestion from '@/layouts/schedule-page/service-location/service-zip-question/service-zip-question.vue';
describe('service-zip-question.vue', () => {
it('Should get the modelValue', async () => {

View file

@ -8,8 +8,8 @@
mask="#####"
isRequired
:hasError="hasError"
@clickEvent="updateModelValue"
:validationRules="`zip-required|${cmsWidgetName}-zip-format`" />
:validationRules="`zip-required|${cmsWidgetName}-zip-format`"
@clickEvent="updateModelValue" />
</template>
<script>
@ -41,24 +41,26 @@ export default {
default: false
}
},
emits: ['update:modelValue'],
data() {
return {
internalZipcode: this.modelValue
}
},
emits: ['update:modelValue'],
methods: {
updateModelValue() {
this.$emit('update:modelValue', this.internalZipcode);
}
};
},
watch: {
modelValue(newValue) {
this.internalZipcode = newValue;
if (newValue !== this.internalZipcode) {
this.internalZipcode = newValue;
}
}
},
mounted() {
defineRule(`${this.cmsWidgetName}-zip-format`, regex(/^\d{5}$/, this.serviceZipFormatErrorMessage));
},
methods: {
updateModelValue() {
this.$emit('update:modelValue', this.internalZipcode);
}
}
};
</script>

View file

@ -5,15 +5,19 @@
<div
class="shop-address"
aria-live="polite">
<div class="address-header">{{ questionText }}</div>
<div
<div class="address-header">
{{ questionText }}
</div>
<div
v-if="modelValue"
class="current-address">
<div class="address-line">
<span class="shop-name">{{ displayedShopName }}</span>
<span class="shop-distance">{{ modelValue.distanceInMiles?.toFixed(2) }} mi</span>
</div>
<div class="address-line">{{ getProviderAddress(modelValue) }}</div>
<div class="address-line">
{{ getProviderAddress(modelValue) }}
</div>
<div class="address-line">
{{ getProviderCityZipState(modelValue) }}
</div>
@ -29,9 +33,9 @@
:variant="'primary'"
@clickEvent="openModal" />
<alert
class="alert-shop-distance"
v-if="modelValue?.distanceInMiles > 30"
ref="alertShopDistance"
class="alert-shop-distance"
cmsWidgetName="AlertShopDistanceWidget"
alertClass="alert-warning" />
<modal
@ -48,9 +52,9 @@
:markers="providerAddresses"
:zipCode="internalZipcode" />
<dropdownQuestion
inputId="searchRadiusDropdown"
ref="searchRadiusQuestion"
v-model="searchRadiusInMiles"
inputId="searchRadiusDropdown"
:cmsWidgetName="searchRadiusQuestionWidgetName"
:variant="dropdownVariants.compact"
:options="searchRadiusOptions" />
@ -93,7 +97,7 @@
import alert from '@/ux-components/alert/alert.vue';
import buttonMain from '@/ux-components/button-main/button-main.vue';
import modal from '@/digital-components/modal/modal.vue';
import serviceZipQuestion from '@/layouts/service-location/service-zip-question/service-zip-question.vue';
import serviceZipQuestion from '@/layouts/schedule-page/service-location/service-zip-question/service-zip-question.vue';
import buttonQuestion from '@/digital-components/button-question/button-question.vue';
import shopListButton from '@/iss-components/shop-list-button/shop-list-button.vue';
import dropdownQuestion from '@/digital-components/dropdown-question/dropdown-question.vue';
@ -136,8 +140,9 @@ export default {
cmsWidgetName: String,
validationRules: String,
modalWidgetName: String,
selectedAppointmentType: String,
selectedAppointmentType: String
},
emits: ['update:modelValue', 'zip-updated'],
data() {
return {
toTitleCase,
@ -147,7 +152,7 @@ export default {
searchRadiusQuestionWidgetName: 'SearchRadiusQuestionWidget',
textboxQuestionWidgetName: 'ChangeLocationZipQuestionWidget',
shopListButton: markRaw(shopListButton),
searchRadiusInMiles: "25",
searchRadiusInMiles: '25',
nearbyShops: [],
modalPositions,
dropdownVariants,
@ -157,7 +162,6 @@ export default {
SERVICE_ZIP_QUESTION_REF_NAME
};
},
emits: ['update:modelValue', 'zip-updated'],
computed: {
questionText() {
return this.getCmsContent(this.cmsWidgetName, widgetFields.INPUT_QUESTION_WIDGET.QUESTION_TEXT);
@ -166,12 +170,13 @@ export default {
return this.getCmsContent('ShowMoreShopsLinkWidget', widgetFields.TEXT_BLOCK_WIDGET.TEXT);
},
displayedShopName() {
if(this.modelValue.isSafeliteShop) {
if (this.modelValue.isSafeliteShop) {
return this.getCmsContent('SafeliteShopNameWidget', widgetFields.TEXT_BLOCK_WIDGET.TEXT);
}
return toTitleCase(this.modelValue.companyName);
},
modalHeaderText() {
// eslint-disable-next-line max-len
return this.getCmsContent(this.modalWidgetName, widgetFields.CONTENT_GROUP_WIDGET.HEADER_TEXT).replaceAll('{custom:serviceZipcode}', this.internalZipcode);
},
modalFooterText() {
@ -193,7 +198,7 @@ export default {
availabilityRatingCallback: getAvailabilityRating,
startDate: formattedStartDate,
endDate: formattedEndDate,
shopAppointmentType: "InshopOrDropoff"
shopAppointmentType: 'InshopOrDropoff'
};
},
serviceZipPrompt() {
@ -218,8 +223,8 @@ export default {
},
searchRadiusOptions() {
const optionsObj = {};
if(this.searchRadiusArray) {
this.searchRadiusArray.forEach(option => {
if (this.searchRadiusArray) {
this.searchRadiusArray.forEach((option) => {
optionsObj[option.Name] = option.Text;
});
}
@ -231,11 +236,11 @@ export default {
fullAddress: this.getFullProviderAddress(provider),
addressLines: [this.getProviderAddress(provider), this.getProviderCityZipState(provider)]
})) ?? [];
},
}
},
watch: {
async internalZipcode() {
if(this.openingModal) {
if (this.openingModal) {
return;
}
const shopsUpdated = await this.updateShops();
@ -245,27 +250,28 @@ export default {
}
},
async searchRadiusInMiles() {
if(this.openingModal) {
if (this.openingModal) {
return;
}
await this.updateShops();
}
},
methods: {
// eslint-disable-next-line consistent-return
async updateShops(autoExpand = false) {
this.errorMessage = '';
let result = await this.getNearbyShops(this.searchRadiusInMiles);
let currentSearchIndex = this.searchRadiusArray.findIndex(option => option.Name === 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;
// eslint-disable-next-line no-await-in-loop
result = await this.getNearbyShops(newSearchRadius);
currentSearchIndex++;
currentSearchIndex += 1;
}
if (result.length === 0) {
this.errorMessage = errorMessages.ZIP_CODE_NOT_SERVICED_FOR_VEHICLE;
}
else {
if (!result.find(shop => shop.providerNumber === this.internalProviderNumber)) {
} else {
if (!result.find((shop) => shop.providerNumber === this.internalProviderNumber)) {
this.internalProviderNumber = '';
await this.$nextTick();
}
@ -283,7 +289,7 @@ export default {
onModalOpened() {
this.openingModal = true;
this.internalZipcode = this.serviceZipcode;
this.searchRadiusInMiles = "25";
this.searchRadiusInMiles = '25';
this.nearbyShops = [];
this.updateShops(true).then(() => {
this.internalProviderNumber = this.nearbyShops[0]?.providerNumber;
@ -295,7 +301,7 @@ export default {
this.forceServiceZipRerender();
},
async updateSelectedProvider() {
const selectedShop = this.nearbyShops.find(shop => shop.providerNumber === this.internalProviderNumber);
const selectedShop = this.nearbyShops.find((shop) => shop.providerNumber === this.internalProviderNumber);
if (selectedShop) {
this.$emit('update:modelValue', selectedShop);
this.$emit('zip-updated', this.internalZipcode);
@ -303,7 +309,7 @@ export default {
}
},
async getNearbyShops(radiusInMiles) {
const result = await useMainStore().getProviders(this.internalZipcode, radiusInMiles);
const result = await useMainStore().getProviders(this.internalZipcode, radiusInMiles);
return result.data.shopProviders;
},
getFullProviderAddress(provider) {
@ -341,7 +347,7 @@ export default {
this.$nextTick(() => {
this.renderServiceZip = true;
});
},
}
}
};
</script>
@ -364,10 +370,11 @@ export default {
margin: .625rem 0;
}
.no-shops-found {
font-size: .75rem;
font-size: .8125rem;
margin: .75rem 0rem;
font-weight: $font-weight-bold;
line-height: 1.375rem;
color: #4d4e53;
}
.more-shops-button {
margin-bottom: 1.25rem;

View file

@ -1,658 +0,0 @@
<template>
<Form
ref="theForm"
v-slot="{ meta }"
@submit="onSubmit"
@invalidSubmit="onInvalidSubmit">
<div class="fade-on-route-transition">
<div class="justify-content-center">
<siteHeader cmsWidgetName="SiteHeaderWidget" />
</div>
<div class="iss-heritage-container-width">
<div class="service-location-container iss-heritage-content-container-width">
<siteSubHeader
cmsWidgetName="SiteSubHeaderWidget"
class="subheader" />
<alert
v-if="displayRecalibrationWarning"
ref="alertRecalNoMobile"
class="my-5"
cmsWidgetName="AlertRecalNoMobileWidget"
alertClass="alert-warning"
@text-link-clicked="openModalAction" />
<alert
v-if="displayBigTruckNoShops"
ref="alertBigTruckNoShops"
class="my-5"
cmsWidgetName="AlertBigTruckNoShopsWidget"
alertClass="alert-warning" />
<div class="appointment-type">
<div class="appointment-type-question-text d-flex">
<span class="w-100 recal-text" v-if="recalibrationRequired">{{ scheduleRecalText }}</span>
<span class="w-100" v-if="!requiresInshopRecalibration">{{ appointmentQuestionText }}</span>
</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
v-if="!requiresInshopRecalibration && isServiceableMobile"
ref="appointmentTypeQuestion"
v-model="selectedAppointmentType"
groupName="appointmentTypeQuestion"
cmsWidgetName="AppointmentTypeQuestionWidget"
validationRules="option-required" />
</div>
<div
v-if="isInshop">
<alert
v-if="displayLowAvailabilityInshop"
ref="alertLowAvailabilityInshop"
cmsWidgetName="AlertLowAvailabilityInshopWidget"
:isCollapsible="true"
alertClass="alert-warning" />
<shopAddress
ref="shopAddress"
v-model="selectedProvider"
:serviceZipcode="zipCode"
:selectedAppointmentType="selectedAppointmentType"
modalWidgetName="ChangeLocationModalWidget"
cmsWidgetName="ShopAddressWidget"
@zip-updated="handleInShopZipUpdated" />
<!-- INTEGRATION TODO: Put this right under the calendar -->
<alert
v-if="selectedProvider === null"
ref="alertNoAvailableShops"
cmsWidgetName="AlertNoAvailableShopsWidget"
:isCollapsible="true"
alertClass="alert-danger" />
</div>
<div
v-if="isMobile">
<div class="expandable-link-container">
<a
v-if="displayMilitaryZipAlert && !militaryBaseWarningExpanded"
href="#"
class="expandable-link"
@click.prevent="militaryBaseWarningExpanded = true">
{{ militaryBaseWarningLinkText }}
</a>
<div
v-if="displayMilitaryZipAlert && militaryBaseWarningExpanded"
class="expandable-link-text">
{{ militaryBaseWarningText }}
</div>
</div>
<serviceZipQuestion
ref="mobileServiceZipCodeQuestion"
v-model="mobileZipCode"
customInputId="mobileServiceZipCode"
class="mobile-service-zip-question"
:placeholderText="mobileZipPlaceholder"
:serviceZipFormatErrorMessage="errorMessages.MOBILE_SERVICE_ZIP_FORMAT"
:hasError="mobileZipError !== ''"
cmsWidgetName="MobileZipWidget" />
<div
v-if="mobileZipError"
ref="errorMessageDiv"
class="row my-1 form-test-error">
<span
class="d-inline-flex mt-0"
role="alert">{{ mobileZipError }}</span>
</div>
<textBlock
v-if="displayMobileFeeDisclaimer"
cmsWidgetName="MobileFeeDisclaimerWidget"
typeStyle="disclaimer" />
</div>
<contentGroupModal
:ref="RECAL_MODAL_REF_NAME"
cssModalHeadlineClass="text-center"
cmsWidgetName="RecalModal" />
<siteFooter
:ref="SITE_FOOTER_REF_NAME"
class="mt-5"
cmsWidgetName="SiteFooterWidget"
:isForwardActionDisabled="
!meta.valid || displayNoShopsAlert || displayBigTruckNoShops
"
@backClicked="navigateBack(this, navigateBackScenario)"
@forwardClicked="forwardButtonAction" />
</div>
</div>
</div>
</Form>
</template>
<script>
// Import Supporting Files
import { AppointmentTypeStrings } from '@/constants/schedule-constants.js';
import { fetchCmsContentForPage } from '@/helpers/cms-content-helper';
import settleAllPromises from '@/helpers/layout-helper';
import errorMessages from '@/constants/error-messages';
import { useMainStore } from '@/store';
import {
getPricedMobileFeePart,
getServiceabilityDetails,
getZipCodeData,
getMobileZipCodeData
} from '@/helpers/service-location-helper';
import { toTitleCase } from '@/helpers/text-helper.js';
import { getAvailabilityRating } from '@/helpers/service-location-helper';
// Import Component
import alert from '@/ux-components/alert/alert.vue';
import textBlock from '@/digital-components/text-block/text-block.vue';
import appointmentTypeQuestion from '@/layouts/service-location/appointment-type-question/appointment-type-question.vue';
import baseFormMixin from '@/mixins/base-form-mixin';
import contentGroupModal from '@/iss-components/content-group-modal/content-group-modal.vue';
import { Form, defineRule } from 'vee-validate';
import siteFooter from '@/iss-components/site-footer/site-footer.vue';
import siteHeader from '@/iss-components/site-header/site-header.vue';
import siteSubHeader from '@/iss-components/site-sub-header/site-sub-header.vue';
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';
export default {
name: 'service-location',
components: {
alert,
textBlock,
appointmentTypeQuestion,
contentGroupModal,
siteFooter,
siteHeader,
siteSubHeader,
// eslint-disable-next-line vue/no-reserved-component-names
Form,
shopAddress,
serviceZipQuestion
},
mixins: [baseFormMixin],
async beforeRouteEnter(to, from, next) {
// Call APIs
const cmsContentPromise = fetchCmsContentForPage(to.query.issPage);
const serviceZipCode = useMainStore().order.serviceLocation.zipCode || useMainStore().order.customer.address.zipCode;
const zipCodeData = getZipCodeData(serviceZipCode);
const mobileFeePartPromise = getPricedMobileFeePart(serviceZipCode);
const serviceabilityDetailsPromise = getServiceabilityDetails(serviceZipCode);
const getGlassFeesPromise = useMainStore().getGlassFees();
const providersPromise = useMainStore().getProviders(serviceZipCode);
// Settle promises and get results
const promiseResultMap = [
{
resultKey: 'cmsContent',
promise: cmsContentPromise
},
{
resultKey: 'mobileFeePart',
promise: mobileFeePartPromise
},
{
resultKey: 'glassFees',
promise: getGlassFeesPromise
},
{
resultKey: 'serviceabilityDetails',
promise: serviceabilityDetailsPromise
},
{
resultKey: 'zipCodeData',
promise: zipCodeData
},
{
resultKey: 'providers',
promise: providersPromise
}
];
const resultMap = await settleAllPromises(promiseResultMap);
useMainStore().updateIsSafeliteProvider(true);
next(async (vm) => {
vm.setCmsContent(resultMap.cmsContent);
await vm.setData(
resultMap.zipCodeData,
resultMap.serviceabilityDetails,
resultMap.mobileFeePart,
resultMap.glassFees,
resultMap.providers
);
});
},
setup() {
const mainStore = useMainStore();
return { mainStore };
},
data() {
return {
streetAddress: this.getServiceAddressFromStore(),
streetAddress2: this.getServiceAddress2FromStore(),
city: this.getServiceCityFromStore(),
state: this.getServiceStateFromStore(),
zipCode: this.getServiceZipCodeFromStore(),
isGlassServiceableInshop: null,
isRecalibrationServiceableInshop: null,
isGlassServiceableMobile: null,
isRecalibrationServiceableMobile: null,
selectedAppointmentType: this.getSelectedAppointmentType(),
selectedProvider: this.getSelectedProvider(),
mobileFeePart: null,
mobileProviderNumber: null,
zipContainsMilitaryBase: false,
zipCodeCtu: null,
mobileZipCode: '',
mobileZipError: '',
militaryBaseWarningExpanded: false,
availabilityRating: null,
RECAL_MODAL_REF_NAME,
SITE_FOOTER_REF_NAME,
errorMessages
};
},
computed: {
questionText() {
return this.getCmsContent(
'ServiceTypeQuestionWidget',
'QuestionText'
);
},
answersFromCms() {
return this.getCmsContent('ServiceTypeQuestionWidget', 'Answers');
},
isServiceableMobile() {
if (this.isRecalibrationServiceableMobile !== null) {
return (
this.isGlassServiceableMobile
&& this.isRecalibrationServiceableMobile
);
}
return this.isGlassServiceableMobile;
},
isServiceableInshop() {
if (this.isRecalibrationServiceableInshop !== null) {
return (
this.isGlassServiceableInshop
&& this.isRecalibrationServiceableInshop
);
}
return this.isGlassServiceableInshop;
},
isInshop() {
return (
this.selectedAppointmentType === 'Inshop'
|| this.selectedAppointmentType === 'Dropoff'
);
},
isMobile() {
return this.selectedAppointmentType === AppointmentTypeStrings.MOBILE
},
requiresInshopRecalibration() {
// Specifically check for isRecalibrationServiceableMobile === false, not null or true.
return (
this.isServiceableInshop
&& this.isGlassServiceableMobile
&& this.isRecalibrationServiceableMobile === false
);
},
displayMilitaryZipAlert() {
return this.zipContainsMilitaryBase && this.isServiceableMobile;
},
displayNoShopsAlert() {
return !this.isBigTruck && !this.isServiceableInshop && !this.isServiceableMobile;
},
displayBigTruckNoShops() {
return this.isBigTruck && !this.isServiceableInshop && !this.isServiceableMobile;
},
displayRecalibrationWarning() {
return this.requiresInshopRecalibration;
},
displayLowAvailabilityInshop() {
return this.availabilityRating === 'low' && this.isInshop;
},
displayServiceableInshopOnly() {
return !this.isServiceableMobile && this.isServiceableInshop && !this.displayRecalibrationWarning;
},
displayServiceableMobileOnly() {
return this.isServiceableMobile && this.recalibrationRequired && !this.isServiceableInshop;
},
displayMobileFeeDisclaimer() {
return this.mainStore.isNoComp || this.mainStore.isITAC;
},
navigateBackScenario() {
const { isNoComp, isITAC } = useMainStore();
return isNoComp || isITAC
? this.navigationScenarios.CLICKED_BACK_CANNOT_REACH_TPA_FLOW
: this.navigationScenarios.CLICKED_BACK_CAN_REACH_TPA_FLOW;
},
isBigTruck() {
return this.mainStore.order.vehicle.isBigTruck;
},
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('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.mainStore.hasRecalibrationPart;
}
},
methods: {
arePagePrerequisitesValid() {
return (
useMainStore().order.serviceLocation.zipCode !== null
);
},
async forwardButtonAction() {
let provider = this.selectedProvider;
this.mainStore.updateMobileFee(null);
if (this.isMobile) {
if (this.mainStore.isNoComp || this.mainStore.isITAC) {
this.mainStore.updateMobileFee(this.mobileFeePart);
}
provider = {
providerNumber: this.mobileProviderNumber,
address: {
streetAddress: null,
city: null,
state: null,
zipCode: null,
zipCodeCtu: null
}
};
}
this.mainStore.saveServiceLocation({
address: this.streetAddress,
address2: this.streetAddress2,
city: this.city,
state: this.state,
zipCode: this.zipCode,
zipCodeCtu: this.zipCodeCtu,
appointmentType: this.selectedAppointmentType,
provider
});
this.$router.navigate(
this.navigationScenarios.CLICKED_FORWARD,
this.$route
);
},
openModalAction(modalName) {
this.$refs[modalName].openModal();
},
resetDependentState() {},
getServiceAddressFromStore() {
return useMainStore().order.serviceLocation.address;
},
getServiceAddress2FromStore() {
return useMainStore().order.serviceLocation.address2;
},
getServiceCityFromStore() {
return useMainStore().order.serviceLocation.city;
},
getServiceStateFromStore() {
return (
useMainStore().order.serviceLocation.state
|| useMainStore().order.customer.address.state
);
},
getServiceZipCodeFromStore() {
return (
useMainStore().order.serviceLocation.zipCode
|| useMainStore().order.customer.address.zipCode
);
},
getSelectedAppointmentType() {
return useMainStore().order.serviceLocation.appointmentType;
},
getSelectedProvider() {
return useMainStore().order.serviceLocation.provider;
},
setCtuForMobile(val) {
this.zipCodeCtu = val;
},
async setData(
zipCodeData,
serviceabilityDetails,
mobileFeePart,
glassFees,
providers
) {
if (zipCodeData) {
this.zipContainsMilitaryBase = zipCodeData.containsMilitaryBase;
this.zipCodeCtu = zipCodeData.zipCodeCtu;
this.city = zipCodeData.city;
}
if (serviceabilityDetails) {
this.setServiceabilityDetails(serviceabilityDetails);
}
if (mobileFeePart) {
this.mobileFeePart = mobileFeePart;
}
if (providers) {
this.setMobileProviderNumber(providers.mobileProviderNumber);
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];
}
else if(providers.shopProviders.length === 0) {
this.selectedProvider = null;
}
}
if (glassFees) {
const pricedGlassFees = await useMainStore().getCombinedQuote(glassFees);
this.setGlassFeeItems(pricedGlassFees);
}
},
setContainsMilitaryBase(val) {
if (this.zipContainsMilitaryBase !== val) {
this.zipContainsMilitaryBase = val;
}
},
setGlassFeeItems(newGlassFeeItems) {
this.mainStore.updateGlassFees(newGlassFeeItems);
},
setMobileFeePart(mobileFeePart) {
this.mobileFeePart = mobileFeePart;
},
setMobileProviderNumber(providerNumber) {
this.mobileProviderNumber = providerNumber;
},
setServiceabilityDetails(serviceabilityDetails) {
this.isGlassServiceableInshop =
serviceabilityDetails.isGlassServiceableInshop;
this.isRecalibrationServiceableInshop =
serviceabilityDetails.isRecalibrationServiceableInshop;
this.isGlassServiceableMobile =
serviceabilityDetails.isGlassServiceableMobile;
this.isRecalibrationServiceableMobile =
serviceabilityDetails.isRecalibrationServiceableMobile;
if (!this.isServiceableMobile) {
this.selectedAppointmentType = AppointmentTypeStrings.IN_SHOP;
}
},
async updateMobileZip() {
this.mobileZipError = '';
const zipCodeData = await getMobileZipCodeData(this.mobileZipCode);
if (!zipCodeData.isValid) {
this.mobileZipError = errorMessages.INVALID_ZIP;
} else if (!zipCodeData.isServiceable) {
this.mobileZipError = errorMessages.NO_SERVICE_IN_AREA(toTitleCase(zipCodeData.city));
} else {
this.city = zipCodeData.city;
this.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];
}
else {
this.selectedProvider = null;
}
}
});
}
},
async handleInShopZipUpdated(newZip) {
this.zipCode = newZip;
const zipCodeData = await getZipCodeData(this.zipCode);
this.city = zipCodeData.city;
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);
}
});
}
},
watch: {
mobileZipCode(newZip) {
if(newZip !== '') {
this.updateMobileZip();
}
},
selectedAppointmentType() {
this.mobileZipCode = '';
if(this.selectedAppointmentType === AppointmentTypeStrings.IN_SHOP && this.selectedProvider) {
const startDate = new Date();
const endDate = new Date();
endDate.setDate(startDate.getDate() + 6);
const formattedStartDate = startDate.toISOString().split('T')[0];
const formattedEndDate = endDate.toISOString().split('T')[0];
getAvailabilityRating(
formattedStartDate,
formattedEndDate,
AppointmentTypeStrings.IN_SHOP,
this.selectedProvider ? this.selectedProvider.providerNumber : null
).then((rating) => {
this.availabilityRating = rating;
});
}
}
}
};
</script>
<style lang="scss" scoped>
$page-side-padding: 1.5rem;
.iss-heritage-container-width {
.service-location-container {
position: relative;
min-height: 1px;
padding-left: .9375rem;
padding-right: .9375rem;
}
}
.service-location-container {
.subheader {
margin: 1.25rem 0;
}
.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;
flex-direction: column;
.recal-text {
margin-bottom: 1.25rem;
font-weight: $font-weight-bold;
}
}
.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;
}
}
}
</style>

View file

@ -386,7 +386,7 @@ describe('tpa-submit', () => {
serviceLocation: {
provider: {
companyName: providerCompanyName
}
}
},
contactInfo: {
firstName,
@ -431,7 +431,7 @@ describe('tpa-submit', () => {
serviceLocation: {
provider: {
companyName: providerCompanyName
}
}
},
contactInfo: {
firstName,
@ -520,7 +520,9 @@ describe('tpa-submit', () => {
// Arrange
const initialStore = {
order: {
currentDeductible: storeValue
currentDeductible: {
replace: storeValue
}
}
};
const { wrapper } = getMountedComponent(initialStore);
@ -568,7 +570,10 @@ describe('tpa-submit', () => {
insuranceCoverage: {
coverageStatus: status
},
currentDeductible: deductible
currentDeductible: {
replace: deductible,
repair: deductible
}
}
};
const { wrapper } = getMountedComponent(initialStore);
@ -596,7 +601,10 @@ describe('tpa-submit', () => {
insuranceCoverage: {
coverageStatus: status
},
currentDeductible: deductible
currentDeductible: {
replace: deductible,
repair: deductible
}
}
};
const { wrapper } = getMountedComponent(initialStore);

View file

@ -16,12 +16,11 @@
<alert
ref="alertIncomplete"
alertClass="alert-warning"
:cmsWidgetName="widget.alertIncomplete"
/>
:cmsWidgetName="widget.alertIncomplete" />
<textBlock
ref="subHeaderTitle"
:customText="subHeaderTitle"
class="tpa-submit-title"/>
class="tpa-submit-title" />
<textBlock
id="tpaSubmitSubHeaderBodyOne"
ref="tpaSubmitSubHeaderBodyOne"
@ -40,9 +39,15 @@
:headerText="recalModalContent.headerText"
:footerButtonText="recalModalContent.footerButtonText"
@footerButtonEvent="closeRecalModal">
<div class="recal-modal-subheader">{{ recalModalContent.subHeaderText }}</div>
<img class="recal-modal-image" :src="recalModalContent.image" />
<div class="recal-modal-body" v-html="recalModalContent.bodyText"></div>
<div class="recal-modal-subheader">
{{ recalModalContent.subHeaderText }}
</div>
<img
class="recal-modal-image"
:src="recalModalContent.image" />
<div
class="recal-modal-body"
v-html="recalModalContent.bodyText"></div>
</modal>
<hr />
<div id="serviceSummarySection">
@ -155,7 +160,7 @@ export default {
alertIncomplete: 'AlertIncompleteWidget',
alertRecalWarning: 'AlertRecalWarningWidget',
editShopLinkText: 'EditShopLinkTextWidget',
contactDetails: 'ContactDetailsSectionWidget'
contactDetails: 'ContactDetailsSectionWidget'
},
modalPositions
};
@ -207,7 +212,7 @@ export default {
return this.mainStore.isVerified;
},
currentDeductible() {
return this.mainStore.order.currentDeductible;
return this.mainStore.currentDeductible;
},
deductibleBoxValue() {
return this.isVerified
@ -269,7 +274,7 @@ export default {
bodyText: this.getCmsContent(widgetName, widgetFields.CONTENT_GROUP_WIDGET.BODY_TEXT),
footerButtonText: this.getCmsContent(widgetName, widgetFields.CONTENT_GROUP_WIDGET.FOOTER_TEXT),
image: this.getCmsContent(widgetName, widgetFields.CONTENT_GROUP_WIDGET.IMAGE)
}
};
},
recalibrationRequired() {
return this.mainStore.hasRecalibrationPart;
@ -281,9 +286,9 @@ export default {
}
return {
glassShop: this.getPreferredShopTitle,
contactPhone: contactPhone,
contactPhone,
contactEmail: this.mainStore.contactInfo.emailAddress ?? ''
}
};
}
},
methods: {
@ -293,7 +298,7 @@ export default {
this.getPreferredShopTitle,
this.getPreferredShopLines,
this.getEditShopLinkText,
() => this.navigate(this.navigationScenarios.EDIT_PREFERRED_SHOP),
() => this.navigate(this.navigationScenarios.EDIT_PREFERRED_SHOP)
),
// eslint-disable-next-line max-len
this.getSection(
@ -306,7 +311,7 @@ export default {
},
getSection(title, lines, editLinkText, onClick) {
return {
title: title,
title,
lines,
editLinkText,
onClickEdit: onClick

View file

@ -354,7 +354,7 @@ describe('vehicle-questions-mixin', () => {
const testCases = [
[issPageValues.PART_QUESTIONS, issPageValues.VEHICLE_PARTS, false],
[issPageValues.VEHICLE_PARTS, issPageValues.VEHICLE_PARTS, false],
[issPageValues.VEHICLE_PARTS, issPageValues.SERVICE_LOCATION, true],
[issPageValues.VEHICLE_PARTS, issPageValues.SCHEDULE_PAGE, true],
[issPageValues.VEHICLE_PARTS, issPageValues.PART_QUESTIONS, true],
[issPageValues.PART_QUESTIONS, issPageValues.CAPABILITY_QUESTIONS, false],
[issPageValues.MOLDING_QUESTIONS, issPageValues.CAPABILITY_QUESTIONS, false]

View file

@ -23,7 +23,6 @@ const issPageValues = Object.freeze({
POLICY_HOLDER_DETAILS: 'policy-holder-details',
PROVIDER_PREFERENCE: 'provider-preference',
REVEAL: 'reveal',
SERVICE_LOCATION: 'service-location',
TPA_CONFIRMATION: 'tpa-confirmation',
SERVICE_PACKAGES: 'service-packages',
SCHEDULE_PAGE: 'schedule-page',

View file

@ -64,7 +64,8 @@ const navigationScenarios = Object.freeze({
CLICKED_BACK_WITH_NO_VIN_NOR_QUESTIONS: 'CLICKED_BACK_WITH_NO_VIN_NOR_QUESTIONS',
// Schedule
CLICKED_CHANGE_LOCATION: 'CLICKED_CHANGE_LOCATION',
CLICKED_BACK_CANNOT_REACH_TPA_FLOW: 'CLICKED_BACK_CANNOT_REACH_TPA_FLOW',
CLICKED_BACK_CAN_REACH_TPA_FLOW: 'CLICKED_BACK_CAN_REACH_TPA_FLOW',
// Coverage Statement
CLICKED_BACK_WITH_REPAIR: 'CLICKED_BACK_WITH_REPAIR',
@ -83,10 +84,6 @@ const navigationScenarios = Object.freeze({
EDIT_PREFERRED_SHOP: 'EDIT_PREFERRED_SHOP',
EDIT_CONTACT_DETAILS: 'EDIT_CONTACT_DETAILS',
// Service location
CLICKED_BACK_CANNOT_REACH_TPA_FLOW: 'CLICKED_BACK_CANNOT_REACH_TPA_FLOW',
CLICKED_BACK_CAN_REACH_TPA_FLOW: 'CLICKED_BACK_CAN_REACH_TPA_FLOW',
// Provider Preference
CLICKED_FORWARD_WITH_SAFELITE: 'CLICKED_FORWARD_WITH_SAFELITE',
CLICKED_FORWARD_WITH_TPA_ENABLED: 'CLICKED_FORWARD_WITH_TPA_ENABLED',

View file

@ -534,7 +534,7 @@ const routingTable = () => [
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_SAFELITE,
destinationIssPageValue: issPageValues.SERVICE_LOCATION
destinationIssPageValue: issPageValues.SCHEDULE_PAGE
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_NON_SAFELITE_SHOP,
@ -559,7 +559,7 @@ const routingTable = () => [
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_SAFELITE,
destinationIssPageValue: issPageValues.SERVICE_LOCATION
destinationIssPageValue: issPageValues.SCHEDULE_PAGE
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_TPA_ENABLED,
@ -572,7 +572,7 @@ const routingTable = () => [
]
},
{
issPageValue: issPageValues.SERVICE_LOCATION,
issPageValue: issPageValues.SCHEDULE_PAGE,
maps: [
{
scenario: navigationScenarios.CLICKED_BACK_CANNOT_REACH_TPA_FLOW,
@ -582,26 +582,9 @@ const routingTable = () => [
scenario: navigationScenarios.CLICKED_BACK_CAN_REACH_TPA_FLOW,
destinationIssPageValue: issPageValues.PROVIDER_PREFERENCE
},
{
scenario: navigationScenarios.CLICKED_FORWARD,
destinationIssPageValue: issPageValues.SCHEDULE_PAGE
}
]
},
{
issPageValue: issPageValues.SCHEDULE_PAGE,
maps: [
{
scenario: navigationScenarios.CLICKED_BACK,
destinationIssPageValue: issPageValues.SERVICE_LOCATION
},
{
scenario: navigationScenarios.CLICKED_FORWARD,
destinationIssPageValue: issPageValues.CONTACT_DETAILS
},
{
scenario: navigationScenarios.CLICKED_CHANGE_LOCATION,
destinationIssPageValue: issPageValues.SERVICE_LOCATION
}
]
},
@ -750,7 +733,7 @@ const routingTable = () => [
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_SAFELITE_SHOP,
destinationIssPageValue: issPageValues.SERVICE_LOCATION
destinationIssPageValue: issPageValues.SCHEDULE_PAGE
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_NON_SAFELITE_SHOP,

View file

@ -220,8 +220,16 @@ export const getDefaultState = () => ({
settledTenderAmount: null,
lockToken: null,
customerPortalLoginToken: null,
originalDeductible: null,
currentDeductible: null,
originalDeductible: {
repair: null,
replace: null
},
currentDeductible: {
repair: null,
replace: null
},
totalTaxAmount: null,
waiverReasons: null,
carrierPhoneNumber: null,
loadedFromCookie: false,
loadedFromDupeCheck: null,
@ -439,7 +447,9 @@ export const useMainStore = defineStore({
experimentSettings: (state) => state.applicationUser.experiments
.filter((x) => !!x.isActive)
.map((x) => x.settings)
.reduce((r, c) => Object.assign(r, c), {}) ?? {}
.reduce((r, c) => Object.assign(r, c), {}) ?? {},
originalDeductible: (state) => (state.order.damage.isRepair ? state.order.originalDeductible.repair : state.order.originalDeductible.replace),
currentDeductible: (state) => (state.order.damage.isRepair ? state.order.currentDeductible.repair : state.order.currentDeductible.replace)
},
actions:
{
@ -653,7 +663,7 @@ export const useMainStore = defineStore({
safelitePolicy: {
policies: []
},
actualDeductible: this.order.currentDeductible?.toString() ?? ''
actualDeductible: this.currentDeductible.toString() ?? ''
},
lossInfo: {
dateOfLoss: this.order.policy.dateOfLoss,
@ -716,8 +726,8 @@ export const useMainStore = defineStore({
manualGlassNames: manualGlassNamesArray,
policyState: this.order.customer.address.state,
status: this.order.policy.status,
originalDeductible: this.order.originalDeductible,
currentDeductible: this.order.currentDeductible,
originalDeductible: this.originalDeductible,
currentDeductible: this.currentDeductible,
noCoverage: false,
isRepair: this.order.damage.isRepair,
policyNumber: this.order.policy.policyNumber,
@ -731,7 +741,7 @@ export const useMainStore = defineStore({
}
}).then((r) => {
this.order.policy.policyData = r.data.policyData;
this.updateDeductible(r.data.deductible);
this.updateDeductible(r.data);
return resolve(r);
}).catch((error) => reject(error));
});
@ -875,7 +885,7 @@ export const useMainStore = defineStore({
logApiCall: true
});
},
getMobileTimeSlots(startDate, endDate) {
getMobileTimeSlots(startDate, endDate, zipCodeOverride = null) {
const { order } = this;
const { vehicle } = this.order;
let lineItems = [
@ -921,8 +931,9 @@ export const useMainStore = defineStore({
style: vehicle.style,
vin: vehicle.vin ?? ''
},
zipCode: order.serviceLocation.zipCode
zipCode: zipCodeOverride ?? order.serviceLocation.zipCode
};
return globalMethods.callHttpClient({
method: endpoints.GetMobileTimeSlots.method,
endpoint: endpoints.GetMobileTimeSlots.url,
@ -931,7 +942,7 @@ export const useMainStore = defineStore({
additionalSuccessEventDataHandler: (response) =>
getTimeSlotsAdditionalEventData(
response.data.provisionalTriggers,
order.serviceLocation.zipCode,
zipCodeOverride ?? order.serviceLocation.zipCode,
response.data.days?.[0]?.date
)
});
@ -1197,7 +1208,7 @@ export const useMainStore = defineStore({
Model: vehicle.model
},
Insurance: {
Deductible: this.order.currentDeductible ?? 0,
Deductible: this.currentDeductible ?? 0,
PolicyNumber: policy.policyNumber,
CoverageStatus: insuranceCoverage.coverageStatus,
CoverageType: insuranceCoverage.coverageType
@ -1447,8 +1458,8 @@ export const useMainStore = defineStore({
},
policyNumber: policy.policyNumber,
policyZipCode: policy.policyZipCode,
originalDeductible: this.order.originalDeductible,
currentDeductible: this.order.currentDeductible,
originalDeductible: this.originalDeductible,
currentDeductible: this.currentDeductible,
OemEndorsement: this.hasOemEndorsement,
noCoverage: this.isNoComp,
isItac: this.isITAC
@ -2030,9 +2041,10 @@ export const useMainStore = defineStore({
this.order.policy.deductible.repair = coverage?.repairWaived ?? false ? 0 : coverage.deductible;
this.order.policy.endorsements = coverage?.endorsements;
// TODO logic should be more complicated later on
this.order.originalDeductible = coverage.deductible;
this.order.currentDeductible = coverage.deductible;
this.order.originalDeductible.replace = this.order.policy.deductible.replace;
this.order.currentDeductible.replace = this.order.policy.deductible.replace;
this.order.originalDeductible.repair = this.order.policy.deductible.repair;
this.order.currentDeductible.repair = this.order.policy.deductible.repair;
},
resetOrder() {
@ -2045,8 +2057,10 @@ export const useMainStore = defineStore({
this.order.settledTenderAmount = null;
this.order.lockToken = null;
this.order.eon = null;
this.order.originalDeductible = null;
this.order.currentDeductible = null;
this.order.originalDeductible.repair = null;
this.order.originalDeductible.replace = null;
this.order.currentDeductible.repair = null;
this.order.currentDeductible.replace = null;
this.order.loadedFromDupeCheck = null;
this.order.loadedSessionClearedPreviousData = null;
this.order.availableVaps = null;
@ -2316,8 +2330,12 @@ export const useMainStore = defineStore({
updateIsSafeliteProvider(isSafelite) {
this.order.serviceLocation.IsSafeliteProvider = isSafelite;
},
updateDeductible(finalDeductible) {
this.order.currentDeductible = finalDeductible;
updateDeductible(deductibleInfo) {
if (this.order.damage.isRepair) {
this.order.currentDeductible.repair = deductibleInfo.deductible;
} else {
this.order.currentDeductible.replace = deductibleInfo.deductible;
}
},
savePartQuestionAnswers(partQuestionAnswersArray) {
// if part question answers have changed, reset subsequent question answers
@ -2717,19 +2735,6 @@ export const useMainStore = defineStore({
},
saveServiceLocation(serviceLocationInfo) {
if (this.order.serviceLocation) {
if (
serviceLocationInfo.zipCode !== this.order.serviceLocation.zipCode
|| !providersEqual(
serviceLocationInfo.provider,
this.order.serviceLocation.provider
)
|| serviceLocationInfo.appointmentType
!== this.order.serviceLocation.appointmentType
) {
this.resetSchedule();
}
}
this.updateServiceLocation(serviceLocationInfo);
},

View file

@ -12,7 +12,6 @@ import coverageStatuses from '@/constants/coverage-statuses.js';
import { paymentMethods } from '@/constants/payment-method-constants';
import endpoints from '@/constants/endpoints';
import { AppointmentTypeStrings } from '@/constants/schedule-constants';
import bailoutCode from '@/constants/bailoutCode';
import coverageType from '@/constants/coverage-type';
import { getEnumName } from '@/helpers/unit-test-helper';
@ -227,6 +226,10 @@ describe('Store', () => {
test(`CoverageType is set to expected ${getEnumName(coverageType, expected)} when vehicle noCoverage is ${noCoverage} and enableNoCompQuote is ${enableNoCompQuote}`, () => {
// Arrange
store.issConfig.enableNoCompQuote = enableNoCompQuote;
store.order.currentDeductible = {
replace: 500,
repair: 0
};
const vehicleCoverage = {
noCoverage
};
@ -407,6 +410,10 @@ describe('Store', () => {
}
};
store.order.insuranceCoverage.coverageType = coverageType.NO_COMP;
store.order.currentDeductible = {
replace: 500,
repair: 0
};
globalMethods.callHttpClient = jest.fn().mockReturnValue(Promise.resolve(response));
@ -434,6 +441,10 @@ describe('Store', () => {
}
};
store.order.insuranceCoverage.coverageType = coverageType.Deductible;
store.order.currentDeductible = {
replace: 500,
repair: 0
};
globalMethods.callHttpClient = jest.fn().mockReturnValue(Promise.resolve(response));
@ -453,6 +464,10 @@ describe('Store', () => {
const error = 'register claim error';
globalMethods.callHttpClient = jest.fn().mockReturnValue(Promise.reject(error));
store.order.insuranceCoverage.coverageStatus = coverageStatuses.PENDING;
store.order.currentDeductible = {
replace: 500,
repair: 0
};
// Act
await store.registerClaim().catch((e) => {
@ -501,7 +516,7 @@ describe('Store', () => {
home: homePhone,
service: servicePhone,
alternative: altPhone,
extension: extension
extension
});
// Assert
@ -670,10 +685,22 @@ describe('Store', () => {
const policyZipCode = getRandomString(6, 6);
const status = getRandomEnum(coverageStatuses);
const type = getRandomEnum(coverageType);
const originalDeductible = getRandomString(6, 6);
const currentDeductible = getRandomString(6, 6);
store.order.originalDeductible = originalDeductible;
store.order.currentDeductible = currentDeductible;
const originalDeductible = {
replace: 500,
repair: 0
};
const currentDeductible = {
replace: 500,
repair: 0
};
store.order.originalDeductible = {
replace: originalDeductible.replace,
repair: originalDeductible.repair
};
store.order.currentDeductible = {
replace: currentDeductible.replace,
repair: currentDeductible.repair
};
store.order.customer.firstName = customerFirstName;
store.order.customer.lastName = customerLastName;
store.order.customer.emailAddress = customerEmail;
@ -684,6 +711,7 @@ describe('Store', () => {
store.order.policy.policyZipCode = policyZipCode;
store.order.insuranceCoverage.coverageStatus = status;
store.order.insuranceCoverage.coverageType = type;
store.order.damage.isRepair = false;
globalMethods.callHttpClient = jest.fn().mockReturnValue(Promise.resolve({}));
// Act
@ -702,8 +730,8 @@ describe('Store', () => {
}),
policyNumber,
policyZipCode,
originalDeductible,
currentDeductible
originalDeductible: originalDeductible.replace,
currentDeductible: currentDeductible.replace
}),
insuranceCoverage: expect.objectContaining({
coverageStatus: status,
@ -1717,8 +1745,8 @@ describe('Store', () => {
const manualGlassNames = [location];
const policyState = getRandomString(2, 2);
const status = getRandomString(6, 8);
const currentDeductible = getRandomInt(0, 5000);
const originalDeductible = getRandomInt(0, 5000);
const currentDeductible = 500;
const originalDeductible = 500;
const isRepair = getRandomBoolean();
const policyNumber = getRandomString(10, 20);
const insuredFirstName = getRandomString(10, 20);
@ -1730,8 +1758,14 @@ describe('Store', () => {
store.order.referralCorrelationId = referralCorrelationId;
store.order.parentAccountNumber = parentAccountNumber;
store.order.currentDeductible = currentDeductible;
store.order.originalDeductible = originalDeductible;
store.order.currentDeductible = {
replace: currentDeductible,
repair: currentDeductible
};
store.order.originalDeductible = {
replace: originalDeductible,
repair: originalDeductible
};
store.order.policy.status = status;
store.order.customer.address.state = policyState;
store.order.damage.isRepair = isRepair;
@ -1827,8 +1861,14 @@ describe('Store', () => {
store.order.referralCorrelationId = referralCorrelationId;
store.order.parentAccountNumber = parentAccountNumber;
store.order.currentDeductible = currentDeductible;
store.order.originalDeductible = originalDeductible;
store.order.currentDeductible = {
replace: currentDeductible,
repair: currentDeductible
};
store.order.originalDeductible = {
replace: originalDeductible,
repair: originalDeductible
};
store.order.policy.status = status;
store.order.customer.address.state = policyState;
store.order.policy.endorsementQuestionAnswers = endorsementAnswers;
@ -1898,23 +1938,29 @@ describe('Store', () => {
describe('updateDeductible method', () => {
it('final deductible is saved as currentDeductible in store', () => {
// Arrange
const finalDeductible = getRandomInt(0, 5000);
store.order.damage.isRepair = false;
const deductibleInfo = {
deductible: 500
};
// Act
store.updateDeductible(finalDeductible);
store.updateDeductible(deductibleInfo);
// Assert
expect(store.order.currentDeductible).toEqual(finalDeductible);
expect(store.order.currentDeductible.replace).toEqual(deductibleInfo.deductible);
});
it('null final deductible => currentDeductible in store set to null', () => {
// Arrange
const finalDeductible = null;
store.order.damage.isRepair = false;
const deductibleInfo = {
deductible: null
};
// Act
store.updateDeductible(finalDeductible);
store.updateDeductible(deductibleInfo);
// Assert
expect(store.order.currentDeductible).toEqual(finalDeductible);
expect(store.order.currentDeductible.replace).toEqual(deductibleInfo.deductible);
});
});

View file

@ -6,15 +6,18 @@
//Define colors for namespaced classes
$header-background: #ffd100;
$accent-fill: #e6f1f3; // Calendar background
$accent-color: #09748b;
$link: #09748b;
$accent-color: #0070d1;
$link: #0070d1;
$svg-fill-color: '%2309748b'; // Color of calendar icon. Place HEX code *after* %23
$progress-bar-success-color: #1a1446;
$progress-bar-background-color: #ffffff;
a,
svg,
.modal-text {
fill: $accent-color;
color: $accent-color;
font-weight: 500;
}
.container-fluid {

View file

@ -8,9 +8,21 @@
cssClassNameForCmsWidget,
]">
<div class="alert-header">
<svg class="alert-icon" xmlns="http://www.w3.org/2000/svg" width="20" height="21" viewBox="0 0 20 21">
<path fill-rule="nonzero" d="M10 .5c5.523 0 10 4.477 10 10s-4.477 10-10 10-10-4.477-10-10S4.477.5 10 .5zm.043 13.6a1 1 0 1 0 0 2 1 1 0 0 0 0-2zm.77-9.2h-1.54l-.088.009a.502.502 0 0 0-.299.193.66.66 0 0 0-.125.47l.747 6.806.017.096c.065.249.263.425.495.426l.084-.008c.22-.042.396-.246.427-.51l.793-6.805.004-.102a.651.651 0 0 0-.127-.37.489.489 0 0 0-.388-.205z"/>
</svg>
<div class="alert-icon-container">
<svg
class="alert-icon"
xmlns="http://www.w3.org/2000/svg"
width="20"
height="21"
viewBox="0 0 20 21">
<path
fill-rule="nonzero"
d="M10 .5c5.523 0 10 4.477 10 10s-4.477 10-10 10-10-4.477-10-10S4.477.5 10 .5zm.043 13.6a1
1 0 1 0 0 2 1 1 0 0 0 0-2zm.77-9.2h-1.54l-.088.009a.502.502 0 0 0-.299.193.66.66 0 0
0-.125.47l.747 6.806.017.096c.065.249.263.425.495.426l.084-.008c.22-.042.396-.246.427-.51l.793-6.805.004-.102a.651.651
0 0 0-.127-.37.489.489 0 0 0-.388-.205z" />
</svg>
</div>
<span class="m-0 alert-header-text">
{{ alertHeadline }}
</span>
@ -19,10 +31,18 @@
class="btn-collapse"
type="button"
@click="toggleCollapse">
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="9" viewBox="0 0 15 9"
<svg
xmlns="http://www.w3.org/2000/svg"
width="15"
height="9"
viewBox="0 0 15 9"
class="btn-collapse-icon"
:class="{ 'rotated': !collapsed }">
<path fill-rule="nonzero" d="M7.5 0a.806.806 0 0 0-.593.265L.246 7.455a.957.957 0 0 0 0 1.28.796.796 0 0 0 1.185 0l6.07-6.55 6.068 6.55a.796.796 0 0 0 1.186 0 .957.957 0 0 0 0-1.28L8.093.265A.806.806 0 0 0 7.5 0"/>
<path
fill-rule="nonzero"
d="M7.5 0a.806.806 0 0 0-.593.265L.246 7.455a.957.957 0 0 0 0 1.28.796.796 0 0 0
1.185 0l6.07-6.55 6.068 6.55a.796.796 0 0 0 1.186 0 .957.957 0 0 0 0-1.28L8.093.265A.806.806
0 0 0 7.5 0" />
</svg>
</button>
<button
@ -36,13 +56,16 @@
viewBox="0 0 23.7 23.7"
xml:space="preserve">
<path
d="m23.24 2.7-9.15 9.15L23.24 21a1.581 1.581 0 0 1-1.12 2.7c-.42 0-.82-.16-1.12-.46l-9.15-9.15-9.15 9.15c-.3.3-.7.46-1.12.46A1.581 1.581 0 0 1 .46 21l8.47-8.47.68-.68L.46 2.7c-.62-.62-.62-1.62 0-2.24.62-.62 1.62-.62 2.24 0l8.47 8.47.68.68L21 .46a1.57 1.57 0 0 1 2.23 0c.63.62.63 1.62.01 2.24z" />
d="m23.24 2.7-9.15 9.15L23.24 21a1.581 1.581 0 0 1-1.12 2.7c-.42 0-.82-.16-1.12-.46l-9.15-9.15-9.15 9.15c-.3.3-.7.46-1.12.46A1.581
1.581 0 0 1 .46 21l8.47-8.47.68-.68L.46 2.7c-.62-.62-.62-1.62 0-2.24.62-.62 1.62-.62 2.24 0l8.47 8.47.68.68L21
.46a1.57 1.57 0 0 1 2.23 0c.63.62.63 1.62.01 2.24z" />
</svg>
</button>
</div>
<div class="alert-body"
<div
v-show="alertCopy"
:id="collapseId">
:id="collapseId"
class="alert-body">
<template
v-for="paragraph in splitAlertCopyForParagraphTag"
:key="paragraph">
@ -69,9 +92,7 @@
:text="getRouterLinkDisplayTextFromCopy(copy)"
href="#!"
:data-bs-target="'#' + getRouterLinkRouteFromCopy(copy)"
@click-event="
$emit('textLinkClicked', getRouterLinkRouteFromCopy(copy))
" />
@clickEvent="$emit('textLinkClicked', getRouterLinkRouteFromCopy(copy))" />
</span>
<span
v-else
@ -132,10 +153,11 @@ export default {
},
startCollapsed: Boolean
},
emits: ['textLinkClicked'],
data() {
return {
collapsed: this.startCollapsed || false,
collapseElement: null,
collapseElement: null
};
},
computed: {
@ -159,7 +181,7 @@ export default {
return this.isCollapsible
? `${this.cmsWidgetName}-collapse`
: null;
},
}
},
mounted() {
this.ensureAlertIsInViewPort();
@ -198,7 +220,7 @@ export default {
} else {
this.collapseElement.show();
}
},
}
}
};
</script>
@ -210,13 +232,18 @@ export default {
margin-bottom: 2rem;
.alert-header {
display: flex;
align-items: center;
align-items: flex-start;
justify-content: center;
flex-direction: row;
padding: .75rem 1rem .75rem 1rem;
font-weight: 600;
color: $black;
}
.alert-icon-container {
display: flex;
align-items: center;
height: 1.625rem;
}
.alert-icon {
min-width: 1rem;
max-height: 1rem;
@ -259,7 +286,7 @@ export default {
padding: .5rem 1rem .75rem 1rem;
border-top: 1px solid;
color: $darker-gray;
line-height: 26px;
line-height: 1.625rem;
font-weight: 400;
}
&.alert-info {