Files
minicrm/public/tasks_files.js
2026-02-13 00:57:50 +05:00

619 lines
22 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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;