Zeigt bei Event-Erstellen, Event-Bearbeiten und Team-Bearbeiten den Hinweis auf erlaubte Dateitypen und maximale Dateigröße (10 MB) unterhalb des Upload-Buttons an. Die Dateibibliothek und die eigenständige Upload-Seite hatten den Hinweis bereits. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
558 lines
34 KiB
PHP
Executable File
558 lines
34 KiB
PHP
Executable File
<x-layouts.admin :title="__('admin.new_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.new_event_title') }}</h1>
|
||
|
||
<div class="bg-white rounded-lg shadow p-6 max-w-2xl">
|
||
<form method="POST" action="{{ route('admin.events.store') }}" id="eventForm" enctype="multipart/form-data"
|
||
x-data="{ currentType: @js(old('type', '')) }" x-init="$watch('currentType', () => {}); document.getElementById('type').addEventListener('change', (e) => currentType = e.target.value); currentType = document.getElementById('type').value;">
|
||
@csrf
|
||
|
||
<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') == $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') === $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') }}" 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') }}" 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') }}" 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') }}" 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') }}" 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') }}" 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', 'draft') === $s->value ? 'selected' : '' }}>{{ $s->label() }}</option>
|
||
@endforeach
|
||
</select>
|
||
@error('status')<p class="mt-1 text-sm text-red-600">{{ $message }}</p>@enderror
|
||
</div>
|
||
|
||
{{-- Wiederholung (nur Training) --}}
|
||
<div class="mb-4" x-data="recurrenceData()" x-show="isTraining" x-cloak>
|
||
<div class="border border-gray-200 rounded-md p-4">
|
||
<h3 class="text-sm font-semibold text-gray-700 mb-3">{{ __('admin.recurrence') }}</h3>
|
||
|
||
<div class="mb-3">
|
||
<label class="block text-xs text-gray-600 mb-1">{{ __('admin.recurrence') }}</label>
|
||
<select x-model="recurrence" class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm">
|
||
<option value="none">{{ __('admin.recurrence_none') }}</option>
|
||
<option value="daily">{{ __('admin.recurrence_daily') }}</option>
|
||
<option value="weekly">{{ __('admin.recurrence_weekly') }}</option>
|
||
<option value="biweekly">{{ __('admin.recurrence_biweekly') }}</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div x-show="recurrence !== 'none'" x-cloak>
|
||
<div class="mb-3">
|
||
<label class="block text-xs text-gray-600 mb-1">{{ __('admin.recurrence_end_type') }}</label>
|
||
<div class="flex gap-4">
|
||
<label class="flex items-center gap-1.5 text-sm text-gray-700 cursor-pointer">
|
||
<input type="radio" x-model="endType" value="date" class="text-blue-600">
|
||
{{ __('admin.recurrence_end_date') }}
|
||
</label>
|
||
<label class="flex items-center gap-1.5 text-sm text-gray-700 cursor-pointer">
|
||
<input type="radio" x-model="endType" value="count" class="text-blue-600">
|
||
{{ __('admin.recurrence_end_count') }}
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<div x-show="endType === 'date'" class="mb-3">
|
||
<label class="block text-xs text-gray-600 mb-1">{{ __('admin.recurrence_end_date') }}</label>
|
||
<input type="date" x-model="endDate" class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm">
|
||
</div>
|
||
|
||
<div x-show="endType === 'count'" class="mb-3">
|
||
<label class="block text-xs text-gray-600 mb-1">{{ __('admin.recurrence_count_label') }}</label>
|
||
<input type="number" x-model.number="count" min="1" max="52" class="w-24 px-3 py-2 border border-gray-300 rounded-md text-sm">
|
||
</div>
|
||
|
||
<div x-show="previewCount > 0" class="text-sm text-blue-600 bg-blue-50 rounded-md px-3 py-2">
|
||
<span x-text="previewText"></span>
|
||
</div>
|
||
<div x-show="previewCount >= 52" class="text-xs text-orange-600 mt-1">
|
||
{{ __('admin.recurrence_max_warning', ['max' => 52]) }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<input type="hidden" name="recurrence" :value="recurrence">
|
||
<input type="hidden" name="recurrence_end_type" :value="endType">
|
||
<input type="hidden" name="recurrence_end_date" :value="endDate">
|
||
<input type="hidden" name="recurrence_count" :value="count">
|
||
</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>
|
||
@if (\App\Models\Setting::isFeatureEnabled('catering'))
|
||
<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>
|
||
@endif
|
||
@if (\App\Models\Setting::isFeatureEnabled('timekeepers'))
|
||
<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>
|
||
@endif
|
||
</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" value="{{ old('address_text') }}" x-ref="addressText">
|
||
<input type="hidden" name="location_lat" id="location_lat" value="{{ old('location_lat') }}" x-ref="lat">
|
||
<input type="hidden" name="location_lng" id="location_lng" value="{{ old('location_lng') }}" x-ref="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', '')) !!}</div>
|
||
<input type="hidden" name="description_html" id="description_html" value="{{ old('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) --}}
|
||
@if (\App\Models\Setting::isFeatureEnabled('catering') || \App\Models\Setting::isFeatureEnabled('timekeepers'))
|
||
<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>
|
||
@if (\App\Models\Setting::isFeatureEnabled('catering'))
|
||
<th class="text-center py-2 font-medium text-gray-600">{{ __('admin.catering_assignment') }}</th>
|
||
@endif
|
||
@if (\App\Models\Setting::isFeatureEnabled('timekeepers'))
|
||
<th class="text-center py-2 font-medium text-gray-600">{{ __('admin.timekeeper_assignment') }}</th>
|
||
@endif
|
||
</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>
|
||
@if (\App\Models\Setting::isFeatureEnabled('catering'))
|
||
<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>
|
||
@endif
|
||
@if (\App\Models\Setting::isFeatureEnabled('timekeepers'))
|
||
<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>
|
||
@endif
|
||
</tr>
|
||
</template>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
@endif
|
||
|
||
{{-- 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>
|
||
|
||
{{-- 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') }} ↓
|
||
</button>
|
||
<div x-show="showPicker" x-cloak class="border border-gray-200 rounded-md p-3 mb-3 max-h-48 overflow-y-auto">
|
||
@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)
|
||
<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>
|
||
@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>
|
||
<p class="mt-1 text-xs text-gray-400">{{ __('admin.allowed_file_types') }} · {{ __('admin.max_file_size') }}</p>
|
||
</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.create') }}</button>
|
||
<a href="{{ route('admin.events.index') }}" class="text-sm text-gray-600 hover:underline">{{ __('ui.cancel') }}</a>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
|
||
{{-- 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>
|
||
// Quill Editor
|
||
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;
|
||
|
||
// Sync Quill content to hidden field on form submit
|
||
document.getElementById('eventForm').addEventListener('submit', function () {
|
||
document.getElementById('description_html').value = quill.root.innerHTML;
|
||
});
|
||
|
||
// Recurrence data for Training events
|
||
function recurrenceData() {
|
||
const typeSel = document.getElementById('type');
|
||
const dateSel = document.getElementById('start_date');
|
||
const previewTpl = @js(__('admin.recurrence_preview', ['count' => '__COUNT__']));
|
||
return {
|
||
recurrence: @js(old('recurrence', 'none')),
|
||
endType: @js(old('recurrence_end_type', 'date')),
|
||
endDate: @js(old('recurrence_end_date', '')),
|
||
count: @js((int) old('recurrence_count', 10)),
|
||
isTraining: typeSel.value === 'training',
|
||
init() {
|
||
typeSel.addEventListener('change', () => {
|
||
this.isTraining = typeSel.value === 'training';
|
||
if (!this.isTraining) this.recurrence = 'none';
|
||
});
|
||
},
|
||
get startDate() { return dateSel.value; },
|
||
get previewCount() {
|
||
if (this.recurrence === 'none') return 0;
|
||
if (this.endType === 'count') return Math.min(Math.max(1, this.count || 0), 52);
|
||
if (!this.startDate || !this.endDate) return 0;
|
||
const days = { daily: 1, weekly: 7, biweekly: 14 };
|
||
const start = new Date(this.startDate);
|
||
const end = new Date(this.endDate);
|
||
const diff = Math.floor((end - start) / (1000 * 60 * 60 * 24));
|
||
if (diff <= 0) return 0;
|
||
return Math.min(Math.floor(diff / days[this.recurrence]), 52);
|
||
},
|
||
get previewText() {
|
||
return previewTpl.replace('__COUNT__', this.previewCount);
|
||
}
|
||
};
|
||
}
|
||
|
||
// Assignment data for Catering/Timekeeper
|
||
function assignmentData() {
|
||
const teamParents = @js($teamParents);
|
||
const assignedCatering = @js(collect(old('catering_users', []))->map(fn($id) => (int) $id)->values()->toArray());
|
||
const assignedTimekeeper = @js(collect(old('timekeeper_users', []))->map(fn($id) => (int) $id)->values()->toArray());
|
||
return {
|
||
parents: [],
|
||
assignedCatering,
|
||
assignedTimekeeper,
|
||
listenTeamChange() {
|
||
const sel = document.getElementById('team_id');
|
||
this.parents = teamParents[sel.value] || [];
|
||
sel.addEventListener('change', () => {
|
||
this.parents = teamParents[sel.value] || [];
|
||
});
|
||
}
|
||
};
|
||
}
|
||
|
||
// Typen ohne Catering/Zeitnehmer
|
||
const noCateringTypes = ['away_game', 'meeting'];
|
||
|
||
// Minimum requirements with dynamic dropdowns
|
||
function minRequirementsData() {
|
||
const defaults = @js($eventDefaults);
|
||
const hasOldInput = @js(old('_token') !== null);
|
||
const toStr = v => (v != null && v !== '') ? String(v) : '';
|
||
const oldValues = {
|
||
min_players: toStr(@js(old('min_players', ''))),
|
||
min_catering: toStr(@js(old('min_catering', ''))),
|
||
min_timekeepers: toStr(@js(old('min_timekeepers', ''))),
|
||
};
|
||
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 },
|
||
};
|
||
const initialType = @js(old('type', ''));
|
||
return {
|
||
currentType: initialType,
|
||
minPlayers: oldValues.min_players,
|
||
minCatering: oldValues.min_catering,
|
||
minTimekeepers: oldValues.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;
|
||
if (!hasOldInput) {
|
||
this.applyDefaults(sel.value);
|
||
}
|
||
sel.addEventListener('change', () => {
|
||
this.currentType = sel.value;
|
||
this.applyDefaults(sel.value);
|
||
});
|
||
},
|
||
applyDefaults(type) {
|
||
const d = defaults[type];
|
||
this.minPlayers = toStr(d?.min_players);
|
||
this.minCatering = toStr(d?.min_catering);
|
||
this.minTimekeepers = toStr(d?.min_timekeepers);
|
||
}
|
||
};
|
||
}
|
||
|
||
// Address search with Photon API (OpenStreetMap) + known locations
|
||
function addressSearch() {
|
||
const knownLocations = @js($knownLocations);
|
||
return {
|
||
locationName: @js(old('location_name', '')),
|
||
query: '',
|
||
results: [],
|
||
loading: false,
|
||
selectedAddress: @js(old('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>
|