- 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>
256 lines
16 KiB
PHP
256 lines
16 KiB
PHP
<x-layouts.admin :title="__('admin.edit_user')">
|
|
<h1 class="text-2xl font-bold mb-6">{{ __('admin.edit_user') }}: {{ $user->name }}</h1>
|
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
{{-- Benutzerdaten --}}
|
|
<div class="bg-white rounded-lg shadow p-6">
|
|
<h2 class="font-semibold mb-4">{{ __('admin.user_data') }}</h2>
|
|
<form method="POST" action="{{ route('admin.users.update', $user) }}" enctype="multipart/form-data">
|
|
@csrf
|
|
@method('PUT')
|
|
|
|
{{-- Profilbild --}}
|
|
<div class="mb-5" x-data="{ preview: null }">
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">{{ __('admin.profile_picture') }}</label>
|
|
<div class="flex items-center gap-4">
|
|
<div class="relative">
|
|
@if ($user->getAvatarUrl())
|
|
<img src="{{ $user->getAvatarUrl() }}" alt="{{ $user->name }}" class="w-14 h-14 rounded-full object-cover border-2 border-gray-200" x-show="!preview">
|
|
@else
|
|
<div class="w-14 h-14 rounded-full bg-blue-100 flex items-center justify-center text-blue-600 font-semibold border-2 border-gray-200" x-show="!preview">
|
|
{{ $user->getInitials() }}
|
|
</div>
|
|
@endif
|
|
<img :src="preview" x-show="preview" class="w-14 h-14 rounded-full object-cover border-2 border-blue-400" x-cloak>
|
|
</div>
|
|
<div class="flex flex-col gap-1">
|
|
<label class="cursor-pointer bg-gray-100 text-gray-700 px-3 py-1.5 rounded-md text-xs hover:bg-gray-200 inline-block">
|
|
{{ __('admin.upload_picture') }}
|
|
<input type="file" name="profile_picture" accept="image/jpeg,image/png,image/gif,image/webp" class="hidden"
|
|
@change="if ($event.target.files[0]) { preview = URL.createObjectURL($event.target.files[0]) }">
|
|
</label>
|
|
<span class="text-xs text-gray-400">{{ __('admin.max_picture_size') }}</span>
|
|
@error('profile_picture')
|
|
<p class="text-red-600 text-xs">{{ $message }}</p>
|
|
@enderror
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-4">
|
|
<label for="name" class="block text-sm font-medium text-gray-700 mb-1">{{ __('ui.name') }} *</label>
|
|
<input type="text" name="name" id="name" value="{{ old('name', $user->name) }}" required
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md @error('name') border-red-500 @enderror">
|
|
@error('name')<p class="mt-1 text-sm text-red-600">{{ $message }}</p>@enderror
|
|
</div>
|
|
|
|
<div class="mb-4">
|
|
<label for="email" class="block text-sm font-medium text-gray-700 mb-1">{{ __('ui.email') }} *</label>
|
|
<input type="email" name="email" id="email" value="{{ old('email', $user->email) }}" required
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md @error('email') border-red-500 @enderror">
|
|
@error('email')<p class="mt-1 text-sm text-red-600">{{ $message }}</p>@enderror
|
|
</div>
|
|
|
|
<div class="mb-4">
|
|
<label for="phone" class="block text-sm font-medium text-gray-700 mb-1">{{ __('admin.phone') }}</label>
|
|
<input type="tel" name="phone" id="phone" value="{{ old('phone', $user->phone) }}"
|
|
placeholder="+49..."
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md @error('phone') border-red-500 @enderror">
|
|
@error('phone')<p class="mt-1 text-sm text-red-600">{{ $message }}</p>@enderror
|
|
</div>
|
|
|
|
<div class="mb-4">
|
|
<label for="role" class="block text-sm font-medium text-gray-700 mb-1">{{ __('ui.role') }}</label>
|
|
<select name="role" id="role" class="w-full px-3 py-2 border border-gray-300 rounded-md {{ $user->id === auth()->id() ? 'bg-gray-100 text-gray-500' : '' }}" {{ $user->id === auth()->id() ? 'disabled' : '' }}>
|
|
<option value="user" {{ old('role', $user->role->value) === 'user' ? 'selected' : '' }}>{{ __('ui.enums.user_role.user') }}</option>
|
|
<option value="parent_rep" {{ old('role', $user->role->value) === 'parent_rep' ? 'selected' : '' }}>{{ __('ui.enums.user_role.parent_rep') }}</option>
|
|
<option value="coach" {{ old('role', $user->role->value) === 'coach' ? 'selected' : '' }}>{{ __('ui.enums.user_role.coach') }}</option>
|
|
@if (auth()->user()->isAdmin())
|
|
<option value="admin" {{ old('role', $user->role->value) === 'admin' ? 'selected' : '' }}>{{ __('ui.enums.user_role.admin') }}</option>
|
|
@endif
|
|
</select>
|
|
@if ($user->id === auth()->id())
|
|
<p class="mt-1 text-xs text-gray-500">{{ __('admin.cannot_edit_own_role') }}</p>
|
|
@endif
|
|
</div>
|
|
|
|
<button type="submit" class="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 text-sm font-medium">{{ __('ui.save') }}</button>
|
|
</form>
|
|
|
|
@if ($user->getAvatarUrl())
|
|
<div class="mt-3 pt-3 border-t">
|
|
<form method="POST" action="{{ route('admin.users.remove-picture', $user) }}" class="inline" onsubmit="return confirm(@js(__('admin.confirm_delete_file')))">
|
|
@csrf
|
|
@method('DELETE')
|
|
<button type="submit" class="text-xs text-red-500 hover:text-red-700">{{ __('admin.remove_picture') }}</button>
|
|
</form>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
{{-- Passwort-Reset --}}
|
|
<div class="bg-white rounded-lg shadow p-6">
|
|
<h2 class="font-semibold mb-4">{{ __('admin.reset_password') }}</h2>
|
|
|
|
@if (session('new_password'))
|
|
<div class="bg-yellow-50 border border-yellow-300 rounded-md p-4 mb-4">
|
|
<p class="text-sm font-medium text-yellow-800 mb-2">{{ __('admin.new_password_label') }}</p>
|
|
<div class="flex items-center gap-2" x-data="{ copied: false }">
|
|
<code class="bg-white border border-yellow-200 rounded px-3 py-2 text-sm font-mono flex-1 select-all">{{ session('new_password') }}</code>
|
|
<button @click="navigator.clipboard.writeText(@js(session('new_password'))); copied = true; setTimeout(() => copied = false, 2000)"
|
|
class="px-3 py-2 text-xs bg-yellow-600 text-white rounded-md hover:bg-yellow-700">
|
|
<span x-show="!copied">{{ __('admin.copy') }}</span>
|
|
<span x-show="copied" x-cloak>{{ __('admin.copied') }}</span>
|
|
</button>
|
|
</div>
|
|
<p class="text-xs text-yellow-700 mt-2">{{ __('admin.password_only_visible_now') }}</p>
|
|
</div>
|
|
@endif
|
|
|
|
@if ($user->id === auth()->id())
|
|
<p class="text-sm text-gray-500">{{ __('admin.cannot_reset_own_password') }}</p>
|
|
@else
|
|
<p class="text-sm text-gray-600 mb-4">{{ __('admin.reset_password_hint') }}</p>
|
|
<form method="POST" action="{{ route('admin.users.reset-password', $user) }}" onsubmit="return confirm(@js(__('admin.reset_password_confirm')))">
|
|
@csrf
|
|
@method('PUT')
|
|
<button type="submit" class="bg-red-600 text-white px-4 py-2 rounded-md hover:bg-red-700 text-sm font-medium">
|
|
{{ __('admin.reset_password') }}
|
|
</button>
|
|
</form>
|
|
@endif
|
|
|
|
{{-- Zusatzinfos --}}
|
|
<div class="mt-6 pt-4 border-t text-xs text-gray-500 space-y-1">
|
|
<p>{{ __('admin.last_login') }}: {{ $user->last_login_at ? $user->last_login_at->diffForHumans() : __('admin.never') }}</p>
|
|
<p>{{ __('admin.registered_at') }}: {{ $user->created_at->translatedFormat(__('ui.date_format')) }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- DSGVO-Einverständniserklärung --}}
|
|
<div class="mt-6 bg-white rounded-lg shadow p-6" x-data="{ dsgvoModal: false }">
|
|
<h2 class="font-semibold mb-4">{{ __('admin.dsgvo_title') }}</h2>
|
|
|
|
@if ($user->hasDsgvoConsent())
|
|
@php
|
|
$ext = strtolower(pathinfo($user->dsgvo_consent_file, PATHINFO_EXTENSION));
|
|
$dsgvoIsPdf = $ext === 'pdf';
|
|
$dsgvoIsImage = in_array($ext, ['jpg', 'jpeg', 'png', 'gif', 'webp']);
|
|
@endphp
|
|
|
|
<div class="flex items-center gap-3 mb-4">
|
|
<button type="button" @click="dsgvoModal = true"
|
|
class="text-sm text-blue-600 hover:text-blue-800 font-medium cursor-pointer">
|
|
{{ __('admin.dsgvo_view_document') }}
|
|
</button>
|
|
</div>
|
|
|
|
{{-- DSGVO-Vorschau-Modal --}}
|
|
<div x-show="dsgvoModal" x-cloak @keydown.escape.window="dsgvoModal = false" class="fixed inset-0 z-50 flex items-center justify-center p-4">
|
|
<div x-show="dsgvoModal"
|
|
x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100"
|
|
x-transition:leave="transition ease-in duration-150" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0"
|
|
@click="dsgvoModal = false" class="fixed inset-0 bg-black/60"></div>
|
|
<div x-show="dsgvoModal"
|
|
x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100"
|
|
x-transition:leave="transition ease-in duration-150" x-transition:leave-start="opacity-100 scale-100" x-transition:leave-end="opacity-0 scale-95"
|
|
@click.outside="dsgvoModal = false"
|
|
class="relative bg-white rounded-xl shadow-2xl flex flex-col overflow-hidden {{ $dsgvoIsPdf ? 'w-full max-w-3xl max-h-[92vh]' : 'w-full max-w-lg max-h-[90vh]' }}">
|
|
<div class="flex items-center justify-between px-5 py-3 border-b">
|
|
<h3 class="font-semibold text-gray-900">{{ __('admin.dsgvo_title') }} — {{ $user->name }}</h3>
|
|
<button @click="dsgvoModal = false" class="text-gray-400 hover:text-gray-600">
|
|
<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="M6 18L18 6M6 6l12 12"/></svg>
|
|
</button>
|
|
</div>
|
|
<div class="flex-1 overflow-y-auto {{ $dsgvoIsPdf ? 'p-0' : 'p-5' }}">
|
|
@if ($dsgvoIsImage)
|
|
<div class="flex justify-center bg-gray-50 rounded-lg p-2">
|
|
<img src="{{ route('admin.users.view-dsgvo-consent', $user) }}" alt="{{ __('admin.dsgvo_title') }}" class="max-w-full max-h-[70vh] rounded object-contain">
|
|
</div>
|
|
@elseif ($dsgvoIsPdf)
|
|
<iframe src="{{ route('admin.users.view-dsgvo-consent', $user) }}" class="w-full border-0" style="height: 75vh;"></iframe>
|
|
@endif
|
|
</div>
|
|
<div class="px-5 py-3 border-t bg-gray-50 flex justify-end">
|
|
<button @click="dsgvoModal = false" class="text-sm text-gray-500 hover:text-gray-700">{{ __('ui.close') }}</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<form method="POST" action="{{ route('admin.users.dsgvo-toggle', $user) }}">
|
|
@csrf
|
|
@method('PUT')
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<p class="text-sm font-medium text-gray-900">{{ __('admin.dsgvo_consent_label') }}</p>
|
|
<p class="text-xs text-gray-500">
|
|
@if ($user->isDsgvoConfirmed())
|
|
{{ __('admin.dsgvo_confirmed_info', [
|
|
'name' => $user->dsgvoAcceptedBy?->name ?? '—',
|
|
'date' => $user->dsgvo_accepted_at->translatedFormat(__('ui.date_format_short'))
|
|
]) }}
|
|
@else
|
|
{{ __('admin.dsgvo_not_confirmed') }}
|
|
@endif
|
|
</p>
|
|
</div>
|
|
<button type="submit" class="px-4 py-2 rounded-md text-sm font-medium
|
|
{{ $user->isDsgvoConfirmed()
|
|
? 'bg-yellow-500 text-white hover:bg-yellow-600'
|
|
: 'bg-green-600 text-white hover:bg-green-700' }}">
|
|
{{ $user->isDsgvoConfirmed() ? __('admin.dsgvo_revoke') : __('admin.dsgvo_confirm') }}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
@else
|
|
<p class="text-sm text-gray-500">{{ __('admin.dsgvo_no_document') }}</p>
|
|
@endif
|
|
</div>
|
|
|
|
{{-- Benutzer deaktivieren / löschen --}}
|
|
@if ($user->id !== auth()->id())
|
|
<div class="mt-6 bg-white rounded-lg shadow p-6 border border-red-200">
|
|
<h2 class="font-semibold text-red-700 mb-2">{{ __('admin.danger_zone') }}</h2>
|
|
|
|
{{-- Deaktivieren / Aktivieren --}}
|
|
<div class="flex items-center justify-between py-3">
|
|
<div>
|
|
<p class="text-sm font-medium text-gray-900">{{ __('admin.user_status_label') }}</p>
|
|
<p class="text-xs text-gray-500">
|
|
{{ $user->is_active ? __('admin.deactivate_user_hint') : __('admin.activate_user_hint') }}
|
|
</p>
|
|
</div>
|
|
<form method="POST" action="{{ route('admin.users.toggle-active', $user) }}">
|
|
@csrf
|
|
@method('PUT')
|
|
<button type="submit" class="px-4 py-2 rounded-md text-sm font-medium {{ $user->is_active ? 'bg-yellow-500 text-white hover:bg-yellow-600' : 'bg-green-600 text-white hover:bg-green-700' }}">
|
|
{{ $user->is_active ? __('admin.deactivate') : __('admin.activate') }}
|
|
</button>
|
|
</form>
|
|
</div>
|
|
|
|
{{-- Löschen --}}
|
|
@if ($user->id !== 1)
|
|
<div class="flex items-center justify-between py-3 border-t border-red-100">
|
|
<div>
|
|
<p class="text-sm font-medium text-gray-900">{{ __('admin.delete_user') }}</p>
|
|
<p class="text-xs text-gray-500">{{ __('admin.delete_user_hint') }}</p>
|
|
</div>
|
|
<form method="POST" action="{{ route('admin.users.destroy', $user) }}" onsubmit="return confirm(@js(__('admin.confirm_delete_user')))">
|
|
@csrf
|
|
@method('DELETE')
|
|
<button type="submit" class="bg-red-600 text-white px-4 py-2 rounded-md hover:bg-red-700 text-sm font-medium">
|
|
{{ __('admin.delete') }}
|
|
</button>
|
|
</form>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
@endif
|
|
|
|
<div class="mt-4">
|
|
<a href="{{ route('admin.users.index') }}" class="text-sm text-gray-600 hover:underline">{{ __('admin.back_to_list') }}</a>
|
|
</div>
|
|
</x-layouts.admin>
|