Add event participation report + Events reporting page - #2036
Conversation
f1313f2 to
15d5150
Compare
Adds a cross-event participation report (sibling to the revenue report): how many unique people completed each training, grouped by calendar year, with a per-status outcome breakdown and an ahoy-style scope/time-period filter. Unique people is the headline (a person who attends two trainings in a year is one person trained); seats and the status detail are additive registration counts, so each aggregation level computes its own distinct-people count rather than summing rows. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Renders the two cross-event report summaries side by side (50% each) at the top of the admin home, each linking to its full report. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
15d5150 to
17ea4a7
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 18 changed files in this pull request and generated no new comments.
Suppressed comments (1)
app/models/event.rb:95
- 🤖 From Copilot: should-fix:
Event.in_yearwill raise (500) if an invalidyearis passed (e.g.0or a very large number), sinceDate.new(year)can raise. Guarding here makes the scope safe even ifparams[:time_period]is tampered with or a stale link passes a bad value.
# Events whose start date falls in the given calendar year.
scope :in_year, ->(year) { where(start_date: Date.new(year).all_year) }
…egistrants Moves the two report summary charts off the admin home onto a dedicated "Events reporting" page reached from a single admin-home card; the page hosts the revenue and participation summaries side by side, each linking to its full report. Makes the participation report's counts drill down into the registrants index: each featured-year KPI links to the index filtered by attendance status, and each per-event outcome links filtered by that event and status. Adds an attendance-status filter to the registrants index (param + filter select) to back those links, and orders the per-event outcome grid to read row-major. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 21 out of 21 changed files in this pull request and generated no new comments.
Suppressed comments (1)
app/models/event.rb:95
- 🤖 From Copilot: should-fix:
start_dateis a datetime column, sowhere(start_date: Date.new(year).all_year)can translate to an end bound atYYYY-12-31 00:00:00, excluding events later on Dec 31. Using a time-range in the app time zone avoids off-by-one-day edge cases.
# Events whose start date falls in the given calendar year.
scope :in_year, ->(year) { where(start_date: Date.new(year).all_year) }
The revenue and participation full-page eyebrows now return to the Events reporting hub (their default origin) instead of the events index. Full-page titles pluralize to "Events revenue" / "Events participation"; the reporting hub's summary cards drop the redundant "Event" prefix (just "Revenue" / "Participation"). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 22 out of 22 changed files in this pull request and generated no new comments.
Suppressed comments (1)
app/models/event.rb:95
- 🤖 From Copilot: should-fix:
Event.in_yearuses aDaterange against adatetimecolumn (events.start_date), which can exclude events later on Dec 31 because the end of the range casts to midnight. Use a time-range in the app time zone so the whole year is covered.
# Events whose start date falls in the given calendar year.
scope :in_year, ->(year) { where(start_date: Date.new(year).all_year) }
The events index linked to the revenue and participation reports separately; now that the Events reporting hub is the entry point, a single "Events reporting" button replaces both. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Arriving from the events index now returns there (via return_to=events); arriving from the admin-home card keeps the default "← Admin home" back link. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Widens the participation report's per-event status grid to all seven outcomes across one row on desktop (two columns on mobile), instead of wrapping to two. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 22 out of 22 changed files in this pull request and generated no new comments.
Suppressed comments (1)
app/views/events/participation.html.erb:27
- 🤖 From Copilot: should-fix: The participation filter form doesn’t preserve
return_to/event_id, so if you arrive from an event dashboard the first filter change drops those params and the eyebrow link stops returning to the dashboard.
<%= form_with url: participation_events_path, method: :get, local: true,
class: "flex flex-wrap items-end gap-4 mb-8" do %>
<div class="flex flex-col gap-1">
Going Events → reporting → a full report → back to reporting dropped the origin, so the reporting eyebrow reverted to "← Admin home". Thread return_to through: the reporting summaries' "Full report" links carry the origin, each full report's eyebrow rebuilds the reporting link with it, and the participation filter form keeps it on re-submit. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 22 out of 22 changed files in this pull request and generated no new comments.
Suppressed comments (1)
app/views/event_registrations/_search_boxes.html.erb:40
- 🤖 From Copilot: should-fix: The attendance status filter labels use
status.humanize, which will render e.g. "Incomplete attendance" instead of the app’s establishedattendance_status_label(e.g. "Partial (1-day)"), leading to inconsistent UI wording; reuse the existing label helper used elsewhere for these statuses.
<%= label_tag :attendance_status, "Attendance", class: "block text-sm font-medium text-gray-700 mb-1" %>
<%= select_tag :attendance_status,
options_for_select(EventRegistration::ATTENDANCE_STATUSES.map { |status| [ status.humanize, status ] }, params[:attendance_status]),
include_blank: "All statuses",
The md:grid-cols-7 utility wasn't taking effect (rendered at the grid-cols-2 fallback), so switch to an explicit inline grid-template-columns of seven columns — the same approach the revenue table uses — so all outcomes sit in one row reliably. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 22 out of 22 changed files in this pull request and generated no new comments.
Suppressed comments (1)
app/views/event_registrations/_search_boxes.html.erb:41
- nit: This dropdown builds option labels with
status.humanize, while other attendance-status selects useEventRegistration#attendance_status_label; using the shared label method avoids UI-label drift if the mapping ever changes.
<%= label_tag :attendance_status, "Attendance", class: "block text-sm font-medium text-gray-700 mb-1" %>
<%= select_tag :attendance_status,
options_for_select(EventRegistration::ATTENDANCE_STATUSES.map { |status| [ status.humanize, status ] }, params[:attendance_status]),
include_blank: "All statuses",
🤖 suggested review level: 5 Inspect 🔬 new cross-event report service with non-additive unique-people aggregation, a new admin page, and registrants drill-down
What is the goal of this PR and why is this important?
EventRegistrationstatus"attended"). Headline counts unique people; seats and the status detail are additive registration counts.How did you approach the change?
EventParticipationReportservice, mirroringEventRevenueReport's year-grouped shape. Counts come from two grouped SQL queries (per-event-per-status, and distinct-people-per-year) — no per-registration Ruby pass.COUNT(DISTINCT registrant_id)rather than summing rows. Seats reveal the repeat attendance.EventsController#participation(admin-only) with an ahoy-style scope select (all events / trainings only) and calendar-year time-period select; view mirrorsrevenue.html.erb.EventsController#reporting(admin-only) renders both summaries; the admin home links to it via one "Events reporting" card (the two charts moved off the home page).attendance_statusparam + filter select on the registrants index.Anything else to add?