togethere.cloud/public_html/administration/users/index.php

1706 lines
64 KiB
PHP
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
require_once __DIR__ . '/../includes/auth.php';
require_once __DIR__ . '/../includes/config.php';
require_once __DIR__ . '/../includes/header.php';
require_once __DIR__ . '/../includes/sidebar.php';
?>
<div class="admin-content">
<style>
.admin-page-title {
font-size: 24px;
font-weight: 600;
color: #23282d;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 2px solid #0073aa;
}
.admin-content-box {
background: #fff;
border: 1px solid #ddd;
border-radius: 4px;
padding: 20px;
margin-top: 20px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
/* Stats Cards */
.users-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-bottom: 20px;
}
.stat-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.stat-card:nth-child(2) {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
}
.stat-card:nth-child(3) {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
}
.stat-value {
font-size: 32px;
font-weight: bold;
margin-bottom: 5px;
}
.stat-label {
font-size: 13px;
opacity: 0.9;
}
/* Controls */
.users-controls {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 20px;
padding: 15px;
background: #f8f9fa;
border-radius: 4px;
}
.control-group {
display: flex;
gap: 8px;
align-items: center;
}
.control-group label {
font-size: 13px;
font-weight: 500;
color: #555;
white-space: nowrap;
}
.users-controls input,
.users-controls select {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 13px;
background: white;
transition: border-color 0.2s;
}
.users-controls input:focus,
.users-controls select:focus {
outline: none;
border-color: #0073aa;
}
.btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
display: inline-flex;
align-items: center;
gap: 5px;
}
.btn-primary {
background: #0073aa;
color: white;
}
.btn-primary:hover {
background: #005a87;
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-secondary:hover {
background: #545b62;
}
.btn:disabled {
background: #ccc;
cursor: not-allowed;
opacity: 0.6;
}
/* Table */
.users-table-wrapper {
overflow-x: auto;
margin-bottom: 20px;
}
.users-table {
width: 100%;
border-collapse: collapse;
background: white;
}
.users-table th,
.users-table td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #e5e5e5;
font-size: 13px;
}
.users-table th {
background: #f8f9fa;
font-weight: 600;
color: #23282d;
cursor: pointer;
user-select: none;
position: relative;
white-space: nowrap;
}
.users-table th:hover {
background: #e9ecef;
}
.users-table th.sortable::after {
content: '⇅';
position: absolute;
right: 8px;
opacity: 0.3;
font-size: 11px;
}
.users-table th.sorted-asc::after {
content: '▲';
opacity: 1;
color: #0073aa;
}
.users-table th.sorted-desc::after {
content: '▼';
opacity: 1;
color: #0073aa;
}
.users-table tbody tr:hover {
background: #f8f9fa;
}
.users-table tbody tr:last-child td {
border-bottom: none;
}
/* Badges */
.badge {
display: inline-block;
padding: 3px 8px;
border-radius: 3px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.3px;
}
.badge-success {
background: #d4edda;
color: #155724;
}
.badge-warning {
background: #fff3cd;
color: #856404;
}
.badge-danger {
background: #f8d7da;
color: #721c24;
}
.badge-info {
background: #d1ecf1;
color: #0c5460;
}
.badge-secondary {
background: #e2e3e5;
color: #383d41;
}
/* User Actions */
.user-actions {
display: flex;
gap: 5px;
}
.btn-action {
padding: 4px 8px;
font-size: 12px;
border-radius: 3px;
border: none;
cursor: pointer;
transition: opacity 0.2s;
}
.btn-action:hover {
opacity: 0.8;
}
.btn-edit {
background: #0073aa;
color: white;
}
.btn-delete {
background: #dc3545;
color: white;
}
.btn-manage {
background: #fd7e14;
color: white;
}
.btn-history {
background: #6f42c1;
color: white;
}
/* Suspended row highlight */
.users-table tbody tr.row-suspended td {
background-color: #ffe8e8 !important;
border-top: 2px solid #e00 !important;
border-bottom: 2px solid #e00 !important;
}
.users-table tbody tr.row-suspended td:first-child {
border-left: 2px solid #e00 !important;
}
.users-table tbody tr.row-suspended td:last-child {
border-right: 2px solid #e00 !important;
}
.users-table tbody tr.row-suspended:hover td {
background-color: #ffd6d6 !important;
}
/* Pagination */
.users-pagination {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px;
background: #f8f9fa;
border-radius: 4px;
flex-wrap: wrap;
gap: 10px;
}
.pagination-info {
font-size: 13px;
color: #666;
}
.pagination-controls {
display: flex;
gap: 8px;
align-items: center;
flex-wrap: wrap;
}
.page-input {
width: 60px;
text-align: center;
padding: 6px 8px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 13px;
}
/* Loading & Error */
.loading-overlay {
text-align: center;
padding: 40px;
color: #666;
}
.loading-overlay .spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #0073aa;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 15px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.error-message {
background: #f8d7da;
color: #721c24;
padding: 12px 15px;
border-radius: 4px;
margin-bottom: 15px;
border: 1px solid #f5c6cb;
font-size: 13px;
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #666;
}
.empty-state .icon {
font-size: 64px;
margin-bottom: 15px;
opacity: 0.3;
}
/* Status tooltip */
.status-badge-wrap {
position: relative;
display: inline-block;
cursor: default;
}
.status-tooltip {
display: none;
position: absolute;
bottom: calc(100% + 6px);
left: 50%;
transform: translateX(-50%);
background: #1e1e1e;
color: #f0f0f0;
font-size: 12px;
line-height: 1.7;
padding: 10px 14px;
border-radius: 6px;
white-space: nowrap;
z-index: 9999;
box-shadow: 0 4px 16px rgba(0,0,0,0.35);
pointer-events: none;
min-width: 200px;
}
.status-tooltip::after {
content: '';
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
border: 6px solid transparent;
border-top-color: #1e1e1e;
}
.status-badge-wrap:hover .status-tooltip {
display: block;
}
/* Custom Modal/Alert Styles */
.modal-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
z-index: 10000;
animation: fadeIn 0.2s ease-out;
}
.modal-overlay.active {
display: flex;
align-items: center;
justify-content: center;
}
.modal-box {
background: white;
border-radius: 8px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
max-width: 620px;
width: 90%;
animation: slideUp 0.3s ease-out;
}
.modal-header {
padding: 20px 25px;
border-bottom: 1px solid #e5e5e5;
display: flex;
align-items: center;
gap: 12px;
}
.modal-header.alert {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 8px 8px 0 0;
}
.modal-header.confirm {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
color: white;
border-radius: 8px 8px 0 0;
}
.modal-header.success {
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
color: white;
border-radius: 8px 8px 0 0;
}
.modal-icon {
font-size: 28px;
}
.modal-title {
font-size: 18px;
font-weight: 600;
margin: 0;
}
.modal-body {
padding: 25px;
font-size: 14px;
line-height: 1.6;
color: #444;
max-height: 70vh;
overflow-y: auto;
}
/* Form Styles in Modal */
.modal-body input[type="text"],
.modal-body input[type="email"],
.modal-body select {
transition: border-color 0.2s, box-shadow 0.2s;
}
.modal-body input[type="text"]:focus,
.modal-body input[type="email"]:focus,
.modal-body select:focus {
outline: none;
border-color: #0073aa;
box-shadow: 0 0 0 3px rgba(0, 115, 170, 0.1);
}
.modal-body input[type="checkbox"] {
accent-color: #0073aa;
}
.modal-footer {
padding: 15px 25px;
border-top: 1px solid #e5e5e5;
display: flex;
gap: 10px;
justify-content: flex-end;
}
.modal-btn {
padding: 10px 20px;
border: none;
border-radius: 5px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.modal-btn-primary {
background: #0073aa;
color: white;
}
.modal-btn-primary:hover {
background: #005a87;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 115, 170, 0.4);
}
.modal-btn-danger {
background: #dc3545;
color: white;
}
.modal-btn-danger:hover {
background: #c82333;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(220, 53, 69, 0.4);
}
.modal-btn-secondary {
background: #f8f9fa;
color: #444;
border: 1px solid #ddd;
}
.modal-btn-secondary:hover {
background: #e9ecef;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideUp {
from {
transform: translateY(50px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
/* Responsive */
@media (max-width: 768px) {
.users-stats {
grid-template-columns: 1fr;
}
.users-controls {
flex-direction: column;
}
.control-group {
width: 100%;
}
.control-group input,
.control-group select {
flex: 1;
}
.users-pagination {
flex-direction: column;
}
.pagination-controls {
width: 100%;
justify-content: center;
}
.modal-box {
width: 95%;
}
}
</style>
<h1 class="admin-page-title">👥 Zarządzanie Użytkownikami</h1>
<!-- Stats Cards -->
<div class="users-stats">
<div class="stat-card">
<div class="stat-value" id="totalUsers">-</div>
<div class="stat-label">Wszystkich użytkowników</div>
</div>
<div class="stat-card">
<div class="stat-value" id="currentPage">-</div>
<div class="stat-label">Aktualna strona</div>
</div>
<div class="stat-card">
<div class="stat-value" id="totalPages">-</div>
<div class="stat-label">Wszystkich stron</div>
</div>
</div>
<div class="admin-content-box">
<!-- Error Message -->
<div id="errorMessage" class="error-message" style="display: none;"></div>
<!-- Controls -->
<div class="users-controls">
<div class="control-group">
<label>🔍 Username:</label>
<input type="text" id="filterUsername" placeholder="Szukaj...">
</div>
<div class="control-group">
<label>📧 Email:</label>
<input type="text" id="filterEmail" placeholder="Szukaj...">
</div>
<div class="control-group">
<label>👤 Rola:</label>
<select id="filterRole">
<option value="">Wszystkie</option>
<option value="user">Player</option>
<option value="admin">Admin</option>
<option value="moderator">Moderator</option>
</select>
</div>
<div class="control-group">
<label>✓ Status:</label>
<select id="filterVerified">
<option value="">Wszystkie</option>
<option value="1">Zweryfikowani</option>
<option value="0">Niezweryfikowani</option>
</select>
</div>
<button class="btn btn-primary" onclick="applyFilters()">
🔍 Filtruj
</button>
<button class="btn btn-secondary" onclick="clearFilters()">
✖ Wyczyść
</button>
</div>
<!-- Loading State -->
<div id="loadingState" class="loading-overlay" style="display: none;">
<div class="spinner"></div>
<div>Ładowanie użytkowników...</div>
</div>
<!-- Users Table -->
<div class="users-table-wrapper" id="tableWrapper">
<table class="users-table">
<thead>
<tr>
<th class="sortable" onclick="sortBy('id')">ID</th>
<th class="sortable" onclick="sortBy('username')">Username</th>
<th class="sortable" onclick="sortBy('email')">Email</th>
<th>Imię i nazwisko</th>
<th class="sortable" onclick="sortBy('role')">Rola</th>
<th>Weryfikacja</th>
<th>Saldo</th>
<th>Statystyki</th>
<th class="sortable" onclick="sortBy('created_at')">Rejestracja</th>
<th>Status</th>
<th>Akcje</th>
</tr>
</thead>
<tbody id="usersBody">
</tbody>
</table>
</div>
<!-- Pagination -->
<div class="users-pagination">
<div class="pagination-info" id="paginationInfo">
Ładowanie...
</div>
<div class="pagination-controls">
<button class="btn btn-secondary" id="btnFirst" onclick="goToFirstPage()">
⏮ Pierwsza
</button>
<button class="btn btn-secondary" id="btnPrev" onclick="previousPage()">
← Poprzednia
</button>
<input type="number" id="pageInput" class="page-input" min="1" value="1">
<button class="btn btn-primary" onclick="goToPageInput()">
Przejdź
</button>
<button class="btn btn-secondary" id="btnNext" onclick="nextPage()">
Następna →
</button>
<button class="btn btn-secondary" id="btnLast" onclick="goToLastPage()">
Ostatnia ⏭
</button>
</div>
</div>
</div>
</div>
<!-- Custom Modal for Alerts and Confirms -->
<div id="customModal" class="modal-overlay">
<div class="modal-box">
<div id="modalHeader" class="modal-header">
<span id="modalIcon" class="modal-icon"></span>
<h3 id="modalTitle" class="modal-title"></h3>
</div>
<div id="modalBody" class="modal-body"></div>
<div id="modalFooter" class="modal-footer"></div>
</div>
</div>
<script src="../../js/loadUsers.js"></script>
<script>
const currentAdminId = <?php echo (int)($_SESSION['user_id'] ?? 0); ?>;
// Inicjalizacja
const loader = new LoadUsers('../../api/loadUsers.php');
let currentData = null;
let currentSort = { column: 'id', order: 'ASC' };
function isProtectedUser(user) {
if (!user) return false;
return Number(user.id) === Number(currentAdminId) || String(user.role || '').toLowerCase() === 'admin';
}
// Ładowanie przy starcie
document.addEventListener('DOMContentLoaded', () => {
loadUsers();
});
async function loadUsers() {
loader.cache = {}; // Zawsze świeże dane - nie używaj cache
showLoading(true);
hideError();
try {
currentData = await loader.getUsers();
renderUsers(currentData);
updatePagination(currentData.pagination);
updateStats(currentData.pagination);
} catch (error) {
showError('Błąd podczas ładowania użytkowników: ' + error.message);
console.error(error);
} finally {
showLoading(false);
}
}
function renderUsers(data) {
const tbody = document.getElementById('usersBody');
tbody.innerHTML = '';
if (data.data.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="10">
<div class="empty-state">
<div class="icon">👥</div>
<h3>Brak użytkowników</h3>
<p>Nie znaleziono użytkowników spełniających kryteria wyszukiwania.</p>
</div>
</td>
</tr>
`;
return;
}
data.data.forEach(user => {
const tr = document.createElement('tr');
const isSuspendedRow = parseInt(user.account_suspended) === 1;
// Styl tylko dla zawieszonego wiersza - box-shadow zamiast border (renderuje się na wierzchu, nie przykrywany przez border-collapse)
const tdS = isSuspendedRow ? 'background-color:#ffe8e8;box-shadow:inset 0 1px 0 red,inset 0 -1px 0 red;' : '';
const tdFirst = isSuspendedRow ? 'background-color:#ffe8e8;box-shadow:inset 0 1px 0 red,inset 0 -1px 0 red,inset 1px 0 0 red;' : '';
const tdLast = isSuspendedRow ? 'background-color:#ffe8e8;box-shadow:inset 0 1px 0 red,inset 0 -1px 0 red,inset -1px 0 0 red;' : '';
// Status badge
let statusBadge;
if (isSuspendedRow) {
const reason = user.suspension_reason || '-';
const until = user.suspended_until ? user.suspended_until : 'bezterminowo';
const byAdmin = user.suspended_by_username || '-';
const tooltipLines = [
`<b>Powód:</b> ${escapeHtml(reason)}`,
`<b>Do:</b> ${escapeHtml(until)}`,
`<b>Przez:</b> ${escapeHtml(byAdmin)}`,
].join('<br>');
statusBadge = `<div class="status-badge-wrap">
<span class="badge badge-danger" style="cursor:help;">&#9940; Zawieszone</span>
<div class="status-tooltip">${tooltipLines}</div>
</div>`;
} else {
statusBadge = '<span class="badge badge-success">&#10003; OK</span>';
}
// Badge weryfikacji
const verifiedBadge = user.email_verified == 1
? '<span class="badge badge-success">✓ Tak</span>'
: '<span class="badge badge-warning">⚠ Nie</span>';
// Badge roli
let roleBadge = '<span class="badge badge-secondary">' + (user.role || 'user') + '</span>';
if (user.role === 'admin') {
roleBadge = '<span class="badge badge-danger">Admin</span>';
} else if (user.role === 'moderator') {
roleBadge = '<span class="badge badge-info">Moderator</span>';
}
// Imię i nazwisko
const fullName = [user.first_name, user.last_name].filter(Boolean).join(' ') || '-';
// Saldo
const balance = user.balance !== null
? parseFloat(user.balance).toFixed(2) + ' Playons'
: '-';
// Statystyki meczów
const matches = user.matches_played !== null
? `<div style="font-size:11px;">
<div>🎮 ${user.matches_played || 0} meczów</div>
<div style="color:#28a745;">✓ ${user.matches_won || 0} wygranych</div>
<div style="color:#dc3545;">✗ ${user.matches_lost || 0} przegranych</div>
</div>`
: '-';
const isProtected = isProtectedUser(user);
const actionButtons = isProtected
? '<div class="user-actions"><span class="badge badge-secondary" title="Konto admina lub Twoje konto">🔒 Brak akcji</span><button class="btn-action btn-history" onclick="userHistory(' + user.id + ')" title="Historia">📋</button></div>'
: '<div class="user-actions"><button class="btn-action btn-edit" onclick="editUser(' + user.id + ')" title="Edytuj">✏️</button><button class="btn-action btn-manage" onclick="manageUser(' + user.id + ')" title="Zarządzaj">🔧</button><button class="btn-action btn-history" onclick="userHistory(' + user.id + ')" title="Historia">📋</button><button class="btn-action btn-delete" onclick="deleteUser(' + user.id + ')" title="Usuń">🗑️</button></div>';
tr.innerHTML = `
<td style="${tdFirst}"><strong>${isSuspendedRow ? `<span style="color:#cc0000">#${user.id}</span>` : `#${user.id}`}</strong></td>
<td style="${tdS}"><strong>${isSuspendedRow ? `<span style="color:#cc0000">${escapeHtml(user.username)}</span>` : escapeHtml(user.username)}</strong></td>
<td style="font-size:12px;${tdS}">${escapeHtml(user.email)}</td>
<td style="${tdS}">${escapeHtml(fullName)}</td>
<td style="${tdS}">${roleBadge}</td>
<td style="${tdS}">${verifiedBadge}</td>
<td style="${tdS}"><strong>${balance}</strong></td>
<td style="${tdS}">${matches}</td>
<td style="font-size:12px;${tdS}">${formatDate(user.created_at)}</td>
<td style="${tdS}">${statusBadge}</td>
<td style="${tdLast}">
${actionButtons}
</td>
`;
tbody.appendChild(tr);
});
updateSortHeaders();
}
function updatePagination(pagination) {
document.getElementById('paginationInfo').textContent =
`Pokazuje użytkowników ${((pagination.currentPage - 1) * pagination.recordsPerPage) + 1}-${Math.min(pagination.currentPage * pagination.recordsPerPage, pagination.totalRecords)} z ${pagination.totalRecords}`;
document.getElementById('pageInput').value = pagination.currentPage;
document.getElementById('pageInput').max = pagination.totalPages;
document.getElementById('btnPrev').disabled = !pagination.hasPreviousPage;
document.getElementById('btnFirst').disabled = !pagination.hasPreviousPage;
document.getElementById('btnNext').disabled = !pagination.hasNextPage;
document.getElementById('btnLast').disabled = !pagination.hasNextPage;
}
function updateStats(pagination) {
document.getElementById('totalUsers').textContent = pagination.totalRecords.toLocaleString('pl-PL');
document.getElementById('currentPage').textContent = pagination.currentPage;
document.getElementById('totalPages').textContent = pagination.totalPages;
}
async function nextPage() {
showLoading(true);
try {
currentData = await loader.nextPage();
renderUsers(currentData);
updatePagination(currentData.pagination);
updateStats(currentData.pagination);
scrollToTop();
} catch (error) {
showError('Błąd: ' + error.message);
} finally {
showLoading(false);
}
}
async function previousPage() {
showLoading(true);
try {
currentData = await loader.previousPage();
if (currentData) {
renderUsers(currentData);
updatePagination(currentData.pagination);
updateStats(currentData.pagination);
scrollToTop();
}
} catch (error) {
showError('Błąd: ' + error.message);
} finally {
showLoading(false);
}
}
async function goToFirstPage() {
showLoading(true);
try {
currentData = await loader.goToPage(1);
renderUsers(currentData);
updatePagination(currentData.pagination);
updateStats(currentData.pagination);
scrollToTop();
} catch (error) {
showError('Błąd: ' + error.message);
} finally {
showLoading(false);
}
}
async function goToLastPage() {
if (!currentData) return;
showLoading(true);
try {
currentData = await loader.goToPage(currentData.pagination.totalPages);
renderUsers(currentData);
updatePagination(currentData.pagination);
updateStats(currentData.pagination);
scrollToTop();
} catch (error) {
showError('Błąd: ' + error.message);
} finally {
showLoading(false);
}
}
async function goToPageInput() {
const page = parseInt(document.getElementById('pageInput').value);
if (page < 1 || (currentData && page > currentData.pagination.totalPages)) {
showError('Nieprawidłowy numer strony');
return;
}
showLoading(true);
try {
currentData = await loader.goToPage(page);
renderUsers(currentData);
updatePagination(currentData.pagination);
updateStats(currentData.pagination);
scrollToTop();
} catch (error) {
showError('Błąd: ' + error.message);
} finally {
showLoading(false);
}
}
async function sortBy(column) {
if (currentSort.column === column) {
currentSort.order = currentSort.order === 'ASC' ? 'DESC' : 'ASC';
} else {
currentSort.column = column;
currentSort.order = 'ASC';
}
showLoading(true);
try {
currentData = await loader.sort(currentSort.column, currentSort.order);
renderUsers(currentData);
updatePagination(currentData.pagination);
updateStats(currentData.pagination);
} catch (error) {
showError('Błąd: ' + error.message);
} finally {
showLoading(false);
}
}
function updateSortHeaders() {
document.querySelectorAll('.users-table th.sortable').forEach(th => {
th.classList.remove('sorted-asc', 'sorted-desc');
});
const columnMap = {
'id': 0,
'username': 1,
'email': 2,
'role': 4,
'created_at': 8
};
const columnIndex = columnMap[currentSort.column];
if (columnIndex !== undefined) {
const ths = document.querySelectorAll('.users-table th');
const th = ths[columnIndex];
if (th) {
th.classList.add(`sorted-${currentSort.order.toLowerCase()}`);
}
}
}
async function applyFilters() {
const filters = {};
const username = document.getElementById('filterUsername').value.trim();
if (username) filters.username = username;
const email = document.getElementById('filterEmail').value.trim();
if (email) filters.email = email;
const role = document.getElementById('filterRole').value;
if (role) filters.role = role;
const verified = document.getElementById('filterVerified').value;
if (verified) filters.email_verified = verified;
showLoading(true);
try {
currentData = await loader.filter(filters);
renderUsers(currentData);
updatePagination(currentData.pagination);
updateStats(currentData.pagination);
} catch (error) {
showError('Błąd: ' + error.message);
} finally {
showLoading(false);
}
}
async function clearFilters() {
document.getElementById('filterUsername').value = '';
document.getElementById('filterEmail').value = '';
document.getElementById('filterRole').value = '';
document.getElementById('filterVerified').value = '';
showLoading(true);
try {
currentData = await loader.clearFilters();
renderUsers(currentData);
updatePagination(currentData.pagination);
updateStats(currentData.pagination);
} catch (error) {
showError('Błąd: ' + error.message);
} finally {
showLoading(false);
}
}
// Custom Alert and Confirm Functions
function showAlert(message, title = 'Informacja', type = 'alert') {
return new Promise((resolve) => {
const modal = document.getElementById('customModal');
const header = document.getElementById('modalHeader');
const icon = document.getElementById('modalIcon');
const titleEl = document.getElementById('modalTitle');
const body = document.getElementById('modalBody');
const footer = document.getElementById('modalFooter');
// Set header class and icon
header.className = 'modal-header ' + type;
if (type === 'alert') {
icon.textContent = '';
} else if (type === 'success') {
icon.textContent = '✅';
} else if (type === 'confirm') {
icon.textContent = '⚠️';
}
titleEl.textContent = title;
body.innerHTML = message;
// Create OK button
footer.innerHTML = '<button class="modal-btn modal-btn-primary" id="modalOk">OK</button>';
modal.classList.add('active');
document.getElementById('modalOk').onclick = () => {
modal.classList.remove('active');
resolve(true);
};
// Close on overlay click
modal.onclick = (e) => {
if (e.target === modal) {
modal.classList.remove('active');
resolve(false);
}
};
});
}
function showConfirm(message, title = 'Potwierdzenie') {
return new Promise((resolve) => {
const modal = document.getElementById('customModal');
const header = document.getElementById('modalHeader');
const icon = document.getElementById('modalIcon');
const titleEl = document.getElementById('modalTitle');
const body = document.getElementById('modalBody');
const footer = document.getElementById('modalFooter');
header.className = 'modal-header confirm';
icon.textContent = '❓';
titleEl.textContent = title;
body.innerHTML = message;
// Create buttons
footer.innerHTML = `
<button class="modal-btn modal-btn-secondary" id="modalCancel">Anuluj</button>
<button class="modal-btn modal-btn-danger" id="modalConfirm">Potwierdź</button>
`;
modal.classList.add('active');
document.getElementById('modalConfirm').onclick = () => {
modal.classList.remove('active');
resolve(true);
};
document.getElementById('modalCancel').onclick = () => {
modal.classList.remove('active');
resolve(false);
};
// Close on overlay click
modal.onclick = (e) => {
if (e.target === modal) {
modal.classList.remove('active');
resolve(false);
}
};
});
}
function showSuccess(message, title = 'Sukces') {
return showAlert(message, title, 'success');
}
function showPrompt(message, title = 'Podaj wartość', defaultValue = '') {
return new Promise((resolve) => {
const modal = document.getElementById('customModal');
const header = document.getElementById('modalHeader');
const icon = document.getElementById('modalIcon');
const titleEl = document.getElementById('modalTitle');
const body = document.getElementById('modalBody');
const footer = document.getElementById('modalFooter');
header.className = 'modal-header alert';
icon.textContent = '✏️';
titleEl.textContent = title;
body.innerHTML = `
<p style="margin:0 0 12px;">${message}</p>
<textarea id="promptInput" rows="3"
style="width:100%;padding:10px;border:1px solid #ddd;border-radius:4px;font-size:14px;resize:vertical;box-sizing:border-box;"
>${escapeHtml(defaultValue)}</textarea>
`;
footer.innerHTML = `
<button class="modal-btn modal-btn-secondary" id="promptCancel">Anuluj</button>
<button class="modal-btn modal-btn-primary" id="promptOk">Potwierdź</button>
`;
modal.classList.add('active');
setTimeout(() => {
const inp = document.getElementById('promptInput');
if (inp) { inp.focus(); inp.select(); }
}, 50);
document.getElementById('promptOk').onclick = () => {
const val = document.getElementById('promptInput').value;
modal.classList.remove('active');
resolve(val);
};
document.getElementById('promptCancel').onclick = () => {
modal.classList.remove('active');
resolve(null);
};
modal.onclick = (e) => {
if (e.target === modal) {
modal.classList.remove('active');
resolve(null);
}
};
});
}
// User actions (placeholder)
async function editUser(userId) {
// Pobierz dane użytkownika
const user = currentData.data.find(u => u.id == userId);
if (!user) {
await showAlert('Nie znaleziono użytkownika', 'Błąd', 'alert');
return;
}
if (isProtectedUser(user)) {
await showAlert('Nie można zarządzać kontami administratorów ani własnym kontem w tym widoku.', 'Brak uprawnień', 'alert');
return;
}
// Utwórz formularz edycji
const formHtml = `
<div style="text-align: left;">
<div style="margin-bottom: 15px;">
<label style="display: block; margin-bottom: 5px; font-weight: 600; color: #333;">Username:</label>
<input type="text" id="editUsername" value="${escapeHtml(user.username)}"
style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;">
</div>
<div style="margin-bottom: 15px;">
<label style="display: block; margin-bottom: 5px; font-weight: 600; color: #333;">Email:</label>
<input type="email" id="editEmail" value="${escapeHtml(user.email)}"
style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;">
</div>
<div style="margin-bottom: 15px;">
<label style="display: block; margin-bottom: 5px; font-weight: 600; color: #333;">Imię:</label>
<input type="text" id="editFirstName" value="${escapeHtml(user.first_name || '')}"
style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;">
</div>
<div style="margin-bottom: 15px;">
<label style="display: block; margin-bottom: 5px; font-weight: 600; color: #333;">Nazwisko:</label>
<input type="text" id="editLastName" value="${escapeHtml(user.last_name || '')}"
style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;">
</div>
<div style="margin-bottom: 15px;">
<label style="display: block; margin-bottom: 5px; font-weight: 600; color: #333;">Rola:</label>
<select id="editRole" style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;">
<option value="user" ${user.role === 'user' ? 'selected' : ''}>Player</option>
<option value="moderator" ${user.role === 'moderator' ? 'selected' : ''}>Moderator</option>
<option value="admin" ${user.role === 'admin' ? 'selected' : ''}>Admin</option>
</select>
</div>
<div style="margin-bottom: 15px;">
<label style="display: flex; align-items: center; cursor: pointer; user-select: none;">
<input type="checkbox" id="editEmailVerified" ${user.email_verified == 1 ? 'checked' : ''}
style="width: 18px; height: 18px; margin-right: 8px; cursor: pointer;">
<span style="font-weight: 600; color: #333;">Email zweryfikowany</span>
</label>
</div>
<div style="margin-bottom: 15px;">
<label style="display: flex; align-items: center; cursor: pointer; user-select: none;">
<input type="checkbox" id="editNewsletter" ${user.newsletter_enabled == 1 ? 'checked' : ''}
style="width: 18px; height: 18px; margin-right: 8px; cursor: pointer;">
<span style="font-weight: 600; color: #333;">Newsletter włączony</span>
</label>
</div>
</div>
`;
// Wyświetl modal z formularzem
const modal = document.getElementById('customModal');
const header = document.getElementById('modalHeader');
const icon = document.getElementById('modalIcon');
const titleEl = document.getElementById('modalTitle');
const body = document.getElementById('modalBody');
const footer = document.getElementById('modalFooter');
header.className = 'modal-header alert';
icon.textContent = '✏️';
titleEl.textContent = `Edycja użytkownika #${userId}`;
body.innerHTML = formHtml;
footer.innerHTML = `
<button class="modal-btn modal-btn-secondary" id="modalCancelEdit">Anuluj</button>
<button class="modal-btn modal-btn-primary" id="modalSaveEdit">💾 Zapisz</button>
`;
modal.classList.add('active');
// Obsługa anulowania
document.getElementById('modalCancelEdit').onclick = () => {
modal.classList.remove('active');
};
// Obsługa zapisywania
document.getElementById('modalSaveEdit').onclick = async () => {
const updateData = {
user_id: userId,
username: document.getElementById('editUsername').value.trim(),
email: document.getElementById('editEmail').value.trim(),
first_name: document.getElementById('editFirstName').value.trim(),
last_name: document.getElementById('editLastName').value.trim(),
role: document.getElementById('editRole').value,
email_verified: document.getElementById('editEmailVerified').checked ? 1 : 0,
newsletter_enabled: document.getElementById('editNewsletter').checked ? 1 : 0
};
// Walidacja
if (!updateData.username || updateData.username.length < 3) {
await showAlert('Username musi mieć minimum 3 znaki', 'Błąd walidacji', 'alert');
return;
}
if (!updateData.email || !updateData.email.includes('@')) {
await showAlert('Nieprawidłowy adres email', 'Błąd walidacji', 'alert');
return;
}
modal.classList.remove('active');
showLoading(true);
try {
const response = await fetch('../../api/updateUser.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(updateData)
});
const result = await response.json();
if (!result.success) {
throw new Error(result.error || 'Błąd podczas aktualizacji');
}
await showSuccess('Użytkownik został zaktualizowany pomyślnie!', 'Sukces');
// Odśwież listę użytkowników
await loadUsers();
} catch (error) {
await showAlert('Błąd podczas zapisywania: ' + error.message, 'Błąd', 'alert');
} finally {
showLoading(false);
}
};
// Close on overlay click
modal.onclick = (e) => {
if (e.target === modal) {
modal.classList.remove('active');
}
};
}
async function deleteUser(userId) {
const user = currentData && Array.isArray(currentData.data)
? currentData.data.find(u => u.id == userId)
: null;
if (isProtectedUser(user)) {
await showAlert('Nie można zarządzać kontami administratorów ani własnym kontem w tym widoku.', 'Brak uprawnień', 'alert');
return;
}
const confirmed = await showConfirm(
`Czy na pewno chcesz usunąć użytkownika <strong>#${userId}</strong>?<br><br>⚠️ Konto zostanie oznaczone jako usunięte i nie będzie już widoczne.`,
'🗑️ Usunięcie użytkownika'
);
if (confirmed) {
showLoading(true);
try {
const response = await fetch('../../api/deleteUser.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ user_id: userId })
});
const result = await response.json();
if (!result.success) {
throw new Error(result.error || 'Błąd podczas usuwania');
}
await showSuccess('Użytkownik został pomyślnie usunięty!', 'Sukces');
// Odśwież listę użytkowników
await loadUsers();
} catch (error) {
await showAlert('Błąd podczas usuwania: ' + error.message, 'Błąd', 'alert');
} finally {
showLoading(false);
}
}
}
// Helper functions
function showLoading(show) {
document.getElementById('loadingState').style.display = show ? 'block' : 'none';
document.getElementById('tableWrapper').style.opacity = show ? '0.5' : '1';
}
function showError(message) {
const errorDiv = document.getElementById('errorMessage');
errorDiv.textContent = '⚠️ ' + message;
errorDiv.style.display = 'block';
setTimeout(() => {
errorDiv.style.display = 'none';
}, 5000);
}
function hideError() {
document.getElementById('errorMessage').style.display = 'none';
}
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function formatDate(dateString) {
if (!dateString) return '-';
const date = new Date(dateString);
return date.toLocaleString('pl-PL', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
}
function scrollToTop() {
window.scrollTo({ top: 0, behavior: 'smooth' });
}
async function manageUser(userId) {
const user = currentData && Array.isArray(currentData.data)
? currentData.data.find(u => u.id == userId)
: null;
if (isProtectedUser(user)) {
await showAlert('Nie można zarządzać kontami administratorów ani własnym kontem w tym widoku.', 'Brak uprawnień', 'alert');
return;
}
showLoading(true);
let userData = null;
try {
const resp = await fetch(`../../api/getUserHistory.php?user_id=${userId}`, { credentials: 'same-origin' });
const json = await resp.json();
if (!json.success) throw new Error(json.error || 'Błąd pobierania danych');
userData = json.user;
} catch (error) {
showLoading(false);
await showAlert('Błąd pobierania danych użytkownika: ' + error.message, 'Błąd', 'alert');
return;
} finally {
showLoading(false);
}
const isSuspended = parseInt(userData.account_suspended) === 1;
const suspendedBadge = isSuspended
? '<span style="background:#dc3545;color:#fff;padding:3px 10px;border-radius:4px;font-size:12px;font-weight:700;">⛔ Zawieszone</span>'
: '<span style="background:#28a745;color:#fff;padding:3px 10px;border-radius:4px;font-size:12px;font-weight:700;">✅ Aktywne</span>';
const currentSuspensionInfo = isSuspended ? `
<div style="background:#fff5f5;border:1px solid #f5c6cb;border-radius:6px;padding:12px;margin:12px 0;">
<strong>Powód zawieszenia:</strong><br>
<span style="color:#555;">${escapeHtml(userData.suspension_reason || '-')}</span><br>
<strong>Zawieszone do:</strong> ${userData.suspended_until ? escapeHtml(userData.suspended_until) : 'bezterminowo'}
</div>` : '';
const formHtml = `
<div style="text-align:left;">
<div style="margin-bottom:14px;">Status konta: ${suspendedBadge}</div>
${currentSuspensionInfo}
<hr style="margin:16px 0;border:0;border-top:1px solid #eee;">
<h4 style="margin:0 0 12px;color:#333;">🔒 Zawieś konto</h4>
<div style="margin-bottom:12px;">
<label style="display:block;margin-bottom:5px;font-weight:600;color:#333;">Powód zawieszenia <span style="color:red;">*</span></label>
<textarea id="suspendReason" rows="3" style="width:100%;padding:8px 10px;border:1px solid #ddd;border-radius:4px;font-size:13px;resize:vertical;" placeholder="Opisz powód zawieszenia..."></textarea>
</div>
<div style="margin-bottom:12px;">
<label style="display:block;margin-bottom:5px;font-weight:600;color:#333;">Czas zawieszenia</label>
<select id="suspendDuration" style="width:100%;padding:8px 10px;border:1px solid #ddd;border-radius:4px;font-size:13px;">
<option value="1h">1 godzina</option>
<option value="6h">6 godzin</option>
<option value="24h">24 godziny</option>
<option value="3d">3 dni</option>
<option value="7d">7 dni</option>
<option value="30d">30 dni</option>
<option value="permanent" selected>Bezterminowo</option>
</select>
</div>
</div>
`;
const modal = document.getElementById('customModal');
const header = document.getElementById('modalHeader');
const icon = document.getElementById('modalIcon');
const titleEl = document.getElementById('modalTitle');
const body = document.getElementById('modalBody');
const footer = document.getElementById('modalFooter');
header.className = 'modal-header confirm';
icon.textContent = '🔧';
titleEl.textContent = `Zarządzaj użytkownikiem #${userId} (${userData.username})`;
body.innerHTML = formHtml;
let footerHtml = `<button class="modal-btn modal-btn-secondary" id="manageModalClose">Anuluj</button>
<button class="modal-btn modal-btn-danger" id="manageModalSuspend">⛔ Zawieś konto</button>`;
if (isSuspended) {
footerHtml += `<button class="modal-btn modal-btn-primary" id="manageModalUnsuspend" style="background:#28a745;">✅ Odwieś konto</button>`;
}
footer.innerHTML = footerHtml;
modal.classList.add('active');
document.getElementById('manageModalClose').onclick = () => modal.classList.remove('active');
modal.onclick = (e) => { if (e.target === modal) modal.classList.remove('active'); };
document.getElementById('manageModalSuspend').onclick = async () => {
const reason = (document.getElementById('suspendReason').value || '').trim();
if (!reason) {
await showAlert('Powód zawieszenia jest wymagany.', 'Brak powodu', 'alert');
return;
}
const durVal = document.getElementById('suspendDuration').value;
let suspendedUntil = 'permanent';
if (durVal !== 'permanent') {
const now = new Date();
const map = { '1h': 3600, '6h': 21600, '24h': 86400, '3d': 259200, '7d': 604800, '30d': 2592000 };
const secs = map[durVal] || 0;
if (secs > 0) {
const d = new Date(now.getTime() + secs * 1000);
const pad = n => String(n).padStart(2, '0');
suspendedUntil = `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
}
}
modal.classList.remove('active');
showLoading(true);
try {
const res = await fetch('../../api/suspendUser.php', {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'suspend', user_id: userId, reason, suspended_until: suspendedUntil })
});
const result = await res.json();
if (!result.success) throw new Error(result.error || 'Błąd zawieszenia');
await showSuccess(result.message, 'Sukces');
await loadUsers();
} catch (error) {
await showAlert('Błąd: ' + error.message, 'Błąd', 'alert');
} finally {
showLoading(false);
}
};
if (isSuspended) {
document.getElementById('manageModalUnsuspend').onclick = async () => {
modal.classList.remove('active');
const reasonInput = await showPrompt(
'Podaj powód odwieszenia (opcjonalnie):',
'✅ Odwieszenie konta użytkownika #' + userId,
'Decyzja administracyjna po weryfikacji sytuacji.'
);
if (reasonInput === null) return;
const reason = (reasonInput || '').trim();
showLoading(true);
try {
const res = await fetch('../../api/suspendUser.php', {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'unsuspend', user_id: userId, reason })
});
const result = await res.json();
if (!result.success) throw new Error(result.error || 'Błąd odwieszenia');
await showSuccess(result.message, 'Sukces');
await loadUsers();
} catch (error) {
await showAlert('Błąd: ' + error.message, 'Błąd', 'alert');
} finally {
showLoading(false);
}
};
}
}
async function userHistory(userId) {
showLoading(true);
let userData = null;
let history = [];
try {
const resp = await fetch(`../../api/getUserHistory.php?user_id=${userId}`, { credentials: 'same-origin' });
const json = await resp.json();
if (!json.success) throw new Error(json.error || 'Błąd pobierania danych');
userData = json.user;
history = json.history || [];
} catch (error) {
showLoading(false);
await showAlert('Błąd pobierania historii: ' + error.message, 'Błąd', 'alert');
return;
} finally {
showLoading(false);
}
const isSuspended = parseInt(userData.account_suspended) === 1;
const suspBadge = isSuspended
? '<span style="background:#dc3545;color:#fff;padding:2px 8px;border-radius:4px;font-size:11px;">⛔ Zawieszone</span>'
: '<span style="background:#28a745;color:#fff;padding:2px 8px;border-radius:4px;font-size:11px;">✅ Aktywne</span>';
let historyRows = '';
if (history.length === 0) {
historyRows = '<tr><td colspan="5" style="text-align:center;padding:20px;color:#888;">Brak historii konta</td></tr>';
} else {
history.forEach(h => {
const actionBadge = h.action === 'suspend'
? '<span style="background:#dc3545;color:#fff;padding:2px 7px;border-radius:3px;font-size:11px;">suspend</span>'
: '<span style="background:#28a745;color:#fff;padding:2px 7px;border-radius:3px;font-size:11px;">unsuspend</span>';
historyRows += `<tr>
<td style="padding:8px;font-size:12px;border-bottom:1px solid #eee;">${escapeHtml(h.created_at || '-')}</td>
<td style="padding:8px;border-bottom:1px solid #eee;">${actionBadge}</td>
<td style="padding:8px;font-size:12px;border-bottom:1px solid #eee;">${escapeHtml(h.reason || '-')}</td>
<td style="padding:8px;font-size:12px;border-bottom:1px solid #eee;">${escapeHtml(h.suspended_until || 'bezterminowo')}</td>
<td style="padding:8px;font-size:12px;border-bottom:1px solid #eee;">${escapeHtml(h.performed_by_username || '-')}</td>
</tr>`;
});
}
const bodyHtml = `
<div style="text-align:left;">
<div style="background:#f8f9fa;border-radius:6px;padding:14px;margin-bottom:16px;">
<table style="width:100%;font-size:13px;border-collapse:collapse;">
<tr><td style="padding:4px 8px;font-weight:600;width:140px;">ID:</td><td>${escapeHtml(String(userData.id))}</td></tr>
<tr><td style="padding:4px 8px;font-weight:600;">Username:</td><td><strong>${escapeHtml(userData.username)}</strong></td></tr>
<tr><td style="padding:4px 8px;font-weight:600;">Email:</td><td>${escapeHtml(userData.email)}</td></tr>
<tr><td style="padding:4px 8px;font-weight:600;">Imię i nazwisko:</td><td>${escapeHtml(((userData.first_name || '') + ' ' + (userData.last_name || '')).trim() || '-')}</td></tr>
<tr><td style="padding:4px 8px;font-weight:600;">Rola:</td><td>${escapeHtml(userData.role || '-')}</td></tr>
<tr><td style="padding:4px 8px;font-weight:600;">Zweryfikowany:</td><td>${userData.email_verified == 1 ? '✅ Tak' : '⚠️ Nie'}</td></tr>
<tr><td style="padding:4px 8px;font-weight:600;">Saldo:</td><td>${parseFloat(userData.balance || 0).toFixed(2)} Playons</td></tr>
<tr><td style="padding:4px 8px;font-weight:600;">Mecze:</td><td>${userData.matches_played || 0} (W: ${userData.matches_won || 0} / L: ${userData.matches_lost || 0})</td></tr>
<tr><td style="padding:4px 8px;font-weight:600;">Zarejestrowany:</td><td>${formatDate(userData.created_at)}</td></tr>
<tr><td style="padding:4px 8px;font-weight:600;">Status konta:</td><td>${suspBadge}</td></tr>
${isSuspended ? `<tr><td style="padding:4px 8px;font-weight:600;">Powód:</td><td>${escapeHtml(userData.suspension_reason || '-')}</td></tr>
<tr><td style="padding:4px 8px;font-weight:600;">Zawieszone do:</td><td>${escapeHtml(userData.suspended_until || 'bezterminowo')}</td></tr>` : ''}
</table>
</div>
<h4 style="margin:0 0 10px;color:#333;">📋 Historia konta</h4>
<div style="overflow-x:auto;border:1px solid #eee;border-radius:6px;">
<table style="width:100%;border-collapse:collapse;min-width:500px;">
<thead><tr style="background:#f8f9fa;">
<th style="padding:8px;text-align:left;font-size:12px;border-bottom:2px solid #dee2e6;">Data</th>
<th style="padding:8px;text-align:left;font-size:12px;border-bottom:2px solid #dee2e6;">Akcja</th>
<th style="padding:8px;text-align:left;font-size:12px;border-bottom:2px solid #dee2e6;">Powód</th>
<th style="padding:8px;text-align:left;font-size:12px;border-bottom:2px solid #dee2e6;">Zawieszone do</th>
<th style="padding:8px;text-align:left;font-size:12px;border-bottom:2px solid #dee2e6;">Wykonał</th>
</tr></thead>
<tbody>${historyRows}</tbody>
</table>
</div>
</div>
`;
const modal = document.getElementById('customModal');
const header = document.getElementById('modalHeader');
const icon = document.getElementById('modalIcon');
const titleEl = document.getElementById('modalTitle');
const body = document.getElementById('modalBody');
const footer = document.getElementById('modalFooter');
header.className = 'modal-header alert';
icon.textContent = '📋';
titleEl.textContent = `Historia użytkownika #${userId}`;
body.innerHTML = bodyHtml;
footer.innerHTML = `<button class="modal-btn modal-btn-secondary" id="historyModalClose">Zamknij</button>`;
modal.classList.add('active');
document.getElementById('historyModalClose').onclick = () => modal.classList.remove('active');
modal.onclick = (e) => { if (e.target === modal) modal.classList.remove('active'); };
}
// Enter key handlers
document.addEventListener('DOMContentLoaded', () => {
['filterUsername', 'filterEmail'].forEach(id => {
document.getElementById(id)?.addEventListener('keypress', (e) => {
if (e.key === 'Enter') applyFilters();
});
});
document.getElementById('pageInput')?.addEventListener('keypress', (e) => {
if (e.key === 'Enter') goToPageInput();
});
});
</script>
<?php
require_once __DIR__ . '/../includes/footer.php';
?>