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>
42 lines
1.8 KiB
TypeScript
42 lines
1.8 KiB
TypeScript
"use client";
|
|
|
|
import React from "react";
|
|
|
|
// Fixed categorical order from the validated reference palette (dataviz skill,
|
|
// references/palette.md) — never cycled or reassigned per-render, so the same category
|
|
// always gets the same color across a session.
|
|
export const CATEGORICAL_COLORS = ["#2a78d6", "#eb6834", "#1baf7a", "#eda100", "#e87ba4", "#4a3aa7", "#e34948"];
|
|
|
|
export type BarDatum = { label: string; value: number };
|
|
|
|
// A simple, dependency-free horizontal bar list: thin rounded track, filled bar, direct value
|
|
// label. Suited to comparing a handful of categories' magnitude (the job most reports need) —
|
|
// per the dataviz skill's form heuristic, magnitude-by-category is exactly a bar chart's job.
|
|
export function HorizontalBarChart({
|
|
data, valueFormatter, labelWidthClass = "w-28",
|
|
}: {
|
|
data: BarDatum[];
|
|
valueFormatter?: (v: number) => string;
|
|
labelWidthClass?: string;
|
|
}) {
|
|
const max = Math.max(1, ...data.map(d => Math.abs(d.value)));
|
|
const fmt = valueFormatter || ((v: number) => String(v));
|
|
return (
|
|
<div className="space-y-2.5">
|
|
{data.map((d, i) => (
|
|
<div key={d.label} className="flex items-center gap-3">
|
|
<div className={labelWidthClass + " text-xs text-gray-600 truncate shrink-0"} title={d.label}>{d.label}</div>
|
|
<div className="flex-1 h-3 rounded-full bg-gray-100 overflow-hidden">
|
|
<div
|
|
className="h-full rounded-full transition-all"
|
|
style={{ width: `${Math.max(d.value > 0 ? 2 : 0, (Math.abs(d.value) / max) * 100)}%`, backgroundColor: CATEGORICAL_COLORS[i % CATEGORICAL_COLORS.length] }}
|
|
/>
|
|
</div>
|
|
<div className="w-24 text-xs text-gray-700 text-right shrink-0 tabular-nums">{fmt(d.value)}</div>
|
|
</div>
|
|
))}
|
|
{data.length === 0 && <div className="text-xs text-gray-400">No data to chart.</div>}
|
|
</div>
|
|
);
|
|
}
|