123 lines
5.2 KiB
JavaScript
123 lines
5.2 KiB
JavaScript
function plural(n, one, few, many) {
|
||
n = Math.abs(n) % 100; const n1 = n % 10;
|
||
if (n > 10 && n < 20) return many;
|
||
if (n1 > 1 && n1 < 5) return few;
|
||
if (n1 === 1) return one;
|
||
return many;
|
||
}
|
||
|
||
function renderStats(s, disk) {
|
||
let diskHtml = '';
|
||
if (disk) {
|
||
const pct = disk.used_pct;
|
||
const color = pct > 90 ? '#ef4444' : pct > 70 ? '#f59e0b' : '#16a34a';
|
||
diskHtml = `
|
||
<div class="stat">
|
||
<div>${disk.used} <span class="stat-sub">/ ${disk.total}</span></div>
|
||
<div class="label">Диск: свободно ${disk.free} (${100 - pct}%)</div>
|
||
<div class="stat-bar"><i style="width:${pct}%;background:${color}"></i></div>
|
||
</div>`;
|
||
}
|
||
document.getElementById('stats').innerHTML = `
|
||
<div class="stat"><div class="num">${s.entries}</div><div class="label">Всего записей</div></div>
|
||
<div class="stat"><div class="num">${s.students}</div><div class="label">Учеников</div></div>
|
||
<div class="stat"><div class="num">${s.groups}</div><div class="label">Групп</div></div>
|
||
<div class="stat"><div class="num">${s.today}</div><div class="label">Сегодня</div></div>
|
||
<div class="stat"><div class="num" style="color:${s.trash ? '#ef4444' : 'var(--muted)'}">${s.trash}</div><div class="label"><a href="trash.html">В корзине →</a></div></div>
|
||
${diskHtml}
|
||
`;
|
||
}
|
||
|
||
function last14days() {
|
||
const out = [];
|
||
for (let i = 13; i >= 0; i--) { const d = new Date(); d.setDate(d.getDate() - i); out.push(d); }
|
||
return out;
|
||
}
|
||
|
||
function renderActivity(activity) {
|
||
const map = {};
|
||
activity.forEach(a => { map[a.d] = a.n; });
|
||
const days = last14days().map(d => {
|
||
const iso = d.toISOString().slice(0, 10);
|
||
return { iso, label: d.toLocaleDateString('ru', { day: '2-digit', month: '2-digit' }), n: map[iso] || 0 };
|
||
});
|
||
const max = Math.max(1, ...days.map(x => x.n));
|
||
const todayIso = new Date().toISOString().slice(0, 10);
|
||
document.getElementById('activityChart').innerHTML = days.map(x => `
|
||
<div class="bar-col" title="${x.iso} — ${x.n} ${plural(x.n, 'запись', 'записи', 'записей')}">
|
||
<div class="bar ${x.iso === todayIso ? 'today' : ''}" style="height:${Math.round(x.n / max * 100)}%"></div>
|
||
<div class="bar-day">${x.label}</div>
|
||
</div>`).join('');
|
||
}
|
||
|
||
function renderRecent(rows) {
|
||
const el = document.getElementById('recentList');
|
||
if (!rows.length) { el.innerHTML = '<div class="dash-empty">Записей пока нет</div>'; return; }
|
||
el.innerHTML = rows.map(e => `
|
||
<div class="recent-item">
|
||
${e.photo_path
|
||
? `<img src="${thumbSrc(e.photo_path)}" data-img="${API}${e.photo_path}" loading="lazy" decoding="async" alt="">`
|
||
: `<div class="thumb"><i data-lucide="graduation-cap"></i></div>`}
|
||
<div class="r-info">
|
||
<div class="r-name">${esc(e.student_name)}</div>
|
||
<div class="r-meta">${esc(e.group_name)} · ${new Date(e.created_at).toLocaleString('ru', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' })}</div>
|
||
</div>
|
||
<a class="r-link" href="journal.html" title="В журнал">→</a>
|
||
</div>`).join('');
|
||
}
|
||
|
||
function renderActive(rows) {
|
||
const el = document.getElementById('activeList');
|
||
if (!rows.length) { el.innerHTML = '<div class="dash-empty">Сейчас нет идущих групп</div>'; return; }
|
||
el.innerHTML = rows.map(g => `
|
||
<div class="active-item">
|
||
<span class="a-name">${esc(g.name)}</span>
|
||
<span class="a-right">
|
||
<span class="a-time">${g.day_label} ${g.time_start}–${g.time_end}</span>
|
||
<span class="a-now">сейчас</span>
|
||
</span>
|
||
</div>`).join('');
|
||
}
|
||
|
||
function renderTop(rows) {
|
||
const el = document.getElementById('topList');
|
||
if (!rows.length) { el.innerHTML = '<div class="dash-empty">Пока нет данных</div>'; return; }
|
||
el.innerHTML = rows.map((x, i) => `
|
||
<div class="top-item">
|
||
<span class="top-rank ${i > 2 ? 'alt' : ''}">${i + 1}</span>
|
||
<span class="top-name">${esc(x.student_name)}</span>
|
||
<span class="top-count">${x.n}</span>
|
||
</div>`).join('');
|
||
}
|
||
|
||
function renderPhotos(rows) {
|
||
const el = document.getElementById('dashPhotos');
|
||
if (!rows.length) { el.innerHTML = '<div class="dash-empty">Фото групп пока нет</div>'; return; }
|
||
el.innerHTML = rows.map(p => `
|
||
<div class="dash-photo" title="${esc(p.group_name)}">
|
||
<img src="${thumbSrc(p.photo_path)}" data-img="${API}${p.photo_path}" loading="lazy" decoding="async" alt="">
|
||
${p.caption ? `<div class="pcap">${esc(p.caption)}</div>` : ''}
|
||
</div>`).join('');
|
||
}
|
||
|
||
async function loadDashboard() {
|
||
const res = await fetch(`${API}/api/dashboard`, { headers: hdr() });
|
||
if (!res.ok) { document.getElementById('stats').innerHTML = '<div class="dash-empty">Не удалось загрузить данные</div>'; return; }
|
||
const d = await res.json();
|
||
renderStats(d.stats, d.disk);
|
||
renderActivity(d.activity);
|
||
renderRecent(d.recent_entries);
|
||
renderActive(d.active_groups);
|
||
renderTop(d.top_students);
|
||
renderPhotos(d.photos);
|
||
renderIcons();
|
||
}
|
||
|
||
document.getElementById('imgModal').addEventListener('click', e => e.currentTarget.classList.remove('open'));
|
||
|
||
(async () => {
|
||
if (await checkAuth()) {
|
||
buildSidebar(document.body.dataset.page);
|
||
loadDashboard();
|
||
}
|
||
})(); |