755 lines
29 KiB
JavaScript
755 lines
29 KiB
JavaScript
// client.js – клиентская логика для работы с внешним API
|
||
|
||
// Глобальные переменные
|
||
let currentConnectionId = null;
|
||
let currentTasks = [];
|
||
let currentPage = 0;
|
||
let totalTasks = 0;
|
||
let pageSize = 50;
|
||
let currentTaskId = null;
|
||
let selectedFiles = [];
|
||
let importTaskData = null; // данные импортируемой задачи
|
||
let currentUserId = null; // ID текущего пользователя (для фильтрации)
|
||
|
||
// ==================== АВТОРИЗАЦИЯ ====================
|
||
|
||
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;
|
||
currentUserId = data.user.id; // сохраняем ID
|
||
} 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-import" onclick="importTask('${task.id}')">
|
||
<i class="fas fa-download"></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> Завершить';
|
||
}
|
||
}
|
||
}
|
||
|
||
// ==================== ИМПОРТ ЗАДАЧИ В ЛОКАЛЬНУЮ 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');
|
||
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');
|
||
}
|
||
}
|
||
|
||
// ==================== ПАГИНАЦИЯ ====================
|
||
|
||
function changePage(delta) {
|
||
const newPage = currentPage + delta;
|
||
if (newPage >= 0 && newPage * pageSize < totalTasks) {
|
||
currentPage = newPage;
|
||
loadTasks();
|
||
}
|
||
}
|
||
|
||
function updatePagination() {
|
||
const totalPages = Math.ceil(totalTasks / pageSize);
|
||
|
||
if (totalPages > 1) {
|
||
document.getElementById('pagination').style.display = 'flex';
|
||
document.getElementById('pageInfo').textContent = `Страница ${currentPage + 1} из ${totalPages}`;
|
||
document.getElementById('prevPage').disabled = currentPage === 0;
|
||
document.getElementById('nextPage').disabled = currentPage >= totalPages - 1;
|
||
} else {
|
||
document.getElementById('pagination').style.display = 'none';
|
||
}
|
||
}
|
||
|
||
// ==================== УВЕДОМЛЕНИЯ ====================
|
||
|
||
function showAlert(message, type) {
|
||
const alert = document.getElementById('alert');
|
||
alert.className = `alert alert-${type} show`;
|
||
alert.innerHTML = message;
|
||
|
||
setTimeout(() => {
|
||
alert.classList.remove('show');
|
||
}, 5000);
|
||
}
|
||
|
||
// ==================== ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ====================
|
||
|
||
function getStatusText(status) {
|
||
const statusMap = {
|
||
'assigned': 'Назначена',
|
||
'in_progress': 'В работе',
|
||
'completed': 'Выполнена',
|
||
'overdue': 'Просрочена',
|
||
'rework': 'На доработке'
|
||
};
|
||
return statusMap[status] || status || 'Неизвестно';
|
||
}
|
||
|
||
function formatFileSize(bytes) {
|
||
if (bytes === 0) return '0 Б';
|
||
const k = 1024;
|
||
const sizes = ['Б', 'КБ', 'МБ', 'ГБ'];
|
||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||
}
|
||
|
||
function escapeHtml(unsafe) {
|
||
if (!unsafe) return '';
|
||
return unsafe
|
||
.replace(/&/g, "&")
|
||
.replace(/</g, "<")
|
||
.replace(/>/g, ">")
|
||
.replace(/"/g, """)
|
||
.replace(/'/g, "'");
|
||
}
|
||
|
||
// ==================== ИНИЦИАЛИЗАЦИЯ ====================
|
||
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
checkAuth();
|
||
loadSavedConnections();
|
||
}); |