Files
hope-events/frontend/src/components/reports/charts/HorizontalBarChart.tsx
T
joshuaandClaude Sonnet 5 0de3f4be7d 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>
2026-08-04 14:52:54 +02:00

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>
);
}