Let vacation years go back to the child's birth year
Settings only offered vacation-day rows from the current year forward, while analytics ran its balance series from the child's birth year. Every year in the gap had no vacation_days row, so the series assumed a full statutory 25 earned with no use and banked earned - floor of it forward — phantom saved days the user had no row to correct. Settings now offers editable rows back to the birth year, and a past year with no row assumes only the floor was earned, so it banks nothing. A past year the user did enter is untouched by that rule. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -90,18 +90,30 @@ function describePartnerError(err: unknown, t: (key: TranslationKey, params?: Re
|
||||
return err instanceof Error ? err.message : fallback
|
||||
}
|
||||
|
||||
/** Current year through (at minimum) the year the child turns 4. */
|
||||
/**
|
||||
* The child's birth year (or the current year, if earlier/no birth date)
|
||||
* through — at minimum — the year the child turns 4.
|
||||
*
|
||||
* Starting at the birth year, not `currentYear`, lets Settings offer an
|
||||
* editable vacation-days row for every past year too — see
|
||||
* `AnalyticsService._vacation_year_series`'s `current_year` rule, which
|
||||
* stops assuming the full statutory default for a past year with no row
|
||||
* precisely so a real figure can be entered here instead.
|
||||
*/
|
||||
function computeVacationYearRange(birthDate: string | undefined, currentYear: number): number[] {
|
||||
let startYear = currentYear
|
||||
let endYear = currentYear
|
||||
if (birthDate) {
|
||||
const fourthBirthday = new Date(birthDate)
|
||||
if (!isNaN(fourthBirthday.getTime())) {
|
||||
const birth = new Date(birthDate)
|
||||
if (!isNaN(birth.getTime())) {
|
||||
startYear = Math.min(startYear, birth.getFullYear())
|
||||
const fourthBirthday = new Date(birthDate)
|
||||
fourthBirthday.setFullYear(fourthBirthday.getFullYear() + 4)
|
||||
endYear = Math.max(endYear, fourthBirthday.getFullYear())
|
||||
}
|
||||
}
|
||||
const years: number[] = []
|
||||
for (let year = currentYear; year <= endYear; year++) {
|
||||
for (let year = startYear; year <= endYear; year++) {
|
||||
years.push(year)
|
||||
}
|
||||
return years
|
||||
@@ -352,6 +364,15 @@ export function Settings(): ReactNode {
|
||||
() => computeVacationYearRange(selectedChild?.birth_date, currentYear),
|
||||
[selectedChild?.birth_date, currentYear],
|
||||
)
|
||||
// Preschool planning-day rows only ever make sense current-year forward
|
||||
// (§Task 1 preschool reserve is a forecast, not a historical record) —
|
||||
// `vacationYears` above was widened back to the child's birth year for
|
||||
// the vacation-days table only, so this block gets its own slice rather
|
||||
// than sharing it.
|
||||
const preschoolYears = useMemo(
|
||||
() => vacationYears.filter((year) => year >= currentYear),
|
||||
[vacationYears, currentYear],
|
||||
)
|
||||
// Keyed on (personalDataUuid, year), NOT year alone — with more than one
|
||||
// workspace member, matching on year alone would silently pick up
|
||||
// whichever member's row for that year happened to come first in
|
||||
@@ -366,21 +387,29 @@ export function Settings(): ReactNode {
|
||||
// — the at-risk figures then default to 0 rather than block the
|
||||
// rest of this editable row on a supplementary fetch.
|
||||
const vacationBalance = projection?.yearBalances.find((b) => b.year === year)?.vacation
|
||||
// No row entered: fall back to the SAME figure analytics assumed
|
||||
// for this year (`AnalyticsService._vacation_year_series`'s
|
||||
// `earned` — statutory default for a current/future year, the
|
||||
// floor for a past one) rather than re-deriving that rule here.
|
||||
// Only falls back further to the statutory default when analytics
|
||||
// hasn't loaded yet, matching the pre-existing no-data behaviour.
|
||||
const fallbackTotalDays = vacationBalance?.earned ?? DEFAULT_VACATION_DAYS
|
||||
return {
|
||||
year,
|
||||
totalDays: existing?.totalDays ?? DEFAULT_VACATION_DAYS,
|
||||
totalDays: existing?.totalDays ?? fallbackTotalDays,
|
||||
savedDaysIn: existing?.savedDaysIn ?? 0,
|
||||
// §3.2 — no vacation_days row for this year yet: the statutory
|
||||
// §3.2 — no vacation_days row for this year yet: an assumed
|
||||
// default is shown, not an entered value. Mirrors the backend's
|
||||
// `VacationBalanceResponse.is_assumed` for the propose-time table.
|
||||
isAssumed: existing === undefined,
|
||||
isPast: year < currentYear,
|
||||
expiring: vacationBalance?.expiring ?? 0,
|
||||
expiringAge: vacationBalance?.expiringAge ?? 0,
|
||||
expiringOverCap: vacationBalance?.expiringOverCap ?? 0,
|
||||
}
|
||||
})
|
||||
}),
|
||||
[orderedMembers, vacationYears, vacDays, analytics],
|
||||
[orderedMembers, vacationYears, vacDays, analytics, currentYear],
|
||||
)
|
||||
|
||||
if (isLoading) {
|
||||
@@ -655,7 +684,7 @@ export function Settings(): ReactNode {
|
||||
<h4>{t('settings.preschoolPerYearTitle')}</h4>
|
||||
<p className="settings-panel__help">{t('settings.preschoolPerYearHelp')}</p>
|
||||
<div className="preschool-year-rows" data-testid="preschool-year-rows">
|
||||
{vacationYears.map((year) => (
|
||||
{preschoolYears.map((year) => (
|
||||
<PreschoolPlanningYearRow
|
||||
key={year}
|
||||
childId={selectedChildId}
|
||||
|
||||
Reference in New Issue
Block a user