Let the planner fund a hand-drawn period, and remove one at a time

Three changes to calendar editing, too interleaved in the same files to
land separately.

Giving the same days to the second parent rewrote the first parent's
days instead of adding their own. The save looked the existing day up
against whoever was logged in rather than the parent being saved for, so
parent A's leave turned into parent B's vacation. The parent choice now
lives with the lookup, and two parents on the same dates produce two
rows and two bands.

Drawing a period by hand meant naming a day type up front. It now asks
who is off and lets the planner fund it, as a one-off period the sync
places, with double days offered when both parents are off and priced
honestly in the hint. Pinning an exact day type is still there, moved
behind Advanced, and editing a pinned day goes straight back to it.

Removing a period offered only the two extremes. There are now three
choices: this day, this year's period, or every year. Taking one year
out of a repeating period narrows it, or splits it in two when the year
is in the middle; where a scope cannot be expressed it is disabled with
a reason rather than doing something surprising.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ei7gCZzdd751uZtPzb1Pep
This commit is contained in:
2026-09-19 15:14:24 +02:00
parent 11f2a17b43
commit 35e479a005
17 changed files with 1514 additions and 750 deletions
+381 -16
View File
@@ -89,12 +89,60 @@ vi.mock('@/components/modals/PlannedDayModal', () => ({
data-parents-count={props.parents.length}
data-is-range-mode={props.isRangeMode ? 'yes' : 'no'}
data-day-count={props.dayCount ?? ''}
data-selected-parent-id={props.selectedParentId}
>
{/* §todo10/11 default path — the planner decides funding. */}
<button
data-testid="modal-save"
onClick={() => void props.onSave({ dayType: 'vacation', parentId: props.selectedParentId, isDubbeldag: false })}
data-testid="modal-save-wish-either"
onClick={() => void props.onSaveWish({ parentMode: 'either', parentPersonalDataUuid: null, wantsDubbeldag: false })}
>
save
save wish (either)
</button>
<button
data-testid="modal-save-wish-both"
onClick={() => void props.onSaveWish({ parentMode: 'both', parentPersonalDataUuid: null, wantsDubbeldag: false })}
>
save wish (both)
</button>
<button
data-testid="modal-save-wish-both-dubbeldag"
onClick={() => void props.onSaveWish({ parentMode: 'both', parentPersonalDataUuid: null, wantsDubbeldag: true })}
>
save wish (both, dubbeldag)
</button>
<button
data-testid="modal-save-wish-specific"
onClick={() => void props.onSaveWish({ parentMode: 'specific', parentPersonalDataUuid: 'pd-b', wantsDubbeldag: false })}
>
save wish (specific)
</button>
{/* Deliberately sends `wantsDubbeldag: true` alongside `parentMode:
'specific'` — the real modal itself disables the checkbox outside
'both' mode, but Calendar.tsx's `handleSaveWish` must not trust
that alone; it re-gates on `parentMode` before sending the wish. */}
<button
data-testid="modal-save-wish-specific-dubbeldag-attempt"
onClick={() => void props.onSaveWish({ parentMode: 'specific', parentPersonalDataUuid: 'pd-b', wantsDubbeldag: true })}
>
save wish (specific, dubbeldag attempt)
</button>
{/* The "Advanced" escape hatch — pins an exact day type for whichever
parent `selectedParentId` currently names (Calendar.tsx's lifted
`advancedParentId`, updated live via `onParentChange`). */}
<button
data-testid="modal-save-pinned"
onClick={() => void props.onSavePinned({ dayType: 'vacation', parentId: props.selectedParentId, isDubbeldag: false })}
>
save pinned (vacation)
</button>
<button
data-testid="modal-save-pinned-sjukpenning"
onClick={() => void props.onSavePinned({ dayType: 'sjukpenning', parentId: props.selectedParentId, isDubbeldag: false })}
>
save pinned (sjukpenning)
</button>
<button data-testid="modal-change-parent-b" onClick={() => props.onParentChange?.('pd-b')}>
switch advanced parent to B
</button>
{props.onDelete && (
<button data-testid="modal-delete" onClick={() => void props.onDelete?.()}>
@@ -129,6 +177,7 @@ vi.mock('@/components/calendar/PeriodPopover', () => ({
parentPersonalDataUuid: null,
recurrence: 'yearly',
lastYear: null,
wantsDubbeldag: false,
} satisfies PeriodPopoverSavePayload)
}
>
@@ -145,13 +194,21 @@ vi.mock('@/components/calendar/PeriodPopover', () => ({
parentPersonalDataUuid: null,
recurrence: 'once',
lastYear: null,
wantsDubbeldag: false,
} satisfies PeriodPopoverSavePayload)
}
>
save once
</button>
<button data-testid="period-delete" onClick={() => void props.onDelete()}>
delete
{/* §todo9 — the three-way remove. */}
<button data-testid="period-delete-day" onClick={() => void props.onDelete('day')}>
delete (day)
</button>
<button data-testid="period-delete-year" onClick={() => void props.onDelete('year')}>
delete (year)
</button>
<button data-testid="period-delete-all-years" onClick={() => void props.onDelete('allYears')}>
delete (all years)
</button>
<button data-testid="period-close" onClick={props.onClose}>
close
@@ -450,7 +507,7 @@ describe('Calendar', () => {
const calendarDays = screen.getAllByTestId('calendar-day')
const currentMonthDay = calendarDays.find((el) => !el.classList.contains('calendar-day--disabled'))
fireEvent.click(currentMonthDay!)
fireEvent.click(screen.getByTestId('modal-save'))
fireEvent.click(screen.getByTestId('modal-save-pinned'))
await waitFor(() => expect(add).toHaveBeenCalledWith(expect.objectContaining({ is_pinned: true })))
})
@@ -469,7 +526,7 @@ describe('Calendar', () => {
const calendarDays = screen.getAllByTestId('calendar-day')
const todayCell = calendarDays.find((el) => el.getAttribute('aria-label')?.startsWith(today))
fireEvent.click(todayCell!)
fireEvent.click(screen.getByTestId('modal-save'))
fireEvent.click(screen.getByTestId('modal-save-pinned'))
await waitFor(() =>
expect(update).toHaveBeenCalledWith('pd-1', expect.objectContaining({ is_pinned: true })),
@@ -484,7 +541,7 @@ describe('Calendar', () => {
const calendarDays = screen.getAllByTestId('calendar-day')
const currentMonthDay = calendarDays.find((el) => !el.classList.contains('calendar-day--disabled'))
fireEvent.click(currentMonthDay!)
fireEvent.click(screen.getByTestId('modal-save'))
fireEvent.click(screen.getByTestId('modal-save-pinned'))
// The mocked modal doesn't surface `error`, but the save must not
// crash and must leave the modal open with the day still selected.
@@ -740,7 +797,7 @@ describe('Calendar', () => {
render(<Calendar />)
fireEvent.click(screen.getAllByTestId(/^calendar-band-/)[0]!)
fireEvent.click(screen.getByTestId('period-delete'))
fireEvent.click(screen.getByTestId('period-delete-all-years'))
await waitFor(() => expect(remove).toHaveBeenCalledWith('lw-1'))
await waitFor(() => expect(sync).toHaveBeenCalled())
@@ -762,7 +819,7 @@ describe('Calendar', () => {
render(<Calendar />)
fireEvent.click(screen.getAllByTestId(/^calendar-band-/)[0]!)
fireEvent.click(screen.getByTestId('period-delete'))
fireEvent.click(screen.getByTestId('period-delete-all-years'))
const undoButton = await screen.findByTestId('calendar-toast-undo')
fireEvent.click(undoButton)
@@ -789,7 +846,7 @@ describe('Calendar', () => {
render(<Calendar />)
fireEvent.click(screen.getAllByTestId(/^calendar-band-/)[0]!)
fireEvent.click(screen.getByTestId('period-delete'))
fireEvent.click(screen.getByTestId('period-delete-all-years'))
await waitFor(() => expect(remove).toHaveBeenCalledWith('lw-1'))
await waitFor(() => expect(screen.getByTestId('calendar-toast')).toBeInTheDocument())
@@ -810,7 +867,7 @@ describe('Calendar', () => {
render(<Calendar />)
fireEvent.click(screen.getAllByTestId(/^calendar-band-/)[0]!)
fireEvent.click(screen.getByTestId('period-delete'))
fireEvent.click(screen.getByTestId('period-delete-year'))
await waitFor(() => expect(remove).toHaveBeenCalledWith('pd-1'))
// Sync was still called once, from the initial auto-sync on mount —
@@ -1220,7 +1277,7 @@ describe('Calendar', () => {
render(<Calendar />)
fireEvent.click(screen.getAllByTestId(/^calendar-band-/)[0]!)
fireEvent.click(screen.getByTestId('period-delete'))
fireEvent.click(screen.getByTestId('period-delete-year'))
const undoButton = await screen.findByTestId('calendar-toast-undo')
fireEvent.click(undoButton)
@@ -1460,7 +1517,7 @@ describe('Calendar', () => {
render(<Calendar />)
fireEvent.click(screen.getAllByTestId(/^calendar-band-/)[0]!)
fireEvent.click(screen.getByTestId('period-delete'))
fireEvent.click(screen.getByTestId('period-delete-all-years'))
await waitFor(() => expect(screen.getByTestId('period-popover')).toHaveAttribute('data-error', 'cannot delete'))
})
@@ -1530,7 +1587,7 @@ describe('Calendar', () => {
// requires a re-render for the new hook return value to take effect.
vi.mocked(usePlannedDays).mockReturnValue(makePlannedDaysResult({ data: [] }))
rerender(<Calendar />)
fireEvent.click(screen.getByTestId('period-delete'))
fireEvent.click(screen.getByTestId('period-delete-year'))
expect(screen.queryByTestId('calendar-toast')).not.toBeInTheDocument()
})
@@ -1585,7 +1642,7 @@ describe('Calendar', () => {
const modal = await screen.findByTestId('modal')
expect(modal).toHaveAttribute('data-day-count', '3')
fireEvent.click(screen.getByTestId('modal-save'))
fireEvent.click(screen.getByTestId('modal-save-pinned'))
// The pre-existing day is updated (pinned), the other two are added
// (also pinned) — never a LeaveWish, never a sync.
@@ -1676,4 +1733,312 @@ describe('Calendar', () => {
expect(reservedHeights[0]).not.toBe('0px')
})
})
describe('todo.md #14 — two parents cannot both be given the same days', () => {
it('saving the pinned/advanced path targets the modal-selected parent, never the logged-in one', async () => {
const today = todayDateString()
const add = vi.fn().mockResolvedValue(makePlannedLeaveDay())
const update = vi.fn().mockResolvedValue(makePlannedLeaveDay())
vi.mocked(useWorkspaceMembers).mockReturnValue(
makeWorkspaceMembersResult({
data: [makePersonalData({ uuid: 'person-1', name: 'Förälder A' }), makePersonalData({ uuid: 'pd-b', name: 'Förälder B' })],
}),
)
// Parent A already has a pinned sjukpenning day on `today` — this is
// exactly the state the bug report described after "parent A takes 2
// days on PL".
vi.mocked(usePlannedDays).mockReturnValue(
makePlannedDaysResult({
data: [
makePlannedLeaveDay({
id: 'pd-a-day',
date: today,
parentPersonalDataUuid: 'person-1',
dayType: 'sjukpenning',
isPinned: true,
}),
],
add,
update,
}),
)
render(<Calendar />)
const calendarDays = screen.getAllByTestId('calendar-day')
const todayCell = calendarDays.find((el) => el.getAttribute('aria-label')?.startsWith(today))!
fireEvent.click(todayCell)
// Opens defaulted to A (the logged-in parent) — A's existing day shows.
expect(screen.getByTestId('modal')).toHaveAttribute('data-existing-day', 'yes')
expect(screen.getByTestId('modal')).toHaveAttribute('data-selected-parent-id', 'person-1')
// Switch the advanced parent to B — B has nothing here yet.
fireEvent.click(screen.getByTestId('modal-change-parent-b'))
expect(screen.getByTestId('modal')).toHaveAttribute('data-existing-day', 'no')
expect(screen.getByTestId('modal')).toHaveAttribute('data-selected-parent-id', 'pd-b')
// Now select vacation for B and save through the advanced/pinned path.
fireEvent.click(screen.getByTestId('modal-save-pinned'))
// A's row must never be touched — the old bug called `update` on it.
await waitFor(() => expect(add).toHaveBeenCalledWith(expect.objectContaining({ parent_personal_data_uuid: 'pd-b', day_type: 'vacation' })))
expect(update).not.toHaveBeenCalled()
})
it('deleting through the advanced path removes the currently-selected parent\'s day, not the other parent\'s', async () => {
const today = todayDateString()
const remove = vi.fn().mockResolvedValue(undefined)
vi.mocked(useWorkspaceMembers).mockReturnValue(
makeWorkspaceMembersResult({
data: [makePersonalData({ uuid: 'person-1', name: 'Förälder A' }), makePersonalData({ uuid: 'pd-b', name: 'Förälder B' })],
}),
)
vi.mocked(usePlannedDays).mockReturnValue(
makePlannedDaysResult({
data: [
makePlannedLeaveDay({ id: 'pd-a-day', date: today, parentPersonalDataUuid: 'person-1', dayType: 'sjukpenning' }),
makePlannedLeaveDay({ id: 'pd-b-day', date: today, parentPersonalDataUuid: 'pd-b', dayType: 'vacation' }),
],
remove,
}),
)
render(<Calendar />)
const calendarDays = screen.getAllByTestId('calendar-day')
const todayCell = calendarDays.find((el) => el.getAttribute('aria-label')?.startsWith(today))!
fireEvent.click(todayCell)
fireEvent.click(screen.getByTestId('modal-change-parent-b'))
fireEvent.click(screen.getByTestId('modal-delete'))
await waitFor(() => expect(remove).toHaveBeenCalledWith('pd-b-day'))
})
})
describe('todo.md #10/#11 — the manual-add modal defaults to a planner-funded wish', () => {
it('a click/drag by default creates a fixed, once-off wish (not a pinned day) for the chosen parent mode', async () => {
const created = makeLeaveWish({ id: 'lw-manual' })
const add = vi.fn().mockResolvedValue(created)
const addDay = vi.fn()
const sync = vi.fn().mockResolvedValue(makePlanSyncResponse())
vi.mocked(useLeaveWishes).mockReturnValue(makeLeaveWishesResult({ add, data: [] }))
vi.mocked(usePlanSync).mockReturnValue(makePlanSyncResult({ sync }))
vi.mocked(usePlannedDays).mockReturnValue(makePlannedDaysResult({ add: addDay }))
render(<Calendar />)
const calendarDays = screen.getAllByTestId('calendar-day')
const currentMonthDay = calendarDays.find((el) => !el.classList.contains('calendar-day--disabled'))!
fireEvent.click(currentMonthDay)
fireEvent.click(screen.getByTestId('modal-save-wish-both'))
await waitFor(() =>
expect(add).toHaveBeenCalledWith(
expect.objectContaining({ windowMode: 'fixed', recurrence: 'once', parentMode: 'both', wantsDubbeldag: false }),
),
)
await waitFor(() => expect(sync).toHaveBeenCalled())
// The default path never touches planned days directly.
expect(addDay).not.toHaveBeenCalled()
})
it('forwards wantsDubbeldag only when the wish targets both parents', async () => {
const created = makeLeaveWish({ id: 'lw-manual' })
const add = vi.fn().mockResolvedValue(created)
const sync = vi.fn().mockResolvedValue(makePlanSyncResponse())
vi.mocked(useLeaveWishes).mockReturnValue(makeLeaveWishesResult({ add, data: [] }))
vi.mocked(usePlanSync).mockReturnValue(makePlanSyncResult({ sync }))
render(<Calendar />)
const calendarDays = screen.getAllByTestId('calendar-day')
const currentMonthDay = calendarDays.find((el) => !el.classList.contains('calendar-day--disabled'))!
fireEvent.click(currentMonthDay)
fireEvent.click(screen.getByTestId('modal-save-wish-both-dubbeldag'))
await waitFor(() => expect(add).toHaveBeenCalledWith(expect.objectContaining({ wantsDubbeldag: true })))
})
it('never sends wantsDubbeldag for a specific-parent wish, even if the modal tried to', async () => {
const created = makeLeaveWish({ id: 'lw-manual' })
const add = vi.fn().mockResolvedValue(created)
const sync = vi.fn().mockResolvedValue(makePlanSyncResponse())
vi.mocked(useLeaveWishes).mockReturnValue(makeLeaveWishesResult({ add, data: [] }))
vi.mocked(usePlanSync).mockReturnValue(makePlanSyncResult({ sync }))
render(<Calendar />)
const calendarDays = screen.getAllByTestId('calendar-day')
const currentMonthDay = calendarDays.find((el) => !el.classList.contains('calendar-day--disabled'))!
fireEvent.click(currentMonthDay)
fireEvent.click(screen.getByTestId('modal-save-wish-specific-dubbeldag-attempt'))
await waitFor(() =>
expect(add).toHaveBeenCalledWith(
expect.objectContaining({ parentMode: 'specific', parentPersonalDataUuid: 'pd-b', wantsDubbeldag: false }),
),
)
})
})
describe('todo.md #9 — the three-way remove', () => {
it('"day" scope is a no-op for a wish-linked band (disabled in the UI; would just reappear on the next sync)', async () => {
const today = todayDateString()
const wish = makeLeaveWish({ id: 'lw-1' })
const remove = vi.fn().mockResolvedValue(undefined)
const update = vi.fn().mockResolvedValue(wish)
vi.mocked(useLeaveWishes).mockReturnValue(makeLeaveWishesResult({ data: [wish], remove, update }))
vi.mocked(usePlannedDays).mockReturnValue(
makePlannedDaysResult({
data: [makePlannedLeaveDay({ id: 'pd-1', date: today, wishId: 'lw-1', parentPersonalDataUuid: 'person-1' })],
}),
)
render(<Calendar />)
fireEvent.click(screen.getAllByTestId(/^calendar-band-/)[0]!)
fireEvent.click(screen.getByTestId('period-delete-day'))
await waitFor(() => expect(screen.queryByTestId('period-popover')).not.toBeInTheDocument())
expect(remove).not.toHaveBeenCalled()
expect(update).not.toHaveBeenCalled()
})
it('"every year" scope is a no-op for an ad-hoc band (disabled in the UI; nothing recurring to remove)', async () => {
const today = todayDateString()
const removeDay = vi.fn()
vi.mocked(usePlannedDays).mockReturnValue(
makePlannedDaysResult({
data: [makePlannedLeaveDay({ id: 'pd-1', date: today, wishId: null, parentPersonalDataUuid: 'person-1' })],
remove: removeDay,
}),
)
render(<Calendar />)
fireEvent.click(screen.getAllByTestId(/^calendar-band-/)[0]!)
fireEvent.click(screen.getByTestId('period-delete-all-years'))
await waitFor(() => expect(screen.queryByTestId('period-popover')).not.toBeInTheDocument())
expect(removeDay).not.toHaveBeenCalled()
})
it('"day" scope on an ad-hoc band removes only the clicked occurrence\'s first date', async () => {
const removeDay = vi.fn().mockResolvedValue(undefined)
vi.mocked(usePlannedDays).mockReturnValue(
makePlannedDaysResult({
data: [
makePlannedLeaveDay({ id: 'pd-1', date: '2027-03-10', wishId: null, parentPersonalDataUuid: 'person-1' }),
makePlannedLeaveDay({ id: 'pd-2', date: '2027-03-11', wishId: null, parentPersonalDataUuid: 'person-1' }),
],
remove: removeDay,
}),
)
vi.mocked(useSearchParams).mockReturnValue([new URLSearchParams('date=2027-03-10'), mockSetSearchParams])
render(<Calendar />)
fireEvent.click(screen.getAllByTestId(/^calendar-band-/)[0]!)
fireEvent.click(screen.getByTestId('period-delete-day'))
await waitFor(() => expect(removeDay).toHaveBeenCalledWith('pd-1'))
expect(removeDay).not.toHaveBeenCalledWith('pd-2')
})
describe('"year" scope on a recurring wish — narrows or splits firstYear/lastYear', () => {
it('removing the first year of an open-ended wish bumps firstYear forward', async () => {
const wish = makeLeaveWish({ id: 'lw-1', recurrence: 'yearly', firstYear: 2026, lastYear: null })
const update = vi.fn().mockResolvedValue(wish)
const remove = vi.fn()
vi.mocked(useLeaveWishes).mockReturnValue(makeLeaveWishesResult({ data: [wish], update, remove }))
vi.mocked(usePlannedDays).mockReturnValue(
makePlannedDaysResult({
data: [makePlannedLeaveDay({ id: 'pd-1', date: '2026-06-15', wishId: 'lw-1', parentPersonalDataUuid: 'person-1' })],
}),
)
vi.mocked(useSearchParams).mockReturnValue([new URLSearchParams('date=2026-06-15'), mockSetSearchParams])
render(<Calendar />)
fireEvent.click(screen.getAllByTestId(/^calendar-band-/)[0]!)
fireEvent.click(screen.getByTestId('period-delete-year'))
await waitFor(() => expect(update).toHaveBeenCalledWith('lw-1', expect.objectContaining({ firstYear: 2027 })))
expect(remove).not.toHaveBeenCalled()
})
it('removing the last year of a bounded wish decrements lastYear', async () => {
const wish = makeLeaveWish({ id: 'lw-1', recurrence: 'yearly', firstYear: 2026, lastYear: 2029 })
const update = vi.fn().mockResolvedValue(wish)
const remove = vi.fn()
vi.mocked(useLeaveWishes).mockReturnValue(makeLeaveWishesResult({ data: [wish], update, remove }))
vi.mocked(usePlannedDays).mockReturnValue(
makePlannedDaysResult({
data: [makePlannedLeaveDay({ id: 'pd-1', date: '2029-06-15', wishId: 'lw-1', parentPersonalDataUuid: 'person-1' })],
}),
)
vi.mocked(useSearchParams).mockReturnValue([new URLSearchParams('date=2029-06-15'), mockSetSearchParams])
render(<Calendar />)
fireEvent.click(screen.getAllByTestId(/^calendar-band-/)[0]!)
fireEvent.click(screen.getByTestId('period-delete-year'))
await waitFor(() => expect(update).toHaveBeenCalledWith('lw-1', expect.objectContaining({ lastYear: 2028 })))
expect(remove).not.toHaveBeenCalled()
})
it('removing a middle year splits the wish into two, excluding just that year', async () => {
const wish = makeLeaveWish({ id: 'lw-1', recurrence: 'yearly', firstYear: 2026, lastYear: 2030 })
const update = vi.fn().mockResolvedValue(wish)
const add = vi.fn().mockResolvedValue(makeLeaveWish({ id: 'lw-1-split' }))
vi.mocked(useLeaveWishes).mockReturnValue(makeLeaveWishesResult({ data: [wish], update, add }))
vi.mocked(usePlannedDays).mockReturnValue(
makePlannedDaysResult({
data: [makePlannedLeaveDay({ id: 'pd-1', date: '2028-06-15', wishId: 'lw-1', parentPersonalDataUuid: 'person-1' })],
}),
)
vi.mocked(useSearchParams).mockReturnValue([new URLSearchParams('date=2028-06-15'), mockSetSearchParams])
render(<Calendar />)
fireEvent.click(screen.getAllByTestId(/^calendar-band-/)[0]!)
fireEvent.click(screen.getByTestId('period-delete-year'))
// The original wish keeps 2026-2027; a new wish covers 2029-2030.
await waitFor(() => expect(update).toHaveBeenCalledWith('lw-1', expect.objectContaining({ lastYear: 2027 })))
expect(add).toHaveBeenCalledWith(expect.objectContaining({ firstYear: 2029, lastYear: 2030 }))
})
it('removing the only year of a single-year wish deletes it entirely instead of leaving an empty range', async () => {
const wish = makeLeaveWish({ id: 'lw-1', recurrence: 'yearly', firstYear: 2026, lastYear: 2026 })
const remove = vi.fn().mockResolvedValue(undefined)
const sync = vi.fn().mockResolvedValue(makePlanSyncResponse())
vi.mocked(useLeaveWishes).mockReturnValue(makeLeaveWishesResult({ data: [wish], remove }))
vi.mocked(usePlanSync).mockReturnValue(makePlanSyncResult({ sync }))
vi.mocked(usePlannedDays).mockReturnValue(
makePlannedDaysResult({
data: [makePlannedLeaveDay({ id: 'pd-1', date: '2026-06-15', wishId: 'lw-1', parentPersonalDataUuid: 'person-1' })],
}),
)
vi.mocked(useSearchParams).mockReturnValue([new URLSearchParams('date=2026-06-15'), mockSetSearchParams])
render(<Calendar />)
fireEvent.click(screen.getAllByTestId(/^calendar-band-/)[0]!)
fireEvent.click(screen.getByTestId('period-delete-year'))
await waitFor(() => expect(remove).toHaveBeenCalledWith('lw-1'))
})
it('a once-off wish treats "this year" the same as "every year" (there is only one occurrence)', async () => {
const wish = makeLeaveWish({ id: 'lw-1', recurrence: 'once', firstYear: 2026 })
const remove = vi.fn().mockResolvedValue(undefined)
const sync = vi.fn().mockResolvedValue(makePlanSyncResponse())
vi.mocked(useLeaveWishes).mockReturnValue(makeLeaveWishesResult({ data: [wish], remove }))
vi.mocked(usePlanSync).mockReturnValue(makePlanSyncResult({ sync }))
vi.mocked(usePlannedDays).mockReturnValue(
makePlannedDaysResult({
data: [makePlannedLeaveDay({ id: 'pd-1', date: '2026-06-15', wishId: 'lw-1', parentPersonalDataUuid: 'person-1' })],
}),
)
vi.mocked(useSearchParams).mockReturnValue([new URLSearchParams('date=2026-06-15'), mockSetSearchParams])
render(<Calendar />)
fireEvent.click(screen.getAllByTestId(/^calendar-band-/)[0]!)
fireEvent.click(screen.getByTestId('period-delete-year'))
await waitFor(() => expect(remove).toHaveBeenCalledWith('lw-1'))
})
})
})
})