false, 'error' => $message ], JSON_UNESCAPED_UNICODE); exit; } require_once $_SERVER['DOCUMENT_ROOT'] . '/includes/session_bootstrap.php'; try { $pdo = og_session_get_pdo(); if (!$pdo instanceof PDO) { throw new PDOException('Nie udało się zainicjalizować połączenia z bazą danych.'); } // Parametry z requestu $page = isset($_GET['page']) ? max(1, (int)$_GET['page']) : 1; $limit = isset($_GET['limit']) ? min(100, max(1, (int)$_GET['limit'])) : 50; $offset = ($page - 1) * $limit; // Sortowanie $sortBy = isset($_GET['sortBy']) ? $_GET['sortBy'] : 'id'; $sortOrder = isset($_GET['sortOrder']) && strtoupper($_GET['sortOrder']) === 'DESC' ? 'DESC' : 'ASC'; // Dozwolone kolumny do sortowania (bezpieczeństwo) $allowedSortColumns = ['id', 'username', 'email', 'created_at', 'role']; if (!in_array($sortBy, $allowedSortColumns)) { $sortBy = 'id'; } // Filtrowanie $filters = []; $params = []; // Filtr po username if (isset($_GET['username']) && $_GET['username'] !== '') { $filters[] = "u.username LIKE :username"; $params[':username'] = '%' . $_GET['username'] . '%'; } // Filtr po email if (isset($_GET['email']) && $_GET['email'] !== '') { $filters[] = "u.email LIKE :email"; $params[':email'] = '%' . $_GET['email'] . '%'; } // Filtr po roli if (isset($_GET['role']) && $_GET['role'] !== '') { $filters[] = "u.role = :role"; $params[':role'] = $_GET['role']; } // Filtr po statusie weryfikacji if (isset($_GET['email_verified'])) { $filters[] = "u.email_verified = :email_verified"; $params[':email_verified'] = (int)$_GET['email_verified']; } // Filtr po dacie rejestracji (od) if (isset($_GET['created_from']) && $_GET['created_from'] !== '') { $filters[] = "u.created_at >= :created_from"; $params[':created_from'] = $_GET['created_from']; } // Filtr po dacie rejestracji (do) if (isset($_GET['created_to']) && $_GET['created_to'] !== '') { $filters[] = "u.created_at <= :created_to"; $params[':created_to'] = $_GET['created_to']; } // Wyklucz użytkowników z disabled = 1 $filters[] = "(u.disabled IS NULL OR u.disabled = 0)"; // Budowanie WHERE clause $whereClause = ''; if (count($filters) > 0) { $whereClause = 'WHERE ' . implode(' AND ', $filters); } // OPTYMALIZACJA: Fast approximate count z limitem 100k // Sprawdzenie czy count jest w cache (ważny 5 minut) $cacheKey = 'users_count_' . md5(serialize($params)); $totalRecords = 0; $isApproximate = false; if (isset($_SESSION[$cacheKey]) && isset($_SESSION[$cacheKey . '_time']) && (time() - $_SESSION[$cacheKey . '_time']) < 300) { // Cache hit - użyj zapisanej wartości $totalRecords = $_SESSION[$cacheKey]; $isApproximate = $_SESSION[$cacheKey . '_approx'] ?? false; } else { // Cache miss - policz z limitem // OPTYMALIZACJA: Limit count do 100k dla wydajności $countSql = "SELECT COUNT(*) as total FROM ( SELECT 1 FROM users u $whereClause LIMIT 100000 ) as limited_count"; $countStmt = $pdo->prepare($countSql); try { $countStmt->execute($params); $totalRecords = $countStmt->fetch(PDO::FETCH_ASSOC)['total']; // Jeśli osiągnięto limit, sprawdź czy jest więcej if ($totalRecords >= 100000) { $checkMoreSql = "SELECT EXISTS( SELECT 1 FROM users u $whereClause LIMIT 100001 ) as has_more"; $checkStmt = $pdo->prepare($checkMoreSql); $checkStmt->execute($params); if ($checkStmt->fetch(PDO::FETCH_ASSOC)['has_more']) { $isApproximate = true; $totalRecords = 100000; // Pokazuj 100k+ } } // Zapisz w cache na 5 minut $_SESSION[$cacheKey] = $totalRecords; $_SESSION[$cacheKey . '_time'] = time(); $_SESSION[$cacheKey . '_approx'] = $isApproximate; } catch (PDOException $e) { returnError('Błąd podczas zliczania rekordów: ' . $e->getMessage()); } } $totalPages = $totalRecords > 0 ? ceil($totalRecords / $limit) : 1; // Pobieranie użytkowników + podstawowe statystyki salda (lekki LEFT JOIN) $sql = "SELECT u.id, u.username, u.email, u.first_name, u.last_name, u.role, u.email_verified, u.created_at, COALESCE(u.account_suspended, 0) as account_suspended, u.suspension_reason, u.suspended_until, a.username AS suspended_by_username, COALESCE(us.balance, 0) as balance, COALESCE(us.matches_played, 0) as matches_played, COALESCE(us.matches_won, 0) as matches_won, COALESCE(us.matches_lost, 0) as matches_lost, us.account_status FROM users u LEFT JOIN user_stats us ON u.id = us.user_id LEFT JOIN users a ON a.id = u.suspended_by $whereClause ORDER BY u.$sortBy $sortOrder LIMIT :limit OFFSET :offset"; $stmt = $pdo->prepare($sql); // Bindowanie parametrów filtrów foreach ($params as $key => $value) { $stmt->bindValue($key, $value); } // Bindowanie limit i offset $stmt->bindValue(':limit', $limit, PDO::PARAM_INT); $stmt->bindValue(':offset', $offset, PDO::PARAM_INT); try { $stmt->execute(); $users = $stmt->fetchAll(PDO::FETCH_ASSOC); } catch (PDOException $e) { returnError('Błąd podczas pobierania użytkowników: ' . $e->getMessage()); } // Formatowanie odpowiedzi $response = [ 'success' => true, 'data' => $users, 'pagination' => [ 'currentPage' => $page, 'totalPages' => $totalPages, 'totalRecords' => (int)$totalRecords, 'totalRecordsApproximate' => $isApproximate, 'totalRecordsDisplay' => $isApproximate ? '100,000+' : number_format($totalRecords, 0, ',', ' '), 'recordsPerPage' => $limit, 'hasNextPage' => $page < $totalPages, 'hasPreviousPage' => $page > 1 ], 'filters' => [ 'sortBy' => $sortBy, 'sortOrder' => $sortOrder, 'appliedFilters' => array_keys($params) ] ]; echo json_encode($response, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); } catch (PDOException $e) { returnError('Błąd połączenia z bazą danych: ' . $e->getMessage()); } catch (Exception $e) { returnError('Nieoczekiwany błąd: ' . $e->getMessage()); } ?>