diff --git a/api-client.js b/api-client.js index 525456d..bedec03 100644 --- a/api-client.js +++ b/api-client.js @@ -445,7 +445,7 @@ module.exports = function(app, db, upload) { }); /** - * POST /api/client/tasks/:taskId/sync - Синхронизировать задачу с целевым сервисом + * POST /api/client/tasks/:taskId/sync - Синхронизировать ЛОКАЛЬНУЮ задачу с целевым сервисом */ router.post('/api/client/tasks/:taskId/sync', requireAuth, async (req, res) => { const { taskId } = req.params; @@ -455,15 +455,9 @@ module.exports = function(app, db, upload) { target_api_key, sync_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; @@ -479,40 +473,43 @@ module.exports = function(app, db, upload) { }); } + const baseUrl = targetUrl.replace(/\/$/, ''); + try { - const baseUrl = targetUrl.replace(/\/$/, ''); - const sourceConnection = req.session.clientConnections[connection_id]; + // 1. Получаем задачу из ЛОКАЛЬНОЙ базы данных + const localTask = await new Promise((resolve, reject) => { + db.get(` + SELECT t.*, u.name as creator_name, u.login as creator_login + FROM tasks t + LEFT JOIN users u ON t.created_by = u.id + WHERE t.id = ? + `, [taskId], (err, row) => { + if (err) reject(err); + else resolve(row); + }); + }); - if (!sourceConnection) { - return res.status(400).json({ error: 'Исходное подключение не найдено' }); + if (!localTask) { + return res.status(404).json({ error: 'Локальная задача не найдена' }); } - // 1. Получаем исходную задачу - const sourceResponse = await axios.get( - `${sourceConnection.url}/api/external/tasks/${taskId}`, - { - headers: { - 'X-API-Key': sourceConnection.api_key - }, - timeout: 10000 - } - ); + // Преобразуем в формат, аналогичный внешнему API + const sourceTask = { + id: localTask.id, + title: localTask.title, + description: localTask.description || '', + due_date: localTask.due_date, + task_type: localTask.task_type || 'regular', + files: await getLocalTaskFiles(taskId) + }; - if (!sourceResponse.data || !sourceResponse.data.success) { - return res.status(404).json({ error: 'Исходная задача не найдена' }); - } - - const sourceTask = sourceResponse.data.task; - // 2. Проверяем, существует ли задача в целевой системе (поиск по ID) let existingTask = null; try { const checkResponse = await axios.get( `${baseUrl}/api/external/tasks/${taskId}`, { - headers: { - 'X-API-Key': targetKey - }, + headers: { 'X-API-Key': targetKey }, timeout: 5000 } ); @@ -534,7 +531,7 @@ module.exports = function(app, db, upload) { title: sourceTask.title, description: sourceTask.description, due_date: sourceTask.due_date, - task_type: sourceTask.task_type || 'regular' + task_type: sourceTask.task_type }; const updateResponse = await axios.put( @@ -550,22 +547,17 @@ module.exports = function(app, db, upload) { ); if (!updateResponse.data || !updateResponse.data.success) { - return res.status(500).json({ - error: 'Не удалось обновить задачу в целевом сервисе' - }); + return res.status(500).json({ error: 'Не удалось обновить задачу в целевом сервисе' }); } - result = { - taskId: taskId, - action: 'updated' - }; + result = { taskId: taskId, action: 'updated' }; } else { - // 4. Создаем новую задачу (без префикса "Копия:") + // 4. Создаём новую задачу const taskData = { title: sourceTask.title, - description: sourceTask.description || '', + description: sourceTask.description, due_date: sourceTask.due_date, - task_type: sourceTask.task_type || 'regular' + task_type: sourceTask.task_type }; const createResponse = await axios.post( @@ -581,9 +573,7 @@ module.exports = function(app, db, upload) { ); if (!createResponse.data || !createResponse.data.success) { - return res.status(500).json({ - error: 'Не удалось создать задачу в целевом сервисе' - }); + return res.status(500).json({ error: 'Не удалось создать задачу в целевом сервисе' }); } result = { @@ -596,21 +586,17 @@ module.exports = function(app, db, upload) { 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`, - headers: { - 'X-API-Key': sourceConnection.api_key - }, - responseType: 'arraybuffer', - timeout: 30000 - }); + // Читаем файл с диска + if (!fs.existsSync(file.file_path)) { + throw new Error(`Файл не найден на диске: ${file.file_path}`); + } + const fileBuffer = fs.readFileSync(file.file_path); + const fileName = file.original_name || path.basename(file.file_path); // Загружаем в целевую систему const formData = new FormData(); - formData.append('files', Buffer.from(fileResponse.data), { - filename: file.filename || file.original_name || 'file', + formData.append('files', fileBuffer, { + filename: fileName, contentType: file.file_type || 'application/octet-stream' }); @@ -629,25 +615,26 @@ module.exports = function(app, db, upload) { ); syncedFiles.push({ - original_name: file.filename || file.original_name, + original_name: fileName, success: uploadResponse.data && uploadResponse.data.success }); } catch (fileError) { console.error(`❌ Ошибка синхронизации файла:`, fileError.message); syncedFiles.push({ - original_name: file.filename || file.original_name, + original_name: file.original_name || 'unknown', success: false, error: fileError.message }); - warnings.push(`Не удалось синхронизировать файл: ${file.filename || file.original_name}`); + warnings.push(`Не удалось синхронизировать файл: ${file.original_name || file.filename}`); } } } + // Логируем действие const { logActivity } = require('./database'); if (logActivity) { logActivity(0, userId, 'API_CLIENT_SYNC_TASK', - `${existingTask ? 'Обновлена' : 'Создана'} задача ${taskId} из ${sourceConnection.url} в ${baseUrl}. ${existingTask ? 'Обновление' : 'Новый ID: ' + result.taskId}`); + `Локальная задача ${taskId} ${existingTask ? 'обновлена' : 'создана'} в ${baseUrl}. Новый ID: ${result.taskId}`); } res.json({ @@ -659,17 +646,17 @@ module.exports = function(app, db, upload) { target_task_id: result.taskId, target_service: baseUrl, synced_files: syncedFiles, - assignees: sourceTask.assignments || [], - warnings: warnings + assignees: [], // исполнители не передаём + warnings: warnings, + source: 'local' } }); } catch (error) { console.error('❌ Ошибка синхронизации задачи:', error.message); - let errorMessage = 'Ошибка синхронизации задачи'; let statusCode = 500; - + if (error.response) { if (error.response.status === 401) { errorMessage = 'Неверный API ключ для целевого сервиса'; @@ -678,7 +665,7 @@ module.exports = function(app, db, upload) { errorMessage = 'Нет прав для создания/обновления задачи в целевом сервисе'; statusCode = 403; } else if (error.response.status === 404) { - errorMessage = 'Исходная задача не найдена'; + errorMessage = 'Целевой эндпоинт не найден'; statusCode = 404; } else { errorMessage = error.response.data?.error || `Ошибка сервера: ${error.response.status}`; @@ -692,10 +679,7 @@ module.exports = function(app, db, upload) { statusCode = 504; } - res.status(statusCode).json({ - error: errorMessage, - details: error.message - }); + res.status(statusCode).json({ error: errorMessage, details: error.message }); } }); @@ -928,4 +912,20 @@ module.exports = function(app, db, upload) { app.use(router); console.log('✅ API клиент для внешних сервисов подключен'); -}; \ No newline at end of file +}; + +// Вспомогательная функция для получения файлов локальной задачи +async function getLocalTaskFiles(taskId) { + return new Promise((resolve, reject) => { + db.all(` + SELECT tf.*, u.name as user_name + FROM task_files tf + LEFT JOIN users u ON tf.user_id = u.id + WHERE tf.task_id = ? + ORDER BY tf.uploaded_at DESC + `, [taskId], (err, files) => { + if (err) reject(err); + else resolve(files || []); + }); + }); +} \ No newline at end of file diff --git a/public/client.html b/public/client.html index cdec17f..d199a2e 100644 --- a/public/client.html +++ b/public/client.html @@ -989,757 +989,6 @@
- + \ No newline at end of file diff --git a/public/client.js b/public/client.js new file mode 100644 index 0000000..30f71b9 --- /dev/null +++ b/public/client.js @@ -0,0 +1,790 @@ +// client.js – клиентская логика для работы с внешним API + +// Глобальные переменные +let currentConnectionId = null; +let currentTasks = []; +let currentPage = 0; +let totalTasks = 0; +let pageSize = 50; +let currentTaskId = null; +let selectedFiles = []; +let syncTaskId = null; +let syncTaskTitle = ''; + +// Проверка авторизации +async function checkAuth() { + try { + const response = await fetch('/api/user'); + const data = await response.json(); + + if (data.user) { + document.getElementById('userName').textContent = data.user.name || data.user.login; + } else { + window.location.href = '/'; + } + } catch (error) { + console.error('Ошибка проверки авторизации:', error); + window.location.href = '/'; + } +} + +// Выход +async function logout() { + try { + await fetch('/api/logout', { method: 'POST' }); + window.location.href = '/'; + } catch (error) { + console.error('Ошибка при выходе:', error); + } +} + +// Загрузка сохраненных подключений +async function loadSavedConnections() { + try { + const response = await fetch('/api/client/connections'); + const data = await response.json(); + + const connectionsList = document.getElementById('connectionsList'); + + if (data.success && data.connections.length > 0) { + connectionsList.innerHTML = data.connections.map(conn => ` +
+ + ${conn.name} + + + +
+ `).join(''); + } else { + connectionsList.innerHTML = '
Нет сохраненных подключений
'; + } + } catch (error) { + console.error('Ошибка загрузки подключений:', error); + document.getElementById('connectionsList').innerHTML = + '
Ошибка загрузки
'; + } +} + +// Выбор сохраненного подключения +function selectConnection(connectionId) { + currentConnectionId = connectionId; + + document.querySelectorAll('.connection-item').forEach(el => { + el.classList.remove('active'); + }); + event.currentTarget.classList.add('active'); + + document.getElementById('loadTasksBtn').disabled = false; + document.getElementById('refreshTasksBtn').disabled = false; + + loadTasks(); +} + +// Удаление подключения +async function removeConnection(connectionId) { + if (!confirm('Удалить это подключение?')) return; + + try { + const response = await fetch(`/api/client/connections/${connectionId}`, { + method: 'DELETE' + }); + + const data = await response.json(); + + if (data.success) { + if (currentConnectionId === connectionId) { + currentConnectionId = null; + document.getElementById('loadTasksBtn').disabled = true; + document.getElementById('refreshTasksBtn').disabled = true; + document.getElementById('tasksContainer').innerHTML = + '

Подключитесь к серверу

'; + document.getElementById('tasksCount').textContent = '0'; + document.getElementById('serverInfo').style.display = 'none'; + } + loadSavedConnections(); + showAlert('Подключение удалено', 'success'); + } + } catch (error) { + console.error('Ошибка удаления подключения:', error); + showAlert('Ошибка при удалении подключения', 'danger'); + } +} + +// Подключение к серверу +async function connect() { + const apiUrl = document.getElementById('apiUrl').value.trim(); + const apiKey = document.getElementById('apiKey').value.trim(); + + if (!apiUrl || !apiKey) { + showAlert('Заполните URL сервиса и API ключ', 'warning'); + return; + } + + const connectBtn = document.getElementById('connectBtn'); + connectBtn.disabled = true; + connectBtn.innerHTML = ' Подключение...'; + + try { + const response = await fetch('/api/client/connect', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ api_url: apiUrl, api_key: apiKey }) + }); + + const data = await response.json(); + + if (data.success) { + showAlert('Подключение успешно установлено', 'success'); + + currentConnectionId = data.connection.id; + + const serverInfo = document.getElementById('serverInfo'); + serverInfo.innerHTML = ` + + Сервер: ${data.connection.url}
+ Пользователь: ${data.server_info.user}
+ Задач на сервере: ${data.server_info.tasks_count} + `; + serverInfo.style.display = 'block'; + + loadSavedConnections(); + + document.getElementById('loadTasksBtn').disabled = false; + document.getElementById('refreshTasksBtn').disabled = false; + + document.getElementById('apiUrl').value = ''; + document.getElementById('apiKey').value = ''; + + loadTasks(); + } else { + showAlert(data.error || 'Ошибка подключения', 'danger'); + } + } catch (error) { + console.error('Ошибка подключения:', error); + showAlert('Ошибка при подключении к серверу', 'danger'); + } finally { + connectBtn.disabled = false; + connectBtn.innerHTML = ' Подключиться'; + } +} + +// Загрузка задач +async function loadTasks() { + if (!currentConnectionId) { + showAlert('Сначала выберите подключение', 'warning'); + return; + } + + const statusFilter = document.getElementById('statusFilter').value; + const searchFilter = document.getElementById('searchFilter').value; + const limit = parseInt(document.getElementById('limitFilter').value); + + pageSize = limit; + + const tasksContainer = document.getElementById('tasksContainer'); + tasksContainer.innerHTML = '

Загрузка задач...

'; + + try { + let url = `/api/client/tasks?connection_id=${currentConnectionId}&limit=${limit}&offset=${currentPage * limit}`; + if (statusFilter) url += `&status=${statusFilter}`; + if (searchFilter) url += `&search=${encodeURIComponent(searchFilter)}`; + + const response = await fetch(url); + const data = await response.json(); + + if (data.success) { + currentTasks = data.tasks || []; + totalTasks = data.meta.total; + + document.getElementById('tasksCount').textContent = totalTasks; + + if (currentTasks.length === 0) { + tasksContainer.innerHTML = '

Задачи не найдены

'; + } else { + renderTasks(currentTasks); + } + + updatePagination(); + } else { + tasksContainer.innerHTML = `

Ошибка: ${data.error || 'Неизвестная ошибка'}

`; + } + } catch (error) { + console.error('Ошибка загрузки задач:', error); + tasksContainer.innerHTML = '

Ошибка загрузки задач

'; + } +} + +// Отрисовка задач +function renderTasks(tasks) { + const container = document.getElementById('tasksContainer'); + + container.innerHTML = tasks.map(task => { + const statusClass = `status-${task.assignment_status || 'default'}`; + const statusText = getStatusText(task.assignment_status); + + const createdDate = task.created_at ? new Date(task.created_at).toLocaleString() : 'Н/Д'; + const dueDate = task.due_date ? new Date(task.due_date).toLocaleString() : 'Нет срока'; + + return ` +
+
+
${escapeHtml(task.title || 'Без названия')}
+
${statusText}
+
+ +
+ ${escapeHtml(task.description || 'Нет описания')} +
+ +
+
+ + Создана: ${createdDate} +
+
+ + Срок: ${dueDate} +
+
+ + Автор: ${escapeHtml(task.creator_name || 'Н/Д')} +
+
+ + ${renderFiles(task.files, task.id)} + +
+ ${task.assignment_status !== 'completed' ? ` + + + ` : ''} + + +
+
+ `; + }).join(''); +} + +// Отрисовка файлов задачи +function renderFiles(files, taskId) { + if (!files || files.length === 0) { + return ''; + } + + return ` +
+
+ + Файлы (${files.length}) +
+ +
+ `; +} + +// Обновление статуса задачи +async function updateTaskStatus(taskId, status) { + if (!currentConnectionId) return; + + const comment = status === 'completed' ? + prompt('Введите комментарий к завершению (необязательно):') : + null; + + const button = document.querySelector(`#task-${taskId} .action-${status === 'in_progress' ? 'progress' : 'complete'}`); + if (button) { + button.disabled = true; + button.innerHTML = ' Обновление...'; + } + + try { + const response = await fetch(`/api/client/tasks/${taskId}/status?connection_id=${currentConnectionId}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ status, comment }) + }); + + const data = await response.json(); + + if (data.success) { + showAlert(`Статус задачи изменен на "${getStatusText(status)}"`, 'success'); + loadTasks(); + } else { + showAlert(data.error || 'Ошибка обновления статуса', 'danger'); + if (button) { + button.disabled = false; + button.innerHTML = status === 'in_progress' ? + ' В работу' : + ' Завершить'; + } + } + } catch (error) { + console.error('Ошибка обновления статуса:', error); + showAlert('Ошибка при обновлении статуса', 'danger'); + if (button) { + button.disabled = false; + button.innerHTML = status === 'in_progress' ? + ' В работу' : + ' Завершить'; + } + } +} + +// Открыть модальное окно загрузки файлов +function openUploadModal(taskId) { + currentTaskId = taskId; + document.getElementById('uploadModal').classList.add('active'); + selectedFiles = []; + document.getElementById('selectedFiles').style.display = 'none'; + document.getElementById('filesList').innerHTML = ''; + document.getElementById('uploadBtn').disabled = true; + document.getElementById('uploadProgress').style.display = 'none'; + document.getElementById('progressBar').style.width = '0%'; + document.getElementById('progressPercent').textContent = '0%'; +} + +// Закрыть модальное окно загрузки +function closeUploadModal() { + document.getElementById('uploadModal').classList.remove('active'); + currentTaskId = null; +} + +// Обработка выбора файлов +function handleFileSelect() { + const files = document.getElementById('fileInput').files; + selectedFiles = Array.from(files); + + if (selectedFiles.length > 0) { + const filesList = document.getElementById('filesList'); + filesList.innerHTML = selectedFiles.map((file, index) => ` +
+ + ${escapeHtml(file.name)} + ${formatFileSize(file.size)} + + + +
+ `).join(''); + + document.getElementById('selectedFiles').style.display = 'block'; + document.getElementById('uploadBtn').disabled = false; + } else { + document.getElementById('selectedFiles').style.display = 'none'; + document.getElementById('uploadBtn').disabled = true; + } +} + +// Удалить файл из списка +function removeFile(index) { + selectedFiles.splice(index, 1); + + if (selectedFiles.length > 0) { + const filesList = document.getElementById('filesList'); + filesList.innerHTML = selectedFiles.map((file, i) => ` +
+ + ${escapeHtml(file.name)} + ${formatFileSize(file.size)} + + + +
+ `).join(''); + } else { + document.getElementById('selectedFiles').style.display = 'none'; + document.getElementById('uploadBtn').disabled = true; + } +} + +// Загрузка файлов +async function uploadFiles() { + if (!currentConnectionId || !currentTaskId || selectedFiles.length === 0) return; + + const formData = new FormData(); + selectedFiles.forEach(file => { + formData.append('files', file); + }); + + const uploadBtn = document.getElementById('uploadBtn'); + const progressDiv = document.getElementById('uploadProgress'); + const progressBar = document.getElementById('progressBar'); + const progressPercent = document.getElementById('progressPercent'); + + uploadBtn.disabled = true; + progressDiv.style.display = 'block'; + + try { + const xhr = new XMLHttpRequest(); + + xhr.upload.addEventListener('progress', (e) => { + if (e.lengthComputable) { + const percent = Math.round((e.loaded / e.total) * 100); + progressBar.style.width = percent + '%'; + progressPercent.textContent = percent + '%'; + } + }); + + xhr.addEventListener('load', () => { + if (xhr.status === 200) { + const data = JSON.parse(xhr.responseText); + if (data.success) { + showAlert(`Успешно загружено ${selectedFiles.length} файлов`, 'success'); + closeUploadModal(); + loadTasks(); + } else { + showAlert(data.error || 'Ошибка загрузки файлов', 'danger'); + uploadBtn.disabled = false; + } + } else { + showAlert('Ошибка загрузки файлов', 'danger'); + uploadBtn.disabled = false; + } + }); + + xhr.addEventListener('error', () => { + showAlert('Ошибка сети при загрузке', 'danger'); + uploadBtn.disabled = false; + }); + + xhr.open('POST', `/api/client/tasks/${currentTaskId}/files?connection_id=${currentConnectionId}`, true); + xhr.send(formData); + + } catch (error) { + console.error('Ошибка загрузки файлов:', error); + showAlert('Ошибка при загрузке файлов', 'danger'); + uploadBtn.disabled = false; + } +} + +// Скачать файл +async function downloadFile(taskId, fileId, fileName) { + if (!currentConnectionId) return; + + try { + const response = await fetch(`/api/client/tasks/${taskId}/files/${fileId}/download?connection_id=${currentConnectionId}`); + + if (!response.ok) { + throw new Error('Ошибка скачивания'); + } + + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = fileName; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + a.remove(); + + } catch (error) { + console.error('Ошибка скачивания файла:', error); + showAlert('Ошибка при скачивании файла', 'danger'); + } +} + +// Загрузка списка подключений для синхронизации +async function loadTargetConnections() { + console.log('[Sync] Загрузка списка сохраненных подключений для синхронизации...'); + try { + const response = await fetch('/api/client/connections/list'); + const data = await response.json(); + + console.log('[Sync] Получен ответ от /api/client/connections/list:', data); + + if (data.success) { + const select = document.getElementById('savedTargetConnections'); + select.innerHTML = data.connections.map(conn => + `` + ).join(''); + console.log(`[Sync] Загружено ${data.connections.length} подключений.`); + } else { + console.warn('[Sync] Не удалось загрузить подключения:', data.error); + } + } catch (error) { + console.error('[Sync] Ошибка загрузки подключений:', error); + } +} + +// Открыть модальное окно синхронизации +function openSyncModal(taskId, taskTitle) { + console.log(`[Sync] Открытие модального окна синхронизации для задачи ID=${taskId}, название="${taskTitle}"`); + syncTaskId = taskId; + syncTaskTitle = taskTitle; + + document.getElementById('syncTaskTitle').textContent = taskTitle; + + loadTargetConnections(); + + document.getElementById('syncModal').classList.add('active'); +} + +// Закрыть модальное окно синхронизации +function closeSyncModal() { + console.log('[Sync] Закрытие модального окна синхронизации'); + 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('syncFiles').checked = true; + document.getElementById('syncProgress').style.display = 'none'; + document.getElementById('syncBtn').disabled = false; + document.getElementById('syncStatus').innerHTML = ''; +} + +// Переключение между сохраненным и новым сервисом +function toggleTargetServiceInput() { + const targetService = document.getElementById('targetService').value; + const isNew = targetService === 'new'; + document.getElementById('newServiceInputs').style.display = isNew ? 'block' : 'none'; + console.log(`[Sync] Переключение режима ввода: ${isNew ? 'новый сервис' : 'сохраненное подключение'}`); +} + +// Синхронизация задачи +async function syncTask() { + if (!syncTaskId) { + console.warn('[Sync] syncTaskId отсутствует, синхронизация невозможна'); + return; + } + + console.log(`[Sync] Начало синхронизации задачи ID=${syncTaskId}, название="${syncTaskTitle}"`); + + const targetService = document.getElementById('targetService').value; + const syncFiles = document.getElementById('syncFiles').checked; + + console.log('[Sync] Выбранный целевой сервис (ID или "new"):', targetService); + console.log('[Sync] Синхронизировать файлы:', syncFiles); + + if (!targetService) { + showAlert('Выберите целевой сервис', 'warning'); + console.warn('[Sync] Целевой сервис не выбран'); + return; + } + + let targetData = {}; + + if (targetService === 'new') { + const targetApiUrl = document.getElementById('targetApiUrl').value.trim(); + const targetApiKey = document.getElementById('targetApiKey').value.trim(); + + console.log('[Sync] Ввод нового сервиса: URL=', targetApiUrl); + + if (!targetApiUrl || !targetApiKey) { + showAlert('Заполните URL и API ключ целевого сервиса', 'warning'); + console.warn('[Sync] Не заполнены URL или ключ для нового сервиса'); + return; + } + + targetData = { + target_api_url: targetApiUrl, + target_api_key: targetApiKey + }; + } else { + targetData = { + target_connection_id: targetService + }; + console.log('[Sync] Используется сохраненное подключение с ID:', targetService); + } + + const requestData = { + ...targetData, + sync_files: syncFiles + }; + + console.log('[Sync] Формирование тела запроса:', requestData); + + document.getElementById('syncProgress').style.display = 'block'; + document.getElementById('syncBtn').disabled = true; + document.getElementById('syncStatus').innerHTML = 'Начинаем синхронизацию...'; + + try { + // ИЗМЕНЕНИЕ: убираем connection_id из URL, так как источник всегда локальный + const url = `/api/client/tasks/${syncTaskId}/sync`; + console.log('[Sync] Отправка POST запроса на', url); + + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(requestData) + }); + + console.log('[Sync] Статус ответа:', response.status, response.statusText); + + const data = await response.json(); + console.log('[Sync] Получен ответ от сервера:', data); + + if (data.success) { + console.log('[Sync] Синхронизация успешно запущена, ожидаем прогресс...'); + let progress = 0; + const interval = setInterval(() => { + progress += 10; + document.getElementById('syncProgressBar').style.width = progress + '%'; + document.getElementById('syncPercent').textContent = progress + '%'; + + if (progress >= 100) { + clearInterval(interval); + + let statusText = `✅ Задача синхронизирована!`; + + if (data.data.sync_type === 'created') { + statusText += `
📋 Создана новая задача в целевой системе. ID: ${data.data.target_task_id}`; + console.log('[Sync] Задача создана в целевой системе, новый ID:', data.data.target_task_id); + } else if (data.data.sync_type === 'updated') { + statusText += `
🔄 Обновлена существующая задача в целевой системе. ID: ${data.data.target_task_id}`; + console.log('[Sync] Задача обновлена в целевой системе, ID:', data.data.target_task_id); + } + + statusText += `
👥 Исполнители: не копируются`; + + 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} ошибок`; + console.log('[Sync] Синхронизация файлов: успешно=', successCount, 'ошибок=', failCount); + + if (failCount > 0) { + const errors = data.data.synced_files + .filter(f => !f.success) + .map(f => f.original_name) + .join(', '); + statusText += `
Ошибки: ${errors}`; + console.warn('[Sync] Ошибки при синхронизации файлов:', errors); + } + } else { + console.log('[Sync] Файлы не синхронизировались (нет файлов или sync_files=false)'); + } + + if (data.data.warnings && data.data.warnings.length > 0) { + statusText += `
⚠️ ${data.data.warnings.join('; ')}`; + console.warn('[Sync] Предупреждения:', data.data.warnings); + } + + document.getElementById('syncStatus').innerHTML = statusText; + console.log('[Sync] Процесс синхронизации завершен'); + + setTimeout(() => { + closeSyncModal(); + showAlert(`Задача синхронизирована с ${data.data.target_service}`, 'success'); + loadTasks(); + console.log('[Sync] Модальное окно закрыто, задачи перезагружены'); + }, 3000); + } + }, 200); + } else { + console.error('[Sync] Ошибка синхронизации:', data.error); + showAlert(data.error || 'Ошибка синхронизации задачи', 'danger'); + document.getElementById('syncProgress').style.display = 'none'; + document.getElementById('syncBtn').disabled = false; + document.getElementById('syncStatus').innerHTML = ''; + } + } catch (error) { + console.error('[Sync] Ошибка при выполнении запроса синхронизации:', error); + showAlert('Ошибка при синхронизации задачи', 'danger'); + document.getElementById('syncProgress').style.display = 'none'; + document.getElementById('syncBtn').disabled = false; + document.getElementById('syncStatus').innerHTML = ''; + } +} + +// Смена страницы +function changePage(delta) { + const newPage = currentPage + delta; + if (newPage >= 0 && newPage * pageSize < totalTasks) { + currentPage = newPage; + loadTasks(); + } +} + +// Обновление пагинации +function updatePagination() { + const totalPages = Math.ceil(totalTasks / pageSize); + + if (totalPages > 1) { + document.getElementById('pagination').style.display = 'flex'; + document.getElementById('pageInfo').textContent = `Страница ${currentPage + 1} из ${totalPages}`; + document.getElementById('prevPage').disabled = currentPage === 0; + document.getElementById('nextPage').disabled = currentPage >= totalPages - 1; + } else { + document.getElementById('pagination').style.display = 'none'; + } +} + +// Показать уведомление +function showAlert(message, type) { + const alert = document.getElementById('alert'); + alert.className = `alert alert-${type} show`; + alert.innerHTML = message; + + setTimeout(() => { + alert.classList.remove('show'); + }, 5000); +} + +// Вспомогательные функции +function getStatusText(status) { + const statusMap = { + 'assigned': 'Назначена', + 'in_progress': 'В работе', + 'completed': 'Выполнена', + 'overdue': 'Просрочена', + 'rework': 'На доработке' + }; + return statusMap[status] || status || 'Неизвестно'; +} + +function formatFileSize(bytes) { + if (bytes === 0) return '0 Б'; + const k = 1024; + const sizes = ['Б', 'КБ', 'МБ', 'ГБ']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; +} + +function escapeHtml(unsafe) { + if (!unsafe) return ''; + return unsafe + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + +// Инициализация +document.addEventListener('DOMContentLoaded', () => { + checkAuth(); + loadSavedConnections(); +}); \ No newline at end of file