feat(admin): pagination для users-таблицы (50/стр)

- adminGetUsers возвращает { users, total, page, limit }

- pagination-controls (← 1 … N →) с ellipsis для длинных списков

- shop/gam search callers адаптированы под новый формат ответа

- helper _renderPgnControls переиспользуем для sessions/shop

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-05-16 20:24:18 +03:00
parent bcee5a57e3
commit bd7a9dbee2
3 changed files with 66 additions and 6 deletions
+1
View File
@@ -1117,6 +1117,7 @@
<tbody id="users-body"><tr><td colspan="7"><div class="spinner"></div></td></tr></tbody>
</table>
</div>
<div id="users-pagination" class="pgn-bar" style="display:none"></div>
<div class="user-panel" id="user-panel">
<div class="user-panel-header">
<div><div class="user-panel-name" id="up-name"></div><div class="user-panel-email" id="up-email"></div></div>
+63 -3
View File
@@ -653,11 +653,63 @@
/* ════════════════════════════════════════════════
ПОЛЬЗОВАТЕЛИ
════════════════════════════════════════════════ */
async function loadUsers() {
let _usersPage = 1;
const _USERS_PER_PAGE = 50;
function _ensurePgnStyles() {
if (document.getElementById('pgn-bar-style')) return;
const s = document.createElement('style');
s.id = 'pgn-bar-style';
s.textContent = `
.pgn-bar { display:flex; align-items:center; justify-content:space-between; gap:10px; padding:14px 4px 4px; font-size:0.85rem; color:var(--text-3); }
.pgn-info { font-weight:600; }
.pgn-ctrls { display:flex; align-items:center; gap:4px; }
.pgn-btn { min-width:32px; height:32px; padding:0 10px; border:1px solid var(--border); background:var(--surface); border-radius:8px; cursor:pointer; font-weight:600; font-family:inherit; font-size:0.85rem; color:var(--text-2); transition:background .12s, color .12s, border-color .12s; }
.pgn-btn:hover:not(:disabled) { background:rgba(155,93,229,.08); color:var(--violet); border-color:rgba(155,93,229,.3); }
.pgn-btn.active { background:var(--violet); color:#fff; border-color:var(--violet); }
.pgn-btn:disabled { opacity:.4; cursor:not-allowed; }
.pgn-ellip { padding:0 6px; color:var(--text-3); }
`;
document.head.appendChild(s);
}
function _renderPgnControls(elId, page, total, perPage, gotoFn) {
const bar = document.getElementById(elId);
if (!bar) return;
const pages = Math.max(1, Math.ceil(total / perPage));
if (pages <= 1) { bar.style.display = 'none'; return; }
_ensurePgnStyles();
const from = (page - 1) * perPage + 1;
const to = Math.min(page * perPage, total);
// page buttons with ellipsis: first, current±2, last
const nums = new Set([1, pages, page, page - 1, page + 1, page - 2, page + 2]);
const sorted = [...nums].filter(n => n >= 1 && n <= pages).sort((a, b) => a - b);
const numHtml = sorted.map((n, i) => {
const prev = sorted[i - 1];
const gap = prev && n - prev > 1 ? '<span class="pgn-ellip">…</span>' : '';
return `${gap}<button class="pgn-btn${n === page ? ' active' : ''}" onclick="${gotoFn}(${n})">${n}</button>`;
}).join('');
bar.innerHTML = `
<div class="pgn-info">${from}${to} из ${total}</div>
<div class="pgn-ctrls">
<button class="pgn-btn" onclick="${gotoFn}(${page - 1})" ${page <= 1 ? 'disabled' : ''}>←</button>
${numHtml}
<button class="pgn-btn" onclick="${gotoFn}(${page + 1})" ${page >= pages ? 'disabled' : ''}>→</button>
</div>`;
bar.style.display = '';
}
async function loadUsers(page) {
if (page) _usersPage = page;
try {
const users = await LS.adminGetUsers();
const r = await LS.adminGetUsers({ page: _usersPage, limit: _USERS_PER_PAGE });
const users = r.users || [];
const tbody = document.getElementById('users-body');
if (!users.length) { tbody.innerHTML = '<tr><td colspan="7"><div class="empty">Пользователей нет</div></td></tr>'; return; }
if (!users.length) {
tbody.innerHTML = '<tr><td colspan="7"><div class="empty">Пользователей нет</div></td></tr>';
document.getElementById('users-pagination').style.display = 'none';
return;
}
tbody.innerHTML = users.map(u => {
const pc = pctClass(u.avg_pct);
const initials = (u.name||'?').split(' ').slice(0,2).map(w=>w[0]?.toUpperCase()||'').join('')||'?';
@@ -691,12 +743,20 @@
<td style="text-align:right;color:var(--text-3);font-size:0.85rem;opacity:0.4"></td>
</tr>`;
}).join('');
_renderPgnControls('users-pagination', _usersPage, r.total || users.length, _USERS_PER_PAGE, 'gotoUsersPage');
} catch (e) {
document.getElementById('users-body').innerHTML = `<tr><td colspan="7"></td></tr>`;
LS.state.error(document.getElementById('users-body').querySelector('td'), e, loadUsers);
}
}
function gotoUsersPage(n) {
_usersPage = n;
loadUsers();
document.getElementById('tab-users')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
window.gotoUsersPage = gotoUsersPage;
async function changeRole(select) {
select.disabled = true;
try { await LS.adminUpdateRole(select.dataset.uid, select.value); LS.toast('Роль изменена', 'success', 2000); }