diff --git a/app/services/event_dashboard.rb b/app/services/event_dashboard.rb
index d752e88cf0..07a1ad18b2 100644
--- a/app/services/event_dashboard.rb
+++ b/app/services/event_dashboard.rb
@@ -59,6 +59,25 @@ def scholarship_applicants
.sort_by(&:name)
end
+ # One organization line shown beneath a recipient's name: the org and, for a
+ # job affiliation, the registrant's title there (nil for the facilitator
+ # fallback, where the title is deliberately suppressed).
+ RecipientAffiliation = Struct.new(:organization, :title, keyword_init: true)
+
+ # The organization line(s) under each recipient's name on the recipients page,
+ # keyed by Person id. Driven by the org(s) connected to the registrant's
+ # registration (the snapshot taken at registration plus any an admin linked
+ # later) rather than all of the person's affiliations. For each connected org
+ # we show the title from the registrant's active, non-facilitator job
+ # affiliation there; absent one, we fall back to a title-less line when they
+ # are a facilitator at that org and the org is active or pending. Orgs matching
+ # neither are omitted, so a recipient can show several lines or none.
+ def recipient_affiliations_by_registrant
+ @recipient_affiliations_by_registrant ||= scholarship_applicants.to_h do |person|
+ [ person.id, recipient_affiliation_rows(person) ]
+ end
+ end
+
# Scholarship-application answers for this event's applicants, keyed by Person
# id and de-duplicated to one answer per question. The scholarship section may
# be captured on the registration submission (registered with scholarship
@@ -660,6 +679,34 @@ def scholarship_applicant_ids
@scholarship_applicant_ids ||= active_registrations.where(scholarship_requested: true).pluck(:registrant_id)
end
+ # Org statuses that still warrant showing a facilitator-only fallback line.
+ FALLBACK_FACILITATOR_STATUSES = %w[Active Pending].freeze
+
+ # Recipient affiliation lines for one person (see #recipient_affiliations_by_registrant).
+ def recipient_affiliation_rows(person)
+ affiliations_by_org = person.affiliations.group_by(&:organization_id)
+ registration_organizations_by_registrant.fetch(person.id, []).filter_map do |organization|
+ org_affiliations = affiliations_by_org[organization.id] || []
+ job = org_affiliations.find { |affiliation| !affiliation.facilitator? && affiliation.active? }
+ if job
+ RecipientAffiliation.new(organization: organization, title: job.title)
+ elsif org_affiliations.any?(&:facilitator?) && FALLBACK_FACILITATOR_STATUSES.include?(organization.organization_status&.name)
+ RecipientAffiliation.new(organization: organization, title: nil)
+ end
+ end
+ end
+
+ # Organizations connected to each applicant's active registration, sorted by
+ # name, with addresses and status preloaded for the recipients header. Keyed by
+ # Person id.
+ def registration_organizations_by_registrant
+ @registration_organizations_by_registrant ||= active_registrations
+ .where(registrant_id: scholarship_applicant_ids)
+ .includes(organizations: [ :addresses, :organization_status ])
+ .index_by(&:registrant_id)
+ .transform_values { |registration| registration.organizations.sort_by(&:name) }
+ end
+
# Registrant (Person) ids behind active registrations that opted into a
# shout-out — the candidates for the recipients page shout-out block.
def shoutout_registrant_ids
diff --git a/app/views/events/recipients.html.erb b/app/views/events/recipients.html.erb
index 7f881c0c73..9d07d0c41e 100644
--- a/app/views/events/recipients.html.erb
+++ b/app/views/events/recipients.html.erb
@@ -2,7 +2,6 @@
<% answers_by_applicant = @dashboard.scholarship_answers_by_applicant %>
<% header_answers = @dashboard.header_answers_by_applicant %>
-<% event_date = @event.start_date %>
<%# Top bar: back link + sub-nav, matching the other event pages %>
@@ -61,11 +60,10 @@
<%# Prefer the registrant's form answers; fall back to their profile data. %>
<% service_area_text = resolve_answer_text(service_area_answer&.form_field, service_area_answer&.submitted_answer).presence || person.sectors.map(&:name).join(", ").presence %>
<% age_group_text = resolve_answer_text(age_group_answer&.form_field, age_group_answer&.submitted_answer).presence || person.categories.select { |c| c.category_type&.name == "AgeRange" }.map(&:name).join(", ").presence %>
- <%# The affiliation active at the time of the event, excluding facilitator roles. %>
- <% recipient_affiliations = person.affiliations.select { |a|
- !a.facilitator? &&
- (event_date.blank? || ((a.start_date.nil? || a.start_date <= event_date) && (a.end_date.nil? || a.end_date >= event_date)))
- } %>
+ <%# Org line(s) from the registrant's registration: the connected org(s),
+ each with the registrant's job title there (title-less for the
+ facilitator fallback). %>
+ <% recipient_affiliations = @dashboard.recipient_affiliations_by_registrant[person.id] || [] %>
"
class="scroll-mt-24 bg-white border <%= DomainTheme.border_class_for(:scholarships) %> rounded-xl shadow-sm overflow-hidden break-inside-avoid"
@@ -108,18 +106,18 @@
<%= link_to person.name, person_path(person), data: { turbo_frame: "_top" },
class: "text-lg font-bold text-gray-900 hover:underline" %>
<% recipient_affiliations.each do |affiliation| %>
+ <% organization = affiliation.organization %>
+ <% location = organization.program_location %>
- <% if affiliation.organization %>
- <% if allowed_to?(:show?, affiliation.organization) %>
- <%= link_to affiliation.organization.name, organization_path(affiliation.organization),
- data: { turbo_frame: "_top" },
- class: "font-medium text-gray-700 hover:text-gray-900 hover:underline" %>
- <% else %>
- <%= affiliation.organization.name %>
- <% end %>
+ <% if affiliation.title.present? %><%= affiliation.title %>,<% end %>
+ <% if allowed_to?(:show?, organization) %>
+ <%= link_to organization.name, organization_path(organization),
+ data: { turbo_frame: "_top" },
+ class: "font-medium text-gray-700 hover:text-gray-900 hover:underline" %>
+ <% else %>
+ <%= organization.name %>
<% end %>
- <% if affiliation.organization && affiliation.title.present? %>·<% end %>
- <% if affiliation.title.present? %><%= affiliation.title %><% end %>
+ <% if location.present? %>(<%= location %>)<% end %>
<% end %>
diff --git a/spec/requests/events_spec.rb b/spec/requests/events_spec.rb
index fab6dfbeb0..cad80551df 100644
--- a/spec/requests/events_spec.rb
+++ b/spec/requests/events_spec.rb
@@ -1505,19 +1505,36 @@ def submit_agency_name(name)
expect(response.body).to include("Mark tasks complete")
end
- it "shows the non-facilitator affiliation's title and linked organization, excluding facilitator roles" do
+ it "shows the connected org's job title, linked organization, and city/state" do
org = create(:organization, name: "Safe Harbor of Sheboygan")
+ create(:address, addressable: org, city: "Sheboygan", state: "WI")
+ registration = event.event_registrations.find_by(registrant: applicant)
+ create(:event_registration_organization, event_registration: registration, organization: org)
create(:affiliation, person: applicant, organization: org,
title: "Prevention, Education, and Outreach Specialist", start_date: 1.year.ago)
- create(:affiliation, person: applicant, organization: create(:organization, name: "Facilitator Org"),
- title: "Facilitator", start_date: 1.year.ago)
get recipients_event_path(event)
expect(response.body).to include("Prevention, Education, and Outreach Specialist")
expect(response.body).to include("Safe Harbor of Sheboygan")
expect(response.body).to include(organization_path(org))
- expect(response.body).not_to include("Facilitator Org")
+ expect(response.body).to include("Sheboygan, WI")
+ end
+
+ it "falls back to a title-less line for a facilitator at an active/pending connected org" do
+ org = create(:organization, name: "Facilitator Org",
+ organization_status: create(:organization_status, name: "Active"))
+ registration = event.event_registrations.find_by(registrant: applicant)
+ create(:event_registration_organization, event_registration: registration, organization: org)
+ create(:affiliation, person: applicant, organization: org, title: "Facilitator", start_date: 1.year.ago)
+
+ get recipients_event_path(event)
+
+ expect(response.body).to include("Facilitator Org")
+ expect(response.body).to include(organization_path(org))
+ # The facilitator title itself is suppressed in the fallback line, so no
+ # "Facilitator, " title prefix precedes the org name.
+ expect(response.body).not_to include('
Facilitator,')
end
it "excludes registrants who did not request a scholarship" do
diff --git a/spec/services/event_dashboard_spec.rb b/spec/services/event_dashboard_spec.rb
index a65621d865..65d61093df 100644
--- a/spec/services/event_dashboard_spec.rb
+++ b/spec/services/event_dashboard_spec.rb
@@ -845,4 +845,82 @@ def opt_in(person, text:)
expect(dashboard.unallocated_bulk_payment_cents).to eq(0)
end
end
+
+ describe "#recipient_affiliations_by_registrant" do
+ let(:event) { create(:event) }
+ let(:active_status) { create(:organization_status, name: "Active") }
+ let(:pending_status) { create(:organization_status, name: "Pending") }
+ let(:suspended_status) { create(:organization_status, name: "Suspended") }
+
+ def applicant_with(organizations)
+ person = create(:person)
+ registration = create(:event_registration, event: event, registrant: person,
+ status: "registered", scholarship_requested: true)
+ organizations.each { |org| create(:event_registration_organization, event_registration: registration, organization: org) }
+ person
+ end
+
+ def rows_for(person)
+ described_class.new(event).recipient_affiliations_by_registrant[person.id]
+ end
+
+ it "shows the job title and the org's city/state for an active job affiliation" do
+ org = create(:organization, name: "Job Org", organization_status: active_status)
+ create(:address, addressable: org, city: "Austin", state: "TX")
+ person = applicant_with([ org ])
+ create(:affiliation, person: person, organization: org, title: "Director")
+
+ rows = rows_for(person)
+ expect(rows.map(&:title)).to eq([ "Director" ])
+ expect(rows.first.organization).to eq(org)
+ expect(rows.first.organization.program_location).to eq("Austin, TX")
+ end
+
+ it "falls back to a title-less line for a facilitator at an active or pending org" do
+ org = create(:organization, organization_status: pending_status)
+ person = applicant_with([ org ])
+ create(:affiliation, person: person, organization: org, title: "Facilitator")
+
+ rows = rows_for(person)
+ expect(rows.map(&:title)).to eq([ nil ])
+ expect(rows.first.organization).to eq(org)
+ end
+
+ it "omits a facilitator-only org whose status is neither active nor pending" do
+ org = create(:organization, organization_status: suspended_status)
+ person = applicant_with([ org ])
+ create(:affiliation, person: person, organization: org, title: "Facilitator")
+
+ expect(rows_for(person)).to be_empty
+ end
+
+ it "prefers the job affiliation over a facilitator one at the same org" do
+ org = create(:organization, organization_status: active_status)
+ person = applicant_with([ org ])
+ create(:affiliation, person: person, organization: org, title: "Facilitator")
+ create(:affiliation, person: person, organization: org, title: "Coordinator")
+
+ expect(rows_for(person).map(&:title)).to eq([ "Coordinator" ])
+ end
+
+ it "shows one line per connected org, sorted by name" do
+ org_b = create(:organization, name: "Beta", organization_status: active_status)
+ org_a = create(:organization, name: "Alpha", organization_status: active_status)
+ person = applicant_with([ org_b, org_a ])
+ create(:affiliation, person: person, organization: org_a, title: "Lead")
+ create(:affiliation, person: person, organization: org_b, title: "Aide")
+
+ expect(rows_for(person).map { |row| row.organization.name }).to eq(%w[Alpha Beta])
+ end
+
+ it "ignores affiliations whose org is not connected to the registration" do
+ connected = create(:organization, name: "Connected", organization_status: active_status)
+ unconnected = create(:organization, name: "Unconnected", organization_status: active_status)
+ person = applicant_with([ connected ])
+ create(:affiliation, person: person, organization: connected, title: "Manager")
+ create(:affiliation, person: person, organization: unconnected, title: "Volunteer")
+
+ expect(rows_for(person).map { |row| row.organization.name }).to eq(%w[Connected])
+ end
+ end
end