Reconcile facilitator affiliations against attendance (admin bulk action) - #2195
Open
maebeale wants to merge 27 commits into
Open
Reconcile facilitator affiliations against attendance (admin bulk action)#2195maebeale wants to merge 27 commits into
maebeale wants to merge 27 commits into
Conversation
maebeale
force-pushed
the
maebeale/facilitator-affiliation-reconciliation
branch
from
August 14, 2026 04:58
68913c9 to
e765a96
Compare
maebeale
marked this pull request as ready for review
August 14, 2026 05:11
maebeale
commented
Aug 14, 2026
|
|
||
| def facilitator_start_date | ||
| (@training_date || Date.current).to_date.beginning_of_month | ||
| (@training_date || Date.current).to_date |
Collaborator
Author
There was a problem hiding this comment.
🤖 From Claude: Intentional shift toward accurate dates: this starts the facilitator affiliation on the actual training date rather than the first of its month (changing code that just landed in #2194). Reconciliation works either way; flagging since it edits a freshly-merged file.
Groundwork for facilitator-affiliation reconciliation: an ownership FK so reconcile only ever touches rows the registration flow created, and a timestamp on events recording when affiliations were last reconciled. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per (person, org): keep the owned facilitator affiliation active iff they have an attended facilitator-training registration for that org; otherwise same-day it (end_date := start_date). Hand-created rows are left alone. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A preview-and-confirm page (under Bulk actions on facilitator trainings) that same-days the owned facilitator affiliation of anyone who didn't complete the training, keeps/reactivates completers, and records when it last ran. Admins can opt individual rows out before applying. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ations on reconcile Start the created facilitator affiliation on the actual training date rather than the first of its month. Extend the Reconcile affiliations action to also create missing facilitator affiliations (pre-event for anyone, post-event for attendees), shown as opt-out-able 'Will be created' rows alongside the deactivations. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On a non-training event, the Reconcile affiliations action now deletes facilitator affiliations that were auto-created off it (job affiliations are left alone), shown as opt-out-able 'Will be deleted' rows. Same-day rows also gain a per-row 'Delete instead' checkbox. The action is now available on every event, not just trainings. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e status Preview now lists every registrant-org pair, grouped by action, and adds a 'Not reconciled' section explaining why each is left alone, with attendance status shown per row. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ttendance Preview now groups actionable rows by person with the shared editable attendance chip and a note of their other-org facilitator affiliations; each facilitator affiliation is an individual row showing its date range with an Edit link to the person page. 'Not reconciled' is a collapsible section grouped by reason (hand-entered last), each reason collapsible too. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Render the shared attendance chip (not plain text) in the Not reconciled rows so status is editable there too, move the Edit link ahead of the status, and shorten the affiliation date range to 'Oct 13, 2026 – present'. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tion controls Reconcile every facilitator affiliation for a linked org (not just app-created), gated to post-event so a pre-event run never deactivates and with per-row opt-out. Move the include checkbox into the action chip so it's clear checking it performs that action, move the other-org facilitator note below the rows, link org/dates to the specific affiliation anchor and names to the registration, and strengthen the Not reconciled section headers (open by default, expand/collapse all). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…eader note Style the include/delete-instead controls as buttons that only turn error-red when selected (peer-checked, no JS); add hover tooltips explaining deactivate/delete (delete as bullets: this affiliation only, job + other-org affiliations untouched). Move the 'Also a facilitator at …' note beside the name, truncated and linking to the single affiliation anchor (or the affiliations section when several). Order the Not reconciled sections with 'Active — attended' second-to-last. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…sections Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…exclusive Move the checkbox back inside each button (has-[:checked] colors the whole button on select, error-red for deactivate/delete). Add an exclusive-checkboxes Stimulus controller so checking 'Delete instead' clears 'Will be deactivated' and vice versa; apply now treats a delete key as delete regardless of the include key. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
'Preview changes' now posts to a confirmation screen that shows exactly which affiliations get created/reactivated/deactivated/deleted (actioned rows only), with Go back to edit (selections restored) or Perform changes. Add per-row instructions under the action buttons and a header row with a warning that checked boxes change affiliations. New exclusive-checkboxes controller registered. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Thread return_to through the attendance status badge and add a reconcile case to EventRegistrations#update so toggling attendance from the reconcile page reloads it (with fresh attendance) and a success flash, instead of jumping to the roster. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Stimulus manifest is explicit, so the new exclusive-checkboxes controller was never loaded — register it and use an explicit change event so Delete instead and Will be deactivated actually clear each other. Reword the deactivate row note to spell out the two options (mark Attended = permanent, uncheck = one-time). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ed or uncheck this box'
… note says 'both boxes' Turbo ignores a 200 HTML render on a form POST (only 4xx/5xx render), so the confirmation screen never showed. Submit the preview form with turbo disabled. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…on attendance toggle
Replace the deactivate/delete checkboxes with a radio group per row
(Deactivate/Delete/Keep active, and action/keep for the others), styled as the same
buttons via has-[:checked]. Radios are natively mutually exclusive, so remove the
exclusive-checkboxes Stimulus controller and the per-row instruction note.
Fix the real reason 'Preview changes' did nothing: the attendance chip's form was
nested inside the reconcile form (invalid HTML), so the submit/inputs fell outside
it. Render the reconcile form standalone and join the radios/submit via the HTML
form= attribute. Switch the params to an outcome map { row.key => choice }.
Toggling attendance now scrolls back to that item's anchor, not the top.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… labels Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Same-daying an affiliation on or after its start date left it active: the callback always recomputed `inactive` from the dates, and a row ending today still reads as active. Admins also had no way to set it — the column was permitted everywhere but had no field, and ticking it alongside a date edit was silently overwritten. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The classification lived in ReconcileEvent while ReconcileFacilitatorAffiliation held a second, owned-only copy that nothing but its own spec reached — two implementations of the same rules, already disagreeing on hand-entered rows. ReconcilePerson is now the only place a decision is made; ReconcileEvent iterates it and keeps the keys, grouping and timestamp. Owned-vs-all becomes an argument, so the per-person reconciler is callable on its own (e.g. from an attendance change) without re-deriving anything. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The badge's Turbo submit answered with a stream that swapped only the status chip, so the row kept offering its pre-toggle action and the new return_to redirect never ran. Pages that pass a return_to now opt out of Turbo and get the full re-render; the registrants and onboarding pages keep the inline swap. Also lists the bulk action on the Features & tips seed and drops the "uncheck" wording left over from before the row controls became radios. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Main moved per-affiliation editing to the gear editor, so that — not the dense inline row — is where the flag belongs. Trims the comments added across this branch down to the ones carrying a why. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
maebeale
force-pushed
the
maebeale/facilitator-affiliation-reconciliation
branch
from
August 18, 2026 14:54
3c31481 to
b0067a1
Compare
The person History card and activity timeline had nothing multi-year to render, so affiliation edits, trainings, memberships and comments couldn't be seen interleaved. Two gaps kept the seeded rows invisible: affiliation comments were missing from PersonCommentAggregator (Affiliation became commentable in #2235 without being added), and payment lifecycle events record the STI subclass ("CashPayment"), which the person's Payment filter never matched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
maebeale
added a commit
that referenced
this pull request
Aug 19, 2026
Reading an affiliation's dates tells you what is true now but not how it got there. The Ahoy lifecycle events already record every edit, and the trainings and membership periods that explain those edits live in their own tables — this puts all three in one time-ordered view so an admin can see why a row looks the way it does without leaving the page. Trainings and memberships are read from their own tables rather than from Ahoy: Ahoy records *changes*, and only those made while a Current.user or Current.source was set, so imported and seeded rows have no events at all. Ahoy is used only for the affiliation's own columns, where nothing else records them. Ahoy events are matched on every STI name for the row's table, not just the record's current class. A row filed as Affiliation before the subtypes existed — or under the other subtype before a retitle re-typed it — would otherwise lose that history. Split out of #2195; stacked on #2259 for the STI subtypes. Co-Authored-By: Claude Opus 5 (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 🔬 new reconciliation service + admin bulk action that creates, deactivates and deletes affiliations across an event's registrants, plus a model-callback change
What is the goal of this PR and why is this important?
Facilitator affiliations are minted optimistically at registration and go Active by date alone — no-shows, cancellations, partial attendance, and never-marked registrants all silently become active facilitators.
The foundation (gating creation on training type + linking the affiliation to its registration) landed separately in #2194. This PR adds the reconciliation layer: an admin-driven step that makes affiliations match who registered and attended.
How did you approach the change?
Rules in one place
ReconcilePerson— the single classifier, per(person, organization)in the context of one event.#planreturns aDecisionper affiliation (:create/:deactivate/:reactivate/:delete/:noop+ reason) with no writes;#performapplies one;#callapplies them all.attendedfacilitator-training registration for that org", so no-showing one training but attending another keeps them active.include_unowned:is the auto-vs-manual gate — the bulk page passestrue, an automatic caller wouldn't.The bulk action
ReconcileEventiteratesReconcilePersonacross the event's registrants and turns decisions into individually-selectable rows. Keys, grouping, and theaffiliations_reconciled_atstamp live here; no rules do.index→confirm→ perform). Each row is a radio group with a leave-as-is option; deactivate rows can delete instead.Inactive is now settable
Affiliation#set_inactive_from_datesyields to an explicit assignment. Without this, same-daying a row whose start date is today (a one-day training reconciled the day it ends) left it active — the date rule reads a row ending today as active.Smaller
affiliations_reconciled_atcolumn on events; entry added to the Features & tips seed.Dev seed
db:seed:affiliation_historygives the owner of affiliation 1 seven years of interleaved trainings, memberships, affiliation edits, comments and emails, so the person History card has something multi-year to render.PersonCommentAggregator(Affiliationbecame commentable in Affiliation edit gear: standalone editor with comments + reassign #2235 without being added), and payment lifecycle events record the STI subclass (CashPayment), which the person'sPaymentfilter never matched.Anything else to add?