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
28 changes: 27 additions & 1 deletion app/components/admin/dashboard/health_card_component.html.erb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<div class="card border-info mb-4">
<div class="card-body">
<h3>Chapter Health</h3>
<h3 class="mb-4">Chapter Health</h3>
<div class="row">
<% tiles.each do |tile| %>
<div class="col-6 col-md-3 text-center mb-2">
Expand All @@ -19,6 +19,32 @@
<div class="col-12">
<%= content %>
</div>
<div class="col-12 mt-4">
<strong class="small mb-2 d-block">New sign-ups (last 3 months)</strong>
<table class="table table-sm align-middle mb-0">
<thead>
<tr>
<th scope="col" class="small fw-normal text-muted">Month</th>
<th scope="col" class="small fw-normal text-muted text-end">Students</th>
<th scope="col" class="small fw-normal text-muted text-end">Coaches</th>
</tr>
</thead>
<tbody>
<% new_sign_ups.each do |row| %>
<tr>
<th scope="row" class="fw-normal"><%= sign_up_month_label(row[:month]) %></th>
<td class="text-end"><%= number_value(row[:students]) %></td>
<td class="text-end"><%= number_value(row[:coaches]) %></td>
</tr>
<% end %>
<tr class="fw-bold">
<th scope="row">Total</th>
<td class="text-end"><%= number_value(sign_up_totals[:students]) %></td>
<td class="text-end"><%= number_value(sign_up_totals[:coaches]) %></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
14 changes: 12 additions & 2 deletions app/components/admin/dashboard/health_card_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand Down
28 changes: 28 additions & 0 deletions app/controllers/admin/chapters_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion app/views/admin/chapters/show.html.haml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 18 additions & 0 deletions spec/components/admin/dashboard/health_card_component_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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:)

Expand Down
23 changes: 23 additions & 0 deletions spec/controllers/admin/chapters_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading