287 lines
11 KiB
JavaScript
287 lines
11 KiB
JavaScript
// 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 = '<tr><td colspan="7" class="loading">Загрузка данных...</td></tr>';
|
||
|
||
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 =
|
||
'<tr><td colspan="7" class="error">Ошибка загрузки данных</td></tr>';
|
||
}
|
||
}
|
||
|
||
// Заполнение выпадающего списка пользователей
|
||
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 = '<option value="">Все пользователи</option>';
|
||
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 += `<option value="${id}">${escapeHtml(name)}</option>`;
|
||
}
|
||
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 = '<option value="">Все номера</option>';
|
||
taskIds.forEach(id => {
|
||
options += `<option value="${id}">${id}</option>`;
|
||
});
|
||
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 = '<tr><td colspan="7" class="no-data">Нет данных для отображения</td></tr>';
|
||
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 += `
|
||
<div class="summary-item">
|
||
<span class="status-badge">${count}</span>
|
||
<span class="status-label">${statusLabels[status] || status}</span>
|
||
</div>
|
||
`;
|
||
}
|
||
summaryDiv.innerHTML = summaryHtml;
|
||
|
||
tbody.innerHTML = data.map(item => `
|
||
<tr>
|
||
<td>${item.task_id}</td>
|
||
<td>${escapeHtml(item.task_title)}</td>
|
||
<td>${formatDateTimereports(item.due_date) || '—'}</td>
|
||
<td>${escapeHtml(item.user_name || 'Неизвестно')}</td>
|
||
<td>${escapeHtml(item.creator_name || 'Неизвестно')}</td>
|
||
<td><span class="status-badge status-${item.displayStatus}">${statusLabels[item.displayStatus] || item.displayStatus}</span></td>
|
||
<td>${formatDateTimereports(item.status_updated_at) || '—'}</td>
|
||
</tr>
|
||
`).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, '"')
|
||
.replace(/'/g, ''');
|
||
}
|
||
|
||
function printReport() {
|
||
window.print();
|
||
}
|
||
|
||
// Экспорт
|
||
window.showReportsSection = showReportsSection;
|
||
window.loadReportData = loadReportData;
|
||
window.applyFilters = applyFilters;
|
||
window.resetReportFilters = resetReportFilters;
|
||
window.printReport = printReport; |