b2in/resources/views/livewire/admin/users.blade.php
2025-11-21 18:21:23 +01:00

352 lines
14 KiB
PHP

<?php
use App\Models\User;
use Livewire\Volt\Component;
use Livewire\WithPagination;
use function Livewire\Volt\{layout, title, state};
layout('components.layouts.app');
title('Users Management');
new class extends Component {
use WithPagination;
public string $search = '';
public string $roleFilter = '';
public string $sortField = 'name';
public string $sortDirection = 'asc';
// Modal state
public bool $showRoleModal = false;
public ?int $selectedUserId = null;
public array $selectedRoles = [];
public function with(): array
{
$query = User::with('roles')
->when($this->search, fn($q, $search) =>
$q->where('name', 'like', "%{$search}%")
->orWhere('email', 'like', "%{$search}%")
)
->when($this->roleFilter, fn($q, $role) =>
$q->whereHas('roles', fn($roleQuery) =>
$roleQuery->where('name', $role)
)
);
return [
'users' => $query->orderBy($this->sortField, $this->sortDirection)->paginate(15),
'totalUsers' => User::count(),
'verifiedUsers' => User::whereNotNull('email_verified_at')->count(),
'availableRoles' => \Spatie\Permission\Models\Role::orderBy('name')->get(),
'selectedUser' => $this->selectedUserId ? User::find($this->selectedUserId) : null,
];
}
public function sortBy(string $field): void
{
if ($this->sortField === $field) {
$this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc';
} else {
$this->sortField = $field;
$this->sortDirection = 'asc';
}
}
public function updatingSearch(): void
{
$this->resetPage();
}
public function updatingRoleFilter(): void
{
$this->resetPage();
}
public function openRoleModal(int $userId): void
{
$user = User::with('roles')->findOrFail($userId);
$this->selectedUserId = $userId;
$this->selectedRoles = $user->roles->pluck('name')->toArray();
$this->showRoleModal = true;
}
public function saveRoles(): void
{
if (!$this->selectedUserId) {
return;
}
$user = User::findOrFail($this->selectedUserId);
$user->syncRoles($this->selectedRoles);
$this->showRoleModal = false;
$this->selectedUserId = null;
$this->selectedRoles = [];
// Optional: Flash message
session()->flash('message', __('Roles updated successfully!'));
}
public function closeRoleModal(): void
{
$this->showRoleModal = false;
$this->selectedUserId = null;
$this->selectedRoles = [];
}
}; ?>
<div class="space-y-6 p-6">
{{-- Header --}}
<div class="flex items-center justify-between">
<div>
<flux:heading size="xl" class="mb-2">{{ __('Users Management') }}</flux:heading>
<flux:subheading>{{ __('Manage users and their roles in your application') }}</flux:subheading>
</div>
<div class="flex gap-2">
<flux:button variant="primary" icon="plus">{{ __('Create User') }}</flux:button>
</div>
</div>
{{-- Statistics --}}
<div class="grid grid-cols-1 gap-6 md:grid-cols-3">
<flux:card class="shadow-elegant">
<div class="flex items-center justify-between">
<div>
<flux:subheading>{{ __('Total Users') }}</flux:subheading>
<flux:heading size="2xl" class="mt-2">{{ $totalUsers }}</flux:heading>
</div>
<div class="flex h-12 w-12 items-center justify-center rounded-lg bg-accent-100 dark:bg-accent-900/20">
<flux:icon.users class="h-6 w-6 text-accent-600 dark:text-accent-400" />
</div>
</div>
</flux:card>
<flux:card class="shadow-elegant">
<div class="flex items-center justify-between">
<div>
<flux:subheading>{{ __('Verified Users') }}</flux:subheading>
<flux:heading size="2xl" class="mt-2">{{ $verifiedUsers }}</flux:heading>
</div>
<div class="flex h-12 w-12 items-center justify-center rounded-lg bg-accent-100 dark:bg-accent-900/20">
<flux:icon.shield-check class="h-6 w-6 text-accent-600 dark:text-accent-400" />
</div>
</div>
</flux:card>
<flux:card class="shadow-elegant">
<div class="flex items-center justify-between">
<div>
<flux:subheading>{{ __('Active Roles') }}</flux:subheading>
<flux:heading size="2xl" class="mt-2">{{ $availableRoles->count() }}</flux:heading>
</div>
<div class="flex h-12 w-12 items-center justify-center rounded-lg bg-accent-100 dark:bg-accent-900/20">
<flux:icon.user-group class="h-6 w-6 text-accent-600 dark:text-accent-400" />
</div>
</div>
</flux:card>
</div>
{{-- Filters --}}
<flux:card class="shadow-elegant">
<div class="grid grid-cols-1 gap-4 md:grid-cols-3">
<flux:input wire:model.live.debounce.300ms="search" icon="magnifying-glass" placeholder="{{ __('Search users...') }}" />
<flux:select wire:model.live="roleFilter" placeholder="{{ __('All Roles') }}">
<flux:select.option value="">{{ __('All Roles') }}</flux:select.option>
@foreach($availableRoles as $role)
<flux:select.option :value="$role->name">{{ $role->display_name ?? $role->name }}</flux:select.option>
@endforeach
</flux:select>
@if($search || $roleFilter)
<flux:button wire:click="$set('search', ''); $set('roleFilter', '')" variant="ghost" icon="x-mark">
{{ __('Clear Filters') }}
</flux:button>
@endif
</div>
</flux:card>
{{-- Users Table --}}
<flux:card class="shadow-elegant">
<flux:table>
<flux:table.columns>
<flux:table.column class="w-1/4" wire:click="sortBy('name')" class="cursor-pointer">
<div class="flex items-center gap-2">
{{ __('User') }}
@if($sortField === 'name')
<flux:icon.{{ $sortDirection === 'asc' ? 'chevron-up' : 'chevron-down' }} variant="micro" />
@endif
</div>
</flux:table.column>
<flux:table.column wire:click="sortBy('email')" class="cursor-pointer">
<div class="flex items-center gap-2">
{{ __('Email') }}
@if($sortField === 'email')
<flux:icon.{{ $sortDirection === 'asc' ? 'chevron-up' : 'chevron-down' }} variant="micro" />
@endif
</div>
</flux:table.column>
<flux:table.column>{{ __('Roles') }}</flux:table.column>
<flux:table.column class="w-32">{{ __('Status') }}</flux:table.column>
<flux:table.column class="w-32">{{ __('Actions') }}</flux:table.column>
</flux:table.columns>
<flux:table.rows>
@forelse($users as $user)
<flux:table.row :key="$user->id" class="odd:bg-zinc-50 dark:odd:bg-zinc-800/30">
<flux:table.cell>
<div class="flex items-center gap-3 pl-2">
<div class="flex h-10 w-10 items-center justify-center rounded-lg bg-accent-100 text-accent-600 dark:bg-accent-900/20 dark:text-accent-400 font-semibold">
{{ $user->initials() }}
</div>
<div>
<div class="font-semibold text-zinc-900 dark:text-white">{{ $user->name }}</div>
<div class="text-xs text-zinc-500 dark:text-zinc-400">
{{ __('ID:') }} {{ $user->id }}
</div>
</div>
</div>
</flux:table.cell>
<flux:table.cell>
<div class="flex items-center gap-2">
<flux:icon.envelope variant="micro" class="text-zinc-400" />
<span>{{ $user->email }}</span>
</div>
</flux:table.cell>
<flux:table.cell>
<div class="flex flex-wrap gap-1.5">
<button
type="button"
wire:click="openRoleModal({{ $user->id }})"
class="flex flex-wrap gap-1.5 cursor-pointer hover:opacity-80 transition-opacity"
>
@forelse($user->roles as $role)
<flux:badge size="sm" :color="$role->color ?? 'zinc'">
@svg('heroicon-o-'.$role->icon, 'w-5 h-5') &nbsp; {{ $role->display_name ?? $role->name }}
</flux:badge>
@empty
<flux:badge size="sm" color="zinc" icon="plus">{{ __('Assign Role') }}</flux:badge>
@endforelse
</button>
</div>
</flux:table.cell>
<flux:table.cell>
@if($user->email_verified_at)
<flux:badge size="sm" color="green" icon="check-circle">
{{ __('Verified') }}
</flux:badge>
@else
<flux:badge size="sm" color="zinc" icon="exclamation-circle">
{{ __('Unverified') }}
</flux:badge>
@endif
</flux:table.cell>
<flux:table.cell>
<div class="flex gap-2">
<flux:button size="sm" variant="ghost" icon="pencil"
tooltip="{{ __('Edit User') }}"></flux:button>
<flux:button size="sm" variant="ghost" icon="trash"
tooltip="{{ __('Delete User') }}"></flux:button>
</div>
</flux:table.cell>
</flux:table.row>
@empty
<flux:table.row>
<flux:table.cell colspan="5">
<div class="py-12 text-center">
<flux:icon.users variant="outline" class="mx-auto h-12 w-12 text-zinc-400" />
<flux:heading size="lg" class="mt-4">{{ __('No users found') }}</flux:heading>
<flux:subheading class="mt-2">
@if($search || $roleFilter)
{{ __('Try adjusting your filters.') }}
@else
{{ __('Get started by creating a new user.') }}
@endif
</flux:subheading>
</div>
</flux:table.cell>
</flux:table.row>
@endforelse
</flux:table.rows>
</flux:table>
{{-- Pagination --}}
@if($users->hasPages())
<div class="border-t border-zinc-200 px-6 py-4 dark:border-zinc-700">
{{ $users->links() }}
</div>
@endif
</flux:card>
{{-- Role Assignment Modal --}}
<flux:modal name="role-modal" :variant="'flyout'" wire:model="showRoleModal">
<form wire:submit="saveRoles" class="space-y-6">
<div>
<flux:heading size="lg">{{ __('Assign Roles') }}</flux:heading>
<flux:subheading>
@if($selectedUser)
{{ __('Managing roles for') }} <strong>{{ $selectedUser->name }}</strong>
@endif
</flux:subheading>
</div>
<flux:separator />
<div class="space-y-4">
<flux:field>
<flux:label>{{ __('Roles') }}</flux:label>
<flux:description>{{ __('Select one or multiple roles for this user') }}</flux:description>
<div class="space-y-2 mt-3">
@foreach($availableRoles as $role)
<flux:checkbox
wire:model="selectedRoles"
:value="$role->name"
:label="$role->display_name ?? $role->name"
/>
@endforeach
</div>
</flux:field>
@if(!empty($selectedRoles))
<div class="rounded-lg bg-zinc-50 p-4 dark:bg-zinc-800/50">
<flux:subheading class="mb-2">{{ __('Selected Roles:') }}</flux:subheading>
<div class="flex flex-wrap gap-2">
@foreach($selectedRoles as $roleName)
@php
$roleObj = $availableRoles->firstWhere('name', $roleName);
@endphp
<flux:badge size="sm" :color="$roleObj?->color ?? 'zinc'">
{{ $roleName }}
</flux:badge>
@endforeach
</div>
</div>
@endif
</div>
<flux:separator />
<div class="flex justify-between gap-2">
<flux:button type="button" variant="ghost" wire:click="closeRoleModal">
{{ __('Cancel') }}
</flux:button>
<flux:button type="submit" variant="primary">
{{ __('Save Roles') }}
</flux:button>
</div>
</form>
</flux:modal>
{{-- Success Message --}}
@if (session()->has('message'))
<flux:toast :variant="'success'">
{{ session('message') }}
</flux:toast>
@endif
</div>