'use client'; import { useState, useEffect, useRef, useCallback } from 'react'; import { createPortal } from 'react-dom'; import { useParams, useRouter } from 'next/navigation'; import Link from 'next/link'; import { useLanguage } from '@/context/LanguageContext'; import { eventsApi, ticketsApi, emailsApi, paymentOptionsApi, adminApi, Event, Ticket, EmailTemplate, PaymentOptionsConfig } from '@/lib/api'; import Card from '@/components/ui/Card'; import Button from '@/components/ui/Button'; import { ArrowLeftIcon, CalendarIcon, MapPinIcon, CurrencyDollarIcon, UsersIcon, TicketIcon, CheckCircleIcon, ClockIcon, XCircleIcon, EnvelopeIcon, PencilIcon, EyeIcon, EyeSlashIcon, PaperAirplaneIcon, UserGroupIcon, MagnifyingGlassIcon, FunnelIcon, PlusIcon, ChatBubbleLeftIcon, ArrowUturnLeftIcon, XMarkIcon, CreditCardIcon, BanknotesIcon, BoltIcon, BuildingLibraryIcon, ArrowPathIcon, ArrowDownTrayIcon, ChevronDownIcon, EllipsisVerticalIcon, } from '@heroicons/react/24/outline'; import toast from 'react-hot-toast'; import clsx from 'clsx'; type TabType = 'overview' | 'attendees' | 'tickets' | 'email' | 'payments'; // ----- Skeleton loaders ----- function TableSkeleton({ rows = 5 }: { rows?: number }) { return (
{Array.from({ length: rows }).map((_, i) => (
))}
); } function CardSkeleton({ count = 3 }: { count?: number }) { return (
{Array.from({ length: count }).map((_, i) => (
))}
); } // ----- Dropdown component (portal-based to escape overflow:hidden) ----- function Dropdown({ trigger, children, open, onOpenChange, align = 'right' }: { trigger: React.ReactNode; children: React.ReactNode; open: boolean; onOpenChange: (open: boolean) => void; align?: 'left' | 'right'; }) { const triggerRef = useRef(null); const menuRef = useRef(null); const [pos, setPos] = useState<{ top: number; left: number } | null>(null); // Recalculate position when opened useEffect(() => { if (open && triggerRef.current) { const rect = triggerRef.current.getBoundingClientRect(); const menuWidth = 192; // w-48 = 12rem = 192px let left = align === 'right' ? rect.right - menuWidth : rect.left; // Clamp so menu doesn't overflow viewport left = Math.max(8, Math.min(left, window.innerWidth - menuWidth - 8)); setPos({ top: rect.bottom + 4, left }); } }, [open, align]); // Close on outside click useEffect(() => { if (!open) return; const handler = (e: MouseEvent) => { const target = e.target as Node; if ( triggerRef.current && !triggerRef.current.contains(target) && menuRef.current && !menuRef.current.contains(target) ) { onOpenChange(false); } }; document.addEventListener('mousedown', handler); return () => document.removeEventListener('mousedown', handler); }, [open, onOpenChange]); // Close on scroll (the menu position would be stale) useEffect(() => { if (!open) return; const handler = () => onOpenChange(false); window.addEventListener('scroll', handler, true); return () => window.removeEventListener('scroll', handler, true); }, [open, onOpenChange]); return ( <>
onOpenChange(!open)}>{trigger}
{open && pos && createPortal(
{children}
, document.body )} ); } function DropdownItem({ onClick, children, className }: { onClick: () => void; children: React.ReactNode; className?: string }) { return ( ); } // ----- Bottom Sheet (mobile) ----- function BottomSheet({ open, onClose, title, children }: { open: boolean; onClose: () => void; title: string; children: React.ReactNode; }) { if (!open) return null; return (
e.stopPropagation()} >

{title}

{children}
); } // ----- More Menu (per-row) ----- function MoreMenu({ children }: { children: React.ReactNode }) { const [open, setOpen] = useState(false); return ( } > {children} ); } export default function AdminEventDetailPage() { const params = useParams(); const router = useRouter(); const eventId = params.id as string; const { t, locale } = useLanguage(); const [loading, setLoading] = useState(true); const [event, setEvent] = useState(null); const [tickets, setTickets] = useState([]); const [activeTab, setActiveTab] = useState('overview'); // Email state const [templates, setTemplates] = useState([]); const [selectedTemplate, setSelectedTemplate] = useState(''); const [recipientFilter, setRecipientFilter] = useState<'all' | 'confirmed' | 'pending' | 'checked_in'>('confirmed'); const [customMessage, setCustomMessage] = useState(''); const [sending, setSending] = useState(false); const [previewHtml, setPreviewHtml] = useState(null); // Attendees tab state const [searchQuery, setSearchQuery] = useState(''); const [statusFilter, setStatusFilter] = useState<'all' | 'pending' | 'confirmed' | 'checked_in' | 'cancelled'>('all'); const [showAddAtDoorModal, setShowAddAtDoorModal] = useState(false); const [showManualTicketModal, setShowManualTicketModal] = useState(false); const [showStats, setShowStats] = useState(true); const [showNoteModal, setShowNoteModal] = useState(false); const [selectedTicket, setSelectedTicket] = useState(null); const [noteText, setNoteText] = useState(''); const [addAtDoorForm, setAddAtDoorForm] = useState({ firstName: '', lastName: '', email: '', phone: '', autoCheckin: true, adminNote: '', }); const [manualTicketForm, setManualTicketForm] = useState({ firstName: '', lastName: '', email: '', phone: '', adminNote: '', }); const [submitting, setSubmitting] = useState(false); // Export state — separate desktop (Dropdown portal) vs mobile (BottomSheet) const [showExportDropdown, setShowExportDropdown] = useState(false); // desktop dropdown const [showExportSheet, setShowExportSheet] = useState(false); // mobile bottom sheet const [showTicketExportDropdown, setShowTicketExportDropdown] = useState(false); // desktop const [showTicketExportSheet, setShowTicketExportSheet] = useState(false); // mobile const [exporting, setExporting] = useState(false); // Add Ticket — separate desktop dropdown vs mobile bottom sheet const [showAddTicketDropdown, setShowAddTicketDropdown] = useState(false); // desktop const [showAddTicketSheet, setShowAddTicketSheet] = useState(false); // mobile FAB // Tickets tab state const [ticketSearchQuery, setTicketSearchQuery] = useState(''); const [ticketStatusFilter, setTicketStatusFilter] = useState<'all' | 'confirmed' | 'checked_in'>('all'); // Payment options state const [globalPaymentOptions, setGlobalPaymentOptions] = useState(null); const [paymentOverrides, setPaymentOverrides] = useState>({}); const [hasPaymentOverrides, setHasPaymentOverrides] = useState(false); const [savingPayments, setSavingPayments] = useState(false); const [loadingPayments, setLoadingPayments] = useState(false); // Mobile-specific state const [mobileHeaderMenuOpen, setMobileHeaderMenuOpen] = useState(false); const [mobileFilterOpen, setMobileFilterOpen] = useState(false); const [mobileStatsExpanded, setMobileStatsExpanded] = useState(false); // Tab bar ref for sticky const tabBarRef = useRef(null); useEffect(() => { loadEventData(); }, [eventId]); const loadEventData = async () => { try { const [eventRes, ticketsRes, templatesRes] = await Promise.all([ eventsApi.getById(eventId), ticketsApi.getAll({ eventId }), emailsApi.getTemplates(), ]); setEvent(eventRes.event); setTickets(ticketsRes.tickets); setTemplates(templatesRes.templates.filter(t => t.isActive)); } catch (error) { toast.error('Failed to load event data'); } finally { setLoading(false); } }; const loadPaymentOptions = async () => { if (globalPaymentOptions) return; setLoadingPayments(true); try { const [globalRes, overridesRes] = await Promise.all([ paymentOptionsApi.getGlobal(), paymentOptionsApi.getEventOverrides(eventId), ]); setGlobalPaymentOptions(globalRes.paymentOptions); if (overridesRes.overrides) { setPaymentOverrides(overridesRes.overrides); setHasPaymentOverrides(true); } } catch (error) { toast.error('Failed to load payment options'); } finally { setLoadingPayments(false); } }; useEffect(() => { if (activeTab === 'payments') { loadPaymentOptions(); } }, [activeTab]); const getEffectivePaymentOption = (key: K): PaymentOptionsConfig[K] => { if (paymentOverrides[key] !== undefined && paymentOverrides[key] !== null) { return paymentOverrides[key] as PaymentOptionsConfig[K]; } return globalPaymentOptions?.[key] as PaymentOptionsConfig[K]; }; const updatePaymentOverride = ( key: K, value: PaymentOptionsConfig[K] | null ) => { setPaymentOverrides((prev) => ({ ...prev, [key]: value })); setHasPaymentOverrides(true); }; const handleSavePaymentOptions = async () => { setSavingPayments(true); try { await paymentOptionsApi.updateEventOverrides(eventId, paymentOverrides); toast.success(locale === 'es' ? 'Opciones de pago guardadas' : 'Payment options saved'); } catch (error: any) { toast.error(error.message || 'Failed to save payment options'); } finally { setSavingPayments(false); } }; const handleResetToGlobal = async () => { if (!confirm(locale === 'es' ? '¿Resetear a la configuración global? Se eliminarán todas las personalizaciones de este evento.' : 'Reset to global settings? This will remove all customizations for this event.')) { return; } setSavingPayments(true); try { await paymentOptionsApi.deleteEventOverrides(eventId); setPaymentOverrides({}); setHasPaymentOverrides(false); toast.success(locale === 'es' ? 'Restablecido a configuración global' : 'Reset to global settings'); } catch (error: any) { toast.error(error.message || 'Failed to reset payment options'); } finally { setSavingPayments(false); } }; const formatDate = (dateStr: string) => { return new Date(dateStr).toLocaleDateString(locale === 'es' ? 'es-ES' : 'en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', timeZone: 'America/Asuncion', }); }; const formatDateShort = (dateStr: string) => { return new Date(dateStr).toLocaleDateString(locale === 'es' ? 'es-ES' : 'en-US', { month: 'short', day: 'numeric', year: 'numeric', timeZone: 'America/Asuncion', }); }; const formatTime = (dateStr: string) => { return new Date(dateStr).toLocaleTimeString(locale === 'es' ? 'es-ES' : 'en-US', { hour: '2-digit', minute: '2-digit', timeZone: 'America/Asuncion', }); }; const formatCurrency = (amount: number, currency: string) => { if (currency === 'PYG') { return `${amount.toLocaleString('es-PY')} PYG`; } return `$${amount.toFixed(2)} ${currency}`; }; const getTicketsByStatus = (status: string) => { return tickets.filter(t => t.status === status); }; const getFilteredRecipientCount = () => { if (recipientFilter === 'all') return tickets.length; return getTicketsByStatus(recipientFilter).length; }; const getStatusBadge = (status: string, compact = false) => { const styles: Record = { pending: 'bg-yellow-100 text-yellow-800', confirmed: 'bg-green-100 text-green-800', cancelled: 'bg-red-100 text-red-800', checked_in: 'bg-blue-100 text-blue-800', }; return ( {status.replace('_', ' ')} ); }; const handleMarkPaid = async (ticketId: string) => { try { await ticketsApi.markPaid(ticketId); toast.success('Payment marked as received'); loadEventData(); } catch (error: any) { toast.error(error.message || 'Failed to mark payment'); } }; const handleCheckin = async (ticketId: string) => { try { await ticketsApi.checkin(ticketId); toast.success('Attendee checked in'); loadEventData(); } catch (error: any) { toast.error(error.message || 'Failed to check in'); } }; const handleRemoveCheckin = async (ticketId: string) => { if (!confirm('Are you sure you want to remove the check-in for this attendee?')) return; try { await ticketsApi.removeCheckin(ticketId); toast.success('Check-in removed'); loadEventData(); } catch (error: any) { toast.error(error.message || 'Failed to remove check-in'); } }; const handleOpenNoteModal = (ticket: Ticket) => { setSelectedTicket(ticket); setNoteText(ticket.adminNote || ''); setShowNoteModal(true); }; const handleSaveNote = async () => { if (!selectedTicket) return; setSubmitting(true); try { await ticketsApi.updateNote(selectedTicket.id, noteText); toast.success('Note saved'); setShowNoteModal(false); setSelectedTicket(null); setNoteText(''); loadEventData(); } catch (error: any) { toast.error(error.message || 'Failed to save note'); } finally { setSubmitting(false); } }; const handleAddAtDoor = async (e: React.FormEvent) => { e.preventDefault(); if (!event) return; setSubmitting(true); try { await ticketsApi.adminCreate({ eventId: event.id, firstName: addAtDoorForm.firstName, lastName: addAtDoorForm.lastName || undefined, email: addAtDoorForm.email, phone: addAtDoorForm.phone, autoCheckin: addAtDoorForm.autoCheckin, adminNote: addAtDoorForm.adminNote || undefined, }); toast.success(addAtDoorForm.autoCheckin ? 'Attendee added and checked in' : 'Attendee added'); setShowAddAtDoorModal(false); setAddAtDoorForm({ firstName: '', lastName: '', email: '', phone: '', autoCheckin: true, adminNote: '' }); loadEventData(); } catch (error: any) { toast.error(error.message || 'Failed to add attendee'); } finally { setSubmitting(false); } }; const handleManualTicket = async (e: React.FormEvent) => { e.preventDefault(); if (!event) return; setSubmitting(true); try { await ticketsApi.manualCreate({ eventId: event.id, firstName: manualTicketForm.firstName, lastName: manualTicketForm.lastName || undefined, email: manualTicketForm.email, phone: manualTicketForm.phone || undefined, adminNote: manualTicketForm.adminNote || undefined, }); toast.success('Manual ticket created — confirmation email sent'); setShowManualTicketModal(false); setManualTicketForm({ firstName: '', lastName: '', email: '', phone: '', adminNote: '' }); loadEventData(); } catch (error: any) { toast.error(error.message || 'Failed to create manual ticket'); } finally { setSubmitting(false); } }; const handleExportAttendees = async (status: 'confirmed' | 'checked_in' | 'confirmed_pending' | 'all') => { if (!event) return; setExporting(true); setShowExportDropdown(false); try { const { blob, filename } = await adminApi.exportAttendees(event.id, { status, format: 'csv', q: searchQuery || undefined }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); toast.success('Export downloaded'); } catch (error: any) { toast.error(error.message || 'Failed to export attendees'); } finally { setExporting(false); } }; const handleExportTickets = async (status: 'confirmed' | 'checked_in' | 'all') => { if (!event) return; setExporting(true); setShowTicketExportDropdown(false); try { const { blob, filename } = await adminApi.exportTicketsCSV(event.id, { status, q: ticketSearchQuery || undefined }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); toast.success('Export downloaded'); } catch (error: any) { toast.error(error.message || 'Failed to export tickets'); } finally { setExporting(false); } }; // Filtered tickets for attendees tab const filteredTickets = tickets.filter((ticket) => { if (statusFilter !== 'all' && ticket.status !== statusFilter) return false; if (searchQuery) { const query = searchQuery.toLowerCase(); const fullName = `${ticket.attendeeFirstName} ${ticket.attendeeLastName || ''}`.trim().toLowerCase(); return ( fullName.includes(query) || (ticket.attendeeEmail?.toLowerCase().includes(query) || false) || (ticket.attendeePhone?.toLowerCase().includes(query) || false) || ticket.id.toLowerCase().includes(query) ); } return true; }); // Filtered tickets for the Tickets tab (only confirmed/checked_in) const confirmedTickets = tickets.filter(t => ['confirmed', 'checked_in'].includes(t.status)); const filteredConfirmedTickets = confirmedTickets.filter((ticket) => { if (ticketStatusFilter !== 'all' && ticket.status !== ticketStatusFilter) return false; if (ticketSearchQuery) { const query = ticketSearchQuery.toLowerCase(); const fullName = `${ticket.attendeeFirstName} ${ticket.attendeeLastName || ''}`.trim().toLowerCase(); return ( fullName.includes(query) || ticket.id.toLowerCase().includes(query) ); } return true; }); const handlePreviewEmail = async () => { if (!selectedTemplate) { toast.error('Please select a template'); return; } try { const res = await emailsApi.preview({ templateSlug: selectedTemplate, variables: { attendeeName: 'John Doe', attendeeEmail: 'john@example.com', ticketId: 'TKT-PREVIEW', eventTitle: event?.title || '', eventDate: event ? formatDate(event.startDatetime) : '', eventTime: event ? formatTime(event.startDatetime) : '', eventLocation: event?.location || '', eventLocationUrl: event?.locationUrl || '', eventPrice: event ? formatCurrency(event.price, event.currency) : '', customMessage: customMessage || 'Your custom message will appear here.', }, locale, }); setPreviewHtml(res.bodyHtml); } catch (error) { toast.error('Failed to preview email'); } }; const handleSendEmail = async () => { if (!selectedTemplate) { toast.error('Please select a template'); return; } const recipientCount = getFilteredRecipientCount(); if (recipientCount === 0) { toast.error('No recipients match the selected filter'); return; } if (!confirm(`Send email to ${recipientCount} ${recipientFilter === 'all' ? 'attendee(s)' : `${recipientFilter} attendee(s)`}?`)) { return; } try { const res = await emailsApi.sendToEvent(eventId, { templateSlug: selectedTemplate, recipientFilter, customVariables: customMessage ? { customMessage } : undefined, }); if (res.success) { toast.success(`${res.queuedCount} email(s) are being sent in the background.`); } else { toast.error(res.error || 'Failed to queue emails'); } } catch (error: any) { toast.error(error.message || 'Failed to send emails'); } }; if (loading) { return (
); } if (!event) { return (

Event not found

); } const confirmedCount = getTicketsByStatus('confirmed').length; const pendingCount = getTicketsByStatus('pending').length; const checkedInCount = getTicketsByStatus('checked_in').length; const cancelledCount = getTicketsByStatus('cancelled').length; const revenue = (confirmedCount + checkedInCount) * event.price; const tabs: { key: TabType; label: string; icon: typeof CalendarIcon; count?: number }[] = [ { key: 'overview', label: 'Overview', icon: CalendarIcon }, { key: 'attendees', label: 'Attendees', icon: UserGroupIcon, count: tickets.length }, { key: 'tickets', label: 'Tickets', icon: TicketIcon, count: confirmedTickets.length }, { key: 'email', label: 'Email', icon: EnvelopeIcon }, { key: 'payments', label: locale === 'es' ? 'Pagos' : 'Payments', icon: CreditCardIcon }, ]; // ========== Primary action for a ticket ========== const getPrimaryAction = (ticket: Ticket) => { if (ticket.status === 'pending') { return { label: 'Mark Paid', onClick: () => handleMarkPaid(ticket.id), variant: 'outline' as const }; } if (ticket.status === 'confirmed') { return { label: 'Check In', onClick: () => handleCheckin(ticket.id), variant: 'primary' as const }; } if (ticket.status === 'checked_in') { return { label: 'Undo', onClick: () => handleRemoveCheckin(ticket.id), variant: 'outline' as const, icon: ArrowUturnLeftIcon }; } return null; }; return (
{/* ============= HEADER ============= */}

{event.title}

{formatDateShort(event.startDatetime)} · {formatTime(event.startDatetime)}

{/* Desktop header actions */}
{/* Mobile header overflow menu */}
} > { window.open(`/events/${event.id}`, '_blank'); setMobileHeaderMenuOpen(false); }}> View Public { router.push(`/admin/events?edit=${event.id}`); setMobileHeaderMenuOpen(false); }}> Edit Event { setShowStats(v => !v); setMobileHeaderMenuOpen(false); }}> {showStats ? : } {showStats ? 'Hide Stats' : 'Show Stats'}
{/* ============= COMPACT META CHIPS (desktop) ============= */}
{formatDateShort(event.startDatetime)} {formatTime(event.startDatetime)}{event.endDatetime && ` – ${formatTime(event.endDatetime)}`} {event.location} {event.price === 0 ? 'Free' : formatCurrency(event.price, event.currency)} {confirmedCount + checkedInCount}/{event.capacity}
{/* ============= STATS ROW ============= */} {/* Desktop: always-visible compact 4-card row */}
{showStats && (
{[ { label: 'Capacity', value: `${confirmedCount + checkedInCount}/${event.capacity}`, icon: UsersIcon, color: 'bg-blue-50 text-blue-600' }, { label: 'Confirmed', value: confirmedCount, icon: CheckCircleIcon, color: 'bg-green-50 text-green-600' }, { label: 'Checked In', value: checkedInCount, icon: TicketIcon, color: 'bg-purple-50 text-purple-600' }, { label: 'Revenue', value: formatCurrency(revenue, event.currency), icon: CurrencyDollarIcon, color: 'bg-gray-50 text-gray-600' }, ].map((stat) => (

{stat.value}

{stat.label}

))}
)}
{/* Mobile: collapsible stats */}
{showStats && (
{mobileStatsExpanded && (
{[ { label: 'Capacity', value: `${confirmedCount + checkedInCount}/${event.capacity}`, icon: UsersIcon, color: 'text-blue-600 bg-blue-50' }, { label: 'Confirmed', value: confirmedCount, icon: CheckCircleIcon, color: 'text-green-600 bg-green-50' }, { label: 'Checked In', value: checkedInCount, icon: TicketIcon, color: 'text-purple-600 bg-purple-50' }, { label: 'Revenue', value: formatCurrency(revenue, event.currency), icon: CurrencyDollarIcon, color: 'text-gray-600 bg-gray-50' }, ].map((stat) => (

{stat.value}

{stat.label}

))}
)}
)}
{/* ============= TABS + CONTENT ============= */} {/* Unified container: tabs are visually connected to the content below */} {/* ============= TAB BAR ============= */} {/* Desktop: tab bar inside a card top-section */}
{/* Mobile: segmented tab bar */}
{tabs.map((tab) => ( ))}
{/* ============= TAB CONTENT ============= */} {/* Desktop: content panel continues from the tab bar card (no top radius) */} {/* Mobile: content flows directly below the segmented control */}
{/* ============= OVERVIEW TAB ============= */} {activeTab === 'overview' && (

Event Information

Date & Time

{formatDate(event.startDatetime)}

{formatTime(event.startDatetime)}{event.endDatetime && ` - ${formatTime(event.endDatetime)}`}

Location

{event.location}

{event.locationUrl && ( View on Map )}

Price

{event.price === 0 ? 'Free' : formatCurrency(event.price, event.currency)}

Capacity

{confirmedCount + checkedInCount} / {event.capacity} spots filled

{Math.max(0, event.capacity - confirmedCount - checkedInCount)} spots remaining

Description

{event.description}

{event.descriptionEs && ( <>

Spanish:

{event.descriptionEs}

)}
{event.bannerUrl && (

Event Banner

{event.title}
)}
)} {/* ============= ATTENDEES TAB ============= */} {activeTab === 'attendees' && (
{/* Desktop toolbar */}
{/* Left: Search + Status */}
setSearchQuery(e.target.value)} className="w-full pl-9 pr-3 py-1.5 text-sm rounded-btn border border-secondary-light-gray focus:outline-none focus:ring-2 focus:ring-primary-yellow" />
{/* Right: Export + Add Ticket dropdown */} {exporting ? (
) : ( )} Export } > handleExportAttendees('all')}>Export All handleExportAttendees('confirmed')}>Export Confirmed handleExportAttendees('checked_in')}>Export Checked-in handleExportAttendees('confirmed_pending')}>Confirmed & Pending
Format: CSV
Add Ticket } > { setShowManualTicketModal(true); setShowAddTicketDropdown(false); }}> Manual Ticket { setShowAddAtDoorModal(true); setShowAddTicketDropdown(false); }}> Add at Door
{(searchQuery || statusFilter !== 'all') && (
Showing {filteredTickets.length} of {tickets.length}
)} {/* Mobile toolbar */}
setSearchQuery(e.target.value)} className="w-full pl-9 pr-3 py-2.5 text-sm rounded-btn border border-secondary-light-gray focus:outline-none focus:ring-2 focus:ring-primary-yellow" />
{(searchQuery || statusFilter !== 'all') && ( )}
{(searchQuery || statusFilter !== 'all') && (

Showing {filteredTickets.length} of {tickets.length}

)}
{/* Desktop: Dense table */}
{filteredTickets.length === 0 ? ( ) : ( filteredTickets.map((ticket) => { const primary = getPrimaryAction(ticket); return ( ); }) )}
Attendee Contact Status Booked Actions
{tickets.length === 0 ? 'No attendees yet' : 'No attendees match the current filters'}

{ticket.attendeeFirstName} {ticket.attendeeLastName || ''}

{ticket.bookingId && ( Group booking )}

{ticket.attendeeEmail}

{ticket.attendeePhone &&

{ticket.attendeePhone}

}
{getStatusBadge(ticket.status, true)} {ticket.checkinAt && (

{new Date(ticket.checkinAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', timeZone: 'America/Asuncion' })}

)}
{new Date(ticket.createdAt).toLocaleDateString(locale === 'es' ? 'es-ES' : 'en-US', { timeZone: 'America/Asuncion' })}
{primary && ( )} handleOpenNoteModal(ticket)}> {ticket.adminNote ? 'Edit Note' : 'Add Note'} {ticket.adminNote && (
Note: {ticket.adminNote}
)}
ID: {ticket.id.slice(0, 8)}...
{/* Mobile: Card layout */}
{filteredTickets.length === 0 ? (
{tickets.length === 0 ? 'No attendees yet' : 'No attendees match the current filters'}
) : ( filteredTickets.map((ticket) => { const primary = getPrimaryAction(ticket); return (

{ticket.attendeeFirstName} {ticket.attendeeLastName || ''}

{ticket.attendeeEmail}

{ticket.attendeePhone &&

{ticket.attendeePhone}

}
{getStatusBadge(ticket.status, true)}

{new Date(ticket.createdAt).toLocaleDateString(locale === 'es' ? 'es-ES' : 'en-US', { timeZone: 'America/Asuncion' })} {ticket.checkinAt && ` · Checked in ${new Date(ticket.checkinAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', timeZone: 'America/Asuncion' })}`}

{primary && ( )} handleOpenNoteModal(ticket)}> {ticket.adminNote ? 'Edit Note' : 'Add Note'}
); }) )}
{/* Mobile FAB */}
)} {/* ============= TICKETS TAB ============= */} {activeTab === 'tickets' && (
{/* Desktop toolbar */}
setTicketSearchQuery(e.target.value)} className="w-full pl-9 pr-3 py-1.5 text-sm rounded-btn border border-secondary-light-gray focus:outline-none focus:ring-2 focus:ring-primary-yellow" />
Export } > handleExportTickets('all')}>Export All handleExportTickets('confirmed')}>Export Valid handleExportTickets('checked_in')}>Export Checked-in
Format: CSV
{(ticketSearchQuery || ticketStatusFilter !== 'all') && (
Showing {filteredConfirmedTickets.length} of {confirmedTickets.length}
)} {/* Mobile toolbar */}
setTicketSearchQuery(e.target.value)} className="w-full pl-9 pr-3 py-2.5 text-sm rounded-btn border border-secondary-light-gray focus:outline-none focus:ring-2 focus:ring-primary-yellow" />
{/* Desktop: Dense table */}
{filteredConfirmedTickets.length === 0 ? ( ) : ( filteredConfirmedTickets.map((ticket) => ( )) )}
Attendee Status Check-in Actions
{confirmedTickets.length === 0 ? 'No confirmed tickets yet' : 'No tickets match the current filters'}

{ticket.attendeeFirstName} {ticket.attendeeLastName || ''}

{ticket.bookingId && ( Group booking )}
{ticket.status === 'confirmed' ? ( Valid ) : ( Checked In )} {ticket.checkinAt ? ( new Date(ticket.checkinAt).toLocaleString(locale === 'es' ? 'es-ES' : 'en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', timeZone: 'America/Asuncion', }) ) : ( )}
{ticket.status === 'confirmed' && ( )} {ticket.status === 'checked_in' && ( )}
ID: {ticket.id.slice(0, 8)}...
{ticket.bookingId && (
Booking: {ticket.bookingId.slice(0, 8)}...
)}
{/* Mobile: Card layout */}
{filteredConfirmedTickets.length === 0 ? (
{confirmedTickets.length === 0 ? 'No confirmed tickets yet' : 'No tickets match the current filters'}
) : ( filteredConfirmedTickets.map((ticket) => (

{ticket.attendeeFirstName} {ticket.attendeeLastName || ''}

{ticket.bookingId &&

Group booking

}
{ticket.status === 'confirmed' ? ( Valid ) : ( Checked In )}

{ticket.checkinAt ? `Checked in ${new Date(ticket.checkinAt).toLocaleString(locale === 'es' ? 'es-ES' : 'en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', timeZone: 'America/Asuncion' })}` : 'Not checked in'}

{ticket.status === 'confirmed' && ( )} {ticket.status === 'checked_in' && ( )}
)) )}
)} {/* ============= EMAIL TAB ============= */} {activeTab === 'email' && (

Send Email to Attendees