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
This commit is contained in:
2026-08-19 23:34:43 +02:00
parent bff685966d
commit adf1120358
157 changed files with 20342 additions and 4008 deletions

View File

@@ -0,0 +1,111 @@
{% load static lucide ui i18n %}
{% comment %}
Standalone shell for allauth screens (password change/reset, MFA, passkeys, recovery
codes) and 403.html/maintenance.html, rendered whenever the request path is under
/manage/ (see club/context_processors.py: MANAGEMENT_BASE_TEMPLATE). Without this, a
club staff member clicking "Change password" or "Manage MFA" from the management app's
user menu would get bounced onto _club_base.html's old daisyUI chain -- the club's
*public* skin, not the management app they were just in. Same reasoning and structure as
controlpanel/_auth_base.html (its own fork of this same problem for the platform side),
but themed with assets/management.css and this club's own colours, matching
management/base.html.
Block names match what templates/allauth/layouts/base.html and templates/403.html/
maintenance.html target (head_title, extra_head, main, extra_body) -- same contract as
controlpanel/_auth_base.html and _club_base.html, so none of those shared templates need
to know this base exists.
{% endcomment %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>
{% block head_title %}{% endblock head_title %} &middot; {{ club.name }}
</title>
<link rel="stylesheet" href="{% static 'css/management.css' %}">
{% if club.secondary_color %}
<style>
:root {
--tenant-club: {{ club.secondary_color }};
--tenant-club-dark: color-mix(in srgb, {{ club.secondary_color }} 80%, black);
}
</style>
{% endif %}
{% block extra_head %}{% endblock extra_head %}
</head>
<body class="flex min-h-screen flex-col items-center gap-10 bg-ink px-4 py-14 font-sans text-slate">
<div class="flex w-full max-w-md items-center justify-between gap-4 bg-ink py-1">
<a class="flex min-w-0 items-center gap-2.5" href="{% url 'management:home' %}">
{% if club.logo %}
<img class="crest h-8 w-8 shrink-0 object-cover" src="{{ club.logo.url }}" alt="">
{% else %}
<span class="crest flex h-8 w-8 shrink-0 items-center justify-center bg-club">
<span class="font-display text-[11px] font-extrabold text-white">{{ club.initials }}</span>
</span>
{% endif %}
<span class="min-w-0 truncate font-display text-lg font-extrabold tracking-[.08em] text-white uppercase">{{ club.name }}</span>
</a>
<a class="flex shrink-0 items-center gap-1.5 font-mono text-xs text-on-dark-dim hover:text-white" href="{% url 'management:home' %}">
{% lucide "arrow-left" size=14 %} {% trans "Back to management" %}
</a>
</div>
<main class="flex w-full max-w-md flex-1 flex-col justify-center gap-4">
{% if messages %}
<div class="flex flex-col gap-2">
{% for message in messages %}
{% with alert=message|as_alert %}
<div class="alert {{ alert.css }}" role="alert">
{% lucide alert.icon size=18 %}
<div>
<div class="font-display text-sm font-bold tracking-wide uppercase">{{ alert.title }}</div>
<div class="text-sm">{{ alert.body }}</div>
</div>
</div>
{% endwith %}
{% endfor %}
</div>
{% endif %}
{% block main %}{% endblock main %}
</main>
<p class="font-mono text-[11px] text-on-dark-faint">&copy; {% now "Y" %} RosterChief</p>
{% comment %}
Same data-otp enhancement as controlpanel/_auth_base.html -- see that file's own
comment for why the real <input> stays invisible-but-focusable and each typed
character is mirrored into its own <span> instead of trying to align the real
glyphs with the box pitch via CSS alone.
{% endcomment %}
<script>
document.querySelectorAll("[data-otp]").forEach((otp) => {
const input = otp.querySelector("input");
const boxes = otp.querySelectorAll("span");
if (!input) return;
const fit = () => {
const boxed = input.value.length <= boxes.length;
otp.classList.toggle("otp", boxed);
otp.classList.toggle("otp-lg", boxed);
boxes.forEach((box, index) => {
box.classList.toggle("hidden", !boxed);
box.textContent = boxed ? input.value[index] || "" : "";
});
input.classList.toggle("input", !boxed);
input.classList.toggle("input-lg", !boxed);
};
input.addEventListener("input", fit);
fit();
});
</script>
{% block extra_body %}{% endblock extra_body %}
</body>
</html>

View File

@@ -9,59 +9,57 @@ management.views.group_by_family: family/guardians/children/others/all) and
passes its own URL so admins land back on the member they were viewing; family_detail.html
leaves it unset, since staying on the family page is already the right place there.
{% endcomment %}
<div class="overflow-x-auto">
<table class="table table-cards">
<thead>
<tr>
<th>{% trans "Name" %}</th>
<th>{% trans "Email" %}</th>
<th>{% trans "Type" %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for person in group.all %}
<tr>
<td><a class="link link-hover font-semibold" href="{% url 'management:member_detail' person.pk %}">{{ person.last_name }}, {{ person.first_name }}</a></td>
<td data-label="{% trans 'Email' %}">{{ person.contact_email|default:"-" }}</td>
<td data-label="{% trans 'Type' %}">
{% if is_club_admin %}
<form method="post" action="{% url 'management:family_membership_role_update' group.family.pk person.pk %}">
{% csrf_token %}
{% if next_url %}<input type="hidden" name="next" value="{{ next_url }}">{% endif %}
<select name="role" class="select select-bordered select-sm" onchange="this.form.requestSubmit()" aria-label="{% trans 'Role in family' %}">
{% for value, label in family_role_choices %}
<option value="{{ value }}" {% if value == person.role_in_family %}selected{% endif %}>{{ label|capfirst }}</option>
{% endfor %}
</select>
</form>
{% else %}
<span class="badge badge-sm badge-ghost">{{ person.role_in_family_display|capfirst }}</span>
<table class="table">
<thead>
<tr>
<th>{% trans "Name" %}</th>
<th>{% trans "Email" %}</th>
<th>{% trans "Type" %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for person in group.all %}
<tr>
<td><a class="link link-hover font-semibold text-ink" href="{% url 'management:member_detail' person.pk %}">{{ person.last_name }}, {{ person.first_name }}</a></td>
<td class="text-sm text-muted">{{ person.contact_email|default:"—" }}</td>
<td>
{% if is_club_admin %}
<form method="post" action="{% url 'management:family_membership_role_update' group.family.pk person.pk %}">
{% csrf_token %}
{% if next_url %}<input type="hidden" name="next" value="{{ next_url }}">{% endif %}
<select name="role" class="select w-auto" onchange="this.form.requestSubmit()" aria-label="{% trans 'Role in family' %}">
{% for value, label in family_role_choices %}
<option value="{{ value }}" {% if value == person.role_in_family %}selected{% endif %}>{{ label|capfirst }}</option>
{% endfor %}
</select>
</form>
{% else %}
<span class="badge badge-sm badge-ghost">{{ person.role_in_family_display|capfirst }}</span>
{% endif %}
</td>
<td class="text-right">
{% if is_club_admin %}
<div class="flex flex-wrap justify-end gap-1">
{% if person.grant_login_form %}
<button class="btn btn-outline btn-xs" type="button" onclick="document.getElementById('grant_login_modal_{{ group.family.pk }}_{{ person.pk }}').showModal()" aria-label="{% trans 'Grant login' %}">
{% lucide "key-round" size=13 %} {% trans "Grant login" %}
</button>
{% endif %}
</td>
<td class="text-right">
{% if is_club_admin %}
<div class="flex flex-wrap justify-end gap-1">
{% if person.grant_login_form %}
<button class="btn btn-outline btn-sm" type="button" onclick="document.getElementById('grant_login_modal_{{ group.family.pk }}_{{ person.pk }}').showModal()" aria-label="{% trans 'Grant login' %}">
{% lucide "key-round" size=14 %} {% trans "Grant login" %}
</button>
{% endif %}
<a class="btn btn-outline btn-sm" href="{% url 'management:member_detail' person.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('remove_family_modal_{{ group.family.pk }}_{{ person.pk }}').showModal()" aria-label="{% trans 'Remove from family' %}">
{% lucide "user-x" size=14 %} {% trans "Remove" %}
</button>
<button class="btn btn-sm btn-outline btn-error" type="button" onclick="document.getElementById('member_delete_modal_{{ group.family.pk }}_{{ person.pk }}').showModal()" aria-label="{% trans 'Delete' %}">
{% lucide "trash-2" size=14 %} {% trans "Delete" %}
</button>
</div>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<a class="btn btn-outline btn-xs" href="{% url 'management:member_detail' person.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('remove_family_modal_{{ group.family.pk }}_{{ person.pk }}').showModal()" aria-label="{% trans 'Remove from family' %}">
{% lucide "user-x" size=13 %} {% trans "Remove" %}
</button>
<button class="btn btn-xs btn-outline btn-error" type="button" onclick="document.getElementById('member_delete_modal_{{ group.family.pk }}_{{ person.pk }}').showModal()" aria-label="{% trans 'Delete' %}">
{% lucide "trash-2" size=13 %} {% trans "Delete" %}
</button>
</div>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if is_club_admin %}
{% trans "Delete member" as delete_member_title %}

View File

@@ -1,5 +1,5 @@
{% extends "management/base.html" %}
{% load i18n %}
{% load i18n lucide %}
{% comment %}
Shared placeholder for every entity that doesn't have its own list template yet
@@ -11,23 +11,26 @@
{% block heading %}{{ page_title }}{% endblock heading %}
{% block panel %}
<div class="card bg-base-100 shadow">
<div class="card-body">
<div class="overflow-x-auto">
<table class="table">
<tbody>
{% for object in object_list %}
<tr>
<td>{{ object }}</td>
</tr>
{% empty %}
<tr>
<td class="text-center opacity-60">{% trans "Nothing here yet." %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="alert alert-info">
{% lucide "construction" size=18 %}
<span>{% trans "This section is still being built. For now it only lists what's on file." %}</span>
</div>
<div class="card overflow-hidden">
<div class="overflow-x-auto">
<table class="table">
<tbody>
{% for object in object_list %}
<tr>
<td>{{ object }}</td>
</tr>
{% empty %}
<tr>
<td class="text-center text-muted">{% trans "Nothing here yet." %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock panel %}

View File

@@ -6,8 +6,8 @@
<tr class="bulk-add-row">
<td>
{{ form.member }}
{% for error in form.member.errors %}<p class="text-xs text-error mt-1">{{ error }}</p>{% endfor %}
{% for error in form.non_field_errors %}<p class="text-xs text-error mt-1">{{ error }}</p>{% endfor %}
{% for error in form.member.errors %}<p class="mt-1 text-xs text-club-dark">{{ error }}</p>{% endfor %}
{% for error in form.non_field_errors %}<p class="mt-1 text-xs text-club-dark">{{ error }}</p>{% endfor %}
</td>
<td class="w-12">
<button class="btn btn-ghost btn-sm remove-row" type="button" aria-label="{% trans 'Remove this row' %}" title="{% trans 'Remove this row' %}">{% lucide "x" size=16 %}</button>

View File

@@ -1,74 +1,109 @@
{% load i18n lucide %}
{% comment %}
The management nav, in one place: the sidebar renders it on a wide screen and the
collapsed menu renders it on a narrow one. Admin-only sections are hidden here for
plain staff -- the views are gated regardless (ClubAdminRequiredMixin), this is
just so the nav never shows a link they can't follow.
The two-level sidebar (design_handoff_rosterchief_platform/README.md, D1/D2): seven
top-level sections -- Overview/Members/Teams/Calendar/News/Finance/Settings -- each
except News expanding into sub-items while it (or a sub-item) is active. `nav`/
`nav_section` come from management.context_processors.active_nav_section.
`nav` (management.context_processors.active_nav_section) is the current page's
section, derived from the resolved URL name -- `menu-active` is daisyUI's active
state, same convention as controlpanel/templates/controlpanel/_nav_items.html.
Admin-only sub-items are hidden here for plain staff -- the views are gated
regardless (ClubAdminRequiredMixin), this is just so the nav never shows a link
they can't follow. Same reasoning as the old flat nav this replaces.
{% endcomment %}
<li>
<a class="{% if nav == 'home' %}menu-active{% endif %}" href="{% url 'management:home' %}">{% lucide "layout-dashboard" size=16 %} {% trans "Dashboard" %}</a>
</li>
<a class="nav-item {% if nav_section == 'overview' %}active{% endif %}" href="{% url 'management:home' %}">{% lucide "layout-dashboard" size=17 %} {% trans "Overview" %}</a>
<li class="menu-title">{% trans "People" %}</li>
<li><a class="{% if nav == 'member_list' %}menu-active{% endif %}" href="{% url 'management:member_list' %}">{% lucide "users" size=16 %} {% trans "Members" %}</a></li>
<li><a class="{% if nav == 'family_list' %}menu-active{% endif %}" href="{% url 'management:family_list' %}">{% lucide "home" size=16 %} {% trans "Families" %}</a></li>
{% if is_club_admin %}
<li>
<a class="{% if nav == 'parent_claim_list' %}menu-active{% endif %}" href="{% url 'management:parent_claim_list' %}">
{% lucide "inbox" size=16 %} {% trans "Parent claims" %}
<span class="badge badge-sm ml-auto {% if pending_parent_claims_count %}badge-error{% else %}badge-neutral{% endif %}">{{ pending_parent_claims_count }}</span>
</a>
</li>
{% endif %}
{% if is_club_admin %}
<li><a class="{% if nav == 'membership_list' %}menu-active{% endif %}" href="{% url 'management:membership_list' %}">{% lucide "wallet" size=16 %} {% trans "Memberships" %}</a></li>
<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 %}
<div>
<a class="nav-item {% if nav_section == 'members' %}active{% endif %}" href="{% url 'management:member_list' %}">{% lucide "users" size=17 %} {% trans "Members" %}</a>
{% if nav_section == 'members' %}
<div class="flex flex-col">
<a class="nav-subitem {% if nav == 'member_list' %}active{% endif %}" href="{% url 'management:member_list' %}">{% trans "All members" %}</a>
{% if is_club_admin %}
{# Sign-up is admin only (finance-adjacent -- fee status feeds it) even though parent claims/households/groups below open up to MEMBER_ADMIN too. #}
<a class="nav-subitem {% if nav == 'signup_list' %}active{% endif %}" href="{% url 'management:signup_list' %}">{% trans "Sign-up" %}</a>
{% endif %}
{% if can_manage_members %}
{# Membership dues/fee tracking (management:membership_list) lives under Finance, not here -- see that section below. #}
<!--
<a class="nav-subitem flex items-center gap-2 {% if nav == 'parent_claim_list' %}active{% endif %}" href="{% url 'management:parent_claim_list' %}">{% trans "Parent claims" %}
{% if pending_parent_claims_count %}<span class="badge badge-error badge-xs">{{ pending_parent_claims_count }}</span>{% endif %}
</a>
-->
{% endif %}
<a class="nav-subitem {% if nav == 'family_list' %}active{% endif %}" href="{% url 'management:family_list' %}">{% trans "Households" %}</a>
{% if can_manage_members %}
<a class="nav-subitem {% if nav == 'group_list' %}active{% endif %}" href="{% url 'management:group_list' %}">{% trans "Groups" %}</a>
{% endif %}
</div>
{% endif %}
</div>
<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>
{% if is_club_admin %}
<li><a class="{% if nav == 'referee_level_list' %}menu-active{% endif %}" href="{% url 'management:referee_level_list' %}">{% lucide "badge-check" size=16 %} {% trans "Referee levels" %}</a></li>
<li>
<a class="{% if nav == 'referee_management' %}menu-active{% endif %}" href="{% url 'management:referee_management' %}">
{% lucide "calendar-check" size=16 %} {% trans "Referee management" %}
<span class="badge badge-sm ml-auto {% if games_missing_referees_count %}badge-error{% else %}badge-neutral{% endif %}">{{ games_missing_referees_count }}</span>
</a>
</li>
{% endif %}
<li><a class="{% if nav == 'referee_list' %}menu-active{% endif %}" href="{% url 'management:referee_list' %}">{% lucide "flag" size=16 %} {% trans "Referees" %}</a></li>
<div>
<a class="nav-item {% if nav_section == 'teams' %}active{% endif %}" href="{% url 'management:team_list' %}">{% lucide "shirt" size=17 %} {% trans "Teams" %}</a>
{% if nav_section == 'teams' %}
<div class="flex flex-col">
<a class="nav-subitem {% if nav == 'team_list' %}active{% endif %}" href="{% url 'management:team_list' %}">{% trans "All teams" %}</a>
<a class="nav-subitem {% if nav == 'referee_list' %}active{% endif %}" href="{% url 'management:referee_list' %}">{% trans "Referees" %}</a>
</div>
{% endif %}
</div>
<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>
<div>
<a class="nav-item {% if nav_section == 'calendar' %}active{% endif %}" href="{% url 'management:event_list' %}">{% lucide "calendar" size=17 %} {% trans "Calendar" %}</a>
{% if nav_section == 'calendar' %}
<div class="flex flex-col">
<a class="nav-subitem {% if nav == 'event_list' %}active{% endif %}" href="{% url 'management:event_list' %}">{% trans "Events" %}</a>
{% if has_management_position %}
<a class="nav-subitem {% if nav == 'location_list' %}active{% endif %}" href="{% url 'management:location_list' %}">{% trans "Locations" %}</a>
<a class="nav-subitem {% if nav == 'opponent_list' %}active{% endif %}" href="{% url 'management:opponent_list' %}">{% trans "Opponents" %}</a>
{% endif %}
</div>
{% endif %}
</div>
<li class="menu-title">{% trans "Calendar" %}</li>
<li><a class="{% if nav == 'event_list' %}menu-active{% endif %}" href="{% url 'management:event_list' %}">{% lucide "calendar" size=16 %} {% trans "Events" %}</a></li>
{% if has_management_position %}
<li><a class="{% if nav == 'location_list' %}menu-active{% endif %}" href="{% url 'management:location_list' %}">{% lucide "map-pin" size=16 %} {% trans "Locations" %}</a></li>
<li><a class="{% if nav == 'opponent_list' %}menu-active{% endif %}" href="{% url 'management:opponent_list' %}">{% lucide "swords" size=16 %} {% trans "Opponents" %}</a></li>
{% endif %}
<a class="nav-item {% if nav_section == 'news' %}active{% endif %}" href="{% url 'management:news_list' %}">{% lucide "newspaper" size=17 %} {% trans "News" %}</a>
{% if is_club_admin %}
<li class="menu-title">{% trans "Sponsors" %}</li>
<li><a class="{% if nav == 'sponsor_list' %}menu-active{% endif %}" href="{% url 'management:sponsor_list' %}">{% lucide "handshake" size=16 %} {% trans "Sponsors" %}</a></li>
<div>
<a class="nav-item {% if nav_section == 'finance' %}active{% endif %}" href="{% url 'management:membership_list' %}">{% lucide "wallet" size=17 %} {% trans "Finance" %}</a>
{% if nav_section == 'finance' %}
<div class="flex flex-col">
<a class="nav-subitem {% if nav == 'membership_list' %}active{% endif %}" href="{% url 'management:membership_list' %}">{% trans "Dues & billing" %}</a>
{% if shop_enabled %}
<a class="nav-subitem {% if nav == 'product_list' %}active{% endif %}" href="{% url 'management:product_list' %}">{% trans "Products" %}</a>
<a class="nav-subitem {% if nav == 'order_list' %}active{% endif %}" href="{% url 'management:order_list' %}">{% trans "Orders" %}</a>
<a class="nav-subitem {% if nav == 'discount_list' %}active{% endif %}" href="{% url 'management:discount_list' %}">{% trans "Discounts" %}</a>
<a class="nav-subitem {% if nav == 'invoice_list' %}active{% endif %}" href="{% url 'management:invoice_list' %}">{% trans "Invoices" %}</a>
{% endif %}
</div>
{% endif %}
</div>
{% endif %}
{% if is_club_admin and shop_enabled %}
<li class="menu-title">{% trans "Shop" %}</li>
<li><a class="{% if nav == 'product_list' %}menu-active{% endif %}" href="{% url 'management:product_list' %}">{% lucide "package" size=16 %} {% trans "Products" %}</a></li>
<li><a class="{% if nav == 'order_list' %}menu-active{% endif %}" href="{% url 'management:order_list' %}">{% lucide "shopping-cart" size=16 %} {% trans "Orders" %}</a></li>
<li><a class="{% if nav == 'discount_list' %}menu-active{% endif %}" href="{% url 'management:discount_list' %}">{% lucide "percent" size=16 %} {% trans "Discounts" %}</a></li>
<li><a class="{% if nav == 'invoice_list' %}menu-active{% endif %}" href="{% url 'management:invoice_list' %}">{% lucide "receipt" size=16 %} {% trans "Invoices" %}</a></li>
{% endif %}
{% if is_club_admin and forms_enabled %}
<li class="menu-title">{% trans "Forms" %}</li>
<li><a class="{% if nav == 'form_list' %}menu-active{% endif %}" href="{% url 'management:form_list' %}">{% lucide "clipboard-list" size=16 %} {% trans "Forms" %}</a></li>
{% if is_club_admin or can_manage_members %}
<div>
{# MEMBER_ADMIN has no Club identity access, so their landing click on "Settings" itself goes to the first sub-item they can actually reach. #}
<a class="nav-item {% if nav_section == 'settings' %}active{% endif %}" href="{% if is_club_admin %}{% url 'management:club_settings' %}{% else %}{% url 'management:onboarding_requirement_list' %}{% endif %}">{% lucide "settings" size=17 %} {% trans "Settings" %}</a>
{% if nav_section == 'settings' %}
<div class="flex flex-col">
{% if is_club_admin %}
<a class="nav-subitem {% if nav == 'club_settings' %}active{% endif %}" href="{% url 'management:club_settings' %}">{% trans "Club identity" %}</a>
{% endif %}
<a class="nav-subitem {% if nav == 'onboarding_requirement_list' %}active{% endif %}" href="{% url 'management:onboarding_requirement_list' %}">{% trans "Onboarding requirements" %}</a>
{% if is_club_admin %}
<a class="nav-subitem {% if nav == 'role_list' %}active{% endif %}" href="{% url 'management:role_list' %}">{% trans "Roles" %}</a>
<a class="nav-subitem {% if nav == 'position_list' %}active{% endif %}" href="{% url 'management:position_list' %}">{% trans "Positions" %}</a>
{% endif %}
<a class="nav-subitem {% if nav == 'referee_level_list' %}active{% endif %}" href="{% url 'management:referee_level_list' %}">{% trans "Referee levels" %}</a>
<a class="nav-subitem flex items-center gap-2 {% if nav == 'referee_management' %}active{% endif %}" href="{% url 'management:referee_management' %}">{% trans "Referee management" %}
{% if games_missing_referees_count %}<span class="badge badge-error badge-xs">{{ games_missing_referees_count }}</span>{% endif %}
</a>
{% if is_club_admin %}
<a class="nav-subitem {% if nav == 'sponsor_list' %}active{% endif %}" href="{% url 'management:sponsor_list' %}">{% trans "Sponsors" %}</a>
{% if forms_enabled %}
<a class="nav-subitem {% if nav == 'form_list' %}active{% endif %}" href="{% url 'management:form_list' %}">{% trans "Forms" %}</a>
{% endif %}
{% endif %}
</div>
{% endif %}
</div>
{% endif %}

View File

@@ -1,13 +1,20 @@
{% 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).
context: event, referees (EventReferee rows -- fee/km/km_rate read straight
off the model instance, no separate form object needed), 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).
The fee is entered right here, inline, rather than behind a "Fee" button
that opened a second (and on the dashboard, nested-inside-a-dialog) modal
-- that extra click-through was the easiest way for a fee to slip past
everyone's attention until PDF export time. Travel (km/rate) stays behind
a small disclosure since most assignments never need it, but posts in the
same form as the fee so there's still only one Save per referee.
{% endcomment %}
<div class="flex flex-wrap items-center justify-between gap-2">
<h2 class="card-title text-base">
<h2 class="card-title">
{% 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 }}
@@ -17,40 +24,66 @@
<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">
<ul class="divide-y">
{% for referee in referees %}
<li class="flex flex-col gap-1 py-2 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
<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|floatformat:2 %}€{{ total }} due{% endblocktrans %}</span>{% endif %}
</span>
<li class="flex flex-wrap items-center justify-between gap-3 py-2.5">
<div class="flex min-w-0 flex-col gap-0.5">
<div class="flex flex-wrap items-center gap-1.5">
<span class="text-sm font-semibold text-ink">{{ referee.display_name }}</span>
{% if referee.is_external %}<span class="badge badge-neutral badge-xs">{% trans "External" %}</span>{% endif %}
{% if can_manage_referees and not referee.fee %}<span class="badge badge-warning badge-xs">{% trans "Fee not set" %}</span>{% endif %}
</div>
{% if referee.assigned_by %}<span class="text-xs text-muted">{% blocktrans with name=referee.assigned_by %}Assigned by {{ name }}{% endblocktrans %}</span>{% endif %}
</div>
{% 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>
<div class="flex shrink-0 items-center gap-1.5">
{% url 'management:event_referee_fee_update' event.pk referee.pk as fee_action_url %}
<form method="post" action="{{ fee_action_url }}?next={{ next_url|urlencode }}" class="flex items-center gap-1.5">
{% csrf_token %}
<label class="flex items-center gap-1">
<span class="font-mono text-xs text-dim">&euro;</span>
<input type="number" step="0.01" min="0" name="fee" value="{{ referee.fee }}" class="input w-20 text-right" aria-label="{% trans 'Fee' %}">
</label>
<details class="relative">
<summary class="btn btn-square btn-outline btn-xs cursor-pointer list-none" title="{% trans 'Travel reimbursement' %}">{% lucide "car" size=12 %}</summary>
<div class="absolute right-0 z-10 mt-1 flex w-44 flex-col gap-2 rounded-lg border border-line bg-white p-2.5 shadow-lg">
<label class="flex flex-col gap-0.5 text-xs text-muted">{% trans "Km" %}
<input type="number" step="0.1" min="0" name="km" value="{{ referee.km|default_if_none:'' }}" class="input">
</label>
<label class="flex flex-col gap-0.5 text-xs text-muted">{% trans "Rate/km" %}
<input type="number" step="any" min="0" name="km_rate" value="{{ referee.km_rate|default_if_none:'' }}" class="input">
</label>
</div>
</details>
<button class="btn btn-square btn-outline btn-xs" type="submit" title="{% trans 'Save fee' %}" aria-label="{% trans 'Save fee' %}">{% lucide "check" size=12 %}</button>
</form>
{% if referee.km %}
<span class="font-mono text-xs text-muted">{% blocktrans with total=referee.total_payable|floatformat:2 %}{{ total }} due{% endblocktrans %}</span>
{% endif %}
<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>
<button class="btn btn-square btn-outline btn-error btn-xs" type="submit" title="{% trans 'Remove' %}" aria-label="{% trans 'Remove' %}">{% lucide "x" size=12 %}</button>
</form>
</div>
{% elif referee.total_payable %}
<span class="shrink-0 font-mono text-xs text-muted">{% blocktrans with total=referee.total_payable|floatformat:2 %}&euro;{{ total }} due{% endblocktrans %}</span>
{% endif %}
</li>
{% empty %}
<li class="py-2 text-sm opacity-60">{% trans "No referees assigned yet." %}</li>
<li class="py-2.5 text-sm text-muted">{% 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 can_manage_referees and not referees_full %}
<div class="mt-3 flex flex-col gap-2 border-t border-rule pt-3">
<span class="font-mono text-[11px] tracking-[.08em] text-dim uppercase">{% trans "Add a referee" %}</span>
{% if referee_candidates %}
<form method="post" action="{% url 'management:event_referee_assign' event.pk %}" class="mt-2 flex flex-wrap items-center gap-2">
<form method="post" action="{% url 'management:event_referee_assign' event.pk %}" class="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">
<select name="member" class="select w-auto">
{% 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 %}
@@ -59,32 +92,22 @@
</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">
<p class="text-sm text-muted">
{% 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>
<a class="link link-hover" 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">
<form method="post" action="{% url 'management:event_referee_add_external' event.pk %}" class="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' %}">
<input type="text" name="name" class="input w-56" 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 %}
<p class="text-xs text-muted">{% trans "⚠ = also expected at another event around this time -- shown as a warning, not blocked." %}</p>
</div>
{% elif can_manage_referees and referees_full %}
<p class="mt-2 text-sm text-muted">{% trans "This game already has its maximum number of referees." %}</p>
{% endif %}

View File

@@ -5,28 +5,28 @@
row" button clones from -- so the two can never drift apart.
{% endcomment %}
<tr class="bulk-add-row">
<td>
<td class="py-2">
{{ form.member }}
{% for error in form.member.errors %}<p class="text-xs text-error mt-1">{{ error }}</p>{% endfor %}
{% for error in form.non_field_errors %}<p class="text-xs text-error mt-1">{{ error }}</p>{% endfor %}
{% for error in form.member.errors %}<p class="mt-1 text-xs text-club-dark">{{ error }}</p>{% endfor %}
{% for error in form.non_field_errors %}<p class="mt-1 text-xs text-club-dark">{{ error }}</p>{% endfor %}
</td>
<td>
<td class="py-2">
{{ form.position }}
{% for error in form.position.errors %}<p class="text-xs text-error mt-1">{{ error }}</p>{% endfor %}
{% for error in form.position.errors %}<p class="mt-1 text-xs text-club-dark">{{ error }}</p>{% endfor %}
</td>
<td class="w-24">
<td class="w-24 py-2">
{{ form.jersey_number }}
{% for error in form.jersey_number.errors %}<p class="text-xs text-error mt-1">{{ error }}</p>{% endfor %}
{% for error in form.jersey_number.errors %}<p class="mt-1 text-xs text-club-dark">{{ error }}</p>{% endfor %}
</td>
<td class="w-16 text-center">
<td class="w-16 py-2 text-center">
{{ form.is_captain }}
{% for error in form.is_captain.errors %}<p class="text-xs text-error mt-1">{{ error }}</p>{% endfor %}
{% for error in form.is_captain.errors %}<p class="mt-1 text-xs text-club-dark">{{ error }}</p>{% endfor %}
</td>
<td class="w-16 text-center">
<td class="w-16 py-2 text-center">
{{ form.is_alternate_captain }}
{% for error in form.is_alternate_captain.errors %}<p class="text-xs text-error mt-1">{{ error }}</p>{% endfor %}
{% for error in form.is_alternate_captain.errors %}<p class="mt-1 text-xs text-club-dark">{{ error }}</p>{% endfor %}
</td>
<td class="w-12">
<button class="btn btn-ghost btn-sm remove-row" type="button" aria-label="{% trans 'Remove this row' %}" title="{% trans 'Remove this row' %}">{% lucide "x" size=16 %}</button>
<td class="w-12 py-2">
<button class="btn btn-ghost btn-sm btn-square remove-row" type="button" aria-label="{% trans 'Remove this row' %}" title="{% trans 'Remove this row' %}">{% lucide "x" size=16 %}</button>
</td>
</tr>

View File

@@ -1,90 +1,169 @@
{% extends "_club_base.html" %}
{% load i18n lucide %}
{% load static lucide ui i18n %}
{% comment %}
The club-staff shell: team managers, coaches and admins only (never parents or
players -- see club.mixins.ClubStaffRequiredMixin). Mirrors controlpanel/base.html's
block structure, extending the club's own skin instead of the platform's.
Standalone shell for club management -- does NOT extend templates/_base.html or
_club_base.html, and does not load assets/app.css or daisyUI. Same reasoning as
controlpanel/templates/controlpanel/base.html's own standalone shell, but this
surface IS club-branded (design_handoff_rosterchief_platform/README.md, "Club
theming"): the --tenant-club/--tenant-club-dark custom properties below feed
assets/management.css's --color-club/--color-club-dark tokens, set per request
from Club.secondary_color. Desktop only, matching the design's own scope for this
surface -- no mobile nav here, unlike the club-facing chrome this replaces.
{% endcomment %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
{% block head_title %}
{% block section_title %}{% trans "Management" %}{% endblock section_title %}
{% endblock head_title %}
<title>
{% block title %}{% block panel_title %}Management{% endblock panel_title %} &middot; {{ club.name }}{% endblock title %}
</title>
{% block nav_toggle %}
<button class="btn btn-ghost btn-square lg:hidden" type="button" onclick="document.getElementById('mobile_nav_modal').showModal()" aria-label="{% trans 'Menu' %}">
{% lucide "menu" size=20 %}
</button>
{% endblock nav_toggle %}
<link rel="stylesheet" href="{% static 'css/management.css' %}">
{% if club.secondary_color %}
<style>
:root {
--tenant-club: {{ club.secondary_color }};
--tenant-club-dark: color-mix(in srgb, {{ club.secondary_color }} 80%, black);
{% comment %}
Club.secondary_content_color is already the WCAG-luminance black-or-white
choice (Club._content_color_for) -- e.g. the active nav item's own text,
which sits on --color-club and can't assume white reads on every colour.
{% endcomment %}
--tenant-club-content: {{ club.secondary_content_color }};
}
</style>
{% endif %}
{% if club.primary_color %}
<style>
:root {
{# Same WCAG black-or-white choice as secondary_content_color above, for the sidebar's own background. #}
--tenant-sidebar-bg: {{ club.primary_color }};
--tenant-sidebar-fg: {{ club.primary_content_color }};
}
</style>
{% endif %}
{% block extra_head %}{% endblock extra_head %}
</head>
{% comment %} Kept in the navbar itself only at `lg`+, where there's no hamburger drawer to hold them instead -- see the comment on `nav_icons_class` in _base.html. {% endcomment %}
{% block nav_icons_class %}hidden items-center lg:flex{% endblock nav_icons_class %}
<body class="flex h-screen flex-col overflow-hidden bg-paper font-sans text-slate">
<div class="flex flex-1 overflow-hidden">
<aside class="flex w-[236px] shrink-0 flex-col bg-sidebar-bg py-5">
<a class="flex items-center gap-2.5 px-[18px] pb-[22px]" href="{% url 'management:home' %}">
{% if club.logo %}
<img class="crest h-[30px] w-7 shrink-0 object-cover" src="{{ club.logo.url }}" alt="">
{% else %}
<span class="crest flex h-[30px] w-7 shrink-0 items-center justify-center bg-club">
<span class="font-display text-[10px] font-extrabold text-club-content">{{ club.initials }}</span>
</span>
{% endif %}
<span class="min-w-0">
<span class="block truncate font-display text-[17px] leading-none font-extrabold tracking-[.06em] text-sidebar-fg uppercase">{{ club.name }}</span>
<span class="block font-mono text-[10px] text-sidebar-fg-faint">RosterChief &middot; management</span>
</span>
</a>
{% block menu %}
<aside class="hidden w-64 shrink-0 overflow-y-auto border-r border-base-300 bg-base-100 lg:block">
<ul class="menu w-full gap-1 p-3 mt-4">
{% include "management/_nav_items.html" %}
</ul>
</aside>
{% endblock menu %}
<nav class="flex flex-col gap-0.5 px-2.5">
{% include "management/_nav_items.html" %}
</nav>
{% block main %}
{# Below `lg` the sidebar is hidden and {% block nav_toggle %} above opens this instead. #}
<dialog id="mobile_nav_modal" class="modal modal-start lg:hidden">
<div class="modal-box h-full max-h-none w-72 max-w-[85vw] rounded-none p-0">
<div class="flex items-center justify-between border-b border-base-300 p-3">
<span class="font-roboto text-lg font-bold tracking-wider">{% trans "Menu" %}</span>
<form method="dialog">
<button class="btn btn-ghost btn-square btn-sm" aria-label="{% trans 'Close' %}">{% lucide "x" size=18 %}</button>
</form>
<div class="flex-1"></div>
<div class="px-[18px]">
{% comment %}
<details>, not a click-driven dropdown -- no JS needed to open it, only
the outside-click/Escape listener below to close it (a <details> stays
open until its own <summary> is clicked again otherwise, which would be
the only sidebar element on the whole page that doesn't dismiss itself).
Opens upward (bottom-full): the trigger sits at the very bottom of the
sidebar, so a menu opening down would run off the viewport.
{% endcomment %}
<details id="user-menu" class="relative border-t border-sidebar-hairline pt-3.5">
<summary class="flex cursor-pointer list-none items-center gap-2.5 [&::-webkit-details-marker]:hidden">
<span class="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-sidebar-chip font-display text-[13px] font-extrabold text-sidebar-fg">{% if user.member %}{{ user.member.first_name|slice:":1" }}{{ user.member.last_name|slice:":1" }}{% else %}?{% endif %}</span>
<span class="min-w-0 flex-1">
<span class="block truncate text-[13px] font-semibold text-sidebar-fg">{{ user.member|default:user.email }}</span>
<span class="block text-[11px] text-sidebar-fg-faint">{% if is_club_admin %}Admin{% else %}Staff{% endif %}</span>
</span>
{% lucide "chevron-up" size=14 class="shrink-0 text-sidebar-fg-faint" %}
</summary>
<div class="absolute bottom-full left-0 z-20 mb-2 w-full rounded-xl border border-sidebar-hairline bg-sidebar-bg p-1.5 shadow-lg">
<a class="flex items-center gap-2 rounded-lg px-2.5 py-2 text-[13px] font-medium text-sidebar-fg-dim hover:bg-sidebar-hairline hover:text-sidebar-fg" href="{% url 'account_change_password' %}">{% lucide "key-round" size=14 %} {% trans "Change password" %}</a>
<a class="flex items-center gap-2 rounded-lg px-2.5 py-2 text-[13px] font-medium text-sidebar-fg-dim hover:bg-sidebar-hairline hover:text-sidebar-fg" href="{% url 'mfa_index' %}">{% lucide "shield-check" size=14 %} {% trans "Manage two-factor auth" %}</a>
<div class="my-1 h-px bg-sidebar-hairline"></div>
<a class="flex items-center gap-2 rounded-lg px-2.5 py-2 text-[13px] font-medium text-sidebar-fg-dim hover:bg-sidebar-hairline hover:text-sidebar-fg" href="{% url 'account_logout' %}">{% lucide "log-out" size=14 %} {% trans "Log out" %}</a>
</div>
</details>
</div>
</aside>
<div class="flex min-w-0 flex-1 flex-col">
<header class="flex h-16 shrink-0 items-center gap-4 border-b border-line bg-white px-7">
<span class="font-display text-2xl font-extrabold text-ink uppercase">{% block heading %}{% block topbar_title %}Management{% endblock topbar_title %}{% endblock heading %}</span>
{% block topbar_context %}{% endblock topbar_context %}
<div class="flex-1"></div>
<div class="flex items-center gap-2">
{% block actions %}{% endblock actions %}
</div>
</header>
{% block filter_strip %}{% endblock filter_strip %}
<main class="flex-1 overflow-y-auto">
<div class="flex flex-col gap-4 px-7 py-6">
{% if billing_notice.is_urgent and nav != "home" %}
{% include "management/_billing_notice.html" %}
{% endif %}
{% if messages %}
<div class="flex flex-col gap-2">
{% for message in messages %}
{% with alert=message|as_alert %}
<div class="alert {{ alert.css }}" role="alert">
{% lucide alert.icon size=18 %}
<div>
<div class="font-display text-sm font-bold tracking-wide uppercase">{{ alert.title }}</div>
<div class="text-sm">{{ alert.body }}</div>
</div>
</div>
{% endwith %}
{% endfor %}
</div>
{% endif %}
{% block panel %}{% endblock panel %}
</div>
</main>
</div>
<ul class="menu w-full gap-1 p-3">
<li>
<button type="button" data-theme-toggle>
<span data-theme-icon="light" class="hidden items-center gap-3">{% lucide "sun" size=16 %} {% trans "Light theme" %}</span>
<span data-theme-icon="dark" class="hidden items-center gap-3">{% lucide "moon" size=16 %} {% trans "Dark theme" %}</span>
<span data-theme-icon="auto" class="hidden items-center gap-3">{% lucide "sun-moon" size=16 %} {% trans "Auto theme" %}</span>
</button>
</li>
{% if has_management_access %}
<li><a href="{% url "management:home" %}">{% lucide "layout-dashboard" size=16 %} {% trans "Management" %}</a></li>
{% endif %}
{% if user.is_superuser %}
<li><a href="{% url "admin:index" %}">{% lucide "shield-cog" size=16 %} {% trans "Django admin" %}</a></li>
{% endif %}
<li class="menu-title">{% trans "Navigation" %}</li>
{% include "management/_nav_items.html" %}
</ul>
</div>
<form method="dialog" class="modal-backdrop">
<button>{% trans "close" %}</button>
</form>
</dialog>
{# flex-col below `sm`: title over a full-width, stacked column of action buttons reads far better on a phone than the same row wrapping mid-button. #}
<div class="mb-6 flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-center sm:justify-between">
<div class="flex flex-col gap-2 grow">
<h1 class="text-3xl font-bold">
{% block heading %}{% trans "Management" %}{% endblock heading %}
</h1>
<span class="text-sm text-base-content/50">{% block subheading %}{% endblock subheading %}</span>
</div>
<div class="flex w-full flex-col gap-2 sm:w-auto sm:flex-row sm:flex-wrap">
{% block actions %}{% endblock actions %}
</div>
</div>
<script>
(() => {
const menu = document.getElementById("user-menu");
if (!menu) return;
document.addEventListener("click", (event) => {
if (menu.open && !menu.contains(event.target)) menu.open = false;
});
document.addEventListener("keydown", (event) => {
if (event.key === "Escape") menu.open = false;
});
})();
{% comment %}
A final billing notice follows the admin onto every management page -- but only at
error level (inside 7 days of archiving, or already past it). Shown from the moment
anything is owed it would sit on every screen for weeks and train people to ignore
the one week it matters. home.html includes the same partial at every level, hence
the guard here rather than inside the partial.
{% endcomment %}
{% if billing_notice.is_urgent and nav != "home" %}
{% include "management/_billing_notice.html" %}
{% endif %}
// Search-as-you-type: any [data-autosubmit] input submits its own form a
// beat after typing stops, instead of needing a separate Search button.
// Debounced (not one submit per keystroke) so a full-page GET reload isn't
// fired on every character -- 400ms is short enough to feel immediate,
// long enough that normal typing speed never triggers it mid-word.
document.querySelectorAll("[data-autosubmit]").forEach((input) => {
let timeout;
input.addEventListener("input", () => {
clearTimeout(timeout);
timeout = setTimeout(() => input.form.requestSubmit(), 400);
});
});
</script>
{% block panel %}{% endblock panel %}
{% endblock main %}
{% block extra_body %}{% endblock extra_body %}
</body>
</html>

View File

@@ -0,0 +1,76 @@
{% extends "management/base.html" %}
{% load i18n lucide ui %}
{% comment %}
A club's own self-service identity/branding editor -- see
management/forms.py's ClubSettingsForm for exactly which fields are
editable here versus platform-staff-only (slug, seasons). D9 "Club
identity & branding" in the design handoff is the reference; its
live-preview and advanced (custom stylesheet / own domain) panels aren't
real features here, so this sticks to the fields the form actually has,
laid out in the same card-per-section language.
{% endcomment %}
{% block panel_title %}{% trans "Club identity" %}{% endblock panel_title %}
{% block heading %}{% trans "Club identity" %}{% endblock heading %}
{% block actions %}
<button class="btn btn-primary gap-2" type="submit" form="club-settings-form">{% lucide "save" size=16 %} {% trans "Save changes" %}</button>
{% endblock actions %}
{% block panel %}
<form id="club-settings-form" method="post" enctype="multipart/form-data">
{% csrf_token %}
{% for error in form.non_field_errors %}
<div class="alert alert-error">
{% lucide "circle-x" size=18 %}
<span>{{ error }}</span>
</div>
{% endfor %}
<div class="grid grid-cols-2 gap-5">
<div class="flex flex-col gap-5">
<div class="card">
<div class="card-body">
<div class="card-title">{% lucide "building-2" size=16 %} {% trans "Club" %}</div>
<div class="flex flex-col gap-4">
{% form_field form.name %}
{% form_field form.legal_name %}
{% form_field form.contact_email %}
</div>
</div>
</div>
<div class="card">
<div class="card-body">
<div class="card-title">{% lucide "image" size=16 %} {% trans "Logo" %}</div>
<div class="flex items-center gap-3.5">
{% if club.logo %}
<img class="h-14 w-14 shrink-0 rounded-lg border border-line bg-paper object-contain p-1" src="{{ club.logo.url }}" alt="">
{% else %}
<span class="flex h-14 w-14 shrink-0 items-center justify-center rounded-lg border border-line bg-paper font-display text-sm font-extrabold text-muted">{{ club.initials }}</span>
{% endif %}
<div class="flex-1">{% form_field form.logo %}</div>
</div>
</div>
</div>
</div>
<div class="card self-start">
<div class="card-body">
<div class="card-title">{% lucide "palette" size=16 %} {% trans "Colours" %}</div>
<div class="grid grid-cols-2 gap-4">
{% form_field form.primary_color %}
{% form_field form.secondary_color %}
</div>
<div class="mt-1 flex items-center gap-2">
<span class="h-7 w-7 rounded-md border border-line" style="background: {{ form.primary_color.value|default:'#fff' }}"></span>
<span class="h-7 w-7 rounded-md border border-line" style="background: {{ form.secondary_color.value|default:'#fff' }}"></span>
<span class="font-mono text-xs text-muted">{% trans "Current colours" %}</span>
</div>
</div>
</div>
</div>
</form>
{% endblock panel %}

View File

@@ -2,7 +2,13 @@
{% load i18n lucide %}
{% block heading %}{{ event.title }}{% endblock heading %}
{% block subheading %}{{ event.get_kind_display }}{% endblock subheading %}
{% block topbar_context %}
<span class="badge {% if event.kind == "game" %}badge-error{% elif event.kind == "training" %}badge-info{% elif event.kind == "tournament" %}badge-warning{% elif event.kind == "meeting" %}badge-neutral{% elif event.kind == "social" %}border-violet/30 bg-violet/10 text-violet{% else %}badge-outline{% endif %}">
{{ event.get_kind_display }}
</span>
{% if event.is_live %}<span class="badge badge-error gap-1 animate-pulse">{% lucide "circle" size=10 %} {% trans "Live" %}</span>{% endif %}
{% endblock topbar_context %}
{% block actions %}
{% if can_manage %}
@@ -22,7 +28,7 @@
{% block panel %}
{% if event.series_id %}
<div class="alert alert-info mb-6">
<div class="alert alert-info">
{% lucide "repeat" size=20 %}
<span>
{% blocktrans with series=event.series %}Part of the recurring series “{{ series }}”.{% endblocktrans %}
@@ -32,54 +38,54 @@
</div>
{% endif %}
<div class="mb-6 grid gap-4 lg:grid-cols-2">
<div class="card bg-base-100 shadow">
<div class="grid gap-4 lg:grid-cols-2">
<div class="card">
<div class="card-body">
<h2 class="card-title text-base">{% lucide "info" size=18 %} {% trans "Details" %}</h2>
<dl class="divide-y divide-base-200">
<div class="flex flex-col gap-0.5 py-2 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
<dt class="text-sm opacity-70">{% trans "Start" %}</dt>
<dd class="sm:text-right">{{ event.start|date:"j M Y H:i" }}</dd>
<dl>
<div class="flex items-center justify-between gap-4 py-2">
<dt class="text-sm text-muted">{% trans "Start" %}</dt>
<dd class="font-mono text-sm text-ink">{{ event.start|date:"j M Y H:i" }}</dd>
</div>
<div class="flex flex-col gap-0.5 py-2 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
<dt class="text-sm opacity-70">{% trans "End" %}</dt>
<dd class="sm:text-right">{{ event.end|date:"j M Y H:i"|default:"—" }}</dd>
<div class="flex items-center justify-between gap-4 py-2">
<dt class="text-sm text-muted">{% trans "End" %}</dt>
<dd class="font-mono text-sm text-ink">{{ event.end|date:"j M Y H:i"|default:"—" }}</dd>
</div>
<div class="flex flex-col gap-0.5 py-2 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
<dt class="text-sm opacity-70">{% trans "Gathering" %}</dt>
<dd class="sm:text-right">{{ event.gathering|date:"j M Y H:i"|default:"—" }}</dd>
<div class="flex items-center justify-between gap-4 py-2">
<dt class="text-sm text-muted">{% trans "Gathering" %}</dt>
<dd class="font-mono text-sm text-ink">{{ event.gathering|date:"j M Y H:i"|default:"—" }}</dd>
</div>
<div class="flex flex-col gap-0.5 py-2 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
<dt class="text-sm opacity-70">{% trans "Registration deadline" %}</dt>
<dd class="sm:text-right">{{ event.deadline|date:"j M Y H:i"|default:"—" }}</dd>
<div class="flex items-center justify-between gap-4 py-2">
<dt class="text-sm text-muted">{% trans "Registration deadline" %}</dt>
<dd class="font-mono text-sm text-ink">{{ event.deadline|date:"j M Y H:i"|default:"—" }}</dd>
</div>
<div class="flex flex-col gap-0.5 py-2 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
<dt class="text-sm opacity-70">{% trans "Location" %}</dt>
<dd class="sm:text-right">{{ event.location|default:"—" }}</dd>
<div class="flex items-center justify-between gap-4 py-2">
<dt class="text-sm text-muted">{% trans "Location" %}</dt>
<dd class="text-sm text-ink">{{ event.location|default:"—" }}</dd>
</div>
<div class="flex flex-col gap-0.5 py-2 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
<dt class="text-sm opacity-70">{% trans "Opponent" %}</dt>
<dd class="sm:text-right">{{ event.opponent|default:"—" }}</dd>
<div class="flex items-center justify-between gap-4 py-2">
<dt class="text-sm text-muted">{% trans "Opponent" %}</dt>
<dd class="text-sm text-ink">{{ event.opponent|default:"—" }}</dd>
</div>
<div class="flex flex-col gap-0.5 py-2 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
<dt class="text-sm opacity-70">{% trans "Teams" %}</dt>
<dd class="sm:text-right">{% for team in event.teams.all %}{{ team.name }}{% if not forloop.last %}, {% endif %}{% empty %}—{% endfor %}</dd>
<div class="flex items-center justify-between gap-4 py-2">
<dt class="text-sm text-muted">{% trans "Teams" %}</dt>
<dd class="text-sm text-ink">{% for team in event.teams.all %}{{ team.name }}{% if not forloop.last %}, {% endif %}{% empty %}—{% endfor %}</dd>
</div>
<div class="flex flex-col gap-0.5 py-2 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
<dt class="text-sm opacity-70">{% trans "Groups" %}</dt>
<dd class="sm:text-right">{% for group in event.groups.all %}{{ group.name }}{% if not forloop.last %}, {% endif %}{% empty %}—{% endfor %}</dd>
<div class="flex items-center justify-between gap-4 py-2">
<dt class="text-sm text-muted">{% trans "Groups" %}</dt>
<dd class="text-sm text-ink">{% for group in event.groups.all %}{{ group.name }}{% if not forloop.last %}, {% endif %}{% empty %}—{% endfor %}</dd>
</div>
{% if event.club_wide %}
<div class="flex flex-col gap-0.5 py-2 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
<dt class="text-sm opacity-70">{% trans "Audience" %}</dt>
<dd class="sm:text-right"><span class="badge badge-primary">{% trans "Whole club" %}</span></dd>
<div class="flex items-center justify-between gap-4 py-2">
<dt class="text-sm text-muted">{% trans "Audience" %}</dt>
<dd><span class="badge badge-neutral">{% trans "Whole club" %}</span></dd>
</div>
{% endif %}
</dl>
</div>
</div>
<div class="card bg-base-100 shadow">
<div class="card">
<div class="card-body">
<div class="flex flex-wrap items-center justify-between gap-2">
<h2 class="card-title text-base">{% lucide "user-check" size=18 %} {% trans "RSVPs" %}</h2>
@@ -89,29 +95,26 @@
</button>
{% endif %}
</div>
<dl class="divide-y divide-base-200">
<dl>
{% for group in attendance_groups %}
<div class="flex flex-col gap-0.5 py-2 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
<dt class="text-sm opacity-70">{{ group.label }}</dt>
<dd class="font-semibold tabular-nums font-mono sm:text-right">{{ group.rows|length }}</dd>
<div class="flex items-center justify-between gap-4 py-2">
<dt class="text-sm text-muted">{{ group.label }}</dt>
<dd class="font-mono text-sm font-semibold text-ink tabular-nums">{{ group.rows|length }}</dd>
</div>
{% endfor %}
</dl>
{% if not has_attendance_rows %}
<p class="text-sm opacity-60">{% trans "No one invited yet." %}</p>
<p class="text-sm text-muted">{% trans "No one invited yet." %}</p>
{% endif %}
</div>
</div>
</div>
{% if event.kind == "game" %}
<div class="mb-6 card bg-base-100 shadow">
<div class="card">
<div class="card-body">
<div class="flex flex-wrap items-center justify-between gap-2">
<h2 class="card-title text-base">
{% lucide "trophy" size=18 %} {% trans "Game" %}
{% if event.is_live %}<span class="badge badge-error gap-1 animate-pulse">{% lucide "circle" size=10 %} {% trans "Live" %}</span>{% endif %}
</h2>
<h2 class="card-title text-base">{% lucide "trophy" size=18 %} {% trans "Game" %}</h2>
{% if can_manage and event.competition %}
<form method="post" action="{% url 'management:event_fetch_game_info' event.pk %}">
{% csrf_token %}
@@ -119,20 +122,20 @@
</form>
{% endif %}
</div>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
<div class="grid grid-cols-3 gap-4">
<div>
<div class="text-sm opacity-70">{% trans "Score" %}</div>
<div class="text-sm font-semibold tabular-nums font-mono">
<div class="text-sm text-muted">{% trans "Score" %}</div>
<div class="font-mono text-sm font-semibold tabular-nums text-ink">
{% if event.score_for is not None and event.score_against is not None %}{{ event.score_for }} - {{ event.score_against }}{% else %}—{% endif %}
</div>
</div>
<div>
<div class="text-sm opacity-70">{% trans "Competition" %}</div>
<div class="text-sm">{{ event.competition|default:"—" }}</div>
<div class="text-sm text-muted">{% trans "Competition" %}</div>
<div class="text-sm text-ink">{{ event.competition|default:"—" }}</div>
</div>
<div>
<div class="text-sm opacity-70">{% trans "External game ID" %}</div>
<div class="font-mono text-sm">{{ event.external_game_id|default:"—" }}</div>
<div class="text-sm text-muted">{% trans "External game ID" %}</div>
<div class="font-mono text-sm text-ink">{{ event.external_game_id|default:"—" }}</div>
</div>
</div>
</div>
@@ -140,14 +143,14 @@
{% endif %}
{% if event.is_home_game and not referee_management_needed %}
<div class="alert alert-info mb-6">
<div class="alert alert-info">
{% 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">
<div class="card-body">
{% include "management/_referee_assignment_panel.html" %}
</div>
@@ -159,19 +162,19 @@
{% if can_manage %}
<dialog id="event_delete_modal" class="modal">
<div class="modal-box">
<h3 class="text-lg font-bold">
<h3 class="text-lg font-bold text-ink">
{% if event.series_id %}{% trans "Cancel occurrence" %}{% else %}{% trans "Delete event" %}{% endif %}
</h3>
<form method="post" action="{% url 'management:event_delete' event.pk %}" id="event_delete_form">
{% csrf_token %}
{% if event.series_id %}
<p class="py-2 text-sm opacity-70">{% trans "Removes this occurrence from the schedule. It won't be regenerated." %}</p>
<label class="label cursor-pointer justify-start gap-2">
<input type="checkbox" name="keep_record" class="checkbox">
<span class="label-text">{% trans "Keep a record of it (marks it cancelled instead of deleting it)" %}</span>
<p class="py-2 text-sm text-muted">{% trans "Removes this occurrence from the schedule. It won't be regenerated." %}</p>
<label class="flex cursor-pointer items-start gap-2 text-sm text-slate">
<input type="checkbox" name="keep_record" class="checkbox mt-0.5">
<span>{% trans "Keep a record of it (marks it cancelled instead of deleting it)" %}</span>
</label>
{% else %}
<p class="py-2 text-sm opacity-70">{% trans "This cannot be undone." %}</p>
<p class="py-2 text-sm text-muted">{% trans "This cannot be undone." %}</p>
{% endif %}
</form>
<div class="modal-action">
@@ -190,37 +193,35 @@
{% if has_attendance_rows %}
<dialog id="rsvp_modal" class="modal">
<div class="modal-box max-w-2xl">
<h3 class="text-lg font-bold">{% trans "Who responded" %}</h3>
<div class="mt-2 space-y-2">
<h3 class="text-lg font-bold text-ink">{% trans "Who responded" %}</h3>
<div class="mt-2 flex flex-col gap-2">
{% for group in attendance_groups %}
{% if group.rows %}
<details class="collapse collapse-arrow border border-base-300 bg-base-100">
<summary class="collapse-title text-sm font-medium">
<details class="collapse-arrow rounded-lg border border-line bg-white">
<summary class="flex cursor-pointer items-center gap-2 px-3 py-2 text-sm font-medium text-ink">
<span class="badge badge-sm
{% if group.value == "present" %}badge-success
{% elif group.value == "absent" %}badge-error
{% elif group.value == "excused" %}badge-warning
{% elif group.value == "selected" %}badge-info
{% elif group.value == "maybe" %}badge-warning badge-outline
{% elif group.value == "maybe" %}badge-warning
{% elif group.value == "not_selected" %}badge-neutral
{% else %}badge-outline{% endif %}">
{{ group.label }}
</span>
<span class="opacity-60">({{ group.rows|length }})</span>
<span class="text-muted">({{ group.rows|length }})</span>
</summary>
<div class="collapse-content">
<div class="overflow-x-auto">
<table class="table table-sm">
<tbody>
{% for row in group.rows %}
<tr>
<td>{{ row.member }}</td>
<td class="text-sm opacity-70">{{ row.note|default:"—" }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="overflow-x-auto border-t border-rule">
<table class="table">
<tbody>
{% for row in group.rows %}
<tr>
<td>{{ row.member }}</td>
<td class="text-sm text-muted">{{ row.note|default:"—" }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</details>
{% endif %}

View File

@@ -4,48 +4,48 @@
{% block heading %}{% if update_view %}{% blocktrans %}Edit {{ object }}{% endblocktrans %}{% else %}{% trans "New event" %}{% endif %}{% endblock heading %}
{% block panel %}
<div class="card w-full bg-base-100 shadow">
<div class="card w-full">
<div class="card-body">
<form method="post">
{% csrf_token %}
{% for error in form.non_field_errors %}
<div class="alert alert-error my-2">
<div class="alert alert-error">
<span>{{ error }}</span>
</div>
{% endfor %}
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
{% form_field form.title %}
{% form_field form.kind %}
</div>
<div class="divider"></div>
<h3 class="text-lg font-semibold mb-3">{% trans "Audience" %}</h3>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="my-5 border-t border-line"></div>
<h3 class="mb-3 font-display text-xs font-extrabold tracking-[.12em] text-muted uppercase">{% trans "Audience" %}</h3>
<div class="grid grid-cols-1 gap-4 md:grid-cols-3">
{% form_field form.teams %}
{% form_field form.groups %}
{% if "club_wide" in form.fields %}
{% form_field form.club_wide %}
{% endif %}
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
<div class="mt-4 grid grid-cols-1 gap-4 md:grid-cols-2">
{% form_field form.invited_members %}
{% form_field form.excluded_members %}
</div>
<div class="divider"></div>
<h3 class="text-lg font-semibold mb-3">{% trans "Where" %}</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="my-5 border-t border-line"></div>
<h3 class="mb-3 font-display text-xs font-extrabold tracking-[.12em] text-muted uppercase">{% trans "Where" %}</h3>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
{% form_field form.location %}
<div class="game-only">
{% form_field form.opponent %}
</div>
</div>
<div class="divider"></div>
<h3 class="text-lg font-semibold mb-3">{% trans "When" %}</h3>
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<div class="my-5 border-t border-line"></div>
<h3 class="mb-3 font-display text-xs font-extrabold tracking-[.12em] text-muted uppercase">{% trans "When" %}</h3>
<div class="grid grid-cols-1 gap-4 md:grid-cols-4">
{% form_field form.start %}
{% form_field form.end %}
{% form_field form.gathering %}
@@ -53,9 +53,9 @@
</div>
<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-3 gap-4">
<div class="my-5 border-t border-line"></div>
<h3 class="mb-3 font-display text-xs font-extrabold tracking-[.12em] text-muted uppercase">{% trans "Game" %}</h3>
<div class="grid grid-cols-1 gap-4 md:grid-cols-3">
{% form_field form.competition %}
{% form_field form.external_game_id %}
{% form_field form.max_referees %}
@@ -73,7 +73,7 @@
</div>
</div>
<div class="card-actions justify-start pt-2 mt-2">
<div class="card-actions justify-start pt-4">
<a class="btn btn-outline gap-2" href="{% if update_view %}{% url "management:event_detail" object.pk %}{% else %}{% url "management:event_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>

View File

@@ -9,74 +9,78 @@
{% endif %}
{% if can_create %}
<a class="btn btn-outline gap-2" href="{% url 'management:event_series_create' %}">{% lucide "repeat" size=16 %} {% trans "New series" %}</a>
<a class="btn btn-outline gap-2" href="{% url 'management:event_create' %}">{% lucide "plus" size=16 %} {% trans "New event" %}</a>
<a class="btn btn-primary gap-2" href="{% url 'management:event_create' %}">{% lucide "plus" size=16 %} {% trans "New event" %}</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>
<select name="kind" class="select select-bordered">
<option value="">{% trans "All kinds" %}</option>
{% for value, label in event_kinds %}
<option value="{{ value }}" {% if value == selected_kind %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
<label class="label cursor-pointer gap-2">
<input type="checkbox" name="show_past" value="1" class="checkbox" {% if show_past %}checked{% endif %} onchange="this.form.submit()">
<span class="label-text">{% trans "Show past events" %}</span>
</label>
<button class="btn btn-outline gap-2" type="submit">{% lucide "filter" size=16 %} {% trans "View" %}</button>
</div>
{% block filter_strip %}
<form method="get" class="flex flex-wrap items-center gap-2 border-b border-line bg-white px-7 py-3">
<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>
<select name="kind" class="select w-auto">
<option value="">{% trans "All kinds" %}</option>
{% for value, label in event_kinds %}
<option value="{{ value }}" {% if value == selected_kind %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
<label class="flex cursor-pointer items-center gap-2 text-sm text-slate">
<input type="checkbox" name="show_past" value="1" class="checkbox" {% if show_past %}checked{% endif %} onchange="this.form.submit()">
{% trans "Show past events" %}
</label>
<button class="btn btn-outline btn-sm gap-2" type="submit">{% lucide "filter" size=14 %} {% trans "View" %}</button>
</form>
{% endblock filter_strip %}
<div class="card bg-base-100 shadow">
<div class="card-body">
<div class="overflow-x-auto">
<table class="table table-cards">
<thead>
{% block panel %}
<div class="card overflow-hidden">
<div class="overflow-x-auto">
<table class="table">
<thead>
<tr>
<th>{% trans "Title" %}</th>
<th>{% trans "Kind" %}</th>
<th>{% trans "When" %}</th>
<th>{% trans "Teams" %}</th>
<th>{% trans "Location" %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for event in events %}
<tr>
<th>{% trans "Title" %}</th>
<th>{% trans "Kind" %}</th>
<th>{% trans "When" %}</th>
<th>{% trans "Teams" %}</th>
<th>{% trans "Location" %}</th>
<th></th>
<td>
<a class="link link-hover font-semibold text-ink" href="{% url 'management:event_detail' event.pk %}">{{ event.title }}</a>
{% if event.series_id %}
<span class="ml-1 inline-flex align-middle text-dim" title="{% trans 'Part of a series' %}" aria-label="{% trans 'Part of a series' %}">{% lucide "repeat" size=12 %}</span>
{% endif %}
</td>
<td>
<span class="badge badge-sm {% if event.kind == "game" %}badge-error{% elif event.kind == "training" %}badge-info{% elif event.kind == "tournament" %}badge-warning{% elif event.kind == "meeting" %}badge-neutral{% elif event.kind == "social" %}border-violet/30 bg-violet/10 text-violet{% else %}badge-outline{% endif %}">
{{ event.get_kind_display }}
</span>
</td>
<td class="font-mono text-xs text-ink">{{ event.start|date:"D j M Y H:i" }}</td>
<td>{% for team in event.teams.all %}{{ team.short_name }}{% if not forloop.last %}, {% endif %}{% empty %}<span class="text-dim"></span>{% endfor %}</td>
<td>{{ event.location.name|default:"—" }}</td>
<td class="text-right">
{% if event.can_manage %}
<div class="flex justify-end gap-1">
<a class="btn btn-outline btn-sm" href="{% url 'management:event_detail' event.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('{{ event.pk|dom_id:"event_delete_modal" }}').showModal()" aria-label="{% trans 'Delete' %}">{% lucide "trash-2" size=14 %} {% trans "Delete" %}</button>
</div>
{% endif %}
</td>
</tr>
</thead>
<tbody>
{% for event in events %}
<tr>
<td class="flex flex-row items-center gap-2 font-semibold">
<a class="link link-hover" href="{% url 'management:event_detail' event.pk %}">{{ event.title }}</a>
{% if event.series_id %}<span class="tooltip" data-tip="{% trans 'Part of a series' %}">{% lucide "repeat" size=12 %}</span>{% endif %}
</td>
<td data-label="{% trans 'Kind' %}">{{ event.get_kind_display }}</td>
<td data-label="{% trans 'When' %}">{{ event.start|date:"j M Y H:i" }}</td>
<td data-label="{% trans 'Teams' %}">{% for team in event.teams.all %}{{ team.short_name }}{% if not forloop.last %}, {% endif %}{% empty %}—{% endfor %}</td>
<td data-label="{% trans 'Location' %}">{{ event.location.name|default:"—" }}</td>
<td class="text-right">
{% if event.can_manage %}
<div class="flex justify-end gap-1">
<a class="btn btn-outline btn-sm" href="{% url 'management:event_detail' event.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('{{ event.pk|dom_id:"event_delete_modal" }}').showModal()" aria-label="{% trans 'Delete' %}">{% lucide "trash-2" size=14 %} {% trans "Delete" %}</button>
</div>
{% endif %}
</td>
</tr>
{% empty %}
<tr>
<td colspan="6" class="text-center opacity-60">{% trans "No events." %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% empty %}
<tr>
<td colspan="6" class="py-8 text-center text-muted">{% trans "No events." %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
@@ -87,21 +91,21 @@
{% if event.can_manage %}
<dialog id="{{ event.pk|dom_id:"event_delete_modal" }}" class="modal">
<div class="modal-box">
<h3 class="text-lg font-bold">
<h3 class="text-lg font-bold text-ink">
{% if event.series_id %}{% trans "Cancel occurrence" %}{% else %}{% trans "Delete event" %}{% endif %}
</h3>
<form method="post" action="{% url 'management:event_delete' event.pk %}" id="{{ event.pk|dom_id:"event_delete_form" }}">
{% csrf_token %}
{% if event.series_id %}
{% blocktrans with name=event.title asvar delete_body %}Removes “{{ name }}” from the schedule. It won't be regenerated.{% endblocktrans %}
<p class="py-2 text-sm opacity-70">{{ delete_body }}</p>
<label class="label cursor-pointer justify-start gap-2">
<input type="checkbox" name="keep_record" class="checkbox">
<span class="label-text">{% trans "Keep a record of it (marks it cancelled instead of deleting it)" %}</span>
<p class="py-2 text-sm text-muted">{{ delete_body }}</p>
<label class="flex cursor-pointer items-start gap-2 text-sm text-slate">
<input type="checkbox" name="keep_record" class="checkbox mt-0.5">
<span>{% trans "Keep a record of it (marks it cancelled instead of deleting it)" %}</span>
</label>
{% else %}
{% blocktrans with name=event.title asvar delete_body %}Delete “{{ name }}”? This cannot be undone.{% endblocktrans %}
<p class="py-2 text-sm opacity-70">{{ delete_body }}</p>
<p class="py-2 text-sm text-muted">{{ delete_body }}</p>
{% endif %}
</form>
<div class="modal-action">

View File

@@ -2,7 +2,13 @@
{% load i18n lucide ui %}
{% block heading %}{{ series.title }}{% endblock heading %}
{% block subheading %}{{ series.get_kind_display }} · {{ recurrence_summary }}{% endblock subheading %}
{% block topbar_context %}
<span class="badge {% if series.kind == "game" %}badge-error{% elif series.kind == "training" %}badge-info{% elif series.kind == "tournament" %}badge-warning{% elif series.kind == "meeting" %}badge-neutral{% elif series.kind == "social" %}border-violet/30 bg-violet/10 text-violet{% else %}badge-outline{% endif %}">
{{ series.get_kind_display }}
</span>
<span class="font-mono text-xs text-muted">{{ recurrence_summary }}</span>
{% endblock topbar_context %}
{% block actions %}
{% if can_manage %}
@@ -17,57 +23,57 @@
{% endblock actions %}
{% block panel %}
<div class="mb-6 grid gap-4 lg:grid-cols-2">
<div class="card bg-base-100 shadow">
<div class="grid gap-4 lg:grid-cols-2">
<div class="card">
<div class="card-body">
<h2 class="card-title text-base">{% lucide "repeat" size=18 %} {% trans "Recurrence" %}</h2>
<dl class="divide-y divide-base-200">
<div class="flex flex-col gap-0.5 py-2 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
<dt class="text-sm opacity-70">{% trans "Pattern" %}</dt>
<dd class="sm:text-right">{{ recurrence_summary }}</dd>
<dl>
<div class="flex items-center justify-between gap-4 py-2">
<dt class="text-sm text-muted">{% trans "Pattern" %}</dt>
<dd class="text-sm text-ink">{{ recurrence_summary }}</dd>
</div>
<div class="flex flex-col gap-0.5 py-2 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
<dt class="text-sm opacity-70">{% trans "First occurrence" %}</dt>
<dd class="sm:text-right">{{ series.dtstart|date:"j M Y H:i" }}</dd>
<div class="flex items-center justify-between gap-4 py-2">
<dt class="text-sm text-muted">{% trans "First occurrence" %}</dt>
<dd class="font-mono text-sm text-ink">{{ series.dtstart|date:"j M Y H:i" }}</dd>
</div>
<div class="flex flex-col gap-0.5 py-2 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
<dt class="text-sm opacity-70">{% trans "Repeats until" %}</dt>
<dd class="sm:text-right">{{ series.until|date:"j M Y H:i"|default:"—" }}</dd>
<div class="flex items-center justify-between gap-4 py-2">
<dt class="text-sm text-muted">{% trans "Repeats until" %}</dt>
<dd class="font-mono text-sm text-ink">{{ series.until|date:"j M Y H:i"|default:"—" }}</dd>
</div>
<div class="flex flex-col gap-0.5 py-2 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
<dt class="text-sm opacity-70">{% trans "Generated up to" %}</dt>
<dd class="sm:text-right">{{ series.generated_until|date:"j M Y"|default:"—" }}</dd>
<div class="flex items-center justify-between gap-4 py-2">
<dt class="text-sm text-muted">{% trans "Generated up to" %}</dt>
<dd class="font-mono text-sm text-ink">{{ series.generated_until|date:"j M Y"|default:"—" }}</dd>
</div>
</dl>
</div>
</div>
<div class="card bg-base-100 shadow">
<div class="card">
<div class="card-body">
<h2 class="card-title text-base">{% lucide "info" size=18 %} {% trans "Template" %}</h2>
<dl class="divide-y divide-base-200">
<div class="flex flex-col gap-0.5 py-2 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
<dt class="text-sm opacity-70">{% trans "Location" %}</dt>
<dd class="sm:text-right">{{ series.location|default:"—" }}</dd>
<dl>
<div class="flex items-center justify-between gap-4 py-2">
<dt class="text-sm text-muted">{% trans "Location" %}</dt>
<dd class="text-sm text-ink">{{ series.location|default:"—" }}</dd>
</div>
<div class="flex flex-col gap-0.5 py-2 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
<dt class="text-sm opacity-70">{% trans "Opponent" %}</dt>
<dd class="sm:text-right">{{ series.opponent|default:"—" }}</dd>
<div class="flex items-center justify-between gap-4 py-2">
<dt class="text-sm text-muted">{% trans "Opponent" %}</dt>
<dd class="text-sm text-ink">{{ series.opponent|default:"—" }}</dd>
</div>
<div class="flex flex-col gap-0.5 py-2 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
<dt class="text-sm opacity-70">{% trans "Teams" %}</dt>
<dd class="sm:text-right">{% for team in series.teams.all %}{{ team.name }}{% if not forloop.last %}, {% endif %}{% empty %}—{% endfor %}</dd>
<div class="flex items-center justify-between gap-4 py-2">
<dt class="text-sm text-muted">{% trans "Teams" %}</dt>
<dd class="text-sm text-ink">{% for team in series.teams.all %}{{ team.name }}{% if not forloop.last %}, {% endif %}{% empty %}—{% endfor %}</dd>
</div>
</dl>
</div>
</div>
</div>
<div class="card bg-base-100 shadow">
<div class="card">
<div class="card-body">
<h2 class="card-title text-base">{% lucide "calendar" size=18 %} {% trans "Occurrences" %}</h2>
<div class="overflow-x-auto">
<table class="table table-cards">
<table class="table">
<thead>
<tr>
<th>{% trans "When" %}</th>
@@ -77,8 +83,8 @@
<tbody>
{% for occurrence in occurrences %}
<tr>
<td class="font-semibold"><a class="link link-hover" href="{% url 'management:event_detail' occurrence.pk %}">{{ occurrence.start|date:"j M Y H:i" }}</a></td>
<td data-label="{% trans 'Status' %}">
<td class="font-mono text-sm font-semibold"><a class="link link-hover" href="{% url 'management:event_detail' occurrence.pk %}">{{ occurrence.start|date:"j M Y H:i" }}</a></td>
<td>
{% if occurrence.cancelled %}
<span class="badge badge-outline gap-1">{% lucide "ban" size=12 %} {% trans "Cancelled" %}</span>
{% elif occurrence.detached %}
@@ -92,7 +98,7 @@
</tr>
{% empty %}
<tr>
<td colspan="2" class="text-center opacity-60">{% trans "No occurrences generated yet." %}</td>
<td colspan="2" class="py-8 text-center text-muted">{% trans "No occurrences generated yet." %}</td>
</tr>
{% endfor %}
</tbody>

View File

@@ -4,25 +4,25 @@
{% block heading %}{% if update_view %}{% blocktrans %}Edit {{ object }}{% endblocktrans %}{% else %}{% trans "New series" %}{% endif %}{% endblock heading %}
{% block panel %}
<div class="card w-full bg-base-100 shadow">
<div class="card w-full">
<div class="card-body">
<form method="post">
{% csrf_token %}
{% for error in form.non_field_errors %}
<div class="alert alert-error my-2">
<div class="alert alert-error">
<span>{{ error }}</span>
</div>
{% endfor %}
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
{% form_field form.title %}
{% form_field form.kind %}
</div>
<div class="divider"></div>
<h3 class="text-lg font-semibold mb-3">{% trans "Repeats" %}</h3>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="my-5 border-t border-line"></div>
<h3 class="mb-3 font-display text-xs font-extrabold tracking-[.12em] text-muted uppercase">{% trans "Repeats" %}</h3>
<div class="grid grid-cols-1 gap-4 md:grid-cols-3">
{% form_field form.frequency %}
{% form_field form.interval %}
{% form_field form.dtstart %}
@@ -30,44 +30,44 @@
</div>
{% trans "Only used for a weekly pattern -- a monthly one repeats on the same day of the month as the first occurrence above." as weekdays_help %}
{% form_field form.weekdays help_text=weekdays_help %}
<details class="collapse collapse-arrow bg-base-200 mt-2">
<summary class="collapse-title text-sm font-medium">{% trans "Advanced: raw recurrence rule" %}</summary>
<div class="collapse-content">
<details class="collapse-arrow mt-3 rounded-lg border border-line bg-white">
<summary class="cursor-pointer px-3 py-2 text-sm font-medium text-ink">{% trans "Advanced: raw recurrence rule" %}</summary>
<div class="border-t border-rule p-3">
{% form_field form.advanced_rrule %}
</div>
</details>
<div class="divider"></div>
<h3 class="text-lg font-semibold mb-3">{% trans "Audience" %}</h3>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="my-5 border-t border-line"></div>
<h3 class="mb-3 font-display text-xs font-extrabold tracking-[.12em] text-muted uppercase">{% trans "Audience" %}</h3>
<div class="grid grid-cols-1 gap-4 md:grid-cols-3">
{% form_field form.teams %}
{% form_field form.groups %}
{% if "club_wide" in form.fields %}
{% form_field form.club_wide %}
{% endif %}
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
<div class="mt-4 grid grid-cols-1 gap-4 md:grid-cols-2">
{% form_field form.invited_members %}
{% form_field form.excluded_members %}
</div>
<div class="divider"></div>
<h3 class="text-lg font-semibold mb-3">{% trans "Where" %}</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="my-5 border-t border-line"></div>
<h3 class="mb-3 font-display text-xs font-extrabold tracking-[.12em] text-muted uppercase">{% trans "Where" %}</h3>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
{% form_field form.location %}
{% form_field form.opponent %}
</div>
<div class="divider"></div>
<h3 class="text-lg font-semibold mb-3">{% trans "Timing" %}</h3>
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<div class="my-5 border-t border-line"></div>
<h3 class="mb-3 font-display text-xs font-extrabold tracking-[.12em] text-muted uppercase">{% trans "Timing" %}</h3>
<div class="grid grid-cols-1 gap-4 md:grid-cols-4">
{% form_field form.duration_hours %}
{% form_field form.duration_minutes %}
{% form_field form.gathering_minutes_before %}
{% form_field form.deadline_minutes_before %}
</div>
<div class="card-actions justify-start pt-2 mt-2">
<div class="card-actions justify-start pt-4">
<a class="btn btn-outline gap-2" href="{% if update_view %}{% url "management:event_series_detail" object.pk %}{% else %}{% url "management:event_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>

View File

@@ -4,28 +4,26 @@
{% block heading %}{% blocktrans with name=family %}{{ name }} Family{% endblocktrans %}{% endblock heading %}
{% block actions %}
{% if is_club_admin %}
{% trans "Add parent" as add_parent_label %}
{% trans "Add child" as add_child_label %}
<button class="btn btn-outline gap-2" type="button" onclick="document.getElementById('add_parent_modal').showModal()">{% lucide "user-plus" size=16 %} {{ add_parent_label }}</button>
<button class="btn btn-outline gap-2" type="button" onclick="document.getElementById('add_child_modal').showModal()">{% lucide "baby" size=16 %} {{ add_child_label }}</button>
{% endif %}
{% if is_club_admin %}
{% trans "Add parent" as add_parent_label %}
{% trans "Add child" as add_child_label %}
<button class="btn btn-outline gap-2" type="button" onclick="document.getElementById('add_parent_modal').showModal()">{% lucide "user-plus" size=16 %} {{ add_parent_label }}</button>
<button class="btn btn-primary gap-2" type="button" onclick="document.getElementById('add_child_modal').showModal()">{% lucide "baby" size=16 %} {{ add_child_label }}</button>
{% endif %}
{% endblock actions %}
{% block panel %}
<div class="card bg-base-100 shadow">
<div class="card-body">
<div class="card card-body">
{% include "management/_family_members_table.html" with group=group %}
</div>
</div>
{% if is_club_admin %}
{% url 'management:family_add_parent' family.pk as add_parent_url %}
{% url 'management:family_add_child' family.pk as add_child_url %}
{% trans "Add parent" as add_parent_title %}
{% trans "Add child" as add_child_title %}
{% trans "If this email has no account yet, one is created and they set a password via the reset link." as add_parent_blurb %}
{% include "controlpanel/_modal_form.html" with modal_id="add_parent_modal" title=add_parent_title form=add_parent_form action_url=add_parent_url submit_label=add_parent_title submit_icon="user-plus" blurb=add_parent_blurb %}
{% include "controlpanel/_modal_form.html" with modal_id="add_child_modal" title=add_child_title form=add_child_form action_url=add_child_url submit_label=add_child_title submit_icon="baby" %}
{% endif %}
{% if is_club_admin %}
{% url 'management:family_add_parent' family.pk as add_parent_url %}
{% url 'management:family_add_child' family.pk as add_child_url %}
{% trans "Add parent" as add_parent_title %}
{% trans "Add child" as add_child_title %}
{% trans "If this email has no account yet, one is created and they set a password via the reset link." as add_parent_blurb %}
{% include "controlpanel/_modal_form.html" with modal_id="add_parent_modal" title=add_parent_title form=add_parent_form action_url=add_parent_url submit_label=add_parent_title submit_icon="user-plus" blurb=add_parent_blurb %}
{% include "controlpanel/_modal_form.html" with modal_id="add_child_modal" title=add_child_title form=add_child_form action_url=add_child_url submit_label=add_child_title submit_icon="baby" %}
{% endif %}
{% endblock panel %}

View File

@@ -4,41 +4,39 @@
{% block heading %}{% trans "Add family" %}{% endblock heading %}
{% block panel %}
<div class="card w-full bg-base-100 shadow">
<div class="card-body">
<form method="post">
{% csrf_token %}
<div class="card 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 %}
<h2 class="card-title text-base">{% lucide "user" size=18 %} {% trans "Parent / guardian" %}</h2>
<p class="text-sm opacity-70">{% trans "Gets a login -- they set a password via the reset link if this is a new email." %}</p>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
{% form_field form.parent_first_name %}
{% form_field form.parent_last_name %}
{% form_field form.parent_email %}
{% form_field form.parent_is_member %}
{% for error in form.non_field_errors %}
<div class="alert alert-error my-2">
<span>{{ error }}</span>
</div>
{% endfor %}
<div class="divider"></div>
<h2 class="card-title">{% lucide "user" size=18 %} {% trans "Parent / guardian" %}</h2>
<p class="text-sm text-muted">{% trans "Gets a login -- they set a password via the reset link if this is a new email." %}</p>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
{% form_field form.parent_first_name %}
{% form_field form.parent_last_name %}
{% form_field form.parent_email %}
{% form_field form.parent_is_member %}
</div>
<h2 class="card-title text-base">{% lucide "baby" size=18 %} {% trans "Child" %}</h2>
<p class="text-sm opacity-70">{% trans "No login of their own -- the parent above manages things for them." %}</p>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
{% form_field form.child_first_name %}
{% form_field form.child_last_name %}
{% form_field form.child_date_of_birth %}
</div>
<div class="my-2 h-px bg-rule"></div>
<div class="card-actions justify-start pt-2 mt-2">
<a class="btn btn-outline gap-2" href="{% url "management:member_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>
<h2 class="card-title">{% lucide "baby" size=18 %} {% trans "Child" %}</h2>
<p class="text-sm text-muted">{% trans "No login of their own -- the parent above manages things for them." %}</p>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
{% form_field form.child_first_name %}
{% form_field form.child_last_name %}
{% form_field form.child_date_of_birth %}
</div>
<div class="mt-2 flex items-center gap-2 pt-2">
<a class="btn btn-outline gap-2" href="{% url "management:member_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>
{% endblock panel %}

View File

@@ -1,72 +1,75 @@
{% extends "management/base.html" %}
{% load i18n lucide %}
{% block heading %}{% trans "Families" %}{% endblock heading %}
{% block subheading %}{% trans "Every household on file -- who their parents/guardians and children are." %}{% endblock subheading %}
{% block heading %}{% trans "Households" %}{% endblock heading %}
{% block topbar_context %}
<span class="font-mono text-[13px] text-muted">{% trans "Every household on file." %}</span>
{% endblock topbar_context %}
{% block actions %}
{% if is_club_admin %}
<a class="btn btn-outline gap-2" href="{% url 'management:family_create' %}">{% lucide "users" size=16 %} {% trans "Add family" %}</a>
<a class="btn btn-primary gap-2" href="{% url 'management:family_create' %}">{% lucide "users" size=16 %} {% trans "Add family" %}</a>
{% endif %}
{% endblock actions %}
{% block panel %}
<form method="get" class="mb-2 flex items-center gap-2">
<label class="input grow sm:grow-0">
<span class="opacity-50">{% lucide "search" size=16 %}</span>
<input type="search" name="q" value="{{ search }}" placeholder="{% trans 'Search by parent or child name ...' %}" class="input input-bordered w-full sm:max-w-xs">
</label>
{# Icon-only below `sm`: label text next to a growing input is what forced this row to wrap on a phone. #}
<button class="btn btn-outline gap-2" type="submit" aria-label="{% trans 'Search' %}">{% lucide "search" size=16 %}<span class="hidden sm:inline">{% trans "Search" %}</span></button>
{% if search %}
<a class="btn gap-2" href="{% url "management:family_list" %}" aria-label="{% trans 'Clear filter' %}">{% lucide "x" size=16 %}<span class="hidden sm:inline">{% trans "Clear filter" %}</span></a>
{% endif %}
</form>
<div class="card bg-base-100 shadow">
<div class="card-body">
<div class="overflow-x-auto">
<table class="table table-cards">
<thead>
<tr>
<th>{% trans "Family" %}</th>
<th>{% trans "Parents / guardians" %}</th>
<th>{% trans "Children" %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for family in families %}
<tr>
<td><a class="link link-hover font-semibold" href="{% url 'management:family_detail' family.pk %}">{{ family }}</a></td>
<td data-label="{% trans 'Parents / guardians' %}">
{% for guardian in family.guardians_display %}
<a class="link link-hover" href="{% url 'management:member_detail' guardian.pk %}">{{ guardian }}</a>{% if not forloop.last %}, {% endif %}
{% empty %}
<span class="opacity-40">-</span>
{% endfor %}
</td>
<td data-label="{% trans 'Children' %}">
{% for child in family.children_display %}
<a class="link link-hover" href="{% url 'management:member_detail' child.pk %}">{{ child }}</a>{% if not forloop.last %}, {% endif %}
{% empty %}
<span class="opacity-40">-</span>
{% endfor %}
</td>
<td class="text-right">
{# Same convention as Groups: "Edit" lands on the overview page, not a standalone rename form -- family_detail is where every family action (add parent/child, change role, remove) actually lives. #}
<a class="btn btn-outline btn-sm" href="{% url 'management:family_detail' family.pk %}" aria-label="{% trans 'Edit' %}">{% lucide "pencil" size=14 %} {% trans "Edit" %}</a>
</td>
</tr>
{% empty %}
<tr>
<td colspan="4" class="text-center opacity-60">{% trans "No families yet." %}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% block filter_strip %}
<div class="flex h-[54px] shrink-0 items-center gap-2 border-b border-line bg-white px-7">
<form method="get" class="flex items-center gap-2">
<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>
<input type="search" name="q" value="{{ search }}" placeholder="{% trans 'Search by parent or child name ...' %}" class="input w-72 pl-9">
</div>
</div>
{# No btn-sm: .input 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 "search" size=14 %} {% trans "Search" %}</button>
{% if search %}
<a class="btn btn-outline gap-2" href="{% url "management:family_list" %}">{% lucide "x" size=14 %} {% trans "Clear" %}</a>
{% endif %}
</form>
</div>
{% endblock filter_strip %}
{% block panel %}
<div class="card overflow-hidden">
<table class="table">
<thead>
<tr>
<th>{% trans "Family" %}</th>
<th>{% trans "Parents / guardians" %}</th>
<th>{% trans "Children" %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for family in families %}
<tr>
<td><a class="link link-hover font-semibold text-ink" href="{% url 'management:family_detail' family.pk %}">{{ family }}</a></td>
<td class="text-sm">
{% for guardian in family.guardians_display %}
<a class="link link-hover" href="{% url 'management:member_detail' guardian.pk %}">{{ guardian }}</a>{% if not forloop.last %}, {% endif %}
{% empty %}
<span class="text-dim"></span>
{% endfor %}
</td>
<td class="text-sm">
{% for child in family.children_display %}
<a class="link link-hover" href="{% url 'management:member_detail' child.pk %}">{{ child }}</a>{% if not forloop.last %}, {% endif %}
{% empty %}
<span class="text-dim"></span>
{% endfor %}
</td>
<td class="text-right">
{# Same convention as Groups: "Edit" lands on the overview page, not a standalone rename form -- family_detail is where every family action (add parent/child, change role, remove) actually lives. #}
<a class="btn btn-outline btn-xs" href="{% url 'management:family_detail' family.pk %}" aria-label="{% trans 'Edit' %}">{% lucide "pencil" size=13 %} {% trans "Edit" %}</a>
</td>
</tr>
{% empty %}
<tr>
<td colspan="4" class="py-6 text-center text-muted">{% trans "No families yet." %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% include "management/_pagination.html" %}

View File

@@ -2,51 +2,49 @@
{% load i18n lucide static %}
{% block heading %}{% trans "Add multiple members" %}{% endblock heading %}
{% block subheading %}{{ group.name }}{% endblock subheading %}
{% block topbar_context %}
<span class="font-mono text-[13px] text-muted">{{ group.name }}</span>
{% endblock topbar_context %}
{% block panel %}
<form method="post">
{% csrf_token %}
{{ formset.management_form }}
<div class="card bg-base-100 shadow">
<div class="card-body">
{% for error in formset.non_form_errors %}
<div class="alert alert-error my-2">
<span>{{ error }}</span>
</div>
{% endfor %}
{% comment %}
overflow-x-auto is safe here: the member picker's dropdown
(searchable-select.js) is position: fixed, computed from the input's
own screen position rather than document flow, so a scrolling
ancestor around the table no longer clips it.
{% endcomment %}
<div class="overflow-x-auto">
<table class="table w-full">
<thead>
<tr>
<th>{% trans "Member" %}</th>
<th></th>
</tr>
</thead>
<tbody id="bulk-add-rows" data-prefix="{{ formset.prefix }}">
{% for form in formset %}
{% include "management/_group_bulk_add_row.html" with form=form %}
{% endfor %}
</tbody>
</table>
<div class="card card-body">
{% for error in formset.non_form_errors %}
<div class="alert alert-error my-2">
<span>{{ error }}</span>
</div>
{% endfor %}
<template id="bulk-add-row-template">
{% include "management/_group_bulk_add_row.html" with form=formset.empty_form %}
</template>
{% comment %}
The member picker's dropdown (searchable-select.js) is position: fixed,
computed from the input's own screen position rather than document flow,
so no ancestor scroll wrapper is needed to keep it from being clipped.
{% endcomment %}
<table class="table w-full">
<thead>
<tr>
<th>{% trans "Member" %}</th>
<th></th>
</tr>
</thead>
<tbody id="bulk-add-rows" data-prefix="{{ formset.prefix }}">
{% for form in formset %}
{% include "management/_group_bulk_add_row.html" with form=form %}
{% endfor %}
</tbody>
</table>
<div class="flex flex-wrap items-center gap-2 mt-2">
<button class="btn btn-outline btn-sm gap-2" type="button" id="add-row">{% lucide "plus" size=14 %} {% trans "Add another row" %}</button>
<button class="btn btn-primary btn-sm gap-2" type="submit">{% lucide "users" size=14 %} {% trans "Add to group" %}</button>
</div>
<template id="bulk-add-row-template">
{% include "management/_group_bulk_add_row.html" with form=formset.empty_form %}
</template>
<div class="mt-2 flex flex-wrap items-center gap-2">
<button class="btn btn-outline btn-sm gap-2" type="button" id="add-row">{% lucide "plus" size=14 %} {% trans "Add another row" %}</button>
<button class="btn btn-primary btn-sm gap-2" type="submit">{% lucide "users" size=14 %} {% trans "Add to group" %}</button>
</div>
</div>

View File

@@ -8,37 +8,33 @@
{% endblock actions %}
{% block panel %}
<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 "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 table-cards">
<thead>
<tr>
<th>{% trans "Member" %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for membership in memberships %}
<tr>
<td class="font-semibold"><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 class="card card-body">
<div class="flex flex-wrap items-center justify-between gap-2">
<h2 class="card-title">{% 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>
<table class="table">
<thead>
<tr>
<th>{% trans "Member" %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for membership in memberships %}
<tr>
<td class="font-semibold text-ink"><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-xs btn-outline btn-error gap-1" type="button" onclick="document.getElementById('{{ membership.pk|dom_id:"remove_member_modal" }}').showModal()">{% lucide "user-minus" size=13 %} {% trans "Remove" %}</button>
</td>
</tr>
{% empty %}
<tr>
<td colspan="2" class="py-6 text-center text-muted">{% trans "No members yet." %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% trans "Remove from group" as remove_member_title %}

View File

@@ -4,29 +4,27 @@
{% 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 %}
<div class="card 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>
{% 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 gap-4 md:grid-cols-2">
{% for field in form %}
{% form_field field %}
{% endfor %}
</div>
<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 class="mt-2 flex items-center gap-2 pt-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>
{% endblock panel %}

View File

@@ -2,45 +2,44 @@
{% 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 topbar_context %}
<span class="font-mono text-[13px] text-muted">{% trans "Named collections of members -- coaches, team managers, referee pools, committees..." %}</span>
{% endblock topbar_context %}
{% block actions %}
<a class="btn btn-outline gap-2" href="{% url 'management:group_create' %}">{% lucide "plus" size=16 %} {% trans "New group" %}</a>
<a class="btn btn-primary 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 table-cards">
<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 font-semibold" href="{% url 'management:group_detail' group.pk %}">{{ group.name }}</a></td>
<td data-label="{% trans 'Members' %}">{{ 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_detail' 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 class="card overflow-hidden">
<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 font-semibold text-ink" href="{% url 'management:group_detail' group.pk %}">{{ group.name }}</a></td>
<td class="font-mono text-sm">{{ group.member_count }}</td>
<td class="text-right">
<div class="flex justify-end gap-1">
<a class="btn btn-outline btn-xs" href="{% url 'management:group_detail' group.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('{{ group.pk|dom_id:"group_delete_modal" }}').showModal()" aria-label="{% trans 'Delete' %}">{% lucide "trash-2" size=13 %} {% trans "Delete" %}</button>
</div>
</td>
</tr>
{% empty %}
<tr>
<td colspan="3" class="py-6 text-center text-muted">{% trans "No groups yet." %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% include "management/_pagination.html" %}

View File

@@ -1,24 +1,32 @@
{% extends "management/base.html" %}
{% load i18n static lucide %}
{% block panel_title %}Overview{% endblock panel_title %}
{% block heading %}{{ club.name }}{% endblock heading %}
{% block subheading %}{% trans "Management" %}{% endblock subheading %}
{% block topbar_context %}
{% if not attention.no_season %}
<span class="badge badge-success">{% blocktrans with start=attention.season.start_date|date:"Y" end=attention.season.end_date|date:"Y" %}Season {{ start }}-{{ end }}{% endblocktrans %}</span>
{% endif %}
{% endblock topbar_context %}
{% block actions %}
<a class="btn btn-primary gap-2" href="{% url 'management:member_create' %}">{% lucide "plus" size=16 %} {% trans "New member" %}</a>
{% endblock actions %}
{% block panel %}
{# Every level here; base.html repeats it on other pages only once it turns urgent. #}
{% include "management/_billing_notice.html" %}
{% if attention.no_season %}
<div class="alert alert-warning mb-6">
<div class="alert alert-warning">
{% lucide "calendar-x" size=20 %}
<span>
{% trans "No season covers today, so this club cannot take a signup or schedule a match. Nothing errors — it is simply inert." %}
</span>
<span>{% trans "No season covers today, so this club cannot take a signup or schedule a match. Nothing errors — it is simply inert." %}</span>
</div>
{% endif %}
{% if is_club_admin and billing_ends_at %}
<div class="alert alert-warning mb-6">
<div class="alert alert-warning">
{% lucide "calendar-clock" size=20 %}
<span>
{% if billing_auto_renews %}
@@ -30,167 +38,198 @@
</div>
{% endif %}
{% comment %}
The club's own numbers that should be zero -- same attention/chart/stat-group
data controlpanel/club_detail.html shows a platform admin drilling into this
club from outside; club_attention/club_charts/club_statistics are already
club-scoped, so this is that same data for the club's own staff. Nothing
money-shaped renders below for non-admins, same line the nav already draws
around the Shop section.
{% endcomment %}
<div class="mb-6 grid gap-4 sm:grid-cols-2 md:grid-cols-5">
<div class="card bg-base-100 shadow border-l-4 {% if attention.teams_without_manager %}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 "user-x" size=16 %} {% trans "Teams without coach" %}</div>
<div class="text-4xl font-bold tabular-nums font-mono">{{ attention.teams_without_manager }}</div>
<div class="text-xs opacity-60">{% trans "Teams nobody can pick a squad for" %}</div>
<div class="flex gap-3.5">
<div class="card min-w-0 flex-1 p-4">
<div class="font-display text-xs font-bold tracking-[.12em] text-muted uppercase">{% trans "Total members" %}</div>
<div class="mt-1 font-display text-[34px] leading-none font-extrabold text-ink tabular-nums">{{ member_count }}</div>
<div class="mt-1 text-[13px] {% if member_count_change > 0 %}text-ok{% elif member_count_change < 0 %}text-club{% else %}text-muted{% endif %}">
{% if member_count_change is None %}
{% trans "No previous season to compare" %}
{% else %}
{% if member_count_change > 0 %}+{% endif %}{{ member_count_change }} {% trans "vs last season" %}
{% endif %}
</div>
</div>
<div class="card bg-base-100 shadow border-l-4 {% if attention.unrostered %}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-minus" size=16 %} {% trans "Unrostered members" %}</div>
<div class="text-4xl font-bold tabular-nums font-mono">{{ attention.unrostered }}</div>
<div class="text-xs opacity-60">{% trans "Active members on no team" %}</div>
</div>
<div class="card min-w-0 flex-1 p-4">
<div class="font-display text-xs font-bold tracking-[.12em] text-muted uppercase">{% trans "Awaiting approval" %}</div>
<div class="mt-1 font-display text-[34px] leading-none font-extrabold tabular-nums {% if attention.pending_approvals %}text-club{% else %}text-ink{% endif %}">{{ attention.pending_approvals }}</div>
<div class="mt-1 text-[13px] text-muted">{% trans "Memberships to review" %}</div>
</div>
<div class="card bg-base-100 shadow border-l-4 {% if attention.pending_approvals %}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 "clock" size=16 %} {% trans "Pending" %}</div>
<div class="text-4xl font-bold tabular-nums font-mono">{{ attention.pending_approvals }}</div>
<div class="text-xs opacity-60">{% trans "Memberships awaiting approval" %}</div>
</div>
<div class="card min-w-0 flex-1 p-4">
<div class="font-display text-xs font-bold tracking-[.12em] text-muted uppercase">{% trans "Teams without coach" %}</div>
<div class="mt-1 font-display text-[34px] leading-none font-extrabold tabular-nums {% if attention.teams_without_manager %}text-club{% else %}text-ink{% endif %}">{{ attention.teams_without_manager }}</div>
<div class="mt-1 text-[13px] text-muted">{% trans "Nobody can pick a squad" %}</div>
</div>
<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 "sparkles" size=16 %} {% trans "New members" %}</div>
<div class="text-4xl font-bold tabular-nums font-mono">{{ attention.new_members }}</div>
<div class="text-xs opacity-60">{% trans "First season at this club" %}</div>
</div>
<div class="card min-w-0 flex-1 p-4">
<div class="font-display text-xs font-bold tracking-[.12em] text-muted uppercase">{% trans "Unrostered members" %}</div>
<div class="mt-1 font-display text-[34px] leading-none font-extrabold tabular-nums {% if attention.unrostered %}text-warn{% else %}text-ink{% endif %}">{{ attention.unrostered }}</div>
<div class="mt-1 text-[13px] text-muted">{% trans "Active, on no team" %}</div>
</div>
<div class="card bg-base-100 shadow border-l-4 {% if attention.attendance.turnout is None %}border-info{% elif attention.attendance.turnout < 30 %}border-error{% elif attention.attendance.turnout < 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 attention.attendance.turnout is None %}
{% trans "N/A" %}
{% else %}
{{ attention.attendance.turnout }}%
{% endif %}
</div>
<div class="text-xs opacity-60">
{% if attention.attendance.turnout is None %}
{% trans "No events this season" %}
{% else %}
<progress class="progress w-full {% if attention.attendance.turnout < 30 %}progress-error{% elif attention.attendance.turnout < 65 %}progress-warning{% else %}progress-success{% endif %}"
value="{{ attention.attendance.turnout }}" max="100"></progress>
{% endif %}
</div>
</div>
<div class="card min-w-0 flex-1 p-4">
<div class="font-display text-xs font-bold tracking-[.12em] text-muted uppercase">{% trans "New members" %}</div>
<div class="mt-1 font-display text-[34px] leading-none font-extrabold text-ink tabular-nums">{{ attention.new_members }}</div>
<div class="mt-1 text-[13px] text-muted">{% trans "First season at this club" %}</div>
</div>
<div class="card min-w-0 flex-1 p-4">
<div class="font-display text-xs font-bold tracking-[.12em] text-muted uppercase">{% trans "Attendance" %}</div>
<div class="mt-1 font-display text-[34px] leading-none font-extrabold text-ink tabular-nums">{% if attention.attendance.turnout is None %}—{% else %}{{ attention.attendance.turnout }}%{% endif %}</div>
<div class="mt-1 text-[13px] text-muted">{% if attention.attendance.turnout is None %}{% trans "No events this season" %}{% else %}{% trans "Turnout this season" %}{% endif %}</div>
</div>
{% if requirements_configured %}
<div class="card min-w-0 flex-1 p-4">
<div class="font-display text-xs font-bold tracking-[.12em] text-muted uppercase">{% trans "Missing documentation" %}</div>
<div class="mt-1 font-display text-[34px] leading-none font-extrabold tabular-nums {% if missing_documentation_count %}text-warn{% else %}text-ink{% endif %}">{{ missing_documentation_count }}</div>
<div class="mt-1 text-[13px] text-muted">{% trans "Members with an open checklist item" %}</div>
</div>
{% endif %}
</div>
<div class="mb-6 grid gap-4 lg:grid-cols-2">
<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">{% lucide "calendar" size=18 %} {% trans "Upcoming events" %}</h2>
<a class="btn btn-outline btn-sm gap-2" href="{% url 'management:event_list' %}">{% lucide "arrow-right" size=14 %} {% trans "View all" %}</a>
<div class="grid grid-cols-1 gap-5 xl:grid-cols-[1.35fr_1fr]">
{# --- left: needs attention + signups ----------------------------- #}
<div class="flex flex-col gap-5">
<div class="card flex flex-col">
<div class="flex items-center border-b border-line px-4.5 py-3.5">
<span class="font-display text-base font-extrabold tracking-[.08em] text-ink uppercase">{% trans "Needs attention" %}</span>
</div>
<ul class="divide-y divide-base-200">
{% for event in upcoming_events %}
<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">
<div class="truncate font-semibold">{{ event.title }}</div>
<div class="truncate text-xs opacity-60">
{{ event.get_kind_display }}
{% for team in event.teams.all %}&middot; {{ team.short_name }}{% if not forloop.last %}, {% endif %}{% endfor %}
</div>
<div class="flex flex-col">
{% if attention.pending_approvals %}
<div class="flex items-center gap-3.5 border-b border-rule px-4.5 py-3.5">
<span class="h-8.5 w-1.5 shrink-0 rounded-sm bg-club"></span>
<div class="flex-1">
<div class="text-[15px] font-semibold text-ink">{% blocktrans count count=attention.pending_approvals %}{{ count }} membership awaiting approval{% plural %}{{ count }} memberships awaiting approval{% endblocktrans %}</div>
</div>
<div class="shrink-0 whitespace-nowrap text-sm opacity-70">{{ event.start|date:"D j M, H:i" }}</div>
</li>
{% empty %}
<li class="py-2 text-center text-sm opacity-60">{% trans "Nothing scheduled." %}</li>
{% endfor %}
</ul>
</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">{% lucide "newspaper" size=18 %} {% trans "News" %}</h2>
<a class="btn btn-outline btn-sm gap-2" href="{% url 'management:news_list' %}">{% lucide "arrow-right" size=14 %} {% trans "View all" %}</a>
</div>
<ul class="divide-y divide-base-200">
{% for news_item in published_news %}
<li class="flex flex-col gap-1 py-2 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
<a class="link link-hover truncate font-semibold" href="{% url 'management:news_detail' news_item.pk %}">{{ news_item.title }}</a>
<span class="shrink-0 whitespace-nowrap text-sm opacity-70">{{ news_item.published_at|date:"D j M" }}</span>
</li>
{% empty %}
<li class="py-2 text-center text-sm opacity-60">{% trans "Nothing published yet." %}</li>
{% endfor %}
</ul>
</div>
</div>
</div>
{% if is_club_admin %}
{# Charts are desktop-only: three squeezed canvases stacked on a phone add scroll without adding much readability -- the same numbers already sit in the stat cards above. #}
<div class="mb-6 hidden gap-4 lg:grid lg:grid-cols-3">
<div class="card bg-base-100 shadow">
<div class="card-body">
<h2 class="card-title text-base">{% lucide "user-plus" size=18 %} {% trans "Signups per month" %}</h2>
<p class="text-sm opacity-70">{% trans "New members against returning ones." %}</p>
<div class="h-56">
<canvas id="signups-chart"></canvas>
</div>
</div>
</div>
<div class="card bg-base-100 shadow">
<div class="card-body">
<h2 class="card-title text-base">{% lucide "wallet" size=18 %} {% trans "Club fee status this season" %}</h2>
<div class="h-56">
<canvas id="fees-chart"></canvas>
</div>
</div>
</div>
<div class="card bg-base-100 shadow">
<div class="card-body">
<h2 class="card-title text-base">{% lucide "repeat" size=18 %} {% trans "Renewal rate" %}</h2>
{% if attention.renewal_rate is None %}
<p class="text-sm opacity-70">{% trans "No previous season to compare." %}</p>
{% else %}
<div class="h-56">
<canvas id="renewal-chart"></canvas>
<a class="btn btn-outline btn-sm" href="{% url 'management:member_list' %}?status=pending">{% trans "Review" %}</a>
</div>
{% endif %}
{% if attention.teams_without_manager %}
<div class="flex items-center gap-3.5 border-b border-rule px-4.5 py-3.5">
<span class="h-8.5 w-1.5 shrink-0 rounded-sm bg-warn"></span>
<div class="flex-1">
<div class="text-[15px] font-semibold text-ink">{% blocktrans count count=attention.teams_without_manager %}{{ count }} team has no coach assigned{% plural %}{{ count }} teams have no coach assigned{% endblocktrans %}</div>
<div class="text-[13px] text-muted">{% trans "Nobody can pick a squad for them" %}</div>
</div>
<a class="btn btn-outline btn-sm" href="{% url 'management:team_list' %}">{% trans "Assign" %}</a>
</div>
{% endif %}
{% if attention.unrostered %}
<div class="flex items-center gap-3.5 border-b border-rule px-4.5 py-3.5">
<span class="h-8.5 w-1.5 shrink-0 rounded-sm bg-warn"></span>
<div class="flex-1">
<div class="text-[15px] font-semibold text-ink">{% blocktrans count count=attention.unrostered %}{{ count }} active member is on no team{% plural %}{{ count }} active members are on no team{% endblocktrans %}</div>
</div>
<a class="btn btn-outline btn-sm" href="{% url 'management:member_list' %}?unrostered=1">{% trans "Review" %}</a>
</div>
{% endif %}
{% if missing_documentation_count %}
<div class="flex items-center gap-3.5 border-b border-rule px-4.5 py-3.5">
<span class="h-8.5 w-1.5 shrink-0 rounded-sm bg-warn"></span>
<div class="flex-1">
<div class="text-[15px] font-semibold text-ink">{% blocktrans count count=missing_documentation_count %}{{ count }} member has an open checklist item{% plural %}{{ count }} members have an open checklist item{% endblocktrans %}</div>
<div class="text-[13px] text-muted">{% trans "Documents/medical certificates still outstanding" %}</div>
</div>
<a class="btn btn-outline btn-sm" href="{% url 'management:member_list' %}?docs=open">{% trans "Review" %}</a>
</div>
{% endif %}
{% if not attention.pending_approvals and not attention.teams_without_manager and not attention.unrostered and not missing_documentation_count %}
<div class="px-4.5 py-6 text-center text-sm text-muted">{% trans "Nothing needs attention." %}</div>
{% endif %}
</div>
</div>
{% if is_club_admin %}
{# Financial/admin-only charts -- desktop-only canvases, same reasoning the old flat template gave: squeezed on a phone, and this surface is desktop-only anyway now. #}
<div class="grid grid-cols-1 gap-5 lg:grid-cols-3">
<div class="card p-4.5">
<div class="mb-3.5 font-display text-base font-extrabold tracking-[.08em] text-ink uppercase">{% trans "Signups per month" %}</div>
<p class="mb-2 text-sm text-muted">{% trans "New members against returning ones." %}</p>
<div class="h-44">
<canvas id="signups-chart"></canvas>
</div>
</div>
<div class="card p-4.5">
<div class="mb-3.5 font-display text-base font-extrabold tracking-[.08em] text-ink uppercase">{% trans "Club fee status" %}</div>
<div class="h-44">
<canvas id="fees-chart"></canvas>
</div>
</div>
<div class="card p-4.5">
<div class="mb-3.5 font-display text-base font-extrabold tracking-[.08em] text-ink uppercase">{% trans "Renewal rate" %}</div>
{% if attention.renewal_rate is None %}
<p class="text-sm text-muted">{% trans "No previous season to compare." %}</p>
{% else %}
<div class="h-44">
<canvas id="renewal-chart"></canvas>
</div>
{% endif %}
</div>
</div>
{% endif %}
</div>
{# --- right: this weekend + news ----------------------------------- #}
<div class="flex flex-col gap-5">
<div class="rounded-xl bg-ink p-4.5 text-white">
<div class="mb-3.5 font-display text-base font-extrabold tracking-[.08em] uppercase">{% trans "Upcoming" %}</div>
<div class="flex flex-col gap-3">
{% for event in upcoming_events %}
<div class="flex items-center gap-3">
<div class="w-11 shrink-0 text-center">
<div class="font-display text-[22px] leading-none font-extrabold">{{ event.start|date:"d" }}</div>
<div class="font-display text-[10px] tracking-[.1em] text-on-dark-dim uppercase">{{ event.start|date:"D" }}</div>
</div>
<div class="min-w-0 flex-1">
<a class="block truncate text-sm font-semibold hover:underline" href="{% url 'management:event_detail' event.pk %}">{{ event.title }}</a>
<div class="font-mono text-xs text-on-dark-dim">{{ event.start|date:"H:i" }}</div>
</div>
<div class="flex max-w-[35%] shrink-0 flex-wrap items-center justify-end gap-1.5">
{% for team in event.teams.all %}
<span class="inline-flex items-center rounded-full border border-on-dark-faint px-1.5 py-0.5 font-display text-[10px] font-bold tracking-[.06em] text-white uppercase">{{ team.short_name }}</span>
{% endfor %}
</div>
<a class="flex h-7 w-7 shrink-0 items-center justify-center rounded-full border border-on-dark-faint text-on-dark-dim hover:border-white hover:text-white" href="{% url 'management:event_detail' event.pk %}" aria-label="{% trans "View event" %}">
{% lucide "chevron-right" size=15 %}
</a>
</div>
{% if not forloop.last %}<div class="h-px bg-hairline"></div>{% endif %}
{% empty %}
<div class="text-sm text-on-dark-dim">{% trans "Nothing scheduled." %}</div>
{% endfor %}
</div>
<a class="mt-3.5 inline-flex items-center gap-1 font-mono text-[11px] text-on-dark-dim hover:text-white" href="{% url 'management:event_list' %}">{% trans "View calendar" %} &rarr;</a>
</div>
<div class="card flex-1 p-4.5">
<div class="mb-3.5 flex items-center justify-between">
<span class="font-display text-base font-extrabold tracking-[.08em] text-ink uppercase">{% trans "News" %}</span>
<a class="btn btn-outline btn-sm" href="{% url 'management:news_list' %}">{% trans "View all" %}</a>
</div>
<div class="divide-y">
{% for news_item in published_news %}
<div class="flex items-center justify-between gap-3 py-2.5">
<a class="truncate text-sm font-semibold text-ink hover:underline" href="{% url 'management:news_detail' news_item.pk %}">{{ news_item.title }}</a>
<span class="shrink-0 font-mono text-xs text-muted">{{ news_item.published_at|date:"j M" }}</span>
</div>
{% empty %}
<div class="py-4 text-center text-sm text-muted">{% trans "Nothing published yet." %}</div>
{% endfor %}
</div>
</div>
</div>
{% endif %}
</div>
<div class="grid gap-4 {% if is_club_admin %}md:grid-cols-4{% else %}md:grid-cols-3{% endif %}">
<div class="grid grid-cols-1 gap-3.5 {% if is_club_admin %}md:grid-cols-4{% else %}md:grid-cols-3{% endif %}">
{% for group in groups %}
{% if group.title != "Shop" or is_club_admin %}
<div class="card bg-base-100 shadow">
<div class="card-body">
<h2 class="card-title text-base">{% lucide group.icon size=18 %} {{ group.title }}</h2>
<dl class="divide-y divide-base-200">
{% for label, value in group.stats %}
<div class="flex items-center justify-between py-2">
<dt class="text-sm opacity-70">{{ label }}</dt>
<dd class="font-semibold tabular-nums font-mono">{% if group.title == "Shop" and label == "Outstanding" or label == "Revenue" %}&euro;{% endif %}{{ value }}</dd>
</div>
{% endfor %}
</dl>
</div>
<div class="card p-4">
<div class="mb-2 flex items-center gap-1.5 font-display text-sm font-extrabold tracking-[.06em] text-ink uppercase">{% lucide group.icon size=16 %} {{ group.title }}</div>
<dl>
{% for label, value in group.stats %}
<div class="flex items-center justify-between py-1.5">
<dt class="text-[13px] text-muted">{{ label }}</dt>
<dd class="font-mono text-sm font-semibold text-ink">{% if group.title == "Shop" and label == "Outstanding" or label == "Revenue" %}&euro;{% endif %}{{ value }}</dd>
</div>
{% endfor %}
</dl>
</div>
{% endif %}
{% endfor %}
@@ -209,96 +248,61 @@
(() => {
const data = JSON.parse(document.getElementById("chart-data").textContent);
const renewalRate = JSON.parse(document.getElementById("renewal-rate-data").textContent);
const css = (name, fallback) => getComputedStyle(document.documentElement).getPropertyValue(name).trim() || fallback;
// The club's own accent (--color-club resolves through --tenant-club, set in
// base.html from Club.secondary_color) -- Chart.js paints to a canvas, so it
// needs the resolved colour, not the CSS variable reference itself.
const clubColor = getComputedStyle(document.documentElement).getPropertyValue("--color-club").trim() || "#E4002B";
const ink = "#3A4658";
const grid = "#EEF0F3";
const signupsCanvas = document.getElementById("signups-chart");
if (signupsCanvas) {
new Chart(signupsCanvas, {
type: "bar",
data: {
labels: data.signups.map((point) => point.month),
datasets: [
{label: "{{ new_label|escapejs }}", data: data.signups.map((point) => point.new), backgroundColor: "#0B1220"},
{label: "{{ returning_label|escapejs }}", data: data.signups.map((point) => point.returning), backgroundColor: clubColor},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {legend: {position: "bottom", labels: {color: ink, font: {family: "Barlow"}}}},
scales: {
x: {stacked: true, ticks: {color: ink}, grid: {display: false}},
y: {stacked: true, beginAtZero: true, ticks: {color: ink, precision: 0}, grid: {color: grid}},
},
},
});
}
const feesCanvas = document.getElementById("fees-chart");
if (feesCanvas) {
// Colour carries the meaning here -- unpaid must read as a problem, waived
// must not -- so the slices are pinned to the semantic tokens, in order.
new Chart(feesCanvas, {
type: "pie",
data: {
labels: data.fees.map((slice) => slice.label),
datasets: [{data: data.fees.map((slice) => slice.value), backgroundColor: ["#14A05A", "#F0A22E", "#E4002B", "#8B95A4"]}],
},
options: {responsive: true, maintainAspectRatio: false, plugins: {legend: {position: "bottom", labels: {color: ink}}}},
});
}
const renewalCanvas = document.getElementById("renewal-chart");
const render = () => {
const ink = css("--color-base-content", "#333");
const grid = "color-mix(in oklab, " + ink + " 15%, transparent)";
const charts = [];
// Both canvases are admin-only -- absent entirely for a manager/coach,
// same gate as the Shop nav section and the rest of this row.
if (signupsCanvas) {
// Stacked: the bar height stays "signups this month" while the split shows
// where they came from. Side-by-side bars would answer a different question.
charts.push(new Chart(signupsCanvas, {
type: "bar",
data: {
labels: data.signups.map((point) => point.month),
datasets: [
{label: "{{ new_label|escapejs }}", data: data.signups.map((point) => point.new), backgroundColor: css("--color-primary", "#4f46e5")},
{label: "{{ returning_label|escapejs }}", data: data.signups.map((point) => point.returning), backgroundColor: css("--color-accent", "#0ea5e9")},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {legend: {position: "bottom", labels: {color: ink}}},
scales: {
x: {stacked: true, ticks: {color: ink}, grid: {color: grid}},
y: {stacked: true, beginAtZero: true, ticks: {color: ink, precision: 0}, grid: {color: grid}},
},
},
}));
}
if (feesCanvas) {
// Colour carries the meaning here — unpaid must read as a problem, waived
// must not — so the slices are pinned to the semantic theme colours, in order.
charts.push(new Chart(feesCanvas, {
type: "pie",
data: {
labels: data.fees.map((slice) => slice.label),
datasets: [
{
data: data.fees.map((slice) => slice.value),
backgroundColor: [css("--color-success", "#16a34a"), css("--color-warning", "#f59e0b"), css("--color-error", "#dc2626"), css("--color-neutral", "#6b7280")],
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {legend: {position: "right", labels: {color: ink}}},
},
}));
}
if (renewalCanvas && renewalRate !== null) {
// Same "colour carries the meaning" reasoning as the fee chart: a low
// renewal rate reads as a problem, so it's pinned to error, not neutral.
charts.push(new Chart(renewalCanvas, {
type: "pie",
data: {
labels: ["{{ renewed_label|escapejs }}", "{{ not_renewed_label|escapejs }}"],
datasets: [
{
data: [renewalRate, 100 - renewalRate],
backgroundColor: [css("--color-success", "#16a34a"), css("--color-error", "#dc2626")],
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {legend: {position: "right", labels: {color: ink}}},
},
}));
}
return charts;
};
let charts = render();
// "auto" removes data-theme entirely, so watch the attribute rather than a click.
new MutationObserver(() => {
charts.forEach((chart) => chart.destroy());
charts = render();
}).observe(document.documentElement, {attributes: true, attributeFilter: ["data-theme"]});
if (renewalCanvas && renewalRate !== null) {
new Chart(renewalCanvas, {
type: "pie",
data: {
labels: ["{{ renewed_label|escapejs }}", "{{ not_renewed_label|escapejs }}"],
datasets: [{data: [renewalRate, 100 - renewalRate], backgroundColor: ["#14A05A", "#E4002B"]}],
},
options: {responsive: true, maintainAspectRatio: false, plugins: {legend: {position: "bottom", labels: {color: ink}}}},
});
}
})();
</script>
{% endblock extra_body %}

View File

@@ -4,24 +4,24 @@
{% block heading %}{% if update_view %}{% blocktrans %}Edit {{ object }}{% endblocktrans %}{% else %}{% trans "New location" %}{% endif %}{% endblock heading %}
{% block panel %}
<div class="card w-full bg-base-100 shadow">
<div class="card w-full">
<div class="card-body">
<form method="post">
{% csrf_token %}
{% for error in form.non_field_errors %}
<div class="alert alert-error my-2">
<div class="alert alert-error">
<span>{{ error }}</span>
</div>
{% endfor %}
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
{% for field in form %}
{% form_field field %}
{% endfor %}
</div>
<div class="card-actions justify-start pt-2 mt-2">
<div class="card-actions justify-start pt-4">
<a class="btn btn-outline gap-2" href="{% url "management:location_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>

View File

@@ -8,44 +8,42 @@
{% endblock actions %}
{% block panel %}
<div class="card bg-base-100 shadow">
<div class="card-body">
<div class="overflow-x-auto">
<table class="table table-cards">
<thead>
<div class="card overflow-hidden">
<div class="overflow-x-auto">
<table class="table">
<thead>
<tr>
<th>{% trans "Name" %}</th>
<th>{% trans "Address" %}</th>
<th>{% trans "City" %}</th>
<th>{% trans "Country" %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for location in locations %}
<tr>
<th>{% trans "Name" %}</th>
<th>{% trans "Address" %}</th>
<th>{% trans "City" %}</th>
<th>{% trans "Country" %}</th>
<th></th>
<td class="font-semibold text-ink">
{{ location.name }}
{% if location.is_home %}<span class="badge badge-sm badge-neutral ml-1">{% trans "Home" %}</span>{% endif %}
</td>
<td>{{ location.address }}</td>
<td>{{ location.city }}</td>
<td>{{ location.country }}</td>
<td class="text-right">
<div class="flex justify-end gap-1">
<a class="btn btn-outline btn-sm" href="{% url 'management:location_update' location.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('{{ location.pk|dom_id:"location_delete_modal" }}').showModal()" aria-label="{% trans 'Delete' %}">{% lucide "trash-2" size=14 %} {% trans "Delete" %}</button>
</div>
</td>
</tr>
</thead>
<tbody>
{% for location in locations %}
<tr>
<td class="font-semibold">
{{ location.name }}
{% if location.is_home %}<span class="badge badge-sm badge-primary ml-1">{% trans "Home" %}</span>{% endif %}
</td>
<td data-label="{% trans 'Address' %}">{{ location.address }}</td>
<td data-label="{% trans 'City' %}">{{ location.city }}</td>
<td data-label="{% trans 'Country' %}">{{ location.country }}</td>
<td class="text-right">
<div class="flex justify-end gap-1">
<a class="btn btn-outline btn-sm" href="{% url 'management:location_update' location.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('{{ location.pk|dom_id:"location_delete_modal" }}').showModal()" aria-label="{% trans 'Delete' %}">{% lucide "trash-2" size=14 %} {% trans "Delete" %}</button>
</div>
</td>
</tr>
{% empty %}
<tr>
<td colspan="5" class="text-center opacity-60">{% trans "No locations yet." %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% empty %}
<tr>
<td colspan="5" class="py-8 text-center text-muted">{% trans "No locations yet." %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>

View File

@@ -2,7 +2,19 @@
{% load lucide ui i18n static %}
{% block heading %}{{ member.get_full_name }}{% endblock heading %}
{% block subheading %}{{ member.contact_email }}{% endblock subheading %}
{% block topbar_context %}
{% if member.contact_email %}<span class="font-mono text-[13px] text-muted">{{ member.contact_email }}</span>{% endif %}
{% if current_membership %}
<span class="badge
{% if current_membership.status == "active" %}badge-success
{% elif current_membership.status == "pending" %}badge-warning
{% elif current_membership.status == "cancelled" %}badge-error
{% else %}badge-neutral{% endif %}">
{{ current_membership.get_status_display }}
</span>
{% endif %}
{% endblock topbar_context %}
{% block actions %}
{% if is_club_admin %}
@@ -12,63 +24,60 @@
{% block panel %}
<div class="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 "user" size=18 %} {% trans "Personal information" %}</h2>
<dl class="divide-y divide-base-200">
<div class="flex flex-col gap-0.5 py-2 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
<dt class="text-sm opacity-70">{% trans "Date of birth" %}</dt>
<dd class="font-semibold sm:text-right">{{ member.date_of_birth|default:"-" }}</dd>
</div>
<div class="flex flex-col gap-0.5 py-2 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
<dt class="text-sm opacity-70">{% trans "Phone" %}</dt>
<dd class="font-semibold sm:text-right">{{ member.phone.as_international|default:"-" }}</dd>
</div>
<div class="flex flex-col gap-0.5 py-2 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
<dt class="text-sm opacity-70">{% trans "Emergency phone" %}</dt>
<dd class="font-semibold sm:text-right">{{ member.emergency_phone.as_international|default:"-" }}</dd>
</div>
{% for guardian in guardians %}
<div class="flex flex-col gap-0.5 py-2 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
<dt class="text-sm opacity-70">{% trans "Parent phone" %} — {{ guardian.get_full_name }}</dt>
<dd class="font-semibold sm:text-right">{{ guardian.phone.as_international|default:"-" }}</dd>
</div>
<div class="flex flex-col gap-0.5 py-2 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
<dt class="text-sm opacity-70">{% trans "Parent emergency phone" %} — {{ guardian.get_full_name }}</dt>
<dd class="font-semibold sm:text-right">{{ guardian.emergency_phone.as_international|default:"-" }}</dd>
</div>
{% endfor %}
</dl>
{# flex-col below `sm`: a button's own label never wraps, so two grow buttons side by side in a fixed row push wider than the screen once either label is long -- stacking avoids that regardless of label length. #}
<div class="flex flex-col gap-2 sm:flex-row sm:items-center mt-2">
{% if member.phone %}
<a class="btn btn-sm btn-outline btn-primary sm:grow" href="tel:{{ member.phone.as_international }}">{% lucide "phone" size=14 %} {% trans "Call" %}</a>
{% endif %}
{% if member.emergency_phone %}
<a class="btn btn-sm btn-outline btn-error sm:grow" href="tel:{{ member.emergency_phone.as_international }}">{% lucide "shield-alert" size=14 %} {% trans "Emergency call" %}</a>
{% endif %}
<div class="card card-body">
<h2 class="card-title">{% lucide "user" size=18 %} {% trans "Personal information" %}</h2>
<dl>
<div class="flex items-center justify-between gap-4 py-2">
<dt class="text-sm text-muted">{% trans "Date of birth" %}</dt>
<dd class="font-mono text-sm font-semibold text-ink">{{ member.date_of_birth|default:"—" }}</dd>
</div>
{% if guardians %}
<div class="divider my-1"></div>
<div class="text-xs font-semibold uppercase opacity-60 flex items-center gap-1 mb-1">
{% lucide "users" size=12 %} {% trans "Parent/guardian contact" %}
<div class="flex items-center justify-between gap-4 py-2">
<dt class="text-sm text-muted">{% trans "Phone" %}</dt>
<dd class="font-mono text-sm font-semibold text-ink">{{ member.phone.as_international|default:"—" }}</dd>
</div>
<div class="flex items-center justify-between gap-4 py-2">
<dt class="text-sm text-muted">{% trans "Emergency phone" %}</dt>
<dd class="font-mono text-sm font-semibold text-ink">{{ member.emergency_phone.as_international|default:"—" }}</dd>
</div>
{% for guardian in guardians %}
<div class="flex items-center justify-between gap-4 py-2">
<dt class="text-sm text-muted">{% trans "Parent phone" %} — {{ guardian.get_full_name }}</dt>
<dd class="font-mono text-sm font-semibold text-ink">{{ guardian.phone.as_international|default:"—" }}</dd>
</div>
{% for guardian in guardians %}
{% if guardian.phone or guardian.emergency_phone %}
<div class="flex flex-col gap-2 sm:flex-row sm:items-center mt-2">
{% if guardian.phone %}
<a class="btn btn-sm btn-outline btn-primary sm:grow" href="tel:{{ guardian.phone.as_international }}">{% lucide "phone" size=14 %} {% blocktrans with name=guardian.get_full_name %}Call {{ name }}{% endblocktrans %}</a>
{% endif %}
{% if guardian.emergency_phone %}
<a class="btn btn-sm btn-outline btn-error sm:grow" href="tel:{{ guardian.emergency_phone.as_international }}">{% lucide "shield-alert" size=14 %} {% blocktrans with name=guardian.get_full_name %}Emergency: {{ name }}{% endblocktrans %}</a>
{% endif %}
</div>
{% endif %}
{% endfor %}
<div class="flex items-center justify-between gap-4 py-2">
<dt class="text-sm text-muted">{% trans "Parent emergency phone" %} — {{ guardian.get_full_name }}</dt>
<dd class="font-mono text-sm font-semibold text-ink">{{ guardian.emergency_phone.as_international|default:"—" }}</dd>
</div>
{% endfor %}
</dl>
<div class="mt-2 flex flex-wrap items-center gap-2">
{% if member.phone %}
<a class="btn btn-outline btn-info btn-sm gap-2 flex-1" href="tel:{{ member.phone.as_international }}">{% lucide "phone" size=14 %} {% trans "Call" %}</a>
{% endif %}
{% if member.emergency_phone %}
<a class="btn btn-outline btn-error btn-sm gap-2 flex-1" href="tel:{{ member.emergency_phone.as_international }}">{% lucide "shield-alert" size=14 %} {% trans "Emergency call" %}</a>
{% endif %}
</div>
{% if guardians %}
<div class="my-1 h-px bg-rule"></div>
<div class="mb-1 flex items-center gap-1.5 font-display text-xs font-bold tracking-[.1em] text-muted uppercase">
{% lucide "users" size=12 %} {% trans "Parent/guardian contact" %}
</div>
{% for guardian in guardians %}
{% if guardian.phone or guardian.emergency_phone %}
<div class="mt-2 flex flex-wrap items-center gap-2">
{% if guardian.phone %}
<a class="btn btn-outline btn-info btn-sm gap-2 flex-1" href="tel:{{ guardian.phone.as_international }}">{% lucide "phone" size=14 %} {% blocktrans with name=guardian.get_full_name %}Call {{ name }}{% endblocktrans %}</a>
{% endif %}
{% if guardian.emergency_phone %}
<a class="btn btn-outline btn-error btn-sm gap-2 flex-1" href="tel:{{ guardian.emergency_phone.as_international }}">{% lucide "shield-alert" size=14 %} {% blocktrans with name=guardian.get_full_name %}Emergency: {{ name }}{% endblocktrans %}</a>
{% endif %}
</div>
{% endif %}
{% endfor %}
{% endif %}
</div>
{% comment %}
@@ -76,126 +85,189 @@
actually ties this person to *this* club, so it gets its own panel: the
current season's standing, plus every season they've been part of.
{% endcomment %}
<div class="card bg-base-100 shadow">
<div class="card-body">
<h2 class="card-title text-base">{% lucide "id-card" size=18 %} {% trans "Club membership" %}</h2>
{% if current_membership %}
{% if current_membership.is_guardian %}
{% comment %}
Stated plainly rather than left to be inferred from an empty
fee: a guardian is deliberately missing from the member list
and every member count, and someone looking at this page needs
to know that is on purpose. See club.models.ClubMembership.Kind.
{% endcomment %}
<div class="alert alert-warning py-2 my-1">
{% lucide "users" size=16 %}
<span class="text-sm">{% trans "Guardian: attached to the club as a parent of a member. Holds the login, owes no fee, and is not counted as a member." %}</span>
</div>
{% endif %}
<dl class="divide-y divide-base-200">
<div class="flex flex-col gap-0.5 py-2 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
<dt class="text-sm opacity-70">{% trans "Joined as" %}</dt>
<dd class="font-semibold sm:text-right">{{ current_membership.get_kind_display|capfirst }}</dd>
</div>
<div class="flex flex-col gap-0.5 py-2 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
<dt class="text-sm opacity-70">{% trans "License" %}</dt>
<dd class="font-semibold sm:text-right">{{ current_membership.license|default:"-" }}</dd>
</div>
<div class="flex flex-col gap-0.5 py-2 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
<dt class="text-sm opacity-70">{% trans "Status" %}</dt>
<dd class="font-semibold sm:text-right">{{ current_membership.get_status_display }}</dd>
</div>
{% if not current_membership.is_guardian %}
<div class="flex flex-col gap-0.5 py-2 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
<dt class="text-sm opacity-70">{% trans "Fee status" %}</dt>
<dd class="font-semibold sm:text-right">{{ current_membership.get_fee_status_display }}</dd>
</div>
{% endif %}
</dl>
{% else %}
<p class="text-sm opacity-60">{% trans "Not rostered for the current season." %}</p>
{% endif %}
{% if membership_history %}
<div class="divider"></div>
<h3 class="text-sm font-semibold opacity-70">{% trans "Season history" %}</h3>
<div class="overflow-x-auto">
<table class="table table-cards">
<thead>
<tr>
<th>{% trans "Season" %}</th>
<th>{% trans "Status" %}</th>
<th>{% trans "Fee status" %}</th>
<th>{% trans "License" %}</th>
</tr>
</thead>
<tbody>
{% for membership in membership_history %}
<tr>
<td class="font-semibold">{{ membership.season.start_date|date:"Y" }} - {{ membership.season.end_date|date:"Y" }}</td>
<td data-label="{% trans 'Status' %}">{{ membership.get_status_display }}</td>
<td data-label="{% trans 'Fee status' %}">{{ membership.get_fee_status_display }}</td>
<td data-label="{% trans 'License' %}">{{ membership.license|default:"-" }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="card card-body">
<h2 class="card-title">{% lucide "id-card" size=18 %} {% trans "Club membership" %}</h2>
{% if current_membership %}
{% if current_membership.is_guardian %}
{% comment %}
Stated plainly rather than left to be inferred from an empty
fee: a guardian is deliberately missing from the member list
and every member count, and someone looking at this page needs
to know that is on purpose. See club.models.ClubMembership.Kind.
{% endcomment %}
<div class="alert alert-warning">
{% lucide "users" size=16 %}
<span>{% trans "Guardian: attached to the club as a parent of a member. Holds the login, owes no fee, and is not counted as a member." %}</span>
</div>
{% endif %}
</div>
<dl>
<div class="flex items-center justify-between gap-4 py-2">
<dt class="text-sm text-muted">{% trans "Joined as" %}</dt>
<dd class="text-sm font-semibold text-ink">{{ current_membership.get_kind_display|capfirst }}</dd>
</div>
<div class="flex items-center justify-between gap-4 py-2">
<dt class="text-sm text-muted">{% trans "License" %}</dt>
<dd class="font-mono text-sm font-semibold text-ink">{{ current_membership.license|default:"—" }}</dd>
</div>
<div class="flex items-center justify-between gap-4 py-2">
<dt class="text-sm text-muted">{% trans "Status" %}</dt>
<dd>
<span class="badge badge-sm
{% if current_membership.status == "active" %}badge-success
{% elif current_membership.status == "pending" %}badge-warning
{% elif current_membership.status == "cancelled" %}badge-error
{% else %}badge-neutral{% endif %}">
{{ current_membership.get_status_display }}
</span>
</dd>
</div>
{% if not current_membership.is_guardian %}
<div class="flex items-center justify-between gap-4 py-2">
<dt class="text-sm text-muted">{% trans "Fee status" %}</dt>
<dd>
<span class="badge badge-sm
{% if current_membership.fee_status == "paid" %}badge-success
{% elif current_membership.fee_status == "partially_paid" %}badge-warning
{% elif current_membership.fee_status == "unpaid" %}badge-error
{% else %}badge-ghost{% endif %}">
{{ current_membership.get_fee_status_display }}
</span>
</dd>
</div>
{% endif %}
</dl>
{% else %}
<p class="text-sm text-muted">{% trans "Not rostered for the current season." %}</p>
{% endif %}
{% if membership_history %}
<div class="my-1 h-px bg-rule"></div>
<h3 class="font-display text-xs font-bold tracking-[.1em] text-muted uppercase">{% trans "Season history" %}</h3>
<table class="table">
<thead>
<tr>
<th>{% trans "Season" %}</th>
<th>{% trans "Status" %}</th>
<th>{% trans "Fee status" %}</th>
<th>{% trans "License" %}</th>
</tr>
</thead>
<tbody>
{% for membership in membership_history %}
<tr>
<td class="font-semibold">{{ membership.season.start_date|date:"Y" }} - {{ membership.season.end_date|date:"Y" }}</td>
<td>{{ membership.get_status_display }}</td>
<td>{{ membership.get_fee_status_display }}</td>
<td class="font-mono text-xs">{{ membership.license|default:"—" }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</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 flex-wrap items-center justify-between gap-2">
<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 flex-col gap-0.5 py-2 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
<dt class="text-sm opacity-70">{% trans "Level" %}</dt>
<dd class="font-semibold sm:text-right">{{ referee_profile.level }}</dd>
</div>
<div class="flex flex-col gap-0.5 py-2 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
<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>
{% comment %}
Onboarding checklist -- club.services.onboarding.checklist_for, paired with
MemberRequirementStatus if one exists. Any staff can mark an item done or
reopen it, not just admins (same visibility as the rest of this page) --
see the module docstring on club/services/onboarding.py for why this stays
separate from ClubMembership.status/fee_status.
{% endcomment %}
<div class="card card-body mt-4">
<h2 class="card-title">{% lucide "clipboard-check" size=18 %} {% trans "Documents" %}</h2>
{% if not current_membership %}
<p class="text-sm text-muted">{% trans "Not rostered for the current season -- nothing to check off yet." %}</p>
{% elif not checklist %}
<p class="text-sm text-muted">{% trans "This club has no onboarding requirements set up." %}</p>
{% else %}
<div class="flex flex-col gap-1">
{% for requirement, status in checklist %}
<div class="flex items-start justify-between gap-4 py-3">
<div class="min-w-0 flex-1">
<div class="flex flex-wrap items-center gap-2">
<span class="font-semibold text-ink">{{ requirement.name }}</span>
{% if status.is_complete %}
<span class="badge badge-sm badge-success">{% trans "Complete" %}</span>
{% elif status %}
<span class="badge badge-sm badge-warning">{% trans "Incomplete" %}</span>
{% else %}
<span class="badge badge-warning badge-sm">{% trans "No validity set" %}</span>
<span class="badge badge-sm badge-ghost">{% trans "Not started" %}</span>
{% endif %}
</dd>
</div>
{% if requirement.description %}<p class="mt-1 text-sm text-muted">{{ requirement.description }}</p>{% endif %}
{% if status.is_complete %}
<p class="mt-1 font-mono text-xs text-dim">{% blocktrans with who=status.completed_by when=status.completed_at|date:"j M Y H:i" %}Completed by {{ who }} on {{ when }}{% endblocktrans %}</p>
{% endif %}
{% if status.note %}<p class="mt-1 text-sm text-slate italic">&ldquo;{{ status.note }}&rdquo;</p>{% endif %}
{% if status.document %}
<a class="mt-1 inline-flex items-center gap-1 text-sm link link-hover" href="{% url 'management:member_requirement_document' pk=member.pk requirement_pk=requirement.pk %}">{% lucide "download" size=13 %} {% trans "Download document" %}</a>
{% endif %}
</div>
</dl>
<div class="flex shrink-0 items-center gap-1.5">
{% if status.is_complete %}
<form method="post" action="{% url 'management:member_requirement_incomplete' pk=member.pk requirement_pk=requirement.pk %}">
{% csrf_token %}
<button class="btn btn-outline btn-xs gap-1" type="submit">{% lucide "rotate-ccw" size=12 %} {% trans "Reopen" %}</button>
</form>
{% else %}
<button class="btn btn-primary btn-xs gap-1" type="button" onclick="document.getElementById('{{ requirement.pk|dom_id:"requirement_complete_modal" }}').showModal()">{% lucide "check" size=12 %} {% trans "Mark complete" %}</button>
{% endif %}
</div>
</div>
{% endfor %}
</div>
{% endif %}
</div>
{% 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>
{% if referee_profile or is_club_admin %}
<div class="card card-body mt-4">
<div class="flex flex-wrap items-center justify-between gap-2">
<h2 class="card-title">{% 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>
<div class="flex items-center justify-between gap-4 py-2">
<dt class="text-sm text-muted">{% trans "Level" %}</dt>
<dd class="text-sm font-semibold text-ink">{{ referee_profile.level }}</dd>
</div>
<div class="flex items-center justify-between gap-4 py-2">
<dt class="text-sm text-muted">{% trans "Valid until" %}</dt>
<dd class="flex items-center gap-2">
<span class="font-mono text-sm font-semibold text-ink">{{ referee_profile.valid_until|default:"—" }}</span>
{% if referee_profile.is_currently_valid %}
<span class="badge badge-sm badge-success">{% trans "Valid" %}</span>
{% elif referee_profile.valid_until %}
<span class="badge badge-sm badge-error">{% trans "Expired" %}</span>
{% else %}
<span class="badge badge-sm badge-warning">{% trans "No validity set" %}</span>
{% endif %}
</dd>
</div>
</dl>
{% if referee_profile.is_eligible %}
<div class="mt-2 mb-1 font-display text-xs font-bold tracking-[.1em] text-muted uppercase">{% 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 text-muted">{% 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 text-muted">{% trans "Not eligible to referee for any team." %}</p>
{% endif %}
</div>
{% if is_club_admin %}
@@ -213,23 +285,21 @@
{% trans "Remove from family" as remove_from_family_title %}
{% trans "Remove" as remove_label %}
{% for family_group in family_groups %}
<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">
{% lucide "users" size=18 %}
<a class="link link-hover" href="{% url 'management:family_detail' family_group.family.pk %}">{{ family_group.family }}</a>
</h2>
{% if is_club_admin %}
<div class="flex flex-wrap gap-2">
<button class="btn btn-outline btn-sm gap-2" type="button" onclick="document.getElementById('{{ family_group.family.pk|dom_id:"add_parent_modal" }}').showModal()">{% lucide "user-plus" size=14 %} {{ add_parent_label }}</button>
<button class="btn btn-outline btn-sm gap-2" type="button" onclick="document.getElementById('{{ family_group.family.pk|dom_id:"add_child_modal" }}').showModal()">{% lucide "baby" size=14 %} {{ add_child_label }}</button>
<button class="btn btn-outline btn-error btn-sm gap-2" type="button" onclick="document.getElementById('{{ family_group.family.pk|dom_id:"detach_family_modal" }}').showModal()">{% lucide "user-x" size=14 %} {{ remove_from_family_label }}</button>
</div>
{% endif %}
</div>
{% include "management/_family_members_table.html" with group=family_group next_url=request.path %}
<div class="card card-body mt-4">
<div class="flex flex-wrap items-center justify-between gap-2">
<h2 class="card-title">
{% lucide "users" size=18 %}
<a class="link link-hover" href="{% url 'management:family_detail' family_group.family.pk %}">{{ family_group.family }}</a>
</h2>
{% if is_club_admin %}
<div class="flex flex-wrap gap-2">
<button class="btn btn-outline btn-sm gap-2" type="button" onclick="document.getElementById('{{ family_group.family.pk|dom_id:"add_parent_modal" }}').showModal()">{% lucide "user-plus" size=14 %} {{ add_parent_label }}</button>
<button class="btn btn-outline btn-sm gap-2" type="button" onclick="document.getElementById('{{ family_group.family.pk|dom_id:"add_child_modal" }}').showModal()">{% lucide "baby" size=14 %} {{ add_child_label }}</button>
<button class="btn btn-outline btn-error btn-sm gap-2" type="button" onclick="document.getElementById('{{ family_group.family.pk|dom_id:"detach_family_modal" }}').showModal()">{% lucide "user-x" size=14 %} {{ remove_from_family_label }}</button>
</div>
{% endif %}
</div>
{% include "management/_family_members_table.html" with group=family_group next_url=request.path %}
</div>
{% if is_club_admin %}
@@ -244,16 +314,14 @@
{% endfor %}
{% if is_club_admin %}
<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">{% lucide "users" size=18 %} {% trans "Family" %}</h2>
<button class="btn btn-outline btn-sm gap-2" type="button" onclick="document.getElementById('attach_family_modal').showModal()">{% lucide "user-plus" size=14 %} {% trans "Add to family" %}</button>
</div>
{% if not family_groups %}
<p class="text-sm opacity-60">{% trans "Not part of a family." %}</p>
{% endif %}
<div class="card card-body mt-4">
<div class="flex flex-wrap items-center justify-between gap-2">
<h2 class="card-title">{% lucide "users" size=18 %} {% trans "Family" %}</h2>
<button class="btn btn-outline btn-sm gap-2" type="button" onclick="document.getElementById('attach_family_modal').showModal()">{% lucide "user-plus" size=14 %} {% trans "Add to family" %}</button>
</div>
{% if not family_groups %}
<p class="text-sm text-muted">{% trans "Not part of a family." %}</p>
{% endif %}
</div>
{% url 'management:member_attach_family' member.pk as attach_family_url %}
@@ -262,6 +330,36 @@
{% trans "Pick an existing family, or leave it blank to start a new one." as attach_family_blurb %}
{% 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 %}
{% for requirement, status in checklist %}
{% if not status.is_complete %}
<dialog id="{{ requirement.pk|dom_id:"requirement_complete_modal" }}" class="modal">
<div class="modal-box">
<h3 class="font-display text-lg font-extrabold text-ink uppercase">{% blocktrans with name=requirement.name %}Mark “{{ name }}” complete{% endblocktrans %}</h3>
<form method="post" action="{% url 'management:member_requirement_complete' pk=member.pk requirement_pk=requirement.pk %}" id="{{ requirement.pk|dom_id:"requirement_complete_form" }}" enctype="multipart/form-data">
{% csrf_token %}
<div class="form-control my-3">
<label class="label-text" for="{{ requirement.pk|dom_id:"requirement_document" }}">{% trans "Document" %}</label>
<input class="file-input mt-1" type="file" name="document" id="{{ requirement.pk|dom_id:"requirement_document" }}">
</div>
<div class="form-control my-3">
<label class="label-text" for="{{ requirement.pk|dom_id:"requirement_note" }}">{% trans "Note" %}</label>
<textarea class="textarea mt-1" name="note" id="{{ requirement.pk|dom_id:"requirement_note" }}" rows="2"></textarea>
</div>
</form>
<div class="modal-action">
<form method="dialog">
<button class="btn btn-outline gap-2">{% lucide "x" size=16 %} {% trans "Cancel" %}</button>
</form>
<button class="btn btn-primary gap-2" type="submit" form="{{ requirement.pk|dom_id:"requirement_complete_form" }}">{% lucide "check" size=16 %} {% trans "Mark complete" %}</button>
</div>
</div>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>
{% endif %}
{% endfor %}
{% endblock panel %}
{% block extra_body %}

View File

@@ -10,46 +10,44 @@
{% endblock actions %}
{% block panel %}
<div class="card w-full bg-base-100 shadow">
<div class="card-body">
<form method="post">
{% csrf_token %}
<div class="card card-body">
<form method="post">
{% csrf_token %}
{% for error in form.non_field_errors %}
{% 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 gap-4 md:grid-cols-2">
{% for field in form %}
{% form_field field %}
{% endfor %}
</div>
{% if membership_form %}
<div class="my-2 h-px bg-rule"></div>
<h2 class="card-title">{% lucide "id-card" size=18 %} {% trans "This season" %}</h2>
{% for error in membership_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 %}
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
{% for field in membership_form %}
{% form_field field %}
{% endfor %}
</div>
{% endif %}
{% if membership_form %}
<div class="divider"></div>
<h2 class="card-title text-base">{% lucide "id-card" size=18 %} {% trans "This season" %}</h2>
{% for error in membership_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 membership_form %}
{% form_field field %}
{% endfor %}
</div>
{% endif %}
<div class="card-actions justify-start pt-2 mt-2">
<a class="btn btn-outline gap-2" href="{% if update_view %}{% url "management:member_detail" object.pk %}{% else %}{% url "management:member_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 class="mt-2 flex items-center gap-2 pt-2">
<a class="btn btn-outline gap-2" href="{% if update_view %}{% url "management:member_detail" object.pk %}{% else %}{% url "management:member_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>
{% if update_view %}

View File

@@ -8,35 +8,33 @@
{% endblock actions %}
{% block panel %}
<div class="card w-full bg-base-100 shadow">
<div class="card-body">
<p class="text-sm opacity-70">
{% blocktrans %}Fill in the downloaded template — one row per member — then upload it
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>
<div class="card card-body">
<p class="text-sm text-muted">
{% blocktrans %}Fill in the downloaded template — one row per member — then upload it
here. Nothing is created yet: you'll see exactly what will be added
before anything is saved.{% endblocktrans %}
</p>
<p class="text-sm text-muted">
{% 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 %}
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{% for error in form.non_field_errors %}
<div class="alert alert-error my-2">
<span>{{ error }}</span>
</div>
{% endfor %}
{% for field in form %}
{% form_field field %}
{% endfor %}
<div class="card-actions justify-start pt-2 mt-2">
<a class="btn btn-outline gap-2" href="{% url 'management:member_list' %}">{% lucide "arrow-left" size=16 %} {% trans "Cancel" %}</a>
<button class="btn btn-primary gap-2" type="submit">{% lucide "upload" size=16 %} {% trans "Upload and preview" %}</button>
{% for error in form.non_field_errors %}
<div class="alert alert-error my-2">
<span>{{ error }}</span>
</div>
</form>
</div>
{% endfor %}
{% for field in form %}
{% form_field field %}
{% endfor %}
<div class="mt-2 flex items-center gap-2 pt-2">
<a class="btn btn-outline gap-2" href="{% url 'management:member_list' %}">{% lucide "arrow-left" size=16 %} {% trans "Cancel" %}</a>
<button class="btn btn-primary gap-2" type="submit">{% lucide "upload" size=16 %} {% trans "Upload and preview" %}</button>
</div>
</form>
</div>
{% endblock panel %}

View File

@@ -11,80 +11,76 @@
</div>
{% endif %}
<div class="card bg-base-100 shadow">
<div class="card-body">
<p class="text-sm opacity-70">
{% blocktrans count counter=valid_count %}{{ counter }} member will be created.{% plural %}{{ counter }} members will be created.{% endblocktrans %}
{% if skipped_count %}{% blocktrans count counter=skipped_count %}{{ counter }} row will be skipped.{% plural %}{{ counter }} rows will be skipped.{% endblocktrans %}{% endif %}
</p>
<div class="card card-body">
<p class="text-sm text-muted">
{% blocktrans count counter=valid_count %}{{ counter }} member will be created.{% plural %}{{ counter }} members will be created.{% endblocktrans %}
{% if skipped_count %}{% blocktrans count counter=skipped_count %}{{ counter }} row will be skipped.{% plural %}{{ counter }} rows will be skipped.{% endblocktrans %}{% endif %}
</p>
<div class="overflow-x-auto">
<table class="table">
<thead>
<tr>
<th>{% trans "Row" %}</th>
<th>{% trans "Last name" %}</th>
<th>{% trans "First name" %}</th>
<th>{% trans "Email" %}</th>
<th>{% trans "Family" %}</th>
<th>{% trans "Joining as" %}</th>
<th>{% trans "Outcome" %}</th>
</tr>
</thead>
<tbody>
{% for result in results %}
<tr>
<td>{{ result.line_number }}</td>
<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>
{% comment %}
A guardian holds the login for a child but is not a member:
no fee, and not counted in any member total. See
club.models.ClubMembership.Kind.
{% endcomment %}
{% if result.membership_kwargs.kind == "guardian" %}
<span class="badge badge-warning badge-sm">{% trans "Guardian" %}</span>
{% else %}
<span class="badge badge-neutral badge-sm">{% trans "Member" %}</span>
{% endif %}
</td>
<td>
{% if result.member %}
<span class="badge badge-success badge-sm">{% trans "Will create" %}</span>
{% else %}
<span class="badge badge-error badge-sm">{% trans "Skipped" %}</span>
<div class="text-xs opacity-70 mt-1">{{ result.errors|join:"; " }}</div>
{% endif %}
</td>
</tr>
{% empty %}
<tr>
<td colspan="7" class="text-center opacity-60">{% trans "No rows found in the uploaded file." %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<table class="table">
<thead>
<tr>
<th>{% trans "Row" %}</th>
<th>{% trans "Last name" %}</th>
<th>{% trans "First name" %}</th>
<th>{% trans "Email" %}</th>
<th>{% trans "Family" %}</th>
<th>{% trans "Joining as" %}</th>
<th>{% trans "Outcome" %}</th>
</tr>
</thead>
<tbody>
{% for result in results %}
<tr>
<td class="font-mono text-xs">{{ result.line_number }}</td>
<td>{{ result.raw.last_name }}</td>
<td>{{ result.raw.first_name }}</td>
<td class="text-muted">{{ 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="text-dim"></span>
{% endif %}
</td>
<td>
{% comment %}
A guardian holds the login for a child but is not a member:
no fee, and not counted in any member total. See
club.models.ClubMembership.Kind.
{% endcomment %}
{% if result.membership_kwargs.kind == "guardian" %}
<span class="badge badge-warning badge-sm">{% trans "Guardian" %}</span>
{% else %}
<span class="badge badge-neutral badge-sm">{% trans "Member" %}</span>
{% endif %}
</td>
<td>
{% if result.member %}
<span class="badge badge-success badge-sm">{% trans "Will create" %}</span>
{% else %}
<span class="badge badge-error badge-sm">{% trans "Skipped" %}</span>
<div class="mt-1 text-xs text-muted">{{ result.errors|join:"; " }}</div>
{% endif %}
</td>
</tr>
{% empty %}
<tr>
<td colspan="7" class="py-6 text-center text-muted">{% trans "No rows found in the uploaded file." %}</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="card-actions justify-start pt-2 mt-2">
<a class="btn btn-outline gap-2" href="{% url 'management:member_import' %}">{% lucide "arrow-left" size=16 %} {% trans "Back" %}</a>
{% if valid_count %}
<form method="post" action="{% url 'management:member_import_confirm' %}">
{% csrf_token %}
<button class="btn btn-primary gap-2" type="submit">{% lucide "check" size=16 %} {% blocktrans %}Confirm import ({{ valid_count }}){% endblocktrans %}</button>
</form>
{% endif %}
</div>
<div class="mt-2 flex items-center gap-2 pt-2">
<a class="btn btn-outline gap-2" href="{% url 'management:member_import' %}">{% lucide "arrow-left" size=16 %} {% trans "Back" %}</a>
{% if valid_count %}
<form method="post" action="{% url 'management:member_import_confirm' %}">
{% csrf_token %}
<button class="btn btn-primary gap-2" type="submit">{% lucide "check" size=16 %} {% blocktrans %}Confirm import ({{ valid_count }}){% endblocktrans %}</button>
</form>
{% endif %}
</div>
</div>
{% endblock panel %}

View File

@@ -3,114 +3,194 @@
{% 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-sm btn-outline btn-info gap-2" href="{% url 'management:member_import_template' %}">{% lucide "download" size=14 %} {% trans "Download upload template" %}</a>
<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-sm btn-outline btn-info gap-2" href="{% url 'management:member_import' %}">{% lucide "upload" size=14 %} {% trans "Mass upload" %}</a>
<a class="btn btn-sm btn-outline gap-2" href="{% url 'management:family_create' %}">{% lucide "users" size=14 %} {% trans "Add family" %}</a>
<a class="btn btn-sm btn-outline gap-2" href="{% url 'management:member_create' %}">{% lucide "user-plus" size=14 %} {% trans "Add member" %}</a>
<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 panel %}
<form method="get" class="mb-2 flex items-center gap-2">
<label class="input grow sm:grow-0">
<span class="opacity-50">{% lucide "search" size=16 %}</span>
<input type="search" name="q" value="{{ search }}" placeholder="{% trans 'Search members ...' %}" class="input input-bordered w-full sm:max-w-xs">
</label>
{% if selected_kind != "member" %}<input type="hidden" name="kind" value="{{ selected_kind }}">{% endif %}
{# Icon-only below `sm`: label text next to a growing input is what forced this row to wrap on a phone. #}
<button class="btn btn-outline gap-2" type="submit" aria-label="{% trans 'Search' %}">{% lucide "search" size=16 %}<span class="hidden sm:inline">{% trans "Search" %}</span></button>
{% if search %}
<a class="btn gap-2" href="{% url "management:member_list" %}" aria-label="{% trans 'Clear filter' %}">{% lucide "x" size=16 %}<span class="hidden sm:inline">{% trans "Clear filter" %}</span></a>
{% endif %}
</form>
{% comment %}
Guardians -- parents attached to the club only through a child -- have no
fee and aren't counted as members, so members_visible_to() leaves them out
of the default list entirely. Without this filter that's silent: a parent
just registered via "Add family"/"Add parent" simply doesn't appear here,
with nothing on the page explaining why. ?kind= makes the exclusion an
explicit, reversible choice instead.
{% endcomment %}
<div class="mb-4 flex flex-col gap-2">
<div class="join w-full">
<a href="{% querystring kind=None page=None %}" class="btn btn-sm join-item flex-1 {% if selected_kind == "member" %}btn-primary{% endif %}">{% trans "Members" %}</a>
<a href="{% querystring kind="guardian" page=None %}" class="btn btn-sm join-item flex-1 {% if selected_kind == "guardian" %}btn-primary{% endif %}">{% trans "Guardians" %}</a>
<a href="{% querystring kind="both" page=None %}" class="btn btn-sm join-item flex-1 {% if selected_kind == "both" %}btn-primary{% endif %}">{% trans "Both" %}</a>
</div>
<span class="text-xs opacity-60">{% trans "Guardians are parents linked only through a child -- no fee, not counted as a member." %}</span>
</div>
<div class="card bg-base-100 shadow">
<div class="card-body">
{# table-cards: below `md` each row reads as a card instead of forcing a horizontal scroll -- see the .table-cards rule in app.css. #}
<div class="overflow-x-auto">
<table class="table table-cards">
<thead>
<tr>
<th>{% trans "Name" %}</th>
<th>{% trans "Email" %}</th>
<th>{% trans "Family" %}</th>
<th>{% trans "Status" %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for member in members %}
<tr>
<td><a class="link link-hover font-semibold" href="{% url 'management:member_detail' member.pk %}">{{ member.last_name }}, {{ member.first_name }}</a></td>
<td data-label="{% trans 'Email' %}">{{ member.contact_email|default:"-" }}</td>
<td data-label="{% trans 'Family' %}">
{% if member.family_memberships_display %}
<div class="flex flex-col gap-1">
{% for fm in member.family_memberships_display %}
<div>
<a class="btn btn-outline btn-sm" href="{% url 'management:family_detail' fm.family.pk %}">{% lucide "users" size=14 %} {{ fm.family }} {% lucide "arrow-right" size=14 %}</a>
</div>
{% endfor %}
</div>
{% else %}
<span class="opacity-40">-</span>
{% endif %}
</td>
<td data-label="{% trans 'Status' %}">
{% 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-neutral">{% trans "unknown" %}</span>
{% endif %}
</td>
<td class="text-right">
{% if is_club_admin %}
<div class="flex flex-wrap justify-end gap-1">
<a class="btn btn-outline btn-sm" href="{% url 'management:member_detail' member.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('{{ member.pk|dom_id:"member_delete_modal" }}').showModal()" aria-label="{% trans 'Delete' %}">{% lucide "trash-2" size=14 %} {% trans "Delete" %}</button>
</div>
{% endif %}
</td>
</tr>
{% empty %}
<tr>
<td colspan="5" class="text-center opacity-60">{% trans "No members yet." %}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% 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 is_club_admin %}
{% if can_manage_members %}
{% trans "Delete member" as delete_member_title %}
{% trans "Delete" as delete_label %}
{% for member in members %}

View File

@@ -1,77 +1,33 @@
{% extends "management/base.html" %}
{% load i18n lucide ui %}
{% block heading %}{% trans "Memberships" %}{% endblock heading %}
{% block subheading %}{% if current_season %}{% blocktrans %}Fee status for season{% endblocktrans %}{% endif %}{% endblock subheading %}
{% block heading %}{% trans "Dues & billing" %}{% endblock heading %}
{% block topbar_context %}
{% if selected_season %}
<span class="font-mono text-sm text-muted">{{ selected_season.start_date|date:"Y" }}&ndash;{{ selected_season.end_date|date:"Y" }}</span>
{% endif %}
{% endblock topbar_context %}
{% block actions %}
<a class="btn btn-outline gap-2" href="{% url 'management:membership_export_pdf' %}?{{ request.GET.urlencode }}">{% lucide "file-down" size=16 %} {% trans "Export to PDF" %}</a>
{% endblock actions %}
{% block panel %}
{% if not current_season %}
<div class="alert alert-warning mb-6">
{% lucide "calendar-x" size=20 %}
<span>{% trans "No season covers today, so there's nothing to show fee status for yet." %}</span>
</div>
{% else %}
<div class="mb-6 grid gap-4 sm:grid-cols-2 lg:grid-cols-6">
<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 "users" size=16 %} {% trans "Registered" %}</div>
<div class="text-4xl font-bold tabular-nums font-mono">{{ kpi_total }}</div>
<div class="text-xs opacity-60">{% trans "This season" %}</div>
</div>
{% block filter_strip %}
<div class="border-b border-line bg-white px-7 py-3">
<form method="get" class="flex flex-row flex-wrap items-center gap-2">
<div class="relative">
<span class="pointer-events-none absolute top-1/2 left-3 -translate-y-1/2 text-dim">{% lucide "search" size=15 %}</span>
<input type="search" name="q" value="{{ search }}" placeholder="{% trans 'Search members ...' %}" class="input w-56 pl-9">
</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 "Paid" %}</div>
<div class="text-4xl font-bold tabular-nums font-mono">{{ kpi_paid }}</div>
</div>
</div>
<div class="card bg-base-100 shadow border-l-4 {% if kpi_partial %}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 paid" %}</div>
<div class="text-4xl font-bold tabular-nums font-mono">{{ kpi_partial }}</div>
</div>
</div>
<div class="card bg-base-100 shadow border-l-4 {% if kpi_unpaid %}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-x" size=16 %} {% trans "Unpaid" %}</div>
<div class="text-4xl font-bold tabular-nums font-mono">{{ kpi_unpaid }}</div>
</div>
</div>
<div class="card bg-base-100 shadow border-l-4 border-neutral">
<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 "Waived" %}</div>
<div class="text-4xl font-bold tabular-nums font-mono">{{ kpi_waived }}</div>
</div>
</div>
<div class="card bg-base-100 shadow border-l-4 {% if kpi_paid_rate is None %}border-info{% elif kpi_paid_rate < 50 %}border-error{% elif kpi_paid_rate < 85 %}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 "percent" size=16 %} {% trans "Paid rate" %}</div>
<div class="text-4xl font-bold tabular-nums font-mono">
{% if kpi_paid_rate is None %}{% trans "N/A" %}{% else %}{{ kpi_paid_rate }}%{% endif %}
</div>
</div>
</div>
</div>
{% endif %}
<form method="get" class="mb-4">
<div class="grid grid-cols-1 gap-2 sm:grid-cols-2 lg:flex lg:flex-row lg:flex-wrap lg:items-center">
<label class="input w-full lg:w-auto">
<span class="opacity-50">{% lucide "search" size=16 %}</span>
<input type="search" name="q" value="{{ search }}" placeholder="{% trans 'Search members ...' %}" class="input input-bordered w-full">
</label>
<select name="season" class="select select-bordered w-full lg:w-auto">
<select name="season" class="select w-48">
{% 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>
<select name="fee_status" class="select select-bordered w-full lg:w-auto">
<select name="fee_status" class="select w-56">
<option value="not_paid" {% if selected_fee_status == "not_paid" %}selected{% endif %}>{% trans "Not paid (unpaid or partially)" %}</option>
{% for value, label in fee_status_choices %}
<option value="{{ value }}" {% if value == selected_fee_status %}selected{% endif %}>{{ label|capfirst }}</option>
@@ -79,119 +35,153 @@
<option value="all" {% if selected_fee_status == "all" %}selected{% endif %}>{% trans "All" %}</option>
</select>
<select name="status" class="select select-bordered w-full lg:w-auto">
<select name="status" class="select w-40">
<option value="all" {% if selected_status == "all" %}selected{% endif %}>{% trans "All statuses" %}</option>
{% for value, label in status_choices %}
<option value="{{ value }}" {% if value == selected_status %}selected{% endif %}>{{ label|capfirst }}</option>
{% endfor %}
</select>
<select name="team" class="select select-bordered w-full lg:w-auto">
<select name="team" class="select w-44">
<option value="" {% if not selected_team %}selected{% endif %}>{% trans "All teams" %}</option>
{% for team in teams %}
<option value="{{ team.pk }}" {% if team.pk|stringformat:"s" == selected_team %}selected{% endif %}>{{ team }}</option>
{% endfor %}
</select>
<div class="col-span-full flex gap-2 lg:col-span-1 lg:contents">
<button class="btn btn-outline grow gap-2 lg:grow-0" type="submit">{% lucide "filter" size=16 %} {% trans "Filter" %}</button>
<a class="btn grow gap-2 lg:grow-0" href="{% url 'management:membership_list' %}">{% lucide "x" size=16 %} {% trans "Reset" %}</a>
<button class="btn btn-outline gap-2" type="submit">{% lucide "filter" size=16 %} {% trans "Filter" %}</button>
<a class="btn btn-ghost gap-2" href="{% url 'management:membership_list' %}">{% lucide "x" size=16 %} {% trans "Reset" %}</a>
</form>
</div>
{% endblock filter_strip %}
{% block panel %}
{% if not current_season %}
<div class="alert alert-warning">
{% lucide "calendar-x" size=20 %}
<span>{% trans "No season covers today, so there's nothing to show fee status for yet." %}</span>
</div>
{% else %}
<div class="grid grid-cols-2 gap-3.5 sm:grid-cols-3 lg:grid-cols-6">
<div class="card border-hairline bg-ink p-4">
<div class="font-display text-xs font-bold tracking-[.12em] text-on-dark uppercase">{% trans "Registered" %}</div>
<div class="mt-1 font-display text-[34px] leading-none font-extrabold tabular-nums text-white">{{ kpi_total }}</div>
<div class="mt-1 text-[13px] text-on-dark-dim">{% trans "This season" %}</div>
</div>
<div class="card border-l-4 border-ok p-4">
<div class="font-display text-xs font-bold tracking-[.12em] text-muted uppercase">{% trans "Paid" %}</div>
<div class="mt-1 font-display text-[34px] leading-none font-extrabold tabular-nums text-ok">{{ kpi_paid }}</div>
</div>
<div class="card border-l-4 p-4 {% if kpi_partial %}border-warn{% else %}border-ok{% endif %}">
<div class="font-display text-xs font-bold tracking-[.12em] text-muted uppercase">{% trans "Partially paid" %}</div>
<div class="mt-1 font-display text-[34px] leading-none font-extrabold tabular-nums {% if kpi_partial %}text-warn{% else %}text-ink{% endif %}">{{ kpi_partial }}</div>
</div>
<div class="card border-l-4 p-4 {% if kpi_unpaid %}border-club{% else %}border-ok{% endif %}">
<div class="font-display text-xs font-bold tracking-[.12em] text-muted uppercase">{% trans "Unpaid" %}</div>
<div class="mt-1 font-display text-[34px] leading-none font-extrabold tabular-nums {% if kpi_unpaid %}text-club{% else %}text-ink{% endif %}">{{ kpi_unpaid }}</div>
</div>
<div class="card border-l-4 border-edge p-4">
<div class="font-display text-xs font-bold tracking-[.12em] text-muted uppercase">{% trans "Waived" %}</div>
<div class="mt-1 font-display text-[34px] leading-none font-extrabold tabular-nums text-ink">{{ kpi_waived }}</div>
</div>
<div class="card border-l-4 p-4 {% if kpi_paid_rate is None %}border-info{% elif kpi_paid_rate < 50 %}border-club{% elif kpi_paid_rate < 85 %}border-warn{% else %}border-ok{% endif %}">
<div class="font-display text-xs font-bold tracking-[.12em] text-muted uppercase">{% trans "Paid rate" %}</div>
<div class="mt-1 font-display text-[34px] leading-none font-extrabold tabular-nums {% if kpi_paid_rate is None %}text-info{% elif kpi_paid_rate < 50 %}text-club{% elif kpi_paid_rate < 85 %}text-warn{% else %}text-ok{% endif %}">
{% if kpi_paid_rate is None %}{% trans "N/A" %}{% else %}{{ kpi_paid_rate }}%{% endif %}
</div>
</div>
</div>
</form>
{% endif %}
<form method="post" action="{% url 'management:membership_mark_paid' %}" id="membership-form">
{% csrf_token %}
<input type="hidden" name="next" value="{{ request.get_full_path }}">
<div class="card bg-base-100 shadow">
<div class="card-body">
<div class="flex flex-wrap items-center justify-between gap-2 mb-2">
<span class="text-sm opacity-70 font-semibold">
{% blocktrans count counter=memberships|length %}{{ counter }} membership{% plural %}{{ counter }} memberships{% endblocktrans %}
</span>
<button class="btn btn-success btn-sm gap-2" type="submit">{% lucide "circle-check" size=14 %} {% trans "Mark selected as paid" %}</button>
</div>
<div class="card overflow-hidden">
<div class="flex flex-wrap items-center justify-between gap-2 border-b border-line px-4.5 py-3.5">
<span class="font-display text-sm font-bold tracking-wide text-muted uppercase">
{% blocktrans count counter=memberships|length %}{{ counter }} membership{% plural %}{{ counter }} memberships{% endblocktrans %}
</span>
<button class="btn btn-success btn-sm gap-2" type="submit">{% lucide "circle-check" size=14 %} {% trans "Mark selected as paid" %}</button>
</div>
{# table-cards: this table is dense enough (10+ columns) that horizontal scroll alone stops being usable below `md` -- see the .table-cards rule in app.css. #}
<div class="overflow-x-auto">
<table class="table table-cards">
<thead>
<div class="overflow-x-auto">
<table class="table">
<thead>
<tr>
<th><input type="checkbox" class="checkbox" id="select-all-memberships"></th>
<th>{% trans "Name" %}</th>
<th>{% trans "Email" %}</th>
<th>{% trans "Family" %}</th>
<th>{% trans "Status" %}</th>
<th>{% trans "Fee status" %}</th>
<th>{% trans "Owed" %}</th>
<th>{% trans "Paid" %}</th>
<th>{% trans "License" %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for membership in memberships %}
<tr>
<th><input type="checkbox" class="checkbox" id="select-all-memberships"></th>
<th>{% trans "Name" %}</th>
<th>{% trans "Email" %}</th>
<th>{% trans "Family" %}</th>
<th>{% trans "Status" %}</th>
<th>{% trans "Fee status" %}</th>
<th>{% trans "Owed" %}</th>
<th>{% trans "Paid" %}</th>
<th>{% trans "License" %}</th>
<th></th>
<td><input type="checkbox" class="checkbox membership-row-checkbox" name="membership_ids" value="{{ membership.pk }}"></td>
<td><a class="link link-hover font-semibold" href="{% url 'management:member_detail' membership.member.pk %}">{{ membership.member.last_name }}, {{ membership.member.first_name }}</a></td>
<td>{{ membership.member.contact_email|default:"-" }}</td>
<td>
{% if membership.member.family_memberships_display %}
{% for fm in membership.member.family_memberships_display %}
{{ fm.family }}{% if not forloop.last %}, {% endif %}
{% endfor %}
{% else %}
<span class="text-dim">-</span>
{% endif %}
</td>
<td>
<span class="badge badge-sm
{% if membership.status == "active" %}badge-success
{% elif membership.status == "pending" %}badge-warning
{% elif membership.status == "cancelled" %}badge-error
{% else %}badge-neutral{% endif %}">
{{ membership.get_status_display }}
</span>
</td>
<td>
<span class="badge badge-sm
{% if membership.fee_status == "paid" %}badge-success
{% elif membership.fee_status == "partially_paid" %}badge-warning
{% elif membership.fee_status == "unpaid" %}badge-error
{% else %}badge-neutral{% endif %}">
{{ membership.get_fee_status_display }}
</span>
</td>
<td class="font-mono tabular-nums">&euro; {{ membership.fee_amount }}</td>
<td class="font-mono tabular-nums">
&euro; {{ membership.amount_paid }}
{% if membership.record_payment_form and membership.fee_amount %}
<div class="font-sans text-xs text-muted">{% blocktrans with remaining=membership.remaining_balance_display %}{{ remaining }} left{% endblocktrans %}</div>
{% endif %}
</td>
<td>{{ membership.license|default:"-" }}</td>
<td class="text-right">
{% if membership.record_payment_form %}
<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:"record_payment_modal" }}').showModal()">
{% lucide "receipt" size=12 %} {% trans "Record payment" %}
</button>
<button class="btn btn-sm btn-outline btn-success" type="submit" form="{{ membership.pk|dom_id:"mark_fully_paid_form" }}">
{% lucide "circle-check" size=12 %} {% trans "Mark fully paid" %}
</button>
</div>
{% endif %}
</td>
</tr>
</thead>
<tbody>
{% for membership in memberships %}
<tr>
<td><input type="checkbox" class="checkbox membership-row-checkbox" name="membership_ids" value="{{ membership.pk }}"></td>
<td><a class="link link-hover font-semibold" href="{% url 'management:member_detail' membership.member.pk %}">{{ membership.member.last_name }}, {{ membership.member.first_name }}</a></td>
<td data-label="{% trans 'Email' %}">{{ membership.member.contact_email|default:"-" }}</td>
<td data-label="{% trans 'Family' %}">
{% if membership.member.family_memberships_display %}
{% for fm in membership.member.family_memberships_display %}
{{ fm.family }} {% if not forloop.last %},{% endif %}
{% endfor %}
{% else %}
<span class="opacity-40">-</span>
{% endif %}
</td>
<td data-label="{% trans 'Status' %}">
<span class="badge badge-sm
{% if membership.status == "active" %}badge-success
{% elif membership.status == "pending" %}badge-warning
{% elif membership.status == "cancelled" %}badge-error
{% else %}badge-neutral{% endif %}">
{{ membership.get_status_display }}
</span>
</td>
<td data-label="{% trans 'Fee status' %}">
<span class="badge badge-sm
{% if membership.fee_status == "paid" %}badge-success
{% elif membership.fee_status == "partially_paid" %}badge-warning
{% elif membership.fee_status == "unpaid" %}badge-error
{% else %}badge-neutral{% endif %}">
{{ membership.get_fee_status_display }}
</span>
</td>
<td data-label="{% trans 'Owed' %}" class="tabular-nums">&euro; {{ membership.fee_amount }}</td>
<td data-label="{% trans 'Paid' %}" class="tabular-nums">
&euro; {{ membership.amount_paid }}
{% if membership.record_payment_form and membership.fee_amount %}
<div class="text-xs opacity-60">{% blocktrans with remaining=membership.remaining_balance_display %}{{ remaining }} left{% endblocktrans %}</div>
{% endif %}
</td>
<td data-label="{% trans 'License' %}">{{ membership.license|default:"-" }}</td>
<td class="text-right">
{% if membership.record_payment_form %}
<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:"record_payment_modal" }}').showModal()">
{% lucide "receipt" size=12 %} {% trans "Record payment" %}
</button>
<button class="btn btn-sm btn-outline btn-success" type="submit" form="{{ membership.pk|dom_id:"mark_fully_paid_form" }}">
{% lucide "circle-check" size=12 %} {% trans "Mark fully paid" %}
</button>
</div>
{% endif %}
</td>
</tr>
{% empty %}
<tr>
<td colspan="10" class="text-center opacity-60">{% trans "Nobody matches these filters." %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% empty %}
<tr>
<td colspan="10" class="text-center text-muted">{% trans "Nobody matches these filters." %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</form>

View File

@@ -2,16 +2,17 @@
{% load i18n lucide ui %}
{% block heading %}{{ news_item.title }}{% endblock heading %}
{% block subheading %}
{% block topbar_context %}
{% if news_item.status == "draft" %}
{% trans "Draft" %}
<span class="badge">{% trans "Draft" %}</span>
{% elif news_item.is_scheduled %}
{% blocktrans with date=news_item.published_at %}Scheduled for {{ date }}{% endblocktrans %}
<span class="badge badge-info">{% blocktrans with date=news_item.published_at %}Scheduled for {{ date }}{% endblocktrans %}</span>
{% else %}
{% blocktrans with date=news_item.published_at %}Published {{ date }}{% endblocktrans %}
<span class="badge badge-success">{% blocktrans with date=news_item.published_at %}Published {{ date }}{% endblocktrans %}</span>
{% endif %}
· {{ news_item.get_visibility_display }}
{% endblock subheading %}
<span class="font-mono text-xs text-dim">{{ news_item.get_visibility_display }}</span>
{% endblock topbar_context %}
{% block actions %}
{% if can_edit %}
@@ -28,47 +29,57 @@
{% endblock actions %}
{% block panel %}
<div class="card bg-base-100 shadow">
<div class="card">
<div class="card-body">
{% if news_item.teams.all %}
<div class="flex flex-wrap gap-2 mb-2">
<div class="mb-1 flex flex-wrap gap-2">
{% for team in news_item.teams.all %}
<span class="badge badge-neutral">{{ team.short_name }}</span>
{% endfor %}
</div>
{% else %}
<div class="mb-1 font-display text-xs font-bold tracking-[.14em] text-club uppercase">{% trans "Club-wide" %}</div>
{% endif %}
<div class="h-[3px] w-14 bg-club"></div>
<p class="whitespace-pre-line text-[15px] leading-relaxed text-ink">{{ news_item.body }}</p>
{% if news_item.created_by %}
<div class="mt-2 flex items-center gap-2.5 border-t border-line pt-3.5 text-sm text-muted">
<span class="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-steel font-display text-xs font-extrabold text-white">{{ news_item.created_by.first_name|slice:":1" }}{{ news_item.created_by.last_name|slice:":1" }}</span>
{% blocktrans with name=news_item.created_by %}Posted by {{ name }}{% endblocktrans %}
</div>
{% endif %}
<p class="whitespace-pre-line">{{ news_item.body }}</p>
</div>
</div>
{% if news_item.title_en or news_item.body_en %}
<div class="card bg-base-100 shadow mt-4">
<div class="card mt-4">
<div class="card-body">
<h2 class="card-title text-base">{% trans "English" %}</h2>
{% if news_item.title_en %}<p class="font-semibold">{{ news_item.title_en }}</p>{% endif %}
{% if news_item.body_en %}<p class="whitespace-pre-line">{{ news_item.body_en }}</p>{% endif %}
<h2 class="card-title">{% lucide "languages" size=18 %} {% trans "English" %}</h2>
{% if news_item.title_en %}<p class="font-display text-lg font-extrabold text-ink uppercase">{{ news_item.title_en }}</p>{% endif %}
{% if news_item.body_en %}<p class="whitespace-pre-line text-[15px] leading-relaxed text-ink">{{ news_item.body_en }}</p>{% endif %}
</div>
</div>
{% endif %}
<div class="card bg-base-100 shadow mt-4">
<div class="card mt-4">
<div class="card-body">
<div class="flex flex-wrap items-center justify-between gap-2">
<h2 class="card-title text-base">{% trans "Photos" %}</h2>
<h2 class="card-title">{% lucide "image" size=18 %} {% trans "Photos" %}</h2>
{% if can_edit %}
<button class="btn btn-outline btn-sm gap-2" type="button" onclick="document.getElementById('add_photos_modal').showModal()">{% lucide "image-plus" size=14 %} {% trans "Add photos" %}</button>
{% endif %}
</div>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mt-2">
<div class="mt-2 grid grid-cols-2 gap-4 md:grid-cols-4">
{% for photo in news_item.photos.all %}
<div class="relative">
<img class="rounded-box aspect-square object-cover w-full" src="{{ photo.image.url }}" alt="">
<img class="aspect-square w-full rounded-box border border-line object-cover" src="{{ photo.image.url }}" alt="">
{% if photo.is_main %}
<span class="badge badge-success badge-sm absolute top-1 left-1">{% trans "Main" %}</span>
<span class="badge badge-success badge-sm absolute top-1.5 left-1.5">{% trans "Main" %}</span>
{% endif %}
{% if can_edit %}
<div class="flex flex-wrap gap-1 mt-1">
<div class="mt-1 flex flex-wrap gap-1">
{% if not photo.is_main %}
<form method="post" action="{% url 'management:news_photo_set_main' news_item.pk photo.pk %}">
{% csrf_token %}
@@ -85,7 +96,7 @@
{% endif %}
</div>
{% empty %}
<p class="opacity-60 col-span-full">{% trans "No photos yet." %}</p>
<p class="col-span-full text-sm text-muted">{% trans "No photos yet." %}</p>
{% endfor %}
</div>
</div>

View File

@@ -4,7 +4,7 @@
{% block heading %}{% if update_view %}{% blocktrans %}Edit {{ object }}{% endblocktrans %}{% else %}{% trans "New news item" %}{% endif %}{% endblock heading %}
{% block panel %}
<div class="card w-full bg-base-100 shadow">
<div class="card w-full">
<div class="card-body">
<form method="post">
{% csrf_token %}
@@ -16,25 +16,25 @@
{% endfor %}
{% if not update_view %}
<p class="opacity-70 text-sm mb-2">{% trans "Photos can be added once the news item is created." %}</p>
<p class="mb-2 text-sm text-muted">{% trans "Photos can be added once the news item is created." %}</p>
{% endif %}
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
{% form_field form.title %}
{% form_field form.title_en %}
</div>
<div class="grid grid-cols-1 gap-4 mt-4">
<div class="mt-4 grid grid-cols-1 gap-4">
{% form_field form.teams %}
{% form_field form.visibility %}
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
<div class="mt-4 grid grid-cols-1 gap-4 md:grid-cols-2">
{% form_field form.body %}
{% form_field form.body_en %}
</div>
<div class="card-actions justify-start pt-2 mt-2">
<div class="mt-4 flex items-center gap-2 border-t border-line pt-4">
<a class="btn btn-outline gap-2" href="{% if update_view %}{% url "management:news_detail" object.pk %}{% else %}{% url "management:news_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>

View File

@@ -5,65 +5,52 @@
{% block actions %}
{% if can_add_news %}
<a class="btn btn-outline gap-2" href="{% url 'management:news_create' %}">{% lucide "plus" size=16 %} {% trans "New news item" %}</a>
<a class="btn btn-primary gap-2" href="{% url 'management:news_create' %}">{% lucide "plus" size=16 %} {% trans "New news item" %}</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 table-cards">
<thead>
<tr>
<th>{% trans "Title" %}</th>
<th>{% trans "Teams" %}</th>
<th>{% trans "Visibility" %}</th>
<th>{% trans "Status" %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for news_item in news_items %}
<tr>
<td><a class="link link-hover font-semibold" href="{% url 'management:news_detail' news_item.pk %}">{{ news_item.title }}</a></td>
<td data-label="{% trans 'Teams' %}">
<div class="flex flex-wrap gap-2">
{% for team in news_item.teams.all %}
<span class="badge badge-neutral badge-sm">{{ team.short_name }}</span>
{% empty %}
<span class="opacity-60">{% trans "Club-wide" %}</span>
{% endfor %}
</div>
</td>
<td data-label="{% trans 'Visibility' %}">{{ news_item.get_visibility_display }}</td>
<td data-label="{% trans 'Status' %}">
{% if news_item.status == "draft" %}
<span class="badge badge-neutral badge-sm">{% trans "Draft" %}</span>
{% elif news_item.is_scheduled %}
<span class="badge badge-warning badge-sm">{% blocktrans with date=news_item.published_at %}Scheduled for {{ date }}{% endblocktrans %}</span>
{% else %}
<span class="badge badge-success badge-sm">{% trans "Published" %}</span>
{% endif %}
</td>
<td class="text-right">
{% if news_item.can_edit %}
<div class="flex flex-wrap justify-end gap-1">
<a class="btn btn-outline btn-sm" href="{% url 'management:news_detail' news_item.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('{{ news_item.pk|dom_id:"news_delete_modal" }}').showModal()" aria-label="{% trans 'Delete' %}">{% lucide "trash-2" size=14 %} {% trans "Delete" %}</button>
</div>
{% endif %}
</td>
</tr>
<div class="card overflow-hidden divide-y">
{% for news_item in news_items %}
<div class="flex items-center gap-3.5 px-4.5 py-3.5 {% if news_item.status == 'draft' %}border-l-[3px] border-club bg-row-sel{% endif %}">
<div class="min-w-0 flex-1">
<div class="mb-1 flex flex-wrap items-center gap-2">
{% if news_item.status == "draft" %}
<span class="badge badge-sm">{% trans "Draft" %}</span>
{% elif news_item.is_scheduled %}
<span class="badge badge-info badge-sm">{% blocktrans with date=news_item.published_at %}Scheduled for {{ date }}{% endblocktrans %}</span>
{% else %}
<span class="badge badge-success badge-sm">{% trans "Published" %}</span>
{% endif %}
<span class="font-mono text-xs text-dim">
{% if news_item.status == "draft" %}
{% blocktrans with time=news_item.modified|timesince %}Edited {{ time }} ago{% endblocktrans %}
{% else %}
{{ news_item.published_at|date:"j M Y" }}
{% endif %}
</span>
</div>
<a class="block truncate font-display text-lg font-extrabold text-ink uppercase hover:text-club" href="{% url 'management:news_detail' news_item.pk %}">{{ news_item.title }}</a>
<div class="mt-0.5 flex flex-wrap items-center gap-2 text-[13px] text-muted">
{% if news_item.created_by %}<span>{{ news_item.created_by }}</span>&middot;{% endif %}
{% for team in news_item.teams.all %}
<span class="badge badge-outline badge-xs">{{ team.short_name }}</span>
{% empty %}
<tr>
<td colspan="5" class="text-center opacity-60">{% trans "No news items yet." %}</td>
</tr>
<span>{% trans "Club-wide" %}</span>
{% endfor %}
</tbody>
</table>
&middot; <span>{{ news_item.get_visibility_display }}</span>
</div>
</div>
{% if news_item.can_edit %}
<div class="flex shrink-0 flex-wrap justify-end gap-1">
<a class="btn btn-outline btn-sm" href="{% url 'management:news_detail' news_item.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('{{ news_item.pk|dom_id:"news_delete_modal" }}').showModal()" aria-label="{% trans 'Delete' %}">{% lucide "trash-2" size=14 %} {% trans "Delete" %}</button>
</div>
{% endif %}
</div>
</div>
{% empty %}
<div class="px-4.5 py-10 text-center text-sm text-muted">{% trans "No news items yet." %}</div>
{% endfor %}
</div>
{% include "management/_pagination.html" %}

View File

@@ -0,0 +1,57 @@
{% extends "management/base.html" %}
{% load i18n lucide ui %}
{% block panel_title %}{% if update_view %}{% trans "Edit requirement" %}{% else %}{% trans "New requirement" %}{% endif %}{% endblock panel_title %}
{% block heading %}{% if update_view %}{% blocktrans %}Edit {{ object }}{% endblocktrans %}{% else %}{% trans "New requirement" %}{% endif %}{% endblock heading %}
{% block panel %}
<div class="card">
<div class="card-body">
<form method="post">
{% csrf_token %}
{% for error in form.non_field_errors %}
<div class="alert alert-error">
{% lucide "circle-x" size=18 %}
<span>{{ error }}</span>
</div>
{% endfor %}
<div class="grid grid-cols-2 gap-4">
{% for field in form %}
{% if field.name != "blocked_event_kinds" %}
<div class="{% if field.widget_type == "textarea" or field.widget_type == "clearablefile" %}col-span-2{% endif %}">
{% form_field field %}
</div>
{% endif %}
{% endfor %}
{% comment %}
Hand-rendered: field.widget_type for CheckboxSelectMultiple
("checkboxselectmultiple") isn't one templatetags/field.html
recognises, so {% form_field %} would fall through to its
plain-input case and render an unusable <input type=
"checkboxselectmultiple">.
{% endcomment %}
<div class="form-control col-span-2">
<label class="label-text">{{ form.blocked_event_kinds.label }}</label>
<div class="flex flex-wrap gap-3">
{% for choice in form.blocked_event_kinds %}
<label class="flex items-center gap-1.5 text-sm text-ink">
{{ choice.tag }} {{ choice.choice_label }}
</label>
{% endfor %}
</div>
<span class="label-text-alt mt-1">{{ form.blocked_event_kinds.help_text }}</span>
{% for error in form.blocked_event_kinds.errors %}<div class="text-xs text-club-dark">{{ error }}</div>{% endfor %}
</div>
</div>
<div class="mt-4 flex justify-start gap-2">
<a class="btn btn-outline gap-2" href="{% url "management:onboarding_requirement_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 %}

View File

@@ -0,0 +1,74 @@
{% extends "management/base.html" %}
{% load i18n lucide ui %}
{% comment %}
What this club requires from every member after they sign up or renew --
see club/models.py's OnboardingRequirement docstring. Order matters (shown
as typed in `order`, ascending) -- it's the sequence staff see on a
member's checklist. No dedicated mockup screen for this (new feature, not
in the original design file) -- extrapolates the card/table vocabulary
used across Settings.
{% endcomment %}
{% block panel_title %}{% trans "Onboarding requirements" %}{% endblock panel_title %}
{% block heading %}{% trans "Onboarding requirements" %}{% endblock heading %}
{% block topbar_context %}
<span class="badge badge-neutral">{% blocktrans count counter=requirements|length %}{{ counter }} requirement{% plural %}{{ counter }} requirements{% endblocktrans %}</span>
{% endblock topbar_context %}
{% block actions %}
<a class="btn btn-primary gap-2" href="{% url 'management:onboarding_requirement_create' %}">{% lucide "plus" size=16 %} {% trans "New requirement" %}</a>
{% endblock actions %}
{% block panel %}
<p class="text-sm text-muted">
{% trans "Shown as a checklist on every member's Documents tab. A member can be fully paid and still show as incomplete here until every active requirement is met — the two are tracked separately on purpose." %}
</p>
<div class="card overflow-hidden">
<table class="table">
<thead>
<tr>
<th>{% trans "Name" %}</th>
<th>{% trans "Description" %}</th>
<th>{% trans "Document required" %}</th>
<th>{% trans "Active" %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for requirement in requirements %}
<tr>
<td class="font-semibold">{{ requirement.name }}</td>
<td class="max-w-xs truncate text-muted">{{ requirement.description|default:"—" }}</td>
<td>
{% if requirement.requires_document %}<span class="badge badge-info">{% trans "Yes" %}</span>{% else %}<span class="text-dim">{% trans "No" %}</span>{% endif %}
</td>
<td>
{% if requirement.is_active %}<span class="badge badge-success">{% trans "Active" %}</span>{% else %}<span class="badge badge-ghost">{% trans "Inactive" %}</span>{% endif %}
</td>
<td class="text-right">
<div class="flex justify-end gap-1">
<a class="btn btn-outline btn-sm" href="{% url 'management:onboarding_requirement_update' requirement.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('{{ requirement.pk|dom_id:"requirement_delete_modal" }}').showModal()" aria-label="{% trans 'Delete' %}">{% lucide "trash-2" size=14 %} {% trans "Delete" %}</button>
</div>
</td>
</tr>
{% empty %}
<tr>
<td colspan="5" class="text-center text-dim">{% trans "No requirements set — every member's checklist is empty until you add one." %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% trans "Delete requirement" as delete_title %}
{% trans "Delete" as delete_label %}
{% for requirement in requirements %}
{% url 'management:onboarding_requirement_delete' requirement.pk as requirement_delete_url %}
{% blocktrans with name=requirement.name asvar delete_body %}Delete “{{ name }}”? Existing checklists keep whatever was already recorded for it, but no member will be asked for it again.{% endblocktrans %}
{% include "controlpanel/_confirm_modal.html" with modal_id=requirement.pk|dom_id:"requirement_delete_modal" title=delete_title body=delete_body action_url=requirement_delete_url submit_label=delete_label %}
{% endfor %}
{% endblock panel %}

View File

@@ -4,24 +4,24 @@
{% block heading %}{% if update_view %}{% blocktrans %}Edit {{ object }}{% endblocktrans %}{% else %}{% trans "New opponent" %}{% endif %}{% endblock heading %}
{% block panel %}
<div class="card w-full bg-base-100 shadow">
<div class="card w-full">
<div class="card-body">
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{% for error in form.non_field_errors %}
<div class="alert alert-error my-2">
<div class="alert alert-error">
<span>{{ error }}</span>
</div>
{% endfor %}
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
{% for field in form %}
{% form_field field %}
{% endfor %}
</div>
<div class="card-actions justify-start pt-2 mt-2">
<div class="card-actions justify-start pt-4">
<a class="btn btn-outline gap-2" href="{% url "management:opponent_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>

View File

@@ -8,41 +8,39 @@
{% endblock actions %}
{% block panel %}
<div class="card bg-base-100 shadow">
<div class="card-body">
<div class="overflow-x-auto">
<table class="table table-cards">
<thead>
<div class="card overflow-hidden">
<div class="overflow-x-auto">
<table class="table">
<thead>
<tr>
<th></th>
<th>{% trans "Name" %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for opponent in opponents %}
<tr>
<th></th>
<th>{% trans "Name" %}</th>
<th></th>
<td>
{% if opponent.logo %}
<img src="{{ opponent.logo.url }}" alt="" class="size-8 rounded object-contain">
{% endif %}
</td>
<td class="font-semibold text-ink">{{ opponent.name }}</td>
<td class="text-right">
<div class="flex justify-end gap-1">
<a class="btn btn-outline btn-sm" href="{% url 'management:opponent_update' opponent.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('{{ opponent.pk|dom_id:"opponent_delete_modal" }}').showModal()" aria-label="{% trans 'Delete' %}">{% lucide "trash-2" size=14 %} {% trans "Delete" %}</button>
</div>
</td>
</tr>
</thead>
<tbody>
{% for opponent in opponents %}
<tr>
<td>
{% if opponent.logo %}
<img src="{{ opponent.logo.url }}" alt="" class="size-8 rounded object-contain">
{% endif %}
</td>
<td class="font-semibold">{{ opponent.name }}</td>
<td class="text-right">
<div class="flex justify-end gap-1">
<a class="btn btn-outline btn-sm" href="{% url 'management:opponent_update' opponent.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('{{ opponent.pk|dom_id:"opponent_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 opponents yet." %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% empty %}
<tr>
<td colspan="3" class="py-8 text-center text-muted">{% trans "No opponents yet." %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>

View File

@@ -2,68 +2,69 @@
{% load i18n lucide static ui %}
{% block heading %}{% trans "Parent claims" %}{% endblock heading %}
{% block subheading %}{% trans "Parents asking to be linked to a child the club already has on file." %}{% endblock subheading %}
{% block topbar_context %}
{% if pending %}
<span class="badge badge-error">{% blocktrans count counter=pending|length %}{{ counter }} waiting{% plural %}{{ counter }} waiting{% endblocktrans %}</span>
{% endif %}
{% endblock topbar_context %}
{% block panel %}
<div class="card bg-base-100 shadow mb-4">
<div class="card-body">
<h2 class="card-title text-base">{% lucide "inbox" size=18 %} {% trans "Waiting for review" %}</h2>
<p class="text-sm opacity-70">
{% blocktrans %}Check each request against your own records before approving. The shortlist only ever contains children who have nobody on file, so approving can never re-parent a child who already has one. An approved parent is added as a guardian: they get the login, but owe no fee and are not counted as a member.{% endblocktrans %}
</p>
<div class="card card-body">
<h2 class="card-title">{% lucide "inbox" size=18 %} {% trans "Waiting for review" %}</h2>
<p class="text-sm text-muted">
{% blocktrans %}Check each request against your own records before approving. The shortlist only ever contains children who have nobody on file, so approving can never re-parent a child who already has one. An approved parent is added as a guardian: they get the login, but owe no fee and are not counted as a member.{% endblocktrans %}
</p>
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4 mt-2">
{% for claim in pending %}
<div class="card border border-base-300">
<div class="card-body p-4 gap-3">
<div>
<div class="text-xs opacity-70">{% trans "Parent says they are" %}</div>
<div class="font-semibold">{{ claim.parent_name }}</div>
<div class="text-sm opacity-70 break-all">{{ claim.parent_email }}</div>
</div>
<div>
<div class="text-xs opacity-70">{% trans "Parent of" %}</div>
<div class="font-semibold">{{ claim.claimed_child_name }}</div>
<div class="text-sm opacity-70">{{ claim.child_date_of_birth|date:"j F Y" }}</div>
</div>
{% comment %}
size="small" + show_label=False on the dropdown: {% form_field %}
defaults to daisyUI's medium control height, taller than the btn-sm
used by Approve/Reject below -- without it the dropdown stands out
against the buttons either side of it.
justify-between (rather than everything in one flex-wrap run) keeps
Approve and Reject apart even as the row wraps on a narrow card --
Approve stays pinned left, Reject right, so the two are never one
stray click apart the way adjacent buttons would be. ms-auto on Reject
is the part that actually guarantees "right": once the row is narrow
enough that Approve and Reject fall onto separate flex-wrap lines,
justify-between has only one item per line and pins it to the *start*
-- margin-inline-start:auto keeps Reject flush right regardless of
which line it ends up sharing (or not sharing) with Approve.
{% endcomment %}
<div class="flex flex-wrap items-center justify-between gap-x-4 gap-y-2">
{% if claim.has_candidates %}
<form method="post" action="{% url 'management:parent_claim_approve' claim.pk %}" class="flex flex-wrap items-center gap-2">
{% csrf_token %}
<div class="min-w-40">{% form_field claim.review_form.child size="small" show_label=False %}</div>
<button class="btn btn-primary btn-sm gap-2" type="submit">{% lucide "check" size=14 %} {% trans "Approve" %}</button>
</form>
{% else %}
<p class="text-sm opacity-70">{% trans "No child without a parent matches this. Check the spelling and the date of birth with them before rejecting." %}</p>
{% endif %}
<button class="btn btn-outline btn-error btn-sm gap-2 ms-auto" type="button" onclick="document.getElementById('{{ claim.pk|dom_id:"claim_reject_modal" }}').showModal()">
{% lucide "x" size=14 %} {% trans "Reject" %}
</button>
</div>
</div>
<div class="mt-2 grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
{% for claim in pending %}
<div class="card card-body gap-3 border-l-[3px] border-l-info">
<div>
<div class="font-display text-xs font-bold tracking-[.1em] text-muted uppercase">{% trans "Parent says they are" %}</div>
<div class="font-semibold text-ink">{{ claim.parent_name }}</div>
<div class="text-sm break-all text-muted">{{ claim.parent_email }}</div>
</div>
{% empty %}
<p class="text-sm opacity-60 mt-2">{% trans "Nothing waiting." %}</p>
{% endfor %}
</div>
<div>
<div class="font-display text-xs font-bold tracking-[.1em] text-muted uppercase">{% trans "Parent of" %}</div>
<div class="font-semibold text-ink">{{ claim.claimed_child_name }}</div>
<div class="text-sm text-muted">{{ claim.child_date_of_birth|date:"j F Y" }}</div>
</div>
{% comment %}
size="small" + show_label=False on the dropdown: {% form_field %}
defaults to a taller control than the btn-sm used by Approve/Reject
below -- without it the dropdown stands out against the buttons
either side of it.
justify-between (rather than everything in one flex-wrap run) keeps
Approve and Reject apart even as the row wraps on a narrow card --
Approve stays pinned left, Reject right, so the two are never one
stray click apart the way adjacent buttons would be. ms-auto on Reject
is the part that actually guarantees "right": once the row is narrow
enough that Approve and Reject fall onto separate flex-wrap lines,
justify-between has only one item per line and pins it to the *start*
-- margin-inline-start:auto keeps Reject flush right regardless of
which line it ends up sharing (or not sharing) with Approve.
{% endcomment %}
<div class="flex flex-wrap items-center justify-between gap-x-4 gap-y-2">
{% if claim.has_candidates %}
<form method="post" action="{% url 'management:parent_claim_approve' claim.pk %}" class="flex flex-wrap items-center gap-2">
{% csrf_token %}
<div class="min-w-40">{% form_field claim.review_form.child size="small" show_label=False %}</div>
<button class="btn btn-primary btn-sm gap-2" type="submit">{% lucide "check" size=14 %} {% trans "Approve" %}</button>
</form>
{% else %}
<p class="text-sm text-muted">{% trans "No child without a parent matches this. Check the spelling and the date of birth with them before rejecting." %}</p>
{% endif %}
<button class="btn btn-outline btn-error btn-sm ms-auto gap-2" type="button" onclick="document.getElementById('{{ claim.pk|dom_id:"claim_reject_modal" }}').showModal()">
{% lucide "x" size=14 %} {% trans "Reject" %}
</button>
</div>
</div>
{% empty %}
<p class="mt-2 text-sm text-muted">{% trans "Nothing waiting." %}</p>
{% endfor %}
</div>
</div>
@@ -75,58 +76,52 @@
{% include "controlpanel/_modal_form.html" with modal_id=claim.pk|dom_id:"claim_reject_modal" title=reject_claim_title form=claim.reject_form action_url=reject_claim_url submit_label=reject_claim_submit_label submit_icon="x" blurb=reject_claim_blurb %}
{% endfor %}
<div class="card bg-base-100 shadow mb-4">
<div class="card-body">
<h2 class="card-title text-base">{% lucide "user-search" size=18 %} {% trans "Children with nobody on file" %}</h2>
<p class="text-sm opacity-70">{% trans "Imported without a parent. They stay here until someone claims them." %}</p>
<ul class="divide-y divide-base-200">
{% for child in awaiting_a_parent %}
<li class="py-2 flex flex-col gap-0.5 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
<a class="link link-hover" href="{% url 'management:member_detail' child.pk %}">{{ child }}</a>
<span class="text-sm opacity-70 sm:text-right">{{ child.date_of_birth|date:"j F Y"|default:"—" }}</span>
</li>
{% empty %}
<li class="py-2 text-sm opacity-60">{% trans "Every child has a parent linked." %}</li>
{% endfor %}
</ul>
<div class="card card-body mt-4">
<h2 class="card-title">{% lucide "user-search" size=18 %} {% trans "Children with nobody on file" %}</h2>
<p class="text-sm text-muted">{% trans "Imported without a parent. They stay here until someone claims them." %}</p>
<div class="divide-y">
{% for child in awaiting_a_parent %}
<div class="flex items-center justify-between gap-4 py-2">
<a class="link link-hover" href="{% url 'management:member_detail' child.pk %}">{{ child }}</a>
<span class="font-mono text-sm text-muted">{{ child.date_of_birth|date:"j F Y"|default:"—" }}</span>
</div>
{% empty %}
<p class="py-2 text-sm text-muted">{% trans "Every child has a parent linked." %}</p>
{% endfor %}
</div>
</div>
{% if reviewed %}
<div class="card bg-base-100 shadow">
<div class="card-body">
<h2 class="card-title text-base">{% lucide "history" size=18 %} {% trans "Already dealt with" %}</h2>
<div class="overflow-x-auto">
<table class="table table-cards">
<thead>
<tr>
<th>{% trans "Parent" %}</th>
<th>{% trans "Claimed" %}</th>
<th>{% trans "Outcome" %}</th>
<th>{% trans "Reviewed by" %}</th>
</tr>
</thead>
<tbody>
{% for claim in reviewed %}
<tr>
<td class="font-semibold">{{ claim.parent_name }}<div class="text-xs font-normal opacity-60 break-all">{{ claim.parent_email }}</div></td>
<td data-label="{% trans 'Claimed' %}">{{ claim.claimed_child_name }}</td>
<td data-label="{% trans 'Outcome' %}">
{% if claim.status == "approved" %}
<span class="badge badge-success badge-sm">{% trans "Approved" %}</span>
{% if claim.child %}<div class="text-xs opacity-60">{{ claim.child }}</div>{% endif %}
{% else %}
<span class="badge badge-error badge-sm">{% trans "Rejected" %}</span>
{% if claim.note %}<div class="text-xs opacity-60">{{ claim.note }}</div>{% endif %}
{% endif %}
</td>
<td data-label="{% trans 'Reviewed by' %}">{{ claim.reviewed_by|default:"—" }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="card card-body mt-4">
<h2 class="card-title">{% lucide "history" size=18 %} {% trans "Already dealt with" %}</h2>
<table class="table">
<thead>
<tr>
<th>{% trans "Parent" %}</th>
<th>{% trans "Claimed" %}</th>
<th>{% trans "Outcome" %}</th>
<th>{% trans "Reviewed by" %}</th>
</tr>
</thead>
<tbody>
{% for claim in reviewed %}
<tr>
<td class="font-semibold text-ink">{{ claim.parent_name }}<div class="text-xs font-normal break-all text-muted">{{ claim.parent_email }}</div></td>
<td>{{ claim.claimed_child_name }}</td>
<td>
{% if claim.status == "approved" %}
<span class="badge badge-success badge-sm">{% trans "Approved" %}</span>
{% if claim.child %}<div class="mt-1 text-xs text-muted">{{ claim.child }}</div>{% endif %}
{% else %}
<span class="badge badge-error badge-sm">{% trans "Rejected" %}</span>
{% if claim.note %}<div class="mt-1 text-xs text-muted">{{ claim.note }}</div>{% endif %}
{% endif %}
</td>
<td class="text-sm text-muted">{{ claim.reviewed_by|default:"—" }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
{% endblock panel %}

View File

@@ -4,31 +4,31 @@
{% block heading %}{% if update_view %}{% blocktrans %}Edit {{ object }}{% endblocktrans %}{% else %}{% trans "New position" %}{% endif %}{% endblock heading %}
{% block panel %}
<div class="card w-full bg-base-100 shadow">
<div class="card max-w-3xl">
<div class="card-body">
<form method="post">
{% csrf_token %}
{% for error in form.non_field_errors %}
<div class="alert alert-error my-2">
<div class="alert alert-error">
<span>{{ error }}</span>
</div>
{% endfor %}
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="grid grid-cols-2 gap-4">
{% form_field form.name %}
{% form_field form.short_name %}
{% form_field form.ordering %}
</div>
<div class="divider"></div>
<div class="my-2 border-t border-line"></div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="grid grid-cols-2 gap-4">
{% form_field form.staff_position %}
{% form_field form.management_position %}
</div>
<div class="card-actions justify-start pt-2 mt-2">
<div class="mt-4 flex gap-2">
<a class="btn btn-outline gap-2" href="{% url "management:position_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>

View File

@@ -5,55 +5,51 @@
{% block actions %}
{% if is_club_admin %}
<a class="btn btn-outline gap-2" href="{% url 'management:position_create' %}">{% lucide "plus" size=16 %} {% trans "New position" %}</a>
<a class="btn btn-primary gap-2" href="{% url 'management:position_create' %}">{% lucide "plus" size=16 %} {% trans "New position" %}</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 table-cards">
<thead>
<tr>
<th>{% trans "Ordering" %}</th>
<th>{% trans "Name" %}</th>
<th>{% trans "Short name" %}</th>
<th>{% trans "Staff position" %}</th>
<th>{% trans "Management position" %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for position in positions %}
<tr>
<td data-label="{% trans 'Ordering' %}">{{ position.ordering }}</td>
<td class="font-semibold">{{ position.name }}</td>
<td data-label="{% trans 'Short name' %}">{{ position.short_name }}</td>
<td data-label="{% trans 'Staff position' %}">
<span class="badge badge-sm {% if position.staff_position %}badge-success{% else %}badge-neutral{% endif %}">
{% if position.staff_position %}{% trans "Yes" %}{% else %}{% trans "No" %}{% endif %}
</span>
</td>
<td data-label="{% trans 'Management position' %}">
<span class="badge badge-sm {% if position.management_position %}badge-success{% else %}badge-neutral{% endif %}">
{% if position.management_position %}{% trans "Yes" %}{% else %}{% trans "No" %}{% endif %}
</span>
</td>
<td class="text-right">
{% if is_club_admin %}
<a class="btn btn-outline btn-sm gap-2" href="{% url 'management:position_update' position.pk %}">{% lucide "pencil" size=14 %} {% trans "Edit" %}</a>
{% endif %}
</td>
</tr>
{% empty %}
<tr>
<td colspan="6" class="text-center opacity-60">{% trans "No positions yet." %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="card overflow-hidden">
<table class="table">
<thead>
<tr>
<th>{% trans "Ordering" %}</th>
<th>{% trans "Name" %}</th>
<th>{% trans "Short name" %}</th>
<th>{% trans "Staff position" %}</th>
<th>{% trans "Management position" %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for position in positions %}
<tr>
<td class="font-mono text-muted">{{ position.ordering }}</td>
<td class="font-semibold text-ink">{{ position.name }}</td>
<td class="font-mono text-muted">{{ position.short_name }}</td>
<td>
<span class="badge badge-sm {% if position.staff_position %}badge-success{% else %}badge-ghost{% endif %}">
{% if position.staff_position %}{% trans "Yes" %}{% else %}{% trans "No" %}{% endif %}
</span>
</td>
<td>
<span class="badge badge-sm {% if position.management_position %}badge-success{% else %}badge-ghost{% endif %}">
{% if position.management_position %}{% trans "Yes" %}{% else %}{% trans "No" %}{% endif %}
</span>
</td>
<td class="text-right">
{% if is_club_admin %}
<a class="btn btn-outline btn-sm gap-2" href="{% url 'management:position_update' position.pk %}">{% lucide "pencil" size=14 %} {% trans "Edit" %}</a>
{% endif %}
</td>
</tr>
{% empty %}
<tr>
<td colspan="6" class="text-center text-muted">{% trans "No positions yet." %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock panel %}

View File

@@ -4,9 +4,9 @@
{% block heading %}{% trans "Import from RBIHF" %}{% endblock heading %}
{% block panel %}
<div class="card w-full bg-base-100 shadow">
<div class="card w-full">
<div class="card-body">
<p class="text-sm opacity-70">
<p class="text-sm text-muted">
{% blocktrans %}Paste the URL of a team's page on the RBIHF website and pick which of
your teams it's for. Nothing is created yet: you'll see exactly what will
be added, changed, or removed before anything is saved.{% endblocktrans %}
@@ -16,7 +16,7 @@
{% csrf_token %}
{% for error in form.non_field_errors %}
<div class="alert alert-error my-2">
<div class="alert alert-error">
<span>{{ error }}</span>
</div>
{% endfor %}
@@ -25,7 +25,7 @@
{% form_field field %}
{% endfor %}
<div class="card-actions justify-start pt-2 mt-2">
<div class="card-actions justify-start pt-4">
<a class="btn btn-outline gap-2" href="{% url 'management:event_list' %}">{% lucide "arrow-left" size=16 %} {% trans "Cancel" %}</a>
<button class="btn btn-primary gap-2" type="submit">{% lucide "download" size=16 %} {% trans "Fetch and preview" %}</button>
</div>

View File

@@ -4,7 +4,7 @@
{% block heading %}{% trans "Review import" %}{% endblock heading %}
{% block panel %}
<div class="alert alert-info mb-4">
<div class="alert alert-info">
{% lucide "info" size=16 %}
<span>{% blocktrans with scraped=plan.scraped_team_name team=plan.team %}Importing fixtures for “{{ scraped }}” onto your team “{{ team }}”. Double check that's the right match before confirming.{% endblocktrans %}</span>
</div>
@@ -12,137 +12,139 @@
<form method="post" action="{% url 'management:rbihf_import_confirm' %}">
{% csrf_token %}
<div class="card bg-base-100 shadow mb-4">
<div class="card-body">
<h2 class="card-title text-base">{% lucide "plus" size=18 %} {% blocktrans count counter=plan.to_create|length %}{{ counter }} game to create{% plural %}{{ counter }} games to create{% endblocktrans %}</h2>
<div class="flex flex-col gap-4">
<div class="card">
<div class="card-body">
<h2 class="card-title text-base">{% lucide "plus" size=18 %} {% blocktrans count counter=plan.to_create|length %}{{ counter }} game to create{% plural %}{{ counter }} games to create{% endblocktrans %}</h2>
{% if plan.to_create %}
<div class="overflow-x-auto">
<table class="table">
<thead>
<tr>
<th>{% trans "#" %}</th>
<th>{% trans "Start" %}</th>
<th>{% trans "Opponent" %}</th>
<th>{% trans "Location" %}</th>
</tr>
</thead>
<tbody>
{% for planned in plan.to_create %}
{% if plan.to_create %}
<div class="overflow-x-auto">
<table class="table">
<thead>
<tr>
<td>{{ planned.fixture.external_game_id }}</td>
<td>{{ planned.fixture.start|date:"Y-m-d H:i" }}</td>
<td>
<div class="flex items-center gap-2">
<select name="opponent_{{ planned.fixture.external_game_id }}" class="select select-sm w-full">
<option value="">{% trans "Use scraped name" %}</option>
{% for opponent in plan.opponent_choices %}
<option value="{{ opponent.pk }}" {% if planned.suggested_opponent.pk == opponent.pk %}selected{% endif %}>{{ opponent.name }}</option>
{% endfor %}
</select>
<span class="text-xs opacity-60 whitespace-nowrap">{% blocktrans with opponent=planned.fixture.opponent_name %}RBIHF: {{ opponent }}{% endblocktrans %}</span>
</div>
</td>
<td>
<div class="flex items-center gap-2">
<select name="location_{{ planned.fixture.external_game_id }}" class="select select-sm w-full">
<option value="">&mdash;</option>
{% for location in plan.location_choices %}
<option value="{{ location.pk }}" {% if planned.suggested_location.pk == location.pk %}selected{% endif %}>{{ location.name }}</option>
{% endfor %}
</select>
<span class="text-xs opacity-60 whitespace-nowrap">{% blocktrans with venue=planned.fixture.venue_text %}RBIHF: {{ venue }}{% endblocktrans %}</span>
</div>
</td>
<th>{% trans "#" %}</th>
<th>{% trans "Start" %}</th>
<th>{% trans "Opponent" %}</th>
<th>{% trans "Location" %}</th>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
</div>
</div>
<div class="card bg-base-100 shadow mb-4">
<div class="card-body">
<h2 class="card-title text-base">{% lucide "refresh-cw" size=18 %} {% blocktrans count counter=plan.to_update|length %}{{ counter }} game to update{% plural %}{{ counter }} games to update{% endblocktrans %}</h2>
{% if plan.to_update %}
<div class="overflow-x-auto">
<table class="table">
<thead>
<tr>
<th>{% trans "#" %}</th>
<th>{% trans "Changes" %}</th>
<th>{% trans "Opponent" %}</th>
<th>{% trans "Location" %}</th>
</tr>
</thead>
<tbody>
{% for planned in plan.to_update %}
<tr>
<td>{{ planned.fixture.external_game_id }}</td>
<td>
{% for field, change in planned.changes.items %}
<div class="text-xs">
<span class="font-semibold">{{ field }}:</span>
<span class="opacity-60 line-through">{{ change.0|default:"—" }}</span>
&rarr;
<span>{{ change.1 }}</span>
</thead>
<tbody>
{% for planned in plan.to_create %}
<tr>
<td class="font-mono text-xs">{{ planned.fixture.external_game_id }}</td>
<td class="font-mono text-xs">{{ planned.fixture.start|date:"Y-m-d H:i" }}</td>
<td>
<div class="flex items-center gap-2">
<select name="opponent_{{ planned.fixture.external_game_id }}" class="select w-full">
<option value="">{% trans "Use scraped name" %}</option>
{% for opponent in plan.opponent_choices %}
<option value="{{ opponent.pk }}" {% if planned.suggested_opponent.pk == opponent.pk %}selected{% endif %}>{{ opponent.name }}</option>
{% endfor %}
</select>
<span class="text-xs whitespace-nowrap text-muted">{% blocktrans with opponent=planned.fixture.opponent_name %}RBIHF: {{ opponent }}{% endblocktrans %}</span>
</div>
{% endfor %}
</td>
<td>
<div class="flex items-center gap-2">
<select name="opponent_{{ planned.fixture.external_game_id }}" class="select select-sm w-full">
<option value="">{% trans "Use scraped name" %}</option>
{% for opponent in plan.opponent_choices %}
<option value="{{ opponent.pk }}" {% if planned.suggested_opponent.pk == opponent.pk %}selected{% endif %}>{{ opponent.name }}</option>
{% endfor %}
</select>
<span class="text-xs opacity-60 whitespace-nowrap">{% blocktrans with opponent=planned.fixture.opponent_name %}RBIHF: {{ opponent }}{% endblocktrans %}</span>
</div>
</td>
<td>
<div class="flex items-center gap-2">
<select name="location_{{ planned.fixture.external_game_id }}" class="select select-sm w-full">
<option value="">&mdash;</option>
{% for location in plan.location_choices %}
<option value="{{ location.pk }}" {% if planned.suggested_location.pk == location.pk %}selected{% endif %}>{{ location.name }}</option>
{% endfor %}
</select>
<span class="text-xs opacity-60 whitespace-nowrap">{% blocktrans with venue=planned.fixture.venue_text %}RBIHF: {{ venue }}{% endblocktrans %}</span>
</div>
</td>
</td>
<td>
<div class="flex items-center gap-2">
<select name="location_{{ planned.fixture.external_game_id }}" class="select w-full">
<option value="">&mdash;</option>
{% for location in plan.location_choices %}
<option value="{{ location.pk }}" {% if planned.suggested_location.pk == location.pk %}selected{% endif %}>{{ location.name }}</option>
{% endfor %}
</select>
<span class="text-xs whitespace-nowrap text-muted">{% blocktrans with venue=planned.fixture.venue_text %}RBIHF: {{ venue }}{% endblocktrans %}</span>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
</div>
</div>
<div class="card">
<div class="card-body">
<h2 class="card-title text-base">{% lucide "refresh-cw" size=18 %} {% blocktrans count counter=plan.to_update|length %}{{ counter }} game to update{% plural %}{{ counter }} games to update{% endblocktrans %}</h2>
{% if plan.to_update %}
<div class="overflow-x-auto">
<table class="table">
<thead>
<tr>
<th>{% trans "#" %}</th>
<th>{% trans "Changes" %}</th>
<th>{% trans "Opponent" %}</th>
<th>{% trans "Location" %}</th>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
</thead>
<tbody>
{% for planned in plan.to_update %}
<tr>
<td class="font-mono text-xs">{{ planned.fixture.external_game_id }}</td>
<td>
{% for field, change in planned.changes.items %}
<div class="text-xs">
<span class="font-semibold text-ink">{{ field }}:</span>
<span class="text-muted line-through">{{ change.0|default:"—" }}</span>
&rarr;
<span class="text-ink">{{ change.1 }}</span>
</div>
{% endfor %}
</td>
<td>
<div class="flex items-center gap-2">
<select name="opponent_{{ planned.fixture.external_game_id }}" class="select w-full">
<option value="">{% trans "Use scraped name" %}</option>
{% for opponent in plan.opponent_choices %}
<option value="{{ opponent.pk }}" {% if planned.suggested_opponent.pk == opponent.pk %}selected{% endif %}>{{ opponent.name }}</option>
{% endfor %}
</select>
<span class="text-xs whitespace-nowrap text-muted">{% blocktrans with opponent=planned.fixture.opponent_name %}RBIHF: {{ opponent }}{% endblocktrans %}</span>
</div>
</td>
<td>
<div class="flex items-center gap-2">
<select name="location_{{ planned.fixture.external_game_id }}" class="select w-full">
<option value="">&mdash;</option>
{% for location in plan.location_choices %}
<option value="{{ location.pk }}" {% if planned.suggested_location.pk == location.pk %}selected{% endif %}>{{ location.name }}</option>
{% endfor %}
</select>
<span class="text-xs whitespace-nowrap text-muted">{% blocktrans with venue=planned.fixture.venue_text %}RBIHF: {{ venue }}{% endblocktrans %}</span>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
</div>
</div>
<div class="card">
<div class="card-body">
<h2 class="card-title text-base">{% lucide "trash-2" size=18 %} {% blocktrans count counter=plan.to_delete|length %}{{ counter }} game to delete{% plural %}{{ counter }} games to delete{% endblocktrans %}</h2>
<p class="text-sm text-muted">{% trans "No longer listed on the RBIHF page — only upcoming games are ever removed this way, past results are left alone." %}</p>
{% if plan.to_delete %}
<ul class="list-inside list-disc text-sm text-ink">
{% for event in plan.to_delete %}
<li class="font-mono text-xs">{{ event.start|date:"Y-m-d H:i" }} &mdash; <span class="font-sans text-sm">{{ event }}</span></li>
{% endfor %}
</ul>
{% endif %}
</div>
</div>
{% if plan.unchanged_count %}
<p class="text-sm text-muted">{% blocktrans count counter=plan.unchanged_count %}{{ counter }} game is already up to date.{% plural %}{{ counter }} games are already up to date.{% endblocktrans %}</p>
{% endif %}
</div>
<div class="card bg-base-100 shadow mb-4">
<div class="card-body">
<h2 class="card-title text-base">{% lucide "trash-2" size=18 %} {% blocktrans count counter=plan.to_delete|length %}{{ counter }} game to delete{% plural %}{{ counter }} games to delete{% endblocktrans %}</h2>
<p class="text-sm opacity-70">{% trans "No longer listed on the RBIHF page — only upcoming games are ever removed this way, past results are left alone." %}</p>
{% if plan.to_delete %}
<ul class="list-disc list-inside text-sm">
{% for event in plan.to_delete %}
<li>{{ event.start|date:"Y-m-d H:i" }} &mdash; {{ event }}</li>
{% endfor %}
</ul>
{% endif %}
</div>
</div>
{% if plan.unchanged_count %}
<p class="text-sm opacity-60 mb-4">{% blocktrans count counter=plan.unchanged_count %}{{ counter }} game is already up to date.{% plural %}{{ counter }} games are already up to date.{% endblocktrans %}</p>
{% endif %}
<div class="card-actions justify-start pt-2 mt-2">
<div class="card-actions justify-start pt-4">
<a class="btn btn-outline gap-2" href="{% url 'management:rbihf_import' %}">{% lucide "arrow-left" size=16 %} {% trans "Back" %}</a>
{% if plan.to_create or plan.to_update or plan.to_delete %}
<button class="btn btn-primary gap-2" type="submit">{% lucide "check" size=16 %} {% trans "Confirm import" %}</button>

View File

@@ -4,24 +4,24 @@
{% 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 max-w-3xl">
<div class="card-body">
<form method="post">
{% csrf_token %}
{% for error in form.non_field_errors %}
<div class="alert alert-error my-2">
<div class="alert alert-error">
<span>{{ error }}</span>
</div>
{% endfor %}
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 items-start">
<div class="grid grid-cols-2 items-start gap-4">
{% for field in form %}
{% form_field field %}
{% endfor %}
</div>
<div class="card-actions justify-start pt-2 mt-2">
<div class="mt-4 flex gap-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>

View File

@@ -2,53 +2,49 @@
{% 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 topbar_context %}<span class="text-sm text-muted">{% trans "Which teams' home games each level qualifies a referee for." %}</span>{% endblock topbar_context %}
{% 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>
<a class="btn btn-primary 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 table-cards">
<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 data-label="{% trans 'Ordering' %}">{{ level.ordering }}</td>
<td class="font-semibold">{{ level.name }}</td>
<td data-label="{% trans 'Qualifies for' %}">
{% 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 class="card overflow-hidden">
<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 class="font-mono text-muted">{{ level.ordering }}</td>
<td class="font-semibold text-ink">{{ level.name }}</td>
<td>
{% for team in level.teams.all %}
<span class="badge badge-outline badge-sm">{{ team.name }}</span>
{% empty %}
<span class="text-dim"></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 text-muted">{% trans "No referee levels yet." %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock panel %}

View File

@@ -2,59 +2,56 @@
{% 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 topbar_context %}<span class="text-sm text-muted">{% trans "Every club referee, their level, and whether their qualification is currently valid." %}</span>{% endblock topbar_context %}
{% block panel %}
<div class="card bg-base-100 shadow">
<div class="card-body">
<div class="overflow-x-auto">
<table class="table table-cards">
<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 font-semibold" href="{% url 'management:member_detail' referee.pk %}">{{ referee }}</a></td>
<td data-label="{% trans 'Level' %}">{{ referee.referee_profile.level|default:"—" }}</td>
<td data-label="{% trans 'Can referee for' %}">
{% 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 data-label="{% trans 'Valid until' %}">{{ referee.referee_profile.valid_until|default:"—" }}</td>
<td data-label="{% trans 'Status' %}">
{% 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 class="card overflow-hidden">
<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 font-semibold" href="{% url 'management:member_detail' referee.pk %}">{{ referee }}</a></td>
<td class="text-slate">{{ 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="text-dim"></span>
{% endfor %}
{% else %}
<span class="text-dim"></span>
{% endif %}
</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">{{ 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 text-muted">{% trans "No referees yet -- set a level and validity from a member's own page." %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock panel %}

View File

@@ -2,87 +2,84 @@
{% 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 topbar_context %}<span class="text-sm text-muted">{% trans "Every upcoming home game that needs a club-arranged referee." %}</span>{% endblock topbar_context %}
{% 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 class="grid grid-cols-5 gap-3">
<div class="card p-3.5">
<div class="mb-2 flex items-center gap-1.5 font-mono text-[10px] tracking-[.08em] text-muted uppercase">{% lucide "calendar" size=13 %} {% trans "Games in view" %}</div>
<div class="font-display text-4xl leading-none font-extrabold text-ink tabular-nums">{{ kpi_total }}</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 class="card p-3.5">
<div class="mb-2 flex items-center gap-1.5 font-mono text-[10px] tracking-[.08em] text-muted uppercase">{% lucide "circle-alert" size=13 %} {% trans "Without a referee" %}</div>
<div class="font-display text-4xl leading-none font-extrabold tabular-nums {% if kpi_no_referee %}text-club-dark{% else %}text-ink{% endif %}">{{ kpi_no_referee }}</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 class="card p-3.5">
<div class="mb-2 flex items-center gap-1.5 font-mono text-[10px] tracking-[.08em] text-muted uppercase">{% lucide "circle-dashed" size=13 %} {% trans "Partially staffed" %}</div>
<div class="font-display text-4xl leading-none font-extrabold tabular-nums {% if kpi_understaffed %}text-warn{% else %}text-ink{% endif %}">{{ kpi_understaffed }}</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 class="card p-3.5">
<div class="mb-2 flex items-center gap-1.5 font-mono text-[10px] tracking-[.08em] text-muted uppercase">{% lucide "circle-check" size=13 %} {% trans "Fully staffed" %}</div>
<div class="font-display text-4xl leading-none font-extrabold text-ink tabular-nums">{{ kpi_fully_staffed }}</div>
</div>
<div class="card p-3.5">
<div class="mb-2 flex items-center gap-1.5 font-mono text-[10px] tracking-[.08em] text-muted uppercase">{% lucide "euro" size=13 %} {% trans "Fees not set" %}</div>
<div class="font-display text-4xl leading-none font-extrabold tabular-nums {% if kpi_fees_pending %}text-warn{% else %}text-ink{% endif %}">{{ kpi_fees_pending }}</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 class="grid grid-cols-5 gap-2">
<a href="?range=week" class="btn btn-sm justify-center {% if range_choice == "week" %}btn-primary{% else %}btn-outline{% endif %}">{% trans "This week" %}</a>
<a href="?range=two_weeks" class="btn btn-sm justify-center {% if range_choice == "two_weeks" %}btn-primary{% else %}btn-outline{% endif %}">{% trans "This + next week" %}</a>
<a href="?range=10" class="btn btn-sm justify-center {% if range_choice == "10" %}btn-primary{% else %}btn-outline{% endif %}">{% trans "Next 10" %}</a>
<a href="?range=25" class="btn btn-sm justify-center {% if range_choice == "25" %}btn-primary{% else %}btn-outline{% endif %}">{% trans "Next 25" %}</a>
<a href="?range=50" class="btn btn-sm justify-center {% if range_choice == "50" %}btn-primary{% else %}btn-outline{% 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">
<div class="font-display text-xs font-extrabold tracking-[.14em] text-muted uppercase">{{ day.grouper|date:"l j F" }}</div>
<div class="grid grid-cols-3 gap-3">
{% 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="flex flex-wrap gap-1 text-xs">
{% for referee in game.referee_rows %}
<span class="badge badge-primary 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 flex-wrap items-center justify-between gap-2">
<div class="card flex flex-col gap-2 p-4 {% if not game.referee_rows %}border-club-dark{% elif not game.referees_full %}border-warn{% endif %}">
<a class="link link-hover font-semibold text-ink" 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="font-mono text-xs text-muted">{{ game.start|date:"H:i" }}{% if game.location %} — {{ game.location }}{% endif %}</div>
<div class="flex flex-wrap gap-1">
{% for referee in game.referee_rows %}
<span class="badge badge-neutral badge-sm">{{ referee.display_name }}{% if referee.is_external %} ({% trans "ext." %}){% endif %}</span>
{% empty %}
<span class="text-xs text-muted">{% trans "No referees assigned yet." %}</span>
{% endfor %}
</div>
<div class="flex flex-wrap items-center justify-between gap-2">
<div class="flex items-center gap-1.5">
<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>
{% if game.fees_pending %}<span class="badge badge-warning badge-sm">{% trans "Fee not set" %}</span>{% endif %}
</div>
<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>
<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">
<div class="modal-box w-[36rem] max-w-[91vw]">
<form method="dialog"><button class="btn btn-ghost btn-square btn-sm absolute top-2 right-2">{% lucide "x" size=16 %}</button></form>
<h3 class="font-display text-lg font-extrabold text-ink uppercase">
{% 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>
<div class="mb-4 font-mono text-sm text-muted">{{ 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>
@@ -90,9 +87,9 @@
{% endfor %}
</div>
{% empty %}
<div class="card bg-base-100 shadow">
<div class="card">
<div class="card-body">
<p class="text-center opacity-60">{% trans "No upcoming home games need a club-arranged referee." %}</p>
<p class="text-center text-muted">{% trans "No upcoming home games need a club-arranged referee." %}</p>
</div>
</div>
{% endfor %}

View File

@@ -12,45 +12,41 @@
{% block heading %}{% trans "Roles" %}{% endblock heading %}
{% block actions %}
<button class="btn btn-outline gap-2" type="button" onclick="document.getElementById('grant_role_modal').showModal()">{% lucide "plus" size=16 %} {% trans "Grant role" %}</button>
<button class="btn btn-primary gap-2" type="button" onclick="document.getElementById('grant_role_modal').showModal()">{% lucide "plus" size=16 %} {% trans "Grant role" %}</button>
{% endblock actions %}
{% block panel %}
<div class="flex flex-col gap-4">
{% for value, label, description, roles_for_section in sections %}
{% with role_label=label|capfirst %}
<div class="card bg-base-100 shadow">
<div class="card-body">
<h2 class="card-title text-base">{{ role_label }}</h2>
{% if description %}<p class="text-sm opacity-70">{{ description }}</p>{% endif %}
<div class="overflow-x-auto">
<table class="table table-cards">
<thead>
<tr>
<th>{% trans "Member" %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for role in roles_for_section %}
<tr>
<td class="font-semibold">{{ role.member }}</td>
<td class="text-right">
<form method="post" action="{% url 'management:role_revoke' role.pk %}">
{% csrf_token %}
<button class="btn btn-outline btn-error btn-sm gap-2" type="submit">{% lucide "x" size=14 %} {% trans "Revoke" %}</button>
</form>
</td>
</tr>
{% empty %}
<tr>
<td colspan="2" class="text-center opacity-60">{% blocktrans %}No one has the {{ role_label }} role yet.{% endblocktrans %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="card card-body">
<h2 class="card-title">{{ role_label }}</h2>
{% if description %}<p class="text-sm text-muted">{{ description }}</p>{% endif %}
<table class="table">
<thead>
<tr>
<th>{% trans "Member" %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for role in roles_for_section %}
<tr>
<td class="font-semibold text-ink">{{ role.member }}</td>
<td class="text-right">
<form method="post" action="{% url 'management:role_revoke' role.pk %}">
{% csrf_token %}
<button class="btn btn-outline btn-error btn-xs gap-1" type="submit">{% lucide "x" size=13 %} {% trans "Revoke" %}</button>
</form>
</td>
</tr>
{% empty %}
<tr>
<td colspan="2" class="py-4 text-center text-muted">{% blocktrans %}No one has the {{ role_label }} role yet.{% endblocktrans %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endwith %}
{% endfor %}

View File

@@ -0,0 +1,270 @@
{% extends "management/base.html" %}
{% load i18n lucide ui %}
{% block heading %}{% trans "Sign-up" %}{% endblock heading %}
{% block topbar_context %}
{% if season %}
<span class="font-mono text-[13px] text-muted">{% blocktrans with start=season.start_date|date:"Y" end=season.end_date|date:"Y" %}Season {{ start }}-{{ end }}{% endblocktrans %}</span>
{% endif %}
{% endblock topbar_context %}
{% block actions %}
{% if season %}
<form method="post" action="{% url 'management:signup_approve_all_clean' %}">
{% csrf_token %}
<button class="btn btn-outline gap-2" type="submit">{% lucide "check-check" size=16 %} {% trans "Approve all clean" %}</button>
</form>
{% endif %}
{% endblock actions %}
{% block panel %}
{% if not season %}
<div class="alert alert-warning">
{% lucide "calendar-x" size=20 %}
<span>{% trans "No season covers today, so there is nothing to process sign-ups for." %}</span>
</div>
{% else %}
<div class="card overflow-hidden">
<table class="table">
<thead>
<tr>
<th>{% trans "Applicant" %}</th>
<th>{% trans "Born" %}</th>
<th>{% trans "Team" %}</th>
<th>{% trans "Fee" %}</th>
<th>{% trans "Checks" %}</th>
<th>{% trans "Waiting" %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for membership in memberships %}
{# A plain border-left on a <tr> is unreliable under border-collapse (varies by browser, can get clipped by the card's rounded corner) -- an inset box-shadow paints the same accent bar without depending on table border behaviour at all. #}
<tr class="cursor-pointer {% if membership.is_oldest_pending %}bg-row-focus shadow-[inset_3px_0_0_var(--color-info)]{% endif %}" onclick="openDrawer('{{ membership.pk|dom_id:"signup_drawer" }}')">
<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">{{ membership.member.first_name|slice:":1" }}{{ membership.member.last_name|slice:":1" }}</div>
</span>
<div>
<div class="font-semibold text-ink">{{ membership.member.get_full_name }}</div>
<div class="text-xs text-muted">
{% if membership.status == "pending" %}{% trans "Awaiting processing" %}{% else %}{{ membership.get_status_display }}{% endif %}
</div>
</div>
</div>
</td>
<td class="font-mono text-sm text-muted">{{ membership.member.date_of_birth|date:"Y"|default:"—" }}</td>
<td class="text-sm text-ink">
{% if membership.teams_registered %}
{% for team in membership.teams_registered %}{{ team.short_name }}{% if not forloop.last %}, {% endif %}{% endfor %}
{% else %}
<span class="text-dim">{% trans "Not placed yet" %}</span>
{% endif %}
</td>
<td>
<span class="badge badge-sm {% if membership.fee_status == "paid" %}badge-success{% elif membership.fee_status == "partially_paid" %}badge-warning{% elif membership.fee_status == "unpaid" %}badge-error{% else %}badge-ghost{% endif %}">{{ membership.get_fee_status_display }}</span>
</td>
<td>
{% if requirements_configured %}
<span class="badge badge-sm {% if membership.onboarding_open %}badge-warning{% else %}badge-success{% endif %}">
{% if membership.onboarding_open %}{% blocktrans count count=membership.onboarding_open %}{{ count }} open{% plural %}{{ count }} open{% endblocktrans %}{% else %}{% trans "Clean" %}{% endif %}
</span>
{% else %}
<span class="text-dim"></span>
{% endif %}
</td>
{# text-xs, not text-sm: IBM Plex Mono reads noticeably larger than Barlow at the same declared size, and "timesince" text (e.g. "2 weeks") is already the most visually busy string in the row. #}
<td class="font-mono text-xs {% if membership.is_oldest_pending %}text-club-dark{% else %}text-muted{% endif %}">
{% if membership.status == "pending" %}{{ membership.created|timesince }}{% else %}—{% endif %}
</td>
<td class="text-right">
<button class="btn btn-outline btn-xs gap-1" type="button" onclick="event.stopPropagation(); openDrawer('{{ membership.pk|dom_id:"signup_drawer" }}')">{% trans "Open" %} {% lucide "chevron-right" size=12 %}</button>
</td>
</tr>
{% empty %}
<tr>
<td colspan="7" class="py-6 text-center text-muted">{% trans "No one is signed up for this season yet." %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
{% endblock panel %}
{% block extra_body %}
{% trans "Confirm" as confirm_label %}
{% trans "Bypass" as bypass_label %}
{% trans "Reopen" as reopen_label %}
{% url 'management:signup_list' as signup_url %}
{% for membership in memberships %}
<div id="{{ membership.pk|dom_id:"signup_drawer" }}" class="drawer-backdrop">
<div class="drawer">
<div class="flex items-center gap-3 border-b border-line p-5">
<span class="avatar avatar-placeholder shrink-0">
<div class="h-11 w-11 rounded-full text-base">{{ membership.member.first_name|slice:":1" }}{{ membership.member.last_name|slice:":1" }}</div>
</span>
<div class="min-w-0 flex-1">
<div class="truncate font-display text-xl font-extrabold text-ink uppercase">{{ membership.member.get_full_name }}</div>
<div class="text-[13px] text-muted">{% blocktrans with date=membership.created|date:"j M" %}Applied {{ date }}{% endblocktrans %}{% if membership.teams_registered %} · {% for team in membership.teams_registered %}{{ team.short_name }}{% if not forloop.last %}, {% endif %}{% endfor %}{% endif %}</div>
</div>
<button type="button" class="btn btn-ghost btn-square btn-sm" aria-label="{% trans 'Close' %}" onclick="closeDrawer('{{ membership.pk|dom_id:"signup_drawer" }}')">{% lucide "x" size=16 %}</button>
</div>
<div class="flex flex-1 flex-col gap-5 overflow-y-auto p-5">
{% if requirements_configured %}
<div>
<div class="mb-2.5 font-display text-xs font-bold tracking-[.14em] text-muted uppercase">{% trans "Checks" %}</div>
<div class="flex flex-col gap-2">
{% for requirement, status in membership.checklist %}
<div class="flex items-center gap-2.5 text-sm">
{% if status.is_complete or status.is_bypassed %}
<span class="flex h-5 w-5 shrink-0 items-center justify-center rounded bg-ok text-white">{% lucide "check" size=13 %}</span>
{% else %}
<span class="flex h-5 w-5 shrink-0 items-center justify-center rounded bg-warn font-display text-xs font-extrabold text-white">!</span>
{% endif %}
<span class="min-w-0 flex-1 text-ink">
{{ requirement.name }}
{% if status.is_bypassed %}<span class="text-xs text-muted">({% trans "bypassed" %})</span>{% endif %}
</span>
{% if not status.is_complete and not status.is_bypassed %}
<form method="post" action="{% url 'management:member_requirement_complete' membership.member.pk requirement.pk %}" class="shrink-0">
{% csrf_token %}<input type="hidden" name="next" value="{{ signup_url }}"><input type="hidden" name="note" value="">
<button class="btn btn-outline btn-success btn-xs" type="submit">{{ confirm_label }}</button>
</form>
<button class="btn btn-outline btn-xs shrink-0" type="button" onclick="document.getElementById('bypass_{{ membership.pk }}_{{ requirement.pk }}').showModal()">{{ bypass_label }}</button>
{% else %}
<form method="post" action="{% url 'management:member_requirement_incomplete' membership.member.pk requirement.pk %}" class="shrink-0">
{% csrf_token %}<input type="hidden" name="next" value="{{ signup_url }}">
<button class="btn btn-outline btn-xs" type="submit">{{ reopen_label }}</button>
</form>
{% endif %}
</div>
{% if not status.is_complete and not status.is_bypassed %}
<dialog id="bypass_{{ membership.pk }}_{{ requirement.pk }}" class="modal">
<div class="modal-box">
<h3 class="text-lg font-bold text-ink">{% blocktrans with name=requirement.name %}Bypass “{{ name }}”{% endblocktrans %}</h3>
<form method="post" action="{% url 'management:member_requirement_bypass' membership.member.pk requirement.pk %}" id="bypass_{{ membership.pk }}_{{ requirement.pk }}_form">
{% csrf_token %}
<input type="hidden" name="next" value="{{ signup_url }}">
<div class="form-control mt-2">
<label class="label-text">{% trans "Why isn't this needed?" %}</label>
<textarea class="textarea" name="note" rows="2" required placeholder="{% trans 'e.g. already has a recent photo on file' %}"></textarea>
</div>
</form>
<div class="modal-action">
<form method="dialog"><button class="btn btn-outline gap-2">{% lucide "x" size=16 %} {% trans "Cancel" %}</button></form>
<button class="btn btn-primary gap-2" type="submit" form="bypass_{{ membership.pk }}_{{ requirement.pk }}_form">{% lucide "check" size=16 %} {{ bypass_label }}</button>
</div>
</div>
<form method="dialog" class="modal-backdrop"><button>close</button></form>
</dialog>
{% endif %}
{% empty %}
<p class="text-sm text-muted">{% trans "This club has no onboarding requirements set up." %}</p>
{% endfor %}
</div>
</div>
{% endif %}
<div>
<div class="mb-2.5 font-display text-xs font-bold tracking-[.14em] text-muted uppercase">{% trans "Place in" %}</div>
<div class="flex flex-wrap gap-2">
{% for team in teams %}
<form method="post" action="{% url 'management:signup_place_in_team' membership.member.pk %}" data-place-in-team>
{% csrf_token %}
<input type="hidden" name="team" value="{{ team.pk }}">
<button type="submit" class="team-chip {% if team in membership.teams_registered %}placed{% endif %}" {% if team in membership.teams_registered %}disabled{% endif %}>
{{ team.short_name }}
</button>
</form>
{% empty %}
<p class="text-sm text-muted">{% trans "This club has no teams set up yet." %}</p>
{% endfor %}
</div>
<p class="mt-2 text-xs text-muted">{% trans "Rosters them immediately so the team's coach can see them -- position/number is the team's own call to set correctly later. They still won't be invited to (or selectable for) an event kind a blocking checklist item covers." %}</p>
</div>
<div>
<div class="mb-2.5 font-display text-xs font-bold tracking-[.14em] text-muted uppercase">{% trans "Fee" %}</div>
<div class="flex items-center justify-between rounded-lg border border-line p-3">
<span class="badge badge-sm {% if membership.fee_status == "paid" %}badge-success{% elif membership.fee_status == "partially_paid" %}badge-warning{% elif membership.fee_status == "unpaid" %}badge-error{% else %}badge-ghost{% endif %}">{{ membership.get_fee_status_display }}</span>
<a class="link link-hover text-sm" href="{% url 'management:membership_list' %}">{% trans "Manage in Finance" %} →</a>
</div>
</div>
</div>
<div class="flex gap-2.5 border-t border-line p-4">
<form method="post" action="{% url 'management:signup_approve_one' membership.member.pk %}" class="flex-1">
{% csrf_token %}
<button class="btn btn-success w-full gap-2" type="submit" {% if not membership.is_clean or membership.status != "pending" %}disabled{% endif %}>{% lucide "check" size=16 %} {% trans "Approve" %}</button>
</form>
<button type="button" class="btn btn-outline gap-2" onclick="closeDrawer('{{ membership.pk|dom_id:"signup_drawer" }}')">{% trans "Close" %}</button>
</div>
</div>
</div>
{% endfor %}
<script>
// The detail drawer needs JS to open at all now (see assets/management.css's
// own .drawer-backdrop comment for why it's a plain div, not a native
// <dialog> -- Safari rounds a shown <dialog>'s corners regardless of
// author CSS). display can't itself transition, so opening adds .visible
// (display:block) and forces a reflow *before* adding .open (the actual
// background-fade/slide-in), otherwise the browser has no "just-appeared,
// still off-screen" frame to animate away from.
function openDrawer(id) {
const backdrop = document.getElementById(id);
backdrop.classList.add("visible");
void backdrop.offsetHeight;
backdrop.classList.add("open");
}
function closeDrawer(id) {
const backdrop = document.getElementById(id);
backdrop.classList.remove("open");
setTimeout(() => backdrop.classList.remove("visible"), 200);
}
document.querySelectorAll(".drawer-backdrop").forEach((backdrop) => {
backdrop.addEventListener("click", (event) => {
if (event.target === backdrop) closeDrawer(backdrop.id);
});
});
document.addEventListener("keydown", (event) => {
if (event.key !== "Escape") return;
document.querySelectorAll(".drawer-backdrop.open").forEach((backdrop) => closeDrawer(backdrop.id));
});
// "Place in [team]" posts in the background -- clicking a team is a single
// fact ("this member is on this team now"), not something that needs a
// full-page reload to confirm. X-Requested-With is what
// SignupPlaceInTeamView checks to answer with JSON instead of a redirect;
// a non-JS client (or a fetch failure) still gets the exact same effect
// through a plain synchronous submit, just with a page reload.
document.querySelectorAll("[data-place-in-team]").forEach((form) => {
form.addEventListener("submit", (event) => {
event.preventDefault();
const button = form.querySelector("button");
if (button.disabled) return;
fetch(form.action, {
method: "POST",
body: new FormData(form),
headers: {"X-Requested-With": "XMLHttpRequest"},
})
.then((response) => response.json())
.then((data) => {
if (data.ok) {
button.classList.add("placed");
button.disabled = true;
}
})
.catch(() => form.submit());
});
});
</script>
{% endblock extra_body %}

View File

@@ -1,27 +1,31 @@
{% extends "management/base.html" %}
{% load i18n lucide ui %}
{% block panel_title %}{% if update_view %}{% trans "Edit sponsor" %}{% else %}{% trans "New sponsor" %}{% endif %}{% endblock panel_title %}
{% block heading %}{% if update_view %}{% blocktrans %}Edit {{ object }}{% endblocktrans %}{% else %}{% trans "New sponsor" %}{% endif %}{% endblock heading %}
{% block panel %}
<div class="card w-full bg-base-100 shadow">
<div class="card">
<div class="card-body">
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{% for error in form.non_field_errors %}
<div class="alert alert-error my-2">
<div class="alert alert-error">
{% lucide "circle-x" size=18 %}
<span>{{ error }}</span>
</div>
{% endfor %}
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="grid grid-cols-2 gap-4">
{% for field in form %}
{% form_field field %}
<div class="{% if field.widget_type == "textarea" or field.widget_type == "clearablefile" %}col-span-2{% endif %}">
{% form_field field %}
</div>
{% endfor %}
</div>
<div class="card-actions justify-start pt-2 mt-2">
<div class="mt-4 flex justify-start gap-2">
<a class="btn btn-outline gap-2" href="{% url "management:sponsor_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>

View File

@@ -1,61 +1,62 @@
{% extends "management/base.html" %}
{% load i18n lucide ui %}
{% block panel_title %}{% trans "Sponsors" %}{% endblock panel_title %}
{% block heading %}{% trans "Sponsors" %}{% endblock heading %}
{% block topbar_context %}
<span class="badge badge-neutral">{% blocktrans count counter=sponsors|length %}{{ counter }} sponsor{% plural %}{{ counter }} sponsors{% endblocktrans %}</span>
{% endblock topbar_context %}
{% block actions %}
<a class="btn btn-primary gap-2" href="{% url 'management:sponsor_create' %}">{% lucide "plus" size=16 %} {% trans "New sponsor" %}</a>
{% endblock actions %}
{% block panel %}
<div class="card bg-base-100 shadow">
<div class="card-body">
<div class="overflow-x-auto">
<table class="table table-cards">
<thead>
<tr>
<th></th>
<th>{% trans "Name" %}</th>
<th>{% trans "URL" %}</th>
<th>{% trans "Start date" %}</th>
<th>{% trans "End date" %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for sponsor in sponsors %}
<tr>
<td>
{% if sponsor.logo %}
<img src="{{ sponsor.logo.url }}" alt="" class="size-8 rounded object-contain">
{% endif %}
</td>
<td class="font-semibold">{{ sponsor.name }}</td>
<td data-label="{% trans 'URL' %}">
{% if sponsor.url %}
<a class="link link-hover break-all" href="{{ sponsor.url }}" target="_blank" rel="noopener noreferrer">{{ sponsor.url }}</a>
{% else %}
<span class="opacity-50">&mdash;</span>
{% endif %}
</td>
<td data-label="{% trans 'Start date' %}">{{ sponsor.start_date|date:"Y-m-d" }}</td>
<td data-label="{% trans 'End date' %}">{{ sponsor.end_date|date:"Y-m-d"|default:"—" }}</td>
<td class="text-right">
<div class="flex justify-end gap-1">
<a class="btn btn-outline btn-sm" href="{% url 'management:sponsor_update' sponsor.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('{{ sponsor.pk|dom_id:"sponsor_delete_modal" }}').showModal()" aria-label="{% trans 'Delete' %}">{% lucide "trash-2" size=14 %} {% trans "Delete" %}</button>
</div>
</td>
</tr>
{% empty %}
<tr>
<td colspan="6" class="text-center opacity-60">{% trans "No sponsors yet." %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="card overflow-hidden">
<table class="table">
<thead>
<tr>
<th></th>
<th>{% trans "Name" %}</th>
<th>{% trans "URL" %}</th>
<th>{% trans "Start date" %}</th>
<th>{% trans "End date" %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for sponsor in sponsors %}
<tr>
<td>
{% if sponsor.logo %}
<img class="size-8 rounded object-contain" src="{{ sponsor.logo.url }}" alt="">
{% endif %}
</td>
<td class="font-semibold">{{ sponsor.name }}</td>
<td>
{% if sponsor.url %}
<a class="link link-hover break-all" href="{{ sponsor.url }}" target="_blank" rel="noopener noreferrer">{{ sponsor.url }}</a>
{% else %}
<span class="text-dim">&mdash;</span>
{% endif %}
</td>
<td class="font-mono text-xs">{{ sponsor.start_date|date:"Y-m-d" }}</td>
<td class="font-mono text-xs">{{ sponsor.end_date|date:"Y-m-d"|default:"—" }}</td>
<td class="text-right">
<div class="flex justify-end gap-1">
<a class="btn btn-outline btn-sm" href="{% url 'management:sponsor_update' sponsor.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('{{ sponsor.pk|dom_id:"sponsor_delete_modal" }}').showModal()" aria-label="{% trans 'Delete' %}">{% lucide "trash-2" size=14 %} {% trans "Delete" %}</button>
</div>
</td>
</tr>
{% empty %}
<tr>
<td colspan="6" class="text-center text-dim">{% trans "No sponsors yet." %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% trans "Delete sponsor" as delete_sponsor_title %}

View File

@@ -2,65 +2,57 @@
{% load i18n lucide static %}
{% block heading %}{% trans "Add multiple people" %}{% endblock heading %}
{% block subheading %}{{ team.name }}{% endblock subheading %}
{% block topbar_context %}<span class="badge badge-outline">{{ team.name }}</span>{% endblock topbar_context %}
{% block actions %}
<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>
{% endblock actions %}
{% block panel %}
<form method="post">
{% csrf_token %}
{{ formset.management_form }}
<div class="card bg-base-100 shadow">
<div class="card">
<div class="card-body">
<p class="text-sm opacity-70 mb-2">
<p class="text-sm text-muted">
{% blocktrans %}Only members who are active (paid up) for this club this season or next can be added. The position you pick decides the role: a staff position adds them to the staff, any other position adds them to the roster. To add someone as both a player and staff, give them two rows.{% endblocktrans %}
</p>
{% for error in formset.non_form_errors %}
<div class="alert alert-error my-2">
<div class="alert alert-error">
<span>{{ error }}</span>
</div>
{% endfor %}
{% comment %}
overflow-x-auto is safe here: the member picker's dropdown
(searchable-select.js) is position: fixed, computed from the input's
own screen position rather than document flow, so a scrolling
ancestor around the table no longer clips it.
{% endcomment %}
<div class="overflow-x-auto">
<table class="table w-full">
<thead>
<tr>
<th>{% trans "Member" %}</th>
<th>{% trans "Position" %}</th>
<th>{% trans "Jersey #" %}</th>
<th class="text-center">{% trans "Captain" %}</th>
<th class="text-center">{% trans "Alternate" %}</th>
<th></th>
</tr>
</thead>
<tbody id="bulk-add-rows" data-prefix="{{ formset.prefix }}">
{% for form in formset %}
{% include "management/_team_bulk_add_row.html" with form=form %}
{% endfor %}
</tbody>
</table>
</div>
<table class="table">
<thead>
<tr>
<th>{% trans "Member" %}</th>
<th>{% trans "Position" %}</th>
<th>{% trans "Jersey #" %}</th>
<th class="text-center">{% trans "Captain" %}</th>
<th class="text-center">{% trans "Alternate" %}</th>
<th></th>
</tr>
</thead>
<tbody id="bulk-add-rows" data-prefix="{{ formset.prefix }}">
{% for form in formset %}
{% include "management/_team_bulk_add_row.html" with form=form %}
{% endfor %}
</tbody>
</table>
<template id="bulk-add-row-template">
{% include "management/_team_bulk_add_row.html" with form=formset.empty_form %}
</template>
<div class="flex flex-wrap items-center gap-2 mt-2">
<div class="flex flex-wrap items-center gap-2">
<button class="btn btn-outline btn-sm gap-2" type="button" id="add-row">{% lucide "plus" size=14 %} {% trans "Add another row" %}</button>
<button class="btn btn-primary btn-sm gap-2" type="submit">{% lucide "users" size=14 %} {% trans "Add to team" %}</button>
</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 %}

View File

@@ -2,14 +2,21 @@
{% 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 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 %}
@@ -18,19 +25,8 @@
{% 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">
<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>
@@ -52,240 +48,221 @@
{% 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>
{# --- 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="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 flex-col gap-1 py-2 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
<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 flex-col gap-1 py-2 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
<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>
<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 %}
</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 table-cards">
<thead>
<tr>
<th>{% trans "Member" %}</th>
<th>{% trans "Position" %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for assignment in staff %}
<tr>
<td class="font-semibold">{{ assignment.member }}</td>
<td data-label="{% trans 'Position' %}">{{ assignment.position }}</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('{{ 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>
<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 %}
<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>
{% lucide "pencil" size=14 %} {{ replace_photo_label }}
{% else %}
{% lucide "upload" size=14 %} {{ upload_photo_label }}
{% endif %}
</div>
</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 %}
</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 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">&middot; {{ 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 %}

View File

@@ -4,28 +4,26 @@
{% block heading %}{% if update_view %}{% blocktrans %}Edit {{ object }}{% endblocktrans %}{% else %}{% trans "New team" %}{% endif %}{% endblock heading %}
{% block panel %}
<div class="card w-full bg-base-100 shadow">
<div class="card-body">
<form method="post">
{% csrf_token %}
<div class="card 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>
{% 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 gap-4 md:grid-cols-2">
{% for field in form %}
{% form_field field %}
{% endfor %}
</div>
<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:team_detail" object.pk %}{% else %}{% url "management:team_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 class="mt-2 flex items-center gap-2 pt-2">
<a class="btn btn-outline gap-2" href="{% if update_view %}{% url "management:team_detail" object.pk %}{% else %}{% url "management:team_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>
{% endblock panel %}

View File

@@ -5,49 +5,45 @@
{% block actions %}
{% if is_club_admin %}
<a class="btn btn-outline gap-2" href="{% url 'management:team_create' %}">{% lucide "plus" size=16 %} {% trans "New team" %}</a>
<a class="btn btn-primary gap-2" href="{% url 'management:team_create' %}">{% lucide "plus" size=16 %} {% trans "New team" %}</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 table-cards">
<thead>
<tr>
<th>{% trans "Name" %}</th>
<th>{% trans "Short name" %}</th>
<th>{% trans "Players" %}</th>
<th>{% trans "Staff" %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for team in teams %}
<tr>
<td><a class="link link-hover font-semibold" href="{% url 'management:team_detail' team.pk %}">{{ team.name }}</a></td>
<td data-label="{% trans 'Short name' %}">{{ team.short_name }}</td>
<td data-label="{% trans 'Players' %}">{{ team.player_count }}</td>
<td data-label="{% trans 'Staff' %}">{{ team.staff_count }}</td>
<td class="text-right">
{% if is_club_admin %}
<div class="flex justify-end gap-1">
<a class="btn btn-outline btn-sm" href="{% url 'management:team_detail' team.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('{{ team.pk|dom_id:"team_delete_modal" }}').showModal()" aria-label="{% trans 'Delete' %}">{% lucide "trash-2" size=14 %} {% trans "Delete" %}</button>
</div>
{% endif %}
</td>
</tr>
{% empty %}
<tr>
<td colspan="5" class="text-center opacity-60">{% trans "No teams yet." %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="card overflow-hidden">
<table class="table">
<thead>
<tr>
<th>{% trans "Name" %}</th>
<th>{% trans "Short name" %}</th>
<th>{% trans "Players" %}</th>
<th>{% trans "Staff" %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for team in teams %}
<tr>
<td><a class="link link-hover font-semibold" href="{% url 'management:team_detail' team.pk %}">{{ team.name }}</a></td>
<td class="font-mono text-muted">{{ team.short_name }}</td>
<td class="font-mono tabular-nums">{{ team.player_count }}</td>
<td class="font-mono tabular-nums">{{ team.staff_count }}</td>
<td class="text-right">
{% if is_club_admin %}
<div class="flex justify-end gap-1">
<a class="btn btn-outline btn-sm" href="{% url 'management:team_detail' team.pk %}" aria-label="{% trans 'Edit' %}">{% lucide "pencil" size=14 %} {% trans "Edit" %}</a>
<button class="btn btn-outline btn-error btn-sm" type="button" onclick="document.getElementById('{{ team.pk|dom_id:"team_delete_modal" }}').showModal()" aria-label="{% trans 'Delete' %}">{% lucide "trash-2" size=14 %} {% trans "Delete" %}</button>
</div>
{% endif %}
</td>
</tr>
{% empty %}
<tr>
<td colspan="5" class="text-center text-muted">{% trans "No teams yet." %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% include "management/_pagination.html" %}