Skip to content

Events IA refactor: submenu, cross-event reports, shared stats - #2113

Draft
maebeale wants to merge 17 commits into
mainfrom
maebeale/helsinki
Draft

Events IA refactor: submenu, cross-event reports, shared stats#2113
maebeale wants to merge 17 commits into
mainfrom
maebeale/helsinki

Conversation

@maebeale

@maebeale maebeale commented Aug 9, 2026

Copy link
Copy Markdown
Collaborator

🤖 suggested review level: 5 Inspect 🔬 multi-phase refactor: event stat services, model scopes, routes, policies, subnav, per-event Roster + cross-event report pages

Phased refactor of the event admin area — consolidating duplicated stats, clarifying naming, and restructuring the per-event nav. Each phase is a self-contained commit.

Subnav (per event)

Dashboard | Manage | Roster | Reports | Scholarships | Staff | Edit event

  • Manage = the old Registrants tab (label + heading; route/action stay registrants).
  • Roster = per-event page showing the event's active registrants as a roster table + demographic charts (shared partials fed by EventDashboard; drill-ins → Manage). Owner-visible.
  • Reports = the shared cross-event reports hub, pre-filtered to this event via event_id. Owner-visible when scoped to their event.

What changed (by phase / commit)

  1. Terminology — "Live" vs "On-demand"; Event.live/.on_demand.
  2. Canonical scopes/constantsEventRegistration.attended_facilitator_trainings, Scholarship.externally_funded/.org_subsidized + Grant.self_donated_ids, Affiliation::STATUSES/#status_on/.with_status (SQL↔Ruby agreement spec), Address::US_STATE_ABBREVIATIONS, EventRegistration.status_counts_by_event. One intentional fix: the Manage "funder" filter now treats an AWBW self-donated grant as unfunded, matching the dashboard.
  3. EventScholarshipFigures — batched loader replacing the one-EventDashboard-per-event N+1 in the scholarship report.
  4. Cross-event rename + owner accessstatisticsreports, training_attendeesattendees (frames + files, no redirects). The report suite (revenue, participation, scholarships, hub, attendees) is visible to event owners as well as admins: authorization lives in the rows via an EventPolicy :reportable relation scope (admin → all events, owner → their own), so no filter combination — including an event_id[] array — can widen a report past what the viewer may see. An explicit event_id is still authorized against that event so an unowned one fails loudly.
  5. Subnav + Background removal + Roster page — subnav restructured; Background page retired; Roster reborn as a per-event page (active registrants, shared partials); dead Onboarding/Bulk-payments highlights fixed.
  6. Follow-ups — fixed a 500 on the attendees charts when a city has scholarship recipients (scholarship-cities sub-table used a nil-event link); widened the reports-hub Event dropdown.
  7. Org program-status fixes — preload affiliations in person_program_status_ids so facilitator_status_on doesn't fire a query per org (N+1); collapse the decorator's facilitator_status_as_of onto the model's facilitator_status_on (single source of truth), removing the drift-prone twin.
  8. Ahoy tracking + rename — extend event-page view tracking to preview, sample_ticket, and the reminder confirm/send steps (send logs inline with a recipient count; confirm skips the empty-recipient bounce). Rename the report suite to "Event reports" in the UI — admin-home card + report page heading now match.

Deferred

  • The deeper EventDashboardRegistrantPopulation/Roster/Breakdowns decomposition (collapsing TrainingAttendees*). Follow-up.

Open

  • Roster population uses the app's EventRegistration.active (registered, attended, incomplete_attendance, transferred_in). If we should exclude incomplete_attendance, that's a small follow-up (diverges the roster count from the dashboard's "active").

Testing

Full suite green except 24 pre-existing Groupdate MySQL-timezone failures in admin/ahoy_activities (unrelated to this diff).

Replace the inconsistent "training"/"non-demand" labels for
non-on-demand trainings with a single "Live" term across the
scholarship report and form, and add Event.live/.on_demand scopes so
the delivery-format split has a canonical query source.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 9, 2026 00:10

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Collapse the many mirrored spellings of the shared event-stat rules
into single model-level sources, so per-event and cross-event views
can't drift:

- EventRegistration.attended_facilitator_trainings (was spelled 4 ways
  across the controller and the training-attendees services)
- Scholarship.externally_funded/.org_subsidized + Grant.self_donated_ids
  for the funded/unfunded split (consumed by EventDashboard,
  EventRevenueFigures, and the EventRegistration funder scopes)
- Affiliation::STATUSES, #status_on, .with_status (Ruby rule in the
  roster + SQL rule in the controller, now one source with an
  executable agreement spec)
- Address::US_STATE_ABBREVIATIONS (deduped from two copies)
- EventRegistration.status_counts_by_event (shared by the dashboard and
  the participation report)

Behavior-preserving except one intentional fix: the EventRegistration
funder filter now treats an AWBW self-donated grant as org-subsidized
(unfunded), matching EventDashboard — previously it split on grant
presence alone. Pinned by a new .funder spec.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 9, 2026 00:24

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

The cross-event scholarship report built one EventDashboard per event
(each ~8 queries) just to read that event's funded/unfunded dollars,
counts, and attended headcount. Replace it with EventScholarshipFigures
— a batched loader modeled on EventRevenueFigures that computes every
event's figures in a fixed number of grouped queries and mirrors the
dashboard's funded/unfunded split (parity spec holds them together).
The optional funder: narrowing carries over.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 9, 2026 00:29

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Rename the cross-event report suite to its new vocabulary and open it
to event owners for their own event:

- Routes/actions/views: statistics -> reports (the hub), training_attendees
  -> attendees; Turbo frame ids and view filenames renamed in lockstep
  (attendees_results / attendees_charts). No redirects for old URLs
  (internal admin tool).
- Authorization: the whole-org view stays admin-only (cross_event_reports?),
  but a single-event slice (event_id filter, from the per-event Reports/Roster
  tabs) is visible to that event's owner too (event_reports?). A before_action
  resolves event_id to the Event and authorizes against it.
- Event-scoped eyebrows: when event_id is present, the reports/attendees pages
  return to that event's dashboard.
- Entry points, helper names (hub_to_report_params etc.), labels, return_to
  tokens, and AGENTS.md updated throughout.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 9, 2026 00:46

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Reshape the per-event subnav and retire the Background page in favor of
the shared cross-event views:

- Subnav is now Dashboard | Manage | Roster | Reports | Scholarships |
  Staff | Edit event. "Manage" is the renamed Registrants tab (label +
  heading; route/action stay registrants). Roster and Reports link to the
  shared attendees / reports pages pre-filtered to this event (event_id),
  visible to the event's owner via event_reports?.
- Remove the Background page entirely (route, action, policy, view) — its
  roster + charts now live on the shared attendees index. Drop the
  background_return_path helper and collapse the shared breakdown/roster/
  city partials to their index (cross-event) form, keeping the
  event-scoped city links the recipients page still needs.
- Fix the dead subnav highlights: Onboarding and Bulk payments now light
  up the Manage tab (previously highlighted nothing).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 9, 2026 00:56

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

The scholarship-only cities sub-table rendered its rows without the
index drill-in paths, so on the cross-event attendees index (where there
is no @event) it fell into the event-scoped registrant-link branch and
raised UrlGenerationError (nil event). Pass the same row/scholarship
drill-in paths the full cities list already uses.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 9, 2026 02:53

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Per updated direction: Roster is a per-event page again (not the shared
attendees index), showing the event's active registrants as a roster
table + demographic charts. It reuses the shared _registrant_roster and
_registrant_breakdowns partials (context: :event), fed by EventDashboard
— the same data source and partials the old Background page used, so no
roster/chart markup is duplicated.

- New roster route/action/policy (roster? = manage?, owner-visible); the
  subnav Roster tab links to roster_event_path (Reports still links to
  the shared reports hub scoped by event_id).
- Restore the dual-context (:event/:index) shared breakdown/roster/city
  partials that the Background-removal commit had collapsed to :index,
  renaming background_return_path -> roster_return_path (Manage eyebrow
  now returns to "← Roster").
- Cross-event index heading renamed "Training attendees" -> "Attendees".
- Widen the reports-hub Event dropdown so long event titles fit.

Population note: uses the app-wide EventRegistration.active scope
(registered, attended, incomplete_attendance, transferred_in).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 9, 2026 03:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

- The participation report's link to the attendees index was a stale
  'Training attendees' button; make it a grey nav link reading
  'Attendees' to match the renamed index and the page's other nav links.
- Widen the reports-hub Event dropdown (w-80 -> w-96) so a full event
  title like '2026-10-07 — AWBW Facilitator Training' fits.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 9, 2026 03:19

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

The shared revenue/participation/reports event-type dropdown now offers
All events (default) · All trainings · Live trainings · On-demand
trainings · Other, using the Event.live/.on_demand scopes to narrow the
report to a delivery format. Widened the select to fit the longer labels.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 9, 2026 03:24

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

The reports-hub Participation card now offers an 'Attendees →' link
beneath 'Full report →', carrying the same filter params (event_id etc.)
so the attendees index opens scoped to the same context.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 9, 2026 03:27

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Reached from the event subnav (Scholarships tab), so the back link
should return to the event's dashboard, not the global events index —
matching the other per-event pages.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 9, 2026 03:35

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

…+ lazy load

- Log a 'view.events.<page>' Ahoy event on every event subpage and report
  (dashboard, roster, manage, scholarships, staff, onboarding, edit, and the
  revenue/participation/reports/scholarships/attendees reports), plus the nested
  bulk-payments page. Uses an after_action so it only fires after a successful,
  authorized render, and skips the lazy Turbo-frame sub-requests so a page counts
  once. Carries event_id where there is one.
- Give the Roster page the attendees-style Hide table / Hide charts toggle, and —
  the main thing worth borrowing from attendees — lazy-load the demographic charts
  into their own Turbo frame so the heavier breakdown queries run only when the
  admin reveals them.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 9, 2026 03:50

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

The stat bar and roster table both classify every represented org by
facilitator program status. Each classification ran Organization#
facilitator_status_on, which fired ~2 affiliations queries per org and
was invoked once per org by program_status_counts AND again by
program_status_by_organization — ~40 affiliations queries on a populated
event.

- program_status_counts now derives from program_status_by_organization
  (classify each org once, not twice).
- EventDashboard preloads :affiliations on its organizations, and
  facilitator_status_on filters the loaded association in Ruby instead of
  re-querying per org.

Net: ~40 affiliations queries -> ~2. (The multi-second page times in the
dev log are Vite asset compilation, not the DB — AR time was already
~30ms; this trims the query count that still matters in production.)
Added a query-count regression guard.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 9, 2026 04:01

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

TrainingAttendeesRoster/Breakdowns classify each linked org by
facilitator program status over a larger (cross-event) population;
preload :affiliations on their organizations so facilitator_status_on
reads the loaded association instead of a query per org (same fix as the
event dashboard). Added a query-count guard on the breakdowns.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 9, 2026 04:05

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

…downs}

Last vestige of the old 'training attendees' page name — the route,
views, frames, and heading are all 'Attendees' now, so the services that
back that index should match. Pure rename (files, classes, references,
specs, AGENTS.md).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 9, 2026 04:08

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

…ents

Admins saw the whole org; owners were bounced off an admin-only gate even
though the per-event report slice was theirs. Authorize the unfiltered report
suite (revenue, participation, scholarships, hub, attendees) for anyone who
owns events, and carry the authorization in the rows via a :reportable policy
scope rather than trusting the event_id filter — so no filter combination
(incl. an event_id[] array) can widen a report past what the viewer may see.

Also fixes two review findings on the org program-status classification:
- Preload affiliations in person_program_status_ids so facilitator_status_on
  doesn't fire an affiliations query per org (N+1).
- Collapse the decorator's facilitator_status_as_of onto the model's
  facilitator_status_on (single source of truth), killing the drift-prone twin.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 9, 2026 10:48

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

…ent reports"

Extend event-page Ahoy tracking to the admin preview, the sample-ticket
preview and the reminder confirm/send steps. confirm_reminder joins the
after_action (guarded so the empty-recipient bounce isn't counted); send_reminder
logs inline on a successful send with the recipient count, since it always
redirects.

Rename the report suite to "Event reports" in the UI — the admin-home card and
the report page heading now match.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 9, 2026 11:01

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants