feat(dashboard): teacher view polish — chips, bars, KPIs, groups, mobile
P0 visual polish: - adm-actions: grouped layout (teaching/content/admin) with 3-col grid at wide, responsive - thick 8px colored progress bars (green ≥75 / amber 50-74 / pink <50) - session % rendered as colored chip (tinted bg + border) - hover state on .adm-sess-row and .asgn-row in admin-grid - empty states with Lucide icon + CTA button (inbox/users/clock) - class-name badge on assignment row (disambiguates duplicates) - relative timestamp on session rows via relativeAgo() - search input above assignment list (filterAdminAssignments()) - adm-act-icon bumped 16px → 20px; card hover: scale + shadow P1 header KPIs + urgency: - dh-kpi-row: classes / students / active-asgn / pending chips under greeting - isTeacherUrgent(): assignments within 48h get pink border + срочно badge - adm-act-badge: count badge on Мои классы and Работы cards - loadTeacherKPIs() fetches /api/classes + teacherAssignments() in parallel P2 grouping + mobile + micro: - chevron-right icons on Все/Все классы section links - mobile ≤640: single-column groups, KPI chips wrap, sess-rows wrap - mobile ≤480: adm-act-group single column - dark mode rules for new elements Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+260
-43
@@ -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 @@
|
||||
<div class="dh-text">
|
||||
<div class="dh-greeting" id="dh-greeting">Привет, <span id="user-name">—</span></div>
|
||||
<div class="dh-sub" id="dh-sub">Выбери тест и начни</div>
|
||||
<div class="dh-kpi-row" id="dh-kpi-row" style="display:none">
|
||||
<span class="dh-kpi"><strong id="kpi-classes">…</strong> классов</span>
|
||||
<span class="dh-kpi"><strong id="kpi-students">…</strong> учеников</span>
|
||||
<span class="dh-kpi"><strong id="kpi-active-asgn">…</strong> активных заданий</span>
|
||||
<span class="dh-kpi warn" id="kpi-pending-wrap" style="display:none"><strong id="kpi-pending">…</strong> требуют внимания</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dh-stats" id="dh-stats" style="display:none">
|
||||
<div class="stat-ring" id="sr-sessions"></div>
|
||||
@@ -1291,26 +1371,32 @@
|
||||
<!-- ADMIN ZONE: Quick actions (hidden for students) -->
|
||||
<div id="admin-actions-zone" style="display:none;margin-bottom:18px">
|
||||
<div class="adm-actions" id="adm-actions-grid">
|
||||
<a class="adm-act" href="/classes">
|
||||
<div class="adm-act-icon" style="background:#9B5DE5"><i data-lucide="graduation-cap"></i></div>
|
||||
Мои классы
|
||||
</a>
|
||||
<a class="adm-act" href="/admin">
|
||||
<div class="adm-act-icon" style="background:#06B6D4"><i data-lucide="settings"></i></div>
|
||||
Управление
|
||||
</a>
|
||||
<a class="adm-act" href="/board">
|
||||
<div class="adm-act-icon" style="background:#06D6A0"><i data-lucide="layout-dashboard"></i></div>
|
||||
Доска
|
||||
</a>
|
||||
<a class="adm-act" href="/library">
|
||||
<div class="adm-act-icon" style="background:#F59E0B"><i data-lucide="book-open"></i></div>
|
||||
Библиотека
|
||||
</a>
|
||||
<a class="adm-act" href="/homework">
|
||||
<div class="adm-act-icon" style="background:#F15BB5"><i data-lucide="file-check"></i></div>
|
||||
Работы
|
||||
</a>
|
||||
<div class="adm-act-group">
|
||||
<a class="adm-act" href="/classes">
|
||||
<div class="adm-act-icon" style="background:#9B5DE5"><i data-lucide="graduation-cap"></i></div>
|
||||
Мои классы
|
||||
</a>
|
||||
<a class="adm-act" href="/homework">
|
||||
<div class="adm-act-icon" style="background:#F15BB5"><i data-lucide="file-check"></i></div>
|
||||
Работы
|
||||
</a>
|
||||
</div>
|
||||
<div class="adm-act-group">
|
||||
<a class="adm-act" href="/board">
|
||||
<div class="adm-act-icon" style="background:#06D6A0"><i data-lucide="layout-dashboard"></i></div>
|
||||
Доска
|
||||
</a>
|
||||
<a class="adm-act" href="/library">
|
||||
<div class="adm-act-icon" style="background:#F59E0B"><i data-lucide="book-open"></i></div>
|
||||
Библиотека
|
||||
</a>
|
||||
</div>
|
||||
<div class="adm-act-group">
|
||||
<a class="adm-act" href="/admin">
|
||||
<div class="adm-act-icon" style="background:#06B6D4"><i data-lucide="settings"></i></div>
|
||||
Управление
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1320,7 +1406,7 @@
|
||||
<div class="widget" id="w-admin-assignments">
|
||||
<div class="w-head">
|
||||
<div class="w-title">Задания</div>
|
||||
<a class="w-more" href="/classes">Все классы</a>
|
||||
<a class="w-more" href="/classes">Все классы <i data-lucide="chevron-right" style="width:13px;height:13px;vertical-align:-2px"></i></a>
|
||||
</div>
|
||||
<div id="admin-assignments-list"><div id="admin-assignments-sk"></div></div>
|
||||
</div>
|
||||
@@ -1328,7 +1414,7 @@
|
||||
<div class="widget" id="w-admin-classes">
|
||||
<div class="w-head">
|
||||
<div class="w-title">Классы</div>
|
||||
<a class="w-more" href="/classes">Все</a>
|
||||
<a class="w-more" href="/classes">Все <i data-lucide="chevron-right" style="width:13px;height:13px;vertical-align:-2px"></i></a>
|
||||
</div>
|
||||
<div class="class-summary" id="admin-classes-body"></div>
|
||||
</div>
|
||||
@@ -1336,7 +1422,7 @@
|
||||
<div class="widget" id="w-admin-sessions">
|
||||
<div class="w-head">
|
||||
<div class="w-title">Последние сессии</div>
|
||||
<a class="w-more" href="/admin">Все</a>
|
||||
<a class="w-more" href="/admin">Все <i data-lucide="chevron-right" style="width:13px;height:13px;vertical-align:-2px"></i></a>
|
||||
</div>
|
||||
<div class="adm-sessions" id="admin-sessions-body"></div>
|
||||
</div>
|
||||
@@ -1987,6 +2073,29 @@
|
||||
return 1e12; // no deadline <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> 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 `<div class="asgn-wrap"><div class="asgn-row stagger-item" style="--i:${idx};--ac:${sColor}" id="asgn-row-${a.id}" onclick="toggleAsgn(${a.id},event)">
|
||||
const barColor = pct >= 75 ? '#059652' : pct >= 50 ? '#F59E0B' : '#F15BB5';
|
||||
const urgent = isTeacherUrgent(a);
|
||||
const titleHtml = esc(a.title)
|
||||
+ (a.class_name && a.class_name !== 'Личное задание' ? `<span class="asgn-class-badge">${esc(a.class_name)}</span>` : '')
|
||||
+ (urgent ? `<span class="asgn-fire">срочно</span>` : '');
|
||||
const meta = [SUBJ[a.subject_slug] || a.subject_slug, dl ? `до ${dl}` : null].filter(Boolean).join(' · ');
|
||||
const urgentCls = urgent ? ' asgn-urgent' : '';
|
||||
return `<div class="asgn-wrap"><div class="asgn-row stagger-item${urgentCls}" style="--i:${idx};--ac:${sColor}" id="asgn-row-${a.id}" onclick="toggleAsgn(${a.id},event)">
|
||||
<div class="ar-icon" style="background:${sColor}18;color:${sColor}">${lci(iconName)}</div>
|
||||
<div class="ar-body"><div class="ar-title">${esc(a.title)}</div><div class="ar-meta">${meta}</div></div>
|
||||
<div class="ar-body"><div class="ar-title">${titleHtml}</div><div class="ar-meta">${meta}</div></div>
|
||||
<div class="ar-progress">
|
||||
<div class="ar-prog-bar"><div class="ar-prog-fill" style="width:${pct}%;background:${sColor}"></div></div>
|
||||
<div class="ar-prog-bar"><div class="ar-prog-fill" style="width:${pct}%;background:${barColor}"></div></div>
|
||||
<span class="ar-prog-text">${done} / ${total}</span>
|
||||
</div>
|
||||
<a class="ar-btn-ghost" href="/classes" onclick="event.stopPropagation()">Подробнее</a>
|
||||
@@ -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 = '<div style="font-size:0.8rem;color:var(--text-3);padding:8px 0">Нет классов</div>';
|
||||
body.innerHTML = `<div class="adm-empty">
|
||||
<div class="adm-empty-icon"><i data-lucide="users" style="width:32px;height:32px;color:var(--text-3)"></i></div>
|
||||
<div class="adm-empty-text">Классов пока нет</div>
|
||||
<a class="adm-empty-cta" href="/classes">+ Создать класс</a>
|
||||
</div>`;
|
||||
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 = '<div style="font-size:0.8rem;color:var(--text-3);padding:8px 0">Нет сессий</div>';
|
||||
body.innerHTML = `<div class="adm-empty">
|
||||
<div class="adm-empty-icon"><i data-lucide="clock" style="width:32px;height:32px;color:var(--text-3)"></i></div>
|
||||
<div class="adm-empty-text">Сессий пока нет. Ученики не начинали тесты.</div>
|
||||
</div>`;
|
||||
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 `<div class="adm-sess-row">
|
||||
const ago = relativeAgo(s.started_at);
|
||||
if (s.status !== 'completed') {
|
||||
const chip = `<span class="adm-sess-chip" style="background:rgba(15,23,42,0.06);color:var(--text-3);border:1px solid rgba(15,23,42,0.10)">…</span>`;
|
||||
return `<div class="adm-sess-row" onclick="window.location='/admin#sessions'" style="cursor:pointer">
|
||||
<span class="adm-sess-name">${esc(s.user_name || s.email || '—')}</span>
|
||||
<span class="adm-sess-subj">${esc(s.subject_name || '')}</span>
|
||||
${ago ? `<span class="adm-sess-ago">${ago}</span>` : ''}
|
||||
${chip}
|
||||
</div>`;
|
||||
}
|
||||
const chip = `<span class="adm-sess-chip" style="background:${pc}22;color:${pc};border:1px solid ${pc}44">${pct}%</span>`;
|
||||
return `<div class="adm-sess-row" onclick="window.location='/admin#sessions'" style="cursor:pointer">
|
||||
<span class="adm-sess-name">${esc(s.user_name || s.email || '—')}</span>
|
||||
<span class="adm-sess-subj">${esc(s.subject_name || '')}</span>
|
||||
<span class="adm-sess-pct" style="color:${pc}">${s.status === 'completed' ? pct + '%' : '…'}</span>
|
||||
${ago ? `<span class="adm-sess-ago">${ago}</span>` : ''}
|
||||
${chip}
|
||||
</div>`;
|
||||
}).join('');
|
||||
} catch {
|
||||
@@ -3568,11 +3758,18 @@
|
||||
try {
|
||||
const list = await LS.teacherAssignments();
|
||||
if (!list.length) {
|
||||
listEl.innerHTML = '<div style="font-size:0.8rem;color:var(--text-3);padding:8px 0">Заданий пока нет</div>';
|
||||
listEl.innerHTML = `<div class="adm-empty">
|
||||
<div class="adm-empty-icon"><i data-lucide="inbox" style="width:32px;height:32px;color:var(--text-3)"></i></div>
|
||||
<div class="adm-empty-text">Заданий пока нет</div>
|
||||
<a class="adm-empty-cta" href="/classes">+ Создать первое задание</a>
|
||||
</div>`;
|
||||
reIcons();
|
||||
return;
|
||||
}
|
||||
const sorted = [...list].sort((a, b) => urgencyScore(a) - urgencyScore(b));
|
||||
listEl.innerHTML = `<div class="tests-list">${sorted.slice(0, 8).map((a, i) => buildAssignCard(a, i)).join('')}</div>`;
|
||||
// Save for search filtering
|
||||
window._adminAssignmentsSorted = sorted;
|
||||
listEl.innerHTML = `<input class="adm-asgn-search" id="adm-asgn-search" type="search" placeholder="Поиск задания..." oninput="filterAdminAssignments(this.value)"><div class="tests-list" id="admin-assignments-inner">${sorted.slice(0, 8).map((a, i) => buildAssignCard(a, i)).join('')}</div>`;
|
||||
if (list.length > 8) {
|
||||
listEl.innerHTML += `<div style="text-align:center;margin-top:8px"><a class="w-more" href="/classes">Ещё ${list.length - 8} заданий</a></div>`;
|
||||
}
|
||||
@@ -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('')
|
||||
: '<div class="adm-empty"><div class="adm-empty-text">Ничего не найдено</div></div>';
|
||||
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();
|
||||
|
||||
Reference in New Issue
Block a user