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>
This commit is contained in:
Rhino
2026-03-02 11:47:34 +01:00
parent 2e24a40d68
commit ad60e7a9f9
46 changed files with 2041 additions and 86 deletions

View File

@@ -6,10 +6,12 @@ use App\Enums\CateringStatus;
use App\Enums\EventStatus;
use App\Enums\EventType;
use App\Enums\ParticipantStatus;
use App\Enums\PlayerPosition;
use App\Http\Controllers\Controller;
use App\Models\ActivityLog;
use App\Models\Event;
use App\Models\EventCatering;
use App\Models\EventPlayerStat;
use App\Models\EventTimekeeper;
use App\Models\File;
use App\Models\FileCategory;
@@ -120,13 +122,16 @@ class EventController extends Controller
$participantRelations = $event->type === EventType::Meeting
? ['participants.user']
: ['participants.player'];
$event->load(array_merge($participantRelations, ['caterings', 'timekeepers', 'files.category']));
$event->load(array_merge($participantRelations, ['caterings', 'timekeepers', 'files.category', 'playerStats']));
$assignedCatering = $event->caterings->where('status', CateringStatus::Yes)->pluck('user_id')->toArray();
$assignedTimekeeper = $event->timekeepers->where('status', CateringStatus::Yes)->pluck('user_id')->toArray();
$knownLocations = Location::orderBy('name')->get();
$fileCategories = FileCategory::active()->ordered()->with(['files' => fn ($q) => $q->latest()])->get();
return view('admin.events.edit', compact('event', 'teams', 'types', 'statuses', 'teamParents', 'assignedCatering', 'assignedTimekeeper', 'eventDefaults', 'knownLocations', 'fileCategories'));
// Spielerstatistik-Daten für Spieltypen
$playerStatsMap = $event->playerStats->keyBy('player_id');
return view('admin.events.edit', compact('event', 'teams', 'types', 'statuses', 'teamParents', 'assignedCatering', 'assignedTimekeeper', 'eventDefaults', 'knownLocations', 'fileCategories', 'playerStatsMap'));
}
public function update(Request $request, Event $event): RedirectResponse
@@ -210,6 +215,58 @@ class EventController extends Controller
->with('success', __('admin.event_restored'));
}
public function updateStats(Request $request, Event $event): RedirectResponse
{
if (! $event->type->isGameType()) {
abort(404);
}
$request->validate([
'stats' => ['required', 'array'],
'stats.*.is_goalkeeper' => ['nullable'],
'stats.*.position' => ['nullable', 'string', \Illuminate\Validation\Rule::in(PlayerPosition::values())],
'stats.*.goalkeeper_saves' => ['nullable', 'integer', 'min:0', 'max:999'],
'stats.*.goalkeeper_shots' => ['nullable', 'integer', 'min:0', 'max:999'],
'stats.*.goals' => ['nullable', 'integer', 'min:0', 'max:999'],
'stats.*.shots' => ['nullable', 'integer', 'min:0', 'max:999'],
'stats.*.note' => ['nullable', 'string', 'max:500'],
]);
$stats = $request->input('stats', []);
foreach ($stats as $playerId => $data) {
$position = ! empty($data['position']) ? $data['position'] : null;
$isGk = $position === 'torwart' || ! empty($data['is_goalkeeper']);
$goals = isset($data['goals']) && $data['goals'] !== '' ? (int) $data['goals'] : null;
$shots = isset($data['shots']) && $data['shots'] !== '' ? (int) $data['shots'] : null;
$gkSaves = $isGk && isset($data['goalkeeper_saves']) && $data['goalkeeper_saves'] !== '' ? (int) $data['goalkeeper_saves'] : null;
$gkShots = $isGk && isset($data['goalkeeper_shots']) && $data['goalkeeper_shots'] !== '' ? (int) $data['goalkeeper_shots'] : null;
$note = ! empty($data['note']) ? trim($data['note']) : null;
// Leere Einträge löschen
if (! $isGk && $goals === null && $shots === null && $note === null && $position === null) {
EventPlayerStat::where('event_id', $event->id)->where('player_id', $playerId)->delete();
continue;
}
EventPlayerStat::updateOrCreate(
['event_id' => $event->id, 'player_id' => (int) $playerId],
[
'is_goalkeeper' => $isGk,
'position' => $position,
'goalkeeper_saves' => $gkSaves,
'goalkeeper_shots' => $gkShots,
'goals' => $goals,
'shots' => $shots,
'note' => $note,
]
);
}
return redirect()->route('admin.events.edit', $event)
->with('success', __('events.stats_saved'));
}
private function validateEvent(Request $request): array
{
$request->validate([