diff --git a/client/src/components/Roadtrip/stageMap.test.ts b/client/src/components/Roadtrip/stageMap.test.ts index 8a63dcd43..529ac81b8 100644 --- a/client/src/components/Roadtrip/stageMap.test.ts +++ b/client/src/components/Roadtrip/stageMap.test.ts @@ -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) diff --git a/client/src/components/Roadtrip/stageMap.ts b/client/src/components/Roadtrip/stageMap.ts index 89079c6bc..51b45d4e3 100644 --- a/client/src/components/Roadtrip/stageMap.ts +++ b/client/src/components/Roadtrip/stageMap.ts @@ -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 { diff --git a/shared/src/roadtrip/assemble.spec.ts b/shared/src/roadtrip/assemble.spec.ts index 9445f49db..decd3447c 100644 --- a/shared/src/roadtrip/assemble.spec.ts +++ b/shared/src/roadtrip/assemble.spec.ts @@ -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 = {}; + 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]); + }); }); diff --git a/shared/src/roadtrip/assemble.ts b/shared/src/roadtrip/assemble.ts index 0c282c393..fc2754507 100644 --- a/shared/src/roadtrip/assemble.ts +++ b/shared/src/roadtrip/assemble.ts @@ -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); }