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 @@ - - @@ -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 = ''; } }