619 lines
22 KiB
JavaScript
619 lines
22 KiB
JavaScript
// 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; |