From 92030b462c2682c0f5afb424bcf04d408b17f0f5 Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Sat, 16 May 2026 22:50:14 +0300 Subject: [PATCH] =?UTF-8?q?feat(admin):=20phase=202=20=E2=80=94=20split=20?= =?UTF-8?q?admin.js=20into=2013=20section=20modules?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace ~3500L admin.js monolith with thin orchestrator (~700L) + 14 IIFE-wrapped per-section modules under /js/admin/sections/. Section modules expose AdminSections..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. --- frontend/admin.html | 15 + frontend/js/admin/_shared.js | 129 + frontend/js/admin/admin.js | 4216 ++++----------------- frontend/js/admin/sections/assignments.js | 477 +++ frontend/js/admin/sections/gam.js | 183 + frontend/js/admin/sections/games.js | 132 + frontend/js/admin/sections/permissions.js | 68 + frontend/js/admin/sections/questions.js | 535 +++ frontend/js/admin/sections/sessions.js | 159 + frontend/js/admin/sections/shop.js | 207 + frontend/js/admin/sections/sims.js | 118 + frontend/js/admin/sections/stats.js | 50 + frontend/js/admin/sections/subjects.js | 338 ++ frontend/js/admin/sections/sublog.js | 104 + frontend/js/admin/sections/tests.js | 283 ++ frontend/js/admin/sections/tpl.js | 73 + frontend/js/admin/sections/users.js | 343 ++ 17 files changed, 3877 insertions(+), 3553 deletions(-) create mode 100644 frontend/js/admin/_shared.js create mode 100644 frontend/js/admin/sections/assignments.js create mode 100644 frontend/js/admin/sections/gam.js create mode 100644 frontend/js/admin/sections/games.js create mode 100644 frontend/js/admin/sections/permissions.js create mode 100644 frontend/js/admin/sections/questions.js create mode 100644 frontend/js/admin/sections/sessions.js create mode 100644 frontend/js/admin/sections/shop.js create mode 100644 frontend/js/admin/sections/sims.js create mode 100644 frontend/js/admin/sections/stats.js create mode 100644 frontend/js/admin/sections/subjects.js create mode 100644 frontend/js/admin/sections/sublog.js create mode 100644 frontend/js/admin/sections/tests.js create mode 100644 frontend/js/admin/sections/tpl.js create mode 100644 frontend/js/admin/sections/users.js diff --git a/frontend/admin.html b/frontend/admin.html index b52b9a7..2eba1d1 100644 --- a/frontend/admin.html +++ b/frontend/admin.html @@ -1982,6 +1982,21 @@ + + + + + + + + + + + + + + + diff --git a/frontend/js/admin/_shared.js b/frontend/js/admin/_shared.js new file mode 100644 index 0000000..be6ed2d --- /dev/null +++ b/frontend/js/admin/_shared.js @@ -0,0 +1,129 @@ +'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 `${MAP[type]||type}`; + } + + function qOptsPreview(q) { + if (q.type === 'short_answer') return q.correct_text ? `Ответ: ${esc(q.correct_text)}` : ''; + if (!q.options?.length) return ''; + const correct = q.options.filter(o => o.is_correct).map(o => esc(o.text)).join(', '); + return ` ${correct}`; + } + + /* ─── 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 ? '' : ''; + return `${gap}`; + }).join(''); + bar.innerHTML = ` +
${from}–${to} из ${total}
+
+ + ${numHtml} + +
`; + 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 || {}; +})(); diff --git a/frontend/js/admin/admin.js b/frontend/js/admin/admin.js index b891c3f..1445d74 100644 --- a/frontend/js/admin/admin.js +++ b/frontend/js/admin/admin.js @@ -1,3591 +1,701 @@ 'use strict'; -// admin.html — main script (extracted from inline