From bd7a9dbee250eaae4e85d47dd549e8da62907a7f Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Sat, 16 May 2026 20:24:18 +0300 Subject: [PATCH] =?UTF-8?q?feat(admin):=20pagination=20=D0=B4=D0=BB=D1=8F?= =?UTF-8?q?=20users-=D1=82=D0=B0=D0=B1=D0=BB=D0=B8=D1=86=D1=8B=20(50/?= =?UTF-8?q?=D1=81=D1=82=D1=80)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- frontend/admin.html | 1 + frontend/js/admin/admin.js | 66 ++++++++++++++++++++++++++++++++++++-- js/api.js | 5 ++- 3 files changed, 66 insertions(+), 6 deletions(-) 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`); }