Free days, vacation forfeiture, and global auto-replanning
Free days (Sat/Sun/red days, subject to DayOverride) can never be vacation, and can only be parental leave when the workspace opts in via take_pl_on_red_days. The allocator already honoured this; the hand-pin path bypassed it entirely. - PlanningService gains optional workspace_repo/day_override_repo deps and _reject_illegal_free_day, enforced in add_planned_day and update. Reuses allocation_service.is_free_day rather than re-deriving it, and no-ops when the deps are unset so existing callers are unaffected. - update_planned_day had no ValidationError handler at all, so a validation failure there 500'd instead of 422'ing. Fixed. - Calendar filters the pin write loop by the day's isFree; the modal explains free days stay off, and that pinning opts a day out of automatic re-planning. Vacation days at risk of forfeiture were computed and sent over the wire but never rendered: an at-risk column with the age-out/over-cap split, and a dashboard key figure. PlanProposal.warnings was likewise parsed but rendered nowhere, for all three kinds. Adds PlanWarningCard in a Risks section above the tips, with parent names resolved and dates formatted. POST /api/plan/sync now fires from the workspace layer on every plan-input change rather than only from Calendar, so a Settings edit or a pinned-day save re-plans day types instead of merely refetching. Overlapping bumps coalesce into one trailing run; a rejection cannot wedge the in-flight guard. eviction_preview was silently dropped by the frontend despite always being on the wire, and is now surfaced through context. Verified: backend 890 passed (91.49% coverage), frontend 1297 passed, ruff/eslint/tsc clean. Browser-checked in Swedish and English. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVQx4NUvPbJhn6UcvRd2QY
This commit is contained in:
@@ -3,9 +3,9 @@ import { render, fireEvent, screen, act } from '@testing-library/react'
|
||||
import { VacationDaysTable, type VacationDaysTableRow } from './VacationDaysTable'
|
||||
|
||||
const ROWS: VacationDaysTableRow[] = [
|
||||
{ year: 2026, totalDays: 10, savedDaysIn: 2 },
|
||||
{ year: 2027, totalDays: 25, savedDaysIn: 0 },
|
||||
{ year: 2028, totalDays: 25, savedDaysIn: 0 },
|
||||
{ year: 2026, totalDays: 10, savedDaysIn: 2, expiring: 0, expiringAge: 0, expiringOverCap: 0 },
|
||||
{ year: 2027, totalDays: 25, savedDaysIn: 0, expiring: 0, expiringAge: 0, expiringOverCap: 0 },
|
||||
{ year: 2028, totalDays: 25, savedDaysIn: 0, expiring: 0, expiringAge: 0, expiringOverCap: 0 },
|
||||
]
|
||||
|
||||
function renderTable(
|
||||
@@ -184,10 +184,43 @@ describe('VacationDaysTable', () => {
|
||||
})
|
||||
|
||||
it('shows an assumed badge for a year with no entered data (§3.2)', () => {
|
||||
const rows = [...ROWS, { year: 2029, totalDays: 25, savedDaysIn: 0, isAssumed: true }]
|
||||
const rows = [
|
||||
...ROWS,
|
||||
{ year: 2029, totalDays: 25, savedDaysIn: 0, isAssumed: true, expiring: 0, expiringAge: 0, expiringOverCap: 0 },
|
||||
]
|
||||
renderTable(rows, vi.fn().mockResolvedValue(undefined))
|
||||
|
||||
expect(screen.getByTestId('vacation-assumed-badge-2029')).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('vacation-assumed-badge-2026')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows the at-risk figure in neutral ink when nothing is expiring', () => {
|
||||
renderTable(ROWS, vi.fn().mockResolvedValue(undefined))
|
||||
const atRisk = screen.getByTestId('vacation-at-risk-2026')
|
||||
expect(atRisk).toHaveTextContent('0')
|
||||
expect(atRisk).not.toHaveClass('vacation-rows__at-risk-value--danger')
|
||||
})
|
||||
|
||||
it('shows the at-risk figure in danger ink when days are expiring, with a tooltip splitting the cause', () => {
|
||||
const rows = [
|
||||
{ year: 2026, totalDays: 30, savedDaysIn: 8, expiring: 5, expiringAge: 2, expiringOverCap: 3 },
|
||||
...ROWS.slice(1),
|
||||
]
|
||||
renderTable(rows, vi.fn().mockResolvedValue(undefined))
|
||||
const atRisk = screen.getByTestId('vacation-at-risk-2026')
|
||||
expect(atRisk).toHaveTextContent('5')
|
||||
expect(atRisk).toHaveClass('vacation-rows__at-risk-value--danger')
|
||||
expect(atRisk.title).toContain('2')
|
||||
expect(atRisk.title).toContain('3')
|
||||
})
|
||||
|
||||
it('shows only the relevant cause in the at-risk tooltip when the other is zero', () => {
|
||||
const rows = [
|
||||
{ year: 2026, totalDays: 30, savedDaysIn: 8, expiring: 4, expiringAge: 4, expiringOverCap: 0 },
|
||||
...ROWS.slice(1),
|
||||
]
|
||||
renderTable(rows, vi.fn().mockResolvedValue(undefined))
|
||||
const atRisk = screen.getByTestId('vacation-at-risk-2026')
|
||||
expect(atRisk.title).toBe('4 dagar har passerat 5-årsgränsen för att spara semester')
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user