- Fix: Notifiable-Trait zum User-Model hinzugefuegt (behebt notify()-500er) - Installer: SMTP-Verbindungstest mit EsmtpTransport + Ueberspringen-Link - Admin: Neuer E-Mail-Tab mit SMTP-Konfiguration + Verbindungstest - Admin: Lazy Quill-Initialisierung (nur sichtbare Locale wird geladen) - Uebersetzungen: 17 neue Mail-Keys in allen 6 Sprachen Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
338 lines
20 KiB
PHP
338 lines
20 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 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.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.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">
|
||
@foreach ($playerRanking as $index => $entry)
|
||
<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->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="{{ $entry->player->trashed() ? 'text-gray-400 line-through' : '' }}">{{ $entry->player->full_name }}</span>
|
||
</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">
|
||
<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>
|
||
@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
|
||
|
||
@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>
|