Add referee assignment/eligibility system, team bulk-add, member Groups, and referee management dashboard with PDF export
Builds the referee workflow end to end: club-defined RefereeLevel/RefereeProfile eligibility tied to teams, EventReferee assignment (member or external, with fee/km payment tracking), an admin dashboard with KPI tiles, date-grouped game tiles and range filters, and a downloadable payment form PDF modeled on the club's existing paper document (using Club.legal_name when set). Also lands team roster bulk-add, member mass-upload with family linking, and the members.Group model, developed alongside this work. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -19,11 +19,17 @@
|
||||
{% 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>
|
||||
<li><a class="{% if nav == 'role_list' %}menu-active{% endif %}" href="{% url 'management:role_list' %}">{% lucide "shield-check" size=16 %} {% trans "Roles" %}</a></li>
|
||||
<li><a class="{% if nav == 'group_list' %}menu-active{% endif %}" href="{% url 'management:group_list' %}">{% lucide "users-round" size=16 %} {% trans "Groups" %}</a></li>
|
||||
{% endif %}
|
||||
|
||||
<li class="menu-title">{% trans "Teams" %}</li>
|
||||
<li><a class="{% if nav == 'team_list' %}menu-active{% endif %}" href="{% url 'management:team_list' %}">{% lucide "shirt" size=16 %} {% trans "Teams" %}</a></li>
|
||||
<li><a class="{% if nav == 'position_list' %}menu-active{% endif %}" href="{% url 'management:position_list' %}">{% lucide "tags" size=16 %} {% trans "Positions" %}</a></li>
|
||||
<li><a class="{% if nav == 'referee_list' %}menu-active{% endif %}" href="{% url 'management:referee_list' %}">{% lucide "flag" size=16 %} {% trans "Referees" %}</a></li>
|
||||
{% if is_club_admin %}
|
||||
<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_level_list' %}menu-active{% endif %}" href="{% url 'management:referee_level_list' %}">{% lucide "badge-check" size=16 %} {% trans "Referee levels" %}</a></li>
|
||||
{% endif %}
|
||||
|
||||
<li class="menu-title">{% trans "News" %}</li>
|
||||
<li><a class="{% if nav == 'news_list' %}menu-active{% endif %}" href="{% url 'management:news_list' %}">{% lucide "newspaper" size=16 %} {% trans "News" %}</a></li>
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
{% load i18n lucide ui %}
|
||||
{% comment %}
|
||||
Shared by the event detail page and the referee management dashboard --
|
||||
context: event, referees (EventReferee rows, each with a .fee_form when
|
||||
can_manage_referees), referee_candidates, referees_full,
|
||||
can_manage_referees, and an optional next_url to return to after a POST
|
||||
(defaults to the event detail page when blank).
|
||||
{% endcomment %}
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="card-title text-base">
|
||||
{% lucide "flag" size=18 %} {% trans "Referees" %}
|
||||
<span class="badge badge-sm {% if referees_full %}badge-neutral{% else %}badge-outline{% endif %}">
|
||||
{{ referees|length }} / {{ event.max_referees }}
|
||||
</span>
|
||||
</h2>
|
||||
{% if can_manage_referees %}
|
||||
<a class="btn btn-outline btn-sm gap-1" href="{% url 'management:event_referee_form_pdf' event.pk %}">{% lucide "file-down" size=12 %} {% trans "Referee form (PDF)" %}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<ul class="divide-y divide-base-200">
|
||||
{% for referee in referees %}
|
||||
<li class="flex items-center justify-between py-2 gap-2">
|
||||
<span>
|
||||
{{ referee.display_name }}
|
||||
{% if referee.is_external %}<span class="badge badge-neutral badge-xs">{% trans "External" %}</span>{% endif %}
|
||||
{% if referee.assigned_by %}<span class="text-xs opacity-60">— {% blocktrans with name=referee.assigned_by %}assigned by {{ name }}{% endblocktrans %}</span>{% endif %}
|
||||
{% if referee.total_payable %}<span class="text-xs opacity-60">— {% blocktrans with total=referee.total_payable %}€{{ total }} due{% endblocktrans %}</span>{% endif %}
|
||||
</span>
|
||||
{% if can_manage_referees %}
|
||||
<div class="flex gap-1 shrink-0">
|
||||
<button class="btn btn-xs btn-outline" type="button" onclick="document.getElementById('{{ referee.pk|dom_id:"referee_fee_modal" }}').showModal()">{% lucide "euro" size=12 %} {% trans "Fee" %}</button>
|
||||
<form method="post" action="{% url 'management:event_referee_remove' event.pk referee.pk %}">
|
||||
{% csrf_token %}
|
||||
{% if next_url %}<input type="hidden" name="next" value="{{ next_url }}">{% endif %}
|
||||
<button class="btn btn-xs btn-outline btn-error gap-1" type="submit">{% lucide "x" size=12 %} {% trans "Remove" %}</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% empty %}
|
||||
<li class="py-2 text-sm opacity-60">{% trans "No referees assigned yet." %}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
{% if can_manage_referees %}
|
||||
{% if referees_full %}
|
||||
<p class="text-sm opacity-60 mt-2">{% trans "This game already has its maximum number of referees." %}</p>
|
||||
{% else %}
|
||||
{% if referee_candidates %}
|
||||
<form method="post" action="{% url 'management:event_referee_assign' event.pk %}" class="mt-2 flex flex-wrap items-center gap-2">
|
||||
{% csrf_token %}
|
||||
{% if next_url %}<input type="hidden" name="next" value="{{ next_url }}">{% endif %}
|
||||
<select name="member" class="select select-bordered select-sm">
|
||||
{% for candidate in referee_candidates %}
|
||||
<option value="{{ candidate.pk }}" {% if candidate.has_conflict %}title="{% blocktrans with events=candidate.conflict_titles %}Also expected at: {{ events }}{% endblocktrans %}"{% endif %}>
|
||||
{{ candidate }}{% if candidate.has_conflict %} ⚠{% endif %}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button class="btn btn-outline btn-sm gap-2" type="submit">{% lucide "user-plus" size=14 %} {% trans "Assign" %}</button>
|
||||
</form>
|
||||
<p class="text-xs opacity-60 mt-1">{% trans "⚠ = also expected at another event around this time -- shown as a warning, not blocked." %}</p>
|
||||
{% else %}
|
||||
<p class="text-sm opacity-60 mt-2">
|
||||
{% trans "No eligible referees for this team yet." %}
|
||||
<a class="link" href="{% url 'management:referee_level_list' %}">{% trans "Link a referee level to this team." %}</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" action="{% url 'management:event_referee_add_external' event.pk %}" class="mt-2 flex flex-wrap items-center gap-2">
|
||||
{% csrf_token %}
|
||||
{% if next_url %}<input type="hidden" name="next" value="{{ next_url }}">{% endif %}
|
||||
<input type="text" name="name" class="input input-bordered input-sm" placeholder="{% trans 'External referee name' %}">
|
||||
<button class="btn btn-outline btn-sm gap-2" type="submit">{% lucide "user-plus" size=14 %} {% trans "Add external" %}</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if can_manage_referees %}
|
||||
{% trans "Referee fee" as fee_title %}
|
||||
{% trans "Save" as save_label %}
|
||||
{% with encoded_next=next_url|urlencode %}
|
||||
{% for referee in referees %}
|
||||
{% url 'management:event_referee_fee_update' event.pk referee.pk as fee_action_url %}
|
||||
{% with fee_action_url=fee_action_url|add:"?next="|add:encoded_next %}
|
||||
{% include "controlpanel/_modal_form.html" with modal_id=referee.pk|dom_id:"referee_fee_modal" title=fee_title form=referee.fee_form action_url=fee_action_url submit_label=save_label submit_icon="save" %}
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
@@ -128,6 +128,21 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if event.is_home_game and not referee_management_needed %}
|
||||
<div class="alert alert-info mb-6">
|
||||
{% lucide "info" size=20 %}
|
||||
<span>{% trans "Referees for this game are managed by the federation, not the club." %}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if referee_management_needed %}
|
||||
<div class="mb-6 card bg-base-100 shadow">
|
||||
<div class="card-body">
|
||||
{% include "management/_referee_assignment_panel.html" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock panel %}
|
||||
|
||||
{% block extra_body %}
|
||||
|
||||
@@ -49,22 +49,22 @@
|
||||
<div id="game-details-section" class="game-only">
|
||||
<div class="divider"></div>
|
||||
<h3 class="text-lg font-semibold mb-3">{% trans "Game" %}</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{% form_field form.competition %}
|
||||
{% form_field form.external_game_id %}
|
||||
</div>
|
||||
{% if update_view %}
|
||||
{% comment %}
|
||||
{% form_field form.max_referees %}
|
||||
{% if update_view %}
|
||||
{% comment %}
|
||||
Score/live status only exist to record once a game has
|
||||
actually been created -- the add form leaves them out
|
||||
entirely (see EventForm's editing=False path).
|
||||
{% endcomment %}
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{% endcomment %}
|
||||
|
||||
{% form_field form.score_for %}
|
||||
{% form_field form.score_against %}
|
||||
{% form_field form.is_live %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-actions justify-start pt-2 mt-2">
|
||||
|
||||
90
management/templates/management/event_referee_form_pdf.html
Normal file
90
management/templates/management/event_referee_form_pdf.html
Normal file
@@ -0,0 +1,90 @@
|
||||
{% load i18n %}
|
||||
|
||||
{% comment %}
|
||||
Rendered by WeasyPrint -- modeled directly on the club's existing paper
|
||||
referee payment form ("Leeg Document RefereesCompetitie25-26.pdf"): a
|
||||
3-line header (club name/address), match details, referee names, a
|
||||
payment sentence, a fee+km breakdown per referee, and blank signature
|
||||
lines. Header uses club.official_name (legal_name if set, else name);
|
||||
the payment sentence uses the plain club name -- the original document
|
||||
does the same, using a longer legal form up top and a shorter one in the
|
||||
body text.
|
||||
{% endcomment %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{% trans "Referee form" %}</title>
|
||||
<style>
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 25mm 20mm;
|
||||
@bottom-center {
|
||||
content: "{{ club.name }} — {% trans "referee form" %} — " counter(page) " / " counter(pages);
|
||||
font-size: 8pt;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
body { font-family: sans-serif; font-size: 10.5pt; color: #111; line-height: 1.5; }
|
||||
.header { text-align: center; margin-bottom: 14mm; }
|
||||
.header div { margin: 0; }
|
||||
.field { margin-bottom: 4mm; }
|
||||
.field .label { display: inline-block; min-width: 32mm; }
|
||||
.fill-line { display: block; border-bottom: 1px solid #333; min-height: 5mm; margin: 2mm 0 4mm; }
|
||||
.section-title { margin: 8mm 0 2mm; }
|
||||
.fee-row { border-bottom: 1px solid #333; padding-bottom: 1mm; margin-bottom: 4mm; min-height: 5mm; }
|
||||
.signature-block { margin-top: 12mm; }
|
||||
.signature-line { border-bottom: 1px solid #333; min-height: 16mm; margin-top: 2mm; }
|
||||
.collected-line { margin-top: 12mm; }
|
||||
.collected-line .fill { display: inline-block; border-bottom: 1px solid #333; min-width: 90mm; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<div>{{ club.official_name }}</div>
|
||||
{% if home_location %}
|
||||
<div>{{ home_location.address }}</div>
|
||||
<div>{{ home_location.zip_code }} {{ home_location.city }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<span class="label">{% trans "Regarding: Game" %}</span>
|
||||
{% for team in event.teams.all %}{{ team.short_name }}{% if not forloop.last %}, {% endif %}{% endfor %}
|
||||
{% if event.opponent %}{% trans "vs" %} {{ event.opponent }}{% endif %}
|
||||
{% if event.external_game_id %}({% trans "ID" %}: {{ event.external_game_id }}){% endif %}
|
||||
</div>
|
||||
<div class="field"><span class="label">{% trans "Date" %}</span> {{ event.start|date:"j F Y" }}</div>
|
||||
<div class="field"><span class="label">{% trans "Time" %}</span> {{ event.start|date:"H:i" }}</div>
|
||||
<div class="field">
|
||||
<span class="label">{% trans "Referee names" %}</span>
|
||||
{% for referee in referees %}{{ referee.display_name }}{% if not forloop.last %}, {% endif %}{% empty %}—{% endfor %}
|
||||
</div>
|
||||
<div class="fill-line"></div>
|
||||
|
||||
<p>{% blocktrans with name=club.name %}{{ name }} pays the fee to the referee(s) for officiating the game mentioned above.{% endblocktrans %}</p>
|
||||
|
||||
<div class="section-title">{% trans "Referees and fee, plus any kilometers" %}</div>
|
||||
{% for referee in referees %}
|
||||
<div class="fee-row">
|
||||
{{ referee.display_name }} —
|
||||
{% blocktrans with fee=referee.fee %}fee: €{{ fee }}{% endblocktrans %}
|
||||
{% if referee.km %}
|
||||
{% blocktrans with km=referee.km rate=referee.km_rate total=referee.km_total %}+ {{ km }} km × €{{ rate }}/km = €{{ total }}{% endblocktrans %}
|
||||
{% endif %}
|
||||
— {% blocktrans with total=referee.total_payable %}total: €{{ total }}{% endblocktrans %}
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="fee-row"></div>
|
||||
{% endfor %}
|
||||
|
||||
<div class="signature-block">
|
||||
<div>{% trans "Referee signature" %}</div>
|
||||
<div class="signature-line"></div>
|
||||
</div>
|
||||
|
||||
<div class="collected-line">
|
||||
{% trans "Collected by team manager" %}: <span class="fill"> </span>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
65
management/templates/management/group_bulk_add.html
Normal file
65
management/templates/management/group_bulk_add.html
Normal file
@@ -0,0 +1,65 @@
|
||||
{% extends "management/base.html" %}
|
||||
{% load i18n lucide %}
|
||||
|
||||
{% block heading %}{% trans "Add multiple members" %}{% endblock heading %}
|
||||
{% block subheading %}{{ group.name }}{% endblock subheading %}
|
||||
|
||||
{% block panel %}
|
||||
<form method="get" class="mb-4">
|
||||
<div class="flex flex-row 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">
|
||||
</label>
|
||||
<button class="btn btn-outline gap-2" type="submit">{% lucide "filter" size=16 %} {% trans "Filter" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span class="text-sm opacity-70 font-semibold">
|
||||
{% blocktrans count counter=members|length %}{{ counter }} member{% plural %}{{ counter }} members{% endblocktrans %}
|
||||
</span>
|
||||
<button class="btn btn-primary btn-sm gap-2" type="submit">{% lucide "users" size=14 %} {% trans "Add selected" %}</button>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>{% trans "Member" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for member in members %}
|
||||
<tr>
|
||||
<td>
|
||||
{% if member.already_in_group %}
|
||||
<span class="badge badge-neutral badge-sm">{% trans "In group" %}</span>
|
||||
{% else %}
|
||||
<input type="checkbox" class="checkbox" name="member_{{ member.pk }}">
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ member }}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="2" class="text-center opacity-60">{% trans "No members match this search." %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<a class="btn btn-outline gap-2" href="{% url 'management:group_detail' group.pk %}">{% lucide "arrow-left" size=16 %} {% trans "Back" %}</a>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock panel %}
|
||||
51
management/templates/management/group_detail.html
Normal file
51
management/templates/management/group_detail.html
Normal file
@@ -0,0 +1,51 @@
|
||||
{% extends "management/base.html" %}
|
||||
{% load i18n lucide ui %}
|
||||
|
||||
{% block heading %}{{ group.name }}{% endblock heading %}
|
||||
|
||||
{% block actions %}
|
||||
<a class="btn btn-outline gap-2" href="{% url 'management:group_update' group.pk %}">{% lucide "pencil" size=16 %} {% trans "Edit" %}</a>
|
||||
{% endblock actions %}
|
||||
|
||||
{% block panel %}
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="card-title text-base">{% trans "Members" %}</h2>
|
||||
<a class="btn btn-outline btn-sm gap-2" href="{% url 'management:group_bulk_add' group.pk %}">{% lucide "users" size=14 %} {% trans "Add multiple" %}</a>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Member" %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for membership in memberships %}
|
||||
<tr>
|
||||
<td><a class="link link-hover" href="{% url 'management:member_detail' membership.member.pk %}">{{ membership.member }}</a></td>
|
||||
<td class="text-right">
|
||||
<button class="btn btn-sm btn-outline btn-error" type="button" onclick="document.getElementById('{{ membership.pk|dom_id:"remove_member_modal" }}').showModal()">{% lucide "user-minus" size=14 %} {% trans "Remove" %}</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="2" class="text-center opacity-60">{% trans "No members yet." %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% trans "Remove from group" as remove_member_title %}
|
||||
{% trans "Remove" as remove_label %}
|
||||
{% for membership in memberships %}
|
||||
{% blocktrans with member=membership.member group_name=group.name asvar remove_member_body %}Remove {{ member }} from “{{ group_name }}”?{% endblocktrans %}
|
||||
{% url 'management:group_member_remove' group.pk membership.pk as remove_member_url %}
|
||||
{% include "controlpanel/_confirm_modal.html" with modal_id=membership.pk|dom_id:"remove_member_modal" title=remove_member_title body=remove_member_body action_url=remove_member_url submit_label=remove_label submit_icon="user-minus" %}
|
||||
{% endfor %}
|
||||
{% endblock panel %}
|
||||
35
management/templates/management/group_form.html
Normal file
35
management/templates/management/group_form.html
Normal file
@@ -0,0 +1,35 @@
|
||||
{% extends "management/base.html" %}
|
||||
{% load i18n lucide static ui %}
|
||||
|
||||
{% block heading %}{% if update_view %}{% blocktrans %}Edit {{ object }}{% endblocktrans %}{% else %}{% trans "New group" %}{% endif %}{% endblock heading %}
|
||||
|
||||
{% block panel %}
|
||||
<div class="card w-full bg-base-100 shadow">
|
||||
<div class="card-body">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
{% for error in form.non_field_errors %}
|
||||
<div class="alert alert-error my-2">
|
||||
<span>{{ error }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{% for field in form %}
|
||||
{% form_field field %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="card-actions justify-start pt-2 mt-2">
|
||||
<a class="btn btn-outline gap-2" href="{% if update_view %}{% url "management:group_detail" object.pk %}{% else %}{% url "management:group_list" %}{% endif %}">{% lucide "arrow-left" size=16 %} {% trans "Cancel" %}</a>
|
||||
<button class="btn btn-primary gap-2" type="submit">{% lucide "save" size=16 %} {% trans "Save" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock panel %}
|
||||
|
||||
{% block extra_body %}
|
||||
<script src="{% static 'js/searchable-select.js' %}"></script>
|
||||
{% endblock extra_body %}
|
||||
53
management/templates/management/group_list.html
Normal file
53
management/templates/management/group_list.html
Normal file
@@ -0,0 +1,53 @@
|
||||
{% extends "management/base.html" %}
|
||||
{% load i18n lucide ui %}
|
||||
|
||||
{% block heading %}{% trans "Groups" %}{% endblock heading %}
|
||||
{% block subheading %}{% trans "Named collections of members -- coaches, team managers, referee pools, committees..." %}{% endblock subheading %}
|
||||
|
||||
{% block actions %}
|
||||
<a class="btn btn-outline gap-2" href="{% url 'management:group_create' %}">{% lucide "plus" size=16 %} {% trans "New group" %}</a>
|
||||
{% 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 "Name" %}</th>
|
||||
<th>{% trans "Members" %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for group in groups %}
|
||||
<tr>
|
||||
<td><a class="link link-hover" href="{% url 'management:group_detail' group.pk %}">{{ group.name }}</a></td>
|
||||
<td>{{ group.member_count }}</td>
|
||||
<td class="text-right">
|
||||
<div class="flex justify-end gap-1">
|
||||
<a class="btn btn-outline btn-sm" href="{% url 'management:group_update' group.pk %}" aria-label="{% trans 'Edit' %}">{% lucide "pencil" size=14 %} {% trans "Edit" %}</a>
|
||||
<button class="btn btn-sm btn-outline btn-error" type="button" onclick="document.getElementById('{{ group.pk|dom_id:"group_delete_modal" }}').showModal()" aria-label="{% trans 'Delete' %}">{% lucide "trash-2" size=14 %} {% trans "Delete" %}</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="3" class="text-center opacity-60">{% trans "No groups yet." %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% trans "Delete group" as delete_group_title %}
|
||||
{% trans "Delete" as delete_label %}
|
||||
{% for group in groups %}
|
||||
{% url 'management:group_delete' group.pk as group_delete_url %}
|
||||
{% blocktrans with name=group.name asvar delete_group_body %}Delete “{{ name }}”? Its members are removed from it, and any teams it's linked to as a referee source lose that link. This cannot be undone.{% endblocktrans %}
|
||||
{% include "controlpanel/_confirm_modal.html" with modal_id=group.pk|dom_id:"group_delete_modal" title=delete_group_title body=delete_group_body action_url=group_delete_url submit_label=delete_label submit_icon="trash-2" %}
|
||||
{% endfor %}
|
||||
{% endblock panel %}
|
||||
@@ -1,5 +1,5 @@
|
||||
{% extends "management/base.html" %}
|
||||
{% load lucide ui i18n %}
|
||||
{% load lucide ui i18n static %}
|
||||
|
||||
{% block heading %}{{ member.get_full_name }}{% endblock heading %}
|
||||
{% block subheading %}{{ member.contact_email }}{% endblock subheading %}
|
||||
@@ -127,6 +127,66 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if referee_profile or is_club_admin %}
|
||||
<div class="card bg-base-100 shadow mt-4">
|
||||
<div class="card-body">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="card-title text-base">{% lucide "flag" size=18 %} {% trans "Referee eligibility" %}</h2>
|
||||
{% if is_club_admin %}
|
||||
<button class="btn btn-outline btn-sm gap-2" type="button" onclick="document.getElementById('referee_eligibility_modal').showModal()">{% lucide "pencil" size=14 %} {% trans "Edit" %}</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if referee_profile and referee_profile.level %}
|
||||
<dl class="divide-y divide-base-200">
|
||||
<div class="flex items-center justify-between py-2">
|
||||
<dt class="text-sm opacity-70">{% trans "Level" %}</dt>
|
||||
<dd class="font-semibold">{{ referee_profile.level }}</dd>
|
||||
</div>
|
||||
<div class="flex items-center justify-between py-2">
|
||||
<dt class="text-sm opacity-70">{% trans "Valid until" %}</dt>
|
||||
<dd class="font-semibold flex items-center gap-2">
|
||||
{{ referee_profile.valid_until|default:"—" }}
|
||||
{% if referee_profile.is_currently_valid %}
|
||||
<span class="badge badge-success badge-sm">{% trans "Valid" %}</span>
|
||||
{% elif referee_profile.valid_until %}
|
||||
<span class="badge badge-error badge-sm">{% trans "Expired" %}</span>
|
||||
{% else %}
|
||||
<span class="badge badge-warning badge-sm">{% trans "No validity set" %}</span>
|
||||
{% endif %}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
{% if referee_profile.is_eligible %}
|
||||
<div class="text-xs font-semibold uppercase opacity-60 mt-2 mb-1">{% trans "Can referee for" %}</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{% for team in referee_profile.eligible_teams %}
|
||||
<span class="badge badge-outline">{{ team.name }}</span>
|
||||
{% empty %}
|
||||
<span class="text-sm opacity-60">{% trans "This level has no teams linked yet." %}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-warning mt-2">
|
||||
{% lucide "triangle-alert" size=16 %}
|
||||
<span>{% trans "Not currently eligible to referee -- set or extend the validity date above to reinstate them." %}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<p class="text-sm opacity-60">{% trans "Not eligible to referee for any team." %}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if is_club_admin %}
|
||||
{% url 'management:member_referee_eligibility_update' member.pk as referee_eligibility_url %}
|
||||
{% trans "Referee eligibility" as referee_eligibility_title %}
|
||||
{% trans "Save" as save_label %}
|
||||
{% include "controlpanel/_modal_form.html" with modal_id="referee_eligibility_modal" title=referee_eligibility_title form=referee_eligibility_form action_url=referee_eligibility_url submit_label=save_label submit_icon="save" %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% trans "Add parent" as add_parent_label %}
|
||||
{% trans "Add child" as add_child_label %}
|
||||
{% trans "If this email has no account yet, one is created and they set a password via the reset link." as add_parent_blurb %}
|
||||
@@ -184,3 +244,7 @@
|
||||
{% include "controlpanel/_modal_form.html" with modal_id="attach_family_modal" title=add_to_family_title form=attach_to_family_form action_url=attach_family_url submit_label=add_label submit_icon="user-plus" blurb=attach_family_blurb %}
|
||||
{% endif %}
|
||||
{% endblock panel %}
|
||||
|
||||
{% block extra_body %}
|
||||
<script src="{% static 'js/searchable-select.js' %}"></script>
|
||||
{% endblock extra_body %}
|
||||
|
||||
@@ -15,6 +15,9 @@
|
||||
here. Nothing is created yet: you'll see exactly what will be added
|
||||
before anything is saved.{% endblocktrans %}
|
||||
</p>
|
||||
<p class="text-sm opacity-70">
|
||||
{% blocktrans %}To register a family, give matching rows the same <code>family_group</code> value (any label works, e.g. a surname) and set each row's <code>family_role</code>. A parent/guardian row with an email gets a login; a child row doesn't — grant one later from their member page if they need it.{% endblocktrans %}
|
||||
</p>
|
||||
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
<th>{% trans "Last name" %}</th>
|
||||
<th>{% trans "First name" %}</th>
|
||||
<th>{% trans "Email" %}</th>
|
||||
<th>{% trans "Family" %}</th>
|
||||
<th>{% trans "Outcome" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -36,6 +37,14 @@
|
||||
<td>{{ result.raw.last_name }}</td>
|
||||
<td>{{ result.raw.first_name }}</td>
|
||||
<td>{{ result.raw.email }}</td>
|
||||
<td>
|
||||
{% if result.family_group %}
|
||||
{{ result.family_group }}
|
||||
{% if result.family_role %}<span class="badge badge-neutral badge-sm">{{ result.family_role|capfirst }}</span>{% endif %}
|
||||
{% else %}
|
||||
<span class="opacity-40">-</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if result.member %}
|
||||
<span class="badge badge-success badge-sm">{% trans "Will create" %}</span>
|
||||
@@ -47,7 +56,7 @@
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="5" class="text-center opacity-60">{% trans "No rows found in the uploaded file." %}</td>
|
||||
<td colspan="6" class="text-center opacity-60">{% trans "No rows found in the uploaded file." %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
||||
35
management/templates/management/referee_level_form.html
Normal file
35
management/templates/management/referee_level_form.html
Normal file
@@ -0,0 +1,35 @@
|
||||
{% extends "management/base.html" %}
|
||||
{% load i18n lucide static ui %}
|
||||
|
||||
{% block heading %}{% if update_view %}{% blocktrans %}Edit {{ object }}{% endblocktrans %}{% else %}{% trans "New referee level" %}{% endif %}{% endblock heading %}
|
||||
|
||||
{% block panel %}
|
||||
<div class="card w-full bg-base-100 shadow">
|
||||
<div class="card-body">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
{% for error in form.non_field_errors %}
|
||||
<div class="alert alert-error my-2">
|
||||
<span>{{ error }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 items-start">
|
||||
{% for field in form %}
|
||||
{% form_field field %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="card-actions justify-start pt-2 mt-2">
|
||||
<a class="btn btn-outline gap-2" href="{% url "management:referee_level_list" %}">{% lucide "arrow-left" size=16 %} {% trans "Cancel" %}</a>
|
||||
<button class="btn btn-primary gap-2" type="submit">{% lucide "save" size=16 %} {% trans "Save" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock panel %}
|
||||
|
||||
{% block extra_body %}
|
||||
<script src="{% static 'js/searchable-select.js' %}"></script>
|
||||
{% endblock extra_body %}
|
||||
54
management/templates/management/referee_level_list.html
Normal file
54
management/templates/management/referee_level_list.html
Normal file
@@ -0,0 +1,54 @@
|
||||
{% extends "management/base.html" %}
|
||||
{% load i18n lucide %}
|
||||
|
||||
{% block heading %}{% trans "Referee levels" %}{% endblock heading %}
|
||||
{% block subheading %}{% trans "Which teams' home games each level qualifies a referee for." %}{% endblock subheading %}
|
||||
|
||||
{% block actions %}
|
||||
{% if is_club_admin %}
|
||||
<a class="btn btn-outline gap-2" href="{% url 'management:referee_level_create' %}">{% lucide "plus" size=16 %} {% trans "New level" %}</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 "Ordering" %}</th>
|
||||
<th>{% trans "Name" %}</th>
|
||||
<th>{% trans "Qualifies for" %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for level in levels %}
|
||||
<tr>
|
||||
<td>{{ level.ordering }}</td>
|
||||
<td>{{ level.name }}</td>
|
||||
<td>
|
||||
{% for team in level.teams.all %}
|
||||
<span class="badge badge-outline badge-sm">{{ team.name }}</span>
|
||||
{% empty %}
|
||||
<span class="opacity-40">—</span>
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
{% if is_club_admin %}
|
||||
<a class="btn btn-outline btn-sm gap-2" href="{% url 'management:referee_level_update' level.pk %}">{% lucide "pencil" size=14 %} {% trans "Edit" %}</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="4" class="text-center opacity-60">{% trans "No referee levels yet." %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock panel %}
|
||||
60
management/templates/management/referee_list.html
Normal file
60
management/templates/management/referee_list.html
Normal file
@@ -0,0 +1,60 @@
|
||||
{% extends "management/base.html" %}
|
||||
{% load i18n lucide %}
|
||||
|
||||
{% block heading %}{% trans "Referees" %}{% endblock heading %}
|
||||
{% block subheading %}{% trans "Every club referee, their level, and whether their qualification is currently valid." %}{% endblock subheading %}
|
||||
|
||||
{% 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 "Referee" %}</th>
|
||||
<th>{% trans "Level" %}</th>
|
||||
<th>{% trans "Can referee for" %}</th>
|
||||
<th>{% trans "Valid until" %}</th>
|
||||
<th>{% trans "Status" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for referee in referees %}
|
||||
<tr>
|
||||
<td><a class="link link-hover" href="{% url 'management:member_detail' referee.pk %}">{{ referee }}</a></td>
|
||||
<td>{{ referee.referee_profile.level|default:"—" }}</td>
|
||||
<td>
|
||||
{% if referee.referee_profile.is_eligible %}
|
||||
{% for team in referee.referee_profile.eligible_teams %}
|
||||
<span class="badge badge-outline badge-sm">{{ team.name }}</span>
|
||||
{% empty %}
|
||||
<span class="opacity-40">—</span>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<span class="opacity-40">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ referee.referee_profile.valid_until|default:"—" }}</td>
|
||||
<td>
|
||||
{% if referee.referee_profile.is_eligible %}
|
||||
<span class="badge badge-success badge-sm">{% trans "Valid" %}</span>
|
||||
{% elif not referee.referee_profile.level %}
|
||||
<span class="badge badge-warning badge-sm">{% trans "No level" %}</span>
|
||||
{% elif not referee.referee_profile.valid_until %}
|
||||
<span class="badge badge-warning badge-sm">{% trans "No validity set" %}</span>
|
||||
{% else %}
|
||||
<span class="badge badge-error badge-sm">{% trans "Expired" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="5" class="text-center opacity-60">{% trans "No referees yet -- set a level and validity from a member's own page." %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock panel %}
|
||||
99
management/templates/management/referee_management.html
Normal file
99
management/templates/management/referee_management.html
Normal file
@@ -0,0 +1,99 @@
|
||||
{% extends "management/base.html" %}
|
||||
{% load i18n lucide ui %}
|
||||
|
||||
{% block heading %}{% trans "Referee management" %}{% endblock heading %}
|
||||
{% block subheading %}{% trans "Every upcoming home game that needs a club-arranged referee." %}{% endblock subheading %}
|
||||
|
||||
{% block panel %}
|
||||
<div class="mb-6 grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<div class="card bg-base-100 shadow border-l-4 border-info">
|
||||
<div class="card-body p-4">
|
||||
<div class="flex items-center gap-2 text-sm opacity-70 mb-3">{% lucide "calendar" size=16 %} {% trans "Games in view" %}</div>
|
||||
<div class="text-4xl font-bold tabular-nums font-mono">{{ kpi_total }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card bg-base-100 shadow border-l-4 {% if kpi_no_referee %}border-error{% else %}border-success{% endif %}">
|
||||
<div class="card-body p-4">
|
||||
<div class="flex items-center gap-2 text-sm opacity-70 mb-3">{% lucide "circle-alert" size=16 %} {% trans "Without a referee" %}</div>
|
||||
<div class="text-4xl font-bold tabular-nums font-mono">{{ kpi_no_referee }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card bg-base-100 shadow border-l-4 {% if kpi_understaffed %}border-warning{% else %}border-success{% endif %}">
|
||||
<div class="card-body p-4">
|
||||
<div class="flex items-center gap-2 text-sm opacity-70 mb-3">{% lucide "circle-dashed" size=16 %} {% trans "Partially staffed" %}</div>
|
||||
<div class="text-4xl font-bold tabular-nums font-mono">{{ kpi_understaffed }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card bg-base-100 shadow border-l-4 border-success">
|
||||
<div class="card-body p-4">
|
||||
<div class="flex items-center gap-2 text-sm opacity-70 mb-3">{% lucide "circle-check" size=16 %} {% trans "Fully staffed" %}</div>
|
||||
<div class="text-4xl font-bold tabular-nums font-mono">{{ kpi_fully_staffed }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="join mb-4 w-full flex-wrap">
|
||||
<a href="?range=week" class="btn join-item flex-1 {% if range_choice == "week" %}btn-primary{% endif %}">{% trans "This week" %}</a>
|
||||
<a href="?range=two_weeks" class="btn join-item flex-1 {% if range_choice == "two_weeks" %}btn-primary{% endif %}">{% trans "This + next week" %}</a>
|
||||
<a href="?range=10" class="btn join-item flex-1 {% if range_choice == "10" %}btn-primary{% endif %}">{% trans "Next 10" %}</a>
|
||||
<a href="?range=25" class="btn join-item flex-1 {% if range_choice == "25" %}btn-primary{% endif %}">{% trans "Next 25" %}</a>
|
||||
<a href="?range=50" class="btn join-item flex-1 {% if range_choice == "50" %}btn-primary{% endif %}">{% trans "Next 50" %}</a>
|
||||
</div>
|
||||
|
||||
{% regroup games by start.date as day_groups %}
|
||||
{% for day in day_groups %}
|
||||
<h3 class="font-semibold text-sm opacity-70 mt-4 mb-2">{{ day.grouper|date:"l j F" }}</h3>
|
||||
<div class="grid gap-3 sm:grid-cols-2 lg:grid-cols-3 mb-2">
|
||||
{% for game in day.list %}
|
||||
<div class="card bg-base-100 shadow-sm border {% if not game.referee_rows %}border-error{% elif not game.referees_full %}border-warning{% else %}border-base-300{% endif %}">
|
||||
<div class="card-body p-4 gap-2">
|
||||
<a class="link link-hover font-semibold" href="{% url 'management:event_detail' game.pk %}">
|
||||
{% for team in game.teams.all %}{{ team.short_name }}{% if not forloop.last %}, {% endif %}{% endfor %}
|
||||
{% if game.opponent %}{% trans "vs" %} {{ game.opponent }}{% endif %}
|
||||
</a>
|
||||
<div class="text-xs opacity-70">{{ game.start|date:"H:i" }}{% if game.location %} — {{ game.location }}{% endif %}</div>
|
||||
<div class="text-xs">
|
||||
{% for referee in game.referee_rows %}
|
||||
<span class="badge badge-ghost badge-sm">{{ referee.display_name }}{% if referee.is_external %} ({% trans "ext." %}){% endif %}</span>
|
||||
{% empty %}
|
||||
<span class="opacity-60">{% trans "No referees assigned yet." %}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="badge badge-sm {% if not game.referee_rows %}badge-error{% elif not game.referees_full %}badge-warning{% else %}badge-success{% endif %}">
|
||||
{{ game.referee_rows|length }} / {{ game.max_referees }}
|
||||
</span>
|
||||
<div class="flex gap-1">
|
||||
<a class="btn btn-xs btn-outline" href="{% url 'management:event_referee_form_pdf' game.pk %}">
|
||||
{% lucide "file-down" size=12 %} {% trans "Referee form" %}
|
||||
</a>
|
||||
<button class="btn btn-xs btn-outline" type="button" onclick="document.getElementById('{{ game.pk|dom_id:"referee_game_modal" }}').showModal()">
|
||||
{% lucide "flag" size=12 %} {% trans "Manage" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<dialog id="{{ game.pk|dom_id:"referee_game_modal" }}" class="modal">
|
||||
<div class="modal-box max-w-2xl">
|
||||
<form method="dialog"><button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">✕</button></form>
|
||||
<h3 class="font-bold text-lg mb-1">
|
||||
{% for team in game.teams.all %}{{ team.short_name }}{% if not forloop.last %}, {% endif %}{% endfor %}
|
||||
{% if game.opponent %}{% trans "vs" %} {{ game.opponent }}{% endif %}
|
||||
</h3>
|
||||
<div class="text-sm opacity-70 mb-4">{{ game.start|date:"j M Y H:i" }}</div>
|
||||
{% include "management/_referee_assignment_panel.html" with event=game referees=game.referee_rows referee_candidates=game.referee_candidates referees_full=game.referees_full can_manage_referees=True next_url=request.get_full_path %}
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop"><button>close</button></form>
|
||||
</dialog>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body">
|
||||
<p class="text-center opacity-60">{% trans "No upcoming home games need a club-arranged referee." %}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endblock panel %}
|
||||
125
management/templates/management/team_bulk_add.html
Normal file
125
management/templates/management/team_bulk_add.html
Normal file
@@ -0,0 +1,125 @@
|
||||
{% extends "management/base.html" %}
|
||||
{% load i18n lucide %}
|
||||
|
||||
{% block heading %}{% trans "Add multiple people" %}{% endblock heading %}
|
||||
{% block subheading %}{{ team.name }}{% endblock subheading %}
|
||||
|
||||
{% block panel %}
|
||||
<form method="get" class="mb-4">
|
||||
<div class="flex flex-row 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">
|
||||
</label>
|
||||
<button class="btn btn-outline gap-2" type="submit">{% lucide "filter" size=16 %} {% trans "Filter" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span class="text-sm opacity-70 font-semibold">
|
||||
{% blocktrans count counter=members|length %}{{ counter }} eligible member{% plural %}{{ counter }} eligible members{% endblocktrans %}
|
||||
</span>
|
||||
<button class="btn btn-primary btn-sm gap-2" type="submit">{% lucide "users" size=14 %} {% trans "Add selected" %}</button>
|
||||
</div>
|
||||
<p class="text-sm opacity-70 mb-2">
|
||||
{% blocktrans %}Only members who are active (paid up) for this club this season or next are eligible. Tick "Player" and/or "Staff" for anyone you want to add -- both at once works too (a playing coach, say).{% endblocktrans %}
|
||||
</p>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Member" %}</th>
|
||||
<th>{% trans "Player" %}</th>
|
||||
<th>{% trans "Position" %}</th>
|
||||
<th>{% trans "Jersey #" %}</th>
|
||||
<th>{% trans "Staff" %}</th>
|
||||
<th>{% trans "Position" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for member in members %}
|
||||
<tr>
|
||||
<td>{{ member }}</td>
|
||||
<td>
|
||||
{% if member.already_player %}
|
||||
<span class="badge badge-neutral badge-sm">{% trans "On roster" %}</span>
|
||||
{% else %}
|
||||
<input type="checkbox" class="checkbox player-checkbox" name="player_{{ member.pk }}" data-member="{{ member.pk }}">
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if not member.already_player %}
|
||||
<select name="player_position_{{ member.pk }}" class="select select-bordered select-sm player-position" data-member="{{ member.pk }}" disabled>
|
||||
<option value="">{% trans "—" %}</option>
|
||||
{% for position in player_positions %}
|
||||
<option value="{{ position.pk }}">{{ position.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if not member.already_player %}
|
||||
<input type="number" min="0" name="jersey_{{ member.pk }}" class="input input-bordered input-sm w-20 player-jersey" data-member="{{ member.pk }}" disabled>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if member.already_staff %}
|
||||
<span class="badge badge-neutral badge-sm">{% trans "On staff" %}</span>
|
||||
{% else %}
|
||||
<input type="checkbox" class="checkbox staff-checkbox" name="staff_{{ member.pk }}" data-member="{{ member.pk }}">
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if not member.already_staff %}
|
||||
<select name="staff_position_{{ member.pk }}" class="select select-bordered select-sm staff-position" data-member="{{ member.pk }}" disabled>
|
||||
<option value="">{% trans "—" %}</option>
|
||||
{% for position in staff_positions %}
|
||||
<option value="{{ position.pk }}">{{ position.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="6" class="text-center opacity-60">{% trans "No eligible members match this search." %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<a class="btn btn-outline gap-2" href="{% url 'management:team_detail' team.pk %}?season={{ season.pk }}">{% lucide "arrow-left" size=16 %} {% trans "Back" %}</a>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock panel %}
|
||||
|
||||
{% block extra_body %}
|
||||
<script>
|
||||
(() => {
|
||||
const pairs = [
|
||||
{checkboxClass: "player-checkbox", fieldClasses: ["player-position", "player-jersey"]},
|
||||
{checkboxClass: "staff-checkbox", fieldClasses: ["staff-position"]},
|
||||
];
|
||||
|
||||
pairs.forEach(({checkboxClass, fieldClasses}) => {
|
||||
document.querySelectorAll(`.${checkboxClass}`).forEach((checkbox) => {
|
||||
const memberId = checkbox.dataset.member;
|
||||
const fields = fieldClasses.flatMap((cls) => Array.from(document.querySelectorAll(`.${cls}[data-member="${memberId}"]`)));
|
||||
checkbox.addEventListener("change", () => {
|
||||
fields.forEach((field) => { field.disabled = !checkbox.checked; });
|
||||
});
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
{% endblock extra_body %}
|
||||
@@ -149,7 +149,10 @@
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="card-title text-base">{% trans "Roster" %}</h2>
|
||||
{% if can_manage %}
|
||||
<button class="btn btn-outline btn-sm gap-2" type="button" onclick="document.getElementById('add_player_modal').showModal()">{% lucide "user-plus" size=14 %} {% trans "Add player" %}</button>
|
||||
<div class="flex gap-2">
|
||||
<a class="btn btn-outline btn-sm gap-2" href="{% url 'management:team_bulk_add' team.pk selected_season.pk %}">{% lucide "users" size=14 %} {% trans "Add multiple" %}</a>
|
||||
<button class="btn btn-outline btn-sm gap-2" type="button" onclick="document.getElementById('add_player_modal').showModal()">{% lucide "user-plus" size=14 %} {% trans "Add player" %}</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
@@ -198,7 +201,10 @@
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="card-title text-base">{% trans "Staff" %}</h2>
|
||||
{% if can_manage %}
|
||||
<button class="btn btn-outline btn-sm gap-2" type="button" onclick="document.getElementById('add_staff_modal').showModal()">{% lucide "user-plus" size=14 %} {% trans "Assign staff" %}</button>
|
||||
<div class="flex gap-2">
|
||||
<a class="btn btn-outline btn-sm gap-2" href="{% url 'management:team_bulk_add' team.pk selected_season.pk %}">{% lucide "users" size=14 %} {% trans "Add multiple" %}</a>
|
||||
<button class="btn btn-outline btn-sm gap-2" type="button" onclick="document.getElementById('add_staff_modal').showModal()">{% lucide "user-plus" size=14 %} {% trans "Assign staff" %}</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
@@ -235,6 +241,26 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-base-100 shadow mt-4">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-base">{% lucide "flag" size=18 %} {% trans "Eligible referees" %}</h2>
|
||||
{% if eligible_referees is None %}
|
||||
<p class="text-sm opacity-70">{% trans "Referees for this team's home games are managed by the federation, not the club -- nothing to configure here." %}</p>
|
||||
{% else %}
|
||||
<p class="text-sm opacity-70">{% trans "Members who can be assigned to referee this team's home games. Set from each member's own page." %}</p>
|
||||
{% if eligible_referees %}
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{% for referee in eligible_referees %}
|
||||
<a class="badge badge-outline hover:badge-neutral" href="{% url 'management:member_detail' referee.pk %}">{{ referee }}</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-sm opacity-60">{% trans "No one yet." %}</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-base-100 shadow mt-4">
|
||||
<div class="card-body">
|
||||
<div class="flex items-center justify-between">
|
||||
|
||||
Reference in New Issue
Block a user