Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions app/services/event_dashboard.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ€– From Claude: "active_or_pending" is read as the organization's status (Active/Pending) β€” the only Active/Pending concept in the schema (affiliations have no pending state). The facilitator fallback shows only when the registrant facilitates at the org and the org is Active/Pending.


# 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
Expand Down
30 changes: 14 additions & 16 deletions app/views/events/recipients.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

<% answers_by_applicant = @dashboard.scholarship_answers_by_applicant %>
<% header_answers = @dashboard.header_answers_by_applicant %>
<% event_date = @event.start_date %>

<div class="max-w-7xl mx-auto bg-white border border-gray-200 rounded-xl shadow p-4 sm:p-6">
<%# Top bar: back link + sub-nav, matching the other event pages %>
Expand Down Expand Up @@ -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] || [] %>

<div id="<%= "participant-#{participant_slug}" if participant_slug %>"
class="scroll-mt-24 bg-white border <%= DomainTheme.border_class_for(:scholarships) %> rounded-xl shadow-sm overflow-hidden break-inside-avoid"
Expand Down Expand Up @@ -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 %>
<div class="text-sm text-gray-600 leading-tight">
<% 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 %>
<span class="font-medium text-gray-700"><%= affiliation.organization.name %></span>
<% end %>
<% if affiliation.title.present? %><span class="text-gray-700"><%= affiliation.title %></span>,<% 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 %>
<span class="font-medium text-gray-700"><%= organization.name %></span>
<% end %>
<% if affiliation.organization && affiliation.title.present? %><span class="text-gray-300">Β·</span><% end %>
<% if affiliation.title.present? %><span><%= affiliation.title %></span><% end %>
<% if location.present? %><span class="text-gray-500">(<%= location %>)</span><% end %>
</div>
<% end %>
</div>
Expand Down
25 changes: 21 additions & 4 deletions spec/requests/events_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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('<span class="text-gray-700">Facilitator</span>,')
end

it "excludes registrants who did not request a scholarship" do
Expand Down
78 changes: 78 additions & 0 deletions spec/services/event_dashboard_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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