From 2a492aaa7cfb9eef1f9a78bb3e1045ab9508f7c4 Mon Sep 17 00:00:00 2001 From: kalugin66 Date: Mon, 9 Mar 2026 14:20:28 +0500 Subject: [PATCH] api-client --- api-client.js | 257 +-------------------------- auth.js | 51 +++--- public/client.html | 29 ++- public/client.js | 429 +++++++++++++++++++++------------------------ 4 files changed, 253 insertions(+), 513 deletions(-) diff --git a/api-client.js b/api-client.js index bedec03..0748025 100644 --- a/api-client.js +++ b/api-client.js @@ -444,245 +444,6 @@ module.exports = function(app, db, upload) { } }); - /** - * POST /api/client/tasks/:taskId/sync - Синхронизировать ЛОКАЛЬНУЮ задачу с целевым сервисом - */ - 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, - sync_files = true - } = req.body; - const userId = req.session.user.id; - - // Определяем целевой сервис - 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 ключ)' - }); - } - - const baseUrl = targetUrl.replace(/\/$/, ''); - - try { - // 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 (!localTask) { - return res.status(404).json({ error: 'Локальная задача не найдена' }); - } - - // Преобразуем в формат, аналогичный внешнему 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) - }; - - // 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; - } - } catch (checkError) { - // Задача не найдена - это нормально, будем создавать новую - console.log('Задача не найдена в целевой системе, будет создана новая'); - } - - let result; - const syncedFiles = []; - const warnings = []; - - if (existingTask) { - // 3. Обновляем существующую задачу - const updateData = { - title: sourceTask.title, - description: sourceTask.description, - due_date: sourceTask.due_date, - task_type: sourceTask.task_type - }; - - 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 - }; - - 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 { - // Читаем файл с диска - 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', fileBuffer, { - filename: fileName, - contentType: file.file_type || 'application/octet-stream' - }); - - const uploadResponse = await axios.post( - `${baseUrl}/api/external/tasks/${result.taskId}/files`, - formData, - { - headers: { - ...formData.getHeaders(), - 'X-API-Key': targetKey - }, - timeout: 60000, - maxContentLength: Infinity, - maxBodyLength: Infinity - } - ); - - syncedFiles.push({ - original_name: fileName, - success: uploadResponse.data && uploadResponse.data.success - }); - } catch (fileError) { - console.error(`❌ Ошибка синхронизации файла:`, fileError.message); - syncedFiles.push({ - original_name: file.original_name || 'unknown', - success: false, - error: fileError.message - }); - warnings.push(`Не удалось синхронизировать файл: ${file.original_name || file.filename}`); - } - } - } - - // Логируем действие - const { logActivity } = require('./database'); - if (logActivity) { - logActivity(0, userId, 'API_CLIENT_SYNC_TASK', - `Локальная задача ${taskId} ${existingTask ? 'обновлена' : 'создана'} в ${baseUrl}. Новый ID: ${result.taskId}`); - } - - res.json({ - success: true, - message: `Задача успешно ${existingTask ? 'обновлена' : 'создана'} в целевой системе`, - data: { - sync_type: result.action, - original_task_id: taskId, - target_task_id: result.taskId, - target_service: baseUrl, - synced_files: syncedFiles, - 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 ключ для целевого сервиса'; - 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 - Загрузить файлы в задачу */ @@ -912,20 +673,4 @@ module.exports = function(app, db, upload) { app.use(router); console.log('✅ API клиент для внешних сервисов подключен'); -}; - -// Вспомогательная функция для получения файлов локальной задачи -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 +}; \ No newline at end of file diff --git a/auth.js b/auth.js index 4433746..7f41a58 100644 --- a/auth.js +++ b/auth.js @@ -22,29 +22,32 @@ class AuthService { try { // Создаем пользователей из .env - const users = [ - { - login: process.env.USER_1_LOGIN, - password: process.env.USER_1_PASSWORD, - name: process.env.USER_1_NAME, - email: process.env.USER_1_EMAIL, - auth_type: 'local' - }, - { - login: process.env.USER_2_LOGIN, - password: process.env.USER_2_PASSWORD, - name: process.env.USER_2_NAME, - email: process.env.USER_2_EMAIL, - auth_type: 'local' - }, - { - login: process.env.USER_3_LOGIN, - password: process.env.USER_3_PASSWORD, - name: process.env.USER_3_NAME, - email: process.env.USER_3_EMAIL, - auth_type: 'local' - } - ]; + const users = [ + { + login: process.env.USER_1_LOGIN, + password: process.env.USER_1_PASSWORD, + name: process.env.USER_1_NAME, + email: process.env.USER_1_EMAIL, + role: process.env.USER_1_ROLE || 'teacher', + auth_type: 'local' + }, + { + login: process.env.USER_2_LOGIN, + password: process.env.USER_2_PASSWORD, + name: process.env.USER_2_NAME, + email: process.env.USER_2_EMAIL, + role: process.env.USER_2_ROLE || 'teacher', + auth_type: 'local' + }, + { + login: process.env.USER_3_LOGIN, + password: process.env.USER_3_PASSWORD, + name: process.env.USER_3_NAME, + email: process.env.USER_3_EMAIL, + role: process.env.USER_3_ROLE || 'teacher', + auth_type: 'local' + } + ]; for (const userData of users) { if (userData.login && userData.password) { @@ -79,7 +82,7 @@ class AuthService { hashedPassword, userData.name, userData.email, - 'teacher', + userData.role, userData.auth_type || 'local' ], function(err) { diff --git a/public/client.html b/public/client.html index d199a2e..9e96d0b 100644 --- a/public/client.html +++ b/public/client.html @@ -988,7 +988,34 @@
- + + \ No newline at end of file diff --git a/public/client.js b/public/client.js index 30f71b9..f79d4b5 100644 --- a/public/client.js +++ b/public/client.js @@ -8,10 +8,11 @@ let totalTasks = 0; let pageSize = 50; let currentTaskId = null; let selectedFiles = []; -let syncTaskId = null; -let syncTaskTitle = ''; +let importTaskData = null; // данные импортируемой задачи +let currentUserId = null; // ID текущего пользователя (для фильтрации) + +// ==================== АВТОРИЗАЦИЯ ==================== -// Проверка авторизации async function checkAuth() { try { const response = await fetch('/api/user'); @@ -19,6 +20,7 @@ async function checkAuth() { if (data.user) { document.getElementById('userName').textContent = data.user.name || data.user.login; + currentUserId = data.user.id; // сохраняем ID } else { window.location.href = '/'; } @@ -28,7 +30,6 @@ async function checkAuth() { } } -// Выход async function logout() { try { await fetch('/api/logout', { method: 'POST' }); @@ -38,7 +39,8 @@ async function logout() { } } -// Загрузка сохраненных подключений +// ==================== УПРАВЛЕНИЕ ПОДКЛЮЧЕНИЯМИ ==================== + async function loadSavedConnections() { try { const response = await fetch('/api/client/connections'); @@ -67,7 +69,6 @@ async function loadSavedConnections() { } } -// Выбор сохраненного подключения function selectConnection(connectionId) { currentConnectionId = connectionId; @@ -82,7 +83,6 @@ function selectConnection(connectionId) { loadTasks(); } -// Удаление подключения async function removeConnection(connectionId) { if (!confirm('Удалить это подключение?')) return; @@ -112,7 +112,6 @@ async function removeConnection(connectionId) { } } -// Подключение к серверу async function connect() { const apiUrl = document.getElementById('apiUrl').value.trim(); const apiKey = document.getElementById('apiKey').value.trim(); @@ -172,7 +171,8 @@ async function connect() { } } -// Загрузка задач +// ==================== ЗАГРУЗКА И ОТОБРАЖЕНИЕ ЗАДАЧ ==================== + async function loadTasks() { if (!currentConnectionId) { showAlert('Сначала выберите подключение', 'warning'); @@ -218,7 +218,6 @@ async function loadTasks() { } } -// Отрисовка задач function renderTasks(tasks) { const container = document.getElementById('tasksContainer'); @@ -269,8 +268,8 @@ function renderTasks(tasks) { - @@ -278,7 +277,6 @@ function renderTasks(tasks) { }).join(''); } -// Отрисовка файлов задачи function renderFiles(files, taskId) { if (!files || files.length === 0) { return ''; @@ -308,7 +306,6 @@ function renderFiles(files, taskId) { `; } -// Обновление статуса задачи async function updateTaskStatus(taskId, status) { if (!currentConnectionId) return; @@ -357,7 +354,185 @@ async function updateTaskStatus(taskId, status) { } } -// Открыть модальное окно загрузки файлов +// ==================== ИМПОРТ ЗАДАЧИ В ЛОКАЛЬНУЮ CRM ==================== + +/** + * Загружает задачу из внешнего сервиса и открывает модальное окно для импорта + */ +async function importTask(taskId) { + if (!currentConnectionId) { + showAlert('Сначала выберите подключение', 'warning'); + return; + } + + console.log(`[Import] Загрузка задачи ${taskId} из внешнего сервиса...`); + + try { + // 1. Получаем задачу из внешнего API + const response = await fetch(`/api/client/tasks/${taskId}?connection_id=${currentConnectionId}`); + const data = await response.json(); + + if (!data.success || !data.task) { + showAlert('Не удалось получить задачу из внешнего сервиса', 'danger'); + return; + } + + const task = data.task; + importTaskData = { + title: task.title, + description: task.description || '', + due_date: task.due_date || '', + task_type: task.task_type || 'regular', + files: task.files || [] + }; + + // 2. Загружаем список локальных пользователей (если ещё не загружен) + await loadLocalUsers(); + + // 3. Открываем модальное окно импорта + openImportModal(task); + + } catch (error) { + console.error('Ошибка импорта задачи:', error); + showAlert('Ошибка при загрузке задачи', 'danger'); + } +} + +/** + * Загружает список локальных пользователей (используется в модальном окне) + */ +let localUsers = []; +async function loadLocalUsers() { + if (localUsers.length > 0) return; + try { + const response = await fetch('/api/users'); + if (response.ok) { + localUsers = await response.json(); + } + } catch (error) { + console.error('Ошибка загрузки локальных пользователей:', error); + } +} + +/** + * Открывает модальное окно импорта задачи + */ +function openImportModal(task) { + const modal = document.getElementById('import-task-modal'); + if (!modal) { + console.error('Модальное окно import-task-modal не найдено'); + return; + } + + // Заполняем данные задачи + document.getElementById('import-task-title').textContent = task.title; + document.getElementById('import-task-description').textContent = task.description || 'Нет описания'; + + // Устанавливаем дату выполнения + const dueDateInput = document.getElementById('import-due-date'); + if (task.due_date) { + const date = new Date(task.due_date); + dueDateInput.value = date.toISOString().slice(0, 16); + } else { + const defaultDate = new Date(); + defaultDate.setDate(defaultDate.getDate() + 7); + dueDateInput.value = defaultDate.toISOString().slice(0, 16); + } + + // Очищаем и заполняем список исполнителей + const container = document.getElementById('import-users-checklist'); + container.innerHTML = ''; + + // Фильтруем текущего пользователя (нельзя назначить самому себе) + const filteredUsers = localUsers.filter(u => u.id !== currentUserId); + + filteredUsers.forEach(user => { + const div = document.createElement('div'); + div.className = 'checkbox-item'; + div.innerHTML = ` + + `; + container.appendChild(div); + }); + + // Показываем модальное окно + modal.style.display = 'block'; +} + +/** + * Закрывает модальное окно импорта + */ +function closeImportModal() { + const modal = document.getElementById('import-task-modal'); + if (modal) { + modal.style.display = 'none'; + } + importTaskData = null; +} + +/** + * Выполняет импорт задачи: создаёт локальную задачу с выбранными исполнителями + */ +async function confirmImport() { + if (!importTaskData) { + showAlert('Нет данных для импорта', 'danger'); + return; + } + + const dueDate = document.getElementById('import-due-date').value; + if (!dueDate) { + showAlert('Укажите дату выполнения', 'warning'); + return; + } + + // Собираем выбранных исполнителей + const checkboxes = document.querySelectorAll('.import-user-checkbox:checked'); + const assignedUsers = Array.from(checkboxes).map(cb => parseInt(cb.value)); + + + if (assignedUsers.length === 0) { + showAlert('Выберите хотя бы одного исполнителя', 'warning'); + return; + } + + // Формируем FormData для отправки + const formData = new FormData(); + formData.append('title', importTaskData.title); + formData.append('description', importTaskData.description); + formData.append('dueDate', dueDate); + formData.append('taskType', importTaskData.task_type); + formData.append('assignedUsers', JSON.stringify(assignedUsers)); + console.log('FormData assignedUsers:', assignedUsers); + + // Если есть файлы – добавляем их (но тут сложность: файлы нужно скачать из внешнего сервиса и загрузить) + // Пока пропустим файлы для простоты, можно добавить позже. + + try { + const response = await fetch('/api/tasks', { + method: 'POST', + body: formData + }); + + const result = await response.json(); + + if (result.success) { + showAlert(`Задача успешно импортирована! Новый ID: ${result.taskId}`, 'success'); + closeImportModal(); + // Можно перезагрузить список локальных задач, если нужно + } else { + showAlert('Ошибка создания задачи: ' + (result.error || 'Неизвестная ошибка'), 'danger'); + } + } catch (error) { + console.error('Ошибка импорта:', error); + showAlert('Ошибка при создании задачи', 'danger'); + } +} + +// ==================== РАБОТА С ФАЙЛАМИ ==================== + function openUploadModal(taskId) { currentTaskId = taskId; document.getElementById('uploadModal').classList.add('active'); @@ -370,13 +545,11 @@ function openUploadModal(taskId) { 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); @@ -402,7 +575,6 @@ function handleFileSelect() { } } -// Удалить файл из списка function removeFile(index) { selectedFiles.splice(index, 1); @@ -424,7 +596,6 @@ function removeFile(index) { } } -// Загрузка файлов async function uploadFiles() { if (!currentConnectionId || !currentTaskId || selectedFiles.length === 0) return; @@ -484,7 +655,6 @@ async function uploadFiles() { } } -// Скачать файл async function downloadFile(taskId, fileId, fileName) { if (!currentConnectionId) return; @@ -511,215 +681,8 @@ async function downloadFile(taskId, fileId, fileName) { } } -// Загрузка списка подключений для синхронизации -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) { @@ -728,7 +691,6 @@ function changePage(delta) { } } -// Обновление пагинации function updatePagination() { const totalPages = Math.ceil(totalTasks / pageSize); @@ -742,7 +704,8 @@ function updatePagination() { } } -// Показать уведомление +// ==================== УВЕДОМЛЕНИЯ ==================== + function showAlert(message, type) { const alert = document.getElementById('alert'); alert.className = `alert alert-${type} show`; @@ -753,7 +716,8 @@ function showAlert(message, type) { }, 5000); } -// Вспомогательные функции +// ==================== ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ==================== + function getStatusText(status) { const statusMap = { 'assigned': 'Назначена', @@ -783,7 +747,8 @@ function escapeHtml(unsafe) { .replace(/'/g, "'"); } -// Инициализация +// ==================== ИНИЦИАЛИЗАЦИЯ ==================== + document.addEventListener('DOMContentLoaded', () => { checkAuth(); loadSavedConnections();