@@ -247,6 +292,14 @@
resubmitted: 'Повторно', accepted: 'Принято'
};
+ /* subject label/colour/icon maps (как на дашборде) */
+ const SUBJ = { bio:'Биология', chem:'Химия', math:'Математика', phys:'Физика', other:'Задание' };
+ const SUBJ_ICONS = { bio:'dna', chem:'flask-conical', math:'calculator', phys:'zap', other:'file-check' };
+ const SUBJ_COLORS = { bio:'#9B5DE5', chem:'#06D6A0', math:'#06B6D4', phys:'#F59E0B', other:'#7c3aed' };
+
+ let _assignments = []; // актуальные задания (LS.myAssignments)
+ let _subByAsgn = new Map(); // assignment_id -> последняя сдача
+
/* ── filter ── */
function filterStatus(st, btn) {
_statusFilter = st;
@@ -257,33 +310,31 @@
/* ── STUDENT VIEW ── */
async function initStudent() {
- document.getElementById('hw-sub').textContent = 'Сдавайте работы и отслеживайте оценки';
+ document.getElementById('hw-sub').textContent = 'Ваши актуальные задания и сданные работы';
document.getElementById('hw-top-student').style.display = '';
+ document.getElementById('hw-mysubs-title').style.display = '';
- // Find student's class
+ // 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();
+ const [assigns, subs] = await Promise.all([
+ LS.myAssignments().catch(() => []),
+ LS.getMySubmissions().catch(() => []),
+ ]);
+ _assignments = Array.isArray(assigns) ? assigns : [];
+ _submissions = Array.isArray(subs) ? subs : [];
+ _subByAsgn.clear();
+ _submissions.forEach(s => { if (s.assignment_id) _subByAsgn.set(s.assignment_id, s); });
+ populateAssignmentSelect(_assignments);
+ renderActiveAssignments();
renderSubmissions();
} catch {
document.getElementById('hw-list').innerHTML = '
';
@@ -312,14 +363,22 @@
}
async function submitHomework() {
- if (!_selectedFile || !_studentClassId) return;
+ if (!_selectedFile) return;
+ const sel = document.getElementById('hw-assignment-sel');
+ const assignId = sel.value;
+ // Класс берём от выбранного задания (важно для учеников в нескольких классах),
+ // иначе — первый класс ученика.
+ let classId = _studentClassId;
+ if (assignId && sel.selectedOptions[0] && sel.selectedOptions[0].dataset.class) {
+ classId = sel.selectedOptions[0].dataset.class;
+ }
+ if (!classId) { LS.toast('Вы не состоите в классе', 'error'); 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;
+ fd.append('class_id', classId);
if (assignId) fd.append('assignment_id', assignId);
const msg = document.getElementById('hw-message').value.trim();
if (msg) fd.append('message', msg);
@@ -336,12 +395,20 @@
// Reload
_submissions = await LS.getMySubmissions();
- renderSubmissions();
+ syncStudentLists();
} catch (e) {
LS.toast(e.message || 'Ошибка отправки', 'error');
} finally { btn.disabled = !_selectedFile; }
}
+ /* Пересобрать карту сдач и перерисовать обе студенческие секции. */
+ function syncStudentLists() {
+ _subByAsgn.clear();
+ _submissions.forEach(s => { if (s.assignment_id) _subByAsgn.set(s.assignment_id, s); });
+ renderActiveAssignments();
+ renderSubmissions();
+ }
+
async function resubmitHomework(subId) {
const input = document.createElement('input');
input.type = 'file';
@@ -355,7 +422,7 @@
await LS.resubmitWork(subId, fd);
LS.toast('Работа отправлена повторно!', 'success');
_submissions = await LS.getMySubmissions();
- renderSubmissions();
+ syncStudentLists();
} catch (e) { LS.toast(e.message || 'Ошибка', 'error'); }
};
input.click();
@@ -366,7 +433,7 @@
try {
await LS.deleteSubmission(id);
_submissions = _submissions.filter(s => s.id !== id);
- renderSubmissions();
+ syncStudentLists();
LS.toast('Удалено', 'info');
} catch (e) { LS.toast(e.message || 'Ошибка', 'error'); }
}
@@ -381,6 +448,167 @@
} catch (e) { LS.toast(e.message || 'Ошибка', 'error'); }
}
+ /* ══ АКТУАЛЬНЫЕ ЗАДАНИЯ (что нужно сделать) ══════════════════════════ */
+
+ // Тип задания — по тем же признакам, что и карточки дашборда.
+ function asgnType(a) {
+ if (a.textbook_id) return 'textbook';
+ if (a.file_id) return 'file';
+ if (a.is_homework && !a.session_id && (a.count == null || a.count <= 1)
+ && (!a.subject_slug || a.subject_slug === 'other')) return 'upload';
+ return 'test';
+ }
+
+ // «Закрыто» — задание уходит из актуальных.
+ function asgnDone(a) {
+ const t = asgnType(a);
+ if (t === 'textbook') return !!(a.textbook_all_read || a.completed_at);
+ if (t === 'upload' || t === 'file') {
+ const s = _subByAsgn.get(a.id);
+ return !!(s && s.status === 'accepted');
+ }
+ // test
+ const maxAtt = a.max_attempts || 0;
+ const usedAtt = a.attempts_used != null ? a.attempts_used : 0;
+ if (maxAtt > 0 && usedAtt >= maxAtt) return true;
+ return a.session_status === 'completed' && a.mode !== 'repeat';
+ }
+
+ // Сортировка по срочности (меньше — выше): идёт → просрочено → <24ч → по дедлайну → без срока.
+ function urgencyScore(a) {
+ if (a.session_status === 'in_progress') return -4;
+ const dlMs = a.deadline ? new Date(a.deadline) - Date.now() : Infinity;
+ if (dlMs < 0) return -3;
+ if (dlMs < 24 * 3600 * 1000) return -2;
+ if (dlMs < Infinity) return dlMs;
+ return 1e12;
+ }
+
+ function deadlineChip(a) {
+ if (!a.deadline) return '
Без срока';
+ const dlMs = new Date(a.deadline) - Date.now();
+ const date = new Date(a.deadline).toLocaleDateString('ru', { day: 'numeric', month: 'short' });
+ if (dlMs < 0) return `
Просрочено · ${date}`;
+ if (dlMs < 24 * 3600 * 1000) return `
Сегодня · до ${date}`;
+ const days = Math.ceil(dlMs / 86400000);
+ const txt = days === 1 ? '1 день' : `${days} дн.`;
+ return `
${txt} · до ${date}`;
+ }
+
+ function actionFor(a) {
+ const t = asgnType(a);
+ if (t === 'textbook') {
+ let hash = '';
+ if (a.textbook_paragraphs) { const m = String(a.textbook_paragraphs).match(/^\s*(\d+)/); if (m) hash = '#p' + m[1]; }
+ const href = `/textbook/${a.textbook_slug || ''}${hash}`;
+ return `
${(a.textbook_read_count || 0) > 0 ? 'Продолжить' : 'Открыть'}`;
+ }
+ if (t === 'file') {
+ const sub = _subByAsgn.get(a.id);
+ const submit = sub && sub.status !== 'revision'
+ ? `
${STATUS_LABELS[sub.status] || sub.status}`
+ : `
`;
+ return `
Скачать${submit}`;
+ }
+ if (t === 'upload') {
+ const sub = _subByAsgn.get(a.id);
+ if (sub && sub.status !== 'revision') {
+ return `
${STATUS_LABELS[sub.status] || sub.status}`;
+ }
+ return `
`;
+ }
+ // test
+ const inProgress = a.session_status === 'in_progress';
+ const isDone = a.session_status === 'completed';
+ const label = inProgress ? 'Продолжить' : (isDone && a.mode === 'repeat') ? 'Повторить' : 'Начать';
+ return `
`;
+ }
+
+ function activeCardHtml(a) {
+ const t = asgnType(a);
+ const color = SUBJ_COLORS[a.subject_slug] || (t === 'textbook' ? '#7c3aed' : '#9B5DE5');
+ const icon = t === 'textbook' ? 'book-open-text'
+ : t === 'file' ? 'paperclip'
+ : t === 'upload' ? 'upload'
+ : (SUBJ_ICONS[a.subject_slug] || 'file-text');
+ const dlMs = a.deadline ? new Date(a.deadline) - Date.now() : Infinity;
+ const over = dlMs < 0;
+ const urgent = !over && dlMs < 24 * 3600 * 1000;
+ const cls = over ? ' over' : urgent ? ' urgent' : '';
+ const classStr = a.class_id ? esc(a.class_name) : 'Личное';
+ const subjStr = SUBJ[a.subject_slug] || (t === 'textbook' ? 'Чтение' : '');
+ const meta = [classStr, subjStr].filter(Boolean).join(' · ');
+ return `
+
+
+
${esc(a.title)}
+
${meta ? `${meta}` : ''}${deadlineChip(a)}
+
+
${actionFor(a)}
+
`;
+ }
+
+ function renderActiveAssignments() {
+ const wrap = document.getElementById('hw-active-wrap');
+ const list = document.getElementById('hw-active-list');
+ if (!wrap || !list) return;
+ const active = _assignments.filter(a => !asgnDone(a)).sort((x, y) => urgencyScore(x) - urgencyScore(y));
+ if (!active.length) { wrap.style.display = 'none'; return; }
+ wrap.style.display = '';
+ document.getElementById('hw-active-count').textContent = active.length;
+ list.innerHTML = active.map(activeCardHtml).join('');
+ lucide.createIcons();
+ }
+
+ // Наполнить выпадашку «Задание» при загрузке работы — по ВСЕМ классам ученика.
+ function populateAssignmentSelect(list) {
+ const sel = document.getElementById('hw-assignment-sel');
+ if (!sel) return;
+ sel.querySelectorAll('option[data-asgn]').forEach(o => o.remove());
+ list.filter(a => !a.session_id && !a.textbook_id).forEach(a => {
+ const opt = document.createElement('option');
+ opt.value = a.id;
+ opt.textContent = a.title + (a.class_name && a.class_name !== 'Личное задание' ? ' · ' + a.class_name : '');
+ opt.dataset.asgn = '1';
+ if (a.class_id) opt.dataset.class = a.class_id;
+ sel.appendChild(opt);
+ });
+ }
+
+ // Начать/продолжить тест-задание (как на дашборде).
+ async function startAsgn(e, id, mode) {
+ const btn = e.currentTarget;
+ const orig = btn.textContent;
+ btn.disabled = true; btn.textContent = '…';
+ try {
+ const r = await LS.startAssignment(id);
+ if (r.error && r.max_attempts) {
+ LS.toast(`Исчерпан лимит попыток (${r.attempts_used}/${r.max_attempts})`, 'warn');
+ btn.disabled = false; btn.textContent = orig; return;
+ }
+ const aMode = r.assignment_mode || mode || 'exam';
+ if (r.status === 'completed' && aMode !== 'repeat') location.href = `/test-result?session=${r.session_id}`;
+ else location.href = `/test-run?session=${r.session_id}&assignment_mode=${aMode}`;
+ } catch (err) {
+ const isLimit = err.message && (err.message.includes('лимит') || err.message.includes('Исчерпан'));
+ LS.toast(isLimit ? err.message : ('Ошибка: ' + err.message), isLimit ? 'warn' : 'error');
+ btn.disabled = false; btn.textContent = orig;
+ }
+ }
+
+ // «Сдать» из карточки → прокрутить к области загрузки и преднабрать задание.
+ function sdatNow(assignId) {
+ const wrap = document.getElementById('hw-upload-wrap');
+ if (!wrap || wrap.style.display === 'none') {
+ LS.toast('Загрузка работ доступна участникам класса', 'warn'); return;
+ }
+ const sel = document.getElementById('hw-assignment-sel');
+ if (sel && [...sel.options].some(o => o.value == assignId)) sel.value = String(assignId);
+ wrap.scrollIntoView({ behavior: 'smooth', block: 'start' });
+ const area = document.getElementById('hw-upload-area');
+ if (area) { area.classList.add('dragover'); setTimeout(() => area.classList.remove('dragover'), 1200); }
+ }
+
/* ── TEACHER VIEW ── */
async function initTeacher() {
document.getElementById('hw-sub').textContent = 'Проверяйте работы учеников и ставьте оценки';