478 lines
25 KiB
PHP
478 lines
25 KiB
PHP
<?php
|
|
|
|
use Spatie\Permission\Models\Role;
|
|
use Spatie\Permission\Models\Permission;
|
|
use Livewire\Volt\Component;
|
|
use function Livewire\Volt\{layout, title};
|
|
|
|
title('Permissions & Roles');
|
|
|
|
new class extends Component {
|
|
public $activeTab = 'roles';
|
|
|
|
// Modal state for editing roles
|
|
public bool $showEditModal = false;
|
|
public ?int $selectedRoleId = null;
|
|
public string $roleName = '';
|
|
public string $roleDisplayName = '';
|
|
public string $roleIcon = 'shield-check';
|
|
public string $roleColor = 'zinc';
|
|
public array $rolePermissions = [];
|
|
|
|
public function with(): array
|
|
{
|
|
return [
|
|
'roles' => Role::with('permissions')->orderBy('name')->get(),
|
|
'permissions' => Permission::with('roles')->orderBy('name')->get(),
|
|
'allPermissions' => Permission::orderBy('name')->get(),
|
|
'selectedRole' => $this->selectedRoleId ? Role::find($this->selectedRoleId) : null,
|
|
];
|
|
}
|
|
|
|
public function switchTab(string $tab): void
|
|
{
|
|
\Log::info('Switching tab to: ' . $tab);
|
|
$this->activeTab = $tab;
|
|
\Log::info('Active tab: ' . $this->activeTab);
|
|
}
|
|
|
|
public function openEditModal(int $roleId): void
|
|
{
|
|
$role = Role::with('permissions')->findOrFail($roleId);
|
|
$this->selectedRoleId = $roleId;
|
|
$this->roleName = $role->name;
|
|
$this->roleDisplayName = $role->display_name ?? $role->name;
|
|
$this->roleIcon = $role->icon ?? 'shield-check';
|
|
$this->roleColor = $role->color ?? 'zinc';
|
|
$this->rolePermissions = $role->permissions->pluck('name')->toArray();
|
|
$this->roleIcon = $role->icon ?? 'shield-check';
|
|
$this->showEditModal = true;
|
|
}
|
|
|
|
public function saveRole(): void
|
|
{
|
|
if (!$this->selectedRoleId) {
|
|
return;
|
|
}
|
|
|
|
$this->validate([
|
|
'roleName' => 'required|string|max:255',
|
|
'roleColor' => 'required|string|max:50',
|
|
]);
|
|
|
|
$role = Role::findOrFail($this->selectedRoleId);
|
|
$role->update([
|
|
'name' => $this->roleName,
|
|
'display_name' => $this->roleDisplayName,
|
|
'icon' => $this->roleIcon,
|
|
'color' => $this->roleColor,
|
|
]);
|
|
|
|
$role->syncPermissions($this->rolePermissions);
|
|
|
|
$this->showEditModal = false;
|
|
$this->reset(['selectedRoleId', 'roleName', 'roleDisplayName', 'roleIcon', 'roleColor', 'rolePermissions']);
|
|
|
|
session()->flash('message', __('Role updated successfully!'));
|
|
}
|
|
|
|
public function closeEditModal(): void
|
|
{
|
|
$this->showEditModal = false;
|
|
$this->reset(['selectedRoleId', 'roleName', 'roleDisplayName', 'roleIcon', 'roleColor', 'rolePermissions']);
|
|
}
|
|
}; ?>
|
|
|
|
<div class="space-y-6 p-6">
|
|
{{-- Header --}}
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<flux:heading size="xl" class="mb-2">{{ __('Permissions & Roles Management') }}</flux:heading>
|
|
<flux:subheading>{{ __('Manage roles and permissions for your application') }}</flux:subheading>
|
|
</div>
|
|
<div class="flex gap-2">
|
|
<flux:button variant="primary" icon="plus">{{ __('Create Role') }}</flux:button>
|
|
<flux:button variant="ghost" icon="shield-check">{{ __('Create Permission') }}</flux:button>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Tabs --}}
|
|
<flux:tabs wire:model.live="activeTab">
|
|
<flux:tab name="roles" icon="user-group">{{ __('Roles Overview') }}</flux:tab>
|
|
<flux:tab name="permissions" icon="shield-check">{{ __('Permissions Overview') }}</flux:tab>
|
|
</flux:tabs>
|
|
|
|
{{-- Roles Tab Content --}}
|
|
@if($activeTab === 'roles')
|
|
<div class="space-y-6">
|
|
<flux:card class="shadow-elegant">
|
|
<flux:table>
|
|
<flux:table.columns>
|
|
<flux:table.column class="w-1/5">{{ __('Role') }}</flux:table.column>
|
|
<flux:table.column>{{ __('Permissions') }}</flux:table.column>
|
|
<flux:table.column class="w-24">{{ __('Count') }}</flux:table.column>
|
|
<flux:table.column class="w-32">{{ __('Actions') }}</flux:table.column>
|
|
</flux:table.columns>
|
|
|
|
<flux:table.rows>
|
|
@forelse($roles as $role)
|
|
<flux:table.row :key="$role->id" class="odd:bg-zinc-50 dark:odd:bg-zinc-800/30">
|
|
<flux:table.cell>
|
|
<div class="flex items-center gap-3 pl-2">
|
|
@php
|
|
$colorClasses = match($role->color ?? 'zinc') {
|
|
'red' => 'bg-red-100 text-red-600 dark:bg-red-900/20 dark:text-red-400',
|
|
'accent' => 'bg-accent-100 text-accent-600 dark:bg-accent-900/20 dark:text-accent-400',
|
|
'blue' => 'bg-blue-100 text-blue-600 dark:bg-blue-900/20 dark:text-blue-400',
|
|
'lime' => 'bg-lime-100 text-lime-600 dark:bg-lime-900/20 dark:text-lime-400',
|
|
'teal' => 'bg-teal-100 text-teal-600 dark:bg-teal-900/20 dark:text-teal-400',
|
|
'orange' => 'bg-orange-100 text-orange-600 dark:bg-orange-900/20 dark:text-orange-400',
|
|
'purple' => 'bg-purple-100 text-purple-600 dark:bg-purple-900/20 dark:text-purple-400',
|
|
'indigo' => 'bg-indigo-100 text-indigo-600 dark:bg-indigo-900/20 dark:text-indigo-400',
|
|
'pink' => 'bg-pink-100 text-pink-600 dark:bg-pink-900/20 dark:text-pink-400',
|
|
'green' => 'bg-green-100 text-green-600 dark:bg-green-900/20 dark:text-green-400',
|
|
'yellow' => 'bg-yellow-100 text-yellow-600 dark:bg-yellow-900/20 dark:text-yellow-400',
|
|
default => 'bg-zinc-100 text-zinc-600 dark:bg-zinc-800 dark:text-zinc-400',
|
|
};
|
|
@endphp
|
|
<div class="flex h-10 w-10 items-center justify-center rounded-lg {{ $colorClasses }}">
|
|
@if($role->icon)
|
|
@svg('heroicon-o-'.$role->icon, 'w-5 h-5')
|
|
@else
|
|
@svg('heroicon-o-shield-check', 'w-5 h-5')
|
|
@endif
|
|
|
|
</div>
|
|
<div>
|
|
<div class="font-semibold text-zinc-900 dark:text-white">
|
|
{{ $role->display_name ?? $role->name }}
|
|
</div>
|
|
<div class="text-xs text-zinc-500 dark:text-zinc-400">
|
|
{{ $role->name }} • {{ __('Guard:') }} {{ $role->guard_name }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</flux:table.cell>
|
|
|
|
<flux:table.cell>
|
|
<div class="flex flex-wrap gap-1.5">
|
|
@if($role->name === 'Super-Admin')
|
|
<flux:badge size="sm" :color="$role->color ?? 'red'" icon="star">{{ __('All Permissions') }}</flux:badge>
|
|
@elseif($role->permissions->isEmpty())
|
|
<flux:badge size="sm" color="zinc">{{ __('No permissions') }}</flux:badge>
|
|
@else
|
|
@foreach($role->permissions->take(5) as $permission)
|
|
<flux:badge size="sm" color="zinc">{{ $permission->name }}</flux:badge>
|
|
@endforeach
|
|
@if($role->permissions->count() > 5)
|
|
<flux:badge size="sm" color="accent">
|
|
+{{ $role->permissions->count() - 5 }} {{ __('more') }}
|
|
</flux:badge>
|
|
@endif
|
|
@endif
|
|
</div>
|
|
</flux:table.cell>
|
|
|
|
<flux:table.cell>
|
|
<div class="text-center font-mono text-sm font-semibold text-zinc-900 dark:text-white">
|
|
@if($role->name === 'Super-Admin')
|
|
∞
|
|
@else
|
|
{{ $role->permissions->count() }}
|
|
@endif
|
|
</div>
|
|
</flux:table.cell>
|
|
|
|
<flux:table.cell>
|
|
<div class="flex gap-2">
|
|
<flux:button size="sm" variant="ghost" icon="eye"
|
|
tooltip="{{ __('View Details') }}"></flux:button>
|
|
<flux:button size="sm" variant="ghost" icon="pencil"
|
|
wire:click="openEditModal({{ $role->id }})"
|
|
tooltip="{{ __('Edit Role') }}"></flux:button>
|
|
</div>
|
|
</flux:table.cell>
|
|
</flux:table.row>
|
|
@empty
|
|
<flux:table.row>
|
|
<flux:table.cell colspan="4">
|
|
<div class="py-12 text-center">
|
|
<flux:icon.shield-exclamation variant="outline" class="mx-auto h-12 w-12 text-zinc-400" />
|
|
<flux:heading size="lg" class="mt-4">{{ __('No roles found') }}</flux:heading>
|
|
<flux:subheading class="mt-2">
|
|
{{ __('Get started by creating a new role.') }}
|
|
</flux:subheading>
|
|
</div>
|
|
</flux:table.cell>
|
|
</flux:table.row>
|
|
@endforelse
|
|
</flux:table.rows>
|
|
</flux:table>
|
|
</flux:card>
|
|
|
|
{{-- Role 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 Roles') }}</flux:subheading>
|
|
<flux:heading size="2xl" class="mt-2">{{ $roles->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>
|
|
|
|
<flux:card class="shadow-elegant">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<flux:subheading>{{ __('Total Permissions') }}</flux:subheading>
|
|
<flux:heading size="2xl" class="mt-2">{{ $permissions->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.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>{{ __('Avg. Permissions/Role') }}</flux:subheading>
|
|
<flux:heading size="2xl" class="mt-2">
|
|
{{ $roles->count() > 0 ? number_format($roles->sum(fn($r) => $r->permissions->count()) / $roles->count(), 1) : 0 }}
|
|
</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.chart-bar class="h-6 w-6 text-accent-600 dark:text-accent-400" />
|
|
</div>
|
|
</div>
|
|
</flux:card>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
{{-- Permissions Tab Content --}}
|
|
@if($activeTab === 'permissions')
|
|
<div class="space-y-6">
|
|
<flux:card class="shadow-elegant">
|
|
<flux:table>
|
|
<flux:table.columns>
|
|
<flux:table.column class="w-1/4">{{ __('Permission') }}</flux:table.column>
|
|
<flux:table.column>{{ __('Assigned to Roles') }}</flux:table.column>
|
|
<flux:table.column class="w-32">{{ __('Role Count') }}</flux:table.column>
|
|
<flux:table.column class="w-32">{{ __('Actions') }}</flux:table.column>
|
|
</flux:table.columns>
|
|
|
|
<flux:table.rows>
|
|
@php
|
|
$groupedPermissions = $permissions->groupBy(function($permission) {
|
|
return explode(' ', $permission->name)[1] ?? 'other';
|
|
});
|
|
@endphp
|
|
|
|
@forelse($groupedPermissions as $group => $groupPermissions)
|
|
{{-- Group Header --}}
|
|
<flux:table.row>
|
|
<flux:table.cell colspan="4" class="bg-zinc-50 dark:bg-zinc-800/50">
|
|
<div class="flex items-center gap-2 py-1 pl-2">
|
|
<flux:icon.folder class="h-5 w-5 text-zinc-500" />
|
|
<flux:heading size="sm" class="uppercase tracking-wide text-zinc-600 dark:text-zinc-400">
|
|
{{ ucfirst($group) }} {{ __('Permissions') }} ({{ $groupPermissions->count() }})
|
|
</flux:heading>
|
|
</div>
|
|
</flux:table.cell>
|
|
</flux:table.row>
|
|
|
|
{{-- Group Permissions --}}
|
|
@foreach($groupPermissions as $permission)
|
|
<flux:table.row :key="$permission->id" class="odd:bg-zinc-50 dark:odd:bg-zinc-800/30">
|
|
<flux:table.cell>
|
|
<div class="flex items-center gap-3 pl-8">
|
|
<div class="flex h-8 w-8 items-center justify-center rounded-lg bg-accent-100 dark:bg-accent-900/20">
|
|
<flux:icon.key variant="micro" class="text-accent-600 dark:text-accent-400" />
|
|
</div>
|
|
<div>
|
|
<div class="font-medium text-zinc-900 dark:text-white">{{ $permission->name }}</div>
|
|
<div class="text-xs text-zinc-500 dark:text-zinc-400">
|
|
{{ __('Guard:') }} {{ $permission->guard_name }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</flux:table.cell>
|
|
|
|
<flux:table.cell>
|
|
<div class="flex flex-wrap gap-1.5">
|
|
@if($permission->roles->isEmpty())
|
|
<flux:badge size="sm" color="zinc" icon="exclamation-triangle">
|
|
{{ __('Not assigned') }}
|
|
</flux:badge>
|
|
@else
|
|
@foreach($permission->roles as $role)
|
|
<flux:badge size="sm" :color="$role->color ?? 'zinc'">
|
|
{{ $role->name }}
|
|
</flux:badge>
|
|
@endforeach
|
|
@endif
|
|
</div>
|
|
</flux:table.cell>
|
|
|
|
<flux:table.cell>
|
|
<div class="text-center font-mono text-sm font-semibold text-zinc-900 dark:text-white">
|
|
{{ $permission->roles->count() }}
|
|
</div>
|
|
</flux:table.cell>
|
|
|
|
<flux:table.cell>
|
|
<div class="flex gap-2">
|
|
<flux:button size="sm" variant="ghost" icon="pencil"
|
|
tooltip="{{ __('Edit Permission') }}"></flux:button>
|
|
<flux:button size="sm" variant="ghost" icon="trash"
|
|
tooltip="{{ __('Delete Permission') }}"></flux:button>
|
|
</div>
|
|
</flux:table.cell>
|
|
</flux:table.row>
|
|
@endforeach
|
|
@empty
|
|
<flux:table.row>
|
|
<flux:table.cell colspan="4">
|
|
<div class="py-12 text-center">
|
|
<flux:icon.shield-exclamation variant="outline" class="mx-auto h-12 w-12 text-zinc-400" />
|
|
<flux:heading size="lg" class="mt-4">{{ __('No permissions found') }}</flux:heading>
|
|
<flux:subheading class="mt-2">
|
|
{{ __('Get started by creating a new permission.') }}
|
|
</flux:subheading>
|
|
</div>
|
|
</flux:table.cell>
|
|
</flux:table.row>
|
|
@endforelse
|
|
</flux:table.rows>
|
|
</flux:table>
|
|
</flux:card>
|
|
</div>
|
|
@endif
|
|
|
|
{{-- Edit Role Modal --}}
|
|
<flux:modal name="edit-role-modal" :variant="'flyout'" wire:model="showEditModal">
|
|
<form wire:submit="saveRole" class="space-y-6 max-w-2xl">
|
|
<div>
|
|
<flux:heading size="lg">{{ __('Edit Role') }}</flux:heading>
|
|
<flux:subheading>
|
|
@if($selectedRole)
|
|
{{ __('Editing role') }}: <strong>{{ $selectedRole->name }}</strong>
|
|
@endif
|
|
</flux:subheading>
|
|
</div>
|
|
|
|
<flux:separator />
|
|
|
|
<div class="space-y-4">
|
|
<flux:field>
|
|
<flux:label>{{ __('Role Name') }}</flux:label>
|
|
<flux:input wire:model="roleName" placeholder="{{ __('Enter role name') }}" />
|
|
@error('roleName') <flux:error>{{ $message }}</flux:error> @enderror
|
|
</flux:field>
|
|
|
|
<flux:field>
|
|
<flux:label>{{ __('Role Icon') }}</flux:label>
|
|
<flux:input wire:model="roleIcon" placeholder="{{ __('Enter role icon') }}" />
|
|
@error('roleIcon') <flux:error>{{ $message }}</flux:error> @enderror
|
|
</flux:field>
|
|
|
|
<flux:field>
|
|
<flux:label>{{ __('Role Display Name') }}</flux:label>
|
|
<flux:input wire:model="roleDisplayName" placeholder="{{ __('Enter role display name') }}" />
|
|
@error('roleDisplayName') <flux:error>{{ $message }}</flux:error> @enderror
|
|
</flux:field>
|
|
|
|
<flux:field>
|
|
<flux:label>{{ __('Color') }}</flux:label>
|
|
<flux:description>{{ __('Select a color for this role') }}</flux:description>
|
|
<flux:select wire:model.live="roleColor">
|
|
<flux:select.option value="red">{{ __('Red') }}</flux:select.option>
|
|
<flux:select.option value="orange">{{ __('Orange') }}</flux:select.option>
|
|
<flux:select.option value="lime">{{ __('Lime') }}</flux:select.option>
|
|
<flux:select.option value="teal">{{ __('Teal') }}</flux:select.option>
|
|
<flux:select.option value="indigo">{{ __('Indigo') }}</flux:select.option>
|
|
<flux:select.option value="purple">{{ __('Purple') }}</flux:select.option>
|
|
<flux:select.option value="pink">{{ __('Pink') }}</flux:select.option>
|
|
<flux:select.option value="accent">{{ __('Accent (Cyan)') }}</flux:select.option>
|
|
<flux:select.option value="yellow">{{ __('Yellow') }}</flux:select.option>
|
|
<flux:select.option value="green">{{ __('Green') }}</flux:select.option>
|
|
<flux:select.option value="zinc">{{ __('Gray') }}</flux:select.option>
|
|
</flux:select>
|
|
@error('roleColor') <flux:error>{{ $message }}</flux:error> @enderror
|
|
</flux:field>
|
|
|
|
@if($roleColor)
|
|
<div class="rounded-lg bg-zinc-50 p-4 dark:bg-zinc-800/50">
|
|
<flux:subheading class="mb-2">{{ __('Preview:') }}</flux:subheading>
|
|
<flux:badge size="md" :color="$roleColor">
|
|
{{ $roleName ?: __('Role Name') }}
|
|
</flux:badge>
|
|
</div>
|
|
@endif
|
|
|
|
<flux:field>
|
|
<flux:label>{{ __('Permissions') }}</flux:label>
|
|
<flux:description>{{ __('Select permissions for this role') }}</flux:description>
|
|
|
|
<div class="mt-3 max-h-96 space-y-2 overflow-y-auto rounded-lg border border-zinc-200 p-4 dark:border-zinc-700">
|
|
@php
|
|
$groupedPerms = $allPermissions->groupBy(function($permission) {
|
|
return explode(' ', $permission->name)[1] ?? 'other';
|
|
});
|
|
@endphp
|
|
|
|
@foreach($groupedPerms as $group => $perms)
|
|
<div class="mb-4">
|
|
<flux:subheading class="mb-2 uppercase text-xs">{{ ucfirst($group) }}</flux:subheading>
|
|
<div class="ml-2 space-y-2">
|
|
@foreach($perms as $permission)
|
|
<flux:checkbox
|
|
wire:model="rolePermissions"
|
|
:value="$permission->name"
|
|
:label="$permission->name"
|
|
/>
|
|
@endforeach
|
|
</div>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
</flux:field>
|
|
|
|
@if(!empty($rolePermissions))
|
|
<div class="rounded-lg bg-zinc-50 p-4 dark:bg-zinc-800/50">
|
|
<flux:subheading class="mb-2">{{ __('Selected Permissions:') }} ({{ count($rolePermissions) }})</flux:subheading>
|
|
<div class="flex flex-wrap gap-2">
|
|
@foreach($rolePermissions as $permName)
|
|
<flux:badge size="sm" color="zinc">
|
|
{{ $permName }}
|
|
</flux:badge>
|
|
@endforeach
|
|
</div>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
<flux:separator />
|
|
|
|
<div class="flex justify-between gap-2">
|
|
<flux:button type="button" variant="ghost" wire:click="closeEditModal">
|
|
{{ __('Cancel') }}
|
|
</flux:button>
|
|
<flux:button type="submit" variant="primary">
|
|
{{ __('Save Role') }}
|
|
</flux:button>
|
|
</div>
|
|
</form>
|
|
</flux:modal>
|
|
|
|
{{-- Success Message --}}
|
|
@if (session()->has('message'))
|
|
<flux:toast :variant="'success'">
|
|
{{ session('message') }}
|
|
</flux:toast>
|
|
@endif
|
|
</div>
|