fix(booking-import): only resolve leg days from local_date on a prefill

Resolving every waypoint day from endpoint.local_date also changed the edit path,
where it inverts the precedence the form uses everywhere else: local_date is
denormalised and the server only restamps it in the day_id branch of insertDay,
and only when reservation_time is set. Dragging a booking to another day rewrites
day_id/end_day_id and leaves the endpoints alone, so local_date points at the old
day — and reopening the form and pressing Update silently moved the booking back.

Gate the lookup on a prefill, where there is no saved day to lose, and the edit
path behaves exactly as it did before. Same change in the mobile sheet, which
shares the seeding code and would otherwise still show empty day selectors on an
imported booking.
This commit is contained in:
Maurice
2026-07-26 11:38:02 +02:00
committed by Maurice
parent 68ebc700cb
commit eae2ade740
3 changed files with 75 additions and 19 deletions
@@ -513,4 +513,47 @@ describe('TransportModal', () => {
expect(payload.metadata.legs).toBeUndefined();
expect(payload.metadata.train_number).toBe('RE 9');
});
// ── Per-endpoint day resolution (#1684) ────────────────────────────────────
const spanDays = [
{ id: 10, trip_id: 1, day_number: 1, date: '2026-08-01', title: 'Day 1' },
{ id: 11, trip_id: 1, day_number: 2, date: '2026-08-02', title: 'Day 2' },
{ id: 12, trip_id: 1, day_number: 3, date: '2026-08-03', title: 'Day 3' },
] as any;
const flightEndpoints = (fromDate: string, toDate: string) => ([
{ id: 1, reservation_id: 1, role: 'from', sequence: 0, name: 'Frankfurt (FRA)', code: 'FRA', lat: 50.03, lng: 8.57, timezone: 'Europe/Berlin', local_date: fromDate, local_time: '10:00' },
{ id: 2, reservation_id: 1, role: 'to', sequence: 1, name: 'New York (JFK)', code: 'JFK', lat: 40.64, lng: -73.78, timezone: 'America/New_York', local_date: toDate, local_time: '13:00' },
]);
it('FE-PLANNER-TRANSMODAL-030: an import prefill resolves each waypoint day from its endpoint local_date (#1684)', async () => {
const onSave = vi.fn().mockResolvedValue(undefined);
// A parsed import carries local_date per endpoint but no day_id at all.
const prefill = { title: 'LH 400', type: 'flight', status: 'pending', endpoints: flightEndpoints('2026-08-02', '2026-08-03') } as any;
render(<TransportModal {...defaultProps} days={spanDays} prefill={prefill} onSave={onSave} />);
await userEvent.click(screen.getByRole('button', { name: /^Add$/i }));
await waitFor(() => expect(onSave).toHaveBeenCalled());
const payload = onSave.mock.calls[0][0];
expect(payload.day_id).toBe(11);
expect(payload.end_day_id).toBe(12);
});
it('FE-PLANNER-TRANSMODAL-031: editing keeps the saved days when the endpoint local_date is stale', async () => {
const onSave = vi.fn().mockResolvedValue(undefined);
// Dragging a booking to another day rewrites day_id/end_day_id but leaves the
// endpoints untouched, so local_date lags behind. Re-saving must not move the
// booking back to the day that stale date points at.
const reservation = buildReservation({
id: 1, title: 'LH 400', type: 'flight', day_id: 10, end_day_id: 11,
reservation_time: '2026-08-01T10:00', reservation_end_time: '2026-08-02T13:00',
endpoints: flightEndpoints('2026-08-03', '2026-08-03'),
} as any) as any;
render(<TransportModal {...defaultProps} days={spanDays} reservation={reservation} onSave={onSave} />);
await userEvent.click(screen.getByRole('button', { name: /^Update$/i }));
await waitFor(() => expect(onSave).toHaveBeenCalled());
const payload = onSave.mock.calls[0][0];
expect(payload.day_id).toBe(10);
expect(payload.end_day_id).toBe(11);
});
});
@@ -234,6 +234,13 @@ export function TransportModal({ isOpen, onClose, onSave, reservation, days, sel
meta_platform: meta.platform || '',
meta_seat: meta.seat || '',
})
// Only an import prefill carries a per-endpoint local_date without a day_id. On an
// edit the saved day wins: local_date is denormalised and can lag behind after a
// day drag, insertDay or a trip-date shift, so resolving from it would silently
// move the booking to another day on a plain re-save.
const endpointDayId = (ep?: { local_date?: string | null } | null) =>
reservation ? '' : resolveDayId(days, ep?.local_date)
if (type === 'flight') {
const orderedEps = orderedEndpoints(src)
const metaLegs: any[] = Array.isArray(meta.legs) ? meta.legs : []
@@ -247,12 +254,12 @@ export function TransportModal({ isOpen, onClose, onSave, reservation, days, sel
return {
airport: airportFromEndpoint(ep),
// An import prefill gives each endpoint its own local_date but no day_id, so
// resolve the day from that date (mirrors the top-level start_day_id fallback)
// — otherwise the review's per-leg day selectors render empty even though the
// time, read from the same endpoint, is filled.
arrDayId: legInto?.arr_day_id ?? (resolveDayId(days, ep.local_date) || (isLast ? (src.end_day_id ?? '') : '')),
// resolve the day from that date — otherwise the review's per-leg day
// selectors render empty even though the time, read from the same
// endpoint, is filled.
arrDayId: legInto?.arr_day_id ?? (endpointDayId(ep) || (isLast ? (src.end_day_id ?? '') : '')),
arrTime: legInto?.arr_time ?? (!isFirst ? (ep.local_time ?? '') : ''),
depDayId: legOut?.dep_day_id ?? (resolveDayId(days, ep.local_date) || (isFirst ? (src.day_id ?? '') : '')),
depDayId: legOut?.dep_day_id ?? (endpointDayId(ep) || (isFirst ? (src.day_id ?? '') : '')),
depTime: legOut?.dep_time ?? (!isLast ? (ep.local_time ?? '') : ''),
airline: legOut?.airline ?? (isFirst ? (meta.airline ?? '') : ''),
flight_number: legOut?.flight_number ?? (isFirst ? (meta.flight_number ?? '') : ''),
@@ -261,13 +268,13 @@ export function TransportModal({ isOpen, onClose, onSave, reservation, days, sel
})
} else {
// Legacy flight with no (or partial) endpoints — seed two waypoints.
const dep = emptyWaypoint(resolveDayId(days, from?.local_date) || (src.day_id ?? ''))
const dep = emptyWaypoint(endpointDayId(from) || (src.day_id ?? ''))
dep.airport = airportFromEndpoint(from)
dep.depTime = splitReservationDateTime(src.reservation_time).time ?? ''
dep.airline = meta.airline ?? ''
dep.flight_number = meta.flight_number ?? ''
dep.seat = meta.seat ?? ''
const arr = emptyWaypoint(resolveDayId(days, to?.local_date) || (src.end_day_id ?? src.day_id ?? ''))
const arr = emptyWaypoint(endpointDayId(to) || (src.end_day_id ?? src.day_id ?? ''))
arr.airport = airportFromEndpoint(to)
arr.arrTime = splitReservationDateTime(src.reservation_end_time).time ?? ''
wps = [dep, arr]
@@ -291,9 +298,9 @@ export function TransportModal({ isOpen, onClose, onSave, reservation, days, sel
location: locationFromEndpoint(ep),
// See the flight branch: resolve each station's day from its endpoint local_date
// so an import prefill doesn't leave the per-leg day selectors empty.
arrDayId: legInto?.arr_day_id ?? (resolveDayId(days, ep.local_date) || (isLast ? (src.end_day_id ?? '') : '')),
arrDayId: legInto?.arr_day_id ?? (endpointDayId(ep) || (isLast ? (src.end_day_id ?? '') : '')),
arrTime: legInto?.arr_time ?? (!isFirst ? (ep.local_time ?? '') : ''),
depDayId: legOut?.dep_day_id ?? (resolveDayId(days, ep.local_date) || (isFirst ? (src.day_id ?? '') : '')),
depDayId: legOut?.dep_day_id ?? (endpointDayId(ep) || (isFirst ? (src.day_id ?? '') : '')),
depTime: legOut?.dep_time ?? (!isLast ? (ep.local_time ?? '') : ''),
train_number: legOut?.train_number ?? (isFirst ? (meta.train_number ?? '') : ''),
platform: legOut?.platform ?? (isFirst ? (meta.platform ?? '') : ''),
@@ -301,13 +308,13 @@ export function TransportModal({ isOpen, onClose, onSave, reservation, days, sel
}
})
} else {
const dep = emptyStationWaypoint(resolveDayId(days, from?.local_date) || (src.day_id ?? ''))
const dep = emptyStationWaypoint(endpointDayId(from) || (src.day_id ?? ''))
dep.location = locationFromEndpoint(from)
dep.depTime = splitReservationDateTime(src.reservation_time).time ?? ''
dep.train_number = meta.train_number ?? ''
dep.platform = meta.platform ?? ''
dep.seat = meta.seat ?? ''
const arr = emptyStationWaypoint(resolveDayId(days, to?.local_date) || (src.end_day_id ?? src.day_id ?? ''))
const arr = emptyStationWaypoint(endpointDayId(to) || (src.end_day_id ?? src.day_id ?? ''))
arr.location = locationFromEndpoint(to)
arr.arrTime = splitReservationDateTime(src.reservation_end_time).time ?? ''
wps = [dep, arr]
@@ -189,6 +189,12 @@ export default function MTransportFormSheet({ planner, onOpenExpense }: MTranspo
confirmation_number: src.confirmation_number || '',
notes: src.notes || '',
})
// Only an import prefill carries a per-endpoint local_date without a day_id. On an
// edit the saved day wins: local_date is denormalised and can lag behind after a
// day drag, insertDay or a trip-date shift (mirrors TransportModal).
const endpointDayId = (ep?: { local_date?: string | null } | null) =>
editingTransport ? '' : resolveDayId(days, ep?.local_date)
if (type === 'flight') {
const orderedEps = orderedEndpoints(src)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -202,9 +208,9 @@ export default function MTransportFormSheet({ planner, onOpenExpense }: MTranspo
const isLast = i === orderedEps.length - 1
return {
airport: airportFromEndpoint(ep),
arrDayId: legInto?.arr_day_id ?? (isLast ? (src.end_day_id ?? '') : ''),
arrDayId: legInto?.arr_day_id ?? (endpointDayId(ep) || (isLast ? (src.end_day_id ?? '') : '')),
arrTime: legInto?.arr_time ?? (!isFirst ? (ep.local_time ?? '') : ''),
depDayId: legOut?.dep_day_id ?? (isFirst ? (src.day_id ?? '') : ''),
depDayId: legOut?.dep_day_id ?? (endpointDayId(ep) || (isFirst ? (src.day_id ?? '') : '')),
depTime: legOut?.dep_time ?? (!isLast ? (ep.local_time ?? '') : ''),
airline: legOut?.airline ?? (isFirst ? (meta.airline ?? '') : ''),
flight_number: legOut?.flight_number ?? (isFirst ? (meta.flight_number ?? '') : ''),
@@ -212,13 +218,13 @@ export default function MTransportFormSheet({ planner, onOpenExpense }: MTranspo
}
})
} else {
const dep = emptyWaypoint(src.day_id ?? '')
const dep = emptyWaypoint(endpointDayId(from) || (src.day_id ?? ''))
dep.airport = airportFromEndpoint(from)
dep.depTime = splitReservationDateTime(src.reservation_time).time ?? ''
dep.airline = meta.airline ?? ''
dep.flight_number = meta.flight_number ?? ''
dep.seat = meta.seat ?? ''
const arr = emptyWaypoint(src.end_day_id ?? src.day_id ?? '')
const arr = emptyWaypoint(endpointDayId(to) || (src.end_day_id ?? src.day_id ?? ''))
arr.airport = airportFromEndpoint(to)
arr.arrTime = splitReservationDateTime(src.reservation_end_time).time ?? ''
wps = [dep, arr]
@@ -239,9 +245,9 @@ export default function MTransportFormSheet({ planner, onOpenExpense }: MTranspo
const isLast = i === orderedEps.length - 1
return {
location: locationFromEndpoint(ep),
arrDayId: legInto?.arr_day_id ?? (isLast ? (src.end_day_id ?? '') : ''),
arrDayId: legInto?.arr_day_id ?? (endpointDayId(ep) || (isLast ? (src.end_day_id ?? '') : '')),
arrTime: legInto?.arr_time ?? (!isFirst ? (ep.local_time ?? '') : ''),
depDayId: legOut?.dep_day_id ?? (isFirst ? (src.day_id ?? '') : ''),
depDayId: legOut?.dep_day_id ?? (endpointDayId(ep) || (isFirst ? (src.day_id ?? '') : '')),
depTime: legOut?.dep_time ?? (!isLast ? (ep.local_time ?? '') : ''),
train_number: legOut?.train_number ?? (isFirst ? (meta.train_number ?? '') : ''),
platform: legOut?.platform ?? (isFirst ? (meta.platform ?? '') : ''),
@@ -249,13 +255,13 @@ export default function MTransportFormSheet({ planner, onOpenExpense }: MTranspo
}
})
} else {
const dep = emptyStationWaypoint(src.day_id ?? '')
const dep = emptyStationWaypoint(endpointDayId(from) || (src.day_id ?? ''))
dep.location = locationFromEndpoint(from)
dep.depTime = splitReservationDateTime(src.reservation_time).time ?? ''
dep.train_number = meta.train_number ?? ''
dep.platform = meta.platform ?? ''
dep.seat = meta.seat ?? ''
const arr = emptyStationWaypoint(src.end_day_id ?? src.day_id ?? '')
const arr = emptyStationWaypoint(endpointDayId(to) || (src.end_day_id ?? src.day_id ?? ''))
arr.location = locationFromEndpoint(to)
arr.arrTime = splitReservationDateTime(src.reservation_end_time).time ?? ''
wps = [dep, arr]