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)) ->when($request->boolean('unassigned'), fn($q) => $q->whereNull('customer_id')); $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]); $service->setAttribute('activity_logs', ActivityLog::where('subject_type', Service::class) ->where('subject_id', $service->id) ->latest('id') ->limit(50) ->get(['id', 'event', 'description', 'causer_type', 'causer_name', 'old_values', 'new_values', 'created_at'])); 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', ]); if ($data['action'] === 'detach-customer') { $updated = $this->detachCustomers($data['ids']); } else { $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()], }; $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 { $this->detachCustomers([Service::findOrFail($id)->id]); return response()->json(['error' => false, 'message' => 'Đã gỡ khách hàng khỏi dịch vụ.']); } private function detachCustomers(array $ids): int { return DB::transaction(function () use ($ids) { $services = Service::with('customer')->whereIn('id', $ids)->whereNotNull('customer_id')->get(); $services->each(fn (Service $service) => $this->assignment->detach($service)); return $services->count(); }); } 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.']); } }