From 27c28691096eecccc9495206b4e76ab861db99ec Mon Sep 17 00:00:00 2001 From: Iszuddin Ismail Date: Mon, 11 May 2026 11:18:23 +0800 Subject: [PATCH 1/6] user listing with pagination --- app/Http/Controllers/UserController.php | 23 ++++++++++ resources/views/layouts/navigation.blade.php | 6 +++ resources/views/user/index.blade.php | 48 ++++++++++++++++++++ routes/web.php | 2 + 4 files changed, 79 insertions(+) create mode 100644 app/Http/Controllers/UserController.php create mode 100644 resources/views/user/index.blade.php diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php new file mode 100644 index 0000000..a694e9f --- /dev/null +++ b/app/Http/Controllers/UserController.php @@ -0,0 +1,23 @@ +orderBy('name') + ->paginate(10, ['id', 'name', 'email', 'created_at']); + + return view('user.index', [ + 'users' => $users, + ]); + } +} diff --git a/resources/views/layouts/navigation.blade.php b/resources/views/layouts/navigation.blade.php index c2d3a65..727e66f 100644 --- a/resources/views/layouts/navigation.blade.php +++ b/resources/views/layouts/navigation.blade.php @@ -15,6 +15,9 @@ {{ __('Dashboard') }} + + {{ __('Users') }} + @@ -70,6 +73,9 @@ {{ __('Dashboard') }} + + {{ __('Users') }} + diff --git a/resources/views/user/index.blade.php b/resources/views/user/index.blade.php new file mode 100644 index 0000000..82c8dea --- /dev/null +++ b/resources/views/user/index.blade.php @@ -0,0 +1,48 @@ + + +

+ {{ __('Users') }} +

+
+ +
+
+
+
+
+ + + + + + + + + + + @forelse ($users as $user) + + + + + + + @empty + + + + @endforelse + +
IDNameEmailCreated
{{ $user->id }}{{ $user->name }}{{ $user->email }}{{ $user->created_at?->format('Y-m-d H:i') }}
+ No users found. +
+
+ +
+ {{ $users->links() }} +
+
+
+
+
+
diff --git a/routes/web.php b/routes/web.php index 74bb7ca..1dfd459 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,6 +1,7 @@ middleware(['auth', 'verified'])->name('dashboard'); Route::middleware('auth')->group(function () { + Route::get('/user', [UserController::class, 'index'])->name('user.index'); Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit'); Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update'); Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy'); From 909c767e01fdd2dfa61c5d9b98430dc38ff6e340 Mon Sep 17 00:00:00 2001 From: Iszuddin Ismail Date: Mon, 11 May 2026 11:41:27 +0800 Subject: [PATCH 2/6] role listing --- app/Http/Controllers/RoleController.php | 23 ++++++++++ app/Models/Role.php | 9 ++++ .../2026_05_11_033145_create_roles_table.php | 28 +++++++++++++ resources/views/layouts/navigation.blade.php | 6 +++ resources/views/role/index.blade.php | 42 +++++++++++++++++++ routes/web.php | 2 + tests/Feature/RoleIndexTest.php | 23 ++++++++++ 7 files changed, 133 insertions(+) create mode 100644 app/Http/Controllers/RoleController.php create mode 100644 app/Models/Role.php create mode 100644 database/migrations/2026_05_11_033145_create_roles_table.php create mode 100644 resources/views/role/index.blade.php create mode 100644 tests/Feature/RoleIndexTest.php diff --git a/app/Http/Controllers/RoleController.php b/app/Http/Controllers/RoleController.php new file mode 100644 index 0000000..50796ca --- /dev/null +++ b/app/Http/Controllers/RoleController.php @@ -0,0 +1,23 @@ +orderBy('name') + ->get(['id', 'name', 'created_at']); + + return view('role.index', [ + 'roles' => $roles, + ]); + } +} diff --git a/app/Models/Role.php b/app/Models/Role.php new file mode 100644 index 0000000..c5fa26b --- /dev/null +++ b/app/Models/Role.php @@ -0,0 +1,9 @@ +id(); + $table->string('name')->unique(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('roles'); + } +}; diff --git a/resources/views/layouts/navigation.blade.php b/resources/views/layouts/navigation.blade.php index c2d3a65..44109d3 100644 --- a/resources/views/layouts/navigation.blade.php +++ b/resources/views/layouts/navigation.blade.php @@ -15,6 +15,9 @@ {{ __('Dashboard') }} + + {{ __('Roles') }} + @@ -70,6 +73,9 @@ {{ __('Dashboard') }} + + {{ __('Roles') }} + diff --git a/resources/views/role/index.blade.php b/resources/views/role/index.blade.php new file mode 100644 index 0000000..e31be38 --- /dev/null +++ b/resources/views/role/index.blade.php @@ -0,0 +1,42 @@ + + +

+ {{ __('Roles') }} +

+
+ +
+
+
+
+
+ + + + + + + + + + @forelse ($roles as $role) + + + + + + @empty + + + + @endforelse + +
IDNameCreated
{{ $role->id }}{{ $role->name }}{{ $role->created_at?->format('Y-m-d H:i') }}
+ No roles found. +
+
+
+
+
+
+
diff --git a/routes/web.php b/routes/web.php index 74bb7ca..36af0fe 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,6 +1,7 @@ middleware(['auth', 'verified'])->name('dashboard'); Route::middleware('auth')->group(function () { + Route::get('/role', [RoleController::class, 'index'])->name('role.index'); Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit'); Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update'); Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy'); diff --git a/tests/Feature/RoleIndexTest.php b/tests/Feature/RoleIndexTest.php new file mode 100644 index 0000000..8381cd4 --- /dev/null +++ b/tests/Feature/RoleIndexTest.php @@ -0,0 +1,23 @@ +get('/role')->assertRedirect('/login'); +}); + +test('authenticated users can view the role index', function () { + Role::query()->create(['name' => 'Admin']); + Role::query()->create(['name' => 'Editor']); + + $response = $this + ->actingAs(User::factory()->create()) + ->get('/role'); + + $response + ->assertSuccessful() + ->assertSee('Roles') + ->assertSee('Admin') + ->assertSee('Editor'); +}); From 6739a5e1699a9dd7101c471bffd50400f82d22d2 Mon Sep 17 00:00:00 2001 From: Iszuddin Ismail Date: Mon, 11 May 2026 11:46:40 +0800 Subject: [PATCH 3/6] new role functionality --- app/Http/Controllers/RoleController.php | 22 +++++++++++++++++ app/Http/Requests/StoreRoleRequest.php | 29 ++++++++++++++++++++++ resources/views/role/create.blade.php | 33 +++++++++++++++++++++++++ resources/views/role/index.blade.php | 18 +++++++++++--- routes/web.php | 2 ++ tests/Feature/RoleIndexTest.php | 15 +++++++++++ 6 files changed, 116 insertions(+), 3 deletions(-) create mode 100644 app/Http/Requests/StoreRoleRequest.php create mode 100644 resources/views/role/create.blade.php diff --git a/app/Http/Controllers/RoleController.php b/app/Http/Controllers/RoleController.php index 50796ca..542de1e 100644 --- a/app/Http/Controllers/RoleController.php +++ b/app/Http/Controllers/RoleController.php @@ -2,7 +2,9 @@ namespace App\Http\Controllers; +use App\Http\Requests\StoreRoleRequest; use App\Models\Role; +use Illuminate\Http\RedirectResponse; use Illuminate\View\View; class RoleController extends Controller @@ -20,4 +22,24 @@ class RoleController extends Controller 'roles' => $roles, ]); } + + /** + * Show the form for creating a new role. + */ + public function create(): View + { + return view('role.create'); + } + + /** + * Store a newly created role. + */ + public function store(StoreRoleRequest $request): RedirectResponse + { + Role::create($request->validated()); + + return redirect() + ->route('role.index') + ->with('status', 'role-created'); + } } diff --git a/app/Http/Requests/StoreRoleRequest.php b/app/Http/Requests/StoreRoleRequest.php new file mode 100644 index 0000000..83e7624 --- /dev/null +++ b/app/Http/Requests/StoreRoleRequest.php @@ -0,0 +1,29 @@ +|string> + */ + public function rules(): array + { + return [ + 'name' => ['required', 'string', 'max:255', 'unique:roles,name'], + ]; + } +} diff --git a/resources/views/role/create.blade.php b/resources/views/role/create.blade.php new file mode 100644 index 0000000..0428483 --- /dev/null +++ b/resources/views/role/create.blade.php @@ -0,0 +1,33 @@ + + +

+ {{ __('Create Role') }} +

+
+ +
+
+
+
+
+ @csrf + +
+ + + +
+ +
+ {{ __('Save') }} + + + {{ __('Cancel') }} + +
+
+
+
+
+
+
\ No newline at end of file diff --git a/resources/views/role/index.blade.php b/resources/views/role/index.blade.php index e31be38..604e19f 100644 --- a/resources/views/role/index.blade.php +++ b/resources/views/role/index.blade.php @@ -1,14 +1,26 @@ -

- {{ __('Roles') }} -

+
+

+ {{ __('Roles') }} +

+ + + {{ __('New Role') }} + +
+ @if (session('status') === 'role-created') +
+ {{ __('Role created successfully.') }} +
+ @endif +
diff --git a/routes/web.php b/routes/web.php index 36af0fe..5ae45dd 100644 --- a/routes/web.php +++ b/routes/web.php @@ -14,6 +14,8 @@ Route::get('/dashboard', function () { Route::middleware('auth')->group(function () { Route::get('/role', [RoleController::class, 'index'])->name('role.index'); + Route::get('/role/create', [RoleController::class, 'create'])->name('role.create'); + Route::post('/role', [RoleController::class, 'store'])->name('role.store'); Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit'); Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update'); Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy'); diff --git a/tests/Feature/RoleIndexTest.php b/tests/Feature/RoleIndexTest.php index 8381cd4..2458028 100644 --- a/tests/Feature/RoleIndexTest.php +++ b/tests/Feature/RoleIndexTest.php @@ -21,3 +21,18 @@ test('authenticated users can view the role index', function () { ->assertSee('Admin') ->assertSee('Editor'); }); + +test('authenticated users can create a role', function () { + $response = $this + ->actingAs(User::factory()->create()) + ->post('/role', [ + 'name' => 'Manager', + ]); + + $response + ->assertRedirect('/role'); + + $this->assertDatabaseHas('roles', [ + 'name' => 'Manager', + ]); +}); From f110790e09e8658c633c90557e1af7e8aa4ca854 Mon Sep 17 00:00:00 2001 From: Iszuddin Ismail Date: Mon, 11 May 2026 11:54:18 +0800 Subject: [PATCH 4/6] edit user functionality --- app/Http/Controllers/UserController.php | 30 ++++++++++++++++ app/Http/Requests/UpdateUserRequest.php | 37 ++++++++++++++++++++ resources/views/user/edit.blade.php | 46 +++++++++++++++++++++++++ resources/views/user/index.blade.php | 8 ++++- routes/web.php | 2 ++ tests/Feature/UserTest.php | 44 +++++++++++++++++++++++ 6 files changed, 166 insertions(+), 1 deletion(-) create mode 100644 app/Http/Requests/UpdateUserRequest.php create mode 100644 resources/views/user/edit.blade.php create mode 100644 tests/Feature/UserTest.php diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index a694e9f..7796359 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -2,7 +2,9 @@ namespace App\Http\Controllers; +use App\Http\Requests\UpdateUserRequest; use App\Models\User; +use Illuminate\Http\RedirectResponse; use Illuminate\View\View; class UserController extends Controller @@ -20,4 +22,32 @@ class UserController extends Controller 'users' => $users, ]); } + + /** + * Show the form for editing the specified user. + */ + public function edit(User $user): View + { + return view('user.edit', [ + 'user' => $user, + ]); + } + + /** + * Update the specified user. + */ + public function update(UpdateUserRequest $request, User $user): RedirectResponse + { + $user->fill($request->validated()); + + if ($user->isDirty('email')) { + $user->email_verified_at = null; + } + + $user->save(); + + return redirect() + ->route('user.edit', $user) + ->with('status', 'user-updated'); + } } diff --git a/app/Http/Requests/UpdateUserRequest.php b/app/Http/Requests/UpdateUserRequest.php new file mode 100644 index 0000000..469ee8f --- /dev/null +++ b/app/Http/Requests/UpdateUserRequest.php @@ -0,0 +1,37 @@ +|string> + */ + public function rules(): array + { + return [ + 'name' => ['required', 'string', 'max:255'], + 'email' => [ + 'required', + 'string', + 'email', + 'max:255', + Rule::unique('users', 'email')->ignore($this->route('user')), + ], + ]; + } +} diff --git a/resources/views/user/edit.blade.php b/resources/views/user/edit.blade.php new file mode 100644 index 0000000..399a4f1 --- /dev/null +++ b/resources/views/user/edit.blade.php @@ -0,0 +1,46 @@ + + +

+ {{ __('Edit User') }} +

+
+ +
+
+
+
+ @if (session('status') === 'user-updated') +
+ {{ __('User updated successfully.') }} +
+ @endif + +
+ @csrf + @method('patch') + +
+ + + +
+ +
+ + + +
+ +
+ {{ __('Save') }} + + + {{ __('Back') }} + +
+ +
+
+
+
+
\ No newline at end of file diff --git a/resources/views/user/index.blade.php b/resources/views/user/index.blade.php index 82c8dea..e1101c0 100644 --- a/resources/views/user/index.blade.php +++ b/resources/views/user/index.blade.php @@ -17,6 +17,7 @@
+ @@ -26,10 +27,15 @@ + @empty - diff --git a/routes/web.php b/routes/web.php index 1dfd459..9ad00ff 100644 --- a/routes/web.php +++ b/routes/web.php @@ -14,6 +14,8 @@ Route::get('/dashboard', function () { Route::middleware('auth')->group(function () { Route::get('/user', [UserController::class, 'index'])->name('user.index'); + Route::get('/user/{user}/edit', [UserController::class, 'edit'])->name('user.edit'); + Route::patch('/user/{user}', [UserController::class, 'update'])->name('user.update'); Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit'); Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update'); Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy'); diff --git a/tests/Feature/UserTest.php b/tests/Feature/UserTest.php new file mode 100644 index 0000000..7d4b744 --- /dev/null +++ b/tests/Feature/UserTest.php @@ -0,0 +1,44 @@ +create(); + + $this->get("/user/{$user->id}/edit")->assertRedirect('/login'); +}); + +test('authenticated users can view the user edit page', function () { + $actingUser = User::factory()->create(); + $user = User::factory()->create(); + + $response = $this + ->actingAs($actingUser) + ->get("/user/{$user->id}/edit"); + + $response + ->assertSuccessful() + ->assertSee('Edit User') + ->assertSee($user->name) + ->assertSee($user->email); +}); + +test('authenticated users can update a user', function () { + $actingUser = User::factory()->create(); + $user = User::factory()->create(); + + $response = $this + ->actingAs($actingUser) + ->patch("/user/{$user->id}", [ + 'name' => 'Updated User', + 'email' => 'updated@example.com', + ]); + + $response + ->assertRedirect("/user/{$user->id}/edit"); + + $user->refresh(); + + $this->assertSame('Updated User', $user->name); + $this->assertSame('updated@example.com', $user->email); +}); From b0eef8fca125c61702a53d2691528e94dff49a03 Mon Sep 17 00:00:00 2001 From: Iszuddin Ismail Date: Mon, 11 May 2026 12:04:56 +0800 Subject: [PATCH 5/6] fixed missed conflict --- resources/views/layouts/navigation.blade.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/resources/views/layouts/navigation.blade.php b/resources/views/layouts/navigation.blade.php index b543973..9e9e0a1 100644 --- a/resources/views/layouts/navigation.blade.php +++ b/resources/views/layouts/navigation.blade.php @@ -76,13 +76,11 @@ {{ __('Dashboard') }} -<<<<<<< HEAD {{ __('Users') }} -======= + {{ __('Roles') }} ->>>>>>> role-module From 8c909adf62e8b9a738863ab7756aa17b932a7275 Mon Sep 17 00:00:00 2001 From: Iszuddin Ismail Date: Mon, 11 May 2026 12:28:38 +0800 Subject: [PATCH 6/6] user role edit --- app/Http/Controllers/UserController.php | 15 ++++++++- app/Http/Requests/UpdateUserRequest.php | 2 ++ app/Models/Role.php | 12 ++++++- app/Models/User.php | 9 ++++++ ...26_05_11_040743_create_role_user_table.php | 29 +++++++++++++++++ resources/views/user/edit.blade.php | 24 ++++++++++++++ tests/Feature/UserTest.php | 32 ++++++++++++++++++- 7 files changed, 120 insertions(+), 3 deletions(-) create mode 100644 database/migrations/2026_05_11_040743_create_role_user_table.php diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index 7796359..9a21442 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers; use App\Http\Requests\UpdateUserRequest; +use App\Models\Role; use App\Models\User; use Illuminate\Http\RedirectResponse; use Illuminate\View\View; @@ -28,8 +29,14 @@ class UserController extends Controller */ public function edit(User $user): View { + $user->load('roles:id'); + $roles = Role::query() + ->orderBy('name') + ->get(['id', 'name']); + return view('user.edit', [ 'user' => $user, + 'roles' => $roles, ]); } @@ -38,13 +45,19 @@ class UserController extends Controller */ public function update(UpdateUserRequest $request, User $user): RedirectResponse { - $user->fill($request->validated()); + $validated = $request->validated(); + $roleIds = $validated['roles'] ?? []; + + unset($validated['roles']); + + $user->fill($validated); if ($user->isDirty('email')) { $user->email_verified_at = null; } $user->save(); + $user->roles()->sync($roleIds); return redirect() ->route('user.edit', $user) diff --git a/app/Http/Requests/UpdateUserRequest.php b/app/Http/Requests/UpdateUserRequest.php index 469ee8f..26a00b5 100644 --- a/app/Http/Requests/UpdateUserRequest.php +++ b/app/Http/Requests/UpdateUserRequest.php @@ -32,6 +32,8 @@ class UpdateUserRequest extends FormRequest 'max:255', Rule::unique('users', 'email')->ignore($this->route('user')), ], + 'roles' => ['nullable', 'array'], + 'roles.*' => ['integer', 'exists:roles,id'], ]; } } diff --git a/app/Models/Role.php b/app/Models/Role.php index c5fa26b..1ff0fd7 100644 --- a/app/Models/Role.php +++ b/app/Models/Role.php @@ -4,6 +4,16 @@ namespace App\Models; use Illuminate\Database\Eloquent\Attributes\Fillable; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; #[Fillable(['name'])] -class Role extends Model {} +class Role extends Model +{ + /** + * The users that belong to the role. + */ + public function users(): BelongsToMany + { + return $this->belongsToMany(User::class); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index f6ba1d2..4ec3016 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -7,6 +7,7 @@ use Database\Factories\UserFactory; use Illuminate\Database\Eloquent\Attributes\Fillable; use Illuminate\Database\Eloquent\Attributes\Hidden; use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; @@ -29,4 +30,12 @@ class User extends Authenticatable 'password' => 'hashed', ]; } + + /** + * The roles that belong to the user. + */ + public function roles(): BelongsToMany + { + return $this->belongsToMany(Role::class); + } } diff --git a/database/migrations/2026_05_11_040743_create_role_user_table.php b/database/migrations/2026_05_11_040743_create_role_user_table.php new file mode 100644 index 0000000..fce2c58 --- /dev/null +++ b/database/migrations/2026_05_11_040743_create_role_user_table.php @@ -0,0 +1,29 @@ +foreignId('role_id')->constrained()->cascadeOnDelete(); + $table->foreignId('user_id')->constrained()->cascadeOnDelete(); + + $table->primary(['role_id', 'user_id']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('role_user'); + } +}; diff --git a/resources/views/user/edit.blade.php b/resources/views/user/edit.blade.php index 399a4f1..9b93407 100644 --- a/resources/views/user/edit.blade.php +++ b/resources/views/user/edit.blade.php @@ -31,6 +31,30 @@ +
+ + +
+ @forelse ($roles as $role) + + @empty +

{{ __('No roles available.') }}

+ @endforelse +
+ + + +
+
{{ __('Save') }} diff --git a/tests/Feature/UserTest.php b/tests/Feature/UserTest.php index 7d4b744..93393de 100644 --- a/tests/Feature/UserTest.php +++ b/tests/Feature/UserTest.php @@ -1,5 +1,6 @@ create(); $user = User::factory()->create(); + $role = Role::query()->create(['name' => 'Admin']); + $user->roles()->attach($role->id); $response = $this ->actingAs($actingUser) @@ -20,18 +23,24 @@ test('authenticated users can view the user edit page', function () { ->assertSuccessful() ->assertSee('Edit User') ->assertSee($user->name) - ->assertSee($user->email); + ->assertSee($user->email) + ->assertSee('Roles') + ->assertSee('Admin') + ->assertSee('checked', false); }); test('authenticated users can update a user', function () { $actingUser = User::factory()->create(); $user = User::factory()->create(); + $adminRole = Role::query()->create(['name' => 'Admin']); + $editorRole = Role::query()->create(['name' => 'Editor']); $response = $this ->actingAs($actingUser) ->patch("/user/{$user->id}", [ 'name' => 'Updated User', 'email' => 'updated@example.com', + 'roles' => [$adminRole->id, $editorRole->id], ]); $response @@ -41,4 +50,25 @@ test('authenticated users can update a user', function () { $this->assertSame('Updated User', $user->name); $this->assertSame('updated@example.com', $user->email); + expect($user->roles()->pluck('roles.id')->all()) + ->toMatchArray([$adminRole->id, $editorRole->id]); +}); + +test('user roles can be cleared by submitting no roles', function () { + $actingUser = User::factory()->create(); + $user = User::factory()->create(); + $role = Role::query()->create(['name' => 'Admin']); + + $user->roles()->attach($role->id); + + $response = $this + ->actingAs($actingUser) + ->patch("/user/{$user->id}", [ + 'name' => $user->name, + 'email' => $user->email, + ]); + + $response->assertRedirect("/user/{$user->id}/edit"); + + expect($user->refresh()->roles()->count())->toBe(0); });
Name Email CreatedActions
{{ $user->name }} {{ $user->email }} {{ $user->created_at?->format('Y-m-d H:i') }} + + {{ __('Edit') }} + +
+ No users found.