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