feat(admin): add event attendees export (CSV) with status filters
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { Hono } from 'hono';
|
||||
import { db, dbGet, dbAll, users, events, tickets, payments, contacts, emailSubscribers } from '../db/index.js';
|
||||
import { eq, and, gte, sql, desc } from 'drizzle-orm';
|
||||
import { eq, and, gte, sql, desc, inArray } from 'drizzle-orm';
|
||||
import { requireAuth } from '../lib/auth.js';
|
||||
import { getNow } from '../lib/utils.js';
|
||||
|
||||
@@ -222,6 +222,103 @@ adminRouter.get('/export/tickets', requireAuth(['admin']), async (c) => {
|
||||
return c.json({ tickets: enrichedTickets });
|
||||
});
|
||||
|
||||
// Export attendees for a specific event (admin) — CSV/XLSX download
|
||||
adminRouter.get('/events/:eventId/export', requireAuth(['admin']), async (c) => {
|
||||
const eventId = c.req.param('eventId');
|
||||
const status = c.req.query('status') || 'all'; // confirmed | checked_in | confirmed_pending | all
|
||||
const format = c.req.query('format') || 'csv'; // csv | xlsx
|
||||
|
||||
// Verify event exists
|
||||
const event = await dbGet<any>(
|
||||
(db as any).select().from(events).where(eq((events as any).id, eventId))
|
||||
);
|
||||
if (!event) {
|
||||
return c.json({ error: 'Event not found' }, 404);
|
||||
}
|
||||
|
||||
// Build query for tickets belonging to this event
|
||||
let conditions: any[] = [eq((tickets as any).eventId, eventId)];
|
||||
|
||||
if (status === 'confirmed') {
|
||||
conditions.push(eq((tickets as any).status, 'confirmed'));
|
||||
} else if (status === 'checked_in') {
|
||||
conditions.push(eq((tickets as any).status, 'checked_in'));
|
||||
} else if (status === 'confirmed_pending') {
|
||||
conditions.push(inArray((tickets as any).status, ['confirmed', 'pending']));
|
||||
} else {
|
||||
// "all" — include everything
|
||||
}
|
||||
|
||||
const ticketList = await dbAll<any>(
|
||||
(db as any)
|
||||
.select()
|
||||
.from(tickets)
|
||||
.where(conditions.length === 1 ? conditions[0] : and(...conditions))
|
||||
.orderBy((tickets as any).createdAt)
|
||||
);
|
||||
|
||||
// Enrich each ticket with payment data
|
||||
const rows = await Promise.all(
|
||||
ticketList.map(async (ticket: any) => {
|
||||
const payment = await dbGet<any>(
|
||||
(db as any)
|
||||
.select()
|
||||
.from(payments)
|
||||
.where(eq((payments as any).ticketId, ticket.id))
|
||||
);
|
||||
|
||||
const fullName = [ticket.attendeeFirstName, ticket.attendeeLastName].filter(Boolean).join(' ');
|
||||
const isCheckedIn = ticket.status === 'checked_in';
|
||||
|
||||
return {
|
||||
'Ticket ID': ticket.id,
|
||||
'Full Name': fullName,
|
||||
'Email': ticket.attendeeEmail || '',
|
||||
'Status': ticket.status,
|
||||
'Checked In': isCheckedIn ? 'true' : 'false',
|
||||
'Check-in Time': ticket.checkinAt || '',
|
||||
'Payment Status': payment?.status || '',
|
||||
'Notes': ticket.adminNote || '',
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
// Generate CSV
|
||||
const csvEscape = (value: string) => {
|
||||
if (value == null) return '';
|
||||
const str = String(value);
|
||||
if (str.includes(',') || str.includes('"') || str.includes('\n') || str.includes('\r')) {
|
||||
return '"' + str.replace(/"/g, '""') + '"';
|
||||
}
|
||||
return str;
|
||||
};
|
||||
|
||||
const columns = [
|
||||
'Ticket ID', 'Full Name', 'Email',
|
||||
'Status', 'Checked In', 'Check-in Time', 'Payment Status',
|
||||
'Notes',
|
||||
];
|
||||
|
||||
const headerLine = columns.map(csvEscape).join(',');
|
||||
const dataLines = rows.map((row: any) =>
|
||||
columns.map((col) => csvEscape(row[col])).join(',')
|
||||
);
|
||||
|
||||
const csvContent = '\uFEFF' + [headerLine, ...dataLines].join('\r\n'); // BOM for UTF-8
|
||||
|
||||
// Build filename: event-slug-attendees-YYYY-MM-DD.csv
|
||||
const slug = (event.title || 'event')
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/(^-|-$)/g, '');
|
||||
const dateStr = new Date().toISOString().split('T')[0];
|
||||
const filename = `${slug}-attendees-${dateStr}.csv`;
|
||||
|
||||
c.header('Content-Type', 'text/csv; charset=utf-8');
|
||||
c.header('Content-Disposition', `attachment; filename="${filename}"`);
|
||||
return c.body(csvContent);
|
||||
});
|
||||
|
||||
// Export financial data (admin)
|
||||
adminRouter.get('/export/financial', requireAuth(['admin']), async (c) => {
|
||||
const startDate = c.req.query('startDate');
|
||||
|
||||
Reference in New Issue
Block a user