feat(admin): add event attendees export (CSV) with status filters

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Michilis
2026-02-14 05:27:17 +00:00
parent 62bf048680
commit c3897efd02
3 changed files with 220 additions and 3 deletions

View File

@@ -1,10 +1,10 @@
'use client';
import { useState, useEffect } from 'react';
import { useState, useEffect, useRef, useCallback } from 'react';
import { useParams, useRouter } from 'next/navigation';
import Link from 'next/link';
import { useLanguage } from '@/context/LanguageContext';
import { eventsApi, ticketsApi, emailsApi, paymentOptionsApi, Event, Ticket, EmailTemplate, PaymentOptionsConfig } from '@/lib/api';
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 {
@@ -34,6 +34,8 @@ import {
BoltIcon,
BuildingLibraryIcon,
ArrowPathIcon,
ArrowDownTrayIcon,
ChevronDownIcon,
} from '@heroicons/react/24/outline';
import toast from 'react-hot-toast';
import clsx from 'clsx';
@@ -85,6 +87,23 @@ export default function AdminEventDetailPage() {
});
const [submitting, setSubmitting] = useState(false);
// Export state
const [showExportDropdown, setShowExportDropdown] = useState(false);
const [exporting, setExporting] = useState(false);
const exportBtnRef = useRef<HTMLButtonElement>(null);
const [exportDropdownPos, setExportDropdownPos] = useState({ top: 0, right: 0 });
const toggleExportDropdown = useCallback(() => {
if (!showExportDropdown && exportBtnRef.current) {
const rect = exportBtnRef.current.getBoundingClientRect();
setExportDropdownPos({
top: rect.bottom + 4,
right: window.innerWidth - rect.right,
});
}
setShowExportDropdown((v) => !v);
}, [showExportDropdown]);
// Tickets tab state
const [ticketSearchQuery, setTicketSearchQuery] = useState('');
const [ticketStatusFilter, setTicketStatusFilter] = useState<'all' | 'confirmed' | 'checked_in'>('all');
@@ -341,6 +360,28 @@ export default function AdminEventDetailPage() {
}
};
const handleExport = 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' });
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);
}
};
// Filtered tickets for attendees tab
const filteredTickets = tickets.filter((ticket) => {
// Status filter
@@ -710,6 +751,21 @@ export default function AdminEventDetailPage() {
</div>
{/* Action Buttons */}
<div className="flex items-center gap-2">
{/* Export Button */}
<Button
ref={exportBtnRef}
variant="outline"
onClick={toggleExportDropdown}
disabled={exporting}
>
{exporting ? (
<div className="w-4 h-4 mr-2 border-2 border-gray-400 border-t-transparent rounded-full animate-spin" />
) : (
<ArrowDownTrayIcon className="w-4 h-4 mr-2" />
)}
Export
<ChevronDownIcon className="w-3 h-3 ml-1" />
</Button>
<Button variant="outline" onClick={() => setShowManualTicketModal(true)}>
<EnvelopeIcon className="w-4 h-4 mr-2" />
Manual Ticket
@@ -988,6 +1044,49 @@ export default function AdminEventDetailPage() {
</div>
)}
{/* Export Dropdown (rendered outside Card to avoid overflow:hidden clipping) */}
{showExportDropdown && (
<>
<div
className="fixed inset-0 z-40"
onClick={() => setShowExportDropdown(false)}
/>
<div
className="fixed w-48 bg-white border border-secondary-light-gray rounded-btn shadow-lg z-50"
style={{ top: exportDropdownPos.top, right: exportDropdownPos.right }}
>
<button
onClick={() => handleExport('confirmed')}
className="w-full text-left px-4 py-2 text-sm hover:bg-gray-50 rounded-t-btn"
>
Export Confirmed
</button>
<button
onClick={() => handleExport('checked_in')}
className="w-full text-left px-4 py-2 text-sm hover:bg-gray-50"
>
Export Checked-in
</button>
<button
onClick={() => handleExport('confirmed_pending')}
className="w-full text-left px-4 py-2 text-sm hover:bg-gray-50"
>
Export Confirmed &amp; Pending
</button>
<button
onClick={() => handleExport('all')}
className="w-full text-left px-4 py-2 text-sm hover:bg-gray-50"
>
Export All
</button>
<div className="border-t border-secondary-light-gray" />
<div className="px-4 py-2 text-xs text-gray-400 rounded-b-btn">
Format: CSV
</div>
</div>
</>
)}
{/* Add at Door Modal */}
{showAddAtDoorModal && (
<div