mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-21 14:36:31 +00:00
fix(roadtrip): a stop with a start is driven where the day list shows it
The day list draws a day by its starts, the road trip drives the order it is stored in. Moving a stop with a start onto another day, or adding a place that carries one, stored it at the end of the day or where it was dropped, so the list showed it among the stops its hour falls between while the road trip went there last. It is now stored right behind the row the list draws it after, with a booked night read by its check-in as the server's time sort reads it. A stop without a start lands exactly where it did before, and wherever a moved stop lands in the middle of a day, the via points drawn on that day stay on the legs they were drawn for. Behind a later start the road trip reached such a stop late, and the night spill took its earlier clock for a midnight: the stop and every stop after it opened the next day's card, and a day left with one stop fell to the placeholders at the bottom. A stop the schedule marks late now stays on the day the schedule files it on, and the stops behind it are read from its hour, so a clock that goes back past that hour still starts the next day. The desktop rail, the phone's road trip tab and the MCP calculate_roadtrip tool share that spill.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { parseTimeToMinutes, getSpanPhase, hidesOnMiddleDay, getTransportRouteEndpoints, getDisplayTimeForDay, getTransportForDay, getAssignmentReservations, getMergedItems } from './dayMerge'
|
||||
import { parseTimeToMinutes, getSpanPhase, hidesOnMiddleDay, getTransportRouteEndpoints, getDisplayTimeForDay, getTransportForDay, getAssignmentReservations, getMergedItems, timedSlot } from './dayMerge'
|
||||
|
||||
describe('parseTimeToMinutes', () => {
|
||||
it('parses HH:MM string', () => {
|
||||
@@ -303,3 +303,52 @@ describe('getMergedItems', () => {
|
||||
expect(result.map(i => i.data.id)).toEqual([1, 3, 10, 2])
|
||||
})
|
||||
})
|
||||
|
||||
// A stop with a start is drawn by it, and the road trip drives the stored order, so a
|
||||
// stop joining a day from elsewhere is stored where the list will draw it.
|
||||
describe('timedSlot', () => {
|
||||
const visit = (order_index: number, place_time: string | null, accommodation_id: number | null = null) =>
|
||||
({ order_index, accommodation_id, place: { place_time } })
|
||||
const day = [visit(0, '09:00'), visit(1, '11:00'), visit(2, '14:00')]
|
||||
|
||||
it('leaves a stop without a start to the drop', () => {
|
||||
expect(timedSlot(day, [], null, 1)).toBeNull()
|
||||
expect(timedSlot(day, [], undefined)).toBeNull()
|
||||
expect(timedSlot(day, [], 'soon')).toBeNull()
|
||||
})
|
||||
|
||||
it('puts a stop between the stored stops its start falls between, wherever it was dropped', () => {
|
||||
expect(timedSlot(day, [], '10:00')).toBe(1)
|
||||
expect(timedSlot(day, [], '10:00', 2)).toBe(1)
|
||||
expect(timedSlot(day, [], '10:00', 0)).toBe(1)
|
||||
expect(timedSlot(day, [], '08:00')).toBe(0)
|
||||
expect(timedSlot(day, [], '15:00', 0)).toBe(3)
|
||||
})
|
||||
|
||||
it('keeps the drop among the stops without a time behind the same start', () => {
|
||||
const loose = [visit(0, '09:00'), visit(1, null), visit(2, '11:00')]
|
||||
expect(timedSlot(loose, [], '10:00')).toBe(2)
|
||||
expect(timedSlot(loose, [], '10:00', 1)).toBe(1)
|
||||
})
|
||||
|
||||
it('reads the day by order_index, not by the order the rows come in', () => {
|
||||
const shuffled = [visit(2, '14:00'), visit(0, '09:00'), visit(1, '11:00')]
|
||||
expect(timedSlot(shuffled, [], '12:00')).toBe(2)
|
||||
expect(timedSlot([], [], '12:00')).toBe(0)
|
||||
})
|
||||
|
||||
it('stores it right behind the stop the list draws it after on a day stored against its hours', () => {
|
||||
// Stored 11:00 then 09:00: the list draws 09:00, 10:00, 11:00, so 10:00 goes behind 09:00.
|
||||
expect(timedSlot([visit(0, '11:00'), visit(1, '09:00')], [], '10:00')).toBe(2)
|
||||
})
|
||||
|
||||
it('counts the booked night the list hides, timed by its check-in', () => {
|
||||
const nights = [{ id: 7, check_in: '15:00' }, { id: 8, check_in: null }]
|
||||
const withCheckIn = [visit(0, '09:00'), visit(1, null, 7), visit(2, '16:00')]
|
||||
expect(timedSlot(withCheckIn, nights, '12:00')).toBe(1)
|
||||
expect(timedSlot(withCheckIn, nights, '15:30')).toBe(2)
|
||||
// Without a check-in the night leads the day and stays in front.
|
||||
const leading = [visit(0, null, 8), visit(1, '09:00')]
|
||||
expect(timedSlot(leading, nights, '08:00')).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -356,3 +356,49 @@ export function getMergedItems(opts: {
|
||||
|
||||
return applyChronoOrder(result.sort((a, b) => a.sortKey - b.sortKey), dayId, getDisplayTime)
|
||||
}
|
||||
|
||||
/** A stored day row, as far as `timedSlot` reads it. */
|
||||
interface StoredVisit {
|
||||
order_index: number
|
||||
accommodation_id?: number | null
|
||||
place?: { place_time?: string | null } | null
|
||||
}
|
||||
|
||||
/**
|
||||
* Where a stop joining a day is stored when it carries a start of its own, as an index
|
||||
* into the day's stored rows sorted by order_index. Null when it has none: that stop
|
||||
* goes where it was dropped, as it always has.
|
||||
*
|
||||
* The list draws a day by time (`applyChronoOrder`) and the road trip drives it in the
|
||||
* order it is stored. Put at the end of the day, or wherever it was dropped, a stop with
|
||||
* a start sat in one place on the list and in another on the road, and behind a later
|
||||
* start the road trip reached it late. So it is stored right behind the row it will be
|
||||
* drawn after. Among stops without a time it keeps the spot it was dropped on; where the
|
||||
* drop and the start disagree, the start wins.
|
||||
*
|
||||
* Read over every stored row, the ones the list hides included, because those are what
|
||||
* the index counts. A booked night is timed by its check-in, the way the server's time
|
||||
* sort reads it, so a stop pinned before the check-in lands ahead of the hotel and one
|
||||
* pinned after it behind. A night without one counts as a stop without a time, so a
|
||||
* night that leads its day keeps leading it.
|
||||
*/
|
||||
export function timedSlot(
|
||||
day: readonly StoredVisit[],
|
||||
nights: readonly { id: number; check_in?: string | null }[],
|
||||
start: string | null | undefined,
|
||||
dropAt?: number | null,
|
||||
): number | null {
|
||||
const minutes = parseTimeToMinutes(start)
|
||||
if (minutes === null) return null
|
||||
const stored = [...day].sort((a, b) => a.order_index - b.order_index)
|
||||
const at = dropAt == null ? stored.length : Math.min(Math.max(dropAt, 0), stored.length)
|
||||
const joining: StoredVisit = { order_index: -1 }
|
||||
const startOf = (row: StoredVisit): number | null => {
|
||||
if (row === joining) return minutes
|
||||
const checkIn = row.accommodation_id == null ? null : nights.find(n => n.id === row.accommodation_id)?.check_in
|
||||
return parseTimeToMinutes(row.place?.place_time ?? checkIn)
|
||||
}
|
||||
const drawn = chronoOrder([...stored.slice(0, at), joining, ...stored.slice(at)], startOf)
|
||||
const before = drawn[drawn.indexOf(joining) - 1]
|
||||
return before ? stored.indexOf(before) + 1 : 0
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user