Files
minicrm/public/script.js
2025-12-21 20:42:12 +05:00

1932 lines
74 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.
let currentUser = null;
let users = [];
let tasks = [];
let filteredUsers = [];
let expandedTasks = new Set();
let showingTasksWithoutDate = false;
let kanbanTasks = [];
let kanbanDays = 14;
document.addEventListener('DOMContentLoaded', function() {
checkAuth();
setupEventListeners();
});
async function checkAuth() {
try {
const response = await fetch('/api/user');
if (response.ok) {
const data = await response.json();
currentUser = data.user;
showMainInterface();
} else {
showLoginInterface();
}
} catch (error) {
showLoginInterface();
}
}
function showLoginInterface() {
document.getElementById('login-modal').style.display = 'block';
document.querySelector('.container').style.display = 'none';
}
function showMainInterface() {
document.getElementById('login-modal').style.display = 'none';
document.querySelector('.container').style.display = 'block';
let userInfo = `Вы вошли как: ${currentUser.name}`;
if (currentUser.auth_type === 'ldap') {
userInfo += ` (LDAP)`;
}
if (currentUser.groups && currentUser.groups.length > 0) {
userInfo += ` | Группы: ${currentUser.groups.join(', ')}`;
}
document.getElementById('current-user').textContent = userInfo;
document.getElementById('tasks-controls').style.display = 'block';
const showDeletedLabel = document.querySelector('.show-deleted-label');
if (showDeletedLabel) {
if (currentUser.role === 'admin') {
showDeletedLabel.style.display = 'flex';
} else {
showDeletedLabel.style.display = 'none';
}
}
loadUsers();
loadTasks();
loadActivityLogs();
showSection('tasks');
loadKanbanTasks();
showingTasksWithoutDate = false;
const btn = document.getElementById('tasks-no-date-btn');
if (btn) btn.classList.remove('active');
}
function setupEventListeners() {
document.getElementById('login-form').addEventListener('submit', login);
document.getElementById('create-task-form').addEventListener('submit', createTask);
document.getElementById('edit-task-form').addEventListener('submit', updateTask);
document.getElementById('copy-task-form').addEventListener('submit', copyTask);
document.getElementById('edit-assignment-form').addEventListener('submit', updateAssignment);
document.getElementById('rework-task-form').addEventListener('submit', sendForRework);
document.getElementById('files').addEventListener('change', updateFileList);
document.getElementById('edit-files').addEventListener('change', updateEditFileList);
}
async function login(event) {
event.preventDefault();
const login = document.getElementById('login').value;
const password = document.getElementById('password').value;
try {
const response = await fetch('/api/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ login, password })
});
if (response.ok) {
const data = await response.json();
currentUser = data.user;
showMainInterface();
} else {
const error = await response.json();
alert(error.error || 'Ошибка входа');
}
} catch (error) {
console.error('Ошибка:', error);
alert('Ошибка подключения к серверу');
}
}
async function logout() {
try {
await fetch('/api/logout', { method: 'POST' });
currentUser = null;
showLoginInterface();
} catch (error) {
console.error('Ошибка выхода:', error);
}
}
function showSection(sectionName) {
document.querySelectorAll('.section').forEach(section => {
section.classList.remove('active');
});
document.getElementById(sectionName + '-section').classList.add('active');
if (sectionName === 'tasks') {
loadTasks();
} else if (sectionName === 'logs') {
loadActivityLogs();
}
if (sectionName === 'kanban') {
loadKanbanTasks();
}
}
async function loadUsers() {
try {
const response = await fetch('/api/users');
users = await response.json();
filteredUsers = [...users];
renderUsersChecklist();
renderEditUsersChecklist();
renderCopyUsersChecklist();
populateFilterDropdowns();
} catch (error) {
console.error('Ошибка загрузки пользователей:', error);
}
}
function populateFilterDropdowns() {
const creatorFilter = document.getElementById('creator-filter');
const assigneeFilter = document.getElementById('assignee-filter');
creatorFilter.innerHTML = '<option value="">Все заказчики</option>';
assigneeFilter.innerHTML = '<option value="">Все исполнители</option>';
users.forEach(user => {
const creatorOption = document.createElement('option');
creatorOption.value = user.id;
creatorOption.textContent = `${user.name} (${user.login})`;
creatorFilter.appendChild(creatorOption.cloneNode(true));
const assigneeOption = creatorOption.cloneNode(true);
assigneeFilter.appendChild(assigneeOption);
});
}
function showKanbanSection() {
showSection('kanban');
}
async function loadKanbanTasks() {
try {
const daysSelect = document.getElementById('kanban-days');
const filterSelect = document.getElementById('kanban-filter');
// Если есть выбор в интерфейсе - используем его, иначе - значение по умолчанию
if (daysSelect) {
kanbanDays = parseInt(daysSelect.value) || 14;
} else {
kanbanDays = 14;
}
let filter = 'all';
if (filterSelect) {
filter = filterSelect.value;
}
const response = await fetch(`/api/kanban-tasks?days=${kanbanDays}&filter=${filter}`);
if (!response.ok) {
throw new Error(`Ошибка сервера: ${response.status}`);
}
const data = await response.json();
kanbanTasks = data.tasks || [];
renderKanban(data.filter);
} catch (error) {
console.error('Ошибка загрузки задач для Канбана:', error);
document.getElementById('kanban-board').innerHTML = `
<div class="error-message">
❌ Ошибка загрузки Канбана: ${error.message}
<button onclick="loadKanbanTasks()" class="retry-btn">Повторить</button>
</div>
`;
}
}
function renderKanban() {
const container = document.getElementById('kanban-board');
// Группируем задачи по статусам
const columns = {
'unassigned': { title: 'Не назначены', tasks: [], color: '#95a5a6' },
'assigned': { title: 'Назначены', tasks: [], color: '#e74c3c' },
'in_progress': { title: 'В работе', tasks: [], color: '#f39c12' },
'rework': { title: 'На доработке', tasks: [], color: '#f1c40f' },
'overdue': { title: 'Просрочены', tasks: [], color: '#c0392b' },
'completed': { title: 'Выполнены', tasks: [], color: '#2ecc71' }
};
// Распределяем задачи по колонкам
kanbanTasks.forEach(task => {
const status = task.kanbanStatus || 'unassigned';
if (columns[status]) {
columns[status].tasks.push(task);
}
});
// Рендерим доску
container.innerHTML = `
<div class="kanban-controls">
<div class="kanban-period">
<label>Период просмотра:</label>
<select id="kanban-days" onchange="loadKanbanTasks()">
${[1, 2, 3, 4, 5, 6, 7, 14].map(days =>
`<option value="${days}" ${days === kanbanDays ? 'selected' : ''}>${days} ${getDayWord(days)}</option>`
).join('')}
</select>
</div>
<div class="kanban-stats">
<span>Всего задач: ${kanbanTasks.length}</span>
<button onclick="loadKanbanTasks()" class="refresh-btn">🔄 Обновить</button>
</div>
</div>
<div class="kanban-columns">
${Object.entries(columns).map(([status, column]) => `
<div class="kanban-column" data-status="${status}">
<div class="kanban-column-header" style="background: ${column.color}">
<h3>${column.title}</h3>
<span class="kanban-count">${column.tasks.length}</span>
</div>
<div class="kanban-column-body" id="kanban-column-${status}">
${renderKanbanCards(column.tasks)}
</div>
</div>
`).join('')}
</div>
`;
// Делаем колонки перетаскиваемыми
makeKanbanDraggable();
}
function renderKanbanCards(tasks, filter) {
if (tasks.length === 0) {
return '<div class="kanban-empty">Нет задач</div>';
}
return tasks.map(task => {
// Определяем иконку роли
let roleIcon = '';
let roleTitle = '';
if (task.userRole === 'creator') {
roleIcon = '👤';
roleTitle = 'Вы поставили эту задачу';
} else if (task.userRole === 'assignee') {
roleIcon = '🎯';
roleTitle = 'Вам поставили эту задачу';
}
// Исправление: безопасное получение имени пользователя
const userName = task.assignments && task.assignments.length > 0 && task.assignments[0]?.user_name
? task.assignments[0].user_name
: 'Неизвестно';
// Исправление: безопасное получение первого символа имени
const userInitial = userName && userName.length > 0 ? userName.charAt(0) : '?';
return `
<div class="kanban-card" draggable="true" data-task-id="${task.id}">
<div class="kanban-card-header">
<div class="kanban-task-id">#${task.id}</div>
<div class="kanban-task-role" title="${roleTitle}">${roleIcon}</div>
<div class="kanban-task-actions">
<button onclick="openKanbanTask(${task.id})" title="Открыть">👁️</button>
<button onclick="copyKanbanTask(${task.id})" title="Копировать">📋</button>
</div>
</div>
<div class="kanban-task-title" onclick="openKanbanTask(${task.id})">
${task.title || 'Без названия'}
</div>
<div class="kanban-task-info">
<div class="kanban-deadline">
${task.due_date ? `<span class="kanban-date">📅 ${formatDate(task.due_date)}</span>` : '<span class="kanban-no-date">Без срока</span>'}
</div>
<div class="kanban-assignees">
${task.assignments && task.assignments.length > 0 ?
task.assignments.slice(0, 3).map(a => {
// Исправление: безопасное получение имени исполнителя
const assigneeName = a.user_name || 'Неизвестно';
const assigneeInitial = assigneeName && assigneeName.length > 0 ? assigneeName.charAt(0) : '?';
return `<span class="kanban-assignee" title="${assigneeName}">${assigneeInitial}</span>`;
}).join('') :
'<span class="kanban-no-assignee">👤</span>'
}
${task.assignments && task.assignments.length > 3 ?
`<span class="kanban-more-assignees">+${task.assignments.length - 3}</span>` : ''
}
</div>
</div>
<div class="kanban-task-footer">
<span class="kanban-creator">👤 ${task.creator_name || 'Неизвестно'}</span>
${task.files && task.files.length > 0 ?
`<span class="kanban-files">📎 ${task.files.length}</span>` : ''
}
</div>
</div>
`;
}).join('');
}
function renderKanban(filter = 'all') {
const container = document.getElementById('kanban-board');
// Группируем задачи по статусам (убрали 'unassigned')
const columns = {
'assigned': { title: 'Назначены', tasks: [], color: '#e74c3c' },
'in_progress': { title: 'В работе', tasks: [], color: '#f39c12' },
'rework': { title: 'На доработке', tasks: [], color: '#f1c40f' },
'overdue': { title: 'Просрочены', tasks: [], color: '#c0392b' },
'completed': { title: 'Выполнены', tasks: [], color: '#2ecc71' }
};
// Распределяем задачи по колонкам
kanbanTasks.forEach(task => {
const status = task.kanbanStatus || 'assigned';
// Преобразуем 'unassigned' в 'assigned'
const actualStatus = status === 'unassigned' ? 'assigned' : status;
if (columns[actualStatus]) {
columns[actualStatus].tasks.push(task);
} else {
// Если статус не найден, добавляем в 'assigned'
columns['assigned'].tasks.push(task);
}
});
// Статистика по фильтру
let filterTitle = 'Все задачи';
if (filter === 'created') filterTitle = 'Задачи, которые я поставил';
if (filter === 'assigned') filterTitle = 'Задачи, которые мне поставили';
container.innerHTML = `
<div class="kanban-controls">
<div class="kanban-filters">
<div class="kanban-period">
<label>Период просмотра:</label>
<select id="kanban-days" onchange="loadKanbanTasks()">
${[1, 2, 3, 4, 5, 6, 7, 14, 30, 62].map(days =>
`<option value="${days}" ${days === kanbanDays ? 'selected' : ''}>${days} ${getDayWord(days)}</option>`
).join('')}
</select>
</div>
<div class="kanban-filter-type">
<label>Показать:</label>
<select id="kanban-filter" onchange="loadKanbanTasks()">
<option value="all" ${filter === 'all' ? 'selected' : ''}>Все задачи</option>
<option value="created" ${filter === 'created' ? 'selected' : ''}>Я поставил</option>
<option value="assigned" ${filter === 'assigned' ? 'selected' : ''}>Мне поставили</option>
</select>
</div>
<div class="kanban-stats">
<span class="filter-title">${filterTitle}</span>
<span class="task-count">Всего задач: ${kanbanTasks.length}</span>
<button onclick="loadKanbanTasks()" class="refresh-btn">🔄 Обновить</button>
</div>
</div>
</div>
<div class="kanban-columns">
${Object.entries(columns).map(([status, column]) => `
<div class="kanban-column" data-status="${status}" ${status === 'overdue' || status === 'assigned' ? 'ondragover="return false" ondrop="return false"' : ''}>
<div class="kanban-column-header" style="background: ${column.color}">
<h3>${column.title}</h3>
<span class="kanban-count">${column.tasks.length}</span>
</div>
<div class="kanban-column-body" id="kanban-column-${status}"
${status === 'overdue' || status === 'assigned' ? 'style="opacity: 0.6; cursor: not-allowed;"' : ''}>
${renderKanbanCards(column.tasks, filter)}
</div>
</div>
`).join('')}
</div>
`;
// Делаем колонки перетаскиваемыми (кроме 'overdue' и 'assigned')
makeKanbanDraggable();
}
function getDayWord(days) {
if (days === 1) return 'день';
if (days >= 2 && days <= 4) return 'дня';
return 'дней';
}
function makeKanbanDraggable() {
const cards = document.querySelectorAll('.kanban-card');
const columns = document.querySelectorAll('.kanban-column-body:not([style*="opacity: 0.6"])');
cards.forEach(card => {
card.addEventListener('dragstart', (e) => {
e.dataTransfer.setData('text/plain', card.dataset.taskId);
card.classList.add('dragging');
});
card.addEventListener('dragend', () => {
card.classList.remove('dragging');
});
});
columns.forEach(column => {
const status = column.parentElement.dataset.status;
// Запрещаем перетаскивание в 'overdue' и 'assigned'
if (status === 'overdue' || status === 'assigned') {
return;
}
column.addEventListener('dragover', (e) => {
e.preventDefault();
const draggingCard = document.querySelector('.dragging');
if (draggingCard) {
column.appendChild(draggingCard);
}
});
column.addEventListener('drop', async (e) => {
e.preventDefault();
const taskId = e.dataTransfer.getData('text/plain');
const newStatus = column.parentElement.dataset.status;
if (taskId) {
try {
// Запрещаем установку статуса 'overdue' и 'assigned'
if (newStatus === 'overdue' || newStatus === 'assigned') {
alert('Невозможно изменить статус задачи на "Просрочены" или "Назначены" через Канбан');
// Возвращаем задачу в исходное положение
loadKanbanTasks();
return;
}
// Обновляем статус на сервере
const response = await fetch(`/api/kanban-tasks/${taskId}/status`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ status: newStatus })
});
if (response.ok) {
// Перезагружаем Канбан
loadKanbanTasks();
} else {
const error = await response.json();
alert(`Ошибка обновления статуса: ${error.error || 'Неизвестная ошибка'}`);
// Возвращаем задачу в исходное положение
loadKanbanTasks();
}
} catch (error) {
console.error('Ошибка обновления статуса:', error);
alert('Ошибка обновления статуса');
loadKanbanTasks();
}
}
});
});
}
function openKanbanTask(taskId) {
// Находим задачу и открываем её в основном интерфейсе
const task = kanbanTasks.find(t => t.id == taskId);
if (task) {
showSection('tasks');
// Прокручиваем к задаче
setTimeout(() => {
const taskElement = document.querySelector(`.task-card[data-task-id="${taskId}"]`);
if (taskElement) {
taskElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
// Раскрываем задачу если она свернута
if (!expandedTasks.has(taskId)) {
toggleTask(taskId);
}
}
}, 100);
}
}
function copyKanbanTask(taskId) {
openCopyModal(taskId);
}
function formatDate(dateString) {
if (!dateString) return '';
const date = new Date(dateString);
return date.toLocaleDateString('ru-RU');
}
function filterUsers() {
const search = document.getElementById('user-search').value.toLowerCase();
filteredUsers = users.filter(user =>
user.name.toLowerCase().includes(search) ||
user.login.toLowerCase().includes(search) ||
user.email.toLowerCase().includes(search)
);
renderUsersChecklist();
}
function filterEditUsers() {
const search = document.getElementById('edit-user-search').value.toLowerCase();
const filtered = users.filter(user =>
user.name.toLowerCase().includes(search) ||
user.login.toLowerCase().includes(search) ||
user.email.toLowerCase().includes(search)
);
renderEditUsersChecklist(filtered);
}
function filterCopyUsers() {
const search = document.getElementById('copy-user-search').value.toLowerCase();
const filtered = users.filter(user =>
user.name.toLowerCase().includes(search) ||
user.login.toLowerCase().includes(search) ||
user.email.toLowerCase().includes(search)
);
renderCopyUsersChecklist(filtered);
}
async function loadTasks() {
try {
showingTasksWithoutDate = false;
const btn = document.getElementById('tasks-no-date-btn');
if (btn) btn.classList.remove('active');
const search = document.getElementById('search-tasks')?.value || '';
const statusFilter = document.getElementById('status-filter')?.value || 'active,in_progress,assigned,overdue,rework';
const creatorFilter = document.getElementById('creator-filter')?.value || '';
const assigneeFilter = document.getElementById('assignee-filter')?.value || '';
const deadlineFilter = document.getElementById('deadline-filter')?.value || '';
const showDeleted = document.getElementById('show-deleted')?.checked || false;
let url = '/api/tasks?';
if (search) url += `search=${encodeURIComponent(search)}&`;
if (statusFilter) url += `status=${encodeURIComponent(statusFilter)}&`;
if (creatorFilter) url += `creator=${encodeURIComponent(creatorFilter)}&`;
if (assigneeFilter) url += `assignee=${encodeURIComponent(assigneeFilter)}&`;
if (deadlineFilter) url += `deadline=${encodeURIComponent(deadlineFilter)}&`;
if (showDeleted) url += `showDeleted=true&`;
const response = await fetch(url);
tasks = await response.json();
// Загружаем файлы для всех задач
await Promise.all(tasks.map(async (task) => {
try {
const filesResponse = await fetch(`/api/tasks/${task.id}/files`);
if (filesResponse.ok) {
task.files = await filesResponse.json();
} else {
task.files = [];
}
} catch (error) {
console.error(`Ошибка загрузки файлов для задачи ${task.id}:`, error);
task.files = [];
}
}));
renderTasks();
} catch (error) {
console.error('Ошибка загрузки задач:', error);
}
}
function showTasksWithoutDate() {
showingTasksWithoutDate = true;
const btn = document.getElementById('tasks-no-date-btn');
if (btn) btn.classList.add('active');
loadTasksWithoutDate();
}
async function loadTasksWithoutDate() {
try {
const response = await fetch('/api/tasks');
if (!response.ok) throw new Error('Ошибка загрузки задач');
const allTasks = await response.json();
tasks = allTasks.filter(task => {
const hasTaskDueDate = !task.due_date;
const hasAssignmentDueDates = task.assignments &&
task.assignments.every(assignment => !assignment.due_date);
return hasTaskDueDate && hasAssignmentDueDates;
});
// Загружаем файлы для всех задач
await Promise.all(tasks.map(async (task) => {
try {
const filesResponse = await fetch(`/api/tasks/${task.id}/files`);
if (filesResponse.ok) {
task.files = await filesResponse.json();
} else {
task.files = [];
}
} catch (error) {
console.error(`Ошибка загрузки файлов для задачи ${task.id}:`, error);
task.files = [];
}
}));
renderTasks();
} catch (error) {
console.error('Ошибка загрузки задач без срока:', error);
}
}
async function loadActivityLogs() {
try {
const response = await fetch('/api/activity-logs');
const logs = await response.json();
renderLogs(logs);
} catch (error) {
console.error('Ошибка загрузки логов:', error);
}
}
function renderUsersChecklist() {
const container = document.getElementById('users-checklist');
container.innerHTML = filteredUsers
.filter(user => user.id !== currentUser.id)
.map(user => `
<div class="checkbox-item">
<label>
<input type="checkbox" name="assignedUsers" value="${user.id}">
${user.name} (${user.email})
${user.auth_type === 'ldap' ? '<small style="color: #666;"> - LDAP</small>' : ''}
</label>
</div>
`).join('');
}
function renderEditUsersChecklist(filtered = users) {
const container = document.getElementById('edit-users-checklist');
container.innerHTML = filtered
.filter(user => user.id !== currentUser.id)
.map(user => `
<div class="checkbox-item">
<label>
<input type="checkbox" name="assignedUsers" value="${user.id}">
${user.name} (${user.email})
${user.auth_type === 'ldap' ? '<small style="color: #666;"> - LDAP</small>' : ''}
</label>
</div>
`).join('');
}
function renderCopyUsersChecklist(filtered = users) {
const container = document.getElementById('copy-users-checklist');
container.innerHTML = filtered
.filter(user => user.id !== currentUser.id)
.map(user => `
<div class="checkbox-item">
<label>
<input type="checkbox" name="assignedUsers" value="${user.id}">
${user.name} (${user.email})
${user.auth_type === 'ldap' ? '<small style="color: #666;"> - LDAP</small>' : ''}
</label>
</div>
`).join('');
}
function renderTasks() {
const container = document.getElementById('tasks-list');
const showDeleted = document.getElementById('show-deleted')?.checked || false;
let filteredTasks = tasks;
if (!showDeleted) {
filteredTasks = tasks.filter(task => task.status === 'active');
}
if (filteredTasks.length === 0) {
container.innerHTML = '<div class="loading">Задачи не найдены</div>';
return;
}
container.innerHTML = filteredTasks.map(task => {
const isExpanded = expandedTasks.has(task.id);
const overallStatus = getTaskOverallStatus(task);
const statusClass = getStatusClass(overallStatus);
const isDeleted = task.status === 'deleted';
const isClosed = task.closed_at !== null;
const userRole = getUserRoleInTask(task);
const canEdit = canUserEditTask(task);
const isCopy = task.original_task_id !== null;
const timeLeftInfo = getTimeLeftInfo(task);
return `
<div class="task-card ${isDeleted ? 'deleted' : ''} ${isClosed ? 'closed' : ''}" data-task-id="${task.id}">
<div class="task-header">
<div class="task-title" onclick="toggleTask(${task.id})" style="cursor: pointer; display: flex; justify-content: space-between; align-items: center;">
<div style="flex: 1;">
<span class="task-number">Задача №${task.id}</span>
<strong>${task.title}</strong>
${isDeleted ? '<span class="deleted-badge">Удалена</span>' : ''}
${isClosed ? '<span class="closed-badge">Закрыта</span>' : ''}
${isCopy ? '<span class="copy-badge">Копия</span>' : ''}
${timeLeftInfo ? `<span class="deadline-badge ${timeLeftInfo.class}">${timeLeftInfo.text}</span>` : ''}
<span class="role-badge ${getRoleBadgeClass(userRole)}">${userRole}</span>
</div>
<div class="task-status ${statusClass}">${getStatusText(overallStatus)}</div>
<div class="expand-icon" style="margin-left: 10px; transition: transform 0.3s; transform: rotate(${isExpanded ? '180deg' : '0deg'});">
</div>
</div>
</div>
<div class="task-content ${isExpanded ? 'expanded' : ''}">
<div class="task-actions">
${!isDeleted && !isClosed ? `
${canEdit ? `<button class="edit-btn" onclick="openEditModal(${task.id})" title="Редактировать">✏️</button>` : ''}
<button class="copy-btn" onclick="openCopyModal(${task.id})" title="Создать копию">📋</button>
${canEdit ? `<button class="rework-btn" onclick="openReworkModal(${task.id})" title="Вернуть на доработку">🔄</button>` : ''}
${canEdit ? `<button class="close-btn" onclick="closeTask(${task.id})" title="Закрыть задачу">🔒</button>` : ''}
${canEdit ? `<button class="delete-btn" onclick="deleteTask(${task.id})" title="Удалить">🗑️</button>` : ''}
` : ''}
${isClosed && canEdit ? `
<button class="reopen-btn" onclick="reopenTask(${task.id})" title="Открыть задачу">🔓</button>
` : ''}
${isDeleted && currentUser.role === 'admin' ? `
<button class="restore-btn" onclick="restoreTask(${task.id})" title="Восстановить">↶</button>
` : ''}
</div>
${isCopy && task.original_task_title ? `
<div class="task-original">
<small>Оригинал: "${task.original_task_title}" (создал: ${task.original_creator_name})</small>
</div>
` : ''}
<div class="task-description">${task.description || 'Нет описания'}</div>
${task.rework_comment ? `
<div class="rework-comment">
<strong>Комментарий к доработке:</strong> ${task.rework_comment}
</div>
` : ''}
<div class="task-dates-files">
<div class="task-dates">
<strong>Создана:</strong> ${formatDateTime(task.start_date || task.created_at)}
${task.due_date ? ` | <strong>Выполнить до:</strong> ${formatDateTime(task.due_date)}` : ''}
${showingTasksWithoutDate ? '<span class="no-date-badge">Без срока</span>' : ''}
</div>
<div class="file-list" id="files-${task.id}">
<strong>Файлы:</strong>
${task.files && task.files.length > 0 ?
`<div class="file-icons-container">${task.files.map(file => renderFileIcon(file)).join('')}</div>` :
'<span class="no-files">нет файлов</span>'
}
</div>
</div>
<div class="task-assignments">
<strong>Исполнители:</strong>
${task.assignments && task.assignments.length > 0 ?
renderAssignmentList(task.assignments, task.id, canEdit) :
'<div>Не назначены</div>'
}
</div>
<div class="task-meta">
<small>Создана: ${formatDateTime(task.created_at)} | Автор: ${task.creator_name}</small>
${task.deleted_at ? `<br><small>Удалена: ${formatDateTime(task.deleted_at)}</small>` : ''}
${task.closed_at ? `<br><small>Закрыта: ${formatDateTime(task.closed_at)}</small>` : ''}
</div>
</div>
</div>
`;
}).join('');
}
function renderAssignmentList(assignments, taskId, canEdit) {
if (!assignments || assignments.length === 0) {
return '<div>Не назначены</div>';
}
// Создаем контейнер с возможностью фильтрации
return `
<div class="assignments-container">
<div class="assignments-filter">
<input type="text"
class="assignment-filter-input"
placeholder="Поиск исполнителя..."
data-task-id="${taskId}"
oninput="filterAssignments(${taskId})">
<span class="filter-count" id="filter-count-${taskId}">${assignments.length} исполнителей</span>
</div>
<div class="assignments-scroll-container" id="assignments-${taskId}">
${assignments.map(assignment => renderAssignment(assignment, taskId, canEdit)).join('')}
</div>
</div>
`;
}
// Функция для фильтрации исполнителей в конкретной задаче
function filterAssignments(taskId) {
const filterInput = document.querySelector(`.assignment-filter-input[data-task-id="${taskId}"]`);
const scrollContainer = document.getElementById(`assignments-${taskId}`);
const filterCount = document.getElementById(`filter-count-${taskId}`);
if (!filterInput || !scrollContainer) return;
const searchTerm = filterInput.value.toLowerCase();
const assignments = scrollContainer.querySelectorAll('.assignment');
let visibleCount = 0;
assignments.forEach(assignment => {
const userName = assignment.querySelector('strong')?.textContent?.toLowerCase() || '';
const userLogin = assignment.querySelector('small')?.textContent?.toLowerCase() || '';
const isVisible = userName.includes(searchTerm) ||
userLogin.includes(searchTerm) ||
searchTerm === '';
assignment.style.display = isVisible ? '' : 'none';
if (isVisible) {
visibleCount++;
}
});
if (filterCount) {
filterCount.textContent = `${visibleCount} из ${assignments.length} исполнителей`;
}
}
function toggleTask(taskId) {
if (expandedTasks.has(taskId)) {
expandedTasks.delete(taskId);
} else {
expandedTasks.add(taskId);
loadTaskFiles(taskId); // Эта строка должна быть
}
renderTasks();
}
function getTimeLeftInfo(task) {
if (!task.due_date || task.closed_at) return null;
const dueDate = new Date(task.due_date);
const now = new Date();
const timeLeft = dueDate.getTime() - now.getTime();
const hoursLeft = Math.floor(timeLeft / (60 * 60 * 1000));
if (hoursLeft <= 0) return null;
if (hoursLeft <= 24) {
return {
text: `Менее 24ч`,
class: 'deadline-24h'
};
} else if (hoursLeft <= 48) {
return {
text: `Менее 48ч`,
class: 'deadline-48h'
};
}
return null;
}
function renderAssignment(assignment, taskId, canEdit) {
const statusClass = getStatusClass(assignment.status);
const isCurrentUser = assignment.user_id === currentUser.id;
const isOverdue = assignment.status === 'overdue';
const isRework = assignment.status === 'rework';
const timeLeftInfo = getAssignmentTimeLeftInfo(assignment);
return `
<div class="assignment ${isOverdue ? 'overdue' : ''} ${isRework ? 'rework' : ''}">
<span class="assignment-status ${statusClass}"></span>
<div style="flex: 1;">
<strong>${assignment.user_name}</strong>
${isCurrentUser ? '<small>(Вы)</small>' : ''}
${timeLeftInfo ? `<span class="deadline-indicator ${timeLeftInfo.class}">${timeLeftInfo.text}</span>` : ''}
${assignment.start_date || assignment.due_date ? `
<div class="assignment-dates">
${assignment.start_date ? `<small>Начало: ${formatDateTime(assignment.start_date)}</small>` : ''}
${assignment.due_date ? `<small>Выполнить до: ${formatDateTime(assignment.due_date)}</small>` : ''}
</div>
` : ''}
${assignment.rework_comment ? `
<div class="assignment-rework-comment">
<small><strong>Комментарий:</strong> ${assignment.rework_comment}</small>
</div>
` : ''}
</div>
<div class="action-buttons">
${isCurrentUser && assignment.status === 'assigned' ?
`<button onclick="updateStatus(${taskId}, ${assignment.user_id}, 'in_progress')">Приступить</button>` : ''}
${isCurrentUser && (assignment.status === 'in_progress' || assignment.status === 'overdue' || assignment.status === 'rework') ?
`<button onclick="updateStatus(${taskId}, ${assignment.user_id}, 'completed')">Выполнено</button>` : ''}
${canEdit ?
`<button class="edit-date-btn" onclick="openEditAssignmentModal(${taskId}, ${assignment.user_id})" title="Редактировать сроки">📅</button>` : ''}
</div>
</div>
`;
}
function getAssignmentTimeLeftInfo(assignment) {
if (!assignment.due_date || assignment.status === 'completed') return null;
const dueDate = new Date(assignment.due_date);
const now = new Date();
const timeLeft = dueDate.getTime() - now.getTime();
const hoursLeft = Math.floor(timeLeft / (60 * 60 * 1000));
if (hoursLeft <= 0) return null;
if (hoursLeft <= 24) {
return {
text: `Осталось ${hoursLeft}ч`,
class: 'deadline-24h'
};
} else if (hoursLeft <= 48) {
return {
text: `Осталось ${hoursLeft}ч`,
class: 'deadline-48h'
};
}
return null;
}
async function createTask(event) {
event.preventDefault();
if (!currentUser) {
alert('Требуется аутентификация');
return;
}
const formData = new FormData();
formData.append('title', document.getElementById('title').value);
formData.append('description', document.getElementById('description').value);
const dueDate = document.getElementById('due-date').value;
if (!dueDate) {
alert('Дата и время выполнения обязательны');
return;
}
formData.append('dueDate', dueDate);
const assignedUsers = document.querySelectorAll('#users-checklist input[name="assignedUsers"]:checked');
if (assignedUsers.length === 0) {
alert('Выберите хотя бы одного исполнителя');
return;
}
assignedUsers.forEach(checkbox => {
formData.append('assignedUsers', checkbox.value);
});
const files = document.getElementById('files').files;
for (let i = 0; i < files.length; i++) {
formData.append('files', files[i]);
}
try {
const response = await fetch('/api/tasks', {
method: 'POST',
body: formData
});
if (response.ok) {
alert('Задача успешно создана!');
document.getElementById('create-task-form').reset();
document.getElementById('file-list').innerHTML = '';
document.getElementById('user-search').value = '';
filterUsers();
loadTasks();
loadActivityLogs();
showSection('tasks');
} else {
const error = await response.json();
alert(error.error || 'Ошибка создания задачи');
}
} catch (error) {
console.error('Ошибка:', error);
alert('Ошибка создания задачи');
}
}
async function openEditModal(taskId) {
try {
const response = await fetch(`/api/tasks/${taskId}`);
if (!response.ok) {
if (response.status === 404) {
alert('Задача не найдена или у вас нет прав доступа');
}
throw new Error('Ошибка загрузки задачи');
}
const task = await response.json();
if (!canUserEditTask(task)) {
alert('У вас нет прав для редактирования этой задачи');
return;
}
document.getElementById('edit-task-id').value = task.id;
document.getElementById('edit-title').value = task.title;
document.getElementById('edit-description').value = task.description || '';
document.getElementById('edit-due-date').value = task.due_date ? formatDateTimeForInput(task.due_date) : '';
const checkboxes = document.querySelectorAll('#edit-users-checklist input[name="assignedUsers"]');
checkboxes.forEach(checkbox => {
checkbox.checked = task.assignments?.some(assignment =>
assignment.user_id === parseInt(checkbox.value)
) || false;
});
document.getElementById('edit-task-modal').style.display = 'block';
} catch (error) {
console.error('Ошибка:', error);
alert('Ошибка загрузки задачи');
}
}
function closeEditModal() {
document.getElementById('edit-task-modal').style.display = 'none';
document.getElementById('edit-file-list').innerHTML = '';
document.getElementById('edit-user-search').value = '';
filterEditUsers();
}
async function updateTask(event) {
event.preventDefault();
const taskId = document.getElementById('edit-task-id').value;
const title = document.getElementById('edit-title').value;
const description = document.getElementById('edit-description').value;
const dueDate = document.getElementById('edit-due-date').value;
if (!dueDate) {
alert('Дата и время выполнения обязательны');
return;
}
const assignedUsers = document.querySelectorAll('#edit-users-checklist input[name="assignedUsers"]:checked');
const assignedUserIds = Array.from(assignedUsers).map(cb => parseInt(cb.value));
const formData = new FormData();
formData.append('title', title);
formData.append('description', description);
formData.append('assignedUsers', JSON.stringify(assignedUserIds));
formData.append('dueDate', dueDate);
const files = document.getElementById('edit-files').files;
for (let i = 0; i < files.length; i++) {
formData.append('files', files[i]);
}
try {
const response = await fetch(`/api/tasks/${taskId}`, {
method: 'PUT',
body: formData
});
if (response.ok) {
alert('Задача успешно обновлена!');
closeEditModal();
loadTasks();
loadActivityLogs();
} else {
const error = await response.json();
alert(error.error || 'Ошибка обновления задачи');
}
} catch (error) {
console.error('Ошибка:', error);
alert('Ошибка обновления задачи');
}
}
function openCopyModal(taskId) {
document.getElementById('copy-task-id').value = taskId;
document.getElementById('copy-task-modal').style.display = 'block';
}
function closeCopyModal() {
document.getElementById('copy-task-modal').style.display = 'none';
document.getElementById('copy-user-search').value = '';
filterCopyUsers();
}
async function copyTask(event) {
event.preventDefault();
const taskId = document.getElementById('copy-task-id').value;
const dueDate = document.getElementById('copy-due-date').value;
if (!dueDate) {
alert('Дата и время выполнения обязательны для копии задачи');
return;
}
const checkboxes = document.querySelectorAll('#copy-users-checklist input[name="assignedUsers"]:checked');
const assignedUserIds = Array.from(checkboxes).map(cb => parseInt(cb.value));
if (assignedUserIds.length === 0) {
alert('Выберите хотя бы одного исполнителя для копии задачи');
return;
}
try {
const response = await fetch(`/api/tasks/${taskId}/copy`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
assignedUsers: assignedUserIds,
dueDate: dueDate
})
});
if (response.ok) {
alert('Копия задачи успешно создана!');
closeCopyModal();
loadTasks();
loadActivityLogs();
} else {
const error = await response.json();
alert(error.error || 'Ошибка создания копии задачи');
}
} catch (error) {
console.error('Ошибка:', error);
alert('Ошибка создания копии задачи');
}
}
function openEditAssignmentModal(taskId, userId) {
const task = tasks.find(t => t.id === taskId);
if (!task) return;
const assignment = task.assignments.find(a => a.user_id === userId);
if (!assignment) return;
document.getElementById('edit-assignment-task-id').value = taskId;
document.getElementById('edit-assignment-user-id').value = userId;
document.getElementById('edit-assignment-due-date').value = assignment.due_date ? formatDateTimeForInput(assignment.due_date) : '';
document.getElementById('edit-assignment-modal').style.display = 'block';
}
function closeEditAssignmentModal() {
document.getElementById('edit-assignment-modal').style.display = 'none';
}
async function updateAssignment(event) {
event.preventDefault();
const taskId = document.getElementById('edit-assignment-task-id').value;
const userId = document.getElementById('edit-assignment-user-id').value;
const dueDate = document.getElementById('edit-assignment-due-date').value;
if (!dueDate) {
alert('Дата и время выполнения обязательны');
return;
}
try {
const response = await fetch(`/api/tasks/${taskId}/assignment/${userId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
dueDate: dueDate
})
});
if (response.ok) {
alert('Сроки исполнителя обновлены!');
closeEditAssignmentModal();
loadTasks();
loadActivityLogs();
} else {
const error = await response.json();
alert(error.error || 'Ошибка обновления сроков');
}
} catch (error) {
console.error('Ошибка:', error);
alert('Ошибка обновления сроков');
}
}
function openReworkModal(taskId) {
document.getElementById('rework-task-id').value = taskId;
document.getElementById('rework-task-modal').style.display = 'block';
}
function closeReworkModal() {
document.getElementById('rework-task-modal').style.display = 'none';
document.getElementById('rework-comment').value = '';
}
async function sendForRework(event) {
event.preventDefault();
const taskId = document.getElementById('rework-task-id').value;
const comment = document.getElementById('rework-comment').value;
try {
const response = await fetch(`/api/tasks/${taskId}/rework`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ comment })
});
if (response.ok) {
alert('Задача возвращена на доработку!');
closeReworkModal();
loadTasks();
loadActivityLogs();
} else {
const error = await response.json();
alert(error.error || 'Ошибка возврата задачи на доработку');
}
} catch (error) {
console.error('Ошибка:', error);
alert('Ошибка возврата задачи на доработку');
}
}
async function closeTask(taskId) {
if (!confirm('Вы уверены, что хотите закрыть эту задачу? Исполнители больше не будут видеть её.')) {
return;
}
try {
const response = await fetch(`/api/tasks/${taskId}/close`, {
method: 'POST'
});
if (response.ok) {
alert('Задача закрыта!');
loadTasks();
loadActivityLogs();
} else {
const error = await response.json();
alert(error.error || 'Ошибка закрытия задачи');
}
} catch (error) {
console.error('Ошибка:', error);
alert('Ошибка закрытия задачи');
}
}
async function reopenTask(taskId) {
try {
const response = await fetch(`/api/tasks/${taskId}/reopen`, {
method: 'POST'
});
if (response.ok) {
alert('Задача открыта!');
loadTasks();
loadActivityLogs();
} else {
const error = await response.json();
alert(error.error || 'Ошибка открытия задачи');
}
} catch (error) {
console.error('Ошибка:', error);
alert('Ошибка открытия задачи');
}
}
async function deleteTask(taskId) {
if (!confirm('Вы уверены, что хотите удалить эту задачу?')) {
return;
}
try {
const response = await fetch(`/api/tasks/${taskId}`, {
method: 'DELETE'
});
if (response.ok) {
alert('Задача удалена!');
loadTasks();
loadActivityLogs();
} else {
const error = await response.json();
alert(error.error || 'Ошибка удаления задачи');
}
} catch (error) {
console.error('Ошибка:', error);
alert('Ошибка удаления задачи');
}
}
async function restoreTask(taskId) {
try {
const response = await fetch(`/api/tasks/${taskId}/restore`, {
method: 'POST'
});
if (response.ok) {
alert('Задача восстановлена!');
loadTasks();
loadActivityLogs();
} else {
const error = await response.json();
alert(error.error || 'Ошибка восстановления задачи');
}
} catch (error) {
console.error('Ошибка:', error);
alert('Ошибка восстановления задачи');
}
}
async function updateStatus(taskId, userId, status) {
try {
const response = await fetch(`/api/tasks/${taskId}/status`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ userId, status })
});
if (response.ok) {
loadTasks();
loadActivityLogs();
} else {
const error = await response.json();
alert(error.error || 'Ошибка обновления статуса');
}
} catch (error) {
console.error('Ошибка:', error);
alert('Ошибка обновления статуса');
}
}
function getTaskOverallStatus(task) {
if (task.status === 'deleted') return 'deleted';
if (task.closed_at) return 'closed';
if (!task.assignments || task.assignments.length === 0) return 'unassigned';
const assignments = task.assignments;
let hasAssigned = false;
let hasInProgress = false;
let hasOverdue = false;
let hasRework = false;
let allCompleted = true;
for (let assignment of assignments) {
if (assignment.status === 'assigned') {
hasAssigned = true;
allCompleted = false;
} else if (assignment.status === 'in_progress') {
hasInProgress = true;
allCompleted = false;
} else if (assignment.status === 'overdue') {
hasOverdue = true;
allCompleted = false;
} else if (assignment.status === 'rework') {
hasRework = true;
allCompleted = false;
} else if (assignment.status !== 'completed') {
allCompleted = false;
}
}
if (allCompleted) return 'completed';
if (hasRework) return 'rework';
if (hasOverdue) return 'overdue';
if (hasInProgress) return 'in_progress';
if (hasAssigned) return 'assigned';
return 'unassigned';
}
function getStatusClass(status) {
switch (status) {
case 'deleted': return 'status-gray';
case 'closed': return 'status-gray';
case 'unassigned': return 'status-purple';
case 'assigned': return 'status-red';
case 'in_progress': return 'status-orange';
case 'rework': return 'status-yellow';
case 'overdue': return 'status-darkred';
case 'completed': return 'status-green';
default: return 'status-purple';
}
}
function getStatusText(status) {
switch (status) {
case 'deleted': return 'Удалена';
case 'closed': return 'Закрыта';
case 'unassigned': return 'Не назначена';
case 'assigned': return 'Назначена';
case 'in_progress': return 'В работе';
case 'rework': return 'На доработке';
case 'overdue': return 'Просрочена';
case 'completed': return 'Выполнена';
default: return 'Неизвестно';
}
}
function getUserRoleInTask(task) {
if (!currentUser) return 'Нет доступа';
if (currentUser.role === 'admin') return 'Администратор';
if (parseInt(task.created_by) === currentUser.id) {
if (task.assignments && task.assignments.length > 0) {
const assignedToOthers = task.assignments.some(assignment =>
parseInt(assignment.user_id) !== currentUser.id
);
if (assignedToOthers) {
return 'Создатель (только просмотр)';
}
}
return 'Создатель';
}
if (task.assignments) {
const isExecutor = task.assignments.some(assignment =>
parseInt(assignment.user_id) === currentUser.id
);
if (isExecutor) return 'Исполнитель';
}
return 'Наблюдатель';
}
function getRoleBadgeClass(role) {
switch (role) {
case 'Администратор': return 'role-admin';
case 'Заказчик': return 'role-creator';
case 'Исполнитель': return 'role-executor';
default: return '';
}
}
function canUserEditTask(task) {
if (!currentUser) return false;
// Администратор может всё
if (currentUser.role === 'admin') return true;
// Создатель может редактировать свою задачу
if (parseInt(task.created_by) === currentUser.id) {
// Но если задача уже назначена другим пользователям,
// создатель может только просматривать
if (task.assignments && task.assignments.length > 0) {
// Проверяем, назначена ли задача другим пользователям (не только себе)
const assignedToOthers = task.assignments.some(assignment =>
parseInt(assignment.user_id) !== currentUser.id
);
if (assignedToOthers) {
// Создатель может только просматривать и закрывать задачу
return false;
}
}
return true;
}
// Исполнитель может менять только свой статус
if (task.assignments) {
const isExecutor = task.assignments.some(assignment =>
parseInt(assignment.user_id) === currentUser.id
);
if (isExecutor) {
// Исполнитель может менять только статус
return false;
}
}
return false;
}
function formatDateTime(dateTimeString) {
if (!dateTimeString) return '';
const date = new Date(dateTimeString);
return date.toLocaleString('ru-RU');
}
function formatDateTimeForInput(dateTimeString) {
if (!dateTimeString) return '';
const date = new Date(dateTimeString);
return date.toISOString().slice(0, 16);
}
function updateFileList() {
const fileInput = document.getElementById('files');
const fileList = document.getElementById('file-list');
updateFileListForInput(fileInput, fileList);
}
function updateEditFileList() {
const fileInput = document.getElementById('edit-files');
const fileList = document.getElementById('edit-file-list');
updateFileListForInput(fileInput, fileList);
}
function updateFileListForInput(fileInput, fileList) {
const files = fileInput.files;
if (files.length === 0) {
fileList.innerHTML = '';
return;
}
let html = '<ul>';
let totalSize = 0;
for (let i = 0; i < files.length; i++) {
const file = files[i];
totalSize += file.size;
html += `<li>${file.name} (${(file.size / 1024 / 1024).toFixed(2)} MB)</li>`;
}
html += '</ul>';
html += `<p><strong>Общий размер: ${(totalSize / 1024 / 1024).toFixed(2)} MB / 300 MB</strong></p>`;
fileList.innerHTML = html;
}
function renderLogs(logs) {
const container = document.getElementById('logs-list');
if (logs.length === 0) {
container.innerHTML = '<div class="loading">Логи не найдены</div>';
return;
}
container.innerHTML = logs.map(log => `
<div class="log-entry">
<div class="log-time">${formatDateTime(log.created_at)}</div>
<div><strong>${log.user_name}</strong> - ${getActionText(log.action)}</div>
<div>Задача: "${log.task_title}"</div>
${log.details ? `<div>Детали: ${log.details}</div>` : ''}
</div>
`).join('');
}
function getActionText(action) {
const actions = {
'TASK_CREATED': 'создал задачу',
'TASK_COPIED': 'создал копию задачи',
'TASK_UPDATED': 'обновил задачу',
'TASK_DELETED': 'удалил задачу',
'TASK_RESTORED': 'восстановил задачу',
'TASK_ASSIGNED': 'назначил задачу',
'TASK_ASSIGNMENTS_UPDATED': 'обновил назначения',
'ASSIGNMENT_UPDATED': 'обновил сроки исполнителя',
'STATUS_CHANGED': 'изменил статус задачи',
'FILE_UPLOADED': 'загрузил файл',
'FILE_COPIED': 'скопировал файл',
'TASK_SENT_FOR_REWORK': 'вернул задачу на доработку',
'TASK_CLOSED': 'закрыл задачу',
'TASK_REOPENED': 'открыл задачу'
};
return actions[action] || action;
}
async function loadTaskFiles(taskId) {
try {
const response = await fetch(`/api/tasks/${taskId}/files`);
const files = await response.json();
const container = document.getElementById(`files-${taskId}`);
if (container) {
if (files.length === 0) {
container.innerHTML = '<strong>Файлы:</strong> <span class="files-placeholder">скрыто</span>';
} else {
container.innerHTML = `
<strong>Файлы:</strong>
<div class="file-icons-container">
${files.map(file => renderFileIcon(file)).join('')}
</div>
`;
}
}
} catch (error) {
console.error('Ошибка загрузки файлов:', error);
}
}
function renderFileIcon(file) {
// Исправляем кодировку имени файла
const fixEncoding = (str) => {
if (!str) return '';
try {
// Пробуем разные способы декодирования
if (str.includes('Ð') || str.includes('Ñ')) {
// UTF-8 неправильно декодированный как Latin-1
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;
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 safeFileName = fileName;
const displayFileName = truncateFileName(safeFileName);
return `
<a href="/api/files/${file.id}/download"
download="${encodeURIComponent(safeFileName)}"
class="file-icon-container"
title="${safeFileName} (${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>
`;
}
function truncateFileName(fileName, maxLength = 20) {
if (fileName.length <= maxLength) return fileName;
const extension = fileName.split('.').pop();
const name = fileName.substring(0, fileName.lastIndexOf('.'));
const truncatedName = name.substring(0, maxLength - extension.length - 3) + '...';
return truncatedName + '.' + extension;
}