const prisma = require('../config/db'); const { safeErrorMessage } = require('../utils/errorUtils'); // Shared building blocks for the per-dashboard stats endpoints below. Each dashboard // (staff/supervisor/admin) gets exactly one endpoint that returns only what it renders, // computed with aggregate queries — never a full payments/events list shipped to the // client just to be reduced down to a couple of numbers. async function computeScanStats(userId) { const startOfDay = new Date(); startOfDay.setHours(0, 0, 0, 0); const whereBase = { scannedAt: { gte: startOfDay } }; const [totalToday, myToday, lastHour, byStaffRaw] = await Promise.all([ prisma.ticketUsage.count({ where: whereBase }), prisma.ticketUsage.count({ where: { ...whereBase, scannedById: userId } }), prisma.ticketUsage.count({ where: { scannedAt: { gte: new Date(Date.now() - 60 * 60 * 1000) } } }), prisma.ticketUsage.groupBy({ by: ['scannedById'], where: whereBase, _count: { _all: true } }), ]); const staffIds = byStaffRaw.map((b) => b.scannedById); const staffUsers = staffIds.length > 0 ? await prisma.user.findMany({ where: { id: { in: staffIds } }, select: { id: true, name: true } }) : []; const nameMap = Object.fromEntries(staffUsers.map((u) => [u.id, u.name])); return { totalToday, myToday, lastHour, byStaff: byStaffRaw.map((b) => ({ scannedById: b.scannedById, name: nameMap[b.scannedById] || 'Staff', count: b._count._all })), }; } function getRecentScans(limit = 10) { return prisma.ticketUsage.findMany({ orderBy: { scannedAt: 'desc' }, take: limit, select: { id: true, scannedAt: true, quantityRedeemed: true, scannedBy: { select: { id: true, name: true } }, ticket: { select: { id: true, event: { select: { title: true } }, registrationOption: { select: { eventOption: { select: { name: true } } } }, }, }, }, }); } function getActiveEventsCount() { return prisma.event.count({ where: { isActive: true, endDate: { gte: new Date() } } }); } async function computePaymentStats({ includeWeekMonth }) { const startOfDay = new Date(new Date().setHours(0, 0, 0, 0)); const queries = [ prisma.payment.aggregate({ _sum: { amount: true }, where: { createdAt: { gte: startOfDay } } }), prisma.payment.count({ where: { isDonation: true, createdAt: { gte: startOfDay } } }), ]; if (includeWeekMonth) { const lastWeek = new Date(startOfDay.getTime() - 7 * 24 * 60 * 60 * 1000); const lastMonth = new Date(startOfDay.getTime() - 30 * 24 * 60 * 60 * 1000); queries.push( prisma.payment.aggregate({ _sum: { amount: true }, where: { createdAt: { gte: lastWeek } } }), prisma.payment.aggregate({ _sum: { amount: true }, where: { createdAt: { gte: lastMonth } } }), ); } const [totalToday, donationsToday, totalWeek, totalMonth] = await Promise.all(queries); const stats = { totalToday: totalToday._sum.amount || 0, donationsToday, }; if (includeWeekMonth) { stats.totalWeek = totalWeek._sum.amount || 0; stats.totalMonth = totalMonth._sum.amount || 0; } return stats; } // @desc All stats the staff dashboard needs, in one call // @route GET /api/stats/staff // @access Private/Staff+ const getStaffDashboardStats = async (req, res) => { try { const [scanStats, recentScans] = await Promise.all([ computeScanStats(req.user.id), getRecentScans(10), ]); res.json({ scanStats, recentScans }); } catch (error) { res.status(res.statusCode === 200 ? 400 : res.statusCode).json({ message: safeErrorMessage(error) }); } }; // @desc All stats the supervisor dashboard needs, in one call // @route GET /api/stats/supervisor // @access Private/Supervisor+ const getSupervisorDashboardStats = async (req, res) => { try { const [scanStats, recentScans, paymentStats, activeEventsCount] = await Promise.all([ computeScanStats(req.user.id), getRecentScans(10), computePaymentStats({ includeWeekMonth: false }), getActiveEventsCount(), ]); res.json({ scanStats, recentScans, paymentStats, activeEventsCount }); } catch (error) { res.status(res.statusCode === 200 ? 400 : res.statusCode).json({ message: safeErrorMessage(error) }); } }; // @desc All stats the admin dashboard needs, in one call // @route GET /api/stats/admin // @access Private/Admin const getAdminDashboardStats = async (req, res) => { try { const [scanStats, recentScans, paymentStats, activeEventsCount] = await Promise.all([ computeScanStats(req.user.id), getRecentScans(10), computePaymentStats({ includeWeekMonth: true }), getActiveEventsCount(), ]); res.json({ scanStats, recentScans, paymentStats, activeEventsCount }); } catch (error) { res.status(res.statusCode === 200 ? 400 : res.statusCode).json({ message: safeErrorMessage(error) }); } }; module.exports = { getStaffDashboardStats, getSupervisorDashboardStats, getAdminDashboardStats, };