feat(materials): Фаза 6b — раздатка материала ученикам/классу

- POST /api/materials/:id/share {classId|userId} (teacher/admin): создаёт независимую КОПИЮ
  материала каждому ученику класса (source_title «Раздатка: <учитель>») + уведомление через SSE.
- /my-materials: кнопка «Раздать» на карточках (видна учителю/админу) → выбор класса.
- Хелпер LS.shareMaterial. На этом план «Мои материалы» (6 фаз) завершён.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-06-04 12:26:46 +03:00
parent e793b4ec09
commit f7357adf1e
4 changed files with 77 additions and 6 deletions
+29 -3
View File
@@ -79,7 +79,8 @@
<script src="/js/svg-sanitize.js"></script>
<script src="/js/svg-draw.js"></script>
<script>
LS.initPage();
const _ip = LS.initPage() || {};
const _canShare = !!(_ip.isTeacher || _ip.isAdmin);
function esc(s) { return LS.escapeHtml(String(s || '')); }
function fmtDate(s) {
@@ -110,6 +111,8 @@
? `<button class="mm-btn" onclick="annotate(${m.id})" title="Аннотировать (рисовать поверх)"><i data-lucide="pencil-ruler"></i></button>` : '';
const fc = (m.kind === 'note')
? `<button class="mm-btn" onclick="toFlashcard(${m.id})" title="В флешкарты"><i data-lucide="copy"></i></button>` : '';
const sh = _canShare
? `<button class="mm-btn" onclick="openShareModal(${m.id})" title="Раздать ученикам"><i data-lucide="send"></i></button>` : '';
const mv = moveSelect(m);
if (m.kind === 'board' || m.kind === 'image') {
return `<div class="mm-card">
@@ -122,7 +125,7 @@
${mv}
<a class="mm-btn" href="${esc(m.url)}" target="_blank" rel="noopener" title="Открыть"><i data-lucide="external-link"></i></a>
<a class="mm-btn" href="${esc(m.url)}" download title="Скачать"><i data-lucide="download"></i></a>
${ann}${edit}${del}
${ann}${sh}${edit}${del}
</div>
</div>
</div>`;
@@ -134,7 +137,7 @@
<div class="mm-card-body">
<div class="mm-card-title">${esc(m.title || kind)}</div>
<div class="mm-card-meta">${meta}</div>
<div class="mm-card-actions">${mv}${fc}${edit}${del}</div>
<div class="mm-card-actions">${mv}${fc}${sh}${edit}${del}</div>
</div>
</div>`;
}
@@ -385,6 +388,29 @@
}
window.toFlashcard = toFlashcard;
/* ── Раздатка: учитель → класс (копия ученикам) ── */
async function openShareModal(id) {
let classes = [];
try { classes = await LS.getClasses(); } catch (e) {}
if (!Array.isArray(classes) || !classes.length) { LS.toast('Нет классов для раздачи', 'warn'); return; }
const opts = classes.map(c => `<option value="${c.id}">${esc(c.name)}</option>`).join('');
const content = `<div style="display:flex;flex-direction:column;gap:8px">
<label style="font-size:.8rem;color:var(--text-3)">Класс</label>
<select id="sh-class" style="${FLD}">${opts}</select>
<div style="font-size:.78rem;color:var(--text-3)">Копия материала появится у всех учеников класса (с уведомлением).</div>
</div>`;
const m = LS.modal({ title: 'Раздать материал', content, size: 'sm', actions: [
{ label: 'Отмена', onClick: () => m.close() },
{ label: 'Раздать', primary: true, onClick: async () => {
try {
const r = await LS.shareMaterial(id, { classId: Number(m.body.querySelector('#sh-class').value) });
m.close(); LS.toast('Отправлено ученикам: ' + (r.sent || 0), 'success');
} catch (e) { LS.toast(e.message || 'Ошибка', 'error'); }
} },
] });
}
window.openShareModal = openShareModal;
load();
</script>
</body>