{"id":"69e4df7bef720ae39668a9ea","title":"2026 04 19 Issue 236","path":"how-to/committee/release-notes/2026-04-19-issue-236","contentMarkdown":"# 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)\n\n## [build 616](https://github.com/nbarrett/ngx-ramblers/actions/runs/24630456820) — [commit dd5ec77](https://github.com/nbarrett/ngx-ramblers/commit/dd5ec77932ba989784518a23053eff51441217d6)\n\n_____\n\nMake booking attendee emails optional for all but the primary attendee, let\nadmins choose whether bookings apply to all events automatically or only to\nspecific events opted in one at a time, give admins a one-click control to\nsend emails of any type for an event without waiting for the overnight cron,\nfix the booking email \"event page\" link to point at the local site, and\nextract the email-preview pane into a single reusable component used by the\ncommittee, group-event and bookings admin flows.\n\n## What changes for end users\n\n### Booking form (public)\n\n- Only the first attendee now needs an email address. Seats 2..n only\nneed a name; the email field is hidden for those rows. Phone is still\noffered for every attendee.\n- Submitting a group booking without an email on the primary attendee\nnow gives a clear error instead of a generic failure.\n- After cancelling a multi-seat booking, the confirmation now reads\n\"all your places have been released\" so it is unambiguous that the\nentire booking was cancelled, not just one seat.\n\n### Booking emails (all types)\n\n- The \"View full event details on the event page\" link now points at\nthe local group site (for example\nhttps://kent.ngx-ramblers.org.uk/walks/<slug>) rather than\nhttps://www.ramblers.org.uk/go-walking. Area is resolved from\nsystemConfig.group.pages (walks / social-events), slug from the\ngroupEvent URL or a kebab-cased title + start-date fallback.\n\n### Admin Configuration tab\n\n- When \"Enable booking on events\" is on, a new radio pair controls the\nscope of bookings for the whole group:\n- All events of the enabled types accept bookings automatically -\nthe historical behaviour.\n- Per event\n- only events with Bookings enabled ticked on the event\nwill accept bookings.\n- Existing groups keep the All-events behaviour by default on first\nload; no admin action required to maintain parity.\n\n### Walk / Group Event editor\n\n- New \"Bookings enabled for this event\" checkbox on the walk edit and\ngroup-event edit forms, bound to the event's bookings-enabled flag.\nIn per-event scope this is the single source of truth for whether\nthe event accepts bookings.\n- The Max Capacity field label no longer says \"leave blank to disable\nbookings\" - capacity and opt-in are now two separate controls.\n\n### Admin bookings screen\n\n- Summary tab gains Group (name + code) and Status (Bookable / Not\nbookable pill) columns.\n- Per-Event Detail tab:\n- Event picker renders each option as a two-line entry (event label\non top, muted group name + group code beneath) with a Bookable /\nNot bookable badge on the right. The custom ng-select search\nfunction matches across label, title, group name, group code,\ndate, time and status word.\n- \"Include past events\" toggle filters the picker to upcoming events\nby default (orphaned entries are always included for\nreassignment).\n- Inline \"Bookings enabled for this event\" checkbox and Max Capacity\ninput let admins flip per-event state without leaving the page;\nsaved via Save all changes.\n- Per-event email overrides selector syncs to the URL via\nemail-type=<kebab-case-type>. The selected event id is also\npersisted via event-id=<id> so the full state is deep-linkable.\n- New Preview button next to the override selector renders the\nresolved email (Brevo template applied) inline via the shared\nEmailPreviewComponent.\n- \"Send reminder emails now\" is replaced by a dynamic \"Send\n{type} emails now\" button that dispatches the currently selected\nemail type (confirmation, cancellation, waitlisted, restored,\nreminder) to the matching set of bookings (active, waitlisted or\ncancelled) for the event. Reminder behaviour preserved.\n- Orphaned-booking reassignment picker shows the same group + bookable\ninformation and now only lists bookable targets.\n\n## What changes under the hood\n\n### Attendee model\n\n- BookingAttendee type loosened: email and phone are now explicit\nstring | null, and everything except displayName is optional.\n- Mongo schema drops required: true on attendee email.\n\n### Per-event booking opt-in\n\n- ExtendedFields gains bookingsEnabled?: boolean, persisted via a new\nboolean field on the Mongoose extended-group-event schema.\n- BookingSummaryRow carries groupName, groupCode, eventStartDateTime,\nupcoming and bookable fields so the admin UI can badge, filter and\nsearch each row without recomputing.\n\n### Booking scope model\n\n- New BookingScope enum (ALL_EVENTS, PER_EVENT) and BookingConfig.scope\nfield. Defaults to ALL_EVENTS in service defaults, environment-setup\ntemplate, and the server-side legacy merge, so unset configs behave\nexactly as before.\n- New helpers in booking-config.model.ts:\n- bookingScope(config)\n- resolves scope with a safe default.\n- effectiveMaxCapacityForEvent(config, event)\n- returns the event's\nexplicit max capacity, falling back to the config default.\n- eventOptedInToBookings(config, event)\n- returns true under\nALL_EVENTS scope, otherwise returns event.fields.bookingsEnabled.\n- bookingEnabledForEvent(config, event)\n- combines the event-type\ncheck, the scoped opt-in check, and the effective-capacity check\ninto one predicate used by both the public booking form and the\nserver create endpoint.\n\n### Shared email preview component\n\n- New EmailPreviewComponent at\nprojects/ngx-ramblers/src/app/modules/common/email-preview. Owns\nthe iframe, blob URL lifecycle, ResizeObserver/MutationObserver\ndriven content observation, loading and error states, and the\nrender() / clear() / showError() public API. Consumers pass a\nTemplateRenderRequest to render() and the component calls\nMailService.renderTemplate under the hood.\n- committee-send-notification and group-event-send-notification-modal\nboth refactored to drop their inline iframe + previewUrl + observer\nmachinery (each shed ~100 lines of duplicated preview state) and\nnow embed <app-email-preview #emailPreview/>, calling\nemailPreview.render(request) in their existing refreshPreview()\nmethods.\n\n### Booking email request builder\n\n- New buildBookingEmailRequest(emailType, bookingRecord, event,\nsuppliedEventLink) in send-booking-email.ts returns the full\nrendered template request (templateId, params, body, sender,\nsubject, etc). sendBookingNotification() now reduces to a thin\nwrapper that builds the request and calls\nsendTransactionalEmailRequest.\n- publicEventLink() takes systemCfg and produces a local group URL as\ndescribed above.\n\n### Send-by-type server plumbing\n\n- New sendEmailsByTypeForEvent(eventId, emailType) in\nbooking-reminder-job.ts dispatches confirmation, cancellation,\nwaitlisted, restored or reminder emails to the matching booking\nstatuses for an event. Reminder type delegates to the existing\nsendReminderEmailsForEvent.\n- New booking controller handlers:\n- sendEmailsByType on POST /booking/send-emails/:emailType/:eventId\n(authenticated) - returns the standard BookingReminderDispatch\nshape.\n- previewEmail on GET /booking/preview-email/:emailType/:eventId\n(authenticated) - loads a sample booking (real or synthesised),\nbuilds the template render request via buildBookingEmailRequest\nand returns {templateId, htmlContent, params, subject} for the\nclient to feed into EmailPreviewComponent.\n\n### Server-side attendee normalisation\n\n- New normaliseBookingAttendees helper in controllers/booking.ts\ntrims each attendee's display name / email / phone, nulls blanks, and\ndrops rows without a display name.\n- Duplicate-email detection and attendee-email list helpers filter out\nnull/undefined emails before deduping.\n- cancel() attendee match uses a shared bookingAttendeeEmail helper so\na null email can never match.\n- create() returns a 400 when the first attendee has no email.\n\n### Reminder dispatch plumbing\n\n- New BookingReminderDispatch model:\n{ eventId, eventTitle, sentCount, alreadySentCount, skippedCount }.\n- server/lib/cron/booking-reminder-job.ts refactored: the per-event\nreminder block becomes an exported sendReminderEmailsForEvent that\nthe cron and the admin button share.\n- Brevo attendee-list markdown renderer handles attendees with no\nemail (renders - Name rather than - Name (undefined)).\n\n### URL-persisted admin state\n\n- StoredValue gains EMAIL_TYPE = \"email-type\" and EVENT_ID =\n\"event-id\" entries (kebab-case values). Per-Event Detail tab pushes\nand reads both via queryParamsHandling: \"merge\", so the email-type\nsection toggle, selected event, and active tab together deep-link\nto the exact same view.\n\n### Salutation de-duplication (#235)\n\n- BookingConfig gains templatesIncludeSalutation (default true) so the\nBrevo layout stops rendering ADDRESS_LINE when the markdown template\nalready opens with \"Hi {{ATTENDEE_NAME}},\". Templates themselves are\nleft untouched. Admin Configuration tab exposes the flag as a\ncheckbox, and both the frontend normalise step and the backend\nloadBookingConfig default the flag to true when the DB value is\nmissing, so existing installs get the fix without a schema backfill.","contentHtml":"<h1>19-Apr-2026 — optional attendee emails, per-event booking scope, on-demand admin email dispatch and p... <a href=\"https://github.com/nbarrett/ngx-ramblers/issues/236\">#236</a></h1>\n<h2><a href=\"https://github.com/nbarrett/ngx-ramblers/actions/runs/24630456820\">build 616</a> — <a href=\"https://github.com/nbarrett/ngx-ramblers/commit/dd5ec77932ba989784518a23053eff51441217d6\">commit dd5ec77</a></h2>\n<hr>\n<p>Make booking attendee emails optional for all but the primary attendee, let\nadmins choose whether bookings apply to all events automatically or only to\nspecific events opted in one at a time, give admins a one-click control to\nsend emails of any type for an event without waiting for the overnight cron,\nfix the booking email &quot;event page&quot; link to point at the local site, and\nextract the email-preview pane into a single reusable component used by the\ncommittee, group-event and bookings admin flows.</p>\n<h2>What changes for end users</h2>\n<h3>Booking form (public)</h3>\n<ul>\n<li>Only the first attendee now needs an email address. Seats 2..n only\nneed a name; the email field is hidden for those rows. Phone is still\noffered for every attendee.</li>\n<li>Submitting a group booking without an email on the primary attendee\nnow gives a clear error instead of a generic failure.</li>\n<li>After cancelling a multi-seat booking, the confirmation now reads\n&quot;all your places have been released&quot; so it is unambiguous that the\nentire booking was cancelled, not just one seat.</li>\n</ul>\n<h3>Booking emails (all types)</h3>\n<ul>\n<li>The &quot;View full event details on the event page&quot; link now points at\nthe local group site (for example\n<a href=\"https://kent.ngx-ramblers.org.uk/walks/\">https://kent.ngx-ramblers.org.uk/walks/</a><slug>) rather than\n<a href=\"https://www.ramblers.org.uk/go-walking\">https://www.ramblers.org.uk/go-walking</a>. Area is resolved from\nsystemConfig.group.pages (walks / social-events), slug from the\ngroupEvent URL or a kebab-cased title + start-date fallback.</li>\n</ul>\n<h3>Admin Configuration tab</h3>\n<ul>\n<li>When &quot;Enable booking on events&quot; is on, a new radio pair controls the\nscope of bookings for the whole group:</li>\n<li>All events of the enabled types accept bookings automatically -\nthe historical behaviour.</li>\n<li>Per event</li>\n<li>only events with Bookings enabled ticked on the event\nwill accept bookings.</li>\n<li>Existing groups keep the All-events behaviour by default on first\nload; no admin action required to maintain parity.</li>\n</ul>\n<h3>Walk / Group Event editor</h3>\n<ul>\n<li>New &quot;Bookings enabled for this event&quot; checkbox on the walk edit and\ngroup-event edit forms, bound to the event&#39;s bookings-enabled flag.\nIn per-event scope this is the single source of truth for whether\nthe event accepts bookings.</li>\n<li>The Max Capacity field label no longer says &quot;leave blank to disable\nbookings&quot; - capacity and opt-in are now two separate controls.</li>\n</ul>\n<h3>Admin bookings screen</h3>\n<ul>\n<li>Summary tab gains Group (name + code) and Status (Bookable / Not\nbookable pill) columns.</li>\n<li>Per-Event Detail tab:</li>\n<li>Event picker renders each option as a two-line entry (event label\non top, muted group name + group code beneath) with a Bookable /\nNot bookable badge on the right. The custom ng-select search\nfunction matches across label, title, group name, group code,\ndate, time and status word.</li>\n<li>&quot;Include past events&quot; toggle filters the picker to upcoming events\nby default (orphaned entries are always included for\nreassignment).</li>\n<li>Inline &quot;Bookings enabled for this event&quot; checkbox and Max Capacity\ninput let admins flip per-event state without leaving the page;\nsaved via Save all changes.</li>\n<li>Per-event email overrides selector syncs to the URL via\nemail-type=<kebab-case-type>. The selected event id is also\npersisted via event-id=<id> so the full state is deep-linkable.</li>\n<li>New Preview button next to the override selector renders the\nresolved email (Brevo template applied) inline via the shared\nEmailPreviewComponent.</li>\n<li>&quot;Send reminder emails now&quot; is replaced by a dynamic &quot;Send\n{type} emails now&quot; button that dispatches the currently selected\nemail type (confirmation, cancellation, waitlisted, restored,\nreminder) to the matching set of bookings (active, waitlisted or\ncancelled) for the event. Reminder behaviour preserved.</li>\n<li>Orphaned-booking reassignment picker shows the same group + bookable\ninformation and now only lists bookable targets.</li>\n</ul>\n<h2>What changes under the hood</h2>\n<h3>Attendee model</h3>\n<ul>\n<li>BookingAttendee type loosened: email and phone are now explicit\nstring | null, and everything except displayName is optional.</li>\n<li>Mongo schema drops required: true on attendee email.</li>\n</ul>\n<h3>Per-event booking opt-in</h3>\n<ul>\n<li>ExtendedFields gains bookingsEnabled?: boolean, persisted via a new\nboolean field on the Mongoose extended-group-event schema.</li>\n<li>BookingSummaryRow carries groupName, groupCode, eventStartDateTime,\nupcoming and bookable fields so the admin UI can badge, filter and\nsearch each row without recomputing.</li>\n</ul>\n<h3>Booking scope model</h3>\n<ul>\n<li>New BookingScope enum (ALL_EVENTS, PER_EVENT) and BookingConfig.scope\nfield. Defaults to ALL_EVENTS in service defaults, environment-setup\ntemplate, and the server-side legacy merge, so unset configs behave\nexactly as before.</li>\n<li>New helpers in booking-config.model.ts:</li>\n<li>bookingScope(config)</li>\n<li>resolves scope with a safe default.</li>\n<li>effectiveMaxCapacityForEvent(config, event)</li>\n<li>returns the event&#39;s\nexplicit max capacity, falling back to the config default.</li>\n<li>eventOptedInToBookings(config, event)</li>\n<li>returns true under\nALL_EVENTS scope, otherwise returns event.fields.bookingsEnabled.</li>\n<li>bookingEnabledForEvent(config, event)</li>\n<li>combines the event-type\ncheck, the scoped opt-in check, and the effective-capacity check\ninto one predicate used by both the public booking form and the\nserver create endpoint.</li>\n</ul>\n<h3>Shared email preview component</h3>\n<ul>\n<li>New EmailPreviewComponent at\nprojects/ngx-ramblers/src/app/modules/common/email-preview. Owns\nthe iframe, blob URL lifecycle, ResizeObserver/MutationObserver\ndriven content observation, loading and error states, and the\nrender() / clear() / showError() public API. Consumers pass a\nTemplateRenderRequest to render() and the component calls\nMailService.renderTemplate under the hood.</li>\n<li>committee-send-notification and group-event-send-notification-modal\nboth refactored to drop their inline iframe + previewUrl + observer\nmachinery (each shed ~100 lines of duplicated preview state) and\nnow embed &lt;app-email-preview #emailPreview/&gt;, calling\nemailPreview.render(request) in their existing refreshPreview()\nmethods.</li>\n</ul>\n<h3>Booking email request builder</h3>\n<ul>\n<li>New buildBookingEmailRequest(emailType, bookingRecord, event,\nsuppliedEventLink) in send-booking-email.ts returns the full\nrendered template request (templateId, params, body, sender,\nsubject, etc). sendBookingNotification() now reduces to a thin\nwrapper that builds the request and calls\nsendTransactionalEmailRequest.</li>\n<li>publicEventLink() takes systemCfg and produces a local group URL as\ndescribed above.</li>\n</ul>\n<h3>Send-by-type server plumbing</h3>\n<ul>\n<li>New sendEmailsByTypeForEvent(eventId, emailType) in\nbooking-reminder-job.ts dispatches confirmation, cancellation,\nwaitlisted, restored or reminder emails to the matching booking\nstatuses for an event. Reminder type delegates to the existing\nsendReminderEmailsForEvent.</li>\n<li>New booking controller handlers:</li>\n<li>sendEmailsByType on POST /booking/send-emails/:emailType/:eventId\n(authenticated) - returns the standard BookingReminderDispatch\nshape.</li>\n<li>previewEmail on GET /booking/preview-email/:emailType/:eventId\n(authenticated) - loads a sample booking (real or synthesised),\nbuilds the template render request via buildBookingEmailRequest\nand returns {templateId, htmlContent, params, subject} for the\nclient to feed into EmailPreviewComponent.</li>\n</ul>\n<h3>Server-side attendee normalisation</h3>\n<ul>\n<li>New normaliseBookingAttendees helper in controllers/booking.ts\ntrims each attendee&#39;s display name / email / phone, nulls blanks, and\ndrops rows without a display name.</li>\n<li>Duplicate-email detection and attendee-email list helpers filter out\nnull/undefined emails before deduping.</li>\n<li>cancel() attendee match uses a shared bookingAttendeeEmail helper so\na null email can never match.</li>\n<li>create() returns a 400 when the first attendee has no email.</li>\n</ul>\n<h3>Reminder dispatch plumbing</h3>\n<ul>\n<li>New BookingReminderDispatch model:\n{ eventId, eventTitle, sentCount, alreadySentCount, skippedCount }.</li>\n<li>server/lib/cron/booking-reminder-job.ts refactored: the per-event\nreminder block becomes an exported sendReminderEmailsForEvent that\nthe cron and the admin button share.</li>\n<li>Brevo attendee-list markdown renderer handles attendees with no\nemail (renders - Name rather than - Name (undefined)).</li>\n</ul>\n<h3>URL-persisted admin state</h3>\n<ul>\n<li>StoredValue gains EMAIL_TYPE = &quot;email-type&quot; and EVENT_ID =\n&quot;event-id&quot; entries (kebab-case values). Per-Event Detail tab pushes\nand reads both via queryParamsHandling: &quot;merge&quot;, so the email-type\nsection toggle, selected event, and active tab together deep-link\nto the exact same view.</li>\n</ul>\n<h3>Salutation de-duplication (#235)</h3>\n<ul>\n<li>BookingConfig gains templatesIncludeSalutation (default true) so the\nBrevo layout stops rendering ADDRESS_LINE when the markdown template\nalready opens with &quot;Hi {{ATTENDEE_NAME}},&quot;. Templates themselves are\nleft untouched. Admin Configuration tab exposes the flag as a\ncheckbox, and both the frontend normalise step and the backend\nloadBookingConfig default the flag to true when the DB value is\nmissing, so existing installs get the fix without a schema backfill.</li>\n</ul>\n"}