feat(groups): assign tutor to group

This commit is contained in:
dev
2026-09-19 13:17:40 +03:00
parent 1ef81f9d1c
commit 5e2533b876
5 changed files with 64 additions and 10 deletions
+33 -8
View File
@@ -773,6 +773,7 @@ async function ensureUserTables() {
await pool.query(`CREATE INDEX IF NOT EXISTS idx_sessions_token ON sessions(token)`);
await pool.query(`CREATE INDEX IF NOT EXISTS idx_sessions_expires_at ON sessions(expires_at)`);
await pool.query('ALTER TABLE audit_log ADD COLUMN IF NOT EXISTS user_id INT REFERENCES users(id) ON DELETE SET NULL');
await pool.query('ALTER TABLE groups ADD COLUMN IF NOT EXISTS tutor_id INT REFERENCES users(id) ON DELETE SET NULL');
}
async function ensureFirstAdmin() {
@@ -883,12 +884,20 @@ app.get('/api/users', requireAuth, requireAdmin, async (_, res) => {
COALESCE(array_agg(ub.branch_id) FILTER (WHERE ub.branch_id IS NOT NULL), '{}') AS branch_ids
FROM users u
LEFT JOIN user_branches ub ON ub.user_id = u.id
WHERE u.role = 'tutor'
GROUP BY u.id
ORDER BY u.id`
);
res.json(rows.map(r => ({ ...r, branch_ids: r.branch_ids || [] })));
});
app.get('/api/users/tutors', requireAuth, async (_, res) => {
const { rows } = await pool.query(
`SELECT id, name, username FROM users WHERE role = 'tutor' AND is_active = true ORDER BY COALESCE(NULLIF(name, ''), username)`
);
res.json(rows);
});
app.get('/api/users/branches', requireAuth, async (req, res) => {
const s = branchScope(req.user);
const params = [];
@@ -1627,8 +1636,8 @@ app.post('/api/restore', requireAdmin, uploadBackup.single('backup'), async (req
}
for (const x of ndata.groups) {
await client.query(
'INSERT INTO groups (id, name, created_at, day_of_week, time_start, time_end, branch_id) VALUES ($1,$2,$3,$4,$5,$6,$7)',
[x.id, x.name, x.created_at, x.day_of_week, x.time_start, x.time_end, x.branch_id]
'INSERT INTO groups (id, name, created_at, day_of_week, time_start, time_end, branch_id, tutor_id) VALUES ($1,$2,$3,$4,$5,$6,$7,$8)',
[x.id, x.name, x.created_at, x.day_of_week, x.time_start, x.time_end, x.branch_id, x.tutor_id]
);
}
for (const x of ndata.students) {
@@ -2091,7 +2100,7 @@ app.get('/api/groups/active', apiLimiter, async (_, res) => {
});
app.put('/api/groups/:id', requireAuth, async (req, res) => {
const { name, day_of_week, time_start, time_end, branch_id } = req.body;
const { name, day_of_week, time_start, time_end, branch_id, tutor_id } = req.body;
if (!(await groupBelongsToBranches(req.user, req.params.id))) {
return res.status(403).json({ error: 'Нет доступа к этой группе' });
}
@@ -2099,6 +2108,13 @@ app.put('/api/groups/:id', requireAuth, async (req, res) => {
if (!isAdmin && branch_id !== undefined) {
return res.status(403).json({ error: 'Назначение филиала — только для администратора' });
}
const tutorProvided = tutor_id !== undefined;
let effectiveTutor = null;
if (tutor_id !== undefined && tutor_id !== null && tutor_id !== '') {
const t = await pool.query(`SELECT id FROM users WHERE id = $1 AND role = 'tutor'`, [tutor_id]);
if (!t.rows.length) return res.status(400).json({ error: 'Пользователь не найден или не является тутором' });
effectiveTutor = t.rows[0].id;
}
try {
const { rows } = await pool.query(
`UPDATE groups SET
@@ -2106,12 +2122,15 @@ app.put('/api/groups/:id', requireAuth, async (req, res) => {
day_of_week = $2,
time_start = $3,
time_end = $4,
branch_id = $5
WHERE id = $6 RETURNING *`,
branch_id = $5,
tutor_id = CASE WHEN $6::boolean THEN $7::int ELSE tutor_id END
WHERE id = $8 RETURNING *`,
[name,
day_of_week === undefined || day_of_week === null || day_of_week === '' ? null : day_of_week,
time_start || null, time_end || null,
branch_id === undefined || branch_id === null || branch_id === '' ? null : branch_id,
tutorProvided,
tutorProvided ? effectiveTutor : null,
req.params.id]
);
await logAudit(req, 'group.update', { id: req.params.id, ...req.body });
@@ -2125,17 +2144,23 @@ app.put('/api/groups/:id', requireAuth, async (req, res) => {
});
app.post('/api/groups', requireAuth, async (req, res) => {
const { name, branch_id } = req.body;
const { name, branch_id, tutor_id } = req.body;
if (!name?.trim()) return res.status(400).json({ error: 'Name required' });
const isAdmin = req.user.role === 'admin';
const effectiveBranch = branch_id === undefined || branch_id === null || branch_id === '' ? null : Number(branch_id);
if (!isAdmin && branch_id !== undefined && branch_id !== null && branch_id !== '') {
return res.status(403).json({ error: 'Назначение филиала — только для администратора' });
}
let effectiveTutor = null;
if (tutor_id !== undefined && tutor_id !== null && tutor_id !== '') {
const t = await pool.query(`SELECT id FROM users WHERE id = $1 AND role = 'tutor'`, [tutor_id]);
if (!t.rows.length) return res.status(400).json({ error: 'Пользователь не найден или не является тутором' });
effectiveTutor = t.rows[0].id;
}
try {
const { rows } = await pool.query(
'INSERT INTO groups (name, branch_id) VALUES ($1, $2) RETURNING *',
[name.trim(), isAdmin ? effectiveBranch : null]
'INSERT INTO groups (name, branch_id, tutor_id) VALUES ($1, $2, $3) RETURNING *',
[name.trim(), isAdmin ? effectiveBranch : null, effectiveTutor]
);
await logAudit(req, 'group.create', { id: rows[0].id, name: name.trim(), branch_id });
invalidateGroups();