// 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.description || 'Нет описания')}
${renderFiles(task.files, task.id)}
${task.assignment_status !== 'completed' ? `
` : ''}
`;
}).join('');
}
function renderFiles(files, taskId) {
if (!files || files.length === 0) {
return '';
}
return `
Файлы (${files.length})
${files.map(file => `
-
${escapeHtml(file.filename || file.original_name)}
${formatFileSize(file.file_size)}
`).join('')}
`;
}
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();
});