Files
RosterChief/management/templates/management/member_list.html
Bernard Siebens adf1120358 Checkpoint: management app redesign, onboarding/signup workflow, and events calendar backend
Large uncommitted body of work accumulated across sessions on this branch --
committing as a checkpoint so it's tracked and future worktree-isolated agents
see the real codebase instead of a stale ancestor commit. Covers the
management app's dedicated Tailwind theme and templates, the club onboarding
requirement/signup workflow (club/services/onboarding.py, requirement/status
models, sign-up dashboard), fee/status auto-activation decoupling, referee
management, and the new events calendar grid service layer.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
2026-08-19 23:34:43 +02:00

203 lines
14 KiB
HTML

{% extends "management/base.html" %}
{% load lucide ui i18n %}
{% block heading %}{% trans "Members" %}{% endblock heading %}
{% block topbar_context %}
<span class="font-mono text-[13px] text-muted">{% blocktrans count counter=paginator.count %}{{ counter }} member{% plural %}{{ counter }} members{% endblocktrans %}</span>
{% endblock topbar_context %}
{% block actions %}
<a class="btn btn-outline gap-2" href="{% url 'management:member_import_template' %}">{% lucide "download" size=16 %} {% trans "Download upload template" %}</a>
{% if is_club_admin %}
<a class="btn btn-outline gap-2" href="{% url 'management:member_import' %}">{% lucide "upload" size=16 %} {% trans "Mass upload" %}</a>
<a class="btn btn-outline gap-2" href="{% url 'management:family_create' %}">{% lucide "users" size=16 %} {% trans "Add family" %}</a>
<a class="btn btn-primary gap-2" href="{% url 'management:member_create' %}">{% lucide "user-plus" size=16 %} {% trans "Add member" %}</a>
{% endif %}
{% endblock actions %}
{% block filter_strip %}
<div class="flex flex-wrap items-center gap-3 border-b border-line bg-white px-7 py-2.5">
<form method="get" id="member-filters" class="flex flex-wrap items-center gap-2">
{% if selected_kind != "member" %}<input type="hidden" name="kind" value="{{ selected_kind }}">{% endif %}
<div class="relative">
<span class="pointer-events-none absolute top-1/2 left-3 -translate-y-1/2 text-dim">{% lucide "search" size=14 %}</span>
{# No submit button -- data-autosubmit (base.html-loaded script) submits the form itself, debounced, as soon as typing pauses. #}
<input type="search" name="q" value="{{ search }}" placeholder="{% trans 'Name, email, licence no.' %}" class="input w-56 pl-9" data-autosubmit>
</div>
<select name="status" class="select w-auto" onchange="this.form.submit()">
<option value="">{% trans "Any status" %}</option>
{% for value, label in status_choices %}
<option value="{{ value }}" {% if selected_status == value %}selected{% endif %}>{{ label|capfirst }}</option>
{% endfor %}
</select>
<select name="fee_status" class="select w-auto" onchange="this.form.submit()">
<option value="">{% trans "Any fee status" %}</option>
{% for value, label in fee_status_choices %}
<option value="{{ value }}" {% if selected_fee_status == value %}selected{% endif %}>{{ label|capfirst }}</option>
{% endfor %}
</select>
<select name="team" class="select w-auto" onchange="this.form.submit()">
<option value="">{% trans "Any team" %}</option>
{% for team in teams %}
<option value="{{ team.pk }}" {% if selected_team == team.pk|stringformat:"s" %}selected{% endif %}>{{ team.name }}</option>
{% endfor %}
</select>
{# Boolean toggles rather than another dropdown -- both are reached most often from the dashboard's own "Needs attention" links (home.html), this just makes them equally reachable/visible from here. #}
{% if selected_unrostered %}
<a href="{% querystring unrostered=None page=None %}" class="btn btn-primary gap-2">{% trans "Unrostered" %}</a>
{% else %}
<a href="{% querystring unrostered="1" page=None %}" class="btn btn-outline gap-2">{% trans "Unrostered" %}</a>
{% endif %}
{% if requirements_configured %}
{% if selected_docs %}
<a href="{% querystring docs=None page=None %}" class="btn btn-primary gap-2">{% trans "Docs open" %}</a>
{% else %}
<a href="{% querystring docs="open" page=None %}" class="btn btn-outline gap-2">{% trans "Docs open" %}</a>
{% endif %}
{% endif %}
{% if search or selected_status or selected_fee_status or selected_team or selected_unrostered or selected_docs %}
{# No btn-sm: the filter fields (.input/.select) are all height:2.25rem, matching plain .btn -- btn-sm would sit 6px shorter and misalign in the row. #}
<a class="btn btn-outline gap-2" href="{% querystring q=None status=None fee_status=None team=None unrostered=None docs=None page=None %}">{% lucide "x" size=14 %} {% trans "Clear filters" %}</a>
{% endif %}
</form>
<div class="flex-1"></div>
{% 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. This segmented control makes the
exclusion an explicit, reversible choice instead.
{% endcomment %}
<div class="flex flex-col items-end gap-1">
<div class="flex items-center gap-1 rounded-full bg-steel p-1">
<a href="{% querystring kind=None page=None %}" class="rounded-full px-3 py-1.5 font-display text-xs font-bold tracking-[.08em] uppercase {% if selected_kind == "member" %}bg-white text-ink{% else %}text-on-dark hover:text-white{% endif %}">{% trans "Members" %}</a>
<a href="{% querystring kind="guardian" page=None %}" class="rounded-full px-3 py-1.5 font-display text-xs font-bold tracking-[.08em] uppercase {% if selected_kind == "guardian" %}bg-white text-ink{% else %}text-on-dark hover:text-white{% endif %}">{% trans "Guardians" %}</a>
<a href="{% querystring kind="both" page=None %}" class="rounded-full px-3 py-1.5 font-display text-xs font-bold tracking-[.08em] uppercase {% if selected_kind == "both" %}bg-white text-ink{% else %}text-on-dark hover:text-white{% endif %}">{% trans "Both" %}</a>
</div>
<span class="font-mono text-xs text-dim">{% trans "Guardians have no fee and aren't counted as members." %}</span>
</div>
</div>
{% endblock filter_strip %}
{% block panel %}
<div class="card overflow-hidden">
<table class="table">
<thead>
<tr>
<th>{% trans "Member" %}</th>
<th>{% trans "Email" %}</th>
<th>{% trans "Licence" %}</th>
<th>{% trans "Family" %}</th>
<th>{% trans "Status" %}</th>
{% if requirements_configured %}<th>{% trans "Documents" %}</th>{% endif %}
{% if is_club_admin %}<th>{% trans "Dues" %}</th>{% endif %}
<th></th>
</tr>
</thead>
<tbody>
{% for member in members %}
<tr>
<td>
<div class="flex items-center gap-2.5">
<span class="avatar avatar-placeholder shrink-0">
<div class="h-8 w-8 rounded-full text-xs">{{ member.first_name|slice:":1" }}{{ member.last_name|slice:":1" }}</div>
</span>
<div>
<div class="flex items-center gap-1.5">
<a class="link link-hover font-semibold text-ink" href="{% url 'management:member_detail' member.pk %}">{{ member.last_name }}, {{ member.first_name }}</a>
{% if selected_kind == "both" and member.current_membership.kind == "guardian" %}
<span class="badge badge-ghost badge-xs">{% trans "Guardian" %}</span>
{% endif %}
</div>
{% if member.date_of_birth %}<div class="font-mono text-xs text-muted">{{ member.date_of_birth|date:"Y" }}</div>{% endif %}
</div>
</div>
</td>
<td class="text-sm text-muted">{{ member.contact_email|default:"—" }}</td>
{# text-xs, not text-sm: IBM Plex Mono reads noticeably larger than Barlow at the same declared size. #}
<td class="font-mono text-xs text-muted">{{ member.current_membership.license|default:"—" }}</td>
<td>
{% if member.family_memberships_display %}
<div class="flex flex-wrap gap-1">
{% for fm in member.family_memberships_display %}
<a class="btn btn-outline btn-xs gap-1" href="{% url 'management:family_detail' fm.family.pk %}">{% lucide "users" size=12 %} {{ fm.family }}</a>
{% endfor %}
</div>
{% else %}
<span class="text-dim"></span>
{% endif %}
</td>
<td>
{% if member.current_membership %}
<span class="badge badge-sm
{% if member.current_membership.status == "active" %}badge-success
{% elif member.current_membership.status == "pending" %}badge-warning
{% elif member.current_membership.status == "cancelled" %}badge-error
{% else %}badge-neutral{% endif %}">
{{ member.current_membership.get_status_display }}
</span>
{% else %}
<span class="badge badge-sm badge-ghost">{% trans "unknown" %}</span>
{% endif %}
</td>
{% if requirements_configured %}
<td>
{% if member.current_membership and not member.current_membership.is_guardian %}
<span class="badge badge-sm {% if member.current_membership.onboarding_open %}badge-warning{% else %}badge-success{% endif %}">
{{ member.current_membership.completed_requirements }}/{{ total_requirements }}
</span>
{% else %}
<span class="text-dim"></span>
{% endif %}
</td>
{% endif %}
{% if is_club_admin %}
<td>
{% if member.current_membership and not member.current_membership.is_guardian %}
<span class="badge badge-sm
{% if member.current_membership.fee_status == "paid" %}badge-success
{% elif member.current_membership.fee_status == "partially_paid" %}badge-warning
{% elif member.current_membership.fee_status == "unpaid" %}badge-error
{% else %}badge-ghost{% endif %}">
{{ member.current_membership.get_fee_status_display }}
</span>
{% else %}
<span class="text-dim"></span>
{% endif %}
</td>
{% endif %}
<td class="text-right">
{% if can_manage_members %}
<div class="flex flex-wrap justify-end gap-1">
<a class="btn btn-outline btn-xs" href="{% url 'management:member_detail' member.pk %}" aria-label="{% trans 'Edit' %}">{% lucide "pencil" size=13 %} {% trans "Edit" %}</a>
<button class="btn btn-xs btn-outline btn-error" type="button" onclick="document.getElementById('{{ member.pk|dom_id:"member_delete_modal" }}').showModal()" aria-label="{% trans 'Delete' %}">{% lucide "trash-2" size=13 %} {% trans "Delete" %}</button>
</div>
{% endif %}
</td>
</tr>
{% empty %}
<tr>
<td colspan="8" class="py-6 text-center text-muted">{% trans "No members yet." %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% include "management/_pagination.html" %}
{% if can_manage_members %}
{% trans "Delete member" as delete_member_title %}
{% trans "Delete" as delete_label %}
{% for member in members %}
{% url 'management:member_delete' member.pk as member_delete_url %}
{% blocktrans with full_name=member.get_full_name asvar delete_member_body %}Delete {{ full_name }}? This also removes their club membership, roster spots, staff assignments, and family links. This cannot be undone.{% endblocktrans %}
{% include "controlpanel/_confirm_modal.html" with modal_id=member.pk|dom_id:"member_delete_modal" title=delete_member_title body=delete_member_body action_url=member_delete_url submit_label=delete_label %}
{% endfor %}
{% endif %}
{% endblock panel %}