sortBy === $column) { $this->sortDir = $this->sortDir === 'asc' ? 'desc' : 'asc'; } else { $this->sortBy = $column; $this->sortDir = 'asc'; } $this->resetPage(); } public function with(): array { $sortable = ['name', 'email', 'portal', 'is_active', 'last_login_at', 'created_at']; $sort = in_array($this->sortBy, $sortable) ? $this->sortBy : 'created_at'; $authUser = Auth::user(); $impersonation = app(UserImpersonation::class); $users = User::query() ->select(['id', 'name', 'email', 'is_active', 'is_super_admin', 'portal', 'registration_type', 'last_login_at', 'created_at']) ->with([ 'roles:id,name', 'roles.permissions:id,name', 'permissions:id,name', ]) ->withCount([ 'contacts', 'pressReleases as published_press_releases_count' => fn ($query) => $query->where('status', PressReleaseStatus::Published->value), ]) ->withExists(['profile', 'billingAddress']) ->when(filled(trim($this->search)), function (Builder $query): void { $this->applySearch($query, $this->search); }) ->when($this->activeFilter !== 'all', function ($query): void { $query->where('is_active', $this->activeFilter === 'active'); }) ->when($this->portalFilter !== 'all', function ($query): void { $query->where('portal', $this->portalFilter); }) ->when($this->roleFilter !== 'all', function ($query): void { $query->whereHas('roles', fn ($roleQuery) => $roleQuery->where('name', $this->roleFilter)); }) ->when($this->qualityFilter !== 'all', function ($query): void { match ($this->qualityFilter) { 'without_company' => $query->whereDoesntHave('companies'), 'with_company' => $query->whereHas('companies'), 'without_profile' => $query->whereDoesntHave('profile'), 'with_profile' => $query->whereHas('profile'), 'without_contacts' => $query->whereDoesntHave('contacts'), 'without_billing' => $query->whereDoesntHave('billingAddress'), 'with_press_releases' => $query->whereHas('pressReleases'), 'without_press_releases' => $query->whereDoesntHave('pressReleases'), 'with_published_press_releases' => $query->whereHas('pressReleases', fn ($pressReleaseQuery) => $pressReleaseQuery->where('status', PressReleaseStatus::Published->value)), 'without_published_press_releases' => $query->whereDoesntHave('pressReleases', fn ($pressReleaseQuery) => $pressReleaseQuery->where('status', PressReleaseStatus::Published->value)), default => null, }; }) ->when($this->permissionFilter !== 'all', function ($query): void { match ($this->permissionFilter) { 'can_publish_press_releases' => $query->where(function ($permissionQuery): void { $permissionQuery ->where('is_super_admin', true) ->orWhereHas('roles.permissions', fn ($rolePermissionQuery) => $rolePermissionQuery->where('name', 'press-releases:publish')) ->orWhereHas('permissions', fn ($directPermissionQuery) => $directPermissionQuery->where('name', 'press-releases:publish')); }), default => null, }; }) ->orderBy($sort, $this->sortDir) ->paginate(50); $this->hydrateCompanyCounts($users); return [ 'users' => $users, 'selectedUser' => $this->selectedUser(), 'stats' => $this->stats(), 'availableRoles' => Role::query()->orderBy('name')->pluck('name')->all(), 'canImpersonateUsers' => $authUser instanceof User && $impersonation->canInitiate($authUser), 'isImpersonating' => $impersonation->isActive(), ]; } public function showUserDetails(int $userId): void { $this->viewingUserId = $userId; Flux::modal('user-details')->show(); } /** * @throws \Illuminate\Auth\Access\AuthorizationException */ public function loginAsUser(int $userId): void { $currentUser = Auth::user(); abort_unless($currentUser instanceof User, 403); $targetUser = User::query() ->with('roles') ->findOrFail($userId); if ($currentUser->is($targetUser)) { return; } app(UserImpersonation::class)->start($currentUser, $targetUser); $this->redirect(route('me.dashboard'), navigate: false); } public function companyUserRoleLabel(?string $role): string { return match ($role ?? 'member') { 'owner' => __('Inhaber'), 'responsible' => __('Verantwortlich'), 'member' => __('Mitglied'), default => (string) ($role ?? 'member'), }; } public function pressReleaseStatusColor(?string $status): string { return match ($status) { 'published' => 'green', 'review' => 'blue', 'rejected' => 'red', 'archived' => 'zinc', default => 'amber', }; } public function resetFilters(): void { $this->search = ''; $this->activeFilter = 'all'; $this->portalFilter = 'all'; $this->roleFilter = 'all'; $this->qualityFilter = 'all'; $this->permissionFilter = 'all'; $this->resetPage(); } /** * @return array{total: int, active: int, inactive: int} */ private function stats(): array { return app(AdminPerformanceCache::class)->remember(AdminPerformanceCache::UserStats, AdminPerformanceCache::StatsTtl, function (): array { $stats = User::query() ->toBase() ->selectRaw('COUNT(*) as total') ->selectRaw('SUM(CASE WHEN is_active = ? THEN 1 ELSE 0 END) as active', [true]) ->selectRaw('SUM(CASE WHEN is_active = ? THEN 1 ELSE 0 END) as inactive', [false]) ->first(); return [ 'total' => (int) ($stats->total ?? 0), 'active' => (int) ($stats->active ?? 0), 'inactive' => (int) ($stats->inactive ?? 0), ]; }); } private function hydrateCompanyCounts($users): void { $userIds = $users->getCollection()->pluck('id'); if ($userIds->isEmpty()) { return; } $companyCounts = Company::query()->join('company_user', 'companies.id', '=', 'company_user.company_id')->whereIn('company_user.user_id', $userIds)->selectRaw('company_user.user_id, COUNT(*) as aggregate')->groupBy('company_user.user_id')->pluck('aggregate', 'user_id'); $users->getCollection()->each(function (User $user) use ($companyCounts): void { $user->setAttribute('companies_count', (int) $companyCounts->get($user->id, 0)); }); } private function selectedUser(): ?User { if ($this->viewingUserId === null) { return null; } return User::query() ->withCount([ 'contacts', 'pressReleases', 'pressReleases as published_press_releases_count' => fn ($query) => $query->where('status', PressReleaseStatus::Published->value), 'userPaymentOptions', 'tokens', ]) ->with([ 'roles' => fn ($query) => $query->orderBy('name'), 'profile', 'billingAddress', 'companies' => fn ($query) => $query ->withoutGlobalScopes() ->select(['companies.id', 'companies.name', 'companies.slug', 'companies.email', 'companies.phone', 'companies.website', 'companies.address', 'companies.country_code', 'companies.portal', 'companies.is_active', 'companies.disable_footer_code']) ->withCount(['contacts', 'pressReleases']) ->orderBy('name'), 'companies.contacts' => fn ($query) => $query ->withoutGlobalScopes() ->select(['contacts.id', 'contacts.company_id', 'contacts.portal', 'contacts.first_name', 'contacts.last_name', 'contacts.responsibility', 'contacts.email', 'contacts.phone']) ->orderBy('last_name') ->orderBy('first_name') ->limit(10), 'companies.pressReleases' => fn ($query) => $query ->withoutGlobalScopes() ->select(['press_releases.id', 'press_releases.company_id', 'press_releases.user_id', 'press_releases.portal', 'press_releases.title', 'press_releases.status', 'press_releases.hits', 'press_releases.no_export', 'press_releases.published_at', 'press_releases.created_at']) ->latest('published_at') ->latest('created_at') ->limit(5), ]) ->find($this->viewingUserId); } private function applySearch(Builder $query, string $search): void { $terms = preg_split('/\s+/', trim($search), -1, PREG_SPLIT_NO_EMPTY); if ($terms === false || $terms === []) { return; } $query->where(function (Builder $searchQuery) use ($terms): void { foreach ($terms as $term) { $pattern = '%'.$this->escapeLikeTerm($term).'%'; $searchQuery->where(function (Builder $termQuery) use ($pattern): void { $termQuery ->whereLike('name', $pattern) ->orWhereLike('email', $pattern); }); } }); } private function escapeLikeTerm(string $term): string { return addcslashes($term, '\%_'); } public function updatedSearch(): void { $this->resetPage(); } public function updatedActiveFilter(): void { $this->resetPage(); } public function updatedPortalFilter(): void { $this->resetPage(); } public function updatedRoleFilter(): void { $this->resetPage(); } public function updatedQualityFilter(): void { $this->resetPage(); } public function updatedPermissionFilter(): void { $this->resetPage(); } }; ?>
{{-- ============== PAGE HEADER ============== --}}
{{ __('Admin Backend') }} {{ __('Administration · User-Verwaltung') }}

{{ __('Benutzer') }}

{{ __('Alle Konten der Plattform mit Rollen, Berechtigungen und Migrations-/Pflegefällen.') }}

{{ __('Benutzer anlegen') }}
{{-- ============== KPI-Reihe ============== --}}
{{ __('User-Konten') }} {{ __('alle Portale') }} {{ __('Login möglich') }} {{ __('Produktiv') }} {{ __('gesperrt / archiviert') }} {{ __('kein Login') }}
{{-- ============== FILTER-PANEL ============== --}}
{{ __('Filter & Suche') }} @if ($search || $activeFilter !== 'all' || $portalFilter !== 'all' || $roleFilter !== 'all' || $qualityFilter !== 'all' || $permissionFilter !== 'all')
{{ __('Filter aktiv') }} {{ __('Zurücksetzen') }}
@endif
@foreach ($availableRoles as $roleName) @endforeach
@if (! ($search || $activeFilter !== 'all' || $portalFilter !== 'all' || $roleFilter !== 'all' || $qualityFilter !== 'all' || $permissionFilter !== 'all'))

{{ __('Nutze die Filter, um offene Migrations- und Pflegefälle schnell zu finden.') }}

@endif
{{-- ============== TABELLE ============== --}}
{{ __('Alle Benutzer') }} {{ __(':count Einträge', ['count' => $users->count()]) }}
{{ __('Aktionen') }} {{ __('Benutzer') }} {{ __('Portal') }} {{ __('Rollen') }} {{ __('Zuordnung') }} {{ __('Datenqualität') }} {{ __('Status') }} {{ __('Letzter Login') }} {{ __('Hinzugefügt') }} @forelse ($users as $user) @php $portalValue = $user->portal?->value ?? (string) ($user->portal ?? '-'); $canPublishPressReleases = (bool) $user->is_super_admin || $user->permissions->contains('name', 'press-releases:publish') || $user->roles->contains(fn ($role) => $role->permissions->contains('name', 'press-releases:publish')); $canLoginAsUser = $canImpersonateUsers && ! $isImpersonating && $user->id !== auth()->id() && app(UserImpersonation::class)->canBeImpersonated($user); @endphp
@if($canLoginAsUser) @endif
{{ $user->name }} {{ $user->email }} ID: {{ $user->id }}
{{ strtoupper($portalValue) }}
@forelse($user->roles as $role) {{ $role->name }} @empty {{ __('–') }} @endforelse @if($canPublishPressReleases) {{ __('PM-Freigabe') }} @endif
@if (($user->companies_count ?? 0) > 0) {{ trans_choice(':count Firma|:count Firmen', $user->companies_count, ['count' => $user->companies_count]) }} @else {{ __('Keine Firma') }} @endif @if (($user->contacts_count ?? 0) > 0) {{ trans_choice(':count Kontakt|:count Kontakte', $user->contacts_count, ['count' => $user->contacts_count]) }} @else {{ __('Keine Kontakte') }} @endif @if (($user->published_press_releases_count ?? 0) > 0) {{ trans_choice(':count veröffentlicht|:count veröffentlicht', $user->published_press_releases_count, ['count' => $user->published_press_releases_count]) }} @else {{ __('Keine veröffentlichten PMs') }} @endif
@if ($user->profile_exists) {{ __('Profil OK') }} @else {{ __('Profil fehlt') }} @endif @if ($user->billing_address_exists) {{ __('Rechnung OK') }} @else {{ __('Keine Rechnung') }} @endif
@if ($user->is_active) {{ __('Aktiv') }} @else {{ __('Inaktiv') }} @endif {{ $user->last_login_at?->format('d.m.Y H:i') ?? __('Nie') }} {{ $user->created_at?->format('d.m.Y H:i') ?? '–' }}
@empty
{{ __('Keine Benutzer gefunden') }}
@endforelse
{{ $users->links('components.portal.pagination') }}
@if ($selectedUser)
{{ $selectedUser->name }}
{{ $selectedUser->email }} ID: {{ $selectedUser->id }} @if($selectedUser->legacy_portal || $selectedUser->legacy_id) {{ __('Legacy') }}: {{ $selectedUser->legacy_portal ?? '—' }} #{{ $selectedUser->legacy_id ?? '—' }} @endif
@if ($selectedUser->is_active) {{ __('Aktiv') }} @else {{ __('Inaktiv') }} @endif {{ $selectedUser->portal?->label() ?? '-' }} {{ $selectedUser->registration_type?->label() ?? '-' }} @if($selectedUser->is_super_admin) {{ __('Super Admin') }} @endif
@if($canImpersonateUsers && ! $isImpersonating && auth()->id() !== $selectedUser->id && app(UserImpersonation::class)->canBeImpersonated($selectedUser)) {{ __('Login als User') }} @endif {{ __('Bearbeiten') }} {{ __('Schließen') }}
{{ __('Firmen') }} {{ $selectedUser->companies->count() }} {{ __('Kontakte') }} {{ $selectedUser->contacts_count }} {{ __('Pressemitteilungen') }} {{ $selectedUser->press_releases_count }} @if($selectedUser->published_press_releases_count > 0) {{ trans_choice(':count veröffentlichte PM anzeigen|:count veröffentlichte PMs anzeigen', $selectedUser->published_press_releases_count, ['count' => $selectedUser->published_press_releases_count]) }} @else {{ __('Keine veröffentlichten PMs') }} @endif {{ __('API-Tokens') }} {{ $selectedUser->tokens_count }} {{ __('Zahloptionen') }} {{ $selectedUser->user_payment_options_count }}
{{ __('Account & Zugriff') }}
{{ __('E-Mail verifiziert') }} {{ $selectedUser->email_verified_at?->format('d.m.Y H:i') ?? __('Nein') }}
{{ __('Sprache') }} {{ strtoupper($selectedUser->language ?? 'de') }}
{{ __('Letzter Login') }} {{ $selectedUser->last_login_at?->format('d.m.Y H:i') ?? __('Nie') }}
{{ __('Letzte IP') }} {{ $selectedUser->last_login_ip ?: '–' }}
{{ __('Zuletzt gesehen') }} {{ $selectedUser->last_seen_at?->format('d.m.Y H:i') ?? '–' }}
{{ __('DSGVO-Einwilligung') }} {{ $selectedUser->gdpr_consent_at?->format('d.m.Y H:i') ?? '–' }}
@forelse($selectedUser->roles as $role) {{ $role->name }} @empty {{ __('Keine Rolle') }} @endforelse
{{ __('Legacy-Profil') }} @if ($selectedUser->profile)
{{ __('Name') }} {{ trim(($selectedUser->profile->first_name ?? '').' '.($selectedUser->profile->last_name ?? '')) ?: '–' }}
{{ __('Telefon') }} {{ $selectedUser->profile->phone ?: '–' }}
{{ __('Geburtsdatum') }} {{ $selectedUser->profile->birthdate?->format('d.m.Y') ?? '–' }}
{{ __('Land') }} {{ $selectedUser->profile->country_code ?: '–' }}
{{ __('Adresse') }} {{ $selectedUser->profile->address ?: '–' }}
{{ __('Validierung') }} {{ $selectedUser->profile->validation_date?->format('d.m.Y') ?? '–' }}
{{ __('Vertrag') }} {{ $selectedUser->profile->contract_date?->format('d.m.Y') ?? '–' }}
@else {{ __('Kein Legacy-Profil hinterlegt') }} @endif
{{ __('Rechnungsadresse') }} @if ($selectedUser->billingAddress)
{{ __('Name') }} {{ $selectedUser->billingAddress->name ?: '–' }}
{{ __('Adresse') }} {{ $selectedUser->billingAddress->address1 ?: '–' }} @if ($selectedUser->billingAddress->address2) {{ $selectedUser->billingAddress->address2 }} @endif
{{ __('Ort') }} {{ trim(($selectedUser->billingAddress->postal_code ?? '').' '.($selectedUser->billingAddress->city ?? '')) ?: '–' }}
{{ __('Land') }} {{ $selectedUser->billingAddress->country_code ?: '–' }}
@else {{ __('Keine Rechnungsadresse hinterlegt') }} @endif
{{ __('Datenqualität') }}
{{ $selectedUser->profile ? __('Profil vorhanden') : __('Profil fehlt') }} {{ $selectedUser->billingAddress ? __('Rechnung vorhanden') : __('Rechnung fehlt') }} {{ $selectedUser->companies->isNotEmpty() ? __('Firma verknüpft') : __('Keine Firma') }} {{ $selectedUser->contacts_count > 0 ? __('Kontakte vorhanden') : __('Keine Kontakte') }} {{ $selectedUser->press_releases_count > 0 ? __('PMs vorhanden') : __('Keine PMs') }}
{{ __('Firmen, Kontakte und Pressemitteilungen') }} {{ __('Struktur: User → Firmen → Kontakte / Pressemitteilungen') }}
@forelse($selectedUser->companies as $company)
@if (\Illuminate\Support\Facades\Route::has('admin.companies.show')) {{ $company->name }} @else {{ $company->name }} @endif {{ $company->slug }}
@if ($company->email) {{ $company->email }} @endif @if ($company->phone) {{ $company->phone }} @endif @if ($company->website) {{ $company->website }} @endif
@if($company->address) {{ $company->address }} @endif
{{ $this->companyUserRoleLabel($company->pivot?->role ?? 'member') }} {{ trans_choice(':count PM|:count PMs', $company->press_releases_count, ['count' => $company->press_releases_count]) }} {{ trans_choice(':count Kontakt|:count Kontakte', $company->contacts_count, ['count' => $company->contacts_count]) }} {{ $company->portal?->label() ?? '—' }} @if ($company->disable_footer_code) {{ __('Footer-Code aus') }} @endif @if (!$company->is_active) {{ __('Inaktiv') }} @endif
{{ __('Kontakte') }} @if ($company->contacts->isNotEmpty())
@foreach ($company->contacts as $contact)
{{ trim(($contact->first_name ?? '').' '.($contact->last_name ?? '')) ?: __('Kontakt ohne Name') }} {{ $contact->responsibility ?? __('Keine Rolle hinterlegt') }}
@if ($contact->email) {{ $contact->email }} @endif @if ($contact->phone) {{ $contact->phone }} @endif
@if (\Illuminate\Support\Facades\Route::has('admin.contacts.edit')) {{ __('Bearbeiten') }} @endif
@endforeach @if ($company->contacts_count > $company->contacts->count()) {{ __(':count weitere Kontakte werden hier nicht geladen. Öffne die Firma, um alle Kontakte zu sehen.', ['count' => $company->contacts_count - $company->contacts->count()]) }} @endif
@else {{ __('Keine Kontakte bei dieser Firma') }} @endif
{{ __('Aktuelle Pressemitteilungen') }} @if ($company->pressReleases->isNotEmpty())
@foreach ($company->pressReleases as $pressRelease)
@if (\Illuminate\Support\Facades\Route::has('admin.press-releases.show')) {{ $pressRelease->title ?? __('Ohne Titel') }} @else {{ $pressRelease->title ?? __('Ohne Titel') }} @endif {{ $pressRelease->published_at?->format('d.m.Y') ?? $pressRelease->created_at?->format('d.m.Y') ?? '–' }} · {{ $pressRelease->portal?->label() ?? '—' }} · {{ __('Hits') }}: {{ $pressRelease->hits }}
{{ $pressRelease->status?->label() ?? $pressRelease->status?->value ?? '–' }}
@endforeach @if ($company->press_releases_count > $company->pressReleases->count()) {{ __(':count weitere Pressemitteilungen werden hier nicht geladen.', ['count' => $company->press_releases_count - $company->pressReleases->count()]) }} @endif
@else {{ __('Keine Pressemitteilungen bei dieser Firma') }} @endif
@empty {{ __('Keine Firmen verknüpft') }} @endforelse
@else {{ __('Benutzer nicht gefunden') }} @endif