mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
keep overnight checkout planning after a later arrival
This commit is contained in:
@@ -36,6 +36,8 @@ export const HANDLED_OUTSIDE_TRIP_STORE = [
|
||||
'roadtripVia:changed',
|
||||
'roadtripTrack:changed',
|
||||
'roadtripPreferences:changed',
|
||||
// components/Roadtrip/useDayBoundaries reloads the shared boundaries.
|
||||
'roadtripBoundary:changed',
|
||||
// In-app notifications — hooks/useInAppNotificationListener
|
||||
'notification:new',
|
||||
'notification:updated',
|
||||
|
||||
@@ -14,6 +14,6 @@ it('edits the shared stay without changing the day ending', () => {
|
||||
|
||||
it('keeps stay editing when daily travel times are off', () => {
|
||||
render(<VisitControls stay={{ minutes: null, onEdit: vi.fn() }} />)
|
||||
expect(screen.getByRole('button', { name: 'roadtrip.stop.stayShort 0 min' })).toBeEnabled()
|
||||
expect(screen.getByRole('button', { name: 'roadtrip.stop.stayShort +' })).toBeEnabled()
|
||||
expect(screen.queryByRole('button', { name: 'roadtrip.window.endHere' })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
@@ -86,7 +86,8 @@ const asStop = (a: Assignment, ownerDayId: number, ownerIndex: number, accommoda
|
||||
name: p.name,
|
||||
lat: p.lat,
|
||||
lng: p.lng,
|
||||
time: a.assignment_time ?? p.place_time ?? stay?.check_in ?? null,
|
||||
time: a.assignment_time ?? p.place_time ?? null,
|
||||
checkInTime: stay?.check_in ?? null,
|
||||
dwellMinutes: typeof p.duration_minutes === 'number' ? p.duration_minutes : null,
|
||||
endDay: a.end_day === true,
|
||||
legMode: a.leg_transport_mode ?? null,
|
||||
|
||||
@@ -20,7 +20,7 @@ describe('useContextMenu', () => {
|
||||
result.current.open({ clientX: 120, clientY: 40, preventDefault, stopPropagation } as unknown as React.MouseEvent, items)
|
||||
})
|
||||
|
||||
expect(result.current.menu).toEqual({ x: 120, y: 40, items })
|
||||
expect(result.current.menu).toEqual({ x: 120, y: 40, items, alignEnd: false })
|
||||
expect(preventDefault).toHaveBeenCalled()
|
||||
expect(stopPropagation).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -23,7 +23,7 @@ export function useContextMenu() {
|
||||
const open = (e: React.MouseEvent, items: MenuItem[], alignEnd = false) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
const anchor = e.currentTarget.getBoundingClientRect()
|
||||
const anchor = alignEnd ? e.currentTarget.getBoundingClientRect() : { right: e.clientX, bottom: e.clientY }
|
||||
setMenu({ x: alignEnd ? anchor.right : e.clientX, y: alignEnd ? anchor.bottom + 6 : e.clientY, items, alignEnd })
|
||||
}
|
||||
|
||||
|
||||
@@ -200,6 +200,8 @@ async function renderRoadtrip() {
|
||||
rt.routes.days = [{ ...rt.corridor.day, stops }]
|
||||
}
|
||||
rt.routes.days = rt.routes.days.map(day => ({
|
||||
schedule: { entries: [] },
|
||||
legs: [],
|
||||
...day,
|
||||
stops: (day.stops as Array<Record<string, unknown>>).map((stop, i) => ({
|
||||
ownerDayId: day.dayId, ownerIndex: i, ...stop,
|
||||
@@ -512,10 +514,8 @@ describe('useTripPlanner road trip: saving a hit', () => {
|
||||
await result.current.saveStopDraftAsNight({ endDayId: 6, checkIn: '15:00', checkOut: '10:00' })
|
||||
})
|
||||
|
||||
// No stop_type and no dwell: a hotel is where the day ends, not something the drive
|
||||
// passes through, so it keeps its number in the chain.
|
||||
const [, payload] = actions.addPlace.mock.calls[0] as unknown as [number, Record<string, unknown>]
|
||||
expect(payload.stop_type).toBeUndefined()
|
||||
expect(payload.stop_type).toBe('hotel')
|
||||
expect(payload.duration_minutes).toBeUndefined()
|
||||
expect(accommodationsApi.create).toHaveBeenCalledWith(42, {
|
||||
place_id: 900, start_day_id: 5, end_day_id: 6, check_in: '15:00', check_out: '10:00',
|
||||
@@ -1292,6 +1292,8 @@ describe('useTripPlanner road trip: folding a day off the map', () => {
|
||||
buildPlace({ id: 1104, lat: 48.13, lng: 11.58 }),
|
||||
],
|
||||
})
|
||||
const places = useTripStore.getState().places
|
||||
useTripStore.setState({ assignments: { '5': places.map((place, i) => buildAssignment({ id: i + 1, day_id: 5, place_id: place.id, place })) } })
|
||||
rt.corridor.day = { dayId: 5, dayNumber: 1 }
|
||||
rt.routes.days = [
|
||||
{ dayId: 5, dayNumber: 1, stops: [drawn(1101, 53.55, 9.99, 5, 0), drawn(1102, 52.52, 13.4, 5, 1)], geometry: LINES[0] },
|
||||
|
||||
@@ -113,7 +113,8 @@ export class RoadtripPlanService {
|
||||
name: v.name,
|
||||
lat: v.lat!,
|
||||
lng: v.lng!,
|
||||
time: v.time ?? v.check_in ?? null,
|
||||
time: v.time,
|
||||
checkInTime: v.check_in,
|
||||
...(v.checkout_day != null && parseClock(v.check_out) !== null ? { checkoutAt: v.checkout_day * 1440 + parseClock(v.check_out)! } : {}),
|
||||
dwellMinutes: v.duration_minutes,
|
||||
endDay: v.end_day === 1,
|
||||
|
||||
@@ -88,7 +88,7 @@ export function assembleRoadtrip({
|
||||
spills: [],
|
||||
schedule: computeSchedule(
|
||||
d.stops.map((s) => ({
|
||||
anchor: s.time,
|
||||
anchor: s.time ?? s.checkInTime ?? null,
|
||||
dwellMinutes: s.dwellMinutes,
|
||||
departureAt: s.checkoutAt === undefined ? undefined : s.checkoutAt - d.dayNumber * 1440,
|
||||
})),
|
||||
|
||||
@@ -390,3 +390,23 @@ describe('booked checkout', () => {
|
||||
expect(plan.chains[0]!.schedule.entries[1]!.arrival).toBe('14:00');
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps checkout when the drive arrives after suggested check-in', () => {
|
||||
const stops = [
|
||||
stop(1, { time: '07:00', dwellMinutes: 60 }),
|
||||
stop(2, { checkInTime: '09:11', checkoutAt: 2 * 1440 + 600 }),
|
||||
stop(3),
|
||||
];
|
||||
const plan = calculate(stops, [72.5, 29]);
|
||||
expect(plan.issue).toBeNull();
|
||||
expect(plan.chains[0]!.schedule.entries[1]!.arrival).toBe('09:13');
|
||||
expect(plan.chains[1]!.stops.at(-1)!.placeId).toBe(3);
|
||||
expect(plan.chains[1]!.schedule.entries.at(-1)!.arrival).toBe('10:29');
|
||||
});
|
||||
|
||||
it('waits for check-in without treating it as a fixed appointment', () => {
|
||||
const plan = calculate([stop(1), stop(2, { checkInTime: '15:00', checkoutAt: 2 * 1440 + 600 }), stop(3)], [60, 29]);
|
||||
expect(plan.issue).toBeNull();
|
||||
expect(plan.chains[0]!.schedule.entries[1]!.arrival).toBe('15:00');
|
||||
expect(plan.chains[1]!.schedule.entries.at(-1)!.arrival).toBe('10:29');
|
||||
});
|
||||
|
||||
@@ -182,6 +182,7 @@ export function planDayWindow(
|
||||
time: null,
|
||||
dwellMinutes: 0,
|
||||
checkoutAt: undefined,
|
||||
checkInTime: undefined,
|
||||
stopType: null,
|
||||
fillPercent: null,
|
||||
automaticNight: { phase: 'end', fromDayNumber: number, position, manual: targets.has(number) },
|
||||
@@ -273,6 +274,7 @@ export function planDayWindow(
|
||||
const target = targets.get(number);
|
||||
if (target !== undefined && target < position) return failed('conflict');
|
||||
if (pin !== null) clock = pin;
|
||||
if (pin === null && number === day.dayNumber) clock = Math.max(clock, parseClock(stop.checkInTime) ?? 0);
|
||||
append(stop, clock);
|
||||
previous = stop;
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ export function spillChains(plan: PlanDay[], quietDays: QuietDay[], legFor: LegL
|
||||
const legs = routed.map((l) => l?.seg);
|
||||
const schedule = computeSchedule(
|
||||
d.stops.map((s) => ({
|
||||
anchor: s.time,
|
||||
anchor: s.time ?? s.checkInTime ?? null,
|
||||
dwellMinutes: s.dwellMinutes,
|
||||
departureAt: s.checkoutAt === undefined ? undefined : s.checkoutAt - d.dayNumber * 1440,
|
||||
})),
|
||||
|
||||
@@ -19,6 +19,7 @@ export interface RoadtripStop {
|
||||
|
||||
dwellMinutes: number | null;
|
||||
checkoutAt?: number;
|
||||
checkInTime?: string | null;
|
||||
endDay?: boolean;
|
||||
|
||||
legMode: string | null;
|
||||
|
||||
Reference in New Issue
Block a user