mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
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:
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user