per_page ?? 15), 100); $query = Service::with(['customer:id,uuid,fullname,email', 'product:id,uuid,name', 'invoice:id,invoice_number', 'vmware:id,name,module_ip_address']) ->where(fn($q) => $q->whereNull('order_item_id')->orWhereNull('type')->orWhere('type', '!=', 'domain')) ->when($request->filter, fn($q, $v) => $q->where('server_name', 'like', "%{$v}%")) ->when($request->status, fn($q, $v) => $q->where('status', $v)) ->when($request->customer_id, fn($q, $v) => $q->where('customer_id', $v)); $paginator = $query->latest()->paginate($perPage); return response()->json([ 'error' => false, 'data' => [ 'items' => $paginator->items(), 'total' => $paginator->total(), 'per_page' => $paginator->perPage(), 'current_page' => $paginator->currentPage(), 'last_page' => $paginator->lastPage(), ], ]); } public function domainQuote(Request $request): JsonResponse { $data = $request->validate([ 'domain' => 'required|string|max:255', 'billing_cycle' => 'nullable|in:monthly,quarterly,semi_annually,annually,biennially,triennially', ]); $pricing = DomainPricing::matchDomain($data['domain']); if (!$pricing) { return response()->json(['error' => false, 'data' => null]); } $years = DomainPricing::yearsForMonths(OrderHelper::cycleToMonths($data['billing_cycle'] ?? 'annually')); return response()->json([ 'error' => false, 'data' => [ 'tld' => $pricing->tld, 'setup_fee' => is_numeric($pricing->setup_fee) ? (int) $pricing->setup_fee : 0, 'first_year' => (int) $pricing->first_year, 'renewal' => (int) $pricing->recurring_fee, 'years' => $years, 'price' => $pricing->orderPrice($years), ], ]); } public function show(int $id): JsonResponse { $service = Service::with(['customer', 'product', 'orderItem', 'invoice', 'vmware.os.osGroup', 'addons', ...Service::HOSTING_ACCOUNT_RELATIONS]) ->findOrFail($id); ModuleVmware::resolveVirtualMachineIds([$service->vmware]); return response()->json(['error' => false, 'data' => $service]); } public function bulkAction(Request $request): JsonResponse { $data = $request->validate([ 'ids' => 'required|array|min:1', 'ids.*' => 'integer|distinct|exists:services,id', 'action' => 'required|in:activate,suspend,cancel,detach-customer', ]); $updates = match ($data['action']) { 'activate' => ['status' => 'active', 'suspended_at' => null, 'cancelled_at' => null], 'suspend' => ['status' => 'suspended', 'suspended_at' => now()], 'cancel' => ['status' => 'cancelled', 'cancelled_at' => now()], 'detach-customer' => ['customer_id' => null], }; $updated = Service::whereIn('id', $data['ids'])->update($updates); return response()->json([ 'error' => false, 'message' => "Đã cập nhật {$updated} dịch vụ.", 'data' => ['updated' => $updated], ]); } public function detachCustomer(int $id): JsonResponse { $service = Service::findOrFail($id); $service->update(['customer_id' => null]); return response()->json(['error' => false, 'message' => 'Đã gỡ khách hàng khỏi dịch vụ.']); } public function resendCredentials(int $id): JsonResponse { $service = Service::findOrFail($id); abort_if(!$service->vm_id, 422, 'Dịch vụ chưa gắn VM.'); ResendVmCredentialsEmailJob::dispatch($service->vm_id); return response()->json(['error' => false, 'message' => 'Đã xếp hàng gửi lại thông tin VPS cho khách hàng.']); } }