feat: kolom phone di manajemen user (tampil di tabel, input modal, validasi regex) + FIX bug: catch-all exception handler mengubah ValidationException jadi 500 (kini redirect+error normal)

This commit is contained in:
Sari
2026-08-03 09:30:14 +07:00
parent 2e7ab00c31
commit f5a80fd4e2
6 changed files with 123 additions and 6 deletions
@@ -43,6 +43,7 @@ class UserController extends Controller
'name' => ['required', 'string', 'max:255'], 'name' => ['required', 'string', 'max:255'],
'username' => ['required', 'string', 'max:255', 'unique:users,username'], 'username' => ['required', 'string', 'max:255', 'unique:users,username'],
'email' => ['required', 'string', 'email', 'max:255', 'unique:users,email'], 'email' => ['required', 'string', 'email', 'max:255', 'unique:users,email'],
'phone' => ['nullable', 'string', 'max:20', 'regex:/^[0-9+\-\s()]*$/'],
'password' => ['required', 'string', 'min:6'], 'password' => ['required', 'string', 'min:6'],
'roles' => ['required', 'array', 'min:1'], 'roles' => ['required', 'array', 'min:1'],
'roles.*' => ['in:admin,bk,wali_kelas,staff,other,kepala_sekolah,waka_kesiswaan,ketua_tim'], 'roles.*' => ['in:admin,bk,wali_kelas,staff,other,kepala_sekolah,waka_kesiswaan,ketua_tim'],
@@ -67,6 +68,7 @@ class UserController extends Controller
'name' => ['required', 'string', 'max:255'], 'name' => ['required', 'string', 'max:255'],
'username' => ['required', 'string', 'max:255', 'unique:users,username,' . $user->id], 'username' => ['required', 'string', 'max:255', 'unique:users,username,' . $user->id],
'email' => ['required', 'string', 'email', 'max:255', 'unique:users,email,' . $user->id], 'email' => ['required', 'string', 'email', 'max:255', 'unique:users,email,' . $user->id],
'phone' => ['nullable', 'string', 'max:20', 'regex:/^[0-9+\-\s()]*$/'],
'password' => ['nullable', 'string', 'min:6'], 'password' => ['nullable', 'string', 'min:6'],
'roles' => ['required', 'array', 'min:1'], 'roles' => ['required', 'array', 'min:1'],
'roles.*' => ['in:admin,bk,wali_kelas,staff,other,kepala_sekolah,waka_kesiswaan,ketua_tim'], 'roles.*' => ['in:admin,bk,wali_kelas,staff,other,kepala_sekolah,waka_kesiswaan,ketua_tim'],
+1 -1
View File
@@ -13,7 +13,7 @@ class User extends Authenticatable
public const ROLES = ['admin', 'bk', 'wali_kelas', 'staff', 'other', 'kepala_sekolah', 'waka_kesiswaan', 'ketua_tim']; public const ROLES = ['admin', 'bk', 'wali_kelas', 'staff', 'other', 'kepala_sekolah', 'waka_kesiswaan', 'ketua_tim'];
protected $fillable = [ protected $fillable = [
'name', 'username', 'email', 'password', 'role', 'roles', 'is_active', 'name', 'username', 'email', 'phone', 'password', 'role', 'roles', 'is_active',
]; ];
protected $hidden = ['password', 'remember_token']; protected $hidden = ['password', 'remember_token'];
+4
View File
@@ -60,6 +60,10 @@ return Application::configure(basePath: dirname(__DIR__))
if ($e instanceof \Illuminate\Auth\AuthenticationException) { if ($e instanceof \Illuminate\Auth\AuthenticationException) {
return null; return null;
} }
// Biarkan default menangani: redirect balik + pesan error validasi
if ($e instanceof \Illuminate\Validation\ValidationException) {
return null;
}
\Illuminate\Support\Facades\Log::error('Unhandled exception: ' . get_class($e) . ' - ' . $e->getMessage() . ' @ ' . $request->path()); \Illuminate\Support\Facades\Log::error('Unhandled exception: ' . get_class($e) . ' - ' . $e->getMessage() . ' @ ' . $request->path());
return $htmlView('errors.500', 500, $request); return $htmlView('errors.500', 500, $request);
}); });
@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->string('phone', 20)->nullable()->after('email');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('phone');
});
}
};
+18 -5
View File
@@ -77,6 +77,9 @@
<div> <div>
<p class="text-sm font-medium text-gray-900">{{ $user->name }}</p> <p class="text-sm font-medium text-gray-900">{{ $user->name }}</p>
<p class="text-xs text-gray-400">{{ $user->email }}</p> <p class="text-xs text-gray-400">{{ $user->email }}</p>
@if ($user->phone)
<p class="text-xs text-gray-400 mt-0.5"><i class="fa-solid fa-phone text-[10px]"></i> {{ $user->phone }}</p>
@endif
</div> </div>
</div> </div>
</td> </td>
@@ -119,7 +122,7 @@
</td> </td>
<td class="px-5 py-4 text-right"> <td class="px-5 py-4 text-right">
<div class="flex items-center justify-end space-x-1"> <div class="flex items-center justify-end space-x-1">
<button @click="openEdit({{ $user->id }}, '{{ addslashes($user->name) }}', '{{ $user->username }}', '{{ $user->email }}', '{{ implode(',', $user->roleList()) }}', {{ $user->is_active ? 'true' : 'false' }})" <button @click="openEdit({{ $user->id }}, '{{ addslashes($user->name) }}', '{{ $user->username }}', '{{ $user->email }}', '{{ addslashes($user->phone ?? '') }}', '{{ implode(',', $user->roleList()) }}', {{ $user->is_active ? 'true' : 'false' }})"
class="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition" title="Edit"> class="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition" title="Edit">
<i class="fa-solid fa-pen-to-square"></i> <i class="fa-solid fa-pen-to-square"></i>
</button> </button>
@@ -234,6 +237,16 @@
placeholder="email@sekolah.sch.id"> placeholder="email@sekolah.sch.id">
</div> </div>
</div> </div>
<div>
<label class="block text-sm font-bold text-gray-700 mb-2">Nomor HP</label>
<div class="relative">
<i class="fa-solid fa-phone absolute left-4 top-1/2 -translate-y-1/2 text-gray-400 text-sm pointer-events-none"></i>
<input type="tel" x-model="formPhone" name="phone"
class="w-full pl-11 pr-4 py-3 border-2 border-gray-200 rounded-2xl text-sm font-semibold bg-gray-50/50 focus:bg-white focus:outline-none focus:ring-4 focus:ring-blue-500/15 focus:border-blue-500 transition"
placeholder="08xxxxxxxxxx">
</div>
<p class="text-xs text-gray-400 mt-1.5">Nomor untuk kontak &amp; notifikasi (opsional)</p>
</div>
</div> </div>
{{-- Password --}} {{-- Password --}}
@@ -331,7 +344,7 @@
function userManager() { function userManager() {
return { return {
modalOpen: false, isEditing: false, editId: null, modalOpen: false, isEditing: false, editId: null,
formName: '', formUsername: '', formEmail: '', formPassword: '', formRoles: ['bk'], formActive: true, formName: '', formUsername: '', formEmail: '', formPhone: '', formPassword: '', formRoles: ['bk'], formActive: true,
showPassword: false, showPassword: false,
get roleInfo() { get roleInfo() {
const map = { const map = {
@@ -349,12 +362,12 @@ function userManager() {
}, },
openCreate() { openCreate() {
this.isEditing = false; this.editId = null; this.isEditing = false; this.editId = null;
this.formName = ''; this.formUsername = ''; this.formEmail = ''; this.formPassword = ''; this.formName = ''; this.formUsername = ''; this.formEmail = ''; this.formPhone = ''; this.formPassword = '';
this.formRoles = ['bk']; this.formActive = true; this.showPassword = false; this.modalOpen = true; this.formRoles = ['bk']; this.formActive = true; this.showPassword = false; this.modalOpen = true;
}, },
openEdit(id, name, username, email, rolesCsv, active) { openEdit(id, name, username, email, phone, rolesCsv, active) {
this.isEditing = true; this.editId = id; this.isEditing = true; this.editId = id;
this.formName = name; this.formUsername = username; this.formEmail = email; this.formName = name; this.formUsername = username; this.formEmail = email; this.formPhone = phone;
this.formPassword = ''; this.formPassword = '';
this.formRoles = rolesCsv ? rolesCsv.split(',') : ['bk']; this.formRoles = rolesCsv ? rolesCsv.split(',') : ['bk'];
this.formActive = active; this.showPassword = false; this.modalOpen = true; this.formActive = active; this.showPassword = false; this.modalOpen = true;
+70
View File
@@ -0,0 +1,70 @@
<?php
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class UserManagementTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
$this->admin = User::factory()->create(['roles' => ['admin']]);
}
public function test_admin_menambah_user_dengan_nomor_hp(): void
{
$this->actingAs($this->admin)
->post('/users', [
'name' => 'Guru Baru',
'username' => 'gurubaru',
'email' => 'gurubaru@sekolah.sch.id',
'phone' => '081234567890',
'password' => 'rahasia123',
'roles' => ['bk'],
'is_active' => 1,
])
->assertRedirect();
$user = User::where('email', 'gurubaru@sekolah.sch.id')->firstOrFail();
$this->assertSame('081234567890', $user->phone);
}
public function test_admin_mengupdate_nomor_hp_user(): void
{
$user = User::factory()->create(['roles' => ['bk'], 'phone' => '081111111111']);
$this->actingAs($this->admin)
->put("/users/{$user->id}", [
'name' => $user->name,
'username' => $user->username,
'email' => $user->email,
'phone' => '082222222222',
'roles' => ['bk'],
'is_active' => 1,
])
->assertRedirect();
$this->assertSame('082222222222', $user->refresh()->phone);
}
public function test_nomor_hp_tidak_boleh_karakter_aneh(): void
{
$this->actingAs($this->admin)
->post('/users', [
'name' => 'Guru Aneh',
'username' => 'guruaneh',
'email' => 'guruaneh@sekolah.sch.id',
'phone' => 'abc; drop table',
'password' => 'rahasia123',
'roles' => ['bk'],
])
->assertSessionHasErrors('phone');
$this->assertDatabaseMissing('users', ['username' => 'guruaneh']);
}
}