From cb8510cf56c9b584c709c989370a95f579d0118f Mon Sep 17 00:00:00 2001 From: pbao9 Date: Fri, 6 Mar 2026 11:15:26 +0700 Subject: [PATCH] feat: Implement full CRUD API for product categories and products, including recursive product counts for categories. --- .../Product/ProductCategoryController.php | 156 ++++++++++++++++++ .../Api/Admin/Product/ProductController.php | 83 ++++++++++ .../Api/Admin/ProductCategoryResource.php | 30 ++++ app/Models/ProductCategories.php | 21 ++- routes/api.php | 13 ++ 5 files changed, 302 insertions(+), 1 deletion(-) create mode 100644 app/Http/Controllers/Api/Admin/Product/ProductCategoryController.php create mode 100644 app/Http/Resources/Api/Admin/ProductCategoryResource.php diff --git a/app/Http/Controllers/Api/Admin/Product/ProductCategoryController.php b/app/Http/Controllers/Api/Admin/Product/ProductCategoryController.php new file mode 100644 index 0000000..5682a22 --- /dev/null +++ b/app/Http/Controllers/Api/Admin/Product/ProductCategoryController.php @@ -0,0 +1,156 @@ +get('per_page', 15), 100); + + $query = ProductCategories::withCount('products') + ->when($request->get('filter'), fn ($q, $f) => + $q->where('name', 'like', "%{$f}%") + ) + ->when( + $request->filled('parent_id') && $request->get('parent_id') !== 'null', + fn ($q) => $q->where('parent_id', $request->get('parent_id')), + fn ($q) => $q->whereNull('parent_id') + ) + ->with(['children']) + ->orderBy('sort_order') + ->orderBy('name'); + + if ($request->boolean('all')) { + $items = $query->whereNull('parent_id')->get(); + return response()->json([ + 'error' => false, + 'data' => ProductCategoryResource::collection($items), + ]); + } + + $paginator = $query->paginate($perPage); + + return response()->json([ + 'error' => false, + 'data' => [ + 'items' => ProductCategoryResource::collection($paginator->items()), + 'total' => $paginator->total(), + 'per_page' => $paginator->perPage(), + 'current_page' => $paginator->currentPage(), + 'last_page' => $paginator->lastPage(), + ], + ]); + } + + public function show(int $id): JsonResponse + { + $category = ProductCategories::with(['children', 'parent']) + ->withCount('products') + ->find($id); + + if (!$category) { + return response()->json(['error' => true, 'message' => 'Không tìm thấy nhóm sản phẩm.'], 404); + } + + return response()->json([ + 'error' => false, + 'data' => new ProductCategoryResource($category), + ]); + } + + public function store(Request $request): JsonResponse + { + $validated = $request->validate([ + 'name' => 'required|string|max:255', + 'parent_id' => 'nullable|integer|exists:product_categories,id', + 'is_active' => 'boolean', + 'sort_order' => 'nullable|integer', + 'type' => 'nullable|string', + ]); + + $validated['slug'] = Str::slug($validated['name']); + + // Ensure unique slug + $originalSlug = $validated['slug']; + $count = 1; + while (ProductCategories::where('slug', $validated['slug'])->exists()) { + $validated['slug'] = "{$originalSlug}-{$count}"; + $count++; + } + + $category = ProductCategories::create($validated); + + return response()->json([ + 'error' => false, + 'message' => 'Tạo nhóm sản phẩm thành công.', + 'data' => new ProductCategoryResource($category), + ], 201); + } + + public function update(Request $request, int $id): JsonResponse + { + $category = ProductCategories::find($id); + + if (!$category) { + return response()->json(['error' => true, 'message' => 'Không tìm thấy nhóm sản phẩm.'], 404); + } + + $validated = $request->validate([ + 'name' => 'sometimes|required|string|max:255', + 'parent_id' => 'nullable|integer|exists:product_categories,id', + 'is_active' => 'boolean', + 'sort_order' => 'nullable|integer', + 'type' => 'nullable|string', + ]); + + if (isset($validated['name']) && $validated['name'] !== $category->name) { + $slug = Str::slug($validated['name']); + $originalSlug = $slug; + $count = 1; + while (ProductCategories::where('slug', $slug)->where('id', '!=', $id)->exists()) { + $slug = "{$originalSlug}-{$count}"; + $count++; + } + $validated['slug'] = $slug; + } + + $category->update($validated); + + return response()->json([ + 'error' => false, + 'message' => 'Cập nhật nhóm sản phẩm thành công.', + 'data' => new ProductCategoryResource($category->fresh(['children'])), + ]); + } + + public function destroy(int $id): JsonResponse + { + $category = ProductCategories::withCount('products')->find($id); + + if (!$category) { + return response()->json(['error' => true, 'message' => 'Không tìm thấy nhóm sản phẩm.'], 404); + } + + if ($category->products_count > 0) { + return response()->json([ + 'error' => true, + 'message' => "Không thể xoá nhóm đang chứa {$category->products_count} sản phẩm.", + ], 422); + } + + $category->delete(); + + return response()->json([ + 'error' => false, + 'message' => 'Xoá nhóm sản phẩm thành công.', + ]); + } +} diff --git a/app/Http/Controllers/Api/Admin/Product/ProductController.php b/app/Http/Controllers/Api/Admin/Product/ProductController.php index adadd35..fb2c384 100644 --- a/app/Http/Controllers/Api/Admin/Product/ProductController.php +++ b/app/Http/Controllers/Api/Admin/Product/ProductController.php @@ -4,9 +4,11 @@ namespace App\Http\Controllers\Api\Admin\Product; use App\Http\Controllers\Controller; use App\Http\Resources\Api\Admin\ProductResource; +use App\Models\Product; use App\Repositories\Admin\Product\AdminProductRepositoryInterface; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; +use Illuminate\Support\Str; class ProductController extends Controller { @@ -47,4 +49,85 @@ class ProductController extends Controller 'data' => new ProductResource($product), ]); } + + public function store(Request $request): JsonResponse + { + $validated = $request->validate([ + 'name' => 'required|string|max:255', + 'product_type' => 'nullable|string', + 'category' => 'nullable|string|max:255', + 'is_active' => 'boolean', + 'setup_fee' => 'nullable|numeric|min:0', + 'cpu' => 'nullable|numeric|min:0', + 'ram' => 'nullable|numeric|min:0', + 'disk' => 'nullable|numeric|min:0', + 'bandwidth' => 'nullable|numeric|min:0', + ]); + + $product = Product::create([ + 'name' => $validated['name'], + 'slug' => Str::slug($validated['name']), + 'product_type' => $validated['product_type'] ?? null, + 'is_active' => $validated['is_active'] ?? true, + 'setup_fee' => $validated['setup_fee'] ?? 0, + 'cpu' => $validated['cpu'] ?? null, + 'ram' => $validated['ram'] ?? null, + 'disk' => $validated['disk'] ?? null, + 'bandwidth' => $validated['bandwidth'] ?? null, + ]); + + return response()->json([ + 'error' => false, + 'message' => 'Tạo sản phẩm thành công.', + 'data' => new ProductResource($product), + ], 201); + } + + public function update(Request $request, int $id): JsonResponse + { + $product = Product::find($id); + + if (!$product) { + return response()->json(['error' => true, 'message' => 'Không tìm thấy sản phẩm.'], 404); + } + + $validated = $request->validate([ + 'name' => 'sometimes|required|string|max:255', + 'product_type' => 'nullable|string', + 'is_active' => 'boolean', + 'setup_fee' => 'nullable|numeric|min:0', + 'cpu' => 'nullable|numeric|min:0', + 'ram' => 'nullable|numeric|min:0', + 'disk' => 'nullable|numeric|min:0', + 'bandwidth' => 'nullable|numeric|min:0', + ]); + + if (isset($validated['name']) && $validated['name'] !== $product->name) { + $validated['slug'] = Str::slug($validated['name']); + } + + $product->update($validated); + + return response()->json([ + 'error' => false, + 'message' => 'Cập nhật sản phẩm thành công.', + 'data' => new ProductResource($product->fresh(['pricings', 'category'])), + ]); + } + + public function destroy(int $id): JsonResponse + { + $product = Product::find($id); + + if (!$product) { + return response()->json(['error' => true, 'message' => 'Không tìm thấy sản phẩm.'], 404); + } + + $product->delete(); + + return response()->json([ + 'error' => false, + 'message' => 'Xoá sản phẩm thành công.', + ]); + } } diff --git a/app/Http/Resources/Api/Admin/ProductCategoryResource.php b/app/Http/Resources/Api/Admin/ProductCategoryResource.php new file mode 100644 index 0000000..1a7256b --- /dev/null +++ b/app/Http/Resources/Api/Admin/ProductCategoryResource.php @@ -0,0 +1,30 @@ + $this->id, + 'name' => $this->name, + 'slug' => $this->slug, + 'parent_id' => $this->parent_id, + 'is_active' => (bool) $this->is_active, + 'sort_order' => $this->sort_order, + 'type' => $this->type, + 'type_label' => $this->type_label, + 'created_at' => $this->created_at?->toISOString(), + 'children' => $this->whenLoaded('children', fn () => + self::collection($this->children) + ), + 'products_count' => $this->whenCounted('products', fn () => + $this->recursive_products_count + ), + ]; + } +} diff --git a/app/Models/ProductCategories.php b/app/Models/ProductCategories.php index 2a581ed..48ee40c 100644 --- a/app/Models/ProductCategories.php +++ b/app/Models/ProductCategories.php @@ -29,7 +29,9 @@ class ProductCategories extends Model public function children() { - return $this->hasMany(ProductCategories::class, 'parent_id')->with('children'); + return $this->hasMany(ProductCategories::class, 'parent_id') + ->withCount('products') + ->with('children'); } public function products() @@ -37,6 +39,23 @@ class ProductCategories extends Model return $this->hasMany(Product::class, 'product_categories_id'); } + /** + * Recursively count products in this category and all descendants. + * Requires `withCount('products')` and `with('children')` to be loaded. + */ + public function getRecursiveProductsCountAttribute(): int + { + $count = (int) ($this->products_count ?? 0); + + if ($this->relationLoaded('children')) { + foreach ($this->children as $child) { + $count += $child->recursive_products_count; + } + } + + return $count; + } + public function getTypeLabelAttribute(): ?string { if (!$this->type) { diff --git a/routes/api.php b/routes/api.php index 67277aa..cdb52de 100644 --- a/routes/api.php +++ b/routes/api.php @@ -4,6 +4,7 @@ use App\Http\Controllers\Api\Admin\Auth\AuthController; use App\Http\Controllers\Api\Admin\Customer\CustomerController; use App\Http\Controllers\Api\Admin\Invoice\InvoiceController; use App\Http\Controllers\Api\Admin\Order\OrderController; +use App\Http\Controllers\Api\Admin\Product\ProductCategoryController; use App\Http\Controllers\Api\Admin\Product\ProductController; use App\Http\Controllers\Api\Admin\VCenter\VCenterController; use App\Http\Services\Media; @@ -43,7 +44,19 @@ Route::prefix('v1') // Products Route::prefix('products')->group(function () { Route::get('/', [ProductController::class, 'index']); + Route::post('/', [ProductController::class, 'store']); Route::get('{id}', [ProductController::class, 'show']); + Route::put('{id}', [ProductController::class, 'update']); + Route::delete('{id}', [ProductController::class, 'destroy']); + }); + + // Product Categories (Groups) + Route::prefix('product-categories')->group(function () { + Route::get('/', [ProductCategoryController::class, 'index']); + Route::post('/', [ProductCategoryController::class, 'store']); + Route::get('{id}', [ProductCategoryController::class, 'show']); + Route::put('{id}', [ProductCategoryController::class, 'update']); + Route::delete('{id}', [ProductCategoryController::class, 'destroy']); }); // Orders