From f4809a67d380318e8496f380be301eaed91051d0 Mon Sep 17 00:00:00 2001 From: Maurice Date: Sun, 13 Sep 2026 14:01:18 +0200 Subject: [PATCH] Revert "fix(roadtrip): a night booked into the next morning holds that morning" This reverts commit d4677bdea, and keeps the departure chip's icon. The premise was wrong. A check-out is the LATEST moment the room has to be handed back, not the earliest one you may leave: whoever wants to set off at seven checks out before that or leaves the bags at the desk. Reading it as a floor turned a perfectly good plan into a finding, and no amount of wording around that would have made it true. The chip keeps the door-out mark. That part says what it always said, only in a symbol that is not a second clock face beside the arrival. --- shared/src/roadtrip/assemble.ts | 6 +-- shared/src/roadtrip/nightSpill.spec.ts | 64 -------------------------- shared/src/roadtrip/nightSpill.ts | 31 ------------- shared/src/roadtrip/roadtripModel.ts | 16 +------ 4 files changed, 3 insertions(+), 114 deletions(-) diff --git a/shared/src/roadtrip/assemble.ts b/shared/src/roadtrip/assemble.ts index 126c483b6..d5b8a3ebb 100644 --- a/shared/src/roadtrip/assemble.ts +++ b/shared/src/roadtrip/assemble.ts @@ -2,7 +2,7 @@ import { spurFor } from './accessSpur'; import { pointAtMeters } from './corridor'; import type { RoadtripDayBoundary } from './day-boundary.schema'; import { planDayWindow, type DayWindow } from './dayWindow'; -import { earliestFreeMinute, spillChains } from './nightSpill'; +import { spillChains } from './nightSpill'; import type { PlanDay, QuietDay, @@ -77,9 +77,6 @@ export function assembleRoadtrip({ ) : null; const automaticSchedule = !!timed && timed.issue === null; - // Same floor the spill chains use: a night booked into the next morning holds the - // traveller until check-out, so the day it lands in cannot start before that. - const heldUntil = earliestFreeMinute([...plan, ...quietDays]); const displayChains = automaticSchedule ? timed.chains : timed?.issue @@ -96,7 +93,6 @@ export function assembleRoadtrip({ departureAt: s.checkoutAt === undefined ? undefined : s.checkoutAt - d.dayNumber * 1440, })), d.stops.slice(0, -1).map((s, i) => storedLegFor(s, d.stops[i + 1]!)?.seg.duration), - { notBefore: heldUntil.get(d.dayNumber) ?? null }, ), })) : splitScheduledDays(chains, [...plan, ...quietDays], storedLegFor); diff --git a/shared/src/roadtrip/nightSpill.spec.ts b/shared/src/roadtrip/nightSpill.spec.ts index e49d6af5d..3093959c1 100644 --- a/shared/src/roadtrip/nightSpill.spec.ts +++ b/shared/src/roadtrip/nightSpill.spec.ts @@ -276,68 +276,4 @@ describe('spillChains', () => { expect(chains[1]!.spills).toHaveLength(1); expect(chains[1]!.spills[0]!).toMatchObject({ at: 0, count: 2, fromDayNumber: 1 }); }); - - describe('a night booked into the next morning holds that morning', () => { - // Check in on day 1 at 08:00, check out on day 2 at 08:00. Until the room is - // handed back the traveller is at the hotel, so day 2 cannot quietly start at - // 07:00 somewhere else as if the night had ended at midnight. - const hotel = () => stop({ - assignmentId: 1, name: 'Mercure Hamburg', checkInTime: '08:00', - checkoutAt: 2 * 1440 + 480, - }); - - it('FE-NIGHTSPILL-013: a stop pinned before check-out keeps its clock and is flagged late', () => { - const plan = [ - day(1, 1, [hotel()]), - day(2, 2, [stop({ assignmentId: 2, name: 'Hotel Panorama', time: '07:00' })]), - ]; - - const chains = spillChains(plan, [], everyLeg(11)); - const dayTwo = chains.find((c) => c.dayNumber === 2)!; - - // The hour the traveller chose is never moved. - expect(dayTwo.schedule.entries[0]!.arrival).toBe('07:00'); - // But it cannot be reached: the room goes back an hour after it. - expect(dayTwo.schedule.warnings).toContainEqual({ index: 0, code: 'late', minutes: 60 }); - }); - - it('FE-NIGHTSPILL-014: a stop with no clock of its own starts at check-out, not at nothing', () => { - const plan = [ - day(1, 1, [hotel()]), - day(2, 2, [stop({ assignmentId: 2, name: 'Bergedorf' })]), - ]; - - const chains = spillChains(plan, [], everyLeg(11)); - - expect(chains.find((c) => c.dayNumber === 2)!.schedule.entries[0]!.arrival).toBe('08:00'); - }); - - it('FE-NIGHTSPILL-015: the days between a longer stay are left alone', () => { - // Monday to Thursday: you sleep there, you do not sit there. Only the day the - // check-out falls in is held. - const plan = [ - day(1, 1, [stop({ assignmentId: 1, name: 'Mercure Hamburg', checkInTime: '08:00', checkoutAt: 4 * 1440 + 480 })]), - day(2, 2, [stop({ assignmentId: 2, name: 'Speicherstadt' })]), - day(4, 4, [stop({ assignmentId: 3, name: 'Bergedorf' })]), - ]; - - const chains = spillChains(plan, [], everyLeg(11)); - - expect(chains.find((c) => c.dayNumber === 2)!.schedule.entries[0]!.arrival).toBeNull(); - expect(chains.find((c) => c.dayNumber === 4)!.schedule.entries[0]!.arrival).toBe('08:00'); - }); - - it('FE-NIGHTSPILL-016: a check-out on the same day it started binds nothing', () => { - // A day room, or a stay whose end day is the day it started: the stops after it - // already follow through the leg chain and need no floor of their own. - const plan = [ - day(1, 1, [stop({ assignmentId: 1, name: 'Day room', checkInTime: '08:00', checkoutAt: 1 * 1440 + 480 })]), - day(2, 2, [stop({ assignmentId: 2, name: 'Bergedorf' })]), - ]; - - const chains = spillChains(plan, [], everyLeg(11)); - - expect(chains.find((c) => c.dayNumber === 2)!.schedule.entries[0]!.arrival).toBeNull(); - }); - }); }); diff --git a/shared/src/roadtrip/nightSpill.ts b/shared/src/roadtrip/nightSpill.ts index 07185044d..9c51af731 100644 --- a/shared/src/roadtrip/nightSpill.ts +++ b/shared/src/roadtrip/nightSpill.ts @@ -54,35 +54,6 @@ interface Placed { from: RoadtripStop | undefined; } -/** - * The minute each day is free to start on, for the days a booking still holds. - * - * A night booked into the next morning does not end at midnight: the room is given - * back at check-out, and until then the traveller is at the hotel and not at the - * first stop of the new day. Without this each day was scheduled from nothing, so a - * stop pinned before check-out read as perfectly fine. - * - * Only the day the check-out falls in. A stay running Monday to Thursday leaves - * Tuesday and Wednesday alone, which is the point: you sleep there, you do not sit - * there. And the bound is a floor, never a move. A stop with a clock of its own - * keeps it and collects the late warning any leg it cannot make would give it. - */ -export function earliestFreeMinute(days: PlanDay[]): Map { - const held = new Map(); - for (const day of days) { - for (const stop of day.stops) { - if (stop.checkoutAt === undefined) continue; - const checkoutDay = Math.floor(stop.checkoutAt / 1440); - // A check-out on the stop's own day binds nothing: that is a day trip, and the - // stops after it already follow through the leg chain. - if (checkoutDay <= day.dayNumber) continue; - const minute = stop.checkoutAt - checkoutDay * 1440; - held.set(checkoutDay, Math.max(held.get(checkoutDay) ?? 0, minute)); - } - } - return held; -} - export function spillChains(plan: PlanDay[], quietDays: QuietDay[], legFor: LegLookup): SpillChain[] { const all: PlanDay[] = [ ...plan, @@ -95,7 +66,6 @@ export function spillChains(plan: PlanDay[], quietDays: QuietDay[], legFor: LegL })), ].sort((a, b) => a.dayNumber - b.dayNumber); const numbers = new Set(all.map((d) => d.dayNumber)); - const heldUntil = earliestFreeMinute(all); const landing = new Map(); for (const d of all) landing.set(d.dayNumber, []); @@ -109,7 +79,6 @@ export function spillChains(plan: PlanDay[], quietDays: QuietDay[], legFor: LegL departureAt: s.checkoutAt === undefined ? undefined : s.checkoutAt - d.dayNumber * 1440, })), legs.map((l) => l?.duration), - { notBefore: heldUntil.get(d.dayNumber) ?? null }, ); let day = 0; diff --git a/shared/src/roadtrip/roadtripModel.ts b/shared/src/roadtrip/roadtripModel.ts index 184ac55bd..efcb9177a 100644 --- a/shared/src/roadtrip/roadtripModel.ts +++ b/shared/src/roadtrip/roadtripModel.ts @@ -154,25 +154,13 @@ function resolveArrival( }; } -/** - * @param opts.notBefore Minute of this day before which the first stop cannot be - * reached, because a booking still holds the traveller somewhere else: the night - * before ran into this morning and its check-out is when the room is given back. - * Behaves like the arrival of an imaginary stop just before the first, so a stop - * pinned earlier keeps its clock and picks up the same late warning it would from - * any other leg it cannot make in time. - */ -export function computeSchedule( - stops: ScheduleStop[], - legSeconds: (number | undefined)[], - opts: { notBefore?: number | null } = {}, -): Schedule { +export function computeSchedule(stops: ScheduleStop[], legSeconds: (number | undefined)[]): Schedule { const warnings: ScheduleWarning[] = []; const arrivals: (number | null)[] = new Array(stops.length).fill(null); const anchored: boolean[] = new Array(stops.length).fill(false); - let cursor: number | null = opts.notBefore ?? null; + let cursor: number | null = null; let dayOffset = 0; for (let i = 0; i < stops.length; i++) {