diff --git a/public/help-tasks.js b/public/help-tasks.js deleted file mode 100644 index 7c19c2a..0000000 --- a/public/help-tasks.js +++ /dev/null @@ -1,597 +0,0 @@ -// help-tasks.js - Основные операции с заявками -let tasks = []; -let expandedTasks = new Set(); -let showingTasksWithoutDate = false; - -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 getHelpUsers() { - try { - const response = await fetch('/api/users/group/help'); - if (response.ok) { - return await response.json(); - } - return []; - } catch (error) { - console.error('Ошибка получения пользователей группы help:', error); - return []; - } -} - -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); - - // Заявка автоматически назначается всем пользователям группы "help" - // Получаем пользователей группы help - const helpUsers = await getHelpUsers(); - - if (helpUsers.length === 0) { - alert('Нет пользователей в группе "help". Заявка не может быть создана.'); - return; - } - - // Добавляем всех пользователей группы help как исполнителей - helpUsers.forEach(user => { - formData.append('assignedUsers', user.id); - }); - - 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('Заявка успешно создана и назначена всем пользователям группы "help"!'); - document.getElementById('create-task-form').reset(); - document.getElementById('file-list').innerHTML = ''; - 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) : ''; - - // Показываем пользователей группы help, назначенных на заявку - showHelpGroupUsersInEditModal(task); - - // Показываем существующие файлы - currentEditTaskFiles = task.files || []; - updateEditFileList(); - - document.getElementById('edit-task-modal').style.display = 'block'; - } catch (error) { - console.error('Ошибка:', error); - alert('Ошибка загрузки заявки'); - } -} - -function showHelpGroupUsersInEditModal(task) { - const container = document.getElementById('edit-help-group-users'); - const helpUsers = users.filter(user => user.groups && user.groups.includes('help')); - - if (helpUsers.length === 0) { - container.innerHTML = '

Нет пользователей в группе "help"

'; - return; - } - - // Получаем назначенных пользователей - const assignedUserIds = task.assignments ? task.assignments.map(a => a.user_id) : []; - - container.innerHTML = helpUsers.map(user => { - const isAssigned = assignedUserIds.includes(user.id.toString()); - return ` -
- - ${user.name} (${user.email}) - ${isAssigned ? 'назначен' : ''} -
- `; - }).join(''); -} - -function closeEditModal() { - document.getElementById('edit-task-modal').style.display = 'none'; - document.getElementById('edit-file-list').innerHTML = ''; - currentEditTaskFiles = []; -} - -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; - } - - // Заявка автоматически назначается всем пользователям группы "help" - const helpUsers = users.filter(user => user.groups && user.groups.includes('help')); - const assignedUserIds = helpUsers.map(user => user.id); - - if (assignedUserIds.length === 0) { - alert('Нет пользователей в группе "help". Заявка не может быть обновлена.'); - return; - } - - 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('Заявка успешно обновлена и назначена всем пользователям группы "help"!'); - 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; - - // Устанавливаем дату по умолчанию (через 7 дней) - const defaultDate = new Date(); - defaultDate.setDate(defaultDate.getDate() + 7); - document.getElementById('copy-due-date').value = defaultDate.toISOString().substring(0, 16); - - document.getElementById('copy-task-modal').style.display = 'block'; -} - -function closeCopyModal() { - document.getElementById('copy-task-modal').style.display = 'none'; -} - -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; - } - - // Копия заявки автоматически назначается всем пользователям группы "help" - const helpUsers = users.filter(user => user.groups && user.groups.includes('help')); - const assignedUserIds = helpUsers.map(user => user.id); - - if (assignedUserIds.length === 0) { - alert('Нет пользователей в группе "help". Копия заявки не может быть создана.'); - 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('Копия заявки успешно создана и назначена всем пользователям группы "help"!'); - closeCopyModal(); - 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('Ошибка восстановления заявки'); - } -} - -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 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 canUserEditTask(task) { - if (!currentUser) return false; - - // Администратор может всё - if (currentUser.role === 'admin') return true; - - // Создатель может редактировать свою заявку - if (parseInt(task.created_by) === currentUser.id) { - // Но если заявка уже назначена группе "help", - // создатель может только просматривать - if (task.assignments && task.assignments.length > 0) { - return false; - } - return true; - } - - // Пользователи группы "help" могут менять только свой статус - if (task.assignments) { - const isHelpUser = task.assignments.some(assignment => - parseInt(assignment.user_id) === currentUser.id - ); - if (isHelpUser) { - return false; // Могут менять только статус - } - } - - return false; -} - -// Функция для отображения пользователей группы help при создании заявки -function showHelpGroupUsers() { - const container = document.getElementById('help-group-users'); - const helpUsers = users.filter(user => user.groups && user.groups.includes('help')); - - container.innerHTML = helpUsers.map(user => ` -
- - ${user.name} (${user.email}) -
- `).join(''); -} \ No newline at end of file diff --git a/public/help-users.js b/public/help-users.js deleted file mode 100644 index 673b53d..0000000 --- a/public/help-users.js +++ /dev/null @@ -1,153 +0,0 @@ -// help-users.js - Управление пользователями -let users = []; -let allUsers = []; -let filteredUsers = []; -let selectedUsers = []; -let editSelectedUsers = []; -let copySelectedUsers = []; - -async function loadUsers() { - try { - const response = await fetch('/api/users'); - users = await response.json(); - allUsers = users; - - // Получаем пользователей группы "help" - const helpUsers = users.filter(user => user.groups && user.groups.includes('help')); - filteredUsers = helpUsers; - - // Показываем пользователей группы help при создании заявки - showHelpGroupUsers(); - - populateFilterDropdowns(); - } catch (error) { - console.error('Ошибка загрузки пользователей:', error); - } -} - -function populateFilterDropdowns() { - const creatorFilter = document.getElementById('creator-filter'); - const assigneeFilter = document.getElementById('assignee-filter'); - - // Проверяем существование элементов (они есть только на странице help.html) - if (!creatorFilter || !assigneeFilter) { - console.log('Фильтры не найдены (возможно, не на странице help.html)'); - return; - } - - 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); - }); - - // Для исполнителей показываем только пользователей группы "help" - const helpUsers = users.filter(user => user.groups && user.groups.includes('help')); - helpUsers.forEach(user => { - const assigneeOption = document.createElement('option'); - assigneeOption.value = user.id; - assigneeOption.textContent = `${user.name} (${user.login}) - группа "help"`; - assigneeFilter.appendChild(assigneeOption); - }); -} - -// Функция для отображения пользователей группы help -function showHelpGroupUsers() { - const container = document.getElementById('help-group-users'); - - // Проверяем существование элемента (он есть только на странице help.html) - if (!container) { - console.log('Контейнер help-group-users не найден (возможно, не на странице help.html)'); - return; - } - - const helpUsers = users.filter(user => user.groups && user.groups.includes('help')); - - if (helpUsers.length === 0) { - container.innerHTML = '
Нет пользователей в группе "help"
'; - return; - } - - container.innerHTML = ` -
- Заявка будет автоматически назначена ${helpUsers.length} пользователям группы "help": -
-
- ${helpUsers.map(user => ` -
- - ${user.name} - (${user.email}) -
- `).join('')} -
- `; -} - -// Старые функции фильтрации оставляем, но они теперь не используются для выбора исполнителей -function filterUsers() { - const searchInput = document.getElementById('user-search'); - if (!searchInput) return; // Элемент есть только на странице help.html - - const search = searchInput.value.toLowerCase(); - // Фильтруем пользователей группы "help" - filteredUsers = users.filter(user => - user.groups && user.groups.includes('help') && ( - user.name.toLowerCase().includes(search) || - user.login.toLowerCase().includes(search) || - user.email.toLowerCase().includes(search) - ) - ); - // Не рендерим чеклист, так как выбираем всех пользователей группы help -} - -function filterEditUsers() { - // Не используется, так как заявка автоматически назначается всем пользователям группы help -} - -function filterCopyUsers() { - // Не используется, так как заявка автоматически назначается всем пользователям группы help -} - -// Старые функции рендеринга оставляем для совместимости, но они не будут использоваться -function renderUsersChecklist() { - // Не рендерим чеклист, так как выбираем всех пользователей группы help -} - -function renderEditUsersChecklist(filtered = users) { - // Не рендерим чеклист, так как заявка автоматически назначается всем пользователям группы help -} - -function renderCopyUsersChecklist(filtered = users) { - // Не рендерим чеклист, так как заявка автоматически назначается всем пользователям группы help -} - -// Старые функции выбора пользователей оставляем для совместимости -function toggleUserSelection(checkbox, userId) { - if (checkbox.checked) { - selectedUsers.push(userId); - } else { - selectedUsers = selectedUsers.filter(id => id !== userId); - } -} - -function toggleEditUserSelection(checkbox, userId) { - if (checkbox.checked) { - editSelectedUsers.push(userId); - } else { - editSelectedUsers = editSelectedUsers.filter(id => id !== userId); - } -} - -function toggleCopyUserSelection(checkbox, userId) { - if (checkbox.checked) { - copySelectedUsers.push(userId); - } else { - copySelectedUsers = copySelectedUsers.filter(id => id !== userId); - } -} \ No newline at end of file diff --git a/public/help.html b/public/help.html deleted file mode 100644 index d322ccf..0000000 --- a/public/help.html +++ /dev/null @@ -1,264 +0,0 @@ - - - - - - School CRM - поддержка - - - - - - -
-
-
-

School CRM - система заявок

- -
- -
- -
-
-

Все заявки

-
-
- -
- -
-
-
- -
-

Создать новую заявку

-
-
- - -
- -
- - -
- -
- - -
- -
- -
- Заявка автоматически будет назначена всем пользователям группы "поддержка" -
-
- -
-
- -
- -
- - -
-
-
- - -
-
-
-
- - - - - - - - - - -
-
-

Канбан-доска заявок

-

Перетаскивайте заявки между колонками для изменения статуса

-
-
- - -
-
-
- -
-
Загрузка Канбан-доски...
-
-
- - - - - - - - - - \ No newline at end of file diff --git a/public/index.html b/public/index.html index 6fc03eb..c6bcad5 100644 --- a/public/index.html +++ b/public/index.html @@ -372,7 +372,8 @@
Загрузка Канбан-доски...
- + + @@ -383,6 +384,7 @@ - + + \ No newline at end of file diff --git a/public/tasks_files.js b/public/tasks_files.js new file mode 100644 index 0000000..c3741af --- /dev/null +++ b/public/tasks_files.js @@ -0,0 +1,619 @@ +// tasks_files.js - Расширенное управление файлами задач +// Функции для удаления файлов, загрузки и управления доступом + +// Сохраняем ссылку на оригинальную функцию при загрузке скрипта +let originalRenderFileIcon = null; +let originalRenderGroupedFiles = null; + +/** + * Проверяет, может ли пользователь удалить файл + * @param {Object} file - Объект файла + * @param {Object} task - Объект задачи + * @returns {boolean} - true если может удалить + */ +function canUserDeleteFile(file, task) { + if (!currentUser) return false; + + // Администратор может удалять любые файлы + if (currentUser.role === 'admin') return true; + + // Пользователи с ролью 'tasks' могут удалять любые файлы + if (currentUser.role === 'tasks') return true; + + // Автор задачи может удалять любые файлы в своей задаче + if (task && parseInt(task.created_by) === currentUser.id) return true; + + // Пользователь может удалять только свои файлы + if (file && parseInt(file.user_id) === currentUser.id) return true; + + return false; +} + +/** + * Удаляет файл из задачи + * @param {number} fileId - ID файла + * @param {number} taskId - ID задачи + */ +async function deleteTaskFile(fileId, taskId) { + if (!confirm('Вы уверены, что хотите удалить этот файл?')) { + return; + } + + // Находим задачу и файл для проверки прав + const task = tasks.find(t => t.id === taskId); + if (!task) { + alert('Задача не найдена'); + return; + } + + // Находим файл в текущих данных + let file = null; + if (task.files) { + file = task.files.find(f => f.id === fileId); + } + + if (!file) { + // Пробуем загрузить файл отдельно + try { + const response = await fetch(`/api/files/${fileId}`); + if (response.ok) { + file = await response.json(); + } + } catch (error) { + console.error('Ошибка загрузки данных файла:', error); + } + } + + // Проверяем права на удаление + if (!canUserDeleteFile(file || { user_id: 0 }, task)) { + alert('У вас нет прав для удаления этого файла'); + return; + } + + try { + const response = await fetch(`/api/tasks/${taskId}/files/${fileId}`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json' + } + }); + + if (response.ok) { + alert('✅ Файл успешно удален'); + + // Обновляем список файлов в задаче + if (task.files) { + task.files = task.files.filter(f => f.id !== fileId); + } + + // Обновляем отображение + const fileContainer = document.getElementById(`files-${taskId}`); + if (fileContainer) { + fileContainer.innerHTML = ` + Файлы: + ${task.files && task.files.length > 0 ? + renderGroupedFilesWithDelete(task) : + 'нет файлов'} + `; + } + + // Перезагружаем задачи для синхронизации + if (typeof loadTasks === 'function') { + loadTasks(); + } + } else { + const error = await response.json(); + alert(`❌ Ошибка: ${error.error || 'Неизвестная ошибка'}`); + } + } catch (error) { + console.error('❌ Ошибка удаления файла:', error); + alert('Сетевая ошибка при удалении файла'); + } +} + +/** + * Рендерит иконку файла с кнопкой удаления + * @param {Object} file - Объект файла + * @param {number} taskId - ID задачи + * @param {Object} task - Объект задачи (опционально) + * @returns {string} HTML строка + */ +/** + * Рендерит иконку файла с кнопкой удаления + * @param {Object} file - Объект файла + * @param {number} taskId - ID задачи + * @param {Object} task - Объект задачи (опционально) + * @returns {string} HTML строка + */ +function renderFileIconWithDelete(file, taskId, task) { + // Получаем задачу, если не передана + if (!task && taskId) { + task = tasks.find(t => t.id === taskId); + } + + // Исправляем кодировку имени файла + const fixEncoding = (str) => { + if (!str) return ''; + try { + if (str.includes('Ð') || str.includes('Ñ')) { + 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; + + // --- ПОЛНАЯ ЛОГИКА ОПРЕДЕЛЕНИЯ ЦВЕТА И ИКОНКИ ИЗ ОРИГИНАЛЬНОЙ renderFileIcon --- + 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 displayFileName = truncateFileName(fileName); + const canDelete = task ? canUserDeleteFile(file, task) : false; + + // Создаем контейнер с flex-расположением + let html = ` +
+ +
+ ${iconText} +
+
${displayFileName}
+
+ `; + + // Добавляем кнопку удаления справа от файла, вертикальную + if (canDelete) { + html += ` +
+ +
+ `; + } + html += `
`; + + return html; +} + +/** + * Рендерит группированные файлы с поддержкой удаления + * @param {Object} task - Объект задачи + * @returns {string} HTML строка + */ +function renderGroupedFilesWithDelete(task) { + if (!task.files || task.files.length === 0) { + return 'нет файлов'; + } + + // Группируем файлы по пользователю + const filesByUploader = {}; + + task.files.forEach(file => { + const uploaderId = file.user_id; + const uploaderName = file.user_name || 'Неизвестный пользователь'; + + if (!filesByUploader[uploaderId]) { + filesByUploader[uploaderId] = { + name: uploaderName, + id: uploaderId, + files: [] + }; + } + filesByUploader[uploaderId].files.push(file); + }); + + // Определяем видимые группы + const visibleGroups = []; + + for (const uploaderId in filesByUploader) { + const uploaderGroup = filesByUploader[uploaderId]; + const uploaderIdNum = parseInt(uploaderId); + + let canSeeThisUploader = false; + + if (currentUser.role === 'admin' || + currentUser.role === 'tasks' || + parseInt(task.created_by) === currentUser.id) { + canSeeThisUploader = true; + } else { + const creatorId = parseInt(task.created_by); + if (uploaderIdNum === creatorId || uploaderIdNum === currentUser.id) { + canSeeThisUploader = true; + } + } + + if (canSeeThisUploader) { + visibleGroups.push({ + name: uploaderGroup.name, + id: uploaderGroup.id, + files: uploaderGroup.files + }); + } + } + + if (visibleGroups.length === 0) { + return 'нет файлов'; + } + + // Рендерим группы + if (visibleGroups.length === 1) { + const uploader = visibleGroups[0]; + return ` +
+
+ ${escapeHtml(uploader.name)}: +
+
+ ${uploader.files.map(file => + renderFileIconWithDelete(file, task.id, task) + ).join('')} +
+
+ `; + } + + return visibleGroups.map(uploader => ` +
+
+ ${escapeHtml(uploader.name)}: +
+
+ ${uploader.files.map(file => + renderFileIconWithDelete(file, task.id, task) + ).join('')} +
+
+ `).join(''); +} + +/** + * Экранирует HTML специальные символы + * @param {string} text - Текст для экранирования + * @returns {string} Экранированный текст + */ +function escapeHtml(text) { + if (!text) return ''; + return String(text) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +/** + * Инициализирует расширенные функции работы с файлами + */ +function initializeFileManagement() { + console.log('📁 Инициализация расширенного управления файлами...'); + + // Сохраняем ссылки на оригинальные функции + if (typeof renderFileIcon === 'function' && renderFileIcon !== renderFileIconWithDelete) { + originalRenderFileIcon = renderFileIcon; + window.originalRenderFileIcon = renderFileIcon; + } + + if (typeof renderGroupedFiles === 'function' && renderGroupedFiles !== renderGroupedFilesWithDelete) { + originalRenderGroupedFiles = renderGroupedFiles; + window.originalRenderGroupedFiles = renderGroupedFiles; + } + + // Переопределяем глобальные функции + window.renderFileIcon = renderFileIconWithDelete; + window.renderGroupedFiles = renderGroupedFilesWithDelete; + + console.log('✅ Расширенное управление файлами инициализировано'); +} + +/** + * Восстанавливает оригинальные функции рендеринга файлов + */ +function restoreOriginalFileRenderers() { + if (window.originalRenderFileIcon) { + window.renderFileIcon = window.originalRenderFileIcon; + } + if (window.originalRenderGroupedFiles) { + window.renderGroupedFiles = window.originalRenderGroupedFiles; + } +} + +// Экспортируем функции для глобального доступа +window.canUserDeleteFile = canUserDeleteFile; +window.deleteTaskFile = deleteTaskFile; +window.renderFileIconWithDelete = renderFileIconWithDelete; +window.renderGroupedFilesWithDelete = renderGroupedFilesWithDelete; +window.initializeFileManagement = initializeFileManagement; +window.restoreOriginalFileRenderers = restoreOriginalFileRenderers; \ No newline at end of file diff --git a/public/ui.js b/public/ui.js index a803458..1d39a79 100644 --- a/public/ui.js +++ b/public/ui.js @@ -118,7 +118,7 @@ function renderTasks() {
Файлы: - ${task.files && task.files.length > 0 ? renderGroupedFiles(task) : 'нет файлов'} + ${task.files && task.files.length > 0 ? renderGroupedFilesWithDelete(task) : 'нет файлов'}
@@ -807,25 +807,25 @@ function closeAddFileModal() { async function loadTaskFiles(taskId) { try { const response = await fetch(`/api/tasks/${taskId}/files`); - const allFiles = await response.json(); // Получаем ВСЕ файлы + const allFiles = await response.json(); - // Получаем задачу const taskIndex = tasks.findIndex(t => t.id === taskId); if (taskIndex === -1) { console.error('Задача не найдена:', taskId); return; } - // Сохраняем ВСЕ файлы в задаче (не фильтруем здесь!) tasks[taskIndex].files = allFiles; - // Обновляем отображение файлов с помощью renderGroupedFiles - // Она сама отфильтрует что показывать const fileContainer = document.getElementById(`files-${taskId}`); if (fileContainer) { fileContainer.innerHTML = ` Файлы: - ${allFiles.length > 0 ? renderGroupedFiles(tasks[taskIndex]) : 'нет файлов'} + ${allFiles.length > 0 ? + (typeof renderGroupedFilesWithDelete === 'function' ? + renderGroupedFilesWithDelete(tasks[taskIndex]) : + renderGroupedFiles(tasks[taskIndex])) : + 'нет файлов'} `; } } catch (error) { diff --git a/server.js b/server.js index 3e9e1ac..9102940 100644 --- a/server.js +++ b/server.js @@ -1183,6 +1183,189 @@ app.post('/api/groups', requireAuth, (req, res) => { res.json({ success: true, id: this.lastID }); }); }); + +// API для удаления файла из задачи +app.delete('/api/tasks/:taskId/files/:fileId', requireAuth, (req, res) => { + const { taskId, fileId } = req.params; + const userId = req.session.user.id; + const userRole = req.session.user.role; + + // Получаем информацию о файле и задаче + db.get(` + SELECT tf.*, t.created_by, t.id as task_id + FROM task_files tf + JOIN tasks t ON tf.task_id = t.id + WHERE tf.id = ? AND tf.task_id = ? + `, [fileId, taskId], (err, file) => { + if (err) { + console.error('❌ Ошибка при поиске файла:', err); + return res.status(500).json({ error: err.message }); + } + + if (!file) { + return res.status(404).json({ error: 'Файл не найден' }); + } + + // Проверяем права на удаление + const canDelete = + userRole === 'admin' || + userRole === 'tasks' || + parseInt(file.created_by) === parseInt(userId) || // Автор задачи + parseInt(file.user_id) === parseInt(userId); // Загрузивший файл + + if (!canDelete) { + return res.status(403).json({ error: 'У вас нет прав для удаления этого файла' }); + } + + // Удаляем файл из базы данных + db.run('DELETE FROM task_files WHERE id = ?', [fileId], function(deleteErr) { + if (deleteErr) { + console.error('❌ Ошибка удаления файла из БД:', deleteErr); + return res.status(500).json({ error: deleteErr.message }); + } + + // Удаляем физический файл с диска + if (file.file_path && fs.existsSync(file.file_path)) { + try { + fs.unlinkSync(file.file_path); + console.log(`✅ Файл ${file.file_path} удален с диска`); + } catch (unlinkErr) { + console.error('⚠️ Ошибка удаления файла с диска:', unlinkErr); + // Не возвращаем ошибку, так как запись в БД уже удалена + } + } + + // Логируем действие + const { logActivity } = require('./database'); + if (logActivity) { + logActivity(taskId, userId, 'FILE_DELETED', `Удален файл: ${file.original_name || fileId}`); + } + + console.log(`✅ Файл ${fileId} удален из задачи ${taskId}`); + res.json({ + success: true, + message: 'Файл успешно удален', + file_id: fileId + }); + }); + }); +}); + +// API для массового удаления файлов +app.delete('/api/tasks/:taskId/files/batch-delete', requireAuth, (req, res) => { + const { taskId } = req.params; + const { file_ids } = req.body; + const userId = req.session.user.id; + const userRole = req.session.user.role; + + if (!file_ids || !Array.isArray(file_ids) || file_ids.length === 0) { + return res.status(400).json({ error: 'Не указаны файлы для удаления' }); + } + + // Проверяем существование задачи + db.get('SELECT created_by FROM tasks WHERE id = ?', [taskId], (err, task) => { + if (err || !task) { + return res.status(404).json({ error: 'Задача не найдена' }); + } + + // Создаем плейсхолдеры для SQL запроса + const placeholders = file_ids.map(() => '?').join(','); + + // Получаем информацию о файлах + db.all(` + SELECT id, file_path, original_name, user_id, created_by + FROM task_files + WHERE id IN (${placeholders}) AND task_id = ? + `, [...file_ids, taskId], async (err, files) => { + if (err) { + console.error('❌ Ошибка при поиске файлов:', err); + return res.status(500).json({ error: err.message }); + } + + // Проверяем права для каждого файла + const canDeleteAll = files.every(file => { + return userRole === 'admin' || + userRole === 'tasks' || + parseInt(task.created_by) === parseInt(userId) || // Автор задачи + parseInt(file.user_id) === parseInt(userId); // Загрузивший файл + }); + + if (!canDeleteAll) { + return res.status(403).json({ error: 'У вас нет прав для удаления некоторых файлов' }); + } + + const deletedIds = []; + const failedIds = []; + + // Удаляем файлы по одному + for (const file of files) { + try { + // Удаляем запись из БД + await new Promise((resolve, reject) => { + db.run('DELETE FROM task_files WHERE id = ?', [file.id], function(deleteErr) { + if (deleteErr) reject(deleteErr); + else resolve(); + }); + }); + + // Удаляем физический файл + if (file.file_path && fs.existsSync(file.file_path)) { + try { + fs.unlinkSync(file.file_path); + } catch (unlinkErr) { + console.error(`⚠️ Ошибка удаления файла ${file.file_path}:`, unlinkErr); + } + } + + deletedIds.push(file.id); + } catch (deleteErr) { + console.error(`❌ Ошибка удаления файла ${file.id}:`, deleteErr); + failedIds.push(file.id); + } + } + + // Логируем действие + const { logActivity } = require('./database'); + if (logActivity && deletedIds.length > 0) { + logActivity(taskId, userId, 'FILES_DELETED', `Удалено файлов: ${deletedIds.length}`); + } + + res.json({ + success: true, + deleted_count: deletedIds.length, + deleted_ids: deletedIds, + failed_ids: failedIds, + message: `Успешно удалено ${deletedIds.length} файлов` + }); + }); + }); +}); +// API для получения информации о файле +app.get('/api/files/:fileId', requireAuth, (req, res) => { + const { fileId } = req.params; + + db.get(` + SELECT tf.*, t.created_by, t.id as task_id + FROM task_files tf + JOIN tasks t ON tf.task_id = t.id + WHERE tf.id = ? + `, [fileId], (err, file) => { + if (err) { + return res.status(500).json({ error: err.message }); + } + if (!file) { + return res.status(404).json({ error: 'Файл не найден' }); + } + + const { checkTaskAccess } = require('./database'); + checkTaskAccess(req.session.user.id, file.task_id, (err, hasAccess) => { + if (err || !hasAccess) { + return res.status(403).json({ error: 'Нет доступа к файлу' }); + } + res.json(file); + }); + }); +}); // Инициализация сервера async function initializeServer() { console.log('🚀 Инициализация сервера...');