api-client

This commit is contained in:
2026-03-08 14:50:55 +05:00
parent ad7868ff1c
commit 08df44734c
3 changed files with 862 additions and 823 deletions

View File

@@ -989,757 +989,6 @@
<!-- Уведомления -->
<div id="alert" class="alert"></div>
<script>
// Глобальные переменные
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 => `
<div class="connection-item ${currentConnectionId === conn.id ? 'active' : ''}"
onclick="selectConnection('${conn.id}')">
<i class="fas fa-database"></i>
<span>${conn.name}</span>
<span class="remove-conn" onclick="event.stopPropagation(); removeConnection('${conn.id}')">
<i class="fas fa-times"></i>
</span>
</div>
`).join('');
} else {
connectionsList.innerHTML = '<div class="connection-item">Нет сохраненных подключений</div>';
}
} catch (error) {
console.error('Ошибка загрузки подключений:', error);
document.getElementById('connectionsList').innerHTML =
'<div class="connection-item">Ошибка загрузки</div>';
}
}
// Выбор сохраненного подключения
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 =
'<div class="loading"><i class="fas fa-plug"></i><p>Подключитесь к серверу</p></div>';
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 = '<i class="fas fa-spinner fa-spin"></i> Подключение...';
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 = `
<i class="fas fa-server"></i>
<strong>Сервер:</strong> ${data.connection.url}<br>
<strong>Пользователь:</strong> ${data.server_info.user}<br>
<strong>Задач на сервере:</strong> ${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 = '<i class="fas fa-link"></i> Подключиться';
}
}
// Загрузка задач
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 = '<div class="loading"><i class="fas fa-circle-notch fa-spin"></i><p>Загрузка задач...</p></div>';
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 = '<div class="no-tasks"><i class="fas fa-folder-open"></i><p>Задачи не найдены</p></div>';
} else {
renderTasks(currentTasks);
}
updatePagination();
} else {
tasksContainer.innerHTML = `<div class="no-tasks"><i class="fas fa-exclamation-triangle"></i><p>Ошибка: ${data.error || 'Неизвестная ошибка'}</p></div>`;
}
} catch (error) {
console.error('Ошибка загрузки задач:', error);
tasksContainer.innerHTML = '<div class="no-tasks"><i class="fas fa-exclamation-triangle"></i><p>Ошибка загрузки задач</p></div>';
}
}
// Отрисовка задач
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 `
<div class="task-card" id="task-${task.id}">
<div class="task-header">
<div class="task-title">${escapeHtml(task.title || 'Без названия')}</div>
<div class="task-status ${statusClass}">${statusText}</div>
</div>
<div class="task-description">
${escapeHtml(task.description || 'Нет описания')}
</div>
<div class="task-meta">
<div class="task-meta-item">
<i class="fas fa-calendar-alt"></i>
<span>Создана: ${createdDate}</span>
</div>
<div class="task-meta-item">
<i class="fas fa-clock"></i>
<span>Срок: ${dueDate}</span>
</div>
<div class="task-meta-item">
<i class="fas fa-user"></i>
<span>Автор: ${escapeHtml(task.creator_name || 'Н/Д')}</span>
</div>
</div>
${renderFiles(task.files, task.id)}
<div class="task-actions">
${task.assignment_status !== 'completed' ? `
<button class="task-action-btn action-progress" onclick="updateTaskStatus('${task.id}', 'in_progress')">
<i class="fas fa-play"></i> В работу
</button>
<button class="task-action-btn action-complete" onclick="updateTaskStatus('${task.id}', 'completed')">
<i class="fas fa-check"></i> Завершить
</button>
` : ''}
<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>
</div>
</div>
`;
}).join('');
}
// Отрисовка файлов задачи
function renderFiles(files, taskId) {
if (!files || files.length === 0) {
return '';
}
return `
<div class="task-files">
<div class="files-title">
<i class="fas fa-paperclip"></i>
Файлы (${files.length})
</div>
<ul class="files-list">
${files.map(file => `
<li class="file-item">
<i class="fas fa-file file-icon"></i>
<span class="file-name" title="${escapeHtml(file.filename || file.original_name)}">
${escapeHtml(file.filename || file.original_name)}
</span>
<span class="file-size">${formatFileSize(file.file_size)}</span>
<a href="#" class="file-download" onclick="downloadFile('${taskId}', '${file.id}', '${escapeHtml(file.filename || file.original_name)}'); return false;">
<i class="fas fa-download"></i>
</a>
</li>
`).join('')}
</ul>
</div>
`;
}
// Обновление статуса задачи
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 = '<i class="fas fa-spinner fa-spin"></i> Обновление...';
}
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' ?
'<i class="fas fa-play"></i> В работу' :
'<i class="fas fa-check"></i> Завершить';
}
}
} catch (error) {
console.error('Ошибка обновления статуса:', error);
showAlert('Ошибка при обновлении статуса', 'danger');
if (button) {
button.disabled = false;
button.innerHTML = status === 'in_progress' ?
'<i class="fas fa-play"></i> В работу' :
'<i class="fas fa-check"></i> Завершить';
}
}
}
// Открыть модальное окно загрузки файлов
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) => `
<div class="selected-file">
<i class="fas fa-file"></i>
<span class="file-name">${escapeHtml(file.name)}</span>
<span class="file-size">${formatFileSize(file.size)}</span>
<span class="remove-file" onclick="removeFile(${index})">
<i class="fas fa-times"></i>
</span>
</div>
`).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) => `
<div class="selected-file">
<i class="fas fa-file"></i>
<span class="file-name">${escapeHtml(file.name)}</span>
<span class="file-size">${formatFileSize(file.size)}</span>
<span class="remove-file" onclick="removeFile(${i})">
<i class="fas fa-times"></i>
</span>
</div>
`).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() {
try {
const response = await fetch('/api/client/connections/list');
const data = await response.json();
if (data.success) {
const select = document.getElementById('savedTargetConnections');
select.innerHTML = data.connections.map(conn =>
`<option value="${conn.id}">${conn.name} (${conn.url})</option>`
).join('');
}
} catch (error) {
console.error('Ошибка загрузки подключений:', error);
}
}
// Открыть модальное окно синхронизации
function openSyncModal(taskId, taskTitle) {
syncTaskId = taskId;
syncTaskTitle = taskTitle;
document.getElementById('syncTaskTitle').textContent = taskTitle;
loadTargetConnections();
document.getElementById('syncModal').classList.add('active');
}
// Закрыть модальное окно синхронизации
function closeSyncModal() {
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;
document.getElementById('newServiceInputs').style.display =
targetService === 'new' ? 'block' : 'none';
}
// Синхронизация задачи
async function syncTask() {
if (!syncTaskId) return;
const targetService = document.getElementById('targetService').value;
const syncFiles = document.getElementById('syncFiles').checked;
if (!targetService) {
showAlert('Выберите целевой сервис', 'warning');
return;
}
let targetData = {};
if (targetService === 'new') {
const targetApiUrl = document.getElementById('targetApiUrl').value.trim();
const targetApiKey = document.getElementById('targetApiKey').value.trim();
if (!targetApiUrl || !targetApiKey) {
showAlert('Заполните URL и API ключ целевого сервиса', 'warning');
return;
}
targetData = {
target_api_url: targetApiUrl,
target_api_key: targetApiKey
};
} else {
targetData = {
target_connection_id: targetService
};
}
const requestData = {
...targetData,
sync_files: syncFiles
};
document.getElementById('syncProgress').style.display = 'block';
document.getElementById('syncBtn').disabled = true;
document.getElementById('syncStatus').innerHTML = 'Начинаем синхронизацию...';
try {
const response = await fetch(`/api/client/tasks/${syncTaskId}/sync?connection_id=${currentConnectionId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requestData)
});
const data = await response.json();
if (data.success) {
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}`;
} else if (data.data.sync_type === 'updated') {
statusText += `<br>🔄 Обновлена существующая задача в целевой системе. ID: ${data.data.target_task_id}`;
}
statusText += `<br>👥 Исполнители: сохранены как в исходной задаче`;
if (data.data.assignees && data.data.assignees.length > 0) {
statusText += `<br>👤 Количество исполнителей: ${data.data.assignees.length}`;
}
if (data.data.synced_files && data.data.synced_files.length > 0) {
const successCount = data.data.synced_files.filter(f => f.success).length;
const failCount = data.data.synced_files.filter(f => !f.success).length;
statusText += `<br>📁 Файлы: ${successCount} синхронизировано, ${failCount} ошибок`;
if (failCount > 0) {
const errors = data.data.synced_files
.filter(f => !f.success)
.map(f => f.original_name)
.join(', ');
statusText += `<br><small style="color: #e74c3c;">Ошибки: ${errors}</small>`;
}
}
if (data.data.warnings && data.data.warnings.length > 0) {
statusText += `<br><small style="color: #f39c12;">⚠️ ${data.data.warnings.join('; ')}</small>`;
}
document.getElementById('syncStatus').innerHTML = statusText;
setTimeout(() => {
closeSyncModal();
showAlert(`Задача синхронизирована с ${data.data.target_service}`, 'success');
loadTasks();
}, 3000);
}
}, 200);
} else {
showAlert(data.error || 'Ошибка синхронизации задачи', 'danger');
document.getElementById('syncProgress').style.display = 'none';
document.getElementById('syncBtn').disabled = false;
document.getElementById('syncStatus').innerHTML = '';
}
} catch (error) {
console.error('Ошибка синхронизации:', 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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
// Инициализация
document.addEventListener('DOMContentLoaded', () => {
checkAuth();
loadSavedConnections();
});
</script>
<script src="client.js"></script>
</body>
</html>