Files
Learn_System/frontend/homework.html
T
Maxim Dolgolyov be4d43105e LearnSpace: full-stack educational whiteboard platform
Node.js/Express backend + vanilla JS frontend.
Features: real-time collaborative whiteboard (SSE), multi-page support,
LaTeX formulas, shapes/connectors, coordinate systems, number lines,
compass, zoom/pan, Catmull-Rom pencil smoothing, ruler/protractor with
rotation & resize controls, minimap navigation overlay, auto-measurements,
multi-page thumbnails sidebar, PNG export, page templates.
Student/teacher workflows: classes, assignments, library, dashboard.
Mobile responsive. SQLite (better-sqlite3).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-12 10:10:37 +03:00

594 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>
<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>