удалить

This commit is contained in:
2026-02-13 00:57:50 +05:00
parent 6cf551a8bf
commit f44553d4a5
7 changed files with 813 additions and 1023 deletions

View File

@@ -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 = '<p class="no-users">Нет пользователей в группе "help"</p>';
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 `
<div class="help-user-item ${isAssigned ? 'assigned' : 'not-assigned'}">
<i class="fas ${isAssigned ? 'fa-user-check' : 'fa-user'}"></i>
<span>${user.name} (${user.email})</span>
${isAssigned ? '<span class="assigned-badge">назначен</span>' : ''}
</div>
`;
}).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 => `
<div class="help-user-item">
<i class="fas fa-user"></i>
<span>${user.name} (${user.email})</span>
</div>
`).join('');
}

View File

@@ -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 = '<option value="">Все заказчики</option>';
assigneeFilter.innerHTML = '<option value="">Все исполнители (группа "help")</option>';
// Для заказчиков показываем всех пользователей
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 = '<div class="help-group-notice"><i class="fas fa-exclamation-triangle"></i> Нет пользователей в группе "help"</div>';
return;
}
container.innerHTML = `
<div class="help-group-notice">
<i class="fas fa-info-circle"></i> Заявка будет автоматически назначена ${helpUsers.length} пользователям группы "help":
</div>
<div class="help-users-list">
${helpUsers.map(user => `
<div class="help-user-item">
<i class="fas fa-user"></i>
<span class="help-user-name">${user.name}</span>
<span class="help-user-email">(${user.email})</span>
</div>
`).join('')}
</div>
`;
}
// Старые функции фильтрации оставляем, но они теперь не используются для выбора исполнителей
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);
}
}

View File

@@ -1,264 +0,0 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>School CRM - поддержка</title>
<link rel="stylesheet" href="style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
</head>
<body>
<div id="login-modal" class="modal">
<div class="modal-content">
<h2><i class="fas fa-sign-in-alt"></i> Вход в School CRM</h2>
<form id="login-form">
<div class="form-group">
<label for="login"><i class="fas fa-user"></i> Логин:</label>
<input type="text" id="login" name="login" required>
</div>
<div class="form-group">
<label for="password"><i class="fas fa-lock"></i> Пароль:</label>
<input type="password" id="password" name="password" required>
</div>
<button type="submit" class="btn-primary">
<i class="fas fa-sign-in-alt"></i> Войти
</button>
</form>
<div class="test-users">
<h3><i class="fas fa-users"></i> Группа поддержки "help"</h3>
<ul>
<li><strong><i class="fas fa-school"></i> @2025</strong> МАОУ - СОШ № 25</li>
</ul>
</div>
</div>
</div>
<div class="container">
<header>
<div class="header-top">
<h1><i class="fas fa-file-signature"></i> School CRM - система заявок</h1>
<div class="user-info">
<span id="current-user"></span>
<button onclick="logout()" class="btn-logout">
<i class="fas fa-sign-out-alt"></i> Выйти
</button>
</div>
</div>
<nav>
<button onclick="window.location.href = '/'" class="nav-btn btn-admin"><i class="fas fa-cog"></i> Главная</button>
<button onclick="showSection('create-task')" class="nav-btn doc"><i class="fas fa-plus-circle"></i> заявка в ИТ</button>
<button onclick="showSection('create-task')" class="nav-btn"><i class="fas fa-plus-circle"></i> заявка в АХЧ</button>
<button onclick="showSection('create-task')" class="nav-btn"><i class="fas fa-plus-circle"></i> заявка психологу</button>
<button onclick="showSection('create-task')" class="nav-btn"><i class="fas fa-plus-circle"></i> заявка логопеду</button>
<button onclick="showSection('create-task')" class="nav-btn"><i class="fas fa-plus-circle"></i> заявка кадры</button>
<button onclick="showSection('create-task')" class="nav-btn kanban"><i class="fas fa-plus-circle"></i> заявка на получение справки</button>
<button onclick="showSection('create-task')" class="nav-btn"><i class="fas fa-plus-circle"></i> заявка электронный журнал</button>
<!--
<button onclick="showSection('tasks')" class="nav-btn"><i class="fas fa-list"></i> Мои заявки</button>
-->
<button onclick="showSection('create-task')" class="nav-btn admin"><i class="fas fa-plus-circle"></i> отчисление ученика</button>
</nav>
</header>
<main>
<section id="tasks-section" class="section">
<h2><i class="fas fa-file-signature"></i> Все заявки</h2>
<div id="tasks-controls">
<div class="filters">
<!-- Фильтры остаются -->
</div>
<label class="show-deleted-label" style="display: none;">
<input type="checkbox" id="show-deleted" onchange="loadTasks()">
<i class="fas fa-trash"></i> Показать удаленные заявки
</label>
</div>
<div id="tasks-list"></div>
</section>
<section id="create-task-section" class="section">
<h2><i class="fas fa-plus-circle"></i> Создать новую заявку</h2>
<form id="create-task-form" enctype="multipart/form-data">
<div class="form-group">
<label for="title"><i class="fas fa-heading"></i> Название заявки:</label>
<input type="text" id="title" name="title" required>
</div>
<div class="form-group">
<label for="description"><i class="fas fa-align-left"></i> Описание:</label>
<textarea id="description" name="description" rows="4"></textarea>
</div>
<div class="form-group">
<label for="due-date"><i class="fas fa-calendar-alt"></i> Дата и время выполнения:</label>
<input type="datetime-local" id="due-date" name="dueDate" required>
</div>
<div class="form-group">
<label><i class="fas fa-users"></i> Исполнители:</label>
<div class="help-group-notice">
<i class="fas fa-info-circle"></i> Заявка автоматически будет назначена всем пользователям группы "поддержка"
</div>
<div id="help-group-users" class="help-group-users">
<!-- Список пользователей группы help будет загружен динамически -->
</div>
</div>
<div class="form-group">
<label for="files"><i class="fas fa-paperclip"></i> Прикрепить файлы (до 15 файлов, максимум 300MB):</label>
<div class="file-upload">
<input type="file" id="files" name="files" multiple>
<label for="files" class="file-upload-label">
<i class="fas fa-cloud-upload-alt"></i> Выберите файлы
</label>
</div>
<div id="file-list"></div>
</div>
<button type="submit" class="btn-primary">
<i class="fas fa-check-circle"></i> Создать заявку
</button>
</form>
</section>
</main>
</div>
<!-- Модальные окна - тексты изменены с "согласование" на "заявка" -->
<div id="edit-task-modal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeEditModal()">&times;</span>
<h3><i class="fas fa-edit"></i> Редактировать заявку</h3>
<form id="edit-task-form" enctype="multipart/form-data">
<input type="hidden" id="edit-task-id">
<div class="form-group">
<label for="edit-title">Название заявки:</label>
<input type="text" id="edit-title" name="title" required>
</div>
<div class="form-group">
<label for="edit-description">Описание:</label>
<textarea id="edit-description" name="description" rows="4"></textarea>
</div>
<div class="form-group">
<label for="edit-due-date">Дата и время выполнения:</label>
<input type="datetime-local" id="edit-due-date" name="dueDate" required>
</div>
<div class="form-group">
<label>Исполнители (группа "help"):</label>
<div id="edit-help-group-users" class="help-group-users">
<!-- Список пользователей группы help будет загружен динамически -->
</div>
<small style="color: #666; display: block; margin-top: 5px;">
<i class="fas fa-info-circle"></i> Заявка автоматически назначается всем пользователям группы "help"
</small>
</div>
<div class="form-group">
<label for="edit-files">Добавить файлы:</label>
<input type="file" id="edit-files" name="files" multiple>
<div id="edit-file-list"></div>
</div>
<button type="submit" class="btn-primary">
<i class="fas fa-save"></i> Сохранить изменения
</button>
</form>
</div>
</div>
<div id="copy-task-modal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeCopyModal()">&times;</span>
<h3><i class="fas fa-copy"></i> Создать копию заявки</h3>
<form id="copy-task-form">
<input type="hidden" id="copy-task-id">
<div class="form-group">
<label for="copy-due-date">Дата и время выполнения для копии:</label>
<input type="datetime-local" id="copy-due-date" name="dueDate" required>
</div>
<div class="form-group">
<label>Исполнители для копии:</label>
<div class="help-group-notice">
<i class="fas fa-info-circle"></i> Копия заявки автоматически будет назначена всем пользователям группы "help"
</div>
</div>
<button type="submit" class="btn-primary">
<i class="fas fa-copy"></i> Создать копию
</button>
</form>
</div>
</div>
<div id="edit-assignment-modal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeEditAssignmentModal()">&times;</span>
<h3><i class="fas fa-clock"></i> Редактировать сроки исполнителя</h3>
<form id="edit-assignment-form">
<input type="hidden" id="edit-assignment-task-id">
<input type="hidden" id="edit-assignment-user-id">
<div class="form-group">
<label for="edit-assignment-due-date">Дата и время выполнения:</label>
<input type="datetime-local" id="edit-assignment-due-date" name="dueDate" required>
</div>
<button type="submit" class="btn-primary">
<i class="fas fa-save"></i> Сохранить сроки
</button>
</form>
</div>
</div>
<div id="rework-task-modal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeReworkModal()">&times;</span>
<h3><i class="fas fa-redo"></i> Вернуть заявку на доработку</h3>
<form id="rework-task-form">
<input type="hidden" id="rework-task-id">
<div class="form-group">
<label for="rework-comment">Комментарий к доработке:</label>
<textarea id="rework-comment" name="comment" rows="4" placeholder="Укажите, что нужно исправить..." required></textarea>
</div>
<button type="submit" class="btn-warning">
<i class="fas fa-redo"></i> Вернуть на доработку
</button>
</form>
</div>
</div>
<div id="kanban-section" class="section kanban-section">
<div class="section-header">
<h2><i class="fas fa-columns"></i> Канбан-доска заявок</h2>
<p>Перетаскивайте заявки между колонками для изменения статуса</p>
<div class="kanban-controls">
<div class="kanban-filters">
<select id="kanban-filter" onchange="loadKanbanBoard()">
<option value="all">Все заявки</option>
<option value="created">Мои заявки (я создал)</option>
<option value="assigned">Назначенные мне (группа "help")</option>
</select>
<select id="kanban-days" onchange="loadKanbanBoard()">
<option value="7">7 дней</option>
<option value="14">14 дней</option>
<option value="30">30 дней</option>
<option value="365">Все заявки</option>
</select>
</div>
</div>
</div>
<div id="kanban-board" class="kanban-board">
<div class="loading">Загрузка Канбан-доски...</div>
</div>
</div>
<script src="auth.js"></script>
<script src="help-users.js"></script>
<script src="help-tasks.js"></script>
<script src="kanban.js"></script>
<script src="files.js"></script>
<script src="ui.js"></script>
<script src="main.js"></script>
</body>
</html>

View File

@@ -373,6 +373,7 @@
</div>
</div>
<script src="navbar.js"></script>
<script src="auth.js"></script>
<script src="users.js"></script>
<script src="tasks.js"></script>
@@ -383,6 +384,7 @@
<script src="time-selector.js"></script>
<script src="openTaskChat.js"></script>
<script src="main.js"></script>
<script src="navbar.js"></script>
<script src="tasks_files.js"></script>
</body>
</html>

619
public/tasks_files.js Normal file
View File

@@ -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 = `
<strong>Файлы:</strong>
${task.files && task.files.length > 0 ?
renderGroupedFilesWithDelete(task) :
'<span class="no-files">нет файлов</span>'}
`;
}
// Перезагружаем задачи для синхронизации
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 = `
<div class="file-icon-wrapper" data-file-id="${file.id}" data-task-id="${task.id}" style="display: flex; align-items: center;">
<a href="/api/files/${file.id}/download"
download="${encodeURIComponent(fileName)}"
class="file-icon-container"
title="${fileName} (${fileSize} MB) - Загрузил: ${uploadedBy}">
<div class="file-icon" style="background: ${iconColor}">
<span class="file-extension ${textClass}">${iconText}</span>
</div>
<div class="file-name">${displayFileName}</div>
</a>
`;
// Добавляем кнопку удаления справа от файла, вертикальную
if (canDelete) {
html += `
<div style="display: flex; flex-direction: column; margin-left: 8px;">
<button class="deadline-badge deadline-24h"
onclick="event.preventDefault(); event.stopPropagation(); deleteTaskFile(${file.id}, ${task.id}); return false;"
title="Удалить файл"
style="writing-mode: vertical-rl;
transform: rotate(180deg);
height: auto;
min-height: 60px;
padding: 8px 4px;
background: red;
font-size: 0.85em;
border: none;
border-radius: 4px;
color: white;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;">
Удалить
</button>
</div>
`;
}
html += `</div>`;
return html;
}
/**
* Рендерит группированные файлы с поддержкой удаления
* @param {Object} task - Объект задачи
* @returns {string} HTML строка
*/
function renderGroupedFilesWithDelete(task) {
if (!task.files || task.files.length === 0) {
return '<span class="no-files">нет файлов</span>';
}
// Группируем файлы по пользователю
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 '<span class="no-files">нет файлов</span>';
}
// Рендерим группы
if (visibleGroups.length === 1) {
const uploader = visibleGroups[0];
return `
<div class="file-group single-user">
<div class="file-group-header">
<strong>${escapeHtml(uploader.name)}:</strong>
</div>
<div class="file-icons-container">
${uploader.files.map(file =>
renderFileIconWithDelete(file, task.id, task)
).join('')}
</div>
</div>
`;
}
return visibleGroups.map(uploader => `
<div class="file-group">
<div class="file-group-header">
<strong>${escapeHtml(uploader.name)}:</strong>
</div>
<div class="file-icons-container">
${uploader.files.map(file =>
renderFileIconWithDelete(file, task.id, task)
).join('')}
</div>
</div>
`).join('');
}
/**
* Экранирует HTML специальные символы
* @param {string} text - Текст для экранирования
* @returns {string} Экранированный текст
*/
function escapeHtml(text) {
if (!text) return '';
return String(text)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
/**
* Инициализирует расширенные функции работы с файлами
*/
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;

View File

@@ -118,7 +118,7 @@ function renderTasks() {
<div class="file-list" id="files-${task.id}">
<strong>Файлы:</strong>
${task.files && task.files.length > 0 ? renderGroupedFiles(task) : '<span class="no-files">нет файлов</span>'}
${task.files && task.files.length > 0 ? renderGroupedFilesWithDelete(task) : '<span class="no-files">нет файлов</span>'}
</div>
@@ -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 = `
<strong>Файлы:</strong>
${allFiles.length > 0 ? renderGroupedFiles(tasks[taskIndex]) : '<span class="no-files">нет файлов</span>'}
${allFiles.length > 0 ?
(typeof renderGroupedFilesWithDelete === 'function' ?
renderGroupedFilesWithDelete(tasks[taskIndex]) :
renderGroupedFiles(tasks[taskIndex])) :
'<span class="no-files">нет файлов</span>'}
`;
}
} catch (error) {

183
server.js
View File

@@ -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('🚀 Инициализация сервера...');