Files
WebAPP/app/Http/Controllers/Admin/StatisticsController.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

210 lines
7.9 KiB
PHP

<?php
namespace App\Http\Controllers\Admin;
use App\Enums\CateringStatus;
use App\Enums\EventStatus;
use App\Enums\EventType;
use App\Enums\ParticipantStatus;
use App\Http\Controllers\Controller;
use App\Models\Event;
use App\Models\EventCatering;
use App\Models\EventParticipant;
use App\Models\EventTimekeeper;
use App\Models\Player;
use App\Models\Setting;
use App\Models\Team;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\View\View;
class StatisticsController extends Controller
{
public function index(Request $request): View
{
if (!Setting::isFeatureVisibleFor('statistics', auth()->user())) {
abort(403);
}
$request->validate([
'team_id' => ['nullable', 'integer', 'exists:teams,id'],
'from' => ['nullable', 'date'],
'to' => ['nullable', 'date'],
]);
$query = Event::with(['team'])
->withCount([
'participants as players_yes_count' => fn ($q) => $q->where('status', 'yes'),
'caterings as caterings_yes_count' => fn ($q) => $q->where('status', 'yes'),
'timekeepers as timekeepers_yes_count' => fn ($q) => $q->where('status', 'yes'),
])
->whereIn('type', [EventType::HomeGame, EventType::AwayGame])
->where('status', EventStatus::Published);
if ($request->filled('team_id')) {
$query->where('team_id', $request->team_id);
}
if ($request->filled('from')) {
$query->where('start_at', '>=', $request->from);
}
if ($request->filled('to')) {
$query->where('start_at', '<=', $request->to . ' 23:59:59');
}
$games = $query->orderByDesc('start_at')->get();
// Statistiken berechnen
$gamesWithScore = $games->filter(fn ($g) => $g->score_home !== null && $g->score_away !== null);
$wins = 0;
$losses = 0;
$draws = 0;
foreach ($gamesWithScore as $game) {
if ($game->type === EventType::HomeGame) {
if ($game->score_home > $game->score_away) {
$wins++;
} elseif ($game->score_home < $game->score_away) {
$losses++;
} else {
$draws++;
}
} else {
if ($game->score_away > $game->score_home) {
$wins++;
} elseif ($game->score_away < $game->score_home) {
$losses++;
} else {
$draws++;
}
}
}
$totalWithScore = $gamesWithScore->count();
$winRate = $totalWithScore > 0 ? round(($wins / $totalWithScore) * 100) : 0;
// Chart-Daten
$chartWinLoss = [
'labels' => [__('admin.wins'), __('admin.losses'), __('admin.draws')],
'data' => [$wins, $losses, $draws],
'colors' => ['#22c55e', '#ef4444', '#9ca3af'],
];
// Spieler-Teilnahme pro Spiel (nur die letzten 15 Spiele)
$recentGames = $games->take(15)->reverse()->values();
$chartPlayerParticipation = [
'labels' => $recentGames->map(fn ($g) => $g->start_at->format('d.m.'))->toArray(),
'data' => $recentGames->map(fn ($g) => $g->players_yes_count)->toArray(),
];
// Eltern-Engagement (Catering + Zeitnehmer)
$chartParentInvolvement = [
'labels' => $recentGames->map(fn ($g) => $g->start_at->format('d.m.'))->toArray(),
'catering' => $recentGames->map(fn ($g) => $g->caterings_yes_count)->toArray(),
'timekeepers' => $recentGames->map(fn ($g) => $g->timekeepers_yes_count)->toArray(),
];
$teams = Team::where('is_active', true)->orderBy('name')->get();
// ── Spieler-Rangliste ──────────────────────────────────
$gameIds = $games->pluck('id');
$totalGames = $games->count();
$playerRanking = collect();
if ($totalGames > 0) {
$playerRanking = EventParticipant::select('player_id', DB::raw('COUNT(*) as total_assigned'), DB::raw('SUM(CASE WHEN status = \'yes\' THEN 1 ELSE 0 END) as games_played'))
->whereIn('event_id', $gameIds)
->whereNotNull('player_id')
->groupBy('player_id')
->get()
->map(function ($row) use ($totalGames) {
$player = Player::withTrashed()->find($row->player_id);
if (!$player) {
return null;
}
return (object) [
'player' => $player,
'games_played' => (int) $row->games_played,
'total_assigned' => (int) $row->total_assigned,
'total_games' => $totalGames,
'rate' => $row->total_assigned > 0
? round(($row->games_played / $row->total_assigned) * 100)
: 0,
];
})
->filter()
->sortByDesc('games_played')
->values();
}
// ── Eltern-Engagement-Rangliste ────────────────────────
// Alle publizierten Events (nicht nur Spiele) mit gleichen Team/Datum-Filtern
$allEventsQuery = Event::where('status', EventStatus::Published);
if ($request->filled('team_id')) {
$allEventsQuery->where('team_id', $request->team_id);
}
if ($request->filled('from')) {
$allEventsQuery->where('start_at', '>=', $request->from);
}
if ($request->filled('to')) {
$allEventsQuery->where('start_at', '<=', $request->to . ' 23:59:59');
}
$allEventIds = $allEventsQuery->pluck('id');
// Catering-Events (nur Typen die Catering haben)
$cateringEventIds = $allEventsQuery->clone()
->whereNotIn('type', [EventType::AwayGame, EventType::Meeting])
->pluck('id');
// Zeitnehmer-Events (identisch wie Catering)
$timekeeperEventIds = $cateringEventIds;
$cateringCounts = EventCatering::select('user_id', DB::raw('COUNT(*) as count'))
->whereIn('event_id', $cateringEventIds)
->where('status', CateringStatus::Yes)
->groupBy('user_id')
->pluck('count', 'user_id');
$timekeeperCounts = EventTimekeeper::select('user_id', DB::raw('COUNT(*) as count'))
->whereIn('event_id', $timekeeperEventIds)
->where('status', CateringStatus::Yes)
->groupBy('user_id')
->pluck('count', 'user_id');
$parentUserIds = $cateringCounts->keys()->merge($timekeeperCounts->keys())->unique();
$parentRanking = User::withTrashed()
->whereIn('id', $parentUserIds)
->get()
->map(function ($user) use ($cateringCounts, $timekeeperCounts) {
$catering = $cateringCounts->get($user->id, 0);
$timekeeper = $timekeeperCounts->get($user->id, 0);
return (object) [
'user' => $user,
'catering_count' => $catering,
'timekeeper_count' => $timekeeper,
'total' => $catering + $timekeeper,
];
})
->sortByDesc('total')
->values();
$totalCateringEvents = $cateringEventIds->count();
$totalTimekeeperEvents = $timekeeperEventIds->count();
return view('admin.statistics.index', compact(
'games', 'teams', 'wins', 'losses', 'draws', 'winRate', 'totalWithScore',
'chartWinLoss', 'chartPlayerParticipation', 'chartParentInvolvement',
'playerRanking', 'totalGames',
'parentRanking', 'totalCateringEvents', 'totalTimekeeperEvents'
));
}
}