feat: Team-Statistik-Dashboard für Headcoaches

Fügt eine neue Seite hinzu, auf der Headcoaches Statistiken für ihre Teams einsehen können.

Die Statistikseite umfasst:
- W-L-Bilanz, Siegquote (PCT) und aktuelle Serie
- Balkendiagramm für erzielte und zugelassene Runs (RS vs. RA)
- "Luck-O-Meter" zum Vergleich der realen und pythagoreischen Siegquote
- Inning-Heatmap zur Anzeige der erzielten Runs pro Inning

Die Seite ist über einen neuen Button auf dem Dashboard für jedes vom Headcoach trainierte Team erreichbar.
This commit is contained in:
Matthias Nagel 2025-11-19 05:31:52 +01:00
parent aba0533b82
commit 56e7393524
14 changed files with 203 additions and 16 deletions

21
.gitignore vendored
View File

@ -1,17 +1,6 @@
*.pyc *.sqlite3
# Generische Python-Ignorierungen
# Kompilierte Python-Dateien
*.pyc
__pycache__/
# Virtuelle Umgebungen (häufige Namen)
venv/ venv/
env/ __pycache__/
.venv/ *.pyc
.DS_Store
# Abhängigkeitsdateien (z.B. bei Setuptools/pip) db.sqlite3
*.egg-info/
.eggs/
dist/
build/

View File

@ -41,6 +41,7 @@ INSTALLED_APPS = [
'clubs', 'clubs',
'calendars', 'calendars',
'dashboard', 'dashboard',
'team_stats',
] ]
MIDDLEWARE = [ MIDDLEWARE = [

View File

@ -23,5 +23,6 @@ urlpatterns = [
path('accounts/', include('accounts.urls')), path('accounts/', include('accounts.urls')),
path('clubs/', include('clubs.urls')), path('clubs/', include('clubs.urls')),
path('calendars/', include('calendars.urls')), path('calendars/', include('calendars.urls')),
path('statistics/', include('team_stats.urls')),
path('', include('dashboard.urls')), path('', include('dashboard.urls')),
] ]

View File

@ -6,6 +6,11 @@
<div class="d-flex justify-content-between align-items-center mb-3"> <div class="d-flex justify-content-between align-items-center mb-3">
<h2>Dashboard</h2> <h2>Dashboard</h2>
<div> <div>
{% if user.coached_teams.all %}
{% for team in user.coached_teams.all %}
<a href="{% url 'team_stats:team_statistics' team.id %}" class="btn btn-secondary">Statistics for {{ team.name }}</a>
{% endfor %}
{% endif %}
{% if user.coached_teams.all or user.assisted_teams.all %} {% if user.coached_teams.all or user.assisted_teams.all %}
<a href="{% url 'select-event-type' %}" class="btn btn-primary">Create New Event</a> <a href="{% url 'select-event-type' %}" class="btn btn-primary">Create New Event</a>
<a href="{% url 'player_list' %}" class="btn btn-info">Player List</a> <a href="{% url 'player_list' %}" class="btn btn-info">Player List</a>

4
docs/statiks.md3 Normal file
View File

@ -0,0 +1,4 @@
1. Headline: W-L Record, PCT, und Streak (z.B. "Won 3").
2. Offense vs Defense: Balkendiagramm RS vs RA.
3. Luck-O-Meter: Vergleich von Real Winning % vs. Pythagorean Winning %. (Fans diskutieren lieben Diskussionen darüber, ob ihr Team "gut" oder nur "glücklich" ist).
4. Inning-Heatmap: Eine Tabelle von Inning 1 bis 9, die zeigt, in welchem Inning das Team die meisten Runs erzielt (z.B. "Wir sind ein 'Late Inning' Team").

0
team_stats/__init__.py Normal file
View File

3
team_stats/admin.py Normal file
View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
team_stats/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class TeamStatsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'team_stats'

View File

3
team_stats/models.py Normal file
View File

@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

View File

@ -0,0 +1,70 @@
{% extends "base.html" %}
{% block content %}
<div class="container mt-4">
<h1 class="mb-4">Statistics for {{ team.name }}</h1>
<!-- W-L Record, PCT, and Streak -->
<div class="row">
<div class="col-md-4">
<div class="card">
<div class="card-header">Record</div>
<div class="card-body">
<h5 class="card-title">{{ wins }} - {{ losses }}</h5>
<p class="card-text">PCT: {{ pct|floatformat:3 }}</p>
<p class="card-text">Streak: {{ streak }}</p>
</div>
</div>
</div>
<div class="col-md-8">
<div class="card">
<div class="card-header">Offense vs Defense</div>
<div class="card-body">
<p>Runs Scored: {{ runs_scored }}</p>
<p>Runs Allowed: {{ runs_allowed }}</p>
<div class="progress" style="height: 30px;">
<div class="progress-bar bg-success" role="progressbar" style="width: {% widthratio runs_scored runs_scored|add:runs_allowed 100 %}%;" aria-valuenow="{{ runs_scored }}" aria-valuemin="0" aria-valuemax="{{ runs_scored|add:runs_allowed }}">{{ runs_scored }}</div>
<div class="progress-bar bg-danger" role="progressbar" style="width: {% widthratio runs_allowed runs_scored|add:runs_allowed 100 %}%;" aria-valuenow="{{ runs_allowed }}" aria-valuemin="0" aria-valuemax="{{ runs_scored|add:runs_allowed }}">{{ runs_allowed }}</div>
</div>
</div>
</div>
</div>
</div>
<!-- Luck-O-Meter and Inning Heatmap -->
<div class="row mt-4">
<div class="col-md-4">
<div class="card">
<div class="card-header">Luck-O-Meter</div>
<div class="card-body">
<p>Real Winning %: {{ pct|floatformat:3 }}</p>
<p>Pythagorean Winning %: {{ pythagorean_pct|floatformat:3 }}</p>
</div>
</div>
</div>
<div class="col-md-8">
<div class="card">
<div class="card-header">Inning Heatmap (Runs Scored)</div>
<div class="card-body">
<table class="table table-bordered text-center">
<thead>
<tr>
{% for inning, runs in inning_runs.items %}
<th>{{ inning }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
<tr>
{% for inning, runs in inning_runs.items %}
<td>{{ runs }}</td>
{% endfor %}
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

3
team_stats/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

8
team_stats/urls.py Normal file
View File

@ -0,0 +1,8 @@
from django.urls import path
from . import views
app_name = 'team_stats'
urlpatterns = [
path('team/<int:team_id>/', views.team_statistics, name='team_statistics'),
]

94
team_stats/views.py Normal file
View File

@ -0,0 +1,94 @@
from django.shortcuts import render, get_object_or_404
from django.contrib.auth.decorators import login_required
from django.http import HttpResponseForbidden
from clubs.models import Team
from calendars.models import Game, GameResult
@login_required
def team_statistics(request, team_id):
team = get_object_or_404(Team, pk=team_id)
# Check if the user is the head coach of the team
if request.user != team.head_coach:
return HttpResponseForbidden("You are not authorized to view this page.")
games = Game.objects.filter(team=team, result__isnull=False).order_by('start_time')
wins = 0
losses = 0
runs_scored = 0
runs_allowed = 0
inning_runs = {i: 0 for i in range(1, 10)}
streak_counter = 0
current_streak_type = None
last_game_result = None
for game in games:
result = game.result
home_score = sum(result.inning_results.get('home', []))
away_score = sum(result.inning_results.get('away', []))
if game.is_home_game:
team_score = home_score
opponent_score = away_score
else:
team_score = away_score
opponent_score = home_score
runs_scored += team_score
runs_allowed += opponent_score
# W-L Record and Streak
if team_score > opponent_score:
wins += 1
if last_game_result == 'win':
streak_counter += 1
else:
streak_counter = 1
last_game_result = 'win'
elif team_score < opponent_score:
losses += 1
if last_game_result == 'loss':
streak_counter += 1
else:
streak_counter = 1
last_game_result = 'loss'
# Inning Heatmap
team_innings = result.inning_results.get('home' if game.is_home_game else 'away', [])
for i, runs in enumerate(team_innings):
if i + 1 in inning_runs:
inning_runs[i + 1] += runs
# Winning Percentage (PCT)
total_games = wins + losses
pct = (wins / total_games) * 100 if total_games > 0 else 0
# Streak
if last_game_result == 'win':
streak_str = f"Won {streak_counter}"
elif last_game_result == 'loss':
streak_str = f"Lost {streak_counter}"
else:
streak_str = "N/A"
# Pythagorean Winning Percentage
if runs_scored > 0 or runs_allowed > 0:
pythagorean_pct = (runs_scored**2 / (runs_scored**2 + runs_allowed**2)) * 100
else:
pythagorean_pct = 0
context = {
'team': team,
'wins': wins,
'losses': losses,
'pct': pct,
'streak': streak_str,
'runs_scored': runs_scored,
'runs_allowed': runs_allowed,
'pythagorean_pct': pythagorean_pct,
'inning_runs': inning_runs,
}
return render(request, 'team_stats/team_statistics.html', context)