import { useState, useEffect, useRef, useMemo } from 'react' import Modal from '../shared/Modal' import CustomSelect from '../shared/CustomSelect' import { Plane, Hotel, Utensils, Train, Car, Ship, Ticket, FileText, Users, Paperclip, X, ExternalLink, Link2 } from 'lucide-react' import { useToast } from '../shared/Toast' import { useTranslation } from '../../i18n' import { CustomDatePicker } from '../shared/CustomDateTimePicker' import CustomTimePicker from '../shared/CustomTimePicker' import type { Day, Place, Reservation, TripFile, AssignmentsMap, Accommodation } from '../../types' const TYPE_OPTIONS = [ { value: 'flight', labelKey: 'reservations.type.flight', Icon: Plane }, { value: 'hotel', labelKey: 'reservations.type.hotel', Icon: Hotel }, { value: 'restaurant', labelKey: 'reservations.type.restaurant', Icon: Utensils }, { value: 'train', labelKey: 'reservations.type.train', Icon: Train }, { value: 'car', labelKey: 'reservations.type.car', Icon: Car }, { value: 'cruise', labelKey: 'reservations.type.cruise', Icon: Ship }, { value: 'event', labelKey: 'reservations.type.event', Icon: Ticket }, { value: 'tour', labelKey: 'reservations.type.tour', Icon: Users }, { value: 'other', labelKey: 'reservations.type.other', Icon: FileText }, ] function buildAssignmentOptions(days, assignments, t, locale) { const options = [] for (const day of (days || [])) { const da = (assignments?.[String(day.id)] || []).slice().sort((a, b) => a.order_index - b.order_index) if (da.length === 0) continue const dayLabel = day.title || t('dayplan.dayN', { n: day.day_number }) const dateStr = day.date ? ` · ${formatDate(day.date, locale)}` : '' const groupLabel = `${dayLabel}${dateStr}` // Group header (non-selectable) options.push({ value: `_header_${day.id}`, label: groupLabel, disabled: true, isHeader: true }) for (let i = 0; i < da.length; i++) { const place = da[i].place if (!place) continue const timeStr = place.place_time ? ` · ${place.place_time}${place.end_time ? ' – ' + place.end_time : ''}` : '' options.push({ value: da[i].id, label: ` ${i + 1}. ${place.name}${timeStr}`, searchLabel: place.name, groupLabel, dayDate: day.date || null, }) } } return options } interface ReservationModalProps { isOpen: boolean onClose: () => void onSave: (data: Record) => Promise | void reservation: Reservation | null days: Day[] places: Place[] assignments: AssignmentsMap selectedDayId: number | null files?: TripFile[] onFileUpload: (fd: FormData) => Promise onFileDelete: (fileId: number) => Promise accommodations?: Accommodation[] } export function ReservationModal({ isOpen, onClose, onSave, reservation, days, places, assignments, selectedDayId, files = [], onFileUpload, onFileDelete, accommodations = [] }: ReservationModalProps) { const toast = useToast() const { t, locale } = useTranslation() const fileInputRef = useRef(null) const [form, setForm] = useState({ title: '', type: 'other', status: 'pending', reservation_time: '', reservation_end_time: '', location: '', confirmation_number: '', notes: '', assignment_id: '', accommodation_id: '', meta_airline: '', meta_flight_number: '', meta_departure_airport: '', meta_arrival_airport: '', meta_train_number: '', meta_platform: '', meta_seat: '', meta_check_in_time: '', meta_check_out_time: '', hotel_place_id: '', hotel_start_day: '', hotel_end_day: '', }) const [isSaving, setIsSaving] = useState(false) const [uploadingFile, setUploadingFile] = useState(false) const [pendingFiles, setPendingFiles] = useState([]) const assignmentOptions = useMemo( () => buildAssignmentOptions(days, assignments, t, locale), [days, assignments, t, locale] ) useEffect(() => { if (reservation) { const meta = typeof reservation.metadata === 'string' ? JSON.parse(reservation.metadata || '{}') : (reservation.metadata || {}) setForm({ title: reservation.title || '', type: reservation.type || 'other', status: reservation.status || 'pending', reservation_time: reservation.reservation_time ? reservation.reservation_time.slice(0, 16) : '', reservation_end_time: reservation.reservation_end_time || '', location: reservation.location || '', confirmation_number: reservation.confirmation_number || '', notes: reservation.notes || '', assignment_id: reservation.assignment_id || '', accommodation_id: reservation.accommodation_id || '', meta_airline: meta.airline || '', meta_flight_number: meta.flight_number || '', meta_departure_airport: meta.departure_airport || '', meta_arrival_airport: meta.arrival_airport || '', meta_train_number: meta.train_number || '', meta_platform: meta.platform || '', meta_seat: meta.seat || '', meta_check_in_time: meta.check_in_time || '', meta_check_out_time: meta.check_out_time || '', hotel_place_id: (() => { const acc = accommodations.find(a => a.id == reservation.accommodation_id); return acc?.place_id || '' })(), hotel_start_day: (() => { const acc = accommodations.find(a => a.id == reservation.accommodation_id); return acc?.start_day_id || '' })(), hotel_end_day: (() => { const acc = accommodations.find(a => a.id == reservation.accommodation_id); return acc?.end_day_id || '' })(), }) } else { setForm({ title: '', type: 'other', status: 'pending', reservation_time: '', reservation_end_time: '', location: '', confirmation_number: '', notes: '', assignment_id: '', accommodation_id: '', meta_airline: '', meta_flight_number: '', meta_departure_airport: '', meta_arrival_airport: '', meta_train_number: '', meta_platform: '', meta_seat: '', meta_check_in_time: '', meta_check_out_time: '', }) setPendingFiles([]) } }, [reservation, isOpen, selectedDayId]) const set = (field, value) => setForm(prev => ({ ...prev, [field]: value })) const handleSubmit = async (e) => { e.preventDefault() if (!form.title.trim()) return setIsSaving(true) try { const metadata: Record = {} if (form.type === 'flight') { if (form.meta_airline) metadata.airline = form.meta_airline if (form.meta_flight_number) metadata.flight_number = form.meta_flight_number if (form.meta_departure_airport) metadata.departure_airport = form.meta_departure_airport if (form.meta_arrival_airport) metadata.arrival_airport = form.meta_arrival_airport } else if (form.type === 'hotel') { if (form.meta_check_in_time) metadata.check_in_time = form.meta_check_in_time if (form.meta_check_out_time) metadata.check_out_time = form.meta_check_out_time } else if (form.type === 'train') { if (form.meta_train_number) metadata.train_number = form.meta_train_number if (form.meta_platform) metadata.platform = form.meta_platform if (form.meta_seat) metadata.seat = form.meta_seat } const saveData: Record = { title: form.title, type: form.type, status: form.status, reservation_time: form.reservation_time, reservation_end_time: form.reservation_end_time, location: form.location, confirmation_number: form.confirmation_number, notes: form.notes, assignment_id: form.assignment_id || null, accommodation_id: form.type === 'hotel' ? (form.accommodation_id || null) : null, metadata: Object.keys(metadata).length > 0 ? metadata : null, } // If hotel with place + days, pass hotel data for auto-creation or update if (form.type === 'hotel' && form.hotel_place_id && form.hotel_start_day && form.hotel_end_day) { saveData.create_accommodation = { place_id: form.hotel_place_id, start_day_id: form.hotel_start_day, end_day_id: form.hotel_end_day, check_in: form.meta_check_in_time || null, check_out: form.meta_check_out_time || null, confirmation: form.confirmation_number || null, } } const saved = await onSave(saveData) if (!reservation?.id && saved?.id && pendingFiles.length > 0) { for (const file of pendingFiles) { const fd = new FormData() fd.append('file', file) fd.append('reservation_id', saved.id) fd.append('description', form.title) await onFileUpload(fd) } } } finally { setIsSaving(false) } } const handleFileChange = async (e) => { const file = (e.target as HTMLInputElement).files?.[0] if (!file) return if (reservation?.id) { setUploadingFile(true) try { const fd = new FormData() fd.append('file', file) fd.append('reservation_id', reservation.id) fd.append('description', reservation.title) await onFileUpload(fd) toast.success(t('reservations.toast.fileUploaded')) } catch { toast.error(t('reservations.toast.uploadError')) } finally { setUploadingFile(false) e.target.value = '' } } else { setPendingFiles(prev => [...prev, file]) e.target.value = '' } } const attachedFiles = reservation?.id ? files.filter(f => f.reservation_id === reservation.id) : [] const inputStyle = { width: '100%', border: '1px solid var(--border-primary)', borderRadius: 10, padding: '8px 12px', fontSize: 13, fontFamily: 'inherit', outline: 'none', boxSizing: 'border-box', color: 'var(--text-primary)', background: 'var(--bg-input)', } const labelStyle = { display: 'block', fontSize: 11, fontWeight: 600, color: 'var(--text-faint)', marginBottom: 5, textTransform: 'uppercase', letterSpacing: '0.03em' } return (
{/* Type selector */}
{TYPE_OPTIONS.map(({ value, labelKey, Icon }) => ( ))}
{/* Title */}
set('title', e.target.value)} required placeholder={t('reservations.titlePlaceholder')} style={inputStyle} />
{/* Assignment Picker + Date (hidden for hotels) */} {form.type !== 'hotel' && (
{assignmentOptions.length > 0 && (
{ set('assignment_id', value) const opt = assignmentOptions.find(o => o.value === value) if (opt?.dayDate) { setForm(prev => { if (prev.reservation_time) return prev return { ...prev, reservation_time: opt.dayDate } }) } }} placeholder={t('reservations.pickAssignment')} options={[ { value: '', label: t('reservations.noAssignment') }, ...assignmentOptions, ]} searchable size="sm" />
)}
{ const [d] = (form.reservation_time || '').split('T'); return d || '' })()} onChange={d => { const [, t] = (form.reservation_time || '').split('T') set('reservation_time', d ? (t ? `${d}T${t}` : d) : '') }} />
)} {/* Start Time + End Time + Status */}
{form.type !== 'hotel' && ( <>
{ const [, t] = (form.reservation_time || '').split('T'); return t || '' })()} onChange={t => { const [d] = (form.reservation_time || '').split('T') const date = d || new Date().toISOString().split('T')[0] set('reservation_time', t ? `${date}T${t}` : date) }} />
set('reservation_end_time', v)} />
)}
set('status', value)} options={[ { value: 'pending', label: t('reservations.pending') }, { value: 'confirmed', label: t('reservations.confirmed') }, ]} size="sm" />
{/* Location + Booking Code */}
set('location', e.target.value)} placeholder={t('reservations.locationPlaceholder')} style={inputStyle} />
set('confirmation_number', e.target.value)} placeholder={t('reservations.confirmationPlaceholder')} style={inputStyle} />
{/* Type-specific fields */} {form.type === 'flight' && (
set('meta_airline', e.target.value)} placeholder="Lufthansa" style={inputStyle} />
set('meta_flight_number', e.target.value)} placeholder="LH 123" style={inputStyle} />
set('meta_departure_airport', e.target.value)} placeholder="FRA" style={inputStyle} />
set('meta_arrival_airport', e.target.value)} placeholder="NRT" style={inputStyle} />
)} {form.type === 'hotel' && ( <> {/* Hotel place + day range */}
{ set('hotel_place_id', value) const p = places.find(pl => pl.id === value) if (p) { if (!form.title) set('title', p.name) if (!form.location && p.address) set('location', p.address) } }} placeholder={t('reservations.meta.pickHotel')} options={[ { value: '', label: '—' }, ...places.map(p => ({ value: p.id, label: p.name })), ]} searchable size="sm" />
set('hotel_start_day', value)} placeholder={t('reservations.meta.selectDay')} options={days.map(d => ({ value: d.id, label: d.title || `${t('dayplan.dayN', { n: d.day_number })}${d.date ? ` · ${formatDate(d.date, locale)}` : ''}` }))} size="sm" />
set('hotel_end_day', value)} placeholder={t('reservations.meta.selectDay')} options={days.map(d => ({ value: d.id, label: d.title || `${t('dayplan.dayN', { n: d.day_number })}${d.date ? ` · ${formatDate(d.date, locale)}` : ''}` }))} size="sm" />
{/* Check-in/out times */}
set('meta_check_in_time', v)} />
set('meta_check_out_time', v)} />
)} {form.type === 'train' && (
set('meta_train_number', e.target.value)} placeholder="ICE 123" style={inputStyle} />
set('meta_platform', e.target.value)} placeholder="12" style={inputStyle} />
set('meta_seat', e.target.value)} placeholder="42A" style={inputStyle} />
)} {/* Notes */}