diff --git a/frontend/src/components/modals/PlannedDayModal.test.tsx b/frontend/src/components/modals/PlannedDayModal.test.tsx index 5a7886b..88a687b 100644 --- a/frontend/src/components/modals/PlannedDayModal.test.tsx +++ b/frontend/src/components/modals/PlannedDayModal.test.tsx @@ -768,11 +768,52 @@ describe('PlannedDayModal', () => { expect(screen.queryByTestId('pin-exclusion-notice')).not.toBeInTheDocument() }) - it('shows neither notice while the type select is still on "let the planner choose"', () => { + it('still warns free days will stay off while on "let the planner choose" (solo) — the pin-only notice does not apply', () => { + // §silent-no-op fix — a free day is marked 'free' by the allocator's + // `is_free_day` branch (allocation_service.py) before any type is even + // considered, so 'auto' stays off here exactly like a concrete pin + // does. Only the pin-exclusion notice (below) is pin-specific. render() - expect(screen.queryByTestId('free-days-stay-off-notice')).not.toBeInTheDocument() + expect(screen.getByTestId('free-days-stay-off-notice')).toBeInTheDocument() expect(screen.queryByTestId('pin-exclusion-notice')).not.toBeInTheDocument() }) + + it('does not warn on "let the planner choose" once takePlOnRedDays is on', () => { + render( + , + ) + expect(screen.queryByTestId('free-days-stay-off-notice')).not.toBeInTheDocument() + }) + + it('warns in the default (non-advanced) couple path too — a "Planera en dag" save on a free day used to accept silently with nothing changed', () => { + render() + expect(screen.getByTestId('free-days-stay-off-notice')).toBeInTheDocument() + // The advanced-only pin-exclusion notice must not leak into this path. + expect(screen.queryByTestId('pin-exclusion-notice')).not.toBeInTheDocument() + }) + + it('does not warn in the default couple path once takePlOnRedDays is on', () => { + render( + , + ) + expect(screen.queryByTestId('free-days-stay-off-notice')).not.toBeInTheDocument() + }) + + it('does not duplicate the notice once Advanced is opened (same warning, one copy)', async () => { + render() + await openAdvanced() + expect(screen.getAllByTestId('free-days-stay-off-notice')).toHaveLength(1) + }) }) describe("removed days (DayAssertion, dayType: 'none') — the way back", () => { diff --git a/frontend/src/components/modals/PlannedDayModal.tsx b/frontend/src/components/modals/PlannedDayModal.tsx index e62d8dc..724e7e1 100644 --- a/frontend/src/components/modals/PlannedDayModal.tsx +++ b/frontend/src/components/modals/PlannedDayModal.tsx @@ -130,16 +130,24 @@ export function PlannedDayModal({ const showPinnedControls = forcedAdvanced || soloDirect || isAdvancedOpen // A concrete type is about to be pinned (not the 'auto' planner-decides - // choice) — the two notices below only make sense once that's true. + // choice) — the pin-exclusion notice below only makes sense once that's + // true. const isPinningConcreteType = pinnedPath && dayType !== 'auto' - // The chosen type would leave every free day in the selection (weekend/red - // day) as a day off: always true for `vacation`, and true for a PL type - // unless the workspace opted in via `takePlOnRedDays` — mirrors - // `Calendar.tsx`'s `handleSavePinnedDay` filter exactly. + // Would every free day (weekend/red day) in the selection stay OFF under + // the save path this dialog is currently set up to take? Pinning a + // concrete type: always for `vacation`, and for a PL type unless the + // workspace opted in via `takePlOnRedDays` — mirrors `Calendar.tsx`'s + // `handleSavePinnedDay` filter exactly. The default/auto (wish) path: + // `allocation_service.py`'s `is_free_day` branch fires BEFORE any type is + // chosen and marks the day 'free' unless `takePlOnRedDays`, regardless of + // parentMode/dubbeldag — so free days stay off there too, always, unless + // that same opt-in is on. Shown regardless of `isPinningConcreteType` so + // the default path warns just as loudly as the advanced one (see the + // "silent no-op" fix: planning a free day through the default dialog used + // to accept with nothing changed and no explanation). const freeDaysWillStayOff = - isPinningConcreteType && freeDatesInSelection.length > 0 && - (dayType === 'vacation' || !takePlOnRedDays) + (pinnedPath ? dayType === 'vacation' || !takePlOnRedDays : !takePlOnRedDays) // Pinning excludes the day from `sync_plan`'s automatic re-planning — // worth saying prospectively (about to pin) even when the day isn't // already pinned; `existingDay?.isPinned` below already covers the @@ -348,6 +356,17 @@ export function PlannedDayModal({ )} + {/* The default path's own copy of the "will stay off" notice — + * `showPinnedControls` is false here (Advanced isn't open), so + * the copy inside that section below never renders for this + * case. Gated on `!showPinnedControls` rather than re-deriving + * "default path" so the two copies can never both render. */} + {!forcedAdvanced && hasMultipleParents && !showPinnedControls && freeDaysWillStayOff && ( +

+ {t('plannedDayModal.freeDaysStayOffNotice')} +

+ )} + {!forcedAdvanced && hasMultipleParents && (