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:
@@ -63,6 +63,7 @@ model User {
|
||||
notificationPreference NotificationPreference @default(email)
|
||||
registrations Registration[]
|
||||
payments Payment[]
|
||||
paymentsRecorded Payment[] @relation("PaymentRecordedBy")
|
||||
tickets Ticket[]
|
||||
ticketScans TicketUsage[]
|
||||
passwordResets PasswordReset[]
|
||||
@@ -74,6 +75,9 @@ model User {
|
||||
eventsReopened Event[] @relation("EventReopenedBy")
|
||||
cashupsPerformed EventCashup[]
|
||||
notifyForEvents Event[] @relation("EventNotifyRecipients")
|
||||
|
||||
personCashCountsFor EventCashupPersonCount[] @relation("EventCashupPersonCountFor")
|
||||
personCashCountsEntered EventCashupPersonCount[] @relation("EventCashupPersonCountEnteredBy")
|
||||
}
|
||||
|
||||
model Event {
|
||||
@@ -112,6 +116,7 @@ model Event {
|
||||
reopenedBy User? @relation("EventReopenedBy", fields: [reopenedById], references: [id], onDelete: SetNull)
|
||||
costs EventCost[]
|
||||
cashups EventCashup[]
|
||||
personCashCounts EventCashupPersonCount[]
|
||||
|
||||
// Users who should receive registration/payment/daily-summary notifications for this
|
||||
// event. Falls back to `createdBy` when empty (see backend/src/utils/notifications.js).
|
||||
@@ -215,6 +220,7 @@ model Payment {
|
||||
amount Float
|
||||
method String
|
||||
userId String
|
||||
recordedById String?
|
||||
registrationId String?
|
||||
eventId String?
|
||||
isDonation Boolean @default(false)
|
||||
@@ -223,6 +229,7 @@ model Payment {
|
||||
originalPaymentId String?
|
||||
createdAt DateTime @default(now())
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Restrict)
|
||||
recordedBy User? @relation("PaymentRecordedBy", fields: [recordedById], references: [id], onDelete: SetNull)
|
||||
registration Registration? @relation(fields: [registrationId], references: [id], onDelete: SetNull)
|
||||
event Event? @relation(fields: [eventId], references: [id], onDelete: SetNull)
|
||||
originalPayment Payment? @relation("SplitPayments", fields: [originalPaymentId], references: [id], onDelete: SetNull)
|
||||
@@ -230,9 +237,11 @@ model Payment {
|
||||
YocoTransaction YocoTransaction[]
|
||||
|
||||
@@index([userId])
|
||||
@@index([recordedById])
|
||||
@@index([registrationId])
|
||||
@@index([eventId])
|
||||
@@index([createdAt])
|
||||
@@index([originalPaymentId])
|
||||
}
|
||||
|
||||
model Ticket {
|
||||
@@ -479,6 +488,36 @@ model EventCashupDenomination {
|
||||
@@index([lineId])
|
||||
}
|
||||
|
||||
// Actual physical cash counted for one staff member's recorded cash payments, entered any
|
||||
// time (not part of the close flow) purely for accountability — compared against the
|
||||
// system-expected amount (computeCashAccountabilityByUser) to show a per-person variance.
|
||||
model EventCashupPersonCount {
|
||||
id String @id @default(uuid())
|
||||
eventId String
|
||||
userId String
|
||||
enteredById String?
|
||||
notes String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
event Event @relation(fields: [eventId], references: [id], onDelete: Cascade)
|
||||
user User @relation("EventCashupPersonCountFor", fields: [userId], references: [id], onDelete: Cascade)
|
||||
enteredBy User? @relation("EventCashupPersonCountEnteredBy", fields: [enteredById], references: [id], onDelete: SetNull)
|
||||
denominations EventCashupPersonCountDenomination[]
|
||||
|
||||
@@unique([eventId, userId])
|
||||
@@index([eventId])
|
||||
}
|
||||
|
||||
model EventCashupPersonCountDenomination {
|
||||
id String @id @default(uuid())
|
||||
countId String
|
||||
value Float
|
||||
count Int
|
||||
personCount EventCashupPersonCount @relation(fields: [countId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([countId])
|
||||
}
|
||||
|
||||
// ─── Church Website Models (disabled — kept for reference, not active Prisma models) ──
|
||||
//
|
||||
// model Ministry {
|
||||
|
||||
Reference in New Issue
Block a user