import { Router } from 'express'; import { authenticateToken, requirePermission, type AuthenticatedRequest } from '../middleware/auth.middleware'; import { tenantIsolation } from '../middleware/tenant.middleware'; import { storage } from '../storage'; import { pool } from '../db'; import { executeTaskTransition } from '../services/task-transition.service'; import { tasksMinimalCache } from '../utils/cache'; import type { ReminderRecipient } from '@shared/schema'; const taskBulkRouter = Router(); const MAX_BULK_TASKS = 1000; function validateTaskIds(body: unknown): number[] | null { if (!body || typeof body !== 'object') return null; const { taskIds } = body as { taskIds?: unknown }; if (!Array.isArray(taskIds)) return null; const ids = taskIds .map((id) => typeof id === 'number' ? id : (typeof id === 'string' ? parseInt(id, 10) : NaN)) .filter((id) => !isNaN(id)); if (ids.length === 0 || ids.length > MAX_BULK_TASKS) return null; return ids; } // Mark notifications as read for multiple tasks // POST /api/tasks/bulk/notifications/read // Body: { taskIds: number[] } taskBulkRouter.post('/api/tasks/bulk/notifications/read', authenticateToken, tenantIsolation, async (req: AuthenticatedRequest, res) => { try { const taskIds = validateTaskIds(req.body); if (!taskIds) { return res.status(400).json({ success: false, error: 'Укажите от 1 до 100 ID задач' }); } const userId = req.user!.id; const organizationId = req.user!.organizationId; await pool.query( `UPDATE user_notifications SET is_read = true WHERE task_id = ANY($1) AND user_id = $2 AND organization_id = $3 AND is_read = false`, [taskIds, userId, organizationId] ); res.json({ success: true, processed: taskIds.length }); } catch (err) { console.error('[bulk/notifications/read]', err); res.status(500).json({ success: false, error: 'Ошибка при отметке уведомлений' }); } } ); // Hide multiple tasks from the current user's inbox // POST /api/tasks/bulk/hide-inbox // Body: { taskIds: number[] } taskBulkRouter.post('/api/tasks/bulk/hide-inbox', authenticateToken, tenantIsolation, async (req: AuthenticatedRequest, res) => { try { const taskIds = validateTaskIds(req.body); if (!taskIds) { return res.status(400).json({ success: false, error: 'Укажите от 1 до 100 ID задач' }); } const userId = req.user!.id; const organizationId = req.user!.organizationId; await pool.query( `INSERT INTO task_inbox_hidden (task_id, user_id, organization_id) SELECT unnest($1::int[]), $2, $3 ON CONFLICT (task_id, user_id) DO NOTHING`, [taskIds, userId, organizationId] ); res.json({ success: true, hidden: taskIds.length }); } catch (err) { console.error('[bulk/hide-inbox]', err); res.status(500).json({ success: false, error: 'Ошибка при скрытии задач' }); } } ); // Unhide multiple tasks for the current user // DELETE /api/tasks/bulk/hide-inbox // Body: { taskIds: number[] } taskBulkRouter.delete('/api/tasks/bulk/hide-inbox', authenticateToken, tenantIsolation, async (req: AuthenticatedRequest, res) => { try { const taskIds = validateTaskIds(req.body); if (!taskIds) { return res.status(400).json({ success: false, error: 'Укажите от 1 до 100 ID задач' }); } const userId = req.user!.id; const organizationId = req.user!.organizationId; await pool.query( `DELETE FROM task_inbox_hidden WHERE task_id = ANY($1) AND user_id = $2 AND organization_id = $3`, [taskIds, userId, organizationId] ); res.json({ success: true, restored: taskIds.length }); } catch (err) { console.error('[bulk/unhide-inbox]', err); res.status(500).json({ success: false, error: 'Ошибка при возврате задач' }); } } ); // Create reminders for multiple tasks // POST /api/tasks/bulk/reminders // Body: { taskIds: number[], remindAt: string, note?: string, recipients: ReminderRecipient[] } taskBulkRouter.post('/api/tasks/bulk/reminders', authenticateToken, tenantIsolation, async (req: AuthenticatedRequest, res) => { try { const taskIds = validateTaskIds(req.body); if (!taskIds) { return res.status(400).json({ success: false, error: 'Укажите от 1 до 100 ID задач' }); } const { remindAt, note, recipients } = req.body as { remindAt?: string; note?: string; recipients?: ReminderRecipient[]; }; if (!remindAt) { return res.status(400).json({ success: false, error: 'Укажите дату и время напоминания' }); } const remindAtDate = new Date(remindAt); if (isNaN(remindAtDate.getTime())) { return res.status(400).json({ success: false, error: 'Неверный формат даты напоминания' }); } if (!Array.isArray(recipients) || recipients.length === 0) { return res.status(400).json({ success: false, error: 'Укажите хотя бы одного получателя' }); } const organizationId = req.user!.organizationId; const createdByUserId = req.user!.id; // Re-use single-task validation logic from mcp-rag.routes.ts const ALLOWED_SYSTEM_ROLES = new Set(['admin', 'user']); const validatedRecipients: ReminderRecipient[] = []; const hasUserRecipients = recipients.some((rec) => rec.type === 'user'); const orgUsersMap = hasUserRecipients ? new Map((await storage.getUsersByOrganization(organizationId)).map((u) => [u.id, u])) : new Map(); for (const rec of recipients) { if (rec.type === 'user') { if (typeof rec.userId !== 'number') { return res.status(400).json({ success: false, error: 'Неверный формат получателя' }); } const orgUser = orgUsersMap.get(rec.userId); if (!orgUser) { return res.status(400).json({ success: false, error: `Пользователь ${rec.userId} не принадлежит организации` }); } validatedRecipients.push({ type: 'user', userId: rec.userId }); } else if (rec.type === 'role') { if (typeof rec.role !== 'string' || !ALLOWED_SYSTEM_ROLES.has(rec.role)) { return res.status(400).json({ success: false, error: `Недопустимая системная роль: ${rec.role}` }); } validatedRecipients.push({ type: 'role', role: rec.role }); } else if (rec.type === 'orgRole') { if (typeof rec.roleId !== 'number') { return res.status(400).json({ success: false, error: 'Неверный формат роли организации' }); } await storage.getUsersByOrgRoleId(rec.roleId, organizationId); validatedRecipients.push({ type: 'orgRole', roleId: rec.roleId }); } else { return res.status(400).json({ success: false, error: 'Неверный тип получателя' }); } } let created = 0; for (const taskId of taskIds) { await storage.createTaskReminder({ taskId, organizationId, createdByUserId, remindAt: remindAtDate, note: note || null, recipients: validatedRecipients, }); created++; } res.json({ success: true, created }); } catch (err) { console.error('[bulk/reminders]', err); res.status(500).json({ success: false, error: 'Ошибка при создании напоминаний' }); } } ); // Apply a status transition to multiple tasks // POST /api/tasks/bulk/transition // Body: { taskIds: number[], transitionId: number, action?: 'approve' | 'reject' } taskBulkRouter.post('/api/tasks/bulk/transition', authenticateToken, tenantIsolation, async (req: AuthenticatedRequest, res) => { try { const taskIds = validateTaskIds(req.body); if (!taskIds) { return res.status(400).json({ success: false, error: 'Укажите от 1 до 100 ID задач' }); } const { transitionId, action } = req.body as { transitionId?: number; action?: 'approve' | 'reject' }; if (!transitionId) { return res.status(400).json({ success: false, error: 'Укажите transitionId' }); } const user = req.user!; const organizationId = user.organizationId; const results: Array<{ taskId: number; success: boolean; error?: string }> = []; for (const taskId of taskIds) { const result = await executeTaskTransition({ taskId, transitionId, action, user, organizationId, }); results.push({ taskId, success: result.success, error: result.success ? undefined : result.error }); } const processed = results.filter(r => r.success).length; const errors = results.filter(r => !r.success).map(r => ({ taskId: r.taskId, error: r.error })); res.json({ success: true, processed, total: taskIds.length, errors }); } catch (err) { console.error('[bulk/transition]', err); res.status(500).json({ success: false, error: 'Ошибка при массовой смене статуса' }); } } ); // Delete multiple tasks (admin only) // DELETE /api/tasks/bulk // Body: { taskIds: number[] } taskBulkRouter.delete('/api/tasks/bulk', authenticateToken, tenantIsolation, requirePermission('tasks.edit_all'), async (req: AuthenticatedRequest, res) => { try { const taskIds = validateTaskIds(req.body); if (!taskIds) { return res.status(400).json({ success: false, error: 'Укажите от 1 до 100 ID задач' }); } const organizationId = req.user!.organizationId; let deleted = 0; const errors: Array<{ taskId: number; error: string }> = []; for (const taskId of taskIds) { const existingTask = await storage.getTask(taskId, organizationId); if (!existingTask) { errors.push({ taskId, error: 'Задача не найдена' }); continue; } try { await storage.deleteTask(taskId, organizationId); deleted++; } catch (err: any) { console.error(`[bulk/delete] Failed to delete task ${taskId}:`, err); errors.push({ taskId, error: err?.message || 'Ошибка удаления' }); } } if (deleted > 0) { tasksMinimalCache.invalidatePrefix(`tasks:${organizationId}:minimal:`); } res.json({ success: true, deleted, total: taskIds.length, errors: errors.length > 0 ? errors : undefined }); } catch (err) { console.error('[bulk/delete]', err); res.status(500).json({ success: false, error: 'Ошибка при массовом удалении задач' }); } } ); export default taskBulkRouter;