Skip to content
Merged
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
13 changes: 12 additions & 1 deletion app/controllers/events_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,24 @@ def registrants
.includes(:comments, :organizations, registrant: [ :user, :contact_methods, { avatar_attachment: :blob } ])
.joins(:registrant)
scope = scope.keyword(params[:keyword]) if params[:keyword].present?
scope = scope.attendance_status(params[:attendance_status]) if params[:attendance_status].present?
scope = scope.payment_status(params[:payment_status]) if params[:payment_status].present?
scope = scope.scholarship_status(params[:scholarship]) if params[:scholarship].present?
scope = scope.registrant_ids(params[:registrant_ids]) if params[:registrant_ids].present?
scope = scope.registrant_state(params[:state]) if params[:state].present?
scope = scope.registrant_county(params[:county]) if params[:county].present?
scope = scope.registrant_sector(params[:sector]) if params[:sector].present?

@active_count = scope.active.count

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.

Counts are computed from the keyword-filtered scope before the active/inactive grouping is applied, so both tab counts always reflect the current search rather than only the visible tab.

@inactive_count = scope.inactive.count

if params[:attendance_status].present?
scope = scope.attendance_status(params[:attendance_status])
else
@status_filter = params[:status_filter].presence || "active"
scope = scope.inactive if @status_filter == "inactive"

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.

An explicit attendance_status takes precedence over the active/inactive default, preserving the existing dropdown drill-down. Absent that param, we default to the active filter.

scope = scope.active if @status_filter == "active"
end

@event_registrations = scope.order(Arel.sql("people.first_name, people.last_name"))
@dashboard = EventDashboard.new(@event)

Expand Down
1 change: 1 addition & 0 deletions app/models/event_registration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ class EventRegistration < ApplicationRecord
OR LOWER(REPLACE(people.last_name, ' ', '')) LIKE :name", name: "%#{registrant_name}%") }
scope :event_title, ->(event_title) { joins(:event).where("LOWER(events.title LIKE ?)", "%#{event_title}%") }
scope :active, -> { where(status: ACTIVE_STATUSES) }
scope :inactive, -> { where(status: INACTIVE_STATUSES) }
scope :registrant_ids, ->(ids) { where(registrant_id: ids.to_s.split("-").map(&:to_i)) }
scope :attendance_status, ->(status) { where(status: status) }
scope :registrant_state, ->(state) {
Expand Down
4 changes: 2 additions & 2 deletions app/views/comments/_comment_fields.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@
placeholder: "Topic",
data: { comment_required_target: "topic" },
class: "w-full bg-white rounded-md border border-gray-300 px-3 py-2 text-sm shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 focus:outline-none" %>
<p class="text-gray-500 text-xs mt-1">Reason you're making a note</p>
<p class="text-gray-500 text-xs mt-1">Reason for this note</p>
</div>
<%= f.input_field :body,
as: :text,
Expand Down Expand Up @@ -100,7 +100,7 @@
placeholder: "Topic",
data: { comment_required_target: "topic" },
class: "w-full bg-white rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 focus:outline-none" %>
<p class="text-gray-500 text-xs mt-1">Reason you're making a note</p>
<p class="text-gray-500 text-xs mt-1">Reason for this note</p>
</div>
<%= f.input_field :body,
as: :text,
Expand Down
2 changes: 1 addition & 1 deletion app/views/comments/_form.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<%= f.input_field :topic,
placeholder: "Topic",
class: "w-full bg-white rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 focus:outline-none" %>
<p class="text-gray-500 text-xs mt-1">Reason you're making a note</p>
<p class="text-gray-500 text-xs mt-1">Reason for this note</p>
</div>
<%= f.input_field :body,
as: :text,
Expand Down
41 changes: 29 additions & 12 deletions app/views/events/_registrants_results.html.erb
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
<%= turbo_frame_tag :registrants_results do %>
<div class="bg-white border border-gray-200 rounded-xl shadow-sm overflow-hidden blur-on-submit" data-controller="column-toggle">
<div class="flex items-center justify-between px-6 py-4 border-b border-gray-100">
<p class="text-sm text-gray-500"><%= @event_registrations.size %> registrant<%= "s" if @event_registrations.size != 1 %></p>
<% current_filter = params[:attendance_status].present? ? nil : (params[:status_filter].presence || "active") %>
<nav class="inline-flex rounded-lg bg-gray-100 p-0.5 text-sm font-medium" aria-label="Attendance filter">
<%= link_to registrants_event_path(@event, status_filter: "active", keyword: params[:keyword].presence),
data: { turbo_frame: "_top" },
class: "px-3 py-1 rounded-md transition-colors #{current_filter == "active" ? "bg-white text-gray-800 shadow-sm" : "text-gray-500 hover:text-gray-700"}" do %>

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.

Tabs use turbo_frame: "_top" (full-page nav) intentionally: the search form lives outside the manage_results frame, so a frame-only swap would leave the form's hidden status_filter stale and out of sync with the visible tab.

Active <span class="text-gray-400">(<%= @active_count %>)</span>
<% end %>
<%= link_to registrants_event_path(@event, status_filter: "inactive", keyword: params[:keyword].presence),
data: { turbo_frame: "_top" },
class: "px-3 py-1 rounded-md transition-colors #{current_filter == "inactive" ? "bg-white text-gray-800 shadow-sm" : "text-gray-500 hover:text-gray-700"}" do %>
Inactive <span class="text-gray-400">(<%= @inactive_count %>)</span>
<% end %>
</nav>

<label class="inline-flex items-center gap-2 cursor-pointer select-none">
<span class="text-sm text-gray-500">Confirmed</span>
<span class="text-sm text-gray-500">User confirmation</span>

<input
type="checkbox"
Expand All @@ -28,15 +40,15 @@
<thead class="bg-gray-100">
<tr>
<th class="px-4 py-2 text-left text-sm font-semibold text-gray-700">Name</th>
<th class="px-4 py-2 text-left text-sm font-semibold text-gray-700">Organization(s)</th>
<th class="px-4 py-2 text-left text-sm font-semibold text-gray-700">Scholarship</th>
<th class="px-4 py-2 text-left text-sm font-semibold text-gray-700 w-full">Organization</th>
<th class="px-4 py-2 text-center text-sm font-semibold text-gray-700">Scholarship</th>

<% if @event.cost_cents.to_i > 0 %>
<th class="px-4 py-2 text-left text-sm font-semibold text-gray-700">Payment status</th>
<th class="px-4 py-2 text-center text-sm font-semibold text-gray-700">Payment</th>
<% end %>

<th class="px-4 py-2 text-center text-sm font-semibold text-gray-700 w-[60px] hidden" data-column-toggle-col>Confirmed</th>
<th class="px-4 py-2 text-left text-sm font-semibold text-gray-700">Attendance status</th>
<th class="px-4 py-2 text-center text-sm font-semibold text-gray-700">Attendance</th>
<th class="px-4 py-2"></th>
</tr>
</thead>
Expand All @@ -50,7 +62,7 @@
<% show_email = person.profile_show_email? || allowed_to?(:manage?, Person) %>

<div class="flex items-center gap-1">
<div class="w-64 min-w-0">
<div class="w-52 min-w-0">
<%= person_profile_button(person, subtitle: (person.preferred_email if show_email), data: { turbo_frame: "_top" }) %>
</div>

Expand Down Expand Up @@ -84,7 +96,12 @@
<% if registration.organizations.any? %>
<ul class="space-y-0.5">
<% registration.organizations.each do |org| %>
<li title="<%= org.name %>"><%= truncate(org.name, length: 10) %></li>
<li>
<%= link_to truncate(org.name, length: 40), organization_path(org),
title: org.name,
class: "text-gray-800 hover:text-gray-900 hover:underline",
data: { turbo_frame: "_top" } %>
</li>
<% end %>
</ul>
<% else %>
Expand All @@ -96,7 +113,7 @@
<% end %>
</td>

<td class="px-4 py-2 text-sm">
<td class="px-4 py-2 text-sm text-center">
<% if (s = registration.scholarships.first) %>
<% if s.tasks_completed? %>
<%= link_to edit_scholarship_path(s),
Expand Down Expand Up @@ -133,12 +150,12 @@
</td>

<% if @event.cost_cents.to_i > 0 %>
<td class="px-4 py-2 text-sm">
<td class="px-4 py-2 text-sm text-center">
<% paid_cents = registration.allocations_sum %>
<% due_cents = @event.cost_cents - paid_cents %>
<% is_paid = registration.paid_in_full? %>

<%= link_to allocations_path(allocatable_sgid: registration.to_sgid.to_s), class: "inline-flex items-center gap-1.5 rounded-full text-xs font-medium border px-5 py-0.5 #{is_paid ? 'bg-green-50 text-green-700 border-green-200' : 'bg-amber-50 text-amber-700 border-amber-200'} hover:opacity-80", title: (!is_paid && paid_cents > 0 ? "$%.2f paid" % (paid_cents / 100.0) : nil), data: { turbo_frame: "_top" } do %>
<%= link_to allocations_path(allocatable_sgid: registration.to_sgid.to_s), class: "inline-flex items-center gap-1.5 whitespace-nowrap rounded-full text-xs font-medium border px-5 py-0.5 #{is_paid ? 'bg-green-50 text-green-700 border-green-200' : 'bg-amber-50 text-amber-700 border-amber-200'} hover:opacity-80", title: (!is_paid && paid_cents > 0 ? "$%.2f paid" % (paid_cents / 100.0) : nil), data: { turbo_frame: "_top" } do %>
<i class="fas <%= is_paid ? 'fa-circle-check' : 'fa-circle-exclamation' %>"></i>
<span><%= is_paid ? 'Paid' : "$%.2f due" % (due_cents / 100.0) %></span>
<% end %>
Expand All @@ -156,7 +173,7 @@
<% end %>
</td>

<td class="px-4 py-2 text-sm text-nowrap"><%= render "event_registrations/attendance_status_badge", registration: registration %></td>
<td class="px-4 py-2 text-sm text-nowrap text-center"><%= render "event_registrations/attendance_status_badge", registration: registration %></td>
<td class="px-4 py-2 text-right text-sm"><%= link_to "Edit", edit_event_registration_path(registration, return_to: "registrants"), class: "text-gray-500 hover:text-gray-700 underline", data: { turbo_frame: "_top" } %></td>
</tr>
<% end %>
Expand Down
2 changes: 2 additions & 0 deletions app/views/events/_registrants_search.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
autocomplete: "off",
class: "flex flex-col md:flex-row md:flex-wrap md:items-end gap-4 mb-6" do %>

<%= hidden_field_tag :status_filter, params[:status_filter].presence || "active" %>

<div class="w-full md:flex-1">
<%= label_tag :keyword, "Keyword", class: "block text-sm font-medium text-gray-700 mb-1" %>
<div class="relative">
Expand Down
4 changes: 2 additions & 2 deletions app/views/events/registrants.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@
<div class="flex flex-wrap items-center justify-between gap-4 mb-6">
<div>
<h1 class="text-2xl font-semibold text-gray-900">
Registrants
Registrants (<%= @active_count %>)
</h1>
<p class="text-gray-600 mt-1">
<%= @event.title %>
<%= @event.title %><% if @event.start_date.present? %> &bull; <%= @event.date_range %><% end %>
</p>
</div>
<div class="flex flex-wrap items-center gap-1">
Expand Down
53 changes: 53 additions & 0 deletions spec/requests/events_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,59 @@
end
end

context "active/inactive filtering" do
let(:active_person) { create(:person, first_name: "Activa", last_name: "Attendee") }
let(:inactive_person) { create(:person, first_name: "Inactiva", last_name: "Cancelled") }
let!(:active_registration) { create(:event_registration, event: event, registrant: active_person, status: "attended") }
let!(:inactive_registration) { create(:event_registration, event: event, registrant: inactive_person, status: "cancelled") }

it "shows active and inactive counts" do
get registrants_event_path(event)

# registration (default "registered") + active_registration are active
expect(response.body).to include("Active <span class=\"text-gray-400\">(2)</span>")
expect(response.body).to include("Inactive <span class=\"text-gray-400\">(1)</span>")
end

it "defaults to the active filter, hiding inactive registrants" do
get registrants_event_path(event)

expect(response.body).to include("Activa")
expect(response.body).not_to include("Inactiva")
end

it "shows only inactive registrants when filtered to inactive" do
get registrants_event_path(event, params: { status_filter: "inactive" })

expect(response.body).to include("Inactiva")
expect(response.body).not_to include("Activa")
end

it "honors an explicit attendance_status over the active/inactive filter" do
get registrants_event_path(event, params: { attendance_status: "cancelled" })

expect(response.body).to include("Inactiva")
expect(response.body).not_to include("Activa")
end

it "shows the active registrant count in the page heading" do
get registrants_event_path(event)

expect(response.body).to include("Registrants (2)")
end
end

context "event heading" do
it "shows the event title and date range after the heading" do
event.update!(start_date: Time.zone.local(2026, 6, 2, 9), end_date: Time.zone.local(2026, 6, 2, 17))

get registrants_event_path(event)

expect(response.body).to include(event.title)
expect(response.body).to include(event.decorate.date_range)
end
end

context "confirmed column toggle" do
it "renders the slide toggle for confirmed column" do
get registrants_event_path(event)
Expand Down