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>
Replaces the explicit "look up existing account" search field with automatic
lookup as email/phone are entered, requires operator confirmation before any
matched account's name/email/phone/preference is changed, adds a password
show/hide toggle, and fixes two bugs found during testing: entering a phone
number belonging to a different account could silently overwrite the form
with that account's details, and re-checking an unchanged field (e.g. from
tapping a ticket quantity button) could revert edits already made. Also adds
a server-side check rejecting registrations whose email and phone resolve to
two different existing accounts, as defense in depth.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
When the account lookup finds no match, carry the typed query into whichever
of Email/Phone it resembles (email-shaped or phone-shaped) instead of
discarding it, so the operator doesn't have to retype it. Leaves both blank
if it matches neither shape, and resets Name/notification preference for a
fresh entry.
The kiosk previously ran a debounced background lookup on every keystroke
in the visitor Email/Phone fields, silently autofilling matches. Replaced
with a dedicated "Look up existing account" field that only searches when
the operator presses Enter or clicks Search, and matches exactly against
either email or phone (never both from one query, to avoid digits in an
email being misread as an unrelated phone number). Also simplified the
"account already exists" banner wording.
- GET /api/events/all gains an opt-in excludeClosed=true param, used only by
the kiosk, so closed events no longer show as selectable there while other
admin/supervisor screens that still need to see closed events are unaffected.
- GET /api/users/check-exists now also returns the matched account's name,
email, phone, and notification preference (safe fields only). The kiosk's
existing debounced lookup uses this to autofill whichever fields are still
blank when a visitor enters an email or phone that matches an existing
account, without overwriting anything already typed.
Lets staff redeem a registration's Main Tickets by quantity at the door
(via the Payment/registration flow) instead of scanning each QR code,
and automatically emails/WhatsApps a check-in confirmation to the
attendee. Also routes the At The Door "Open" button and post-payment
flow dynamically: paid registrations jump straight to Check-In instead
of a forced ticket print, since tickets are already sent automatically.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
registration/success read the token from the wrong localStorage key
(token instead of hope_events_token), which broke registration/form
loading there and made the new "Pay with Yoco" checkout call fail with
"Not authorized, token failed" — switched to the shared auth context
like the rest of the app.
dashboard/user's "Make payment" reused the ticket-email dialog, which
hardcoded "Sending tickets…" while loading — the dialog now takes an
optional loading title/subtitle so payNow can show its own message.
Self-service "pay now" flows (registration success + user dashboard) now go
straight to a Yoco checkout for the full outstanding balance instead of
prompting for a partial amount; that page has been removed. Partial-amount
payment links remain supervisor/admin-only via the Payments dashboard.
Also adds a "Record Payment" tab to the supervisor manual registration page,
pre-filled with the most recently created registration, so staff can capture
a payment right after registering someone without leaving the page.
Two consistency fixes requested after the payment-method work:
1. Registration status (pending/confirmed/partial_paid/paid/cancelled)
was printed as a raw string on the user dashboard. Added
RegistrationStatusBadge mirroring the existing EventStatusBadge
pattern, using the same status colors already established on
dashboard/admin/registrations.
2. Inline success/error banners across dashboard pages persisted
indefinitely. Added a shared useDismissingState hook (drop-in
useState replacement that auto-clears a truthy value after 7s,
resetting the timer on each update) and swapped it in across ~24
dashboard files. Excluded: message-only modal dialogs (ticket-
scanning's success/error confirmations) and two states that mix
live form-validation feedback with async results inside actively-
open forms (the registration-edit modal's editError, the event
create/edit modal's error) - those keep persisting until the user
acts, since auto-hiding a "fix this field" message mid-edit would
be a regression. Also fixed at-the-door's existing bespoke
auto-dismiss timers (10s/15s, one mislabeled as "5s") to the same
consistent 7s, and removed admin/settings' manual x dismiss button
in favor of the same auto-only behavior.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Audited every place Payment.method reaches the UI. The main user
dashboard's per-registration payment list (fed by GET
/api/payments/registration/:id) was still showing the raw gateway
string, since that endpoint is shared with the staff-facing supervisor
payments page and wasn't touched by the earlier /mypayments fix.
Extracted the cash/card/eft/voucher/other normalization (matching
normalizeUserMethod on the backend) into frontend/src/lib/paymentMethod.ts
and applied it to both user-facing payment displays. Staff-facing views
(supervisor payments, at-the-door, reports, cashup) intentionally keep
showing the raw method for reconciliation and were left unchanged.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Per feedback: card-network wallet payments (Apple Pay, Google Pay) should
report and filter as "card" on the user payment history page rather than
"other", since they settle the same way as a card payment. Any other
gateway-reported method still falls under "other".
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Per feedback, drop the dynamic /mypayments/methods lookup (wasn't
loading reliably) in favor of a static cash/card/eft/voucher/other
dropdown. Server-side normalization now folds any gateway-reported
method outside those four manual-entry values (apple_pay, google_pay,
yoco, etc.) into "other" instead of "card", both in the returned data
and in the filter query.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Per feedback, stop trying to surface every gateway-reported wallet
type (apple_pay, google_pay, yoco, ...) as its own filter/display
value on the user payment history page. Both /mypayments and
/mypayments/methods now fold anything outside the four manual-entry
methods into "card", both in the returned data and in the filter
query, so Apple Pay/Google Pay payments show up under Card.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The previous fix hardcoded apple_pay/google_pay as extra filter options,
but Payment.method is free-text set by whatever the gateway reports, so
guessing at literal values was fragile and still didn't surface them for
this user. Add GET /api/payments/mypayments/methods returning the
distinct method values actually present in the user's payments, and have
the dashboard filter build its options from that instead. Also switch
the method filter from a startsWith match to an exact match, since the
values now come straight from the same column being filtered.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Yoco webhook payments are tagged with whatever wallet type the gateway
reports, not just cash/card/eft/voucher. Cashup/report totals were
silently dropping those into the "other" bucket instead of "card", and
the new user payment history page couldn't filter by them and showed
raw snake_case values.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Users can now view their own payments (excluding donations) with
server-side pagination (25/page), date range, method, and
payment/refund filters, both on the API and the new client page.
Adds an EventStatusBadge component shown next to every event title on
the dashboard (registrations list, upcoming events, tickets, and the
registration modal), with precedence Closed > Past > Inactive when
more than one applies — reusing the badge styling already established
in the supervisor events list.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Extends the earlier user-dashboard fix: canEditActive is renamed
canModifyActive and now also gates the "Make payment" and "Cancel
registration" actions, not just editing. Backend enforcement added to
cancelRegistration and createYocoCheckout to block past-event
self-service payment/cancellation server-side (cashup-closed events
were already blocked via assertEventOpen; admins/supervisors are
exempt from the past-date check, consistent with existing overrides).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Registration-edit errors were set into a page-level error state rendered
behind the edit modal overlay; they now render inside the modal. The
"Show past events" toggle only hid date-based past events, letting
cashup-closed events leak through by default; a shared isEventOver()
check now covers both, applied to registrations, tickets, and the
Edit button visibility (mirroring the backend's own edit block).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>