import { useCallback, useEffect, useState } from 'react'; import { ActivityIndicator, ScrollView, StyleSheet, Text, TouchableOpacity, View, } from 'react-native'; import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; import type { RouteProp } from '@react-navigation/native'; import { loadEvents, loadOriginStation, loadNotificationSettings } from '../store/eventStore'; import { api } from '../services/api'; import type { Journey, BikeRoute, Station, Event as CalendarEvent, WalkRoute } from '@timetoleave/core'; import { useDestinationStation } from '../hooks/useDestinationStation'; import { useDepartureTime } from '../hooks/useDepartureTime'; import { useGeocode } from '../hooks/useGeocode'; import { useOriginStationWalk } from '../hooks/useOriginStationWalk'; import { useWalkRoute } from '../hooks/useWalkRoute'; import { useWienerLinien } from '../hooks/useWienerLinien'; import { useColors } from '../hooks/useColors'; import { EventHeader } from '../components/EventHeader'; import { JourneyList } from '../components/JourneyList'; import { BikeSection } from '../components/BikeSection'; import { NearbyStops } from '../components/NearbyStops'; import type { RootStack } from '../types/navigation'; type ScreenProps = { navigation: NativeStackNavigationProp; route: RouteProp; }; type TransportMode = 'train' | 'bike'; /** * Compute the target arrival time at the destination station. * Subtracts both the arrival buffer (time before event) and the walking * duration from station to event location. */ function stationArrivalTarget( eventTime: Date, arrivalBufferMinutes: number, walkDurationSeconds: number, ) { return new Date(eventTime.getTime() - arrivalBufferMinutes * 60_000 - walkDurationSeconds * 1000); } export function EventDetailScreen({ navigation, route }: ScreenProps) { const { eventId } = route.params; const colors = useColors(); const [event, setEvent] = useState(null); const [journeys, setJourneys] = useState([]); const [bikeRoute, setBikeRoute] = useState(null); const [walkRoute, setWalkRoute] = useState(null); const [origin, setOrigin] = useState(null); const [loading, setLoading] = useState(true); const [loadingBike, setLoadingBike] = useState(false); const [loadingWalk, setLoadingWalk] = useState(false); const [activeMode, setActiveMode] = useState('train'); const [error, setError] = useState(null); const [arrivalBufferMinutes, setArrivalBufferMinutes] = useState(5); const [showBikeOption, setShowBikeOption] = useState(true); const [showWalkingOption, setShowWalkingOption] = useState(true); const destStation = useDestinationStation(event?.destination); const destCoords = useGeocode(event?.destination); const walkHook = useWalkRoute( destStation.station?.lat, destStation.station?.lng, destCoords.coords?.lat, destCoords.coords?.lng, ); const originWalk = useOriginStationWalk(origin); const wienerLinien = useWienerLinien(destCoords.coords?.lat, destCoords.coords?.lng); const fetchData = useCallback(async () => { setLoading(true); setError(null); try { const [events, originStation, settings] = await Promise.all([ loadEvents(), loadOriginStation(), loadNotificationSettings(), ]); setOrigin(originStation); setArrivalBufferMinutes(settings.arrivalBufferMinutes); setShowBikeOption(settings.showBikeOption); setShowWalkingOption(settings.showWalkingOption); const found = events.find((e) => e.id === eventId); if (!found) { setError('Termin nicht gefunden'); return; } setEvent(found); if (originStation) { const destExtId = destStation.station?.extId; if (destExtId) { const finalWalkLookupPending = settings.showWalkingOption && destCoords.coords !== null && destStation.station?.lat != null && destStation.station?.lng != null && !walkHook.walkRoute && !walkHook.error; if (finalWalkLookupPending) { setJourneys([]); return; } const walkDurationSeconds = settings.showWalkingOption ? (walkHook.walkRoute?.duration ?? 0) : 0; const target = stationArrivalTarget(found.eventTime, settings.arrivalBufferMinutes, walkDurationSeconds); const results = await api.searchJourneys(originStation.extId, destExtId, target, { arriveBy: true }); setJourneys(results); } else if (destStation.error) { setError(`Ziel-Station nicht auflösbar: ${destStation.error}`); } try { setLoadingBike(true); if (destCoords.coords && originStation.lat && originStation.lng) { const bike = await api.getBikeRoute( originStation.lat, originStation.lng, destCoords.coords.lat, destCoords.coords.lng, ); setBikeRoute(bike); } } catch { setBikeRoute(null); } finally { setLoadingBike(false); } } } catch (err) { setError(err instanceof Error ? err.message : 'Fehler beim Laden'); } finally { setLoading(false); } }, [eventId, destStation.station, destStation.error, destCoords.coords, walkHook.walkRoute, walkHook.error]); useEffect(() => { fetchData(); }, [fetchData]); useEffect(() => { setWalkRoute(walkHook.walkRoute); setLoadingWalk(walkHook.loading); }, [walkHook.walkRoute, walkHook.loading]); const handleRefresh = () => { setBikeRoute(null); setWalkRoute(null); setJourneys([]); fetchData(); }; const departureInfo = useDepartureTime( event?.eventTime ?? new Date(), journeys.length > 0 ? journeys : null, bikeRoute?.duration ?? null, activeMode === 'train' && journeys.length > 0 ? 'train' : activeMode === 'bike' && bikeRoute ? 'bike' : null, arrivalBufferMinutes, showWalkingOption ? (walkRoute?.duration ?? 0) : 0, originWalk.walkRoute?.duration ?? 0, ); if (loading) { return ( {destStation.loading && !event ? 'Ziel-Station wird aufgelöst…' : 'Termine werden geladen…'} ); } const bikeDisabled = !showBikeOption; const effectiveMode: TransportMode = bikeDisabled && activeMode === 'bike' ? 'train' : activeMode; return ( {event && ( )} {error && ( ⚠ {error} )} {!origin && !error && ( Keine Ursprungstation festgelegt.{' '} navigation.navigate('Settings')}> Einstellungen öffnen )} {origin && ( setActiveMode('train')} > 🚆 Zug {effectiveMode === 'train' && ( Aktiv )} {showWalkingOption ? 'Bahn + finaler Fußweg' : 'Nur Bahn'} !bikeDisabled && setActiveMode('bike')} disabled={bikeDisabled} > 🚲 Rad {effectiveMode === 'bike' && ( Aktiv )} {bikeDisabled ? 'In Einstellungen deaktiviert' : loadingBike ? 'Route wird berechnet...' : 'Direktweg'} )} {event && effectiveMode === 'train' && ( )} {effectiveMode === 'bike' && ( )} 🔄 Neu laden ); } const styles = StyleSheet.create({ container: { flex: 1 }, center: { flex: 1, justifyContent: 'center', alignItems: 'center' }, loadingText: { marginTop: 12, fontSize: 15 }, errorBanner: { padding: 12, marginBottom: 12 }, warningBanner: { padding: 12, marginBottom: 12 }, bannerText: { color: '#fff', fontSize: 14 }, bannerLink: { color: '#fff', fontWeight: '700', textDecorationLine: 'underline' }, modeSelector: { flexDirection: 'row', padding: 12, gap: 12, marginBottom: 12, borderWidth: 1, borderRadius: 12, }, modeButton: { flex: 1, padding: 12, borderRadius: 10, borderWidth: 1, borderColor: 'transparent' }, modeHeader: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }, modeLabel: { fontSize: 15, fontWeight: '600' }, modeMeta: { fontSize: 11, marginTop: 4 }, activeBadge: { paddingHorizontal: 8, paddingVertical: 2, borderRadius: 10 }, activeBadgeText: { color: '#fff', fontSize: 10, fontWeight: '700' }, refreshBtn: { alignSelf: 'center', marginTop: 20, paddingVertical: 12, paddingHorizontal: 24, borderRadius: 12 }, refreshBtnText: { fontSize: 15, fontWeight: '600' }, });