feat: Implement full CRUD API for product categories and products, including recursive product counts for categories.
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Admin\Product;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\Api\Admin\ProductCategoryResource;
|
||||
use App\Models\ProductCategories;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class ProductCategoryController extends Controller
|
||||
{
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$perPage = min((int) $request->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.',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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.',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources\Api\Admin;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class ProductCategoryResource extends JsonResource
|
||||
{
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $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
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user