Files
WebAPP/resources/views/admin/statistics/index.blade.php
Rhino 2e24a40d68 Stand: SMTP-Test, Admin-Mail-Tab, Notifiable-Fix, Lazy-Quill
- 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>
2026-03-02 07:30:37 +01:00

338 lines
20 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 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>