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:
@@ -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,
|
||||
}
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user