// tasks_files.js - Расширенное управление файлами задач // Функции для удаления файлов, загрузки и управления доступом // Сохраняем ссылку на оригинальную функцию при загрузке скрипта let originalRenderFileIcon = null; let originalRenderGroupedFiles = null; /** * Проверяет, может ли пользователь удалить файл * @param {Object} file - Объект файла * @param {Object} task - Объект задачи * @returns {boolean} - true если может удалить */ function canUserDeleteFile(file, task) { if (!currentUser) return false; // Администратор может удалять любые файлы if (currentUser.role === 'admin') return true; // Пользователи с ролью 'tasks' могут удалять любые файлы if (currentUser.role === 'tasks') return true; // Автор задачи может удалять любые файлы в своей задаче if (task && parseInt(task.created_by) === currentUser.id) return true; // Пользователь может удалять только свои файлы if (file && parseInt(file.user_id) === currentUser.id) return true; return false; } /** * Удаляет файл из задачи * @param {number} fileId - ID файла * @param {number} taskId - ID задачи */ async function deleteTaskFile(fileId, taskId) { if (!confirm('Вы уверены, что хотите удалить этот файл?')) { return; } // Находим задачу и файл для проверки прав const task = tasks.find(t => t.id === taskId); if (!task) { alert('Задача не найдена'); return; } // Находим файл в текущих данных let file = null; if (task.files) { file = task.files.find(f => f.id === fileId); } if (!file) { // Пробуем загрузить файл отдельно try { const response = await fetch(`/api/files/${fileId}`); if (response.ok) { file = await response.json(); } } catch (error) { console.error('Ошибка загрузки данных файла:', error); } } // Проверяем права на удаление if (!canUserDeleteFile(file || { user_id: 0 }, task)) { alert('У вас нет прав для удаления этого файла'); return; } try { const response = await fetch(`/api/tasks/${taskId}/files/${fileId}`, { method: 'DELETE', headers: { 'Content-Type': 'application/json' } }); if (response.ok) { alert('✅ Файл успешно удален'); // Обновляем список файлов в задаче if (task.files) { task.files = task.files.filter(f => f.id !== fileId); } // Обновляем отображение const fileContainer = document.getElementById(`files-${taskId}`); if (fileContainer) { fileContainer.innerHTML = ` Файлы: ${task.files && task.files.length > 0 ? renderGroupedFilesWithDelete(task) : 'нет файлов'} `; } // Перезагружаем задачи для синхронизации if (typeof loadTasks === 'function') { loadTasks(); } } else { const error = await response.json(); alert(`❌ Ошибка: ${error.error || 'Неизвестная ошибка'}`); } } catch (error) { console.error('❌ Ошибка удаления файла:', error); alert('Сетевая ошибка при удалении файла'); } } /** * Рендерит иконку файла с кнопкой удаления * @param {Object} file - Объект файла * @param {number} taskId - ID задачи * @param {Object} task - Объект задачи (опционально) * @returns {string} HTML строка */ /** * Рендерит иконку файла с кнопкой удаления * @param {Object} file - Объект файла * @param {number} taskId - ID задачи * @param {Object} task - Объект задачи (опционально) * @returns {string} HTML строка */ function renderFileIconWithDelete(file, taskId, task) { // Получаем задачу, если не передана if (!task && taskId) { task = tasks.find(t => t.id === taskId); } // Исправляем кодировку имени файла const fixEncoding = (str) => { if (!str) return ''; try { if (str.includes('Ð') || str.includes('Ñ')) { return decodeURIComponent(escape(str)); } return str; } catch (e) { return str; } }; const fileName = fixEncoding(file.original_name); const fileSize = (file.file_size / 1024 / 1024).toFixed(2); const uploadedBy = file.user_name; // --- ПОЛНАЯ ЛОГИКА ОПРЕДЕЛЕНИЯ ЦВЕТА И ИКОНКИ ИЗ ОРИГИНАЛЬНОЙ renderFileIcon --- let iconColor = ''; let iconText = ''; let textClass = ''; // Определяем расширение файла const extension = fileName.includes('.') ? fileName.split('.').pop().toLowerCase() : ''; // Определяем тип файла на основе расширения if (extension) { switch (extension) { case 'pdf': iconColor = '#e74c3c'; iconText = 'PDF'; textClass = 'short'; break; case 'doc': iconColor = '#3498db'; iconText = 'DOC'; textClass = 'short'; break; case 'docx': iconColor = '#3498db'; iconText = 'DOCX'; textClass = 'medium'; break; case 'xls': iconColor = '#2ecc71'; iconText = 'XLS'; textClass = 'short'; break; case 'xlsx': iconColor = '#2ecc71'; iconText = 'XLSX'; textClass = 'medium'; break; case 'csv': iconColor = '#2ecc71'; iconText = 'CSV'; textClass = 'short'; break; case 'ppt': iconColor = '#e67e22'; iconText = 'PPT'; textClass = 'short'; break; case 'pptx': iconColor = '#e67e22'; iconText = 'PPTX'; textClass = 'medium'; break; case 'zip': iconColor = '#f39c12'; iconText = 'ZIP'; textClass = 'short'; break; case 'rar': iconColor = '#f39c12'; iconText = 'RAR'; textClass = 'short'; break; case '7z': iconColor = '#f39c12'; iconText = '7Z'; textClass = 'short'; break; case 'tar': iconColor = '#f39c12'; iconText = 'TAR'; textClass = 'short'; break; case 'gz': iconColor = '#f39c12'; iconText = 'GZ'; textClass = 'short'; break; case 'txt': iconColor = '#95a5a6'; iconText = 'TXT'; textClass = 'short'; break; case 'log': iconColor = '#95a5a6'; iconText = 'LOG'; textClass = 'short'; break; case 'md': iconColor = '#95a5a6'; iconText = 'MD'; textClass = 'short'; break; case 'jpg': iconColor = '#9b59b6'; iconText = 'JPG'; textClass = 'short'; break; case 'jpeg': iconColor = '#9b59b6'; iconText = 'JPEG'; textClass = 'medium'; break; case 'png': iconColor = '#9b59b6'; iconText = 'PNG'; textClass = 'short'; break; case 'gif': iconColor = '#9b59b6'; iconText = 'GIF'; textClass = 'short'; break; case 'bmp': iconColor = '#9b59b6'; iconText = 'BMP'; textClass = 'short'; break; case 'svg': iconColor = '#9b59b6'; iconText = 'SVG'; textClass = 'short'; break; case 'webp': iconColor = '#9b59b6'; iconText = 'WEBP'; textClass = 'medium'; break; case 'mp3': iconColor = '#1abc9c'; iconText = 'MP3'; textClass = 'short'; break; case 'wav': iconColor = '#1abc9c'; iconText = 'WAV'; textClass = 'short'; break; case 'ogg': iconColor = '#1abc9c'; iconText = 'OGG'; textClass = 'short'; break; case 'flac': iconColor = '#1abc9c'; iconText = 'FLAC'; textClass = 'medium'; break; case 'mp4': iconColor = '#d35400'; iconText = 'MP4'; textClass = 'short'; break; case 'avi': iconColor = '#d35400'; iconText = 'AVI'; textClass = 'short'; break; case 'mkv': iconColor = '#d35400'; iconText = 'MKV'; textClass = 'short'; break; case 'mov': iconColor = '#d35400'; iconText = 'MOV'; textClass = 'short'; break; case 'wmv': iconColor = '#d35400'; iconText = 'WMV'; textClass = 'short'; break; case 'exe': iconColor = '#c0392b'; iconText = 'EXE'; textClass = 'short'; break; case 'msi': iconColor = '#c0392b'; iconText = 'MSI'; textClass = 'short'; break; case 'js': iconColor = '#2980b9'; iconText = 'JS'; textClass = 'short'; break; case 'html': iconColor = '#2980b9'; iconText = 'HTML'; textClass = 'medium'; break; case 'css': iconColor = '#2980b9'; iconText = 'CSS'; textClass = 'short'; break; case 'php': iconColor = '#2980b9'; iconText = 'PHP'; textClass = 'short'; break; case 'py': iconColor = '#2980b9'; iconText = 'PY'; textClass = 'short'; break; case 'java': iconColor = '#2980b9'; iconText = 'JAVA'; textClass = 'medium'; break; case 'json': iconColor = '#8e44ad'; iconText = 'JSON'; textClass = 'medium'; break; case 'xml': iconColor = '#8e44ad'; iconText = 'XML'; textClass = 'short'; break; case 'yml': iconColor = '#8e44ad'; iconText = 'YML'; textClass = 'short'; break; case 'yaml': iconColor = '#8e44ad'; iconText = 'YAML'; textClass = 'medium'; break; case 'sql': iconColor = '#27ae60'; iconText = 'SQL'; textClass = 'short'; break; case 'db': iconColor = '#27ae60'; iconText = 'DB'; textClass = 'short'; break; case 'sqlite': iconColor = '#27ae60'; iconText = 'SQLITE'; textClass = 'long'; break; default: // Для других расширений используем расширение или первые 4 символа iconColor = '#7f8c8d'; iconText = extension.length > 4 ? extension.substring(0, 4).toUpperCase() : extension.toUpperCase(); // Определяем класс по длине текста if (iconText.length <= 2) { textClass = 'short'; } else if (iconText.length <= 4) { textClass = 'medium'; } else { textClass = 'long'; } } } else { // Если нет расширения iconColor = '#7f8c8d'; iconText = 'ФАЙЛ'; textClass = 'short'; } // --- КОНЕЦ ЛОГИКИ ОПРЕДЕЛЕНИЯ ЦВЕТА --- const displayFileName = truncateFileName(fileName); const canDelete = task ? canUserDeleteFile(file, task) : false; // Создаем контейнер с flex-расположением let html = `
${iconText}
${displayFileName}
`; // Добавляем кнопку удаления справа от файла, вертикальную if (canDelete) { html += `
`; } html += `
`; return html; } /** * Рендерит группированные файлы с поддержкой удаления * @param {Object} task - Объект задачи * @returns {string} HTML строка */ function renderGroupedFilesWithDelete(task) { if (!task.files || task.files.length === 0) { return 'нет файлов'; } // Группируем файлы по пользователю const filesByUploader = {}; task.files.forEach(file => { const uploaderId = file.user_id; const uploaderName = file.user_name || 'Неизвестный пользователь'; if (!filesByUploader[uploaderId]) { filesByUploader[uploaderId] = { name: uploaderName, id: uploaderId, files: [] }; } filesByUploader[uploaderId].files.push(file); }); // Определяем видимые группы const visibleGroups = []; for (const uploaderId in filesByUploader) { const uploaderGroup = filesByUploader[uploaderId]; const uploaderIdNum = parseInt(uploaderId); let canSeeThisUploader = false; if (currentUser.role === 'admin' || currentUser.role === 'tasks' || parseInt(task.created_by) === currentUser.id) { canSeeThisUploader = true; } else { const creatorId = parseInt(task.created_by); if (uploaderIdNum === creatorId || uploaderIdNum === currentUser.id) { canSeeThisUploader = true; } } if (canSeeThisUploader) { visibleGroups.push({ name: uploaderGroup.name, id: uploaderGroup.id, files: uploaderGroup.files }); } } if (visibleGroups.length === 0) { return 'нет файлов'; } // Рендерим группы if (visibleGroups.length === 1) { const uploader = visibleGroups[0]; return `
${escapeHtml(uploader.name)}:
${uploader.files.map(file => renderFileIconWithDelete(file, task.id, task) ).join('')}
`; } return visibleGroups.map(uploader => `
${escapeHtml(uploader.name)}:
${uploader.files.map(file => renderFileIconWithDelete(file, task.id, task) ).join('')}
`).join(''); } /** * Экранирует HTML специальные символы * @param {string} text - Текст для экранирования * @returns {string} Экранированный текст */ function escapeHtml(text) { if (!text) return ''; return String(text) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } /** * Инициализирует расширенные функции работы с файлами */ function initializeFileManagement() { console.log('📁 Инициализация расширенного управления файлами...'); // Сохраняем ссылки на оригинальные функции if (typeof renderFileIcon === 'function' && renderFileIcon !== renderFileIconWithDelete) { originalRenderFileIcon = renderFileIcon; window.originalRenderFileIcon = renderFileIcon; } if (typeof renderGroupedFiles === 'function' && renderGroupedFiles !== renderGroupedFilesWithDelete) { originalRenderGroupedFiles = renderGroupedFiles; window.originalRenderGroupedFiles = renderGroupedFiles; } // Переопределяем глобальные функции window.renderFileIcon = renderFileIconWithDelete; window.renderGroupedFiles = renderGroupedFilesWithDelete; console.log('✅ Расширенное управление файлами инициализировано'); } /** * Восстанавливает оригинальные функции рендеринга файлов */ function restoreOriginalFileRenderers() { if (window.originalRenderFileIcon) { window.renderFileIcon = window.originalRenderFileIcon; } if (window.originalRenderGroupedFiles) { window.renderGroupedFiles = window.originalRenderGroupedFiles; } } // Экспортируем функции для глобального доступа window.canUserDeleteFile = canUserDeleteFile; window.deleteTaskFile = deleteTaskFile; window.renderFileIconWithDelete = renderFileIconWithDelete; window.renderGroupedFilesWithDelete = renderGroupedFilesWithDelete; window.initializeFileManagement = initializeFileManagement; window.restoreOriginalFileRenderers = restoreOriginalFileRenderers;