# 11-Mar-2026 — Lightweight Booking System, Maps cleanup unused components, mail & walks fixes [#175](https://github.com/nbarrett/ngx-ramblers/issues/175)

## [build 517](https://github.com/nbarrett/ngx-ramblers/actions/runs/22978121001) — [commit 6edae09](https://github.com/nbarrett/ngx-ramblers/commit/6edae09123092ead7878ccdb12c02afeba8e9e35)
_____

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](https://github.com/nbarrett/ngx-ramblers/issues/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.


![](https://ngx-ramblers.org.uk/api/aws/s3/site-content/d60b2793-41a8-42b8-a0d6-5c03cfbc30b4.jpeg)

- **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
1. Visitors enter their name and email (required) plus optional phone number
1. Additional attendees can be added (up to `maxGroupSize`, default 3)
1. Clicking **Book Now** creates a booking and confirms with a success message
1. The capacity counter updates in real time — once all places are taken the
panel shows **Fully booked**
1. An email is sent to the visitor and optionally the BCC address specified within the email configuration for Bookings.

![](https://ngx-ramblers.org.uk/api/aws/s3/site-content/3c174bb5-16b7-4832-b042-bd9cf74be0b3.jpeg)

![](https://ngx-ramblers.org.uk/api/aws/s3/site-content/23125588-0eb2-4e87-8dc8-ac77937b294e.jpeg)


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

![](https://ngx-ramblers.org.uk/api/aws/s3/site-content/437f5b31-ba12-4bd6-a224-756ed3ed3e08.jpeg)

![](https://ngx-ramblers.org.uk/api/aws/s3/site-content/464a11d2-8786-42b5-ad6a-447720df04e2.jpeg)

## 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 <rte> 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"`
