6fed18f819
Не было UI для управления exam_tracks.enabled (только флаг в БД, ставился
миграцией). Добавлена админ-секция «Экзамен-модули»:
- backend exam-prep.js: GET /admin/tracks (все треки, вкл. выключенные, + число
заданий) и PATCH /admin/track (exam_key, enabled), обе requireRole('admin').
Пути без :examKey, чтобы не задеть гейт content_access.
- frontend: секция sections/exams.js (список треков + переключатель enabled),
вкладка в admin.html (admin-only через ADMIN_ONLY_TABS, locked для не-админов),
регистрация в admin.js (ROUTE_TO_SECTION).
Выключенный трек скрыт у учеников и пропадает из каталога прав доступа (тот
берёт exam_tracks WHERE enabled=1). Доступ ученикам по-прежнему в «Доступ · контент».
Требует перезапуска бэкенда + Ctrl+F5.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
73 lines
3.6 KiB
JavaScript
73 lines
3.6 KiB
JavaScript
'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 = '<p style="color:var(--muted);font-size:13px">Нет экзамен-модулей.</p>'; 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 `<div class="perm-card${t.enabled ? ' enabled' : ''}" id="examcard-${esc(t.exam_key)}" style="flex-wrap:wrap">
|
|
<div class="perm-info">
|
|
<div class="perm-label">${esc(t.title)}</div>
|
|
<div class="perm-desc" style="font-size:11px;margin-top:2px;opacity:.7">${esc(t.exam_key)}${meta ? ' · ' + esc(meta) : ''}</div>
|
|
</div>
|
|
<div style="display:flex;align-items:center;gap:10px">
|
|
<a href="/exam-prep/${esc(t.exam_key)}" target="_blank" title="Открыть модуль"
|
|
style="font-size:.72rem;color:var(--text-2);text-decoration:none;border:1px solid var(--border,rgba(255,255,255,.14));border-radius:8px;padding:4px 8px">Открыть</a>
|
|
<label class="perm-toggle" title="${t.enabled ? 'Выключить модуль' : 'Включить модуль'}">
|
|
<input type="checkbox" ${t.enabled ? 'checked' : ''} onchange="examToggle('${esc(t.exam_key)}', this.checked)" />
|
|
<span class="perm-track"></span>
|
|
<span class="perm-thumb"></span>
|
|
</label>
|
|
</div>
|
|
</div>`;
|
|
}).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,
|
|
};
|
|
})();
|