let currentUser = null; let users = []; let tasks = []; let filteredUsers = []; let expandedTasks = new Set(); let showingTasksWithoutDate = false; document.addEventListener('DOMContentLoaded', function() { checkAuth(); setupEventListeners(); }); async function checkAuth() { try { const response = await fetch('/api/user'); if (response.ok) { const data = await response.json(); currentUser = data.user; showMainInterface(); } else { showLoginInterface(); } } catch (error) { showLoginInterface(); } } function showLoginInterface() { document.getElementById('login-modal').style.display = 'block'; document.querySelector('.container').style.display = 'none'; } function showMainInterface() { document.getElementById('login-modal').style.display = 'none'; document.querySelector('.container').style.display = 'block'; let userInfo = `Вы вошли как: ${currentUser.name}`; if (currentUser.auth_type === 'ldap') { userInfo += ` (LDAP)`; } if (currentUser.groups && currentUser.groups.length > 0) { userInfo += ` | Группы: ${currentUser.groups.join(', ')}`; } document.getElementById('current-user').textContent = userInfo; document.getElementById('tasks-controls').style.display = 'block'; const showDeletedLabel = document.querySelector('.show-deleted-label'); if (showDeletedLabel) { if (currentUser.role === 'admin') { showDeletedLabel.style.display = 'flex'; } else { showDeletedLabel.style.display = 'none'; } } loadUsers(); loadTasks(); loadActivityLogs(); showSection('tasks'); showingTasksWithoutDate = false; const btn = document.getElementById('tasks-no-date-btn'); if (btn) btn.classList.remove('active'); } function setupEventListeners() { document.getElementById('login-form').addEventListener('submit', login); document.getElementById('create-task-form').addEventListener('submit', createTask); document.getElementById('edit-task-form').addEventListener('submit', updateTask); document.getElementById('copy-task-form').addEventListener('submit', copyTask); document.getElementById('edit-assignment-form').addEventListener('submit', updateAssignment); document.getElementById('rework-task-form').addEventListener('submit', sendForRework); document.getElementById('files').addEventListener('change', updateFileList); document.getElementById('edit-files').addEventListener('change', updateEditFileList); } async function login(event) { event.preventDefault(); const login = document.getElementById('login').value; const password = document.getElementById('password').value; try { const response = await fetch('/api/login', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ login, password }) }); if (response.ok) { const data = await response.json(); currentUser = data.user; showMainInterface(); } else { const error = await response.json(); alert(error.error || 'Ошибка входа'); } } catch (error) { console.error('Ошибка:', error); alert('Ошибка подключения к серверу'); } } async function logout() { try { await fetch('/api/logout', { method: 'POST' }); currentUser = null; showLoginInterface(); } catch (error) { console.error('Ошибка выхода:', error); } } function showSection(sectionName) { document.querySelectorAll('.section').forEach(section => { section.classList.remove('active'); }); document.getElementById(sectionName + '-section').classList.add('active'); if (sectionName === 'tasks') { loadTasks(); } else if (sectionName === 'logs') { loadActivityLogs(); } } async function loadUsers() { try { const response = await fetch('/api/users'); users = await response.json(); filteredUsers = [...users]; renderUsersChecklist(); renderEditUsersChecklist(); renderCopyUsersChecklist(); populateFilterDropdowns(); } catch (error) { console.error('Ошибка загрузки пользователей:', error); } } function populateFilterDropdowns() { const creatorFilter = document.getElementById('creator-filter'); const assigneeFilter = document.getElementById('assignee-filter'); creatorFilter.innerHTML = ''; assigneeFilter.innerHTML = ''; users.forEach(user => { const creatorOption = document.createElement('option'); creatorOption.value = user.id; creatorOption.textContent = `${user.name} (${user.login})`; creatorFilter.appendChild(creatorOption.cloneNode(true)); const assigneeOption = creatorOption.cloneNode(true); assigneeFilter.appendChild(assigneeOption); }); } function filterUsers() { const search = document.getElementById('user-search').value.toLowerCase(); filteredUsers = users.filter(user => user.name.toLowerCase().includes(search) || user.login.toLowerCase().includes(search) || user.email.toLowerCase().includes(search) ); renderUsersChecklist(); } function filterEditUsers() { const search = document.getElementById('edit-user-search').value.toLowerCase(); const filtered = users.filter(user => user.name.toLowerCase().includes(search) || user.login.toLowerCase().includes(search) || user.email.toLowerCase().includes(search) ); renderEditUsersChecklist(filtered); } function filterCopyUsers() { const search = document.getElementById('copy-user-search').value.toLowerCase(); const filtered = users.filter(user => user.name.toLowerCase().includes(search) || user.login.toLowerCase().includes(search) || user.email.toLowerCase().includes(search) ); renderCopyUsersChecklist(filtered); } async function loadTasks() { try { showingTasksWithoutDate = false; const btn = document.getElementById('tasks-no-date-btn'); if (btn) btn.classList.remove('active'); const search = document.getElementById('search-tasks')?.value || ''; const statusFilter = document.getElementById('status-filter')?.value || 'active,in_progress,assigned,overdue,rework'; const creatorFilter = document.getElementById('creator-filter')?.value || ''; const assigneeFilter = document.getElementById('assignee-filter')?.value || ''; const deadlineFilter = document.getElementById('deadline-filter')?.value || ''; const showDeleted = document.getElementById('show-deleted')?.checked || false; let url = '/api/tasks?'; if (search) url += `search=${encodeURIComponent(search)}&`; if (statusFilter) url += `status=${encodeURIComponent(statusFilter)}&`; if (creatorFilter) url += `creator=${encodeURIComponent(creatorFilter)}&`; if (assigneeFilter) url += `assignee=${encodeURIComponent(assigneeFilter)}&`; if (deadlineFilter) url += `deadline=${encodeURIComponent(deadlineFilter)}&`; if (showDeleted) url += `showDeleted=true&`; const response = await fetch(url); tasks = await response.json(); // Загружаем файлы для всех задач await Promise.all(tasks.map(async (task) => { try { const filesResponse = await fetch(`/api/tasks/${task.id}/files`); if (filesResponse.ok) { task.files = await filesResponse.json(); } else { task.files = []; } } catch (error) { console.error(`Ошибка загрузки файлов для задачи ${task.id}:`, error); task.files = []; } })); renderTasks(); } catch (error) { console.error('Ошибка загрузки задач:', error); } } function showTasksWithoutDate() { showingTasksWithoutDate = true; const btn = document.getElementById('tasks-no-date-btn'); if (btn) btn.classList.add('active'); loadTasksWithoutDate(); } async function loadTasksWithoutDate() { try { const response = await fetch('/api/tasks'); if (!response.ok) throw new Error('Ошибка загрузки задач'); const allTasks = await response.json(); tasks = allTasks.filter(task => { const hasTaskDueDate = !task.due_date; const hasAssignmentDueDates = task.assignments && task.assignments.every(assignment => !assignment.due_date); return hasTaskDueDate && hasAssignmentDueDates; }); // Загружаем файлы для всех задач await Promise.all(tasks.map(async (task) => { try { const filesResponse = await fetch(`/api/tasks/${task.id}/files`); if (filesResponse.ok) { task.files = await filesResponse.json(); } else { task.files = []; } } catch (error) { console.error(`Ошибка загрузки файлов для задачи ${task.id}:`, error); task.files = []; } })); renderTasks(); } catch (error) { console.error('Ошибка загрузки задач без срока:', error); } } async function loadActivityLogs() { try { const response = await fetch('/api/activity-logs'); const logs = await response.json(); renderLogs(logs); } catch (error) { console.error('Ошибка загрузки логов:', error); } } function renderUsersChecklist() { const container = document.getElementById('users-checklist'); container.innerHTML = filteredUsers .filter(user => user.id !== currentUser.id) .map(user => `
`).join(''); } function renderEditUsersChecklist(filtered = users) { const container = document.getElementById('edit-users-checklist'); container.innerHTML = filtered .filter(user => user.id !== currentUser.id) .map(user => `
`).join(''); } function renderCopyUsersChecklist(filtered = users) { const container = document.getElementById('copy-users-checklist'); container.innerHTML = filtered .filter(user => user.id !== currentUser.id) .map(user => `
`).join(''); } function renderTasks() { const container = document.getElementById('tasks-list'); const showDeleted = document.getElementById('show-deleted')?.checked || false; let filteredTasks = tasks; if (!showDeleted) { filteredTasks = tasks.filter(task => task.status === 'active'); } if (filteredTasks.length === 0) { container.innerHTML = '
Задачи не найдены
'; return; } container.innerHTML = filteredTasks.map(task => { const isExpanded = expandedTasks.has(task.id); const overallStatus = getTaskOverallStatus(task); const statusClass = getStatusClass(overallStatus); const isDeleted = task.status === 'deleted'; const isClosed = task.closed_at !== null; const userRole = getUserRoleInTask(task); const canEdit = canUserEditTask(task); const isCopy = task.original_task_id !== null; const timeLeftInfo = getTimeLeftInfo(task); return `
Задача №${task.id} ${task.title} ${isDeleted ? 'Удалена' : ''} ${isClosed ? 'Закрыта' : ''} ${isCopy ? 'Копия' : ''} ${timeLeftInfo ? `${timeLeftInfo.text}` : ''} ${userRole}
${getStatusText(overallStatus)}
${!isDeleted && !isClosed ? ` ${canEdit ? `` : ''} ${canEdit ? `` : ''} ${canEdit ? `` : ''} ${canEdit ? `` : ''} ` : ''} ${isClosed && canEdit ? ` ` : ''} ${isDeleted && currentUser.role === 'admin' ? ` ` : ''}
${isCopy && task.original_task_title ? `
Оригинал: "${task.original_task_title}" (создал: ${task.original_creator_name})
` : ''}
${task.description || 'Нет описания'}
${task.rework_comment ? `
Комментарий к доработке: ${task.rework_comment}
` : ''}
Создана: ${formatDateTime(task.start_date || task.created_at)} ${task.due_date ? ` | Выполнить до: ${formatDateTime(task.due_date)}` : ''} ${showingTasksWithoutDate ? 'Без срока' : ''}
Файлы: ${task.files && task.files.length > 0 ? `
${task.files.map(file => renderFileIcon(file)).join('')}
` : 'нет файлов' }
Исполнители: ${task.assignments && task.assignments.length > 0 ? renderAssignmentList(task.assignments, task.id, canEdit) : '
Не назначены
' }
Создана: ${formatDateTime(task.created_at)} | Автор: ${task.creator_name} ${task.deleted_at ? `
Удалена: ${formatDateTime(task.deleted_at)}` : ''} ${task.closed_at ? `
Закрыта: ${formatDateTime(task.closed_at)}` : ''}
`; }).join(''); } function renderAssignmentList(assignments, taskId, canEdit) { if (!assignments || assignments.length === 0) { return '
Не назначены
'; } // Создаем контейнер с возможностью фильтрации return `
${assignments.length} исполнителей
${assignments.map(assignment => renderAssignment(assignment, taskId, canEdit)).join('')}
`; } // Функция для фильтрации исполнителей в конкретной задаче function filterAssignments(taskId) { const filterInput = document.querySelector(`.assignment-filter-input[data-task-id="${taskId}"]`); const scrollContainer = document.getElementById(`assignments-${taskId}`); const filterCount = document.getElementById(`filter-count-${taskId}`); if (!filterInput || !scrollContainer) return; const searchTerm = filterInput.value.toLowerCase(); const assignments = scrollContainer.querySelectorAll('.assignment'); let visibleCount = 0; assignments.forEach(assignment => { const userName = assignment.querySelector('strong')?.textContent?.toLowerCase() || ''; const userLogin = assignment.querySelector('small')?.textContent?.toLowerCase() || ''; const isVisible = userName.includes(searchTerm) || userLogin.includes(searchTerm) || searchTerm === ''; assignment.style.display = isVisible ? '' : 'none'; if (isVisible) { visibleCount++; } }); if (filterCount) { filterCount.textContent = `${visibleCount} из ${assignments.length} исполнителей`; } } function toggleTask(taskId) { if (expandedTasks.has(taskId)) { expandedTasks.delete(taskId); } else { expandedTasks.add(taskId); loadTaskFiles(taskId); // Эта строка должна быть } renderTasks(); } function getTimeLeftInfo(task) { if (!task.due_date || task.closed_at) return null; const dueDate = new Date(task.due_date); const now = new Date(); const timeLeft = dueDate.getTime() - now.getTime(); const hoursLeft = Math.floor(timeLeft / (60 * 60 * 1000)); if (hoursLeft <= 0) return null; if (hoursLeft <= 24) { return { text: `Менее 24ч`, class: 'deadline-24h' }; } else if (hoursLeft <= 48) { return { text: `Менее 48ч`, class: 'deadline-48h' }; } return null; } function renderAssignment(assignment, taskId, canEdit) { const statusClass = getStatusClass(assignment.status); const isCurrentUser = assignment.user_id === currentUser.id; const isOverdue = assignment.status === 'overdue'; const isRework = assignment.status === 'rework'; const timeLeftInfo = getAssignmentTimeLeftInfo(assignment); return `
${assignment.user_name} ${isCurrentUser ? '(Вы)' : ''} ${timeLeftInfo ? `${timeLeftInfo.text}` : ''} ${assignment.start_date || assignment.due_date ? `
${assignment.start_date ? `Начало: ${formatDateTime(assignment.start_date)}` : ''} ${assignment.due_date ? `Выполнить до: ${formatDateTime(assignment.due_date)}` : ''}
` : ''} ${assignment.rework_comment ? `
Комментарий: ${assignment.rework_comment}
` : ''}
${isCurrentUser && assignment.status === 'assigned' ? `` : ''} ${isCurrentUser && (assignment.status === 'in_progress' || assignment.status === 'overdue' || assignment.status === 'rework') ? `` : ''} ${canEdit ? `` : ''}
`; } function getAssignmentTimeLeftInfo(assignment) { if (!assignment.due_date || assignment.status === 'completed') return null; const dueDate = new Date(assignment.due_date); const now = new Date(); const timeLeft = dueDate.getTime() - now.getTime(); const hoursLeft = Math.floor(timeLeft / (60 * 60 * 1000)); if (hoursLeft <= 0) return null; if (hoursLeft <= 24) { return { text: `Осталось ${hoursLeft}ч`, class: 'deadline-24h' }; } else if (hoursLeft <= 48) { return { text: `Осталось ${hoursLeft}ч`, class: 'deadline-48h' }; } return null; } async function createTask(event) { event.preventDefault(); if (!currentUser) { alert('Требуется аутентификация'); return; } const formData = new FormData(); formData.append('title', document.getElementById('title').value); formData.append('description', document.getElementById('description').value); const dueDate = document.getElementById('due-date').value; if (!dueDate) { alert('Дата и время выполнения обязательны'); return; } formData.append('dueDate', dueDate); const assignedUsers = document.querySelectorAll('#users-checklist input[name="assignedUsers"]:checked'); if (assignedUsers.length === 0) { alert('Выберите хотя бы одного исполнителя'); return; } assignedUsers.forEach(checkbox => { formData.append('assignedUsers', checkbox.value); }); const files = document.getElementById('files').files; for (let i = 0; i < files.length; i++) { formData.append('files', files[i]); } try { const response = await fetch('/api/tasks', { method: 'POST', body: formData }); if (response.ok) { alert('Задача успешно создана!'); document.getElementById('create-task-form').reset(); document.getElementById('file-list').innerHTML = ''; document.getElementById('user-search').value = ''; filterUsers(); loadTasks(); loadActivityLogs(); showSection('tasks'); } else { const error = await response.json(); alert(error.error || 'Ошибка создания задачи'); } } catch (error) { console.error('Ошибка:', error); alert('Ошибка создания задачи'); } } async function openEditModal(taskId) { try { const response = await fetch(`/api/tasks/${taskId}`); if (!response.ok) { if (response.status === 404) { alert('Задача не найдена или у вас нет прав доступа'); } throw new Error('Ошибка загрузки задачи'); } const task = await response.json(); if (!canUserEditTask(task)) { alert('У вас нет прав для редактирования этой задачи'); return; } document.getElementById('edit-task-id').value = task.id; document.getElementById('edit-title').value = task.title; document.getElementById('edit-description').value = task.description || ''; document.getElementById('edit-due-date').value = task.due_date ? formatDateTimeForInput(task.due_date) : ''; const checkboxes = document.querySelectorAll('#edit-users-checklist input[name="assignedUsers"]'); checkboxes.forEach(checkbox => { checkbox.checked = task.assignments?.some(assignment => assignment.user_id === parseInt(checkbox.value) ) || false; }); document.getElementById('edit-task-modal').style.display = 'block'; } catch (error) { console.error('Ошибка:', error); alert('Ошибка загрузки задачи'); } } function closeEditModal() { document.getElementById('edit-task-modal').style.display = 'none'; document.getElementById('edit-file-list').innerHTML = ''; document.getElementById('edit-user-search').value = ''; filterEditUsers(); } async function updateTask(event) { event.preventDefault(); const taskId = document.getElementById('edit-task-id').value; const title = document.getElementById('edit-title').value; const description = document.getElementById('edit-description').value; const dueDate = document.getElementById('edit-due-date').value; if (!dueDate) { alert('Дата и время выполнения обязательны'); return; } const assignedUsers = document.querySelectorAll('#edit-users-checklist input[name="assignedUsers"]:checked'); const assignedUserIds = Array.from(assignedUsers).map(cb => parseInt(cb.value)); const formData = new FormData(); formData.append('title', title); formData.append('description', description); formData.append('assignedUsers', JSON.stringify(assignedUserIds)); formData.append('dueDate', dueDate); const files = document.getElementById('edit-files').files; for (let i = 0; i < files.length; i++) { formData.append('files', files[i]); } try { const response = await fetch(`/api/tasks/${taskId}`, { method: 'PUT', body: formData }); if (response.ok) { alert('Задача успешно обновлена!'); closeEditModal(); loadTasks(); loadActivityLogs(); } else { const error = await response.json(); alert(error.error || 'Ошибка обновления задачи'); } } catch (error) { console.error('Ошибка:', error); alert('Ошибка обновления задачи'); } } function openCopyModal(taskId) { document.getElementById('copy-task-id').value = taskId; document.getElementById('copy-task-modal').style.display = 'block'; } function closeCopyModal() { document.getElementById('copy-task-modal').style.display = 'none'; document.getElementById('copy-user-search').value = ''; filterCopyUsers(); } async function copyTask(event) { event.preventDefault(); const taskId = document.getElementById('copy-task-id').value; const dueDate = document.getElementById('copy-due-date').value; if (!dueDate) { alert('Дата и время выполнения обязательны для копии задачи'); return; } const checkboxes = document.querySelectorAll('#copy-users-checklist input[name="assignedUsers"]:checked'); const assignedUserIds = Array.from(checkboxes).map(cb => parseInt(cb.value)); if (assignedUserIds.length === 0) { alert('Выберите хотя бы одного исполнителя для копии задачи'); return; } try { const response = await fetch(`/api/tasks/${taskId}/copy`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ assignedUsers: assignedUserIds, dueDate: dueDate }) }); if (response.ok) { alert('Копия задачи успешно создана!'); closeCopyModal(); loadTasks(); loadActivityLogs(); } else { const error = await response.json(); alert(error.error || 'Ошибка создания копии задачи'); } } catch (error) { console.error('Ошибка:', error); alert('Ошибка создания копии задачи'); } } function openEditAssignmentModal(taskId, userId) { const task = tasks.find(t => t.id === taskId); if (!task) return; const assignment = task.assignments.find(a => a.user_id === userId); if (!assignment) return; document.getElementById('edit-assignment-task-id').value = taskId; document.getElementById('edit-assignment-user-id').value = userId; document.getElementById('edit-assignment-due-date').value = assignment.due_date ? formatDateTimeForInput(assignment.due_date) : ''; document.getElementById('edit-assignment-modal').style.display = 'block'; } function closeEditAssignmentModal() { document.getElementById('edit-assignment-modal').style.display = 'none'; } async function updateAssignment(event) { event.preventDefault(); const taskId = document.getElementById('edit-assignment-task-id').value; const userId = document.getElementById('edit-assignment-user-id').value; const dueDate = document.getElementById('edit-assignment-due-date').value; if (!dueDate) { alert('Дата и время выполнения обязательны'); return; } try { const response = await fetch(`/api/tasks/${taskId}/assignment/${userId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ dueDate: dueDate }) }); if (response.ok) { alert('Сроки исполнителя обновлены!'); closeEditAssignmentModal(); loadTasks(); loadActivityLogs(); } else { const error = await response.json(); alert(error.error || 'Ошибка обновления сроков'); } } catch (error) { console.error('Ошибка:', error); alert('Ошибка обновления сроков'); } } function openReworkModal(taskId) { document.getElementById('rework-task-id').value = taskId; document.getElementById('rework-task-modal').style.display = 'block'; } function closeReworkModal() { document.getElementById('rework-task-modal').style.display = 'none'; document.getElementById('rework-comment').value = ''; } async function sendForRework(event) { event.preventDefault(); const taskId = document.getElementById('rework-task-id').value; const comment = document.getElementById('rework-comment').value; try { const response = await fetch(`/api/tasks/${taskId}/rework`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ comment }) }); if (response.ok) { alert('Задача возвращена на доработку!'); closeReworkModal(); loadTasks(); loadActivityLogs(); } else { const error = await response.json(); alert(error.error || 'Ошибка возврата задачи на доработку'); } } catch (error) { console.error('Ошибка:', error); alert('Ошибка возврата задачи на доработку'); } } async function closeTask(taskId) { if (!confirm('Вы уверены, что хотите закрыть эту задачу? Исполнители больше не будут видеть её.')) { return; } try { const response = await fetch(`/api/tasks/${taskId}/close`, { method: 'POST' }); if (response.ok) { alert('Задача закрыта!'); loadTasks(); loadActivityLogs(); } else { const error = await response.json(); alert(error.error || 'Ошибка закрытия задачи'); } } catch (error) { console.error('Ошибка:', error); alert('Ошибка закрытия задачи'); } } async function reopenTask(taskId) { try { const response = await fetch(`/api/tasks/${taskId}/reopen`, { method: 'POST' }); if (response.ok) { alert('Задача открыта!'); loadTasks(); loadActivityLogs(); } else { const error = await response.json(); alert(error.error || 'Ошибка открытия задачи'); } } catch (error) { console.error('Ошибка:', error); alert('Ошибка открытия задачи'); } } async function deleteTask(taskId) { if (!confirm('Вы уверены, что хотите удалить эту задачу?')) { return; } try { const response = await fetch(`/api/tasks/${taskId}`, { method: 'DELETE' }); if (response.ok) { alert('Задача удалена!'); loadTasks(); loadActivityLogs(); } else { const error = await response.json(); alert(error.error || 'Ошибка удаления задачи'); } } catch (error) { console.error('Ошибка:', error); alert('Ошибка удаления задачи'); } } async function restoreTask(taskId) { try { const response = await fetch(`/api/tasks/${taskId}/restore`, { method: 'POST' }); if (response.ok) { alert('Задача восстановлена!'); loadTasks(); loadActivityLogs(); } else { const error = await response.json(); alert(error.error || 'Ошибка восстановления задачи'); } } catch (error) { console.error('Ошибка:', error); alert('Ошибка восстановления задачи'); } } async function updateStatus(taskId, userId, status) { try { const response = await fetch(`/api/tasks/${taskId}/status`, { method: 'PUT', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ userId, status }) }); if (response.ok) { loadTasks(); loadActivityLogs(); } else { const error = await response.json(); alert(error.error || 'Ошибка обновления статуса'); } } catch (error) { console.error('Ошибка:', error); alert('Ошибка обновления статуса'); } } function getTaskOverallStatus(task) { if (task.status === 'deleted') return 'deleted'; if (task.closed_at) return 'closed'; if (!task.assignments || task.assignments.length === 0) return 'unassigned'; const assignments = task.assignments; let hasAssigned = false; let hasInProgress = false; let hasOverdue = false; let hasRework = false; let allCompleted = true; for (let assignment of assignments) { if (assignment.status === 'assigned') { hasAssigned = true; allCompleted = false; } else if (assignment.status === 'in_progress') { hasInProgress = true; allCompleted = false; } else if (assignment.status === 'overdue') { hasOverdue = true; allCompleted = false; } else if (assignment.status === 'rework') { hasRework = true; allCompleted = false; } else if (assignment.status !== 'completed') { allCompleted = false; } } if (allCompleted) return 'completed'; if (hasRework) return 'rework'; if (hasOverdue) return 'overdue'; if (hasInProgress) return 'in_progress'; if (hasAssigned) return 'assigned'; return 'unassigned'; } function getStatusClass(status) { switch (status) { case 'deleted': return 'status-gray'; case 'closed': return 'status-gray'; case 'unassigned': return 'status-purple'; case 'assigned': return 'status-red'; case 'in_progress': return 'status-orange'; case 'rework': return 'status-yellow'; case 'overdue': return 'status-darkred'; case 'completed': return 'status-green'; default: return 'status-purple'; } } function getStatusText(status) { switch (status) { case 'deleted': return 'Удалена'; case 'closed': return 'Закрыта'; case 'unassigned': return 'Не назначена'; case 'assigned': return 'Назначена'; case 'in_progress': return 'В работе'; case 'rework': return 'На доработке'; case 'overdue': return 'Просрочена'; case 'completed': return 'Выполнена'; default: return 'Неизвестно'; } } function getUserRoleInTask(task) { if (!currentUser) return 'Нет доступа'; if (currentUser.role === 'admin') return 'Администратор'; if (parseInt(task.created_by) === currentUser.id) return 'Заказчик'; if (task.assignments) { const isExecutor = task.assignments.some(assignment => parseInt(assignment.user_id) === currentUser.id ); if (isExecutor) return 'Исполнитель'; } return 'Наблюдатель'; } function getRoleBadgeClass(role) { switch (role) { case 'Администратор': return 'role-admin'; case 'Заказчик': return 'role-creator'; case 'Исполнитель': return 'role-executor'; default: return ''; } } function canUserEditTask(task) { if (!currentUser) return false; if (currentUser.role === 'admin') return true; if (parseInt(task.created_by) === currentUser.id) return true; return false; } function formatDateTime(dateTimeString) { if (!dateTimeString) return ''; const date = new Date(dateTimeString); return date.toLocaleString('ru-RU'); } function formatDateTimeForInput(dateTimeString) { if (!dateTimeString) return ''; const date = new Date(dateTimeString); return date.toISOString().slice(0, 16); } function updateFileList() { const fileInput = document.getElementById('files'); const fileList = document.getElementById('file-list'); updateFileListForInput(fileInput, fileList); } function updateEditFileList() { const fileInput = document.getElementById('edit-files'); const fileList = document.getElementById('edit-file-list'); updateFileListForInput(fileInput, fileList); } function updateFileListForInput(fileInput, fileList) { const files = fileInput.files; if (files.length === 0) { fileList.innerHTML = ''; return; } let html = ''; html += `

Общий размер: ${(totalSize / 1024 / 1024).toFixed(2)} MB / 300 MB

`; fileList.innerHTML = html; } function renderLogs(logs) { const container = document.getElementById('logs-list'); if (logs.length === 0) { container.innerHTML = '
Логи не найдены
'; return; } container.innerHTML = logs.map(log => `
${formatDateTime(log.created_at)}
${log.user_name} - ${getActionText(log.action)}
Задача: "${log.task_title}"
${log.details ? `
Детали: ${log.details}
` : ''}
`).join(''); } function getActionText(action) { const actions = { 'TASK_CREATED': 'создал задачу', 'TASK_COPIED': 'создал копию задачи', 'TASK_UPDATED': 'обновил задачу', 'TASK_DELETED': 'удалил задачу', 'TASK_RESTORED': 'восстановил задачу', 'TASK_ASSIGNED': 'назначил задачу', 'TASK_ASSIGNMENTS_UPDATED': 'обновил назначения', 'ASSIGNMENT_UPDATED': 'обновил сроки исполнителя', 'STATUS_CHANGED': 'изменил статус задачи', 'FILE_UPLOADED': 'загрузил файл', 'FILE_COPIED': 'скопировал файл', 'TASK_SENT_FOR_REWORK': 'вернул задачу на доработку', 'TASK_CLOSED': 'закрыл задачу', 'TASK_REOPENED': 'открыл задачу' }; return actions[action] || action; } async function loadTaskFiles(taskId) { try { const response = await fetch(`/api/tasks/${taskId}/files`); const files = await response.json(); const container = document.getElementById(`files-${taskId}`); if (container) { if (files.length === 0) { container.innerHTML = 'Файлы: скрыто'; } else { container.innerHTML = ` Файлы:
${files.map(file => renderFileIcon(file)).join('')}
`; } } } catch (error) { console.error('Ошибка загрузки файлов:', error); } } function renderFileIcon(file) { // Исправляем кодировку имени файла const fixEncoding = (str) => { if (!str) return ''; try { // Пробуем разные способы декодирования if (str.includes('Ð') || str.includes('Ñ')) { // UTF-8 неправильно декодированный как Latin-1 return decodeURIComponent(escape(str)); } return str; } catch (e) { return str; } }; const fileName = fixEncoding(file.original_name); const fileSize = (file.file_size / 1024 / 1024).toFixed(2); const uploadedBy = file.user_name; let iconColor = ''; let iconText = ''; let textClass = ''; // Определяем расширение файла const extension = fileName.includes('.') ? fileName.split('.').pop().toLowerCase() : ''; // Определяем тип файла на основе расширения if (extension) { switch (extension) { case 'pdf': iconColor = '#e74c3c'; iconText = 'PDF'; textClass = 'short'; break; case 'doc': iconColor = '#3498db'; iconText = 'DOC'; textClass = 'short'; break; case 'docx': iconColor = '#3498db'; iconText = 'DOCX'; textClass = 'medium'; break; case 'xls': iconColor = '#2ecc71'; iconText = 'XLS'; textClass = 'short'; break; case 'xlsx': iconColor = '#2ecc71'; iconText = 'XLSX'; textClass = 'medium'; break; case 'csv': iconColor = '#2ecc71'; iconText = 'CSV'; textClass = 'short'; break; case 'ppt': iconColor = '#e67e22'; iconText = 'PPT'; textClass = 'short'; break; case 'pptx': iconColor = '#e67e22'; iconText = 'PPTX'; textClass = 'medium'; break; case 'zip': iconColor = '#f39c12'; iconText = 'ZIP'; textClass = 'short'; break; case 'rar': iconColor = '#f39c12'; iconText = 'RAR'; textClass = 'short'; break; case '7z': iconColor = '#f39c12'; iconText = '7Z'; textClass = 'short'; break; case 'tar': iconColor = '#f39c12'; iconText = 'TAR'; textClass = 'short'; break; case 'gz': iconColor = '#f39c12'; iconText = 'GZ'; textClass = 'short'; break; case 'txt': iconColor = '#95a5a6'; iconText = 'TXT'; textClass = 'short'; break; case 'log': iconColor = '#95a5a6'; iconText = 'LOG'; textClass = 'short'; break; case 'md': iconColor = '#95a5a6'; iconText = 'MD'; textClass = 'short'; break; case 'jpg': iconColor = '#9b59b6'; iconText = 'JPG'; textClass = 'short'; break; case 'jpeg': iconColor = '#9b59b6'; iconText = 'JPEG'; textClass = 'medium'; break; case 'png': iconColor = '#9b59b6'; iconText = 'PNG'; textClass = 'short'; break; case 'gif': iconColor = '#9b59b6'; iconText = 'GIF'; textClass = 'short'; break; case 'bmp': iconColor = '#9b59b6'; iconText = 'BMP'; textClass = 'short'; break; case 'svg': iconColor = '#9b59b6'; iconText = 'SVG'; textClass = 'short'; break; case 'webp': iconColor = '#9b59b6'; iconText = 'WEBP'; textClass = 'medium'; break; case 'mp3': iconColor = '#1abc9c'; iconText = 'MP3'; textClass = 'short'; break; case 'wav': iconColor = '#1abc9c'; iconText = 'WAV'; textClass = 'short'; break; case 'ogg': iconColor = '#1abc9c'; iconText = 'OGG'; textClass = 'short'; break; case 'flac': iconColor = '#1abc9c'; iconText = 'FLAC'; textClass = 'medium'; break; case 'mp4': iconColor = '#d35400'; iconText = 'MP4'; textClass = 'short'; break; case 'avi': iconColor = '#d35400'; iconText = 'AVI'; textClass = 'short'; break; case 'mkv': iconColor = '#d35400'; iconText = 'MKV'; textClass = 'short'; break; case 'mov': iconColor = '#d35400'; iconText = 'MOV'; textClass = 'short'; break; case 'wmv': iconColor = '#d35400'; iconText = 'WMV'; textClass = 'short'; break; case 'exe': iconColor = '#c0392b'; iconText = 'EXE'; textClass = 'short'; break; case 'msi': iconColor = '#c0392b'; iconText = 'MSI'; textClass = 'short'; break; case 'js': iconColor = '#2980b9'; iconText = 'JS'; textClass = 'short'; break; case 'html': iconColor = '#2980b9'; iconText = 'HTML'; textClass = 'medium'; break; case 'css': iconColor = '#2980b9'; iconText = 'CSS'; textClass = 'short'; break; case 'php': iconColor = '#2980b9'; iconText = 'PHP'; textClass = 'short'; break; case 'py': iconColor = '#2980b9'; iconText = 'PY'; textClass = 'short'; break; case 'java': iconColor = '#2980b9'; iconText = 'JAVA'; textClass = 'medium'; break; case 'json': iconColor = '#8e44ad'; iconText = 'JSON'; textClass = 'medium'; break; case 'xml': iconColor = '#8e44ad'; iconText = 'XML'; textClass = 'short'; break; case 'yml': iconColor = '#8e44ad'; iconText = 'YML'; textClass = 'short'; break; case 'yaml': iconColor = '#8e44ad'; iconText = 'YAML'; textClass = 'medium'; break; case 'sql': iconColor = '#27ae60'; iconText = 'SQL'; textClass = 'short'; break; case 'db': iconColor = '#27ae60'; iconText = 'DB'; textClass = 'short'; break; case 'sqlite': iconColor = '#27ae60'; iconText = 'SQLITE'; textClass = 'long'; break; default: // Для других расширений используем расширение или первые 4 символа iconColor = '#7f8c8d'; iconText = extension.length > 4 ? extension.substring(0, 4).toUpperCase() : extension.toUpperCase(); // Определяем класс по длине текста if (iconText.length <= 2) { textClass = 'short'; } else if (iconText.length <= 4) { textClass = 'medium'; } else { textClass = 'long'; } } } else { // Если нет расширения iconColor = '#7f8c8d'; iconText = 'ФАЙЛ'; textClass = 'short'; } // Исправляем кодировку для отображения const safeFileName = fileName; const displayFileName = truncateFileName(safeFileName); return `
${iconText}
${displayFileName}
`; } function truncateFileName(fileName, maxLength = 20) { if (fileName.length <= maxLength) return fileName; const extension = fileName.split('.').pop(); const name = fileName.substring(0, fileName.lastIndexOf('.')); const truncatedName = name.substring(0, maxLength - extension.length - 3) + '...'; return truncatedName + '.' + extension; }