Files
RosterChief/management/templates/management/team_detail.html
Bernard Siebens 9f4a0ea687 Make the management app responsive on mobile
Replace the mobile nav's horizontal scroll strip (which wrapped into
an unreadable jumble because daisyUI's .menu sets flex-wrap: wrap and
.menu-horizontal never resets it) with a hamburger button that opens
a proper left-side drawer, mirrored in controlpanel for the same bug.
Move the theme toggle, "Management", and "Django admin" controls into
that drawer below `lg`, leaving only the account icon in the navbar,
so the club name has room to render in full instead of truncating to
initials.

Add a reusable `.table-cards` CSS pattern (app.css) that turns a list
table into a stack of labelled cards below `md`, and apply it to every
list/detail table in the management app -- members, families, teams,
memberships, events, groups, locations, opponents, positions, referee
levels/list, roles, sponsors, news, parent claims, team roster/staff,
and the shared family-members table. Tables revert to normal desktop
layout at `md` and up.

Member list gets extra passes: search submits icon-only below `sm` so
it fits next to the input, the Members/Guardians/Both tabs go
full-width, and last/first name merge into one column. Action-button
rows across every page stack full-width on mobile instead of wrapping
mid-button (shared fix in base.html). Home dashboard's upcoming-events
and news mini-tables become simple lists instead of overflowing
tables. Also: brand truncates instead of overflowing on a long club
name, a stray invalid xmlns attribute removed, a wrapping UUID badge
hidden below `sm` on team detail.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 09:55:08 +02:00

330 lines
20 KiB
HTML

{% extends "management/base.html" %}
{% load i18n lucide static ui %}
{% block heading %}{{ team.name }}{% endblock heading %}
{% block subheading %}
{# The raw id is a reference for support/debugging, not something a coach glancing at their phone needs -- hidden below `sm` rather than left to wrap awkwardly inside the pill. #}
<span class="flex flex-row items-center gap-2">
<span>{{ team.short_name }}</span>
<span class="hidden sm:inline">&middot;</span>
<span class="badge badge-outline badge-sm hidden sm:inline-flex">{% lucide "bookmark" size=14 %} {{ team.pk }}</span>
</span>
{% endblock subheading %}
{% 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 %}
<form method="get" class="mb-4">
<div class="flex flex-row flex-wrap items-center gap-2">
<select name="season" class="select select-bordered">
{% 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>
<button class="btn btn-outline gap-2" type="submit">{% lucide "filter" size=16 %} {% trans "View" %}</button>
</div>
</form>
{% if not selected_season %}
<div class="alert alert-warning mb-6">
{% 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 %}
<div class="mb-4 grid gap-4 lg:grid-cols-3">
<div class="card bg-base-100 shadow border-l-4 {% if attendance_rate is None %}border-info{% elif attendance_rate < 30 %}border-error{% elif attendance_rate < 65 %}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 "user-check" size=16 %} {% trans "Attendance rate" %}</div>
<div class="text-4xl font-bold tabular-nums font-mono">
{% if attendance_rate is None %}
{% trans "N/A" %}
{% else %}
{{ attendance_rate }}%
{% endif %}
</div>
<div class="text-xs opacity-60">
{% if attendance_rate is None %}
{% trans "No past events this season" %}
{% else %}
<progress class="progress w-full {% 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>
<div class="card bg-base-100 shadow">
<div class="card-body">
<h2 class="card-title text-base">{% lucide "trending-up" size=18 %} {% trans "Top attenders" %}</h2>
<ul class="divide-y divide-base-200">
{% for entry in top_attenders %}
<li class="flex items-center justify-between py-2">
<span class="text-sm">{{ entry.member }}</span>
<span class="font-semibold tabular-nums font-mono">{{ entry.rate }}%</span>
</li>
{% empty %}
<li class="py-2 text-center text-sm opacity-60">{% trans "Not enough data yet." %}</li>
{% endfor %}
</ul>
</div>
</div>
<div class="card bg-base-100 shadow">
<div class="card-body">
<h2 class="card-title text-base">{% lucide "trending-down" size=18 %} {% trans "Needs attention" %}</h2>
<ul class="divide-y divide-base-200">
{% for entry in bottom_attenders %}
<li class="flex items-center justify-between py-2">
<span class="text-sm">{{ entry.member }}</span>
<span class="font-semibold tabular-nums font-mono">{{ entry.rate }}%</span>
</li>
{% empty %}
<li class="py-2 text-center text-sm opacity-60">{% trans "Not enough data yet." %}</li>
{% endfor %}
</ul>
</div>
</div>
</div>
<div class="mb-6 grid gap-4 lg:grid-cols-2">
<div class="card bg-base-100 shadow">
<div class="card-body">
<h2 class="card-title text-base">{% lucide "alert-triangle" size=18 %} {% trans "Missed the last 2 practices" %}</h2>
<ul class="divide-y divide-base-200">
{% for member in missed_practices %}
<li class="py-2 text-sm">{{ member }}</li>
{% empty %}
<li class="py-2 text-center text-sm opacity-60">{% trans "No one -- or not enough practice history yet." %}</li>
{% endfor %}
</ul>
</div>
</div>
<div class="card bg-base-100 shadow">
<div class="card-body">
<h2 class="card-title text-base">{% lucide "user-x" size=18 %} {% trans "No-shows" %}</h2>
<!-- <p class="text-sm opacity-70">{% trans "Said they'd attend, but were checked in as absent." %}</p> -->
<ul class="divide-y divide-base-200">
{% for entry in no_shows %}
<li class="flex flex-col gap-1 py-2 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
<div class="min-w-0">
<span class="font-semibold">{{ entry.member }}</span>
<span class="text-sm opacity-70">&middot; {{ entry.event.title }}</span>
</div>
<div class="shrink-0 whitespace-nowrap text-sm opacity-60">{{ entry.event.start|date:"j M" }}</div>
</li>
{% empty %}
<li class="py-2 text-center text-sm opacity-60">{% trans "None recorded." %}</li>
{% endfor %}
</ul>
</div>
</div>
</div>
<div class="card bg-base-100 shadow">
<div class="card-body">
<div class="flex flex-wrap items-center justify-between gap-2">
<h2 class="card-title text-base">{% trans "Roster" %}</h2>
{% if can_manage %}
<div class="flex flex-wrap 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">
<table class="table table-cards">
<thead>
<tr>
<th>{% trans "Member" %}</th>
<th>{% trans "Position" %}</th>
<th>{% trans "Jersey #" %}</th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
{% for membership in roster %}
<tr>
<td class="font-semibold">{{ membership.member }}</td>
<td data-label="{% trans 'Position' %}">{{ membership.position }}</td>
<td data-label="{% trans 'Jersey #' %}">{{ membership.jersey_number|default:"—" }}</td>
<td>
{% if membership.is_captain %}<span class="badge badge-neutral badge-sm">{% trans "Captain" %}</span>{% endif %}
{% if membership.is_alternate_captain %}<span class="badge badge-neutral badge-sm">{% trans "Alternate captain" %}</span>{% endif %}
</td>
<td class="text-right">
{% if can_manage %}
<div class="flex flex-wrap 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-sm btn-outline btn-error" 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="5" class="text-center opacity-60">{% trans "No one on the roster for this season yet." %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<div class="card bg-base-100 shadow mt-4">
<div class="card-body">
<div class="flex flex-wrap items-center justify-between gap-2">
<h2 class="card-title text-base">{% trans "Staff" %}</h2>
{% if can_manage %}
<div class="flex flex-wrap 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">
<table class="table">
<thead>
<tr>
<th>{% trans "Member" %}</th>
<th>{% trans "Position" %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for assignment in staff %}
<tr>
<td>{{ assignment.member }}</td>
<td>{{ assignment.position }}</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('{{ assignment.pk|dom_id:"edit_staff_modal" }}').showModal()" aria-label="{% trans 'Edit' %}">{% lucide "pencil" size=14 %} {% trans "Edit" %}</button>
<button class="btn btn-sm btn-outline btn-error" type="button" onclick="document.getElementById('{{ assignment.pk|dom_id:"remove_staff_modal" }}').showModal()" aria-label="{% trans 'Remove' %}">{% lucide "user-minus" size=14 %} {% trans "Remove" %}</button>
</div>
{% endif %}
</td>
</tr>
{% empty %}
<tr>
<td colspan="3" class="text-center opacity-60">{% trans "No staff assigned for this season yet." %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</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 flex-wrap items-center justify-between gap-2">
<h2 class="card-title text-base">{% trans "Team photo" %}</h2>
{% if can_manage %}
<div class="flex flex-wrap gap-2">
{% trans "Upload photo" as upload_photo_label %}
{% trans "Replace photo" as replace_photo_label %}
<button class="btn btn-outline btn-sm gap-2" 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-outline btn-error btn-sm gap-2" type="button" onclick="document.getElementById('team_photo_delete_modal').showModal()">{% lucide "trash-2" size=14 %} {% trans "Remove" %}</button>
{% endif %}
</div>
{% endif %}
</div>
{% if team_photo %}
<img class="mt-2 w-full max-w-md rounded-lg object-cover" src="{{ team_photo.image.url }}" alt="{{ team }}">
{% else %}
<p class="text-sm opacity-60">{% trans "No photo uploaded for this season yet." %}</p>
{% endif %}
</div>
</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 %}