// reports.js – Отчёт по задачам с фильтрацией и группировкой let reportData = []; // все назначения для отчёта let currentReportFiltered = []; // отфильтрованные данные // Конфигурация статусов и типов для фильтров const STATUS_OPTIONS = [ { value: '', label: 'Все статусы' }, { value: 'assigned', label: 'Назначена' }, { value: 'assigned_overdue', label: 'Назначена (просрочена)' }, { value: 'in_progress', label: 'В работе' }, { value: 'in_progress_overdue', label: 'В работе (просрочена)' }, { value: 'completed', label: 'Выполнена' }, { value: 'completed_after_due', label: 'Выполнена после срока' }, { value: 'overdue', label: 'Просрочена (системная)' }, { value: 'rework', label: 'На доработке' }, { value: 'deleted', label: 'Удалена' } ]; const TASK_TYPE_OPTIONS = [ { value: '', label: 'Все типы' }, { value: 'regular', label: 'Обычная задача' }, { value: 'document', label: 'Согласование документа' }, { value: 'it', label: 'ИТ отдел' }, { value: 'ahch', label: 'АХЧ' }, { value: 'psychologist', label: 'Психолог' }, { value: 'speech_therapist', label: 'Логопед' }, { value: 'hr', label: 'Диспетчер расписания' }, { value: 'certificate', label: 'Справка' }, { value: 'e_journal', label: 'Эл. журнал' } ]; // Функция показа секции отчёта function showReportsSection() { // Используем глобальный currentUser из auth.js if (typeof currentUser === 'undefined' || !currentUser) { console.error('currentUser не определён'); return; } showSection('reports'); if (reportData.length === 0) { loadReportData(); } else { applyFilters(); } } // Проверка, имеет ли пользователь право видеть все задачи function canViewAllTasks() { if (!currentUser) return false; // Администратор if (currentUser.role === 'admin') return true; // Проверка группы "Руководители" (предполагаем, что группы хранятся в currentUser.groups) if (currentUser.groups && Array.isArray(currentUser.groups)) { if (currentUser.groups.some(g => g === 'Руководители' || g.includes('Руководители'))) { return true; } } return false; } // Вычисление уточнённого статуса с учётом просрочки function computeDisplayStatus(item) { const now = new Date(); const due = item.due_date ? new Date(item.due_date) : null; const completedAt = item.status === 'completed' && item.status_updated_at ? new Date(item.status_updated_at) : null; if (item.status === 'assigned') { if (due && due < now) return 'assigned_overdue'; return 'assigned'; } if (item.status === 'in_progress') { if (due && due < now) return 'in_progress_overdue'; return 'in_progress'; } if (item.status === 'completed') { if (due && completedAt && completedAt > due) return 'completed_after_due'; return 'completed'; } return item.status; } // Загрузка данных для отчёта async function loadReportData() { try { const tbody = document.getElementById('report-table-body'); tbody.innerHTML = 'Загрузка данных...'; const response = await fetch('/api/tasks?status=all&limit=1000'); if (!response.ok) throw new Error('Ошибка загрузки задач'); const tasks = await response.json(); reportData = []; const canViewAll = canViewAllTasks(); tasks.forEach(task => { // Если не админ и не руководитель, показываем только задачи, где пользователь - автор if (!canViewAll && task.created_by !== currentUser.id) { return; } if (task.assignments && task.assignments.length) { task.assignments.forEach(ass => { const item = { task_id: task.id, task_title: task.title, task_description: task.description || '', task_type: task.task_type || 'regular', due_date: task.due_date, user_id: ass.user_id, user_name: ass.user_name, status: ass.status, status_updated_at: ass.updated_at || task.updated_at, creator_name: task.creator_name, created_by: task.created_by }; item.displayStatus = computeDisplayStatus(item); reportData.push(item); }); } }); populateUserFilter(reportData); populateTaskIdFilter(reportData); applyFilters(); } catch (error) { console.error('Ошибка загрузки отчёта:', error); document.getElementById('report-table-body').innerHTML = 'Ошибка загрузки данных'; } } // Заполнение выпадающего списка пользователей function populateUserFilter(data) { const select = document.getElementById('report-user-filter'); const usersMap = new Map(); data.forEach(item => { const name = item.user_name || 'Без имени'; usersMap.set(item.user_id, name); }); let options = ''; const sorted = Array.from(usersMap.entries()).sort((a, b) => { const nameA = a[1] || ''; const nameB = b[1] || ''; return nameA.localeCompare(nameB); }); for (let [id, name] of sorted) { options += ``; } select.innerHTML = options; } // Заполнение выпадающего списка номеров задач (по убыванию) function populateTaskIdFilter(data) { const select = document.getElementById('report-task-id-filter'); const taskIds = [...new Set(data.map(item => item.task_id))]; taskIds.sort((a, b) => b - a); let options = ''; taskIds.forEach(id => { options += ``; }); select.innerHTML = options; } // Применение всех фильтров и рендеринг function applyFilters() { const userId = document.getElementById('report-user-filter').value; const statusFilter = document.getElementById('report-status-filter').value; const typeFilter = document.getElementById('report-type-filter').value; const taskIdFilter = document.getElementById('report-task-id-filter').value; currentReportFiltered = reportData.filter(item => { if (userId && item.user_id != userId) return false; if (typeFilter && item.task_type !== typeFilter) return false; if (taskIdFilter && item.task_id != taskIdFilter) return false; if (statusFilter) { if (item.displayStatus !== statusFilter) return false; } return true; }); renderReport(currentReportFiltered); } // Сброс всех фильтров function resetReportFilters() { document.getElementById('report-user-filter').value = ''; document.getElementById('report-status-filter').value = ''; document.getElementById('report-type-filter').value = ''; document.getElementById('report-task-id-filter').value = ''; applyFilters(); } // Рендеринг таблицы и сводки function renderReport(data) { const tbody = document.getElementById('report-table-body'); const summaryDiv = document.getElementById('report-summary'); if (!data.length) { tbody.innerHTML = 'Нет данных для отображения'; summaryDiv.innerHTML = ''; return; } const statusCounts = {}; data.forEach(item => { statusCounts[item.displayStatus] = (statusCounts[item.displayStatus] || 0) + 1; }); const statusLabels = { 'assigned': 'Назначена', 'assigned_overdue': 'Назначена (просрочена)', 'in_progress': 'В работе', 'in_progress_overdue': 'В работе (просрочена)', 'completed': 'Выполнена', 'completed_after_due': 'Выполнена после срока', 'overdue': 'Просрочена (системная)', 'rework': 'На доработке', 'deleted': 'Удалена' }; let summaryHtml = ''; for (let [status, count] of Object.entries(statusCounts)) { summaryHtml += `
${count} ${statusLabels[status] || status}
`; } summaryDiv.innerHTML = summaryHtml; tbody.innerHTML = data.map(item => ` ${item.task_id} ${escapeHtml(item.task_title)} ${formatDateTimereports(item.due_date) || '—'} ${escapeHtml(item.user_name || 'Неизвестно')} ${escapeHtml(item.creator_name || 'Неизвестно')} ${statusLabels[item.displayStatus] || item.displayStatus} ${formatDateTimereports(item.status_updated_at) || '—'} `).join(''); } function formatDateTimereports(dateTimeString) { if (!dateTimeString) return ''; let date; // Если строка в формате SQLite (без часового пояса) if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(dateTimeString)) { // Добавляем 'Z', чтобы интерпретировать как UTC date = new Date(dateTimeString.replace(' ', 'T') + 'Z'); } else { // Стандартная дата с часовым поясом (например, с Z или смещением) date = new Date(dateTimeString); } return date.toLocaleString('ru-RU'); } // Вспомогательные функции function truncateText(text, maxLen) { if (!text) return ''; return text.length > maxLen ? text.substr(0, maxLen) + '…' : text; } function escapeHtml(unsafe) { if (!unsafe) return ''; return String(unsafe) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function printReport() { window.print(); } // Экспорт window.showReportsSection = showReportsSection; window.loadReportData = loadReportData; window.applyFilters = applyFilters; window.resetReportFilters = resetReportFilters; window.printReport = printReport;