'use strict'; /* admin → session-detail (Phase 6) — deep page for a single test session * (#sessions/:id). Replaces the inline drawer rendering when a row is clicked. * * Lazy-init via AdminSections['session-detail'].init(id). */ (function () { 'use strict'; /* ── one-time CSS injection ── */ function ensureSdStyles() { if (document.getElementById('session-detail-style')) return; const s = document.createElement('style'); s.id = 'session-detail-style'; s.textContent = ` .sd-wrap { padding: 4px 2px 24px; } .sd-back { display:inline-flex; align-items:center; gap:6px; font-size:0.82rem; color:var(--text-3); text-decoration:none; padding:6px 10px; border-radius:8px; margin-bottom:16px; transition:background .12s, color .12s; cursor:pointer; background:transparent; border:0; font-family:inherit; } .sd-back:hover { background:rgba(155,93,229,.07); color:var(--violet); } .sd-back svg { width:14px; height:14px; } .sd-header { display:flex; align-items:center; gap:20px; padding:22px 26px; background:var(--surface); border:1px solid var(--border); border-radius:var(--r-lg); margin-bottom:20px; flex-wrap:wrap; } .sd-user-block { flex:1; min-width:200px; } .sd-user-name { font-family:'Unbounded',sans-serif; font-size:1.05rem; font-weight:800; cursor:pointer; transition:color .12s; } .sd-user-name:hover { color:var(--violet); } .sd-user-meta { font-size:0.82rem; color:var(--text-3); margin-top:4px; } .sd-score { font-family:'Unbounded',sans-serif; font-size:1.5rem; font-weight:800; padding:14px 20px; border-radius:16px; } .sd-score.pct-hi { color:var(--green); background:rgba(16,185,129,.1); } .sd-score.pct-mid { color:var(--amber); background:rgba(255,179,71,.12); } .sd-score.pct-lo { color:var(--pink); background:rgba(241,91,181,.1); } .sd-score.pct-none { color:var(--text-3); background:rgba(15,23,42,.04); } .sd-stats { display:flex; gap:24px; flex-wrap:wrap; } .sd-stat { text-align:center; } .sd-stat-val { font-family:'Unbounded',sans-serif; font-weight:700; font-size:0.95rem; } .sd-stat-val.correct { color:var(--green); } .sd-stat-val.wrong { color:var(--pink); } .sd-stat-val.skipped { color:var(--text-3); } .sd-stat-label { font-size:0.72rem; color:var(--text-3); margin-top:2px; } .sd-actions { display:flex; gap:6px; flex-wrap:wrap; margin-left:auto; } .sd-q-list { display:flex; flex-direction:column; gap:10px; } .sd-q-item { padding:16px 18px; background:var(--surface); border:1px solid var(--border); border-radius:14px; border-left:4px solid var(--text-3); } .sd-q-item.correct { border-left-color:var(--green); } .sd-q-item.wrong { border-left-color:var(--pink); } .sd-q-item.skipped { border-left-color:var(--amber); } .sd-q-header { display:flex; align-items:center; gap:10px; margin-bottom:8px; flex-wrap:wrap; } .sd-q-num { font-size:0.74rem; color:var(--text-3); font-weight:700; text-transform:uppercase; letter-spacing:.04em; } .sd-q-badge { font-size:0.7rem; padding:2px 8px; border-radius:var(--r-pill); font-weight:700; } .sd-q-badge.correct { background:rgba(16,185,129,.12); color:var(--green); } .sd-q-badge.wrong { background:rgba(241,91,181,.12); color:var(--pink); } .sd-q-badge.skipped { background:rgba(255,179,71,.14); color:var(--amber); } .sd-q-time { font-size:0.72rem; color:var(--text-3); margin-left:auto; } .sd-q-text { font-size:0.92rem; line-height:1.45; margin-bottom:10px; } .sd-q-opts { display:flex; flex-direction:column; gap:5px; } .sd-q-opt { padding:8px 12px; border-radius:8px; background:rgba(15,23,42,.03); font-size:0.86rem; display:flex; align-items:center; gap:8px; } .sd-q-opt.correct-opt { background:rgba(16,185,129,.08); color:var(--green); font-weight:600; } .sd-q-opt.chosen-wrong { background:rgba(241,91,181,.08); color:var(--pink); font-weight:600; } .sd-q-opt-icon { width:14px; height:14px; flex-shrink:0; display:inline-flex; } .sd-q-expl { margin-top:10px; padding:10px 14px; background:rgba(155,93,229,.06); border-radius:8px; font-size:0.84rem; color:var(--text-2); } .sd-empty { padding:30px; text-align:center; color:var(--text-3); font-size:0.88rem; background:var(--surface); border:1px dashed var(--border); border-radius:var(--r-lg); } @media (max-width: 640px) { .sd-header { padding:16px 14px; gap:14px; } .sd-actions { margin-left:0; width:100%; } .sd-score { font-size:1.2rem; padding:10px 14px; } .sd-stats { gap:14px; } } `; document.head.appendChild(s); } const ICONS = { arrowLeft: '', trash: '', }; let _sessionId = null; let _data = null; async function init(id) { ensureSdStyles(); const newId = Number(id); if (!Number.isFinite(newId) || newId <= 0) { renderError('Некорректный ID сессии'); return; } if (_sessionId === newId && _data) { render(); return; } _sessionId = newId; _data = null; renderLoading(); try { _data = await LS.adminGetSessionDetail(newId); render(); } catch (e) { renderError(e.message || String(e)); } } function renderLoading() { const el = document.getElementById('session-detail-content'); if (!el) return; el.innerHTML = '
'; } function renderError(msg) { const el = document.getElementById('session-detail-content'); if (!el) return; el.innerHTML = `
${esc(msg)}
`; } function render() { const el = document.getElementById('session-detail-content'); if (!el || !_data) return; const d = _data; const { MODES, pctClass, fmtDate, fmtTime, renderMath } = AdminCtx; const pct = (d.score !== null && d.score !== undefined && d.total) ? Math.round((d.score / d.total) * 100) : null; const pc = pct === null ? 'pct-none' : 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 isAdmin = AdminCtx.isAdmin; 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 || '')} · ${esc(d.subject_name || 'Тест')} · ${MODES[d.mode] || d.mode}
${fmtDate(d.started_at)}${d.finished_at ? ' · завершена ' + fmtDate(d.finished_at) : ''}
${pct !== null ? pct + '%' : '—'}
${correct}
Верно
${wrong}
Неверно
${skipped}
Пропущено
${fmtTime(d.duration_sec)}
Время
${isAdmin ? `
` : ''}
${qHtml || '
Вопросы не найдены
'}
`; renderMath(el); if (window.lucide) lucide.createIcons({ nodes: [el] }); } async function deleteSession() { if (!_sessionId) return; if (!await LS.confirm( 'Удалить эту сессию? Все ответы и связанные данные будут удалены.\nЭто действие нельзя отменить.', { title: 'Удалить сессию', confirmText: 'Удалить' } )) return; try { await LS.adminDeleteSession(_sessionId); LS.toast('Сессия удалена', 'success'); AdminRouter.navigate('#sessions'); } catch (e) { LS.toast('Ошибка: ' + e.message, 'error'); } } /* ── Expose ── */ window.sdDeleteSession = deleteSession; window.AdminSections = window.AdminSections || {}; window.AdminSections['session-detail'] = { init, reload: () => init(_sessionId), }; })();