fix(vacay): open on the current year instead of the last one added (#2056)

The year list comes back sorted, so picking its last element meant Vacay always
landed on the highest year — add 2030 to plan ahead and every visit from then on
opened 2030. It now opens the leave-year period today falls into, falls back to
the closest year the plan has when that period has none, and keeps the year the
viewer is on when a live update reloads the list.
This commit is contained in:
Maurice
2026-08-27 10:55:27 +02:00
committed by GitHub
parent d194d2cf6f
commit f61bab3ffd
4 changed files with 114 additions and 11 deletions
+58 -1
View File
@@ -1,4 +1,4 @@
// FE-STORE-VCY-001 to FE-STORE-VCY-018
// FE-STORE-VCY-001 to FE-STORE-VCY-023
// Complements tests/unit/stores/vacayStore.test.ts: optimistic toggles and their
// rollbacks, the leave-year window plumbing and the holiday-marker merge rules.
import { describe, it, expect, beforeEach, vi } from 'vitest';
@@ -366,3 +366,60 @@ describe('vacayStore holiday markers', () => {
expect(useVacayStore.getState().holidays).toEqual({});
});
});
describe('vacayStore year selection', () => {
it('FE-STORE-VCY-019: the first load opens on the current period, not on the highest year', async () => {
const current = new Date().getFullYear();
server.use(http.get('/api/addons/vacay/years', () =>
HttpResponse.json({ years: [current - 1, current, current + 4] })));
await useVacayStore.getState().loadYears();
expect(useVacayStore.getState().selectedYear).toBe(current);
});
it('FE-STORE-VCY-020: with no year for the current period the closest one is opened', async () => {
const current = new Date().getFullYear();
server.use(http.get('/api/addons/vacay/years', () =>
HttpResponse.json({ years: [current - 3, current + 1] })));
await useVacayStore.getState().loadYears();
expect(useVacayStore.getState().selectedYear).toBe(current + 1);
});
it('FE-STORE-VCY-021: a reload keeps the year the viewer is looking at', async () => {
const current = new Date().getFullYear();
useVacayStore.setState({ years: [current, current + 1], selectedYear: current + 1 });
server.use(http.get('/api/addons/vacay/years', () =>
HttpResponse.json({ years: [current, current + 1] })));
await useVacayStore.getState().loadYears();
expect(useVacayStore.getState().selectedYear).toBe(current + 1);
});
it('FE-STORE-VCY-022: a reload that lost the selected year falls back to the current period', async () => {
const current = new Date().getFullYear();
useVacayStore.setState({ years: [current, current + 1], selectedYear: current + 1 });
server.use(http.get('/api/addons/vacay/years', () =>
HttpResponse.json({ years: [current] })));
await useVacayStore.getState().loadYears();
expect(useVacayStore.getState().selectedYear).toBe(current);
});
it('FE-STORE-VCY-023: removing the open year drops back to the current period', async () => {
const current = new Date().getFullYear();
useVacayStore.setState({ years: [current, current + 1], selectedYear: current + 1 });
server.use(
http.delete('/api/addons/vacay/years/:year', () => HttpResponse.json({ years: [current] })),
http.get('/api/addons/vacay/stats/:year', () => HttpResponse.json({ stats: [] })),
);
await useVacayStore.getState().removeYear(current + 1);
expect(useVacayStore.getState().selectedYear).toBe(current);
});
});
+11 -10
View File
@@ -7,7 +7,7 @@ import type {
VacayShareOutgoing, VacayShareIncoming, SharedVacayCalendar, VacayYearSettings,
} from '../types'
import { isSchoolHolidayCountrySupported } from '../vacay/schoolHolidayCountries'
import { DEFAULT_YEAR_SETTINGS, currentPeriodYear, inGridWindow, windowCalendarYears } from '../vacay/yearWindow'
import { DEFAULT_YEAR_SETTINGS, defaultPeriodYear, inGridWindow, windowCalendarYears } from '../vacay/yearWindow'
import type {
VacaySetColorRequest, VacayInviteRequest, VacayInviteActionRequest,
VacayAddYearRequest, VacayToggleEntryRequest, VacayCompanyHolidayRequest,
@@ -302,12 +302,15 @@ export const useVacayStore = create<VacayState>((set, get) => ({
loadYears: async () => {
const data = await api.getYears()
set({ years: data.years })
if (data.years.length > 0) {
set({ selectedYear: data.years[data.years.length - 1] })
} else {
set({ selectedYear: currentPeriodYear(get().yearSettings) })
}
// Opening Vacay lands on the period we are in, not on whichever year sorts
// last. A reload (invite, live settings update) keeps the year the viewer is
// looking at as long as it still exists — only the first load picks one.
const previous = get().selectedYear
const keepSelection = get().years.length > 0 && data.years.includes(previous)
set({
years: data.years,
selectedYear: keepSelection ? previous : defaultPeriodYear(data.years, get().yearSettings),
})
},
addYear: async (year: number) => {
@@ -320,9 +323,7 @@ export const useVacayStore = create<VacayState>((set, get) => ({
const data = await api.removeYear(year)
const updates: Partial<VacayState> = { years: data.years }
if (get().selectedYear === year) {
updates.selectedYear = data.years.length > 0
? data.years[data.years.length - 1]
: currentPeriodYear(get().yearSettings)
updates.selectedYear = defaultPeriodYear(data.years, get().yearSettings)
}
set(updates)
await get().loadStats()
+29
View File
@@ -2,6 +2,7 @@ import { describe, it, expect } from 'vitest'
import {
DEFAULT_YEAR_SETTINGS,
currentPeriodYear,
defaultPeriodYear,
gridWindow,
inGridWindow,
resolveYearWindow,
@@ -125,3 +126,31 @@ describe('currentPeriodYear', () => {
expect(currentPeriodYear(fiscal(7), new Date(2026, 6, 1))).toBe(2026)
})
})
describe('defaultPeriodYear', () => {
// FE-VACAY-YEARWINDOW-014
it('picks the current period when the plan has it', () => {
const today = new Date(2026, 2, 15)
expect(defaultPeriodYear([2024, 2025, 2026, 2030], DEFAULT_YEAR_SETTINGS, today)).toBe(2026)
})
// FE-VACAY-YEARWINDOW-015
it('falls back to the closest year and prefers the coming one on a tie', () => {
const today = new Date(2026, 2, 15)
expect(defaultPeriodYear([2022, 2024], DEFAULT_YEAR_SETTINGS, today)).toBe(2024)
expect(defaultPeriodYear([2028, 2030], DEFAULT_YEAR_SETTINGS, today)).toBe(2028)
expect(defaultPeriodYear([2024, 2028], DEFAULT_YEAR_SETTINGS, today)).toBe(2028)
})
// FE-VACAY-YEARWINDOW-016
it('is the current period for an empty plan', () => {
expect(defaultPeriodYear([], DEFAULT_YEAR_SETTINGS, new Date(2026, 2, 15))).toBe(2026)
})
// FE-VACAY-YEARWINDOW-017
it('follows a shifted leave year rather than the calendar year', () => {
const today = new Date(2026, 2, 15)
expect(defaultPeriodYear([2025, 2026], fiscal(7), today)).toBe(2025)
expect(defaultPeriodYear([2026], fiscal(7), today)).toBe(2026)
})
})
+16
View File
@@ -100,3 +100,19 @@ export function currentPeriodYear(s: VacayYearSettings = DEFAULT_YEAR_SETTINGS,
const iso = `${year}-${pad2(today.getMonth() + 1)}-${pad2(today.getDate())}`
return iso < resolveYearWindow(year, s).start ? year - 1 : year
}
/**
* The year Vacay opens on. The period we are actually in wins; adding 2030 to plan
* ahead should not make 2030 the year the page lands on from then on. When that
* period has no year yet, the closest one the plan does have is used, and a tie
* goes to the later year because planning looks forward.
*/
export function defaultPeriodYear(years: number[], s: VacayYearSettings = DEFAULT_YEAR_SETTINGS, today = new Date()): number {
const current = currentPeriodYear(s, today)
if (years.length === 0 || years.includes(current)) return current
return years.reduce((best, year) => {
const distance = Math.abs(year - current)
const bestDistance = Math.abs(best - current)
return distance < bestDistance || (distance === bestDistance && year > best) ? year : best
})
}