feat(access): Фаза 1b — управление доступом к симуляциям в админке
Бэкенд /api/access обобщён на тип 'sim': catalog отдаёт симуляции (lab_sims), summary/matrix/class — карты по всем типам. Админ-секция «Доступ» теперь показывает «Симуляции» во всех трёх режимах (по контенту / по классу / матрица) + поиск; helpers (bucket/keyName/itemsOf) обобщены через карты типов (CONTENT_TYPES=textbook,exam,sim; course зарезервирован). Теперь админ/учитель могут открывать/закрывать конкретные симуляции классам и ученикам — закрыт UX- разрыв из 1a (новые классы без UI-управления). Тест: каталог включает sims; 210 pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -29,7 +29,16 @@ router.get('/catalog', (_req, res) => {
|
||||
WHERE enabled = 1
|
||||
ORDER BY sort_order, exam_key
|
||||
`).all();
|
||||
res.json({ textbooks, exams });
|
||||
let sims = [];
|
||||
try {
|
||||
sims = db.prepare(`
|
||||
SELECT id, title, cat AS subject, grade
|
||||
FROM lab_sims
|
||||
WHERE enabled = 1
|
||||
ORDER BY sort_order, id
|
||||
`).all();
|
||||
} catch (_e) { /* lab_sims может отсутствовать на старом инстансе — деградация */ }
|
||||
res.json({ textbooks, exams, sims });
|
||||
});
|
||||
|
||||
/* ── Цели назначения: классы (+ученики) и отдельные ученики ────────────── */
|
||||
@@ -99,12 +108,9 @@ router.get('/summary', (req, res) => {
|
||||
AND target_id IN (SELECT id FROM classes WHERE teacher_id = ?)
|
||||
GROUP BY content_type, content_ref`).all(req.user.id);
|
||||
|
||||
const textbooks = {}, exams = {};
|
||||
for (const r of rows) {
|
||||
if (r.content_type === 'textbook') textbooks[r.content_ref] = r.n;
|
||||
else exams[r.content_ref] = r.n;
|
||||
}
|
||||
res.json({ totalClasses, textbooks, exams });
|
||||
const out = { textbook: {}, exam: {}, sim: {}, course: {} };
|
||||
for (const r of rows) { (out[r.content_type] || (out[r.content_type] = {}))[r.content_ref] = r.n; }
|
||||
res.json({ totalClasses, textbooks: out.textbook, exams: out.exam, sims: out.sim, courses: out.course });
|
||||
});
|
||||
|
||||
/* ── Матрица класс × контент (обзор и правка одним экраном) ────────────── */
|
||||
@@ -116,7 +122,7 @@ router.get('/matrix', (req, res) => {
|
||||
? 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: [] }; });
|
||||
classes.forEach(c => { open[c.id] = { textbook: [], exam: [], sim: [], course: [] }; });
|
||||
const ids = classes.map(c => c.id);
|
||||
if (ids.length) {
|
||||
const ph = ids.map(() => '?').join(',');
|
||||
@@ -169,9 +175,9 @@ router.get('/class/:id', requireRole('admin', 'teacher'), (req, res) => {
|
||||
SELECT content_type, content_ref FROM content_access
|
||||
WHERE scope = 'class' AND target_id = ? AND allow = 1
|
||||
`).all(cid);
|
||||
const textbooks = [], exams = [];
|
||||
for (const r of rows) (r.content_type === 'textbook' ? textbooks : exams).push(r.content_ref);
|
||||
res.json({ textbooks, exams });
|
||||
const out = { textbook: [], exam: [], sim: [], course: [] };
|
||||
for (const r of rows) (out[r.content_type] || (out[r.content_type] = [])).push(r.content_ref);
|
||||
res.json({ textbooks: out.textbook, exams: out.exam, sims: out.sim, courses: out.course });
|
||||
});
|
||||
function teacherCanManageStudent(teacherId, studentId) {
|
||||
const inClass = db.prepare(`
|
||||
|
||||
Reference in New Issue
Block a user