mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
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:
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user