Merge branch 'develop' into feature/CASH-1123

This commit is contained in:
kpatel8hs4io 2025-09-26 10:26:48 -04:00 committed by GitHub
commit 12baa76a49
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 634 additions and 539 deletions

View file

@ -110,18 +110,6 @@ stages:
clearFolder: true clearFolder: true
deployFolder: "" deployFolder: ""
region: us-east-2 region: us-east-2
appDeployVariables:
__VUE_APP_CONSUMER_CF_DISTRO__: $(__VUE_APP_CONSUMER_CF_DISTRO__)
__VUE_APP_GOOGLE_PLACES_API_KEY__: $(__VUE_APP_GOOGLE_PLACES_API_KEY__)
__VUE_APP_HERITAGE_FUNNEL__: $(__VUE_APP_HERITAGE_FUNNEL__)
__VUE_APP_SAFELITE_HOP__: $(__VUE_APP_SAFELITE_HOP__)
__VUE_APP_CURRENT_ENVIRONMENT__: $(__VUE_APP_CURRENT_ENVIRONMENT__)
__VUE_APP_MY_ACCOUNT__: $(__VUE_APP_MY_ACCOUNT__)
__VUE_APP_SESSION_TIMEOUT_MINUTES__: $(__VUE_APP_SESSION_TIMEOUT_MINUTES__)
indexDeployVariables:
__VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY__: $(__VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY__)
__VUE_APP_GOOGLE_TAG_MANAGER_NOSCRIPT_FRAME_SRC__: $(__VUE_APP_GOOGLE_TAG_MANAGER_NOSCRIPT_FRAME_SRC__)
__VUE_APP_SOLARWINDS_MONITORING_SCRIPT__: $(__VUE_APP_SOLARWINDS_MONITORING_SCRIPT__)
- template: templates/digital/invalidate-cloudfront-cache.yml@AzureDevOps - template: templates/digital/invalidate-cloudfront-cache.yml@AzureDevOps
parameters: parameters:
awsCliContainer: awscli awsCliContainer: awscli
@ -171,18 +159,6 @@ stages:
clearFolder: true clearFolder: true
deployFolder: "" deployFolder: ""
region: us-east-2 region: us-east-2
appDeployVariables:
__VUE_APP_CONSUMER_CF_DISTRO__: $(__VUE_APP_CONSUMER_CF_DISTRO__)
__VUE_APP_GOOGLE_PLACES_API_KEY__: $(__VUE_APP_GOOGLE_PLACES_API_KEY__)
__VUE_APP_HERITAGE_FUNNEL__: $(__VUE_APP_HERITAGE_FUNNEL__)
__VUE_APP_SAFELITE_HOP__: $(__VUE_APP_SAFELITE_HOP__)
__VUE_APP_CURRENT_ENVIRONMENT__: $(__VUE_APP_CURRENT_ENVIRONMENT__)
__VUE_APP_MY_ACCOUNT__: $(__VUE_APP_MY_ACCOUNT__)
__VUE_APP_SESSION_TIMEOUT_MINUTES__: $(__VUE_APP_SESSION_TIMEOUT_MINUTES__)
indexDeployVariables:
__VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY__: $(__VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY__)
__VUE_APP_GOOGLE_TAG_MANAGER_NOSCRIPT_FRAME_SRC__: $(__VUE_APP_GOOGLE_TAG_MANAGER_NOSCRIPT_FRAME_SRC__)
__VUE_APP_SOLARWINDS_MONITORING_SCRIPT__: $(__VUE_APP_SOLARWINDS_MONITORING_SCRIPT__)
- template: templates/digital/invalidate-cloudfront-cache.yml@AzureDevOps - template: templates/digital/invalidate-cloudfront-cache.yml@AzureDevOps
parameters: parameters:
awsCliContainer: awscli awsCliContainer: awscli
@ -232,18 +208,6 @@ stages:
clearFolder: true clearFolder: true
deployFolder: "" deployFolder: ""
region: us-east-2 region: us-east-2
appDeployVariables:
__VUE_APP_CONSUMER_CF_DISTRO__: $(__VUE_APP_CONSUMER_CF_DISTRO__)
__VUE_APP_GOOGLE_PLACES_API_KEY__: $(__VUE_APP_GOOGLE_PLACES_API_KEY__)
__VUE_APP_HERITAGE_FUNNEL__: $(__VUE_APP_HERITAGE_FUNNEL__)
__VUE_APP_SAFELITE_HOP__: $(__VUE_APP_SAFELITE_HOP__)
__VUE_APP_CURRENT_ENVIRONMENT__: $(__VUE_APP_CURRENT_ENVIRONMENT__)
__VUE_APP_MY_ACCOUNT__: $(__VUE_APP_MY_ACCOUNT__)
__VUE_APP_SESSION_TIMEOUT_MINUTES__: $(__VUE_APP_SESSION_TIMEOUT_MINUTES__)
indexDeployVariables:
__VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY__: $(__VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY__)
__VUE_APP_GOOGLE_TAG_MANAGER_NOSCRIPT_FRAME_SRC__: $(__VUE_APP_GOOGLE_TAG_MANAGER_NOSCRIPT_FRAME_SRC__)
__VUE_APP_SOLARWINDS_MONITORING_SCRIPT__: $(__VUE_APP_SOLARWINDS_MONITORING_SCRIPT__)
- template: templates/digital/invalidate-cloudfront-cache.yml@AzureDevOps - template: templates/digital/invalidate-cloudfront-cache.yml@AzureDevOps
parameters: parameters:
awsCliContainer: awscli awsCliContainer: awscli
@ -293,18 +257,6 @@ stages:
clearFolder: true clearFolder: true
deployFolder: "" deployFolder: ""
region: us-east-2 region: us-east-2
appDeployVariables:
__VUE_APP_CONSUMER_CF_DISTRO__: $(__VUE_APP_CONSUMER_CF_DISTRO__)
__VUE_APP_GOOGLE_PLACES_API_KEY__: $(__VUE_APP_GOOGLE_PLACES_API_KEY__)
__VUE_APP_HERITAGE_FUNNEL__: $(__VUE_APP_HERITAGE_FUNNEL__)
__VUE_APP_SAFELITE_HOP__: $(__VUE_APP_SAFELITE_HOP__)
__VUE_APP_CURRENT_ENVIRONMENT__: $(__VUE_APP_CURRENT_ENVIRONMENT__)
__VUE_APP_MY_ACCOUNT__: $(__VUE_APP_MY_ACCOUNT__)
__VUE_APP_SESSION_TIMEOUT_MINUTES__: $(__VUE_APP_SESSION_TIMEOUT_MINUTES__)
indexDeployVariables:
__VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY__: $(__VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY__)
__VUE_APP_GOOGLE_TAG_MANAGER_NOSCRIPT_FRAME_SRC__: $(__VUE_APP_GOOGLE_TAG_MANAGER_NOSCRIPT_FRAME_SRC__)
__VUE_APP_SOLARWINDS_MONITORING_SCRIPT__: $(__VUE_APP_SOLARWINDS_MONITORING_SCRIPT__)
- template: templates/digital/invalidate-cloudfront-cache.yml@AzureDevOps - template: templates/digital/invalidate-cloudfront-cache.yml@AzureDevOps
parameters: parameters:
awsCliContainer: awscli awsCliContainer: awscli

23
environment-variables.js Normal file
View file

@ -0,0 +1,23 @@
// DEFINE every environment variable we will use here,
// with a default value to use in local development and unit tests.
//
// Default values will not be used when we deploy to prod or non-prod.
const environmentVariables = {
['__VUE_APP_CONSUMER_CF_DISTRO__']: "https://digitalapi.dev.safelite.io",
['__VUE_APP_CURRENT_ENVIRONMENT__']: "Localhost",
['__VUE_APP_GOOGLE_PLACES_API_KEY__']: "AIzaSyDptGCkOPgN2uWJOy4ou4M33phRD4MAoJo",
['__VUE_APP_GOOGLE_TAG_MANAGER_NOSCRIPT_FRAME_SRC__']: "https://www.googletagmanager.com/ns.html?id=GTM-M6XCRH&gtm_auth=amlAYNhxUxuskQo7jmjadg&gtm_preview=env-38&gtm_cookies_win=x",
['__VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY__']: "(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl+ '&gtm_auth=amlAYNhxUxuskQo7jmjadg&gtm_preview=env-38&gtm_cookies_win=x';f.parentNode.insertBefore(j,f);})(window,document,'script','dataLayer','GTM-M6XCRH');",
['__VUE_APP_HERITAGE_FUNNEL__']: "https://fixmyglassdev.safelite.com/default.aspx",
['__VUE_APP_MY_ACCOUNT__']: "https://myaccountdev.safelite.com/",
['__VUE_APP_SAFELITE_HOP__']: "https://sv2-safelitehop-dev.safelite.com/fmgCheckoutShared.aspx",
['__VUE_APP_SESSION_TIMEOUT_MINUTES__']: 30,
['__VUE_APP_SOLARWINDS_MONITORING_SCRIPT__']: "",
};
const keysOnly = Object.keys(environmentVariables);
module.exports = {
environmentVariablesWithoutDefaults: keysOnly,
environmentVariablesWithDefaults: environmentVariables,
};

View file

@ -1,3 +1,5 @@
const { environmentVariablesWithDefaults } = require('./environment-variables.js');
module.exports = { module.exports = {
verbose: true, verbose: true,
coverageReporters: ["html", "text", "jest-junit", "cobertura"], reporters: ['default', 'jest-junit'], coverageReporters: ["html", "text", "jest-junit", "cobertura"], reporters: ['default', 'jest-junit'],
@ -49,3 +51,5 @@ module.exports = {
// Uncomment this to avoid the massive amount of warnings we are getting for onSubmit and onInvalidSubmit // Uncomment this to avoid the massive amount of warnings we are getting for onSubmit and onInvalidSubmit
//silent: true, //silent: true,
}; };
process.env = Object.assign(process.env, environmentVariablesWithDefaults);

View file

@ -4,7 +4,7 @@
<script> <script>
window.dataLayer = [{}]; window.dataLayer = [{}];
</script> </script>
<script><%= process.env.VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY %></script> <script><%= process.env.__VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY__ %></script>
<script src="https://applepay.cdn-apple.com/jsapi/v1.1.0/apple-pay-sdk.js"></script> <script src="https://applepay.cdn-apple.com/jsapi/v1.1.0/apple-pay-sdk.js"></script>
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
@ -16,7 +16,7 @@
<!-- Google Tag Manager --> <!-- Google Tag Manager -->
<noscript> <noscript>
<iframe src="<%= process.env.VUE_APP_GOOGLE_TAG_MANAGER_NOSCRIPT_FRAME_SRC %>" <iframe src="<%= process.env.__VUE_APP_GOOGLE_TAG_MANAGER_NOSCRIPT_FRAME_SRC__ %>"
height="0" width="0" style="display:none;visibility:hidden"></iframe> height="0" width="0" style="display:none;visibility:hidden"></iframe>
</noscript> </noscript>
@ -27,6 +27,6 @@
<div id="app"></div> <div id="app"></div>
<!-- built files will be auto injected --> <!-- built files will be auto injected -->
<%= process.env.VUE_APP_SOLARWINDS_MONITORING_SCRIPT %> <%= process.env.__VUE_APP_SOLARWINDS_MONITORING_SCRIPT__ %>
</body> </body>
</html> </html>

View file

@ -1,12 +1,12 @@
const applicationConfig = { const applicationConfig = {
CONSUMER_CF_DISTRO: process.env.VUE_APP_CONSUMER_CF_DISTRO, CONSUMER_CF_DISTRO: process.env.__VUE_APP_CONSUMER_CF_DISTRO__,
GOOGLE_PLACES_API_KEY: process.env.VUE_APP_GOOGLE_PLACES_API_KEY, GOOGLE_PLACES_API_KEY: process.env.__VUE_APP_GOOGLE_PLACES_API_KEY__,
ANALYTICS_SESSION_TIMEOUT_MINUTES: 30, ANALYTICS_SESSION_TIMEOUT_MINUTES: 30,
SAVED_SESSION_TIMEOUT_DAYS: 45, SAVED_SESSION_TIMEOUT_DAYS: 45,
COOKIE_PATH: "/", COOKIE_PATH: "/",
CURRENT_ENVIRONMENT: process.env.VUE_APP_CURRENT_ENVIRONMENT, // "Localhost", "Dev", "QA", and "Prod" CURRENT_ENVIRONMENT: process.env.__VUE_APP_CURRENT_ENVIRONMENT__, // "Localhost", "Dev", "QA", and "Prod"
HERITAGE_FUNNEL: process.env.VUE_APP_HERITAGE_FUNNEL, HERITAGE_FUNNEL: process.env.__VUE_APP_HERITAGE_FUNNEL__,
SAFELITE_HOP: process.env.VUE_APP_SAFELITE_HOP, SAFELITE_HOP: process.env.__VUE_APP_SAFELITE_HOP__,
APPLICATION_NAME: "FixMyGlass", APPLICATION_NAME: "FixMyGlass",
ANALYTICS_APPLICATION_NAME: "FixMyGlassNextGen", ANALYTICS_APPLICATION_NAME: "FixMyGlassNextGen",
APPLICATION_ABBREVIATION: "fmg", APPLICATION_ABBREVIATION: "fmg",
@ -14,7 +14,7 @@ const applicationConfig = {
SITE_ENTRY_TRIGGER_VALUE: "FixMyGlass", SITE_ENTRY_TRIGGER_VALUE: "FixMyGlass",
CASH_PARENT_ACCOUNT_NUMBER: 167132, CASH_PARENT_ACCOUNT_NUMBER: 167132,
CASH_DEFAULT_BILL_TO_ACCOUNT_NUMBER: "87291", CASH_DEFAULT_BILL_TO_ACCOUNT_NUMBER: "87291",
MY_ACCOUNT: process.env.VUE_APP_MY_ACCOUNT, MY_ACCOUNT: process.env.__VUE_APP_MY_ACCOUNT__,
PIA_RESPONSE_URL: PIA_RESPONSE_URL:
location.protocol + "//" + location.host + "/fmg/payment-pia-return?src=concept-funnel", location.protocol + "//" + location.host + "/fmg/payment-pia-return?src=concept-funnel",
PIA_CANCEL_URL: PIA_CANCEL_URL:
@ -29,7 +29,7 @@ const applicationConfig = {
FRONTEND_LOGGER_PATH: "/analytics/api/v1/logging", FRONTEND_LOGGER_PATH: "/analytics/api/v1/logging",
AFFILIATE_COOKIE_CONTAINING_NAME: "_Track", AFFILIATE_COOKIE_CONTAINING_NAME: "_Track",
COOKIE_NAME_LENGTH_MAX_LIMIT: 44, COOKIE_NAME_LENGTH_MAX_LIMIT: 44,
SESSION_TIMEOUT_MINUTES: process.env.VUE_APP_SESSION_TIMEOUT_MINUTES, SESSION_TIMEOUT_MINUTES: process.env.__VUE_APP_SESSION_TIMEOUT_MINUTES__,
RETURN_USER_PAGE: "/fmg/return-user", RETURN_USER_PAGE: "/fmg/return-user",
}; };

View file

@ -2,16 +2,6 @@
<div <div
class="date-picker text-center" class="date-picker text-center"
:class="`${calendarViewDirection} calendar-2025 ${showPricingByDayClass}`"> :class="`${calendarViewDirection} calendar-2025 ${showPricingByDayClass}`">
<div class="date-picker-header">
<funnelSubHeader cmsWidgetName="DatePickerSubHeaderWidget" class="mt-5" />
<textBlock
v-show="durationTextBlockCopy"
:customText="durationTextBlockCopy"
typeStyle="small"
marginTopSizeOverride="0"
class="duration-text-block" />
</div>
<fieldset id="date-picker-fieldset" ref="datePickerFieldset"> <fieldset id="date-picker-fieldset" ref="datePickerFieldset">
<legend class="sr-only">Select a day and time</legend> <legend class="sr-only">Select a day and time</legend>
<div <div
@ -139,36 +129,11 @@
@click="showAnotherMonth"> @click="showAnotherMonth">
View more dates View more dates
</button> </button>
<timeSlotQuestion
ref="timeSlotModalQuestion"
v-model="selectedTimeSlotInfo"
cmsWidgetName="TimeSlotModalQuestion"
waitListLabelWidget="WaitListLabelWidget"
waitListQuestionWidget="WaitListQuestionWidget"
mobilePremiumCmsWidgetName="MobilePremiumTimeSlotModal"
mobileCmsWidgetName="MobileTimeSlotModal"
dropoffCmsWidgetName="DropOffTimeSlotModal"
sameDayDropOffCmsWidgetName="SameDayDropOffTimeSlotModal"
overnightDropOffCmsWidgetName="OvernightDropOffTimeSlotModal"
dropOffOrPickATimeQuestionCmsWidgetName="DropOffOrPickATimeQuestionWidget"
:selectedDate="selectedDate"
:appointmentType="appointmentType"
:premiumAppointmentFee="premiumAppointmentFee"
:displayWaitList="displayWaitList"
:timeSlotsForSelectedDate="timeSlotsForSelectedDate"
:estimatedServiceMinutesMinimum="estimatedServiceMinutesMinimum"
:estimatedServiceMinutesMaximum="estimatedServiceMinutesMaximum"
@waitListRequested="handleWaitListRequested"
@time-slot-modal-closed="timeSlotModalClosed" />
</fieldset> </fieldset>
</div> </div>
</template> </template>
<script> <script>
import timeSlotQuestion from "@/layouts/schedule/time-slot-question/time-slot-question.vue";
import textBlock from "@/digital-components/text-block/text-block";
import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header";
// Supporting files // Supporting files
import loader from "@/ux-components/loader/loader"; import loader from "@/ux-components/loader/loader";
import store from "@/store"; import store from "@/store";
@ -178,12 +143,6 @@ import {
MONTHS_OF_YEAR, MONTHS_OF_YEAR,
DAYS_OF_WEEK, DAYS_OF_WEEK,
} from "@/digital-components/date-picker/mixins/constants"; } from "@/digital-components/date-picker/mixins/constants";
import {
AppointmentTypeStrings,
RouteCodeFlags,
PREMIUM_FEE_PART_TYPE,
cmsWidgetFieldMappings,
} from "@/constants/schedule-constants";
import { import {
selectableDaysOptions, selectableDaysOptions,
requiredParameter, requiredParameter,
@ -195,7 +154,6 @@ import {
import { useField, ErrorMessage } from "vee-validate"; import { useField, ErrorMessage } from "vee-validate";
import { deepClone } from "@/helpers/object-helper"; import { deepClone } from "@/helpers/object-helper";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
import { getDisplayTextForDurationLength } from "@/helpers/duration-length-helper";
export default { export default {
name: "datePicker", name: "datePicker",
@ -205,10 +163,8 @@ export default {
months: null, months: null,
disableViewMoreDatesButton: false, disableViewMoreDatesButton: false,
hideSomeDaysForInitialView: null, hideSomeDaysForInitialView: null,
selectedTimeSlotInfo: this.getSelectedTimeSlotInfo(),
selectableDatesInshop: [], // NOTE: this and the mobile version below use monthNum (1-based), NOT monthIndex (0-based) selectableDatesInshop: [], // NOTE: this and the mobile version below use monthNum (1-based), NOT monthIndex (0-based)
selectableDatesMobile: [], selectableDatesMobile: [],
durationTextBlockCopyForInshopOrDropoff: null,
}; };
}, },
props: { props: {
@ -242,12 +198,6 @@ export default {
pricingByDayBasePrice: Number, pricingByDayBasePrice: Number,
pricingByDayUpcharge: Number, pricingByDayUpcharge: Number,
isPricingByDayExperiment: Boolean, isPricingByDayExperiment: Boolean,
timeSlotsForSelectedDate: Object,
appointmentType: String,
premiumAppointmentFee: Object,
estimatedServiceMinutesMinimum: Number,
estimatedServiceMinutesMaximum: Number,
displayWaitList: Boolean,
isMobileSelected: Boolean, isMobileSelected: Boolean,
}, },
setup(props) { setup(props) {
@ -309,80 +259,10 @@ export default {
this.dispatchStoreAction(this.storeActions.SAVE_WAITLIST_REQUESTED, false, false); this.dispatchStoreAction(this.storeActions.SAVE_WAITLIST_REQUESTED, false, false);
}, },
}, },
dropOffDurationText() {
return this.getCmsContent("DropOffTimeSlotModal", cmsWidgetFieldMappings.DURATION);
},
sameDayDropoffDurationText() {
return this.getCmsContent(
"SameDayDropOffTimeSlotModal",
cmsWidgetFieldMappings.DURATION
);
},
overnightDropoffDurationText() {
return this.getCmsContent(
"OvernightDropOffTimeSlotModal",
cmsWidgetFieldMappings.DURATION
);
},
inshopDurationText() {
const inshopDurationTextWithoutTime = this.getCmsContent(
"TimeSlotModalQuestion",
cmsWidgetFieldMappings.DURATION
);
const inshopDurationTime = getDisplayTextForDurationLength(
this.estimatedServiceMinutesMinimum,
this.estimatedServiceMinutesMaximum
);
if (this.estimatedServiceMinutesMinimum && this.estimatedServiceMinutesMaximum) {
return `${inshopDurationTextWithoutTime} <b>${inshopDurationTime}</b>`;
}
return null;
},
mobileDurationText() {
const mobileDurationTextWithoutTime = this.getCmsContent(
"TimeSlotModalQuestion",
cmsWidgetFieldMappings.DURATION
);
const mobileDurationTime = getDisplayTextForDurationLength(
this.estimatedServiceMinutesMinimum,
this.estimatedServiceMinutesMaximum
);
if (this.estimatedServiceMinutesMinimum && this.estimatedServiceMinutesMaximum) {
return `${mobileDurationTextWithoutTime} <b>${mobileDurationTime}</b>`;
}
return null;
},
durationTextBlockCopy() {
if (this.appointmentType === AppointmentTypeStrings.MOBILE) {
return this.mobileDurationText;
} else if (this.appointmentType === AppointmentTypeStrings.IN_SHOP) {
return this.inshopDurationText;
} else if (
this.appointmentType === AppointmentTypeStrings.IN_SHOP_OR_DROP_OFF ||
this.appointmentType === AppointmentTypeStrings.DROP_OFF
) {
return this.durationTextBlockCopyForInshopOrDropoff ?? this.inshopDurationText;
}
return null;
},
isSameDay() {
if (!this.timeSlotsForSelectedDate) {
return false;
}
const todaysDate = new Date().toISOString().split("T")[0];
return this.selectedDate === todaysDate;
},
}, },
methods: { methods: {
async initializeComponent(initialData) { async initializeComponent(initialData) {
await this.setCalendarData(initialData); await this.setCalendarData(initialData);
this.$refs.timeSlotModalQuestion.initializeComponent();
}, },
fireDateSelectedEvent(event, date) { fireDateSelectedEvent(event, date) {
// Ignore if arrow key selected radioButton // Ignore if arrow key selected radioButton
@ -949,42 +829,9 @@ export default {
}; };
window.requestAnimationFrame(step); window.requestAnimationFrame(step);
}, },
getSelectedTimeSlotInfo() {
const supportingItems = this.getSupportingItems();
var isPremiumAppointment = false;
if (supportingItems) {
isPremiumAppointment =
!!supportingItems.filter(
(lineItem) => lineItem.partType === PREMIUM_FEE_PART_TYPE
).length > 0;
}
const selectedTimeSlotInfo = {
timeSlot: store.getters.order.schedule,
isPremiumAppointment: isPremiumAppointment,
};
return selectedTimeSlotInfo;
},
getSupportingItems() { getSupportingItems() {
return store.getters.lineItems.supportingItems; return store.getters.lineItems.supportingItems;
}, },
handleWaitListRequested(value) {
this.$emit("waitListRequested", value);
},
getDurationTextBlockCopyForInshopOrDropoff(selectedRouteCode) {
if (selectedRouteCode?.includes(RouteCodeFlags.OVERNIGHT_DROP_OFF)) {
return this.overnightDropoffDurationText;
} else if (selectedRouteCode?.includes(RouteCodeFlags.ALL_DAY_DROP_OFF)) {
if (this.isSameDay) {
return this.sameDayDropoffDurationText;
} else {
return this.dropOffDurationText;
}
}
return this.inshopDurationText;
},
}, },
watch: { watch: {
modelValue(newValue) { modelValue(newValue) {
@ -998,19 +845,10 @@ export default {
this.scrollToElement("date-of-month-error"); this.scrollToElement("date-of-month-error");
} }
}, },
selectedTimeSlotInfo(newValue) {
const routeCode = newValue?.timeSlot?.routeCode;
this.durationTextBlockCopyForInshopOrDropoff =
this.getDurationTextBlockCopyForInshopOrDropoff(routeCode);
this.$emit("TimeSlotSelected", newValue); // needed to update footer button text on Schedule page and to save to Store correctly
},
}, },
components: { components: {
loader, loader,
ErrorMessage, ErrorMessage,
timeSlotQuestion,
textBlock,
funnelSubHeader,
}, },
}; };
</script> </script>
@ -1028,14 +866,6 @@ export default {
align-items: flex-start; align-items: flex-start;
text-align: left; text-align: left;
.duration-text-block {
text-align: left;
}
.date-picker-header {
margin-bottom: 1.5rem;
}
fieldset { fieldset {
flex-grow: 1; flex-grow: 1;
position: relative; position: relative;

View file

@ -43,7 +43,7 @@
]" ]"
:validationRules="validationRules" :validationRules="validationRules"
@change="handleChange" @change="handleChange"
@blur="handleChange" @blur="handleBlur"
:maxlength="maxLength ? maxLength : '999'" :maxlength="maxLength ? maxLength : '999'"
@focus="$emit('focus', $event.target.value)" @focus="$emit('focus', $event.target.value)"
@keydown="keyDownHandler" @keydown="keyDownHandler"

View file

@ -195,6 +195,7 @@ export default {
name: "cart", name: "cart",
props: { props: {
modelValue: Object, modelValue: Object,
inactivePromos: Object,
damage: Object, damage: Object,
servicePackageOptionsCmsName: String, servicePackageOptionsCmsName: String,
availableVaps: Object, availableVaps: Object,
@ -327,15 +328,26 @@ export default {
this.lineItems.vaps = [...lineItems.vaps]; this.lineItems.vaps = [...lineItems.vaps];
this.lineItems.promos = [...lineItems.promos]; this.lineItems.promos = [...lineItems.promos];
await this.dispatchStoreAction(storeActions.SAVE_VAPS, this.lineItems.vaps, false); await this.dispatchStoreAction(storeActions.SAVE_VAPS, this.lineItems.vaps, false);
// any new promos that are also in inactive list?
const newInactivePromos = this.inactivePromos.filter((promo) => {
return this.lineItems.promos.includes(promo);
});
this.updateInactivePromos(newInactivePromos);
await this.dispatchStoreAction( await this.dispatchStoreAction(
storeActions.SAVE_ACTIVE_AND_OR_INACTIVE_PROMOS, storeActions.SAVE_ACTIVE_AND_OR_INACTIVE_PROMOS,
{ {
activePromos: this.lineItems.promos, activePromos: this.lineItems.promos,
inactivePromos: this.inactivePromos,
}, },
false false
); );
}, },
updateInactivePromos(newInactivePromos) {
this.$emit("update:inactivePromos", newInactivePromos);
},
getPromoCodeList() { getPromoCodeList() {
if (this.$refs["promoModalQuestion"]) { if (this.$refs["promoModalQuestion"]) {
return this.$refs["promoModalQuestion"].getPromoCodeList(); return this.$refs["promoModalQuestion"].getPromoCodeList();

View file

@ -42,12 +42,12 @@ export async function navigateToHeritageFunnel({ shouldSaveSession, pageNameToLo
} }
if ( if (
(process.env.VUE_APP_HERITAGE_FUNNEL.includes("fixmyglasstest") && (applicationConfig.HERITAGE_FUNNEL.includes("fixmyglasstest") &&
process.env.VUE_APP_CURRENT_ENVIRONMENT === "Localhost") || applicationConfig.CURRENT_ENVIRONMENT === "Localhost") ||
(process.env.VUE_APP_HERITAGE_FUNNEL.includes("fixmyglassdev") && (applicationConfig.HERITAGE_FUNNEL.includes("fixmyglassdev") &&
process.env.VUE_APP_CURRENT_ENVIRONMENT === "Localhost") || applicationConfig.CURRENT_ENVIRONMENT === "Localhost") ||
(process.env.VUE_APP_HERITAGE_FUNNEL.includes("localhost") && (applicationConfig.HERITAGE_FUNNEL.includes("localhost") &&
process.env.VUE_APP_CURRENT_ENVIRONMENT === "Localhost") applicationConfig.CURRENT_ENVIRONMENT === "Localhost")
) { ) {
heritageParms["forceEndToEndInsurance"] = true; heritageParms["forceEndToEndInsurance"] = true;
} }

View file

@ -275,6 +275,7 @@ export default {
} }
.sub-text strong { .sub-text strong {
text-transform: uppercase; text-transform: uppercase;
font-weight: 400;
} }
} }

View file

@ -6,6 +6,15 @@ import { shallowMount } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js"; import { getMountOptions } from "@/helpers/unit-test-helper.js";
import store from "@/store"; import store from "@/store";
import { experimentSettings } from "@/constants/experiments"; import { experimentSettings } from "@/constants/experiments";
import { storeMutations } from "@/constants/store-mutations";
import globalMethods from "@/global-methods";
globalMethods.callHttpClient = jest.fn();
global.crypto = { randomUUID: jest.fn() };
const mockPriceOrderItemsAndSaveServerData = jest
.fn()
.mockResolvedValue(/* your mock return value */);
// Mock our module for promises. // Mock our module for promises.
jest.mock("@/helpers/layout-helper.js", () => ({ jest.mock("@/helpers/layout-helper.js", () => ({
@ -34,7 +43,6 @@ describe("payment-method.vue", () => {
// Assert // Assert
expect(wrapper.vm.$router.navigateWithoutSaving).toHaveBeenCalled(); expect(wrapper.vm.$router.navigateWithoutSaving).toHaveBeenCalled();
}); });
test("if the continue button is clicked, navigate forward", async () => { test("if the continue button is clicked, navigate forward", async () => {
// Arrange // Arrange
const { wrapper } = setupMocks(); const { wrapper } = setupMocks();
@ -81,7 +89,10 @@ function setupMocks() {
city: "Anytown", city: "Anytown",
state: "OH", state: "OH",
zipCode: "00000", zipCode: "00000",
provider: { providerNumber: "0000567" },
}, },
vehicle: { year: 2020, make: "Toyota", model: "Camry", carId: "12345" },
damage: { isRepair: false },
}, },
policy: { policy: {
currentDeductible: 123, currentDeductible: 123,
@ -101,6 +112,9 @@ function setupMocks() {
store: { store: {
getters: store.getters, getters: store.getters,
}, },
actions: {
priceOrderItemsAndSaveServerData: mockPriceOrderItemsAndSaveServerData,
},
}); });
const mockMixin = { const mockMixin = {

View file

@ -22,6 +22,7 @@
:availableVaps="availableVaps" :availableVaps="availableVaps"
:allowItemRemoval="true" :allowItemRemoval="true"
v-model="lineItems" v-model="lineItems"
v-model:inactivePromos="inactivePromos"
pageName="payment-method" pageName="payment-method"
servicePackageOptionsCmsName="ServicePackageTitle" servicePackageOptionsCmsName="ServicePackageTitle"
recyclingModalCmsWidgetName="RecycleModal" recyclingModalCmsWidgetName="RecycleModal"
@ -263,7 +264,7 @@ export default {
: (resultMap.wipers.filter((wiper) => wiper.partType == partTypeStrings.REAR_WIPER) ?? : (resultMap.wipers.filter((wiper) => wiper.partType == partTypeStrings.REAR_WIPER) ??
[]); []);
const lineItemsToTax = [ const allLineItems = [
resultMap.rainDefense, resultMap.rainDefense,
...supportingItems, ...supportingItems,
...availableFrontWipers, ...availableFrontWipers,
@ -272,6 +273,10 @@ export default {
...vaps, ...vaps,
]; ];
const lineItemsToTax = Array.from(
new Map(allLineItems.map((item) => [item.partNumber, item])).values()
);
const availableVaps = [ const availableVaps = [
resultMap.rainDefense, resultMap.rainDefense,
...availableFrontWipers, ...availableFrontWipers,
@ -542,6 +547,16 @@ export default {
getPromoCodeWithoutBundleIdentifier(x.promoCode) getPromoCodeWithoutBundleIdentifier(x.promoCode)
); );
// SAVE PROMO CHANGES TO STORE
this.dispatchStoreAction(
storeActions.SAVE_ACTIVE_AND_OR_INACTIVE_PROMOS,
{
activePromos: this.lineItems.promos,
inactivePromos: this.inactivePromos,
},
false
);
this.$refs.loadingModal.hideModal(); this.$refs.loadingModal.hideModal();
}, },
getIsRecalAckOptInFromStore() { getIsRecalAckOptInFromStore() {
@ -563,6 +578,7 @@ export default {
this.paymentMethod, this.paymentMethod,
false false
); );
// save lineitems as they now have salestax added // save lineitems as they now have salestax added
await this.dispatchStoreAction( await this.dispatchStoreAction(
storeActions.SAVE_GLASS_PARTS_SUPPRESSING_STATE_RESETTING, storeActions.SAVE_GLASS_PARTS_SUPPRESSING_STATE_RESETTING,
@ -820,7 +836,7 @@ export default {
isAfterpayBreakoutDisplay() { isAfterpayBreakoutDisplay() {
return ( return (
this.showInsuranceCoverageAs !== coverageStatus.PENDING && this.showInsuranceCoverageAs !== coverageStatus.PENDING &&
(!this.isRecalPriceRemove || !this.isRecalibrationOnOrder) && !this.isRecalPriceRemove &&
this.hasSettingEqualTo(experimentSettings.DISPLAY_AFTERPAY_BREAKOUT_DISPLAY, "true") this.hasSettingEqualTo(experimentSettings.DISPLAY_AFTERPAY_BREAKOUT_DISPLAY, "true")
); );
}, },

View file

@ -2,7 +2,9 @@
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm" v-slot="{ meta }"> <Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm" v-slot="{ meta }">
<loadingModal ref="loadingModal" /> <loadingModal ref="loadingModal" />
<funnelHeader cmsWidgetName="FunnelHeaderWidget" ref="funnelHeader" /> <funnelHeader cmsWidgetName="FunnelHeaderWidget" ref="funnelHeader" />
<div class="container quote position-relative"> <div
class="container quote position-relative"
:class="[showSaveProgressPopup ? 'initial-popup-open' : '']">
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-12"> <div class="col-12">
<funnelSubHeader class="mb-5" cmsWidgetName="FunnelSubHeaderWidget" /> <funnelSubHeader class="mb-5" cmsWidgetName="FunnelSubHeaderWidget" />
@ -286,6 +288,7 @@ export default {
const isEmailInStoreOnPageLoad = emailFromStore?.length > 0; const isEmailInStoreOnPageLoad = emailFromStore?.length > 0;
const isPopupSkipped = const isPopupSkipped =
store.getters.pageData(routeData.QUOTE.name)?.saveProgressPopupSkipped === true; store.getters.pageData(routeData.QUOTE.name)?.saveProgressPopupSkipped === true;
// "popup" is the automatic opening centered popup on load
const showSaveProgressPopup = !(isEmailInStoreOnPageLoad || isPopupSkipped); const showSaveProgressPopup = !(isEmailInStoreOnPageLoad || isPopupSkipped);
const showSaveProgressModal = isEmailInStoreOnPageLoad ? false : true; const showSaveProgressModal = isEmailInStoreOnPageLoad ? false : true;
const shouldSkipToInsurance = const shouldSkipToInsurance =
@ -555,8 +558,8 @@ export default {
servicePackage: null, servicePackage: null,
holdParentAccountNumber: null, holdParentAccountNumber: null,
holdBillToAccountNumber: null, holdBillToAccountNumber: null,
showSaveProgressPopup: null, showSaveProgressPopup: null, // "popup" is the automatic opening centered popup on load
showSaveProgressModal: null, showSaveProgressModal: null, // "modal" is the user-triggered slide-in drawer
isSaveProgressComplete: null, isSaveProgressComplete: null,
packageNumber: null, packageNumber: null,
skipToInsurance: null, skipToInsurance: null,
@ -899,6 +902,10 @@ export default {
<style lang="scss" scoped> <style lang="scss" scoped>
// Unique to quote page for 2.0/Heritage parity // Unique to quote page for 2.0/Heritage parity
// prettier-ignore
.prevent-modal-scroll.modal-open .container.quote.initial-popup-open .row:not(.save-progress-popup) {
display: none;
}
.quote { .quote {
padding-bottom: 2rem; padding-bottom: 2rem;

View file

@ -246,7 +246,7 @@ export default {
}, },
isAfterpayBreakoutDisplay() { isAfterpayBreakoutDisplay() {
return ( return (
(!this.shouldHideRecalibration || !this.isRecalibrationOnOrder) && !this.shouldHideRecalibration &&
experimentMixin.methods.hasSettingEqualTo( experimentMixin.methods.hasSettingEqualTo(
experimentSettings.DISPLAY_AFTERPAY_BREAKOUT_DISPLAY, experimentSettings.DISPLAY_AFTERPAY_BREAKOUT_DISPLAY,
"true" "true"

View file

@ -0,0 +1,114 @@
<template>
<textBlock
:customText="durationTextBlockCopy"
typeStyle="small"
marginTopSizeOverride="0"
class="duration-text-block" />
</template>
<script>
// Components
import textBlock from "@/digital-components/text-block/text-block";
// Constants
import { AppointmentTypeStrings, cmsWidgetFieldMappings } from "@/constants/schedule-constants";
// Helpers
import { getDisplayTextForDurationLength } from "@/helpers/duration-length-helper";
export default {
name: "duration-text-block",
props: {
isSameDay: Boolean,
appointmentType: String,
estimatedServiceMinutesMinimum: String,
estimatedServiceMinutesMaximum: String,
isOvernightDropoff: Boolean,
},
data() {
return {
selectedRouteCode: null,
selectedAnswerForDropOffOrInshop: null,
selectedAnswerForTimeSlots: null,
};
},
computed: {
dropOffDurationText() {
return this.getCmsContent("DropOffTimeSlotModal", cmsWidgetFieldMappings.DURATION);
},
sameDayDropoffDurationText() {
return this.getCmsContent(
"SameDayDropOffTimeSlotModal",
cmsWidgetFieldMappings.DURATION
);
},
overnightDropoffDurationText() {
return this.getCmsContent(
"OvernightDropOffTimeSlotModal",
cmsWidgetFieldMappings.DURATION
);
},
inshopDurationText() {
const inshopDurationTextWithoutTime = this.getCmsContent(
"TimeSlotModalQuestion",
cmsWidgetFieldMappings.DURATION
);
const inshopDurationTime = getDisplayTextForDurationLength(
this.estimatedServiceMinutesMinimum,
this.estimatedServiceMinutesMaximum
);
if (this.estimatedServiceMinutesMinimum && this.estimatedServiceMinutesMaximum) {
return `${inshopDurationTextWithoutTime} <b>${inshopDurationTime}</b>`;
}
return null;
},
mobileDurationText() {
const mobileDurationTextWithoutTime = this.getCmsContent(
"TimeSlotModalQuestion",
cmsWidgetFieldMappings.DURATION
);
const mobileDurationTime = getDisplayTextForDurationLength(
this.estimatedServiceMinutesMinimum,
this.estimatedServiceMinutesMaximum
);
if (this.estimatedServiceMinutesMinimum && this.estimatedServiceMinutesMaximum) {
return `${mobileDurationTextWithoutTime} ${mobileDurationTime}`;
}
return null;
},
durationTextBlockCopy() {
if (this.appointmentType === AppointmentTypeStrings.MOBILE) {
return this.mobileDurationText;
} else if (this.appointmentType === AppointmentTypeStrings.DROP_OFF) {
return this.getDurationTextBlockCopyForDropoff();
} else {
return this.inshopDurationText;
}
},
},
methods: {
getDurationTextBlockCopyForDropoff() {
if (this.isOvernightDropoff) {
return this.overnightDropoffDurationText;
} else {
if (this.isSameDay) {
return this.sameDayDropoffDurationText;
} else {
return this.dropOffDurationText;
}
}
},
},
components: {
textBlock,
},
};
</script>
<style></style>

View file

@ -0,0 +1,141 @@
<template>
<div
class="mobile-fee-waiver-alert-container py-3 px-4 my-2"
:class="[isExpanded ? 'expanded' : '']">
<div
class="mobile-fee-waiver-header d-flex align-items-center justify-content-between"
@click="toggleIsExpanded">
<span>
{{ headerText }}
</span>
</div>
<div class="mobile-fee-waiver-body">
<div class="mobile-fee-waiver-copy">
{{ subheaderText }}
</div>
<div class="mobile-fee-waiver-price-comp">
<span class="mobile-fee-waiver-strikethrough-price">
{{ priceStrikethroughText }}
</span>
<span class="mobile-fee-waiver-free-price">{{ freePriceText }}</span>
</div>
</div>
</div>
</template>
<script>
export default {
name: "mobile-fee-waiver-alert",
props: {
cmsWidgetName: String,
},
data() {
return {
isExpanded: true,
};
},
computed: {
headerText() {
return this.getCmsContent(this.cmsWidgetName, "HeaderText");
},
subheaderText() {
return this.getCmsContent(this.cmsWidgetName, "SubheaderText");
},
priceStrikethroughText() {
return this.getCmsContent(this.cmsWidgetName, "FooterText");
},
freePriceText() {
return this.getCmsContent(this.cmsWidgetName, "FooterText2");
},
},
methods: {
toggleIsExpanded() {
this.setIsExpanded(!this.isExpanded);
},
setIsExpanded(newVal) {
this.isExpanded = newVal;
},
},
components: {},
};
</script>
<style lang="scss">
.mobile-fee-waiver-alert-container {
background: $green-100;
border-radius: 0.25em;
border: 1px solid $green-400;
.mobile-fee-waiver-header {
font-weight: bold;
span {
flex-grow: 1;
color: $black;
}
&::before {
content: "";
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3E%3Cpath fill='%230C7E47' d='M8 0a8 8 0 1 0 0 16A8 8 0 0 0 8 0Zm3.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/svg%3E");
width: 1rem;
height: 1rem;
display: inline-block;
margin-right: 0.5rem;
}
&::after {
content: "";
transition: all 0.5s ease;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 9'%3E%3Cpath d='M7.99282 0.117702C7.75716 0.116049 7.53044 0.207797 7.3623 0.37286L0.255553 7.46878C0.0862976 7.63796 -0.00878906 7.86742 -0.00878906 8.10667C-0.00878906 8.34593 0.0862976 8.57539 0.255553 8.74457C0.424808 8.91374 0.654368 9.00879 0.893731 9.00879C1.13309 9.00879 1.36265 8.91374 1.53191 8.74457L7.99282 2.27378L14.4614 8.74457C14.6307 8.91205 14.8595 9.00547 15.0977 9.00428C15.3359 9.00308 15.5638 8.90737 15.7314 8.73819C15.8989 8.56901 15.9924 8.34022 15.9912 8.10216C15.99 7.8641 15.8942 7.63627 15.725 7.46878L8.6259 0.377963C8.45765 0.21088 8.22999 0.117289 7.99282 0.117702Z' fill='%230C7E47'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: center center;
width: 16px;
height: 16px;
display: inline-flex;
position: relative;
}
}
.mobile-fee-waiver-body {
transition: all 500ms ease-out;
visibility: hidden;
overflow: hidden;
max-height: 0;
.mobile-fee-waiver-price-comp {
margin-top: 0.5em;
.mobile-fee-waiver-strikethrough-price {
font-size: smaller;
text-decoration: line-through;
}
.mobile-fee-waiver-free-price {
display: inline-block;
font-weight: bolder;
border-radius: 1em;
background-color: $green-300;
padding: 0 0.5em;
margin-inline-start: 0.5em;
}
}
}
&.expanded {
.mobile-fee-waiver-header {
&::after {
transform: rotate(180deg);
}
}
.mobile-fee-waiver-body {
visibility: visible;
max-height: none;
border-top: 1px solid $green-400;
padding-top: 0.5em;
margin-top: 0.5em;
}
}
}
</style>

View file

@ -5,17 +5,16 @@
<div class="container page-container-grouped-styles"> <div class="container page-container-grouped-styles">
<div class="row"> <div class="row">
<div class="col-12 col-md-10 col-lg-8 col-xl-7"> <div class="col-12 col-md-10 col-lg-8 col-xl-7">
<alert <mobileFeeWaiverAlert
ref="alertMobileFeeFree" cmsWidgetName="MobileFeeFreeDropdownWidget"
class="mb-5" v-if="showMobileFreeAlert" />
cmsWidgetName="AlertMobileFeeFreeWidget"
v-if="showMobileFreeAlert"
alertClass="alert-success" />
</div> </div>
</div> </div>
<div class="row appointment-type"> <div class="row appointment-type">
<div class="col-12 col-md-10 col-lg-8 col-xl-7"> <div class="col-12 col-md-10 col-lg-8 col-xl-7">
<funnelSubHeader cmsWidgetName="ScheduleYourServiceWidget" class="mb-5" /> <funnelSubHeader
cmsWidgetName="ScheduleYourServiceWidget"
id="schedule-your-service" />
<appointmentTypeQuestion <appointmentTypeQuestion
v-model="appointmentTypeFromAppointmentTypeQuestion" v-model="appointmentTypeFromAppointmentTypeQuestion"
v-show="isAppointmentTypeDisplayed" v-show="isAppointmentTypeDisplayed"
@ -118,6 +117,15 @@
<div class="row date-picker-wrapper"> <div class="row date-picker-wrapper">
<div class="col-12 col-md-10 col-lg-8 col-xl-7"> <div class="col-12 col-md-10 col-lg-8 col-xl-7">
<locationAlerts cmsWidgetPrefix="LocationAlert-" ref="locationAlerts" /> <locationAlerts cmsWidgetPrefix="LocationAlert-" ref="locationAlerts" />
<div v-show="appointmentType" class="date-picker-header fw-normal mt-5 mb-5">
<textBlock :customText="datePickerHeaderText" typeStyle="button-question" />
<durationTextBlock
:isSameDay="isSameDay"
:appointmentType="appointmentType"
:estimatedServiceMinutesMinimum="estimatedServiceMinutesMinimum"
:estimatedServiceMinutesMaximum="estimatedServiceMinutesMaximum"
:isOvernightDropoff="isOvernightDropoff" />
</div>
<datePicker <datePicker
:currentZip="zipCode" :currentZip="zipCode"
:currentProviderNumber="selectedProvider?.providerNumber" :currentProviderNumber="selectedProvider?.providerNumber"
@ -134,14 +142,25 @@
:pricingByDayBasePrice="pricingByDayBasePrice" :pricingByDayBasePrice="pricingByDayBasePrice"
:pricingByDayUpcharge="pricingByDayUpcharge" :pricingByDayUpcharge="pricingByDayUpcharge"
:showPricingByDay="showPricingByDay" :showPricingByDay="showPricingByDay"
:isPricingByDayExperiment="isPricingByDayExperiment" :isPricingByDayExperiment="isPricingByDayExperiment" />
:timeSlotsForSelectedDate="timeSlotsForSelectedDate" <timeSlotQuestion
ref="timeSlotModalQuestion"
v-if="showTimeSlotQuestion"
@time-slot-selection-changed="updateTimeSlot"
waitListLabelWidget="WaitListLabelWidget"
waitListQuestionWidget="WaitListQuestionWidget"
mobilePremiumCmsWidgetName="MobilePremiumTimeSlotModal"
mobileCmsWidgetName="MobileTimeSlotModal"
dropoffCmsWidgetName="DropOffTimeSlotModal"
sameDayDropOffCmsWidgetName="SameDayDropOffTimeSlotModal"
overnightDropOffCmsWidgetName="OvernightDropOffTimeSlotModal"
dropOffOrPickATimeQuestionCmsWidgetName="DropOffOrPickATimeQuestionWidget"
:appointmentType="appointmentType" :appointmentType="appointmentType"
:premiumAppointmentFee="mobilePremiumAppointmentFee" :premiumAppointmentFee="mobilePremiumAppointmentFee"
:estimatedServiceMinutesMinimum="getServiceMinutesMin"
:estimatedServiceMinutesMaximum="getServiceMinutesMax"
@timeSlotSelected="updateTimeSlot"
:displayWaitList="displayWaitList" :displayWaitList="displayWaitList"
:timeSlotsForSelectedDate="timeSlotsForSelectedDate"
:isSameDay="isSameDay"
:selectedRouteCodeData="selectedRouteCodeData"
@waitListRequested="handleWaitListRequested" /> @waitListRequested="handleWaitListRequested" />
<navbar <navbar
cmsWidgetName="FunnelFooterWidget" cmsWidgetName="FunnelFooterWidget"
@ -160,9 +179,14 @@
import alert from "@/ux-components/alert/alert"; import alert from "@/ux-components/alert/alert";
import serviceZipModalQuestion from "@/layouts/service-location/service-zip-modal-question/service-zip-modal-question"; import serviceZipModalQuestion from "@/layouts/service-location/service-zip-modal-question/service-zip-modal-question";
import appointmentTypeQuestion from "@/layouts/service-location/appointment-type-question/appointment-type-question"; import appointmentTypeQuestion from "@/layouts/service-location/appointment-type-question/appointment-type-question";
import shopQuestion from "@/layouts/service-location/shop-question/shop-question";
import shopListButton from "@/layouts/service-location/shop-question/shop-list-button/shop-list-button"; import shopListButton from "@/layouts/service-location/shop-question/shop-list-button/shop-list-button";
import shopLocation from "@/layouts/schedule/shop-location/shop-location"; import shopLocation from "@/layouts/schedule/shop-location/shop-location";
import timeSlotQuestion from "@/layouts/schedule/time-slot-question/time-slot-question.vue";
import durationTextBlock from "@/layouts/schedule/duration-text-block/duration-text-block.vue";
import shopQuestion from "@/layouts/service-location/shop-question/shop-question";
import shopQuestionPopup from "@/layouts/service-location/shop-question/shop-question-popup";
import buttonQuestion from "@/digital-components/button-question/button-question.vue";
import funnelHeader from "@/fmg-components/funnel-header/funnel-header"; import funnelHeader from "@/fmg-components/funnel-header/funnel-header";
import navbar from "@/fmg-components/nav-bar/nav-bar"; import navbar from "@/fmg-components/nav-bar/nav-bar";
@ -174,6 +198,7 @@ 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 contentGroupModal from "@/fmg-components/content-group-modal/content-group-modal"; import contentGroupModal from "@/fmg-components/content-group-modal/content-group-modal";
import textBlock from "@/digital-components/text-block/text-block"; import textBlock from "@/digital-components/text-block/text-block";
import mobileFeeWaiverAlert from "./mobile-fee-waiver-alert/mobile-fee-waiver-alert.vue";
// Supporting files // Supporting files
import baseMixin from "@/mixins/base-mixin.js"; import baseMixin from "@/mixins/base-mixin.js";
@ -497,14 +522,14 @@ export default {
); );
const serviceZipCode = store.getters.order.serviceLocation.zipCode; const serviceZipCode = store.getters.order.serviceLocation.zipCode;
const zipCodeDataPromise = getZipCodeData(serviceZipCode); const zipCodeDataPromise = getZipCodeData(serviceZipCode, to.name);
const serviceabilityDetailsPromise = getServiceabilityDetails( const serviceabilityDetailsPromise = getServiceabilityDetails(
serviceZipCode, serviceZipCode,
null, null,
"service-location" to.name
); );
const mobileFeePartPromise = getPricedMobileFeePart(serviceZipCode, "service-location"); const mobileFeePartPromise = getPricedMobileFeePart(serviceZipCode, to.name);
const shopProviderData = await getShopProviderData(serviceZipCode); const shopProviderData = await getShopProviderData(serviceZipCode, to.name);
const providerNumber = shopProviderData?.data?.shopProviders[0]?.providerNumber; const providerNumber = shopProviderData?.data?.shopProviders[0]?.providerNumber;
// Get pricingByDayUpcharge needed for Pricing By Day // Get pricingByDayUpcharge needed for Pricing By Day
@ -515,7 +540,7 @@ export default {
const premiumFeePromise = baseMixin.methods.dispatchStoreActionWithLogging( const premiumFeePromise = baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.GET_MOBILE_PREMIUM_FEE, storeActions.GET_MOBILE_PREMIUM_FEE,
null, null,
"schedule" to.name
); );
const premiumFeeWithPricePromise = premiumFeePromise.then((result) => { const premiumFeeWithPricePromise = premiumFeePromise.then((result) => {
if (result.data) { if (result.data) {
@ -524,7 +549,7 @@ export default {
{ {
availableLineItems: [result.data], availableLineItems: [result.data],
}, },
"schedule", to.name,
false false
); );
} else { } else {
@ -626,6 +651,16 @@ export default {
return this.isGlassServiceableMobile; return this.isGlassServiceableMobile;
} }
}, },
showTimeSlotQuestion() {
if (
this.selectedDate &&
this.selectedDate.includes("mobile") &&
!this.isMobileSelected
) {
this.getNewSelectedDate();
}
return this.selectedDate && this.appointmentTypeFromAppointmentTypeQuestion;
},
isMobileStaticRecalibrationApplicable() { isMobileStaticRecalibrationApplicable() {
return ( return (
this.displayMSR && this.displayMSR &&
@ -785,6 +820,21 @@ export default {
} }
return []; return [];
}, },
selectedRouteCodeData() {
if (!this.selectedTimeSlotInfo?.timeSlot?.routeCode) {
return null;
}
return {
routeCode: this.selectedTimeSlotInfo.timeSlot.routeCode,
isPremiumAppointment: this.selectedTimeSlotInfo.isPremiumAppointment,
};
},
questionText() {
return this.getCmsContent("YourSafeliteShopWidget", "QuestionText");
},
datePickerHeaderText() {
return this.getCmsContent("DatePickerSubHeaderWidget", "HeaderText");
},
appointmentTypeStrings() { appointmentTypeStrings() {
return AppointmentTypeStrings; return AppointmentTypeStrings;
}, },
@ -795,12 +845,12 @@ export default {
// Splits content when brackets are found in text so that text can be looped through and router-link can be injected when needed // Splits content when brackets are found in text so that text can be looped through and router-link can be injected when needed
return this.splitCopyOnCMSPlaceHolder(this.ChangeShopLinkText); return this.splitCopyOnCMSPlaceHolder(this.ChangeShopLinkText);
}, },
getServiceMinutesMin() { estimatedServiceMinutesMinimum() {
return this.isMobileSelected return this.isMobileSelected
? this.selectableDatesMobile.estimatedServiceMinutesMinimum ? this.selectableDatesMobile.estimatedServiceMinutesMinimum
: this.selectableDatesInshop.estimatedServiceMinutesMinimum; : this.selectableDatesInshop.estimatedServiceMinutesMinimum;
}, },
getServiceMinutesMax() { estimatedServiceMinutesMaximum() {
return this.isMobileSelected return this.isMobileSelected
? this.selectableDatesMobile.estimatedServiceMinutesMaximum ? this.selectableDatesMobile.estimatedServiceMinutesMaximum
: this.selectableDatesInshop.estimatedServiceMinutesMaximum; : this.selectableDatesInshop.estimatedServiceMinutesMaximum;
@ -820,6 +870,18 @@ export default {
); );
} }
}, },
isSameDay() {
const todaysDate = new Date().toISOString().split("T")[0];
return this.selectedDate === todaysDate;
},
isOvernightDropoff() {
return (
this.selectedTimeSlotInfo?.timeSlot?.routeCode &&
this.selectedTimeSlotInfo.timeSlot.routeCode.includes(
RouteCodeFlags.OVERNIGHT_DROP_OFF
)
);
},
}, },
methods: { methods: {
splitCopyOnCMSPlaceHolder, splitCopyOnCMSPlaceHolder,
@ -1305,7 +1367,7 @@ export default {
) { ) {
navigateToHeritageFunnel({ navigateToHeritageFunnel({
shouldSaveSession: false, shouldSaveSession: false,
pageNameToLog: "service-location", pageNameToLog: this.pageName,
navType: "back", navType: "back",
}); });
} else { } else {
@ -1558,22 +1620,46 @@ export default {
} }
}, },
updateTimeSlot(timeSlotObj) { updateTimeSlot(timeSlotObj) {
if (this.preSelectedDate && !timeSlotObj?.timeSlot?.date) return; if (!timeSlotObj?.routeCode) {
this.selectedTimeSlotInfo = timeSlotObj; this.appointmentType = AppointmentTypeStrings.IN_SHOP_OR_DROP_OFF;
this.selectedTimeSlotInfo = {
timeSlot: {
date: null,
routeCode: null,
startTime: null,
endTime: null,
jobMaxMinutes: null,
jobMinMinutes: null,
},
isPremiumAppointment: null,
};
return;
}
const timeSlot = this.timeSlotsForSelectedDate?.timeSlots?.find(
(slot) => slot.id == timeSlotObj.routeCode
);
this.selectedTimeSlotInfo = {
timeSlot: {
date: this.timeSlotsForSelectedDate.date,
routeCode: timeSlotObj.routeCode,
startTime: timeSlot.startTime,
endTime: timeSlot.endTime,
jobMaxMinutes: this.estimatedServiceMinutesMaximum.toString(),
jobMinMinutes: this.estimatedServiceMinutesMinimum.toString(),
},
isPremiumAppointment: timeSlotObj.isPremiumAppointment ? true : false,
};
if ( if (
this.appointmentType !== AppointmentTypeStrings.MOBILE && this.appointmentType !== AppointmentTypeStrings.MOBILE &&
this.appointmentType !== null this.appointmentType !== null
) { ) {
// update apptType based on routeCode to determine if it should be dropoff or inshop // update apptType based on routeCode to determine if it should be dropoff or inshop
debugLog(`Updating appointment type based on route code.`); debugLog(`Updating appointment type based on route code.`);
debugLog(`Route code =`, timeSlotObj.timeSlot.routeCode); debugLog(`Route code =`, timeSlotObj.routeCode);
debugLog( debugLog(`Parsed type = `, this.getInShopOrDropOffApptType(timeSlotObj.routeCode));
`Parsed type = `, this.appointmentType = this.getInShopOrDropOffApptType(timeSlotObj.routeCode);
this.getInShopOrDropOffApptType(timeSlotObj.timeSlot.routeCode)
);
this.appointmentType = this.getInShopOrDropOffApptType(
timeSlotObj.timeSlot.routeCode
);
} }
}, },
getInShopOrDropOffApptType(routeCode) { getInShopOrDropOffApptType(routeCode) {
@ -1607,8 +1693,13 @@ export default {
}, },
updateSelectedProvider(newProvider) { updateSelectedProvider(newProvider) {
if (newProvider) { if (newProvider) {
const didProviderNumberChange =
this.selectedProvider.providerNumber !== newProvider.providerNumber;
this.selectedProvider = newProvider; this.selectedProvider = newProvider;
if (didProviderNumberChange) {
this.initializeDatePicker(); this.initializeDatePicker();
}
} else if (this.appointmentType === this.appointmentTypeStrings.MOBILE) { } else if (this.appointmentType === this.appointmentTypeStrings.MOBILE) {
this.selectedProvider = { this.selectedProvider = {
providerNumber: this.shopProviderData.mobileProviderNumber.toString(), providerNumber: this.shopProviderData.mobileProviderNumber.toString(),
@ -1630,7 +1721,7 @@ export default {
handleZipCodeChange(newZipCode) { handleZipCodeChange(newZipCode) {
this.resetMobileLocation(); this.resetMobileLocation();
getShopProviderData(newZipCode.zipCode).then((result) => { getShopProviderData(newZipCode.zipCode, this.pageName).then((result) => {
this.shopProviderData = result.data; this.shopProviderData = result.data;
this.resetSelectedProvider(); this.resetSelectedProvider();
this.state = newZipCode.state; this.state = newZipCode.state;
@ -1697,6 +1788,9 @@ export default {
this.selectedDate = this.getSelectedDate(); this.selectedDate = this.getSelectedDate();
this.setDisplayWaitList(); this.setDisplayWaitList();
}, },
getNewSelectedDate() {
this.selectedDate = this.getSelectedDate();
},
}, },
watch: { watch: {
appointmentTypeFromAppointmentTypeQuestion: { appointmentTypeFromAppointmentTypeQuestion: {
@ -1740,12 +1834,15 @@ export default {
datePicker, datePicker,
locationAlerts, locationAlerts,
textBlock, textBlock,
timeSlotQuestion,
alert, alert,
serviceZipModalQuestion, serviceZipModalQuestion,
appointmentTypeQuestion, appointmentTypeQuestion,
contentGroupModal, contentGroupModal,
durationTextBlock,
shopLocation, shopLocation,
mobileFeeWaiverAlert,
}, },
}; };
</script> </script>
@ -1780,12 +1877,17 @@ export default {
font-weight: 600; font-weight: 600;
} }
:deep(.funnel-sub-header:not(.mb-5)) { :deep(.funnel-sub-header:not(#schedule-your-service)) {
h5.dark-header { h5.dark-header {
font-weight: 600; font-weight: 600;
font-size: 1rem; font-size: 1rem;
} }
} }
:deep(.funnel-sub-header#schedule-your-service) {
h5.dark-header {
margin-bottom: 0;
}
}
.col-12 a { .col-12 a {
font-family: UrbanistSemibold; font-family: UrbanistSemibold;

View file

@ -3,6 +3,7 @@
<buttonQuestion <buttonQuestion
v-if="isDropOffAppointmentAvailable" v-if="isDropOffAppointmentAvailable"
v-model="selectedAnswerForDropOffOrInshop" v-model="selectedAnswerForDropOffOrInshop"
@update:modelValue="dropOffSelectionChanged"
:questionText="dropOffOrPickATimeQuestionLabelText" :questionText="dropOffOrPickATimeQuestionLabelText"
:questionTextDescription="dropOffOrPickATimeQuestionDescription" :questionTextDescription="dropOffOrPickATimeQuestionDescription"
:answers="answersForDropOffQuestion" :answers="answersForDropOffQuestion"
@ -24,6 +25,7 @@
<buttonQuestion <buttonQuestion
ref="buttonQuestion" ref="buttonQuestion"
v-if="shouldDisplayTimeSlotQuestion" v-if="shouldDisplayTimeSlotQuestion"
@update:modelValue="timeSlotSelectionChanged"
buttonTypeString="timeSlotModalListButton" buttonTypeString="timeSlotModalListButton"
:buttonTypeObject="timeSlotModalListButton" :buttonTypeObject="timeSlotModalListButton"
class="mt-4 timeslots" class="mt-4 timeslots"
@ -44,7 +46,7 @@
v-if="supplementalInformationBlock" v-if="supplementalInformationBlock"
v-html="supplementalInformationBlock" /> v-html="supplementalInformationBlock" />
<div <div
v-if="hasWaitListExperiment && displayWaitListFeature && selectedDate" v-if="hasWaitListExperiment && displayWaitListFeature"
class="mt-5 bg-light rounded waitlist"> class="mt-5 bg-light rounded waitlist">
<textBlock <textBlock
cmsWidgetName="WaitListLabelWidget" cmsWidgetName="WaitListLabelWidget"
@ -59,7 +61,7 @@
@click="waitListChecked" /> @click="waitListChecked" />
</div> </div>
<div <div
v-if="waitListRequested && displayWaitList && selectedDate" v-if="waitListRequested && displayWaitList"
class="rounded waitlist-success" class="rounded waitlist-success"
ref="waitlistSuccessMessage"> ref="waitlistSuccessMessage">
<img :src="waitListSuccessImage" class="success-image" /> <img :src="waitListSuccessImage" class="success-image" />
@ -92,7 +94,6 @@ import {
isDropOffRouteCode, isDropOffRouteCode,
militaryToTwelveHourTime, militaryToTwelveHourTime,
} from "@/layouts/schedule/helpers/schedule-helper"; } from "@/layouts/schedule/helpers/schedule-helper";
import { getDisplayTextForDurationLength } from "@/helpers/duration-length-helper";
// Validation // Validation
import { defineRule, useField } from "vee-validate"; import { defineRule, useField } from "vee-validate";
@ -116,23 +117,8 @@ defineRule("time-slot-required", required(errorMessages.OPTION_REQUIRED));
export default { export default {
name: "time-slot-question", name: "time-slot-question",
emits: ["update:modelValue", "TimeSlotSelected", "click-event"], emits: ["update:modelValue", "time-slot-selection-changed"],
props: { props: {
modelValue: {
type: Object,
default: () => ({
timeSlot: {
routeCode: null,
date: null,
startTime: null,
endTime: null,
jobMaxMinutes: null,
jobMinMinutes: null,
},
isPremiumAppointment: null,
}),
},
cmsWidgetName: String,
mobileCmsWidgetName: String, mobileCmsWidgetName: String,
mobilePremiumCmsWidgetName: String, mobilePremiumCmsWidgetName: String,
dropoffCmsWidgetName: String, dropoffCmsWidgetName: String,
@ -142,21 +128,23 @@ export default {
appointmentType: String, appointmentType: String,
timeSlotsForSelectedDate: Object, timeSlotsForSelectedDate: Object,
premiumAppointmentFee: Object, premiumAppointmentFee: Object,
estimatedServiceMinutesMinimum: Number,
estimatedServiceMinutesMaximum: Number,
validationRules: String, validationRules: String,
customComponentId: String, customComponentId: String,
selectedDate: String, isSameDay: Boolean,
displayWaitList: Boolean, displayWaitList: Boolean,
selectedRouteCodeData: Object,
}, },
data() { data() {
return { return {
selectedRouteCode: null,
timeSlotModalListButton: timeSlotModalListButton, timeSlotModalListButton: timeSlotModalListButton,
selectedAnswerForDropOffOrInshop: null,
selectedAnswerForTimeSlots: null, selectedAnswerForTimeSlots: null,
selectedAnswerForDropOffOrInshop: null,
}; };
}, },
mounted() {
this.updateDropoffAndTimeSlotAnswersFromSelectedRouteCodeData(this.selectedRouteCodeData);
this.autoSelectTimeSlotIfOnlyOneIsAvailable();
},
setup(props) { setup(props) {
const uuid = uuidv4(); const uuid = uuidv4();
const componentId = !props.customComponentId const componentId = !props.customComponentId
@ -186,20 +174,33 @@ export default {
errors, errors,
}; };
}, },
watch: {
waitListRequested(newVal) {
if (newVal) {
this.$nextTick(() => {
this.scrollToSuccessMessage();
});
}
},
selectedRouteCodeData(newValue) {
this.updateDropoffAndTimeSlotAnswersFromSelectedRouteCodeData(newValue);
},
timeSlotsForSelectedDate() {
this.selectedAnswerForDropOffOrInshop = null;
this.selectedAnswerForTimeSlots = null;
this.autoSelectTimeSlotIfOnlyOneIsAvailable();
},
},
computed: { computed: {
supplementalInformationBlock() { supplementalInformationBlock() {
let appointmentTypeCmsWidgetName; let appointmentTypeCmsWidgetName;
if ( if (
this.selectedDate == null ||
this.appointmentType === AppointmentTypeStrings.IN_SHOP_OR_DROP_OFF || this.appointmentType === AppointmentTypeStrings.IN_SHOP_OR_DROP_OFF ||
this.appointmentType === AppointmentTypeStrings.IN_SHOP || this.appointmentType === AppointmentTypeStrings.IN_SHOP ||
this.appointmentType === AppointmentTypeStrings.DROP_OFF || this.appointmentType === AppointmentTypeStrings.DROP_OFF ||
!this.availableTimeSlots !this.availableTimeSlots
) { ) {
if (!this.selectedAnswerForTimeSlots && !this.selectedAnswerForDropOffOrInshop) {
this.autoSelectTimeSlotIfOnlyOneIsAvailable();
}
return null; return null;
// This is planned to be used again for drop-off // This is planned to be used again for drop-off
// Wrote this to be ready for that eventuality, is untested and no HTML work done yet // Wrote this to be ready for that eventuality, is untested and no HTML work done yet
@ -214,11 +215,7 @@ export default {
// ); // );
// } // }
} else { } else {
if (!this.selectedAnswerForTimeSlots && this.selectedRouteCode) { appointmentTypeCmsWidgetName = this.selectedRouteCodeData?.routeCode?.includes(
// Clearing selectedRouteCode if no time slot is selected
this.resetSelectedTimeSlot();
}
appointmentTypeCmsWidgetName = this.selectedRouteCode?.includes(
PREMIUM_TIME_SLOT_ID_FLAG PREMIUM_TIME_SLOT_ID_FLAG
) )
? this.mobilePremiumCmsWidgetName ? this.mobilePremiumCmsWidgetName
@ -299,13 +296,6 @@ export default {
// return null; // return null;
// } // }
// }, // },
isSameDay() {
if (!this.timeSlotsForSelectedDate) {
return false;
}
const todaysDate = new Date().toISOString().split("T")[0];
return this.selectedDate === todaysDate;
},
availableTimeSlots() { availableTimeSlots() {
if (!this.timeSlotsForSelectedDate) { if (!this.timeSlotsForSelectedDate) {
return null; return null;
@ -407,29 +397,60 @@ export default {
}, },
}, },
methods: { methods: {
initializeComponent() { updateDropoffAndTimeSlotAnswersFromSelectedRouteCodeData(selectedRouteCodeData) {
this.setSelectedRouteCodeFromParent(); if (selectedRouteCodeData?.routeCode) {
}, let routeCode = selectedRouteCodeData.routeCode;
async setSelectedTimeSlot() { if (isDropOffRouteCode(routeCode)) {
this.selectedRouteCode = this.selectedAnswerForDropOffOrInshop = routeCode;
this.selectedAnswerForTimeSlots || this.selectedAnswerForDropOffOrInshop; } else {
this.$emit( this.selectedAnswerForDropOffOrInshop = PICK_A_TIME_BUTTON_VALUE;
"update:modelValue", if (this.selectedRouteCodeData.isPremiumAppointment) {
this.getSelectedTimeSlotInfoObject(this.selectedRouteCode) routeCode = this.addPremiumFlagToInput(routeCode);
); }
}, this.selectedAnswerForTimeSlots = routeCode;
getRelevantDropOffCmsWidgetNameForSelectedTimeSlot( }
selectedRouteCode, } else {
isSameDayRelevant = false this.selectedAnswerForDropOffOrInshop = null;
) { this.selectedAnswerForTimeSlots = null;
if (selectedRouteCode.includes(RouteCodeFlags.OVERNIGHT_DROP_OFF)) {
return this.overnightDropOffCmsWidgetName;
} else if (selectedRouteCode.includes(RouteCodeFlags.ALL_DAY_DROP_OFF)) {
return this.isSameDay && isSameDayRelevant
? this.sameDayDropOffCmsWidgetName
: this.dropoffCmsWidgetName;
} }
}, },
dropOffSelectionChanged(newValue) {
let newSelectedRouteCodeData = null;
if (newValue && newValue != PICK_A_TIME_BUTTON_VALUE) {
newSelectedRouteCodeData = {
routeCode: newValue,
isPremiumAppointment: false,
};
}
this.selectedAnswerForTimeSlots = null;
this.$emit("time-slot-selection-changed", newSelectedRouteCodeData);
},
timeSlotSelectionChanged(newValue) {
if (newValue) {
let routeCode = newValue;
let isPremiumAppointment = routeCode.includes(PREMIUM_TIME_SLOT_ID_FLAG);
if (isPremiumAppointment) {
routeCode = this.removePremiumFlagFromInput(routeCode);
}
this.$emit("time-slot-selection-changed", {
routeCode: routeCode,
isPremiumAppointment: isPremiumAppointment,
});
}
},
// This is unused but planned to be used again
// getRelevantDropOffCmsWidgetNameForSelectedTimeSlot(
// selectedRouteCode,
// isSameDayRelevant = false
// ) {
// if (selectedRouteCode.includes(RouteCodeFlags.OVERNIGHT_DROP_OFF)) {
// return this.overnightDropOffCmsWidgetName;
// } else if (selectedRouteCode.includes(RouteCodeFlags.ALL_DAY_DROP_OFF)) {
// return this.isSameDay && isSameDayRelevant
// ? this.sameDayDropOffCmsWidgetName
// : this.dropoffCmsWidgetName;
// }
// },
getAvailableTimeSlotsForInshop(timeSlotsForSelectedDate) { getAvailableTimeSlotsForInshop(timeSlotsForSelectedDate) {
return timeSlotsForSelectedDate return timeSlotsForSelectedDate
.filter((timeSlot) => { .filter((timeSlot) => {
@ -463,8 +484,6 @@ export default {
); );
} }
this.resetSelectedTimeSlotIfDateChange(timeSlotsForSelectedDate);
return availableTimeSlots; return availableTimeSlots;
}, },
getPremiumAppointmentTimeSlot(timeSlotData) { getPremiumAppointmentTimeSlot(timeSlotData) {
@ -480,35 +499,19 @@ export default {
}, },
}; };
}, },
setSelectedRouteCodeFromParent() {
if (!this.modelValue?.timeSlot?.routeCode) {
return;
}
const routeCodeFromParent = this.modelValue.timeSlot.routeCode;
if (this.modelValue?.isPremiumAppointment) {
// Mobile Only
this.selectedAnswerForTimeSlots = this.addPremiumFlagToInput(routeCodeFromParent);
} else {
if (isDropOffRouteCode(routeCodeFromParent)) {
this.selectedAnswerForDropOffOrInshop = routeCodeFromParent;
} else {
this.selectedAnswerForDropOffOrInshop = PICK_A_TIME_BUTTON_VALUE;
this.selectedAnswerForTimeSlots = routeCodeFromParent;
}
}
},
getWaitListRequestedFromStore() { getWaitListRequestedFromStore() {
return store.getters.order.customer?.waitListRequested; return store.getters.order.customer?.waitListRequested;
}, },
autoSelectTimeSlotIfOnlyOneIsAvailable() { autoSelectTimeSlotIfOnlyOneIsAvailable() {
const numberOfOptions = this.timeSlotsForSelectedDate?.timeSlots?.length; const numberOfOptions = this.timeSlotsForSelectedDate?.timeSlots?.length;
if (this.appointmentType && numberOfOptions === 1) { if (numberOfOptions === 1) {
if (this.availableTimeSlots.length > 0) { if (this.availableTimeSlots.length > 0) {
// this.availableTimeSlots only returns mobile/inshop slots so we know
// the only available slot is not dropOFf
this.selectedAnswerForTimeSlots = this.availableTimeSlots[0].value; this.selectedAnswerForTimeSlots = this.availableTimeSlots[0].value;
} else { } else {
this.selectedAnswerForDropOffOrInshop = this.answersForDropOffQuestion[0].value; this.selectedAnswerForDropOffOrInshop = this.answersForDropOffQuestion[0].value;
} }
this.setSelectedTimeSlot();
} }
}, },
addPremiumFlagToInput(routeCode) { addPremiumFlagToInput(routeCode) {
@ -517,41 +520,6 @@ export default {
removePremiumFlagFromInput(routeCode) { removePremiumFlagFromInput(routeCode) {
return routeCode.replace(PREMIUM_TIME_SLOT_ID_FLAG, ""); return routeCode.replace(PREMIUM_TIME_SLOT_ID_FLAG, "");
}, },
getSelectedTimeSlotInfoObject(routeCode) {
const routeCodeIncludesPremium = routeCode?.includes(PREMIUM_TIME_SLOT_ID_FLAG);
if (routeCodeIncludesPremium) {
routeCode = this.removePremiumFlagFromInput(routeCode);
}
const timeSlot = this.timeSlotsForSelectedDate?.timeSlots?.find(
(slot) => slot.id == routeCode
);
if (timeSlot) {
return {
timeSlot: {
date: this.timeSlotsForSelectedDate.date,
routeCode: timeSlot.id,
startTime: timeSlot.startTime,
endTime: timeSlot.endTime,
jobMaxMinutes: this.estimatedServiceMinutesMaximum?.toString() || null,
jobMinMinutes: this.estimatedServiceMinutesMinimum?.toString() || null,
},
isPremiumAppointment: routeCodeIncludesPremium ? true : false,
};
}
return {
timeSlot: {
date: null,
routeCode: null,
startTime: null,
endTime: null,
jobMaxMinutes: null,
jobMinMinutes: null,
},
isPremiumAppointment: null,
};
},
waitListChecked(event) { waitListChecked(event) {
this.$emit("waitListRequested", event.target.checked); this.$emit("waitListRequested", event.target.checked);
}, },
@ -561,65 +529,6 @@ export default {
successMessageElement.scrollIntoView({ behavior: "smooth" }); successMessageElement.scrollIntoView({ behavior: "smooth" });
} }
}, },
resetSelectedTimeSlotIfDateChange(timeSlotsForSelectedDate) {
if (this.selectedRouteCode) {
const selectedRouteCodeString = this.selectedRouteCode.replace(
PREMIUM_TIME_SLOT_ID_FLAG,
""
);
const matchingTimeSlot = timeSlotsForSelectedDate.find((slot) => {
return slot.id === selectedRouteCodeString;
});
if (!matchingTimeSlot) {
this.resetSelectedTimeSlot();
}
}
},
resetSelectedTimeSlot() {
this.selectedRouteCode = null;
},
},
watch: {
waitListRequested(newVal) {
if (newVal) {
this.$nextTick(() => {
this.scrollToSuccessMessage();
});
}
},
modelValue: {
handler(newValue) {
this.handleChange(newValue);
},
deep: true,
},
selectedDate: {
handler() {
this.resetSelectedTimeSlot();
this.selectedAnswerForDropOffOrInshop = null;
this.selectedAnswerForTimeSlots = null;
this.autoSelectTimeSlotIfOnlyOneIsAvailable();
},
},
selectedRouteCode(newValue) {
if (newValue) {
this.setSelectedTimeSlot();
}
},
selectedAnswerForDropOffOrInshop(newValue) {
if (newValue == PICK_A_TIME_BUTTON_VALUE) {
this.selectedRouteCode = null;
this.setSelectedTimeSlot();
} else {
this.selectedAnswerForTimeSlots = null;
this.selectedRouteCode = newValue;
}
},
selectedAnswerForTimeSlots(newValue) {
if (newValue) {
this.selectedRouteCode = newValue;
}
},
}, },
components: { components: {
textBlock, textBlock,

View file

@ -7,8 +7,8 @@ export async function getPricedMobileFeePart(serviceZipCode, pageNameToLog) {
return null; return null;
} }
const zipCodeData = await getZipCodeData(serviceZipCode); const zipCodeData = await getZipCodeData(serviceZipCode, pageNameToLog);
const providerData = await getShopProviderData(serviceZipCode); const providerData = await getShopProviderData(serviceZipCode, pageNameToLog);
// Get the Mobile Fee Part // Get the Mobile Fee Part
const mobileFeePart = await baseMixin.methods.dispatchStoreActionWithLogging( const mobileFeePart = await baseMixin.methods.dispatchStoreActionWithLogging(
@ -50,7 +50,7 @@ export async function getPricedRecycleFeePart(serviceZipCode, pageNameToLog) {
return null; return null;
} }
const zipCodeData = await getZipCodeData(serviceZipCode); const zipCodeData = await getZipCodeData(serviceZipCode, pageNameToLog);
// Get supporting items // Get supporting items
const supportingItems = await baseMixin.methods.dispatchStoreActionWithLogging( const supportingItems = await baseMixin.methods.dispatchStoreActionWithLogging(
@ -102,23 +102,23 @@ export async function getServiceabilityDetails(serviceZipCode, lineItems, pageNa
); );
} }
export async function getBillToAccountNumber(providerNumber) { export async function getBillToAccountNumber(providerNumber, pageNameToLog) {
// Get the BillTo Account Number // Get the BillTo Account Number
return await baseMixin.methods.dispatchStoreActionWithLogging( return await baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.GET_BILL_TO_ACCOUNT_NUMBER, storeActions.GET_BILL_TO_ACCOUNT_NUMBER,
{ providerNumber: providerNumber }, { providerNumber: providerNumber },
"service-location", pageNameToLog,
false false
); );
} }
export async function getShopProviderData(serviceZipCode) { export async function getShopProviderData(serviceZipCode, pageNameToLog) {
return await baseMixin.methods.dispatchStoreActionWithLogging( return await baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.GET_PROVIDERS, storeActions.GET_PROVIDERS,
{ {
serviceZipCode: serviceZipCode, serviceZipCode: serviceZipCode,
}, },
"service-location" pageNameToLog
); );
} }
@ -139,6 +139,6 @@ export async function getClosestApplicableShops(serviceZipCode, carId, pageNameT
return closestShops.data; return closestShops.data;
} }
export async function getZipCodeData(serviceZipCode) { export async function getZipCodeData(serviceZipCode, pageNameToLog) {
return await baseMixin.methods.getZipCodeData(serviceZipCode, "service-location"); return await baseMixin.methods.getZipCodeData(serviceZipCode, pageNameToLog);
} }

View file

@ -193,8 +193,7 @@ export default {
return; return;
} }
const zipCodeData = await this.getZipCodeData( const zipCodeData = await this.getZipCodeData(
this.internalModel.addressQuestions.zipCode, this.internalModel.addressQuestions.zipCode
"service-location"
); );
if (!zipCodeData.isValid) { if (!zipCodeData.isValid) {
@ -216,26 +215,21 @@ export default {
// retrieve mobile fee part // retrieve mobile fee part
const serviceZipCode = this.internalModel.addressQuestions.zipCode; const serviceZipCode = this.internalModel.addressQuestions.zipCode;
const mobileFeePart = await getPricedMobileFeePart( const mobileFeePart = await getPricedMobileFeePart(serviceZipCode, this.pageName);
serviceZipCode,
"service-location"
);
// retrieve recycle fee part // retrieve recycle fee part
const recycleFeePart = await getPricedRecycleFeePart( const recycleFeePart = await getPricedRecycleFeePart(serviceZipCode, this.pageName);
serviceZipCode,
"service-location"
);
// retrieve serviceability details // retrieve serviceability details
const serviceabilityDetails = await getServiceabilityDetails( const serviceabilityDetails = await getServiceabilityDetails(
serviceZipCode, serviceZipCode,
null, null,
"service-location" this.pageName
); );
const billToAccountNumber = await getBillToAccountNumber( const billToAccountNumber = await getBillToAccountNumber(
this.internalModel.zipCodeCtu this.internalModel.zipCodeCtu,
this.pageName
); );
// update content related to service zip code // update content related to service zip code

View file

@ -226,17 +226,17 @@ export default {
const cmsContentPromise = fetchCmsContentForPage(to.name); const cmsContentPromise = fetchCmsContentForPage(to.name);
const serviceZipCode = store.getters.order.serviceLocation.zipCode; const serviceZipCode = store.getters.order.serviceLocation.zipCode;
const zipCodeDataPromise = getZipCodeData(serviceZipCode); const zipCodeDataPromise = getZipCodeData(serviceZipCode, to.name);
const serviceabilityDetailsPromise = getServiceabilityDetails( const serviceabilityDetailsPromise = getServiceabilityDetails(
serviceZipCode, serviceZipCode,
null, null,
"service-location" to.name
); );
const mobileFeePartPromise = getPricedMobileFeePart(serviceZipCode, "service-location"); const mobileFeePartPromise = getPricedMobileFeePart(serviceZipCode, to.name);
const shopProviderDataPromise = getShopProviderData(serviceZipCode); // shopQuestion.methods.loadInitialData(serviceZipCode); const shopProviderDataPromise = getShopProviderData(serviceZipCode, to.name); // shopQuestion.methods.loadInitialData(serviceZipCode);
// Settle promises and get results // Settle promises and get results
const promiseResultMap = [ const promiseResultMap = [
@ -846,7 +846,7 @@ export default {
zipCode: { zipCode: {
handler(newValue) { handler(newValue) {
if (!this.navigatingForward) { if (!this.navigatingForward) {
getShopProviderData(this.zipCode).then(async (result) => { getShopProviderData(this.zipCode, this.pageName).then(async (result) => {
this.shopProviderData = result.data; this.shopProviderData = result.data;
if (this.selectedAppointmentType === "Mobile") { if (this.selectedAppointmentType === "Mobile") {
this.selectedProvider = new Provider( this.selectedProvider = new Provider(

View file

@ -162,7 +162,7 @@ export default {
const zipCodeData = await this.getZipCodeData( const zipCodeData = await this.getZipCodeData(
this.internalModel.zipCode, this.internalModel.zipCode,
"service-location" this.pageName
); );
if (!zipCodeData.isValid) { if (!zipCodeData.isValid) {
@ -175,7 +175,7 @@ export default {
const closestShops = await getClosestApplicableShops( const closestShops = await getClosestApplicableShops(
this.internalModel.zipCode, this.internalModel.zipCode,
this.carId, this.carId,
"service-location" this.pageName
); );
if (!closestShops || closestShops.inShopProviders?.length === 0) { if (!closestShops || closestShops.inShopProviders?.length === 0) {
@ -193,24 +193,25 @@ export default {
const serviceZipCode = this.internalModel.zipCode; const serviceZipCode = this.internalModel.zipCode;
const mobileFeePart = await getPricedMobileFeePart( const mobileFeePart = await getPricedMobileFeePart(
serviceZipCode, serviceZipCode,
"service-location" this.pageName
); );
// retrieve recycle fee part // retrieve recycle fee part
const recycleFeePart = await getPricedRecycleFeePart( const recycleFeePart = await getPricedRecycleFeePart(
serviceZipCode, serviceZipCode,
"service-location" this.pageName
); );
// retrieve serviceability details // retrieve serviceability details
const serviceabilityDetails = await getServiceabilityDetails( const serviceabilityDetails = await getServiceabilityDetails(
serviceZipCode, serviceZipCode,
null, null,
"service-location" this.pageName
); );
const billToAccountNumber = await getBillToAccountNumber( const billToAccountNumber = await getBillToAccountNumber(
this.internalModel.zipCodeCtu this.internalModel.zipCodeCtu,
this.pageName
); );
// update content related to service zip code // update content related to service zip code

View file

@ -280,7 +280,7 @@ export default {
} }
this.isLoading = true; this.isLoading = true;
this.zipCodeData = await this.getZipCodeData(this.localZipCode, "service-location"); this.zipCodeData = await this.getZipCodeData(this.localZipCode, this.pageName);
// Add zipCode to zipCodeData as it does not come back from endpoint // Add zipCode to zipCodeData as it does not come back from endpoint
this.zipCodeData.zipCode = this.localZipCode; this.zipCodeData.zipCode = this.localZipCode;
if (this.zipCodeData.isValid) { if (this.zipCodeData.isValid) {
@ -289,7 +289,7 @@ export default {
const closestShops = await getClosestApplicableShops( const closestShops = await getClosestApplicableShops(
this.localZipCode, this.localZipCode,
this.$store.getters.order.vehicle.carId, this.$store.getters.order.vehicle.carId,
"service-location" this.pageName
); );
if (!closestShops || closestShops.inShopProviders?.length === 0) { if (!closestShops || closestShops.inShopProviders?.length === 0) {
this.isLoading = false; this.isLoading = false;

View file

@ -318,10 +318,6 @@ export default {
"vin-lookup" "vin-lookup"
); );
// Update button "Continue with..."
this.$refs.navbar.updateButtonText(
`Continue with ${resultMap.vehicleLookupResponse.year} ${resultMap.vehicleLookupResponse.make} ${resultMap.vehicleLookupResponse.model}`
);
return this.$refs.navbar.removeLoader(); return this.$refs.navbar.removeLoader();
} }

View file

@ -1,6 +1,8 @@
import { applicationConfig } from "@/constants/application-config";
const externalUrls = { const externalUrls = {
HERITAGE_FUNNEL: process.env.VUE_APP_HERITAGE_FUNNEL, HERITAGE_FUNNEL: applicationConfig.HERITAGE_FUNNEL,
SAFELITE_HOP: process.env.VUE_APP_SAFELITE_HOP, SAFELITE_HOP: applicationConfig.SAFELITE_HOP,
}; };
export { externalUrls }; export { externalUrls };

View file

@ -1,26 +1,21 @@
<template> <template>
<loadingModal v-if="useLoadingModal" notFullScreen ref="loadingModal" /> <loadingModal v-if="useLoadingModal" notFullScreen ref="loadingModal" />
<a v-if="linkType === 'navigation'" @click="handleClick" class="navigation-link" :href="href"> <!-- NOTE: each prettier-ignore is needed below...
{{ text }} each anchor that follows needs to be one single line in markup or adds unwanted spaces within link (fixes CASH-1502) -->
<slot name="after-text"></slot>
</a> <!-- prettier-ignore -->
<a v-if="linkType === 'navigation'" @click="handleClick" class="navigation-link" :href="href">{{ text }}<slot name="after-text"></slot></a>
<!-- prettier-ignore -->
<a <a
v-else-if="linkType === 'newWindowLink'" v-else-if="linkType === 'newWindowLink'"
@click="handleClick" @click="handleClick"
class="new-window-link" class="new-window-link"
:href="href" :href="href"
target="_blank"> target="_blank">{{ text }}<slot name="after-text"></slot></a>
{{ text }} <!-- prettier-ignore -->
<slot name="after-text"></slot> <a v-else-if="linkType === 'textSmall'" @click="handleClick" class="small" :href="href">{{ text }}<slot name="after-text"></slot></a>
</a> <!-- prettier-ignore -->
<a v-else-if="linkType === 'textSmall'" @click="handleClick" class="small" :href="href"> <a v-else-if="linkType === 'text'" @click="handleClick" :href="href">{{ text }}<slot name="after-text"></slot></a>
{{ 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> </template>
<script> <script>

View file

@ -1,20 +1,5 @@
process.env.VUE_APP_CONSUMER_CF_DISTRO = "https://digitalapi.dev.safelite.io"; const webpack = require('webpack');
//process.env.VUE_APP_HERITAGE_FUNNEL = "http://localhost:38000/default.aspx"; const { environmentVariablesWithDefaults } = require('./environment-variables.js');
process.env.VUE_APP_HERITAGE_FUNNEL = "https://fixmyglassdev.safelite.com/default.aspx";
process.env.VUE_APP_GOOGLE_PLACES_API_KEY = "AIzaSyDptGCkOPgN2uWJOy4ou4M33phRD4MAoJo";
process.env.VUE_APP_CURRENT_ENVIRONMENT = "Localhost";
process.env.VUE_APP_MY_ACCOUNT = "https://myaccountdev.safelite.com/";
//process.env.VUE_APP_SAFELITE_HOP = "http://localhost:60966/fmgCheckoutShared.aspx";
process.env.VUE_APP_SAFELITE_HOP = "https://sv2-safelitehop-dev.safelite.com/fmgCheckoutShared.aspx";
process.env.VUE_APP_SOLARWINDS_MONITORING = "";
process.env.VUE_APP_SESSION_TIMEOUT_MINUTES = 30;
// GA & GTM
process.env.VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY =
"(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl+ '&gtm_auth=amlAYNhxUxuskQo7jmjadg&gtm_preview=env-38&gtm_cookies_win=x';f.parentNode.insertBefore(j,f);})(window,document,'script','dataLayer','GTM-M6XCRH');";
process.env.VUE_APP_GOOGLE_TAG_MANAGER_NOSCRIPT_FRAME_SRC =
"https://www.googletagmanager.com/ns.html?id=GTM-M6XCRH&gtm_auth=amlAYNhxUxuskQo7jmjadg&gtm_preview=env-38&gtm_cookies_win=x";
module.exports = { module.exports = {
outputDir: "dist/fmg", outputDir: "dist/fmg",
@ -36,5 +21,8 @@ module.exports = {
}, },
configureWebpack: { configureWebpack: {
devtool: "source-map", devtool: "source-map",
plugins: [
new webpack.EnvironmentPlugin(environmentVariablesWithDefaults),
],
}, },
}; };

View file

@ -1,16 +1,5 @@
process.env.VUE_APP_CONSUMER_CF_DISTRO = "__VUE_APP_CONSUMER_CF_DISTRO__"; const webpack = require('webpack');
process.env.VUE_APP_GOOGLE_PLACES_API_KEY = "__VUE_APP_GOOGLE_PLACES_API_KEY__"; const { environmentVariablesWithoutDefaults } = require('./environment-variables.js');
process.env.VUE_APP_HERITAGE_FUNNEL = "__VUE_APP_HERITAGE_FUNNEL__";
process.env.VUE_APP_CURRENT_ENVIRONMENT = "__VUE_APP_CURRENT_ENVIRONMENT__";
process.env.VUE_APP_MY_ACCOUNT = "__VUE_APP_MY_ACCOUNT__";
process.env.VUE_APP_SAFELITE_HOP = "__VUE_APP_SAFELITE_HOP__";
process.env.VUE_APP_SOLARWINDS_MONITORING_SCRIPT = "__VUE_APP_SOLARWINDS_MONITORING_SCRIPT__";
process.env.VUE_APP_SESSION_TIMEOUT_MINUTES = 30;
// GA & GTM
process.env.VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY = "__VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY__";
process.env.VUE_APP_GOOGLE_TAG_MANAGER_NOSCRIPT_FRAME_SRC =
"__VUE_APP_GOOGLE_TAG_MANAGER_NOSCRIPT_FRAME_SRC__";
module.exports = { module.exports = {
outputDir: "dist/fmg", outputDir: "dist/fmg",
@ -30,4 +19,9 @@ module.exports = {
}, },
}, },
}, },
configureWebpack: {
plugins: [
new webpack.EnvironmentPlugin(environmentVariablesWithoutDefaults),
],
},
}; };