Merge branch 'develop' into feature/CSR-1124

This commit is contained in:
Leah Schumann 2023-06-08 14:01:45 -04:00
commit 1133132e90
14 changed files with 345 additions and 104 deletions

View file

@ -26,7 +26,7 @@ module.exports = {
testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"], testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"],
coverageThreshold: { coverageThreshold: {
global: { global: {
statements: 78, statements: 77,
// Got the go ahead from Mark to temporarily lower this. Taking out initialize component made the year,make,model and style coverage drop a bit. Once unit tests for license plate lookup, vin lookup and address lookup are in the coverage should go back up to 90 // Got the go ahead from Mark to temporarily lower this. Taking out initialize component made the year,make,model and style coverage drop a bit. Once unit tests for license plate lookup, vin lookup and address lookup are in the coverage should go back up to 90
}, },
}, },

View file

@ -61,6 +61,7 @@ const storeMutations = {
RESET_SERVICE_LOCATION_APPOINTMENT_TYPE: "resetServiceLocationAppointmentType", RESET_SERVICE_LOCATION_APPOINTMENT_TYPE: "resetServiceLocationAppointmentType",
RESET_SERVICE_LOCATION_PROVIDER: "resetServiceLocationProvider", RESET_SERVICE_LOCATION_PROVIDER: "resetServiceLocationProvider",
RESET_SERVICE_LOCATION_MOBILE_ADDRESS: "resetServiceLocationMobileAddress", RESET_SERVICE_LOCATION_MOBILE_ADDRESS: "resetServiceLocationMobileAddress",
RESET_SCHEDULE: "resetSchedule",
// OTHER MUTATIONS // OTHER MUTATIONS
UPDATE_PAGE_DATA: "updatePageData", UPDATE_PAGE_DATA: "updatePageData",

View file

@ -1,7 +1,7 @@
import { shallowMount } from "@vue/test-utils"; import { shallowMount } from "@vue/test-utils";
import TextBlock from "./text-block"; import TextBlock from "./text-block";
describe("modal.vue", () => { describe("text-block.vue", () => {
it("Should display 'Text' when 'Text' is defined in the CMS", async () => { it("Should display 'Text' when 'Text' is defined in the CMS", async () => {
// Act // Act
const wrapper = shallowMount(TextBlock, { const wrapper = shallowMount(TextBlock, {

View file

@ -1,11 +1,43 @@
<template> <template>
<div <div class="text-block w-100" :class="[justifyText, typeStyle, fontWeight, marginTopClass]">
class="text-block w-100 mt-2" <span v-for="copy in splitCopyOnCMSPlaceHolder(this.textBlockCopy)" :key="copy">
:class="[justifyText, typeStyle, fontWeight]" <span v-if="doesCopyContainRouterLink(copy)">
v-html="this.TextBlockCopy"></div> <router-link
:to="{
query: { [pageQueryString]: `${getRouterLinkRouteFromCopy(copy)}` },
name: 'root',
}"
>{{ getRouterLinkDisplayTextFromCopy(copy) }}</router-link
>
</span>
<span v-else-if="doesCopyContainTextLink(copy)">
<textLink
linkType="text"
:text="getRouterLinkDisplayTextFromCopy(copy)"
href="#!"
@click-event="$emit('textLinkClicked', getRouterLinkRouteFromCopy(copy))"
:data-bs-target="'#' + getRouterLinkRouteFromCopy(copy)"
aria-label="Modal window" />
</span>
<span v-else v-html="copy"></span>
</span>
</div>
</template> </template>
<script> <script>
// Components
import textLink from "@/ux-components/text-link/text-link";
// Supporting files
import {
doesCopyContainRouterLink,
doesCopyContainTextLink,
splitCopyOnCMSPlaceHolder,
getRouterLinkRouteFromCopy,
getRouterLinkDisplayTextFromCopy,
} from "@/helpers/cms-content-helper";
import { applicationConfig } from "@/constants/application-config";
export default { export default {
name: "textBlock", name: "textBlock",
props: { props: {
@ -14,14 +46,37 @@ export default {
typeStyle: String, // h1-h6, body, small, label, caption (see Figma or Confluence documentation) typeStyle: String, // h1-h6, body, small, label, caption (see Figma or Confluence documentation)
fontWeight: String, // bold=500, default is 400 fontWeight: String, // bold=500, default is 400
cmsWidgetName: String, cmsWidgetName: String,
marginTopSizeOverride: Number, // override mt-2 with a bootstrap size from 0-5 or auto
},
methods: {
doesCopyContainRouterLink,
doesCopyContainTextLink,
splitCopyOnCMSPlaceHolder,
getRouterLinkRouteFromCopy,
getRouterLinkDisplayTextFromCopy,
}, },
computed: { computed: {
TextBlockCopy() { pageQueryString() {
return applicationConfig.PAGE_QUERYSTRING;
},
textBlockCopy() {
if (this.customText) { if (this.customText) {
return this.customText; return this.customText;
} }
return this.getCmsContent(this.cmsWidgetName, "Text"); return this.getCmsContent(this.cmsWidgetName, "Text");
}, },
marginTopClass() {
if (this.marginTopSizeOverride === "auto") {
return "mt-auto";
}
if (this.marginTopSizeOverride >= 0 && this.marginTopSizeOverride <= 5) {
return "mt-" + this.marginTopSizeOverride;
}
return "mt-2";
},
},
components: {
textLink,
}, },
}; };
</script> </script>

View file

@ -1,6 +1,6 @@
import { storeActions } from "@/constants/store-actions.js"; import { storeActions } from "@/constants/store-actions.js";
import store from "@/store"; import store from "@/store";
import { dynamicStrings } from "../constants/dynamic-strings"; import { dynamicStrings } from "@/constants/dynamic-strings";
export function fetchCmsContentForPage(fmgPage) { export function fetchCmsContentForPage(fmgPage) {
return store.dispatch(storeActions.GET_PAGE_DATA, { pageName: fmgPage }).then((response) => { return store.dispatch(storeActions.GET_PAGE_DATA, { pageName: fmgPage }).then((response) => {
@ -284,7 +284,7 @@ function getIfStatementRegexExpression() {
////////////////////////////////////////// //////////////////////////////////////////
export function doesCopyContainRouterLink(copy) { export function doesCopyContainRouterLink(copy) {
return copy.includes(this.dynamicStrings.ROUTER_LINK); return copy.includes(dynamicStrings.ROUTER_LINK);
} }
export function doesCopyContainTextLink(copy) { export function doesCopyContainTextLink(copy) {

View file

@ -20,19 +20,12 @@
validationRules="vehicle-required" validationRules="vehicle-required"
v-model="selectedVehicleVin" v-model="selectedVehicleVin"
:isCarIdDifferent="isCarIdDifferent" /> :isCarIdDifferent="isCarIdDifferent" />
<div class="alert-provide-vin my-3" v-if="splitAlertProvideVinBodyForLink.length"> <div class="alert-provide-vin" v-if="splitAlertProvideVinBodyForLink.length">
<span v-for="copy in splitAlertProvideVinBodyForLink" :key="copy"> <textBlock
<span v-if="doesCopyContainRouterLink(copy)" class="text-body"> :customText="AlertProvideVinBody"
<router-link justifyText="left"
:to="{ class="mb-3"
query: { fmgPage: `${getRouterLinkRouteFromCopy(copy)}` }, marginTopSizeOverride="3" />
name: 'root',
}"
>{{ getRouterLinkDisplayTextFromCopy(copy) }}</router-link
>
</span>
<span v-else class="m-0 text-body" v-html="copy"></span>
</span>
</div> </div>
<funnelFooter <funnelFooter
cmsWidgetName="FunnelFooterWidget" cmsWidgetName="FunnelFooterWidget"
@ -52,6 +45,7 @@ import vehicleBanner from "@/fmg-components/vehicle-banner/vehicle-banner";
import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header"; import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header";
import alert from "@/ux-components/alert/alert"; import alert from "@/ux-components/alert/alert";
import addressVehiclesQuestion from "@/layouts/address-vehicles/address-vehicles-question/address-vehicles-question"; import addressVehiclesQuestion from "@/layouts/address-vehicles/address-vehicles-question/address-vehicles-question";
import textBlock from "@/digital-components/text-block/text-block";
// Supporting files // Supporting files
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper"; import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
@ -237,6 +231,7 @@ export default {
alert, alert,
funnelFooter, funnelFooter,
addressVehiclesQuestion, addressVehiclesQuestion,
textBlock,
}, },
}; };
</script> </script>

View file

@ -36,8 +36,12 @@ export default {
}, },
}, },
methods: { methods: {
loadInitialData(zipCodeCtu) { loadInitialData(serviceLocationCtu, providerCtu) {
return getAlertReasons(zipCodeCtu); let ctuToUse = serviceLocationCtu;
if (providerCtu) {
ctuToUse = providerCtu;
}
return getAlertReasons(ctuToUse);
}, },
initializeComponent(initialData) { initializeComponent(initialData) {
this.alertReasons = initialData; this.alertReasons = initialData;

View file

@ -4,20 +4,13 @@
<loadingModal ref="loadingModal" /> <loadingModal ref="loadingModal" />
<funnelHeader cmsWidgetName="FunnelHeaderWidget" /> <funnelHeader cmsWidgetName="FunnelHeaderWidget" />
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" class="mt-5" /> <funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" class="mt-5" />
<div class="text-center mt-1 mb-3" v-if="ChangeShopLink.length"> <template v-if="ChangeShopLink.length">
<span v-for="copy in ChangeShopLink" :key="copy"> <textBlock
<span v-if="doesCopyContainRouterLink(copy)" class="text-body"> cmsWidgetName="ChangeShopLink"
<router-link justifyText="center"
:to="{ class="mb-3"
query: { fmgPage: `${getRouterLinkRouteFromCopy(copy)}` }, marginTopSizeOverride="1" />
name: 'root', </template>
}"
>{{ getRouterLinkDisplayTextFromCopy(copy) }}</router-link
>
</span>
<span v-else class="m-0 text-body" v-html="copy"></span>
</span>
</div>
<location-alerts cmsWidgetPrefix="LocationAlert-" ref="locationAlerts" /> <location-alerts cmsWidgetPrefix="LocationAlert-" ref="locationAlerts" />
<date-picker <date-picker
selectableDatesSetting="custom" selectableDatesSetting="custom"
@ -57,9 +50,11 @@ import funnelFooter from "@/fmg-components/funnel-footer/funnel-footer";
import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header"; import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header";
import loadingModal from "@/fmg-components/loading-modal/loading-modal.vue"; import loadingModal from "@/fmg-components/loading-modal/loading-modal.vue";
import { Form, defineRule } from "vee-validate"; import { Form, defineRule } from "vee-validate";
import { processIfStatements } from "@/helpers/cms-content-helper";
import datePicker from "@/digital-components/date-picker/date-picker"; import datePicker from "@/digital-components/date-picker/date-picker";
import locationAlerts from "@/layouts/schedule/location-alerts/location-alerts"; import locationAlerts from "@/layouts/schedule/location-alerts/location-alerts";
import timeSlotModalQuestion from "./time-slot-modal-question/time-slot-modal-question"; import timeSlotModalQuestion from "./time-slot-modal-question/time-slot-modal-question";
import textBlock from "@/digital-components/text-block/text-block";
// Supporting files // Supporting files
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper"; import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
@ -73,7 +68,7 @@ import {
getRouterLinkRouteFromCopy, getRouterLinkRouteFromCopy,
getRouterLinkDisplayTextFromCopy, getRouterLinkDisplayTextFromCopy,
} from "@/helpers/cms-content-helper"; } from "@/helpers/cms-content-helper";
import { AppointmentTypeStrings, PREMIUM_FEE_PART_TYPE } from "./constants/schedule-constants"; import { AppointmentTypeStrings, PREMIUM_FEE_PART_TYPE } from "@/constants/schedule-constants";
import { errorMessages } from "@/constants/error-messages"; import { errorMessages } from "@/constants/error-messages";
import { required } from "@/helpers/validation-rules"; import { required } from "@/helpers/validation-rules";
import store from "@/store"; import store from "@/store";
@ -133,26 +128,27 @@ export default {
customSelectableDatesCallback: getAvailableDates, customSelectableDatesCallback: getAvailableDates,
}); });
// Price EARLY BIRD pre-emptively to allow for asynchronous call to pricing
const pricingPromise = baseMixin.methods.dispatchStoreAction(
storeActions.PRICE_ORDER_ITEMS_AND_SAVE_SERVER_DATA,
{
availableLineItems: [
{
partNumber: PREMIUM_FEE_PART_TYPE,
partType: PREMIUM_FEE_PART_TYPE,
description: null,
},
],
},
false
);
const premiumFeePromise = baseMixin.methods.dispatchStoreAction( const premiumFeePromise = baseMixin.methods.dispatchStoreAction(
storeActions.GET_MOBILE_PREMIUM_FEE storeActions.GET_MOBILE_PREMIUM_FEE
); );
const premiumFeeWithPricePromise = premiumFeePromise.then((result) => {
if (result.data) {
return baseMixin.methods.dispatchStoreAction(
storeActions.PRICE_ORDER_ITEMS_AND_SAVE_SERVER_DATA,
{
availableLineItems: [result.data],
},
false
);
} else {
return result.data;
}
});
const alertReasonsPromise = locationAlerts.methods.loadInitialData( const alertReasonsPromise = locationAlerts.methods.loadInitialData(
store.getters.order.serviceLocation.zipCodeCtu store.getters.order.serviceLocation.zipCodeCtu,
store.getters.order.serviceLocation.provider?.address?.zipCtu
); );
// Settle promises and get results // Settle promises and get results
const promiseResultMap = [ const promiseResultMap = [
@ -169,12 +165,8 @@ export default {
promise: datePickerInitialDataPromise, promise: datePickerInitialDataPromise,
}, },
{ {
resultKey: "premiumFee", resultKey: "premiumFeeWithPrice",
promise: premiumFeePromise, promise: premiumFeeWithPricePromise,
},
{
resultKey: "pricingResults",
promise: pricingPromise,
}, },
]; ];
@ -186,9 +178,10 @@ export default {
vm.$refs.datePicker.initializeComponent(resultMap.datePickerInitialData); vm.$refs.datePicker.initializeComponent(resultMap.datePickerInitialData);
vm.$refs.locationAlerts.initializeComponent(resultMap.alertReasons); vm.$refs.locationAlerts.initializeComponent(resultMap.alertReasons);
vm.selectableDatesData = resultMap.datePickerInitialData.initialShopTimeSlotsResponse; vm.selectableDatesData = resultMap.datePickerInitialData.initialShopTimeSlotsResponse;
if (resultMap.premiumFee) { vm.mobilePremiumAppointmentFee = resultMap.premiumFeeWithPrice
vm.mobilePremiumAppointmentFee = resultMap.pricingResults[0]; ? resultMap.premiumFeeWithPrice[0]
} : null;
vm.updateFooterButtonText(vm.selectedTimeSlotData);
}); });
}, },
computed: { computed: {
@ -203,11 +196,11 @@ export default {
return this.$store.getters.order.serviceLocation.appointmentType; return this.$store.getters.order.serviceLocation.appointmentType;
}, },
timeSlotsForSelectedDate() { timeSlotsForSelectedDate() {
if (this.selectedDate === null) { if (!this.selectedDate) {
return null; return null;
} }
return this.selectableDatesData.days?.find( return this.selectableDatesData.days?.find(
(selectableDate) => selectableDate.dateString === this.selectedDate.dateString (selectableDate) => selectableDate.date === this.selectedDate
); );
}, },
appointmentDateAndTime() { appointmentDateAndTime() {
@ -217,10 +210,9 @@ export default {
const timeSlotSelectedObject = this.getTimeSlotObjectFromTimeSlotId( const timeSlotSelectedObject = this.getTimeSlotObjectFromTimeSlotId(
this.selectedTimeSlotData.id this.selectedTimeSlotData.id
); );
if (timeSlotSelectedObject) { if (timeSlotSelectedObject) {
return { return {
date: this.selectedDate.dateString, date: this.selectedDate,
startTime: timeSlotSelectedObject.startTime, startTime: timeSlotSelectedObject.startTime,
endTime: timeSlotSelectedObject.endTime, endTime: timeSlotSelectedObject.endTime,
routeCode: this.selectedTimeSlotData.id, routeCode: this.selectedTimeSlotData.id,
@ -237,9 +229,22 @@ export default {
splitCopyOnCMSPlaceHolder, splitCopyOnCMSPlaceHolder,
getRouterLinkRouteFromCopy, getRouterLinkRouteFromCopy,
getRouterLinkDisplayTextFromCopy, getRouterLinkDisplayTextFromCopy,
processIfStatements,
arePagePrerequisitesValid() { arePagePrerequisitesValid() {
return true; const serviceLocation = store.getters.order.serviceLocation;
// NEED TODO - WHAT ARE PAGE REQ'S FOR THIS PAGE? 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) { async getAvailableDatesMethod(startDate, endDate) {
const newShopTimeSlots = await getAvailableDates( const newShopTimeSlots = await getAvailableDates(
@ -262,7 +267,7 @@ export default {
}, },
getTimeSlotObjectFromTimeSlotId(timeSlotId) { getTimeSlotObjectFromTimeSlotId(timeSlotId) {
const timeSlots = this.selectableDatesData.days.find( const timeSlots = this.selectableDatesData.days.find(
(selectableDate) => selectableDate.dateString === this.selectedDate.dateString (selectableDate) => selectableDate.date === this.selectedDate
).timeSlots; ).timeSlots;
return timeSlots.find((timeSlot) => timeSlot.id === timeSlotId); return timeSlots.find((timeSlot) => timeSlot.id === timeSlotId);
}, },
@ -312,7 +317,7 @@ export default {
}, },
convertSelectedDateToShortMonthAndDay(selectedDate) { convertSelectedDateToShortMonthAndDay(selectedDate) {
// This conversion ensures we don't get get GMT induced date changes // This conversion ensures we don't get get GMT induced date changes
const dateObject = new Date(`${selectedDate.dateString}T00:00:00`); const dateObject = new Date(`${selectedDate}T00:00:00`);
// Ex: April 25 // Ex: April 25
return dateObject.toLocaleDateString("en-us", { month: "short", day: "numeric" }); return dateObject.toLocaleDateString("en-us", { month: "short", day: "numeric" });
}, },
@ -333,7 +338,8 @@ export default {
backButtonAction() { backButtonAction() {
this.$router.navigateWithoutSaving(this.navigationScenarios.CLICKED_BACK, this.$route); this.$router.navigateWithoutSaving(this.navigationScenarios.CLICKED_BACK, this.$route);
}, },
async forwardButtonAction() { forwardButtonAction() {
this.updateSupportingItems();
this.dispatchStoreAction( this.dispatchStoreAction(
this.storeActions.SAVE_SCHEDULE, this.storeActions.SAVE_SCHEDULE,
this.appointmentDateAndTime, this.appointmentDateAndTime,
@ -341,6 +347,51 @@ export default {
); );
navigateToHeritageFunnel({ loadingModal: this.$refs.loadingModal }); navigateToHeritageFunnel({ loadingModal: this.$refs.loadingModal });
}, },
updateSupportingItems() {
const supportingItems = store.getters.lineItems.supportingItems;
// if we have a premium fee(early bird), then save/update supporting items
if (
this.appointmentType === AppointmentTypeStrings.MOBILE &&
this.selectedTimeSlotData?.isPremiumAppointment
) {
const earlyBirdIndex = supportingItems.findIndex(
(item) => item.partNumber == PREMIUM_FEE_PART_TYPE
);
if (earlyBirdIndex >= 0) {
supportingItems[earlyBirdIndex].laborAmount =
this.mobilePremiumAppointmentFee.laborAmount;
supportingItems[earlyBirdIndex].selingPrice =
this.mobilePremiumAppointmentFee.selingPrice;
supportingItems[earlyBirdIndex].kitPrice =
this.mobilePremiumAppointmentFee.kitPrice;
} else {
supportingItems.push(this.mobilePremiumAppointmentFee);
}
this.dispatchStoreAction(
this.storeActions.SAVE_SUPPORTING_ITEMS,
supportingItems,
false
);
} else {
// if it's not a mobile and/or premium early bird, then make sure we remove any that may have been added
const removeEarlyBirdIndex = supportingItems.findIndex(
(item) => item.partNumber == PREMIUM_FEE_PART_TYPE
);
if (removeEarlyBirdIndex >= 0) {
supportingItems.splice(removeEarlyBirdIndex, 1);
this.dispatchStoreAction(
this.storeActions.SAVE_SUPPORTING_ITEMS,
supportingItems,
false
);
}
}
},
}, },
watch: { watch: {
selectedDate(newValue, oldValue) { selectedDate(newValue, oldValue) {
@ -352,7 +403,7 @@ export default {
}; };
} }
}, },
selectedTimeSlotData() { selectedTimeSlotData(newValue) {
this.updateFooterButtonText(this.selectedTimeSlotData); this.updateFooterButtonText(this.selectedTimeSlotData);
}, },
}, },
@ -365,6 +416,7 @@ export default {
datePicker, datePicker,
locationAlerts, locationAlerts,
timeSlotModalQuestion, timeSlotModalQuestion,
textBlock,
}, },
}; };
</script> </script>

View file

@ -53,7 +53,7 @@ import {
AppointmentTypeStrings, AppointmentTypeStrings,
PREMIUM_TIME_SLOT_ID_FLAG, PREMIUM_TIME_SLOT_ID_FLAG,
PREMIUM_FEE_PART_TYPE, PREMIUM_FEE_PART_TYPE,
} from "../constants/schedule-constants"; } from "@/constants/schedule-constants";
// Validation for the modal button // Validation for the modal button
defineRule("time-slot-required", required(errorMessages.OPTION_REQUIRED)); defineRule("time-slot-required", required(errorMessages.OPTION_REQUIRED));
@ -78,13 +78,14 @@ export default {
}, },
data() { data() {
return { return {
selectedTimeSlotId: null, selectedTimeSlotId: this.modelValue.id,
timeSlotModalListButton: timeSlotModalListButton, timeSlotModalListButton: timeSlotModalListButton,
}; };
}, },
setup(props) { setup(props) {
const { handleChange } = useField("time-slot-modal-question", props.validationRules); const { handleChange } = useField("time-slot-modal-question", props.validationRules);
// Run validation on component load
handleChange(props.modelValue.id);
return { return {
handleChange, handleChange,
}; };
@ -92,6 +93,11 @@ export default {
watch: { watch: {
modelValue() { modelValue() {
// Run component validation that is used at parent level // Run component validation that is used at parent level
if (this.modelValue.isPremiumAppointment) {
this.selectedTimeSlotId = this.addPremiumFlagToInput(this.modelValue.id);
} else {
this.selectedTimeSlotId = this.modelValue.id;
}
this.handleChange(this.modelValue.id); this.handleChange(this.modelValue.id);
}, },
availableTimeSlots(newValue) { availableTimeSlots(newValue) {
@ -162,7 +168,7 @@ export default {
} }
// This conversion ensures we don't get get GMT induced date changes // This conversion ensures we don't get get GMT induced date changes
const dateObject = new Date(`${this.dateAndTimeSlotData.dateString}T00:00:00`); const dateObject = new Date(`${this.dateAndTimeSlotData.date}T00:00:00`);
// Ex: Tuesday, April 22 // Ex: Tuesday, April 22
return dateObject.toLocaleDateString("en-us", { return dateObject.toLocaleDateString("en-us", {
weekday: "long", weekday: "long",
@ -203,12 +209,6 @@ export default {
}, },
// fires any time the modal is closed, AFTER "closeModal" fires if footer button is used // fires any time the modal is closed, AFTER "closeModal" fires if footer button is used
onModalClosed() { onModalClosed() {
// Reset component state to parent's state
if (this.modelValue.isPremiumAppointment) {
this.selectedTimeSlotId = this.addPremiumFlagToInput(this.modelValue.id);
} else {
this.selectedTimeSlotId = this.modelValue.id;
}
this.$emit("time-slot-modal-closed"); this.$emit("time-slot-modal-closed");
}, },
// Expected input: "HH:MM" // Expected input: "HH:MM"

View file

@ -138,7 +138,7 @@ export default {
data() { data() {
return { return {
streetAddress: this.getServiceAddressFromStore(), streetAddress: this.getServiceAddressFromStore(),
apartmentNumberOrBusinessName: "", apartmentNumberOrBusinessName: this.getServiceAddress2FromStore(),
city: this.getServiceCityFromStore(), city: this.getServiceCityFromStore(),
state: this.getServiceStateFromStore(), state: this.getServiceStateFromStore(),
zipCode: this.getServiceZipCodeFromStore(), zipCode: this.getServiceZipCodeFromStore(),
@ -339,6 +339,9 @@ export default {
getServiceAddressFromStore() { getServiceAddressFromStore() {
return store.getters.order.serviceLocation.address; return store.getters.order.serviceLocation.address;
}, },
getServiceAddress2FromStore() {
return store.getters.order.serviceLocation.address2;
},
getServiceCityFromStore() { getServiceCityFromStore() {
return store.getters.order.serviceLocation.city; return store.getters.order.serviceLocation.city;
}, },
@ -394,6 +397,7 @@ export default {
city: this.selectedProvider?.address?.city, city: this.selectedProvider?.address?.city,
state: this.selectedProvider?.address?.state, state: this.selectedProvider?.address?.state,
zip: this.selectedProvider?.address?.zipCode, zip: this.selectedProvider?.address?.zipCode,
zipCtu: this.selectedProvider?.address?.zipCodeCtu,
}, },
}, },
}, },

View file

@ -11,6 +11,7 @@ import { damageLocationsSelected } from "@/constants/damage-locations-selected";
import { fmgPageValues } from "@/router/router-constants/fmgPage-values"; import { fmgPageValues } from "@/router/router-constants/fmgPage-values";
import { deleteFunnelCookie } from "@/helpers/heritage-integration/cookie-helper.js"; import { deleteFunnelCookie } from "@/helpers/heritage-integration/cookie-helper.js";
import { deepEqual } from "@/layouts/service-location/helpers/object-helper/object-helper.js"; import { deepEqual } from "@/layouts/service-location/helpers/object-helper/object-helper.js";
import { AppointmentTypeStrings, PREMIUM_FEE_PART_TYPE } from "@/constants/schedule-constants";
// Export State // Export State
const getDefaultState = () => { const getDefaultState = () => {
@ -333,6 +334,24 @@ export const mutations = {
state.applicationUser.pageData[fmgPageValues.MOLDING_QUESTIONS] = null; state.applicationUser.pageData[fmgPageValues.MOLDING_QUESTIONS] = null;
state.applicationUser.pageData[fmgPageValues.CAPABILITY_QUESTIONS] = null; state.applicationUser.pageData[fmgPageValues.CAPABILITY_QUESTIONS] = null;
}, },
resetSchedule(state) {
state.order.schedule.date = null;
state.order.schedule.startTime = null;
state.order.schedule.endTime = null;
state.order.schedule.routeCode = null;
state.order.schedule.jobMaxMinutes = null;
//early bird fee used on schedule page also needs reset when schedule is reset
const supportingItems = state.order.lineItems.supportingItems;
const removeEarlyBirdIndex = supportingItems.findIndex(
(item) => item.partNumber == PREMIUM_FEE_PART_TYPE
);
if (removeEarlyBirdIndex >= 0) {
supportingItems.splice(removeEarlyBirdIndex, 1);
state.order.lineItems.supportingItems = supportingItems;
}
},
resetState(state) { resetState(state) {
Object.assign(state, getDefaultState()); Object.assign(state, getDefaultState());
}, },
@ -1197,7 +1216,6 @@ export const actions = {
method: endpoints.GetShopTimeSlots.method, method: endpoints.GetShopTimeSlots.method,
endpoint: endpoints.GetShopTimeSlots.url, endpoint: endpoints.GetShopTimeSlots.url,
payload: payload, payload: payload,
logApiCall: false,
}); });
}, },
@ -1249,7 +1267,6 @@ export const actions = {
method: endpoints.GetMobileTimeSlots.method, method: endpoints.GetMobileTimeSlots.method,
endpoint: endpoints.GetMobileTimeSlots.url, endpoint: endpoints.GetMobileTimeSlots.url,
payload: payload, payload: payload,
logApiCall: false,
}); });
}, },
@ -1344,6 +1361,7 @@ export const actions = {
city: order.serviceLocation.provider?.address?.city, city: order.serviceLocation.provider?.address?.city,
state: order.serviceLocation.provider?.address?.state, state: order.serviceLocation.provider?.address?.state,
zip: order.serviceLocation.provider?.address?.zip, zip: order.serviceLocation.provider?.address?.zip,
zipCtu: order.serviceLocation.provider?.address?.zipCtu,
}, },
}, },
}, },
@ -1391,7 +1409,7 @@ export const actions = {
}, },
}) })
.then( .then(
(response) => { async (response) => {
// Flatten location and name properties // Flatten location and name properties
response.data.order.damage?.glassToReplace?.map((glass) => { response.data.order.damage?.glassToReplace?.map((glass) => {
glass.glassLocation = glass.location; glass.glassLocation = glass.location;
@ -1405,10 +1423,13 @@ export const actions = {
if (context.state.order.eon && context.state.order.eon != response.data.eon) { if (context.state.order.eon && context.state.order.eon != response.data.eon) {
context.commit(storeMutations.RESET_STATE); context.commit(storeMutations.RESET_STATE);
} }
context.commit( context.commit(
storeMutations.UPDATE_STATE_WITH_ORDER_INFORMATION, storeMutations.UPDATE_STATE_WITH_ORDER_INFORMATION,
response.data response.data
); );
await validateAppointment(context, response.data.order);
return response; return response;
}, },
(error) => { (error) => {
@ -1801,7 +1822,6 @@ export const actions = {
`&${availableLineItemsFormattedForRequest}`; `&${availableLineItemsFormattedForRequest}`;
const lineItemServerData = context.getters.order.lineItems.serverData; const lineItemServerData = context.getters.order.lineItems.serverData;
if (lineItemServerData) { if (lineItemServerData) {
queryString += `&ServerData=${encodeURIComponent(lineItemServerData)}`; queryString += `&ServerData=${encodeURIComponent(lineItemServerData)}`;
} }
@ -2040,3 +2060,117 @@ function convertGlassPieceToBackEndCompatibleFormat(glassPieces) {
}; };
}); });
} }
// This function will verify schedule info is still valid.
// check to see if we have an appointment date on the order object.
// if so, make sure it's not in the past. if in the past, clear schedule info in store.
// if date not in past, then call schedule service to verify appointment is still available.
async function validateAppointment(context, order) {
if (!order.schedule?.date) {
return;
}
// Date string with slashes is parsed as local time, not UTC. Our date has dashes, '-'.
// If you put any kind of time stamp on the date string with dashes, then it IS parsed as local time.
var aptDate = new Date(order.schedule.date + "T00:00:00");
var curDate = new Date();
// if appointment date is in the past, clear schedule
if (aptDate.getTime() < curDate.getTime()) {
context.commit(storeMutations.RESET_SCHEDULE);
return;
}
// create date range to pass to the schedule service to see if our appointment is still available.
var endRange = new Date(aptDate);
endRange.setDate(aptDate.getDate() + 1);
var endDay = "" + endRange.getDate();
var endMonth = "" + (endRange.getMonth() + 1); // 0 based so add 1
const endYear = endRange.getFullYear();
if (endMonth.length < 2) {
endMonth = "0" + endMonth;
}
if (endDay.length < 2) {
endDay = "0" + endDay;
}
const endDate = [endYear, endMonth, endDay].join("-");
let newTimeSlotsResponse;
if (order.serviceLocation.appointmentType === AppointmentTypeStrings.MOBILE) {
newTimeSlotsResponse = await context.dispatch(
storeActions.GET_MOBILE_TIME_SLOTS,
{
startDate: order.schedule.date,
endDate: endDate,
},
false
);
if (newTimeSlotsResponse?.data.days?.length === 0) {
context.commit(storeMutations.RESET_SCHEDULE);
return;
}
var mobileRouteCodeFound = false;
// if early bird fee is in supporting items then need to check the timeslot to see if offer premium is also still available
if (
order.lineItem?.supportingItems?.findIndex(
(item) => item.partNumber == PREMIUM_FEE_PART_TYPE
)
) {
newTimeSlotsResponse.data.days?.forEach((day) => {
day.timeSlots.forEach((ts) => {
if (ts.id === order.schedule.routeCode && ts.offerPremium) {
mobileRouteCodeFound = true;
}
});
});
} else {
newTimeSlotsResponse.data.days?.forEach((day) => {
day.timeSlots.forEach((ts) => {
if (ts.id === order.schedule.routeCode) {
mobileRouteCodeFound = true;
}
});
});
}
if (!mobileRouteCodeFound) {
context.commit(storeMutations.RESET_SCHEDULE);
return;
}
} else {
newTimeSlotsResponse = await context.dispatch(
storeActions.GET_SHOP_TIME_SLOTS,
{
startDate: order.schedule.date,
endDate: endDate,
shopAppointmentType: order.serviceLocation.appointmentType,
providerNumber: order.serviceLocation.provider.providerNumber,
},
false
);
if (newTimeSlotsResponse?.data.days?.length === 0) {
context.commit(storeMutations.RESET_SCHEDULE);
return;
}
var routeCodeFound = false;
newTimeSlotsResponse.data.days?.forEach((day) => {
day.timeSlots.forEach((ts) => {
if (ts.id === order.schedule.routeCode) {
routeCodeFound = true;
}
});
});
if (!routeCodeFound) {
context.commit(storeMutations.RESET_SCHEDULE);
return;
}
}
}

View file

@ -15,7 +15,6 @@ describe("alert.vue", () => {
}, },
}) })
); );
const wrapperDiv = wrapper.find("div"); const wrapperDiv = wrapper.find("div");
// Assert // Assert
@ -49,7 +48,7 @@ describe("alert.vue", () => {
expect(wrapper.vm.alertCopy).toBe("testCopy"); expect(wrapper.vm.alertCopy).toBe("testCopy");
}); });
it("Should container a <router-link> tag if the manualCopy contains a {routerLink: testName, testLink} placeholder", () => { it("Should container a <textBlock> tag if the manualCopy contains a {routerLink: testName, testLink} placeholder", () => {
// Arrange & Act // Arrange & Act
const wrapper = shallowMount( const wrapper = shallowMount(
alert, alert,
@ -58,11 +57,12 @@ describe("alert.vue", () => {
manualHeadline: "testHeader", manualHeadline: "testHeader",
manualCopy: "testCopy with a {routerLink: testName, testLink} inside of it", manualCopy: "testCopy with a {routerLink: testName, testLink} inside of it",
}, },
stubs: ["router-link"], stubs: ["textBlock"],
}) })
); );
// Assert // Assert
expect(wrapper.find("router-link").exists()).toBe(true); expect(wrapper.html()).toEqual(expect.stringContaining("text-block-stub"));
}); });
it("Should contain 'n+1' <p> tags if the body copy has 'n' <p> tags", () => { it("Should contain 'n+1' <p> tags if the body copy has 'n' <p> tags", () => {

View file

@ -14,17 +14,11 @@
v-if="!doesCopyContainRouterLink(paragraph) && !doesCopyContainTextLink(paragraph)" v-if="!doesCopyContainRouterLink(paragraph) && !doesCopyContainTextLink(paragraph)"
v-html="paragraph"></p> v-html="paragraph"></p>
<p class="m-0 text-body small" v-else> <p class="m-0 text-body small" v-else>
<template v-for="copy in splitCopyOnCMSPlaceHolder(paragraph)" :key="copy"> <template v-if="doesCopyContainRouterLink(paragraph)">
<span v-if="doesCopyContainRouterLink(copy)"> <textBlock :customText="paragraph" class="mb-1" marginTopSizeOverride="0" />
<router-link </template>
:to="{ <template v-else v-for="copy in splitCopyOnCMSPlaceHolder(paragraph)" :key="copy">
query: { [pageQueryString]: `${getRouterLinkRouteFromCopy(copy)}` }, <span v-if="doesCopyContainTextLink(copy)">
name: 'root',
}"
>{{ getRouterLinkDisplayTextFromCopy(copy) }}</router-link
>
</span>
<span v-else-if="doesCopyContainTextLink(copy)">
<textLink <textLink
linkType="text" linkType="text"
:text="getRouterLinkDisplayTextFromCopy(copy)" :text="getRouterLinkDisplayTextFromCopy(copy)"
@ -59,6 +53,7 @@ import {
} from "@/helpers/cms-content-helper"; } from "@/helpers/cms-content-helper";
import textLink from "@/ux-components/text-link/text-link"; import textLink from "@/ux-components/text-link/text-link";
import { applicationConfig } from "@/constants/application-config"; import { applicationConfig } from "@/constants/application-config";
import textBlock from "@/digital-components/text-block/text-block";
export default { export default {
name: "alert", name: "alert",
@ -136,6 +131,7 @@ export default {
}, },
components: { components: {
textLink, textLink,
textBlock,
}, },
}; };
</script> </script>