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:
@@ -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();
|
||||
|
||||
@@ -40,10 +40,12 @@
|
||||
<span class="q-badge q-badge-subj">${SUBJ_N[t.subject_slug]||t.subject_slug}</span>
|
||||
<span style="font-size:0.75rem;color:var(--text-3)">${t.question_count} вопросов</span>
|
||||
<span style="font-size:0.75rem;color:var(--text-3)">${fmtDate(t.created_at)}</span>
|
||||
${t.available_to_students ? `<span class="q-badge" style="background:rgba(6,214,160,.14);color:#059669">Доступен ученикам</span>` : ''}
|
||||
${t.description ? `<span style="font-size:0.75rem;color:var(--text-2)">${esc(t.description)}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div class="q-card-actions">
|
||||
<button class="btn-edit-q" onclick="toggleTstAvail(${t.id})" title="Показывать ли тест ученикам в каталоге">${t.available_to_students ? 'Скрыть' : 'Ученикам'}</button>
|
||||
<button class="btn-edit-q" onclick="editTst(${t.id})">Изменить</button>
|
||||
<button class="btn-del-q" onclick="deleteTst(${t.id})" title="Удалить"><i data-lucide="x" style="width:14px;height:14px"></i></button>
|
||||
</div>
|
||||
@@ -261,6 +263,20 @@
|
||||
} catch (e) { LS.toast('Ошибка: ' + e.message, 'error'); }
|
||||
}
|
||||
|
||||
// Открыть/скрыть тест для учеников (попадает в каталог на дашборде)
|
||||
async function toggleTstAvail(id) {
|
||||
const t = allTests.find(x => x.id === id);
|
||||
if (!t) return;
|
||||
if (!t.question_count) { LS.toast('Сначала добавьте вопросы в тест', 'error'); return; }
|
||||
const next = t.available_to_students ? 0 : 1;
|
||||
try {
|
||||
await LS.updateTest(id, { available_to_students: next });
|
||||
t.available_to_students = next;
|
||||
renderTests();
|
||||
LS.toast(next ? 'Тест открыт ученикам' : 'Тест скрыт от учеников', 'success');
|
||||
} catch (e) { LS.toast('Ошибка: ' + e.message, 'error'); }
|
||||
}
|
||||
|
||||
// Expose handlers
|
||||
window.loadTests = load;
|
||||
window.renderTests = renderTests;
|
||||
@@ -274,6 +290,7 @@
|
||||
window.closeTstModal = closeTstModal;
|
||||
window.saveTst = saveTst;
|
||||
window.deleteTst = deleteTst;
|
||||
window.toggleTstAvail = toggleTstAvail;
|
||||
|
||||
window.AdminSections = window.AdminSections || {};
|
||||
window.AdminSections.tests = {
|
||||
|
||||
Reference in New Issue
Block a user