Files
WebAPP/resources/views/admin/statistics/index.blade.php
Rhino ad60e7a9f9 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>
2026-03-02 11:47:34 +01:00

612 lines
38 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>