diff --git a/backend/src/routes/exam-prep.js b/backend/src/routes/exam-prep.js index 112c8c8..629a053 100644 --- a/backend/src/routes/exam-prep.js +++ b/backend/src/routes/exam-prep.js @@ -1,7 +1,7 @@ 'use strict'; const router = require('express').Router(); const db = require('../db/db'); -const { authMiddleware } = require('../middleware/auth'); +const { authMiddleware, requireRole } = require('../middleware/auth'); const access = require('../services/contentAccess'); router.use(authMiddleware); @@ -416,6 +416,28 @@ router.get('/tracks', (req, res) => { res.json({ tracks }); }); +/* ── Админ: управление экзамен-модулями (вкл/выкл) ── + Отдельные пути (без :examKey, чтобы не задеть гейт content_access). */ +router.get('/admin/tracks', requireRole('admin'), (_req, res) => { + const tracks = db.prepare(` + SELECT exam_key, title, subject_slug, grade, enabled, variants_count, sort_order, + (SELECT COUNT(*) FROM exam_tasks t WHERE t.exam_key = exam_tracks.exam_key) AS task_count + FROM exam_tracks + ORDER BY sort_order, exam_key + `).all(); + res.json({ tracks }); +}); + +router.patch('/admin/track', requireRole('admin'), (req, res) => { + const key = String(req.body && req.body.exam_key || '').trim(); + if (!key) return res.status(400).json({ error: 'exam_key required' }); + if (!db.prepare('SELECT 1 FROM exam_tracks WHERE exam_key = ?').get(key)) + return res.status(404).json({ error: 'Unknown exam track' }); + const enabled = req.body && req.body.enabled ? 1 : 0; + db.prepare('UPDATE exam_tracks SET enabled = ? WHERE exam_key = ?').run(enabled, key); + res.json({ ok: true, exam_key: key, enabled }); +}); + /* ── GET /api/exam-prep/:examKey/info ── Track metadata + global counts + this user's aggregate progress. */ // @public-by-design: router-level authMiddleware (line 6) covers this route diff --git a/frontend/admin.html b/frontend/admin.html index 2ccb7b5..25803df 100644 --- a/frontend/admin.html +++ b/frontend/admin.html @@ -1067,6 +1067,9 @@ + @@ -1616,6 +1619,16 @@
+ +
+
Экзамен-модули
+

+ Включение/выключение модулей подготовки к экзамену (/exam-prep). Выключенный модуль + скрыт у учеников и не показывается в каталоге прав доступа. Доступ ученикам открывается отдельно + в разделе «Доступ · контент» → «Экзамены».

+
+
+
Доступ к учебникам и экзаменам
@@ -2136,6 +2149,7 @@ + diff --git a/frontend/js/admin/admin.js b/frontend/js/admin/admin.js index aeef22b..c6a0bae 100644 --- a/frontend/js/admin/admin.js +++ b/frontend/js/admin/admin.js @@ -15,7 +15,7 @@ AdminCtx.isAdmin = isAdmin; /* Admin-only tabs: show to everyone for discoverability, but lock for non-admins */ - const ADMIN_ONLY_TABS = ['btn-tab-subjects','btn-tab-permissions','btn-tab-shop','btn-tab-gam','btn-tab-tpl','btn-tab-sims','btn-tab-games','btn-tab-assistant','btn-tab-imggen']; + const ADMIN_ONLY_TABS = ['btn-tab-subjects','btn-tab-permissions','btn-tab-shop','btn-tab-gam','btn-tab-tpl','btn-tab-sims','btn-tab-exams','btn-tab-games','btn-tab-assistant','btn-tab-imggen']; const lockSvg = ''; ADMIN_ONLY_TABS.forEach(id => { const el = document.getElementById(id); @@ -64,6 +64,7 @@ gam: 'gam', tpl: 'tpl', sims: 'sims', + exams: 'exams', games: 'games', assistant: 'assistant', imggen: 'imggen', diff --git a/frontend/js/admin/sections/exams.js b/frontend/js/admin/sections/exams.js new file mode 100644 index 0000000..93d5a42 --- /dev/null +++ b/frontend/js/admin/sections/exams.js @@ -0,0 +1,72 @@ +'use strict'; +/* admin → exams (exam-prep modules) section. + * Список ВСЕХ экзамен-треков (вкл. выключенные) + тумблер enabled. + * Источник: GET /api/exam-prep/admin/tracks; переключение: PATCH /api/exam-prep/admin/track. + * Влияет на видимость модуля в /exam-prep и в каталоге прав доступа (Экзамены). */ +(function () { + 'use strict'; + let inited = false; + let _tracks = []; + + const SUBJ = { math: 'Математика', physics: 'Физика', phys: 'Физика', chemistry: 'Химия', + chem: 'Химия', biology: 'Биология', bio: 'Биология' }; + function esc(s) { + return String(s == null ? '' : s).replace(/[&<>"']/g, c => + ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); + } + + async function load() { + try { + const data = await LS.api('/api/exam-prep/admin/tracks'); + _tracks = Array.isArray(data.tracks) ? data.tracks : []; + _render(); + } catch (e) { LS.toast('Ошибка загрузки экзамен-модулей: ' + e.message, 'error'); } + } + + function _render() { + const grid = document.getElementById('exams-grid'); + if (!grid) return; + if (!_tracks.length) { grid.innerHTML = '

Нет экзамен-модулей.

'; return; } + grid.innerHTML = _tracks.map(t => { + const subj = SUBJ[t.subject_slug] || t.subject_slug || ''; + const meta = [subj, t.grade ? (t.grade + ' кл.') : '', (t.task_count || 0) + ' заданий'] + .filter(Boolean).join(' · '); + return `
+
+
${esc(t.title)}
+
${esc(t.exam_key)}${meta ? ' · ' + esc(meta) : ''}
+
+
+ Открыть + +
+
`; + }).join(''); + if (window.lucide) lucide.createIcons(); + } + + async function examToggle(examKey, enabled) { + try { + await LS.api('/api/exam-prep/admin/track', { + method: 'PATCH', body: JSON.stringify({ exam_key: examKey, enabled }), + }); + const t = _tracks.find(x => x.exam_key === examKey); + if (t) t.enabled = enabled ? 1 : 0; + const card = document.getElementById('examcard-' + examKey); + if (card) card.classList.toggle('enabled', !!enabled); + LS.toast(enabled ? `«${examKey}» включён` : `«${examKey}» выключен`, enabled ? 'success' : 'warning'); + } catch (e) { LS.toast('Ошибка: ' + e.message, 'error'); } + } + + window.examToggle = examToggle; + window.AdminSections = window.AdminSections || {}; + window.AdminSections.exams = { + init: async () => { if (inited) return; inited = true; await load(); }, + reload: load, + }; +})();