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

@@ -357,6 +357,106 @@
</div>
@endif
{{-- Spielerstatistik (nur Spieltypen mit zugesagten Spielern) --}}
@if ($event->type->isGameType())
@php
$confirmedPlayers = $event->participants
->where('status', \App\Enums\ParticipantStatus::Yes)
->whereNotNull('player_id')
->sortBy(fn($p) => $p->player->last_name ?? '');
@endphp
@if ($confirmedPlayers->isNotEmpty())
<div class="bg-white rounded-lg shadow p-6 max-w-4xl mt-6">
<h2 class="text-lg font-semibold mb-1">{{ __('events.stats') }}</h2>
<p class="text-xs text-gray-500 mb-4">{{ __('events.stats_confirmed_only') }}</p>
<form method="POST" action="{{ route('admin.events.update-stats', $event) }}">
@csrf
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead class="bg-gray-50">
<tr>
<th class="text-left px-2 py-2 font-medium text-gray-600">{{ __('admin.nav_players') }}</th>
<th class="text-center px-2 py-2 font-medium text-gray-600 w-28">{{ __('events.stats_position') }}</th>
<th class="text-center px-2 py-2 font-medium text-gray-600 w-10" title="{{ __('events.stats_goalkeeper_long') }}">{{ __('events.stats_goalkeeper') }}</th>
<th class="text-center px-2 py-2 font-medium text-gray-600 w-20">{{ __('events.stats_shots_on_goal') }}</th>
<th class="text-center px-2 py-2 font-medium text-gray-600 w-20">{{ __('events.stats_saves') }}</th>
<th class="text-center px-2 py-2 font-medium text-gray-600 w-20">{{ __('events.stats_shots') }}</th>
<th class="text-center px-2 py-2 font-medium text-gray-600 w-20">{{ __('events.stats_goals') }}</th>
<th class="text-left px-2 py-2 font-medium text-gray-600">{{ __('events.stats_note') }}</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
@foreach ($confirmedPlayers as $participant)
@php
$pid = $participant->player_id;
$stat = $playerStatsMap[$pid] ?? null;
@endphp
<tr x-data="{ isGk: {{ $stat && $stat->is_goalkeeper ? 'true' : 'false' }} }">
<td class="px-2 py-2 font-medium text-gray-900 whitespace-nowrap">
{{ $participant->player->full_name }}
</td>
<td class="px-2 py-2">
<select name="stats[{{ $pid }}][position]"
class="w-full px-1 py-1 border border-gray-300 rounded text-sm"
@change="isGk = ($event.target.value === 'torwart')">
<option value=""></option>
@foreach (\App\Enums\PlayerPosition::cases() as $pos)
<option value="{{ $pos->value }}"
{{ ($stat?->position?->value ?? $participant->player->position?->value) === $pos->value ? 'selected' : '' }}>
{{ $pos->shortLabel() }}
</option>
@endforeach
</select>
</td>
<td class="px-2 py-2 text-center">
<input type="checkbox" name="stats[{{ $pid }}][is_goalkeeper]" value="1"
x-model="isGk"
class="rounded border-gray-300 text-blue-600">
</td>
<td class="px-2 py-2 text-center" x-show="isGk" x-cloak>
<input type="number" name="stats[{{ $pid }}][goalkeeper_shots]" min="0" max="999"
value="{{ $stat?->goalkeeper_shots }}"
class="w-16 px-1 py-1 border border-gray-300 rounded text-center text-sm">
</td>
<td class="px-2 py-2 text-center" x-show="!isGk"><span class="text-gray-300"></span></td>
<td class="px-2 py-2 text-center" x-show="isGk" x-cloak>
<input type="number" name="stats[{{ $pid }}][goalkeeper_saves]" min="0" max="999"
value="{{ $stat?->goalkeeper_saves }}"
class="w-16 px-1 py-1 border border-gray-300 rounded text-center text-sm">
</td>
<td class="px-2 py-2 text-center" x-show="!isGk"><span class="text-gray-300"></span></td>
<td class="px-2 py-2 text-center">
<input type="number" name="stats[{{ $pid }}][shots]" min="0" max="999"
value="{{ $stat?->shots }}"
class="w-16 px-1 py-1 border border-gray-300 rounded text-center text-sm">
</td>
<td class="px-2 py-2 text-center">
<input type="number" name="stats[{{ $pid }}][goals]" min="0" max="999"
value="{{ $stat?->goals }}"
class="w-16 px-1 py-1 border border-gray-300 rounded text-center text-sm">
</td>
<td class="px-2 py-2">
<input type="text" name="stats[{{ $pid }}][note]" maxlength="500"
value="{{ $stat?->note }}"
placeholder="{{ __('events.stats_note') }}..."
class="w-full px-2 py-1 border border-gray-300 rounded text-sm">
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
<div class="mt-4">
<button type="submit" class="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 text-sm font-medium">
{{ __('events.stats_save') }}
</button>
</div>
</form>
</div>
@endif
@endif
{{-- Quill JS --}}
<script src="https://cdn.jsdelivr.net/npm/quill@1.3.7/dist/quill.min.js" integrity="sha384-QUJ+ckWz1M+a7w0UfG1sEn4pPrbQwSxGm/1TIPyioqXBrwuT9l4f9gdHWLDLbVWI" crossorigin="anonymous"></script>
<script>

View File

@@ -44,6 +44,16 @@
</div>
</div>
<div class="mb-4">
<label for="position" class="block text-sm font-medium text-gray-700 mb-1">{{ __('admin.position') }}</label>
<select name="position" id="position" class="w-full px-3 py-2 border border-gray-300 rounded-md">
<option value="">{{ __('admin.please_select') }}</option>
@foreach (\App\Enums\PlayerPosition::cases() as $pos)
<option value="{{ $pos->value }}" {{ old('position') === $pos->value ? 'selected' : '' }}>{{ $pos->label() }}</option>
@endforeach
</select>
</div>
<div class="mb-4 space-y-2">
<label class="flex items-center">
<input type="hidden" name="is_active" value="0">

View File

@@ -74,6 +74,16 @@
</div>
</div>
<div class="mb-4">
<label for="position" class="block text-sm font-medium text-gray-700 mb-1">{{ __('admin.position') }}</label>
<select name="position" id="position" class="w-full px-3 py-2 border border-gray-300 rounded-md">
<option value="">{{ __('admin.please_select') }}</option>
@foreach (\App\Enums\PlayerPosition::cases() as $pos)
<option value="{{ $pos->value }}" {{ old('position', $player->position?->value) === $pos->value ? 'selected' : '' }}>{{ $pos->label() }}</option>
@endforeach
</select>
</div>
<div class="mb-4">
<label class="flex items-center">
<input type="hidden" name="photo_permission" value="0">

View File

@@ -95,6 +95,50 @@
<p class="mt-1.5 text-xs text-gray-400">{{ __('admin.favicon_hint') }}</p>
</div>
{{-- Logo Login --}}
<div class="bg-white rounded-lg shadow p-6 mb-6">
<label class="block text-sm font-semibold text-gray-700 mb-1">{{ __('admin.logo_login_label') }}</label>
<p class="text-xs text-gray-400 mb-3">{{ __('admin.logo_login_desc') }}</p>
@php $currentLogoLogin = \App\Models\Setting::get('app_logo_login'); @endphp
@if ($currentLogoLogin)
<div class="flex items-center gap-4 mb-3 p-3 bg-gray-50 rounded-md border border-gray-200">
<img src="{{ asset('storage/' . $currentLogoLogin) }}" alt="Login-Logo" class="h-16 max-w-[200px] object-contain">
<div class="flex flex-col gap-1">
<span class="text-sm text-gray-500">{{ __('admin.logo_current') }}</span>
<label class="flex items-center gap-1.5 text-sm text-red-600 cursor-pointer">
<input type="checkbox" name="remove_logo_login" value="1" class="rounded border-gray-300">
{{ __('admin.logo_remove') }}
</label>
</div>
</div>
@endif
<input type="file" name="logo_login" accept=".png,.svg,.jpg,.jpeg,.gif,.webp"
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm file:mr-3 file:py-1 file:px-3 file:rounded-md file:border-0 file:text-sm file:bg-gray-100 file:text-gray-700 hover:file:bg-gray-200">
<p class="mt-1.5 text-xs text-gray-400">{{ __('admin.logo_hint') }}</p>
</div>
{{-- Logo App (Navbar) --}}
<div class="bg-white rounded-lg shadow p-6 mb-6">
<label class="block text-sm font-semibold text-gray-700 mb-1">{{ __('admin.logo_app_label') }}</label>
<p class="text-xs text-gray-400 mb-3">{{ __('admin.logo_app_desc') }}</p>
@php $currentLogoApp = \App\Models\Setting::get('app_logo_app'); @endphp
@if ($currentLogoApp)
<div class="flex items-center gap-4 mb-3 p-3 bg-gray-50 rounded-md border border-gray-200">
<img src="{{ asset('storage/' . $currentLogoApp) }}" alt="App-Logo" class="h-10 max-w-[200px] object-contain">
<div class="flex flex-col gap-1">
<span class="text-sm text-gray-500">{{ __('admin.logo_current') }}</span>
<label class="flex items-center gap-1.5 text-sm text-red-600 cursor-pointer">
<input type="checkbox" name="remove_logo_app" value="1" class="rounded border-gray-300">
{{ __('admin.logo_remove') }}
</label>
</div>
</div>
@endif
<input type="file" name="logo_app" accept=".png,.svg,.jpg,.jpeg,.gif,.webp"
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm file:mr-3 file:py-1 file:px-3 file:rounded-md file:border-0 file:text-sm file:bg-gray-100 file:text-gray-700 hover:file:bg-gray-200">
<p class="mt-1.5 text-xs text-gray-400">{{ __('admin.logo_hint') }}</p>
</div>
{{-- Richtext-Settings (Slogan mit Mini-Quill) --}}
@foreach ($settings as $key => $setting)
@if ($setting->type === 'richtext')

View File

@@ -142,6 +142,7 @@
{{-- Spieler-Rangliste (nur Staff) --}}
@if (auth()->user()->isStaff() && $playerRanking->isNotEmpty())
<div x-data="playerDetailModal()">
<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>
@@ -153,15 +154,23 @@
<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.position') }}</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.player_goals') }}</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">
@php $separatorShown = false; @endphp
@foreach ($playerRanking as $index => $entry)
<tr class="hover:bg-gray-50">
@if (!$separatorShown && !$entry->is_primary_gk)
@php $separatorShown = true; @endphp
@if ($index > 0)
<tr><td colspan="7" class="px-4 py-1"><hr class="border-gray-300"></td></tr>
@endif
@endif
<tr class="hover:bg-gray-50 cursor-pointer" @click="openModal({{ $entry->player->id }})">
<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())
@@ -169,10 +178,25 @@
@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>
<span class="text-blue-600 hover:underline {{ $entry->player->trashed() ? 'text-gray-400 line-through' : '' }}">{{ $entry->player->full_name }}</span>
</td>
<td class="px-4 py-2 text-center">
@if ($entry->primary_position)
<span class="inline-block px-1.5 py-0.5 rounded text-xs font-medium {{ $entry->is_primary_gk ? 'bg-purple-100 text-purple-800' : 'bg-gray-100 text-gray-700' }}">
{{ $entry->primary_position->shortLabel() }}
</span>
@else
<span class="text-gray-300"></span>
@endif
</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">
@if ($entry->total_goals > 0)
<span class="font-medium text-green-600">{{ $entry->total_goals }}</span>
@else
<span class="text-gray-300">0</span>
@endif
</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 }}%
@@ -188,7 +212,229 @@
</tbody>
</table>
</div>
</div>
{{-- Spielfeld-Aufstellung --}}
@if ($courtPlayers->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.court_visualization') }}</h3>
</div>
<div class="p-4 flex justify-center">
@php
// Positionskoordinaten auf dem Spielfeld (viewBox 0 0 400 320)
$courtPositions = [
'torwart' => ['x' => 200, 'y' => 280],
'links_aussen' => ['x' => 55, 'y' => 185],
'rechts_aussen' => ['x' => 345, 'y' => 185],
'rueckraum_links' => ['x' => 115, 'y' => 105],
'rueckraum_mitte' => ['x' => 200, 'y' => 75],
'rueckraum_rechts' => ['x' => 285, 'y' => 105],
'kreislaeufer' => ['x' => 200, 'y' => 180],
];
$colorMap = [
'green' => ['fill' => '#22c55e', 'text' => '#fff'],
'yellow' => ['fill' => '#eab308', 'text' => '#fff'],
'red' => ['fill' => '#ef4444', 'text' => '#fff'],
'gray' => ['fill' => '#9ca3af', 'text' => '#fff'],
];
@endphp
<svg viewBox="0 0 400 320" class="w-full max-w-lg" xmlns="http://www.w3.org/2000/svg">
{{-- Spielfeld-Hintergrund --}}
<rect x="0" y="0" width="400" height="320" rx="8" fill="#16a34a" />
<rect x="10" y="10" width="380" height="300" rx="4" fill="none" stroke="#fff" stroke-width="1.5" opacity="0.5" />
{{-- Mittellinie --}}
<line x1="10" y1="160" x2="390" y2="160" stroke="#fff" stroke-width="1" opacity="0.3" />
{{-- Tor (unten) --}}
<rect x="155" y="298" width="90" height="12" rx="2" fill="none" stroke="#fff" stroke-width="2" opacity="0.7" />
{{-- 6m-Torraum (Halbkreis) --}}
<path d="M 120 310 Q 120 230 200 220 Q 280 230 280 310" fill="none" stroke="#fff" stroke-width="1.5" opacity="0.5" />
{{-- 9m-Freiwurflinie (gestrichelt) --}}
<path d="M 80 310 Q 80 200 200 185 Q 320 200 320 310" fill="none" stroke="#fff" stroke-width="1" stroke-dasharray="6,4" opacity="0.35" />
{{-- 7m-Markierung --}}
<line x1="193" y1="248" x2="207" y2="248" stroke="#fff" stroke-width="2" opacity="0.5" />
{{-- Spieler-Positionen --}}
@foreach ($courtPositions as $posValue => $coords)
@php
$entry = $courtPlayers->get($posValue);
$color = $entry ? $colorMap[$entry->performance_color] : $colorMap['gray'];
$posEnum = \App\Enums\PlayerPosition::tryFrom($posValue);
@endphp
<g @if ($entry) @click="openModal({{ $entry->player->id }})" style="cursor: pointer;" @endif>
<circle cx="{{ $coords['x'] }}" cy="{{ $coords['y'] }}" r="22" fill="{{ $color['fill'] }}" opacity="0.9" stroke="#fff" stroke-width="1.5" />
@if ($entry)
<text x="{{ $coords['x'] }}" y="{{ $coords['y'] - 4 }}" text-anchor="middle" fill="{{ $color['text'] }}" font-size="11" font-weight="bold" style="pointer-events: none;">
{{ $entry->player->jersey_number ?? $entry->player->getInitials() }}
</text>
<text x="{{ $coords['x'] }}" y="{{ $coords['y'] + 8 }}" text-anchor="middle" fill="{{ $color['text'] }}" font-size="7" style="pointer-events: none;">
{{ Str::limit($entry->player->first_name, 8, '') }}
</text>
@else
<text x="{{ $coords['x'] }}" y="{{ $coords['y'] + 4 }}" text-anchor="middle" fill="{{ $color['text'] }}" font-size="9" font-weight="bold">
{{ $posEnum?->shortLabel() }}
</text>
@endif
{{-- Positions-Kürzel unter dem Kreis --}}
<text x="{{ $coords['x'] }}" y="{{ $coords['y'] + 35 }}" text-anchor="middle" fill="#fff" font-size="8" opacity="0.7" style="pointer-events: none;">
{{ $posEnum?->shortLabel() }}
</text>
</g>
@endforeach
</svg>
</div>
{{-- Legende --}}
<div class="px-4 pb-4 flex flex-wrap gap-4 justify-center text-xs text-gray-600">
<div class="flex items-center gap-1.5">
<span class="inline-block w-3 h-3 rounded-full bg-green-500"></span>
{{ __('admin.performance_good') }}
</div>
<div class="flex items-center gap-1.5">
<span class="inline-block w-3 h-3 rounded-full bg-yellow-500"></span>
{{ __('admin.performance_average') }}
</div>
<div class="flex items-center gap-1.5">
<span class="inline-block w-3 h-3 rounded-full bg-red-500"></span>
{{ __('admin.performance_below') }}
</div>
<div class="flex items-center gap-1.5">
<span class="inline-block w-3 h-3 rounded-full bg-gray-400"></span>
{{ __('admin.court_no_data') }}
</div>
</div>
</div>
@endif
{{-- Spieler-Detail-Modal --}}
<div x-show="show" x-cloak x-transition.opacity class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50" @click.self="show = false" @keydown.escape.window="show = false">
<div class="bg-white rounded-xl shadow-xl w-full max-w-2xl max-h-[85vh] overflow-hidden" @click.stop>
{{-- Header --}}
<div class="flex items-center justify-between px-5 py-4 border-b border-gray-200">
<div class="flex items-center gap-3">
<template x-if="data && data.player.avatar">
<img :src="data.player.avatar" class="w-10 h-10 rounded-full object-cover">
</template>
<template x-if="data && !data.player.avatar">
<div class="w-10 h-10 rounded-full bg-gray-200 flex items-center justify-center text-gray-500 text-sm font-semibold" x-text="data ? data.player.initials : ''"></div>
</template>
<div>
<h3 class="font-semibold text-gray-900" x-text="data ? data.player.name : ''"></h3>
<p class="text-xs text-gray-500">
<span x-text="data && data.player.position ? data.player.position + ' · ' : ''"></span>{{ __('admin.stats_player_detail') }}
</p>
</div>
</div>
<button @click="show = false" class="text-gray-400 hover:text-gray-600">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
</button>
</div>
{{-- Zusammenfassung --}}
<template x-if="data">
<div class="px-5 py-4 border-b border-gray-100">
<div class="grid grid-cols-2 sm:grid-cols-4 gap-3">
<div class="text-center p-2 bg-green-50 rounded-lg">
<div class="text-lg font-bold text-green-700" x-text="data.summary.total_goals"></div>
<div class="text-xs text-green-600">{{ __('admin.stats_total_goals') }}</div>
</div>
<div class="text-center p-2 bg-blue-50 rounded-lg">
<div class="text-lg font-bold text-blue-700" x-text="data.summary.total_shots"></div>
<div class="text-xs text-blue-600">{{ __('admin.stats_total_shots') }}</div>
</div>
<div class="text-center p-2 bg-amber-50 rounded-lg">
<div class="text-lg font-bold text-amber-700" x-text="data.summary.hit_rate !== null ? data.summary.hit_rate + '%' : ''"></div>
<div class="text-xs text-amber-600">{{ __('events.stats_hit_rate') }}</div>
</div>
<div class="text-center p-2 bg-purple-50 rounded-lg">
<div class="text-lg font-bold text-purple-700" x-text="data.summary.gk_appearances"></div>
<div class="text-xs text-purple-600">{{ __('admin.stats_gk_appearances') }}</div>
</div>
</div>
<template x-if="data.summary.gk_appearances > 0">
<div class="grid grid-cols-2 sm:grid-cols-3 gap-3 mt-3">
<div class="text-center p-2 bg-indigo-50 rounded-lg">
<div class="text-lg font-bold text-indigo-700" x-text="data.summary.total_saves"></div>
<div class="text-xs text-indigo-600">{{ __('admin.stats_total_saves') }}</div>
</div>
<div class="text-center p-2 bg-gray-50 rounded-lg">
<div class="text-lg font-bold text-gray-700" x-text="data.summary.total_gk_shots"></div>
<div class="text-xs text-gray-500">{{ __('events.stats_shots_on_goal') }}</div>
</div>
<div class="text-center p-2 bg-teal-50 rounded-lg">
<div class="text-lg font-bold text-teal-700" x-text="data.summary.save_rate !== null ? data.summary.save_rate + '%' : ''"></div>
<div class="text-xs text-teal-600">{{ __('events.stats_save_rate') }}</div>
</div>
</div>
</template>
</div>
</template>
{{-- Spiel-Liste --}}
<div class="overflow-y-auto" style="max-height: 40vh;">
<template x-if="loading">
<div class="flex items-center justify-center py-8 text-gray-400">
<svg class="animate-spin w-5 h-5 mr-2" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/></svg>
</div>
</template>
<template x-if="data && data.games.length > 0">
<table class="w-full text-sm">
<thead class="bg-gray-50 sticky top-0">
<tr>
<th class="text-left px-4 py-2 text-xs font-medium text-gray-500">{{ __('admin.date') }}</th>
<th class="text-left px-4 py-2 text-xs font-medium text-gray-500">{{ __('events.opponent') }}</th>
<th class="text-center px-4 py-2 text-xs font-medium text-gray-500">{{ __('events.score') }}</th>
<th class="text-center px-4 py-2 text-xs font-medium text-gray-500">{{ __('events.stats_position') }}</th>
<th class="text-center px-4 py-2 text-xs font-medium text-gray-500">{{ __('events.stats_goals') }}</th>
<th class="text-center px-4 py-2 text-xs font-medium text-gray-500">{{ __('events.stats_shots') }}</th>
<th class="text-center px-4 py-2 text-xs font-medium text-gray-500">{{ __('events.stats_goalkeeper') }}</th>
<th class="text-left px-4 py-2 text-xs font-medium text-gray-500">{{ __('events.stats_note') }}</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
<template x-for="game in data.games" :key="game.date + game.opponent">
<tr class="hover:bg-gray-50">
<td class="px-4 py-2 whitespace-nowrap text-gray-600" x-text="game.date"></td>
<td class="px-4 py-2" x-text="game.opponent"></td>
<td class="px-4 py-2 text-center font-medium" x-text="game.score"></td>
<td class="px-4 py-2 text-center">
<span x-text="game.position ?? ''" :class="game.position ? 'text-xs bg-gray-100 text-gray-700 px-1.5 py-0.5 rounded' : 'text-gray-300'"></span>
</td>
<td class="px-4 py-2 text-center">
<span x-text="game.goals ?? ''" :class="game.goals > 0 ? 'font-medium text-green-600' : 'text-gray-400'"></span>
</td>
<td class="px-4 py-2 text-center text-gray-500" x-text="game.shots ?? ''"></td>
<td class="px-4 py-2 text-center">
<template x-if="game.is_goalkeeper">
<span class="text-xs bg-purple-100 text-purple-700 px-1.5 py-0.5 rounded" x-text="(game.goalkeeper_saves ?? 0) + '/' + (game.goalkeeper_shots ?? 0)"></span>
</template>
<template x-if="!game.is_goalkeeper">
<span class="text-gray-300"></span>
</template>
</td>
<td class="px-4 py-2 text-gray-500 text-xs" x-text="game.note ?? ''"></td>
</tr>
</template>
</tbody>
</table>
</template>
<template x-if="data && data.games.length === 0">
<div class="py-8 text-center text-gray-400 text-sm">{{ __('events.stats_no_data') }}</div>
</template>
</div>
{{-- Footer --}}
<div class="px-5 py-3 border-t border-gray-200 text-right">
<button @click="show = false" class="px-4 py-2 text-sm text-gray-600 bg-gray-100 rounded-md hover:bg-gray-200">{{ __('admin.stats_close') }}</button>
</div>
</div>
</div>
</div>{{-- /x-data playerDetailModal --}}
@endif
{{-- Eltern-Engagement-Rangliste --}}
@@ -255,6 +501,34 @@
@endif
@endif
@push('scripts')
<script>
function playerDetailModal() {
return {
show: false,
loading: false,
data: null,
async openModal(playerId) {
this.show = true;
this.loading = true;
this.data = null;
try {
const resp = await fetch(@js(url('/admin/statistics/player')) + '/' + playerId, {
headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }
});
if (resp.ok) {
this.data = await resp.json();
}
} catch (e) {
// Fehler still ignorieren
}
this.loading = false;
}
};
}
</script>
@endpush
@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>

View File

@@ -28,8 +28,9 @@
<div class="max-w-5xl mx-auto px-4">
<div class="flex justify-between h-14">
<div class="flex items-center space-x-6 rtl:space-x-reverse">
@php $logoApp = \App\Models\Setting::get('app_logo_app'); @endphp
<a href="{{ route('dashboard') }}" class="flex items-center gap-2 font-bold text-gray-900">
<img src="/images/logo_woelfe.png" alt="Logo" class="h-8 w-8 object-contain">
<img src="{{ $logoApp ? asset('storage/' . $logoApp) : asset('images/logo_woelfe.png') }}" alt="Logo" class="h-8 w-8 object-contain">
{{ \App\Models\Setting::get('app_name', config('app.name')) }}
</a>
<div class="hidden sm:flex items-center space-x-6 rtl:space-x-reverse">

View File

@@ -24,8 +24,9 @@
<main class="flex-1 flex items-center justify-center px-4 py-12">
<div class="w-full max-w-md">
<div class="text-center mb-8">
@php $logoLogin = \App\Models\Setting::get('app_logo_login'); @endphp
<a href="{{ auth()->check() ? route('dashboard') : route('login') }}">
<img src="/images/logo_sg_woelfe.png" alt="Logo" class="mx-auto h-24 mb-3">
<img src="{{ $logoLogin ? asset('storage/' . $logoLogin) : asset('images/logo_sg_woelfe.png') }}" alt="Logo" class="mx-auto h-24 mb-3 object-contain">
</a>
<h1 class="text-2xl font-bold text-gray-900">{{ \App\Models\Setting::get('app_name', config('app.name')) }}</h1>
@php $slogan = \App\Models\Setting::get('app_slogan'); @endphp

View File

@@ -390,6 +390,149 @@
</div>
@endif
{{-- Fahrgemeinschaften --}}
@if ($event->type->hasCarpool())
<div id="carpool" class="bg-white rounded-lg shadow p-6 mb-6">
<h2 class="text-lg font-semibold mb-3">{{ __('events.carpool') }}</h2>
@if ($event->status !== \App\Enums\EventStatus::Cancelled && !auth()->user()->isDsgvoRestricted())
{{-- Eigenes Angebot --}}
@if ($myCarpool)
<div class="mb-4 p-4 bg-blue-50 border border-blue-200 rounded-lg">
<div class="flex items-center gap-2 mb-3">
<span class="text-sm font-semibold text-blue-700">{{ __('events.carpool_my_offer') }}</span>
</div>
<form method="POST" action="{{ route('carpool.offer', $event) }}" class="flex flex-col sm:flex-row gap-3 mb-2">
@csrf
<div class="flex items-center gap-2">
<label for="carpool-seats" class="text-sm text-gray-700 whitespace-nowrap">{{ __('events.carpool_seats') }}:</label>
<input type="number" name="seats" id="carpool-seats" min="1" max="9" value="{{ $myCarpool->seats }}"
class="w-16 px-2 py-1.5 border border-gray-300 rounded-md text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
</div>
<input type="text" name="note" placeholder="{{ __('events.carpool_note_placeholder') }}" value="{{ $myCarpool->note }}"
class="flex-1 px-3 py-1.5 border border-gray-300 rounded-md text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<button type="submit" class="px-3 py-1.5 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700 whitespace-nowrap">
{{ __('events.carpool_update') }}
</button>
</form>
<form method="POST" action="{{ route('carpool.withdraw', $event) }}" class="inline"
onsubmit="return confirm(@js(__('events.carpool_withdraw_confirm')))">
@csrf
<button type="submit" class="text-sm text-red-600 hover:text-red-800 hover:underline">
{{ __('events.carpool_withdraw') }}
</button>
</form>
</div>
@else
<form method="POST" action="{{ route('carpool.offer', $event) }}" class="mb-4">
@csrf
<div class="flex flex-col sm:flex-row gap-3">
<div class="flex items-center gap-2">
<label for="carpool-seats-new" class="text-sm text-gray-700 whitespace-nowrap">{{ __('events.carpool_seats') }}:</label>
<input type="number" name="seats" id="carpool-seats-new" min="1" max="9" value="3"
class="w-16 px-2 py-1.5 border border-gray-300 rounded-md text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
</div>
<input type="text" name="note" placeholder="{{ __('events.carpool_note_placeholder') }}"
class="flex-1 px-3 py-1.5 border border-gray-300 rounded-md text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<button type="submit" class="px-3 py-1.5 text-sm font-medium text-white bg-green-600 rounded-md hover:bg-green-700 whitespace-nowrap">
{{ __('events.carpool_offer') }}
</button>
</div>
</form>
@endif
@error('seats') <p class="text-red-600 text-xs mb-3">{{ $message }}</p> @enderror
@error('carpool') <p class="text-red-600 text-xs mb-3">{{ $message }}</p> @enderror
@elseif (auth()->user()->isDsgvoRestricted())
<p class="text-sm text-orange-600 mb-4">{{ __('ui.dsgvo_restricted_hint') }}</p>
@endif
{{-- Liste aller Fahrten --}}
@if ($event->carpools->isNotEmpty())
<div class="space-y-3">
@foreach ($event->carpools as $carpool)
@php
$isOwn = $carpool->user_id === auth()->id();
$passengerCount = $carpool->passengers->count();
$remaining = $carpool->seats - $passengerCount;
$fillPercent = $carpool->seats > 0 ? ($passengerCount / $carpool->seats) * 100 : 0;
$assignedChildIds = $carpool->passengers->pluck('player_id')->toArray();
$assignableChildren = $userChildIds->filter(fn ($id) => !in_array($id, $assignedChildIds));
@endphp
<div class="p-4 border rounded-lg {{ $isOwn ? 'border-blue-300 bg-blue-50/30' : 'border-gray-200' }}">
<div class="flex items-center justify-between mb-2">
<div class="flex items-center gap-2">
<img src="{{ $carpool->driver->getAvatarUrl() ?? asset('images/profil_empty.png') }}" alt="" class="w-7 h-7 rounded-full object-cover flex-shrink-0">
<span class="text-sm font-semibold text-gray-900">{{ $carpool->driver->name }}</span>
@if ($isOwn)
<span class="text-xs bg-blue-100 text-blue-700 px-1.5 py-0.5 rounded">{{ __('events.carpool_my_offer') }}</span>
@endif
</div>
<div class="text-sm text-gray-600">
{{ __('events.carpool_seats_count', ['free' => $remaining, 'total' => $carpool->seats]) }}
</div>
</div>
{{-- Fortschrittsbalken --}}
<div class="w-full bg-gray-200 rounded-full h-1.5 mb-2">
<div class="h-1.5 rounded-full transition-all {{ $fillPercent >= 100 ? 'bg-red-500' : ($fillPercent >= 60 ? 'bg-yellow-500' : 'bg-green-500') }}"
style="width: {{ min($fillPercent, 100) }}%"></div>
</div>
@if ($carpool->note)
<p class="text-xs text-gray-500 mb-2">{{ $carpool->note }}</p>
@endif
{{-- Passagiere --}}
@if ($carpool->passengers->isNotEmpty())
<div class="flex flex-wrap gap-1.5 mb-2">
@foreach ($carpool->passengers as $passenger)
<span class="inline-flex items-center gap-1 bg-gray-100 text-gray-700 text-xs px-2 py-1 rounded-full">
{{ $passenger->player->full_name }}
@if ($event->status !== \App\Enums\EventStatus::Cancelled && ($passenger->added_by === auth()->id() || auth()->user()->isAdmin()))
<form method="POST" action="{{ route('carpool.leave', $event) }}" class="inline">
@csrf
<input type="hidden" name="carpool_id" value="{{ $carpool->id }}">
<input type="hidden" name="player_id" value="{{ $passenger->player_id }}">
<button type="submit" class="text-red-400 hover:text-red-600 ml-0.5" title="{{ __('events.carpool_leave') }}">
<svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
</button>
</form>
@endif
</span>
@endforeach
</div>
@endif
{{-- Kinder zuordnen --}}
@if ($event->status !== \App\Enums\EventStatus::Cancelled && !auth()->user()->isDsgvoRestricted() && $remaining > 0 && $assignableChildren->isNotEmpty())
<div class="flex flex-wrap gap-1.5">
@foreach ($assignableChildren as $childId)
@php $child = $userChildren->firstWhere('id', $childId); @endphp
@if ($child)
<form method="POST" action="{{ route('carpool.join', $event) }}" class="inline">
@csrf
<input type="hidden" name="carpool_id" value="{{ $carpool->id }}">
<input type="hidden" name="player_id" value="{{ $childId }}">
<button type="submit" class="inline-flex items-center gap-1 text-xs px-2 py-1 rounded-full border border-green-300 text-green-700 bg-green-50 hover:bg-green-100 transition">
<svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/></svg>
{{ $child->full_name }}
</button>
</form>
@endif
@endforeach
</div>
@elseif ($remaining <= 0 && !$isOwn)
<p class="text-xs text-red-500">{{ __('events.carpool_full') }}</p>
@endif
</div>
@endforeach
</div>
@else
<p class="text-sm text-gray-500">{{ __('events.no_carpool_yet') }}</p>
@endif
</div>
@endif
{{-- Kommentare --}}
<div id="comments" class="bg-white rounded-lg shadow p-6 mb-6">
<h2 class="text-lg font-semibold mb-3">{{ __('events.comments') }}</h2>