Create job + Facilitator affiliations on registration and org linking#1809
Conversation
b938bc8 to
0ef8e8f
Compare
| return unless @job_title | ||
| return if active_affiliation_with_title?(@job_title) | ||
|
|
||
| create_affiliation(@job_title, start_date: nil) |
There was a problem hiding this comment.
🤖 From Claude: Job affiliation is created with no start date on purpose — registration ≠ when they began this role (they may have been at the org for years), so dating it to today would misrepresent it. The Facilitator affiliation does get a date (first of the training month).
| def create_facilitator_affiliation | ||
| return if active_affiliation_with_title?(Affiliation::FACILITATOR_TITLE) | ||
|
|
||
| create_affiliation(Affiliation::FACILITATOR_TITLE, start_date: facilitator_start_date) |
There was a problem hiding this comment.
🤖 From Claude: Ordering matters: the job affiliation is created first, so a typed title of exactly "Facilitator" satisfies this guard and suppresses a duplicate, while "Lead Facilitator" does not — it still gets its own standing Facilitator affiliation.
| # registration form. Uses the same "primary" submission as link_organization | ||
| # (the first submission that named an org, else the first), so the title applied | ||
| # when linking matches what the editor shows. | ||
| def submitted_position(registration) |
There was a problem hiding this comment.
🤖 From Claude: Rebase reconciliation with #1804: derives the title from the same "primary" submission the org-linking editor shows (via registration_submission_entries), so the applied title matches the displayed one. Replaced the old find_submitted_answer helper.
0ef8e8f to
383c712
Compare
| .where("affiliations.end_date IS NULL OR affiliations.end_date >= ?", Date.current) | ||
| } | ||
|
|
||
| scope :active, -> { active_or_pending } |
There was a problem hiding this comment.
🤖 From Claude: active delegates to active_or_pending so behavior is unchanged today; this just names the set the Facilitator dedupe needs. A follow-up PR will tighten active to also require the start date to have arrived (and reclassify call sites), at which point the two scopes diverge.
| end | ||
|
|
||
| def call | ||
| ActiveRecord::Base.transaction do |
There was a problem hiding this comment.
🤖 From Claude: Wrapped both creates in one transaction so a job affiliation is never persisted without its Facilitator (or vice versa) if the second create! raises.
| # (future-dated) facilitator affiliations too, so registering for a second | ||
| # upcoming training doesn't mint a duplicate. | ||
| def active_or_pending_facilitator_affiliation? | ||
| @person.affiliations.active_or_pending.facilitators.where(organization: @organization).exists? |
There was a problem hiding this comment.
🤖 From Claude: Checking active_or_pending (not just active) here prevents a duplicate pending Facilitator when someone registers for a second upcoming training before the first has started.
| " starting #{start_month}" | ||
| "since #{start_month}" | ||
| else | ||
| "no dates" |
There was a problem hiding this comment.
🤖 From Claude: Dates now always render in parens, including (no dates) when an affiliation has neither start nor end — previously an undated pill showed a bare title with no signal its dates were just unknown. starting became since to read naturally inside the parens.
| <p class="font-medium text-gray-800"><%= link_to org.name, person_path(@person, anchor: "affiliations"), | ||
| target: "_blank", rel: "noopener", | ||
| title: "View #{@person.first_name.presence || @person.full_name}'s affiliations", | ||
| class: "after:absolute after:inset-0 hover:underline focus-visible:underline focus:outline-none", |
There was a problem hiding this comment.
🤖 From Claude: Stretched-link pattern: the org-name anchor is the real link (accessible text), and after:absolute after:inset-0 expands its hit area over the whole card. The <li> is relative and the Unlink form is relative z-10 so it stays clickable above the overlay. Opens in a new tab so the in-progress linking flow is preserved.
| # Where to send the admin after a successful update. Defaults to the person's | ||
| # profile, but returns to the registration org-linking page when the edit was | ||
| # opened from one of its linked-org cards (preserving that page's own return_to). | ||
| def person_update_return_path |
There was a problem hiding this comment.
🤖 From Claude: Scoped strictly to return_to == "registration_link" so existing flows (incl. the onboarding eyebrow, which never redirected on save) are unaffected — default stays redirect_to @person. The origin is threaded via top-level params (hidden fields in the form), so it survives the POST without touching person_params.
| <%# Every jump to the person's affiliations editor (header link, linked-org cards, | ||
| other-affiliations card) shares this path so a save returns here — carrying the | ||
| origin and this page's own return_to. %> | ||
| <% edit_affiliations_path = edit_person_path(@person, anchor: "affiliations", return_to: "registration_link", event_registration_id: @event_registration.id, link_org_return_to: params[:return_to].presence) %> |
There was a problem hiding this comment.
🤖 From Claude: One shared path for all three jumps to the affiliations editor (header link, linked-org cards, other-affiliations card) so they stay in sync — each carries the origin and this page's return_to, opens in the same tab, and a save returns here.
When a person registers for an event or an admin links an organization to their registration, record their affiliation with that org so the program roster reflects it without manual data entry: a "job" affiliation carrying the title they typed on the form (when given), plus a standing "Facilitator" affiliation. The Facilitator affiliation is skipped only when an active affiliation titled exactly "Facilitator" already exists, so a job title like "Lead Facilitator" still gets its own. The job affiliation is left without a start date — we don't know when the person began that role (they may have been with the org for years) and dating it to registration would misrepresent that; the Facilitator affiliation starts on the first of the training's month. Logic lives in AffiliationServices::CreateFromRegistration, shared by the public registration service and the admin org-linking actions. Dev seeds now submit a job title on most (but not all) registration forms so both outcomes are visible. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ctive A Facilitator affiliation dated to a future training is "pending" until its start date arrives. The dedupe guard only checked currently-active affiliations, so registering for a second upcoming training (or any flow that re-ran the service before the first training started) would mint a duplicate pending Facilitator. Introduce an `active_or_pending` scope and dedupe against it for both the job-title and Facilitator affiliations. Also wrap the two creates in one transaction so a job affiliation is never left without its Facilitator (or vice versa). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The inline affiliation pills omitted the date entirely when an affiliation had no start/end date, so a bare "Counselor" gave no signal that its dates were simply unknown. Always render the date in parentheses — "(since June 2025)", "(until June 2025)", "(March 2024 – June 2025)", or "(no dates)" — so the pill reads consistently and an undated affiliation is explicit. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Affiliations are managed on the Person record (the Unlink confirm dialog even says so), so the whole registration-linked org card now navigates there — a stretched link on the org name covers the card, opening the person's affiliations section in a new tab so the linking flow isn't lost. The Unlink button is lifted above the overlay so it still works. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Affiliations are added/changed on the edit form, so send the card straight into edit mode (the form's #affiliations section) rather than the read-only show section. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Navigating to the person's edit form is a primary action here, not an aside, so keep it in the same tab (turbo_frame _top still breaks out of any frame). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Clicking a registration-linked org card opens the person's edit form, but saving previously dumped the admin on the person profile, losing the linking flow. Thread the origin (return_to=registration_link + event_registration_id, plus the linking page's own return_to) through the edit link, the form, and PeopleController#update so a successful save lands back on the org-linking page. Adds a matching back-link eyebrow on the edit page. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The header "Edit affiliations" link and the other-affiliations card already jumped to the person's affiliations editor, but in a new tab with no way back. Route all three entry points (header link, linked-org cards, other-affiliations card) through one shared path that carries the return context, so each opens in the same tab and a save lands back on the org-linking page. Renamed the header link "Edit …'s affiliations" and dropped the now-misleading new-tab icons. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
27ec2b2 to
79550f2
Compare
REVIEW NEEDED: 📖 Read — light-logic: contained changes to affiliation creation/dedupe; one new model scope that
activedelegates to (no behavior change today)What is the goal of this PR and why is this important?
How did you approach the change?
AffiliationServices::CreateFromRegistration, a single PORO shared by every entry point so the behavior can't drift:PublicRegistration)EventRegistrationsController#select_organization)EventRegistrationsController#create_organization)facilitatorsscope considers canonical (exactly "Facilitator", trimmed, case-sensitive) already exists with that org — a current one or one dated to a future training that hasn't started yet. An ended Facilitator affiliation does not block a new one (a returning facilitator gets a fresh second affiliation). The job affiliation is created first, so a typed title of exactly "Facilitator" suppresses the duplicate — but a facilitator-ish title like "Lead Facilitator" still gets its own standing "Facilitator" alongside it.active_or_pendingscope: added toAffiliation(not-inactive, not-ended; includes future-dated/pending affiliations).activenow delegates to it, so existing behavior is unchanged — the scope just names the set the dedupe needs.agency_name/agency_position) are localized behind organization-named constants onPublicRegistrationpending a future form-field rename.Anything else to add?
activecall sites betweenactive/active_or_pending, tighteningactiveto also require the start date to have arrived, and a symmetric org-status callback.main(now includes Match facilitator affiliations exactly and case-sensitively #1815/Highlight only exact "Facilitator" affiliations; refactor editor row #1814, which made facilitator matching exact + case-sensitive; Link an affiliation to one of its organization's addresses #1818, which addsorganization_addressto affiliations; and Add per-event Onboarding tab to track registrant setup pipeline #1788's Onboarding tab). Reconciliation notes for reviewers:facilitatorsscope, so it agrees with Match facilitator affiliations exactly and case-sensitively #1815's exact/case-sensitive definition instead of a looserwhere(title:).Affiliation::FACILITATOR_TITLEsits alongside Link an affiliation to one of its organization's addresses #1818's neworganization_addressassociation; affiliations are created with no linked address (the new validation is a no-op when blank).update_onboarding; my org-linking actions andsubmitted_position(built on Reorganize the registration org-linking popup #1804'sregistration_submission_entries) are unchanged.🤖 Generated with Claude Code