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:
Rhino
2026-03-02 23:50:03 +01:00
parent ee89141628
commit f24f7f12a3
12 changed files with 303 additions and 31 deletions

View File

@@ -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,
]
);

View File

@@ -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,

View File

@@ -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;
}
}

View File

@@ -0,0 +1,26 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('event_player_stats', function (Blueprint $table) {
$table->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']);
});
}
};

View File

@@ -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' => 'المركز',

View File

@@ -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',

View File

@@ -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',

View File

@@ -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',

View File

@@ -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' => 'Позиция',

View File

@@ -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',

View File

@@ -383,6 +383,11 @@
<th class="text-center px-2 py-2 font-medium text-gray-600 w-20">{{ __('events.stats_saves') }}</th>
<th class="text-center px-2 py-2 font-medium text-gray-600 w-20">{{ __('events.stats_shots') }}</th>
<th class="text-center px-2 py-2 font-medium text-gray-600 w-20">{{ __('events.stats_goals') }}</th>
<th class="text-center px-2 py-2 font-medium text-gray-600 w-16" title="{{ __('events.stats_penalty_shots') }}">7m-W</th>
<th class="text-center px-2 py-2 font-medium text-gray-600 w-16" title="{{ __('events.stats_penalty_goals') }}">7m-T</th>
<th class="text-center px-2 py-2 font-medium text-gray-600 w-14" title="{{ __('events.stats_yellow_cards') }}">{{ __('events.stats_yellow_cards') }}</th>
<th class="text-center px-2 py-2 font-medium text-gray-600 w-14" title="{{ __('events.stats_two_min') }}">{{ __('events.stats_two_min') }}</th>
<th class="text-center px-2 py-2 font-medium text-gray-600 w-16" title="{{ __('events.stats_playing_time') }}">{{ __('events.stats_playing_time_short') }}</th>
<th class="text-left px-2 py-2 font-medium text-gray-600">{{ __('events.stats_note') }}</th>
</tr>
</thead>
@@ -436,6 +441,31 @@
value="{{ $stat?->goals }}"
class="w-16 px-1 py-1 border border-gray-300 rounded text-center text-sm">
</td>
<td class="px-2 py-2 text-center">
<input type="number" name="stats[{{ $pid }}][penalty_shots]" min="0" max="99"
value="{{ $stat?->penalty_shots }}"
class="w-14 px-1 py-1 border border-gray-300 rounded text-center text-sm">
</td>
<td class="px-2 py-2 text-center">
<input type="number" name="stats[{{ $pid }}][penalty_goals]" min="0" max="99"
value="{{ $stat?->penalty_goals }}"
class="w-14 px-1 py-1 border border-gray-300 rounded text-center text-sm">
</td>
<td class="px-2 py-2 text-center">
<input type="number" name="stats[{{ $pid }}][yellow_cards]" min="0" max="3"
value="{{ $stat?->yellow_cards }}"
class="w-12 px-1 py-1 border border-gray-300 rounded text-center text-sm">
</td>
<td class="px-2 py-2 text-center">
<input type="number" name="stats[{{ $pid }}][two_minute_suspensions]" min="0" max="3"
value="{{ $stat?->two_minute_suspensions }}"
class="w-12 px-1 py-1 border border-gray-300 rounded text-center text-sm">
</td>
<td class="px-2 py-2 text-center">
<input type="number" name="stats[{{ $pid }}][playing_time_minutes]" min="0" max="90"
value="{{ $stat?->playing_time_minutes }}"
class="w-14 px-1 py-1 border border-gray-300 rounded text-center text-sm">
</td>
<td class="px-2 py-2">
<input type="text" name="stats[{{ $pid }}][note]" maxlength="500"
value="{{ $stat?->note }}"

View File

@@ -3,7 +3,7 @@
{{-- Filter --}}
<div class="bg-white rounded-lg shadow p-4 mb-6">
<form method="GET" action="{{ route('admin.statistics.index') }}" class="flex flex-wrap items-end gap-4">
<form method="GET" action="{{ route('admin.statistics.index') }}" class="flex flex-wrap items-end gap-4" x-data="{ useSeason: {{ request()->filled('season_id') || (!request()->filled('from') && !request()->filled('to')) ? 'true' : 'false' }} }">
<div>
<label for="team_id" class="block text-xs font-medium text-gray-600 mb-1">{{ __('ui.team') }}</label>
<select name="team_id" id="team_id" class="px-3 py-2 border border-gray-300 rounded-md text-sm">
@@ -13,16 +13,29 @@
@endforeach
</select>
</div>
@if ($seasons->isNotEmpty())
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">{{ __('admin.season') }}</label>
<select name="season_id" class="px-3 py-2 border border-gray-300 rounded-md text-sm" @change="useSeason = $el.value !== ''">
<option value=""></option>
@foreach ($seasons as $season)
<option value="{{ $season->id }}" {{ request('season_id', $activeSeason?->id) == $season->id ? 'selected' : '' }}>
{{ $season->name }}{{ $season->is_current ? ' ●' : '' }}
</option>
@endforeach
</select>
</div>
@endif
<div x-show="!useSeason" x-cloak>
<label for="from" class="block text-xs font-medium text-gray-600 mb-1">{{ __('admin.filter_from') }}</label>
<input type="date" name="from" id="from" value="{{ request('from') }}" class="px-3 py-2 border border-gray-300 rounded-md text-sm">
</div>
<div>
<div x-show="!useSeason" x-cloak>
<label for="to" class="block text-xs font-medium text-gray-600 mb-1">{{ __('admin.filter_to') }}</label>
<input type="date" name="to" id="to" value="{{ request('to') }}" class="px-3 py-2 border border-gray-300 rounded-md text-sm">
</div>
<button type="submit" class="bg-blue-600 text-white px-4 py-2 rounded-md text-sm hover:bg-blue-700">{{ __('admin.filter_apply') }}</button>
@if (request()->hasAny(['team_id', 'from', 'to']))
@if (request()->hasAny(['team_id', 'from', 'to', 'season_id']))
<a href="{{ route('admin.statistics.index') }}" class="text-sm text-gray-500 hover:underline">{{ __('admin.filter_reset') }}</a>
@endif
</form>
@@ -157,6 +170,9 @@
<th class="text-center px-4 py-2.5 font-medium text-gray-600">{{ __('admin.position') }}</th>
<th class="text-center px-4 py-2.5 font-medium text-gray-600">{{ __('admin.games_played') }}</th>
<th class="text-center px-4 py-2.5 font-medium text-gray-600">{{ __('admin.player_goals') }}</th>
<th class="text-center px-4 py-2.5 font-medium text-gray-600" title="{{ __('admin.stats_penalties') }}">7m</th>
<th class="text-center px-4 py-2.5 font-medium text-gray-600" title="{{ __('admin.stats_cards') }}">{{ __('admin.stats_cards') }}</th>
<th class="text-center px-4 py-2.5 font-medium text-gray-600" title="{{ __('admin.stats_avg_time') }}">{{ __('admin.stats_avg_time') }}</th>
<th class="text-center px-4 py-2.5 font-medium text-gray-600">{{ __('admin.participation_rate') }}</th>
<th class="px-4 py-2.5 font-medium text-gray-600 w-32"></th>
</tr>
@@ -167,7 +183,7 @@
@if (!$separatorShown && !$entry->is_primary_gk)
@php $separatorShown = true; @endphp
@if ($index > 0)
<tr><td colspan="7" class="px-4 py-1"><hr class="border-gray-300"></td></tr>
<tr><td colspan="10" class="px-4 py-1"><hr class="border-gray-300"></td></tr>
@endif
@endif
<tr class="hover:bg-gray-50 cursor-pointer" @click="openModal({{ $entry->player->id }})">
@@ -197,6 +213,32 @@
<span class="text-gray-300">0</span>
@endif
</td>
<td class="px-4 py-2 text-center">
@if ($entry->total_penalty_shots > 0)
<span class="text-xs" title="{{ $entry->total_penalty_goals }}/{{ $entry->total_penalty_shots }} ({{ $entry->total_penalty_shots > 0 ? round(($entry->total_penalty_goals / $entry->total_penalty_shots) * 100) : 0 }}%)">{{ $entry->total_penalty_goals }}/{{ $entry->total_penalty_shots }}</span>
@else
<span class="text-gray-300"></span>
@endif
</td>
<td class="px-4 py-2 text-center">
@if ($entry->total_yellow_cards > 0 || $entry->total_suspensions > 0)
@if ($entry->total_yellow_cards > 0)
<span class="inline-block w-3.5 h-4.5 bg-yellow-400 rounded-sm text-[9px] font-bold text-yellow-900 leading-[18px] text-center" title="{{ $entry->total_yellow_cards }}× Gelbe Karte">{{ $entry->total_yellow_cards }}</span>
@endif
@if ($entry->total_suspensions > 0)
<span class="inline-block px-1 py-0.5 rounded text-[10px] font-medium bg-red-100 text-red-700" title="{{ $entry->total_suspensions }}× 2-Min">{{ $entry->total_suspensions }}×2'</span>
@endif
@else
<span class="text-gray-300"></span>
@endif
</td>
<td class="px-4 py-2 text-center">
@if ($entry->avg_playing_time)
<span class="text-xs text-gray-600">{{ $entry->avg_playing_time }}'</span>
@else
<span class="text-gray-300"></span>
@endif
</td>
<td class="px-4 py-2 text-center">
<span class="inline-block px-2 py-0.5 rounded-full text-xs font-medium {{ $entry->rate >= 75 ? 'bg-green-100 text-green-800' : ($entry->rate >= 50 ? 'bg-yellow-100 text-yellow-800' : 'bg-red-100 text-red-800') }}">
{{ $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
<svg viewBox="0 0 400 320" class="w-full max-w-lg" xmlns="http://www.w3.org/2000/svg">
{{-- Spielfeld-Hintergrund --}}
<rect x="0" y="0" width="400" height="320" rx="8" fill="#16a34a" />
<rect x="10" y="10" width="380" height="300" rx="4" fill="none" stroke="#fff" stroke-width="1.5" opacity="0.5" />
<rect x="0" y="0" width="400" height="320" rx="8" fill="#BBFEC3" />
<rect x="10" y="10" width="380" height="300" rx="4" fill="none" stroke="#000" stroke-width="1.5" opacity="0.4" />
{{-- Mittellinie --}}
<line x1="10" y1="160" x2="390" y2="160" stroke="#fff" stroke-width="1" opacity="0.3" />
<line x1="10" y1="160" x2="390" y2="160" stroke="#000" stroke-width="1" opacity="0.25" />
{{-- Tor (unten) --}}
<rect x="155" y="298" width="90" height="12" rx="2" fill="none" stroke="#fff" stroke-width="2" opacity="0.7" />
<rect x="155" y="298" width="90" height="12" rx="2" fill="none" stroke="#000" stroke-width="2" opacity="0.5" />
{{-- 6m-Torraum (Halbkreis) --}}
<path d="M 120 310 Q 120 230 200 220 Q 280 230 280 310" fill="none" stroke="#fff" stroke-width="1.5" opacity="0.5" />
<path d="M 120 310 Q 120 230 200 220 Q 280 230 280 310" fill="none" stroke="#000" stroke-width="1.5" opacity="0.4" />
{{-- 9m-Freiwurflinie (gestrichelt) --}}
<path d="M 80 310 Q 80 200 200 185 Q 320 200 320 310" fill="none" stroke="#fff" stroke-width="1" stroke-dasharray="6,4" opacity="0.35" />
<path d="M 80 310 Q 80 200 200 185 Q 320 200 320 310" fill="none" stroke="#000" stroke-width="1" stroke-dasharray="6,4" opacity="0.3" />
{{-- 7m-Markierung --}}
<line x1="193" y1="248" x2="207" y2="248" stroke="#fff" stroke-width="2" opacity="0.5" />
<line x1="193" y1="248" x2="207" y2="248" stroke="#000" stroke-width="2" opacity="0.4" />
{{-- Spieler-Positionen --}}
@foreach ($courtPositions as $posValue => $coords)
@@ -268,7 +310,7 @@
$posEnum = \App\Enums\PlayerPosition::tryFrom($posValue);
@endphp
<g @if ($entry) @click="openModal({{ $entry->player->id }})" style="cursor: pointer;" @endif>
<circle cx="{{ $coords['x'] }}" cy="{{ $coords['y'] }}" r="22" fill="{{ $color['fill'] }}" opacity="0.9" stroke="#fff" stroke-width="1.5" />
<circle cx="{{ $coords['x'] }}" cy="{{ $coords['y'] }}" r="22" fill="{{ $color['fill'] }}" opacity="0.9" stroke="#1a3a1a" stroke-width="1.5" />
@if ($entry)
<text x="{{ $coords['x'] }}" y="{{ $coords['y'] - 4 }}" text-anchor="middle" fill="{{ $color['text'] }}" font-size="11" font-weight="bold" style="pointer-events: none;">
{{ $entry->player->jersey_number ?? $entry->player->getInitials() }}
@@ -282,7 +324,7 @@
</text>
@endif
{{-- Positions-Kürzel unter dem Kreis --}}
<text x="{{ $coords['x'] }}" y="{{ $coords['y'] + 35 }}" text-anchor="middle" fill="#fff" font-size="8" opacity="0.7" style="pointer-events: none;">
<text x="{{ $coords['x'] }}" y="{{ $coords['y'] + 35 }}" text-anchor="middle" fill="#1a3a1a" font-size="8" opacity="0.6" style="pointer-events: none;">
{{ $posEnum?->shortLabel() }}
</text>
</g>
@@ -372,6 +414,32 @@
</div>
</div>
</template>
{{-- Erweiterte Statistiken --}}
<div class="grid grid-cols-2 sm:grid-cols-3 gap-3 mt-3">
<template x-if="data.summary.total_penalty_shots > 0">
<div class="text-center p-2 bg-orange-50 rounded-lg">
<div class="text-lg font-bold text-orange-700" x-text="data.summary.total_penalty_goals + '/' + data.summary.total_penalty_shots"></div>
<div class="text-xs text-orange-600">7-Meter <span x-show="data.summary.penalty_rate !== null" x-text="'(' + data.summary.penalty_rate + '%)'"></span></div>
</div>
</template>
<template x-if="data.summary.total_yellow_cards > 0 || data.summary.total_suspensions > 0">
<div class="text-center p-2 bg-red-50 rounded-lg">
<div class="text-lg font-bold text-red-700">
<span x-show="data.summary.total_yellow_cards > 0" x-text="data.summary.total_yellow_cards + '× Gelb'"></span>
<span x-show="data.summary.total_yellow_cards > 0 && data.summary.total_suspensions > 0">, </span>
<span x-show="data.summary.total_suspensions > 0" x-text="data.summary.total_suspensions + '× 2-Min'"></span>
</div>
<div class="text-xs text-red-600">{{ __('events.stats_cards') }}</div>
</div>
</template>
<template x-if="data.summary.avg_playing_time !== null">
<div class="text-center p-2 bg-cyan-50 rounded-lg">
<div class="text-lg font-bold text-cyan-700" x-text="Math.round(data.summary.avg_playing_time) + ' Min.'"></div>
<div class="text-xs text-cyan-600"> {{ __('events.stats_playing_time') }}</div>
</div>
</template>
</div>
</div>
</template>
@@ -393,6 +461,9 @@
<th class="text-center px-4 py-2 text-xs font-medium text-gray-500">{{ __('events.stats_goals') }}</th>
<th class="text-center px-4 py-2 text-xs font-medium text-gray-500">{{ __('events.stats_shots') }}</th>
<th class="text-center px-4 py-2 text-xs font-medium text-gray-500">{{ __('events.stats_goalkeeper') }}</th>
<th class="text-center px-4 py-2 text-xs font-medium text-gray-500">7m</th>
<th class="text-center px-4 py-2 text-xs font-medium text-gray-500">{{ __('events.stats_cards') }}</th>
<th class="text-center px-4 py-2 text-xs font-medium text-gray-500">Min.</th>
<th class="text-left px-4 py-2 text-xs font-medium text-gray-500">{{ __('events.stats_note') }}</th>
</tr>
</thead>
@@ -417,6 +488,20 @@
<span class="text-gray-300"></span>
</template>
</td>
<td class="px-4 py-2 text-center">
<template x-if="game.penalty_shots > 0">
<span class="text-xs text-orange-700" x-text="game.penalty_goals + '/' + game.penalty_shots"></span>
</template>
<template x-if="!game.penalty_shots || game.penalty_shots === 0">
<span class="text-gray-300"></span>
</template>
</td>
<td class="px-4 py-2 text-center">
<span x-show="game.yellow_cards > 0" class="text-xs bg-yellow-100 text-yellow-800 px-1 py-0.5 rounded mr-0.5" x-text="game.yellow_cards + '×🟡'"></span>
<span x-show="game.two_minute_suspensions > 0" class="text-xs bg-red-100 text-red-700 px-1 py-0.5 rounded" x-text="game.two_minute_suspensions + '×2m'"></span>
<span x-show="!game.yellow_cards && !game.two_minute_suspensions" class="text-gray-300"></span>
</td>
<td class="px-4 py-2 text-center text-gray-500" x-text="game.playing_time_minutes ? game.playing_time_minutes + '\'' : ''"></td>
<td class="px-4 py-2 text-gray-500 text-xs" x-text="game.note ?? ''"></td>
</tr>
</template>
@@ -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,
}
]