- 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>
870 lines
58 KiB
PHP
Executable File
870 lines
58 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 = 'mail'"
|
|
:class="tab === 'mail' ? '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_mail') }}
|
|
</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 = 'visibility'"
|
|
:class="tab === 'visibility' ? '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_visibility') }}
|
|
</button>
|
|
<button type="button" @click="tab = 'license'"
|
|
:class="tab === 'license' ? '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_license') }}
|
|
</button>
|
|
<button type="button" @click="tab = 'maintenance'"
|
|
:class="tab === 'maintenance' ? 'border-red-500 text-red-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_maintenance') }}
|
|
</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')
|
|
<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: E-Mail --}}
|
|
<div x-show="tab === 'mail'" role="tabpanel">
|
|
<form method="POST" action="{{ route('admin.settings.update-mail') }}"
|
|
x-data="{
|
|
mailMailer: @js($mailConfig['mailer'] ?? 'log'),
|
|
mailTesting: false,
|
|
mailTestResult: false,
|
|
mailTestSuccess: false,
|
|
mailTestMessage: '',
|
|
async testSmtp() {
|
|
this.mailTesting = true;
|
|
this.mailTestResult = false;
|
|
try {
|
|
const res = await fetch('{{ route("admin.settings.test-mail") }}', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
|
'Accept': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
mail_host: this.$refs.mailHost.value,
|
|
mail_port: this.$refs.mailPort.value,
|
|
mail_username: this.$refs.mailUsername.value,
|
|
mail_password: this.$refs.mailPassword.value,
|
|
mail_encryption: this.$refs.mailEncryption.value,
|
|
}),
|
|
});
|
|
const data = await res.json();
|
|
this.mailTestSuccess = data.success;
|
|
this.mailTestMessage = data.message;
|
|
} catch (e) {
|
|
this.mailTestSuccess = false;
|
|
this.mailTestMessage = 'Netzwerkfehler: ' + e.message;
|
|
}
|
|
this.mailTesting = false;
|
|
this.mailTestResult = true;
|
|
}
|
|
}">
|
|
@csrf
|
|
@method('PUT')
|
|
|
|
<div class="bg-white rounded-lg shadow p-6 mb-6">
|
|
<h3 class="text-base font-semibold text-gray-800 mb-1">{{ __('admin.mail_config_title') }}</h3>
|
|
<p class="text-sm text-gray-500 mb-5">{{ __('admin.mail_config_hint') }}</p>
|
|
|
|
{{-- Versandmethode --}}
|
|
<div class="mb-5">
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">{{ __('admin.mail_mailer_label') }}</label>
|
|
<div class="flex gap-4">
|
|
<label class="flex items-center gap-2 cursor-pointer">
|
|
<input type="radio" name="mail_mailer" value="smtp" x-model="mailMailer"
|
|
class="text-blue-600 focus:ring-blue-500">
|
|
<span class="text-sm">SMTP</span>
|
|
</label>
|
|
<label class="flex items-center gap-2 cursor-pointer">
|
|
<input type="radio" name="mail_mailer" value="log" x-model="mailMailer"
|
|
class="text-blue-600 focus:ring-blue-500">
|
|
<span class="text-sm">{{ __('admin.mail_log_mode') }}</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- SMTP-Felder --}}
|
|
<div x-show="mailMailer === 'smtp'" x-cloak class="space-y-4 p-4 bg-gray-50 border border-gray-200 rounded-md">
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('admin.mail_host_label') }}</label>
|
|
<input type="text" name="mail_host" x-ref="mailHost"
|
|
value="{{ $mailConfig['host'] }}"
|
|
placeholder="z.B. smtp.strato.de"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
|
@error('mail_host') <p class="text-xs text-red-600 mt-1">{{ $message }}</p> @enderror
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('admin.mail_port_label') }}</label>
|
|
<input type="number" name="mail_port" x-ref="mailPort"
|
|
value="{{ $mailConfig['port'] }}"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
|
@error('mail_port') <p class="text-xs text-red-600 mt-1">{{ $message }}</p> @enderror
|
|
</div>
|
|
</div>
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('admin.mail_username_label') }}</label>
|
|
<input type="text" name="mail_username" x-ref="mailUsername"
|
|
value="{{ $mailConfig['username'] }}"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
|
@error('mail_username') <p class="text-xs text-red-600 mt-1">{{ $message }}</p> @enderror
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('admin.mail_password_label') }}</label>
|
|
<input type="password" name="mail_password" x-ref="mailPassword"
|
|
value="{{ $mailConfig['password'] }}"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
|
@error('mail_password') <p class="text-xs text-red-600 mt-1">{{ $message }}</p> @enderror
|
|
</div>
|
|
</div>
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('admin.mail_from_address_label') }}</label>
|
|
<input type="email" name="mail_from_address"
|
|
value="{{ $mailConfig['from_address'] }}"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
|
@error('mail_from_address') <p class="text-xs text-red-600 mt-1">{{ $message }}</p> @enderror
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('admin.mail_from_name_label') }} <span class="text-gray-400 font-normal">(optional)</span></label>
|
|
<input type="text" name="mail_from_name"
|
|
value="{{ $mailConfig['from_name'] }}"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('admin.mail_encryption_label') }}</label>
|
|
<select name="mail_encryption" x-ref="mailEncryption"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
|
@php $enc = $mailConfig['encryption'] ?? 'tls'; @endphp
|
|
<option value="tls" {{ $enc === 'tls' ? 'selected' : '' }}>TLS (Port 587)</option>
|
|
<option value="ssl" {{ $enc === 'ssl' ? 'selected' : '' }}>SSL (Port 465)</option>
|
|
<option value="none" {{ !in_array($enc, ['tls', 'ssl']) ? 'selected' : '' }}>{{ __('admin.mail_encryption_none') }}</option>
|
|
</select>
|
|
</div>
|
|
|
|
{{-- SMTP-Test --}}
|
|
<div class="pt-3 border-t border-gray-200">
|
|
<button type="button" @click="testSmtp()"
|
|
:disabled="mailTesting"
|
|
class="px-4 py-2 text-sm font-medium text-white bg-emerald-600 rounded-md hover:bg-emerald-700 transition disabled:opacity-50 disabled:cursor-wait inline-flex items-center gap-2">
|
|
<template x-if="mailTesting">
|
|
<svg class="animate-spin h-4 w-4 text-white" 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>
|
|
</template>
|
|
<span x-text="mailTesting ? '{{ __("admin.mail_testing") }}' : '{{ __("admin.mail_test_button") }}'"></span>
|
|
</button>
|
|
<p x-show="mailTestResult" x-cloak x-text="mailTestMessage"
|
|
:class="mailTestSuccess ? 'text-green-600' : 'text-red-600'"
|
|
class="text-sm mt-2"></p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex justify-end">
|
|
<button type="submit"
|
|
class="px-5 py-2 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700 transition">
|
|
{{ __('admin.mail_save') }}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</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: Sichtbarkeit (nur Admin) --}}
|
|
@if (auth()->user()->isAdmin())
|
|
<div x-show="tab === 'visibility'" role="tabpanel">
|
|
<div class="bg-white rounded-lg shadow p-6">
|
|
<p class="text-sm text-gray-500 mb-5">{{ __('admin.visibility_description') }}</p>
|
|
|
|
@php
|
|
$features = [
|
|
'statistics' => __('admin.visibility_feature_statistics'),
|
|
'catering_history' => __('admin.visibility_feature_catering_history'),
|
|
];
|
|
$roles = [
|
|
'coach' => __('ui.enums.user_role.coach'),
|
|
'parent_rep' => __('ui.enums.user_role.parent_rep'),
|
|
];
|
|
@endphp
|
|
|
|
<div class="space-y-4">
|
|
@foreach ($features as $featureKey => $featureLabel)
|
|
<div class="border border-gray-200 rounded-md p-4">
|
|
<h3 class="text-sm font-semibold text-gray-700 mb-3">{{ $featureLabel }}</h3>
|
|
<div class="flex flex-wrap gap-6">
|
|
@foreach ($roles as $roleKey => $roleLabel)
|
|
@php
|
|
$settingKey = "visibility_{$featureKey}_{$roleKey}";
|
|
$currentValue = $visibilitySettings[$settingKey]->value ?? '1';
|
|
@endphp
|
|
<label class="flex items-center gap-3 cursor-pointer" x-data="{ on: {{ $currentValue === '1' ? 'true' : 'false' }} }">
|
|
<input type="hidden" name="settings[{{ $settingKey }}]" :value="on ? '1' : '0'">
|
|
<button type="button" @click="on = !on"
|
|
:class="on ? 'bg-blue-600' : 'bg-gray-300'"
|
|
class="relative inline-flex h-6 w-11 flex-shrink-0 rounded-full transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2">
|
|
<span :class="on ? 'translate-x-5' : 'translate-x-0'"
|
|
class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out mt-0.5 ml-0.5"></span>
|
|
</button>
|
|
<span class="text-sm text-gray-700">{{ $roleLabel }}</span>
|
|
</label>
|
|
@endforeach
|
|
</div>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
{{-- Tab: Lizenz & Support (nur Admin) --}}
|
|
@if (auth()->user()->isAdmin())
|
|
<div x-show="tab === 'license'" role="tabpanel">
|
|
{{-- License Key --}}
|
|
<div class="bg-white rounded-lg shadow p-6 mb-6">
|
|
<h2 class="text-lg font-semibold text-gray-900 mb-1">{{ __('admin.license_title') }}</h2>
|
|
<p class="text-sm text-gray-500 mb-4">{{ __('admin.license_description') }}</p>
|
|
|
|
<label for="setting-license_key" class="block text-sm font-semibold text-gray-700 mb-2">{{ __('admin.license_key_label') }}</label>
|
|
<input type="text" name="settings[license_key]" id="setting-license_key"
|
|
value="{{ $settings['license_key']->value ?? '' }}"
|
|
placeholder="XXXX-XXXX-XXXX-XXXX"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm font-mono focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
|
|
|
{{-- Registration Status --}}
|
|
<div class="mt-6 pt-4 border-t border-gray-200">
|
|
<h3 class="text-sm font-semibold text-gray-700 mb-2">{{ __('admin.registration_status') }}</h3>
|
|
@if ($isRegistered)
|
|
<div class="flex items-center gap-2 text-sm text-green-700">
|
|
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
|
</svg>
|
|
{{ __('admin.registration_active') }}
|
|
</div>
|
|
<p class="text-xs text-gray-500 mt-1">Installation-ID: <span class="font-mono">{{ $installationId }}</span></p>
|
|
@else
|
|
<div class="flex items-center gap-2 text-sm text-gray-500 mb-3">
|
|
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
|
</svg>
|
|
{{ __('admin.registration_inactive') }}
|
|
</div>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
|
|
{{-- System Info --}}
|
|
<div class="bg-white rounded-lg shadow p-6 mb-6">
|
|
<h3 class="text-sm font-semibold text-gray-700 mb-3">{{ __('admin.version_info') }}</h3>
|
|
<dl class="grid grid-cols-2 gap-y-2 gap-x-4 text-sm">
|
|
<dt class="text-gray-500">App-Version:</dt>
|
|
<dd class="text-gray-800 font-mono">{{ config('app.version') }}</dd>
|
|
<dt class="text-gray-500">PHP:</dt>
|
|
<dd class="text-gray-800 font-mono">{{ PHP_VERSION }}</dd>
|
|
<dt class="text-gray-500">Laravel:</dt>
|
|
<dd class="text-gray-800 font-mono">{{ app()->version() }}</dd>
|
|
<dt class="text-gray-500">Datenbank:</dt>
|
|
<dd class="text-gray-800 font-mono">{{ config('database.default') }}</dd>
|
|
</dl>
|
|
|
|
@if ($updateInfo && version_compare($updateInfo['latest_version'] ?? '0', config('app.version'), '>'))
|
|
<div class="mt-4 p-3 bg-blue-50 border border-blue-200 rounded-md">
|
|
<p class="text-sm font-medium text-blue-800">
|
|
{{ __('admin.update_available', ['version' => $updateInfo['latest_version']]) }}
|
|
</p>
|
|
@if ($updateInfo['changelog'] ?? null)
|
|
<p class="text-xs text-blue-600 mt-1">{{ $updateInfo['changelog'] }}</p>
|
|
@endif
|
|
@if (($updateInfo['download_url'] ?? null) && str_starts_with($updateInfo['download_url'], 'https://'))
|
|
<a href="{{ $updateInfo['download_url'] }}" target="_blank" rel="noopener"
|
|
class="inline-block mt-2 text-sm text-blue-700 underline">
|
|
{{ __('admin.download_update') }}
|
|
</a>
|
|
@endif
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
</div>
|
|
@endif
|
|
|
|
{{-- Save/Cancel (sichtbar auf allen Form-Tabs, nicht auf Wartung) --}}
|
|
<div x-show="tab !== 'categories' && tab !== 'maintenance'" 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>
|
|
|
|
{{-- Registration (außerhalb der Settings-Form, nur auf Lizenz-Tab) --}}
|
|
@if (auth()->user()->isAdmin() && !$isRegistered)
|
|
<div x-show="tab === 'license'" class="mt-6">
|
|
<div class="bg-white rounded-lg shadow p-6">
|
|
<p class="text-sm text-gray-600 mb-3">{{ __('admin.support_not_registered') }}</p>
|
|
<form method="POST" action="{{ route('admin.support.register') }}">
|
|
@csrf
|
|
<button type="submit" class="bg-green-600 text-white px-4 py-2 rounded-md text-sm hover:bg-green-700">
|
|
{{ __('admin.register_now') }}
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
{{-- Tab: Wartung (nur Admin, eigenes Formular) --}}
|
|
@if (auth()->user()->isAdmin())
|
|
<div x-show="tab === 'maintenance'" role="tabpanel">
|
|
<div class="bg-white rounded-lg shadow p-6">
|
|
<h2 class="text-lg font-semibold text-gray-900 mb-4">{{ __('admin.demo_data_delete_title') }}</h2>
|
|
<p class="text-sm text-gray-600 mb-4">{{ __('admin.demo_data_delete_description') }}</p>
|
|
|
|
<div class="grid sm:grid-cols-2 gap-4 mb-5">
|
|
<div class="border border-red-200 bg-red-50 rounded-md p-4">
|
|
<h3 class="text-sm font-semibold text-red-700 mb-2">{{ __('admin.demo_data_deletes') }}</h3>
|
|
<ul class="text-sm text-red-600 space-y-1 list-disc list-inside">
|
|
<li>{{ __('admin.stat_users') }} ({{ __('admin.demo_data_except_admin') }})</li>
|
|
<li>{{ __('admin.nav_teams') }}</li>
|
|
<li>{{ __('admin.nav_players') }}</li>
|
|
<li>{{ __('admin.nav_events') }}</li>
|
|
<li>Kommentare</li>
|
|
<li>{{ __('admin.nav_locations') }}</li>
|
|
<li>{{ __('admin.nav_files') }}</li>
|
|
<li>{{ __('admin.activity_log_title') }}</li>
|
|
</ul>
|
|
</div>
|
|
<div class="border border-green-200 bg-green-50 rounded-md p-4">
|
|
<h3 class="text-sm font-semibold text-green-700 mb-2">{{ __('admin.demo_data_keeps') }}</h3>
|
|
<ul class="text-sm text-green-600 space-y-1 list-disc list-inside">
|
|
<li>{{ __('admin.demo_data_keeps_admin') }}</li>
|
|
<li>{{ __('admin.nav_settings') }}</li>
|
|
<li>{{ __('admin.settings_tab_categories') }}</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="border border-red-300 bg-red-50 rounded-md p-4 mb-5">
|
|
<p class="text-sm text-red-700 font-medium">{{ __('admin.demo_data_delete_warning') }}</p>
|
|
</div>
|
|
|
|
<form method="POST" action="{{ route('admin.settings.destroy-demo-data') }}"
|
|
onsubmit="return confirm(@js(__('admin.demo_data_delete_confirm')))">
|
|
@csrf
|
|
@method('DELETE')
|
|
<div class="mb-4">
|
|
<label for="demo-delete-password" class="block text-sm font-medium text-gray-700 mb-1">{{ __('admin.factory_reset_password_label') }}</label>
|
|
<input type="password" name="password" id="demo-delete-password" required autocomplete="current-password"
|
|
class="w-full max-w-sm px-3 py-2 border border-gray-300 rounded-md text-sm focus:ring-2 focus:ring-red-500 focus:border-red-500">
|
|
</div>
|
|
<button type="submit"
|
|
class="px-5 py-2.5 text-sm font-medium text-white bg-red-600 rounded-md hover:bg-red-700 transition">
|
|
{{ __('admin.demo_data_delete_button') }}
|
|
</button>
|
|
</form>
|
|
</div>
|
|
|
|
{{-- Factory Reset (nur Admin) --}}
|
|
@if (auth()->user()->isAdmin())
|
|
<div class="bg-white rounded-lg shadow p-6 mt-6 border-2 border-red-300">
|
|
<h2 class="text-lg font-semibold text-red-700 mb-4">{{ __('admin.factory_reset_title') }}</h2>
|
|
<p class="text-sm text-gray-600 mb-4">{{ __('admin.factory_reset_description') }}</p>
|
|
|
|
<div class="border border-red-200 bg-red-50 rounded-md p-4 mb-5">
|
|
<h3 class="text-sm font-semibold text-red-700 mb-2">{{ __('admin.factory_reset_deletes') }}</h3>
|
|
<ul class="text-sm text-red-600 space-y-1 list-disc list-inside">
|
|
<li>{{ __('admin.factory_reset_item_users') }}</li>
|
|
<li>{{ __('admin.factory_reset_item_data') }}</li>
|
|
<li>{{ __('admin.factory_reset_item_settings') }}</li>
|
|
<li>{{ __('admin.factory_reset_item_files') }}</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="bg-red-100 border border-red-300 rounded-md p-4 mb-5">
|
|
<p class="text-sm text-red-800 font-bold">{{ __('admin.factory_reset_warning') }}</p>
|
|
</div>
|
|
|
|
<form method="POST" action="{{ route('admin.settings.factory-reset') }}"
|
|
onsubmit="return confirm(@js(__('admin.factory_reset_confirm')))">
|
|
@csrf
|
|
@method('DELETE')
|
|
<div class="mb-4">
|
|
<label for="factory-reset-password" class="block text-sm font-medium text-gray-700 mb-1">{{ __('admin.factory_reset_password_label') }}</label>
|
|
<input type="password" name="password" id="factory-reset-password" required autocomplete="current-password"
|
|
class="w-full max-w-sm px-3 py-2 border border-gray-300 rounded-md text-sm focus:ring-2 focus:ring-red-500 focus:border-red-500">
|
|
</div>
|
|
<div class="mb-5">
|
|
<label for="factory-reset-confirmation" class="block text-sm font-medium text-gray-700 mb-1">{{ __('admin.factory_reset_confirmation_label') }}</label>
|
|
<input type="text" name="confirmation" id="factory-reset-confirmation" required
|
|
placeholder="RESET-BESTÄTIGT"
|
|
class="w-full max-w-sm px-3 py-2 border border-gray-300 rounded-md text-sm font-mono focus:ring-2 focus:ring-red-500 focus:border-red-500">
|
|
<p class="mt-1 text-xs text-gray-500">{{ __('admin.factory_reset_confirmation_hint') }}</p>
|
|
</div>
|
|
<button type="submit"
|
|
class="px-5 py-2.5 text-sm font-medium text-white bg-red-700 rounded-md hover:bg-red-800 transition">
|
|
{{ __('admin.factory_reset_button') }}
|
|
</button>
|
|
</form>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
@endif
|
|
|
|
{{-- 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', 'mail', 'legal', 'defaults', 'categories', 'visibility', 'license', 'maintenance'];
|
|
const hash = window.location.hash.replace('#', '');
|
|
const stored = sessionStorage.getItem('settings_tab');
|
|
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>
|