Stand: SMTP-Test, Admin-Mail-Tab, Notifiable-Fix, Lazy-Quill

- Fix: Notifiable-Trait zum User-Model hinzugefuegt (behebt notify()-500er)
- Installer: SMTP-Verbindungstest mit EsmtpTransport + Ueberspringen-Link
- Admin: Neuer E-Mail-Tab mit SMTP-Konfiguration + Verbindungstest
- Admin: Lazy Quill-Initialisierung (nur sichtbare Locale wird geladen)
- Uebersetzungen: 17 neue Mail-Keys in allen 6 Sprachen

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Rhino
2026-03-02 07:30:37 +01:00
commit 2e24a40d68
9633 changed files with 1300799 additions and 0 deletions

View File

@@ -0,0 +1,86 @@
<x-layouts.installer :currentStep="3">
<h2 class="text-lg font-semibold text-gray-900 mb-4">App-Einstellungen</h2>
<p class="text-sm text-gray-600 mb-4">Gib den Namen deines Vereins und die Administrator-Zugangsdaten ein.</p>
<form method="POST" action="{{ route('install.app.store') }}">
@csrf
{{-- App settings --}}
<div class="space-y-4 mb-6">
<h3 class="text-sm font-semibold text-gray-700 uppercase tracking-wider">Verein</h3>
<div>
<label for="app_name" class="block text-sm font-medium text-gray-700 mb-1">Vereinsname / App-Name *</label>
<input type="text" name="app_name" id="app_name" value="{{ old('app_name', session('installer.app_name', '')) }}"
required maxlength="100" placeholder="z.B. SG Musterstadt Handball"
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('app_name') <p class="mt-1 text-xs text-red-600">{{ $message }}</p> @enderror
</div>
<div>
<label for="app_slogan" class="block text-sm font-medium text-gray-700 mb-1">Slogan</label>
<input type="text" name="app_slogan" id="app_slogan" value="{{ old('app_slogan', session('installer.app_slogan', '')) }}"
maxlength="255" placeholder="z.B. Gemeinsam stark — auf und neben dem Spielfeld"
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">
<p class="mt-1 text-xs text-gray-400">Optional. Wird auf der Login-Seite und im Footer angezeigt.</p>
@error('app_slogan') <p class="mt-1 text-xs text-red-600">{{ $message }}</p> @enderror
</div>
<div>
<label for="app_url" class="block text-sm font-medium text-gray-700 mb-1">App-URL *</label>
<input type="url" name="app_url" id="app_url" value="{{ old('app_url', session('installer.app_url', request()->getSchemeAndHttpHost())) }}"
required placeholder="https://handball.example.com"
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">
<p class="mt-1 text-xs text-gray-400">Die URL, unter der die App erreichbar ist.</p>
@error('app_url') <p class="mt-1 text-xs text-red-600">{{ $message }}</p> @enderror
</div>
</div>
{{-- Admin credentials --}}
<div class="space-y-4 border-t border-gray-200 pt-5">
<h3 class="text-sm font-semibold text-gray-700 uppercase tracking-wider">Administrator-Konto</h3>
<div>
<label for="admin_name" class="block text-sm font-medium text-gray-700 mb-1">Name *</label>
<input type="text" name="admin_name" id="admin_name" value="{{ old('admin_name', session('installer.admin_name', '')) }}"
required maxlength="255" placeholder="Vor- und Nachname"
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('admin_name') <p class="mt-1 text-xs text-red-600">{{ $message }}</p> @enderror
</div>
<div>
<label for="admin_email" class="block text-sm font-medium text-gray-700 mb-1">E-Mail *</label>
<input type="email" name="admin_email" id="admin_email" value="{{ old('admin_email', session('installer.admin_email', '')) }}"
required maxlength="255" placeholder="admin@example.com"
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('admin_email') <p class="mt-1 text-xs text-red-600">{{ $message }}</p> @enderror
</div>
<div>
<label for="admin_password" class="block text-sm font-medium text-gray-700 mb-1">Passwort *</label>
<input type="password" name="admin_password" id="admin_password"
required minlength="8" placeholder="Mindestens 8 Zeichen"
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('admin_password') <p class="mt-1 text-xs text-red-600">{{ $message }}</p> @enderror
</div>
<div>
<label for="admin_password_confirmation" class="block text-sm font-medium text-gray-700 mb-1">Passwort bestätigen *</label>
<input type="password" name="admin_password_confirmation" id="admin_password_confirmation"
required minlength="8" placeholder="Passwort wiederholen"
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 class="mt-6 flex justify-between items-center">
<a href="{{ route('install.database') }}"
class="px-4 py-2 text-sm text-gray-600 bg-gray-100 rounded-md hover:bg-gray-200 transition">
Zurück
</a>
<button type="submit"
class="px-5 py-2 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700 transition">
Weiter
</button>
</div>
</form>
</x-layouts.installer>

View File

@@ -0,0 +1,85 @@
<x-layouts.installer :currentStep="2">
<h2 class="text-lg font-semibold text-gray-900 mb-4">Datenbank einrichten</h2>
<p class="text-sm text-gray-600 mb-4">Wähle den Datenbanktyp und gib die Verbindungsdaten ein.</p>
<form method="POST" action="{{ route('install.database.store') }}" x-data="{ driver: @js(old('db_driver', $dbDriver)), submitting: false }" @submit="submitting = true">
@csrf
{{-- Driver selection --}}
<div class="mb-5">
<label class="block text-sm font-medium text-gray-700 mb-2">Datenbanktyp</label>
<div class="flex gap-4">
<label class="flex items-center gap-2 cursor-pointer">
<input type="radio" name="db_driver" value="sqlite" x-model="driver"
class="text-blue-600 focus:ring-blue-500">
<span class="text-sm text-gray-800">SQLite</span>
<span class="text-xs text-gray-400">(Empfohlen für Einzelbetrieb)</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="radio" name="db_driver" value="mysql" x-model="driver"
class="text-blue-600 focus:ring-blue-500">
<span class="text-sm text-gray-800">MySQL</span>
</label>
</div>
</div>
{{-- SQLite info --}}
<div x-show="driver === 'sqlite'" x-cloak class="mb-4 p-3 bg-blue-50 border border-blue-200 rounded-md text-sm text-blue-700">
SQLite speichert alle Daten in einer einzelnen Datei. Ideal für kleine bis mittlere Installationen.
Es werden keine weiteren Angaben benötigt.
</div>
{{-- MySQL fields --}}
<div x-show="driver === 'mysql'" x-cloak class="space-y-4 mb-4">
<div class="grid grid-cols-2 gap-4">
<div>
<label for="db_host" class="block text-sm font-medium text-gray-700 mb-1">Host</label>
<input type="text" name="db_host" id="db_host" value="{{ old('db_host', '127.0.0.1') }}"
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('db_host') <p class="mt-1 text-xs text-red-600">{{ $message }}</p> @enderror
</div>
<div>
<label for="db_port" class="block text-sm font-medium text-gray-700 mb-1">Port</label>
<input type="number" name="db_port" id="db_port" value="{{ old('db_port', '3306') }}"
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('db_port') <p class="mt-1 text-xs text-red-600">{{ $message }}</p> @enderror
</div>
</div>
<div>
<label for="db_database" class="block text-sm font-medium text-gray-700 mb-1">Datenbankname</label>
<input type="text" name="db_database" id="db_database" value="{{ old('db_database') }}"
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('db_database') <p class="mt-1 text-xs text-red-600">{{ $message }}</p> @enderror
</div>
<div>
<label for="db_username" class="block text-sm font-medium text-gray-700 mb-1">Benutzername</label>
<input type="text" name="db_username" id="db_username" value="{{ old('db_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('db_username') <p class="mt-1 text-xs text-red-600">{{ $message }}</p> @enderror
</div>
<div>
<label for="db_password" class="block text-sm font-medium text-gray-700 mb-1">Passwort</label>
<input type="password" name="db_password" id="db_password" value="{{ old('db_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('db_password') <p class="mt-1 text-xs text-red-600">{{ $message }}</p> @enderror
</div>
</div>
<div class="mt-6 flex justify-between items-center">
<a href="{{ route('install.requirements') }}"
class="px-4 py-2 text-sm text-gray-600 bg-gray-100 rounded-md hover:bg-gray-200 transition">
Zurück
</a>
<button type="submit" :disabled="submitting"
class="px-5 py-2 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700 transition disabled:opacity-50 disabled:cursor-wait flex items-center gap-2">
<template x-if="submitting">
<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="submitting ? 'Migrationen werden ausgeführt...' : 'Datenbank einrichten'"></span>
</button>
</div>
</form>
</x-layouts.installer>

View File

@@ -0,0 +1,154 @@
<x-layouts.installer :currentStep="5">
@if ($installed ?? false)
{{-- ══════ SUCCESS PAGE ══════ --}}
<div class="text-center mb-4">
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-green-100 mb-4">
<svg class="w-8 h-8 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
</svg>
</div>
<h2 class="text-lg font-semibold text-gray-900">Installation erfolgreich!</h2>
<p class="text-sm text-gray-600 mt-1">Die Handball WebApp ist einsatzbereit.</p>
</div>
{{-- Credentials table --}}
<div class="bg-gray-50 rounded-md border border-gray-200 p-4 mb-4">
<h3 class="text-sm font-semibold text-gray-700 mb-3">Zugangsdaten</h3>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-gray-200">
<th class="text-left py-1.5 px-2 font-medium text-gray-600">Rolle</th>
<th class="text-left py-1.5 px-2 font-medium text-gray-600">E-Mail</th>
<th class="text-left py-1.5 px-2 font-medium text-gray-600">Passwort</th>
</tr>
</thead>
<tbody>
<tr class="border-b border-gray-100 bg-blue-50">
<td class="py-1.5 px-2 font-medium text-gray-800">Administrator</td>
<td class="py-1.5 px-2 text-gray-700">{{ $adminEmail }}</td>
<td class="py-1.5 px-2 text-gray-500 italic">Dein gewähltes Passwort</td>
</tr>
@if ($installDemo ?? false)
<tr class="border-b border-gray-100">
<td class="py-1.5 px-2 font-medium text-gray-800">Trainer</td>
<td class="py-1.5 px-2 text-gray-700 font-mono text-xs">trainer@handball.local</td>
<td class="py-1.5 px-2 text-gray-700 font-mono text-xs">trainer1234</td>
</tr>
<tr class="border-b border-gray-100">
<td class="py-1.5 px-2 font-medium text-gray-800">Elternvertretung</td>
<td class="py-1.5 px-2 text-gray-700 font-mono text-xs">elternvertretung@handball.local</td>
<td class="py-1.5 px-2 text-gray-700 font-mono text-xs">eltern1234</td>
</tr>
<tr class="border-b border-gray-100">
<td class="py-1.5 px-2 text-gray-800">Eltern (Beispiel)</td>
<td class="py-1.5 px-2 text-gray-700 font-mono text-xs">mary.parker@handball.local</td>
<td class="py-1.5 px-2 text-gray-700 font-mono text-xs">eltern1234</td>
</tr>
<tr>
<td class="py-1.5 px-2 text-gray-800">Eltern (Beispiel)</td>
<td class="py-1.5 px-2 text-gray-700 font-mono text-xs">tony.stark@handball.local</td>
<td class="py-1.5 px-2 text-gray-700 font-mono text-xs">eltern1234</td>
</tr>
@endif
</tbody>
</table>
</div>
</div>
@if ($installDemo ?? false)
<p class="text-xs text-gray-500 mb-4">
Die Beispieldaten umfassen ein Demo-Team mit 27 Spielern, 35 Eltern, 8 Events und weitere
Testdaten. Alle Demo-Eltern nutzen das Passwort <code class="bg-gray-100 px-1 rounded">eltern1234</code>.
</p>
@endif
<div class="text-center">
<a href="/login"
class="inline-block px-6 py-2.5 text-sm font-medium text-white bg-green-600 rounded-md hover:bg-green-700 transition">
Zum Login
</a>
</div>
@else
{{-- ══════ FINALIZE FORM ══════ --}}
<h2 class="text-lg font-semibold text-gray-900 mb-4">Installation abschließen</h2>
{{-- Summary --}}
<div class="bg-gray-50 rounded-md border border-gray-200 p-4 mb-5">
<h3 class="text-sm font-semibold text-gray-700 mb-2">Zusammenfassung</h3>
<dl class="grid grid-cols-2 gap-y-2 gap-x-4 text-sm">
<dt class="text-gray-500">App-Name:</dt>
<dd class="text-gray-800 font-medium">{{ $appName }}</dd>
<dt class="text-gray-500">Administrator:</dt>
<dd class="text-gray-800">{{ $adminName }} ({{ $adminEmail }})</dd>
<dt class="text-gray-500">Datenbank:</dt>
<dd class="text-gray-800">{{ $dbDriver === 'mysql' ? 'MySQL' : 'SQLite' }}</dd>
</dl>
</div>
<form method="POST" action="{{ route('install.finalize.store') }}" x-data="{ submitting: false }" @submit="submitting = true">
@csrf
{{-- Demo data checkbox --}}
<div class="mb-5 p-4 bg-blue-50 border border-blue-200 rounded-md">
<label class="flex items-start gap-3 cursor-pointer">
<input type="checkbox" name="install_demo" value="1" checked
class="mt-0.5 rounded border-gray-300 text-blue-600 focus:ring-blue-500">
<div>
<span class="text-sm font-medium text-gray-800">Beispieldaten installieren</span>
<p class="text-xs text-gray-600 mt-1">
Erstellt ein Demo-Team mit 27 Spielern, 35 Eltern-Accounts, 8 Events
(Training, Heimspiel, Auswärtsspiel, Turnier, Besprechung, etc.),
Catering-Einträge, Zeitnehmer, Kommentare und weitere Testdaten.
Ideal, um alle Funktionen der App kennenzulernen.
</p>
</div>
</label>
</div>
{{-- Registration opt-in --}}
<div class="mb-5 p-4 bg-green-50 border border-green-200 rounded-md">
<label class="flex items-start gap-3 cursor-pointer">
<input type="checkbox" name="register_installation" value="1" checked
class="mt-0.5 rounded border-gray-300 text-green-600 focus:ring-green-500">
<div>
<span class="text-sm font-medium text-gray-800">Installation registrieren</span>
<p class="text-xs text-gray-600 mt-1">
Registriert diese Installation beim Entwickler. Es werden nur technische Daten
übermittelt (App-Name, URL, PHP-Version, App-Version). Keine persönlichen Daten.
Ermöglicht Update-Benachrichtigungen und Support-Anfragen.
</p>
</div>
</label>
</div>
{{-- License key (optional) --}}
<div class="mb-5">
<label for="license_key" class="block text-sm font-medium text-gray-700 mb-1">
Lizenzschlüssel <span class="text-gray-400 font-normal">(optional)</span>
</label>
<input type="text" name="license_key" id="license_key"
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">
<p class="text-xs text-gray-400 mt-1">Falls vorhanden. Kann auch später in den Einstellungen eingetragen werden.</p>
</div>
<div class="flex justify-between items-center">
<a href="{{ route('install.mail') }}"
class="px-4 py-2 text-sm text-gray-600 bg-gray-100 rounded-md hover:bg-gray-200 transition">
Zurück
</a>
<button type="submit" :disabled="submitting"
class="px-5 py-2 text-sm font-medium text-white bg-green-600 rounded-md hover:bg-green-700 transition disabled:opacity-50 disabled:cursor-wait flex items-center gap-2">
<template x-if="submitting">
<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="submitting ? 'Installation läuft...' : 'Installation abschließen'"></span>
</button>
</div>
</form>
@endif
</x-layouts.installer>

View File

@@ -0,0 +1,237 @@
<x-layouts.installer :currentStep="4">
<h2 class="text-lg font-semibold text-gray-900 mb-2">E-Mail-Konfiguration</h2>
<p class="text-sm text-gray-600 mb-5">
Damit Funktionen wie "Passwort vergessen" funktionieren, muss ein E-Mail-Versand konfiguriert werden.
Du kannst dies auch spaeter in den Admin-Einstellungen aendern.
</p>
<form method="POST" action="{{ route('install.mail.store') }}"
x-data="{
mailMode: '{{ old('mail_mode', 'smtp') }}',
editor: null,
testing: false,
testResult: false,
testSuccess: false,
testMessage: '',
syncEditor() {
if (this.editor) {
document.getElementById('input-pw-reset-de').value = this.editor.root.innerHTML;
}
},
async testSmtp() {
this.testing = true;
this.testResult = false;
try {
const res = await fetch('{{ route("install.test-mail") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json',
},
body: JSON.stringify({
mail_host: document.getElementById('mail_host').value,
mail_port: document.getElementById('mail_port').value,
mail_username: document.getElementById('mail_username').value,
mail_password: document.getElementById('mail_password').value,
mail_encryption: document.getElementById('mail_encryption').value,
}),
});
const data = await res.json();
this.testSuccess = data.success;
this.testMessage = data.message;
} catch (e) {
this.testSuccess = false;
this.testMessage = 'Netzwerkfehler: ' + e.message;
}
this.testing = false;
this.testResult = true;
}
}"
@submit="syncEditor()">
@csrf
{{-- Mail-Modus --}}
<div class="mb-5">
<label class="block text-sm font-semibold text-gray-700 mb-3">Versandmethode</label>
<div class="space-y-2">
<label class="flex items-start gap-3 p-3 border rounded-md cursor-pointer transition"
:class="mailMode === 'smtp' ? 'border-blue-400 bg-blue-50' : 'border-gray-200 hover:bg-gray-50'">
<input type="radio" name="mail_mode" value="smtp" x-model="mailMode"
class="mt-0.5 text-blue-600 focus:ring-blue-500">
<div>
<span class="text-sm font-medium text-gray-800">SMTP-Server</span>
<p class="text-xs text-gray-500 mt-0.5">E-Mails werden ueber einen SMTP-Server versendet (empfohlen).</p>
</div>
</label>
<label class="flex items-start gap-3 p-3 border rounded-md cursor-pointer transition"
:class="mailMode === 'log' ? 'border-blue-400 bg-blue-50' : 'border-gray-200 hover:bg-gray-50'">
<input type="radio" name="mail_mode" value="log" x-model="mailMode"
class="mt-0.5 text-blue-600 focus:ring-blue-500">
<div>
<span class="text-sm font-medium text-gray-800">Kein Versand (Log)</span>
<p class="text-xs text-gray-500 mt-0.5">E-Mails werden nur ins Log geschrieben. Passwort-Reset funktioniert dann nicht.</p>
</div>
</label>
</div>
</div>
{{-- SMTP-Felder --}}
<div x-show="mailMode === 'smtp'" x-cloak class="space-y-4 mb-5 p-4 bg-gray-50 border border-gray-200 rounded-md">
<div class="grid grid-cols-2 gap-4">
<div>
<label for="mail_host" class="block text-sm font-medium text-gray-700 mb-1">SMTP-Host</label>
<input type="text" name="mail_host" id="mail_host" value="{{ old('mail_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 for="mail_port" class="block text-sm font-medium text-gray-700 mb-1">Port</label>
<input type="number" name="mail_port" id="mail_port" value="{{ old('mail_port', '587') }}"
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-2 gap-4">
<div>
<label for="mail_username" class="block text-sm font-medium text-gray-700 mb-1">Benutzername</label>
<input type="text" name="mail_username" id="mail_username" value="{{ old('mail_username') }}"
placeholder="z.B. noreply@deinverein.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_username') <p class="text-xs text-red-600 mt-1">{{ $message }}</p> @enderror
</div>
<div>
<label for="mail_password" class="block text-sm font-medium text-gray-700 mb-1">Passwort</label>
<input type="password" name="mail_password" id="mail_password" value="{{ old('mail_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-2 gap-4">
<div>
<label for="mail_from_address" class="block text-sm font-medium text-gray-700 mb-1">Absender-Adresse</label>
<input type="email" name="mail_from_address" id="mail_from_address" value="{{ old('mail_from_address') }}"
placeholder="z.B. noreply@deinverein.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_from_address') <p class="text-xs text-red-600 mt-1">{{ $message }}</p> @enderror
</div>
<div>
<label for="mail_from_name" class="block text-sm font-medium text-gray-700 mb-1">Absender-Name <span class="text-gray-400 font-normal">(optional)</span></label>
<input type="text" name="mail_from_name" id="mail_from_name" value="{{ old('mail_from_name') }}"
placeholder="z.B. SG Woelfe Handball"
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 for="mail_encryption" class="block text-sm font-medium text-gray-700 mb-1">Verschluesselung</label>
<select name="mail_encryption" id="mail_encryption"
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">
<option value="tls" {{ old('mail_encryption', 'tls') === 'tls' ? 'selected' : '' }}>TLS (Port 587, empfohlen)</option>
<option value="ssl" {{ old('mail_encryption') === 'ssl' ? 'selected' : '' }}>SSL (Port 465)</option>
<option value="none" {{ old('mail_encryption') === 'none' ? 'selected' : '' }}>Keine</option>
</select>
</div>
{{-- SMTP-Test --}}
<div class="pt-2 border-t border-gray-200">
<button type="button" @click="testSmtp()"
:disabled="testing"
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="testing">
<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="testing ? 'Teste Verbindung...' : 'Verbindung testen'"></span>
</button>
<p x-show="testResult" x-cloak x-text="testMessage"
:class="testSuccess ? 'text-green-600' : 'text-red-600'"
class="text-sm mt-2"></p>
</div>
</div>
{{-- Passwort-Reset E-Mail Template --}}
<div class="mb-5">
<h3 class="text-sm font-semibold text-gray-700 mb-1">Passwort-Reset E-Mail (Deutsch)</h3>
<p class="text-xs text-gray-500 mb-3">
Dieser Text wird versendet, wenn ein Benutzer sein Passwort zuruecksetzen moechte.
Platzhalter: <code class="bg-gray-100 px-1 rounded">{name}</code>,
<code class="bg-gray-100 px-1 rounded">{app_name}</code>.
Der Reset-Link wird automatisch als Button angefuegt.
</p>
<div id="editor-pw-reset" class="bg-white border border-gray-300 rounded-md" style="min-height: 150px;">{!! $defaultPwResetDe !!}</div>
<input type="hidden" name="password_reset_email_de" id="input-pw-reset-de" value="{{ old('password_reset_email_de', $defaultPwResetDe) }}">
<p class="text-xs text-gray-400 mt-2">Texte fuer weitere Sprachen werden automatisch erstellt und koennen spaeter in den Einstellungen angepasst werden.</p>
</div>
@if ($errors->any())
<div class="mb-4 p-3 bg-red-50 border border-red-200 rounded-md">
<ul class="text-sm text-red-700 list-disc list-inside">
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<div class="flex justify-between items-center">
<a href="{{ route('install.app') }}"
class="px-4 py-2 text-sm text-gray-600 bg-gray-100 rounded-md hover:bg-gray-200 transition">
Zurueck
</a>
<div class="flex items-center gap-3">
<button type="submit"
class="px-5 py-2 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700 transition">
Weiter
</button>
</div>
</div>
</form>
{{-- Ueberspringen-Link (separates Formular) --}}
<div class="text-center mt-3">
<form method="POST" action="{{ route('install.mail.store') }}" class="inline">
@csrf
<input type="hidden" name="mail_mode" value="log">
<button type="submit" class="text-xs text-gray-400 hover:text-gray-600 underline transition">
Schritt ueberspringen (kein E-Mail-Versand)
</button>
</form>
</div>
{{-- Quill Editor --}}
<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>
#editor-pw-reset .ql-editor { min-height: 120px; }
.ql-toolbar.ql-snow { border-radius: 0.375rem 0.375rem 0 0; border-color: #d1d5db; }
.ql-container.ql-snow { border-radius: 0 0 0.375rem 0.375rem; border-color: #d1d5db; }
</style>
<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>
document.addEventListener('DOMContentLoaded', function() {
const editorEl = document.getElementById('editor-pw-reset');
if (!editorEl) return;
const quill = new Quill('#editor-pw-reset', {
theme: 'snow',
modules: {
toolbar: [
[{ 'header': [2, 3, false] }],
['bold', 'italic', 'underline'],
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
['link'],
['clean']
]
}
});
// Speichere Referenz fuer Alpine.js syncEditor()
const component = document.querySelector('[x-data]').__x.$data;
component.editor = quill;
});
</script>
</x-layouts.installer>

View File

@@ -0,0 +1,79 @@
<x-layouts.installer :currentStep="1">
@if (! session('setup_token_hash'))
{{-- Token-Eingabe: User muss zuerst das Setup-Token eingeben --}}
<h2 class="text-lg font-semibold text-gray-900 mb-4">Setup-Token</h2>
<div class="bg-amber-50 border border-amber-200 rounded-lg p-4 mb-6">
<p class="text-sm text-amber-700 mb-3">
Zum Schutz der Installation wird ein Setup-Token benoetigt.
Du findest es in der Datei <code class="bg-amber-100 px-1 rounded font-mono text-xs">storage/setup-token</code>
auf dem Server (per FTP oder Dateimanager).
</p>
<form method="GET" action="{{ route('install.requirements') }}" class="flex gap-2">
<input type="text" name="token"
class="flex-1 px-3 py-2 text-sm border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500"
placeholder="Token eingeben..." required autofocus>
<button type="submit"
class="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700 transition">
Bestaetigen
</button>
</form>
</div>
@else
<h2 class="text-lg font-semibold text-gray-900 mb-4">Systemvoraussetzungen</h2>
<p class="text-sm text-gray-600 mb-4">Bitte stelle sicher, dass alle Voraussetzungen erfüllt sind, bevor du fortfährst.</p>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-gray-200">
<th class="text-left py-2 px-2 font-medium text-gray-700">Prüfung</th>
<th class="text-left py-2 px-2 font-medium text-gray-700">Status</th>
<th class="text-center py-2 px-1 font-medium text-gray-700 w-10">OK</th>
</tr>
</thead>
<tbody>
@foreach ($checks as $check)
<tr class="border-b border-gray-100 {{ $check['passed'] ? '' : ($check['required'] ? 'bg-red-50' : 'bg-yellow-50') }}">
<td class="py-2 px-2 text-gray-800">
{{ $check['name'] }}
@if (! $check['required'])
<span class="text-xs text-gray-400">(optional)</span>
@endif
</td>
<td class="py-2 px-2 text-gray-500">{{ $check['current'] }}</td>
<td class="py-2 px-1 text-center">
@if ($check['passed'])
<svg class="w-5 h-5 text-green-500 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
@elseif ($check['required'])
<svg class="w-5 h-5 text-red-500 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
@else
<svg class="w-5 h-5 text-yellow-500 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
@endif
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
<div class="mt-6 flex justify-between items-center">
<a href="{{ route('install.requirements') }}"
class="px-4 py-2 text-sm text-gray-600 bg-gray-100 rounded-md hover:bg-gray-200 transition">
Erneut prüfen
</a>
@if ($allPassed)
<a href="{{ route('install.database') }}"
class="px-5 py-2 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700 transition">
Weiter
</a>
@else
<span class="px-5 py-2 text-sm font-medium text-white bg-gray-300 rounded-md cursor-not-allowed">
Weiter
</span>
@endif
</div>
@endif
</x-layouts.installer>