управления исполнителями
This commit is contained in:
555
api-users.js
Normal file
555
api-users.js
Normal file
@@ -0,0 +1,555 @@
|
||||
// api-users.js - API для управления исполнителями в задачах
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
|
||||
module.exports = function(app, db) {
|
||||
|
||||
// Проверка прав доступа к задаче
|
||||
function checkTaskAccess(userId, taskId, callback) {
|
||||
db.get(`
|
||||
SELECT t.created_by, ta.user_id
|
||||
FROM tasks t
|
||||
LEFT JOIN task_assignments ta ON t.id = ta.task_id AND ta.user_id = ?
|
||||
WHERE t.id = ? AND t.status = 'active'
|
||||
`, [userId, taskId], (err, result) => {
|
||||
if (err || !result) {
|
||||
return callback(err || new Error('Задача не найдена'), false);
|
||||
}
|
||||
|
||||
// Проверяем права: создатель или администратор
|
||||
if (parseInt(result.created_by) === parseInt(userId)) {
|
||||
return callback(null, true);
|
||||
}
|
||||
|
||||
// Проверяем, является ли пользователь администратором
|
||||
db.get("SELECT role FROM users WHERE id = ?", [userId], (err, user) => {
|
||||
if (err || !user) {
|
||||
return callback(err || new Error('Пользователь не найден'), false);
|
||||
}
|
||||
|
||||
const isAdmin = user.role === 'admin';
|
||||
callback(null, isAdmin);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// API для получения доступных исполнителей для добавления
|
||||
router.get('/api/tasks/:taskId/available-assignees', (req, res) => {
|
||||
const { taskId } = req.params;
|
||||
const userId = req.session.user.id;
|
||||
|
||||
checkTaskAccess(userId, taskId, (err, hasAccess) => {
|
||||
if (err || !hasAccess) {
|
||||
return res.status(403).json({ error: 'Нет доступа к задаче' });
|
||||
}
|
||||
|
||||
// Получаем текущих исполнителей
|
||||
db.all(`
|
||||
SELECT user_id
|
||||
FROM task_assignments
|
||||
WHERE task_id = ? AND status != 'deleted'
|
||||
`, [taskId], (err, currentAssignees) => {
|
||||
if (err) {
|
||||
return res.status(500).json({ error: err.message });
|
||||
}
|
||||
|
||||
const currentAssigneeIds = currentAssignees.map(a => a.user_id);
|
||||
|
||||
// Получаем всех пользователей, кроме текущих исполнителей
|
||||
db.all(`
|
||||
SELECT id, login, name, email, role, auth_type
|
||||
FROM users
|
||||
WHERE id NOT IN (${currentAssigneeIds.map(() => '?').join(',')})
|
||||
AND id != ?
|
||||
ORDER BY name
|
||||
`, [...currentAssigneeIds, userId], (err, users) => {
|
||||
if (err) {
|
||||
return res.status(500).json({ error: err.message });
|
||||
}
|
||||
|
||||
res.json(users);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// API для добавления исполнителя к задаче
|
||||
router.post('/api/tasks/:taskId/assignees', (req, res) => {
|
||||
const { taskId } = req.params;
|
||||
const userId = req.session.user.id;
|
||||
const { assigneeIds } = req.body;
|
||||
|
||||
if (!Array.isArray(assigneeIds) || assigneeIds.length === 0) {
|
||||
return res.status(400).json({ error: 'Не указаны исполнители' });
|
||||
}
|
||||
|
||||
checkTaskAccess(userId, taskId, (err, hasAccess) => {
|
||||
if (err || !hasAccess) {
|
||||
return res.status(403).json({ error: 'Нет доступа к задаче' });
|
||||
}
|
||||
|
||||
// Получаем информацию о задаче
|
||||
db.get("SELECT due_date FROM tasks WHERE id = ?", [taskId], (err, task) => {
|
||||
if (err || !task) {
|
||||
return res.status(404).json({ error: 'Задача не найдена' });
|
||||
}
|
||||
|
||||
// Начинаем транзакцию
|
||||
db.serialize(() => {
|
||||
db.run("BEGIN TRANSACTION");
|
||||
|
||||
const errors = [];
|
||||
const addedAssignees = [];
|
||||
|
||||
// Добавляем каждого исполнителя
|
||||
assigneeIds.forEach((assigneeId, index) => {
|
||||
db.run(`
|
||||
INSERT OR IGNORE INTO task_assignments
|
||||
(task_id, user_id, status, created_at, due_date)
|
||||
VALUES (?, ?, 'assigned', datetime('now'), ?)
|
||||
`, [taskId, assigneeId, task.due_date], function(err) {
|
||||
if (err) {
|
||||
errors.push(`Ошибка добавления исполнителя ${assigneeId}: ${err.message}`);
|
||||
} else if (this.changes > 0) {
|
||||
addedAssignees.push(assigneeId);
|
||||
|
||||
// Логируем действие
|
||||
const activityService = require('./database');
|
||||
if (activityService.logActivity) {
|
||||
activityService.logActivity(
|
||||
taskId,
|
||||
userId,
|
||||
'TASK_ASSIGNED',
|
||||
`Добавлен исполнитель: ${assigneeId}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Если это последний запрос, завершаем транзакцию
|
||||
if (index === assigneeIds.length - 1) {
|
||||
if (errors.length > 0) {
|
||||
db.run("ROLLBACK");
|
||||
return res.status(500).json({
|
||||
error: 'Ошибка добавления исполнителей',
|
||||
details: errors
|
||||
});
|
||||
} else {
|
||||
db.run("COMMIT");
|
||||
|
||||
// Отправляем уведомления
|
||||
const { sendTaskNotifications } = require('./notifications');
|
||||
if (sendTaskNotifications) {
|
||||
sendTaskNotifications(taskId, 'assigned', userId, assigneeIds);
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
added: addedAssignees.length,
|
||||
message: `Добавлено ${addedAssignees.length} исполнителей`
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// API для удаления исполнителя из задачи
|
||||
router.delete('/api/tasks/:taskId/assignees/:assigneeId', (req, res) => {
|
||||
const { taskId, assigneeId } = req.params;
|
||||
const userId = req.session.user.id;
|
||||
|
||||
checkTaskAccess(userId, taskId, (err, hasAccess) => {
|
||||
if (err || !hasAccess) {
|
||||
return res.status(403).json({ error: 'Нет доступа к задаче' });
|
||||
}
|
||||
|
||||
// Проверяем, что исполнитель существует
|
||||
db.get(`
|
||||
SELECT id FROM task_assignments
|
||||
WHERE task_id = ? AND user_id = ? AND status != 'deleted'
|
||||
`, [taskId, assigneeId], (err, assignment) => {
|
||||
if (err || !assignment) {
|
||||
return res.status(404).json({ error: 'Исполнитель не найден в задаче' });
|
||||
}
|
||||
|
||||
// Удаляем исполнителя (помечаем как удаленного)
|
||||
db.run(`
|
||||
UPDATE task_assignments
|
||||
SET status = 'deleted', updated_at = datetime('now')
|
||||
WHERE task_id = ? AND user_id = ?
|
||||
`, [taskId, assigneeId], function(err) {
|
||||
if (err) {
|
||||
return res.status(500).json({ error: err.message });
|
||||
}
|
||||
|
||||
if (this.changes > 0) {
|
||||
// Логируем действие
|
||||
const activityService = require('./database');
|
||||
if (activityService.logActivity) {
|
||||
activityService.logActivity(
|
||||
taskId,
|
||||
userId,
|
||||
'TASK_ASSIGNMENTS_UPDATED',
|
||||
`Удален исполнитель: ${assigneeId}`
|
||||
);
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Исполнитель удален из задачи'
|
||||
});
|
||||
} else {
|
||||
res.status(404).json({ error: 'Исполнитель не найден' });
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// API для замены всех исполнителей
|
||||
router.put('/api/tasks/:taskId/replace-all-assignees', (req, res) => {
|
||||
const { taskId } = req.params;
|
||||
const userId = req.session.user.id;
|
||||
const { newAssigneeIds } = req.body;
|
||||
|
||||
if (!Array.isArray(newAssigneeIds) || newAssigneeIds.length === 0) {
|
||||
return res.status(400).json({ error: 'Не указаны новые исполнители' });
|
||||
}
|
||||
|
||||
checkTaskAccess(userId, taskId, (err, hasAccess) => {
|
||||
if (err || !hasAccess) {
|
||||
return res.status(403).json({ error: 'Нет доступа к задаче' });
|
||||
}
|
||||
|
||||
// Получаем информацию о задаче
|
||||
db.get("SELECT due_date FROM tasks WHERE id = ?", [taskId], (err, task) => {
|
||||
if (err || !task) {
|
||||
return res.status(404).json({ error: 'Задача не найдена' });
|
||||
}
|
||||
|
||||
// Начинаем транзакцию
|
||||
db.serialize(() => {
|
||||
db.run("BEGIN TRANSACTION");
|
||||
|
||||
// Удаляем всех текущих исполнителей
|
||||
db.run(`
|
||||
UPDATE task_assignments
|
||||
SET status = 'deleted', updated_at = datetime('now')
|
||||
WHERE task_id = ? AND status != 'deleted'
|
||||
`, [taskId], function(err) {
|
||||
if (err) {
|
||||
db.run("ROLLBACK");
|
||||
return res.status(500).json({ error: err.message });
|
||||
}
|
||||
|
||||
const removedCount = this.changes;
|
||||
const errors = [];
|
||||
let addedCount = 0; // Изменено с const на let
|
||||
|
||||
// Если нет новых исполнителей для добавления, просто завершаем
|
||||
if (newAssigneeIds.length === 0) {
|
||||
db.run("COMMIT");
|
||||
|
||||
// Логируем действие
|
||||
const activityService = require('./database');
|
||||
if (activityService.logActivity) {
|
||||
activityService.logActivity(
|
||||
taskId,
|
||||
userId,
|
||||
'TASK_ASSIGNMENTS_UPDATED',
|
||||
`Удалены все исполнители. Удалено: ${removedCount}`
|
||||
);
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
removed: removedCount,
|
||||
added: 0,
|
||||
message: `Удалены все исполнители: ${removedCount}`
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Счетчик для отслеживания завершенных операций
|
||||
let completedCount = 0;
|
||||
|
||||
// Добавляем новых исполнителей
|
||||
newAssigneeIds.forEach((assigneeId) => {
|
||||
db.run(`
|
||||
INSERT INTO task_assignments
|
||||
(task_id, user_id, status, created_at, due_date)
|
||||
VALUES (?, ?, 'assigned', datetime('now'), ?)
|
||||
`, [taskId, assigneeId, task.due_date], function(err) {
|
||||
if (err) {
|
||||
errors.push(`Ошибка добавления исполнителя ${assigneeId}: ${err.message}`);
|
||||
} else {
|
||||
addedCount++;
|
||||
|
||||
// Логируем добавление
|
||||
const activityService = require('./database');
|
||||
if (activityService.logActivity) {
|
||||
activityService.logActivity(
|
||||
taskId,
|
||||
userId,
|
||||
'TASK_ASSIGNED',
|
||||
`Добавлен исполнитель: ${assigneeId}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
completedCount++;
|
||||
|
||||
// Если все запросы завершены, завершаем транзакцию
|
||||
if (completedCount === newAssigneeIds.length) {
|
||||
if (errors.length > 0) {
|
||||
db.run("ROLLBACK");
|
||||
return res.status(500).json({
|
||||
error: 'Ошибка замены исполнителей',
|
||||
details: errors
|
||||
});
|
||||
} else {
|
||||
db.run("COMMIT");
|
||||
|
||||
// Логируем замену всех
|
||||
const activityService = require('./database');
|
||||
if (activityService.logActivity) {
|
||||
activityService.logActivity(
|
||||
taskId,
|
||||
userId,
|
||||
'TASK_ASSIGNMENTS_UPDATED',
|
||||
`Заменены все исполнители. Удалено: ${removedCount}, добавлено: ${addedCount}`
|
||||
);
|
||||
}
|
||||
|
||||
// Отправляем уведомления
|
||||
const { sendTaskNotifications } = require('./notifications');
|
||||
if (sendTaskNotifications) {
|
||||
sendTaskNotifications(taskId, 'assigned', userId, newAssigneeIds);
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
removed: removedCount,
|
||||
added: addedCount,
|
||||
message: `Заменены исполнители: удалено ${removedCount}, добавлено ${addedCount}`
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
// API для замены конкретного исполнителя
|
||||
router.put('/api/tasks/:taskId/replace-assignee', (req, res) => {
|
||||
const { taskId } = req.params;
|
||||
const userId = req.session.user.id;
|
||||
const { oldAssigneeId, newAssigneeId } = req.body;
|
||||
|
||||
if (!oldAssigneeId || !newAssigneeId) {
|
||||
return res.status(400).json({ error: 'Не указан старый или новый исполнитель' });
|
||||
}
|
||||
|
||||
if (oldAssigneeId === newAssigneeId) {
|
||||
return res.status(400).json({ error: 'Старый и новый исполнитель одинаковы' });
|
||||
}
|
||||
|
||||
checkTaskAccess(userId, taskId, (err, hasAccess) => {
|
||||
if (err || !hasAccess) {
|
||||
return res.status(403).json({ error: 'Нет доступа к задаче' });
|
||||
}
|
||||
|
||||
// Получаем информацию о задаче
|
||||
db.get("SELECT due_date FROM tasks WHERE id = ?", [taskId], (err, task) => {
|
||||
if (err || !task) {
|
||||
return res.status(404).json({ error: 'Задача не найдена' });
|
||||
}
|
||||
|
||||
// Начинаем транзакцию
|
||||
db.serialize(() => {
|
||||
db.run("BEGIN TRANSACTION");
|
||||
|
||||
// Удаляем старого исполнителя
|
||||
db.run(`
|
||||
UPDATE task_assignments
|
||||
SET status = 'deleted', updated_at = datetime('now')
|
||||
WHERE task_id = ? AND user_id = ? AND status != 'deleted'
|
||||
`, [taskId, oldAssigneeId], function(err) {
|
||||
if (err) {
|
||||
db.run("ROLLBACK");
|
||||
return res.status(500).json({ error: err.message });
|
||||
}
|
||||
|
||||
if (this.changes === 0) {
|
||||
db.run("ROLLBACK");
|
||||
return res.status(404).json({ error: 'Старый исполнитель не найден' });
|
||||
}
|
||||
|
||||
// Добавляем нового исполнителя
|
||||
db.run(`
|
||||
INSERT INTO task_assignments
|
||||
(task_id, user_id, status, created_at, due_date)
|
||||
VALUES (?, ?, 'assigned', datetime('now'), ?)
|
||||
`, [taskId, newAssigneeId, task.due_date], function(err) {
|
||||
if (err) {
|
||||
db.run("ROLLBACK");
|
||||
return res.status(500).json({ error: err.message });
|
||||
}
|
||||
|
||||
db.run("COMMIT");
|
||||
|
||||
// Логируем действие
|
||||
const activityService = require('./database');
|
||||
if (activityService.logActivity) {
|
||||
activityService.logActivity(
|
||||
taskId,
|
||||
userId,
|
||||
'TASK_ASSIGNMENTS_UPDATED',
|
||||
`Заменен исполнитель: ${oldAssigneeId} -> ${newAssigneeId}`
|
||||
);
|
||||
}
|
||||
|
||||
// Отправляем уведомления
|
||||
const { sendTaskNotifications } = require('./notifications');
|
||||
if (sendTaskNotifications) {
|
||||
sendTaskNotifications(taskId, 'assigned', userId, [newAssigneeId]);
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `Исполнитель заменен: ${oldAssigneeId} -> ${newAssigneeId}`
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// API для смены всех исполнителей на конкретного пользователя (например, kalugin.o)
|
||||
router.put('/api/tasks/:taskId/assign-all-to-user', (req, res) => {
|
||||
const { taskId } = req.params;
|
||||
const userId = req.session.user.id;
|
||||
const { targetUserId } = req.body;
|
||||
|
||||
if (!targetUserId) {
|
||||
return res.status(400).json({ error: 'Не указан целевой пользователь' });
|
||||
}
|
||||
|
||||
checkTaskAccess(userId, taskId, (err, hasAccess) => {
|
||||
if (err || !hasAccess) {
|
||||
return res.status(403).json({ error: 'Нет доступа к задаче' });
|
||||
}
|
||||
|
||||
// Получаем информацию о задаче
|
||||
db.get("SELECT due_date FROM tasks WHERE id = ?", [taskId], (err, task) => {
|
||||
if (err || !task) {
|
||||
return res.status(404).json({ error: 'Задача не найдена' });
|
||||
}
|
||||
|
||||
// Получаем текущих исполнителей
|
||||
db.all(`
|
||||
SELECT user_id FROM task_assignments
|
||||
WHERE task_id = ? AND status != 'deleted' AND user_id != ?
|
||||
`, [taskId, targetUserId], (err, currentAssignees) => {
|
||||
if (err) {
|
||||
return res.status(500).json({ error: err.message });
|
||||
}
|
||||
|
||||
// Начинаем транзакцию
|
||||
db.serialize(() => {
|
||||
db.run("BEGIN TRANSACTION");
|
||||
|
||||
// Удаляем всех текущих исполнителей, кроме целевого
|
||||
if (currentAssignees.length > 0) {
|
||||
const currentAssigneeIds = currentAssignees.map(a => a.user_id);
|
||||
const placeholders = currentAssigneeIds.map(() => '?').join(',');
|
||||
|
||||
db.run(`
|
||||
UPDATE task_assignments
|
||||
SET status = 'deleted', updated_at = datetime('now')
|
||||
WHERE task_id = ? AND user_id IN (${placeholders})
|
||||
`, [taskId, ...currentAssigneeIds], function(err) {
|
||||
if (err) {
|
||||
db.run("ROLLBACK");
|
||||
return res.status(500).json({ error: err.message });
|
||||
}
|
||||
processNextStep();
|
||||
});
|
||||
} else {
|
||||
processNextStep();
|
||||
}
|
||||
|
||||
function processNextStep() {
|
||||
// Проверяем, есть ли уже целевой пользователь в исполнителях
|
||||
db.get(`
|
||||
SELECT id FROM task_assignments
|
||||
WHERE task_id = ? AND user_id = ? AND status != 'deleted'
|
||||
`, [taskId, targetUserId], (err, existing) => {
|
||||
if (err) {
|
||||
db.run("ROLLBACK");
|
||||
return res.status(500).json({ error: err.message });
|
||||
}
|
||||
|
||||
if (!existing) {
|
||||
// Добавляем целевого пользователя
|
||||
db.run(`
|
||||
INSERT INTO task_assignments
|
||||
(task_id, user_id, status, created_at, due_date)
|
||||
VALUES (?, ?, 'assigned', datetime('now'), ?)
|
||||
`, [taskId, targetUserId, task.due_date], function(err) {
|
||||
if (err) {
|
||||
db.run("ROLLBACK");
|
||||
return res.status(500).json({ error: err.message });
|
||||
}
|
||||
finishTransaction();
|
||||
});
|
||||
} else {
|
||||
finishTransaction();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function finishTransaction() {
|
||||
db.run("COMMIT");
|
||||
|
||||
// Логируем действие
|
||||
const activityService = require('./database');
|
||||
if (activityService.logActivity) {
|
||||
activityService.logActivity(
|
||||
taskId,
|
||||
userId,
|
||||
'TASK_ASSIGNMENTS_UPDATED',
|
||||
`Все исполнители заменены на: ${targetUserId}`
|
||||
);
|
||||
}
|
||||
|
||||
// Отправляем уведомления
|
||||
const { sendTaskNotifications } = require('./notifications');
|
||||
if (sendTaskNotifications) {
|
||||
sendTaskNotifications(taskId, 'assigned', userId, [targetUserId]);
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
removed: currentAssignees.length,
|
||||
added: 1,
|
||||
message: `Все исполнители заменены на пользователя ${targetUserId}`
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Подключаем роутер к приложению
|
||||
app.use(router);
|
||||
|
||||
console.log('✅ API для управления исполнителями подключено');
|
||||
};
|
||||
434
public/ui.js
434
public/ui.js
@@ -84,6 +84,7 @@ function renderTasks() {
|
||||
${!isDeleted && !isClosed ? `
|
||||
${canEdit ? `<button class="add-file-btn" onclick="openAddFileModal(${task.id})" title="Добавить файл">📎</button>` : ''}
|
||||
${currentUser && currentUser.login === 'kalugin.o' ? `<button class="edit-btn" onclick="openEditModal(${task.id})" title="Редактировать">✏️</button>` : ''}
|
||||
${currentUser && currentUser.login === 'kalugin.o' ? `<button class="manage-assignees-btn" onclick="openManageAssigneesModal(${task.id})" title="Управление исполнителями">👥</button>` : ''}
|
||||
<button class="copy-btn" onclick="openCopyModal(${task.id})" title="Создать копию">📋</button>
|
||||
${canEdit ? `<button class="rework-btn" onclick="openReworkModal(${task.id})" title="Вернуть на доработку">🔄</button>` : ''}
|
||||
${canEdit ? `<button class="close-btn" onclick="closeTask(${task.id})" title="Закрыть задачу">🔒</button>` : ''}
|
||||
@@ -804,3 +805,436 @@ function getTaskTypeIcon(type) {
|
||||
};
|
||||
return icons[type] || 'fas fa-tasks';
|
||||
}
|
||||
|
||||
// Функция для открытия модального окна управления исполнителями
|
||||
async function openManageAssigneesModal(taskId) {
|
||||
// Находим задачу
|
||||
const task = tasks.find(t => t.id === taskId);
|
||||
if (!task) {
|
||||
alert('Задача не найдена');
|
||||
return;
|
||||
}
|
||||
|
||||
// Проверяем права
|
||||
if (!canUserEditTask(task)) {
|
||||
alert('У вас нет прав для управления исполнителями этой задачи');
|
||||
return;
|
||||
}
|
||||
|
||||
// Получаем текущих исполнителей
|
||||
const currentAssignees = task.assignments || [];
|
||||
|
||||
// Получаем доступных для добавления исполнителей
|
||||
try {
|
||||
const response = await fetch(`/api/tasks/${taskId}/available-assignees`);
|
||||
const availableUsers = await response.json();
|
||||
|
||||
// Получаем всех пользователей для выбора замены
|
||||
const allUsersResponse = await fetch('/api/users');
|
||||
const allUsers = await allUsersResponse.json();
|
||||
|
||||
// Создаем модальное окно
|
||||
const modalHtml = `
|
||||
<div class="modal" id="manage-assignees-modal">
|
||||
<div class="modal-content" style="max-width: 800px;">
|
||||
<div class="modal-header">
|
||||
<h3>Управление исполнителями задачи: "${task.title}"</h3>
|
||||
<span class="close" onclick="closeManageAssigneesModal()">×</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="tabs">
|
||||
<button class="tab-btn active" onclick="switchManageTab('add')">➕ Добавить</button>
|
||||
<button class="tab-btn" onclick="switchManageTab('remove')">➖ Удалить</button>
|
||||
<button class="tab-btn" onclick="switchManageTab('replace-one')">🔄 Заменить одного</button>
|
||||
<button class="tab-btn" onclick="switchManageTab('replace-all')">🔄 Заменить всех</button>
|
||||
<button class="tab-btn" onclick="switchManageTab('assign-to-user')">👤 Назначить всем</button>
|
||||
</div>
|
||||
|
||||
<div id="add-assignee-tab" class="tab-content active">
|
||||
<h4>Добавить исполнителей</h4>
|
||||
<p>Текущие исполнители: ${currentAssignees.map(a => a.user_name).join(', ') || 'нет'}</p>
|
||||
|
||||
<div class="user-search-box">
|
||||
<input type="text" id="available-users-search" placeholder="Поиск пользователей..."
|
||||
oninput="filterAvailableUsers()">
|
||||
</div>
|
||||
|
||||
<div class="users-checklist" id="available-users-list" style="max-height: 200px; overflow-y: auto;">
|
||||
${availableUsers.map(user => `
|
||||
<div class="checkbox-item">
|
||||
<label>
|
||||
<input type="checkbox" class="available-user-checkbox" value="${user.id}">
|
||||
${user.name} (${user.email})
|
||||
</label>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn-cancel" onclick="closeManageAssigneesModal()">Отмена</button>
|
||||
<button type="button" class="btn-primary" onclick="addAssignees(${taskId})">Добавить выбранных</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="remove-assignee-tab" class="tab-content">
|
||||
<h4>Удалить исполнителей</h4>
|
||||
${currentAssignees.length > 0 ? `
|
||||
<div class="users-checklist" style="max-height: 200px; overflow-y: auto;">
|
||||
${currentAssignees.map(assignee => `
|
||||
<div class="checkbox-item">
|
||||
<label>
|
||||
<input type="checkbox" class="remove-user-checkbox" value="${assignee.user_id}">
|
||||
${assignee.user_name} (${assignee.user_login})
|
||||
<small>Статус: ${getStatusText(assignee.status)}</small>
|
||||
</label>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn-cancel" onclick="closeManageAssigneesModal()">Отмена</button>
|
||||
<button type="button" class="btn-danger" onclick="removeAssignees(${taskId})">Удалить выбранных</button>
|
||||
</div>
|
||||
` : '<p>Нет исполнителей для удаления</p>'}
|
||||
</div>
|
||||
|
||||
<div id="replace-one-assignee-tab" class="tab-content">
|
||||
<h4>Заменить одного исполнителя</h4>
|
||||
${currentAssignees.length > 0 ? `
|
||||
<div class="form-group">
|
||||
<label>Выберите исполнителя для замены:</label>
|
||||
<select id="old-assignee-select" class="form-control">
|
||||
${currentAssignees.map(assignee => `
|
||||
<option value="${assignee.user_id}">${assignee.user_name} (${assignee.user_login})</option>
|
||||
`).join('')}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Выберите нового исполнителя:</label>
|
||||
<select id="new-assignee-select" class="form-control">
|
||||
<option value="">-- Выберите пользователя --</option>
|
||||
${allUsers
|
||||
.filter(user => !currentAssignees.some(a => a.user_id === user.id))
|
||||
.map(user => `
|
||||
<option value="${user.id}">${user.name} (${user.email})</option>
|
||||
`).join('')}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn-cancel" onclick="closeManageAssigneesModal()">Отмена</button>
|
||||
<button type="button" class="btn-warning" onclick="replaceAssignee(${taskId})">Заменить</button>
|
||||
</div>
|
||||
` : '<p>Нет исполнителей для замены</p>'}
|
||||
</div>
|
||||
|
||||
<div id="replace-all-assignee-tab" class="tab-content">
|
||||
<h4>Заменить всех исполнителей</h4>
|
||||
<p>Текущие исполнители будут удалены, будут назначены новые.</p>
|
||||
|
||||
<div class="user-search-box">
|
||||
<input type="text" id="replace-all-users-search" placeholder="Поиск пользователей..."
|
||||
oninput="filterReplaceAllUsers()">
|
||||
</div>
|
||||
|
||||
<div class="users-checklist" id="replace-all-users-list" style="max-height: 200px; overflow-y: auto;">
|
||||
${allUsers.map(user => `
|
||||
<div class="checkbox-item">
|
||||
<label>
|
||||
<input type="checkbox" class="replace-all-user-checkbox" value="${user.id}">
|
||||
${user.name} (${user.email})
|
||||
</label>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn-cancel" onclick="closeManageAssigneesModal()">Отмена</button>
|
||||
<button type="button" class="btn-warning" onclick="replaceAllAssignees(${taskId})">Заменить всех</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="assign-to-user-tab" class="tab-content">
|
||||
<h4>Назначить всем выбранному пользователю</h4>
|
||||
<p>Все текущие исполнители будут удалены, задача будет назначена только выбранному пользователю.</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Выберите пользователя:</label>
|
||||
<select id="target-user-select" class="form-control">
|
||||
<option value="">-- Выберите пользователя --</option>
|
||||
${allUsers.map(user => `
|
||||
<option value="${user.id}" ${user.login === 'kalugin.o' ? 'selected' : ''}>
|
||||
${user.name} (${user.email})
|
||||
</option>
|
||||
`).join('')}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn-cancel" onclick="closeManageAssigneesModal()">Отмена</button>
|
||||
<button type="button" class="btn-primary" onclick="assignAllToUser(${taskId})">Назначить всем</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Добавляем модальное окно в DOM
|
||||
const modalContainer = document.createElement('div');
|
||||
modalContainer.innerHTML = modalHtml;
|
||||
document.body.appendChild(modalContainer);
|
||||
|
||||
// Показываем модальное окно
|
||||
setTimeout(() => {
|
||||
document.getElementById('manage-assignees-modal').style.display = 'block';
|
||||
}, 10);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки данных:', error);
|
||||
alert('Ошибка загрузки данных пользователей');
|
||||
}
|
||||
}
|
||||
|
||||
// Функция для переключения вкладок в модальном окне
|
||||
function switchManageTab(tabName) {
|
||||
// Убираем активный класс со всех вкладок и контента
|
||||
document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));
|
||||
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
|
||||
|
||||
// Активируем выбранную вкладку
|
||||
const activeTabBtn = document.querySelector(`.tab-btn[onclick*="${tabName}"]`);
|
||||
if (activeTabBtn) activeTabBtn.classList.add('active');
|
||||
|
||||
const activeContent = document.getElementById(`${tabName}-tab`);
|
||||
if (activeContent) activeContent.classList.add('active');
|
||||
}
|
||||
|
||||
// Функция для фильтрации доступных пользователей
|
||||
function filterAvailableUsers() {
|
||||
const search = document.getElementById('available-users-search')?.value.toLowerCase() || '';
|
||||
const checkboxes = document.querySelectorAll('.available-user-checkbox');
|
||||
|
||||
checkboxes.forEach(checkbox => {
|
||||
const label = checkbox.parentElement.textContent.toLowerCase();
|
||||
const isVisible = label.includes(search) || search === '';
|
||||
checkbox.parentElement.parentElement.style.display = isVisible ? '' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
// Функция для фильтрации пользователей для замены всех
|
||||
function filterReplaceAllUsers() {
|
||||
const search = document.getElementById('replace-all-users-search')?.value.toLowerCase() || '';
|
||||
const checkboxes = document.querySelectorAll('.replace-all-user-checkbox');
|
||||
|
||||
checkboxes.forEach(checkbox => {
|
||||
const label = checkbox.parentElement.textContent.toLowerCase();
|
||||
const isVisible = label.includes(search) || search === '';
|
||||
checkbox.parentElement.parentElement.style.display = isVisible ? '' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
// Функция для добавления исполнителей
|
||||
async function addAssignees(taskId) {
|
||||
const selectedCheckboxes = document.querySelectorAll('.available-user-checkbox:checked');
|
||||
const assigneeIds = Array.from(selectedCheckboxes).map(cb => cb.value);
|
||||
|
||||
if (assigneeIds.length === 0) {
|
||||
alert('Выберите хотя бы одного пользователя для добавления');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/tasks/${taskId}/assignees`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ assigneeIds })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
alert(result.message);
|
||||
closeManageAssigneesModal();
|
||||
loadTasks(); // Перезагружаем задачи
|
||||
} else {
|
||||
const error = await response.json();
|
||||
alert(`Ошибка: ${error.error || 'Неизвестная ошибка'}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка:', error);
|
||||
alert('Сетевая ошибка при добавлении исполнителей');
|
||||
}
|
||||
}
|
||||
|
||||
// Функция для удаления исполнителей
|
||||
async function removeAssignees(taskId) {
|
||||
const selectedCheckboxes = document.querySelectorAll('.remove-user-checkbox:checked');
|
||||
const assigneeIds = Array.from(selectedCheckboxes).map(cb => cb.value);
|
||||
|
||||
if (assigneeIds.length === 0) {
|
||||
alert('Выберите хотя бы одного исполнителя для удаления');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm(`Вы уверены, что хотите удалить ${assigneeIds.length} исполнителей из задачи?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Удаляем каждого исполнителя по отдельности
|
||||
const results = [];
|
||||
for (const assigneeId of assigneeIds) {
|
||||
try {
|
||||
const response = await fetch(`/api/tasks/${taskId}/assignees/${assigneeId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
results.push({ id: assigneeId, success: true });
|
||||
} else {
|
||||
const error = await response.json();
|
||||
results.push({ id: assigneeId, success: false, error: error.error });
|
||||
}
|
||||
} catch (error) {
|
||||
results.push({ id: assigneeId, success: false, error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
const successful = results.filter(r => r.success).length;
|
||||
const failed = results.filter(r => !r.success);
|
||||
|
||||
if (failed.length > 0) {
|
||||
alert(`Удалено ${successful} исполнителей. Ошибки: ${failed.map(f => `ID ${f.id}: ${f.error}`).join('; ')}`);
|
||||
} else {
|
||||
alert(`Успешно удалено ${successful} исполнителей`);
|
||||
}
|
||||
|
||||
closeManageAssigneesModal();
|
||||
loadTasks(); // Перезагружаем задачи
|
||||
}
|
||||
|
||||
// Функция для замены одного исполнителя
|
||||
async function replaceAssignee(taskId) {
|
||||
const oldAssigneeId = document.getElementById('old-assignee-select').value;
|
||||
const newAssigneeId = document.getElementById('new-assignee-select').value;
|
||||
|
||||
if (!oldAssigneeId || !newAssigneeId) {
|
||||
alert('Выберите старого и нового исполнителя');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm('Вы уверены, что хотите заменить исполнителя?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/tasks/${taskId}/replace-assignee`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ oldAssigneeId, newAssigneeId })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
alert(result.message);
|
||||
closeManageAssigneesModal();
|
||||
loadTasks(); // Перезагружаем задачи
|
||||
} else {
|
||||
const error = await response.json();
|
||||
alert(`Ошибка: ${error.error || 'Неизвестная ошибка'}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка:', error);
|
||||
alert('Сетевая ошибка при замене исполнителя');
|
||||
}
|
||||
}
|
||||
|
||||
// Функция для замены всех исполнителей
|
||||
async function replaceAllAssignees(taskId) {
|
||||
const selectedCheckboxes = document.querySelectorAll('.replace-all-user-checkbox:checked');
|
||||
const newAssigneeIds = Array.from(selectedCheckboxes).map(cb => cb.value);
|
||||
|
||||
if (newAssigneeIds.length === 0) {
|
||||
alert('Выберите хотя бы одного нового исполнителя');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm('Вы уверены, что хотите заменить всех исполнителей? Текущие исполнители будут удалены.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/tasks/${taskId}/replace-all-assignees`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ newAssigneeIds })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
alert(result.message);
|
||||
closeManageAssigneesModal();
|
||||
loadTasks(); // Перезагружаем задачи
|
||||
} else {
|
||||
const error = await response.json();
|
||||
alert(`Ошибка: ${error.error || 'Неизвестная ошибка'}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка:', error);
|
||||
alert('Сетевая ошибка при замене исполнителей');
|
||||
}
|
||||
}
|
||||
|
||||
// Функция для назначения всех исполнителей одному пользователю
|
||||
async function assignAllToUser(taskId) {
|
||||
const targetUserId = document.getElementById('target-user-select').value;
|
||||
|
||||
if (!targetUserId) {
|
||||
alert('Выберите пользователя для назначения');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm('Вы уверены, что хотите назначить задачу только этому пользователю? Все текущие исполнители будут удалены.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/tasks/${taskId}/assign-all-to-user`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ targetUserId })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
alert(result.message);
|
||||
closeManageAssigneesModal();
|
||||
loadTasks(); // Перезагружаем задачи
|
||||
} else {
|
||||
const error = await response.json();
|
||||
alert(`Ошибка: ${error.error || 'Неизвестная ошибка'}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка:', error);
|
||||
alert('Сетевая ошибка при назначении задачи');
|
||||
}
|
||||
}
|
||||
|
||||
// Функция для закрытия модального окна управления исполнителями
|
||||
function closeManageAssigneesModal() {
|
||||
const modal = document.getElementById('manage-assignees-modal');
|
||||
if (modal) {
|
||||
modal.style.display = 'none';
|
||||
// Удаляем модальное окно из DOM через некоторое время
|
||||
setTimeout(() => {
|
||||
modal.parentElement.remove();
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,8 @@ const { setupUploadMiddleware } = require('./upload-middleware');
|
||||
const { setupTaskEndpoints } = require('./task-endpoints');
|
||||
// doc
|
||||
const apiDoc = require('./api-doc');
|
||||
// Подключаем API для управления исполнителями
|
||||
const userManagementAPI = require('./api-users');
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
@@ -1192,6 +1194,10 @@ async function initializeServer() {
|
||||
setupTaskEndpoints(app, db, upload);
|
||||
console.log('✅ Endpoint\'ы задач настроены');
|
||||
|
||||
// 5. Подключаем API для управления исполнителями
|
||||
userManagementAPI(app, db);
|
||||
console.log('✅ API для управления исполнителями подключено');
|
||||
|
||||
apiDoc(app, db, upload);
|
||||
console.log('✅ Endpoint\'ы документов настроены');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user