Files
WebAPP/resources/views/admin/settings/edit.blade.php
Rhino 8ccadbe89f Feature-Toggles, Administration, wiederkehrende Events und Event-Serien
- 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>
2026-03-03 08:38:45 +01:00

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">
&lt;/&gt; 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">
&lt;/&gt; 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">
&lt;/&gt; 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>