refactor(admin): перенос блоков «Статистики» в «Обзор», удаление вкладки «Статистика»

Обзор теперь показывает и итоги за всё время (Пользователей, Тестов пройдено, Средний
результат) и средний результат по предметам за всё время — данные грузятся из
adminGetStats параллельно с обзором. Дублей нет: Обзор был про 24ч и контент.

Убрано полностью: nav-кнопка «Статистика», панель #tab-stats, маршрут stats в
ROUTE_TO_SECTION, подключение и файл sections/stats.js. #stats-хэш падает на #overview.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-06-03 19:10:42 +03:00
parent ecce4b013a
commit 71d94f45f1
5 changed files with 46 additions and 67 deletions
-12
View File
@@ -988,9 +988,6 @@
<button class="admin-nav-item active" data-tab="overview" onclick="switchTab(this)">
<i data-lucide="layout-dashboard" style="width:15px;height:15px"></i> Обзор
</button>
<button class="admin-nav-item" data-tab="stats" onclick="switchTab(this)">
<i data-lucide="bar-chart-2" style="width:15px;height:15px"></i> Статистика
</button>
<button class="admin-nav-item" data-tab="sessions" onclick="switchTab(this)">
<i data-lucide="clock" style="width:15px;height:15px"></i> История сессий
</button>
@@ -1096,14 +1093,6 @@
<div id="overview-content"><div class="spinner"></div></div>
</div>
<!-- ── Статистика ── -->
<div class="tab-pane" id="tab-stats">
<div class="section-title">Общая статистика</div>
<div class="stats-grid" id="stats-grid"><div class="spinner"></div></div>
<div class="section-title">По предметам</div>
<div class="subj-stats" id="subj-stats"><div class="spinner"></div></div>
</div>
<!-- ── Вопросы ── -->
<div class="tab-pane" id="tab-questions">
<div class="t-toolbar">
@@ -2125,7 +2114,6 @@
<script src="/js/admin/router.js"></script>
<script src="/js/admin/_shared.js"></script>
<script src="/js/admin/sections/overview.js"></script>
<script src="/js/admin/sections/stats.js"></script>
<script src="/js/admin/sections/sublog.js"></script>
<script src="/js/admin/sections/sims.js"></script>
<script src="/js/admin/sections/games.js"></script>
-1
View File
@@ -53,7 +53,6 @@
// Routes that map 1:1 to a section module (Phase 2-extracted).
const ROUTE_TO_SECTION = {
overview: 'overview',
stats: 'stats',
questions: 'questions',
tests: 'tests',
assignments: 'assignments',
+1 -1
View File
@@ -3,7 +3,7 @@
* Wraps the existing switchTab() flow without replacing it.
*
* Hash format: #<route>[/<param1>[/<param2>...]]
* #stats → { route: 'stats', params: [] }
* #overview → { route: 'overview', params: [] }
* #users → { route: 'users', params: [] }
* #users/123 → { route: 'users', params: ['123'] }
* #sessions/456/foo → { route: 'sessions', params: ['456','foo'] }
+45 -3
View File
@@ -250,7 +250,7 @@
}).join('');
}
function render(data) {
function render(data, stats) {
const el = document.getElementById('overview-content');
if (!el) return;
ensureOvStyles();
@@ -331,6 +331,42 @@
<div class="ov-section-title">По предметам (24ч)</div>
${renderSubjectBar(subjects24h)}`;
/* ── all-time totals (перенесено из бывшей вкладки «Статистика») ── */
const allTimeHtml = stats ? `
<div class="ov-section-title">Итоги за всё время</div>
<div class="ov-grid">
<div class="ov-card" style="--ov-top:var(--violet)">
<div class="ov-card-icon" style="background:rgba(155,93,229,0.1);color:var(--violet)"><i data-lucide="users" style="width:18px;height:18px"></i></div>
<div class="ov-card-val" style="color:var(--violet)">${fmtNum(stats.totalUsers)}</div>
<div class="ov-card-label">Пользователей</div>
</div>
<div class="ov-card" style="--ov-top:var(--cyan)">
<div class="ov-card-icon" style="background:rgba(6,214,224,0.1);color:var(--cyan)"><i data-lucide="file-text" style="width:18px;height:18px"></i></div>
<div class="ov-card-val" style="color:var(--cyan)">${fmtNum(stats.totalTests)}</div>
<div class="ov-card-label">Тестов пройдено</div>
</div>
<div class="ov-card" style="--ov-top:var(--green)">
<div class="ov-card-icon" style="background:rgba(6,214,100,0.1);color:var(--green)"><i data-lucide="target" style="width:18px;height:18px"></i></div>
<div class="ov-card-val" style="color:var(--green)">${stats.avgScore != null ? stats.avgScore + '%' : '—'}</div>
<div class="ov-card-label">Средний результат</div>
</div>
</div>` : '';
/* ── per-subject all-time performance (перенесено из «Статистики») ── */
const subjAllTimeHtml = (stats && Array.isArray(stats.bySubject) && stats.bySubject.length) ? `
<div class="ov-section-title">Результаты по предметам (всё время)</div>
<div class="subj-stats">
${stats.bySubject.map(function (b) {
const p = b.avg_pct == null ? 0 : b.avg_pct;
const barColor = p >= 75 ? 'var(--green)' : p >= 50 ? 'var(--amber)' : 'var(--pink)';
return '<div class="subj-stat">' +
'<div><div class="subj-stat-name">' + e(b.name) + '</div><div class="subj-stat-info">' + b.tests + ' тестов</div></div>' +
'<div><div class="subj-stat-pct">' + (b.avg_pct == null ? '—' : b.avg_pct) + '%</div>' +
'<div style="width:60px;height:3px;background:rgba(15,23,42,0.06);border-radius:99px;margin-top:5px;overflow:hidden"><div style="width:' + p + '%;height:100%;background:' + barColor + ';border-radius:99px"></div></div></div>' +
'</div>';
}).join('')}
</div>` : '';
/* ── results tables ────────────────────────────────────────── */
const topTableHtml = top.length
? `<table class="ov-top-table">
@@ -386,7 +422,9 @@
${alertsHtml}
${invHtml}
${allTimeHtml}
${subjHtml}
${subjAllTimeHtml}
<div class="ov-results-grid" style="margin-top: 28px">
<div>
@@ -429,9 +467,13 @@
if (!el) return;
renderSkeleton(el);
try {
const data = await LS.adminGetOverview();
// Обзор + перенесённые из бывшей вкладки «Статистика» итоги за всё время.
const [data, stats] = await Promise.all([
LS.adminGetOverview(),
LS.adminGetStats().catch(() => null),
]);
_lastLoadTs = Date.now();
render(data);
render(data, stats);
startTsInterval();
} catch (e) {
LS.state.error(el, e, () => load());
-50
View File
@@ -1,50 +0,0 @@
'use strict';
/* admin → stats section */
(function () {
'use strict';
let inited = false;
async function load() {
try {
const s = await LS.adminGetStats();
document.getElementById('stats-grid').innerHTML = `
<div class="stat-card" style="--stat-top:var(--violet)">
<div class="stat-card-icon" style="background:rgba(155,93,229,0.1)"><i data-lucide="users" class="stat-icon"></i></div>
<div class="stat-val violet">${s.totalUsers}</div>
<div class="stat-label">Пользователей</div>
</div>
<div class="stat-card" style="--stat-top:var(--cyan)">
<div class="stat-card-icon" style="background:rgba(6,214,224,0.1)"><i data-lucide="file-text" class="stat-icon"></i></div>
<div class="stat-val cyan">${s.totalTests}</div>
<div class="stat-label">Тестов пройдено</div>
</div>
<div class="stat-card" style="--stat-top:var(--green)">
<div class="stat-card-icon" style="background:rgba(6,214,100,0.1)"><i data-lucide="target" class="stat-icon"></i></div>
<div class="stat-val green">${s.avgScore ?? '—'}%</div>
<div class="stat-label">Средний результат</div>
</div>`;
if (window.lucide) lucide.createIcons();
const subjEl = document.getElementById('subj-stats');
if (!s.bySubject?.length) { subjEl.innerHTML = '<div class="empty">Нет данных</div>'; return; }
subjEl.innerHTML = s.bySubject.map(b => {
const pct = b.avg_pct ?? 0;
const barColor = pct >= 75 ? 'var(--green)' : pct >= 50 ? 'var(--amber)' : 'var(--pink)';
return `<div class="subj-stat">
<div><div class="subj-stat-name">${esc(b.name)}</div><div class="subj-stat-info">${b.tests} тестов</div></div>
<div>
<div class="subj-stat-pct">${b.avg_pct ?? '—'}%</div>
<div style="width:60px;height:3px;background:rgba(15,23,42,0.06);border-radius:99px;margin-top:5px;overflow:hidden"><div style="width:${pct}%;height:100%;background:${barColor};border-radius:99px"></div></div>
</div>
</div>`;
}).join('');
} catch (e) {
LS.state.error(document.getElementById('stats-grid'), e, load);
}
}
window.AdminSections = window.AdminSections || {};
window.AdminSections.stats = {
init: async () => { if (inited) return; inited = true; await load(); },
reload: load,
};
})();