first commit
This commit is contained in:
284
backend/src/routes/admin.ts
Normal file
284
backend/src/routes/admin.ts
Normal file
@@ -0,0 +1,284 @@
|
||||
import { Hono } from 'hono';
|
||||
import { db, users, events, tickets, payments, contacts, emailSubscribers } from '../db/index.js';
|
||||
import { eq, and, gte, sql, desc } from 'drizzle-orm';
|
||||
import { requireAuth } from '../lib/auth.js';
|
||||
import { getNow } from '../lib/utils.js';
|
||||
|
||||
const adminRouter = new Hono();
|
||||
|
||||
// Dashboard overview stats (admin)
|
||||
adminRouter.get('/dashboard', requireAuth(['admin', 'organizer']), async (c) => {
|
||||
const now = getNow();
|
||||
|
||||
// Get upcoming events
|
||||
const upcomingEvents = await (db as any)
|
||||
.select()
|
||||
.from(events)
|
||||
.where(
|
||||
and(
|
||||
eq((events as any).status, 'published'),
|
||||
gte((events as any).startDatetime, now)
|
||||
)
|
||||
)
|
||||
.orderBy((events as any).startDatetime)
|
||||
.limit(5)
|
||||
.all();
|
||||
|
||||
// Get recent tickets
|
||||
const recentTickets = await (db as any)
|
||||
.select()
|
||||
.from(tickets)
|
||||
.orderBy(desc((tickets as any).createdAt))
|
||||
.limit(10)
|
||||
.all();
|
||||
|
||||
// Get total stats
|
||||
const totalUsers = await (db as any)
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(users)
|
||||
.get();
|
||||
|
||||
const totalEvents = await (db as any)
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(events)
|
||||
.get();
|
||||
|
||||
const totalTickets = await (db as any)
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(tickets)
|
||||
.get();
|
||||
|
||||
const confirmedTickets = await (db as any)
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(tickets)
|
||||
.where(eq((tickets as any).status, 'confirmed'))
|
||||
.get();
|
||||
|
||||
const pendingPayments = await (db as any)
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(payments)
|
||||
.where(eq((payments as any).status, 'pending'))
|
||||
.get();
|
||||
|
||||
const paidPayments = await (db as any)
|
||||
.select()
|
||||
.from(payments)
|
||||
.where(eq((payments as any).status, 'paid'))
|
||||
.all();
|
||||
|
||||
const totalRevenue = paidPayments.reduce((sum: number, p: any) => sum + (p.amount || 0), 0);
|
||||
|
||||
const newContacts = await (db as any)
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(contacts)
|
||||
.where(eq((contacts as any).status, 'new'))
|
||||
.get();
|
||||
|
||||
const totalSubscribers = await (db as any)
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(emailSubscribers)
|
||||
.where(eq((emailSubscribers as any).status, 'active'))
|
||||
.get();
|
||||
|
||||
return c.json({
|
||||
dashboard: {
|
||||
stats: {
|
||||
totalUsers: totalUsers?.count || 0,
|
||||
totalEvents: totalEvents?.count || 0,
|
||||
totalTickets: totalTickets?.count || 0,
|
||||
confirmedTickets: confirmedTickets?.count || 0,
|
||||
pendingPayments: pendingPayments?.count || 0,
|
||||
totalRevenue,
|
||||
newContacts: newContacts?.count || 0,
|
||||
totalSubscribers: totalSubscribers?.count || 0,
|
||||
},
|
||||
upcomingEvents,
|
||||
recentTickets,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// Get analytics data (admin)
|
||||
adminRouter.get('/analytics', requireAuth(['admin']), async (c) => {
|
||||
// Get events with ticket counts
|
||||
const allEvents = await (db as any).select().from(events).all();
|
||||
|
||||
const eventStats = await Promise.all(
|
||||
allEvents.map(async (event: any) => {
|
||||
const ticketCount = await (db as any)
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(tickets)
|
||||
.where(eq((tickets as any).eventId, event.id))
|
||||
.get();
|
||||
|
||||
const confirmedCount = await (db as any)
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(tickets)
|
||||
.where(
|
||||
and(
|
||||
eq((tickets as any).eventId, event.id),
|
||||
eq((tickets as any).status, 'confirmed')
|
||||
)
|
||||
)
|
||||
.get();
|
||||
|
||||
const checkedInCount = await (db as any)
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(tickets)
|
||||
.where(
|
||||
and(
|
||||
eq((tickets as any).eventId, event.id),
|
||||
eq((tickets as any).status, 'checked_in')
|
||||
)
|
||||
)
|
||||
.get();
|
||||
|
||||
return {
|
||||
id: event.id,
|
||||
title: event.title,
|
||||
date: event.startDatetime,
|
||||
capacity: event.capacity,
|
||||
totalBookings: ticketCount?.count || 0,
|
||||
confirmedBookings: confirmedCount?.count || 0,
|
||||
checkedIn: checkedInCount?.count || 0,
|
||||
revenue: (confirmedCount?.count || 0) * event.price,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return c.json({
|
||||
analytics: {
|
||||
events: eventStats,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// Export data (admin)
|
||||
adminRouter.get('/export/tickets', requireAuth(['admin']), async (c) => {
|
||||
const eventId = c.req.query('eventId');
|
||||
|
||||
let query = (db as any).select().from(tickets);
|
||||
|
||||
if (eventId) {
|
||||
query = query.where(eq((tickets as any).eventId, eventId));
|
||||
}
|
||||
|
||||
const ticketList = await query.all();
|
||||
|
||||
// Get user and event details for each ticket
|
||||
const enrichedTickets = await Promise.all(
|
||||
ticketList.map(async (ticket: any) => {
|
||||
const user = await (db as any)
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq((users as any).id, ticket.userId))
|
||||
.get();
|
||||
|
||||
const event = await (db as any)
|
||||
.select()
|
||||
.from(events)
|
||||
.where(eq((events as any).id, ticket.eventId))
|
||||
.get();
|
||||
|
||||
const payment = await (db as any)
|
||||
.select()
|
||||
.from(payments)
|
||||
.where(eq((payments as any).ticketId, ticket.id))
|
||||
.get();
|
||||
|
||||
return {
|
||||
ticketId: ticket.id,
|
||||
ticketStatus: ticket.status,
|
||||
qrCode: ticket.qrCode,
|
||||
checkinAt: ticket.checkinAt,
|
||||
userName: user?.name,
|
||||
userEmail: user?.email,
|
||||
userPhone: user?.phone,
|
||||
eventTitle: event?.title,
|
||||
eventDate: event?.startDatetime,
|
||||
paymentStatus: payment?.status,
|
||||
paymentAmount: payment?.amount,
|
||||
createdAt: ticket.createdAt,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return c.json({ tickets: enrichedTickets });
|
||||
});
|
||||
|
||||
// Export financial data (admin)
|
||||
adminRouter.get('/export/financial', requireAuth(['admin']), async (c) => {
|
||||
const startDate = c.req.query('startDate');
|
||||
const endDate = c.req.query('endDate');
|
||||
const eventId = c.req.query('eventId');
|
||||
|
||||
// Get all payments
|
||||
let query = (db as any).select().from(payments);
|
||||
|
||||
const allPayments = await query.all();
|
||||
|
||||
// Enrich with event and ticket data
|
||||
const enrichedPayments = await Promise.all(
|
||||
allPayments.map(async (payment: any) => {
|
||||
const ticket = await (db as any)
|
||||
.select()
|
||||
.from(tickets)
|
||||
.where(eq((tickets as any).id, payment.ticketId))
|
||||
.get();
|
||||
|
||||
if (!ticket) return null;
|
||||
|
||||
const event = await (db as any)
|
||||
.select()
|
||||
.from(events)
|
||||
.where(eq((events as any).id, ticket.eventId))
|
||||
.get();
|
||||
|
||||
// Apply filters
|
||||
if (eventId && ticket.eventId !== eventId) return null;
|
||||
if (startDate && payment.createdAt < startDate) return null;
|
||||
if (endDate && payment.createdAt > endDate) return null;
|
||||
|
||||
return {
|
||||
paymentId: payment.id,
|
||||
amount: payment.amount,
|
||||
currency: payment.currency,
|
||||
provider: payment.provider,
|
||||
status: payment.status,
|
||||
reference: payment.reference,
|
||||
paidAt: payment.paidAt,
|
||||
createdAt: payment.createdAt,
|
||||
ticketId: ticket.id,
|
||||
attendeeFirstName: ticket.attendeeFirstName,
|
||||
attendeeLastName: ticket.attendeeLastName,
|
||||
attendeeEmail: ticket.attendeeEmail,
|
||||
eventId: event?.id,
|
||||
eventTitle: event?.title,
|
||||
eventDate: event?.startDatetime,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
const filteredPayments = enrichedPayments.filter(p => p !== null);
|
||||
|
||||
// Calculate summary
|
||||
const summary = {
|
||||
totalPayments: filteredPayments.length,
|
||||
totalPaid: filteredPayments.filter((p: any) => p.status === 'paid').reduce((sum: number, p: any) => sum + p.amount, 0),
|
||||
totalPending: filteredPayments.filter((p: any) => p.status === 'pending').reduce((sum: number, p: any) => sum + p.amount, 0),
|
||||
totalRefunded: filteredPayments.filter((p: any) => p.status === 'refunded').reduce((sum: number, p: any) => sum + p.amount, 0),
|
||||
byProvider: {
|
||||
bancard: filteredPayments.filter((p: any) => p.provider === 'bancard' && p.status === 'paid').reduce((sum: number, p: any) => sum + p.amount, 0),
|
||||
lightning: filteredPayments.filter((p: any) => p.provider === 'lightning' && p.status === 'paid').reduce((sum: number, p: any) => sum + p.amount, 0),
|
||||
cash: filteredPayments.filter((p: any) => p.provider === 'cash' && p.status === 'paid').reduce((sum: number, p: any) => sum + p.amount, 0),
|
||||
},
|
||||
paidCount: filteredPayments.filter((p: any) => p.status === 'paid').length,
|
||||
pendingCount: filteredPayments.filter((p: any) => p.status === 'pending').length,
|
||||
refundedCount: filteredPayments.filter((p: any) => p.status === 'refunded').length,
|
||||
failedCount: filteredPayments.filter((p: any) => p.status === 'failed').length,
|
||||
};
|
||||
|
||||
return c.json({ payments: filteredPayments, summary });
|
||||
});
|
||||
|
||||
export default adminRouter;
|
||||
Reference in New Issue
Block a user