user())) { abort(403); } $request->validate([ 'team_id' => ['nullable', 'integer', 'exists:teams,id'], 'from' => ['nullable', 'date'], 'to' => ['nullable', 'date'], ]); $query = Event::with(['team']) ->withCount([ 'participants as players_yes_count' => fn ($q) => $q->where('status', 'yes'), 'caterings as caterings_yes_count' => fn ($q) => $q->where('status', 'yes'), 'timekeepers as timekeepers_yes_count' => fn ($q) => $q->where('status', 'yes'), ]) ->whereIn('type', [EventType::HomeGame, EventType::AwayGame]) ->where('status', EventStatus::Published); if ($request->filled('team_id')) { $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'); } $games = $query->orderByDesc('start_at')->get(); // Statistiken berechnen $gamesWithScore = $games->filter(fn ($g) => $g->score_home !== null && $g->score_away !== null); $wins = 0; $losses = 0; $draws = 0; foreach ($gamesWithScore as $game) { if ($game->type === EventType::HomeGame) { if ($game->score_home > $game->score_away) { $wins++; } elseif ($game->score_home < $game->score_away) { $losses++; } else { $draws++; } } else { if ($game->score_away > $game->score_home) { $wins++; } elseif ($game->score_away < $game->score_home) { $losses++; } else { $draws++; } } } $totalWithScore = $gamesWithScore->count(); $winRate = $totalWithScore > 0 ? round(($wins / $totalWithScore) * 100) : 0; // Chart-Daten $chartWinLoss = [ 'labels' => [__('admin.wins'), __('admin.losses'), __('admin.draws')], 'data' => [$wins, $losses, $draws], 'colors' => ['#22c55e', '#ef4444', '#9ca3af'], ]; // Spieler-Teilnahme pro Spiel (nur die letzten 15 Spiele) $recentGames = $games->take(15)->reverse()->values(); $chartPlayerParticipation = [ 'labels' => $recentGames->map(fn ($g) => $g->start_at->format('d.m.'))->toArray(), 'data' => $recentGames->map(fn ($g) => $g->players_yes_count)->toArray(), ]; // Eltern-Engagement (Catering + Zeitnehmer) $chartParentInvolvement = [ 'labels' => $recentGames->map(fn ($g) => $g->start_at->format('d.m.'))->toArray(), 'catering' => $recentGames->map(fn ($g) => $g->caterings_yes_count)->toArray(), 'timekeepers' => $recentGames->map(fn ($g) => $g->timekeepers_yes_count)->toArray(), ]; $teams = Team::where('is_active', true)->orderBy('name')->get(); // ── Spieler-Rangliste ────────────────────────────────── $gameIds = $games->pluck('id'); $totalGames = $games->count(); // Tore pro Spieler aus event_player_stats $goalsByPlayer = EventPlayerStat::whereIn('event_id', $gameIds) ->select('player_id', DB::raw('COALESCE(SUM(goals), 0) as total_goals')) ->groupBy('player_id') ->pluck('total_goals', 'player_id'); // Häufigste Position pro Spieler aus event_player_stats $positionCounts = EventPlayerStat::whereIn('event_id', $gameIds) ->whereNotNull('position') ->select('player_id', 'position', DB::raw('COUNT(*) as cnt')) ->groupBy('player_id', 'position') ->get() ->groupBy('player_id') ->map(fn ($g) => $g->sortByDesc('cnt')->first()->position); // Aggregierte Performance-Daten pro Spieler $playerAggStats = EventPlayerStat::whereIn('event_id', $gameIds) ->select( 'player_id', 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') ) ->groupBy('player_id') ->get() ->keyBy('player_id'); $playerRanking = collect(); if ($totalGames > 0) { $playerRanking = EventParticipant::select('player_id', DB::raw('COUNT(*) as total_assigned'), DB::raw('SUM(CASE WHEN status = \'yes\' THEN 1 ELSE 0 END) as games_played')) ->whereIn('event_id', $gameIds) ->whereNotNull('player_id') ->groupBy('player_id') ->get() ->map(function ($row) use ($totalGames, $goalsByPlayer, $positionCounts, $playerAggStats) { $player = Player::withTrashed()->find($row->player_id); if (!$player) { return null; } // Primäre Position: häufigste aus Stats, Fallback auf player.position $primaryPosition = $positionCounts->get($row->player_id) ?? $player->position; $isPrimaryGk = $primaryPosition?->isGoalkeeper() ?? false; // Performance-Rate + Ampelfarbe berechnen $agg = $playerAggStats->get($row->player_id); $performanceRate = null; $performanceColor = 'gray'; if ($agg) { if ($isPrimaryGk) { // Torwart: Fangquote if ($agg->total_gk_shots > 0) { $performanceRate = round(($agg->total_gk_saves / $agg->total_gk_shots) * 100, 1); $performanceColor = $performanceRate >= 40 ? 'green' : ($performanceRate >= 25 ? 'yellow' : 'red'); } } else { // Feldspieler: Trefferquote if ($agg->total_shots_agg > 0) { $performanceRate = round(($agg->total_goals_agg / $agg->total_shots_agg) * 100, 1); $performanceColor = $performanceRate >= 50 ? 'green' : ($performanceRate >= 30 ? 'yellow' : 'red'); } } } return (object) [ 'player' => $player, 'games_played' => (int) $row->games_played, 'total_assigned' => (int) $row->total_assigned, 'total_games' => $totalGames, 'total_goals' => (int) ($goalsByPlayer[$row->player_id] ?? 0), 'rate' => $row->total_assigned > 0 ? round(($row->games_played / $row->total_assigned) * 100) : 0, 'primary_position' => $primaryPosition, 'is_primary_gk' => $isPrimaryGk, 'performance_rate' => $performanceRate, 'performance_color' => $performanceColor, ]; }) ->filter() ->sortBy([ // Torwarte zuerst, dann Feldspieler ['is_primary_gk', 'desc'], ['games_played', 'desc'], ]) ->values(); } // Spielfeld-Aufstellung: Bester Spieler pro Position (meiste Spiele) $courtPlayers = $playerRanking ->filter(fn ($e) => $e->primary_position !== null) ->groupBy(fn ($e) => $e->primary_position->value) ->map(fn ($group) => $group->sortByDesc('games_played')->first()); // ── Eltern-Engagement-Rangliste ──────────────────────── // Alle publizierten Events (nicht nur Spiele) mit gleichen Team/Datum-Filtern $allEventsQuery = Event::where('status', EventStatus::Published); if ($request->filled('team_id')) { $allEventsQuery->where('team_id', $request->team_id); } if ($request->filled('from')) { $allEventsQuery->where('start_at', '>=', $request->from); } if ($request->filled('to')) { $allEventsQuery->where('start_at', '<=', $request->to . ' 23:59:59'); } $allEventIds = $allEventsQuery->pluck('id'); // Catering-Events (nur Typen die Catering haben) $cateringEventIds = $allEventsQuery->clone() ->whereNotIn('type', [EventType::AwayGame, EventType::Meeting]) ->pluck('id'); // Zeitnehmer-Events (identisch wie Catering) $timekeeperEventIds = $cateringEventIds; $cateringCounts = EventCatering::select('user_id', DB::raw('COUNT(*) as count')) ->whereIn('event_id', $cateringEventIds) ->where('status', CateringStatus::Yes) ->groupBy('user_id') ->pluck('count', 'user_id'); $timekeeperCounts = EventTimekeeper::select('user_id', DB::raw('COUNT(*) as count')) ->whereIn('event_id', $timekeeperEventIds) ->where('status', CateringStatus::Yes) ->groupBy('user_id') ->pluck('count', 'user_id'); $parentUserIds = $cateringCounts->keys()->merge($timekeeperCounts->keys())->unique(); $parentRanking = User::withTrashed() ->whereIn('id', $parentUserIds) ->get() ->map(function ($user) use ($cateringCounts, $timekeeperCounts) { $catering = $cateringCounts->get($user->id, 0); $timekeeper = $timekeeperCounts->get($user->id, 0); return (object) [ 'user' => $user, 'catering_count' => $catering, 'timekeeper_count' => $timekeeper, 'total' => $catering + $timekeeper, ]; }) ->sortByDesc('total') ->values(); $totalCateringEvents = $cateringEventIds->count(); $totalTimekeeperEvents = $timekeeperEventIds->count(); return view('admin.statistics.index', compact( 'games', 'teams', 'wins', 'losses', 'draws', 'winRate', 'totalWithScore', 'chartWinLoss', 'chartPlayerParticipation', 'chartParentInvolvement', 'playerRanking', 'totalGames', 'courtPlayers', 'parentRanking', 'totalCateringEvents', 'totalTimekeeperEvents' )); } public function playerDetail(Player $player): JsonResponse { if (!Setting::isFeatureVisibleFor('statistics', auth()->user())) { abort(403); } $stats = EventPlayerStat::where('player_id', $player->id) ->with(['event' => fn ($q) => $q->select('id', 'title', 'opponent', 'score_home', 'score_away', 'type', 'start_at', 'team_id')]) ->whereHas('event', fn ($q) => $q->where('status', EventStatus::Published)) ->get() ->sortByDesc(fn ($s) => $s->event->start_at) ->values(); $totalGoals = $stats->sum('goals'); $totalShots = $stats->sum('shots'); $gkGames = $stats->where('is_goalkeeper', true); $totalGkSaves = $gkGames->sum('goalkeeper_saves'); $totalGkShots = $gkGames->sum('goalkeeper_shots'); return response()->json([ 'player' => [ 'name' => $player->full_name, 'avatar' => $player->getAvatarUrl(), 'initials' => $player->getInitials(), 'position' => $player->position?->label(), ], 'summary' => [ 'total_goals' => $totalGoals, 'total_shots' => $totalShots, 'hit_rate' => $totalShots > 0 ? round(($totalGoals / $totalShots) * 100, 1) : null, 'gk_appearances' => $gkGames->count(), 'total_saves' => $totalGkSaves, 'total_gk_shots' => $totalGkShots, 'save_rate' => $totalGkShots > 0 ? round(($totalGkSaves / $totalGkShots) * 100, 1) : null, ], 'games' => $stats->map(fn ($s) => [ 'date' => $s->event->start_at->format('d.m.Y'), 'opponent' => $s->event->opponent ?? '–', 'score' => $s->event->score_home !== null ? $s->event->score_home . ':' . ($s->event->score_away ?? '?') : '–', 'position' => $s->position?->shortLabel(), 'goals' => $s->goals, 'shots' => $s->shots, 'is_goalkeeper' => $s->is_goalkeeper, 'goalkeeper_saves' => $s->goalkeeper_saves, 'goalkeeper_shots' => $s->goalkeeper_shots, 'note' => $s->note, ])->toArray(), ]); } }