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:
2026-09-19 16:05:17 +02:00
parent 6d92b87bf3
commit 8cfb9c82c6
39 changed files with 2362 additions and 31 deletions
@@ -23,6 +23,12 @@ export interface PlannedDayModalSavePinnedPayload {
isDubbeldag: boolean
}
/** One day the user removed by hand, ready to be offered back. */
export interface PlannedDayExclusionEntry {
id: string
parentName: string
}
export interface PlannedDayModalProps {
date: string
isOpen: boolean
@@ -39,6 +45,11 @@ export interface PlannedDayModalProps {
onSaveWish: (payload: PlannedDayModalSaveWishPayload) => Promise<void>
onSavePinned: (payload: PlannedDayModalSavePinnedPayload) => Promise<void>
onDelete?: (() => Promise<void>) | undefined
/** Removals recorded for THIS date (see `PlannedDayExclusion`), one per
* parent. Shown with a way back: a day the user can't restore is a trap,
* and the undo toast is gone as soon as it times out. */
excludedEntries?: PlannedDayExclusionEntry[] | undefined
onRestoreExcluded?: ((exclusionId: string) => Promise<void>) | undefined
onClose: () => void
isSaving?: boolean | undefined
error?: string | null | undefined
@@ -57,6 +68,8 @@ export function PlannedDayModal({
onSaveWish,
onSavePinned,
onDelete,
excludedEntries = [],
onRestoreExcluded,
onClose,
isSaving = false,
error = null,
@@ -217,6 +230,28 @@ export function PlannedDayModal({
</div>
)}
{!isRangeMode &&
excludedEntries.map((entry) => (
<div
key={entry.id}
className="planned-day-modal__excluded-notice"
data-testid="excluded-notice"
>
<span>{t('plannedDayModal.excludedNotice', { parent: entry.parentName })}</span>
{onRestoreExcluded && (
<button
type="button"
className="btn btn--secondary btn--sm"
onClick={() => void onRestoreExcluded(entry.id)}
disabled={isSaving}
data-testid={`restore-excluded-${entry.id}`}
>
{t('plannedDayModal.restoreExcludedButton')}
</button>
)}
</div>
))}
<div className="stack">
{!forcedAdvanced && hasMultipleParents && (
<div className="field">