'good', 'reject_stock' => 'reject', 'retail_stock' => 'retail', ]; public function paginated(Model $model, int $perPage = 20): LengthAwarePaginator { return StockMutation::query() ->where('stockable_type', get_class($model)) ->where('stockable_id', $model->id) ->with('user:id,username,email') ->latest() ->paginate($perPage); } public function paginatedAll(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator { return StockMutation::query() ->where('stockable_type', ProductVariant::class) ->with([ 'user:id,username,email', 'user.userProfile:id,user_id,full_name', 'stockable' => fn ($q) => $q->withTrashed()->select('id', 'product_id', 'name'), 'stockable.product' => fn ($q) => $q->withTrashed()->select('id', 'name'), ]) ->when($search, function ($q) use ($search) { $q->where(function ($sq) use ($search) { $sq->whereHasMorph('stockable', [ProductVariant::class], function ($vq) use ($search) { $vq->withTrashed() ->where('name', 'like', "%{$search}%") ->orWhereHas('product', fn ($pq) => $pq->withTrashed()->where('name', 'like', "%{$search}%")); })->orWhere('description', 'like', "%{$search}%"); }); }) ->when($filters['product_id'] ?? null, function ($q, $productId) { $q->whereHasMorph('stockable', [ProductVariant::class], fn ($vq) => $vq->withTrashed()->where('product_id', $productId)); }) ->when($filters['type'] ?? null, fn ($q, $type) => $q->where('type', $type)) ->when($filters['stock_quality'] ?? null, fn ($q, $quality) => $q->where('stock_quality', $quality)) ->when($filters['date_from'] ?? null, fn ($q, $dateFrom) => $q->whereDate('created_at', '>=', $dateFrom)) ->when($filters['date_to'] ?? null, fn ($q, $dateTo) => $q->whereDate('created_at', '<=', $dateTo)) ->orderBy($sort, $direction) ->paginate($perPage); } public function getFilterOptions(): array { return [ 'products' => Product::query() ->select(['id', 'name']) ->whereHas('productVariants.stockMutations') ->orderBy('name') ->get(), 'typeOptions' => [ ['value' => 'in', 'label' => 'Stok Masuk'], ['value' => 'out', 'label' => 'Stok Keluar'], ], 'stockQualityOptions' => [ ['value' => 'good', 'label' => 'Bagus'], ['value' => 'reject', 'label' => 'Reject'], ['value' => 'retail', 'label' => 'Ecer'], ], ]; } public static function qualityFromField(string $field): string { return self::QUALITY_MAP[$field] ?? 'good'; } public function record( Model $model, string $type, int $quantity, int $stockBefore, int $stockAfter, string $stockQuality, string $description, ?Model $source = null, ): StockMutation { return StockMutation::create([ 'user_id' => auth()->id(), 'stockable_type' => get_class($model), 'stockable_id' => $model->id, 'type' => $type, 'quantity' => $quantity, 'stock_before' => $stockBefore, 'stock_after' => $stockAfter, 'stock_quality' => $stockQuality, 'description' => $description, 'source_type' => $source ? get_class($source) : null, 'source_id' => $source?->id, ]); } public function recordInitial(Model $model, array $stockData, string $description = 'Stok awal'): void { $userId = auth()->id(); foreach (self::QUALITY_MAP as $field => $quality) { $quantity = (int) ($stockData[$field] ?? 0); if ($quantity > 0) { StockMutation::create([ 'user_id' => $userId, 'stockable_type' => get_class($model), 'stockable_id' => $model->id, 'type' => 'in', 'quantity' => $quantity, 'stock_before' => 0, 'stock_after' => $quantity, 'stock_quality' => $quality, 'description' => $description, ]); } } } public function recordAdjustment(Model $model, array $oldData, array $newData, string $description = 'Penyesuaian stok'): void { $userId = auth()->id(); foreach (self::QUALITY_MAP as $field => $quality) { $old = (int) ($oldData[$field] ?? 0); $new = (int) ($newData[$field] ?? 0); $diff = $new - $old; if ($diff !== 0) { StockMutation::create([ 'user_id' => $userId, 'stockable_type' => get_class($model), 'stockable_id' => $model->id, 'type' => $diff > 0 ? 'in' : 'out', 'quantity' => $diff, 'stock_before' => $old, 'stock_after' => $new, 'stock_quality' => $quality, 'description' => $description, ]); } } } 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, string $fromQuality, string $toQuality, int $fromBefore, int $toBefore, string $description = 'Transfer stok', ): void { DB::transaction(function () use ($model, $quantity, $fromQuality, $toQuality, $fromBefore, $toBefore, $description) { $userId = auth()->id(); StockMutation::create([ 'user_id' => $userId, 'stockable_type' => get_class($model), 'stockable_id' => $model->id, 'type' => 'out', 'quantity' => -$quantity, 'stock_before' => $fromBefore, 'stock_after' => $fromBefore - $quantity, 'stock_quality' => $fromQuality, 'description' => $description, ]); StockMutation::create([ 'user_id' => $userId, 'stockable_type' => get_class($model), 'stockable_id' => $model->id, 'type' => 'in', 'quantity' => $quantity, 'stock_before' => $toBefore, 'stock_after' => $toBefore + $quantity, 'stock_quality' => $toQuality, 'description' => $description, ]); }); } }