feat(tests): витрина доступных тестов ученику + флаг «доступен ученикам»

Раньше ученик видел лишь 1 тест на предмет (дефолтный). Теперь учитель/админ
может пометить любой свой тест доступным, и он появляется в каталоге на дашборде.

- Миграция 079: tests.available_to_students (default 0).
- testController: list для ученика отдаёт тесты с available_to_students=1 и вопросами;
  create/update принимают флаг; update сделан частичным (не затирает поля при toggle).
- admin «Тесты»: бейдж «Доступен ученикам» + быстрый тумблер «Ученикам/Скрыть»
  (toggleTstAvail; конструктор доступен и учителям — видят свои тесты).
- Дашборд: виджет «Тесты» → секция «Доступные тесты» (loadAvailableTests), клик
  запускает фикс-тест. Прячется, если доступных нет.

⚠️ Живой БД нужен npm run migrate (колонка).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-06-23 11:03:42 +03:00
parent c5d440a7a9
commit c6d323ec6d
4 changed files with 75 additions and 13 deletions
+32
View File
@@ -1750,6 +1750,11 @@
<div class="widget" id="w-tests">
<div class="w-head"><div class="w-title">Тесты</div></div>
<div class="subj-mini-grid" id="subjects-list"><div id="subjects-sk"></div></div>
<!-- Витрина: тесты, открытые учителем/админом ученикам -->
<div id="avail-tests-wrap" style="display:none;margin-top:14px">
<div style="font-size:.74rem;font-weight:800;letter-spacing:.04em;text-transform:uppercase;color:var(--text-3);margin:0 0 8px 2px">Доступные тесты</div>
<div class="subj-mini-grid" id="available-tests-list"></div>
</div>
</div>
<!-- Col 3: Progress -->
@@ -2257,6 +2262,32 @@
window.location.href = url;
}
/* Витрина доступных тестов (бэкенд ученику отдаёт только помеченные доступными). */
async function loadAvailableTests() {
const wrap = document.getElementById('avail-tests-wrap');
const list = document.getElementById('available-tests-list');
if (!wrap || !list) return;
try {
const tests = await LS.getTests();
if (!tests || !tests.length) { wrap.style.display = 'none'; return; }
const SUBJ_N = { bio:'Биология', chem:'Химия', math:'Математика', phys:'Физика' };
list.innerHTML = tests.map((t, i) => {
const color = SUBJ_COLORS[t.subject_slug] || '#9B5DE5';
const iconName = ICONS[t.subject_slug] || 'book-open';
return `<div class="subj-mini-card stagger-item" style="--i:${i}" onclick="startSubjectTest('${t.subject_slug}','exam',25,${t.id})">
<div class="smc-icon" style="background:${color}">${lci(iconName)}</div>
<div class="smc-body">
<div class="smc-name">${esc(t.title)}</div>
<div class="smc-meta">${SUBJ_N[t.subject_slug] || t.subject_slug} · ${t.question_count} вопр.</div>
</div>
<i data-lucide="chevron-right" class="smc-arrow"></i>
</div>`;
}).join('');
wrap.style.display = '';
reIcons();
} catch { wrap.style.display = 'none'; }
}
/* ══ ЗАДАНИЯ ══════════════════════════════════════════════════════════ */
async function loadAssignments() {
try {
@@ -4505,6 +4536,7 @@
} else {
// Student: full layout
loadSubjects();
loadAvailableTests();
loadAssignments();
loadStats();
loadGamification();