- Administration & Rollenmanagement: Neuer Admin-Bereich mit Feature-Toggles und Sichtbarkeitseinstellungen pro Rolle (11 Toggles, 24 Visibility-Settings) - AdministrationController mit eigenem Settings-Tab, aus SettingsController extrahiert - Feature-Toggle-Guards in Controllers (Invitation, File, ListGenerator, Comment) und Views (events/show, events/edit, events/create) - Setting::isFeatureEnabled() und isFeatureVisibleFor() Hilfsmethoden - Wiederkehrende Trainings: Täglich/Wöchentlich/2-Wöchentlich mit Ende per Datum oder Anzahl (max. 52), Vorschau im Formular - Event-Serien: Verknüpfung über event_series_id (UUID), Modal-Dialog beim Speichern und Löschen mit Optionen "nur dieses" / "alle folgenden" - Löschen-Button direkt in der Event-Bearbeitung mit Serien-Dialog - DemoDataSeeder: 4 Trainings als Serie mit gemeinsamer event_series_id - Übersetzungen in allen 6 Sprachen (de, en, pl, ru, ar, tr) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
589 lines
41 KiB
PHP
Executable File
589 lines
41 KiB
PHP
Executable File
<x-layouts.admin :title="__('admin.settings_title')">
|
|
<div x-data="settingsPage()" x-init="init()">
|
|
<h1 class="text-2xl font-bold mb-6">{{ __('admin.settings_title') }}</h1>
|
|
|
|
{{-- Tab Navigation --}}
|
|
<div class="border-b border-gray-200 mb-6 -mx-4 px-4 overflow-x-auto">
|
|
<nav class="flex -mb-px gap-1" role="tablist">
|
|
<button type="button" @click="tab = 'general'"
|
|
:class="tab === 'general' ? 'border-blue-500 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'"
|
|
class="whitespace-nowrap px-4 py-2.5 border-b-2 text-sm font-medium transition-colors" role="tab">
|
|
{{ __('admin.settings_tab_general') }}
|
|
</button>
|
|
<button type="button" @click="tab = 'legal'"
|
|
:class="tab === 'legal' ? 'border-blue-500 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'"
|
|
class="whitespace-nowrap px-4 py-2.5 border-b-2 text-sm font-medium transition-colors" role="tab">
|
|
{{ __('admin.settings_tab_legal') }}
|
|
</button>
|
|
<button type="button" @click="tab = 'defaults'"
|
|
:class="tab === 'defaults' ? 'border-blue-500 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'"
|
|
class="whitespace-nowrap px-4 py-2.5 border-b-2 text-sm font-medium transition-colors" role="tab">
|
|
{{ __('admin.settings_tab_defaults') }}
|
|
</button>
|
|
<button type="button" @click="tab = 'categories'"
|
|
:class="tab === 'categories' ? 'border-blue-500 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'"
|
|
class="whitespace-nowrap px-4 py-2.5 border-b-2 text-sm font-medium transition-colors" role="tab">
|
|
{{ __('admin.settings_tab_categories') }}
|
|
</button>
|
|
@if (auth()->user()->isAdmin())
|
|
<button type="button" @click="tab = 'seasons'"
|
|
:class="tab === 'seasons' ? 'border-blue-500 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'"
|
|
class="whitespace-nowrap px-4 py-2.5 border-b-2 text-sm font-medium transition-colors" role="tab">
|
|
{{ __('admin.settings_tab_seasons') }}
|
|
</button>
|
|
@endif
|
|
</nav>
|
|
</div>
|
|
|
|
<form id="settings-form" method="POST" action="{{ route('admin.settings.update') }}" enctype="multipart/form-data" @submit="syncEditors()">
|
|
@csrf
|
|
@method('PUT')
|
|
|
|
{{-- Tab: Allgemein --}}
|
|
<div x-show="tab === 'general'" x-effect="if (tab === 'general' && !sloganInitialized) $nextTick(() => initSloganEditors())" role="tabpanel">
|
|
{{-- Text-Inputs (app_name etc.) --}}
|
|
@foreach ($settings as $key => $setting)
|
|
@if ($setting->type !== 'html' && $setting->type !== 'richtext' && $key !== 'app_favicon' && $key !== 'statistics_enabled' && $key !== 'license_key')
|
|
<div class="bg-white rounded-lg shadow p-6 mb-6">
|
|
<label for="setting-{{ $key }}" class="block text-sm font-semibold text-gray-700 mb-2">{{ $setting->label }}</label>
|
|
<input
|
|
type="text"
|
|
id="setting-{{ $key }}"
|
|
name="settings[{{ $key }}]"
|
|
value="{{ $setting->value }}"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
>
|
|
</div>
|
|
@endif
|
|
@endforeach
|
|
|
|
{{-- Favicon --}}
|
|
<div class="bg-white rounded-lg shadow p-6 mb-6">
|
|
<label class="block text-sm font-semibold text-gray-700 mb-2">{{ __('admin.favicon_label') }}</label>
|
|
@php $currentFavicon = \App\Models\Setting::get('app_favicon'); @endphp
|
|
@if ($currentFavicon)
|
|
<div class="flex items-center gap-3 mb-3">
|
|
<img src="{{ asset('storage/' . $currentFavicon) }}" alt="Favicon" class="w-8 h-8 object-contain border border-gray-200 rounded">
|
|
<span class="text-sm text-gray-500">{{ __('admin.favicon_current') }}</span>
|
|
<label class="flex items-center gap-1.5 text-sm text-red-600 cursor-pointer">
|
|
<input type="checkbox" name="remove_favicon" value="1" class="rounded border-gray-300">
|
|
{{ __('admin.favicon_remove') }}
|
|
</label>
|
|
</div>
|
|
@endif
|
|
<input
|
|
type="file"
|
|
name="favicon"
|
|
accept=".ico,.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.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')
|
|
<div class="bg-white rounded-lg shadow p-6 mb-6">
|
|
<label class="block text-sm font-semibold text-gray-700 mb-1">{{ $setting->label }}</label>
|
|
<p class="text-xs text-gray-400 mb-3">{{ __('admin.slogan_hint') }}</p>
|
|
<div id="slogan-editor-{{ $key }}" class="bg-white" style="min-height: 60px;">{!! app(\App\Services\HtmlSanitizerService::class)->sanitize($setting->value ?? '') !!}</div>
|
|
<input type="hidden" name="settings[{{ $key }}]" id="slogan-input-{{ $key }}" value="{{ $setting->value }}">
|
|
</div>
|
|
@endif
|
|
@endforeach
|
|
</div>
|
|
|
|
{{-- Tab: Rechtliches — Multi-Language mit Flaggen --}}
|
|
<div x-show="tab === 'legal'" x-effect="if (tab === 'legal' && !editorsInitialized) $nextTick(() => initEditors())" role="tabpanel">
|
|
{{-- Sprach-Flaggen-Leiste --}}
|
|
<div class="flex items-center gap-1 mb-6 bg-white rounded-lg shadow px-4 py-3">
|
|
<span class="text-sm font-medium text-gray-600 mr-3">{{ __('admin.legal_language_label') }}:</span>
|
|
@php
|
|
$localeFlags = ['de' => "\u{1F1E9}\u{1F1EA}", 'en' => "\u{1F1EC}\u{1F1E7}", 'pl' => "\u{1F1F5}\u{1F1F1}", 'ru' => "\u{1F1F7}\u{1F1FA}", 'ar' => "\u{1F1F8}\u{1F1E6}", 'tr' => "\u{1F1F9}\u{1F1F7}"];
|
|
$localeNames = ['de' => 'DE', 'en' => 'EN', 'pl' => 'PL', 'ru' => 'RU', 'ar' => 'AR', 'tr' => 'TR'];
|
|
@endphp
|
|
@foreach ($availableLocales as $loc)
|
|
<button type="button"
|
|
@click="legalLocale = @js($loc)"
|
|
:class="legalLocale === @js($loc) ? 'bg-blue-600 text-white shadow-sm' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'"
|
|
class="px-3 py-1.5 rounded-md text-sm font-medium transition-colors cursor-pointer">
|
|
{{ $localeFlags[$loc] }} {{ $localeNames[$loc] }}
|
|
</button>
|
|
@endforeach
|
|
</div>
|
|
|
|
{{-- Impressum pro Sprache --}}
|
|
@foreach ($availableLocales as $loc)
|
|
@php $impKey = "impressum_html_{$loc}"; @endphp
|
|
<div x-show="legalLocale === '{{ $loc }}'" class="bg-white rounded-lg shadow p-6 mb-6">
|
|
<div class="flex items-center justify-between mb-3">
|
|
<label class="block text-sm font-semibold text-gray-700">{{ __('admin.legal_impressum_label') }} ({{ strtoupper($loc) }})</label>
|
|
<button type="button"
|
|
@click="toggleHtml(@js($impKey))"
|
|
:class="htmlMode[@js($impKey)] ? 'bg-blue-600 text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'"
|
|
class="text-xs px-2.5 py-1 rounded font-mono transition-colors cursor-pointer">
|
|
</> HTML
|
|
</button>
|
|
</div>
|
|
<div :class="htmlMode[@js($impKey)] && 'hidden'">
|
|
<div id="editor-{{ $impKey }}" class="bg-white" style="min-height: 250px;">{!! app(\App\Services\HtmlSanitizerService::class)->sanitize($localeSettings[$loc]['impressum_html'] ?? '') !!}</div>
|
|
</div>
|
|
<textarea x-show="htmlMode[@js($impKey)]" x-cloak id="html-{{ $impKey }}"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm font-mono leading-relaxed"
|
|
style="min-height: 350px;" spellcheck="false"></textarea>
|
|
<p x-show="htmlMode[@js($impKey)]" x-cloak class="mt-2 text-xs text-gray-400">{{ __('admin.html_anchor_hint') }}</p>
|
|
<input type="hidden" name="settings[{{ $impKey }}]" id="input-{{ $impKey }}" value="{{ $localeSettings[$loc]['impressum_html'] }}">
|
|
</div>
|
|
@endforeach
|
|
|
|
{{-- Datenschutz pro Sprache --}}
|
|
@foreach ($availableLocales as $loc)
|
|
@php $dsKey = "datenschutz_html_{$loc}"; @endphp
|
|
<div x-show="legalLocale === '{{ $loc }}'" class="bg-white rounded-lg shadow p-6 mb-6">
|
|
<div class="flex items-center justify-between mb-3">
|
|
<label class="block text-sm font-semibold text-gray-700">{{ __('admin.legal_datenschutz_label') }} ({{ strtoupper($loc) }})</label>
|
|
<button type="button"
|
|
@click="toggleHtml(@js($dsKey))"
|
|
:class="htmlMode[@js($dsKey)] ? 'bg-blue-600 text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'"
|
|
class="text-xs px-2.5 py-1 rounded font-mono transition-colors cursor-pointer">
|
|
</> HTML
|
|
</button>
|
|
</div>
|
|
<div :class="htmlMode[@js($dsKey)] && 'hidden'">
|
|
<div id="editor-{{ $dsKey }}" class="bg-white" style="min-height: 250px;">{!! app(\App\Services\HtmlSanitizerService::class)->sanitize($localeSettings[$loc]['datenschutz_html'] ?? '') !!}</div>
|
|
</div>
|
|
<textarea x-show="htmlMode[@js($dsKey)]" x-cloak id="html-{{ $dsKey }}"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm font-mono leading-relaxed"
|
|
style="min-height: 350px;" spellcheck="false"></textarea>
|
|
<p x-show="htmlMode[@js($dsKey)]" x-cloak class="mt-2 text-xs text-gray-400">{{ __('admin.html_anchor_hint') }}</p>
|
|
<input type="hidden" name="settings[{{ $dsKey }}]" id="input-{{ $dsKey }}" value="{{ $localeSettings[$loc]['datenschutz_html'] }}">
|
|
</div>
|
|
@endforeach
|
|
|
|
{{-- Passwort-Reset E-Mail pro Sprache --}}
|
|
@foreach ($availableLocales as $loc)
|
|
@php $prKey = "password_reset_email_{$loc}"; @endphp
|
|
<div x-show="legalLocale === '{{ $loc }}'" class="bg-white rounded-lg shadow p-6 mb-6">
|
|
<div class="flex items-center justify-between mb-3">
|
|
<label class="block text-sm font-semibold text-gray-700">{{ __('admin.legal_password_reset_email_label') }} ({{ strtoupper($loc) }})</label>
|
|
<button type="button"
|
|
@click="toggleHtml(@js($prKey))"
|
|
:class="htmlMode[@js($prKey)] ? 'bg-blue-600 text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'"
|
|
class="text-xs px-2.5 py-1 rounded font-mono transition-colors cursor-pointer">
|
|
</> HTML
|
|
</button>
|
|
</div>
|
|
<div :class="htmlMode[@js($prKey)] && 'hidden'">
|
|
<div id="editor-{{ $prKey }}" class="bg-white" style="min-height: 150px;">{!! app(\App\Services\HtmlSanitizerService::class)->sanitize($localeSettings[$loc]['password_reset_email'] ?? '') !!}</div>
|
|
</div>
|
|
<textarea x-show="htmlMode[@js($prKey)]" x-cloak id="html-{{ $prKey }}"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm font-mono leading-relaxed"
|
|
style="min-height: 200px;" spellcheck="false"></textarea>
|
|
<p class="mt-2 text-xs text-gray-400">{{ __('admin.password_reset_email_hint') }}</p>
|
|
<input type="hidden" name="settings[{{ $prKey }}]" id="input-{{ $prKey }}" value="{{ $localeSettings[$loc]['password_reset_email'] }}">
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
|
|
{{-- Tab: Event-Defaults --}}
|
|
<div x-show="tab === 'defaults'" role="tabpanel">
|
|
<div class="bg-white rounded-lg shadow p-6">
|
|
<p class="text-sm text-gray-500 mb-5">{{ __('admin.event_defaults_description') }}</p>
|
|
|
|
@php
|
|
$noCateringTypes = ['away_game', 'meeting'];
|
|
@endphp
|
|
<div class="space-y-4">
|
|
@foreach (['home_game', 'away_game', 'training', 'tournament', 'meeting'] as $eventType)
|
|
<div class="border border-gray-200 rounded-md p-4">
|
|
<h3 class="text-sm font-semibold text-gray-700 mb-3">{{ __("ui.enums.event_type.{$eventType}") }}</h3>
|
|
<div class="grid grid-cols-3 gap-2 sm:gap-3">
|
|
@foreach (['players', 'catering', 'timekeepers'] as $field)
|
|
@php
|
|
$key = "default_min_{$field}_{$eventType}";
|
|
$currentVal = $eventDefaults[$key] ?? null;
|
|
$isDisabled = in_array($eventType, $noCateringTypes) && in_array($field, ['catering', 'timekeepers']);
|
|
$label = $eventType === 'meeting' && $field === 'players' ? __('admin.min_users') : __("admin.min_{$field}");
|
|
@endphp
|
|
<div>
|
|
<label class="block text-xs text-gray-600 mb-1 truncate" title="{{ $label }}">{{ $label }}</label>
|
|
@if ($isDisabled)
|
|
<div class="w-full px-2 py-1.5 border border-gray-200 rounded-md text-sm text-gray-400 bg-gray-50">{{ __('admin.not_applicable') }}</div>
|
|
@else
|
|
<select name="settings[{{ $key }}]" class="w-full px-2 py-1.5 border border-gray-300 rounded-md text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
|
<option value="">--</option>
|
|
@for ($i = 0; $i <= 20; $i++)
|
|
<option value="{{ $i }}" {{ $currentVal !== null && (string) $currentVal === (string) $i ? 'selected' : '' }}>{{ $i }}</option>
|
|
@endfor
|
|
</select>
|
|
@endif
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Tab: Saisons (nur Admin) --}}
|
|
@if (auth()->user()->isAdmin())
|
|
<div x-show="tab === 'seasons'" role="tabpanel" x-data="{ editId: null, editName: '', editStart: '', editEnd: '', editCurrent: false }">
|
|
<div class="bg-white rounded-lg shadow p-6">
|
|
<h3 class="text-lg font-semibold text-gray-900 mb-4">{{ __('admin.seasons_title') }}</h3>
|
|
|
|
{{-- Bestehende Saisons --}}
|
|
@if ($seasons->isNotEmpty())
|
|
<div class="overflow-x-auto mb-6">
|
|
<table class="w-full text-sm">
|
|
<thead class="bg-gray-50 border-b">
|
|
<tr>
|
|
<th class="text-left px-4 py-2 font-medium text-gray-600">{{ __('admin.season_name') }}</th>
|
|
<th class="text-left px-4 py-2 font-medium text-gray-600">{{ __('admin.season_start') }}</th>
|
|
<th class="text-left px-4 py-2 font-medium text-gray-600">{{ __('admin.season_end') }}</th>
|
|
<th class="text-center px-4 py-2 font-medium text-gray-600">{{ __('admin.season_current') }}</th>
|
|
<th class="text-right px-4 py-2 font-medium text-gray-600">{{ __('admin.actions') }}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-gray-100">
|
|
@foreach ($seasons as $season)
|
|
<tr>
|
|
<template x-if="editId === {{ $season->id }}">
|
|
<td colspan="5" class="px-4 py-3">
|
|
<form method="POST" action="{{ route('admin.seasons.update', $season) }}" class="flex flex-wrap items-end gap-3">
|
|
@csrf
|
|
@method('PUT')
|
|
<div>
|
|
<label class="block text-xs text-gray-500 mb-1">{{ __('admin.season_name') }}</label>
|
|
<input type="text" name="name" x-model="editName" required class="px-2 py-1.5 border border-gray-300 rounded-md text-sm w-32">
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs text-gray-500 mb-1">{{ __('admin.season_start') }}</label>
|
|
<input type="date" name="start_date" x-model="editStart" required class="px-2 py-1.5 border border-gray-300 rounded-md text-sm">
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs text-gray-500 mb-1">{{ __('admin.season_end') }}</label>
|
|
<input type="date" name="end_date" x-model="editEnd" required class="px-2 py-1.5 border border-gray-300 rounded-md text-sm">
|
|
</div>
|
|
<label class="flex items-center gap-1.5 text-sm">
|
|
<input type="hidden" name="is_current" value="0">
|
|
<input type="checkbox" name="is_current" value="1" x-model="editCurrent" class="rounded border-gray-300">
|
|
{{ __('admin.season_current') }}
|
|
</label>
|
|
<div class="flex gap-2">
|
|
<button type="submit" class="px-3 py-1.5 bg-blue-600 text-white rounded-md text-sm hover:bg-blue-700">{{ __('ui.save') }}</button>
|
|
<button type="button" @click="editId = null" class="px-3 py-1.5 bg-gray-100 text-gray-700 rounded-md text-sm hover:bg-gray-200">{{ __('ui.cancel') }}</button>
|
|
</div>
|
|
</form>
|
|
</td>
|
|
</template>
|
|
<template x-if="editId !== {{ $season->id }}">
|
|
<td class="px-4 py-2 font-medium">{{ $season->name }}</td>
|
|
</template>
|
|
<template x-if="editId !== {{ $season->id }}">
|
|
<td class="px-4 py-2">{{ $season->start_date->format('d.m.Y') }}</td>
|
|
</template>
|
|
<template x-if="editId !== {{ $season->id }}">
|
|
<td class="px-4 py-2">{{ $season->end_date->format('d.m.Y') }}</td>
|
|
</template>
|
|
<template x-if="editId !== {{ $season->id }}">
|
|
<td class="px-4 py-2 text-center">
|
|
@if ($season->is_current)
|
|
<span class="inline-block px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">{{ __('admin.season_current') }}</span>
|
|
@endif
|
|
</td>
|
|
</template>
|
|
<template x-if="editId !== {{ $season->id }}">
|
|
<td class="px-4 py-2 text-right space-x-2">
|
|
<button type="button" @click="editId = {{ $season->id }}; editName = @js($season->name); editStart = @js($season->start_date->format('Y-m-d')); editEnd = @js($season->end_date->format('Y-m-d')); editCurrent = {{ $season->is_current ? 'true' : 'false' }}" class="text-blue-600 hover:underline text-sm">{{ __('ui.edit') }}</button>
|
|
<form method="POST" action="{{ route('admin.seasons.destroy', $season) }}" class="inline" onsubmit="return confirm('{{ __('admin.season_confirm_delete') }}')">
|
|
@csrf
|
|
@method('DELETE')
|
|
<button type="submit" class="text-red-600 hover:underline text-sm">{{ __('ui.delete') }}</button>
|
|
</form>
|
|
</td>
|
|
</template>
|
|
</tr>
|
|
@endforeach
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
@else
|
|
<p class="text-sm text-gray-500 mb-6">{{ __('admin.no_seasons_yet') }}</p>
|
|
@endif
|
|
|
|
{{-- Neue Saison erstellen --}}
|
|
<h4 class="text-sm font-semibold text-gray-700 mb-3">{{ __('admin.new_season') }}</h4>
|
|
<form method="POST" action="{{ route('admin.seasons.store') }}" class="flex flex-wrap items-end gap-3">
|
|
@csrf
|
|
<div>
|
|
<label class="block text-xs text-gray-500 mb-1">{{ __('admin.season_name') }}</label>
|
|
<input type="text" name="name" required placeholder="2025/2026" class="px-2 py-1.5 border border-gray-300 rounded-md text-sm w-32">
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs text-gray-500 mb-1">{{ __('admin.season_start') }}</label>
|
|
<input type="date" name="start_date" required class="px-2 py-1.5 border border-gray-300 rounded-md text-sm">
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs text-gray-500 mb-1">{{ __('admin.season_end') }}</label>
|
|
<input type="date" name="end_date" required class="px-2 py-1.5 border border-gray-300 rounded-md text-sm">
|
|
</div>
|
|
<label class="flex items-center gap-1.5 text-sm">
|
|
<input type="hidden" name="is_current" value="0">
|
|
<input type="checkbox" name="is_current" value="1" class="rounded border-gray-300">
|
|
{{ __('admin.season_current') }}
|
|
</label>
|
|
<button type="submit" class="px-3 py-1.5 bg-blue-600 text-white rounded-md text-sm hover:bg-blue-700">{{ __('ui.create') }}</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
{{-- Save/Cancel (sichtbar auf allen Form-Tabs) --}}
|
|
<div x-show="tab !== 'categories'" class="flex gap-3 mt-6">
|
|
<button type="submit" class="bg-blue-600 text-white px-6 py-2 rounded-md hover:bg-blue-700 font-medium">
|
|
{{ __('ui.save') }}
|
|
</button>
|
|
<a href="{{ route('admin.dashboard') }}" class="bg-gray-200 text-gray-700 px-6 py-2 rounded-md hover:bg-gray-300">
|
|
{{ __('ui.cancel') }}
|
|
</a>
|
|
</div>
|
|
</form>
|
|
|
|
{{-- Tab: Dateikategorien (eigene Formulare) --}}
|
|
<div x-show="tab === 'categories'" role="tabpanel">
|
|
<div class="bg-white rounded-lg shadow p-6">
|
|
<p class="text-sm text-gray-500 mb-5">{{ __('admin.file_categories_description') }}</p>
|
|
|
|
@if ($fileCategories->isNotEmpty())
|
|
<div class="space-y-2 mb-5">
|
|
@foreach ($fileCategories as $cat)
|
|
<div class="flex flex-wrap items-center gap-2 sm:gap-3 border border-gray-200 rounded-md px-3 py-2.5">
|
|
<form method="POST" action="{{ route('admin.file-categories.update', $cat) }}" class="flex flex-wrap items-center gap-2 sm:gap-3 flex-1 min-w-0">
|
|
@csrf
|
|
@method('PUT')
|
|
<input type="text" name="name" value="{{ $cat->name }}" required
|
|
class="flex-1 min-w-[140px] px-2 py-1.5 border border-gray-300 rounded text-sm">
|
|
<label class="flex items-center gap-1.5 text-xs text-gray-600">
|
|
<input type="hidden" name="is_active" value="0">
|
|
<input type="checkbox" name="is_active" value="1" {{ $cat->is_active ? 'checked' : '' }} class="rounded border-gray-300">
|
|
{{ __('admin.active') }}
|
|
</label>
|
|
<span class="text-xs text-gray-400 tabular-nums">{{ $cat->files_count }} {{ __('admin.nav_files') }}</span>
|
|
<button type="submit" class="text-xs text-blue-600 hover:text-blue-800 font-medium">{{ __('ui.save') }}</button>
|
|
</form>
|
|
<form method="POST" action="{{ route('admin.file-categories.destroy', $cat) }}" class="inline" onsubmit="return confirm(@js(__('admin.confirm_delete_category')))">
|
|
@csrf
|
|
@method('DELETE')
|
|
<button type="submit" class="text-xs font-medium {{ $cat->files_count === 0 ? 'text-red-600 hover:text-red-800' : 'text-gray-300 cursor-not-allowed' }}"
|
|
{{ $cat->files_count > 0 ? 'disabled' : '' }}>{{ __('ui.delete') }}</button>
|
|
</form>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
@endif
|
|
|
|
<form method="POST" action="{{ route('admin.file-categories.store') }}" class="flex items-center gap-3 border-t border-gray-200 pt-4">
|
|
@csrf
|
|
<input type="text" name="name" placeholder="{{ __('admin.category_name') }}" required
|
|
class="flex-1 px-3 py-2 border border-gray-300 rounded-md text-sm">
|
|
<button type="submit" class="bg-gray-800 text-white px-3 py-2 rounded-md hover:bg-gray-900 text-sm whitespace-nowrap">{{ __('admin.new_category') }}</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
@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: 200px; }
|
|
.ql-toolbar.ql-snow { border-radius: 0.375rem 0.375rem 0 0; }
|
|
.ql-container.ql-snow { border-radius: 0 0 0.375rem 0.375rem; }
|
|
[id^="slogan-editor-"] .ql-editor { min-height: 60px; }
|
|
[id^="editor-password_reset_email_"] .ql-editor { min-height: 120px; }
|
|
</style>
|
|
@endpush
|
|
|
|
@push('scripts')
|
|
<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>
|
|
function settingsPage() {
|
|
return {
|
|
tab: 'general',
|
|
legalLocale: 'de',
|
|
editorsInitialized: false,
|
|
sloganInitialized: false,
|
|
initializedLocales: [],
|
|
editors: {},
|
|
sloganEditors: {},
|
|
htmlMode: {},
|
|
toolbarOptions: [
|
|
[{ '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']
|
|
],
|
|
|
|
init() {
|
|
const validTabs = ['general', 'legal', 'defaults', 'categories', 'seasons'];
|
|
const urlTab = new URLSearchParams(window.location.search).get('tab');
|
|
const hash = window.location.hash.replace('#', '');
|
|
const stored = sessionStorage.getItem('settings_tab');
|
|
if (validTabs.includes(urlTab)) {
|
|
this.tab = urlTab;
|
|
} else if (validTabs.includes(hash)) {
|
|
this.tab = hash;
|
|
} else if (validTabs.includes(stored)) {
|
|
this.tab = stored;
|
|
}
|
|
this.$watch('tab', val => {
|
|
sessionStorage.setItem('settings_tab', val);
|
|
history.replaceState(null, '', '#' + val);
|
|
});
|
|
// Bei Locale-Wechsel: Editoren fuer neue Locale lazy initialisieren
|
|
this.$watch('legalLocale', (locale) => {
|
|
if (this.editorsInitialized) {
|
|
this.$nextTick(() => this.initLocaleEditors(locale));
|
|
}
|
|
});
|
|
},
|
|
|
|
initSloganEditors() {
|
|
if (this.sloganInitialized) return;
|
|
|
|
const miniToolbar = [['bold', 'italic']];
|
|
|
|
@foreach ($settings as $key => $setting)
|
|
@if ($setting->type === 'richtext')
|
|
this.sloganEditors[@js($key)] = new Quill('#slogan-editor-' + @js($key), {
|
|
theme: 'snow',
|
|
modules: { toolbar: miniToolbar }
|
|
});
|
|
document.getElementById('slogan-input-' + @js($key)).value = this.sloganEditors[@js($key)].root.innerHTML;
|
|
@endif
|
|
@endforeach
|
|
|
|
this.sloganInitialized = true;
|
|
},
|
|
|
|
// Nur die aktuelle Locale initialisieren (sichtbare Editoren)
|
|
initEditors() {
|
|
if (this.editorsInitialized) return;
|
|
this.initLocaleEditors(this.legalLocale);
|
|
this.editorsInitialized = true;
|
|
},
|
|
|
|
initLocaleEditors(locale) {
|
|
if (this.initializedLocales.includes(locale)) return;
|
|
|
|
const editorTypes = ['impressum_html', 'datenschutz_html', 'password_reset_email'];
|
|
editorTypes.forEach(type => {
|
|
const key = type + '_' + locale;
|
|
const el = document.getElementById('editor-' + key);
|
|
if (el) {
|
|
this.editors[key] = new Quill('#editor-' + key, {
|
|
theme: 'snow',
|
|
modules: { toolbar: this.toolbarOptions }
|
|
});
|
|
this.htmlMode[key] = false;
|
|
|
|
// Content aus Hidden-Input laden
|
|
const input = document.getElementById('input-' + key);
|
|
if (input && input.value) {
|
|
this.editors[key].root.innerHTML = input.value;
|
|
}
|
|
}
|
|
});
|
|
|
|
this.initializedLocales.push(locale);
|
|
},
|
|
|
|
toggleHtml(key) {
|
|
if (!this.editors[key]) return;
|
|
|
|
if (!this.htmlMode[key]) {
|
|
const html = this.editors[key].root.innerHTML;
|
|
document.getElementById('html-' + key).value = this.formatHtml(html);
|
|
this.htmlMode[key] = true;
|
|
} else {
|
|
const html = document.getElementById('html-' + key).value;
|
|
this.editors[key].root.innerHTML = html;
|
|
this.htmlMode[key] = false;
|
|
}
|
|
},
|
|
|
|
formatHtml(html) {
|
|
return html
|
|
.replace(/<(h[2-4]|p|ul|ol|blockquote)/g, '\n<$1')
|
|
.replace(/<li>/g, '\n <li>')
|
|
.trimStart();
|
|
},
|
|
|
|
syncEditors() {
|
|
// Nur initialisierte Editoren synchronisieren
|
|
// Nicht-initialisierte Locales: Hidden-Inputs behalten Server-Werte
|
|
for (const [key, editor] of Object.entries(this.editors)) {
|
|
if (this.htmlMode[key]) {
|
|
document.getElementById('input-' + key).value = document.getElementById('html-' + key).value;
|
|
} else {
|
|
document.getElementById('input-' + key).value = editor.root.innerHTML;
|
|
}
|
|
}
|
|
// Sync slogan editors (richtext type)
|
|
for (const [key, editor] of Object.entries(this.sloganEditors)) {
|
|
document.getElementById('slogan-input-' + key).value = editor.root.innerHTML;
|
|
}
|
|
}
|
|
};
|
|
}
|
|
</script>
|
|
@endpush
|
|
</x-layouts.admin>
|