Admins (with 2FA) can view the site as a non-admin user for up to 30 minutes, with a required reason and a "Viewing as" banner. It is strictly read-only: a guard mounted before every route refuses any write made with an impersonation token, protect/optionalAuth check the session is live, and GET handlers skip the price-refresh writes. Start, end, reason and every page/API call (including blocked attempts) are audited; optional email to the user. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CP62L9JxHomA36aYP1ekCe
72 KiB
Changelog
All notable changes to this project are documented in this file.
The format is based on Keep a Changelog, and this project follows Semantic Versioning.
Unreleased
Added
- Impersonate user (Admin → Manage users → Impersonate): an admin can see the site exactly as a user does, to help with problems such as "my ticket isn't showing".
- A reason is required, and the session lasts at most 30 minutes.
- A bright "Viewing as [name]" banner with a countdown and "End impersonation" shows on every page.
- Impersonation is strictly read-only.
- A server-side guard, mounted before every route, refuses any change (registrations, payments, refunds, cancellations, profile, consent, messages and everything else) made with an impersonation token.
- Viewing pages doesn't trigger the price refresh it normally does.
- Admins can't be impersonated, and the admin must have two-factor authentication on.
- Impersonation is recorded in the audit log: start, end, reason, and every page and API call, including blocked attempts.
- The audit log page lists recent sessions with their full trail.
- New setting: email users when an admin views their account (Site Settings → Legal → Support access; off by default).
- New
/api/admin/impersonateendpoints.- Requires the
add_impersonationdatabase migration.
- Requires the
[1.22.0] - 2026-10-19
Added
- Registration funnel stats (first-party, no third-party analytics), showing where people drop off between seeing an event and paying.
- A new cookie notice asks visitors once. Only those who choose "Allow" are counted, using a random anonymous visit id.
- We record the event page view and the start of registration. The submit and paid stages come from the registration itself.
- Only the stage, the traffic source and the device type are stored. No name, IP address or User-Agent is kept.
- New report, Registration funnel: counts and conversion % per stage over a date range, a daily trend, and breakdowns by traffic source, device and ticket option.
- Selecting several events compares them.
- Sources come from
utm_sourcelinks and from short links, which now add?src=<alias>when redirecting.
- A "Conversion (past month)" tile on the supervisor and admin dashboards covers live events.
- Raw visit records are rolled up into daily totals after 90 days by a daily job (
ANALYTICS_ROLLUP_ENABLED=falseto turn it off). - The privacy policy's cookies section now describes these statistics.
- New
/api/analyticsendpoints.- Requires the
add_funnel_analyticsdatabase migration.
- Requires the
[1.21.0] - 2026-10-19
Added
- Post-event feedback, set up in a new Feedback step in the event wizard.
- Questions use the form builder.
- You pick when it's sent (the default is the morning after the event) and who gets it: everyone who checked in, or everyone registered.
- Each person gets a personal link by email (and WhatsApp where they use it).
- Anyone who hasn't answered gets one reminder 3 days later.
- Answers can be anonymous or named.
- "Send now" is available, and the questions lock once people start answering.
- New form field types: Rating (1–5 stars) and Multiple choice. They're used for feedback, and can also go on attendee forms (registration page, My Events, the kiosk, manual registration and Supervisor → Forms).
- New report, Feedback summary:
- response rate;
- average rating per question, with star distribution;
- multiple-choice and yes/no breakdowns as bar charts;
- a list of text answers (with names unless the form is anonymous).
- Select several events to compare their ratings (e.g. Camp 2025 vs Camp 2026). Exports to PDF and Excel like other reports.
- New
/api/feedbackendpoints and a job every 15 minutes (FEEDBACK_JOB_ENABLED=falseto turn it off).- Requires the
add_feedback_formsdatabase migration.
- Requires the
[1.20.0] - 2026-10-19
Added
- Sponsor a spot: a new event setting, "Allow sponsorships" (event wizard → Sponsor a spot).
- It adds a "Sponsor a spot" button on the event page so people can fund a place for someone who can't afford it.
- Sponsors choose one or more whole spots (at the main ticket price) or a custom amount, and pay through Yoco. Guests just give a name and email.
- Sponsors can give anonymously, or add a name and a message for the organisers.
- An optional goal ("12 of 20 spots sponsored") and public progress bar.
- Each sponsorship is recorded as a donation tagged to the event, in that event's sponsorship pool.
- Supervisors assign pool money to registrations with the existing donation assign/unassign flow, fully or partly.
- Under Payments → Donations, sponsorships are labelled, there's an "Only the sponsorship pool" filter, and the sponsor's message is shown.
- Who receives help is never shown to sponsors.
- Sponsors get a "Thank you for sponsoring a spot" receipt.
- Automations has a new "Thank Your Sponsors" email for events with sponsorships.
- Email attendees can target an event's sponsors (
filter.audience: 'sponsors').
- The Donations breakdown report has sponsorship pool columns per event: received, assigned and remaining.
- New
/api/sponsorshipsendpoints.- Requires the
add_sponsorshipsdatabase migration.
- Requires the
[1.19.0] - 2026-10-19
Added
- Rooms & groups (Supervisor → Rooms & groups), for camps and lockdowns: put attendees into rooms, cabins, small groups, teams or buses.
- Each event can have several allocation sets (e.g. "Rooms" and "Buses"). Each set has groups with an optional capacity, an optional gender restriction and an optional leader (a staff member).
- A drag-and-drop board puts people not yet placed on the left and groups, with capacity counters, on the right. Full groups and groups for the other gender won't accept a person.
- Auto-allocate fills groups evenly with everyone not yet placed, respecting capacity and gender. It can keep people on the same booking together or spread them out.
- Printable lists per group (with leader) and a master list. On youth events they can include medical and emergency details from consent forms; viewing those is recorded in the audit log.
- "Show on tickets" prints the group on each person's ticket and shows it in My Events.
- Email and WhatsApp attendees can be sent to one group ("Bus 2 leaves at 07:30"). Messages go to whoever booked for the people in it.
- Group leaders see their people under Staff → My groups. Medical details are shown to supervisors only.
- New event setting "Ask each person's gender" (under Named attendees). It adds a gender choice for each named person on the registration page, at the door, on the kiosk, in manual registration and in My Events, and is needed for gender-split rooms.
- New
/api/allocationsendpoints.- Requires the
add_room_allocationdatabase migration.
- Requires the
[1.18.0] - 2026-10-19
Added
- Ticket transfer. On My Events, "Transfer" hands a ticket to someone else before the event: enter their name and email or phone.
- The new holder gets the ticket by email/WhatsApp with a new QR code. If they're new, an inactive account is created and they get an activation link.
- The old QR code stops working, so screenshots of it won't scan. Scanning it says who the ticket was transferred to, and offline-mode sync flags it.
- For a ticket covering several people, one place is split off to the new holder.
- The payer stays responsible for the registration.
- Transfer rules:
- Only before the event starts, and only for tickets that haven't been scanned.
- It can be turned off per event (new "Ticket transfer" setting in the event wizard, on by default). Supervisors can always transfer, from At the door → Tickets.
- On youth events, the new holder's date of birth, and a parent's details for a minor, are needed, and a new consent request is sent.
- The previous holder's attendee form answers and consent medical details are removed.
- Every transfer is recorded in the audit log.
- New endpoints
POST /api/tickets/:id/transferandGET /api/tickets/:id/transfers.- Requires the
add_ticket_transferdatabase migration.
- Requires the
[1.17.0] - 2026-10-19
Added
- Offline ticket scanning. While online, staff pick an event and section on the scanner and tap "Enable offline mode". The section (or the whole event) is locked to their device and its tickets are downloaded to the device.
- With no connection, scans are checked against that list and queued on the device.
- The panel shows when the list was downloaded and how many scans are waiting.
- Queued scans sync automatically when the device is back online (or with "Sync now"), and check-in messages go out then.
- "Release section" ends offline mode and deletes the list from the device.
- Only one device can hold a section at a time (a whole-event lock clashes with every section). While a section is locked, other devices see who holds it and can't scan it, online or offline, so a ticket can never be redeemed twice by two offline devices.
- Syncing is idempotent and never redeems more than is left on a ticket. Anything that needs a person to look goes into a sync report: tickets not found, cancelled registrations, used-up tickets, scans outside the locked section, and check-ins without consent.
- Supervisor → Offline scanning page: active locks with force release (for a lost or flat phone; its scans still sync if it reconnects, and are flagged), and sync reports to review. Locks, releases, force-releases and syncs are recorded in the audit log.
- The scanner is installable as an app (web app manifest), and a service worker caches the scanner page and its scripts so it opens with no connection.
- The app now stays signed in with the last confirmed profile when it's reloaded without a connection, instead of sending the user to the login page.
- New
/api/scan-locksendpoints.- Requires the
add_offline_scanningdatabase migration.
- Requires the
Changed
POST /api/tickets/scan/:qrCodenow acceptssectionIdand checks it on the server; this was previously only checked in the browser.- It now refuses (423) scans in a section that another device has locked for offline scanning.
[1.16.0] - 2026-10-19
Added
- Waitlist for sold-out tickets. A new event setting (event wizard → Visibility) turns it on, with how long offered places are held (24 hours by default) and whether someone can be offered fewer places than they asked for.
- Sold-out tickets show "Join waitlist" on the event page, the register page and the events list. Guests can join on events that allow guest registration.
- When places free up, the next person is emailed/WhatsApped a claim link, and the places are held for them so nobody else can buy them. Places free up through a cancellation (by the person or staff), a smaller booking, or a raised stock limit.
- The claim link opens the register page with the places pre-selected, and works without logging in.
- Unclaimed offers expire (checked every 10 minutes) and move on to the next person.
- People can leave a waitlist from My Events, which now lists their waitlists and any places held for them.
- Supervisor → Waitlist page: the queue per event, with offer now (even over the stock limit), move up/down and remove.
- The daily summary email shows each event's waitlist count.
- New
/api/waitlistendpoints.POST /api/registrationsacceptswaitlistToken.- Requires the
add_waitlistdatabase migration.
- Requires the
Fixed
- Manual registrations (Manual registration, At the door, the kiosk) and adding places to an existing booking never checked stock, so either could take an option over its stock limit. Both are now checked like a normal registration.
- Stock is now counted in one place (
utils/stock.js) for registration checks,availableCountandisSoldOut, including places held for waitlist offers.
[1.15.0] - 2026-10-19
Added
- Parental consent for youth events. A new "Requires parental consent" event setting (event wizard → Visibility) has its own consent text and optional medical info, emergency contact and photo consent questions. It turns on named attendees automatically.
- Registering then needs a name and date of birth for every main-ticket place. Anyone under 18 on the event's start date needs a parent or guardian's name, email and phone.
- This applies on the registration page, manual registration, at the door, the kiosk, and when editing names in My Events.
- The parent gets an email and a WhatsApp (always both) with a link to a new consent page (
/consent/<token>). There they read the text, answer the optional questions, type their full name and confirm they're the parent or guardian, then give or decline.- The system records the time, IP, device, signed name, and the exact consent text and version shown.
- Medical and emergency details are stored encrypted.
- The registrant and the event's notification recipients are told the outcome.
- Tickets are still issued, but the scanner blocks check-in until consent is given, showing why. Supervisors can override, and the override is recorded in the audit log. On a youth event, a ticket with no named attendee counts as missing consent.
- A daily 09:00 job reminds unanswered requests after 2 days, then every 3 days until the event. It also deletes medical and emergency details a set number of days after the event (Site Settings → Legal, default 30).
CONSENT_JOBS_ENABLED=falseturns it off. - Supervisor → Parental consent page: consent status per attendee, resend one or all pending requests, re-request consent after the consent text changes, and a printable, confidential medical & emergency list. Every view of the list is recorded in the audit log.
- New reports: "Parental consent status" and "Medical & emergency list".
- My Events shows each attendee's consent status, with a "Resend consent" button.
- Anonymising or closing an account also clears that person's attendees' parent, date-of-birth, medical and emergency details.
- New endpoints under
/api/consent.GET /api/tickets/scan-preview/:qrCodenow returnsconsentStatus, andPOST /api/tickets/scan/:qrCodeacceptsoverrideConsent.- Requires the
add_parental_consentdatabase migration.
- Requires the
[1.14.0] - 2026-10-19
Added
- Group tickets (named attendees). A new "Named attendees" event setting (event wizard → Visibility) lets someone registering several people give each main-ticket place a name, with an optional email and phone. It's offered on the registration page, Manual registration, At the door and the self-service kiosk.
- Each named person gets their own single-person ticket, with their name on the ticket PDF, the scanner's confirm screen and the staff ticket list. Places left unnamed share one quantity ticket as before.
- The payer chooses whether every ticket comes to them or each person gets their own by email/WhatsApp; anyone without contact details falls back to the payer.
- The payer stays the account owner for billing, invoices and payments.
- An attendee whose email or phone matches an existing account is linked to it (never when the email and phone belong to two different accounts). Their ticket then appears in their own My Events, marked "Booked by …", and they can open and submit their own attendee form.
- My Events → registration popup has a new "People" section for adding or changing names and contact details, switching ticket delivery, and sending an attendee a link to fill in their own form (
/forms?registrationId=…&attendeeId=…). An attendee whose ticket has been scanned can't be removed. - Attendee forms now show one block per named person, labelled with their name, and each response is saved against that attendee.
- New endpoints
PUT /api/registrations/:id/attendeesandPOST /api/registrations/:id/attendees/:attendeeId/send-form-link.POST /api/registrationsand/manualacceptattendeesandticketDelivery.- Requires the
add_named_attendeesdatabase migration.
- Requires the
Changed
- Reducing the quantity of a main ticket removes the newest names beyond the new quantity.
- Check-in confirmations for a named ticket go to that person when the registration sends each person their own tickets.
- Unauthenticated guest registration now matches an existing account by phone as well as email, and is refused (409) when the email and phone belong to two different accounts, instead of silently reusing whichever account the email matched.
- Free registrations now issue tickets through the same generator as paid ones, instead of a separate copy of the logic.
Security
- Registration details (
GET /api/registrations/:id) hide attendees' email addresses and phone numbers from anyone other than the payer and staff, such as an attendee opening their form link.
[1.13.0] - 2026-10-19
Added
- Coupon codes. Supervisors create percentage, fixed-amount or free ("comp") codes under Supervisor → Coupon codes.
- Codes can be limited to particular events, ticket options or variants, a date window, total and per-person uses (one per person by default), a minimum number of tickets, and specific people, roles (e.g. Staff and above) or an email domain. Restricted codes need a logged-in account, so they can't be shared around.
- By default the better of the code and the early-bird price wins; a code can instead stack on top of early-bird pricing.
- People enter codes under "Have a code?" on the registration page and in My Events (for registrations not yet paid in full). Staff can apply them in Manual registration, At the door and the self-service kiosk.
- The discount is locked in when it's applied, the same way early-bird prices are. It appears as its own line on invoices, receipts, registration and payment emails, WhatsApp messages and the Yoco checkout.
- Editing a registration's items recalculates percentage and free codes; fixed-amount codes keep their amount. Cancelling a registration frees the use. A free code that covers everything marks the registration paid and sends the tickets.
- Use limits are enforced under a database lock, so the last available use can't be claimed twice.
- Staff price: a ticket option or variant can carry a staff price (event wizard → Items & pricing) that Staff and above pay automatically when it's lower than the normal price.
- New "Discounts (coupon codes)" report (Reports → Financial), showing uses and value given away per code and event. The Finance report's "What was sold" is now shown after discounts.
- Coupon activity in the admin audit log: codes created, edited and turned off, and codes applied or removed by staff.
- New endpoints:
POST /api/coupons/validate,GET|POST /api/coupons,GET|PUT /api/coupons/:id,GET /api/coupons/redemptions,POST|DELETE /api/registrations/:id/coupon.POST /api/registrationsand/manualacceptcouponCode.- Registration responses now include
discountTotalandsubtotalComputed. - Requires the
add_coupon_codesdatabase migration (npm run prisma:deployinbackend).
- Registration responses now include
Fixed
- Yoco checkout line items priced each item at its most recent price times the whole quantity. When a line spanned several price tranches (some tickets at early-bird, some at full price), the item lines didn't match the amount charged. They now list each price actually charged.
- The free-registration check on
POST /api/registrationsand/manualhad the same fault: it used the latest price for the whole quantity instead of the real total.
[1.12.0] - 2026-10-19
Added
-
Two-factor authentication is now required for Admin and Supervisor accounts. An admin or supervisor without it is sent a code at their next login (by email, or WhatsApp when SMTP isn't set up), and entering it turns two-factor on — the same code step as before, with a note explaining why. Trusted devices don't skip this first code. They can change whether codes come by email or WhatsApp from Profile & Security, but can no longer turn two-factor off (
POST /api/users/2fa/disablereturns 403 for them). -
Existing admin and supervisor sessions without two-factor are ended on their next request (
401withcode: "TWO_FACTOR_REQUIRED"), and the login page explains why. Promoting a user to supervisor or admin also ends their sessions if they don't have two-factor yet, so they enrol at their next login. An admin "Reset 2FA" for a supervisor/admin now means they set it up again at their next login. -
The requirement is skipped when no channel can deliver a code — no SMTP configured and no WhatsApp — so a fresh install (or the setup wizard, which also gets a short grace period) never locks the first admin out.
TWO_FACTOR_REQUIRED_ROLES=falseturns the requirement off as an emergency switch. -
FEATURES.md, a repo-root overview of everything the platform does — roles, public site, events, registration, payments, tickets, messaging, dashboards, cashup, reports, settings, security, background jobs, and a page map.
Changed
info.md(the overview for prospective churches) brought up to date with features added since it was written — donations, invoices and receipts, cashup and costs, profit reporting, audit trail, scheduled messaging, self-service kiosk, scan points, backups, branding, and privacy controls.
[1.11.0] - 2026-10-08
Added
- Optional two-factor authentication. Any user can turn it on from Profile & Security and choose to receive a 6-digit code by email or WhatsApp; the chosen channel is confirmed with a code before it takes effect. At login the code is asked for after the password, expires after 10 minutes and allows 5 attempts, with a "Resend code" option. If a WhatsApp code can't be sent, it falls back to the account's email address.
- "Trust this device for 30 days" on the code step, so a remembered device skips the code. Trusted devices are forgotten when the password is changed or reset, when sessions are signed out everywhere, and when two-factor is turned off.
- Admins can reset two-factor authentication for a user who can no longer receive codes ("Reset 2FA" on the Users page). Turning two-factor on or off shows in Account activity.
- The self-service kiosk asks supervisors with two-factor on for their code at setup. Changing the event now re-confirms the password against the existing kiosk session (
POST /api/users/verify-password) instead of logging in again.
Changed
POST /api/users/loginreturns{ twoFactorRequired, challengeId, method, destination }instead of a token for accounts with two-factor on; the token is issued by the newPOST /api/users/login/2fa. Accounts without two-factor are unaffected. Requires theadd_two_factordatabase migration (npm run prisma:deployinbackend).
[1.10.6] - 2026-10-08
Fixed
- The frontend production build (
next build) no longer hangs or fails when the backend isn't reachable. With noNEXT_PUBLIC_API_URL/API_URLset, the root layout fetched a relative/api/settingsURL that never settled during prerender, so every page timed out after 60 seconds; server-side fetches now fail fast when no API base is configured. The home page and the Events page also no longer fail the build when/api/eventscan't be reached — they build with an empty list and fill in on the first revalidation. At runtime a failed fetch still errors, so the last good page keeps being served.
Security
- Updated dependencies to clear
npm auditadvisories:express,multer,proxy-addr,qsand others in the backend,next(15.5.27) andsharp(0.35.5) in the frontend, andnodemailerfrom 9 to 10 (requires Node.js 20 or newer). The root package now overridesshell-quoteto a patched release, since everyconcurrentlyversion pins a vulnerable one. Backend production dependencies now report no known vulnerabilities; the remaining advisories are in development tooling (nodemon,jest,tailwindcss3) and have no non-breaking fix. - Dependency install scripts are now explicitly allow-listed via
allowScriptsin the backend and frontendpackage.json(Prisma,@sentry/cli,unrs-resolver), so npm no longer blocks them.
[1.10.5] - 2026-10-01
Fixed
- Events that have already started but not yet finished now show on the home page and the Events page. Both pages were hiding an event as soon as its start time passed; they now keep it listed until its end time, which matches what
GET /api/eventsalready returns.
[1.10.4] - 2026-09-01
Fixed
- "Add to calendar" (
GET /api/events/:id/ics) always failed with a 400 in production: theicslibrary'surlfield requires a scheme (https://...), butFRONTEND_URL/APP_BASE_URLwas configured without one, socreateEvent()rejected it before generating any output. A missing scheme is now assumed to behttps://instead of failing.
[1.10.3] - 2026-08-28
Fixed
- Creating an event was never recorded in the admin audit log — 1.10.0 added
event_createdas a logged action on the frontend's filter list, but the backend'screateEventnever actually calledlogAdminAction, onlyupdateEvent/deleteEventdid. Event creation is now logged the same way. - Staff cancelling a registration via
PUT /api/registrations/:id(the staff status-change endpoint, separate from the owner-facingDELETE /:idcancel route) wasn't logged at all — only theDELETEpath loggedregistration_cancelled. Both paths now log it, verified against all six audit categories promised in 1.10.0 (refunds, donation assign/unassign, manual registrations, staff-initiated cancellations, event create/update/delete, settings changes) with no other gaps found. - Added a "Back to dashboard" link to Admin → Audit log, matching the back-link pattern already used on the Cashup page.
[1.10.2] - 2026-08-28
Fixed
- Sentry wasn't instrumenting Express (
[Sentry] express is not instrumentedat startup):express,cors, and@prisma/clientwere required at the top ofbackend/src/index.jsbeforeSentry.init()ran, but Sentry's auto-instrumentation patches those modules via a require hook that only works ifSentry.init()runs first.Sentry.init()now runs immediately afterdotenv.config(), before any of the libraries it instruments are required.
[1.10.1] - 2026-08-28
Security
- Fixed a path-traversal vulnerability in event-image, branding (logo/favicon), and event-attachment uploads: the stored filename embedded the client-supplied
originalnameunsanitized, so a crafted filename (e.g. containing../) could write the uploaded file outside the intendedpublic/uploadssubfolder anywhere the server process could write. Uploaded files are now always saved under a server-generated random name; the original filename is preserved only as display metadata. express-rate-limitwas declared as a root-only dependency despite being required directly by the backend (backend/src/index.js) — a plaincd backend && npm install, as documented in the deployment guide, would not have installed it. It's now a properbackend/package.jsondependency.- Bumped
next(frontend) off a version affected by a critical unauthenticated RCE in the React Flight protocol (GHSA-9qr9-h5gf-34mp) and several other CVEs, and switched it from an exact pin to^15.5.24so future patch releases install automatically. - Bumped
multer,nodemailer,jsonwebtoken, anduuid(backend) to versions fixing DoS, SMTP/CRLF-injection, HMAC-verification, and buffer-bounds advisories; added anoverridesentry so the vulnerableuuidnested underexceljsis also patched. Rannpm audit fixacross all three workspaces (root/backend/frontend) — 0 known vulnerabilities remain. jwt.verify()now pinsalgorithms: ['HS256']explicitly rather than trusting the algorithm from the token header.- Uploaded assets served from
/uploadsnow getContent-Security-Policy: default-src 'none'; sandboxandX-Content-Type-Options: nosniff, so an uploaded SVG containing a<script>can no longer execute if opened directly.
[1.10.0] - 2026-08-28
Added
- Event detail pages now have an "Add to calendar" button, and registration confirmation emails include an "Add to calendar" link (requires
BACKEND_URLto be set), both downloading a.icsfile for the event. - SEO: the site now serves a
sitemap.xml(every public page plus every event) androbots.txt, and event/event-list pages have proper Open Graph/Twitter metadata for link previews. - Error monitoring via Sentry — set
SENTRY_DSN(backend) andNEXT_PUBLIC_SENTRY_DSN(frontend) to enable; a no-op otherwise. Captures unhandled backend errors/promise rejections and frontend errors. - Nightly local database backups (
pg_dump, gzipped, 02:00, 14-day retention by default) with a new Site Settings → Backups tab to view, manually trigger, and download them. Requirespg_dumpto be installed on the app server; setBACKUP_ENABLED=falseto disable. Local-disk only — not uploaded anywhere else. - Admin audit trail: refunds, donation assign/unassign, manual registrations, staff-initiated cancellations, event create/update/delete, and settings changes are now logged with who/when/what, viewable at Admin → Audit log.
- Starter backend test suite (Jest) covering the early-bird tranche pricing logic and donation-balance calculation — the two areas with the most complex money-handling history in this changelog.
[1.9.5] - 2026-08-27
Fixed
- The public event-alias route (
/:redirectUrl, used for short links like/camp-2025) is a catch-all matching any unmatched top-level path, so every bot/scanner probe for a nonexistent page (/wp-login.php,/.env, etc.) was hitting the backend and firing a live database query. Both the frontend route and theGET /api/events/by-alias/:redirectUrlendpoint now reject anything that isn't a plausible alias (letters/numbers/hyphens/underscores) before touching the database, instead of forwarding scanner noise straight through — this traffic pattern could exhaust the database connection pool and take the server down under load.
Added
- PM2 (
ecosystem.config.js) now restarts either process if it exceeds 500MB of memory, instead of letting an unbounded leak run until the OS OOM-kills it.
[1.9.4] - 2026-08-26
Added
- New
TRUST_PROXYbackend env var — set it when the app runs behind a reverse proxy (e.g. nginx on a separate server) so rate limiting reads the real client IP fromX-Forwarded-Forinstead of the proxy's. Accepts a hop count,true/false, or trusted proxy IP(s)/CIDR(s).
[1.9.3] - 2026-08-26
Fixed
- The server used to hard-crash (
process.exit(1)) on any unhandled promise rejection, so a single missed error handler anywhere in the app's fire-and-forget notification code (email/WhatsApp sending) could take the whole server down. It now logs the error and keeps running. - Accounts created on someone's behalf (at-the-door walk-in registration, or manual registration from the Admin/Supervisor dashboard) now get their activation link (email or WhatsApp, whichever they have) sent immediately when the account is created, instead of only on their first failed login attempt — matching what the Terms of Use already promised.
- Manual registration with a real email address used to create the account already active with a fixed, undisclosed password (
Hope123) — the visitor had no way to know it. That account is now created inactive and gets the same immediate activation link, so the visitor sets their own password — unless a password was supplied directly (see below), in which case it's activated immediately with no link needed. - The self-service kiosk's "Create an account" password field never actually worked — the account was always created with a different password behind the scenes, so visitors who set one couldn't log in with it. Manual registration now honours a caller-supplied password and activates the account immediately instead of discarding it.
- Removed the "Guest (no account)" checkboxes from the Manual Registration pages (both the current one and the legacy form) and the equivalent flag from the at-the-door kiosk — they stopped affecting backend behaviour once every walk-in account started being created inactive with an activation link. The self-service kiosk's own "Create an account" toggle still controls whether that link is sent, since that one is the visitor's own choice rather than staff acting on their behalf.
[1.9.2] - 2026-08-22
Fixed
- Contact-only events (no registration possible) were still getting the daily summary email — it now skips them since there's nothing to summarize.
[1.9.1] - 2026-08-22
Fixed
- The browser tab title and homepage "Welcome to..." heading always showed the app's built-in default name instead of the organisation name configured in Site Settings → Organisation.
- The backend's status page (
/) and API docs page (/docs) always showed "Cross Code Events" instead of the configured organisation name.
[1.9.0] - 2026-08-21
Added
- Events now have an optional Location field (address), defaulting to the organisation's configured address when creating a new event. Wherever an address is shown — event admin form, public event page, event card listings, the Contact page, and Site Settings → Organisation — there's now a "Directions"/"View on map" link, and the event detail and Contact pages also show an embedded Google Maps view (no API key required).
[1.8.0] - 2026-08-21
Added
- Events can now be marked "contact-only" (e.g. baptism) — they appear on the public events list/detail pages with a "Contact us" button (opening a popup with name/phone/email) instead of a Register button, and have no ticket options or registration flow. Configurable from a new toggle in the admin event wizard's Basic Details step.
- Registration and payment notifications now include a branded PDF: registration confirmations attach an invoice (itemized breakdown, early-bird discount, balance due, and — when there's an outstanding balance — a clickable "pay now" link and QR code pointing at a Yoco checkout) whenever the registration isn't fully paid, and payment/donation confirmations attach a payment receipt (itemized breakdown plus the amount paid on that transaction). Sent as an email attachment and, over WhatsApp, as the PDF itself (with the existing message text as its caption) in place of the previous text-only send. Both documents pick up the org's configured name/logo/brand color from Site Settings → Branding.
- The user dashboard's registration detail popup now has an "Invoice" button and, next to each payment, a "Receipt" button — both pop up a small Email/WhatsApp choice and (re)send that document on demand, via two new endpoints (
POST /api/registrations/:id/send-invoice,POST /api/payments/:id/send-receipt), restricted to the registration/payment's own owner. The invoice works regardless of balance, showing "Paid in full" when nothing is owed. The Payment history page (/dashboard/user/payments) got the same per-payment "Receipt" button too.
Fixed
- The user dashboard's "(early bird)" label was a price-comparison heuristic — it fired on any line priced below the option's base price, including plain cheaper variants that were never actually early-bird tickets. It now checks the real applied-tier flag the backend already tracks.
- Buying more of an already-purchased ticket type after its early-bird tier expired re-priced the entire line at the new price instead of adding the new quantity at the new price (e.g. 5 tickets @ R50 + 1 more after the price rose to R100 came out to R600 instead of R350). Each purchase now gets its own price "tranche" recorded against the registration line, so previously-bought tickets keep their original price and only the newly added quantity uses the current price. Registration/reporting pages that showed a single blended price per line now render (or total) each tranche separately.
- An unpaid (or partially paid) registration's price only ever got refreshed when a payment was actually attempted — an early-bird tier that expired while tickets sat unpaid kept showing its old, no-longer-honoured price (and its "(early bird)" tag) indefinitely on the dashboard until the user tried to pay. Viewing a registration (dashboard, registration detail, or an event's registration list) now refreshes still-outstanding pricing on the spot, same as payment already did.
- The user dashboard's registration detail popup listed one line per price tranche in raw creation order, so a ticket bought across several separate registrations (e.g. some early-bird, some not) showed as a wall of near-duplicate lines. It now merges tranches with the same item/price/tier into one line and groups early-bird lines together, separately from standard-price lines.
- Registration confirmation, payment/donation, and reminder emails and WhatsApp messages computed "Total due"/"Balance" by loading a registration without its price tranches, so any line spanning more than one tranche (e.g. some tickets bought at the early-bird price, more added later at full price) fell back to charging the entire quantity at the most recent tranche's price — silently wiping out the early-bird discount from the outstanding balance shown to the user. These sends now load tranches and total each line the same tranche-aware way the dashboard already did; the itemized line amounts in those messages (which previously always showed the option's undiscounted base price) are now correct too.
- Editing an event option's early-bird tiers (even just tweaking one tier's deadline or price) deleted and recreated every tier for that option with brand-new ids, including tiers the admin didn't touch. Since past purchases point at a specific tier by id, this silently severed that link on every edit — wiping the "early bird" attribution (and locked-in stock-limit counts, risking oversold tiers) off historical registrations that were never meant to change. Tiers are now upserted by id, so their identity — and everything referencing them — survives an edit; a tier is only ever removed if it's no longer in the saved list and has no purchase history against it.
[1.7.0] - 2026-08-20
Added
- Site Settings → Branding now supports a full Primary/Secondary/Accent brand color system (previously a single accent color that only recolored the org name in the navbar) applied site-wide — buttons, nav highlights, hover states, and links — plus a favicon upload alongside the existing logo upload, and a live preview panel (with website/email and desktop/mobile views) that updates as colors are picked, before saving. Selecting a logo now also suggests brand colors extracted from it. Outgoing emails' header and call-to-action buttons now pick up the Primary color too, while semantic alert colors (success/warning/danger) stay fixed. The setup wizard's Branding step got the same preview and color-suggestion treatment.
Fixed
- The setup wizard's logo/favicon upload was silently failing — by the time it ran, the admin account already existed and the upload endpoint required admin auth, but the request never attached the token. It now does, and a failed upload surfaces as a visible error instead of quietly leaving branding unset.
- A custom favicon saved via Site Settings never actually showed in the browser tab: the site had a static
favicon.icoin the special Next.jsapp/convention path, which generates its own<link rel="icon">tag that always took priority over the admin's saved one. Moved the static file topublic/so it's just the default fallback, not a competing tag. - Every unconfigured-organisation default across the app (org name, email subjects, WhatsApp messages, report file metadata, backend status/API-docs pages,
From:fallback address) said "Hope Events"/"Hope Family Church" instead of a neutral placeholder — now defaults to "Cross Code" (the platform vendor) consistently. Several email subject lines (login/welcome/activation/password-changed/account-closed) were also fully hardcoded regardless of the configured org name; they now interpolate it dynamically like the email body already did. - The optional legal settings (operator name, Information Officer name/email, website URL, effective date, and the Legal pages' own email fallback) defaulted to this project's real founder's name/email/church details — actively misleading if ever shown on another organisation's deployed site before they're configured. These now default to obviously-generic placeholders (e.g.
example.com,[not yet configured]) instead. - Added
SETTINGS.md, a repo-root reference documenting every setting's default behaviour when left unconfigured.
[1.6.0] - 2026-08-08
Added
- Supervisors can now unassign a donation that was previously applied to a registration, from a new "Assigned donations" list on the Payments page. This reverses the allocation (the registration's balance goes back up and the donation becomes available again), reverting the registration's status and revoking any tickets issued only because that allocation completed payment — blocked if a ticket has already been scanned. The registrant is notified by email/WhatsApp, mirroring the notification sent when a donation is first applied. The list can be searched (registrant, donor, or event) and filtered by event.
Fixed
- The internal admin notification for a donation applied to a registration read "Payment recorded" / "Registration payment", indistinguishable from a real incoming payment even though no new money changed hands — it now says "Donation applied" throughout, matching the registrant-facing email's distinct wording.
Performance
- Reconciling a Yoco card payment or sending a payment link on the supervisor Payments page no longer blocks the response on ticket-PDF generation and email/WhatsApp sends — these now run in the background, matching how manual payments already worked.
- Registration creation and payment capture now resolve per-option pricing, stock checks, and ticket generation concurrently instead of one option at a time.
- The supervisor Payments page no longer fires one request per registration to compute outstanding balances (that data was already included in the registrations response); post-action refreshes also run in parallel instead of sequentially.
- Added indexes for
Registration(createdAt, status),Ticket(createdAt), andPayment(originalPaymentId, isDonation)to speed up dashboard stats and donation-leg lookups.GET /api/registrationsnow supports optionalpage/limitpagination.
[1.5.4] - 2026-08-07
Fixed
- The homepage "Join Us" button sent logged-in users to the registration page instead of somewhere useful. It now shows "My Dashboard" and links to their dashboard when a user is already logged in.
[1.5.3] - 2026-08-07
Fixed
- Help guide popups could render taller than the screen on mobile, with no way to reach the close button or "Got it" button: only the tab content area had a height cap, so the header, quick links, and footer weren't accounted for. The whole popup is now capped to the screen height, with just the tab content scrolling internally.
[1.5.2] - 2026-08-07
Fixed
- Stat card labels on the Admin and Supervisor dashboards (e.g. "Registrations (past month)") were getting cut off mid-word on mobile, where a cramped 2-column grid left too little room for the text. The mobile layout now shows one card per row, and labels wrap onto two lines instead of truncating.
- The "Revenue trend" and "Top performing events" cards on the Admin and Supervisor dashboards could render outside the viewport on mobile: a wide events table (long titles, four columns) forced the containing grid column past the screen width instead of scrolling internally. Long event titles are now truncated in the table, and the page properly contains horizontal overflow.
- The navbar Logout button sat about 2px lower than the other nav links (Home/Events/Contact/Dashboard) because it was missing the same underline-spacing classes those links use.
Added
- Tab/mode switcher buttons on the Payments, At the door, Manual registration, Email attendees, WhatsApp attendees, and Cashup detail pages now show icons, matching the icons already used for the same tabs in each page's help guide.
[1.5.1] - 2026-08-06
Fixed
- Scheduling a WhatsApp message was silently sent as an email instead: the scheduled-job store never persisted the
channelfield, so the send worker always fell through to its email branch regardless of what was requested. Scheduled WhatsApp jobs now correctly send via WhatsApp. - Scheduled emails/WhatsApp messages showed no content when viewing or editing them in "Manage scheduled", even though the message existed in storage: the list endpoint never returned the message body (
html/textfor email,messagefor WhatsApp), and editing a scheduled WhatsApp message saved to the wrong payload field (textinstead ofmessage), so edits were silently lost. Both are now fixed, and the email list also shows a body preview like the WhatsApp one already did.
Added
- Scheduled emails/WhatsApp messages are now automatically purged from storage 24 hours after they're sent, instead of accumulating indefinitely.
- The "Manage scheduled" lists on the Email Attendees and WhatsApp Attendees pages now show who each scheduled job will be sent to, and each tab only shows jobs for its own channel (previously both tabs showed the same unfiltered list).
[1.5.0] - 2026-08-06
Added
-
Site redesign, phase 3: the fixed indigo/purple brand palette is now applied consistently across the whole site — every remaining page that still used the old ad-hoc indigo/blue colors (home, events, registration flow, auth pages, legal pages, payment result pages, and every Admin/Supervisor/Staff/User tool page) now uses the same design tokens as the redesigned dashboards. Deliberate exceptions (status badges like "Confirmed", multi-option selectors like Email/WhatsApp/Both, the WhatsApp role badge) were left alone since they're meaningfully distinct colors, not leftover brand color.
-
New help guides for account pages (login/register/forgot-password) and My Payments/Donate, replacing the generic fallback.
-
Site redesign, deep pass: beyond the phase 3 color sweep, the home page, events list, and event detail page were rebuilt with real layout upgrades (hero section, icon-chip headers, image placeholders, a sticky two-column tickets layout on event detail). Every Admin/Supervisor/Staff/User tool page now has an icon-chip header consistent with the dashboards, and the smaller public/auth pages (Login, Register, Forgot Password, Activate Account, Reset Password, Site Banner, Registration Success, Payment Success/Failure/Cancel, the standalone Attendee Forms page) were restyled to match, including wrapping the previously bare Attendee Forms page in the site's Navbar/Footer for the first time.
-
17 new dedicated help guides were added (home, events, event detail, and every remaining Admin/Supervisor/Staff/User tool page), so the whole site now has page-specific help content instead of falling back to the generic guide.
-
Site redesign, phase 1 (foundation): fixed indigo/purple design tokens wired into Tailwind and the previously-unstyled shadcn components (Button, Card, Badge, Table, Sheet, etc. were silently missing their CSS variables); a site-wide floating "Need help?" button, present on every page, showing a contextual guide with quick links (e.g. "Browse events", "View my tickets") — content is resolved per page via a new registry, with a general fallback (how to register / manage tickets) for any page without dedicated content yet.
-
Dashboard sidebar (My Events, Profile & Security, Admin, Site Settings) now also shows on
/dashboard/user/profileand/dashboard/admin/settings, not just each role's root page, and is now fixed in place so it no longer scrolls with page content. -
New shared components (
StatCard,QuickActionTile,AreaTrendChart) scaffolded for the upcoming dashboard redesign. -
Contact now has its own dedicated page (
/contact) instead of being a scroll-to section on the home page — same org email/phone/address, restyled to match the new design. -
The general (public) and My Events help guides now go into much more detail: the general guide explains guest vs. account-required registration with quick links to log in or create an account, and the My Events guide walks through registration status badges, attendee forms, early-bird pricing, editing/cancelling, bulk ticket actions, and the difference between paying a balance, payment history, and donations. The Admin, Supervisor, Staff, and Site Settings pages now have their own dedicated help guides too, instead of falling back to the general one.
-
Site redesign, phase 2 (dashboards): Admin and Supervisor dashboards now show a KPI row (active events, revenue, donations, registrations, tickets sold — each with a "vs last month" comparison), a revenue trend chart for the current month, and a top-performing-events table, backed by a new
GET /api/stats/overviewendpoint. Staff and My Events dashboards are visually reskinned onto the new design tokens only — no new financial data for Staff, by design. -
Admin/Supervisor dashboard stat tiles now link somewhere useful: Revenue/Donations/Tickets sold jump straight into the matching Reports report, pre-filtered to this month (new
?report=&range=this_monthdeep-linking support in Reports); Active events links to Manage events. -
Reports revenue trend chart now shows gridlines, axis labels, and a marker dot per data point, so a sparse day-or-two of data still renders as a visible chart instead of an empty-looking box.
-
WhatsApp API management moved from its own page into a new "WhatsApp" tab on Site Settings (
/dashboard/admin/settings?tab=whatsapp); the old/dashboard/admin/whatsapproute now just redirects there. -
Site Settings restyled to match the rest of the redesign (icon tab bar, card layout, brand colors).
-
My Events dashboard now shows event thumbnails on registrations, upcoming events, and tickets, with upcoming events and tickets laid out side by side.
-
Profile & Security page restyled into a two-column layout with a new "Account activity" section showing recent logins and password changes (device + timestamp), backed by a new append-only
SecurityEventlog andGET /api/users/activityendpoint. -
Help guide modal now animates in instead of appearing abruptly, and got a general visual polish pass (rounded corners, active-tab styling, hover states).
Fixed
- Admin/Supervisor dashboard "Revenue" figures (today/week/month) were overcounting: they didn't exclude donation-application "legs" (the money was already counted once via the original donation) the way Reports and the admin payments-stats endpoint already did. Both endpoints now use the same exclusion.
- Admin/Supervisor dashboard "Revenue" KPI, trend chart, and top-performing-events table were undercounting the other way — donations themselves weren't being counted as revenue at all, only tracked in the separate "Donations" figure. Donations now count toward revenue (donation-application legs are still excluded either way, since that money was already counted once via the original donation).
- Admin/Supervisor dashboard KPIs (revenue, donations, registrations, tickets sold) used a calendar-month-to-date window, which under-counted for most of the month (e.g. only 3 days' worth of data on the 3rd). They now use a rolling trailing-month window instead — today back one calendar month, e.g. 9 May through 8 June if today is 8 June — compared against the equal-length month before that. The "View report" links from those tiles now deep-link into the same trailing-month range.
- Admin dashboard was missing the "At the door" quick action that Supervisor already had (walk-in registration, payments, and ticket printing at the door) — added.
Changed
- Reports' "Reporting guide" is now powered by the new site-wide help system instead of a Reports-only modal — same content, opened from the floating help button instead of a sidebar button.
- Navbar/bottom nav link colors are now fixed to the site's indigo brand color instead of the admin-configurable accent color; the accent color setting now only affects the org name text next to the logo.
- "Contact" links across the nav and help content now point to
/contactinstead of/#contact. - Admin/Supervisor dashboard quick actions no longer list "Manage sections" or "Event tickets & printing" (redundant with the Events and At-the-door pages) or "Manage WhatsApp API" (moved into Site Settings). Staff keeps "Event tickets & printing" — their only other tool is ticket scanning, and they can't reach At-the-door.
- Admin dashboard no longer shows ticket-scanning stats ("Recent scans") — kept on Supervisor/Staff, where it's actually actionable.
- The old hand-rolled
Buttoncomponent (components/shared/Button.tsx) is gone — its one remaining caller now uses the standardcomponents/ui/button.tsx. - The site-wide help guide no longer opens itself automatically on first visit to a page — it only opens when the help button is clicked. The now-pointless "Don't show this again" checkbox was removed along with the per-page dismissal tracking it drove.
Removed
- Deleted dead code found while migrating the last
Buttonusage: an unusedEventFormcomponent, a stale pre-redesignReports.tsx, and a strayReportsV2_backup.tsx— none were imported anywhere.
[1.4.2] - 2026-08-06
Fixed
- Supervisor Payments page: the mode tabs (Payment/Refund/Donations/Reconcile/Payment Link) now wrap onto multiple lines on mobile instead of overflowing off-screen and becoming unreachable.
- Reports: the payments report and its "by method" breakdown/chart now bucket into the same 4 categories used everywhere else (Cash, Card, EFT, Other) instead of showing raw method strings — Apple Pay, Google Pay, and Yoco checkout-portal payments now fold into "Card".
- Cashup/Finance/Profit reports: refunds were being dropped entirely from the per-method breakdown (
paymentsByMethod) instead of netting against the method they were refunded against, so e.g. a card refund silently vanished instead of reducing the "Card" total — the per-method figures now correctly sum back to total revenue. - Refund method now nets against the correct bucket everywhere a payment's method is normalized for display/filtering (My Payments page, reports) —
card-refundetc. was falling through to "Other" instead of being recognized as a refund of its base method. - Supervisor Payments page: the Refund form's method dropdown now mirrors the actual payment methods (Cash/Card/EFT/Voucher refund) instead of offering an ambiguous generic "Refund" option that couldn't be attributed to any method bucket; it now auto-fills from the original payment's method when refunding a specific payment.
- Donations: refunding a donation (fully or partially) was inflating its remaining/unallocated balance by the refunded amount instead of reducing it, since the refund's negative amount was subtracted straight into the balance (subtracting a negative adds). Could let staff over-allocate a donation that had actually shrunk. Fixed in the donation-assignment leg totals used by the assign-donation endpoint, the Cashup/Finance/Profit reports' unallocated-donations figure, the Donations and Master Orders reports' Used/Unused breakdown, and the "Assign donation" panel on the Supervisor Payments page.
Added
- Supervisor Payments page: reconciling a Yoco transaction as a donation now lets staff optionally pick who the donation is from, instead of it always being attributed to whatever the checkout metadata (or the reconciling staff member) happened to resolve to.
[1.4.1] - 2026-08-05
Fixed
- Reports: on mobile, the report popup's close button now sits pinned beside the title instead of getting cut off inline with the Print/Email/Excel/WhatsApp buttons, which now have their own wrapping row below.
- Reports: on mobile, the page header's search box and Back button no longer run off-screen — they now sit on their own row and shrink to fit.
- Reports: the sidebar's custom date range inputs (From/To) are now stacked instead of side-by-side, fixing them overflowing outside the filter box on desktop.
1.4.0 - 2026-08-04
Added
- Admin Manage Users page: notification preference (Email/WhatsApp/Both) can now be viewed and edited directly from the user list, instead of only being self-editable from the user's own profile.
- Admin Registrations dashboard: added aggregate stat tiles (counts per status, total revenue, total outstanding) and per-registration paid/outstanding amounts, plus a "Payments" detail block per registration showing each payment's amount, method, date, and who recorded it.
- Reports: registration status breakdown now has a "Count by" toggle to switch between counting one per registration and counting by ticket quantity (so a registration with 3 tickets counts as 3).
- Reports: donations breakdown now shows Used/Unused amounts per event, reflecting the new donation-leg tracking below.
- Payment accountability: payments now record who recorded them (
recordedBy), separate from who they're for. Self-service/webhook payments record the payer as the recorder. Surfaced across the payments report, supervisor payments page, and the registrations dashboard. - Cashup: new "Payment accountability by staff member" section breaking down recorded payments per staff member for an event, by method (Cash/Card/EFT/Other) — cash also shows a live actual-vs-expected variance once staff enter physical denomination counts per person.
- Cashup: staff can now enter each other's actual cash denomination counts at any time (not required to close the event); the event's cash "actual" figure is the live sum of these per-person counts instead of one manual event-wide entry.
- Cashup: new "Report" tab presenting a clean read-only summary of the cashup, which opens automatically once an event is closed.
- Reports: complete redesign — a sidebar of universal filters (events, date range with presets, include past/inactive/closed events) that apply across whichever report is open, a searchable/categorized report card grid, and a popup viewer with report-specific filters and Print/Email/Excel/WhatsApp export actions. Includes a new in-app Reporting Guide.
- Reports: WhatsApp added as an export channel alongside Print, Email, and Excel — sends the report PDF to the current user's own WhatsApp number.
- Reports: visual pass on the report popup — colored stat tiles for key totals (Order Total, Paid, Paid via donations, Outstanding, Unassigned donations, etc.) and small bar charts (revenue by method, registration status, ticket usage, donations used/unused, registration types, income by method, net profit by event) added to most reports, plus search boxes on the Master Orders' Orders/Donations tables. The Reports page also hides the dashboard sidebar since it's a full-width workspace of its own.
- Reports: Master Orders Breakdown moved from "Orders" into the "Registration" category.
- Reports: the Reporting Guide's non-Overview tabs now use the same icon-card layout as Overview instead of plain bullet lists; "Need more help?" now points to the site administrator's email (admin@crosscode.co.za).
- Email/WhatsApp Attendees: new
{{payment.link}}placeholder that generates a live, per-recipient Yoco payment link for their outstanding balance at send time. - Reports: PDF exports (download, Print, Email, WhatsApp) now share a branded design matching the web report — a colored title band, the same colored stat tiles and bar chart shown on screen (where applicable), an explanatory note box, and a highlighted totals row in tables — instead of a plain black-and-white dump. Since Email and WhatsApp already reuse the same PDF renderer, both now send this styled PDF automatically.
- Reports: the "Excel" export now downloads a styled
.xlsxworkbook (colored header, stat rows, a chart rendered with a native data-bar, a bold colored table header, a highlighted totals row, and frozen header/auto-sized columns) instead of a plain CSV.
Changed
- Donations are no longer mutated when assigned to a registration — assignment now creates an immutable "leg" record referencing the original donation, so a partially-used donation keeps its original amount and remains assignable for its remaining balance instead of losing its history.
- Email Attendees / WhatsApp Attendees now respond immediately after queuing recipients and send in the background, instead of blocking the page until every message has been sent.
- Dashboard: the sidebar (My Events, Profile, Admin, etc.) is now hidden on every sub-page under Staff, Supervisor, and Admin — not just Reports — so those pages get the full width for their own content and navigation. It's still shown on each role's root landing page (
/dashboard/staff,/dashboard/supervisor,/dashboard/admin); the top navbar's "Dashboard" link always leads back there.
Fixed
- Reports/Cashup: fixed several places where money was double-counted once a donation was partially or fully assigned to a registration (e.g. a R250 donation with R50 assigned was showing as R300 received). Payments between dates, Revenue summary, Cashup reconciliation, Finance report, and Profit report now count each real inflow exactly once.
- Reports: Revenue Detailed and Master Orders Breakdown no longer attribute a donation-funded portion of an order to the registrant as if they'd paid it themselves — "Paid" now reflects only what the person actually paid directly, with the donation-covered amount broken out separately and attributed to the donor.
- Reports: Finance report was double-counting a registration's ticket value once under "what was sold" and again under a separate "Donations" line when the order was funded (even partially) by a donation.
- Reports: Master Orders Breakdown's "Donations made" table (donor, amount, used/unused) is now included in the PDF, Excel, Email, and WhatsApp exports — previously only the Orders table was exported and the donations breakdown was visible on screen only. Also fixed a PDF rendering bug where a table title following another table (e.g. "Donations made" below the Orders table) could render at the page's right edge instead of the left margin.
- Reports: "Email" export failed with
connect ECONNREFUSED 127.0.0.1:587— it built its own mail transporter directly fromEMAIL_HOST/etc. env vars instead of using the shared, DB-configurable SMTP settings (Admin → Site Settings) that the rest of the app already sends through, so it never picked up a working mail server. Now reuses the same shared mailer as tickets and account emails, with matching branded HTML styling. - Reports: "WhatsApp" export surfaced an unhelpful
Request failed with status code 500on failure. WhatsApp send errors now report the actual reason from the WhatsApp API, and a disconnected WhatsApp session is now detected and auto-recovered the same way it already is for other WhatsApp actions (previously only ticket/text sends had this handling — PDF sends did not). - Dashboard mobile: the Reports page's Events filter dropdown and the Email/WhatsApp Attendees recipient-picker dropdown could extend past the right edge of narrow screens instead of staying within the viewport; they now stretch to match their trigger's width like the other dropdowns in the app already do. The Cashup event-costs table on the Admin Cashup detail page now scrolls horizontally on narrow screens instead of squeezing its columns.
1.3.2 - 2026-08-03
Added
- Self-service kiosk: all password fields (supervisor sign-in, change event, and the visitor "Choose a password" field) now have a show/hide toggle button, so staff can verify what they've typed on the touchscreen instead of typing blind.
Fixed
- Self-service kiosk: removed the separate "Look up existing account" search field — for privacy, staff no longer type a visitor's email/phone into a dedicated search box. Instead, entering an email or phone number in the registration form itself (Email and Cell Number are now the first two fields, followed by Name) automatically checks for a matching account once that field is left.
- Self-service kiosk: matched accounts are no longer updated silently. If the operator's typed Name, Email, Cell Number, or "Send tickets via" preference differs from what's on file, a confirmation dialog now lists exactly what will change (old value → new value) and requires the operator to confirm before the account is updated.
- Self-service kiosk / manual registration: an existing account's name is now actually updated when confirmed changed (previously silently discarded), and email/phone corrections are applied even when the account already had a real value on file (previously only blank phone numbers or guest-placeholder emails could be replaced).
- Self-service kiosk: fixed a bug where changing the phone number to one belonging to a different account would silently replace the Name/Email fields with that other account's details, and re-editing the email back to the original value afterward would not re-check it — together this could result in a registration being (or looking like it would be) saved under the wrong account. Email and phone matches are now tracked independently; if they resolve to two different existing accounts, the kiosk shows a clear warning naming both accounts and blocks registration until the operator corrects one of the fields, instead of silently merging or overwriting details.
- Manual registration API: added a server-side check, independent of the kiosk UI, that rejects (
409) a registration whose submitted email and phone number belong to two different existing accounts — a defense-in-depth safeguard against one account's contact details being overwritten with, or hijacked by, another's. - Manual registration API: an existing account's notification preference is now validated against its final email/phone after any confirmed update (e.g. falls back off "WhatsApp"/"Both" if no valid phone remains, or onto "WhatsApp" if the email was cleared in favor of a real phone), instead of persisting a preference that no longer matches the account's actual contact info.
- Self-service kiosk: tapping anywhere else on the page (e.g. a ticket quantity +/− button) while Email or Cell Number was focused blurred that field and silently re-ran its account lookup; even though the match hadn't changed, this reset Name/Email/Phone/preference back to the matched account's original values, discarding any edits the operator had just made. The autofill now only applies once per distinct matched account instead of on every re-check.
1.3.1 - 2026-07-28
Fixed
- Self-service kiosk: closed events no longer appear in the event picker — only open events are selectable.
- Self-service kiosk: added a "Look up existing account" search field (by email or phone, triggered only by Enter or the Search button — never as-you-type) that autofills a returning visitor's name, contact details, and notification preference from their exact matching account, instead of requiring staff to re-enter details already on file.
- Self-service kiosk: when the account lookup finds no match, the typed query now carries over into whichever of Email/Phone it resembles (instead of being discarded), while Name and everything else resets blank for a fresh entry.
1.3.0 - 2026-07-27
Added
- At The Door: new "Check-In" tab (before "Tickets" in the tab order) for quickly redeeming a registration's Main Tickets by quantity (e.g. checking in 2 of 3 people on a booking) without scanning each QR code individually. Like the Payment tab, it only ever shows the currently opened registration — reached via the "Open" button, not a general search.
- Checking in a Main Ticket (via the new Check-In tab or the existing QR camera scanner) now automatically sends the ticket holder a confirmation email and WhatsApp message (respecting their notification preference) stating how many were checked in and how many remain.
Changed
- At The Door: the "Open" button on a registration now jumps to the Payment tab only when a balance is still outstanding; fully paid registrations jump straight to the new Check-In tab instead.
- At The Door: recording a payment that fully settles a registration's balance now jumps straight to the Check-In tab instead of generating and print-previewing a paper ticket — tickets are already emailed/WhatsApped to the attendee automatically once the registration is paid, so a physical print is no longer forced on this path.
1.2.0 - 2026-07-27
Added
- Supervisor dashboard:
/dashboard/supervisor/manualnow has a "Record Payment" tab (alongside "Register"), for capturing a payment without leaving the page. It pre-fills with the user and registration from the most recently created manual registration, but only switches tabs when a supervisor/admin clicks it themselves.
Changed
- Self-service payments ("Pay with Yoco" after registering, and "Make payment" on the user dashboard) now go straight to a Yoco checkout for the full remaining outstanding balance, instead of first showing a page to choose a custom/partial amount. Generating a partial-amount payment link remains available only from the supervisor/admin Payments dashboard.
Removed
/dashboard/user/pay— the self-service partial-payment page — has been removed; it's no longer linked to from anywhere in the app.
Fixed
/registration/successread the auth token from the wronglocalStoragekey (tokeninstead ofhope_events_token), which silently broke loading the registration/attendee-form data on that page and made the "Pay with Yoco" button fail with "Not authorized, token failed". Now uses the shared auth context, like the rest of the app.- User dashboard: clicking "Make payment" briefly showed a "Sending tickets…" loading dialog (borrowed from the ticket-email flow) instead of a payment-specific message.
1.1.0 - 2026-07-24
Added
- User dashboard: new "Payment history" page listing the user's own payments (donations excluded), with server-side pagination (25 per page), date range, method, and payment/refund filters.
- User dashboard: registration status (Pending/Confirmed/Partially Paid/Paid/Cancelled) is now shown as a colored badge, matching the existing event Closed/Past/Inactive badge convention, instead of a raw status string.
- Dashboard-wide: inline success/error/confirmation messages (e.g. after creating a manual registration on
/dashboard/supervisor/manual) now auto-dismiss after 7 seconds instead of persisting indefinitely, via a new shareduseDismissingStatehook. Applied consistently across all dashboard pages with this pattern; excluded are message-only modal dialogs (e.g. ticket-scanning's success/error confirmations, which still require a manual OK) and a couple of mixed validation/async error states shown inside actively-open forms (the registration-edit modal and the event create/edit modal), which continue to persist until the user acts.
Fixed
- Cashup/reports: payments tagged with a digital wallet method (e.g.
apple_pay,google_payfrom online checkouts) are now bucketed as "card" for reconciliation instead of silently falling into "other". - User dashboard payment history:
GET /api/payments/mypaymentsnow normalizesmethodto the fixed set cash/card/eft/voucher/other. Card-network wallet payments (apple_pay,google_pay) are reported and filterable as "card"; any other gateway-reported value falls under "other" — instead of exposing raw, inconsistent gateway strings the filter dropdown didn't know about. - User dashboard: the registration payment list (shown on the main dashboard when viewing a registration's bill) applies the same cash/card/eft/voucher/other normalization client-side, so it no longer shows a raw
apple_pay/google_paystring. Staff-facing payment views (supervisor payments, reports, cashup) are unaffected — they still show the raw method, which is what reconciliation needs.
1.0.1 - 2026-07-23
Added
- User dashboard: event titles now show a status badge (Closed / Past / Inactive, in that precedence) wherever they're listed.
Fixed
- User dashboard: registration-edit errors now show inside the edit popup instead of being hidden behind it.
- User dashboard: closed events are now hidden by default alongside past events (revealed via "Show past events"), and the Edit, Make payment, and Cancel registration actions no longer appear for closed or past registrations (also enforced server-side).
1.0.0 - 2026-07-23
Added
- Initial release of the Hope Family Church event management app (Next.js frontend + Express/Prisma backend).