Add families list, member kind filter, sidebar counters, pagination

New Families list page (parents/children columns, edit goes to the
detail view). Members list gets a member/guardian/both filter so
guardians aren't just invisible. Sidebar now shows live counts for
pending parent claims and games missing a referee, always numeric.
Member, event, team, group, news and family lists are paginated with
a shared pager partial that preserves the query string.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-11 23:53:00 +02:00
parent b2657bb15d
commit 4be10a57e0
13 changed files with 550 additions and 17 deletions

View File

@@ -16,8 +16,14 @@
<li class="menu-title">{% trans "People" %}</li>
<li><a class="{% if nav == 'member_list' %}menu-active{% endif %}" href="{% url 'management:member_list' %}">{% lucide "users" size=16 %} {% trans "Members" %}</a></li>
<li><a class="{% if nav == 'family_list' %}menu-active{% endif %}" href="{% url 'management:family_list' %}">{% lucide "home" size=16 %} {% trans "Families" %}</a></li>
{% if is_club_admin %}
<li><a class="{% if nav == 'parent_claim_list' %}menu-active{% endif %}" href="{% url 'management:parent_claim_list' %}">{% lucide "inbox" size=16 %} {% trans "Parent claims" %}</a></li>
<li>
<a class="{% if nav == 'parent_claim_list' %}menu-active{% endif %}" href="{% url 'management:parent_claim_list' %}">
{% lucide "inbox" size=16 %} {% trans "Parent claims" %}
<span class="badge badge-sm ml-auto {% if pending_parent_claims_count %}badge-error{% else %}badge-neutral{% endif %}">{{ pending_parent_claims_count }}</span>
</a>
</li>
{% endif %}
{% if is_club_admin %}
<li><a class="{% if nav == 'membership_list' %}menu-active{% endif %}" href="{% url 'management:membership_list' %}">{% lucide "wallet" size=16 %} {% trans "Memberships" %}</a></li>
@@ -30,7 +36,12 @@
<li><a class="{% if nav == 'position_list' %}menu-active{% endif %}" href="{% url 'management:position_list' %}">{% lucide "tags" size=16 %} {% trans "Positions" %}</a></li>
{% if is_club_admin %}
<li><a class="{% if nav == 'referee_level_list' %}menu-active{% endif %}" href="{% url 'management:referee_level_list' %}">{% lucide "badge-check" size=16 %} {% trans "Referee levels" %}</a></li>
<li><a class="{% if nav == 'referee_management' %}menu-active{% endif %}" href="{% url 'management:referee_management' %}">{% lucide "calendar-check" size=16 %} {% trans "Referee management" %}</a></li>
<li>
<a class="{% if nav == 'referee_management' %}menu-active{% endif %}" href="{% url 'management:referee_management' %}">
{% lucide "calendar-check" size=16 %} {% trans "Referee management" %}
<span class="badge badge-sm ml-auto {% if games_missing_referees_count %}badge-error{% else %}badge-neutral{% endif %}">{{ games_missing_referees_count }}</span>
</a>
</li>
{% endif %}
<li><a class="{% if nav == 'referee_list' %}menu-active{% endif %}" href="{% url 'management:referee_list' %}">{% lucide "flag" size=16 %} {% trans "Referees" %}</a></li>

View File

@@ -0,0 +1,32 @@
{% load i18n lucide %}
{% comment %}
Shared pager for every paginated management list (see paginate_by on
MemberListView/EventListView/TeamListView/GroupListView/NewsListView/
FamilyListView) -- {% include %} this once rather than duplicating prev/next
markup per template. Expects the standard ListView pagination context
(page_obj/paginator/is_paginated); renders nothing on an unpaginated page.
{% querystring %} (built-in since Django 5.1) rebuilds the current query
string with just `page` overridden, so an active ?q=/?kind=/?season= filter
survives a page link instead of being dropped.
{% endcomment %}
{% if is_paginated %}
<div class="mt-4 flex flex-wrap items-center justify-between gap-2">
<span class="text-sm opacity-60">
{% blocktrans with number=page_obj.number total=paginator.num_pages %}Page {{ number }} of {{ total }}{% endblocktrans %}
</span>
<div class="join">
{% if page_obj.has_previous %}
<a href="{% querystring page=page_obj.previous_page_number %}" class="btn btn-sm join-item">{% lucide "chevron-left" size=14 %} {% trans "Previous" %}</a>
{% else %}
<span class="btn btn-sm join-item btn-disabled">{% lucide "chevron-left" size=14 %} {% trans "Previous" %}</span>
{% endif %}
{% if page_obj.has_next %}
<a href="{% querystring page=page_obj.next_page_number %}" class="btn btn-sm join-item">{% trans "Next" %} {% lucide "chevron-right" size=14 %}</a>
{% else %}
<span class="btn btn-sm join-item btn-disabled">{% trans "Next" %} {% lucide "chevron-right" size=14 %}</span>
{% endif %}
</div>
</div>
{% endif %}

View File

@@ -80,6 +80,8 @@
</div>
</div>
{% include "management/_pagination.html" %}
{% comment %} Dialogs live outside the table: <tbody> may only contain <tr> elements. {% endcomment %}
{% for event in events %}
{% if event.can_manage %}

View File

@@ -0,0 +1,61 @@
{% extends "management/base.html" %}
{% load i18n lucide %}
{% block heading %}{% trans "Families" %}{% endblock heading %}
{% block subheading %}{% trans "Every household on file -- who their parents/guardians and children are." %}{% endblock subheading %}
{% block actions %}
{% if is_club_admin %}
<a class="btn btn-outline gap-2" href="{% url 'management:family_create' %}">{% lucide "users" size=16 %} {% trans "Add family" %}</a>
{% endif %}
{% endblock actions %}
{% block panel %}
<div class="card bg-base-100 shadow">
<div class="card-body">
<div class="overflow-x-auto">
<table class="table">
<thead>
<tr>
<th>{% trans "Family" %}</th>
<th>{% trans "Parents / guardians" %}</th>
<th>{% trans "Children" %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for family in families %}
<tr>
<td><a class="link link-hover font-semibold" href="{% url 'management:family_detail' family.pk %}">{{ family }}</a></td>
<td>
{% for guardian in family.guardians_display %}
<a class="link link-hover" href="{% url 'management:member_detail' guardian.pk %}">{{ guardian }}</a>{% if not forloop.last %}, {% endif %}
{% empty %}
<span class="opacity-40">-</span>
{% endfor %}
</td>
<td>
{% for child in family.children_display %}
<a class="link link-hover" href="{% url 'management:member_detail' child.pk %}">{{ child }}</a>{% if not forloop.last %}, {% endif %}
{% empty %}
<span class="opacity-40">-</span>
{% endfor %}
</td>
<td class="text-right">
{# Same convention as Groups: "Edit" lands on the overview page, not a standalone rename form -- family_detail is where every family action (add parent/child, change role, remove) actually lives. #}
<a class="btn btn-outline btn-sm" href="{% url 'management:family_detail' family.pk %}" aria-label="{% trans 'Edit' %}">{% lucide "pencil" size=14 %} {% trans "Edit" %}</a>
</td>
</tr>
{% empty %}
<tr>
<td colspan="4" class="text-center opacity-60">{% trans "No families yet." %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% include "management/_pagination.html" %}
{% endblock panel %}

View File

@@ -43,6 +43,8 @@
</div>
</div>
{% include "management/_pagination.html" %}
{% trans "Delete group" as delete_group_title %}
{% trans "Delete" as delete_label %}
{% for group in groups %}

View File

@@ -13,17 +13,35 @@
{% endblock actions %}
{% block panel %}
<form method="get" class="mb-4 flex gap-2">
<form method="get" class="mb-2 flex flex-wrap items-center gap-2">
<label class="input">
<span class="opacity-50">{% lucide "search" size=16 %}</span>
<input type="search" name="q" value="{{ search }}" placeholder="{% trans 'Search members ...' %}" class="input input-bordered w-full max-w-xs">
</label>
{% if selected_kind != "member" %}<input type="hidden" name="kind" value="{{ selected_kind }}">{% endif %}
<button class="btn btn-outline gap-2" type="submit">{% lucide "search" size=16 %} {% trans "Search" %}</button>
{% if search %}
<a class="btn gap-2" href="{% url "management:member_list" %}">{% lucide "x" size=16 %} {% trans "Clear filter" %}</a>
{% endif %}
</form>
{% comment %}
Guardians -- parents attached to the club only through a child -- have no
fee and aren't counted as members, so members_visible_to() leaves them out
of the default list entirely. Without this filter that's silent: a parent
just registered via "Add family"/"Add parent" simply doesn't appear here,
with nothing on the page explaining why. ?kind= makes the exclusion an
explicit, reversible choice instead.
{% endcomment %}
<div class="mb-4 flex flex-wrap items-center gap-2">
<div class="join">
<a href="{% querystring kind=None page=None %}" class="btn btn-sm join-item {% if selected_kind == "member" %}btn-primary{% endif %}">{% trans "Members" %}</a>
<a href="{% querystring kind="guardian" page=None %}" class="btn btn-sm join-item {% if selected_kind == "guardian" %}btn-primary{% endif %}">{% trans "Guardians" %}</a>
<a href="{% querystring kind="both" page=None %}" class="btn btn-sm join-item {% if selected_kind == "both" %}btn-primary{% endif %}">{% trans "Both" %}</a>
</div>
<span class="text-xs opacity-60">{% trans "Guardians are parents linked only through a child -- no fee, not counted as a member." %}</span>
</div>
<div class="card bg-base-100 shadow">
<div class="card-body">
<div class="overflow-x-auto">
@@ -90,6 +108,8 @@
</div>
</div>
{% include "management/_pagination.html" %}
{% if is_club_admin %}
{% trans "Delete member" as delete_member_title %}
{% trans "Delete" as delete_label %}

View File

@@ -64,6 +64,8 @@
</div>
</div>
{% include "management/_pagination.html" %}
{% trans "Delete news item" as delete_news_title %}
{% trans "Delete" as delete_label %}
{% for news_item in news_items %}

View File

@@ -50,6 +50,8 @@
</div>
</div>
{% include "management/_pagination.html" %}
{% if is_club_admin %}
{% trans "Delete team" as delete_team_title %}
{% trans "Delete" as delete_label %}