Erweiterte Spielerstatistiken: 7-Meter, Strafen, Spielzeit
Neue Metriken für Jugendhandball: 7m-Würfe/-Tore, Gelbe Karten, 2-Minuten-Strafen und Spielzeit. Migration, Model, Controller, Views und Übersetzungen (6 Sprachen) vollständig implementiert. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,7 @@ use App\Models\EventTimekeeper;
|
||||
use App\Models\File;
|
||||
use App\Models\FileCategory;
|
||||
use App\Models\Location;
|
||||
use App\Models\Season;
|
||||
use App\Models\Setting;
|
||||
use App\Models\Team;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
@@ -55,11 +56,20 @@ class EventController extends Controller
|
||||
$query->where('status', $request->status);
|
||||
}
|
||||
|
||||
// Saison-Filter
|
||||
if ($request->filled('season_id')) {
|
||||
$season = Season::find($request->season_id);
|
||||
if ($season) {
|
||||
$query->whereBetween('start_at', [$season->start_date, $season->end_date->endOfDay()]);
|
||||
}
|
||||
}
|
||||
|
||||
$events = $query->paginate(20)->withQueryString();
|
||||
$teams = Team::active()->orderBy('name')->get();
|
||||
$seasons = Season::orderByDesc('start_date')->get();
|
||||
$trashedEvents = Event::onlyTrashed()->with('team')->latest('deleted_at')->get();
|
||||
|
||||
return view('admin.events.index', compact('events', 'teams', 'trashedEvents'));
|
||||
return view('admin.events.index', compact('events', 'teams', 'seasons', 'trashedEvents'));
|
||||
}
|
||||
|
||||
public function create(): View
|
||||
@@ -229,6 +239,11 @@ class EventController extends Controller
|
||||
'stats.*.goalkeeper_shots' => ['nullable', 'integer', 'min:0', 'max:999'],
|
||||
'stats.*.goals' => ['nullable', 'integer', 'min:0', 'max:999'],
|
||||
'stats.*.shots' => ['nullable', 'integer', 'min:0', 'max:999'],
|
||||
'stats.*.penalty_goals' => ['nullable', 'integer', 'min:0', 'max:99'],
|
||||
'stats.*.penalty_shots' => ['nullable', 'integer', 'min:0', 'max:99'],
|
||||
'stats.*.yellow_cards' => ['nullable', 'integer', 'min:0', 'max:3'],
|
||||
'stats.*.two_minute_suspensions' => ['nullable', 'integer', 'min:0', 'max:3'],
|
||||
'stats.*.playing_time_minutes' => ['nullable', 'integer', 'min:0', 'max:90'],
|
||||
'stats.*.note' => ['nullable', 'string', 'max:500'],
|
||||
]);
|
||||
|
||||
@@ -242,9 +257,17 @@ class EventController extends Controller
|
||||
$gkSaves = $isGk && isset($data['goalkeeper_saves']) && $data['goalkeeper_saves'] !== '' ? (int) $data['goalkeeper_saves'] : null;
|
||||
$gkShots = $isGk && isset($data['goalkeeper_shots']) && $data['goalkeeper_shots'] !== '' ? (int) $data['goalkeeper_shots'] : null;
|
||||
$note = ! empty($data['note']) ? trim($data['note']) : null;
|
||||
$penaltyGoals = isset($data['penalty_goals']) && $data['penalty_goals'] !== '' ? (int) $data['penalty_goals'] : null;
|
||||
$penaltyShots = isset($data['penalty_shots']) && $data['penalty_shots'] !== '' ? (int) $data['penalty_shots'] : null;
|
||||
$yellowCards = isset($data['yellow_cards']) && $data['yellow_cards'] !== '' ? (int) $data['yellow_cards'] : null;
|
||||
$twoMinSuspensions = isset($data['two_minute_suspensions']) && $data['two_minute_suspensions'] !== '' ? (int) $data['two_minute_suspensions'] : null;
|
||||
$playingTime = isset($data['playing_time_minutes']) && $data['playing_time_minutes'] !== '' ? (int) $data['playing_time_minutes'] : null;
|
||||
|
||||
// Leere Einträge löschen
|
||||
if (! $isGk && $goals === null && $shots === null && $note === null && $position === null) {
|
||||
$hasData = $isGk || $goals !== null || $shots !== null || $note !== null || $position !== null
|
||||
|| $penaltyGoals !== null || $penaltyShots !== null || $yellowCards !== null
|
||||
|| $twoMinSuspensions !== null || $playingTime !== null;
|
||||
if (! $hasData) {
|
||||
EventPlayerStat::where('event_id', $event->id)->where('player_id', $playerId)->delete();
|
||||
continue;
|
||||
}
|
||||
@@ -258,6 +281,11 @@ class EventController extends Controller
|
||||
'goalkeeper_shots' => $gkShots,
|
||||
'goals' => $goals,
|
||||
'shots' => $shots,
|
||||
'penalty_goals' => $penaltyGoals,
|
||||
'penalty_shots' => $penaltyShots,
|
||||
'yellow_cards' => $yellowCards,
|
||||
'two_minute_suspensions' => $twoMinSuspensions,
|
||||
'playing_time_minutes' => $playingTime,
|
||||
'note' => $note,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -14,6 +14,7 @@ use App\Models\EventParticipant;
|
||||
use App\Models\EventPlayerStat;
|
||||
use App\Models\EventTimekeeper;
|
||||
use App\Models\Player;
|
||||
use App\Models\Season;
|
||||
use App\Models\Setting;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
@@ -32,6 +33,7 @@ class StatisticsController extends Controller
|
||||
|
||||
$request->validate([
|
||||
'team_id' => ['nullable', 'integer', 'exists:teams,id'],
|
||||
'season_id' => ['nullable', 'integer', 'exists:seasons,id'],
|
||||
'from' => ['nullable', 'date'],
|
||||
'to' => ['nullable', 'date'],
|
||||
]);
|
||||
@@ -49,12 +51,20 @@ class StatisticsController extends Controller
|
||||
$query->where('team_id', $request->team_id);
|
||||
}
|
||||
|
||||
if ($request->filled('from')) {
|
||||
$query->where('start_at', '>=', $request->from);
|
||||
}
|
||||
|
||||
if ($request->filled('to')) {
|
||||
$query->where('start_at', '<=', $request->to . ' 23:59:59');
|
||||
// Saison-Filter (hat Vorrang vor from/to)
|
||||
$activeSeason = null;
|
||||
if ($request->filled('season_id')) {
|
||||
$activeSeason = Season::find($request->season_id);
|
||||
if ($activeSeason) {
|
||||
$query->whereBetween('start_at', [$activeSeason->start_date, $activeSeason->end_date->endOfDay()]);
|
||||
}
|
||||
} elseif ($request->filled('from') || $request->filled('to')) {
|
||||
if ($request->filled('from')) {
|
||||
$query->where('start_at', '>=', $request->from);
|
||||
}
|
||||
if ($request->filled('to')) {
|
||||
$query->where('start_at', '<=', $request->to . ' 23:59:59');
|
||||
}
|
||||
}
|
||||
|
||||
$games = $query->orderByDesc('start_at')->get();
|
||||
@@ -93,7 +103,7 @@ class StatisticsController extends Controller
|
||||
$chartWinLoss = [
|
||||
'labels' => [__('admin.wins'), __('admin.losses'), __('admin.draws')],
|
||||
'data' => [$wins, $losses, $draws],
|
||||
'colors' => ['#22c55e', '#ef4444', '#9ca3af'],
|
||||
'colors' => ['#3e7750', '#8f504b', '#8e9db3'],
|
||||
];
|
||||
|
||||
// Spieler-Teilnahme pro Spiel (nur die letzten 15 Spiele)
|
||||
@@ -138,7 +148,12 @@ class StatisticsController extends Controller
|
||||
DB::raw('COALESCE(SUM(goals), 0) as total_goals_agg'),
|
||||
DB::raw('COALESCE(SUM(shots), 0) as total_shots_agg'),
|
||||
DB::raw('COALESCE(SUM(goalkeeper_saves), 0) as total_gk_saves'),
|
||||
DB::raw('COALESCE(SUM(goalkeeper_shots), 0) as total_gk_shots')
|
||||
DB::raw('COALESCE(SUM(goalkeeper_shots), 0) as total_gk_shots'),
|
||||
DB::raw('COALESCE(SUM(penalty_goals), 0) as total_penalty_goals'),
|
||||
DB::raw('COALESCE(SUM(penalty_shots), 0) as total_penalty_shots'),
|
||||
DB::raw('COALESCE(SUM(yellow_cards), 0) as total_yellow_cards'),
|
||||
DB::raw('COALESCE(SUM(two_minute_suspensions), 0) as total_suspensions'),
|
||||
DB::raw('AVG(playing_time_minutes) as avg_playing_time')
|
||||
)
|
||||
->groupBy('player_id')
|
||||
->get()
|
||||
@@ -195,6 +210,11 @@ class StatisticsController extends Controller
|
||||
'is_primary_gk' => $isPrimaryGk,
|
||||
'performance_rate' => $performanceRate,
|
||||
'performance_color' => $performanceColor,
|
||||
'total_penalty_goals' => $agg ? (int) $agg->total_penalty_goals : 0,
|
||||
'total_penalty_shots' => $agg ? (int) $agg->total_penalty_shots : 0,
|
||||
'total_yellow_cards' => $agg ? (int) $agg->total_yellow_cards : 0,
|
||||
'total_suspensions' => $agg ? (int) $agg->total_suspensions : 0,
|
||||
'avg_playing_time' => $agg && $agg->avg_playing_time ? (int) round($agg->avg_playing_time) : null,
|
||||
];
|
||||
})
|
||||
->filter()
|
||||
@@ -270,11 +290,14 @@ class StatisticsController extends Controller
|
||||
$totalCateringEvents = $cateringEventIds->count();
|
||||
$totalTimekeeperEvents = $timekeeperEventIds->count();
|
||||
|
||||
$seasons = Season::orderByDesc('start_date')->get();
|
||||
|
||||
return view('admin.statistics.index', compact(
|
||||
'games', 'teams', 'wins', 'losses', 'draws', 'winRate', 'totalWithScore',
|
||||
'chartWinLoss', 'chartPlayerParticipation', 'chartParentInvolvement',
|
||||
'playerRanking', 'totalGames', 'courtPlayers',
|
||||
'parentRanking', 'totalCateringEvents', 'totalTimekeeperEvents'
|
||||
'parentRanking', 'totalCateringEvents', 'totalTimekeeperEvents',
|
||||
'seasons', 'activeSeason'
|
||||
));
|
||||
}
|
||||
|
||||
@@ -296,6 +319,12 @@ class StatisticsController extends Controller
|
||||
$gkGames = $stats->where('is_goalkeeper', true);
|
||||
$totalGkSaves = $gkGames->sum('goalkeeper_saves');
|
||||
$totalGkShots = $gkGames->sum('goalkeeper_shots');
|
||||
$totalPenaltyGoals = $stats->sum('penalty_goals');
|
||||
$totalPenaltyShots = $stats->sum('penalty_shots');
|
||||
$totalYellowCards = $stats->sum('yellow_cards');
|
||||
$totalSuspensions = $stats->sum('two_minute_suspensions');
|
||||
$playingTimeStats = $stats->whereNotNull('playing_time_minutes');
|
||||
$avgPlayingTime = $playingTimeStats->count() > 0 ? (int) round($playingTimeStats->avg('playing_time_minutes')) : null;
|
||||
|
||||
return response()->json([
|
||||
'player' => [
|
||||
@@ -312,6 +341,12 @@ class StatisticsController extends Controller
|
||||
'total_saves' => $totalGkSaves,
|
||||
'total_gk_shots' => $totalGkShots,
|
||||
'save_rate' => $totalGkShots > 0 ? round(($totalGkSaves / $totalGkShots) * 100, 1) : null,
|
||||
'total_penalty_goals' => $totalPenaltyGoals,
|
||||
'total_penalty_shots' => $totalPenaltyShots,
|
||||
'penalty_rate' => $totalPenaltyShots > 0 ? round(($totalPenaltyGoals / $totalPenaltyShots) * 100, 1) : null,
|
||||
'total_yellow_cards' => $totalYellowCards,
|
||||
'total_suspensions' => $totalSuspensions,
|
||||
'avg_playing_time' => $avgPlayingTime,
|
||||
],
|
||||
'games' => $stats->map(fn ($s) => [
|
||||
'date' => $s->event->start_at->format('d.m.Y'),
|
||||
@@ -320,6 +355,11 @@ class StatisticsController extends Controller
|
||||
'position' => $s->position?->shortLabel(),
|
||||
'goals' => $s->goals,
|
||||
'shots' => $s->shots,
|
||||
'penalty_goals' => $s->penalty_goals,
|
||||
'penalty_shots' => $s->penalty_shots,
|
||||
'yellow_cards' => $s->yellow_cards,
|
||||
'two_minute_suspensions' => $s->two_minute_suspensions,
|
||||
'playing_time_minutes' => $s->playing_time_minutes,
|
||||
'is_goalkeeper' => $s->is_goalkeeper,
|
||||
'goalkeeper_saves' => $s->goalkeeper_saves,
|
||||
'goalkeeper_shots' => $s->goalkeeper_shots,
|
||||
|
||||
@@ -17,6 +17,11 @@ class EventPlayerStat extends Model
|
||||
'goalkeeper_shots',
|
||||
'goals',
|
||||
'shots',
|
||||
'penalty_goals',
|
||||
'penalty_shots',
|
||||
'yellow_cards',
|
||||
'two_minute_suspensions',
|
||||
'playing_time_minutes',
|
||||
'note',
|
||||
];
|
||||
|
||||
@@ -27,6 +32,11 @@ class EventPlayerStat extends Model
|
||||
'goalkeeper_shots' => 'integer',
|
||||
'goals' => 'integer',
|
||||
'shots' => 'integer',
|
||||
'penalty_goals' => 'integer',
|
||||
'penalty_shots' => 'integer',
|
||||
'yellow_cards' => 'integer',
|
||||
'two_minute_suspensions' => 'integer',
|
||||
'playing_time_minutes' => 'integer',
|
||||
];
|
||||
|
||||
public function event(): BelongsTo
|
||||
@@ -63,6 +73,18 @@ class EventPlayerStat extends Model
|
||||
return round(($this->goals / $this->shots) * 100, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 7-Meter-Quote in Prozent.
|
||||
*/
|
||||
public function penaltyRate(): ?float
|
||||
{
|
||||
if (! $this->penalty_shots || $this->penalty_shots === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return round(($this->penalty_goals / $this->penalty_shots) * 100, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob der Eintrag leer ist (keine relevanten Daten).
|
||||
*/
|
||||
@@ -73,6 +95,11 @@ class EventPlayerStat extends Model
|
||||
&& ! $this->goalkeeper_shots
|
||||
&& ! $this->goals
|
||||
&& ! $this->shots
|
||||
&& ! $this->penalty_goals
|
||||
&& ! $this->penalty_shots
|
||||
&& ! $this->yellow_cards
|
||||
&& ! $this->two_minute_suspensions
|
||||
&& ! $this->playing_time_minutes
|
||||
&& ! $this->note;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user