feat(basicdata): tambahkan fitur relasi parent-child pada cabang
- Menambahkan kolom `parent_id` pada tabel `branches` dengan migrasi baru. - Update model `Branch`: - Menambahkan relasi `parent()` untuk mendapatkan cabang induk. - Menambahkan relasi `children()` untuk mendapatkan anak cabang. - Update `BranchController`: - Menampilkan daftar cabang induk saat membuat atau mengedit cabang. - Cek validasi agar cabang tidak bisa menjadi induk dirinya sendiri. - Tambahkan larangan hapus cabang jika memiliki anak cabang, baik untuk hapus tunggal maupun multiple. - Update validation rules pada `BranchRequest` untuk memastikan validitas `parent_id`. - Update tampilan: - Formulir pembuatan/edit cabang: Menampilkan dropdown untuk memilih cabang induk. - Daftar cabang: Menampilkan kolom untuk cabang induk. - Tambahkan test unit: - Validasi relasi parent-child pada penyimpanan dan pembaruan cabang. - Melarang penghapusan cabang yang memiliki anak. - Memastikan perilaku relasi parent-child sesuai ekspektasi. Signed-off-by: Daeng Deni Mardaeni <ddeni05@gmail.com>
This commit is contained in:
@@ -14,7 +14,8 @@
|
||||
{
|
||||
protected $user;
|
||||
|
||||
public function __construct(){
|
||||
public function __construct()
|
||||
{
|
||||
$this->user = auth()->user();
|
||||
}
|
||||
|
||||
@@ -58,8 +59,8 @@
|
||||
if (is_null($this->user) || !$this->user->can('basic-data.create')) {
|
||||
abort(403, 'Sorry! You are not allowed to create branches.');
|
||||
}
|
||||
|
||||
return view('basicdata::branch.create');
|
||||
$branches = Branch::all();
|
||||
return view('basicdata::branch.create', compact('branches'));
|
||||
}
|
||||
|
||||
public function edit($id)
|
||||
@@ -69,8 +70,9 @@
|
||||
abort(403, 'Sorry! You are not allowed to update branches.');
|
||||
}
|
||||
|
||||
$branch = Branch::find($id);
|
||||
return view('basicdata::branch.create', compact('branch'));
|
||||
$branch = Branch::findOrFail($id);
|
||||
$branches = Branch::all();
|
||||
return view('basicdata::branch.create', compact('branch', 'branches'));
|
||||
}
|
||||
|
||||
public function update(BranchRequest $request, $id)
|
||||
@@ -82,6 +84,14 @@
|
||||
|
||||
$validate = $request->validated();
|
||||
|
||||
// Tambahkan validasi manual untuk memeriksa parent_id
|
||||
if (isset($validate['parent_id']) && $validate['parent_id'] == $id) {
|
||||
return redirect()
|
||||
->back()
|
||||
->withInput()
|
||||
->withErrors(['parent_id' => 'Cabang tidak dapat menjadi induk dari dirinya sendiri.']);
|
||||
}
|
||||
|
||||
if ($validate) {
|
||||
try {
|
||||
// Update in database
|
||||
@@ -102,12 +112,25 @@
|
||||
{
|
||||
// Check if the authenticated user has the required permission to delete branches
|
||||
if (is_null($this->user) || !$this->user->can('basic-data.delete')) {
|
||||
return response()->json(['success' => false, 'message' => 'Sorry! You are not allowed to delete branches.'], 403);
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Sorry! You are not allowed to delete branches.'
|
||||
], 403);
|
||||
}
|
||||
|
||||
try {
|
||||
// Delete from database
|
||||
// Find the branch
|
||||
$branch = Branch::find($id);
|
||||
|
||||
// Check if the branch has children
|
||||
if ($branch->children()->exists()) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Cabang dengan anak cabang tidak dapat dihapus.'
|
||||
], 422);
|
||||
}
|
||||
|
||||
// Delete from database
|
||||
$branch->delete();
|
||||
|
||||
return response()->json(['success' => true, 'message' => 'Branch deleted successfully']);
|
||||
@@ -120,10 +143,26 @@
|
||||
{
|
||||
// Check if the authenticated user has the required permission to delete branches
|
||||
if (is_null($this->user) || !$this->user->can('basic-data.delete')) {
|
||||
return response()->json(['success' => false, 'message' => 'Sorry! You are not allowed to delete branches.'], 403);
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Sorry! You are not allowed to delete branches.'
|
||||
], 403);
|
||||
}
|
||||
|
||||
$ids = $request->input('ids');
|
||||
|
||||
// Check if any of the branches have children
|
||||
$branchesWithChildren = Branch::whereIn('id', $ids)
|
||||
->whereHas('children')
|
||||
->get();
|
||||
|
||||
if ($branchesWithChildren->count() > 0) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Beberapa cabang memiliki anak cabang dan tidak dapat dihapus.'
|
||||
], 422);
|
||||
}
|
||||
|
||||
Branch::whereIn('id', $ids)->delete();
|
||||
return response()->json(['success' => true, 'message' => 'Branches deleted successfully']);
|
||||
}
|
||||
@@ -132,7 +171,10 @@
|
||||
{
|
||||
// Check if the authenticated user has the required permission to view branches
|
||||
if (is_null($this->user) || !$this->user->can('basic-data.read')) {
|
||||
return response()->json(['success' => false, 'message' => 'Sorry! You are not allowed to view branches.'], 403);
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Sorry! You are not allowed to view branches.'
|
||||
], 403);
|
||||
}
|
||||
|
||||
// Retrieve data from the database
|
||||
@@ -172,12 +214,22 @@
|
||||
// Get the data for the current page
|
||||
$data = $query->get();
|
||||
|
||||
$data = $data->map(function ($item) {
|
||||
return [
|
||||
'id' => $item->id,
|
||||
'code' => $item->code,
|
||||
'name' => $item->name,
|
||||
'parent_id' => $item->parent?->name ?? null,
|
||||
];
|
||||
});
|
||||
|
||||
// Calculate the page count
|
||||
$pageCount = ceil($totalRecords / $request->get('size'));
|
||||
|
||||
// Calculate the current page number
|
||||
$currentPage = 0 + 1;
|
||||
|
||||
|
||||
// Return the response data as a JSON object
|
||||
return response()->json([
|
||||
'draw' => $request->get('draw'),
|
||||
|
||||
@@ -15,6 +15,15 @@
|
||||
{
|
||||
$rules = [
|
||||
'name' => 'required|string|max:255',
|
||||
'parent_id' => [
|
||||
'nullable',
|
||||
'exists:branches,id',
|
||||
function ($attribute, $value, $fail) {
|
||||
if ($value == $this->route('branch')) {
|
||||
$fail('Cabang tidak dapat menjadi induk dari dirinya sendiri.');
|
||||
}
|
||||
},
|
||||
],
|
||||
'status' => 'nullable|boolean',
|
||||
'authorized_at' => 'nullable|datetime',
|
||||
'authorized_status' => 'nullable|string|max:1',
|
||||
|
||||
@@ -6,5 +6,21 @@
|
||||
class Branch extends Base
|
||||
{
|
||||
protected $table = 'branches';
|
||||
protected $fillable = ['code', 'name', 'status', 'authorized_at', 'authorized_status', 'authorized_by'];
|
||||
protected $fillable = ['code', 'name', 'status', 'authorized_at', 'authorized_status', 'authorized_by', 'parent_id'];
|
||||
|
||||
/**
|
||||
* Get the parent branch of this branch
|
||||
*/
|
||||
public function parent()
|
||||
{
|
||||
return $this->belongsTo(Branch::class, 'parent_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the child branches of this branch
|
||||
*/
|
||||
public function children()
|
||||
{
|
||||
return $this->hasMany(Branch::class, 'parent_id');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
<?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('branches', function (Blueprint $table) {
|
||||
$table->unsignedBigInteger('parent_id')->nullable()->after('name');
|
||||
$table->foreign('parent_id')->references('id')->on('branches')->onDelete('set null');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('branches', function (Blueprint $table) {
|
||||
$table->dropForeign(['parent_id']);
|
||||
$table->dropColumn('parent_id');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -46,6 +46,26 @@
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-baseline flex-wrap lg:flex-nowrap gap-2.5">
|
||||
<label class="form-label max-w-56">
|
||||
Parent Branch
|
||||
</label>
|
||||
<div class="flex flex-wrap items-baseline w-full">
|
||||
<select class="input @error('parent_id') border-danger bg-danger-light @enderror" name="parent_id">
|
||||
<option value="">-- Select Parent Branch --</option>
|
||||
@foreach($branches as $parentBranch)
|
||||
@if(!isset($branch->id) || $parentBranch->id != $branch->id)
|
||||
<option value="{{ $parentBranch->id }}" {{ (isset($branch->parent_id) && $branch->parent_id == $parentBranch->id) ? 'selected' : '' }}>
|
||||
{{ $parentBranch->code }} - {{ $parentBranch->name }}
|
||||
</option>
|
||||
@endif
|
||||
@endforeach
|
||||
</select>
|
||||
@error('parent_id')
|
||||
<em class="alert text-danger text-sm">{{ $message }}</em>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
@if(isset($branch->id))
|
||||
@can('basic-data.update')
|
||||
|
||||
@@ -47,6 +47,10 @@
|
||||
<span class="sort"> <span class="sort-label"> Cabang </span>
|
||||
<span class="sort-icon"> </span> </span>
|
||||
</th>
|
||||
<th class="min-w-[250px]" data-datatable-column="name">
|
||||
<span class="sort"> <span class="sort-label"> Cabang Induk</span>
|
||||
<span class="sort-icon"> </span> </span>
|
||||
</th>
|
||||
<th class="min-w-[50px] text-center" data-datatable-column="actions">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -168,6 +172,9 @@
|
||||
name: {
|
||||
title: 'Cabang',
|
||||
},
|
||||
parent_id: {
|
||||
title: 'Cabang Induk',
|
||||
},
|
||||
actions: {
|
||||
title: 'Status',
|
||||
render: (item, data) => {
|
||||
|
||||
@@ -18,6 +18,7 @@ class BranchControllerTest extends TestCase
|
||||
protected $user;
|
||||
protected $adminRole;
|
||||
protected $branch;
|
||||
protected $parentBranch;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
@@ -64,6 +65,12 @@ class BranchControllerTest extends TestCase
|
||||
$this->user = User::factory()->create();
|
||||
$this->user->assignRole($this->adminRole);
|
||||
|
||||
// Create a parent branch for testing
|
||||
$this->parentBranch = Branch::create([
|
||||
'code' => 'PARENT',
|
||||
'name' => 'Parent Branch'
|
||||
]);
|
||||
|
||||
// Create a branch for testing
|
||||
$this->branch = Branch::create([
|
||||
'code' => 'TEST',
|
||||
@@ -137,6 +144,28 @@ class BranchControllerTest extends TestCase
|
||||
$this->assertDatabaseHas('branches', $branchData);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function user_with_permission_can_store_branch_with_parent()
|
||||
{
|
||||
$branchData = [
|
||||
'code' => 'CHILD',
|
||||
'name' => 'Child Branch',
|
||||
'parent_id' => $this->parentBranch->id
|
||||
];
|
||||
|
||||
$response = $this->actingAs($this->user)
|
||||
->post(route('basicdata.branch.store'), $branchData);
|
||||
|
||||
$response->assertRedirect(route('basicdata.branch.index'));
|
||||
$this->assertDatabaseHas('branches', $branchData);
|
||||
|
||||
// Verify the relationship
|
||||
$childBranch = Branch::where('code', 'CHILD')->first();
|
||||
$this->assertEquals($this->parentBranch->id, $childBranch->parent_id);
|
||||
$this->assertTrue($childBranch->parent()->exists());
|
||||
$this->assertEquals($this->parentBranch->id, $childBranch->parent->id);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function user_without_permission_cannot_store_branch()
|
||||
{
|
||||
@@ -201,6 +230,53 @@ class BranchControllerTest extends TestCase
|
||||
$this->assertDatabaseHas('branches', $updatedData);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function user_with_permission_can_update_branch_with_parent()
|
||||
{
|
||||
$updatedData = [
|
||||
'code' => 'UPD',
|
||||
'name' => 'Updated Branch',
|
||||
'parent_id' => $this->parentBranch->id
|
||||
];
|
||||
|
||||
$response = $this->actingAs($this->user)
|
||||
->put(route('basicdata.branch.update', $this->branch->id), $updatedData);
|
||||
|
||||
$response->assertRedirect(route('basicdata.branch.index'));
|
||||
$this->assertDatabaseHas('branches', $updatedData);
|
||||
|
||||
// Verify the relationship
|
||||
$this->branch->refresh();
|
||||
$this->assertEquals($this->parentBranch->id, $this->branch->parent_id);
|
||||
$this->assertTrue($this->branch->parent()->exists());
|
||||
$this->assertEquals($this->parentBranch->id, $this->branch->parent->id);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function user_with_permission_can_remove_parent_from_branch()
|
||||
{
|
||||
// First set a parent
|
||||
$this->branch->update(['parent_id' => $this->parentBranch->id]);
|
||||
$this->assertEquals($this->parentBranch->id, $this->branch->parent_id);
|
||||
|
||||
// Then remove it
|
||||
$updatedData = [
|
||||
'code' => 'UPD',
|
||||
'name' => 'Updated Branch',
|
||||
'parent_id' => null
|
||||
];
|
||||
|
||||
$response = $this->actingAs($this->user)
|
||||
->put(route('basicdata.branch.update', $this->branch->id), $updatedData);
|
||||
|
||||
$response->assertRedirect(route('basicdata.branch.index'));
|
||||
|
||||
// Verify the relationship is removed
|
||||
$this->branch->refresh();
|
||||
$this->assertNull($this->branch->parent_id);
|
||||
$this->assertFalse($this->branch->parent()->exists());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function user_without_permission_cannot_update_branch()
|
||||
{
|
||||
@@ -277,4 +353,126 @@ class BranchControllerTest extends TestCase
|
||||
|
||||
$response->assertStatus(403);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function branch_cannot_be_its_own_parent()
|
||||
{
|
||||
$updatedData = [
|
||||
'code' => 'SELF',
|
||||
'name' => 'Self-Referencing Branch',
|
||||
'parent_id' => $this->branch->id
|
||||
];
|
||||
|
||||
$response = $this->actingAs($this->user)
|
||||
->from(route('basicdata.branch.edit', $this->branch->id)) // Tambahkan ini untuk mengetahui URL sebelumnya
|
||||
->put(route('basicdata.branch.update', $this->branch->id), $updatedData);
|
||||
|
||||
// Pastikan redirect back dengan kesalahan
|
||||
$response->assertRedirect();
|
||||
$response->assertSessionHasErrors('parent_id');
|
||||
|
||||
// Periksa bahwa parent_id tidak berubah
|
||||
$this->branch->refresh();
|
||||
$this->assertNull($this->branch->parent_id);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function cannot_delete_branch_with_children()
|
||||
{
|
||||
// Create a child branch
|
||||
$childBranch = Branch::create([
|
||||
'code' => 'CHILD',
|
||||
'name' => 'Child Branch',
|
||||
'parent_id' => $this->parentBranch->id
|
||||
]);
|
||||
|
||||
// Verify the relationship is established
|
||||
$this->assertEquals($this->parentBranch->id, $childBranch->parent_id);
|
||||
$this->assertTrue($this->parentBranch->children()->exists());
|
||||
|
||||
// Try to delete the parent branch
|
||||
$response = $this->actingAs($this->user)
|
||||
->delete(route('basicdata.branch.destroy', $this->parentBranch->id));
|
||||
|
||||
// Assert that the request fails with the expected message
|
||||
$response->assertJson([
|
||||
'success' => false,
|
||||
'message' => 'Cabang dengan anak cabang tidak dapat dihapus.'
|
||||
]);
|
||||
$response->assertStatus(422); // Unprocessable Entity
|
||||
|
||||
// Verify the parent branch was not deleted
|
||||
$this->assertDatabaseHas('branches', [
|
||||
'id' => $this->parentBranch->id,
|
||||
'deleted_at' => null
|
||||
]);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function cannot_delete_multiple_branches_if_any_has_children()
|
||||
{
|
||||
// Create a child branch
|
||||
$childBranch = Branch::create([
|
||||
'code' => 'CHILD',
|
||||
'name' => 'Child Branch',
|
||||
'parent_id' => $this->parentBranch->id
|
||||
]);
|
||||
|
||||
// Create another branch without children
|
||||
$anotherBranch = Branch::create([
|
||||
'code' => 'ANOTHER',
|
||||
'name' => 'Another Branch'
|
||||
]);
|
||||
|
||||
// Try to delete both the parent branch and another branch
|
||||
$response = $this->actingAs($this->user)
|
||||
->post(route('basicdata.branch.deleteMultiple'), [
|
||||
'ids' => [$this->parentBranch->id, $anotherBranch->id]
|
||||
]);
|
||||
|
||||
// Assert that the request fails with the expected message
|
||||
$response->assertJson([
|
||||
'success' => false,
|
||||
'message' => 'Beberapa cabang memiliki anak cabang dan tidak dapat dihapus.'
|
||||
]);
|
||||
$response->assertStatus(422); // Unprocessable Entity
|
||||
|
||||
// Verify neither branch was deleted
|
||||
$this->assertDatabaseHas('branches', [
|
||||
'id' => $this->parentBranch->id,
|
||||
'deleted_at' => null
|
||||
]);
|
||||
$this->assertDatabaseHas('branches', [
|
||||
'id' => $anotherBranch->id,
|
||||
'deleted_at' => null
|
||||
]);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function branch_has_correct_children_relationship()
|
||||
{
|
||||
// Create a child branch
|
||||
$childBranch = Branch::create([
|
||||
'code' => 'CHILD1',
|
||||
'name' => 'Child Branch 1',
|
||||
'parent_id' => $this->parentBranch->id
|
||||
]);
|
||||
|
||||
// Create another child branch
|
||||
$anotherChildBranch = Branch::create([
|
||||
'code' => 'CHILD2',
|
||||
'name' => 'Child Branch 2',
|
||||
'parent_id' => $this->parentBranch->id
|
||||
]);
|
||||
|
||||
// Refresh parent branch
|
||||
$this->parentBranch->refresh();
|
||||
|
||||
// Assert that the parent has two children
|
||||
$this->assertEquals(2, $this->parentBranch->children()->count());
|
||||
|
||||
// Assert that the children collection contains the two child branches
|
||||
$this->assertTrue($this->parentBranch->children->contains($childBranch));
|
||||
$this->assertTrue($this->parentBranch->children->contains($anotherChildBranch));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user