Files
minicrm/public/ui.js

1648 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.
// ui.js - UI функции и рендеринг
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();
} else if (sectionName === 'kanban') {
loadKanbanTasks();
}
// Загрузка профиля при переходе в личный кабинет
if (sectionName === 'profile') {
loadUserProfile();
loadNotificationSettings();
}
}
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;">
<!--
${task.task_type ? `<span class="task-type-badge ${task.task_type}">${getTaskTypeDisplayName(task.task_type)}</span>` : ''}
-->
<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>
${task.assignments && task.assignments.length > 0 ? `<span class="task-number">${task.assignments.map(a => a.user_login || a.user_name).join(', ')}</span>` : ''}
</div>
<!--
<div class="task-status ${statusClass}">
${getStatusText(overallStatus)}
</div>
-->
<span class="task-status ${statusClass}">
Выполнить до: ${formatDateTime(task.due_date || task.created_at)}
</span>
<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' : ''}">
${isExpanded ? `
<div class="task-actions">
${!isDeleted && !isClosed ? `
<button class="copy-btn" onclick="openTaskChat(${task.id})" title="Открыть чат">💬</button>
<button class="add-file-btn" onclick="openAddFileModal(${task.id})" title="Добавить файл">📎</button>
${currentUser && currentUser.login === 'minicrm' ? `<button class="edit-btn" onclick="openEditModal(${task.id})" title="Редактировать">✏️</button>` : ''}
${currentUser && currentUser.login === 'minicrm' ? `<button class="manage-assignees-btn" onclick="openManageAssigneesModal(${task.id})" title="Управление исполнителями">👥</button>` : ''}
<button class="copy-btn" onclick="openCopyModal(${task.id})" title="Создать копию">📋</button>
${currentUser && currentUser.login === 'minicrm' ? `<button class="rework-btn" onclick="openReworkModal(${task.id})" title="Вернуть на доработку">🔄</button>` : ''}
${currentUser && currentUser.login === 'minicrm' ? `<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="file-list" id="files-${task.id}">
<strong>Файлы:</strong>
${task.files && task.files.length > 0 ? renderGroupedFiles(task) : '<span class="no-files">нет файлов</span>'}
</div>
<div class="task-assignments">
<strong>Исполнители:</strong>
${task.assignments && task.assignments.length > 0 ?
renderAssignmentList(task.assignments, task.id, canEdit) :
'<div>Не назначены</div>'
}
</div>
</div>
<div class="task-meta">
<small>
Создана: ${formatDateTime(task.start_date || task.created_at)}
| Выполнить до: ${formatDateTime(task.due_date || task.created_at)}
| Автор: ${task.creator_name}
| Тип: ${task.task_type ? `<span class="task-type-badge ${task.task_type}">${getTaskTypeDisplayName(task.task_type)}</span>` : ''}
</small>
${task.deleted_at ? `<br><small>Удалена: ${formatDateTime(task.deleted_at)}</small>` : ''}
${task.closed_at ? `<br><small>Закрыта: ${formatDateTime(task.closed_at)}</small>` : ''}
</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 openReworkAssignmentModal(taskId, userId, userName) {
const task = tasks.find(t => t.id === taskId);
if (!task) {
alert('Задача не найдена');
return;
}
// Проверяем права (только автор задачи или администратор)
if (parseInt(task.created_by) !== currentUser.id && currentUser.role !== 'admin') {
alert('Только автор задачи может отправлять на доработку');
return;
}
// Удаляем предыдущее модальное окно, если оно есть
const existingModal = document.getElementById('rework-assignment-modal');
if (existingModal) {
existingModal.remove();
}
// Создаем модальное окно
const modal = document.createElement('div');
modal.className = 'modal';
modal.id = 'rework-assignment-modal';
modal.style.display = 'block';
modal.innerHTML = `
<div class="modal-content">
<div class="modal-header">
<h3>Отправить на доработку</h3>
<span class="close" onclick="closeReworkAssignmentModal()">&times;</span>
</div>
<div class="modal-body">
<p><strong>Задача:</strong> ${escapeHtml(task.title)}</p>
<p><strong>Исполнитель:</strong> ${escapeHtml(userName)}</p>
<div class="form-group">
<label for="rework-comment-${taskId}-${userId}"><strong>Комментарий к доработке:</strong></label>
<textarea id="rework-comment-${taskId}-${userId}"
class="form-control"
rows="5"
placeholder="Опишите, что нужно доработать..."></textarea>
</div>
<div class="modal-footer" style="margin-top: 20px; text-align: right;">
<button type="button" class="btn-rework-cancel" onclick="closeReworkAssignmentModal()">Отмена</button>
<button type="button" class="btn-rework-primary" onclick="submitReworkAssignment('${taskId}', '${userId}')">🔄 Отправить на доработку</button>
</div>
</div>
</div>
`;
document.body.appendChild(modal);
// Фокусируемся на textarea
const textarea = document.getElementById(`rework-comment-${taskId}-${userId}`);
if (textarea) {
setTimeout(() => textarea.focus(), 100);
}
}
// Функция для принудительного завершения задачи исполнителя
async function forceCompleteAssignment(taskId, userId, userName) {
if (!confirm(`Вы уверены, что хотите отметить задачу как выполненную для сотрудника ${userName}?\nЭто действие нельзя отменить.`)) {
return;
}
try {
const response = await fetch(`/api/tasks/${taskId}/force-complete/${userId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
}
});
if (response.ok) {
alert(`✅ Задача отмечена как выполненная для сотрудника ${userName}`);
loadTasks(); // Перезагружаем задачи
} else {
const error = await response.json();
alert(`❌ Ошибка: ${error.error || 'Неизвестная ошибка'}`);
}
} catch (error) {
console.error('❌ Ошибка:', error);
alert('Сетевая ошибка при выполнении операции');
}
}
// Вспомогательная функция для экранирования HTML
function escapeHtml(text) {
if (!text) return '';
return String(text)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
// Функция для отправки на доработку конкретного исполнителя
async function submitReworkAssignment(taskId, userId) {
// Получаем textarea по уникальному ID
const textarea = document.getElementById(`rework-comment-${taskId}-${userId}`);
if (!textarea) {
alert('Ошибка: поле комментария не найдено');
return;
}
const comment = textarea.value.trim();
// Детальная отладка
console.log('=== ОТПРАВКА НА ДОРАБОТКУ ===');
console.log('Task ID:', taskId);
console.log('User ID:', userId);
console.log('Comment length:', comment.length);
console.log('Comment text:', comment);
console.log('============================');
if (!comment) {
alert('Пожалуйста, укажите комментарий к доработке');
textarea.style.border = '2px solid red';
textarea.focus();
return;
}
// Блокируем кнопку отправки
const submitBtn = document.querySelector(`#rework-assignment-modal .btn-warning`);
if (submitBtn) {
submitBtn.disabled = true;
submitBtn.innerHTML = '⏳ Отправка...';
}
try {
const response = await fetch(`/api/tasks/${taskId}/rework-assignment/${userId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
comment: comment // Отправляем явно
})
});
const data = await response.json();
if (response.ok) {
alert('✅ Исполнитель отправлен на доработку');
closeReworkAssignmentModal();
// Перезагружаем задачи
if (typeof loadTasks === 'function') {
loadTasks();
} else {
location.reload();
}
} else {
alert(`❌ Ошибка: ${data.error || 'Неизвестная ошибка'}`);
}
} catch (error) {
console.error('❌ Ошибка:', error);
alert('Сетевая ошибка при отправке на доработку: ' + error.message);
} finally {
// Разблокируем кнопку
if (submitBtn) {
submitBtn.disabled = false;
submitBtn.innerHTML = '🔄 Отправить на доработку';
}
}
}
// Функция для закрытия модального окна доработки
function closeReworkAssignmentModal() {
const modal = document.getElementById('rework-assignment-modal');
if (modal) {
modal.style.display = 'none';
setTimeout(() => {
modal.remove();
}, 300);
}
}
// Функция для фильтрации исполнителей в конкретной задаче
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);
// Проверяем, является ли текущий пользователь автором задачи
const task = tasks.find(t => t.id === taskId);
const isTaskCreator = task && parseInt(task.created_by) === currentUser.id;
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>` : ''}
${isTaskCreator && assignment.status !== 'assigned' ?
`<button class="rework-btn" onclick="openReworkAssignmentModal(${taskId}, ${assignment.user_id}, '${assignment.user_name}')" title="Отправить на доработку">🔄Переделать</button>` : ''}
${isTaskCreator && assignment.status !== 'completed' ?
`<button class="force-complete-btn" onclick="forceCompleteAssignment(${taskId}, ${assignment.user_id}, '${assignment.user_name}')" title="Принудительно отметить как выполненное">✅ Завершить</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;
}
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) {
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 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);
}
// Логи активности
async function loadActivityLogs() {
try {
const response = await fetch('/api/activity-logs');
const logs = await response.json();
renderLogs(logs);
} catch (error) {
console.error('Ошибка загрузки логов:', error);
}
}
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 viewTaskDetails(taskId) {
try {
const response = await fetch(`/api/tasks/${taskId}`);
const task = await response.json();
// Можно открыть модальное окно с подробной информацией
// Или показать в отдельной секции
alert(`Задача: ${task.title}\n\nОписание: ${task.description || 'Нет описания'}\n\nСоздатель: ${task.creator_name}\nСрок: ${task.due_date ? new Date(task.due_date).toLocaleString('ru-RU') : 'Не установлен'}`);
} catch (error) {
console.error('Ошибка загрузки деталей задачи:', error);
}
}
// Функция для открытия модального окна добавления файла
function openAddFileModal(taskId) {
// Находим задачу
const task = tasks.find(t => t.id === taskId);
if (!task) {
alert('Задача не найдена');
return;
}
// Проверяем права
if (!canUserAddFilesToTask(task)) {
alert('У вас нет прав для добавления файлов к этой задаче');
return;
}
// Создаем модальное окно
const modalHtml = `
<div class="modal" id="add-file-modal">
<div class="modal-content">
<div class="modal-header">
<h3>Добавить файл к задаче: "${task.title}"</h3>
<span class="close" onclick="closeAddFileModal()">&times;</span>
</div>
<div class="modal-body">
<form id="add-file-form">
<input type="hidden" name="task_id" value="${taskId}">
<div class="form-group">
<label for="file-input">Выберите файл:</label>
<input type="file" id="file-input" name="files" multiple>
<small>Максимальный размер: 10MB</small>
</div>
<div class="form-group">
<label for="file-description">Описание файла (необязательно):</label>
<textarea id="file-description" name="description" rows="3"
placeholder="Описание файла..."></textarea>
</div>
<div class="modal-footer">
<button type="button" class="btn-cancel" onclick="closeAddFileModal()">Отмена</button>
<button type="submit" class="btn-primary">Добавить файл</button>
</div>
</form>
</div>
</div>
</div>
`;
// Добавляем модальное окно в DOM
const modalContainer = document.createElement('div');
modalContainer.innerHTML = modalHtml;
document.body.appendChild(modalContainer);
// Обработчик отправки формы
document.getElementById('add-file-form').addEventListener('submit', async function(e) {
e.preventDefault();
const fileInput = document.getElementById('file-input');
const description = document.getElementById('file-description').value;
if (fileInput.files.length === 0) {
alert('Выберите файл для загрузки');
return;
}
const file = fileInput.files[0];
const formData = new FormData();
// Пробуем сначала 'files', затем 'file'
formData.append('files', file); // Или 'file' в зависимости от сервера
formData.append('task_id', taskId);
if (description) {
formData.append('description', description);
}
try {
// Попробуем с полем 'files' (скорее всего это правильное имя)
let response = await fetch(`/api/tasks/${taskId}/files`, {
method: 'POST',
body: formData
});
console.log('Попытка 1');
if (!response.ok) {
// Попробуем с полем 'file'
console.log('Попытка 2 с именем поля "file"...');
formData.delete('files');
formData.append('file', file);
response = await fetch(`/api/tasks/${taskId}/files`, {
method: 'POST',
body: formData
});
}
if (response.ok) {
alert('Файл успешно добавлен');
await loadTaskFiles(taskId);
closeAddFileModal();
// Обновляем задачу, если она развернута
if (expandedTasks.has(taskId)) {
renderTasks();
}
} else {
const errorText = await response.text();
console.error('Ошибка сервера:', errorText);
alert(`Ошибка при добавлении файла: ${response.status} ${response.statusText}`);
}
} catch (error) {
console.error('Ошибка:', error);
alert('Сетевая ошибка при добавлении файла: ' + error.message);
}
});
// Показываем модальное окно
setTimeout(() => {
document.getElementById('add-file-modal').style.display = 'block';
}, 10);
}
// Функция для закрытия модального окна добавления файла
function closeAddFileModal() {
const modal = document.getElementById('add-file-modal');
if (modal) {
modal.style.display = 'none';
// Удаляем модальное окно из DOM через некоторое время
setTimeout(() => {
modal.parentElement.remove();
}, 300);
}
}
// Функция для закрытия модального окна добавления файла
function closeAddFileModal() {
const modal = document.getElementById('add-file-modal');
if (modal) {
modal.style.display = 'none';
// Удаляем модальное окно из DOM через некоторое время
setTimeout(() => {
modal.parentElement.remove();
}, 300);
}
}
// Обновленная функция для загрузки файлов задачи
async function loadTaskFiles(taskId) {
try {
const response = await fetch(`/api/tasks/${taskId}/files`);
const allFiles = await response.json(); // Получаем ВСЕ файлы
// Получаем задачу
const taskIndex = tasks.findIndex(t => t.id === taskId);
if (taskIndex === -1) {
console.error('Задача не найдена:', taskId);
return;
}
// Сохраняем ВСЕ файлы в задаче (не фильтруем здесь!)
tasks[taskIndex].files = allFiles;
// Обновляем отображение файлов с помощью renderGroupedFiles
// Она сама отфильтрует что показывать
const fileContainer = document.getElementById(`files-${taskId}`);
if (fileContainer) {
fileContainer.innerHTML = `
<strong>Файлы:</strong>
${allFiles.length > 0 ? renderGroupedFiles(tasks[taskIndex]) : '<span class="no-files">нет файлов</span>'}
`;
}
} catch (error) {
console.error('Ошибка загрузки файлов:', error);
}
}
// Функция для выбора типа задачи
async function selectTaskType(type) {
// Убираем активный класс со всех кнопок
document.querySelectorAll('.task-type-btn').forEach(btn => {
btn.classList.remove('active');
});
// Добавляем активный класс выбранной кнопке
document.querySelector(`.task-type-btn[data-type="${type}"]`).classList.add('active');
// Устанавливаем значение в скрытое поле
document.getElementById('task-type').value = type;
// Можем добавить дополнительную логику в зависимости от типа
updateTaskFormBasedOnType(type);
// Предлагаем заголовок по умолчанию
suggestDefaultTitle(type);
// Для типа "document" перезагружаем пользователей с фильтрацией
if (type === 'document') {
await reloadUsersForDocumentType();
} else if (type === 'it') {
await reloadUsersForType('ИТ специалист');
} else if (type === 'ahch') {
await reloadUsersForType('АХЧ');
} else if (type === 'psychologist') {
await reloadUsersForType('психолог');
} else if (type === 'speech_therapist') {
await reloadUsersForType('логопед');
} else if (type === 'hr') {
await reloadUsersForType('кадровичка');
} else if (type === 'certificate') {
await reloadUsersForType('Администрация');
} else if (type === 'e_journal') {
await reloadUsersForType('Админ ЭЖ');
} else {
// Для других типов перезагружаем обычных пользователей
await loadUsers();
}
}
// функция для перезагрузки пользователей для типа "document"
async function reloadUsersForDocumentType() {
try {
const response = await fetch('/api/users');
const allUsersData = await response.json();
// Фильтруем только пользователей из группы "Секретарь"
const secretaries = [];
for (const user of allUsersData) {
if (user.id === currentUser.id) continue;
const groups = await getUserGroups(user.id);
const hasSecretaryGroup = groups.some(group =>
group.name === 'Секретарь' ||
(typeof group === 'string' && group.includes('Секретарь'))
);
if (hasSecretaryGroup) {
secretaries.push(user);
}
}
users = secretaries;
filteredUsers = [...users];
renderUsersChecklist();
} catch (error) {
console.error('Ошибка загрузки секретарей:', error);
}
}
// функция для перезагрузки пользователей для типа "в переменной"
async function reloadUsersForType(TypegroupName) {
try {
const response = await fetch('/api/users');
const allUsersData = await response.json();
const TypegroupUsers = [];
for (const user of allUsersData) {
if (user.id === currentUser.id) continue;
const groups = await getUserGroups(user.id);
const hasTargetGroup = groups.some(group =>
group.name === TypegroupName ||
(typeof group === 'string' && group.includes(TypegroupName))
);
if (hasTargetGroup) {
TypegroupUsers.push(user);
}
}
users = TypegroupUsers;
filteredUsers = [...users];
renderUsersChecklist();
return filteredUsers;
} catch (error) {
console.error(`Ошибка загрузки пользователей группы "${TypegroupName}":`, error);
throw error;
}
}
// Используем кэширование, чтобы не делать лишние запросы
async function getUserGroups(userId) {
// Используем кэширование, чтобы не делать лишние запросы
if (!window.userGroupsCache) window.userGroupsCache = {};
if (window.userGroupsCache[userId]) {
return window.userGroupsCache[userId];
}
try {
const response = await fetch(`/api2/idusers/user/${userId}/groups`);
if (!response.ok) return [];
const groups = await response.json();
window.userGroupsCache[userId] = groups || [];
return window.userGroupsCache[userId];
} catch (error) {
console.error('Ошибка получения групп пользователя:', error);
return [];
}
}
// Функция для предложения заголовка по умолчанию
function suggestDefaultTitle(type) {
const titleField = document.getElementById('title');
if (!titleField) return;
// Если поле уже заполнено, не меняем
if (titleField.value.trim() !== '') return;
const defaultTitles = {
'regular': '',
'document': 'Согласование документа: ',
'it': 'Заявка в ИТ отдел: ',
'ahch': 'Заявка в АХЧ: ',
'psychologist': 'Заявка к психологу: ',
'speech_therapist': 'Заявка к логопеду: ',
'hr': 'Заявка в кадры: ',
'certificate': 'Заявка на получение справки: ',
'e_journal': 'Заявка на доступ в электронный журнал: '
};
const defaultTitle = defaultTitles[type] || '';
titleField.placeholder = `${defaultTitle}укажите детали...`;
}
// Функция для обновления формы в зависимости от типа задачи
function updateTaskFormBasedOnType(type) {
const userSearchField = document.getElementById('user-search');
const taskTypeInfo = document.getElementById('task-type-info');
// Устанавливаем заголовки placeholder для поиска исполнителей
const defaultPlaceholders = {
'regular': 'Поиск исполнителей...',
'document': 'Поиск секретарей/администрации...',
'it': 'Поиск ИТ специалистов...',
'ahch': 'Поиск АХЧ сотрудников...',
'psychologist': 'Поиск психологов...',
'speech_therapist': 'Поиск логопедов...',
'hr': 'Поиск сотрудников кадровой службы...',
'certificate': 'Поиск секретаря/завуча...',
'e_journal': 'Поиск администратора электронного журнала...'
};
// Обновляем placeholder поля поиска исполнителей
if (userSearchField) {
userSearchField.placeholder = defaultPlaceholders[type] || 'Поиск исполнителей...';
}
// Показываем информацию о типе задачи
if (taskTypeInfo) {
const typeInfo = {
'regular': 'Доступны все пользователи в зависимости от ваших прав',
'document': 'Доступны только пользователи из группы "Секретарь"',
'it': 'Доступны ИТ специалисты',
'ahch': 'Доступны сотрудники АХЧ',
'psychologist': 'Доступны психологи',
'speech_therapist': 'Доступны логопеды',
'hr': 'Доступны сотрудники кадровой службы',
'certificate': 'Доступны секретари и завучи',
'e_journal': 'Доступны администраторы электронного журнала'
};
taskTypeInfo.textContent = typeInfo[type] || '';
}
// Показываем/скрываем дополнительные поля в зависимости от типа
showAdditionalFieldsForType(type);
}
// Функция для показа дополнительных полей
function showAdditionalFieldsForType(type) {
// Пока скрываем все дополнительные поля (если они есть)
hideAllAdditionalFields();
// Показываем поля в зависимости от типа
switch(type) {
case 'certificate':
showCertificateFields();
break;
case 'e_journal':
showEJournalFields();
break;
case 'psychologist':
case 'speech_therapist':
showSpecialistFields(type);
break;
// Можно добавить другие типы
}
}
// Функции для управления дополнительными полями
function hideAllAdditionalFields() {
// Если есть дополнительные поля, скрываем их
// Пока оставляем пустым, можно добавить позже
}
function showCertificateFields() {
// Можно добавить поля для справок
// Например: тип справки, для кого, срок действия
}
function showEJournalFields() {
// Можно добавить поля для доступа к электронному журналу
// Например: логин, класс, предметы
}
function showSpecialistFields(type) {
// Можно добавить поля для специалистов
// Например: ученик, класс, причина обращения
}
// Вспомогательная функция для получения названия типа задачи
function getTaskTypeName(type) {
const typeNames = {
'regular': 'задачу',
'document': 'название документа',
'it': 'описание проблемы',
'ahch': 'описание заявки',
'psychologist': 'повод для обращения к психологу',
'speech_therapist': 'повод для обращения к логопеду',
'hr': 'вопрос к кадровой службе',
'certificate': 'тип необходимой справки',
'e_journal': 'информацию для доступа к журналу'
};
return typeNames[type] || 'задачу';
}
// Функция для получения отображаемого имени типа задачи
function getTaskTypeDisplayName(type) {
const typeNames = {
'regular': 'Задача',
'document': 'Документ',
'it': 'ИТ',
'ahch': 'АХЧ',
'psychologist': 'Психолог',
'speech_therapist': 'Логопед',
'hr': 'Кадры',
'certificate': 'Справка',
'e_journal': 'Эл. журнал'
};
return typeNames[type] || type;
}
// Функция для получения иконки типа задачи
function getTaskTypeIcon(type) {
const icons = {
'regular': 'fas fa-tasks',
'document': 'fas fa-file-signature',
'it': 'fas fa-desktop',
'ahch': 'fas fa-tools',
'psychologist': 'fas fa-brain',
'speech_therapist': 'fas fa-comment-medical',
'hr': 'fas fa-users',
'certificate': 'fas fa-file-certificate',
'e_journal': 'fas fa-book'
};
return icons[type] || 'fas fa-tasks';
}
// Функция для открытия модального окна управления исполнителями
async function openManageAssigneesModal(taskId) {
// Находим задачу
const task = tasks.find(t => t.id === taskId);
if (!task) {
alert('Задача не найдена');
return;
}
// Проверяем права
if (!canUserEditTask(task)) {
alert('У вас нет прав для управления исполнителями этой задачи');
return;
}
// Получаем текущих исполнителей
const currentAssignees = task.assignments || [];
// Получаем доступных для добавления исполнителей
try {
const response = await fetch(`/api/tasks/${taskId}/available-assignees`);
const availableUsers = await response.json();
// Получаем всех пользователей для выбора замены
const allUsersResponse = await fetch('/api/users');
const allUsers = await allUsersResponse.json();
// Создаем модальное окно
const modalHtml = `
<div class="modal" id="manage-assignees-modal">
<div class="modal-content" style="max-width: 800px;">
<div class="modal-header">
<h3>Управление исполнителями задачи: "${task.title}"</h3>
<span class="close" onclick="closeManageAssigneesModal()">&times;</span>
</div>
<div class="modal-body">
<div class="tabs">
<button class="tab-btn active" onclick="switchManageTab('add')"> Добавить</button>
<button class="tab-btn" onclick="switchManageTab('remove')"> Удалить</button>
<button class="tab-btn" onclick="switchManageTab('replace-one')">🔄 Заменить одного</button>
<button class="tab-btn" onclick="switchManageTab('replace-all')">🔄 Заменить всех</button>
<button class="tab-btn" onclick="switchManageTab('assign-to-user')">👤 Назначить всем</button>
</div>
<div id="add-assignee-tab" class="tab-content active">
<h4>Добавить исполнителей</h4>
<p>Текущие исполнители: ${currentAssignees.map(a => a.user_name).join(', ') || 'нет'}</p>
<div class="user-search-box">
<input type="text" id="available-users-search" placeholder="Поиск пользователей..."
oninput="filterAvailableUsers()">
</div>
<div class="users-checklist" id="available-users-list" style="max-height: 200px; overflow-y: auto;">
${availableUsers.map(user => `
<div class="checkbox-item">
<label>
<input type="checkbox" class="available-user-checkbox" value="${user.id}">
${user.name}
</label>
</div>
`).join('')}
</div>
<div class="modal-footer">
<button type="button" class="btn-cancel" onclick="closeManageAssigneesModal()">Отмена</button>
<button type="button" class="btn-primary" onclick="addAssignees(${taskId})">Добавить выбранных</button>
</div>
</div>
<div id="remove-assignee-tab" class="tab-content">
<h4>Удалить исполнителей</h4>
${currentAssignees.length > 0 ? `
<div class="users-checklist" style="max-height: 200px; overflow-y: auto;">
${currentAssignees.map(assignee => `
<div class="checkbox-item">
<label>
<input type="checkbox" class="remove-user-checkbox" value="${assignee.user_id}">
${assignee.user_name} (${assignee.user_login})
<small>Статус: ${getStatusText(assignee.status)}</small>
</label>
</div>
`).join('')}
</div>
<div class="modal-footer">
<button type="button" class="btn-cancel" onclick="closeManageAssigneesModal()">Отмена</button>
<button type="button" class="btn-danger" onclick="removeAssignees(${taskId})">Удалить выбранных</button>
</div>
` : '<p>Нет исполнителей для удаления</p>'}
</div>
<div id="replace-one-assignee-tab" class="tab-content">
<h4>Заменить одного исполнителя</h4>
${currentAssignees.length > 0 ? `
<div class="form-group">
<label>Выберите исполнителя для замены:</label>
<select id="old-assignee-select" class="form-control">
${currentAssignees.map(assignee => `
<option value="${assignee.user_id}">${assignee.user_name} (${assignee.user_login})</option>
`).join('')}
</select>
</div>
<div class="form-group">
<label>Выберите нового исполнителя:</label>
<select id="new-assignee-select" class="form-control">
<option value="">-- Выберите пользователя --</option>
${allUsers
.filter(user => !currentAssignees.some(a => a.user_id === user.id))
.map(user => `
<option value="${user.id}">${user.name} (${user.email})</option>
`).join('')}
</select>
</div>
<div class="modal-footer">
<button type="button" class="btn-cancel" onclick="closeManageAssigneesModal()">Отмена</button>
<button type="button" class="btn-warning" onclick="replaceAssignee(${taskId})">Заменить</button>
</div>
` : '<p>Нет исполнителей для замены</p>'}
</div>
<div id="replace-all-assignee-tab" class="tab-content">
<h4>Заменить всех исполнителей</h4>
<p>Текущие исполнители будут удалены, будут назначены новые.</p>
<div class="user-search-box">
<input type="text" id="replace-all-users-search" placeholder="Поиск пользователей..."
oninput="filterReplaceAllUsers()">
</div>
<div class="users-checklist" id="replace-all-users-list" style="max-height: 200px; overflow-y: auto;">
${allUsers.map(user => `
<div class="checkbox-item">
<label>
<input type="checkbox" class="replace-all-user-checkbox" value="${user.id}">
${user.name}
</label>
</div>
`).join('')}
</div>
<div class="modal-footer">
<button type="button" class="btn-cancel" onclick="closeManageAssigneesModal()">Отмена</button>
<button type="button" class="btn-warning" onclick="replaceAllAssignees(${taskId})">Заменить всех</button>
</div>
</div>
<div id="assign-to-user-tab" class="tab-content">
<h4>Назначить всем выбранному пользователю</h4>
<p>Все текущие исполнители будут удалены, задача будет назначена только выбранному пользователю.</p>
<div class="form-group">
<label>Выберите пользователя:</label>
<select id="target-user-select" class="form-control">
<option value="">-- Выберите пользователя --</option>
${allUsers.map(user => `
<option value="${user.id}" ${user.login === 'kalugin.o' ? 'selected' : ''}>
${user.name} (${user.email})
</option>
`).join('')}
</select>
</div>
<div class="modal-footer">
<button type="button" class="btn-cancel" onclick="closeManageAssigneesModal()">Отмена</button>
<button type="button" class="btn-primary" onclick="assignAllToUser(${taskId})">Назначить всем</button>
</div>
</div>
</div>
</div>
</div>
`;
// Добавляем модальное окно в DOM
const modalContainer = document.createElement('div');
modalContainer.innerHTML = modalHtml;
document.body.appendChild(modalContainer);
// Показываем модальное окно
setTimeout(() => {
document.getElementById('manage-assignees-modal').style.display = 'block';
}, 10);
} catch (error) {
console.error('Ошибка загрузки данных:', error);
alert('Ошибка загрузки данных пользователей');
}
}
// Функция для переключения вкладок в модальном окне
function switchManageTab(tabName) {
// Убираем активный класс со всех вкладок и контента
document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
// Активируем выбранную вкладку
const activeTabBtn = document.querySelector(`.tab-btn[onclick*="${tabName}"]`);
if (activeTabBtn) activeTabBtn.classList.add('active');
const activeContent = document.getElementById(`${tabName}-tab`);
if (activeContent) activeContent.classList.add('active');
}
// Функция для фильтрации доступных пользователей
function filterAvailableUsers() {
const search = document.getElementById('available-users-search')?.value.toLowerCase() || '';
const checkboxes = document.querySelectorAll('.available-user-checkbox');
checkboxes.forEach(checkbox => {
const label = checkbox.parentElement.textContent.toLowerCase();
const isVisible = label.includes(search) || search === '';
checkbox.parentElement.parentElement.style.display = isVisible ? '' : 'none';
});
}
// Функция для фильтрации пользователей для замены всех
function filterReplaceAllUsers() {
const search = document.getElementById('replace-all-users-search')?.value.toLowerCase() || '';
const checkboxes = document.querySelectorAll('.replace-all-user-checkbox');
checkboxes.forEach(checkbox => {
const label = checkbox.parentElement.textContent.toLowerCase();
const isVisible = label.includes(search) || search === '';
checkbox.parentElement.parentElement.style.display = isVisible ? '' : 'none';
});
}
// Функция для добавления исполнителей
async function addAssignees(taskId) {
const selectedCheckboxes = document.querySelectorAll('.available-user-checkbox:checked');
const assigneeIds = Array.from(selectedCheckboxes).map(cb => cb.value);
if (assigneeIds.length === 0) {
alert('Выберите хотя бы одного пользователя для добавления');
return;
}
try {
const response = await fetch(`/api/tasks/${taskId}/assignees`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ assigneeIds })
});
if (response.ok) {
const result = await response.json();
alert(result.message);
closeManageAssigneesModal();
loadTasks(); // Перезагружаем задачи
} else {
const error = await response.json();
alert(`Ошибка: ${error.error || 'Неизвестная ошибка'}`);
}
} catch (error) {
console.error('Ошибка:', error);
alert('Сетевая ошибка при добавлении исполнителей');
}
}
// Функция для удаления исполнителей
async function removeAssignees(taskId) {
const selectedCheckboxes = document.querySelectorAll('.remove-user-checkbox:checked');
const assigneeIds = Array.from(selectedCheckboxes).map(cb => cb.value);
if (assigneeIds.length === 0) {
alert('Выберите хотя бы одного исполнителя для удаления');
return;
}
if (!confirm(`Вы уверены, что хотите удалить ${assigneeIds.length} исполнителей из задачи?`)) {
return;
}
// Удаляем каждого исполнителя по отдельности
const results = [];
for (const assigneeId of assigneeIds) {
try {
const response = await fetch(`/api/tasks/${taskId}/assignees/${assigneeId}`, {
method: 'DELETE'
});
if (response.ok) {
results.push({ id: assigneeId, success: true });
} else {
const error = await response.json();
results.push({ id: assigneeId, success: false, error: error.error });
}
} catch (error) {
results.push({ id: assigneeId, success: false, error: error.message });
}
}
const successful = results.filter(r => r.success).length;
const failed = results.filter(r => !r.success);
if (failed.length > 0) {
alert(`Удалено ${successful} исполнителей. Ошибки: ${failed.map(f => `ID ${f.id}: ${f.error}`).join('; ')}`);
} else {
alert(`Успешно удалено ${successful} исполнителей`);
}
closeManageAssigneesModal();
loadTasks(); // Перезагружаем задачи
}
// Функция для замены одного исполнителя
async function replaceAssignee(taskId) {
const oldAssigneeId = document.getElementById('old-assignee-select').value;
const newAssigneeId = document.getElementById('new-assignee-select').value;
if (!oldAssigneeId || !newAssigneeId) {
alert('Выберите старого и нового исполнителя');
return;
}
if (!confirm('Вы уверены, что хотите заменить исполнителя?')) {
return;
}
try {
const response = await fetch(`/api/tasks/${taskId}/replace-assignee`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ oldAssigneeId, newAssigneeId })
});
if (response.ok) {
const result = await response.json();
alert(result.message);
closeManageAssigneesModal();
loadTasks(); // Перезагружаем задачи
} else {
const error = await response.json();
alert(`Ошибка: ${error.error || 'Неизвестная ошибка'}`);
}
} catch (error) {
console.error('Ошибка:', error);
alert('Сетевая ошибка при замене исполнителя');
}
}
// Функция для замены всех исполнителей
async function replaceAllAssignees(taskId) {
const selectedCheckboxes = document.querySelectorAll('.replace-all-user-checkbox:checked');
const newAssigneeIds = Array.from(selectedCheckboxes).map(cb => cb.value);
if (newAssigneeIds.length === 0) {
alert('Выберите хотя бы одного нового исполнителя');
return;
}
if (!confirm('Вы уверены, что хотите заменить всех исполнителей? Текущие исполнители будут удалены.')) {
return;
}
try {
const response = await fetch(`/api/tasks/${taskId}/replace-all-assignees`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ newAssigneeIds })
});
if (response.ok) {
const result = await response.json();
alert(result.message);
closeManageAssigneesModal();
loadTasks(); // Перезагружаем задачи
} else {
const error = await response.json();
alert(`Ошибка: ${error.error || 'Неизвестная ошибка'}`);
}
} catch (error) {
console.error('Ошибка:', error);
alert('Сетевая ошибка при замене исполнителей');
}
}
// Функция для назначения всех исполнителей одному пользователю
async function assignAllToUser(taskId) {
const targetUserId = document.getElementById('target-user-select').value;
if (!targetUserId) {
alert('Выберите пользователя для назначения');
return;
}
if (!confirm('Вы уверены, что хотите назначить задачу только этому пользователю? Все текущие исполнители будут удалены.')) {
return;
}
try {
const response = await fetch(`/api/tasks/${taskId}/assign-all-to-user`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ targetUserId })
});
if (response.ok) {
const result = await response.json();
alert(result.message);
closeManageAssigneesModal();
loadTasks(); // Перезагружаем задачи
} else {
const error = await response.json();
alert(`Ошибка: ${error.error || 'Неизвестная ошибка'}`);
}
} catch (error) {
console.error('Ошибка:', error);
alert('Сетевая ошибка при назначении задачи');
}
}
// Функция для закрытия модального окна управления исполнителями
function closeManageAssigneesModal() {
const modal = document.getElementById('manage-assignees-modal');
if (modal) {
modal.style.display = 'none';
// Удаляем модальное окно из DOM через некоторое время
setTimeout(() => {
modal.parentElement.remove();
}, 300);
}
}
// Обновленная функция рендеринга файлов с группировкой по пользователям
function renderGroupedFiles(task) {
if (!task.files || task.files.length === 0) {
return '<span class="no-files">нет файлов</span>';
}
// 1. Группируем ВСЕ файлы по пользователю, который их загрузил
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);
});
// 2. Определяем, какие группы (загрузчики) может видеть текущий пользователь
const visibleGroups = [];
for (const uploaderId in filesByUploader) {
const uploaderGroup = filesByUploader[uploaderId];
const uploaderIdNum = parseInt(uploaderId);
// Проверяем, может ли текущий пользователь видеть файлы этого загрузчика
let canSeeThisUploader = false;
// 1. Администратор видит все файлы всех загрузчиков
if (currentUser.role === 'admin') {
canSeeThisUploader = true;
}
// 2. Создатель задачи видит все файлы всех загрузчиков
else if (parseInt(task.created_by) === currentUser.id) {
canSeeThisUploader = true;
}
// 3. Исполнитель видит:
// - Файлы создателя задачи
// - Свои файлы (если он их загрузил)
else {
const creatorId = parseInt(task.created_by);
// Файлы загружены создателем задачи
if (uploaderIdNum === creatorId) {
canSeeThisUploader = true;
}
// Файлы загружены текущим пользователем
else if (uploaderIdNum === currentUser.id) {
canSeeThisUploader = true;
}
// Если файлы загружены другим исполнителем - не показываем эту группу
else {
canSeeThisUploader = false;
}
}
// Если текущий пользователь может видеть эту группу, добавляем её
if (canSeeThisUploader) {
// В группе показываем ТОЛЬКО файлы этого загрузчика
visibleGroups.push({
name: uploaderGroup.name,
id: uploaderGroup.id,
files: uploaderGroup.files // Все файлы этого загрузчика
});
}
}
if (visibleGroups.length === 0) {
return '<span class="no-files">нет файлов</span>';
}
// 3. Рендерим только видимые группы
// Если файлы загружены только одним пользователем, показываем просто список
if (visibleGroups.length === 1) {
const uploader = visibleGroups[0];
return `
<div class="file-group single-user">
<div class="file-group-header">
<strong>${uploader.name}:</strong>
</div>
<div class="file-icons-container">
${uploader.files.map(file => renderFileIcon(file)).join('')}
</div>
</div>
`;
}
// Если файлы загружены несколькими пользователями, группируем
return visibleGroups.map(uploader => `
<div class="file-group">
<div class="file-group-header">
<strong>${uploader.name}:</strong>
</div>
<div class="file-icons-container">
${uploader.files.map(file => renderFileIcon(file)).join('')}
</div>
</div>
`).join('');
}