1482 lines
64 KiB
JavaScript
1482 lines
64 KiB
JavaScript
// 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;
|
||
|
||
// Создатель может редактировать свою задачу только если она не назначена другим
|
||
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 || '';
|
||
|
||
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 (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";
|
||
|
||
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);
|
||
});
|
||
});
|
||
});
|
||
|
||
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;
|
||
|
||
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) !== currentUserId) {
|
||
return res.status(403).json({ error: 'Только автор задачи может отправлять на доработку' });
|
||
}
|
||
|
||
db.serialize(() => {
|
||
// Обновляем статус конкретного исполнителя
|
||
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;
|
||
}
|
||
|
||
if (this.changes === 0) {
|
||
return res.status(404).json({ error: 'Исполнитель не найден в задаче' });
|
||
}
|
||
|
||
// Логируем действие
|
||
const { logActivity } = require('./database');
|
||
if (logActivity) {
|
||
logActivity(taskId, currentUserId, 'ASSIGNMENT_REWORK',
|
||
`Исполнитель ${userId} отправлен на доработку: ${comment}`);
|
||
}
|
||
|
||
// Получаем данные для уведомления
|
||
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) {
|
||
const { sendTaskNotifications } = require('./notifications');
|
||
sendTaskNotifications(
|
||
'rework_assignment',
|
||
taskId,
|
||
taskData.title,
|
||
taskData.description,
|
||
currentUserId,
|
||
comment,
|
||
'rework',
|
||
req.session.user.name,
|
||
userId // ID исполнителя для персонального уведомления
|
||
);
|
||
}
|
||
});
|
||
|
||
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 }; |