'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) => `
${i + 1}
Задание ${i + 1}
${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 ───────────────────────────────────────────── */ let assignVariantNum = null; async function loadTeacherClasses() { if (teacherClasses) return teacherClasses; try { const list = await LS.api('/api/classes'); teacherClasses = Array.isArray(list) ? list : []; } catch { teacherClasses = []; } return teacherClasses; } async function openAssignModal(variantNum) { if (!variantTests[variantNum]) return; assignVariantNum = variantNum; document.getElementById('assign-title').textContent = `Назначить «Вариант ${variantNum}» как ДЗ`; document.getElementById('ax-error').classList.remove('visible'); document.getElementById('ax-success').classList.remove('visible'); document.getElementById('ax-deadline').value = ''; document.getElementById('ax-submit').disabled = false; document.getElementById('ax-submit').textContent = 'Назначить'; const listEl = document.getElementById('ax-classes-list'); listEl.textContent = 'Загрузка…'; const classes = await loadTeacherClasses(); if (!classes.length) { listEl.innerHTML = '
У вас пока нет классов. Создайте класс на странице «Классы».
'; } else { listEl.innerHTML = classes.map(c => ` `).join(''); } document.getElementById('assign-overlay').classList.add('visible'); document.addEventListener('keydown', onAssignEsc); } function closeAssignModal() { document.getElementById('assign-overlay').classList.remove('visible'); document.removeEventListener('keydown', onAssignEsc); assignVariantNum = null; } function onAssignOverlayClick(e) { if (e.target === document.getElementById('assign-overlay')) closeAssignModal(); } function onAssignEsc(e) { if (e.key === 'Escape') closeAssignModal(); } function escapeHtml(s) { return String(s || '').replace(/[&<>"']/g, c => ({ '&':'&', '<':'<', '>':'>', '"':'"', "'":''' }[c])); } async function submitAssign() { const errorEl = document.getElementById('ax-error'); const successEl = document.getElementById('ax-success'); const submitBtn = document.getElementById('ax-submit'); errorEl.classList.remove('visible'); successEl.classList.remove('visible'); const checked = Array.from(document.querySelectorAll('#ax-classes-list input[name="cls"]:checked')) .map(el => Number(el.value)); if (!checked.length) { errorEl.textContent = 'Выберите хотя бы один класс'; errorEl.classList.add('visible'); return; } const testId = variantTests[assignVariantNum]; const deadline = document.getElementById('ax-deadline').value || null; if (!testId) { errorEl.textContent = 'Вариант не в банке вопросов'; errorEl.classList.add('visible'); return; } submitBtn.disabled = true; submitBtn.textContent = 'Назначаю…'; try { const r = await LS.api('/api/assignments/bulk', { method: 'POST', body: { title: `Экзамен 9 — Вариант ${assignVariantNum}`, class_ids: checked, mode: 'exam', count: 10, test_id: testId, deadline: deadline, is_homework: 1, }, }); successEl.textContent = `Назначено в ${r.count || checked.length} классе(ах)`; successEl.classList.add('visible'); submitBtn.textContent = 'Готово'; setTimeout(closeAssignModal, 1500); } catch (e) { errorEl.textContent = e.message || 'Не удалось создать задание'; errorEl.classList.add('visible'); submitBtn.disabled = false; submitBtn.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); })();