141 lines
5.0 KiB
JavaScript
141 lines
5.0 KiB
JavaScript
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,
|
|
}; |