This commit is contained in:
2026-02-26 11:26:19 +05:00
parent 9d28e67388
commit 318cbc8e71
2 changed files with 193 additions and 143 deletions

View File

@@ -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 { taskId } = req.params;
const { const {
target_connection_id, target_connection_id,
target_api_url, target_api_url,
target_api_key, target_api_key,
new_assignees, sync_files = true
due_date,
copy_files = true
} = req.body; } = req.body;
const { connection_id } = req.query; const { connection_id } = req.query;
const userId = req.session.user.id; const userId = req.session.user.id;
@@ -506,19 +504,69 @@ module.exports = function(app, db, upload) {
const sourceTask = sourceResponse.data.task; const sourceTask = sourceResponse.data.task;
// 2. Создаем копию задачи в целевом сервисе // 2. Проверяем, существует ли задача в целевой системе (поиск по ID)
const newTaskTitle = `Копия: ${sourceTask.title}`; 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;
}
} catch (checkError) {
// Задача не найдена - это нормально, будем создавать новую
console.log('Задача не найдена в целевой системе, будет создана новая');
}
const taskData = { let result;
title: newTaskTitle, const syncedFiles = [];
description: sourceTask.description || '', const warnings = [];
due_date: due_date || sourceTask.due_date,
if (existingTask) {
// 3. Обновляем существующую задачу
const updateData = {
title: sourceTask.title,
description: sourceTask.description,
due_date: sourceTask.due_date,
task_type: sourceTask.task_type || 'regular' task_type: sourceTask.task_type || 'regular'
}; };
if (new_assignees && new_assignees.length > 0) { const updateResponse = await axios.put(
taskData.assignedUsers = new_assignees; `${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( const createResponse = await axios.post(
`${baseUrl}/api/external/tasks/create`, `${baseUrl}/api/external/tasks/create`,
@@ -538,13 +586,17 @@ module.exports = function(app, db, upload) {
}); });
} }
const newTaskId = createResponse.data.taskId; result = {
taskId: createResponse.data.taskId,
action: 'created'
};
}
// 3. Копируем файлы, если нужно // 5. Синхронизируем файлы, если нужно
const copiedFiles = []; if (sync_files && sourceTask.files && sourceTask.files.length > 0) {
if (copy_files && sourceTask.files && sourceTask.files.length > 0) {
for (const file of sourceTask.files) { for (const file of sourceTask.files) {
try { try {
// Скачиваем файл из источника
const fileResponse = await axios({ const fileResponse = await axios({
method: 'GET', method: 'GET',
url: `${sourceConnection.url}/api/external/tasks/${taskId}/files/${file.id}/download`, url: `${sourceConnection.url}/api/external/tasks/${taskId}/files/${file.id}/download`,
@@ -555,6 +607,7 @@ module.exports = function(app, db, upload) {
timeout: 30000 timeout: 30000
}); });
// Загружаем в целевую систему
const formData = new FormData(); const formData = new FormData();
formData.append('files', Buffer.from(fileResponse.data), { formData.append('files', Buffer.from(fileResponse.data), {
filename: file.filename || file.original_name || 'file', filename: file.filename || file.original_name || 'file',
@@ -562,7 +615,7 @@ module.exports = function(app, db, upload) {
}); });
const uploadResponse = await axios.post( const uploadResponse = await axios.post(
`${baseUrl}/api/external/tasks/${newTaskId}/files`, `${baseUrl}/api/external/tasks/${result.taskId}/files`,
formData, formData,
{ {
headers: { headers: {
@@ -575,44 +628,46 @@ module.exports = function(app, db, upload) {
} }
); );
copiedFiles.push({ syncedFiles.push({
original_name: file.filename || file.original_name, original_name: file.filename || file.original_name,
success: uploadResponse.data && uploadResponse.data.success success: uploadResponse.data && uploadResponse.data.success
}); });
} catch (fileError) { } catch (fileError) {
console.error(`❌ Ошибка копирования файла:`, fileError.message); console.error(`❌ Ошибка синхронизации файла:`, fileError.message);
copiedFiles.push({ syncedFiles.push({
original_name: file.filename || file.original_name, original_name: file.filename || file.original_name,
success: false, success: false,
error: fileError.message error: fileError.message
}); });
warnings.push(`Не удалось синхронизировать файл: ${file.filename || file.original_name}`);
} }
} }
} }
const { logActivity } = require('./database'); const { logActivity } = require('./database');
if (logActivity) { if (logActivity) {
logActivity(0, userId, 'API_CLIENT_COPY_TASK', logActivity(0, userId, 'API_CLIENT_SYNC_TASK',
`Скопирована задача ${taskId} из ${sourceConnection.url} в ${baseUrl}. Новый ID: ${newTaskId}`); `${existingTask ? 'Обновлена' : 'Создана'} задача ${taskId} из ${sourceConnection.url} в ${baseUrl}. ${existingTask ? 'Обновление' : 'Новый ID: ' + result.taskId}`);
} }
res.json({ res.json({
success: true, success: true,
message: `Задача успешно скопирована${copiedFiles.length > 0 ? `, скопировано файлов: ${copiedFiles.filter(f => f.success).length}` : ''}`, message: `Задача успешно ${existingTask ? 'обновлена' : 'создана'} в целевой системе`,
data: { data: {
sync_type: result.action,
original_task_id: taskId, original_task_id: taskId,
new_task_id: newTaskId, target_task_id: result.taskId,
new_task_title: newTaskTitle,
target_service: baseUrl, target_service: baseUrl,
copied_files: copiedFiles, synced_files: syncedFiles,
assignees: new_assignees || 'не изменены' assignees: sourceTask.assignments || [],
warnings: warnings
} }
}); });
} catch (error) { } catch (error) {
console.error('❌ Ошибка копирования задачи:', error.message); console.error('❌ Ошибка синхронизации задачи:', error.message);
let errorMessage = 'Ошибка копирования задачи'; let errorMessage = 'Ошибка синхронизации задачи';
let statusCode = 500; let statusCode = 500;
if (error.response) { if (error.response) {
@@ -620,7 +675,7 @@ module.exports = function(app, db, upload) {
errorMessage = 'Неверный API ключ для целевого сервиса'; errorMessage = 'Неверный API ключ для целевого сервиса';
statusCode = 401; statusCode = 401;
} else if (error.response.status === 403) { } else if (error.response.status === 403) {
errorMessage = 'Нет прав для создания задачи в целевом сервисе'; errorMessage = 'Нет прав для создания/обновления задачи в целевом сервисе';
statusCode = 403; statusCode = 403;
} else if (error.response.status === 404) { } else if (error.response.status === 404) {
errorMessage = 'Исходная задача не найдена'; errorMessage = 'Исходная задача не найдена';

View File

@@ -457,12 +457,12 @@
background: #2980b9; background: #2980b9;
} }
.action-copy { .action-sync {
background: #9b59b6; background: #9b59b6;
color: white; color: white;
} }
.action-copy:hover { .action-sync:hover {
background: #8e44ad; background: #8e44ad;
} }
@@ -634,7 +634,7 @@
cursor: pointer; cursor: pointer;
} }
.upload-progress, .copy-progress { .upload-progress, .sync-progress {
margin-top: 15px; margin-top: 15px;
padding: 10px; padding: 10px;
background: #ecf0f1; background: #ecf0f1;
@@ -655,7 +655,7 @@
transition: width 0.3s; transition: width 0.3s;
} }
.copy-status { .sync-status {
margin-top: 10px; margin-top: 10px;
padding: 10px; padding: 10px;
background: #ecf0f1; background: #ecf0f1;
@@ -923,18 +923,18 @@
</div> </div>
</div> </div>
<!-- Модальное окно копирования задачи --> <!-- Модальное окно синхронизации задачи -->
<div class="modal" id="copyModal"> <div class="modal" id="syncModal">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h3>Копирование задачи</h3> <h3>Синхронизация задачи</h3>
<span class="modal-close" onclick="closeCopyModal()">&times;</span> <span class="modal-close" onclick="closeSyncModal()">&times;</span>
</div> </div>
<div style="margin-bottom: 20px;"> <div style="margin-bottom: 20px;">
<p>Вы копируете задачу: <strong id="copyTaskTitle"></strong></p> <p>Вы синхронизируете задачу: <strong id="syncTaskTitle"></strong></p>
<p style="font-size: 14px; color: #7f8c8d; margin-top: 5px;"> <p style="font-size: 14px; color: #7f8c8d; margin-top: 5px;">
Автором новой задачи будете <strong id="currentUserName"></strong> <i class="fas fa-info-circle"></i> Исполнители будут сохранены как в исходной задаче
</p> </p>
</div> </div>
@@ -958,36 +958,29 @@
</div> </div>
</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;"> <div class="form-group" style="margin-bottom: 20px;">
<label style="display: flex; align-items: center; gap: 10px;"> <label style="display: flex; align-items: center; gap: 10px;">
<input type="checkbox" id="copyFiles" checked> <input type="checkbox" id="syncFiles" checked>
<span>Копировать файлы</span> <span>Синхронизировать файлы</span>
</label> </label>
<small style="display: block; margin-top: 5px; color: #7f8c8d;">
<i class="fas fa-exchange-alt"></i> При синхронизации задача будет обновлена в целевой системе,
если она там уже существует, или создана новая.
</small>
</div> </div>
<div id="copyProgress" style="display: none;"> <div id="syncProgress" style="display: none;">
<div style="margin-bottom: 5px;">Копирование: <span id="copyPercent">0%</span></div> <div style="margin-bottom: 5px;">Синхронизация: <span id="syncPercent">0%</span></div>
<div class="progress-bar"> <div class="progress-bar">
<div class="progress-fill" id="copyProgressBar" style="width: 0%;"></div> <div class="progress-fill" id="syncProgressBar" style="width: 0%;"></div>
</div> </div>
<div id="copyStatus" class="copy-status"></div> <div id="syncStatus" class="sync-status"></div>
</div> </div>
<div style="display: flex; gap: 10px; justify-content: flex-end;"> <div style="display: flex; gap: 10px; justify-content: flex-end;">
<button class="btn btn-secondary" onclick="closeCopyModal()">Отмена</button> <button class="btn btn-secondary" onclick="closeSyncModal()">Отмена</button>
<button class="btn btn-success" onclick="copyTask()" id="copyBtn"> <button class="btn btn-success" onclick="syncTask()" id="syncBtn">
<i class="fas fa-copy"></i> Копировать задачу <i class="fas fa-sync-alt"></i> Синхронизировать задачу
</button> </button>
</div> </div>
</div> </div>
@@ -1005,8 +998,8 @@
let pageSize = 50; let pageSize = 50;
let currentTaskId = null; let currentTaskId = null;
let selectedFiles = []; let selectedFiles = [];
let copyTaskId = null; let syncTaskId = null;
let copyTaskTitle = ''; let syncTaskTitle = '';
// Проверка авторизации // Проверка авторизации
async function checkAuth() { async function checkAuth() {
@@ -1016,7 +1009,6 @@
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 = '/';
} }
@@ -1267,8 +1259,8 @@
<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)}')"> <button class="task-action-btn action-sync" onclick="openSyncModal('${task.id}', '${escapeHtml(task.title)}')">
<i class="fas fa-copy"></i> Копировать <i class="fas fa-sync-alt"></i> Синхронизировать
</button> </button>
</div> </div>
</div> </div>
@@ -1509,7 +1501,7 @@
} }
} }
// Загрузка списка подключений для копирования // Загрузка списка подключений для синхронизации
async function loadTargetConnections() { async function loadTargetConnections() {
try { try {
const response = await fetch('/api/client/connections/list'); const response = await fetch('/api/client/connections/list');
@@ -1526,32 +1518,31 @@
} }
} }
// Открыть модальное окно копирования // Открыть модальное окно синхронизации
function openCopyModal(taskId, taskTitle) { function openSyncModal(taskId, taskTitle) {
copyTaskId = taskId; syncTaskId = taskId;
copyTaskTitle = taskTitle; syncTaskTitle = taskTitle;
document.getElementById('copyTaskTitle').textContent = taskTitle; document.getElementById('syncTaskTitle').textContent = taskTitle;
loadTargetConnections(); loadTargetConnections();
document.getElementById('copyModal').classList.add('active'); document.getElementById('syncModal').classList.add('active');
} }
// Закрыть модальное окно копирования // Закрыть модальное окно синхронизации
function closeCopyModal() { function closeSyncModal() {
document.getElementById('copyModal').classList.remove('active'); document.getElementById('syncModal').classList.remove('active');
copyTaskId = null; syncTaskId = null;
document.getElementById('targetService').value = ''; document.getElementById('targetService').value = '';
document.getElementById('newServiceInputs').style.display = 'none'; document.getElementById('newServiceInputs').style.display = 'none';
document.getElementById('targetApiUrl').value = ''; document.getElementById('targetApiUrl').value = '';
document.getElementById('targetApiKey').value = ''; document.getElementById('targetApiKey').value = '';
document.getElementById('newAssignees').value = ''; document.getElementById('syncFiles').checked = true;
document.getElementById('newDueDate').value = ''; document.getElementById('syncProgress').style.display = 'none';
document.getElementById('copyFiles').checked = true; document.getElementById('syncBtn').disabled = false;
document.getElementById('copyProgress').style.display = 'none'; document.getElementById('syncStatus').innerHTML = '';
document.getElementById('copyBtn').disabled = false;
} }
// Переключение между сохраненным и новым сервисом // Переключение между сохраненным и новым сервисом
@@ -1561,14 +1552,12 @@
targetService === 'new' ? 'block' : 'none'; targetService === 'new' ? 'block' : 'none';
} }
// Копирование задачи // Синхронизация задачи
async function copyTask() { async function syncTask() {
if (!copyTaskId) return; if (!syncTaskId) return;
const targetService = document.getElementById('targetService').value; const targetService = document.getElementById('targetService').value;
const newAssignees = document.getElementById('newAssignees').value; const syncFiles = document.getElementById('syncFiles').checked;
const newDueDate = document.getElementById('newDueDate').value;
const copyFiles = document.getElementById('copyFiles').checked;
if (!targetService) { if (!targetService) {
showAlert('Выберите целевой сервис', 'warning'); showAlert('Выберите целевой сервис', 'warning');
@@ -1598,22 +1587,15 @@
const requestData = { const requestData = {
...targetData, ...targetData,
copy_files: copyFiles sync_files: syncFiles
}; };
if (newAssignees) { document.getElementById('syncProgress').style.display = 'block';
requestData.new_assignees = newAssignees.split(',').map(id => parseInt(id.trim())); document.getElementById('syncBtn').disabled = true;
} document.getElementById('syncStatus').innerHTML = 'Начинаем синхронизацию...';
if (newDueDate) {
requestData.due_date = new Date(newDueDate).toISOString();
}
document.getElementById('copyProgress').style.display = 'block';
document.getElementById('copyBtn').disabled = true;
try { 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', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
@@ -1627,27 +1609,33 @@
let progress = 0; let progress = 0;
const interval = setInterval(() => { const interval = setInterval(() => {
progress += 10; progress += 10;
document.getElementById('copyProgressBar').style.width = progress + '%'; document.getElementById('syncProgressBar').style.width = progress + '%';
document.getElementById('copyPercent').textContent = progress + '%'; document.getElementById('syncPercent').textContent = progress + '%';
if (progress >= 100) { if (progress >= 100) {
clearInterval(interval); clearInterval(interval);
let statusText = `✅ Задача скопирована! Новый ID: ${data.data.new_task_id}`; let statusText = `✅ Задача синхронизирована!`;
if (data.data.assignees && data.data.assignees !== 'не изменены') { if (data.data.sync_type === 'created') {
if (Array.isArray(data.data.assignees)) { statusText += `<br>📋 Создана новая задача в целевой системе. ID: ${data.data.target_task_id}`;
statusText += `<br>👥 Исполнители: ${data.data.assignees.join(', ')}`; } else if (data.data.sync_type === 'updated') {
} statusText += `<br>🔄 Обновлена существующая задача в целевой системе. ID: ${data.data.target_task_id}`;
} }
if (data.data.copied_files && data.data.copied_files.length > 0) { statusText += `<br>👥 Исполнители: сохранены как в исходной задаче`;
const successCount = data.data.copied_files.filter(f => f.success).length;
const failCount = data.data.copied_files.filter(f => !f.success).length; if (data.data.assignees && data.data.assignees.length > 0) {
statusText += `<br>📁 Файлы: ${successCount} скопировано, ${failCount} ошибок`; statusText += `<br>👤 Количество исполнителей: ${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 += `<br>📁 Файлы: ${successCount} синхронизировано, ${failCount} ошибок`;
if (failCount > 0) { if (failCount > 0) {
const errors = data.data.copied_files const errors = data.data.synced_files
.filter(f => !f.success) .filter(f => !f.success)
.map(f => f.original_name) .map(f => f.original_name)
.join(', '); .join(', ');
@@ -1655,24 +1643,31 @@
} }
} }
document.getElementById('copyStatus').innerHTML = statusText; if (data.data.warnings && data.data.warnings.length > 0) {
statusText += `<br><small style="color: #f39c12;">⚠️ ${data.data.warnings.join('; ')}</small>`;
}
document.getElementById('syncStatus').innerHTML = statusText;
setTimeout(() => { setTimeout(() => {
closeCopyModal(); closeSyncModal();
showAlert(`Задача скопирована в ${data.data.target_service}`, 'success'); showAlert(`Задача синхронизирована с ${data.data.target_service}`, 'success');
loadTasks();
}, 3000); }, 3000);
} }
}, 200); }, 200);
} else { } else {
showAlert(data.error || 'Ошибка копирования задачи', 'danger'); showAlert(data.error || 'Ошибка синхронизации задачи', 'danger');
document.getElementById('copyProgress').style.display = 'none'; document.getElementById('syncProgress').style.display = 'none';
document.getElementById('copyBtn').disabled = false; document.getElementById('syncBtn').disabled = false;
document.getElementById('syncStatus').innerHTML = '';
} }
} catch (error) { } catch (error) {
console.error('Ошибка копирования:', error); console.error('Ошибка синхронизации:', error);
showAlert('Ошибка при копировании задачи', 'danger'); showAlert('Ошибка при синхронизации задачи', 'danger');
document.getElementById('copyProgress').style.display = 'none'; document.getElementById('syncProgress').style.display = 'none';
document.getElementById('copyBtn').disabled = false; document.getElementById('syncBtn').disabled = false;
document.getElementById('syncStatus').innerHTML = '';
} }
} }