Feat: Saison-Report für Head Coaches

Implementiert einen druckbaren Saison-Report für Head Coaches, der eine
Übersicht über die Spielerteilnahme an allen Spielen einer ausgewählten
Saison bietet.

Wesentliche Änderungen:
- **Neue URL und View (`season_report` in `team_stats/views.py`):**
  Empfängt `team_id` und `season`, holt Spieler, Spiele und deren
  Teilnahmestatus und bereitet die Daten auf. Enthält eine
  Berechtigungsprüfung für den Head Coach.
- **Neue Vorlage (`team_stats/templates/team_stats/season_report.html`):**
  Zeigt eine Tabelle mit Spielen als Zeilen und Spielern als Spalten an.
  Teilnahmestatus wird mit Symbolen (✔, ✖, ?) dargestellt. Enthält
  druckspezifisches CSS, um die Lesbarkeit und das Seitenlayout für DIN A4
  (Querformat) zu optimieren, inklusive vertikal gedrehter Spielernamen in
  der Kopfzeile zur Platzersparnis.
- **Integration (`team_statistics.html`):** Ein "Saison-Report generieren"-Button
  wurde zur Team-Statistikseite hinzugefügt, der den Report für die
  aktuell ausgewählte Saison öffnet.
This commit is contained in:
Matthias Nagel 2025-11-23 14:33:56 +01:00
parent d45fc54280
commit 5cc9b387b9
4 changed files with 160 additions and 0 deletions

View File

@ -0,0 +1,107 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Season Report for {{ team.name }} - {{ season }}</title>
<style>
body {
font-family: sans-serif;
font-size: 10px;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
border: 1px solid #ccc;
padding: 4px;
text-align: center;
}
thead th {
background-color: #f2f2f2;
}
.player-header {
height: 150px;
white-space: nowrap;
position: relative;
}
.player-header > div {
transform: rotate(-90deg);
position: absolute;
left: 0;
bottom: 0;
transform-origin: 0 100%;
width: 150px; /* Should match height */
}
.status-attending {
color: green;
}
.status-rejected {
color: red;
}
.status-maybe {
color: #aaa;
}
.print-button {
margin: 1rem;
padding: 0.5rem 1rem;
}
@media print {
@page {
size: A4 landscape;
margin: 1cm;
}
body {
font-size: 9px;
}
.print-button {
display: none;
}
}
</style>
</head>
<body>
<button onclick="window.print();" class="print-button">Print Report</button>
<h2>Season Report</h2>
<p><strong>Team:</strong> {{ team.name }}</p>
<p><strong>Season:</strong> {{ season }}</p>
<table>
<thead>
<tr>
<th>Date</th>
<th>Opponent</th>
{% for player in players %}
<th class="player-header"><div>{{ player.get_full_name }}</div></th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for row in report_data %}
<tr>
<td>{{ row.game.start_time|date:"d.m.Y" }}</td>
<td>{{ row.game.opponent }}</td>
{% for status in row.statuses %}
<td class="status-{{ status }}">
{% if status == 'attending' %}
{% elif status == 'rejected' %}
{% else %}
?
{% endif %}
</td>
{% endfor %}
</tr>
{% empty %}
<tr>
<td colspan="{{ players|length|add:2 }}">No games found for this season.</td>
</tr>
{% endfor %}
</tbody>
</table>
</body>
</html>

View File

@ -22,6 +22,11 @@
<div class="col-md-2">
<button type="submit" class="btn btn-primary">Filter</button>
</div>
{% if selected_season %}
<div class="col-md-3">
<a href="{% url 'team_stats:season_report' team_id=team.id season=selected_season %}" class="btn btn-secondary" target="_blank">Generate Season Report</a>
</div>
{% endif %}
</div>
</form>
</div>

View File

@ -5,4 +5,5 @@ app_name = 'team_stats'
urlpatterns = [
path('team/<int:team_id>/', views.team_statistics, name='team_statistics'),
path('team/<int:team_id>/report/<str:season>/', views.season_report, name='season_report'),
]

View File

@ -147,3 +147,50 @@ def team_statistics(request, team_id):
}
return render(request, 'team_stats/team_statistics.html', context)
@login_required
def season_report(request, team_id, season):
team = get_object_or_404(Team, pk=team_id)
# Security check: only head coach can view
if request.user != team.head_coach:
return HttpResponseForbidden("You are not authorized to view this report.")
# 1. Get all players for the team, ordered
players = team.players.all().order_by('last_name', 'first_name')
player_ids = [p.id for p in players]
# 2. Get all games for the team and season
games = Game.objects.filter(team=team, season=season).order_by('start_time')
game_ids = [g.id for g in games]
# 3. Get all relevant participation data in one query
participations = EventParticipation.objects.filter(
event_id__in=game_ids,
user_id__in=player_ids
)
# 4. Create a fast lookup map
participation_map = {
(p.event_id, p.user_id): p.status for p in participations
}
# 5. Build the final data structure for the template
report_data = []
for game in games:
statuses = []
for player in players:
status = participation_map.get((game.id, player.id), 'maybe')
statuses.append(status)
report_data.append({
'game': game,
'statuses': statuses
})
context = {
'team': team,
'season': season,
'players': players,
'report_data': report_data,
}
return render(request, 'team_stats/season_report.html', context)