From bca6da8653e2f9e624247e73b62370ea5039c941 Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Fri, 24 Jul 2026 23:31:08 +0700 Subject: [PATCH] feat: implement raw material deletion validation to prevent deletion if used in active cutting --- app/Services/Master/RawMaterialService.php | 18 +++++ .../table/RawMaterialGroupedTable.vue | 21 +----- .../table/data-table-actions.vue | 25 ++++--- .../table/raw-material-status-toggle.vue | 4 +- resources/js/types/raw-material.ts | 5 -- .../Feature/Admin/Master/RawMaterialTest.php | 66 ++++++++++--------- 6 files changed, 69 insertions(+), 70 deletions(-) diff --git a/app/Services/Master/RawMaterialService.php b/app/Services/Master/RawMaterialService.php index 1ef1099..1e1ee5a 100644 --- a/app/Services/Master/RawMaterialService.php +++ b/app/Services/Master/RawMaterialService.php @@ -2,8 +2,10 @@ namespace App\Services\Master; +use App\Enums\CuttingStatus; use App\Enums\OwnerVerificationAction; use App\Enums\RawMaterialUnit; +use App\Models\CuttingMaterial; use App\Models\OwnerVerificationRequest; use App\Models\RawMaterial; use App\Models\RawMaterialPrice; @@ -286,6 +288,8 @@ public function applyDelete(OwnerVerificationRequest $verificationRequest): void private function applyDeleteSubject(RawMaterial $rawMaterial): void { + $this->ensureNotUsedInActiveCutting($rawMaterial); + $this->runInTransaction( function () use ($rawMaterial): void { $rawMaterial->prices()->delete(); @@ -295,6 +299,20 @@ function () use ($rawMaterial): void { ); } + private function ensureNotUsedInActiveCutting(RawMaterial $rawMaterial): void + { + $usedInActiveCutting = CuttingMaterial::query() + ->whereHas('rawMaterialPrice', fn ($q) => $q->where('raw_material_id', $rawMaterial->id)) + ->whereHas('cutting', fn ($q) => $q->where('status', '!=', CuttingStatus::COMPLETED)) + ->exists(); + + if ($usedInActiveCutting) { + throw ValidationException::withMessages([ + 'raw_material' => 'Bahan baku tidak dapat dihapus karena masih digunakan dalam proses cutting yang belum selesai.', + ]); + } + } + public function applyToggleStatus(OwnerVerificationRequest $verificationRequest): void { $rawMaterial = $verificationRequest->subject; diff --git a/resources/js/pages/admin/master/raw-materials/table/RawMaterialGroupedTable.vue b/resources/js/pages/admin/master/raw-materials/table/RawMaterialGroupedTable.vue index c3d7a2b..fabdf30 100644 --- a/resources/js/pages/admin/master/raw-materials/table/RawMaterialGroupedTable.vue +++ b/resources/js/pages/admin/master/raw-materials/table/RawMaterialGroupedTable.vue @@ -5,7 +5,6 @@ import { DataTableEmpty } from '@/components/data-table'; import DataTableToolbar from '@/components/data-table/DataTableToolbar.vue'; import GroupedTableFooter from '@/components/data-table/GroupedTableFooter.vue'; import MediaThumbnailCell from '@/components/media/MediaThumbnailCell.vue'; -import VerificationDetailModal from '@/components/owner-verification/VerificationDetailModal.vue'; import { Badge } from '@/components/ui/badge'; import { Table, @@ -67,16 +66,6 @@ function allVariantImageStocks(material: RawMaterialListItem): string[] { ); } -const verificationModalOpen = ref(false); -const selectedRequestId = ref(null); - -function openVerificationDetail(requestId: number | undefined) { - if (requestId) { - selectedRequestId.value = requestId; - verificationModalOpen.value = true; - } -} - // ─── Modal Edit Varian State ───────────────────────────────────────────────── const isEditing = ref(false); const editingMaterial = ref(null); @@ -118,12 +107,6 @@ function openEditModal(price: RawMaterialPrice, material: RawMaterialListItem) { {{ material.unit_label }} -

- {{ material.pending_request_submitted_by_name }} mengajukan {{ - material.pending_request_action_label?.toLowerCase() }} bahan baku ini — - -

Total stok @@ -183,7 +166,7 @@ function openEditModal(price: RawMaterialPrice, material: RawMaterialListItem) { {{ price.price_formatted }} - @@ -197,8 +180,6 @@ function openEditModal(price: RawMaterialPrice, material: RawMaterialListItem) { - - import { RowDeleteAction, RowEditAction } from '@/components/button'; -import OwnerVerificationRowActions from '@/components/owner-verification/OwnerVerificationRowActions.vue'; import { useCan } from '@/composables/useCan'; import { edit, destroy } from '@/routes/admin/master/raw_materials'; import type { RawMaterialListItem } from '@/types/raw-material'; @@ -10,30 +9,30 @@ defineProps<{ }>(); const { can } = useCan(); + +function onDeleteError(errors: Record): string | void { + const firstError = Object.values(errors)[0]; + + if (firstError) { + return firstError; + } +} diff --git a/resources/js/pages/admin/master/raw-materials/table/raw-material-status-toggle.vue b/resources/js/pages/admin/master/raw-materials/table/raw-material-status-toggle.vue index fdf4b56..d6d053a 100644 --- a/resources/js/pages/admin/master/raw-materials/table/raw-material-status-toggle.vue +++ b/resources/js/pages/admin/master/raw-materials/table/raw-material-status-toggle.vue @@ -30,7 +30,7 @@ watch( ); function toggleStatus(checked: boolean) { - if (!can('raw_materials.toggle_status') || props.material.has_pending_request) { + if (!can('raw_materials.toggle_status')) { return; } @@ -66,7 +66,7 @@ function toggleStatus(checked: boolean) {
diff --git a/resources/js/types/raw-material.ts b/resources/js/types/raw-material.ts index 2c0b773..035fd19 100644 --- a/resources/js/types/raw-material.ts +++ b/resources/js/types/raw-material.ts @@ -29,11 +29,6 @@ export type RawMaterialListItem = { total_inventory_value_formatted?: string; is_active: boolean; prices: RawMaterialPrice[]; - has_pending_request?: boolean; - pending_request_id?: number; - pending_request_action?: string; - pending_request_action_label?: string; - pending_request_submitted_by_name?: string; display_is_active?: boolean; }; diff --git a/tests/Feature/Admin/Master/RawMaterialTest.php b/tests/Feature/Admin/Master/RawMaterialTest.php index 3cb26db..7c7a8f2 100644 --- a/tests/Feature/Admin/Master/RawMaterialTest.php +++ b/tests/Feature/Admin/Master/RawMaterialTest.php @@ -1,9 +1,12 @@ actingAs($user) - ->delete(route('admin.master.raw_materials.destroy', $rawMaterial)) - ->assertRedirect(route('admin.master.raw_materials.index')); - - $this->assertDatabaseHas('owner_verification_requests', [ - 'subject_id' => $rawMaterial->id, - 'subject_type' => RawMaterial::class, - 'action' => OwnerVerificationAction::DELETE->value, - 'status' => OwnerVerificationStatus::PENDING->value, - ]); - - $this->assertNotSoftDeleted('raw_materials', ['id' => $rawMaterial->id]); - }); - - test('approving delete request soft deletes raw material and prices', function () { - $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_DELETE); - $verifier = createRawMaterialVerifierUser(); - $rawMaterial = createRawMaterialWithPrices(); $priceIds = $rawMaterial->prices->pluck('id')->toArray(); $this->actingAs($user) - ->delete(route('admin.master.raw_materials.destroy', $rawMaterial)); - - approveLatestOwnerVerificationRequest($verifier); + ->delete(route('admin.master.raw_materials.destroy', $rawMaterial)) + ->assertRedirect(route('admin.master.raw_materials.index')); $this->assertSoftDeleted('raw_materials', ['id' => $rawMaterial->id]); @@ -733,20 +715,44 @@ function createRawMaterialVerifierUser(): User $this->assertNotSoftDeleted('raw_materials', ['id' => $rawMaterial->id]); }); - test('deleting raw material is not applied until owner approves', function () { + test('cannot delete raw material that is used in an in-progress cutting', function () { $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_DELETE); $rawMaterial = createRawMaterialWithPrices(); - $priceIds = $rawMaterial->prices->pluck('id')->toArray(); + $price = $rawMaterial->prices->first(); + + $cutting = Cutting::factory()->create(['status' => CuttingStatus::IN_PROGRESS]); + CuttingMaterial::factory()->create([ + 'cutting_id' => $cutting->id, + 'raw_material_price_id' => $price->id, + 'user_id' => null, + ]); $this->actingAs($user) - ->delete(route('admin.master.raw_materials.destroy', $rawMaterial)); + ->delete(route('admin.master.raw_materials.destroy', $rawMaterial)) + ->assertRedirect(); $this->assertNotSoftDeleted('raw_materials', ['id' => $rawMaterial->id]); + }); - foreach ($priceIds as $priceId) { - $this->assertNotSoftDeleted('raw_material_prices', ['id' => $priceId]); - } + test('can delete raw material that is used in a completed cutting', function () { + $user = createRawMaterialUserWithPermission(PermissionEnum::RAW_MATERIALS_VIEW, PermissionEnum::RAW_MATERIALS_DELETE); + + $rawMaterial = createRawMaterialWithPrices(); + $price = $rawMaterial->prices->first(); + + $cutting = Cutting::factory()->create(['status' => CuttingStatus::COMPLETED]); + CuttingMaterial::factory()->create([ + 'cutting_id' => $cutting->id, + 'raw_material_price_id' => $price->id, + 'user_id' => null, + ]); + + $this->actingAs($user) + ->delete(route('admin.master.raw_materials.destroy', $rawMaterial)) + ->assertRedirect(route('admin.master.raw_materials.index')); + + $this->assertSoftDeleted('raw_materials', ['id' => $rawMaterial->id]); }); });