-
-
@@ -358,6 +359,12 @@
+
+
+
+
+
+
@@ -373,12 +380,15 @@
-
-
@@ -386,14 +396,13 @@
-
+
| № задачи |
Название |
- Описание |
Срок выполнения |
Исполнитель |
Автор |
@@ -402,7 +411,7 @@
- | Загрузка данных... |
+ | Загрузка данных... |
@@ -410,7 +419,7 @@
-
+
×
diff --git a/public/reports.js b/public/reports.js
index 85c532b..f2c49b0 100644
--- a/public/reports.js
+++ b/public/reports.js
@@ -7,9 +7,12 @@ 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: 'overdue', label: 'Просрочена' },
+ { value: 'completed_after_due', label: 'Выполнена после срока' },
+ { value: 'overdue', label: 'Просрочена (системная)' },
{ value: 'rework', label: 'На доработке' },
{ value: 'deleted', label: 'Удалена' }
];
@@ -29,33 +32,75 @@ const TASK_TYPE_OPTIONS = [
// Функция показа секции отчёта
function showReportsSection() {
+ // Используем глобальный currentUser из auth.js
+ if (typeof currentUser === 'undefined' || !currentUser) {
+ console.error('currentUser не определён');
+ return;
+ }
showSection('reports');
- // Если данные ещё не загружены – загружаем
if (reportData.length === 0) {
loadReportData();
} else {
- applyFilters(); // применяем текущие фильтры
+ applyFilters();
}
}
-// Загрузка данных для отчёта (используем существующее API)
+// Проверка, имеет ли пользователь право видеть все задачи
+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 = '
| Загрузка данных... |
';
+ 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 => {
- reportData.push({
+ const item = {
task_id: task.id,
task_title: task.title,
task_description: task.description || '',
@@ -65,22 +110,23 @@ async function loadReportData() {
user_name: ass.user_name,
status: ass.status,
status_updated_at: ass.updated_at || task.updated_at,
- creator_name: task.creator_name
- });
+ 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 =
- '| Ошибка загрузки данных |
';
+ '| Ошибка загрузки данных |
';
}
}
@@ -89,13 +135,11 @@ function populateUserFilter(data) {
const select = document.getElementById('report-user-filter');
const usersMap = new Map();
data.forEach(item => {
- // Убедимся, что имя не null
const name = item.user_name || 'Без имени';
usersMap.set(item.user_id, name);
});
let options = '';
- // Сортируем по имени, защищая от null/undefined
const sorted = Array.from(usersMap.entries()).sort((a, b) => {
const nameA = a[1] || '';
const nameB = b[1] || '';
@@ -107,44 +151,72 @@ function populateUserFilter(data) {
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 (statusFilter && item.status !== statusFilter) 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 = '| Нет данных для отображения |
';
+ tbody.innerHTML = '| Нет данных для отображения |
';
summaryDiv.innerHTML = '';
return;
}
- // Сводка по статусам
const statusCounts = {};
data.forEach(item => {
- statusCounts[item.status] = (statusCounts[item.status] || 0) + 1;
+ statusCounts[item.displayStatus] = (statusCounts[item.displayStatus] || 0) + 1;
});
const statusLabels = {
'assigned': 'Назначена',
+ 'assigned_overdue': 'Назначена (просрочена)',
'in_progress': 'В работе',
+ 'in_progress_overdue': 'В работе (просрочена)',
'completed': 'Выполнена',
- 'overdue': 'Просрочена',
+ 'completed_after_due': 'Выполнена после срока',
+ 'overdue': 'Просрочена (системная)',
'rework': 'На доработке',
'deleted': 'Удалена'
};
@@ -160,16 +232,14 @@ function renderReport(data) {
}
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} |
+ ${statusLabels[item.displayStatus] || item.displayStatus} |
${formatDateTime(item.status_updated_at) || '—'} |
`).join('');
@@ -200,13 +270,13 @@ function formatDateTime(dateStr) {
});
}
-// Печать отчёта
function printReport() {
window.print();
}
-// Экспортируем функции
+// Экспорт
window.showReportsSection = showReportsSection;
window.loadReportData = loadReportData;
window.applyFilters = applyFilters;
+window.resetReportFilters = resetReportFilters;
window.printReport = printReport;
\ No newline at end of file
diff --git a/public/style.css b/public/style.css
index 6586284..98b7480 100644
--- a/public/style.css
+++ b/public/style.css
@@ -5170,4 +5170,35 @@ button.btn-primary {
.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
+.status-deleted { background: #6c757d; color: white; }
+.reports-filters {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 15px;
+ align-items: flex-end;
+ margin-bottom: 20px;
+}
+.filter-group {
+ min-width: 150px;
+}
+.filter-group.buttons-group {
+ display: flex;
+ gap: 8px;
+ align-items: center;
+ flex-wrap: wrap;
+}
+.btn-reset {
+ background: #6c757d;
+ color: white;
+ border: none;
+ padding: 8px 16px;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 14px;
+ display: inline-flex;
+ align-items: center;
+ gap: 5px;
+}
+.btn-reset:hover {
+ background: #5a6268;
+}
\ No newline at end of file