diff --git a/public/index.html b/public/index.html index b3bcf3f..cb88a74 100644 --- a/public/index.html +++ b/public/index.html @@ -422,6 +422,7 @@ + diff --git a/public/navbar.js b/public/navbar.js index d9c98b5..d950890 100644 --- a/public/navbar.js +++ b/public/navbar.js @@ -1,3 +1,47 @@ +// Функция для проверки наличия указанной группы у текущего пользователя +function navbar_checkUserGroup(navbar_groupName) { + try { + // Проверяем, есть ли данные пользователя + if (!currentUser || !currentUser.id) { + console.error('Пользователь не аутентифицирован или данные отсутствуют'); + return false; + } + + console.log('Текущий пользователь:', currentUser.login || currentUser.name); + const navbar_currentUserId = currentUser.id; + + // Делаем синхронный запрос с помощью XMLHttpRequest + const xhr = new XMLHttpRequest(); + xhr.open('GET', `/api2/idusers/user/${navbar_currentUserId}/groups`, false); // false = синхронный запрос + xhr.send(); + + if (xhr.status !== 200) { + console.error('Ошибка получения групп пользователя'); + return false; + } + + const navbar_groups = JSON.parse(xhr.responseText); + + // Проверяем наличие указанной группы + const navbar_hasGroup = navbar_groups.some(userGroup => { + return userGroup === navbar_groupName || + userGroup.includes(navbar_groupName) || + userGroup.toLowerCase().includes(navbar_groupName.toLowerCase()); + }); + + if (navbar_hasGroup) { + console.log(`✓ Пользователь состоит в группе "${navbar_groupName}"`); + return true; + } else { + console.log(`✗ Пользователь НЕ состоит в группе "${navbar_groupName}"`); + return false; + } + + } catch (error) { + console.error(`Ошибка при проверке группы "${navbar_groupName}":`, error); + return false; + } +} // Функция для создания навигационной панели function createNavigation() { const navbar = document.getElementById('navbar-container'); @@ -38,6 +82,15 @@ navButtons.push( id: "create-task-btn" } ); + if (currentUser && navbar_checkUserGroup('Секретарь') || currentUser && currentUser.role === 'admin') { + navButtons.push({ + onclick: "TasksType.show('document')", + className: "nav-btn tasks", + icon: "fas fa-list", + text: "Согласование", + id: "create-task-btn" + }); + } navButtons.push( { onclick: "showKanbanSection()", diff --git a/public/tasks-type.js b/public/tasks-type.js new file mode 100644 index 0000000..debc877 --- /dev/null +++ b/public/tasks-type.js @@ -0,0 +1,949 @@ +// tasks-type.js - Управление отображением задач по типам +// Не конфликтует с ui.js, использует собственные функции и пространство имен + +const TasksType = (function() { + // Приватные переменные + let currentTasks = []; + let expandedTasks = new Set(); + let currentType = 'document'; // Тип по умолчанию + + // Конфигурация типов задач + const taskTypeConfig = { + 'document': { + endpoint: '/api/tasks_by_type?task_type=document', + title: 'Документы', + icon: '📄', + badgeClass: 'document', + emptyMessage: 'Нет задач по документам' + }, + 'it': { + endpoint: '/api/tasks_by_type?task_type=it', + title: 'ИТ задачи', + icon: '💻', + badgeClass: 'it', + emptyMessage: 'Нет ИТ задач' + }, + 'ahch': { + endpoint: '/api/tasks_by_type?task_type=ahch', + title: 'АХЧ задачи', + icon: '🔧', + badgeClass: 'ahch', + emptyMessage: 'Нет задач АХЧ' + }, + 'psychologist': { + endpoint: '/api/tasks_by_type?task_type=psychologist', + title: 'Психолог', + icon: '🧠', + badgeClass: 'psychologist', + emptyMessage: 'Нет задач для психолога' + }, + 'speech_therapist': { + endpoint: '/api/tasks_by_type?task_type=speech_therapist', + title: 'Логопед', + icon: '🗣️', + badgeClass: 'speech_therapist', + emptyMessage: 'Нет задач для логопеда' + }, + 'hr': { + endpoint: '/api/tasks_by_type?task_type=hr', + title: 'Кадры', + icon: '👥', + badgeClass: 'hr', + emptyMessage: 'Нет кадровых задач' + }, + 'certificate': { + endpoint: '/api/tasks_by_type?task_type=certificate', + title: 'Справки', + icon: '📜', + badgeClass: 'certificate', + emptyMessage: 'Нет задач по справкам' + }, + 'e_journal': { + endpoint: '/api/tasks_by_type?task_type=e_journal', + title: 'Электронный журнал', + icon: '📊', + badgeClass: 'e_journal', + emptyMessage: 'Нет задач по ЭЖ' + } + }; + + // Инициализация + function init() { + createTaskTypeSection(); + setupEventListeners(); + } + + // Создание секции для задач по типам + function createTaskTypeSection() { + // Проверяем, существует ли уже секция + if (document.getElementById('tasks-type-section')) { + return; + } + + const section = document.createElement('div'); + section.id = 'tasks-type-section'; + section.className = 'section'; + section.innerHTML = ` +
+
+

Документы

+ +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
⏳ Загрузка задач...
+
+
+ `; + + // Добавляем секцию после основного контента или в указанное место + const container = document.querySelector('.container') || document.body; + container.appendChild(section); + + // Добавляем стили + addStyles(); + } + + // Добавление стилей + function addStyles() { + if (document.getElementById('tasks-type-styles')) { + return; + } + + const style = document.createElement('style'); + style.id = 'tasks-type-styles'; + style.textContent = ` + .tasks-type-container { + padding: 20px; + max-width: 95%; + margin: 0 auto; + } + + .tasks-type-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + } + + .tasks-type-header h2 { + margin: 0; + font-size: 24px; + color: #333; + } + + .tasks-type-controls { + display: flex; + gap: 10px; + } + + .tasks-type-select { + padding: 8px 12px; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 14px; + background-color: white; + cursor: pointer; + } + + .tasks-type-select:hover { + border-color: #999; + } + + .tasks-type-refresh-btn { + padding: 8px 12px; + background-color: #f0f0f0; + border: 1px solid #ddd; + border-radius: 4px; + cursor: pointer; + font-size: 16px; + } + + .tasks-type-refresh-btn:hover { + background-color: #e0e0e0; + } + + .tasks-type-filters { + display: flex; + gap: 15px; + margin-bottom: 20px; + flex-wrap: wrap; + } + + .filter-group { + flex: 1; + min-width: 200px; + } + + .tasks-type-search-input, + .tasks-type-filter-select { + width: 100%; + padding: 8px 12px; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 14px; + } + + .tasks-type-checkbox { + display: flex; + align-items: center; + gap: 5px; + cursor: pointer; + } + + .tasks-type-list { + display: flex; + flex-direction: column; + gap: 15px; + } + + .tasks-type-card { + background-color: white; + border: 1px solid #e0e0e0; + border-radius: 8px; + overflow: hidden; + transition: box-shadow 0.3s; + } + + .tasks-type-card:hover { + box-shadow: 0 4px 12px rgba(0,0,0,0.1); + } + + .tasks-type-card.deleted { + opacity: 0.7; + background-color: #f9f9f9; + } + + .tasks-type-card.closed { + background-color: #f5f5f5; + } + + .tasks-type-header-card { + padding: 15px; + cursor: pointer; + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid #e0e0e0; + } + + .tasks-type-title-info { + flex: 1; + } + + .tasks-type-badge { + display: inline-block; + padding: 3px 8px; + border-radius: 4px; + font-size: 12px; + font-weight: bold; + margin-right: 10px; + } + + .tasks-type-badge.document { background-color: #e3f2fd; color: #1976d2; } + .tasks-type-badge.it { background-color: #f3e5f5; color: #7b1fa2; } + .tasks-type-badge.ahch { background-color: #fff3e0; color: #f57c00; } + .tasks-type-badge.psychologist { background-color: #e8f5e8; color: #388e3c; } + .tasks-type-badge.speech_therapist { background-color: #ffebee; color: #d32f2f; } + .tasks-type-badge.hr { background-color: #e1f5fe; color: #0288d1; } + .tasks-type-badge.certificate { background-color: #fce4ec; color: #c2185b; } + .tasks-type-badge.e_journal { background-color: #ede7f6; color: #512da8; } + + .tasks-type-status { + padding: 4px 10px; + border-radius: 4px; + font-size: 12px; + font-weight: 500; + margin: 0 10px; + } + + .tasks-type-status.status-red { background-color: #ffebee; color: #c62828; } + .tasks-type-status.status-orange { background-color: #fff3e0; color: #ef6c00; } + .tasks-type-status.status-green { background-color: #e8f5e8; color: #2e7d32; } + .tasks-type-status.status-yellow { background-color: #fff9c4; color: #fbc02d; } + .tasks-type-status.status-darkred { background-color: #ffcdd2; color: #b71c1c; } + .tasks-type-status.status-purple { background-color: #f3e5f5; color: #7b1fa2; } + .tasks-type-status.status-gray { background-color: #eeeeee; color: #616161; } + + .tasks-type-expand-icon { + margin-left: 10px; + transition: transform 0.3s; + } + + .tasks-type-content { + display: none; + padding: 15px; + } + + .tasks-type-content.expanded { + display: block; + } + + .tasks-type-actions { + display: flex; + gap: 8px; + margin-bottom: 15px; + flex-wrap: wrap; + } + + .tasks-type-btn { + padding: 6px 10px; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 13px; + transition: background-color 0.2s; + } + + .tasks-type-btn:hover { + opacity: 0.9; + } + + .tasks-type-description { + background-color: #f9f9f9; + padding: 10px; + border-radius: 4px; + margin: 10px 0; + } + + .tasks-type-rework { + background-color: #fff3e0; + padding: 10px; + border-radius: 4px; + margin: 10px 0; + border-left: 4px solid #ff9800; + } + + .tasks-type-files { + margin: 10px 0; + } + + .tasks-type-assignments { + margin: 10px 0; + } + + .tasks-type-assignment { + display: flex; + align-items: center; + padding: 8px; + border-bottom: 1px solid #f0f0f0; + } + + .tasks-type-assignment:last-child { + border-bottom: none; + } + + .tasks-type-assignment-status { + width: 10px; + height: 10px; + border-radius: 50%; + margin-right: 10px; + } + + .tasks-type-meta { + margin-top: 10px; + padding-top: 10px; + border-top: 1px solid #e0e0e0; + color: #666; + font-size: 12px; + } + + .tasks-type-empty { + text-align: center; + padding: 40px; + color: #999; + font-size: 16px; + background-color: #f9f9f9; + border-radius: 8px; + } + + .tasks-type-loading { + text-align: center; + padding: 40px; + color: #666; + } + + .tasks-type-error { + text-align: center; + padding: 40px; + color: #d32f2f; + background-color: #ffebee; + border-radius: 8px; + } + + .file-group { + margin: 10px 0; + } + + .file-icons-container { + display: flex; + gap: 10px; + flex-wrap: wrap; + margin-top: 5px; + } + + .file-icon-link { + text-decoration: none; + color: #1976d2; + padding: 4px 8px; + background-color: #e3f2fd; + border-radius: 4px; + font-size: 13px; + } + + .file-icon-link:hover { + background-color: #bbdefb; + } + + .deadline-indicator { + display: inline-block; + padding: 2px 6px; + border-radius: 3px; + font-size: 11px; + margin-left: 5px; + } + + .deadline-indicator.deadline-24h { + background-color: #ffebee; + color: #c62828; + } + + .deadline-indicator.deadline-48h { + background-color: #fff3e0; + color: #ef6c00; + } + `; + + document.head.appendChild(style); + } + + // Настройка обработчиков событий + function setupEventListeners() { + // Селектор типа задач + const selector = document.getElementById('tasks-type-selector'); + if (selector) { + selector.addEventListener('change', function(e) { + currentType = e.target.value; + updateTitle(); + loadTasks(); + }); + } + + // Кнопка обновления + const refreshBtn = document.getElementById('tasks-type-refresh'); + if (refreshBtn) { + refreshBtn.addEventListener('click', function() { + loadTasks(); + }); + } + + // Поиск + const searchInput = document.getElementById('tasks-type-search'); + if (searchInput) { + searchInput.addEventListener('input', debounce(function() { + loadTasks(); + }, 300)); + } + + // Фильтр статуса + const statusFilter = document.getElementById('tasks-type-status-filter'); + if (statusFilter) { + statusFilter.addEventListener('change', function() { + loadTasks(); + }); + } + + // Показ удаленных + const showDeleted = document.getElementById('tasks-type-show-deleted'); + if (showDeleted) { + showDeleted.addEventListener('change', function() { + loadTasks(); + }); + } + } + + // Debounce функция для поиска + function debounce(func, wait) { + let timeout; + return function executedFunction(...args) { + const later = () => { + clearTimeout(timeout); + func(...args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; + } + + // Обновление заголовка + function updateTitle() { + const title = document.getElementById('tasks-type-title'); + const config = taskTypeConfig[currentType] || taskTypeConfig.document; + if (title) { + title.textContent = `${config.icon} ${config.title}`; + } + } + + // Загрузка задач + async function loadTasks() { + const listContainer = document.getElementById('tasks-type-list'); + if (!listContainer) return; + + listContainer.innerHTML = '
⏳ Загрузка задач...
'; + + try { + const config = taskTypeConfig[currentType] || taskTypeConfig.document; + let url = config.endpoint; + + // Добавляем параметры фильтрации + const params = new URLSearchParams(); + + const search = document.getElementById('tasks-type-search')?.value; + if (search) { + params.append('search', search); + } + + const status = document.getElementById('tasks-type-status-filter')?.value; + if (status && status !== 'all') { + if (status === 'active') { + params.append('status', 'active'); + } else { + params.append('status', status); + } + } + + const showDeleted = document.getElementById('tasks-type-show-deleted')?.checked; + if (showDeleted) { + params.append('showDeleted', 'true'); + } + + const queryString = params.toString(); + if (queryString) { + url += '&' + queryString; + } + + const response = await fetch(url); + const data = await response.json(); + + // Проверяем структуру ответа + if (data.tasks) { + currentTasks = data.tasks; + } else if (Array.isArray(data)) { + currentTasks = data; + } else { + currentTasks = []; + } + + renderTasks(); + + } catch (error) { + console.error('Ошибка загрузки задач:', error); + listContainer.innerHTML = '
❌ Ошибка загрузки задач
'; + } + } + + // Рендеринг задач + function renderTasks() { + const container = document.getElementById('tasks-type-list'); + if (!container) return; + + if (!currentTasks || currentTasks.length === 0) { + const config = taskTypeConfig[currentType] || taskTypeConfig.document; + container.innerHTML = `
${config.emptyMessage}
`; + return; + } + + container.innerHTML = currentTasks.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 timeLeftInfo = getTimeLeftInfo(task); + const config = taskTypeConfig[task.task_type] || taskTypeConfig.document; + + return ` +
+
+
+ + ${config.icon} ${getTaskTypeDisplayName(task.task_type)} + + №${task.id} + ${escapeHtml(task.title)} + ${isDeleted ? 'Удалена' : ''} + ${isClosed ? 'Закрыта' : ''} + ${timeLeftInfo ? `${timeLeftInfo.text}` : ''} +
+ + Выполнить до: ${formatDateTime(task.due_date || task.created_at)} + +
+ ▼ +
+
+ +
+ ${isExpanded ? renderExpandedContent(task) : ''} +
+
+ `; + }).join(''); + } + + // Рендеринг развернутого содержимого + function renderExpandedContent(task) { + return ` +
+ ${currentUser && currentUser.login === 'minicrm' ? ` + + + + + + ` : ''} +
+ +
+ ${task.description || 'Нет описания'} +
+ + ${task.rework_comment ? ` +
+ Комментарий к доработке: ${escapeHtml(task.rework_comment)} +
+ ` : ''} + +
+ Файлы: + ${task.files && task.files.length > 0 ? + renderGroupedFiles(task) : + 'нет файлов'} +
+ +
+ Исполнители: + ${task.assignments && task.assignments.length > 0 ? + renderAssignments(task.assignments, task.id) : + '
Не назначены
'} +
+ +
+ + Создана: ${formatDateTime(task.start_date || task.created_at)} + | Автор: ${task.creator_name || 'Неизвестно'} + ${task.due_date ? `| Срок: ${formatDateTime(task.due_date)}` : ''} + +
+ `; + } + + // Рендеринг файлов + function renderGroupedFiles(task) { + if (!task.files || task.files.length === 0) { + return 'нет файлов'; + } + + const filesByUploader = {}; + + task.files.forEach(file => { + const uploaderId = file.user_id; + const uploaderName = file.user_name || 'Неизвестный пользователь'; + + if (!filesByUploader[uploaderId]) { + filesByUploader[uploaderId] = { + name: uploaderName, + files: [] + }; + } + filesByUploader[uploaderId].files.push(file); + }); + + return Object.values(filesByUploader).map(uploader => ` +
+
${escapeHtml(uploader.name)}:
+
+ ${uploader.files.map(file => ` + + 📎 ${escapeHtml(file.original_name).substring(0, 15)}${file.original_name.length > 15 ? '...' : ''} + + `).join('')} +
+
+ `).join(''); + } + + // Рендеринг исполнителей + function renderAssignments(assignments, taskId) { + return assignments.map(assignment => ` +
+ +
+ ${escapeHtml(assignment.user_name)} + ${assignment.user_id === currentUser?.id ? '(Вы)' : ''} + ${assignment.due_date ? ` + Срок: ${formatDateTime(assignment.due_date)} + ` : ''} +
+
+ ${assignment.user_id === currentUser?.id ? ` + + ` : ''} +
+
+ `).join(''); + } + + // Публичные методы + return { + init: init, + + show: function(type = 'document') { + currentType = type; + const selector = document.getElementById('tasks-type-selector'); + if (selector) { + selector.value = type; + } + updateTitle(); + loadTasks(); + + // Показываем секцию + const section = document.getElementById('tasks-type-section'); + if (section) { + // Скрываем другие секции + document.querySelectorAll('.section').forEach(s => { + s.classList.remove('active'); + }); + section.classList.add('active'); + } + }, + + loadTasks: loadTasks, + + toggleTask: function(taskId) { + if (expandedTasks.has(taskId)) { + expandedTasks.delete(taskId); + } else { + expandedTasks.add(taskId); + } + renderTasks(); + }, + + updateStatus: async function(taskId, userId, status) { + try { + const response = await fetch(`/api/tasks/${taskId}/status`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ userId, status }) + }); + + if (response.ok) { + await loadTasks(); + } else { + const error = await response.json(); + alert('Ошибка: ' + (error.error || 'Неизвестная ошибка')); + } + } catch (error) { + console.error('Ошибка:', error); + alert('Сетевая ошибка'); + } + }, + + openTaskChat: function(taskId) { + window.open(`/chat?task_id=${taskId}`, '_blank'); + }, + + openAddFileModal: function(taskId) { + if (typeof openAddFileModal === 'function') { + openAddFileModal(taskId); + } else { + alert('Функция добавления файлов недоступна'); + } + }, + + openEditModal: function(taskId) { + if (typeof openEditModal === 'function') { + openEditModal(taskId); + } else { + alert('Функция редактирования недоступна'); + } + }, + + openCopyModal: function(taskId) { + if (typeof openCopyModal === 'function') { + openCopyModal(taskId); + } else { + alert('Функция копирования недоступна'); + } + } + }; +})(); + +// Вспомогательные функции (не конфликтуют с ui.js) +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 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: `Осталось ${hoursLeft}ч`, + class: 'deadline-24h' + }; + } else if (hoursLeft <= 48) { + return { + text: `Осталось ${hoursLeft}ч`, + class: 'deadline-48h' + }; + } + + return null; +} + +function formatDateTime(dateTimeString) { + if (!dateTimeString) return ''; + const date = new Date(dateTimeString); + return date.toLocaleString('ru-RU'); +} + +function getTaskTypeDisplayName(type) { + const typeNames = { + 'regular': 'Задача', + 'document': 'Документ', + 'it': 'ИТ', + 'ahch': 'АХЧ', + 'psychologist': 'Психолог', + 'speech_therapist': 'Логопед', + 'hr': 'Кадры', + 'certificate': 'Справка', + 'e_journal': 'Эл. журнал' + }; + return typeNames[type] || type; +} + +function escapeHtml(text) { + if (!text) return ''; + return String(text) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +// Инициализация при загрузке страницы +document.addEventListener('DOMContentLoaded', function() { + // Проверяем, что currentUser определен (из основного скрипта) + if (typeof currentUser !== 'undefined') { + TasksType.init(); + } else { + // Ждем загрузки currentUser + const checkUser = setInterval(function() { + if (typeof currentUser !== 'undefined') { + clearInterval(checkUser); + TasksType.init(); + } + }, 100); + } +}); + +// Экспортируем в глобальную область +window.TasksType = TasksType; \ No newline at end of file diff --git a/task-endpoints.js b/task-endpoints.js index 1082657..b0c07c2 100644 --- a/task-endpoints.js +++ b/task-endpoints.js @@ -209,7 +209,185 @@ function setupTaskEndpoints(app, db, upload) { }); }); }); + // API для задач по типу +app.get('/api/tasks_by_type', requireAuth, (req, res) => { + const userId = req.session.user.id; + const userRole = req.session.user.role; + + // Получаем параметры фильтрации из query string + const showDeleted = userRole === 'admin' && req.query.showDeleted === 'true'; + const search = req.query.search || ''; + const statusFilter = req.query.status || 'active,in_progress,assigned,overdue,rework'; + const creatorFilter = req.query.creator || ''; + const assigneeFilter = req.query.assignee || ''; + const deadlineFilter = req.query.deadline || ''; + const taskType = req.query.task_type || ''; // Новый параметр для фильтрации по типу задачи + let query = ` + SELECT DISTINCT + t.*, + u.name as creator_name, + u.login as creator_login, + ot.title as original_task_title, + ou.name as original_creator_name, + GROUP_CONCAT(DISTINCT ta.user_id) as assigned_user_ids, + GROUP_CONCAT(DISTINCT u2.name) as assigned_user_names + FROM tasks t + LEFT JOIN users u ON t.created_by = u.id + LEFT JOIN tasks ot ON t.original_task_id = ot.id + LEFT JOIN users ou ON ot.created_by = ou.id + LEFT JOIN task_assignments ta ON t.id = ta.task_id + LEFT JOIN users u2 ON ta.user_id = u2.id + WHERE 1=1 + `; + + const params = []; + + // Фильтрация по типу задачи + if (taskType) { + if (taskType.includes(',')) { + // Если перечислено несколько типов через запятую + const types = taskType.split(','); + query += ` AND t.task_type IN (${types.map(() => '?').join(',')})`; + types.forEach(type => params.push(type)); + } else { + // Один конкретный тип + query += ` AND t.task_type = ?`; + params.push(taskType); + } + } + + // Фильтрация по правам доступа + if (userRole !== 'admin') { + query += ` AND (t.created_by = ? OR ta.user_id = ?)`; + params.push(userId, userId); + } + + // Фильтрация по удаленным задачам + if (!showDeleted) { + query += " AND t.status = 'active'"; + } + + // Фильтрация по статусу + if (statusFilter && statusFilter !== 'all') { + const statuses = statusFilter.split(','); + + if (statuses.includes('closed')) { + if (userRole !== 'admin') { + query += ` AND (t.closed_at IS NOT NULL AND t.created_by = ?)`; + params.push(userId); + } else { + query += ` AND t.closed_at IS NOT NULL`; + } + } else { + query += ` AND t.closed_at IS NULL`; + + if (statuses.length > 0 && !statuses.includes('all')) { + query += ` AND EXISTS ( + SELECT 1 FROM task_assignments ta2 + WHERE ta2.task_id = t.id AND ta2.status IN (${statuses.map(() => '?').join(',')}) + )`; + statuses.forEach(status => params.push(status)); + } + } + } else { + if (userRole !== 'admin') { + query += ` AND (t.closed_at IS NULL OR t.created_by = ?)`; + params.push(userId); + } + } + + // Фильтрация по создателю + if (creatorFilter) { + query += ` AND t.created_by = ?`; + params.push(creatorFilter); + } + + // Фильтрация по исполнителю + if (assigneeFilter) { + query += ` AND ta.user_id = ?`; + params.push(assigneeFilter); + } + + // Фильтрация по дедлайну + if (deadlineFilter) { + const now = new Date(); + let hours = 48; + if (deadlineFilter === '24h') hours = 24; + + const deadlineTime = new Date(now.getTime() + hours * 60 * 60 * 1000); + const deadlineISO = deadlineTime.toISOString(); + const nowISO = now.toISOString(); + + query += ` AND ta.due_date IS NOT NULL + AND ta.due_date > ? + AND ta.due_date <= ? + AND ta.status NOT IN ('completed', 'overdue')`; + params.push(nowISO, deadlineISO); + } + + // Поиск по тексту + if (search) { + query += ` AND (t.title LIKE ? OR t.description LIKE ?)`; + const searchPattern = `%${search}%`; + params.push(searchPattern, searchPattern); + } + + query += " GROUP BY t.id ORDER BY t.created_at DESC"; + + db.all(query, params, (err, tasks) => { + if (err) { + res.status(500).json({ error: err.message }); + return; + } + + const taskPromises = tasks.map(task => { + return new Promise((resolve) => { + db.all(` + SELECT ta.*, u.name as user_name, u.login as user_login + FROM task_assignments ta + LEFT JOIN users u ON ta.user_id = u.id + WHERE ta.task_id = ? + `, [task.id], (err, assignments) => { + if (err) { + task.assignments = []; + resolve(task); + return; + } + + assignments.forEach(assignment => { + if (checkIfOverdue(assignment.due_date, assignment.status) && assignment.status !== 'completed') { + assignment.status = 'overdue'; + } + }); + + task.assignments = assignments || []; + resolve(task); + }); + }); + }); + + Promise.all(taskPromises).then(completedTasks => { + // Добавляем мета-информацию о фильтрации + const response = { + tasks: completedTasks, + meta: { + total: completedTasks.length, + filters: { + task_type: taskType || 'all', + status: statusFilter, + creator: creatorFilter || 'all', + assignee: assigneeFilter || 'all', + deadline: deadlineFilter || 'none', + search: search || 'none' + } + } + }; + + res.json(response); + }); + }); +}); app.get('/api/tasks/no-date', requireAuth, (req, res) => { const userId = req.session.user.id;