presseportale/resources/views/livewire/admin/users.blade.php
Kevin Adametz 036a53499f Responsive-Härtung: Seiten-Header, Kontextleiste, Stat-Cards
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 14:08:08 +00:00

967 lines
54 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
use App\Actions\Admin\UserImpersonation;
use App\Enums\PressReleaseStatus;
use App\Models\Company;
use App\Models\User;
use App\Services\Admin\AdminPerformanceCache;
use Flux\Flux;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Auth;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Volt\Component;
use Livewire\WithPagination;
use Spatie\Permission\Models\Role;
new #[Layout('components.layouts.app'), Title('Benutzer')] class extends Component
{
use WithPagination;
public string $search = '';
public string $activeFilter = 'all';
public string $portalFilter = 'all';
public string $roleFilter = 'all';
public string $qualityFilter = 'all';
public string $permissionFilter = 'all';
public string $sortBy = 'created_at';
public string $sortDir = 'desc';
public ?int $viewingUserId = null;
public function sort(string $column): void
{
if ($this->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();
}
}; ?>
<div class="space-y-8">
{{-- ============== PAGE HEADER ============== --}}
<header class="page-header">
<div class="min-w-0">
<div class="flex items-center gap-3 mb-3 flex-nowrap whitespace-nowrap">
<span class="badge hub dot">{{ __('Admin Backend') }}</span>
<span class="eyebrow muted">{{ __('Administration · User-Verwaltung') }}</span>
</div>
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)]">
{{ __('Benutzer') }}
</h1>
<p class="text-[13px] leading-[1.55] mt-2 m-0 max-w-[640px] text-[color:var(--color-ink-2)]">
{{ __('Alle Konten der Plattform mit Rollen, Berechtigungen und Migrations-/Pflegefällen.') }}
</p>
</div>
<div class="flex items-center gap-2 flex-shrink-0">
<flux:button variant="primary" icon="plus" href="{{ route('admin.users.create') }}" wire:navigate>
{{ __('Benutzer anlegen') }}
</flux:button>
</div>
</header>
{{-- ============== KPI-Reihe ============== --}}
<section class="grid grid-cols-1 gap-4 sm:grid-cols-3">
<x-portal.stat-card variant="primary" :label="__('Gesamt')" :value="number_format($stats['total'])">
<x-slot:meta>{{ __('User-Konten') }}</x-slot:meta>
<x-slot:trend>{{ __('alle Portale') }}</x-slot:trend>
</x-portal.stat-card>
<x-portal.stat-card variant="ok" :label="__('Aktiv')" :value="number_format($stats['active'])">
<x-slot:meta>{{ __('Login möglich') }}</x-slot:meta>
<x-slot:trend>{{ __('Produktiv') }}</x-slot:trend>
</x-portal.stat-card>
<x-portal.stat-card variant="muted" :label="__('Inaktiv')" :value="number_format($stats['inactive'])">
<x-slot:meta>{{ __('gesperrt / archiviert') }}</x-slot:meta>
<x-slot:trend>{{ __('kein Login') }}</x-slot:trend>
</x-portal.stat-card>
</section>
{{-- ============== FILTER-PANEL ============== --}}
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Filter & Suche') }}</span>
@if ($search || $activeFilter !== 'all' || $portalFilter !== 'all' || $roleFilter !== 'all' || $qualityFilter !== 'all' || $permissionFilter !== 'all')
<div class="flex items-center gap-2">
<span class="badge hub dot">{{ __('Filter aktiv') }}</span>
<flux:button size="sm" variant="filled" icon="arrow-path" type="button" wire:click="resetFilters">
{{ __('Zurücksetzen') }}
</flux:button>
</div>
@endif
</div>
<div class="p-5 space-y-4">
<div class="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
<flux:input wire:model.live.debounce.300ms="search" placeholder="{{ __('Name oder E-Mail suchen...') }}"
icon="magnifying-glass" class="lg:max-w-sm" />
<div class="flex flex-col gap-3 sm:flex-row sm:flex-wrap">
<flux:select wire:model.live="activeFilter" class="sm:w-40">
<option value="all">{{ __('Alle Status') }}</option>
<option value="active">{{ __('Aktiv') }}</option>
<option value="inactive">{{ __('Inaktiv') }}</option>
</flux:select>
<flux:select wire:model.live="portalFilter" class="sm:w-48">
<option value="all">{{ __('Alle Portale') }}</option>
<option value="presseecho">Presseecho</option>
<option value="businessportal24">Businessportal24</option>
<option value="both">{{ __('Beide') }}</option>
</flux:select>
<flux:select wire:model.live="roleFilter" class="sm:w-44">
<option value="all">{{ __('Alle Rollen') }}</option>
@foreach ($availableRoles as $roleName)
<option value="{{ $roleName }}">{{ $roleName }}</option>
@endforeach
</flux:select>
<flux:select wire:model.live="qualityFilter" class="sm:w-56">
<option value="all">{{ __('Alle Datenstände') }}</option>
<option value="without_company">{{ __('Ohne Firma') }}</option>
<option value="with_company">{{ __('Mit Firma') }}</option>
<option value="without_profile">{{ __('Ohne Legacy-Profil') }}</option>
<option value="with_profile">{{ __('Mit Legacy-Profil') }}</option>
<option value="without_contacts">{{ __('Ohne Kontakte') }}</option>
<option value="without_billing">{{ __('Ohne Rechnungsadresse') }}</option>
<option value="with_press_releases">{{ __('Mit Pressemitteilungen') }}</option>
<option value="without_press_releases">{{ __('Ohne Pressemitteilungen') }}</option>
<option value="with_published_press_releases">{{ __('Mit veröffentlichten PMs') }}</option>
<option value="without_published_press_releases">{{ __('Ohne veröffentlichte PMs') }}</option>
</flux:select>
<flux:select wire:model.live="permissionFilter" class="sm:w-56">
<option value="all">{{ __('Alle Berechtigungen') }}</option>
<option value="can_publish_press_releases">{{ __('Kann PMs freigeben') }}</option>
</flux:select>
</div>
</div>
@if (! ($search || $activeFilter !== 'all' || $portalFilter !== 'all' || $roleFilter !== 'all' || $qualityFilter !== 'all' || $permissionFilter !== 'all'))
<p class="text-[12px] text-[color:var(--color-ink-3)] m-0">
{{ __('Nutze die Filter, um offene Migrations- und Pflegefälle schnell zu finden.') }}
</p>
@endif
</div>
</article>
{{-- ============== TABELLE ============== --}}
<article class="panel overflow-hidden">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Alle Benutzer') }}</span>
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">
{{ __(':count Einträge', ['count' => $users->count()]) }}
</span>
</div>
<flux:table>
<flux:table.columns>
<flux:table.column>{{ __('Aktionen') }}</flux:table.column>
<flux:table.column sortable :sorted="$sortBy === 'name'" :direction="$sortDir"
wire:click="sort('name')">
{{ __('Benutzer') }}</flux:table.column>
<flux:table.column sortable :sorted="$sortBy === 'portal'" :direction="$sortDir"
wire:click="sort('portal')">{{ __('Portal') }}</flux:table.column>
<flux:table.column>{{ __('Rollen') }}</flux:table.column>
<flux:table.column>{{ __('Zuordnung') }}</flux:table.column>
<flux:table.column>{{ __('Datenqualität') }}</flux:table.column>
<flux:table.column sortable :sorted="$sortBy === 'is_active'" :direction="$sortDir"
wire:click="sort('is_active')">{{ __('Status') }}</flux:table.column>
<flux:table.column sortable :sorted="$sortBy === 'last_login_at'" :direction="$sortDir"
wire:click="sort('last_login_at')">{{ __('Letzter Login') }}</flux:table.column>
<flux:table.column sortable :sorted="$sortBy === 'created_at'" :direction="$sortDir"
wire:click="sort('created_at')">{{ __('Hinzugefügt') }}</flux:table.column>
</flux:table.columns>
<flux:table.rows>
@forelse ($users as $user)
<flux:table.row :key="$user->id">
@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
<flux:table.cell>
<div class="flex flex-wrap gap-1">
<flux:button size="sm" variant="filled" icon="eye"
wire:click="showUserDetails({{ $user->id }})" />
<flux:button size="sm" variant="filled" icon="pencil"
href="{{ route('admin.users.edit', $user->id) }}" wire:navigate />
@if($canLoginAsUser)
<flux:button
size="sm"
variant="filled"
icon="user"
square
type="button"
tooltip="{{ __('Login als User') }}"
wire:click="loginAsUser({{ $user->id }})"
wire:confirm="{{ __('Als :name anmelden?', ['name' => $user->name]) }}"
/>
@endif
</div>
</flux:table.cell>
<flux:table.cell>
<div class="min-w-0">
<flux:text weight="semibold">{{ $user->name }}</flux:text>
<flux:text class="text-sm text-zinc-600 dark:text-zinc-400">{{ $user->email }}</flux:text>
<flux:text class="text-xs text-zinc-400">ID: {{ $user->id }}</flux:text>
</div>
</flux:table.cell>
<flux:table.cell>
<flux:badge color="zinc" size="sm">{{ strtoupper($portalValue) }}</flux:badge>
</flux:table.cell>
<flux:table.cell>
<div class="flex flex-wrap gap-1">
@forelse($user->roles as $role)
<flux:badge color="zinc" size="sm">{{ $role->name }}</flux:badge>
@empty
<flux:text class="text-xs text-zinc-500">{{ __('') }}</flux:text>
@endforelse
@if($canPublishPressReleases)
<flux:badge color="emerald" size="sm">{{ __('PM-Freigabe') }}</flux:badge>
@endif
</div>
</flux:table.cell>
<flux:table.cell>
<div class="flex flex-wrap gap-1">
@if (($user->companies_count ?? 0) > 0)
<flux:badge color="green" size="sm" icon="building-office">
{{ trans_choice(':count Firma|:count Firmen', $user->companies_count, ['count' => $user->companies_count]) }}
</flux:badge>
@else
<flux:badge color="amber" size="sm">
{{ __('Keine Firma') }}
</flux:badge>
@endif
@if (($user->contacts_count ?? 0) > 0)
<flux:badge color="blue" size="sm">
{{ trans_choice(':count Kontakt|:count Kontakte', $user->contacts_count, ['count' => $user->contacts_count]) }}
</flux:badge>
@else
<flux:badge color="zinc" size="sm">{{ __('Keine Kontakte') }}</flux:badge>
@endif
@if (($user->published_press_releases_count ?? 0) > 0)
<flux:badge color="emerald" size="sm">
{{ trans_choice(':count veröffentlicht|:count veröffentlicht', $user->published_press_releases_count, ['count' => $user->published_press_releases_count]) }}
</flux:badge>
@else
<flux:badge color="zinc" size="sm">{{ __('Keine veröffentlichten PMs') }}</flux:badge>
@endif
</div>
</flux:table.cell>
<flux:table.cell>
<div class="flex flex-wrap gap-1">
@if ($user->profile_exists)
<flux:badge color="green" size="sm">{{ __('Profil OK') }}</flux:badge>
@else
<flux:badge color="amber" size="sm">{{ __('Profil fehlt') }}</flux:badge>
@endif
@if ($user->billing_address_exists)
<flux:badge color="green" size="sm">{{ __('Rechnung OK') }}</flux:badge>
@else
<flux:badge color="zinc" size="sm">{{ __('Keine Rechnung') }}</flux:badge>
@endif
</div>
</flux:table.cell>
<flux:table.cell>
@if ($user->is_active)
<flux:badge color="green" size="sm" icon="check">{{ __('Aktiv') }}
</flux:badge>
@else
<flux:badge color="red" size="sm" icon="x-mark">{{ __('Inaktiv') }}
</flux:badge>
@endif
</flux:table.cell>
<flux:table.cell>
<flux:text class="text-sm text-zinc-500">
{{ $user->last_login_at?->format('d.m.Y H:i') ?? __('Nie') }}
</flux:text>
</flux:table.cell>
<flux:table.cell>
<flux:text class="text-sm text-zinc-500">
{{ $user->created_at?->format('d.m.Y H:i') ?? '' }}
</flux:text>
</flux:table.cell>
</flux:table.row>
@empty
<flux:table.row>
<flux:table.cell colspan="9">
<div class="flex flex-col items-center justify-center px-4 py-10 text-center">
<div class="w-14 h-14 rounded-[6px] flex items-center justify-center mb-3
bg-[color:var(--color-hub-soft)] border border-[color:var(--color-hub-soft-2)] text-[color:var(--color-hub)]">
<flux:icon.users class="size-6" />
</div>
<div class="text-[14px] font-semibold text-[color:var(--color-ink)] mb-1">
{{ __('Keine Benutzer gefunden') }}
</div>
</div>
</flux:table.cell>
</flux:table.row>
@endforelse
</flux:table.rows>
</flux:table>
<div class="border-t border-[color:var(--color-bg-rule)] p-4">
{{ $users->links('components.portal.pagination') }}
</div>
</article>
<flux:modal name="user-details" class="w-full max-w-5xl" scroll="body">
<div class="space-y-6">
@if ($selectedUser)
<div class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<div>
<flux:heading size="xl">{{ $selectedUser->name }}</flux:heading>
<div class="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-sm text-zinc-500">
<span>{{ $selectedUser->email }}</span>
<span>ID: {{ $selectedUser->id }}</span>
@if($selectedUser->legacy_portal || $selectedUser->legacy_id)
<span>{{ __('Legacy') }}: {{ $selectedUser->legacy_portal ?? '—' }} #{{ $selectedUser->legacy_id ?? '—' }}</span>
@endif
</div>
<div class="mt-3 flex flex-wrap gap-2">
@if ($selectedUser->is_active)
<flux:badge color="green" size="sm">{{ __('Aktiv') }}</flux:badge>
@else
<flux:badge color="red" size="sm">{{ __('Inaktiv') }}</flux:badge>
@endif
<flux:badge color="zinc" size="sm">{{ $selectedUser->portal?->label() ?? '-' }}</flux:badge>
<flux:badge color="zinc" size="sm">{{ $selectedUser->registration_type?->label() ?? '-' }}</flux:badge>
@if($selectedUser->is_super_admin)
<flux:badge color="purple" size="sm">{{ __('Super Admin') }}</flux:badge>
@endif
</div>
</div>
<div class="flex flex-wrap gap-2">
@if($canImpersonateUsers && ! $isImpersonating && auth()->id() !== $selectedUser->id && app(UserImpersonation::class)->canBeImpersonated($selectedUser))
<flux:button
type="button"
variant="primary"
wire:click="loginAsUser({{ $selectedUser->id }})"
wire:confirm="{{ __('Als :name anmelden?', ['name' => $selectedUser->name]) }}"
>
{{ __('Login als User') }}
</flux:button>
@endif
<flux:button icon="pencil" href="{{ route('admin.users.edit', $selectedUser->id) }}" wire:navigate>
{{ __('Bearbeiten') }}
</flux:button>
<flux:modal.close>
<flux:button variant="filled">{{ __('Schließen') }}</flux:button>
</flux:modal.close>
</div>
</div>
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-5">
<flux:card>
<flux:text class="text-xs text-zinc-500">{{ __('Firmen') }}</flux:text>
<flux:text size="xl" weight="bold">{{ $selectedUser->companies->count() }}</flux:text>
</flux:card>
<flux:card>
<flux:text class="text-xs text-zinc-500">{{ __('Kontakte') }}</flux:text>
<flux:text size="xl" weight="bold">{{ $selectedUser->contacts_count }}</flux:text>
</flux:card>
<flux:card>
<flux:text class="text-xs text-zinc-500">{{ __('Pressemitteilungen') }}</flux:text>
<flux:text size="xl" weight="bold">{{ $selectedUser->press_releases_count }}</flux:text>
@if($selectedUser->published_press_releases_count > 0)
<flux:button
size="sm"
variant="filled"
icon="arrow-top-right-on-square"
class="mt-2"
href="{{ route('admin.press-releases.index', ['user' => $selectedUser->id, 'status' => \App\Enums\PressReleaseStatus::Published->value]) }}"
wire:navigate
>
{{ trans_choice(':count veröffentlichte PM anzeigen|:count veröffentlichte PMs anzeigen', $selectedUser->published_press_releases_count, ['count' => $selectedUser->published_press_releases_count]) }}
</flux:button>
@else
<flux:text class="mt-1 text-xs text-zinc-500">{{ __('Keine veröffentlichten PMs') }}</flux:text>
@endif
</flux:card>
<flux:card>
<flux:text class="text-xs text-zinc-500">{{ __('API-Tokens') }}</flux:text>
<flux:text size="xl" weight="bold">{{ $selectedUser->tokens_count }}</flux:text>
</flux:card>
<flux:card>
<flux:text class="text-xs text-zinc-500">{{ __('Zahloptionen') }}</flux:text>
<flux:text size="xl" weight="bold">{{ $selectedUser->user_payment_options_count }}</flux:text>
</flux:card>
</div>
<div class="grid gap-4 lg:grid-cols-2">
<flux:card>
<flux:heading size="lg" class="mb-4">{{ __('Account & Zugriff') }}</flux:heading>
<div class="grid gap-3 sm:grid-cols-2">
<div>
<flux:text class="text-xs text-zinc-500">{{ __('E-Mail verifiziert') }}</flux:text>
<flux:text>{{ $selectedUser->email_verified_at?->format('d.m.Y H:i') ?? __('Nein') }}</flux:text>
</div>
<div>
<flux:text class="text-xs text-zinc-500">{{ __('Sprache') }}</flux:text>
<flux:text>{{ strtoupper($selectedUser->language ?? 'de') }}</flux:text>
</div>
<div>
<flux:text class="text-xs text-zinc-500">{{ __('Letzter Login') }}</flux:text>
<flux:text>{{ $selectedUser->last_login_at?->format('d.m.Y H:i') ?? __('Nie') }}</flux:text>
</div>
<div>
<flux:text class="text-xs text-zinc-500">{{ __('Letzte IP') }}</flux:text>
<flux:text>{{ $selectedUser->last_login_ip ?: '' }}</flux:text>
</div>
<div>
<flux:text class="text-xs text-zinc-500">{{ __('Zuletzt gesehen') }}</flux:text>
<flux:text>{{ $selectedUser->last_seen_at?->format('d.m.Y H:i') ?? '' }}</flux:text>
</div>
<div>
<flux:text class="text-xs text-zinc-500">{{ __('DSGVO-Einwilligung') }}</flux:text>
<flux:text>{{ $selectedUser->gdpr_consent_at?->format('d.m.Y H:i') ?? '' }}</flux:text>
</div>
</div>
<div class="mt-4 flex flex-wrap gap-2">
@forelse($selectedUser->roles as $role)
<flux:badge color="zinc" size="sm">{{ $role->name }}</flux:badge>
@empty
<flux:badge color="amber" size="sm">{{ __('Keine Rolle') }}</flux:badge>
@endforelse
</div>
</flux:card>
<flux:card>
<flux:heading size="lg" class="mb-4">{{ __('Legacy-Profil') }}</flux:heading>
@if ($selectedUser->profile)
<div class="grid gap-3 sm:grid-cols-2">
<div>
<flux:text class="text-xs text-zinc-500">{{ __('Name') }}</flux:text>
<flux:text>{{ trim(($selectedUser->profile->first_name ?? '').' '.($selectedUser->profile->last_name ?? '')) ?: '' }}</flux:text>
</div>
<div>
<flux:text class="text-xs text-zinc-500">{{ __('Telefon') }}</flux:text>
<flux:text>{{ $selectedUser->profile->phone ?: '' }}</flux:text>
</div>
<div>
<flux:text class="text-xs text-zinc-500">{{ __('Geburtsdatum') }}</flux:text>
<flux:text>{{ $selectedUser->profile->birthdate?->format('d.m.Y') ?? '' }}</flux:text>
</div>
<div>
<flux:text class="text-xs text-zinc-500">{{ __('Land') }}</flux:text>
<flux:text>{{ $selectedUser->profile->country_code ?: '' }}</flux:text>
</div>
<div class="sm:col-span-2">
<flux:text class="text-xs text-zinc-500">{{ __('Adresse') }}</flux:text>
<flux:text>{{ $selectedUser->profile->address ?: '' }}</flux:text>
</div>
<div>
<flux:text class="text-xs text-zinc-500">{{ __('Validierung') }}</flux:text>
<flux:text>{{ $selectedUser->profile->validation_date?->format('d.m.Y') ?? '' }}</flux:text>
</div>
<div>
<flux:text class="text-xs text-zinc-500">{{ __('Vertrag') }}</flux:text>
<flux:text>{{ $selectedUser->profile->contract_date?->format('d.m.Y') ?? '' }}</flux:text>
</div>
</div>
@else
<flux:text class="text-sm text-zinc-500">{{ __('Kein Legacy-Profil hinterlegt') }}</flux:text>
@endif
</flux:card>
<flux:card>
<flux:heading size="lg" class="mb-4">{{ __('Rechnungsadresse') }}</flux:heading>
@if ($selectedUser->billingAddress)
<div class="grid gap-3 sm:grid-cols-2">
<div class="sm:col-span-2">
<flux:text class="text-xs text-zinc-500">{{ __('Name') }}</flux:text>
<flux:text>{{ $selectedUser->billingAddress->name ?: '' }}</flux:text>
</div>
<div class="sm:col-span-2">
<flux:text class="text-xs text-zinc-500">{{ __('Adresse') }}</flux:text>
<flux:text>{{ $selectedUser->billingAddress->address1 ?: '' }}</flux:text>
@if ($selectedUser->billingAddress->address2)
<flux:text>{{ $selectedUser->billingAddress->address2 }}</flux:text>
@endif
</div>
<div>
<flux:text class="text-xs text-zinc-500">{{ __('Ort') }}</flux:text>
<flux:text>{{ trim(($selectedUser->billingAddress->postal_code ?? '').' '.($selectedUser->billingAddress->city ?? '')) ?: '' }}</flux:text>
</div>
<div>
<flux:text class="text-xs text-zinc-500">{{ __('Land') }}</flux:text>
<flux:text>{{ $selectedUser->billingAddress->country_code ?: '' }}</flux:text>
</div>
</div>
@else
<flux:text class="text-sm text-zinc-500">{{ __('Keine Rechnungsadresse hinterlegt') }}</flux:text>
@endif
</flux:card>
<flux:card>
<flux:heading size="lg" class="mb-4">{{ __('Datenqualität') }}</flux:heading>
<div class="flex flex-wrap gap-2">
<flux:badge :color="$selectedUser->profile ? 'green' : 'amber'" size="sm">
{{ $selectedUser->profile ? __('Profil vorhanden') : __('Profil fehlt') }}
</flux:badge>
<flux:badge :color="$selectedUser->billingAddress ? 'green' : 'zinc'" size="sm">
{{ $selectedUser->billingAddress ? __('Rechnung vorhanden') : __('Rechnung fehlt') }}
</flux:badge>
<flux:badge :color="$selectedUser->companies->isNotEmpty() ? 'green' : 'amber'" size="sm">
{{ $selectedUser->companies->isNotEmpty() ? __('Firma verknüpft') : __('Keine Firma') }}
</flux:badge>
<flux:badge :color="$selectedUser->contacts_count > 0 ? 'blue' : 'zinc'" size="sm">
{{ $selectedUser->contacts_count > 0 ? __('Kontakte vorhanden') : __('Keine Kontakte') }}
</flux:badge>
<flux:badge :color="$selectedUser->press_releases_count > 0 ? 'blue' : 'zinc'" size="sm">
{{ $selectedUser->press_releases_count > 0 ? __('PMs vorhanden') : __('Keine PMs') }}
</flux:badge>
</div>
</flux:card>
</div>
<flux:card>
<div class="mb-4 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<div>
<flux:heading size="lg">{{ __('Firmen, Kontakte und Pressemitteilungen') }}</flux:heading>
<flux:text class="text-sm text-zinc-500">
{{ __('Struktur: User → Firmen → Kontakte / Pressemitteilungen') }}
</flux:text>
</div>
</div>
<div class="space-y-4">
@forelse($selectedUser->companies as $company)
<div class="rounded-lg border border-zinc-200 p-4 dark:border-zinc-700">
<div class="mb-3 flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
<div class="min-w-0 flex-1">
@if (\Illuminate\Support\Facades\Route::has('admin.companies.show'))
<a href="{{ route('admin.companies.show', $company->id) }}" wire:navigate class="block">
<flux:text weight="semibold" class="text-blue-600 hover:underline dark:text-blue-400">
{{ $company->name }}
</flux:text>
</a>
@else
<flux:text weight="semibold">{{ $company->name }}</flux:text>
@endif
<flux:text class="text-xs text-zinc-500">{{ $company->slug }}</flux:text>
<div class="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-zinc-500">
@if ($company->email)
<span>{{ $company->email }}</span>
@endif
@if ($company->phone)
<span>{{ $company->phone }}</span>
@endif
@if ($company->website)
<span>{{ $company->website }}</span>
@endif
</div>
@if($company->address)
<flux:text class="mt-1 text-xs text-zinc-500">{{ $company->address }}</flux:text>
@endif
</div>
<div class="flex flex-wrap items-center gap-2">
<flux:badge color="zinc" size="sm">
{{ $this->companyUserRoleLabel($company->pivot?->role ?? 'member') }}
</flux:badge>
<flux:badge color="blue" size="sm">
{{ trans_choice(':count PM|:count PMs', $company->press_releases_count, ['count' => $company->press_releases_count]) }}
</flux:badge>
<flux:badge color="zinc" size="sm">
{{ trans_choice(':count Kontakt|:count Kontakte', $company->contacts_count, ['count' => $company->contacts_count]) }}
</flux:badge>
<flux:badge color="zinc" size="sm">
{{ $company->portal?->label() ?? '—' }}
</flux:badge>
@if ($company->disable_footer_code)
<flux:badge color="amber" size="sm">{{ __('Footer-Code aus') }}</flux:badge>
@endif
@if (!$company->is_active)
<flux:badge color="red" size="sm">{{ __('Inaktiv') }}</flux:badge>
@endif
</div>
</div>
<div class="grid gap-4 lg:grid-cols-2">
<div>
<flux:text weight="semibold" class="mb-2 text-sm">{{ __('Kontakte') }}</flux:text>
@if ($company->contacts->isNotEmpty())
<div class="space-y-2">
@foreach ($company->contacts as $contact)
<div class="flex flex-col gap-2 rounded-md bg-zinc-50 p-2 dark:bg-zinc-900 sm:flex-row sm:items-center sm:justify-between">
<div class="min-w-0">
<flux:text class="text-sm" weight="medium">
{{ trim(($contact->first_name ?? '').' '.($contact->last_name ?? '')) ?: __('Kontakt ohne Name') }}
</flux:text>
<flux:text class="text-xs text-zinc-500">
{{ $contact->responsibility ?? __('Keine Rolle hinterlegt') }}
</flux:text>
<div class="mt-0.5 flex flex-wrap items-center gap-2 text-xs text-zinc-500">
@if ($contact->email)
<a href="mailto:{{ $contact->email }}" class="text-blue-600 hover:underline dark:text-blue-400">{{ $contact->email }}</a>
@endif
@if ($contact->phone)
<span>{{ $contact->phone }}</span>
@endif
</div>
</div>
@if (\Illuminate\Support\Facades\Route::has('admin.contacts.edit'))
<flux:button size="sm" variant="filled" icon="pencil" href="{{ route('admin.contacts.edit', $contact->id) }}" wire:navigate>
{{ __('Bearbeiten') }}
</flux:button>
@endif
</div>
@endforeach
@if ($company->contacts_count > $company->contacts->count())
<flux:text class="text-xs text-zinc-500">
{{ __(':count weitere Kontakte werden hier nicht geladen. Öffne die Firma, um alle Kontakte zu sehen.', ['count' => $company->contacts_count - $company->contacts->count()]) }}
</flux:text>
@endif
</div>
@else
<flux:text class="text-xs text-zinc-500">{{ __('Keine Kontakte bei dieser Firma') }}</flux:text>
@endif
</div>
<div>
<flux:text weight="semibold" class="mb-2 text-sm">{{ __('Aktuelle Pressemitteilungen') }}</flux:text>
@if ($company->pressReleases->isNotEmpty())
<div class="space-y-2">
@foreach ($company->pressReleases as $pressRelease)
<div class="rounded-md bg-zinc-50 p-2 dark:bg-zinc-900">
<div class="flex items-start justify-between gap-2">
<div class="min-w-0">
@if (\Illuminate\Support\Facades\Route::has('admin.press-releases.show'))
<a href="{{ route('admin.press-releases.show', $pressRelease->id) }}" wire:navigate class="text-sm font-medium text-blue-600 hover:underline dark:text-blue-400">
{{ $pressRelease->title ?? __('Ohne Titel') }}
</a>
@else
<flux:text class="text-sm" weight="medium">{{ $pressRelease->title ?? __('Ohne Titel') }}</flux:text>
@endif
<flux:text class="text-xs text-zinc-500">
{{ $pressRelease->published_at?->format('d.m.Y') ?? $pressRelease->created_at?->format('d.m.Y') ?? '' }}
· {{ $pressRelease->portal?->label() ?? '—' }}
· {{ __('Hits') }}: {{ $pressRelease->hits }}
</flux:text>
</div>
<flux:badge :color="$this->pressReleaseStatusColor($pressRelease->status?->value)" size="sm">
{{ $pressRelease->status?->label() ?? $pressRelease->status?->value ?? '' }}
</flux:badge>
</div>
</div>
@endforeach
@if ($company->press_releases_count > $company->pressReleases->count())
<flux:text class="text-xs text-zinc-500">
{{ __(':count weitere Pressemitteilungen werden hier nicht geladen.', ['count' => $company->press_releases_count - $company->pressReleases->count()]) }}
</flux:text>
@endif
</div>
@else
<flux:text class="text-xs text-zinc-500">{{ __('Keine Pressemitteilungen bei dieser Firma') }}</flux:text>
@endif
</div>
</div>
</div>
@empty
<flux:text class="text-sm text-zinc-500">{{ __('Keine Firmen verknüpft') }}</flux:text>
@endforelse
</div>
</flux:card>
@else
<flux:heading size="lg">{{ __('Benutzer nicht gefunden') }}</flux:heading>
@endif
</div>
</flux:modal>
</div>