Files
Learn_System/frontend/js/admin/_shared.js
T
Maxim Dolgolyov 92030b462c feat(admin): phase 2 — split admin.js into 13 section modules
Replace ~3500L admin.js monolith with thin orchestrator (~700L) +

14 IIFE-wrapped per-section modules under /js/admin/sections/.

Section modules expose AdminSections.<name>.init/reload (lazy init via

switchTab/router) and re-expose onclick handlers via window.X for

backward compat. Shared helpers (MODES/DIFFS, fmtDate, pctClass,

renderMath, qTypeBadge, pagination) live in /js/admin/_shared.js

exposed on window.AdminCtx.

switchTab now dispatches to AdminSections via ROUTE_TO_SECTION map;

non-extracted system tabs (topics/audit/errors/health/classroom/avatars)

remain inline in admin.js. user-panel overlay markup untouched — Phase 6

will remove it.
2026-05-16 22:50:14 +03:00

130 lines
5.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
/* Admin shared helpers — referenced by admin.js orchestrator + every section module.
* Exposed on window.AdminCtx (filled by admin.js after LS.initPage()) and
* on window directly for utility functions used by HTML onclicks.
*/
(function () {
'use strict';
/* ─── Constants ─── */
const MODES = { exam:'Экзамен', practice:'Тренировка', repeat:'Обычный', ct:'ЦТ/ЦЭ', topic:'По теме', random:'Случайный' };
const DIFFS = { 1:'Лёгкий', 2:'Средний', 3:'Сложный' };
const DIFF_LABELS = DIFFS;
const TYPE_LABELS = { single:'Один', multi:'Несколько', true_false:'Верно/Нет', short_answer:'Краткий', matching:'Сопоставление' };
/* ─── Generic formatters ─── */
function pctClass(p) { return p === null ? '' : p >= 75 ? 'pct-hi' : p >= 50 ? 'pct-mid' : 'pct-lo'; }
function fmtDate(d) { return new Date(d).toLocaleDateString('ru', { day:'numeric', month:'short', year:'numeric' }); }
function fmtTime(sec) {
if (!sec || sec < 0) return '—';
const m = Math.floor(sec / 60), s = sec % 60;
return m ? `${m} мин ${s} сек` : `${s} сек`;
}
function fmtDuration(sec) {
if (!sec || sec < 0) return '—';
const h = Math.floor(sec / 3600), m = Math.floor((sec % 3600) / 60), s = sec % 60;
if (h) return `${h}ч ${m}м`;
if (m) return `${m} мин ${s} сек`;
return `${s} сек`;
}
/* ─── KaTeX rendering ─── */
const KATEX_OPTS = {
delimiters: [
{ left: '\\(', right: '\\)', display: false },
{ left: '\\[', right: '\\]', display: true },
],
throwOnError: false,
};
function renderMath(el) {
if (!el) return;
const run = () => { if (window.renderMathInElement) renderMathInElement(el, KATEX_OPTS); };
if (window._katexReady) run(); else window._katexCb = run;
}
/* ─── Question type badges (used by tests + subjects sections) ─── */
function qTypeBadge(type) {
const MAP = { single:'Один', multi:'Несколько', true_false:'Верно/Нет', short_answer:'Ответ', matching:'Сопост.' };
const CLR = { single:'rgba(155,93,229,0.12)', multi:'rgba(6,214,224,0.12)', true_false:'rgba(255,179,71,0.14)', short_answer:'rgba(6,214,100,0.12)', matching:'rgba(241,91,181,0.10)' };
const TXT = { single:'var(--violet)', multi:'#05aab3', true_false:'var(--amber)', short_answer:'var(--green)', matching:'var(--pink)' };
return `<span class="tst-q-badge" style="background:${CLR[type]||'rgba(15,23,42,0.06)'};color:${TXT[type]||'var(--text-3)'}">${MAP[type]||type}</span>`;
}
function qOptsPreview(q) {
if (q.type === 'short_answer') return q.correct_text ? `<span class="tst-q-opts">Ответ: ${esc(q.correct_text)}</span>` : '';
if (!q.options?.length) return '';
const correct = q.options.filter(o => o.is_correct).map(o => esc(o.text)).join(', ');
return `<span class="tst-q-opts"><i data-lucide="check" style="width:12px;height:12px;vertical-align:-2px"></i> ${correct}</span>`;
}
/* ─── Pagination controls (users + future tables) ─── */
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);
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 ? '<span class="pgn-ellip">…</span>' : '';
return `${gap}<button class="pgn-btn${n === page ? ' active' : ''}" onclick="${gotoFn}(${n})">${n}</button>`;
}).join('');
bar.innerHTML = `
<div class="pgn-info">${from}${to} из ${total}</div>
<div class="pgn-ctrls">
<button class="pgn-btn" onclick="${gotoFn}(${page - 1})" ${page <= 1 ? 'disabled' : ''}>←</button>
${numHtml}
<button class="pgn-btn" onclick="${gotoFn}(${page + 1})" ${page >= pages ? 'disabled' : ''}>→</button>
</div>`;
bar.style.display = '';
}
/* ─── Export ─── */
window.AdminCtx = window.AdminCtx || {
// filled by admin.js after LS.initPage():
user: null,
isTeacher: false,
isAdmin: false,
// constants:
MODES,
DIFFS,
DIFF_LABELS,
TYPE_LABELS,
// formatters:
pctClass,
fmtDate,
fmtTime,
fmtDuration,
// rendering:
renderMath,
qTypeBadge,
qOptsPreview,
// pagination:
renderPgnControls,
ensurePgnStyles,
};
window.AdminSections = window.AdminSections || {};
})();