feat(planner): make transit journeys first-class entries (#1065)

A saved transit route is now its own reservation type instead of piggybacking
on train/bus: it gets a tram icon and its own color everywhere, and the day
timeline renders the itinerary inline — line badges in their official colors
with walk segments, plus the transfer count and walking time — instead of a
generic transport row.

Clicking the row opens the itinerary view (journey summary, stop-by-stop legs
with times, platforms, headsigns and operators) rather than the edit form;
editing stays reachable from an Edit action inside that view. The transit
type is registered across the timeline merge, transport modal, reservations
panel, file manager, map overlays and detail panels, with a translated type
label in every locale.
This commit is contained in:
Maurice
2026-07-02 18:08:28 +02:00
committed by Maurice
parent f2304e076b
commit 3c87d37cd9
38 changed files with 148 additions and 44 deletions
@@ -1 +1 @@
export const TRANSPORT_TYPES = new Set(['flight', 'train', 'bus', 'car', 'taxi', 'bicycle', 'cruise', 'ferry', 'transport_other'])
export const TRANSPORT_TYPES = new Set(['flight', 'train', 'bus', 'car', 'taxi', 'bicycle', 'cruise', 'ferry', 'transit', 'transport_other'])
@@ -2,7 +2,7 @@ import { createElement, useEffect, useMemo, useRef, useState } from 'react'
import { renderToStaticMarkup } from 'react-dom/server'
import { Marker, Polyline, Tooltip, useMap, useMapEvents } from 'react-leaflet'
import L from 'leaflet'
import { Plane, Train, Ship, Car, Bus, Sailboat, Bike, CarTaxiFront, Route } from 'lucide-react'
import { Plane, Train, Ship, Car, Bus, Sailboat, Bike, CarTaxiFront, Route, TramFront } from 'lucide-react'
import { escapeHtml } from '@trek/shared'
import { useSettingsStore } from '../../store/settingsStore'
import type { Reservation, ReservationEndpoint } from '../../types'
@@ -11,8 +11,8 @@ const ENDPOINT_PANE = 'reservation-endpoints'
const AIRPORT_BADGE_HALF_PX = 16
const BADGE_GAP_PX = 5
type TransportType = 'flight' | 'train' | 'cruise' | 'car' | 'bus' | 'taxi' | 'bicycle' | 'ferry' | 'transport_other'
const TRANSPORT_TYPES: TransportType[] = ['flight', 'train', 'cruise', 'car', 'bus', 'taxi', 'bicycle', 'ferry', 'transport_other']
type TransportType = 'flight' | 'train' | 'cruise' | 'car' | 'bus' | 'taxi' | 'bicycle' | 'ferry' | 'transit' | 'transport_other'
const TRANSPORT_TYPES: TransportType[] = ['flight', 'train', 'cruise', 'car', 'bus', 'taxi', 'bicycle', 'ferry', 'transit', 'transport_other']
const TRANSPORT_COLOR = '#3b82f6'
@@ -25,6 +25,7 @@ const TYPE_META: Record<TransportType, { color: string; icon: typeof Plane; geod
taxi: { color: TRANSPORT_COLOR, icon: CarTaxiFront, geodesic: false },
bicycle: { color: TRANSPORT_COLOR, icon: Bike, geodesic: false },
ferry: { color: TRANSPORT_COLOR, icon: Sailboat, geodesic: true },
transit: { color: TRANSPORT_COLOR, icon: TramFront, geodesic: false },
transport_other: { color: TRANSPORT_COLOR, icon: Route, geodesic: false },
}
@@ -9,15 +9,15 @@
import { createElement } from 'react'
import { renderToStaticMarkup } from 'react-dom/server'
import type mapboxgl from 'mapbox-gl'
import { Plane, Train, Ship, Car, Bus, Sailboat, Bike, CarTaxiFront, Route } from 'lucide-react'
import { Plane, Train, Ship, Car, Bus, Sailboat, Bike, CarTaxiFront, Route, TramFront } from 'lucide-react'
import { escapeHtml } from '@trek/shared'
import type { Reservation, ReservationEndpoint } from '../../types'
export const RESERVATION_SOURCE_ID = 'trek-reservations'
export const RESERVATION_LINE_LAYER_ID = 'trek-reservations-lines'
type TransportType = 'flight' | 'train' | 'cruise' | 'car' | 'bus' | 'taxi' | 'bicycle' | 'ferry' | 'transport_other'
const TRANSPORT_TYPES: TransportType[] = ['flight', 'train', 'cruise', 'car', 'bus', 'taxi', 'bicycle', 'ferry', 'transport_other']
type TransportType = 'flight' | 'train' | 'cruise' | 'car' | 'bus' | 'taxi' | 'bicycle' | 'ferry' | 'transit' | 'transport_other'
const TRANSPORT_TYPES: TransportType[] = ['flight', 'train', 'cruise', 'car', 'bus', 'taxi', 'bicycle', 'ferry', 'transit', 'transport_other']
const TRANSPORT_COLOR = '#3b82f6'
const TYPE_META: Record<TransportType, { icon: typeof Plane; geodesic: boolean }> = {
@@ -29,6 +29,7 @@ const TYPE_META: Record<TransportType, { icon: typeof Plane; geodesic: boolean }
taxi: { icon: CarTaxiFront, geodesic: false },
bicycle: { icon: Bike, geodesic: false },
ferry: { icon: Sailboat, geodesic: true },
transit: { icon: TramFront, geodesic: false },
transport_other: { icon: Route, geodesic: false },
}
@@ -1,9 +1,9 @@
import React, { useState, useEffect, useRef } from 'react'
import ReactDOM from 'react-dom'
import { X, Sun, Cloud, CloudRain, CloudSnow, CloudDrizzle, CloudLightning, Wind, Droplets, Sunrise, Sunset, Hotel, Calendar, MapPin, LogIn, LogOut, Hash, Pencil, Plane, Utensils, Train, Car, Ship, Ticket, FileText, Users, ChevronsDown, ChevronsUp } from 'lucide-react'
import { X, Sun, Cloud, CloudRain, CloudSnow, CloudDrizzle, CloudLightning, Wind, Droplets, Sunrise, Sunset, Hotel, Calendar, MapPin, LogIn, LogOut, Hash, Pencil, Plane, Utensils, Train, Car, Ship, Ticket, FileText, Users, ChevronsDown, ChevronsUp, TramFront } from 'lucide-react'
const RES_TYPE_ICONS = { flight: Plane, hotel: Hotel, restaurant: Utensils, train: Train, car: Car, cruise: Ship, event: Ticket, tour: Users, other: FileText }
const RES_TYPE_COLORS = { flight: '#3b82f6', hotel: '#8b5cf6', restaurant: '#ef4444', train: '#06b6d4', car: '#6b7280', cruise: '#0ea5e9', event: '#f59e0b', tour: '#10b981', other: '#6b7280' }
const RES_TYPE_ICONS = { flight: Plane, hotel: Hotel, restaurant: Utensils, train: Train, car: Car, cruise: Ship, transit: TramFront, event: Ticket, tour: Users, other: FileText }
const RES_TYPE_COLORS = { flight: '#3b82f6', hotel: '#8b5cf6', restaurant: '#ef4444', train: '#06b6d4', car: '#6b7280', cruise: '#0ea5e9', transit: '#7c3aed', event: '#f59e0b', tour: '#10b981', other: '#6b7280' }
import { weatherApi, accommodationsApi } from '../../api/client'
import { useCanDo } from '../../store/permissionsStore'
import { useTripStore } from '../../store/tripStore'
@@ -1,11 +1,11 @@
import {
FileText, Info, Clock, MapPin, Navigation, Train, Plane, Bus, Car, Ship,
Coffee, Ticket, Star, Heart, Camera, Flag, Lightbulb, AlertTriangle,
ShoppingBag, Bookmark, Hotel, Utensils, Users, Sailboat, Bike, CarTaxiFront, Route,
ShoppingBag, Bookmark, Hotel, Utensils, Users, Sailboat, Bike, CarTaxiFront, Route, TramFront,
Wine, ParkingSquare, Fuel, Footprints, Mountain, Waves, Sun, Umbrella, Music, Landmark, Gift,
} from 'lucide-react'
export const RES_ICONS = { flight: Plane, hotel: Hotel, restaurant: Utensils, train: Train, car: Car, cruise: Ship, bus: Bus, ferry: Sailboat, bicycle: Bike, taxi: CarTaxiFront, transport_other: Route, event: Ticket, tour: Users, other: FileText }
export const RES_ICONS = { flight: Plane, hotel: Hotel, restaurant: Utensils, train: Train, car: Car, cruise: Ship, bus: Bus, ferry: Sailboat, bicycle: Bike, taxi: CarTaxiFront, transit: TramFront, transport_other: Route, event: Ticket, tour: Users, other: FileText }
export const NOTE_ICONS = [
{ id: 'FileText', Icon: FileText },
@@ -50,4 +50,4 @@ export const TYPE_ICONS = {
transport_other: '🧭', event: '🎫', other: '📋',
}
export const TRANSPORT_DETAIL_COLORS = { flight: '#3b82f6', train: '#06b6d4', bus: '#059669', ferry: '#0d9488', bicycle: '#84cc16', taxi: '#ca8a04', car: '#6b7280', cruise: '#0ea5e9', transport_other: '#6b7280' }
export const TRANSPORT_DETAIL_COLORS = { flight: '#3b82f6', train: '#06b6d4', bus: '#059669', ferry: '#0d9488', bicycle: '#84cc16', taxi: '#ca8a04', car: '#6b7280', cruise: '#0ea5e9', transit: '#7c3aed', transport_other: '#6b7280' }
@@ -321,6 +321,36 @@ describe('DayPlanSidebar', () => {
expect(screen.queryByLabelText('Edit')).not.toBeInTheDocument()
})
it('FE-PLANNER-DAYPLAN-104: a transit journey renders line chips and opens its itinerary view, not the edit form (#1065)', async () => {
const user = userEvent.setup()
const onEditTransport = vi.fn()
const day = buildDay({ id: 10, date: '2025-06-01', title: 'Day 1' })
const res = {
...buildReservation({
id: 300, type: 'transit', title: 'Fernsehturm → Zoo',
reservation_time: '2025-06-01T08:30:00', day_id: 10,
}),
metadata: {
transit: {
provider: 'transitous', duration: 1800, transfers: 1, walk_seconds: 240,
legs: [
{ mode: 'WALK', duration: 240, from: { name: 'Start' }, to: { name: 'Alexanderplatz' } },
{ mode: 'SUBWAY', line: 'U2', line_color: '#FF3300', line_text_color: '#FFFFFF', headsign: 'Ruhleben', duration: 1440, stops: 6, from: { name: 'Alexanderplatz', time: '08:36' }, to: { name: 'Zoo', time: '09:00' } },
],
},
},
}
render(<DayPlanSidebar {...makeDefaultProps({ days: [day], reservations: [res as any], onEditTransport })} />)
// Line chip + transfer summary render inline in the timeline row.
expect(screen.getByText('U2')).toBeInTheDocument()
expect(screen.getByText(/1 transfers/)).toBeInTheDocument()
// Clicking the row opens the itinerary detail view — not the edit form.
await user.click(screen.getByText('Fernsehturm → Zoo'))
expect(onEditTransport).not.toHaveBeenCalled()
expect(await screen.findByText('Itinerary')).toBeInTheDocument()
expect(screen.getByText(/Ruhleben/)).toBeInTheDocument()
})
// ── Day info button ─────────────────────────────────────────────────────
it('FE-PLANNER-DAYPLAN-018: clicking day header calls onDayDetail', async () => {
@@ -1955,6 +1955,11 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
subtitle = [meta.train_number, meta.platform ? `Gl. ${meta.platform}` : '', meta.seat ? `Sitz ${meta.seat}` : ''].filter(Boolean).join(' · ')
}
// A transit journey (#1065) renders its itinerary inline —
// line badges in their colors instead of a plain subtitle,
// so the connection is recognisable at a glance.
const transitMeta = res.type === 'transit' && meta.transit && Array.isArray(meta.transit.legs) ? meta.transit : null
// Multi-day span phase (single-leg / non-flight only — a
// multi-leg flight is shown as one row per leg, see below).
const spanLabel = res.__leg ? null : getSpanLabel(res, spanPhase)
@@ -1965,8 +1970,12 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
<React.Fragment key={`transport-${res.id}-${legKey}-${day.id}`}>
<div
onClick={() => {
if (!canEditDays) return
const target = reservations.find(x => x.id === res.id) ?? res
// A transit journey opens its itinerary view — the rich
// stop-by-stop breakdown, not the generic edit form (#1065).
// Editing stays reachable from inside that view.
if (transitMeta) { setTransportDetail(target); return }
if (!canEditDays) return
if (TRANSPORT_TYPES.has(res.type)) onEditTransport?.(target)
else onEditReservation?.(target)
}}
@@ -2024,7 +2033,7 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
borderTop: showDropLine ? '2px solid var(--text-primary)' : undefined,
borderBottom: showDropLineAfter ? '2px solid var(--text-primary)' : undefined,
background: `${color}08`,
cursor: canEditDays && onEditTransport ? 'pointer' : 'default', userSelect: 'none',
cursor: (transitMeta || (canEditDays && onEditTransport)) ? 'pointer' : 'default', userSelect: 'none',
transition: 'background 0.1s',
opacity: draggingId === res.id ? 0.4 : spanPhase === 'middle' ? 0.65 : 1,
}}
@@ -2068,7 +2077,31 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
)
})()}
</div>
{subtitle && (
{transitMeta ? (
<div style={{ display: 'flex', alignItems: 'center', gap: 4, marginTop: 3, flexWrap: 'wrap' }}>
{transitMeta.legs.map((leg: { mode?: string; line?: string | null; line_color?: string | null; line_text_color?: string | null }, li: number) => (
<React.Fragment key={li}>
{li > 0 && <span style={{ fontSize: 9, color: 'var(--text-faint)' }}>›</span>}
{leg.mode === 'WALK'
? <Footprints size={10} style={{ color: 'var(--text-faint)', flexShrink: 0 }} />
: (
<span style={{
display: 'inline-flex', alignItems: 'center', borderRadius: 4, padding: '0 5px',
fontSize: 'calc(9.5px * var(--fs-scale-caption, 1))', fontWeight: 700, lineHeight: '15px',
background: leg.line_color || 'var(--bg-tertiary)',
color: leg.line_color ? (leg.line_text_color || '#fff') : 'var(--text-primary)',
}}>
{leg.line || leg.mode}
</span>
)}
</React.Fragment>
))}
<span style={{ fontSize: 'calc(9.5px * var(--fs-scale-caption, 1))', color: 'var(--text-faint)', marginLeft: 2 }}>
{transitMeta.transfers > 0 ? t('transit.transfers', { count: transitMeta.transfers }) : t('transit.direct')}
{transitMeta.walk_seconds > 59 && <> · <Footprints size={9} style={{ display: 'inline', verticalAlign: '-1px' }} /> {t('transit.min', { count: Math.round(transitMeta.walk_seconds / 60) })}</>}
</span>
</div>
) : subtitle && (
<div style={{ fontSize: 'calc(10px * var(--fs-scale-caption, 1))', color: 'var(--text-faint)', marginTop: 2, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{subtitle}
</div>
@@ -2400,6 +2433,7 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
transportDetail={transportDetail}
setTransportDetail={setTransportDetail}
onNavigateToFiles={onNavigateToFiles}
onEdit={canEditDays && onEditTransport ? (res) => { setTransportDetail(null); onEditTransport(res) } : undefined}
t={t}
locale={locale}
timeFormat={timeFormat}
@@ -1,5 +1,5 @@
import ReactDOM from 'react-dom'
import { Ticket, FileText, ExternalLink, Footprints, ArrowRight } from 'lucide-react'
import { Ticket, FileText, ExternalLink, Footprints, ArrowRight, Pencil } from 'lucide-react'
import Markdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
import remarkBreaks from 'remark-breaks'
@@ -13,13 +13,15 @@ interface DayPlanSidebarTransportDetailModalProps {
transportDetail: Reservation | null
setTransportDetail: (v: Reservation | null) => void
onNavigateToFiles?: () => void
/** Opens the edit form for this reservation (shown as a footer action). */
onEdit?: (res: Reservation) => void
t: (key: string, params?: Record<string, any>) => string
locale: string
timeFormat: string
}
export function DayPlanSidebarTransportDetailModal({
transportDetail, setTransportDetail, onNavigateToFiles, t, locale, timeFormat,
transportDetail, setTransportDetail, onNavigateToFiles, onEdit, t, locale, timeFormat,
}: DayPlanSidebarTransportDetailModalProps) {
if (!transportDetail) return null
return ReactDOM.createPortal(
@@ -119,10 +121,24 @@ export function DayPlanSidebarTransportDetailModal({
{/* Public-transit itinerary (#1065) — legs from the transit search */}
{meta.transit?.legs && Array.isArray(meta.transit.legs) && meta.transit.legs.length > 0 && (
<>
{/* journey summary: duration · transfers · walking */}
<div className="bg-surface-tertiary text-content" style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 10, padding: '9px 12px', borderRadius: 8, fontSize: 'calc(13px * var(--fs-scale-body, 1))', fontWeight: 600, flexWrap: 'wrap' }}>
{meta.transit.duration > 0 && (
<span>{Math.floor(meta.transit.duration / 3600) > 0 ? `${Math.floor(meta.transit.duration / 3600)} h ${Math.round((meta.transit.duration % 3600) / 60)} min` : t('transit.min', { count: Math.round(meta.transit.duration / 60) })}</span>
)}
<span className="text-content-faint">·</span>
<span>{meta.transit.transfers > 0 ? t('transit.transfers', { count: meta.transit.transfers }) : t('transit.direct')}</span>
{meta.transit.walk_seconds > 59 && (
<>
<span className="text-content-faint">·</span>
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 4 }}><Footprints size={13} /> {t('transit.min', { count: Math.round(meta.transit.walk_seconds / 60) })}</span>
</>
)}
</div>
<div className="bg-surface-tertiary" style={{ padding: '10px 12px', borderRadius: 8 }}>
<div className="text-content-faint" style={{ fontSize: 'calc(9px * var(--fs-scale-caption, 1))', fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.03em', marginBottom: 8 }}>
{t('transit.itinerary')}
{typeof meta.transit.transfers === 'number' && meta.transit.transfers > 0 && ` · ${t('transit.transfers', { count: meta.transit.transfers })}`}
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{meta.transit.legs.map((leg: { mode?: string; line?: string | null; line_color?: string | null; line_text_color?: string | null; headsign?: string | null; duration?: number; stops?: number; from?: { name?: string; time?: string | null }; to?: { name?: string; time?: string | null } }, i: number) => {
@@ -163,6 +179,7 @@ export function DayPlanSidebarTransportDetailModal({
})}
</div>
</div>
</>
)}
{/* Notizen */}
@@ -210,8 +227,17 @@ export function DayPlanSidebarTransportDetailModal({
)
})()}
{/* Schließen */}
<div style={{ textAlign: 'right' }}>
{/* Aktionen */}
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
{onEdit && (
<button onClick={() => onEdit(res)} className="bg-surface-tertiary text-content" style={{
fontSize: 'calc(12px * var(--fs-scale-body, 1))',
border: 'none', borderRadius: 8, padding: '6px 14px', cursor: 'pointer', fontWeight: 600, fontFamily: 'inherit',
display: 'inline-flex', alignItems: 'center', gap: 5,
}}>
<Pencil size={12} /> {t('common.edit')}
</button>
)}
<button onClick={() => setTransportDetail(null)} className="bg-accent text-accent-text" style={{
fontSize: 'calc(12px * var(--fs-scale-body, 1))',
border: 'none', borderRadius: 8, padding: '6px 16px', cursor: 'pointer', fontWeight: 600, fontFamily: 'inherit',
@@ -109,7 +109,7 @@ function ReservationCard({ r, tripId, onEdit, onDelete, files = [], onNavigateTo
const hasCode = !!r.confirmation_number
const dateCols = [hasDate, hasTime, hasCode].filter(Boolean).length
const TRANSPORT_TYPES_SET = new Set(['flight', 'train', 'bus', 'car', 'taxi', 'bicycle', 'cruise', 'ferry', 'transport_other'])
const TRANSPORT_TYPES_SET = new Set(['flight', 'train', 'bus', 'car', 'taxi', 'bicycle', 'cruise', 'ferry', 'transit', 'transport_other'])
const isTransportType = TRANSPORT_TYPES_SET.has(r.type)
const isHotel = r.type === 'hotel'
// For a hotel linked to an accommodation, the accommodation's own start/end days are
@@ -98,7 +98,7 @@ describe('TransitSearchModal', () => {
await waitFor(() => expect(onAdd).toHaveBeenCalled())
const payload = onAdd.mock.calls[0][0]
expect(payload.type).toBe('train') // dominant leg = SUBWAY → train
expect(payload.type).toBe('transit') // first-class transit type (#1065)
expect(payload.title).toBe('Fernsehturm → Zoologischer Garten')
expect(payload.day_id).toBe(10)
expect(payload.reservation_time).toBe('2025-06-01T08:30')
@@ -56,20 +56,6 @@ function legIcon(mode: string) {
return Train
}
/** TREK reservation type for the itinerary's dominant (longest) transit leg. */
function dominantType(legs: TransitLeg[]): 'train' | 'bus' | 'ferry' | 'transport_other' {
let best: TransitLeg | null = null
for (const l of legs) {
if (l.mode === 'WALK') continue
if (!best || l.duration > best.duration) best = l
}
if (!best) return 'transport_other'
if (best.mode === 'BUS' || best.mode === 'COACH') return 'bus'
if (best.mode === 'FERRY') return 'ferry'
if (best.mode === 'FUNICULAR' || best.mode === 'AERIAL_LIFT') return 'transport_other'
return 'train'
}
function tzAt(lat: number, lng: number): string {
try { return tzlookup(lat, lng) } catch { return 'UTC' }
}
@@ -419,7 +405,10 @@ export default function TransitSearchModal({ isOpen, onClose, day, days, places,
const payload = {
title: `${from.name} → ${to.name}`,
type: dominantType(it.legs),
// Its own first-class type: transit journeys get their own icon, their
// rich timeline row and the itinerary detail view instead of the
// generic transport rendering.
type: 'transit',
status: 'confirmed',
day_id: day.id,
end_day_id: endDay ? endDay.id : day.id,
@@ -1,6 +1,6 @@
import { useState, useEffect, useRef } from 'react'
import { useParams } from 'react-router-dom'
import { Plane, Train, Car, Ship, Bus, Sailboat, Bike, CarTaxiFront, Route, Paperclip, FileText, X, ExternalLink, Link2, Plus, Trash2 } from 'lucide-react'
import { Plane, Train, Car, Ship, Bus, Sailboat, Bike, CarTaxiFront, Route, TramFront, Paperclip, FileText, X, ExternalLink, Link2, Plus, Trash2 } from 'lucide-react'
import Modal from '../shared/Modal'
import CustomSelect from '../shared/CustomSelect'
import CustomTimePicker from '../shared/CustomTimePicker'
@@ -20,7 +20,7 @@ import type { BookingExpenseRequest } from './BookingCostsSection.types'
import type { BookingReviewDraft } from './parsedItemToDraft'
import { typeToCostCategory } from '@trek/shared'
const TRANSPORT_TYPES = ['flight', 'train', 'bus', 'car', 'taxi', 'bicycle', 'cruise', 'ferry', 'transport_other'] as const
const TRANSPORT_TYPES = ['flight', 'train', 'bus', 'car', 'taxi', 'bicycle', 'cruise', 'ferry', 'transit', 'transport_other'] as const
type TransportType = typeof TRANSPORT_TYPES[number]
interface EndpointPick {
@@ -96,6 +96,7 @@ const TYPE_OPTIONS = [
{ value: 'bicycle', labelKey: 'reservations.type.bicycle', Icon: Bike },
{ value: 'cruise', labelKey: 'reservations.type.cruise', Icon: Ship },
{ value: 'ferry', labelKey: 'reservations.type.ferry', Icon: Sailboat },
{ value: 'transit', labelKey: 'reservations.type.transit', Icon: TramFront },
{ value: 'transport_other', labelKey: 'reservations.type.transport_other', Icon: Route },
]
@@ -43,7 +43,7 @@ export function parsedItemToDraft(item: BookingImportPreviewItem): BookingReview
}
/** Transport types route to the TransportModal; everything else to the ReservationModal. */
const TRANSPORT_TYPES = new Set(['flight', 'train', 'bus', 'car', 'taxi', 'bicycle', 'cruise', 'ferry', 'transport_other'])
const TRANSPORT_TYPES = new Set(['flight', 'train', 'bus', 'car', 'taxi', 'bicycle', 'cruise', 'ferry', 'transit', 'transport_other'])
export function isTransportItem(item: BookingImportPreviewItem): boolean {
return TRANSPORT_TYPES.has(item.type)
}
+1 -1
View File
@@ -7,7 +7,7 @@ import { getDayBookendHotels } from '../utils/dayOrder'
import type { TripStoreState } from '../store/tripStore'
import type { RouteSegment, RouteResult, Accommodation } from '../types'
const TRANSPORT_TYPES = ['flight', 'train', 'bus', 'car', 'taxi', 'bicycle', 'cruise', 'ferry', 'transport_other']
const TRANSPORT_TYPES = ['flight', 'train', 'bus', 'car', 'taxi', 'bicycle', 'cruise', 'ferry', 'transit', 'transport_other']
const NO_ACCOMMODATIONS: Accommodation[] = []
+1 -1
View File
@@ -1,4 +1,4 @@
export const TRANSPORT_TYPES = new Set(['flight', 'train', 'bus', 'car', 'taxi', 'bicycle', 'cruise', 'ferry', 'transport_other'])
export const TRANSPORT_TYPES = new Set(['flight', 'train', 'bus', 'car', 'taxi', 'bicycle', 'cruise', 'ferry', 'transit', 'transport_other'])
export interface MergedItem {
type: 'place' | 'note' | 'transport'