Files
Learn_System/frontend/homework.html
T
Maxim Dolgolyov fd29acbbdd feat: WebSocket real-time + rAF render gate + guest board + screen picker
Classroom performance:
- WebSocket server (ws-server.js) for low-latency cursor & stroke preview
  Replaces HTTP POST per event → eliminates per-message auth overhead
  Session member cache (30s TTL) avoids SQLite query per WS message
  Fallback to HTTP POST when WS not connected
- Cursor throttle reduced 100ms → 33ms (~30fps)
- Stroke preview throttle reduced 50ms → 20ms
- whiteboard.js: render() is now rAF-gated (_doRender/_rafPending)
  Multiple render() calls within one frame collapse into one repaint
  document.hidden check — zero CPU when tab is in background
  visibilitychange listener restores canvas on tab focus

Guest board:
- guestClassroom.js route: public token-based read-only access
- guest-board.html: name entry + read-only whiteboard + SSE
- SSE: addGuestClient/removeGuestClient/emitToGuests

Screen share picker:
- Discord-style modal with tab switching (screen/window/tab)
- Live video preview before confirming share
- useExistingScreenStream() in ClassroomRTC

Fullscreen exit overlay:
- #cr-fs-exit-overlay button inside cr-board-wrap
- Visible only via CSS :fullscreen selector (touchpad users)

File sharing from library:
- Teacher picks file from library, sends as styled card in chat
- crDownloadLibraryFile() fetches with Bearer auth

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 18:04:59 +03:00

595 lines
29 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Домашние задания — LearnSpace</title>
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<link href="https://fonts.googleapis.com/css2?family=Unbounded:wght@400;700;800&family=Manrope:wght@400;500;600;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="/css/ls.css" />
<script src="https://cdn.jsdelivr.net/npm/lucide@0.469.0/dist/umd/lucide.min.js"></script>
<style>
.sb-content { background: #f4f5f8; }
.container { max-width: 960px; margin: 0 auto; padding: 28px 32px 100px; }
.page-title {
font-family: 'Unbounded', sans-serif; font-size: 1.1rem; font-weight: 800;
color: #0F172A; margin-bottom: 6px;
}
.page-sub { font-size: 0.82rem; color: #8898AA; margin-bottom: 24px; }
/* ── top bar with class selector ── */
.hw-top { display: flex; align-items: center; gap: 14px; margin-bottom: 24px; flex-wrap: wrap; }
.hw-class-sel {
padding: 8px 14px; border-radius: 12px; border: 1.5px solid rgba(15,23,42,0.12);
background: #fff; font-family: 'Manrope', sans-serif; font-size: 0.82rem;
font-weight: 600; color: #0F172A; cursor: pointer; min-width: 180px;
}
.hw-status-filters { display: flex; gap: 4px; flex-wrap: wrap; }
.hw-sf-btn {
padding: 6px 14px; border-radius: 999px; border: 1.5px solid rgba(15,23,42,0.1);
background: transparent; font-family: 'Manrope', sans-serif; font-size: 0.75rem;
font-weight: 600; color: #8898AA; cursor: pointer; transition: all .15s;
}
.hw-sf-btn:hover { border-color: var(--violet); color: var(--violet); }
.hw-sf-btn.active { background: rgba(155,93,229,0.08); border-color: var(--violet); color: var(--violet); }
/* ── submission cards ── */
.hw-list { display: flex; flex-direction: column; gap: 10px; }
.hw-card {
background: #fff; border-radius: 16px; padding: 18px 20px;
border: 1px solid rgba(15,23,42,0.06);
display: flex; align-items: flex-start; gap: 14px;
transition: all .15s;
}
.hw-card:hover { border-color: rgba(155,93,229,0.15); box-shadow: 0 2px 12px rgba(15,23,42,0.05); }
.hw-card-icon {
width: 42px; height: 42px; border-radius: 12px; display: flex;
align-items: center; justify-content: center; flex-shrink: 0;
}
.hw-card-body { flex: 1; min-width: 0; }
.hw-card-title { font-size: 0.88rem; font-weight: 700; color: #0F172A; margin-bottom: 4px; }
.hw-card-meta { font-size: 0.75rem; color: #8898AA; display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 6px; }
.hw-card-note { font-size: 0.78rem; color: #3D4F6B; background: rgba(15,23,42,0.03); padding: 8px 12px; border-radius: 10px; margin-top: 8px; line-height: 1.5; }
.hw-card-note strong { color: #0F172A; }
.hw-card-actions { display: flex; gap: 8px; margin-top: 10px; flex-wrap: wrap; }
.hw-card-right { display: flex; flex-direction: column; align-items: flex-end; gap: 6px; flex-shrink: 0; }
/* status badges */
.hw-badge {
padding: 4px 10px; border-radius: 999px; font-size: 0.68rem; font-weight: 700;
text-transform: uppercase; letter-spacing: 0.03em; white-space: nowrap;
}
.hw-badge-new { background: rgba(6,214,224,0.12); color: #06B6D4; }
.hw-badge-reviewed { background: rgba(155,93,229,0.12); color: #9B5DE5; }
.hw-badge-revision { background: rgba(245,158,11,0.12); color: #F59E0B; }
.hw-badge-resubmitted{ background: rgba(6,214,100,0.12); color: #06D664; }
.hw-badge-accepted { background: rgba(16,185,129,0.12); color: #10B981; }
.hw-grade {
font-family: 'Unbounded', sans-serif; font-size: 1rem; font-weight: 800;
padding: 4px 10px; border-radius: 10px; min-width: 50px; text-align: center;
}
.hw-grade-high { background: rgba(16,185,129,0.1); color: #10B981; }
.hw-grade-mid { background: rgba(245,158,11,0.1); color: #F59E0B; }
.hw-grade-low { background: rgba(239,71,111,0.1); color: #EF476F; }
/* action buttons */
.hw-btn {
padding: 6px 14px; border-radius: 10px; border: 1.5px solid rgba(15,23,42,0.12);
background: #fff; font-family: 'Manrope', sans-serif; font-size: 0.75rem;
font-weight: 600; color: #3D4F6B; cursor: pointer; transition: all .15s;
display: flex; align-items: center; gap: 5px;
}
.hw-btn:hover { border-color: var(--violet); color: var(--violet); }
.hw-btn-primary { background: var(--grad-1); color: #fff; border-color: transparent; }
.hw-btn-primary:hover { opacity: .88; color: #fff; }
.hw-btn-danger { border-color: rgba(239,71,111,0.2); color: #EF476F; }
.hw-btn-danger:hover { background: rgba(239,71,111,0.06); }
.hw-btn-accept { border-color: rgba(16,185,129,0.3); color: #10B981; }
.hw-btn-accept:hover { background: rgba(16,185,129,0.06); }
.hw-btn-revision { border-color: rgba(245,158,11,0.3); color: #F59E0B; }
.hw-btn-revision:hover { background: rgba(245,158,11,0.06); }
/* upload area */
.hw-upload-area {
background: #fff; border-radius: 16px; border: 2px dashed rgba(15,23,42,0.12);
padding: 28px; text-align: center; margin-bottom: 24px;
transition: border-color .15s, background .15s; cursor: pointer;
}
.hw-upload-area:hover, .hw-upload-area.dragover {
border-color: var(--violet); background: rgba(155,93,229,0.02);
}
.hw-upload-icon { color: #8898AA; margin-bottom: 8px; }
.hw-upload-text { font-size: 0.85rem; font-weight: 600; color: #3D4F6B; margin-bottom: 4px; }
.hw-upload-hint { font-size: 0.72rem; color: #8898AA; }
/* review modal (inline) */
.hw-review-panel {
background: rgba(255,255,255,0.95); border: 1.5px solid rgba(15,23,42,0.08);
border-radius: 14px; padding: 16px; margin-top: 10px;
}
.hw-review-panel label { font-size: 0.75rem; font-weight: 700; color: #3D4F6B; display: block; margin-bottom: 4px; }
.hw-review-panel textarea, .hw-review-panel input {
width: 100%; padding: 8px 12px; border: 1.5px solid rgba(15,23,42,0.1); border-radius: 10px;
font-family: 'Manrope', sans-serif; font-size: 0.82rem; margin-bottom: 10px;
outline: none; transition: border-color .15s;
}
.hw-review-panel textarea:focus, .hw-review-panel input:focus { border-color: var(--violet); }
.hw-review-panel textarea { min-height: 60px; resize: vertical; }
/* empty state */
.hw-empty { text-align: center; padding: 60px 20px; color: #8898AA; }
.hw-empty-icon { font-size: 3rem; margin-bottom: 12px; opacity: 0.3; }
.hw-empty-text { font-size: 0.88rem; font-weight: 600; }
/* student name in teacher view */
.hw-student-name { font-size: 0.78rem; font-weight: 700; color: var(--violet); }
@media (max-width: 768px) {
.container { padding: 16px 14px 80px; }
.hw-top { gap: 8px; }
.hw-class-sel { flex: 1; min-width: 0; }
.hw-card { flex-direction: column; gap: 10px; }
.hw-card-right { flex-direction: row; align-items: center; justify-content: flex-start; width: 100%; }
.hw-card-actions { flex-wrap: wrap; }
.hw-upload-area { padding: 20px 16px; }
}
@media (max-width: 480px) {
.container { padding: 12px 10px 80px; }
.hw-status-filters { gap: 4px; }
.hw-sf-btn { font-size: 0.72rem; padding: 5px 10px; }
.hw-card { padding: 14px 14px; }
.hw-btn { font-size: 0.72rem; padding: 5px 10px; }
}
</style>
</head>
<body>
<div class="app-layout">
<aside class="sidebar">
<div class="sb-brand">
<a href="/dashboard" class="sb-logo"><span class="sb-lbl">Learn<span>Space</span></span></a>
<button class="sb-toggle" title="Свернуть меню"><i data-lucide="chevron-left" class="sb-icon"></i></button>
</div>
<nav class="sb-nav">
<button class="sb-link" onclick="lsSearchOpen()" title="Ctrl+K"><i data-lucide="search" class="sb-icon"></i><span class="sb-lbl">Поиск</span></button>
<a href="/dashboard" class="sb-link"><i data-lucide="home" class="sb-icon"></i><span class="sb-lbl">Дашборд</span></a>
<a href="/board" class="sb-link" id="btn-board" style="display:none"><i data-lucide="layout-dashboard" class="sb-icon"></i><span class="sb-lbl">Доска</span></a>
<a href="/classes" class="sb-link" id="btn-classes" style="display:none"><i data-lucide="graduation-cap" class="sb-icon"></i><span class="sb-lbl">Классы</span></a>
<a href="/library" class="sb-link"><i data-lucide="book-open" class="sb-icon"></i><span class="sb-lbl">Библиотека</span></a>
<a href="/theory" class="sb-link"><i data-lucide="brain" class="sb-icon"></i><span class="sb-lbl">Теория</span></a>
<a href="/lab" class="sb-link"><i data-lucide="atom" class="sb-icon"></i><span class="sb-lbl">Лаборатория</span></a>
<a href="/biochem" class="sb-link"><i data-lucide="flask-conical" class="sb-icon"></i><span class="sb-lbl">Биохимия</span></a>
<a href="/hangman" class="sb-link"><i data-lucide="gamepad-2" class="sb-icon"></i><span class="sb-lbl">Виселица</span></a>
<a href="/crossword" class="sb-link"><i data-lucide="grid-3x3" class="sb-icon"></i><span class="sb-lbl">Кроссворд</span></a>
<a href="/pet" class="sb-link"><i data-lucide="heart" class="sb-icon"></i><span class="sb-lbl">Питомец</span></a>
<a href="/collection" class="sb-link"><i data-lucide="layers" class="sb-icon"></i><span class="sb-lbl">Коллекция</span></a>
<a href="/knowledge-map" class="sb-link"><i data-lucide="share-2" class="sb-icon"></i><span class="sb-lbl">Карта знаний</span></a>
<a href="/red-book.html" class="sb-link"><i data-lucide="leaf" class="sb-icon"></i><span class="sb-lbl">Красная книга</span></a>
<a href="/classroom" class="sb-link"><i data-lucide="presentation" class="sb-icon"></i><span class="sb-lbl">Онлайн-урок</span></a>
<a href="/lesson-history" class="sb-link"><i data-lucide="archive" class="sb-icon"></i><span class="sb-lbl">Архив уроков</span></a>
<div class="sb-divider"></div>
<a href="/analytics" class="sb-link sb-teacher-only" style="display:none"><i data-lucide="bar-chart-2" class="sb-icon"></i><span class="sb-lbl">Аналитика</span></a>
<a href="/question-bank" class="sb-link sb-teacher-only" style="display:none"><i data-lucide="database" class="sb-icon"></i><span class="sb-lbl">Банк вопросов</span></a>
<a href="/live-quiz" class="sb-link sb-teacher-only" style="display:none"><i data-lucide="radio" class="sb-icon"></i><span class="sb-lbl">Live-квиз</span></a>
<a href="/gradebook" class="sb-link sb-teacher-only" style="display:none"><i data-lucide="table" class="sb-icon"></i><span class="sb-lbl">Журнал</span></a>
<a href="/admin" class="sb-link" id="btn-admin" style="display:none"><i data-lucide="settings" class="sb-icon"></i><span class="sb-lbl">Управление</span></a>
</nav>
<div style="padding: 4px 2px">
<div id="notif-wrap">
<button class="sb-link" id="notif-btn" onclick="LS.notif.toggle()">
<i data-lucide="bell" class="sb-icon"></i><span class="sb-lbl">Уведомления</span>
<span class="sb-badge" id="notif-badge" style="display:none"></span>
</button>
</div>
</div>
<div class="sb-foot">
<a href="/profile" class="sb-user-row" style="text-decoration:none">
<div class="sb-avatar" id="nav-avatar">?</div>
<div class="sb-user-info">
<div class="sb-user-name" id="nav-user"></div>
<span class="sb-logout" style="pointer-events:none">Мой профиль</span>
</div>
</a>
</div>
</aside>
<div class="notif-drop" id="notif-drop"></div>
<div class="sb-content">
<div class="container">
<div class="page-title">Домашние задания</div>
<div class="page-sub" id="hw-sub">Загрузка…</div>
<!-- Student: upload area -->
<div id="hw-upload-wrap" style="display:none">
<div class="hw-upload-area" id="hw-upload-area" onclick="document.getElementById('hw-file-input').click()">
<div class="hw-upload-icon"><i data-lucide="upload-cloud" style="width:36px;height:36px"></i></div>
<div class="hw-upload-text">Загрузить работу</div>
<div class="hw-upload-hint">PDF, Word, изображения · до 50 МБ</div>
<div class="hw-upload-hint" id="hw-selected-file" style="color:var(--violet);font-weight:600;margin-top:6px"></div>
</div>
<input type="file" id="hw-file-input" style="display:none"
accept=".pdf,.doc,.docx,.ppt,.pptx,.xls,.xlsx,.png,.jpg,.jpeg,.gif,.webp,.txt" />
<div style="display:flex;gap:10px;margin-bottom:24px;flex-wrap:wrap;align-items:flex-end">
<div style="flex:1;min-width:200px">
<label style="font-size:.72rem;font-weight:700;color:#8898AA;display:block;margin-bottom:4px">Задание (необязательно)</label>
<select id="hw-assignment-sel" class="hw-class-sel" style="width:100%">
<option value="">— Без привязки к заданию —</option>
</select>
</div>
<div style="flex:1;min-width:200px">
<label style="font-size:.72rem;font-weight:700;color:#8898AA;display:block;margin-bottom:4px">Комментарий</label>
<input type="text" id="hw-message" class="hw-class-sel" style="width:100%" placeholder="Опишите работу…" />
</div>
<button class="hw-btn hw-btn-primary" id="hw-submit-btn" onclick="submitHomework()" disabled>
<i data-lucide="send" style="width:13px;height:13px"></i> Отправить
</button>
</div>
</div>
<!-- Teacher: class selector -->
<div class="hw-top" id="hw-top-teacher" style="display:none">
<select class="hw-class-sel" id="hw-class-sel" onchange="loadTeacherSubmissions()">
<option value="">Выберите класс</option>
</select>
<div class="hw-status-filters">
<button class="hw-sf-btn active" onclick="filterStatus(null,this)">Все</button>
<button class="hw-sf-btn" onclick="filterStatus('new',this)">Новые</button>
<button class="hw-sf-btn" onclick="filterStatus('resubmitted',this)">Повторные</button>
<button class="hw-sf-btn" onclick="filterStatus('revision',this)">На доработке</button>
<button class="hw-sf-btn" onclick="filterStatus('reviewed',this)">Проверены</button>
<button class="hw-sf-btn" onclick="filterStatus('accepted',this)">Приняты</button>
</div>
</div>
<!-- Student: status filters -->
<div class="hw-top" id="hw-top-student" style="display:none">
<div class="hw-status-filters">
<button class="hw-sf-btn active" onclick="filterStatus(null,this)">Все</button>
<button class="hw-sf-btn" onclick="filterStatus('new',this)">Отправлено</button>
<button class="hw-sf-btn" onclick="filterStatus('revision',this)">На доработке</button>
<button class="hw-sf-btn" onclick="filterStatus('reviewed',this)">Проверено</button>
<button class="hw-sf-btn" onclick="filterStatus('accepted',this)">Принято</button>
</div>
</div>
<div class="hw-list" id="hw-list"></div>
</div>
</div>
</div>
<script src="/js/api.js"></script>
<script src="/js/notifications.js"></script>
<script>
const { user, isTeacher, isAdmin } = LS.initPage();
if (!user) throw new Error('Not logged in');
function fmtDate(s) {
if (!s) return '—';
const d = new Date(s.includes('T') ? s : s.replace(' ','T')+'Z');
return d.toLocaleDateString('ru',{day:'numeric',month:'short'}) + ' ' + d.toLocaleTimeString('ru',{hour:'2-digit',minute:'2-digit'});
}
function fmtSize(b) {
if (b < 1024) return b + ' Б';
if (b < 1024*1024) return (b/1024).toFixed(1) + ' КБ';
return (b/1024/1024).toFixed(1) + ' МБ';
}
if (isAdmin) {
const btnAdmin = document.getElementById('btn-admin');
if (btnAdmin) btnAdmin.style.display = '';
}
LS.showBoardIfAllowed();
LS.notif.init();
/* ── state ── */
let _submissions = [];
let _statusFilter = null;
let _selectedFile = null;
let _studentClassId = null;
const STATUS_LABELS = {
new: 'Новое', reviewed: 'Проверено', revision: 'На доработке',
resubmitted: 'Повторно', accepted: 'Принято'
};
/* ── filter ── */
function filterStatus(st, btn) {
_statusFilter = st;
document.querySelectorAll('.hw-sf-btn').forEach(b => b.classList.remove('active'));
if (btn) btn.classList.add('active');
renderSubmissions();
}
/* ── STUDENT VIEW ── */
async function initStudent() {
document.getElementById('hw-sub').textContent = 'Сдавайте работы и отслеживайте оценки';
document.getElementById('hw-top-student').style.display = '';
// Find student's class
try {
const classes = await LS.myClasses();
if (classes.length) {
_studentClassId = classes[0].id;
document.getElementById('hw-upload-wrap').style.display = '';
// Load assignments for selector
try {
const feed = await LS.classFeed(classes[0].id);
const sel = document.getElementById('hw-assignment-sel');
(feed.assignments || []).forEach(a => {
const opt = document.createElement('option');
opt.value = a.id;
opt.textContent = a.title;
sel.appendChild(opt);
});
} catch {}
}
} catch {}
// Load submissions
try {
_submissions = await LS.getMySubmissions();
renderSubmissions();
} catch {
document.getElementById('hw-list').innerHTML = '<div class="hw-empty"><div class="hw-empty-text">Ошибка загрузки</div></div>';
}
// File input
const fileInput = document.getElementById('hw-file-input');
fileInput.addEventListener('change', () => {
_selectedFile = fileInput.files[0] || null;
document.getElementById('hw-selected-file').textContent = _selectedFile ? _selectedFile.name : '';
document.getElementById('hw-submit-btn').disabled = !_selectedFile;
});
// Drag and drop
const area = document.getElementById('hw-upload-area');
area.addEventListener('dragover', e => { e.preventDefault(); area.classList.add('dragover'); });
area.addEventListener('dragleave', () => area.classList.remove('dragover'));
area.addEventListener('drop', e => {
e.preventDefault(); area.classList.remove('dragover');
if (e.dataTransfer.files.length) {
_selectedFile = e.dataTransfer.files[0];
document.getElementById('hw-selected-file').textContent = _selectedFile.name;
document.getElementById('hw-submit-btn').disabled = false;
}
});
}
async function submitHomework() {
if (!_selectedFile || !_studentClassId) return;
const btn = document.getElementById('hw-submit-btn');
btn.disabled = true;
try {
const fd = new FormData();
fd.append('file', _selectedFile);
fd.append('class_id', _studentClassId);
const assignId = document.getElementById('hw-assignment-sel').value;
if (assignId) fd.append('assignment_id', assignId);
const msg = document.getElementById('hw-message').value.trim();
if (msg) fd.append('message', msg);
await LS.submitWork(fd);
LS.toast('Работа отправлена!', 'success');
// Reset
_selectedFile = null;
document.getElementById('hw-file-input').value = '';
document.getElementById('hw-selected-file').textContent = '';
document.getElementById('hw-message').value = '';
document.getElementById('hw-assignment-sel').value = '';
// Reload
_submissions = await LS.getMySubmissions();
renderSubmissions();
} catch (e) {
LS.toast(e.message || 'Ошибка отправки', 'error');
} finally { btn.disabled = !_selectedFile; }
}
async function resubmitHomework(subId) {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.pdf,.doc,.docx,.ppt,.pptx,.xls,.xlsx,.png,.jpg,.jpeg,.gif,.webp,.txt';
input.onchange = async () => {
const file = input.files[0];
if (!file) return;
try {
const fd = new FormData();
fd.append('file', file);
await LS.resubmitWork(subId, fd);
LS.toast('Работа отправлена повторно!', 'success');
_submissions = await LS.getMySubmissions();
renderSubmissions();
} catch (e) { LS.toast(e.message || 'Ошибка', 'error'); }
};
input.click();
}
async function deleteHomework(id) {
if (!await LS.confirm('Удалить эту работу?', { title: 'Удаление', confirmText: 'Удалить' })) return;
try {
await LS.deleteSubmission(id);
_submissions = _submissions.filter(s => s.id !== id);
renderSubmissions();
LS.toast('Удалено', 'info');
} catch (e) { LS.toast(e.message || 'Ошибка', 'error'); }
}
async function deleteSubmissionAdmin(id) {
if (!await LS.confirm('Удалить работу ученика? Это действие будет записано в журнал.', { title: 'Удаление работы', confirmText: 'Удалить', danger: true })) return;
try {
await LS.deleteSubmission(id);
_submissions = _submissions.filter(s => s.id !== id);
renderSubmissions();
LS.toast('Работа удалена', 'info');
} catch (e) { LS.toast(e.message || 'Ошибка', 'error'); }
}
/* ── TEACHER VIEW ── */
async function initTeacher() {
document.getElementById('hw-sub').textContent = 'Проверяйте работы учеников и ставьте оценки';
document.getElementById('hw-top-teacher').style.display = '';
try {
const classes = await LS.getClasses();
const sel = document.getElementById('hw-class-sel');
classes.forEach(c => {
const opt = document.createElement('option');
opt.value = c.id;
opt.textContent = c.name;
sel.appendChild(opt);
});
if (classes.length) {
sel.value = classes[0].id;
loadTeacherSubmissions();
}
} catch {}
}
async function loadTeacherSubmissions() {
const classId = document.getElementById('hw-class-sel').value;
if (!classId) { _submissions = []; renderSubmissions(); return; }
document.getElementById('hw-list').innerHTML = LS.skeleton(3);
try {
_submissions = await LS.getClassSubmissions(classId);
renderSubmissions();
} catch {
document.getElementById('hw-list').innerHTML = '<div class="hw-empty"><div class="hw-empty-text">Ошибка загрузки</div></div>';
}
}
function openReviewPanel(subId) {
const el = document.getElementById('review-' + subId);
if (el) { el.style.display = el.style.display === 'none' ? '' : 'none'; return; }
}
async function submitReview(subId, action) {
const noteEl = document.getElementById('note-' + subId);
const gradeEl = document.getElementById('grade-' + subId);
const teacher_note = noteEl?.value?.trim() || undefined;
const grade = gradeEl?.value ? Number(gradeEl.value) : undefined;
let status;
if (action === 'accept') status = 'accepted';
else if (action === 'revision') status = 'revision';
else status = 'reviewed';
try {
await LS.reviewSubmission(subId, { status, teacher_note, grade });
LS.toast(action === 'accept' ? 'Работа принята!' : action === 'revision' ? 'Отправлено на доработку' : 'Проверено', 'success');
loadTeacherSubmissions();
} catch (e) { LS.toast(e.message || 'Ошибка', 'error'); }
}
/* ── RENDER ── */
function renderSubmissions() {
const list = document.getElementById('hw-list');
const filtered = _statusFilter ? _submissions.filter(s => s.status === _statusFilter) : _submissions;
if (!filtered.length) {
list.innerHTML = `<div class="hw-empty">
<div class="hw-empty-icon"><i data-lucide="inbox" style="width:48px;height:48px"></i></div>
<div class="hw-empty-text">${_submissions.length ? 'Нет работ с этим статусом' : 'Работ пока нет'}</div>
</div>`;
lucide.createIcons();
return;
}
list.innerHTML = filtered.map(s => {
const badge = `<span class="hw-badge hw-badge-${s.status}">${STATUS_LABELS[s.status] || s.status}</span>`;
const gradeHtml = s.grade != null
? `<div class="hw-grade ${s.grade >= 80 ? 'hw-grade-high' : s.grade >= 50 ? 'hw-grade-mid' : 'hw-grade-low'}">${s.grade}</div>`
: '';
const iconBg = s.status === 'accepted' ? 'rgba(16,185,129,0.1)' :
s.status === 'revision' ? 'rgba(245,158,11,0.1)' :
'rgba(155,93,229,0.1)';
const iconColor = s.status === 'accepted' ? '#10B981' :
s.status === 'revision' ? '#F59E0B' : '#9B5DE5';
const iconName = s.status === 'accepted' ? 'check-circle' :
s.status === 'revision' ? 'alert-circle' : 'file-text';
let noteHtml = '';
if (s.teacher_note) noteHtml = `<div class="hw-card-note"><strong>Учитель:</strong> ${esc(s.teacher_note)}</div>`;
if (s.message) noteHtml += `<div class="hw-card-note" style="margin-top:6px"><strong>Комментарий:</strong> ${esc(s.message)}</div>`;
let actionsHtml = '';
if (isTeacher || isAdmin) {
actionsHtml = `
<div class="hw-card-actions">
<a class="hw-btn" href="${LS.submissionDownloadUrl(s.id)}" target="_blank">
<i data-lucide="download" style="width:12px;height:12px"></i> Скачать
</a>
<button class="hw-btn" onclick="openReviewPanel(${s.id})">
<i data-lucide="pencil" style="width:12px;height:12px"></i> Проверить
</button>
<button class="hw-btn hw-btn-danger" onclick="deleteSubmissionAdmin(${s.id})">
<i data-lucide="trash-2" style="width:12px;height:12px"></i>
</button>
</div>
<div class="hw-review-panel" id="review-${s.id}" style="display:none">
<label>Комментарий</label>
<textarea id="note-${s.id}" placeholder="Замечания, рекомендации…">${esc(s.teacher_note || '')}</textarea>
<label>Оценка (0100)</label>
<input type="number" id="grade-${s.id}" min="0" max="100" value="${s.grade != null ? s.grade : ''}" placeholder="—" />
<div style="display:flex;gap:8px;flex-wrap:wrap">
<button class="hw-btn hw-btn-accept" onclick="submitReview(${s.id},'accept')">
<i data-lucide="check" style="width:12px;height:12px"></i> Принять
</button>
<button class="hw-btn hw-btn-revision" onclick="submitReview(${s.id},'revision')">
<i data-lucide="rotate-ccw" style="width:12px;height:12px"></i> На доработку
</button>
<button class="hw-btn" onclick="submitReview(${s.id},'reviewed')">Проверено</button>
</div>
</div>`;
} else {
// Student actions
const canResubmit = s.status === 'revision';
const canDelete = ['new', 'revision', 'resubmitted'].includes(s.status);
actionsHtml = `<div class="hw-card-actions">
<a class="hw-btn" href="${LS.submissionDownloadUrl(s.id)}" target="_blank">
<i data-lucide="download" style="width:12px;height:12px"></i> Скачать
</a>
${canResubmit ? `<button class="hw-btn hw-btn-primary" onclick="resubmitHomework(${s.id})"><i data-lucide="upload" style="width:12px;height:12px"></i> Пересдать</button>` : ''}
${canDelete ? `<button class="hw-btn hw-btn-danger" onclick="deleteHomework(${s.id})"><i data-lucide="trash-2" style="width:12px;height:12px"></i></button>` : ''}
</div>`;
}
const studentLine = (isTeacher || isAdmin) && s.student_name
? `<span class="hw-student-name">${esc(s.student_name)}</span> · `
: '';
return `<div class="hw-card">
<div class="hw-card-icon" style="background:${iconBg};color:${iconColor}">
<i data-lucide="${iconName}" style="width:20px;height:20px"></i>
</div>
<div class="hw-card-body">
<div class="hw-card-title">${esc(s.assignment_title || s.original_name)}</div>
<div class="hw-card-meta">
<span>${studentLine}${fmtDate(s.submitted_at)}</span>
<span>${esc(s.original_name)} · ${fmtSize(s.size || 0)}</span>
</div>
${noteHtml}
${actionsHtml}
</div>
<div class="hw-card-right">
${badge}
${gradeHtml}
</div>
</div>`;
}).join('');
lucide.createIcons();
}
/* ── init ── */
if (isTeacher || isAdmin) initTeacher();
else initStudent();
if (window.lucide) lucide.createIcons();
</script>
<script src="/js/search.js"></script>
<script src="/js/mobile.js"></script>
</body>
</html>