Rework org index/profile, unify per-event program status, and add its annual report - #1993
Rework org index/profile, unify per-event program status, and add its annual report#1993maebeale wants to merge 35 commits into
Conversation
| # already-loaded affiliations so a profile can classify many events without an | ||
| # N+1. No facilitator affiliation starting before the date => :new; an earlier | ||
| # one still active on the date => :ongoing; all earlier ones ended => :reinstated. | ||
| def facilitator_status_as_of(date) |
There was a problem hiding this comment.
🤖 From Claude: Deliberately computed in-memory (over the already-loaded affiliations) rather than reusing the DB-backed Organization#facilitator_status_on, so classifying a paginated page of ~9 events doesn't fire 2 queries per event. Same classification rule, just no N+1.
| title: event.title, | ||
| data: { turbo_frame: "_top" }, | ||
| class: "inline-flex items-center rounded-full text-xs font-medium border px-2.5 py-0.5 #{OrganizationDecorator.program_status_classes(status)}" do %> | ||
| <%= status.to_s.titleize %> · <%= event.abbreviation.presence || event.title.truncate(24) %> |
There was a problem hiding this comment.
🤖 From Claude: Falls back to a truncated title when abbreviation is blank so existing events still render a sensible chip. Chips are gated behind manage? to keep program status admin-only, matching how it was treated on the old index column.
06ca55a to
7fb2709
Compare
7fb2709 to
c1bb63c
Compare
| class SimplifyOrganizationStatuses < ActiveRecord::Migration[8.1] | ||
| # Retiring the six-value status set down to Active / Formerly active / Unknown. | ||
| # Each retired status folds into one of the survivors; Reinstate counts as Active. | ||
| RETIRED_TO_REPLACEMENT = { |
There was a problem hiding this comment.
🤖 From Claude: The load-bearing decisions to eyeball: Reinstate folds into Active (per request), Inactive/Suspended into Formerly active, Pending into Unknown. down recreates the retired records but cannot restore per-org mappings — a merge is inherently lossy.
|
|
||
| // Merge affiliation intervals into periods and format them as year-based ranges | ||
| // — the client-side mirror of app/services/affiliation_periods.rb. | ||
| affiliatedSinceLabel(affiliations, today) { |
There was a problem hiding this comment.
🤖 From Claude: This is a hand-kept mirror of AffiliationPeriods (app/services/affiliation_periods.rb) so the edit-form live preview matches the server render. If the Ruby formatting rules change, change both.
f3e576d to
594d262
Compare
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 (3)
app/views/organizations/organizations_results.html.erb:55
- 🤖 From Copilot: should-fix: When an organization has never had a facilitator affiliation,
@program_since_display[org.id]is an empty string, so this table cell renders blank (unlike other empty states that show a placeholder). Rendering a dash makes “no program history” explicit.
<td class="px-4 py-2 text-sm text-gray-700 whitespace-nowrap">
<%= @program_since_display[organization.id] %>
</td>
app/services/affiliation_periods.rb:62
- 🤖 From Copilot: blocker:
AffiliationPeriodsclaims “overlapping or touching intervals” are merged, butoverlaps?only merges whennext_start <= end, so back-to-back ranges like…end_date: 2012-06-30thenstart_date: 2012-07-01will incorrectly show as a gap. Treat adjacent dates as continuous (end date is inclusive elsewhere, e.g.Affiliation.active_or_pending).
# A nil end is ongoing and swallows every later interval.
def overlaps?(period, next_start)
period[1].nil? || next_start <= period[1]
end
app/views/organizations/organizations_results.html.erb:14
- 🤖 From Copilot: should-fix: The PR description says the merged “Affiliated since” display is applied on the org index, but this results table now shows “Program since” instead and no longer renders “Affiliated since” anywhere in the index results. If this is intentional, the PR description likely needs updating; if not, consider restoring the Affiliated-since column (or adding it alongside Program-since).
<th class="px-4 py-2 text-left text-sm font-semibold text-gray-700">Organization</th>
<th class="px-4 py-2 text-left text-sm font-semibold text-gray-700">Designations</th>
<th class="px-4 py-2 text-left text-sm font-semibold text-gray-700">Age group(s)</th>
<th class="px-4 py-2 text-left text-sm font-semibold text-gray-700">Program since</th>
<th class="px-4 py-2 text-center text-sm font-semibold text-gray-700">People (<%= number_with_delimiter(@active_people_count) %>)</th>
…p dead windows-type filter - The per-event chips now open the event participation report filtered to that event (participation_events_path event_id: …, with a dashboard back-link), replacing the dashboard placeholder now that the report exists on main. - Remove the now-dead windows_type_name filter from Organization.search_by_params and the unused :windows_type eager-load on the index (the windows-type column and dropdown were both removed earlier). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
"Formerly active" is a display bucket, not a stored OrganizationStatus value; the search spec should set up an org with an actual legacy status. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…us tooltip - The org index is admin-only (OrganizationPolicy#index? => admin?), so tint each row admin-only bg-blue-100 unless the org is published, mirroring the people index. Simplifies the Program-since cell (drops the now-dead non-admin branch). - Break the edit-form "Program status" tooltip's New/Ongoing/Reinstated onto their own bulleted lines. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The affiliation-dates Stimulus controller was dropped from the org form when "Affiliated since" moved server-side, which also stopped the Program status chip from reacting as facilitator rows are edited. Re-attach the controller and derive the chip's bucket client-side (mirroring OrganizationDecorator#organization_status_bucket): active when any Facilitator row is still active, formerly active when they've all ended, else the stored-status fallback. Bucket labels/classes come from the decorator so no theme classes are hard-coded in JS. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…r-event chips to trainings ADR-0001 pins the definitions we kept re-deriving: affiliated-since (all affiliations), program-since (facilitator affiliations), the org-wide status bucket, and the per-event New/Ongoing/Reinstate status — including that it's per-event (not per-registrant, no self-exclusion) and only meaningful on facilitator-training events. Per that decision, the per-event "Program status by event" chips now render only for facilitator_training events the org is represented at. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The org edit form now live-updates "Affiliated since" as merged year-based periods (mirroring AffiliationPeriods, with the org start_date fallback), alongside the already-live program-status chip. Also adds a request spec asserting the event-registration linked-org chip links to the org profile (the chip itself is rendered by main's #2077). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per-event program-status chips only render for facilitator_training events (ADR-0001), but this example created a plain event, so no chip appeared and the "TOS205" assertion failed. Make the event a training so the chip renders. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Leads with the event date, then the facilitator-program status, then the event label — e.g. "Aug 2026 · Ongoing · PES205" — instead of "Ongoing as of Aug 2026 · …". Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The constant landed between the AGENCY_TYPES explanation and the constant it documents, so that comment read as if it described the preload nest. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rebasing picked up two changes that bear on the program-status decisions. #2176 dates new affiliations to the actual date rather than the 1st, which retires most of the date-precision caveat — but only for rows created since, so historical rows can still misread as Ongoing. #2194 restricts facilitator minting to training registrations, which is what makes keying status off facilitator affiliations alone a read on training participation. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The system-spec selection reads the branch diff, which lists deleted files too, so a branch that removes a system spec handed rspec a path that no longer exists and the whole run died with a LoadError before any example ran. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The profile showed merged periods while the edit form built its own earliest-start-to-latest-end span, under the same label. That was not two formats of one fact: for an org that facilitated 2015-2018, lapsed, and returned in 2024, the form rendered "Aug 2015" and the gap disappeared, because facilitation_end_date is nil whenever any facilitator is active. Give AffiliationPeriods a month precision and let both surfaces read the one decorator method, so the exact month a program started or lapsed survives and the two cannot drift again. The Stimulus mirror follows suit, which drops one of its two rendering paths. The same date-range span was copy-pasted three times across the org and person forms; the two remaining person-form copies become decorator methods, and the org decorator's now-unused facilitator date readers go. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rebasing picked up #2075, whose report subnav forwards REPORT_SUBNAV_PARAMS between report pages — and that list includes organization_id, which the attendees index treats as a real filter. This branch had been passing organization_id on the participation URL purely as a back-link breadcrumb, so the two together meant: org profile -> program-status chip -> Attendees tab silently narrowed the attendee list to that org, with no visible filter saying why. Neither change is wrong alone. Rename the breadcrumb to return_organization_id, which the subnav does not carry and nothing reads as a filter. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Several code paths decided New/Ongoing/Reinstate their own way. Two of them excluded the registrant's own affiliation, differently, and the dashboard re-anchored on that affiliation's start date instead of the event's — so the same org at the same event could read Ongoing on the onboarding matrix and New in the dashboard pie, and the pie's answer moved when a different registrant signed up. FacilitatorProgramStatus is now the only classifier. Dropping self-exclusion is safe because a training's minted affiliation starts on the training date and "before" is strict, so a first-time org still reads New at its own first training. Statuses now carry their own reasoning, so every badge hovers to explain the anchor date, what made the program active, and the facilitator history behind it. Cross-event lists have no event to anchor on, so they read as of Jan 1 and say so rather than quietly using "today" — which the attendees filter was doing while its column would have said otherwise. Adds the annual-reporting page these counts were being assembled by hand for: organizations by status at each training, summed per year, plus the distinct- organization view, because an org at three trainings shouldn't silently count as three programs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
56ec625 to
026fe2a
Compare
The "affiliated since"/"art program since" doc comments restated inferable detail (single-source-of-truth intent, repeated N+1 hint); keep the non-obvious why (precision choice, fallback chain) in fewer lines. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Trim leaning-verbose comment blocks (agency-type folding, self-funding org, program-status scope/method, agency_type_option, the affiliation-dates JS header + periodsLabel) to keep only the non-obvious why: drop restated code, duplicated gotchas, and the stale "confirm with the team" hedge. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
@jmilljr24 i don't think you care about much in here but added you since it's bigger |
| # each event). Only facilitator-training events — program status is meaningless | ||
| # for other events (see ADR-0001). Skip the query for non-managers, who don't | ||
| # see the block. | ||
| @organization_events = if allowed_to?(:manage?, @organization) |
There was a problem hiding this comment.
Should this be an authorized_scope for consistency?
There was a problem hiding this comment.
🤖 From Claude: Done — wrapped it (and the matching query in set_form_variables) in authorized_scope, so EventPolicy visibility is applied like #index. Both pages are admin-only, so results are unchanged for the actual audience; it just adds defense-in-depth.
Per review: wrap both @organization_events queries (show's program-status block and the edit form's per-event chips) in authorized_scope so EventPolicy visibility is applied consistently with #index, instead of the manual manage?/persisted? gate. Both pages are admin-only, so the result is identical for the actual audience; the scope adds defense-in-depth. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
🤖 suggested review level: 5 Inspect 🔬 changes what "active program" means (now derived from facilitator affiliations, not a manual column) and unifies per-event New/Ongoing/Returning into one rule — some historical events will report different numbers; also a new report page, index filters, and a query cleanup; no schema change
What is the goal of this PR and why is this important?
An organization's "status" was answering two different questions in one tangled way — and giving different answers on different screens.
organization_statuscolumn that people forgot to update, so it drifted out of sync with reality.This PR makes both answers come from one source of truth: who is actually facilitating at the org (its facilitator affiliations), so every screen agrees.
How did you approach the change?
Two clear questions, one answer each:
FacilitatorProgramStatus), judged against one date: the event's own date. No event in view falls back to Jan 1 of the current year (shown and captioned, not silently "today").Everything else built on that:
/events/program_statuses— orgs by status per training, with year totals, plus a distinct-org table (each org counted once) for "how many programs" questions.docs/adr/0001-...rewritten to record the new rules.ai/testno longer aborts on branches that delete a system spec.UI Testing Checklist
/events/program_statuses: per-training rows, year totals, and the distinct-organization table.Anything else to add?
organization_statusrecords are left as-is, just no longer read.🤖 From Claude: this PR description was drafted by Claude.