- Privacy page: news opt-in per channel with history, download my data (JSON + PDF), correction and deletion requests - Bulk messages are an Event update or News & announcements; news only reaches people who opted in, with unsubscribe links, one-click List-Unsubscribe headers and "Reply STOP" on WhatsApp - WhatsApp STOP via WAWP's message webhook, checked against a secret in the webhook URL - Sign-up opt-in box; existing accounts kept opted in and asked once - Admin -> Data requests with 30-day due dates and a daily reminder job - Deleting an account is a request; anonymising now covers attendees, form answers, consent details, payer details in provider payloads, etc. - Unit tests (privacy.test.js) and e2e (privacy.spec.ts); docs Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012mt6TCv85T9VKRq3cV7rXq
667 lines
38 KiB
Markdown
667 lines
38 KiB
Markdown
# Hope Events — Feature Overview
|
||
|
||
A complete map of what the platform does, grouped by who uses it. For setup and
|
||
deployment see [README.md](./README.md); for the API see
|
||
[backend/API_DOCUMENTATION.md](./backend/API_DOCUMENTATION.md); for setting
|
||
defaults see [SETTINGS.md](./SETTINGS.md); for history see
|
||
[CHANGELOG.md](./CHANGELOG.md).
|
||
|
||
**Stack:** Next.js (React, Tailwind, shadcn/ui) frontend · Express + Prisma +
|
||
PostgreSQL backend · Yoco for card payments · SMTP for email · WAWP for
|
||
WhatsApp · PM2 for process management · optional Sentry for error monitoring.
|
||
|
||
---
|
||
|
||
## Contents
|
||
|
||
1. [Roles](#roles)
|
||
2. [Public site](#public-site)
|
||
3. [Events](#events)
|
||
4. [Registration](#registration)
|
||
5. [Payments, donations & refunds](#payments-donations--refunds)
|
||
6. [Tickets & check-in](#tickets--check-in)
|
||
7. [Attendee forms](#attendee-forms)
|
||
8. [Messaging & notifications](#messaging--notifications)
|
||
9. [User dashboard (My Events)](#user-dashboard-my-events)
|
||
10. [Staff tools](#staff-tools)
|
||
11. [Supervisor tools](#supervisor-tools)
|
||
12. [Admin tools](#admin-tools)
|
||
13. [Cashup & event costs](#cashup--event-costs)
|
||
14. [Reports](#reports)
|
||
15. [Site settings & setup wizard](#site-settings--setup-wizard)
|
||
16. [Accounts & security](#accounts--security)
|
||
17. [Privacy (POPIA)](#privacy-popia)
|
||
18. [Help system](#help-system)
|
||
19. [Background jobs](#background-jobs)
|
||
20. [Operations](#operations)
|
||
21. [Page map](#page-map)
|
||
|
||
---
|
||
|
||
## Roles
|
||
|
||
Four roles, each including everything the roles below it can do:
|
||
|
||
| Role | Can do |
|
||
|------|--------|
|
||
| **User** | Browse events, register, pay, donate, view tickets, fill in forms, manage their own profile |
|
||
| **Staff** | Scan tickets, view/print event tickets, view registrations and form responses |
|
||
| **Supervisor** | Create and manage events, record payments/refunds, assign donations, at-the-door desk, manual registration, bulk email/WhatsApp, reports, site banner, self-service kiosk |
|
||
| **Admin** | Manage users and roles, all registrations, event costs and cashup (close/reopen events), audit log, site settings, WhatsApp connection, backups |
|
||
|
||
---
|
||
|
||
## Public site
|
||
|
||
- **Home page** — hero section with the organisation's name and branding, upcoming
|
||
and in-progress events, and a "Join Us" button (becomes "My Dashboard" when
|
||
logged in).
|
||
- **Events list** (`/events`) — event cards with picture, dates, location,
|
||
price, and a Register / Sold Out / Contact us button. A diagonal "Early bird"
|
||
ribbon in the top-right corner while an early-bird tier is running and
|
||
registration is open.
|
||
- **Event detail** (`/events/[id]`) — description, ticket options with
|
||
variant and early-bird pricing, low-stock and sold-out badges, downloadable
|
||
attachments, embedded Google Map with "Directions" link, and an
|
||
**Add to calendar** (`.ics`) button.
|
||
- **Short links** — events can have an alias (e.g. `/camp-2025`) that redirects
|
||
to the event. Only plausible aliases hit the database, so bot probes are
|
||
rejected cheaply.
|
||
- **Contact page** (`/contact`) — organisation email, phone, address and map.
|
||
- **Legal pages** — Terms of Use and Privacy Policy, filled from Site Settings.
|
||
- **Site banner** — a site-wide announcement bar set by supervisors.
|
||
- **Lockdown rules** (`/lockdown-rules`) — static rules page for the youth
|
||
lockdown event.
|
||
- **SEO** — `sitemap.xml` (every public page and event), `robots.txt`, and
|
||
Open Graph/Twitter link-preview metadata.
|
||
- **Theming** — brand colours, logo and favicon come from Site Settings.
|
||
|
||
---
|
||
|
||
## Events
|
||
|
||
Created and edited by supervisors through a 9-step wizard: **Basic Details →
|
||
Items & Pricing → Sections → Form → Visibility → Notifications → Attachments →
|
||
Feedback → Attendance**.
|
||
|
||
- **Basic details** — title, description, start/end dates, registration
|
||
deadline, picture (upload or URL), location (defaults to the org's address),
|
||
contact person.
|
||
- **Contact-only events** — for things like baptisms: shown publicly with a
|
||
"Contact us" popup instead of registration, with no tickets.
|
||
- **Multi-day events (sessions)** — one event on several dates, e.g. every
|
||
Wednesday 18:30–20:30 for 5 weeks. A repeat pattern (daily/weekly, every N,
|
||
chosen weekdays, a number of sessions or an end date) creates the sessions,
|
||
which can then be edited one by one (label, times, location), added to, or
|
||
cancelled (kept for history; one with check-ins can't be deleted). People
|
||
register and pay once and get one ticket. The event page shows the schedule
|
||
with the next session highlighted; cards show "5 sessions · Wed 4 Nov – Wed
|
||
2 Dec" and stay listed until the last session. Calendar files get one entry
|
||
per session. Saving moved or cancelled sessions offers to message attendees.
|
||
- **Pick sessions** (multi-day) — instead of the whole course, people can choose
|
||
which sessions to come to (day passes, drop-in classes). Tickets set to "Pick
|
||
sessions" are priced and bought per session — one line and one ticket per session,
|
||
bookable until the session starts (staff: until it ends) — alongside optional
|
||
whole-course tickets. Sessions can have a capacity (picked places plus whole-course
|
||
tickets); a per-session ticket's stock limit is places per session. Picked tickets
|
||
only scan on their own session; reminders, the register and certificates follow
|
||
the sessions each person booked. "All sessions" tickets on such an event also show
|
||
the sessions: people tick the ones they'll come to and pay the ticket's one price; the
|
||
ticket scans, reminds and counts towards capacity for those sessions only. The waitlist
|
||
works per session (a full session shows "Join waitlist"). Bookable from the registration page, Manual registration,
|
||
At the door (today's session is preselected) and the kiosk. Not available on youth
|
||
events.
|
||
- **Late joining** (multi-day) — registration closes when the first session
|
||
starts, or later (until session N) with late joining on. Late joiners pay
|
||
full price, a pro-rata price for the sessions left (rounded to the nearest
|
||
R10), or a fixed late price — main tickets only.
|
||
- **Completion certificates** — optional per event: four designs (Classic,
|
||
Modern, Church, Kids), custom wording with placeholders, signature name/role/
|
||
image, and a completion rule (every session, at least N sessions, or N%).
|
||
Issued automatically when the event ends; each has a code and QR code checked
|
||
on a public verify page.
|
||
- **Ticket options** — multiple options per event (e.g. Adult, Child, Dinner,
|
||
T-shirt), each with a price and optional **stock limit**. One option is marked
|
||
as the main ticket, which is what gets checked in at the door.
|
||
- **Variants** — sub-types of an option (sizes, colours, Adult/Child/VIP), each
|
||
with an optional price override and its own stock limit.
|
||
- **Early-bird tiers** — deadline- and/or stock-limited cheaper prices per
|
||
option or variant. Prices are locked in per purchase ("tranches"), so buying
|
||
more later doesn't re-price what was already bought.
|
||
- **Sections** — named scan points (e.g. "Main hall", "Dinner") limited to
|
||
certain ticket options, so a scanner at that point only accepts those tickets.
|
||
- **Visibility** — active/inactive, hidden (reachable only by link), scheduled
|
||
go-live date, require login vs. allow guest registration, or no registration
|
||
at all.
|
||
- **Attachments** — downloadable files (maps, schedules, waivers) shown on the
|
||
event page.
|
||
- **Notify recipients** — choose which staff get registration/payment alerts and
|
||
the daily summary for the event.
|
||
- **Sold-out handling** — an event is sold out when every stock-limited option
|
||
is exhausted. "X remaining" badges appear at a threshold that scales with
|
||
capacity.
|
||
|
||
---
|
||
|
||
## Registration
|
||
|
||
- **Online registration** (`/register/[eventId]`) — pick options, variants and
|
||
quantities; see early-bird prices and stock warnings; fill in the event form.
|
||
- **Guest registration** — when an event doesn't require login, an inactive
|
||
account is created and an activation link is sent by email or WhatsApp.
|
||
- **Statuses** — Pending, Confirmed, Partially Paid, Paid, Cancelled.
|
||
- **Edit options** — users can add or change items on their registration
|
||
(not for past or closed events).
|
||
- **Self-cancellation** — allowed only before any payment; admins can cancel
|
||
any time.
|
||
- **Pricing refresh** — when an unpaid registration is viewed or paid, expired
|
||
early-bird prices are re-evaluated, and the user is warned if the total
|
||
changed.
|
||
- **Manual registration** (supervisor) — register someone on their behalf,
|
||
creating an account if needed. Includes a "Record Payment" tab.
|
||
- **Duplicate-account protection** — if an email and phone belong to two
|
||
different accounts, the registration is blocked instead of merging them.
|
||
- **Group tickets** — on events with "Named attendees" turned on, someone booking
|
||
several places can name each person (with optional email/phone). Each named person
|
||
gets their own ticket and their own attendee form; unnamed places share one ticket.
|
||
Tickets go all to the payer, or each to the person (falling back to the payer).
|
||
Attendees matching an existing account see their ticket in My Events. Names can be
|
||
added or changed later from My Events, and attendees can be sent a link to fill in
|
||
their own form. Available on the registration page, manual registration, At the
|
||
door and the kiosk.
|
||
- **Waitlist** — on events with a waitlist, sold-out tickets show "Join waitlist"
|
||
(on the event page, the register page and the event list). When places free up — a
|
||
cancellation, a smaller booking or a higher stock limit — the next person is
|
||
emailed/WhatsApped a claim link and the places are held for them (24 hours by
|
||
default) so nobody else can buy them. Unclaimed offers expire and move on; people
|
||
who asked for more than came free can be offered what's free or skipped. Claiming
|
||
works without logging in. People can leave from My Events. On pick-sessions events
|
||
people wait for a particular session; offers never run past the session's start.
|
||
- **Parental consent (youth events)** — on events that require it, every attendee
|
||
gives a date of birth; for anyone under 18, a parent or guardian gets an email and
|
||
a WhatsApp with a consent link. They read the event's consent text, optionally give
|
||
medical info, an emergency contact and photo consent, type their name to sign, and
|
||
give or decline. Tickets are issued but can't be checked in until consent is given
|
||
(supervisors can override, audited). Reminders go after 2 days, then every 3 days.
|
||
Medical details are encrypted and deleted a set number of days after the event.
|
||
- **Coupon codes** — percentage, fixed-amount or free (comp) codes, entered under
|
||
"Have a code?" on the registration page, in My Events (for unpaid registrations),
|
||
manual registration, At the door and the kiosk. Codes can be limited to events,
|
||
ticket options or variants, dates, total and per-person uses, a minimum quantity,
|
||
and specific people, roles or an email domain (restricted codes need a login).
|
||
Early-bird prices either stack or the better price wins. The discount is locked in
|
||
when applied and shows on invoices, receipts, emails and the Yoco checkout;
|
||
cancelling frees the use. Managed from Supervisor → Coupon codes.
|
||
- **Staff price** — a ticket option or variant can have a staff price that Staff and
|
||
above get automatically, without a code.
|
||
|
||
---
|
||
|
||
## Payments, donations & refunds
|
||
|
||
- **Online card payments via Yoco** — checkout for the full outstanding balance,
|
||
confirmed by a signed webhook. Keys and the webhook are set up from
|
||
Site Settings → Payments, where providers are switched on or off and put in order
|
||
(the first one is the default). Online payments are at least R15; payments are
|
||
recorded at the full amount and provider fees are dealt with at cashup.
|
||
- **PayFast** — the second online provider (card, instant EFT…), with sandbox and live
|
||
modes. When several providers are on, payers choose on the "Pay online" page; a
|
||
payment of a different amount from the checkout is held for review instead of being
|
||
recorded. When no provider is on (or the amount is under R15), nothing offers to pay
|
||
online.
|
||
- **Payment status checks** — if a provider's notification never arrives, Payments →
|
||
Reconcile → "Waiting for payment" checks with the provider (PayFast; Yoco's API can't be
|
||
asked) and records what it finds. A job checks recent PayFast checkouts every 15 minutes.
|
||
- **Payment links** — supervisors can generate and send a Yoco link for any
|
||
amount (including partial).
|
||
- **Manual payments** — cash, card, EFT, voucher or other, recorded by staff.
|
||
Each payment records **who recorded it** as well as who it's for.
|
||
- **Refunds** — negative payments against an original payment, netted against
|
||
the original method.
|
||
- **Impersonate user** (Admin → Manage users) — an admin with 2FA can view the site as a
|
||
user, to debug problems.
|
||
- It needs a reason, lasts at most 30 minutes, and shows a bright "Viewing as" banner
|
||
on every page.
|
||
- It is strictly read-only. A server-side guard refuses every write made with the
|
||
impersonation token before it reaches any route, and pages don't even refresh
|
||
prices while viewing.
|
||
- Admins can't be impersonated.
|
||
- The start, end, reason and every page and API call are in the audit log, with each
|
||
session's trail.
|
||
- Optional email to the user (`notify_user_on_impersonation`).
|
||
- **Registration funnel stats** — first-party and consent-based, with no third-party analytics.
|
||
- A cookie notice asks once. Only visitors who allow it get a random visit id.
|
||
- We record event page views and registration starts. Submits and payments come
|
||
from the registration itself.
|
||
- Each record holds only the stage, the traffic source (utm_source, or the short-link
|
||
alias, which short links now pass as `?src=`) and the device type.
|
||
- The Registration funnel report shows counts and conversion per stage over a date
|
||
range, a daily trend, and breakdowns by source, device and ticket option.
|
||
- A "Conversion (past month)" tile on the supervisor/admin dashboards covers live
|
||
events.
|
||
- Raw rows are rolled up into daily totals after 90 days.
|
||
- **Post-event feedback** — a Feedback step in the event wizard.
|
||
- Questions use the form builder, with new **star rating (1–5)** and **multiple
|
||
choice** types (also available on attendee forms).
|
||
- The form goes out at a chosen time to everyone who checked in, or to everyone
|
||
registered, each with a personal link (`/feedback/<token>`). One reminder goes
|
||
3 days later to anyone who hasn't answered.
|
||
- Answers can be anonymous or named. "Send now" is available, and the questions
|
||
lock once people have answered.
|
||
- **Sponsor a spot** — per-event setting. A "Sponsor a spot" button on the event page
|
||
lets anyone, including guests, pay for one or more spots (at the main ticket price)
|
||
or any amount through Yoco. They can give anonymously or add a name and a message.
|
||
The money is a donation tagged to the event and goes into its sponsorship pool, which
|
||
supervisors assign to registrations with the usual donation flow.
|
||
- An optional public progress bar ("12 of 20 spots sponsored") with an optional goal.
|
||
- Sponsors get a thank-you receipt, and an Automations thank-you can go to all of an
|
||
event's sponsors.
|
||
- The Donations breakdown report shows each pool: received, assigned and remaining.
|
||
- Who is helped is never shown.
|
||
- **Donations** — users can donate from their dashboard (`/dashboard/user/donate`).
|
||
Supervisors can **assign** a donation (fully or partly) to someone's
|
||
registration and **unassign** it later. Assignments are kept as separate
|
||
records, so the original donation never changes.
|
||
- **Online payment reconciliation** — provider (Yoco) transactions can be matched to a
|
||
registration, recorded as a donation (optionally choosing the donor), or
|
||
ignored.
|
||
- **Invoices & receipts** — branded PDFs: an invoice (itemised, early-bird
|
||
discount, balance due, pay-now link and QR code) and a payment receipt. Sent
|
||
automatically and re-sendable on demand by email or WhatsApp.
|
||
- **Method normalisation** — Apple Pay / Google Pay / checkout payments count as
|
||
"Card" in reports and user views.
|
||
|
||
---
|
||
|
||
## Tickets & check-in
|
||
|
||
- **Automatic tickets** — QR-coded tickets are generated when a registration is
|
||
fully paid (or free), and sent as a PDF by email and/or WhatsApp.
|
||
- **Quantity tickets** — one ticket can cover several people and be redeemed
|
||
partially (e.g. check in 2 of 3).
|
||
- **Named tickets** — group bookings can give each person their own ticket, shown
|
||
with their name on the PDF, the scanner and the staff ticket list.
|
||
- **QR scanner** — works in any mobile browser. A preview checks the ticket is
|
||
for the selected event/section, then a confirmation screen shows holder, type
|
||
and remaining quantity.
|
||
- **Check-in confirmation** — holders get an email/WhatsApp saying how many were
|
||
checked in and how many remain.
|
||
- **Per-session check-in** — multi-day tickets are checked in once per session.
|
||
The scanner uses today's session (or one picked from a list). Check-in is open
|
||
the whole day of a session, or only from 2 hours before it until it ends, per
|
||
event. Optional reminders (1 h / 3 h / 1 day before each session) by email
|
||
and/or WhatsApp.
|
||
- **Attendance register** — attendees × sessions with ticks, totals per session,
|
||
completion, filters (completed, missed the last two), Excel/PDF export.
|
||
Supervisors can tick someone present or absent (audit-logged), and issue,
|
||
send, download or revoke certificates.
|
||
- **Scan stats** — recent scans and per-event scan counts.
|
||
- **Offline scanning** — while online, a staff member locks an event section (or the
|
||
whole event) to their device and downloads its tickets. They can then keep scanning
|
||
with no connection; scans are queued on the device and sync automatically when it's
|
||
back online (never redeeming more than is left). Only one device can hold a section,
|
||
and while it's locked nobody else can scan it, online or offline. Problems found
|
||
while syncing go into a report for a supervisor. Supervisors can force-release a
|
||
lost device. The scanner can be added to the home screen (PWA) and opens offline.
|
||
- **Printing** — A4 sheets (8 tickets per page) and A6 single tickets.
|
||
- **Resend** — send tickets to a different email/phone at the door.
|
||
|
||
---
|
||
|
||
## Attendee forms
|
||
|
||
- **Form builder** per event — field types: Yes/No, Text, Paragraph, Number,
|
||
Date, plus Statement and Heading blocks; required/optional, help text,
|
||
reordering. The whole form can be marked required.
|
||
- **Drafts** — partially filled forms are auto-saved.
|
||
- **Responses** — view, filter (event, user, past/inactive) and print one
|
||
response per A4 page. Supervisors can edit responses.
|
||
|
||
---
|
||
|
||
## Messaging & notifications
|
||
|
||
Every user picks a **notification preference**: Email, WhatsApp, or Both.
|
||
|
||
**Automatic notifications**
|
||
|
||
| Trigger | Goes to |
|
||
|---------|---------|
|
||
| Registration created / updated | Registrant (with invoice if unpaid) + event notify recipients |
|
||
| Payment recorded / donation applied / unassigned | Registrant (with receipt) + admins |
|
||
| Refund processed | Registrant + admins |
|
||
| Tickets generated | Registrant (ticket PDF) |
|
||
| Ticket checked in | Ticket holder |
|
||
| Daily summary, 07:00, for each live event that hasn't started yet | Registrations inbox + event notify recipients |
|
||
| Account registered, activation, login, password changed/reset, account closed | The account holder |
|
||
|
||
**Bulk messaging** (supervisor)
|
||
|
||
- **Email attendees** and **WhatsApp attendees** — filter by status or pick
|
||
specific attendees, preview, send now or schedule for later.
|
||
- **Placeholders** — `{{name}}`, `{{event.title}}`, `{{event.link}}`,
|
||
`{{event.start}}`, `{{balance}}`, and `{{payment.link}}` (a live Yoco link for each recipient's
|
||
outstanding balance).
|
||
- **Broadcasts** — ad-hoc email/WhatsApp to any users or addresses, not tied to
|
||
one event.
|
||
- **Message type** — Event update or News & announcements; news only reaches people
|
||
who opted in (see [Privacy](#privacy-popia)).
|
||
- **Automations** — schedule a sequence of lifecycle emails for an event
|
||
(pre-event, final reminder, thank-you, promo for another event).
|
||
- **Manage scheduled** — view, edit or delete queued jobs per channel. Sends
|
||
run in the background and are purged 24 h after sending.
|
||
|
||
**WhatsApp** is connected through the WAWP API: an admin links a number in
|
||
Site Settings → WhatsApp (QR code or pairing code), with status, restart and
|
||
logout controls. Disconnected sessions are recovered automatically.
|
||
|
||
---
|
||
|
||
## User dashboard (My Events)
|
||
|
||
- Registrations with status badges, event thumbnails, and Closed/Past/Inactive
|
||
tags. Past and closed events are hidden by default.
|
||
- Registration detail: itemised bill (early-bird lines grouped), payments,
|
||
**Invoice** and **Receipt** buttons, edit, cancel, **Make payment**.
|
||
- Upcoming events and tickets side by side, with bulk ticket email/WhatsApp.
|
||
- **Ticket transfer** — offer a ticket to someone else before the event by typing
|
||
their name and email/phone (no user search, and nothing reveals whether they have an
|
||
account). They get a link to accept or decline; the ticket only moves once they
|
||
accept, and the sender can cancel until then (offers expire after 7 days or at the
|
||
event start). On accepting they get a new ticket and QR code, and an inactive account
|
||
with an activation link if they're new. The old code stops working, and scanning it says it
|
||
was transferred, both online and in offline-mode sync reports. One place can be split
|
||
off a ticket covering several people. Billing stays with the payer. It's not allowed
|
||
once a ticket has been scanned, and it can be turned off per event (supervisors can
|
||
always transfer, from At the door → Tickets). On youth events the new holder needs
|
||
their own parental consent, and the previous holder's form answers are removed.
|
||
Every transfer is in the audit log.
|
||
- **Payment history** (`/dashboard/user/payments`) — paginated, with filters by
|
||
date, method and payment/refund.
|
||
- **Donate** page.
|
||
- **Forms** — complete outstanding attendee forms.
|
||
- **Profile & Security** — edit details and notification preference, change
|
||
password, see recent account activity (logins, password changes, device),
|
||
sign out everywhere, deactivate or delete my data.
|
||
|
||
---
|
||
|
||
## Staff tools
|
||
|
||
- **Staff dashboard** — scan stats and quick actions.
|
||
- **Ticket scanning** (`/dashboard/staff/ticket-scanning`) — camera QR scanner
|
||
with event and section filters.
|
||
- **Event tickets & printing** (`/dashboard/staff/event-tickets`) — list, search
|
||
and print an event's tickets.
|
||
|
||
---
|
||
|
||
## Supervisor tools
|
||
|
||
- **Dashboard** — KPIs (active events, revenue, donations, registrations,
|
||
tickets sold, each vs. the previous month), revenue trend chart, top events,
|
||
and recent scans. Tiles link to the matching report.
|
||
- **Manage events** — the 9-step event wizard, ticket options, variants,
|
||
sections, form and attachments.
|
||
- **At the door** — search by name/phone/ticket ID, walk-in registration,
|
||
payments, a **Check-In** tab for redeeming main tickets by quantity, and
|
||
ticket printing/resending.
|
||
- **Self-service kiosk** (`/self-service`) — a touchscreen page a supervisor
|
||
unlocks for an event, where visitors register themselves. Existing accounts
|
||
are matched by email/phone, and any change to their details needs
|
||
confirmation.
|
||
- **Manual registration** — register someone on their behalf and record their
|
||
payment.
|
||
- **Payments** — tabs for Payment, Refund, Donations (assign/unassign),
|
||
Reconcile (Yoco transactions), and Payment Link.
|
||
- **Coupon codes** (`/dashboard/supervisor/coupons`) — create, edit and turn off
|
||
discount codes, and see who used each one.
|
||
- **Rooms & groups** (`/dashboard/supervisor/allocations`):
|
||
- Per event, allocation sets such as "Rooms", "Small groups" or "Buses". Each set
|
||
has groups with an optional capacity, gender restriction and leader (staff).
|
||
- A drag-and-drop board, plus auto-allocate: fill evenly, respecting capacity and
|
||
gender, keeping bookings together or splitting them.
|
||
- Printable per-group and master lists. On youth events they can include medical
|
||
details, and viewing those is audited.
|
||
- The group can show on tickets and in My Events.
|
||
- Email and WhatsApp attendees can be filtered to one group.
|
||
- Leaders see their people under Staff → **My groups**.
|
||
- Works on named attendees. Events can ask each person's gender.
|
||
- **Offline scanning** (`/dashboard/supervisor/offline-scans`) — devices holding
|
||
offline locks (with force release) and sync reports to review.
|
||
- **Waitlist** (`/dashboard/supervisor/waitlist`) — the queue per event: offer a
|
||
place now, move people up or down, or remove them.
|
||
- **Parental consent** (`/dashboard/supervisor/consent`) — consent status per
|
||
attendee on youth events, resend (one or all pending), re-request after the
|
||
consent text changes, and a printable medical & emergency list (audited).
|
||
- **Email attendees / WhatsApp attendees** — bulk and scheduled messaging.
|
||
- **Forms** — view, filter, edit and print form responses.
|
||
- **Reports** — see [Reports](#reports).
|
||
- **Site banner** (`/set-banner`) — set the announcement bar.
|
||
|
||
---
|
||
|
||
## Admin tools
|
||
|
||
- **Admin dashboard** — same KPIs as supervisor, plus admin quick actions.
|
||
- **Manage users** — server-side search and filters (role, active), paging,
|
||
edit role and notification preference, revoke sessions, delete or
|
||
**anonymise** personal data.
|
||
- **Data requests** — POPIA correction and deletion requests (see
|
||
[Privacy](#privacy-popia)).
|
||
- **Registrations** — all registrations, with filters (event, status, text),
|
||
status counts, revenue and outstanding totals, and per-registration options,
|
||
form answers and payments (with who recorded each one).
|
||
- **Cashup** — see below.
|
||
- **Audit log** (`/dashboard/admin/audit-log`) — who did what and when for
|
||
refunds, donation assign/unassign, manual registrations, staff
|
||
cancellations, event create/update/delete, settings changes, and coupon codes
|
||
(created, edited, turned off, applied or removed by staff).
|
||
- **Site settings** — see below.
|
||
|
||
---
|
||
|
||
## Cashup & event costs
|
||
|
||
- **Event costs** — once-off or per-item costs (optionally linked to a ticket
|
||
option), with a "paid from takings" method so cash on hand is correct.
|
||
- **Expected vs. actual** — expected takings per method (cash/card/EFT/other)
|
||
compared with actual amounts, with variance shown.
|
||
- **Cash denomination counts** — entered per staff member, at any time. The
|
||
event's actual cash is the sum of these counts.
|
||
- **Payment accountability by staff member** — what each person recorded, by
|
||
method, with live cash variance.
|
||
- **Close / quick close / reopen** an event — closing locks it from further
|
||
edits and hides it from the kiosk and user actions. Drafts are saved as you
|
||
go.
|
||
- **Report tab** — a read-only cashup summary, shown automatically once the
|
||
event is closed.
|
||
- **Cashup audit trail** — every close/reopen with totals and who did it.
|
||
|
||
---
|
||
|
||
## Reports
|
||
|
||
A full-width workspace with shared filters (events, date range with presets,
|
||
include past/inactive/closed) and a searchable grid of reports, each opening in
|
||
a viewer with its own filters, stat tiles and bar charts.
|
||
|
||
| Category | Reports |
|
||
|----------|---------|
|
||
| Financial | Payments between dates · Revenue summary (by method) · Revenue detailed · Finance report (revenue & costs, after discounts) · Profit report · Discounts (coupon codes) |
|
||
| Registration | Registration funnel (views → started → submitted → paid, daily trend, by source / device / ticket option) · Attendees per event (grouped by option) · Registration type counts · Master orders breakdown · Registration status breakdown (count by registration or ticket quantity) · Parental consent status · Medical & emergency list · Feedback summary (response rate, average ratings, choice breakdowns, comments; compare events) |
|
||
| Ticketing | Ticket usage summary |
|
||
| Donations | Donations breakdown (used / unused per event, plus the sponsorship pool) |
|
||
| Cashup | Cashup reconciliation · Cashup audit trail |
|
||
|
||
**Export:** Print, branded PDF download, styled Excel (`.xlsx`) workbook,
|
||
Email (to yourself), or WhatsApp (the PDF to your own number). Dashboard tiles
|
||
deep-link into reports with `?report=&range=`.
|
||
|
||
---
|
||
|
||
## Site settings & setup wizard
|
||
|
||
**Setup wizard** (`/setup`) — on a fresh database, walks through organisation
|
||
details, the first admin account and branding. No manual seeding needed.
|
||
|
||
**Site Settings** (`/dashboard/admin/settings`) — stored in the database and
|
||
applied without a restart. Secrets are encrypted with AES-256-GCM.
|
||
|
||
| Tab | Contains |
|
||
|-----|----------|
|
||
| Organisation | Name, tagline, contact email/phone, address (with map link) |
|
||
| Branding | Primary/secondary/accent colours, logo, favicon, colours suggested from the logo, live website/email preview on desktop and mobile |
|
||
| Notifications | Registrations inbox and admin notification emails |
|
||
| Email | SMTP host/port/TLS/user/password/from, with a "Test connection" button |
|
||
| Legal | Operator, Information Officer, website, effective date for the Terms/Privacy pages |
|
||
| WhatsApp | WAWP credentials, connect a number, status/restart/logout |
|
||
| Payments | Online payment providers: Enabled toggle and order; PayFast merchant ID/key, passphrase and sandbox/live; Yoco's secret key (masked, test/live), webhook secret (generate by registering with Yoco, or enter), list/delete the account's webhooks |
|
||
| Backups | List, run now, download database backups |
|
||
|
||
See [SETTINGS.md](./SETTINGS.md) for the default of every setting.
|
||
|
||
---
|
||
|
||
## Accounts & security
|
||
|
||
- Register, log in, forgot/reset password, activate account (for accounts
|
||
created on someone's behalf).
|
||
- JWT authentication (HS256 pinned) with **session revocation** (token
|
||
versioning) — users can sign out everywhere, and admins can do it for any user.
|
||
- **Two-factor authentication** — a 6-digit login code by email or WhatsApp
|
||
(10-minute expiry, 5 attempts, resend), with "Trust this device for 30 days".
|
||
**Required for Admins and Supervisors**: their next login sends a code that turns
|
||
it on, and they can change the channel but not turn it off (skipped only when no
|
||
email or WhatsApp channel can deliver a code; `TWO_FACTOR_REQUIRED_ROLES=false`
|
||
is the emergency switch). Optional for everyone else from Profile & Security.
|
||
Admins can reset it for a locked-out user, and the kiosk asks for the code at
|
||
setup.
|
||
- **Login lockout** after repeated failed attempts, plus login rate limiting
|
||
(`TRUST_PROXY` to read real IPs behind a proxy).
|
||
- **Security event log** — logins, password changes and resets, with device
|
||
and IP.
|
||
- **Account closure** — deactivate, or close and ask for the personal data to be
|
||
deleted (see [Privacy](#privacy-popia)).
|
||
- **Upload hardening** — server-generated filenames, and uploads served with a
|
||
sandbox CSP and `nosniff`.
|
||
|
||
---
|
||
|
||
## Privacy (POPIA)
|
||
|
||
Tools that help a church handle personal information the way POPIA expects. They
|
||
support compliance; wording and retention choices are for the church's Information
|
||
Officer.
|
||
|
||
**For everyone** — the **Privacy** page (`/dashboard/user/privacy`):
|
||
|
||
- **News & announcements** opt-in per channel (email, WhatsApp), with the history of
|
||
every choice: when, and how (sign-up, privacy page, unsubscribe link, STOP reply…).
|
||
Sign-up has an unticked opt-in box; accounts that never answered are asked once
|
||
after logging in.
|
||
- **Download my data** — readable PDF, or the complete copy as JSON.
|
||
- **Correct my data** — profile fields directly; anything else as a correction request.
|
||
- **Delete my account** — closes the account at once and files a deletion request.
|
||
|
||
**Bulk messages** are either an **Event update** (about an event the recipients
|
||
registered for; always sent) or **News & announcements** (only to people who opted in
|
||
on that channel and haven't unsubscribed). News emails carry an unsubscribe link and
|
||
one-click `List-Unsubscribe` headers; news WhatsApps end with "Reply STOP" and a link.
|
||
Event updates say why they were sent and link to the preferences. A STOP reply on
|
||
WhatsApp turns off WhatsApp news (through WAWP's `message` webhook, checked against a
|
||
secret in the webhook URL).
|
||
|
||
**For admins** — **Data requests** (`/dashboard/admin/data-requests`): correction and
|
||
deletion requests with a 30-day due date, emailed to the Information Officer and
|
||
admins, reminded daily when due within a week. Completing a deletion anonymises the
|
||
person: name, contact details, the people they registered, form answers, consent
|
||
details, sponsor names, login history and payer details in provider notifications are
|
||
removed; payments, registrations, tickets and check-ins stay as "Deleted User". It
|
||
waits while they have a registration for an upcoming event. Everything is in the audit
|
||
log.
|
||
|
||
---
|
||
|
||
## Help system
|
||
|
||
A floating **"Need help?"** button on every page opens a page-specific guide
|
||
(tabs, quick links). Every page has dedicated content (`frontend/src/content/help/`),
|
||
falling back to a general guide.
|
||
|
||
---
|
||
|
||
## Background jobs
|
||
|
||
Timed jobs run on a job queue in Postgres (pg-boss, `backend/src/jobs/index.js`): each
|
||
runs once per schedule even with several server processes, every run is recorded,
|
||
failed runs are retried twice, and a slow run never overlaps the next. Admin → Site
|
||
Settings → Background jobs shows each job's schedule, next run and last 10 runs, with
|
||
"Run now". Times are in `APP_TIMEZONE` (default Africa/Johannesburg). Change a schedule
|
||
with `JOB_SCHEDULE_<NAME>` (cron); `JOBS_ENABLED=false` turns them all off. If the queue
|
||
can't start, the same jobs run on in-process timers.
|
||
|
||
| Job | Schedule |
|
||
|-----|----------|
|
||
| Scheduled email/WhatsApp worker | Polls every 30 s (`SCHEDULED_EMAILS_INTERVAL_MS`) and purges sent jobs after 24 h (not on the queue) |
|
||
| `daily-summary` — daily event summary email | 07:00 daily (`DAILY_SUMMARY_ENABLED=false` to disable) |
|
||
| `waitlist` — expire unclaimed offers, offer freed places | Every 10 minutes (`WAITLIST_JOB_ENABLED=false`) |
|
||
| `payment-status` — look up PayFast checkouts (10 min – 3 days old) whose notification never arrived, and record them | Every 15 minutes (`PAYMENT_STATUS_JOB_ENABLED=false`) |
|
||
| `feedback` — post-event feedback forms and their 3-day reminder | Every 15 minutes (`FEEDBACK_JOB_ENABLED=false`) |
|
||
| `sessions` — session reminders, and certificates for events that ended in the last 14 days | Every 5 minutes (`SESSION_JOBS_ENABLED=false`) |
|
||
| `consent` — parental consent reminders, and deleting medical details after events | 09:00 daily (`CONSENT_JOBS_ENABLED=false`) |
|
||
| `privacy` — remind admins of privacy requests due within a week or overdue | 08:00 daily (`PRIVACY_JOBS_ENABLED=false`) |
|
||
| `analytics-rollup` — roll raw visit rows older than 90 days into daily totals | 04:15 daily (`ANALYTICS_ROLLUP_ENABLED=false`) |
|
||
| `backup` — database backup (`pg_dump`, gzipped, 14-day retention) | 02:00 daily (`BACKUP_ENABLED=false`) |
|
||
| `temp-cleanup` — temp files older than 7 days in `/temp` | 03:00 daily |
|
||
|
||
---
|
||
|
||
## Operations
|
||
|
||
- **PM2** (`ecosystem.config.js`) runs backend and frontend, restarting either
|
||
one above 500 MB of memory.
|
||
- **Sentry** error monitoring when `SENTRY_DSN` / `NEXT_PUBLIC_SENTRY_DSN` are
|
||
set.
|
||
- Unhandled promise rejections are logged and don't crash the server.
|
||
- Backend status page (`/`) and built-in API docs page (`/docs`).
|
||
- **Tests** — Jest unit tests in `backend/tests` (`npm test`), and Playwright
|
||
end-to-end tests in `e2e/` (`npm run test:e2e`) that start their own backend and
|
||
frontend against a separate, freshly seeded database: public pages, free and paid
|
||
registration through to check-in and cashup, multi-day check-in, the attendance
|
||
register and certificates, the event wizard, the kiosk, and the admin jobs page.
|
||
- **CI** — `.gitea/workflows/ci.yml` runs the backend tests, frontend lint/typecheck/
|
||
build and the end-to-end tests on every push (needs a Gitea Actions runner). A `v*`
|
||
tag also deploys over SSH with `scripts/deploy.sh` once the `DEPLOY_*` secrets are set:
|
||
it backs up the database, checks out the tag, migrates, builds and reloads PM2, and
|
||
goes back to the previous version if a step fails before the reload.
|
||
- Versioning via git tags and [CHANGELOG.md](./CHANGELOG.md).
|
||
|
||
---
|
||
|
||
## Page map
|
||
|
||
| Path | Who | What |
|
||
|------|-----|------|
|
||
| `/` | Public | Home |
|
||
| `/events`, `/events/[id]` | Public | Event list and detail |
|
||
| `/[alias]` | Public | Event short link |
|
||
| `/register/[eventId]` | Public / User | Registration |
|
||
| `/registration/success` | Public / User | Confirmation, form and pay |
|
||
| `/pay/[id]` | Public | Pay online: choose a provider, or go on to PayFast |
|
||
| `/payment/success`, `/payment/failure`, `/payment/cancel` | Public | Return pages after paying online |
|
||
| `/forms` | Public / User | Attendee forms |
|
||
| `/contact`, `/legal/terms`, `/legal/privacy`, `/lockdown-rules` | Public | Info pages |
|
||
| `/login`, `/register`, `/forgot-password`, `/reset-password`, `/activate-account` | Public | Accounts |
|
||
| `/setup` | First run | Setup wizard |
|
||
| `/consent/[token]` | Parent/guardian | Give or decline parental consent |
|
||
| `/waitlist/claim/[token]` | Public | Claim a waitlist place |
|
||
| `/certificates/verify/[code]` | Public | Check a completion certificate |
|
||
| `/unsubscribe?t=…` | Public | Unsubscribe from news (link in news emails/WhatsApps) |
|
||
| `/self-service` | Supervisor-unlocked | Kiosk |
|
||
| `/set-banner` | Supervisor | Site banner |
|
||
| `/dashboard/user` (+ `/payments`, `/donate`, `/forms`, `/profile`, `/privacy`, `/reset-password`) | User | My Events |
|
||
| `/dashboard/staff` (+ `/ticket-scanning`, `/event-tickets`) | Staff | Staff tools |
|
||
| `/dashboard/supervisor` (+ `/events`, `/event-options`, `/at-the-door`, `/manual`, `/manual-registration`, `/payments`, `/coupons`, `/waitlist`, `/attendance`, `/consent`, `/offline-scans`, `/allocations`, `/email-attendees`, `/whatsapp-attendees`, `/forms`, `/reports`) | Supervisor | Supervisor tools |
|
||
| `/dashboard/admin` (+ `/users`, `/registrations`, `/forms`, `/cashup`, `/cashup/[id]`, `/audit-log`, `/data-requests`, `/settings`) | Admin | Admin tools |
|