Files
WebAPP/resources/views/admin/events/edit.blade.php
Rhino ad60e7a9f9 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>
2026-03-02 11:47:34 +01:00

693 lines
44 KiB
PHP
Executable File
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.edit_event_title')">
@push('styles')
<link href="https://cdn.jsdelivr.net/npm/quill@1.3.7/dist/quill.snow.css" rel="stylesheet" integrity="sha384-cPa8kzsYWhqpAfWOLWYIw3V0BhPi/m3lrd8tBTPxr2NrYCHRVZ7xy1cEoRGOM/03" crossorigin="anonymous">
<style>
.ql-editor { min-height: 120px; }
.ql-toolbar.ql-snow { border-radius: 0.375rem 0.375rem 0 0; }
.ql-container.ql-snow { border-radius: 0 0 0.375rem 0.375rem; }
</style>
@endpush
<h1 class="text-2xl font-bold mb-6">{{ __('admin.edit_event_title') }}</h1>
<div class="bg-white rounded-lg shadow p-6 max-w-2xl">
<form method="POST" action="{{ route('admin.events.update', $event) }}" id="eventForm" enctype="multipart/form-data"
x-data="{ currentType: @js(old('type', $event->type->value)) }" x-init="document.getElementById('type').addEventListener('change', (e) => currentType = e.target.value);">
@csrf
@method('PUT')
<div class="grid grid-cols-2 gap-4 mb-4">
<div>
<label for="team_id" class="block text-sm font-medium text-gray-700 mb-1">{{ __('ui.team') }} *</label>
<select name="team_id" id="team_id" required class="w-full px-3 py-2 border border-gray-300 rounded-md">
<option value="">{{ __('admin.please_select') }}</option>
@foreach ($teams as $team)
<option value="{{ $team->id }}" {{ old('team_id', $event->team_id) == $team->id ? 'selected' : '' }}>{{ $team->name }}</option>
@endforeach
</select>
@error('team_id')<p class="mt-1 text-sm text-red-600">{{ $message }}</p>@enderror
</div>
<div>
<label for="type" class="block text-sm font-medium text-gray-700 mb-1">{{ __('ui.type') }} *</label>
<select name="type" id="type" required class="w-full px-3 py-2 border border-gray-300 rounded-md" x-model="currentType">
@foreach ($types as $t)
<option value="{{ $t->value }}" {{ old('type', $event->type->value) === $t->value ? 'selected' : '' }}>{{ $t->label() }}</option>
@endforeach
</select>
@error('type')<p class="mt-1 text-sm text-red-600">{{ $message }}</p>@enderror
</div>
</div>
<div class="mb-4">
<label for="title" class="block text-sm font-medium text-gray-700 mb-1">{{ __('admin.event_title') }} *</label>
<input type="text" name="title" id="title" value="{{ old('title', $event->title) }}" required
class="w-full px-3 py-2 border border-gray-300 rounded-md @error('title') border-red-500 @enderror">
@error('title')<p class="mt-1 text-sm text-red-600">{{ $message }}</p>@enderror
</div>
{{-- Gegner (nur für Spiel-Typen) --}}
<div class="mb-4" x-show="['home_game', 'away_game'].includes(currentType)" x-cloak>
<label for="opponent" class="block text-sm font-medium text-gray-700 mb-1">{{ __('events.opponent') }}</label>
<input type="text" name="opponent" id="opponent" value="{{ old('opponent', $event->opponent) }}" maxlength="100"
class="w-full px-3 py-2 border border-gray-300 rounded-md" placeholder="{{ __('events.opponent') }}">
@error('opponent')<p class="mt-1 text-sm text-red-600">{{ $message }}</p>@enderror
</div>
{{-- Ergebnis (nur für Spiel-Typen) --}}
<div class="mb-4" x-show="['home_game', 'away_game'].includes(currentType)" x-cloak>
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('events.score') }}</label>
<div class="flex items-center gap-2">
<input type="number" name="score_home" value="{{ old('score_home', $event->score_home) }}" min="0" max="99" placeholder="{{ __('events.score_home') }}"
class="w-20 px-3 py-2 border border-gray-300 rounded-md text-center">
<span class="text-gray-500 font-medium">:</span>
<input type="number" name="score_away" value="{{ old('score_away', $event->score_away) }}" min="0" max="99" placeholder="{{ __('events.score_away') }}"
class="w-20 px-3 py-2 border border-gray-300 rounded-md text-center">
</div>
@error('score_home')<p class="mt-1 text-sm text-red-600">{{ $message }}</p>@enderror
@error('score_away')<p class="mt-1 text-sm text-red-600">{{ $message }}</p>@enderror
</div>
<div class="grid grid-cols-2 gap-4 mb-4">
<div>
<label for="start_date" class="block text-sm font-medium text-gray-700 mb-1">{{ __('admin.date') }} *</label>
<input type="date" name="start_date" id="start_date" value="{{ old('start_date', $event->start_at->format('Y-m-d')) }}" required
class="w-full px-3 py-2 border border-gray-300 rounded-md @error('start_date') border-red-500 @enderror">
@error('start_date')<p class="mt-1 text-sm text-red-600">{{ $message }}</p>@enderror
</div>
<div>
<label for="start_time" class="block text-sm font-medium text-gray-700 mb-1">{{ __('admin.time') }} *</label>
<input type="time" name="start_time" id="start_time" value="{{ old('start_time', $event->start_at->format('H:i')) }}" required
class="w-full px-3 py-2 border border-gray-300 rounded-md @error('start_time') border-red-500 @enderror">
@error('start_time')<p class="mt-1 text-sm text-red-600">{{ $message }}</p>@enderror
</div>
</div>
<div class="mb-4">
<label for="status" class="block text-sm font-medium text-gray-700 mb-1">{{ __('admin.status') }} *</label>
<select name="status" id="status" required class="w-full px-3 py-2 border border-gray-300 rounded-md">
@foreach ($statuses as $s)
<option value="{{ $s->value }}" {{ old('status', $event->status->value) === $s->value ? 'selected' : '' }}>{{ $s->label() }}</option>
@endforeach
</select>
@error('status')<p class="mt-1 text-sm text-red-600">{{ $message }}</p>@enderror
</div>
{{-- Mindestanforderungen --}}
<div class="mb-4" x-data="minRequirementsData()" x-init="listenTypeChange()">
<div x-show="showDropdowns" x-cloak class="border border-gray-200 rounded-md p-4">
<h3 class="text-sm font-semibold text-gray-700 mb-3">{{ __('admin.min_requirements') }}</h3>
<div class="grid grid-cols-3 gap-4">
<div>
<label class="block text-xs text-gray-600 mb-1" x-text="playersLabel"></label>
<select name="min_players" x-model="minPlayers" class="w-full px-2 py-1.5 border border-gray-300 rounded-md text-sm">
<option value="">--</option>
@for ($n = 1; $n <= 30; $n++)
<option value="{{ $n }}" :hidden="{{ $n }} > playersMax" :disabled="{{ $n }} > playersMax">{{ $n }}</option>
@endfor
</select>
</div>
<div x-show="showCatering">
<label class="block text-xs text-gray-600 mb-1">{{ __('admin.min_catering') }}</label>
<select name="min_catering" x-model="minCatering" class="w-full px-2 py-1.5 border border-gray-300 rounded-md text-sm">
<option value="">--</option>
@for ($n = 1; $n <= 8; $n++)
<option value="{{ $n }}" :hidden="{{ $n }} > cateringMax" :disabled="{{ $n }} > cateringMax">{{ $n }}</option>
@endfor
</select>
</div>
<div x-show="showTimekeepers">
<label class="block text-xs text-gray-600 mb-1">{{ __('admin.min_timekeepers') }}</label>
<select name="min_timekeepers" x-model="minTimekeepers" class="w-full px-2 py-1.5 border border-gray-300 rounded-md text-sm">
<option value="">--</option>
@for ($n = 1; $n <= 8; $n++)
<option value="{{ $n }}" :hidden="{{ $n }} > timekeepersMax" :disabled="{{ $n }} > timekeepersMax">{{ $n }}</option>
@endfor
</select>
</div>
</div>
</div>
</div>
{{-- Ort & Adress-Suche --}}
<div class="mb-4" x-data="addressSearch()">
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('admin.location_address') }}</label>
{{-- Ortsname mit bekannten Orten --}}
<div class="mb-2 relative">
<label for="location_name" class="block text-xs text-gray-500 mb-1">{{ __('admin.location_name_hint') }}</label>
<input type="text" name="location_name" id="location_name" x-model="locationName"
@input.debounce.200ms="filterKnown()" @focus="filterKnown()" @keydown.escape="knownMatches = []"
class="w-full px-3 py-2 border border-gray-300 rounded-md" placeholder="{{ __('admin.location_name_placeholder') }}" autocomplete="off">
<div x-show="knownMatches.length > 0" x-cloak @click.outside="knownMatches = []"
class="absolute z-10 mt-1 w-full bg-white border border-blue-300 rounded-md shadow-lg max-h-48 overflow-y-auto">
<div class="px-3 py-1.5 bg-blue-50 text-xs font-medium text-blue-700 border-b">{{ __('admin.known_locations') }}</div>
<template x-for="(loc, idx) in knownMatches" :key="idx">
<button type="button" @click="selectKnown(loc)" class="w-full text-left px-3 py-2 hover:bg-blue-50 border-b border-gray-100">
<span class="block text-sm font-medium text-gray-900" x-text="loc.name"></span>
<span class="block text-xs text-gray-500" x-text="loc.address_text || ''" x-show="loc.address_text"></span>
</button>
</template>
</div>
</div>
{{-- Photon Adress-Suche --}}
<div class="relative">
<label class="block text-xs text-gray-500 mb-1">{{ __('admin.search_address') }}</label>
<input type="text" x-model="query" @input.debounce.300ms="search()" @keydown.escape="results = []"
placeholder="{{ __('admin.search_address_hint') }}"
class="w-full px-3 py-2 border border-gray-300 rounded-md" autocomplete="off">
<div x-show="loading" x-cloak class="absolute right-3 top-8">
<svg class="animate-spin h-4 w-4 text-gray-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path></svg>
</div>
<div x-show="results.length > 0" x-cloak @click.outside="results = []"
class="absolute z-10 mt-1 w-full bg-white border border-gray-300 rounded-md shadow-lg max-h-60 overflow-y-auto">
<template x-for="(r, idx) in results" :key="idx">
<button type="button" @click="select(r)" class="w-full text-left px-3 py-2 hover:bg-blue-50 border-b border-gray-100">
<span class="block text-sm font-medium text-gray-900" x-text="r.title"></span>
<span class="block text-xs text-gray-500" x-text="r.subtitle"></span>
</button>
</template>
</div>
</div>
<input type="hidden" name="address_text" id="address_text" x-ref="addressText" value="{{ old('address_text', $event->address_text) }}">
<input type="hidden" name="location_lat" id="location_lat" x-ref="lat" value="{{ old('location_lat', $event->location_lat) }}">
<input type="hidden" name="location_lng" id="location_lng" x-ref="lng" value="{{ old('location_lng', $event->location_lng) }}">
<p x-show="selectedAddress" x-text="selectedAddress" class="mt-1 text-xs text-green-700"></p>
@error('address_text')<p class="mt-1 text-sm text-red-600">{{ $message }}</p>@enderror
</div>
{{-- Beschreibung mit Quill --}}
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('admin.description') }}</label>
<div id="quill-editor" class="bg-white">{!! app(\App\Services\HtmlSanitizerService::class)->sanitize(old('description_html', $event->description_html ?? '')) !!}</div>
<input type="hidden" name="description_html" id="description_html" value="{{ old('description_html', $event->description_html) }}">
@error('description_html')<p class="mt-1 text-sm text-red-600">{{ $message }}</p>@enderror
</div>
{{-- Catering/Zeitnehmer-Zuweisungen (nicht für away_game/meeting) --}}
<div class="mb-4" x-data="assignmentData()" x-init="listenTeamChange()"
x-show="!['away_game', 'meeting'].includes(currentType)" x-cloak>
<template x-if="parents.length > 0">
<div class="border border-gray-200 rounded-md p-4">
<h3 class="text-sm font-semibold text-gray-700 mb-3">{{ __('admin.assignments') }}</h3>
<table class="w-full text-sm">
<thead>
<tr class="border-b border-gray-200">
<th class="text-left py-2 font-medium text-gray-600">{{ __('ui.name') }}</th>
<th class="text-center py-2 font-medium text-gray-600">{{ __('admin.catering_assignment') }}</th>
<th class="text-center py-2 font-medium text-gray-600">{{ __('admin.timekeeper_assignment') }}</th>
</tr>
</thead>
<tbody>
<template x-for="p in parents" :key="p.id">
<tr class="border-b border-gray-100">
<td class="py-2" x-text="p.name"></td>
<td class="py-2 text-center">
<input type="checkbox" name="catering_users[]" :value="p.id"
:checked="assignedCatering.includes(p.id)"
class="rounded border-gray-300 text-blue-600">
</td>
<td class="py-2 text-center">
<input type="checkbox" name="timekeeper_users[]" :value="p.id"
:checked="assignedTimekeeper.includes(p.id)"
class="rounded border-gray-300 text-blue-600">
</td>
</tr>
</template>
</tbody>
</table>
</div>
</template>
</div>
{{-- Dateien --}}
<div class="mb-4" x-data="{ showPicker: false, newFileCount: 0 }">
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('admin.event_files') }}</label>
{{-- Bereits angehängte Dateien --}}
@if ($event->files->isNotEmpty())
<div class="mb-3 space-y-1">
<p class="text-xs font-semibold text-gray-500">{{ __('admin.attached_files') }}</p>
@foreach ($event->files as $file)
<label class="flex items-center gap-2 py-0.5 text-sm text-gray-700 bg-blue-50 px-2 rounded">
<input type="checkbox" name="existing_files[]" value="{{ $file->id }}" checked class="rounded border-gray-300">
{{ $file->original_name }}
<span class="text-xs text-gray-400">({{ $file->category->name }} &middot; {{ $file->humanSize() }})</span>
</label>
@endforeach
</div>
@endif
{{-- Aus Bibliothek anhängen --}}
<button type="button" @click="showPicker = !showPicker" class="text-sm text-blue-600 hover:text-blue-800 mb-2">
{{ __('admin.attach_from_library') }} &darr;
</button>
<div x-show="showPicker" x-cloak class="border border-gray-200 rounded-md p-3 mb-3 max-h-48 overflow-y-auto">
@php $attachedIds = $event->files->pluck('id')->toArray(); @endphp
@foreach ($fileCategories as $cat)
@if ($cat->files->isNotEmpty())
<p class="text-xs font-semibold text-gray-500 mt-2 first:mt-0 mb-1">{{ $cat->name }}</p>
@foreach ($cat->files as $file)
@if (!in_array($file->id, $attachedIds))
<label class="flex items-center gap-2 py-0.5 text-sm text-gray-700 hover:bg-gray-50 px-1 rounded">
<input type="checkbox" name="existing_files[]" value="{{ $file->id }}" class="rounded border-gray-300">
{{ $file->original_name }} <span class="text-xs text-gray-400">({{ $file->humanSize() }})</span>
</label>
@endif
@endforeach
@endif
@endforeach
</div>
{{-- Neue Datei hochladen --}}
<div class="space-y-2">
<template x-for="i in newFileCount" :key="i">
<div class="flex items-center gap-2">
<input type="file" :name="'new_files[' + (i-1) + ']'" accept=".pdf,.docx,.xlsx,.jpg,.jpeg,.png,.gif,.webp"
class="flex-1 text-sm text-gray-600 file:mr-2 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">
<select :name="'new_file_categories[' + (i-1) + ']'" required class="px-2 py-1 border border-gray-300 rounded-md text-sm">
<option value="">{{ __('admin.select_category') }}</option>
@foreach ($fileCategories as $cat)
<option value="{{ $cat->id }}">{{ $cat->name }}</option>
@endforeach
</select>
</div>
</template>
</div>
<button type="button" @click="newFileCount++" class="mt-2 text-sm text-blue-600 hover:text-blue-800">+ {{ __('admin.upload_new_file') }}</button>
</div>
<div class="flex items-center gap-3">
<button type="submit" class="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 text-sm font-medium">{{ __('ui.save') }}</button>
<a href="{{ route('admin.events.index') }}" class="text-sm text-gray-600 hover:underline">{{ __('ui.cancel') }}</a>
</div>
</form>
</div>
{{-- Teilnehmer-Verwaltung --}}
@if ($event->participants->isNotEmpty())
@php
$isMeeting = $event->type === \App\Enums\EventType::Meeting;
$yesCount = $event->participants->where('status', \App\Enums\ParticipantStatus::Yes)->count();
$noCount = $event->participants->where('status', \App\Enums\ParticipantStatus::No)->count();
$openCount = $event->participants->where('status', \App\Enums\ParticipantStatus::Unknown)->count();
@endphp
<div class="bg-white rounded-lg shadow p-6 max-w-2xl mt-6" x-data="participantsSection()">
<h2 class="text-lg font-semibold mb-3">{{ __('events.participants') }}</h2>
<div class="flex gap-4 text-sm mb-4">
<span class="text-green-700 font-medium"><span x-text="counts.yes"></span> {{ __('events.confirmations') }}</span>
<span class="text-red-700 font-medium"><span x-text="counts.no"></span> {{ __('events.rejections') }}</span>
<span class="text-gray-500"><span x-text="counts.open"></span> {{ __('events.open_responses') }}</span>
</div>
<div class="divide-y divide-gray-100">
@if ($isMeeting)
@foreach ($event->participants->sortBy(fn($p) => $p->user->name ?? '') as $participant)
<div class="py-2 flex items-center justify-between gap-2">
<span class="text-sm font-medium text-gray-900">{{ $participant->user->name ?? '' }}</span>
<div class="flex items-center gap-1">
<button type="button" @click="setStatus({{ $participant->id }}, 'yes')"
class="px-2 py-1 text-xs rounded-md transition-colors"
:class="statuses['{{ $participant->id }}'] === 'yes' ? 'bg-green-600 text-white' : 'bg-gray-100 text-gray-600 hover:bg-green-100'">
{{ __('ui.yes') }}
</button>
<button type="button" @click="setStatus({{ $participant->id }}, 'no')"
class="px-2 py-1 text-xs rounded-md transition-colors"
:class="statuses['{{ $participant->id }}'] === 'no' ? 'bg-red-600 text-white' : 'bg-gray-100 text-gray-600 hover:bg-red-100'">
{{ __('ui.no') }}
</button>
<button type="button" @click="setStatus({{ $participant->id }}, 'unknown')"
class="px-2 py-1 text-xs rounded-md transition-colors"
:class="statuses['{{ $participant->id }}'] === 'unknown' ? 'bg-gray-600 text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'">
{{ __('ui.open') }}
</button>
<span x-show="savingId === {{ $participant->id }}" class="text-xs text-gray-400 ml-1">...</span>
<span x-show="savedId === {{ $participant->id }}" x-cloak class="text-xs text-green-600 ml-1">&#10003;</span>
</div>
</div>
@endforeach
@else
@foreach ($event->participants->sortBy('player.last_name') as $participant)
<div class="py-2 flex items-center justify-between gap-2">
<span class="text-sm font-medium text-gray-900">{{ $participant->player->full_name ?? '' }}</span>
<div class="flex items-center gap-1">
<button type="button" @click="setStatus({{ $participant->id }}, 'yes')"
class="px-2 py-1 text-xs rounded-md transition-colors"
:class="statuses['{{ $participant->id }}'] === 'yes' ? 'bg-green-600 text-white' : 'bg-gray-100 text-gray-600 hover:bg-green-100'">
{{ __('ui.yes') }}
</button>
<button type="button" @click="setStatus({{ $participant->id }}, 'no')"
class="px-2 py-1 text-xs rounded-md transition-colors"
:class="statuses['{{ $participant->id }}'] === 'no' ? 'bg-red-600 text-white' : 'bg-gray-100 text-gray-600 hover:bg-red-100'">
{{ __('ui.no') }}
</button>
<button type="button" @click="setStatus({{ $participant->id }}, 'unknown')"
class="px-2 py-1 text-xs rounded-md transition-colors"
:class="statuses['{{ $participant->id }}'] === 'unknown' ? 'bg-gray-600 text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'">
{{ __('ui.open') }}
</button>
<span x-show="savingId === {{ $participant->id }}" class="text-xs text-gray-400 ml-1">...</span>
<span x-show="savedId === {{ $participant->id }}" x-cloak class="text-xs text-green-600 ml-1">&#10003;</span>
</div>
</div>
@endforeach
@endif
</div>
</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>
const quill = new Quill('#quill-editor', {
theme: 'snow',
modules: {
toolbar: [
[{ 'header': [2, 3, 4, false] }],
['bold', 'italic', 'underline'],
[{ 'color': ['#000000', '#e60000', '#ff9900', '#008a00', '#0066cc', '#9933ff', '#ffffff', '#888888'] },
{ 'background': ['', '#ffd6d6', '#fff3cd', '#d4edda', '#cce5ff', '#e8d5f5'] }],
[{ 'list': 'ordered' }, { 'list': 'bullet' }],
['blockquote', 'link'],
['clean']
]
},
placeholder: @js(__('admin.description_placeholder'))
});
// Sofort nach Init den hidden Input befüllen
document.getElementById('description_html').value = quill.root.innerHTML;
document.getElementById('eventForm').addEventListener('submit', function () {
document.getElementById('description_html').value = quill.root.innerHTML;
});
// Typen ohne Catering/Zeitnehmer
const noCateringTypes = ['away_game', 'meeting'];
function minRequirementsData() {
const defaults = @js($eventDefaults);
const toStr = v => (v != null && v !== '') ? String(v) : '';
const existing = {
min_players: toStr(@js(old('min_players', $event->min_players))),
min_catering: toStr(@js(old('min_catering', $event->min_catering))),
min_timekeepers: toStr(@js(old('min_timekeepers', $event->min_timekeepers))),
};
const originalType = @js(old('type', $event->type->value));
const ranges = {
home_game: { players: 14, catering: 4, timekeepers: 4 },
away_game: { players: 14, catering: 0, timekeepers: 0 },
training: { players: 30, catering: 4, timekeepers: 4 },
tournament: { players: 14, catering: 8, timekeepers: 8 },
meeting: { players: 20, catering: 0, timekeepers: 0 },
};
return {
currentType: originalType,
minPlayers: existing.min_players,
minCatering: existing.min_catering,
minTimekeepers: existing.min_timekeepers,
get showDropdowns() { return this.currentType && this.currentType !== 'other'; },
get showCatering() { return !noCateringTypes.includes(this.currentType); },
get showTimekeepers() { return !noCateringTypes.includes(this.currentType); },
get playersLabel() { return this.currentType === 'meeting' ? @js(__('admin.min_users')) : @js(__('admin.min_players')); },
get playersMax() { const r = ranges[this.currentType]; return r ? r.players : 0; },
get cateringMax() { const r = ranges[this.currentType]; return r ? r.catering : 0; },
get timekeepersMax() { const r = ranges[this.currentType]; return r ? r.timekeepers : 0; },
listenTypeChange() {
const sel = document.getElementById('type');
this.currentType = sel.value;
sel.addEventListener('change', () => {
this.currentType = sel.value;
if (sel.value !== originalType) {
const d = defaults[sel.value];
this.minPlayers = toStr(d?.min_players);
this.minCatering = toStr(d?.min_catering);
this.minTimekeepers = toStr(d?.min_timekeepers);
} else {
this.minPlayers = existing.min_players;
this.minCatering = existing.min_catering;
this.minTimekeepers = existing.min_timekeepers;
}
});
}
};
}
function assignmentData() {
const teamParents = @js($teamParents);
const assignedCatering = @js($assignedCatering);
const assignedTimekeeper = @js($assignedTimekeeper);
return {
parents: [],
assignedCatering,
assignedTimekeeper,
listenTeamChange() {
const sel = document.getElementById('team_id');
this.parents = teamParents[sel.value] || [];
sel.addEventListener('change', () => {
this.parents = teamParents[sel.value] || [];
});
}
};
}
function participantsSection() {
const initStatuses = @js($event->participants->mapWithKeys(fn($p) => [$p->id => $p->status->value]));
const vals = Object.values(initStatuses);
return {
counts: {
yes: vals.filter(s => s === 'yes').length,
no: vals.filter(s => s === 'no').length,
open: vals.filter(s => s === 'unknown').length,
},
statuses: initStatuses,
savingId: null,
savedId: null,
async setStatus(id, newStatus) {
const oldStatus = this.statuses[id];
if (oldStatus === newStatus) return;
this.statuses[id] = newStatus;
this.savingId = id;
this.savedId = null;
this.adjustCount(oldStatus, -1);
this.adjustCount(newStatus, 1);
try {
const res = await fetch(`{{ route('admin.events.update-participant', $event) }}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
'Accept': 'application/json',
},
body: JSON.stringify({ participant_id: id, status: newStatus }),
});
if (res.ok) {
this.savedId = id;
setTimeout(() => { if (this.savedId === id) this.savedId = null; }, 1500);
} else {
this.statuses[id] = oldStatus;
this.adjustCount(newStatus, -1);
this.adjustCount(oldStatus, 1);
}
} catch (e) {
this.statuses[id] = oldStatus;
this.adjustCount(newStatus, -1);
this.adjustCount(oldStatus, 1);
console.error(e);
} finally {
if (this.savingId === id) this.savingId = null;
}
},
adjustCount(status, delta) {
if (status === 'yes') this.counts.yes += delta;
else if (status === 'no') this.counts.no += delta;
else this.counts.open += delta;
}
};
}
function addressSearch() {
const knownLocations = @js($knownLocations);
return {
locationName: @js(old('location_name', $event->location_name ?? '')),
query: '',
results: [],
loading: false,
selectedAddress: @js(old('address_text', $event->address_text ?? '')),
knownMatches: [],
_abortCtrl: null,
filterKnown() {
const input = this.locationName.trim().toLowerCase();
if (input.length < 1) { this.knownMatches = knownLocations.slice(0, 8); return; }
const words = input.split(/\s+/);
this.knownMatches = knownLocations.filter(loc => {
const haystack = (loc.name + ' ' + (loc.address_text || '')).toLowerCase();
return words.every(w => haystack.includes(w));
}).slice(0, 8);
},
selectKnown(loc) {
this.locationName = loc.name;
this.$refs.addressText.value = loc.address_text || '';
this.$refs.lat.value = loc.location_lat || '';
this.$refs.lng.value = loc.location_lng || '';
this.selectedAddress = loc.address_text || '';
this.query = loc.address_text || '';
this.knownMatches = [];
},
formatFeature(f) {
const p = f.properties;
const street = [p.street, p.housenumber].filter(Boolean).join(' ');
const effectiveStreet = street || (p.name && p.name !== p.city ? p.name : '');
const cityLine = [p.postcode, p.city].filter(Boolean).join(' ');
const address = [effectiveStreet, cityLine].filter(Boolean).join(', ');
const name = p.name || '';
const isPlace = name && name !== effectiveStreet && name !== p.city && name !== p.street;
return {
title: isPlace ? name : (address || name || ''),
subtitle: isPlace ? address : (cityLine || p.state || ''),
address: address || name || '',
name: isPlace ? name : '',
lat: f.geometry.coordinates[1],
lon: f.geometry.coordinates[0],
};
},
async search() {
const q = this.query.trim();
if (q.length < 2) { this.results = []; return; }
if (this._abortCtrl) this._abortCtrl.abort();
this._abortCtrl = new AbortController();
this.loading = true;
try {
const params = new URLSearchParams({
q: q, lang: 'de', limit: '7',
lat: '51.4', lon: '7.5',
bbox: '5.87,50.32,9.46,52.53',
});
const resp = await fetch('https://photon.komoot.io/api/?' + params, {
signal: this._abortCtrl.signal
});
const data = await resp.json();
this.results = (data.features || []).map(f => this.formatFeature(f));
} catch (e) {
if (e.name !== 'AbortError') this.results = [];
} finally {
this.loading = false;
}
},
select(r) {
this.$refs.addressText.value = r.address;
this.$refs.lat.value = r.lat;
this.$refs.lng.value = r.lon;
this.selectedAddress = r.address;
this.query = r.address;
this.results = [];
if (!this.locationName && r.name) {
this.locationName = r.name;
}
}
};
}
</script>
</x-layouts.admin>