Skip to content

Rework org index/profile, unify per-event program status, and add its annual report - #1993

Open
maebeale wants to merge 35 commits into
mainfrom
maebeale/remove-org-status-columns
Open

Rework org index/profile, unify per-event program status, and add its annual report#1993
maebeale wants to merge 35 commits into
mainfrom
maebeale/remove-org-status-columns

Conversation

@maebeale

@maebeale maebeale commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator

🤖 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.

  • "Is this org an active art program?" came from a hand-maintained organization_status column that people forgot to update, so it drifted out of sync with reality.
  • "Is this org New / Ongoing / Returning at this training?" was computed five different ways, so the same org at the same event could read "New" on the dashboard pie but "Ongoing" on the roster — and the pie even moved when another person signed up.

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:

  • Org-wide status — read from facilitators: someone active ⇒ Active, all ended ⇒ Formerly active, never any ⇒ Never active. The index filter uses the same rule, so the filter and the badge can't disagree. The legacy column is no longer read (records left untouched).
  • Per-event status — New/Ongoing/Returning now runs through one classifier (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:

  • New annual report at /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.
  • "Art program since" shown the same way everywhere, at month precision, and correctly showing gaps when an org lapsed and returned (the old edit-form version hid the gap).
  • Org index gains Sector + Age-group columns and filters (org's own tags plus affiliated people's primary tags), replacing the old windows-type filter.
  • "New" badge recolored green → indigo so it no longer clashes with the green "Active" badge.
  • Every status badge explains itself on hover — the date it was judged against and what made the program active or lapse.
  • Faster index — per-row roll-ups consolidated and preloaded: 66 → 18 queries on a 6-org page, with a test that the count doesn't grow with the rows.
  • Navigation — per-event chips open the participation report in a new tab with a back-link to the page you came from.
  • Cleanup + ADR — retired the now-unused status helpers/params; docs/adr/0001-... rewritten to record the new rules.
  • Tooling fixai/test no longer aborts on branches that delete a system spec.

UI Testing Checklist

  • Org index: "Art program since" chips, Sectors column, Sector + Age-group filters, Program-status bucket filter.
  • Org profile + edit: "Program status" block; per-event chip → participation report → back-link returns to the page you came from.
  • Org edit: status chip and "Art program since" live-update as Facilitator rows change; a warning shows where the stored legacy status disagrees.
  • An org that lapsed and returned reads the same on the profile and the edit form.
  • "New" is indigo on the event dashboard and reporting.
  • A given org + training reads the same on the dashboard pie, onboarding matrix, roster, org chip, and the new report.
  • /events/program_statuses: per-training rows, year totals, and the distinct-organization table.
  • Hover any status badge — it names the date it was judged against and what made the program active.

Anything else to add?

  • No data or schema changeorganization_status records are left as-is, just no longer read.
  • Some historical events will report different numbers, by design — the old math was inconsistent. The one real risk: a few pre-Affiliations: default start date to the actual date, not first of month #2176 affiliations dated to the 1st of a month can read an org as "Ongoing" where it should be "New," so historical year counts are worth spot-checking (noted in the ADR's open items).
  • Screenshots to follow.

🤖 From Claude: this PR description was drafted by Claude.

Copilot AI lite review requested due to automatic review settings July 15, 2026 04:25

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.

# 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)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

🤖 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) %>

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

🤖 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.

@maebeale
maebeale force-pushed the maebeale/remove-org-status-columns branch from 06ca55a to 7fb2709 Compare July 15, 2026 04:51
Copilot AI review requested due to automatic review settings July 15, 2026 04:51

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 marked this pull request as ready for review July 15, 2026 04:53
Copilot AI review requested due to automatic review settings July 15, 2026 05:06
@maebeale
maebeale force-pushed the maebeale/remove-org-status-columns branch from 7fb2709 to c1bb63c Compare July 15, 2026 05:06

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 changed the title Move org program status out of org-wide UI into event context Remove program status from org-wide UI Jul 15, 2026
Copilot AI review requested due to automatic review settings July 15, 2026 19: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.

@maebeale maebeale changed the title Remove program status from org-wide UI Rework org status: remove program status, merged affiliated-since, 3-value status Jul 15, 2026
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 = {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

🤖 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) {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

🤖 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.

Copilot AI review requested due to automatic review settings July 15, 2026 19:39

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 force-pushed the maebeale/remove-org-status-columns branch from f3e576d to 594d262 Compare July 31, 2026 00:20
Copilot AI review requested due to automatic review settings July 31, 2026 00:20

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 July 31, 2026 21:28

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 1, 2026 10:51

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.

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: AffiliationPeriods claims “overlapping or touching intervals” are merged, but overlaps? only merges when next_start <= end, so back-to-back ranges like …end_date: 2012-06-30 then start_date: 2012-07-01 will 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>

Copilot AI review requested due to automatic review settings August 1, 2026 11:22
maebeale and others added 16 commits August 16, 2026 21:13
…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>
Copilot AI review requested due to automatic review settings August 17, 2026 01:16
@maebeale
maebeale force-pushed the maebeale/remove-org-status-columns branch from 56ec625 to 026fe2a Compare August 17, 2026 01:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

The "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>
Copilot AI review requested due to automatic review settings August 17, 2026 01:44

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.

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>
Copilot AI review requested due to automatic review settings August 17, 2026 09:02

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 requested a review from jmilljr24 August 17, 2026 09:07
@maebeale

Copy link
Copy Markdown
Collaborator Author

@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)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Should this be an authorized_scope for consistency?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

🤖 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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants