fix(roadtrip): four more from the audit, each one traced to what the user would see

**"Today" was read off UTC while the clock beside it is local.** `localDate.ts` exists for
exactly this and says so in its own comment: between local midnight and the UTC rollover the
two disagree, so east of Greenwich the up-next card hung on yesterday's stage for the first
hours of every night, which is when somebody on a long stage is actually driving. Both the
stage hook and the corridor hook now ask the same helper the rest of the app asks.

**A refused day end left the switch showing a state the trip never reached.** The writer
catches its own failures and shows a toast, so it never throws and the sheet's catch could
not fire, which left the optimistic value standing. It answers whether the day end moved
now, and the switch comes back when it did not. The desktop control ignores that answer, as
it reads its state back from the trip.

**A timed-out corridor search reported more unsearched boxes than it had.** Every worker
still alive ran the deadline branch and each added the whole remainder, so three workers
reported three times the boxes left, which can exceed the number of boxes there ever were.
The remainder is claimed before it is counted, which also ends the other workers' loops.

**A night that fell mid-leg had its morning drive dropped from the stage map.** The join
mark went on the first leg of every day opening on an automatic night, but only a night
standing ON a stop is a connection between two days. `dayWindow` records that difference as
a whole `position` against a fractional one, and where it is fractional the morning drive is
the last stretch to this day's own first stop. It was being filtered off the stage while its
distance stayed in the day's total.
This commit is contained in:
Maurice
2026-09-17 16:49:03 +02:00
parent c8f9edad87
commit 7806560c8f
9 changed files with 95 additions and 15 deletions
+13 -3
View File
@@ -1239,17 +1239,27 @@ export function useTripPlanner() {
[dayBoundaries.boundaries],
);
const setRoadtripEndDay = useCallback(async (stop: RoadtripStop) => {
if (!dailyTimesActive || !can('day_edit', trip)) return
/**
* Returns whether the day end actually moved.
*
* It reports rather than throws, because it shows its own toast and a second one from the
* caller would be the same news twice. A caller that flipped a switch optimistically has
* to hear about a refusal all the same, or it sits there showing a state the trip never
* reached: the phone sheet's catch was unreachable for exactly this reason.
*/
const setRoadtripEndDay = useCallback(async (stop: RoadtripStop): Promise<boolean> => {
if (!dailyTimesActive || !can('day_edit', trip)) return false
try {
const manual = dayBoundaries.boundaries.find(b => b.to_assignment_id === null && b.from_assignment_id === stop.assignmentId)
if (manual) {
await dayBoundaries.save(manual.day_number, null)
if (!stop.endDay) return
if (!stop.endDay) return true
}
await tripActions.setAssignmentEndDay(tripId, stop.ownerDayId, stop.assignmentId, !stop.endDay)
return true
} catch (err: unknown) {
toast.error(err instanceof Error ? err.message : t('common.unknownError'))
return false
}
}, [dailyTimesActive, can, trip, tripActions, tripId, toast, t, dayBoundaries.boundaries, dayBoundaries.save])