@
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:
@@ -0,0 +1,81 @@
|
||||
'use strict';
|
||||
const router = require('express').Router();
|
||||
const db = require('../db/db');
|
||||
const { authMiddleware, requireRole } = require('../middleware/auth');
|
||||
const { emit } = require('../sse');
|
||||
|
||||
router.use(authMiddleware);
|
||||
router.use(requireRole('teacher', 'admin'));
|
||||
|
||||
/* GET /api/teacher-students — my linked students (not in classes, can be assigned to) */
|
||||
router.get('/', (req, res) => {
|
||||
const rows = db.prepare(`
|
||||
SELECT u.id, u.name, u.email, ts.added_at, ts.note,
|
||||
(SELECT COUNT(*) FROM assignments WHERE user_id = u.id AND created_by = ?) AS assignment_count
|
||||
FROM teacher_students ts
|
||||
JOIN users u ON u.id = ts.student_id
|
||||
WHERE ts.teacher_id = ?
|
||||
ORDER BY u.name
|
||||
`).all(req.user.id, req.user.id);
|
||||
res.json({ students: rows });
|
||||
});
|
||||
|
||||
/* POST /api/teacher-students — add by email
|
||||
body: { email: string, note?: string } */
|
||||
router.post('/', (req, res) => {
|
||||
const email = (req.body?.email || '').trim().toLowerCase();
|
||||
const note = String(req.body?.note || '').slice(0, 200);
|
||||
if (!email) return res.status(400).json({ error: 'email обязателен' });
|
||||
|
||||
const student = db.prepare(
|
||||
"SELECT id, name, email FROM users WHERE email = ? AND role IN ('student','free_student')"
|
||||
).get(email);
|
||||
if (!student) return res.status(404).json({ error: 'Ученик с таким email не зарегистрирован' });
|
||||
|
||||
// Can't add yourself
|
||||
if (student.id === req.user.id) return res.status(400).json({ error: 'Нельзя добавить себя' });
|
||||
|
||||
// Check if already added
|
||||
const existing = db.prepare(
|
||||
'SELECT 1 FROM teacher_students WHERE teacher_id=? AND student_id=?'
|
||||
).get(req.user.id, student.id);
|
||||
if (existing) return res.status(409).json({ error: 'Ученик уже добавлен в ваш список' });
|
||||
|
||||
db.prepare(
|
||||
'INSERT INTO teacher_students (teacher_id, student_id, note) VALUES (?, ?, ?)'
|
||||
).run(req.user.id, student.id, note);
|
||||
|
||||
// Notify student
|
||||
try {
|
||||
emit(student.id, {
|
||||
type: 'notification', notif_type: 'teacher_added',
|
||||
message: `${req.user.name || 'Учитель'} добавил вас как своего ученика`,
|
||||
link: '/profile',
|
||||
});
|
||||
} catch {}
|
||||
|
||||
res.status(201).json({ ok: true, student });
|
||||
});
|
||||
|
||||
/* PATCH /api/teacher-students/:student_id — update note */
|
||||
router.patch('/:student_id', (req, res) => {
|
||||
const sid = Number(req.params.student_id);
|
||||
const link = db.prepare('SELECT 1 FROM teacher_students WHERE teacher_id=? AND student_id=?').get(req.user.id, sid);
|
||||
if (!link) return res.status(404).json({ error: 'Связь не найдена' });
|
||||
const note = String(req.body?.note || '').slice(0, 200);
|
||||
db.prepare('UPDATE teacher_students SET note=? WHERE teacher_id=? AND student_id=?')
|
||||
.run(note, req.user.id, sid);
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
/* DELETE /api/teacher-students/:student_id */
|
||||
router.delete('/:student_id', (req, res) => {
|
||||
const sid = Number(req.params.student_id);
|
||||
const r = db.prepare(
|
||||
'DELETE FROM teacher_students WHERE teacher_id=? AND student_id=?'
|
||||
).run(req.user.id, sid);
|
||||
if (r.changes === 0) return res.status(404).json({ error: 'Связь не найдена' });
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user