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) + + + + + + + + + + <% new_sign_ups.each do |row| %> + + + + + + <% end %> + + + + + + +
MonthStudentsCoaches
<%= sign_up_month_label(row[:month]) %><%= number_value(row[:students]) %><%= number_value(row[:coaches]) %>
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