удалить
This commit is contained in:
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;
|
||||
Reference in New Issue
Block a user