Add a season workload chart to the referee management dashboard

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
This commit is contained in:
2026-08-22 17:25:09 +02:00
parent 747514f1ff
commit cf1a2f0f5a
4 changed files with 168 additions and 2 deletions

View File

@@ -1,5 +1,5 @@
{% extends "management/base.html" %}
{% load i18n lucide ui %}
{% 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 %}
@@ -46,6 +46,53 @@
</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 %} &middot; &euro;{{ 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>
@@ -104,3 +151,40 @@
</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 %}