feat(dashboard): командный центр администратора на /dashboard
Админ при входе на /dashboard видит редизайн-обзор (порт макета admin-dashboard-redesign.html) на реальных данных /api/admin/overview: KPI-пульс со спарклайнами, инбокс «Требует внимания» с табами (блокировки/зависшие/брошенные), лента топ-сессий, распределение по предметам, здоровье контента, топ/худшие результаты, быстрые действия. Стили заскоуплены под #admin-command-center. Учитель/ученик без изменений. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+94
-17
@@ -67,20 +67,78 @@
|
||||
text-decoration: none;
|
||||
}
|
||||
.ab-btn:hover { background: rgba(255,255,255,0.25); }
|
||||
.action-cards { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
||||
.ac-card {
|
||||
display: flex; align-items: center; gap: 12px;
|
||||
padding: 14px 16px; border-radius: 14px;
|
||||
background: #fff; border: 1.5px solid rgba(15,23,42,0.07);
|
||||
text-decoration: none; color: inherit; transition: all 0.15s;
|
||||
box-shadow: 0 2px 8px rgba(15,23,42,0.04);
|
||||
/* ── Hero cards row (Reading · Lab of day · Pet) ── */
|
||||
.hero-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; }
|
||||
.hero-card {
|
||||
position: relative; border-radius: 18px; padding: 18px 20px;
|
||||
display: flex; flex-direction: column; min-height: 196px;
|
||||
text-decoration: none; color: inherit; overflow: hidden;
|
||||
transition: transform 0.16s, box-shadow 0.16s;
|
||||
}
|
||||
.ac-card:hover { border-color: rgba(155,93,229,0.25); transform: translateY(-2px); box-shadow: 0 6px 20px rgba(15,23,42,0.08); }
|
||||
.ac-emoji { font-size: 1.4rem; flex-shrink: 0; }
|
||||
.ac-body { flex: 1; min-width: 0; }
|
||||
.ac-title { font-size: 0.84rem; font-weight: 700; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.ac-sub { font-size: 0.72rem; color: var(--text-3); margin-top: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.ac-badge { font-family: 'Unbounded', sans-serif; font-size: 0.82rem; font-weight: 800; color: var(--violet); flex-shrink: 0; }
|
||||
.hero-card:hover { transform: translateY(-3px); box-shadow: 0 14px 34px rgba(15,23,42,0.16); }
|
||||
.hc-tag {
|
||||
display: inline-flex; align-items: center; gap: 7px;
|
||||
font-size: 0.66rem; font-weight: 800; letter-spacing: .07em; text-transform: uppercase;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.hc-tag svg { width: 15px; height: 15px; }
|
||||
.hc-h { font-family: 'Unbounded', sans-serif; font-size: 1.15rem; font-weight: 800; line-height: 1.15; }
|
||||
.hc-p { font-size: 0.76rem; line-height: 1.45; margin-top: 7px; }
|
||||
.hc-foot { margin-top: auto; display: flex; align-items: center; justify-content: space-between; gap: 10px; padding-top: 14px; }
|
||||
.hc-meta { font-size: 0.7rem; font-weight: 600; }
|
||||
.hc-btn {
|
||||
display: inline-flex; align-items: center; gap: 6px;
|
||||
padding: 7px 15px; border-radius: 99px; flex-shrink: 0;
|
||||
font-family: 'Manrope', sans-serif; font-size: 0.78rem; font-weight: 700;
|
||||
border: none; cursor: pointer; transition: filter 0.15s, transform 0.12s;
|
||||
}
|
||||
.hc-btn svg { width: 14px; height: 14px; }
|
||||
.hc-btn:hover { filter: brightness(1.08); }
|
||||
.hc-btn:active { transform: translateY(1px); }
|
||||
.hc-progress { height: 6px; border-radius: 99px; margin-top: 12px; overflow: hidden; }
|
||||
.hc-progress > i { display: block; height: 100%; border-radius: 99px; }
|
||||
|
||||
/* Card 1 — Reading (warm gradient) */
|
||||
.hc-read { background: linear-gradient(135deg, #d9742a 0%, #b3531a 100%); color: #fff; }
|
||||
.hc-read .hc-tag { color: rgba(255,255,255,.82); }
|
||||
.hc-read .hc-p { color: rgba(255,255,255,.78); }
|
||||
.hc-read .hc-meta { color: rgba(255,255,255,.7); }
|
||||
.hc-read .hc-progress { background: rgba(255,255,255,.2); }
|
||||
.hc-read .hc-progress > i { background: rgba(255,255,255,.92); }
|
||||
.hc-read .hc-pct { font-family: 'Unbounded', sans-serif; font-weight: 800; font-size: 0.82rem; color: #fff; }
|
||||
.hc-read .hc-btn { background: #fff; color: #b3531a; }
|
||||
|
||||
/* Card 2 — Lab of day (dark) */
|
||||
.hc-lab { background: linear-gradient(150deg, #16131f 0%, #1d1830 100%); color: #fff; border: 1px solid rgba(155,93,229,.18); }
|
||||
.hc-lab .hc-bg { position: absolute; inset: 0; opacity: .5; z-index: 0; }
|
||||
.hc-lab .hc-bg svg { width: 100%; height: 100%; }
|
||||
.hc-lab > *:not(.hc-bg) { position: relative; z-index: 1; }
|
||||
.hc-lab .hc-tag { color: #06D6E0; }
|
||||
.hc-lab .hc-tag svg { stroke: #06D6E0; }
|
||||
.hc-lab .hc-p { color: rgba(255,255,255,.74); }
|
||||
.hc-lab .hc-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
|
||||
.hc-lab .hc-chip { font-size: 0.66rem; font-weight: 700; padding: 3px 9px; border-radius: 99px; background: rgba(255,255,255,.08); color: rgba(255,255,255,.82); }
|
||||
.hc-lab .hc-chip.subj { background: rgba(6,214,224,.16); color: #06D6E0; }
|
||||
.hc-lab .hc-meta { color: rgba(255,255,255,.6); }
|
||||
.hc-lab .hc-btn { background: rgba(255,255,255,.12); color: #fff; backdrop-filter: blur(6px); }
|
||||
|
||||
/* Card 3 — Pet (light, accent top) */
|
||||
.hc-pet { background: var(--surface, #fff); border: 1.5px solid rgba(15,23,42,.07); border-top: 3px solid #F9C74F; }
|
||||
.hc-pet .hc-tag { color: #b3531a; }
|
||||
.hc-pet .hc-tag svg { stroke: #F9C74F; }
|
||||
.hc-pet .hc-pet-top { display: flex; align-items: center; gap: 10px; }
|
||||
.hc-pet .hc-pet-name { font-family: 'Unbounded', sans-serif; font-size: 1.15rem; font-weight: 800; }
|
||||
.hc-pet .hc-pet-art { width: 50px; height: 50px; margin-left: auto; flex-shrink: 0; }
|
||||
.hc-pet .hc-pet-art svg { width: 100%; height: 100%; }
|
||||
.hc-pet .hc-xp-row { display: flex; align-items: baseline; justify-content: space-between; margin-top: 12px; font-size: 0.7rem; color: var(--text-3); }
|
||||
.hc-pet .hc-xp-row b { color: var(--text); font-weight: 800; }
|
||||
.hc-pet .hc-progress { background: rgba(15,23,42,.07); }
|
||||
.hc-pet .hc-progress > i { background: linear-gradient(90deg, #F9C74F, #F98231); }
|
||||
.hc-pet .hc-pet-chips { display: grid; grid-template-columns: repeat(3, 1fr); gap: 7px; margin-top: 12px; }
|
||||
.hc-pet .hc-pchip { text-align: center; padding: 6px 2px; border-radius: 10px; background: rgba(15,23,42,.04); }
|
||||
.hc-pet .hc-pchip b { display: block; font-family: 'Unbounded', sans-serif; font-size: 0.86rem; font-weight: 800; color: var(--text); }
|
||||
.hc-pet .hc-pchip span { display: block; font-size: 0.58rem; font-weight: 700; letter-spacing: .04em; text-transform: uppercase; color: var(--text-3); margin-top: 2px; }
|
||||
.hc-pet .hc-btn { background: rgba(249,199,79,.16); color: #b3531a; align-self: flex-start; }
|
||||
|
||||
/* ── ZONE 3: Three-Column Grid ── */
|
||||
.main-grid {
|
||||
@@ -1176,6 +1234,7 @@
|
||||
/* Grids <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> single column */
|
||||
.main-grid { grid-template-columns: 1fr; }
|
||||
.action-cards { grid-template-columns: 1fr; }
|
||||
.hero-row { grid-template-columns: 1fr; }
|
||||
.admin-grid { grid-template-columns: 1fr; gap: 14px; }
|
||||
.adm-actions { grid-template-columns: 1fr; gap: 10px; }
|
||||
.adm-act-group { grid-template-columns: 1fr 1fr; }
|
||||
@@ -1461,6 +1520,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ADMIN COMMAND CENTER: full redesign overview (admin only) -->
|
||||
<div id="admin-command-center" style="display:none"></div>
|
||||
|
||||
<!-- ZONE 3: Three-Column Grid -->
|
||||
<div class="main-grid">
|
||||
<!-- Col 1: Assignments -->
|
||||
@@ -1658,7 +1720,10 @@
|
||||
<script src="/js/sound.js"></script>
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script src="/js/notifications.js"></script>
|
||||
<script src="/js/pet-sprite.js"></script>
|
||||
<script src="/js/lab-previews.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
||||
<script src="/js/dashboard-admin-center.js"></script>
|
||||
<script>
|
||||
const { user, isTeacher, isAdmin } = LS.initPage();
|
||||
if (!user) throw new Error('Not logged in');
|
||||
@@ -1676,8 +1741,16 @@
|
||||
document.getElementById('dh-sub').textContent = user?.role === 'admin' ? 'Панель администратора' : 'Панель учителя';
|
||||
// teacher/admin: hide student-only widgets, show admin compact layout
|
||||
document.querySelectorAll('.action-zone,.main-grid,#w-theory-progress,.full-row').forEach(el => { if (el) el.style.display = 'none'; });
|
||||
document.getElementById('admin-actions-zone').style.display = '';
|
||||
document.getElementById('admin-grid').style.display = '';
|
||||
if (isAdmin) {
|
||||
// admin: full command center (redesign) instead of compact layout
|
||||
const dh = document.querySelector('.dash-header'); if (dh) dh.style.display = 'none';
|
||||
document.getElementById('admin-actions-zone').style.display = 'none';
|
||||
document.getElementById('admin-grid').style.display = 'none';
|
||||
document.getElementById('admin-command-center').style.display = '';
|
||||
} else {
|
||||
document.getElementById('admin-actions-zone').style.display = '';
|
||||
document.getElementById('admin-grid').style.display = '';
|
||||
}
|
||||
} else {
|
||||
// Приветствие по времени суток
|
||||
const h = new Date().getHours();
|
||||
@@ -4098,8 +4171,12 @@
|
||||
}).join('');
|
||||
}
|
||||
|
||||
if (isTeacher) {
|
||||
// Admin/Teacher: compact admin layout
|
||||
if (isAdmin) {
|
||||
// Admin: command center (redesign) on real /api/admin/overview data
|
||||
const ccEl = document.getElementById('admin-command-center');
|
||||
if (ccEl && window.DashAdminCenter) DashAdminCenter.mount(ccEl);
|
||||
} else if (isTeacher) {
|
||||
// Teacher: compact admin layout
|
||||
loadAdminStats();
|
||||
loadTeacherKPIs();
|
||||
loadAdminAssignments();
|
||||
|
||||
@@ -0,0 +1,753 @@
|
||||
'use strict';
|
||||
/* ════════════════════════════════════════════════════════════════
|
||||
DASHBOARD · ADMIN COMMAND CENTER
|
||||
Рендерит «командный центр администратора» в /dashboard для роли admin.
|
||||
Дизайн — порт макета frontend/admin-dashboard-redesign.html
|
||||
(cobalt accent · Hanken Grotesk · JetBrains Mono · hairline borders),
|
||||
но на реальных данных GET /api/admin/overview.
|
||||
Весь CSS заскоуплен под #admin-command-center, чтобы не конфликтовать
|
||||
с ls.css / dashboard.html. Деструктивные действия не выполняются
|
||||
inline — кнопки ведут в соответствующие разделы /admin.
|
||||
Точка входа: window.DashAdminCenter.mount(rootEl).
|
||||
════════════════════════════════════════════════════════════════ */
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
let _root = null;
|
||||
let _data = null;
|
||||
let _tab = 'all';
|
||||
let _clockTimer = null;
|
||||
|
||||
/* ── subject hue cycle (совпадает с overview.js) ──────────────── */
|
||||
const SUBJ_COLORS = [
|
||||
'#3558e0', '#0ea5b7', '#7c3aed', '#d97706', '#16a34a',
|
||||
'#e11d48', '#4FC3F7', '#FFD54F', '#FF8A65', '#BA68C8',
|
||||
];
|
||||
|
||||
/* ── one-time font + CSS injection ────────────────────────────── */
|
||||
function ensureAssets() {
|
||||
if (!document.getElementById('acc-font')) {
|
||||
const l = document.createElement('link');
|
||||
l.id = 'acc-font';
|
||||
l.rel = 'stylesheet';
|
||||
l.href = 'https://fonts.googleapis.com/css2?family=Hanken+Grotesk:ital,wght@0,300..800;1,400..600&family=JetBrains+Mono:wght@400;500;600;700&display=swap';
|
||||
document.head.appendChild(l);
|
||||
}
|
||||
if (document.getElementById('acc-style')) return;
|
||||
const s = document.createElement('style');
|
||||
s.id = 'acc-style';
|
||||
s.textContent = CSS;
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
|
||||
/* ── helpers ──────────────────────────────────────────────────── */
|
||||
const e = (str) => (window.LS && LS.esc ? LS.esc(str) : String(str == null ? '' : str));
|
||||
|
||||
function initials(name) {
|
||||
if (!name) return '?';
|
||||
return name.trim().split(/\s+/).slice(0, 2)
|
||||
.map((w) => (w[0] ? w[0].toUpperCase() : '')).join('') || '?';
|
||||
}
|
||||
function hashHue(str) {
|
||||
let h = 0;
|
||||
for (let i = 0; i < (str || '').length; i++) h = ((h << 5) - h + str.charCodeAt(i)) | 0;
|
||||
return Math.abs(h) % 360;
|
||||
}
|
||||
function pctClass(p) {
|
||||
if (p == null) return 'mid';
|
||||
return p >= 75 ? 'hi' : p >= 50 ? 'mid' : 'lo';
|
||||
}
|
||||
function fmtNum(n) {
|
||||
if (n == null) return '0';
|
||||
return String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ' ');
|
||||
}
|
||||
function parseTs(s) {
|
||||
if (!s) return null;
|
||||
try { return new Date(s.replace(' ', 'T') + (s.endsWith('Z') ? '' : 'Z')); }
|
||||
catch (_) { return null; }
|
||||
}
|
||||
function fmtAgo(s) {
|
||||
const d = parseTs(s);
|
||||
if (!d) return '';
|
||||
const sec = Math.floor((Date.now() - d.getTime()) / 1000);
|
||||
if (sec < 60) return 'только что';
|
||||
const min = Math.floor(sec / 60);
|
||||
if (min < 60) return min + ' мин';
|
||||
const hr = Math.floor(min / 60);
|
||||
if (hr < 24) return hr + ' ч';
|
||||
return Math.floor(hr / 24) + ' дн';
|
||||
}
|
||||
function fmtSince(s) {
|
||||
const d = parseTs(s);
|
||||
if (!d) return '—';
|
||||
let min = Math.floor((Date.now() - d.getTime()) / 60000);
|
||||
if (min < 0) min = 0;
|
||||
const hr = Math.floor(min / 60);
|
||||
return hr > 0 ? hr + 'ч ' + (min % 60) + 'м' : min + 'м';
|
||||
}
|
||||
function fmtBannedDate(s) {
|
||||
const d = parseTs(s);
|
||||
if (!d) return '';
|
||||
return d.toLocaleDateString('ru', { day: 'numeric', month: 'short' });
|
||||
}
|
||||
function go(hash) { window.location.href = hash; }
|
||||
|
||||
/* ── sparkline path (array of {d, n} → 7-day path) ────────────── */
|
||||
function sparkPath(raw, w, h) {
|
||||
const map = {};
|
||||
(raw || []).forEach((r) => { map[r.d] = r.n; });
|
||||
const pts = [];
|
||||
for (let i = 6; i >= 0; i--) {
|
||||
const dt = new Date();
|
||||
dt.setDate(dt.getDate() - i);
|
||||
pts.push(map[dt.toISOString().slice(0, 10)] || 0);
|
||||
}
|
||||
const max = Math.max.apply(null, pts) || 1;
|
||||
const pad = 2;
|
||||
const xs = pts.map((_, i) => pad + (i / 6) * (w - 2 * pad));
|
||||
const ys = pts.map((v) => h - pad - (v / max) * (h - 2 * pad));
|
||||
return xs.map((x, i) => x.toFixed(1) + ' ' + ys[i].toFixed(1)).join(' L ');
|
||||
}
|
||||
|
||||
/* ── greeting by hour ─────────────────────────────────────────── */
|
||||
function greeting() {
|
||||
const hr = new Date().getHours();
|
||||
if (hr < 6) return 'Доброй ночи';
|
||||
if (hr < 12) return 'Доброе утро';
|
||||
if (hr < 18) return 'Добрый день';
|
||||
return 'Добрый вечер';
|
||||
}
|
||||
function clockStr() {
|
||||
const d = new Date();
|
||||
return d.toLocaleTimeString('ru', { hour: '2-digit', minute: '2-digit' });
|
||||
}
|
||||
function dateStr() {
|
||||
const d = new Date();
|
||||
const s = d.toLocaleDateString('ru', { weekday: 'long', day: 'numeric', month: 'long' });
|
||||
return s.charAt(0).toUpperCase() + s.slice(1);
|
||||
}
|
||||
|
||||
/* ── KPI pulse row ────────────────────────────────────────────── */
|
||||
function kpiRow(d) {
|
||||
const sp = d.sparks || {};
|
||||
const sessSpark = sparkPath(sp.sessions, 120, 34);
|
||||
return `
|
||||
<section class="acc-pulse">
|
||||
<div class="acc-kpi hero">
|
||||
<div class="acc-kpi-top">
|
||||
<div class="acc-kpi-ic b"><svg class="acc-ic sm" viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg></div>
|
||||
<span class="acc-lbl">Сессий запущено · 24ч</span>
|
||||
<span class="acc-live"><span class="acc-dot"></span>LIVE</span>
|
||||
</div>
|
||||
<div class="acc-num">${fmtNum(d.newSessions24h)}</div>
|
||||
<div class="acc-kpi-foot">за последние сутки</div>
|
||||
<svg class="acc-spark" viewBox="0 0 120 34" preserveAspectRatio="none">
|
||||
<path class="acc-line" d="M ${sessSpark}" stroke="#3558e0"/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="acc-kpi">
|
||||
<div class="acc-kpi-top">
|
||||
<div class="acc-kpi-ic g"><svg class="acc-ic sm" viewBox="0 0 24 24"><path d="M4 18V7M9 18V4M14 18v-7M19 18V9"/></svg></div>
|
||||
<span class="acc-lbl">Активных юзеров</span>
|
||||
</div>
|
||||
<div class="acc-num">${fmtNum(d.activeUsers24h)}</div>
|
||||
<svg class="acc-spark" viewBox="0 0 74 26" preserveAspectRatio="none">
|
||||
<path class="acc-line" d="M ${sparkPath(sp.active, 74, 26)}" stroke="#16a34a"/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="acc-kpi">
|
||||
<div class="acc-kpi-top">
|
||||
<div class="acc-kpi-ic c"><svg class="acc-ic sm" viewBox="0 0 24 24"><circle cx="9" cy="8" r="3.4"/><path d="M3.5 20a5.5 5.5 0 0 1 11 0"/><path d="M18 7v6M21 10h-6"/></svg></div>
|
||||
<span class="acc-lbl">Новых за 24ч</span>
|
||||
</div>
|
||||
<div class="acc-num">${fmtNum(d.newUsers24h)}</div>
|
||||
<svg class="acc-spark" viewBox="0 0 74 26" preserveAspectRatio="none">
|
||||
<path class="acc-line" d="M ${sparkPath(sp.users, 74, 26)}" stroke="#0ea5b7"/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="acc-kpi">
|
||||
<div class="acc-kpi-top">
|
||||
<div class="acc-kpi-ic v"><svg class="acc-ic sm" viewBox="0 0 24 24"><rect x="3" y="4.5" width="18" height="15" rx="2.5"/><path d="M3 9h18M8 4.5v3M16 4.5v3"/></svg></div>
|
||||
<span class="acc-lbl">Всего классов</span>
|
||||
</div>
|
||||
<div class="acc-num">${fmtNum(d.classesTotal)}</div>
|
||||
<div class="acc-kpi-foot">активных учебных групп</div>
|
||||
</div>
|
||||
</section>`;
|
||||
}
|
||||
|
||||
/* ── attention inbox items (unified queue) ────────────────────── */
|
||||
function buildAttnItems(d) {
|
||||
const items = [];
|
||||
(d.bannedThisWeek || []).forEach((u) => {
|
||||
items.push({
|
||||
sev: 'rose', kind: 'block', kindLabel: 'Блокировка',
|
||||
title: u.name || '—',
|
||||
meta: `<span class="acc-mono">${e(u.email || '')}</span> · ${fmtBannedDate(u.banned_at)}`,
|
||||
act: 'Разблокировать', actHash: '#users', solid: false,
|
||||
});
|
||||
});
|
||||
(d.stuckSessions || []).forEach((s) => {
|
||||
items.push({
|
||||
sev: 'amber', kind: 'stuck', kindLabel: 'Зависла',
|
||||
title: s.user_name || '—',
|
||||
meta: `${e(s.subject_name || '—')} · висит <span class="acc-mono">${fmtSince(s.started_at)}</span>`,
|
||||
act: 'Открыть', actHash: '#sessions', solid: true,
|
||||
});
|
||||
});
|
||||
const ab = d.abandonedSessions24h || 0;
|
||||
if (ab > 0) {
|
||||
items.push({
|
||||
sev: 'amber', kind: 'stuck', kindLabel: 'Брошено',
|
||||
title: 'Всплеск брошенных сессий',
|
||||
meta: `<span class="acc-mono">${ab}</span> сессий прервано за 24ч`,
|
||||
act: 'Разобрать', actHash: '#sessions', solid: false,
|
||||
});
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
function attnRowHtml(it) {
|
||||
const icon = it.sev === 'rose'
|
||||
? '<svg class="acc-ic sm" viewBox="0 0 24 24"><circle cx="12" cy="8" r="3.4"/><path d="M5 20a7 7 0 0 1 14 0"/><path d="M18 5l3 3M21 5l-3 3"/></svg>'
|
||||
: '<svg class="acc-ic sm" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M12 7.5v5l3 2"/></svg>';
|
||||
return `
|
||||
<div class="acc-attn-row">
|
||||
<div class="acc-sev ${it.sev}">${icon}</div>
|
||||
<div class="acc-attn-main">
|
||||
<div class="acc-a-row1"><span class="acc-kind ${it.sev}">${e(it.kindLabel)}</span><h4>${e(it.title)}</h4></div>
|
||||
<div class="acc-attn-meta">${it.meta}</div>
|
||||
</div>
|
||||
<button class="acc-attn-act${it.solid ? ' solid' : ''}" data-go="${it.actHash}">${e(it.act)}</button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function attnCard(d) {
|
||||
const items = buildAttnItems(d);
|
||||
const blocks = items.filter((i) => i.kind === 'block');
|
||||
const stuck = items.filter((i) => i.kind === 'stuck');
|
||||
let shown = items;
|
||||
if (_tab === 'block') shown = blocks;
|
||||
else if (_tab === 'stuck') shown = stuck;
|
||||
|
||||
const body = shown.length
|
||||
? `<div class="acc-attn-list">${shown.map(attnRowHtml).join('')}</div>`
|
||||
: `<div class="acc-attn-empty">
|
||||
<svg class="acc-ic" viewBox="0 0 24 24"><path d="M20 6 9 17l-5-5"/></svg>
|
||||
<b>Всё в норме</b><span>нет событий, требующих внимания</span>
|
||||
</div>`;
|
||||
|
||||
return `
|
||||
<section class="acc-card acc-attn">
|
||||
<div class="acc-card-head">
|
||||
<div class="acc-ttl-ic"><svg class="acc-ic sm" viewBox="0 0 24 24"><path d="M12 3l9 16H3z"/><path d="M12 10v4M12 17h.01"/></svg></div>
|
||||
<h2>Требует внимания</h2>
|
||||
<span class="acc-count">${items.length} ${items.length === 1 ? 'событие' : 'событий'}</span>
|
||||
<span class="acc-more" data-go="#sessions">все алерты <svg class="acc-ic xs" viewBox="0 0 24 24"><path d="M5 12h14M13 6l6 6-6 6"/></svg></span>
|
||||
</div>
|
||||
<div class="acc-attn-tabs">
|
||||
<button class="acc-attn-tab${_tab === 'all' ? ' on' : ''}" data-tab="all">Все <span class="acc-tag">${items.length}</span></button>
|
||||
<button class="acc-attn-tab${_tab === 'block' ? ' on' : ''}" data-tab="block">Блокировки <span class="acc-tag rose">${blocks.length}</span></button>
|
||||
<button class="acc-attn-tab${_tab === 'stuck' ? ' on' : ''}" data-tab="stuck">Зависшие <span class="acc-tag amber">${stuck.length}</span></button>
|
||||
</div>
|
||||
${body}
|
||||
<div class="acc-attn-foot">
|
||||
<span>Единая очередь действий вместо разрозненных карточек.</span>
|
||||
<span><b>${items.length}</b> в очереди</span>
|
||||
</div>
|
||||
</section>`;
|
||||
}
|
||||
|
||||
/* ── live feed (top sessions) ─────────────────────────────────── */
|
||||
function feedCard(d) {
|
||||
const rows = (d.topSessions24h || []).slice(0, 8);
|
||||
const subj = d.sessionsBySubject24h || [];
|
||||
const total = subj.reduce((a, r) => a + r.n, 0) || 1;
|
||||
|
||||
const feedHtml = rows.length ? rows.map((s) => {
|
||||
const name = s.user_name || '—';
|
||||
const pc = s.percent;
|
||||
return `
|
||||
<div class="acc-feed-row">
|
||||
<div class="acc-feed-av" style="background:hsl(${hashHue(name)},55%,55%)">${e(initials(name))}</div>
|
||||
<div class="acc-feed-main">
|
||||
<b>${e(name)}</b>
|
||||
<div class="acc-f-meta">${e(s.subject_name || '—')} · ${fmtAgo(s.finished_at)}</div>
|
||||
</div>
|
||||
<div class="acc-feed-right">
|
||||
<div class="acc-feed-pct ${pctClass(pc)}">${pc != null ? pc : '—'}%</div>
|
||||
<div class="acc-feed-ago">${(s.score != null ? s.score : 0)}/${(s.total != null ? s.total : 0)}</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('') : '<div class="acc-attn-empty" style="padding:30px 16px"><span>Нет завершённых сессий за 24ч</span></div>';
|
||||
|
||||
let segs = '', legend = '';
|
||||
subj.forEach((r, i) => {
|
||||
const pct = (r.n / total * 100).toFixed(1);
|
||||
const col = SUBJ_COLORS[i % SUBJ_COLORS.length];
|
||||
segs += `<div class="acc-seg" style="width:${pct}%;background:${col}" title="${e(r.name)}: ${r.n}"></div>`;
|
||||
legend += `<span><span class="acc-subj-dot" style="background:${col}"></span>${e(r.name)} <b>${r.n}</b></span>`;
|
||||
});
|
||||
const subjBlock = subj.length ? `
|
||||
<div class="acc-subj-mini">
|
||||
<div class="acc-sm-head"><span>Сессии по предметам · 24ч</span><b>${total}</b></div>
|
||||
<div class="acc-subj-track">${segs}</div>
|
||||
<div class="acc-subj-legend">${legend}</div>
|
||||
</div>` : '';
|
||||
|
||||
return `
|
||||
<section class="acc-card">
|
||||
<div class="acc-card-head">
|
||||
<div class="acc-ttl-ic" style="background:var(--acc-green-50)"><svg class="acc-ic sm" style="stroke:var(--acc-green)" viewBox="0 0 24 24"><path d="M4 18V7M9 18V4M14 18v-7M19 18V9"/></svg></div>
|
||||
<h2>Топ сегодня</h2>
|
||||
<span class="acc-more" data-go="#sessions">все сессии <svg class="acc-ic xs" viewBox="0 0 24 24"><path d="M5 12h14M13 6l6 6-6 6"/></svg></span>
|
||||
</div>
|
||||
<div class="acc-feed">${feedHtml}</div>
|
||||
${subjBlock}
|
||||
</section>`;
|
||||
}
|
||||
|
||||
/* ── content health ───────────────────────────────────────────── */
|
||||
function healthRow(d) {
|
||||
const inv = d.inventory || {};
|
||||
const card = (icon, n, lbl) => `
|
||||
<div class="acc-hcard">
|
||||
<div class="acc-hcard-top">
|
||||
<div class="acc-hcard-ic">${icon}</div>
|
||||
<span class="acc-lbl">${lbl}</span>
|
||||
</div>
|
||||
<div class="acc-hn">${fmtNum(n != null ? n : 0)}</div>
|
||||
</div>`;
|
||||
return `
|
||||
<div class="acc-sec-title"><span>Здоровье контента</span><span class="acc-ln"></span></div>
|
||||
<div class="acc-health">
|
||||
${card('<svg class="acc-ic sm" viewBox="0 0 24 24"><path d="M9.5 9a2.5 2.5 0 1 1 3.4 2.3c-.8.4-1.4.9-1.4 1.7v.4"/><path d="M11.5 17h.01"/><circle cx="12" cy="12" r="9"/></svg>', inv.questions, 'вопросов')}
|
||||
${card('<svg class="acc-ic sm" viewBox="0 0 24 24"><path d="M9 5h7a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2z"/><path d="M9 9h6M9 13h6M9 17h4"/></svg>', inv.tests, 'тестов')}
|
||||
${card('<svg class="acc-ic sm" viewBox="0 0 24 24"><path d="M4 5a2 2 0 0 1 2-2h6v17H6a2 2 0 0 0-2 2z"/><path d="M20 5a2 2 0 0 0-2-2h-6v17h6a2 2 0 0 1 2 2z"/></svg>', inv.courses, 'курсов')}
|
||||
${card('<svg class="acc-ic sm" viewBox="0 0 24 24"><rect x="3" y="4.5" width="18" height="15" rx="2.5"/><path d="M3 9h18M8 4.5v3M16 4.5v3"/></svg>', inv.classes, 'классов')}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/* ── results tables (top / worst) ─────────────────────────────── */
|
||||
function resTable(rows) {
|
||||
if (!rows || !rows.length) return '<div class="acc-attn-empty" style="padding:26px 14px"><span>Нет данных за 24ч</span></div>';
|
||||
const body = rows.slice(0, 5).map((s) => {
|
||||
const name = s.user_name || '—';
|
||||
return `<tr>
|
||||
<td><div class="acc-rt-user"><span class="acc-rt-av" style="background:hsl(${hashHue(name)},55%,55%)">${e(initials(name))}</span>${e(name)}</div></td>
|
||||
<td><span class="acc-rt-subj">${e(s.subject_name || '—')}</span></td>
|
||||
<td class="r"><span class="acc-rt-score">${(s.score != null ? s.score : 0)}/${(s.total != null ? s.total : 0)}</span></td>
|
||||
<td class="r"><span class="acc-rt-pct ${pctClass(s.percent)}">${s.percent != null ? s.percent : '—'}%</span></td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
return `<table class="acc-rtable">
|
||||
<thead><tr><th>Ученик</th><th>Предмет</th><th class="r">Счёт</th><th class="r">%</th></tr></thead>
|
||||
<tbody>${body}</tbody></table>`;
|
||||
}
|
||||
function resultsRow(d) {
|
||||
return `
|
||||
<div class="acc-sec-title"><span>Результаты · 24ч</span><span class="acc-ln"></span></div>
|
||||
<div class="acc-results">
|
||||
<section class="acc-card">
|
||||
<div class="acc-card-head"><h2>Топ-5 сегодня</h2></div>
|
||||
${resTable(d.topSessions24h)}
|
||||
</section>
|
||||
<section class="acc-card">
|
||||
<div class="acc-card-head"><h2>Худшие 5 сегодня</h2></div>
|
||||
${resTable(d.worstSessions24h)}
|
||||
</section>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/* ── quick actions ────────────────────────────────────────────── */
|
||||
function quickRow() {
|
||||
const btn = (icon, title, sub, hash) => `
|
||||
<button class="acc-qbtn" data-go="${hash}">
|
||||
<div class="acc-qbtn-ic">${icon}</div>
|
||||
<b>${title}</b><span>${sub}</span>
|
||||
</button>`;
|
||||
return `
|
||||
<div class="acc-sec-title"><span>Быстрые действия</span><span class="acc-ln"></span></div>
|
||||
<div class="acc-quick">
|
||||
${btn('<svg class="acc-ic sm" viewBox="0 0 24 24"><circle cx="9" cy="8" r="3.4"/><path d="M3.5 20a5.5 5.5 0 0 1 11 0"/><circle cx="17.5" cy="9" r="2.6"/><path d="M16 14.6A4.6 4.6 0 0 1 21 19"/></svg>', 'Пользователи', '#users', '/admin#users')}
|
||||
${btn('<svg class="acc-ic sm" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M12 7.5v5l3.5 2"/></svg>', 'Сессии', '#sessions', '/admin#sessions')}
|
||||
${btn('<svg class="acc-ic sm" viewBox="0 0 24 24"><path d="M9 5h7a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2z"/><path d="M9 9h6M9 13h6"/></svg>', 'Тесты', '#tests', '/admin#tests')}
|
||||
${btn('<svg class="acc-ic sm" viewBox="0 0 24 24"><rect x="3" y="4.5" width="18" height="15" rx="2.5"/><path d="M3 9h18M8 4.5v3M16 4.5v3"/></svg>', 'Классы', '#classes', '/classes')}
|
||||
${btn('<svg class="acc-ic sm" viewBox="0 0 24 24"><rect x="5" y="11" width="14" height="9" rx="2"/><path d="M8 11V8a4 4 0 0 1 8 0v3"/></svg>', 'Права', '#permissions', '/admin#permissions')}
|
||||
${btn('<svg class="acc-ic sm" viewBox="0 0 24 24"><path d="M6 3h9l3 3v15H6a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1z"/><path d="M9 9h6M9 13h6M9 17h4"/></svg>', 'Аудит-лог', '#sublog', '/admin#sublog')}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/* ── header (page-head + topbar strip) ────────────────────────── */
|
||||
function headHtml(d) {
|
||||
const items = buildAttnItems(d);
|
||||
const blocks = items.filter((i) => i.kind === 'block').length;
|
||||
const stuck = (d.stuckSessions || []).length;
|
||||
const ab = d.abandonedSessions24h || 0;
|
||||
let sub;
|
||||
if (!items.length) {
|
||||
sub = 'Сегодня всё спокойно — событий, требующих внимания, нет.';
|
||||
} else {
|
||||
const parts = [];
|
||||
if (blocks) parts.push(`<b>${blocks}</b> ${blocks === 1 ? 'блокировка' : 'блокировок'}`);
|
||||
if (stuck) parts.push(`<b>${stuck}</b> ${stuck === 1 ? 'зависшая сессия' : 'зависших'}`);
|
||||
if (ab) parts.push(`<b>${ab}</b> брошенных`);
|
||||
sub = 'Требует внимания: ' + parts.join(', ') + '.';
|
||||
}
|
||||
return `
|
||||
<header class="acc-topbar">
|
||||
<div class="acc-crumbs">
|
||||
<span>Дашборд</span>
|
||||
<svg class="acc-ic xs" viewBox="0 0 24 24"><path d="M9 6l6 6-6 6"/></svg>
|
||||
<span class="here">Командный центр</span>
|
||||
</div>
|
||||
<div class="acc-clock"><span id="acc-date">${dateStr()}</span> · <b id="acc-clock">${clockStr()}</b></div>
|
||||
<div class="acc-tb-right">
|
||||
<button class="acc-tb-icon" data-act="refresh" title="Обновить">
|
||||
<svg class="acc-ic" viewBox="0 0 24 24"><path d="M20 11a8 8 0 1 0-1.5 5.5"/><path d="M20 5v6h-6"/></svg>
|
||||
</button>
|
||||
<button class="acc-btn" data-go="/admin">
|
||||
<svg class="acc-ic sm" viewBox="0 0 24 24"><path d="M4 6h16M4 12h16M4 18h10"/></svg>
|
||||
Полная админка
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<section class="acc-page-head">
|
||||
<div>
|
||||
<div class="acc-kicker">
|
||||
<svg class="acc-ic sm" viewBox="0 0 24 24"><path d="M4 14l5-5 3 3 4-6 4 5"/><path d="M4 19h16"/></svg>
|
||||
<span>${greeting()}</span><span class="muted">· сводка за 24 часа</span>
|
||||
</div>
|
||||
<h1>Командный <span>центр</span></h1>
|
||||
<p class="acc-sub">${sub}</p>
|
||||
</div>
|
||||
</section>`;
|
||||
}
|
||||
|
||||
/* ── full render ──────────────────────────────────────────────── */
|
||||
function render() {
|
||||
if (!_root || !_data) return;
|
||||
const d = _data;
|
||||
_root.innerHTML = `
|
||||
<div class="acc-shell">
|
||||
${headHtml(d)}
|
||||
${kpiRow(d)}
|
||||
<div class="acc-grid">
|
||||
${attnCard(d)}
|
||||
<div class="acc-col">${feedCard(d)}</div>
|
||||
</div>
|
||||
${healthRow(d)}
|
||||
${resultsRow(d)}
|
||||
${quickRow()}
|
||||
<div class="acc-foot">
|
||||
<span>LearnSpace · командный центр администратора</span>
|
||||
<span>обновлено ${clockStr()}</span>
|
||||
</div>
|
||||
</div>`;
|
||||
wire();
|
||||
}
|
||||
|
||||
function wire() {
|
||||
_root.querySelectorAll('[data-go]').forEach((el) => {
|
||||
el.addEventListener('click', () => go(el.getAttribute('data-go')));
|
||||
});
|
||||
_root.querySelectorAll('[data-tab]').forEach((el) => {
|
||||
el.addEventListener('click', () => { _tab = el.getAttribute('data-tab'); render(); });
|
||||
});
|
||||
const rb = _root.querySelector('[data-act="refresh"]');
|
||||
if (rb) rb.addEventListener('click', () => load());
|
||||
}
|
||||
|
||||
function renderSkeleton() {
|
||||
_root.innerHTML = `
|
||||
<div class="acc-shell">
|
||||
<div class="acc-skel-cards">
|
||||
<div class="acc-skel hero"></div><div class="acc-skel"></div>
|
||||
<div class="acc-skel"></div><div class="acc-skel"></div>
|
||||
</div>
|
||||
<div class="acc-skel-rows">
|
||||
<div class="acc-skel row"></div><div class="acc-skel row"></div>
|
||||
<div class="acc-skel row"></div><div class="acc-skel row"></div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function startClock() {
|
||||
if (_clockTimer) return;
|
||||
_clockTimer = setInterval(() => {
|
||||
const c = document.getElementById('acc-clock');
|
||||
if (c) c.textContent = clockStr();
|
||||
}, 30000);
|
||||
}
|
||||
|
||||
async function load() {
|
||||
if (!_root) return;
|
||||
renderSkeleton();
|
||||
try {
|
||||
_data = await LS.adminGetOverview();
|
||||
render();
|
||||
startClock();
|
||||
} catch (err) {
|
||||
_root.innerHTML = `<div class="acc-shell"><div class="acc-attn-empty" style="padding:60px 20px">
|
||||
<b>Не удалось загрузить данные</b>
|
||||
<span>${e(err && err.message ? err.message : 'Ошибка сети')}</span>
|
||||
<button class="acc-btn" style="margin-top:14px" data-act="retry">Повторить</button>
|
||||
</div></div>`;
|
||||
const rb = _root.querySelector('[data-act="retry"]');
|
||||
if (rb) rb.addEventListener('click', () => load());
|
||||
}
|
||||
}
|
||||
|
||||
function mount(rootEl) {
|
||||
ensureAssets();
|
||||
_root = rootEl;
|
||||
load();
|
||||
}
|
||||
|
||||
window.DashAdminCenter = { mount, reload: load };
|
||||
|
||||
/* ════════════════════ SCOPED CSS ════════════════════ */
|
||||
const CSS = `
|
||||
#admin-command-center{
|
||||
--bg:#f3f4f7; --surface:#fff; --surface-2:#fafbfc; --surface-3:#f0f2f6;
|
||||
--border:#e7e9ef; --border-2:#dadde5;
|
||||
--tx:#181b22; --tx2:#525866; --tx3:#828997; --tx4:#aab0bb;
|
||||
--acc:#3558e0; --acc-600:#2a47bd; --acc-700:#233a9c; --acc-50:#eef1fe; --acc-100:#dee5fc;
|
||||
--acc-green:#16a34a; --acc-green-50:#e8f6ed; --acc-amber:#d97706; --acc-amber-50:#fdf1e2;
|
||||
--acc-rose:#e11d48; --acc-rose-50:#fdeaef; --acc-cyan:#0ea5b7; --acc-cyan-50:#e4f6f8;
|
||||
--acc-violet:#7c3aed; --acc-violet-50:#f1ecfe;
|
||||
--acc-sh-xs:0 1px 2px rgba(18,22,31,.04);
|
||||
--acc-sh-sm:0 1px 2px rgba(18,22,31,.05),0 1px 3px rgba(18,22,31,.05);
|
||||
--acc-sh:0 2px 4px -1px rgba(18,22,31,.05),0 8px 20px -6px rgba(18,22,31,.10);
|
||||
--acc-sh-accent:0 4px 14px -2px rgba(53,88,224,.40);
|
||||
--r-sm:9px; --r:12px; --r-lg:16px;
|
||||
--sans:'Hanken Grotesk',system-ui,-apple-system,sans-serif;
|
||||
--mono:'JetBrains Mono',ui-monospace,Menlo,monospace;
|
||||
font-family:var(--sans); color:var(--tx); font-size:14px; line-height:1.5;
|
||||
-webkit-font-smoothing:antialiased;
|
||||
}
|
||||
#admin-command-center *{ box-sizing:border-box; }
|
||||
#admin-command-center .acc-shell{ max-width:1380px; margin:0 auto; }
|
||||
#admin-command-center .acc-mono{ font-family:var(--mono); font-variant-numeric:tabular-nums; color:var(--tx2); }
|
||||
#admin-command-center .acc-ic{ width:18px; height:18px; flex:0 0 auto; display:inline-block;
|
||||
stroke:currentColor; fill:none; stroke-width:1.7; stroke-linecap:round; stroke-linejoin:round; }
|
||||
#admin-command-center .acc-ic.sm{ width:15px; height:15px; stroke-width:1.8; }
|
||||
#admin-command-center .acc-ic.xs{ width:13px; height:13px; stroke-width:1.9; }
|
||||
|
||||
/* topbar */
|
||||
#admin-command-center .acc-topbar{ display:flex; align-items:center; gap:14px; margin-bottom:18px; flex-wrap:wrap; }
|
||||
#admin-command-center .acc-crumbs{ display:flex; align-items:center; gap:8px; font-size:13px; color:var(--tx3); }
|
||||
#admin-command-center .acc-crumbs .here{ color:var(--tx); font-weight:600; }
|
||||
#admin-command-center .acc-crumbs .acc-ic{ stroke:var(--tx4); }
|
||||
#admin-command-center .acc-clock{ font-family:var(--mono); font-size:12px; color:var(--tx3);
|
||||
padding-left:14px; border-left:1px solid var(--border); }
|
||||
#admin-command-center .acc-clock b{ color:var(--tx2); font-weight:600; }
|
||||
#admin-command-center .acc-tb-right{ margin-left:auto; display:flex; align-items:center; gap:8px; }
|
||||
#admin-command-center .acc-tb-icon{ width:34px; height:34px; display:grid; place-items:center;
|
||||
border-radius:var(--r-sm); border:1px solid var(--border); background:var(--surface);
|
||||
color:var(--tx2); box-shadow:var(--acc-sh-xs); transition:border-color .14s,color .14s; }
|
||||
#admin-command-center .acc-tb-icon:hover{ border-color:var(--border-2); color:var(--tx); }
|
||||
#admin-command-center .acc-btn{ display:inline-flex; align-items:center; gap:7px; height:34px; padding:0 14px;
|
||||
border-radius:var(--r-sm); font-size:13px; font-weight:600; border:1px solid var(--acc-600);
|
||||
background:linear-gradient(180deg,#4763e6,#3558e0); color:#fff;
|
||||
box-shadow:var(--acc-sh-accent),inset 0 1px 0 rgba(255,255,255,.2); transition:filter .14s,transform .12s; }
|
||||
#admin-command-center .acc-btn:hover{ filter:brightness(1.06); }
|
||||
#admin-command-center .acc-btn:active{ transform:translateY(1px); }
|
||||
#admin-command-center .acc-btn .acc-ic{ stroke:#fff; }
|
||||
|
||||
/* page head */
|
||||
#admin-command-center .acc-page-head{ display:flex; align-items:flex-end; justify-content:space-between; gap:24px; margin-bottom:20px; }
|
||||
#admin-command-center .acc-kicker{ display:inline-flex; align-items:center; gap:8px; font-family:var(--mono);
|
||||
font-size:11px; font-weight:500; letter-spacing:.03em; color:var(--acc); margin-bottom:9px; }
|
||||
#admin-command-center .acc-kicker .acc-ic{ stroke:var(--acc); }
|
||||
#admin-command-center .acc-kicker .muted{ color:var(--tx3); }
|
||||
#admin-command-center .acc-page-head h1{ font-size:30px; font-weight:800; letter-spacing:-.025em; line-height:1.05; margin:0; font-family:var(--sans); }
|
||||
#admin-command-center .acc-page-head h1 span{ color:var(--acc); }
|
||||
#admin-command-center .acc-sub{ margin:7px 0 0; font-size:14px; color:var(--tx2); max-width:60ch; }
|
||||
#admin-command-center .acc-sub b{ color:var(--tx); font-weight:700; }
|
||||
|
||||
/* pulse kpi */
|
||||
#admin-command-center .acc-pulse{ display:grid; grid-template-columns:1.4fr 1fr 1fr 1fr; gap:14px; margin-bottom:22px; }
|
||||
#admin-command-center .acc-kpi{ background:var(--surface); border:1px solid var(--border); border-radius:var(--r-lg);
|
||||
padding:16px 17px; box-shadow:var(--acc-sh-xs); position:relative; overflow:hidden; transition:transform .16s,box-shadow .16s,border-color .16s; }
|
||||
#admin-command-center .acc-kpi:hover{ transform:translateY(-2px); box-shadow:var(--acc-sh); border-color:var(--border-2); }
|
||||
#admin-command-center .acc-kpi-top{ display:flex; align-items:center; gap:9px; margin-bottom:11px; }
|
||||
#admin-command-center .acc-kpi-ic{ width:30px; height:30px; border-radius:8px; display:grid; place-items:center; flex:0 0 auto; }
|
||||
#admin-command-center .acc-kpi-ic.b{ background:var(--acc-50); } #admin-command-center .acc-kpi-ic.b .acc-ic{ stroke:var(--acc); }
|
||||
#admin-command-center .acc-kpi-ic.c{ background:var(--acc-cyan-50); } #admin-command-center .acc-kpi-ic.c .acc-ic{ stroke:var(--acc-cyan); }
|
||||
#admin-command-center .acc-kpi-ic.g{ background:var(--acc-green-50); } #admin-command-center .acc-kpi-ic.g .acc-ic{ stroke:var(--acc-green); }
|
||||
#admin-command-center .acc-kpi-ic.v{ background:var(--acc-violet-50); } #admin-command-center .acc-kpi-ic.v .acc-ic{ stroke:var(--acc-violet); }
|
||||
#admin-command-center .acc-lbl{ font-size:12px; font-weight:600; color:var(--tx3); }
|
||||
#admin-command-center .acc-live{ margin-left:auto; display:inline-flex; align-items:center; gap:5px;
|
||||
font-family:var(--mono); font-size:9px; font-weight:600; letter-spacing:.06em; color:var(--acc-green); }
|
||||
#admin-command-center .acc-dot{ width:6px; height:6px; border-radius:50%; background:var(--acc-green); position:relative; }
|
||||
#admin-command-center .acc-dot::after{ content:""; position:absolute; inset:-3px; border-radius:50%;
|
||||
border:1.4px solid var(--acc-green); animation:acc-ping 1.8s ease-out infinite; }
|
||||
@keyframes acc-ping{ 0%{ transform:scale(.5); opacity:.85 } 100%{ transform:scale(1.8); opacity:0 } }
|
||||
#admin-command-center .acc-num{ font-family:var(--mono); font-size:30px; font-weight:700; letter-spacing:-.03em;
|
||||
line-height:1; font-variant-numeric:tabular-nums; }
|
||||
#admin-command-center .acc-kpi.hero .acc-num{ font-size:38px; }
|
||||
#admin-command-center .acc-kpi-foot{ margin-top:11px; font-size:12px; color:var(--tx3); }
|
||||
#admin-command-center .acc-spark{ position:absolute; right:14px; bottom:14px; width:74px; height:26px; opacity:.85; pointer-events:none; }
|
||||
#admin-command-center .acc-kpi.hero .acc-spark{ width:120px; height:34px; }
|
||||
#admin-command-center .acc-line{ fill:none; stroke-width:2; stroke-linecap:round; stroke-linejoin:round; }
|
||||
|
||||
/* main grid */
|
||||
#admin-command-center .acc-grid{ display:grid; grid-template-columns:minmax(0,1.55fr) minmax(0,1fr); gap:20px; align-items:start; margin-bottom:20px; }
|
||||
#admin-command-center .acc-col{ display:flex; flex-direction:column; gap:20px; }
|
||||
#admin-command-center .acc-card{ background:var(--surface); border:1px solid var(--border); border-radius:var(--r-lg); box-shadow:var(--acc-sh-xs); overflow:hidden; }
|
||||
#admin-command-center .acc-card-head{ display:flex; align-items:center; gap:10px; padding:14px 17px; border-bottom:1px solid var(--border); }
|
||||
#admin-command-center .acc-ttl-ic{ width:28px; height:28px; border-radius:8px; display:grid; place-items:center; flex:0 0 auto; background:var(--acc-50); }
|
||||
#admin-command-center .acc-ttl-ic .acc-ic{ stroke:var(--acc); }
|
||||
#admin-command-center .acc-card-head h2{ font-size:15px; font-weight:700; letter-spacing:-.01em; margin:0; font-family:var(--sans); }
|
||||
#admin-command-center .acc-count{ font-family:var(--mono); font-size:11px; font-weight:600; color:var(--tx2);
|
||||
background:var(--surface-3); border:1px solid var(--border); border-radius:99px; padding:2px 9px; }
|
||||
#admin-command-center .acc-more{ margin-left:auto; display:inline-flex; align-items:center; gap:5px;
|
||||
font-size:12.5px; font-weight:600; color:var(--tx3); cursor:pointer; transition:color .14s; }
|
||||
#admin-command-center .acc-more:hover{ color:var(--acc); }
|
||||
|
||||
/* attention */
|
||||
#admin-command-center .acc-attn{ border-color:#f1d9c4; box-shadow:0 0 0 1px rgba(217,119,6,.05),var(--acc-sh-sm); }
|
||||
#admin-command-center .acc-attn .acc-card-head{ background:linear-gradient(180deg,#fffaf3,var(--surface)); border-bottom-color:#f3e3d0; }
|
||||
#admin-command-center .acc-attn .acc-ttl-ic{ background:var(--acc-amber-50); }
|
||||
#admin-command-center .acc-attn .acc-ttl-ic .acc-ic{ stroke:var(--acc-amber); }
|
||||
#admin-command-center .acc-attn-tabs{ display:flex; gap:4px; padding:10px 12px 0; flex-wrap:wrap; }
|
||||
#admin-command-center .acc-attn-tab{ display:inline-flex; align-items:center; gap:7px; height:30px; padding:0 12px; border:none;
|
||||
background:none; border-radius:8px; font-size:12.5px; font-weight:600; color:var(--tx3); cursor:pointer; transition:background .14s,color .14s; font-family:var(--sans); }
|
||||
#admin-command-center .acc-attn-tab:hover{ background:var(--surface-3); color:var(--tx); }
|
||||
#admin-command-center .acc-attn-tab.on{ background:var(--tx); color:#fff; }
|
||||
#admin-command-center .acc-tag{ font-family:var(--mono); font-size:10px; font-weight:700; padding:1px 6px; border-radius:99px; background:var(--surface-3); color:var(--tx2); }
|
||||
#admin-command-center .acc-attn-tab.on .acc-tag{ background:rgba(255,255,255,.16); color:#fff; }
|
||||
#admin-command-center .acc-attn-list{ padding:8px; }
|
||||
#admin-command-center .acc-attn-row{ display:grid; grid-template-columns:40px minmax(0,1fr) auto; gap:12px; align-items:center;
|
||||
padding:11px 10px; border-radius:var(--r); transition:background .14s; }
|
||||
#admin-command-center .acc-attn-row+.acc-attn-row{ margin-top:1px; }
|
||||
#admin-command-center .acc-attn-row:hover{ background:var(--surface-3); }
|
||||
#admin-command-center .acc-sev{ width:40px; height:40px; border-radius:11px; display:grid; place-items:center; }
|
||||
#admin-command-center .acc-sev.rose{ background:var(--acc-rose-50); } #admin-command-center .acc-sev.rose .acc-ic{ stroke:var(--acc-rose); }
|
||||
#admin-command-center .acc-sev.amber{ background:var(--acc-amber-50); } #admin-command-center .acc-sev.amber .acc-ic{ stroke:var(--acc-amber); }
|
||||
#admin-command-center .acc-attn-main{ min-width:0; }
|
||||
#admin-command-center .acc-a-row1{ display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
|
||||
#admin-command-center .acc-attn-main h4{ font-size:13.5px; font-weight:600; letter-spacing:-.01em; margin:0; font-family:var(--sans); }
|
||||
#admin-command-center .acc-kind{ font-family:var(--mono); font-size:9.5px; font-weight:600; letter-spacing:.04em; text-transform:uppercase; padding:2px 7px; border-radius:6px; }
|
||||
#admin-command-center .acc-kind.rose{ background:var(--acc-rose-50); color:var(--acc-rose); }
|
||||
#admin-command-center .acc-kind.amber{ background:var(--acc-amber-50); color:var(--acc-amber); }
|
||||
#admin-command-center .acc-attn-meta{ font-size:11.5px; color:var(--tx3); margin-top:3px; }
|
||||
#admin-command-center .acc-attn-meta .acc-mono{ color:var(--tx2); }
|
||||
#admin-command-center .acc-attn-act{ display:inline-flex; align-items:center; gap:6px; height:30px; padding:0 12px; white-space:nowrap;
|
||||
border:1px solid var(--border); border-radius:var(--r-sm); background:var(--surface); font-size:12px; font-weight:600; color:var(--tx2);
|
||||
box-shadow:var(--acc-sh-xs); cursor:pointer; transition:border-color .14s,color .14s,background .14s; font-family:var(--sans); }
|
||||
#admin-command-center .acc-attn-act:hover{ border-color:var(--acc-600); color:var(--acc-700); background:var(--acc-50); }
|
||||
#admin-command-center .acc-attn-act.solid{ background:var(--tx); color:#fff; border-color:var(--tx); }
|
||||
#admin-command-center .acc-attn-act.solid:hover{ background:#000; }
|
||||
#admin-command-center .acc-attn-foot{ display:flex; align-items:center; justify-content:space-between; gap:12px;
|
||||
padding:11px 17px; border-top:1px solid var(--border); background:var(--surface-2); font-size:12px; color:var(--tx3); }
|
||||
#admin-command-center .acc-attn-foot b{ color:var(--tx); font-weight:700; }
|
||||
#admin-command-center .acc-attn-empty{ display:flex; flex-direction:column; align-items:center; justify-content:center; gap:4px;
|
||||
padding:46px 16px; text-align:center; color:var(--tx3); }
|
||||
#admin-command-center .acc-attn-empty .acc-ic{ stroke:var(--acc-green); width:30px; height:30px; margin-bottom:6px; }
|
||||
#admin-command-center .acc-attn-empty b{ color:var(--tx); font-size:14px; }
|
||||
#admin-command-center .acc-attn-empty span{ font-size:12.5px; }
|
||||
|
||||
/* feed */
|
||||
#admin-command-center .acc-feed{ padding:6px 8px 8px; max-height:430px; overflow-y:auto; }
|
||||
#admin-command-center .acc-feed-row{ display:grid; grid-template-columns:30px minmax(0,1fr) auto; gap:11px; align-items:center; padding:9px 10px; border-radius:var(--r); transition:background .14s; }
|
||||
#admin-command-center .acc-feed-row:hover{ background:var(--surface-3); }
|
||||
#admin-command-center .acc-feed-av{ width:30px; height:30px; border-radius:50%; display:grid; place-items:center; color:#fff; font-size:11px; font-weight:700; }
|
||||
#admin-command-center .acc-feed-main{ min-width:0; }
|
||||
#admin-command-center .acc-feed-main b{ font-size:13px; font-weight:600; display:block; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
||||
#admin-command-center .acc-f-meta{ font-size:11px; color:var(--tx3); margin-top:1px; }
|
||||
#admin-command-center .acc-feed-right{ text-align:right; }
|
||||
#admin-command-center .acc-feed-pct{ font-family:var(--mono); font-size:14px; font-weight:700; line-height:1; }
|
||||
#admin-command-center .acc-feed-pct.hi{ color:var(--acc-green); } #admin-command-center .acc-feed-pct.mid{ color:var(--acc-amber); } #admin-command-center .acc-feed-pct.lo{ color:var(--acc-rose); }
|
||||
#admin-command-center .acc-feed-ago{ font-size:10px; color:var(--tx4); font-family:var(--mono); margin-top:2px; }
|
||||
|
||||
/* subject mini */
|
||||
#admin-command-center .acc-subj-mini{ padding:13px 17px; border-top:1px solid var(--border); background:var(--surface-2); }
|
||||
#admin-command-center .acc-sm-head{ display:flex; align-items:center; justify-content:space-between; margin-bottom:9px; font-size:11.5px; color:var(--tx3); font-weight:600; }
|
||||
#admin-command-center .acc-sm-head b{ font-family:var(--mono); color:var(--tx2); }
|
||||
#admin-command-center .acc-subj-track{ height:9px; border-radius:5px; overflow:hidden; display:flex; background:var(--border); margin-bottom:9px; }
|
||||
#admin-command-center .acc-seg{ height:100%; transition:width .6s cubic-bezier(.22,.72,.28,1); }
|
||||
#admin-command-center .acc-subj-legend{ display:flex; flex-wrap:wrap; gap:6px 13px; font-size:11px; color:var(--tx3); }
|
||||
#admin-command-center .acc-subj-legend span{ display:inline-flex; align-items:center; gap:5px; }
|
||||
#admin-command-center .acc-subj-legend b{ font-family:var(--mono); color:var(--tx2); font-weight:600; }
|
||||
#admin-command-center .acc-subj-dot{ width:7px; height:7px; border-radius:50%; }
|
||||
|
||||
/* section title */
|
||||
#admin-command-center .acc-sec-title{ display:flex; align-items:center; gap:9px; margin:4px 0 13px;
|
||||
font-family:var(--mono); font-size:11px; font-weight:500; letter-spacing:.1em; text-transform:uppercase; color:var(--tx3); }
|
||||
#admin-command-center .acc-ln{ flex:1; height:1px; background:linear-gradient(90deg,var(--border),transparent); }
|
||||
|
||||
/* health */
|
||||
#admin-command-center .acc-health{ display:grid; grid-template-columns:repeat(4,1fr); gap:14px; margin-bottom:24px; }
|
||||
#admin-command-center .acc-hcard{ background:var(--surface); border:1px solid var(--border); border-radius:var(--r-lg); padding:15px 16px;
|
||||
box-shadow:var(--acc-sh-xs); transition:transform .16s,box-shadow .16s,border-color .16s; }
|
||||
#admin-command-center .acc-hcard:hover{ transform:translateY(-2px); box-shadow:var(--acc-sh); border-color:var(--border-2); }
|
||||
#admin-command-center .acc-hcard-top{ display:flex; align-items:center; gap:9px; margin-bottom:10px; }
|
||||
#admin-command-center .acc-hcard-ic{ width:28px; height:28px; border-radius:8px; display:grid; place-items:center; background:var(--surface-3); color:var(--tx2); }
|
||||
#admin-command-center .acc-hcard-ic .acc-ic{ stroke:currentColor; }
|
||||
#admin-command-center .acc-hn{ font-family:var(--mono); font-size:25px; font-weight:700; letter-spacing:-.02em; line-height:1; }
|
||||
|
||||
/* results */
|
||||
#admin-command-center .acc-results{ display:grid; grid-template-columns:1fr 1fr; gap:20px; margin-bottom:24px; }
|
||||
#admin-command-center .acc-rtable{ width:100%; border-collapse:collapse; }
|
||||
#admin-command-center .acc-rtable th{ text-align:left; font-family:var(--mono); font-size:10px; text-transform:uppercase; letter-spacing:.06em;
|
||||
color:var(--tx3); font-weight:600; padding:9px 14px; border-bottom:1px solid var(--border); background:none; position:static; }
|
||||
#admin-command-center .acc-rtable th.r,#admin-command-center .acc-rtable td.r{ text-align:right; }
|
||||
#admin-command-center .acc-rtable td{ padding:10px 14px; font-size:13px; border-bottom:1px solid var(--border); }
|
||||
#admin-command-center .acc-rtable tr:last-child td{ border-bottom:none; }
|
||||
#admin-command-center .acc-rtable tbody tr{ transition:background .12s; }
|
||||
#admin-command-center .acc-rtable tbody tr:hover{ background:var(--surface-3); }
|
||||
#admin-command-center .acc-rt-user{ display:flex; align-items:center; gap:9px; font-weight:600; }
|
||||
#admin-command-center .acc-rt-av{ width:26px; height:26px; border-radius:50%; display:grid; place-items:center; color:#fff; font-size:10px; font-weight:700; flex:0 0 auto; }
|
||||
#admin-command-center .acc-rt-subj{ color:var(--tx2); font-size:12.5px; }
|
||||
#admin-command-center .acc-rt-score{ font-family:var(--mono); color:var(--tx3); font-size:12px; }
|
||||
#admin-command-center .acc-rt-pct{ font-family:var(--mono); font-weight:700; font-size:13px; }
|
||||
#admin-command-center .acc-rt-pct.hi{ color:var(--acc-green); } #admin-command-center .acc-rt-pct.mid{ color:var(--acc-amber); } #admin-command-center .acc-rt-pct.lo{ color:var(--acc-rose); }
|
||||
|
||||
/* quick */
|
||||
#admin-command-center .acc-quick{ display:grid; grid-template-columns:repeat(6,1fr); gap:12px; }
|
||||
#admin-command-center .acc-qbtn{ display:flex; flex-direction:column; gap:10px; padding:15px 14px; background:var(--surface);
|
||||
border:1px solid var(--border); border-radius:var(--r-lg); box-shadow:var(--acc-sh-xs); text-align:left; cursor:pointer;
|
||||
transition:transform .14s,box-shadow .14s,border-color .14s; font-family:var(--sans); }
|
||||
#admin-command-center .acc-qbtn:hover{ transform:translateY(-2px); box-shadow:var(--acc-sh); border-color:var(--acc-100); }
|
||||
#admin-command-center .acc-qbtn-ic{ width:34px; height:34px; border-radius:9px; display:grid; place-items:center; background:var(--acc-50); color:var(--acc); }
|
||||
#admin-command-center .acc-qbtn-ic .acc-ic{ stroke:currentColor; }
|
||||
#admin-command-center .acc-qbtn b{ font-size:13px; font-weight:700; letter-spacing:-.01em; }
|
||||
#admin-command-center .acc-qbtn span{ font-size:11px; color:var(--tx3); font-family:var(--mono); }
|
||||
|
||||
/* foot */
|
||||
#admin-command-center .acc-foot{ margin-top:26px; display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:10px;
|
||||
font-size:12px; color:var(--tx4); font-family:var(--mono); }
|
||||
|
||||
/* skeleton */
|
||||
@keyframes acc-shimmer{ 0%{ background-position:-400px 0 } 100%{ background-position:400px 0 } }
|
||||
#admin-command-center .acc-skel{ border-radius:var(--r-lg);
|
||||
background:linear-gradient(90deg,var(--border) 25%,var(--surface) 50%,var(--border) 75%); background-size:400px 100%;
|
||||
animation:acc-shimmer 1.4s infinite linear; }
|
||||
#admin-command-center .acc-skel-cards{ display:grid; grid-template-columns:1.4fr 1fr 1fr 1fr; gap:14px; margin-bottom:22px; }
|
||||
#admin-command-center .acc-skel-cards .acc-skel{ height:118px; }
|
||||
#admin-command-center .acc-skel-rows{ display:flex; flex-direction:column; gap:12px; }
|
||||
#admin-command-center .acc-skel.row{ height:60px; }
|
||||
|
||||
/* responsive */
|
||||
@media (max-width:1160px){
|
||||
#admin-command-center .acc-grid{ grid-template-columns:1fr; }
|
||||
#admin-command-center .acc-pulse{ grid-template-columns:1fr 1fr; }
|
||||
#admin-command-center .acc-kpi.hero{ grid-column:span 2; }
|
||||
#admin-command-center .acc-quick{ grid-template-columns:repeat(3,1fr); }
|
||||
#admin-command-center .acc-health{ grid-template-columns:1fr 1fr; }
|
||||
}
|
||||
@media (max-width:760px){
|
||||
#admin-command-center .acc-pulse{ grid-template-columns:1fr; }
|
||||
#admin-command-center .acc-kpi.hero{ grid-column:auto; }
|
||||
#admin-command-center .acc-results{ grid-template-columns:1fr; }
|
||||
#admin-command-center .acc-quick{ grid-template-columns:1fr 1fr; }
|
||||
#admin-command-center .acc-page-head h1{ font-size:25px; }
|
||||
#admin-command-center .acc-clock{ display:none; }
|
||||
}
|
||||
`;
|
||||
})();
|
||||
Reference in New Issue
Block a user