# 19-Apr-2026 — optional attendee emails, per-event booking scope, on-demand admin email dispatch and p... [#236](https://github.com/nbarrett/ngx-ramblers/issues/236)

## [build 616](https://github.com/nbarrett/ngx-ramblers/actions/runs/24630456820) — [commit dd5ec77](https://github.com/nbarrett/ngx-ramblers/commit/dd5ec77932ba989784518a23053eff51441217d6)

_____

Make booking attendee emails optional for all but the primary attendee, let
admins choose whether bookings apply to all events automatically or only to
specific events opted in one at a time, give admins a one-click control to
send emails of any type for an event without waiting for the overnight cron,
fix the booking email "event page" link to point at the local site, and
extract the email-preview pane into a single reusable component used by the
committee, group-event and bookings admin flows.

## What changes for end users

### Booking form (public)

- Only the first attendee now needs an email address. Seats 2..n only
need a name; the email field is hidden for those rows. Phone is still
offered for every attendee.
- Submitting a group booking without an email on the primary attendee
now gives a clear error instead of a generic failure.
- After cancelling a multi-seat booking, the confirmation now reads
"all your places have been released" so it is unambiguous that the
entire booking was cancelled, not just one seat.

### Booking emails (all types)

- The "View full event details on the event page" link now points at
the local group site (for example
https://kent.ngx-ramblers.org.uk/walks/<slug>) rather than
https://www.ramblers.org.uk/go-walking. Area is resolved from
systemConfig.group.pages (walks / social-events), slug from the
groupEvent URL or a kebab-cased title + start-date fallback.

### Admin Configuration tab

- When "Enable booking on events" is on, a new radio pair controls the
scope of bookings for the whole group:
- All events of the enabled types accept bookings automatically -
the historical behaviour.
- Per event
- only events with Bookings enabled ticked on the event
will accept bookings.
- Existing groups keep the All-events behaviour by default on first
load; no admin action required to maintain parity.

### Walk / Group Event editor

- New "Bookings enabled for this event" checkbox on the walk edit and
group-event edit forms, bound to the event's bookings-enabled flag.
In per-event scope this is the single source of truth for whether
the event accepts bookings.
- The Max Capacity field label no longer says "leave blank to disable
bookings" - capacity and opt-in are now two separate controls.

### Admin bookings screen

- Summary tab gains Group (name + code) and Status (Bookable / Not
bookable pill) columns.
- Per-Event Detail tab:
- Event picker renders each option as a two-line entry (event label
on top, muted group name + group code beneath) with a Bookable /
Not bookable badge on the right. The custom ng-select search
function matches across label, title, group name, group code,
date, time and status word.
- "Include past events" toggle filters the picker to upcoming events
by default (orphaned entries are always included for
reassignment).
- Inline "Bookings enabled for this event" checkbox and Max Capacity
input let admins flip per-event state without leaving the page;
saved via Save all changes.
- Per-event email overrides selector syncs to the URL via
email-type=<kebab-case-type>. The selected event id is also
persisted via event-id=<id> so the full state is deep-linkable.
- New Preview button next to the override selector renders the
resolved email (Brevo template applied) inline via the shared
EmailPreviewComponent.
- "Send reminder emails now" is replaced by a dynamic "Send
{type} emails now" button that dispatches the currently selected
email type (confirmation, cancellation, waitlisted, restored,
reminder) to the matching set of bookings (active, waitlisted or
cancelled) for the event. Reminder behaviour preserved.
- Orphaned-booking reassignment picker shows the same group + bookable
information and now only lists bookable targets.

## What changes under the hood

### Attendee model

- BookingAttendee type loosened: email and phone are now explicit
string | null, and everything except displayName is optional.
- Mongo schema drops required: true on attendee email.

### Per-event booking opt-in

- ExtendedFields gains bookingsEnabled?: boolean, persisted via a new
boolean field on the Mongoose extended-group-event schema.
- BookingSummaryRow carries groupName, groupCode, eventStartDateTime,
upcoming and bookable fields so the admin UI can badge, filter and
search each row without recomputing.

### Booking scope model

- New BookingScope enum (ALL_EVENTS, PER_EVENT) and BookingConfig.scope
field. Defaults to ALL_EVENTS in service defaults, environment-setup
template, and the server-side legacy merge, so unset configs behave
exactly as before.
- New helpers in booking-config.model.ts:
- bookingScope(config)
- resolves scope with a safe default.
- effectiveMaxCapacityForEvent(config, event)
- returns the event's
explicit max capacity, falling back to the config default.
- eventOptedInToBookings(config, event)
- returns true under
ALL_EVENTS scope, otherwise returns event.fields.bookingsEnabled.
- bookingEnabledForEvent(config, event)
- combines the event-type
check, the scoped opt-in check, and the effective-capacity check
into one predicate used by both the public booking form and the
server create endpoint.

### Shared email preview component

- New EmailPreviewComponent at
projects/ngx-ramblers/src/app/modules/common/email-preview. Owns
the iframe, blob URL lifecycle, ResizeObserver/MutationObserver
driven content observation, loading and error states, and the
render() / clear() / showError() public API. Consumers pass a
TemplateRenderRequest to render() and the component calls
MailService.renderTemplate under the hood.
- committee-send-notification and group-event-send-notification-modal
both refactored to drop their inline iframe + previewUrl + observer
machinery (each shed ~100 lines of duplicated preview state) and
now embed <app-email-preview #emailPreview/>, calling
emailPreview.render(request) in their existing refreshPreview()
methods.

### Booking email request builder

- New buildBookingEmailRequest(emailType, bookingRecord, event,
suppliedEventLink) in send-booking-email.ts returns the full
rendered template request (templateId, params, body, sender,
subject, etc). sendBookingNotification() now reduces to a thin
wrapper that builds the request and calls
sendTransactionalEmailRequest.
- publicEventLink() takes systemCfg and produces a local group URL as
described above.

### Send-by-type server plumbing

- New sendEmailsByTypeForEvent(eventId, emailType) in
booking-reminder-job.ts dispatches confirmation, cancellation,
waitlisted, restored or reminder emails to the matching booking
statuses for an event. Reminder type delegates to the existing
sendReminderEmailsForEvent.
- New booking controller handlers:
- sendEmailsByType on POST /booking/send-emails/:emailType/:eventId
(authenticated) - returns the standard BookingReminderDispatch
shape.
- previewEmail on GET /booking/preview-email/:emailType/:eventId
(authenticated) - loads a sample booking (real or synthesised),
builds the template render request via buildBookingEmailRequest
and returns {templateId, htmlContent, params, subject} for the
client to feed into EmailPreviewComponent.

### Server-side attendee normalisation

- New normaliseBookingAttendees helper in controllers/booking.ts
trims each attendee's display name / email / phone, nulls blanks, and
drops rows without a display name.
- Duplicate-email detection and attendee-email list helpers filter out
null/undefined emails before deduping.
- cancel() attendee match uses a shared bookingAttendeeEmail helper so
a null email can never match.
- create() returns a 400 when the first attendee has no email.

### Reminder dispatch plumbing

- New BookingReminderDispatch model:
{ eventId, eventTitle, sentCount, alreadySentCount, skippedCount }.
- server/lib/cron/booking-reminder-job.ts refactored: the per-event
reminder block becomes an exported sendReminderEmailsForEvent that
the cron and the admin button share.
- Brevo attendee-list markdown renderer handles attendees with no
email (renders - Name rather than - Name (undefined)).

### URL-persisted admin state

- StoredValue gains EMAIL_TYPE = "email-type" and EVENT_ID =
"event-id" entries (kebab-case values). Per-Event Detail tab pushes
and reads both via queryParamsHandling: "merge", so the email-type
section toggle, selected event, and active tab together deep-link
to the exact same view.

### Salutation de-duplication (#235)

- BookingConfig gains templatesIncludeSalutation (default true) so the
Brevo layout stops rendering ADDRESS_LINE when the markdown template
already opens with "Hi {{ATTENDEE_NAME}},". Templates themselves are
left untouched. Admin Configuration tab exposes the flag as a
checkbox, and both the frontend normalise step and the backend
loadBookingConfig default the flag to true when the DB value is
missing, so existing installs get the fix without a schema backfill.