diff --git a/src/layouts/scheduling/date-picker/date-picker.spec.js b/src/layouts/scheduling/date-picker/date-picker.spec.js
index f64d27a12..d6ae3a6ae 100644
--- a/src/layouts/scheduling/date-picker/date-picker.spec.js
+++ b/src/layouts/scheduling/date-picker/date-picker.spec.js
@@ -133,9 +133,12 @@ describe("date-picker.vue", () => {
test("does not emit when a disabled card is clicked", async () => {
const wrapper = mountDesktop();
+ const emissionCountBeforeClick = (wrapper.emitted("update:modelValue") ?? []).length;
const disabledCard = wrapper.find(".date-picker__day-card--disabled");
await disabledCard.trigger("click");
- expect(wrapper.emitted("update:modelValue")).toBeFalsy();
+ expect((wrapper.emitted("update:modelValue") ?? []).length).toBe(
+ emissionCountBeforeClick
+ );
wrapper.unmount();
});
@@ -214,6 +217,76 @@ describe("date-picker.vue", () => {
});
});
+ describe("forward button disabled at 180-date limit", () => {
+ function generateDateRange(startStr, count) {
+ const dates = [];
+ const [y, m, d] = startStr.split("-").map(Number);
+ const cursor = new Date(y, m - 1, d);
+ for (let i = 0; i < count; i++) {
+ const yy = cursor.getFullYear();
+ const mm = String(cursor.getMonth() + 1).padStart(2, "0");
+ const dd = String(cursor.getDate()).padStart(2, "0");
+ dates.push(`${yy}-${mm}-${dd}`);
+ cursor.setDate(cursor.getDate() + 1);
+ }
+ return dates;
+ }
+
+ test("canGoForward is false when allDates.length >= 180 and the last date is in the current view", async () => {
+ const allDateValues = generateDateRange("2026-01-01", 180);
+ const wrapper = mountDesktop({
+ availableDates: allDateValues,
+ startDate: allDateValues[0],
+ endDate: allDateValues[allDateValues.length - 1],
+ });
+ // Desktop window size is 5; set windowStart to last window (index 175)
+ wrapper.vm.windowStart = 175;
+ await wrapper.vm.$nextTick();
+ expect(wrapper.vm.canGoForward).toBe(false);
+ wrapper.unmount();
+ });
+
+ test("forward nav button has disabled attribute when at the end of a 180-date range", async () => {
+ const allDateValues = generateDateRange("2026-01-01", 180);
+ const wrapper = mountDesktop({
+ availableDates: allDateValues,
+ startDate: allDateValues[0],
+ endDate: allDateValues[allDateValues.length - 1],
+ });
+ wrapper.vm.windowStart = 175;
+ await wrapper.vm.$nextTick();
+ const forwardBtn = wrapper.findAll(".date-picker__nav-btn")[1];
+ expect(forwardBtn.attributes("disabled")).toBeDefined();
+ wrapper.unmount();
+ });
+
+ test("canGoForward is true when allDates.length >= 180 but the last date is not yet in view", () => {
+ const allDateValues = generateDateRange("2026-01-01", 180);
+ const wrapper = mountDesktop({
+ availableDates: allDateValues,
+ startDate: allDateValues[0],
+ endDate: allDateValues[allDateValues.length - 1],
+ });
+ // windowStart = 0, first 5 of 180 dates are visible — last date is not in view
+ expect(wrapper.vm.canGoForward).toBe(true);
+ wrapper.unmount();
+ });
+
+ test("canGoForward remains true at the last window when allDates.length < 180", async () => {
+ const allDateValues = generateDateRange("2026-01-01", 10);
+ const wrapper = mountDesktop({
+ availableDates: allDateValues,
+ startDate: allDateValues[0],
+ endDate: allDateValues[allDateValues.length - 1],
+ });
+ // windowStart = 5 puts the last 5 dates (indices 5–9) in view
+ wrapper.vm.windowStart = 5;
+ await wrapper.vm.$nextTick();
+ expect(wrapper.vm.canGoForward).toBe(true);
+ 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
@@ -222,4 +295,225 @@ describe("date-picker.vue", () => {
wrapper.unmount();
});
});
+
+ describe("mounted initialization", () => {
+ test("emits first available date when mounted with no modelValue and dates present", () => {
+ const wrapper = mountDesktop();
+ expect(wrapper.emitted("update:modelValue")).toBeTruthy();
+ expect(wrapper.emitted("update:modelValue")[0][0]).toBe("2026-01-21");
+ wrapper.unmount();
+ });
+ });
+
+ describe("goBack auto-selection", () => {
+ test("emits update:modelValue with first available date in the new window", async () => {
+ // modelValue set → mounted() uses scroll path, no emission on mount
+ const wrapper = mountDesktop({
+ availableDates: ["2026-01-01", "2026-01-07"],
+ startDate: "2026-01-01",
+ endDate: "2026-01-10",
+ modelValue: "2026-01-07",
+ });
+ // "2026-01-07" is index 6 → mounted sets windowStart = Math.floor(6/5)*5 = 5
+ expect(wrapper.vm.windowStart).toBe(5);
+ expect(wrapper.emitted("update:modelValue")).toBeFalsy();
+ await wrapper.vm.goBack();
+ const emissions = wrapper.emitted("update:modelValue");
+ expect(emissions).toBeTruthy();
+ expect(emissions[emissions.length - 1][0]).toBe("2026-01-01");
+ wrapper.unmount();
+ });
+ });
+
+ describe("goForward pending flow", () => {
+ test("emits requestMoreDates when at the last window", async () => {
+ // 5 dates, desktop window size 5 → already at end on mount
+ const wrapper = mountDesktop({ modelValue: "2026-01-21" });
+ await wrapper.vm.goForward();
+ expect(wrapper.emitted("requestMoreDates")).toBeTruthy();
+ wrapper.unmount();
+ });
+
+ test("sets pendingAutoSelect when at the last window", async () => {
+ const wrapper = mountDesktop({ modelValue: "2026-01-21" });
+ await wrapper.vm.goForward();
+ expect(wrapper.vm.pendingAutoSelect).toBe(true);
+ wrapper.unmount();
+ });
+
+ test("emits update:modelValue with first available date in the new window when not at the end", async () => {
+ const wrapper = mountDesktop({
+ availableDates: ["2026-01-01", "2026-01-07"],
+ startDate: "2026-01-01",
+ endDate: "2026-01-10",
+ modelValue: "2026-01-01",
+ });
+ // windowStart = 0, not at end (0 + 5 < 10), no emission on mount
+ await wrapper.vm.goForward();
+ const emissions = wrapper.emitted("update:modelValue");
+ expect(emissions).toBeTruthy();
+ expect(emissions[emissions.length - 1][0]).toBe("2026-01-07");
+ wrapper.unmount();
+ });
+ });
+
+ describe("advanceWindowAndAutoSelect", () => {
+ test("advances windowStart by windowSize", () => {
+ const wrapper = mountDesktop({
+ availableDates: ["2026-01-01", "2026-01-07"],
+ startDate: "2026-01-01",
+ endDate: "2026-01-10",
+ modelValue: "2026-01-01",
+ });
+ expect(wrapper.vm.windowStart).toBe(0);
+ wrapper.vm.advanceWindowAndAutoSelect();
+ expect(wrapper.vm.windowStart).toBe(5);
+ wrapper.unmount();
+ });
+
+ test("caps windowStart at maxStart when already near the end", () => {
+ const wrapper = mountDesktop({
+ availableDates: ["2026-01-01", "2026-01-07"],
+ startDate: "2026-01-01",
+ endDate: "2026-01-10",
+ modelValue: "2026-01-07",
+ });
+ // mounted() sets windowStart = 5 (index 6, Math.floor(6/5)*5)
+ // maxStart = Math.max(0, 10 - 5) = 5 → min(5, 5+5) = 5
+ wrapper.vm.advanceWindowAndAutoSelect();
+ expect(wrapper.vm.windowStart).toBe(5);
+ wrapper.unmount();
+ });
+
+ test("emits update:modelValue with first available date in the advanced window", () => {
+ const wrapper = mountDesktop({
+ availableDates: ["2026-01-01", "2026-01-07"],
+ startDate: "2026-01-01",
+ endDate: "2026-01-10",
+ modelValue: "2026-01-01",
+ });
+ wrapper.vm.advanceWindowAndAutoSelect();
+ const emissions = wrapper.emitted("update:modelValue");
+ expect(emissions).toBeTruthy();
+ expect(emissions[emissions.length - 1][0]).toBe("2026-01-07");
+ wrapper.unmount();
+ });
+ });
+
+ describe("autoSelectFirstAvailable", () => {
+ test("emits update:modelValue with the first available date in the visible window", () => {
+ const wrapper = mountDesktop({ modelValue: "2026-01-21" });
+ wrapper.vm.autoSelectFirstAvailable();
+ const emissions = wrapper.emitted("update:modelValue");
+ expect(emissions[emissions.length - 1][0]).toBe("2026-01-21");
+ wrapper.unmount();
+ });
+
+ test("emits null when no available dates are visible", () => {
+ // availableDates outside the date range → all visible dates are unavailable
+ const wrapper = mountDesktop({
+ availableDates: ["9999-12-31"],
+ startDate: "2026-01-21",
+ endDate: "2026-01-25",
+ modelValue: "2026-01-21",
+ });
+ // modelValue set → mounted uses scroll path, no emission on mount
+ wrapper.vm.autoSelectFirstAvailable();
+ const emissions = wrapper.emitted("update:modelValue");
+ expect(emissions[emissions.length - 1][0]).toBeNull();
+ wrapper.unmount();
+ });
+ });
+
+ describe("initializeWindow", () => {
+ test("sets the initialized flag to true", async () => {
+ const wrapper = mountDesktop({ availableDates: [] });
+ expect(wrapper.vm.initialized).toBe(false);
+ await wrapper.setProps({ availableDates: AVAILABLE_DATES });
+ expect(wrapper.vm.initialized).toBe(true);
+ wrapper.unmount();
+ });
+
+ test("positions windowStart to the window containing the first available date", async () => {
+ const wrapper = mountDesktop({
+ availableDates: [],
+ startDate: "2026-01-01",
+ endDate: "2026-01-10",
+ });
+ // "2026-01-07" is index 6 → Math.floor(6/5)*5 = 5
+ await wrapper.setProps({ availableDates: ["2026-01-07"] });
+ expect(wrapper.vm.windowStart).toBe(5);
+ wrapper.unmount();
+ });
+
+ test("emits update:modelValue with the first available date", async () => {
+ const wrapper = mountDesktop({ availableDates: [] });
+ await wrapper.setProps({ availableDates: AVAILABLE_DATES });
+ expect(wrapper.emitted("update:modelValue")[0][0]).toBe("2026-01-21");
+ wrapper.unmount();
+ });
+
+ test("positions windowStart to the last window and does not emit when no dates are available", async () => {
+ const wrapper = mountDesktop({
+ availableDates: [],
+ startDate: "2026-01-01",
+ endDate: "2026-01-10",
+ });
+ // Dates are present but none fall in the Jan 1–10 range
+ await wrapper.setProps({ availableDates: ["9999-12-31"] });
+ // Math.max(0, 10 - 5) = 5
+ expect(wrapper.vm.windowStart).toBe(5);
+ expect(wrapper.emitted("update:modelValue")).toBeFalsy();
+ wrapper.unmount();
+ });
+
+ test("does not re-run after initialized flag is set", async () => {
+ const wrapper = mountDesktop({ availableDates: [] });
+ await wrapper.setProps({ availableDates: AVAILABLE_DATES });
+ const emissionCount = wrapper.emitted("update:modelValue").length;
+ // Add a newly available date within the range to trigger the watcher again
+ await wrapper.setProps({ availableDates: [...AVAILABLE_DATES, "2026-01-24"] });
+ // initialized=true and pendingAutoSelect=false → watcher takes no action
+ expect(wrapper.emitted("update:modelValue").length).toBe(emissionCount);
+ wrapper.unmount();
+ });
+ });
+
+ describe("allDates watcher", () => {
+ test("calls initializeWindow when dates first arrive with no modelValue set", async () => {
+ const wrapper = mountDesktop({ availableDates: [] });
+ // No dates → mounted() does nothing; initialized stays false
+ expect(wrapper.vm.initialized).toBe(false);
+ await wrapper.setProps({ availableDates: AVAILABLE_DATES });
+ expect(wrapper.vm.initialized).toBe(true);
+ expect(wrapper.emitted("update:modelValue")[0][0]).toBe("2026-01-21");
+ wrapper.unmount();
+ });
+
+ test("clears pendingAutoSelect and advances the window when pendingAutoSelect is true", async () => {
+ const wrapper = mountDesktop({
+ availableDates: [],
+ startDate: "2026-01-01",
+ endDate: "2026-01-10",
+ });
+ wrapper.vm.initialized = true;
+ wrapper.vm.pendingAutoSelect = true;
+ await wrapper.setProps({ availableDates: ["2026-01-01", "2026-01-07"] });
+ expect(wrapper.vm.pendingAutoSelect).toBe(false);
+ expect(wrapper.vm.windowStart).toBe(5);
+ const emissions = wrapper.emitted("update:modelValue");
+ expect(emissions[emissions.length - 1][0]).toBe("2026-01-07");
+ wrapper.unmount();
+ });
+
+ test("takes no action when allDates becomes empty", async () => {
+ const wrapper = mountDesktop({ availableDates: AVAILABLE_DATES });
+ // mounted() calls initializeWindow() → emits; record count
+ const emissionCount = wrapper.emitted("update:modelValue").length;
+ await wrapper.setProps({ availableDates: [] });
+ // watcher fires with newDates=[] → early return, nothing changes
+ expect(wrapper.emitted("update:modelValue").length).toBe(emissionCount);
+ wrapper.unmount();
+ });
+ });
});
diff --git a/src/layouts/scheduling/date-picker/date-picker.vue b/src/layouts/scheduling/date-picker/date-picker.vue
index 6b3fb7638..66dbe997a 100644
--- a/src/layouts/scheduling/date-picker/date-picker.vue
+++ b/src/layouts/scheduling/date-picker/date-picker.vue
@@ -6,13 +6,7 @@
aria-label="Previous dates"
@click="goBack">
-
@@ -38,16 +32,7 @@
:disabled="!canGoForward"
aria-label="Next dates"
@click="goForward">
-
-
+
@@ -73,6 +58,7 @@ const MONTH_ABBRS = [
const MOBILE_WINDOW_SIZE = 3;
const DESKTOP_WINDOW_SIZE = 5;
const DESKTOP_BREAKPOINT_PX = Breakpoints.MD;
+const MAX_DATE_RANGE = 180;
function toLocalDateString(date) {
const y = date.getFullYear();
@@ -86,6 +72,10 @@ function parseLocalDate(str) {
return new Date(year, month - 1, day);
}
+function getWindowSize() {
+ return window.innerWidth >= DESKTOP_BREAKPOINT_PX ? DESKTOP_WINDOW_SIZE : MOBILE_WINDOW_SIZE;
+}
+
export default {
name: "datePicker",
props: {
@@ -114,10 +104,9 @@ export default {
data() {
return {
windowStart: 0,
- windowSize:
- window.innerWidth >= DESKTOP_BREAKPOINT_PX
- ? DESKTOP_WINDOW_SIZE
- : MOBILE_WINDOW_SIZE,
+ windowSize: getWindowSize(),
+ pendingAutoSelect: false,
+ initialized: false,
};
},
computed: {
@@ -151,40 +140,92 @@ export default {
return this.windowStart > 0;
},
canGoForward() {
- return !this.isLoadingDates && this.allDates.length > 0;
+ if (this.isLoadingDates || !this.allDates.length) return false;
+ const atEnd = this.windowStart + this.windowSize >= this.allDates.length;
+ return !(atEnd && this.allDates.length >= MAX_DATE_RANGE);
+ },
+ },
+ watch: {
+ allDates(newDates) {
+ if (!newDates.length) return;
+
+ if (!this.initialized && !this.modelValue) {
+ this.initialize();
+ return;
+ }
+ // Pending auto select is set when the user is at the end of the dates and the next page of dates is loaded.
+ if (this.pendingAutoSelect) {
+ this.pendingAutoSelect = false;
+ this.advanceWindowAndAutoSelect();
+ }
},
},
mounted() {
this.updateWindowSize();
window.addEventListener("resize", this.updateWindowSize);
- if (this.modelValue && this.allDates.length) {
+ if (!this.allDates.length) return;
+ // If a model value is set, we need to set the window start to the index of the model value.
+ // Otherwise, we need to initialize the window.
+ if (this.modelValue) {
const index = this.allDates.findIndex((d) => d.value === this.modelValue);
- if (index !== -1) {
- this.windowStart = Math.floor(index / this.windowSize) * this.windowSize;
- }
+ if (index !== -1) this.windowStart = this.getWindowStartFromIndex(index);
+ } else {
+ this.initialize();
}
+ this.initialized = true;
},
beforeUnmount() {
window.removeEventListener("resize", this.updateWindowSize);
},
methods: {
updateWindowSize() {
- this.windowSize =
- window.innerWidth >= DESKTOP_BREAKPOINT_PX
- ? DESKTOP_WINDOW_SIZE
- : MOBILE_WINDOW_SIZE;
+ this.windowSize = getWindowSize();
+ },
+ navIconSrc(enabled) {
+ return enabled
+ ? require("@/assets/img/icons/chevron-right-blue.svg")
+ : require("@/assets/img/icons/chevron-right-grey.svg");
+ },
+ getWindowStartFromIndex(index) {
+ return Math.floor(index / this.windowSize) * this.windowSize;
},
goBack() {
this.windowStart = Math.max(0, this.windowStart - this.windowSize);
+ this.autoSelectFirstAvailable();
},
goForward() {
if (this.isLoadingDates) return;
const isAtEnd = this.windowStart + this.windowSize >= this.allDates.length;
if (isAtEnd) {
this.$emit("requestMoreDates");
+ this.pendingAutoSelect = true;
} else {
- const maxStart = Math.max(0, this.allDates.length - this.windowSize);
- this.windowStart = Math.min(maxStart, this.windowStart + this.windowSize);
+ this.advanceWindowAndAutoSelect();
+ }
+ },
+ advanceWindowAndAutoSelect() {
+ const maxStart = Math.max(0, this.allDates.length - this.windowSize);
+ this.windowStart = Math.min(maxStart, this.windowStart + this.windowSize);
+ this.autoSelectFirstAvailable();
+ },
+ autoSelectFirstAvailable() {
+ // Finds the first available date in the visible dates and emits the value.
+ const first = this.visibleDates.find((d) => d.isAvailable);
+ this.$emit("update:modelValue", first ? first.value : null);
+ },
+ initialize() {
+ this.initialized = true;
+ if (this.modelValue !== null) return;
+
+ const firstAvailable = this.allDates.find((d) => d.isAvailable);
+ if (firstAvailable) {
+ // If an available date is found, set the window start ensures the first available date is visible.
+ const index = this.allDates.indexOf(firstAvailable);
+ this.windowStart = this.getWindowStartFromIndex(index);
+ this.$emit("update:modelValue", firstAvailable.value);
+ } else {
+ // If no available dates are found, set the window start to the last window.
+ this.windowStart = Math.max(0, this.allDates.length - this.windowSize);
}
},
selectDate(date) {
@@ -226,7 +267,7 @@ export default {
}
&__track {
- gap: 0.5rem;
+ gap: 0.25rem;
}
&__day-card {
@@ -267,18 +308,19 @@ export default {
}
}
- &__day-abbr {
- font-family: $font-family-sans-serif-semibold;
- font-size: $font-size-12;
+ &__day-abbr,
+ &__day-date {
+ font-family: $font-family-sans-serif-bold;
+ font-size: $font-size-14;
color: $blue;
+ }
+
+ &__day-abbr {
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;
}