26ba289019
- css/ls.css: --text-3 #8898AA → #56687A (5.1:1 contrast), min-height 44px on .btn-primary/.btn-ghost/.sb-link, new .icon-btn utility (44×44px) - js/api.js: lsConfirm — role=dialog, aria-modal, aria-labelledby, Tab focus trap, restore focus on close; lsToast — aria-live=polite on container, role=alert on errors; live quiz — role=dialog, role=radiogroup, role=radio, aria-checked, keyboard support - test-run.html: q-opt divs — role=radio/checkbox, aria-checked, tabindex, keyboard enter/space; confirm modal — role=dialog, aria-modal; btn-flag — aria-pressed; dots — aria-label, aria-current; touch targets 44px - board.html: btn-del-ann — aria-label; reaction buttons — aria-label, aria-pressed - All 18 HTML files: replace hardcoded color:#8898AA with color:var(--text-3) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
550 lines
25 KiB
HTML
550 lines
25 KiB
HTML
<!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: var(--text-3); 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: var(--text-3); 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: var(--text-3); 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: var(--text-3); 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: var(--text-3); }
|
||
|
||
/* 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: var(--text-3); }
|
||
.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" id="app-sidebar"></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:var(--text-3);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:var(--text-3);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/sidebar.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>Оценка (0–100)</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>
|