import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react' import ReactDOM from 'react-dom' import { useParams, useNavigate } from 'react-router-dom' import { useTripStore } from '../store/tripStore' import { useSettingsStore } from '../store/settingsStore' import { MapView } from '../components/Map/MapView' import DayPlanSidebar from '../components/Planner/DayPlanSidebar' import PlacesSidebar from '../components/Planner/PlacesSidebar' import PlaceInspector from '../components/Planner/PlaceInspector' import PlaceFormModal from '../components/Planner/PlaceFormModal' import TripFormModal from '../components/Trips/TripFormModal' import TripMembersModal from '../components/Trips/TripMembersModal' import { ReservationModal } from '../components/Planner/ReservationModal' import ReservationsPanel from '../components/Planner/ReservationsPanel' import PackingListPanel from '../components/Packing/PackingListPanel' import FileManager from '../components/Files/FileManager' import BudgetPanel from '../components/Budget/BudgetPanel' import Navbar from '../components/Layout/Navbar' import { useToast } from '../components/shared/Toast' import { Map, X, PanelLeftClose, PanelLeftOpen, PanelRightClose, PanelRightOpen } from 'lucide-react' import { useTranslation } from '../i18n' import { joinTrip, leaveTrip, addListener, removeListener } from '../api/websocket' import { addonsApi } from '../api/client' const MIN_SIDEBAR = 200 const MAX_SIDEBAR = 520 export default function TripPlannerPage() { const { id: tripId } = useParams() const navigate = useNavigate() const toast = useToast() const { t } = useTranslation() const { settings } = useSettingsStore() const tripStore = useTripStore() const { trip, days, places, assignments, packingItems, categories, reservations, budgetItems, files, selectedDayId, isLoading } = tripStore const [enabledAddons, setEnabledAddons] = useState({ packing: true, budget: true, documents: true }) useEffect(() => { addonsApi.enabled().then(data => { const map = {} data.addons.forEach(a => { map[a.id] = true }) setEnabledAddons({ packing: !!map.packing, budget: !!map.budget, documents: !!map.documents }) }).catch(() => {}) }, []) const TRIP_TABS = [ { id: 'plan', label: t('trip.tabs.plan') }, { id: 'buchungen', label: t('trip.tabs.reservations'), shortLabel: t('trip.tabs.reservationsShort') }, ...(enabledAddons.packing ? [{ id: 'packliste', label: t('trip.tabs.packing'), shortLabel: t('trip.tabs.packingShort') }] : []), ...(enabledAddons.budget ? [{ id: 'finanzplan', label: t('trip.tabs.budget') }] : []), ...(enabledAddons.documents ? [{ id: 'dateien', label: t('trip.tabs.files') }] : []), ] const [activeTab, setActiveTab] = useState('plan') const handleTabChange = (tabId) => { setActiveTab(tabId) if (tabId === 'finanzplan') tripStore.loadBudgetItems?.(tripId) if (tabId === 'dateien' && (!files || files.length === 0)) tripStore.loadFiles?.(tripId) } const [leftWidth, setLeftWidth] = useState(() => parseInt(localStorage.getItem('sidebarLeftWidth')) || 340) const [rightWidth, setRightWidth] = useState(() => parseInt(localStorage.getItem('sidebarRightWidth')) || 300) const [leftCollapsed, setLeftCollapsed] = useState(false) const [rightCollapsed, setRightCollapsed] = useState(false) const isResizingLeft = useRef(false) const isResizingRight = useRef(false) const [selectedPlaceId, setSelectedPlaceId] = useState(null) const [showPlaceForm, setShowPlaceForm] = useState(false) const [editingPlace, setEditingPlace] = useState(null) const [showTripForm, setShowTripForm] = useState(false) const [showMembersModal, setShowMembersModal] = useState(false) const [showReservationModal, setShowReservationModal] = useState(false) const [editingReservation, setEditingReservation] = useState(null) const [route, setRoute] = useState(null) const [routeInfo, setRouteInfo] = useState(null) const [fitKey, setFitKey] = useState(0) const [mobileSidebarOpen, setMobileSidebarOpen] = useState(null) // 'left' | 'right' | null // Load trip + files (needed for place inspector file section) useEffect(() => { if (tripId) { tripStore.loadTrip(tripId).catch(() => { toast.error(t('trip.toast.loadError')); navigate('/dashboard') }) tripStore.loadFiles(tripId) } }, [tripId]) useEffect(() => { if (tripId) tripStore.loadReservations(tripId) }, [tripId]) // WebSocket: join trip and listen for remote events useEffect(() => { if (!tripId) return const handler = useTripStore.getState().handleRemoteEvent joinTrip(tripId) addListener(handler) return () => { leaveTrip(tripId) removeListener(handler) } }, [tripId]) useEffect(() => { const onMove = (e) => { if (isResizingLeft.current) { const w = Math.max(MIN_SIDEBAR, Math.min(MAX_SIDEBAR, e.clientX - 10)) setLeftWidth(w) localStorage.setItem('sidebarLeftWidth', w) } if (isResizingRight.current) { const w = Math.max(MIN_SIDEBAR, Math.min(MAX_SIDEBAR, window.innerWidth - e.clientX - 10)) setRightWidth(w) localStorage.setItem('sidebarRightWidth', w) } } const onUp = () => { isResizingLeft.current = false isResizingRight.current = false document.body.style.cursor = '' document.body.style.userSelect = '' } document.addEventListener('mousemove', onMove) document.addEventListener('mouseup', onUp) return () => { document.removeEventListener('mousemove', onMove) document.removeEventListener('mouseup', onUp) } }, []) const mapPlaces = useCallback(() => { return places.filter(p => p.lat && p.lng) }, [places]) const handleSelectDay = useCallback((dayId) => { tripStore.setSelectedDay(dayId) setRouteInfo(null) setFitKey(k => k + 1) setMobileSidebarOpen(null) // Auto-show Luftlinien for the selected day const da = (tripStore.assignments[String(dayId)] || []).slice().sort((a, b) => a.order_index - b.order_index) const waypoints = da.map(a => a.place).filter(p => p?.lat && p?.lng) if (waypoints.length >= 2) { setRoute(waypoints.map(p => [p.lat, p.lng])) } else { setRoute(null) } }, [tripStore]) const handlePlaceClick = useCallback((placeId) => { setSelectedPlaceId(placeId) if (placeId) { setLeftCollapsed(false); setRightCollapsed(false) } }, []) const handleMarkerClick = useCallback((placeId) => { const opening = placeId !== undefined setSelectedPlaceId(prev => prev === placeId ? null : placeId) if (opening) { setLeftCollapsed(false); setRightCollapsed(false) } }, []) const handleMapClick = useCallback(() => { setSelectedPlaceId(null) }, []) const handleSavePlace = useCallback(async (data) => { if (editingPlace) { await tripStore.updatePlace(tripId, editingPlace.id, data) toast.success(t('trip.toast.placeUpdated')) } else { await tripStore.addPlace(tripId, data) toast.success(t('trip.toast.placeAdded')) } }, [editingPlace, tripId, tripStore, toast]) const handleDeletePlace = useCallback(async (placeId) => { if (!confirm(t('trip.confirm.deletePlace'))) return try { await tripStore.deletePlace(tripId, placeId) if (selectedPlaceId === placeId) setSelectedPlaceId(null) toast.success(t('trip.toast.placeDeleted')) } catch (err) { toast.error(err.message) } }, [tripId, tripStore, toast, selectedPlaceId]) const handleAssignToDay = useCallback(async (placeId, dayId, position) => { const target = dayId || selectedDayId if (!target) { toast.error(t('trip.toast.selectDay')); return } try { await tripStore.assignPlaceToDay(tripId, target, placeId, position) toast.success(t('trip.toast.assignedToDay')) } catch (err) { toast.error(err.message) } }, [selectedDayId, tripId, tripStore, toast]) const handleRemoveAssignment = useCallback(async (dayId, assignmentId) => { try { await tripStore.removeAssignment(tripId, dayId, assignmentId) } catch (err) { toast.error(err.message) } }, [tripId, tripStore, toast]) const handleReorder = useCallback(async (dayId, orderedIds) => { try { await tripStore.reorderAssignments(tripId, dayId, orderedIds) } catch { toast.error(t('trip.toast.reorderError')) } }, [tripId, tripStore, toast]) const handleUpdateDayTitle = useCallback(async (dayId, title) => { try { await tripStore.updateDayTitle(tripId, dayId, title) } catch (err) { toast.error(err.message) } }, [tripId, tripStore, toast]) const handleSaveReservation = async (data) => { try { if (editingReservation) { const r = await tripStore.updateReservation(tripId, editingReservation.id, data) toast.success(t('trip.toast.reservationUpdated')) setShowReservationModal(false) return r } else { const r = await tripStore.addReservation(tripId, { ...data, day_id: selectedDayId || null }) toast.success(t('trip.toast.reservationAdded')) setShowReservationModal(false) return r } } catch (err) { toast.error(err.message) } } const handleDeleteReservation = async (id) => { try { await tripStore.deleteReservation(tripId, id); toast.success(t('trip.toast.deleted')) } catch (err) { toast.error(err.message) } } const selectedPlace = selectedPlaceId ? places.find(p => p.id === selectedPlaceId) : null // Build placeId → order-number map from the selected day's assignments const dayOrderMap = useMemo(() => { if (!selectedDayId) return {} const da = assignments[String(selectedDayId)] || [] const sorted = [...da].sort((a, b) => a.order_index - b.order_index) const map = {} sorted.forEach((a, i) => { if (a.place?.id) map[a.place.id] = i + 1 }) return map }, [selectedDayId, assignments]) const mapTileUrl = settings.map_tile_url || 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png' const defaultCenter = [settings.default_lat || 48.8566, settings.default_lng || 2.3522] const defaultZoom = settings.default_zoom || 10 const fontStyle = { fontFamily: "-apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Segoe UI', system-ui, sans-serif" } if (isLoading) { return (