feat(prep): фронтенд мастер-флага ЦТ — папка-коллекция карточек + тумблер у учителя
- flashcards.html: колоды коллекции рендерятся сворачиваемой папкой «Подготовка к ЦТ» (deckCardHtml вынесен, секции <details> по collection; метки из LS.prepListTracks) - classes.html: в таблице учеников колонка «ЦТ» с тумблером флага + кнопки «Весь класс → ЦТ»/ «Снять ЦТ» (LS.prepClassStatus/prepSetStudent/prepUnsetStudent/prepSetClass) Иконки — inline SVG, без эмодзи. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+56
-2
@@ -86,6 +86,14 @@
|
||||
tr:last-child td { border-bottom: none; }
|
||||
.pct-cell { font-family: 'Unbounded', sans-serif; font-size: 0.82rem; font-weight: 700; }
|
||||
.pct-hi { color: var(--green); } .pct-mid { color: var(--amber); } .pct-lo { color: var(--pink); }
|
||||
/* ── флаг «готовится к ЦТ» ── */
|
||||
.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: 0.74rem;
|
||||
font-weight: 600; color: var(--text-3); transition: all .15s; }
|
||||
.prep-toggle .ic { width: 13px; height: 13px; }
|
||||
.prep-toggle:hover { border-color: var(--violet); color: var(--violet); }
|
||||
.prep-toggle.on { background: rgba(123,97,255,.12); border-color: var(--violet); color: var(--violet); }
|
||||
.prep-bulk { font-size: 0.78rem; }
|
||||
|
||||
/* ── Assignments ── */
|
||||
.assign-list { display: flex; flex-direction: column; gap: 12px; }
|
||||
@@ -642,10 +650,13 @@
|
||||
</div>
|
||||
<button class="btn-ghost" onclick="doAddMember()" id="btn-add-member" disabled>+ Добавить</button>
|
||||
<span id="add-member-err" style="font-size:0.82rem;color:var(--pink)"></span>
|
||||
<span style="flex:1"></span>
|
||||
<button class="btn-ghost prep-bulk" onclick="bulkPrep(true)" title="Включить флаг «готовится к ЦТ» всем ученикам класса">Весь класс → ЦТ</button>
|
||||
<button class="btn-ghost prep-bulk" onclick="bulkPrep(false)" title="Снять флаг «готовится к ЦТ» со всех учеников класса">Снять ЦТ</button>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead><tr><th>Имя</th><th>Email</th><th>Тестов</th><th>Средний %</th><th>Вступил</th><th></th></tr></thead>
|
||||
<thead><tr><th>Имя</th><th>Email</th><th>Тестов</th><th>Средний %</th><th>Вступил</th><th title="Готовится к ЦТ — открывает карточки, курс и пробники ЦТ">ЦТ</th><th></th></tr></thead>
|
||||
<tbody id="d-members"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -1089,6 +1100,7 @@
|
||||
document.getElementById('d-name').textContent = d.name;
|
||||
document.getElementById('d-sub').innerHTML = esc(d.description || '') + ' · Код: <strong style="color:var(--violet);letter-spacing:0.05em;user-select:all">' + esc(d.invite_code) + '</strong> <button onclick="navigator.clipboard.writeText(\'' + esc(d.invite_code) + '\');LS.toast(\'Код скопирован\',\'success\')" style="background:none;border:1px solid var(--border);border-radius:6px;padding:2px 8px;font-size:0.72rem;cursor:pointer;color:var(--text-2);margin-left:4px" title="Копировать код"><svg class="ic" viewBox="0 0 24 24" style="width:12px;height:12px"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button>';
|
||||
renderMembers(d.members);
|
||||
loadPrep(d.id); // статус флага «готовится к ЦТ» → тумблеры в колонке ЦТ
|
||||
renderAssignments(d.assignments, d.members.length);
|
||||
renderDashboard(d);
|
||||
} catch (e) {
|
||||
@@ -1096,10 +1108,20 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Флаг «готовится к ЦТ»: трек ct-math открывает ученику карточки + курс + пробники. */
|
||||
const PREP_TRACK = 'ct-math';
|
||||
let _prepStatus = {}; // { studentId: 0|1 } для текущего класса
|
||||
|
||||
function prepToggleHtml(id) {
|
||||
const on = _prepStatus[id] === 1;
|
||||
return `<button class="prep-toggle${on ? ' on' : ''}" onclick="togglePrep(${id})" title="${on ? 'Готовится к ЦТ — нажмите, чтобы снять' : 'Отметить «готовится к ЦТ»'}">
|
||||
${on ? '<svg class="ic" viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12"/></svg>ЦТ' : '<svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/></svg>нет'}</button>`;
|
||||
}
|
||||
|
||||
function renderMembers(members) {
|
||||
const tbody = document.getElementById('d-members');
|
||||
if (!members.length) {
|
||||
tbody.innerHTML = '<tr><td colspan="6"><div class="empty">Нет учеников. Поделитесь кодом приглашения.</div></td></tr>';
|
||||
tbody.innerHTML = '<tr><td colspan="7"><div class="empty">Нет учеников. Поделитесь кодом приглашения.</div></td></tr>';
|
||||
return;
|
||||
}
|
||||
tbody.innerHTML = members.map(m => {
|
||||
@@ -1110,11 +1132,43 @@
|
||||
<td>${m.tests_count}</td>
|
||||
<td><span class="pct-cell ${pc}">${m.avg_pct!==null?m.avg_pct+'%':'—'}</span></td>
|
||||
<td style="color:var(--text-3);font-size:0.78rem">${fmtDate(m.joined_at)}</td>
|
||||
<td>${prepToggleHtml(m.id)}</td>
|
||||
<td><button class="btn-danger" onclick="kickMember(${m.id},'${esc(m.name)}')">Удалить</button></td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
/* Подгрузить статус флага для членов класса и перерисовать тумблеры. */
|
||||
async function loadPrep(classId) {
|
||||
try {
|
||||
const r = await LS.prepClassStatus(classId, PREP_TRACK);
|
||||
_prepStatus = {};
|
||||
(r.students || []).forEach(s => { _prepStatus[s.id] = s.prep ? 1 : 0; });
|
||||
if (currentClass && currentClass.members) renderMembers(currentClass.members);
|
||||
} catch (_) { /* нет прав/трека — тумблеры останутся в «нет» */ }
|
||||
}
|
||||
|
||||
async function togglePrep(studentId) {
|
||||
const on = _prepStatus[studentId] === 1;
|
||||
try {
|
||||
if (on) await LS.prepUnsetStudent(studentId, PREP_TRACK);
|
||||
else await LS.prepSetStudent(studentId, PREP_TRACK);
|
||||
_prepStatus[studentId] = on ? 0 : 1;
|
||||
if (currentClass && currentClass.members) renderMembers(currentClass.members);
|
||||
LS.toast(on ? 'Снято' : 'Готовится к ЦТ', 'success');
|
||||
} catch (e) { LS.toast(e.message || 'Ошибка', 'error'); }
|
||||
}
|
||||
|
||||
async function bulkPrep(on) {
|
||||
if (!currentClass) return;
|
||||
if (!confirm(on ? 'Отметить ВСЕХ учеников класса как готовящихся к ЦТ?' : 'Снять флаг ЦТ со ВСЕХ учеников класса?')) return;
|
||||
try {
|
||||
await LS.prepSetClass(currentClass.id, PREP_TRACK, on);
|
||||
await loadPrep(currentClass.id);
|
||||
LS.toast('Готово', 'success');
|
||||
} catch (e) { LS.toast(e.message || 'Ошибка', 'error'); }
|
||||
}
|
||||
|
||||
function renderAssignments(assignments, totalMembers) {
|
||||
_classAssignments = assignments;
|
||||
const el = document.getElementById('d-assignments');
|
||||
|
||||
Reference in New Issue
Block a user