diff --git a/client/src/components/Planner/TransportModal.test.tsx b/client/src/components/Planner/TransportModal.test.tsx
index 4210988df..07a1d1303 100644
--- a/client/src/components/Planner/TransportModal.test.tsx
+++ b/client/src/components/Planner/TransportModal.test.tsx
@@ -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();
+ 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();
+ 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);
+ });
});
diff --git a/client/src/components/Planner/TransportModal.tsx b/client/src/components/Planner/TransportModal.tsx
index c8d237f4d..9dadfb963 100644
--- a/client/src/components/Planner/TransportModal.tsx
+++ b/client/src/components/Planner/TransportModal.tsx
@@ -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]
diff --git a/client/src/mobile/screens/trip/sheets/MTransportFormSheet.tsx b/client/src/mobile/screens/trip/sheets/MTransportFormSheet.tsx
index aa2eb0171..b269982e6 100644
--- a/client/src/mobile/screens/trip/sheets/MTransportFormSheet.tsx
+++ b/client/src/mobile/screens/trip/sheets/MTransportFormSheet.tsx
@@ -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]