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:
@@ -0,0 +1,81 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { X, Info, RefreshCw, type LucideIcon } from "lucide-react";
|
||||
|
||||
export default function ReportViewerModal({
|
||||
title, description, icon: Icon, onClose, actions, filters, onRefresh, busy, children,
|
||||
}: {
|
||||
title: string;
|
||||
description?: string;
|
||||
icon?: LucideIcon;
|
||||
onClose: () => void;
|
||||
actions?: React.ReactNode;
|
||||
filters?: React.ReactNode;
|
||||
onRefresh?: () => void;
|
||||
busy?: boolean;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
// Above the site header (Navbar is `sticky top-0 z-50`) so the popup never sits behind it.
|
||||
<div className="fixed inset-0 z-[60]">
|
||||
<div className="absolute inset-0 bg-black/40" onClick={onClose} />
|
||||
<div className="absolute inset-0 flex items-start justify-center p-4 overflow-auto">
|
||||
<div className="w-full max-w-6xl bg-white rounded-xl shadow-xl my-8" onClick={e => e.stopPropagation()}>
|
||||
<div className="flex items-start justify-between gap-4 px-5 py-4 border-b">
|
||||
<div className="flex items-start gap-3 min-w-0">
|
||||
{Icon && (
|
||||
<div className="w-11 h-11 rounded-xl bg-indigo-50 flex items-center justify-center shrink-0">
|
||||
<Icon className="w-5 h-5 text-indigo-600" />
|
||||
</div>
|
||||
)}
|
||||
<div className="min-w-0">
|
||||
<h2 className="text-lg font-semibold text-gray-900">{title}</h2>
|
||||
{description && <p className="text-sm text-gray-500 mt-0.5">{description}</p>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
{actions}
|
||||
<button className="p-2 rounded-lg hover:bg-gray-100 ml-1" onClick={onClose} aria-label="Close">
|
||||
<X className="w-5 h-5 text-gray-500" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 px-5 py-3 border-b bg-indigo-50/50">
|
||||
<div className="flex items-center gap-2 text-sm text-indigo-900 flex-1 min-w-0">
|
||||
<Info className="w-4 h-4 text-indigo-400 shrink-0" />
|
||||
{filters || <span>This report has no extra filters beyond Events and Date range in the sidebar.</span>}
|
||||
</div>
|
||||
{onRefresh && (
|
||||
<button
|
||||
onClick={onRefresh}
|
||||
disabled={busy}
|
||||
className="shrink-0 flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-lg bg-indigo-600 text-white hover:bg-indigo-700 disabled:opacity-50"
|
||||
>
|
||||
<RefreshCw className={"w-3.5 h-3.5 " + (busy ? "animate-spin" : "")} /> {busy ? "Loading…" : "Refresh"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="p-5">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ReportActionButton({ icon: Icon, label, onClick }: { icon: LucideIcon; label: string; onClick: () => void }) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className="flex items-center gap-1.5 px-3 py-2 text-sm rounded-lg border border-gray-200 text-gray-700 hover:bg-gray-50 whitespace-nowrap"
|
||||
>
|
||||
<Icon className="w-4 h-4 text-gray-500" />
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user