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>
35 lines
1.3 KiB
TypeScript
35 lines
1.3 KiB
TypeScript
"use client";
|
|
|
|
import React from "react";
|
|
import type { LucideIcon } from "lucide-react";
|
|
|
|
const TONES = {
|
|
green: { bg: "bg-emerald-50", icon: "text-emerald-600" },
|
|
blue: { bg: "bg-blue-50", icon: "text-blue-600" },
|
|
violet: { bg: "bg-violet-50", icon: "text-violet-600" },
|
|
amber: { bg: "bg-amber-50", icon: "text-amber-600" },
|
|
rose: { bg: "bg-rose-50", icon: "text-rose-600" },
|
|
gray: { bg: "bg-gray-100", icon: "text-gray-600" },
|
|
} as const;
|
|
|
|
export type StatTileTone = keyof typeof TONES;
|
|
|
|
export function StatTile({ icon: Icon, label, value, tone = "gray" }: { icon: LucideIcon; label: string; value: string; tone?: StatTileTone }) {
|
|
const t = TONES[tone] || TONES.gray;
|
|
return (
|
|
<div className={"flex items-center gap-3 rounded-xl p-3 " + t.bg}>
|
|
<div className="w-9 h-9 rounded-lg bg-white/70 flex items-center justify-center shrink-0">
|
|
<Icon className={"w-5 h-5 " + t.icon} />
|
|
</div>
|
|
<div className="min-w-0">
|
|
<div className="text-xs text-gray-500 truncate">{label}</div>
|
|
<div className="text-sm font-semibold text-gray-900 truncate">{value}</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function StatTileRow({ children }: { children: React.ReactNode }) {
|
|
return <div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3">{children}</div>;
|
|
}
|