Files
hope-events/backend/src/controllers/statsController.js
T
joshua 3d381944d2 Initial commit
Next.js + Express event management app for Hope Family Church.
2026-07-23 15:26:47 +02:00

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,
};