api-client
This commit is contained in:
255
api-client.js
255
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 - Загрузить файлы в задачу
|
||||
*/
|
||||
@@ -913,19 +674,3 @@ module.exports = function(app, db, upload) {
|
||||
|
||||
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 || []);
|
||||
});
|
||||
});
|
||||
}
|
||||
5
auth.js
5
auth.js
@@ -28,6 +28,7 @@ class AuthService {
|
||||
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'
|
||||
},
|
||||
{
|
||||
@@ -35,6 +36,7 @@ class AuthService {
|
||||
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'
|
||||
},
|
||||
{
|
||||
@@ -42,6 +44,7 @@ class AuthService {
|
||||
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'
|
||||
}
|
||||
];
|
||||
@@ -79,7 +82,7 @@ class AuthService {
|
||||
hashedPassword,
|
||||
userData.name,
|
||||
userData.email,
|
||||
'teacher',
|
||||
userData.role,
|
||||
userData.auth_type || 'local'
|
||||
],
|
||||
function(err) {
|
||||
|
||||
@@ -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()">×</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>
|
||||
429
public/client.js
429
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) {
|
||||
<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, "'");
|
||||
}
|
||||
|
||||
// Инициализация
|
||||
// ==================== ИНИЦИАЛИЗАЦИЯ ====================
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
checkAuth();
|
||||
loadSavedConnections();
|
||||
|
||||
Reference in New Issue
Block a user