diff --git a/app/components/admin/dashboard/health_card_component.html.erb b/app/components/admin/dashboard/health_card_component.html.erb
index 66c5a7329..447b63bed 100644
--- a/app/components/admin/dashboard/health_card_component.html.erb
+++ b/app/components/admin/dashboard/health_card_component.html.erb
@@ -1,6 +1,6 @@
-
Chapter Health
+
Chapter Health
<% tiles.each do |tile| %>
@@ -19,6 +19,32 @@
<%= content %>
+
+
New sign-ups (last 3 months)
+
+
+
+ | Month |
+ Students |
+ Coaches |
+
+
+
+ <% new_sign_ups.each do |row| %>
+
+ | <%= sign_up_month_label(row[:month]) %> |
+ <%= number_value(row[:students]) %> |
+ <%= number_value(row[:coaches]) %> |
+
+ <% end %>
+
+ | Total |
+ <%= number_value(sign_up_totals[:students]) %> |
+ <%= number_value(sign_up_totals[:coaches]) %> |
+
+
+
+
diff --git a/app/components/admin/dashboard/health_card_component.rb b/app/components/admin/dashboard/health_card_component.rb
index c6875ca7a..7d99daf52 100644
--- a/app/components/admin/dashboard/health_card_component.rb
+++ b/app/components/admin/dashboard/health_card_component.rb
@@ -9,13 +9,14 @@ class HealthCardComponent < ViewComponent::Base
BUCKET_CLASSES = { inactive: 'bg-secondary', dormant: 'bg-warning text-dark',
active: 'bg-success' }.freeze
- def initialize(health:) # rubocop:disable Lint/MissingSuper
+ def initialize(health:, new_sign_ups: []) # rubocop:disable Lint/MissingSuper
@health = health
+ @new_sign_ups = new_sign_ups
end
private
- attr_reader :health
+ attr_reader :health, :new_sign_ups
def bucket_badge_class = BUCKET_CLASSES.fetch(health.bucket)
@@ -54,6 +55,15 @@ def eligible_members_value
number_value(health.eligible_students + health.eligible_coaches)
end
+ def sign_up_month_label(month)
+ month.strftime('%b %Y')
+ end
+
+ def sign_up_totals
+ { students: new_sign_ups.sum { |row| row[:students] },
+ coaches: new_sign_ups.sum { |row| row[:coaches] } }
+ end
+
def number_value(value)
number_with_delimiter(value)
end
diff --git a/app/controllers/admin/chapters_controller.rb b/app/controllers/admin/chapters_controller.rb
index 9ccf4ac19..3109244db 100644
--- a/app/controllers/admin/chapters_controller.rb
+++ b/app/controllers/admin/chapters_controller.rb
@@ -34,6 +34,7 @@ def show
@groups = @chapter.groups
@subscribers = @chapter.subscriptions.kept.last(20).reverse
@how_you_found_us = HowYouFoundUsPresenter.new(@chapter)
+ @new_sign_ups = new_sign_ups(@chapter)
end
def edit
@@ -117,6 +118,33 @@ def set_chapter
@chapter = Chapter.find(params[:id])
end
+ def new_sign_ups(chapter)
+ months = new_sign_up_months
+ counts = sign_up_counts(chapter, months)
+ months.map do |month|
+ { month: month.to_date,
+ students: counts.fetch([month.to_date, 'Students'], 0),
+ coaches: counts.fetch([month.to_date, 'Coaches'], 0) }
+ end
+ end
+
+ def sign_up_counts(chapter, months)
+ chapter.subscriptions
+ .where(created_at: months.first.beginning_of_day..)
+ .joins(:group)
+ .group(month_bucket, 'groups.name')
+ .pluck(month_bucket, 'groups.name', Arel.sql('COUNT(*)'))
+ .to_h { |month, name, count| [[month, name], count] }
+ end
+
+ def new_sign_up_months
+ [2.months.ago, 1.month.ago, Time.current].map(&:beginning_of_month)
+ end
+
+ def month_bucket
+ Arel.sql("DATE_TRUNC('month', subscriptions.created_at AT TIME ZONE '#{Time.zone.tzinfo.identifier}')::date")
+ end
+
def member_emails(chapter, type)
members =
case type
diff --git a/app/views/admin/chapters/show.html.haml b/app/views/admin/chapters/show.html.haml
index 44607031a..c2d5e28dc 100644
--- a/app/views/admin/chapters/show.html.haml
+++ b/app/views/admin/chapters/show.html.haml
@@ -58,7 +58,7 @@
%div.text-muted.small= "Based on #{pluralize(@how_you_found_us.total_responses, 'response')}"
.col-12.col-lg-8
- = render Admin::Dashboard::HealthCardComponent.new(health: @chapter_health) do
+ = render Admin::Dashboard::HealthCardComponent.new(health: @chapter_health, new_sign_ups: @new_sign_ups) do
= render Admin::Dashboard::WorkshopTimelineComponent.new(past: @past_workshop_dates, future: @planned_workshop_dates, event_past: @past_event_dates, event_future: @planned_event_dates)
.mb-4.mt-md-4.mt-lg-0
.d-md-flex.justify-content-between.align-items-center
diff --git a/spec/components/admin/dashboard/health_card_component_spec.rb b/spec/components/admin/dashboard/health_card_component_spec.rb
index 050205754..1778b61e3 100644
--- a/spec/components/admin/dashboard/health_card_component_spec.rb
+++ b/spec/components/admin/dashboard/health_card_component_spec.rb
@@ -38,6 +38,24 @@
expect(page).to have_css('.badge', text: 'inactive')
end
+ it 'renders new sign-ups split across the last 3 months' do
+ health = Admin::Dashboard::ChapterHealth.row(chapter:)
+ sign_ups = [
+ { month: Date.new(2026, 9, 1), students: 5, coaches: 3 },
+ { month: Date.new(2026, 10, 1), students: 8, coaches: 4 },
+ { month: Date.new(2026, 11, 1), students: 2, coaches: 1 }
+ ]
+
+ render_inline(described_class.new(health:, new_sign_ups: sign_ups))
+
+ expect(page).to have_text('New sign-ups (last 3 months)')
+ expect(page).to have_css('th', text: 'Sep 2026')
+ expect(page).to have_css('th', text: 'Oct 2026')
+ expect(page).to have_css('th', text: 'Nov 2026')
+ expect(page).to have_css('tr', text: /Sep 2026\s+5\s+3/)
+ expect(page).to have_css('tr.fw-bold', text: /Total\s+15\s+8/)
+ end
+
it 'renders the yielded content inside the card' do
health = Admin::Dashboard::ChapterHealth.row(chapter:)
diff --git a/spec/controllers/admin/chapters_controller_spec.rb b/spec/controllers/admin/chapters_controller_spec.rb
index 780d20b21..b90d116dc 100644
--- a/spec/controllers/admin/chapters_controller_spec.rb
+++ b/spec/controllers/admin/chapters_controller_spec.rb
@@ -125,5 +125,28 @@
expect(response).to be_successful
expect(response.body).to include('Chapter Health')
end
+
+ it 'shows new sign-ups split across the last 3 months' do
+ travel_to(Time.zone.local(2026, 11, 6, 12, 0)) do
+ login_as_admin(Fabricate(:member))
+ chapter = Fabricate(:chapter)
+ students = Fabricate(:group, name: 'Students', chapter:)
+ coaches = Fabricate(:group, name: 'Coaches', chapter:)
+ Fabricate(:subscription, group: students, created_at: Time.zone.local(2026, 9, 10))
+ Fabricate(:subscription, group: coaches, created_at: Time.zone.local(2026, 10, 10))
+ Fabricate(:subscription, group: students, created_at: Time.zone.local(2026, 11, 2))
+ Fabricate(:subscription, group: students, created_at: Time.zone.local(2026, 8, 20))
+
+ get :show, params: { id: chapter.id }
+
+ expect(response.body).to include('New sign-ups (last 3 months)')
+ expected = [
+ { month: Date.new(2026, 9, 1), students: 1, coaches: 0 },
+ { month: Date.new(2026, 10, 1), students: 0, coaches: 1 },
+ { month: Date.new(2026, 11, 1), students: 1, coaches: 0 }
+ ]
+ expect(controller.view_assigns['new_sign_ups']).to eq(expected)
+ end
+ end
end
end