Commit 172a4836 authored by Mahmoud Aglan's avatar Mahmoud Aglan

Upgrade permission matrix: editable roles, super admin fix, comparison feature

- Fix super admin assignment: sync is_super_admin flag when assigning role
- Replace is_system block with level-based guard (edit roles below your level)
- Add role deletion for non-system custom roles
- Allow editing system role permissions and metadata (name_ar, description)
- Add role comparison page (/roles/compare) with differences-only filter
- Add permissions:sync artisan command for syncing seeder to DB
- Add events permissions to seeder, make getPermissionsList() static
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent d8010595
<?php
namespace App\Console\Commands;
use Database\Seeders\PermissionSeeder;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class SyncPermissions extends Command
{
protected $signature = 'permissions:sync';
protected $description = 'Sync permissions from seeder definition to database and assign new ones to wildcard roles';
public function handle(): int
{
$canonical = PermissionSeeder::getPermissionsList();
$existing = DB::table('permissions')->pluck('name')->toArray();
$missing = array_diff($canonical, $existing);
if (empty($missing)) {
$this->info('All ' . count($canonical) . ' permissions already exist in the database.');
return 0;
}
$now = now();
$inserts = [];
foreach ($missing as $permission) {
$parts = explode('.', $permission);
$inserts[] = [
'name' => $permission,
'module' => $parts[0],
'action' => $parts[1] ?? $permission,
'created_at' => $now,
];
}
DB::table('permissions')->insert($inserts);
$this->info('Inserted ' . count($inserts) . ' missing permissions:');
foreach ($missing as $perm) {
$this->line(" + {$perm}");
}
$newPermIds = DB::table('permissions')
->whereIn('name', $missing)
->pluck('id')
->toArray();
$wildcardRoles = DB::table('roles')
->whereIn('slug', ['super_admin', 'academy_owner'])
->pluck('id')
->toArray();
$pivotInserts = [];
foreach ($wildcardRoles as $roleId) {
foreach ($newPermIds as $permId) {
$pivotInserts[] = [
'role_id' => $roleId,
'permission_id' => $permId,
'scope' => 'all',
'created_at' => $now,
];
}
}
if (!empty($pivotInserts)) {
foreach (array_chunk($pivotInserts, 100) as $chunk) {
DB::table('permission_role')->insertOrIgnore($chunk);
}
$this->info('Assigned new permissions to ' . count($wildcardRoles) . ' wildcard roles (super_admin, academy_owner).');
}
return 0;
}
}
......@@ -11,17 +11,17 @@ class RoleService
{
public function assignRoleToUser(User $user, Role $role, ?int $branchId, User $actor): void
{
// Cannot assign role with equal or higher level
$actorRole = $actor->primaryRole;
if ($actorRole && $role->level >= $actorRole->level) {
if (!$actor->is_super_admin && $actorRole && $role->level >= $actorRole->level) {
throw new DomainException(__('لا يمكنك تعيين صلاحية أعلى من صلاحيتك'));
}
DB::transaction(function () use ($user, $role, $branchId, $actor) {
// Set as primary role
$user->update(['role_id' => $role->id]);
$user->update([
'role_id' => $role->id,
'is_super_admin' => $role->slug === 'super_admin',
]);
// Add to pivot
$user->roles()->syncWithoutDetaching([
$role->id => [
'branch_id' => $branchId,
......@@ -31,11 +31,10 @@ public function assignRoleToUser(User $user, Role $role, ?int $branchId, User $a
});
}
public function syncPermissions(Role $role, array $permissionData): void
public function syncPermissions(Role $role, array $permissionData, ?User $actor = null): void
{
// permissionData: [{permission_id, scope}, ...]
if ($role->is_system) {
throw new DomainException(__('لا يمكن تعديل صلاحيات الأدوار الأساسية'));
if ($actor && !$actor->is_super_admin && $role->level >= ($actor->primaryRole?->level ?? 0)) {
throw new DomainException(__('لا يمكنك تعديل صلاحيات دور بمستوى أعلى من مستواك'));
}
$sync = [];
......
......@@ -5,6 +5,7 @@
use App\Domain\Identity\Models\Permission;
use App\Domain\Identity\Models\Role;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Gate;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Component;
......@@ -26,8 +27,13 @@ public function togglePermission(int $roleId, int $permissionId, string $current
$this->authorize('roles.assign_permissions');
$role = Role::find($roleId);
if (!$role || $role->is_system) {
session()->flash('error', __('لا يمكن تعديل صلاحيات دور النظام'));
if (!$role) {
return;
}
$actor = auth()->user();
if (!$actor->is_super_admin && $role->level >= ($actor->primaryRole?->level ?? 0)) {
session()->flash('error', __('لا يمكنك تعديل صلاحيات دور بمستوى أعلى من مستواك'));
return;
}
......@@ -87,6 +93,12 @@ public function render()
$roles = $roles->filter(fn ($r) => $r->slug === $this->filterRole)->values();
}
$actor = auth()->user();
$canEdit = $actor->is_super_admin || Gate::allows('roles.assign_permissions');
$canEditRole = $roles->mapWithKeys(fn ($r) => [
$r->id => $canEdit && ($actor->is_super_admin || $r->level < ($actor->primaryRole?->level ?? 0))
])->toArray();
return view('livewire.roles.permission-matrix', [
'roles' => $roles,
'grouped' => $grouped,
......@@ -94,6 +106,7 @@ public function render()
'pivotData' => $pivotData,
'moduleLabels' => $this->getModuleLabels(),
'scopeLabels' => $this->getScopeLabels(),
'canEditRole' => $canEditRole,
]);
}
......
<?php
namespace App\Livewire\Roles;
use App\Domain\Identity\Models\Permission;
use App\Domain\Identity\Models\Role;
use Illuminate\Support\Facades\DB;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Component;
#[Layout('layouts.app')]
#[Title('مقارنة الأدوار')]
class RoleComparison extends Component
{
public array $selectedRoleIds = [];
public string $filterModule = '';
public bool $showDifferencesOnly = false;
public function mount(): void
{
$this->authorize('roles.list');
}
public function addRole(int $roleId): void
{
if (count($this->selectedRoleIds) >= 4) {
session()->flash('error', __('يمكنك مقارنة 4 أدوار كحد أقصى'));
return;
}
if (!in_array($roleId, $this->selectedRoleIds)) {
$this->selectedRoleIds[] = $roleId;
}
}
public function removeRole(int $roleId): void
{
$this->selectedRoleIds = array_values(array_filter(
$this->selectedRoleIds,
fn ($id) => $id !== $roleId
));
}
public function toggleDifferencesOnly(): void
{
$this->showDifferencesOnly = !$this->showDifferencesOnly;
}
public function render()
{
$allRoles = Role::where('academy_id', app('current_academy')->id)
->orderByDesc('level')
->get();
$selectedRoles = $allRoles->whereIn('id', $this->selectedRoleIds)->values();
$permissionsQuery = Permission::orderBy('module')->orderBy('action');
if ($this->filterModule) {
$permissionsQuery->where('module', $this->filterModule);
}
$permissions = $permissionsQuery->get();
$grouped = $permissions->groupBy('module');
$modules = Permission::distinct()->orderBy('module')->pluck('module');
$pivotData = [];
if ($selectedRoles->isNotEmpty()) {
$pivotData = DB::table('permission_role')
->whereIn('role_id', $selectedRoles->pluck('id'))
->get()
->groupBy('role_id')
->map(fn ($items) => $items->pluck('scope', 'permission_id')->toArray())
->toArray();
}
// Filter to differences only
$filteredGrouped = $grouped;
if ($this->showDifferencesOnly && $selectedRoles->count() >= 2) {
$filteredGrouped = $grouped->map(function ($perms) use ($pivotData, $selectedRoles) {
return $perms->filter(function ($perm) use ($pivotData, $selectedRoles) {
$scopes = $selectedRoles->map(fn ($r) => $pivotData[$r->id][$perm->id] ?? '')->toArray();
return count(array_unique($scopes)) > 1;
});
})->filter(fn ($perms) => $perms->isNotEmpty());
}
return view('livewire.roles.role-comparison', [
'allRoles' => $allRoles,
'selectedRoles' => $selectedRoles,
'grouped' => $filteredGrouped,
'modules' => $modules,
'pivotData' => $pivotData,
'moduleLabels' => $this->getModuleLabels(),
'scopeLabels' => $this->getScopeLabels(),
]);
}
private function getModuleLabels(): array
{
return [
'dashboard' => 'لوحة التحكم',
'academies' => 'الأكاديمية',
'branches' => 'الفروع',
'users' => 'المستخدمين',
'roles' => 'الأدوار',
'people' => 'الأشخاص',
'participants' => 'المشتركين',
'guardians' => 'أولياء الأمور',
'activities' => 'الأنشطة',
'programs' => 'البرامج',
'groups' => 'المجموعات',
'sessions' => 'الحصص',
'schedules' => 'الجداول',
'enrollments' => 'التسجيلات',
'holidays' => 'العطلات',
'waitlists' => 'قوائم الانتظار',
'attendance' => 'الحضور',
'excuses' => 'الأعذار',
'invoices' => 'الفواتير',
'payments' => 'المدفوعات',
'transactions' => 'المعاملات المالية',
'accounts' => 'الحسابات',
'wallets' => 'المحافظ',
'payment_plans' => 'خطط الدفع',
'cash_sessions' => 'ورديات الكاشير',
'refunds' => 'المرتجعات',
'daily_closing' => 'الإقفال اليومي',
'expenses' => 'المصروفات',
'pricing' => 'التسعير',
'base_prices' => 'الأسعار الأساسية',
'pricing_rules' => 'قواعد التسعير',
'promotions' => 'العروض',
'coupons' => 'الكوبونات',
'pos' => 'نقطة البيع',
'pos_sessions' => 'ورديات نقطة البيع',
'products' => 'المنتجات',
'categories' => 'التصنيفات',
'warehouses' => 'المخازن',
'inventory' => 'المخزون',
'purchase_orders' => 'أوامر الشراء',
'stock_counts' => 'جرد المخزون',
'kits' => 'الأطقم',
'suppliers' => 'الموردين',
'facilities' => 'المنشآت',
'reservations' => 'الحجوزات',
'employees' => 'الموظفين',
'trainers' => 'المدربين',
'payroll' => 'الرواتب',
'assignments' => 'التكليفات',
'notifications' => 'الإشعارات',
'notification_preferences' => 'إعدادات الإشعارات',
'reports' => 'التقارير',
'approvals' => 'الموافقات',
'settings' => 'الإعدادات',
'evaluations' => 'التقييمات',
'documents' => 'المستندات',
'audit' => 'سجل المراجعة',
'super_admin' => 'مدير النظام',
'events' => 'الفعاليات',
];
}
private function getScopeLabels(): array
{
return [
'own' => 'الخاصة',
'own_groups' => 'مجموعاته',
'own_children' => 'أبنائه',
'branch' => 'الفرع',
'academy' => 'الأكاديمية',
'all' => 'الكل',
];
}
public function getActionLabel(string $action): string
{
return match ($action) {
'list' => 'عرض القائمة',
'view' => 'عرض التفاصيل',
'show' => 'عرض',
'create' => 'إنشاء',
'update' => 'تعديل',
'delete' => 'حذف',
'export' => 'تصدير',
'import' => 'استيراد',
'cancel' => 'إلغاء',
'send' => 'إرسال',
'manage' => 'إدارة',
'mark' => 'تسجيل',
'edit' => 'تحرير',
'view_reports' => 'عرض التقارير',
'submit' => 'تقديم',
'approve' => 'موافقة',
'reject' => 'رفض',
'change_status' => 'تغيير الحالة',
'view_financial' => 'عرض المالي',
'view_attendance' => 'عرض الحضور',
'bulk_status_change' => 'تغيير حالة جماعي',
'manage_enrollments' => 'إدارة التسجيلات',
'reschedule' => 'إعادة جدولة',
'start' => 'بدء',
'complete' => 'إكمال',
'generate' => 'إنشاء تلقائي',
'override' => 'تجاوز',
'transfer' => 'نقل',
'access' => 'الوصول',
'sell' => 'بيع',
'override_price' => 'تجاوز السعر',
'override_stock' => 'تجاوز المخزون',
'void_transaction' => 'إلغاء عملية',
'receive' => 'استلام',
'adjust' => 'تسوية',
'view_levels' => 'عرض المستويات',
'manage_layouts' => 'إدارة التخطيطات',
'manage_hours' => 'إدارة المواعيد',
'confirm' => 'تأكيد',
'manage_qualifications' => 'إدارة المؤهلات',
'manage_availability' => 'إدارة التوفر',
'view_compensation' => 'عرض المكافآت',
'pay' => 'صرف',
'manage_templates' => 'إدارة القوالب',
'assign_roles' => 'تعيين أدوار',
'assign_permissions' => 'تعيين صلاحيات',
default => $action,
};
}
}
......@@ -16,6 +16,7 @@ class RoleForm extends Component
{
public ?Role $role = null;
public bool $editing = false;
public bool $isSystem = false;
public string $name = '';
public string $name_ar = '';
......@@ -27,8 +28,14 @@ class RoleForm extends Component
public function mount(?Role $role = null): void
{
if ($role && $role->exists) {
$actor = auth()->user();
if (!$actor->is_super_admin && $role->level >= ($actor->primaryRole?->level ?? 0)) {
abort(403);
}
$this->role = $role;
$this->editing = true;
$this->isSystem = (bool) $role->is_system;
$this->name = $role->name;
$this->name_ar = $role->name_ar;
$this->slug = $role->slug;
......@@ -65,29 +72,43 @@ public function save(RoleService $roleService): void
{
$this->validate();
try {
$data = [
'name' => $this->name,
'name_ar' => $this->name_ar,
'slug' => $this->slug,
'level' => $this->level,
'description' => $this->description ?: null,
'is_system' => false,
];
$actor = auth()->user();
try {
if ($this->editing) {
if ($this->role->is_system) {
session()->flash('error', 'لا يمكن تعديل دور النظام');
if (!$actor->is_super_admin && $this->role->level >= ($actor->primaryRole?->level ?? 0)) {
session()->flash('error', __('لا يمكنك تعديل دور بمستوى أعلى من مستواك'));
return;
}
$this->role->update($data);
if ($this->isSystem) {
$this->role->update([
'name_ar' => $this->name_ar,
'description' => $this->description ?: null,
]);
} else {
$this->role->update([
'name' => $this->name,
'name_ar' => $this->name_ar,
'slug' => $this->slug,
'level' => $this->level,
'description' => $this->description ?: null,
]);
}
$role = $this->role;
} else {
$data['academy_id'] = app('current_academy')->id;
$data = [
'name' => $this->name,
'name_ar' => $this->name_ar,
'slug' => $this->slug,
'level' => $this->level,
'description' => $this->description ?: null,
'academy_id' => app('current_academy')->id,
'is_system' => false,
];
$role = Role::create($data);
}
// Sync permissions with default 'academy' scope
$pivotData = collect($this->selectedPermissions)
->mapWithKeys(fn ($permId) => [$permId => ['scope' => 'academy']])
->toArray();
......
......@@ -3,6 +3,7 @@
namespace App\Livewire\Roles;
use App\Domain\Identity\Models\Role;
use App\Domain\Shared\Exceptions\DomainException;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Component;
......@@ -11,14 +12,65 @@
#[Title('الأدوار والصلاحيات')]
class RoleList extends Component
{
public ?int $confirmingDeleteId = null;
public function confirmDelete(int $roleId): void
{
$this->confirmingDeleteId = $roleId;
}
public function cancelDelete(): void
{
$this->confirmingDeleteId = null;
}
public function deleteRole(int $roleId): void
{
$this->authorize('roles.delete');
$role = Role::findOrFail($roleId);
$actor = auth()->user();
if ($role->is_system) {
session()->flash('error', __('لا يمكن حذف دور نظامي'));
$this->confirmingDeleteId = null;
return;
}
if (!$actor->is_super_admin && $role->level >= ($actor->primaryRole?->level ?? 0)) {
session()->flash('error', __('لا يمكنك حذف دور بمستوى أعلى من مستواك'));
$this->confirmingDeleteId = null;
return;
}
$userCount = $role->users()->count();
if ($userCount > 0) {
session()->flash('error', __('لا يمكن حذف دور مرتبط بـ :count مستخدم. قم بإعادة تعيين المستخدمين أولاً.', ['count' => $userCount]));
$this->confirmingDeleteId = null;
return;
}
$role->permissions()->detach();
$role->delete();
session()->flash('success', __('تم حذف الدور بنجاح'));
$this->confirmingDeleteId = null;
}
public function render()
{
$roles = Role::withCount('users')
->orderBy('level', 'desc')
->get();
$actor = auth()->user();
$canEditRole = $roles->mapWithKeys(fn ($r) => [
$r->id => $actor->is_super_admin || $r->level < ($actor->primaryRole?->level ?? 0)
])->toArray();
return view('livewire.roles.role-list', [
'roles' => $roles,
'canEditRole' => $canEditRole,
]);
}
}
......@@ -267,6 +267,7 @@ public function save(): void
$person = $this->resolveOrCreatePerson($academyId, $actor);
// Build user data
$targetRole = $this->role_id ? Role::find($this->role_id) : null;
$data = [
'name' => $this->name,
'name_ar' => $this->name_ar,
......@@ -274,6 +275,7 @@ public function save(): void
'role_id' => $this->role_id,
'person_id' => $person?->id ?? $this->person_id,
'status' => $this->status,
'is_super_admin' => $targetRole && $targetRole->slug === 'super_admin',
];
if ($this->password) {
......
......@@ -9,7 +9,7 @@ class PermissionSeeder extends Seeder
{
public function run(): void
{
$permissions = $this->getPermissionsList();
$permissions = self::getPermissionsList();
$now = now();
foreach ($permissions as $permission) {
......@@ -74,7 +74,7 @@ public function run(): void
}
}
private function getPermissionsList(): array
public static function getPermissionsList(): array
{
return [
// Dashboard
......@@ -195,6 +195,10 @@ private function getPermissionsList(): array
// Audit
'audit.list', 'audit.view', 'audit.export',
// Events
'events.list', 'events.create', 'events.view', 'events.update',
'events.manage', 'events.export',
// Super Admin
'super_admin.access',
];
......@@ -224,7 +228,7 @@ private function getRolePermissions(): array
private function academyAdminPermissions(): array
{
$excluded = ['roles.delete', 'super_admin.access'];
$all = $this->getPermissionsList();
$all = self::getPermissionsList();
$map = [];
foreach ($all as $perm) {
if (!in_array($perm, $excluded)) {
......@@ -236,7 +240,7 @@ private function academyAdminPermissions(): array
private function branchManagerPermissions(): array
{
$allPerms = $this->getPermissionsList();
$allPerms = self::getPermissionsList();
$allowedModules = [
'participants', 'guardians', 'activities', 'programs', 'groups',
'sessions', 'schedules', 'enrollments', 'attendance', 'excuses',
......
......@@ -5,10 +5,24 @@
<h1 class="text-xl sm:text-2xl font-bold text-gray-800">{{ __('مصفوفة الصلاحيات') }}</h1>
<p class="text-sm text-gray-500 mt-1">{{ __('عرض وإدارة صلاحيات كل دور في النظام') }}</p>
</div>
<a href="{{ route('roles.list') }}" wire:navigate
class="inline-flex items-center gap-2 px-4 py-2 text-gray-600 bg-gray-100 rounded-lg hover:bg-gray-200 text-sm font-medium">
&larr; {{ __('قائمة الأدوار') }}
</a>
<div class="flex items-center gap-2">
<a href="{{ route('roles.compare') }}" wire:navigate
class="inline-flex items-center gap-2 px-3 py-2 text-gray-600 bg-gray-100 rounded-lg hover:bg-gray-200 text-sm font-medium">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/></svg>
<span class="hidden sm:inline">{{ __('مقارنة') }}</span>
</a>
@can('roles.create')
<a href="{{ route('roles.create') }}" wire:navigate
class="inline-flex items-center gap-2 px-3 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 text-sm font-medium">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/></svg>
<span class="hidden sm:inline">{{ __('دور جديد') }}</span>
</a>
@endcan
<a href="{{ route('roles.list') }}" wire:navigate
class="inline-flex items-center gap-2 px-3 py-2 text-gray-600 bg-gray-100 rounded-lg hover:bg-gray-200 text-sm font-medium">
&larr; {{ __('قائمة الأدوار') }}
</a>
</div>
</div>
{{-- Flash Messages --}}
......@@ -51,7 +65,7 @@ class="inline-flex items-center gap-2 px-4 py-2 text-gray-600 bg-gray-100 rounde
</span>
</div>
</div>
<p class="text-xs text-gray-500 mt-2">{{ __('اضغط على أي خلية لتدوير النطاق (لا يوجد ← الخاصة ← مجموعاته ← الفرع ← الأكاديمية ← الكل ← لا يوجد). الأدوار النظامية لا يمكن تعديلها.') }}</p>
<p class="text-xs text-gray-500 mt-2">{{ __('اضغط على أي خلية لتدوير النطاق (لا يوجد ← الخاصة ← مجموعاته ← الفرع ← الأكاديمية ← الكل ← لا يوجد). يمكنك تعديل صلاحيات الأدوار الأقل مستوى منك فقط.') }}</p>
</div>
{{-- Filters --}}
......@@ -89,7 +103,14 @@ class="inline-flex items-center gap-2 px-4 py-2 text-gray-600 bg-gray-100 rounde
</th>
@foreach($roles as $role)
<th class="px-3 py-3 text-center text-xs font-semibold text-gray-600 min-w-[90px] whitespace-nowrap">
<div>{{ $role->name_ar }}</div>
<div class="flex items-center justify-center gap-1">
<span>{{ $role->name_ar }}</span>
@if($canEditRole[$role->id] ?? false)
<a href="{{ route('roles.edit', $role) }}" wire:navigate class="text-gray-400 hover:text-blue-600" title="{{ __('تعديل الدور') }}">
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"/></svg>
</a>
@endif
</div>
<div class="font-normal text-[10px] text-gray-400">{{ $role->level }}</div>
</th>
@endforeach
......@@ -131,7 +152,7 @@ class="inline-flex items-center gap-2 px-4 py-2 text-gray-600 bg-gray-100 rounde
};
@endphp
<td class="px-1 py-1.5 text-center">
@if($role->is_system)
@if(!($canEditRole[$role->id] ?? false))
<span class="inline-flex items-center gap-1 px-2 py-1 rounded text-[10px] font-medium {{ $scopeConfig['bg'] }} {{ $scopeConfig['text'] }}"
title="{{ $scope ? ($scopeLabels[$scope] ?? $scope) : __('لا يوجد') }}">
<span class="w-1.5 h-1.5 rounded-full {{ $scopeConfig['dot'] }}"></span>
......
This diff is collapsed.
......@@ -4,6 +4,12 @@
<a href="{{ route('roles.list') }}" wire:navigate class="text-sm text-gray-500 hover:text-gray-700">&larr; العودة</a>
</div>
@if($isSystem && $editing)
<div class="mb-4 p-3 bg-amber-50 border border-amber-200 rounded-lg text-amber-700 text-sm">
{{ __('هذا دور نظامي — يمكنك تعديل الاسم العربي والوصف والصلاحيات فقط.') }}
</div>
@endif
<form wire:submit="save" class="space-y-6">
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-4 sm:p-6">
<h2 class="text-base sm:text-lg font-semibold text-gray-700 mb-4">معلومات الدور</h2>
......@@ -15,17 +21,20 @@
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">الاسم بالإنجليزية *</label>
<input type="text" wire:model="name" dir="ltr" class="w-full text-sm px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 @error('name') border-red-500 @enderror">
<input type="text" wire:model="name" dir="ltr" {{ $isSystem ? 'disabled' : '' }}
class="w-full text-sm px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 @error('name') border-red-500 @enderror {{ $isSystem ? 'bg-gray-100 cursor-not-allowed' : '' }}">
@error('name') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">المعرف (slug) *</label>
<input type="text" wire:model="slug" dir="ltr" class="w-full text-sm px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 @error('slug') border-red-500 @enderror" placeholder="custom_role">
<input type="text" wire:model="slug" dir="ltr" {{ $isSystem ? 'disabled' : '' }}
class="w-full text-sm px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 @error('slug') border-red-500 @enderror {{ $isSystem ? 'bg-gray-100 cursor-not-allowed' : '' }}" placeholder="custom_role">
@error('slug') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">المستوى (0-99) *</label>
<input type="number" wire:model="level" min="0" max="99" dir="ltr" class="w-full text-sm px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 @error('level') border-red-500 @enderror">
<input type="number" wire:model="level" min="0" max="99" dir="ltr" {{ $isSystem ? 'disabled' : '' }}
class="w-full text-sm px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 @error('level') border-red-500 @enderror {{ $isSystem ? 'bg-gray-100 cursor-not-allowed' : '' }}">
@error('level') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
</div>
<div class="sm:col-span-2">
......
......@@ -2,6 +2,11 @@
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-3 mb-4 sm:mb-6">
<h1 class="text-xl sm:text-2xl font-bold text-gray-800">الأدوار والصلاحيات</h1>
<div class="flex items-center gap-2">
<a href="{{ route('roles.compare') }}" wire:navigate
class="inline-flex items-center gap-2 px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 text-sm font-medium">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/></svg>
<span class="hidden sm:inline">{{ __('مقارنة') }}</span>
</a>
<a href="{{ route('roles.matrix') }}" wire:navigate
class="inline-flex items-center gap-2 px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 text-sm font-medium">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"/></svg>
......@@ -17,6 +22,14 @@ class="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-l
</div>
</div>
{{-- Flash Messages --}}
@if(session('success'))
<div class="mb-4 p-3 bg-green-50 border border-green-200 rounded-lg text-green-700 text-sm">{{ session('success') }}</div>
@endif
@if(session('error'))
<div class="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm">{{ session('error') }}</div>
@endif
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3 sm:gap-4">
@foreach($roles as $role)
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-4 sm:p-5">
......@@ -33,12 +46,25 @@ class="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-l
<span>المستوى: {{ $role->level }}</span>
<span>{{ $role->users_count }} مستخدم</span>
</div>
@if(!$role->is_system)
@can('roles.update')
<div class="mt-3 pt-3 border-t border-gray-100">
<a href="{{ route('roles.edit', $role) }}" wire:navigate class="text-blue-600 hover:text-blue-800 text-sm py-2 inline-block">تعديل الصلاحيات</a>
@if($canEditRole[$role->id] ?? false)
<div class="mt-3 pt-3 border-t border-gray-100 flex items-center justify-between">
@can('roles.update')
<a href="{{ route('roles.edit', $role) }}" wire:navigate class="text-blue-600 hover:text-blue-800 text-sm">{{ __('تعديل') }}</a>
@endcan
@if(!$role->is_system)
@can('roles.delete')
@if($confirmingDeleteId === $role->id)
<div class="flex items-center gap-2">
<button wire:click="deleteRole({{ $role->id }})" class="text-red-600 hover:text-red-800 text-xs font-medium">{{ __('تأكيد الحذف') }}</button>
<button wire:click="cancelDelete" class="text-gray-500 hover:text-gray-700 text-xs">{{ __('إلغاء') }}</button>
</div>
@else
<button wire:click="confirmDelete({{ $role->id }})" class="text-red-500 hover:text-red-700 text-xs">{{ __('حذف') }}</button>
@endif
@endcan
@endif
</div>
@endcan
@endif
</div>
@endforeach
......
......@@ -167,6 +167,8 @@
->middleware('permission:roles.list');
Route::get('/roles/matrix', \App\Livewire\Roles\PermissionMatrix::class)->name('roles.matrix')
->middleware('permission:roles.list');
Route::get('/roles/compare', \App\Livewire\Roles\RoleComparison::class)->name('roles.compare')
->middleware('permission:roles.list');
Route::get('/roles/create', \App\Livewire\Roles\RoleForm::class)->name('roles.create')
->middleware('permission:roles.create');
Route::get('/roles/{role}/edit', \App\Livewire\Roles\RoleForm::class)->name('roles.edit')
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment