- 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>
210 lines
7.9 KiB
PHP
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'
|
|
));
|
|
}
|
|
}
|