From d2ddc5e6b79aba56a31cd0668f476bb0a4c6e74b Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Mon, 6 Jul 2026 21:24:26 +0700 Subject: [PATCH] refactor: enhance stock filtering in CuttingService and OrderService, update UI components for better display of stock information --- app/Services/Manage/CuttingService.php | 3 +- app/Services/Manage/OrderService.php | 22 ++-- .../form/CuttingPosMaterialCatalogPanel.vue | 8 +- .../orders/form/OrderPosCatalogPanel.vue | 110 +++++++++--------- .../products/table/ProductGroupedTable.vue | 6 +- .../table/RawMaterialGroupedTable.vue | 8 +- tests/Feature/Admin/Manage/CuttingTest.php | 46 ++++++++ tests/Feature/Admin/Manage/OrderTest.php | 52 +++++++++ 8 files changed, 184 insertions(+), 71 deletions(-) diff --git a/app/Services/Manage/CuttingService.php b/app/Services/Manage/CuttingService.php index ce759e3..b13418a 100644 --- a/app/Services/Manage/CuttingService.php +++ b/app/Services/Manage/CuttingService.php @@ -138,7 +138,8 @@ public function rawMaterialCatalog(?Cutting $cutting = null, ?User $user = null) ->with([ 'prices' => fn ($query) => $query ->orderBy('created_at') - ->with('media'), + ->with('media') + ->where(fn ($q) => $q->where('stock', '>', 0)->orWhereIn('id', $selectedPriceIds)), ]) ->where(function (Builder $query) use ($selectedPriceIds): void { $query->active(); diff --git a/app/Services/Manage/OrderService.php b/app/Services/Manage/OrderService.php index 601280e..60aa552 100644 --- a/app/Services/Manage/OrderService.php +++ b/app/Services/Manage/OrderService.php @@ -158,7 +158,13 @@ public function catalogItems(?Order $order = null, ?User $user = null): Collecti ->with([ 'variants' => fn ($query) => $query ->with('media') - ->orderBy('created_at'), + ->orderBy('created_at') + ->where(fn ($q) => $q + ->where('stock', '>', 0) + ->orWhere('reject_stock', '>', 0) + ->orWhere('retail_stock', '>', 0) + ->orWhereIn('id', $orderVariantIds) + ), ]) ->where(function (Builder $query) use ($orderVariantIds): void { $query->active(); @@ -857,13 +863,13 @@ private function presentVariantPricesFromCollection(int $variantId, Collection $ return $allPricesByVariant ->get($variantId, collect()) ->map(fn ($price) => [ - 'type' => $price->price_type->value, - 'type_label' => $price->price_type->label(), - 'price' => (int) $price->price, - 'price_formatted' => $price->price_formatted, - 'price_input' => (string) $price->price, - 'cost_per_unit' => (int) $price->cost_per_unit, - 'cost_per_unit_formatted' => $price->cost_per_unit_formatted, + 'type' => is_array($price) ? $price['price_type'] : $price->price_type->value, + 'type_label' => PriceType::from(is_array($price) ? $price['price_type'] : $price->price_type->value)->label(), + 'price' => (int) (is_array($price) ? $price['price'] : $price->price), + 'price_formatted' => is_array($price) ? $price['price_formatted'] : $price->price_formatted, + 'price_input' => (string) (is_array($price) ? $price['price'] : $price->price), + 'cost_per_unit' => (int) (is_array($price) ? ($price['cost_per_unit'] ?? $price['price']) : $price->cost_per_unit), + 'cost_per_unit_formatted' => is_array($price) ? ($price['cost_per_unit_formatted'] ?? $price['price_formatted']) : $price->cost_per_unit_formatted, ]) ->values() ->all(); diff --git a/resources/js/pages/admin/manage/cuttings/form/CuttingPosMaterialCatalogPanel.vue b/resources/js/pages/admin/manage/cuttings/form/CuttingPosMaterialCatalogPanel.vue index 19b98ad..b5098fc 100644 --- a/resources/js/pages/admin/manage/cuttings/form/CuttingPosMaterialCatalogPanel.vue +++ b/resources/js/pages/admin/manage/cuttings/form/CuttingPosMaterialCatalogPanel.vue @@ -77,9 +77,10 @@ function openCombination(rawMaterial: CuttingRawMaterialCatalogItem, price: Cutt -
- +
+
diff --git a/resources/js/pages/admin/master/products/table/ProductGroupedTable.vue b/resources/js/pages/admin/master/products/table/ProductGroupedTable.vue index 168ad59..8de6eb6 100644 --- a/resources/js/pages/admin/master/products/table/ProductGroupedTable.vue +++ b/resources/js/pages/admin/master/products/table/ProductGroupedTable.vue @@ -125,6 +125,7 @@ function openVerificationDetail(requestId: number | undefined) { + # Varian Foto Stok Bagus @@ -139,7 +140,10 @@ function openVerificationDetail(requestId: number | undefined) { Belum ada varian - + + + {{ variantIndex + 1 }} + {{ variant.name }} 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 1b812fd..f731a33 100644 --- a/resources/js/pages/admin/master/raw-materials/table/RawMaterialGroupedTable.vue +++ b/resources/js/pages/admin/master/raw-materials/table/RawMaterialGroupedTable.vue @@ -109,6 +109,7 @@ function openVerificationDetail(requestId: number | undefined) {
+ # Varian Foto Stok @@ -117,11 +118,14 @@ function openVerificationDetail(requestId: number | undefined) { - + Belum ada varian - + + + {{ priceIndex + 1 }} + {{ price.variant }} diff --git a/tests/Feature/Admin/Manage/CuttingTest.php b/tests/Feature/Admin/Manage/CuttingTest.php index 382ee21..24150df 100644 --- a/tests/Feature/Admin/Manage/CuttingTest.php +++ b/tests/Feature/Admin/Manage/CuttingTest.php @@ -1122,3 +1122,49 @@ function setupDraftItems(User $user): array expect($combination->material_result)->toBe(15); }); }); + +// ─── Raw Material Catalog Stock Filter ─────────────────────── + +describe('Raw Material Catalog Stock Filter', function () { + test('rawMaterialCatalog excludes variants with zero stock', function () { + $user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_CREATE); + + $rawMaterial = RawMaterial::factory()->create(['is_active' => true]); + $priceWithStock = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id, 'stock' => 10]); + $priceWithoutStock = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id, 'stock' => 0]); + + $this->actingAs($user) + ->get(route('admin.manage.cuttings.create')) + ->assertOk(); + + $service = app(\App\Services\Manage\CuttingService::class); + $catalog = $service->rawMaterialCatalog(user: $user); + $material = $catalog->firstWhere('id', $rawMaterial->id); + + $priceIds = $material->prices->pluck('id')->all(); + + expect($priceIds)->toContain($priceWithStock->id); + expect($priceIds)->not->toContain($priceWithoutStock->id); + }); + + test('rawMaterialCatalog includes zero-stock variants already used in cutting', function () { + $user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_UPDATE); + + $rawMaterial = RawMaterial::factory()->create(['is_active' => true]); + $priceZeroStock = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id, 'stock' => 0]); + + $cutting = Cutting::factory()->create(['created_by_id' => $user->id]); + CuttingMaterial::factory()->create([ + 'cutting_id' => $cutting->id, + 'raw_material_price_id' => $priceZeroStock->id, + ]); + + $service = app(\App\Services\Manage\CuttingService::class); + $catalog = $service->rawMaterialCatalog($cutting); + $material = $catalog->firstWhere('id', $rawMaterial->id); + + $priceIds = $material?->prices->pluck('id')->all() ?? []; + + expect($priceIds)->toContain($priceZeroStock->id); + }); +}); diff --git a/tests/Feature/Admin/Manage/OrderTest.php b/tests/Feature/Admin/Manage/OrderTest.php index 0f3ceea..9dbd684 100644 --- a/tests/Feature/Admin/Manage/OrderTest.php +++ b/tests/Feature/Admin/Manage/OrderTest.php @@ -613,3 +613,55 @@ function setupOrderDraftItems(User $user): ProductVariant expect($item->trashed())->toBeTrue(); }); }); + +// ─── Order Catalog Stock Filter ───────────────────────────── + +describe('Order Catalog Stock Filter', function () { + test('catalogItems excludes variants where all stock types are zero', function () { + $user = createOrderUserWithPermission(PermissionEnum::ORDERS_VIEW, PermissionEnum::ORDERS_CREATE); + + $product = Product::factory()->create(['is_active' => true]); + + $variantWithStock = ProductVariant::factory()->create(['product_id' => $product->id, 'stock' => 10, 'reject_stock' => 0, 'retail_stock' => 0]); + $variantAllZero = ProductVariant::factory()->create(['product_id' => $product->id, 'stock' => 0, 'reject_stock' => 0, 'retail_stock' => 0]); + $variantRejectOnly = ProductVariant::factory()->create(['product_id' => $product->id, 'stock' => 0, 'reject_stock' => 5, 'retail_stock' => 0]); + $variantRetailOnly = ProductVariant::factory()->create(['product_id' => $product->id, 'stock' => 0, 'reject_stock' => 0, 'retail_stock' => 3]); + + $service = app(\App\Services\Manage\OrderService::class); + $catalog = $service->catalogItems(user: $user); + $found = $catalog->firstWhere('id', $product->id); + + $variantIds = $found?->variants->pluck('id')->all() ?? []; + + expect($variantIds)->toContain($variantWithStock->id); + expect($variantIds)->toContain($variantRejectOnly->id); + expect($variantIds)->toContain($variantRetailOnly->id); + expect($variantIds)->not->toContain($variantAllZero->id); + }); + + test('catalogItems includes all-zero-stock variants already in order', function () { + $user = createOrderUserWithPermission(PermissionEnum::ORDERS_VIEW, PermissionEnum::ORDERS_UPDATE); + + $product = Product::factory()->create(['is_active' => true]); + $variantAllZero = ProductVariant::factory()->create([ + 'product_id' => $product->id, + 'stock' => 0, + 'reject_stock' => 0, + 'retail_stock' => 0, + ]); + + $order = Order::factory()->create(['created_by_id' => $user->id]); + OrderItem::factory()->create([ + 'order_id' => $order->id, + 'product_variant_id' => $variantAllZero->id, + ]); + + $service = app(\App\Services\Manage\OrderService::class); + $catalog = $service->catalogItems($order); + $found = $catalog->firstWhere('id', $product->id); + + $variantIds = $found?->variants->pluck('id')->all() ?? []; + + expect($variantIds)->toContain($variantAllZero->id); + }); +});