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:
@@ -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
@@ -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'];
|
||||||
|
|||||||
@@ -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');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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 & 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;
|
||||||
|
|||||||
@@ -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']);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user