Files
hope-events/CHANGELOG.md
T
2026-08-07 01:16:53 +02:00

30 KiB
Raw Permalink Blame History

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

[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 channel field, 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/text for email, message for WhatsApp), and editing a scheduled WhatsApp message saved to the wrong payload field (text instead of message), 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/profile and /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/overview endpoint. 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_month deep-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/whatsapp route 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 SecurityEvent log and GET /api/users/activity endpoint.

  • 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 /contact instead 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 Button component (components/shared/Button.tsx) is gone — its one remaining caller now uses the standard components/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 Button usage: an unused EventForm component, a stale pre-redesign Reports.tsx, and a stray ReportsV2_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-refund etc. 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 .xlsx workbook (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 from EMAIL_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 500 on 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/manual now 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/success read the auth token from the wrong localStorage key (token instead of hope_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 shared useDismissingState hook. 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_pay from online checkouts) are now bucketed as "card" for reconciliation instead of silently falling into "other".
  • User dashboard payment history: GET /api/payments/mypayments now normalizes method to 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_pay string. 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).