feat: textbooks Phase 4 — A1+A2+A3+B4+C7 + назначение ученику
A1 — карточка ДЗ-чтения у ученика на /dashboard: - Новая ветка в buildAssignCard для assignments с textbook_id - Прогресс-бар «X из Y §», цвет берётся из textbook.color - Кнопка «Открыть / Продолжить» с deep-link на первый требуемый параграф - В classify(): textbook_all_read → done, deadline → overdue A2 — авто-проверка выполнения: - При POST /:slug/progress с mark_read: проверяются активные textbook-assignments - Если все требуемые § прочитаны → INSERT в assignment_completion - SSE-уведомление учителю «Ученик завершил чтение: <title>» - myAssignments возвращает completed_at и textbook_all_read A3 — учительский UI прогресса класса: - Новая страница /textbook-progress (учитель/админ) - Селекторы «учебник × класс» → таблица учеников с прогрессом - Сортировка по количеству прочитанного, дата last_at - Кнопка «Прогресс класса» добавлена в /textbooks (видна учителям) B4 — admin-UI управления учебниками: - /admin-textbooks (только admin) — таблица всех учебников - Inline-редактирование title/author, тоггл is_active - Колонка «Читателей» (count из textbook_progress) - Endpoints: GET /api/textbooks/admin/all, PATCH /admin/:id C7 — закладки/заметки внутри учебника: - Таблица textbook_bookmarks (user, textbook, para, text, note, color) - API: GET/POST/PATCH/DELETE для CRUD закладок - В tracker: при выделении текста (8-400 симв) появляется плавающая «+ Закладка» - Кнопка-иконка в overlay top-left открывает панель «Мои закладки» - Хранится paragraph-якорь, цвет, заметка, кнопка удалить Назначение ученику (в дополнение к классу): - В модалке /textbooks — переключатель «Классу / Ученику» - Поиск ученика по имени/email через /api/classes/students - Submit использует POST /api/assignments (createDirectAssignment) - createDirectAssignment расширен textbook_slug + textbook_paragraphs - Учитель может назначать только ученикам своих классов myAssignments расширен: возвращает textbook fields + post-process считает textbook_required_count, textbook_read_count, textbook_all_read. Deep-link поддержка: /textbook/<slug>#pN в tracker.js — на load и hashchange вызывает setParaTab(pN) (нативная функция учебника). Миграция 005: assignment_completion + textbook_bookmarks + индексы.
This commit is contained in:
@@ -2108,6 +2108,60 @@
|
||||
</div></div>`;
|
||||
}
|
||||
|
||||
/* ── Textbook reading assignment ── */
|
||||
if (a.textbook_id) {
|
||||
const reqCount = a.textbook_required_count || 0;
|
||||
const readCount = a.textbook_read_count || 0;
|
||||
const allRead = !!a.textbook_all_read || !!a.completed_at;
|
||||
const tbPct = reqCount > 0 ? Math.round(100 * readCount / reqCount) : 0;
|
||||
const tbColorMap = { amber:'#d97706', blue:'#2563eb', green:'#059669', violet:'#7c3aed', pink:'#db2777' };
|
||||
const tbColor = tbColorMap[a.textbook_color] || '#7c3aed';
|
||||
const over = !allRead && a.deadline && new Date(a.deadline) < new Date();
|
||||
const cardCls = allRead ? 'done' : over ? 'over' : isUrgent ? 'urgent' : '';
|
||||
const parasText = a.textbook_paragraphs ? `§${a.textbook_paragraphs}` : 'весь учебник';
|
||||
const metaParts = [
|
||||
classStr,
|
||||
parasText,
|
||||
a.is_homework ? `<span class="ar-tag-hw">ДЗ</span>` : null,
|
||||
dl ? `до ${dl}` : null,
|
||||
isUrgent ? `<span class="ar-tag-urgent"><i data-lucide="zap" style="width:10px;height:10px;vertical-align:-1px"></i> ${hoursLeft} ч</span>` : null,
|
||||
over ? `<span class="ar-tag-over">просрочено</span>` : null,
|
||||
].filter(Boolean);
|
||||
|
||||
// Find first required paragraph for deep-link
|
||||
let firstHash = '';
|
||||
if (a.textbook_paragraphs) {
|
||||
const m = String(a.textbook_paragraphs).match(/^\s*(\d+)/);
|
||||
if (m) firstHash = '#p' + m[1];
|
||||
}
|
||||
const openHref = `/textbook/${a.textbook_slug}${firstHash}`;
|
||||
|
||||
const actionBtn = allRead
|
||||
? `<span class="ar-score hi" style="background:#06D6A018;color:#059669">${lci('check')} Прочитано</span>`
|
||||
: `<a class="ar-btn" href="${openHref}" onclick="event.stopPropagation()" style="background:${tbColor};color:#fff;text-decoration:none">${readCount > 0 ? 'Продолжить' : 'Открыть'}</a>`;
|
||||
|
||||
return `<div class="asgn-wrap"><div class="asgn-row stagger-item ${cardCls}${isFirst && !allRead ? ' spotlight' : ''}" style="--i:${idx};--ac:${tbColor}" id="asgn-row-${a.id}" onclick="toggleAsgn(${a.id},event)">
|
||||
<div class="ar-icon" style="background:${tbColor}18;color:${tbColor}">${lci('book-open-text')}</div>
|
||||
<div class="ar-body"><div class="ar-title">${esc(a.title)}</div><div class="ar-meta">${metaParts.join(' · ')}</div></div>
|
||||
<div class="ar-progress">
|
||||
<div class="ar-prog-bar"><div class="ar-prog-fill" style="width:${tbPct}%;background:${tbColor}"></div></div>
|
||||
<span class="ar-prog-text">${readCount} / ${reqCount} §</span>
|
||||
</div>
|
||||
<div class="ar-right">${actionBtn}</div>
|
||||
</div>
|
||||
<div class="asgn-expand" id="asgn-exp-${a.id}">
|
||||
<div class="ae-row">
|
||||
<div class="ae-pills">
|
||||
<span class="ae-pill">${lci('book-open')} ${esc(a.textbook_title || 'Учебник')}</span>
|
||||
<span class="ae-pill">${lci('layers')} ${parasText}</span>
|
||||
${a.is_homework ? `<span class="ae-pill">${lci('book-open')} Домашнее задание</span>` : ''}
|
||||
${allRead ? `<span class="ae-pill" style="color:#059669">${lci('check')} Завершено</span>` : ''}
|
||||
</div>
|
||||
<a class="ae-btn" href="${openHref}" onclick="event.stopPropagation()" style="background:${tbColor}">Открыть учебник</a>
|
||||
</div>
|
||||
</div></div>`;
|
||||
}
|
||||
|
||||
/* ── Test assignment ── */
|
||||
const isDone = a.session_status === 'completed';
|
||||
const inProgress = a.session_status === 'in_progress';
|
||||
@@ -2248,6 +2302,11 @@
|
||||
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';
|
||||
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';
|
||||
|
||||
Reference in New Issue
Block a user