11-Mar-2026 — Lightweight Booking System, Maps cleanup unused components, mail & walks fixes #175
Note that during the development of this ticket, it transpired that there was a sizeable amount of clean up work that was required following issue #12, as navigation from the walk summary → detail → edit and back was broken. The same issues were found with the same social event navigations. Several old components were deleted and the social-event* objects were renamed to group-event* in order to be more consistent with Ramblers data model.
Lightweight Booking System
Events can now accept bookings directly from any events page. When an event has bookingEnabled: true and a maxCapacity set on its fields, a Book a Place panel appears below the event details showing remaining capacity.
Admin — Booking (/admin/bookings)
To enable this feature, visit the above URL. A bookings button has also been added to the admin menu.

- Configuration tab: Enable Booking on Events and choose which event types are enabled for bookings, default max capacity, max attendees per booking and member priority.
- Per-Event Detail tab is the event-level booking admin view which lets you:
- select an upcoming or existing bookable event
- see each booking for that event in a table
- view attendee names, email addresses, phone numbers, booking date, status, and places booked
- spot waitlisted bookings separately from active ones
- delete individual bookings if needed
- download the selected event’s booking data as CSV
- It is also used to prepare events before bookings open:
- find an upcoming event from the selector
- set or update that event’s max booking capacity even if nobody has booked yet
- Select an event to see individual bookings with attendee
names, emails, phone numbers, and booking dates — also downloadable as CSV
- Admin can delete bookings directly from the detail view
- Summary tab: table of all bookable events with booked/capacity counts
Booking flow
- Visitors enter their name and email (required) plus optional phone number
- Additional attendees can be added (up to
maxGroupSize, default 3)
- Clicking Book Now creates a booking and confirms with a success message
- The capacity counter updates in real time — once all places are taken the
panel shows Fully booked
- An email is sent to the visitor and optionally the BCC address specified within the email configuration for Bookings.


Cancellation flow
- After booking, a Need to cancel a booking? link appears
- The visitor enters their email to look up active bookings
- Each matching booking can be individually cancelled
- Cancellation is verified server-side: the email must match an attendee
on the booking, preventing unauthorised cancellations
- An email is sent to the visitor and optionally the BCC address specified within the email configuration for Bookings.


Booking Follow-up Enhancements
Booking email notifications
- Booking workflows can now send confirmation, cancellation, waitlisted, and
restored emails using the configured booking notification template
- Booking emails include attendee lists, event dates, salutation, and a direct
link back to the website event page
- The event page link is passed from the current walk/group event view so the
email uses the same deep link already exposed in the UI
Booking configuration defaults
- Added booking settings to walks config with defaults for enabled state,
maximum capacity, maximum attendees per booking, and member priority days
- Added the walks config admin tab to manage those defaults centrally
- Updated the unreleased walks config migration to seed booking defaults and to
move notification configuration role copies from
ccRoles to bccRoles
Member priority and waitlist handling
- During member priority periods, authenticated member bookings can displace
non-member bookings onto the waiting list when an event is full
- Waitlisted bookings are restored automatically when places are freed by a
cancellation and restored attendees are notified by email
- Eligibility responses now expose member priority state and waiting list totals
Booking safeguards and lookup fixes
- Prevent duplicate active or waitlisted bookings on the same event by attendee
email address
- Fixed booking lookup responses to return API objects with
id, allowing
cancellation to work reliably after lookup
- Added explicit validation for missing booking ids on cancellation requests
- Cancellation entry points are available whenever an event already has bookings
Booking email configuration and workflow alignment
- Booking notification configurations now behave as workflow-managed email
configs instead of generic mailing-list selections
- Booking notifications now use BCC recipient roles consistently, with support
for migrated legacy
ccRoles data during the transition
- Booking emails now send through the shared transactional mail sender logic so
recipient handling, BCC behaviour, template substitution, and Brevo request
construction are maintained in one place
Walk and event view integration
- Booking panels now render correctly on standalone deep-link walk pages after
walks config has loaded
- Walk and group event views pass their canonical website links into booking
workflows so correspondence uses the same event URLs shown in related links
- Booking form layout was refined for stacked narrow layouts and single-row wide
layouts, with panel spacing aligned to surrounding event panels
Migration consistency
- Corrected booking-related config migrations to use the
config collection,
matching the live mongoose model and runtime code
Global booking configuration and admin controls
- Booking configuration is now stored in its own global
booking config rather
than under walks, with environment setup and the unreleased migration moved
to seed and migrate the new config cleanly before release
/admin/bookings now owns booking configuration through a dedicated
Configuration tab instead of the walks config area
- Booking configuration now supports per-event-type enablement so booking can
be turned on for walks, group events, and wellbeing walks independently
Per-event booking preparation
- Booking admins can now search upcoming events from the existing autocomplete
pattern without typing before results appear
- Upcoming event choices now include event type in the label for clarity
- Admins can set event-specific maximum capacity before any bookings exist,
allowing events to be prepared ahead of opening booking
Booking UX refinements
- Cancellation lookup guidance now uses the same alert treatment as the rest of
the booking panel
- Booking success and waiting-list messaging now use shared pluralisation
helpers for consistent wording
- Group event and walk views now own their own booking panel spacing so page-
specific layout tuning no longer leaks through the shared booking component
GPX Parser: Support Route Elements
- Root cause: GPX parser only handled
<trk>/<trkpt> (track)
elements but ignored <rte>/<rtept> (route) elements. 37 of 39
Sevenoaks walking route GPX files use <rte> format, so only 2
routes were rendering on the map
- Fix: added
parseRoute() and parseRoutePoints() methods to
convert <rte>/<rtept> elements into renderable GpxTrack objects
alongside existing <trk> parsing
- Waypoint dedup: changed
parseWaypoints() from selecting
"wpt, rtept" to just "wpt" — <rtept> elements are now parsed
as route tracks, not waypoints, preventing duplicate rendering
Map Viewport Hang Fix
- Root cause: Dragging/zooming the map triggered an infinite feedback
loop —
moveend → loadRoutes() → layer reassignment + fitBounds
→ Leaflet re-render → moveend again
- Debounced viewport handler: 300ms debounce on
moveend/zoomend
prevents rapid-fire reloads during panning
- Re-entrancy guard:
loadRoutesInProgress flag skips concurrent
loadRoutes() calls
- Suppress programmatic events:
suppressViewportHandler flag
prevents layer/bounds assignment from re-triggering the viewport
handler (cleared after 200ms)
- Skip fitBounds on viewport reloads:
loadRoutes(skipFitBounds)
parameter prevents calculateFitBounds on user-initiated pan/zoom,
which was the primary loop trigger
- Removed cache eviction on viewport change: the viewport filtering
already uses
currentBounds.intersects() on cached data, so deleting
and re-fetching GPX on every drag was unnecessary
Mail API Resilience (No API Key / Invalid Key)
- Short-circuit when no API key:
MailMessagingService.initialise()
checks mailConfig.apiKey after loading config — if absent, skips all
Brevo API calls (account, lists, folders, templates) and sets safe
empty defaults. Zero 401 errors in the console
- Short-circuit on account error: if the account query fails (invalid
key), remaining Brevo calls are skipped with empty defaults
- Senders list guard:
MailSendersListComponent waits for the
mailMessagingConfig via the events() observable before calling
senders/domain APIs — skips if accountError is set
- Create-or-amend sender guard: checks
brevoAccountConfigured()
before calling querySenders()
- Mail provider stats guard:
calculateMailProviderStats() handles
this.list being undefined when Brevo lists fail to load, showing
"No Brevo lists available" instead of crashing
- List key values null safety: added
?. and || [] fallback when
brevo.lists.lists is undefined
- Alert icon: changed error alert icon from
faCircleCheck to
faExclamationTriangle for the account error panel
CommonDataService Double HTTP Request Fix
- Root cause:
responseFrom() used .subscribe() + .toPromise()
on a shared observable, causing two subscriptions and duplicate HTTP
requests
- Fix: replaced with single
firstValueFrom() call — one
subscription, one HTTP request, proper error propagation via throw
Config Save: Allow Clearing Sensitive Fields
- Root cause:
restoreSensitiveFields() in the backend config
controller treated both missing keys AND falsy values ("", null)
as "restore from DB", making it impossible to clear the API key
- Fix: changed condition from
!(k in incoming) || !incoming[k] to
!(k in incoming) — only restores when the key is truly absent
(redacted), not when explicitly cleared
Mail Settings: Prevent Config Overwrite During Editing
- Root cause:
MailMessagingService.initialise() created new
subscriptions to committeeConfig and systemConfigService on every
call, causing accumulated subscriptions that re-emitted config and
overwrote unsaved edits
- Fix: moved one-time subscriptions to the constructor;
acceptNextConfigEmission flag in MailSettingsComponent prevents
subscription emissions from overwriting local edits except after
save or undo
Domain API URL Refactor
- Changed domain endpoints from path parameters
(
/domains/:domainName/configuration) to query parameters
(/domains/configuration?domainName=...) across backend routes and
frontend service
Walk Search Layout
- Alert inline when default filtering, own row with pagination/filters
- Clear filters resets to default preset
- Bug button hidden by default
Events
- Bug button visibility fix
Server API for #175
POST /api/database/booking — create booking
PUT /api/database/booking/:id — update booking
DELETE /api/database/booking/:id — delete booking
GET /api/database/booking/all — list bookings (with query criteria)
GET /api/database/booking/capacity/:eventId — public capacity check
POST /api/database/booking/lookup — find bookings by event + email
PUT /api/database/booking/cancel/:id — cancel with email verification
Contact Us Persistence
Contact form submissions are now persisted to the database via
ContactInteractionService and a new /api/database/contact-interaction
endpoint. Each interaction records: name, email, subject, message, whether
the sender was anonymous, the recipient role, and a status
(new/read/archived).
Social → Group Events Rename
All social event components, services, models, and routes have been renamed
from social-* to group-event-* to align with the Ramblers API naming
convention. Key renames:
SocialEditComponent → GroupEventEdit
SocialDisplayService → GroupEventDisplayService
SocialViewPage → GroupEventViewPage
SocialCard → GroupEventCard
social-events.model.ts → group-events.model.ts
social-auth-guard.ts → group-event-auth-guard.ts
Removed Orphaned Code
Deleted components and services no longer referenced anywhere:
WalkList (723 lines) — replaced by EventsFull CMS component
GroupEventList + sass
GroupEventViewSelector
SocialViewSelector
SocialList + sass
SocialRoutingModule
SocialPopulationLocalGuard
- Hardcoded
PAGE_HEADER and ACTION_BUTTONS anchors — walks and social
pages are now 100% content-managed
Event Navigation Fixes
Deep path dispatch (fixes wrong event for generic slugs)
For paths with 3+ segments (e.g. /walks/weekends-away/swanage/day-2),
page content is now checked before event lookup. This prevents generic
slugs like day-2 from regex-matching unrelated events. Two-segment paths
(e.g. /walks/some-event-slug) still try event lookup first.
Walk card title links (fixes pre-existing non-navigation)
walkRouterLink() now generates relative URLs so routerLinkUrl() returns
a valid path instead of null (absolute URLs were rejected as "remote")
- Added
stopPropagation on the title <a> to prevent the parent card's
toggleView() handler from intercepting the routerLink click
Edit/cancel navigation symmetry
close() strips /edit from the current path instead of reconstructing
the URL via groupEventLink(), which was reading the still-dirty URL
- Added explicit
edit and new routes to walks-routing.module.ts before
the ** catch-all to prevent /edit/edit/edit URL corruption
Cleanup
walksArea() simplified to urlService.area() — removed stale fallbacks
to pageService.walksPage()?.href and hardcoded "walks"