// 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 => `
${conn.name}
`).join(''); } else { connectionsList.innerHTML = '
Нет сохраненных подключений
'; } } catch (error) { console.error('Ошибка загрузки подключений:', error); document.getElementById('connectionsList').innerHTML = '
Ошибка загрузки
'; } } 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 = '

Подключитесь к серверу

'; 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 = ' Подключение...'; 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 = ` Сервер: ${data.connection.url}
Пользователь: ${data.server_info.user}
Задач на сервере: ${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 = ' Подключиться'; } } // ==================== ЗАГРУЗКА И ОТОБРАЖЕНИЕ ЗАДАЧ ==================== 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 = '

Загрузка задач...

'; 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 = '

Задачи не найдены

'; } else { renderTasks(currentTasks); } updatePagination(); } else { tasksContainer.innerHTML = `

Ошибка: ${data.error || 'Неизвестная ошибка'}

`; } } catch (error) { console.error('Ошибка загрузки задач:', error); tasksContainer.innerHTML = '

Ошибка загрузки задач

'; } } 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 `
${escapeHtml(task.title || 'Без названия')}
${statusText}
${escapeHtml(task.description || 'Нет описания')}
Создана: ${createdDate}
Срок: ${dueDate}
Автор: ${escapeHtml(task.creator_name || 'Н/Д')}
${renderFiles(task.files, task.id)}
${task.assignment_status !== 'completed' ? ` ` : ''}
`; }).join(''); } function renderFiles(files, taskId) { if (!files || files.length === 0) { return ''; } return `
Файлы (${files.length})
`; } 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 = ' Обновление...'; } 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' ? ' В работу' : ' Завершить'; } } } catch (error) { console.error('Ошибка обновления статуса:', error); showAlert('Ошибка при обновлении статуса', 'danger'); if (button) { button.disabled = false; button.innerHTML = status === 'in_progress' ? ' В работу' : ' Завершить'; } } } // ==================== ИМПОРТ ЗАДАЧИ В ЛОКАЛЬНУЮ CRM ==================== /** * Загружает задачу из внешнего сервиса и открывает модальное окно для импорта */ async function importTask(taskId) { if (!currentConnectionId) { showAlert('Сначала выберите подключение', 'warning'); return; } console.log(`[Import] Загрузка задачи ${taskId} из внешнего сервиса...`); try { // 1. Получаем задачу из внешнего API const response = await fetch(`/api/client/tasks/${taskId}?connection_id=${currentConnectionId}`); const data = await response.json(); if (!data.success || !data.task) { showAlert('Не удалось получить задачу из внешнего сервиса', 'danger'); return; } const task = data.task; importTaskData = { title: task.title, description: task.description || '', due_date: task.due_date || '', task_type: task.task_type || 'regular', files: task.files || [] }; // 2. Загружаем список локальных пользователей (если ещё не загружен) await loadLocalUsers(); // 3. Открываем модальное окно импорта openImportModal(task); } catch (error) { console.error('Ошибка импорта задачи:', error); showAlert('Ошибка при загрузке задачи', 'danger'); } } /** * Загружает список локальных пользователей (используется в модальном окне) */ let localUsers = []; async function loadLocalUsers() { if (localUsers.length > 0) return; try { const response = await fetch('/api/users'); if (response.ok) { localUsers = await response.json(); } } catch (error) { console.error('Ошибка загрузки локальных пользователей:', error); } } /** * Открывает модальное окно импорта задачи */ function openImportModal(task) { const modal = document.getElementById('import-task-modal'); if (!modal) { console.error('Модальное окно import-task-modal не найдено'); return; } // Заполняем данные задачи document.getElementById('import-task-title').textContent = task.title; document.getElementById('import-task-description').textContent = task.description || 'Нет описания'; // Устанавливаем дату выполнения const dueDateInput = document.getElementById('import-due-date'); if (task.due_date) { const date = new Date(task.due_date); dueDateInput.value = date.toISOString().slice(0, 16); } else { const defaultDate = new Date(); defaultDate.setDate(defaultDate.getDate() + 7); dueDateInput.value = defaultDate.toISOString().slice(0, 16); } // Очищаем и заполняем список исполнителей const container = document.getElementById('import-users-checklist'); container.innerHTML = ''; // Фильтруем текущего пользователя (нельзя назначить самому себе) const filteredUsers = localUsers.filter(u => u.id !== currentUserId); filteredUsers.forEach(user => { const div = document.createElement('div'); div.className = 'checkbox-item'; div.innerHTML = ` `; container.appendChild(div); }); // Показываем модальное окно modal.style.display = 'block'; } /** * Закрывает модальное окно импорта */ function closeImportModal() { const modal = document.getElementById('import-task-modal'); if (modal) { modal.style.display = 'none'; } importTaskData = null; } /** * Выполняет импорт задачи: создаёт локальную задачу с выбранными исполнителями */ async function confirmImport() { if (!importTaskData) { showAlert('Нет данных для импорта', 'danger'); return; } const dueDate = document.getElementById('import-due-date').value; if (!dueDate) { showAlert('Укажите дату выполнения', 'warning'); return; } // Собираем выбранных исполнителей const checkboxes = document.querySelectorAll('.import-user-checkbox:checked'); const assignedUsers = Array.from(checkboxes).map(cb => parseInt(cb.value)); if (assignedUsers.length === 0) { showAlert('Выберите хотя бы одного исполнителя', 'warning'); return; } // Формируем FormData для отправки const formData = new FormData(); formData.append('title', importTaskData.title); formData.append('description', importTaskData.description); formData.append('dueDate', dueDate); formData.append('taskType', importTaskData.task_type); formData.append('assignedUsers', JSON.stringify(assignedUsers)); console.log('FormData assignedUsers:', assignedUsers); // Если есть файлы – добавляем их (но тут сложность: файлы нужно скачать из внешнего сервиса и загрузить) // Пока пропустим файлы для простоты, можно добавить позже. try { const response = await fetch('/api/tasks', { method: 'POST', body: formData }); const result = await response.json(); if (result.success) { showAlert(`Задача успешно импортирована! Новый ID: ${result.taskId}`, 'success'); closeImportModal(); // Можно перезагрузить список локальных задач, если нужно } else { showAlert('Ошибка создания задачи: ' + (result.error || 'Неизвестная ошибка'), 'danger'); } } catch (error) { console.error('Ошибка импорта:', error); showAlert('Ошибка при создании задачи', 'danger'); } } // ==================== РАБОТА С ФАЙЛАМИ ==================== function openUploadModal(taskId) { currentTaskId = taskId; document.getElementById('uploadModal').classList.add('active'); 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) => `
${escapeHtml(file.name)} ${formatFileSize(file.size)}
`).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) => `
${escapeHtml(file.name)} ${formatFileSize(file.size)}
`).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, "'"); } // ==================== ИНИЦИАЛИЗАЦИЯ ==================== document.addEventListener('DOMContentLoaded', () => { checkAuth(); loadSavedConnections(); });