Merge branch 'develop' into feature/SSR-1042

This commit is contained in:
Katie Kroell 2024-01-03 11:32:33 -05:00
commit d3cc84cb6c
28 changed files with 3335 additions and 118 deletions

View file

@ -0,0 +1,7 @@
const packageNames = Object.freeze({
TIER_ONE: 'TierOne',
TIER_TWO: 'TierTwo',
TIER_THREE: 'TierThree'
});
export default packageNames;

View file

@ -2,7 +2,11 @@
FRONT_WIPER: 'FRONT WIPER',
REAR_WIPER: 'REAR WIPER',
RAIN_DEFENSE: 'RAIN DEFENSE',
RECALIBRATION: 'RECALIBRATION'
RECALIBRATION: 'RECALIBRATION',
REPLACE_FEE: 'REPLACE FEE',
RECYCLE_FEE: 'RECYCLE FEE',
MOBILE_FEE: 'MOBILE FEE',
REPAIR_FEE: 'REPAIR FEE'
});
export default partTypeStrings;

View file

@ -106,10 +106,7 @@ import loader from '@/ux-components/loader/loader.vue';
import { useMainStore } from '@/store';
import { useField, ErrorMessage } from 'vee-validate';
import { deepClone } from '@/helpers/object-helper';
import {
convertDateToDateString,
convertDateStringToDate
} from '@/layouts/schedule-page/helpers/schedule-helper';
import { convertDateStringToDate, convertDateToDateString } from '@/helpers/date-helper';
import { TIMINGFUNC_MAP, BUFFER_OFFSET, MONTHS_OF_YEAR } from './mixins/constants';
import { selectableDaysOptions, requiredParameter } from './mixins/helpers';

View file

@ -1,4 +1,30 @@
const getDateDifferenceInDays = (startDate, endDate) => {
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 getDateDifferenceInDays(startDate, endDate) {
const date1 = new Date(endDate);
date1.setHours(0, 0, 0, 0);
const date2 = new Date(startDate);
@ -7,6 +33,42 @@ const getDateDifferenceInDays = (startDate, endDate) => {
const DifferenceInTime = date1.getTime() - date2.getTime();
// To calculate the no. of days between two dates
return DifferenceInTime / (1000 * 3600 * 24);
};
}
export default getDateDifferenceInDays;
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 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 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);
}

View file

@ -0,0 +1,238 @@
import partTypeStrings from '@/constants/part-type-strings';
import damageLocationsSelected from '@/constants/damage-locations-selected';
import packageNames from '@/constants/package-names';
export function findLineItemsWithPartType(typeToFind, itemsToSearch) {
const partTypeMatches = itemsToSearch?.filter((lineItem) => lineItem.partType.toUpperCase() === typeToFind.toUpperCase());
return partTypeMatches;
}
export function containsLineItemWithPartType(typeToFind, itemsToSearch) {
const partTypeMatches = findLineItemsWithPartType(typeToFind, itemsToSearch);
return !!partTypeMatches?.length;
}
export function getVapsLineItems(availableLineItems, vapTypes) {
const lineItems = [];
for (let i = 0; i < vapTypes.length; i++) {
lineItems.push(...findLineItemsWithPartType(vapTypes[i], availableLineItems));
}
return lineItems;
}
export function containsGlassPieceWithLocation(locationToFind, glassPiecesToSearch) {
const glassLocationMatches = glassPiecesToSearch?.filter((glassPiece) =>
glassPiece.glassLocation.toUpperCase() === locationToFind.toUpperCase());
return !!glassLocationMatches?.length;
}
export function shouldFrontWipersBeAvailable(
glassToReplace,
availableLineItems,
isRepair,
targetTier
) {
const frontWiperIsAvailable = containsLineItemWithPartType(
partTypeStrings.FRONT_WIPER,
availableLineItems
);
const isFrontWindshieldTask =
isRepair || containsGlassPieceWithLocation(damageLocationsSelected.WINDSHIELD, glassToReplace);
switch (targetTier) {
case packageNames.TIER_TWO:
return frontWiperIsAvailable && isFrontWindshieldTask;
case packageNames.TIER_THREE:
return frontWiperIsAvailable;
default:
return false;
}
}
export function shouldRearWipersBeAvailable(
glassToReplace,
availableLineItems,
isRepair,
targetTier
) {
const rearWiperIsAvailable = containsLineItemWithPartType(
partTypeStrings.REAR_WIPER,
availableLineItems
);
const frontWiperIsAvailable = containsLineItemWithPartType(
partTypeStrings.FRONT_WIPER,
availableLineItems
);
const isRearWindshieldTask = containsGlassPieceWithLocation(
damageLocationsSelected.REAR,
glassToReplace
);
switch (targetTier) {
case packageNames.TIER_TWO:
return rearWiperIsAvailable && isRearWindshieldTask;
case packageNames.TIER_THREE:
return rearWiperIsAvailable && (isRearWindshieldTask || !frontWiperIsAvailable);
default:
return false;
}
}
export function shouldRainDefenseBeAvailable(
glassToReplace,
availableLineItems,
isRepair,
targetTier
) {
const isTierThree = targetTier === packageNames.TIER_THREE;
const frontWipersInTierTwo = shouldFrontWipersBeAvailable(
glassToReplace,
availableLineItems,
isRepair,
packageNames.TIER_TWO
);
const frontWipersInTierThree = shouldFrontWipersBeAvailable(
glassToReplace,
availableLineItems,
isRepair,
packageNames.TIER_THREE
);
const rearWipersInTierTwo = shouldRearWipersBeAvailable(
glassToReplace,
availableLineItems,
isRepair,
packageNames.TIER_TWO
);
return isTierThree && !(rearWipersInTierTwo && !frontWipersInTierTwo && frontWipersInTierThree);
}
export function getPackageContents(glassToReplace, availableLineItems, isRepair, targetTier) {
const vaps = [];
if (shouldFrontWipersBeAvailable(glassToReplace, availableLineItems, isRepair, targetTier)) {
vaps.push(partTypeStrings.FRONT_WIPER);
}
if (shouldRearWipersBeAvailable(glassToReplace, availableLineItems, isRepair, targetTier)) {
vaps.push(partTypeStrings.REAR_WIPER);
}
if (shouldRainDefenseBeAvailable(glassToReplace, availableLineItems, isRepair, targetTier)) {
vaps.push(partTypeStrings.RAIN_DEFENSE);
}
return vaps;
}
export function getAvailablePackages(glassToReplace, availableLineItems, isRepair) {
const tierOneVaps = getPackageContents(
glassToReplace,
availableLineItems,
isRepair,
packageNames.TIER_ONE
);
const tierTwoVaps = getPackageContents(
glassToReplace,
availableLineItems,
isRepair,
packageNames.TIER_TWO
);
const tierThreeVaps = getPackageContents(
glassToReplace,
availableLineItems,
isRepair,
packageNames.TIER_THREE
);
if (tierTwoVaps.length === 0) {
return [
{
packageName: packageNames.TIER_ONE,
vapTypes: tierOneVaps
},
{
packageName: packageNames.TIER_THREE,
vapTypes: tierThreeVaps
}
];
}
return [
{
packageName: packageNames.TIER_ONE,
vapTypes: tierOneVaps
},
{
packageName: packageNames.TIER_TWO,
vapTypes: tierTwoVaps
},
{
packageName: packageNames.TIER_THREE,
vapTypes: tierThreeVaps
}
];
}
export function getLowestTierForType(glassToReplace, availableLineItems, isRepair, partType) {
const packages = getAvailablePackages(glassToReplace, availableLineItems, isRepair);
for (let i = 0; i < packages.length; i++) {
for (let j = 0; j < packages[i].vapTypes.length; j++) {
if (packages[i].vapTypes[j].toUpperCase() === partType.toUpperCase()) {
return packages[i].packageName;
}
}
}
return packageNames.TIER_ONE;
}
function maxTier(tierA, tierB) {
if (tierA === packageNames.TIER_THREE || tierB === packageNames.TIER_THREE) {
return packageNames.TIER_THREE;
}
if (tierA === packageNames.TIER_TWO || tierB === packageNames.TIER_TWO) {
return packageNames.TIER_TWO;
}
return packageNames.TIER_ONE;
}
export function getHighestRequiredTier(glassToReplace, availableLineItems, isRepair, vaps) {
let currentHighestTier = packageNames.TIER_ONE;
for (let i = 0; i < vaps.length; i++) {
const lowestTierForItem = getLowestTierForType(
glassToReplace,
availableLineItems,
isRepair,
vaps[i].partType
);
currentHighestTier = maxTier(currentHighestTier, lowestTierForItem);
}
return currentHighestTier;
}
export function getHighestFullySatisfiedTier(glassToReplace, availableLineItems, isRepair, vaps) {
const packages = getAvailablePackages(glassToReplace, availableLineItems, isRepair);
let highestSatisfiedPackage = packageNames.TIER_ONE;
for (let i = 0; i < packages.length; i++) {
let isPackageSatisfied = true;
for (let j = 0; j < packages[i].vapTypes.length; j++) {
isPackageSatisfied =
isPackageSatisfied && containsLineItemWithPartType(packages[i].vapTypes[j], vaps);
}
if (isPackageSatisfied) {
highestSatisfiedPackage = packages[i].packageName;
}
}
return highestSatisfiedPackage;
}

View file

@ -0,0 +1,44 @@
// Components
import reviewBlock from '@/layouts/review-page/review-block/review-block.vue';
// Supporting Files
import { shallowMount } from '@vue/test-utils';
import { getMountOptions } from '@/helpers/unit-test-helper.js';
// Mock fetchCmsContentForPage
jest.mock('@/helpers/cms-content-helper', () => ({
fetchCmsContentForPage: jest.fn()
}));
function getShallowMountedComponent(initialData = {}, methodToRun = () => {}) {
const mountOptions = getMountOptions({
router: {
navigate: jest.fn()
}
});
methodToRun();
mountOptions.data = () => (
initialData
);
const wrapper = shallowMount(reviewBlock, mountOptions);
return { wrapper };
}
describe('Review Content Block', () => {
test('Displays one new line of content for each element in the content prop.', () => {
// Arrange
const { wrapper } = getShallowMountedComponent({
headerCmsWidgetName: 'TestWidget',
content: ['a', 'b', 'c', 'd']
});
// Act
const contentLines = wrapper.findAll("[data-test='contentLine']");
// Assert
expect(contentLines.length).toBe(4);
});
});

View file

@ -0,0 +1,73 @@
<template>
<div class="py-3">
<div class="d-flex">
<textBlock
:cmsWidgetName="headerCmsWidgetName"
:customText="customHeaderText"
typeStyle="body small bold dark"
:marginTopSizeOverride="0" />
<textLink
linkType="textSmall"
text="Edit"
class="ml-auto"
useLoadingModal
href="javascript:void(0)"
@click-event="linkClicked">
<template
v-if="editScreenReaderTextCmsWidgetName"
#after-text>
<span class="sr-only"> {{ screenReaderOnlyText }} </span>
</template>
</textLink>
</div>
<div
v-for="item in content"
:key="item"
class="small review-block-content"
data-test="contentLine"
v-html="item"></div>
</div>
</template>
<script>
import textBlock from '@/digital-components/text-block/text-block.vue';
import textLink from '@/ux-components/text-link/text-link.vue';
export default {
name: 'review-block',
components: {
textBlock,
textLink
},
props: {
headerCmsWidgetName: String,
customHeaderText: String,
// Specifically an array of strings.
content: Array,
editScreenReaderTextCmsWidgetName: String
},
emits: ['edit-clicked'],
data() {
return {};
},
computed: {
screenReaderOnlyText() {
return this.getCmsContent(this.editScreenReaderTextCmsWidgetName, 'Text');
}
},
methods: {
linkClicked() {
this.$emit('edit-clicked');
}
}
};
</script>
<style lang="scss" scoped>
:deep(.review-block-content > ul) {
margin-bottom: 0rem;
}
.review-block-content {
width: 90%;
}
</style>

View file

@ -0,0 +1,386 @@
// Components
import review from '@/layouts/review-page/review-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';
// Mock fetchCmsContentForPage
jest.mock('@/helpers/cms-content-helper', () => ({
fetchCmsContentForPage: jest.fn()
}));
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,
loadingModal: loadingModalStub
};
methodToRun();
mountOptions.mixins = [mockMixin];
mountOptions.data = () => (
initialData
);
const wrapper = shallowMount(review, mountOptions);
return { wrapper };
}
beforeEach(() => {
const testingPinia = createTestingPinia({
initialState: {
main: {
order: {
vehicle: {
year: '2020',
make: 'Acura',
model: 'MDX',
style: '4 door sedan'
},
damage: {
isRepair: false,
numberOfChips: 2,
glassToReplace: ['dummy location value']
},
lineItems: {
glassParts: ['dummy part value'],
supportingItems: ['dummy supporting item'],
vaps: ['dummy vap']
},
serviceLocation: {
address: 'address 1',
address2: 'address 2',
city: 'city',
state: 'state',
zipCode: 'zip code',
appointmentType: 'Mobile',
provider: {
providerNumber: 1,
address: {
streetAddress: 'provider address 1',
city: 'provider city',
state: 'provider state',
zipCode: 'provider zip code'
}
}
},
schedule: {
date: 'date',
startTime: 'start',
endTime: 'end',
jobMinMinutes: '30',
jobMaxMinutes: '45'
},
customer: {
firstName: 'first name',
lastName: 'last name',
emailAddress: 'builddigitaltest@safelite.com',
phoneNumber: '555-555-5555',
isSmsOptIn: true
}
}
}
}
});
useMainStore(testingPinia);
});
afterEach(() => {
jest.clearAllMocks();
});
describe('Review Page', () => {
describe('arePagePrerequisitesValid', () => {
test('Returns true for baseline valid state', () => {
// Arrange
const { wrapper } = getShallowMountedComponent();
// Act
const isValid = wrapper.vm.arePagePrerequisitesValid();
// Assert
expect(isValid).toBe(true);
});
test('Returns false for empty state', () => {
// Arrange
const { wrapper } = getShallowMountedComponent();
wrapper.vm.mainStore.order = {
vehicle: {
year: null,
make: null,
model: null,
style: null,
carId: null,
category: null,
vin: null,
imageUrl: null,
imageVifNumber: null,
imageColor: null,
registration: {
licensePlate: null
}
},
serviceLocation: {
address: null,
address2: null,
city: null,
state: null,
zipCode: null,
zipCodeCtu: null,
appointmentType: null,
isVehicleProtected: null,
provider: {
providerNumber: null,
address: {
streetAddress: null,
city: null,
state: null,
zipCode: null,
zipCodeCtu: null
}
},
techNotes: null
},
customer: {
firstName: null,
lastName: null,
emailAddress: null,
phoneNumber: null,
isSmsOptIn: null
},
damage: {
isRepair: null,
numberOfChips: null,
glassToReplace: null,
partQuestionAnswers: null,
moldingQuestionAnswers: null,
capabilityQuestionAnswers: null
},
lineItems: {
glassParts: null,
supportingItems: null,
vaps: null,
serverData: null
},
payment: {
isInsurance: null,
insuranceCoverage: {
isVerified: null,
coverageStatus: null
},
parentAccountNumber: 0
},
schedule: {
date: null,
startTime: null,
endTime: null,
routeCode: null,
jobMaxMinutes: null,
jobMinMinutes: null
},
referralNumber: null,
referralSequenceNumber: null,
referralDate: null,
referralCorrelationId: null,
eon: null
};
// Act
const isValid = wrapper.vm.arePagePrerequisitesValid();
// Assert
expect(isValid).toBe(false);
});
describe('Damage requirements', () => {
test('Accepts null glassToReplace when is repair', () => {
// Arrange
const { wrapper } = getShallowMountedComponent();
wrapper.vm.mainStore.order.damage.isRepair = true;
wrapper.vm.mainStore.order.damage.glassToReplace = null;
wrapper.vm.mainStore.order.damage.numberOfChips = 1;
// Act
const isValid = wrapper.vm.arePagePrerequisitesValid();
// Assert
expect(isValid).toBe(true);
});
test('Rejects 0 chips when repair', () => {
// Arrange
const { wrapper } = getShallowMountedComponent();
wrapper.vm.mainStore.order.damage.isRepair = true;
wrapper.vm.mainStore.order.damage.numberOfChips = 0;
// Act
const isValid = wrapper.vm.arePagePrerequisitesValid();
// Assert
expect(isValid).toBe(false);
});
test('Rejects null chips when repair', () => {
// Arrange
const { wrapper } = getShallowMountedComponent();
wrapper.vm.mainStore.order.damage.isRepair = true;
wrapper.vm.mainStore.order.damage.numberOfChips = null;
// Act
const isValid = wrapper.vm.arePagePrerequisitesValid();
// Assert
expect(isValid).toBe(false);
});
test('Accepts null chips when not repair', () => {
// Arrange
const { wrapper } = getShallowMountedComponent();
wrapper.vm.mainStore.order.damage.isRepair = false;
wrapper.vm.mainStore.order.damage.numberOfChips = null;
// Act
const isValid = wrapper.vm.arePagePrerequisitesValid();
// Assert
expect(isValid).toBe(true);
});
test('Rejects empty glassToReplace when not repair', () => {
// Arrange
const { wrapper } = getShallowMountedComponent();
wrapper.vm.mainStore.order.damage.isRepair = false;
wrapper.vm.mainStore.order.damage.glassToReplace = null;
// Act
const isValid = wrapper.vm.arePagePrerequisitesValid();
// Assert
expect(isValid).toBe(false);
});
test('Rejects null glassToReplace when not repair', () => {
// Arrange
const { wrapper } = getShallowMountedComponent();
wrapper.vm.mainStore.order.damage.isRepair = false;
wrapper.vm.mainStore.order.damage.glassToReplace = [];
// Act
const isValid = wrapper.vm.arePagePrerequisitesValid();
// Assert
expect(isValid).toBe(false);
});
});
describe('Package requirements', () => {
test('Accepts null glassParts when is repair', () => {
// Arrange
const { wrapper } = getShallowMountedComponent();
wrapper.vm.mainStore.order.damage.isRepair = true;
wrapper.vm.mainStore.order.lineItems.glassParts = null;
// Act
const isValid = wrapper.vm.arePagePrerequisitesValid();
// Assert
expect(isValid).toBe(true);
});
test('Rejects null glassParts when not repair', () => {
// Arrange
const { wrapper } = getShallowMountedComponent();
wrapper.vm.mainStore.order.damage.isRepair = false;
wrapper.vm.mainStore.order.lineItems.glassParts = null;
// Act
const isValid = wrapper.vm.arePagePrerequisitesValid();
// Assert
expect(isValid).toBe(false);
});
});
describe('Service Location requirements', () => {
test('Accepts null provider address when mobile appointment', () => {
// Arrange
const { wrapper } = getShallowMountedComponent();
wrapper.vm.mainStore.order.serviceLocation.appointmentType = 'Mobile';
wrapper.vm.mainStore.order.serviceLocation.provider.address = {};
// Act
const isValid = wrapper.vm.arePagePrerequisitesValid();
// Assert
expect(isValid).toBe(true);
});
test('Reject null provider address when non-mobile appointment', () => {
// Arrange
const { wrapper } = getShallowMountedComponent();
wrapper.vm.mainStore.order.serviceLocation.appointmentType = 'Inshop';
wrapper.vm.mainStore.order.serviceLocation.provider.address = {};
// Act
const isValid = wrapper.vm.arePagePrerequisitesValid();
// Assert
expect(isValid).toBe(false);
});
test('Accepts null service location address when non-mobile appointment', () => {
// Arrange
const { wrapper } = getShallowMountedComponent();
wrapper.vm.mainStore.order.serviceLocation.appointmentType = 'Inshop';
wrapper.vm.mainStore.order.serviceLocation.address = null;
wrapper.vm.mainStore.order.serviceLocation.address2 = null;
wrapper.vm.mainStore.order.serviceLocation.zipCode = null;
wrapper.vm.mainStore.order.serviceLocation.city = null;
wrapper.vm.mainStore.order.serviceLocation.state = null;
// Act
const isValid = wrapper.vm.arePagePrerequisitesValid();
// Assert
expect(isValid).toBe(true);
});
test('Rejects null service location address when mobile appointment', () => {
// Arrange
const { wrapper } = getShallowMountedComponent();
wrapper.vm.mainStore.order.serviceLocation.appointmentType = 'Mobile';
wrapper.vm.mainStore.order.serviceLocation.address = null;
wrapper.vm.mainStore.order.serviceLocation.address2 = null;
wrapper.vm.mainStore.order.serviceLocation.zipCode = null;
wrapper.vm.mainStore.order.serviceLocation.city = null;
wrapper.vm.mainStore.order.serviceLocation.state = null;
// Act
const isValid = wrapper.vm.arePagePrerequisitesValid();
// Assert
expect(isValid).toBe(false);
});
});
});
});

View file

@ -1,41 +1,134 @@
<template>
<Form
ref="theForm"
v-slot="{ meta }"
@submit="onSubmit"
@invalidSubmit="onInvalidSubmit">
<!-- When customer-details is added: v-slot="{ meta }" -->
<div class="page-container-grouped-styles">
<div class="fade-on-route-transition position-relative">
<siteHeader cmsWidgetName="SiteHeaderWidget" />
<div class="container-fluid pb-2 review">
<p>Placeholder for review page</p>
<siteFooter
ref="siteFooter"
cmsWidgetName="SiteFooterWidget"
:isForwardActionDisabled="!meta.valid"
@ForwardClicked="navigateForward"
@backClicked="backButtonAction" />
<siteHeader cmsWidgetName="SiteHeaderWidget" />
<div class="main-content-container">
<vehicleBanner
cmsWidgetName="VehicleBannerWidget"
class="mb-4"
:displayGenericVehicleImage="false" />
<textBlock
:customText="subHeaderTitle"
typeStyle="h5"
justifyText="center"
margin="mt-1"
class="dark-header" />
<textBlock
:customText="subHeaderBody"
typeStyle="body"
justifyText="left"
class="mb-4"
margin="mt-0 mb-2" />
<buttonMain
ref="buttonMain"
:buttonText="forwardButtonText"
isPrimary
loaderColor="white"
class="mb-2 w-100"
@clickEvent="forwardButtonAction" />
<div>
<hr />
</div>
<textBlock
customText="Appointment Details"
typeStyle="label bold"
justifyText="justify-text-left"
margin="mt-0"
class="dark-header" />
<div class="px-4">
<vehicleReview
cmsWidgetName="VehicleReviewWidget"
:vehicle="vehicleInfo"
@editClicked="editVehicle" />
<hr class="my-0" />
<damageReview
cmsWidgetName="DamageReviewWidget"
damageLocationsWidgetName="DamageLocationsWidget"
:damage="damageInfo"
@editClicked="editDamage" />
<hr class="my-0" />
<servicePackageReview
ref="servicePackageReview"
servicePackageOptionsCmsName="ServicePackageTitle"
defaultPackageItemsCmsName="DefaultPackageItemDescriptions"
vapsItemsCmsName="VapsItemDescriptions"
:damage="damageInfo"
:lineItems="lineItems"
@editClicked="editServicePackage" />
<hr class="my-0" />
<serviceLocationReview
cmsWidgetName="ServiceLocationTitleWidget"
:serviceLocation="serviceLocationInfo"
@editClicked="editServiceLocation" />
<hr class="my-0" />
<scheduleReview
cmsWidgetName="ScheduleWidget"
:appointmentType="appointmentType"
@editClicked="editSchedule" />
<hr class="my-0" />
<customerReview
cmsWidgetName="CustomerReviewWidget"
:customer="customerInfo"
@editClicked="editCustomerDetails" />
</div>
<div>
<hr class="my-0" />
</div>
<siteFooter
ref="siteFooter"
class="mt-5"
cmsWidgetName="SiteFooterWidget"
@ForwardClicked="forwardButtonAction"
@backClicked="backButtonAction" />
</div>
</div>
</Form>
</template>
<script>
// Components
import siteHeader from '@/iss-components/site-header/site-header.vue';
import siteFooter from '@/iss-components/site-footer/site-footer.vue';
import vehicleBanner from '@/iss-components/vehicle-banner/vehicle-banner.vue';
import buttonMain from '@/ux-components/button-main/button-main.vue';
import textBlock from '@/digital-components/text-block/text-block.vue';
import customerReview from '@/layouts/review-page/review-sections/customer-review/customer-review.vue';
import damageReview from '@/layouts/review-page/review-sections/damage-review/damage-review.vue';
import scheduleReview from '@/layouts/review-page/review-sections/schedule-review/schedule-review.vue';
import serviceLocationReview from '@/layouts/review-page/review-sections/service-location-review/service-location-review.vue';
import servicePackageReview from '@/layouts/review-page/review-sections/service-package-review/service-package-review.vue';
import vehicleReview from '@/layouts/review-page/review-sections/vehicle-review/vehicle-review.vue';
// Supporting files
import { AppointmentTypeStrings } from '@/constants/schedule-constants';
import { fetchCmsContentForPage } from '@/helpers/cms-content-helper';
import navigationScenarios from '@/router/router-constants/navigation-scenarios.js';
import settleAllPromises from '@/helpers/layout-helper';
import { Form } from 'vee-validate';
import BaseFormMixin from '@/mixins/base-form-mixin.js';
import { useMainStore } from '@/store';
// Validation
import { Form } from 'vee-validate';
export default {
name: 'review-page',
components: {
buttonMain,
customerReview,
damageReview,
scheduleReview,
servicePackageReview,
serviceLocationReview,
siteHeader,
siteFooter,
textBlock,
vehicleBanner,
vehicleReview,
// eslint-disable-next-line vue/no-reserved-component-names
Form
},
@ -43,13 +136,13 @@ export default {
async beforeRouteEnter(to, from, next) {
// Call APIs
const cmsContentPromise = fetchCmsContentForPage(to.query.issPage);
// Settle promises and get results
const promiseResultMap = [
{
resultKey: 'cmsContent',
promise: cmsContentPromise
}];
// use resultMap to populate layout content.
const resultMap = await settleAllPromises(promiseResultMap);
next((vm) => {
vm.setCmsContent(resultMap.cmsContent);
@ -61,23 +154,170 @@ export default {
},
data() {
},
computed: {
subHeaderTitle() {
return this.getCmsContent('SiteSubHeaderWidget', 'HeaderText');
},
subHeaderBody() {
return this.getCmsContent('SiteSubHeaderWidget', 'BodyText');
},
forwardButtonText() {
return this.getCmsContent('SiteFooterWidget', 'ForwardButtonText');
},
vehicleInfo() {
return useMainStore().vehicle;
},
damageInfo() {
return useMainStore().damage;
},
lineItems() {
return useMainStore().lineItems;
},
serviceLocationInfo() {
return useMainStore().order.serviceLocation;
},
appointmentType() {
return useMainStore().order.serviceLocation.appointmentType;
},
customerInfo() {
return useMainStore().order.customer;
}
},
methods: {
arePagePrerequisitesValid() {
// Vehicle
const { vehicle } = useMainStore().order;
const vehicleReqs = !!(vehicle.year && vehicle.make && vehicle.model && vehicle.style);
// Damage
const { damage } = useMainStore().order;
const damageReqs = !!(
(damage.isRepair && damage.numberOfChips)
|| (!damage.isRepair && damage.glassToReplace?.length)
);
// Service Package
const { lineItems } = useMainStore().order;
// damageReqs handles checking for damage, even though it is also required for this section.
const packageReqs = !!(
(damage.isRepair || lineItems.glassParts)
&& lineItems.supportingItems
);
// Service Location
const { serviceLocation } = useMainStore().order;
const mobileReqs = !!(
serviceLocation.address
&& serviceLocation.city
&& serviceLocation.state
&& serviceLocation.zipCode
);
const providerLocation = serviceLocation.provider.address;
const dropOffInshopReqs = !!(
providerLocation.streetAddress
&& providerLocation.city
&& providerLocation.state
&& providerLocation.zipCode
);
const isMobile = serviceLocation.appointmentType === AppointmentTypeStrings.MOBILE;
const serviceLocationReqs =
(isMobile && mobileReqs) || (!isMobile && dropOffInshopReqs);
// Schedule
const { schedule } = useMainStore().order;
const scheduleReqs = !!(
schedule.date
&& schedule.startTime
&& schedule.endTime
&& schedule.jobMaxMinutes
&& schedule.jobMinMinutes
);
// Customer
const { customer } = useMainStore().order;
const customerReqs = !!(
customer.firstName
&& customer.lastName
&& customer.phoneNumber
&& customer.emailAddress
);
return (
vehicleReqs
&& damageReqs
&& packageReqs
&& serviceLocationReqs
&& scheduleReqs
&& customerReqs
);
},
backButtonAction() {
this.$router.navigate(this.navigationScenarios.CLICKED_BACK, this.$route);
this.$router.navigate(navigationScenarios.CLICKED_BACK, this.$route);
},
editVehicle() {
this.$router.navigateWithoutSaving(
navigationScenarios.CLICKED_VEHICLE_EDIT,
this.$route
);
},
editDamage() {
this.$router.navigateWithoutSaving(
navigationScenarios.CLICKED_DAMAGE_EDIT,
this.$route
);
},
editServicePackage() {
this.$router.navigateWithoutSaving(
navigationScenarios.CLICKED_SERVICE_PACKAGE_EDIT,
this.$route
);
},
editServiceLocation() {
this.$router.navigateWithoutSaving(
navigationScenarios.CLICKED_SERVICE_LOCATION_EDIT,
this.$route
);
},
editSchedule() {
this.$router.navigateWithoutSaving(
navigationScenarios.CLICKED_SCHEDULE_EDIT,
this.$route
);
},
editCustomerDetails() {
this.$router.navigateWithoutSaving(
navigationScenarios.CLICKED_CUSTOMER_EDIT,
this.$route
);
},
forwardButtonAction() {
this.navigateForward();
},
navigateForward() {
this.$router.navigate(
this.navigationScenarios.CLICKED_FORWARD,
this.$router.navigateWithoutSaving(
navigationScenarios.CLICKED_FORWARD,
this.$route
);
}
}
};
</script>
<style lang="scss" scoped>
.review p{
margin-bottom:0.75rem;
$page-side-padding: 1.5rem;
.page-container-grouped-styles {
overflow: auto;
.main-content-container {
padding: 0 1.5rem !important;
}
}
.dark-header {
color: $black;
line-height: 1.6;
}
</style>

View file

@ -0,0 +1,129 @@
// Components
import customerReview from '@/layouts/review-page/review-sections/customer-review/customer-review.vue';
// Supporting Files
import { shallowMount } from '@vue/test-utils';
import { getMountOptions } from '@/helpers/unit-test-helper.js';
const testConstants = {
cms: {
header: {
text: 'Header'
},
sms: {
text: 'Sms'
}
},
customer: {
firstName: 'First',
lastName: 'Last',
phoneNumber: '111-111-1111',
emailAddress: 'builddigitaltest@safelite.com',
isSmsOptIn: false
},
displayContent: {
fullName: 'First Last',
phoneNumber: '111-111-1111',
emailAddress: 'builddigitaltest@safelite.com',
smsOptIn: 'Sms'
}
};
function generateDefaultProps() {
return {
cmsWidgetName: 'CustomerWidget',
customer: {
firstName: testConstants.customer.firstName,
lastName: testConstants.customer.lastName,
phoneNumber: testConstants.customer.phoneNumber,
emailAddress: testConstants.customer.emailAddress,
isSmsOptIn: testConstants.customer.isSmsOptIn
}
};
}
let cmsContent;
const mockMixin = {
methods: {
getCmsContent: jest.fn((widgetName, cmsFieldName) => cmsContent?.[widgetName]?.[cmsFieldName] ?? '')
}
};
function getShallowMountedComponent(initialData = {}, methodToRun = () => {}) {
const mountOptions = getMountOptions({
router: {
navigate: jest.fn()
}
});
methodToRun();
mountOptions.data = () => (
initialData
);
mountOptions.mixins = [mockMixin];
const wrapper = shallowMount(customerReview, mountOptions);
return { wrapper };
}
beforeEach(() => {
cmsContent = {
CustomerWidget: {
HeaderText: testConstants.cms.header.text,
SubheaderText: testConstants.cms.sms.text
}
};
});
describe('Customer Review Block', () => {
test('Should display header text from cms', async () => {
// Arrange
const props = generateDefaultProps();
const { wrapper } = getShallowMountedComponent({
...props
});
// Act
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.header).toEqual(testConstants.cms.header.text);
});
test('Should display sms text from cms', async () => {
// Arrange
const props = generateDefaultProps();
const { wrapper } = getShallowMountedComponent({
...props
});
// Act
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.smsOptIn).toEqual(testConstants.cms.sms.text);
});
test('Should render correct display content', async () => {
// Arrange
const props = generateDefaultProps();
const { wrapper } = getShallowMountedComponent({
...props
});
// Act
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.displayContent).toEqual([
testConstants.displayContent.fullName,
testConstants.displayContent.emailAddress,
testConstants.displayContent.phoneNumber,
testConstants.displayContent.smsOptIn
]);
});
});

View file

@ -0,0 +1,51 @@
<template>
<reviewBlock
editScreenReaderTextCmsWidgetName="EditContactDetailsScreenReader"
:customHeaderText="header"
:content="displayContent"
@editClicked="editClicked" />
</template>
<script>
import reviewBlock from '@/layouts/review-page/review-block/review-block.vue';
export default {
name: 'customer-review',
components: {
reviewBlock
},
props: {
cmsWidgetName: String,
customer: Object
},
emits: ['edit-clicked'],
data() {
return {};
},
computed: {
displayContent() {
return [this.fullName, this.email, this.phoneNumber, this.smsOptIn];
},
header() {
return this.getCmsContent(this.cmsWidgetName, 'HeaderText');
},
fullName() {
return `${this.customer?.firstName} ${this.customer?.lastName}`;
},
email() {
return this.customer?.emailAddress;
},
phoneNumber() {
return this.customer?.phoneNumber;
},
smsOptIn() {
return this.getCmsContent(this.cmsWidgetName, 'SubheaderText');
}
},
methods: {
editClicked() {
this.$emit('edit-clicked');
}
}
};
</script>

View file

@ -0,0 +1,436 @@
// Components
import damageReview from '@/layouts/review-page/review-sections/damage-review/damage-review.vue';
// Supporting Files
import { shallowMount } from '@vue/test-utils';
import { getMountOptions } from '@/helpers/unit-test-helper.js';
import damageLocationsSelected from '@/constants/damage-locations-selected';
const testConstants = {
cmsConstants: {
widgetNames: {
header: 'DamageReviewWidget',
locations: 'DamageLocationsWidget',
driverDamages: 'DriverDamagesWidget',
passengerDamages: 'PassengerDamagesWidget'
},
header: {
text: 'Damage'
},
damageLocations: {
windshield: damageLocationsSelected.WINDSHIELD,
driver: damageLocationsSelected.DRIVER,
passenger: damageLocationsSelected.PASSENGER,
rear: damageLocationsSelected.REAR
},
damageNames: {
vent: damageLocationsSelected.VENT,
front: damageLocationsSelected.FRONT,
back: damageLocationsSelected.BACK,
quarter: damageLocationsSelected.QUARTER,
side: damageLocationsSelected.SIDEDOOR
},
locationCopy: {
windshield: 'Windshield copy',
driver: 'Driver copy',
passenger: 'Passenger copy',
rear: 'Rear copy'
},
damageCopy: {
vent: 'Vent copy',
front: 'Front copy',
back: 'Back copy',
quarter: 'Quarter copy',
side: 'Side copy'
},
imageId: '00000000-0000-0000-0000-000000000000'
},
makeBulletedList: (items) => {
let list = '<ul>';
items.forEach((item) => {
list += `<li>${item}</li>`;
});
list += '</ul>';
return list;
},
glassItems: {
windshield: {
glassLocation: damageLocationsSelected.WINDSHIELD,
glassName: damageLocationsSelected.SINGLE
},
rear: {
glassLocation: damageLocationsSelected.REAR,
glassName: damageLocationsSelected.STATIONARY
},
passengerItems: {
vent: {
glassLocation: damageLocationsSelected.PASSENGER,
glassName: damageLocationsSelected.VENT
},
front: {
glassLocation: damageLocationsSelected.PASSENGER,
glassName: damageLocationsSelected.FRONT
},
back: {
glassLocation: damageLocationsSelected.PASSENGER,
glassName: damageLocationsSelected.BACK
},
quarter: {
glassLocation: damageLocationsSelected.PASSENGER,
glassName: damageLocationsSelected.QUARTER
},
side: {
glassLocation: damageLocationsSelected.PASSENGER,
glassName: damageLocationsSelected.SIDEDOOR
}
},
driverItems: {
vent: {
glassLocation: damageLocationsSelected.DRIVER,
glassName: damageLocationsSelected.VENT
},
front: {
glassLocation: damageLocationsSelected.DRIVER,
glassName: damageLocationsSelected.FRONT
},
back: {
glassLocation: damageLocationsSelected.DRIVER,
glassName: damageLocationsSelected.BACK
},
quarter: {
glassLocation: damageLocationsSelected.DRIVER,
glassName: damageLocationsSelected.QUARTER
},
side: {
glassLocation: damageLocationsSelected.DRIVER,
glassName: damageLocationsSelected.SIDEDOOR
}
}
}
};
let cmsContent;
const mockMixin = {
methods: {
getCmsContent: jest.fn((widgetName, cmsFieldName) => cmsContent?.[widgetName]?.[cmsFieldName] ?? '')
}
};
function getShallowMountedComponent(initialData = {}, methodToRun = () => {}) {
const mountOptions = getMountOptions({
router: {
navigate: jest.fn()
}
});
methodToRun();
mountOptions.data = () => (
initialData
);
mountOptions.mixins = [mockMixin];
const wrapper = shallowMount(damageReview, mountOptions);
return { wrapper };
}
beforeEach(() => {
cmsContent = {
DamageReviewWidget: {
Text: testConstants.cmsConstants.header.text
},
DamageLocationsWidget: {
Answers: [
{
Name: testConstants.cmsConstants.damageLocations.windshield,
Text: testConstants.cmsConstants.locationCopy.windshield,
SubText: '',
ImageId: testConstants.cmsConstants.imageId,
Image: '',
SubWidgetName: ''
},
{
Name: testConstants.cmsConstants.damageLocations.driver,
Text: testConstants.cmsConstants.locationCopy.driver,
SubText: '',
ImageId: testConstants.cmsConstants.imageId,
Image: '',
SubWidgetName: testConstants.cmsConstants.widgetNames.driverDamages
},
{
Name: testConstants.cmsConstants.damageLocations.passenger,
Text: testConstants.cmsConstants.locationCopy.passenger,
SubText: '',
ImageId: testConstants.cmsConstants.imageId,
Image: '',
SubWidgetName: testConstants.cmsConstants.widgetNames.passengerDamages
},
{
Name: testConstants.cmsConstants.damageLocations.rear,
Text: testConstants.cmsConstants.locationCopy.rear,
SubText: '',
ImageId: testConstants.cmsConstants.imageId,
Image: '',
SubWidgetName: ''
}
]
},
DriverDamagesWidget: {
Answers: [
{
Name: testConstants.cmsConstants.damageNames.vent,
Text: testConstants.cmsConstants.damageCopy.vent,
SubText: '',
ImageId: testConstants.cmsConstants.imageId,
Image: '',
SubWidgetName: ''
},
{
Name: testConstants.cmsConstants.damageNames.front,
Text: testConstants.cmsConstants.damageCopy.front,
SubText: '',
ImageId: testConstants.cmsConstants.imageId,
Image: '',
SubWidgetName: ''
},
{
Name: testConstants.cmsConstants.damageNames.back,
Text: testConstants.cmsConstants.damageCopy.back,
SubText: '',
ImageId: testConstants.cmsConstants.imageId,
Image: '',
SubWidgetName: ''
},
{
Name: testConstants.cmsConstants.damageNames.quarter,
Text: testConstants.cmsConstants.damageCopy.quarter,
SubText: '',
ImageId: testConstants.cmsConstants.imageId,
Image: '',
SubWidgetName: ''
},
{
Name: testConstants.cmsConstants.damageNames.side,
Text: testConstants.cmsConstants.damageCopy.side,
SubText: '',
ImageId: testConstants.cmsConstants.imageId,
Image: '',
SubWidgetName: ''
}
]
},
PassengerDamagesWidget: {
Answers: [
{
Name: testConstants.cmsConstants.damageNames.vent,
Text: testConstants.cmsConstants.damageCopy.vent,
SubText: '',
ImageId: testConstants.cmsConstants.imageId,
Image: '',
SubWidgetName: ''
},
{
Name: testConstants.cmsConstants.damageNames.front,
Text: testConstants.cmsConstants.damageCopy.front,
SubText: '',
ImageId: testConstants.cmsConstants.imageId,
Image: '',
SubWidgetName: ''
},
{
Name: testConstants.cmsConstants.damageNames.back,
Text: testConstants.cmsConstants.damageCopy.back,
SubText: '',
ImageId: testConstants.cmsConstants.imageId,
Image: '',
SubWidgetName: ''
},
{
Name: testConstants.cmsConstants.damageNames.quarter,
Text: testConstants.cmsConstants.damageCopy.quarter,
SubText: '',
ImageId: testConstants.cmsConstants.imageId,
Image: '',
SubWidgetName: ''
},
{
Name: testConstants.cmsConstants.damageNames.side,
Text: testConstants.cmsConstants.damageCopy.side,
SubText: '',
ImageId: testConstants.cmsConstants.imageId,
Image: '',
SubWidgetName: ''
}
]
}
};
});
describe('Damage Review Block', () => {
describe('Correctly assembles damage info into a display string', () => {
test('Shows windshield copy when windshield damage is included', async () => {
// Arrange
const { wrapper } = getShallowMountedComponent({
cmsWidgetName: testConstants.cmsConstants.widgetNames.header,
damageLocationsWidgetName: testConstants.cmsConstants.widgetNames.locations,
damage: {
isRepair: false,
glassToReplace: [testConstants.glassItems.windshield]
}
});
// Act
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.displayContent).toStrictEqual([
testConstants.cmsConstants.locationCopy.windshield
]);
});
test('Windshield copy is shown when order is a repair', async () => {
// Arrange
const { wrapper } = getShallowMountedComponent({
cmsWidgetName: testConstants.cmsConstants.widgetNames.header,
damageLocationsWidgetName: testConstants.cmsConstants.widgetNames.locations,
damage: {
isRepair: true,
numberOfChips: 2,
glassToReplace: []
}
});
// Act
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.displayContent).toStrictEqual([
testConstants.cmsConstants.locationCopy.windshield
]);
});
test('Rear windshield copy shows when rear damage is present', async () => {
// Arrange
const { wrapper } = getShallowMountedComponent({
cmsWidgetName: testConstants.cmsConstants.widgetNames.header,
damageLocationsWidgetName: testConstants.cmsConstants.widgetNames.locations,
damage: {
isRepair: false,
numberOfChips: null,
glassToReplace: [testConstants.glassItems.rear]
}
});
// Act
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.displayContent).toStrictEqual([
testConstants.cmsConstants.locationCopy.rear
]);
});
test('Driver side copy and items are shown when driver side damage is present', async () => {
// Arrange
const { wrapper } = getShallowMountedComponent({
cmsWidgetName: testConstants.cmsConstants.widgetNames.header,
damageLocationsWidgetName: testConstants.cmsConstants.widgetNames.locations,
damage: {
isRepair: false,
numberOfChips: null,
glassToReplace: [
testConstants.glassItems.driverItems.back,
testConstants.glassItems.driverItems.front
]
}
});
// Act
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.displayContent).toStrictEqual([
testConstants.cmsConstants.locationCopy.driver,
testConstants.makeBulletedList([
testConstants.cmsConstants.damageCopy.front,
testConstants.cmsConstants.damageCopy.back
])
]);
});
test('Passenger side copy and items are shown when passenger side damage is present', async () => {
// Arrange
const { wrapper } = getShallowMountedComponent({
cmsWidgetName: testConstants.cmsConstants.widgetNames.header,
damageLocationsWidgetName: testConstants.cmsConstants.widgetNames.locations,
damage: {
isRepair: false,
numberOfChips: null,
glassToReplace: [
testConstants.glassItems.passengerItems.quarter,
testConstants.glassItems.passengerItems.vent,
testConstants.glassItems.passengerItems.side
]
}
});
// Act
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.displayContent).toStrictEqual([
testConstants.cmsConstants.locationCopy.passenger,
testConstants.makeBulletedList([
testConstants.cmsConstants.damageCopy.vent,
testConstants.cmsConstants.damageCopy.quarter,
testConstants.cmsConstants.damageCopy.side
])
]);
});
test('All relevant sections are shown in order in multiglass scenario', async () => {
// Arrange
const { wrapper } = getShallowMountedComponent({
cmsWidgetName: testConstants.cmsConstants.widgetNames.header,
damageLocationsWidgetName: testConstants.cmsConstants.widgetNames.locations,
damage: {
isRepair: false,
numberOfChips: null,
glassToReplace: [
testConstants.glassItems.windshield,
testConstants.glassItems.rear,
testConstants.glassItems.driverItems.vent,
testConstants.glassItems.driverItems.front,
testConstants.glassItems.driverItems.back,
testConstants.glassItems.passengerItems.quarter,
testConstants.glassItems.passengerItems.back,
testConstants.glassItems.passengerItems.side
]
}
});
// Act
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.displayContent).toStrictEqual([
testConstants.cmsConstants.locationCopy.windshield,
testConstants.cmsConstants.locationCopy.driver,
testConstants.makeBulletedList([
testConstants.cmsConstants.damageCopy.vent,
testConstants.cmsConstants.damageCopy.front,
testConstants.cmsConstants.damageCopy.back
]),
testConstants.cmsConstants.locationCopy.passenger,
testConstants.makeBulletedList([
testConstants.cmsConstants.damageCopy.back,
testConstants.cmsConstants.damageCopy.quarter,
testConstants.cmsConstants.damageCopy.side
]),
testConstants.cmsConstants.locationCopy.rear
]);
});
});
});

View file

@ -0,0 +1,134 @@
<template>
<reviewBlock
editScreenReaderTextCmsWidgetName="EditDamageScreenReader"
:headerCmsWidgetName="cmsWidgetName"
:content="displayContent"
@editClicked="editClicked" />
</template>
<script>
import damageLocationsSelected from '@/constants/damage-locations-selected.js';
import reviewBlock from '@/layouts/review-page/review-block/review-block.vue';
export default {
name: 'damage-review',
components: {
reviewBlock
},
props: {
cmsWidgetName: String,
damageLocationsWidgetName: String,
damage: Object
},
emits: ['edit-clicked'],
data() {
return {};
},
computed: {
displayContent() {
return [
...this.windshieldCopy,
...this.driverSideCopy,
...this.passengerSideCopy,
...this.rearCopy
];
},
hasWindshieldDamage() {
const windshieldPieces = this.getGlassPieces(damageLocationsSelected.WINDSHIELD);
return this.damage.isRepair || !!windshieldPieces?.length;
},
hasDriverSideDamage() {
const driverPieces = this.getGlassPieces(damageLocationsSelected.DRIVER);
return !!driverPieces?.length;
},
hasPassengerSideDamage() {
const passengerPieces = this.getGlassPieces(damageLocationsSelected.PASSENGER);
return !!passengerPieces?.length;
},
hasRearDamage() {
const rearPieces = this.getGlassPieces(damageLocationsSelected.REAR);
return !!rearPieces?.length;
},
windshieldCopy() {
const answerContent = this.getLocationAnswer(damageLocationsSelected.WINDSHIELD);
if (this.hasWindshieldDamage) {
return [answerContent?.Text];
}
return [];
},
driverSideCopy() {
const answerContent = this.getLocationAnswer(damageLocationsSelected.DRIVER);
const damageAnswers = this.getAnswersNullSafe(answerContent?.SubWidgetName);
const driverSideItems = this.getGlassPieces(damageLocationsSelected.DRIVER);
const damageAnswersOnOrder = damageAnswers?.filter((answer) =>
driverSideItems?.some((glassPiece) => answer.Name === glassPiece.glassName));
if (this.hasDriverSideDamage) {
return [
answerContent?.Text,
this.generateBulletedListFromAnswers(damageAnswersOnOrder)
];
}
return [];
},
passengerSideCopy() {
const answerContent = this.getLocationAnswer(damageLocationsSelected.PASSENGER);
const damageAnswers = this.getAnswersNullSafe(answerContent?.SubWidgetName);
const passengerSideItems = this.getGlassPieces(damageLocationsSelected.PASSENGER);
const damageAnswersOnOrder = damageAnswers?.filter((answer) =>
passengerSideItems?.some((glassPiece) => answer.Name === glassPiece.glassName));
if (this.hasPassengerSideDamage) {
return [
answerContent?.Text,
this.generateBulletedListFromAnswers(damageAnswersOnOrder)
];
}
return [];
},
rearCopy() {
const answerContent = this.getLocationAnswer(damageLocationsSelected.REAR);
if (this.hasRearDamage) {
return [answerContent?.Text];
}
return [];
},
locationAnswers() {
return this.getAnswersNullSafe(this.damageLocationsWidgetName);
}
},
methods: {
editClicked() {
this.$emit('edit-clicked');
},
getAnswersNullSafe(widgetName) {
const rawAnswers = this.getCmsContent(widgetName, 'Answers');
return rawAnswers || [];
},
getLocationAnswer(damageLocation) {
return this.locationAnswers?.find((answer) => answer.Name === damageLocation);
},
getGlassPieces(damageLocation) {
return this.damage?.glassToReplace?.filter((item) => item.glassLocation === damageLocation);
},
generateBulletedListFromAnswers(answers) {
let list = '<ul>';
answers.forEach((answer) => {
list += `<li>${answer.Text}</li>`;
});
list += '</ul>';
return list;
}
}
};
</script>

View file

@ -0,0 +1,42 @@
<template>
<reviewBlock
editScreenReaderTextCmsWidgetName="EditAppointmentScreenReader"
:customHeaderText="headerText"
:content="displayContent"
@editClicked="editClicked" />
</template>
<script>
import reviewBlock from '@/layouts/review-page/review-block/review-block.vue';
export default {
name: 'schedule-review',
components: {
reviewBlock
},
props: {
cmsWidgetName: String,
appointmentType: String
},
emits: ['edit-clicked'],
data() {
return {};
},
computed: {
displayContent() {
return [
this.getCmsContent(this.cmsWidgetName, 'BodyText'),
this.getCmsContent(this.cmsWidgetName, 'BodyText2')
];
},
headerText() {
return this.getCmsContent(this.cmsWidgetName, 'HeaderText');
}
},
methods: {
editClicked() {
this.$emit('edit-clicked');
}
}
};
</script>

View file

@ -0,0 +1,134 @@
// Components
import serviceLocationReview from '@/layouts/review-page/review-sections/service-location-review/service-location-review.vue';
// Supporting Files
import { shallowMount } from '@vue/test-utils';
import { getMountOptions } from '@/helpers/unit-test-helper.js';
import { AppointmentTypeStrings } from '@/constants/schedule-constants';
const cmsContent = {
ServiceLocationTitleWidget: {
Text: 'Title Text'
}
};
const mockMixin = {
methods: {
getCmsContent: jest.fn((widgetName, cmsFieldName) => cmsContent?.[widgetName]?.[cmsFieldName] ?? '')
}
};
function generateDefaultProps() {
return {
cmsWidgetName: 'ServiceLocationTitleWidget',
serviceLocation: {
address: 'Mobile Address 1',
address2: 'Mobile Address 2',
city: 'Mobile City',
state: 'MO',
zipCode: '11111',
zipCodeCtu: '',
appointmentType: AppointmentTypeStrings.MOBILE,
isVehicleProtected: false,
provider: {
providerNumber: '',
address: {
streetAddress: 'Service Location Address',
city: 'Service Location City',
state: 'SL',
zipCode: '22222',
zipCodeCtu: ''
}
}
}
};
}
function getShallowMountedComponent(initialData = {}, methodToRun = () => {}) {
const mountOptions = getMountOptions({
router: {
navigate: jest.fn()
}
});
methodToRun();
mountOptions.data = () => (
initialData
);
mountOptions.mixins = [mockMixin];
const wrapper = shallowMount(serviceLocationReview, mountOptions);
return { wrapper };
}
describe('Service Location Review Block', () => {
test('Should render mobile address if mobile appointment', async () => {
// Arrange
const props = generateDefaultProps();
const { wrapper } = getShallowMountedComponent({
...props
});
// Act
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.displayContent).toEqual([
'Mobile Address 1, Mobile Address 2, Mobile City, MO 11111'
]);
});
test('Should render service location address if inshop appointment.', async () => {
// Arrange
const props = generateDefaultProps();
props.serviceLocation.appointmentType = AppointmentTypeStrings.IN_SHOP;
const { wrapper } = getShallowMountedComponent({
...props
});
// Act
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.displayContent).toEqual([
'Service Location Address, Service Location City, SL 22222'
]);
});
test('Should render service location address if drop-off appointment', async () => {
// Arrange
const props = generateDefaultProps();
props.serviceLocation.appointmentType = AppointmentTypeStrings.DROP_OFF;
const { wrapper } = getShallowMountedComponent({
...props
});
// Act
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.displayContent).toEqual([
'Service Location Address, Service Location City, SL 22222'
]);
});
test('Should not add comma or any text if address2 is null', async () => {
// Arrange
const props = generateDefaultProps();
props.serviceLocation.address2 = null;
const { wrapper } = getShallowMountedComponent({
...props
});
// Act
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.displayContent).toEqual(['Mobile Address 1, Mobile City, MO 11111']);
});
});

View file

@ -0,0 +1,59 @@
<template>
<reviewBlock
editScreenReaderTextCmsWidgetName="EditLocationScreenReader"
:headerCmsWidgetName="cmsWidgetName"
:content="displayContent"
@editClicked="editClicked" />
</template>
<script>
import { AppointmentTypeStrings } from '@/constants/schedule-constants';
import reviewBlock from '@/layouts/review-page/review-block/review-block.vue';
export default {
name: 'service-location-review',
components: {
reviewBlock
},
props: {
cmsWidgetName: String,
serviceLocation: Object
},
emits: ['edit-clicked'],
data() {
return {};
},
computed: {
displayContent() {
return [
`${this.addressInfo.address}${this.addressInfo.address2 ? ', ' : ''}${
this.addressInfo.address2
}, ${this.addressInfo.city}, ${this.addressInfo.state} ${this.addressInfo.zipCode}`
];
},
addressInfo() {
if (this.serviceLocation?.appointmentType === AppointmentTypeStrings.MOBILE) {
return {
address: this.serviceLocation?.address,
address2: this.serviceLocation?.address2 ?? '',
city: this.serviceLocation?.city,
state: this.serviceLocation?.state,
zipCode: this.serviceLocation?.zipCode
};
}
return {
address: this.serviceLocation?.provider?.address?.streetAddress,
address2: '',
city: this.serviceLocation?.provider?.address?.city,
state: this.serviceLocation?.provider?.address?.state,
zipCode: this.serviceLocation?.provider?.address?.zipCode
};
}
},
methods: {
editClicked() {
this.$emit('edit-clicked');
}
}
};
</script>

View file

@ -0,0 +1,969 @@
// Components
import servicePackageReview from '@/layouts/review-page/review-sections/service-package-review/service-package-review.vue';
// Supporting Files
import { shallowMount } from '@vue/test-utils';
import { getMountOptions } from '@/helpers/unit-test-helper.js';
import packageNames from '@/constants/package-names';
import partTypeStrings from '@/constants/part-type-strings';
import damageLocationsSelected from '@/constants/damage-locations-selected';
const testConstants = {
cmsPropValues: {
servicePackageOptionsCmsName: 'ServicePackageTitle',
defaultPackageItemsCmsName: 'DefaultPackageItemDescriptions',
vapsItemsCmsName: 'VapsItemDescriptions'
},
widgetNames: {
tierOneTitle: 'EconomyServiceTitle',
tierTwoTitle: 'StandardServiceTitle',
tierThreeTitle: 'PremiumServiceTitle'
},
defaultItemCopy: {
itemOne: 'Item Description 1',
itemTwo: 'Item Description 2',
itemThree: 'Item Description 3',
itemFour: 'Item Description 4',
defaultItemCopyArray: ['Item Description 1', 'Item Description 2', 'Item Description 3']
},
vapsCopy: {
frontWiperCopy: 'Front Wiper copy',
rearWiperCopy: 'Rear Wiper copy',
rainDefenseCopy: 'Rain defense copy'
},
parts: {
frontWiperPart: {
partNumber: 'SBB16',
description: 'SAFELITE BEAM BLADE 16',
partType: 'FRONT WIPER',
price: 32.64
},
rearWiperPart: {
partNumber: 'SBBR12A',
description: 'SAFELITE REAR BLADE 12A',
partType: 'REAR WIPER',
price: 24.48
},
rainDefensePart: {
partNumber: 'RAIN DEFENSE',
description: null,
partType: 'RAIN DEFENSE',
price: 35.5
},
recalPart: {
partNumber: 'RECAL STATIC',
Description: 'Recalibration',
partType: 'recalibration',
Quantity: '1',
price: 150.0
}
},
damages: {
frontWindshield: {
glassLocation: damageLocationsSelected.WINDSHIELD,
glassName: damageLocationsSelected.SINGLE
},
rearWindshield: {
glassLocation: damageLocationsSelected.REAR,
glassName: damageLocationsSelected.STATIONARY
},
sideGlass: {
glassLocation: damageLocationsSelected.PASSENGER,
glassName: damageLocationsSelected.QUARTER
}
},
imageId: '00000000-0000-0000-0000-000000000000'
};
const figmaScenarios = [
{
name: '05_01_CSR_Quote_Cash',
params: {
wiperResponse: [testConstants.parts.frontWiperPart, testConstants.parts.rearWiperPart],
rainDefenseResponse: testConstants.parts.rainDefensePart,
damage: {
isRepair: false,
glassToReplace: [testConstants.damages.frontWindshield]
},
glassParts: [],
supportingItems: []
},
iterations: [
{
name: 'Economy',
vapsCombo: [],
expected: {
packageNameWidget: testConstants.widgetNames.tierOneTitle,
displayContent: testConstants.defaultItemCopy.defaultItemCopyArray
}
},
{
name: 'Standard',
vapsCombo: [testConstants.parts.frontWiperPart],
expected: {
packageNameWidget: testConstants.widgetNames.tierTwoTitle,
displayContent: [
...testConstants.defaultItemCopy.defaultItemCopyArray,
testConstants.vapsCopy.frontWiperCopy
]
}
},
{
name: 'Premium',
vapsCombo: [
testConstants.parts.rainDefensePart,
testConstants.parts.frontWiperPart
],
expected: {
packageNameWidget: testConstants.widgetNames.tierThreeTitle,
displayContent: [
...testConstants.defaultItemCopy.defaultItemCopyArray,
testConstants.vapsCopy.frontWiperCopy,
testConstants.vapsCopy.rainDefenseCopy
]
}
},
{
name: 'Standard+RearWiper',
vapsCombo: [testConstants.parts.frontWiperPart, testConstants.parts.rearWiperPart],
expected: {
packageNameWidget: testConstants.widgetNames.tierTwoTitle,
displayContent: [
...testConstants.defaultItemCopy.defaultItemCopyArray,
testConstants.vapsCopy.frontWiperCopy,
testConstants.vapsCopy.rearWiperCopy
]
}
}
]
},
{
name: '05_01_CSR_Quote_Standard_Repair',
params: {
wiperResponse: [testConstants.parts.frontWiperPart, testConstants.parts.rearWiperPart],
rainDefenseResponse: testConstants.parts.rainDefensePart,
damage: {
isRepair: true,
glassToReplace: []
},
glassParts: [],
supportingItems: []
},
iterations: [
{
name: 'Economy',
vapsCombo: [],
expected: {
packageNameWidget: testConstants.widgetNames.tierOneTitle,
displayContent: testConstants.defaultItemCopy.defaultItemCopyArray
}
},
{
name: 'Standard',
vapsCombo: [testConstants.parts.frontWiperPart],
expected: {
packageNameWidget: testConstants.widgetNames.tierTwoTitle,
displayContent: [
...testConstants.defaultItemCopy.defaultItemCopyArray,
testConstants.vapsCopy.frontWiperCopy
]
}
},
{
name: 'Premium',
vapsCombo: [
testConstants.parts.rainDefensePart,
testConstants.parts.frontWiperPart
],
expected: {
packageNameWidget: testConstants.widgetNames.tierThreeTitle,
displayContent: [
...testConstants.defaultItemCopy.defaultItemCopyArray,
testConstants.vapsCopy.frontWiperCopy,
testConstants.vapsCopy.rainDefenseCopy
]
}
},
{
name: 'Standard+RearWiper',
vapsCombo: [testConstants.parts.frontWiperPart, testConstants.parts.rearWiperPart],
expected: {
packageNameWidget: testConstants.widgetNames.tierTwoTitle,
displayContent: [
...testConstants.defaultItemCopy.defaultItemCopyArray,
testConstants.vapsCopy.frontWiperCopy,
testConstants.vapsCopy.rearWiperCopy
]
}
}
]
},
{
name: '05_01_CSR_Quote_Recal',
params: {
wiperResponse: [testConstants.parts.frontWiperPart, testConstants.parts.rearWiperPart],
rainDefenseResponse: testConstants.parts.rainDefensePart,
damage: {
isRepair: false,
glassToReplace: [testConstants.damages.frontWindshield]
},
glassParts: [],
supportingItems: [testConstants.parts.recalPart]
},
iterations: [
{
name: 'Economy',
vapsCombo: [],
expected: {
packageNameWidget: testConstants.widgetNames.tierOneTitle,
displayContent: testConstants.defaultItemCopy.defaultItemCopyArray
}
},
{
name: 'Standard',
vapsCombo: [testConstants.parts.frontWiperPart],
expected: {
packageNameWidget: testConstants.widgetNames.tierTwoTitle,
displayContent: [
...testConstants.defaultItemCopy.defaultItemCopyArray,
testConstants.vapsCopy.frontWiperCopy
]
}
},
{
name: 'Premium',
vapsCombo: [
testConstants.parts.rainDefensePart,
testConstants.parts.frontWiperPart
],
expected: {
packageNameWidget: testConstants.widgetNames.tierThreeTitle,
displayContent: [
...testConstants.defaultItemCopy.defaultItemCopyArray,
testConstants.vapsCopy.frontWiperCopy,
testConstants.vapsCopy.rainDefenseCopy
]
}
},
{
name: 'Standard+RearWiper',
vapsCombo: [testConstants.parts.frontWiperPart, testConstants.parts.rearWiperPart],
expected: {
packageNameWidget: testConstants.widgetNames.tierTwoTitle,
displayContent: [
...testConstants.defaultItemCopy.defaultItemCopyArray,
testConstants.vapsCopy.frontWiperCopy,
testConstants.vapsCopy.rearWiperCopy
]
}
}
]
},
// 05_01_CSR_Quote_RearGlass omitted as a duplicate of below.
{
name: '05_01_CSR_Quote_RearGlass+NonWindshield',
params: {
wiperResponse: [testConstants.parts.frontWiperPart, testConstants.parts.rearWiperPart],
rainDefenseResponse: testConstants.parts.rainDefensePart,
damage: {
isRepair: false,
glassToReplace: [testConstants.damages.rearWindshield]
},
glassParts: [],
supportingItems: []
},
iterations: [
{
name: 'Economy',
vapsCombo: [],
expected: {
packageNameWidget: testConstants.widgetNames.tierOneTitle,
displayContent: testConstants.defaultItemCopy.defaultItemCopyArray
}
},
{
name: 'Economy+Frontwiper',
vapsCombo: [testConstants.parts.frontWiperPart],
expected: {
packageNameWidget: testConstants.widgetNames.tierOneTitle,
displayContent: [
...testConstants.defaultItemCopy.defaultItemCopyArray,
testConstants.vapsCopy.frontWiperCopy
]
}
},
{
name: 'Standard',
vapsCombo: [testConstants.parts.rearWiperPart],
expected: {
packageNameWidget: testConstants.widgetNames.tierTwoTitle,
displayContent: [
...testConstants.defaultItemCopy.defaultItemCopyArray,
testConstants.vapsCopy.rearWiperCopy
]
}
},
{
name: 'Standard+RainDefense',
vapsCombo: [testConstants.parts.rearWiperPart, testConstants.parts.rainDefensePart],
expected: {
packageNameWidget: testConstants.widgetNames.tierTwoTitle,
displayContent: [
...testConstants.defaultItemCopy.defaultItemCopyArray,
testConstants.vapsCopy.rearWiperCopy,
testConstants.vapsCopy.rainDefenseCopy
]
}
},
{
name: 'Premium',
vapsCombo: [testConstants.parts.rearWiperPart, testConstants.parts.frontWiperPart],
expected: {
packageNameWidget: testConstants.widgetNames.tierThreeTitle,
displayContent: [
...testConstants.defaultItemCopy.defaultItemCopyArray,
testConstants.vapsCopy.frontWiperCopy,
testConstants.vapsCopy.rearWiperCopy
]
}
}
]
},
{
name: '05_01_CSR_Quote_RearGlass+Windshield',
params: {
wiperResponse: [testConstants.parts.frontWiperPart, testConstants.parts.rearWiperPart],
rainDefenseResponse: testConstants.parts.rainDefensePart,
damage: {
isRepair: false,
glassToReplace: [
testConstants.damages.frontWindshield,
testConstants.damages.rearWindshield
]
},
glassParts: [],
supportingItems: []
},
iterations: [
{
name: 'Economy',
vapsCombo: [],
expected: {
packageNameWidget: testConstants.widgetNames.tierOneTitle,
displayContent: testConstants.defaultItemCopy.defaultItemCopyArray
}
},
{
name: 'Economy+Frontwiper',
vapsCombo: [testConstants.parts.frontWiperPart],
expected: {
packageNameWidget: testConstants.widgetNames.tierOneTitle,
displayContent: [
...testConstants.defaultItemCopy.defaultItemCopyArray,
testConstants.vapsCopy.frontWiperCopy
]
}
},
{
name: 'Economy+Rearwiper',
vapsCombo: [testConstants.parts.rearWiperPart],
expected: {
packageNameWidget: testConstants.widgetNames.tierOneTitle,
displayContent: [
...testConstants.defaultItemCopy.defaultItemCopyArray,
testConstants.vapsCopy.rearWiperCopy
]
}
},
{
name: 'Economy+RainDefense',
vapsCombo: [testConstants.parts.rainDefensePart],
expected: {
packageNameWidget: testConstants.widgetNames.tierOneTitle,
displayContent: [
...testConstants.defaultItemCopy.defaultItemCopyArray,
testConstants.vapsCopy.rainDefenseCopy
]
}
},
{
name: 'Economy+Frontwiper+RainDefense',
vapsCombo: [
testConstants.parts.frontWiperPart,
testConstants.parts.rainDefensePart
],
expected: {
packageNameWidget: testConstants.widgetNames.tierOneTitle,
displayContent: [
...testConstants.defaultItemCopy.defaultItemCopyArray,
testConstants.vapsCopy.frontWiperCopy,
testConstants.vapsCopy.rainDefenseCopy
]
}
},
{
name: 'Economy+Rearwiper+RainDefense',
vapsCombo: [testConstants.parts.rearWiperPart, testConstants.parts.rainDefensePart],
expected: {
packageNameWidget: testConstants.widgetNames.tierOneTitle,
displayContent: [
...testConstants.defaultItemCopy.defaultItemCopyArray,
testConstants.vapsCopy.rearWiperCopy,
testConstants.vapsCopy.rainDefenseCopy
]
}
},
{
name: 'Standard',
vapsCombo: [testConstants.parts.frontWiperPart, testConstants.parts.rearWiperPart],
expected: {
packageNameWidget: testConstants.widgetNames.tierTwoTitle,
displayContent: [
...testConstants.defaultItemCopy.defaultItemCopyArray,
testConstants.vapsCopy.frontWiperCopy,
testConstants.vapsCopy.rearWiperCopy
]
}
},
{
name: 'Premium',
vapsCombo: [
testConstants.parts.rearWiperPart,
testConstants.parts.frontWiperPart,
testConstants.parts.rainDefensePart
],
expected: {
packageNameWidget: testConstants.widgetNames.tierThreeTitle,
displayContent: [
...testConstants.defaultItemCopy.defaultItemCopyArray,
testConstants.vapsCopy.frontWiperCopy,
testConstants.vapsCopy.rearWiperCopy,
testConstants.vapsCopy.rainDefenseCopy
]
}
}
]
},
{
name: '05_01_CSR_Quote_RearGlassNoFrontFit',
params: {
wiperResponse: [testConstants.parts.rearWiperPart],
rainDefenseResponse: testConstants.parts.rainDefensePart,
damage: {
isRepair: false,
glassToReplace: [testConstants.damages.rearWindshield]
},
glassParts: [],
supportingItems: []
},
iterations: [
{
name: 'Economy',
vapsCombo: [],
expected: {
packageNameWidget: testConstants.widgetNames.tierOneTitle,
displayContent: testConstants.defaultItemCopy.defaultItemCopyArray
}
},
{
name: 'Economy+Raindefense',
vapsCombo: [testConstants.parts.rainDefensePart],
expected: {
packageNameWidget: testConstants.widgetNames.tierOneTitle,
displayContent: [
...testConstants.defaultItemCopy.defaultItemCopyArray,
testConstants.vapsCopy.rainDefenseCopy
]
}
},
{
name: 'Standard',
vapsCombo: [testConstants.parts.rearWiperPart],
expected: {
packageNameWidget: testConstants.widgetNames.tierTwoTitle,
displayContent: [
...testConstants.defaultItemCopy.defaultItemCopyArray,
testConstants.vapsCopy.rearWiperCopy
]
}
},
{
name: 'Premium',
vapsCombo: [testConstants.parts.rearWiperPart, testConstants.parts.rainDefensePart],
expected: {
packageNameWidget: testConstants.widgetNames.tierThreeTitle,
displayContent: [
...testConstants.defaultItemCopy.defaultItemCopyArray,
testConstants.vapsCopy.rearWiperCopy,
testConstants.vapsCopy.rainDefenseCopy
]
}
}
]
},
// 05_01_CSR_Quote_Windshield+SideGlass has identical outcomes to 05_01_CSR_Quote_Cash, but included in case that changes in the future.
{
name: '05_01_CSR_Quote_Windshield+SideGlass',
params: {
wiperResponse: [testConstants.parts.frontWiperPart, testConstants.parts.rearWiperPart],
rainDefenseResponse: testConstants.parts.rainDefensePart,
damage: {
isRepair: false,
glassToReplace: [
testConstants.damages.frontWindshield,
testConstants.damages.sideGlass
]
},
glassParts: [],
supportingItems: []
},
iterations: [
{
name: 'Economy',
vapsCombo: [],
expected: {
packageNameWidget: testConstants.widgetNames.tierOneTitle,
displayContent: testConstants.defaultItemCopy.defaultItemCopyArray
}
},
{
name: 'Standard',
vapsCombo: [testConstants.parts.frontWiperPart],
expected: {
packageNameWidget: testConstants.widgetNames.tierTwoTitle,
displayContent: [
...testConstants.defaultItemCopy.defaultItemCopyArray,
testConstants.vapsCopy.frontWiperCopy
]
}
},
{
name: 'Premium',
vapsCombo: [
testConstants.parts.rainDefensePart,
testConstants.parts.frontWiperPart
],
expected: {
packageNameWidget: testConstants.widgetNames.tierThreeTitle,
displayContent: [
...testConstants.defaultItemCopy.defaultItemCopyArray,
testConstants.vapsCopy.frontWiperCopy,
testConstants.vapsCopy.rainDefenseCopy
]
}
},
{
name: 'Standard+RearWiper',
vapsCombo: [testConstants.parts.frontWiperPart, testConstants.parts.rearWiperPart],
expected: {
packageNameWidget: testConstants.widgetNames.tierTwoTitle,
displayContent: [
...testConstants.defaultItemCopy.defaultItemCopyArray,
testConstants.vapsCopy.frontWiperCopy,
testConstants.vapsCopy.rearWiperCopy
]
}
}
]
},
// Has no standard package
{
name: '05_01_CSR_Quote_SideGlass',
params: {
wiperResponse: [testConstants.parts.frontWiperPart, testConstants.parts.rearWiperPart],
rainDefenseResponse: testConstants.parts.rainDefensePart,
damage: {
isRepair: false,
glassToReplace: [testConstants.damages.sideGlass]
},
glassParts: [],
supportingItems: []
},
iterations: [
{
name: 'Economy',
vapsCombo: [],
expected: {
packageNameWidget: testConstants.widgetNames.tierOneTitle,
displayContent: testConstants.defaultItemCopy.defaultItemCopyArray
}
},
{
name: 'Economy+Frontwiper',
vapsCombo: [testConstants.parts.frontWiperPart],
expected: {
packageNameWidget: testConstants.widgetNames.tierOneTitle,
displayContent: [
...testConstants.defaultItemCopy.defaultItemCopyArray,
testConstants.vapsCopy.frontWiperCopy
]
}
},
{
name: 'Economy+RainDefense',
vapsCombo: [testConstants.parts.rainDefensePart],
expected: {
packageNameWidget: testConstants.widgetNames.tierOneTitle,
displayContent: [
...testConstants.defaultItemCopy.defaultItemCopyArray,
testConstants.vapsCopy.rainDefenseCopy
]
}
},
{
name: 'Economy+Rearwiper',
vapsCombo: [testConstants.parts.rearWiperPart],
expected: {
packageNameWidget: testConstants.widgetNames.tierOneTitle,
displayContent: [
...testConstants.defaultItemCopy.defaultItemCopyArray,
testConstants.vapsCopy.rearWiperCopy
]
}
},
{
name: 'Premium',
vapsCombo: [
testConstants.parts.rainDefensePart,
testConstants.parts.frontWiperPart
],
expected: {
packageNameWidget: testConstants.widgetNames.tierThreeTitle,
displayContent: [
...testConstants.defaultItemCopy.defaultItemCopyArray,
testConstants.vapsCopy.frontWiperCopy,
testConstants.vapsCopy.rainDefenseCopy
]
}
},
{
name: 'Premium+Rearwiper',
vapsCombo: [
testConstants.parts.rainDefensePart,
testConstants.parts.frontWiperPart,
testConstants.parts.rearWiperPart
],
expected: {
packageNameWidget: testConstants.widgetNames.tierThreeTitle,
displayContent: [
...testConstants.defaultItemCopy.defaultItemCopyArray,
testConstants.vapsCopy.frontWiperCopy,
testConstants.vapsCopy.rearWiperCopy,
testConstants.vapsCopy.rainDefenseCopy
]
}
}
]
},
// Has no standard package
{
name: '05_01_CSR_Quote_NoWiperFit',
params: {
wiperResponse: [],
rainDefenseResponse: testConstants.parts.rainDefensePart,
damage: {
isRepair: false,
glassToReplace: [testConstants.damages.frontWindshield]
},
glassParts: [],
supportingItems: []
},
iterations: [
{
name: 'Economy',
vapsCombo: [],
expected: {
packageNameWidget: testConstants.widgetNames.tierOneTitle,
displayContent: testConstants.defaultItemCopy.defaultItemCopyArray
}
},
{
name: 'Premium',
vapsCombo: [testConstants.parts.rainDefensePart],
expected: {
packageNameWidget: testConstants.widgetNames.tierThreeTitle,
displayContent: [
...testConstants.defaultItemCopy.defaultItemCopyArray,
testConstants.vapsCopy.rainDefenseCopy
]
}
}
]
}
];
function generateDefaultProps() {
return {
servicePackageOptionsCmsName: testConstants.cmsPropValues.servicePackageOptionsCmsName,
defaultPackageItemsCmsName: testConstants.cmsPropValues.defaultPackageItemsCmsName,
vapsItemsCmsName: testConstants.cmsPropValues.vapsItemsCmsName,
lineItems: {
glassParts: [],
supportingItems: [],
vaps: [testConstants.parts.frontWiperPart]
},
damage: {
isRepair: false,
glassToReplace: [testConstants.damages.frontWindshield]
}
};
}
let cmsContent;
const mockMixin = {
methods: {
getCmsContent: jest.fn((widgetName, cmsFieldName) => cmsContent?.[widgetName]?.[cmsFieldName] ?? '')
}
};
function initializeWithDefault(wrapper) {
wrapper.vm.initializeComponent({
wipers: [testConstants.parts.frontWiperPart],
rainDefense: testConstants.parts.rainDefensePart
});
}
function getShallowMountedComponent(initialData = {}, methodToRun = () => {}) {
const mountOptions = getMountOptions({
router: {
navigate: jest.fn()
}
});
methodToRun();
mountOptions.data = () => (
initialData
);
mountOptions.mixins = [mockMixin];
const wrapper = shallowMount(servicePackageReview, mountOptions);
wrapper.vm.setCmsContent = jest.fn();
return { wrapper };
}
beforeEach(() => {
cmsContent = {
ServicePackageTitle: {
Answers: [
{
Name: packageNames.TIER_ONE,
Text: '',
SubText: '',
ImageId: testConstants.imageId,
Image: '',
SubWidgetName: testConstants.widgetNames.tierOneTitle
},
{
Name: packageNames.TIER_TWO,
Text: '',
SubText: '',
ImageId: testConstants.imageId,
Image: '',
SubWidgetName: testConstants.widgetNames.tierTwoTitle
},
{
Name: packageNames.TIER_THREE,
Text: '',
SubText: '',
ImageId: testConstants.imageId,
Image: '',
SubWidgetName: testConstants.widgetNames.tierThreeTitle
}
]
},
DefaultPackageItemDescriptions: {
Answers: [
{
Name: 'Item1',
Text: testConstants.defaultItemCopy.itemOne,
SubText: '',
ImageId: testConstants.imageId,
Image: '',
SubWidgetName: ''
},
{
Name: 'Item2',
Text: testConstants.defaultItemCopy.itemTwo,
SubText: '',
ImageId: testConstants.imageId,
Image: '',
SubWidgetName: ''
},
{
Name: 'Item3',
Text: testConstants.defaultItemCopy.itemThree,
SubText: '',
ImageId: testConstants.imageId,
Image: '',
SubWidgetName: ''
}
]
},
VapsItemDescriptions: {
Answers: [
{
Name: partTypeStrings.FRONT_WIPER,
Text: testConstants.vapsCopy.frontWiperCopy,
SubText: '',
ImageId: testConstants.imageId,
Image: '',
SubWidgetName: ''
},
{
Name: partTypeStrings.REAR_WIPER,
Text: testConstants.vapsCopy.rearWiperCopy,
SubText: '',
ImageId: testConstants.imageId,
Image: '',
SubWidgetName: ''
},
{
Name: partTypeStrings.RAIN_DEFENSE,
Text: testConstants.vapsCopy.rainDefenseCopy,
SubText: '',
ImageId: testConstants.imageId,
Image: '',
SubWidgetName: ''
}
]
}
};
});
describe('Service Package Review Block', () => {
describe('General functionality', () => {
test('Should properly "Round Down" package tier', async () => {
// Slightly longer explanation:
// Should only return the highest tier where *every* offered VAP is part of the order.
// However, there may be vaps not offered in the qualifying tier. Hence rounding *down*.
//
// I.e. Economy=[], Standard=[front wipers], Premium=[front wipers, rain defense].
// Current vaps=[rain defense]. Though rain defense is in Premium, we don't satisfy it or standard.
// So our tier should still be Economy.
// Should still display extra vaps.
// Arrange
const props = generateDefaultProps();
props.lineItems.vaps = [testConstants.parts.rainDefensePart];
const { wrapper } = getShallowMountedComponent({
...props
});
initializeWithDefault(wrapper);
// Act
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.packageNameWidget).toEqual(testConstants.widgetNames.tierOneTitle);
const containsRainDefenseCopy = wrapper.vm.displayContent.includes(testConstants.vapsCopy.rainDefenseCopy);
expect(containsRainDefenseCopy).toBe(true);
});
test('Should display all default items from cms', async () => {
// Arrange
cmsContent.DefaultPackageItemDescriptions.Answers.push({
Name: 'Item4',
Text: testConstants.defaultItemCopy.itemFour,
SubText: '',
ImageId: testConstants.imageId,
Image: '',
SubWidgetName: ''
});
const props = generateDefaultProps();
props.lineItems.vaps = [];
const { wrapper } = getShallowMountedComponent({
...props
});
initializeWithDefault(wrapper);
// Act
await wrapper.vm.$nextTick();
// Assert
const expectedResult = [
testConstants.defaultItemCopy.itemOne,
testConstants.defaultItemCopy.itemTwo,
testConstants.defaultItemCopy.itemThree,
testConstants.defaultItemCopy.itemFour
];
expect(wrapper.vm.displayContent).toEqual(expectedResult);
});
test('Should display vaps if and only if they are added', async () => {
// Arrange
const props = generateDefaultProps();
const { wrapper } = getShallowMountedComponent({
...props
});
// Act
await wrapper.vm.$nextTick();
initializeWithDefault(wrapper);
// Assert
const includesFrontWiperCopy = wrapper.vm.displayContent.includes(testConstants.vapsCopy.frontWiperCopy);
const includesRainDefenseCopy = wrapper.vm.displayContent.includes(testConstants.vapsCopy.rainDefenseCopy);
expect(includesFrontWiperCopy).toBe(true);
expect(includesRainDefenseCopy).toBe(false);
});
test('Should not error if cms content is missing (though may display poorly).', async () => {
// Arrange
cmsContent = {};
const props = generateDefaultProps();
const { wrapper } = getShallowMountedComponent({
...props
});
initializeWithDefault(wrapper);
// Act
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.packageNameWidget).toEqual('');
expect(wrapper.vm.displayContent).toEqual([]);
});
});
describe('Match Figma Scenarios', () => {
figmaScenarios.forEach((scenario) => {
scenario.iterations.forEach((iteration) => {
it(`Should match figma scenario "${scenario.name}", iteration "${iteration.name}"`, async () => {
// Arrange
const props = generateDefaultProps();
props.damage = scenario.params.damage;
props.glassParts = scenario.params.glassParts;
props.lineItems.supportingItems = scenario.params.supportingItems;
props.lineItems.vaps = iteration.vapsCombo;
const { wrapper } = getShallowMountedComponent({
...props
});
wrapper.vm.initializeComponent({
wipers: scenario.params.wiperResponse,
rainDefense: scenario.params.rainDefenseResponse
});
// Act
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.packageNameWidget).toEqual(iteration.expected.packageNameWidget);
expect(wrapper.vm.displayContent).toEqual(iteration.expected.displayContent);
});
});
});
});
});

View file

@ -0,0 +1,141 @@
<template>
<reviewBlock
editScreenReaderTextCmsWidgetName="EditServiceTypeScreenReader"
:headerCmsWidgetName="packageNameWidget"
:content="displayContent"
@editClicked="editClicked" />
</template>
<script>
// import settleAllPromises from '@/helpers/layout-helper';
// import baseMixin from '@/mixins/base-mixin.js';
// import store from '@/store';
import reviewBlock from '@/layouts/review-page/review-block/review-block.vue';
import {
getHighestFullySatisfiedTier,
containsLineItemWithPartType
} from '@/helpers/service-package-helper.js';
// import { storeActions } from '@/constants/store-actions.js';
export default {
name: 'service-package-review',
components: {
reviewBlock
},
props: {
servicePackageOptionsCmsName: String,
defaultPackageItemsCmsName: String,
vapsItemsCmsName: String,
lineItems: Object,
damage: Object
},
emits: ['edit-clicked'],
data() {
return {
availableVaps: []
};
},
computed: {
displayContent() {
return [...this.defaultPackageText, ...this.vapsItemText];
},
packageNameWidget() {
const servicePackageNames = this.getCmsContent(
this.servicePackageOptionsCmsName,
'Answers'
);
if (!servicePackageNames) {
return '';
}
const currentPackage = servicePackageNames.find((entry) => entry.Name === this.packageLevel);
return currentPackage.SubWidgetName;
},
defaultPackageText() {
const defaultItems = this.getCmsContent(this.defaultPackageItemsCmsName, 'Answers');
if (!defaultItems) {
return [];
}
return defaultItems.map((answer) => answer.Text);
},
vapsItemText() {
const vapsDescriptions = this.getCmsContent(this.vapsItemsCmsName, 'Answers');
if (!vapsDescriptions) {
return [];
}
const vapsDescriptionsOnOrder = vapsDescriptions.filter((answer) =>
containsLineItemWithPartType(answer.Name, this.vaps));
return vapsDescriptionsOnOrder.map((answer) => answer.Text);
},
packageLevel() {
return getHighestFullySatisfiedTier(
this.glassToReplace,
this.availableLineItems,
this.isRepair,
this.vaps
);
},
glassToReplace() {
return this.damage.glassToReplace;
},
isRepair() {
return this.damage.isRepair;
},
vaps() {
return this.lineItems?.vaps;
},
availableLineItems() {
return [
...(this.lineItems?.glassParts ?? []),
...(this.lineItems?.supportingItems ?? []),
...this.availableVaps
];
}
},
methods: {
loadInitialData() {
/* const wipersPromise = baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.GET_WIPERS,
{
serviceZipCode: store.getters.order.serviceLocation.zipCode,
carId: store.getters.vehicle.carId
},
'review'
);
const rainDefensePromise = baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.GET_RAIN_DEFENSE,
null,
'review'
);
const promiseResultMap = [
{
resultKey: 'wipers',
promise: wipersPromise
},
{
resultKey: 'rainDefense',
promise: rainDefensePromise
}
];
return settleAllPromises(promiseResultMap); */
},
initializeComponent(apiResponses) {
const vapsFromApi = [...apiResponses.wipers, apiResponses.rainDefense];
this.availableVaps = vapsFromApi;
},
editClicked() {
this.$emit('edit-clicked');
}
}
};
</script>

View file

@ -0,0 +1,47 @@
// Components
import vehicleReview from '@/layouts/review-page/review-sections/vehicle-review/vehicle-review.vue';
// Supporting Files
import { shallowMount } from '@vue/test-utils';
import { getMountOptions } from '@/helpers/unit-test-helper.js';
jest.mock('@/helpers/cms-content-helper', () => ({
fetchCmsContentForPage: () => Promise.resolve('content')
}));
function getShallowMountedComponent(initialData = {}, methodToRun = () => {}) {
const mountOptions = getMountOptions({
router: {
navigate: jest.fn()
}
});
methodToRun();
mountOptions.data = () => (
initialData
);
const wrapper = shallowMount(vehicleReview, mountOptions);
wrapper.vm.setCmsContent = jest.fn();
return { wrapper };
}
describe('Vehicle Review Block', () => {
test('Correctly assembles vehicle info into a display string', async () => {
// Arrange
const { wrapper } = getShallowMountedComponent({
vehicle: {
year: '2019',
make: 'Honda',
model: 'Odyssey'
}
});
// Act
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.displayContent).toStrictEqual(['2019 Honda Odyssey']);
});
});

View file

@ -0,0 +1,36 @@
<template>
<reviewBlock
editScreenReaderTextCmsWidgetName="EditVehicleScreenReader"
:headerCmsWidgetName="cmsWidgetName"
:content="displayContent"
@editClicked="editClicked" />
</template>
<script>
import reviewBlock from '@/layouts/review-page/review-block/review-block.vue';
export default {
name: 'vehicle-review',
components: {
reviewBlock
},
props: {
cmsWidgetName: String,
vehicle: Object
},
emits: ['edit-clicked'],
data() {
return {};
},
computed: {
displayContent() {
return [`${this.vehicle.year} ${this.vehicle.make} ${this.vehicle.model}`];
}
},
methods: {
editClicked() {
this.$emit('edit-clicked');
}
}
};
</script>

View file

@ -1,72 +1,8 @@
import { useMainStore } from '@/store';
export async function getAlertReasons(ctu) {
export default async function getAlertReasons(ctu) {
const store = useMainStore();
const alertReasons = await store.getAlertReasonsByCtu(ctu);
return Promise.resolve(alertReasons);
}
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}`;
}

View file

@ -9,7 +9,7 @@
</template>
<script>
import alert from '@/ux-components/alert/alert.vue';
import { getAlertReasons } from '@/layouts/schedule-page/helpers/schedule-helper';
import getAlertReasons from '@/layouts/schedule-page/helpers/schedule-helper';
export default {
name: 'location-alerts',

View file

@ -70,11 +70,6 @@ import siteFooter from '@/iss-components/site-footer/site-footer.vue';
import textBlock from '@/digital-components/text-block/text-block.vue';
// Supporting files
import {
calcDaysBetweenDates,
convertDateStringToDate,
sumDateString
} from '@/layouts/schedule-page/helpers/schedule-helper';
import {
AppointmentTypeStrings,
GET_MOBILE_TIME_SLOTS,
@ -82,6 +77,7 @@ import {
PREMIUM_FEE_PART_TYPE
} from '@/constants/schedule-constants.js';
import { fetchCmsContentForPage, splitCopyOnCMSPlaceHolder } from '@/helpers/cms-content-helper';
import { calcDaysBetweenDates, convertDateStringToDate, sumDateString } from '@/helpers/date-helper';
import settleAllPromises from '@/helpers/layout-helper';
import { Form, defineRule } from 'vee-validate';
import BaseFormMixin from '@/mixins/base-form-mixin.js';

View file

@ -60,11 +60,7 @@ import {
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 { convertDateStringToDate, getDisplayTextForDurationLength, militaryToTwelveHourTime } from '@/helpers/date-helper';
import timeSlotModalListButton from './time-slot-modal-list-button/time-slot-modal-list-button.vue';
const cmsWidgetFieldMappings = {

View file

@ -217,7 +217,6 @@ function navigate(
optionalParams = {},
optionalPageData = {}
) {
/*eslint-disable-line*/
if (!scenario) {
window.console.error('No scenario provided. Please review the routing table.');
return;

View file

@ -74,6 +74,14 @@ const navigationScenarios = Object.freeze({
CLICKED_FORWARD_WITH_TPA_DISABLED: 'CLICKED_FORWARD_WITH_TPA_DISABLED',
CLICKED_FORWARD_WITH_POLICY_AND_VEHICLES: 'CLICKED_FORWARD_WITH_POLICY_AND_VEHICLES',
// Review Page
CLICKED_CUSTOMER_EDIT: 'CLICKED_CUSTOMER_EDIT',
CLICKED_DAMAGE_EDIT: 'CLICKED_DAMAGE_EDIT',
CLICKED_SCHEDULE_EDIT: 'CLICKED_SCHEDULE_EDIT',
CLICKED_SERVICE_LOCATION_EDIT: 'CLICKED_SERVICE_LOCATION_EDIT',
CLICKED_SERVICE_PACKAGE_EDIT: 'CLICKED_SERVICE_PACKAGE_EDIT',
CLICKED_VEHICLE_EDIT: 'CLICKED_VEHICLE_EDIT',
// Bailout
CLICKED_FORWARD_WITH_BAILOUT: 'CLICKED_FORWARD_WITH_BAILOUT'
});

View file

@ -609,6 +609,30 @@ const routingTable = () => [
{
scenario: navigationScenarios.CLICKED_FORWARD,
destinationIssPageValue: issPageValues.PAYMENT_PAGE
},
{
scenario: navigationScenarios.CLICKED_CUSTOMER_EDIT,
destinationIssPageValue: issPageValues.CONTACT_DETAILS
},
{
scenario: navigationScenarios.CLICKED_DAMAGE_EDIT,
destinationIssPageValue: issPageValues.VEHICLE_DAMAGE
},
{
scenario: navigationScenarios.CLICKED_SCHEDULE_EDIT,
destinationIssPageValue: issPageValues.SCHEDULE_PAGE
},
{
scenario: navigationScenarios.CLICKED_SERVICE_LOCATION_EDIT,
destinationIssPageValue: issPageValues.SERVICE_LOCATION
},
{
scenario: navigationScenarios.CLICKED_SERVICE_PACKAGE_EDIT,
destinationIssPageValue: issPageValues.SERVICE_PACKAGES
},
{
scenario: navigationScenarios.CLICKED_VEHICLE_EDIT,
destinationIssPageValue: issPageValues.VEHICLE_SELECTION
}
]
},

View file

@ -11,13 +11,28 @@ 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 { deepEqual } from '@/helpers/object-helper';
import { AppointmentTypeStrings, PREMIUM_FEE_PART_TYPE } from '@/constants/schedule-constants';
import getDateDifferenceInDays from '@/helpers/date-helper';
import endorsementOptions from '@/constants/endorsement-options';
import { convertDateStringToDate, getDateDifferenceInDays, militaryToTwelveHourTime } from '@/helpers/date-helper';
// import endorsementOptions from '@/constants/endorsement-options';
const storeId = 'main';
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}`;
}
function getTimeSlotsAdditionalEventData(
provisionalTriggers,
zipCode,
@ -201,7 +216,7 @@ export const useMainStore = defineStore({
lineItems: (state) => state.order.lineItems,
payment: (state) => state.order.payment,
policy: (state) => state.order.policy,
hasAnyNonWindshieldGlassParts: (state) => !state.order.policy.isDamageGlassOnly,
hasAnyNonWindshieldGlassParts: (s) => !s.order.policy.isDamageGlassOnly,
isMobileAppointment: (state) => state.order.serviceLocation.appointmentType === AppointmentTypeStrings.MOBILE
|| state.order.serviceLocation.appointmentType === AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP,
isDropOffAppointment: (state) => state.order.serviceLocation.appointmentType === AppointmentTypeStrings.DROP_OFF,
@ -250,6 +265,30 @@ export const useMainStore = defineStore({
requestTextUpdates: s.order.contactInfo.requestTextUpdates ?? false,
notesForTechnician: s.order.contactInfo.notesForTechnician
}),
scheduleDisplayText: (s) => {
const dateModel = convertDateStringToDate(s.order.schedule.date);
const readableDate = dateModel?.toLocaleDateString('en-us', {
weekday: 'long',
month: 'long',
day: 'numeric',
year: 'numeric'
});
const readableStartTime = militaryToTwelveHourTime(s.order.schedule.startTime);
const readableEndTime = militaryToTwelveHourTime(s.order.schedule.endTime);
const readableDuration = getDisplayTextForDurationLength(
s.order.schedule.jobMinMinutes,
s.order.schedule.jobMaxMinutes
);
return {
date: readableDate,
startTime: readableStartTime,
endTime: readableEndTime,
duration: readableDuration
};
},
experimentOrder: (state) => ({
issVehicleYear: state.order.vehicle.year,
issVehicleMake: state.order.vehicle.make,
@ -1590,11 +1629,9 @@ export const useMainStore = defineStore({
updateIsSafeliteProvider(isSafelite) {
this.order.serviceLocation.IsSafeliteProvider = isSafelite;
},
updateDeductible(finalDeductible) {
this.order.currentDeductible = finalDeductible;
},
savePartQuestionAnswers(partQuestionAnswersArray) {
// if part question answers have changed, reset subsequent question answers
const sortedPreviousResultsArray = sortArrayOfObjectsByPropertyValue(this.order.damage.partQuestionAnswers, 'result');
@ -1637,7 +1674,6 @@ export const useMainStore = defineStore({
// Save new values
this.updateMoldingQuestionAnswers(moldingQuestionAnswersArray);
},
saveCapabilityQuestionAnswers(capabilityQuestionAnswersArray) {
const sortedPreviousResultsArray = sortArrayOfObjectsByPropertyValue(this.order.damage.capabilityQuestionAnswers, 'result');
const sortedCapabilityQuestionAnswersArray = sortArrayOfObjectsByPropertyValue(capabilityQuestionAnswersArray, 'result');
@ -1655,16 +1691,9 @@ export const useMainStore = defineStore({
this.updateCapabilityQuestionAnswers(capabilityQuestionAnswersArray);
},
saveGlassParts(glassParts) {
if (!deepEqual(glassParts, this.order.lineItems.glassParts)) {
this.resetServiceLocationAndDependencies();
}
this.order.lineItems.glassParts = glassParts;
},
saveSupportingItems(supportingItems) {
if (!deepEqual(supportingItems, this.order.lineItems.supportingItems)) {
this.resetServiceLocationAndDependencies();
}
this.order.lineItems.supportingItems = supportingItems;
},
saveSupportingItemsSuppressingStateResetting(supportingItems) {