feat(access): Фаза 2a — режим «Матрица» класс × контент в админке

GET /api/access/matrix (классы + карта открытого контента одним запросом,
скоуп учителя). Клиент LS.accessMatrix. Третий режим вкладки «Доступ»:
таблица контент × классы с чекбоксами (правка в один клик) + поиск по
названию (обновляет только tbody — фокус ввода сохраняется), залипающие
заголовки. Тест /api/access смонтирован в харнесс; content-access.test 11/11
(+матрица: учитель видит свои классы и открытый контент, ученику 403).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-06-03 12:43:00 +03:00
parent 1bbddc00c8
commit 67a70c672d
5 changed files with 126 additions and 10 deletions
+24
View File
@@ -107,6 +107,30 @@ router.get('/summary', (req, res) => {
res.json({ totalClasses, textbooks, exams });
});
/* ── Матрица класс × контент (обзор и правка одним экраном) ────────────── */
/* GET /api/access/matrix
→ { classes:[{id,name}], open:{ [class_id]:{ textbook:[ref], exam:[ref] } } } */
router.get('/matrix', (req, res) => {
const admin = isAdmin(req);
const classes = admin
? db.prepare('SELECT id, name FROM classes ORDER BY name').all()
: db.prepare('SELECT id, name FROM classes WHERE teacher_id = ? ORDER BY name').all(req.user.id);
const open = {};
classes.forEach(c => { open[c.id] = { textbook: [], exam: [] }; });
const ids = classes.map(c => c.id);
if (ids.length) {
const ph = ids.map(() => '?').join(',');
const rows = db.prepare(`
SELECT content_type, content_ref, target_id FROM content_access
WHERE scope = 'class' AND allow = 1 AND target_id IN (${ph})`).all(...ids);
for (const r of rows) {
const o = open[r.target_id];
if (o && o[r.content_type]) o[r.content_type].push(r.content_ref);
}
}
res.json({ classes, open });
});
/* ── Текущие правила для одного контента ───────────────────────────────── */
/* GET /api/access/rules?content_type=&content_ref=
→ { classRules:{[class_id]:allow}, studentRules:{[user_id]:allow} } */