Merge branch 'develop' into CSR-1382-order-quote-initial-setup

This commit is contained in:
Bryan Mauger 2023-09-11 10:45:09 -04:00
commit 6085cd1b13
26 changed files with 1123 additions and 680 deletions

View file

@ -4,7 +4,7 @@ const AppointmentTypeStrings = {
DROP_OFF: "Dropoff",
};
const PREMIUM_TIME_SLOT_ID_FLAG = "-premium";
const PREMIUM_TIME_SLOT_ID_FLAG = "-PREMIUM";
const PREMIUM_FEE_PART_TYPE = "EARLY BIRD";

View file

@ -26,7 +26,7 @@
{{ value }}
</option>
</select>
<div v-show="errorMessage" class="row mt-2 form-test-error">
<div v-show="errorMessage && this.disableValidation" class="row mt-2 form-test-error">
<span aria-atomic="true" aria-live="polite">{{ errorMessage }}</span>
</div>
</div>
@ -56,6 +56,11 @@ export default {
required: false,
default: false,
},
disableValidationIfElementDisabled: {
type: Boolean,
required: false,
default: false,
},
},
setup(props) {
const uuid = uuidv4();
@ -110,6 +115,9 @@ export default {
this.$emit("update:modelValue", newValue);
},
},
disableValidation() {
return this.disableValidationIfElementDisabled ? !this.isDisabled : true;
},
},
watch: {
selectedOption(newValue) {

View file

@ -2,13 +2,12 @@
<div class="text-block w-100" :class="[justifyText, typeStyle, fontWeight, marginTopClass]">
<span v-for="copy in splitCopyOnCMSPlaceHolder(this.textBlockCopy)" :key="copy">
<span v-if="doesCopyContainRouterLink(copy)">
<router-link
:to="{
query: { [pageQueryString]: `${getRouterLinkRouteFromCopy(copy)}` },
name: 'root',
}"
>{{ getRouterLinkDisplayTextFromCopy(copy) }}</router-link
>
<textLink
linkType="text"
:text="getRouterLinkDisplayTextFromCopy(copy)"
href="javascript:void(0)"
useLoadingModal
@click-event="navigateWithScenario(getRouterLinkRouteFromCopy(copy))" />
</span>
<span v-else-if="doesCopyContainTextLink(copy)">
<textLink
@ -55,6 +54,9 @@ export default {
getRouterLinkRouteFromCopy,
getRouterLinkDisplayTextFromCopy,
getExternalLink,
navigateWithScenario(scenarioName) {
this.$router.navigateWithoutSaving(scenarioName, this.$route);
},
},
computed: {
pageQueryString() {

View file

@ -20,6 +20,7 @@
<textLink
linkType="navigation"
:text="backLink"
useLoadingModal
@click-event="linkClick"
href="javascript:void(0)"
data-bs-target="#footerModal"

View file

@ -1,47 +1,50 @@
<template>
<div v-if="isModalVisible" class="container-fluid modal-loader">
<div class="row g-2 h-100 d-flex align-items-center">
<div class="container-fluid overflow-hidden">
<div class="row h-100">
<div class="col text-center tagbg mb-4">
<div class="spinner-border text-danger" role="status">
<span class="visually-hidden">Loading...</span>
<div v-if="isModalVisible">
<div class="dimmer-overlay"></div>
<div class="container-fluid modal-loader" :class="layoutClass">
<div class="row g-2 h-100 d-flex align-items-center">
<div class="container-fluid overflow-hidden">
<div class="row h-100">
<div class="col text-center tagbg mb-4">
<div class="spinner-border text-danger" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col">
<div class="text-container slide">
<p>
Tidying up the shop
<span class="dot-1">.</span>
<span class="dot-2">.</span>
<span class="dot-3">.</span>
</p>
<p>
Getting all the glass shined up
<span class="dot-1">.</span>
<span class="dot-2">.</span>
<span class="dot-3">.</span>
</p>
<p>
Planning your new view of the road
<span class="dot-1">.</span>
<span class="dot-2">.</span>
<span class="dot-3">.</span>
</p>
<p>
Nearly there
<span class="dot-1">.</span>
<span class="dot-2">.</span>
<span class="dot-3">.</span>
</p>
<p>
Finishing up
<span class="dot-1">.</span>
<span class="dot-2">.</span>
<span class="dot-3">.</span>
</p>
<div class="row">
<div class="col">
<div class="text-container slide">
<p>
Tidying up the shop
<span class="dot-1">.</span>
<span class="dot-2">.</span>
<span class="dot-3">.</span>
</p>
<p>
Getting all the glass shined up
<span class="dot-1">.</span>
<span class="dot-2">.</span>
<span class="dot-3">.</span>
</p>
<p>
Planning your new view of the road
<span class="dot-1">.</span>
<span class="dot-2">.</span>
<span class="dot-3">.</span>
</p>
<p>
Nearly there
<span class="dot-1">.</span>
<span class="dot-2">.</span>
<span class="dot-3">.</span>
</p>
<p>
Finishing up
<span class="dot-1">.</span>
<span class="dot-2">.</span>
<span class="dot-3">.</span>
</p>
</div>
</div>
</div>
</div>
@ -58,6 +61,17 @@ export default {
isModalVisible: false,
};
},
props: {
notFullScreen: {
type: Boolean,
default: false,
},
},
computed: {
layoutClass() {
return this.notFullScreen ? "card-layout" : "full-screen";
},
},
methods: {
showModal() {
//Display modal
@ -80,20 +94,43 @@ export default {
</script>
<style lang="scss">
.modal-loader {
.dimmer-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1057;
width: 100%;
height: 100%;
width: 100%;
background-color: $black;
opacity: 0.4;
z-index: 1056;
}
.modal-loader {
position: fixed;
z-index: 1057;
overflow-x: hidden;
overflow-y: auto;
outline: 0;
background: $gray-100;
&.full-screen {
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
}
&.card-layout {
height: 186px;
width: calc(100% - 3rem);
border-radius: 0.5rem;
position: absolute;
top: calc(50% - 93px);
left: 1.5rem;
}
.tagbg {
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg fill='none' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 84 32'%3e%3cpath d='M22.39 27.313a3.53 3.53 0 0 0 7.058 0' fill='%23fff'/%3e%3cpath d='M22.39 27.313a3.53 3.53 0 0 0 7.058 0' stroke='%23000' stroke-width='.75' stroke-linecap='round' stroke-linejoin='round'/%3e%3cpath d='M54.459 27.313a3.527 3.527 0 0 0 7.054 0' fill='%23fff'/%3e%3cpath d='M54.459 27.313a3.527 3.527 0 0 0 7.054 0' stroke='%23000' stroke-width='.75' stroke-linecap='round' stroke-linejoin='round'/%3e%3cpath d='M42.06.8c8.08 0 11.89.935 11.89.935 3.58.576 5.696 7.207 5.696 7.207h.727c0-2.427 1.302-2.397 2.341-2 .723.292 1.363.76 1.86 1.361 1.319 1.547.465 1.674.465 1.674h-5.067l3.846 3.01v12.016c0 2.41-2.029 2.31-2.029 2.31H22.338s-2.029.1-2.029-2.31V12.996l3.84-3.009H19.07s-.853-.127.466-1.674a4.693 4.693 0 0 1 1.86-1.361c1.032-.397 2.341-.427 2.341 2h.736s2.117-6.64 5.696-7.21c0 0 3.81-.942 11.89-.942Z' fill='%23fff' stroke='%23000' stroke-width='.75' stroke-linecap='round' stroke-linejoin='round'/%3e%3cpath d='M42.06 8.847c7.924 0 14.685.511 14.685.511 0-2.585-2.38-6.011-2.38-6.011S50.4 2.519 42.06 2.519s-12.303.828-12.303.828-2.38 3.426-2.38 6.011c0 0 6.76-.51 14.683-.51Z' fill='%23DA291C' stroke='%23000' stroke-width='.75' stroke-linecap='round' stroke-linejoin='round'/%3e%3cpath d='M35.277 16.523a62.558 62.558 0 0 1 13.277 0m3.276-.439s2.384-2.257 8.608-2.257c0 0 1.179 2.657-2.54 3.37m-25.894-1.113s-2.387-2.257-8.611-2.257c0 0-1.16 2.579 2.543 3.37m-3.546 5.856s21.52 3.647 39.123 0' stroke='%23000' stroke-width='.75' stroke-linecap='round' stroke-linejoin='round'/%3e%3c/svg%3e");
background-repeat: no-repeat;

View file

@ -10,6 +10,7 @@
linkType="textSmall"
text="Edit"
class="ml-auto"
useLoadingModal
@click-event="linkClicked"
href="javascript:void(0)" />
</div>

View file

@ -1,150 +0,0 @@
import { shallowMount } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import customerDetailsModalQuestion from "@/layouts/review/review-sections/customer-review/customer-details-modal-question/customer-details-modal-question";
const testConstants = {
previousCustomerValues: {
firstName: "First",
lastName: "Last",
emailAddress: "builddigitaltest@safelite.com",
phoneNumber: "111-111-1111",
isSmsOptIn: false,
},
newValues: {
firstName: "New First",
lastName: "New Last",
emailAddress: "builddigitaltest2@safelite.com",
phoneNumber: "222-222-2222",
isSmsOptIn: true,
},
};
let cmsContent;
describe("Customer Details Modal", () => {
beforeEach(() => {
cmsContent = {};
});
describe("Submit", () => {
test("Should push to store if a field has changed", async () => {
// Arrange
let props = generateDefaultProps();
const { wrapper } = setupMocks({
propsData: props,
});
// Act
wrapper.vm.onModalOpened();
wrapper.vm.firstName = testConstants.newValues.firstName;
await wrapper.vm.setContactDetails();
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.dispatchStoreAction).toHaveBeenCalled();
expect(wrapper.vm.closeModal).toHaveBeenCalled();
});
test("Should not push to store if no fields have changed", async () => {
// Arrange
let props = generateDefaultProps();
const { wrapper } = setupMocks({
propsData: props,
});
// Act
wrapper.vm.onModalOpened();
await wrapper.vm.setContactDetails();
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.dispatchStoreAction).not.toHaveBeenCalled();
expect(wrapper.vm.closeModal).toHaveBeenCalled();
});
});
describe("Default values", () => {
test("Should populate on open", () => {
// Arrange
let props = generateDefaultProps();
const { wrapper } = setupMocks({
propsData: props,
});
// Act
wrapper.vm.onModalOpened();
// Assert
expect(wrapper.vm.firstName).toBe(testConstants.previousCustomerValues.firstName);
expect(wrapper.vm.lastName).toBe(testConstants.previousCustomerValues.lastName);
expect(wrapper.vm.emailAddress).toBe(testConstants.previousCustomerValues.emailAddress);
expect(wrapper.vm.phoneNumber).toBe(testConstants.previousCustomerValues.phoneNumber);
expect(wrapper.vm.isSmsOptIn).toBe(testConstants.previousCustomerValues.isSmsOptIn);
});
test("Should replace old values on open", async () => {
// Arrange
let props = generateDefaultProps();
const { wrapper } = setupMocks({
propsData: props,
});
// Act
wrapper.vm.firstName = testConstants.newValues.firstName;
wrapper.vm.lastName = testConstants.newValues.lastName;
wrapper.vm.emailAddress = testConstants.newValues.emailAddress;
wrapper.vm.phoneNumber = testConstants.newValues.phoneNumber;
wrapper.vm.isSmsOptIn = testConstants.newValues.isSmsOptIn;
wrapper.vm.onModalOpened();
// Assert
expect(wrapper.vm.firstName).toBe(testConstants.previousCustomerValues.firstName);
expect(wrapper.vm.lastName).toBe(testConstants.previousCustomerValues.lastName);
expect(wrapper.vm.emailAddress).toBe(testConstants.previousCustomerValues.emailAddress);
expect(wrapper.vm.phoneNumber).toBe(testConstants.previousCustomerValues.phoneNumber);
expect(wrapper.vm.isSmsOptIn).toBe(testConstants.previousCustomerValues.isSmsOptIn);
});
});
});
function generateDefaultProps() {
return {
previousCustomerValues: {
firstName: testConstants.previousCustomerValues.firstName,
lastName: testConstants.previousCustomerValues.lastName,
emailAddress: testConstants.previousCustomerValues.emailAddress,
phoneNumber: testConstants.previousCustomerValues.phoneNumber,
isSmsOptIn: testConstants.previousCustomerValues.isSmsOptIn,
},
};
}
function setupMocks(customMountOptions) {
const mountOptions = getMountOptions(customMountOptions);
const mockMixin = {
methods: {
getCmsContent: jest.fn((widgetName, cmsFieldName) => {
return cmsContent?.[widgetName]?.[cmsFieldName] ?? "";
}),
},
};
mountOptions.global.mixins = [mockMixin];
const wrapper = shallowMount(customerDetailsModalQuestion, mountOptions);
wrapper.vm.setCmsContent = jest.fn();
wrapper.vm.openModal = jest.fn();
wrapper.vm.closeModal = jest.fn();
wrapper.vm.dispatchStoreAction = jest.fn();
return { wrapper };
}

View file

@ -1,148 +0,0 @@
<template>
<transition name="fade" mode="out-in">
<div class="customer-details-question">
<modal
ref="CustomerDetailsModal"
headerText="Contact details"
footerButtonText="Save contact details"
:onModalOpenedCallback="onModalOpened"
:onModalClosedCallback="onModalClosed"
@isModalOpened="setModalStatus"
@footer-button-event="setContactDetails">
<template v-if="isModalOpened">
<textboxQuestion
class="mb-4"
cmsWidgetName="CustomerFirstNameWidget"
v-model="firstName"
ref="firstName"
customInputId="firstName"
validation-rules="first-name-required" />
<textboxQuestion
class="mb-4"
cmsWidgetName="CustomerLastNameWidget"
v-model="lastName"
ref="lastName"
customInputId="lastName"
validation-rules="last-name-required" />
<textboxQuestion
class="mb-4"
cmsWidgetName="CustomerEmailWidget"
v-model="emailAddress"
ref="emailAddress"
customInputId="emailAddress"
validation-rules="email-address-required|email-address-format" />
<phoneNumberQuestion
class="mb-4"
cmsWidgetName="CustomerPhoneWidget"
v-model="phoneNumber"
ref="phoneNumber"
isRequired
validation-rules="phone-number-required" />
<checkboxQuestion
class="mb-5"
cmsWidgetName="CustomerTextMeWidget"
v-model="isSmsOptIn" />
<textBlock cmsWidgetName="CustomerTextDisclaimerWidget" typeStyle="caption" />
</template>
</modal>
</div>
</transition>
</template>
<script>
import modal from "@/digital-components/modal/modal";
import textboxQuestion from "@/digital-components/textbox-question/textbox-question";
import phoneNumberQuestion from "@/digital-components/phone-number-question/phone-number-question";
import checkboxQuestion from "@/digital-components/checkbox-question/checkbox-question";
import textBlock from "@/digital-components/text-block/text-block";
import { errorMessages } from "@/constants/error-messages";
import { required, regex } from "@/helpers/validation-rules";
import { defineRule } from "vee-validate";
// DEFINE VALIDATION RULES
defineRule("first-name-required", required(errorMessages.FIRST_NAME_REQUIRED));
defineRule("last-name-required", required(errorMessages.LAST_NAME_REQUIRED));
defineRule("phone-number-required", required(errorMessages.PHONE_REQUIRED));
defineRule("email-address-required", required(errorMessages.EMAIL_ADDRESS_REQUIRED));
defineRule(
"email-address-format",
regex(
/^([a-zA-Z0-9_\-.+]+)@([a-zA-Z0-9-]+)\.([a-zA-Z]{2,})$/,
errorMessages.EMAIL_ADDRESS_FORMAT
)
);
export default {
name: "customer-details-modal-question",
data() {
return {
isModalOpened: false,
firstName: "",
lastName: "",
emailAddress: "",
phoneNumber: "",
isSmsOptIn: false,
};
},
props: {
previousCustomerValues: Object,
},
computed: {
modal() {
return this.$refs.CustomerDetailsModal;
},
haveCustomerDetailsChanged() {
return (
this.firstName !== this.previousCustomerValues?.firstName ||
this.lastName !== this.previousCustomerValues?.lastName ||
this.emailAddress !== this.previousCustomerValues?.emailAddress ||
this.phoneNumber !== this.previousCustomerValues?.phoneNumber ||
this.isSmsOptIn !== this.previousCustomerValues?.isSmsOptIn
);
},
},
methods: {
openModal() {
this.modal.openModal();
},
closeModal() {
this.modal.closeModal();
},
onModalOpened() {
this.firstName = this.previousCustomerValues?.firstName ?? "";
this.lastName = this.previousCustomerValues?.lastName ?? "";
this.emailAddress = this.previousCustomerValues?.emailAddress ?? "";
this.phoneNumber = this.previousCustomerValues?.phoneNumber ?? "";
this.isSmsOptIn = this.previousCustomerValues?.isSmsOptIn ?? false;
},
onModalClosed() {},
setModalStatus(isOpened) {
this.isModalOpened = isOpened;
},
async setContactDetails() {
if (this.haveCustomerDetailsChanged) {
await this.dispatchStoreAction(
this.storeActions.SAVE_CUSTOMER_DETAILS,
{
firstName: this.firstName,
lastName: this.lastName,
emailAddress: this.emailAddress,
phoneNumber: this.phoneNumber,
isSmsOptIn: this.isSmsOptIn,
},
false
);
}
this.closeModal();
},
},
components: {
modal,
textboxQuestion,
phoneNumberQuestion,
checkboxQuestion,
textBlock,
},
};
</script>

View file

@ -9,11 +9,7 @@ const testConstants = {
text: "Header",
},
sms: {
template: "Test {custom:smsOptInJoiner}",
expected: {
ifTrue: "Test in to",
ifFalse: "Test out of",
},
text: "Sms",
},
},
customer: {
@ -27,7 +23,7 @@ const testConstants = {
fullName: "First Last",
phoneNumber: "111-111-1111",
emailAddress: "builddigitaltest@safelite.com",
smsOptIn: "Test out of",
smsOptIn: "Sms",
},
};
@ -38,7 +34,7 @@ describe("Customer Review Block", () => {
cmsContent = {
CustomerWidget: {
HeaderText: testConstants.cms.header.text,
SubheaderText: testConstants.cms.sms.template,
SubheaderText: testConstants.cms.sms.text,
},
};
});
@ -58,52 +54,19 @@ describe("Customer Review Block", () => {
expect(wrapper.vm.header).toEqual(testConstants.cms.header.text);
});
describe("SMS Opt In Text", () => {
test("Should render correctly when opt in is true:", async () => {
// Arrange
let props = generateDefaultProps();
props.customer.isSmsOptIn = true;
test("Should display sms text from cms", async () => {
// Arrange
let props = generateDefaultProps();
const { wrapper } = setupMocks({
propsData: props,
});
// Act
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.smsOptIn).toEqual(testConstants.cms.sms.expected.ifTrue);
const { wrapper } = setupMocks({
propsData: props,
});
test("Should render correctly when opt in is false:", async () => {
// Arrange
let props = generateDefaultProps();
props.customer.isSmsOptIn = false;
const { wrapper } = setupMocks({
propsData: props,
});
// Act
await wrapper.vm.$nextTick();
// Act
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.smsOptIn).toEqual(testConstants.cms.sms.expected.ifFalse);
});
test("Should render correctly when opt in is null:", async () => {
// Arrange
let props = generateDefaultProps();
props.customer.isSmsOptIn = null;
const { wrapper } = setupMocks({
propsData: props,
});
// Act
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.smsOptIn).toEqual(testConstants.cms.sms.expected.ifFalse);
});
// Assert
expect(wrapper.vm.smsOptIn).toEqual(testConstants.cms.sms.text);
});
test("Should render correct display content", async () => {

View file

@ -36,10 +36,7 @@ export default {
return this.customer?.phoneNumber;
},
smsOptIn() {
const rawCmsText = this.getCmsContent(this.cmsWidgetName, "SubheaderText");
const joinerText = this.customer?.isSmsOptIn ? "in to" : "out of";
return rawCmsText.replace("{custom:smsOptInJoiner}", joinerText);
return this.getCmsContent(this.cmsWidgetName, "SubheaderText");
},
},
components: {

View file

@ -1,3 +1,362 @@
import { shallowMount } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import store from "@/store";
import review from "@/layouts/review/review";
const testConstants = {};
jest.mock("@/store", () => ({
commit: jest.fn(),
dispatch: jest.fn(),
}));
describe("Review Page", () => {
test.todo("Add more tests as specific functionality is added.");
beforeEach(() => {
store.getters = {
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,
},
},
};
});
describe("arePagePrerequisitesValid", () => {
test("Returns true for baseline valid state", () => {
// Arrange
const { wrapper } = setupMocks({});
// Act
const isValid = wrapper.vm.arePagePrerequisitesValid();
// Assert
expect(isValid).toBe(true);
});
test("Returns false for empty state", () => {
// Arrange
store.getters.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,
};
const { wrapper } = setupMocks({});
// Act
const isValid = wrapper.vm.arePagePrerequisitesValid();
// Assert
expect(isValid).toBe(false);
});
describe("Damage requirements", () => {
test("Accepts null glassToReplace when is repair", () => {
// Arrange
store.getters.order.damage.isRepair = true;
store.getters.order.damage.glassToReplace = null;
store.getters.order.damage.numberOfChips = 1;
const { wrapper } = setupMocks({});
// Act
const isValid = wrapper.vm.arePagePrerequisitesValid();
// Assert
expect(isValid).toBe(true);
});
test("Rejects 0 chips when repair", () => {
// Arrange
store.getters.order.damage.isRepair = true;
store.getters.order.damage.numberOfChips = 0;
const { wrapper } = setupMocks({});
// Act
const isValid = wrapper.vm.arePagePrerequisitesValid();
// Assert
expect(isValid).toBe(false);
});
test("Rejects null chips when repair", () => {
// Arrange
store.getters.order.damage.isRepair = true;
store.getters.order.damage.numberOfChips = null;
const { wrapper } = setupMocks({});
// Act
const isValid = wrapper.vm.arePagePrerequisitesValid();
// Assert
expect(isValid).toBe(false);
});
test("Accepts null chips when not repair", () => {
// Arrange
store.getters.order.damage.isRepair = false;
store.getters.order.damage.numberOfChips = null;
const { wrapper } = setupMocks({});
// Act
const isValid = wrapper.vm.arePagePrerequisitesValid();
// Assert
expect(isValid).toBe(true);
});
test("Rejects empty glassToReplace when not repair", () => {
// Arrange
store.getters.order.damage.isRepair = false;
store.getters.order.damage.glassToReplace = null;
const { wrapper } = setupMocks({});
// Act
const isValid = wrapper.vm.arePagePrerequisitesValid();
// Assert
expect(isValid).toBe(false);
});
test("Rejects null glassToReplace when not repair", () => {
// Arrange
store.getters.order.damage.isRepair = false;
store.getters.order.damage.glassToReplace = [];
const { wrapper } = setupMocks({});
// Act
const isValid = wrapper.vm.arePagePrerequisitesValid();
// Assert
expect(isValid).toBe(false);
});
});
describe("Package requirements", () => {
test("Accepts null glassParts when is repair", () => {
// Arrange
store.getters.order.damage.isRepair = true;
store.getters.order.lineItems.glassParts = null;
const { wrapper } = setupMocks({});
// Act
const isValid = wrapper.vm.arePagePrerequisitesValid();
// Assert
expect(isValid).toBe(true);
});
test("Rejects null glassParts when not repair", () => {
// Arrange
store.getters.order.damage.isRepair = false;
store.getters.order.lineItems.glassParts = null;
const { wrapper } = setupMocks({});
// Act
const isValid = wrapper.vm.arePagePrerequisitesValid();
// Assert
expect(isValid).toBe(false);
});
});
describe("Service Location requirements", () => {
test("Accepts null provider address when mobile appointment", () => {
// Arrange
store.getters.order.serviceLocation.appointmentType = "Mobile";
store.getters.order.serviceLocation.provider.address = {};
const { wrapper } = setupMocks({});
// Act
const isValid = wrapper.vm.arePagePrerequisitesValid();
// Assert
expect(isValid).toBe(true);
});
test("Reject null provider address when non-mobile appointment", () => {
// Arrange
store.getters.order.serviceLocation.appointmentType = "Inshop";
store.getters.order.serviceLocation.provider.address = {};
const { wrapper } = setupMocks({});
// Act
const isValid = wrapper.vm.arePagePrerequisitesValid();
// Assert
expect(isValid).toBe(false);
});
test("Accepts null service location address when non-mobile appointment", () => {
// Arrange
store.getters.order.serviceLocation.appointmentType = "Inshop";
store.getters.order.serviceLocation.address = null;
store.getters.order.serviceLocation.address2 = null;
store.getters.order.serviceLocation.zipCode = null;
store.getters.order.serviceLocation.city = null;
store.getters.order.serviceLocation.state = null;
const { wrapper } = setupMocks({});
// Act
const isValid = wrapper.vm.arePagePrerequisitesValid();
// Assert
expect(isValid).toBe(true);
});
test("Rejects null service location address when mobile appointment", () => {
// Arrange
store.getters.order.serviceLocation.appointmentType = "Mobile";
store.getters.order.serviceLocation.address = null;
store.getters.order.serviceLocation.address2 = null;
store.getters.order.serviceLocation.zipCode = null;
store.getters.order.serviceLocation.city = null;
store.getters.order.serviceLocation.state = null;
const { wrapper } = setupMocks({});
// Act
const isValid = wrapper.vm.arePagePrerequisitesValid();
// Assert
expect(isValid).toBe(false);
});
});
});
});
function setupMocks(customMountOptions) {
customMountOptions.store = store;
const mountOptions = getMountOptions(customMountOptions);
const mockMixin = {
methods: {
getCmsContent: jest.fn((widgetName, cmsFieldName) => {
return `${widgetName} ${cmsFieldName}`;
}),
},
};
mountOptions.global.mixins = [mockMixin];
const wrapper = shallowMount(review, mountOptions);
wrapper.vm.setCmsContent = jest.fn();
return { wrapper };
}

View file

@ -89,10 +89,6 @@
<hr class="my-0" />
</div>
<customerDetailsModalQuestion
ref="customerDetailsModalQuestion"
:previousCustomerValues="customerInfo" />
<funnelFooter
cmsWidgetName="FunnelFooterWidget"
@back-clicked="backButtonAction"
@ -115,10 +111,11 @@ import serviceLocationReview from "@/layouts/review/review-sections/service-loca
import scheduleReview from "@/layouts/review/review-sections/schedule-review/schedule-review";
import customerReview from "@/layouts/review/review-sections/customer-review/customer-review";
import customerDetailsModalQuestion from "@/layouts/review/review-sections/customer-review/customer-details-modal-question/customer-details-modal-question.vue";
import { AppointmentTypeStrings } from "@/constants/schedule-constants";
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { settleAllPromises } from "@/helpers/layout-helper";
import store from "@/store";
export default {
name: "review",
@ -153,7 +150,75 @@ export default {
},
methods: {
arePagePrerequisitesValid() {
return true;
// Vehicle
const vehicle = store.getters.order.vehicle;
const vehicleReqs = !!(vehicle.year && vehicle.make && vehicle.model && vehicle.style);
// Damage
const damage = store.getters.order.damage;
const damageReqs = !!(
(damage.isRepair && damage.numberOfChips) ||
(!damage.isRepair && damage.glassToReplace?.length)
);
// Service Package
const lineItems = store.getters.order.lineItems;
// damageReqs handles checking for damage, even though it is also required for this section.
const packageReqs = !!(
(damage.isRepair || lineItems.glassParts) &&
lineItems.supportingItems &&
lineItems.vaps
);
// Service Location
const serviceLocation = store.getters.order.serviceLocation;
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 = store.getters.order.schedule;
const scheduleReqs = !!(
schedule.date &&
schedule.startTime &&
schedule.endTime &&
schedule.jobMaxMinutes &&
schedule.jobMinMinutes
);
// Customer
const customer = store.getters.order.customer;
const customerReqs = !!(
customer.firstName &&
customer.lastName &&
customer.phoneNumber &&
customer.emailAddress
);
return (
vehicleReqs &&
damageReqs &&
packageReqs &&
serviceLocationReqs &&
scheduleReqs &&
customerReqs
);
},
backButtonAction() {
this.$router.navigateWithoutSaving(this.navigationScenarios.CLICKED_BACK, this.$route);
@ -190,7 +255,10 @@ export default {
);
},
editCustomerDetails() {
this.$refs.customerDetailsModalQuestion.openModal();
this.$router.navigateWithoutSaving(
this.navigationScenarios.CLICKED_CUSTOMER_EDIT,
this.$route
);
},
},
computed: {
@ -234,7 +302,6 @@ export default {
serviceLocationReview,
scheduleReview,
customerReview,
customerDetailsModalQuestion,
},
};
</script>

View file

@ -6,7 +6,6 @@ import { shallowMount } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import store from "@/store";
import router from "@/router";
import { nextTick } from "vue";
import baseMixin from "../../mixins/base-mixin";
// Mock basemixin
@ -130,64 +129,67 @@ afterEach(() => {
describe("schedule.vue...", () => {
describe("initial load", () => {
test("should pass arePagePrerequisitesValid with a mobile order and no providerNumber", () => {
//Arrange
// Arrange
const { wrapper } = setupMocks({});
store.getters.order.serviceLocation.appointmentType = "Mobile";
store.getters.order.serviceLocation.provider = null;
//Act
// Act
const arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid();
//Assert
// Assert
expect(arePagePrerequisitesValid).toBe(true);
});
test("should pass arePagePrerequisitesValid with a inshop order and providerNumber", () => {
//Arrange
// Arrange
const { wrapper } = setupMocks({});
//Act
// Act
const arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid();
//Assert
// Assert
expect(arePagePrerequisitesValid).toBe(true);
});
test("should fail arePagePrerequisitesValid with a replace with no glass parts", async () => {
//Arrange
// Arrange
const { wrapper } = setupMocks({});
store.getters.order.lineItems.glassParts = [];
//Act
// Act
const arePagePrerequisitesValid2 = await wrapper.vm.arePagePrerequisitesValid();
//Assert
// Assert
expect(arePagePrerequisitesValid2).toBe(false);
});
test("should fail arePagePrerequisitesValid without isInsurance", () => {
//Arrange
// Arrange
const { wrapper } = setupMocks({});
store.getters.payment.isInsurance = null;
//Act
// Act
const arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid();
//Assert
// Assert
expect(arePagePrerequisitesValid).toBe(false);
});
test("should return newShopTimeSlots when getAvailableDatesMethod is called", async () => {
//Arrange
// Arrange
const { wrapper } = setupMocks({});
wrapper.vm.selectableDatesData = {
days: [],
};
//Act
// Act
const newShopTimeSlots = await wrapper.vm.getAvailableDatesMethod(
"2023-01-01",
"2023-01-31"
);
//Assert
// Assert
expect(newShopTimeSlots).toStrictEqual({
days: [
{
@ -208,13 +210,13 @@ describe("schedule.vue...", () => {
});
test("should call API service in day ranges of 34 or less when getAvailableDatesMethod is called with large date ranges", async () => {
//Arrange
// Arrange
const { wrapper } = setupMocks({});
wrapper.vm.selectableDatesData = {
days: [],
};
//Act
// Act
await wrapper.vm.getAvailableDates.call(
wrapper.vm,
"2023-01-01",
@ -223,7 +225,7 @@ describe("schedule.vue...", () => {
"123"
);
//Assert
// Assert
expect(baseMixin.methods.dispatchStoreAction).toHaveBeenCalledTimes(3);
expect(baseMixin.methods.dispatchStoreAction).toHaveBeenCalledWith(
"getShopTimeSlots",
@ -234,7 +236,7 @@ describe("schedule.vue...", () => {
describe("beforeRouteEnter function... ", () => {
test("should call next() and call all functions within next", async () => {
//Arrange
// Arrange
const { wrapper } = setupMocks({});
wrapper.vm.selectableDatesData = {
days: [],
@ -244,7 +246,7 @@ describe("schedule.vue...", () => {
c(wrapper.vm);
});
//Act
// Act
await schedule.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "schedule" } },
@ -252,7 +254,7 @@ describe("schedule.vue...", () => {
nextFunction
);
//Assert
// Assert
expect(nextFunction).toHaveBeenCalled();
expect(wrapper.vm.setCmsContent).toHaveBeenCalledWith("content");
expect(wrapper.vm.$refs.datePicker.initializeComponent).toHaveBeenCalledWith(
@ -279,7 +281,7 @@ describe("schedule.vue...", () => {
describe("computed properties...", () => {
test("timeSlotsForSelectedDate should return timeslots if selected date is available", () => {
//Arrange
// Arrange
const { wrapper } = setupMocks({});
wrapper.vm.selectableDatesData = {
days: [
@ -306,10 +308,10 @@ describe("schedule.vue...", () => {
selectedDate: "2022-11-11",
});
//Act
// Act
const testValue = wrapper.vm.timeSlotsForSelectedDate;
//Assert
// Assert
expect(testValue).toStrictEqual(
expect.objectContaining({
date: "2022-11-11",
@ -318,7 +320,7 @@ describe("schedule.vue...", () => {
});
test("timeSlotsForSelectedDate should be null if no date has been selected", () => {
//Arrange
// Arrange
const { wrapper } = setupMocks({});
wrapper.vm.selectableDatesData = {
days: [
@ -345,10 +347,10 @@ describe("schedule.vue...", () => {
selectedDate: undefined,
});
//Act
// Act
const testValue = wrapper.vm.timeSlotsForSelectedDate;
//Assert
// Assert
expect(testValue).toBe(null);
});
});
@ -356,75 +358,67 @@ describe("schedule.vue...", () => {
describe("schedule page methods...", () => {
test("getServiceZipCtuCodeFromStore should return zipCodeCtu", () => {
//Arrange
// Arrange
const { wrapper } = setupMocks({});
wrapper.vm.selectableDatesData = {
days: [],
};
//Act
// Act
const testValue = wrapper.vm.getServiceZipCtuCodeFromStore();
//Assert
// Assert
expect(testValue).toStrictEqual("01234");
});
test("openInshopTimeSlotsModal should trigger openModal method", () => {
//Arrange
// Arrange
const { wrapper } = setupMocks({});
wrapper.vm.selectableDatesData = {
days: [],
};
//Act
// Act
wrapper.vm.openInshopTimeSlotsModal();
//Assert
// Assert
expect(wrapper.vm.$refs.timeSlotModalQuestion.openModal).toBeCalled();
});
test("getSelectedRouteCode should return schedule routeCode", () => {
//Arrange
const { wrapper } = setupMocks({});
wrapper.vm.selectableDatesData = {
days: [],
};
//Act
const testValue = wrapper.vm.getSelectedRouteCode();
//Assert
expect(testValue).toStrictEqual("000");
});
test("timeSlotModalClosed should null any selected date when there's no route code", () => {
//Arrange
// Arrange
const { wrapper } = setupMocks({});
wrapper.vm.selectableDatesData = {
days: [],
};
wrapper.setData({
selectedDate: "1980-05-05",
});
wrapper.setData({
selectedTimeSlot: {
const selectedTimeSlotInfo = {
timeSlot: {
date: "2019-01-01",
startTime: "09:00",
endTime: "10:00",
routeCode: null,
},
isPremiumAppointment: null,
};
wrapper.setData({
selectedTimeSlotInfo: selectedTimeSlotInfo,
});
//Act
// Act
wrapper.vm.timeSlotModalClosed();
//Assert
// Assert
expect(wrapper.vm.selectedDate).toBe(null);
});
test("getDisplayTextForMilitaryTime should return the correctly formatted string", () => {
//Arrange
// Arrange
const { wrapper } = setupMocks({});
wrapper.vm.selectableDatesData = {
days: [],
@ -432,13 +426,13 @@ describe("schedule.vue...", () => {
const timeInput1 = "15:00";
const timeInput2 = "15:30";
//Act
// Act
const testOutput1 = wrapper.vm.getDisplayTextForMilitaryTime(timeInput1);
const testOutput2 = wrapper.vm.getDisplayTextForMilitaryTime(timeInput2);
const testOutput3 = wrapper.vm.getDisplayTextForMilitaryTime(timeInput1, true);
const testOutput4 = wrapper.vm.getDisplayTextForMilitaryTime(timeInput2, true);
//Assert
// Assert
expect(testOutput1).toBe("3:00 PM");
expect(testOutput2).toBe("3:30 PM");
expect(testOutput3).toBe("3 PM");
@ -446,7 +440,7 @@ describe("schedule.vue...", () => {
});
test("getDisplayTextForMilitaryTime should return the correctly formatted string", () => {
//Arrange
// Arrange
const { wrapper } = setupMocks({});
wrapper.vm.selectableDatesData = {
days: [],
@ -454,10 +448,10 @@ describe("schedule.vue...", () => {
wrapper.vm.$router.navigateWithoutSaving = jest.fn();
wrapper.vm.$route = "testRoute";
//Act
// Act
wrapper.vm.backButtonAction();
//Assert
// Assert
expect(wrapper.vm.$router.navigateWithoutSaving).toBeCalledWith(
"CLICKED_BACK",
"testRoute"
@ -466,7 +460,7 @@ describe("schedule.vue...", () => {
});
test("forwardButtonAction should call route method navigateWithoutSaving", async () => {
//Arrange
// Arrange
const { wrapper } = setupMocks({});
wrapper.vm.dispatchStoreAction = jest.fn(() => {
return {
@ -477,15 +471,15 @@ describe("schedule.vue...", () => {
return {};
});
//Act
// Act
await wrapper.vm.forwardButtonAction();
//Assert
// Assert
expect(wrapper.vm.$router.navigateWithSaving).toHaveBeenCalled();
});
test("for mobile appts, updateSupportingItems should call store action to save supporting items", async () => {
//Arrange
// Arrange
store.getters.order.serviceLocation.appointmentType = "Mobile";
store.getters.lineItems.supportingItems = [
{
@ -514,10 +508,10 @@ describe("schedule.vue...", () => {
},
});
//Act
// Act
await wrapper.vm.updateSupportingItems();
//Assert
// Assert
expect(wrapper.vm.dispatchStoreAction).toBeCalledWith(
"saveSupportingItemsSuppressingStateResetting",
expect.arrayContaining([
@ -530,7 +524,7 @@ describe("schedule.vue...", () => {
});
test("for Inshop appts, updateSupportingItems should call store action to save supporting items WITHOUT the EARLY BIRD supporting item", async () => {
//Arrange
// Arrange
store.getters.order.serviceLocation.appointmentType = "Inshop";
store.getters.lineItems.supportingItems = [
{
@ -559,10 +553,10 @@ describe("schedule.vue...", () => {
},
});
//Act
// Act
await wrapper.vm.updateSupportingItems();
//Assert
// Assert
expect(wrapper.vm.dispatchStoreAction).toBeCalledWith(
"saveSupportingItemsSuppressingStateResetting",
expect.not.arrayContaining([
@ -575,7 +569,7 @@ describe("schedule.vue...", () => {
});
test("if no EARLY BIRD supporting item, then updateSupportingItems should NOT call store action", async () => {
//Arrange
// Arrange
store.getters.order.serviceLocation.appointmentType = "Mobile";
store.getters.lineItems.supportingItems = [];
const { wrapper } = setupMocks({});
@ -595,10 +589,10 @@ describe("schedule.vue...", () => {
},
});
//Act
// Act
await wrapper.vm.updateSupportingItems();
//Assert
// Assert
expect(wrapper.vm.dispatchStoreAction).not.toBeCalled();
});
});

View file

@ -24,17 +24,17 @@
<timeSlotModalQuestion
ref="timeSlotModalQuestion"
customComponentId="timeSlotModalQuestion"
v-model="selectedTimeSlotInfo"
cmsWidgetName="TimeSlotModalQuestion"
mobilePremiumCmsWidgetName="MobilePremiumTimeSlotModal"
mobileCmsWidgetName="MobileTimeSlotModal"
dropoffCmsWidgetName="DropOffTimeSlotModal"
sameDayDropOffCmsWidgetName="SameDayDropOffTimeSlotModal"
overnightDropOffCmsWidgetName="OvernightDropOffTimeSlotModal"
v-model="selectedTimeSlot"
:selectedDate="selectedDate"
:appointmentType="appointmentType"
:premiumAppointmentFee="mobilePremiumAppointmentFee"
:dateAndTimeSlotData="timeSlotsForSelectedDate"
:timeSlotsForSelectedDate="timeSlotsForSelectedDate"
:estimatedServiceMinutesMinimum="selectableDatesData.estimatedServiceMinutesMinimum"
:estimatedServiceMinutesMaximum="selectableDatesData.estimatedServiceMinutesMaximum"
@time-slot-modal-closed="timeSlotModalClosed"
@ -81,12 +81,7 @@ import store from "@/store";
// DEFINE VALIDATION RULES
defineRule("date-required", required(errorMessages.DATE_REQUIRED));
defineRule("time-slot-selection-required", (value) => {
if (
value.date == null ||
value.startTime == null ||
value.routeCode == null ||
value.estimatedServiceMinutesMaximum
) {
if (value?.timeSlot?.routeCode == null) {
return errorMessages.DATE_REQUIRED;
}
return true;
@ -190,7 +185,7 @@ export default {
data() {
return {
selectedDate: this.getSelectedDate(),
selectedTimeSlot: this.getSelectedTimeSlot(),
selectedTimeSlotInfo: this.getSelectedTimeSlotInfo(),
selectableDatesData: [],
mobilePremiumAppointmentFee: null,
};
@ -265,7 +260,7 @@ export default {
vm.mobilePremiumAppointmentFee = resultMap.premiumFeeWithPrice
? resultMap.premiumFeeWithPrice[0]
: null;
vm.updateFooterButtonText(vm.selectedTimeSlot);
vm.updateFooterButtonText(vm.selectedTimeSlotInfo);
});
},
computed: {
@ -293,18 +288,22 @@ export default {
splitCopyOnCMSPlaceHolder,
arePagePrerequisitesValid() {
const serviceLocation = store.getters.order.serviceLocation;
const serviceLocationPreReqs =
serviceLocation.zipCode &&
serviceLocation.zipCodeCtu &&
serviceLocation.appointmentType &&
(serviceLocation.appointmentType === AppointmentTypeStrings.MOBILE ||
serviceLocation.provider.providerNumber);
const paymentInfo = store.getters.payment.isInsurance !== null;
const supportingItems = store.getters.lineItems.supportingItems !== null;
const damageInfo =
store.getters.order.damage.isRepair ||
(store.getters.order.lineItems?.glassParts != null &&
store.getters.order.lineItems.glassParts.length > 0);
return serviceLocationPreReqs && paymentInfo && supportingItems && damageInfo;
},
async getAvailableDatesMethod(startDate, endDate) {
@ -327,45 +326,55 @@ export default {
openInshopTimeSlotsModal() {
this.$refs["timeSlotModalQuestion"].openModal();
},
getSelectedTimeSlot() {
return store.getters.order.schedule;
},
getSelectedDate() {
return store.getters.order.schedule.date;
},
getSelectedRouteCode() {
return store.getters.order.schedule.routeCode;
getSelectedTimeSlotInfo() {
const supportingItems = this.getSupportingItems();
const isPremiumAppointment =
!!supportingItems.filter((lineItem) => lineItem.partType === PREMIUM_FEE_PART_TYPE)
.length > 0;
const selectedTimeSlotInfo = {
timeSlot: store.getters.order.schedule,
isPremiumAppointment: isPremiumAppointment,
};
return selectedTimeSlotInfo;
},
getSupportingItems() {
return store.getters.lineItems.supportingItems;
},
timeSlotModalClosed() {
// Clear the selectedDate if no timeSlot has been selected
if (this.selectedTimeSlot.routeCode == null) {
if (this.selectedTimeSlotInfo.timeSlot.routeCode == null) {
this.selectedDate = null;
}
},
updateFooterButtonText(timeSlot) {
updateFooterButtonText(timeSlotInfo) {
let funnelFooterButtonText;
if (!timeSlot || !timeSlot.date) {
if (!timeSlotInfo || !timeSlotInfo.timeSlot.date) {
funnelFooterButtonText = "Continue";
} else {
funnelFooterButtonText = `Select ${this.convertSelectedDateToShortMonthAndDay(
timeSlot.date
timeSlotInfo.timeSlot.date
)}`;
if (this.appointmentType === AppointmentTypeStrings.IN_SHOP) {
funnelFooterButtonText += ` at ${this.getDisplayTextForMilitaryTime(
timeSlot.startTime
timeSlotInfo.timeSlot.startTime
)}`;
} else if (
this.appointmentType === AppointmentTypeStrings.MOBILE &&
!timeSlot.isPremiumAppointment
!timeSlotInfo.isPremiumAppointment
) {
funnelFooterButtonText += ` at ${this.getDisplayTextForMilitaryTime(
timeSlot.startTime,
timeSlotInfo.timeSlot.startTime,
true
)} - ${this.getDisplayTextForMilitaryTime(timeSlot.endTime, true)}`;
)} - ${this.getDisplayTextForMilitaryTime(
timeSlotInfo.timeSlot.endTime,
true
)}`;
}
}
this.$refs.funnelFooter.updateButtonText(funnelFooterButtonText);
@ -376,14 +385,16 @@ export default {
// Ex: April 25
return dateObject.toLocaleDateString("en-us", { month: "short", day: "numeric" });
},
// Expected input: "HH:MM"
getDisplayTextForMilitaryTime(militaryTimeInput, shouldTrimMinutesIfEmpty = false) {
// Expected input: "HH:MM"
let hours = parseInt(militaryTimeInput.split(":")[0]);
const minutes = militaryTimeInput.split(":")[1];
const meridianNotation = hours > 11 ? "PM" : "AM";
if (hours > 12) {
hours -= 12;
}
if (shouldTrimMinutesIfEmpty && minutes === "00") {
return `${hours} ${meridianNotation}`;
} else {
@ -395,7 +406,12 @@ export default {
},
forwardButtonAction() {
this.updateSupportingItems();
this.dispatchStoreAction(this.storeActions.SAVE_SCHEDULE, this.selectedTimeSlot, false);
this.dispatchStoreAction(
this.storeActions.SAVE_SCHEDULE,
this.selectedTimeSlotInfo.timeSlot,
false
);
this.$router.navigateWithSaving(this.navigationScenarios.CLICKED_FORWARD, this.$route);
},
@ -405,7 +421,7 @@ export default {
// if we have a premium fee(early bird), then save/update supporting items
if (
this.appointmentType === AppointmentTypeStrings.MOBILE &&
this.selectedTimeSlot?.isPremiumAppointment
this.selectedTimeSlotInfo?.isPremiumAppointment
) {
const earlyBirdIndex = supportingItems.findIndex(
(item) => item.partType == PREMIUM_FEE_PART_TYPE
@ -448,17 +464,20 @@ export default {
selectedDate(newValue, oldValue) {
// Clear time slot selection if date selected changes
if (newValue !== oldValue) {
this.selectedTimeSlot = {
date: null,
routeCode: null,
startTime: null,
endTime: null,
jobMaxMinutes: null,
jobMinMinutes: null,
this.selectedTimeSlotInfo = {
timeSlot: {
date: null,
routeCode: null,
startTime: null,
endTime: null,
jobMaxMinutes: null,
jobMinMinutes: null,
},
isPremiumAppointment: null,
};
}
},
selectedTimeSlot(newValue) {
selectedTimeSlotInfo(newValue) {
this.updateFooterButtonText(newValue);
},
},

View file

@ -2,30 +2,76 @@ import { shallowMount, flushPromises } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import timeSlotModalQuestion from "@/layouts/schedule/time-slot-modal-question/time-slot-modal-question.vue";
import store from "@/store";
import {
RouteCodeFlags,
PREMIUM_TIME_SLOT_ID_FLAG,
PREMIUM_FEE_PART_TYPE,
} from "@/constants/schedule-constants";
describe("time-slot-modal-list-button-question.vue", () => {
describe("time-slot-modal-question.vue", () => {
const timeSlotsForSelectedDate = {
date: "2023-09-02",
timeSlots: [
{
id: "03335-01820-S-B*20334*8 AM",
startTime: "08:00",
endTime: "09:00",
offerPremium: false,
},
{
id: "03335-01820-S-B*20334*830 AM",
startTime: "08:30",
endTime: "09:30",
offerPremium: false,
},
{
id: "03335-01820-S-B*20334*9 AM",
startTime: "09:00",
endTime: "10:00",
offerPremium: false,
},
{
id: "03335-01820-S-B*20334*930 AM",
startTime: "09:30",
endTime: "10:30",
offerPremium: false,
},
{
id: "03335-01820-S-B*20334*10 AM",
startTime: "10:00",
endTime: "11:00",
offerPremium: false,
},
{
id: "03335-01820-S-B*20334*1030 AM",
startTime: "10:30",
endTime: "11:30",
offerPremium: false,
},
],
};
test("When selected time slot is changed, a correctly formatted selectedTimeSlot should be emitted", async () => {
//Arrange
const date = "2023-07-01";
const timeSlotId = "testRouteCodeId";
const startTime = "8:00 AM";
const endTime = "8:30 AM";
const date = "2023-09-02";
const routeCode = "03335-01820-S-B*20334*830 AM";
const startTime = "08:30";
const endTime = "09:30";
const jobMaxMinutes = 100;
const jobMinMinutes = 50;
const expectedEmit = [
[
{
date: date,
routeCode: timeSlotId,
startTime: startTime,
endTime: endTime,
jobMaxMinutes: jobMaxMinutes.toString(),
jobMinMinutes: jobMinMinutes.toString(),
timeSlot: {
date: date,
routeCode: routeCode,
startTime: startTime,
endTime: endTime,
jobMaxMinutes: jobMaxMinutes.toString(),
jobMinMinutes: jobMinMinutes.toString(),
},
isPremiumAppointment: false,
},
],
];
@ -35,22 +81,16 @@ describe("time-slot-modal-list-button-question.vue", () => {
propsData: {
estimatedServiceMinutesMaximum: jobMaxMinutes,
estimatedServiceMinutesMinimum: jobMinMinutes,
dateAndTimeSlotData: {
date: date,
timeSlots: [
{
id: timeSlotId,
startTime: startTime,
endTime: endTime,
},
],
},
timeSlotsForSelectedDate: timeSlotsForSelectedDate,
},
},
});
wrapper.vm.$refs.timeSlots.closeModal = jest.fn();
wrapper.vm.selectedTimeSlotId = timeSlotId;
await wrapper.setData({
selectedRouteCode: routeCode,
});
//Act
wrapper.vm.setSelectedTimeSlot();
@ -59,7 +99,8 @@ describe("time-slot-modal-list-button-question.vue", () => {
expect(wrapper.emitted()["update:modelValue"]).toEqual(expectedEmit);
});
});
describe("time-slot-modal-list-button-question.vue Supplemental information", () => {
describe("time-slot-modal-question.vue Supplemental information", () => {
test("The correct supplemental information should be obtained from CMS - Mobile - Not Premium ", async () => {
//Arrange
const mobileCmsWidgetName = "MobileNotPremium";
@ -72,11 +113,13 @@ describe("time-slot-modal-list-button-question.vue Supplemental information", ()
},
},
});
//Assert
expect(wrapper.vm.supplementalInformationBlock).toEqual(
mockCmsContent[mobileCmsWidgetName]["BodyText"]
);
});
test("The correct supplemental information should be obtained from CMS - Mobile - Premium", async () => {
//Arrange
const mobileCmsWidgetName = "MobilePremium";
@ -89,11 +132,13 @@ describe("time-slot-modal-list-button-question.vue Supplemental information", ()
},
},
});
//Assert
expect(wrapper.vm.supplementalInformationBlock).toEqual(
mockCmsContent[mobileCmsWidgetName]["BodyText"]
);
});
test("No supplemental information should be obtained from CMS - Inshop", async () => {
//Arrange
const inshopWidgetName = "Inshop";
@ -109,6 +154,7 @@ describe("time-slot-modal-list-button-question.vue Supplemental information", ()
//Assert
expect(wrapper.vm.supplementalInformationBlock).toBeNull();
});
test("No supplemental information should be obtained from CMS - Drop off, nothing selected", async () => {
//Arrange
const dropOffWidgetName = "Dropoff";
@ -124,40 +170,69 @@ describe("time-slot-modal-list-button-question.vue Supplemental information", ()
//Assert
expect(wrapper.vm.supplementalInformationBlock).toBeNull();
});
test("Correct supplemental information should be retrieved, Dropoff regular", async () => {
//Arrange
const dropOffWidgetName = "Dropoff";
const modelValue = {
timeSlot: {
routeCode: `test${RouteCodeFlags.ALL_DAY_DROP_OFF}`,
date: null,
startTime: null,
endTime: null,
jobMaxMinutes: null,
jobMinMinutes: null,
},
isPremiumAppointment: null,
};
const { wrapper } = setupMocks({
customMountOptions: {
propsData: {
modelValue: { routeCode: "Test" + RouteCodeFlags.ALL_DAY_DROP_OFF },
modelValue: modelValue,
appointmentType: "Dropoff",
dropoffCmsWidgetName: dropOffWidgetName,
},
},
});
//Assert
expect(wrapper.vm.supplementalInformationBlock).toEqual(
mockCmsContent[dropOffWidgetName]["BodyText"]
);
});
test("Correct supplemental information should be retrieved, Dropoff Overnight", async () => {
//Arrange
const overnightDropOffCmsWidgetName = "Overnight";
const modelValue = {
timeSlot: {
routeCode: `test${RouteCodeFlags.OVERNIGHT_DROP_OFF}`,
date: null,
startTime: null,
endTime: null,
jobMaxMinutes: null,
jobMinMinutes: null,
},
isPremiumAppointment: null,
};
const { wrapper } = setupMocks({
customMountOptions: {
propsData: {
modelValue: { routeCode: "Test" + RouteCodeFlags.OVERNIGHT_DROP_OFF },
modelValue: modelValue,
appointmentType: "Dropoff",
overnightDropOffCmsWidgetName: overnightDropOffCmsWidgetName,
},
},
});
//Assert
expect(wrapper.vm.supplementalInformationBlock).toEqual(
mockCmsContent[overnightDropOffCmsWidgetName]["BodyText"]
);
});
test("Correct supplemental information should be retrieved, Dropoff Sameday", async () => {
//Arrange
const sameDayDropOffCmsWidgetName = "Sameday";
@ -167,13 +242,24 @@ describe("time-slot-modal-list-button-question.vue Supplemental information", ()
const endTime = "8:30 AM";
const jobMaxMinutes = 100;
const jobMinMinutes = 50;
const modelValue = {
timeSlot: {
routeCode: `test${RouteCodeFlags.ALL_DAY_DROP_OFF}`,
date: null,
startTime: null,
endTime: null,
jobMaxMinutes: null,
jobMinMinutes: null,
},
isPremiumAppointment: null,
};
const { wrapper } = setupMocks({
customMountOptions: {
propsData: {
estimatedServiceMinutesMaximum: jobMaxMinutes,
estimatedServiceMinutesMinimum: jobMinMinutes,
dateAndTimeSlotData: {
timeSlotsForSelectedDate: {
date: date,
timeSlots: [
{
@ -183,7 +269,7 @@ describe("time-slot-modal-list-button-question.vue Supplemental information", ()
},
],
},
modelValue: { routeCode: "Test" + RouteCodeFlags.ALL_DAY_DROP_OFF },
modelValue: modelValue,
appointmentType: "Dropoff",
sameDayDropOffCmsWidgetName: sameDayDropOffCmsWidgetName,
},
@ -208,12 +294,24 @@ describe("time-slot-modal-list-button-question.vue Disclaimer", () => {
const jobMaxMinutes = 100;
const jobMinMinutes = 50;
const modelValue = {
timeSlot: {
routeCode: `Test${RouteCodeFlags.ALL_DAY_DROP_OFF}`,
date: null,
startTime: null,
endTime: null,
jobMaxMinutes: null,
jobMinMinutes: null,
},
isPremiumAppointment: null,
};
const { wrapper } = setupMocks({
customMountOptions: {
propsData: {
estimatedServiceMinutesMaximum: jobMaxMinutes,
estimatedServiceMinutesMinimum: jobMinMinutes,
dateAndTimeSlotData: {
timeSlotsForSelectedDate: {
date: date,
timeSlots: [
{
@ -223,7 +321,7 @@ describe("time-slot-modal-list-button-question.vue Disclaimer", () => {
},
],
},
modelValue: { routeCode: "Test" + RouteCodeFlags.ALL_DAY_DROP_OFF },
modelValue: modelValue,
appointmentType: "Dropoff",
sameDayDropOffCmsWidgetName: sameDayDropOffCmsWidgetName,
},
@ -235,47 +333,88 @@ describe("time-slot-modal-list-button-question.vue Disclaimer", () => {
mockCmsContent[sameDayDropOffCmsWidgetName]["FooterText"]
);
});
test("Correct disclaimer information should be retrieved, Dropoff regular", async () => {
//Arrange
const dropOffWidgetName = "Dropoff";
const modelValue = {
timeSlot: {
routeCode: `Test${RouteCodeFlags.ALL_DAY_DROP_OFF}`,
date: null,
startTime: null,
endTime: null,
jobMaxMinutes: null,
jobMinMinutes: null,
},
isPremiumAppointment: null,
};
const { wrapper } = setupMocks({
customMountOptions: {
propsData: {
modelValue: { routeCode: "Test" + RouteCodeFlags.ALL_DAY_DROP_OFF },
modelValue: modelValue,
appointmentType: "Dropoff",
dropoffCmsWidgetName: dropOffWidgetName,
},
},
});
//Assert
expect(wrapper.vm.disclaimerTextBlockCopy).toEqual(
mockCmsContent[dropOffWidgetName]["FooterText"]
);
});
test("Correct disclaimer information should be retrieved, Dropoff overnight", async () => {
//Arrange
const overnightDropOffCmsWidgetName = "Overnight";
const modelValue = {
timeSlot: {
routeCode: `Test${RouteCodeFlags.OVERNIGHT_DROP_OFF}`,
date: null,
startTime: null,
endTime: null,
jobMaxMinutes: null,
jobMinMinutes: null,
},
isPremiumAppointment: null,
};
const { wrapper } = setupMocks({
customMountOptions: {
propsData: {
modelValue: { routeCode: "Test" + RouteCodeFlags.OVERNIGHT_DROP_OFF },
modelValue: modelValue,
appointmentType: "Dropoff",
overnightDropOffCmsWidgetName: overnightDropOffCmsWidgetName,
},
},
});
//Assert
expect(wrapper.vm.disclaimerTextBlockCopy).toEqual(
mockCmsContent[overnightDropOffCmsWidgetName]["FooterText"]
);
});
test("No disclaimer information should be retrieved, non-dropoff", async () => {
//Arrange
const inshopWidgetName = "Inshop";
const modelValue = {
timeSlot: {
routeCode: `test`,
date: null,
startTime: null,
endTime: null,
jobMaxMinutes: null,
jobMinMinutes: null,
},
isPremiumAppointment: null,
};
const { wrapper } = setupMocks({
customMountOptions: {
propsData: {
modelValue: { routeCode: "test" },
modelValue: modelValue,
appointmentType: "Inshop",
cmsWidgetName: inshopWidgetName,
},
@ -286,7 +425,7 @@ describe("time-slot-modal-list-button-question.vue Disclaimer", () => {
});
});
describe("time-slot-modal-list-button-question.vue Duration", () => {
describe("time-slot-modal-question.vue Duration", () => {
test("The correct duration information should be displayed - Dropoff - Sameday ", async () => {
//Arrange
const sameDayDropOffCmsWidgetName = "Sameday";
@ -297,12 +436,24 @@ describe("time-slot-modal-list-button-question.vue Duration", () => {
const jobMaxMinutes = 100;
const jobMinMinutes = 50;
const modelValue = {
timeSlot: {
routeCode: `test` + RouteCodeFlags.ALL_DAY_DROP_OFF,
date: null,
startTime: null,
endTime: null,
jobMaxMinutes: null,
jobMinMinutes: null,
},
isPremiumAppointment: null,
};
const { wrapper } = setupMocks({
customMountOptions: {
propsData: {
estimatedServiceMinutesMaximum: jobMaxMinutes,
estimatedServiceMinutesMinimum: jobMinMinutes,
dateAndTimeSlotData: {
timeSlotsForSelectedDate: {
date: date,
timeSlots: [
{
@ -312,7 +463,7 @@ describe("time-slot-modal-list-button-question.vue Duration", () => {
},
],
},
modelValue: { routeCode: "Test" + RouteCodeFlags.ALL_DAY_DROP_OFF },
modelValue: modelValue,
appointmentType: "Dropoff",
sameDayDropOffCmsWidgetName: sameDayDropOffCmsWidgetName,
},
@ -324,30 +475,59 @@ describe("time-slot-modal-list-button-question.vue Duration", () => {
mockCmsContent[sameDayDropOffCmsWidgetName]["SubheaderText"]
);
});
test("Correct duration information should be retrieved, Dropoff overnight", async () => {
//Arrange
const overnightDropOffCmsWidgetName = "Overnight";
const modelValue = {
timeSlot: {
routeCode: `test` + RouteCodeFlags.OVERNIGHT_DROP_OFF,
date: null,
startTime: null,
endTime: null,
jobMaxMinutes: null,
jobMinMinutes: null,
},
isPremiumAppointment: null,
};
const { wrapper } = setupMocks({
customMountOptions: {
propsData: {
modelValue: { routeCode: "Test" + RouteCodeFlags.OVERNIGHT_DROP_OFF },
modelValue: modelValue,
appointmentType: "Dropoff",
overnightDropOffCmsWidgetName: overnightDropOffCmsWidgetName,
},
},
});
//Assert
expect(wrapper.vm.durationTextBlockCopy).toEqual(
mockCmsContent[overnightDropOffCmsWidgetName]["SubheaderText"]
);
});
test("Correct duration information should be retrieved, Dropoff", async () => {
//Arrange
const dropoffCmsWidgetName = "Dropoff";
const modelValue = {
timeSlot: {
routeCode: `test` + RouteCodeFlags.ALL_DAY_DROP_OFF,
date: null,
startTime: null,
endTime: null,
jobMaxMinutes: null,
jobMinMinutes: null,
},
isPremiumAppointment: null,
};
const { wrapper } = setupMocks({
customMountOptions: {
propsData: {
modelValue: { routeCode: "Test" + RouteCodeFlags.ALL_DAY_DROP_OFF },
modelValue: modelValue,
appointmentType: "Dropoff",
dropoffCmsWidgetName: dropoffCmsWidgetName,
},
@ -358,25 +538,39 @@ describe("time-slot-modal-list-button-question.vue Duration", () => {
mockCmsContent[dropoffCmsWidgetName]["SubheaderText"]
);
});
test("Correct duration information should be retrieved, Inshop - convert to hours", async () => {
//Arrange
const cmsWidgetName = "Inshop";
const modelValue = {
timeSlot: {
routeCode: `test`,
date: null,
startTime: null,
endTime: null,
},
isPremiumAppointment: null,
};
const { wrapper } = setupMocks({
customMountOptions: {
propsData: {
estimatedServiceMinutesMaximum: 180,
estimatedServiceMinutesMinimum: 120,
modelValue: { routeCode: "Test" },
modelValue: modelValue,
appointmentType: "Inshop",
cmsWidgetName: cmsWidgetName,
},
},
});
//Assert
expect(wrapper.vm.durationTextBlockCopy).toEqual(
mockCmsContent[cmsWidgetName]["SubheaderText"] + " 2 - 3 hours"
);
});
test("Correct duration information should be retrieved, Inshop - convert to minutes", async () => {
//Arrange
const cmsWidgetName = "Inshop";
@ -396,6 +590,7 @@ describe("time-slot-modal-list-button-question.vue Duration", () => {
mockCmsContent[cmsWidgetName]["SubheaderText"] + " 60 - 90 minutes"
);
});
test("Correct duration information should be retrieved, Inshop - min = maximum time", async () => {
//Arrange
const cmsWidgetName = "Inshop";
@ -415,6 +610,7 @@ describe("time-slot-modal-list-button-question.vue Duration", () => {
mockCmsContent[cmsWidgetName]["SubheaderText"] + " 4 hours"
);
});
test("No duration information should be displayed, Mobile", async () => {
//Arrange
const mobileCmsWidgetName = "Mobile";
@ -450,7 +646,7 @@ describe("time-slot-modal-list-button-question.vue Available Timeslots", () => {
propsData: {
estimatedServiceMinutesMaximum: jobMaxMinutes,
estimatedServiceMinutesMinimum: jobMinMinutes,
dateAndTimeSlotData: {
timeSlotsForSelectedDate: {
date: date,
timeSlots: [
{
@ -473,6 +669,7 @@ describe("time-slot-modal-list-button-question.vue Available Timeslots", () => {
};
expect(wrapper.vm.availableTimeSlots[0]).toEqual(expectedTimeSlotObject);
});
test("Available Timeslots should be formatted correctly - Dropoff - Overnight ", async () => {
//Arrange
const overnightDropOffCmsWidgetName = "Overnight";
@ -488,7 +685,7 @@ describe("time-slot-modal-list-button-question.vue Available Timeslots", () => {
propsData: {
estimatedServiceMinutesMaximum: jobMaxMinutes,
estimatedServiceMinutesMinimum: jobMinMinutes,
dateAndTimeSlotData: {
timeSlotsForSelectedDate: {
date: date,
timeSlots: [
{
@ -511,6 +708,7 @@ describe("time-slot-modal-list-button-question.vue Available Timeslots", () => {
};
expect(wrapper.vm.availableTimeSlots[0]).toEqual(expectedTimeSlotObject);
});
test("Available Timeslots should be formatted correctly - Dropoff", async () => {
//Arrange
const dropoffCmsWidgetName = "Dropoff";
@ -526,7 +724,7 @@ describe("time-slot-modal-list-button-question.vue Available Timeslots", () => {
propsData: {
estimatedServiceMinutesMaximum: jobMaxMinutes,
estimatedServiceMinutesMinimum: jobMinMinutes,
dateAndTimeSlotData: {
timeSlotsForSelectedDate: {
date: date,
timeSlots: [
{
@ -549,6 +747,7 @@ describe("time-slot-modal-list-button-question.vue Available Timeslots", () => {
};
expect(wrapper.vm.availableTimeSlots[0]).toEqual(expectedTimeSlotObject);
});
test("Available Timeslots should be formatted correctly - Mobile not premium", async () => {
//Arrange
const mobileCmsWidgetName = "MobileNotPremium";
@ -564,7 +763,7 @@ describe("time-slot-modal-list-button-question.vue Available Timeslots", () => {
propsData: {
estimatedServiceMinutesMaximum: jobMaxMinutes,
estimatedServiceMinutesMinimum: jobMinMinutes,
dateAndTimeSlotData: {
timeSlotsForSelectedDate: {
date: date,
timeSlots: [
{
@ -587,6 +786,7 @@ describe("time-slot-modal-list-button-question.vue Available Timeslots", () => {
};
expect(wrapper.vm.availableTimeSlots[0]).toEqual(expectedTimeSlotObject);
});
test("Available Timeslots should be formatted correctly - Mobile with premium", async () => {
//Arrange
const mobilePremiumCmsWidgetName = "MobilePremium";
@ -605,7 +805,7 @@ describe("time-slot-modal-list-button-question.vue Available Timeslots", () => {
premiumAppointmentFee: {
partType: PREMIUM_FEE_PART_TYPE,
},
dateAndTimeSlotData: {
timeSlotsForSelectedDate: {
date: date,
timeSlots: [
{
@ -631,8 +831,10 @@ describe("time-slot-modal-list-button-question.vue Available Timeslots", () => {
buttonLabelSubCopy: "+$15.99",
value: timeSlotId + PREMIUM_TIME_SLOT_ID_FLAG,
};
expect(wrapper.vm.availableTimeSlots[0]).toEqual(expectedTimeSlotObject);
});
test("Available Timeslots should be formatted correctly - Inshop", async () => {
//Arrange
const cmsWidgetName = "Inshop";
@ -648,7 +850,7 @@ describe("time-slot-modal-list-button-question.vue Available Timeslots", () => {
propsData: {
estimatedServiceMinutesMaximum: jobMaxMinutes,
estimatedServiceMinutesMinimum: jobMinMinutes,
dateAndTimeSlotData: {
timeSlotsForSelectedDate: {
date: date,
timeSlots: [
{
@ -678,7 +880,7 @@ const mockCmsContent = {
HeaderText: "Mobile, not premium HeaderText",
},
MobilePremium: {
"BodyText:": "Mobile, with premium BodyText",
BodyText: "Mobile, with premium BodyText",
FooterText: "Mobile, with premium FooterText",
SubheaderText: "Mobile, with premium SubheaderText",
HeaderText: "Mobile, with premium HeaderText",

View file

@ -22,7 +22,7 @@
:answers="availableTimeSlots"
groupName="chooseTimeSlot"
textPosition="text-center"
v-model="selectedTimeSlotId"
v-model="selectedRouteCode"
isRequired
validationRules="time-slot-required" />
<div
@ -47,12 +47,12 @@ import buttonQuestion from "@/digital-components/button-question/button-question
import timeSlotModalListButton from "./time-slot-modal-list-button/time-slot-modal-list-button";
// Helpers
import { deepClone } from "@/helpers/object-helper";
import {
convertDateStringToDate,
militaryToTwelveHourTime,
getDisplayTextForDurationLength,
} from "@/layouts/schedule/helpers/schedule-helper";
import { deepClone } from "@/helpers/object-helper";
// Validation - TODO: Move this somewhere more global?
import { defineRule, useField } from "vee-validate";
@ -87,12 +87,15 @@ export default {
modelValue: {
type: Object,
default: () => ({
routeCode: null,
date: null,
startTime: null,
endTime: null,
jobMaxMinutes: null,
jobMinMinutes: null,
timeSlot: {
routeCode: null,
date: null,
startTime: null,
endTime: null,
jobMaxMinutes: null,
jobMinMinutes: null,
},
isPremiumAppointment: null,
}),
},
cmsWidgetName: String,
@ -102,7 +105,7 @@ export default {
sameDayDropOffCmsWidgetName: String,
overnightDropOffCmsWidgetName: String,
appointmentType: String,
dateAndTimeSlotData: Object,
timeSlotsForSelectedDate: Object,
premiumAppointmentFee: Object,
estimatedServiceMinutesMinimum: Number,
estimatedServiceMinutesMaximum: Number,
@ -113,7 +116,7 @@ export default {
data() {
return {
isModalOpened: false,
selectedValue: null,
selectedRouteCode: this.getSelectedRouteCode(),
timeSlotModalListButton: timeSlotModalListButton,
};
},
@ -150,36 +153,31 @@ export default {
modalName() {
return "timeSlots";
},
selectedTimeSlotId: {
get: function () {
return this.modelValue?.routeCode;
},
set: function (newValue) {
// Button Question only supports Number, or String data types so we must convert to the full object before emitting
this.selectedValue = this.getSelectedTimeSlotObject(newValue);
},
modal() {
return this.$refs[this.modalName];
},
supplementalInformationBlock() {
let appointmentTypeCmsWidgetName;
if (this.appointmentType === AppointmentTypeStrings.IN_SHOP) {
return null;
} else if (this.appointmentType === AppointmentTypeStrings.MOBILE) {
appointmentTypeCmsWidgetName = this.selectedTimeSlotId?.includes(
appointmentTypeCmsWidgetName = this.selectedRouteCode?.includes(
PREMIUM_TIME_SLOT_ID_FLAG
)
? this.mobilePremiumCmsWidgetName
: this.mobileCmsWidgetName;
} else {
if (!this.selectedTimeSlotId) {
if (!this.selectedRouteCode) {
return null;
} else {
appointmentTypeCmsWidgetName =
this.getRelevantDropOffCmsWidgetNameForSelectedTimeSlot(
this.selectedTimeSlotId,
this.selectedRouteCode,
true
);
}
}
return this.getCmsContent(
appointmentTypeCmsWidgetName,
cmsWidgetFieldMappings.SUPPLEMENTAL_INFORMATION
@ -232,13 +230,13 @@ export default {
},
disclaimerTextBlockCopy() {
if (this.appointmentType === AppointmentTypeStrings.DROP_OFF) {
if (this.selectedTimeSlotId?.includes(RouteCodeFlags.ALL_DAY_DROP_OFF)) {
if (this.selectedRouteCode?.includes(RouteCodeFlags.ALL_DAY_DROP_OFF)) {
if (this.isSameDay) {
return this.sameDayDropOffDisclaimerText;
} else {
return this.dropoffDisclaimerText;
}
} else if (this.selectedTimeSlotId?.includes(RouteCodeFlags.OVERNIGHT_DROP_OFF)) {
} else if (this.selectedRouteCode?.includes(RouteCodeFlags.OVERNIGHT_DROP_OFF)) {
return this.overnightDropOffDisclaimerText;
} else {
return null;
@ -281,9 +279,9 @@ export default {
} else if (this.appointmentType === AppointmentTypeStrings.IN_SHOP) {
return this.inshopDurationText;
} else {
if (this.selectedTimeSlotId?.includes(RouteCodeFlags.OVERNIGHT_DROP_OFF)) {
if (this.selectedRouteCode?.includes(RouteCodeFlags.OVERNIGHT_DROP_OFF)) {
return this.overnightDropoffDurationText;
} else if (this.selectedTimeSlotId?.includes(RouteCodeFlags.ALL_DAY_DROP_OFF)) {
} else if (this.selectedRouteCode?.includes(RouteCodeFlags.ALL_DAY_DROP_OFF)) {
if (this.isSameDay) {
return this.sameDayDropoffDurationText;
} else {
@ -295,21 +293,21 @@ export default {
}
},
isSameDay() {
if (!this.dateAndTimeSlotData) {
if (!this.timeSlotsForSelectedDate) {
return false;
}
const selectedDate = this.dateAndTimeSlotData.date;
const selectedDate = this.timeSlotsForSelectedDate.date;
const todaysDate = new Date().toISOString().split("T")[0];
return selectedDate === todaysDate;
},
dateSelectedReadableDate() {
if (!this.dateAndTimeSlotData) {
if (!this.timeSlotsForSelectedDate) {
return null;
}
// This conversion ensures we don't get get GMT induced date changes
const dateObject = convertDateStringToDate(this.dateAndTimeSlotData.date);
const dateObject = convertDateStringToDate(this.timeSlotsForSelectedDate.date);
// Ex: Tuesday, April 22
return dateObject.toLocaleDateString("en-us", {
weekday: "long",
@ -318,21 +316,20 @@ export default {
});
},
availableTimeSlots() {
if (!this.dateAndTimeSlotData) {
if (!this.timeSlotsForSelectedDate) {
return null;
}
if (this.appointmentType === AppointmentTypeStrings.DROP_OFF) {
return this.getAvailableTimeSlotsForDropOff(this.dateAndTimeSlotData.timeSlots);
return this.getAvailableTimeSlotsForDropOff(
this.timeSlotsForSelectedDate.timeSlots
);
} else if (this.appointmentType === AppointmentTypeStrings.MOBILE) {
return this.getAvailableTimeSlotsForMobile(this.dateAndTimeSlotData.timeSlots);
return this.getAvailableTimeSlotsForMobile(this.timeSlotsForSelectedDate.timeSlots);
} else {
return this.getAvailableTimeSlotsForInshop(this.dateAndTimeSlotData.timeSlots);
return this.getAvailableTimeSlotsForInshop(this.timeSlotsForSelectedDate.timeSlots);
}
},
modal() {
return this.$refs[this.modalName];
},
},
methods: {
openModal() {
@ -348,16 +345,19 @@ export default {
this.$emit("time-slot-modal-closed");
},
async setSelectedTimeSlot() {
this.$emit("update:modelValue", this.selectedValue);
this.$emit(
"update:modelValue",
this.getSelectedTimeSlotInfoObject(this.selectedRouteCode)
);
this.closeModal();
},
getRelevantDropOffCmsWidgetNameForSelectedTimeSlot(
selectedTimeSlotId,
selectedRouteCode,
isSameDayRelevant = false
) {
if (selectedTimeSlotId.includes(RouteCodeFlags.OVERNIGHT_DROP_OFF)) {
if (selectedRouteCode.includes(RouteCodeFlags.OVERNIGHT_DROP_OFF)) {
return this.overnightDropOffCmsWidgetName;
} else if (selectedTimeSlotId.includes(RouteCodeFlags.ALL_DAY_DROP_OFF)) {
} else if (selectedRouteCode.includes(RouteCodeFlags.ALL_DAY_DROP_OFF)) {
return this.isSameDay && isSameDayRelevant
? this.sameDayDropOffCmsWidgetName
: this.dropoffCmsWidgetName;
@ -416,8 +416,9 @@ export default {
getPremiumAppointmentTimeSlot(timeSlotData) {
const formattedPrice =
"+$" + this.getTotalLineItemPrice(this.premiumAppointmentFee).toFixed(2);
return {
// Unique value is required for each <input> and the premium appoinment shares a timeSlot ID
// Unique value is required for each <input> and the premium appoinment shares an id
value: this.addPremiumFlagToInput(timeSlotData.id),
buttonLabel: this.premiumAppointmentButtonText,
buttonLabelSubCopy: formattedPrice,
@ -426,41 +427,68 @@ export default {
},
};
},
autoSelectTimeSlotIfOnlyOneIsAvailable(newAvailableTimeSlotsValue) {
const numberOfOptions = newAvailableTimeSlotsValue?.length;
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.selectedTimeSlotId = newAvailableTimeSlotsValue[0].value;
this.selectedRouteCode = this.availableTimeSlots[0].value;
}
},
addPremiumFlagToInput(timeSlotId) {
return (timeSlotId += PREMIUM_TIME_SLOT_ID_FLAG);
addPremiumFlagToInput(routeCode) {
return (routeCode += PREMIUM_TIME_SLOT_ID_FLAG);
},
removePremiumFlagFromInput(timeSlotId) {
return timeSlotId.substring(0, timeSlotId.length - PREMIUM_TIME_SLOT_ID_FLAG.length);
removePremiumFlagFromInput(routeCode) {
return routeCode.replace(PREMIUM_TIME_SLOT_ID_FLAG, "");
},
getSelectedTimeSlotObject(timeSlotId) {
const timeSlot = this.dateAndTimeSlotData?.timeSlots?.find(
(timeSlot) => timeSlot.id == timeSlotId
getSelectedTimeSlotInfoObject(routeCode) {
const routeCodeIncludesPremium = routeCode?.includes(PREMIUM_TIME_SLOT_ID_FLAG);
if (routeCodeIncludesPremium) {
routeCode = this.removePremiumFlagFromInput(routeCode);
}
const timeSlot = this.timeSlotsForSelectedDate?.timeSlots?.find(
(timeSlot) => timeSlot.id == routeCode
);
if (timeSlot) {
return {
date: this.dateAndTimeSlotData.date,
routeCode: timeSlot.id,
startTime: timeSlot.startTime,
endTime: timeSlot.endTime,
jobMaxMinutes: this.estimatedServiceMinutesMaximum.toString(),
jobMinMinutes: this.estimatedServiceMinutesMinimum.toString(),
timeSlot: {
date: this.timeSlotsForSelectedDate.date,
routeCode: timeSlot.id,
startTime: timeSlot.startTime,
endTime: timeSlot.endTime,
jobMaxMinutes: this.estimatedServiceMinutesMaximum.toString(),
jobMinMinutes: this.estimatedServiceMinutesMinimum.toString(),
},
isPremiumAppointment: routeCodeIncludesPremium ? true : false,
};
}
return {
date: null,
startTime: null,
endTime: null,
routeCode: null,
jobMaxMinutes: null,
jobMinMinutes: null,
timeSlot: {
date: null,
startTime: null,
endTime: null,
routeCode: null,
jobMaxMinutes: null,
jobMinMinutes: null,
},
isPremiumAppointment: null,
};
},
},
@ -473,12 +501,10 @@ export default {
},
selectedDate: {
handler() {
this.selectedTimeSlotId = null;
this.selectedRouteCode = null;
this.autoSelectTimeSlotIfOnlyOneIsAvailable();
},
},
availableTimeSlots(newValue) {
this.autoSelectTimeSlotIfOnlyOneIsAvailable(newValue);
},
},
components: {
modal,

View file

@ -5,7 +5,7 @@
<label
for="mobileLocationLinkPromptId"
:aria-label="mobileLocationLinkPromptText"
class="form-label fw-bold w-100 ps-4 pe-4 pt-4"
class="form-label fw-bold w-100 ps-4 pe-4 pt-4 text-black"
v-html="mobileLocationLinkPromptText"></label>
<div class="update-mobile-location-text-link">
<textLink
@ -309,4 +309,7 @@ export default {
#mobileLocationLinkPromptId {
white-space: pre-line;
}
.text-black {
color: $black;
}
</style>

View file

@ -430,6 +430,7 @@ export default {
.question-text {
& > span {
text-align: center;
color: $black;
}
}
</style>

View file

@ -21,7 +21,7 @@
<div
v-if="!isLoaderDisplayed"
class="availability-badge"
:class="availabilityRating == 'high' ? 'green' : 'red'"></div>
:class="availabilityRating == 'high' ? 'green' : 'orange'"></div>
<span v-if="!isLoaderDisplayed" class="m-0 button-auxillary-copy">{{
badgeText
}}</span>
@ -88,7 +88,7 @@ export default {
if (this.availabilityRating == null) {
return "gray";
} else {
return this.availabilityRating == "high" ? "green" : "red";
return this.availabilityRating == "high" ? "green" : "orange";
}
},
badgeText() {
@ -187,11 +187,10 @@ export default {
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 13 12' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M6.5 12C3.19159 12 0.5 9.30841 0.5 6C0.5 2.69159 3.19159 0 6.5 0C9.80841 0 12.5 2.69159 12.5 6C12.5 9.30841 9.80841 12 6.5 12ZM6.5 0.785047C3.62449 0.785047 1.28505 3.12449 1.28505 6C1.28505 8.87551 3.62449 11.215 6.5 11.215C9.37551 11.215 11.715 8.87551 11.715 6C11.715 3.12449 9.37551 0.785047 6.5 0.785047Z' fill='%23006A36'/%3E%3Cpath d='M5.697 7.95252C5.5927 7.95252 5.49289 7.91102 5.41999 7.837L3.90597 6.32299C3.75233 6.16934 3.75233 5.92149 3.90597 5.76785C4.05962 5.6142 4.30747 5.6142 4.46111 5.76785L5.69812 7.00373L8.53999 4.16186C8.69364 4.00822 8.94149 4.00822 9.09513 4.16186C9.24878 4.31551 9.24878 4.56336 9.09513 4.717L5.97626 7.83588C5.90224 7.9099 5.80242 7.9514 5.69925 7.9514L5.697 7.95252Z' fill='%23006A36'/%3E%3C/svg%3E%0A");
}
&.red {
background-image: url("data:image/svg+xml,%3Csvg width='13' height='12' viewBox='0 0 13 12' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M6.5 12C3.19159 12 0.5 9.30841 0.5 6C0.5 2.69159 3.19159 0 6.5 0C9.80841 0 12.5 2.69159 12.5 6C12.5 9.30841 9.80841 12 6.5 12ZM6.5 0.785047C3.62449 0.785047 1.28505 3.12449 1.28505 6C1.28505 8.87551 3.62449 11.215 6.5 11.215C9.37551 11.215 11.715 8.87551 11.715 6C11.715 3.12449 9.37551 0.785047 6.5 0.785047Z' fill='%23006A36'/%3E%3Csvg width='13' height='12' viewBox='0 0 13 12' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M6.5 0C9.81368 0 12.5 2.68632 12.5 6C12.5 9.31368 9.81368 12 6.5 12C3.18632 12 0.5 9.31368 0.5 6C0.5 2.68632 3.18632 0 6.5 0ZM6.5 0.84C3.6548 0.84 1.34 3.1548 1.34 6C1.34 8.8452 3.6548 11.16 6.5 11.16C9.3452 11.16 11.66 8.8452 11.66 6C11.66 3.1548 9.3452 0.84 6.5 0.84ZM7.90018 4.00596C8.06422 3.84204 8.33002 3.84192 8.49406 4.00596C8.6581 4.17 8.6581 4.43592 8.49406 4.59996L7.0939 6L8.49406 7.40004C8.6581 7.56408 8.6581 7.83 8.49406 7.99404C8.4121 8.076 8.30458 8.11704 8.19706 8.11704C8.08966 8.11704 7.98214 8.076 7.90018 7.99404L6.50002 6.594L5.09986 7.99404C5.01778 8.076 4.91038 8.11704 4.80286 8.11704C4.69546 8.11704 4.58794 8.076 4.50598 7.99404C4.34182 7.83 4.34182 7.56408 4.50598 7.40004L5.90614 6L4.50598 4.59996C4.34182 4.43592 4.34182 4.17 4.50598 4.00596C4.66978 3.84192 4.93582 3.84204 5.09986 4.00596L6.50002 5.406L7.90018 4.00596Z' fill='%23AC160B'/%3E%3C/svg%3E%3C/svg%3E%0A");
&.orange {
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 13 12' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M6.5 0C9.81368 0 12.5 2.68632 12.5 6C12.5 9.31368 9.81368 12 6.5 12C3.18632 12 0.5 9.31368 0.5 6C0.5 2.68632 3.18632 0 6.5 0ZM6.5 0.84C3.6548 0.84 1.34 3.1548 1.34 6C1.34 8.8452 3.6548 11.16 6.5 11.16C9.3452 11.16 11.66 8.8452 11.66 6C11.66 3.1548 9.3452 0.84 6.5 0.84ZM7.90018 4.00596C8.06422 3.84204 8.33002 3.84192 8.49406 4.00596C8.6581 4.17 8.6581 4.43592 8.49406 4.59996L7.0939 6L8.49406 7.40004C8.6581 7.56408 8.6581 7.83 8.49406 7.99404C8.4121 8.076 8.30458 8.11704 8.19706 8.11704C8.08966 8.11704 7.98214 8.076 7.90018 7.99404L6.50002 6.594L5.09986 7.99404C5.01778 8.076 4.91038 8.11704 4.80286 8.11704C4.69546 8.11704 4.58794 8.076 4.50598 7.99404C4.34182 7.83 4.34182 7.56408 4.50598 7.40004L5.90614 6L4.50598 4.59996C4.34182 4.43592 4.34182 4.17 4.50598 4.00596C4.66978 3.84192 4.93582 3.84204 5.09986 4.00596L6.50002 5.406L7.90018 4.00596Z' fill='%23E86421'/%3E%3C/svg%3E%0A");
}
}
.button-auxillary-copy {
box-sizing: border-box;
justify-content: right;
@ -218,9 +217,9 @@ export default {
background-color: $green-100;
}
&.red {
color: $red-600;
background-color: $red-100;
&.orange {
color: $orange-600;
background-color: $orange-100;
}
&.gray {

View file

@ -1,5 +1,9 @@
<template>
<dropdownQuestion disableAutoFill v-model="selectedValue" :valueAsKey="true" />
<dropdownQuestion
disableAutoFill
v-model="selectedValue"
:valueAsKey="true"
:disableValidationIfElementDisabled="true" />
</template>
<script>

View file

@ -112,17 +112,15 @@ export default {
selectedMake: this.selectedMakefromStore(),
selectedModel: this.selectedModelfromStore(),
selectedStyle: this.selectedStylefromStore(),
carId: null,
category: null,
imageUrl: null,
imageVifNumber: null,
imageVifColor: null,
carId: this.getCarIdfromStore(),
category: this.getCategoryfromStore(),
imageUrl: this.getImagefromStore(),
imageVifNumber: this.getImageVifNumberfromStore(),
imageVifColor: this.getImageVifColorfromStore(),
yearOptions: [],
makeOptions: [],
modelOptions: [],
styleOptions: [],
displayGeneric: this.displayGenericFromStore(),
unmatchedVehicleIcon: false,
};
},
@ -242,19 +240,24 @@ export default {
}
} else {
this.makeOptions = [];
this.selectedMake = null;
this.selectedModel = null;
this.selectedStyle = null;
}
},
async selectedMake(make) {
if (make) {
const result = await this.getModelOptions(this.selectedYear, make);
this.modelOptions = result?.data;
if (this.selectedMakefromStore() !== make) {
if (this.modelOptions.length === 1) this.selectedModel = this.modelOptions[0];
else this.selectedModel = null;
if (this.modelOptions.length === 1) this.selectedModel = this.modelOptions[0];
else {
this.selectedModel = null;
this.selectedStyle = null;
}
} else {
this.modelOptions = [];
this.selectedModel = null;
this.selectedStyle = null;
}
},
async selectedModel(model) {
@ -265,16 +268,16 @@ export default {
model
);
this.styleOptions = result?.data;
if (this.selectedModelfromStore() !== model) {
if (this.styleOptions.length === 1) this.selectedStyle = this.styleOptions[0];
else this.selectedStyle = null;
}
if (this.styleOptions.length === 1) this.selectedStyle = this.styleOptions[0];
else this.selectedStyle = null;
} else {
this.styleOptions = [];
this.selectedStyle = null;
}
},
async selectedStyle(style) {
if (style) {
this.carId = null;
const result = await this.getVehicle(
this.selectedYear,
this.selectedMake,
@ -286,17 +289,23 @@ export default {
this.imageUrl = result?.data.imageUrl;
this.imageVifNumber = result?.data.imageVifNumber;
this.imageVifColor = result?.data.imageVifColor;
if (this.imageUrl == null) {
this.displayGeneric = false;
this.unmatchedVehicleIcon = true;
} else this.unmatchedVehicleIcon = false;
} else {
this.imageUrl = null;
this.displayGeneric = true;
}
},
},
computed: {
unmatchedVehicleIcon() {
if (this.imageUrl == null && this.carId !== null) return true;
else return false;
},
displayGeneric() {
if (!this.selectedStyle) return true;
else if (this.carId == null) return true;
else if (this.imageUrl == null && this.carId !== null) return false;
else return true;
},
},
methods: {
getVehicle(year, make, model, style) {
return this.dispatchStoreAction(this.storeActions.GET_VEHICLE, {
@ -306,15 +315,6 @@ export default {
style: style,
});
},
displayGenericFromStore() {
if (store.getters.vehicle.imageUrl !== null) return false;
else if (
store.getters.vehicle.imageUrl === null &&
store.getters.vehicle.carId !== null
)
return false;
else return true;
},
arePagePrerequisitesValid() {
return true;
},
@ -377,6 +377,21 @@ export default {
model: model,
});
},
getImagefromStore() {
return store.getters.vehicle.imageUrl;
},
getCarIdfromStore() {
return store.getters.vehicle.carId;
},
getCategoryfromStore() {
return store.getters.vehicle.category;
},
getImageVifNumberfromStore() {
return store.getters.vehicle.imageVifNumber;
},
getImageVifColorfromStore() {
return store.getters.vehicle.imageVifColor;
},
},
components: {

View file

@ -26,6 +26,7 @@ const navigationScenarios = {
SELECTED_LICENSE_PLATE: "SELECTED_LICENSE_PLATE",
SELECTED_HOME_ADDRESS: "SELECTED_HOME_ADDRESS",
CLICKED_FORWARD_WITH_NO_QUESTIONS: "CLICKED_FORWARD_WITH_NO_QUESTIONS",
CLICKED_VIN_RETRY: "CLICKED_VIN_RETRY",
// Part selection
CLICKED_FORWARD_WITH_PART_QUESTIONS: "CLICKED_FORWARD_WITH_PART_QUESTIONS",
@ -40,12 +41,16 @@ const navigationScenarios = {
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",
// Review
CLICKED_VEHICLE_EDIT: "CLICKED_VEHICLE_EDIT",
CLICKED_DAMAGE_EDIT: "CLICKED_DAMAGE_EDIT",
CLICKED_SERVICE_PACKAGE_EDIT: "CLICKED_SERVICE_PACKAGE_EDIT",
CLICKED_SERVICE_LOCATION_EDIT: "CLICKED_SERVICE_LOCATION_EDIT",
CLICKED_SCHEDULE_EDIT: "CLICKED_SCHEDULE_EDIT",
CLICKED_CUSTOMER_EDIT: "CLICKED_CUSTOMER_EDIT",
};
export { navigationScenarios };

View file

@ -76,6 +76,10 @@ const routingTable = function (store) {
scenario: navigationScenarios.CLICKED_FORWARD_WITH_NO_MORE_QUESTIONS,
destinationFmgPageValue: fmgPageValues.QUOTE,
},
{
scenario: navigationScenarios.CLICKED_VIN_RETRY,
destinationFmgPageValue: fmgPageValues.ESTIMATE,
},
],
},
{
@ -109,6 +113,10 @@ const routingTable = function (store) {
scenario: navigationScenarios.CLICKED_FORWARD_WITH_NO_MORE_QUESTIONS,
destinationFmgPageValue: fmgPageValues.QUOTE,
},
{
scenario: navigationScenarios.CLICKED_VIN_RETRY,
destinationFmgPageValue: fmgPageValues.ESTIMATE,
},
],
},
{
@ -146,6 +154,10 @@ const routingTable = function (store) {
scenario: navigationScenarios.CLICKED_FORWARD_WITH_NO_MORE_QUESTIONS,
destinationFmgPageValue: fmgPageValues.QUOTE,
},
{
scenario: navigationScenarios.CLICKED_VIN_RETRY,
destinationFmgPageValue: fmgPageValues.ESTIMATE,
},
],
},
{
@ -179,6 +191,10 @@ const routingTable = function (store) {
scenario: navigationScenarios.CLICKED_FORWARD_WITH_NO_MORE_QUESTIONS,
destinationFmgPageValue: fmgPageValues.QUOTE,
},
{
scenario: navigationScenarios.CLICKED_VIN_RETRY,
destinationFmgPageValue: fmgPageValues.ESTIMATE,
},
],
},
{
@ -399,6 +415,10 @@ const routingTable = function (store) {
scenario: navigationScenarios.CLICKED_FORWARD,
destinationFmgPageValue: fmgPageValues.CUSTOMER_DETAILS,
},
{
scenario: navigationScenarios.CLICKED_CHANGE_LOCATION,
destinationFmgPageValue: fmgPageValues.SERVICE_LOCATION,
},
],
},
{
@ -441,6 +461,10 @@ const routingTable = function (store) {
scenario: navigationScenarios.CLICKED_SCHEDULE_EDIT,
destinationFmgPageValue: fmgPageValues.SCHEDULE,
},
{
scenario: navigationScenarios.CLICKED_CUSTOMER_EDIT,
destinationFmgPageValue: fmgPageValues.CUSTOMER_DETAILS,
},
],
},
];

View file

@ -336,8 +336,6 @@ export const mutations = {
},
resetRegistrationState(state) {
state.order.vehicle.registration.licensePlate = null;
state.order.customer.firstName = null;
state.order.customer.lastName = null;
},
resetGlassPartsState(state) {
state.order.lineItems.glassParts = null;
@ -481,7 +479,7 @@ export const mutations = {
state.order.customer.emailAddress = sessionInformation.order.customer.emailAddress;
state.order.customer.firstName = sessionInformation.order.customer.firstName;
state.order.customer.lastName = sessionInformation.order.customer.lastName;
state.order.customer.phoneNumber = sessionInformation.order.customer.servicePhoneNumber;
state.order.customer.phoneNumber = sessionInformation.order.customer.phoneNumber;
state.order.customer.isSmsOptIn = sessionInformation.order.customer.isSmsOptIn;
state.order.existingPromoCode = sessionInformation.order.existingPromoCode;
@ -1436,7 +1434,7 @@ export const actions = {
firstName: order.customer.firstName,
lastName: order.customer.lastName,
isSmsOptIn: order.customer.isSmsOptIn,
servicePhoneNumber: order.customer.phoneNumber,
phoneNumber: order.customer.phoneNumber,
},
damage: {
numberOfChips: damage.numberOfChips,
@ -1675,10 +1673,7 @@ export const actions = {
{ isSelectedGlassAvailableForVehicle, vehicleInfo, registrationInfo }
) {
//Reset dependent state when changing
if (
registrationInfo?.firstName !== context.state.order.customer?.firstName ||
registrationInfo?.lastName !== context.state.order.customer?.lastName
) {
if (vehicleInfo.vin !== context.state.order.vehicle.vin) {
context.dispatch(storeActions.RESET_REGISTRATION_STATE_AND_DEPENDENCIES);
if (!isSelectedGlassAvailableForVehicle) {

View file

@ -60,6 +60,10 @@ $gray-700: #303333; // Used in theme
$gray-800: #222424; // Used in theme
$gray-900: #181a1a; // Used in theme
//orange
$orange-600: #e86421;
$orange-100: #fff5eb;
// Miscellaneous Colors
$indigo: #6610f2;
$purple: #6f42c1;

View file

@ -1,4 +1,5 @@
<template>
<loadingModal v-if="useLoadingModal" notFullScreen ref="loadingModal" />
<a v-if="linkType === 'navigation'" @click="handleClick" class="navigation-link" :href="href"
>{{ text }}<slot name="after-text"></slot
></a>
@ -19,6 +20,8 @@
</template>
<script>
import loadingModal from "@/fmg-components/loading-modal/loading-modal";
export default {
name: "textLink",
props: {
@ -27,6 +30,10 @@ export default {
href: {
type: String,
},
useLoadingModal: {
type: Boolean,
default: false,
},
},
methods: {
handleClick(event) {
@ -36,9 +43,17 @@ export default {
this.text,
true
);
if (this.useLoadingModal) {
this.$refs.loadingModal.showModal();
}
this.$emit("click-event");
},
},
components: {
loadingModal,
},
};
</script>