Show club health on the clubs list too

The list page carried the same vanity counts the dashboard just lost. It now shows
active members, unpaid members, money owed, teams (flagging those nobody can pick a
squad for), upcoming events and admins, with the same No season / Dormant badges --
and the annotations survive search and the archived filter.

The table lives in one partial, included by both pages, so they cannot drift apart.
Archived clubs are badged "Archived" rather than "Dormant": their subdomain does not
resolve, so of course nothing is scheduled, and flagging that as a problem would be
noise on the one page where every row has it.

assertNumQueries(1) covers the list as well now -- health is annotated per club, so
the page must not fan out as clubs are added.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 01:24:31 +02:00
parent 3a6dc00e05
commit 6899e203f6
3 changed files with 8 additions and 84 deletions

View File

@@ -24,39 +24,11 @@
</form> </form>
<div class="card bg-base-100 shadow"> <div class="card bg-base-100 shadow">
<div class="card-body"> <div class="card-body">
<div class="overflow-x-auto"> {% if show_archived %}
<table class="table"> {% include "controlpanel/_club_health_table.html" with empty_message="No archived clubs." %}
<thead> {% else %}
<tr> {% include "controlpanel/_club_health_table.html" %}
<th>Club</th> {% endif %}
<th>Members</th>
<th>Teams</th>
<th>Events</th>
<th>Admins</th>
</tr>
</thead>
<tbody>
{% for club in clubs %}
<tr>
<td>
<a class="link link-hover font-medium" href="{% url 'controlpanel:club_detail' club.pk %}">{{ club.name }}</a>
<div class="text-xs opacity-60">{{ club.slug }}</div>
</td>
<td>{{ club.member_count }}</td>
<td>{{ club.team_count }}</td>
<td>{{ club.event_count }}</td>
<td>{{ club.admin_count }}</td>
</tr>
{% empty %}
<tr>
<td colspan="5" class="text-center opacity-60">
{% if show_archived %}No archived clubs.{% else %}No clubs yet.{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div> </div>
</div> </div>
{% endblock panel %} {% endblock panel %}

View File

@@ -143,55 +143,7 @@
<div class="card bg-base-100 shadow"> <div class="card bg-base-100 shadow">
<div class="card-body"> <div class="card-body">
<h2 class="card-title">Clubs</h2> <h2 class="card-title">Clubs</h2>
{% comment %} {% include "controlpanel/_club_health_table.html" %}
Health, not vanity: a club's member count says nothing you can act on, while
"no coach", "nothing scheduled" and "€ owed" each name a thing somebody has to
go and fix. Every column here is annotated in the same single query.
{% endcomment %}
<div class="overflow-x-auto">
<table class="table">
<thead>
<tr>
<th>Club</th>
<th class="text-right">Members</th>
<th class="text-right">Unpaid</th>
<th class="text-right">Owed</th>
<th class="text-right">Teams</th>
<th class="text-right">Upcoming</th>
<th class="text-right">Admins</th>
</tr>
</thead>
<tbody>
{% for club in clubs %}
<tr>
<td>
<a class="link link-hover font-medium" href="{% url 'controlpanel:club_detail' club.pk %}">{{ club.name }}</a>
<div class="mt-1 flex flex-wrap items-center gap-1">
<span class="text-xs opacity-60">{{ club.slug }}</span>
{% if not club.has_season %}<span class="badge badge-warning badge-xs gap-1">{% lucide "calendar-x" size=10 %} No season</span>{% endif %}
{% if not club.upcoming_events %}<span class="badge badge-ghost badge-xs gap-1">{% lucide "moon-star" size=10 %} Dormant</span>{% endif %}
</div>
</td>
<td class="text-right tabular-nums">{{ club.active_members }}</td>
<td class="text-right tabular-nums {% if club.unpaid_members %}text-warning{% endif %}">{{ club.unpaid_members }}</td>
<td class="text-right tabular-nums {% if club.outstanding %}font-semibold text-error{% endif %}">€{{ club.outstanding|floatformat:2 }}</td>
<td class="text-right tabular-nums">
{{ club.team_count }}
{% if club.teams_without_coach %}
<span class="badge badge-error badge-xs ml-1" title="Teams with nobody able to pick the squad">{{ club.teams_without_coach }} no coach</span>
{% endif %}
</td>
<td class="text-right tabular-nums">{{ club.upcoming_events }}</td>
<td class="text-right tabular-nums {% if not club.admin_count %}text-error{% endif %}">{{ club.admin_count }}</td>
</tr>
{% empty %}
<tr>
<td colspan="7" class="text-center opacity-60">No clubs yet.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div> </div>
</div> </div>
{% endblock panel %} {% endblock panel %}

View File

@@ -17,7 +17,7 @@ from .services.platform_admins import (
revoke_platform_access, revoke_platform_access,
set_platform_access, set_platform_access,
) )
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 from .services.statistics import club_attention, club_charts, club_statistics, clubs_with_health, flag_adoption, onboarding_funnel, platform_attention, platform_charts, platform_totals
Flag = get_waffle_flag_model() Flag = get_waffle_flag_model()
Switch = get_waffle_switch_model() Switch = get_waffle_switch_model()
@@ -52,7 +52,7 @@ class ClubListView(PlatformStaffRequiredMixin, ListView):
search = self.request.GET.get("q", "").strip() search = self.request.GET.get("q", "").strip()
if search: if search:
clubs = clubs.filter(name__icontains=search) clubs = clubs.filter(name__icontains=search)
return clubs_with_totals(clubs) return clubs_with_health(clubs)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
return super().get_context_data(nav="clubs", show_archived=self.show_archived, search=self.request.GET.get("q", ""), **kwargs) return super().get_context_data(nav="clubs", show_archived=self.show_archived, search=self.request.GET.get("q", ""), **kwargs)