822 lines
31 KiB
JavaScript
822 lines
31 KiB
JavaScript
// 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; |