Spielerpositionen, Statistiken, Fahrgemeinschaften, Spielfeld-Visualisierung
- PlayerPosition Enum (7 Handball-Positionen) mit Label/ShortLabel - Spielerstatistik pro Spiel (Tore, Würfe, TW-Paraden, Bemerkung) - Position-Dropdown in Spieler-Editor und Event-Stats-Formular - Statistik-Seite: TW zuerst, Trennlinie, Feldspieler, Position-Badges - Spielfeld-SVG mit Ampel-Performance (grün/gelb/rot) - Anklickbare Spieler im Spielfeld öffnen Detail-Modal - Fahrgemeinschaften (Anbieten, Zuordnen, Zurückziehen) - Übersetzungen in allen 6 Sprachen (de, en, pl, ru, ar, tr) - .gitignore für Laravel hinzugefügt - Demo-Daten mit Positionen und Statistiken Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,15 +6,18 @@ use App\Enums\CateringStatus;
|
||||
use App\Enums\EventStatus;
|
||||
use App\Enums\EventType;
|
||||
use App\Enums\ParticipantStatus;
|
||||
use App\Enums\PlayerPosition;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Event;
|
||||
use App\Models\EventCatering;
|
||||
use App\Models\EventParticipant;
|
||||
use App\Models\EventPlayerStat;
|
||||
use App\Models\EventTimekeeper;
|
||||
use App\Models\Player;
|
||||
use App\Models\Setting;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\View\View;
|
||||
@@ -113,6 +116,34 @@ class StatisticsController extends Controller
|
||||
$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'))
|
||||
@@ -120,27 +151,67 @@ class StatisticsController extends Controller
|
||||
->whereNotNull('player_id')
|
||||
->groupBy('player_id')
|
||||
->get()
|
||||
->map(function ($row) use ($totalGames) {
|
||||
->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()
|
||||
->sortByDesc('games_played')
|
||||
->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);
|
||||
@@ -202,8 +273,58 @@ class StatisticsController extends Controller
|
||||
return view('admin.statistics.index', compact(
|
||||
'games', 'teams', 'wins', 'losses', 'draws', 'winRate', 'totalWithScore',
|
||||
'chartWinLoss', 'chartPlayerParticipation', 'chartParentInvolvement',
|
||||
'playerRanking', 'totalGames',
|
||||
'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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user