Добавлена сущность «что прошли на занятии»: - 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 без смещения.
139 lines
5.3 KiB
JavaScript
139 lines
5.3 KiB
JavaScript
const PAGE_SIZE = 20;
|
|
let page = 1;
|
|
let total = 0;
|
|
let searchTimer = null;
|
|
let groupsLoaded = false;
|
|
|
|
function lessonDateText(iso) {
|
|
const s = String(iso || '').slice(0, 10);
|
|
if (!s) return '';
|
|
const d = new Date(s + 'T00:00:00');
|
|
if (Number.isNaN(d.getTime())) return s;
|
|
return d.toLocaleDateString('ru-RU', { day: '2-digit', month: '2-digit', year: '2-digit' });
|
|
}
|
|
|
|
function lessonWhenText(r) {
|
|
const date = lessonDateText(r.lesson_date);
|
|
const time = r.lesson_time ? String(r.lesson_time).slice(0, 5) : '';
|
|
return time ? `${date}, ${time}` : date;
|
|
}
|
|
|
|
function lessonAuthorText(r) {
|
|
const name = r.author_name || r.author_username;
|
|
if (!name) return '';
|
|
const upd = r.updated_at && r.created_at && new Date(r.updated_at).getTime() - new Date(r.created_at).getTime() > 60000;
|
|
return upd ? `${name} · изменён ${notifyTime(r.updated_at)}` : name;
|
|
}
|
|
|
|
function lessonItemHtml(r) {
|
|
const isToday = String(r.lesson_date).slice(0, 10) === lessonTodayIso();
|
|
const author = lessonAuthorText(r);
|
|
return `
|
|
<div class="lesson-item" data-lesson="${r.id}">
|
|
<div class="lesson-item-head">
|
|
<span class="lesson-group">${esc(r.group_name || '')}</span>
|
|
<span class="lesson-when"><i data-lucide="clock-3"></i>${esc(lessonWhenText(r))}</span>
|
|
${isToday ? '<span class="lesson-badge today">сегодня</span>' : ''}
|
|
<div class="lesson-actions">
|
|
<button type="button" class="btn-link" data-edit="${r.id}">Редактировать</button>
|
|
${isAdmin() ? `<button type="button" class="btn-link danger" data-del="${r.id}">Удалить</button>` : ''}
|
|
</div>
|
|
</div>
|
|
<div class="lesson-text">${esc(r.text || '')}</div>
|
|
${author ? `<div class="lesson-author">${esc(author)}</div>` : ''}
|
|
</div>`;
|
|
}
|
|
|
|
function renderLessons(items) {
|
|
const list = document.getElementById('lessonsList');
|
|
const state = document.getElementById('lessonsState');
|
|
list.innerHTML = items.length
|
|
? items.map(lessonItemHtml).join('')
|
|
: '<div class="empty">Отчётов пока нет</div>';
|
|
if (state) {
|
|
state.textContent = total ? `Показано ${items.length} из ${total}` : '';
|
|
}
|
|
renderIcons();
|
|
}
|
|
|
|
async function loadLessons() {
|
|
const q = new URLSearchParams();
|
|
const gid = document.getElementById('groupFilter').value;
|
|
const df = document.getElementById('dateFrom').value;
|
|
const dt = document.getElementById('dateTo').value;
|
|
const search = document.getElementById('searchInput').value.trim();
|
|
if (gid) q.set('group_id', gid);
|
|
if (df) q.set('date_from', df);
|
|
if (dt) q.set('date_to', dt);
|
|
if (search) q.set('search', search);
|
|
q.set('limit', String(PAGE_SIZE));
|
|
q.set('offset', String((page - 1) * PAGE_SIZE));
|
|
const list = document.getElementById('lessonsList');
|
|
try {
|
|
const res = await fetch(`${API}/api/lesson-reports?${q.toString()}`, { headers: hdr() });
|
|
if (!res.ok) {
|
|
list.innerHTML = `<div class="empty">Ошибка загрузки (${res.status})</div>`;
|
|
return;
|
|
}
|
|
const data = await res.json();
|
|
total = Number(data.total) || 0;
|
|
const maxPage = Math.max(1, Math.ceil(total / PAGE_SIZE));
|
|
if (page > maxPage) { page = maxPage; return loadLessons(); }
|
|
renderLessons(data.items || []);
|
|
renderPager(document.getElementById('pager'), page, maxPage, p => { page = p; loadLessons(); });
|
|
} catch {
|
|
list.innerHTML = '<div class="empty">Ошибка сети</div>';
|
|
}
|
|
}
|
|
|
|
async function loadGroupFilter() {
|
|
if (groupsLoaded) return;
|
|
const sel = document.getElementById('groupFilter');
|
|
const groups = await loadLessonGroups();
|
|
groups.forEach(g => {
|
|
const o = document.createElement('option');
|
|
o.value = g.id;
|
|
o.textContent = g.branch_name ? `${g.name} (${g.branch_name})` : g.name;
|
|
sel.appendChild(o);
|
|
});
|
|
groupsLoaded = true;
|
|
}
|
|
|
|
async function deleteLesson(id) {
|
|
if (!confirm('Удалить отчёт о занятии?')) return;
|
|
const res = await fetch(`${API}/api/lesson-reports/${id}`, { method: 'DELETE', headers: hdr() });
|
|
const data = await res.json().catch(() => ({}));
|
|
if (!res.ok) { showToast(data.error || 'Не удалось удалить отчёт'); return; }
|
|
showToast('Отчёт удалён');
|
|
loadLessons();
|
|
}
|
|
|
|
function onLessonReportSaved() {
|
|
loadLessons();
|
|
}
|
|
|
|
document.getElementById('lessonsList').addEventListener('click', e => {
|
|
const edit = e.target.closest('[data-edit]');
|
|
if (edit) { openLessonModal({ id: edit.dataset.edit }); return; }
|
|
const del = e.target.closest('[data-del]');
|
|
if (del) { deleteLesson(del.dataset.del); }
|
|
});
|
|
|
|
document.getElementById('addLessonBtn').addEventListener('click', () => {
|
|
openLessonModal({ groupId: document.getElementById('groupFilter').value });
|
|
});
|
|
|
|
document.getElementById('groupFilter').addEventListener('change', () => { page = 1; loadLessons(); });
|
|
document.getElementById('dateFrom').addEventListener('change', () => { page = 1; loadLessons(); });
|
|
document.getElementById('dateTo').addEventListener('change', () => { page = 1; loadLessons(); });
|
|
document.getElementById('searchInput').addEventListener('input', () => {
|
|
clearTimeout(searchTimer);
|
|
searchTimer = setTimeout(() => { page = 1; loadLessons(); }, 350);
|
|
});
|
|
|
|
(async () => {
|
|
if (!(await checkAuth())) return;
|
|
buildSidebar(document.body.dataset.page);
|
|
await loadGroupFilter();
|
|
loadLessons();
|
|
})(); |