diff --git a/api-client.js b/api-client.js
index 65ac3ee..525456d 100644
--- a/api-client.js
+++ b/api-client.js
@@ -445,17 +445,15 @@ module.exports = function(app, db, upload) {
});
/**
- * POST /api/client/tasks/:taskId/copy - Скопировать задачу в целевой сервис
+ * POST /api/client/tasks/:taskId/sync - Синхронизировать задачу с целевым сервисом
*/
- router.post('/api/client/tasks/:taskId/copy', requireAuth, async (req, res) => {
+ router.post('/api/client/tasks/:taskId/sync', 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
+ sync_files = true
} = req.body;
const { connection_id } = req.query;
const userId = req.session.user.id;
@@ -505,46 +503,100 @@ module.exports = function(app, db, upload) {
}
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
+ // 2. Проверяем, существует ли задача в целевой системе (поиск по ID)
+ let existingTask = null;
+ try {
+ const checkResponse = await axios.get(
+ `${baseUrl}/api/external/tasks/${taskId}`,
+ {
+ headers: {
+ 'X-API-Key': targetKey
+ },
+ timeout: 5000
+ }
+ );
+ if (checkResponse.data && checkResponse.data.success) {
+ existingTask = checkResponse.data.task;
}
- );
-
- if (!createResponse.data || !createResponse.data.success) {
- return res.status(500).json({
- error: 'Не удалось создать задачу в целевом сервисе'
- });
+ } catch (checkError) {
+ // Задача не найдена - это нормально, будем создавать новую
+ console.log('Задача не найдена в целевой системе, будет создана новая');
}
- const newTaskId = createResponse.data.taskId;
+ let result;
+ const syncedFiles = [];
+ const warnings = [];
- // 3. Копируем файлы, если нужно
- const copiedFiles = [];
- if (copy_files && sourceTask.files && sourceTask.files.length > 0) {
+ if (existingTask) {
+ // 3. Обновляем существующую задачу
+ const updateData = {
+ title: sourceTask.title,
+ description: sourceTask.description,
+ due_date: sourceTask.due_date,
+ task_type: sourceTask.task_type || 'regular'
+ };
+
+ const updateResponse = await axios.put(
+ `${baseUrl}/api/external/tasks/${taskId}`,
+ updateData,
+ {
+ headers: {
+ 'X-API-Key': targetKey,
+ 'Content-Type': 'application/json'
+ },
+ timeout: 15000
+ }
+ );
+
+ if (!updateResponse.data || !updateResponse.data.success) {
+ return res.status(500).json({
+ error: 'Не удалось обновить задачу в целевом сервисе'
+ });
+ }
+
+ result = {
+ taskId: taskId,
+ action: 'updated'
+ };
+ } else {
+ // 4. Создаем новую задачу (без префикса "Копия:")
+ const taskData = {
+ title: sourceTask.title,
+ description: sourceTask.description || '',
+ due_date: sourceTask.due_date,
+ task_type: sourceTask.task_type || 'regular'
+ };
+
+ 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: 'Не удалось создать задачу в целевом сервисе'
+ });
+ }
+
+ result = {
+ taskId: createResponse.data.taskId,
+ action: 'created'
+ };
+ }
+
+ // 5. Синхронизируем файлы, если нужно
+ if (sync_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`,
@@ -555,6 +607,7 @@ module.exports = function(app, db, upload) {
timeout: 30000
});
+ // Загружаем в целевую систему
const formData = new FormData();
formData.append('files', Buffer.from(fileResponse.data), {
filename: file.filename || file.original_name || 'file',
@@ -562,7 +615,7 @@ module.exports = function(app, db, upload) {
});
const uploadResponse = await axios.post(
- `${baseUrl}/api/external/tasks/${newTaskId}/files`,
+ `${baseUrl}/api/external/tasks/${result.taskId}/files`,
formData,
{
headers: {
@@ -575,44 +628,46 @@ module.exports = function(app, db, upload) {
}
);
- copiedFiles.push({
+ syncedFiles.push({
original_name: file.filename || file.original_name,
success: uploadResponse.data && uploadResponse.data.success
});
} catch (fileError) {
- console.error(`❌ Ошибка копирования файла:`, fileError.message);
- copiedFiles.push({
+ console.error(`❌ Ошибка синхронизации файла:`, fileError.message);
+ syncedFiles.push({
original_name: file.filename || file.original_name,
success: false,
error: fileError.message
});
+ warnings.push(`Не удалось синхронизировать файл: ${file.filename || file.original_name}`);
}
}
}
const { logActivity } = require('./database');
if (logActivity) {
- logActivity(0, userId, 'API_CLIENT_COPY_TASK',
- `Скопирована задача ${taskId} из ${sourceConnection.url} в ${baseUrl}. Новый ID: ${newTaskId}`);
+ logActivity(0, userId, 'API_CLIENT_SYNC_TASK',
+ `${existingTask ? 'Обновлена' : 'Создана'} задача ${taskId} из ${sourceConnection.url} в ${baseUrl}. ${existingTask ? 'Обновление' : 'Новый ID: ' + result.taskId}`);
}
res.json({
success: true,
- message: `Задача успешно скопирована${copiedFiles.length > 0 ? `, скопировано файлов: ${copiedFiles.filter(f => f.success).length}` : ''}`,
+ message: `Задача успешно ${existingTask ? 'обновлена' : 'создана'} в целевой системе`,
data: {
+ sync_type: result.action,
original_task_id: taskId,
- new_task_id: newTaskId,
- new_task_title: newTaskTitle,
+ target_task_id: result.taskId,
target_service: baseUrl,
- copied_files: copiedFiles,
- assignees: new_assignees || 'не изменены'
+ synced_files: syncedFiles,
+ assignees: sourceTask.assignments || [],
+ warnings: warnings
}
});
} catch (error) {
- console.error('❌ Ошибка копирования задачи:', error.message);
+ console.error('❌ Ошибка синхронизации задачи:', error.message);
- let errorMessage = 'Ошибка копирования задачи';
+ let errorMessage = 'Ошибка синхронизации задачи';
let statusCode = 500;
if (error.response) {
@@ -620,7 +675,7 @@ module.exports = function(app, db, upload) {
errorMessage = 'Неверный API ключ для целевого сервиса';
statusCode = 401;
} else if (error.response.status === 403) {
- errorMessage = 'Нет прав для создания задачи в целевом сервисе';
+ errorMessage = 'Нет прав для создания/обновления задачи в целевом сервисе';
statusCode = 403;
} else if (error.response.status === 404) {
errorMessage = 'Исходная задача не найдена';
diff --git a/public/client.html b/public/client.html
index 7b6f4f4..74abf84 100644
--- a/public/client.html
+++ b/public/client.html
@@ -457,12 +457,12 @@
background: #2980b9;
}
- .action-copy {
+ .action-sync {
background: #9b59b6;
color: white;
}
- .action-copy:hover {
+ .action-sync:hover {
background: #8e44ad;
}
@@ -634,7 +634,7 @@
cursor: pointer;
}
- .upload-progress, .copy-progress {
+ .upload-progress, .sync-progress {
margin-top: 15px;
padding: 10px;
background: #ecf0f1;
@@ -655,7 +655,7 @@
transition: width 0.3s;
}
- .copy-status {
+ .sync-status {
margin-top: 10px;
padding: 10px;
background: #ecf0f1;
@@ -923,18 +923,18 @@
-
-
+
+
-
Вы копируете задачу:
+
Вы синхронизируете задачу:
- Автором новой задачи будете
+ Исполнители будут сохранены как в исходной задаче
@@ -958,36 +958,29 @@
-
-
-
- Оставьте пустым, чтобы сохранить текущих исполнителей
-
-
-
-
-
-
-
+
+ При синхронизации задача будет обновлена в целевой системе,
+ если она там уже существует, или создана новая.
+
-
-
Копирование: 0%
+
-
-
@@ -1005,8 +998,8 @@
let pageSize = 50;
let currentTaskId = null;
let selectedFiles = [];
- let copyTaskId = null;
- let copyTaskTitle = '';
+ let syncTaskId = null;
+ let syncTaskTitle = '';
// Проверка авторизации
async function checkAuth() {
@@ -1016,7 +1009,6 @@
if (data.user) {
document.getElementById('userName').textContent = data.user.name || data.user.login;
- document.getElementById('currentUserName').textContent = data.user.name || data.user.login;
} else {
window.location.href = '/';
}
@@ -1267,8 +1259,8 @@
Файлы
-
- Копировать
+
+ Синхронизировать
@@ -1509,7 +1501,7 @@
}
}
- // Загрузка списка подключений для копирования
+ // Загрузка списка подключений для синхронизации
async function loadTargetConnections() {
try {
const response = await fetch('/api/client/connections/list');
@@ -1526,32 +1518,31 @@
}
}
- // Открыть модальное окно копирования
- function openCopyModal(taskId, taskTitle) {
- copyTaskId = taskId;
- copyTaskTitle = taskTitle;
+ // Открыть модальное окно синхронизации
+ function openSyncModal(taskId, taskTitle) {
+ syncTaskId = taskId;
+ syncTaskTitle = taskTitle;
- document.getElementById('copyTaskTitle').textContent = taskTitle;
+ document.getElementById('syncTaskTitle').textContent = taskTitle;
loadTargetConnections();
- document.getElementById('copyModal').classList.add('active');
+ document.getElementById('syncModal').classList.add('active');
}
- // Закрыть модальное окно копирования
- function closeCopyModal() {
- document.getElementById('copyModal').classList.remove('active');
- copyTaskId = null;
+ // Закрыть модальное окно синхронизации
+ function closeSyncModal() {
+ document.getElementById('syncModal').classList.remove('active');
+ syncTaskId = 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;
+ document.getElementById('syncFiles').checked = true;
+ document.getElementById('syncProgress').style.display = 'none';
+ document.getElementById('syncBtn').disabled = false;
+ document.getElementById('syncStatus').innerHTML = '';
}
// Переключение между сохраненным и новым сервисом
@@ -1561,14 +1552,12 @@
targetService === 'new' ? 'block' : 'none';
}
- // Копирование задачи
- async function copyTask() {
- if (!copyTaskId) return;
+ // Синхронизация задачи
+ async function syncTask() {
+ if (!syncTaskId) 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;
+ const syncFiles = document.getElementById('syncFiles').checked;
if (!targetService) {
showAlert('Выберите целевой сервис', 'warning');
@@ -1598,22 +1587,15 @@
const requestData = {
...targetData,
- copy_files: copyFiles
+ sync_files: syncFiles
};
- 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;
+ document.getElementById('syncProgress').style.display = 'block';
+ document.getElementById('syncBtn').disabled = true;
+ document.getElementById('syncStatus').innerHTML = 'Начинаем синхронизацию...';
try {
- const response = await fetch(`/api/client/tasks/${copyTaskId}/copy?connection_id=${currentConnectionId}`, {
+ const response = await fetch(`/api/client/tasks/${syncTaskId}/sync?connection_id=${currentConnectionId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
@@ -1627,27 +1609,33 @@
let progress = 0;
const interval = setInterval(() => {
progress += 10;
- document.getElementById('copyProgressBar').style.width = progress + '%';
- document.getElementById('copyPercent').textContent = progress + '%';
+ document.getElementById('syncProgressBar').style.width = progress + '%';
+ document.getElementById('syncPercent').textContent = progress + '%';
if (progress >= 100) {
clearInterval(interval);
- let statusText = `✅ Задача скопирована! Новый ID: ${data.data.new_task_id}`;
+ let statusText = `✅ Задача синхронизирована!`;
- if (data.data.assignees && data.data.assignees !== 'не изменены') {
- if (Array.isArray(data.data.assignees)) {
- statusText += `
👥 Исполнители: ${data.data.assignees.join(', ')}`;
- }
+ if (data.data.sync_type === 'created') {
+ statusText += `
📋 Создана новая задача в целевой системе. ID: ${data.data.target_task_id}`;
+ } else if (data.data.sync_type === 'updated') {
+ statusText += `
🔄 Обновлена существующая задача в целевой системе. ID: ${data.data.target_task_id}`;
}
- 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 += `
📁 Файлы: ${successCount} скопировано, ${failCount} ошибок`;
+ statusText += `
👥 Исполнители: сохранены как в исходной задаче`;
+
+ if (data.data.assignees && data.data.assignees.length > 0) {
+ statusText += `
👤 Количество исполнителей: ${data.data.assignees.length}`;
+ }
+
+ if (data.data.synced_files && data.data.synced_files.length > 0) {
+ const successCount = data.data.synced_files.filter(f => f.success).length;
+ const failCount = data.data.synced_files.filter(f => !f.success).length;
+ statusText += `
📁 Файлы: ${successCount} синхронизировано, ${failCount} ошибок`;
if (failCount > 0) {
- const errors = data.data.copied_files
+ const errors = data.data.synced_files
.filter(f => !f.success)
.map(f => f.original_name)
.join(', ');
@@ -1655,24 +1643,31 @@
}
}
- document.getElementById('copyStatus').innerHTML = statusText;
+ if (data.data.warnings && data.data.warnings.length > 0) {
+ statusText += `
⚠️ ${data.data.warnings.join('; ')}`;
+ }
+
+ document.getElementById('syncStatus').innerHTML = statusText;
setTimeout(() => {
- closeCopyModal();
- showAlert(`Задача скопирована в ${data.data.target_service}`, 'success');
+ closeSyncModal();
+ showAlert(`Задача синхронизирована с ${data.data.target_service}`, 'success');
+ loadTasks();
}, 3000);
}
}, 200);
} else {
- showAlert(data.error || 'Ошибка копирования задачи', 'danger');
- document.getElementById('copyProgress').style.display = 'none';
- document.getElementById('copyBtn').disabled = false;
+ showAlert(data.error || 'Ошибка синхронизации задачи', 'danger');
+ document.getElementById('syncProgress').style.display = 'none';
+ document.getElementById('syncBtn').disabled = false;
+ document.getElementById('syncStatus').innerHTML = '';
}
} catch (error) {
- console.error('Ошибка копирования:', error);
- showAlert('Ошибка при копировании задачи', 'danger');
- document.getElementById('copyProgress').style.display = 'none';
- document.getElementById('copyBtn').disabled = false;
+ console.error('Ошибка синхронизации:', error);
+ showAlert('Ошибка при синхронизации задачи', 'danger');
+ document.getElementById('syncProgress').style.display = 'none';
+ document.getElementById('syncBtn').disabled = false;
+ document.getElementById('syncStatus').innerHTML = '';
}
}