Merge branch 'develop' into feature/CASH-185

This commit is contained in:
Johnny Shultz 2025-02-24 09:58:23 -05:00 committed by GitHub
commit ea03889bb1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
51 changed files with 2624 additions and 235 deletions

View file

@ -1,5 +1,6 @@
{
"tabWidth": 4,
"htmlWhitespaceSensitivity": "ignore",
"bracketSameLine": true,
"printWidth": 100,
"trailingComma" : "es5"

View file

@ -28,12 +28,13 @@ module.exports = {
"!src/layouts/insurance/*.vue", // Temp test exclusion while in development
"!src/layouts/insurance-company/*.vue", // Temp test exclusion while in development
"!src/layouts/insurance-company/insurance-company-question/*.vue", // Temp test exclusion while in development
"!src/experiment-components/*.vue",
// END
], // ! means exclude from coverage.
testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"],
coverageThreshold: {
global: {
statements: 75,
statements: 74,
},
},
// Uncomment this to avoid the massive amount of warnings we are getting for onSubmit and onInvalidSubmit

View file

@ -20,6 +20,8 @@ const experimentSettings = {
DISPLAY_MSR: "DisplayMSR",
DISPLAY_WAITLIST: "DisplayWaitlist2.0",
WAITLIST_THRESHOLD_DAYS: "Waitlist_Threshold_Days",
PRICING_BY_DAY: "DisplayPricingByDay",
FOSTER_LOVE: "DisplayFosterLove",
};
const experimentTriggers = {

View file

@ -108,6 +108,7 @@ const storeActions = {
CREATE_SUBMITTED_ORDER: "createSubmittedOrder",
RESET_SUBMITTED_ORDER: "resetSubmittedOrder",
ADD_DONATION_TO_SUBMITTED_ORDER: "addDonationToSubmittedOrder",
RESET_EXTERNAL_PARAMETER_STATE: "resetExternalParameterState",
UPDATE_EXTERNAL_PARAMETER_MMS: "updateExternalParameterMMS",

View file

@ -16,8 +16,6 @@ const storeMutations = {
UPDATE_VEHICLE_IMAGE_COLOR: "updateVehicleImageColor",
UPDATE_VEHICLE_VIN: "updateVehicleVin",
UPDATE_VEHICLE: "updateVehicle",
UPDATE_VEHICLE_MOBILE_STATIC_RECALIBRATION_APPLICABLE:
"updateIsMobileStaticRecalibrationApplicable",
UPDATE_IS_REPAIR: "updateIsRepair",
UPDATE_NUMBER_OF_CHIPS: "updateNumberOfChips",

View file

@ -55,6 +55,7 @@ https://safelite.atlassian.net/wiki/spaces/DC/pages/76644418/Button+Question+Com
:additionalButtonData="answer.additionalButtonData"
:lastValuePushedToGa="lastValuePushedToGa"
:setLastValuePushedToGa="setLastValuePushedToGa"
:isInsuranceSelected="isInsuranceSelected"
:suppressError="suppressError"
@buttonEvent="handleButtonEvent"
v-model="selectedValues" />
@ -139,6 +140,7 @@ export default {
type: Boolean,
default: false,
},
isInsuranceSelected: Boolean,
},
setup(props) {
const propsClone = Object.assign({}, props);

View file

@ -17,18 +17,40 @@
<div
v-if="calendarViewDirection === 'future'"
class="legend caption d-flex align-items-center justify-content-end">
<span class="legend-circle me-1"></span> &equals; Available
<span class="legend-circle me-1"></span>
&equals; Available
</div>
<div class="separator-line"></div>
<div class="nav-back ps-3"><button></button></div>
<div class="nav-forward pe-3"><button></button></div>
<div class="grid-item caption"><span class="sr-only">Sunday</span>S</div>
<div class="grid-item caption"><span class="sr-only">Monday</span>M</div>
<div class="grid-item caption"><span class="sr-only">Tuesday</span>T</div>
<div class="grid-item caption"><span class="sr-only">Wednesday</span>W</div>
<div class="grid-item caption"><span class="sr-only">Thursday</span>T</div>
<div class="grid-item caption"><span class="sr-only">Friday</span>F</div>
<div class="grid-item caption"><span class="sr-only">Saturday</span>S</div>
<div class="grid-item caption">
<span class="sr-only">Sunday</span>
S
</div>
<div class="grid-item caption">
<span class="sr-only">Monday</span>
M
</div>
<div class="grid-item caption">
<span class="sr-only">Tuesday</span>
T
</div>
<div class="grid-item caption">
<span class="sr-only">Wednesday</span>
W
</div>
<div class="grid-item caption">
<span class="sr-only">Thursday</span>
T
</div>
<div class="grid-item caption">
<span class="sr-only">Friday</span>
F
</div>
<div class="grid-item caption">
<span class="sr-only">Saturday</span>
S
</div>
<div
v-for="date in month.dates"
:key="date.inputValue"

View file

@ -16,8 +16,7 @@
:maxlength="maxLength"
role="textbox"
aria-multiline="true"
:aria-required="isRequired">
</textarea>
:aria-required="isRequired"></textarea>
<p
tabindex="-1"
class="caption mt-2 mb-0 w-100"

View file

@ -75,9 +75,9 @@
class="d-inline-flex small mt-1"
aria-atomic="true"
aria-live="polite"
:class="[centerErrorMessage ? 'center-error-message' : '']"
>{{ errorMessage }}</span
>
:class="[centerErrorMessage ? 'center-error-message' : '']">
{{ errorMessage }}
</span>
</div>
</div>
</template>

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,284 @@
<template>
<div class="donation-block">
<form ref="donationForm" id="donation-form" @submit.prevent="handleDonationAction">
<div class="row g-0">
<div class="col-12">
<p class="headline text-center" v-html="DonationHeadline"></p>
</div>
</div>
<div class="row-align d-flex px-4">
<div>
<img class="w-100 mt-1 me-4" :src="LogoImage" />
</div>
<div>
<p class="small subheadline mb-2" v-html="DonationSubHeadline"></p>
<p class="caption mb-0" v-html="DonationBodyText"></p>
<textLink
linkType="newWindowLink"
text="Learn more"
:href="DonationBodySubText"
target="_blank" />
</div>
</div>
<div
class="px-4"
:class="[showDonationSuccess ? 'hide-donation-block' : 'show-donation-block']">
<div class="row">
<div class="col-12">
<div class="donation-slider my-4">
<input
name="amount"
id="one"
type="radio"
:value="donationValues[0]"
checked />
<label for="one" class="label">$1</label>
<input
name="amount"
id="three"
type="radio"
:value="donationValues[1]" />
<label for="three" class="label">$3</label>
<input
name="amount"
id="five"
type="radio"
:value="donationValues[2]" />
<label for="five" class="label">$5</label>
<div class="indicator-bar"></div>
</div>
</div>
</div>
<div class="row">
<div class="col-12">
<button
class="btn btn-secondary w-100 mb-2"
v-html="DonationAddButtonWidget"></button>
</div>
</div>
</div>
<div class="row px-4">
<div class="col-12">
<div
v-if="showDonationSuccess"
class="d-flex justify-content-center align-items-center btn-success w-100 mb-2"
v-html="DonationThanksAlertWidget"></div>
</div>
</div>
<div class="row px-4">
<div class="col">
<alert
v-if="showDonationError"
cmsWidgetName="DonationErrorWidget"
alertClass="alert-danger" />
</div>
</div>
<div class="row">
<div class="col-12">
<p class="my-4 mx-0 text-center caption" v-html="DonationFooterText"></p>
</div>
</div>
</form>
</div>
</template>
<script>
import textLink from "@/ux-components/text-link/text-link";
import alert from "@/ux-components/alert/alert";
export default {
name: "donationBlock",
props: {
cmsWidgetName: String,
modelValue: String,
showDonationSuccess: Boolean,
showDonationError: Boolean,
donationValues: Array,
},
computed: {
DonationHeadline() {
return this.getCmsContent(this.cmsWidgetName, "HeaderText");
},
DonationSubHeadline() {
return this.getCmsContent(this.cmsWidgetName, "SubheaderText");
},
DonationBodyText() {
return this.getCmsContent(this.cmsWidgetName, "BodyText");
},
DonationBodySubText() {
return this.getCmsContent(this.cmsWidgetName, "BodyText2");
},
DonationAddButtonWidget() {
return this.getCmsContent("DonationAddButtonWidget", "Text");
},
DonationThanksAlertWidget() {
return this.getCmsContent("DonationThanksAlertWidget", "Text");
},
DonationFooterText() {
return this.getCmsContent(this.cmsWidgetName, "FooterText");
},
LogoImage() {
return this.getCmsContent("DonationWidget", "Image");
},
selectedValue: {
get: function () {
return this.modelValue?.toString();
},
set: function (newValue) {
this.$emit("update:modelValue", newValue);
},
},
},
methods: {
handleDonationAction(submitEvent) {
console.log(
"running handleDonationAction(), submitEvent.target.elements.amount.value: ",
submitEvent.target.elements.amount.value
);
this.selectedValue = submitEvent.target.elements.amount.value;
},
},
components: {
textLink,
alert,
},
};
</script>
<style lang="scss" scoped>
.donation-block {
padding: 1rem 0;
background-color: $gray-100;
border-radius: 0;
.hide-donation-block {
max-height: 0;
overflow: hidden;
margin-top: 1rem;
opacity: 0;
transition: all 250ms ease-in;
}
.show-donation-block {
max-height: 136px;
overflow: hidden;
margin-top: 0;
opacity: 1;
transition: all 250ms ease-in;
}
p {
&.headline {
font-family: AvertaBold;
color: $red;
font-weight: 600;
}
&.subheadline {
font-family: AvertaBold;
font-weight: 600;
}
}
:deep(a) {
&.new-window-link {
color: $blue;
font-size: 0.75rem;
}
}
img {
width: 5rem;
}
.donation-slider {
position: relative;
width: 100%;
padding: 0;
display: flex;
flex-direction: row;
justify-content: center;
background: $blue-100;
border-radius: 3rem;
box-shadow: 0px 0px 4px 1px rgba(0, 112, 209, 1) inset;
font-size: 0.875rem;
.label {
position: relative;
display: inline-flex;
width: 33.3333%;
height: 3rem;
padding: 0;
cursor: pointer;
transition: color 200ms ease-out;
align-items: center;
justify-content: center;
color: $blue;
z-index: 1;
}
.indicator-bar {
width: 33.3333%;
height: 3rem;
position: absolute;
top: 0;
left: -2rem;
background: $blue;
border-radius: 3rem;
box-shadow: 0px 0px 0px 1px rgba(0, 112, 209, 1) inset;
transition: transform 750ms cubic-bezier(0.02, 0.94, 0.09, 0.97);
transform: translate3d(2rem, 0, 0);
}
.indicator-bar {
background: $blue;
}
input {
&#one:checked {
~ .indicator-bar {
transform: translateX(2rem);
}
}
&#three:checked {
~ .indicator-bar {
transform: translateX(calc(100% + 2rem));
}
}
&#five:checked {
~ .indicator-bar {
transform: translateX(calc(200% + 2rem));
}
}
}
input:checked {
+ label {
color: $white;
}
}
input[type="radio"] {
&:not(:checked),
&:checked {
position: absolute;
left: -999999px;
display: none;
}
}
}
.alert {
border: 1px solid $red;
}
.btn-success {
background: $green-100;
border: 1px solid $green-400;
border-radius: 0.5rem;
height: 3rem;
padding: 0.5rem;
font-weight: 600;
&:before {
content: "";
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 17 16'%3E%3Cg clip-path='url(%23a)'%3E%3Cpath fill='%230C7E47' d='M8.5 0a8 8 0 1 0 0 16 8 8 0 0 0 0-16Zm3.7 6.171-4.449 4.45a.559.559 0 0 1-.8 0l-2.16-2.16a.563.563 0 0 1 .792-.8l1.76 1.76 4.066-4.042a.563.563 0 1 1 .792.8v-.008Z'/%3E%3C/g%3E%3Cdefs%3E%3CclipPath id='a'%3E%3Cpath fill='%23fff' d='M.5 0h16v16H.5z'/%3E%3C/clipPath%3E%3C/defs%3E%3C/svg%3E");
width: 1rem;
height: 0.95rem;
position: relative;
left: -0.25rem;
}
}
}
</style>

View file

@ -0,0 +1,493 @@
<template>
<baseInputButton v-bind="$props" @buttonClicked="handleAnswerChange" v-model="selectedValue">
<div
class="package-label"
:class="[
this.buttonLabelSubCopy ? 'has-subheader' : '',
this.isInsuranceSelected ? 'pricing-by-day-pkg-lbl-ins' : 'pricing-by-day-pkg-lbl',
!this.additionalButtonData.servicePackageDiscount ? 'adjust-top' : '',
]"
for="testradio">
<div class="package-specs">
<div v-if="this.additionalButtonData.servicePackageDiscount" class="row">
<div class="col md-6">
<p class="m-0">
<span v-html="this.buttonLabel"></span>
</p>
<p
class="sub-label m-0"
v-if="this.buttonLabelSubCopy"
v-html="this.buttonLabelSubCopy"></p>
</div>
<span class="col md-6 service-package-discount-pricing-by-day">
<div>
<span v-if="!isInsuranceSelected" class="caption pricing-by-day">
as low as
</span>
<div
class="strikethrough-discount-price"
v-if="shouldDisplayStrikeThroughPrice"
v-html="this.additionalButtonData.strikeThroughPrice"></div>
<div class="discount-price" v-html="getDisplayPrice"></div>
</div>
</span>
</div>
<div v-else>
<p class="m-0">
<span v-html="this.buttonLabel"></span>
<span class="pricing-info-pricing-by-day">
<span v-if="!isInsuranceSelected" class="caption pricing-by-day">
as low as
</span>
<span
class="strikethrough-price"
v-if="shouldDisplayStrikeThroughPrice"
v-html="this.additionalButtonData.strikeThroughPrice"></span>
<span v-html="getDisplayPrice"></span>
</span>
</p>
<p
class="sub-label m-0"
v-if="this.buttonLabelSubCopy"
v-html="this.buttonLabelSubCopy"></p>
</div>
<div class="hide-when-closed">
<!-- Parse the body text containing <ul> with logic -->
<ul>
<li v-for="listItem in this.arrayOfListItemsFromBodyText" :key="listItem">
<!-- no textLink -->
<span
v-if="!doesCopyContainTextLink(listItem)"
v-html="listItem"></span>
<!-- with textLink -->
<template
v-else
v-for="copy in splitCopyOnCMSPlaceHolder(listItem)"
:key="copy">
<span v-if="!doesCopyContainTextLink(copy)" v-html="copy"></span>
<span v-else>
<textLink
linkType="text"
:text="getRouterLinkDisplayTextFromCopy(copy)"
href="#!"
@click-event="
$emit('buttonEvent', {
eventName: 'openModal',
args: getRouterLinkRouteFromCopy(copy),
})
"
:data-bs-target="'#' + getRouterLinkRouteFromCopy(copy)" />
</span>
</template>
</li>
</ul>
<!-- End of body text parsing -->
<div
class="package-footer fw-bold caption ms-n4 mt-4"
v-if="this.buttonFooterCopy"
v-html="this.buttonFooterCopy"></div>
</div>
<div
v-if="
this.additionalButtonData.servicePackageDiscount &&
this.additionalButtonData.Text
"
class="special-save-box">
<span v-html="this.additionalButtonData.Text"></span>
</div>
</div>
</div>
</baseInputButton>
</template>
<script>
import baseInputButton from "@/digital-components/base-input-button/base-input-button";
import textLink from "@/ux-components/text-link/text-link";
import inputButtonWrapperMixin from "@/mixins/input-button-wrapper-mixin";
import {
getRouterLinkDisplayTextFromCopy,
getRouterLinkRouteFromCopy,
splitCopyOnCMSPlaceHolder,
doesCopyContainTextLink,
} from "@/helpers/cms-content-helper";
import experimentMixin from "@/mixins/experiment-mixin";
import { experimentSettings } from "@/constants/experiments";
import store from "@/store";
export default {
name: "servicePackageRadio",
mixins: [inputButtonWrapperMixin],
props: {
isInsuranceSelected: Boolean,
},
components: {
baseInputButton,
textLink,
},
computed: {
arrayOfListItemsFromBodyText() {
return this.getArrayOfListItemsFromRawCmsCopy(this.buttonBodyCopy);
},
shouldDisplayStrikeThroughPrice() {
const displayPrice = this.buttonAuxillaryCopy.substring(
this.buttonAuxillaryCopy.indexOf("$")
);
return displayPrice != this.additionalButtonData.strikeThroughPrice;
},
getDisplayPrice() {
return this.buttonAuxillaryCopy.split(".")[0];
},
},
methods: {
getRouterLinkDisplayTextFromCopy,
getRouterLinkRouteFromCopy,
splitCopyOnCMSPlaceHolder,
doesCopyContainTextLink,
stripUlTagFromCopy(copy) {
const regex = /(?:<ul(?:.*?)>)|(?:<\/ul>)/g;
return copy.replace(regex, "");
},
getArrayOfListItemsFromRawCmsCopy(copy) {
const withoutUlTags = this.stripUlTagFromCopy(copy);
return withoutUlTags
.split(/(?:<li(?:.*?)>)|(?:<\/li>)/g)
.filter((lineItem) => lineItem);
},
},
};
</script>
<style lang="scss">
.package-main {
.package-wrapper {
margin: 1rem 0;
&:first-child {
margin: 1rem 0 0 0;
}
&:last-child {
margin: 1rem 0 0 0;
}
@include media-breakpoint-up(md) {
margin: 1rem 0.5rem 0 0.5rem;
display: flex;
flex-direction: column;
&:first-child {
margin: 1rem 0.5rem 0 0;
}
&:last-child {
margin: 1rem 0 0 0.5rem;
}
}
label {
display: block;
@include media-breakpoint-up(md) {
display: flex;
height: 100%;
}
}
input[type="radio"] {
opacity: 0;
position: absolute;
left: -9999px;
+ .package-label.pricing-by-day-pkg-lbl.adjust-top {
/* Copying a component for an experiment will cause duplication of css classes.
This can cause problems if you want change the value of something only for the experiment
version of the component. So added a more specific selector for the parts I want to change
If this experiment wins, move this to package-label ::before/::after below
*/
&:before {
top: 0.9375rem;
}
&:after {
top: 1.875rem;
}
@include media-breakpoint-up(lg) {
&:before {
top: .5rem;
}
&:after {
top: 1.4375rem;
}
p {
span {
align-self: center;
}
}
}
}
+ .package-label.pricing-by-day-pkg-lbl-ins {
&:before {
top: .3125rem;
}
&:after {
top: 1.25rem;
}
}
+ .package-label.pricing-by-day-pkg-lbl {
&:before {
top: 0.9375rem;
@include media-breakpoint-up(lg) {
top: .3125rem;
}
}
&:after {
top: 1.25rem;
}
}
+ .package-label {
display: flex;
align-items: flex-start;
position: relative;
cursor: pointer;
width: 100%;
padding: 0.75rem;
border: 1px solid $gray-300;
box-shadow:
0px 4px 8px -4px rgba(0, 0, 0, 0.15),
0px 4px 24px -8px rgba(0, 0, 0, 0.2);
border-radius: 0.5rem;
overflow: hidden;
min-height: 52px;
max-height: 126px;
@include media-breakpoint-up(md) {
max-height: 500px;
}
transition: max-height 0.25s ease-in;
&.has-subheader {
min-height: 72px;
}
&:before {
content: "";
position: relative;
margin-right: 0.5rem;
border-radius: 50%;
border: 1px solid $gray-500;
width: 16px;
height: 16px;
min-width: 16px;
}
&:after {
content: "";
position: absolute;
left: 15px;
border-radius: 50%;
width: 10px;
height: 10px;
min-width: 10px;
}
}
&:hover {
+ .package-label {
&:before {
border: 1px solid #8e9292;
box-shadow:
0px 0px 0px 4px #9fcee6,
0px 1px 4px rgba(0, 0, 0, 0.2);
}
}
}
&:checked {
+ .package-label {
.package-specs {
max-height: 1000px;
}
}
+ .package-label {
.package-specs {
.hide-when-closed {
display: block;
}
}
}
+ .package-label {
background-color: $blue-100;
border: 1px solid $blue;
max-height: 500px;
.special-save-box {
background-color: $green-200;
}
}
+ .package-label {
&:before {
box-shadow: 0px 0px 0px 1px $blue;
}
}
+ .package-label {
&:after {
background: $blue;
}
}
}
&:focus {
+ .package-label {
&:before {
border: 2px solid $blue;
}
}
}
}
.package-footer {
color: $red;
font-weight: 600;
}
.package-specs {
display: flex;
flex-direction: column;
width: 100%;
transition: all 0.5s ease;
@include media-breakpoint-up(md) {
max-height: 500px;
}
span.pricing-by-day {
color: #4d5151;
display: block;
white-space: nowrap;
}
.row {
align-items: center;
}
p {
font-weight: 600;
display: flex;
justify-content: space-between;
align-items: center;
white-space: nowrap;
span {
&.pricing-info-pricing-by-day {
text-align: end;
color: $green;
font-size: 1rem;
line-height: 1;
span.strikethrough-price {
text-decoration: line-through;
font-size: 0.75rem;
font-weight: 400;
color: $gray-550;
padding-right: 8px;
}
}
}
&.sub-label {
color: $green;
text-transform: uppercase;
font-size: 0.75rem;
}
}
span {
&.service-package-discount-pricing-by-day {
color: $green;
text-align: end;
font-weight: 600;
font-size: 0.875rem;
display: flex;
justify-content: flex-end;
align-items: center;
align-self: flex-start;
line-height: 1;
div.strikethrough-discount-price {
display: inline;
text-decoration: line-through;
margin-right: 0.5rem;
font-weight: 400;
font-size: 0.75rem;
line-height: 20px;
color: $gray-550;
@include media-breakpoint-up(md) {
margin-right: 0.5rem;
font-size: 0.75rem;
}
}
div.discount-price {
color: $green;
display: inline;
font-size: 1rem;
}
}
.caption {
font-size: 0.625rem;
}
}
ul {
margin: 1rem 0 0 -5px;
padding: 0;
li {
font-size: 0.875rem;
line-height: 1.714;
color: $gray-550;
a {
line-height: 1.714;
padding: 0;
}
}
}
@keyframes slideaway {
from {
display: block;
}
to {
transform: translateY(40px);
opacity: 0;
}
}
.hide-when-closed {
display: none;
@include media-breakpoint-up(md) {
display: flex;
flex-direction: column;
}
}
}
.discount-text {
height: 2rem;
display: flex;
border-top-right-radius: 0.5rem;
border-top-left-radius: 0.5rem;
background-color: $green;
align-items: center;
justify-content: center;
width: 100%;
color: white;
text-transform: uppercase;
font-size: 0.875rem;
font-weight: 600;
letter-spacing: 0.75px;
}
.special-save-box {
background-color: $green-100;
display: flex;
flex-direction: row;
margin: auto;
margin-left: -1.25rem;
padding: 0.5rem 1rem;
border-radius: 0.25rem;
margin-top: 0.5rem;
span {
color: $green;
font-size: 0.875rem;
font-weight: 600;
}
}
}
}
</style>

View file

@ -10,9 +10,9 @@
href="javascript:void(0)"
class="col d-flex justify-content-between py-0">
<span class="label">{{ OrderDetailsText }}</span>
<span class="label amount-due">{{
getLineItemAmount(amountDue, showCoverageAsPending)
}}</span>
<span class="label amount-due">
{{ getLineItemAmount(amountDue, showCoverageAsPending) }}
</span>
</a>
</div>
<div class="cart-panel">
@ -48,11 +48,11 @@
v-for="(cartItem, i) in packageCartItems"
:key="i"
class="packaged-cart-item">
<span
><span :class="cartItem.category === 'promos' ? 'promo' : ''">{{
cartItem.name
}}</span></span
>
<span>
<span :class="cartItem.category === 'promos' ? 'promo' : ''">
{{ cartItem.name }}
</span>
</span>
<textLink
v-if="allowItemRemoval"
ref="removeLink"
@ -99,11 +99,9 @@
!showCoverageAsVerified ||
cartItem.name === 'Recycling' ||
(showCoverageAsVerified && !cartItem.isCoveredByInsurance)
"
>{{
getLineItemAmount(cartItem.subTotal, null, cartItem.category)
}}</span
>
">
{{ getLineItemAmount(cartItem.subTotal, null, cartItem.category) }}
</span>
</div>
<!-- Sub total, sales tax, total columns -->
@ -143,8 +141,10 @@
class="my-2 lh-1 applied-promo-tag"
v-for="(promoCode, i) in getPromoCodeList"
:key="i">
<span class="caption"
>Promo code <span class="promo-code">{{ promoCode }}</span> applied
<span class="caption">
Promo code
<span class="promo-code">{{ promoCode }}</span>
applied
</span>
</div>
<contentGroupModal ref="RecycleModal" cmsWidgetName="RecycleModal">

View file

@ -54,8 +54,10 @@
class="mt-4 d-flex align-items-center justify-content-between"
v-for="(promoCode, i) in this.getPromoCodeList()"
:key="i">
<span class="applied-promo"
>Promo code <span class="promo-code">{{ promoCode }}</span> applied
<span class="applied-promo">
Promo code
<span class="promo-code">{{ promoCode }}</span>
applied
</span>
<textLink
ref="removeLink"

View file

@ -149,6 +149,21 @@ export default {
transition: all 150ms linear;
background: none;
&.btn {
&.btn-secondary {
font-weight: 900;
border: none;
&:hover,
&:active,
&:focus,
&:focus-visible {
color: $blue-700;
background: none;
box-shadow: none;
}
}
}
&:hover,
&:active,
&:focus,
@ -166,6 +181,7 @@ export default {
transition: background 0s 0s ease-in-out;
}
}
.modal.modal-component .modal-dialog .modal-content {
.modal-header {
padding-top: 0;

View file

@ -58,3 +58,15 @@ export function getTopLevelPartsWithRecal(lineItems) {
return lineItems.filter((li) => isRecalPartOrHasChildRecalPart(li));
}
export function getRecalPartNumbers(glassPartsArray) {
if (glassPartsArray && glassPartsArray.length > 0) {
const topLevel = glassPartsArray.filter((gp) => isRecalPart(gp)).map((gp) => gp.partNumber);
const children = glassPartsArray.map((gp) => getRecalPartNumbers(gp.childParts)).flat();
return [...topLevel, ...children];
} else {
return [];
}
}

View file

@ -357,15 +357,21 @@ export default {
false
);
if (vinPagesMixin.methods.isPhoneNumber(this.customerQuestions.emailOrSms)) {
const phone = this.customerQuestions.emailOrSms.replace(/[()]/g, "");
await this.dispatchStoreAction(storeActions.SAVE_PHONE_NUMBER, phone, false);
// if no value due to field being optional, blank both phone and email address
if (!this.customerQuestions.emailOrSms) {
await this.dispatchStoreAction(storeActions.SAVE_PHONE_NUMBER, "", false);
await this.dispatchStoreAction(storeActions.SAVE_EMAIL, "", false);
} else {
await this.dispatchStoreAction(
storeActions.SAVE_EMAIL,
this.customerQuestions.emailOrSms,
false
);
if (vinPagesMixin.methods.isPhoneNumber(this.customerQuestions.emailOrSms)) {
const phone = this.customerQuestions.emailOrSms.replace(/[()]/g, "");
await this.dispatchStoreAction(storeActions.SAVE_PHONE_NUMBER, phone, false);
} else {
await this.dispatchStoreAction(
storeActions.SAVE_EMAIL,
this.customerQuestions.emailOrSms,
false
);
}
}
await this.dispatchStoreAction(

View file

@ -4,8 +4,8 @@
<button v-on:click="openCalendarModal">
<span class="add-to-calendar-span">
<img src="@/assets/img/icons/add-to-calendar.svg" class="add-to-calendar-img" />
<span class="add-to-calendar-text">Add to calendar</span></span
>
<span class="add-to-calendar-text">Add to calendar</span>
</span>
</button>
</div>
<calendarModalQuestion
@ -14,8 +14,7 @@
cmsWidgetName="CalendarModalQuestion"
@calendar-modal-closed="calendarModalClosed"
:calendarOptionsData="calendarValues"
v-model="selectedCalendarOption">
</calendarModalQuestion>
v-model="selectedCalendarOption"></calendarModalQuestion>
</div>
</template>
<script>

View file

@ -64,6 +64,19 @@
:isItac="isItac"
:isNoComp="isNoComp" />
<div class="mt-3" v-if="displayFosterLove">
<donationBlock
cmsWidgetName="DonationWidget"
v-model="donationAmount"
:donationValues="donationValues"
:showDonationSuccess="showDonationSuccess"
:showDonationError="showDonationError" />
</div>
<!-- <button type="button" @click="addDonation(3)">
Add Donation of $3
</button> -->
<hr class="mb-5" />
<div class="emailtext" v-html="ConfirmationEmailText"></div>
@ -108,6 +121,7 @@ import experimentMixin from "@/mixins/experiment-mixin.js";
import { partTypeStrings } from "@/constants/part-type-strings";
import { containsLineItemWithPartType } from "@/helpers/service-package-helper";
import { containsRecalParts } from "@/helpers/recal-helper.js";
import donationBlock from "@/experiment-components/donation-block.vue";
export default {
name: "confirmation",
@ -204,6 +218,11 @@ export default {
return hasRecalPriceRemoveExperiment && isRecalibrationOnOrder;
};
const donationItems = lineItemsFromSubmittedOrder.supportingItems?.filter(
(item) => item.partNumber === "DONATION"
);
const donationAmount = donationItems?.length > 0 ? donationItems[0].sellingPrice : 0;
// Call the "next" function to complete the transition to this page.
next((vm) => {
vm.setCmsContent(resultMap.cmsContent);
@ -211,6 +230,7 @@ export default {
vm.vaps = availableVaps;
vm.submittedOrder = submittedOrder;
vm.shouldHideRecalibration = shouldHideRecalibration();
vm.donationAmount = donationAmount;
});
},
data() {
@ -219,9 +239,18 @@ export default {
vaps: [],
submittedOrder: null,
shouldHideRecalibration: null,
donationAmount: 0,
showDonationSuccess: null,
showDonationError: null,
};
},
computed: {
DisplayFosterLove() {
return experimentMixin.methods.hasSettingEqualTo(
experimentSettings.FOSTER_LOVE,
"true"
);
},
isRecalibrationOnOrder() {
return containsRecalParts(this?.lineItems);
},
@ -447,6 +476,9 @@ export default {
);
}
},
donationValues() {
return [1, 3, 5];
},
},
methods: {
arePagePrerequisitesValid() {
@ -489,6 +521,56 @@ export default {
forwardButtonAction() {
window.location.assign(location.protocol + "//" + location.host);
},
async addDonation(amount, plateTemp) {
// try to call API to add donation to order
// TEMP DUMMY CALL TO SIMULATE SUCCESS OR ERROR FROM API CALL
// TO BE REPLACED BY REAL CALL LATER
const donationResponse = await this.dispatchStoreActionWithLogging(
storeActions.LOOKUP_VIN_BY_PLATE,
{ licensePlate: plateTemp, licenseState: "OH" },
"test",
false
).catch((error) => {
// this.$refs.navbar.removeLoader();
console.error("Error!!! error: ", error);
return error;
});
console.log("ran addDonation()... (*fake call*) donationResponse: ", donationResponse);
if (donationResponse?.status == "200") {
this.showDonationSuccess = true;
this.showDonationError = false;
const donationResponse = await baseMixin.methods.dispatchStoreAction(
storeActions.ADD_DONATION_TO_SUBMITTED_ORDER,
amount,
false
);
} else {
this.showDonationError = true;
this.showDonationSuccess = false;
const donationResponse = await baseMixin.methods.dispatchStoreAction(
storeActions.ADD_DONATION_TO_SUBMITTED_ORDER,
0,
false
);
}
},
},
watch: {
donationAmount(newValue) {
newValue = parseInt(newValue);
console.log("WATCHED donationAmount changed: ", newValue);
// this.addDonation(newValue);
// TODO - when backend code is final, then remove the below and restore the line above
if (newValue === 5) {
this.addDonation(newValue, "FAILING_PLATE"); // throws error
} else {
this.addDonation(newValue, "2fast"); // should give successful response
}
},
},
components: {
funnelHeader,
@ -497,6 +579,7 @@ export default {
addToCalendar,
cart,
textBlock,
donationBlock,
},
};
</script>

View file

@ -1,3 +0,0 @@
describe("insurance-company.vue", () => {
it.todo("Some test for the future");
});

View file

@ -0,0 +1,102 @@
import { shallowMount } from "@vue/test-utils";
import InsuranceCompany from "@/layouts/insurance-company/insurance-company.vue";
import { applicationConfig } from "@/constants/application-config.js";
import store from "@/store";
import baseMixin from "@/mixins/base-mixin.js";
jest.mock("@/store", () => ({
getters: {
order: {
payment: {
parentAccountNumber: "12345",
},
},
},
}));
var mockRouter = {
navigateWithoutSaving: jest.fn(),
};
jest.mock("@/mixins/base-mixin.js", () => ({
methods: {
navigateWithoutSaving: jest.fn(),
},
data() {
return {
navigationScenarios: {
CLICKED_BACK: "clicked_back",
},
};
},
}));
describe("insurance-company.vue", () => {
beforeEach(() => {
// Reset the store's parentAccountNumber before each test
store.getters.order.payment.parentAccountNumber = "12345";
});
test("should return true when parentAccountNumber matches CASH_PARENT_ACCOUNT_NUMBER", () => {
applicationConfig.CASH_PARENT_ACCOUNT_NUMBER = "12345";
const wrapper = shallowMount(InsuranceCompany, {
global: {
mocks: {
$store: store,
},
},
});
expect(wrapper.vm.isCashParentAccountNumber()).toBe(true);
});
test("should return false when parentAccountNumber does not match CASH_PARENT_ACCOUNT_NUMBER", () => {
applicationConfig.CASH_PARENT_ACCOUNT_NUMBER = "12345";
store.getters.order.payment.parentAccountNumber = "67890";
const wrapper = shallowMount(InsuranceCompany, {
global: {
mocks: {
$store: store,
},
},
});
expect(wrapper.vm.isCashParentAccountNumber()).toBe(false);
});
test("should call navigateWithoutSaving with { isCashSelected: true } when forceCashSelection is true", () => {
const wrapper = shallowMount(InsuranceCompany, {
global: {
mocks: {
$store: store,
$router: mockRouter,
},
mixins: [baseMixin],
},
});
wrapper.vm.backButtonAction(true);
expect(mockRouter.navigateWithoutSaving).toHaveBeenCalledWith(
wrapper.vm.navigationScenarios.CLICKED_BACK,
wrapper.vm.$route,
{ isCashSelected: true }
);
});
test("should call navigateWithoutSaving with no extra parameter when forceCashSelection is false", () => {
const wrapper = shallowMount(InsuranceCompany, {
global: {
mocks: {
$store: store,
$router: mockRouter,
},
mixins: [baseMixin],
},
});
wrapper.vm.backButtonAction(false);
expect(mockRouter.navigateWithoutSaving).toHaveBeenCalledWith(
wrapper.vm.navigationScenarios.CLICKED_BACK,
wrapper.vm.$route
);
});
});

View file

@ -16,6 +16,13 @@
v-model="parentAccountNumber"
:originalList="originalList" />
</div>
<textLink
id="payOnMyOwnBackButton"
useLoadingModal
linkType="text"
:text="payWithCashButtonCopy"
href="javascript:void(0)"
@click-event="backButtonAction((foreceCashSelectionOnQuote = true))" />
<navbar
cmsWidgetName="FunnelFooterWidget"
@ -36,6 +43,7 @@ import navbar from "@/fmg-components/nav-bar/nav-bar";
import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header";
import loadingModal from "@/fmg-components/loading-modal/loading-modal.vue";
import insuranceCompanyQuestion from "@/layouts/insurance-company/insurance-company-question";
import textLink from "../../ux-components/text-link/text-link.vue";
// Supporting files
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
@ -92,6 +100,9 @@ export default {
isParentAccountNumber() {
return !!this.parentAccountNumber;
},
payWithCashButtonCopy() {
return this.getCmsContent("PayWithCashWidget", "Text");
},
},
methods: {
isCashParentAccountNumber() {
@ -109,8 +120,17 @@ export default {
arePagePrerequisitesValid() {
return store.getters.order.payment.isInsurance === true;
},
backButtonAction() {
backButtonAction(forceCashSelectionOnQuote) {
// Go back to Quote page
if (forceCashSelectionOnQuote) {
this.$router.navigateWithoutSaving(
this.navigationScenarios.CLICKED_BACK,
this.$route,
{ isCashSelected: true }
);
return;
}
this.$router.navigateWithoutSaving(this.navigationScenarios.CLICKED_BACK, this.$route);
},
async forwardButtonAction() {
@ -157,6 +177,7 @@ export default {
Form,
loadingModal,
insuranceCompanyQuestion,
textLink,
},
};
</script>

View file

@ -311,11 +311,17 @@ export default {
false
);
if (vinPagesMixin.methods.isPhoneNumber(this.emailOrSms)) {
const phone = this.emailOrSms.replace(/[()]/g, "");
await this.dispatchStoreAction(storeActions.SAVE_PHONE_NUMBER, phone, false);
// if no value due to field being optional, blank both phone and email address
if (!this.emailOrSms) {
await this.dispatchStoreAction(storeActions.SAVE_PHONE_NUMBER, "", false);
await this.dispatchStoreAction(storeActions.SAVE_EMAIL, "", false);
} else {
await this.dispatchStoreAction(storeActions.SAVE_EMAIL, this.emailOrSms, false);
if (vinPagesMixin.methods.isPhoneNumber(this.emailOrSms)) {
const phone = this.emailOrSms.replace(/[()]/g, "");
await this.dispatchStoreAction(storeActions.SAVE_PHONE_NUMBER, phone, false);
} else {
await this.dispatchStoreAction(storeActions.SAVE_EMAIL, this.emailOrSms, false);
}
}
await this.dispatchStoreAction(

View file

@ -78,6 +78,20 @@ export default {
);
}
if (store.getters.externalParameterCustomer?.phoneNumber) {
await baseMixin.methods.dispatchStoreAction(
storeActions.SAVE_PHONE_NUMBER,
store.getters.externalParameterCustomer.phoneNumber,
false
);
await baseMixin.methods.dispatchStoreAction(
storeActions.SAVE_IS_SMS_OPT_IN,
true,
false
);
}
baseMixin.methods.ResetExternalParamsAndHideModal();
}
});

View file

@ -79,6 +79,21 @@ export default {
false
);
}
if (store.getters.externalParameterCustomer?.phoneNumber) {
await baseMixin.methods.dispatchStoreAction(
storeActions.SAVE_PHONE_NUMBER,
store.getters.externalParameterCustomer.phoneNumber,
false
);
await baseMixin.methods.dispatchStoreAction(
storeActions.SAVE_IS_SMS_OPT_IN,
true,
false
);
}
baseMixin.methods.ResetExternalParamsAndHideModal();
}
});

View file

@ -18,7 +18,7 @@
v-if="hasImage"
:src="buttonImage"
:alt="getInlineAltText(token)" />
<span v-else> {{ getInlineAltText(token) }}</span>
<span v-else>{{ getInlineAltText(token) }}</span>
</span>
<span v-else>
{{ token }}

View file

@ -11,7 +11,7 @@
class="ms-2"
:src="buttonImage"
:alt="getInlineAltText(token)" />
<span v-else> {{ getInlineAltText(token) }}</span>
<span v-else>{{ getInlineAltText(token) }}</span>
</span>
<span v-else>
{{ token }}

View file

@ -41,8 +41,7 @@
id="card-frame"
seamless
scrolling="no"
allow="payment *; microphone *; geolocation *;">
</iframe>
allow="payment *; microphone *; geolocation *;"></iframe>
</div>
<hr />

View file

@ -59,6 +59,9 @@ jest.mock("@/mixins/base-mixin", () => ({
filterOutFees(items) {
return null;
},
filterOutServicePackageDiscountPart(items) {
return null;
},
isFormValid(form) {
return true;
},
@ -577,6 +580,52 @@ describe("quote.vue", () => {
//Assert
expect(wrapper.vm.isInsuranceSelected).toBe(false);
});
test("should default to cash if isCashSelected is true, despite all other checks pointing to insurance selection", async () => {
// Also needs no query parameter or previous selection in store to be present
//Arrange
store.getters = {
lineItems: {
glassParts: ["item", "item2"],
},
applicationUser: {
experiments: [],
},
order: {
lineItems: {
glassParts: ["item", "item2"],
},
payment: {
isInsurance: true,
},
customer: {
emailAddress: "test@test.com",
},
serviceLocation: {
state: null,
},
},
vehicle: {
cardId: "123",
},
experimentSettings: {
settingName: "SERVICE_PACKAGE_DISCOUNT",
},
};
mockTierOnePrice = 505;
const { wrapper } = setupMocks({});
//Act
await quote.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "quote", isInsuranceSelected: true, isCashSelected: true } },
undefined,
(c) => c(wrapper.vm)
);
//Assert
expect(wrapper.vm.isInsuranceSelected).toBe(false);
});
test("should default to insurance if total economy package price is over $500", async () => {
// Also needs no query parameter or previous selection in store to be present
//Arrange

View file

@ -328,6 +328,10 @@ export default {
}
const getIsInsuranceSelectedValue = (availableLineItems, insuranceThreshold) => {
// query param checked here is set on insurance-selection "Pay on my own" link click
if (to.query?.isCashSelected || to.query?.iscashselected) {
return false;
}
const serviceLocationState = store.getters.order.serviceLocation.state;
// usually true when returning from heritage but can be false when returning from heritage on a save quote
@ -357,16 +361,28 @@ export default {
)
: baseMixin.methods.filterOutFees(availableLineItems);
const lineItemsNoDiscount =
baseMixin.methods.filterOutServicePackageDiscountPart(
lineItemsForCalculatingPrice
);
if (isInsuranceFromQueryString != null) {
if (isInsuranceFromQueryString.toLowerCase() === "true") {
return true;
}
}
return (
baseMixin.methods.getTierOnePackagePrice(lineItemsForCalculatingPrice) >
insuranceThreshold
); // compare base price vs arbitrary threshold (representing insurance price)
const tierOnePrice =
baseMixin.methods.getTierOnePackagePrice(lineItemsNoDiscount);
// prettier-ignore
{
if (store.getters.applicationUser.loggingOption) {
console.log(new Date() + " quote tierOnePrice comparison: " + tierOnePrice + " > " + insuranceThreshold + "=" + (tierOnePrice > insuranceThreshold));
}
}
// compare base price vs arbitrary threshold (representing insurance price)
return tierOnePrice > insuranceThreshold;
}
return null;
@ -383,8 +399,7 @@ export default {
// prettier-ignore
{
const log = getQuerystringParameter(queryStrings.LOG);
if (log === "true") {
if (store.getters.applicationUser.loggingOption) {
console.log("------------- quote.vue thresholds start -----------------");
console.log(new Date() + " isExternalParameter: " + isExternalParameter);
console.log(new Date() + " internalThreshold: " + internalThreshold);
@ -394,6 +409,12 @@ export default {
}
if (isExternalParameter === externalParameterStatus.NOT_SET) {
// prettier-ignore
{
if (store.getters.applicationUser.loggingOption) {
console.log(new Date() + "quote.vue tab select EXTERNAL not set");
}
}
// there are no active or inactive external parameters; use internal threshold
if (internalThreshold) thresholdToUse = internalThreshold;
@ -405,6 +426,13 @@ export default {
} else {
// user came from an external source, however, the externalParms may have been reset on service-zip, property-questions, etc...
if (getBoolFromString(store.getters.externalParameterQuote?.isInsurance)) {
// prettier-ignore
{
if (store.getters.applicationUser.loggingOption) {
console.log(new Date() + "quote.vue tab select EXTERNAL ISINSURANCE");
}
}
// did user intentionally select insurance?
vm.isInsuranceSelected = true;
vm.servicePackage = store.getters.externalParameterQuote.servicePackage;
@ -424,6 +452,13 @@ export default {
} else {
if (externalThreshold) thresholdToUse = externalThreshold;
// prettier-ignore
{
if (store.getters.applicationUser.loggingOption) {
console.log(new Date() + "quote.vue tab select NOT EXTERNAL");
}
}
vm.isInsuranceSelected = getIsInsuranceSelectedValue(
vm.availableLineItems,
thresholdToUse

View file

@ -3,7 +3,7 @@
<div class="alert-heading">
<span>
<img v-if="hasImage" :src="imageUrl" />
<span class="px-2"> {{ recalHeaderText }}</span>
<span class="px-2">{{ recalHeaderText }}</span>
</span>
</div>
<div>

View file

@ -18,6 +18,11 @@ import { nextTick } from "vue";
jest.mock("@/store", () => ({
commit: jest.fn(),
dispatch: jest.fn(),
getters: {
applicationUser: {
loggingOption: false,
},
},
}));
const mockExperimentSettings = experimentSettings;
@ -29,6 +34,9 @@ jest.mock("@/mixins/experiment-mixin.js", () => ({
return true;
}
},
hasSettingEqualTo(settingName, settingValue) {
return true;
},
},
}));
@ -843,6 +851,7 @@ function setupMocks({ mountOptionsMockData, props = mockProps }) {
},
},
};
mountOptionsMockData = Object.assign(mountOptionsMockDataDefault, mountOptionsMockData);
const mountOptions = getMountOptions(mountOptionsMockData);

View file

@ -4,11 +4,12 @@
:answers="servicePackageAnswers"
:groupName="groupName"
buttonTypeString="servicePackageRadio"
:buttonTypeObject="servicePackageRadio"
:buttonTypeObject="hasPricingByDay() ? servicePackageRadioExperiment : servicePackageRadio"
v-model="selectedPackageName"
:validationRules="validationRules"
:isRequired="isRequired"
:logDisplayedValuesEvent="true" />
:logDisplayedValuesEvent="true"
:isInsuranceSelected="isInsuranceSelected" />
</template>
<script>
@ -16,6 +17,7 @@ import buttonQuestion from "@/digital-components/button-question/button-question
import baseMixin from "@/mixins/base-mixin.js";
import { processIfStatements } from "@/helpers/cms-content-helper";
import servicePackageRadio from "./service-package-radio/service-package-radio";
import servicePackageRadioExperiment from "@/experiment-components/service-package-radio";
import { partTypeStrings } from "@/constants/part-type-strings";
import { packageNames } from "@/constants/package-names";
import {
@ -58,6 +60,7 @@ export default {
data() {
return {
servicePackageRadio: servicePackageRadio,
servicePackageRadioExperiment: servicePackageRadioExperiment,
selectedPackageName: null,
};
},
@ -223,6 +226,14 @@ export default {
},
methods: {
processIfStatements,
hasPricingByDay() {
return (
experimentMixin.methods.hasSettingEqualTo(
experimentSettings.PRICING_BY_DAY,
"true"
) && !this.isInsuranceSelected
);
},
getHeaderTextFromCms(cmsWidgetName) {
return this.getCmsContent(cmsWidgetName, "HeaderText");
},

View file

@ -27,9 +27,10 @@
</div>
<div class="row justify-content-center">
<div class="col-md-6 col-xl-4 start-over">
<a @click="startOver" alt="Start Over Button"
><img v-bind:src="startOverImage" />{{ startOverText }}</a
>
<a @click="startOver" alt="Start Over Button">
<img v-bind:src="startOverImage" />
{{ startOverText }}
</a>
</div>
</div>
</div>

View file

@ -7,6 +7,7 @@ import { getMountOptions } from "@/helpers/unit-test-helper.js";
import store from "@/store";
import router from "@/router";
import baseMixin from "../../mixins/base-mixin";
import { experimentSettings } from "../../constants/experiments";
// Mock basemixin
jest.mock("@/mixins/base-mixin.js", () => ({
@ -177,6 +178,7 @@ beforeEach(() => {
glassParts: [],
supportingItems: [],
},
experimentSettings: {},
};
});
afterEach(() => {

View file

@ -18,7 +18,18 @@
marginTopSizeOverride="1" />
</template>
<locationAlerts cmsWidgetPrefix="LocationAlert-" ref="locationAlerts" />
<datePickerForPricingByDay
v-if="isPricingByDayExperiment"
customComponentId="dateQuestion"
selectableDatesSetting="custom"
ref="datePicker"
v-model="selectedDate"
class="text-link-small"
:customSelectableDatesCallback="getAvailableDatesMethod"
validationRules="date-required"
@date-clicked="openInshopTimeSlotsModal" />
<datePicker
v-else
customComponentId="dateQuestion"
selectableDatesSetting="custom"
ref="datePicker"
@ -74,6 +85,7 @@ import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-heade
import loadingModal from "@/fmg-components/loading-modal/loading-modal.vue";
import { Form, defineRule } from "vee-validate";
import datePicker from "@/digital-components/date-picker/date-picker";
import datePickerForPricingByDay from "@/experiment-components/date-picker-for-pricing-by-day";
import locationAlerts from "@/layouts/schedule/location-alerts/location-alerts";
import timeSlotModalQuestion from "./time-slot-modal-question/time-slot-modal-question";
import textBlock from "@/digital-components/text-block/text-block";
@ -97,6 +109,8 @@ import { AppointmentTypeStrings, PREMIUM_FEE_PART_TYPE } from "@/constants/sched
import { errorMessages } from "@/constants/error-messages";
import { required } from "@/helpers/validation-rules";
import store from "@/store";
import experimentMixin from "@/mixins/experiment-mixin.js";
import { experimentSettings } from "@/constants/experiments";
// DEFINE VALIDATION RULES
defineRule("date-required", required(errorMessages.DATE_REQUIRED));
@ -228,6 +242,8 @@ export default {
customSelectableDatesCallback: getAvailableDates,
preSelectedDate: preSelectedDate,
});
// TODO - ONCE A DATEPICKER VERSION IS FINALIZED,
// MAKE SURE THE ABOVE METHOD loadInitialData POINTS TO THE RIGHT FILE
const premiumFeePromise = baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.GET_MOBILE_PREMIUM_FEE,
@ -310,6 +326,13 @@ export default {
(selectableDate) => selectableDate.date === this.selectedDate
);
},
isPricingByDayExperiment() {
return (
experimentMixin.methods
.getSettingValue(experimentSettings.PRICING_BY_DAY)
?.toLowerCase() === "true"
);
},
},
methods: {
splitCopyOnCMSPlaceHolder,
@ -602,6 +625,7 @@ export default {
Form,
loadingModal,
datePicker,
datePickerForPricingByDay,
locationAlerts,
timeSlotModalQuestion,
textBlock,

View file

@ -6,7 +6,7 @@ export async function getPricedMobileFeePart(serviceZipCode, pageNameToLog) {
return null;
}
const zipCodeData = await baseMixin.methods.getZipCodeData(serviceZipCode, pageNameToLog);
const zipCodeData = await getZipCodeData(serviceZipCode);
// Get the Mobile Fee Part
const mobileFeePart = await baseMixin.methods.dispatchStoreActionWithLogging(
@ -103,3 +103,7 @@ export async function getAvailabilityRating(
return shopStatus;
}
export async function getZipCodeData(serviceZipCode) {
return await baseMixin.methods.getZipCodeData(serviceZipCode, "service-location");
}

View file

@ -203,7 +203,7 @@ describe("mobile-location-modal-questions.vue", () => {
expect(wrapper.vm.$refs.MobileLocationModalWidget.openModal).toHaveBeenCalled();
});
it("Should emit updated-mobile-location-questions on setMobileLocation for a valid address and vehicle protection answer", async () => {
it("Should emit setMobileLocationAndProceed on setMobileLocation for a valid address and vehicle protection answer", async () => {
// Arrange
const mobileLocationQuestions = {
addressQuestions: {
@ -240,26 +240,19 @@ describe("mobile-location-modal-questions.vue", () => {
});
wrapper.vm.$refs.MobileLocationModalWidget.closeModal = jest.fn();
// Mock the emitEvent function
wrapper.vm.emitEvent = jest.fn(async (eventName, payload) => {
return new Promise((resolve) => {
wrapper.vm.$emit(eventName, newMobileLocationQuestions, resolve);
resolve(newMobileLocationQuestions);
});
});
// Act
wrapper.vm.internalModel = newMobileLocationQuestions;
await wrapper.vm.setMobileLocation();
// Assert
expect(wrapper.emitted()["updated-mobile-location-questions"]).toBeTruthy();
expect(wrapper.emitted()["updated-mobile-location-questions"][0][0]).toBe(
expect(wrapper.emitted()["setMobileLocationAndProceed"]).toBeTruthy();
expect(wrapper.emitted()["setMobileLocationAndProceed"][0][0]).toStrictEqual(
newMobileLocationQuestions
);
});
it("Should not emit updated-mobile-location-questions on setMobileLocation for an invalid address zip code", async () => {
it("Should not emit setMobileLocationAndProceed on setMobileLocation for an invalid address zip code", async () => {
// Arrange
const mobileLocationQuestions = {
addressQuestions: {
@ -302,7 +295,7 @@ describe("mobile-location-modal-questions.vue", () => {
await wrapper.vm.setMobileLocation();
// Assert
expect(wrapper.emitted("updated-mobile-location-questions")).not.toBeTruthy();
expect(wrapper.emitted("setMobileLocationAndProceed")).not.toBeTruthy();
});
it("Should display invalid zip alert for invalid address zip inputs", async () => {

View file

@ -97,8 +97,7 @@ export default {
"updated-mobile-fee-part",
"updated-contains-military-base",
"updated-bill-to-account-number",
"mobileLocationSelected",
"updated-mobile-location-questions",
"setMobileLocationAndProceed",
],
data() {
return {
@ -287,15 +286,7 @@ export default {
this.$emit("updated-bill-to-account-number", billToAccountNumber);
}
// update the page level model
await this.emitEvent("updated-mobile-location-questions", this.internalModel);
this.$emit("mobileLocationSelected");
},
//creating emitEvent function to raise events with resolve callback to handle events sequencing
async emitEvent(eventName, payload) {
return new Promise((resolve) => {
this.$emit(eventName, payload, resolve);
});
this.$emit("setMobileLocationAndProceed", this.internalModel);
},
},
watch: {

View file

@ -69,6 +69,15 @@ jest.mock(
getShopProviderData: jest.fn((mockServiceZipCode) => {
return mockGetShopProviderData(mockServiceZipCode);
}),
getZipCodeData: jest.fn().mockImplementation(() => {
return Promise.resolve({
containsMilitaryBase: false,
isServiceable: true,
isValid: true,
state: "testState",
zipCodeCtu: "testCTU",
});
}),
})
);
@ -182,9 +191,6 @@ beforeEach(() => {
zipCode: "43235",
state: "OH",
},
vehicle: {
isMobileStaticRecalibrationApplicable: true,
},
},
damage: {
isRepair: false,
@ -427,6 +433,7 @@ describe("service-location.vue", () => {
},
selectedAppointmentType: "Mobile",
providerData: { mobileProviderNumber: "01820" },
isGlassServiceableMobile: true,
});
const mobileLocationQuestionsComponent = wrapper.findComponent({
@ -438,6 +445,7 @@ describe("service-location.vue", () => {
ref: "serviceZipCodeQuestion",
});
serviceZipCodeComponent.resetMobileFeePart = jest.fn();
wrapper.vm.forwardButtonAction = jest.fn();
const mobileLocationQuestions = {
addressQuestions: {
@ -450,7 +458,6 @@ describe("service-location.vue", () => {
isVehicleProtected: null,
};
wrapper.vm.mobileLocationQuestions = mobileLocationQuestions;
const newMobileLocationQuestions = {
addressQuestions: {
streetAddress: "5555 Sulgrave Dr",
@ -462,23 +469,16 @@ describe("service-location.vue", () => {
isVehicleProtected: true,
};
wrapper.vm.closeModalAction = jest.fn();
// Act
// Mock the emitEvent function
mobileLocationQuestionsComponent.vm.emitEvent = jest.fn(async (eventName, payload) => {
return new Promise((resolve) => {
mobileLocationQuestionsComponent.vm.$emit(eventName, payload, resolve);
});
});
// Trigger the event
await mobileLocationQuestionsComponent.vm.emitEvent(
"updated-mobile-location-questions",
await mobileLocationQuestionsComponent.vm.$emit(
"setMobileLocationAndProceed",
newMobileLocationQuestions
);
await wrapper.vm.$nextTick(); // Wait for DOM updates
await new Promise((resolve) => setTimeout(resolve, 0)); // Wait for promise to resolve
// Assert
expect(wrapper.vm.mobileLocationQuestions).toStrictEqual(newMobileLocationQuestions);
@ -494,6 +494,7 @@ describe("service-location.vue", () => {
},
selectedAppointmentType: "Mobile",
providerData: { mobileProviderNumber: "01820" },
isGlassServiceableMobile: true,
});
const mobileLocationQuestionsComponent = wrapper.findComponent({
@ -505,7 +506,7 @@ describe("service-location.vue", () => {
ref: "serviceZipCodeQuestion",
});
serviceZipCodeComponent.resetMobileFeePart = jest.fn();
wrapper.vm.forwardButtonAction = jest.fn();
const mobileLocationQuestions = {
addressQuestions: {
streetAddress: "",
@ -543,23 +544,16 @@ describe("service-location.vue", () => {
zipCodeCtu: "01526",
};
wrapper.vm.closeModalAction = jest.fn();
// Act
// Mock the emitEvent function
mobileLocationQuestionsComponent.vm.emitEvent = jest.fn(async (eventName, payload) => {
return new Promise((resolve) => {
mobileLocationQuestionsComponent.vm.$emit(eventName, payload, resolve);
});
});
// Trigger the event
await mobileLocationQuestionsComponent.vm.emitEvent(
"updated-mobile-location-questions",
await mobileLocationQuestionsComponent.vm.$emit(
"setMobileLocationAndProceed",
newMobileLocationQuestions
);
await wrapper.vm.$nextTick(); // Wait for DOM updates
await new Promise((resolve) => setTimeout(resolve, 0)); // Wait for promise to resolve
// Assert
expect(wrapper.vm.serviceZipCodeQuestion).toStrictEqual(newServiceZipCodeInfo);
@ -581,6 +575,8 @@ describe("service-location.vue", () => {
ref: "mobileLocationQuestions",
});
mobileLocationQuestionsComponent.resetComponent = jest.fn();
wrapper.vm.forwardButtonAction = jest.fn();
wrapper.vm.isServiceableMobile = true;
const serviceZipCodeComponent = wrapper.findComponent({
ref: "serviceZipCodeQuestion",
@ -598,8 +594,12 @@ describe("service-location.vue", () => {
isVehicleProtected: "YesAnswer",
};
wrapper.vm.closeModalAction = jest.fn();
// Act
mobileLocationQuestionsComponent.vm.$emit("update:modelValue", mobileLocationQuestions);
mobileLocationQuestionsComponent.vm.$emit(
"setMobileLocationAndProceed",
mobileLocationQuestions
);
// Assert
expect(wrapper.vm.selectedAppointmentType).toStrictEqual("Mobile");
@ -1432,6 +1432,5 @@ function setupMocks({ mountOptionsMockData = {} }) {
const mountOptions = getMountOptions(mountOptionsMockData);
const wrapper = shallowMount(serviceLocation, mountOptions);
wrapper.vm.setCmsContent = baseMixin.methods.setCmsContent;
return { wrapper, apiPromise };
}

View file

@ -93,12 +93,11 @@
@updated-serviceability="setServiceabilityDetails"
@updated-contains-military-base="setContainsMilitaryBase"
@updated-mobile-ctu="setCtuForMobile"
@updated-mobile-location-questions="setMobileLocationQuestions"
validationRules="mobile-location-required"
ref="mobileLocationQuestions"
linkWidgetName="MobileLocationLinkWidget"
modalWidgetName="MobileLocationModalWidget"
@mobileLocationSelected="forwardButtonAction" />
@setMobileLocationAndProceed="setMobileLocationQuestions" />
<shopQuestion
ref="shopQuestion"
@ -150,6 +149,7 @@ import {
getPricedMobileFeePart,
getServiceabilityDetails,
getShopProviderData,
getZipCodeData,
} from "@/layouts/service-location/helpers/service-location-helper/service-location-helper";
import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
import { applicationConfig } from "@/constants/application-config";
@ -192,8 +192,6 @@ export default {
isRecalibrationServiceableInshop: null,
isGlassServiceableMobile: null,
isRecalibrationServiceableMobile: null,
isVehicleMobileStaticRecalibrationApplicable:
this.getIsVehicleMobileStaticRecalibrationApplicableFromStore(),
selectedAppointmentType: this.getSelectedAppointmentType(),
selectedProvider: this.getSelectedProvider(),
mobileFeePart: null,
@ -209,10 +207,7 @@ export default {
const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage);
const serviceZipCode = store.getters.order.serviceLocation.zipCode;
const zipCodeDataPromise = baseMixin.methods.getZipCodeData(
serviceZipCode,
"service-location"
);
const zipCodeDataPromise = getZipCodeData(serviceZipCode);
const serviceabilityDetailsPromise = getServiceabilityDetails(
serviceZipCode,
@ -297,19 +292,6 @@ export default {
isVehicleProtected: this.isVehicleProtected,
};
},
async set(newValue) {
if (newValue.addressQuestions.zipCode !== this.zipCode) {
await getShopProviderData(newValue.addressQuestions.zipCode).then((result) => {
this.shopProviderData = result.data;
this.selectedProvider = new Provider(
this.shopProviderData.mobileProviderNumber
);
});
}
this.setMobileLocation(newValue);
//handle promise
newValue.resolve?.();
},
},
isServiceableMobile() {
if (this.isRecalibrationServiceableMobile !== null) {
@ -324,9 +306,8 @@ export default {
isMobileStaticRecalibrationApplicable() {
return (
this.displayMSR &&
this.isVehicleMobileStaticRecalibrationApplicable &&
this.mobileFeePart?.partNumber == partNumberStrings.MOBILE_STATIC_RECAL_FEE &&
(this.isInsurance ? this.mobileFeePart?.isInsurable : true)
(this.isInsurance && !this.isITAC ? this.mobileFeePart?.isInsurable : true)
);
},
displayMSR() {
@ -403,6 +384,9 @@ export default {
isInsurance() {
return store.getters.payment.isInsurance;
},
isITAC() {
return store.getters.order.policy.isItac;
},
},
methods: {
arePagePrerequisitesValid() {
@ -488,8 +472,32 @@ export default {
this.mobileFeePart = mobileFeePart;
},
//creating async function as the computed property can not directly handle asynchronous operations or promises.
async setMobileLocationQuestions(newValue, resolve) {
this.mobileLocationQuestions = { ...newValue, resolve };
async setMobileLocationQuestions(newValue) {
var newZipCode = newValue.addressQuestions.zipCode;
if (newZipCode !== this.zipCode) {
await getShopProviderData(newZipCode).then((result) => {
if (this.isServiceableMobile) {
this.shopProviderData = result.data;
this.selectedProvider = new Provider(
this.shopProviderData.mobileProviderNumber
);
}
});
}
if (this.isServiceableMobile) {
this.setMobileLocation(newValue);
//proceed to next page
this.forwardButtonAction();
} else {
this.closeModalAction("mobileLocationQuestions");
await getZipCodeData(newZipCode).then((zipCodeData) => {
this.serviceZipCodeQuestion = {
state: zipCodeData.state,
zipCode: newZipCode,
zipCodeCtu: zipCodeData.zipCodeCtu,
};
});
}
},
getServiceAddressFromStore() {
return store.getters.order.serviceLocation.address;
@ -515,9 +523,6 @@ export default {
getSelectedProvider() {
return store.getters.order.serviceLocation.provider;
},
getIsVehicleMobileStaticRecalibrationApplicableFromStore() {
return store.getters.order.vehicle.isMobileStaticRecalibrationApplicable;
},
resetMobileLocation() {
this.streetAddress = "";
this.apartmentNumberOrBusinessName = "";
@ -676,6 +681,9 @@ export default {
openModalAction(modalName) {
this.$refs[modalName].openModal();
},
closeModalAction(modalName) {
this.$refs[modalName].closeModal();
},
},
watch: {
zipCode: {

View file

@ -8,7 +8,7 @@
href="#!"
@click-event="openModal">
<template v-slot:after-text v-if="editScreenReaderTextCmsWidgetName">
<span class="sr-only"> {{ screenReaderOnlyText }} </span>
<span class="sr-only">{{ screenReaderOnlyText }}</span>
</template>
</textLink>
</div>

View file

@ -8,12 +8,12 @@
:aria-label="buttonLabel"
class="button-content list-button-content d-flex flex-column justify-content-center py-3 px-4">
<div class="row-one">
<span class="m-0 button-label-copy text-truncate" :class="textPosition">{{
buttonLabel
}}</span>
<span class="m-0 caption mx-1 text-nowrap" :class="textPosition">{{
buttonLabelSubCopy
}}</span>
<span class="m-0 button-label-copy text-truncate" :class="textPosition">
{{ buttonLabel }}
</span>
<span class="m-0 caption mx-1 text-nowrap" :class="textPosition">
{{ buttonLabelSubCopy }}
</span>
<div
v-if="displayAvailabilityIndicators"
class="availability-indicator rounded-pill text-nowrap"
@ -22,9 +22,9 @@
v-if="!isLoaderDisplayed"
class="availability-badge"
:class="availabilityRating == 'high' ? 'green' : 'orange'"></div>
<span v-if="!isLoaderDisplayed" class="m-0 button-auxillary-copy">{{
badgeText
}}</span>
<span v-if="!isLoaderDisplayed" class="m-0 button-auxillary-copy">
{{ badgeText }}
</span>
<loader
v-if="isLoaderDisplayed"
:loaderPosition="left"

View file

@ -202,11 +202,17 @@ export default {
async forwardButtonAction() {
const zipCodeData = await this.getZipCodeData(this.serviceZipCode);
if (vinPagesMixin.methods.isPhoneNumber(this.emailOrSms)) {
const phone = this.emailOrSms.replace(/[()]/g, "");
await this.dispatchStoreAction(storeActions.SAVE_PHONE_NUMBER, phone, false);
// if no value due to field being optional, blank both phone and email address
if (!this.emailOrSms) {
await this.dispatchStoreAction(storeActions.SAVE_PHONE_NUMBER, "", false);
await this.dispatchStoreAction(storeActions.SAVE_EMAIL, "", false);
} else {
await this.dispatchStoreAction(storeActions.SAVE_EMAIL, this.emailOrSms, false);
if (vinPagesMixin.methods.isPhoneNumber(this.emailOrSms)) {
const phone = this.emailOrSms.replace(/[()]/g, "");
await this.dispatchStoreAction(storeActions.SAVE_PHONE_NUMBER, phone, false);
} else {
await this.dispatchStoreAction(storeActions.SAVE_EMAIL, this.emailOrSms, false);
}
}
await this.dispatchStoreAction(

View file

@ -152,6 +152,20 @@ export default {
if (isValid) {
vm.forwardButtonAction();
} else {
if (store.getters.externalParameterCustomer?.phoneNumber) {
await baseMixin.methods.dispatchStoreAction(
storeActions.SAVE_PHONE_NUMBER,
store.getters.externalParameterCustomer.phoneNumber,
false
);
await baseMixin.methods.dispatchStoreAction(
storeActions.SAVE_IS_SMS_OPT_IN,
true,
false
);
}
if (store.getters.externalParameterServiceZip.zipCode) {
await baseMixin.methods.dispatchStoreAction(
storeActions.SAVE_SERVICE_ZIP_CODE_INFO,

View file

@ -121,7 +121,6 @@ export default {
modelOptions: [],
styleOptions: [],
displayNoServiceAlert: false,
isMobileStaticRecalibrationApplicable: this.getIsMobileStaticRecalibrationApplicable(),
};
},
@ -444,8 +443,6 @@ export default {
this.imageVifNumber = result?.data.imageVifNumber;
this.imageVifColor = result?.data.imageVifColor;
this.displayNoServiceAlert = !result?.data.canSafeliteService;
this.isMobileStaticRecalibrationApplicable =
result?.data.isMobileStaticRecalibrationApplicable;
},
resetAlert() {
this.displayNoServiceAlert = false;
@ -464,8 +461,6 @@ export default {
imageUrl: this.imageUrl,
imageVifNumber: this.imageVifNumber,
imageVifColor: this.imageVifColor,
isMobileStaticRecalibrationApplicable:
this.isMobileStaticRecalibrationApplicable,
},
false
);
@ -541,9 +536,6 @@ export default {
getImageVifColorfromStore() {
return store.getters.vehicle.imageVifColor;
},
getIsMobileStaticRecalibrationApplicable() {
return store.getters.vehicle.isMobileStaticRecalibrationApplicable;
},
},
components: {

View file

@ -330,11 +330,25 @@ export default {
false
);
if (vinPagesMixin.methods.isPhoneNumber(this.emailOrSms)) {
const phone = this.emailOrSms.replace(/[()]/g, "");
await this.dispatchStoreAction(storeActions.SAVE_PHONE_NUMBER, phone, false);
// if no value due to field being optional, blank both phone and email address
if (!this.emailOrSms) {
await this.dispatchStoreAction(storeActions.SAVE_PHONE_NUMBER, "", false);
await this.dispatchStoreAction(storeActions.SAVE_EMAIL, "", false);
} else {
await this.dispatchStoreAction(storeActions.SAVE_EMAIL, this.emailOrSms, false);
if (vinPagesMixin.methods.isPhoneNumber(this.emailOrSms)) {
const phone = this.emailOrSms.replace(/[()]/g, "");
await this.dispatchStoreAction(
storeActions.SAVE_PHONE_NUMBER,
phone,
false
);
} else {
await this.dispatchStoreAction(
storeActions.SAVE_EMAIL,
this.emailOrSms,
false
);
}
}
await this.dispatchStoreAction(
@ -380,11 +394,25 @@ export default {
);
}
if (vinPagesMixin.methods.isPhoneNumber(this.emailOrSms)) {
const phone = this.emailOrSms.replace(/[()]/g, "");
await this.dispatchStoreAction(storeActions.SAVE_PHONE_NUMBER, phone, false);
// if no value due to field being optional, blank both phone and email address
if (!this.emailOrSms) {
await this.dispatchStoreAction(storeActions.SAVE_PHONE_NUMBER, "", false);
await this.dispatchStoreAction(storeActions.SAVE_EMAIL, "", false);
} else {
await this.dispatchStoreAction(storeActions.SAVE_EMAIL, this.emailOrSms, false);
if (vinPagesMixin.methods.isPhoneNumber(this.emailOrSms)) {
const phone = this.emailOrSms.replace(/[()]/g, "");
await this.dispatchStoreAction(
storeActions.SAVE_PHONE_NUMBER,
phone,
false
);
} else {
await this.dispatchStoreAction(
storeActions.SAVE_EMAIL,
this.emailOrSms,
false
);
}
}
await this.dispatchStoreAction(

View file

@ -92,6 +92,13 @@ export default {
});
let totalPrice = this.getTotalPriceOfAllLineItemsAndChildParts(lineItemsToPrice, false);
// prettier-ignore
{
if (store.getters.applicationUser.loggingOption) {
console.log(new Date() + " base-mixin getTierOnePackagePrice lineItemsToPrice: " + JSON.stringify(lineItemsToPrice));
console.log(new Date() + " base-mixin getTierOnePackagePrice totalPrice: " + totalPrice);
}
}
return totalPrice;
},
filterOutFees(lineItems) {
@ -103,6 +110,12 @@ export default {
});
return filteredLineItems;
},
filterOutServicePackageDiscountPart(lineItems) {
const filteredLineItems = lineItems?.filter((item) => {
return !item?.partType?.includes(partTypeStrings.SERVICE_PACKAGE_DISCOUNT);
});
return filteredLineItems;
},
filterOutRecalibration(lineItems) {
return getItemsWithoutRecalParts(lineItems);
},

View file

@ -12,6 +12,7 @@ import { singleWindshieldCarIds } from "@/constants/single-windshield-carids";
import { fmgPageValues } from "@/router/router-constants/fmgPage-values";
import { deleteFunnelCookie } from "@/helpers/heritage-integration/cookie-helper.js";
import { deepEqual } from "@/helpers/object-helper";
import baseMixin from "@/mixins/base-mixin.js";
import {
AppointmentTypeStrings,
PREMIUM_FEE_PART_TYPE,
@ -45,6 +46,7 @@ import {
containsRecalParts,
getTopLevelPartsWithRecal,
isRecalPartOrHasChildRecalPart,
getRecalPartNumbers,
} from "@/helpers/recal-helper";
import { externalParameterStatus } from "@/constants/external-parameters";
import { experimentSettings } from "@/constants/experiments";
@ -68,7 +70,6 @@ const getDefaultState = () => {
registration: {
licensePlate: null,
},
isMobileStaticRecalibrationApplicable: false,
},
serviceLocation: {
address: null,
@ -227,10 +228,6 @@ export const mutations = {
updateVehicleVin(state, vin) {
state.order.vehicle.vin = vin;
},
updateIsMobileStaticRecalibrationApplicable(state, isMobileStaticRecalibrationApplicable) {
state.order.vehicle.isMobileStaticRecalibrationApplicable =
isMobileStaticRecalibrationApplicable;
},
updateIsRepair(state, isRepair) {
state.order.damage.isRepair = isRepair;
},
@ -365,8 +362,6 @@ export const mutations = {
state.order.vehicle.imageUrl = vehicleInfo.imageUrl;
state.order.vehicle.imageVifNumber = vehicleInfo.imageVifNumber;
state.order.vehicle.imageColor = vehicleInfo.imageVifColor;
state.order.vehicle.isMobileStaticRecalibrationApplicable =
vehicleInfo.isMobileStaticRecalibrationApplicable;
},
updateRegistration(state, registrationInfo) {
state.order.vehicle.registration.licensePlate = registrationInfo?.licensePlate;
@ -535,6 +530,7 @@ export const mutations = {
},
resetExternalParameterQuoteState(state) {
externalParameterState.quote.servicePackage = null;
externalParameterState.quote.isInsurance = null;
saveExternalParameterState(externalParameterState);
},
resetIsExternalParameter(state) {
@ -559,7 +555,6 @@ export const mutations = {
state.order.vehicle.imageUrl = null;
state.order.vehicle.imageVifNumber = null;
state.order.vehicle.imageColor = null;
state.order.vehicle.isMobileStaticRecalibrationApplicable = false;
},
resetDamageState(state) {
state.order.damage.isRepair = null;
@ -1028,8 +1023,12 @@ export const getters = {
return email;
}
// if we're here, then we have both email and phone so get the value from pageData to reflect what they entered on the page
return state.applicationUser.pageData[fmgPageValues.EMAIL_SMS_PAGES]?.emailOrSmsValue;
// if we're here, then we have both email and phone so get the value from pageData to reflect what they entered on the page.
// if they didn't enter anything on the page default to email
return (
state.applicationUser.pageData[fmgPageValues.EMAIL_SMS_PAGES]?.emailOrSmsValue ??
state.order.customer.emailAddress
);
},
};
@ -1705,8 +1704,6 @@ export const actions = {
const coverageStatus = coverageStatusEnum(order.payment?.insuranceCoverage?.coverageStatus);
const coverageType = coverageTypeEnum(order.payment?.insuranceCoverage?.coverageType);
const isItacOptimized = order.policy?.isItac ?? false;
const isMobileStaticRecalibrationApplicable =
order.vehicle?.isMobileStaticRecalibrationApplicable;
const zipCode =
serviceZipCode ??
order.serviceLocation?.provider?.address?.zipCode ??
@ -1722,12 +1719,15 @@ export const actions = {
var endPoint = `${endpoints.GetMobileFeePart.url}/?serviceType=${serviceType}&facilityType=${facilityType}&parentAccountNumber=${parentAccountNumber}&billToAccountNumber=${billToAccountNumber}&providerNumber=${providerNumber}&isItacOptimized=${isItacOptimized}&zipCode=${zipCode}`;
if (isMobileStaticRecalibrationApplicable) {
const recalPartNumber = getRecalPartNumber(order.lineItems?.glassParts[0]);
const carId = order.vehicle?.carId;
if (recalPartNumber && carId) {
endPoint = `${endPoint}&partNumbers=${recalPartNumber}&carId=${carId}`;
const recalPartNumbers = getRecalPartNumbers(order.lineItems?.glassParts);
const carId = order.vehicle?.carId;
if (recalPartNumbers && recalPartNumbers.length > 0 && carId) {
let additionalQueryParams = ``;
for (let i = 0; i < recalPartNumbers.length; i++) {
additionalQueryParams += `&partNumbers[${i}]=${recalPartNumbers[i]}`;
}
additionalQueryParams += `&carId=${carId}`;
endPoint += additionalQueryParams;
}
if (coverageStatus) {
@ -2330,18 +2330,7 @@ export const actions = {
// Vehicle
saveVehicle(
context,
{
year,
make,
model,
style,
carId,
category,
imageUrl,
imageVifNumber,
imageVifColor,
isMobileStaticRecalibrationApplicable,
}
{ year, make, model, style, carId, category, imageUrl, imageVifNumber, imageVifColor }
) {
if (
context.state.order.vehicle.year != year ||
@ -2362,10 +2351,6 @@ export const actions = {
context.commit(storeMutations.UPDATE_VEHICLE_IMAGE_URL, imageUrl);
context.commit(storeMutations.UPDATE_VEHICLE_IMAGE_VIF_NUMBER, imageVifNumber);
context.commit(storeMutations.UPDATE_VEHICLE_IMAGE_COLOR, imageVifColor);
context.commit(
storeMutations.UPDATE_VEHICLE_MOBILE_STATIC_RECALIBRATION_APPLICABLE,
isMobileStaticRecalibrationApplicable
);
}
},
@ -3190,6 +3175,42 @@ export const actions = {
context.commit(storeMutations.UPDATE_AFFILIATE_COOKIES, affiliateCookies);
},
addDonationToSubmittedOrder(context, donationAmount) {
console.log("running addDonationToSubmittedOrder()... donationAmount: ", donationAmount);
const submittedOrder = deepClone(baseMixin.methods.getSubmittedOrder());
if (donationAmount > 0) {
console.log(
"running addDonationToSubmittedOrder()... adding donation to supportingItems in session storage "
);
submittedOrder.lineItems.supportingItems.push({
partNumber: "DONATION",
description: null,
partType: "DONATION",
isInsurable: false,
isChildPart: false,
laborAmount: 0,
sellingPrice: donationAmount,
kitPrice: 0,
salesTax: null,
id: "donation-" + donationAmount,
});
} else {
// filter out all donations
console.log(
"running addDonationToSubmittedOrder()... removing all donation items in supportingItems in session storage "
);
const nonDonationItems = submittedOrder.lineItems.supportingItems.filter((item) => {
return item.partType !== "DONATION";
});
submittedOrder.lineItems.supportingItems = nonDonationItems;
}
// set to local storage
window.sessionStorage.setItem("submittedOrder", JSON.stringify(submittedOrder));
},
resetSubmittedOrder(context) {
// clear from local storage
window.sessionStorage.removeItem("submittedOrder");
@ -3791,15 +3812,3 @@ function getExternalParameterDefaultState() {
function saveExternalParameterState(externalParameterState) {
window.sessionStorage.setItem("externalParameterState", JSON.stringify(externalParameterState));
}
//This function finds the recalibration part and returns the part number for it.
function getRecalPartNumber(glassPartsArray) {
const recalPart = glassPartsArray.childParts.find(
(item) => item.partType === partTypeStrings.ADAS_RECALIBRATION
);
if (recalPart) {
return recalPart.partNumber;
} else {
return null;
}
}

View file

@ -156,5 +156,48 @@ export default {
transition: background 0s 0s ease-in-out;
}
}
&.btn-success {
position: relative;
background: $green-100;
border: 1px solid $green-400;
border-radius: $border-radius-lg;
color: $black;
font-weight: 500;
transition: all 150ms linear;
height: 3rem;
&:hover {
color: $black;
background: $green-200;
}
&:focus, // Mouse, touch, stylus focus
&:focus-visible {
// Keyboard focus for accessibility
outline: none;
box-shadow:
0 0 0 3px $white,
0 0 0 5.5px $green-400;
color: $black;
background: $green-100;
}
&:disabled {
background: transparent;
color: $gray-550 !important;
font-weight: 400;
height: 48px;
border: 1px solid $gray-550;
border-radius: $border-radius-lg;
cursor: pointer;
pointer-events: all;
}
&.has-loader {
color: $white;
@include blue-gradient;
pointer-events: none;
}
&.delay {
// fixes flicker while transitioning between states
transition: background 0s 0s ease-in-out;
}
}
}
</style>

View file

@ -1,22 +1,26 @@
<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>
<a v-if="linkType === 'navigation'" @click="handleClick" class="navigation-link" :href="href">
{{ text }}
<slot name="after-text"></slot>
</a>
<a
v-else-if="linkType === 'newWindowLink'"
@click="handleClick"
class="new-window-link"
:href="href"
target="_blank"
>{{ text }}<slot name="after-text"></slot
></a>
<a v-else-if="linkType === 'textSmall'" @click="handleClick" class="small" :href="href"
>{{ text }}<slot name="after-text"></slot
></a>
<a v-else-if="linkType === 'text'" @click="handleClick" :href="href"
>{{ text }}<slot name="after-text"></slot
></a>
target="_blank">
{{ text }}
<slot name="after-text"></slot>
</a>
<a v-else-if="linkType === 'textSmall'" @click="handleClick" class="small" :href="href">
{{ text }}
<slot name="after-text"></slot>
</a>
<a v-else-if="linkType === 'text'" @click="handleClick" :href="href">
{{ text }}
<slot name="after-text"></slot>
</a>
</template>
<script>