Merge pull request #3196 from Safelite/feature/CASH-2572

CASH-2571 & CASH-2572 | Scheduling page work
This commit is contained in:
scottkiener-at-safelite 2026-05-28 12:51:44 -04:00 committed by GitHub
commit bda7d7ed83
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 804 additions and 1 deletions

View file

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M-6.24658e-06 12C-6.55781e-06 14.3734 0.703782 16.6935 2.02236 18.6668C3.34094 20.6402 5.21508 22.1783 7.40779 23.0866C9.60051 23.9948 12.0133 24.2324 14.3411 23.7694C16.6688 23.3064 18.807 22.1635 20.4853 20.4853C22.1635 18.8071 23.3064 16.6689 23.7694 14.3411C24.2324 12.0133 23.9948 9.60051 23.0865 7.4078C22.1783 5.21509 20.6402 3.34094 18.6668 2.02237C16.6934 0.703788 14.3734 -2.13306e-07 12 -5.24537e-07C8.8174 -9.41884e-07 5.76515 1.26428 3.51472 3.51472C1.26428 5.76515 -5.82924e-06 8.8174 -6.24658e-06 12ZM11.28 5.8872L16.8 11.4072C16.9568 11.5646 17.0449 11.7778 17.0449 12C17.0449 12.2222 16.9568 12.4354 16.8 12.5928L11.28 18.1128C11.1228 18.2719 10.9088 18.3621 10.6851 18.3634C10.4614 18.3648 10.2463 18.2772 10.0872 18.12C9.92806 17.9628 9.83791 17.7488 9.83656 17.5251C9.83521 17.3014 9.92277 17.0863 10.08 16.9272L15.012 12L10.08 7.0728C9.92278 6.91367 9.83521 6.6986 9.83656 6.47491C9.83791 6.25122 9.92807 6.03722 10.0872 5.88C10.2463 5.72278 10.4614 5.63521 10.6851 5.63656C10.9088 5.63791 11.1228 5.72807 11.28 5.8872Z" fill="#0070D1"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M-6.24658e-06 12C-6.55781e-06 14.3734 0.703782 16.6935 2.02236 18.6668C3.34094 20.6402 5.21508 22.1783 7.40779 23.0866C9.60051 23.9948 12.0133 24.2324 14.3411 23.7694C16.6688 23.3064 18.807 22.1635 20.4853 20.4853C22.1635 18.8071 23.3064 16.6689 23.7694 14.3411C24.2324 12.0133 23.9948 9.60051 23.0865 7.4078C22.1783 5.21509 20.6402 3.34094 18.6668 2.02237C16.6934 0.703788 14.3734 -2.13306e-07 12 -5.24537e-07C8.8174 -9.41884e-07 5.76515 1.26428 3.51472 3.51472C1.26428 5.76515 -5.82924e-06 8.8174 -6.24658e-06 12ZM11.28 5.8872L16.8 11.4072C16.9568 11.5646 17.0449 11.7778 17.0449 12C17.0449 12.2222 16.9568 12.4354 16.8 12.5928L11.28 18.1128C11.1228 18.2719 10.9088 18.3621 10.6851 18.3634C10.4614 18.3648 10.2463 18.2772 10.0872 18.12C9.92806 17.9628 9.83791 17.7488 9.83656 17.5251C9.83521 17.3014 9.92277 17.0863 10.08 16.9272L15.012 12L10.08 7.0728C9.92278 6.91367 9.83521 6.6986 9.83656 6.47491C9.83791 6.25122 9.92807 6.03722 10.0872 5.88C10.2463 5.72278 10.4614 5.63521 10.6851 5.63656C10.9088 5.63791 11.1228 5.72807 11.28 5.8872Z" fill="#B3B4B5"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -0,0 +1,5 @@
const Breakpoints = {
MD: 768, // This value is to match the md breakpoint in src/styles/ux-variables.scss
};
export { Breakpoints };

View file

@ -47,6 +47,7 @@ const experimentSettings = {
USE_ADYEN_PAYMENT: "UseAdyenPayment",
SHOW_UNVERIFIED_DEDUCT_ENTRY: "ShowUnverifiedDeducEntry",
SERVICE_PACKAGE_ORDER: "ServicePackageOrder",
USE_SCHEDULING_PAGE: "UseSchedulingPage",
// Claim and Coverage for 2.0
ENABLE_NON_CLAIM_AND_COVERAGE_FLOW: "EnableNonC&CFlow",

View file

@ -104,6 +104,10 @@ export const pageProgressMapper = {
percent: 76,
step: 3,
},
scheduling: {
percent: 76,
step: 3,
},
"mobile-details": {
percent: 76,
step: 3,

View file

@ -0,0 +1,225 @@
import { shallowMount } from "@vue/test-utils";
import datePicker from "./date-picker";
const AVAILABLE_DATES = ["2026-01-21", "2026-01-22", "2026-01-23", "2026-01-25"];
function mountDesktop(props = {}) {
Object.defineProperty(window, "innerWidth", {
writable: true,
configurable: true,
value: 1024,
});
return shallowMount(datePicker, {
props: {
availableDates: AVAILABLE_DATES,
startDate: "2026-01-21",
endDate: "2026-01-25",
modelValue: null,
...props,
},
});
}
function mountMobile(props = {}) {
Object.defineProperty(window, "innerWidth", {
writable: true,
configurable: true,
value: 375,
});
return shallowMount(datePicker, {
props: {
availableDates: AVAILABLE_DATES,
startDate: "2026-01-21",
endDate: "2026-01-25",
modelValue: null,
...props,
},
});
}
describe("date-picker.vue", () => {
afterEach(() => {
Object.defineProperty(window, "innerWidth", {
writable: true,
configurable: true,
value: 1024,
});
});
describe("rendering", () => {
test("renders 5 date cards on desktop", () => {
const wrapper = mountDesktop();
expect(wrapper.findAll(".date-picker__day-card").length).toBe(5);
wrapper.unmount();
});
test("renders 3 date cards on mobile", () => {
const wrapper = mountMobile();
expect(wrapper.findAll(".date-picker__day-card").length).toBe(3);
wrapper.unmount();
});
test("renders disabled cards for dates not in availableDates", () => {
const wrapper = mountDesktop();
// Jan 24 is not in AVAILABLE_DATES but falls in the range Jan 2125
const disabledCards = wrapper.findAll(".date-picker__day-card--disabled");
expect(disabledCards.length).toBeGreaterThan(0);
wrapper.unmount();
});
test("back navigation button is disabled at the start", () => {
const wrapper = mountDesktop();
const backBtn = wrapper.findAll(".date-picker__nav-btn")[0];
expect(backBtn.attributes("disabled")).toBeDefined();
wrapper.unmount();
});
});
describe("allDates computation", () => {
test("builds a contiguous date range between first and last available date", () => {
const wrapper = mountDesktop();
const allValues = wrapper.vm.allDates.map((d) => d.value);
// Range Jan 2125 should include all 5 dates
expect(allValues).toEqual([
"2026-01-21",
"2026-01-22",
"2026-01-23",
"2026-01-24",
"2026-01-25",
]);
wrapper.unmount();
});
test("marks dates not in availableDates as unavailable", () => {
const wrapper = mountDesktop();
const jan24 = wrapper.vm.allDates.find((d) => d.value === "2026-01-24");
expect(jan24.isAvailable).toBe(false);
wrapper.unmount();
});
test("marks dates in availableDates as available", () => {
const wrapper = mountDesktop();
const jan21 = wrapper.vm.allDates.find((d) => d.value === "2026-01-21");
expect(jan21.isAvailable).toBe(true);
wrapper.unmount();
});
test("attaches correct day abbreviation to each date", () => {
const wrapper = mountDesktop();
const jan21 = wrapper.vm.allDates.find((d) => d.value === "2026-01-21");
// 2026-01-21 is a Wednesday
expect(jan21.dayAbbr).toBe("WED");
wrapper.unmount();
});
test("returns empty array when availableDates is empty", () => {
const wrapper = mountDesktop({ availableDates: [] });
expect(wrapper.vm.allDates).toEqual([]);
wrapper.unmount();
});
});
describe("date selection", () => {
test("emits update:modelValue with the date string when an available card is clicked", async () => {
const wrapper = mountDesktop();
const availableCard = wrapper.find(
".date-picker__day-card:not(.date-picker__day-card--disabled)"
);
await availableCard.trigger("click");
expect(wrapper.emitted("update:modelValue")).toBeTruthy();
expect(wrapper.emitted("update:modelValue")[0][0]).toBe("2026-01-21");
wrapper.unmount();
});
test("does not emit when a disabled card is clicked", async () => {
const wrapper = mountDesktop();
const disabledCard = wrapper.find(".date-picker__day-card--disabled");
await disabledCard.trigger("click");
expect(wrapper.emitted("update:modelValue")).toBeFalsy();
wrapper.unmount();
});
test("applies the selected class to the card matching modelValue", async () => {
const wrapper = mountDesktop({ modelValue: "2026-01-21" });
const selectedCard = wrapper.find(".date-picker__day-card--selected");
expect(selectedCard.exists()).toBeTruthy();
wrapper.unmount();
});
test("no card has the selected class when modelValue is null", () => {
const wrapper = mountDesktop({ modelValue: null });
expect(wrapper.find(".date-picker__day-card--selected").exists()).toBe(false);
wrapper.unmount();
});
});
describe("navigation", () => {
test("goForward advances windowStart by the window size", async () => {
// Use a longer range so forward is possible on desktop (window 5)
const manyDates = ["2026-01-01", "2026-01-02", "2026-01-03", "2026-01-10"];
const wrapper = mountDesktop({
availableDates: manyDates,
startDate: "2026-01-01",
endDate: "2026-01-10",
});
expect(wrapper.vm.canGoForward).toBe(true);
await wrapper.vm.goForward();
expect(wrapper.vm.windowStart).toBe(5);
wrapper.unmount();
});
test("goBack decrements windowStart by the window size", async () => {
const manyDates = ["2026-01-01", "2026-01-02", "2026-01-03", "2026-01-10"];
const wrapper = mountDesktop({
availableDates: manyDates,
startDate: "2026-01-01",
endDate: "2026-01-10",
});
await wrapper.vm.goForward();
await wrapper.vm.goBack();
expect(wrapper.vm.windowStart).toBe(0);
wrapper.unmount();
});
test("goBack does not go below 0", () => {
const wrapper = mountDesktop();
wrapper.vm.goBack();
expect(wrapper.vm.windowStart).toBe(0);
wrapper.unmount();
});
test("goForward does not exceed the last window position", async () => {
// Range Jan 2125 = 5 dates, window 5 on desktop → already at max
const wrapper = mountDesktop();
await wrapper.vm.goForward();
// windowStart should not push visible window past the array length
expect(wrapper.vm.windowStart + wrapper.vm.windowSize).toBeLessThanOrEqual(
wrapper.vm.allDates.length + wrapper.vm.windowSize
);
wrapper.unmount();
});
test("back button becomes enabled after navigating forward", async () => {
const manyDates = ["2026-01-01", "2026-01-02", "2026-01-03", "2026-01-10"];
const wrapper = mountDesktop({
availableDates: manyDates,
startDate: "2026-01-01",
endDate: "2026-01-10",
});
await wrapper.vm.goForward();
await wrapper.vm.$nextTick();
const backBtn = wrapper.findAll(".date-picker__nav-btn")[0];
expect(backBtn.attributes("disabled")).toBeUndefined();
wrapper.unmount();
});
});
describe("initial window positioning", () => {
test("scrolls to the window containing the pre-selected date on mount", () => {
// Jan 25 is the 5th date (index 4); with window size 5 on desktop it's still in window 0
const wrapper = mountDesktop({ modelValue: "2026-01-25" });
expect(wrapper.vm.windowStart).toBe(0);
wrapper.unmount();
});
});
});

View file

@ -0,0 +1,295 @@
<template>
<div class="date-picker d-flex align-items-center">
<button
class="date-picker__nav-btn"
:disabled="!canGoBack"
aria-label="Previous dates"
@click="goBack">
<img
v-if="canGoBack"
src="@/assets/img/icons/chevron-right-blue.svg"
class="date-picker__nav-icon date-picker__nav-icon--flipped"
alt="" />
<img
v-else
src="@/assets/img/icons/chevron-right-grey.svg"
class="date-picker__nav-icon date-picker__nav-icon--flipped"
alt="" />
</button>
<div class="date-picker__track d-flex flex-grow-1">
<button
v-for="date in visibleDates"
:key="date.value"
class="date-picker__day-card d-flex flex-column align-items-center justify-content-center"
:class="{
'date-picker__day-card--selected': isSelected(date.value),
'date-picker__day-card--disabled': !date.isAvailable,
}"
:disabled="!date.isAvailable"
:aria-pressed="isSelected(date.value)"
:aria-label="`${date.dayAbbr} ${date.monthAbbr} ${date.day}`"
@click="selectDate(date)">
<span class="date-picker__day-abbr">{{ date.dayAbbr }}</span>
<span class="date-picker__day-date">{{ date.monthAbbr }} {{ date.day }}</span>
</button>
</div>
<button
class="date-picker__nav-btn"
:disabled="!canGoForward"
aria-label="Next dates"
@click="goForward">
<img
v-if="canGoForward"
src="@/assets/img/icons/chevron-right-blue.svg"
class="date-picker__nav-icon"
alt="" />
<img
v-else
src="@/assets/img/icons/chevron-right-grey.svg"
class="date-picker__nav-icon"
alt="" />
</button>
</div>
</template>
<script>
import { Breakpoints } from "@/constants/breakpoints";
const DAY_ABBRS = ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"];
const MONTH_ABBRS = [
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec",
];
const MOBILE_WINDOW_SIZE = 3;
const DESKTOP_WINDOW_SIZE = 5;
const DESKTOP_BREAKPOINT_PX = Breakpoints.MD;
function toLocalDateString(date) {
const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, "0");
const d = String(date.getDate()).padStart(2, "0");
return `${y}-${m}-${d}`;
}
function parseLocalDate(str) {
const [year, month, day] = str.split("-").map(Number);
return new Date(year, month - 1, day);
}
export default {
name: "datePicker",
props: {
availableDates: {
type: Array,
default: () => [],
},
startDate: {
type: String,
default: null,
},
endDate: {
type: String,
default: null,
},
modelValue: {
type: String,
default: null,
},
isLoadingDates: {
type: Boolean,
default: false,
},
},
emits: ["update:modelValue", "requestMoreDates"],
data() {
return {
windowStart: 0,
windowSize:
window.innerWidth >= DESKTOP_BREAKPOINT_PX
? DESKTOP_WINDOW_SIZE
: MOBILE_WINDOW_SIZE,
};
},
computed: {
allDates() {
const availableSet = new Set(this.availableDates);
if (!this.availableDates.length || !this.startDate || !this.endDate) return [];
const first = parseLocalDate(this.startDate);
const last = parseLocalDate(this.endDate);
const dates = [];
const cursor = new Date(first);
while (cursor <= last) {
const value = toLocalDateString(cursor);
dates.push({
value,
isAvailable: availableSet.has(value),
dayAbbr: DAY_ABBRS[cursor.getDay()],
monthAbbr: MONTH_ABBRS[cursor.getMonth()],
day: cursor.getDate(),
});
cursor.setDate(cursor.getDate() + 1);
}
return dates;
},
visibleDates() {
return this.allDates.slice(this.windowStart, this.windowStart + this.windowSize);
},
canGoBack() {
return this.windowStart > 0;
},
canGoForward() {
return !this.isLoadingDates && this.allDates.length > 0;
},
},
mounted() {
this.updateWindowSize();
window.addEventListener("resize", this.updateWindowSize);
if (this.modelValue && this.allDates.length) {
const index = this.allDates.findIndex((d) => d.value === this.modelValue);
if (index !== -1) {
this.windowStart = Math.floor(index / this.windowSize) * this.windowSize;
}
}
},
beforeUnmount() {
window.removeEventListener("resize", this.updateWindowSize);
},
methods: {
updateWindowSize() {
this.windowSize =
window.innerWidth >= DESKTOP_BREAKPOINT_PX
? DESKTOP_WINDOW_SIZE
: MOBILE_WINDOW_SIZE;
},
goBack() {
this.windowStart = Math.max(0, this.windowStart - this.windowSize);
},
goForward() {
if (this.isLoadingDates) return;
const isAtEnd = this.windowStart + this.windowSize >= this.allDates.length;
if (isAtEnd) {
this.$emit("requestMoreDates");
} else {
const maxStart = Math.max(0, this.allDates.length - this.windowSize);
this.windowStart = Math.min(maxStart, this.windowStart + this.windowSize);
}
},
selectDate(date) {
if (!date.isAvailable) return;
this.$emit("update:modelValue", date.value);
},
isSelected(value) {
return this.modelValue === value;
},
},
};
</script>
<style lang="scss">
.date-picker {
gap: 0.25rem;
&__nav-btn {
flex-shrink: 0;
width: 2rem;
height: 2rem;
border: none;
background: transparent;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
border-radius: 50%;
transition: background 150ms ease;
&:hover:not(:disabled) {
background: $blue-100;
}
&:disabled {
cursor: default;
}
}
&__track {
gap: 0.5rem;
}
&__day-card {
flex: 1;
min-width: 0;
border: 1px solid $blue;
border-radius: $border-radius-lg;
background: $white;
padding: 0.625rem 0.25rem;
cursor: pointer;
transition:
background 150ms ease,
border-color 150ms ease;
&:hover:not(:disabled):not(.date-picker__day-card--selected) {
background: $blue-100;
}
&--selected {
background: $blue;
border-color: $blue;
.date-picker__day-abbr,
.date-picker__day-date {
color: $white;
}
}
&--disabled {
background: $gray-100;
border-color: $gray-200;
cursor: default;
.date-picker__day-abbr,
.date-picker__day-date {
color: $gray-300;
}
}
}
&__day-abbr {
font-family: $font-family-sans-serif-semibold;
font-size: $font-size-12;
color: $blue;
text-transform: uppercase;
line-height: 1.2;
}
&__day-date {
font-family: $font-family-sans-serif-bold;
font-size: $font-size-14;
color: $blue;
line-height: 1.4;
white-space: nowrap;
}
&__nav-icon {
width: 24px;
height: 24px;
&--flipped {
transform: scaleX(-1);
}
}
}
</style>

View file

@ -0,0 +1,223 @@
<template>
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm">
<loadingModal notFullScreen ref="loadingModal" />
<funnelHeader cmsWidgetName="FunnelHeaderWidget" ref="funnelHeader" />
<div class="container page-container-grouped-styles">
<div class="row">
<div class="col-12 col-md-10 col-lg-8 col-xl-7">
<h5 class="fw-normal mb-0 dark-header" :class="headerColor">
<span>
{{ serviceLocationText }}
</span>
</h5>
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" alignLeft />
<datePicker
class="mt-5"
v-model="selectedDate"
:startDate="datePickerStartDate"
:endDate="datePickerEndDate"
:availableDates="availableDates"
:isLoadingDates="isLoadingDates"
@requestMoreDates="handleRequestMoreDates" />
<navbar
cmsWidgetName="FunnelFooterWidget"
ref="navbar"
@back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction" />
</div>
</div>
</div>
</Form>
</template>
<script>
import funnelHeader from "@/fmg-components/funnel-header/funnel-header";
import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header";
import navbar from "@/fmg-components/nav-bar/nav-bar";
import { Form } from "vee-validate";
import datePicker from "@/layouts/scheduling/date-picker/date-picker";
import loadingModal from "@/fmg-components/loading-modal/loading-modal.vue";
import store from "@/store";
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { settleAllPromises } from "@/helpers/layout-helper";
import { AppointmentTypeStrings } from "@/constants/schedule-constants";
import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
import {
flushPagePrereqsLogs,
hasServiceZipInfo,
hasGlassPartsOrRepairInfo,
hasInsuranceInfo,
} from "@/helpers/page-prerequisites-helper.js";
import { debugLog } from "@/helpers/debug-log-helper";
/**
* Returns a YYYY-MM-DD date string offset by the given number of days from a base date.
* @param {number} offsetDays - Number of days to offset (positive or negative).
* @param {Date} base - The starting date (defaults to today).
*/
function toDateString(offsetDays, base = new Date()) {
const d = new Date(base);
d.setDate(d.getDate() + offsetDays);
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
}
const SCHEDULE_FETCH_DAYS = 15;
export default {
name: "scheduling",
async beforeRouteEnter(to, from, next) {
const serviceZipCode = store.getters.order.serviceLocation.zipCode;
const shopProviderData = await store.dispatch("getProviders", {
payload: { serviceZipCode },
pageNameToLog: to.name,
});
// Get the first 3 providers from the shopProviderData
const providers = shopProviderData?.data?.shopProviders?.slice(0, 3) ?? [];
const mobileProviderNumber = shopProviderData?.data?.mobileProviderNumber ?? null;
const startDate = toDateString(0);
const endDate = toDateString(SCHEDULE_FETCH_DAYS - 1);
const promiseResultMap = [
{
resultKey: "cmsContent",
promise: fetchCmsContentForPage(to.name),
},
...providers.map((provider, i) => ({
resultKey: `inshopTimeSlots_${i}`,
promise: store.dispatch("getShopTimeSlots", {
payload: {
startDate,
endDate,
shopAppointmentType: "InshopOrDropoff",
providerNumber: provider.providerNumber,
},
pageNameToLog: to.name,
}),
})),
// Get the mobile time slots if a mobile provider number is available
...(mobileProviderNumber
? [
{
resultKey: "mobileTimeSlots",
promise: store.dispatch("getMobileTimeSlots", {
payload: { startDate, endDate, zipCode: serviceZipCode },
pageNameToLog: to.name,
}),
},
]
: []),
];
const resultMap = await settleAllPromises(promiseResultMap);
next((vm) => {
vm.setCmsContent(resultMap.cmsContent);
vm.inShopProvidersAndTimeslots = providers.map((provider, i) => ({
provider,
timeSlots: resultMap[`inshopTimeSlots_${i}`] ?? null,
}));
vm.mobileProviderAndTimeSlot = mobileProviderNumber
? {
providerNumber: mobileProviderNumber,
timeSlots: resultMap.mobileTimeSlots ?? null,
}
: null;
});
},
computed: {
serviceLocationText() {
return this.getCmsContent("ServiceLocationText", "Text");
},
availableDates() {
const inshopDates = this.inShopProvidersAndTimeslots.flatMap(({ timeSlots }) =>
(timeSlots?.days ?? []).map((d) => d.date)
);
const mobileDates = (this.mobileProviderAndTimeSlot?.timeSlots?.days ?? []).map(
(d) => d.date
);
return [...new Set([...inshopDates, ...mobileDates])].sort();
},
},
data() {
return {
selectedDate: null,
isLoadingDates: false,
datePickerStartDate: toDateString(0),
datePickerEndDate: toDateString(SCHEDULE_FETCH_DAYS - 1),
inShopProvidersAndTimeslots: [],
mobileProviderAndTimeSlot: null,
};
},
methods: {
arePagePrerequisitesValid() {
const order = store.getters.order;
const logQueue = [];
const serviceZip = hasServiceZipInfo(order, logQueue);
const glassPartsOrRepair = hasGlassPartsOrRepairInfo(order, logQueue);
const insuranceInfo = hasInsuranceInfo(order, logQueue);
const preReqResult = serviceZip && insuranceInfo && glassPartsOrRepair;
return preReqResult;
},
backButtonAction() {
const payment = store.getters.order.payment;
if (
payment?.isInsurance &&
(payment?.insuranceCoverage?.isVerified ||
store.getters.order.referralNumber.length === 6)
) {
navigateToHeritageFunnel({
shouldSaveSession: false,
pageNameToLog: this.pageName,
navType: "back",
});
} else {
this.$router.navigateWithoutSaving(
this.navigationScenarios.CLICKED_BACK,
this.pageName
);
}
},
async handleRequestMoreDates() {
this.isLoadingDates = true;
this.$refs.loadingModal.showModal();
// TODO: call getShopTimeSlots with the next date range and append results
// to this.availableDates and advance this.datePickerEndDate
console.log("handleRequestMoreDates");
this.isLoadingDates = false;
this.$refs.loadingModal.hideModal();
},
forwardButtonAction() {
const appointmentType = store.getters.order.serviceLocation.appointmentType; // TODO: remove this once we have a proper appointment type
if (appointmentType === AppointmentTypeStrings.MOBILE) {
this.$router.navigateWithSaving(
this.navigationScenarios.CLICKED_FORWARD_WITH_MOBILE_SERVICE,
this.pageName
);
} else {
this.$router.navigateWithSaving(
this.navigationScenarios.CLICKED_FORWARD,
this.pageName
);
}
},
},
components: {
funnelHeader,
funnelSubHeader,
navbar,
Form,
datePicker,
loadingModal,
},
};
</script>
<style lang="scss" scoped>
.dark-header {
color: $black;
}
</style>

View file

@ -109,6 +109,10 @@ export const routeData = {
name: "schedule",
path: "/schedule",
},
SCHEDULING: {
name: "scheduling",
path: "/scheduling",
},
MOBILE_DETAILS: {
name: "mobile-details",
path: "/mobile-details",

View file

@ -655,6 +655,27 @@ const routingTable = function () {
},
],
},
{
pageName: routeData.SCHEDULING.name,
maps: [
{
scenario: navigationScenarios.CLICKED_BACK,
destinationPageData: routeData.QUOTE,
},
{
scenario: navigationScenarios.CLICKED_FORWARD,
destinationPageData: routeData.CUSTOMER_DETAILS,
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_MOBILE_SERVICE,
destinationPageData: routeData.MOBILE_DETAILS,
},
{
scenario: navigationScenarios.ZIP_CODE_CHANGED_RECAL_ACK,
destinationPageData: routeData.SERVICE_ZIP,
},
],
},
{
pageName: routeData.MOBILE_DETAILS.name,
maps: [

View file

@ -0,0 +1,17 @@
import experimentMixin from "@/mixins/experiment-mixin.js";
import { experimentSettings } from "@/constants/experiments";
import { routeData } from "@/router/constants/routes";
export async function scheduleBeforeEnter(to, from) {
const isSchedulingEnabled = experimentMixin.methods.hasSettingEqualTo(
experimentSettings.USE_SCHEDULING_PAGE,
"true"
);
if (isSchedulingEnabled) {
return {
name: routeData.SCHEDULING.name,
replace: true,
};
}
}

View file

@ -14,6 +14,7 @@ import { paymentMethodBeforeEnter } from "@/router/methods/route-logic/payment-m
import { paymentBeforeEnter } from "@/router/methods/route-logic/payment";
import { serviceLocationBeforeEnter } from "@/router/methods/route-logic/service-location";
import { adyenReturnBeforeEnter } from "@/router/methods/route-logic/adyen-return";
import { scheduleBeforeEnter } from "@/router/methods/route-logic/schedule";
export const routes = [
// Non-virtual pages.
@ -32,7 +33,8 @@ export const routes = [
createRoute(routeData.QUOTE, quoteBeforeEnter),
createRoute(routeData.INSURANCE_COMPANY, insuranceCompanyBeforeEnter),
createRoute(routeData.SERVICE_LOCATION, serviceLocationBeforeEnter),
createRoute(routeData.SCHEDULE),
createRoute(routeData.SCHEDULE, scheduleBeforeEnter),
createRoute(routeData.SCHEDULING),
createRoute(routeData.CUSTOMER_DETAILS),
createRoute(routeData.PAYMENT_METHOD, paymentMethodBeforeEnter),
createRoute(routeData.PAYMENT, paymentBeforeEnter),