Files
WebAPP/resources/views/profile/edit.blade.php
Rhino 2e24a40d68 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>
2026-03-02 07:30:37 +01:00

262 lines
15 KiB
PHP
Executable File

<x-layouts.app :title="__('profile.title')">
<h1 class="text-2xl font-bold mb-6">{{ __('profile.title') }}</h1>
<div class="bg-white rounded-lg shadow p-6 mb-6">
<form method="POST" action="{{ route('profile.update') }}" enctype="multipart/form-data">
@csrf
@method('PUT')
{{-- Profilbild --}}
<div class="mb-6" x-data="{ preview: null }">
<label class="block text-sm font-medium text-gray-700 mb-2">{{ __('profile.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-16 h-16 rounded-full object-cover border-2 border-gray-200" x-show="!preview">
@else
<div class="w-16 h-16 rounded-full bg-blue-100 flex items-center justify-center text-blue-600 font-semibold text-lg border-2 border-gray-200" x-show="!preview">
{{ $user->getInitials() }}
</div>
@endif
<img :src="preview" x-show="preview" class="w-16 h-16 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-sm hover:bg-gray-200 inline-block">
{{ __('profile.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">{{ __('profile.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">{{ __('profile.name_label') }}</label>
<input type="text" name="name" id="name" value="{{ old('name', $user->name) }}" required
class="w-full rounded border-gray-300 text-sm">
@error('name')
<p class="text-red-600 text-xs mt-1">{{ $message }}</p>
@enderror
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('profile.email_label') }}</label>
<p class="text-sm text-gray-600 bg-gray-50 px-3 py-2 rounded">{{ $user->email }}</p>
<p class="text-xs text-gray-400 mt-1">{{ __('profile.email_readonly') }}</p>
</div>
<div class="mb-4">
<label for="phone" class="block text-sm font-medium text-gray-700 mb-1">{{ __('profile.phone_label') }}</label>
<input type="tel" name="phone" id="phone" value="{{ old('phone', $user->phone) }}"
placeholder="+49..."
class="w-full rounded border-gray-300 text-sm">
@error('phone')
<p class="text-red-600 text-xs mt-1">{{ $message }}</p>
@enderror
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('profile.role_label') }}</label>
<p class="text-sm text-gray-600 bg-gray-50 px-3 py-2 rounded">{{ __('ui.enums.user_role.' . $user->role->value) }}</p>
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-2">{{ __('profile.language_label') }}</label>
@php
$flags = ['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}"];
@endphp
<div class="flex items-center gap-3" x-data="{ locale: @js(old('locale', $user->locale)) }">
<input type="hidden" name="locale" :value="locale">
@foreach ($flags as $code => $flag)
<button type="button" @click="locale = @js($code)"
:class="locale === @js($code) ? 'ring-2 ring-blue-500 ring-offset-1 scale-110' : 'opacity-50 hover:opacity-80'"
class="text-2xl leading-none transition-all rounded cursor-pointer"
title="{{ __('ui.locales.' . $code) }}">
{{ $flag }}
</button>
@endforeach
</div>
</div>
<button type="submit" class="bg-blue-600 text-white px-4 py-2 rounded text-sm hover:bg-blue-700">{{ __('ui.save') }}</button>
</form>
@if ($user->getAvatarUrl())
<div class="mt-3 pt-3 border-t">
<form method="POST" action="{{ route('profile.remove-picture') }}" 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">{{ __('profile.remove_picture') }}</button>
</form>
</div>
@endif
</div>
{{-- DSGVO-Einverständniserklärung --}}
<div id="dsgvo-consent" class="bg-white rounded-lg shadow p-6 mb-6" x-data="{ dsgvoModal: false }">
<h2 class="text-lg font-semibold mb-2">{{ __('profile.dsgvo_title') }}</h2>
<p class="text-sm text-gray-600 mb-4">{{ __('profile.dsgvo_description') }}</p>
@if ($user->dsgvo_consent_file)
@php
$ext = strtolower(pathinfo($user->dsgvo_consent_file, PATHINFO_EXTENSION));
$dsgvoIsPdf = $ext === 'pdf';
$dsgvoIsImage = in_array($ext, ['jpg', 'jpeg', 'png', 'gif', 'webp']);
@endphp
{{-- Status-Banner --}}
<div class="flex items-center gap-3 p-3 rounded-md mb-4
{{ $user->isDsgvoConfirmed() ? 'bg-green-50 border border-green-200' : 'bg-yellow-50 border border-yellow-200' }}">
<div class="flex-1">
<p class="text-sm font-medium {{ $user->isDsgvoConfirmed() ? 'text-green-800' : 'text-yellow-800' }}">
@if ($user->isDsgvoConfirmed())
{{ __('profile.dsgvo_confirmed') }}
<span class="text-xs font-normal block mt-0.5">
{{ __('profile.dsgvo_confirmed_by', [
'name' => $user->dsgvoAcceptedBy?->name ?? '—',
'date' => $user->dsgvo_accepted_at->translatedFormat(__('ui.date_format'))
]) }}
</span>
@else
{{ __('profile.dsgvo_pending') }}
@endif
</p>
</div>
<button type="button" @click="dsgvoModal = true"
class="text-sm text-blue-600 hover:text-blue-800 font-medium cursor-pointer">
{{ __('profile.dsgvo_view') }}
</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">{{ __('profile.dsgvo_title') }}</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('profile.dsgvo-consent') }}" alt="{{ __('profile.dsgvo_title') }}" class="max-w-full max-h-[70vh] rounded object-contain">
</div>
@elseif ($dsgvoIsPdf)
<iframe src="{{ route('profile.dsgvo-consent') }}" 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>
{{-- Ersetzen --}}
<form method="POST" action="{{ route('profile.upload-dsgvo-consent') }}" enctype="multipart/form-data" class="mb-3">
@csrf
<div class="flex items-center gap-3">
<label class="cursor-pointer bg-gray-100 text-gray-700 px-3 py-1.5 rounded-md text-sm hover:bg-gray-200">
{{ __('profile.dsgvo_replace') }}
<input type="file" name="dsgvo_consent_file"
accept="application/pdf,image/jpeg,image/png,image/gif,image/webp"
class="hidden" onchange="this.form.submit()">
</label>
<span class="text-xs text-gray-400">{{ __('profile.dsgvo_file_hint') }}</span>
</div>
@error('dsgvo_consent_file')
<p class="text-red-600 text-xs mt-1">{{ $message }}</p>
@enderror
</form>
{{-- Entfernen --}}
<form method="POST" action="{{ route('profile.remove-dsgvo-consent') }}"
onsubmit="return confirm(@js(__('profile.dsgvo_confirm_remove')))">
@csrf
@method('DELETE')
<button type="submit" class="text-xs text-red-500 hover:text-red-700">
{{ __('profile.dsgvo_remove') }}
</button>
</form>
@else
{{-- Upload-Formular --}}
<form method="POST" action="{{ route('profile.upload-dsgvo-consent') }}" enctype="multipart/form-data">
@csrf
<div class="flex items-center gap-3">
<label class="cursor-pointer bg-blue-600 text-white px-4 py-2 rounded-md text-sm hover:bg-blue-700">
{{ __('profile.dsgvo_upload') }}
<input type="file" name="dsgvo_consent_file"
accept="application/pdf,image/jpeg,image/png,image/gif,image/webp"
class="hidden" onchange="this.form.submit()">
</label>
<span class="text-xs text-gray-400">{{ __('profile.dsgvo_file_hint') }}</span>
</div>
@error('dsgvo_consent_file')
<p class="text-red-600 text-xs mt-1">{{ $message }}</p>
@enderror
</form>
@endif
</div>
{{-- Zugeordnete Kinder --}}
@if ($user->children->isNotEmpty())
<div class="bg-white rounded-lg shadow p-6 mb-6">
<h2 class="text-lg font-semibold mb-3">{{ __('profile.my_children') }}</h2>
<div class="divide-y divide-gray-100">
@foreach ($user->children as $child)
<div class="py-2 flex items-center justify-between">
<div>
<span class="text-sm font-medium text-gray-900">{{ $child->full_name }}</span>
@if ($child->pivot->relationship_label)
<span class="text-xs text-gray-500 ml-1 rtl:mr-1 rtl:ml-0">({{ $child->pivot->relationship_label }})</span>
@endif
</div>
<span class="text-xs text-gray-500">{{ $child->team->name ?? '—' }}</span>
</div>
@endforeach
</div>
</div>
@endif
{{-- Gefahrenzone: Account löschen (nur für Eltern) --}}
@if ($user->role === \App\Enums\UserRole::User)
<div class="bg-white rounded-lg shadow p-6 border border-red-200">
<h2 class="font-semibold text-red-700 mb-2">{{ __('profile.danger_zone') }}</h2>
<p class="text-sm text-gray-600 mb-3">{{ __('profile.delete_account_hint') }}</p>
@php $orphanedChildren = $user->getOrphanedChildren(); @endphp
@if ($orphanedChildren->isNotEmpty())
<div class="bg-yellow-50 border border-yellow-200 rounded-md p-3 mb-4">
<p class="text-sm text-yellow-800 font-medium">{{ __('profile.delete_warning_children') }}</p>
<ul class="list-disc list-inside text-sm text-yellow-700 mt-1">
@foreach ($orphanedChildren as $child)
<li>{{ $child->full_name }}</li>
@endforeach
</ul>
</div>
@endif
<form method="POST" action="{{ route('profile.destroy') }}"
onsubmit="return confirm(@js(__('profile.delete_confirm')))">
@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">
{{ __('profile.delete_account') }}
</button>
</form>
</div>
@endif
</x-layouts.app>