diff --git a/app/Http/Controllers/Admin/EventController.php b/app/Http/Controllers/Admin/EventController.php index 150873b..1e77612 100755 --- a/app/Http/Controllers/Admin/EventController.php +++ b/app/Http/Controllers/Admin/EventController.php @@ -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, ] ); diff --git a/app/Http/Controllers/Admin/StatisticsController.php b/app/Http/Controllers/Admin/StatisticsController.php index 6efff85..029c79a 100644 --- a/app/Http/Controllers/Admin/StatisticsController.php +++ b/app/Http/Controllers/Admin/StatisticsController.php @@ -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, diff --git a/app/Models/EventPlayerStat.php b/app/Models/EventPlayerStat.php index 7cf6079..45e6729 100644 --- a/app/Models/EventPlayerStat.php +++ b/app/Models/EventPlayerStat.php @@ -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; } } diff --git a/database/migrations/0040_01_01_000000_add_extended_stats_to_event_player_stats.php b/database/migrations/0040_01_01_000000_add_extended_stats_to_event_player_stats.php new file mode 100644 index 0000000..ec81dae --- /dev/null +++ b/database/migrations/0040_01_01_000000_add_extended_stats_to_event_player_stats.php @@ -0,0 +1,26 @@ +unsignedSmallInteger('penalty_goals')->nullable()->after('shots'); + $table->unsignedSmallInteger('penalty_shots')->nullable()->after('penalty_goals'); + $table->unsignedTinyInteger('yellow_cards')->nullable()->after('penalty_shots'); + $table->unsignedTinyInteger('two_minute_suspensions')->nullable()->after('yellow_cards'); + $table->unsignedSmallInteger('playing_time_minutes')->nullable()->after('two_minute_suspensions'); + }); + } + + public function down(): void + { + Schema::table('event_player_stats', function (Blueprint $table) { + $table->dropColumn(['penalty_goals', 'penalty_shots', 'yellow_cards', 'two_minute_suspensions', 'playing_time_minutes']); + }); + } +}; diff --git a/lang/ar/events.php b/lang/ar/events.php index 2a12ee2..bbb16af 100755 --- a/lang/ar/events.php +++ b/lang/ar/events.php @@ -89,6 +89,12 @@ return [ 'stats_note' => 'ملاحظة', 'stats_hit_rate' => 'نسبة الإصابة', 'stats_save_rate' => 'نسبة التصدي', + 'stats_penalty_shots' => 'رميات 7م', + 'stats_penalty_goals' => 'أهداف 7م', + 'stats_yellow_cards' => 'بطاقة صفراء', + 'stats_two_min' => '2 دقيقة', + 'stats_playing_time_short' => 'دقيقة', + 'stats_cards' => 'عقوبات', 'stats_no_data' => 'لا توجد بيانات إحصائية.', 'stats_position' => 'المركز', diff --git a/lang/de/events.php b/lang/de/events.php index c391ca8..bd5ea6e 100755 --- a/lang/de/events.php +++ b/lang/de/events.php @@ -102,6 +102,12 @@ return [ 'stats_note' => 'Bemerkung', 'stats_hit_rate' => 'Trefferquote', 'stats_save_rate' => 'Fangquote', + 'stats_penalty_shots' => '7m-Würfe', + 'stats_penalty_goals' => '7m-Tore', + 'stats_yellow_cards' => 'Gelbe K.', + 'stats_two_min' => '2-Min', + 'stats_playing_time_short' => 'Min.', + 'stats_cards' => 'Strafen', 'stats_no_data' => 'Keine Statistikdaten.', 'stats_position' => 'Position', diff --git a/lang/en/events.php b/lang/en/events.php index 7c41607..0467a49 100755 --- a/lang/en/events.php +++ b/lang/en/events.php @@ -88,6 +88,12 @@ return [ 'stats_note' => 'Note', 'stats_hit_rate' => 'Hit rate', 'stats_save_rate' => 'Save rate', + 'stats_penalty_shots' => 'Pen. shots', + 'stats_penalty_goals' => 'Pen. goals', + 'stats_yellow_cards' => 'Yellow C.', + 'stats_two_min' => '2-Min', + 'stats_playing_time_short' => 'Min.', + 'stats_cards' => 'Cards', 'stats_no_data' => 'No statistics data.', 'stats_position' => 'Position', diff --git a/lang/pl/events.php b/lang/pl/events.php index abb8902..da23c3c 100755 --- a/lang/pl/events.php +++ b/lang/pl/events.php @@ -89,6 +89,12 @@ return [ 'stats_note' => 'Uwaga', 'stats_hit_rate' => 'Skuteczność', 'stats_save_rate' => 'Skuteczność obron', + 'stats_penalty_shots' => 'Rzuty 7m', + 'stats_penalty_goals' => 'Bramki 7m', + 'stats_yellow_cards' => 'Żółte k.', + 'stats_two_min' => '2-Min', + 'stats_playing_time_short' => 'Min.', + 'stats_cards' => 'Kary', 'stats_no_data' => 'Brak danych statystycznych.', 'stats_position' => 'Pozycja', diff --git a/lang/ru/events.php b/lang/ru/events.php index 0558496..bef84c9 100755 --- a/lang/ru/events.php +++ b/lang/ru/events.php @@ -102,6 +102,12 @@ return [ 'stats_note' => 'Примечание', 'stats_hit_rate' => 'Процент попаданий', 'stats_save_rate' => 'Процент отражений', + 'stats_penalty_shots' => 'Бр. 7м', + 'stats_penalty_goals' => 'Голы 7м', + 'stats_yellow_cards' => 'Жёлт. к.', + 'stats_two_min' => '2 мин', + 'stats_playing_time_short' => 'Мин.', + 'stats_cards' => 'Наказания', 'stats_no_data' => 'Нет данных статистики.', 'stats_position' => 'Позиция', diff --git a/lang/tr/events.php b/lang/tr/events.php index 34843b7..1dd96f2 100755 --- a/lang/tr/events.php +++ b/lang/tr/events.php @@ -89,6 +89,12 @@ return [ 'stats_note' => 'Not', 'stats_hit_rate' => 'İsabet oranı', 'stats_save_rate' => 'Kurtarış oranı', + 'stats_penalty_shots' => '7m atış', + 'stats_penalty_goals' => '7m gol', + 'stats_yellow_cards' => 'Sarı k.', + 'stats_two_min' => '2 dk', + 'stats_playing_time_short' => 'Dk.', + 'stats_cards' => 'Cezalar', 'stats_no_data' => 'İstatistik verisi yok.', 'stats_position' => 'Pozisyon', diff --git a/resources/views/admin/events/edit.blade.php b/resources/views/admin/events/edit.blade.php index f26c15d..925528e 100755 --- a/resources/views/admin/events/edit.blade.php +++ b/resources/views/admin/events/edit.blade.php @@ -383,6 +383,11 @@ {{ __('events.stats_saves') }} {{ __('events.stats_shots') }} {{ __('events.stats_goals') }} + 7m-W + 7m-T + {{ __('events.stats_yellow_cards') }} + {{ __('events.stats_two_min') }} + {{ __('events.stats_playing_time_short') }} {{ __('events.stats_note') }} @@ -436,6 +441,31 @@ value="{{ $stat?->goals }}" class="w-16 px-1 py-1 border border-gray-300 rounded text-center text-sm"> + + + + + + + + + + + + + + + -
+
+ @if ($seasons->isNotEmpty())
+ + +
+ @endif +
-
+
- @if (request()->hasAny(['team_id', 'from', 'to'])) + @if (request()->hasAny(['team_id', 'from', 'to', 'season_id'])) {{ __('admin.filter_reset') }} @endif @@ -157,6 +170,9 @@ {{ __('admin.position') }} {{ __('admin.games_played') }} {{ __('admin.player_goals') }} + 7m + {{ __('admin.stats_cards') }} + {{ __('admin.stats_avg_time') }} {{ __('admin.participation_rate') }} @@ -167,7 +183,7 @@ @if (!$separatorShown && !$entry->is_primary_gk) @php $separatorShown = true; @endphp @if ($index > 0) -
+
@endif @endif @@ -197,6 +213,32 @@ 0 @endif + + @if ($entry->total_penalty_shots > 0) + {{ $entry->total_penalty_goals }}/{{ $entry->total_penalty_shots }} + @else + + @endif + + + @if ($entry->total_yellow_cards > 0 || $entry->total_suspensions > 0) + @if ($entry->total_yellow_cards > 0) + {{ $entry->total_yellow_cards }} + @endif + @if ($entry->total_suspensions > 0) + {{ $entry->total_suspensions }}×2' + @endif + @else + + @endif + + + @if ($entry->avg_playing_time) + {{ $entry->avg_playing_time }}' + @else + + @endif + {{ $entry->rate }}% @@ -234,31 +276,31 @@ 'kreislaeufer' => ['x' => 200, 'y' => 180], ]; $colorMap = [ - 'green' => ['fill' => '#22c55e', 'text' => '#fff'], - 'yellow' => ['fill' => '#eab308', 'text' => '#fff'], - 'red' => ['fill' => '#ef4444', 'text' => '#fff'], - 'gray' => ['fill' => '#9ca3af', 'text' => '#fff'], + 'green' => ['fill' => '#305f3f', 'text' => '#fff'], + 'yellow' => ['fill' => '#806130', 'text' => '#fff'], + 'red' => ['fill' => '#76403b', 'text' => '#fff'], + 'gray' => ['fill' => '#667788', 'text' => '#fff'], ]; @endphp {{-- Spielfeld-Hintergrund --}} - - + + {{-- Mittellinie --}} - + {{-- Tor (unten) --}} - + {{-- 6m-Torraum (Halbkreis) --}} - + {{-- 9m-Freiwurflinie (gestrichelt) --}} - + {{-- 7m-Markierung --}} - + {{-- Spieler-Positionen --}} @foreach ($courtPositions as $posValue => $coords) @@ -268,7 +310,7 @@ $posEnum = \App\Enums\PlayerPosition::tryFrom($posValue); @endphp - + @if ($entry) {{ $entry->player->jersey_number ?? $entry->player->getInitials() }} @@ -282,7 +324,7 @@ @endif {{-- Positions-Kürzel unter dem Kreis --}} - + {{ $posEnum?->shortLabel() }} @@ -372,6 +414,32 @@
+ + {{-- Erweiterte Statistiken --}} +
+ + + +
@@ -393,6 +461,9 @@ {{ __('events.stats_goals') }} {{ __('events.stats_shots') }} {{ __('events.stats_goalkeeper') }} + 7m + {{ __('events.stats_cards') }} + Min. {{ __('events.stats_note') }} @@ -417,6 +488,20 @@ + + + + + + + + + + @@ -564,7 +649,7 @@ datasets: [{ label: @js(__('admin.nav_players')), data: playerData.data, - backgroundColor: '#3b82f6', + backgroundColor: '#4a5e7a', borderRadius: 3, }] }, @@ -586,13 +671,13 @@ { label: @js(__('events.catering_short')), data: parentData.catering, - backgroundColor: '#f59e0b', + backgroundColor: '#99783a', borderRadius: 3, }, { label: @js(__('events.timekeeper_short')), data: parentData.timekeepers, - backgroundColor: '#8b5cf6', + backgroundColor: '#6b5a84', borderRadius: 3, } ]