diff --git a/app/services/event_dashboard.rb b/app/services/event_dashboard.rb index f27aac74bc..27a3770116 100644 --- a/app/services/event_dashboard.rb +++ b/app/services/event_dashboard.rb @@ -52,15 +52,16 @@ def scholarship_recipient_count HEADER_SECTOR_KEY = "sector".freeze # Active registrants who requested a scholarship for this event, as Person - # records sorted by display name. Sectors, age-range tags, and affiliations are - # preloaded for the recipients page header; their application answers appear - # below it. + # records sorted by display name. Sectors, age-range tags, and affiliations → + # organization → addresses are preloaded for the recipients page header (job + # title, program org, and its city/state add no per-row queries); their + # application answers appear below it. def scholarship_applicants @scholarship_applicants ||= Person .where(id: scholarship_applicant_ids) .includes(:sectors, { categories: :category_type }, { categorizable_items: { category: :category_type } }, - { affiliations: :organization }) + { affiliations: { organization: :addresses } }) .sort_by(&:name) end @@ -99,14 +100,18 @@ def header_answer_key(identifier) # Shout outs for the recipients page: each active registrant the admin flagged # for a shout-out who also has shout-out text on their profile, paired with that - # text, their first active affiliated organization (if any), and their primary - # sector / age group (from their profile) for the parenthetical after their name. - # Flagged registrants with blank shout-out text are omitted; org/sector/age are optional. + # text, the organization to credit, and their primary sector / age group (from + # their profile) for the parenthetical after their name. The credited org + # prefers their real job, then their AWBW Facilitator role, then the org + # snapshotted on their registration for this event. Flagged registrants with + # blank shout-out text are omitted; org/sector/age are optional. def shoutouts @shoutouts ||= shoutout_registrants.filter_map do |person| text = person.shoutout_text.to_s.strip.presence next unless text - organization = person.affiliations.reject(&:inactive?).filter_map(&:organization).first + organization = person.job_affiliation&.organization || + person.facilitator_affiliation&.organization || + registration_org_by_registrant[person.id] Shoutout.new( recipient: person, organization: organization, @@ -117,6 +122,21 @@ def shoutouts end end + # The organization snapshotted on each shout-out registrant's registration for + # this event (their active affiliations at registration time), keyed by Person + # id. The last fallback for the shout-out credit when a recipient currently + # holds neither a job nor a facilitator affiliation. Preloaded so the shout-out + # list adds no per-row queries. + def registration_org_by_registrant + @registration_org_by_registrant ||= active_registrations + .where(registrant_id: shoutout_registrant_ids) + .includes(event_registration_organizations: :organization) + .each_with_object({}) do |registration, map| + org = registration.event_registration_organizations.filter_map(&:organization).first + map[registration.registrant_id] ||= org if org + end + end + # The scholarship record per recipient, keyed by Person id — lets the roster # decide whether to flag a registrant as a scholarship recipient. First # scholarship wins if a person has several. diff --git a/app/views/events/recipients.html.erb b/app/views/events/recipients.html.erb index ca2d82b4be..21a171839a 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,12 @@ <%# Prefer the registrant's form answers; fall back to their profile data. %> <% sector_text = resolve_answer_text(sector_answer&.form_field, sector_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))) - } %> + <%# Program org + its city/state come from the recipient's AWBW Facilitator + affiliation; the job title comes from their real-job (non-facilitator) + affiliation. See Person#facilitator_organization / #job_affiliation. %> + <% program_org = person.facilitator_organization %> + <% job_title = person.job_affiliation&.title %> + <% program_location = program_org&.program_location %>
" class="scroll-mt-24 bg-white border <%= DomainTheme.border_class_for(:scholarships) %> rounded-xl shadow-sm overflow-hidden break-inside-avoid" @@ -107,21 +107,24 @@
<%= 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| %> + <% if program_org || job_title.present? %>
- <% if affiliation.organization %> - <% if allowed_to?(:show?, affiliation.organization) %> - <%= link_to affiliation.organization.name, organization_path(affiliation.organization), + <% if program_org %> + <% if allowed_to?(:show?, program_org) %> + <%= link_to program_org.name, organization_path(program_org), data: { turbo_frame: "_top" }, class: "font-medium text-gray-700 hover:text-gray-900 hover:underline" %> <% else %> - <%= affiliation.organization.name %> + <%= program_org.name %> <% end %> <% end %> - <% if affiliation.organization && affiliation.title.present? %>·<% end %> - <% if affiliation.title.present? %><%= affiliation.title %><% end %> + <% if program_org && job_title.present? %>·<% end %> + <% if job_title.present? %><%= job_title %><% end %>
<% end %> + <% if program_location.present? %> +
<%= program_location %>
+ <% end %>
<%# Serves / Ages — parallel with the name %> diff --git a/app/views/events/staff.html.erb b/app/views/events/staff.html.erb index 23ebb94535..fcd2eab0cb 100644 --- a/app/views/events/staff.html.erb +++ b/app/views/events/staff.html.erb @@ -25,8 +25,9 @@
<% @event_staffs.each do |event_staff| %> <% person = event_staff.person.decorate %> - <% active_affiliation = person.affiliations.find { |a| !a.inactive? } %> - <% org = active_affiliation&.organization %> + <%# The staffer's real-job organization, falling back to the org they + facilitate for when they have no separate job affiliation. %> + <% org = person.job_affiliation&.organization || person.facilitator_organization %> <% bio = event_staff.public_bio %> <%# Primary sector first (starred, darker chip), then the rest alphabetically — the same treatment as the recipients page. %> diff --git a/spec/requests/events_spec.rb b/spec/requests/events_spec.rb index 6366abd982..ed1e455e1c 100644 --- a/spec/requests/events_spec.rb +++ b/spec/requests/events_spec.rb @@ -1360,6 +1360,27 @@ def submit_agency_name(name) expect(response.body).to include("Attending") end + it "shows the staffer's real-job organization on their card" do + job_org = create(:organization, name: "County Health") + create(:affiliation, person: staff_member, organization: job_org, title: "Social Worker") + create(:affiliation, person: staff_member, organization: job_org, title: "Facilitator") + create(:event_staff, event: public_event, person: staff_member) + + get staff_event_path(public_event) + + expect(response.body).to include("County Health") + end + + it "falls back to the facilitator org when the staffer has no separate job affiliation" do + facilitator_org = create(:organization, name: "Prevail") + create(:affiliation, person: staff_member, organization: facilitator_org, title: "Facilitator") + create(:event_staff, event: public_event, person: staff_member) + + get staff_event_path(public_event) + + expect(response.body).to include("Prevail") + end + context "when the event has ended" do let(:ended_event) { create(:event, :published, :ended) } @@ -1505,19 +1526,24 @@ 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 - org = create(:organization, name: "Safe Harbor of Sheboygan") - 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"), + it "shows the program org (from the facilitator affiliation) with its city/state and the real-job title" do + # A recipient holds two affiliations to the same org: their real job and + # their AWBW Facilitator role. The header shows the program org + its + # city/state (facilitator side) and the job title (real-job side). + program_org = create(:organization, name: "Safe Harbor of Sheboygan") + create(:address, addressable: program_org, city: "Sheboygan", state: "WI") + create(:affiliation, person: applicant, organization: program_org, title: "Facilitator", start_date: 1.year.ago) + create(:affiliation, person: applicant, organization: program_org, + title: "Prevention, Education, and Outreach Specialist", 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(organization_path(program_org)) + expect(response.body).to include("Sheboygan, WI") + expect(response.body).to include("Prevention, Education, and Outreach Specialist") + 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 1c3d2a03e6..9d63942d08 100644 --- a/spec/services/event_dashboard_spec.rb +++ b/spec/services/event_dashboard_spec.rb @@ -486,6 +486,25 @@ def opt_in(person, text:) expect(shoutout.organization).to eq(org) end + it "prefers the real-job affiliation's org over the AWBW Facilitator role" do + job_org = create(:organization, name: "County Health") + opt_in(embedded_applicant, text: "Honored to serve.") + create(:affiliation, person: embedded_applicant, organization: org, title: "Facilitator") + create(:affiliation, person: embedded_applicant, organization: job_org, title: "Social Worker") + + shoutout = dashboard.shoutouts.find { |s| s.recipient == embedded_applicant } + expect(shoutout.organization).to eq(job_org) + end + + it "falls back to the registration's snapshotted org when the recipient has no affiliations" do + opt_in(separate_applicant, text: "Art has been my way through.") + registration = EventRegistration.find_by(event: event, registrant: separate_applicant) + create(:event_registration_organization, event_registration: registration, organization: org) + + shoutout = dashboard.shoutouts.find { |s| s.recipient == separate_applicant } + expect(shoutout.organization).to eq(org) + end + it "exposes the registrant's primary sector and age group" do age_range = create(:category_type, name: "AgeRange") embedded_applicant.sectorable_items.create!(sector: create(:sector, name: "Sexual Assault"), is_primary: true)