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