Files
Spanglish/frontend/src/app/admin/events/[id]/page.tsx
T
MichilisandCursor 75e317d73e Replace spinners with skeleton loading states for faster perceived UX.
Add reusable Skeleton components and route-level loading files across public pages and admin lists so layouts stay stable while data loads.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-12 23:14:11 +00:00

826 lines
33 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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,
EyeSlashIcon,
UserGroupIcon,
CreditCardIcon,
ChevronDownIcon,
EllipsisVerticalIcon,
ArrowUturnLeftIcon,
} from '@heroicons/react/24/outline';
import toast from 'react-hot-toast';
import clsx from 'clsx';
import { useStatsPrivacy } from '@/hooks/useStatsPrivacy';
import type {
TabType,
AttendeeStatusFilter,
TicketStatusFilter,
RecipientFilter,
AttendeeFormState,
AddAtDoorFormState,
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 EventFormModal from '../_components/EventFormModal';
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<TabType>('overview');
// Email state
const [selectedTemplate, setSelectedTemplate] = useState<string>('');
const [recipientFilter, setRecipientFilter] = useState<RecipientFilter>('confirmed');
const [customMessage, setCustomMessage] = useState('');
const [sending, setSending] = useState(false);
const [previewHtml, setPreviewHtml] = useState<string | null>(null);
// Attendees tab state
const [searchQuery, setSearchQuery] = useState('');
const [statusFilter, setStatusFilter] = useState<AttendeeStatusFilter>('all');
const [showAddAtDoorModal, setShowAddAtDoorModal] = useState(false);
const [showManualTicketModal, setShowManualTicketModal] = useState(false);
const [showStats, , toggleStats] = useStatsPrivacy();
const [showNoteModal, setShowNoteModal] = useState(false);
const [selectedTicket, setSelectedTicket] = useState<Ticket | null>(null);
const [noteText, setNoteText] = useState('');
const [addAtDoorForm, setAddAtDoorForm] = useState<AddAtDoorFormState>({
firstName: '',
lastName: '',
email: '',
phone: '',
autoCheckin: true,
adminNote: '',
});
const [manualTicketForm, setManualTicketForm] = useState<AttendeeFormState>({
firstName: '',
lastName: '',
email: '',
phone: '',
adminNote: '',
});
const [showInviteGuestModal, setShowInviteGuestModal] = useState(false);
const [inviteGuestForm, setInviteGuestForm] = useState<AttendeeFormState>({
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<TicketStatusFilter>('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<string | null>(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<HTMLDivElement>(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 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 handleInviteGuest = async (e: React.FormEvent) => {
e.preventDefault();
if (!event) return;
setSubmitting(true);
try {
await ticketsApi.guestCreate({
eventId: event.id,
firstName: inviteGuestForm.firstName,
lastName: inviteGuestForm.lastName || undefined,
email: inviteGuestForm.email || undefined,
phone: inviteGuestForm.phone || undefined,
adminNote: inviteGuestForm.adminNote || undefined,
});
toast.success('Guest invited successfully');
setShowInviteGuestModal(false);
setInviteGuestForm({ firstName: '', lastName: '', email: '', phone: '', adminNote: '' });
loadEventData();
} catch (error: any) {
toast.error(error.message || 'Failed to invite guest');
} 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 (
<div>
<div className="flex items-center justify-between mb-6">
<Skeleton className="h-8 w-64" />
<Skeleton className="h-10 w-36 rounded-btn hidden md:block" />
</div>
<TableSkeleton rows={5} cols={4} />
</div>
);
}
if (!event) {
return (
<div className="text-center py-12">
<p className="text-gray-500">Event not found</p>
<Link href="/admin/events">
<Button variant="outline" className="mt-4">
Back to Events
</Button>
</Link>
</div>
);
}
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;
const paidConfirmedCount = getTicketsByStatus('confirmed').filter(t => !t.isGuest).length;
const paidCheckedInCount = getTicketsByStatus('checked_in').filter(t => !t.isGuest).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') {
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 (
<div className="max-w-full overflow-x-hidden">
{/* ============= HEADER ============= */}
<div className="flex items-start gap-3 mb-3">
<Link href="/admin/events" className="mt-1 flex-shrink-0">
<button className="p-2 hover:bg-gray-100 rounded-btn min-h-[44px] min-w-[44px] flex items-center justify-center">
<ArrowLeftIcon className="w-5 h-5" />
</button>
</Link>
<div className="flex-1 min-w-0">
<h1 className="text-xl md:text-2xl font-bold text-primary-dark truncate">{event.title}</h1>
<p className="text-sm text-gray-500">{formatDateShort(event.startDatetime)} &middot; {fmtTime(event.startDatetime)}</p>
</div>
{/* Desktop header actions */}
<div className="hidden md:flex items-center gap-2 flex-shrink-0">
<Button variant="outline" size="sm" onClick={toggleStats} title={showStats ? 'Hide stats' : 'Show stats'}>
{showStats ? <EyeSlashIcon className="w-4 h-4 mr-1.5" /> : <EyeIcon className="w-4 h-4 mr-1.5" />}
{showStats ? 'Hide Stats' : 'Show Stats'}
</Button>
<Link href={`/events/${event.slug}`} target="_blank">
<Button variant="outline" size="sm">
<EyeIcon className="w-4 h-4 mr-1.5" />
View Public
</Button>
</Link>
<Button variant="outline" size="sm" onClick={() => setShowEditForm(true)}>
<PencilIcon className="w-4 h-4 mr-1.5" />
Edit
</Button>
</div>
{/* Mobile header overflow menu */}
<div className="md:hidden flex-shrink-0">
<Dropdown
open={mobileHeaderMenuOpen}
onOpenChange={setMobileHeaderMenuOpen}
trigger={
<button className="p-2 hover:bg-gray-100 rounded-btn min-h-[44px] min-w-[44px] flex items-center justify-center">
<EllipsisVerticalIcon className="w-5 h-5" />
</button>
}
>
<DropdownItem onClick={() => { window.open(`/events/${event.slug}`, '_blank'); setMobileHeaderMenuOpen(false); }}>
<EyeIcon className="w-4 h-4 mr-2" /> View Public
</DropdownItem>
<DropdownItem onClick={() => { setShowEditForm(true); setMobileHeaderMenuOpen(false); }}>
<PencilIcon className="w-4 h-4 mr-2" /> Edit Event
</DropdownItem>
<DropdownItem onClick={() => { toggleStats(); setMobileHeaderMenuOpen(false); }}>
{showStats ? <EyeSlashIcon className="w-4 h-4 mr-2" /> : <EyeIcon className="w-4 h-4 mr-2" />}
{showStats ? 'Hide Stats' : 'Show Stats'}
</DropdownItem>
</Dropdown>
</div>
</div>
{/* ============= COMPACT META CHIPS (desktop) ============= */}
<div className="hidden md:flex flex-wrap items-center gap-2 mb-4 ml-[52px]">
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 bg-gray-100 rounded-full text-xs text-gray-700">
<CalendarIcon className="w-3.5 h-3.5" />
{formatDateShort(event.startDatetime)} {fmtTime(event.startDatetime)}{event.endDatetime && ` ${fmtTime(event.endDatetime)}`}
</span>
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 bg-gray-100 rounded-full text-xs text-gray-700">
<MapPinIcon className="w-3.5 h-3.5" />
{event.location}
</span>
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 bg-gray-100 rounded-full text-xs text-gray-700">
<CurrencyDollarIcon className="w-3.5 h-3.5" />
{event.price === 0 ? 'Free' : formatCurrency(event.price, event.currency)}
</span>
{showStats && (
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 bg-gray-100 rounded-full text-xs text-gray-700">
<UsersIcon className="w-3.5 h-3.5" />
{confirmedCount + checkedInCount}/{event.capacity}
</span>
)}
</div>
{/* ============= STATS ROW ============= */}
{/* Desktop: always-visible compact 4-card row */}
<div className="hidden md:block mb-4">
{showStats && (
<div className="grid grid-cols-4 gap-3">
{[
{ 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) => (
<div key={stat.label} className="flex items-center gap-2.5 bg-white rounded-card shadow-card px-3 py-2.5">
<div className={clsx('w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0', stat.color)}>
<stat.icon className="w-4 h-4" />
</div>
<div className="min-w-0">
<p className="text-lg font-bold leading-tight truncate">{stat.value}</p>
<p className="text-xs text-gray-500">{stat.label}</p>
</div>
</div>
))}
</div>
)}
</div>
{/* Mobile: collapsible stats */}
<div className="md:hidden mb-3">
{showStats && (
<div>
<button
onClick={() => setMobileStatsExpanded(!mobileStatsExpanded)}
className="w-full flex items-center justify-between px-3 py-2 bg-white rounded-card shadow-card mb-2 min-h-[44px]"
>
<div className="flex items-center gap-3 overflow-x-auto">
<span className="text-xs font-medium text-gray-500 whitespace-nowrap">Stats</span>
<span className="text-sm font-semibold whitespace-nowrap">{confirmedCount + checkedInCount}/{event.capacity}</span>
<span className="text-xs text-gray-400">|</span>
<span className="text-sm whitespace-nowrap">{checkedInCount} in</span>
<span className="text-xs text-gray-400">|</span>
<span className="text-sm whitespace-nowrap">{formatCurrency(revenue, event.currency)}</span>
</div>
<ChevronDownIcon className={clsx('w-4 h-4 text-gray-400 transition-transform flex-shrink-0 ml-2', mobileStatsExpanded && 'rotate-180')} />
</button>
{mobileStatsExpanded && (
<div className="grid grid-cols-2 gap-2">
{[
{ 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) => (
<div key={stat.label} className="flex items-center gap-2 bg-white rounded-card shadow-card px-3 py-2">
<div className={clsx('w-7 h-7 rounded-full flex items-center justify-center flex-shrink-0', stat.color)}>
<stat.icon className="w-3.5 h-3.5" />
</div>
<div className="min-w-0">
<p className="text-base font-bold leading-tight truncate">{stat.value}</p>
<p className="text-[10px] text-gray-500">{stat.label}</p>
</div>
</div>
))}
</div>
)}
</div>
)}
</div>
{/* ============= TAB BAR ============= */}
{/* Desktop: tab bar inside a card top-section */}
<div className="hidden md:block bg-white rounded-t-2xl shadow-card border-b border-gray-200">
<div ref={tabBarRef} className="sticky top-0 z-30 bg-white rounded-t-2xl px-4">
<nav className="flex gap-1">
{tabs.map((tab) => (
<button
key={tab.key}
onClick={() => setActiveTab(tab.key)}
className={clsx(
'py-2.5 px-3 font-medium text-sm transition-colors flex items-center gap-1.5 whitespace-nowrap relative',
activeTab === tab.key
? 'text-primary-dark'
: 'text-gray-500 hover:text-gray-700 hover:bg-gray-50 rounded-t-lg'
)}
>
<tab.icon className="w-4 h-4" />
{tab.label}
{tab.count !== undefined && <span className="text-xs text-gray-400">({tab.count})</span>}
{activeTab === tab.key && (
<span className="absolute bottom-0 left-0 right-0 h-0.5 bg-primary-yellow rounded-full" />
)}
</button>
))}
</nav>
</div>
</div>
{/* Mobile: segmented tab bar */}
<div className="md:hidden sticky top-0 z-30 -mx-4 px-3 py-1.5 bg-gray-100">
<div className="overflow-x-auto scrollbar-hide">
<div className="flex gap-0.5 min-w-max">
{tabs.map((tab) => (
<button
key={tab.key}
onClick={() => setActiveTab(tab.key)}
className={clsx(
'px-3 py-2 rounded-lg text-xs font-medium transition-all whitespace-nowrap min-h-[36px] flex items-center gap-1',
activeTab === tab.key
? 'bg-white shadow-sm text-primary-dark'
: 'text-gray-500 hover:text-gray-700'
)}
>
<tab.icon className="w-3.5 h-3.5" />
{tab.label}
{tab.count !== undefined && <span className="text-[10px] opacity-70">({tab.count})</span>}
</button>
))}
</div>
</div>
</div>
{/* ============= TAB CONTENT ============= */}
<div className="md:bg-white md:rounded-b-2xl md:shadow-card md:p-4 pt-3 md:pt-4">
{activeTab === 'overview' && (
<OverviewTab
event={event}
formatDate={formatDate}
fmtTime={fmtTime}
formatCurrency={formatCurrency}
confirmedCount={confirmedCount}
checkedInCount={checkedInCount}
/>
)}
{activeTab === 'attendees' && (
<AttendeesTab
locale={locale}
tickets={tickets}
filteredTickets={filteredTickets}
searchQuery={searchQuery}
setSearchQuery={setSearchQuery}
statusFilter={statusFilter}
setStatusFilter={setStatusFilter}
pendingCount={pendingCount}
confirmedCount={confirmedCount}
checkedInCount={checkedInCount}
cancelledCount={cancelledCount}
onHoldCount={onHoldCount}
exporting={exporting}
showExportDropdown={showExportDropdown}
setShowExportDropdown={setShowExportDropdown}
showAddTicketDropdown={showAddTicketDropdown}
setShowAddTicketDropdown={setShowAddTicketDropdown}
handleExportAttendees={handleExportAttendees}
setShowManualTicketModal={setShowManualTicketModal}
setShowAddAtDoorModal={setShowAddAtDoorModal}
setShowInviteGuestModal={setShowInviteGuestModal}
setMobileFilterOpen={setMobileFilterOpen}
setShowExportSheet={setShowExportSheet}
setShowAddTicketSheet={setShowAddTicketSheet}
getPrimaryAction={getPrimaryAction}
handleOpenNoteModal={handleOpenNoteModal}
handleReactivate={handleReactivate}
/>
)}
{activeTab === 'tickets' && (
<TicketsTab
locale={locale}
confirmedTickets={confirmedTickets}
filteredConfirmedTickets={filteredConfirmedTickets}
ticketSearchQuery={ticketSearchQuery}
setTicketSearchQuery={setTicketSearchQuery}
ticketStatusFilter={ticketStatusFilter}
setTicketStatusFilter={setTicketStatusFilter}
confirmedCount={confirmedCount}
checkedInCount={checkedInCount}
exporting={exporting}
showTicketExportDropdown={showTicketExportDropdown}
setShowTicketExportDropdown={setShowTicketExportDropdown}
handleExportTickets={handleExportTickets}
handleCheckin={handleCheckin}
handleRemoveCheckin={handleRemoveCheckin}
setShowTicketExportSheet={setShowTicketExportSheet}
/>
)}
{activeTab === 'email' && (
<EmailTab
templates={templates}
selectedTemplate={selectedTemplate}
setSelectedTemplate={setSelectedTemplate}
recipientFilter={recipientFilter}
setRecipientFilter={setRecipientFilter}
customMessage={customMessage}
setCustomMessage={setCustomMessage}
sending={sending}
handlePreviewEmail={handlePreviewEmail}
handleSendEmail={handleSendEmail}
getFilteredRecipientCount={getFilteredRecipientCount}
ticketsCount={tickets.length}
confirmedCount={confirmedCount}
pendingCount={pendingCount}
checkedInCount={checkedInCount}
cancelledCount={cancelledCount}
/>
)}
{activeTab === 'payments' && (
<PaymentsTab locale={locale} payments={payments} />
)}
</div>
{/* ============= MODALS ============= */}
<EventModals
ticketsCount={tickets.length}
pendingCount={pendingCount}
confirmedCount={confirmedCount}
checkedInCount={checkedInCount}
cancelledCount={cancelledCount}
onHoldCount={onHoldCount}
statusFilter={statusFilter}
setStatusFilter={setStatusFilter}
mobileFilterOpen={mobileFilterOpen}
setMobileFilterOpen={setMobileFilterOpen}
showAddTicketSheet={showAddTicketSheet}
setShowAddTicketSheet={setShowAddTicketSheet}
showExportSheet={showExportSheet}
setShowExportSheet={setShowExportSheet}
handleExportAttendees={handleExportAttendees}
showTicketExportSheet={showTicketExportSheet}
setShowTicketExportSheet={setShowTicketExportSheet}
handleExportTickets={handleExportTickets}
showAddAtDoorModal={showAddAtDoorModal}
setShowAddAtDoorModal={setShowAddAtDoorModal}
addAtDoorForm={addAtDoorForm}
setAddAtDoorForm={setAddAtDoorForm}
handleAddAtDoor={handleAddAtDoor}
showManualTicketModal={showManualTicketModal}
setShowManualTicketModal={setShowManualTicketModal}
manualTicketForm={manualTicketForm}
setManualTicketForm={setManualTicketForm}
handleManualTicket={handleManualTicket}
showInviteGuestModal={showInviteGuestModal}
setShowInviteGuestModal={setShowInviteGuestModal}
inviteGuestForm={inviteGuestForm}
setInviteGuestForm={setInviteGuestForm}
handleInviteGuest={handleInviteGuest}
submitting={submitting}
showNoteModal={showNoteModal}
setShowNoteModal={setShowNoteModal}
selectedTicket={selectedTicket}
setSelectedTicket={setSelectedTicket}
noteText={noteText}
setNoteText={setNoteText}
handleSaveNote={handleSaveNote}
previewHtml={previewHtml}
setPreviewHtml={setPreviewHtml}
/>
<EventFormModal
open={showEditForm}
event={event}
featuredEventId={featuredEventId}
onFeaturedChange={setFeaturedEventId}
onClose={() => setShowEditForm(false)}
onSaved={() => { setShowEditForm(false); loadEventData(); }}
/>
<AdminMobileStyles />
</div>
);
}