import React from 'react' import ReactDOM from 'react-dom' import { useState, useRef, useEffect, useMemo } from 'react' import { Plane, X, Check } from 'lucide-react' import type { AirtrailFlight, AirtrailImportResult } from '@trek/shared' import { useTranslation } from '../../i18n' import { useToast } from '../shared/Toast' import { airtrailApi, reservationsApi } from '../../api/client' import { useTripStore } from '../../store/tripStore' interface AirTrailImportModalProps { isOpen: boolean onClose: () => void tripId: number pushUndo?: (label: string, undoFn: () => Promise | void) => void } /** Locale-aware date (e.g. de → 13.06.2026, en-US → 06/13/2026). */ function fmtDate(d: string | null, locale: string): string { if (!d) return '' try { return new Date(d + 'T00:00:00Z').toLocaleDateString(locale, { year: 'numeric', month: '2-digit', day: '2-digit', timeZone: 'UTC', }) } catch { return d } } export default function AirTrailImportModal({ isOpen, onClose, tripId, pushUndo }: AirTrailImportModalProps) { const { t, locale } = useTranslation() const toast = useToast() const trip = useTripStore(s => s.trip) const reservations = useTripStore(s => s.reservations) const loadReservations = useTripStore(s => s.loadReservations) const mouseDownTarget = useRef(null) const [loading, setLoading] = useState(false) const [importing, setImporting] = useState(false) const [error, setError] = useState('') const [flights, setFlights] = useState([]) const [selected, setSelected] = useState>(() => new Set()) // AirTrail flight ids already linked to a reservation in this trip. const importedIds = useMemo(() => { const set = new Set() for (const r of reservations) { if (r.external_source === 'airtrail' && r.external_id) set.add(String(r.external_id)) } return set }, [reservations]) const inRange = (f: AirtrailFlight): boolean => !!(f.date && trip?.start_date && trip?.end_date && f.date >= trip.start_date && f.date <= trip.end_date) useEffect(() => { if (!isOpen) return setError('') setSelected(new Set()) setLoading(true) airtrailApi .flights() .then((d: { flights: AirtrailFlight[] }) => { const list = d.flights ?? [] setFlights(list) // Pre-select the flights that fall inside the trip and aren't imported yet. const pre = new Set() for (const f of list) if (inRange(f) && !importedIds.has(f.id)) pre.add(f.id) setSelected(pre) }) .catch((err: any) => setError(err?.response?.data?.error ?? t('reservations.airtrail.loadError'))) .finally(() => setLoading(false)) // eslint-disable-next-line react-hooks/exhaustive-deps }, [isOpen]) const { during, others } = useMemo(() => { const during: AirtrailFlight[] = [] const others: AirtrailFlight[] = [] for (const f of flights) (inRange(f) ? during : others).push(f) const byDateDesc = (a: AirtrailFlight, b: AirtrailFlight) => (b.date ?? '').localeCompare(a.date ?? '') return { during: during.sort(byDateDesc), others: others.sort(byDateDesc) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [flights, trip?.start_date, trip?.end_date]) const toggle = (id: string) => { setSelected(prev => { const next = new Set(prev) if (next.has(id)) next.delete(id) else next.add(id) return next }) } const handleClose = () => { onClose() } const handleImport = async () => { const ids = [...selected].filter(id => !importedIds.has(id)) if (ids.length === 0 || importing) return setImporting(true) setError('') try { const result: AirtrailImportResult = await airtrailApi.import(tripId, ids) await loadReservations(tripId) const imported = result.imported ?? [] if (imported.length > 0) { pushUndo?.(t('reservations.airtrail.undo'), async () => { const linked = useTripStore.getState().reservations.filter( r => r.external_source === 'airtrail' && r.external_id && imported.includes(String(r.external_id)), ) await Promise.all(linked.map(r => reservationsApi.delete(tripId, r.id).catch(() => {}))) await loadReservations(tripId) }) toast.success(t('reservations.airtrail.imported', { count: imported.length })) } const skippedInTrip = (result.skipped ?? []).filter(s => s.reason === 'already-in-trip').length if (skippedInTrip > 0) toast.warning(t('reservations.airtrail.skippedDuplicate', { count: skippedInTrip })) if (imported.length === 0 && skippedInTrip === 0) toast.warning(t('reservations.airtrail.nothingImported')) handleClose() } catch (err: any) { setError(err?.response?.data?.error ?? t('reservations.airtrail.importError')) } finally { setImporting(false) } } const selectableCount = [...selected].filter(id => !importedIds.has(id)).length if (!isOpen) return null const renderFlight = (f: AirtrailFlight) => { const already = importedIds.has(f.id) const isSelected = selected.has(f.id) const label = f.flightNumber ? `${f.airline ? `${f.airline} ` : ''}${f.flightNumber}` : `${f.fromCode ?? '?'} → ${f.toCode ?? '?'}` return ( ) } return ReactDOM.createPortal(
{ mouseDownTarget.current = e.target }} onClick={e => { if (e.target === e.currentTarget && mouseDownTarget.current === e.currentTarget) handleClose() mouseDownTarget.current = null }} >
e.stopPropagation()} className="bg-surface-card" style={{ borderRadius: 16, width: '100%', maxWidth: 540, padding: 24, boxShadow: '0 8px 32px rgba(0,0,0,0.2)', fontFamily: 'var(--font-system)', maxHeight: '90vh', display: 'flex', flexDirection: 'column' }} >
{t('reservations.airtrail.title')}
{loading && (
{t('common.loading')}
)} {!loading && flights.length === 0 && !error && (
{t('reservations.airtrail.empty')}
)} {!loading && during.length > 0 && ( <>
{t('reservations.airtrail.duringTrip')}
{during.map(renderFlight)} )} {!loading && others.length > 0 && ( <>
0 ? 14 : 2}px 0 8px` }}> {t('reservations.airtrail.otherFlights')}
{others.map(renderFlight)} )} {error && (
{error}
)}
, document.body, ) }