// admin-stats.js
// Функции для работы с детальной статистикой
let usersStats = [];
let filteredStats = [];
let currentPage = 1;
const pageSize = 20;
let currentFilters = {
user: '',
status: '',
role: '',
authType: ''
};
/**
* Преобразует внутреннее имя роли в отображаемое.
* Для известных ролей возвращает локализованное название,
* для неизвестных – само имя роли.
*/
function formatRole(role) {
const roleMap = {
'admin': 'Администратор',
'teacher': 'Учитель'
// при необходимости можно добавить другие соответствия
};
return roleMap[role] || role;
}
function renderStatsSection() {
const statsContainer = document.getElementById('admin-stats-section');
if (!statsContainer) return;
statsContainer.innerHTML = `
Фильтры
Пользователь
Все пользователи
Статус назначений
Все статусы
Назначено
В работе
Выполнено
Просрочено
На доработке
Роль
Все роли
Тип авторизации
Все типы
Локальная
LDAP
Сбросить фильтры
Детальная статистика по пользователям
Всего пользователей: 0
Пользователь
Роль
Тип
Всего задач
Статусы назначений
Активные задачи
Закрытые задачи
Последняя активность
Загрузка статистики...
`;
// Загружаем данные
loadUsersStats();
loadOverallStats();
}
async function loadUsersStats() {
try {
const tbody = document.getElementById('users-stats-body');
if (tbody) {
tbody.innerHTML = 'Загрузка статистики... ';
}
// Загружаем пользователей из существующего 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();
populateRoleFilter(); // добавляем динамическое заполнение ролей
// Применяем фильтры
applyFilters();
} catch (error) {
console.error('Ошибка загрузки статистики по пользователям:', error);
// Если всё не удалось, используем мок-данные для демонстрации
usersStats = getMockStatsData();
populateUserFilter();
populateRoleFilter(); // и здесь тоже
applyFilters();
const tbody = document.getElementById('users-stats-body');
if (tbody && tbody.querySelector('.stats-loading')) {
const firstRow = tbody.querySelector('tr');
if (firstRow) {
firstRow.innerHTML = 'Загрузка данных... (используются демонстрационные данные) ';
}
}
}
}
/**
* Заполняет select фильтра по ролям уникальными значениями из загруженной статистики.
*/
function populateRoleFilter() {
const roleSelect = document.getElementById('filter-role');
if (!roleSelect) return;
// Сохраняем текущее выбранное значение
const selectedValue = roleSelect.value;
// Получаем уникальные роли из usersStats (отбрасываем пустые)
const roles = [...new Set(usersStats.map(stat => stat.role).filter(Boolean))];
// Очищаем select и добавляем опцию "Все роли"
roleSelect.innerHTML = 'Все роли ';
// Добавляем опции для каждой уникальной роли
roles.forEach(role => {
const option = document.createElement('option');
option.value = role;
option.textContent = formatRole(role); // отображаем локализованное название, если есть
roleSelect.appendChild(option);
});
// Восстанавливаем выбранное значение, если оно всё ещё актуально
if (selectedValue && roles.includes(selectedValue)) {
roleSelect.value = selectedValue;
} else {
roleSelect.value = '';
}
}
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 = 'Все пользователи ';
// Добавляем пользователей
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 = `
Пользователи
${stats.totalUsers || 0}
Админы:
${stats.adminUsers || 0}
Учителя:
${stats.teacherUsers || 0}
LDAP:
${stats.ldapUsers || 0}
Локальные:
${stats.localUsers || 0}
Задачи
${stats.totalTasks || 0}
Активные:
${stats.activeTasks || 0}
Закрытые:
${stats.closedTasks || 0}
Удаленные:
${stats.deletedTasks || 0}
Назначения
${stats.totalAssignments || 0}
Назначено:
${stats.assignedCount || 0}
В работе:
${stats.inProgressCount || 0}
Выполнено:
${stats.completedCount || 0}
Просрочено:
${stats.overdueCount || 0}
На доработке:
${stats.reworkCount || 0}
Файлы
${stats.totalFiles || 0}
${fileSizeMB} MB
`;
}
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 = 'Нет данных для отображения ';
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 => `
${stat.userName || 'Не указано'}
${formatRole(stat.role)}
${stat.authType === 'ldap' ? 'LDAP' : 'Локальная'}
${renderStatuses(stat.assignmentStatuses)}
${stat.activeTasks || 0}
${stat.closedTasks || 0}
${formatDateTime(stat.lastActivity) || 'Нет данных'}
`).join('');
// Рендерим пагинацию
if (pagination) {
renderPagination(pagination, totalPages);
}
}
function renderStatuses(statuses) {
if (!statuses || Object.keys(statuses).length === 0) {
return 'Нет данных
';
}
const statusOrder = ['assigned', 'in_progress', 'completed', 'overdue', 'rework'];
let html = '';
statusOrder.forEach(status => {
const count = statuses[status] || 0;
if (count > 0) {
html += `
${getStatusLabel(status)}
${count}
`;
}
});
// Если нет статусов с количеством > 0
if (!html) {
return 'Нет назначений
';
}
return html;
}
function renderPagination(container, totalPages) {
if (!container || totalPages <= 1) {
container.innerHTML = '';
return;
}
let paginationHTML = '';
// Кнопка "Назад"
paginationHTML += `
←
`;
// Номера страниц
for (let i = 1; i <= totalPages; i++) {
if (i === 1 || i === totalPages || (i >= currentPage - 2 && i <= currentPage + 2)) {
paginationHTML += `
${i}
`;
} else if (i === currentPage - 3 || i === currentPage + 3) {
paginationHTML += `... `;
}
}
// Кнопка "Вперед"
paginationHTML += `
→
`;
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 || ''}"`,
`"${formatRole(stat.role)}"`, // используем formatRole и здесь
`"${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;