feat: Implement client-facing service management API with UUID for services and bulk actions for product services.

This commit is contained in:
pbao9
2026-03-13 15:17:23 +07:00
parent 5160ac967a
commit e0341363f6
16 changed files with 479 additions and 3 deletions
@@ -78,10 +78,10 @@ class ProductCategoryController extends Controller
$validated['slug'] = Str::slug($validated['name']);
// Ensure unique slug
// Ensure unique slug (include soft-deleted to avoid DB unique constraint violation)
$originalSlug = $validated['slug'];
$count = 1;
while (ProductCategories::where('slug', $validated['slug'])->exists()) {
while (ProductCategories::withTrashed()->where('slug', $validated['slug'])->exists()) {
$validated['slug'] = "{$originalSlug}-{$count}";
$count++;
}
@@ -115,7 +115,7 @@ class ProductCategoryController extends Controller
$slug = Str::slug($validated['name']);
$originalSlug = $slug;
$count = 1;
while (ProductCategories::where('slug', $slug)->where('id', '!=', $id)->exists()) {
while (ProductCategories::withTrashed()->where('slug', $slug)->where('id', '!=', $id)->exists()) {
$slug = "{$originalSlug}-{$count}";
$count++;
}
@@ -153,4 +153,118 @@ class ProductCategoryController extends Controller
'message' => 'Xoá nhóm sản phẩm thành công.',
]);
}
public function trashed(): JsonResponse
{
$items = ProductCategories::onlyTrashed()
->withCount('products')
->orderBy('deleted_at', 'desc')
->get();
return response()->json([
'error' => false,
'data' => ProductCategoryResource::collection($items),
]);
}
public function restore(int $id): JsonResponse
{
$category = ProductCategories::onlyTrashed()->find($id);
if (!$category) {
return response()->json(['error' => true, 'message' => 'Không tìm thấy nhóm trong thùng rác.'], 404);
}
$category->restore();
return response()->json([
'error' => false,
'message' => 'Khôi phục nhóm sản phẩm thành công.',
]);
}
public function forceDelete(int $id): JsonResponse
{
$category = ProductCategories::onlyTrashed()->find($id);
if (!$category) {
return response()->json(['error' => true, 'message' => 'Không tìm thấy nhóm trong thùng rác.'], 404);
}
$category->forceDelete();
return response()->json([
'error' => false,
'message' => 'Xoá vĩnh viễn nhóm sản phẩm thành công.',
]);
}
public function bulkUpdateStatus(Request $request): JsonResponse
{
$validated = $request->validate([
'ids' => 'required|array',
'ids.*' => 'integer',
'is_active' => 'required|boolean',
]);
$updated = ProductCategories::whereIn('id', $validated['ids'])
->update(['is_active' => $validated['is_active']]);
$label = $validated['is_active'] ? 'kích hoạt' : 'ẩn';
return response()->json([
'error' => false,
'message' => "Đã {$label} {$updated} nhóm sản phẩm.",
]);
}
public function bulkDestroy(Request $request): JsonResponse
{
$ids = $request->validate(['ids' => 'required|array', 'ids.*' => 'integer'])['ids'];
$blocked = ProductCategories::withCount('products')
->whereIn('id', $ids)
->get()
->filter(fn ($c) => $c->products_count > 0);
if ($blocked->isNotEmpty()) {
$names = $blocked->pluck('name')->implode(', ');
return response()->json([
'error' => true,
'message' => "Không thể xoá các nhóm đang chứa sản phẩm: {$names}.",
], 422);
}
$deleted = ProductCategories::whereIn('id', $ids)->delete();
return response()->json([
'error' => false,
'message' => "Đã xoá {$deleted} nhóm sản phẩm.",
]);
}
public function bulkRestore(Request $request): JsonResponse
{
$ids = $request->validate(['ids' => 'required|array', 'ids.*' => 'integer'])['ids'];
$restored = ProductCategories::onlyTrashed()->whereIn('id', $ids)->restore();
return response()->json([
'error' => false,
'message' => "Đã khôi phục {$restored} nhóm sản phẩm.",
]);
}
public function bulkForceDelete(Request $request): JsonResponse
{
$ids = $request->validate(['ids' => 'required|array', 'ids.*' => 'integer'])['ids'];
$count = ProductCategories::onlyTrashed()->whereIn('id', $ids)->count();
ProductCategories::onlyTrashed()->whereIn('id', $ids)->forceDelete();
return response()->json([
'error' => false,
'message' => "Đã xoá vĩnh viễn {$count} nhóm sản phẩm.",
]);
}
}
@@ -0,0 +1,49 @@
<?php
namespace App\Http\Controllers\Api\Client\Service;
use App\Http\Controllers\Controller;
use App\Http\Resources\Api\Client\Service\ServiceClientDetailResource;
use \App\Http\Resources\Api\Client\Service\ServiceClientResource;
use App\Repositories\Api\ServiceClient\ServiceClientRepository;
use App\Services\Api\ServiceClient\ServiceClientService;
use Illuminate\Http\Request;
class ServiceController extends Controller
{
protected $serviceClientService;
protected $serviceClientRepository;
public function __construct(
ServiceClientService $serviceClientService,
ServiceClientRepository $serviceClientRepository
) {
$this->serviceClientService = $serviceClientService;
$this->serviceClientRepository = $serviceClientRepository;
}
public function getService(Request $request)
{
$limit = $request->get('limit', 10);
$services = $this->serviceClientService->getActiveServices($limit);
return ServiceClientResource::collection($services)->additional([
'error' => false,
'message' => 'Lấy dữ liệu thành công',
'page_type' => 'service',
]);
}
public function getDetail(Request $request, string $uuid)
{
$service = $this->serviceClientService->getDetailService($uuid);
return (new ServiceClientDetailResource($service))->additional([
'error' => false,
'message' => 'Lấy dữ liệu thành công',
'page_type' => 'service',
]);
}
}
@@ -345,6 +345,74 @@ class OrderController extends Controller
}
}
/**
* GET /v1/client/dich-vu/{uuid} — service detail by UUID
*/
public function getClientServiceDetail(Request $request, string $uuid): JsonResponse
{
try {
$service = Service::query()
->with(['product', 'addons', 'invoice', 'orderItem'])
->where('uuid', $uuid)
->where('customer_id', $request->user()->id)
->first();
if (!$service) {
return $this->notFound('Không tìm thấy dịch vụ.');
}
$vmware = $service->vm_id
? ModuleVmware::with(['ipAddress', 'os', 'vCenter'])->find((int) $service->vm_id)
: null;
$data = [
'id' => $service->id,
'uuid' => $service->uuid,
'service_name' => $service->product?->name ?? $service->server_name,
'server_name' => $service->server_name,
'product_type' => $service->product?->product_type,
'billing_cycle' => $service->orderItem?->billing_cycle,
'status' => $service->status,
'status_text' => $service->status_label,
'auto_renew' => (bool) $service->auto_renew,
'started_at' => $this->isoDate($service->started_at),
'expires_at' => $this->isoDate($service->expires_at),
'note' => $service->note,
'invoice_id' => $service->invoice_id,
'invoice_number' => $service->invoice?->invoice_number,
'product' => $service->product ? [
'id' => $service->product->id,
'uuid' => $service->product->uuid,
'name' => $service->product->name,
'product_type' => $service->product->product_type,
'specs' => [
'cpu' => $service->product->cpu,
'ram' => $service->product->ram,
'disk' => $service->product->disk,
'bandwidth' => $service->product->bandwidth,
],
] : null,
'addons' => $service->addons->map(fn($a) => [
'type' => $a->addon_type,
'value' => $a->addon_value,
'price' => $a->price,
])->all(),
'vmware' => $vmware ? [
'id' => $vmware->id,
'name' => $vmware->name,
'hostname' => $vmware->hostname,
'ip_address' => $vmware->ipAddress?->ip,
'os_name' => $vmware->os?->name,
'status' => $vmware->status,
] : null,
];
return response()->json(['success' => true, 'data' => $data]);
} catch (\Exception $e) {
return $this->error($e->getMessage());
}
}
/**
* PATCH /v1/client/dich-vu/{id}/auto-renew — toggle auto-renewal on a service
*/
@@ -16,6 +16,7 @@ use App\Models\OrderAddon;
use App\Models\OrderItem;
use App\Models\Product;
use App\Models\ProductPricings;
use App\Models\Service;
use App\Repositories\Api\Service\ServiceRepository;
use App\Services\Api\Service\ServiceService;
use Illuminate\Http\Request;
@@ -190,6 +191,31 @@ class ServiceController extends Controller
};
}
public function getDetailServiceCustomer(Request $request, $uuid)
{
$service = Service::where('uuid', $uuid)->first();
if (!$service) {
return response()->json([
'error' => true,
'message' => 'Dịch vụ không tồn tại'
], 404);
}
$service->load([
'product.category',
'orderItem.os.osGroup',
'orderItem.pricing',
'invoice',
]);
return response()->json([
'error' => false,
'message' => 'Lấy dữ liệu thành công',
'data' => new ServiceResource($service),
]);
}
/**
* Get addons for a specific product
* GET /v1/products/{productId}/addons