import { useState, useMemo } from 'react' import ReactDOM from 'react-dom' import { useTripStore } from '../../store/tripStore' import { useCanDo } from '../../store/permissionsStore' import { useSettingsStore } from '../../store/settingsStore' import { useToast } from '../shared/Toast' import { useTranslation } from '../../i18n' import { Plane, Hotel, Utensils, Train, Car, Ship, Ticket, FileText, MapPin, Calendar, Hash, CheckCircle2, Circle, Pencil, Trash2, Plus, ChevronDown, ChevronRight, Users, ExternalLink, BookMarked, Lightbulb, Link2, Clock, } from 'lucide-react' import type { Reservation, Day, TripFile, AssignmentsMap } from '../../types' interface AssignmentLookupEntry { dayNumber: number dayTitle: string | null dayDate: string placeName: string startTime: string | null endTime: string | null } const TYPE_OPTIONS = [ { value: 'flight', labelKey: 'reservations.type.flight', Icon: Plane, color: '#3b82f6' }, { value: 'hotel', labelKey: 'reservations.type.hotel', Icon: Hotel, color: '#8b5cf6' }, { value: 'restaurant', labelKey: 'reservations.type.restaurant', Icon: Utensils, color: '#ef4444' }, { value: 'train', labelKey: 'reservations.type.train', Icon: Train, color: '#06b6d4' }, { value: 'car', labelKey: 'reservations.type.car', Icon: Car, color: '#6b7280' }, { value: 'cruise', labelKey: 'reservations.type.cruise', Icon: Ship, color: '#0ea5e9' }, { value: 'event', labelKey: 'reservations.type.event', Icon: Ticket, color: '#f59e0b' }, { value: 'tour', labelKey: 'reservations.type.tour', Icon: Users, color: '#10b981' }, { value: 'other', labelKey: 'reservations.type.other', Icon: FileText, color: '#6b7280' }, ] function getType(type) { return TYPE_OPTIONS.find(t => t.value === type) || TYPE_OPTIONS[TYPE_OPTIONS.length - 1] } function buildAssignmentLookup(days, assignments) { const map = {} for (const day of (days || [])) { const da = (assignments?.[String(day.id)] || []).slice().sort((a, b) => a.order_index - b.order_index) for (const a of da) { if (!a.place) continue map[a.id] = { dayNumber: day.day_number, dayTitle: day.title, dayDate: day.date, placeName: a.place.name, startTime: a.place.place_time, endTime: a.place.end_time } } } return map } interface ReservationCardProps { r: Reservation tripId: number onEdit: (reservation: Reservation) => void onDelete: (id: number) => void files?: TripFile[] onNavigateToFiles: () => void assignmentLookup: Record canEdit: boolean } function ReservationCard({ r, tripId, onEdit, onDelete, files = [], onNavigateToFiles, assignmentLookup, canEdit }: ReservationCardProps) { const { toggleReservationStatus } = useTripStore() const toast = useToast() const { t, locale } = useTranslation() const timeFormat = useSettingsStore(s => s.settings.time_format) || '24h' const blurCodes = useSettingsStore(s => s.settings.blur_booking_codes) const [codeRevealed, setCodeRevealed] = useState(false) const typeInfo = getType(r.type) const TypeIcon = typeInfo.Icon const confirmed = r.status === 'confirmed' const attachedFiles = files.filter(f => f.reservation_id === r.id || (f.linked_reservation_ids || []).includes(r.id)) const linked = r.assignment_id ? assignmentLookup[r.assignment_id] : null const [showDeleteConfirm, setShowDeleteConfirm] = useState(false) const handleToggle = async () => { try { await toggleReservationStatus(tripId, r.id) } catch { toast.error(t('reservations.toast.updateError')) } } const handleDelete = async () => { setShowDeleteConfirm(false) try { await onDelete(r.id) } catch { toast.error(t('reservations.toast.deleteError')) } } const fmtDate = (str) => { const d = new Date(str) return d.toLocaleDateString(locale, { weekday: 'short', day: 'numeric', month: 'short' }) } const fmtTime = (str) => { const d = new Date(str) return d.toLocaleTimeString(locale, { hour: '2-digit', minute: '2-digit', hour12: timeFormat === '12h' }) } return (
{/* Header bar */}
{canEdit ? ( ) : ( {confirmed ? t('reservations.confirmed') : t('reservations.pending')} )}
{t(typeInfo.labelKey)} {r.title} {canEdit && ( )} {canEdit && ( )}
{/* Details */} {(r.reservation_time || r.confirmation_number || r.location || linked || r.metadata) && (
{/* Row 1: Date, Time, Code */} {(r.reservation_time || r.confirmation_number) && (
{r.reservation_time && (
{t('reservations.date')}
{fmtDate(r.reservation_time)}
)} {r.reservation_time?.includes('T') && (
{t('reservations.time')}
{fmtTime(r.reservation_time)}{r.reservation_end_time ? ` โ€“ ${r.reservation_end_time.includes('T') ? fmtTime(r.reservation_end_time) : fmtTime(r.reservation_time.split('T')[0] + 'T' + r.reservation_end_time)}` : ''}
)} {r.confirmation_number && (
{t('reservations.confirmationCode')}
blurCodes && setCodeRevealed(true)} onMouseLeave={() => blurCodes && setCodeRevealed(false)} onClick={() => blurCodes && setCodeRevealed(v => !v)} style={{ fontSize: 11, fontWeight: 600, color: 'var(--text-primary)', marginTop: 1, filter: blurCodes && !codeRevealed ? 'blur(5px)' : 'none', cursor: blurCodes ? 'pointer' : 'default', transition: 'filter 0.2s', }} > {r.confirmation_number}
)}
)} {/* Row 1b: Type-specific metadata */} {(() => { const meta = typeof r.metadata === 'string' ? JSON.parse(r.metadata || '{}') : (r.metadata || {}) if (!meta || Object.keys(meta).length === 0) return null const cells: { label: string; value: string }[] = [] if (meta.airline) cells.push({ label: t('reservations.meta.airline'), value: meta.airline }) if (meta.flight_number) cells.push({ label: t('reservations.meta.flightNumber'), value: meta.flight_number }) if (meta.departure_airport) cells.push({ label: t('reservations.meta.from'), value: meta.departure_airport }) if (meta.arrival_airport) cells.push({ label: t('reservations.meta.to'), value: meta.arrival_airport }) if (meta.train_number) cells.push({ label: t('reservations.meta.trainNumber'), value: meta.train_number }) if (meta.platform) cells.push({ label: t('reservations.meta.platform'), value: meta.platform }) if (meta.seat) cells.push({ label: t('reservations.meta.seat'), value: meta.seat }) if (meta.check_in_time) cells.push({ label: t('reservations.meta.checkIn'), value: meta.check_in_time }) if (meta.check_out_time) cells.push({ label: t('reservations.meta.checkOut'), value: meta.check_out_time }) if (cells.length === 0) return null return (
{cells.map((c, i) => (
{c.label}
{c.value}
))}
) })()} {/* Row 2: Location + Assignment */} {(r.location || linked || r.accommodation_name) && (
{r.location && (
{t('reservations.locationAddress')}
{r.location}
)} {r.accommodation_name && (
{t('reservations.meta.linkAccommodation')}
{r.accommodation_name}
)} {linked && (
{t('reservations.linkAssignment')}
{linked.dayTitle || t('dayplan.dayN', { n: linked.dayNumber })} โ€” {linked.placeName} {linked.startTime ? ` ยท ${linked.startTime}${linked.endTime ? ' โ€“ ' + linked.endTime : ''}` : ''}
)}
)}
)} {/* Notes */} {r.notes && (
{t('reservations.notes')}
{r.notes}
)} {/* Files */} {attachedFiles.length > 0 && (
{t('files.title')}
{attachedFiles.map(f => ( {f.original_name} ))}
)} {/* Delete confirmation popup */} {showDeleteConfirm && ReactDOM.createPortal(
setShowDeleteConfirm(false)}>
e.stopPropagation()}>
{t('reservations.confirm.deleteTitle')}
{t('reservations.confirm.deleteBody', { name: r.title })}
, document.body )}
) } interface SectionProps { title: string count: number children: React.ReactNode defaultOpen?: boolean accent: 'green' | string } function Section({ title, count, children, defaultOpen = true, accent }: SectionProps) { const [open, setOpen] = useState(defaultOpen) return (
{open &&
{children}
}
) } interface ReservationsPanelProps { tripId: number reservations: Reservation[] days: Day[] assignments: AssignmentsMap files?: TripFile[] onAdd: () => void onEdit: (reservation: Reservation) => void onDelete: (id: number) => void onNavigateToFiles: () => void } export default function ReservationsPanel({ tripId, reservations, days, assignments, files = [], onAdd, onEdit, onDelete, onNavigateToFiles }: ReservationsPanelProps) { const { t, locale } = useTranslation() const can = useCanDo() const trip = useTripStore((s) => s.trip) const canEdit = can('reservation_edit', trip) const [showHint, setShowHint] = useState(() => !localStorage.getItem('hideReservationHint')) const assignmentLookup = useMemo(() => buildAssignmentLookup(days, assignments), [days, assignments]) const allPending = reservations.filter(r => r.status !== 'confirmed') const allConfirmed = reservations.filter(r => r.status === 'confirmed') const total = reservations.length return (
{/* Header */}

{t('reservations.title')}

{total === 0 ? t('reservations.empty') : t('reservations.summary', { confirmed: allConfirmed.length, pending: allPending.length })}

{canEdit && ( )}
{/* Content */}
{total === 0 ? (

{t('reservations.empty')}

{t('reservations.emptyHint')}

) : ( <> {allPending.length > 0 && (
{allPending.map(r => )}
)} {allConfirmed.length > 0 && (
{allConfirmed.map(r => )}
)} )}
) }