with([ 'categories:id,name', 'productVariants:id,product_id,name,stock,reject_stock,retail_stock', 'productVariants.productPrices:id,variant_id,type,price', 'productVariants.media', ]) ->when($filters['status'] ?? null, fn ($q, $status) => $q->where('status', $status)) ->when($filters['name'] ?? null, fn ($q, $name) => $q->where('name', 'like', "%{$name}%")) ->latest() ->get(); $products->each(function ($product) { $product->productVariants->each(function ($variant) { $media = $variant->getMedia('photos')->first(); $variant->photo_url = $media ? $this->s3Service->getTemporaryUrl($media->file_name) : null; }); }); return $products; } public function paginated(int $perPage = 15, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator { $paginator = Product::query() ->select('id', 'name', 'slug', 'description', 'status') ->with([ 'categories:id,name', 'productVariants:id,product_id,name,stock,reject_stock,retail_stock', 'productVariants.productPrices:id,variant_id,type,price', ]) ->when($search, fn ($q) => $q->where('name', 'like', "%{$search}%")) ->when($filters['status'] ?? null, fn ($q, $status) => $q->where('status', $status)) ->orderBy($sort, $direction) ->paginate($perPage); $paginator->getCollection()->each(function ($product) { $product->productVariants->each(function ($variant) { $media = $variant->getMedia('photos')->first(); $variant->photo_url = $media ? $this->s3Service->getTemporaryUrl($media->file_name) : null; }); }); return $paginator; } public function create(array $data): Product { $product = DB::transaction(function () use ($data) { $product = Product::create([ 'name' => $data['name'], 'description' => $data['description'] ?? null, 'status' => $data['status'] ?? 'active', ]); $product->categories()->sync($data['category_ids']); $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'], ]); $prices = $useSamePrice ? $data['shared_prices'] : $variantData['prices']; foreach ($prices as $priceData) { ProductPrice::create([ 'variant_id' => $variant->id, 'type' => $priceData['type'], 'price' => $priceData['price'], ]); } if (! empty($variantData['photo_key'])) { $this->registerPhotos($variant, [$variantData['photo_key']]); } } return $product; }); NotificationService::notify( roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'], title: 'Produk Baru', body: "Produk \"{$product->name}\" berhasil ditambahkan".' oleh '.auth()->user()->full_name.'.', url: route('admin.master.products.index'), ); return $product; } public function getForEdit(Product $product): array { $product->load([ 'categories:id,name', 'productVariants.productPrices', 'productVariants.media', ]); $variants = $product->productVariants->map(function (ProductVariant $variant) { $media = $variant->getMedia('photos')->first(); return [ 'id' => $variant->id, 'name' => $variant->name, 'stock' => $variant->stock, 'reject_stock' => $variant->reject_stock, 'retail_stock' => $variant->retail_stock, 'photo_key' => $media?->file_name, 'photo_url' => $media ? $this->s3Service->getTemporaryUrl($media->file_name) : null, 'prices' => $variant->productPrices->map(fn ($p) => [ 'type' => $p->type->value, 'price' => $p->price, ]), ]; }); return [ 'id' => $product->id, 'name' => $product->name, 'description' => $product->description, 'status' => $product->status->value, 'category_ids' => $product->categories->pluck('id'), 'product_variants' => $variants, ]; } public function update(Product $product, array $data): Product { $product = DB::transaction(function () use ($product, $data) { $product->update([ 'name' => $data['name'], 'description' => $data['description'] ?? null, 'status' => $data['status'] ?? $product->status, ]); $product->categories()->sync($data['category_ids']); $useSamePrice = $data['use_same_price'] ?? false; $existingVariantIds = collect($data['variants']) ->pluck('id') ->filter() ->toArray(); $product->productVariants() ->whereNotIn('id', $existingVariantIds) ->each(function (ProductVariant $variant) { $variant->productPrices()->delete(); $variant->clearMediaCollection('photos'); $variant->delete(); }); foreach ($data['variants'] as $variantData) { $variantId = $variantData['id'] ?? null; $variant = $variantId ? $product->productVariants()->findOrFail($variantId) : $product->productVariants()->create([]); $variant->update([ 'name' => $variantData['name'], 'stock' => $variantData['stock'], 'reject_stock' => $variantData['reject_stock'], 'retail_stock' => $variantData['retail_stock'], ]); $variant->productPrices()->delete(); $prices = $useSamePrice ? $data['shared_prices'] : $variantData['prices']; foreach ($prices as $priceData) { ProductPrice::create([ 'variant_id' => $variant->id, 'type' => $priceData['type'], 'price' => $priceData['price'], ]); } if (! empty($variantData['photo_key'])) { $variant->clearMediaCollection('photos'); $this->registerPhotos($variant, [$variantData['photo_key']]); } } return $product; }); NotificationService::notify( roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'], title: 'Produk Diperbarui', body: "Produk \"{$product->name}\" berhasil diperbarui".' oleh '.auth()->user()->full_name.'.', url: route('admin.master.products.index'), ); return $product; } public function delete(Product $product): bool { return DB::transaction(function () use ($product) { $product->productVariants->each(function (ProductVariant $variant) { $variant->productPrices()->delete(); $variant->clearMediaCollection('photos'); $variant->delete(); }); $product->categories()->detach(); return $product->delete(); }); } public function toggleStatus(Product $product): void { $product->update([ 'status' => $product->status->value === 'active' ? 'inactive' : 'active', ]); } private function registerPhotos(ProductVariant $variant, array $photoKeys): void { foreach ($photoKeys as $order => $s3Key) { $fileName = pathinfo($s3Key, PATHINFO_BASENAME); $name = pathinfo($s3Key, PATHINFO_FILENAME); Media::create([ 'model_type' => ProductVariant::class, 'model_id' => $variant->id, 'uuid' => Str::uuid(), 'collection_name' => 'photos', 'name' => $name, 'file_name' => $s3Key, 'mime_type' => 'image/jpeg', 'disk' => 's3', 'conversions_disk' => 's3', 'size' => 0, 'manipulations' => [], 'custom_properties' => [], 'generated_conversions' => [], 'responsive_images' => [], 'order_column' => $order + 1, ]); } } }