- 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>
612 lines
38 KiB
PHP
612 lines
38 KiB
PHP
<x-layouts.admin :title="__('admin.statistics_title')">
|
||
<h1 class="text-2xl font-bold mb-6">{{ __('admin.statistics_title') }}</h1>
|
||
|
||
{{-- 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">
|
||
<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">
|
||
<option value="">{{ __('admin.all_teams') }}</option>
|
||
@foreach ($teams as $team)
|
||
<option value="{{ $team->id }}" {{ request('team_id') == $team->id ? 'selected' : '' }}>{{ $team->name }}</option>
|
||
@endforeach
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<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>
|
||
<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']))
|
||
<a href="{{ route('admin.statistics.index') }}" class="text-sm text-gray-500 hover:underline">{{ __('admin.filter_reset') }}</a>
|
||
@endif
|
||
</form>
|
||
</div>
|
||
|
||
@if ($games->isEmpty())
|
||
<div class="bg-white rounded-lg shadow p-8 text-center text-gray-500">
|
||
{{ __('admin.no_games_yet') }}
|
||
</div>
|
||
@else
|
||
{{-- Statistik-Cards --}}
|
||
<div class="grid grid-cols-2 sm:grid-cols-5 gap-4 mb-6">
|
||
<div class="bg-white rounded-lg shadow p-4 text-center">
|
||
<div class="text-2xl font-bold text-gray-900">{{ $games->count() }}</div>
|
||
<div class="text-xs text-gray-500 mt-1">{{ __('admin.total_games') }}</div>
|
||
</div>
|
||
<div class="bg-white rounded-lg shadow p-4 text-center">
|
||
<div class="text-2xl font-bold text-green-600">{{ $wins }}</div>
|
||
<div class="text-xs text-gray-500 mt-1">{{ __('admin.wins') }}</div>
|
||
</div>
|
||
<div class="bg-white rounded-lg shadow p-4 text-center">
|
||
<div class="text-2xl font-bold text-red-600">{{ $losses }}</div>
|
||
<div class="text-xs text-gray-500 mt-1">{{ __('admin.losses') }}</div>
|
||
</div>
|
||
<div class="bg-white rounded-lg shadow p-4 text-center">
|
||
<div class="text-2xl font-bold text-gray-500">{{ $draws }}</div>
|
||
<div class="text-xs text-gray-500 mt-1">{{ __('admin.draws') }}</div>
|
||
</div>
|
||
<div class="bg-white rounded-lg shadow p-4 text-center">
|
||
<div class="text-2xl font-bold text-blue-600">{{ $winRate }}%</div>
|
||
<div class="text-xs text-gray-500 mt-1">{{ __('admin.win_rate') }}</div>
|
||
</div>
|
||
</div>
|
||
|
||
{{-- Charts --}}
|
||
@if ($totalWithScore > 0)
|
||
<div class="grid grid-cols-1 {{ auth()->user()->isStaff() ? 'lg:grid-cols-3' : 'lg:grid-cols-2' }} gap-6 mb-6">
|
||
{{-- Siege/Niederlagen Pie Chart --}}
|
||
<div class="bg-white rounded-lg shadow p-4">
|
||
<h3 class="text-sm font-semibold text-gray-700 mb-3">{{ __('admin.chart_win_loss') }}</h3>
|
||
<div class="relative" style="height: 220px;">
|
||
<canvas id="chartWinLoss"></canvas>
|
||
</div>
|
||
</div>
|
||
|
||
{{-- Spieler-Teilnahme Bar Chart (nur Staff) --}}
|
||
@if (auth()->user()->isStaff())
|
||
<div class="bg-white rounded-lg shadow p-4">
|
||
<h3 class="text-sm font-semibold text-gray-700 mb-3">{{ __('admin.chart_player_participation') }}</h3>
|
||
<div class="relative" style="height: 220px;">
|
||
<canvas id="chartPlayers"></canvas>
|
||
</div>
|
||
</div>
|
||
@endif
|
||
|
||
{{-- Eltern-Engagement Bar Chart --}}
|
||
<div class="bg-white rounded-lg shadow p-4">
|
||
<h3 class="text-sm font-semibold text-gray-700 mb-3">{{ __('admin.chart_parent_involvement') }}</h3>
|
||
<div class="relative" style="height: 220px;">
|
||
<canvas id="chartParents"></canvas>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
@endif
|
||
|
||
{{-- Spiel-Tabelle --}}
|
||
<div class="bg-white rounded-lg shadow overflow-hidden">
|
||
<div class="overflow-x-auto">
|
||
<table class="w-full text-sm">
|
||
<thead class="bg-gray-50">
|
||
<tr>
|
||
<th class="text-left px-4 py-3 font-medium text-gray-600">{{ __('admin.date') }}</th>
|
||
<th class="text-left px-4 py-3 font-medium text-gray-600">{{ __('ui.team') }}</th>
|
||
<th class="text-center px-4 py-3 font-medium text-gray-600">{{ __('ui.type') }}</th>
|
||
<th class="text-left px-4 py-3 font-medium text-gray-600">{{ __('events.opponent') }}</th>
|
||
<th class="text-center px-4 py-3 font-medium text-gray-600">{{ __('events.score') }}</th>
|
||
@if (auth()->user()->isStaff())
|
||
<th class="text-center px-4 py-3 font-medium text-gray-600">{{ __('admin.nav_players') }}</th>
|
||
@endif
|
||
<th class="text-center px-4 py-3 font-medium text-gray-600">{{ __('events.catering_short') }}</th>
|
||
<th class="text-center px-4 py-3 font-medium text-gray-600">{{ __('events.timekeeper_short') }}</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody class="divide-y divide-gray-100">
|
||
@foreach ($games as $game)
|
||
<tr class="hover:bg-gray-50">
|
||
<td class="px-4 py-2.5 whitespace-nowrap">
|
||
<a href="{{ route('admin.events.edit', $game) }}" class="text-blue-600 hover:underline">
|
||
{{ $game->start_at->translatedFormat(__('ui.date_format_short')) }}
|
||
</a>
|
||
</td>
|
||
<td class="px-4 py-2.5">{{ $game->team->name }}</td>
|
||
<td class="px-4 py-2.5 text-center">
|
||
<span class="inline-block px-1.5 py-0.5 rounded text-xs font-medium {{ $game->type === \App\Enums\EventType::HomeGame ? 'bg-blue-100 text-blue-800' : 'bg-indigo-100 text-indigo-800' }}">
|
||
{{ $game->type === \App\Enums\EventType::HomeGame ? __('admin.home_short') : __('admin.away_short') }}
|
||
</span>
|
||
</td>
|
||
<td class="px-4 py-2.5">{{ $game->opponent ?? '–' }}</td>
|
||
<td class="px-4 py-2.5 text-center font-medium">
|
||
@if ($game->hasScore())
|
||
{{ $game->scoreDisplay() }}
|
||
@else
|
||
<span class="text-gray-400">–</span>
|
||
@endif
|
||
</td>
|
||
@if (auth()->user()->isStaff())
|
||
<td class="px-4 py-2.5 text-center">{{ $game->players_yes_count }}</td>
|
||
@endif
|
||
<td class="px-4 py-2.5 text-center">{{ $game->type->hasCatering() ? $game->caterings_yes_count : '–' }}</td>
|
||
<td class="px-4 py-2.5 text-center">{{ $game->type->hasTimekeepers() ? $game->timekeepers_yes_count : '–' }}</td>
|
||
</tr>
|
||
@endforeach
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
{{-- 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>
|
||
<p class="text-xs text-gray-500 mt-0.5">{{ __('admin.player_ranking_desc', ['count' => $totalGames]) }}</p>
|
||
</div>
|
||
<div class="overflow-x-auto">
|
||
<table class="w-full text-sm">
|
||
<thead class="bg-gray-50">
|
||
<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.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)
|
||
@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())
|
||
<img src="{{ $entry->player->getAvatarUrl() }}" alt="" class="w-6 h-6 rounded-full object-cover">
|
||
@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="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">
|
||
@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 }}%
|
||
</span>
|
||
</td>
|
||
<td class="px-4 py-2">
|
||
<div class="w-full bg-gray-200 rounded-full h-1.5">
|
||
<div class="h-1.5 rounded-full {{ $entry->rate >= 75 ? 'bg-green-500' : ($entry->rate >= 50 ? 'bg-yellow-500' : 'bg-red-500') }}" style="width: {{ $entry->rate }}%"></div>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
@endforeach
|
||
</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 --}}
|
||
@if ($parentRanking->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.parent_ranking_title') }}</h3>
|
||
<p class="text-xs text-gray-500 mt-0.5">{{ __('admin.parent_ranking_desc', ['catering' => $totalCateringEvents, 'timekeeper' => $totalTimekeeperEvents]) }}</p>
|
||
</div>
|
||
<div class="overflow-x-auto">
|
||
<table class="w-full text-sm">
|
||
<thead class="bg-gray-50">
|
||
<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_users') }}</th>
|
||
<th class="text-center px-4 py-2.5 font-medium text-gray-600">{{ __('events.catering_short') }}</th>
|
||
<th class="text-center px-4 py-2.5 font-medium text-gray-600">{{ __('events.timekeeper_short') }}</th>
|
||
<th class="text-center px-4 py-2.5 font-medium text-gray-600">{{ __('admin.total_contributions') }}</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">
|
||
@foreach ($parentRanking as $index => $entry)
|
||
@php
|
||
$maxTotal = $parentRanking->first()->total;
|
||
$barWidth = $maxTotal > 0 ? round(($entry->total / $maxTotal) * 100) : 0;
|
||
@endphp
|
||
<tr class="hover:bg-gray-50">
|
||
<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->user->getAvatarUrl())
|
||
<img src="{{ $entry->user->getAvatarUrl() }}" alt="" class="w-6 h-6 rounded-full object-cover">
|
||
@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->user->getInitials() }}</div>
|
||
@endif
|
||
<span class="{{ $entry->user->trashed() ? 'text-gray-400 line-through' : '' }}">{{ $entry->user->name }}</span>
|
||
</td>
|
||
<td class="px-4 py-2 text-center">
|
||
@if ($entry->catering_count > 0)
|
||
<span class="text-amber-600 font-medium">{{ $entry->catering_count }}</span>
|
||
@else
|
||
<span class="text-gray-300">0</span>
|
||
@endif
|
||
</td>
|
||
<td class="px-4 py-2 text-center">
|
||
@if ($entry->timekeeper_count > 0)
|
||
<span class="text-purple-600 font-medium">{{ $entry->timekeeper_count }}</span>
|
||
@else
|
||
<span class="text-gray-300">0</span>
|
||
@endif
|
||
</td>
|
||
<td class="px-4 py-2 text-center font-bold">{{ $entry->total }}</td>
|
||
<td class="px-4 py-2">
|
||
<div class="w-full bg-gray-200 rounded-full h-1.5">
|
||
<div class="h-1.5 rounded-full bg-blue-500" style="width: {{ $barWidth }}%"></div>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
@endforeach
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
@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>
|
||
<script>
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
const winLossData = @js($chartWinLoss);
|
||
const parentData = @js($chartParentInvolvement);
|
||
|
||
// Pie Chart: Siege/Niederlagen
|
||
new Chart(document.getElementById('chartWinLoss'), {
|
||
type: 'doughnut',
|
||
data: {
|
||
labels: winLossData.labels,
|
||
datasets: [{
|
||
data: winLossData.data,
|
||
backgroundColor: winLossData.colors,
|
||
}]
|
||
},
|
||
options: {
|
||
responsive: true,
|
||
maintainAspectRatio: false,
|
||
plugins: { legend: { position: 'bottom', labels: { boxWidth: 12, font: { size: 11 } } } }
|
||
}
|
||
});
|
||
|
||
// Bar Chart: Spieler-Teilnahme (nur Staff)
|
||
@if (auth()->user()->isStaff())
|
||
const playerData = @js($chartPlayerParticipation);
|
||
new Chart(document.getElementById('chartPlayers'), {
|
||
type: 'bar',
|
||
data: {
|
||
labels: playerData.labels,
|
||
datasets: [{
|
||
label: @js(__('admin.nav_players')),
|
||
data: playerData.data,
|
||
backgroundColor: '#3b82f6',
|
||
borderRadius: 3,
|
||
}]
|
||
},
|
||
options: {
|
||
responsive: true,
|
||
maintainAspectRatio: false,
|
||
scales: { y: { beginAtZero: true, ticks: { stepSize: 1 } } },
|
||
plugins: { legend: { display: false } }
|
||
}
|
||
});
|
||
@endif
|
||
|
||
// Bar Chart: Eltern-Engagement
|
||
new Chart(document.getElementById('chartParents'), {
|
||
type: 'bar',
|
||
data: {
|
||
labels: parentData.labels,
|
||
datasets: [
|
||
{
|
||
label: @js(__('events.catering_short')),
|
||
data: parentData.catering,
|
||
backgroundColor: '#f59e0b',
|
||
borderRadius: 3,
|
||
},
|
||
{
|
||
label: @js(__('events.timekeeper_short')),
|
||
data: parentData.timekeepers,
|
||
backgroundColor: '#8b5cf6',
|
||
borderRadius: 3,
|
||
}
|
||
]
|
||
},
|
||
options: {
|
||
responsive: true,
|
||
maintainAspectRatio: false,
|
||
scales: { y: { beginAtZero: true, ticks: { stepSize: 1 } } },
|
||
plugins: { legend: { position: 'bottom', labels: { boxWidth: 12, font: { size: 11 } } } }
|
||
}
|
||
});
|
||
});
|
||
</script>
|
||
@endpush
|
||
@endif
|
||
</x-layouts.admin>
|