feat(planner): reach a hotel's booking from the plan, not only its place

A booked stay's pill in the day head gets a second target that opens the booking; the name still opens the place. The place panel of a hotel, on the desktop and on the phone, lists the stay booked for it next to the bookings pinned to the stop. (#2363)
This commit is contained in:
Maurice
2026-10-01 09:43:53 +02:00
parent 104f0eef38
commit c80ee6b12a
34 changed files with 155 additions and 17 deletions
+23 -1
View File
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest'
import { parseTimeToMinutes, getSpanPhase, hidesOnMiddleDay, getTransportRouteEndpoints, getDisplayTimeForDay, getTransportForDay, getAssignmentReservations, getMergedItems, rideSeatKey, stayStartingOn, storedRideSlot, timedSlot } from './dayMerge'
import { parseTimeToMinutes, getSpanPhase, hidesOnMiddleDay, getTransportRouteEndpoints, getDisplayTimeForDay, getTransportForDay, getAssignmentReservations, getPlaceBookings, getMergedItems, rideSeatKey, stayStartingOn, storedRideSlot, timedSlot } from './dayMerge'
describe('parseTimeToMinutes', () => {
it('parses HH:MM string', () => {
@@ -199,6 +199,28 @@ describe('getTransportForDay', () => {
})
})
describe('getPlaceBookings (#2363)', () => {
const reservations = [
{ id: 1, assignment_id: 42, reservation_time: '2025-06-01T10:00:00' },
{ id: 2, assignment_id: null, reservation_time: '2025-06-01T15:00:00', accommodation_place_id: 9 },
{ id: 3, assignment_id: 42, reservation_time: '2025-06-01T09:00:00', accommodation_place_id: 9 },
{ id: 4, assignment_id: null, reservation_time: null, accommodation_place_id: 8 },
]
it('adds the stay booked for a hotel after the bookings pinned to the stop, each once', () => {
expect(getPlaceBookings(reservations, 42, 9).map(r => r.id)).toEqual([3, 1, 2])
})
it('finds the stay even when no stop is open, as from the pill in the day head', () => {
expect(getPlaceBookings(reservations, null, 9).map(r => r.id)).toEqual([2, 3])
})
it('without a place it is the pinned list alone', () => {
expect(getPlaceBookings(reservations, 42, null).map(r => r.id)).toEqual([3, 1])
expect(getPlaceBookings(reservations, null, 7)).toEqual([])
})
})
describe('getAssignmentReservations', () => {
it('returns every booking pinned to the assignment, not just the first (#2201)', () => {
const reservations = [
+19
View File
@@ -267,6 +267,25 @@ export function getAssignmentReservations<T extends {
})
}
/**
* The bookings that belong to a place: the ones pinned to the open stop, and the
* stay booked for it when the place is a hotel (#2363). Without the second, a
* hotel opened from its pill in the day head showed the place but no route to its
* booking. Pinned ones keep their order and come first.
*/
export function getPlaceBookings<T extends {
id: number
assignment_id?: number | null
reservation_time?: string | null
accommodation_place_id?: number | null
}>(reservations: T[], assignmentId: number | null | undefined, placeId: number | null | undefined): T[] {
const pinned = getAssignmentReservations(reservations, assignmentId)
if (placeId == null) return pinned
const pinnedIds = new Set(pinned.map(r => r.id))
const stays = reservations.filter(r => r.accommodation_place_id === placeId && !pinnedIds.has(r.id))
return [...pinned, ...stays]
}
/**
* Order items chronologically: anything with a time (a place's place_time, a
* transport/leg display time, a timed note) sorts by that time. An item WITHOUT a