Administrator
{{ $counts['admins'] }}Full tickets, settings, documentation and user management access.
diff --git a/Modules/Helpdesk/Livewire/KanbanBoard.php b/Modules/Helpdesk/Livewire/KanbanBoard.php index b81eaee..6dff560 100644 --- a/Modules/Helpdesk/Livewire/KanbanBoard.php +++ b/Modules/Helpdesk/Livewire/KanbanBoard.php @@ -206,7 +206,9 @@ final class KanbanBoard extends Component } return Ticket::query() - ->with(['user:id,name,email', 'agent:id,name,email', 'status', 'priority', 'category', 'project', 'team', 'product', 'tags', 'checklistItems', 'board']) + ->leftJoin('users as helpdesk_agents', 'helpdesk_agents.id', '=', 'helpdesk_tickets.agent_id') + ->select('helpdesk_tickets.*', 'helpdesk_agents.name as agent_name') + ->with(['user:id,name,email', 'status', 'priority', 'category', 'project', 'team', 'product', 'tags', 'checklistItems', 'board']) ->visibleTo(Auth::user()) ->where('board_id', $board->id) ->latest('last_activity_at') diff --git a/Modules/UserManagement/Database/Migrations/2026_08_26_090000_create_user_management_roles_permissions.php b/Modules/UserManagement/Database/Migrations/2026_08_26_090000_create_user_management_roles_permissions.php new file mode 100644 index 0000000..30848c0 --- /dev/null +++ b/Modules/UserManagement/Database/Migrations/2026_08_26_090000_create_user_management_roles_permissions.php @@ -0,0 +1,106 @@ +id(); + $table->string('name'); + $table->string('slug')->unique(); + $table->text('description')->nullable(); + $table->boolean('is_system')->default(false); + $table->timestamps(); + }); + + Schema::create('user_management_permissions', function (Blueprint $table): void { + $table->id(); + $table->string('name'); + $table->string('slug')->unique(); + $table->text('description')->nullable(); + $table->timestamps(); + }); + + Schema::create('user_management_role_user', function (Blueprint $table): void { + $table->foreignId('role_id')->constrained('user_management_roles')->cascadeOnDelete(); + $table->foreignId('user_id')->constrained('users')->cascadeOnDelete(); + $table->primary(['role_id', 'user_id']); + }); + + Schema::create('user_management_permission_role', function (Blueprint $table): void { + $table->foreignId('permission_id')->constrained('user_management_permissions')->cascadeOnDelete(); + $table->foreignId('role_id')->constrained('user_management_roles')->cascadeOnDelete(); + $table->primary(['permission_id', 'role_id']); + }); + + $now = now(); + + $adminRole = DB::table('user_management_roles')->insertGetId([ + 'name' => 'Administrator', + 'slug' => 'administrator', + 'description' => 'Full application administration.', + 'is_system' => true, + 'created_at' => $now, + 'updated_at' => $now, + ]); + + $agentRole = DB::table('user_management_roles')->insertGetId([ + 'name' => 'Helpdesk Agent', + 'slug' => 'helpdesk-agent', + 'description' => 'Helpdesk workflow and ticket operations.', + 'is_system' => true, + 'created_at' => $now, + 'updated_at' => $now, + ]); + + foreach ([ + ['Manage users', 'users.manage'], + ['Manage roles', 'roles.manage'], + ['Manage permissions', 'permissions.manage'], + ['Manage helpdesk', 'helpdesk.manage'], + ['Manage documentation', 'documentation.manage'], + ] as [$name, $slug]) { + $permissionId = DB::table('user_management_permissions')->insertGetId([ + 'name' => $name, + 'slug' => $slug, + 'created_at' => $now, + 'updated_at' => $now, + ]); + + DB::table('user_management_permission_role')->insert([ + 'permission_id' => $permissionId, + 'role_id' => $adminRole, + ]); + + if ($slug === 'helpdesk.manage') { + DB::table('user_management_permission_role')->insert([ + 'permission_id' => $permissionId, + 'role_id' => $agentRole, + ]); + } + } + + foreach (DB::table('users')->where('helpdesk_admin', true)->pluck('id') as $userId) { + DB::table('user_management_role_user')->insertOrIgnore(['role_id' => $adminRole, 'user_id' => $userId]); + } + + foreach (DB::table('users')->where('helpdesk_agent', true)->where('helpdesk_admin', false)->pluck('id') as $userId) { + DB::table('user_management_role_user')->insertOrIgnore(['role_id' => $agentRole, 'user_id' => $userId]); + } + } + + public function down(): void + { + Schema::dropIfExists('user_management_permission_role'); + Schema::dropIfExists('user_management_role_user'); + Schema::dropIfExists('user_management_permissions'); + Schema::dropIfExists('user_management_roles'); + } +}; diff --git a/Modules/UserManagement/Http/Controllers/UserController.php b/Modules/UserManagement/Http/Controllers/UserController.php index 254be62..8efccfd 100644 --- a/Modules/UserManagement/Http/Controllers/UserController.php +++ b/Modules/UserManagement/Http/Controllers/UserController.php @@ -10,7 +10,10 @@ use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Routing\Controller; use Illuminate\Support\Facades\Hash; +use Illuminate\Support\Str; use Illuminate\Validation\Rule; +use Modules\UserManagement\Models\Permission; +use Modules\UserManagement\Models\Role; final class UserController extends Controller { @@ -19,11 +22,11 @@ final class UserController extends Controller $search = trim((string) $request->query('search', '')); $users = User::query() + ->with('roles') ->when($search !== '', static function ($query) use ($search): void { - $query->where(static function ($query) use ($search): void { - $query->where('name', 'like', '%'.$search.'%') - ->orWhere('email', 'like', '%'.$search.'%'); - }); + $query->where(fn ($query) => $query + ->where('name', 'like', '%'.$search.'%') + ->orWhere('email', 'like', '%'.$search.'%')); }) ->orderBy('name') ->paginate(20) @@ -34,18 +37,12 @@ final class UserController extends Controller public function create(): View { - return view('modules.user-management.create'); + return view('modules.user-management.create', ['roles' => Role::query()->orderBy('name')->get()]); } public function store(Request $request): RedirectResponse { - $data = $request->validate([ - 'name' => ['required', 'string', 'max:255'], - 'email' => ['required', 'email', 'max:255', 'unique:users,email'], - 'password' => ['required', 'string', 'min:8', 'confirmed'], - 'helpdesk_agent' => ['sometimes', 'boolean'], - 'helpdesk_admin' => ['sometimes', 'boolean'], - ]); + $data = $this->validatedUser($request); $user = User::query()->create([ 'name' => $data['name'], @@ -56,24 +53,24 @@ final class UserController extends Controller 'helpdesk_admin' => $request->boolean('helpdesk_admin'), ]); - return redirect()->route('user-management.edit', $user) - ->with('status', 'User created successfully.'); + $user->roles()->sync($request->input('roles', [])); + + return redirect()->route('user-management.edit', $user)->with('status', 'User created successfully.'); } public function edit(User $user): View { - return view('modules.user-management.edit', compact('user')); + $user->load('roles'); + + return view('modules.user-management.edit', [ + 'user' => $user, + 'roles' => Role::query()->orderBy('name')->get(), + ]); } public function update(Request $request, User $user): RedirectResponse { - $data = $request->validate([ - 'name' => ['required', 'string', 'max:255'], - 'email' => ['required', 'email', 'max:255', Rule::unique('users', 'email')->ignore($user->id)], - 'password' => ['nullable', 'string', 'min:8', 'confirmed'], - 'helpdesk_agent' => ['sometimes', 'boolean'], - 'helpdesk_admin' => ['sometimes', 'boolean'], - ]); + $data = $this->validatedUser($request, $user); $user->name = $data['name']; $user->email = $data['email']; @@ -85,6 +82,7 @@ final class UserController extends Controller } $user->save(); + $user->roles()->sync($request->input('roles', [])); return back()->with('status', 'User updated successfully.'); } @@ -104,12 +102,128 @@ final class UserController extends Controller public function roles(): View { - $counts = [ - 'admins' => User::query()->where('helpdesk_admin', true)->count(), - 'agents' => User::query()->where('helpdesk_agent', true)->count(), - 'users' => User::query()->count(), - ]; + return view('modules.user-management.roles', [ + 'roles' => Role::query()->withCount(['users', 'permissions'])->orderBy('name')->get(), + 'permissions' => Permission::query()->withCount('roles')->orderBy('name')->get(), + ]); + } - return view('modules.user-management.roles', compact('counts')); + public function createRole(): View + { + return view('modules.user-management.role-form', [ + 'role' => new Role(), + 'permissions' => Permission::query()->orderBy('name')->get(), + ]); + } + + public function storeRole(Request $request): RedirectResponse + { + $data = $this->validatedRole($request); + $role = Role::query()->create($data); + $role->permissions()->sync($request->input('permissions', [])); + + return redirect()->route('user-management.roles')->with('status', 'Role created.'); + } + + public function editRole(Role $role): View + { + $role->load('permissions'); + + return view('modules.user-management.role-form', [ + 'role' => $role, + 'permissions' => Permission::query()->orderBy('name')->get(), + ]); + } + + public function updateRole(Request $request, Role $role): RedirectResponse + { + $role->update($this->validatedRole($request, $role)); + $role->permissions()->sync($request->input('permissions', [])); + + return redirect()->route('user-management.roles')->with('status', 'Role updated.'); + } + + public function destroyRole(Role $role): RedirectResponse + { + if ($role->is_system) { + return back()->withErrors(['role' => 'System roles cannot be deleted.']); + } + + $role->delete(); + + return back()->with('status', 'Role deleted.'); + } + + public function createPermission(): View + { + return view('modules.user-management.permission-form', ['permission' => new Permission()]); + } + + public function storePermission(Request $request): RedirectResponse + { + Permission::query()->create($this->validatedPermission($request)); + + return redirect()->route('user-management.roles')->with('status', 'Permission created.'); + } + + public function editPermission(Permission $permission): View + { + return view('modules.user-management.permission-form', compact('permission')); + } + + public function updatePermission(Request $request, Permission $permission): RedirectResponse + { + $permission->update($this->validatedPermission($request, $permission)); + + return redirect()->route('user-management.roles')->with('status', 'Permission updated.'); + } + + public function destroyPermission(Permission $permission): RedirectResponse + { + $permission->delete(); + + return back()->with('status', 'Permission deleted.'); + } + + private function validatedUser(Request $request, ?User $user = null): array + { + return $request->validate([ + 'name' => ['required', 'string', 'max:255'], + 'email' => ['required', 'email', 'max:255', Rule::unique('users', 'email')->ignore($user?->id)], + 'password' => [$user ? 'nullable' : 'required', 'string', 'min:8', 'confirmed'], + 'helpdesk_agent' => ['sometimes', 'boolean'], + 'helpdesk_admin' => ['sometimes', 'boolean'], + 'roles' => ['array'], + 'roles.*' => ['integer', 'exists:user_management_roles,id'], + ]); + } + + private function validatedRole(Request $request, ?Role $role = null): array + { + $data = $request->validate([ + 'name' => ['required', 'string', 'max:255'], + 'slug' => ['nullable', 'string', 'max:255', Rule::unique('user_management_roles', 'slug')->ignore($role?->id)], + 'description' => ['nullable', 'string', 'max:2000'], + 'permissions' => ['array'], + 'permissions.*' => ['integer', 'exists:user_management_permissions,id'], + ]); + + $data['slug'] = filled($data['slug'] ?? null) ? Str::slug($data['slug']) : Str::slug($data['name']); + unset($data['permissions']); + + return $data; + } + + private function validatedPermission(Request $request, ?Permission $permission = null): array + { + $data = $request->validate([ + 'name' => ['required', 'string', 'max:255'], + 'slug' => ['nullable', 'string', 'max:255', Rule::unique('user_management_permissions', 'slug')->ignore($permission?->id)], + 'description' => ['nullable', 'string', 'max:2000'], + ]); + + $data['slug'] = filled($data['slug'] ?? null) ? Str::slug($data['slug'], '.') : Str::slug($data['name'], '.'); + + return $data; } } diff --git a/Modules/UserManagement/Models/Permission.php b/Modules/UserManagement/Models/Permission.php new file mode 100644 index 0000000..95001b6 --- /dev/null +++ b/Modules/UserManagement/Models/Permission.php @@ -0,0 +1,25 @@ +belongsToMany( + Role::class, + 'user_management_permission_role', + 'permission_id', + 'role_id' + ); + } +} diff --git a/Modules/UserManagement/Models/Role.php b/Modules/UserManagement/Models/Role.php new file mode 100644 index 0000000..3a6ea02 --- /dev/null +++ b/Modules/UserManagement/Models/Role.php @@ -0,0 +1,38 @@ + 'boolean']; + + public function permissions(): BelongsToMany + { + return $this->belongsToMany( + Permission::class, + 'user_management_permission_role', + 'role_id', + 'permission_id' + ); + } + + public function users(): BelongsToMany + { + return $this->belongsToMany( + User::class, + 'user_management_role_user', + 'role_id', + 'user_id' + ); + } +} diff --git a/Modules/UserManagement/Routes/web.php b/Modules/UserManagement/Routes/web.php index 13ba516..29c2e65 100644 --- a/Modules/UserManagement/Routes/web.php +++ b/Modules/UserManagement/Routes/web.php @@ -8,7 +8,20 @@ use Modules\UserManagement\Http\Controllers\UserController; Route::get('/', [UserController::class, 'index'])->name('index'); Route::get('/create', [UserController::class, 'create'])->name('create'); Route::post('/', [UserController::class, 'store'])->name('store'); + Route::get('/roles', [UserController::class, 'roles'])->name('roles'); +Route::get('/roles/create', [UserController::class, 'createRole'])->name('roles.create'); +Route::post('/roles', [UserController::class, 'storeRole'])->name('roles.store'); +Route::get('/roles/{role}/edit', [UserController::class, 'editRole'])->name('roles.edit'); +Route::put('/roles/{role}', [UserController::class, 'updateRole'])->name('roles.update'); +Route::delete('/roles/{role}', [UserController::class, 'destroyRole'])->name('roles.destroy'); + +Route::get('/permissions/create', [UserController::class, 'createPermission'])->name('permissions.create'); +Route::post('/permissions', [UserController::class, 'storePermission'])->name('permissions.store'); +Route::get('/permissions/{permission}/edit', [UserController::class, 'editPermission'])->name('permissions.edit'); +Route::put('/permissions/{permission}', [UserController::class, 'updatePermission'])->name('permissions.update'); +Route::delete('/permissions/{permission}', [UserController::class, 'destroyPermission'])->name('permissions.destroy'); + Route::get('/{user}/edit', [UserController::class, 'edit'])->name('edit'); Route::put('/{user}', [UserController::class, 'update'])->name('update'); Route::delete('/{user}', [UserController::class, 'destroy'])->name('destroy'); diff --git a/app/Models/User.php b/app/Models/User.php index 0bfb92a..e2e6a48 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -13,6 +13,8 @@ use Illuminate\Support\Str; use Laravel\Fortify\Contracts\PasskeyUser; use Laravel\Fortify\PasskeyAuthenticatable; use Laravel\Fortify\TwoFactorAuthenticatable; +use Modules\UserManagement\Models\Role; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; /** * @property int $id @@ -48,6 +50,29 @@ class User extends Authenticatable implements PasskeyUser : $initials; } + + public function roles(): BelongsToMany + { + return $this->belongsToMany( + Role::class, + 'user_management_role_user', + 'user_id', + 'role_id' + ); + } + + public function hasRole(string $role): bool + { + return $this->roles()->where('slug', $role)->exists(); + } + + public function hasPermission(string $permission): bool + { + return $this->roles() + ->whereHas('permissions', static fn ($query) => $query->where('slug', $permission)) + ->exists(); + } + public function isHelpdeskAdmin(): bool { return (bool) $this->helpdesk_admin; diff --git a/resources/themes/backend/views/modules/helpdesk/livewire/kanban-board.blade.php b/resources/themes/backend/views/modules/helpdesk/livewire/kanban-board.blade.php index b10b0c3..56e67b4 100644 --- a/resources/themes/backend/views/modules/helpdesk/livewire/kanban-board.blade.php +++ b/resources/themes/backend/views/modules/helpdesk/livewire/kanban-board.blade.php @@ -138,7 +138,7 @@
Define a reusable permission for application roles.
Define the role and the permissions it grants.
Helpdesk uses three clear access levels.
Full tickets, settings, documentation and user management access.
Can work tickets, Kanban workflow and support operations.
Standard authenticated requester access.
Create roles, define permissions and assign permissions to roles.
+| Name | Slug | Users | Permissions | Actions |
|---|---|---|---|---|
| {{ $role->name }}@if($role->is_system) System@endif | +{{ $role->slug }} |
+ {{ $role->users_count }} | +{{ $role->permissions_count }} | +
+ Edit
+ @unless($role->is_system)
+
+ @endunless
+ |
+
| No roles created. | ||||
| Name | Slug | Roles | Actions |
|---|---|---|---|
| {{ $permission->name }} | +{{ $permission->slug }} |
+ {{ $permission->roles_count }} | +
+ Edit
+
+ |
+
| No permissions created. | |||