diff --git a/public/auth.js b/public/auth.js index 1a695b8..0e3c78b 100644 --- a/public/auth.js +++ b/public/auth.js @@ -111,7 +111,8 @@ function reloadAllScripts() { 'chat-ui.js', 'loadMyCreatedTasks.js', 'main.js', - 'nav-task-actions.js' + 'nav-task-actions.js', + 'reports.js' ]; // Удаляем существующие скрипты diff --git a/public/index.html b/public/index.html index 4608c2d..0bb6247 100644 --- a/public/index.html +++ b/public/index.html @@ -335,6 +335,78 @@ +
+

Отчёт по задачам

+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + +
+ + + + + + + + + + + + + + + + +
№ задачиНазваниеОписаниеСрок выполненияИсполнительАвторСтатус исполнителяПоследнее изменение
Загрузка данных...
+
+
diff --git a/public/main.js b/public/main.js index c616266..e2468ea 100644 --- a/public/main.js +++ b/public/main.js @@ -566,6 +566,11 @@ function showSection(sectionName) { loadUserProfile(); loadNotificationSettings(); } +if (sectionName === 'reports') { + if (typeof loadReportData === 'function') { + loadReportData(); + } +} } // Функция для отображения Канбан доски diff --git a/public/navbar.js b/public/navbar.js index f0281c3..8f38d59 100644 --- a/public/navbar.js +++ b/public/navbar.js @@ -138,6 +138,16 @@ if (currentUser && currentUser.role === 'admin') { id: "admin-btn" }); } +// Отчеты + if (currentUser && (currentUser.role === 'admin' || currentUser.role === 'tasks')) { + navButtons.push({ + onclick: "showReportsSection()", + className: "nav-btn reports", + icon: "fas fa-chart-pie", + text: "Отчёты", + id: "reports-btn" + }); +} // Кнопка выхода navButtons.push({ onclick: "logout()", diff --git a/public/reports.js b/public/reports.js new file mode 100644 index 0000000..a0221e5 --- /dev/null +++ b/public/reports.js @@ -0,0 +1,206 @@ +// reports.js – Отчёт по задачам с фильтрацией и группировкой + +let reportData = []; // все назначения для отчёта +let currentReportFiltered = []; // отфильтрованные данные + +// Конфигурация статусов и типов для фильтров +const STATUS_OPTIONS = [ + { value: '', label: 'Все статусы' }, + { value: 'assigned', label: 'Назначена' }, + { value: 'in_progress', label: 'В работе' }, + { value: 'completed', 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() { + showSection('reports'); + // Если данные ещё не загружены – загружаем + if (reportData.length === 0) { + loadReportData(); + } else { + applyFilters(); // применяем текущие фильтры + } +} + +// Загрузка данных для отчёта (используем существующее API) +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 = []; + tasks.forEach(task => { + if (task.assignments && task.assignments.length) { + task.assignments.forEach(ass => { + reportData.push({ + 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 + }); + }); + } + }); + + // Заполняем фильтр пользователей + populateUserFilter(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 => { + usersMap.set(item.user_id, item.user_name); + }); + + let options = ''; + // Сортируем по имени + const sorted = Array.from(usersMap.entries()).sort((a, b) => a[1].localeCompare(b[1])); + for (let [id, name] of sorted) { + 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; + + currentReportFiltered = reportData.filter(item => { + if (userId && item.user_id != userId) return false; + if (statusFilter && item.status !== statusFilter) return false; + if (typeFilter && item.task_type !== typeFilter) return false; + return true; + }); + + renderReport(currentReportFiltered); +} + +// Рендеринг таблицы и сводки +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.status] = (statusCounts[item.status] || 0) + 1; + }); + + const statusLabels = { + 'assigned': 'Назначена', + 'in_progress': 'В работе', + 'completed': 'Выполнена', + '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)} + ${escapeHtml(truncateText(item.task_description, 50))} + ${formatDateTime(item.due_date) || '—'} + ${escapeHtml(item.user_name)} + ${escapeHtml(item.creator_name)} + ${statusLabels[item.status] || item.status} + ${formatDateTime(item.status_updated_at) || '—'} + + `).join(''); +} + +// Вспомогательные функции +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 formatDateTime(dateStr) { + if (!dateStr) return ''; + const d = new Date(dateStr); + return d.toLocaleString('ru-RU', { + day: '2-digit', month: '2-digit', year: 'numeric', + hour: '2-digit', minute: '2-digit' + }); +} + +// Печать отчёта +function printReport() { + window.print(); +} + +// Экспортируем функции +window.showReportsSection = showReportsSection; +window.loadReportData = loadReportData; +window.applyFilters = applyFilters; +window.printReport = printReport; \ No newline at end of file diff --git a/public/style.css b/public/style.css index ec30ccd..6586284 100644 --- a/public/style.css +++ b/public/style.css @@ -5069,4 +5069,105 @@ button.btn-primary { border-radius: 6px; padding: 10px; background: #fff; -} \ No newline at end of file +} +.nav-btn.reports { + background: linear-gradient(135deg, #9b59b6, #8e44ad); + box-shadow: 0 4px 15px rgba(155, 89, 182, 0.3); +} +.nav-btn.reports:hover { + box-shadow: 0 6px 20px rgba(155, 89, 182, 0.4); +} +.reports-filters { + display: flex; + gap: 20px; + align-items: flex-end; + margin-bottom: 20px; + flex-wrap: wrap; +} +.report-summary { + background: #f8f9fa; + border-radius: 8px; + padding: 15px; + margin-bottom: 20px; + display: flex; + gap: 15px; + flex-wrap: wrap; +} +.summary-item { + background: white; + padding: 10px 15px; + border-radius: 6px; + box-shadow: 0 2px 4px rgba(0,0,0,0.05); + min-width: 120px; + text-align: center; +} +.summary-item .status-badge { + display: block; + font-size: 20px; + font-weight: bold; +} +.summary-item .status-label { + color: #666; + font-size: 12px; +} +.report-table-container { + overflow-x: auto; +} +.report-table { + width: 100%; + border-collapse: collapse; + font-size: 14px; +} +.report-table th { + background: #f1f3f5; + padding: 12px; + text-align: left; + font-weight: 600; + color: #495057; +} +.report-table td { + padding: 10px 12px; + border-bottom: 1px solid #e9ecef; +} +.report-table tr:hover { + background: #f8f9fa; +} +.report-table .task-description { + max-width: 300px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +/* Печатные стили */ +@media print { + body * { + visibility: hidden; + } + #reports-section, #reports-section * { + visibility: visible; + } + #reports-section { + position: absolute; + left: 0; + top: 0; + width: 100%; + background: white; + padding: 20px; + } + .reports-filters, .summary-item, .btn-primary, .btn-secondary { + display: none !important; + } + .report-table th { + background: #f0f0f0 !important; + color: black !important; + } + .report-table td { + border: 1px solid #ccc; + } +} +.status-assigned { background: #ffc107; color: #212529; } +.status-in_progress { background: #17a2b8; color: white; } +.status-completed { background: #28a745; color: white; } +.status-overdue { background: #dc3545; color: white; } +.status-rework { background: #fd7e14; color: white; } +.status-deleted { background: #6c757d; color: white; } \ No newline at end of file