import React, { useState, useCallback, useEffect, useRef } from 'react' import { Plus, Search, X, Navigation, RotateCcw, ExternalLink, ChevronDown, ChevronRight, ChevronUp, Clock, MapPin, CalendarDays, FileText, Check, Pencil, Trash2, } from 'lucide-react' import { calculateRoute, generateGoogleMapsUrl, optimizeRoute } from '../Map/RouteCalculator' import PackingListPanel from '../Packing/PackingListPanel' import FileManager from '../Files/FileManager' import { ReservationModal } from './ReservationModal' import { PlaceDetailPanel } from './PlaceDetailPanel' import WeatherWidget from '../Weather/WeatherWidget' import { useTripStore } from '../../store/tripStore' import { useToast } from '../shared/Toast' const SEGMENTS = [ { id: 'plan', label: 'Plan' }, { id: 'orte', label: 'Orte' }, { id: 'reservierungen', label: 'Buchungen' }, { id: 'packliste', label: 'Packliste' }, { id: 'dokumente', label: 'Dokumente' }, ] const TRANSPORT_MODES = [ { value: 'driving', label: 'Auto', icon: '🚗' }, { value: 'walking', label: 'Fuß', icon: '🚶' }, { value: 'cycling', label: 'Rad', icon: '🚲' }, ] function formatShortDate(dateStr) { if (!dateStr) return '' return new Date(dateStr + 'T00:00:00').toLocaleDateString('de-DE', { day: 'numeric', month: 'short', }) } function formatDateTime(dt) { if (!dt) return '' try { return new Date(dt).toLocaleString('de-DE', { dateStyle: 'medium', timeStyle: 'short' }) } catch { return dt } } export default function PlannerSidebar({ trip, days, places, categories, tags, assignments, reservations, packingItems, selectedDayId, selectedPlaceId, onSelectDay, onPlaceClick, onPlaceEdit, onPlaceDelete, onAssignToDay, onRemoveAssignment, onReorder, onAddPlace, onEditTrip, onRouteCalculated, tripId, }) { const [activeSegment, setActiveSegment] = useState('plan') const [search, setSearch] = useState('') const [categoryFilter, setCategoryFilter] = useState('') const [transportMode, setTransportMode] = useState('driving') const [isCalculatingRoute, setIsCalculatingRoute] = useState(false) const [showReservationModal, setShowReservationModal] = useState(false) const [editingReservation, setEditingReservation] = useState(null) const [routeInfo, setRouteInfo] = useState(null) const [expandedDays, setExpandedDays] = useState(new Set()) // Day notes inline UI state: { [dayId]: { mode: 'add'|'edit', noteId?, text, time } } const [noteUi, setNoteUi] = useState({}) const noteInputRef = useRef(null) const tripStore = useTripStore() const toast = useToast() const dayNotes = tripStore.dayNotes || {} // Auto-expand selected day useEffect(() => { if (selectedDayId) { setExpandedDays(prev => new Set([...prev, selectedDayId])) } }, [selectedDayId]) const toggleDay = (dayId) => { setExpandedDays(prev => { const next = new Set(prev) if (next.has(dayId)) next.delete(dayId) else next.add(dayId) return next }) } const getDayAssignments = (dayId) => (assignments[String(dayId)] || []).slice().sort((a, b) => a.order_index - b.order_index) const selectedDayAssignments = selectedDayId ? getDayAssignments(selectedDayId) : [] const selectedDay = selectedDayId ? days.find(d => d.id === selectedDayId) : null const filteredPlaces = places.filter(p => { const matchSearch = !search || p.name.toLowerCase().includes(search.toLowerCase()) || (p.address || '').toLowerCase().includes(search.toLowerCase()) const matchCat = !categoryFilter || String(p.category_id) === String(categoryFilter) return matchSearch && matchCat }) const isAssignedToDay = (placeId) => selectedDayId && selectedDayAssignments.some(a => a.place?.id === placeId) const totalCost = days.reduce((sum, d) => { const da = assignments[String(d.id)] || [] return sum + da.reduce((s, a) => s + (parseFloat(a.place?.price) || 0), 0) }, 0) const currency = trip?.currency || 'EUR' const filteredReservations = selectedDayId ? reservations.filter(r => String(r.day_id) === String(selectedDayId) || !r.day_id) : reservations // Get representative location for a day (first place with coords) const getDayLocation = (dayId) => { const da = getDayAssignments(dayId) const p = da.find(a => a.place?.lat && a.place?.lng) return p ? { lat: p.place.lat, lng: p.place.lng } : null } // Route handlers const handleCalculateRoute = async () => { if (!selectedDayId) return const waypoints = selectedDayAssignments .map(a => a.place) .filter(p => p?.lat && p?.lng) .map(p => ({ lat: p.lat, lng: p.lng })) if (waypoints.length < 2) { toast.error('Mindestens 2 Orte mit Koordinaten benötigt') return } setIsCalculatingRoute(true) try { const result = await calculateRoute(waypoints, transportMode) setRouteInfo({ distance: result.distanceText, duration: result.durationText }) onRouteCalculated?.(result) toast.success('Route berechnet') } catch { toast.error('Route konnte nicht berechnet werden') } finally { setIsCalculatingRoute(false) } } const handleOptimizeRoute = async () => { if (!selectedDayId || selectedDayAssignments.length < 3) return const withCoords = selectedDayAssignments.map(a => a.place).filter(p => p?.lat && p?.lng) const optimized = optimizeRoute(withCoords) const reorderedIds = optimized .map(p => selectedDayAssignments.find(a => a.place?.id === p.id)?.id) .filter(Boolean) // Append assignments without coordinates at end for (const a of selectedDayAssignments) { if (!reorderedIds.includes(a.id)) reorderedIds.push(a.id) } await onReorder(selectedDayId, reorderedIds) toast.success('Route optimiert') } const handleOpenGoogleMaps = () => { const ps = selectedDayAssignments.map(a => a.place).filter(p => p?.lat && p?.lng) const url = generateGoogleMapsUrl(ps) if (url) window.open(url, '_blank') else toast.error('Keine Orte mit Koordinaten vorhanden') } const handleMoveUp = async (dayId, idx) => { const da = getDayAssignments(dayId) if (idx === 0) return const ids = da.map(a => a.id) ;[ids[idx - 1], ids[idx]] = [ids[idx], ids[idx - 1]] await onReorder(dayId, ids) } const handleMoveDown = async (dayId, idx) => { const da = getDayAssignments(dayId) if (idx === da.length - 1) return const ids = da.map(a => a.id) ;[ids[idx], ids[idx + 1]] = [ids[idx + 1], ids[idx]] await onReorder(dayId, ids) } // Merge place assignments + day notes into a single sorted list const getMergedDayItems = (dayId) => { const da = getDayAssignments(dayId) const dn = (dayNotes[String(dayId)] || []).slice().sort((a, b) => a.sort_order - b.sort_order) return [ ...da.map(a => ({ type: 'place', sortKey: a.order_index, data: a })), ...dn.map(n => ({ type: 'note', sortKey: n.sort_order, data: n })), ].sort((a, b) => a.sortKey - b.sortKey) } const openAddNote = (dayId) => { const merged = getMergedDayItems(dayId) const maxKey = merged.length > 0 ? Math.max(...merged.map(i => i.sortKey)) : -1 setNoteUi(prev => ({ ...prev, [dayId]: { mode: 'add', text: '', time: '', sortOrder: maxKey + 1 } })) setTimeout(() => noteInputRef.current?.focus(), 50) } const openEditNote = (dayId, note) => { setNoteUi(prev => ({ ...prev, [dayId]: { mode: 'edit', noteId: note.id, text: note.text, time: note.time || '' } })) setTimeout(() => noteInputRef.current?.focus(), 50) } const cancelNote = (dayId) => { setNoteUi(prev => { const n = { ...prev }; delete n[dayId]; return n }) } const saveNote = async (dayId) => { const ui = noteUi[dayId] if (!ui?.text?.trim()) return try { if (ui.mode === 'add') { await tripStore.addDayNote(tripId, dayId, { text: ui.text.trim(), time: ui.time || null, sort_order: ui.sortOrder }) } else { await tripStore.updateDayNote(tripId, dayId, ui.noteId, { text: ui.text.trim(), time: ui.time || null }) } cancelNote(dayId) } catch (err) { toast.error(err.message) } } const handleDeleteNote = async (dayId, noteId) => { try { await tripStore.deleteDayNote(tripId, dayId, noteId) } catch (err) { toast.error(err.message) } } const handleNoteMoveUp = async (dayId, noteId) => { const merged = getMergedDayItems(dayId) const idx = merged.findIndex(item => item.type === 'note' && item.data.id === noteId) if (idx <= 0) return const newSortOrder = idx >= 2 ? (merged[idx - 2].sortKey + merged[idx - 1].sortKey) / 2 : merged[idx - 1].sortKey - 1 try { await tripStore.updateDayNote(tripId, dayId, noteId, { sort_order: newSortOrder }) } catch (err) { toast.error(err.message) } } const handleNoteMoveDown = async (dayId, noteId) => { const merged = getMergedDayItems(dayId) const idx = merged.findIndex(item => item.type === 'note' && item.data.id === noteId) if (idx === -1 || idx >= merged.length - 1) return const newSortOrder = idx < merged.length - 2 ? (merged[idx + 1].sortKey + merged[idx + 2].sortKey) / 2 : merged[idx + 1].sortKey + 1 try { await tripStore.updateDayNote(tripId, dayId, noteId, { sort_order: newSortOrder }) } catch (err) { toast.error(err.message) } } const handleSaveReservation = async (data) => { try { if (editingReservation) { await tripStore.updateReservation(tripId, editingReservation.id, data) toast.success('Reservierung aktualisiert') } else { await tripStore.addReservation(tripId, { ...data, day_id: selectedDayId || null }) toast.success('Reservierung hinzugefügt') } setShowReservationModal(false) } catch (err) { toast.error(err.message) } } const handleDeleteReservation = async (id) => { if (!confirm('Reservierung löschen?')) return try { await tripStore.deleteReservation(tripId, id) toast.success('Reservierung gelöscht') } catch (err) { toast.error(err.message) } } // Inspector: show when a place is selected const selectedPlace = selectedPlaceId ? places.find(p => p.id === selectedPlaceId) : null return (
Noch keine Tage geplant
{day.title || `Tag ${index + 1}`}
{da.length > 0 && ( {da.length} {da.length === 1 ? 'Ort' : 'Orte'} )}Keine Einträge für diesen Tag
{place.name}
{(place.description || place.notes) && ({place.description || place.notes}
)}Keine Orte gefunden
{category.icon} {category.name}
} {place.address &&{place.address}
}Keine Reservierungen
{r.notes}
}