Skip to content

Treat a future-start facilitator affiliation as Upcoming, not Active - #2336

Open
maebeale wants to merge 23 commits into
mainfrom
maebeale/active-facilitator-status
Open

Treat a future-start facilitator affiliation as Upcoming, not Active#2336
maebeale wants to merge 23 commits into
mainfrom
maebeale/active-facilitator-status

Conversation

@maebeale

@maebeale maebeale commented Aug 22, 2026

Copy link
Copy Markdown
Collaborator

🤖 suggested review level: 5 Inspect 🔬 changes shared active/active? semantics feeding person + org facilitator status, filters, decorators, policies, and two Stimulus controllers

Goal

  • A facilitator affiliation whose start date is in the future reads as Upcoming, not Active.
  • "Active" now means: not flagged inactive, started (no future start), and not ended — i.e. status_on == "Active".

Affiliations

  • #active?status_on == "Active"; new #upcoming?; the active scope gains the start-date clause.
  • active_or_pending unchanged — registration dedup and scenario end-dating still count future starts.
  • Editor row shows live Inactive / Upcoming badges as the dates are edited.
  • Those badges take today from the server, not the browser. Requests render in the signed-in user's zone (set_time_zone_from_user), so a viewer whose OS zone sits ahead of their profile zone would otherwise see a row starting today badged Upcoming while the server disagreed.

People

  • Facilitator-status filter gains Upcoming.
  • Inactive is the not-active umbrella — everyone who is not a currently-active facilitator, including never-facilitators and upcoming ones. Mirrors the org index's Inactive.
  • Index column is now Facilitator since — facilitator affiliations only, blank for someone who never facilitated (the old member_since fallback would have printed a membership year under that heading; it still applies to a real facilitator whose rows carry no start date). Upcoming/Inactive label under the year.
  • The Affiliation(s) column and the profile's affiliations tab keep upcoming rows — otherwise a row labelled "Upcoming" showed no org beside it.
  • Public directory = active facilitators only: with_active_affiliationswith_active_facilitator_affiliations, behind published / PersonPolicy. A Counselor-only or lapsed person is now admin-visible only.

Organizations

  • New distinct :upcoming status bucket (precedence: active → upcoming → formerly_active → never_active), blue :org_upcoming badge.
  • Organization.program_status: new upcoming bucket; formerly_active excludes it; formerly_or_never ("Inactive") still includes it, so upcoming orgs also surface under Inactive.
  • Index dropdown options moved to Organization::PROGRAM_STATUS_FILTER_OPTIONS, ordered like the people filter (Active, Inactive, Upcoming, Formerly active, Never active).
  • affiliation_dates_controller.js mirrors the new bucket for the edit form's live status chip.
  • Legacy-status warning: the stored column has no Upcoming, so :upcoming is compared as :never_active — a stored Pending (or blank/Unknown) on an upcoming org isn't drift.

Notes for reviewers

  • The stored organization_status is a no-op for upcoming-only programs. The affiliation-save callback deactivates on all-lapsed/none and reactivates on active, but leaves an org alone when its only facilitator hasn't started — stamping it Inactive would stick (nothing re-runs when the start date arrives) and stamping it Active would claim a program that hasn't begun. Either way the legacy-drift warning above would misfire. The displayed bucket (organization_status_bucket, ADR-0001 D3) and published? re-derive live regardless.
  • ADR-0001 updated: Active vs Upcoming definitions, the four-bucket chip vs the five-option filter (Inactive is a filter value, never a chip), and the Inactive umbrella.
  • Rebased on main after Judge each scholarship's program status at its own training #2367 (the scholarship program-status anchoring, split out of this branch) merged — those commits dropped out, so this PR is now purely the Upcoming taxonomy.
  • UI screenshots (Upcoming badges + filters) to add.

Tests

  • #active? / #upcoming? / .active; people Active/Upcoming/Inactive buckets; org bucket precedence + program_status scope buckets; a system spec for the editor's live badges.
  • Org-status callback: upcoming-only leaves the stored status untouched in both directions. The two legacy-mismatch specs now seed the Inactive status row — without it the callback early-returned and they passed vacuously.

maebeale added a commit that referenced this pull request Aug 22, 2026
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
maebeale added a commit that referenced this pull request Aug 22, 2026
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@maebeale
maebeale force-pushed the maebeale/active-facilitator-status branch from f049d93 to b3f21ee Compare August 22, 2026 15:36
maebeale added a commit that referenced this pull request Aug 23, 2026
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@maebeale
maebeale force-pushed the maebeale/active-facilitator-status branch from b3f21ee to 2764311 Compare August 23, 2026 00:40
@maebeale
maebeale marked this pull request as ready for review August 23, 2026 15:55
maebeale added a commit that referenced this pull request Aug 23, 2026
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@maebeale
maebeale force-pushed the maebeale/active-facilitator-status branch from 2764311 to 926dba2 Compare August 23, 2026 16:05
maebeale added a commit that referenced this pull request Aug 24, 2026
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@maebeale
maebeale force-pushed the maebeale/active-facilitator-status branch from a2edc07 to 83dbeeb Compare August 24, 2026 02:15
maebeale and others added 19 commits August 24, 2026 09:01
An affiliation whose start date is still in the future is not genuinely
active yet, so #active? and the `active` scope now exclude it — Active
means started, not ended, and not flagged inactive (the same rule as
status_on == "Active"). This flows through the person edit form's
facilitator status and the people directory, and to org status.

The people directory's facilitator-status filter gains an "Upcoming"
option (and stops lumping these people under "Inactive"), and the
affiliation editor row shows an "Upcoming" badge, live-toggled as the
start date is edited. `active_or_pending` is left intact for registration
dedup, which still counts scheduled future trainings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
An org whose only facilitator affiliation is scheduled for a future start
was landing in "Formerly active", which is wrong for one that was never
active. It now reads as "Upcoming" — a distinct bucket (precedence:
active > upcoming > formerly_active > never_active) with its own amber
badge, an "Upcoming" option on the organization index filter, and a live
chip on the edit form. Upcoming orgs still appear under the "Inactive"
(not-active) filter umbrella too.

Adds Affiliation#upcoming? and mirrors the bucket logic in the
affiliation-dates Stimulus controller.

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

The people directory's "Inactive" facilitator filter is a not-active umbrella,
so it now returns upcoming (future-start) facilitators too — matching the org
index's "Inactive" (formerly_or_never) option. Upcoming remains its own filter
option and status. ADR-0001 D3 now lists the Upcoming bucket and spells out that
"Inactive" includes it, for both people and orgs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
isUpcoming/isPast parsed the date inputs as UTC midnight but compared against a
local-midnight "today", so a start/end equal to today could be misjudged in
timezones ahead of UTC (badging a today-start affiliation as Upcoming). Compare
the inputs' own YYYY-MM-DD strings against a local YYYY-MM-DD "today" instead —
matching the server's strict `> today` / `< today`. Adds a headless system spec
covering past/ended/today/tomorrow/future.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The affiliation-status demo had no upcoming facilitator, so the Upcoming badge,
status, and filter couldn't be seen in dev. Adds "A7 Upcoming facilitator": an
active Counselor role plus a Facilitator affiliation dated one month out, so the
person reads Upcoming (and appears under both the Upcoming and Inactive filters).
add_affiliation now takes an optional start_date.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Point A7's future-dated Facilitator affiliation at a dedicated org whose only
facilitator is that upcoming one, so the org's program-status chip reads
"Upcoming" and it appears under the org index's Upcoming (and Inactive) filters —
letting the org-level Upcoming state be seen in dev, not just the person-level.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Upcoming is a benign not-yet-active state that falls under the "Inactive"
(not-active) umbrella, so amber (a warning hue) overstated it. Use blue for the
affiliation-row Upcoming badge and the org program-status chip (org_upcoming).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rename the people-index "Affiliated since" column to "Facilitator since" and show
the facilitator-since year (matching the edit form and org index) instead of the
earliest any-role affiliation. Under the year, show a blue "Upcoming"/"Inactive"
label for non-active facilitators (nothing for active or non-facilitators). Both
new decorator methods compute from the eager-loaded affiliations — no per-row query.

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

An affiliation that isn't currently active now shows a blue "Inactive" badge
(ended, flagged, or not-yet-started); a future-start one additionally shows
"Upcoming". So an ended row reads Inactive only, while an upcoming row reads
Inactive + Upcoming — Inactive is the not-active state, Upcoming the "starting
soon" qualifier. Both toggle live in the inactive-toggle controller.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The public people directory is a directory of active facilitators, so a person
is "published" (visible to non-admins, white row) only with a currently-active
Facilitator affiliation — a non-facilitator role or a lapsed/upcoming facilitator
is admin-only (blue row) and excluded from the non-admin search scope. Renames
with_active_affiliations → with_active_facilitator_affiliations and updates
#published? to match; both now key off Affiliation.facilitators.active.

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

Broaden facilitators_inactive to the full not-active umbrella: people with an
ended/flagged or upcoming facilitator affiliation AND people with no facilitator
affiliation at all (never active). facilitator_status_label likewise reads
"Inactive" for a never-facilitator. Mirrors the org index's Inactive
(formerly_or_never), which already includes Never active. ADR-0001 D3 sharpened.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drop the not-also-active exclusion from facilitators_upcoming: a person who is an
active facilitator at one org and has an upcoming facilitator affiliation at
another is now returned by both the Active and Upcoming filters, since each is a
membership test on that person's affiliations.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The two indexes offer the same facilitator-status taxonomy, so a reader
shouldn't have to re-learn the order between them: Inactive is the
not-active umbrella and the narrower buckets it contains follow it.

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

The legacy column predates Upcoming, so a program whose facilitator is
scheduled but hasn't started has no "right" value to store — Pending is a
fair record of it, and flagging every such org as drift is noise the
warning was never meant to carry.

The active scope had grown a verbatim copy of with_status("Active"),
which is the duplication #active? already avoids by going through
#status_on.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Narrowing `.active` to exclude future starts also reached the write path:
sync_organization_status_with_affiliations stamped an org "Inactive" the
moment its only facilitator was dated to a future training, and nothing
re-runs the callback when that start date arrives. It also defeated the
new :upcoming exemption in legacy_status_mismatch?, since a forced
"Inactive" buckets as :formerly_active rather than :never_active.

Leaving it at active_or_pending would only mirror the problem — an
Inactive org gaining an upcoming facilitator would read "Active" before
anyone had facilitated. So an upcoming-only program is now a no-op in
both directions.

The two mismatch specs passed vacuously: without an "Inactive" status row
the callback early-returns, so they never exercised it. They seed it now.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The people index labelled someone "Upcoming" and then rendered an empty
Affiliation(s) cell beside it, because the column had moved to the
narrowed active? — the org they're about to facilitate for was the one
thing the label was about. The profile's affiliations tab dropped them
the same way.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The column kept the member_since fallback it had as "Affiliated since",
so a Counselor with no facilitator role showed a membership year under a
heading claiming they facilitated then. The fallback still applies to a
real facilitator whose rows carry no start date.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
todayISO() read the browser's local date while the ERB badges render
against the request's Date.current — and requests run in the signed-in
user's zone (ApplicationController#set_time_zone_from_user), not the
machine's. Anyone whose OS zone sits ahead of their profile zone saw a
row starting today badged "Upcoming" while the server disagreed.

The server passes its own today in as a value, so the two can't drift.
The system spec pins the user's zone for the same reason: it was
building "starts today" in a different zone than the page renders in,
and failed for real once the clock crossed midnight UTC.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
maebeale and others added 3 commits August 24, 2026 09:01
D3 named the chip's four buckets and then described the filter's Inactive
umbrella in the same breath, which reads as though Inactive were a fifth
chip. Spell out that Inactive is a filter option only, and list the
dropdown's five options in display order.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`inactive` is only re-derived when an affiliation is saved
(set_inactive_from_dates), so a term that simply lapsed still reads
inactive: false until something touches the row. Filtering on the flag
alone credited a registrant's shout-out to an organization they had
already left.

Judge it the way every other display does — flagged OR ended — via a new
Affiliation#inactive_on?, the in-memory twin of status_on == "Inactive".
That keeps a not-yet-started affiliation, which is what we want here: a
registrant at a training to be trained is credited to the org they're
about to facilitate for.

Also pins the program-status anchor with a test: an org whose only
facilitator affiliation is minted at a training reads New at that
training on every day of a multi-day event and looking back years later,
because the anchor is the event's start date and never "today".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The attendees roster called itself cross-event and fell back to the year
anchor, but every row already knows the registration that linked the org
— the query joins event_registrations and then plucks the event away.
Keep the event start date and judge each (organization, event) pair
there, so an org linked at a 2018 training reads New while the same org
at a 2024 one reads Ongoing, and no row carries the "no event in view"
caveat any more.

Also gates the Upcoming chip on admin. The org index is becoming more
than admin-facing, and to everyone else a program that starts next month
is simply not active today; showing "Upcoming" there would promise a
state of the world that hasn't arrived. Non-admins get plain "Inactive",
coloured like Never active since the org has never facilitated. Admins
keep Upcoming, which is the distinction they act on. status_bucket_styles
takes the same flag, because the org edit form is admin-or-owner and its
live chip would otherwise contradict the server render for an owner.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@maebeale
maebeale force-pushed the maebeale/active-facilitator-status branch from 79599f7 to a89f2ff Compare August 24, 2026 13:07
The roster now anchors program status on each row's event, which invites
the same move for the Affiliation status column beside it. It shouldn't
move: that column answers where a person stands *now* — the point is to
spot a lapsed affiliation and chase it — and its filter
(person_affiliation_status_ids → Affiliation.with_status) also judges
today. Re-anchoring the column on the event would leave it contradicting
the filter that selected the rows. Written down so the next reader
doesn't "fix" it.

Both comments also still named the old "Pending" status; the taxonomy has
been Active / Upcoming / Inactive since Affiliation::STATUSES.

Sorts Tailwind classes on the files this branch touches (ai/tw-sort) —
pre-existing drift, reordering only, no class added or dropped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

1 participant