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:
@@ -357,6 +357,106 @@
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Spielerstatistik (nur Spieltypen mit zugesagten Spielern) --}}
|
||||
@if ($event->type->isGameType())
|
||||
@php
|
||||
$confirmedPlayers = $event->participants
|
||||
->where('status', \App\Enums\ParticipantStatus::Yes)
|
||||
->whereNotNull('player_id')
|
||||
->sortBy(fn($p) => $p->player->last_name ?? '');
|
||||
@endphp
|
||||
@if ($confirmedPlayers->isNotEmpty())
|
||||
<div class="bg-white rounded-lg shadow p-6 max-w-4xl mt-6">
|
||||
<h2 class="text-lg font-semibold mb-1">{{ __('events.stats') }}</h2>
|
||||
<p class="text-xs text-gray-500 mb-4">{{ __('events.stats_confirmed_only') }}</p>
|
||||
|
||||
<form method="POST" action="{{ route('admin.events.update-stats', $event) }}">
|
||||
@csrf
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="text-left px-2 py-2 font-medium text-gray-600">{{ __('admin.nav_players') }}</th>
|
||||
<th class="text-center px-2 py-2 font-medium text-gray-600 w-28">{{ __('events.stats_position') }}</th>
|
||||
<th class="text-center px-2 py-2 font-medium text-gray-600 w-10" title="{{ __('events.stats_goalkeeper_long') }}">{{ __('events.stats_goalkeeper') }}</th>
|
||||
<th class="text-center px-2 py-2 font-medium text-gray-600 w-20">{{ __('events.stats_shots_on_goal') }}</th>
|
||||
<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-left px-2 py-2 font-medium text-gray-600">{{ __('events.stats_note') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-100">
|
||||
@foreach ($confirmedPlayers as $participant)
|
||||
@php
|
||||
$pid = $participant->player_id;
|
||||
$stat = $playerStatsMap[$pid] ?? null;
|
||||
@endphp
|
||||
<tr x-data="{ isGk: {{ $stat && $stat->is_goalkeeper ? 'true' : 'false' }} }">
|
||||
<td class="px-2 py-2 font-medium text-gray-900 whitespace-nowrap">
|
||||
{{ $participant->player->full_name }}
|
||||
</td>
|
||||
<td class="px-2 py-2">
|
||||
<select name="stats[{{ $pid }}][position]"
|
||||
class="w-full px-1 py-1 border border-gray-300 rounded text-sm"
|
||||
@change="isGk = ($event.target.value === 'torwart')">
|
||||
<option value="">–</option>
|
||||
@foreach (\App\Enums\PlayerPosition::cases() as $pos)
|
||||
<option value="{{ $pos->value }}"
|
||||
{{ ($stat?->position?->value ?? $participant->player->position?->value) === $pos->value ? 'selected' : '' }}>
|
||||
{{ $pos->shortLabel() }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</td>
|
||||
<td class="px-2 py-2 text-center">
|
||||
<input type="checkbox" name="stats[{{ $pid }}][is_goalkeeper]" value="1"
|
||||
x-model="isGk"
|
||||
class="rounded border-gray-300 text-blue-600">
|
||||
</td>
|
||||
<td class="px-2 py-2 text-center" x-show="isGk" x-cloak>
|
||||
<input type="number" name="stats[{{ $pid }}][goalkeeper_shots]" min="0" max="999"
|
||||
value="{{ $stat?->goalkeeper_shots }}"
|
||||
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" x-show="!isGk"><span class="text-gray-300">–</span></td>
|
||||
<td class="px-2 py-2 text-center" x-show="isGk" x-cloak>
|
||||
<input type="number" name="stats[{{ $pid }}][goalkeeper_saves]" min="0" max="999"
|
||||
value="{{ $stat?->goalkeeper_saves }}"
|
||||
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" x-show="!isGk"><span class="text-gray-300">–</span></td>
|
||||
<td class="px-2 py-2 text-center">
|
||||
<input type="number" name="stats[{{ $pid }}][shots]" min="0" max="999"
|
||||
value="{{ $stat?->shots }}"
|
||||
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 }}][goals]" min="0" max="999"
|
||||
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">
|
||||
<input type="text" name="stats[{{ $pid }}][note]" maxlength="500"
|
||||
value="{{ $stat?->note }}"
|
||||
placeholder="{{ __('events.stats_note') }}..."
|
||||
class="w-full px-2 py-1 border border-gray-300 rounded text-sm">
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<button type="submit" class="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 text-sm font-medium">
|
||||
{{ __('events.stats_save') }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
|
||||
{{-- Quill JS --}}
|
||||
<script src="https://cdn.jsdelivr.net/npm/quill@1.3.7/dist/quill.min.js" integrity="sha384-QUJ+ckWz1M+a7w0UfG1sEn4pPrbQwSxGm/1TIPyioqXBrwuT9l4f9gdHWLDLbVWI" crossorigin="anonymous"></script>
|
||||
<script>
|
||||
|
||||
@@ -44,6 +44,16 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="position" class="block text-sm font-medium text-gray-700 mb-1">{{ __('admin.position') }}</label>
|
||||
<select name="position" id="position" class="w-full px-3 py-2 border border-gray-300 rounded-md">
|
||||
<option value="">{{ __('admin.please_select') }}</option>
|
||||
@foreach (\App\Enums\PlayerPosition::cases() as $pos)
|
||||
<option value="{{ $pos->value }}" {{ old('position') === $pos->value ? 'selected' : '' }}>{{ $pos->label() }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-4 space-y-2">
|
||||
<label class="flex items-center">
|
||||
<input type="hidden" name="is_active" value="0">
|
||||
|
||||
@@ -74,6 +74,16 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="position" class="block text-sm font-medium text-gray-700 mb-1">{{ __('admin.position') }}</label>
|
||||
<select name="position" id="position" class="w-full px-3 py-2 border border-gray-300 rounded-md">
|
||||
<option value="">{{ __('admin.please_select') }}</option>
|
||||
@foreach (\App\Enums\PlayerPosition::cases() as $pos)
|
||||
<option value="{{ $pos->value }}" {{ old('position', $player->position?->value) === $pos->value ? 'selected' : '' }}>{{ $pos->label() }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="flex items-center">
|
||||
<input type="hidden" name="photo_permission" value="0">
|
||||
|
||||
@@ -95,6 +95,50 @@
|
||||
<p class="mt-1.5 text-xs text-gray-400">{{ __('admin.favicon_hint') }}</p>
|
||||
</div>
|
||||
|
||||
{{-- Logo Login --}}
|
||||
<div class="bg-white rounded-lg shadow p-6 mb-6">
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-1">{{ __('admin.logo_login_label') }}</label>
|
||||
<p class="text-xs text-gray-400 mb-3">{{ __('admin.logo_login_desc') }}</p>
|
||||
@php $currentLogoLogin = \App\Models\Setting::get('app_logo_login'); @endphp
|
||||
@if ($currentLogoLogin)
|
||||
<div class="flex items-center gap-4 mb-3 p-3 bg-gray-50 rounded-md border border-gray-200">
|
||||
<img src="{{ asset('storage/' . $currentLogoLogin) }}" alt="Login-Logo" class="h-16 max-w-[200px] object-contain">
|
||||
<div class="flex flex-col gap-1">
|
||||
<span class="text-sm text-gray-500">{{ __('admin.logo_current') }}</span>
|
||||
<label class="flex items-center gap-1.5 text-sm text-red-600 cursor-pointer">
|
||||
<input type="checkbox" name="remove_logo_login" value="1" class="rounded border-gray-300">
|
||||
{{ __('admin.logo_remove') }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
<input type="file" name="logo_login" accept=".png,.svg,.jpg,.jpeg,.gif,.webp"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm file:mr-3 file:py-1 file:px-3 file:rounded-md file:border-0 file:text-sm file:bg-gray-100 file:text-gray-700 hover:file:bg-gray-200">
|
||||
<p class="mt-1.5 text-xs text-gray-400">{{ __('admin.logo_hint') }}</p>
|
||||
</div>
|
||||
|
||||
{{-- Logo App (Navbar) --}}
|
||||
<div class="bg-white rounded-lg shadow p-6 mb-6">
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-1">{{ __('admin.logo_app_label') }}</label>
|
||||
<p class="text-xs text-gray-400 mb-3">{{ __('admin.logo_app_desc') }}</p>
|
||||
@php $currentLogoApp = \App\Models\Setting::get('app_logo_app'); @endphp
|
||||
@if ($currentLogoApp)
|
||||
<div class="flex items-center gap-4 mb-3 p-3 bg-gray-50 rounded-md border border-gray-200">
|
||||
<img src="{{ asset('storage/' . $currentLogoApp) }}" alt="App-Logo" class="h-10 max-w-[200px] object-contain">
|
||||
<div class="flex flex-col gap-1">
|
||||
<span class="text-sm text-gray-500">{{ __('admin.logo_current') }}</span>
|
||||
<label class="flex items-center gap-1.5 text-sm text-red-600 cursor-pointer">
|
||||
<input type="checkbox" name="remove_logo_app" value="1" class="rounded border-gray-300">
|
||||
{{ __('admin.logo_remove') }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
<input type="file" name="logo_app" accept=".png,.svg,.jpg,.jpeg,.gif,.webp"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm file:mr-3 file:py-1 file:px-3 file:rounded-md file:border-0 file:text-sm file:bg-gray-100 file:text-gray-700 hover:file:bg-gray-200">
|
||||
<p class="mt-1.5 text-xs text-gray-400">{{ __('admin.logo_hint') }}</p>
|
||||
</div>
|
||||
|
||||
{{-- Richtext-Settings (Slogan mit Mini-Quill) --}}
|
||||
@foreach ($settings as $key => $setting)
|
||||
@if ($setting->type === 'richtext')
|
||||
|
||||
@@ -142,6 +142,7 @@
|
||||
|
||||
{{-- Spieler-Rangliste (nur Staff) --}}
|
||||
@if (auth()->user()->isStaff() && $playerRanking->isNotEmpty())
|
||||
<div x-data="playerDetailModal()">
|
||||
<div class="bg-white rounded-lg shadow overflow-hidden mt-6">
|
||||
<div class="px-4 py-3 border-b border-gray-200">
|
||||
<h3 class="text-sm font-semibold text-gray-700">{{ __('admin.player_ranking_title') }}</h3>
|
||||
@@ -153,15 +154,23 @@
|
||||
<tr>
|
||||
<th class="text-left px-4 py-2.5 font-medium text-gray-600 w-8">#</th>
|
||||
<th class="text-left px-4 py-2.5 font-medium text-gray-600">{{ __('admin.nav_players') }}</th>
|
||||
<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.games_assigned') }}</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">{{ __('admin.participation_rate') }}</th>
|
||||
<th class="px-4 py-2.5 font-medium text-gray-600 w-32"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-100">
|
||||
@php $separatorShown = false; @endphp
|
||||
@foreach ($playerRanking as $index => $entry)
|
||||
<tr class="hover:bg-gray-50">
|
||||
@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>
|
||||
@endif
|
||||
@endif
|
||||
<tr class="hover:bg-gray-50 cursor-pointer" @click="openModal({{ $entry->player->id }})">
|
||||
<td class="px-4 py-2 text-gray-400">{{ $index + 1 }}</td>
|
||||
<td class="px-4 py-2 flex items-center gap-2">
|
||||
@if ($entry->player->getAvatarUrl())
|
||||
@@ -169,10 +178,25 @@
|
||||
@else
|
||||
<div class="w-6 h-6 rounded-full bg-gray-200 flex items-center justify-center text-gray-500 text-xs font-semibold">{{ $entry->player->getInitials() }}</div>
|
||||
@endif
|
||||
<span class="{{ $entry->player->trashed() ? 'text-gray-400 line-through' : '' }}">{{ $entry->player->full_name }}</span>
|
||||
<span class="text-blue-600 hover:underline {{ $entry->player->trashed() ? 'text-gray-400 line-through' : '' }}">{{ $entry->player->full_name }}</span>
|
||||
</td>
|
||||
<td class="px-4 py-2 text-center">
|
||||
@if ($entry->primary_position)
|
||||
<span class="inline-block px-1.5 py-0.5 rounded text-xs font-medium {{ $entry->is_primary_gk ? 'bg-purple-100 text-purple-800' : 'bg-gray-100 text-gray-700' }}">
|
||||
{{ $entry->primary_position->shortLabel() }}
|
||||
</span>
|
||||
@else
|
||||
<span class="text-gray-300">–</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-4 py-2 text-center font-medium">{{ $entry->games_played }}</td>
|
||||
<td class="px-4 py-2 text-center text-gray-500">{{ $entry->total_assigned }}</td>
|
||||
<td class="px-4 py-2 text-center">
|
||||
@if ($entry->total_goals > 0)
|
||||
<span class="font-medium text-green-600">{{ $entry->total_goals }}</span>
|
||||
@else
|
||||
<span class="text-gray-300">0</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 }}%
|
||||
@@ -188,7 +212,229 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{{-- Spielfeld-Aufstellung --}}
|
||||
@if ($courtPlayers->isNotEmpty())
|
||||
<div class="bg-white rounded-lg shadow overflow-hidden mt-6">
|
||||
<div class="px-4 py-3 border-b border-gray-200">
|
||||
<h3 class="text-sm font-semibold text-gray-700">{{ __('admin.court_visualization') }}</h3>
|
||||
</div>
|
||||
<div class="p-4 flex justify-center">
|
||||
@php
|
||||
// Positionskoordinaten auf dem Spielfeld (viewBox 0 0 400 320)
|
||||
$courtPositions = [
|
||||
'torwart' => ['x' => 200, 'y' => 280],
|
||||
'links_aussen' => ['x' => 55, 'y' => 185],
|
||||
'rechts_aussen' => ['x' => 345, 'y' => 185],
|
||||
'rueckraum_links' => ['x' => 115, 'y' => 105],
|
||||
'rueckraum_mitte' => ['x' => 200, 'y' => 75],
|
||||
'rueckraum_rechts' => ['x' => 285, 'y' => 105],
|
||||
'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'],
|
||||
];
|
||||
@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" />
|
||||
|
||||
{{-- Mittellinie --}}
|
||||
<line x1="10" y1="160" x2="390" y2="160" stroke="#fff" stroke-width="1" opacity="0.3" />
|
||||
|
||||
{{-- Tor (unten) --}}
|
||||
<rect x="155" y="298" width="90" height="12" rx="2" fill="none" stroke="#fff" stroke-width="2" opacity="0.7" />
|
||||
|
||||
{{-- 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" />
|
||||
|
||||
{{-- 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" />
|
||||
|
||||
{{-- 7m-Markierung --}}
|
||||
<line x1="193" y1="248" x2="207" y2="248" stroke="#fff" stroke-width="2" opacity="0.5" />
|
||||
|
||||
{{-- Spieler-Positionen --}}
|
||||
@foreach ($courtPositions as $posValue => $coords)
|
||||
@php
|
||||
$entry = $courtPlayers->get($posValue);
|
||||
$color = $entry ? $colorMap[$entry->performance_color] : $colorMap['gray'];
|
||||
$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" />
|
||||
@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() }}
|
||||
</text>
|
||||
<text x="{{ $coords['x'] }}" y="{{ $coords['y'] + 8 }}" text-anchor="middle" fill="{{ $color['text'] }}" font-size="7" style="pointer-events: none;">
|
||||
{{ Str::limit($entry->player->first_name, 8, '') }}
|
||||
</text>
|
||||
@else
|
||||
<text x="{{ $coords['x'] }}" y="{{ $coords['y'] + 4 }}" text-anchor="middle" fill="{{ $color['text'] }}" font-size="9" font-weight="bold">
|
||||
{{ $posEnum?->shortLabel() }}
|
||||
</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;">
|
||||
{{ $posEnum?->shortLabel() }}
|
||||
</text>
|
||||
</g>
|
||||
@endforeach
|
||||
</svg>
|
||||
</div>
|
||||
{{-- Legende --}}
|
||||
<div class="px-4 pb-4 flex flex-wrap gap-4 justify-center text-xs text-gray-600">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<span class="inline-block w-3 h-3 rounded-full bg-green-500"></span>
|
||||
{{ __('admin.performance_good') }}
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<span class="inline-block w-3 h-3 rounded-full bg-yellow-500"></span>
|
||||
{{ __('admin.performance_average') }}
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<span class="inline-block w-3 h-3 rounded-full bg-red-500"></span>
|
||||
{{ __('admin.performance_below') }}
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<span class="inline-block w-3 h-3 rounded-full bg-gray-400"></span>
|
||||
{{ __('admin.court_no_data') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Spieler-Detail-Modal --}}
|
||||
<div x-show="show" x-cloak x-transition.opacity class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50" @click.self="show = false" @keydown.escape.window="show = false">
|
||||
<div class="bg-white rounded-xl shadow-xl w-full max-w-2xl max-h-[85vh] overflow-hidden" @click.stop>
|
||||
{{-- Header --}}
|
||||
<div class="flex items-center justify-between px-5 py-4 border-b border-gray-200">
|
||||
<div class="flex items-center gap-3">
|
||||
<template x-if="data && data.player.avatar">
|
||||
<img :src="data.player.avatar" class="w-10 h-10 rounded-full object-cover">
|
||||
</template>
|
||||
<template x-if="data && !data.player.avatar">
|
||||
<div class="w-10 h-10 rounded-full bg-gray-200 flex items-center justify-center text-gray-500 text-sm font-semibold" x-text="data ? data.player.initials : ''"></div>
|
||||
</template>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900" x-text="data ? data.player.name : ''"></h3>
|
||||
<p class="text-xs text-gray-500">
|
||||
<span x-text="data && data.player.position ? data.player.position + ' · ' : ''"></span>{{ __('admin.stats_player_detail') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button @click="show = false" class="text-gray-400 hover:text-gray-600">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{-- Zusammenfassung --}}
|
||||
<template x-if="data">
|
||||
<div class="px-5 py-4 border-b border-gray-100">
|
||||
<div class="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
||||
<div class="text-center p-2 bg-green-50 rounded-lg">
|
||||
<div class="text-lg font-bold text-green-700" x-text="data.summary.total_goals"></div>
|
||||
<div class="text-xs text-green-600">{{ __('admin.stats_total_goals') }}</div>
|
||||
</div>
|
||||
<div class="text-center p-2 bg-blue-50 rounded-lg">
|
||||
<div class="text-lg font-bold text-blue-700" x-text="data.summary.total_shots"></div>
|
||||
<div class="text-xs text-blue-600">{{ __('admin.stats_total_shots') }}</div>
|
||||
</div>
|
||||
<div class="text-center p-2 bg-amber-50 rounded-lg">
|
||||
<div class="text-lg font-bold text-amber-700" x-text="data.summary.hit_rate !== null ? data.summary.hit_rate + '%' : '–'"></div>
|
||||
<div class="text-xs text-amber-600">{{ __('events.stats_hit_rate') }}</div>
|
||||
</div>
|
||||
<div class="text-center p-2 bg-purple-50 rounded-lg">
|
||||
<div class="text-lg font-bold text-purple-700" x-text="data.summary.gk_appearances"></div>
|
||||
<div class="text-xs text-purple-600">{{ __('admin.stats_gk_appearances') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<template x-if="data.summary.gk_appearances > 0">
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 gap-3 mt-3">
|
||||
<div class="text-center p-2 bg-indigo-50 rounded-lg">
|
||||
<div class="text-lg font-bold text-indigo-700" x-text="data.summary.total_saves"></div>
|
||||
<div class="text-xs text-indigo-600">{{ __('admin.stats_total_saves') }}</div>
|
||||
</div>
|
||||
<div class="text-center p-2 bg-gray-50 rounded-lg">
|
||||
<div class="text-lg font-bold text-gray-700" x-text="data.summary.total_gk_shots"></div>
|
||||
<div class="text-xs text-gray-500">{{ __('events.stats_shots_on_goal') }}</div>
|
||||
</div>
|
||||
<div class="text-center p-2 bg-teal-50 rounded-lg">
|
||||
<div class="text-lg font-bold text-teal-700" x-text="data.summary.save_rate !== null ? data.summary.save_rate + '%' : '–'"></div>
|
||||
<div class="text-xs text-teal-600">{{ __('events.stats_save_rate') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
{{-- Spiel-Liste --}}
|
||||
<div class="overflow-y-auto" style="max-height: 40vh;">
|
||||
<template x-if="loading">
|
||||
<div class="flex items-center justify-center py-8 text-gray-400">
|
||||
<svg class="animate-spin w-5 h-5 mr-2" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/></svg>
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="data && data.games.length > 0">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-gray-50 sticky top-0">
|
||||
<tr>
|
||||
<th class="text-left px-4 py-2 text-xs font-medium text-gray-500">{{ __('admin.date') }}</th>
|
||||
<th class="text-left px-4 py-2 text-xs font-medium text-gray-500">{{ __('events.opponent') }}</th>
|
||||
<th class="text-center px-4 py-2 text-xs font-medium text-gray-500">{{ __('events.score') }}</th>
|
||||
<th class="text-center px-4 py-2 text-xs font-medium text-gray-500">{{ __('events.stats_position') }}</th>
|
||||
<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-left px-4 py-2 text-xs font-medium text-gray-500">{{ __('events.stats_note') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-100">
|
||||
<template x-for="game in data.games" :key="game.date + game.opponent">
|
||||
<tr class="hover:bg-gray-50">
|
||||
<td class="px-4 py-2 whitespace-nowrap text-gray-600" x-text="game.date"></td>
|
||||
<td class="px-4 py-2" x-text="game.opponent"></td>
|
||||
<td class="px-4 py-2 text-center font-medium" x-text="game.score"></td>
|
||||
<td class="px-4 py-2 text-center">
|
||||
<span x-text="game.position ?? '–'" :class="game.position ? 'text-xs bg-gray-100 text-gray-700 px-1.5 py-0.5 rounded' : 'text-gray-300'"></span>
|
||||
</td>
|
||||
<td class="px-4 py-2 text-center">
|
||||
<span x-text="game.goals ?? '–'" :class="game.goals > 0 ? 'font-medium text-green-600' : 'text-gray-400'"></span>
|
||||
</td>
|
||||
<td class="px-4 py-2 text-center text-gray-500" x-text="game.shots ?? '–'"></td>
|
||||
<td class="px-4 py-2 text-center">
|
||||
<template x-if="game.is_goalkeeper">
|
||||
<span class="text-xs bg-purple-100 text-purple-700 px-1.5 py-0.5 rounded" x-text="(game.goalkeeper_saves ?? 0) + '/' + (game.goalkeeper_shots ?? 0)"></span>
|
||||
</template>
|
||||
<template x-if="!game.is_goalkeeper">
|
||||
<span class="text-gray-300">–</span>
|
||||
</template>
|
||||
</td>
|
||||
<td class="px-4 py-2 text-gray-500 text-xs" x-text="game.note ?? ''"></td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
<template x-if="data && data.games.length === 0">
|
||||
<div class="py-8 text-center text-gray-400 text-sm">{{ __('events.stats_no_data') }}</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
{{-- Footer --}}
|
||||
<div class="px-5 py-3 border-t border-gray-200 text-right">
|
||||
<button @click="show = false" class="px-4 py-2 text-sm text-gray-600 bg-gray-100 rounded-md hover:bg-gray-200">{{ __('admin.stats_close') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>{{-- /x-data playerDetailModal --}}
|
||||
@endif
|
||||
|
||||
{{-- Eltern-Engagement-Rangliste --}}
|
||||
@@ -255,6 +501,34 @@
|
||||
@endif
|
||||
@endif
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
function playerDetailModal() {
|
||||
return {
|
||||
show: false,
|
||||
loading: false,
|
||||
data: null,
|
||||
async openModal(playerId) {
|
||||
this.show = true;
|
||||
this.loading = true;
|
||||
this.data = null;
|
||||
try {
|
||||
const resp = await fetch(@js(url('/admin/statistics/player')) + '/' + playerId, {
|
||||
headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }
|
||||
});
|
||||
if (resp.ok) {
|
||||
this.data = await resp.json();
|
||||
}
|
||||
} catch (e) {
|
||||
// Fehler still ignorieren
|
||||
}
|
||||
this.loading = false;
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
|
||||
@if ($totalWithScore > 0)
|
||||
@push('scripts')
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js" integrity="sha384-vsrfeLOOY6KuIYKDlmVH5UiBmgIdB1oEf7p01YgWHuqmOHfZr374+odEv96n9tNC" crossorigin="anonymous"></script>
|
||||
|
||||
@@ -28,8 +28,9 @@
|
||||
<div class="max-w-5xl mx-auto px-4">
|
||||
<div class="flex justify-between h-14">
|
||||
<div class="flex items-center space-x-6 rtl:space-x-reverse">
|
||||
@php $logoApp = \App\Models\Setting::get('app_logo_app'); @endphp
|
||||
<a href="{{ route('dashboard') }}" class="flex items-center gap-2 font-bold text-gray-900">
|
||||
<img src="/images/logo_woelfe.png" alt="Logo" class="h-8 w-8 object-contain">
|
||||
<img src="{{ $logoApp ? asset('storage/' . $logoApp) : asset('images/logo_woelfe.png') }}" alt="Logo" class="h-8 w-8 object-contain">
|
||||
{{ \App\Models\Setting::get('app_name', config('app.name')) }}
|
||||
</a>
|
||||
<div class="hidden sm:flex items-center space-x-6 rtl:space-x-reverse">
|
||||
|
||||
@@ -24,8 +24,9 @@
|
||||
<main class="flex-1 flex items-center justify-center px-4 py-12">
|
||||
<div class="w-full max-w-md">
|
||||
<div class="text-center mb-8">
|
||||
@php $logoLogin = \App\Models\Setting::get('app_logo_login'); @endphp
|
||||
<a href="{{ auth()->check() ? route('dashboard') : route('login') }}">
|
||||
<img src="/images/logo_sg_woelfe.png" alt="Logo" class="mx-auto h-24 mb-3">
|
||||
<img src="{{ $logoLogin ? asset('storage/' . $logoLogin) : asset('images/logo_sg_woelfe.png') }}" alt="Logo" class="mx-auto h-24 mb-3 object-contain">
|
||||
</a>
|
||||
<h1 class="text-2xl font-bold text-gray-900">{{ \App\Models\Setting::get('app_name', config('app.name')) }}</h1>
|
||||
@php $slogan = \App\Models\Setting::get('app_slogan'); @endphp
|
||||
|
||||
@@ -390,6 +390,149 @@
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Fahrgemeinschaften --}}
|
||||
@if ($event->type->hasCarpool())
|
||||
<div id="carpool" class="bg-white rounded-lg shadow p-6 mb-6">
|
||||
<h2 class="text-lg font-semibold mb-3">{{ __('events.carpool') }}</h2>
|
||||
|
||||
@if ($event->status !== \App\Enums\EventStatus::Cancelled && !auth()->user()->isDsgvoRestricted())
|
||||
{{-- Eigenes Angebot --}}
|
||||
@if ($myCarpool)
|
||||
<div class="mb-4 p-4 bg-blue-50 border border-blue-200 rounded-lg">
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<span class="text-sm font-semibold text-blue-700">{{ __('events.carpool_my_offer') }}</span>
|
||||
</div>
|
||||
<form method="POST" action="{{ route('carpool.offer', $event) }}" class="flex flex-col sm:flex-row gap-3 mb-2">
|
||||
@csrf
|
||||
<div class="flex items-center gap-2">
|
||||
<label for="carpool-seats" class="text-sm text-gray-700 whitespace-nowrap">{{ __('events.carpool_seats') }}:</label>
|
||||
<input type="number" name="seats" id="carpool-seats" min="1" max="9" value="{{ $myCarpool->seats }}"
|
||||
class="w-16 px-2 py-1.5 border border-gray-300 rounded-md text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
</div>
|
||||
<input type="text" name="note" placeholder="{{ __('events.carpool_note_placeholder') }}" value="{{ $myCarpool->note }}"
|
||||
class="flex-1 px-3 py-1.5 border border-gray-300 rounded-md text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
<button type="submit" class="px-3 py-1.5 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700 whitespace-nowrap">
|
||||
{{ __('events.carpool_update') }}
|
||||
</button>
|
||||
</form>
|
||||
<form method="POST" action="{{ route('carpool.withdraw', $event) }}" class="inline"
|
||||
onsubmit="return confirm(@js(__('events.carpool_withdraw_confirm')))">
|
||||
@csrf
|
||||
<button type="submit" class="text-sm text-red-600 hover:text-red-800 hover:underline">
|
||||
{{ __('events.carpool_withdraw') }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
@else
|
||||
<form method="POST" action="{{ route('carpool.offer', $event) }}" class="mb-4">
|
||||
@csrf
|
||||
<div class="flex flex-col sm:flex-row gap-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<label for="carpool-seats-new" class="text-sm text-gray-700 whitespace-nowrap">{{ __('events.carpool_seats') }}:</label>
|
||||
<input type="number" name="seats" id="carpool-seats-new" min="1" max="9" value="3"
|
||||
class="w-16 px-2 py-1.5 border border-gray-300 rounded-md text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
</div>
|
||||
<input type="text" name="note" placeholder="{{ __('events.carpool_note_placeholder') }}"
|
||||
class="flex-1 px-3 py-1.5 border border-gray-300 rounded-md text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
<button type="submit" class="px-3 py-1.5 text-sm font-medium text-white bg-green-600 rounded-md hover:bg-green-700 whitespace-nowrap">
|
||||
{{ __('events.carpool_offer') }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@endif
|
||||
@error('seats') <p class="text-red-600 text-xs mb-3">{{ $message }}</p> @enderror
|
||||
@error('carpool') <p class="text-red-600 text-xs mb-3">{{ $message }}</p> @enderror
|
||||
@elseif (auth()->user()->isDsgvoRestricted())
|
||||
<p class="text-sm text-orange-600 mb-4">{{ __('ui.dsgvo_restricted_hint') }}</p>
|
||||
@endif
|
||||
|
||||
{{-- Liste aller Fahrten --}}
|
||||
@if ($event->carpools->isNotEmpty())
|
||||
<div class="space-y-3">
|
||||
@foreach ($event->carpools as $carpool)
|
||||
@php
|
||||
$isOwn = $carpool->user_id === auth()->id();
|
||||
$passengerCount = $carpool->passengers->count();
|
||||
$remaining = $carpool->seats - $passengerCount;
|
||||
$fillPercent = $carpool->seats > 0 ? ($passengerCount / $carpool->seats) * 100 : 0;
|
||||
$assignedChildIds = $carpool->passengers->pluck('player_id')->toArray();
|
||||
$assignableChildren = $userChildIds->filter(fn ($id) => !in_array($id, $assignedChildIds));
|
||||
@endphp
|
||||
<div class="p-4 border rounded-lg {{ $isOwn ? 'border-blue-300 bg-blue-50/30' : 'border-gray-200' }}">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<img src="{{ $carpool->driver->getAvatarUrl() ?? asset('images/profil_empty.png') }}" alt="" class="w-7 h-7 rounded-full object-cover flex-shrink-0">
|
||||
<span class="text-sm font-semibold text-gray-900">{{ $carpool->driver->name }}</span>
|
||||
@if ($isOwn)
|
||||
<span class="text-xs bg-blue-100 text-blue-700 px-1.5 py-0.5 rounded">{{ __('events.carpool_my_offer') }}</span>
|
||||
@endif
|
||||
</div>
|
||||
<div class="text-sm text-gray-600">
|
||||
{{ __('events.carpool_seats_count', ['free' => $remaining, 'total' => $carpool->seats]) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Fortschrittsbalken --}}
|
||||
<div class="w-full bg-gray-200 rounded-full h-1.5 mb-2">
|
||||
<div class="h-1.5 rounded-full transition-all {{ $fillPercent >= 100 ? 'bg-red-500' : ($fillPercent >= 60 ? 'bg-yellow-500' : 'bg-green-500') }}"
|
||||
style="width: {{ min($fillPercent, 100) }}%"></div>
|
||||
</div>
|
||||
|
||||
@if ($carpool->note)
|
||||
<p class="text-xs text-gray-500 mb-2">{{ $carpool->note }}</p>
|
||||
@endif
|
||||
|
||||
{{-- Passagiere --}}
|
||||
@if ($carpool->passengers->isNotEmpty())
|
||||
<div class="flex flex-wrap gap-1.5 mb-2">
|
||||
@foreach ($carpool->passengers as $passenger)
|
||||
<span class="inline-flex items-center gap-1 bg-gray-100 text-gray-700 text-xs px-2 py-1 rounded-full">
|
||||
{{ $passenger->player->full_name }}
|
||||
@if ($event->status !== \App\Enums\EventStatus::Cancelled && ($passenger->added_by === auth()->id() || auth()->user()->isAdmin()))
|
||||
<form method="POST" action="{{ route('carpool.leave', $event) }}" class="inline">
|
||||
@csrf
|
||||
<input type="hidden" name="carpool_id" value="{{ $carpool->id }}">
|
||||
<input type="hidden" name="player_id" value="{{ $passenger->player_id }}">
|
||||
<button type="submit" class="text-red-400 hover:text-red-600 ml-0.5" title="{{ __('events.carpool_leave') }}">
|
||||
<svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
</span>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Kinder zuordnen --}}
|
||||
@if ($event->status !== \App\Enums\EventStatus::Cancelled && !auth()->user()->isDsgvoRestricted() && $remaining > 0 && $assignableChildren->isNotEmpty())
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
@foreach ($assignableChildren as $childId)
|
||||
@php $child = $userChildren->firstWhere('id', $childId); @endphp
|
||||
@if ($child)
|
||||
<form method="POST" action="{{ route('carpool.join', $event) }}" class="inline">
|
||||
@csrf
|
||||
<input type="hidden" name="carpool_id" value="{{ $carpool->id }}">
|
||||
<input type="hidden" name="player_id" value="{{ $childId }}">
|
||||
<button type="submit" class="inline-flex items-center gap-1 text-xs px-2 py-1 rounded-full border border-green-300 text-green-700 bg-green-50 hover:bg-green-100 transition">
|
||||
<svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/></svg>
|
||||
{{ $child->full_name }}
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
@elseif ($remaining <= 0 && !$isOwn)
|
||||
<p class="text-xs text-red-500">{{ __('events.carpool_full') }}</p>
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<p class="text-sm text-gray-500">{{ __('events.no_carpool_yet') }}</p>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Kommentare --}}
|
||||
<div id="comments" class="bg-white rounded-lg shadow p-6 mb-6">
|
||||
<h2 class="text-lg font-semibold mb-3">{{ __('events.comments') }}</h2>
|
||||
|
||||
Reference in New Issue
Block a user