Skip to content

Events IA refactor: subnav, cross-event reports, self-filtering breakdowns - #2113

Merged
maebeale merged 39 commits into
mainfrom
maebeale/helsinki
Aug 10, 2026
Merged

Events IA refactor: subnav, cross-event reports, self-filtering breakdowns#2113
maebeale merged 39 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 | 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.
  • Staff has no tab — it's the public "Meet the staff" roster, not a management surface, and is reached from the event's own page. Page, routes and policy unchanged.

What changed (by phase / commit)

  1. Terminology — "Live" vs "On-demand"; Event.live/.on_demand.

  2. Canonical scopes/constantsEventRegistration.attended_facilitator_trainings (superseded in phase 9), Scholarship.externally_funded/.org_subsidized + Grant.self_funded_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-funded 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.

  9. Breakdowns filter their own page — the breakdown cards are a drill-in index, not a report: every row exists to reach the people behind it, and on the roster they left the page for Manage, walking past the table that already held the answer. Now one rule across all three people-pages: a row links to the page you're on, plus a filter. Single-event pages carry registrant_ids; the cross-event index keeps named filters, since a dash-joined id list there would outgrow the URL. Only the list narrows — stat bar and charts stay whole, with an "Applied … Showing 12 of 39 ✕" chip. This is a net deletion: roster_return_path, the return_to=roster eyebrow and _breakdown_card's "/registrants" string sniff all became dead. AttendeesBreakdowns gained *_registrant_ids_by_* maps mirroring EventDashboard's (regrouped from rows already loaded — no extra queries), so the recipients charts drill in too instead of rendering read-only.

  10. Attendees index opened past attended-trainings — it was hardcoded to attended registrations on facilitator trainings, so it could never answer what the report KPIs ask: a "No show" link landed on an empty page, because no-shows are excluded by definition. Attendance outcome and event type became filters defaulting to today's valuesattended stays a scope, as the default rather than the base, since you can't filter into rows a scope already removed. All five filters chain existing EventRegistration scopes (no new SQL), and the defaults render pre-selected and in the subtitle so they're visible rather than hidden. With that, all 13 report drill-ins moved from event_registrations_path to the people index, and the roster/breakdown columns follow the current event filter rather than a person's whole history. *_training_* naming went event-neutral throughout.

  11. Section polish — sections read "Breakdowns", not "Demographics" (the scholarship and CE pies and the program-status card aren't demographic, and on the recipients page the scholarship pie is the headline). Every section keeps its header while collapsed, with its Show/Hide control in the heading row: the page's toggle bar pins to the right of the first section's heading, and each section further down gets a short Show/Hide pill beside its own (the heading names the section, so the pill doesn't repeat it) — shout outs included. The heading + toggle row is a shared partial; panel-toggle's self-hiding CTA branch went with it. The reports hub links out to the breakdowns rather than hosting them; ?charts=1 opens the panel on arrival so the link doesn't land on a collapsed section.

  12. Review pass — four fixes found reviewing the branch end to end. A stale training_attendees_events_path in the registration form's Cancel path (500 on any pre-rename return_to) plus the two origins it was missing; the attendees index's person-name link had no return_to, so saving dumped you on that event's Manage list; the reports hub's live/on_demand event types fell through the attendees index's trainings/other vocabulary and silently widened the drill-in, now one shared EVENT_TYPE_FILTER_OPTIONS across the report filter, the index and the event_type scope; and the recipients charts frame paid for the full scholarship_applicants load it never renders.

  13. Toggle bar in the heading row — the bar sat on its own row directly above the heading, so the page opened with two stacked rows of controls and the heading pill duplicated the bar's button for the same panel.

  14. Second review pass — three fixes from reviewing the branch end to end again, each with a spec that fails without it. Breakdown drill-ins dropped charts, so a row clicked from inside the charts panel came back with that panel collapsed — the charts closed under the row just clicked, which is the opposite of the phase-9 rule that a row filters the page you're on; every path built inside the panel now carries charts: 1, as the hub's "Breakdowns" link already did. The roster's sector/state drill-ins resolved every matching person in the app only to intersect the result with a roster of at most a few hundred, so they're now bounded to the roster's own ids. And the recipients drill-in and the funder grouping each rebuilt their path from scratch, so switching one cleared the other — which left phase 12's bucket-narrowing unreachable from the UI; all three links now thread the rest of the query through.

  15. Incomplete attendance: two reasons to count, one population — an incomplete attendance was being counted as if it were one thing. It isn't: the money (scholarships awarded, fees paid) stays with the event however the attendance went, while "trained" means an attendance that was completed. Narrowing the Roster's population to serve the second broke the first — a partial attendee's scholarship fell out of the roster's totals, so the page stopped reconciling with the dashboard and the Scholarships tab. So: one population per event (EventRegistration.active), and the distinction is a figure, not a filter — which is what attendance counts already are. The Roster header names how many of its registrants were partial ("14 active registrants (2 with incomplete attendance)"), each row already carries its attendance pill, and the scholarship report reports them beside the attended count — 15 (2) per training, with the totals row spelling out that they're counted in the money but not as attended. Separately, payment_status and funder narrowed the attendees index with no select, no chip and no hidden field: they shrank the list with nothing on screen to say why, no way to clear them, and the next filter change dropped them. Adding them to CHIP_PARAMS fixes all three at once (the chip row and the form's hidden fields both read that list), with labels from shared option lists on EventRegistration so the select and the chip can't word a value differently.

Deferred

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

Resolved since

  • Chartkick blank charts: confirmed fixed in a browser. The CSP nonce + csp_meta_tag were the cause.
  • Roster population: settled in phase 15incomplete_attendance stays in (its money belongs to the event); the header and the scholarship report name it separately.

Rebase notes (onto #2054, the recipients funder grouping)

Rebase notes

Rebased onto origin/main at 901851127 — one conflict, in EventDashboard. main's #2164 landed the same donorfunder rename this branch had already made, and its copy of scholarship_applicant_ids sits under private; this branch had moved that method above private because the recipients charts frame calls it from the controller. Kept the public one — taking main's would have re-privatised it and 500'd that frame (the same trap recorded in the #2054 notes above). No other conflicts; main's other commit is ai/test tooling.

Earlier — onto 47def6cfc

Rebased onto origin/main at 47def6cfc — clean, no conflicts, nothing reapplied by hand. That includes 4e737f988 ("Shared registrant filter bar"), which was the one overlapping commit: its four new registrants filters, five new EventRegistration scopes and the _registrants_search_registrant_filters swap all landed intact alongside this branch's changes to the same files. The rest are Story Share and a json CVE bump, which do not touch this area.

Earlier (onto #2130#2134)

Testing

Green on the new base: 5,539 examples, 0 failures (8 pending) across spec/requests spec/views spec/models spec/services spec/policies spec/helpers spec/decorators, RuboCop clean on 1,449 files. Each of phase 14's three commits is green on its own. The regression guard for the widened attendees population is in spec/requests/events/attendees_spec.rb — omitting every new filter must reproduce the old population exactly — and the delivery-format splits are covered there and in spec/models/event_registration_spec.rb.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

S Mae Beale and others added 22 commits August 10, 2026 11:23
…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>
…tendees program-status filter

- Extract events/_report_header and adopt it across the roster, scholarship
  recipients, and the report suite (reports/attendees/revenue/participation/
  scholarships) so every event/report page shares one header treatment
- Add roster/recipients as save-return origins for the registration editor;
  recipient/roster name links now open the registration
- Add a Program status filter to the attendees index
  (Organization::FACILITATOR_PROGRAM_STATUSES); rename recipients "Statistics"
  section to "Charts" ahead of wiring the real breakdowns
- Restore the funded/unfunded memoization the scopes refactor dropped, so the
  dashboard stops re-running Grant.self_donated_ids per scope call

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Filters like a registrant_ids selection, an organization/city, an age-group or
scholarship/CE drill-in narrow the attendees roster but have no field in the
visible filter form — they arrive from a chart-row click, the participation
summary, or a shared link, and previously narrowed the list with no on-screen
indication and no way to clear them short of wiping every filter.

Surface each applied drill-in as a labeled chip in the filter form (shown only
when present), with a ✕ that reloads the index with just that param dropped. The
drill-ins also ride along as hidden fields so tweaking a visible select no longer
silently drops them. Labels are resolved by a new AttendeesActiveFilters PORO.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… charts

Give all three event list pages the same table/charts structure via a shared
events/_charts_toggle_bar (name-based panel-toggle) plus "Registrants"/"Attendees"/
"Recipients" and "Charts" section headers and an in-section "Show charts" prompt
(events/_charts_cta) that hides once the charts open.

- panel-toggle controller now pairs buttons to panels by data-panel-toggle-name
  (not DOM index), so a panel can have several controls (top toggle + CTA) and a
  CTA button hides itself while its panel is shown
- Recipients: remove the hard-coded "Registrants by city" box and the "Coming
  soon" placeholders; render the shared breakdown cards read-only over just this
  event's scholarship recipients, loaded lazily in a recipients_charts frame; drop
  the external-link icons on the Reports/Grants header links
- AttendeesBreakdowns takes an injectable registrations: scope so it can back the
  recipients charts (EventRegistration.active, one event, any attendance) as well
  as the attendees index (attended facilitator trainings); add a read-only
  (:static) context to _registrant_breakdowns and guard the org/city rows for it
- Attendees count subtitle reads "N people attended this event/these events";
  drop the "year-by-year detail" clause from the reports subtitle

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Shared events/_charts_loading skeleton (pulsing breakdown-card grid) replaces the
plain "Loading charts…" line in the roster, attendees, and recipients charts
frames, so the lazy load reads as loading and doesn't jump the layout on reveal.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reconcile every button (label, aria-expanded, and CTA self-hide) with its panel's
actual visibility on connect, not just on click — so a section's top toggle and
its in-section "Show" CTA can never disagree, and the state stays correct after a
Turbo restore or a frame reload.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ded-trainings

The breakdown cards are a drill-in index, not a report: every row exists to reach
the people behind it. On the roster they left the page for Manage, walking past
the registrant table that already held the answer — so they now filter in place,
as the attendees index has always done. That makes one rule across all three
people-pages: a row links to the page you're on, plus a filter. Single-event
pages carry registrant_ids (tens of people); the cross-event index keeps named
filters, since a dash-joined id list there would outgrow the URL.

Filtering in place also deletes the machinery that existed only to get back:
roster_return_path, the return_to=roster eyebrow, and _breakdown_card's
"/registrants" string sniff.

The attendees index was hardcoded to attended registrations on facilitator
trainings, so it could never answer the questions the report KPIs ask — a "No
show" link landed on an empty page, because no-shows were excluded by definition.
That's why those KPIs pointed at the raw registrations table. Attendance outcome
and event type become filters defaulting to today's values, so the page is
unchanged unless asked otherwise, and all 13 report drill-ins can now land on the
people index. attended stays a scope; it's the default, not the base — you can't
filter into rows a scope already removed.

Sections read "Breakdowns" rather than "Demographics": the scholarship and CE
pies and the program-status card aren't demographic, and on the recipients page
the scholarship pie is the headline. Every section keeps its header while
collapsed and offers its own Show prompt, shout outs included.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…w it

A full-width "Show registrants" button stacked under a "Registrants" heading
repeated the word and pushed the content down. The heading already names the
section, so the control only needs to say Show or Hide, sitting next to it.

That also makes it a real toggle rather than a one-way prompt: it stays put and
flips its own label, so the self-hiding CTA behaviour in panel-toggle has no
callers left and goes. Extracted the heading + toggle row into a shared partial,
since roster, attendees and recipients were each building it by hand.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Staff isn't a management surface like the tabs around it — it's the public
"Meet the staff" roster, reached from the event's own page. The page, routes and
policy are untouched; only the tab goes, so the subnav reads as the admin
workflow it is.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…d per event

main's recipient drilldowns (#2125) read them off EventDashboard, which this
branch had already replaced with EventScholarshipFigures — the rebase merged the
two textually and left Column calling a `dashboard` member that no longer exists,
500ing the scholarships report.

Carrying the splits in the batched loader keeps the point of that replacement:
recipients and their dollars now ride along with the figures, so the expanders
cost one extra Person query in total rather than reviving one dashboard (and its
~8 queries) per training. The query-count guard moves 5 → 6 and a parity spec
holds the splits to the dashboard's.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…s returns

Two things the rebase surfaced.

Revenue figures are money on registrations, so sending them to the attendees
people-index landed admins on a list with no payment columns — and main's new
per-registrant breakdown rows already went to Manage, leaving one report drilling
into two different places. Both halves now agree: one helper picks that event's
Manage list when the report is scoped to a single event, and the cross-event
registrations index otherwise (it takes the same payment_status/funder filters).

main's shout-out feature and this branch's recipient-name link both arrived at
`return_to: "recipients"` from the same page, wanting different anchors — the
first branch silently won. The name link gets its own token so each returns where
it came from: the recipient's card, or the shout-outs section.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ontrols

Onboarding's program-status badge classifies each linked org via
Organization#facilitator_status_on, which now filters the loaded association
rather than probing with EXISTS. Without the preload that's a query per org
pulling every affiliation row — the same N+1 this branch removed on the roster,
attendees and recipients breakdowns, missed at this call site.

The recipients status switch and expand/collapse were pushed to opposite ends of
the row; grouped left they read as one set of controls for the list they act on.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every one of these is gated by admin? || owner? — manage? for the management
pages, dashboard?/edit? for the rest, and the report suite's own rules — but they
all claimed "admin-only". The marker is a policy statement, so a reviewer reading
it got the access model wrong.

No visual change: the markers aren't styled, so the page colour comes from the
bg-* utility riding alongside, which is untouched. events/new stays admin-only —
owner? can't hold for an unsaved event, so new? is admin-only in practice.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ip code

#2147 renamed the column and the filter value; the rebase merged around this
branch's refactor without touching what it introduced, leaving live references to
a column that no longer exists — Grant.self_donated_ids and
EventScholarshipFigures#funder_grant_ids both still queried `donor:`, and the
funder filter's "donor" value no longer matches anything.

Also renamed self_donated_* to self_funded_*, since #2147's point was one word
for this concept and "donated" was the drift it set out to remove.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The preload added with the donor→funder rebase had nothing holding it in place:
removing it broke no test, so the N+1 could silently come back. Verified the new
guard by mutation — it fails without the preload and passes with it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The bar sat on its own row directly above the heading, so these pages opened
with two stacked rows of controls — and the heading's own Show/Hide pill
duplicated the bar's button for the very same panel. Pin the bar to the right
of the heading row instead and drop that heading's pill. Sections further down
keep theirs, where the bar is too far away to serve as their control.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
following the rename, so the funder grouping calls grant.donor on a column that
no longer exists — main is red on its own recipients specs, not just here. Same
mechanical rename through the grouping, its FunderGroup member, and the specs.

Also restores two things the rebase dropped: scholarship_applicant_ids landed
below `private` where the merge placed it among the new grouping helpers, 500ing
the recipients charts frame the controller calls it from; and the recipient name
links to the registration's edit form again, now from the extracted card partial
(the ticket icon beside it goes to the registrant's own view — different page).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A breakdown row is clicked from inside the charts panel, but the paths it
built dropped `charts`, so the page came back with the panel collapsed:
the charts closed under the row that was just clicked, and stopped being
the navigation surface the drill-in design leans on. Every path built
inside the panel now carries charts: 1, matching the Reports hub's
"Breakdowns" link, which already did.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both branches resolved every matching person in the app — everyone tagged
with that sector, everyone with an address in that state — only to
intersect the result with a roster of at most a few hundred. Scoping the
lookup to the roster's own ids keeps the query proportional to what it
filters, and drops the redundant round trip back through people.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both controls rebuilt the path from scratch, so toggling the grouping
cleared an applied drill-in and clearing the chip cleared the grouping —
which left the bucket-narrowing that shipped with the drill-in unreachable
from the UI. Threading the rest of the query through all three links
matches the roster's chip, and keeps the charts panel open across the
toggle as a side effect.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The index narrows on payment_status and funder — the revenue report's KPIs
link in with them — but neither had a select, a chip, or a hidden field. So
they shrank the list with nothing on screen to say why, no way to clear
them, and the next filter change silently dropped them. Adding them to
CHIP_PARAMS fixes all three at once: the form's hidden fields and the chip
row both read that list.

Their labels come from option lists on EventRegistration, shared with the
registrants filter bar, so the select and the chip can't word a value
differently. A value the list doesn't offer gets no chip, matching the
scopes, which treat it as no filter at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Roster drew on EventRegistration.active, which includes
incomplete_attendance — so a trainee who made one day of a two-day event sat
in a roster meant to answer "who was here", and in every stat and breakdown
built from it.

EventDashboard now takes the registration scope its people-facing figures
draw from (the same shape AttendeesRoster and AttendeesBreakdowns already
take), defaulting to .active so the dashboard, recipients page and revenue
figures are untouched. The Roster passes the new .on_roster. Its header
reports the count it left out, so the narrower number reads as a deliberate
exclusion rather than a discrepancy against the dashboard.

Renamed the dashboard's private active_registrations to scoped_registrations
now that the caller chooses the scope, matching AttendeesRoster.

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

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.

Backs out the Roster's on_roster population. There are two reasons to count
on an event and they want opposite things: the money — scholarships awarded,
fees paid — stays with the event however the attendance went, while
"trained" means an attendance that was completed. A narrower population
serves the second by breaking the first: a partial attendee's scholarship
fell out of the roster's totals, so the page stopped reconciling with the
dashboard and the Scholarships tab.

So one population per event (EventRegistration.active) and the distinction
becomes a figure, not a filter — which is what attendance counts already
are. The Roster's header names how many of its registrants were partial, and
the scholarship report reports them beside the attended count: "15 (2)" per
training, with the totals row spelling out that they're counted in the money
but not as attended.

Nothing narrows a population now, so EventDashboard's registrations: kwarg
and EventRegistration.on_roster go with it.

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

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.

@maebeale
maebeale merged commit bbce84e into main Aug 10, 2026
3 checks passed
@maebeale
maebeale deleted the maebeale/helsinki branch August 10, 2026 22:46
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