Fix financial double-counting, rebuild cashup accountability, and redesign the Reports page
Financial correctness (donation-leg model):
- Donations are no longer mutated when assigned to a registration; assignment now
creates an immutable "leg" record referencing the original donation instead.
- Fixed several places where money was double-counted once a donation was partially
or fully assigned (Payments, Revenue summary, Cashup reconciliation, Finance
report, Profit report, Master Orders, Revenue Detailed).
- Payments now record who recorded them (recordedBy), separate from who they're for.
Cashup:
- Per-user cash denomination counting (optional, any time) replaces the single
event-wide manual entry; the event's cash actual is the live sum of these counts.
- New "Payment accountability by staff member" breakdown across all methods, and a
read-only "Report" tab that opens automatically once an event is closed.
Reports page redesign:
- New shell: sidebar of universal filters (events, date range, past/inactive/closed
toggles), searchable/categorized report grid, and a popup viewer with
Print/Email/Excel/WhatsApp actions plus an in-app Reporting Guide.
- Visual pass: colored stat tiles and bar charts on most reports, matching mockups.
- PDF exports (download/Print/Email/WhatsApp) now share a branded design mirroring
the web report — colored header, stat tiles, bar chart, highlighted totals.
- Excel export now produces a styled .xlsx (via exceljs) instead of a plain CSV.
- Master Orders' "Donations made" table is now included in every export channel.
Bug fixes discovered while testing exports:
- Report emails now go through the shared, DB-configurable mail utility instead of
a one-off transporter that ignored Site Settings SMTP config.
- WhatsApp report sends now surface the actual WAWP API error and auto-recover a
disconnected session, instead of a bare axios status-code message.
Also: Admin-editable notification preference, richer Admin Registrations dashboard,
{{payment.link}} placeholder for Email/WhatsApp Attendees, and background
email/WhatsApp attendee sending.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -105,6 +105,12 @@ function isSessionNotFound(e) {
|
||||
return msg.includes('session not found') || msg.includes('instance not found');
|
||||
}
|
||||
|
||||
/** Extracts the WAWP API's own error reason when present, instead of axios's generic
|
||||
* "Request failed with status code NNN" (which carries no information about what went wrong). */
|
||||
function wawpErrorMessage(e) {
|
||||
return e?.response?.data?.message || e?.message || 'Unknown WhatsApp API error';
|
||||
}
|
||||
|
||||
/**
|
||||
* If the WAWP API reports "Session not found", clear the stale instance ID
|
||||
* from the DB so the admin UI drops back to the Session Instance setup step.
|
||||
@@ -221,12 +227,20 @@ async function sendText(toPhone, message) {
|
||||
const chatId = toChatId(toPhone);
|
||||
if (!chatId) { console.warn('[whatsapp] Invalid phone, skipping text:', toPhone); return; }
|
||||
const { token, instanceId } = await getConfig();
|
||||
await axios.post(`${BASE}/send/text`, {
|
||||
access_token: token,
|
||||
instance_id: instanceId,
|
||||
chatId,
|
||||
message,
|
||||
});
|
||||
try {
|
||||
await axios.post(`${BASE}/send/text`, {
|
||||
access_token: token,
|
||||
instance_id: instanceId,
|
||||
chatId,
|
||||
message,
|
||||
});
|
||||
} catch (e) {
|
||||
if (isSessionNotFound(e)) {
|
||||
try { await handleSessionNotFound(e); } catch {}
|
||||
throw new Error('Your WhatsApp session has disconnected. Reconnect it in Admin → WhatsApp Settings, then try again.');
|
||||
}
|
||||
throw new Error(`WhatsApp text send failed: ${wawpErrorMessage(e)}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -258,17 +272,26 @@ async function sendPdf(toPhone, localPdfPath, filename, caption) {
|
||||
const pdfUrl = `${backendUrl}/uploads/tickets-temp/${tempName}`;
|
||||
|
||||
const { token, instanceId } = await getConfig();
|
||||
await axios.post(`${BASE}/send/pdf`, {
|
||||
access_token: token,
|
||||
instance_id: instanceId,
|
||||
chatId,
|
||||
file: {
|
||||
url: pdfUrl,
|
||||
filename: filename || 'tickets.pdf',
|
||||
mimetype: 'application/pdf',
|
||||
},
|
||||
caption: caption || '',
|
||||
});
|
||||
try {
|
||||
await axios.post(`${BASE}/send/pdf`, {
|
||||
access_token: token,
|
||||
instance_id: instanceId,
|
||||
chatId,
|
||||
file: {
|
||||
url: pdfUrl,
|
||||
filename: filename || 'tickets.pdf',
|
||||
mimetype: 'application/pdf',
|
||||
},
|
||||
caption: caption || '',
|
||||
});
|
||||
} catch (e) {
|
||||
try { fs.unlinkSync(tempPath); } catch {}
|
||||
if (isSessionNotFound(e)) {
|
||||
try { await handleSessionNotFound(e); } catch {}
|
||||
throw new Error('Your WhatsApp session has disconnected. Reconnect it in Admin → WhatsApp Settings, then try again.');
|
||||
}
|
||||
throw new Error(`WhatsApp PDF send failed: ${wawpErrorMessage(e)}`);
|
||||
}
|
||||
|
||||
// Clean up after 5 minutes — WAWP will have fetched the file by then
|
||||
setTimeout(() => { try { fs.unlinkSync(tempPath); } catch {} }, 5 * 60 * 1000);
|
||||
|
||||
Reference in New Issue
Block a user