301 lines
11 KiB
TypeScript
301 lines
11 KiB
TypeScript
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;
|