api-client

This commit is contained in:
2026-03-09 14:20:28 +05:00
parent 08df44734c
commit 2a492aaa7c
4 changed files with 253 additions and 513 deletions

View File

@@ -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 || []);
});
});
}
};

51
auth.js
View File

@@ -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) {

View File

@@ -988,7 +988,34 @@
<!-- Уведомления -->
<div id="alert" class="alert"></div>
<!-- Модальное окно импорта задачи -->
<div class="modal" id="import-task-modal">
<div class="modal-content" style="max-width: 600px;">
<div class="modal-header">
<h3>Копирование задачи в локальную CRM</h3>
<span class="modal-close" onclick="closeImportModal()">&times;</span>
</div>
<div class="modal-body">
<p><strong>Задача:</strong> <span id="import-task-title"></span></p>
<p><strong>Описание:</strong> <span id="import-task-description"></span></p>
<div class="form-group">
<label for="import-due-date">Дата выполнения:</label>
<input type="datetime-local" id="import-due-date" class="form-control" required>
</div>
<div class="form-group">
<label>Выберите исполнителей (локальные пользователи):</label>
<div class="users-checklist" id="import-users-checklist" style="max-height: 200px; overflow-y: auto; border: 1px solid #ddd; padding: 10px;"></div>
</div>
<div class="modal-footer" style="margin-top: 20px;">
<button class="btn btn-secondary" onclick="closeImportModal()">Отмена</button>
<button class="btn btn-success" onclick="confirmImport()">✅ Импортировать</button>
</div>
</div>
</div>
</div>
<script src="client.js"></script>
</body>
</html>

View File

@@ -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) {
<button class="task-action-btn action-upload" onclick="openUploadModal('${task.id}')">
<i class="fas fa-upload"></i> Файлы
</button>
<button class="task-action-btn action-sync" onclick="openSyncModal('${task.id}', '${escapeHtml(task.title)}')">
<i class="fas fa-sync-alt"></i> Синхронизировать
<button class="task-action-btn action-import" onclick="importTask('${task.id}')">
<i class="fas fa-download"></i> Копировать локально
</button>
</div>
</div>
@@ -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 = `
<label>
<input type="checkbox" class="import-user-checkbox" value="${user.id}">
${escapeHtml(user.name)} (${escapeHtml(user.login)})
</label>
`;
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 =>
`<option value="${conn.id}">${conn.name} (${conn.url})</option>`
).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 += `<br>📋 Создана новая задача в целевой системе. ID: ${data.data.target_task_id}`;
console.log('[Sync] Задача создана в целевой системе, новый ID:', data.data.target_task_id);
} else if (data.data.sync_type === 'updated') {
statusText += `<br>🔄 Обновлена существующая задача в целевой системе. ID: ${data.data.target_task_id}`;
console.log('[Sync] Задача обновлена в целевой системе, ID:', data.data.target_task_id);
}
statusText += `<br>👥 Исполнители: не копируются`;
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} ошибок`;
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 += `<br><small style="color: #e74c3c;">Ошибки: ${errors}</small>`;
console.warn('[Sync] Ошибки при синхронизации файлов:', errors);
}
} else {
console.log('[Sync] Файлы не синхронизировались (нет файлов или sync_files=false)');
}
if (data.data.warnings && data.data.warnings.length > 0) {
statusText += `<br><small style="color: #f39c12;">⚠️ ${data.data.warnings.join('; ')}</small>`;
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, "&#039;");
}
// Инициализация
// ==================== ИНИЦИАЛИЗАЦИЯ ====================
document.addEventListener('DOMContentLoaded', () => {
checkAuth();
loadSavedConnections();