CASH-1535: Mobile First Appointment
This commit is contained in:
parent
169d8b8949
commit
387906baa8
5 changed files with 348 additions and 3 deletions
|
|
@ -4,6 +4,7 @@ const experimentUniverses = {
|
|||
MSR: "MSR",
|
||||
IGQ_SkipQuote: "NextGen_IGQSkipToInsurance",
|
||||
AFTERPAY_BREAKOUT_DISPLAY: "AfterpayBreakoutDisplay",
|
||||
MOBILE_FIRST_APPOINTMENT: "MobileFirstAppointment",
|
||||
};
|
||||
|
||||
const experimentSettings = {
|
||||
|
|
@ -28,6 +29,10 @@ const experimentSettings = {
|
|||
DISPLAY_AFTERPAY_BREAKOUT_DISPLAY: "Display_AfterpayBreakoutDisplay",
|
||||
AFTERPAY_EXTENDED_PAY_OPTION_THRESHOLD: "AfterPayExtendedPayOptionThreshold",
|
||||
DYNAMO_LOGGING: "DynamoLogging",
|
||||
SHOW_MOBILE_FIRST_APPT: "ShowMobileFirstAppt",
|
||||
SHOW_PM_MOBILE_DAYS: "Show_PMmobileDays",
|
||||
SHOW_NO_PM_MOBILE_DAYS: "Show_NoPMmobileDays",
|
||||
SHOW_NO_MOBILE_AVAILABLE_DAYS: "Show_NoMobileAvailableDays",
|
||||
};
|
||||
|
||||
const experimentTriggers = {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
<div class="modal-dialog" :class="{ 'modal-dialog-centered': !isRecal }">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header mb-0 mt-6">
|
||||
<slot name="modal-header-slot"></slot>
|
||||
<label
|
||||
v-if="headerText"
|
||||
class="modal-title d-flex justify-content-center pb-0 w-100">
|
||||
|
|
|
|||
214
src/layouts/schedule/mobile-first-modal/mobile-first-modal.vue
Normal file
214
src/layouts/schedule/mobile-first-modal/mobile-first-modal.vue
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
<template>
|
||||
<div id="mobile-first-modal-container">
|
||||
<modal
|
||||
:ref="modalName"
|
||||
:headerText="modalHeaderText"
|
||||
suppressPageScroll
|
||||
:onModalClosedCallback="onModalClosed"
|
||||
:footerButtonText="modalFooterText"
|
||||
:isFooterButtonPrimary="true"
|
||||
@footer-button-event="confirmAppointment">
|
||||
<p class="modal-body-inner" v-html="modalBodyText"></p>
|
||||
<template v-slot:modal-header-slot>
|
||||
<span>{{ modalSubHeaderText }}</span>
|
||||
</template>
|
||||
<template v-slot:modal-footer-slot>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-link"
|
||||
id="see-more-options"
|
||||
:disabled="isLoading"
|
||||
@click="closeModal">
|
||||
{{ buttonText }}
|
||||
</button>
|
||||
</template>
|
||||
</modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// Supporting files
|
||||
import store from "@/store";
|
||||
import modal from "@/digital-components/modal/modal";
|
||||
import { splitCopyOnCMSPlaceHolder } from "@/helpers/cms-content-helper";
|
||||
import { get12HourTimeFormat } from "@/helpers/date-helper";
|
||||
import { getDisplayTextForDurationLength } from "@/helpers/duration-length-helper";
|
||||
|
||||
const INLINE_SERVICETYPE_TOKEN = "custom:serviceType";
|
||||
const INLINE_DAY_TOKEN = "custom:day";
|
||||
const INLINE_DATE_TOKEN = "custom:date";
|
||||
const INLINE_TIMESLOT_TOKEN = "custom:timeslot";
|
||||
const INLINE_SERVICE_LENGTH_TOKEN = "custom:serviceLength";
|
||||
|
||||
export default {
|
||||
name: "mobile-first-modal",
|
||||
props: {
|
||||
cmsWidgetName: String,
|
||||
modalWidgetName: String,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedAppontment: {
|
||||
estimatedServiceMinutes: {
|
||||
minimum: null,
|
||||
maximum: null,
|
||||
},
|
||||
timeSlot: null,
|
||||
date: null,
|
||||
},
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
splitCopyOnCMSPlaceHolder,
|
||||
get12HourTimeFormat,
|
||||
openModal() {
|
||||
this.modal.openModal();
|
||||
},
|
||||
onModalClosed() {
|
||||
this.$emit("close-mobile-first-modal");
|
||||
},
|
||||
closeModal() {
|
||||
this.modal.closeModal();
|
||||
},
|
||||
confirmAppointment() {
|
||||
let selectedTimeSlot = {
|
||||
timeSlot: this.selectedAppontment.timeSlot,
|
||||
routeCode: this.selectedAppontment.timeSlot.id,
|
||||
};
|
||||
this.$emit("confirm-appointment", selectedTimeSlot);
|
||||
this.modal.closeModal();
|
||||
},
|
||||
setSelectedAppointment(appointment) {
|
||||
this.selectedAppontment = appointment;
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
modalName() {
|
||||
return this.modalWidgetName;
|
||||
},
|
||||
modal() {
|
||||
return this.$refs[this.modalName];
|
||||
},
|
||||
modalSubHeaderText() {
|
||||
return this.getCmsContent(this.modalWidgetName, "SubheaderText");
|
||||
},
|
||||
modalHeaderText() {
|
||||
return this.getCmsContent(this.modalWidgetName, "HeaderText");
|
||||
},
|
||||
modalBodyText() {
|
||||
var tokens = this.splitCopyOnCMSPlaceHolder(
|
||||
this.getCmsContent(this.modalWidgetName, "BodyText")
|
||||
);
|
||||
tokens.forEach((token) => {
|
||||
if (token.includes(INLINE_SERVICETYPE_TOKEN)) {
|
||||
const serviceType = "<b>" + this.getServiceType + "</b>";
|
||||
tokens.splice(tokens.indexOf(token), 1, serviceType);
|
||||
} else if (token.includes(INLINE_DAY_TOKEN) && this.selectedAppontment.date) {
|
||||
const day =
|
||||
"<b>" +
|
||||
new Date(this.selectedAppontment.date).toLocaleDateString("en-US", {
|
||||
weekday: "long",
|
||||
timeZone: "UTC",
|
||||
}) +
|
||||
"</b>";
|
||||
tokens.splice(tokens.indexOf(token), 1, day);
|
||||
} else if (token.includes(INLINE_DATE_TOKEN) && this.selectedAppontment.date) {
|
||||
const date = new Date(this.selectedAppontment.date).toLocaleDateString(
|
||||
"en-US",
|
||||
{ timeZone: "UTC", weekday: "long", month: "long", day: "numeric" }
|
||||
);
|
||||
tokens.splice(tokens.indexOf(token), 1, date);
|
||||
} else if (
|
||||
token.includes(INLINE_TIMESLOT_TOKEN) &&
|
||||
this.selectedAppontment.timeSlot
|
||||
) {
|
||||
const timeslot =
|
||||
this.get12HourTimeFormat(this.selectedAppontment.timeSlot.startTime) +
|
||||
" - " +
|
||||
this.get12HourTimeFormat(this.selectedAppontment.timeSlot.endTime);
|
||||
tokens.splice(tokens.indexOf(token), 1, timeslot);
|
||||
} else if (
|
||||
token.includes(INLINE_SERVICE_LENGTH_TOKEN) &&
|
||||
this.selectedAppontment.estimatedServiceMinutes
|
||||
) {
|
||||
const inshopDurationTime = getDisplayTextForDurationLength(
|
||||
this.selectedAppontment.estimatedServiceMinutes.minimum,
|
||||
this.selectedAppontment.estimatedServiceMinutes.maximum
|
||||
);
|
||||
tokens.splice(tokens.indexOf(token), 1, inshopDurationTime);
|
||||
}
|
||||
});
|
||||
return tokens.join("");
|
||||
},
|
||||
modalFooterText() {
|
||||
return this.getCmsContent(this.modalWidgetName, "FooterText");
|
||||
},
|
||||
buttonText() {
|
||||
return this.getCmsContent(this.modalWidgetName, "FooterText2");
|
||||
},
|
||||
getServiceType() {
|
||||
return store.getters.order.damage.isRepair ? "repair" : "replace";
|
||||
},
|
||||
},
|
||||
components: { modal },
|
||||
};
|
||||
</script>
|
||||
<style lang="scss">
|
||||
#mobile-first-modal-container {
|
||||
.modal-component {
|
||||
@include media-breakpoint-up(md) {
|
||||
.modal-dialog {
|
||||
left: 0;
|
||||
align-content: center;
|
||||
flex-wrap: wrap;
|
||||
width: 22.063rem;
|
||||
transform: translate(0, 0);
|
||||
|
||||
.modal-content {
|
||||
border-radius: $border-radius-lg;
|
||||
}
|
||||
}
|
||||
}
|
||||
.modal-dialog {
|
||||
.modal-content {
|
||||
.modal-header {
|
||||
flex-direction: column;
|
||||
& > span {
|
||||
color: $red;
|
||||
font-size: $font-size-14;
|
||||
font-weight: $font-weight-600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: $font-size-20;
|
||||
font-weight: $font-weight-normal;
|
||||
}
|
||||
}
|
||||
.modal-body {
|
||||
padding: 0.5rem 1rem;
|
||||
|
||||
.modal-body-inner {
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 1rem;
|
||||
background-color: #f4f4f4;
|
||||
font-size: $font-size-14;
|
||||
}
|
||||
}
|
||||
}
|
||||
.modal-footer {
|
||||
flex-flow: wrap-reverse;
|
||||
|
||||
#see-more-options {
|
||||
width: 100%;
|
||||
text-decoration: none;
|
||||
margin-top: 1.5rem;
|
||||
font-weight: $font-weight-600;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -32,6 +32,10 @@
|
|||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-10 col-lg-8 col-xl-7">
|
||||
<mobileFirstModal
|
||||
modalWidgetName="MobileFirstModalWidget"
|
||||
ref="mobileFirstModal"
|
||||
@confirm-appointment="updateTimeSlotandNavigateForward" />
|
||||
<alert
|
||||
ref="alertMobileOnly"
|
||||
class="mt-5 mb-5"
|
||||
|
|
@ -183,6 +187,7 @@ import shopListButton from "@/layouts/service-location/shop-question/shop-list-b
|
|||
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 mobileFirstModal from "@/layouts/schedule/mobile-first-modal/mobile-first-modal.vue";
|
||||
|
||||
import shopQuestion from "@/layouts/service-location/shop-question/shop-question";
|
||||
import shopQuestionPopup from "@/layouts/service-location/shop-question/shop-question-popup";
|
||||
|
|
@ -203,7 +208,7 @@ import mobileFeeWaiverAlert from "./mobile-fee-waiver-alert/mobile-fee-waiver-al
|
|||
// Supporting files
|
||||
import baseMixin from "@/mixins/base-mixin.js";
|
||||
import experimentMixin from "@/mixins/experiment-mixin.js";
|
||||
import { experimentSettings } from "@/constants/experiments";
|
||||
import { experimentUniverses, experimentSettings } from "@/constants/experiments";
|
||||
import {
|
||||
AppointmentTypeStrings,
|
||||
RouteCodeFlags,
|
||||
|
|
@ -242,6 +247,11 @@ import { getAmountDue, getPricingByDayPartWithPrice } from "@/helpers/pricing-he
|
|||
import { getItemsWithoutRecalParts } from "@/helpers/recal-helper";
|
||||
import { deepClone } from "@/helpers/object-helper";
|
||||
import { debugLog } from "@/helpers/debug-log-helper";
|
||||
import {
|
||||
getSessionKeyValue,
|
||||
getUserIdValue,
|
||||
getDeviceIdValue,
|
||||
} from "@/helpers/heritage-integration/cookie-helper";
|
||||
|
||||
// DEFINE VALIDATION RULES
|
||||
defineRule("mobile-location-required", (value) => {
|
||||
|
|
@ -1172,6 +1182,8 @@ export default {
|
|||
moreShopTimeSlots.mobileTimeSlotsData.days
|
||||
);
|
||||
|
||||
this.showMobileFirstModal();
|
||||
|
||||
return moreShopTimeSlots;
|
||||
},
|
||||
|
||||
|
|
@ -1699,6 +1711,7 @@ export default {
|
|||
}
|
||||
this.appointmentType = AppointmentTypeStrings.MOBILE;
|
||||
this.updateSelectedProvider();
|
||||
this.showMobileFirstModal();
|
||||
} else if (newAppointmentType) {
|
||||
if (this.appointmentType != AppointmentTypeStrings.MOBILE) {
|
||||
// Clear last shop selected if appointment type was changed in any manner other than from Mobile
|
||||
|
|
@ -1723,6 +1736,116 @@ export default {
|
|||
setSelectedDateToFirstAvailable() {
|
||||
this.selectedDate = this.getFirstAvailableDate();
|
||||
},
|
||||
showMobileFirstModal() {
|
||||
if (this.appointmentType == AppointmentTypeStrings.MOBILE) {
|
||||
const isShowMobileFirstAppt = experimentMixin.methods.hasSettingEqualTo(
|
||||
experimentSettings.SHOW_MOBILE_FIRST_APPT,
|
||||
"true"
|
||||
);
|
||||
const pmMobileDays = experimentMixin.methods.getSettingValue(
|
||||
experimentSettings.SHOW_PM_MOBILE_DAYS
|
||||
);
|
||||
const noPMMobileDays = experimentMixin.methods.getSettingValue(
|
||||
experimentSettings.SHOW_NO_PM_MOBILE_DAYS
|
||||
);
|
||||
const noMobileAvailableDays = experimentMixin.methods.getSettingValue(
|
||||
experimentSettings.SHOW_NO_MOBILE_AVAILABLE_DAYS
|
||||
);
|
||||
|
||||
let preSelectedMobileAppointment = null;
|
||||
const todaysDate = getTodayDate();
|
||||
let firstMobilePMAppt = this.getFirstAvailableMobileApptByTOD("PM");
|
||||
if (!firstMobilePMAppt) {
|
||||
return;
|
||||
}
|
||||
let firstMobilePMDate = firstMobilePMAppt ? firstMobilePMAppt.date : null;
|
||||
let numberOfDaysToFirstMobilePMDate =
|
||||
(new Date(firstMobilePMDate) - todaysDate) / (1000 * 60 * 60 * 24);
|
||||
|
||||
const shouldExposeMobileFirstAppointment = () => {
|
||||
return (
|
||||
firstMobilePMDate &&
|
||||
numberOfDaysToFirstMobilePMDate <= noMobileAvailableDays
|
||||
);
|
||||
};
|
||||
|
||||
if (shouldExposeMobileFirstAppointment()) {
|
||||
const mobileFirstExperiment = store.getters.applicationUser.experiments.find(
|
||||
(e) => e.universeName === experimentUniverses.MOBILE_FIRST_APPOINTMENT
|
||||
);
|
||||
const hasExposedMobileFirst = mobileFirstExperiment?.isExposed;
|
||||
|
||||
if (!hasExposedMobileFirst && mobileFirstExperiment) {
|
||||
baseMixin.methods.dispatchStoreActionWithLogging(
|
||||
storeActions.LOG_EXPERIMENT_EXPOSURE_AND_UPDATE_STORE,
|
||||
{
|
||||
userId: getUserIdValue(),
|
||||
deviceId: getDeviceIdValue(),
|
||||
sessionKey: getSessionKeyValue(),
|
||||
pageName: "schedule",
|
||||
experiment: mobileFirstExperiment,
|
||||
},
|
||||
"schedule",
|
||||
false
|
||||
);
|
||||
|
||||
if (isShowMobileFirstAppt) {
|
||||
if (
|
||||
firstMobilePMDate &&
|
||||
numberOfDaysToFirstMobilePMDate <= pmMobileDays
|
||||
) {
|
||||
preSelectedMobileAppointment = firstMobilePMAppt;
|
||||
} else if (
|
||||
firstMobilePMDate &&
|
||||
numberOfDaysToFirstMobilePMDate >= noPMMobileDays
|
||||
) {
|
||||
let firstMobileAMAppt = this.getFirstAvailableMobileApptByTOD("AM");
|
||||
preSelectedMobileAppointment = firstMobileAMAppt
|
||||
? firstMobileAMAppt
|
||||
: null;
|
||||
!preSelectedMobileAppointment &&
|
||||
(preSelectedMobileAppointment = firstMobilePMAppt);
|
||||
}
|
||||
|
||||
this.$refs.mobileFirstModal.setSelectedAppointment(
|
||||
preSelectedMobileAppointment
|
||||
);
|
||||
this.$refs.mobileFirstModal.openModal();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
getFirstAvailableMobileApptByTOD(timeOfDay) {
|
||||
if (!this.selectableDatesMobile?.days?.length) {
|
||||
return null;
|
||||
} else {
|
||||
for (const dateObj of this.selectableDatesMobile.days) {
|
||||
let matchingTimeSlot = {
|
||||
estimatedServiceMinutes: {
|
||||
minimum: this.estimatedServiceMinutesMinimum,
|
||||
maximum: this.estimatedServiceMinutesMaximum,
|
||||
},
|
||||
timeSlot: null,
|
||||
date: null,
|
||||
};
|
||||
const isMatchingApptDay = dateObj.timeSlots.some(
|
||||
(slot) =>
|
||||
slot.id.includes(timeOfDay) &&
|
||||
(matchingTimeSlot.timeSlot = slot) &&
|
||||
(matchingTimeSlot.date = dateObj.date)
|
||||
);
|
||||
if (isMatchingApptDay && matchingTimeSlot) {
|
||||
return matchingTimeSlot;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
},
|
||||
updateTimeSlotandNavigateForward(timeSlotObj) {
|
||||
this.updateTimeSlot(timeSlotObj);
|
||||
this.forwardButtonAction();
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
appointmentTypeFromAppointmentTypeQuestion: {
|
||||
|
|
@ -1767,6 +1890,7 @@ export default {
|
|||
locationAlerts,
|
||||
textBlock,
|
||||
timeSlotQuestion,
|
||||
mobileFirstModal,
|
||||
|
||||
alert,
|
||||
serviceZipModalQuestion,
|
||||
|
|
|
|||
|
|
@ -123,14 +123,15 @@ $body-color: $gray-600;
|
|||
|
||||
//Fonts
|
||||
$font-family-sans-serif: UrbanistRegular, Arial, Helvetica, sans-serif;
|
||||
$font-family-monospace: UrbanistRegular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New",
|
||||
monospace;
|
||||
$font-family-monospace:
|
||||
UrbanistRegular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
// stylelint-enable value-keyword-case
|
||||
$font-family-base: $font-family-sans-serif;
|
||||
$font-family-code: $font-family-monospace;
|
||||
$font-size-base: 1rem; // Assumes the browser default, typically `16px`
|
||||
|
||||
$font-size-12: $font-size-base * 0.75; // 12px
|
||||
$font-size-14: $font-size-base * 0.875; // 14px
|
||||
$font-size-20: $font-size-base * 1.25; // 20px
|
||||
|
||||
//Custom Font size (extra small)
|
||||
|
|
|
|||
Loading…
Reference in a new issue