fix(roadtrip): the connection built by a day window is a connection too

The last change marked the drive into the next day and kept it off a phone stage, but it
only caught the connection `connectDays` builds through `inboundAt`. A trip with a day
window set never takes that path: the window switches `inboundAt` off and builds the days
itself, so day 2 opens on an automatic night standing where day 1 stopped and its first
leg is the drive on from there. That leg is the same connection, it just arrives as one
of day 2's own legs under day 2's number, and the stage drew the whole way back to
yesterday's last stop on today's map. The setting is not even involved: with a window it
happens either way.

So the leg out of an opening night is marked as a join as well, and the phone drops it
with the rest.

One exception, because the filter must not empty the screen: a day that is nothing but
driving, night to night with no stop of its own, has only the connection to show, and
there the line IS the day. When dropping the joins would leave a stage with no line at
all, it keeps them.
This commit is contained in:
Maurice
2026-09-17 13:55:56 +02:00
parent c0ff4dabb3
commit 84923b97a7
4 changed files with 71 additions and 3 deletions
@@ -135,6 +135,14 @@ describe('stageMapData with a stage', () => {
expect(stageMapData(withJoin, stage, true).lines).toEqual([LINES[1]])
})
it('FE-RTSTAGE-020: a day that is nothing but driving keeps its connection rather than going blank', () => {
// Night to night, no stop of its own: the connection is the only line the day has, and
// the line IS the day. Better a map that shows the drive than an empty one.
const onlyJoin = routes({ lineDays: [1, 2, 3, 3], lineJoins: [false, true, false, false] })
expect(stageMapData(onlyJoin, stage, true).lines).toEqual([LINES[1]])
})
it('FE-RTSTAGE-017: the colours still line up once a join has been dropped', () => {
const data = stageMapData(routes({ lineJoins: [false, false, true, false] }), stage, true)
+7 -2
View File
@@ -59,8 +59,13 @@ export function stageMapData(
// was the longest thing on screen. The desktop has the same connection, but it has it
// beside every other day, which is the view the setting exists for; here that view is
// the "all days" one, a tap away on the same map.
const keep: number[] = []
routes.lineDays.forEach((n, i) => { if (n === stage.dayNumber && !routes.lineJoins?.[i]) keep.push(i) })
const ofDay: number[] = []
routes.lineDays.forEach((n, i) => { if (n === stage.dayNumber) ofDay.push(i) })
const own = ofDay.filter(i => !routes.lineJoins?.[i])
// A day that is nothing but driving — no stop of its own, night to night — has only the
// connection to show. Dropping it there would leave the map blank on the very day the
// line IS the day, so the filter gives way rather than emptying the screen.
const keep = own.length ? own : ofDay
const placeIds = stagePlaceIds(stage)
return {
+50
View File
@@ -145,6 +145,39 @@ describe('assembleRoadtrip connected days', () => {
});
}
/** The same two days, driven under an 08:00 to 20:00 window, which builds the nights. */
function assembleWindowedTwoDays() {
const first = [stop({ ownerIndex: 0 }), stop({ ownerIndex: 1 }), stop({ ownerIndex: 2 })];
const second = [
stop({ ownerDayId: 2, ownerIndex: 0, assignmentId: 300, placeId: 400, lat: 60 }),
stop({ ownerDayId: 2, ownerIndex: 1, assignmentId: 301, placeId: 401, lat: 61 }),
];
const allLegs: Record<string, RoutedLeg> = {};
for (const run of [first, second, [first[2]!, second[0]!]]) {
run.slice(0, -1).forEach((from, i) => {
allLegs[legKey(from, run[i + 1]!)] = lineFrom(from, run[i + 1]!);
});
}
return assembleRoadtrip({
plan: [
{ dayId: 1, dayNumber: 1, date: '2026-06-01', title: null, stops: first },
{ dayId: 2, dayNumber: 2, date: '2026-06-02', title: null, stops: second },
],
quietDays: [],
window: { start: 8 * 60, end: 20 * 60, endMode: 'time' } as never,
distanceUnit: 'metric',
allLegs,
snapByDay: {},
missedByDay: {},
loading: false,
limits: { rangeKm: null, legMinutes: null, dayMinutes: null },
vehicleKind: null,
connectDays: false,
boundaries: [],
labels: { start: 'start', end: 'end' },
});
}
it('ROADTRIP-ASSEMBLE-004: the drive drawn at the head of a connected day names the stop it left from', () => {
const routes = assembleTwoDays(true);
const [first, second] = routes.days;
@@ -181,4 +214,21 @@ describe('assembleRoadtrip connected days', () => {
expect(routes.lineDays).toEqual([1, 1, 2]);
expect(routes.lineJoins).toEqual([false, false, false]);
});
it('ROADTRIP-ASSEMBLE-008: with a day window the connection runs through the night stop, and is marked there', () => {
// A window builds the days itself: day 2 opens on an automatic night standing where
// day 1 stopped, and its first leg is the drive on from there. That leg is the SAME
// connection `connectDays` draws without a window, but it arrives as one of day 2's
// own legs, under day 2's number, and `inboundAt` is switched off entirely. Unmarked,
// a surface showing one day had no way to tell it from a drive within the day, and the
// phone drew the whole way back to yesterday's last stop on today's map.
const routes = assembleWindowedTwoDays();
const second = routes.days.find((d) => d.dayNumber === 2)!;
expect(second.stops[0]!.automaticNight?.phase).toBe('start');
// Day 1 drives its own two legs; day 2 opens with the drive on from the night and then
// drives its own. Only that first one of day 2's is the connection.
expect(routes.lineDays).toEqual([1, 1, 2, 2]);
expect(routes.lineJoins).toEqual([false, false, true, false]);
});
});
+6 -1
View File
@@ -149,7 +149,12 @@ export function assembleRoadtrip({
if (leg.line.length > 1) {
lines.push(leg.line);
lineDays.push(chain.dayNumber);
lineJoins.push(false);
// A day that opens on an automatic night opens where the last one stopped, and its
// first leg is the drive on from there. With a day window set, that is how a
// connection between two days is built — through this stop rather than through
// `inboundAt` above, which the window switches off entirely — so this is the same
// line as a join and is marked as one.
lineJoins.push(chain.stops[i]?.automaticNight?.phase === 'start');
}
segments.push(leg.seg);
}