feat(trainer): P6 — учительская аналитика класса + общий прогресс
- GET /api/practice/class-stats (classStats): агрегаты по навыкам + матрица ученик×навык; доступ владелец класса/админ - клиент: кнопка «Аналитика класса» (учителю) → модалка с тепловой картой (точность/освоено) + пикер классов; LS.practiceClassStats - лёгкая геймификация: строка «Освоено навыков M из N · решено всего K» из агрегатов practice_progress - тесты practice.test.js +4 (владелец видит; чужой/ученик → 403; без class_id → 400); смоук страницы 27/27; план P6 → DONE Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -131,4 +131,46 @@ async function generateProblem(req, res) {
|
||||
res.json({ ok: true, problem: toClientProblem(row), attempts: result.attempts });
|
||||
}
|
||||
|
||||
module.exports = { listProgress, submitAttempt, listPool, generateProblem };
|
||||
/* GET /api/practice/class-stats?class_id= — аналитика класса для учителя.
|
||||
* Возвращает агрегаты по навыкам (кто застрял) + матрицу ученик×навык для
|
||||
* тепловой карты. Доступ: владелец класса (teacher_id) или админ. */
|
||||
function classStats(req, res) {
|
||||
const uid = req.user.id, role = req.user.role;
|
||||
const classId = parseInt((req.query && req.query.class_id), 10);
|
||||
if (!classId) return res.status(400).json({ error: 'class_id обязателен' });
|
||||
|
||||
if (role !== 'admin') {
|
||||
const own = db.prepare('SELECT 1 FROM classes WHERE id = ? AND teacher_id = ?').get(classId, uid);
|
||||
if (!own) return res.status(403).json({ error: 'не ваш класс' });
|
||||
}
|
||||
|
||||
const students = db.prepare(
|
||||
'SELECT u.id, u.name FROM class_members cm JOIN users u ON u.id = cm.user_id WHERE cm.class_id = ? ORDER BY u.name'
|
||||
).all(classId);
|
||||
if (!students.length) return res.json({ students: [], skills: [], perSkill: [] });
|
||||
|
||||
const ids = students.map(s => s.id);
|
||||
const ph = ids.map(() => '?').join(',');
|
||||
const rows = db.prepare(
|
||||
`SELECT user_id, skill, solved, attempts, mastered FROM practice_progress WHERE user_id IN (${ph})`
|
||||
).all(...ids);
|
||||
|
||||
const bySkill = {}, byStudent = {};
|
||||
for (const r of rows) {
|
||||
const s = bySkill[r.skill] || (bySkill[r.skill] = { skill: r.skill, attempted: 0, solved: 0, attempts: 0, mastered: 0 });
|
||||
s.attempted++; s.solved += r.solved; s.attempts += r.attempts; if (r.mastered) s.mastered++;
|
||||
const st = byStudent[r.user_id] || (byStudent[r.user_id] = {});
|
||||
st[r.skill] = { solved: r.solved, attempts: r.attempts, mastered: r.mastered ? 1 : 0,
|
||||
accuracy: r.attempts ? Math.round(100 * r.solved / r.attempts) : 0 };
|
||||
}
|
||||
const skills = Object.keys(bySkill).sort();
|
||||
const perSkill = skills.map(k => {
|
||||
const s = bySkill[k];
|
||||
return { skill: k, attempted: s.attempted, mastered: s.mastered,
|
||||
accuracy: s.attempts ? Math.round(100 * s.solved / s.attempts) : 0 };
|
||||
});
|
||||
const studentRows = students.map(s => ({ id: s.id, name: s.name, perSkill: byStudent[s.id] || {} }));
|
||||
res.json({ students: studentRows, skills, perSkill });
|
||||
}
|
||||
|
||||
module.exports = { listProgress, submitAttempt, listPool, generateProblem, classStats };
|
||||
|
||||
Reference in New Issue
Block a user