CSR-1383: add product tiles; some cart updates for CSR-1382

This commit is contained in:
Adam Caouette 2023-09-28 16:52:07 -04:00
parent 166978bc3a
commit 86add1e358
7 changed files with 404 additions and 156 deletions

View file

@ -14,6 +14,9 @@ module.exports = {
"!src/helpers/unit-test-helper.js",
"!src/layouts/vehicle-damage/windshield-options/windshield-options.vue",
"!src/layouts/reveal/**/*.vue",
"!src/layouts/payment-method/**/*.vue",
"!src/fmg-components/cart/**/*.vue",
"!src/fmg-components/add-vaps-modal-buttons/**/*.vue",
"!src/ux-components/text-link/**/*.vue",
"!src/layouts/vin-lookup/**/*.vue", //Temporary for Quote page testing
"!src/common-components/funnel-header/menu-modal/**/*.vue",

View file

@ -197,6 +197,12 @@ export default {
classes += "flex-column";
}
break;
case "addVapsButton":
classes = "row g-4 justify-content-center mb-2";
if (this.isWide) {
classes += " flex-column";
}
break;
case "radio":
classes = "ui-radio d-flex";
break;

View file

@ -0,0 +1,218 @@
<template>
<baseInputButton
v-bind="$props"
:buttonWrapperClasses="[
'add-vaps-button w-100 rounded-3 d-flex align-items-center h-100 base-input-button',
{ horizontal: isWide },
]"
v-model="selectedValue">
<div
class="d-flex w-100 align-items-center px-1 h-100 add-vaps-button-content button-content rounded-3"
:class="labelClasses">
<img :id="buttonImageId" :src="buttonImage" :alt="altText" />
<div class="copy-wrapper" :class="isWide ? 'ms-4 me-2' : ''">
<p class="small mb-0" :class="isWide ? 'mt-0' : 'mt-1'">
{{ buttonLabel }}
</p>
<p v-if="buttonLabelSubCopy" class="fs-7 m-0 sub-copy">
{{ buttonLabelSubCopy }}
</p>
</div>
<p class="fs-7 mb-0 link-styling" :class="isWide ? 'mt-0' : 'mt-2'">View details</p>
</div>
</baseInputButton>
</template>
<script>
import baseInputButton from "@/digital-components/base-input-button/base-input-button";
import inputButtonWrapperMixin from "@/mixins/input-button-wrapper-mixin";
export default {
name: "addVapsButton",
mixins: [inputButtonWrapperMixin],
components: {
baseInputButton,
},
computed: {
labelClasses() {
if (this.isWide) {
let classes = "flex-row py-2 ps-4 pe-4";
if (this.buttonLabelSubCopy) {
classes += " checkboxTop";
}
return classes;
} else {
return "flex-column py-2";
}
},
},
};
</script>
<style lang="scss">
@mixin add-vaps-button-focus($box-shadow-color) {
&:focus-visible + .add-vaps-button-content {
box-shadow: 0 0 0 2.5px $box-shadow-color;
border-radius: 0.5rem;
}
&:focus + .add-vaps-button-content {
box-shadow: 0 0 0 2.5px $box-shadow-color;
border-radius: 0.5rem;
}
&:checked:focus + .add-vaps-button-content {
box-shadow: 0 0 0 2.5px $box-shadow-color;
}
}
.add-vaps-button {
border: 1px solid $gray-500;
&.has-error {
input[type="checkbox"],
input[type="radio"] {
@include add-vaps-button-focus($red);
}
}
img {
height: auto;
width: 2.6rem;
max-width: 100%;
}
.copy-wrapper {
flex: 1 1 auto;
}
.link-styling {
font-weight: 500;
color: $blue;
text-decoration: underline;
text-underline-offset: 4px;
}
input[type="checkbox"],
input[type="radio"] {
position: absolute;
+ .add-vaps-button-content {
outline: none;
display: flex;
position: relative;
justify-content: space-between;
p {
color: $gray-600;
text-align: center;
&.sub-copy {
color: $green;
}
}
}
@include add-vaps-button-focus($blue);
+ .add-vaps-button-content::before {
content: "";
position: absolute;
display: flex;
margin: 0 auto;
width: 1rem;
height: 1rem;
margin: 3rem 0 0 0;
background: white;
border: 1px solid $gray-500;
border-radius: 2px;
order: 2;
flex-shrink: 0;
color: $gray-600;
}
+ .add-vaps-button-content.checkboxTop::before {
margin: -1.25rem 0.5rem 0 0 !important;
}
+ .add-vaps-button-content.checkboxTop::after {
margin: -1.5rem 0.5rem 0 0 !important;
}
&:checked + .add-vaps-button-content::before {
background: $blue;
}
&:checked + .add-vaps-button-content::after {
content: "";
position: absolute;
margin: 3.2rem 0 0 0;
border-left: 2px solid $white;
border-bottom: 2px solid $white;
height: 6px;
width: 11px;
transform: rotate(-45deg);
z-index: 1;
}
}
input[type="radio"] {
+ .add-vaps-button-content::before {
content: "";
display: none;
}
+ .add-vaps-button-content::after {
content: "";
display: none;
}
+ .add-vaps-button-content {
img {
margin-bottom: 0;
}
}
}
&.horizontal {
input[type="checkbox"],
input[type="radio"] {
+ .add-vaps-button-content::before {
content: "";
position: relative;
margin: 0 0.5rem 0 0;
order: 1;
}
&:checked + .add-vaps-button-content::after {
content: "";
margin: -0.15rem 0 0 0;
left: 1.175rem;
}
&:checked + .add-vaps-button-content {
p {
color: $black;
font-weight: 500;
}
.sub-copy {
color: $gray-600;
font-weight: 400;
}
}
+ .add-vaps-button-content {
outline: none;
min-height: 48px;
color: $gray-600;
img {
margin-bottom: 0;
}
p {
color: $gray-600;
text-align: left;
&.sub-copy {
color: $gray-550;
}
}
}
}
}
}
</style>

View file

@ -0,0 +1,102 @@
<template>
<transition name="fade" mode="out-in">
<div class="add-vaps-buttons" aria-live="polite" v-if="isDisplayed">
<buttonQuestion
ref="buttonQuestion"
:answers="answersToDisplay"
groupName="addVaps"
v-model="selectedValues"
buttonTypeString="addVapsButton"
:buttonTypeObject="addVapsButton"
:isWide="answersToDisplay.length > 1 ? false : true" />
</div>
</transition>
</template>
<script>
import buttonQuestion from "@/digital-components/button-question/button-question";
import addVapsButton from "./add-vaps-button/add-vaps-button";
export default {
name: "add-vaps-modal-buttons",
props: {
cart: Object,
rainDefenseItem: Object,
wiperItems: Object,
vapsTilesCmsName: String,
},
data() {
return {
addVapsButton: addVapsButton,
};
},
computed: {
selectedValues: {
get: function () {
return this.answersToDisplay;
},
set: function (newValue) {
// TODO - emit event to open modal
},
},
isDisplayed() {
return this.showAddRainDefense || this.showAddWipers;
},
showAddRainDefense() {
const rainDefenseIndex = this.cart.findIndex((vap) => {
return vap.partType === "RAIN DEFENSE";
});
return rainDefenseIndex > -1 ? false : true;
},
showAddWipers() {
const rainDefenseIndex = this.cart.findIndex((vap) => {
return vap.partType.includes(" WIPER");
});
return rainDefenseIndex > -1 ? false : true;
},
rainDefensePrice() {
const lineItem = this.rainDefenseItem;
const price = lineItem.laborAmount + lineItem.sellingPrice + lineItem.kitPrice;
return parseFloat(price).toFixed(2);
},
wipersPrice() {
let price = 0;
const lineItems = this.wiperItems;
lineItems?.forEach((item) => {
price += item.laborAmount + item.sellingPrice + item.kitPrice;
});
return parseFloat(price).toFixed(2);
},
answersCmsData() {
return this.getCmsContent(this.vapsTilesCmsName, "Answers");
},
answersToDisplay() {
const answers = [];
if (!this.answersCmsData) return answers;
const getAnswer = (name, answers) => {
const answerIndex = answers.findIndex((answer) => {
return answer.Name === name;
});
return answers[answerIndex];
};
if (this.showAddWipers) {
const answer = getAnswer("ADD WIPER BLADES", this.answersCmsData);
answer.SubText = "+$" + this.wipersPrice;
answers.push(answer);
}
if (this.showAddRainDefense) {
const answer = getAnswer("ADD RAIN DEFENSE", this.answersCmsData);
answer.SubText = "+$" + this.rainDefensePrice;
answers.push(answer);
}
return answers;
},
},
components: {
buttonQuestion,
},
};
</script>

View file

@ -14,13 +14,17 @@
<div class="service-type" :class="{ packageWithVAPS: packageWithVAPS }">
<textBlock :cmsWidgetName="packageNameWidget" /><span>{{ packagePrice }}</span>
</div>
<div v-for="(lineItem, i) in displayedPackageLineItems" :key="i"
<div
v-for="(lineItem, i) in displayedPackageLineItems"
:key="i"
:class="[this.displayedPackageLineItems ? 'vaps' : '']">
<span>{{ lineItem.name }}</span>
<span><a href="#">Remove</a></span>
</div>
<hr style="background: red;">
<div v-for="(lineItem, i) in displayedNonPackageLineItems" :key="i"
<hr style="background: red" />
<div
v-for="(lineItem, i) in displayedNonPackageLineItems"
:key="i"
:class="[this.displayedNonPackageLineItems ? 'vaps' : '']">
<span>{{ lineItem.name }}</span>
<span><a href="#">Remove</a></span>
@ -122,7 +126,7 @@ export default {
...findLineItemsWithPartType(vapType, this.nullSafeAvailableLineItems)
);
});
console.log("VapsLineItems:", vapsLineItemsForSelectedPackage);
// console.log("VapsLineItems:", vapsLineItemsForSelectedPackage);
return vapsLineItemsForSelectedPackage;
},
},
@ -174,15 +178,12 @@ export default {
};
displayedPackageLineItems.push(packageLineItem);
});
console.log("What displayedPackageLineItems is", packageLineItems);
// console.log("What displayedPackageLineItems is", packageLineItems);
return displayedPackageLineItems;
},
displayedNonPackageLineItems() {
const displayedNonPackageLineItems = [];
const possibleLineItems = [
...this.orderLineItems.vaps,
...this.orderLineItems.supportingItems,
];
const possibleLineItems = [...this.vaps, ...this.supportingItems];
possibleLineItems?.forEach((item) => {
let price = item.sellingPrice + item.kitPrice + item.laborAmount;
const isAllowed = this.allowableLineItems?.findIndex((lineItem) => {
@ -207,13 +208,24 @@ export default {
return displayedNonPackageLineItems;
},
glassToReplace() {
if (!this.damage) {
return;
}
return this.damage.glassToReplace;
},
isRepair() {
if (!this.damage) {
return;
}
return this.damage.isRepair;
},
vaps() {
return this.orderLineItems?.vaps;
const vaps = this.orderLineItems?.vaps;
return vaps?.length > 0 ? vaps : [];
},
supportingItems() {
const supportingItems = this.orderLineItems?.supportingItems;
return supportingItems?.length > 0 ? supportingItems : [];
},
recycleLabel() {
return "Recycling";

View file

@ -211,7 +211,6 @@ export function getHighestRequiredTier(glassToReplace, availableLineItems, isRep
export function getHighestFullySatisfiedTier(glassToReplace, availableLineItems, isRepair, vaps) {
const packages = getAvailablePackages(glassToReplace, availableLineItems, isRepair);
console.log(glassToReplace);
let highestSatisfiedPackage = packageNames.TIER_ONE;
for (let i = 0; i < packages.length; i++) {
@ -224,7 +223,6 @@ export function getHighestFullySatisfiedTier(glassToReplace, availableLineItems,
highestSatisfiedPackage = packages[i].packageName;
}
}
console.log(highestSatisfiedPackage);
return highestSatisfiedPackage;
}

View file

@ -1,56 +1,31 @@
<template>
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm" v-slot="{ meta }">
<div class="page-container-grouped-styles">
<loadingModal ref="loadingModal" />
<div class="container-fluid page-container-grouped-styles">
<div class="row">
<div class="col">
<funnelHeader cmsWidgetName="FunnelHeaderWidget" />
</div>
</div>
<div class="row justify-content-md-center">
<div class="col-md-6 col-xl-4">
<vehicleBanner
cmsWidgetName="VehicleBannerWidget"
:displayGenericVehicleImage="false" />
<funnelSubHeader
typeStyle="h5"
class="mb-5 mt-4 dark-header"
leftAlignHeader
cmsWidgetName="FunnelSubHeaderWidget" />
<funnelSubHeader class="mb-5" cmsWidgetName="FunnelSubHeaderWidget" />
<div class="fade-on-route-transition sub-container make-tall">
<div>
<cart
:damage="damageInfo"
:orderLineItems="orderLineItems"
:availableLineItems="availableLineItems"
servicePackageOptionsCmsName="ServicePackageTitle"
:allowableCartItems="ALLOWABLE_CART_ITEMS" />
:availableLineItems="cartItems"
servicePackageOptionsCmsName="ServicePackageTitle" />
<vehicleReview
cmsWidgetName="VehicleReviewWidget"
:vehicle="vehicleInfo"
@edit-clicked="editVehicle" />
<hr class="my-0" />
<damageReview
cmsWidgetName="DamageReviewWidget"
damageLocationsWidgetName="DamageLocationsWidget"
:damage="damageInfo"
@edit-clicked="editDamage" />
<hr class="my-0" />
<!-- <servicePackageReview
ref="servicePackageReview"
servicePackageOptionsCmsName="ServicePackageTitle"
defaultPackageItemsCmsName="DefaultPackageItemDescriptions"
vapsItemsCmsName="VapsItemDescriptions"
:damage="damageInfo"
:lineItems="lineItems"
@edit-clicked="editServicePackage" /> -->
<hr class="my-0" />
<serviceLocationReview
cmsWidgetName="ServiceLocationTitleWidget"
:serviceLocation="serviceLocationInfo"
@edit-clicked="editServiceLocation" />
</div>
<add-vaps-modal-buttons
:cart="cartItems"
:wiperItems="availableWipers"
:rainDefenseItem="availableRainDefense"
vapsTilesCmsName="VapsProductTiles" />
<navbar
cmsWidgetName="FunnelFooterWidget"
@ -60,6 +35,7 @@
@ForwardClicked="forwardButtonAction" />
</div>
</div>
</div>
</Form>
</template>
@ -69,57 +45,35 @@ import funnelHeader from "@/fmg-components/funnel-header/funnel-header";
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 vehicleReview from "@/layouts/review/review-sections/vehicle-review/vehicle-review";
import damageReview from "@/layouts/review/review-sections/damage-review/damage-review";
import servicePackageReview from "@/layouts/review/review-sections/service-package-review/service-package-review";
import serviceLocationReview from "@/layouts/review/review-sections/service-location-review/service-location-review";
import cart from "@/fmg-components/cart/cart";
import addVapsModalButtons from "@/fmg-components/add-vaps-modal-buttons/add-vaps-modal-buttons";
import baseMixin from "@/mixins/base-mixin.js";
import vehicleQuestionsMixin from "../../mixins/vehicle-questions-mixin";
import { settleAllPromises } from "@/helpers/layout-helper";
import { storeActions } from "@/constants/store-actions";
import store from "@/store";
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { required } from "@/helpers/validation-rules";
import { errorMessages } from "@/constants/error-messages";
import { Form, defineRule } from "vee-validate";
import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
import { applicationConfig } from "@/constants/application-config";
defineRule("option-required", required(errorMessages.OPTION_REQUIRED));
const ALLOWABLE_CART_ITEMS = [
{
partType: "FRONT WIPER",
isRemovable: true,
},
{
partType: "REAR WIPER",
isRemovable: true,
},
{
partType: "RAIN DEFENSE",
isRemovable: true,
},
{
partType: "RECYCLE FEE",
isRemovable: false,
}
]
import { Form } from "vee-validate";
export default {
name: "paymentMethod",
async beforeRouteEnter(to, from, next) {
// Call APIs
const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage);
const wipersPromise = baseMixin.methods.dispatchStoreAction(storeActions.GET_WIPERS);
const rainDefensePromise = baseMixin.methods.dispatchStoreAction(
storeActions.GET_RAIN_DEFENSE
const wipersPromise = baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.GET_WIPERS,
null,
"payment-method"
);
const supportingItemsPromise = baseMixin.methods.dispatchStoreAction(
storeActions.GET_SUPPORTING_ITEMS
const rainDefensePromise = baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.GET_RAIN_DEFENSE,
null,
"payment-method"
);
const supportingItemsPromise = baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.GET_SUPPORTING_ITEMS,
null,
"payment-method"
);
const promiseResultMap = [
@ -151,12 +105,12 @@ export default {
...resultMap.wipers,
...clonedGlassParts,
];
console.log("Available Line Items", availableLineItems);
const pricingResults = await baseMixin.methods.dispatchStoreAction(
const pricingResults = await baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.PRICE_ORDER_ITEMS_AND_SAVE_SERVER_DATA,
{
availableLineItems: availableLineItems,
},
"payment-method",
false
);
@ -166,13 +120,15 @@ export default {
vm.pricedGlassParts = clonedGlassParts;
vm.supportingItems = resultMap.supportingItems;
vm.availableLineItems = pricingResults;
vm.isInsuranceSelected = vm.getDefaultIsInsuranceSelectedValue(vm.availableLineItems);
vm.availableWipers = resultMap.wipers;
vm.availableRainDefense = resultMap.rainDefense;
});
},
data() {
return {
isInsuranceSelected: null,
selectedVaps: null,
cartItems: [],
availableWipers: null,
availableRainDefense: null,
availableLineItems: null,
supportingItems: null,
pricedGlassParts: null,
@ -180,34 +136,10 @@ export default {
},
methods: {
arePagePrerequisitesValid() {
return (
store.getters.order.serviceLocation.zipCode &&
store.getters.order.serviceLocation.zipCodeCtu &&
(store.getters.order.damage.isRepair ||
(store.getters.order.lineItems?.glassParts != null &&
store.getters.order.lineItems.glassParts.length > 0)) &&
store.getters.order.referralNumber?.length !== 6
);
},
getDefaultIsInsuranceSelectedValue(availableLineItems) {
// Override if coming back from QuoteDetails. Remove override after Quote release
const isInsuranceOverrideValue = this.$route.query?.isInsurance;
const defaultIsInsuranceSelectedValue = this.$store.getters.order.payment.isInsurance;
if (isInsuranceOverrideValue != null) {
return isInsuranceOverrideValue == "true";
} else if (defaultIsInsuranceSelectedValue != null) {
return defaultIsInsuranceSelectedValue;
} else {
return availableLineItems
? baseMixin.methods.getTierOnePackagePrice(
baseMixin.methods.filterOutFees(availableLineItems)
) > 300
: null;
}
return true;
},
vapsItemsSelectedAction(vapsItemsSelected) {
this.selectedVaps = vapsItemsSelected;
this.cartItems = vapsItemsSelected;
},
backButtonAction() {},
forwardButtonAction() {
@ -215,27 +147,12 @@ export default {
},
},
computed: {
subHeaderTitle() {
return this.getCmsContent("FunnelSubHeaderWidget", "HeaderText");
},
subHeaderBody() {
return this.getCmsContent("FunnelSubHeaderWidget", "BodyText");
},
forwardButtonText() {
return this.getCmsContent("FunnelFooterWidget", "ForwardButtonText");
},
vehicleInfo() {
return this.$store.getters.vehicle;
},
damageInfo() {
return this.$store.getters.damage;
},
orderLineItems() {
return this.$store.getters.lineItems;
},
serviceLocationInfo() {
return this.$store.getters.order.serviceLocation;
},
},
components: {
funnelHeader,
@ -243,16 +160,8 @@ export default {
funnelSubHeader,
Form,
loadingModal,
// servicePackageReview,
serviceLocationReview,
vehicleReview,
damageReview,
cart,
addVapsModalButtons,
},
};
</script>
<style scoped>
.text-block {
display: block;
}
</style>