diff --git a/frontend/dashboard.html b/frontend/dashboard.html
index 0e81729..c90ac63 100644
--- a/frontend/dashboard.html
+++ b/frontend/dashboard.html
@@ -418,22 +418,31 @@
.admin-grid .widget { padding: 18px; border-radius: 16px; }
.admin-grid .w-head { margin-bottom: 10px; }
- /* Quick-action buttons grid */
- .adm-actions { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; }
+ /* Quick-action buttons grid — grouped layout */
+ .adm-actions { display: grid; grid-template-columns: 2fr 2fr 1fr; gap: 16px; }
+ .adm-act-group { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 10px; }
+ @media (max-width: 900px) { .adm-actions { grid-template-columns: 1fr 1fr; } }
+ @media (max-width: 640px) { .adm-actions { grid-template-columns: 1fr; } }
.adm-act {
display: flex; align-items: center; gap: 10px;
padding: 12px 14px; border-radius: 12px;
border: 1.5px solid rgba(15,23,42,0.07); background: #fff;
font-family: 'Manrope', sans-serif; font-size: 0.8rem; font-weight: 700;
- color: #0F172A; cursor: pointer; transition: all 0.15s;
- text-decoration: none;
+ color: #0F172A; cursor: pointer; transition: all 0.18s;
+ text-decoration: none; position: relative;
}
- .adm-act:hover { border-color: rgba(155,93,229,0.25); transform: translateY(-1px); box-shadow: 0 4px 16px rgba(15,23,42,0.08); }
+ .adm-act:hover { border-color: rgba(155,93,229,0.25); transform: translateY(-2px) scale(1.01); box-shadow: 0 6px 20px rgba(15,23,42,0.10); }
.adm-act-icon {
- width: 32px; height: 32px; border-radius: 8px;
+ width: 36px; height: 36px; border-radius: 9px;
display: flex; align-items: center; justify-content: center; flex-shrink: 0;
}
- .adm-act-icon svg, .adm-act-icon i { width: 16px; height: 16px; stroke: #fff; stroke-width: 2; }
+ .adm-act-icon svg, .adm-act-icon i { width: 20px; height: 20px; stroke: #fff; stroke-width: 2; }
+ .adm-act-badge {
+ position: absolute; top: 6px; right: 8px;
+ background: #E0335E; color: #fff; font-size: 0.62rem; font-weight: 800;
+ padding: 1px 5px; border-radius: 999px; font-family: 'Unbounded', sans-serif;
+ line-height: 1.4;
+ }
/* Compact stat chips row for admin header */
.adm-stat-chips { display: flex; gap: 10px; flex-shrink: 0; flex-wrap: wrap; }
@@ -468,11 +477,67 @@
.adm-sess-name { flex: 1; font-weight: 600; color: #0F172A; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.adm-sess-subj { color: var(--text-3); font-weight: 600; min-width: 60px; }
.adm-sess-pct { font-family: 'Unbounded', sans-serif; font-weight: 900; font-size: 0.74rem; min-width: 36px; text-align: right; }
+ /* Colored chip for session % */
+ .adm-sess-chip {
+ display: inline-flex; padding: 3px 9px; border-radius: 999px;
+ font-size: 0.74rem; font-weight: 700; min-width: 42px; justify-content: center;
+ font-family: 'Unbounded', sans-serif;
+ }
+ .adm-sess-ago { color: var(--text-3); font-size: 0.7rem; flex-shrink: 0; }
+ /* Hover on rows in admin-grid */
+ .admin-grid .adm-sess-row:hover,
+ .admin-grid .asgn-row:hover {
+ background: rgba(155,93,229,0.04);
+ cursor: pointer;
+ }
+ /* Thick colored progress bar */
+ .admin-grid .ar-prog-bar { height: 8px !important; border-radius: 4px; overflow: hidden; background: rgba(15,23,42,0.06); }
+ .admin-grid .ar-prog-fill { height: 100%; border-radius: 4px; }
+ /* Empty states */
+ .adm-empty { display: flex; flex-direction: column; align-items: center; gap: 10px; padding: 24px 12px; text-align: center; color: var(--text-3); }
+ .adm-empty i { display: block; }
+ .adm-empty-text { font-size: 0.82rem; }
+ .adm-empty-cta {
+ display: inline-flex; align-items: center; gap: 6px;
+ padding: 7px 16px; border-radius: 9px; font-size: 0.78rem; font-weight: 700;
+ background: var(--violet); color: #fff; text-decoration: none;
+ transition: opacity 0.15s;
+ }
+ .adm-empty-cta:hover { opacity: 0.85; }
+ /* Class badge on assignment row */
+ .asgn-class-badge {
+ display: inline-flex; padding: 1px 6px; border-radius: 6px;
+ font-size: 0.68rem; font-weight: 700;
+ background: rgba(155,93,229,0.08); color: #7c3aed;
+ margin-left: 4px; vertical-align: middle;
+ }
+ /* Urgency highlight */
+ .admin-grid .asgn-urgent { border-left: 3px solid var(--pink, #F15BB5); padding-left: 9px !important; }
+ .asgn-fire {
+ background: rgba(241,91,181,0.12); color: var(--pink, #F15BB5);
+ padding: 1px 7px; border-radius: 999px; font-size: 0.68rem; font-weight: 700; margin-left: 6px;
+ }
+ /* KPI row */
+ .dh-kpi-row { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 6px; }
+ .dh-kpi { font-size: 0.84rem; color: var(--text-3); }
+ .dh-kpi strong { color: var(--text); font-weight: 700; }
+ .dh-kpi.warn strong { color: var(--amber, #F59E0B); }
+ /* Admin assignment search */
+ .adm-asgn-search {
+ width: 100%; padding: 7px 10px; border: 1px solid var(--border);
+ border-radius: 8px; font: inherit; font-size: 0.82rem;
+ margin-bottom: 10px; background: var(--bg, #fff); color: var(--text);
+ box-sizing: border-box;
+ }
+ .adm-asgn-search:focus { outline: none; border-color: var(--violet); }
/* Dark mode for admin */
.app-layout.dark .adm-act { background: #1A1D27; border-color: rgba(255,255,255,0.06); color: #E8ECF2; }
.app-layout.dark .adm-stat-chip { background: rgba(255,255,255,0.04); border-color: rgba(255,255,255,0.06); }
.app-layout.dark .adm-sess-name { color: #E8ECF2; }
+ .app-layout.dark .adm-asgn-search { background: #1A1D27; color: #E8ECF2; border-color: rgba(255,255,255,0.1); }
+ .app-layout.dark .adm-empty-text { color: var(--text-3); }
+ .app-layout.dark .asgn-class-badge { background: rgba(155,93,229,0.15); color: #b07de0; }
/* C1: Teacher class summary widget */
.class-summary { display: flex; flex-direction: column; gap: 12px; }
@@ -1079,7 +1144,15 @@
.main-grid { grid-template-columns: 1fr; }
.action-cards { grid-template-columns: 1fr; }
.admin-grid { grid-template-columns: 1fr; gap: 14px; }
- .adm-actions { grid-template-columns: repeat(2, 1fr); }
+ .adm-actions { grid-template-columns: 1fr; gap: 10px; }
+ .adm-act-group { grid-template-columns: 1fr 1fr; }
+ /* KPI chips wrap to 2 cols */
+ .dh-kpi-row { gap: 8px; }
+ /* Session rows: wrap on small screens */
+ .adm-sess-row { flex-wrap: wrap; }
+ .adm-sess-subj { min-width: unset; }
+ /* Assignment title: allow wrapping */
+ .admin-grid .ar-title { white-space: normal; }
.theory-courses-grid { grid-template-columns: 1fr; }
.subj-charts-grid { grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); }
@@ -1152,8 +1225,9 @@
.stat-ring { min-width: 60px; }
.dh-greeting { font-size: 0.82rem; }
- /* Admin quick actions: stay 2 columns (narrow enough) */
+ /* Admin quick actions: narrow */
.adm-actions { gap: 8px; }
+ .adm-act-group { grid-template-columns: 1fr; }
.adm-act { padding: 10px 10px; font-size: 0.76rem; gap: 8px; }
/* Assignment rows tighter */
@@ -1213,6 +1287,12 @@
Привет, —
Выбери тест и начни
+
+ … классов
+ … учеников
+ … активных заданий
+ … требуют внимания
+
@@ -1291,26 +1371,32 @@
@@ -1320,7 +1406,7 @@
@@ -1328,7 +1414,7 @@
@@ -1336,7 +1422,7 @@
@@ -1987,6 +2073,29 @@
return 1e12; // no deadline
last
}
+ /* ── Is assignment urgent for teacher (within 48h) ── */
+ function isTeacherUrgent(a) {
+ if (!a.deadline) return false;
+ const ms = parseDate(a.deadline).getTime() - Date.now();
+ return ms > 0 && ms < 48 * 3600 * 1000;
+ }
+
+ /* ── Relative timestamp helper ("12 мин назад", "сегодня", "вчера") ── */
+ function relativeAgo(dateStr) {
+ if (!dateStr) return '';
+ const d = parseDate(dateStr);
+ const diffMs = Date.now() - d.getTime();
+ const diffMin = Math.floor(diffMs / 60000);
+ if (diffMin < 1) return 'только что';
+ if (diffMin < 60) return `${diffMin} мин назад`;
+ const diffH = Math.floor(diffMin / 60);
+ if (diffH < 24) return `${diffH} ч назад`;
+ const diffD = Math.floor(diffH / 24);
+ if (diffD === 1) return 'вчера';
+ if (diffD < 7) return `${diffD} дн назад`;
+ return d.toLocaleDateString('ru', { day: 'numeric', month: 'short' });
+ }
+
function buildAssignCard(a, idx, isFirst = false) {
const dl = a.deadline ? new Date(a.deadline).toLocaleDateString('ru',{day:'numeric',month:'short'}) : null;
const dlMs = a.deadline ? new Date(a.deadline) - Date.now() : Infinity;
@@ -2001,12 +2110,18 @@
const done = a.completed_count || 0;
const total = a.total_members || 0;
const pct = total > 0 ? Math.round((done / total) * 100) : 0;
- const meta = [classStr, SUBJ[a.subject_slug] || a.subject_slug, dl ? `до ${dl}` : null].filter(Boolean).join(' · ');
- return `
+ const barColor = pct >= 75 ? '#059652' : pct >= 50 ? '#F59E0B' : '#F15BB5';
+ const urgent = isTeacherUrgent(a);
+ const titleHtml = esc(a.title)
+ + (a.class_name && a.class_name !== 'Личное задание' ? `
${esc(a.class_name)}` : '')
+ + (urgent ? `
срочно` : '');
+ const meta = [SUBJ[a.subject_slug] || a.subject_slug, dl ? `до ${dl}` : null].filter(Boolean).join(' · ');
+ const urgentCls = urgent ? ' asgn-urgent' : '';
+ return `
${lci(iconName)}
-
+
Подробнее
@@ -3500,6 +3615,61 @@
} catch { /* silent */ }
}
+ /* ══ ADMIN: KPI chips in header ═════════════════════════════════════ */
+ async function loadTeacherKPIs() {
+ if (!isTeacher) return;
+ const kpiRow = document.getElementById('dh-kpi-row');
+ if (!kpiRow) return;
+ try {
+ const [classes, assignments] = await Promise.all([
+ LS.api('/api/classes'),
+ LS.teacherAssignments(),
+ ]);
+ const classCount = (classes || []).length;
+ const studentCount = (classes || []).reduce((s, c) => s + (c.member_count || 0), 0);
+ const now = Date.now();
+ const activeAsgn = (assignments || []).filter(a =>
+ !a.deadline || parseDate(a.deadline).getTime() >= now
+ ).length;
+ // Pending: overdue with submissions missing
+ const pending = (assignments || []).filter(a =>
+ a.deadline && parseDate(a.deadline).getTime() < now &&
+ (a.completed_count || 0) < (a.total_members || 0)
+ ).length;
+
+ document.getElementById('kpi-classes').textContent = classCount;
+ document.getElementById('kpi-students').textContent = studentCount;
+ document.getElementById('kpi-active-asgn').textContent = activeAsgn;
+ kpiRow.style.display = '';
+
+ if (pending > 0) {
+ document.getElementById('kpi-pending').textContent = pending;
+ document.getElementById('kpi-pending-wrap').style.display = '';
+ }
+
+ // Add badge to «Мои классы» card
+ if (classCount > 0) {
+ const classCard = document.querySelector('.adm-act[href="/classes"]');
+ if (classCard && !classCard.querySelector('.adm-act-badge')) {
+ const badge = document.createElement('span');
+ badge.className = 'adm-act-badge';
+ badge.textContent = classCount;
+ classCard.appendChild(badge);
+ }
+ }
+ // Add badge to «Работы» card if pending
+ if (pending > 0) {
+ const workCard = document.querySelector('.adm-act[href="/homework"]');
+ if (workCard && !workCard.querySelector('.adm-act-badge')) {
+ const badge = document.createElement('span');
+ badge.className = 'adm-act-badge';
+ badge.textContent = pending;
+ workCard.appendChild(badge);
+ }
+ }
+ } catch { /* silent */ }
+ }
+
/* ══ ADMIN: Load classes into admin panel ═══════════════════════════ */
async function loadAdminClasses() {
if (!isTeacher) return;
@@ -3508,7 +3678,12 @@
try {
const classes = await LS.api('/api/classes');
if (!classes || !classes.length) {
- body.innerHTML = '
Нет классов
';
+ body.innerHTML = `
`;
+ reIcons();
return;
}
const colors = ['#9B5DE5','#06D6A0','#F59E0B','#06B6D4','#E0335E'];
@@ -3542,17 +3717,32 @@
const data = await LS.api('/api/admin/sessions?limit=8');
const rows = Array.isArray(data) ? data : (data.rows || []);
if (!rows.length) {
- body.innerHTML = '
Нет сессий
';
+ body.innerHTML = `
+
+
Сессий пока нет. Ученики не начинали тесты.
+
`;
+ reIcons();
return;
}
body.innerHTML = rows.slice(0, 8).map(s => {
const pct = s.total > 0 ? Math.round(s.score / s.total * 100) : 0;
const pc = pct >= 75 ? '#059652' : pct >= 50 ? '#F59E0B' : '#E0335E';
- const dt = s.started_at ? parseDate(s.started_at).toLocaleDateString('ru', {day:'numeric',month:'short'}) : '';
- return `
+ const ago = relativeAgo(s.started_at);
+ if (s.status !== 'completed') {
+ const chip = `
…`;
+ return `
+ ${esc(s.user_name || s.email || '—')}
+ ${esc(s.subject_name || '')}
+ ${ago ? `${ago}` : ''}
+ ${chip}
+
`;
+ }
+ const chip = `
${pct}%`;
+ return `
${esc(s.user_name || s.email || '—')}
${esc(s.subject_name || '')}
- ${s.status === 'completed' ? pct + '%' : '…'}
+ ${ago ? `${ago}` : ''}
+ ${chip}
`;
}).join('');
} catch {
@@ -3568,11 +3758,18 @@
try {
const list = await LS.teacherAssignments();
if (!list.length) {
- listEl.innerHTML = '
Заданий пока нет
';
+ listEl.innerHTML = `
`;
+ reIcons();
return;
}
const sorted = [...list].sort((a, b) => urgencyScore(a) - urgencyScore(b));
- listEl.innerHTML = `
${sorted.slice(0, 8).map((a, i) => buildAssignCard(a, i)).join('')}
`;
+ // Save for search filtering
+ window._adminAssignmentsSorted = sorted;
+ listEl.innerHTML = `
${sorted.slice(0, 8).map((a, i) => buildAssignCard(a, i)).join('')}
`;
if (list.length > 8) {
listEl.innerHTML += `
`;
}
@@ -3582,6 +3779,25 @@
}
}
+ /* ── Filter admin assignments by search query ── */
+ function filterAdminAssignments(q) {
+ const innerEl = document.getElementById('admin-assignments-inner');
+ if (!innerEl || !window._adminAssignmentsSorted) return;
+ const ql = q.toLowerCase().trim();
+ const filtered = ql
+ ? window._adminAssignmentsSorted.filter(a =>
+ (a.title || '').toLowerCase().includes(ql) ||
+ (a.class_name || '').toLowerCase().includes(ql) ||
+ (SUBJ[a.subject_slug] || a.subject_slug || '').toLowerCase().includes(ql)
+ )
+ : window._adminAssignmentsSorted;
+ const show = filtered.slice(0, 8);
+ innerEl.innerHTML = show.length
+ ? show.map((a, i) => buildAssignCard(a, i)).join('')
+ : '
';
+ reIcons();
+ }
+
/* ══ Load all student widgets ═════════════════════════════════════ */
/* Show a widget section, but respect the cfg-hidden flag */
function showWidget(id) {
@@ -3776,6 +3992,7 @@
if (isTeacher) {
// Admin/Teacher: compact admin layout
loadAdminStats();
+ loadTeacherKPIs();
loadAdminAssignments();
loadAdminClasses();
loadAdminSessions();