diff --git a/apps/backend/app/Http/Controllers/Api/RegionController.php b/apps/backend/app/Http/Controllers/Api/RegionController.php new file mode 100644 index 00000000..0090a47e --- /dev/null +++ b/apps/backend/app/Http/Controllers/Api/RegionController.php @@ -0,0 +1,96 @@ +json([ + 'data' => Hamlet::orderBy('name')->get(), + ]); + } + + public function storeHamlet(Request $request) + { + $this->authorizePetugasDesa($request); + + $data = $request->validate([ + 'name' => 'required|string|max:255', + 'code' => 'required|string|max:50|unique:hamlets,code', + ], [ + 'code.unique' => 'Kode dusun sudah digunakan', + ]); + + $hamlet = Hamlet::create([ + 'name' => $data['name'], + 'code' => $data['code'], + 'village_id' => $request->user()->village_id, + 'is_active' => true, + ]); + + return response()->json(['data' => $hamlet], 201); + } + + public function updateHamlet(Request $request, Hamlet $hamlet) + { + $this->authorizePetugasDesa($request); + + $data = $request->validate([ + 'name' => 'sometimes|string|max:255', + 'is_active' => 'sometimes|boolean', + ]); + + $this->guardDeactivation($data, $hamlet, Citizen::where('hamlet_id', $hamlet->id)); + + $hamlet->update($data); + + return response()->json(['data' => $hamlet]); + } + + public function destroyHamlet(Request $request, Hamlet $hamlet) + { + $this->authorizePetugasDesa($request); + + $citizenQuery = Citizen::where('hamlet_id', $hamlet->id); + + if ($citizenQuery->exists()) { + abort(409, 'Dusun tidak bisa dihapus karena masih ada warga terdaftar di wilayah ini.'); + } + + $hamlet->delete(); + + return response()->json(['message' => 'Dusun berhasil dihapus.']); + } + + // ========================================== + // HELPERS + // ========================================== + + private function authorizePetugasDesa(Request $request) + { + if ($request->user()->role !== 'petugas_desa') { + abort(403, 'Hanya Petugas Desa yang berwenang.'); + } + } + + private function guardDeactivation(array $data, $region, $citizenQuery) + { + $isDeactivating = array_key_exists('is_active', $data) + && !$data['is_active'] + && $region->is_active; + + if ($isDeactivating && $citizenQuery->where('is_active', true)->exists()) { + abort(409, 'Dusun tidak bisa dinonaktifkan karena masih ada warga aktif terdaftar di wilayah ini.'); + } + } +} diff --git a/apps/backend/routes/api.php b/apps/backend/routes/api.php index 40026d79..31678a35 100644 --- a/apps/backend/routes/api.php +++ b/apps/backend/routes/api.php @@ -19,6 +19,7 @@ use App\Http\Controllers\Api\LetterDownloadController; use App\Http\Controllers\VillageController; use App\Http\Controllers\Api\UserController; +use App\Http\Controllers\Api\RegionController; //use App\Http\Controllers\Api\OfficialController; /* @@ -97,6 +98,11 @@ Route::get('/letter-types', [LetterTypeController::class, 'index']); + Route::get('/hamlets', [RegionController::class, 'indexHamlets']); + Route::post('/hamlets', [RegionController::class, 'storeHamlet']); + Route::patch('/hamlets/{hamlet}', [RegionController::class, 'updateHamlet']); + Route::delete('/hamlets/{hamlet}', [RegionController::class, 'destroyHamlet']); + Route::post('/letters', [LetterController::class, 'store']); Route::get('/letters', [LetterController::class, 'index']); diff --git a/apps/backend/tests/Feature/RegionControllerTest.php b/apps/backend/tests/Feature/RegionControllerTest.php new file mode 100644 index 00000000..845ba560 --- /dev/null +++ b/apps/backend/tests/Feature/RegionControllerTest.php @@ -0,0 +1,105 @@ +create([ + 'village_id' => $village->id, + 'role' => 'petugas_desa', + ]); + } + + public function test_petugas_desa_can_create_list_and_update_hamlets(): void + { + $village = Village::create(['name' => 'Desa Cibenda', 'code' => 'CBD']); + $user = $this->petugasDesa($village); + + $this->actingAs($user) + ->postJson('/api/hamlets', ['name' => 'Dusun Patrol', 'code' => 'PTR']) + ->assertCreated() + ->assertJsonPath('data.name', 'Dusun Patrol'); + + $hamlet = Hamlet::firstWhere('code', 'PTR'); + + $this->actingAs($user) + ->getJson('/api/hamlets') + ->assertOk() + ->assertJsonCount(1, 'data'); + + $this->actingAs($user) + ->patchJson("/api/hamlets/{$hamlet->id}", ['name' => 'Dusun Patrol Baru']) + ->assertOk() + ->assertJsonPath('data.name', 'Dusun Patrol Baru'); + } + + public function test_non_petugas_desa_cannot_create_hamlet(): void + { + $village = Village::create(['name' => 'Desa Cibenda', 'code' => 'CBD']); + $user = User::factory()->create(['village_id' => $village->id, 'role' => 'rt']); + + $this->actingAs($user) + ->postJson('/api/hamlets', ['name' => 'Dusun Patrol', 'code' => 'PTR']) + ->assertForbidden(); + } + + public function test_duplicate_hamlet_code_is_rejected(): void + { + $village = Village::create(['name' => 'Desa Cibenda', 'code' => 'CBD']); + $user = $this->petugasDesa($village); + Hamlet::create(['name' => 'Dusun A', 'code' => 'PTR', 'village_id' => $village->id, 'is_active' => true]); + + $this->actingAs($user) + ->postJson('/api/hamlets', ['name' => 'Dusun B', 'code' => 'PTR']) + ->assertUnprocessable() + ->assertJsonValidationErrors('code'); + } + + public function test_deactivating_hamlet_with_active_citizens_is_blocked(): void + { + $village = Village::create(['name' => 'Desa Cibenda', 'code' => 'CBD']); + $user = $this->petugasDesa($village); + $hamlet = Hamlet::create(['name' => 'Dusun A', 'code' => 'PTR', 'village_id' => $village->id, 'is_active' => true]); + + Citizen::create([ + 'village_id' => $village->id, + 'nik' => '3218030101010001', + 'nik_hash' => hash('sha256', '3218030101010001'), + 'name' => 'Warga Aktif', + 'date_of_birth' => '1990-01-01', + 'gender' => 'L', + 'address' => 'Desa Cibenda', + 'hamlet_id' => $hamlet->id, + 'is_active' => true, + ]); + + $this->actingAs($user) + ->patchJson("/api/hamlets/{$hamlet->id}", ['is_active' => false]) + ->assertStatus(409); + + $this->assertTrue($hamlet->fresh()->is_active); + } + + public function test_deactivating_hamlet_without_active_citizens_succeeds(): void + { + $village = Village::create(['name' => 'Desa Cibenda', 'code' => 'CBD']); + $user = $this->petugasDesa($village); + $hamlet = Hamlet::create(['name' => 'Dusun A', 'code' => 'PTR', 'village_id' => $village->id, 'is_active' => true]); + + $this->actingAs($user) + ->patchJson("/api/hamlets/{$hamlet->id}", ['is_active' => false]) + ->assertOk() + ->assertJsonPath('data.is_active', false); + } +}