From a300eced76965d22bad37a087d297752d2306f5e Mon Sep 17 00:00:00 2001 From: Saufi Date: Mon, 11 May 2026 12:26:07 +0800 Subject: [PATCH] role listing --- .claude/settings.local.json | 9 ++++ app/Http/Controllers/RoleController.php | 20 +++++++++ app/Models/Role.php | 15 +++++++ database/factories/RoleFactory.php | 25 +++++++++++ .../2026_05_11_042341_create_roles_table.php | 29 +++++++++++++ database/seeders/RoleSeeder.php | 22 ++++++++++ resources/views/layouts/navigation.blade.php | 6 +++ resources/views/roles/_table.blade.php | 34 +++++++++++++++ resources/views/roles/index.blade.php | 42 +++++++++++++++++++ routes/web.php | 2 + tests/Feature/RoleIndexTest.php | 41 ++++++++++++++++++ 11 files changed, 245 insertions(+) create mode 100644 .claude/settings.local.json create mode 100644 app/Http/Controllers/RoleController.php create mode 100644 app/Models/Role.php create mode 100644 database/factories/RoleFactory.php create mode 100644 database/migrations/2026_05_11_042341_create_roles_table.php create mode 100644 database/seeders/RoleSeeder.php create mode 100644 resources/views/roles/_table.blade.php create mode 100644 resources/views/roles/index.blade.php create mode 100644 tests/Feature/RoleIndexTest.php diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..b94054c --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,9 @@ +{ + "permissions": { + "allow": [ + "mcp__laravel-boost__database-schema", + "Bash(php artisan *)", + "Bash(vendor/bin/pint --dirty --format agent)" + ] + } +} diff --git a/app/Http/Controllers/RoleController.php b/app/Http/Controllers/RoleController.php new file mode 100644 index 0000000..f2dbc01 --- /dev/null +++ b/app/Http/Controllers/RoleController.php @@ -0,0 +1,20 @@ +paginate(10); + + if ($request->ajax()) { + return view('roles._table', compact('roles')); + } + + return view('roles.index', compact('roles')); + } +} diff --git a/app/Models/Role.php b/app/Models/Role.php new file mode 100644 index 0000000..a68d904 --- /dev/null +++ b/app/Models/Role.php @@ -0,0 +1,15 @@ + */ + use HasFactory; + + protected $fillable = ['name', 'description']; +} diff --git a/database/factories/RoleFactory.php b/database/factories/RoleFactory.php new file mode 100644 index 0000000..ffbe866 --- /dev/null +++ b/database/factories/RoleFactory.php @@ -0,0 +1,25 @@ + + */ +class RoleFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'name' => fake()->unique()->word(), + 'description' => fake()->sentence(), + ]; + } +} diff --git a/database/migrations/2026_05_11_042341_create_roles_table.php b/database/migrations/2026_05_11_042341_create_roles_table.php new file mode 100644 index 0000000..5ea3089 --- /dev/null +++ b/database/migrations/2026_05_11_042341_create_roles_table.php @@ -0,0 +1,29 @@ +id(); + $table->string('name')->unique(); + $table->string('description')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('roles'); + } +}; diff --git a/database/seeders/RoleSeeder.php b/database/seeders/RoleSeeder.php new file mode 100644 index 0000000..1933f21 --- /dev/null +++ b/database/seeders/RoleSeeder.php @@ -0,0 +1,22 @@ + 'Admin', 'description' => 'Full access to all resources'], + ['name' => 'Editor', 'description' => 'Can create and edit content'], + ['name' => 'Viewer', 'description' => 'Read-only access'], + ]; + + foreach ($roles as $role) { + Role::firstOrCreate(['name' => $role['name']], $role); + } + } +} diff --git a/resources/views/layouts/navigation.blade.php b/resources/views/layouts/navigation.blade.php index c64bf64..a2b2431 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/roles/_table.blade.php b/resources/views/roles/_table.blade.php new file mode 100644 index 0000000..b6b38b2 --- /dev/null +++ b/resources/views/roles/_table.blade.php @@ -0,0 +1,34 @@ + + + + + + + + + + + @foreach ($roles as $role) + + + + + + + @endforeach + + @if ($roles->isEmpty()) + + + + @endif + +
#{{ __('Name') }}{{ __('Description') }}{{ __('Created') }}
{{ $roles->firstItem() + $loop->index }}{{ $role->name }}{{ $role->description ?? '—' }}{{ $role->created_at->format('d M Y') }}
+ {{ __('No roles found.') }} +
+ +@if ($roles->hasPages()) +
+ {{ $roles->links() }} +
+@endif diff --git a/resources/views/roles/index.blade.php b/resources/views/roles/index.blade.php new file mode 100644 index 0000000..114f6de --- /dev/null +++ b/resources/views/roles/index.blade.php @@ -0,0 +1,42 @@ + + +

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

+
+ +
+
+
+
+
+ {{ __('Loading...') }} +
+ +
+ @include('roles._table') +
+
+
+
+
+
diff --git a/routes/web.php b/routes/web.php index 74bb7ca..9c022e8 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('/roles', [RoleController::class, 'index'])->name('roles.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..78a2f2f --- /dev/null +++ b/tests/Feature/RoleIndexTest.php @@ -0,0 +1,41 @@ +get('/roles')->assertRedirect('/login'); +}); + +test('authenticated users can view the roles page', function () { + $this->actingAs(User::factory()->create()) + ->get('/roles') + ->assertOk() + ->assertViewIs('roles.index'); +}); + +test('roles page passes paginated roles to view', function () { + $this->actingAs(User::factory()->create()) + ->get('/roles') + ->assertOk() + ->assertViewHas('roles'); +}); + +test('ajax request returns roles table partial', function () { + $this->actingAs(User::factory()->create()) + ->withHeader('X-Requested-With', 'XMLHttpRequest') + ->get('/roles') + ->assertOk() + ->assertViewIs('roles._table'); +}); + +test('ajax pagination returns correct page', function () { + Role::factory()->count(15)->create(); + + $this->actingAs(User::factory()->create()) + ->withHeader('X-Requested-With', 'XMLHttpRequest') + ->get('/roles?page=2') + ->assertOk() + ->assertViewIs('roles._table') + ->assertViewHas('roles', fn ($roles) => $roles->currentPage() === 2); +});