diff --git a/frontend/admin.html b/frontend/admin.html index 24ee3fb..816e69b 100644 --- a/frontend/admin.html +++ b/frontend/admin.html @@ -1117,6 +1117,7 @@
+
diff --git a/frontend/js/admin/admin.js b/frontend/js/admin/admin.js index 0480983..5b68719 100644 --- a/frontend/js/admin/admin.js +++ b/frontend/js/admin/admin.js @@ -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 ? '' : ''; + return `${gap}`; + }).join(''); + bar.innerHTML = ` +
${from}–${to} из ${total}
+
+ + ${numHtml} + +
`; + 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 = '
Пользователей нет
'; return; } + if (!users.length) { + tbody.innerHTML = '
Пользователей нет
'; + 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 @@ › `; }).join(''); + _renderPgnControls('users-pagination', _usersPage, r.total || users.length, _USERS_PER_PAGE, 'gotoUsersPage'); } catch (e) { document.getElementById('users-body').innerHTML = ``; 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); } diff --git a/js/api.js b/js/api.js index ae5623a..ba5d825 100644 --- a/js/api.js +++ b/js/api.js @@ -156,9 +156,8 @@ async function adminGetUsers(params = {}) { if (params.limit) p.set('limit', params.limit); if (params.role) p.set('role', params.role); if (params.q) p.set('q', params.q); - const data = await req('GET', `/admin/users?${p}`); - // API returns { users, total, page, limit } — extract users for compat - return Array.isArray(data) ? data : data.users; + // Returns { users, total, page, limit } (or { users, nextCursor, limit } if cursor used) + return req('GET', `/admin/users?${p}`); } async function adminUpdateRole(id, role) { return req('PATCH', `/admin/users/${id}/role`, { role }); } async function adminGetUserSessions(id) { return req('GET', `/admin/users/${id}/sessions`); }