Files
WhatIDo/public/js/dashboard.js
T
dev a84f33307e feat(lessons): отчёты о занятиях — таблица, CRUD, страница и блок на дашборде
Добавлена сущность «что прошли на занятии»:
- lesson_reports (init.sql + migration.sql + ensureLessonReportsTable)
- GET/POST /api/lesson-reports, PUT/DELETE /api/lesson-reports/:id
  с branchScope, уникальностью (group_id, lesson_date) и лимитом текста
- уведомление lesson.report (NOTIFY_TYPES + настройка + иконка)
- кэш-префикс lessons: + инвалидация stats:/dashboard:
- восстановление lesson_reports в normalizeRestoreData
- блок recent_lessons в /api/dashboard

Фронтенд:
- public/lessons.html + public/js/lessons.js — список с фильтрами и правкой
- openLessonModal в admin.js — общая модалка из журнала и дашборда
- кнопки в журнале и быстрые действия дашборда

Также исправлен сдвиг индексов параметров в notificationsScope —
$1 уходил повторно в список филиалов из-за params.push без смещения.
2026-10-03 23:33:45 +03:00

154 lines
6.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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('');
}
function renderLessons(rows) {
const el = document.getElementById('lessonList');
if (!el) return;
if (!rows.length) { el.innerHTML = '<div class="dash-empty">Отчётов пока нет</div>'; return; }
el.innerHTML = rows.map(r => {
const date = String(r.lesson_date || '').slice(0, 10).split('-').reverse().join('.');
const time = r.lesson_time ? String(r.lesson_time).slice(0, 5) : '';
return `
<div class="lesson-dash-item" data-lesson="${r.id}">
<div class="lesson-dash-head">
<span class="lesson-dash-group">${esc(r.group_name)}</span>
<span class="lesson-dash-when">${esc(date + (time ? ' · ' + time : ''))}</span>
</div>
<div class="lesson-dash-text">${esc(r.text)}</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);
renderLessons(d.recent_lessons || []);
renderIcons();
}
document.getElementById('imgModal').addEventListener('click', e => e.currentTarget.classList.remove('open'));
const lessonBtn = document.getElementById('lessonReportBtn');
if (lessonBtn) lessonBtn.addEventListener('click', () => openLessonModal());
function onLessonReportSaved() {
loadDashboard();
}
document.addEventListener('click', e => {
const item = e.target.closest('[data-lesson]');
if (item && !e.target.closest('button')) openLessonModal({ id: item.dataset.lesson });
});
(async () => {
if (await checkAuth()) {
buildSidebar(document.body.dataset.page);
loadDashboard();
}
})();