Files
minicrm/public/ui.js
2026-03-26 17:20:04 +05:00

2241 lines
98 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(); // из tasks.js
} else if (sectionName === 'logs') {
loadActivityLogs();
} else if (sectionName === 'kanban') {
loadKanbanTasks();
} else if (sectionName === 'mytasks') {
console.log('загружаю loadMyTasks');
window.currentTaskView = 'my_assigned';
loadTasks();
} else if (sectionName === 'runtasks') {
console.log('загружаю loadRunTasks');
window.currentTaskView = 'assigned_to_me';
loadTasks();
}
// Загрузка профиля при переходе в личный кабинет
if (sectionName === 'profile') {
loadUserProfile();
loadNotificationSettings();
}
}
// Вызываем добавление стилей при загрузке страницы
document.addEventListener('DOMContentLoaded', function() {
addDocumentFieldsStyles();
});
// Функция для красивого отображения реквизитов документа
function renderDocumentFields(taskId, fields) {
const hasData = fields.document_n || fields.document_d || fields.document_a;
if (!hasData) {
return `
<div class="document-fields-container empty">
<div class="document-fields-header">
<span class="document-icon">📄</span>
<span class="document-title">Реквизиты документа</span>
</div>
<div class="document-fields-empty">
<span class="empty-message">Реквизиты документа отсутствуют</span>
</div>
</div>
`;
}
// Формируем поля с проверкой на длинные значения
const fields_html = [];
if (fields.document_n && fields.document_d) {
const numberValue = escapeHtml(fields.document_n);
const dateValue = escapeHtml(fields.document_d);
const isLongNumber = numberValue.length > 20;
fields_html.push(`
<div class="document-field ${isLongNumber ? 'long-value' : ''}">
<span class="field-label">Номер:</span>
<span class="field-value document-number" title="${numberValue}">${numberValue}</span>
</div>
`);
}
if (fields.document_n && fields.document_d) {
const numberValue = escapeHtml(fields.document_n);
const dateValue = escapeHtml(fields.document_d);
const isLongNumber = numberValue.length > 20;
fields_html.push(`
<div class="document-field ${isLongNumber ? 'long-value' : ''}">
<span class="field-label">Дата:</span>
<span class="field-value document-date">${dateValue}</span>
</div>
`);
}
if (fields.document_a) {
const authorValue = escapeHtml(fields.document_a);
const isLongAuthor = authorValue.length > 15;
fields_html.push(`
<div class="document-field ${isLongAuthor ? 'long-value' : ''}">
<span class="field-label">Автор:</span>
<span class="field-value document-author" title="${authorValue}">${authorValue}</span>
</div>
`);
}
return `
<div class="document-fields-container">
<div class="document-fields-header">
<span class="document-icon">📄</span>
<span class="document-title">Реквизиты документа</span>
</div>
<div class="document-fields-content">
${fields_html.join('')}
</div>
</div>
`;
}
// ========== УДАЛЕНА функция loadTasks теперь используется общая из tasks.js ==========
// Функция рендеринга задач в общий контейнер (секция "Все задачи")
function renderTasks() {
const container = document.getElementById('tasks-list');
const showDeleted = document.getElementById('show-deleted')?.checked || false;
// Получаем выбранное значение из селекта статусов
const statusFilter = document.getElementById('status-filter')?.value;
let filteredTasks = window.tasks;
if (!showDeleted) {
filteredTasks = window.tasks.filter(task => task.status === 'active');
}
if (statusFilter !== 'completed') {
filteredTasks = filteredTasks.filter(task => {
const myAssignment = task.assignments?.find(a => parseInt(a.user_id) === currentUser.id);
return !myAssignment || myAssignment.status !== 'completed';
});
}
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);
// Получаем реквизиты документа (если есть)
const documentFields = task.document_fields || {};
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>
${task.assignments && task.assignments.length > 0 ? `<span class="task-number">${task.assignments.map(a => a.user_login || a.user_name).join(', ')}</span>` : ''}
</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>
${isExpanded ? (window.renderTaskActions ? window.renderTaskActions(task) : '') : ''}
${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>` : ''}
${currentUser && currentUser.login === 'minicrm' ? `<button class="manage-assignees-btn" onclick="assignAdd_openModal(${task.id})" title="Управление исполнителями">🧑‍💼➕Добавить</button>` : ''}
${currentUser && currentUser.login === 'minicrm' ? `<button class="manage-assignees-btn" onclick="assignRemove_openModal(${task.id})" title="Управление исполнителями">🧑‍💼❌Удалить</button>` : ''}
${currentUser && currentUser.login === 'minicrm' ? `<button class="rework-btn" onclick="openReworkModal(${task.id})" title="Вернуть на доработку">🔄</button>` : ''}
<!-- Кнопка переделки документа для исполнителей -->
${task.task_type === 'document' && userRole === 'Исполнитель' && currentUser.login === 'minicrm' ? `<button class="rework-btn" onclick="openReworkModal(${task.id})" title="Переделать документ">🔄Переделать</button>` : ''}
${!isDeleted && !isClosed && task.task_type !== 'regular' && task.assignments && task.assignments.some(a => parseInt(a.user_id) === currentUser.id) && currentUser.login === 'minicrm' ? `
<button class="rework-btn" onclick="openReworkModal(${task.id})" title="Вернуть на доработку">🔄Доработка</button>
<button class="change-deadline-btn" onclick="openChangeDeadlineModal(${task.id})" title="Изменить срок">📅</button>
` : ''}
${currentUser && currentUser.login === 'minicrm' ? `<button class="close-btn" onclick="closeTask(${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>
` : ''}
<!-- Отображение реквизитов документа -->
${task.task_type === 'document' ? renderDocumentFields(task.id, documentFields) : ''}
<div class="file-list" id="files-${task.id}">
<strong>Файлы:</strong>
${task.files && task.files.length > 0 ? renderGroupedFilesWithDelete(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('');
}
// Функция для рендеринга задач в указанный контейнер (для секций mytasks и runtasks)
function renderTasksInContainer(containerId, tasksArray) {
const container = document.getElementById(containerId);
if (!container) return;
let filteredTasks = tasksArray;
if (containerId === 'runtasks-list' && window.currentTaskView === 'assigned_to_me' && currentUser) {
filteredTasks = tasksArray.filter(task => {
const myAssignment = task.assignments?.find(a => parseInt(a.user_id) === currentUser.id);
console.log('Task', task.id, 'myAssignment status:', myAssignment?.status);
return !myAssignment || myAssignment.status !== 'completed';
});
}
if (!filteredTasks || 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);
const documentFields = task.document_fields || {};
// Тот же шаблон, что и в renderTasks
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>
${task.assignments && task.assignments.length > 0 ? `<span class="task-number">${task.assignments.map(a => a.user_login || a.user_name).join(', ')}</span>` : ''}
</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 === 'kalugin.o' ? `<button class="manage-assignees-btn" onclick="openManageAssigneesModal(${task.id})" title="Управление исполнителями">👥</button>` : ''}
${currentUser && currentUser.role === 'tasks' && canEdit || currentUser.role === 'admin' ? `<button class="manage-assignees-btn" onclick="assignAdd_openModal(${task.id})" title="Управление исполнителями">🧑‍💼➕Добавить</button>` : ''}
${currentUser && currentUser.role === 'tasks' && canEdit || currentUser.role === 'admin' ? `<button class="manage-assignees-btn" onclick="assignRemove_openModal(${task.id})" title="Управление исполнителями">🧑‍💼❌Удалить</button>` : ''}
${currentUser && currentUser.login === 'minicrm' ? `<button class="rework-btn" onclick="openReworkModal(${task.id})" title="Вернуть на доработку">🔄</button>` : ''}
<!-- Кнопка переделки документа для исполнителей -->
${task.task_type === 'document' && userRole === 'Исполнитель' ? `<button class="rework-btn" onclick="openReworkModal(${task.id})" title="Переделать документ">🔄Переделать</button>` : ''}
${!isDeleted && !isClosed && task.task_type !== 'regular' && task.assignments && task.assignments.some(a => parseInt(a.user_id) === currentUser.id) ? `
<button class="rework-btn" onclick="openReworkModal(${task.id})" title="Вернуть на доработку">🔄Доработка</button>
<button class="change-deadline-btn" onclick="openChangeDeadlineModal(${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>
` : ''}
${task.task_type === 'document' ? renderDocumentFields(task.id, documentFields) : ''}
<div class="file-list" id="files-${task.id}">
<strong>Файлы:</strong>
${task.files && task.files.length > 0 ? renderGroupedFilesWithDelete(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);
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('Сетевая ошибка при выполнении операции');
}
}
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) {
const textarea = document.getElementById(`rework-comment-${taskId}-${userId}`);
if (!textarea) {
alert('Ошибка: поле комментария не найдено');
return;
}
const comment = textarea.value.trim();
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} исполнителей`;
}
}
// Добавим проверку на существование глобального множества expandedTasks
if (typeof expandedTasks === 'undefined') {
window.expandedTasks = new Set();
}
async function toggleTask(taskId) {
const wasExpanded = expandedTasks.has(taskId);
// Сворачиваем все задачи
expandedTasks.clear();
if (!wasExpanded) {
// Если задача была свёрнута разворачиваем её и загружаем файлы
expandedTasks.add(taskId);
await loadTaskFiles(taskId); // ждём загрузки файлов
}
// если была развёрнута после clear() она свернута, loadTaskFiles не нужен
// Перерисовываем активную секцию
const activeSection = document.querySelector('.section.active');
if (activeSection) {
const sectionId = activeSection.id;
if (sectionId === 'mytasks-section') {
renderTasksInContainer('mytasks-list', window.tasks);
} else if (sectionId === 'runtasks-section') {
renderTasksInContainer('runtasks-list', window.tasks);
} else if (sectionId === 'tasks-section') {
renderTasks();
}
} else {
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;
const isDocumentTask = task && task.task_type === 'document';
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="${isDocumentTask ? `openDocumentCompleteModal(${taskId}, ${assignment.user_id})` : `updateStatus(${taskId}, ${assignment.user_id}, 'completed')`}">Выполнено</button>` : ''}
${isTaskCreator && assignment.status !== 'assigned' && task.task_type !== 'document' ?
`<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 openDocumentCompleteModal(taskId, userId) {
const task = tasks.find(t => t.id === taskId);
if (!task) {
alert('Задача не найдена');
return;
}
const modalHtml = `
<div class="modal" id="document-complete-modal">
<div class="modal-content" style="max-width: 500px;">
<div class="modal-header">
<h3>📄 Завершение документа</h3>
<span class="close" onclick="closeDocumentCompleteModal()">&times;</span>
</div>
<div class="modal-body">
<p><strong>Задача:</strong> ${escapeHtml(task.title)}</p>
<div class="form-group">
<label for="doc-number"><strong>Номер документа:</strong></label>
<input type="text"
id="doc-number"
class="form-control"
placeholder="Введите номер документа"
maxlength="50">
</div>
<div class="form-group">
<label for="doc-date"><strong>Дата документа:</strong></label>
<input type="date"
id="doc-date"
class="form-control"
value="${new Date().toISOString().split('T')[0]}">
</div>
<div class="modal-footer" style="margin-top: 20px; text-align: right;">
<button type="button" class="btn-cancel" onclick="closeDocumentCompleteModal()">Отмена</button>
<button type="button" class="btn-success" onclick="submitDocumentComplete(${taskId}, ${userId})">✅ Завершить</button>
</div>
</div>
</div>
</div>
`;
const modalContainer = document.createElement('div');
modalContainer.innerHTML = modalHtml;
document.body.appendChild(modalContainer);
setTimeout(() => {
document.getElementById('document-complete-modal').style.display = 'block';
document.getElementById('doc-number').focus();
}, 10);
}
function closeDocumentCompleteModal() {
const modal = document.getElementById('document-complete-modal');
if (modal) {
modal.style.display = 'none';
setTimeout(() => {
modal.parentElement.remove();
}, 300);
}
}
async function submitDocumentComplete(taskId, userId) {
const docNumber = document.getElementById('doc-number').value.trim();
const docDate = document.getElementById('doc-date').value;
if (!docNumber) {
alert('Пожалуйста, укажите номер документа');
document.getElementById('doc-number').focus();
return;
}
if (!docDate) {
alert('Пожалуйста, укажите дату документа');
return;
}
const submitBtn = document.querySelector('#document-complete-modal .btn-success');
if (submitBtn) {
submitBtn.disabled = true;
submitBtn.innerHTML = '⏳ Сохранение...';
}
try {
const docResponse = await fetch(`/api/tasks/${taskId}/document-fields`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
document_n: docNumber,
document_d: formatDateForDocument(docDate),
document_a: currentUser.login
})
});
if (!docResponse.ok) {
const error = await docResponse.json();
throw new Error(error.error || 'Ошибка сохранения реквизитов документа');
}
await updateStatus(taskId, userId, 'completed');
closeDocumentCompleteModal();
} catch (error) {
console.error('❌ Ошибка:', error);
alert(`❌ Ошибка: ${error.message}`);
if (submitBtn) {
submitBtn.disabled = false;
submitBtn.innerHTML = '✅ Завершить';
}
}
}
function formatDateForDocument(dateString) {
if (!dateString) return '';
const date = new Date(dateString);
const day = String(date.getDate()).padStart(2, '0');
const month = String(date.getMonth() + 1).padStart(2, '0');
const year = date.getFullYear();
return `${day}.${month}.${year}`;
}
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="название фаила ограничено 45 символами"></textarea>
</div>
<div class="modal-footer">
<!--
<button type="button" class="btn-primary" onclick="closeAddFileModal()">Отмена</button>
-->
<button type="submit" class="btn-primary">Добавить файл</button>
</div>
</form>
</div>
</div>
</div>
`;
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();
formData.append('files', file);
formData.append('task_id', taskId);
if (description) {
formData.append('description', description);
}
try {
let response = await fetch(`/api/tasks/${taskId}/files`, {
method: 'POST',
body: formData
});
if (!response.ok) {
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';
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;
const fileContainer = document.getElementById(`files-${taskId}`);
if (fileContainer) {
fileContainer.innerHTML = `
<strong>Файлы:</strong>
${allFiles.length > 0 ?
(typeof renderGroupedFilesWithDelete === 'function' ?
renderGroupedFilesWithDelete(tasks[taskIndex]) :
renderGroupedFiles(tasks[taskIndex])) :
'<span class="no-files">нет файлов</span>'}
`;
}
} catch (error) {
console.error('Ошибка загрузки файлов:', error);
}
}
async function selectTaskType(type) {
// Управление видимостью дополнительных полей для ИТ
const itFields = document.getElementById('it-additional-fields');
const cabinetInput = document.getElementById('it-cabinet');
const corpusInput = document.getElementById('it-corpus');
const problemSelect = document.getElementById('it-problem-type');
if (type === 'it') {
if (itFields) itFields.style.display = 'block';
if (cabinetInput) cabinetInput.setAttribute('required', 'required');
if (corpusInput) corpusInput.setAttribute('required', 'required');
if (problemSelect) problemSelect.setAttribute('required', 'required');
} else {
if (itFields) itFields.style.display = 'none';
if (cabinetInput) cabinetInput.removeAttribute('required');
if (corpusInput) corpusInput.removeAttribute('required');
if (problemSelect) problemSelect.removeAttribute('required');
}
document.querySelectorAll('.task-type-btn').forEach(btn => {
btn.classList.remove('active');
});
const selectedBtn = document.querySelector(`.task-type-btn[data-type="${type}"]`);
if (selectedBtn) {
selectedBtn.classList.add('active');
}
document.getElementById('task-type').value = type;
updateTaskFormBasedOnType(type);
suggestDefaultTitle(type);
selectedUsers = [];
const checklist = document.getElementById('users-checklist');
if (checklist) {
checklist.innerHTML = '<div class="loading-spinner">⏳ Загрузка доступных пользователей...</div>';
}
try {
if (type === 'document') {
await reloadUsersForDocumentType();
} else if (['it', 'ahch', 'psychologist', 'speech_therapist', 'Social_educator', 'hr', 'certificate', 'e_journal'].includes(type)) {
const groupNames = {
'it': 'ИТ специалист',
'ahch': 'АХЧ',
'psychologist': 'психолог',
'speech_therapist': 'логопед',
'Social_educator': 'Социальный педагог',
'hr': 'Диспетчер',
'certificate': 'Администрация',
'e_journal': 'Админ ЭЖ'
};
await reloadUsersForType(groupNames[type]);
} else {
await loadUsers();
}
} catch (error) {
console.error('Ошибка загрузки пользователей:', error);
if (checklist) {
checklist.innerHTML = '<div class="error">Ошибка загрузки пользователей</div>';
}
}
}
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': 'Заявка к логопеду: ',
'Social_educator': 'Заявка к cоциальному педагогу: ',
'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');
const defaultPlaceholders = {
'regular': 'Поиск исполнителей...',
'document': 'Поиск секретарей/администрации...',
'it': 'Поиск ИТ специалистов...',
'ahch': 'Поиск АХЧ сотрудников...',
'psychologist': 'Поиск психологов...',
'speech_therapist': 'Поиск логопедов...',
'Social_educator': 'Поиск cоциальных педагогов...: ',
'hr': 'Поиск сотрудников кадровой службы...',
'certificate': 'Поиск секретаря/завуча...',
'e_journal': 'Поиск администратора электронного журнала...'
};
if (userSearchField) {
userSearchField.placeholder = defaultPlaceholders[type] || 'Поиск исполнителей...';
}
if (taskTypeInfo) {
const typeInfo = {
'regular': 'Доступны все пользователи в зависимости от ваших прав',
'document': 'Доступны только пользователи из группы "Секретарь"',
'it': 'Доступны ИТ специалисты',
'ahch': 'Доступны сотрудники АХЧ',
'psychologist': 'Доступны психологи',
'speech_therapist': 'Доступны логопеды',
'Social_educator': 'Доступны cоциальные педагоги: ',
'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': 'повод для обращения к логопеду',
'Social_educator': 'повод для обращения к cоциальному педагогу: ',
'hr': 'вопрос к кадровой службе',
'certificate': 'тип необходимой справки',
'e_journal': 'информацию для доступа к журналу',
'acquaintance': 'Ознакомление с документом'
};
return typeNames[type] || 'задачу';
}
function getTaskTypeDisplayName(type) {
const typeNames = {
'regular': 'Задача',
'document': 'Документ',
'it': 'ИТ',
'ahch': 'АХЧ',
'psychologist': 'Психолог',
'speech_therapist': 'Логопед',
'Social_educator': 'Социальный педагог: ',
'hr': 'Кадры',
'certificate': 'Справка',
'e_journal': 'Эл. журнал',
'acquaintance': 'Ознакомление'
};
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',
'Social_educator': 'fas fa-brain',
'hr': 'fas fa-users',
'certificate': 'fas fa-file-certificate',
'e_journal': 'fas fa-book',
'acquaintance': 'fas fa-eye'
};
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>
`;
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('Ошибка загрузки данных пользователей');
}
}
async function assignAdd_openModal(taskId) {
const task = tasks.find(t => t.id === taskId);
if (!task) {
alert('Задача не найдена');
return;
}
if (!canUserEditTask(task)) {
alert('У вас нет прав для управления исполнителями этой задачи');
return;
}
try {
const response = await fetch(`/api/tasks/${taskId}/available-assignees`);
let availableUsers = await response.json();
const taskCreatorId = task.created_by;
availableUsers = availableUsers.filter(user =>
parseInt(user.id) !== parseInt(taskCreatorId)
);
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 id="add-assignee-tab" class="tab-content active">
<h4>Добавить исполнителей</h4>
<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>
</div>
</div>
`;
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('Ошибка загрузки данных пользователей');
}
}
async function assignRemove_openModal(taskId) {
const task = tasks.find(t => t.id === taskId);
if (!task) {
alert('Задача не найдена');
return;
}
if (!canUserEditTask(task)) {
alert('У вас нет прав для управления исполнителями этой задачи');
return;
}
const currentAssignees = (task.assignments || []).filter(assignee =>
assignee.status !== 'deleted' && assignee.status !== 'удален'
);
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 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>
</div>
</div>
`;
const modalContainer = document.createElement('div');
modalContainer.innerHTML = modalHtml;
document.body.appendChild(modalContainer);
setTimeout(() => {
document.getElementById('manage-assignees-modal').style.display = 'block';
}, 10);
}
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';
setTimeout(() => {
modal.parentElement.remove();
}, 300);
}
}
function renderGroupedFiles(task) {
if (!task.files || task.files.length === 0) {
return '<span class="no-files">нет файлов</span>';
}
const filesByUploader = {};
task.files.forEach(file => {
const uploaderId = file.user_id;
const uploaderName = file.user_name || 'Неизвестный пользователь';
if (!filesByUploader[uploaderId]) {
filesByUploader[uploaderId] = {
name: uploaderName,
id: uploaderId,
files: []
};
}
filesByUploader[uploaderId].files.push(file);
});
const visibleGroups = [];
for (const uploaderId in filesByUploader) {
const uploaderGroup = filesByUploader[uploaderId];
const uploaderIdNum = parseInt(uploaderId);
let canSeeThisUploader = false;
if (currentUser.role === 'admin') {
canSeeThisUploader = true;
}
else if (parseInt(task.created_by) === currentUser.id) {
canSeeThisUploader = true;
}
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>';
}
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('');
}
// ++++++++++++++++++++++++++++++ кнопки изменения срока задачи для исполнителей ++++++++++++++++++++++++++++++
// Функция для открытия модального окна изменения срока задачи
function openChangeDeadlineModal(taskId) {
const task = tasks.find(t => t.id === taskId);
if (!task) {
alert('Задача не найдена');
return;
}
// Проверяем, что задача не обычная (regular)
if (task.task_type === 'regular') {
alert('Для обычных задач изменение срока недоступно');
return;
}
// Проверяем, является ли пользователь исполнителем
const isExecutor = task.assignments && task.assignments.some(a =>
parseInt(a.user_id) === currentUser.id
);
if (!isExecutor && currentUser.role !== 'admin') {
alert('Только исполнители могут изменять срок задачи');
return;
}
const modalHtml = `
<div class="modal" id="change-deadline-modal">
<div class="modal-content" style="max-width: 500px;">
<div class="modal-header">
<h3>📅 Изменение срока задачи</h3>
<span class="close" onclick="closeChangeDeadlineModal()">&times;</span>
</div>
<div class="modal-body">
<p><strong>Задача:</strong> ${escapeHtml(task.title)}</p>
<p><strong>Тип:</strong> ${getTaskTypeDisplayName(task.task_type)}</p>
<p><strong>Текущий срок:</strong> ${formatDateTime(task.due_date)}</p>
<div class="form-group">
<label for="new-deadline-date"><strong>Новая дата выполнения:</strong></label>
<input type="date"
id="new-deadline-date"
class="form-control"
value="${new Date().toISOString().split('T')[0]}"
min="${new Date().toISOString().split('T')[0]}">
</div>
<div class="form-group">
<label for="deadline-change-comment"><strong>Комментарий к изменению срока:</strong></label>
<textarea id="deadline-change-comment"
class="form-control"
rows="4"
placeholder="Укажите причину изменения срока..."
required></textarea>
</div>
<div class="modal-footer">
<button type="button" class="nav-btn admin" onclick="closeChangeDeadlineModal()">Отмена</button>
<button type="button" class="btn-primary" onclick="submitDeadlineChange(${taskId})">✅ Изменить срок</button>
</div>
</div>
</div>
</div>
`;
const modalContainer = document.createElement('div');
modalContainer.innerHTML = modalHtml;
document.body.appendChild(modalContainer);
setTimeout(() => {
document.getElementById('change-deadline-modal').style.display = 'block';
document.getElementById('deadline-change-comment').focus();
}, 10);
}
function closeChangeDeadlineModal() {
const modal = document.getElementById('change-deadline-modal');
if (modal) {
modal.style.display = 'none';
setTimeout(() => {
modal.parentElement.remove();
}, 300);
}
}
async function submitDeadlineChange(taskId) {
const newDate = document.getElementById('new-deadline-date').value;
const comment = document.getElementById('deadline-change-comment').value.trim();
if (!newDate) {
alert('Пожалуйста, выберите новую дату');
return;
}
if (!comment) {
alert('Пожалуйста, укажите комментарий к изменению срока');
document.getElementById('deadline-change-comment').style.border = '2px solid red';
document.getElementById('deadline-change-comment').focus();
return;
}
// Формируем дату с временем 19:01 по местному времени
const deadlineDateTime = `${newDate}T19:01:00`;
const submitBtn = document.querySelector('#change-deadline-modal .btn-primary');
if (submitBtn) {
submitBtn.disabled = true;
submitBtn.innerHTML = '⏳ Сохранение...';
}
try {
// Получаем текущие данные задачи
const task = tasks.find(t => t.id === taskId);
// Обновляем задачу с новой датой
const response = await fetch(`/api/tasks/${taskId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
title: task.title,
description: task.description,
dueDate: deadlineDateTime,
taskType: task.task_type
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Ошибка изменения срока');
}
// Добавляем комментарий в лог активности через отдельный эндпоинт
// (если есть API для комментариев)
try {
await fetch(`/api/tasks/${taskId}/comment`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
comment: `📅 Изменение срока: ${comment}`,
type: 'deadline_change'
})
});
} catch (commentError) {
console.warn('Не удалось сохранить комментарий:', commentError);
}
alert('✅ Срок задачи успешно изменен');
closeChangeDeadlineModal();
// Перезагружаем задачи
if (typeof loadTasks === 'function') {
loadTasks();
} else {
location.reload();
}
} catch (error) {
console.error('❌ Ошибка:', error);
alert(`❌ Ошибка: ${error.message}`);
if (submitBtn) {
submitBtn.disabled = false;
submitBtn.innerHTML = '✅ Изменить срок';
}
}
}
// ++++++++++++++++++++++++++++++ конец кнопок изменения срока ++++++++++++++++++++++++++++++