From 791468e5b39198c2ba2d0938b478f529327821f0 Mon Sep 17 00:00:00 2001 From: Maurice Date: Fri, 24 Jul 2026 12:20:23 +0200 Subject: [PATCH] fix(airtrail): also treat an endpoint-count > 2 booking as a local multi-leg shape, mirroring the server so a locally-grown chain is not mislabelled removed --- .../components/Planner/ReservationsPanel.test.tsx | 13 +++++++++++++ client/src/components/Planner/ReservationsPanel.tsx | 8 ++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/client/src/components/Planner/ReservationsPanel.test.tsx b/client/src/components/Planner/ReservationsPanel.test.tsx index ccd3a797f..1afd90925 100644 --- a/client/src/components/Planner/ReservationsPanel.test.tsx +++ b/client/src/components/Planner/ReservationsPanel.test.tsx @@ -493,4 +493,17 @@ describe('ReservationsPanel', () => { expect(screen.getByText('Not synced')).toBeInTheDocument(); expect(screen.getByTitle('This flight was removed in AirTrail and no longer syncs.')).toBeInTheDocument(); }); + + it('FE-PLANNER-RESP-049: a flight grown into multiple legs locally (endpoints > 2, no legs array) shows the layover hint, not "Not synced"', () => { + // Matches the server's second hasLocalMultiLegShape criterion (endpoint count > 2). + const res = buildReservation({ + title: 'Grown multi-leg', type: 'flight', external_source: 'airtrail', sync_enabled: 0, + endpoints: [{ sequence: 0 }, { sequence: 1 }, { sequence: 2 }], + } as any); + render(); + expect(screen.queryByText('Not synced')).not.toBeInTheDocument(); + expect( + screen.getByTitle('Imported from AirTrail. A multi-leg flight with a layover has no single AirTrail flight to sync back to, so it stays as a one-time import.'), + ).toBeInTheDocument(); + }); }); diff --git a/client/src/components/Planner/ReservationsPanel.tsx b/client/src/components/Planner/ReservationsPanel.tsx index 3b6d314d4..7e2d14227 100644 --- a/client/src/components/Planner/ReservationsPanel.tsx +++ b/client/src/components/Planner/ReservationsPanel.tsx @@ -101,11 +101,15 @@ function ReservationCard({ r, tripId, onEdit, onDelete, files = [], onNavigateTo // no single flight to round-trip a layover chain to, so it's created with // sync_enabled=0 (#1535). Distinguish it from a flight that was removed // upstream so the badge doesn't falsely claim it "was removed in AirTrail" (#1646). + // Mirror the server's hasLocalMultiLegShape (airtrailSync.ts): a metadata legs + // array of length > 1, OR — for a flight grown into multiple legs locally, which + // carries no legs array — more than two endpoints. Both are detached, not removed. const airtrailMultiLeg = r.external_source === 'airtrail' && !r.sync_enabled && (() => { try { const m = typeof r.metadata === 'string' ? JSON.parse(r.metadata || '{}') : (r.metadata || {}) - return Array.isArray(m?.legs) && m.legs.length > 1 - } catch { return false } + if (Array.isArray(m?.legs) && m.legs.length > 1) return true + } catch { /* malformed metadata — fall through to the endpoint count */ } + return (r.endpoints || []).length > 2 })() const attachedFiles = files.filter(f => f.reservation_id === r.id || (f.linked_reservation_ids || []).includes(r.id)) const linked = r.assignment_id ? assignmentLookup[r.assignment_id] : null