Version: 1.0
Last updated: December 10, 2025
Role Management is the core administrative module for managing user roles and permissions in the DTR System. Administrators can:
Key files involved:
resources/views/users/admin/role-management/ (index)App\Http\Controllers\Web\Admin\RoleController.phpSpatie\Permission\Models\Role, App\Models\Userroles, model_has_roles tablesPurpose:
Scope:
Out of scope:
Required permissions:
view_roles — view role list and detailscreate_roles — create new rolesedit_roles — modify existing rolesdelete_roles — delete role recordsassign_roles — assign roles to usersview_role_assignments — view user role historyRecommended roles:
Implementation note:
authorize() in controller methods or Gate policiesAdmin URLs:
/admin/role-management/admin/role-management?search={query}/admin/role-management/{userId}/assign-role (POST)/admin/role-management/{userId}/remove-role (POST)Browser requirements: Modern Chromium-based browsers, recent Safari/Firefox. Mobile viewing supported; recommend desktop for full role management.
Purpose: Display all roles and users with filtering, search and bulk actions.
View file: resources/views/users/admin/role-management/index.blade.php
UI elements:
Left Panel — Roles List:
Right Panel — Users & Role Assignment:
Key features:
/admin/role-management?search={query})Controller method: RoleController::index(Request $request)
public function index(Request $request)
{
$search = $request->search;
// Get all roles except admin and employee (system roles)
$roles = Role::whereNotIn('name', ['admin', 'employee'])->get();
// Query employees with role filter
$query = User::role('employee');
// Search by name or email
if($search){
$query = $query->where('name', 'like', '%' . $search . '%')
->orWhere('email', 'like', '%' . $search . '%');
}
$users = $query->latest()->paginate(10);
return view('users.admin.role-management.index', compact(['roles', 'users']));
}
Purpose: Assign a new role to a user.
Trigger: "Add Role" button/action next to user in index view.
Flow:
Modal / Inline Form opens:
Form submission (POST /admin/role-management/{userId}/assign-role):
role_id is provided and existsUser::assignRole(Role)Form fields:
{id}Validation:
role_id required, must exist in roles tableassign_roles permissionServer-side processing:
public function assignRole(Request $request, string $id)
{
// Validate
$request->validate([
'role_id' => 'required|exists:roles,id'
]);
$user = User::findOrFail($id);
$role = Role::findOrFail($request->role_id);
// Check user doesn't already have role
if ($user->hasRole($role)) {
return back()->with('error', 'User already has this role');
}
// Assign role
$user->assignRole($role);
// Audit log
activity()
->causedBy(auth()->user())
->performedOn($user)
->withProperties(['role' => $role->name, 'notes' => $request->notes ?? null])
->log('role_assigned');
// Notification
// Mail::queue(new RoleAssigned($user, $role));
return back()->with(['message' => 'New Role Assign!']);
}
Controller method:
public function assignRole(Request $request, string $id)
{
$user = User::find($id);
$role_id = $request->role_id;
$role = Role::find($role_id);
$user->assignRole($role);
return back()->with(['message' => 'New Role Assign!']);
}
Purpose: Remove an assigned role from a user.
Trigger: "Remove" button/action (X icon) next to role badge in user row.
Flow:
Confirmation modal opens:
Form submission (POST /admin/role-management/{userId}/remove-role):
role_id is provided and user has this roleUser::removeRole(Role)Form fields:
{id}Validation:
role_id required, must exist in roles tableassign_roles permissionServer-side processing:
public function removeRole(string $id, Request $request)
{
// Validate
$request->validate([
'role_id' => 'required|exists:roles,id'
]);
$user = User::findOrFail($id);
$role = Role::findOrFail($request->role_id);
// Check user has this role
if (!$user->hasRole($role)) {
return back()->with('error', 'User does not have this role');
}
// Check user won't have zero roles
if ($user->roles()->count() <= 1) {
return back()->with('error', 'User must have at least one role');
}
// Remove role
$user->removeRole($role);
// Audit log
activity()
->causedBy(auth()->user())
->performedOn($user)
->withProperties(['role' => $role->name, 'reason' => $request->reason ?? null])
->log('role_removed');
// Notification
// Mail::queue(new RoleRemoved($user, $role));
return back()->with(['message' => 'Role is Remove!']);
}
Controller method:
public function removeRole(string $id, Request $request)
{
$role = Role::find($request->role_id);
$user = User::find($id);
$user->removeRole($role);
return back()->with(['message' => 'Role is Remove!']);
}
Role model fields (Spatie Laravel-Permission):
id — Primary key
name — Unique identifier (string, e.g., "team-lead", "scheduler")
guard_name — Guard name (string, default 'web')
created_at — Timestamp
updated_at — Timestamp
model_has_roles table (pivot):
role_id — Foreign key to roles
model_id — Foreign key to users
model_type — Model class name (App\Models\User)
Sample records:
Role:
id: 1
name: "admin"
guard_name: "web"
id: 2
name: "employee"
guard_name: "web"
id: 3
name: "team-lead"
guard_name: "web"
id: 4
name: "scheduler"
guard_name: "web"
id: 5
name: "hr-manager"
guard_name: "web"
model_has_roles:
role_id: 3 (team-lead)
model_id: 42 (Jane Doe)
model_type: App\Models\User
role_id: 5 (hr-manager)
model_id: 43 (John Smith)
model_type: App\Models\User
System Roles (Built-in, cannot be deleted):
Admin
├─ Description: System administrator with full access
├─ Permissions: All
├─ Users: Limited (1-2 super admins)
└─ Can manage: users, roles, permissions, system settings
Employee
├─ Description: Default role for all regular employees
├─ Permissions: Basic (view own profile, apply leave, view shifts)
├─ Users: Majority of users
└─ Cannot manage: other users, roles, system
Custom Roles (Created by Admin):
Team Lead
├─ Description: Team leader with management capabilities
├─ Permissions: Approve leaves, assign shifts, view team reports, manage team
├─ Users: ~5-10 per department
└─ Scope: Own team members
HR Manager
├─ Description: Human Resources manager
├─ Permissions: Manage users, view reports, approve leaves, manage roles
├─ Users: 2-5
└─ Scope: All employees
Scheduler
├─ Description: Scheduling specialist
├─ Permissions: Create/edit shifts, manage schedules, bulk assign shifts
├─ Users: 1-3
└─ Scope: All shifts and schedules
Manager / Supervisor
├─ Description: Department manager
├─ Permissions: Approve timesheets, manage attendance, view payroll summaries
├─ Users: ~3-10
└─ Scope: Own department
Payroll Administrator
├─ Description: Payroll processor
├─ Permissions: View payroll data, process payroll, generate reports
├─ Users: 1-2
└─ Scope: All employees
IT Admin
├─ Description: IT system administrator
├─ Permissions: Manage SSO clients, system logs, user accounts
├─ Users: 1-3
└─ Scope: System administration
Role Hierarchy Example:
Super Admin
├── Admin
│ ├── HR Manager
│ │ ├── Team Lead
│ │ └── Scheduler
│ └── IT Admin
└── Employee (base level)
Scenario: Assign "Team Lead" role to Jane Doe.
Steps:
Server-side processing:
User::assignRole(Role)model_has_roles tablePermissions granted:
Scenario: User can have multiple roles (e.g., "Team Lead" + "Scheduler").
Steps:
Validation:
Scenario: Remove "Team Lead" role from Jane Doe.
Steps:
Server-side processing:
User::removeRole(Role)model_has_roles tableGET /admin/role-management
search (user name/email), role_id (filter by role)$roles — all custom roles (excluding admin, employee)$users — paginated employees with their rolesPOST /admin/role-management/{id}/assign-role
{
"role_id": 3
}POST /admin/role-management/{id}/remove-role
{
"role_id": 3,
"reason": "Demotion"
}GET /api/admin/roles
search, guard_name{
"data": [
{
"id": 3,
"name": "team-lead",
"guard_name": "web",
"permissions_count": 5,
"users_count": 8,
"created_at": "2025-01-01T00:00:00Z"
}
],
"pagination": { "total": 10, "per_page": 25, "current_page": 1 }
}GET /api/admin/roles/{id}
{
"id": 3,
"name": "team-lead",
"guard_name": "web",
"permissions": [
{ "id": 1, "name": "view_team_members" },
{ "id": 2, "name": "approve_leaves" }
],
"users": [{ "id": 42, "name": "Jane Doe", "email": "jane@example.com" }]
}POST /api/admin/roles
{
"name": "new-role",
"description": "Role description"
}PUT /api/admin/roles/{id}
DELETE /api/admin/roles/{id}
GET /api/admin/users/{id}/roles
{
"user_id": 42,
"roles": [
{ "id": 3, "name": "team-lead" },
{ "id": 5, "name": "hr-manager" }
]
}Location: app/Http/Controllers/Web/Admin/RoleController.php
index() — List all roles and users
public function index(Request $request)
{
$search = $request->search;
// Get all custom roles (exclude system roles)
$roles = Role::whereNotIn('name', ['admin', 'employee'])->get();
// Query users with 'employee' role
$query = User::role('employee');
// Search by name or email
if($search){
$query = $query->where('name', 'like', '%' . $search . '%')
->orWhere('email', 'like', '%' . $search . '%');
}
// Paginate results
$users = $query->latest()->paginate(10);
return view('users.admin.role-management.index', compact(['roles', 'users']));
}
assignRole() — Assign role to user
public function assignRole(Request $request, string $id)
{
// Validate request
$request->validate([
'role_id' => 'required|exists:roles,id'
]);
// Find user and role
$user = User::find($id);
$role_id = $request->role_id;
$role = Role::find($role_id);
// Check user doesn't already have role
if ($user->hasRole($role)) {
return back()->with('error', 'User already has this role');
}
// Assign role
$user->assignRole($role);
// Audit log (optional)
// activity()
// ->causedBy(auth()->user())
// ->performedOn($user)
// ->withProperties(['role' => $role->name])
// ->log('role_assigned');
// Notification (optional)
// Mail::queue(new RoleAssigned($user, $role));
return back()->with(['message' => 'New Role Assign!']);
}
removeRole() — Remove role from user
public function removeRole(string $id, Request $request)
{
// Validate request
$request->validate([
'role_id' => 'required|exists:roles,id'
]);
// Find user and role
$user = User::find($id);
$role = Role::find($request->role_id);
// Check user has this role
if (!$user->hasRole($role)) {
return back()->with('error', 'User does not have this role');
}
// Check user won't have zero roles
if ($user->roles()->count() <= 1) {
return back()->with('error', 'User must have at least one role');
}
// Remove role
$user->removeRole($role);
// Audit log (optional)
// activity()
// ->causedBy(auth()->user())
// ->performedOn($user)
// ->withProperties(['role' => $role->name, 'reason' => $request->reason ?? null])
// ->log('role_removed');
// Notification (optional)
// Mail::queue(new RoleRemoved($user, $role));
return back()->with(['message' => 'Role is Remove!']);
}
Location: app/Services/RoleServices.php (recommended, not yet implemented)
Suggested methods:
namespace App\Services;
use Spatie\Permission\Models\Role;
use App\Models\User;
class RoleServices
{
/**
* Get all custom roles (excluding system roles)
*/
public function getCustomRoles()
{
return Role::whereNotIn('name', ['admin', 'employee'])->get();
}
/**
* Assign role to user with validation
*/
public function assignRoleToUser(User $user, Role $role, $reason = null)
{
if ($user->hasRole($role)) {
throw new \Exception("User already has {$role->name} role");
}
$user->assignRole($role);
// Log activity
activity()
->causedBy(auth()->user())
->performedOn($user)
->withProperties(['role' => $role->name, 'reason' => $reason])
->log('role_assigned');
return $user;
}
/**
* Remove role from user with validation
*/
public function removeRoleFromUser(User $user, Role $role, $reason = null)
{
if (!$user->hasRole($role)) {
throw new \Exception("User does not have {$role->name} role");
}
if ($user->roles()->count() <= 1) {
throw new \Exception('User must have at least one role');
}
$user->removeRole($role);
// Log activity
activity()
->causedBy(auth()->user())
->performedOn($user)
->withProperties(['role' => $role->name, 'reason' => $reason])
->log('role_removed');
return $user;
}
/**
* Get all users with a specific role
*/
public function getUsersByRole(Role $role)
{
return $role->users()->paginate(25);
}
/**
* Get role assignment history for a user
*/
public function getRoleHistory(User $user)
{
return activity()
->performedOn($user)
->where('log_name', 'role_assigned')
->orWhere('log_name', 'role_removed')
->latest()
->paginate(10);
}
/**
* Bulk assign role to multiple users
*/
public function bulkAssignRole(array $userIds, Role $role)
{
$results = ['success' => 0, 'failed' => 0, 'errors' => []];
foreach ($userIds as $userId) {
try {
$user = User::findOrFail($userId);
$this->assignRoleToUser($user, $role);
$results['success']++;
} catch (\Exception $e) {
$results['failed']++;
$results['errors'][] = "User {$userId}: {$e->getMessage()}";
}
}
return $results;
}
}
Index filters:
Search functionality:
/admin/role-management?search={term}Reports:
Export formats: CSV, Excel, PDF (role matrix)
DTR Integration:
Scheduling Integration:
Payroll Integration:
Sync behavior:
Task 1: Promote Employee to Team Lead
Task 2: Remove Role from User
Task 3: Assign Multiple Roles to User
Task 4: Search and Filter Users by Role
Task 5: Bulk Assign Role to Multiple Users
user_id,role_id
42,3
43,3
44,3Task 6: View Role Assignment History
Audit logging:
Notifications:
Compliance:
Example 1: Department Leads and Permissions
Department: Sales
Users:
- Alice Johnson: Team Lead
- Roles: Team Lead, Employee
- Permissions: view_team_members, approve_leaves, assign_shifts, view_team_reports, + basic employee permissions
- Bob Smith: Team Lead
- Roles: Team Lead, Employee
- Permissions: (same as Alice)
- Carol Davis: Employee
- Roles: Employee
- Permissions: view_own_profile, apply_leave, view_assigned_shifts
- David Wilson: Employee
- Roles: Employee
- Permissions: (same as Carol)
Example 2: Multi-Role User (HR Manager who schedules)
User: Sarah Johnson
Roles:
- HR Manager
- Permissions: manage_users, view_users, approve_leaves, view_payroll_summary
- Scheduler
- Permissions: create_shifts, edit_shifts, bulk_assign_shifts, view_schedules
Combined Permissions:
- manage_users, view_users, approve_leaves, view_payroll_summary, create_shifts, edit_shifts, bulk_assign_shifts, view_schedules
Use Case: Sarah manages HR operations and handles shift scheduling
Example 3: Role Hierarchy in Action
Organization Structure:
Super Admin (1 user)
├── Admin (2 users)
│ ├── HR Manager (3 users)
│ │ ├── Team Lead (8 users)
│ │ └── Scheduler (2 users)
│ └── IT Admin (1 user)
└── Employees (150 users)
Unit tests:
User::assignRole() assigns role correctlyUser::removeRole() removes role correctlyUser::hasRole() checks role existenceIntegration tests:
model_has_roles record createdmodel_has_roles record deletedE2E tests (manual):
Validation tests:
Performance tests:
Q: "Cannot assign role — User already has this role"
Q: "Cannot remove role — User must have at least one role"
Q: "Role not appearing in dropdown"
roles tablephp artisan cache:clearQ: "User assigned role but permissions not taking effect"
php artisan cache:clearQ: "Search not returning expected results"
Q: "Audit log shows role changes but permissions not updating in DTR"
php artisan queue:failedphp artisan dtr:sync-permissions {userId}Q: "Cannot delete system role (admin, employee)"
SQL: Find all users with a specific role
SELECT u.* FROM users u
JOIN model_has_roles mhr ON u.id = mhr.model_id
JOIN roles r ON mhr.role_id = r.id
WHERE r.name = 'team-lead'
AND mhr.model_type = 'App\Models\User'
AND u.status = 'active'
ORDER BY u.last_name;
SQL: Find role assignment history
SELECT al.* FROM activity_log al
WHERE al.log_name IN ('role_assigned', 'role_removed')
AND al.model_type = 'App\Models\User'
ORDER BY al.created_at DESC
LIMIT 100;
SQL: Count users per role
SELECT r.name, COUNT(mhr.model_id) as user_count
FROM roles r
LEFT JOIN model_has_roles mhr ON r.id = mhr.role_id
WHERE mhr.model_type = 'App\Models\User' OR mhr.model_type IS NULL
GROUP BY r.id
ORDER BY user_count DESC;
SQL: Find roles with no users
SELECT r.* FROM roles r
LEFT JOIN model_has_roles mhr ON r.id = mhr.role_id
WHERE mhr.id IS NULL
ORDER BY r.name;
Laravel Model: User with roles relationship
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Spatie\Permission\Traits\HasRoles;
class User extends Model
{
use HasRoles;
protected $fillable = ['first_name', 'last_name', 'email', 'password', 'status'];
// Spatie provides: roles(), hasRole(), assignRole(), removeRole(), syncRoles()
public function getRolesAttribute()
{
return $this->roles()->pluck('name');
}
public function getPermissionsAttribute()
{
return $this->getAllPermissions()->pluck('name');
}
}
Laravel Model: Role with users relationship
namespace Spatie\Permission\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class Role extends Model
{
protected $fillable = ['name', 'guard_name'];
public function users(): BelongsToMany
{
return $this->morphedByMany(
\App\Models\User::class,
'model',
'model_has_roles'
);
}
public function permissions(): BelongsToMany
{
return $this->belongsToMany(Permission::class);
}
}
Blade template: Role assignment modal
<!-- Add Role Modal -->
<div class="modal fade" id="assignRoleModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<form action="{{ route('admin.role.assign', $user->id) }}" method="POST">
@csrf
<div class="modal-header">
<h5 class="modal-title">Assign Role to {{ $user->name }}</h5>
<button type="button" class="close" data-dismiss="modal">
<span>×</span>
</button>
</div>
<div class="modal-body">
<div class="form-group">
<label>Current Roles</label>
<div>
@forelse($user->roles as $role)
<span class="badge badge-primary">{{ $role->name }}</span>
@empty
<span class="text-muted">No roles assigned</span>
@endforelse
</div>
</div>
<div class="form-group">
<label for="role_id">Select Role</label>
<select name="role_id" id="role_id" class="form-control" required>
<option value="">-- Select a role --</option>
@foreach($roles as $role)
@unless($user->hasRole($role->name))
<option value="{{ $role->id }}">{{ $role->name }}</option>
@endunless
@endforeach
</select>
</div>
<div class="form-group">
<label for="notes">Notes (optional)</label>
<textarea name="notes" id="notes" class="form-control" rows="2"></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Assign Role</button>
</div>
</form>
</div>
</div>
</div>
Blade template: User roles display with remove button
@foreach($user->roles as $role)
<span class="badge badge-success">
{{ $role->name }}
<form action="{{ route('admin.role.remove', $user->id) }}" method="POST" style="display:inline;">
@csrf
<input type="hidden" name="role_id" value="{{ $role->id }}">
<button type="submit" class="btn-close" title="Remove role"
onclick="return confirm('Remove {{ $role->name }}?')">
×
</button>
</form>
</span>
@endforeach
Seeder: Create default roles and permissions
namespace Database\Seeders;
use Spatie\Permission\Models\Role;
use Spatie\Permission\Models\Permission;
use Illuminate\Database\Seeder;
class RoleAndPermissionSeeder extends Seeder
{
public function run()
{
// Create permissions
$permissions = [
'view_users', 'create_users', 'edit_users', 'delete_users',
'view_roles', 'create_roles', 'edit_roles', 'delete_roles',
'assign_roles', 'view_shifts', 'create_shifts', 'edit_shifts',
'assign_shifts', 'approve_leaves', 'view_payroll', 'edit_payroll'
];
foreach ($permissions as $permission) {
Permission::findOrCreate($permission);
}
// Create roles
$admin = Role::firstOrCreate(['name' => 'admin']);
$admin->syncPermissions(Permission::all());
$employee = Role::firstOrCreate(['name' => 'employee']);
$employee->syncPermissions(['view_own_profile', 'apply_leave', 'view_assigned_shifts']);
$teamLead = Role::firstOrCreate(['name' => 'team-lead']);
$teamLead->syncPermissions([
'view_users', 'view_team_members', 'approve_leaves',
'assign_shifts', 'view_team_reports'
]);
$hrManager = Role::firstOrCreate(['name' => 'hr-manager']);
$hrManager->syncPermissions([
'view_users', 'create_users', 'edit_users', 'delete_users',
'assign_roles', 'approve_leaves', 'view_payroll'
]);
}
}
Document version: 1.0
Maintainers: HR / Admin / Engineering
Last updated: December 10, 2025