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
{{ __('Alle Status') }}
{{ __('Aktiv') }}
{{ __('Inaktiv') }}
{{ __('Alle Portale') }}
Presseecho
Businessportal24
{{ __('Beide') }}
{{ __('Alle Rollen') }}
@foreach ($availableRoles as $roleName)
{{ $roleName }}
@endforeach
{{ __('Alle Datenstände') }}
{{ __('Ohne Firma') }}
{{ __('Mit Firma') }}
{{ __('Ohne Legacy-Profil') }}
{{ __('Mit Legacy-Profil') }}
{{ __('Ohne Kontakte') }}
{{ __('Ohne Rechnungsadresse') }}
{{ __('Mit Pressemitteilungen') }}
{{ __('Ohne Pressemitteilungen') }}
{{ __('Mit veröffentlichten PMs') }}
{{ __('Ohne veröffentlichte PMs') }}
{{ __('Alle Berechtigungen') }}
{{ __('Kann PMs freigeben') }}
@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