diff --git a/app/Livewire/RoleForm.php b/app/Livewire/RoleForm.php index ed03f3d..3864741 100644 --- a/app/Livewire/RoleForm.php +++ b/app/Livewire/RoleForm.php @@ -6,7 +6,6 @@ use Livewire\Component; use Livewire\Attributes\Layout; use Illuminate\Support\Facades\Auth; use Spatie\Permission\Models\Role; -use Spatie\Permission\Models\Permission; use Spatie\Permission\PermissionRegistrar; #[Layout('layouts.app')] @@ -16,7 +15,6 @@ class RoleForm extends Component public string $name = ''; public string $description = ''; - public array $rolePermissions = []; private const PROTECTED_ROLES = ['Admin']; private const CORE_PERMISSION = 'manage all'; @@ -26,10 +24,9 @@ class RoleForm extends Component abort_unless(Auth::user()?->can(self::CORE_PERMISSION), 403); if ($role && $role->exists) { - $this->role = $role; - $this->name = $role->name; - $this->description = $role->description ?? ''; - $this->rolePermissions = $role->permissions->pluck('name')->toArray(); + $this->role = $role; + $this->name = $role->name; + $this->description = $role->description ?? ''; } } @@ -41,26 +38,17 @@ class RoleForm extends Component ], [], ['name' => 'nombre', 'description' => 'descripción']); if ($this->role) { - $isProtected = in_array($this->role->name, self::PROTECTED_ROLES, true); - if (! $isProtected) { + // Protected roles can't be renamed + if (! in_array($this->role->name, self::PROTECTED_ROLES, true)) { $this->role->name = $this->name; } $this->role->description = $this->description ?: null; $this->role->save(); - - $perms = $this->rolePermissions; - if ($this->role->name === 'Admin' && ! in_array(self::CORE_PERMISSION, $perms, true)) { - $perms[] = self::CORE_PERMISSION; - } - $this->role->syncPermissions($perms); } else { - $role = Role::create([ + Role::create([ 'name' => $this->name, 'description' => $this->description ?: null, ]); - if (! empty($this->rolePermissions)) { - $role->syncPermissions($this->rolePermissions); - } } app(PermissionRegistrar::class)->forgetCachedPermissions(); @@ -72,7 +60,6 @@ class RoleForm extends Component public function render() { return view('livewire.roles.role-form', [ - 'permissions' => Permission::orderBy('name')->get(), 'isProtected' => $this->role && in_array($this->role->name, self::PROTECTED_ROLES, true), ]); } diff --git a/app/Livewire/RoleManager.php b/app/Livewire/RoleManager.php deleted file mode 100644 index c62cff5..0000000 --- a/app/Livewire/RoleManager.php +++ /dev/null @@ -1,95 +0,0 @@ -can(self::CORE_PERMISSION), 403); - } - - private function flushCache(): void - { - app(PermissionRegistrar::class)->forgetCachedPermissions(); - } - - public function updatedSelectAll($value): void - { - $this->selected = $value - ? Role::pluck('id')->map(fn ($id) => (string) $id)->toArray() - : []; - } - - // ── View ───────────────────────────────────────────────────────────────── - - public function openView(int $id): void - { - $this->viewingRole = $id; - } - - public function closeView(): void - { - $this->viewingRole = null; - } - - // ── Delete (single / bulk) ───────────────────────────────────────────────── - - public function delete(int $id): void - { - $role = Role::findOrFail($id); - if (in_array($role->name, self::PROTECTED_ROLES, true)) { - $this->dispatch('notify', "El rol '{$role->name}' está protegido y no se puede borrar."); - return; - } - $role->delete(); - $this->selected = array_values(array_diff($this->selected, [(string) $id, $id])); - $this->flushCache(); - $this->dispatch('notify', 'Rol eliminado'); - } - - public function bulkDelete(): void - { - $roles = Role::whereIn('id', $this->selected)->get(); - $deleted = 0; - $skipped = 0; - foreach ($roles as $role) { - if (in_array($role->name, self::PROTECTED_ROLES, true)) { $skipped++; continue; } - $role->delete(); - $deleted++; - } - $this->selected = []; - $this->selectAll = false; - $this->flushCache(); - $msg = "{$deleted} rol(es) eliminados"; - if ($skipped) $msg .= " ({$skipped} protegido(s) omitido(s))"; - $this->dispatch('notify', $msg); - } - - public function render() - { - return view('livewire.role-manager', [ - 'roles' => Role::with('permissions')->withCount('users')->orderBy('name')->get(), - 'viewing' => $this->viewingRole - ? Role::with('permissions')->withCount('users')->find($this->viewingRole) - : null, - ]); - } -} diff --git a/app/Livewire/RoleTable.php b/app/Livewire/RoleTable.php new file mode 100644 index 0000000..f2f595d --- /dev/null +++ b/app/Livewire/RoleTable.php @@ -0,0 +1,105 @@ +setPrimaryKey('id') + ->setDefaultSort('name', 'asc') + ->setSortingPillsEnabled(false); + } + + public function builder(): Builder + { + return Role::withCount(['permissions', 'users']); + } + + public function columns(): array + { + return [ + Column::make(__('Name'), 'name') + ->sortable() + ->searchable() + ->format(fn ($value, $row) => + ''.e($value).'' + . (in_array($row->name, self::PROTECTED_ROLES, true) ? ' protegido' : '') + ) + ->html(), + + Column::make(__('Description'), 'description') + ->sortable() + ->searchable() + ->format(fn ($value) => $value + ? ''.e($value).'' + : '') + ->html(), + + Column::make(__('Permissions')) + ->label(fn ($row) => ''.(int) $row->permissions_count.'') + ->html(), + + Column::make(__('Users')) + ->label(fn ($row) => ''.(int) $row->users_count.'') + ->html(), + + Column::make(__('Actions')) + ->label(function ($row) { + $show = route('admin.roles.show', $row->id); + $edit = route('admin.roles.edit', $row->id); + $eye = ''; + $pencil = ''; + $trash = ''; + + $html = '
'; + $html .= ''.$eye.''; + $html .= ''.$pencil.''; + if (! in_array($row->name, self::PROTECTED_ROLES, true)) { + $html .= ''; + } + $html .= '
'; + return $html; + }) + ->html(), + ]; + } + + public function bulkActions(): array + { + return ['bulkDelete' => __('Delete selected')]; + } + + public function bulkDelete(): void + { + $roles = Role::whereIn('id', $this->selected)->get(); + foreach ($roles as $role) { + if (in_array($role->name, self::PROTECTED_ROLES, true)) continue; + $role->delete(); + } + $this->clearSelected(); + app(PermissionRegistrar::class)->forgetCachedPermissions(); + $this->dispatch('notify', __('Roles deleted')); + } + + public function deleteRole(int $id): void + { + $role = Role::findOrFail($id); + if (in_array($role->name, self::PROTECTED_ROLES, true)) { + return; + } + $role->delete(); + app(PermissionRegistrar::class)->forgetCachedPermissions(); + $this->dispatch('notify', __('Role deleted')); + } +} diff --git a/app/Livewire/RoleView.php b/app/Livewire/RoleView.php new file mode 100644 index 0000000..f61c413 --- /dev/null +++ b/app/Livewire/RoleView.php @@ -0,0 +1,95 @@ +can(self::CORE_PERMISSION), 403); + $this->role = $role; + } + + public function setTab(string $tab): void + { + $this->tab = in_array($tab, ['ficha', 'permisos'], true) ? $tab : 'ficha'; + } + + public function togglePermission(string $permissionName): void + { + // Admin must always keep the core permission + if ($this->role->name === 'Admin' + && $permissionName === self::CORE_PERMISSION + && $this->role->hasPermissionTo($permissionName)) { + $this->dispatch('notify', "El rol Admin no puede perder '" . self::CORE_PERMISSION . "'."); + return; + } + + if ($this->role->hasPermissionTo($permissionName)) { + $this->role->revokePermissionTo($permissionName); + } else { + $this->role->givePermissionTo($permissionName); + } + + app(PermissionRegistrar::class)->forgetCachedPermissions(); + $this->role->load('permissions'); + $this->dispatch('notify', 'Permisos actualizados'); + } + + public function delete() + { + if (in_array($this->role->name, self::PROTECTED_ROLES, true)) { + $this->dispatch('notify', "El rol '{$this->role->name}' está protegido y no se puede borrar."); + return; + } + $this->role->delete(); + app(PermissionRegistrar::class)->forgetCachedPermissions(); + session()->flash('message', 'Rol eliminado.'); + + return $this->redirect(route('admin.roles'), navigate: true); + } + + /** Section title for a permission name (groups by the resource / last word). */ + private function sectionFor(string $name): string + { + if ($name === self::CORE_PERMISSION) { + return 'General'; + } + $resource = Str::afterLast($name, ' '); + return Str::headline($resource ?: 'General'); + } + + public function render() + { + $users = $this->role->users() + ->orderBy('first_name') + ->orderBy('name') + ->get(); + + $grouped = Permission::orderBy('name')->get() + ->groupBy(fn ($perm) => $this->sectionFor($perm->name)) + ->sortKeys(); + + return view('livewire.roles.role-view', [ + 'users' => $users, + 'grouped' => $grouped, + 'rolePerms' => $this->role->permissions->pluck('name')->toArray(), + 'isProtected' => in_array($this->role->name, self::PROTECTED_ROLES, true), + ]); + } +} diff --git a/resources/views/admin/roles.blade.php b/resources/views/admin/roles.blade.php new file mode 100644 index 0000000..19fe7cc --- /dev/null +++ b/resources/views/admin/roles.blade.php @@ -0,0 +1,25 @@ + + +
+

+ {{ __('Roles & permissions') }} +

+ +
+
+ +
+
+
+ +
+
+
+
diff --git a/resources/views/livewire/role-manager.blade.php b/resources/views/livewire/role-manager.blade.php deleted file mode 100644 index 4ac11a7..0000000 --- a/resources/views/livewire/role-manager.blade.php +++ /dev/null @@ -1,126 +0,0 @@ -
- - {{-- Cabecera --}} -
-
-

{{ __('Roles & permissions') }}

-

{{ __('Manage role groups and the permissions assigned to each.') }}

-
- -
- - {{-- Barra de acciones en grupo --}} - @if(count($selected) > 0) -
- {{ count($selected) }} {{ __('selected') }} - -
- @endif - - {{-- Tabla de roles --}} -
- - - - - - - - - - - - - @forelse($roles as $role) - - - - - - - - - @empty - - @endforelse - -
- - {{ __('Name') }}{{ __('Description') }}{{ __('Permissions') }}{{ __('Users') }}{{ __('Actions') }}
- - - {{ $role->name }} - @if(in_array($role->name, ['Admin'], true)) - {{ __('protected') }} - @endif - {{ $role->description ?: '—' }} - {{ $role->permissions->count() }} - - {{ $role->users_count }} - -
- - - - - @unless(in_array($role->name, ['Admin'], true)) - - @endunless -
-
{{ __('No roles') }}
-
- - {{-- ════════════════ MODAL VER ════════════════ --}} - @if($viewing) - - @endif -
diff --git a/resources/views/livewire/roles/role-form.blade.php b/resources/views/livewire/roles/role-form.blade.php index 6b1580b..7dfcd1d 100644 --- a/resources/views/livewire/roles/role-form.blade.php +++ b/resources/views/livewire/roles/role-form.blade.php @@ -42,23 +42,9 @@ - {{-- Permisos --}} -
- -
-
- @foreach($permissions as $perm) - - @endforeach -
-
-
+

+ {{ __('Permissions are assigned from the role view, in the "Permissions" tab.') }} +

{{ __('Cancel') }} diff --git a/resources/views/livewire/roles/role-view.blade.php b/resources/views/livewire/roles/role-view.blade.php new file mode 100644 index 0000000..0746027 --- /dev/null +++ b/resources/views/livewire/roles/role-view.blade.php @@ -0,0 +1,128 @@ +
+ + {{-- Cabecera: nombre del rol + botón Volver --}} +
+

+ + {{ $role->name }} + @if($isProtected) + {{ __('protected') }} + @endif +

+ + {{ __('Back') }} + +
+ + {{-- Tabs --}} +
+ + +
+ + {{-- ═══════════════ TAB FICHA ═══════════════ --}} + @if($tab === 'ficha') +
+
+
+

{{ __('Description') }}

+

{{ $role->description ?: __('No description') }}

+
+
+ + {{ __('Edit') }} + + @unless($isProtected) + + @endunless +
+
+
+ + {{-- Usuarios con este rol --}} +
+
+

{{ __('Users with this role') }} ({{ $users->count() }})

+
+
+ + + + + + + + + + @forelse($users as $u) + + + + + + @empty + + @endforelse + +
{{ __('Name') }}{{ __('Last name') }}{{ __('Status') }}
+
+
+
+ {{ strtoupper(mb_substr($u->first_name ?: $u->name, 0, 1)) }} +
+
+
+

{{ $u->first_name ?: $u->name }}

+

{{ $u->email }}

+
+
+
{{ $u->last_name ?: '—' }} + @php + [$cls, $label] = match($u->status) { + 'active' => ['badge-success', __('Active')], + 'inactive' => ['badge-ghost', __('Inactive')], + 'suspended' => ['badge-error', __('Suspended')], + default => ['badge-ghost', ucfirst((string) $u->status)], + }; + @endphp + {{ $label }} +
{{ __('No users with this role') }}
+
+
+ @endif + + {{-- ═══════════════ TAB PERMISOS ═══════════════ --}} + @if($tab === 'permisos') +
+ @forelse($grouped as $section => $perms) +
+

+ {{ $section }} +

+
+ @foreach($perms as $perm) + + @endforeach +
+
+ @empty +

{{ __('No permissions') }}

+ @endforelse +
+ @endif +
diff --git a/routes/web.php b/routes/web.php index 53c3b3b..ddeef43 100644 --- a/routes/web.php +++ b/routes/web.php @@ -136,9 +136,10 @@ Route::get('/reports/dashboard', ReportsDashboard::class)->name('reports.dashboa Route::get('/users/create', \App\Livewire\UserForm::class)->name('users.create'); Route::get('/users/{user}', \App\Livewire\UserView::class)->name('users.show'); Route::get('/users/{user}/edit', \App\Livewire\UserForm::class)->name('users.edit'); - Route::get('/roles', \App\Livewire\RoleManager::class)->name('roles'); + Route::get('/roles', function () { return view('admin.roles'); })->name('roles'); Route::get('/roles/create', \App\Livewire\RoleForm::class)->name('roles.create'); Route::get('/roles/{role}/edit', \App\Livewire\RoleForm::class)->name('roles.edit'); + Route::get('/roles/{role}', \App\Livewire\RoleView::class)->name('roles.show'); Route::get('/permissions', \App\Livewire\RolePermissionManager::class)->name('permissions'); });