From 0d4c658d93509e7788d431afd036ad6a34eafbbe Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Tue, 23 Jun 2026 14:09:34 +0300 Subject: [PATCH] =?UTF-8?q?refactor(assignments):=20=D0=B5=D0=B4=D0=B8?= =?UTF-8?q?=D0=BD=D1=8B=D0=B9=20=D0=BC=D0=BE=D0=B4=D1=83=D0=BB=D1=8C=20ass?= =?UTF-8?q?ignment-utils.js=20(=D1=82=D0=B8=D0=BF/=C2=AB=D1=81=D0=B4=D0=B0?= =?UTF-8?q?=D0=BD=D0=BE=C2=BB/=D1=81=D1=80=D0=BE=D1=87=D0=BD=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D1=8C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Логика классификации типа задания и статуса «сдано» дублировалась в трёх местах (dashboard.html, homework.html, assignmentController.js) и начала расходиться. Вынес в один UMD-модуль frontend/js/assignment-utils.js (грузится и в браузере, и в Node через require — как svg-sanitize.js): type(a), isDone(a, sub, opts), urgencyScore(a). Нюанс «сдано» для upload/file параметризован: вид ученика (acceptedOnly) — закрыто только при принятой сдаче; учитель/обзор долгов — любая сдача не на доработке. Поведение всех трёх поверхностей сохранено 1:1. - homework.html: asgnType/asgnDone/urgencyScore → тонкие делегаты в AssignmentUtils. - dashboard.html: urgencyScore делегирует; classify и upload-ветка buildAssignCard через AssignmentUtils.type/isDone (заодно корректнее: учебник-ДЗ больше не путается с upload). - assignmentController: classOutstanding/_assignTypeOf → AssignmentUtils. Verified: AU-контракт 25/25 (типы, isDone teacher vs acceptedOnly, порядок urgency); интеграция 8/8 (classOutstanding те же 14 уч./42 просрочено; homework делегирует). node --check всех файлов + инлайна обоих HTML. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/controllers/assignmentController.js | 34 +++-------- frontend/dashboard.html | 34 +++++------ frontend/homework.html | 39 ++---------- frontend/js/assignment-utils.js | 61 +++++++++++++++++++ 4 files changed, 93 insertions(+), 75 deletions(-) create mode 100644 frontend/js/assignment-utils.js diff --git a/backend/src/controllers/assignmentController.js b/backend/src/controllers/assignmentController.js index b635729..1e6df80 100644 --- a/backend/src/controllers/assignmentController.js +++ b/backend/src/controllers/assignmentController.js @@ -2,6 +2,7 @@ const db = require('../db/db'); const { pushNotif } = require('../utils/notifications'); const { stripTags } = require('../utils/sanitize'); const { SESSION_MODES } = require('../constants'); +const AssignmentUtils = require('../../../frontend/js/assignment-utils.js'); // единый источник: тип/«сдано» const VALID_ASSIGN_MODES = SESSION_MODES; @@ -344,14 +345,6 @@ function myAssignments(req, res) { res.json(assignmentRowsForUser(req.user.id)); } -/* Тип задания — те же признаки, что на /homework (asgnType). */ -function _assignTypeOf(r) { - if (r.textbook_id) return 'textbook'; - if (r.file_id) return 'file'; - if (r.is_homework && (r.count == null || r.count <= 1) && (!r.subject_slug || r.subject_slug === 'other')) return 'upload'; - return 'test'; -} - /* ── GET /api/classes/:id/outstanding ── что «висит» у каждого ученика класса ── Учитель/админ видят по каждому ученику его НЕзакрытые задания (классовые + личные от этого учителя) со статусом: не начато / в процессе / на доработке / просрочено. */ @@ -388,23 +381,16 @@ function classOutstanding(req, res) { ); const pending = []; for (const r of rows) { - const type = _assignTypeOf(r); + const t = AssignmentUtils.type(r); + const st = (t === 'upload' || t === 'file') ? subMap.get(r.id + '_' + m.id) : null; + // Учительская семантика: любая сдача не на доработке = не долг (default opts). + if (AssignmentUtils.isDone(r, st ? { status: st } : null)) continue; const overdue = r.deadline && new Date(r.deadline).getTime() < now; - let done = false, status = overdue ? 'overdue' : 'not_started'; - if (type === 'textbook') { - done = !!(r.textbook_all_read || r.completed_at); - } else if (type === 'upload' || type === 'file') { - const st = subMap.get(r.id + '_' + m.id); - if (st && st !== 'revision') done = true; // сдал — на проверке/принято - else if (st === 'revision') status = 'revision'; // вернули на доработку - } else { // test - const maxAtt = r.max_attempts || 0, used = r.attempts_used || 0; - if (maxAtt > 0 && used >= maxAtt) done = true; - else if (r.session_status === 'completed' && r.mode !== 'repeat') done = true; - else if (r.session_status === 'in_progress') status = 'in_progress'; - } - if (!done) pending.push({ - assignment_id: r.id, title: r.title, type, deadline: r.deadline, + let status = overdue ? 'overdue' : 'not_started'; + if (st === 'revision') status = 'revision'; // вернули на доработку + else if (t === 'test' && r.session_status === 'in_progress') status = 'in_progress'; + pending.push({ + assignment_id: r.id, title: r.title, type: t, deadline: r.deadline, status, is_homework: r.is_homework ? 1 : 0, scope: r.class_id === cidNum ? 'class' : 'direct', }); diff --git a/frontend/dashboard.html b/frontend/dashboard.html index 5797f9a..3a85a0a 100644 --- a/frontend/dashboard.html +++ b/frontend/dashboard.html @@ -1889,6 +1889,7 @@ + @@ -2381,15 +2382,8 @@ body.classList.toggle('collapsed'); } - /* ── Urgency sort score (lower = shown first) ── */ - function urgencyScore(a) { - if (a.session_status === 'in_progress') return -4; // in progress top - const dlMs = a.deadline ? new Date(a.deadline) - Date.now() : Infinity; - if (dlMs < 0) return -3; // overdue - if (dlMs < 24 * 3600 * 1000) return -2; // urgent <24h - if (dlMs < Infinity) return dlMs; // sorted by deadline - return 1e12; // no deadline last - } + /* ── Urgency sort score (lower = shown first) — общий модуль ── */ + function urgencyScore(a) { return AssignmentUtils.urgencyScore(a); } /* ── Is assignment urgent for teacher (within 48h) ── */ function isTeacherUrgent(a) { @@ -2457,7 +2451,7 @@ } /* ── Upload-only homework (no test, no file) ── */ - if (a.is_homework && !a.file_id && !a.session_id && a.count <= 1 && (!a.subject_slug || a.subject_slug === 'other')) { + if (AssignmentUtils.type(a) === 'upload') { const over = a.deadline && new Date(a.deadline) < new Date(); const sub = _mySubmissions.get(a.id); const metaParts = [classStr, dl ? `до ${dl}` : null, @@ -2694,18 +2688,22 @@ reIcons(); return; } - // Classify + // Classify (active/overdue/done) — тип и «сдано» из общего модуля AssignmentUtils. function classify(a) { - const maxAtt = a.max_attempts || 0; - const usedAtt = a.attempts_used ?? 0; - if (a.textbook_id) { - if (a.completed_at || a.textbook_all_read) return 'done'; + const t = AssignmentUtils.type(a); + if (t === 'textbook') { + if (AssignmentUtils.isDone(a)) return 'done'; if (a.deadline && new Date(a.deadline) < now) return 'overdue'; return 'active'; } - if (maxAtt > 0 && usedAtt >= maxAtt) return 'done'; - if (a.session_status === 'completed' && a.mode !== 'repeat') return 'done'; - if (!a.file_id && a.deadline && new Date(a.deadline) < now && a.session_status !== 'in_progress') return 'overdue'; + if (t === 'test') { + if (AssignmentUtils.isDone(a)) return 'done'; + if (a.deadline && new Date(a.deadline) < now && a.session_status !== 'in_progress') return 'overdue'; + return 'active'; + } + // upload / file: «сдано» по сабмишену здесь не считаем (как и раньше — статус + // показывает чип сдачи в карточке); upload просрочивается по дедлайну, file — всегда активен. + if (t === 'upload' && a.deadline && new Date(a.deadline) < now) return 'overdue'; return 'active'; } diff --git a/frontend/homework.html b/frontend/homework.html index 2f0578c..507e987 100644 --- a/frontend/homework.html +++ b/frontend/homework.html @@ -258,6 +258,7 @@ +