dataTable($request); } $permissions = Permission::where('is_active', 1) ->whereNull('deleted_at') ->orderBy('name') ->get(); // Categorize permissions for UI Matrix $groupedPermissions = $this->groupPermissions($permissions); return view('pages.access_control.roles', [ 'permissions' => $permissions, 'groupedPermissions' => $groupedPermissions, ]); } /** * Group permissions into a hierarchical tree: category → menu → tabs. * * Each entry in $groups[$category][$baseName] has: * 'view' => Permission|null (menu-level view) * 'manage' => Permission|null (menu-level manage) * 'tabs' => [ * $tabSlug => ['view' => Permission|null, 'manage' => Permission|null] * ] */ protected function groupPermissions($permissions) { $groups = []; foreach ($permissions as $permission) { $name = $permission->name; // Determine category $category = match (true) { str_contains($name, 'ai ') => 'AI Intelligence', str_contains($name, 'dashboard') => 'Dashboard', str_contains($name, 'user directory') => 'User Directory', str_contains($name, 'access rights') => 'Access Rights', str_contains($name, 'health and logs') => 'Health & Logs', str_contains($name, 'action history') => 'Action History', str_contains($name, 'global settings') => 'Global Settings', str_contains($name, 'mobile settings') => 'Mobile Settings', str_contains($name, 'active sessions') => 'Active Sessions', str_contains($name, 'notification center') => 'Notification Center', str_contains($name, 'impersonate') => 'Impersonation', str_contains($name, 'backup') || str_contains($name, 'maintenance') => 'System Config', default => 'Other', }; // Strip action prefix $type = 'other'; $resource = $name; if (str_starts_with($name, 'manage ')) { $type = 'manage'; $resource = substr($name, 7); } elseif (str_starts_with($name, 'view ')) { $type = 'view'; $resource = substr($name, 5); } // Detect scoped (tab-level) permission: "resource:tab-slug" if (str_contains($resource, ':')) { [$menu, $tab] = explode(':', $resource, 2); $menu = trim($menu); $tab = trim($tab); if (! isset($groups[$category][$menu])) { $groups[$category][$menu] = ['view' => null, 'manage' => null, 'tabs' => []]; } if (! isset($groups[$category][$menu]['tabs'][$tab])) { $groups[$category][$menu]['tabs'][$tab] = ['view' => null, 'manage' => null]; } $groups[$category][$menu]['tabs'][$tab][$type] = $permission; } else { $menu = trim($resource); if (! isset($groups[$category][$menu])) { $groups[$category][$menu] = ['view' => null, 'manage' => null, 'tabs' => []]; } $groups[$category][$menu][$type] = $permission; } } // Sort categories $priority = ['Dashboard', 'User Directory', 'Access Rights', 'AI Intelligence', 'Health & Logs', 'Action History', 'Global Settings', 'Mobile Settings', 'Notification Center', 'Active Sessions', 'Impersonation', 'System Config']; uksort($groups, function ($a, $b) use ($priority) { $posA = array_search($a, $priority); $posB = array_search($b, $priority); if ($posA === false && $posB === false) { return strcmp($a, $b); } return ($posA === false) ? 1 : (($posB === false) ? -1 : $posA - $posB); }); return $groups; } protected function dataTable(Request $request) { try { $authUser = $request->user(); $canManage = $authUser->can('manage access rights'); $query = Role::query()->withTrashed()->with(['permissions:id,name', 'creator:id,email', 'updater:id,email']); $globalSearch = DataTable::globalSearch($request); $trashed = $request->input('trashed'); if ($trashed === 'archived') { $query->onlyTrashed(); } elseif ($trashed === 'active') { $query->withoutTrashed(); } if ($canManage) { $status = DataTable::columnSearch($request, 1); $name = DataTable::columnSearch($request, 2); $guard = DataTable::columnSearch($request, 3); $permission = DataTable::columnSearch($request, 4); } else { $status = null; $name = DataTable::columnSearch($request, 0); $guard = DataTable::columnSearch($request, 1); $permission = DataTable::columnSearch($request, 2); } if ($status) { $query->where('is_active', $status === 'active'); } if ($name) { $query->where('name', 'like', "%{$name}%"); } if ($guard) { $query->where('guard_name', 'like', "%{$guard}%"); } if ($permission) { $query->whereHas('permissions', function ($permissionQuery) use ($permission) { $permissionQuery->where('name', 'like', "%{$permission}%"); }); } // Audit columns (fixed indices from the end or just using canManage logic) if ($canManage) { $createdAt = DataTable::columnSearch($request, 5); $createdBy = DataTable::columnSearch($request, 6); $updatedAt = DataTable::columnSearch($request, 7); $updatedBy = DataTable::columnSearch($request, 8); } else { $createdAt = DataTable::columnSearch($request, 3); $createdBy = DataTable::columnSearch($request, 4); $updatedAt = DataTable::columnSearch($request, 5); $updatedBy = DataTable::columnSearch($request, 6); } if ($createdAt) { $query->whereDate('created_at', $createdAt); } if ($createdBy) { $query->whereHas('creator', function ($creatorQuery) use ($createdBy) { $creatorQuery->where('email', 'like', "%{$createdBy}%"); }); } if ($updatedAt) { $query->whereDate('updated_at', $updatedAt); } if ($updatedBy) { $query->whereHas('updater', function ($updaterQuery) use ($updatedBy) { $updaterQuery->where('email', 'like', "%{$updatedBy}%"); }); } if ($globalSearch) { $query->where(function ($searchQuery) use ($globalSearch) { $searchQuery ->where('name', 'like', "%{$globalSearch}%") ->orWhere('guard_name', 'like', "%{$globalSearch}%") ->orWhereHas('permissions', function ($permissionQuery) use ($globalSearch) { $permissionQuery->where('name', 'like', "%{$globalSearch}%"); }) ->orWhereHas('creator', function ($creatorQuery) use ($globalSearch) { $creatorQuery->where('email', 'like', "%{$globalSearch}%"); }) ->orWhereHas('updater', function ($updaterQuery) use ($globalSearch) { $updaterQuery->where('email', 'like', "%{$globalSearch}%"); }); }); } // Perform filtered count WITHOUT eager loading or ordering $countQuery = clone $query; $countQuery->setEagerLoads([]); $countQuery->orders = null; $recordsFiltered = $countQuery->count(); $recordsTotal = Role::withTrashed()->count(); // Total across all states without eager loads [$orderIndex, $orderDirection] = DataTable::order($request, $canManage ? 5 : 3, 'desc'); $sortColumn = match (true) { $canManage && $orderIndex === 1 => 'is_active', ($canManage && $orderIndex === 2) || (! $canManage && $orderIndex === 0) => 'name', ($canManage && $orderIndex === 3) || (! $canManage && $orderIndex === 1) => 'guard_name', ($canManage && $orderIndex === 5) || (! $canManage && $orderIndex === 3) => 'created_at', ($canManage && $orderIndex === 7) || (! $canManage && $orderIndex === 5) => 'updated_at', default => 'created_at', }; $roles = $query ->orderBy($sortColumn, $orderDirection) ->skip(DataTable::start($request)) ->take(DataTable::length($request)) ->get(); $rows = $roles->map(function (Role $role) use ($canManage) { $permissionNames = $role->permissions->pluck('name')->values(); $permissionBadges = $permissionNames->isNotEmpty() ? $permissionNames->map(fn ($permission) => ''.e($permission).'')->implode(' ') : '-'; $row = []; if ($canManage) { $row[] = sprintf( '', $role->id ); $row[] = sprintf( '
%s
', $role->is_active ? 'checked' : '', $role->id, e($role->name), $role->is_active ? 'text-success' : 'text-danger', $role->is_active ? 'Active' : 'Inactive' ); } $row[] = e($role->name); $row[] = e($role->guard_name); $row[] = '
'.$permissionBadges.'
'; $row[] = e(format_datetime($role->created_at)); $row[] = e($role->creator->email ?? 'System'); $row[] = e(format_datetime($role->updated_at)); $row[] = e($role->updater->email ?? '-'); if ($canManage) { if ($role->trashed()) { $row[] = '
' .'' .'' .'
'; } else { $row[] = '
' .'' .'' .'
'; } } return $row; })->all(); return DataTable::response($request, $recordsTotal, $recordsFiltered, $rows); } catch (\Exception $e) { Log::error('DataTable Error [RoleManagement]: '.$e->getMessage()); return DataTable::response($request, 0, 0, []); } } public function store(Request $request) { $request->validate([ 'name' => [ 'required', 'string', 'min:3', 'max:50', 'regex:/^[a-zA-Z0-9_\-]+$/', Rule::unique('roles', 'name'), ], 'guard_name' => [ 'required', 'in:web,api', ], 'permissions' => [ 'required', 'array', 'min:1', ], 'permissions.*' => [ Rule::exists('permissions', 'id'), ], ]); try { $role = Role::create([ 'name' => $request->name, 'guard_name' => $request->guard_name, 'created_by' => Auth::id(), ]); $permissionNames = Permission::whereIn('id', $request->permissions) ->pluck('name') ->toArray(); $role->syncPermissions($permissionNames); if ($request->expectsJson()) { return response()->json([ 'success' => true, 'message' => __('Role has been successfully created and permissions assigned.'), ]); } return back()->with('success', __('Role has been successfully created and permissions assigned.')); } catch (\Exception $e) { if ($request->expectsJson()) { return response()->json([ 'success' => false, 'message' => __('Failed to create role: ').$e->getMessage(), ], 500); } return back()->with('error', __('Failed to create role: ').$e->getMessage()); } } public function update(Request $request, $id) { $request->validate([ 'name' => [ 'required', 'string', 'min:3', 'max:50', 'regex:/^[a-zA-Z0-9_\-]+$/', Rule::unique('roles', 'name')->ignore($id), ], 'guard_name' => [ 'required', 'in:web,api', ], 'permissions' => [ 'required', 'array', 'min:1', ], 'permissions.*' => [ Rule::exists('permissions', 'id'), ], ]); try { $role = Role::findOrFail($id); $role->update([ 'name' => $request->name, 'guard_name' => $request->guard_name, 'updated_by' => Auth::id(), ]); $permissionNames = Permission::whereIn('id', $request->permissions) ->pluck('name') ->toArray(); $role->syncPermissions($permissionNames); if ($request->expectsJson()) { return response()->json([ 'success' => true, 'message' => __('Role successfully updated.'), ]); } return back()->with('success', __('Role successfully updated.')); } catch (\Exception $e) { if ($request->expectsJson()) { return response()->json([ 'success' => false, 'message' => __('Failed to update role: ').$e->getMessage(), ], 500); } return back()->with('error', __('Failed to update role: ').$e->getMessage()); } } public function toggleStatus(Request $request) { $request->validate([ 'id' => 'required|exists:roles,id', 'status' => 'required|in:activate,deactivate', ]); $role = Role::findOrFail($request->id); $role->update([ 'is_active' => $request->status === 'activate' ? 1 : 0, 'updated_by' => Auth::id(), ]); return response()->json([ 'success' => true, 'message' => __('Role status updated successfully.'), ]); } public function destroy($id) { $role = Role::findOrFail($id); // Check if role is still used by users if ($role->users()->exists()) { return response()->json([ 'success' => false, 'message' => __('Cannot archive. This role is still assigned to some users.'), ], 422); } $role->delete(); return response()->json([ 'success' => true, 'message' => __('Role has been archived.'), ]); } /** * RESTORE — Restore role (Soft Delete) */ public function restore($id) { $role = Role::withTrashed()->findOrFail($id); $role->restore(); return response()->json([ 'success' => true, 'message' => __('Role has been restored.'), ]); } /** * FORCE DELETE — Permanent removal */ public function forceDelete($id) { $role = Role::withTrashed()->findOrFail($id); // Final usage check before permanent deletion if ($role->users()->exists()) { return response()->json([ 'success' => false, 'message' => __('Cannot delete permanently. This role is still assigned to some users.'), ], 422); } $role->forceDelete(); return response()->json([ 'success' => true, 'message' => __('Role has been permanently removed.'), ]); } public function bulkToggleStatus(Request $request) { $request->validate([ 'ids' => 'required|array', 'ids.*' => 'exists:roles,id', 'status' => 'required|in:activate,deactivate', ]); Role::whereIn('id', $request->ids)->update([ 'is_active' => $request->status === 'activate' ? 1 : 0, 'updated_by' => Auth::id(), ]); return response()->json([ 'success' => true, 'message' => __('Selected roles status updated successfully.'), ]); } public function bulkDelete(Request $request) { $request->validate([ 'ids' => 'required|array', 'ids.*' => 'exists:roles,id', ]); $roles = Role::whereIn('id', $request->ids)->get(); $archivedCount = 0; $failedIds = []; foreach ($roles as $role) { if (! $role->users()->exists()) { $role->delete(); $archivedCount++; } else { $failedIds[] = $role->name; } } $message = __(':count roles archived.', ['count' => $archivedCount]); if (! empty($failedIds)) { $message .= ' '.__('Could not archive :names because they are still assigned to users.', ['names' => implode(', ', $failedIds)]); } return response()->json([ 'success' => true, 'message' => $message, ]); } public function bulkRestore(Request $request) { $request->validate([ 'ids' => 'required|array', 'ids.*' => 'exists:roles,id', ]); Role::withTrashed()->whereIn('id', $request->ids)->restore(); return response()->json([ 'success' => true, 'message' => __('Selected roles restored successfully.'), ]); } public function bulkForceDelete(Request $request) { $request->validate([ 'ids' => 'required|array', 'ids.*' => 'exists:roles,id', ]); $roles = Role::withTrashed()->whereIn('id', $request->ids)->get(); $deletedCount = 0; $failedIds = []; foreach ($roles as $role) { if (! $role->users()->exists()) { $role->forceDelete(); $deletedCount++; } else { $failedIds[] = $role->name; } } $message = __(':count roles permanently removed.', ['count' => $deletedCount]); if (! empty($failedIds)) { $message .= ' '.__('Could not remove :names because they are still assigned to users.', ['names' => implode(', ', $failedIds)]); } return response()->json([ 'success' => true, 'message' => $message, ]); } }