Split platform signups, and rework the club table into health

The platform signups chart gets the same stacked new/returning split as the club
one. "First season" is keyed on (club, member), never the member alone: the same
person can be new at one club while renewing at another, and collapsing that would
file their second club's very first signup as a renewal.

The dashboard's club table stops reporting vanity counts. A member total says
nothing you can act on; "no coach", "nothing scheduled", "€ owed" and "no admins"
each name something somebody has to go and fix. Columns are now active members,
unpaid members, money owed, teams (flagging those nobody can pick a squad for),
upcoming events, and admins -- with No season / Dormant badges on the club itself.

Every column is annotated in ONE query, each aggregate in its own subquery. That
is not stylistic: aggregates spanning different joins multiply each other's rows,
so a Sum of orders sitting next to a Count of memberships returns the club's debt
multiplied by its membership count. A test pins €100 against three memberships and
would catch it coming back as €300.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 01:17:26 +02:00
parent 639807b2d2
commit 192fe5ad0e
5 changed files with 203 additions and 31 deletions

View File

@@ -17,7 +17,7 @@ from .services.platform_admins import (
revoke_platform_access,
set_platform_access,
)
from .services.statistics import club_attention, club_charts, club_statistics, clubs_with_totals, flag_adoption, onboarding_funnel, platform_attention, platform_charts, platform_totals
from .services.statistics import club_attention, club_charts, club_statistics, clubs_with_health, clubs_with_totals, flag_adoption, onboarding_funnel, platform_attention, platform_charts, platform_totals
Flag = get_waffle_flag_model()
Switch = get_waffle_switch_model()
@@ -34,7 +34,7 @@ class DashboardView(PlatformStaffRequiredMixin, TemplateView):
funnel=onboarding_funnel(),
flags=flag_adoption(),
charts=platform_charts(),
clubs=clubs_with_totals(Club.objects.active()),
clubs=clubs_with_health(),
**kwargs,
)