'use strict';
/* ──────────────────────────────────────────────────────────────────
Exam 9 — Math 2025 renderer
Variants loaded into window.VARIANTS by /js/exam9/variants/vNN.js
────────────────────────────────────────────────────────────────── */
const STORAGE_KEY = 'exam9_progress_v1';
let currentVariant = null;
let katexLoaded = false;
let variantTests = {}; // { variantNum: testId } — populated by /api/exam9/variants
let userRole = null; // populated by LS.getUser()
let teacherClasses = null; // lazy-loaded from /api/classes
/* ── KaTeX bootstrap ────────────────────────────────────────────── */
function onKatexLoad() {
katexLoaded = true;
if (currentVariant !== null) runKatex(document.getElementById('ex-main'));
}
function runKatex(el) {
if (!katexLoaded || !el) return;
try {
renderMathInElement(el, {
delimiters: [
{ left: '$$', right: '$$', display: true },
{ left: '$', right: '$', display: false },
],
throwOnError: false,
});
} catch {}
}
/* ── Progress in localStorage ───────────────────────────────────── */
function loadProgress() {
try { return JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}'); }
catch { return {}; }
}
function saveProgress(p) {
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(p)); } catch {}
}
function markSolutionViewed(variantNum, taskIdx) {
const p = loadProgress();
p[variantNum] = p[variantNum] || [];
if (!p[variantNum].includes(taskIdx)) {
p[variantNum].push(taskIdx);
saveProgress(p);
}
}
/* ── Variant picker ─────────────────────────────────────────────── */
function buildGrid() {
const grid = document.getElementById('variant-grid');
const progress = loadProgress();
grid.innerHTML = '';
Object.keys(VARIANTS).sort((a, b) => Number(a) - Number(b)).forEach(n => {
const v = VARIANTS[n];
const total = (v.tasks || []).length;
const viewed = (progress[n] || []).length;
let cls = '';
if (viewed === total && total > 0) cls = ' done';
else if (viewed > 0) cls = ' partial';
const isActive = Number(n) === currentVariant ? ' active' : '';
const btn = document.createElement('button');
btn.className = 'vg-btn' + cls + isActive;
btn.textContent = n;
btn.title = `${v.label}${viewed === total ? ' ✓' : viewed > 0 ? ` (${viewed}/${total})` : ''}`;
btn.onclick = () => { selectVariant(Number(n)); closePicker(); };
grid.appendChild(btn);
});
}
function togglePicker() {
const overlay = document.getElementById('picker-overlay');
const btn = document.getElementById('picker-btn');
if (overlay.classList.contains('visible')) closePicker();
else {
buildGrid();
overlay.classList.add('visible');
btn.classList.add('open');
document.addEventListener('keydown', onEsc);
}
}
function closePicker() {
document.getElementById('picker-overlay').classList.remove('visible');
document.getElementById('picker-btn').classList.remove('open');
document.removeEventListener('keydown', onEsc);
}
function onOverlayClick(e) {
if (e.target === document.getElementById('picker-overlay')) closePicker();
}
function onEsc(e) { if (e.key === 'Escape') closePicker(); }
/* ── Task rendering ─────────────────────────────────────────────── */
function buildOpts(opts) {
const isLong = opts.some(([, t]) => t.length > 40 && !t.startsWith('$'));
const cls = isLong ? 'opts-vertical' : 'opts';
return `
` +
opts.map(([l, t]) =>
`${l})${t}`
).join('') + `
`;
}
const SOL_ICON_CLOSED = ``;
function renderVariant(num) {
const main = document.getElementById('ex-main');
const v = VARIANTS[num];
if (!v) {
main.innerHTML = 'Вариант не найден
';
return;
}
const isTeacher = userRole === 'teacher' || userRole === 'admin';
const testId = variantTests[num];
const assignBtn = isTeacher
? `
${testId ? '' : '
Этот вариант ещё не импортирован в банк (только нечётные)'}
`
: '';
main.innerHTML =
`${v.label}${v.tasks.length} заданий
` +
assignBtn +
v.tasks.map((t, i) => `
${t.text}
${t.figure ? `
${t.figure}
` : ''}
${t.opts ? buildOpts(t.opts) : ''}
${t.sol ? `
${t.sol}
` : ''}
`
).join('');
runKatex(main);
}
function toggleSol(btn, variantNum, taskIdx) {
const panel = btn.nextElementSibling;
const open = panel.classList.contains('visible');
panel.classList.toggle('visible', !open);
btn.classList.toggle('open', !open);
btn.querySelector('span').textContent = open ? 'Показать решение' : 'Скрыть решение';
if (!open) {
if (!panel.dataset.k) { runKatex(panel); panel.dataset.k = '1'; }
markSolutionViewed(variantNum, taskIdx);
}
}
function selectVariant(num) {
currentVariant = num;
document.getElementById('picker-label').textContent = VARIANTS[num].label;
document.querySelectorAll('.vg-btn').forEach(b => {
b.classList.toggle('active', Number(b.textContent) === num);
});
renderVariant(num);
// Persist last opened variant
try { localStorage.setItem('exam9_last_variant', String(num)); } catch {}
window.scrollTo({ top: 0, behavior: 'smooth' });
}
/* ── Assignment modal ───────────────────────────────────────────── */
async function loadTeacherClasses() {
if (teacherClasses) return teacherClasses;
try {
const list = await LS.api('/api/classes');
teacherClasses = Array.isArray(list) ? list : [];
} catch {
teacherClasses = [];
}
return teacherClasses;
}
function escapeHtml(s) {
return String(s || '').replace(/[&<>"']/g, c => ({ '&':'&', '<':'<', '>':'>', '"':'"', "'":''' }[c]));
}
async function openAssignModal(variantNum) {
if (!variantTests[variantNum]) return;
const testId = variantTests[variantNum];
// Build body with shared LS.modal helper
const classes = await loadTeacherClasses();
const classesHtml = classes.length
? classes.map(c => `
`).join('')
: 'У вас пока нет классов. Создайте класс на странице «Классы».
';
const body = `
`;
const m = LS.modal({
title: `Назначить «Вариант ${variantNum}» как ДЗ`,
content: body,
size: 'sm',
actions: [
{ label: 'Отмена', onClick: () => m.close() },
{
label: 'Назначить', primary: true,
onClick: async () => {
const checked = Array.from(m.body.querySelectorAll('input[name="cls"]:checked')).map(el => Number(el.value));
if (!checked.length) { m.setError('Выберите хотя бы один класс'); return; }
const btns = m.root.querySelectorAll('.ls-mod-btn');
btns.forEach(b => b.disabled = true);
btns[1].textContent = 'Назначаю…';
try {
const r = await LS.api('/api/assignments/bulk', {
method: 'POST',
body: {
title: `Экзамен 9 — Вариант ${variantNum}`,
class_ids: checked,
mode: 'exam', count: 10,
test_id: testId,
deadline: m.body.querySelector('#ax-deadline').value || null,
is_homework: 1,
},
});
LS.toast(`Назначено в ${r.count || checked.length} класс(ах)`, 'success');
m.close();
} catch (e) {
m.setError(e.message || 'Не удалось создать задание');
btns.forEach(b => b.disabled = false);
btns[1].textContent = 'Назначить';
}
},
},
],
});
}
/* ── Boot ───────────────────────────────────────────────────────── */
(async function boot() {
const keys = Object.keys(VARIANTS);
if (!keys.length) {
document.getElementById('ex-main').innerHTML = 'Варианты не загружены
';
return;
}
// Load user role + variant-to-test map (parallel)
const user = (typeof LS !== 'undefined') ? LS.getUser?.() : null;
userRole = user?.role || null;
if (userRole === 'teacher' || userRole === 'admin') {
try {
const r = await LS.api('/api/exam9/variants');
variantTests = r.variants || {};
} catch { variantTests = {}; }
}
// Resume last opened variant or open first one
let initial = Number(keys[0]);
try {
const last = Number(localStorage.getItem('exam9_last_variant'));
if (last && VARIANTS[last]) initial = last;
} catch {}
selectVariant(initial);
})();