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:
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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'] }
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user