Files
minicrm/public/reports.js
2026-04-02 11:25:38 +05:00

297 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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: 'Эл. журнал' },
{ value: 'acquaintance', 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;
// Секретарь (по роли)
if (currentUser.role === 'secretary') return true;
// Проверка групп "Руководители" и "Секретарь"
if (currentUser.groups && Array.isArray(currentUser.groups)) {
if (currentUser.groups.some(g =>
g === 'Руководители' || g.includes('Руководители') ||
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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
function printReport() {
window.print();
}
// Экспорт
window.showReportsSection = showReportsSection;
window.loadReportData = loadReportData;
window.applyFilters = applyFilters;
window.resetReportFilters = resetReportFilters;
window.printReport = printReport;