Merge
This commit is contained in:
commit
96348c48eb
29 changed files with 2905 additions and 85 deletions
|
|
@ -11,6 +11,22 @@ const endpoints = Object.freeze({
|
|||
url: (applicationAbbreviation, pageName) => `/content/api/v1/content/${applicationAbbreviation}/${pageName}`,
|
||||
method: 'GET'
|
||||
},
|
||||
GetAlertReasons: {
|
||||
url: '/location/api/v1/location/alert-reasons',
|
||||
method: 'GET'
|
||||
},
|
||||
GetShopTimeSlots: {
|
||||
url: '/schedule/api/v1/schedule/shop-time-slots',
|
||||
method: 'POST'
|
||||
},
|
||||
GetMobileTimeSlots: {
|
||||
url: '/schedule/api/v1/schedule/mobile-time-slots',
|
||||
method: 'POST'
|
||||
},
|
||||
GetMobilePremiumFee: {
|
||||
url: '/parts/api/v1/parts/mobile-premium-fee',
|
||||
method: 'GET'
|
||||
},
|
||||
GetVehicleYears: {
|
||||
url: '/vehicle/api/v1/vehicle/years',
|
||||
method: 'GET'
|
||||
|
|
|
|||
|
|
@ -41,6 +41,8 @@ const errorMessages = Object.freeze({
|
|||
LOSS_STATE_REQUIRED: 'Please select an option',
|
||||
LOSS_DATE_REQUIRED:
|
||||
'Please select a date. Format must be MM/DD/YYYY and the date must be not in the future',
|
||||
DAMAGE_DATE_REQUIREMENT:
|
||||
'Damage date must be within the past 10 years',
|
||||
DAMAGE_OPTION_REQUIRED: 'Please select an option',
|
||||
|
||||
POLICYHOLDER_FIRST_NAME_REQUIRED: 'Please enter the policyholder first name',
|
||||
|
|
@ -51,7 +53,8 @@ const errorMessages = Object.freeze({
|
|||
MAKE_REQUIRED: 'Please select your vehicle make',
|
||||
MODEL_REQUIRED: 'Please select your vehicle model',
|
||||
STYLE_REQUIRED: 'Please select your vehicle style',
|
||||
MOBILE_LOCATION_REQUIRED: 'Please enter your service address'
|
||||
MOBILE_LOCATION_REQUIRED: 'Please enter your service address',
|
||||
DATE_REQUIRED: 'Please select a date'
|
||||
});
|
||||
|
||||
export default errorMessages;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
const AppointmentTypeStrings = {
|
||||
IN_SHOP: 'Inshop',
|
||||
MOBILE: 'Mobile',
|
||||
MOBILE_NOT_ITAC: 'Mobile-Not-ITAC',
|
||||
DROP_OFF: 'Dropoff'
|
||||
};
|
||||
const PREMIUM_FEE_PART_TYPE = 'EARLY BIRD';
|
||||
|
|
@ -10,4 +11,14 @@ const RouteCodeFlags = {
|
|||
OVERNIGHT_DROP_OFF: 'OVERNIGHT DROP OFF'
|
||||
};
|
||||
|
||||
export { AppointmentTypeStrings, PREMIUM_TIME_SLOT_ID_FLAG, PREMIUM_FEE_PART_TYPE, RouteCodeFlags };
|
||||
const GET_SHOP_TIME_SLOTS = 'getShopTimeSlots';
|
||||
const GET_MOBILE_TIME_SLOTS = 'getMobileTimeSlots';
|
||||
|
||||
export {
|
||||
AppointmentTypeStrings,
|
||||
PREMIUM_TIME_SLOT_ID_FLAG,
|
||||
PREMIUM_FEE_PART_TYPE,
|
||||
RouteCodeFlags,
|
||||
GET_SHOP_TIME_SLOTS,
|
||||
GET_MOBILE_TIME_SLOTS
|
||||
};
|
||||
|
|
|
|||
1064
src/digital-components/date-picker/date-picker.vue
Normal file
1064
src/digital-components/date-picker/date-picker.vue
Normal file
File diff suppressed because it is too large
Load diff
26
src/digital-components/date-picker/mixins/constants.js
Normal file
26
src/digital-components/date-picker/mixins/constants.js
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
const TIMINGFUNC_MAP = {
|
||||
linear: (t) => t,
|
||||
'ease-in': (t) => t * t,
|
||||
'ease-out': (t) => t * (2 - t),
|
||||
'ease-in-out': (t) => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t)
|
||||
};
|
||||
const BUFFER_OFFSET = 10;
|
||||
|
||||
const MONTHS_OF_YEAR = [
|
||||
'January',
|
||||
'February',
|
||||
'March',
|
||||
'April',
|
||||
'May',
|
||||
'June',
|
||||
'July',
|
||||
'August',
|
||||
'September',
|
||||
'October',
|
||||
'November',
|
||||
'December'
|
||||
];
|
||||
|
||||
const DAYS_OF_WEEK = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
||||
|
||||
export { TIMINGFUNC_MAP, BUFFER_OFFSET, MONTHS_OF_YEAR, DAYS_OF_WEEK };
|
||||
10
src/digital-components/date-picker/mixins/helpers.js
Normal file
10
src/digital-components/date-picker/mixins/helpers.js
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
const selectableDaysOptions = Object.freeze({
|
||||
CUSTOM: 'custom',
|
||||
PAST: 'past'
|
||||
});
|
||||
|
||||
const requiredParameter = () => {
|
||||
throw new Error('parameter is required');
|
||||
};
|
||||
|
||||
export { selectableDaysOptions, requiredParameter };
|
||||
|
|
@ -271,7 +271,9 @@ function getStoreValueFromString(str) {
|
|||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const s of str.split('.')) {
|
||||
if (s === 'getters') continue; // For backward compatibility
|
||||
if (typeof storeOrStateObject[s] !== 'undefined') {
|
||||
// TODO: I don't think this next line is doing what they think it's doing.
|
||||
// eslint-disable-next-line eqeqeq, valid-typeof
|
||||
if (typeof storeOrStateObject[s] != undefined) {
|
||||
storeOrStateObject = storeOrStateObject[s];
|
||||
} else {
|
||||
break;
|
||||
|
|
|
|||
12
src/helpers/date-helper.js
Normal file
12
src/helpers/date-helper.js
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
const getDateDifferenceInDays = (startDate, endDate) => {
|
||||
const date1 = new Date(endDate);
|
||||
date1.setHours(0, 0, 0, 0);
|
||||
const date2 = new Date(startDate);
|
||||
date2.setHours(0, 0, 0, 0);
|
||||
// To calculate the time difference of two dates
|
||||
const DifferenceInTime = date1.getTime() - date2.getTime();
|
||||
// To calculate the no. of days between two dates
|
||||
return DifferenceInTime / (1000 * 3600 * 24);
|
||||
};
|
||||
|
||||
export default getDateDifferenceInDays;
|
||||
|
|
@ -46,15 +46,7 @@ export async function getAvailabilityRating(
|
|||
providerNumber
|
||||
) {
|
||||
// For a given shop provider number and date range, get the appointment time slots available
|
||||
const shopTimeSlots = await useMainStore().getShopTimeSlots(
|
||||
{
|
||||
providerNumber,
|
||||
startDate,
|
||||
endDate,
|
||||
shopAppointmentType
|
||||
},
|
||||
false
|
||||
);
|
||||
const shopTimeSlots = await useMainStore().getShopTimeSlots(startDate, endDate, shopAppointmentType, providerNumber);
|
||||
|
||||
const numberOfDaysToEvaluate = 2;
|
||||
const isGoodAvailability =
|
||||
|
|
|
|||
|
|
@ -162,7 +162,7 @@ export default {
|
|||
border-top: 1px solid $gray-300;
|
||||
overflow-x: visible;
|
||||
overflow-y: visible;
|
||||
z-index: 2;
|
||||
z-index: 3;
|
||||
.modal-body {
|
||||
padding: 2rem;
|
||||
.ccpa-icon {
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
:class="justifySubheader">
|
||||
<p
|
||||
class="fw-normal mb-0"
|
||||
:class="alternateFormatting">
|
||||
:class="[alternateFormatting, subTextClasses]">
|
||||
<span v-html="subText"> </span>
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -49,7 +49,8 @@ export default {
|
|||
issContainingPage: String,
|
||||
justification: String,
|
||||
stripRteStyle: Boolean,
|
||||
subContentProperty: String
|
||||
subContentProperty: String,
|
||||
subTextClasses: String
|
||||
},
|
||||
emits: ['click-event'],
|
||||
computed: {
|
||||
|
|
|
|||
|
|
@ -795,3 +795,26 @@ describe.skip('coverageStatement.vue', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('coverageStatement.vue-working', () => {
|
||||
describe('Navigation', () => {
|
||||
test('Function called on any FORWARD navigation', () => {
|
||||
// Arrange
|
||||
const mainInitialState = {
|
||||
order: {
|
||||
policy: {
|
||||
isITAC: null
|
||||
}
|
||||
}
|
||||
};
|
||||
const { wrapper } = getMountedComponent(mainInitialState);
|
||||
|
||||
// Act
|
||||
wrapper.vm.forwardButtonAction();
|
||||
|
||||
// Assert
|
||||
expect(wrapper.vm.mainStore.updatePolicyITACFlag)
|
||||
.toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -77,6 +77,10 @@
|
|||
isRequired
|
||||
:validationRules="rules.selectionRequired">
|
||||
</buttonQuestion>
|
||||
<text-block
|
||||
v-if="displayQuote"
|
||||
cmsWidgetName="DisclaimerWidget"
|
||||
typeStyle="caption" />
|
||||
</div>
|
||||
<siteFooter
|
||||
ref="siteFooter"
|
||||
|
|
@ -111,6 +115,7 @@ import alert from '@/ux-components/alert/alert.vue';
|
|||
import contentGroupModal from '@/iss-components/content-group-modal/content-group-modal.vue';
|
||||
import buttonQuestion from '@/digital-components/button-question/button-question.vue';
|
||||
import loadingModal from '@/iss-components/loading-modal/loading-modal.vue';
|
||||
import textBlock from '@/digital-components/text-block/text-block.vue';
|
||||
|
||||
// Import Supporting Files
|
||||
import { fetchCmsContentForPage, setupModalLinks, setupModalLink, processIfStatements } from '@/helpers/cms-content-helper.js';
|
||||
|
|
@ -134,14 +139,13 @@ export default {
|
|||
alert,
|
||||
contentGroupModal,
|
||||
buttonQuestion,
|
||||
loadingModal
|
||||
loadingModal,
|
||||
textBlock
|
||||
},
|
||||
mixins: [baseFormMixin, vehicleQuestionsMixin],
|
||||
async beforeRouteEnter(to, from, next) {
|
||||
// Call APIs
|
||||
const cmsContentPromise = fetchCmsContentForPage(to?.query?.issPage);
|
||||
const wipersPromise = await useMainStore().getWipers();
|
||||
const rainDefensePromise = await useMainStore().getRainDefense();
|
||||
const supportingItemsPromise = await useMainStore().getSupportingItems();
|
||||
|
||||
// Settle promises and get results
|
||||
|
|
@ -150,14 +154,6 @@ export default {
|
|||
resultKey: 'cmsContent',
|
||||
promise: cmsContentPromise
|
||||
},
|
||||
{
|
||||
resultKey: 'wipers',
|
||||
promise: wipersPromise
|
||||
},
|
||||
{
|
||||
resultKey: 'rainDefense',
|
||||
promise: rainDefensePromise
|
||||
},
|
||||
{
|
||||
resultKey: 'supportingItems',
|
||||
promise: supportingItemsPromise
|
||||
|
|
@ -169,9 +165,7 @@ export default {
|
|||
? JSON.parse(JSON.stringify(useMainStore().order.lineItems.glassParts))
|
||||
: [];
|
||||
const availableLineItems = [
|
||||
resultMap.rainDefense,
|
||||
...(resultMap.supportingItems ?? []),
|
||||
...(resultMap.wipers ?? []),
|
||||
...(clonedGlassParts ?? [])
|
||||
];
|
||||
|
||||
|
|
@ -335,6 +329,7 @@ export default {
|
|||
nextStepsBody(newValue, oldValue) {
|
||||
if (newValue !== oldValue) {
|
||||
setupModalLink(this, 'RecalModal');
|
||||
setupModalLink(this, 'DeductibleModal');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -357,6 +352,7 @@ export default {
|
|||
return this.navigateForward();
|
||||
},
|
||||
async navigateForward() {
|
||||
useMainStore().updatePolicyITACFlag(this.verifiedITAC);
|
||||
if (this.unverified || this.verifiedDeductible) {
|
||||
this.mainStore.saveSupportingItems(this.supportingItems);
|
||||
this.$router.navigate(
|
||||
|
|
@ -465,7 +461,6 @@ export default {
|
|||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
.cost {
|
||||
color: $green;
|
||||
font-size: 2rem;
|
||||
|
|
@ -512,5 +507,8 @@ export default {
|
|||
margin-top: 0.5rem;
|
||||
}
|
||||
}
|
||||
.modal {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
|
|||
94
src/layouts/schedule-page/helpers/schedule-helper.js
Normal file
94
src/layouts/schedule-page/helpers/schedule-helper.js
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
import { useMainStore } from '@/store';
|
||||
|
||||
const getAlertReasons = async (ctu) => {
|
||||
const store = useMainStore();
|
||||
const alertReasons = await store.getAlertReasonsByCtu(ctu);
|
||||
|
||||
return Promise.resolve(alertReasons);
|
||||
};
|
||||
|
||||
// This function will go away after testing.
|
||||
// The Service already went through testing and had these removed from it.
|
||||
export async function mockGetAlertReasons(ctu) {
|
||||
let retList = [];
|
||||
if (ctu === '01853') { // Ocala, FL 34470
|
||||
retList = [
|
||||
'Hurricane', 'ExtremeTemperature'
|
||||
];
|
||||
} else if (ctu === '01814') { // Phoenix, AZ 85026
|
||||
retList = [
|
||||
'ExtremeTemperature'
|
||||
];
|
||||
} else if (ctu === '01845') { // Raleigh, NC 27601
|
||||
retList = [
|
||||
'Hurricane'
|
||||
];
|
||||
}
|
||||
return Promise.resolve(retList);
|
||||
}
|
||||
|
||||
export function calcDaysBetweenDates(dateString1, dateString2) {
|
||||
const date1 = new Date(dateString1);
|
||||
const date2 = new Date(dateString2);
|
||||
const timeDifference = Math.abs(date2 - date1); // Calculate the time difference in milliseconds
|
||||
return Math.ceil(timeDifference / (1000 * 60 * 60 * 24)); // Convert milliseconds to days
|
||||
}
|
||||
|
||||
export function convertDateToDateString(date) {
|
||||
// returns YYYY-MM-DD format
|
||||
if (date instanceof Date !== true) return null;
|
||||
return (
|
||||
`${date.getFullYear()
|
||||
}-${
|
||||
(`0${date.getMonth() + 1}`).slice(-2)
|
||||
}-${
|
||||
(`0${date.getDate()}`).slice(-2)}`
|
||||
);
|
||||
}
|
||||
|
||||
export function convertDateStringToDate(dateString) {
|
||||
// dateString must be YYYY-MM-DD format
|
||||
if (typeof dateString !== 'string') return null;
|
||||
const dateParts = dateString.split('-');
|
||||
return new Date(dateParts[0], parseInt(dateParts[1], 10) - 1, dateParts[2]);
|
||||
}
|
||||
|
||||
export function sumDateString(dateString, daysToAdd) {
|
||||
// dateString must be YYYY-MM-DD format
|
||||
if (typeof dateString !== 'string') return null;
|
||||
const date = convertDateStringToDate(dateString);
|
||||
date.setDate(date.getDate() + daysToAdd);
|
||||
return convertDateToDateString(date);
|
||||
}
|
||||
|
||||
export function militaryToTwelveHourTime(timeString) {
|
||||
// Expected input: "HH:MM"
|
||||
if (typeof timeString !== 'string') return null;
|
||||
let hours = parseInt(timeString.split(':')[0], 10);
|
||||
const minutes = timeString.split(':')[1];
|
||||
const meridianNotation = hours > 11 ? 'PM' : 'AM';
|
||||
|
||||
if (hours > 12) {
|
||||
hours -= 12;
|
||||
}
|
||||
|
||||
return `${hours}:${minutes} ${meridianNotation}`;
|
||||
}
|
||||
|
||||
export function getDisplayTextForDurationLength(durationMinimum, durationMaximum) {
|
||||
const isLongAppointment = durationMaximum >= 120;
|
||||
const isDurationRange = durationMinimum !== durationMaximum;
|
||||
|
||||
const adjustedMinimum = isLongAppointment ? durationMinimum / 60 : durationMinimum;
|
||||
const adjustedMaximum = isLongAppointment ? durationMaximum / 60 : durationMaximum;
|
||||
|
||||
const durationText = isDurationRange
|
||||
? `${adjustedMinimum} - ${adjustedMaximum}`
|
||||
: adjustedMinimum;
|
||||
|
||||
const unitText = isLongAppointment ? 'hours' : 'minutes';
|
||||
|
||||
return `${durationText} ${unitText}`;
|
||||
}
|
||||
|
||||
export default getAlertReasons;
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
<template>
|
||||
<alert
|
||||
v-for="alert in prefixedAlertReasons"
|
||||
:key="alert.cmsWidgetName"
|
||||
:ref="alert.cmsWidgetName"
|
||||
class="mt-2 mb-3"
|
||||
:cmsWidgetName="alert.cmsWidgetName"
|
||||
alertClass="alert-warning" />
|
||||
</template>
|
||||
<script>
|
||||
import alert from '@/ux-components/alert/alert.vue';
|
||||
import { mockGetAlertReasons } from '@/layouts/schedule-page/helpers/schedule-helper';
|
||||
|
||||
export default {
|
||||
name: 'location-alerts',
|
||||
components: {
|
||||
alert
|
||||
},
|
||||
props: {
|
||||
cmsWidgetPrefix: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
alertReasons: []
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
prefixedAlertReasons() {
|
||||
return this.alertReasons.reduce((newObj, alertReason) => {
|
||||
newObj.push({
|
||||
cmsWidgetName: `${this.cmsWidgetPrefix}${alertReason}`,
|
||||
alertReason
|
||||
});
|
||||
return newObj;
|
||||
}, []);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
loadInitialData(serviceLocationCtu, providerCtu) {
|
||||
let ctuToUse = serviceLocationCtu;
|
||||
if (providerCtu) {
|
||||
ctuToUse = providerCtu;
|
||||
}
|
||||
return mockGetAlertReasons(ctuToUse); // Change to getAlertReasons after testing.
|
||||
},
|
||||
initializeComponent(initialData) {
|
||||
this.alertReasons = initialData;
|
||||
},
|
||||
cmsHeadlineTextFound(widgetName) {
|
||||
return this.getCmsContent(widgetName, 'HeadlineText') !== '';
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
184
src/layouts/schedule-page/schedule-page.spec.js
Normal file
184
src/layouts/schedule-page/schedule-page.spec.js
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
// Components
|
||||
import schedule from '@/layouts/schedule-page/schedule-page.vue';
|
||||
|
||||
// Supporting Files
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import { getMountOptions } from '@/helpers/unit-test-helper.js';
|
||||
import { useMainStore } from '@/store/index.js';
|
||||
|
||||
const mockMixin = {
|
||||
methods: {
|
||||
getCmsContent: jest.fn().mockImplementation(() => ''),
|
||||
setCmsContent: jest.fn()
|
||||
}
|
||||
};
|
||||
|
||||
const footerStub = {
|
||||
render: () => {},
|
||||
methods: {
|
||||
updateButtonText: jest.fn()
|
||||
}
|
||||
};
|
||||
|
||||
const loadingModalStub = {
|
||||
render: () => {},
|
||||
methods: {
|
||||
showModal: jest.fn(),
|
||||
hideModal: jest.fn()
|
||||
}
|
||||
};
|
||||
|
||||
function getShallowMountedComponent(initialData = {}, methodToRun = () => {}) {
|
||||
const mountOptions = getMountOptions({
|
||||
router: {
|
||||
navigate: jest.fn()
|
||||
}
|
||||
});
|
||||
|
||||
// mountOptions.global.mocks["$store"] = store;
|
||||
|
||||
mountOptions.global.stubs = {
|
||||
siteFooter: footerStub,
|
||||
siteHeader: true,
|
||||
recalModal: true,
|
||||
contentGroupModal: true,
|
||||
alert: true,
|
||||
loadingModal: loadingModalStub
|
||||
};
|
||||
|
||||
methodToRun();
|
||||
|
||||
mountOptions.mixins = [mockMixin];
|
||||
mountOptions.data = () => (
|
||||
initialData
|
||||
);
|
||||
|
||||
const wrapper = shallowMount(schedule, mountOptions);
|
||||
return { wrapper };
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
const testingPinia = createTestingPinia({
|
||||
initialState: {
|
||||
main: {
|
||||
order: {
|
||||
schedule: {
|
||||
date: '2019-01-01',
|
||||
startTime: '09:00',
|
||||
endTime: '10:00',
|
||||
routeCode: '000'
|
||||
},
|
||||
lineItems: {
|
||||
glassParts: [
|
||||
{
|
||||
partNumber: 'ABC123'
|
||||
}
|
||||
],
|
||||
supportingItems: []
|
||||
},
|
||||
serviceLocation: {
|
||||
appointmentType: 'Inshop',
|
||||
zipCode: '12345',
|
||||
zipCodeCtu: '01234',
|
||||
provider: {
|
||||
providerNumber: '123'
|
||||
}
|
||||
},
|
||||
damage: {
|
||||
isRepair: false
|
||||
},
|
||||
referralNumber: '1234567'
|
||||
},
|
||||
payment: {
|
||||
isInsurance: true
|
||||
},
|
||||
lineItems: {
|
||||
glassParts: [],
|
||||
supportingItems: []
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
useMainStore(testingPinia);
|
||||
});
|
||||
|
||||
describe('schedule-page.vue', () => {
|
||||
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', () => {
|
||||
// Arrange
|
||||
const { wrapper } = getShallowMountedComponent();
|
||||
|
||||
// Act
|
||||
const arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid();
|
||||
|
||||
// Assert
|
||||
expect(arePagePrerequisitesValid).toBe(true);
|
||||
});
|
||||
test('Should fail arePagePrerequisitesValid with an inshop order and no providerNumber', () => {
|
||||
// Arrange
|
||||
const { wrapper } = getShallowMountedComponent();
|
||||
wrapper.vm.mainStore.order.serviceLocation.provider.providerNumber = null;
|
||||
|
||||
// Act
|
||||
const arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid();
|
||||
|
||||
// Assert
|
||||
expect(arePagePrerequisitesValid).toBeFalsy();
|
||||
});
|
||||
test('Should fail arePagePrerequisitesValid without isInsurance', () => {
|
||||
// Arrange
|
||||
const { wrapper } = getShallowMountedComponent();
|
||||
wrapper.vm.mainStore.payment.isInsurance = null;
|
||||
|
||||
// Act
|
||||
const arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid();
|
||||
|
||||
// Assert
|
||||
expect(arePagePrerequisitesValid).toBe(false);
|
||||
});
|
||||
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);
|
||||
});
|
||||
});
|
||||
describe('Rendering', () => {
|
||||
test('Schedule page loads', () => {
|
||||
// Arrange
|
||||
const { wrapper } = getShallowMountedComponent();
|
||||
|
||||
// Assert
|
||||
expect(wrapper).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -5,18 +5,56 @@
|
|||
@submit="onSubmit"
|
||||
@invalidSubmit="onInvalidSubmit">
|
||||
<div class="page-container-grouped-styles">
|
||||
<div class="fade-on-route-transition position-relative">
|
||||
<siteHeader cmsWidgetName="SiteHeaderWidget" />
|
||||
<div class="container-fluid pb-2">
|
||||
<p>Placeholder for schedule page</p>
|
||||
<siteFooter
|
||||
ref="siteFooter"
|
||||
class="mt-5"
|
||||
cmsWidgetName="SiteFooterWidget"
|
||||
:isForwardActionDisabled="!meta.valid"
|
||||
@ForwardClicked="forwardButtonAction"
|
||||
@backClicked="backButtonAction" />
|
||||
</div>
|
||||
<siteHeader cmsWidgetName="SiteHeaderWidget" />
|
||||
<siteSubHeader
|
||||
cmsWidgetName="ScheduleSubHeaderWidget"
|
||||
subTextClasses="text-center small sub-text"
|
||||
class="mt-5" />
|
||||
<template v-if="ChangeShopLink.length">
|
||||
<textBlock
|
||||
cmsWidgetName="ChangeShopLink"
|
||||
justifyText="center"
|
||||
class="mb-3 text-link-small"
|
||||
:marginTopSizeOverride="1" />
|
||||
</template>
|
||||
<div class="main-content-container">
|
||||
<locationAlerts
|
||||
ref="locationAlerts"
|
||||
cmsWidgetPrefix="LocationAlert-" />
|
||||
<datePicker
|
||||
ref="datePicker"
|
||||
v-model="selectedDate"
|
||||
customComponentId="dateQuestion"
|
||||
selectableDatesSetting="custom"
|
||||
class="text-link-small"
|
||||
:customSelectableDatesCallback="getAvailableDatesMethod"
|
||||
validationRules="date-required"
|
||||
@dateClicked="openInshopTimeSlotsModal" />
|
||||
<timeSlotModalQuestion
|
||||
ref="timeSlotModalQuestion"
|
||||
v-model="selectedTimeSlotInfo"
|
||||
customComponentId="timeSlotModalQuestion"
|
||||
cmsWidgetName="TimeSlotModalQuestion"
|
||||
mobilePremiumCmsWidgetName="MobilePremiumTimeSlotModal"
|
||||
mobileCmsWidgetName="MobileTimeSlotModal"
|
||||
dropoffCmsWidgetName="DropOffTimeSlotModal"
|
||||
sameDayDropOffCmsWidgetName="SameDayDropOffTimeSlotModal"
|
||||
overnightDropOffCmsWidgetName="OvernightDropOffTimeSlotModal"
|
||||
:selectedDate="selectedDate"
|
||||
:appointmentType="appointmentType"
|
||||
:premiumAppointmentFee="mobilePremiumAppointmentFee"
|
||||
:timeSlotsForSelectedDate="timeSlotsForSelectedDate"
|
||||
:estimatedServiceMinutesMinimum="selectableDatesData.estimatedServiceMinutesMinimum"
|
||||
:estimatedServiceMinutesMaximum="selectableDatesData.estimatedServiceMinutesMaximum"
|
||||
validationRules="time-slot-selection-required"
|
||||
@timeSlotModalClosed="timeSlotModalClosed" />
|
||||
<siteFooter
|
||||
ref="navbar"
|
||||
class="mt-5"
|
||||
cmsWidgetName="SiteFooterWidget"
|
||||
:isForwardActionDisabled="!meta.valid"
|
||||
@backClicked="backButtonAction"
|
||||
@forwardClicked="forwardButtonAction" />
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
|
|
@ -24,41 +62,209 @@
|
|||
<script>
|
||||
// Components
|
||||
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 timeSlotModalQuestion from '@/layouts/schedule-page/time-slot-modal-question/time-slot-modal-question.vue';
|
||||
import siteFooter from '@/iss-components/site-footer/site-footer.vue';
|
||||
import textBlock from '@/digital-components/text-block/text-block.vue';
|
||||
|
||||
// Supporting files
|
||||
import { AppointmentTypeStrings } from '@/constants/schedule-constants.js';
|
||||
import { fetchCmsContentForPage } from '@/helpers/cms-content-helper';
|
||||
import {
|
||||
calcDaysBetweenDates,
|
||||
convertDateStringToDate,
|
||||
sumDateString
|
||||
} from '@/layouts/schedule-page/helpers/schedule-helper';
|
||||
import {
|
||||
AppointmentTypeStrings,
|
||||
GET_MOBILE_TIME_SLOTS,
|
||||
GET_SHOP_TIME_SLOTS,
|
||||
PREMIUM_FEE_PART_TYPE
|
||||
} from '@/constants/schedule-constants.js';
|
||||
import { fetchCmsContentForPage, splitCopyOnCMSPlaceHolder } from '@/helpers/cms-content-helper';
|
||||
import settleAllPromises from '@/helpers/layout-helper';
|
||||
import { Form } from 'vee-validate';
|
||||
import { Form, defineRule } from 'vee-validate';
|
||||
import BaseFormMixin from '@/mixins/base-form-mixin.js';
|
||||
import errorMessages from '@/constants/error-messages';
|
||||
import { required } from '@/helpers/validation-rules';
|
||||
import { useMainStore } from '@/store';
|
||||
|
||||
// DEFINE VALIDATION RULES
|
||||
defineRule('date-required', required(errorMessages.DATE_REQUIRED));
|
||||
defineRule('time-slot-selection-required', (value) => {
|
||||
if (value?.timeSlot?.routeCode == null) {
|
||||
return errorMessages.DATE_REQUIRED;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
// Define constants
|
||||
const TIME_SLOTS_CALL_DAYS_LIMIT = 34; // needs to be 34 for API limits (35 does not consistently work)
|
||||
|
||||
const getAvailableDates = async (
|
||||
startDateString,
|
||||
endDateString,
|
||||
appointmentType,
|
||||
providerNumber
|
||||
) => {
|
||||
const apiEndDateLimit = sumDateString(startDateString, TIME_SLOTS_CALL_DAYS_LIMIT);
|
||||
const difference = calcDaysBetweenDates(startDateString, endDateString);
|
||||
const apiCallsCount = Math.ceil(difference / TIME_SLOTS_CALL_DAYS_LIMIT);
|
||||
const storeActionConfigs = [];
|
||||
const timeSlotsData = {};
|
||||
timeSlotsData.days = [];
|
||||
let apiStartDate = startDateString;
|
||||
let apiEndDate = endDateString;
|
||||
|
||||
for (let i = 1; i <= apiCallsCount; i++) {
|
||||
let storeActionConfig;
|
||||
|
||||
if (i > 1) {
|
||||
apiStartDate = sumDateString(apiEndDate, 1);
|
||||
apiEndDate = sumDateString(apiStartDate, TIME_SLOTS_CALL_DAYS_LIMIT);
|
||||
|
||||
if (i === apiCallsCount) {
|
||||
apiEndDate = endDateString;
|
||||
}
|
||||
} else if (apiEndDate > apiEndDateLimit) {
|
||||
apiEndDate = apiEndDateLimit;
|
||||
}
|
||||
|
||||
if (appointmentType === AppointmentTypeStrings.MOBILE
|
||||
|| appointmentType === AppointmentTypeStrings.MOBILE_NOT_ITAC) {
|
||||
storeActionConfig = {
|
||||
storeAction: GET_MOBILE_TIME_SLOTS,
|
||||
payload: {
|
||||
startDate: apiStartDate,
|
||||
endDate: apiEndDate
|
||||
}
|
||||
};
|
||||
} else {
|
||||
storeActionConfig = {
|
||||
storeAction: GET_SHOP_TIME_SLOTS,
|
||||
payload: {
|
||||
startDate: apiStartDate,
|
||||
endDate: apiEndDate,
|
||||
shopAppointmentType: appointmentType,
|
||||
providerNumber
|
||||
}
|
||||
};
|
||||
}
|
||||
if (apiStartDate < apiEndDate) storeActionConfigs.push(storeActionConfig);
|
||||
}
|
||||
|
||||
const timeSlotsResponsesData = {
|
||||
days: []
|
||||
};
|
||||
|
||||
function compareDayStrings(a, b) {
|
||||
if (a.date < b.date) return -1;
|
||||
if (a.date > b.date) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
const makeParallelCalls = async () => {
|
||||
await Promise.all(storeActionConfigs.map(async (storeAction) => {
|
||||
let timeSlotsResponse = null;
|
||||
if (storeAction.storeAction === GET_SHOP_TIME_SLOTS) {
|
||||
timeSlotsResponse = await useMainStore().getShopTimeSlots(
|
||||
storeAction.payload.startDate,
|
||||
storeAction.payload.endDate,
|
||||
storeAction.payload.shopAppointmentType,
|
||||
storeAction.payload.providerNumber
|
||||
);
|
||||
} else {
|
||||
timeSlotsResponse = await useMainStore().getMobileTimeSlots(storeAction.payload.startDate, storeAction.payload.endDate);
|
||||
}
|
||||
|
||||
timeSlotsResponsesData.estimatedServiceMinutesMinimum = timeSlotsResponse.data.estimatedServiceMinutesMinimum;
|
||||
timeSlotsResponsesData.estimatedServiceMinutesMaximum = timeSlotsResponse.data.estimatedServiceMinutesMaximum;
|
||||
timeSlotsResponsesData.days = [
|
||||
...timeSlotsResponsesData.days,
|
||||
...timeSlotsResponse.data.days
|
||||
];
|
||||
}));
|
||||
};
|
||||
|
||||
return makeParallelCalls().then(() => {
|
||||
// sort days chronologically
|
||||
timeSlotsResponsesData.days.sort(compareDayStrings);
|
||||
return timeSlotsResponsesData;
|
||||
});
|
||||
};
|
||||
|
||||
export default {
|
||||
name: 'schedule-page',
|
||||
components: {
|
||||
siteHeader,
|
||||
siteSubHeader,
|
||||
locationAlerts,
|
||||
datePicker,
|
||||
timeSlotModalQuestion,
|
||||
siteFooter,
|
||||
textBlock,
|
||||
// eslint-disable-next-line vue/no-reserved-component-names
|
||||
Form
|
||||
},
|
||||
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 premiumFeePromise = useMainStore().getMobilePremiumFee();
|
||||
|
||||
const premiumFeeWithPricePromise = premiumFeePromise.then((result) => {
|
||||
if (result.data) {
|
||||
return useMainStore().priceOrderItemsAndSaveServerData(result.data);
|
||||
}
|
||||
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 = [
|
||||
{
|
||||
resultKey: 'cmsContent',
|
||||
promise: cmsContentPromise
|
||||
}];
|
||||
},
|
||||
{
|
||||
resultKey: 'alertReasons',
|
||||
promise: alertReasonsPromise
|
||||
},
|
||||
{
|
||||
resultKey: 'datePickerInitialData',
|
||||
promise: datePickerInitialDataPromise
|
||||
},
|
||||
{
|
||||
resultKey: 'premiumFeeWithPrice',
|
||||
promise: premiumFeeWithPricePromise
|
||||
}
|
||||
];
|
||||
|
||||
// use resultMap to populate layout content.
|
||||
const resultMap = await settleAllPromises(promiseResultMap);
|
||||
|
||||
next((vm) => {
|
||||
vm.setCmsContent(resultMap.cmsContent);
|
||||
vm.$refs.datePicker.initializeComponent(resultMap.datePickerInitialData);
|
||||
vm.$refs.locationAlerts.initializeComponent(resultMap.alertReasons);
|
||||
vm.setData(resultMap.datePickerInitialData.initialShopTimeSlotsResponse, resultMap.premiumFeeWithPrice);
|
||||
vm.updateFooterButtonText(vm.selectedTimeSlotInfo);
|
||||
});
|
||||
},
|
||||
setup() {
|
||||
|
|
@ -66,15 +272,61 @@ export default {
|
|||
return { mainStore };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedDate: this.getSelectedDate(),
|
||||
selectedTimeSlotInfo: this.getSelectedTimeSlotInfo(),
|
||||
selectableDatesData: [],
|
||||
mobilePremiumAppointmentFee: null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
ChangeShopLinkText() {
|
||||
return this.getCmsContent('ChangeShopLink', 'Text');
|
||||
},
|
||||
ChangeShopLink() {
|
||||
return this.splitCopyOnCMSPlaceHolder(this.ChangeShopLinkText);
|
||||
},
|
||||
appointmentType() {
|
||||
return useMainStore().order.serviceLocation.appointmentType;
|
||||
},
|
||||
timeSlotsForSelectedDate() {
|
||||
if (!this.selectedDate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.selectableDatesData.days?.find((selectableDate) => selectableDate.date === this.selectedDate);
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
selectedDate(newValue, oldValue) {
|
||||
// Clear time slot selection if date selected changes
|
||||
if (newValue !== oldValue) {
|
||||
this.selectedTimeSlotInfo = {
|
||||
timeSlot: {
|
||||
date: null,
|
||||
routeCode: null,
|
||||
startTime: null,
|
||||
endTime: null,
|
||||
jobMaxMinutes: null,
|
||||
jobMinMinutes: null
|
||||
},
|
||||
isPremiumAppointment: null
|
||||
};
|
||||
}
|
||||
},
|
||||
selectedTimeSlotInfo(newValue) {
|
||||
this.updateFooterButtonText(newValue);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
splitCopyOnCMSPlaceHolder,
|
||||
arePagePrerequisitesValid() {
|
||||
const { serviceLocation } = useMainStore().order;
|
||||
const serviceLocationPreReqs = serviceLocation.zipCode
|
||||
&& serviceLocation.zipCodeCtu
|
||||
&& serviceLocation.appointmentType
|
||||
&& (serviceLocation.appointmentType === AppointmentTypeStrings.MOBILE
|
||||
&& ((serviceLocation.appointmentType === AppointmentTypeStrings.MOBILE
|
||||
|| serviceLocation.appointmentType === AppointmentTypeStrings.MOBILE_NOT_ITAC)
|
||||
|| serviceLocation.provider.providerNumber);
|
||||
const paymentInfo = useMainStore().payment.isInsurance !== null;
|
||||
const supportingItems = useMainStore().lineItems.supportingItems !== null;
|
||||
|
|
@ -82,19 +334,167 @@ export default {
|
|||
useMainStore().order.damage.isRepair
|
||||
|| (useMainStore().order.lineItems?.glassParts != null
|
||||
&& useMainStore().order.lineItems.glassParts.length > 0);
|
||||
|
||||
return serviceLocationPreReqs && paymentInfo && supportingItems && damageInfo;
|
||||
},
|
||||
setData(initialShopTimeSlotsResponse, premiumFeeWithPriceResponse) {
|
||||
this.selectableDatesData = initialShopTimeSlotsResponse;
|
||||
this.mobilePremiumAppointmentFee = premiumFeeWithPriceResponse
|
||||
? premiumFeeWithPriceResponse[0]
|
||||
: null;
|
||||
},
|
||||
async getAvailableDatesMethod(startDate, endDate) {
|
||||
const newShopTimeSlots = await getAvailableDates(
|
||||
startDate,
|
||||
endDate,
|
||||
this.appointmentType,
|
||||
this.mainStore.order.serviceLocation.provider.providerNumber
|
||||
);
|
||||
// ADD API CALL RESULTS TO EXISTING DATE DATA
|
||||
this.selectableDatesData.days = this.selectableDatesData.days.concat(newShopTimeSlots.days);
|
||||
return newShopTimeSlots;
|
||||
},
|
||||
getAvailableDates,
|
||||
getServiceZipCtuCodeFromStore() {
|
||||
return this.mainStore.order.serviceLocation.zipCodeCtu;
|
||||
},
|
||||
openInshopTimeSlotsModal() {
|
||||
this.$refs.timeSlotModalQuestion.openModal();
|
||||
},
|
||||
getSelectedDate() {
|
||||
return this.mainStore.order.schedule.date;
|
||||
},
|
||||
getSelectedTimeSlotInfo() {
|
||||
const supportingItems = this.getSupportingItems();
|
||||
const isPremiumAppointment =
|
||||
!!supportingItems.filter((lineItem) => lineItem.partType === PREMIUM_FEE_PART_TYPE)
|
||||
.length > 0;
|
||||
|
||||
const selectedTimeSlotInfo = {
|
||||
timeSlot: this.mainStore.order.schedule,
|
||||
isPremiumAppointment
|
||||
};
|
||||
|
||||
return selectedTimeSlotInfo;
|
||||
},
|
||||
getSupportingItems() {
|
||||
return this.mainStore.lineItems.supportingItems;
|
||||
},
|
||||
timeSlotModalClosed() {
|
||||
// Clear the selectedDate if no timeSlot has been selected
|
||||
if (this.selectedTimeSlotInfo.timeSlot.routeCode == null) {
|
||||
this.selectedDate = null;
|
||||
}
|
||||
},
|
||||
updateFooterButtonText(timeSlotInfo) {
|
||||
let navbarButtonText;
|
||||
if (!timeSlotInfo || !timeSlotInfo.timeSlot.date) {
|
||||
navbarButtonText = 'Continue';
|
||||
} else {
|
||||
navbarButtonText = `Select ${this.convertSelectedDateToShortMonthAndDay(timeSlotInfo.timeSlot.date)}`;
|
||||
if (this.appointmentType === AppointmentTypeStrings.IN_SHOP) {
|
||||
navbarButtonText += ` at ${this.getDisplayTextForMilitaryTime(timeSlotInfo.timeSlot.startTime)}`;
|
||||
} else if (
|
||||
this.appointmentType === AppointmentTypeStrings.MOBILE
|
||||
&& !timeSlotInfo.isPremiumAppointment
|
||||
) {
|
||||
navbarButtonText += ` at ${this.getDisplayTextForMilitaryTime(
|
||||
timeSlotInfo.timeSlot.startTime,
|
||||
true
|
||||
)} - ${this.getDisplayTextForMilitaryTime(
|
||||
timeSlotInfo.timeSlot.endTime,
|
||||
true
|
||||
)}`;
|
||||
}
|
||||
}
|
||||
this.$refs.navbar.updateButtonText(navbarButtonText);
|
||||
},
|
||||
convertSelectedDateToShortMonthAndDay(selectedDate) {
|
||||
// This conversion ensures we don't get get GMT induced date changes
|
||||
const dateObject = convertDateStringToDate(selectedDate);
|
||||
return dateObject.toLocaleDateString('en-us', { month: 'short', day: 'numeric' });
|
||||
},
|
||||
getDisplayTextForMilitaryTime(militaryTimeInput, shouldTrimMinutesIfEmpty = false) {
|
||||
// Expected input: "HH:MM"
|
||||
let hours = parseInt(militaryTimeInput.split(':')[0], 10);
|
||||
const minutes = militaryTimeInput.split(':')[1];
|
||||
const meridianNotation = hours > 11 ? 'PM' : 'AM';
|
||||
|
||||
if (hours > 12) {
|
||||
hours -= 12;
|
||||
}
|
||||
|
||||
if (shouldTrimMinutesIfEmpty && minutes === '00') {
|
||||
return `${hours} ${meridianNotation}`;
|
||||
}
|
||||
return `${hours}:${minutes} ${meridianNotation}`;
|
||||
},
|
||||
updateSupportingItems() {
|
||||
const supportingItems = this.getSupportingItems();
|
||||
|
||||
// if we have a premium fee(early bird), then save/update supporting items
|
||||
if (
|
||||
this.appointmentType === AppointmentTypeStrings.MOBILE
|
||||
&& this.selectedTimeSlotInfo?.isPremiumAppointment
|
||||
) {
|
||||
const earlyBirdIndex = supportingItems.findIndex((item) => item.partType === PREMIUM_FEE_PART_TYPE);
|
||||
|
||||
if (earlyBirdIndex >= 0) {
|
||||
supportingItems[earlyBirdIndex].laborAmount =
|
||||
this.mobilePremiumAppointmentFee.laborAmount;
|
||||
supportingItems[earlyBirdIndex].selingPrice =
|
||||
this.mobilePremiumAppointmentFee.selingPrice;
|
||||
supportingItems[earlyBirdIndex].kitPrice =
|
||||
this.mobilePremiumAppointmentFee.kitPrice;
|
||||
} else {
|
||||
supportingItems.push(this.mobilePremiumAppointmentFee);
|
||||
}
|
||||
|
||||
this.mainStore.saveSupportingItemsSuppressingStateResetting(supportingItems);
|
||||
} else {
|
||||
// if it's not a mobile and/or premium early bird, then make sure we remove any that may have been added
|
||||
const removeEarlyBirdIndex = supportingItems.findIndex((item) => item.partType === PREMIUM_FEE_PART_TYPE);
|
||||
|
||||
if (removeEarlyBirdIndex >= 0) {
|
||||
supportingItems.splice(removeEarlyBirdIndex, 1);
|
||||
this.mainStore.saveSupportingItemsSuppressingStateResetting(supportingItems);
|
||||
}
|
||||
}
|
||||
},
|
||||
backButtonAction() {
|
||||
this.$router.navigate(this.navigationScenarios.CLICKED_BACK, this.$route);
|
||||
},
|
||||
|
||||
forwardButtonAction() {
|
||||
// validate and save here
|
||||
this.$router.navigate(this.navigationScenarios.CLICKED_FORWARD, this.$route);
|
||||
},
|
||||
this.updateSupportingItems();
|
||||
this.mainStore.saveSchedule(this.selectedTimeSlotInfo.timeSlot);
|
||||
|
||||
navigateForward() {
|
||||
this.$router.navigate(this.navigationScenarios.CLICKED_FORWARD, this.$route);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$page-side-padding: 1.5rem;
|
||||
|
||||
.page-container-grouped-styles {
|
||||
overflow: auto;
|
||||
|
||||
.main-content-container {
|
||||
padding: 0 1.5rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.text-link-small) {
|
||||
a, .btn-link {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.subheader-primary) {
|
||||
h5.dark-header {
|
||||
margin-bottom: 0.25rem !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,118 @@
|
|||
<template>
|
||||
<baseInputButton
|
||||
v-bind="$props"
|
||||
v-model="selectedValue"
|
||||
buttonWrapperClasses="list-group base-input-button list-button rounded-3 d-flex flex-column w-100 mb-2">
|
||||
<div
|
||||
:aria-label="buttonLabel"
|
||||
class="button-content list-button-content d-flex flex-column justify-content-center py-3 px-4">
|
||||
<span
|
||||
class="m-0 position-relative"
|
||||
:class="textPosition">
|
||||
{{ buttonLabel }}
|
||||
<span
|
||||
v-if="buttonLabelSubCopy"
|
||||
class="premium-appointment-price"
|
||||
:class="textPosition">
|
||||
{{ formattedButtonLabelSubCopy }}
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
v-if="screenReaderOnlyText"
|
||||
class="sr-only">
|
||||
{{ screenReaderOnlyText }}
|
||||
</span>
|
||||
</div>
|
||||
</baseInputButton>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import baseInputButton from '@/digital-components/base-input-button/base-input-button.vue';
|
||||
import inputButtonWrapperMixin from '@/mixins/input-button-wrapper-mixin';
|
||||
|
||||
export default {
|
||||
name: 'time-slot-modal-list-button',
|
||||
components: {
|
||||
baseInputButton
|
||||
},
|
||||
mixins: [inputButtonWrapperMixin],
|
||||
computed: {
|
||||
formattedButtonLabelSubCopy() {
|
||||
return this.buttonLabelSubCopy;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
displayLoader() {
|
||||
this.isLoaderDisplayed = true;
|
||||
},
|
||||
preHandleAnswerChange() {
|
||||
if (this.selectingInitiatesLoad) {
|
||||
this.displayLoader();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.list-button {
|
||||
outline: none;
|
||||
input[type="radio"],
|
||||
input[type="checkbox"] {
|
||||
position: static; //override bootstrap
|
||||
|
||||
&:focus-visible + .list-button-content {
|
||||
box-shadow: 0 0 0 2.5px $blue;
|
||||
}
|
||||
&:focus + .list-button-content {
|
||||
box-shadow: 0 0 0 2.5px $blue;
|
||||
}
|
||||
&:checked + .list-button-content {
|
||||
color: $black;
|
||||
font-weight: 500;
|
||||
background: $blue-100;
|
||||
box-shadow: 0 0 0 1px $blue;
|
||||
span.premium-appointment-price {
|
||||
background: $green-200;
|
||||
}
|
||||
}
|
||||
&:checked:focus + .list-button-content {
|
||||
box-shadow: 0 0 0 2.5px $blue;
|
||||
}
|
||||
&:checked + .list-button-content p,
|
||||
&:checked + .list-button-content span {
|
||||
font-weight: 500;
|
||||
}
|
||||
&:checked + .list-button-content span:nth-child(2) {
|
||||
font-weight: 400;
|
||||
color: $gray-600;
|
||||
}
|
||||
}
|
||||
}
|
||||
.list-button-content {
|
||||
color: $gray-600;
|
||||
position: relative;
|
||||
background: $white;
|
||||
transition: all 150ms linear;
|
||||
border-radius: $border-radius-lg;
|
||||
border: 1px solid $gray-500;
|
||||
width: 100%;
|
||||
outline: none;
|
||||
|
||||
span.premium-appointment-price {
|
||||
position: absolute;
|
||||
background: $green-100;
|
||||
border-radius: 4.5rem;
|
||||
line-height: 1.25rem;
|
||||
color: $green-700;
|
||||
font-size: 0.75rem;
|
||||
margin-left: 4px;
|
||||
padding: 2px 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.position-relative {
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,525 @@
|
|||
<template>
|
||||
<modal
|
||||
:ref="modalName"
|
||||
:headerText="dateSelectedReadableDate"
|
||||
:footerButtonText="footerCloseButtonText"
|
||||
:onModalClosedCallback="onModalClosed"
|
||||
class="time-slots-modal"
|
||||
@isModalOpened="setModalStatus"
|
||||
@footer-button-event="setSelectedTimeSlot">
|
||||
<template v-if="isModalOpened">
|
||||
<textBlock
|
||||
v-show="durationTextBlockCopy"
|
||||
:customText="durationTextBlockCopy"
|
||||
justifyText="center"
|
||||
typeStyle="small"
|
||||
class="duration-text-block" />
|
||||
<buttonQuestion
|
||||
ref="buttonQuestion"
|
||||
v-model="selectedRouteCode"
|
||||
buttonTypeString="timeSlotModalListButton"
|
||||
:buttonTypeObject="timeSlotModalListButton"
|
||||
class="mt-5"
|
||||
:answers="availableTimeSlots"
|
||||
groupName="chooseTimeSlot"
|
||||
textPosition="text-center"
|
||||
isRequired
|
||||
validationRules="time-slot-required" />
|
||||
<div
|
||||
v-if="supplementalInformationBlock"
|
||||
class="mt-1 mb-2 supplemental-information"
|
||||
v-html="supplementalInformationBlock"></div>
|
||||
<textBlock
|
||||
v-show="disclaimerTextBlockCopy"
|
||||
:customText="disclaimerTextBlockCopy"
|
||||
justifyText="left"
|
||||
typeStyle="caption"
|
||||
class="mb-2" />
|
||||
</template>
|
||||
</modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// Components
|
||||
import modal from '@/digital-components/modal/modal.vue';
|
||||
import textBlock from '@/digital-components/text-block/text-block.vue';
|
||||
import buttonQuestion from '@/digital-components/button-question/button-question.vue';
|
||||
|
||||
// Helpers
|
||||
import { deepClone } from '@/helpers/object-helper';
|
||||
|
||||
// Validation
|
||||
import { defineRule, useField } from 'vee-validate';
|
||||
import errorMessages from '@/constants/error-messages';
|
||||
import { required } from '@/helpers/validation-rules';
|
||||
|
||||
// Constants
|
||||
import {
|
||||
AppointmentTypeStrings,
|
||||
RouteCodeFlags,
|
||||
PREMIUM_TIME_SLOT_ID_FLAG,
|
||||
PREMIUM_FEE_PART_TYPE
|
||||
} from '@/constants/schedule-constants';
|
||||
import {
|
||||
convertDateStringToDate,
|
||||
militaryToTwelveHourTime,
|
||||
getDisplayTextForDurationLength
|
||||
} from '@/layouts/schedule-page/helpers/schedule-helper';
|
||||
import timeSlotModalListButton from './time-slot-modal-list-button/time-slot-modal-list-button.vue';
|
||||
|
||||
const cmsWidgetFieldMappings = {
|
||||
MODAL_CLOSE_BUTTON: 'FooterText',
|
||||
SUPPLEMENTAL_INFORMATION: 'BodyText',
|
||||
TIME_SLOT_BUTTON: 'HeaderText',
|
||||
DISCLAIMER: 'FooterText',
|
||||
DURATION: 'SubheaderText'
|
||||
};
|
||||
|
||||
// Validation for the modal button
|
||||
defineRule('time-slot-required', required(errorMessages.OPTION_REQUIRED));
|
||||
|
||||
export default {
|
||||
name: 'time-slot-modal-question',
|
||||
components: {
|
||||
modal,
|
||||
textBlock,
|
||||
buttonQuestion
|
||||
},
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
timeSlot: {
|
||||
routeCode: null,
|
||||
date: null,
|
||||
startTime: null,
|
||||
endTime: null,
|
||||
jobMaxMinutes: null,
|
||||
jobMinMinutes: null
|
||||
},
|
||||
isPremiumAppointment: null
|
||||
})
|
||||
},
|
||||
cmsWidgetName: String,
|
||||
mobileCmsWidgetName: String,
|
||||
mobilePremiumCmsWidgetName: String,
|
||||
dropoffCmsWidgetName: String,
|
||||
sameDayDropOffCmsWidgetName: String,
|
||||
overnightDropOffCmsWidgetName: String,
|
||||
appointmentType: String,
|
||||
timeSlotsForSelectedDate: Object,
|
||||
premiumAppointmentFee: Object,
|
||||
estimatedServiceMinutesMinimum: Number,
|
||||
estimatedServiceMinutesMaximum: Number,
|
||||
validationRules: String,
|
||||
customComponentId: String,
|
||||
selectedDate: String
|
||||
},
|
||||
emits: ['update:modelValue', 'time-slot-modal-closed'],
|
||||
setup(props) {
|
||||
const uuid = crypto.randomUUID();
|
||||
const componentId = !props.customComponentId
|
||||
? `component-${uuid}`
|
||||
: props.customComponentId;
|
||||
|
||||
const { modelValue } = deepClone(props);
|
||||
const initialValue = modelValue;
|
||||
|
||||
const fieldOptions = {
|
||||
value: modelValue,
|
||||
initialValue
|
||||
};
|
||||
|
||||
const { errorMessage, handleChange, meta, validate, errors } = useField(
|
||||
componentId,
|
||||
props.validationRules,
|
||||
fieldOptions
|
||||
);
|
||||
|
||||
return {
|
||||
componentId,
|
||||
errorMessage,
|
||||
handleChange,
|
||||
validate,
|
||||
meta,
|
||||
errors
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isModalOpened: false,
|
||||
selectedRouteCode: this.getSelectedRouteCode(),
|
||||
timeSlotModalListButton
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
modalName() {
|
||||
return 'timeSlots';
|
||||
},
|
||||
modal() {
|
||||
return this.$refs[this.modalName];
|
||||
},
|
||||
supplementalInformationBlock() {
|
||||
let appointmentTypeCmsWidgetName;
|
||||
if (this.appointmentType === AppointmentTypeStrings.IN_SHOP) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this.appointmentType === AppointmentTypeStrings.MOBILE
|
||||
|| this.appointmentType === AppointmentTypeStrings.MOBILE_NOT_ITAC) {
|
||||
appointmentTypeCmsWidgetName = this.selectedRouteCode?.includes(PREMIUM_TIME_SLOT_ID_FLAG)
|
||||
? this.mobilePremiumCmsWidgetName
|
||||
: this.mobileCmsWidgetName;
|
||||
} else {
|
||||
if (!this.selectedRouteCode) {
|
||||
return null;
|
||||
}
|
||||
appointmentTypeCmsWidgetName =
|
||||
this.getRelevantDropOffCmsWidgetNameForSelectedTimeSlot(
|
||||
this.selectedRouteCode,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
return this.getCmsContent(
|
||||
appointmentTypeCmsWidgetName,
|
||||
cmsWidgetFieldMappings.SUPPLEMENTAL_INFORMATION
|
||||
);
|
||||
},
|
||||
footerCloseButtonText() {
|
||||
return this.getCmsContent(
|
||||
this.cmsWidgetName,
|
||||
cmsWidgetFieldMappings.MODAL_CLOSE_BUTTON
|
||||
);
|
||||
},
|
||||
premiumAppointmentButtonText() {
|
||||
return this.getCmsContent(
|
||||
this.mobilePremiumCmsWidgetName,
|
||||
cmsWidgetFieldMappings.TIME_SLOT_BUTTON
|
||||
);
|
||||
},
|
||||
dropoffButtonText() {
|
||||
return this.getCmsContent(
|
||||
this.dropoffCmsWidgetName,
|
||||
cmsWidgetFieldMappings.TIME_SLOT_BUTTON
|
||||
);
|
||||
},
|
||||
sameDayDropoffButtonText() {
|
||||
return this.getCmsContent(
|
||||
this.sameDayDropOffCmsWidgetName,
|
||||
cmsWidgetFieldMappings.TIME_SLOT_BUTTON
|
||||
);
|
||||
},
|
||||
overnightDropoffButtonText() {
|
||||
return this.getCmsContent(
|
||||
this.overnightDropOffCmsWidgetName,
|
||||
cmsWidgetFieldMappings.TIME_SLOT_BUTTON
|
||||
);
|
||||
},
|
||||
dropoffDisclaimerText() {
|
||||
return this.getCmsContent(this.dropoffCmsWidgetName, cmsWidgetFieldMappings.DISCLAIMER);
|
||||
},
|
||||
sameDayDropOffDisclaimerText() {
|
||||
return this.getCmsContent(
|
||||
this.sameDayDropOffCmsWidgetName,
|
||||
cmsWidgetFieldMappings.DISCLAIMER
|
||||
);
|
||||
},
|
||||
overnightDropOffDisclaimerText() {
|
||||
return this.getCmsContent(
|
||||
this.overnightDropOffCmsWidgetName,
|
||||
cmsWidgetFieldMappings.DISCLAIMER
|
||||
);
|
||||
},
|
||||
disclaimerTextBlockCopy() {
|
||||
if (this.appointmentType === AppointmentTypeStrings.DROP_OFF) {
|
||||
if (this.selectedRouteCode?.includes(RouteCodeFlags.ALL_DAY_DROP_OFF)) {
|
||||
if (this.isSameDay) {
|
||||
return this.sameDayDropOffDisclaimerText;
|
||||
}
|
||||
return this.dropoffDisclaimerText;
|
||||
} if (this.selectedRouteCode?.includes(RouteCodeFlags.OVERNIGHT_DROP_OFF)) {
|
||||
return this.overnightDropOffDisclaimerText;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
dropOffDurationText() {
|
||||
return this.getCmsContent(this.dropoffCmsWidgetName, cmsWidgetFieldMappings.DURATION);
|
||||
},
|
||||
sameDayDropoffDurationText() {
|
||||
return this.getCmsContent(
|
||||
this.sameDayDropOffCmsWidgetName,
|
||||
cmsWidgetFieldMappings.DURATION
|
||||
);
|
||||
},
|
||||
overnightDropoffDurationText() {
|
||||
return this.getCmsContent(
|
||||
this.overnightDropOffCmsWidgetName,
|
||||
cmsWidgetFieldMappings.DURATION
|
||||
);
|
||||
},
|
||||
inshopDurationText() {
|
||||
const inshopDurationTextWithoutTime = this.getCmsContent(
|
||||
this.cmsWidgetName,
|
||||
cmsWidgetFieldMappings.DURATION
|
||||
);
|
||||
|
||||
const inshopDurationTime = getDisplayTextForDurationLength(
|
||||
this.estimatedServiceMinutesMinimum,
|
||||
this.estimatedServiceMinutesMaximum
|
||||
);
|
||||
|
||||
return `${inshopDurationTextWithoutTime} ${inshopDurationTime}`;
|
||||
},
|
||||
durationTextBlockCopy() {
|
||||
if (this.appointmentType === AppointmentTypeStrings.MOBILE
|
||||
|| this.appointmentType === AppointmentTypeStrings.MOBILE_NOT_ITAC) {
|
||||
return null;
|
||||
} if (this.appointmentType === AppointmentTypeStrings.IN_SHOP) {
|
||||
return this.inshopDurationText;
|
||||
}
|
||||
if (this.selectedRouteCode?.includes(RouteCodeFlags.OVERNIGHT_DROP_OFF)) {
|
||||
return this.overnightDropoffDurationText;
|
||||
} if (this.selectedRouteCode?.includes(RouteCodeFlags.ALL_DAY_DROP_OFF)) {
|
||||
if (this.isSameDay) {
|
||||
return this.sameDayDropoffDurationText;
|
||||
}
|
||||
return this.dropOffDurationText;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
isSameDay() {
|
||||
if (!this.timeSlotsForSelectedDate) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const selectedDate = this.timeSlotsForSelectedDate.date;
|
||||
const todaysDate = new Date().toISOString().split('T')[0];
|
||||
return selectedDate === todaysDate;
|
||||
},
|
||||
dateSelectedReadableDate() {
|
||||
if (!this.timeSlotsForSelectedDate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// This conversion ensures we don't get get GMT induced date changes
|
||||
const dateObject = convertDateStringToDate(this.timeSlotsForSelectedDate.date);
|
||||
// Ex: Tuesday, April 22
|
||||
return dateObject.toLocaleDateString('en-us', {
|
||||
weekday: 'long',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
});
|
||||
},
|
||||
availableTimeSlots() {
|
||||
if (!this.timeSlotsForSelectedDate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this.appointmentType === AppointmentTypeStrings.DROP_OFF) {
|
||||
return this.getAvailableTimeSlotsForDropOff(this.timeSlotsForSelectedDate.timeSlots);
|
||||
} if (this.appointmentType === AppointmentTypeStrings.MOBILE
|
||||
|| this.appointmentType === AppointmentTypeStrings.MOBILE_NOT_ITAC) {
|
||||
return this.getAvailableTimeSlotsForMobile(this.timeSlotsForSelectedDate.timeSlots);
|
||||
}
|
||||
return this.getAvailableTimeSlotsForInshop(this.timeSlotsForSelectedDate.timeSlots);
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
modelValue: {
|
||||
handler(newValue) {
|
||||
this.handleChange(newValue);
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
selectedDate: {
|
||||
handler() {
|
||||
this.selectedRouteCode = null;
|
||||
this.autoSelectTimeSlotIfOnlyOneIsAvailable();
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openModal() {
|
||||
this.modal.openModal();
|
||||
},
|
||||
setModalStatus(isOpened) {
|
||||
this.isModalOpened = isOpened;
|
||||
},
|
||||
closeModal() {
|
||||
this.modal.closeModal();
|
||||
},
|
||||
onModalClosed() {
|
||||
this.$emit('time-slot-modal-closed');
|
||||
},
|
||||
async setSelectedTimeSlot() {
|
||||
this.$emit(
|
||||
'update:modelValue',
|
||||
this.getSelectedTimeSlotInfoObject(this.selectedRouteCode)
|
||||
);
|
||||
this.closeModal();
|
||||
},
|
||||
getRelevantDropOffCmsWidgetNameForSelectedTimeSlot(
|
||||
selectedRouteCode,
|
||||
isSameDayRelevant = false
|
||||
) {
|
||||
if (selectedRouteCode.includes(RouteCodeFlags.OVERNIGHT_DROP_OFF)) {
|
||||
return this.overnightDropOffCmsWidgetName;
|
||||
}
|
||||
|
||||
if (selectedRouteCode.includes(RouteCodeFlags.ALL_DAY_DROP_OFF)) {
|
||||
return this.isSameDay && isSameDayRelevant
|
||||
? this.sameDayDropOffCmsWidgetName
|
||||
: this.dropoffCmsWidgetName;
|
||||
}
|
||||
|
||||
return '';
|
||||
},
|
||||
getAvailableTimeSlotsForInshop(timeSlotsForSelectedDate) {
|
||||
return timeSlotsForSelectedDate.map((timeSlot) => {
|
||||
const readableTime = militaryToTwelveHourTime(timeSlot.startTime);
|
||||
return {
|
||||
value: timeSlot.id,
|
||||
buttonLabel: readableTime
|
||||
};
|
||||
});
|
||||
},
|
||||
getAvailableTimeSlotsForDropOff(timeSlotsForSelectedDate) {
|
||||
const availableTimeSlots = timeSlotsForSelectedDate.map((timeSlot) => {
|
||||
let buttonLabelValue;
|
||||
if (timeSlot.id.includes(RouteCodeFlags.OVERNIGHT_DROP_OFF)) {
|
||||
buttonLabelValue = this.overnightDropoffButtonText;
|
||||
} else if (this.isSameDay) {
|
||||
buttonLabelValue = this.sameDayDropoffButtonText;
|
||||
} else {
|
||||
buttonLabelValue = this.dropoffButtonText;
|
||||
}
|
||||
|
||||
return {
|
||||
value: timeSlot.id,
|
||||
buttonLabel: buttonLabelValue
|
||||
};
|
||||
});
|
||||
|
||||
return availableTimeSlots;
|
||||
},
|
||||
getAvailableTimeSlotsForMobile(timeSlotsForSelectedDate) {
|
||||
const availableTimeSlots = timeSlotsForSelectedDate.map((timeSlot) => {
|
||||
const readableTime = `${militaryToTwelveHourTime(timeSlot.startTime)} - ${militaryToTwelveHourTime(timeSlot.endTime)}`;
|
||||
return {
|
||||
value: timeSlot.id,
|
||||
buttonLabel: readableTime
|
||||
};
|
||||
});
|
||||
|
||||
const isPremiumTimeSlot = timeSlotsForSelectedDate[0].offerPremium;
|
||||
const hasPremiumPartAvailable =
|
||||
this.premiumAppointmentFee?.partType === PREMIUM_FEE_PART_TYPE;
|
||||
if (isPremiumTimeSlot && hasPremiumPartAvailable) {
|
||||
availableTimeSlots.unshift(this.getPremiumAppointmentTimeSlot(timeSlotsForSelectedDate[0]));
|
||||
}
|
||||
|
||||
return availableTimeSlots;
|
||||
},
|
||||
getPremiumAppointmentTimeSlot(timeSlotData) {
|
||||
const formattedPrice =
|
||||
`+$${this.getTotalLineItemPrice(this.premiumAppointmentFee).toFixed(2)}`;
|
||||
|
||||
return {
|
||||
// Unique value is required for each <input> and the premium appoinment shares an id
|
||||
value: this.addPremiumFlagToInput(timeSlotData.id),
|
||||
buttonLabel: this.premiumAppointmentButtonText,
|
||||
buttonLabelSubCopy: formattedPrice,
|
||||
additionalButtonData: {
|
||||
isPremiumAppointment: true
|
||||
}
|
||||
};
|
||||
},
|
||||
getSelectedRouteCode() {
|
||||
let selectedRouteCode;
|
||||
if (!this.modelValue?.timeSlot) {
|
||||
selectedRouteCode = null;
|
||||
}
|
||||
|
||||
if (this.modelValue?.isPremiumAppointment) {
|
||||
selectedRouteCode = this.addPremiumFlagToInput(this.modelValue?.timeSlot?.routeCode);
|
||||
} else {
|
||||
selectedRouteCode = this.modelValue?.timeSlot?.routeCode;
|
||||
}
|
||||
|
||||
return selectedRouteCode;
|
||||
},
|
||||
autoSelectTimeSlotIfOnlyOneIsAvailable() {
|
||||
const numberOfOptions = this.availableTimeSlots?.length;
|
||||
if (numberOfOptions === 1) {
|
||||
this.selectedRouteCode = this.availableTimeSlots[0].value;
|
||||
}
|
||||
},
|
||||
addPremiumFlagToInput(routeCode) {
|
||||
return (`${routeCode}${PREMIUM_TIME_SLOT_ID_FLAG}`);
|
||||
},
|
||||
removePremiumFlagFromInput(routeCode) {
|
||||
return routeCode.replace(PREMIUM_TIME_SLOT_ID_FLAG, '');
|
||||
},
|
||||
getSelectedTimeSlotInfoObject(routeCode) {
|
||||
let routeCodeToUse = routeCode;
|
||||
const routeCodeIncludesPremium = routeCode?.includes(PREMIUM_TIME_SLOT_ID_FLAG);
|
||||
if (routeCodeIncludesPremium) {
|
||||
routeCodeToUse = this.removePremiumFlagFromInput(routeCode);
|
||||
}
|
||||
|
||||
const timeSlot = this.timeSlotsForSelectedDate?.timeSlots?.find((ts) => ts.id === routeCodeToUse);
|
||||
|
||||
if (timeSlot) {
|
||||
return {
|
||||
timeSlot: {
|
||||
date: this.timeSlotsForSelectedDate.date,
|
||||
routeCode: timeSlot.id,
|
||||
startTime: timeSlot.startTime,
|
||||
endTime: timeSlot.endTime,
|
||||
jobMaxMinutes: this.estimatedServiceMinutesMaximum.toString(),
|
||||
jobMinMinutes: this.estimatedServiceMinutesMinimum.toString()
|
||||
},
|
||||
isPremiumAppointment: !!routeCodeIncludesPremium
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
timeSlot: {
|
||||
date: null,
|
||||
startTime: null,
|
||||
endTime: null,
|
||||
routeCode: null,
|
||||
jobMaxMinutes: null,
|
||||
jobMinMinutes: null
|
||||
},
|
||||
isPremiumAppointment: null
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.time-slots-modal.modal.modal-component {
|
||||
.modal-header {
|
||||
padding-bottom: 0;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
.text-block.duration-text-block {
|
||||
margin-top: 4px !important;
|
||||
}
|
||||
.supplemental-information {
|
||||
line-height: 1.5rem;
|
||||
font-size: 0.875rem;
|
||||
li strong {
|
||||
font-weight: $font-weight-bold;
|
||||
}
|
||||
li:not(:last-child) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -51,13 +51,15 @@ export default {
|
|||
return this.getCmsContent(this.cmsWidgetName, 'Answers');
|
||||
},
|
||||
answersToDisplay() {
|
||||
const shouldShowMobile = this.isServiceableMobile;
|
||||
const shouldShowMobile = this.isServiceableMobile && this.isITAC;
|
||||
const shouldShowMobileNotITAC = this.isServiceableMobile && !this.isITAC;
|
||||
const shouldShowInshop = this.isServiceableInshop;
|
||||
const shouldShowDropoff = this.isServiceableInshop && !useMainStore().damage.isRepair;
|
||||
return this.answersFromCms
|
||||
? this.answersFromCms.filter((answer) => (
|
||||
(answer.Name === AppointmentTypeStrings.IN_SHOP && shouldShowInshop)
|
||||
|| (answer.Name === AppointmentTypeStrings.MOBILE && shouldShowMobile)
|
||||
|| (answer.Name === AppointmentTypeStrings.MOBILE_NOT_ITAC && shouldShowMobileNotITAC)
|
||||
|| (answer.Name === AppointmentTypeStrings.DROP_OFF && shouldShowDropoff)
|
||||
))
|
||||
: [];
|
||||
|
|
@ -70,6 +72,9 @@ export default {
|
|||
this.$emit('update:modelValue', newValue);
|
||||
}
|
||||
},
|
||||
isITAC() {
|
||||
return useMainStore().policy.isITAC;
|
||||
},
|
||||
isMobileOnly() {
|
||||
return this.isServiceableMobile && !this.isServiceableInshop;
|
||||
}
|
||||
|
|
@ -80,9 +85,14 @@ export default {
|
|||
// If there is only one option to display and that option is 'Mobile' then select it
|
||||
if (
|
||||
newValue.length === 1
|
||||
&& newValue.findIndex((answer) => answer.Name === 'Mobile') !== -1
|
||||
&& newValue.findIndex((answer) => (answer.Name === AppointmentTypeStrings.MOBILE
|
||||
|| answer.Name === AppointmentTypeStrings.MOBILE_NOT_ITAC)) !== -1
|
||||
) {
|
||||
this.selectedValues = 'Mobile';
|
||||
if (this.isITAC) {
|
||||
this.selectedValues = AppointmentTypeStrings.MOBILE;
|
||||
} else {
|
||||
this.selectedValues = AppointmentTypeStrings.MOBILE_NOT_ITAC;
|
||||
}
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
|
|
@ -90,7 +100,11 @@ export default {
|
|||
isMobileOnly: {
|
||||
handler(newValue) {
|
||||
if (newValue) {
|
||||
this.selectedValues = 'Mobile';
|
||||
if (this.isITAC) {
|
||||
this.selectedValues = AppointmentTypeStrings.MOBILE;
|
||||
} else {
|
||||
this.selectedValues = AppointmentTypeStrings.MOBILE_NOT_ITAC;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@
|
|||
</span>
|
||||
</div>
|
||||
<textBlock
|
||||
v-if="isITAC"
|
||||
:customText="mobileFeeText"
|
||||
cmsWidgetName="MobileFeeDisclaimerWidget"
|
||||
typeStyle="caption" />
|
||||
|
|
@ -81,6 +82,7 @@ import addressQuestions from '@/iss-components/address-questions/address-questio
|
|||
import vehicleProtectedQuestion from '@/layouts/service-location/mobile-location-modal-questions/vehicle-protected-question/vehicle-protected-question.vue';
|
||||
|
||||
// Helpers
|
||||
import { useMainStore } from '@/store';
|
||||
import {
|
||||
getPricedMobileFeePart,
|
||||
getServiceabilityDetails,
|
||||
|
|
@ -167,6 +169,9 @@ export default {
|
|||
};
|
||||
},
|
||||
computed: {
|
||||
isITAC() {
|
||||
return useMainStore().policy.isITAC;
|
||||
},
|
||||
mobileLocationLinkPromptText() {
|
||||
return this.getCmsContent(this.linkWidgetName, 'HeaderText');
|
||||
},
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@
|
|||
cmsWidgetName="AppointmentTypeQuestionWidget"
|
||||
validationRules="option-required" />
|
||||
<mobileLocationModalQuestions
|
||||
v-if="selectedAppointmentType === 'Mobile'"
|
||||
v-if="isMobileLocationDisplayed"
|
||||
ref="mobileLocationQuestions"
|
||||
v-model="mobileLocationQuestions"
|
||||
customComponentId="mobileLocationQuestions"
|
||||
|
|
@ -94,6 +94,7 @@
|
|||
</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 { required } from '@/helpers/validation-rules';
|
||||
|
|
@ -260,7 +261,8 @@ export default {
|
|||
this.isVehicleProtected = newValue.isVehicleProtected;
|
||||
|
||||
if (newValue.zipCode !== this.zipCode) {
|
||||
if (!this.selectedAppointmentType === 'Mobile') {
|
||||
if (!(this.selectedAppointmentType === AppointmentTypeStrings.MOBILE
|
||||
|| this.selectedAppointmentType === AppointmentTypeStrings.MOBILE_NOT_ITAC)) {
|
||||
this.selectedAppointmentType = null;
|
||||
}
|
||||
this.selectedProvider = null;
|
||||
|
|
@ -289,6 +291,10 @@ export default {
|
|||
isAppointmentTypeDisplayed() {
|
||||
return this.zipCode && !this.displayNoShopsAlert;
|
||||
},
|
||||
isMobileLocationDisplayed() {
|
||||
return this.selectedAppointmentType === AppointmentTypeStrings.MOBILE
|
||||
|| this.selectedAppointmentType === AppointmentTypeStrings.MOBILE_NOT_ITAC;
|
||||
},
|
||||
requiresInshopRecalibration() {
|
||||
// Specifically check for isRecalibrationServiceableMobile === false, not null or true.
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@
|
|||
:text="showMoreShopsLinkText"
|
||||
href="#!"
|
||||
:aria-label="showMoreShopsLinkText"
|
||||
@click-event="getNextShopsFromList" />
|
||||
@clickEvent="getNextShopsFromList" />
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
|
@ -48,6 +48,7 @@ import buttonQuestion from '@/digital-components/button-question/button-question
|
|||
import textLink from '@/ux-components/text-link/text-link.vue';
|
||||
|
||||
// Supporting files
|
||||
import { AppointmentTypeStrings } from '@/constants/schedule-constants.js';
|
||||
import baseMixin from '@/mixins/base-mixin.js';
|
||||
import { defineRule } from 'vee-validate';
|
||||
import { required } from '@/helpers/validation-rules';
|
||||
|
|
@ -141,7 +142,7 @@ export default {
|
|||
|
||||
await nextTick();
|
||||
|
||||
if (newValue !== 'Mobile') {
|
||||
if (newValue !== AppointmentTypeStrings.MOBILE && newValue !== AppointmentTypeStrings.MOBILE_NOT_ITAC) {
|
||||
this.getNextShopsFromList();
|
||||
}
|
||||
}
|
||||
|
|
@ -253,6 +254,8 @@ export default {
|
|||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "@/styles/ux-variables-svg-strings.scss";
|
||||
|
||||
.shop-question {
|
||||
margin-top: 1rem;
|
||||
text-align: center;
|
||||
|
|
@ -264,7 +267,7 @@ export default {
|
|||
}
|
||||
|
||||
.drop-off-alert {
|
||||
background: url("data:image/svg+xml,%3Csvg viewBox='0 0 12 12' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cg clip-path='url(%23clip0_13957_112512)'%3E%3Cpath d='M5.99865 0C4.81147 4.82643e-07 3.65095 0.352111 2.66392 1.01179C1.67688 1.67146 0.907678 2.60907 0.45361 3.70599C-0.000459241 4.80291 -0.11899 6.00986 0.113013 7.17415C0.345015 8.33845 0.917126 9.40778 1.75697 10.2469C2.59682 11.086 3.66666 11.6571 4.83117 11.8881C5.99567 12.119 7.20251 11.9994 8.29902 11.5443C9.39553 11.0893 10.3324 10.3192 10.9912 9.33159C11.65 8.34396 12.0011 7.18313 12 5.99594C11.9971 4.40566 11.3638 2.88142 10.2388 1.75742C9.11375 0.633431 7.58894 0.00143011 5.99865 0V0ZM5.99865 11.2478C4.96135 11.2473 3.94748 10.9392 3.08518 10.3627C2.22288 9.7861 1.55085 8.96685 1.15401 8.00846C0.75718 7.05006 0.653353 5.99554 0.855656 4.97815C1.05796 3.96077 1.55731 3.02618 2.29061 2.29251C3.0239 1.55884 3.95823 1.059 4.97551 0.856176C5.99279 0.653349 7.04737 0.756633 8.00597 1.15297C8.96457 1.54931 9.78416 2.22092 10.3612 3.08293C10.9382 3.94493 11.2467 4.95864 11.2478 5.99594C11.2478 7.38835 10.6949 8.72377 9.71053 9.70861C8.7262 10.6934 7.39106 11.2471 5.99865 11.2478V11.2478Z' fill='%2306577C'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M6.22736 8.84695C6.30613 8.76818 6.35038 8.66135 6.35038 8.54996V5.30996C6.35038 5.19857 6.30613 5.09174 6.22736 5.01298C6.1486 4.93421 6.04177 4.88996 5.93038 4.88996C5.81899 4.88996 5.71216 4.93421 5.63339 5.01298C5.55463 5.09174 5.51038 5.19857 5.51038 5.30996V8.54996C5.51038 8.66135 5.55463 8.76818 5.63339 8.84695C5.71216 8.92571 5.81899 8.96996 5.93038 8.96996C6.04177 8.96996 6.1486 8.92571 6.22736 8.84695ZM5.69704 3.97918C5.76611 4.02533 5.84731 4.04996 5.93038 4.04996C5.98558 4.05012 6.04026 4.03936 6.09129 4.01831C6.14232 3.99726 6.18868 3.96633 6.22771 3.9273C6.26675 3.88827 6.29768 3.8419 6.31873 3.79088C6.33978 3.73985 6.35053 3.68516 6.35038 3.62996C6.35038 3.54689 6.32574 3.46569 6.27959 3.39662C6.23344 3.32755 6.16785 3.27372 6.0911 3.24193C6.01436 3.21014 5.92991 3.20183 5.84844 3.21803C5.76697 3.23424 5.69213 3.27424 5.63339 3.33298C5.57465 3.39171 5.53465 3.46655 5.51845 3.54802C5.50224 3.6295 5.51056 3.71394 5.54235 3.79069C5.57414 3.86743 5.62797 3.93303 5.69704 3.97918Z' fill='%2306577C'/%3E%3C/g%3E%3Cdefs%3E%3CclipPath id='clip0_13957_112512'%3E%3Crect width='12' height='12' fill='white'/%3E%3C/clipPath%3E%3C/defs%3E%3C/svg%3E%0A");
|
||||
background: url($svg-drop-off-alert);
|
||||
background-repeat: no-repeat;
|
||||
background-size: 0.75rem;
|
||||
background-position: 0.5rem 0.75rem;
|
||||
|
|
|
|||
|
|
@ -198,6 +198,16 @@ defineRule(
|
|||
'policy-zip-format',
|
||||
regex(/(^\d{5}$)|(^\d{5}-\d{4}$)/, errorMessages.POLICY_ZIP_FORMAT)
|
||||
);
|
||||
defineRule('loss-date-gt-10-years', (value) => {
|
||||
const lossDate = new Date(Date.parse(`${value}T00:00:00`));
|
||||
const today = new Date();
|
||||
const tenYearsAgo = new Date(today.getFullYear() - 10, today.getMonth(), today.getDate(), 0, 0, 0, 0);
|
||||
if (lossDate < tenYearsAgo) {
|
||||
return errorMessages.DAMAGE_DATE_REQUIREMENT;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
export default {
|
||||
name: 'welcome-page',
|
||||
|
|
@ -247,7 +257,7 @@ export default {
|
|||
rules: {
|
||||
policyNumber: 'policy-number-required',
|
||||
policyZip: 'policy-zip-required|policy-zip-format',
|
||||
lossDate: 'loss-date-required',
|
||||
lossDate: 'loss-date-required|loss-date-gt-10-years',
|
||||
damageOption: 'damage-option-required',
|
||||
phoneNumber: `${globalRules.PHONE_NUMBER_REQUIRED}|${globalRules.PHONE_NUMBER_FORMAT}`,
|
||||
email: `${globalRules.EMAIL_ADDRESS_REQUIRED}|${globalRules.EMAIL_ADDRESS_FORMAT}`,
|
||||
|
|
|
|||
|
|
@ -54,6 +54,9 @@ const navigationScenarios = Object.freeze({
|
|||
CLICKED_BACK_WITH_VIN_AND_NO_MORE_QUESTIONS: 'CLICKED_BACK_WITH_VIN_AND_NO_MORE_QUESTIONS',
|
||||
CLICKED_BACK_WITH_NO_VIN_NOR_QUESTIONS: 'CLICKED_BACK_WITH_NO_VIN_NOR_QUESTIONS',
|
||||
|
||||
// Schedule
|
||||
CLICKED_CHANGE_LOCATION: 'CLICKED_CHANGE_LOCATION',
|
||||
|
||||
// Coverage Statement
|
||||
CLICKED_BACK_WITH_REPAIR: 'CLICKED_BACK_WITH_REPAIR',
|
||||
CLICKED_FORWARD_WITH_INVALID_STATE: 'CLICKED_FORWARD_WITH_INVALID_STATE',
|
||||
|
|
|
|||
|
|
@ -555,6 +555,10 @@ const routingTable = () => [
|
|||
{
|
||||
scenario: navigationScenarios.CLICKED_FORWARD,
|
||||
destinationIssPageValue: issPageValues.CONTACT_DETAILS
|
||||
},
|
||||
{
|
||||
scenario: navigationScenarios.CLICKED_CHANGE_LOCATION,
|
||||
destinationIssPageValue: issPageValues.SERVICE_LOCATION
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -11,10 +11,24 @@ import applicationConfig from '@/constants/application-config';
|
|||
import issPageValues from '@/router/router-constants/issPage-values';
|
||||
import damageLocationsSelected from '@/constants/damage-locations-selected';
|
||||
import coverageStatuses from '@/constants/coverage-statuses';
|
||||
import { PREMIUM_FEE_PART_TYPE } from '@/constants/schedule-constants';
|
||||
import { AppointmentTypeStrings, PREMIUM_FEE_PART_TYPE } from '@/constants/schedule-constants';
|
||||
import getDateDifferenceInDays from '@/helpers/date-helper';
|
||||
|
||||
const storeId = 'main';
|
||||
|
||||
function getTimeSlotsAdditionalEventData(
|
||||
provisionalTriggers,
|
||||
zipCode,
|
||||
firstAvailableAppointmentDateString,
|
||||
shopAppointmentType
|
||||
) {
|
||||
let numberOfDays = null;
|
||||
if (firstAvailableAppointmentDateString) numberOfDays = getDateDifferenceInDays(new Date(), firstAvailableAppointmentDateString);
|
||||
|
||||
if (shopAppointmentType) return `FirstAvailableAppointment:${numberOfDays},Zip:${zipCode},ShopAppointmentType:${shopAppointmentType},ProvisionalTriggers:${provisionalTriggers.join(',')}`;
|
||||
return `FirstAvailableAppointment:${numberOfDays},Zip:${zipCode},ProvisionalTriggers:${provisionalTriggers.join(',')}`;
|
||||
}
|
||||
|
||||
const getDefaultState = () => ({
|
||||
order: {
|
||||
vehicle: {
|
||||
|
|
@ -60,6 +74,7 @@ const getDefaultState = () => ({
|
|||
repair: null, // numerical value; how much customer owes on deductible in repair case
|
||||
replace: null // numerical value; how much customer owes on deductible in replace case,
|
||||
},
|
||||
isITAC: null,
|
||||
vehicles: [],
|
||||
endorsementQuestionAnswers: null
|
||||
},
|
||||
|
|
@ -106,7 +121,8 @@ const getDefaultState = () => ({
|
|||
isInsurance: true, // TODO delete; irrelevant to ISS
|
||||
insuranceCoverage: {
|
||||
isVerified: false,
|
||||
coverageStatus: coverageStatuses.PENDING
|
||||
coverageStatus: coverageStatuses.PENDING,
|
||||
claimNumber: null
|
||||
},
|
||||
parentAccountNumber: 0
|
||||
},
|
||||
|
|
@ -123,7 +139,8 @@ const getDefaultState = () => ({
|
|||
startTime: null,
|
||||
endTime: null,
|
||||
routeCode: null,
|
||||
jobMaxMinutes: null
|
||||
jobMaxMinutes: null,
|
||||
jobMinMinutes: null
|
||||
},
|
||||
referralNumber: null,
|
||||
referralDate: null,
|
||||
|
|
@ -178,6 +195,9 @@ export const useMainStore = defineStore({
|
|||
payment: (state) => state.order.payment,
|
||||
policy: (state) => state.order.policy,
|
||||
hasAnyNonWindshieldGlassParts: (state) => !state.order.policy.isDamageGlassOnly,
|
||||
isMobileAppointment: (state) => state.order.serviceLocation.appointmentType === AppointmentTypeStrings.MOBILE
|
||||
|| state.order.serviceLocation.appointmentType === AppointmentTypeStrings.MOBILE_NOT_ITAC,
|
||||
isDropOffAppointment: (state) => state.order.serviceLocation.appointmentType === AppointmentTypeStrings.DROP_OFF,
|
||||
isClaimRegistrationRequired: (state) => state.issConfig.isClaimRegistrationRequired,
|
||||
eventBusItem: (state) => (eventCategory, eventSubCategory) => {
|
||||
const matchedEvent = state.applicationUser.eventBus.find(({ category, subCategory }) => category === eventCategory && subCategory === eventSubCategory);
|
||||
|
|
@ -464,17 +484,20 @@ export const useMainStore = defineStore({
|
|||
}).then((response) => {
|
||||
const registerClaimFailed = response.data.isError;
|
||||
order.payment.insuranceCoverage.isVerified = !registerClaimFailed;
|
||||
order.payment.insuranceCoverage.claimNumber = null;
|
||||
if (registerClaimFailed) {
|
||||
this.order.payment.insuranceCoverage.coverageStatus = coverageStatuses.PENDING;
|
||||
} else if (this.policy.noCoverage) {
|
||||
this.order.payment.insuranceCoverage.coverageStatus = coverageStatuses.NO_COMP;
|
||||
} else {
|
||||
this.order.payment.insuranceCoverage.coverageStatus = coverageStatuses.VERIFIED;
|
||||
this.order.payment.insuranceCoverage.claimNumber = response.data.claimNumber;
|
||||
}
|
||||
return resolve(response);
|
||||
}, (error) => {
|
||||
this.order.payment.insuranceCoverage.isVerified = false;
|
||||
this.order.payment.insuranceCoverage.coverageStatus = coverageStatuses.PENDING;
|
||||
this.order.payment.insuranceCoverage.claimNumber = null;
|
||||
return reject(error);
|
||||
});
|
||||
});
|
||||
|
|
@ -608,7 +631,75 @@ export const useMainStore = defineStore({
|
|||
}
|
||||
});
|
||||
},
|
||||
getMobilePremiumFee() {
|
||||
const damageType = this.damage.isRepair ? 'Repair' : 'Replace';
|
||||
const paymentType = this.order.payment.isInsurance ? 'Insurance' : 'Cash';
|
||||
|
||||
return globalMethods.callHttpClient({
|
||||
method: endpoints.GetMobilePremiumFee.method,
|
||||
endpoint: `${endpoints.GetMobilePremiumFee.url}/${paymentType}/${damageType}`,
|
||||
logApiCall: true
|
||||
});
|
||||
},
|
||||
getMobileTimeSlots(startDate, endDate) {
|
||||
const { order } = this;
|
||||
const { vehicle } = this.order;
|
||||
let lineItems = [
|
||||
...(order.lineItems.supportingItems ?? []),
|
||||
...(order.lineItems.vaps ?? []),
|
||||
...getFlattenedArrayOfLineItemsWithChildParts(order.lineItems.glassParts)
|
||||
];
|
||||
lineItems = lineItems.map((lineItem) => ({
|
||||
partNumber: lineItem.partNumber,
|
||||
partType: lineItem.partType
|
||||
}));
|
||||
const glassPieces = order.damage.glassToReplace
|
||||
? convertGlassPieceToBackEndCompatibleFormat(order.damage.glassToReplace)
|
||||
: [];
|
||||
const payload = {
|
||||
startDate,
|
||||
endDate,
|
||||
applicationName: applicationConfig.APPLICATION_NAME,
|
||||
parentAccountNumber: this.issConfig.parentAccountNumber, // this.payment.parentAccountNumber,
|
||||
carId: vehicle.carId,
|
||||
lineItems,
|
||||
glassPieces,
|
||||
eon: order.eon,
|
||||
coverage: {
|
||||
status: '',
|
||||
deductible: 0,
|
||||
additionalAuthFlag: ''
|
||||
},
|
||||
partSelection: {
|
||||
hasAnsweredPartQuestions: !!order.damage.partQuestionAnswers?.length,
|
||||
hasAnsweredMoldingQuestions: !!order.damage.moldingQuestionAnswers?.length,
|
||||
hasAnsweredCapabilityQuestions: !!order.damage.capabilityQuestionAnswers?.length,
|
||||
hasManuallySelectedParts:
|
||||
!!this.applicationUser.pageData['vehicle-parts']?.partsOrQuestions
|
||||
.length
|
||||
},
|
||||
vehicle: {
|
||||
year: vehicle.year,
|
||||
make: vehicle.make,
|
||||
model: vehicle.model,
|
||||
style: vehicle.style,
|
||||
vin: vehicle.vin ?? ''
|
||||
},
|
||||
zipCode: order.serviceLocation.zipCode
|
||||
};
|
||||
return globalMethods.callHttpClient({
|
||||
method: endpoints.GetMobileTimeSlots.method,
|
||||
endpoint: endpoints.GetMobileTimeSlots.url,
|
||||
payload,
|
||||
logApiCall: true,
|
||||
additionalSuccessEventDataHandler: (response) =>
|
||||
getTimeSlotsAdditionalEventData(
|
||||
response.data.provisionalTriggers,
|
||||
order.serviceLocation.zipCode,
|
||||
response.data.days?.[0]?.date
|
||||
)
|
||||
});
|
||||
},
|
||||
getShopTimeSlots(startDate, endDate, shopAppointmentType, providerNumber) {
|
||||
const { order } = this;
|
||||
const { vehicle } = this.order;
|
||||
|
|
@ -631,7 +722,7 @@ export const useMainStore = defineStore({
|
|||
endDate,
|
||||
shopAppointmentType,
|
||||
applicationName: applicationConfig.APPLICATION_NAME,
|
||||
parentAccountNumber: this.payment.parentAccountNumber,
|
||||
parentAccountNumber: this.issConfig.parentAccountNumber, // this.payment.parentAccountNumber,
|
||||
carId: vehicle.carId,
|
||||
lineItems,
|
||||
glassPieces,
|
||||
|
|
@ -645,9 +736,7 @@ export const useMainStore = defineStore({
|
|||
hasAnsweredPartQuestions: !!order.damage.partQuestionAnswers?.length,
|
||||
hasAnsweredMoldingQuestions: !!order.damage.moldingQuestionAnswers?.length,
|
||||
hasAnsweredCapabilityQuestions: !!order.damage.capabilityQuestionAnswers?.length,
|
||||
hasManuallySelectedParts:
|
||||
!!this.applicationUser.pageData['vehicle-parts']?.partsOrQuestions
|
||||
.length
|
||||
hasManuallySelectedParts: !!this.applicationUser.pageData['vehicle-parts']?.partsOrQuestions.length
|
||||
},
|
||||
vehicle: {
|
||||
year: vehicle.year,
|
||||
|
|
@ -871,7 +960,8 @@ export const useMainStore = defineStore({
|
|||
policyFirstName: customer.firstName,
|
||||
policyLastName: customer.lastName,
|
||||
policyPhoneNumber: customer.phoneNumber,
|
||||
policyEmail: customer.emailAddress
|
||||
policyEmail: customer.emailAddress,
|
||||
policyState: customer.address.state
|
||||
},
|
||||
policyNumber: policy.policyNumber,
|
||||
policyZipCode: policy.policyZipCode,
|
||||
|
|
@ -902,7 +992,8 @@ export const useMainStore = defineStore({
|
|||
payment: {
|
||||
InsuranceCoverage: {
|
||||
isVerified: payment.insuranceCoverage?.isVerified ?? false,
|
||||
coverageStatus: payment.insuranceCoverage?.coverageStatus
|
||||
coverageStatus: payment.insuranceCoverage?.coverageStatus,
|
||||
claimNumber: payment.insuranceCoverage?.claimNumber
|
||||
},
|
||||
isInsurance: payment.isInsurance ?? true,
|
||||
parentAccountNumber: this.issConfig.parentAccountNumber
|
||||
|
|
@ -934,7 +1025,8 @@ export const useMainStore = defineStore({
|
|||
startTime: schedule.startTime,
|
||||
endTime: schedule.endTime,
|
||||
routeCode: schedule.routeCode,
|
||||
jobMaxMinutes: schedule.jobMaxMinutes
|
||||
jobMaxMinutes: schedule.jobMaxMinutes,
|
||||
jobMinMinutes: schedule.jobMinMinutes
|
||||
},
|
||||
referralDate: this.order.referralDate,
|
||||
referralNumber: this.order.referralNumber?.toString(),
|
||||
|
|
@ -1127,16 +1219,16 @@ export const useMainStore = defineStore({
|
|||
this.order.vehicle.registration.lastName = registrationInfo?.lastName;
|
||||
},
|
||||
updateServiceLocation(serviceLocationInfo) {
|
||||
state.order.serviceLocation.address = serviceLocationInfo.address;
|
||||
state.order.serviceLocation.address2 = serviceLocationInfo.address2;
|
||||
state.order.serviceLocation.city = serviceLocationInfo.city;
|
||||
state.order.serviceLocation.state = serviceLocationInfo.state;
|
||||
state.order.serviceLocation.zipCode = serviceLocationInfo.zipCode;
|
||||
state.order.serviceLocation.zipCodeCtu = serviceLocationInfo.zipCodeCtu;
|
||||
state.order.serviceLocation.appointmentType = serviceLocationInfo.appointmentType;
|
||||
state.order.serviceLocation.isVehicleProtected = serviceLocationInfo.isVehicleProtected;
|
||||
this.order.serviceLocation.address = serviceLocationInfo.address;
|
||||
this.order.serviceLocation.address2 = serviceLocationInfo.address2;
|
||||
this.order.serviceLocation.city = serviceLocationInfo.city;
|
||||
this.order.serviceLocation.state = serviceLocationInfo.state;
|
||||
this.order.serviceLocation.zipCode = serviceLocationInfo.zipCode;
|
||||
this.order.serviceLocation.zipCodeCtu = serviceLocationInfo.zipCodeCtu;
|
||||
this.order.serviceLocation.appointmentType = serviceLocationInfo.appointmentType;
|
||||
this.order.serviceLocation.isVehicleProtected = serviceLocationInfo.isVehicleProtected;
|
||||
|
||||
state.order.serviceLocation.provider = {
|
||||
this.order.serviceLocation.provider = {
|
||||
providerNumber: serviceLocationInfo.provider?.providerNumber,
|
||||
address: {
|
||||
streetAddress: serviceLocationInfo.provider?.address?.streetAddress,
|
||||
|
|
@ -1251,7 +1343,7 @@ export const useMainStore = defineStore({
|
|||
this.order.schedule.endTime = null;
|
||||
this.order.schedule.routeCode = null;
|
||||
this.order.schedule.jobMaxMinutes = null;
|
||||
// this.order.schedule.jobMinMinutes = null;
|
||||
this.order.schedule.jobMinMinutes = null;
|
||||
|
||||
// premium appointment fee used on schedule page also needs reset when schedule is reset
|
||||
const { supportingItems } = this.order.lineItems;
|
||||
|
|
@ -1396,6 +1488,9 @@ export const useMainStore = defineStore({
|
|||
this.order.customer.lastName = customerQuestions.lastName;
|
||||
this.order.serviceLocation.zipCode = customerQuestions.addressQuestions.zipCode;
|
||||
},
|
||||
updatePolicyITACFlag(isITAC) {
|
||||
this.order.policy.isITAC = isITAC;
|
||||
},
|
||||
savePartQuestionAnswers(partQuestionAnswersArray) {
|
||||
// if part question answers have changed, reset subsequent question answers
|
||||
const sortedPreviousResultsArray = sortArrayOfObjectsByPropertyValue(this.order.damage.partQuestionAnswers, 'result');
|
||||
|
|
@ -1453,6 +1548,10 @@ export const useMainStore = defineStore({
|
|||
this.order.lineItems.glassParts = glassParts;
|
||||
},
|
||||
saveSupportingItems(supportingItems) {
|
||||
// TODO: RESET_SERVICE_LOCATION_STATE_AND_DEPENDENCIES
|
||||
this.order.lineItems.supportingItems = supportingItems;
|
||||
},
|
||||
saveSupportingItemsSuppressingStateResetting(supportingItems) {
|
||||
this.order.lineItems.supportingItems = supportingItems;
|
||||
},
|
||||
saveVaps(vaps) {
|
||||
|
|
@ -1522,6 +1621,31 @@ export const useMainStore = defineStore({
|
|||
}
|
||||
},
|
||||
|
||||
// Location API Actions
|
||||
getAlertReasonsByCtu(ctu) {
|
||||
return globalMethods.callHttpClient({
|
||||
method: endpoints.GetAlertReasons.method,
|
||||
endpoint: `${endpoints.GetAlertReasons.url}/${ctu}`,
|
||||
payload: {},
|
||||
logApiCall: true
|
||||
});
|
||||
},
|
||||
|
||||
// Schedule Actions
|
||||
saveSchedule(scheduleInfo) {
|
||||
this.updateSchedule(scheduleInfo);
|
||||
},
|
||||
updateSchedule(scheduleInfo) {
|
||||
if (scheduleInfo) {
|
||||
this.order.schedule.date = scheduleInfo.date;
|
||||
this.order.schedule.startTime = scheduleInfo.startTime;
|
||||
this.order.schedule.endTime = scheduleInfo.endTime;
|
||||
this.order.schedule.routeCode = scheduleInfo.routeCode;
|
||||
this.order.schedule.jobMaxMinutes = scheduleInfo.jobMaxMinutes;
|
||||
this.order.schedule.jobMinMinutes = scheduleInfo.jobMinMinutes;
|
||||
}
|
||||
},
|
||||
|
||||
// Analytics Actions
|
||||
logExperimentExposure({ userId, sessionKey, pageName, experiment }) {
|
||||
return globalMethods.callHttpClient({
|
||||
|
|
@ -1803,6 +1927,7 @@ export const useMainStore = defineStore({
|
|||
this.order.policy.damageCause = null;
|
||||
this.order.policy.damageCity = null;
|
||||
this.order.policy.damageState = null;
|
||||
this.order.policy.isITAC = null;
|
||||
this.order.customer.phoneNumber = null;
|
||||
this.order.customer.emailAddress = null;
|
||||
this.order.customer.firstName = null;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import globalMethods from '@/global-methods.js';
|
|||
import { getRandomString, getRandomGuid, getRandomInt, getRandomBoolean } from '@/helpers/data-generation.js';
|
||||
import coverageStatuses from '@/constants/coverage-statuses.js';
|
||||
import { endpoints } from '@/constants/endpoints';
|
||||
import { AppointmentTypeStrings } from '@/constants/schedule-constants';
|
||||
|
||||
describe('Store', () => {
|
||||
let store;
|
||||
|
|
@ -396,6 +397,7 @@ describe('Store', () => {
|
|||
expect(globalMethods.callHttpClient).toHaveBeenCalled();
|
||||
expect(store.payment.insuranceCoverage.isVerified).toBe(true);
|
||||
expect(store.payment.insuranceCoverage.coverageStatus).toBe(coverageStatuses.NO_COMP);
|
||||
expect(store.payment.insuranceCoverage.claimNumber).toBe(null);
|
||||
});
|
||||
|
||||
it('successful response with coverage => isVerified true and coverage status verified', async () => {
|
||||
|
|
@ -422,11 +424,12 @@ describe('Store', () => {
|
|||
expect(globalMethods.callHttpClient).toHaveBeenCalled();
|
||||
expect(store.payment.insuranceCoverage.isVerified).toBe(true);
|
||||
expect(store.payment.insuranceCoverage.coverageStatus).toBe(coverageStatuses.VERIFIED);
|
||||
expect(store.payment.insuranceCoverage.claimNumber).toBe(response.data.claimNumber);
|
||||
});
|
||||
|
||||
it('Call to client returns exception, resulting in object with error property being returned', async () => {
|
||||
// Arrange
|
||||
expect.assertions(4);
|
||||
expect.assertions(5);
|
||||
const error = 'this is the error';
|
||||
globalMethods.callHttpClient = jest.fn().mockReturnValue(Promise.reject(error));
|
||||
|
||||
|
|
@ -439,6 +442,7 @@ describe('Store', () => {
|
|||
expect(globalMethods.callHttpClient).toHaveBeenCalled();
|
||||
expect(store.payment.insuranceCoverage.isVerified).toBe(false);
|
||||
expect(store.payment.insuranceCoverage.coverageStatus).toBe(coverageStatuses.PENDING);
|
||||
expect(store.payment.insuranceCoverage.claimNumber).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -624,6 +628,7 @@ describe('Store', () => {
|
|||
const customerLastName = getRandomString(6, 6);
|
||||
const customerEmail = getRandomString(6, 6);
|
||||
const customerPhoneNumber = getRandomString(6, 6);
|
||||
const customerState = getRandomString(2, 2);
|
||||
const policyNumber = getRandomString(6, 6);
|
||||
const policyZipCode = getRandomString(6, 6);
|
||||
const policyLookupSuccessful = getRandomString(6, 6);
|
||||
|
|
@ -636,6 +641,7 @@ describe('Store', () => {
|
|||
store.order.customer.lastName = customerLastName;
|
||||
store.order.customer.emailAddress = customerEmail;
|
||||
store.order.customer.phoneNumber = customerPhoneNumber;
|
||||
state.order.customer.address.state = customerState;
|
||||
store.order.policy.policyNumber = policyNumber;
|
||||
store.order.policy.policyZipCode = policyZipCode;
|
||||
store.order.policy.policyLookupSuccessful = policyLookupSuccessful;
|
||||
|
|
@ -653,7 +659,8 @@ describe('Store', () => {
|
|||
policyFirstName: customerFirstName,
|
||||
policyLastName: customerLastName,
|
||||
policyPhoneNumber: customerPhoneNumber,
|
||||
policyEmail: customerEmail
|
||||
policyEmail: customerEmail,
|
||||
policyState: customerState
|
||||
}),
|
||||
policyNumber,
|
||||
policyZipCode,
|
||||
|
|
@ -687,6 +694,7 @@ describe('Store', () => {
|
|||
store.order.customer.address.city = city;
|
||||
store.order.customer.address.state = state;
|
||||
store.order.customer.address.zipCode = zipCode;
|
||||
|
||||
globalMethods.callHttpClient = jest.fn().mockReturnValue(Promise.resolve({}));
|
||||
|
||||
// Act
|
||||
|
|
@ -720,6 +728,7 @@ describe('Store', () => {
|
|||
store.order.lineItems.glassParts = glassParts;
|
||||
store.order.lineItems.supportingItems = supportingItems;
|
||||
store.order.lineItems.vaps = vaps;
|
||||
|
||||
globalMethods.callHttpClient = jest.fn().mockReturnValue(Promise.resolve({}));
|
||||
|
||||
// Act
|
||||
|
|
@ -776,6 +785,7 @@ describe('Store', () => {
|
|||
const state = getRandomString(6, 6);
|
||||
const zipCode = getRandomString(6, 6);
|
||||
const zipCodeCtu = getRandomString(6, 6);
|
||||
|
||||
store.order.contactInfo.notesForTechnician = notesForTechnician;
|
||||
store.order.serviceLocation.address = address;
|
||||
store.order.serviceLocation.city = city;
|
||||
|
|
@ -810,11 +820,14 @@ describe('Store', () => {
|
|||
const endTime = getRandomString(6, 6);
|
||||
const routeCode = getRandomString(6, 6);
|
||||
const jobMaxMinutes = getRandomString(6, 6);
|
||||
const jobMinMinutes = getRandomString(6, 6);
|
||||
store.order.schedule.date = date;
|
||||
store.order.schedule.startTime = startTime;
|
||||
store.order.schedule.endTime = endTime;
|
||||
store.order.schedule.routeCode = routeCode;
|
||||
store.order.schedule.jobMaxMinutes = jobMaxMinutes;
|
||||
store.order.schedule.jobMinMinutes = jobMinMinutes;
|
||||
|
||||
globalMethods.callHttpClient = jest.fn().mockReturnValue(Promise.resolve({}));
|
||||
|
||||
// Act
|
||||
|
|
@ -828,7 +841,8 @@ describe('Store', () => {
|
|||
startTime,
|
||||
endTime,
|
||||
routeCode,
|
||||
jobMaxMinutes
|
||||
jobMaxMinutes,
|
||||
jobMinMinutes
|
||||
})
|
||||
})
|
||||
}));
|
||||
|
|
@ -1396,4 +1410,101 @@ describe('Store', () => {
|
|||
expect(store.applicationUser.duplicateOrders.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updatePolicyITACFlag method', () => {
|
||||
it('updatePolicyITACFlag updates policy.isITAC flag in store', () => {
|
||||
// Arrange
|
||||
const moqIsITAC = getRandomBoolean();
|
||||
|
||||
// Act
|
||||
store.updatePolicyITACFlag(moqIsITAC);
|
||||
|
||||
// Assert
|
||||
expect(store.order.policy.isITAC).toEqual(moqIsITAC);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isMobileAppointment', () => {
|
||||
it('Should return true for mobile appointments', () => {
|
||||
// Arrange
|
||||
|
||||
// Act
|
||||
store.updateServiceLocation({
|
||||
appointmentType: AppointmentTypeStrings.MOBILE
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(store.isMobileAppointment).toBe(true);
|
||||
});
|
||||
|
||||
it('Should return true for mobile-insurance appointments', () => {
|
||||
// Arrange
|
||||
|
||||
// Act
|
||||
store.updateServiceLocation({
|
||||
appointmentType: AppointmentTypeStrings.MOBILE_NOT_ITAC
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(store.isMobileAppointment).toBe(true);
|
||||
});
|
||||
|
||||
it('Should return false for non-mobile appointments', () => {
|
||||
// Arrange
|
||||
|
||||
// Act
|
||||
store.updateServiceLocation({
|
||||
appointmentType: AppointmentTypeStrings.IN_SHOP
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(store.isMobileAppointment).toBe(false);
|
||||
});
|
||||
|
||||
it('Should return false for null appointments', () => {
|
||||
// Arrange
|
||||
|
||||
// Act
|
||||
store.updateServiceLocation({ appointmentType: null });
|
||||
|
||||
// Assert
|
||||
expect(store.isMobileAppointment).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isDropoffAppointment', () => {
|
||||
it('Should return true for drop-off appointments', () => {
|
||||
// Arrange
|
||||
|
||||
// Act
|
||||
store.updateServiceLocation({
|
||||
appointmentType: AppointmentTypeStrings.DROP_OFF
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(store.isDropOffAppointment).toBe(true);
|
||||
});
|
||||
|
||||
it('Should return false for non-drop-off appointments', () => {
|
||||
// Arrange
|
||||
|
||||
// Act
|
||||
store.updateServiceLocation({
|
||||
appointmentType: AppointmentTypeStrings.MOBILE
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(store.isDropOffAppointment).toBe(false);
|
||||
});
|
||||
|
||||
it('Should return false for null appointments', () => {
|
||||
// Arrange
|
||||
|
||||
// Act
|
||||
store.updateServiceLocation({ appointmentType: null });
|
||||
|
||||
// Assert
|
||||
expect(store.isDropOffAppointment).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
3
src/styles/ux-variables-svg-strings.scss
Normal file
3
src/styles/ux-variables-svg-strings.scss
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
$svg-date-picker-nav-back-button: "data:image/svg+xml,%3Csvg width='7' height='12' viewBox='0 0 7 12' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0.331685 6.00121C0.330445 5.82446 0.399256 5.65442 0.523053 5.52832L5.84499 0.198256C5.97188 0.0713149 6.14397 -8.63821e-08 6.32341 -6.72174e-08C6.50285 -4.80527e-08 6.67495 0.071315 6.80183 0.198256C6.92872 0.325198 7 0.497368 7 0.67689C7 0.856412 6.92872 1.02858 6.80183 1.15552L1.94874 6.00121L6.80183 10.8526C6.92745 10.9796 6.99751 11.1512 6.99662 11.3299C6.99572 11.5085 6.92393 11.6794 6.79705 11.8051C6.67016 11.9308 6.49857 12.0009 6.32003 12C6.14148 11.9991 5.97061 11.9273 5.84499 11.8003L0.526881 6.47601C0.401568 6.34983 0.331375 6.17908 0.331685 6.00121Z' fill='%231574A1'/%3E%3C/svg%3E%0A";
|
||||
$svg-date-picker-forward-button: "data:image/svg+xml,%3Csvg width='7' height='12' viewBox='0 0 7 12' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M6.66831 5.99879C6.66955 6.17554 6.60074 6.34558 6.47695 6.47168L1.15501 11.8017C1.02812 11.9287 0.85603 12 0.676587 12C0.497145 12 0.325053 11.9287 0.198168 11.8017C0.0712831 11.6748 7.268e-09 11.5026 8.07183e-09 11.3231C8.87567e-09 11.1436 0.0712831 10.9714 0.198168 10.8445L5.05126 5.99879L0.198168 1.14736C0.0725521 1.02042 0.00248585 0.848755 0.00338306 0.670131C0.00428028 0.491506 0.0760674 0.320554 0.202952 0.194881C0.329837 0.0692091 0.501426 -0.000888818 0.679971 9.54485e-06C0.858515 0.000906955 1.02939 0.0727263 1.15501 0.199668L6.47312 5.52399C6.59843 5.65017 6.66862 5.82092 6.66831 5.99879Z' fill='%231574A1'/%3E%3C/svg%3E%0A";
|
||||
$svg-drop-off-alert: "data:image/svg+xml,%3Csvg viewBox='0 0 12 12' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cg clip-path='url(%23clip0_13957_112512)'%3E%3Cpath d='M5.99865 0C4.81147 4.82643e-07 3.65095 0.352111 2.66392 1.01179C1.67688 1.67146 0.907678 2.60907 0.45361 3.70599C-0.000459241 4.80291 -0.11899 6.00986 0.113013 7.17415C0.345015 8.33845 0.917126 9.40778 1.75697 10.2469C2.59682 11.086 3.66666 11.6571 4.83117 11.8881C5.99567 12.119 7.20251 11.9994 8.29902 11.5443C9.39553 11.0893 10.3324 10.3192 10.9912 9.33159C11.65 8.34396 12.0011 7.18313 12 5.99594C11.9971 4.40566 11.3638 2.88142 10.2388 1.75742C9.11375 0.633431 7.58894 0.00143011 5.99865 0V0ZM5.99865 11.2478C4.96135 11.2473 3.94748 10.9392 3.08518 10.3627C2.22288 9.7861 1.55085 8.96685 1.15401 8.00846C0.75718 7.05006 0.653353 5.99554 0.855656 4.97815C1.05796 3.96077 1.55731 3.02618 2.29061 2.29251C3.0239 1.55884 3.95823 1.059 4.97551 0.856176C5.99279 0.653349 7.04737 0.756633 8.00597 1.15297C8.96457 1.54931 9.78416 2.22092 10.3612 3.08293C10.9382 3.94493 11.2467 4.95864 11.2478 5.99594C11.2478 7.38835 10.6949 8.72377 9.71053 9.70861C8.7262 10.6934 7.39106 11.2471 5.99865 11.2478V11.2478Z' fill='%2306577C'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M6.22736 8.84695C6.30613 8.76818 6.35038 8.66135 6.35038 8.54996V5.30996C6.35038 5.19857 6.30613 5.09174 6.22736 5.01298C6.1486 4.93421 6.04177 4.88996 5.93038 4.88996C5.81899 4.88996 5.71216 4.93421 5.63339 5.01298C5.55463 5.09174 5.51038 5.19857 5.51038 5.30996V8.54996C5.51038 8.66135 5.55463 8.76818 5.63339 8.84695C5.71216 8.92571 5.81899 8.96996 5.93038 8.96996C6.04177 8.96996 6.1486 8.92571 6.22736 8.84695ZM5.69704 3.97918C5.76611 4.02533 5.84731 4.04996 5.93038 4.04996C5.98558 4.05012 6.04026 4.03936 6.09129 4.01831C6.14232 3.99726 6.18868 3.96633 6.22771 3.9273C6.26675 3.88827 6.29768 3.8419 6.31873 3.79088C6.33978 3.73985 6.35053 3.68516 6.35038 3.62996C6.35038 3.54689 6.32574 3.46569 6.27959 3.39662C6.23344 3.32755 6.16785 3.27372 6.0911 3.24193C6.01436 3.21014 5.92991 3.20183 5.84844 3.21803C5.76697 3.23424 5.69213 3.27424 5.63339 3.33298C5.57465 3.39171 5.53465 3.46655 5.51845 3.54802C5.50224 3.6295 5.51056 3.71394 5.54235 3.79069C5.57414 3.86743 5.62797 3.93303 5.69704 3.97918Z' fill='%2306577C'/%3E%3C/g%3E%3Cdefs%3E%3CclipPath id='clip0_13957_112512'%3E%3Crect width='12' height='12' fill='white'/%3E%3C/clipPath%3E%3C/defs%3E%3C/svg%3E%0A";
|
||||
Loading…
Reference in a new issue