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
+6 -33
View File
@@ -258,6 +258,7 @@
<script src="/js/api.js"></script>
<script src="/js/sidebar.js"></script>
<script src="/js/notifications.js"></script>
<script src="/js/assignment-utils.js"></script>
<script>
const { user, isTeacher, isAdmin } = LS.initPage();
if (!user) throw new Error('Not logged in');
@@ -450,39 +451,11 @@
/* ══ АКТУАЛЬНЫЕ ЗАДАНИЯ (что нужно сделать) ══════════════════════════ */
// Тип задания — по тем же признакам, что и карточки дашборда.
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;
}
// Тип / «сдано» / срочность — из общего модуля AssignmentUtils (тот же, что у дашборда
// и сервера). Вид ученика: upload/file закрыт ТОЛЬКО при принятой сдаче (acceptedOnly).
function asgnType(a) { return AssignmentUtils.type(a); }
function asgnDone(a) { return AssignmentUtils.isDone(a, _subByAsgn.get(a.id), { acceptedOnly: true }); }
function urgencyScore(a) { return AssignmentUtils.urgencyScore(a); }
function deadlineChip(a) {
if (!a.deadline) return '<span class="hw-dl-chip hw-dl-ok">Без срока</span>';