import { useState, useEffect, useRef, useMemo } from 'react' import Modal from '../shared/Modal' import CustomSelect from '../shared/CustomSelect' import { mapsApi } from '../../api/client' import { useAuthStore } from '../../store/authStore' import { useCanDo } from '../../store/permissionsStore' import { useTripStore } from '../../store/tripStore' import { useToast } from '../shared/Toast' import { Search, Paperclip, X, AlertTriangle } from 'lucide-react' import { useTranslation } from '../../i18n' import CustomTimePicker from '../shared/CustomTimePicker' import type { Place, Category, Assignment } from '../../types' interface PlaceFormData { name: string description: string address: string lat: string lng: string category_id: string place_time: string end_time: string notes: string transport_mode: string website: string } const DEFAULT_FORM: PlaceFormData = { name: '', description: '', address: '', lat: '', lng: '', category_id: '', place_time: '', end_time: '', notes: '', transport_mode: 'walking', website: '', } interface PlaceFormModalProps { isOpen: boolean onClose: () => void onSave: (data: PlaceFormData, files?: File[]) => Promise | void place: Place | null prefillCoords?: { lat: number; lng: number; name?: string; address?: string } | null tripId: number categories: Category[] onCategoryCreated: (category: Category) => void assignmentId: number | null dayAssignments?: Assignment[] } export default function PlaceFormModal({ isOpen, onClose, onSave, place, prefillCoords, tripId, categories, onCategoryCreated, assignmentId, dayAssignments = [], }: PlaceFormModalProps) { const [form, setForm] = useState(DEFAULT_FORM) const [mapsSearch, setMapsSearch] = useState('') const [mapsResults, setMapsResults] = useState([]) const [isSearchingMaps, setIsSearchingMaps] = useState(false) const [newCategoryName, setNewCategoryName] = useState('') const [showNewCategory, setShowNewCategory] = useState(false) const [isSaving, setIsSaving] = useState(false) const [pendingFiles, setPendingFiles] = useState([]) const fileRef = useRef(null) const toast = useToast() const { t, language } = useTranslation() const { hasMapsKey } = useAuthStore() const can = useCanDo() const tripObj = useTripStore((s) => s.trip) const canUploadFiles = can('file_upload', tripObj) useEffect(() => { if (place) { setForm({ name: place.name || '', description: place.description || '', address: place.address || '', lat: place.lat || '', lng: place.lng || '', category_id: place.category_id || '', place_time: place.place_time || '', end_time: place.end_time || '', notes: place.notes || '', transport_mode: place.transport_mode || 'walking', website: place.website || '', }) } else if (prefillCoords) { setForm({ ...DEFAULT_FORM, lat: String(prefillCoords.lat), lng: String(prefillCoords.lng), name: prefillCoords.name || '', address: prefillCoords.address || '', }) } else { setForm(DEFAULT_FORM) } setPendingFiles([]) }, [place, prefillCoords, isOpen]) const handleChange = (field, value) => { setForm(prev => ({ ...prev, [field]: value })) } const handleMapsSearch = async () => { if (!mapsSearch.trim()) return setIsSearchingMaps(true) try { // Detect Google Maps URLs and resolve them directly const trimmed = mapsSearch.trim() if (trimmed.match(/^https?:\/\/(www\.)?(google\.[a-z.]+\/maps|maps\.google\.[a-z.]+|maps\.app\.goo\.gl|goo\.gl)/i)) { const resolved = await mapsApi.resolveUrl(trimmed) if (resolved.lat && resolved.lng) { setForm(prev => ({ ...prev, name: resolved.name || prev.name, address: resolved.address || prev.address, lat: String(resolved.lat), lng: String(resolved.lng), })) setMapsResults([]) setMapsSearch('') toast.success(t('places.urlResolved')) return } } const result = await mapsApi.search(mapsSearch, language) setMapsResults(result.places || []) } catch (err: unknown) { toast.error(t('places.mapsSearchError')) } finally { setIsSearchingMaps(false) } } const handleSelectMapsResult = (result) => { setForm(prev => ({ ...prev, name: result.name || prev.name, address: result.address || prev.address, lat: result.lat || prev.lat, lng: result.lng || prev.lng, google_place_id: result.google_place_id || prev.google_place_id, osm_id: result.osm_id || prev.osm_id, website: result.website || prev.website, phone: result.phone || prev.phone, })) setMapsResults([]) setMapsSearch('') } const handleCreateCategory = async () => { if (!newCategoryName.trim()) return try { const cat = await onCategoryCreated?.({ name: newCategoryName, color: '#6366f1', icon: 'MapPin' }) if (cat) setForm(prev => ({ ...prev, category_id: cat.id })) setNewCategoryName('') setShowNewCategory(false) } catch (err: unknown) { toast.error(t('places.categoryCreateError')) } } const handleFileAdd = (e) => { const files = Array.from((e.target as HTMLInputElement).files || []) setPendingFiles(prev => [...prev, ...files]) e.target.value = '' } const handleRemoveFile = (idx) => { setPendingFiles(prev => prev.filter((_, i) => i !== idx)) } // Paste support for files/images const handlePaste = (e) => { if (!canUploadFiles) return const items = e.clipboardData?.items if (!items) return for (const item of Array.from(items)) { if (item.type.startsWith('image/') || item.type === 'application/pdf') { e.preventDefault() const file = item.getAsFile() if (file) setPendingFiles(prev => [...prev, file]) return } } } const hasTimeError = place && form.place_time && form.end_time && form.place_time.length >= 5 && form.end_time.length >= 5 && form.end_time <= form.place_time const handleSubmit = async (e) => { e.preventDefault() if (!form.name.trim()) { toast.error(t('places.nameRequired')) return } setIsSaving(true) try { await onSave({ ...form, lat: form.lat ? parseFloat(form.lat) : null, lng: form.lng ? parseFloat(form.lng) : null, category_id: form.category_id || null, _pendingFiles: pendingFiles.length > 0 ? pendingFiles : undefined, }) onClose() } catch (err: unknown) { toast.error(err instanceof Error ? err.message : t('places.saveError')) } finally { setIsSaving(false) } } return (
{/* Place Search */}
{!hasMapsKey && (

{t('places.osmActive')}

)}
setMapsSearch(e.target.value)} onKeyDown={e => e.key === 'Enter' && (e.preventDefault(), handleMapsSearch())} placeholder={t('places.mapsSearchPlaceholder')} autoFocus className="flex-1 border border-slate-200 rounded-lg px-3 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-slate-400 bg-white" />
{mapsResults.length > 0 && (
{mapsResults.map((result, idx) => ( ))}
)}
{/* Name */}
handleChange('name', e.target.value)} required placeholder={t('places.formNamePlaceholder')} className="form-input" />
{/* Description */}