удалить

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

619
public/tasks_files.js Normal file
View File

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