feat(prep): тумблер «готовится к ЦТ» на странице персональных учеников

my-students.html: колонка «ЦТ» с тумблером у каждого ученика (teacher_students вне
классов). Бэкенд уже поддерживал (canManageStudent включает teacher_students);
статус грузится per-student через LS.prepStudentTracks, переключение —
prepSetStudent/prepUnsetStudent. togglePrep на window (скрипт-модуль). Иконки — SVG.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-06-19 16:06:41 +03:00
parent 4aacb2d369
commit 82d323547f
+45 -2
View File
@@ -66,11 +66,17 @@
overflow:hidden; overflow:hidden;
} }
.ms-row { .ms-row {
display:grid; grid-template-columns: 36px 1.5fr 2fr 1fr auto auto; display:grid; grid-template-columns: 36px 1.5fr 2fr 1fr auto auto auto;
gap:14px; padding:14px 20px; align-items:center; gap:14px; padding:14px 20px; align-items:center;
border-bottom:1px solid var(--border); border-bottom:1px solid var(--border);
transition:background .12s; transition:background .12s;
} }
.prep-toggle { display:inline-flex; align-items:center; gap:5px; padding:4px 9px; border-radius:999px;
border:1px solid var(--border); background:var(--surface); cursor:pointer; font-size:.74rem;
font-weight:600; color:var(--text-3); transition:all .15s; white-space:nowrap; }
.prep-toggle svg { width:13px; height:13px; }
.prep-toggle:hover { border-color:var(--violet); color:var(--violet); }
.prep-toggle.on { background:rgba(155,93,229,.12); border-color:var(--violet); color:var(--violet); }
.ms-row:last-child { border-bottom:none; } .ms-row:last-child { border-bottom:none; }
.ms-row:hover { background:rgba(155,93,229,.04); } .ms-row:hover { background:rgba(155,93,229,.04); }
.ms-row.head { .ms-row.head {
@@ -110,7 +116,7 @@
@media (max-width: 700px) { @media (max-width: 700px) {
.ms-row { grid-template-columns: 36px 1fr auto; gap:10px; } .ms-row { grid-template-columns: 36px 1fr auto; gap:10px; }
.ms-row > :nth-child(3), .ms-row > :nth-child(4) { display:none; } .ms-row > :nth-child(3), .ms-row > :nth-child(4), .ms-row > :nth-child(5), .ms-row > :nth-child(6) { display:none; }
.ms-row.head { display:none; } .ms-row.head { display:none; }
} }
</style> </style>
@@ -193,11 +199,46 @@
let students = []; let students = [];
/* Флаг «готовится к ЦТ» (трек ct-math открывает карточки + курс + пробники). */
const PREP_TRACK = 'ct-math';
let _prep = {}; // { studentId: true|false }
function prepCellHtml(id) {
const on = _prep[id] === true;
return `<button class="prep-toggle${on ? ' on' : ''}" onclick="togglePrep(${id})" title="${on ? 'Готовится к ЦТ — нажмите, чтобы снять' : 'Отметить «готовится к ЦТ»'}">
${on
? '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>ЦТ'
: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/></svg>нет'}</button>`;
}
async function loadPrepAll() {
if (!students.length) return;
const results = await Promise.all(students.map(s =>
LS.prepStudentTracks(s.id)
.then(r => ({ id: s.id, on: (r.tracks || []).includes(PREP_TRACK) }))
.catch(() => ({ id: s.id, on: false }))));
_prep = {};
results.forEach(r => { _prep[r.id] = r.on; });
render();
}
window.togglePrep = async function (id) {
const on = _prep[id] === true;
try {
if (on) await LS.prepUnsetStudent(id, PREP_TRACK);
else await LS.prepSetStudent(id, PREP_TRACK);
_prep[id] = !on;
render();
if (LS.toast) LS.toast(on ? 'Снято' : 'Готовится к ЦТ', 'success');
} catch (e) { if (LS.toast) LS.toast(e.message || 'Ошибка', 'error'); else alert(e.message || 'Ошибка'); }
};
async function loadStudents() { async function loadStudents() {
try { try {
const r = await LS.api('/api/teacher-students'); const r = await LS.api('/api/teacher-students');
students = r.students || []; students = r.students || [];
render(); render();
loadPrepAll();
} catch (e) { } catch (e) {
document.getElementById('ms-list-container').innerHTML = `<div class="ms-empty">Ошибка: ${esc(e.message)}</div>`; document.getElementById('ms-list-container').innerHTML = `<div class="ms-empty">Ошибка: ${esc(e.message)}</div>`;
} }
@@ -222,6 +263,7 @@
<div>Email</div> <div>Email</div>
<div>Заданий</div> <div>Заданий</div>
<div>Добавлен</div> <div>Добавлен</div>
<div title="Готовится к ЦТ — открывает карточки, курс и пробники ЦТ">ЦТ</div>
<div></div> <div></div>
</div> </div>
${students.map(s => ` ${students.map(s => `
@@ -234,6 +276,7 @@
<div class="ms-email">${esc(s.email)}</div> <div class="ms-email">${esc(s.email)}</div>
<div class="ms-meta"><b style="color:var(--text)">${s.assignment_count || 0}</b></div> <div class="ms-meta"><b style="color:var(--text)">${s.assignment_count || 0}</b></div>
<div class="ms-meta">${fmtDate(s.added_at)}</div> <div class="ms-meta">${fmtDate(s.added_at)}</div>
<div>${prepCellHtml(s.id)}</div>
<div style="display:flex;gap:6px"> <div style="display:flex;gap:6px">
<button class="ms-btn" onclick="showProfile(${s.id}, '${esc(s.name).replace(/'/g, "\\'")}')" title="Профиль для Квантика (слабые темы)"> <button class="ms-btn" onclick="showProfile(${s.id}, '${esc(s.name).replace(/'/g, "\\'")}')" title="Профиль для Квантика (слабые темы)">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v4M12 17v4M3 12h4M17 12h4M5.6 5.6l2.8 2.8M15.6 15.6l2.8 2.8M18.4 5.6l-2.8 2.8M8.4 15.6l-2.8 2.8"/></svg> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v4M12 17v4M3 12h4M17 12h4M5.6 5.6l2.8 2.8M15.6 15.6l2.8 2.8M18.4 5.6l-2.8 2.8M8.4 15.6l-2.8 2.8"/></svg>