+ <%= form_with url: registrants_event_path(@event), method: :get,
+ data: { controller: "collection", turbo_frame: "registrants_results" },
+ autocomplete: "off",
+ class: "flex flex-col gap-4" do %>
- <%= hidden_field_tag :status_filter, params[:status_filter].presence || "active" %>
+ <%= hidden_field_tag :status_filter, params[:status_filter].presence || "active" %>
-
- <%= label_tag :keyword, "Keyword", class: "block text-sm font-medium text-gray-700 mb-1" %>
-
- <%= text_field_tag :keyword, params[:keyword],
+ <%# Row 1: keyword, organization status, scholarship, payment, attendance %>
+
+
+ <%= label_tag :keyword, "Keyword", class: "block text-sm font-medium text-gray-700 mb-1" %>
+
+ <%= text_field_tag :keyword, params[:keyword],
+ class: "w-full rounded-lg border border-gray-300 px-3 py-2 text-gray-800 shadow-sm
+ focus:border-blue-500 focus:ring focus:ring-blue-200 focus:outline-none",
+ placeholder: "Name, email, phone, city, or organization" %>
+
+
+
+
+
+ <%= label_tag :organization_status, "Organization status", class: "block text-sm font-medium text-gray-700 mb-1" %>
+ <%= select_tag :organization_status,
+ options_for_select(
+ [ [ "Linked", "linked" ], [ "Pending", "pending" ], [ "None", "none" ] ],
+ params[:organization_status]
+ ),
+ include_blank: "All",
+ class: "w-full rounded-lg border border-gray-300 px-3 py-2 text-gray-800 shadow-sm
+ focus:border-blue-500 focus:ring focus:ring-blue-200 focus:outline-none" %>
+
+
+ <% if @event.cost_cents.to_i > 0 %>
+
+ <%= label_tag :scholarship, "Scholarship", class: "block text-sm font-medium text-gray-700 mb-1" %>
+ <%= select_tag :scholarship,
+ options_for_select(
+ [ [ "All recipients", "yes" ], [ "Tasks complete", "complete" ], [ "Tasks not complete", "incomplete" ] ],
+ params[:scholarship]
+ ),
+ include_blank: "All registrants",
class: "w-full rounded-lg border border-gray-300 px-3 py-2 text-gray-800 shadow-sm
- focus:border-blue-500 focus:ring focus:ring-blue-200 focus:outline-none",
- placeholder: "Name, email, phone, city, or organization" %>
-
-
-
+ focus:border-blue-500 focus:ring focus:ring-blue-200 focus:outline-none" %>
+
-
- <%= label_tag :attendance_status, "Attendance status", class: "block text-sm font-medium text-gray-700 mb-1" %>
- <%= select_tag :attendance_status,
- options_for_select(
- EventRegistration::ATTENDANCE_STATUSES.map { |s| [EventRegistration.new(status: s).attendance_status_label, s] },
- params[:attendance_status]
- ),
- include_blank: "All statuses",
- class: "w-full rounded-lg border border-gray-300 px-3 py-2 text-gray-800 shadow-sm
- focus:border-blue-500 focus:ring focus:ring-blue-200 focus:outline-none" %>
-
+
+ <%= label_tag :payment_status, "Payment", class: "block text-sm font-medium text-gray-700 mb-1" %>
+ <%= select_tag :payment_status,
+ options_for_select(
+ [ [ "Due", "unpaid" ], [ "Paid", "paid" ], [ "Intends to pay", "intends_to_pay" ] ],
+ params[:payment_status]
+ ),
+ include_blank: "Any payment status",
+ class: "w-full rounded-lg border border-gray-300 px-3 py-2 text-gray-800 shadow-sm
+ focus:border-blue-500 focus:ring focus:ring-blue-200 focus:outline-none" %>
+
+ <% end %>
- <% if @event.cost_cents.to_i > 0 %>
-
- <%= label_tag :payment_status, "Payment", class: "block text-sm font-medium text-gray-700 mb-1" %>
- <%= select_tag :payment_status,
- options_for_select(
- [ [ "Due", "unpaid" ], [ "Paid", "paid" ], [ "Intends to pay", "intends_to_pay" ] ],
- params[:payment_status]
- ),
- include_blank: "Any payment status",
- class: "w-full rounded-lg border border-gray-300 px-3 py-2 text-gray-800 shadow-sm
+
+ <%= label_tag :attendance_status, "Attendance status", class: "block text-sm font-medium text-gray-700 mb-1" %>
+ <%= select_tag :attendance_status,
+ options_for_select(
+ EventRegistration::ATTENDANCE_STATUSES.map { |s| [EventRegistration.new(status: s).attendance_status_label, s] },
+ params[:attendance_status]
+ ),
+ include_blank: "All statuses",
+ class: "w-full rounded-lg border border-gray-300 px-3 py-2 text-gray-800 shadow-sm
focus:border-blue-500 focus:ring focus:ring-blue-200 focus:outline-none" %>
+
-
- <%= label_tag :scholarship, "Scholarship", class: "block text-sm font-medium text-gray-700 mb-1" %>
- <%= select_tag :scholarship,
- options_for_select(
- [ [ "All recipients", "yes" ], [ "Tasks complete", "complete" ], [ "Tasks not complete", "incomplete" ] ],
- params[:scholarship]
- ),
- include_blank: "All registrants",
- class: "w-full rounded-lg border border-gray-300 px-3 py-2 text-gray-800 shadow-sm
- focus:border-blue-500 focus:ring focus:ring-blue-200 focus:outline-none" %>
-
- <% end %>
+ <%# Row 2: state, locality, county, country %>
+
+ <% if @dashboard.states.any? %>
+
+ <%= label_tag :state, "State", class: "block text-sm font-medium text-gray-700 mb-1" %>
+ <%= select_tag :state,
+ options_for_select(@dashboard.states, params[:state]),
+ include_blank: "All states",
+ class: "w-full rounded-lg border border-gray-300 px-3 py-2 text-gray-800 shadow-sm
+ focus:border-blue-500 focus:ring focus:ring-blue-200 focus:outline-none" %>
+
+ <% end %>
- <% if @dashboard.states.any? %>
-
- <%= label_tag :state, "State", class: "block text-sm font-medium text-gray-700 mb-1" %>
- <%= select_tag :state,
- options_for_select(@dashboard.states, params[:state]),
- include_blank: "All states",
- class: "w-full rounded-lg border border-gray-300 px-3 py-2 text-gray-800 shadow-sm
- focus:border-blue-500 focus:ring focus:ring-blue-200 focus:outline-none" %>
-
- <% end %>
+ <% if @dashboard.localities.any? %>
+
+ <%= label_tag :locality, "Locality", class: "block text-sm font-medium text-gray-700 mb-1" %>
+ <%= select_tag :locality,
+ options_for_select(@dashboard.localities, params[:locality]),
+ include_blank: "All localities",
+ class: "w-full rounded-lg border border-gray-300 px-3 py-2 text-gray-800 shadow-sm
+ focus:border-blue-500 focus:ring focus:ring-blue-200 focus:outline-none" %>
+
+ <% end %>
- <% if @dashboard.counties.any? %>
-
- <%= label_tag :county, "County", class: "block text-sm font-medium text-gray-700 mb-1" %>
- <%= select_tag :county,
- options_for_select(
- @dashboard.counties.map { |state, county| [ "#{state} - #{county}", "#{state}|#{county}" ] },
- params[:county]
- ),
- include_blank: "All counties",
- class: "w-full rounded-lg border border-gray-300 px-3 py-2 text-gray-800 shadow-sm
- focus:border-blue-500 focus:ring focus:ring-blue-200 focus:outline-none" %>
+ <% if @dashboard.counties.any? %>
+
+ <%= label_tag :county, "County", class: "block text-sm font-medium text-gray-700 mb-1" %>
+ <%= select_tag :county,
+ options_for_select(
+ @dashboard.counties.map { |state, county| [ "#{state} - #{county}", "#{state}|#{county}" ] },
+ params[:county]
+ ),
+ include_blank: "All counties",
+ class: "w-full rounded-lg border border-gray-300 px-3 py-2 text-gray-800 shadow-sm
+ focus:border-blue-500 focus:ring focus:ring-blue-200 focus:outline-none" %>
+
+ <% end %>
+
+ <% if @dashboard.countries.any? %>
+
+ <%= label_tag :country, "Country", class: "block text-sm font-medium text-gray-700 mb-1" %>
+ <%= select_tag :country,
+ options_for_select(@dashboard.countries, params[:country]),
+ include_blank: "All countries",
+ class: "w-full rounded-lg border border-gray-300 px-3 py-2 text-gray-800 shadow-sm
+ focus:border-blue-500 focus:ring focus:ring-blue-200 focus:outline-none" %>
+
+ <% end %>
+
+
+ <%= link_to "Clear filters", registrants_event_path(@event),
+ class: "btn btn-utility-outline",
+ data: { action: "collection#clearAndSubmit" } %>
+
<% end %>
-
-
- <%= link_to "Clear filters", registrants_event_path(@event),
- class: "btn btn-utility-outline",
- data: { action: "collection#clearAndSubmit" } %>
-
-<% end %>
+
diff --git a/spec/models/event_registration_spec.rb b/spec/models/event_registration_spec.rb
index 9fdc1ef1b8..988ff43984 100644
--- a/spec/models/event_registration_spec.rb
+++ b/spec/models/event_registration_spec.rb
@@ -348,6 +348,35 @@
end
end
+ describe ".registrant_locality scope" do
+ it "filters to registrations whose registrant has an active address in the locality" do
+ match = create(:event_registration)
+ create(:address, addressable: match.registrant, locality: "LA City", inactive: false)
+ other = create(:event_registration)
+ create(:address, addressable: other.registrant, locality: "Northern CA", inactive: false)
+
+ expect(EventRegistration.registrant_locality("LA City")).to contain_exactly(match)
+ end
+
+ it "ignores inactive addresses" do
+ reg = create(:event_registration)
+ create(:address, addressable: reg.registrant, locality: "LA City", inactive: true)
+
+ expect(EventRegistration.registrant_locality("LA City")).to be_empty
+ end
+ end
+
+ describe ".registrant_country scope" do
+ it "filters to registrations whose registrant has an active address in the country" do
+ match = create(:event_registration)
+ create(:address, addressable: match.registrant, country: "Canada", inactive: false)
+ other = create(:event_registration)
+ create(:address, addressable: other.registrant, country: "Mexico", inactive: false)
+
+ expect(EventRegistration.registrant_country("Canada")).to contain_exactly(match)
+ end
+ end
+
describe "#paid_in_full?" do
let(:event) { create(:event, cost_cents: 1000) }
let(:user) { create(:user, :with_person) }
diff --git a/spec/requests/events_spec.rb b/spec/requests/events_spec.rb
index 6366abd982..6bd0528771 100644
--- a/spec/requests/events_spec.rb
+++ b/spec/requests/events_spec.rb
@@ -138,6 +138,50 @@
end
end
+ describe "GET /registrants with organization_status filter" do
+ let(:form) { create(:form) }
+ let!(:event_form) { create(:event_form, :registration, event: event, form: form) }
+ let!(:agency_field) { create(:form_field, form: form, field_identifier: "agency_name") }
+
+ let(:linked_person) { create(:person, first_name: "Linny", last_name: "Linked") }
+ let(:pending_person) { create(:person, first_name: "Penny", last_name: "Pending") }
+ let(:none_person) { create(:person, first_name: "Nora", last_name: "None") }
+
+ before do
+ linked = create(:event_registration, event: event, registrant: linked_person)
+ create(:event_registration_organization, event_registration: linked, organization: create(:organization, name: "Acme Org"))
+
+ create(:event_registration, event: event, registrant: pending_person)
+ submission = create(:form_submission, person: pending_person, form: form)
+ create(:form_answer, form_submission: submission, form_field: agency_field, submitted_answer: "Unlinked Agency")
+
+ create(:event_registration, event: event, registrant: none_person)
+
+ sign_in admin
+ end
+
+ it "shows only linked registrants when filtering by linked" do
+ get registrants_event_path(event, organization_status: "linked")
+ expect(response.body).to include("Linny")
+ expect(response.body).not_to include("Penny")
+ expect(response.body).not_to include("Nora None")
+ end
+
+ it "shows only pending registrants when filtering by pending" do
+ get registrants_event_path(event, organization_status: "pending")
+ expect(response.body).to include("Penny")
+ expect(response.body).not_to include("Linny")
+ expect(response.body).not_to include("Nora None")
+ end
+
+ it "shows only unresolved registrants when filtering by none" do
+ get registrants_event_path(event, organization_status: "none")
+ expect(response.body).to include("Nora None")
+ expect(response.body).not_to include("Linny")
+ expect(response.body).not_to include("Penny")
+ end
+ end
+
describe "GET /details" do
let(:event) { create(:event, :published, :publicly_visible) }
@@ -581,15 +625,15 @@ def submit_agency_name(name)
get registrants_event_path(event)
- expect(response.body).to include(">Pending<")
- expect(response.body).not_to include(">None<")
+ expect(response.body).to include("
Pending")
+ expect(response.body).not_to include("
None")
end
it "shows a 'None' chip when a registrant has no linked org and submitted nothing" do
get registrants_event_path(event)
- expect(response.body).to include(">None<")
- expect(response.body).not_to include(">Pending<")
+ expect(response.body).to include("
None")
+ expect(response.body).not_to include("
Pending")
end
it "shows the linked org AND a 'Pending' chip when the submitted name is not among the linked orgs" do
@@ -599,7 +643,7 @@ def submit_agency_name(name)
get registrants_event_path(event)
expect(response.body).to include(organization.name)
- expect(response.body).to include(">Pending<")
+ expect(response.body).to include("
Pending")
end
it "does not show 'Pending' when the submitted name matches a linked org" do
@@ -609,7 +653,7 @@ def submit_agency_name(name)
get registrants_event_path(event)
expect(response.body).to include(organization.name)
- expect(response.body).not_to include(">Pending<")
+ expect(response.body).not_to include("
Pending")
end
end
diff --git a/spec/services/event_dashboard_spec.rb b/spec/services/event_dashboard_spec.rb
index 1c3d2a03e6..81e2a1b031 100644
--- a/spec/services/event_dashboard_spec.rb
+++ b/spec/services/event_dashboard_spec.rb
@@ -350,6 +350,16 @@
end
end
+ describe "localities" do
+ it "lists distinct localities from active registrants' active addresses, sorted and excluding inactive" do
+ person1.addresses.first.update!(locality: "Northern CA")
+ person2.addresses.where(inactive: false).first.update!(locality: "LA City")
+ person2.addresses.where(inactive: true).first.update!(locality: "Southern CA")
+
+ expect(dashboard.localities).to eq([ "LA City", "Northern CA" ])
+ end
+ end
+
describe "countries" do
it "returns the registrant ids that have a country on file, excluding inactive and cancelled" do
expect(dashboard.country_registrant_ids).to contain_exactly(person1.id, person2.id)
diff --git a/spec/services/event_organization_link_status_spec.rb b/spec/services/event_organization_link_status_spec.rb
new file mode 100644
index 0000000000..d01a861456
--- /dev/null
+++ b/spec/services/event_organization_link_status_spec.rb
@@ -0,0 +1,63 @@
+require "rails_helper"
+
+RSpec.describe EventOrganizationLinkStatus do
+ let(:event) { create(:event) }
+ let(:form) { create(:form) }
+ let!(:event_form) { create(:event_form, :registration, event: event, form: form) }
+ let!(:agency_field) { create(:form_field, form: form, field_identifier: "agency_name") }
+
+ def submit_agency_name(person, name)
+ submission = create(:form_submission, person: person, form: form)
+ create(:form_answer, form_submission: submission, form_field: agency_field, submitted_answer: name)
+ end
+
+ describe "#registration_ids_for" do
+ it "classifies linked, pending, and none registrations" do
+ linked = create(:event_registration, event: event)
+ organization = create(:organization, name: "Acme Org")
+ create(:event_registration_organization, event_registration: linked, organization: organization)
+
+ pending = create(:event_registration, event: event)
+ submit_agency_name(pending.registrant, "Unlinked Agency")
+
+ none = create(:event_registration, event: event)
+
+ service = described_class.new(event)
+
+ expect(service.registration_ids_for("linked")).to contain_exactly(linked.id)
+ expect(service.registration_ids_for("pending")).to contain_exactly(pending.id)
+ expect(service.registration_ids_for("none")).to contain_exactly(none.id)
+ end
+
+ it "treats a submitted name matching a linked organization as resolved (not pending)" do
+ registration = create(:event_registration, event: event)
+ organization = create(:organization, name: "Acme Org")
+ create(:event_registration_organization, event_registration: registration, organization: organization)
+ submit_agency_name(registration.registrant, "acme org")
+
+ service = described_class.new(event)
+
+ expect(service.registration_ids_for("linked")).to contain_exactly(registration.id)
+ expect(service.registration_ids_for("pending")).to be_empty
+ end
+
+ it "lists a registration that is both linked and pending under each status" do
+ registration = create(:event_registration, event: event)
+ organization = create(:organization, name: "Acme Org")
+ create(:event_registration_organization, event_registration: registration, organization: organization)
+ submit_agency_name(registration.registrant, "A Different Agency")
+
+ service = described_class.new(event)
+
+ expect(service.registration_ids_for("linked")).to contain_exactly(registration.id)
+ expect(service.registration_ids_for("pending")).to contain_exactly(registration.id)
+ expect(service.registration_ids_for("none")).to be_empty
+ end
+
+ it "returns an empty array for an unrecognized status" do
+ create(:event_registration, event: event)
+
+ expect(described_class.new(event).registration_ids_for("bogus")).to eq([])
+ end
+ end
+end