Files
minicrm/task-endpoints.js
2026-02-19 20:10:39 +05:00

1805 lines
78 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// task-endpoints.js
const path = require('path');
const fs = require('fs');
function getApproverUsers(groupId) {
return new Promise((resolve) => {
db.all(`
SELECT u.id, u.login, u.name, u.email
FROM users u
JOIN user_group_memberships ugm ON u.id = ugm.user_id
WHERE ugm.group_id = ?
`, [groupId], (err, users) => {
resolve(err ? [] : users);
});
});
}
function setupTaskEndpoints(app, db, upload) {
const { logActivity, createUserTaskFolder, saveTaskMetadata, updateTaskMetadata, checkTaskAccess } = require('./database');
const { sendTaskNotifications } = require('./notifications');
function checkIfOverdue(dueDate, status) {
if (!dueDate || status === 'completed') return false;
const now = new Date();
const due = new Date(dueDate);
return due < now;
}
// Функция проверки прав редактирования
function canUserEditTask(task, user) {
if (!user) return false;
// Администратор может всё
if (user.role === 'admin') return true;
// Пользователи с ролью 'tasks' могут редактировать любые задачи
if (user.role === 'tasks') return true;
// Создатель может редактировать свою задачу только если она не назначена другим
if (parseInt(task.created_by) === user.id) {
if (task.assignments && task.assignments.length > 0) {
// Проверяем, назначена ли задача другим пользователям
const assignedToOthers = task.assignments.some(assignment =>
parseInt(assignment.user_id) !== user.id
);
if (assignedToOthers) {
// Создатель не может редактировать задачу, назначенную другим
return false;
}
}
return true;
}
// Исполнитель может загружать файлы в свою задачу
// Проверяем, назначен ли пользователь на эту задачу
if (task.assignments && task.assignments.length > 0) {
const isAssigned = task.assignments.some(assignment =>
parseInt(assignment.user_id) === user.id
);
if (isAssigned) {
return true; // Исполнитель может загружать файлы
}
}
return false;
}
// Middleware для аутентификации
function requireAuth(req, res, next) {
if (!req.session.user) {
return res.status(401).json({ error: 'Требуется аутентификация' });
}
next();
}
// API для задач
app.get('/api/tasks', requireAuth, (req, res) => {
const userId = req.session.user.id;
const showDeleted = req.session.user.role === 'admin' && req.query.showDeleted === 'true';
const search = req.query.search || '';
const statusFilter = req.query.status || 'active,in_progress,assigned,overdue,rework';
const creatorFilter = req.query.creator || '';
const assigneeFilter = req.query.assignee || '';
const deadlineFilter = req.query.deadline || '';
const taskType = req.query.task_type || ''; // Новый параметр для фильтрации по типу задачи
let query = `
SELECT DISTINCT
t.*,
u.name as creator_name,
u.login as creator_login,
ot.title as original_task_title,
ou.name as original_creator_name,
GROUP_CONCAT(DISTINCT ta.user_id) as assigned_user_ids,
GROUP_CONCAT(DISTINCT u2.name) as assigned_user_names
FROM tasks t
LEFT JOIN users u ON t.created_by = u.id
LEFT JOIN tasks ot ON t.original_task_id = ot.id
LEFT JOIN users ou ON ot.created_by = ou.id
LEFT JOIN task_assignments ta ON t.id = ta.task_id
LEFT JOIN users u2 ON ta.user_id = u2.id
WHERE 1=1
`;
const params = [];
// ===== ДОБАВЛЯЕМ ФИЛЬТР ПО ТИПУ ЗАДАЧИ =====
if (taskType) {
query += ` AND t.task_type = ?`;
params.push(taskType);
}
if (req.session.user.role !== 'admin') {
query += ` AND (t.created_by = ? OR ta.user_id = ?)`;
params.push(userId, userId);
}
if (!showDeleted) {
query += " AND t.status = 'active'";
}
if (statusFilter && statusFilter !== 'all') {
const statuses = statusFilter.split(',');
if (statuses.includes('closed')) {
if (req.session.user.role !== 'admin') {
query += ` AND (t.closed_at IS NOT NULL AND t.created_by = ?)`;
params.push(userId);
} else {
query += ` AND t.closed_at IS NOT NULL`;
}
} else {
query += ` AND t.closed_at IS NULL`;
if (statuses.length > 0 && !statuses.includes('all')) {
query += ` AND EXISTS (
SELECT 1 FROM task_assignments ta2
WHERE ta2.task_id = t.id AND ta2.status IN (${statuses.map(() => '?').join(',')})
)`;
statuses.forEach(status => params.push(status));
}
}
} else {
if (req.session.user.role !== 'admin') {
query += ` AND (t.closed_at IS NULL OR t.created_by = ?)`;
params.push(userId);
}
}
if (creatorFilter) {
query += ` AND t.created_by = ?`;
params.push(creatorFilter);
}
if (assigneeFilter) {
query += ` AND ta.user_id = ?`;
params.push(assigneeFilter);
}
if (deadlineFilter) {
const now = new Date();
let hours = 48;
if (deadlineFilter === '24h') hours = 24;
const deadlineTime = new Date(now.getTime() + hours * 60 * 60 * 1000);
const deadlineISO = deadlineTime.toISOString();
const nowISO = now.toISOString();
query += ` AND ta.due_date IS NOT NULL
AND ta.due_date > ?
AND ta.due_date <= ?
AND ta.status NOT IN ('completed', 'overdue')`;
params.push(nowISO, deadlineISO);
}
if (search) {
query += ` AND (t.title LIKE ? OR t.description LIKE ?)`;
const searchPattern = `%${search}%`;
params.push(searchPattern, searchPattern);
}
query += " GROUP BY t.id ORDER BY t.created_at DESC";
console.log('SQL Query:', query);
console.log('Params:', params);
db.all(query, params, (err, tasks) => {
if (err) {
res.status(500).json({ error: err.message });
return;
}
const taskPromises = tasks.map(task => {
return new Promise((resolve) => {
db.all(`
SELECT ta.*, u.name as user_name, u.login as user_login
FROM task_assignments ta
LEFT JOIN users u ON ta.user_id = u.id
WHERE ta.task_id = ?
`, [task.id], (err, assignments) => {
if (err) {
task.assignments = [];
resolve(task);
return;
}
assignments.forEach(assignment => {
if (checkIfOverdue(assignment.due_date, assignment.status) && assignment.status !== 'completed') {
assignment.status = 'overdue';
}
});
task.assignments = assignments || [];
resolve(task);
});
});
});
Promise.all(taskPromises).then(completedTasks => {
res.json(completedTasks);
});
});
});
// API для задач по типу
app.get('/api/tasks_by_type', requireAuth, (req, res) => {
const userId = req.session.user.id;
const userRole = req.session.user.role;
// Получаем параметры фильтрации из query string
const showDeleted = userRole === 'admin' && req.query.showDeleted === 'true';
const search = req.query.search || '';
const statusFilter = req.query.status || 'active,in_progress,assigned,overdue,rework';
const creatorFilter = req.query.creator || '';
const assigneeFilter = req.query.assignee || '';
const deadlineFilter = req.query.deadline || '';
const taskType = req.query.task_type || ''; // Новый параметр для фильтрации по типу задачи
let query = `
SELECT DISTINCT
t.*,
u.name as creator_name,
u.login as creator_login,
ot.title as original_task_title,
ou.name as original_creator_name,
GROUP_CONCAT(DISTINCT ta.user_id) as assigned_user_ids,
GROUP_CONCAT(DISTINCT u2.name) as assigned_user_names
FROM tasks t
LEFT JOIN users u ON t.created_by = u.id
LEFT JOIN tasks ot ON t.original_task_id = ot.id
LEFT JOIN users ou ON ot.created_by = ou.id
LEFT JOIN task_assignments ta ON t.id = ta.task_id
LEFT JOIN users u2 ON ta.user_id = u2.id
WHERE 1=1
`;
const params = [];
// Фильтрация по типу задачи
if (taskType) {
if (taskType.includes(',')) {
// Если перечислено несколько типов через запятую
const types = taskType.split(',');
query += ` AND t.task_type IN (${types.map(() => '?').join(',')})`;
types.forEach(type => params.push(type));
} else {
// Один конкретный тип
query += ` AND t.task_type = ?`;
params.push(taskType);
}
}
// Фильтрация по правам доступа
if (userRole !== 'admin') {
query += ` AND (t.created_by = ? OR ta.user_id = ?)`;
params.push(userId, userId);
}
// Фильтрация по удаленным задачам
if (!showDeleted) {
query += " AND t.status = 'active'";
}
// Фильтрация по статусу
if (statusFilter && statusFilter !== 'all') {
const statuses = statusFilter.split(',');
if (statuses.includes('closed')) {
if (userRole !== 'admin') {
query += ` AND (t.closed_at IS NOT NULL AND t.created_by = ?)`;
params.push(userId);
} else {
query += ` AND t.closed_at IS NOT NULL`;
}
} else {
query += ` AND t.closed_at IS NULL`;
if (statuses.length > 0 && !statuses.includes('all')) {
query += ` AND EXISTS (
SELECT 1 FROM task_assignments ta2
WHERE ta2.task_id = t.id AND ta2.status IN (${statuses.map(() => '?').join(',')})
)`;
statuses.forEach(status => params.push(status));
}
}
} else {
if (userRole !== 'admin') {
query += ` AND (t.closed_at IS NULL OR t.created_by = ?)`;
params.push(userId);
}
}
// Фильтрация по создателю
if (creatorFilter) {
query += ` AND t.created_by = ?`;
params.push(creatorFilter);
}
// Фильтрация по исполнителю
if (assigneeFilter) {
query += ` AND ta.user_id = ?`;
params.push(assigneeFilter);
}
// Фильтрация по дедлайну
if (deadlineFilter) {
const now = new Date();
let hours = 48;
if (deadlineFilter === '24h') hours = 24;
const deadlineTime = new Date(now.getTime() + hours * 60 * 60 * 1000);
const deadlineISO = deadlineTime.toISOString();
const nowISO = now.toISOString();
query += ` AND ta.due_date IS NOT NULL
AND ta.due_date > ?
AND ta.due_date <= ?
AND ta.status NOT IN ('completed', 'overdue')`;
params.push(nowISO, deadlineISO);
}
// Поиск по тексту
if (search) {
query += ` AND (t.title LIKE ? OR t.description LIKE ?)`;
const searchPattern = `%${search}%`;
params.push(searchPattern, searchPattern);
}
query += " GROUP BY t.id ORDER BY t.created_at DESC";
db.all(query, params, (err, tasks) => {
if (err) {
res.status(500).json({ error: err.message });
return;
}
const taskPromises = tasks.map(task => {
return new Promise((resolve) => {
db.all(`
SELECT ta.*, u.name as user_name, u.login as user_login
FROM task_assignments ta
LEFT JOIN users u ON ta.user_id = u.id
WHERE ta.task_id = ?
`, [task.id], (err, assignments) => {
if (err) {
task.assignments = [];
resolve(task);
return;
}
assignments.forEach(assignment => {
if (checkIfOverdue(assignment.due_date, assignment.status) && assignment.status !== 'completed') {
assignment.status = 'overdue';
}
});
task.assignments = assignments || [];
resolve(task);
});
});
});
Promise.all(taskPromises).then(completedTasks => {
// Добавляем мета-информацию о фильтрации
const response = {
tasks: completedTasks,
meta: {
total: completedTasks.length,
filters: {
task_type: taskType || 'all',
status: statusFilter,
creator: creatorFilter || 'all',
assignee: assigneeFilter || 'all',
deadline: deadlineFilter || 'none',
search: search || 'none'
}
}
};
res.json(response);
});
});
});
app.get('/api/tasks/no-date', requireAuth, (req, res) => {
const userId = req.session.user.id;
const query = `
SELECT DISTINCT
t.*,
u.name as creator_name,
u.login as creator_login,
ot.title as original_task_title,
ou.name as original_creator_name,
GROUP_CONCAT(DISTINCT ta.user_id) as assigned_user_ids,
GROUP_CONCAT(DISTINCT u2.name) as assigned_user_names
FROM tasks t
LEFT JOIN users u ON t.created_by = u.id
LEFT JOIN tasks ot ON t.original_task_id = ot.id
LEFT JOIN users ou ON ot.created_by = ou.id
LEFT JOIN task_assignments ta ON t.id = ta.task_id
LEFT JOIN users u2 ON ta.user_id = u2.id
WHERE t.status = 'active'
AND t.closed_at IS NULL
AND (t.due_date IS NULL OR t.due_date = '')
AND (ta.due_date IS NULL OR ta.due_date = '')
`;
const params = [];
if (req.session.user.role !== 'admin') {
query += ` AND (t.created_by = ? OR ta.user_id = ?)`;
params.push(userId, userId);
}
query += " GROUP BY t.id ORDER BY t.created_at DESC";
db.all(query, params, (err, tasks) => {
if (err) {
res.status(500).json({ error: err.message });
return;
}
const taskPromises = tasks.map(task => {
return new Promise((resolve) => {
db.all(`
SELECT ta.*, u.name as user_name, u.login as user_login
FROM task_assignments ta
LEFT JOIN users u ON ta.user_id = u.id
WHERE ta.task_id = ?
`, [task.id], (err, assignments) => {
if (err) {
task.assignments = [];
resolve(task);
return;
}
task.assignments = assignments || [];
resolve(task);
});
});
});
Promise.all(taskPromises).then(completedTasks => {
res.json(completedTasks);
});
});
});
// API для Канбан-доски с фильтром
app.get('/api/kanban-tasks', requireAuth, (req, res) => {
const userId = req.session.user.id;
const days = parseInt(req.query.days) || 7;
const filter = req.query.filter || 'all'; // 'all', 'created', 'assigned'
const now = new Date();
const futureDate = new Date(now.getTime() + days * 24 * 60 * 60 * 1000);
const futureISO = futureDate.toISOString();
let query = `
SELECT DISTINCT
t.*,
u.name as creator_name,
u.login as creator_login,
GROUP_CONCAT(DISTINCT ta.user_id) as assigned_user_ids,
GROUP_CONCAT(DISTINCT u2.name) as assigned_user_names
FROM tasks t
LEFT JOIN users u ON t.created_by = u.id
LEFT JOIN task_assignments ta ON t.id = ta.task_id
LEFT JOIN users u2 ON ta.user_id = u2.id
WHERE t.status = 'active'
AND t.closed_at IS NULL
AND (t.due_date IS NULL OR t.due_date <= ?)
`;
const params = [futureISO];
// Применяем фильтр
if (filter === 'created') {
// Задачи, которые я создал
query += ` AND t.created_by = ?`;
params.push(userId);
} else if (filter === 'assigned') {
// Задачи, которые мне назначены
query += ` AND ta.user_id = ?`;
params.push(userId);
} else {
// Все задачи (по умолчанию)
query += ` AND (t.created_by = ? OR ta.user_id = ?)`;
params.push(userId, userId);
}
query += " GROUP BY t.id ORDER BY t.due_date ASC, t.created_at DESC";
db.all(query, params, async (err, tasks) => {
if (err) {
res.status(500).json({ error: err.message });
return;
}
// Добавляем статус для Канбана и загружаем файлы
const tasksWithKanban = await Promise.all(tasks.map(async (task) => {
// Получаем назначения для задачи
const assignments = await new Promise((resolve) => {
db.all(`
SELECT ta.*, u.name as user_name, u.login as user_login
FROM task_assignments ta
LEFT JOIN users u ON ta.user_id = u.id
WHERE ta.task_id = ?
`, [task.id], (err, rows) => {
resolve(rows || []);
});
});
// Определяем статус для Канбана
let kanbanStatus = 'unassigned';
if (assignments.length === 0) {
kanbanStatus = 'unassigned';
} else {
const hasAssigned = assignments.some(a => a.status === 'assigned');
const hasInProgress = assignments.some(a => a.status === 'in_progress');
const hasOverdue = assignments.some(a => a.status === 'overdue');
const hasRework = assignments.some(a => a.status === 'rework');
const allCompleted = assignments.every(a => a.status === 'completed');
if (allCompleted) {
kanbanStatus = 'completed';
} else if (hasRework) {
kanbanStatus = 'rework';
} else if (hasOverdue) {
kanbanStatus = 'overdue';
} else if (hasInProgress) {
kanbanStatus = 'in_progress';
} else if (hasAssigned) {
kanbanStatus = 'assigned';
}
}
// Загружаем файлы
const files = await new Promise((resolve) => {
db.all(`
SELECT tf.*, u.name as user_name
FROM task_files tf
LEFT JOIN users u ON tf.user_id = u.id
WHERE tf.task_id = ?
ORDER BY tf.uploaded_at DESC
`, [task.id], (err, rows) => {
resolve(rows || []);
});
});
// Определяем роль пользователя в задаче
let userRole = 'observer';
if (parseInt(task.created_by) === userId) {
userRole = 'creator';
} else if (assignments.some(a => parseInt(a.user_id) === userId)) {
userRole = 'assignee';
}
return {
...task,
kanbanStatus,
assignments,
files,
userRole,
canEdit: canUserEditTask(task, req.session.user)
};
}));
res.json({
tasks: tasksWithKanban,
filter: filter,
userRole: req.session.user.role
});
});
});
// API для статистики Канбана
app.get('/api/kanban-stats', requireAuth, (req, res) => {
const userId = req.session.user.id;
const filter = req.query.filter || 'all';
const now = new Date();
const futureDate = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);
const futureISO = futureDate.toISOString();
let query = `
SELECT
COUNT(DISTINCT t.id) as total_tasks,
COUNT(DISTINCT CASE WHEN ta.status = 'assigned' THEN t.id END) as assigned_count,
COUNT(DISTINCT CASE WHEN ta.status = 'in_progress' THEN t.id END) as in_progress_count,
COUNT(DISTINCT CASE WHEN ta.status = 'rework' THEN t.id END) as rework_count,
COUNT(DISTINCT CASE WHEN ta.status = 'overdue' THEN t.id END) as overdue_count,
COUNT(DISTINCT CASE WHEN ta.status = 'completed' THEN t.id END) as completed_count,
COUNT(DISTINCT CASE WHEN ta.user_id IS NULL THEN t.id END) as unassigned_count,
COUNT(DISTINCT CASE WHEN t.created_by = ? THEN t.id END) as created_by_me,
COUNT(DISTINCT CASE WHEN ta.user_id = ? THEN t.id END) as assigned_to_me
FROM tasks t
LEFT JOIN task_assignments ta ON t.id = ta.task_id AND ta.user_id = ?
WHERE t.status = 'active'
AND t.closed_at IS NULL
AND (t.due_date IS NULL OR t.due_date <= ?)
`;
const params = [userId, userId, userId, futureISO];
// Применяем фильтр
if (filter === 'created') {
query += ` AND t.created_by = ?`;
params.push(userId);
} else if (filter === 'assigned') {
query += ` AND ta.user_id = ?`;
params.push(userId);
} else {
query += ` AND (t.created_by = ? OR ta.user_id = ?)`;
params.push(userId, userId);
}
db.get(query, params, (err, stats) => {
if (err) {
res.status(500).json({ error: err.message });
return;
}
res.json({
stats: stats || {
total_tasks: 0,
assigned_count: 0,
in_progress_count: 0,
rework_count: 0,
overdue_count: 0,
completed_count: 0,
unassigned_count: 0,
created_by_me: 0,
assigned_to_me: 0
},
filter: filter,
timestamp: new Date().toISOString()
});
});
});
// API для создания задачи согласования документов
app.post('/api/document-approval-tasks', requireAuth, upload.array('files', 15), (req, res) => {
const { title, description, approverGroupId, documentId, dueDate } = req.body;
const createdBy = req.session.user.id;
if (!title) {
return res.status(400).json({ error: 'Название задачи обязательно' });
}
if (!approverGroupId) {
return res.status(400).json({ error: 'Выберите группу для согласования' });
}
if (!dueDate) {
return res.status(400).json({ error: 'Дата и время выполнения обязательны' });
}
// Проверяем, может ли группа согласовывать документы
db.get("SELECT can_approve_documents FROM user_groups WHERE id = ?",
[approverGroupId], (err, group) => {
if (err || !group || !group.can_approve_documents) {
return res.status(400).json({ error: 'Выбранная группа не может согласовывать документы' });
}
db.serialize(async () => {
const startDate = new Date().toISOString();
// Создаем задачу с типом "document_approval"
db.run(
`INSERT INTO tasks (title, description, created_by, task_type,
approver_group_id, document_id, start_date, due_date)
VALUES (?, ?, ?, 'document_approval', ?, ?, ?, ?)`,
[title, description, createdBy, approverGroupId, documentId || null, startDate, dueDate || null],
async function(err) {
if (err) {
res.status(500).json({ error: err.message });
return;
}
const taskId = this.lastID;
// Сохраняем метаданные
saveTaskMetadata(taskId, title, description, createdBy, null, startDate, dueDate);
// Получаем всех пользователей из группы согласующих
const approvers = await getApproverUsers(approverGroupId);
// Назначаем задачу всем согласующим в группе
if (approvers.length > 0) {
approvers.forEach(approver => {
db.run(
"INSERT INTO task_assignments (task_id, user_id, status, start_date, due_date) VALUES (?, ?, 'assigned', ?, ?)",
[taskId, approver.id, startDate, dueDate || null]
);
});
// Отправляем уведомления
sendTaskNotifications('created', taskId, title, description, createdBy);
}
logActivity(taskId, createdBy, 'DOCUMENT_APPROVAL_TASK_CREATED',
`Создана задача согласования документа для группы ${approverGroupId}`);
// Загрузка файлов (если есть)
if (req.files && req.files.length > 0) {
const userFolder = createUserTaskFolder(taskId, req.session.user.login);
req.files.forEach(file => {
const newPath = path.join(userFolder, path.basename(file.filename));
fs.renameSync(file.path, newPath);
const originalName = file.originalname;
db.run(
"INSERT INTO task_files (task_id, user_id, filename, original_name, file_path, file_size) VALUES (?, ?, ?, ?, ?, ?)",
[taskId, createdBy, path.basename(file.filename), originalName, newPath, file.size]
);
logActivity(taskId, createdBy, 'FILE_UPLOADED', `Загружен файл: ${originalName}`);
});
}
res.json({
success: true,
taskId: taskId,
message: 'Задача согласования документа создана',
approversCount: approvers.length
});
}
);
});
});
});
// API для получения задач согласования документов
app.get('/api/document-approval-tasks', requireAuth, (req, res) => {
const userId = req.session.user.id;
const query = `
SELECT DISTINCT
t.*,
u.name as creator_name,
u.login as creator_login,
g.name as approver_group_name,
g.color as approver_group_color,
GROUP_CONCAT(DISTINCT ta.user_id) as assigned_user_ids,
GROUP_CONCAT(DISTINCT u2.name) as assigned_user_names
FROM tasks t
LEFT JOIN users u ON t.created_by = u.id
LEFT JOIN user_groups g ON t.approver_group_id = g.id
LEFT JOIN task_assignments ta ON t.id = ta.task_id
LEFT JOIN users u2 ON ta.user_id = u2.id
WHERE t.task_type = 'document_approval'
AND t.status = 'active'
AND t.closed_at IS NULL
`;
const params = [];
if (req.session.user.role !== 'admin') {
query += ` AND (t.created_by = ? OR ta.user_id = ?)`;
params.push(userId, userId);
}
query += " GROUP BY t.id ORDER BY t.created_at DESC";
db.all(query, params, (err, tasks) => {
if (err) {
res.status(500).json({ error: err.message });
return;
}
const taskPromises = tasks.map(task => {
return new Promise((resolve) => {
// Получаем назначения
db.all(`
SELECT ta.*, u.name as user_name, u.login as user_login
FROM task_assignments ta
LEFT JOIN users u ON ta.user_id = u.id
WHERE ta.task_id = ?
`, [task.id], (err, assignments) => {
if (err) {
task.assignments = [];
} else {
assignments.forEach(assignment => {
if (checkIfOverdue(assignment.due_date, assignment.status) && assignment.status !== 'completed') {
assignment.status = 'overdue';
}
});
task.assignments = assignments || [];
}
// Получаем информацию о группе согласующих
if (task.approver_group_id) {
db.get(`
SELECT name, color, can_approve_documents
FROM user_groups
WHERE id = ?
`, [task.approver_group_id], (err, group) => {
task.approver_group = group || null;
resolve(task);
});
} else {
resolve(task);
}
});
});
});
Promise.all(taskPromises).then(completedTasks => {
res.json(completedTasks);
});
});
});
// API для обновления статуса в Канбане
app.put('/api/kanban-tasks/:taskId/status', requireAuth, (req, res) => {
const { taskId } = req.params;
const { status } = req.body;
const currentUserId = req.session.user.id;
if (!status) {
return res.status(400).json({ error: 'Статус обязателен' });
}
// Проверяем, что пользователь назначен на задачу
db.get("SELECT 1 FROM task_assignments WHERE task_id = ? AND user_id = ?",
[taskId, currentUserId], (err, assignment) => {
if (err || !assignment) {
return res.status(403).json({ error: 'Вы не назначены на эту задачу' });
}
// Определяем реальный статус из канбан-статуса
let actualStatus = status;
if (status === 'unassigned') {
actualStatus = 'assigned'; // Нельзя вернуть в не назначенные
}
db.run(
"UPDATE task_assignments SET status = ?, updated_at = CURRENT_TIMESTAMP WHERE task_id = ? AND user_id = ?",
[actualStatus, taskId, currentUserId],
function(err) {
if (err) {
res.status(500).json({ error: err.message });
return;
}
if (this.changes === 0) {
return res.status(404).json({ error: 'Назначение не найдено' });
}
logActivity(taskId, currentUserId, 'STATUS_CHANGED', `Статус изменен на: ${actualStatus} через Канбан`);
// Отправляем уведомление
db.get("SELECT title, description FROM tasks WHERE id = ?", [taskId], (err, taskData) => {
if (!err && taskData) {
sendTaskNotifications(
'status_changed',
taskId,
taskData.title,
taskData.description,
currentUserId,
'',
actualStatus,
req.session.user.name
);
}
});
res.json({ success: true, message: 'Статус обновлен' });
}
);
});
});
app.post('/api/tasks', requireAuth, upload.array('files', 15), (req, res) => {
const { title, description, assignedUsers, originalTaskId, dueDate } = req.body;
const createdBy = req.session.user.id;
// ПРОВЕРКА ЧТО ПРИХОДИТ В ЗАПРОСЕ
console.log('📋 ДАННЫЕ ИЗ ЗАПРОСА:');
console.log('req.body:', JSON.stringify(req.body, null, 2));
console.log('req.body.taskType:', req.body.taskType);
console.log('Все поля req.body:', Object.keys(req.body));
// Проверяем заголовки запроса
console.log('Content-Type заголовок:', req.headers['content-type']);
if (!title) {
return res.status(400).json({ error: 'Название задачи обязательно' });
}
if (!dueDate) {
return res.status(400).json({ error: 'Дата и время выполнения обязательны' });
}
db.serialize(() => {
const startDate = new Date().toISOString();
const taskType = req.body.taskType || 'regular';
db.run(
"INSERT INTO tasks (title, description, created_by, original_task_id, start_date, due_date, task_type) VALUES (?, ?, ?, ?, ?, ?, ?)",
[title, description, createdBy, originalTaskId || null, startDate, dueDate || null, taskType],
function(err) {
if (err) {
res.status(500).json({ error: err.message });
return;
}
const taskId = this.lastID;
saveTaskMetadata(taskId, title, description, createdBy, originalTaskId, startDate, dueDate);
const action = originalTaskId ? 'TASK_COPIED' : 'TASK_CREATED';
const details = originalTaskId ?
`Создана копия задачи: ${title}` :
`Создана задача: ${title}`;
logActivity(taskId, createdBy, action, details);
if (req.files && req.files.length > 0) {
const userFolder = createUserTaskFolder(taskId, req.session.user.login);
req.files.forEach(file => {
const newPath = path.join(userFolder, path.basename(file.filename));
fs.renameSync(file.path, newPath);
const originalName = file.originalname;
db.run(
"INSERT INTO task_files (task_id, user_id, filename, original_name, file_path, file_size) VALUES (?, ?, ?, ?, ?, ?)",
[taskId, createdBy, path.basename(file.filename), originalName, newPath, file.size]
);
logActivity(taskId, createdBy, 'FILE_UPLOADED', `Загружен файл: ${originalName}`);
});
const tempDir = path.join(__dirname, 'data', 'uploads', 'temp');
if (fs.existsSync(tempDir)) {
fs.rmSync(tempDir, { recursive: true, force: true });
}
}
if (assignedUsers) {
const userIds = Array.isArray(assignedUsers) ? assignedUsers : [assignedUsers];
userIds.forEach(userId => {
db.run(
"INSERT INTO task_assignments (task_id, user_id, start_date, due_date) VALUES (?, ?, ?, ?)",
[taskId, userId, startDate, dueDate || null]
);
logActivity(taskId, createdBy, 'TASK_ASSIGNED', `Задача назначена пользователю ${userId}`);
});
sendTaskNotifications('created', taskId, title, description, createdBy);
}
res.json({
success: true,
taskId: taskId,
message: originalTaskId ? 'Копия задачи создана' : 'Задача успешно создана'
});
}
);
});
});
app.post('/api/tasks/:taskId/copy', requireAuth, (req, res) => {
const { taskId } = req.params;
const { assignedUsers, dueDate } = req.body;
const createdBy = req.session.user.id;
if (!dueDate) {
return res.status(400).json({ error: 'Дата и время выполнения обязательны для копии задачи' });
}
checkTaskAccess(createdBy, taskId, (err, hasAccess) => {
if (err || !hasAccess) {
return res.status(404).json({ error: 'Задача не найдена или у вас нет прав доступа' });
}
db.serialize(() => {
db.get("SELECT title, description FROM tasks WHERE id = ?", [taskId], (err, originalTask) => {
if (err || !originalTask) {
return res.status(404).json({ error: 'Оригинальная задача не найдена' });
}
const newTitle = `Копия: ${originalTask.title}`;
const startDate = new Date().toISOString();
db.run(
"INSERT INTO tasks (title, description, created_by, original_task_id, start_date, due_date, task_type) VALUES (?, ?, ?, ?, ?, ?, ?)",
[newTitle, originalTask.description, createdBy, taskId, startDate, dueDate || null, originalTask.task_type || 'regular'],
function(err) {
if (err) {
res.status(500).json({ error: err.message });
return;
}
const newTaskId = this.lastID;
saveTaskMetadata(newTaskId, newTitle, originalTask.description, createdBy, taskId, startDate, dueDate);
logActivity(newTaskId, createdBy, 'TASK_COPIED', `Создана копия задачи: ${newTitle}`);
db.all("SELECT * FROM task_files WHERE task_id = ?", [taskId], (err, originalFiles) => {
if (!err && originalFiles && originalFiles.length > 0) {
originalFiles.forEach(originalFile => {
const originalFilePath = originalFile.file_path;
const newFilename = Date.now() + '-' + Math.round(Math.random() * 1E9) + path.extname(originalFile.original_name);
const userFolder = createUserTaskFolder(newTaskId, req.session.user.login);
const newFilePath = path.join(userFolder, newFilename);
if (fs.existsSync(originalFilePath)) {
fs.copyFileSync(originalFilePath, newFilePath);
db.run(
"INSERT INTO task_files (task_id, user_id, filename, original_name, file_path, file_size) VALUES (?, ?, ?, ?, ?, ?)",
[newTaskId, createdBy, newFilename, originalFile.original_name, newFilePath, originalFile.file_size]
);
logActivity(newTaskId, createdBy, 'FILE_COPIED', `Скопирован файл: ${originalFile.original_name}`);
}
});
}
});
if (assignedUsers && assignedUsers.length > 0) {
assignedUsers.forEach(userId => {
db.run(
"INSERT INTO task_assignments (task_id, user_id, start_date, due_date) VALUES (?, ?, ?, ?)",
[newTaskId, userId, startDate, dueDate || null]
);
});
logActivity(newTaskId, createdBy, 'TASK_ASSIGNED', `Задача назначена пользователям: ${assignedUsers.join(', ')}`);
sendTaskNotifications('created', newTaskId, newTitle, originalTask.description, createdBy);
}
res.json({
success: true,
taskId: newTaskId,
message: 'Копия задачи успешно создана'
});
}
);
});
});
});
});
app.put('/api/tasks/:taskId', requireAuth, upload.array('files', 15), (req, res) => {
const { taskId } = req.params;
const { title, description, assignedUsers, dueDate } = req.body;
const userId = req.session.user.id;
if (!title) {
return res.status(400).json({ error: 'Название задачи обязательно' });
}
if (!dueDate) {
return res.status(400).json({ error: 'Дата и время выполнения обязательны' });
}
db.get("SELECT created_by, status, closed_at FROM tasks WHERE id = ?", [taskId], (err, task) => {
if (err || !task) {
return res.status(404).json({ error: 'Задача не найдена' });
}
// Проверяем права на редактирование
if (!canUserEditTask(task, req.session.user)) {
return res.status(403).json({ error: 'У вас нет прав для редактирования этой задачи' });
}
const taskType = req.body.taskType || 'regular';
db.serialize(() => {
db.run(
"UPDATE tasks SET title = ?, description = ?, due_date = ?, task_type = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?",
[title, description, dueDate || null, taskType, taskId],
function(err) {
if (err) {
res.status(500).json({ error: err.message });
return;
}
updateTaskMetadata(taskId, { title, description, due_date: dueDate });
logActivity(taskId, userId, 'TASK_UPDATED', `Задача обновлена: ${title}`);
if (req.files && req.files.length > 0) {
const userFolder = createUserTaskFolder(taskId, req.session.user.login);
req.files.forEach(file => {
const newPath = path.join(userFolder, path.basename(file.filename));
fs.renameSync(file.path, newPath);
const originalName = file.originalname;
db.run(
"INSERT INTO task_files (task_id, user_id, filename, original_name, file_path, file_size) VALUES (?, ?, ?, ?, ?, ?)",
[taskId, userId, path.basename(file.filename), originalName, newPath, file.size]
);
logActivity(taskId, userId, 'FILE_UPLOADED', `Загружен файл: ${originalName}`);
});
const tempDir = path.join(__dirname, 'data', 'uploads', 'temp');
if (fs.existsSync(tempDir)) {
fs.rmSync(tempDir, { recursive: true, force: true });
}
}
if (assignedUsers) {
db.run("DELETE FROM task_assignments WHERE task_id = ?", [taskId], (err) => {
if (err) {
console.error('❌ Ошибка удаления старых назначений:', err);
}
const userIds = Array.isArray(assignedUsers) ? assignedUsers : [assignedUsers];
userIds.forEach(userId => {
const startDate = new Date().toISOString();
db.run(
"INSERT INTO task_assignments (task_id, user_id, start_date, due_date) VALUES (?, ?, ?, ?)",
[taskId, userId, startDate, dueDate || null]
);
});
logActivity(taskId, userId, 'TASK_ASSIGNMENTS_UPDATED', `Назначения обновлены`);
sendTaskNotifications('updated', taskId, title, description, userId);
});
} else {
sendTaskNotifications('updated', taskId, title, description, userId);
}
res.json({ success: true, message: 'Задача обновлена' });
}
);
});
});
});
app.post('/api/tasks/:taskId/rework', requireAuth, (req, res) => {
const { taskId } = req.params;
const { comment } = req.body;
const userId = req.session.user.id;
db.get("SELECT created_by, status FROM tasks WHERE id = ?", [taskId], (err, task) => {
if (err || !task) {
return res.status(404).json({ error: 'Задача не найдена' });
}
// Проверяем права на возврат на доработку
if (!canUserEditTask(task, req.session.user)) {
return res.status(403).json({ error: 'У вас нет прав для возврата задачи на доработку' });
}
db.serialize(() => {
db.run(
"UPDATE tasks SET rework_comment = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?",
[comment || 'Требуется доработка', taskId],
function(err) {
if (err) {
res.status(500).json({ error: err.message });
return;
}
db.run(
"UPDATE task_assignments SET status = 'rework', rework_comment = ?, updated_at = CURRENT_TIMESTAMP WHERE task_id = ?",
[comment || 'Требуется доработка', taskId],
function(err) {
if (err) {
res.status(500).json({ error: err.message });
return;
}
logActivity(taskId, userId, 'TASK_SENT_FOR_REWORK', `Задача возвращена на доработку: ${comment}`);
db.get("SELECT title, description FROM tasks WHERE id = ?", [taskId], (err, taskData) => {
if (!err && taskData) {
sendTaskNotifications('rework', taskId, taskData.title, taskData.description, userId, comment);
}
});
res.json({ success: true, message: 'Задача возвращена на доработку' });
}
);
}
);
});
});
});
app.post('/api/tasks/:taskId/close', requireAuth, (req, res) => {
const { taskId } = req.params;
const userId = req.session.user.id;
db.get("SELECT created_by FROM tasks WHERE id = ?", [taskId], (err, task) => {
if (err || !task) {
return res.status(404).json({ error: 'Задача не найдена' });
}
// Создатель и администратор могут закрывать задачу
if (req.session.user.role !== 'admin' && task.created_by !== userId) {
return res.status(403).json({ error: 'У вас нет прав для закрытия этой задачи' });
}
db.run(
"UPDATE tasks SET closed_at = CURRENT_TIMESTAMP, closed_by = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?",
[userId, taskId],
function(err) {
if (err) {
res.status(500).json({ error: err.message });
return;
}
logActivity(taskId, userId, 'TASK_CLOSED', `Задача закрыта`);
db.get("SELECT title FROM tasks WHERE id = ?", [taskId], (err, taskData) => {
if (!err && taskData) {
sendTaskNotifications('closed', taskId, taskData.title, '', userId);
}
});
res.json({ success: true, message: 'Задача закрыта' });
}
);
});
});
app.post('/api/tasks/:taskId/reopen', requireAuth, (req, res) => {
const { taskId } = req.params;
const userId = req.session.user.id;
db.get("SELECT created_by FROM tasks WHERE id = ?", [taskId], (err, task) => {
if (err || !task) {
return res.status(404).json({ error: 'Задача не найдена' });
}
// Создатель и администратор могут открывать задачу
if (req.session.user.role !== 'admin' && task.created_by !== userId) {
return res.status(403).json({ error: 'У вас нет прав для открытия этой задачи' });
}
db.run(
"UPDATE tasks SET closed_at = NULL, closed_by = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?",
[taskId],
function(err) {
if (err) {
res.status(500).json({ error: err.message });
return;
}
logActivity(taskId, userId, 'TASK_REOPENED', `Задача открыта`);
res.json({ success: true, message: 'Задача открыта' });
}
);
});
});
app.put('/api/tasks/:taskId/assignment/:userId', requireAuth, (req, res) => {
const { taskId, userId } = req.params;
const { dueDate } = req.body;
const currentUserId = req.session.user.id;
if (!dueDate) {
return res.status(400).json({ error: 'Дата и время выполнения обязательны' });
}
db.get("SELECT created_by FROM tasks WHERE id = ?", [taskId], (err, task) => {
if (err || !task) {
return res.status(404).json({ error: 'Задача не найдена' });
}
// Проверяем права на изменение сроков
if (!canUserEditTask(task, req.session.user)) {
return res.status(403).json({ error: 'У вас нет прав для редактирования сроков' });
}
db.run(
"UPDATE task_assignments SET due_date = ?, updated_at = CURRENT_TIMESTAMP WHERE task_id = ? AND user_id = ?",
[dueDate || null, taskId, userId],
function(err) {
if (err) {
res.status(500).json({ error: err.message });
return;
}
if (this.changes === 0) {
return res.status(404).json({ error: 'Назначение не найдено' });
}
logActivity(taskId, currentUserId, 'ASSIGNMENT_UPDATED', `Обновлены сроки для пользователя ${userId}`);
db.get("SELECT title, description FROM tasks WHERE id = ?", [taskId], (err, taskData) => {
if (!err && taskData) {
sendTaskNotifications('updated', taskId, taskData.title, taskData.description, currentUserId);
}
});
res.json({ success: true, message: 'Сроки обновлены' });
}
);
});
});
app.delete('/api/tasks/:taskId', requireAuth, (req, res) => {
const { taskId } = req.params;
const userId = req.session.user.id;
db.get("SELECT created_by, status FROM tasks WHERE id = ? AND status = 'active'", [taskId], (err, task) => {
if (err || !task) {
return res.status(404).json({ error: 'Задача не найдена' });
}
// Проверяем права на удаление
if (!canUserEditTask(task, req.session.user)) {
return res.status(403).json({ error: 'У вас нет прав для удаления этой задачи' });
}
db.run(
"UPDATE tasks SET status = 'deleted', deleted_at = CURRENT_TIMESTAMP, deleted_by = ? WHERE id = ?",
[userId, taskId],
function(err) {
if (err) {
res.status(500).json({ error: err.message });
return;
}
updateTaskMetadata(taskId, {
status: 'deleted',
deleted_at: new Date().toISOString(),
deleted_by: userId
});
logActivity(taskId, userId, 'TASK_DELETED', `Задача помечена как удаленная`);
res.json({ success: true, message: 'Задача удалена' });
}
);
});
});
app.post('/api/tasks/:taskId/restore', requireAuth, (req, res) => {
const { taskId } = req.params;
const userId = req.session.user.id;
if (req.session.user.role !== 'admin') {
return res.status(403).json({ error: 'Недостаточно прав' });
}
db.run(
"UPDATE tasks SET status = 'active', deleted_at = NULL, deleted_by = NULL WHERE id = ?",
[taskId],
function(err) {
if (err) {
res.status(500).json({ error: err.message });
return;
}
if (this.changes === 0) {
return res.status(404).json({ error: 'Задача не найдена' });
}
updateTaskMetadata(taskId, {
status: 'active',
deleted_at: null,
deleted_by: null
});
logActivity(taskId, userId, 'TASK_RESTORED', `Задача восстановлена`);
res.json({ success: true, message: 'Задача восстановлена' });
}
);
});
app.put('/api/tasks/:taskId/status', requireAuth, (req, res) => {
const { taskId } = req.params;
const { userId: targetUserId, status } = req.body;
const currentUserId = req.session.user.id;
if (parseInt(targetUserId) !== currentUserId) {
return res.status(403).json({ error: 'Недостаточно прав' });
}
if (!targetUserId || !status) {
return res.status(400).json({ error: 'userId и status обязательны' });
}
db.get("SELECT 1 FROM task_assignments WHERE task_id = ? AND user_id = ?", [taskId, currentUserId], (err, assignment) => {
if (err || !assignment) {
return res.status(403).json({ error: 'Вы не назначены на эту задачу' });
}
db.get(`
SELECT t.title, t.description, u.name as user_name
FROM tasks t
LEFT JOIN users u ON u.id = ?
WHERE t.id = ?
`, [currentUserId, taskId], (err, taskData) => {
if (err) {
console.error('❌ Ошибка получения данных задачи:', err);
}
const finalStatus = status === 'completed' ? 'completed' : status;
db.run(
"UPDATE task_assignments SET status = ?, updated_at = CURRENT_TIMESTAMP WHERE task_id = ? AND user_id = ?",
[finalStatus, taskId, targetUserId],
function(err) {
if (err) {
res.status(500).json({ error: err.message });
return;
}
if (this.changes === 0) {
res.status(404).json({ error: 'Назначение не найдено' });
return;
}
logActivity(taskId, targetUserId, 'STATUS_CHANGED', `Статус изменен на: ${finalStatus}`);
if (taskData) {
sendTaskNotifications(
'status_changed',
taskId,
taskData.title,
taskData.description,
currentUserId,
'',
finalStatus,
taskData.user_name || req.session.user.name
);
}
res.json({ success: true, message: 'Статус обновлен' });
}
);
});
});
});
// API для отправки конкретного исполнителя на доработку
app.put('/api/tasks/:taskId/rework-assignment/:userId', requireAuth, (req, res) => {
const { taskId, userId } = req.params;
const { comment } = req.body;
const currentUserId = req.session.user.id;
console.log('=== ОТПРАВКА НА ДОРАБОТКУ ===');
console.log('taskId:', taskId);
console.log('userId:', userId);
console.log('comment:', comment);
console.log('==============================');
if (!comment) {
return res.status(400).json({ error: 'Комментарий к доработке обязателен' });
}
db.get("SELECT created_by, status FROM tasks WHERE id = ?", [taskId], (err, task) => {
if (err || !task) {
return res.status(404).json({ error: 'Задача не найдена' });
}
// Проверяем права: автор задачи или администратор
if (req.session.user.role !== 'admin' && parseInt(task.created_by) !== parseInt(currentUserId)) {
return res.status(403).json({ error: 'Только автор задачи может отправлять на доработку' });
}
// Проверяем существование назначения
db.get("SELECT id, status FROM task_assignments WHERE task_id = ? AND user_id = ?",
[taskId, userId],
(err, assignment) => {
if (err) {
return res.status(500).json({ error: err.message });
}
if (!assignment) {
return res.status(404).json({ error: 'Исполнитель не назначен на эту задачу' });
}
// Обновляем статус конкретного исполнителя
db.run(
`UPDATE task_assignments
SET status = 'rework',
rework_comment = ?,
updated_at = CURRENT_TIMESTAMP
WHERE task_id = ? AND user_id = ?`,
[comment, taskId, userId],
function(err) {
if (err) {
res.status(500).json({ error: err.message });
return;
}
// Логируем действие
try {
logActivity(parseInt(taskId), currentUserId, 'ASSIGNMENT_REWORK',
`Исполнитель ${userId} отправлен на доработку: ${comment.substring(0, 50)}`);
} catch (e) {
console.error('Ошибка логирования:', e);
}
// Получаем данные для уведомления
db.get(`SELECT t.title, t.description, u.name as executor_name
FROM tasks t
LEFT JOIN users u ON u.id = ?
WHERE t.id = ?`,
[userId, taskId], (err, taskData) => {
if (!err && taskData) {
try {
sendTaskNotifications(
'rework_assignment',
taskId,
taskData.title,
taskData.description,
currentUserId,
comment,
'rework',
req.session.user.name,
userId
);
} catch (e) {
console.error('Ошибка отправки уведомления:', e);
}
}
});
res.json({
success: true,
message: 'Исполнитель отправлен на доработку'
});
}
);
}
);
});
});
// API для принудительного завершения задачи исполнителем (для автора/админа)
app.put('/api/tasks/:taskId/force-complete/:userId', requireAuth, (req, res) => {
const { taskId, userId } = req.params;
const currentUserId = req.session.user.id;
console.log('=== ПРИНУДИТЕЛЬНОЕ ЗАВЕРШЕНИЕ ===');
console.log('taskId:', taskId);
console.log('userId:', userId);
console.log('currentUserId:', currentUserId);
console.log('==================================');
db.get("SELECT created_by, title, description, closed_at FROM tasks WHERE id = ?", [taskId], (err, task) => {
if (err) {
console.error('❌ Ошибка при поиске задачи:', err);
return res.status(500).json({ error: 'Ошибка базы данных' });
}
if (!task) {
return res.status(404).json({ error: 'Задача не найдена' });
}
// Проверяем права: автор задачи или администратор
if (req.session.user.role !== 'admin' && parseInt(task.created_by) !== parseInt(currentUserId)) {
return res.status(403).json({ error: 'Только автор задачи может принудительно завершать выполнение' });
}
// Проверяем, что задача не закрыта
if (task.closed_at) {
return res.status(400).json({ error: 'Задача уже закрыта' });
}
// Проверяем существование назначения
db.get("SELECT id, status FROM task_assignments WHERE task_id = ? AND user_id = ?",
[taskId, userId],
(err, assignment) => {
if (err) {
console.error('❌ Ошибка при поиске назначения:', err);
return res.status(500).json({ error: 'Ошибка базы данных' });
}
if (!assignment) {
return res.status(404).json({ error: 'Исполнитель не назначен на эту задачу' });
}
// Если уже выполнено, просто возвращаем успех
if (assignment.status === 'completed') {
return res.json({
success: true,
message: 'Задача уже была отмечена как выполненная'
});
}
// Обновляем статус конкретного исполнителя на "completed"
db.run(
`UPDATE task_assignments
SET status = 'completed',
updated_at = CURRENT_TIMESTAMP
WHERE task_id = ? AND user_id = ?`,
[taskId, userId],
function(err) {
if (err) {
console.error('❌ Ошибка при обновлении статуса:', err);
return res.status(500).json({ error: err.message });
}
console.log(`✅ Статус исполнителя ${userId} обновлен на completed`);
// Логируем действие
try {
logActivity(parseInt(taskId), currentUserId, 'ASSIGNMENT_FORCE_COMPLETED',
`Исполнитель ${userId} принудительно отмечен как выполнивший задачу`);
} catch (e) {
console.error('Ошибка логирования:', e);
}
// Проверяем, все ли исполнители выполнили задачу
db.all(
"SELECT status FROM task_assignments WHERE task_id = ?",
[taskId],
(err, assignments) => {
if (!err && assignments && assignments.length > 0) {
const allCompleted = assignments.every(a => a.status === 'completed');
if (allCompleted) {
// Если все исполнители выполнили, автоматически закрываем задачу
db.run(
"UPDATE tasks SET closed_at = CURRENT_TIMESTAMP, closed_by = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?",
[currentUserId, taskId],
function(err) {
if (!err) {
console.log(`✅ Задача ${taskId} автоматически закрыта`);
try {
logActivity(parseInt(taskId), currentUserId, 'TASK_CLOSED',
'Задача автоматически закрыта после выполнения всеми исполнителями');
} catch (e) {
console.error('Ошибка логирования:', e);
}
}
}
);
}
}
}
);
res.json({
success: true,
message: 'Исполнитель отмечен как выполнивший задачу'
});
}
);
}
);
});
});
app.post('/api/tasks/:taskId/files', requireAuth, upload.array('files', 15), (req, res) => {
const { taskId } = req.params;
const userId = req.session.user.id;
if (!req.files || req.files.length === 0) {
return res.status(400).json({ error: 'Нет файлов для загрузки' });
}
// Проверяем доступ к задаче через checkTaskAccess
const { checkTaskAccess } = require('./database');
checkTaskAccess(userId, taskId, (err, hasAccess) => {
if (err || !hasAccess) {
return res.status(403).json({ error: 'Нет доступа к задаче' });
}
// Дополнительно проверяем, что задача активна
db.get("SELECT status, closed_at FROM tasks WHERE id = ?", [taskId], (err, task) => {
if (err || !task) {
return res.status(404).json({ error: 'Задача не найдена' });
}
if (task.closed_at) {
return res.status(400).json({ error: 'Задача уже закрыта' });
}
const userFolder = createUserTaskFolder(taskId, req.session.user.login);
const uploadPromises = req.files.map(file => {
return new Promise((resolve, reject) => {
const newPath = path.join(userFolder, path.basename(file.filename));
fs.renameSync(file.path, newPath);
const originalName = file.originalname;
db.run(
"INSERT INTO task_files (task_id, user_id, filename, original_name, file_path, file_size) VALUES (?, ?, ?, ?, ?, ?)",
[taskId, userId, path.basename(file.filename), originalName, newPath, file.size],
function(err) {
if (err) {
reject(err);
return;
}
logActivity(taskId, userId, 'FILE_UPLOADED', `Загружен файл: ${originalName}`);
resolve({
id: this.lastID,
originalName,
size: file.size
});
}
);
});
});
Promise.all(uploadPromises)
.then((uploadedFiles) => {
res.json({
success: true,
message: 'Файлы успешно загружены',
files: uploadedFiles,
count: uploadedFiles.length
});
})
.catch((error) => {
console.error('Ошибка при загрузке файлов:', error);
res.status(500).json({ error: 'Ошибка при сохранении файлов в БД' });
});
});
});
});
// API для получения одной задачи
app.get('/api/tasks/:taskId', requireAuth, (req, res) => {
const { taskId } = req.params;
const userId = req.session.user.id;
checkTaskAccess(userId, taskId, (err, hasAccess) => {
if (err || !hasAccess) {
return res.status(404).json({ error: 'Задача не найдена или у вас нет прав доступа' });
}
const query = `
SELECT t.*, u.name as creator_name, u.login as creator_login,
ot.title as original_task_title, ou.name as original_creator_name
FROM tasks t
LEFT JOIN users u ON t.created_by = u.id
LEFT JOIN tasks ot ON t.original_task_id = ot.id
LEFT JOIN users ou ON ot.created_by = ou.id
WHERE t.id = ?
`;
db.get(query, [taskId], async (err, task) => {
if (err || !task) {
return res.status(404).json({ error: 'Задача не найдена' });
}
// Получаем назначения
const assignments = await new Promise((resolve) => {
db.all(`
SELECT ta.*, u.name as user_name, u.login as user_login
FROM task_assignments ta
LEFT JOIN users u ON ta.user_id = u.id
WHERE ta.task_id = ?
`, [taskId], (err, rows) => {
resolve(rows || []);
});
});
// Получаем файлы
const files = await new Promise((resolve) => {
db.all(`
SELECT tf.*, u.name as user_name
FROM task_files tf
LEFT JOIN users u ON tf.user_id = u.id
WHERE tf.task_id = ?
ORDER BY tf.uploaded_at DESC
`, [taskId], (err, rows) => {
resolve(rows || []);
});
});
task.assignments = assignments;
task.files = files;
task.canEdit = canUserEditTask(task, req.session.user);
res.json(task);
});
});
});
}
module.exports = { setupTaskEndpoints,getApproverUsers };