Initial commit
Next.js + Express event management app for Hope Family Church.
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
const prisma = require('../config/db');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const { safeErrorMessage } = require('../utils/errorUtils');
|
||||
const { ALL_METHODS, assertEventOpen, computeEventFinancials } = require('../utils/cashupUtils');
|
||||
|
||||
// @desc Cashup preview for an event: live expected/actual numbers, costs, donations-to-profit, and history
|
||||
// @route GET /api/cashups/event/:eventId
|
||||
// @access Private/Supervisor
|
||||
const getEventCashup = async (req, res) => {
|
||||
try {
|
||||
const financials = await computeEventFinancials(req.params.eventId);
|
||||
res.json(financials);
|
||||
} catch (error) {
|
||||
res.status(res.statusCode === 200 ? 400 : res.statusCode).json({ message: safeErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
// @desc Save in-progress reconciliation entries without closing the event
|
||||
// @route PUT /api/cashups/event/:eventId/draft
|
||||
// @access Private/Admin
|
||||
const saveEventCashupDraft = async (req, res) => {
|
||||
try {
|
||||
const { eventId } = req.params;
|
||||
const { lines, notes } = req.body;
|
||||
|
||||
await assertEventOpen(eventId, res);
|
||||
|
||||
await prisma.event.update({
|
||||
where: { id: eventId },
|
||||
data: { cashupDraft: { lines: Array.isArray(lines) ? lines : [], notes: notes || null, savedAt: new Date().toISOString() } }
|
||||
});
|
||||
|
||||
res.json({ message: 'Draft saved' });
|
||||
} catch (error) {
|
||||
res.status(res.statusCode === 200 ? 400 : res.statusCode).json({ message: safeErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
// @desc Close an event, optionally with a full per-method cashup
|
||||
// @route POST /api/cashups/event/:eventId/close
|
||||
// @access Private/Admin
|
||||
const closeEvent = async (req, res) => {
|
||||
try {
|
||||
const { eventId } = req.params;
|
||||
const { lines, notes } = req.body;
|
||||
|
||||
await assertEventOpen(eventId, res);
|
||||
|
||||
const financials = await computeEventFinancials(eventId);
|
||||
const isFullCashup = Array.isArray(lines) && lines.length > 0;
|
||||
|
||||
const cashupLines = isFullCashup
|
||||
? lines
|
||||
.filter(l => l && ALL_METHODS.includes(l.method))
|
||||
.map(l => {
|
||||
const expected = financials.expectedCashByMethod[l.method] || 0;
|
||||
const denominations = l.method === 'cash' && Array.isArray(l.denominations)
|
||||
? l.denominations
|
||||
.map(d => ({ value: parseFloat(d.value), count: parseInt(d.count, 10) || 0 }))
|
||||
.filter(d => d.value > 0 && d.count > 0)
|
||||
: [];
|
||||
const actual = denominations.length > 0
|
||||
? denominations.reduce((sum, d) => sum + d.value * d.count, 0)
|
||||
: (l.actualAmount !== undefined && l.actualAmount !== null && l.actualAmount !== '' ? parseFloat(l.actualAmount) : null);
|
||||
return {
|
||||
id: uuidv4(),
|
||||
method: l.method,
|
||||
expectedAmount: expected,
|
||||
actualAmount: actual,
|
||||
variance: actual !== null ? actual - expected : null,
|
||||
notes: l.notes || null,
|
||||
denominations: denominations.length > 0 ? { create: denominations } : undefined
|
||||
};
|
||||
})
|
||||
: [];
|
||||
|
||||
const totalActualRevenue = isFullCashup
|
||||
? cashupLines.reduce((sum, l) => sum + (l.actualAmount !== null ? l.actualAmount : 0), 0)
|
||||
: null;
|
||||
const totalExpectedRevenue = ALL_METHODS.reduce((sum, m) => sum + financials.expectedCashByMethod[m], 0);
|
||||
|
||||
const cashup = await prisma.eventCashup.create({
|
||||
data: {
|
||||
id: uuidv4(),
|
||||
eventId,
|
||||
action: isFullCashup ? 'closed' : 'quick_closed',
|
||||
unallocatedDonationsTotal: financials.unallocatedDonationsTotal,
|
||||
totalCosts: financials.totalCosts,
|
||||
totalExpectedRevenue,
|
||||
totalActualRevenue,
|
||||
notes: notes || null,
|
||||
performedById: req.user.id,
|
||||
lines: { create: cashupLines }
|
||||
},
|
||||
include: { lines: { include: { denominations: true } }, performedBy: { select: { id: true, name: true, email: true } } }
|
||||
});
|
||||
|
||||
await prisma.event.update({
|
||||
where: { id: eventId },
|
||||
data: { cashupStatus: 'closed', cashupDraft: null, closedAt: new Date(), closedById: req.user.id }
|
||||
});
|
||||
|
||||
res.status(201).json(cashup);
|
||||
} catch (error) {
|
||||
res.status(res.statusCode === 200 ? 400 : res.statusCode).json({ message: safeErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
// @desc Reopen a closed event (admin only)
|
||||
// @route POST /api/cashups/event/:eventId/reopen
|
||||
// @access Private/Admin
|
||||
const reopenEvent = async (req, res) => {
|
||||
try {
|
||||
const { eventId } = req.params;
|
||||
const { notes } = req.body;
|
||||
|
||||
const event = await prisma.event.findUnique({ where: { id: eventId } });
|
||||
if (!event) {
|
||||
res.status(404);
|
||||
throw new Error('Event not found');
|
||||
}
|
||||
if (event.cashupStatus !== 'closed') {
|
||||
res.status(400);
|
||||
throw new Error('Event is not closed');
|
||||
}
|
||||
|
||||
const auditRow = await prisma.eventCashup.create({
|
||||
data: {
|
||||
id: uuidv4(),
|
||||
eventId,
|
||||
action: 'reopened',
|
||||
notes: notes || null,
|
||||
performedById: req.user.id
|
||||
},
|
||||
include: { performedBy: { select: { id: true, name: true, email: true } } }
|
||||
});
|
||||
|
||||
await prisma.event.update({
|
||||
where: { id: eventId },
|
||||
data: { cashupStatus: 'open', reopenedAt: new Date(), reopenedById: req.user.id }
|
||||
});
|
||||
|
||||
res.status(201).json(auditRow);
|
||||
} catch (error) {
|
||||
res.status(res.statusCode === 200 ? 400 : res.statusCode).json({ message: safeErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
// @desc Flat audit log of every close/quick-close/reopen, optionally filtered
|
||||
// @route GET /api/cashups/audit
|
||||
// @access Private/Supervisor
|
||||
const getCashupAudit = async (req, res) => {
|
||||
try {
|
||||
const { eventId, from, to } = req.query;
|
||||
|
||||
const where = {};
|
||||
if (eventId) where.eventId = eventId;
|
||||
if (from || to) {
|
||||
where.createdAt = {};
|
||||
if (from) where.createdAt.gte = new Date(from);
|
||||
if (to) where.createdAt.lte = new Date(to);
|
||||
}
|
||||
|
||||
const rows = await prisma.eventCashup.findMany({
|
||||
where,
|
||||
include: {
|
||||
event: { select: { id: true, title: true } },
|
||||
performedBy: { select: { id: true, name: true, email: true } },
|
||||
lines: { include: { denominations: true } }
|
||||
},
|
||||
orderBy: { createdAt: 'desc' }
|
||||
});
|
||||
|
||||
res.json(rows);
|
||||
} catch (error) {
|
||||
res.status(res.statusCode === 200 ? 400 : res.statusCode).json({ message: safeErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getEventCashup,
|
||||
saveEventCashupDraft,
|
||||
closeEvent,
|
||||
reopenEvent,
|
||||
getCashupAudit
|
||||
};
|
||||
Reference in New Issue
Block a user