удалить
This commit is contained in:
@@ -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('');
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
264
public/help.html
264
public/help.html
@@ -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()">×</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()">×</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()">×</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()">×</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>
|
||||
@@ -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
619
public/tasks_files.js
Normal 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, '&')
|
||||
.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;
|
||||
14
public/ui.js
14
public/ui.js
@@ -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
183
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('🚀 Инициализация сервера...');
|
||||
|
||||
Reference in New Issue
Block a user