feat: teacher_students — назначения ученикам без класса

Новая модель «Мои ученики» — учитель связывает с собой учеников
независимо от классов (репетиторский сценарий).

Backend:
  - Таблица teacher_students (teacher_id, student_id, added_at, note)
    + индекс на student_id для обратного поиска
  - GET/POST/PATCH/DELETE /api/teacher-students — управление списком
  - Добавление по email с проверкой роли student/free_student
  - Уведомление ученику при добавлении

  - createDirectAssignment: проверка inClass расширена до
    inClass OR (teacher_id, student_id) в teacher_students
  - listStudents (/api/classes/students): возвращает объединение
    учеников из классов + из teacher_students. Это автоматически
    обновляет student-picker в /textbooks без правок UI.

Frontend:
  - /my-students — таблица личных учеников + форма добавления
    по email + заметка + счётчик созданных заданий
  - Сайдбар: пункт «Мои ученики» (user-plus, только для учителей)

Миграция 006_teacher_students.sql.

Что работает end-to-end:
  - Добавить ученика на /my-students
  - Открыть /textbooks → «Назначить» → «Ученику» → ученик ищется
    в общем списке (классовые + личные)
  - Создаётся запись в assignments с user_id, видна ученику на
    дашборде с пометкой «Личное задание»
@
This commit is contained in:
Maxim Dolgolyov
2026-05-16 17:01:11 +03:00
parent 3ff2f01178
commit eeb79246db
7 changed files with 409 additions and 9 deletions
@@ -529,14 +529,21 @@ function createDirectAssignment(req, res) {
if (!student) return res.status(404).json({ error: 'Ученик с таким email не найден' });
}
// Учитель может выдать личное задание только ученику из своего класса
// Учитель может выдать личное задание ученику из своего класса ИЛИ из «Мои ученики»
if (req.user.role === 'teacher') {
const inClass = db.prepare(`
SELECT 1 FROM class_members cm
JOIN classes c ON c.id = cm.class_id
WHERE cm.user_id = ? AND c.teacher_id = ?
`).get(student.id, req.user.id);
if (!inClass) return res.status(403).json({ error: 'Ученик не входит ни в один из ваших классов' });
const linked = inClass ? null : db.prepare(
'SELECT 1 FROM teacher_students WHERE teacher_id=? AND student_id=?'
).get(req.user.id, student.id);
if (!inClass && !linked) {
return res.status(403).json({
error: 'Ученик не входит в ваши классы и не добавлен в «Мои ученики». Добавьте его на странице «Мои ученики».',
});
}
}
test_id = test_id ? Number(test_id) : null;