feat(admin): Phase 6 sub-commit 2 — remove .user-panel overlay
Now that the deep pages (sub-commit 1) work, retire the legacy
.user-panel inline overlay entirely.
* admin.html: removed <div class="user-panel" id="user-panel"> block
inside #tab-users, removed dead .user-panel* CSS (kept .btn-close
for any external use).
* users.js: removed openUserPanel / closeUserPanel / reloadUserPanel
and their closure state (activeTr, activeUserRole). User row onclick
switched from openUserPanel(...) → AdminRouter.navigate('#users/N').
clearUserHistory / toggleBanUser / confirmDeleteUser / openEditUserModal
/ openUserPermsModal / doSet/doReset* all refactored to use the
getActiveUid() helper (reads window.activeUid, set by user-detail.init)
+ reloadDetailAndList() helper (refreshes deep page + list together).
* sessions.js: row click + eye-button switched from toggleDrawer(id)
→ gotoSession(id) → AdminRouter.navigate('#sessions/N'). Removed
toggleDrawer + renderDrawer functions (~60L) and openDrawerId state.
Inline drawer markup removed from the row template.
Verified node --check on all touched JS. ast-index confirms zero
remaining usages of openUserPanel / closeUserPanel / reloadUserPanel /
toggleDrawer across the repo.
This completes Phase 6 and the admin-redesign feature.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -40,12 +40,21 @@
|
||||
eye: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/></svg>',
|
||||
};
|
||||
|
||||
// user-panel + edit modal + perms modal state
|
||||
let activeTr = null;
|
||||
let activeUid = null;
|
||||
let activeUserRole = null;
|
||||
/* User-related modal state.
|
||||
* After Phase 6 the .user-panel overlay is gone — instead the modals
|
||||
* (edit, perms) operate on window.activeUid which is set by user-detail.js
|
||||
* when the deep page opens, or transiently by row actions on the list. */
|
||||
let _editUid = null;
|
||||
let _upPermsData = null;
|
||||
// Helper: read the currently-active user id (set by user-detail.js or quick actions).
|
||||
const getActiveUid = () => window.activeUid || null;
|
||||
// Helper: after a mutation that may affect the active user, refresh the deep page
|
||||
// (if it's currently showing the same user) AND the list.
|
||||
function reloadDetailAndList() {
|
||||
const sec = (window.AdminSections || {})['user-detail'];
|
||||
if (sec && typeof sec.reload === 'function') sec.reload();
|
||||
load();
|
||||
}
|
||||
|
||||
async function load(page) {
|
||||
const { pctClass, fmtDate, renderPgnControls } = AdminCtx;
|
||||
@@ -74,7 +83,7 @@
|
||||
<option value="admin" ${u.role==='admin' ?'selected':''}>Админ</option>
|
||||
</select>`
|
||||
: `<span class="role-badge ${u.role}">${{student:'Ученик',free_student:'Своб. ученик',teacher:'Учитель',admin:'Админ'}[u.role]||u.role}</span>`;
|
||||
return `<tr class="clickable${u.is_banned ? ' banned-row' : ''}" onclick="openUserPanel(event,${u.id},'${u.role}')">
|
||||
return `<tr class="clickable${u.is_banned ? ' banned-row' : ''}" onclick="AdminRouter.navigate('#users/${u.id}')">
|
||||
<td>
|
||||
<div style="display:flex;align-items:center;gap:12px">
|
||||
<div style="width:36px;height:36px;border-radius:10px;background:${avatarBg};display:flex;align-items:center;justify-content:center;font-family:'Unbounded',sans-serif;font-size:0.62rem;font-weight:800;color:#fff;flex-shrink:0;${u.is_banned?'filter:grayscale(1);opacity:.5':''}">${initials}</div>
|
||||
@@ -133,7 +142,10 @@
|
||||
await LS.adminBanUser(uid, !isBanned);
|
||||
LS.toast(isBanned ? 'Пользователь разблокирован' : 'Пользователь заблокирован', isBanned ? 'success' : 'warning');
|
||||
await load();
|
||||
if (activeUid === uid) await reloadUserPanel(uid);
|
||||
if (getActiveUid() === uid) {
|
||||
const sec = (window.AdminSections || {})['user-detail'];
|
||||
if (sec && typeof sec.reload === 'function') sec.reload();
|
||||
}
|
||||
} catch (e) {
|
||||
LS.toast('Ошибка: ' + e.message, 'error');
|
||||
btn.disabled = false;
|
||||
@@ -193,7 +205,10 @@
|
||||
try {
|
||||
await LS.adminDeleteUser(uid);
|
||||
LS.toast('Пользователь удалён', 'success');
|
||||
if (activeUid === uid) closeUserPanel();
|
||||
// If the deleted user is currently open as a deep page, go back to the list.
|
||||
if (getActiveUid() === uid && window.AdminRouter) {
|
||||
AdminRouter.navigate('#users');
|
||||
}
|
||||
await load();
|
||||
} catch (e) {
|
||||
LS.toast('Ошибка: ' + e.message, 'error');
|
||||
@@ -214,105 +229,53 @@
|
||||
finally { select.disabled = false; }
|
||||
}
|
||||
|
||||
/* ─── User panel ─── */
|
||||
async function openUserPanel(e, uid, role) {
|
||||
const isAdmin = AdminCtx.isAdmin;
|
||||
if (activeTr) activeTr.classList.remove('selected');
|
||||
activeTr = e.currentTarget; activeTr.classList.add('selected');
|
||||
activeUid = uid;
|
||||
activeUserRole = role;
|
||||
const panel = document.getElementById('user-panel');
|
||||
panel.classList.add('visible');
|
||||
panel.scrollIntoView({ behavior:'smooth', block:'nearest' });
|
||||
document.getElementById('up-sessions').innerHTML = LS.skeleton(3, 'row');
|
||||
document.getElementById('up-name').textContent = '…';
|
||||
document.getElementById('up-email').textContent = '';
|
||||
if (isAdmin) {
|
||||
document.getElementById('up-edit-btn').style.display = '';
|
||||
document.getElementById('up-clear-btn').style.display = '';
|
||||
document.getElementById('up-perms-btn').style.display = role === 'teacher' ? '' : 'none';
|
||||
document.getElementById('up-ban-btn').style.display = '';
|
||||
document.getElementById('up-delete-btn').style.display = '';
|
||||
}
|
||||
await reloadUserPanel(uid);
|
||||
}
|
||||
|
||||
async function reloadUserPanel(uid) {
|
||||
const { MODES, pctClass, fmtDate } = AdminCtx;
|
||||
const isAdmin = AdminCtx.isAdmin;
|
||||
try {
|
||||
const { user: u, sessions } = await LS.adminGetUserSessions(uid);
|
||||
activeUserRole = u.role;
|
||||
document.getElementById('up-name').innerHTML = LS.esc(u.name) + (u.is_banned ? ' <svg class="ic" viewBox="0 0 24 24"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>' : '');
|
||||
document.getElementById('up-email').textContent = u.email;
|
||||
if (isAdmin) {
|
||||
document.getElementById('up-perms-btn').style.display = u.role === 'teacher' ? '' : 'none';
|
||||
const banBtn = document.getElementById('up-ban-btn');
|
||||
const banLbl = document.getElementById('up-ban-label');
|
||||
if (u.is_banned) {
|
||||
banBtn.style.background = 'rgba(34,197,94,.12)';
|
||||
banBtn.style.color = '#22C55E';
|
||||
banBtn.style.borderColor = 'rgba(34,197,94,.25)';
|
||||
banLbl.textContent = 'Разблокировать';
|
||||
} else {
|
||||
banBtn.style.background = '';
|
||||
banBtn.style.color = '';
|
||||
banBtn.style.borderColor = '';
|
||||
banLbl.textContent = 'Заблокировать';
|
||||
}
|
||||
}
|
||||
const el = document.getElementById('up-sessions');
|
||||
if (!sessions.length) { el.innerHTML = '<div class="empty">Тестов нет</div>'; return; }
|
||||
el.innerHTML = '<div class="sess-list">' + sessions.map(s => {
|
||||
const pct = s.score !== null ? Math.round((s.score/s.total)*100) : null;
|
||||
return `<div class="sess-item">
|
||||
<div class="sess-pct ${pctClass(pct)}">${pct !== null ? pct+'%' : '—'}</div>
|
||||
<div class="sess-info"><div class="sess-subj">${s.subject_name||'Тест'}</div><div class="sess-meta">${fmtDate(s.started_at)} · ${MODES[s.mode]||s.mode}</div></div>
|
||||
<div class="sess-score">${s.score??'—'} / ${s.total}</div>
|
||||
</div>`;
|
||||
}).join('') + '</div>';
|
||||
} catch (e) { LS.state.error(document.getElementById('up-sessions'), e); }
|
||||
}
|
||||
|
||||
function closeUserPanel() {
|
||||
document.getElementById('user-panel').classList.remove('visible');
|
||||
if (activeTr) { activeTr.classList.remove('selected'); activeTr = null; }
|
||||
activeUid = null;
|
||||
/* ─── User actions (called from the user-detail deep page header buttons) ───
|
||||
* Pre-Phase 6 these talked to the .user-panel overlay; now they:
|
||||
* - read the active uid via getActiveUid() (set by user-detail.init)
|
||||
* - read display name from the #up-name span rendered inside the deep page
|
||||
* - reload via AdminSections['user-detail'].reload() */
|
||||
function _activeName() {
|
||||
const el = document.getElementById('up-name');
|
||||
return el ? el.textContent.trim() : '';
|
||||
}
|
||||
|
||||
async function clearUserHistory() {
|
||||
const name = document.getElementById('up-name').textContent;
|
||||
const uid = getActiveUid();
|
||||
if (!uid) return;
|
||||
const name = _activeName();
|
||||
if (!await LS.confirm(`Удалить всю историю тестов пользователя «${name}»?\nЭто действие нельзя отменить.`, { title: 'Очистить историю', confirmText: 'Удалить историю' })) return;
|
||||
try {
|
||||
await LS.adminClearUserSessions(activeUid);
|
||||
await reloadUserPanel(activeUid);
|
||||
load();
|
||||
await LS.adminClearUserSessions(uid);
|
||||
reloadDetailAndList();
|
||||
} catch (e) { LS.toast('Ошибка очистки истории: ' + e.message, 'error'); }
|
||||
}
|
||||
|
||||
async function toggleBanUser() {
|
||||
const uid = getActiveUid();
|
||||
if (!uid) return;
|
||||
const banLbl = document.getElementById('up-ban-label');
|
||||
const isBanning = banLbl.textContent === 'Заблокировать';
|
||||
const name = document.getElementById('up-name').innerHTML.replace(' <svg class="ic" viewBox="0 0 24 24"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>','');
|
||||
const isBanning = banLbl ? banLbl.textContent.trim() === 'Заблокировать' : true;
|
||||
const name = _activeName();
|
||||
const msg = isBanning
|
||||
? `Заблокировать пользователя «${name}»?\nОн не сможет войти в систему.`
|
||||
: `Разблокировать пользователя «${name}»?`;
|
||||
if (!await LS.confirm(msg, { title: isBanning ? 'Блокировка' : 'Разблокировка', confirmText: isBanning ? 'Заблокировать' : 'Разблокировать' })) return;
|
||||
try {
|
||||
await LS.adminBanUser(activeUid, isBanning);
|
||||
await LS.adminBanUser(uid, isBanning);
|
||||
LS.toast(isBanning ? 'Пользователь заблокирован' : 'Пользователь разблокирован', isBanning ? 'warning' : 'success');
|
||||
await reloadUserPanel(activeUid);
|
||||
load();
|
||||
reloadDetailAndList();
|
||||
} catch (e) { LS.toast('Ошибка: ' + e.message, 'error'); }
|
||||
}
|
||||
|
||||
async function confirmDeleteUser() {
|
||||
const name = document.getElementById('up-name').innerHTML.replace(' <svg class="ic" viewBox="0 0 24 24"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>','');
|
||||
const uid = getActiveUid();
|
||||
if (!uid) return;
|
||||
const name = _activeName();
|
||||
if (!await LS.confirm(`Удалить пользователя «${name}» навсегда?\nВсе его данные, тесты и прогресс будут удалены. Это действие нельзя отменить.`, { title: 'Удалить пользователя', confirmText: 'Удалить навсегда' })) return;
|
||||
try {
|
||||
await LS.adminDeleteUser(activeUid);
|
||||
await LS.adminDeleteUser(uid);
|
||||
LS.toast('Пользователь удалён', 'success');
|
||||
closeUserPanel();
|
||||
if (window.AdminRouter) AdminRouter.navigate('#users');
|
||||
load();
|
||||
} catch (e) { LS.toast('Ошибка: ' + e.message, 'error'); }
|
||||
}
|
||||
@@ -324,7 +287,8 @@
|
||||
}
|
||||
|
||||
function openEditUserModal() {
|
||||
_editUid = activeUid;
|
||||
_editUid = getActiveUid();
|
||||
if (!_editUid) return;
|
||||
document.getElementById('eu-name').value = document.getElementById('up-name').textContent;
|
||||
document.getElementById('eu-email').value = document.getElementById('up-email').textContent;
|
||||
document.getElementById('eu-password').value = '';
|
||||
@@ -349,8 +313,7 @@
|
||||
try {
|
||||
await LS.adminUpdateUser(_editUid, payload);
|
||||
closeEditUserModal();
|
||||
await reloadUserPanel(activeUid);
|
||||
load();
|
||||
reloadDetailAndList();
|
||||
} catch (e) {
|
||||
errEl.textContent = 'Ошибка: ' + e.message;
|
||||
} finally {
|
||||
@@ -365,13 +328,14 @@
|
||||
}
|
||||
|
||||
async function openUserPermsModal() {
|
||||
if (!activeUid) return;
|
||||
const name = document.getElementById('up-name').textContent;
|
||||
const uid = getActiveUid();
|
||||
if (!uid) return;
|
||||
const name = _activeName();
|
||||
document.getElementById('up-modal-title').textContent = `Права: ${name}`;
|
||||
document.getElementById('up-modal-list').innerHTML = LS.skeleton(5, 'row');
|
||||
document.getElementById('up-modal').classList.add('open');
|
||||
try {
|
||||
_upPermsData = await LS.getUserPermissions(activeUid);
|
||||
_upPermsData = await LS.getUserPermissions(uid);
|
||||
renderUserPerms();
|
||||
} catch(e) {
|
||||
document.getElementById('up-modal-list').innerHTML = `<p style="color:var(--danger);font-size:13px">Ошибка: ${esc(e.message)}</p>`;
|
||||
@@ -415,10 +379,12 @@
|
||||
}
|
||||
|
||||
async function doSetUserPerm(key, enabled, checkbox) {
|
||||
const uid = getActiveUid();
|
||||
if (!uid) return;
|
||||
checkbox.disabled = true;
|
||||
try {
|
||||
await LS.setUserPermission(activeUid, key, enabled);
|
||||
_upPermsData = await LS.getUserPermissions(activeUid);
|
||||
await LS.setUserPermission(uid, key, enabled);
|
||||
_upPermsData = await LS.getUserPermissions(uid);
|
||||
renderUserPerms();
|
||||
LS.toast(enabled ? 'Право включено' : 'Право отключено', 'success');
|
||||
} catch(e) {
|
||||
@@ -430,20 +396,24 @@
|
||||
}
|
||||
|
||||
async function doResetOneUserPerm(key) {
|
||||
const uid = getActiveUid();
|
||||
if (!uid) return;
|
||||
try {
|
||||
await LS.resetUserPermissions(activeUid, key);
|
||||
_upPermsData = await LS.getUserPermissions(activeUid);
|
||||
await LS.resetUserPermissions(uid, key);
|
||||
_upPermsData = await LS.getUserPermissions(uid);
|
||||
renderUserPerms();
|
||||
LS.toast('Сброшено к значению роли', 'success');
|
||||
} catch(e) { LS.toast('Ошибка: ' + e.message, 'error'); }
|
||||
}
|
||||
|
||||
async function doResetAllUserPerms() {
|
||||
const name = document.getElementById('up-name').textContent;
|
||||
const uid = getActiveUid();
|
||||
if (!uid) return;
|
||||
const name = _activeName();
|
||||
if (!await LS.confirm(`Сбросить все индивидуальные права «${name}»?\nБудут применены права роли.`, { title: 'Сбросить права', confirmText: 'Сбросить' })) return;
|
||||
try {
|
||||
await LS.resetUserPermissions(activeUid);
|
||||
_upPermsData = await LS.getUserPermissions(activeUid);
|
||||
await LS.resetUserPermissions(uid);
|
||||
_upPermsData = await LS.getUserPermissions(uid);
|
||||
renderUserPerms();
|
||||
LS.toast('Права сброшены к роли', 'success');
|
||||
} catch(e) { LS.toast('Ошибка: ' + e.message, 'error'); }
|
||||
@@ -453,9 +423,6 @@
|
||||
window.loadUsers = load;
|
||||
window.gotoUsersPage = gotoUsersPage;
|
||||
window.changeRole = changeRole;
|
||||
window.openUserPanel = openUserPanel;
|
||||
window.reloadUserPanel = reloadUserPanel;
|
||||
window.closeUserPanel = closeUserPanel;
|
||||
window.clearUserHistory = clearUserHistory;
|
||||
window.toggleBanUser = toggleBanUser;
|
||||
window.confirmDeleteUser = confirmDeleteUser;
|
||||
|
||||
Reference in New Issue
Block a user