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:
111
management/templates/management/_auth_base.html
Normal file
111
management/templates/management/_auth_base.html
Normal 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 %} · {{ 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">© {% 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>
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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">€</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 %}€{{ 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 %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %} · {{ 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 · 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>
|
||||
|
||||
76
management/templates/management/club_settings.html
Normal file
76
management/templates/management/club_settings.html
Normal 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 %}
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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" %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
|
||||
@@ -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" %}
|
||||
|
||||
@@ -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 %}· {{ 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" %} →</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" %}€{% 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" %}€{% 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 %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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">“{{ status.note }}”</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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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" }}–{{ 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">€ {{ membership.fee_amount }}</td>
|
||||
<td class="font-mono tabular-nums">
|
||||
€ {{ 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">€ {{ membership.fee_amount }}</td>
|
||||
<td data-label="{% trans 'Paid' %}" class="tabular-nums">
|
||||
€ {{ 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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>·{% 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>
|
||||
· <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" %}
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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="">—</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>
|
||||
→
|
||||
<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="">—</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="">—</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>
|
||||
→
|
||||
<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="">—</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" }} — <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" }} — {{ 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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
270
management/templates/management/signup_list.html
Normal file
270
management/templates/management/signup_list.html
Normal 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 %}
|
||||
@@ -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>
|
||||
|
||||
@@ -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">—</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">—</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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
|
||||
@@ -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">·</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">· {{ 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">· {{ 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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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" %}
|
||||
|
||||
Reference in New Issue
Block a user