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
307 lines
20 KiB
HTML
307 lines
20 KiB
HTML
{% extends "management/base.html" %}
|
||
{% load i18n lucide static ui %}
|
||
|
||
{% block heading %}{{ team.name }}{% endblock heading %}
|
||
|
||
{% block topbar_context %}
|
||
<span class="badge badge-outline">{{ team.short_name }}</span>
|
||
{% if seasons %}
|
||
<form method="get" class="flex items-center gap-2">
|
||
<select name="season" class="select w-auto">
|
||
{% for season in seasons %}
|
||
<option value="{{ season.pk }}" {% if season.pk == selected_season.pk %}selected{% endif %}>{% trans "Season" %} {{ season.start_date|date:"Y" }} - {{ season.end_date|date:"Y" }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
{# No btn-sm: .select is height:2.25rem, matching plain .btn -- btn-sm would sit 6px shorter and misalign in the row. #}
|
||
<button class="btn btn-outline gap-2" type="submit">{% lucide "filter" size=14 %} {% trans "View" %}</button>
|
||
</form>
|
||
{% endif %}
|
||
{% endblock topbar_context %}
|
||
|
||
{% block actions %}
|
||
{% if is_club_admin %}
|
||
<a class="btn btn-outline gap-2" href="{% url 'management:team_update' team.pk %}">{% lucide "pencil" size=16 %} {% trans "Edit" %}</a>
|
||
{% endif %}
|
||
{% endblock actions %}
|
||
|
||
{% block panel %}
|
||
{% if not selected_season %}
|
||
<div class="alert alert-warning">
|
||
{% lucide "calendar-x" size=20 %}
|
||
<span>{% trans "This club has no seasons yet, so there's no roster or staff to show." %}</span>
|
||
</div>
|
||
{% else %}
|
||
{% if can_manage %}
|
||
{% if team_photo %}
|
||
{% trans "Replace photo" as photo_modal_title %}
|
||
{% else %}
|
||
{% trans "Upload photo" as photo_modal_title %}
|
||
{% endif %}
|
||
{% url 'management:team_photo_set' team.pk selected_season.pk as team_photo_set_url %}
|
||
{% include "controlpanel/_modal_form.html" with modal_id="team_photo_modal" title=photo_modal_title form=team_photo_form action_url=team_photo_set_url submit_label="Save" submit_icon="save" %}
|
||
|
||
{% if team_photo %}
|
||
{% trans "Remove team photo?" as remove_photo_body %}
|
||
{% trans "Remove photo" as remove_photo_title %}
|
||
{% url 'management:team_photo_delete' team.pk selected_season.pk as team_photo_delete_url %}
|
||
{% include "controlpanel/_confirm_modal.html" with modal_id="team_photo_delete_modal" title=remove_photo_title body=remove_photo_body action_url=team_photo_delete_url submit_label="Remove" submit_icon="trash-2" %}
|
||
{% endif %}
|
||
{% endif %}
|
||
|
||
{# --- hero: team cover photo (or an ink fallback), headline meta, photo controls and the bulk-add CTA --- #}
|
||
<div class="relative flex h-[180px] shrink-0 flex-col justify-between overflow-hidden rounded-box bg-ink p-6">
|
||
{% if team_photo %}
|
||
<img class="absolute inset-0 h-full w-full object-cover" src="{{ team_photo.image.url }}" alt="">
|
||
<div class="absolute inset-0" style="background:linear-gradient(90deg, rgba(11,18,32,.95) 30%, rgba(11,18,32,.4))"></div>
|
||
{% endif %}
|
||
|
||
<div class="relative flex items-center justify-end gap-2">
|
||
{% if can_manage %}
|
||
{% trans "Upload photo" as upload_photo_label %}
|
||
{% trans "Replace photo" as replace_photo_label %}
|
||
{% if not team_photo %}
|
||
<span class="text-xs text-on-dark-faint">{% trans "No photo uploaded for this season yet." %}</span>
|
||
{% endif %}
|
||
<button class="btn btn-ghost btn-sm gap-2 bg-white/15 text-white hover:bg-white/25" type="button" onclick="document.getElementById('team_photo_modal').showModal()">
|
||
{% if team_photo %}
|
||
{% lucide "pencil" size=14 %} {{ replace_photo_label }}
|
||
{% else %}
|
||
{% lucide "upload" size=14 %} {{ upload_photo_label }}
|
||
{% endif %}
|
||
</button>
|
||
{% if team_photo %}
|
||
<button class="btn btn-ghost btn-sm gap-2 bg-white/15 text-white hover:bg-white/25" type="button" onclick="document.getElementById('team_photo_delete_modal').showModal()">{% lucide "trash-2" size=14 %} {% trans "Remove" %}</button>
|
||
{% endif %}
|
||
{% endif %}
|
||
</div>
|
||
|
||
<div class="relative flex items-end justify-between gap-4">
|
||
<div class="min-w-0 text-white">
|
||
<div class="font-display text-xs font-extrabold tracking-[.14em] text-info uppercase">{% trans "Season" %} {{ selected_season.start_date|date:"Y" }}–{{ selected_season.end_date|date:"Y" }}</div>
|
||
<div class="font-display text-5xl leading-[.94] font-extrabold uppercase">{{ team.name }}</div>
|
||
<div class="mt-2 flex gap-5 text-sm text-on-dark">
|
||
<span>{% blocktrans count counter=roster|length %}{{ counter }} player{% plural %}{{ counter }} players{% endblocktrans %}</span>
|
||
<span>{% blocktrans count counter=staff|length %}{{ counter }} staff member{% plural %}{{ counter }} staff members{% endblocktrans %}</span>
|
||
</div>
|
||
</div>
|
||
{% if can_manage %}
|
||
<a class="btn btn-primary shrink-0 gap-2" href="{% url 'management:team_bulk_add' team.pk selected_season.pk %}">{% lucide "users" size=16 %} {% trans "Add multiple" %}</a>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="grid grid-cols-3 gap-4">
|
||
<div class="card p-4">
|
||
<div class="mb-3 flex items-center gap-1.5 font-mono text-[10px] tracking-[.08em] text-muted uppercase">{% lucide "user-check" size=13 %} {% trans "Attendance rate" %}</div>
|
||
<div class="font-display text-4xl leading-none font-extrabold tabular-nums {% if attendance_rate is None %}text-info{% elif attendance_rate < 30 %}text-club-dark{% elif attendance_rate < 65 %}text-warn{% else %}text-ok{% endif %}">
|
||
{% if attendance_rate is None %}
|
||
{% trans "N/A" %}
|
||
{% else %}
|
||
{{ attendance_rate }}%
|
||
{% endif %}
|
||
</div>
|
||
<div class="mt-2">
|
||
{% if attendance_rate is None %}
|
||
<span class="text-xs text-muted">{% trans "No past events this season" %}</span>
|
||
{% else %}
|
||
<progress class="progress {% if attendance_rate < 30 %}progress-error{% elif attendance_rate < 65 %}progress-warning{% else %}progress-success{% endif %}" value="{{ attendance_rate }}" max="100"></progress>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card p-4">
|
||
<div class="card-title mb-1">{% lucide "trending-up" size=16 %} {% trans "Top attenders" %}</div>
|
||
<ul class="divide-y">
|
||
{% for entry in top_attenders %}
|
||
<li class="flex items-center justify-between gap-4 py-2">
|
||
<span class="text-sm text-ink">{{ entry.member }}</span>
|
||
<span class="font-mono text-sm font-semibold text-ink tabular-nums">{{ entry.rate }}%</span>
|
||
</li>
|
||
{% empty %}
|
||
<li class="py-2 text-center text-sm text-muted">{% trans "Not enough data yet." %}</li>
|
||
{% endfor %}
|
||
</ul>
|
||
</div>
|
||
|
||
<div class="card p-4">
|
||
<div class="card-title mb-1">{% lucide "trending-down" size=16 %} {% trans "Needs attention" %}</div>
|
||
<ul class="divide-y">
|
||
{% for entry in bottom_attenders %}
|
||
<li class="flex items-center justify-between gap-4 py-2">
|
||
<span class="text-sm text-ink">{{ entry.member }}</span>
|
||
<span class="font-mono text-sm font-semibold text-ink tabular-nums">{{ entry.rate }}%</span>
|
||
</li>
|
||
{% empty %}
|
||
<li class="py-2 text-center text-sm text-muted">{% trans "Not enough data yet." %}</li>
|
||
{% endfor %}
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="grid grid-cols-2 gap-4">
|
||
<div class="card p-4">
|
||
<div class="card-title mb-1">{% lucide "alert-triangle" size=16 %} {% trans "Missed the last 2 practices" %}</div>
|
||
<ul class="divide-y">
|
||
{% for member in missed_practices %}
|
||
<li class="py-2 text-sm text-ink">{{ member }}</li>
|
||
{% empty %}
|
||
<li class="py-2 text-center text-sm text-muted">{% trans "No one -- or not enough practice history yet." %}</li>
|
||
{% endfor %}
|
||
</ul>
|
||
</div>
|
||
|
||
<div class="card p-4">
|
||
<div class="card-title mb-1">{% lucide "user-x" size=16 %} {% trans "No-shows" %}</div>
|
||
<ul class="divide-y">
|
||
{% for entry in no_shows %}
|
||
<li class="flex items-center justify-between gap-4 py-2">
|
||
<div class="min-w-0">
|
||
<span class="text-sm font-semibold text-ink">{{ entry.member }}</span>
|
||
<span class="text-sm text-muted">· {{ entry.event.title }}</span>
|
||
</div>
|
||
<div class="shrink-0 font-mono text-xs whitespace-nowrap text-muted">{{ entry.event.start|date:"j M" }}</div>
|
||
</li>
|
||
{% empty %}
|
||
<li class="py-2 text-center text-sm text-muted">{% trans "None recorded." %}</li>
|
||
{% endfor %}
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card flex flex-col overflow-hidden">
|
||
<div class="flex items-center gap-2 border-b border-line px-4 py-3">
|
||
<span class="font-display text-sm font-extrabold tracking-[.1em] text-ink uppercase">{% trans "Roster" %}</span>
|
||
<span class="flex-1"></span>
|
||
{% 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>
|
||
{% endif %}
|
||
</div>
|
||
<table class="table">
|
||
<thead>
|
||
<tr>
|
||
<th>{% trans "No." %}</th>
|
||
<th>{% trans "Player" %}</th>
|
||
<th>{% trans "Position" %}</th>
|
||
<th></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for membership in roster %}
|
||
<tr>
|
||
<td class="font-display text-lg font-extrabold text-ink tabular-nums">{{ membership.jersey_number|default:"—" }}</td>
|
||
<td class="font-semibold text-ink">
|
||
{{ membership.member }}
|
||
{% if membership.is_captain %}<span class="ml-1 font-display text-xs font-extrabold tracking-[.08em] text-club-dark" title="{% trans 'Captain' %}">C</span>{% endif %}
|
||
{% if membership.is_alternate_captain %}<span class="ml-1 font-display text-xs font-extrabold tracking-[.08em] text-club-dark" title="{% trans 'Alternate captain' %}">A</span>{% endif %}
|
||
</td>
|
||
<td class="text-slate">{{ membership.position|default:"—" }}</td>
|
||
<td class="text-right">
|
||
{% if can_manage %}
|
||
<div class="flex justify-end gap-1">
|
||
<button class="btn btn-outline btn-sm" type="button" onclick="document.getElementById('{{ membership.pk|dom_id:"edit_player_modal" }}').showModal()" aria-label="{% trans 'Edit' %}">{% lucide "pencil" size=14 %} {% trans "Edit" %}</button>
|
||
<button class="btn btn-outline btn-error btn-sm" type="button" onclick="document.getElementById('{{ membership.pk|dom_id:"remove_player_modal" }}').showModal()" aria-label="{% trans 'Remove' %}">{% lucide "user-minus" size=14 %} {% trans "Remove" %}</button>
|
||
</div>
|
||
{% endif %}
|
||
</td>
|
||
</tr>
|
||
{% empty %}
|
||
<tr>
|
||
<td colspan="4" class="text-center text-muted">{% trans "No one on the roster for this season yet." %}</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div class="card flex flex-col">
|
||
<div class="flex items-center gap-2 border-b border-line px-4 py-3">
|
||
<span class="font-display text-sm font-extrabold tracking-[.1em] text-ink uppercase">{% trans "Staff" %}</span>
|
||
<span class="flex-1"></span>
|
||
{% 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>
|
||
{% endif %}
|
||
</div>
|
||
<div class="flex flex-col divide-y">
|
||
{% for assignment in staff %}
|
||
<div class="flex items-center gap-3 px-4 py-2.5">
|
||
<div class="avatar avatar-placeholder">
|
||
<div class="h-8 w-8 {% if assignment.position.management_position %}bg-club{% endif %}">
|
||
<span class="text-xs">{{ assignment.member.first_name|slice:":1" }}{{ assignment.member.last_name|slice:":1" }}</span>
|
||
</div>
|
||
</div>
|
||
<div class="min-w-0 flex-1">
|
||
<div class="text-sm font-semibold text-ink">{{ assignment.member }}</div>
|
||
<div class="text-xs text-muted">{{ assignment.position }}</div>
|
||
</div>
|
||
<span class="font-mono text-xs text-dim">{% blocktrans with year=assignment.created|date:"Y" %}since {{ year }}{% endblocktrans %}</span>
|
||
{% if can_manage %}
|
||
<div class="flex shrink-0 gap-1">
|
||
<button class="btn btn-outline btn-xs" type="button" onclick="document.getElementById('{{ assignment.pk|dom_id:"edit_staff_modal" }}').showModal()" aria-label="{% trans 'Edit' %}">{% lucide "pencil" size=12 %}</button>
|
||
<button class="btn btn-outline btn-error btn-xs" type="button" onclick="document.getElementById('{{ assignment.pk|dom_id:"remove_staff_modal" }}').showModal()" aria-label="{% trans 'Remove' %}">{% lucide "user-minus" size=12 %}</button>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
{% empty %}
|
||
<div class="px-4 py-4 text-center text-sm text-muted">{% trans "No staff assigned for this season yet." %}</div>
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card p-4">
|
||
<div class="card-title mb-1">{% lucide "flag" size=16 %} {% trans "Eligible referees" %}</div>
|
||
{% if eligible_referees is None %}
|
||
<p class="text-sm text-muted">{% 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 text-muted">{% 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 mt-2">
|
||
{% for referee in eligible_referees %}
|
||
<a class="badge badge-outline transition-colors hover:bg-ink hover:text-white" href="{% url 'management:member_detail' referee.pk %}">{{ referee }}</a>
|
||
{% endfor %}
|
||
</div>
|
||
{% else %}
|
||
<p class="text-sm text-dim">{% trans "No one yet." %}</p>
|
||
{% endif %}
|
||
{% endif %}
|
||
</div>
|
||
|
||
{% if can_manage %}
|
||
{% trans "Add player" as add_player_label %}
|
||
{% url 'management:team_roster_add' team.pk selected_season.pk as add_player_url %}
|
||
{% include "controlpanel/_modal_form.html" with modal_id="add_player_modal" title=add_player_label form=roster_form action_url=add_player_url submit_label=add_player_label submit_icon="user-plus" %}
|
||
|
||
{% trans "Assign staff" as add_staff_label %}
|
||
{% url 'management:team_staff_add' team.pk selected_season.pk as add_staff_url %}
|
||
{% include "controlpanel/_modal_form.html" with modal_id="add_staff_modal" title=add_staff_label form=staff_form action_url=add_staff_url submit_label=add_staff_label submit_icon="user-plus" %}
|
||
|
||
{% trans "Edit player" as edit_player_label %}
|
||
{% trans "Save" as save_label %}
|
||
{% for membership in roster %}
|
||
{% url 'management:team_roster_update' team.pk membership.pk as edit_player_url %}
|
||
{% include "controlpanel/_modal_form.html" with modal_id=membership.pk|dom_id:"edit_player_modal" title=edit_player_label form=membership.edit_form action_url=edit_player_url submit_label=save_label submit_icon="save" %}
|
||
|
||
{% blocktrans asvar remove_player_body with member=membership.member %}Remove {{ member }} from the roster?{% endblocktrans %}
|
||
{% trans "Remove player" as remove_player_title %}
|
||
{% trans "Remove" as remove_label %}
|
||
{% url 'management:team_roster_remove' team.pk membership.pk as remove_player_url %}
|
||
{% include "controlpanel/_confirm_modal.html" with modal_id=membership.pk|dom_id:"remove_player_modal" title=remove_player_title body=remove_player_body action_url=remove_player_url submit_label=remove_label submit_icon="user-minus" %}
|
||
{% endfor %}
|
||
|
||
{% trans "Edit staff assignment" as edit_staff_label %}
|
||
{% for assignment in staff %}
|
||
{% url 'management:team_staff_update' team.pk assignment.pk as edit_staff_url %}
|
||
{% include "controlpanel/_modal_form.html" with modal_id=assignment.pk|dom_id:"edit_staff_modal" title=edit_staff_label form=assignment.edit_form action_url=edit_staff_url submit_label=save_label submit_icon="save" %}
|
||
|
||
{% blocktrans asvar remove_staff_body with member=assignment.member %}Remove {{ member }} from staff?{% endblocktrans %}
|
||
{% trans "Remove staff" as remove_staff_title %}
|
||
{% url 'management:team_staff_remove' team.pk assignment.pk as remove_staff_url %}
|
||
{% include "controlpanel/_confirm_modal.html" with modal_id=assignment.pk|dom_id:"remove_staff_modal" title=remove_staff_title body=remove_staff_body action_url=remove_staff_url submit_label=remove_label submit_icon="user-minus" %}
|
||
{% endfor %}
|
||
{% endif %}
|
||
{% endif %}
|
||
{% endblock panel %}
|
||
|
||
{% block extra_body %}
|
||
<script src="{% static 'js/searchable-select.js' %}"></script>
|
||
{% endblock extra_body %}
|