Games refereed per referee this season, as a chart.js bar chart plus a small table with fees paid per referee, alongside season-at-a-glance KPIs (active referees, total games refereed, average per referee) -- separate from the existing "games in view" KPIs, which track the upcoming range picked by the filter strip rather than season-to-date workload. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
191 lines
13 KiB
HTML
191 lines
13 KiB
HTML
{% extends "management/base.html" %}
|
|
{% load i18n lucide static ui %}
|
|
|
|
{% block heading %}{% trans "Referee management" %}{% endblock heading %}
|
|
{% 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 filter_strip %}
|
|
<div class="flex flex-wrap items-center gap-3 border-b border-line bg-white px-7 py-3">
|
|
<div class="flex-1"></div>
|
|
<div class="flex items-center gap-1 rounded-full bg-steel p-1">
|
|
<a href="{% querystring range="week" %}" class="rounded-full px-3 py-1.5 font-display text-xs font-bold tracking-[.08em] uppercase {% if range_choice == "week" %}bg-white text-ink{% else %}text-on-dark hover:text-white{% endif %}">{% trans "This week" %}</a>
|
|
<a href="{% querystring range="two_weeks" %}" class="rounded-full px-3 py-1.5 font-display text-xs font-bold tracking-[.08em] uppercase {% if range_choice == "two_weeks" %}bg-white text-ink{% else %}text-on-dark hover:text-white{% endif %}">{% trans "+ next week" %}</a>
|
|
<a href="{% querystring range="10" %}" class="rounded-full px-3 py-1.5 font-display text-xs font-bold tracking-[.08em] uppercase {% if range_choice == "10" %}bg-white text-ink{% else %}text-on-dark hover:text-white{% endif %}">{% trans "Next 10" %}</a>
|
|
<a href="{% querystring range="25" %}" class="rounded-full px-3 py-1.5 font-display text-xs font-bold tracking-[.08em] uppercase {% if range_choice == "25" %}bg-white text-ink{% else %}text-on-dark hover:text-white{% endif %}">{% trans "Next 25" %}</a>
|
|
<a href="{% querystring range="50" %}" class="rounded-full px-3 py-1.5 font-display text-xs font-bold tracking-[.08em] uppercase {% if range_choice == "50" %}bg-white text-ink{% else %}text-on-dark hover:text-white{% endif %}">{% trans "Next 50" %}</a>
|
|
</div>
|
|
</div>
|
|
{% endblock filter_strip %}
|
|
|
|
{% block panel %}
|
|
<div class="flex flex-wrap 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 "Games in view" %}</div>
|
|
<div class="mt-1 font-display text-[34px] leading-none font-extrabold text-ink tabular-nums">{{ kpi_total }}</div>
|
|
<div class="mt-1 text-[13px] text-muted">{% trans "In the selected range" %}</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 "Without a referee" %}</div>
|
|
<div class="mt-1 font-display text-[34px] leading-none font-extrabold tabular-nums {% if kpi_no_referee %}text-club{% else %}text-ink{% endif %}">{{ kpi_no_referee }}</div>
|
|
<div class="mt-1 text-[13px] text-muted">{% trans "Nobody assigned yet" %}</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 "Partially staffed" %}</div>
|
|
<div class="mt-1 font-display text-[34px] leading-none font-extrabold tabular-nums {% if kpi_understaffed %}text-warn{% else %}text-ink{% endif %}">{{ kpi_understaffed }}</div>
|
|
<div class="mt-1 text-[13px] text-muted">{% trans "Still short a referee" %}</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 "Fully staffed" %}</div>
|
|
<div class="mt-1 font-display text-[34px] leading-none font-extrabold text-ok tabular-nums">{{ kpi_fully_staffed }}</div>
|
|
<div class="mt-1 text-[13px] text-muted">{% trans "Nothing left to do" %}</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 "Fees not set" %}</div>
|
|
<div class="mt-1 font-display text-[34px] leading-none font-extrabold tabular-nums {% if kpi_fees_pending %}text-warn{% else %}text-ink{% endif %}">{{ kpi_fees_pending }}</div>
|
|
<div class="mt-1 text-[13px] text-muted">{% trans "Referee fee missing" %}</div>
|
|
</div>
|
|
</div>
|
|
|
|
{% comment %}
|
|
Season workload -- separate from the "games in view" KPIs above
|
|
(which track the upcoming range picked by the filter strip), this is
|
|
about who's actually done the refereeing this season, all-time
|
|
within it, not just what's still upcoming.
|
|
{% endcomment %}
|
|
<div class="grid grid-cols-1 gap-3.5 lg:grid-cols-3">
|
|
<div class="card p-4.5 lg:col-span-2">
|
|
<div class="mb-1 font-display text-base font-extrabold tracking-[.08em] text-ink uppercase">{% trans "Games refereed per referee" %}</div>
|
|
<p class="mb-3 text-sm text-muted">{% trans "This season." %}</p>
|
|
{% if referee_stats %}
|
|
<div class="h-56">
|
|
<canvas id="referee-games-chart"></canvas>
|
|
</div>
|
|
{% else %}
|
|
<p class="py-6 text-center text-sm text-muted">{% trans "No referee assignments recorded yet this season." %}</p>
|
|
{% endif %}
|
|
</div>
|
|
<div class="card p-4.5">
|
|
<div class="mb-3 font-display text-base font-extrabold tracking-[.08em] text-ink uppercase">{% trans "Season at a glance" %}</div>
|
|
<dl>
|
|
<div class="flex items-center justify-between py-1.5">
|
|
<dt class="text-[13px] text-muted">{% trans "Active referees" %}</dt>
|
|
<dd class="font-mono text-sm font-semibold text-ink">{{ kpi_active_referees }}</dd>
|
|
</div>
|
|
<div class="flex items-center justify-between py-1.5">
|
|
<dt class="text-[13px] text-muted">{% trans "Games refereed" %}</dt>
|
|
<dd class="font-mono text-sm font-semibold text-ink">{{ kpi_total_assignments }}</dd>
|
|
</div>
|
|
<div class="flex items-center justify-between py-1.5">
|
|
<dt class="text-[13px] text-muted">{% trans "Average per referee" %}</dt>
|
|
<dd class="font-mono text-sm font-semibold text-ink">{{ kpi_avg_games_per_referee }}</dd>
|
|
</div>
|
|
</dl>
|
|
{% if referee_stats %}
|
|
<div class="mt-2 max-h-40 overflow-y-auto border-t border-rule">
|
|
{% for row in referee_stats %}
|
|
<div class="flex items-center justify-between gap-3 py-1.5 {% if not forloop.last %}border-b border-rule{% endif %}">
|
|
<span class="truncate text-[13px] text-ink">{{ row.member__first_name }} {{ row.member__last_name }}</span>
|
|
<span class="shrink-0 font-mono text-xs text-muted">{% blocktrans count counter=row.games %}{{ counter }} game{% plural %}{{ counter }} games{% endblocktrans %}{% if row.total_fees %} · €{{ row.total_fees|floatformat:2 }}{% endif %}</span>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
{% regroup games by start.date as day_groups %}
|
|
{% for day in day_groups %}
|
|
<div class="font-display text-xs font-bold tracking-[.12em] text-muted uppercase">{{ day.grouper|date:"l j F" }}</div>
|
|
<div class="grid grid-cols-1 gap-3 md:grid-cols-2 xl:grid-cols-3">
|
|
{% for game in day.list %}
|
|
<div class="card card-body gap-2 border-l-[3px] {% if not game.referee_rows %}border-l-club{% elif not game.referees_full %}border-l-warn{% else %}border-l-transparent{% 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>
|
|
{% 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 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="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 pending_signups=game.pending_signups can_manage_referees=True next_url=request.get_full_path %}
|
|
</div>
|
|
<form method="dialog" class="modal-backdrop"><button>close</button></form>
|
|
</dialog>
|
|
{% endfor %}
|
|
</div>
|
|
{% empty %}
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<p class="text-center text-muted">{% trans "No upcoming home games need a club-arranged referee." %}</p>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
{% endblock panel %}
|
|
|
|
{% block extra_body %}
|
|
{% trans "Games" as games_label %}
|
|
{{ charts|json_script:"referee-chart-data" }}
|
|
<script src="{% static 'js/chart.js' %}"></script>
|
|
<script>
|
|
(() => {
|
|
const data = JSON.parse(document.getElementById("referee-chart-data").textContent);
|
|
// Same pattern as management/home.html's own charts -- Chart.js paints to a
|
|
// canvas, so it needs the club accent resolved, not the CSS variable itself.
|
|
const clubColor = getComputedStyle(document.documentElement).getPropertyValue("--color-club").trim() || "#E4002B";
|
|
const ink = "#3A4658";
|
|
const grid = "#EEF0F3";
|
|
|
|
const gamesCanvas = document.getElementById("referee-games-chart");
|
|
if (gamesCanvas && data.referee_games.labels.length) {
|
|
new Chart(gamesCanvas, {
|
|
type: "bar",
|
|
data: {
|
|
labels: data.referee_games.labels,
|
|
datasets: [{label: "{{ games_label|escapejs }}", data: data.referee_games.games, backgroundColor: clubColor}],
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
indexAxis: "y",
|
|
plugins: {legend: {display: false}},
|
|
scales: {
|
|
x: {beginAtZero: true, ticks: {color: ink, precision: 0}, grid: {color: grid}},
|
|
y: {ticks: {color: ink}, grid: {display: false}},
|
|
},
|
|
},
|
|
});
|
|
}
|
|
})();
|
|
</script>
|
|
{% endblock extra_body %}
|