копия задачи, доделать
This commit is contained in:
264
api-client.js
264
api-client.js
@@ -4,6 +4,7 @@ const router = express.Router();
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
|
const FormData = require('form-data');
|
||||||
|
|
||||||
module.exports = function(app, db, upload) {
|
module.exports = function(app, db, upload) {
|
||||||
|
|
||||||
@@ -34,11 +35,6 @@ module.exports = function(app, db, upload) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* POST /api/client/connect - Проверка подключения к внешнему сервису
|
* POST /api/client/connect - Проверка подключения к внешнему сервису
|
||||||
* Тело запроса:
|
|
||||||
* {
|
|
||||||
* "api_url": "https://example.com",
|
|
||||||
* "api_key": "ваш_ключ"
|
|
||||||
* }
|
|
||||||
*/
|
*/
|
||||||
router.post('/api/client/connect', requireAuth, async (req, res) => {
|
router.post('/api/client/connect', requireAuth, async (req, res) => {
|
||||||
const { api_url, api_key } = req.body;
|
const { api_url, api_key } = req.body;
|
||||||
@@ -51,7 +47,7 @@ module.exports = function(app, db, upload) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Нормализуем URL (убираем слеш в конце если есть)
|
// Нормализуем URL
|
||||||
const baseUrl = api_url.replace(/\/$/, '');
|
const baseUrl = api_url.replace(/\/$/, '');
|
||||||
|
|
||||||
// Пробуем подключиться к сервису
|
// Пробуем подключиться к сервису
|
||||||
@@ -60,9 +56,9 @@ module.exports = function(app, db, upload) {
|
|||||||
'X-API-Key': api_key
|
'X-API-Key': api_key
|
||||||
},
|
},
|
||||||
params: {
|
params: {
|
||||||
limit: 1 // Запрашиваем только одну задачу для проверки
|
limit: 1
|
||||||
},
|
},
|
||||||
timeout: 10000 // 10 секунд таймаут
|
timeout: 10000
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.data && response.data.success) {
|
if (response.data && response.data.success) {
|
||||||
@@ -152,6 +148,25 @@ module.exports = function(app, db, upload) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/client/connections/list - Получить список всех подключений для выбора
|
||||||
|
*/
|
||||||
|
router.get('/api/client/connections/list', requireAuth, (req, res) => {
|
||||||
|
const connections = req.session.clientConnections || {};
|
||||||
|
|
||||||
|
const connectionsList = Object.values(connections).map(conn => ({
|
||||||
|
id: conn.id,
|
||||||
|
name: conn.name,
|
||||||
|
url: conn.url,
|
||||||
|
last_used: conn.last_used
|
||||||
|
}));
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
connections: connectionsList
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DELETE /api/client/connections/:id - Удалить сохраненное подключение
|
* DELETE /api/client/connections/:id - Удалить сохраненное подключение
|
||||||
*/
|
*/
|
||||||
@@ -180,12 +195,6 @@ module.exports = function(app, db, upload) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /api/client/tasks - Получить список задач из внешнего сервиса
|
* GET /api/client/tasks - Получить список задач из внешнего сервиса
|
||||||
* Параметры запроса:
|
|
||||||
* - connection_id: ID сохраненного подключения (из сессии)
|
|
||||||
* - api_url: URL сервиса (если нет connection_id)
|
|
||||||
* - api_key: API ключ (если нет connection_id)
|
|
||||||
* - status: фильтр по статусу (опционально)
|
|
||||||
* - search: поиск по тексту (опционально)
|
|
||||||
*/
|
*/
|
||||||
router.get('/api/client/tasks', requireAuth, async (req, res) => {
|
router.get('/api/client/tasks', requireAuth, async (req, res) => {
|
||||||
const { connection_id, api_url, api_key, status, search, limit = 50, offset = 0 } = req.query;
|
const { connection_id, api_url, api_key, status, search, limit = 50, offset = 0 } = req.query;
|
||||||
@@ -194,13 +203,11 @@ module.exports = function(app, db, upload) {
|
|||||||
let targetUrl = api_url;
|
let targetUrl = api_url;
|
||||||
let targetKey = api_key;
|
let targetKey = api_key;
|
||||||
|
|
||||||
// Если указан connection_id, берем данные из сессии
|
|
||||||
if (connection_id && req.session.clientConnections && req.session.clientConnections[connection_id]) {
|
if (connection_id && req.session.clientConnections && req.session.clientConnections[connection_id]) {
|
||||||
const connection = req.session.clientConnections[connection_id];
|
const connection = req.session.clientConnections[connection_id];
|
||||||
targetUrl = connection.url;
|
targetUrl = connection.url;
|
||||||
targetKey = connection.api_key;
|
targetKey = connection.api_key;
|
||||||
|
|
||||||
// Обновляем время последнего использования
|
|
||||||
connection.last_used = new Date().toISOString();
|
connection.last_used = new Date().toISOString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,11 +220,9 @@ module.exports = function(app, db, upload) {
|
|||||||
try {
|
try {
|
||||||
const baseUrl = targetUrl.replace(/\/$/, '');
|
const baseUrl = targetUrl.replace(/\/$/, '');
|
||||||
|
|
||||||
// Формируем параметры запроса
|
|
||||||
const params = { limit, offset };
|
const params = { limit, offset };
|
||||||
if (status) params.status = status;
|
if (status) params.status = status;
|
||||||
|
|
||||||
// Получаем список задач
|
|
||||||
const response = await axios.get(`${baseUrl}/api/external/tasks`, {
|
const response = await axios.get(`${baseUrl}/api/external/tasks`, {
|
||||||
headers: {
|
headers: {
|
||||||
'X-API-Key': targetKey
|
'X-API-Key': targetKey
|
||||||
@@ -229,7 +234,6 @@ module.exports = function(app, db, upload) {
|
|||||||
if (response.data && response.data.success) {
|
if (response.data && response.data.success) {
|
||||||
let tasks = response.data.tasks || [];
|
let tasks = response.data.tasks || [];
|
||||||
|
|
||||||
// Дополнительная фильтрация по поиску на стороне клиента
|
|
||||||
if (search && tasks.length > 0) {
|
if (search && tasks.length > 0) {
|
||||||
const searchLower = search.toLowerCase();
|
const searchLower = search.toLowerCase();
|
||||||
tasks = tasks.filter(task =>
|
tasks = tasks.filter(task =>
|
||||||
@@ -238,7 +242,6 @@ module.exports = function(app, db, upload) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Логируем действие
|
|
||||||
const { logActivity } = require('./database');
|
const { logActivity } = require('./database');
|
||||||
if (logActivity) {
|
if (logActivity) {
|
||||||
logActivity(0, userId, 'API_CLIENT_GET_TASKS',
|
logActivity(0, userId, 'API_CLIENT_GET_TASKS',
|
||||||
@@ -296,7 +299,6 @@ module.exports = function(app, db, upload) {
|
|||||||
router.get('/api/client/tasks/:taskId', requireAuth, async (req, res) => {
|
router.get('/api/client/tasks/:taskId', requireAuth, async (req, res) => {
|
||||||
const { taskId } = req.params;
|
const { taskId } = req.params;
|
||||||
const { connection_id, api_url, api_key } = req.query;
|
const { connection_id, api_url, api_key } = req.query;
|
||||||
const userId = req.session.user.id;
|
|
||||||
|
|
||||||
let targetUrl = api_url;
|
let targetUrl = api_url;
|
||||||
let targetKey = api_key;
|
let targetKey = api_key;
|
||||||
@@ -357,11 +359,6 @@ module.exports = function(app, db, upload) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* PUT /api/client/tasks/:taskId/status - Изменить статус задачи
|
* PUT /api/client/tasks/:taskId/status - Изменить статус задачи
|
||||||
* Тело запроса:
|
|
||||||
* {
|
|
||||||
* "status": "in_progress" | "completed",
|
|
||||||
* "comment": "Комментарий к изменению статуса" (опционально)
|
|
||||||
* }
|
|
||||||
*/
|
*/
|
||||||
router.put('/api/client/tasks/:taskId/status', requireAuth, async (req, res) => {
|
router.put('/api/client/tasks/:taskId/status', requireAuth, async (req, res) => {
|
||||||
const { taskId } = req.params;
|
const { taskId } = req.params;
|
||||||
@@ -404,7 +401,6 @@ module.exports = function(app, db, upload) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (response.data && response.data.success) {
|
if (response.data && response.data.success) {
|
||||||
// Логируем действие
|
|
||||||
const { logActivity } = require('./database');
|
const { logActivity } = require('./database');
|
||||||
if (logActivity) {
|
if (logActivity) {
|
||||||
logActivity(0, userId, 'API_CLIENT_UPDATE_STATUS',
|
logActivity(0, userId, 'API_CLIENT_UPDATE_STATUS',
|
||||||
@@ -448,6 +444,206 @@ module.exports = function(app, db, upload) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/client/tasks/:taskId/copy - Скопировать задачу в целевой сервис
|
||||||
|
*/
|
||||||
|
router.post('/api/client/tasks/:taskId/copy', requireAuth, async (req, res) => {
|
||||||
|
const { taskId } = req.params;
|
||||||
|
const {
|
||||||
|
target_connection_id,
|
||||||
|
target_api_url,
|
||||||
|
target_api_key,
|
||||||
|
new_assignees,
|
||||||
|
due_date,
|
||||||
|
copy_files = true
|
||||||
|
} = req.body;
|
||||||
|
const { connection_id } = req.query;
|
||||||
|
const userId = req.session.user.id;
|
||||||
|
|
||||||
|
if (!connection_id && !req.session.clientConnections) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'Не указан источник (текущее подключение)'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let targetUrl = target_api_url;
|
||||||
|
let targetKey = target_api_key;
|
||||||
|
|
||||||
|
if (target_connection_id && req.session.clientConnections && req.session.clientConnections[target_connection_id]) {
|
||||||
|
const connection = req.session.clientConnections[target_connection_id];
|
||||||
|
targetUrl = connection.url;
|
||||||
|
targetKey = connection.api_key;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!targetUrl || !targetKey) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'Не указан целевой сервис (URL или API ключ)'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const baseUrl = targetUrl.replace(/\/$/, '');
|
||||||
|
const sourceConnection = req.session.clientConnections[connection_id];
|
||||||
|
|
||||||
|
if (!sourceConnection) {
|
||||||
|
return res.status(400).json({ error: 'Исходное подключение не найдено' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Получаем исходную задачу
|
||||||
|
const sourceResponse = await axios.get(
|
||||||
|
`${sourceConnection.url}/api/external/tasks/${taskId}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'X-API-Key': sourceConnection.api_key
|
||||||
|
},
|
||||||
|
timeout: 10000
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!sourceResponse.data || !sourceResponse.data.success) {
|
||||||
|
return res.status(404).json({ error: 'Исходная задача не найдена' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceTask = sourceResponse.data.task;
|
||||||
|
|
||||||
|
// 2. Создаем копию задачи в целевом сервисе
|
||||||
|
const newTaskTitle = `Копия: ${sourceTask.title}`;
|
||||||
|
|
||||||
|
const taskData = {
|
||||||
|
title: newTaskTitle,
|
||||||
|
description: sourceTask.description || '',
|
||||||
|
due_date: due_date || sourceTask.due_date,
|
||||||
|
task_type: sourceTask.task_type || 'regular'
|
||||||
|
};
|
||||||
|
|
||||||
|
if (new_assignees && new_assignees.length > 0) {
|
||||||
|
taskData.assignedUsers = new_assignees;
|
||||||
|
}
|
||||||
|
|
||||||
|
const createResponse = await axios.post(
|
||||||
|
`${baseUrl}/api/external/tasks/create`,
|
||||||
|
taskData,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'X-API-Key': targetKey,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
timeout: 15000
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!createResponse.data || !createResponse.data.success) {
|
||||||
|
return res.status(500).json({
|
||||||
|
error: 'Не удалось создать задачу в целевом сервисе'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const newTaskId = createResponse.data.taskId;
|
||||||
|
|
||||||
|
// 3. Копируем файлы, если нужно
|
||||||
|
const copiedFiles = [];
|
||||||
|
if (copy_files && sourceTask.files && sourceTask.files.length > 0) {
|
||||||
|
for (const file of sourceTask.files) {
|
||||||
|
try {
|
||||||
|
const fileResponse = await axios({
|
||||||
|
method: 'GET',
|
||||||
|
url: `${sourceConnection.url}/api/external/tasks/${taskId}/files/${file.id}/download`,
|
||||||
|
headers: {
|
||||||
|
'X-API-Key': sourceConnection.api_key
|
||||||
|
},
|
||||||
|
responseType: 'arraybuffer',
|
||||||
|
timeout: 30000
|
||||||
|
});
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('files', Buffer.from(fileResponse.data), {
|
||||||
|
filename: file.filename || file.original_name || 'file',
|
||||||
|
contentType: file.file_type || 'application/octet-stream'
|
||||||
|
});
|
||||||
|
|
||||||
|
const uploadResponse = await axios.post(
|
||||||
|
`${baseUrl}/api/external/tasks/${newTaskId}/files`,
|
||||||
|
formData,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
...formData.getHeaders(),
|
||||||
|
'X-API-Key': targetKey
|
||||||
|
},
|
||||||
|
timeout: 60000,
|
||||||
|
maxContentLength: Infinity,
|
||||||
|
maxBodyLength: Infinity
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
copiedFiles.push({
|
||||||
|
original_name: file.filename || file.original_name,
|
||||||
|
success: uploadResponse.data && uploadResponse.data.success
|
||||||
|
});
|
||||||
|
} catch (fileError) {
|
||||||
|
console.error(`❌ Ошибка копирования файла:`, fileError.message);
|
||||||
|
copiedFiles.push({
|
||||||
|
original_name: file.filename || file.original_name,
|
||||||
|
success: false,
|
||||||
|
error: fileError.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { logActivity } = require('./database');
|
||||||
|
if (logActivity) {
|
||||||
|
logActivity(0, userId, 'API_CLIENT_COPY_TASK',
|
||||||
|
`Скопирована задача ${taskId} из ${sourceConnection.url} в ${baseUrl}. Новый ID: ${newTaskId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: `Задача успешно скопирована${copiedFiles.length > 0 ? `, скопировано файлов: ${copiedFiles.filter(f => f.success).length}` : ''}`,
|
||||||
|
data: {
|
||||||
|
original_task_id: taskId,
|
||||||
|
new_task_id: newTaskId,
|
||||||
|
new_task_title: newTaskTitle,
|
||||||
|
target_service: baseUrl,
|
||||||
|
copied_files: copiedFiles,
|
||||||
|
assignees: new_assignees || 'не изменены'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Ошибка копирования задачи:', error.message);
|
||||||
|
|
||||||
|
let errorMessage = 'Ошибка копирования задачи';
|
||||||
|
let statusCode = 500;
|
||||||
|
|
||||||
|
if (error.response) {
|
||||||
|
if (error.response.status === 401) {
|
||||||
|
errorMessage = 'Неверный API ключ для целевого сервиса';
|
||||||
|
statusCode = 401;
|
||||||
|
} else if (error.response.status === 403) {
|
||||||
|
errorMessage = 'Нет прав для создания задачи в целевом сервисе';
|
||||||
|
statusCode = 403;
|
||||||
|
} else if (error.response.status === 404) {
|
||||||
|
errorMessage = 'Исходная задача не найдена';
|
||||||
|
statusCode = 404;
|
||||||
|
} else {
|
||||||
|
errorMessage = error.response.data?.error || `Ошибка сервера: ${error.response.status}`;
|
||||||
|
statusCode = error.response.status;
|
||||||
|
}
|
||||||
|
} else if (error.code === 'ECONNREFUSED') {
|
||||||
|
errorMessage = 'Целевой сервис недоступен';
|
||||||
|
statusCode = 503;
|
||||||
|
} else if (error.code === 'ETIMEDOUT') {
|
||||||
|
errorMessage = 'Превышено время ожидания ответа';
|
||||||
|
statusCode = 504;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(statusCode).json({
|
||||||
|
error: errorMessage,
|
||||||
|
details: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* POST /api/client/tasks/:taskId/files - Загрузить файлы в задачу
|
* POST /api/client/tasks/:taskId/files - Загрузить файлы в задачу
|
||||||
*/
|
*/
|
||||||
@@ -470,7 +666,6 @@ module.exports = function(app, db, upload) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!targetUrl || !targetKey) {
|
if (!targetUrl || !targetKey) {
|
||||||
// Очищаем временные файлы
|
|
||||||
req.files.forEach(file => {
|
req.files.forEach(file => {
|
||||||
if (file.path && fs.existsSync(file.path)) {
|
if (file.path && fs.existsSync(file.path)) {
|
||||||
fs.unlinkSync(file.path);
|
fs.unlinkSync(file.path);
|
||||||
@@ -482,8 +677,6 @@ module.exports = function(app, db, upload) {
|
|||||||
try {
|
try {
|
||||||
const baseUrl = targetUrl.replace(/\/$/, '');
|
const baseUrl = targetUrl.replace(/\/$/, '');
|
||||||
|
|
||||||
// Создаем FormData для отправки файлов
|
|
||||||
const FormData = require('form-data');
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
|
|
||||||
req.files.forEach(file => {
|
req.files.forEach(file => {
|
||||||
@@ -493,7 +686,6 @@ module.exports = function(app, db, upload) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Отправляем файлы на внешний сервис
|
|
||||||
const response = await axios.post(
|
const response = await axios.post(
|
||||||
`${baseUrl}/api/external/tasks/${taskId}/files`,
|
`${baseUrl}/api/external/tasks/${taskId}/files`,
|
||||||
formData,
|
formData,
|
||||||
@@ -502,13 +694,12 @@ module.exports = function(app, db, upload) {
|
|||||||
...formData.getHeaders(),
|
...formData.getHeaders(),
|
||||||
'X-API-Key': targetKey
|
'X-API-Key': targetKey
|
||||||
},
|
},
|
||||||
timeout: 60000, // 60 секунд для загрузки файлов
|
timeout: 60000,
|
||||||
maxContentLength: Infinity,
|
maxContentLength: Infinity,
|
||||||
maxBodyLength: Infinity
|
maxBodyLength: Infinity
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Удаляем временные файлы
|
|
||||||
req.files.forEach(file => {
|
req.files.forEach(file => {
|
||||||
if (file.path && fs.existsSync(file.path)) {
|
if (file.path && fs.existsSync(file.path)) {
|
||||||
fs.unlinkSync(file.path);
|
fs.unlinkSync(file.path);
|
||||||
@@ -516,7 +707,6 @@ module.exports = function(app, db, upload) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (response.data && response.data.success) {
|
if (response.data && response.data.success) {
|
||||||
// Логируем действие
|
|
||||||
const { logActivity } = require('./database');
|
const { logActivity } = require('./database');
|
||||||
if (logActivity) {
|
if (logActivity) {
|
||||||
logActivity(0, userId, 'API_CLIENT_UPLOAD_FILES',
|
logActivity(0, userId, 'API_CLIENT_UPLOAD_FILES',
|
||||||
@@ -532,7 +722,6 @@ module.exports = function(app, db, upload) {
|
|||||||
res.status(400).json({ error: 'Не удалось загрузить файлы' });
|
res.status(400).json({ error: 'Не удалось загрузить файлы' });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Удаляем временные файлы в случае ошибки
|
|
||||||
req.files.forEach(file => {
|
req.files.forEach(file => {
|
||||||
if (file.path && fs.existsSync(file.path)) {
|
if (file.path && fs.existsSync(file.path)) {
|
||||||
fs.unlinkSync(file.path);
|
fs.unlinkSync(file.path);
|
||||||
@@ -597,7 +786,6 @@ module.exports = function(app, db, upload) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Сначала получаем детали задачи, чтобы увидеть файлы
|
|
||||||
const baseUrl = targetUrl.replace(/\/$/, '');
|
const baseUrl = targetUrl.replace(/\/$/, '');
|
||||||
|
|
||||||
const response = await axios.get(`${baseUrl}/api/external/tasks/${taskId}`, {
|
const response = await axios.get(`${baseUrl}/api/external/tasks/${taskId}`, {
|
||||||
@@ -625,7 +813,7 @@ module.exports = function(app, db, upload) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /api/client/tasks/:taskId/files/:fileId/download - Скачать файл (через редирект)
|
* GET /api/client/tasks/:taskId/files/:fileId/download - Скачать файл
|
||||||
*/
|
*/
|
||||||
router.get('/api/client/tasks/:taskId/files/:fileId/download', requireAuth, async (req, res) => {
|
router.get('/api/client/tasks/:taskId/files/:fileId/download', requireAuth, async (req, res) => {
|
||||||
const { taskId, fileId } = req.params;
|
const { taskId, fileId } = req.params;
|
||||||
@@ -647,7 +835,6 @@ module.exports = function(app, db, upload) {
|
|||||||
try {
|
try {
|
||||||
const baseUrl = targetUrl.replace(/\/$/, '');
|
const baseUrl = targetUrl.replace(/\/$/, '');
|
||||||
|
|
||||||
// Делаем запрос на скачивание файла
|
|
||||||
const response = await axios({
|
const response = await axios({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: `${baseUrl}/api/external/tasks/${taskId}/files/${fileId}/download`,
|
url: `${baseUrl}/api/external/tasks/${taskId}/files/${fileId}/download`,
|
||||||
@@ -658,7 +845,6 @@ module.exports = function(app, db, upload) {
|
|||||||
timeout: 30000
|
timeout: 30000
|
||||||
});
|
});
|
||||||
|
|
||||||
// Проксируем ответ клиенту
|
|
||||||
const contentType = response.headers['content-type'] || 'application/octet-stream';
|
const contentType = response.headers['content-type'] || 'application/octet-stream';
|
||||||
const contentDisposition = response.headers['content-disposition'] || 'attachment';
|
const contentDisposition = response.headers['content-disposition'] || 'attachment';
|
||||||
|
|
||||||
|
|||||||
@@ -112,7 +112,7 @@
|
|||||||
color: #7f8c8d;
|
color: #7f8c8d;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group input, .form-group select {
|
.form-group input, .form-group select, .form-group textarea {
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
border: 1px solid #dce4ec;
|
border: 1px solid #dce4ec;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
@@ -120,7 +120,7 @@
|
|||||||
transition: border-color 0.3s;
|
transition: border-color 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group input:focus, .form-group select:focus {
|
.form-group input:focus, .form-group select:focus, .form-group textarea:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
border-color: #3498db;
|
border-color: #3498db;
|
||||||
}
|
}
|
||||||
@@ -273,7 +273,7 @@
|
|||||||
|
|
||||||
.tasks-grid {
|
.tasks-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
margin-bottom: 30px;
|
margin-bottom: 30px;
|
||||||
}
|
}
|
||||||
@@ -427,6 +427,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
|
min-width: 80px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-progress {
|
.action-progress {
|
||||||
@@ -456,6 +457,15 @@
|
|||||||
background: #2980b9;
|
background: #2980b9;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.action-copy {
|
||||||
|
background: #9b59b6;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-copy:hover {
|
||||||
|
background: #8e44ad;
|
||||||
|
}
|
||||||
|
|
||||||
.pagination {
|
.pagination {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -501,6 +511,12 @@
|
|||||||
color: #3498db;
|
color: #3498db;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.loading-small {
|
||||||
|
text-align: center;
|
||||||
|
padding: 10px;
|
||||||
|
color: #7f8c8d;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
0% { transform: rotate(0deg); }
|
0% { transform: rotate(0deg); }
|
||||||
100% { transform: rotate(360deg); }
|
100% { transform: rotate(360deg); }
|
||||||
@@ -534,9 +550,9 @@
|
|||||||
background: white;
|
background: white;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
padding: 30px;
|
padding: 30px;
|
||||||
max-width: 500px;
|
max-width: 600px;
|
||||||
width: 90%;
|
width: 90%;
|
||||||
max-height: 80vh;
|
max-height: 90vh;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -618,7 +634,7 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.upload-progress {
|
.upload-progress, .copy-progress {
|
||||||
margin-top: 15px;
|
margin-top: 15px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
background: #ecf0f1;
|
background: #ecf0f1;
|
||||||
@@ -639,15 +655,42 @@
|
|||||||
transition: width 0.3s;
|
transition: width 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.copy-status {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
background: #ecf0f1;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
|
|
||||||
.alert {
|
.alert {
|
||||||
padding: 15px;
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
padding: 15px 20px;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
display: none;
|
display: none;
|
||||||
|
z-index: 2000;
|
||||||
|
max-width: 400px;
|
||||||
|
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert.show {
|
.alert.show {
|
||||||
display: block;
|
display: block;
|
||||||
|
animation: slideIn 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideIn {
|
||||||
|
from {
|
||||||
|
transform: translateX(100%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-success {
|
.alert-success {
|
||||||
@@ -715,6 +758,12 @@
|
|||||||
background: #3498db;
|
background: #3498db;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
small {
|
||||||
|
color: #95a5a6;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 3px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||||
</head>
|
</head>
|
||||||
@@ -874,10 +923,81 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Модальное окно копирования задачи -->
|
||||||
|
<div class="modal" id="copyModal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3>Копирование задачи</h3>
|
||||||
|
<span class="modal-close" onclick="closeCopyModal()">×</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-bottom: 20px;">
|
||||||
|
<p>Вы копируете задачу: <strong id="copyTaskTitle"></strong></p>
|
||||||
|
<p style="font-size: 14px; color: #7f8c8d; margin-top: 5px;">
|
||||||
|
Автором новой задачи будете <strong id="currentUserName"></strong>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" style="margin-bottom: 15px;">
|
||||||
|
<label>Целевой сервис <span style="color: #e74c3c;">*</span></label>
|
||||||
|
<select id="targetService" onchange="toggleTargetServiceInput()">
|
||||||
|
<option value="">-- Выберите сервис --</option>
|
||||||
|
<optgroup label="Сохраненные подключения" id="savedTargetConnections"></optgroup>
|
||||||
|
<option value="new">Указать новый сервис</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="newServiceInputs" style="display: none;">
|
||||||
|
<div class="form-group" style="margin-bottom: 15px;">
|
||||||
|
<label>URL сервиса</label>
|
||||||
|
<input type="url" id="targetApiUrl" placeholder="https://example.com">
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="margin-bottom: 15px;">
|
||||||
|
<label>API ключ</label>
|
||||||
|
<input type="text" id="targetApiKey" placeholder="Введите API ключ">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" style="margin-bottom: 15px;">
|
||||||
|
<label>Новые исполнители (ID пользователей через запятую)</label>
|
||||||
|
<input type="text" id="newAssignees" placeholder="123, 456, 789">
|
||||||
|
<small>Оставьте пустым, чтобы сохранить текущих исполнителей</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" style="margin-bottom: 15px;">
|
||||||
|
<label>Новый срок выполнения (необязательно)</label>
|
||||||
|
<input type="datetime-local" id="newDueDate">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" style="margin-bottom: 20px;">
|
||||||
|
<label style="display: flex; align-items: center; gap: 10px;">
|
||||||
|
<input type="checkbox" id="copyFiles" checked>
|
||||||
|
<span>Копировать файлы</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="copyProgress" style="display: none;">
|
||||||
|
<div style="margin-bottom: 5px;">Копирование: <span id="copyPercent">0%</span></div>
|
||||||
|
<div class="progress-bar">
|
||||||
|
<div class="progress-fill" id="copyProgressBar" style="width: 0%;"></div>
|
||||||
|
</div>
|
||||||
|
<div id="copyStatus" class="copy-status"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: flex; gap: 10px; justify-content: flex-end;">
|
||||||
|
<button class="btn btn-secondary" onclick="closeCopyModal()">Отмена</button>
|
||||||
|
<button class="btn btn-success" onclick="copyTask()" id="copyBtn">
|
||||||
|
<i class="fas fa-copy"></i> Копировать задачу
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Уведомления -->
|
<!-- Уведомления -->
|
||||||
<div id="alert" class="alert"></div>
|
<div id="alert" class="alert"></div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
// Глобальные переменные
|
||||||
let currentConnectionId = null;
|
let currentConnectionId = null;
|
||||||
let currentTasks = [];
|
let currentTasks = [];
|
||||||
let currentPage = 0;
|
let currentPage = 0;
|
||||||
@@ -885,6 +1005,8 @@
|
|||||||
let pageSize = 50;
|
let pageSize = 50;
|
||||||
let currentTaskId = null;
|
let currentTaskId = null;
|
||||||
let selectedFiles = [];
|
let selectedFiles = [];
|
||||||
|
let copyTaskId = null;
|
||||||
|
let copyTaskTitle = '';
|
||||||
|
|
||||||
// Проверка авторизации
|
// Проверка авторизации
|
||||||
async function checkAuth() {
|
async function checkAuth() {
|
||||||
@@ -894,6 +1016,7 @@
|
|||||||
|
|
||||||
if (data.user) {
|
if (data.user) {
|
||||||
document.getElementById('userName').textContent = data.user.name || data.user.login;
|
document.getElementById('userName').textContent = data.user.name || data.user.login;
|
||||||
|
document.getElementById('currentUserName').textContent = data.user.name || data.user.login;
|
||||||
} else {
|
} else {
|
||||||
window.location.href = '/';
|
window.location.href = '/';
|
||||||
}
|
}
|
||||||
@@ -946,17 +1069,14 @@
|
|||||||
function selectConnection(connectionId) {
|
function selectConnection(connectionId) {
|
||||||
currentConnectionId = connectionId;
|
currentConnectionId = connectionId;
|
||||||
|
|
||||||
// Подсвечиваем активное подключение
|
|
||||||
document.querySelectorAll('.connection-item').forEach(el => {
|
document.querySelectorAll('.connection-item').forEach(el => {
|
||||||
el.classList.remove('active');
|
el.classList.remove('active');
|
||||||
});
|
});
|
||||||
event.currentTarget.classList.add('active');
|
event.currentTarget.classList.add('active');
|
||||||
|
|
||||||
// Активируем кнопки
|
|
||||||
document.getElementById('loadTasksBtn').disabled = false;
|
document.getElementById('loadTasksBtn').disabled = false;
|
||||||
document.getElementById('refreshTasksBtn').disabled = false;
|
document.getElementById('refreshTasksBtn').disabled = false;
|
||||||
|
|
||||||
// Загружаем задачи
|
|
||||||
loadTasks();
|
loadTasks();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1018,10 +1138,8 @@
|
|||||||
if (data.success) {
|
if (data.success) {
|
||||||
showAlert('Подключение успешно установлено', 'success');
|
showAlert('Подключение успешно установлено', 'success');
|
||||||
|
|
||||||
// Сохраняем ID подключения
|
|
||||||
currentConnectionId = data.connection.id;
|
currentConnectionId = data.connection.id;
|
||||||
|
|
||||||
// Показываем информацию о сервере
|
|
||||||
const serverInfo = document.getElementById('serverInfo');
|
const serverInfo = document.getElementById('serverInfo');
|
||||||
serverInfo.innerHTML = `
|
serverInfo.innerHTML = `
|
||||||
<i class="fas fa-server"></i>
|
<i class="fas fa-server"></i>
|
||||||
@@ -1031,18 +1149,14 @@
|
|||||||
`;
|
`;
|
||||||
serverInfo.style.display = 'block';
|
serverInfo.style.display = 'block';
|
||||||
|
|
||||||
// Обновляем список сохраненных подключений
|
|
||||||
loadSavedConnections();
|
loadSavedConnections();
|
||||||
|
|
||||||
// Активируем кнопки
|
|
||||||
document.getElementById('loadTasksBtn').disabled = false;
|
document.getElementById('loadTasksBtn').disabled = false;
|
||||||
document.getElementById('refreshTasksBtn').disabled = false;
|
document.getElementById('refreshTasksBtn').disabled = false;
|
||||||
|
|
||||||
// Очищаем поля ввода
|
|
||||||
document.getElementById('apiUrl').value = '';
|
document.getElementById('apiUrl').value = '';
|
||||||
document.getElementById('apiKey').value = '';
|
document.getElementById('apiKey').value = '';
|
||||||
|
|
||||||
// Загружаем задачи
|
|
||||||
loadTasks();
|
loadTasks();
|
||||||
} else {
|
} else {
|
||||||
showAlert(data.error || 'Ошибка подключения', 'danger');
|
showAlert(data.error || 'Ошибка подключения', 'danger');
|
||||||
@@ -1110,7 +1224,6 @@
|
|||||||
const statusClass = `status-${task.assignment_status || 'default'}`;
|
const statusClass = `status-${task.assignment_status || 'default'}`;
|
||||||
const statusText = getStatusText(task.assignment_status);
|
const statusText = getStatusText(task.assignment_status);
|
||||||
|
|
||||||
// Форматируем даты
|
|
||||||
const createdDate = task.created_at ? new Date(task.created_at).toLocaleString() : 'Н/Д';
|
const createdDate = task.created_at ? new Date(task.created_at).toLocaleString() : 'Н/Д';
|
||||||
const dueDate = task.due_date ? new Date(task.due_date).toLocaleString() : 'Нет срока';
|
const dueDate = task.due_date ? new Date(task.due_date).toLocaleString() : 'Нет срока';
|
||||||
|
|
||||||
@@ -1154,6 +1267,9 @@
|
|||||||
<button class="task-action-btn action-upload" onclick="openUploadModal('${task.id}')">
|
<button class="task-action-btn action-upload" onclick="openUploadModal('${task.id}')">
|
||||||
<i class="fas fa-upload"></i> Файлы
|
<i class="fas fa-upload"></i> Файлы
|
||||||
</button>
|
</button>
|
||||||
|
<button class="task-action-btn action-copy" onclick="openCopyModal('${task.id}', '${escapeHtml(task.title)}')">
|
||||||
|
<i class="fas fa-copy"></i> Копировать
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -1176,11 +1292,11 @@
|
|||||||
${files.map(file => `
|
${files.map(file => `
|
||||||
<li class="file-item">
|
<li class="file-item">
|
||||||
<i class="fas fa-file file-icon"></i>
|
<i class="fas fa-file file-icon"></i>
|
||||||
<span class="file-name" title="${escapeHtml(file.filename)}">
|
<span class="file-name" title="${escapeHtml(file.filename || file.original_name)}">
|
||||||
${escapeHtml(file.filename)}
|
${escapeHtml(file.filename || file.original_name)}
|
||||||
</span>
|
</span>
|
||||||
<span class="file-size">${formatFileSize(file.file_size)}</span>
|
<span class="file-size">${formatFileSize(file.file_size)}</span>
|
||||||
<a href="#" class="file-download" onclick="downloadFile('${taskId}', '${file.id}', '${escapeHtml(file.filename)}'); return false;">
|
<a href="#" class="file-download" onclick="downloadFile('${taskId}', '${file.id}', '${escapeHtml(file.filename || file.original_name)}'); return false;">
|
||||||
<i class="fas fa-download"></i>
|
<i class="fas fa-download"></i>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -1217,7 +1333,6 @@
|
|||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
showAlert(`Статус задачи изменен на "${getStatusText(status)}"`, 'success');
|
showAlert(`Статус задачи изменен на "${getStatusText(status)}"`, 'success');
|
||||||
// Обновляем список задач
|
|
||||||
loadTasks();
|
loadTasks();
|
||||||
} else {
|
} else {
|
||||||
showAlert(data.error || 'Ошибка обновления статуса', 'danger');
|
showAlert(data.error || 'Ошибка обновления статуса', 'danger');
|
||||||
@@ -1253,7 +1368,7 @@
|
|||||||
document.getElementById('progressPercent').textContent = '0%';
|
document.getElementById('progressPercent').textContent = '0%';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Закрыть модальное окно
|
// Закрыть модальное окно загрузки
|
||||||
function closeUploadModal() {
|
function closeUploadModal() {
|
||||||
document.getElementById('uploadModal').classList.remove('active');
|
document.getElementById('uploadModal').classList.remove('active');
|
||||||
currentTaskId = null;
|
currentTaskId = null;
|
||||||
@@ -1339,9 +1454,9 @@
|
|||||||
if (xhr.status === 200) {
|
if (xhr.status === 200) {
|
||||||
const data = JSON.parse(xhr.responseText);
|
const data = JSON.parse(xhr.responseText);
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
showAlert(`Успешно загружено ${data.data?.files_uploaded || selectedFiles.length} файлов`, 'success');
|
showAlert(`Успешно загружено ${selectedFiles.length} файлов`, 'success');
|
||||||
closeUploadModal();
|
closeUploadModal();
|
||||||
loadTasks(); // Обновляем список задач
|
loadTasks();
|
||||||
} else {
|
} else {
|
||||||
showAlert(data.error || 'Ошибка загрузки файлов', 'danger');
|
showAlert(data.error || 'Ошибка загрузки файлов', 'danger');
|
||||||
uploadBtn.disabled = false;
|
uploadBtn.disabled = false;
|
||||||
@@ -1394,6 +1509,173 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Загрузка списка подключений для копирования
|
||||||
|
async function loadTargetConnections() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/client/connections/list');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
const select = document.getElementById('savedTargetConnections');
|
||||||
|
select.innerHTML = data.connections.map(conn =>
|
||||||
|
`<option value="${conn.id}">${conn.name} (${conn.url})</option>`
|
||||||
|
).join('');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка загрузки подключений:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Открыть модальное окно копирования
|
||||||
|
function openCopyModal(taskId, taskTitle) {
|
||||||
|
copyTaskId = taskId;
|
||||||
|
copyTaskTitle = taskTitle;
|
||||||
|
|
||||||
|
document.getElementById('copyTaskTitle').textContent = taskTitle;
|
||||||
|
|
||||||
|
loadTargetConnections();
|
||||||
|
|
||||||
|
document.getElementById('copyModal').classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Закрыть модальное окно копирования
|
||||||
|
function closeCopyModal() {
|
||||||
|
document.getElementById('copyModal').classList.remove('active');
|
||||||
|
copyTaskId = null;
|
||||||
|
|
||||||
|
document.getElementById('targetService').value = '';
|
||||||
|
document.getElementById('newServiceInputs').style.display = 'none';
|
||||||
|
document.getElementById('targetApiUrl').value = '';
|
||||||
|
document.getElementById('targetApiKey').value = '';
|
||||||
|
document.getElementById('newAssignees').value = '';
|
||||||
|
document.getElementById('newDueDate').value = '';
|
||||||
|
document.getElementById('copyFiles').checked = true;
|
||||||
|
document.getElementById('copyProgress').style.display = 'none';
|
||||||
|
document.getElementById('copyBtn').disabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Переключение между сохраненным и новым сервисом
|
||||||
|
function toggleTargetServiceInput() {
|
||||||
|
const targetService = document.getElementById('targetService').value;
|
||||||
|
document.getElementById('newServiceInputs').style.display =
|
||||||
|
targetService === 'new' ? 'block' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Копирование задачи
|
||||||
|
async function copyTask() {
|
||||||
|
if (!copyTaskId) return;
|
||||||
|
|
||||||
|
const targetService = document.getElementById('targetService').value;
|
||||||
|
const newAssignees = document.getElementById('newAssignees').value;
|
||||||
|
const newDueDate = document.getElementById('newDueDate').value;
|
||||||
|
const copyFiles = document.getElementById('copyFiles').checked;
|
||||||
|
|
||||||
|
if (!targetService) {
|
||||||
|
showAlert('Выберите целевой сервис', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let targetData = {};
|
||||||
|
|
||||||
|
if (targetService === 'new') {
|
||||||
|
const targetApiUrl = document.getElementById('targetApiUrl').value.trim();
|
||||||
|
const targetApiKey = document.getElementById('targetApiKey').value.trim();
|
||||||
|
|
||||||
|
if (!targetApiUrl || !targetApiKey) {
|
||||||
|
showAlert('Заполните URL и API ключ целевого сервиса', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
targetData = {
|
||||||
|
target_api_url: targetApiUrl,
|
||||||
|
target_api_key: targetApiKey
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
targetData = {
|
||||||
|
target_connection_id: targetService
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestData = {
|
||||||
|
...targetData,
|
||||||
|
copy_files: copyFiles
|
||||||
|
};
|
||||||
|
|
||||||
|
if (newAssignees) {
|
||||||
|
requestData.new_assignees = newAssignees.split(',').map(id => parseInt(id.trim()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newDueDate) {
|
||||||
|
requestData.due_date = new Date(newDueDate).toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('copyProgress').style.display = 'block';
|
||||||
|
document.getElementById('copyBtn').disabled = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/client/tasks/${copyTaskId}/copy?connection_id=${currentConnectionId}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(requestData)
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
let progress = 0;
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
progress += 10;
|
||||||
|
document.getElementById('copyProgressBar').style.width = progress + '%';
|
||||||
|
document.getElementById('copyPercent').textContent = progress + '%';
|
||||||
|
|
||||||
|
if (progress >= 100) {
|
||||||
|
clearInterval(interval);
|
||||||
|
|
||||||
|
let statusText = `✅ Задача скопирована! Новый ID: ${data.data.new_task_id}`;
|
||||||
|
|
||||||
|
if (data.data.assignees && data.data.assignees !== 'не изменены') {
|
||||||
|
if (Array.isArray(data.data.assignees)) {
|
||||||
|
statusText += `<br>👥 Исполнители: ${data.data.assignees.join(', ')}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.data.copied_files && data.data.copied_files.length > 0) {
|
||||||
|
const successCount = data.data.copied_files.filter(f => f.success).length;
|
||||||
|
const failCount = data.data.copied_files.filter(f => !f.success).length;
|
||||||
|
statusText += `<br>📁 Файлы: ${successCount} скопировано, ${failCount} ошибок`;
|
||||||
|
|
||||||
|
if (failCount > 0) {
|
||||||
|
const errors = data.data.copied_files
|
||||||
|
.filter(f => !f.success)
|
||||||
|
.map(f => f.original_name)
|
||||||
|
.join(', ');
|
||||||
|
statusText += `<br><small style="color: #e74c3c;">Ошибки: ${errors}</small>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('copyStatus').innerHTML = statusText;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
closeCopyModal();
|
||||||
|
showAlert(`Задача скопирована в ${data.data.target_service}`, 'success');
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
}, 200);
|
||||||
|
} else {
|
||||||
|
showAlert(data.error || 'Ошибка копирования задачи', 'danger');
|
||||||
|
document.getElementById('copyProgress').style.display = 'none';
|
||||||
|
document.getElementById('copyBtn').disabled = false;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка копирования:', error);
|
||||||
|
showAlert('Ошибка при копировании задачи', 'danger');
|
||||||
|
document.getElementById('copyProgress').style.display = 'none';
|
||||||
|
document.getElementById('copyBtn').disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Смена страницы
|
// Смена страницы
|
||||||
function changePage(delta) {
|
function changePage(delta) {
|
||||||
const newPage = currentPage + delta;
|
const newPage = currentPage + delta;
|
||||||
@@ -1449,6 +1731,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function escapeHtml(unsafe) {
|
function escapeHtml(unsafe) {
|
||||||
|
if (!unsafe) return '';
|
||||||
return unsafe
|
return unsafe
|
||||||
.replace(/&/g, "&")
|
.replace(/&/g, "&")
|
||||||
.replace(/</g, "<")
|
.replace(/</g, "<")
|
||||||
|
|||||||
Reference in New Issue
Block a user