'use strict'; /* ────────────────────────────────────────────────────────────────── Mock exam view — three phases on the same page: setup : pick source (variant / random N) + start active : countdown timer + tasks (no auto-check) + finish result : score + breakdown with solutions URL: /exam-prep/:examKey/mock → setup /exam-prep/:examKey/mock/:id → active | result (by session.status) ────────────────────────────────────────────────────────────────── */ (async function () { await EP.boot(); const examKey = EP.examKey; const main = document.getElementById('ep-main'); // Parse :id from path: /exam-prep//mock/ const mockId = (() => { const m = location.pathname.match(/\/mock\/(\d+)/); return m ? Number(m[1]) : null; })(); if (!mockId) { renderSetup(); return; } // Load session + tasks let payload; try { payload = await LS.api(`/api/exam-prep/mock/${mockId}`); } catch (e) { main.innerHTML = errorHtml('Не удалось загрузить пробник', e); if (window.lucide) lucide.createIcons(); return; } if (payload.session.status === 'finished') { renderResult(payload); } else { renderActive(payload); } /* ════════════════════════════════════════════════════════════ PHASE 1: SETUP ════════════════════════════════════════════════════════════ */ function renderSetup() { const title = EP.info?.track?.title || 'Пробный экзамен'; const dur = EP.info?.track?.duration_min || 180; const tpv = EP.info?.track?.tasks_per_variant || 10; const vc = EP.info?.track?.variants_count || 80; main.innerHTML = `

Новый пробник

${dur} минут · ${tpv} задач · в конце — балл по сетке и разбор каждого задания. Во время прохождения ответы не проверяются и решения скрыты — как на реальном экзамене.

По варианту
Один из ${vc} реальных вариантов целиком.
Случайные задачи
Микс из всего банка (только mc + open).
`; if (window.lucide) lucide.createIcons(); // Source selection let source = 'variant'; main.querySelectorAll('.mk-source-card').forEach(c => { c.onclick = () => { source = c.dataset.src; main.querySelectorAll('.mk-source-card').forEach(x => x.classList.toggle('mk-source-active', x === c)); }; }); document.getElementById('mk-start').onclick = startMock; async function startMock() { const btn = document.getElementById('mk-start'); btn.disabled = true; btn.textContent = 'Запуск…'; const body = { source }; if (source === 'variant') { const v = Number(document.getElementById('mk-variant-input').value); if (!Number.isInteger(v) || v < 1) { btn.disabled = false; btn.innerHTML = ' Начать пробник'; if (window.lucide) lucide.createIcons(); return alert('Введите номер варианта'); } body.variant = v; } else { body.count = Number(document.getElementById('mk-count-input').value) || 10; } try { const r = await EP.api.startMock(examKey, body); location.href = `/exam-prep/${examKey}/mock/${r.id}`; } catch (e) { btn.disabled = false; btn.innerHTML = ' Начать пробник'; if (window.lucide) lucide.createIcons(); alert(`Не удалось начать: ${e.message || e}`); } } } /* ════════════════════════════════════════════════════════════ PHASE 2: ACTIVE ════════════════════════════════════════════════════════════ */ function renderActive(payload) { const { session, tasks } = payload; const startMs = session.started_at; const totalMs = session.duration_planned_min * 60 * 1000; const sourceLabel = session.source === 'variant' ? `Вариант ${session.variant}` : `Случайные ${tasks.length} задач`; main.innerHTML = `
${sourceLabel} 0/${tasks.length} отвечено
--:--:--
`; const taskContainer = document.getElementById('mk-tasks'); const answeredSet = new Set(); tasks.forEach((task, i) => { // Prefill if resuming if (task.user_answer != null) answeredSet.add(task.id); EP.TaskCard.render(taskContainer, task, { mode: 'mock', sessionId: session.id, autoCheck: false, showSolution: false, numbering: i + 1, prefillAnswer: task.user_answer ?? null, onAnswerChange: (taskId, value) => { // Save (best-effort). Empty → don't bother if (value == null || value === '') return; EP.api.mockAnswer(session.id, { exam_task_id: taskId, user_answer: value, }).then(() => { if (!answeredSet.has(taskId)) { answeredSet.add(taskId); updateAnsweredCount(); } }).catch(() => { /* silent — user can finish anyway */ }); }, }); }); function updateAnsweredCount() { const el = document.getElementById('mk-answered'); if (el) el.textContent = `${answeredSet.size}/${tasks.length} отвечено`; } updateAnsweredCount(); /* Timer */ let timerInterval = null; function tick() { const left = startMs + totalMs - Date.now(); const el = document.getElementById('mk-timer'); if (!el) { clearInterval(timerInterval); return; } if (left <= 0) { el.textContent = '00:00:00'; el.classList.add('mk-timer-zero'); clearInterval(timerInterval); finish(true); return; } const h = Math.floor(left / 3600000); const m = Math.floor((left % 3600000) / 60000); const s = Math.floor((left % 60000) / 1000); el.textContent = `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`; if (left < 10 * 60 * 1000) el.classList.add('mk-timer-warn'); } tick(); timerInterval = setInterval(tick, 1000); /* Finish */ const finishBtn = document.getElementById('mk-finish'); finishBtn.onclick = async () => { if (answeredSet.size < tasks.length) { const left = tasks.length - answeredSet.size; const ok = await LS.confirm( `Не отвечено заданий: ${left}.\nЗавершить пробник сейчас?`, { title: 'Завершить пробник?', confirmText: 'Завершить', danger: true } ); if (!ok) return; } finish(false); }; async function finish(autoExpired) { clearInterval(timerInterval); finishBtn.disabled = true; finishBtn.innerHTML = ' Подведение итогов…'; if (window.lucide) lucide.createIcons(); try { await EP.api.mockFinish(session.id); location.reload(); // will render result phase } catch (e) { LS.toast(`Не удалось завершить: ${e.message || e}`, 'error'); finishBtn.disabled = false; finishBtn.innerHTML = ' Завершить'; if (window.lucide) lucide.createIcons(); } } if (window.lucide) lucide.createIcons(); } /* ════════════════════════════════════════════════════════════ PHASE 3: RESULT ════════════════════════════════════════════════════════════ */ function renderResult(payload) { const { session, tasks } = payload; const dur = (session.finished_at - session.started_at) / 1000; const h = Math.floor(dur / 3600), m = Math.floor((dur % 3600) / 60); const durStr = h ? `${h} ч ${m} мин` : `${m} мин`; const acc = session.total_tasks ? Math.round((session.total_correct / session.total_tasks) * 100) : 0; main.innerHTML = `

Результат пробника

Балл
${session.score != null ? session.score : '—'}
по сетке экзамена
Верно
${session.total_correct}/${session.total_tasks}
${acc}% точности
Время
${durStr}
из ${session.duration_planned_min} мин

Разбор задач

`; const container = document.getElementById('mk-breakdown'); tasks.forEach((task, i) => { EP.TaskCard.render(container, task, { mode: 'mock', sessionId: session.id, autoCheck: false, showSolution: true, readonly: true, numbering: i + 1, prefillAnswer: task.user_answer ?? null, forceVerdict: task.is_correct != null ? { isCorrect: task.is_correct } : null, }); }); if (window.lucide) lucide.createIcons(); } /* ── utils ──────────────────────────────────────────────────── */ function errorHtml(title, e) { return `

${escapeHtml(title)}

${escapeHtml(e?.message || String(e))}

`; } function escapeHtml(s) { return String(s || '').replace(/[&<>"']/g, c => ({ '&':'&', '<':'<', '>':'>', '"':'"', "'":''' }[c])); } })();