refactor(assignments): единый модуль assignment-utils.js (тип/«сдано»/срочность)

Логика классификации типа задания и статуса «сдано» дублировалась в трёх местах
(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) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-06-23 14:09:34 +03:00
parent 5a4bc48027
commit 0d4c658d93
4 changed files with 93 additions and 75 deletions
+10 -24
View File
@@ -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',
});