'use strict'; /* admin → sessions section: sessions timeline + drawer detail */ (function () { 'use strict'; let inited = false; let allSessions = []; let openDrawerId = null; /* SVG icons (Lucide-style) — kept local to mirror users.js without coupling */ const SESS_ICONS = { eye: '', trash: '', }; /* Inject .row-actions / .row-action-btn styles only if users.js hasn't (sessions can render first). */ function ensureRowActionsStyles() { if (document.getElementById('row-actions-style')) return; const s = document.createElement('style'); s.id = 'row-actions-style'; s.textContent = ` .row-actions { opacity: 0; transition: opacity .15s ease; display: inline-flex; gap: 4px; vertical-align: middle; } tr:hover .row-actions, .sess-tl-item:hover .row-actions { opacity: 1; } tr.selected .row-actions, .sess-tl-item.open .row-actions { opacity: 1; } .row-action-btn { width: 28px; height: 28px; border-radius: 6px; border: 1px solid var(--border); background: transparent; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; color: var(--text-3); transition: background .12s ease, border-color .12s ease, color .12s ease; padding: 0; } .row-action-btn:hover { background: rgba(155,93,229,.08); border-color: var(--violet); color: var(--violet); } .row-action-btn:focus-visible { outline: 2px solid var(--violet); outline-offset: 1px; } .row-action-btn.danger:hover { background: rgba(239,68,68,.08); border-color: var(--red, #EF4444); color: var(--red, #EF4444); } .row-action-btn svg { width: 14px; height: 14px; pointer-events: none; } .row-action-btn:disabled { opacity: .5; cursor: wait; } .row-actions-cell { text-align: right; white-space: nowrap; padding-right: 12px; } @media (max-width: 768px) { .row-actions { display: none; } } `; document.head.appendChild(s); } async function load() { const subject = document.getElementById('t-subject').value; document.getElementById('t-body').innerHTML = '
'; openDrawerId = null; ensureRowActionsStyles(); try { allSessions = await LS.adminGetSessions({ subject: subject || undefined }); renderSessions(); } catch (e) { document.getElementById('t-body').innerHTML = `
Ошибка: ${esc(e.message)}
`; } } function sessPctRing(pct) { const { pctClass } = AdminCtx; const pc = pctClass(pct); const colorMap = {'pct-hi':'var(--green)','pct-mid':'var(--amber)','pct-lo':'var(--pink)'}; const color = colorMap[pc] || 'var(--text-3)'; const circ = 106.8; const dash = (pct / 100 * circ).toFixed(1); return ` ${pct}% `; } function renderSessions() { const { MODES, fmtDate, fmtTime } = AdminCtx; const modeF = document.getElementById('t-mode').value; const searchF = document.getElementById('t-search').value.toLowerCase(); const filtered = allSessions.filter(s => { if (modeF && s.mode !== modeF) return false; if (searchF && !s.user_name.toLowerCase().includes(searchF) && !s.user_email.toLowerCase().includes(searchF)) return false; return true; }); document.getElementById('t-count').textContent = `${filtered.length} тестов`; if (!filtered.length) { document.getElementById('t-body').innerHTML = '
Нет тестов
'; return; } const groups = {}; filtered.forEach(s => { const key = fmtDate(s.started_at); (groups[key] = groups[key] || []).push(s); }); document.getElementById('t-body').innerHTML = Object.entries(groups).map(([date, sessions]) => `
${date}
${sessions.map(s => { const ring = s.percent !== null ? sessPctRing(s.percent) : `
`; return `
${ring}
${esc(s.user_name)}
${esc(s.subject_name||'?')} · ${MODES[s.mode]||s.mode}
${s.score??'—'} / ${s.total}
${fmtTime(s.duration_sec)}
`; }).join('')}
` ).join(''); } async function toggleDrawer(id) { const drawerEl = document.getElementById('tdrawer-' + id); const drawer = document.getElementById('drawer-' + id); const trow = document.getElementById('trow-' + id); if (openDrawerId && openDrawerId !== id) { document.getElementById('tdrawer-' + openDrawerId)?.classList.remove('open'); document.getElementById('drawer-' + openDrawerId)?.classList.remove('open'); document.getElementById('trow-' + openDrawerId)?.classList.remove('open'); } if (openDrawerId === id) { drawerEl.classList.remove('open'); drawer.classList.remove('open'); trow.classList.remove('open'); openDrawerId = null; return; } openDrawerId = id; trow.classList.add('open'); drawerEl.classList.add('open'); requestAnimationFrame(() => drawer.classList.add('open')); const inner = document.getElementById('drawer-inner-' + id); if (inner.dataset.loaded) return; inner.dataset.loaded = '1'; try { const d = await LS.adminGetSessionDetail(id); renderDrawer(inner, d); } catch (e) { inner.innerHTML = `
Ошибка: ${esc(e.message)}
`; } } function renderDrawer(el, d) { const { MODES, pctClass, fmtDate, fmtTime, renderMath } = AdminCtx; const pct = d.score !== null && d.total ? Math.round((d.score/d.total)*100) : null; const pc = pctClass(pct); const correct = d.questions.filter(q => q.is_correct).length; const wrong = d.questions.filter(q => !q.is_correct && q.chosen_option_id).length; const skipped = d.questions.filter(q => !q.chosen_option_id).length; const qHtml = d.questions.map((q,i) => { const status = !q.chosen_option_id ? 'skipped' : q.is_correct ? 'correct' : 'wrong'; const badgeTxt = { correct:'Верно', wrong:'Неверно', skipped:'Пропущено' }[status]; const opts = q.options.map(o => { const isCor = o.is_correct, isCho = o.id === q.chosen_option_id; let cls='', icon=''; if (isCor) { cls='correct-opt'; icon=''; } else if (isCho && !isCor) { cls='chosen-wrong'; icon=''; } return `
${icon}${esc(o.text)}
`; }).join(''); const expl = q.explanation ? `
Пояснение: ${esc(q.explanation)}
` : ''; return `
Вопрос ${i+1}${badgeTxt}${q.time_spent_sec?q.time_spent_sec+' сек':''}
${esc(q.text)}
${opts}
${expl}
`; }).join(''); el.innerHTML = `
${esc(d.user_name)}
${esc(d.user_email)} · ${d.subject_name||'?'} · ${MODES[d.mode]||d.mode} · ${fmtDate(d.started_at)}
${pct !== null ? pct+'%' : '—'}
${correct}
Верно
${wrong}
Неверно
${skipped}
Пропущено
${fmtTime(d.duration_sec)}
Время
${qHtml||'
Вопросы не найдены
'}
`; renderMath(el); if (window.lucide) lucide.createIcons(); } async function quickDeleteSession(id, btn) { if (!await LS.confirm( 'Удалить эту сессию? Все ответы и связанные данные будут удалены.\nЭто действие нельзя отменить.', { title: 'Удалить сессию', confirmText: 'Удалить' } )) return; btn.disabled = true; try { await LS.adminDeleteSession(id); LS.toast('Сессия удалена', 'success'); // Refresh from server — keeps grouped layout consistent. await load(); } catch (e) { LS.toast('Ошибка: ' + e.message, 'error'); btn.disabled = false; } } // Expose handlers window.loadSessions = load; window.renderSessions = renderSessions; window.toggleDrawer = toggleDrawer; window.quickDeleteSession = quickDeleteSession; window.AdminSections = window.AdminSections || {}; window.AdminSections.sessions = { init: async () => { if (inited) return; inited = true; await load(); }, reload: load, }; })();