import React, { useState, useEffect, useRef, useCallback } from 'react' import { X, Clock, MapPin, ExternalLink, Phone, Euro, Edit2, Trash2, Plus, Minus, ChevronDown, ChevronUp, FileText, Upload, File, FileImage, Star, Navigation } from 'lucide-react' import PlaceAvatar from '../shared/PlaceAvatar' import { mapsApi } from '../../api/client' import { useSettingsStore } from '../../store/settingsStore' import { getCategoryIcon } from '../shared/categoryIcons' import { useTranslation } from '../../i18n' const detailsCache = new Map() function getSessionCache(key) { try { const raw = sessionStorage.getItem(key) return raw ? JSON.parse(raw) : undefined } catch { return undefined } } function setSessionCache(key, value) { try { sessionStorage.setItem(key, JSON.stringify(value)) } catch {} } function useGoogleDetails(googlePlaceId, language) { const [details, setDetails] = useState(null) const cacheKey = `gdetails_${googlePlaceId}_${language}` useEffect(() => { if (!googlePlaceId) { setDetails(null); return } // In-memory cache (fastest) if (detailsCache.has(cacheKey)) { setDetails(detailsCache.get(cacheKey)); return } // sessionStorage cache (survives reload) const cached = getSessionCache(cacheKey) if (cached) { detailsCache.set(cacheKey, cached); setDetails(cached); return } // Fetch from API mapsApi.details(googlePlaceId, language).then(data => { detailsCache.set(cacheKey, data.place) setSessionCache(cacheKey, data.place) setDetails(data.place) }).catch(() => {}) }, [googlePlaceId, language]) return details } function getWeekdayIndex(dateStr) { // weekdayDescriptions[0] = Monday … [6] = Sunday const d = dateStr ? new Date(dateStr + 'T12:00:00') : new Date() const jsDay = d.getDay() return jsDay === 0 ? 6 : jsDay - 1 } function convertHoursLine(line, timeFormat) { if (!line) return '' const hasAmPm = /\d{1,2}:\d{2}\s*(AM|PM)/i.test(line) if (timeFormat === '12h' && !hasAmPm) { // 24h → 12h: "10:00" → "10:00 AM", "21:00" → "9:00 PM", "Uhr" entfernen return line.replace(/\s*Uhr/g, '').replace(/(\d{1,2}):(\d{2})/g, (match, h, m) => { const hour = parseInt(h) if (isNaN(hour)) return match const period = hour >= 12 ? 'PM' : 'AM' const h12 = hour === 0 ? 12 : hour > 12 ? hour - 12 : hour return `${h12}:${m} ${period}` }) } if (timeFormat !== '12h' && hasAmPm) { // 12h → 24h: "10:00 AM" → "10:00", "9:00 PM" → "21:00" return line.replace(/(\d{1,2}):(\d{2})\s*(AM|PM)/gi, (_, h, m, p) => { let hour = parseInt(h) if (p.toUpperCase() === 'PM' && hour !== 12) hour += 12 if (p.toUpperCase() === 'AM' && hour === 12) hour = 0 return `${String(hour).padStart(2, '0')}:${m}` }) } return line } function formatTime(timeStr, locale, timeFormat) { if (!timeStr) return '' try { const [h, m] = timeStr.split(':').map(Number) if (timeFormat === '12h') { const period = h >= 12 ? 'PM' : 'AM' const h12 = h === 0 ? 12 : h > 12 ? h - 12 : h return `${h12}:${String(m).padStart(2, '0')} ${period}` } const str = `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}` return locale?.startsWith('de') ? `${str} Uhr` : str } catch { return timeStr } } function formatFileSize(bytes) { if (!bytes) return '' if (bytes < 1024) return `${bytes} B` if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB` return `${(bytes / 1024 / 1024).toFixed(1)} MB` } export default function PlaceInspector({ place, categories, days, selectedDayId, selectedAssignmentId, assignments, reservations = [], onClose, onEdit, onDelete, onAssignToDay, onRemoveAssignment, files, onFileUpload, }) { const { t, locale, language } = useTranslation() const timeFormat = useSettingsStore(s => s.settings.time_format) || '24h' const [hoursExpanded, setHoursExpanded] = useState(false) const [filesExpanded, setFilesExpanded] = useState(false) const [isUploading, setIsUploading] = useState(false) const fileInputRef = useRef(null) const googleDetails = useGoogleDetails(place?.google_place_id, language) if (!place) return null const category = categories?.find(c => c.id === place.category_id) const dayAssignments = selectedDayId ? (assignments[String(selectedDayId)] || []) : [] const assignmentInDay = selectedDayId ? dayAssignments.find(a => a.place?.id === place.id) : null const openingHours = googleDetails?.opening_hours || null const openNow = googleDetails?.open_now ?? null const selectedDay = days?.find(d => d.id === selectedDayId) const weekdayIndex = getWeekdayIndex(selectedDay?.date) const placeFiles = (files || []).filter(f => String(f.place_id) === String(place.id)) const handleFileUpload = useCallback(async (e) => { const selectedFiles = Array.from(e.target.files || []) if (!selectedFiles.length || !onFileUpload) return setIsUploading(true) try { for (const file of selectedFiles) { const fd = new FormData() fd.append('file', file) fd.append('place_id', place.id) await onFileUpload(fd) } setFilesExpanded(true) } catch (err) { console.error('Upload failed', err) } finally { setIsUploading(false) if (fileInputRef.current) fileInputRef.current.value = '' } }, [onFileUpload, place.id]) return (
{/* Header */}
{/* Avatar with open/closed ring + tag */}
{openNow !== null && ( {openNow ? t('inspector.opened') : t('inspector.closed')} )}
{place.name} {category && (() => { const CatIcon = getCategoryIcon(category.icon) return ( {category.name} ) })()}
{place.address && (
{place.address}
)} {place.place_time && (
{formatTime(place.place_time, locale, timeFormat)}
)} {place.lat && place.lng && (
{Number(place.lat).toFixed(6)}, {Number(place.lng).toFixed(6)}
)}
{/* Content — scrollable */}
{/* Info-Chips */}
{googleDetails?.rating && (() => { const shortReview = (googleDetails.reviews || []).find(r => r.text && r.text.length > 5) return ( } text={<> {googleDetails.rating.toFixed(1)} {googleDetails.rating_count ? ({googleDetails.rating_count.toLocaleString('de-DE')}) : ''} {shortReview && · „{shortReview.text}"} } color="var(--text-secondary)" bg="var(--bg-hover)" /> ) })()} {place.price > 0 && ( } text={`${place.price} ${place.currency || '€'}`} color="#059669" bg="#ecfdf5" /> )}
{/* Telefon */} {place.phone && (
{place.phone}
)} {/* Description */} {(place.description || place.notes) && (

{place.description || place.notes}

)} {/* Reservation for this specific assignment */} {(() => { const res = selectedAssignmentId ? reservations.find(r => r.assignment_id === selectedAssignmentId) : null if (!res) return null const confirmed = res.status === 'confirmed' const accentColor = confirmed ? '#16a34a' : '#d97706' return (
{/* Header bar */}
{confirmed ? t('reservations.confirmed') : t('reservations.pending')} {res.title}
{/* Details grid */} {(res.reservation_time || res.confirmation_number || res.location || res.notes) && (
{res.reservation_time && (
{t('reservations.date')}
{new Date(res.reservation_time).toLocaleDateString(locale, { weekday: 'short', day: 'numeric', month: 'short' })}
)} {res.reservation_time && (
{t('reservations.time')}
{new Date(res.reservation_time).toLocaleTimeString(locale, { hour: '2-digit', minute: '2-digit', hour12: timeFormat === '12h' })}
)} {res.confirmation_number && (
{t('reservations.confirmationCode')}
{res.confirmation_number}
)} {res.location && (
{t('reservations.locationAddress')}
{res.location}
)}
{res.notes && (
{res.notes}
)}
)}
) })()} {/* Opening hours */} {openingHours && openingHours.length > 0 && (
{hoursExpanded && (
{openingHours.map((line, i) => (
{convertHoursLine(line, timeFormat)}
))}
)}
)} {/* Files section */} {(placeFiles.length > 0 || onFileUpload) && (
{onFileUpload && ( )}
{filesExpanded && placeFiles.length > 0 && (
{placeFiles.map(f => (
{(f.mime_type || '').startsWith('image/') ? : } {f.original_name} {f.file_size && {formatFileSize(f.file_size)}}
))}
)}
)}
{/* Footer actions */}
{selectedDayId && ( assignmentInDay ? ( onRemoveAssignment(selectedDayId, assignmentInDay.id)} variant="ghost" icon={} label={<>{t('inspector.removeFromDay')}Remove} /> ) : ( onAssignToDay(place.id)} variant="primary" icon={} label={t('inspector.addToDay')} /> ) )} {googleDetails?.google_maps_url && ( window.open(googleDetails.google_maps_url, '_blank')} variant="ghost" icon={} label={{t('inspector.google')}} /> )} {place.website && ( window.open(place.website, '_blank')} variant="ghost" icon={} label={{t('inspector.website')}} /> )}
} label={{t('common.edit')}} /> } label={{t('common.delete')}} />
) } function Chip({ icon, text, color = 'var(--text-secondary)', bg = 'var(--bg-hover)' }) { return (
{icon} {text}
) } function Row({ icon, children }) { return (
{icon}
{children}
) } function ActionButton({ onClick, variant, icon, label }) { const base = { primary: { background: 'var(--accent)', color: 'var(--accent-text)', border: 'none', hoverBg: 'var(--text-secondary)' }, ghost: { background: 'var(--bg-hover)', color: 'var(--text-secondary)', border: 'none', hoverBg: 'var(--bg-tertiary)' }, danger: { background: 'rgba(239,68,68,0.08)', color: '#dc2626', border: 'none', hoverBg: 'rgba(239,68,68,0.16)' }, } const s = base[variant] || base.ghost return ( ) }