diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index 1100f18..978127f 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -15,7 +15,7 @@ protected function handleAction(callable $action, string $successMessage, string $action(); Inertia::flash('toast', ['type' => 'success', 'message' => $successMessage]); - return to_route($redirectRoute, $parameters); + return to_route($redirectRoute); } catch (ValidationException $e) { $firstError = collect($e->errors())->flatten()->first(); Inertia::flash('toast', ['type' => 'error', 'message' => $firstError ?? 'Terjadi kesalahan.']); diff --git a/app/Services/Admin/Master/Product/ProductService.php b/app/Services/Admin/Master/Product/ProductService.php index 1016323..f6686f9 100644 --- a/app/Services/Admin/Master/Product/ProductService.php +++ b/app/Services/Admin/Master/Product/ProductService.php @@ -3,7 +3,6 @@ namespace App\Services\Admin\Master\Product; use App\Models\Product; -use App\Models\ProductPrice; use App\Models\ProductVariant; use App\Services\NotificationService; use App\Services\S3PresignedService; @@ -90,31 +89,60 @@ public function create(array $data): Product $useSamePrice = $data['use_same_price'] ?? false; - foreach ($data['variants'] as $index => $variantData) { - $variant = $product->productVariants()->create([ - 'name' => $variantData['name'], - 'stock' => $variantData['stock'], - 'reject_stock' => $variantData['reject_stock'], - 'retail_stock' => $variantData['retail_stock'], - ]); + // Bulk insert variants + $now = now(); + $variantRows = collect($data['variants'])->map(fn ($v) => [ + 'product_id' => $product->id, + 'name' => $v['name'], + 'stock' => $v['stock'], + 'reject_stock' => $v['reject_stock'], + 'retail_stock' => $v['retail_stock'], + 'created_at' => $now, + 'updated_at' => $now, + ])->toArray(); - $prices = $useSamePrice - ? $data['shared_prices'] - : $variantData['prices']; + DB::table('product_variants')->insert($variantRows); + + // Map variant name -> variant ID + $insertedVariants = ProductVariant::where('product_id', $product->id)->get(); + $variantMap = $insertedVariants->mapWithKeys(fn ($v) => [$v->name => $v->id]); + + // Bulk insert prices + $priceRows = []; + foreach ($data['variants'] as $variantData) { + $variantId = $variantMap[$variantData['name']]; + $prices = $useSamePrice ? $data['shared_prices'] : $variantData['prices']; foreach ($prices as $priceData) { - ProductPrice::create([ - 'variant_id' => $variant->id, + $priceRows[] = [ + 'variant_id' => $variantId, 'type' => $priceData['type'], 'price' => $priceData['price'], - ]); + 'created_at' => $now, + 'updated_at' => $now, + ]; } + } + if ($priceRows !== []) { + DB::table('product_prices')->insert($priceRows); + } + + // Bulk record stock mutations + $stockDataMap = []; + foreach ($data['variants'] as $variantData) { + $variantId = $variantMap[$variantData['name']]; + $stockDataMap[$variantId] = $variantData; + } + + $this->stockMutationService->recordBulkInitial($insertedVariants, $stockDataMap, 'Stok awal saat pembuatan varian'); + + // Register photos (per-variant, S3 operation) + foreach ($data['variants'] as $variantData) { if (! empty($variantData['photo_keys']) && is_array($variantData['photo_keys'])) { + $variant = $insertedVariants->firstWhere('name', $variantData['name']); $this->variantService->registerPhotos($variant, $variantData['photo_keys']); } - - $this->stockMutationService->recordInitial($variant, $variantData, 'Stok awal saat pembuatan varian'); } return $product; @@ -180,12 +208,15 @@ public function update(Product $product, array $data): Product $product->categories()->sync($data['category_ids']); $useSamePrice = $data['use_same_price'] ?? false; + $now = now(); + // Identify existing vs new variants $existingVariantIds = collect($data['variants']) ->pluck('id') ->filter() ->toArray(); + // Delete removed variants (cascade prices + media) $product->productVariants() ->whereNotIn('id', $existingVariantIds) ->each(function (ProductVariant $variant) { @@ -194,44 +225,162 @@ public function update(Product $product, array $data): Product $variant->delete(); }); + // Bulk load all existing variants with media (1 query instead of N) + $existingVariantsMap = ProductVariant::whereIn('id', $existingVariantIds) + ->with('media') + ->get() + ->mapWithKeys(fn ($v) => [$v->id => $v]); + + // Collect old stock data + identify changed variants + $oldStockDataMap = []; + $changedIds = []; foreach ($data['variants'] as $variantData) { $variantId = $variantData['id'] ?? null; + if (! $variantId || ! isset($existingVariantsMap[$variantId])) { + continue; + } + $variant = $existingVariantsMap[$variantId]; + $oldStockDataMap[$variantId] = $variant->only(['stock', 'reject_stock', 'retail_stock']); + if ($variant->name !== $variantData['name'] + || $variant->stock != $variantData['stock'] + || $variant->reject_stock != $variantData['reject_stock'] + || $variant->retail_stock != $variantData['retail_stock']) { + $changedIds[] = $variantId; + } + } + + // Bulk update changed variants (only changed ones, not all) + if ($changedIds !== []) { + $changedUpdates = collect($data['variants']) + ->filter(fn ($v) => isset($v['id']) && in_array($v['id'], $changedIds)); + + foreach ($changedUpdates as $variantData) { + $existingVariantsMap[$variantData['id']]->update([ + 'name' => $variantData['name'], + 'stock' => $variantData['stock'], + 'reject_stock' => $variantData['reject_stock'], + 'retail_stock' => $variantData['retail_stock'], + ]); + } + } + + // Bulk create new variants + $newVariantsData = collect($data['variants'])->filter(fn ($v) => ! isset($v['id'])); + $newVariantIdMap = []; + + if ($newVariantsData->isNotEmpty()) { + $newVariantRows = $newVariantsData->map(fn ($v) => [ + 'product_id' => $product->id, + 'name' => $v['name'], + 'stock' => $v['stock'], + 'reject_stock' => $v['reject_stock'], + 'retail_stock' => $v['retail_stock'], + 'created_at' => $now, + 'updated_at' => $now, + ])->toArray(); + + DB::table('product_variants')->insert($newVariantRows); + + // Map name -> id for new variants + $newlyCreated = ProductVariant::where('product_id', $product->id) + ->whereIn('name', $newVariantsData->pluck('name')->toArray()) + ->get(); + + $newVariantIdMap = $newlyCreated->mapWithKeys(fn ($v) => [$v->name => $v->id])->toArray(); + } + + // Build variant_id lookup: existing by id, new by name + $variantIdLookup = []; + foreach ($data['variants'] as $variantData) { + $variantId = $variantData['id'] ?? $newVariantIdMap[$variantData['name']] ?? null; if ($variantId) { - $variant = $product->productVariants()->findOrFail($variantId); - $oldData = $variant->only(['stock', 'reject_stock', 'retail_stock']); - $variant->update([ - 'name' => $variantData['name'], - 'stock' => $variantData['stock'], - 'reject_stock' => $variantData['reject_stock'], - 'retail_stock' => $variantData['retail_stock'], - ]); - $this->stockMutationService->recordAdjustment($variant, $oldData, $variantData, 'Penyesuaian stok saat edit varian'); - } else { - $variant = $product->productVariants()->create([ - 'name' => $variantData['name'], - 'stock' => $variantData['stock'], - 'reject_stock' => $variantData['reject_stock'], - 'retail_stock' => $variantData['retail_stock'], - ]); - $this->stockMutationService->recordInitial($variant, $variantData, 'Stok awal saat pembuatan varian'); + $variantIdLookup[$variantData['name']] = $variantId; + } + } + + // Upsert all prices (1 query) + $priceRows = []; + foreach ($data['variants'] as $variantData) { + $variantId = $variantData['id'] ?? $newVariantIdMap[$variantData['name']] ?? null; + if (! $variantId) { + continue; } - $variant->productPrices()->delete(); - - $prices = $useSamePrice - ? $data['shared_prices'] - : $variantData['prices']; - + $prices = $useSamePrice ? $data['shared_prices'] : $variantData['prices']; foreach ($prices as $priceData) { - ProductPrice::create([ - 'variant_id' => $variant->id, + $priceRows[] = [ + 'variant_id' => $variantId, 'type' => $priceData['type'], 'price' => $priceData['price'], - ]); + 'created_at' => $now, + 'updated_at' => $now, + ]; + } + } + + if ($priceRows !== []) { + DB::table('product_prices')->upsert( + $priceRows, + ['variant_id', 'type'], + ['price', 'updated_at'] + ); + } + + // Bulk record stock mutations for new variants + if ($newVariantIdMap !== []) { + $newStockDataMap = []; + foreach ($newVariantsData as $variantData) { + $variantId = $newVariantIdMap[$variantData['name']] ?? null; + if ($variantId) { + $newStockDataMap[$variantId] = $variantData; + } } - if (! empty($variantData['photo_keys']) && is_array($variantData['photo_keys'])) { + $newVariantModels = ProductVariant::whereIn('id', array_values($newVariantIdMap))->get(); + $this->stockMutationService->recordBulkInitial($newVariantModels, $newStockDataMap, 'Stok awal saat pembuatan varian'); + } + + // Bulk record stock mutations for changed existing variants + if ($changedIds !== []) { + $newDataForExisting = []; + foreach ($data['variants'] as $variantData) { + $variantId = $variantData['id'] ?? null; + if ($variantId && in_array($variantId, $changedIds)) { + $newDataForExisting[$variantId] = $variantData; + } + } + + $changedModels = collect($changedIds)->map(fn ($id) => $existingVariantsMap[$id])->filter(); + $this->stockMutationService->recordBulkAdjustment( + $changedModels, + $oldStockDataMap, + $newDataForExisting, + 'Penyesuaian stok saat edit varian' + ); + } + + // Register photos (per-variant, S3 operation) - only if changed + foreach ($data['variants'] as $variantData) { + if (empty($variantData['photo_keys']) || ! is_array($variantData['photo_keys'])) { + continue; + } + + $variantId = $variantData['id'] ?? $newVariantIdMap[$variantData['name']] ?? null; + if (! $variantId) { + continue; + } + + $variant = $existingVariantsMap[$variantId] ?? ProductVariant::find($variantId); + if (! $variant) { + continue; + } + + // Compare existing photo keys vs new ones + $existingKeys = $variant->getMedia('photos')->pluck('file_name')->sort()->values()->toArray(); + $newKeys = collect($variantData['photo_keys'])->sort()->values()->toArray(); + + if ($existingKeys !== $newKeys) { $variant->clearMediaCollection('photos'); $this->variantService->registerPhotos($variant, $variantData['photo_keys']); } diff --git a/app/Services/Admin/Master/Product/ProductVariantService.php b/app/Services/Admin/Master/Product/ProductVariantService.php index 761bd39..fd65edb 100644 --- a/app/Services/Admin/Master/Product/ProductVariantService.php +++ b/app/Services/Admin/Master/Product/ProductVariantService.php @@ -3,7 +3,6 @@ namespace App\Services\Admin\Master\Product; use App\Models\Product; -use App\Models\ProductPrice; use App\Models\ProductVariant; use App\Services\Concerns\RegistersMedia; use App\Services\NotificationService; @@ -23,7 +22,10 @@ public function __construct( public function getForEdit(ProductVariant $variant): array { - $variant->load('productPrices'); + $variant->load([ + 'productPrices', + 'media', + ]); $media = $variant->getMedia('photos'); $photoKeys = $media->pluck('file_name')->toArray(); @@ -59,15 +61,20 @@ public function update(ProductVariant $variant, array $data): ProductVariant $this->stockMutationService->recordAdjustment($variant, $oldData, $data, 'Penyesuaian stok saat edit varian'); - $variant->productPrices()->delete(); + // Upsert prices instead of delete+recreate + $priceRows = collect($data['prices'])->map(fn ($p) => [ + 'variant_id' => $variant->id, + 'type' => $p['type'], + 'price' => $p['price'], + 'created_at' => now(), + 'updated_at' => now(), + ])->toArray(); - foreach ($data['prices'] as $priceData) { - ProductPrice::create([ - 'variant_id' => $variant->id, - 'type' => $priceData['type'], - 'price' => $priceData['price'], - ]); - } + DB::table('product_prices')->upsert( + $priceRows, + ['variant_id', 'type'], + ['price', 'updated_at'] + ); if (! empty($data['photo_keys']) && is_array($data['photo_keys'])) { $variant->clearMediaCollection('photos'); diff --git a/app/Services/StockMutationService.php b/app/Services/StockMutationService.php index 5618e2e..fd5c030 100644 --- a/app/Services/StockMutationService.php +++ b/app/Services/StockMutationService.php @@ -2,9 +2,12 @@ namespace App\Services; +use App\Models\ProductVariant; use App\Models\StockMutation; use Illuminate\Contracts\Pagination\LengthAwarePaginator; use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Collection; +use Illuminate\Support\Facades\DB; class StockMutationService { @@ -61,6 +64,77 @@ public function recordAdjustment(Model $model, array $oldData, array $newData, s } } + public function recordBulkInitial(Collection $variants, array $stockDataMap, string $description = 'Stok awal'): void + { + $userId = auth()->id(); + $now = now(); + $mutations = []; + + foreach ($variants as $variant) { + $stockData = $stockDataMap[$variant->id] ?? []; + foreach (self::QUALITY_MAP as $field => $quality) { + $quantity = (int) ($stockData[$field] ?? 0); + if ($quantity > 0) { + $mutations[] = [ + 'user_id' => $userId, + 'stockable_type' => ProductVariant::class, + 'stockable_id' => $variant->id, + 'type' => 'in', + 'quantity' => $quantity, + 'stock_before' => 0, + 'stock_after' => $quantity, + 'stock_quality' => $quality, + 'description' => $description, + 'created_at' => $now, + 'updated_at' => $now, + ]; + } + } + } + + if ($mutations !== []) { + DB::table('stock_mutations')->insert($mutations); + } + } + + public function recordBulkAdjustment(Collection $variants, array $oldDataMap, array $newDataMap, string $description = 'Penyesuaian stok'): void + { + $userId = auth()->id(); + $now = now(); + $mutations = []; + + foreach ($variants as $variant) { + $oldData = $oldDataMap[$variant->id] ?? []; + $newData = $newDataMap[$variant->id] ?? []; + + foreach (self::QUALITY_MAP as $field => $quality) { + $old = (int) ($oldData[$field] ?? 0); + $new = (int) ($newData[$field] ?? 0); + $diff = $new - $old; + + if ($diff !== 0) { + $mutations[] = [ + 'user_id' => $userId, + 'stockable_type' => ProductVariant::class, + 'stockable_id' => $variant->id, + 'type' => $diff > 0 ? 'in' : 'out', + 'quantity' => $diff, + 'stock_before' => $old, + 'stock_after' => $new, + 'stock_quality' => $quality, + 'description' => $description, + 'created_at' => $now, + 'updated_at' => $now, + ]; + } + } + } + + if ($mutations !== []) { + DB::table('stock_mutations')->insert($mutations); + } + } + public function recordTransfer( Model $model, int $quantity,