fix(maps): build the day's road from the stops the day plan shows

Booking a night writes the hotel onto its check-in day as a stop, for the road
trip's driving chain. The day plan hides that stop and draws the hotel as the
day's bookends instead, but the route builder behind the map line and the
phone's travel connectors still read it from the store. It carries no time of
its own, and a timed booking is seated behind the last timed stop, so on a day
that opened with a flight the hidden stop led the day: the map drove from the
hotel to the departure airport and back to the hotel after landing.

The builder now leaves the booked night's stop out, as the list does, and the
bookend rules put the hotel where it belongs: behind the flight, on the road
from the arrival airport. A hotel the traveller placed as a stop of their own
is untouched.

Closes #2430
This commit is contained in:
Maurice
2026-09-21 17:57:42 +02:00
parent 84d3dd0c4a
commit 903378f8ad
2 changed files with 52 additions and 1 deletions
@@ -102,6 +102,46 @@ describe('buildDayRouteRuns', () => {
expect(runs.flat().some(p => p.lat === 48.80)).toBe(false)
})
it('FE-MAP-DRP-009: the stop a booked night wrote is no waypoint; the hotel is the bookend, behind the flight (#2430)', () => {
// Check-in at four, a flight in the morning, nothing else: the day plan hides the
// hotel's own stop and shows the flight, then the hotel. The road used to start at
// the hotel and drive to the departure airport, because the untimed stop kept
// its stored place ahead of the timed booking.
const hotel = { id: 30, trip_id: 1, place_lat: 53.5465, place_lng: 9.9727, start_day_id: 1, end_day_id: 3, check_in: '16:00', check_out: '11:00' } as unknown as Accommodation
const flight = {
id: 7, trip_id: 1, type: 'flight', title: 'KL 1783', status: 'confirmed', day_id: 1, end_day_id: 1,
reservation_time: '2026-10-19T10:00', reservation_end_time: '2026-10-19T11:00',
endpoints: [
{ role: 'from', sequence: 0, name: 'AMS', lat: 52.3105, lng: 4.7683 },
{ role: 'to', sequence: 1, name: 'HAM', lat: 53.6304, lng: 9.9882 },
],
} as unknown as Reservation
const runs = buildDayRouteRuns(1, inputs({
days: [buildDay({ id: 1, day_number: 1 }), buildDay({ id: 2, day_number: 2 }), buildDay({ id: 3, day_number: 3 })],
assignments: { '1': [at(53.5465, 9.9727, 0, { accommodation_id: 30 })] },
reservations: [flight],
accommodations: [hotel],
optimizeFromAccommodation: true,
}))
// One road: from the arrival airport to the hotel. None out of the hotel.
expect(runs).toHaveLength(1)
expect(runs[0].map(p => [p.lat, p.lng])).toEqual([[53.6304, 9.9882], [53.5465, 9.9727]])
})
it('FE-MAP-DRP-010: a hotel the traveller placed as a stop of their own still ends the road (#2430)', () => {
// The same day, but the hotel stop is not the booking's: it stays a waypoint, and
// the bookend rule declines the zero-kilometre leg onto itself as before.
const hotel = { id: 30, trip_id: 1, place_lat: 53.5465, place_lng: 9.9727, start_day_id: 1, end_day_id: 3, check_in: '16:00', check_out: '11:00' } as unknown as Accommodation
const runs = buildDayRouteRuns(1, inputs({
days: [buildDay({ id: 1, day_number: 1 }), buildDay({ id: 3, day_number: 3 })],
assignments: { '1': [at(53.5503, 9.9937, 0), at(53.5465, 9.9727, 1)] },
accommodations: [hotel],
optimizeFromAccommodation: true,
}))
expect(runs).toHaveLength(1)
expect(runs[0].map(p => p.lat)).toEqual([53.5503, 53.5465])
})
it('FE-MAP-DRP-008: a day that is not in the trip has no route', () => {
expect(buildDayRouteRuns(99, inputs({ assignments: { '1': [at(48.86, 2.35, 0)] } }))).toEqual([])
})
+12 -1
View File
@@ -34,7 +34,18 @@ export interface DayRouteInputs {
*/
export function buildDayRouteRuns(dayId: number, input: DayRouteInputs): DayRoutePoint[][] {
const { days: allDays, assignments, reservations: allReservations, accommodations, optimizeFromAccommodation } = input
const da = (assignments[String(dayId)] || []).slice().sort((a, b) => a.order_index - b.order_index)
// Without the stop a booked night wrote. The day plan hides that stop (the list
// and `useTripPlanner` both filter it) and draws the hotel as the day's bookends
// instead, so the road has to be built from the same stops the list shows. Left
// in, the stop sat first on a day that opened with a flight, since it carries no
// time of its own and a timed booking is seated behind the last timed stop: the
// map then drove from the hotel to the airport the traveller had not landed at
// yet, and back after the flight (#2430). Road trip mode keeps the stop; it plans
// its own drive and never comes through here.
const da = (assignments[String(dayId)] || [])
.filter(a => a.accommodation_id == null)
.slice()
.sort((a, b) => a.order_index - b.order_index)
const dayOrder = (id: number | null | undefined): number | null => {
if (id == null) return null
const d = allDays.find(x => x.id === id)