Let a day removed from a period stay removed
Taking one day out of a period the planner placed did nothing that lasted: the period still asked for a full week, so the next sync put the day straight back. The option was disabled rather than lying about it. A removed day is now recorded as its own thing — no leave for this parent, this child, this date — and the planner honours it on every later sync. It is kept apart from the working-day overrides, which answer a different question about the calendar rather than about the plan. The day belongs to the parent whose band was clicked, so removing one parent's day leaves their partner's alone; where both parents were to be off on one funding arrangement, the partner is simply funded on their own instead. A removed day shortens what the period is asking for rather than counting as a day the plan failed to find, so the advice about closing a shortfall does not start arguing for a day nobody wants. Nothing is removed without a way back: the toast undoes it, and the day itself is marked in the calendar and offers to restore. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ei7gCZzdd751uZtPzb1Pep
This commit is contained in:
@@ -13,6 +13,7 @@ import {
|
||||
useLeaveWishes,
|
||||
useWorkspaceMembers,
|
||||
usePlanSync,
|
||||
usePlannedDayExclusions,
|
||||
} from '@/hooks'
|
||||
import { LanguageProvider } from '@/i18n'
|
||||
import type { PlannedDayModalProps } from '@/components/modals/PlannedDayModal'
|
||||
@@ -35,6 +36,8 @@ import {
|
||||
makeRejection,
|
||||
makePlanProposal,
|
||||
makePlanApplyResponse,
|
||||
makePlannedDayExclusionsResult,
|
||||
makePlannedDayExclusion,
|
||||
} from '../../tests/fixtures'
|
||||
import { makeCalendarDayEntry, makeCalendarWindow } from '../../tests/fixtures/calendar'
|
||||
|
||||
@@ -61,6 +64,7 @@ vi.mock('@/hooks', () => ({
|
||||
useLeaveWishes: vi.fn(),
|
||||
useWorkspaceMembers: vi.fn(),
|
||||
usePlanSync: vi.fn(),
|
||||
usePlannedDayExclusions: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/components/layout/Layout', () => ({
|
||||
@@ -149,6 +153,16 @@ vi.mock('@/components/modals/PlannedDayModal', () => ({
|
||||
delete
|
||||
</button>
|
||||
)}
|
||||
{(props.excludedEntries ?? []).map((entry) => (
|
||||
<button
|
||||
key={entry.id}
|
||||
data-testid={`modal-restore-excluded-${entry.id}`}
|
||||
data-parent-name={entry.parentName}
|
||||
onClick={() => void props.onRestoreExcluded?.(entry.id)}
|
||||
>
|
||||
restore {entry.parentName}
|
||||
</button>
|
||||
))}
|
||||
<button data-testid="modal-close" onClick={props.onClose}>
|
||||
close
|
||||
</button>
|
||||
@@ -164,6 +178,8 @@ vi.mock('@/components/calendar/PeriodPopover', () => ({
|
||||
data-wish-id={props.wish?.id ?? 'null'}
|
||||
data-start={props.startDate}
|
||||
data-end={props.endDate}
|
||||
data-remove-day-label={props.removeDayLabel ?? ''}
|
||||
data-remove-day-parent={props.removeDayParentName ?? ''}
|
||||
data-error={props.error ?? ''}
|
||||
>
|
||||
<button
|
||||
@@ -272,12 +288,14 @@ describe('Calendar', () => {
|
||||
const mockLeaveWishes = makeLeaveWishesResult()
|
||||
const mockMembers = makeWorkspaceMembersResult()
|
||||
const mockSync = makePlanSyncResult()
|
||||
const mockExclusions = makePlannedDayExclusionsResult()
|
||||
|
||||
beforeEach(() => {
|
||||
vi.mocked(useCalendarDays).mockReturnValue(mockCalendarWindow)
|
||||
vi.mocked(useLeaveWishes).mockReturnValue(mockLeaveWishes)
|
||||
vi.mocked(useWorkspaceMembers).mockReturnValue(mockMembers)
|
||||
vi.mocked(usePlanSync).mockReturnValue(mockSync)
|
||||
vi.mocked(usePlannedDayExclusions).mockReturnValue(mockExclusions)
|
||||
vi.mocked(useWorkspace).mockReturnValue(mockWorkspace)
|
||||
vi.mocked(usePlannedDays).mockReturnValue(mockPlannedDays)
|
||||
vi.mocked(useSearchParams).mockReturnValue([new URLSearchParams(), mockSetSearchParams])
|
||||
@@ -1947,15 +1965,31 @@ describe('Calendar', () => {
|
||||
})
|
||||
|
||||
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 () => {
|
||||
// Intent changed (was: '"day" scope is a no-op for a wish-linked band').
|
||||
// The user asked for it to work: a single day removed from a
|
||||
// planner-placed period is now recorded as a `PlannedDayExclusion`, which
|
||||
// is what stops the next sync re-deriving it.
|
||||
it('"day" scope records an exclusion for a wish-linked band, keyed on the band\'s parent', async () => {
|
||||
const today = todayDateString()
|
||||
const wish = makeLeaveWish({ id: 'lw-1' })
|
||||
const remove = vi.fn().mockResolvedValue(undefined)
|
||||
const update = vi.fn().mockResolvedValue(wish)
|
||||
const exclude = vi.fn().mockResolvedValue(makePlannedDayExclusion({ id: 'ex-1' }))
|
||||
vi.mocked(useLeaveWishes).mockReturnValue(makeLeaveWishesResult({ data: [wish], remove, update }))
|
||||
vi.mocked(usePlannedDayExclusions).mockReturnValue(
|
||||
makePlannedDayExclusionsResult({ exclude }),
|
||||
)
|
||||
vi.mocked(usePlannedDays).mockReturnValue(
|
||||
makePlannedDaysResult({
|
||||
data: [makePlannedLeaveDay({ id: 'pd-1', date: today, wishId: 'lw-1', parentPersonalDataUuid: 'person-1' })],
|
||||
data: [
|
||||
makePlannedLeaveDay({
|
||||
id: 'pd-1',
|
||||
date: today,
|
||||
wishId: 'lw-1',
|
||||
parentPersonalDataUuid: 'person-1',
|
||||
childId: 'child-1',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -1963,9 +1997,116 @@ describe('Calendar', () => {
|
||||
fireEvent.click(screen.getAllByTestId(/^calendar-band-/)[0]!)
|
||||
fireEvent.click(screen.getByTestId('period-delete-day'))
|
||||
|
||||
await waitFor(() => expect(screen.queryByTestId('period-popover')).not.toBeInTheDocument())
|
||||
await waitFor(() =>
|
||||
expect(exclude).toHaveBeenCalledWith({
|
||||
workspaceId: 'ws-1',
|
||||
childId: 'child-1',
|
||||
parentPersonalDataUuid: 'person-1',
|
||||
date: today,
|
||||
}),
|
||||
)
|
||||
// The wish itself is untouched — only this one date goes.
|
||||
expect(remove).not.toHaveBeenCalled()
|
||||
expect(update).not.toHaveBeenCalled()
|
||||
await waitFor(() => expect(screen.queryByTestId('period-popover')).not.toBeInTheDocument())
|
||||
})
|
||||
|
||||
it('offers an undo that restores the removed day', async () => {
|
||||
const today = todayDateString()
|
||||
const wish = makeLeaveWish({ id: 'lw-1' })
|
||||
const exclude = vi.fn().mockResolvedValue(makePlannedDayExclusion({ id: 'ex-9' }))
|
||||
const restore = vi.fn().mockResolvedValue(undefined)
|
||||
vi.mocked(useLeaveWishes).mockReturnValue(makeLeaveWishesResult({ data: [wish] }))
|
||||
vi.mocked(usePlannedDayExclusions).mockReturnValue(
|
||||
makePlannedDayExclusionsResult({ exclude, restore }),
|
||||
)
|
||||
vi.mocked(usePlannedDays).mockReturnValue(
|
||||
makePlannedDaysResult({
|
||||
data: [
|
||||
makePlannedLeaveDay({
|
||||
id: 'pd-1',
|
||||
date: today,
|
||||
wishId: 'lw-1',
|
||||
parentPersonalDataUuid: 'person-1',
|
||||
childId: 'child-1',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
)
|
||||
|
||||
render(<Calendar />)
|
||||
fireEvent.click(screen.getAllByTestId(/^calendar-band-/)[0]!)
|
||||
fireEvent.click(screen.getByTestId('period-delete-day'))
|
||||
|
||||
await waitFor(() => expect(screen.getByTestId('calendar-toast-undo')).toBeInTheDocument())
|
||||
fireEvent.click(screen.getByTestId('calendar-toast-undo'))
|
||||
|
||||
await waitFor(() => expect(restore).toHaveBeenCalledWith('ex-9'))
|
||||
})
|
||||
|
||||
it('offers the removed day back from the day popover, long after the toast is gone', async () => {
|
||||
const today = todayDateString()
|
||||
const restore = vi.fn().mockResolvedValue(undefined)
|
||||
vi.mocked(useWorkspaceMembers).mockReturnValue(
|
||||
makeWorkspaceMembersResult({
|
||||
data: [makePersonalData({ uuid: 'person-1', name: 'Alex' })],
|
||||
}),
|
||||
)
|
||||
vi.mocked(usePlannedDayExclusions).mockReturnValue(
|
||||
makePlannedDayExclusionsResult({
|
||||
restore,
|
||||
data: [
|
||||
makePlannedDayExclusion({
|
||||
id: 'ex-7',
|
||||
date: today,
|
||||
parentPersonalDataUuid: 'person-1',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
)
|
||||
|
||||
render(<Calendar />)
|
||||
const todayCell = screen
|
||||
.getAllByTestId('calendar-day')
|
||||
.find((el) => el.getAttribute('aria-label')?.startsWith(today))
|
||||
fireEvent.click(todayCell!)
|
||||
|
||||
const restoreButton = await screen.findByTestId('modal-restore-excluded-ex-7')
|
||||
expect(restoreButton).toHaveAttribute('data-parent-name', 'Alex')
|
||||
fireEvent.click(restoreButton)
|
||||
|
||||
await waitFor(() => expect(restore).toHaveBeenCalledWith('ex-7'))
|
||||
})
|
||||
|
||||
it('tells the popover which date and which parent "this day" would remove', async () => {
|
||||
const today = todayDateString()
|
||||
const wish = makeLeaveWish({ id: 'lw-1' })
|
||||
vi.mocked(useLeaveWishes).mockReturnValue(makeLeaveWishesResult({ data: [wish] }))
|
||||
vi.mocked(useWorkspaceMembers).mockReturnValue(
|
||||
makeWorkspaceMembersResult({
|
||||
data: [makePersonalData({ uuid: 'person-1', name: 'Alex' })],
|
||||
}),
|
||||
)
|
||||
vi.mocked(usePlannedDays).mockReturnValue(
|
||||
makePlannedDaysResult({
|
||||
data: [
|
||||
makePlannedLeaveDay({
|
||||
id: 'pd-1',
|
||||
date: today,
|
||||
wishId: 'lw-1',
|
||||
parentPersonalDataUuid: 'person-1',
|
||||
childId: 'child-1',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
)
|
||||
|
||||
render(<Calendar />)
|
||||
fireEvent.click(screen.getAllByTestId(/^calendar-band-/)[0]!)
|
||||
|
||||
const popover = screen.getByTestId('period-popover')
|
||||
expect(popover).toHaveAttribute('data-remove-day-parent', 'Alex')
|
||||
expect(popover.getAttribute('data-remove-day-label')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('"every year" scope is a no-op for an ad-hoc band (disabled in the UI; nothing recurring to remove)', async () => {
|
||||
|
||||
Reference in New Issue
Block a user