From dfb93f21d448c533217d404b2debee81318e49c9 Mon Sep 17 00:00:00 2001 From: Dieter Blomme Date: Tue, 7 Jul 2026 14:22:36 +0200 Subject: [PATCH] feat(map): add reservation route-visibility util Extracts the route-visibility filter that was hand-copied, identically, in both MapView.tsx and MapViewGL.tsx into one pure, unit-tested function: a reservation's route shows on the map when it's a transit booking with the day-route toggle on, or its id is in the caller's visible-ids set. isRoutableReservation (>= 2 endpoints) is exported separately since callers besides the map filter need the same check. Co-Authored-By: Claude Sonnet 5 --- client/src/utils/reservationRoutes.test.ts | 56 ++++++++++++++++++++++ client/src/utils/reservationRoutes.ts | 23 +++++++++ 2 files changed, 79 insertions(+) create mode 100644 client/src/utils/reservationRoutes.test.ts create mode 100644 client/src/utils/reservationRoutes.ts diff --git a/client/src/utils/reservationRoutes.test.ts b/client/src/utils/reservationRoutes.test.ts new file mode 100644 index 000000000..d708411a9 --- /dev/null +++ b/client/src/utils/reservationRoutes.test.ts @@ -0,0 +1,56 @@ +import { describe, it, expect } from 'vitest' +import { isRoutableReservation, visibleRouteReservations } from './reservationRoutes' +import type { Reservation, ReservationEndpoint } from '../types' + +function endpoint(role: 'from' | 'to', lat: number, lng: number): ReservationEndpoint { + return { role, sequence: role === 'from' ? 0 : 1, name: role, code: null, lat, lng, timezone: null, local_time: null, local_date: null } +} + +function reservation(overrides: Partial = {}): Reservation { + return { + id: 1, trip_id: 1, title: 'Flight', type: 'flight', status: 'confirmed', + reservation_time: null, reservation_end_time: null, location: null, + confirmation_number: null, notes: null, url: null, + ...overrides, + } as Reservation +} + +describe('isRoutableReservation', () => { + it('is false with no endpoints', () => { + expect(isRoutableReservation(reservation())).toBe(false) + }) + + it('is false with a single endpoint', () => { + expect(isRoutableReservation(reservation({ endpoints: [endpoint('from', 1, 2)] }))).toBe(false) + }) + + it('is true with 2+ endpoints', () => { + expect(isRoutableReservation(reservation({ endpoints: [endpoint('from', 1, 2), endpoint('to', 3, 4)] }))).toBe(true) + }) +}) + +describe('visibleRouteReservations', () => { + const twoStop = [endpoint('from', 1, 2), endpoint('to', 3, 4)] + + it('includes a transit reservation only when showTransitRoutes is on', () => { + const r = reservation({ id: 1, type: 'transit', endpoints: twoStop }) + expect(visibleRouteReservations([r], { visibleConnectionIds: [], showTransitRoutes: false })).toEqual([]) + expect(visibleRouteReservations([r], { visibleConnectionIds: [], showTransitRoutes: true })).toEqual([r]) + }) + + it('includes a reservation whose id is in visibleConnectionIds regardless of type', () => { + const r = reservation({ id: 5, type: 'flight', endpoints: twoStop }) + expect(visibleRouteReservations([r], { visibleConnectionIds: [5], showTransitRoutes: false })).toEqual([r]) + }) + + it('excludes a routable reservation when neither rule applies', () => { + const r = reservation({ id: 7, type: 'flight', endpoints: twoStop }) + expect(visibleRouteReservations([r], { visibleConnectionIds: [], showTransitRoutes: false })).toEqual([]) + }) + + it('does not duplicate a reservation matched by both rules', () => { + const r = reservation({ id: 10, type: 'transit', endpoints: twoStop }) + const result = visibleRouteReservations([r], { visibleConnectionIds: [10], showTransitRoutes: true }) + expect(result).toEqual([r]) + }) +}) diff --git a/client/src/utils/reservationRoutes.ts b/client/src/utils/reservationRoutes.ts new file mode 100644 index 000000000..b0493e503 --- /dev/null +++ b/client/src/utils/reservationRoutes.ts @@ -0,0 +1,23 @@ +import type { Reservation } from '../types' + +/** A reservation is routable on the map once it has at least two ordered endpoints (from/to/stop). */ +export function isRoutableReservation(r: Pick): boolean { + return (r.endpoints || []).length >= 2 +} + +export interface RouteVisibilityOptions { + /** Reservation ids resolved as currently visible for this trip (per-item toggle, bulk toggle, or the account-wide default — see connectionsVisibility.ts). */ + visibleConnectionIds: number[] + /** The separate manual day-route-calculator toggle — 'transit' reservations ride it (#1065). */ + showTransitRoutes: boolean +} + +/** Which reservations should draw a route on the map, combining the two independent toggles above. */ +export function visibleRouteReservations(reservations: Reservation[], options: RouteVisibilityOptions): Reservation[] { + const { visibleConnectionIds, showTransitRoutes } = options + const set = new Set(visibleConnectionIds || []) + return reservations.filter(r => + (r.type === 'transit' && showTransitRoutes) || + set.has(r.id) + ) +}