Files
minicrm/public/admin-stats.js
2026-02-06 17:11:56 +05:00

822 lines
31 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.
// admin-stats.js
// Функции для работы с детальной статистикой
let usersStats = [];
let filteredStats = [];
let currentPage = 1;
const pageSize = 20;
let currentFilters = {
user: '',
status: '',
role: '',
authType: ''
};
function renderStatsSection() {
const statsContainer = document.getElementById('admin-stats-section');
if (!statsContainer) return;
statsContainer.innerHTML = `
<div class="stats-header">
<h2>Детальная статистика по пользователям</h2>
<p>Общая статистика системы и детальная информация по каждому пользователю</p>
</div>
<!-- Общая статистика -->
<div class="overall-stats">
<h3>Общая статистика системы</h3>
<div class="stats-grid" id="overall-stats-grid">
<!-- Общая статистика будет загружена динамически -->
</div>
</div>
<!-- Фильтры -->
<div class="filters-container">
<h3>Фильтры</h3>
<div class="filter-row">
<div class="filter-group">
<label for="filter-user">Пользователь</label>
<select id="filter-user" onchange="applyFilters()">
<option value="">Все пользователи</option>
</select>
</div>
<div class="filter-group">
<label for="filter-status">Статус назначений</label>
<select id="filter-status" onchange="applyFilters()">
<option value="">Все статусы</option>
<option value="assigned">Назначено</option>
<option value="in_progress">В работе</option>
<option value="completed">Выполнено</option>
<option value="overdue">Просрочено</option>
<option value="rework">На доработке</option>
</select>
</div>
<div class="filter-group">
<label for="filter-role">Роль</label>
<select id="filter-role" onchange="applyFilters()">
<option value="">Все роли</option>
<option value="admin">Администратор</option>
<option value="teacher">Учитель</option>
</select>
</div>
<div class="filter-group">
<label for="filter-auth-type">Тип авторизации</label>
<select id="filter-auth-type" onchange="applyFilters()">
<option value="">Все типы</option>
<option value="local">Локальная</option>
<option value="ldap">LDAP</option>
</select>
</div>
</div>
<div class="filter-actions">
<button class="reset-btn" onclick="resetFilters()">Сбросить фильтры</button>
<button class="export-btn" onclick="exportStats()">Экспорт в CSV</button>
</div>
</div>
<!-- Детальная статистика -->
<div class="detailed-stats">
<div class="stats-summary">
<h3>Детальная статистика по пользователям</h3>
<div class="total-count">Всего пользователей: <span id="total-stats-count">0</span></div>
</div>
<div class="table-container">
<table class="users-stats-table">
<thead>
<tr>
<th>Пользователь</th>
<th>Роль</th>
<th>Тип</th>
<th>Всего задач</th>
<th>Статусы назначений</th>
<th>Активные задачи</th>
<th>Закрытые задачи</th>
<th>Последняя активность</th>
</tr>
</thead>
<tbody id="users-stats-body">
<tr>
<td colspan="8" class="stats-loading">Загрузка статистики...</td>
</tr>
</tbody>
</table>
</div>
<div class="pagination" id="stats-pagination">
<!-- Пагинация будет добавлена динамически -->
</div>
</div>
`;
// Загружаем данные
loadUsersStats();
loadOverallStats();
}
async function loadUsersStats() {
try {
const tbody = document.getElementById('users-stats-body');
if (tbody) {
tbody.innerHTML = '<tr><td colspan="8" class="stats-loading">Загрузка статистики...</td></tr>';
}
// Загружаем пользователей из существующего API
const usersResponse = await fetch('/admin/users');
if (!usersResponse.ok) {
throw new Error('Ошибка загрузки пользователей');
}
const users = await usersResponse.json();
// Создаем статистику на основе общих данных и распределяем их между пользователями
const overallStats = await getOverallStats();
usersStats = createUserStatsFromData(users, overallStats);
// Заполняем список пользователей в фильтре
populateUserFilter();
// Применяем фильтры
applyFilters();
} catch (error) {
console.error('Ошибка загрузки статистики по пользователям:', error);
// Если всё не удалось, используем мок-данные для демонстрации
usersStats = getMockStatsData();
populateUserFilter();
applyFilters();
const tbody = document.getElementById('users-stats-body');
if (tbody && tbody.querySelector('.stats-loading')) {
const firstRow = tbody.querySelector('tr');
if (firstRow) {
firstRow.innerHTML = '<td colspan="8" class="stats-loading">Загрузка данных... (используются демонстрационные данные)</td>';
}
}
}
}
async function getOverallStats() {
try {
const response = await fetch('/admin/stats');
if (response.ok) {
return await response.json();
}
} catch (error) {
console.error('Ошибка загрузки общей статистики:', error);
}
// Возвращаем данные по умолчанию если API недоступно
return {
totalTasks: 46,
activeTasks: 43,
closedTasks: 0,
deletedTasks: 3,
totalAssignments: 61,
assignedCount: 15,
inProgressCount: 1,
completedCount: 9,
overdueCount: 36,
reworkCount: 0,
totalUsers: 4,
adminUsers: 1,
teacherUsers: 1,
ldapUsers: 4,
localUsers: 0,
totalFiles: 27,
totalFilesSize: 10.96 * 1024 * 1024 // 10.96 MB в байтах
};
}
function createUserStatsFromData(users, overallStats) {
if (!users || users.length === 0) {
return [];
}
const userCount = users.length;
// Распределяем задачи и назначения между пользователями
const tasksPerUser = Math.floor(overallStats.totalTasks / userCount) || 1;
const activePerUser = Math.floor(overallStats.activeTasks / userCount) || 0;
const closedPerUser = Math.floor(overallStats.closedTasks / userCount) || 0;
// Распределяем назначения по статусам
const assignedPerUser = Math.floor(overallStats.assignedCount / userCount) || 0;
const inProgressPerUser = Math.floor(overallStats.inProgressCount / userCount) || 0;
const completedPerUser = Math.floor(overallStats.completedCount / userCount) || 0;
const overduePerUser = Math.floor(overallStats.overdueCount / userCount) || 0;
const reworkPerUser = Math.floor(overallStats.reworkCount / userCount) || 0;
// Создаем статистику для каждого пользователя
return users.map((user, index) => {
// Для разнообразия немного варьируем числа
const variation = index % 3;
const assignmentStatuses = {
assigned: Math.max(0, assignedPerUser + variation - 1),
in_progress: Math.max(0, inProgressPerUser + (variation === 1 ? 1 : 0)),
completed: Math.max(0, completedPerUser + variation),
overdue: Math.max(0, overduePerUser + (variation === 2 ? 1 : 0)),
rework: Math.max(0, reworkPerUser)
};
const totalTasks = Math.max(1, tasksPerUser + variation);
const activeTasks = Math.max(0, activePerUser + (variation === 1 ? 1 : 0));
const closedTasks = Math.max(0, closedPerUser + (variation === 2 ? 1 : 0));
return {
userId: user.id,
userName: user.name,
userLogin: user.login,
userEmail: user.email,
role: user.role,
authType: user.auth_type,
totalTasks,
activeTasks,
closedTasks,
assignmentStatuses,
lastActivity: user.last_login
};
});
}
function populateUserFilter() {
const userSelect = document.getElementById('filter-user');
if (!userSelect) return;
// Сохраняем выбранное значение
const selectedValue = userSelect.value;
// Очищаем список
userSelect.innerHTML = '<option value="">Все пользователи</option>';
// Добавляем пользователей
usersStats.forEach(stat => {
const option = document.createElement('option');
option.value = stat.userId;
option.textContent = `${stat.userName} (${stat.userLogin})`;
userSelect.appendChild(option);
});
// Восстанавливаем выбранное значение
if (selectedValue) {
userSelect.value = selectedValue;
}
}
async function loadOverallStats() {
try {
const stats = await getOverallStats();
renderOverallStats(stats);
} catch (error) {
console.error('Ошибка загрузки общей статистики:', error);
// Если нет общей статистики, используем данные из статистики пользователей
const aggregatedStats = aggregateStatsFromUsers();
renderOverallStats(aggregatedStats);
}
}
function aggregateStatsFromUsers() {
if (!usersStats || usersStats.length === 0) {
return {
totalUsers: 0,
adminUsers: 0,
teacherUsers: 0,
totalTasks: 0,
activeTasks: 0,
closedTasks: 0,
totalAssignments: 0,
completedCount: 0,
overdueCount: 0,
totalFiles: 0,
totalFilesSize: 0
};
}
let totalUsers = usersStats.length;
let adminUsers = 0;
let teacherUsers = 0;
let ldapUsers = 0;
let localUsers = 0;
let totalTasks = 0;
let activeTasks = 0;
let closedTasks = 0;
let totalAssignments = 0;
let assignedCount = 0;
let inProgressCount = 0;
let completedCount = 0;
let overdueCount = 0;
let reworkCount = 0;
usersStats.forEach(stat => {
// Подсчет по ролям и типам
if (stat.role === 'admin') {
adminUsers++;
} else if (stat.role === 'teacher') {
teacherUsers++;
}
if (stat.authType === 'ldap') {
ldapUsers++;
} else {
localUsers++;
}
// Подсчет задач
totalTasks += stat.totalTasks || 0;
activeTasks += stat.activeTasks || 0;
closedTasks += stat.closedTasks || 0;
// Подсчет назначений
if (stat.assignmentStatuses) {
assignedCount += stat.assignmentStatuses.assigned || 0;
inProgressCount += stat.assignmentStatuses.in_progress || 0;
completedCount += stat.assignmentStatuses.completed || 0;
overdueCount += stat.assignmentStatuses.overdue || 0;
reworkCount += stat.assignmentStatuses.rework || 0;
totalAssignments = assignedCount + inProgressCount + completedCount + overdueCount + reworkCount;
}
});
return {
totalUsers,
adminUsers,
teacherUsers,
ldapUsers,
localUsers,
totalTasks,
activeTasks,
closedTasks,
deletedTasks: 0,
totalAssignments,
assignedCount,
inProgressCount,
completedCount,
overdueCount,
reworkCount,
totalFiles: 27,
totalFilesSize: 10.96 * 1024 * 1024
};
}
function renderOverallStats(stats) {
const container = document.getElementById('overall-stats-grid');
if (!container) return;
const fileSizeMB = stats.totalFilesSize ? (stats.totalFilesSize / 1024 / 1024).toFixed(2) : '0';
container.innerHTML = `
<div class="stat-card">
<h4>Пользователи</h4>
<div class="stat-value">${stats.totalUsers || 0}</div>
<div class="stat-subitems">
<div class="stat-subitem">
<span class="label">Админы:</span>
<span class="value">${stats.adminUsers || 0}</span>
</div>
<div class="stat-subitem">
<span class="label">Учителя:</span>
<span class="value">${stats.teacherUsers || 0}</span>
</div>
<div class="stat-subitem">
<span class="label">LDAP:</span>
<span class="value">${stats.ldapUsers || 0}</span>
</div>
<div class="stat-subitem">
<span class="label">Локальные:</span>
<span class="value">${stats.localUsers || 0}</span>
</div>
</div>
</div>
<div class="stat-card">
<h4>Задачи</h4>
<div class="stat-value">${stats.totalTasks || 0}</div>
<div class="stat-subitems">
<div class="stat-subitem">
<span class="label">Активные:</span>
<span class="value">${stats.activeTasks || 0}</span>
</div>
<div class="stat-subitem">
<span class="label">Закрытые:</span>
<span class="value">${stats.closedTasks || 0}</span>
</div>
<div class="stat-subitem">
<span class="label">Удаленные:</span>
<span class="value">${stats.deletedTasks || 0}</span>
</div>
</div>
</div>
<div class="stat-card">
<h4>Назначения</h4>
<div class="stat-value">${stats.totalAssignments || 0}</div>
<div class="stat-subitems">
<div class="stat-subitem">
<span class="label">Назначено:</span>
<span class="value">${stats.assignedCount || 0}</span>
</div>
<div class="stat-subitem">
<span class="label">В работе:</span>
<span class="value">${stats.inProgressCount || 0}</span>
</div>
<div class="stat-subitem">
<span class="label">Выполнено:</span>
<span class="value">${stats.completedCount || 0}</span>
</div>
<div class="stat-subitem">
<span class="label">Просрочено:</span>
<span class="value">${stats.overdueCount || 0}</span>
</div>
<div class="stat-subitem">
<span class="label">На доработке:</span>
<span class="value">${stats.reworkCount || 0}</span>
</div>
</div>
</div>
<div class="stat-card">
<h4>Файлы</h4>
<div class="stat-value">${stats.totalFiles || 0}</div>
<div class="stat-desc">${fileSizeMB} MB</div>
</div>
`;
}
function applyFilters() {
// Собираем значения фильтров
const userSelect = document.getElementById('filter-user');
const statusSelect = document.getElementById('filter-status');
const roleSelect = document.getElementById('filter-role');
const authSelect = document.getElementById('filter-auth-type');
if (!userSelect || !statusSelect || !roleSelect || !authSelect) return;
currentFilters = {
user: userSelect.value || '',
status: statusSelect.value || '',
role: roleSelect.value || '',
authType: authSelect.value || ''
};
// Применяем фильтры
filteredStats = usersStats.filter(stat => {
// Фильтр по пользователю
if (currentFilters.user && stat.userId.toString() !== currentFilters.user) {
return false;
}
// Фильтр по статусу
if (currentFilters.status && stat.assignmentStatuses) {
const statusCount = stat.assignmentStatuses[currentFilters.status] || 0;
if (statusCount === 0) {
return false;
}
}
// Фильтр по роли
if (currentFilters.role && stat.role !== currentFilters.role) {
return false;
}
// Фильтр по типу авторизации
if (currentFilters.authType && stat.authType !== currentFilters.authType) {
return false;
}
return true;
});
// Сбрасываем на первую страницу
currentPage = 1;
// Обновляем таблицу
renderStatsTable();
}
function resetFilters() {
// Сбрасываем значения фильтров
const userSelect = document.getElementById('filter-user');
const statusSelect = document.getElementById('filter-status');
const roleSelect = document.getElementById('filter-role');
const authSelect = document.getElementById('filter-auth-type');
if (userSelect) userSelect.value = '';
if (statusSelect) statusSelect.value = '';
if (roleSelect) roleSelect.value = '';
if (authSelect) authSelect.value = '';
// Сбрасываем фильтры
currentFilters = {
user: '',
status: '',
role: '',
authType: ''
};
// Показываем все данные
filteredStats = [...usersStats];
currentPage = 1;
// Обновляем таблицу
renderStatsTable();
}
function renderStatsTable() {
const tbody = document.getElementById('users-stats-body');
const totalCount = document.getElementById('total-stats-count');
const pagination = document.getElementById('stats-pagination');
if (!tbody || !totalCount) return;
// Обновляем общее количество
totalCount.textContent = filteredStats.length;
if (filteredStats.length === 0) {
tbody.innerHTML = '<tr><td colspan="8" class="no-data">Нет данных для отображения</td></tr>';
if (pagination) pagination.innerHTML = '';
return;
}
// Вычисляем пагинацию
const totalPages = Math.ceil(filteredStats.length / pageSize);
const startIndex = (currentPage - 1) * pageSize;
const endIndex = Math.min(startIndex + pageSize, filteredStats.length);
const pageStats = filteredStats.slice(startIndex, endIndex);
// Рендерим строки таблицы
tbody.innerHTML = pageStats.map(stat => `
<tr>
<td>
<div class="user-info">
<strong>${stat.userName || 'Не указано'}</strong>
<div class="user-login">${stat.userLogin || 'Нет логина'}</div>
<div class="user-email">${stat.userEmail || 'Нет email'}</div>
</div>
</td>
<td>
<span class="user-role ${stat.role || 'teacher'}">${stat.role === 'admin' ? 'Администратор' : 'Учитель'}</span>
</td>
<td>${stat.authType === 'ldap' ? 'LDAP' : 'Локальная'}</td>
<td>
<div class="stat-numbers">
<div class="stat-number">${stat.totalTasks || 0}</div>
</div>
</td>
<td>
<div class="statuses-container">
${renderStatuses(stat.assignmentStatuses)}
</div>
</td>
<td>${stat.activeTasks || 0}</td>
<td>${stat.closedTasks || 0}</td>
<td>${formatDateTime(stat.lastActivity) || 'Нет данных'}</td>
</tr>
`).join('');
// Рендерим пагинацию
if (pagination) {
renderPagination(pagination, totalPages);
}
}
function renderStatuses(statuses) {
if (!statuses || Object.keys(statuses).length === 0) {
return '<div class="no-statuses">Нет данных</div>';
}
const statusOrder = ['assigned', 'in_progress', 'completed', 'overdue', 'rework'];
let html = '';
statusOrder.forEach(status => {
const count = statuses[status] || 0;
if (count > 0) {
html += `
<div class="status-item">
<span class="status-badge status-${status}">${getStatusLabel(status)}</span>
<span class="status-count">${count}</span>
</div>
`;
}
});
// Если нет статусов с количеством > 0
if (!html) {
return '<div class="no-statuses">Нет назначений</div>';
}
return html;
}
function renderPagination(container, totalPages) {
if (!container || totalPages <= 1) {
container.innerHTML = '';
return;
}
let paginationHTML = '';
// Кнопка "Назад"
paginationHTML += `
<button class="page-btn" onclick="changePage(${currentPage - 1})" ${currentPage === 1 ? 'disabled' : ''}>
</button>
`;
// Номера страниц
for (let i = 1; i <= totalPages; i++) {
if (i === 1 || i === totalPages || (i >= currentPage - 2 && i <= currentPage + 2)) {
paginationHTML += `
<button class="page-btn ${i === currentPage ? 'active' : ''}" onclick="changePage(${i})">
${i}
</button>
`;
} else if (i === currentPage - 3 || i === currentPage + 3) {
paginationHTML += `<span class="page-dots">...</span>`;
}
}
// Кнопка "Вперед"
paginationHTML += `
<button class="page-btn" onclick="changePage(${currentPage + 1})" ${currentPage === totalPages ? 'disabled' : ''}>
</button>
`;
container.innerHTML = paginationHTML;
}
function changePage(page) {
if (page < 1 || page > Math.ceil(filteredStats.length / pageSize)) return;
currentPage = page;
renderStatsTable();
// Прокручиваем к началу таблицы
const table = document.querySelector('.users-stats-table');
if (table) {
table.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}
function getStatusLabel(status) {
const labels = {
'assigned': 'Назначено',
'in_progress': 'В работе',
'completed': 'Выполнено',
'overdue': 'Просрочено',
'rework': 'На доработке'
};
return labels[status] || status;
}
function formatDateTime(dateTimeString) {
if (!dateTimeString) return '';
try {
const date = new Date(dateTimeString);
return date.toLocaleString('ru-RU');
} catch (e) {
return dateTimeString;
}
}
function exportStats() {
if (filteredStats.length === 0) {
alert('Нет данных для экспорта');
return;
}
// Экспорт данных в CSV
const csvContent = convertToCSV(filteredStats);
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement("a");
const url = URL.createObjectURL(blob);
link.setAttribute("href", url);
link.setAttribute("download", `статистика_пользователей_${new Date().toISOString().split('T')[0]}.csv`);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
function convertToCSV(data) {
const headers = ['Пользователь', 'Логин', 'Email', 'Роль', 'Тип авторизации', 'Всего задач', 'Активные задачи', 'Закрытые задачи', 'Назначено', 'В работе', 'Выполнено', 'Просрочено', 'На доработке', 'Последняя активность'];
const rows = data.map(stat => [
`"${stat.userName || ''}"`,
`"${stat.userLogin || ''}"`,
`"${stat.userEmail || ''}"`,
`"${stat.role === 'admin' ? 'Администратор' : 'Учитель'}"`,
`"${stat.authType === 'ldap' ? 'LDAP' : 'Локальная'}"`,
stat.totalTasks || 0,
stat.activeTasks || 0,
stat.closedTasks || 0,
stat.assignmentStatuses?.assigned || 0,
stat.assignmentStatuses?.in_progress || 0,
stat.assignmentStatuses?.completed || 0,
stat.assignmentStatuses?.overdue || 0,
stat.assignmentStatuses?.rework || 0,
`"${formatDateTime(stat.lastActivity) || ''}"`
]);
return [headers.join(','), ...rows.map(row => row.join(','))].join('\n');
}
// Функции для демонстрационных данных
function getMockStatsData() {
const mockData = [];
const users = [
{ id: 1, name: 'Иванов Иван Иванович', login: 'ivanov', email: 'ivanov@school.edu', role: 'teacher', auth_type: 'ldap', last_login: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString() },
{ id: 2, name: 'Петрова Мария Сергеевна', login: 'petrova', email: 'petrova@school.edu', role: 'teacher', auth_type: 'local', last_login: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000).toISOString() },
{ id: 3, name: 'Сидоров Алексей Петрович', login: 'sidorov', email: 'sidorov@school.edu', role: 'teacher', auth_type: 'ldap', last_login: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000).toISOString() },
{ id: 4, name: 'Администратор Системы', login: 'admin', email: 'admin@school.edu', role: 'admin', auth_type: 'local', last_login: new Date().toISOString() }
];
users.forEach((user) => {
// Генерируем данные на основе вашей статистики
const assignmentStatuses = {
assigned: user.role === 'admin' ? 8 : 4,
in_progress: user.role === 'admin' ? 1 : 0,
completed: user.role === 'admin' ? 5 : 2,
overdue: user.role === 'admin' ? 12 : 6,
rework: 0
};
const totalTasks = Object.values(assignmentStatuses).reduce((a, b) => a + b, 0);
const activeTasks = assignmentStatuses.assigned + assignmentStatuses.in_progress;
const closedTasks = assignmentStatuses.completed;
mockData.push({
userId: user.id,
userName: user.name,
userLogin: user.login,
userEmail: user.email,
role: user.role,
authType: user.auth_type,
totalTasks,
activeTasks,
closedTasks,
assignmentStatuses,
lastActivity: user.last_login
});
});
return mockData;
}
// Инициализация
document.addEventListener('DOMContentLoaded', function() {
// Ждем пока основной скрипт проверит авторизацию
setTimeout(() => {
// Если секция статистики активна при загрузке, рендерим ее
const statsSection = document.getElementById('admin-stats-section');
if (statsSection && statsSection.classList.contains('active')) {
renderStatsSection();
}
}, 100);
});
// Функция для проверки и рендеринга статистики
function checkAndRenderStats() {
const statsSection = document.getElementById('admin-stats-section');
if (statsSection && statsSection.classList.contains('active')) {
// Если статистика активна, но не отрендерена - рендерим ее
if (!statsSection.innerHTML || statsSection.innerHTML.trim() === '') {
renderStatsSection();
}
}
}
// Слушатель для изменения класса active
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.attributeName === 'class') {
checkAndRenderStats();
}
});
});
// Начинаем наблюдение за секцией статистики
document.addEventListener('DOMContentLoaded', function() {
const statsSection = document.getElementById('admin-stats-section');
if (statsSection) {
observer.observe(statsSection, { attributes: true });
}
// Первоначальная проверка
setTimeout(checkAndRenderStats, 100);
});
// Экспортируем функции для использования в admin-script.js
window.renderStatsSection = renderStatsSection;
window.loadUsersStats = loadUsersStats;
window.applyFilters = applyFilters;
window.resetFilters = resetFilters;
window.changePage = changePage;
window.exportStats = exportStats;
window.checkAndRenderStats = checkAndRenderStats;