Events IA refactor: subnav, cross-event reports, self-filtering breakdowns - #2113
Merged
Conversation
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>
maebeale
force-pushed
the
maebeale/helsinki
branch
from
August 10, 2026 15:26
297a2ec to
51caf98
Compare
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>
maebeale
added a commit
that referenced
this pull request
Aug 13, 2026
main's #2113 gave every event admin sub-page a shared subnav and moved page_bg_class from "admin-only" to "admin-or-owner" where the policy allows owners — this report is one of those pages, and was the only one left without either. Also derive the sign-in notice's "30 minutes" from ATTENDANCE_SIGN_IN_LEAD, so widening the window can't leave the copy claiming a stale gap. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
maebeale
added a commit
that referenced
this pull request
Aug 15, 2026
main's #2113 gave every event admin sub-page a shared subnav and moved page_bg_class from "admin-only" to "admin-or-owner" where the policy allows owners — this report is one of those pages, and was the only one left without either. Also derive the sign-in notice's "30 minutes" from ATTENDANCE_SIGN_IN_LEAD, so widening the window can't leave the copy claiming a stale gap. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
maebeale
added a commit
that referenced
this pull request
Aug 15, 2026
main's #2113 gave every event admin sub-page a shared subnav and moved page_bg_class from "admin-only" to "admin-or-owner" where the policy allows owners — this report is one of those pages, and was the only one left without either. Also derive the sign-in notice's "30 minutes" from ATTENDANCE_SIGN_IN_LEAD, so widening the window can't leave the copy claiming a stale gap. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
maebeale
added a commit
that referenced
this pull request
Aug 15, 2026
* Add EventAttendanceTimeEntry model + per-day sign-in window
Introduce generic day-of-event attendance timekeeping: one sign-in/sign-out
pair per row (many per day for breaks/lunch), audited by created_by/updated_by
for staff edits. Event#attendance_sign_in_open? derives a per-day window from
the event's single start/end time-of-day, since events store no per-day schedule.
Backs the CE sign-in/out flow that replaces AWBW's paper CE hour sign-in sheet.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Add CE sign-in/out to the callout + live card reminder
Registrants sign in/out from their private CE callout once CE is paid in full:
one button by state (Sign in inside the day's window, Sign out whenever an entry
is open). The CE callout card shows a live nudge — "Sign in for today" (orange)
or "Signed in" (teal) — mirroring the payment card's due badge. Today's entries
and running total render on the CE page. Self-service is public and unattributed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Let staff edit attendance times on the CE edit form
Add editable in/out rows (correct, add, remove) to the CE registration edit page,
mapped onto the registration's attendance entries and attributed to the editing
admin via created_by/updated_by. Surfaces the failing record's validation errors
on save (e.g. sign-out before sign-in) instead of an empty alert.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Add per-event attendance report, linked from participation
EventAttendanceReport groups a training's sign-in/out entries by day then
registrant, with per-day and grand-total minutes — the in-portal CE hour
sign-in sheet. `?ce=true` scopes to CE registrants and shows license number and
awarded hours; the generic view covers anyone who logged time. Reached at
attendance_event_path (dashboard-level auth) and linked from CE-eligible events
on the participation report. Also shows a "X of Y signed in" nudge for chasing
sign-ins during the training.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Cover EventPolicy#attendance? in the policy spec
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Gate CE certificate on logged time approximating awarded hours
When attendance time has been tracked for a CE registrant, the certificate now
also requires the logged minutes to cover ~90% of the awarded contact hours — you
can't certify hours the sign-in sheet doesn't support. Events that never tracked
time (no entries) are unaffected: day-level attendance alone still governs, so this
never retroactively blocks existing certificates.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Link the attendance report from registrants + add per-CE edit
Add a "CE sign-in report" entry to the registrants bulk-actions dropdown (CE
events only), returning to the registrants page. On the report, each CE registrant
gets an Edit link to their CE edit page. Report exposes ce_registration_for.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Address review: report round-trip, 5-day cap warning, index + dead code
- CE edit opened from the sign-in report now returns there (eyebrow,
Cancel, and after-save/destroy redirects all honor return_to=attendance,
anchored to the totals section)
- The report warns when an event outruns event_dates' 5-day cap, so
missing day sections aren't mistaken for missing data
- Drop the redundant single-column FK index (the composite covers it,
declared in-table so MySQL doesn't auto-create one) and the unused
EventAttendanceReport#any_entries?
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Harden CE attendance edits and clarify the sign-in gating
A stale remove (a second tab, browser-back on the turbo:false form, or a
double-submit) sent a _destroy for an entry that was already gone, so nested
attributes raised RecordNotFound and 500'd the save. Drop rows pointing at
entries no longer on the registration so a stale remove is a quiet no-op.
Also: one blank add-row instead of three (only saved entries get a Remove box,
so three empties read as broken); the three form sections were touching, so
wrap them in space-y-6; and when the sign-in window isn't open, show a standard
gating notice naming the concrete opening (event zone, labeled) plus the rule,
and hide the arrive/leave hint until sign-in is actually usable.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Lead the attendance report with totals, teal-set-apart
Move the all-days totals above the per-day sections and give them a teal
treatment so the headline CE figures read first, with Day 1 / Day 2 below under
a "By day" heading.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Guard attendance entries against >24h/day and same-day overlaps
Reject an entry (or nested batch) that pushes a day's logged time past 24 hours,
or that overlaps another sign-in on the same day — you can't be signed in twice
at once. Cross-entry checks read persisted rows plus the in-memory nested batch,
so both self-service sign-in and the CE edit form are covered; self-service now
redirects with the error instead of raising.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Attendance report: per-day logged columns, consistent totals, row/name links
Add Day N logged columns (and per-day + hours-awarded totals in the All row) to
the Totals table. Scope a registrant's Total logged to the event's days so it
always equals the day columns — time logged on non-event dates no longer inflates
it invisibly. Each row now links to the CE edit page and the name to that
registrant's CE callout.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Stack the Totals header labels for readability
Put the "… logged" / "awarded" qualifier on a second line in smaller lowercase
text, so the day columns stop crowding into one hard-to-read caps line.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Show daily training hours on the attendance report
Staff reading the sign-in sheet need the day's expected hours next to the
logged times to spot short days at a glance — reuse the event decorator's
times in the page header and each day header. Also tighten the CE callout's
sign-in-window note ("30 minutes before."), since the concrete opening
time already says when.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Callout header date line + concrete sign-in/event times
- Callout page header: event dates move to their own line (the combined
title · date line wrapped awkwardly) and gain the daily hours
- The pre-window CE note names the opening time and, on its own line, the
event's actual start ("Event begins 30 min later, at 9:00 PDT") — a
countdown was tried and cut in favor of the concrete times
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Hide the Signed out chip until something is logged today
"Signed out" implied a session that never happened for first-time
visitors — show only the Sign in button until an entry exists today.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* CE sign-in polish: minimum 1m durations, zone label, CTA sign-out
- A sub-minute in/out pair counted as 0m; round up in the attendee's
favor so every completed session logs at least a minute
- Label the today's-entries columns with the display zone ("Time in
(PDT)") since bare clock times were ambiguous
- Sign out gets the primary CTA treatment while signed in (it's the only
action), and the button reads "Sign in again" once a session exists
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Compact the CE credit + license sections to single rows
The stacked label/value rows made the card tall and left the status chip
stranded mid-card — status now sits on the heading row, and the credit
stats (hours at the same scale as cost) and license fields each share
one wrapping row.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Admin chip on the sign-in section linking to the attendance report
Staff viewing a registrant's CE page had no direct path to the event-wide
report; reuse the admin_edit_link chip (gated on the attendance? policy)
beside the Training sign-in heading.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Report grouping toggle + generic sign-in report links
- Session detail now defaults to grouped-by-person (one card per
registrant with a row per day), with a registrants-style pill toggle to
the by-day tables — staff usually chase one person's hours, not a day
- Non-CE events get the report too: participation rows and the
registrants bulk-actions menu link a generic "Sign-in report" where the
CE-scoped link doesn't apply
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Make report session rows clickable to the right edit page
Rows in both report groupings now open the CE edit page on the CE report
and the registration edit page on the generic one — with return_to
wiring (eyebrow + after-save redirect) added to the registration edit
page so the round trip lands back on the report.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Keep a forgotten sign-out from swallowing the next training day
Forgetting to sign out is the failure mode this feature plans for, but an
open entry had no date, so it carried into the next day of a multi-day
training: sign-in was refused as "already signed in", and the only way out
banked a ~24h session against the previous day (or tripped the daily limit
and stranded the registrant entirely). Open entries are now scoped to the
day being asked about, so each day starts fresh and the stale row stays
flagged on the attendance report for staff to close.
Two smaller fixes from the same review:
- Drop the sign-in section once the last day's window has passed, instead
of telling a registrant collecting their certificate that "Sign-in opens
30 minutes before" a training that's already over.
- Render the staff time-entry rows from the in-memory association, so a
save rejected for overlapping or backwards times re-renders with the
admin's typed values instead of silently reverting them.
* Let registrants close a day they forgot to sign out of
Review follow-ups on the attendance work:
A forgotten sign-out previously sat open until staff noticed it on the
report, which also under-counted the registrant's hours against the CE
certificate gate. It now gets its own catch-up button on the callout,
stamped with that day's scheduled end rather than "now" — the correction
staff made on the paper sheet — kept separate from today's sign-in/out so
the two days can't be confused.
The report's "signed in" chip was computed across every entry on the
registration but rendered inside a per-day row, so one forgotten sign-out
lit up every later day too — the opposite of what staff scan the report
for. Scope it to the day.
Attendance-entry validation messages reached the admin through the parent
registration's nested attributes, which pasted the humanized association
name in front of sentences written to display verbatim ("Event attendance
time entries This sign-in overlaps…").
The report's registrant-name links led to a callout whose eyebrow returned
to the CE edit page, leaving no way back to the report.
Co-Authored-By: Claude <noreply@anthropic.com>
* Correct a day's sign-in times on the report instead of clicking out
Fixing one missed sign-in meant leaving the attendance report for the CE
edit page, correcting a datetime, and navigating back — for a sheet whose
whole point is scanning a training day at a glance.
Each day's sessions cell now opens in place. Times are clock times, not
datetimes: the day is the section the editor sits in, so a correction is two
fields rather than two dates to retype and get wrong. A blank trailing row
adds a session (including both halves of a day nobody signed in on), Remove
drops one, and an empty sign-out leaves the session open. Registrants still
stamp their own times from the CE callout — this is the correction surface,
not a replacement for it.
Server-rendered via an `edit` param rather than a Stimulus toggle, so a
rejected save can hand the submitted times back through the flash and reopen
the cell with them; the CE edit page keeps that property and the report
shouldn't lose it. The whole-row link is suppressed while a cell is being
edited so a stray click can't navigate away mid-correction.
The nested-attributes write and audit stamping now live in
EventAttendanceEntriesUpdate, shared with the CE edit form, and the
verbatim-message error formatting moves to ApplicationController since two
controllers now surface nested-association failures.
Co-Authored-By: Claude <noreply@anthropic.com>
* Pin the report name links' return path with a regression spec
The CE-report name links once pointed the CE page eyebrow at
return_to=ce_registration, stranding admins one hop from the report;
assert the attendance value (and the old value's absence) so the
eyebrow chain can't silently regress.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Match the two sign-in-notice times' hour format
Both the sign-in-opening and event-begins times already show AM/PM; align the
event-begins hour directive (%-l → %-I) so the adjacent times format identically.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Show a nested record's validation error the way its own model phrases it
error_sentence stripped the association prefix by falling back to the bare
error message, which loses the subject for anything not written as a whole
sentence — a child's "Signed in at can't be blank" reached the flash as
"can't be blank". Ask the nested error for its inner error's full message
instead: sentence-style messages still read verbatim, attribute-level ones
keep their subject.
Also drops EventAttendanceEntriesUpdate's private attr_readers for its
constructor arguments, per the project convention.
* Format attendance times without going through the view context
The report decorates entries from a service, and Draper resolves `h` against
whatever view context happens to be cached — outside a request that is often a
leftover mailer one, which carries ApplicationHelper and nothing else. So
signed_in_label raised NoMethodError for attendance_clock_time depending on
which spec ran first; main's new specs reshuffled the order enough to surface
it. Move the two formats into AttendanceTimeFormatter, alongside MoneyFormatter:
the helper stays the view-side front door, the decorator calls the PORO.
* Let registrants write up their own hours, not just stamp them
The sign-in/out buttons only ever recorded "now", so anyone who arrived
before signing in, forgot to tap them, or wrote the training up afterwards
had to ask staff to fix it. Their own times are now editable day by day on
the CE callout, outside the sign-in window and after the training ends —
the buttons stay as the fast path, not the only path.
Registrant edits stay unattributed like their taps, so created_by/updated_by
still mean "a staff correction happened here".
An open entry now occupies the rest of its day for the overlap guard.
Treating it as a moment let a later session be recorded underneath it, and
the eventual sign-out then failed validation every time — leaving the
registrant signed in with no way out.
The sample-ticket preview builds an unsaved CE registration with no
payments, so the attendance section it was meant to preview was never
reachable; treat the preview as paid the way the "Paid in full" badge does.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Fit the attendance report into the events IA refactor
main's #2113 gave every event admin sub-page a shared subnav and moved
page_bg_class from "admin-only" to "admin-or-owner" where the policy allows
owners — this report is one of those pages, and was the only one left
without either.
Also derive the sign-in notice's "30 minutes" from ATTENDANCE_SIGN_IN_LEAD,
so widening the window can't leave the copy claiming a stale gap.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Give each CE licence its own sign-in sheet
A registrant claiming CE against two licences is audited by two boards, and
each board is shown only its own licence number and its own awarded hours.
One sheet listing both was no use to either.
The times stay shared — one person, one room, one set of hours — so the
sheets can't disagree about a day the way two independent records could.
Signing in once fills in every sheet, which is why the controls stay
singular above them rather than repeating per licence.
Gating is now per licence on the view (a second licence still being paid
for doesn't take the first one's sheet away) and any-of on the write, which
closes a mismatch that let the section render while sign-in refused: the
view had always read .first, the controller required every CE paid.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Report a sign-in line per CE licence, not per registrant
The CE report is the artifact a board audits, so it has to follow the split
the callout already makes: a registrant claiming CE against two licences was
one row reading "AAA111, BBB222" with the two boards' hours added together,
which is no use to either board.
The reported unit is now a Row — a registration plus, on the CE report, the
licence its hours are certified against — and cells are keyed by the row, so
two lines for one person don't share a DOM id and open both editors at once.
Hours awarded sum across lines, since each board awards its own. Logged
minutes deliberately don't: the lines share one set of times, so the All row
counts each person once rather than banking their hours twice.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Improve notice re truncated events
* Ask for a recap on every completed task, not just on request
A prose summary reads the same whether everything is clean or something
needs attention — the user has to read all of it and infer. The count
header answers that in one line, so it belongs on every message that
reports work as done, not only on "ai recap" or after a PR.
Same reason for the formatting section: long replies are for scanning
first and reading second.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Group the sign-in report with the CSV export in bulk actions
Everything above it in the menu acts on the registrants; the sign-in
report and the CSV are both ways of taking the roster's record out of
the page, so they belong next to each other.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Give the participation report a subnav, and sign-ins a cross-event page
The report, the people behind it, their breakdowns and their logged time
are four angles on one population, but only Attendees was reachable, and
only as a lone link beside the title. They now sit together as tabs on the
eyebrow's row, each carrying the report's filters so switching angle keeps
the same events in scope.
Sign-ins had nowhere to point when no single event is filtered — the
existing report is per-event — so this adds the cross-event page the other
reports already have: a totals table per event in scope, linking into that
event's own report for the editable session detail. Repeating all three
parts per event would be unreadable across a year of trainings.
CE columns follow any event in scope granting CE, not all of them: a mixed
selection must not hide the licence numbers boards audit.
Dropped "report" from the feature's name throughout — it's "CE sign-ins"
now — and moved the per-event page onto the shared report header so it
reads like the registrants page it sits beside.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Add subnav navigation
* comments updated
* hardening options
* Call it CE sign-ins on the CE registrations index too
main's new index links here as "CE sign-in reports", which is the name the
page no longer uses — leaving it would put "report" back in front of a page
titled "CE sign-ins".
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🤖 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
registrants).EventDashboard; drill-ins → Manage). Owner-visible.event_id. Owner-visible when scoped to their event.What changed (by phase / commit)
Terminology — "Live" vs "On-demand";
Event.live/.on_demand.Canonical scopes/constants —
EventRegistration.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.EventScholarshipFigures— batched loader replacing the one-EventDashboard-per-event N+1 in the scholarship report.Cross-event rename + owner access —
statistics→reports,training_attendees→attendees(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 anEventPolicy:reportablerelation scope (admin → all events, owner → their own), so no filter combination — including anevent_id[]array — can widen a report past what the viewer may see. An explicitevent_idis still authorized against that event so an unowned one fails loudly.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.
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.
Org program-status fixes — preload affiliations in
person_program_status_idssofacilitator_status_ondoesn't fire a query per org (N+1); collapse the decorator'sfacilitator_status_as_ofonto the model'sfacilitator_status_on(single source of truth), removing the drift-prone twin.Ahoy tracking + rename — extend event-page view tracking to
preview,sample_ticket, and the reminderconfirm/sendsteps (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.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, thereturn_to=rostereyebrow and_breakdown_card's"/registrants"string sniff all became dead.AttendeesBreakdownsgained*_registrant_ids_by_*maps mirroringEventDashboard's (regrouped from rows already loaded — no extra queries), so the recipients charts drill in too instead of rendering read-only.Attendees index opened past attended-trainings — it was hardcoded to
attendedregistrations 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 values —attendedstays a scope, as the default rather than the base, since you can't filter into rows a scope already removed. All five filters chain existingEventRegistrationscopes (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 fromevent_registrations_pathto 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.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=1opens the panel on arrival so the link doesn't land on a collapsed section.Review pass — four fixes found reviewing the branch end to end. A stale
training_attendees_events_pathin the registration form's Cancel path (500 on any pre-renamereturn_to) plus the two origins it was missing; the attendees index's person-name link had noreturn_to, so saving dumped you on that event's Manage list; the reports hub'slive/on_demandevent types fell through the attendees index'strainings/othervocabulary and silently widened the drill-in, now one sharedEVENT_TYPE_FILTER_OPTIONSacross the report filter, the index and theevent_typescope; and the recipients charts frame paid for the fullscholarship_applicantsload it never renders.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.
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 carriescharts: 1, as the hub's "Breakdowns" link already did. The roster'ssector/statedrill-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.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_statusandfundernarrowed 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 toCHIP_PARAMSfixes all three at once (the chip row and the form's hidden fields both read that list), with labels from shared option lists onEventRegistrationso the select and the chip can't word a value differently.Deferred
EventDashboard→RegistrantPopulation/Roster/Breakdownsdecomposition (collapsingTrainingAttendees*). Follow-up.Resolved since
csp_meta_tagwere the cause.incomplete_attendancestays in (its money belongs to the event); the header and the scholarship report name it separately.Rebase notes (onto #2054, the recipients funder grouping)
main is red on its own recipients specs— Recipients: mute chips, link age chips + registration, group by funder #2054 calledgrant.donorafter Unify grant-funder naming on "Funder" (drop Donor/Grantor) #2147 renamed the column. Fixed here at the time, and since fixed on main independently by Fix NoMethodError in recipients funder grouping (grant.donor → grant.funder) #2164, which is now this branch's base._recipient_card.html.erb; this branch's name→registration link moved with it, andscholarship_applicant_idswas restored to public after the merge placed it belowprivate(it 500'd the recipients charts frame).Rebase notes
Rebased onto
origin/mainat901851127— one conflict, inEventDashboard. main's #2164 landed the samedonor→funderrename this branch had already made, and its copy ofscholarship_applicant_idssits underprivate; this branch had moved that method aboveprivatebecause 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 isai/testtooling.Earlier — onto
47def6cfcRebased onto
origin/mainat47def6cfc— clean, no conflicts, nothing reapplied by hand. That includes4e737f988("Shared registrant filter bar"), which was the one overlapping commit: its four newregistrantsfilters, five newEventRegistrationscopes and the_registrants_search→_registrant_filtersswap 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)
EventDashboard, which this branch had replaced withEventScholarshipFigures; git merged the two textually and leftColumncalling a member that no longer exists (500 on the report). The splits now ride along in the batched loader — one extraPersonquery in total instead of a dashboard per training.return_to: "recipients"from the same page with different anchors, so one silently shadowed the other; the name link now usesrecipient_card.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 inspec/requests/events/attendees_spec.rb— omitting every new filter must reproduce the old population exactly — and the delivery-format splits are covered there and inspec/models/event_registration_spec.rb.