'use client'; import { useState, useEffect, useRef } from 'react'; import { useParams } from 'next/navigation'; import Link from 'next/link'; import { useLanguage } from '@/context/LanguageContext'; import { ticketsApi, emailsApi, adminApi, paymentsApi, siteSettingsApi, Ticket } from '@/lib/api'; import { formatDateLong, formatDateCompact, formatTime } from '@/lib/utils'; import Card from '@/components/ui/Card'; import Button from '@/components/ui/Button'; import { Skeleton, TableSkeleton } from '@/components/ui/Skeleton'; import { Dropdown, DropdownItem, AdminMobileStyles } from '@/components/admin/MobileComponents'; import { ArrowLeftIcon, CalendarIcon, MapPinIcon, CurrencyDollarIcon, UsersIcon, TicketIcon, CheckCircleIcon, EnvelopeIcon, PencilIcon, EyeIcon, UserGroupIcon, CreditCardIcon, ChevronDownIcon, EllipsisVerticalIcon, ArrowUturnLeftIcon, } from '@heroicons/react/24/outline'; import toast from 'react-hot-toast'; import clsx from 'clsx'; import { usePrivacy } from '@/context/PrivacyContext'; import type { TabType, AttendeeStatusFilter, TicketStatusFilter, RecipientFilter, AddTicketType, AddTicketFormState, PrimaryAction, } from './_types'; import { formatCurrency, downloadBlob } from './_utils/format'; import { useEventDetailData } from './_hooks/useEventDetailData'; import { usePaymentOverrides } from './_hooks/usePaymentOverrides'; import { OverviewTab } from './_tabs/OverviewTab'; import { AttendeesTab } from './_tabs/AttendeesTab'; import { TicketsTab } from './_tabs/TicketsTab'; import { EmailTab } from './_tabs/EmailTab'; import { PaymentsTab } from './_tabs/PaymentsTab'; import { EventModals } from './_modals/EventModals'; import { AddTicketModal } from './_modals/AddTicketModal'; import EventFormModal from '../_components/EventFormModal'; const EMPTY_ADD_TICKET_FORM: AddTicketFormState = { type: 'paid', firstName: '', lastName: '', email: '', phone: '', adminNote: '', checkinNow: false, }; export default function AdminEventDetailPage() { const params = useParams(); const eventId = params.id as string; const { locale } = useLanguage(); const { loading, event, tickets, templates, loadEventData } = useEventDetailData(eventId); const [activeTab, setActiveTab] = useState('overview'); // Email state const [selectedTemplate, setSelectedTemplate] = useState(''); const [recipientFilter, setRecipientFilter] = useState('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'); const { privacyMode } = usePrivacy(); const showStats = !privacyMode; const [showNoteModal, setShowNoteModal] = useState(false); const [selectedTicket, setSelectedTicket] = useState(null); const [noteText, setNoteText] = useState(''); // Unified Add Ticket modal (paid / unpaid / guest via segmented control) const [showAddTicketModal, setShowAddTicketModal] = useState(false); const [addTicketForm, setAddTicketForm] = useState(EMPTY_ADD_TICKET_FORM); const [submitting, setSubmitting] = useState(false); const openAddTicket = (type: AddTicketType) => { setAddTicketForm({ ...EMPTY_ADD_TICKET_FORM, type }); setShowAddTicketModal(true); }; // 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'); // Payment options state + handlers const payments = usePaymentOverrides(eventId, locale); // Edit event modal (opens in place instead of redirecting to the list page) const [showEditForm, setShowEditForm] = useState(false); const [featuredEventId, setFeaturedEventId] = useState(null); useEffect(() => { siteSettingsApi .get() .then(({ settings }) => setFeaturedEventId(settings.featuredEventId || null)) .catch(() => {}); }, []); // 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(() => { if (activeTab === 'payments') { payments.loadPaymentOptions(); } }, [activeTab]); const formatDate = (dateStr: string) => formatDateLong(dateStr, locale as 'en' | 'es'); const formatDateShort = (dateStr: string) => formatDateCompact(dateStr, locale as 'en' | 'es'); const fmtTime = (dateStr: string) => formatTime(dateStr, locale as 'en' | 'es'); const getTicketsByStatus = (status: string) => { return tickets.filter(t => t.status === status); }; const getFilteredRecipientCount = () => { if (recipientFilter === 'all') return tickets.length; return getTicketsByStatus(recipientFilter).length; }; 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 handleReactivate = async (ticket: Ticket) => { if (!ticket.payment?.id) return; try { await paymentsApi.reactivate(ticket.payment.id); toast.success('Booking reactivated'); loadEventData(); } catch (error: any) { toast.error(error.message || 'Failed to reactivate booking'); } }; 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 handleAddTicket = async (e: React.FormEvent) => { e.preventDefault(); if (!event) return; setSubmitting(true); try { const res = await ticketsApi.adminAdd({ eventId: event.id, type: addTicketForm.type, firstName: addTicketForm.firstName, lastName: addTicketForm.lastName || undefined, email: addTicketForm.email || undefined, phone: addTicketForm.phone || undefined, checkinNow: addTicketForm.checkinNow, adminNote: addTicketForm.adminNote || undefined, }); toast.success(res.message || 'Ticket created'); setShowAddTicketModal(false); setAddTicketForm(EMPTY_ADD_TICKET_FORM); loadEventData(); } catch (error: any) { toast.error(error.message || 'Failed to add 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 }); downloadBlob(blob, filename); 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 }); downloadBlob(blob, filename); 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 ? fmtTime(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; } setSending(true); 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'); } finally { setSending(false); } }; 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 onHoldCount = getTicketsByStatus('on_hold').length; // Revenue counts only settled tickets: unpaid (balance due) and comp (guest) // tickets are excluded; legacy rows without paymentStatus fall back to !isGuest const isRevenueTicket = (t: Ticket) => (t.paymentStatus ? t.paymentStatus === 'paid' : !t.isGuest); const paidConfirmedCount = getTicketsByStatus('confirmed').filter(isRevenueTicket).length; const paidCheckedInCount = getTicketsByStatus('checked_in').filter(isRevenueTicket).length; const revenue = (paidConfirmedCount + paidCheckedInCount) * 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): PrimaryAction | null => { if (ticket.status === 'pending' || ticket.status === 'on_hold') { return { label: 'Mark Paid', onClick: () => handleMarkPaid(ticket.id), variant: 'outline' }; } if (ticket.status === 'confirmed') { // Unpaid tickets resolve their balance first; check-in stays available via scanner if (ticket.paymentStatus === 'unpaid') { return { label: 'Mark Paid', onClick: () => handleMarkPaid(ticket.id), variant: 'outline' }; } return { label: 'Check In', onClick: () => handleCheckin(ticket.id), variant: 'primary' }; } if (ticket.status === 'checked_in') { return { label: 'Undo', onClick: () => handleRemoveCheckin(ticket.id), variant: 'outline', icon: ArrowUturnLeftIcon }; } return null; }; return (
{/* ============= HEADER ============= */}

{event.title}

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

{/* Desktop header actions */}
{/* Mobile header overflow menu */}
} > { window.open(`/events/${event.slug}`, '_blank'); setMobileHeaderMenuOpen(false); }}> View Public { setShowEditForm(true); setMobileHeaderMenuOpen(false); }}> Edit Event
{/* ============= COMPACT META CHIPS (desktop) ============= */}
{formatDateShort(event.startDatetime)} {fmtTime(event.startDatetime)}{event.endDatetime && ` – ${fmtTime(event.endDatetime)}`} {event.location} {event.price === 0 ? 'Free' : formatCurrency(event.price, event.currency)} {showStats && ( {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}

))}
)}
)}
{/* ============= TAB BAR ============= */} {/* Desktop: tab bar inside a card top-section */}
{/* Mobile: segmented tab bar */}
{tabs.map((tab) => ( ))}
{/* ============= TAB CONTENT ============= */}
{activeTab === 'overview' && ( )} {activeTab === 'attendees' && ( )} {activeTab === 'tickets' && ( )} {activeTab === 'email' && ( )} {activeTab === 'payments' && ( )}
{/* ============= MODALS ============= */} setShowAddTicketModal(false)} form={addTicketForm} setForm={setAddTicketForm} onSubmit={handleAddTicket} submitting={submitting} eventPriceLabel={event.price === 0 ? 'Free' : formatCurrency(event.price, event.currency)} /> setShowEditForm(false)} onSaved={() => { setShowEditForm(false); loadEventData(); }} />
); }