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 без смещения.
This commit is contained in:
@@ -658,4 +658,32 @@ body[data-page=settings] .page-head{align-items:flex-end}
|
||||
.notify-panel{left:12px;right:12px;width:auto;bottom:12px}
|
||||
}
|
||||
|
||||
.lesson-modal{max-width:560px;width:92vw}
|
||||
.lesson-modal .lesson-row{display:grid;grid-template-columns:1fr 1fr;gap:12px}
|
||||
.lesson-modal .hint{font-size:.78rem;color:var(--muted);text-align:right}
|
||||
.lesson-modal textarea{min-height:130px}
|
||||
@media(max-width:560px){
|
||||
.lesson-modal .lesson-row{grid-template-columns:1fr}
|
||||
}
|
||||
.lesson-item{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);padding:14px 16px;display:flex;flex-direction:column;gap:8px}
|
||||
.lesson-item-head{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
|
||||
.lesson-group{font-weight:600;font-size:.95rem}
|
||||
.lesson-when{color:var(--muted);font-size:.78rem;display:inline-flex;align-items:center;gap:5px}
|
||||
.lesson-when i{width:13px;height:13px}
|
||||
.lesson-badge{font-size:.7rem;font-weight:700;padding:2px 9px;border-radius:999px;background:rgba(37,99,235,.1);color:var(--accent)}
|
||||
.lesson-badge.today{background:rgba(16,185,129,.14);color:#059669}
|
||||
.lesson-text{font-size:.88rem;color:var(--text);white-space:pre-wrap;word-break:break-word;line-height:1.55}
|
||||
.lesson-author{font-size:.75rem;color:var(--muted)}
|
||||
.lesson-actions{display:flex;gap:8px;flex-wrap:wrap;margin-left:auto}
|
||||
.lesson-item .btn-link{padding:6px 12px;font-size:.78rem}
|
||||
.lesson-item .btn-link.danger{color:#ef4444;border-color:rgba(239,68,68,.35)}
|
||||
.lesson-item .btn-link.danger:hover{border-color:#ef4444}
|
||||
.lesson-list{display:flex;flex-direction:column;gap:10px}
|
||||
.lesson-dash-item{display:flex;flex-direction:column;gap:3px;padding:8px 6px;border-radius:10px;transition:background .15s;cursor:pointer}
|
||||
.lesson-dash-item:hover{background:var(--bg)}
|
||||
.lesson-dash-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
||||
.lesson-dash-group{font-weight:600;font-size:.86rem}
|
||||
.lesson-dash-when{color:var(--muted);font-size:.74rem;margin-left:auto;white-space:nowrap}
|
||||
.lesson-dash-text{font-size:.8rem;color:var(--muted);overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;line-height:1.45}
|
||||
|
||||
|
||||
|
||||
+227
@@ -49,6 +49,7 @@ function buildSidebar(active) {
|
||||
const base = [
|
||||
{ page: 'dashboard', label: 'Дашборд', icon: 'layout-dashboard' },
|
||||
{ page: 'journal', label: 'Журнал', icon: 'book-open' },
|
||||
{ page: 'lessons', label: 'Отчёты о занятиях', icon: 'notebook-pen' },
|
||||
{ page: 'notifications', label: 'Уведомления', icon: 'bell', badge: true },
|
||||
{ page: 'students', label: 'Ученики', icon: 'graduation-cap' },
|
||||
{ page: 'groups', label: 'Группы', icon: 'users' },
|
||||
@@ -176,6 +177,7 @@ const NOTIFY_ICONS = {
|
||||
'backup.restore': 'database',
|
||||
'backup.create': 'download',
|
||||
'system.test': 'send',
|
||||
'lesson.report': 'notebook-pen',
|
||||
};
|
||||
const NOTIFY_LEVELS = { warning: 'Предупреждение', critical: 'Важно', info: '' };
|
||||
|
||||
@@ -483,6 +485,231 @@ function copyText(text) {
|
||||
}
|
||||
}
|
||||
|
||||
let lessonModalGroups = null;
|
||||
let lessonModalGroupsPromise = null;
|
||||
|
||||
function lessonTodayIso() {
|
||||
const d = new Date();
|
||||
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
function lessonNowTime() {
|
||||
const d = new Date();
|
||||
return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
async function loadLessonGroups() {
|
||||
if (lessonModalGroups) return lessonModalGroups;
|
||||
if (!lessonModalGroupsPromise) {
|
||||
lessonModalGroupsPromise = fetch(`${API}/api/groups`, { headers: hdr() })
|
||||
.then(res => (res.ok ? res.json() : []))
|
||||
.then(g => { lessonModalGroups = Array.isArray(g) ? g.filter(x => !x.deleted_at) : []; return lessonModalGroups; })
|
||||
.catch(() => { lessonModalGroups = []; return lessonModalGroups; });
|
||||
}
|
||||
return lessonModalGroupsPromise;
|
||||
}
|
||||
|
||||
function ensureLessonModal() {
|
||||
let modal = document.getElementById('lessonModal');
|
||||
if (modal) return modal;
|
||||
modal = document.createElement('div');
|
||||
modal.className = 'modal-overlay';
|
||||
modal.id = 'lessonModal';
|
||||
modal.innerHTML = `
|
||||
<div class="edit-modal lesson-modal">
|
||||
<h3 id="lessonModalTitle">Что прошли на занятии</h3>
|
||||
<form id="lessonForm" class="settings-stack" style="gap:12px" autocomplete="off">
|
||||
<input type="hidden" id="lessonId">
|
||||
<div class="settings-field">
|
||||
<label>Группа <span style="color:#ef4444">*</span></label>
|
||||
<select id="lessonGroup" class="settings-input" required></select>
|
||||
</div>
|
||||
<div class="lesson-row">
|
||||
<div class="settings-field">
|
||||
<label>Дата занятия <span style="color:#ef4444">*</span></label>
|
||||
<input type="date" id="lessonDate" class="settings-input" required>
|
||||
</div>
|
||||
<div class="settings-field">
|
||||
<label>Время</label>
|
||||
<input type="time" id="lessonTime" class="settings-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-field">
|
||||
<label>Что прошли на занятии <span style="color:#ef4444">*</span></label>
|
||||
<textarea id="lessonText" class="settings-input" rows="6" maxlength="5000" required
|
||||
placeholder="Например: разобрали циклы for и while, решали задачи на списки, домашнее задание — функции"></textarea>
|
||||
<span class="hint" id="lessonHint">0 / 5000</span>
|
||||
</div>
|
||||
<div class="card-foot" style="justify-content:flex-end;gap:8px">
|
||||
<button type="button" class="btn-primary ghost" id="lessonCancel">Отмена</button>
|
||||
<button type="submit" class="btn-primary" id="lessonSubmit">Сохранить</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>`;
|
||||
document.body.appendChild(modal);
|
||||
|
||||
const groupSel = modal.querySelector('#lessonGroup');
|
||||
const textEl = modal.querySelector('#lessonText');
|
||||
const hintEl = modal.querySelector('#lessonHint');
|
||||
const submitBtn = modal.querySelector('#lessonSubmit');
|
||||
|
||||
textEl.addEventListener('input', () => { hintEl.textContent = `${textEl.value.length} / 5000`; });
|
||||
modal.addEventListener('click', e => { if (e.target === modal) closeLessonModal(); });
|
||||
modal.querySelector('#lessonCancel').addEventListener('click', closeLessonModal);
|
||||
groupSel.addEventListener('change', () => prefetchLessonForGroupDate());
|
||||
modal.querySelector('#lessonForm').addEventListener('submit', e => submitLessonModal(e));
|
||||
return modal;
|
||||
}
|
||||
|
||||
async function submitLessonModal(e) {
|
||||
e.preventDefault();
|
||||
const modal = document.getElementById('lessonModal');
|
||||
if (!modal) return;
|
||||
const groupSel = modal.querySelector('#lessonGroup');
|
||||
const dateEl = modal.querySelector('#lessonDate');
|
||||
const timeEl = modal.querySelector('#lessonTime');
|
||||
const textEl = modal.querySelector('#lessonText');
|
||||
const submitBtn = modal.querySelector('#lessonSubmit');
|
||||
const groupId = groupSel.value;
|
||||
const date = dateEl.value;
|
||||
const text = textEl.value.trim();
|
||||
if (!groupId) { showToast('Выберите группу'); groupSel.focus(); return; }
|
||||
if (!date) { showToast('Укажите дату занятия'); return; }
|
||||
if (!text) { showToast('Введите текст отчёта'); textEl.focus(); return; }
|
||||
const id = modal.querySelector('#lessonId').value;
|
||||
submitBtn.disabled = true;
|
||||
try {
|
||||
const res = await fetch(`${API}/api/lesson-reports${id ? '/' + id : ''}`, {
|
||||
method: id ? 'PUT' : 'POST',
|
||||
headers: hdrJson(),
|
||||
body: JSON.stringify({ group_id: Number(groupId), lesson_date: date, lesson_time: timeEl.value || null, text }),
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) {
|
||||
if (res.status === 409 && data.id) {
|
||||
closeLessonModal();
|
||||
openLessonModal({ id: data.id });
|
||||
showToast('За эту дату отчёт уже есть — открыт для редактирования');
|
||||
return;
|
||||
}
|
||||
showToast(data.error || 'Не удалось сохранить отчёт');
|
||||
return;
|
||||
}
|
||||
closeLessonModal();
|
||||
showToast(id ? 'Отчёт о занятии обновлён' : 'Отчёт о занятии сохранён');
|
||||
if (typeof onLessonReportSaved === 'function') onLessonReportSaved();
|
||||
} catch {
|
||||
showToast('Ошибка сети');
|
||||
} finally {
|
||||
submitBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function prefetchLessonForGroupDate() {
|
||||
const modal = document.getElementById('lessonModal');
|
||||
if (!modal || !modal.classList.contains('open')) return;
|
||||
if (modal.querySelector('#lessonId').value) return;
|
||||
const groupId = modal.querySelector('#lessonGroup').value;
|
||||
const date = modal.querySelector('#lessonDate').value;
|
||||
if (!groupId || !date) return;
|
||||
try {
|
||||
const q = `group_id=${encodeURIComponent(groupId)}&date_from=${encodeURIComponent(date)}&date_to=${encodeURIComponent(date)}&limit=1`;
|
||||
const res = await fetch(`${API}/api/lesson-reports?${q}`, { headers: hdr() });
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
const item = (data.items || [])[0];
|
||||
if (!item) return;
|
||||
fillLessonModalFromReport(modal, item);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function fillLessonModalFromReport(modal, r) {
|
||||
modal.querySelector('#lessonId').value = r.id;
|
||||
if (r.lesson_time && !modal.querySelector('#lessonTime').value) {
|
||||
modal.querySelector('#lessonTime').value = String(r.lesson_time).slice(0, 5);
|
||||
}
|
||||
if (!modal.querySelector('#lessonText').value) {
|
||||
const textEl = modal.querySelector('#lessonText');
|
||||
textEl.value = r.text || '';
|
||||
textEl.dispatchEvent(new Event('input'));
|
||||
}
|
||||
modal.querySelector('#lessonModalTitle').textContent = 'Редактирование отчёта';
|
||||
modal.querySelector('#lessonSubmit').textContent = 'Обновить';
|
||||
}
|
||||
|
||||
function resetLessonModalFields(modal) {
|
||||
modal.querySelector('#lessonId').value = '';
|
||||
modal.querySelector('#lessonDate').value = lessonTodayIso();
|
||||
modal.querySelector('#lessonTime').value = lessonNowTime();
|
||||
const textEl = modal.querySelector('#lessonText');
|
||||
textEl.value = '';
|
||||
textEl.dispatchEvent(new Event('input'));
|
||||
modal.querySelector('#lessonModalTitle').textContent = 'Что прошли на занятии';
|
||||
modal.querySelector('#lessonSubmit').textContent = 'Сохранить';
|
||||
}
|
||||
|
||||
async function openLessonModal(opts = {}) {
|
||||
const modal = ensureLessonModal();
|
||||
const groupSel = modal.querySelector('#lessonGroup');
|
||||
const previous = groupSel.value;
|
||||
const groups = await loadLessonGroups();
|
||||
groupSel.innerHTML = '';
|
||||
if (!groups.length) {
|
||||
const o = document.createElement('option');
|
||||
o.value = '';
|
||||
o.textContent = 'Нет доступных групп';
|
||||
groupSel.appendChild(o);
|
||||
} else {
|
||||
groups.forEach(g => {
|
||||
const o = document.createElement('option');
|
||||
o.value = g.id;
|
||||
o.textContent = g.branch_name ? `${g.name} (${g.branch_name})` : g.name;
|
||||
groupSel.appendChild(o);
|
||||
});
|
||||
}
|
||||
|
||||
resetLessonModalFields(modal);
|
||||
const wanted = opts.groupId ? String(opts.groupId) : previous;
|
||||
if (wanted && groups.some(g => String(g.id) === wanted)) groupSel.value = wanted;
|
||||
|
||||
if (opts.text) {
|
||||
const textEl = modal.querySelector('#lessonText');
|
||||
textEl.value = opts.text;
|
||||
textEl.dispatchEvent(new Event('input'));
|
||||
}
|
||||
|
||||
modal.classList.add('open');
|
||||
|
||||
if (opts.id) {
|
||||
try {
|
||||
const res = await fetch(`${API}/api/lesson-reports/${opts.id}`, { headers: hdr() });
|
||||
if (res.ok) {
|
||||
const r = await res.json();
|
||||
groupSel.value = String(r.group_id);
|
||||
modal.querySelector('#lessonDate').value = String(r.lesson_date).slice(0, 10);
|
||||
modal.querySelector('#lessonTime').value = r.lesson_time ? String(r.lesson_time).slice(0, 5) : '';
|
||||
fillLessonModalFromReport(modal, r);
|
||||
}
|
||||
} catch {}
|
||||
} else {
|
||||
await prefetchLessonForGroupDate();
|
||||
}
|
||||
|
||||
renderIcons();
|
||||
setTimeout(() => modal.querySelector('#lessonText').focus(), 50);
|
||||
}
|
||||
|
||||
function closeLessonModal() {
|
||||
const modal = document.getElementById('lessonModal');
|
||||
if (modal) modal.classList.remove('open');
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.key !== 'Escape') return;
|
||||
const modal = document.getElementById('lessonModal');
|
||||
if (modal && modal.classList.contains('open')) closeLessonModal();
|
||||
});
|
||||
|
||||
function renderPager(el, page, totalPages, onPage) {
|
||||
if (!el) return;
|
||||
if (totalPages <= 1) { el.innerHTML = ''; return; }
|
||||
|
||||
@@ -28,6 +28,11 @@
|
||||
<div class="recent" id="recentList"></div>
|
||||
<div class="dash-link-row"><a href="journal.html">Все записи →</a></div>
|
||||
</div>
|
||||
<div class="dash-card">
|
||||
<h3><span class="ic"><i data-lucide="notebook-pen"></i></span> Что прошли на занятиях</h3>
|
||||
<div class="recent" id="lessonList"></div>
|
||||
<div class="dash-link-row"><a href="lessons.html">Все отчёты →</a></div>
|
||||
</div>
|
||||
<div class="dash-card">
|
||||
<h3><span class="ic"><i data-lucide="camera"></i></span> Фото групп</h3>
|
||||
<div class="photos-grid-dash" id="dashPhotos"></div>
|
||||
@@ -40,6 +45,7 @@
|
||||
<h3><span class="ic"><i data-lucide="zap"></i></span> Быстрые действия</h3>
|
||||
<div class="actions-grid">
|
||||
<a class="action-btn" href="journal.html"><span class="aic"><i data-lucide="book-open"></i></span>Журнал</a>
|
||||
<button type="button" class="action-btn" id="lessonReportBtn" style="border:1px solid var(--border);cursor:pointer;font-family:inherit"><span class="aic"><i data-lucide="notebook-pen"></i></span>Что прошли на занятии</button>
|
||||
<a class="action-btn" href="files.html"><span class="aic"><i data-lucide="folder"></i></span>Файлы</a>
|
||||
<a class="action-btn" href="photos.html"><span class="aic"><i data-lucide="Image"></i></span>Фото</a>
|
||||
<a class="action-btn" href="groups.html"><span class="aic"><i data-lucide="users"></i></span>Группы</a>
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
<div><label>По</label><input type="date" id="dateTo"></div>
|
||||
<div><label> </label><button type="button" id="exportCsvBtn" style="background:var(--card);border:1px solid var(--border);color:var(--text);border-radius:8px;padding:8px 16px;font-size:.85rem;cursor:pointer;white-space:nowrap">Экспорт CSV</button></div>
|
||||
<div><label> </label><button type="button" id="createLinkBtn" style="background:var(--card);border:1px solid var(--border);color:var(--text);border-radius:8px;padding:8px 16px;font-size:.85rem;cursor:pointer;white-space:nowrap">Ссылка</button></div>
|
||||
<div><label> </label><button type="button" id="lessonReportBtn" style="background:var(--card);border:1px solid var(--border);color:var(--accent);border-radius:8px;padding:8px 16px;font-size:.85rem;font-weight:600;cursor:pointer;white-space:nowrap">Что прошли на занятии</button></div>
|
||||
<div><label> </label>
|
||||
<div class="view-toggle" id="viewToggle">
|
||||
<button data-view="list" type="button">Список</button>
|
||||
|
||||
@@ -100,6 +100,24 @@ function renderPhotos(rows) {
|
||||
</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; }
|
||||
@@ -110,11 +128,24 @@ async function loadDashboard() {
|
||||
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);
|
||||
|
||||
@@ -1648,6 +1648,9 @@ document.addEventListener('click', e => {
|
||||
|
||||
document.getElementById('exportCsvBtn').addEventListener('click', exportCSV);
|
||||
document.getElementById('createLinkBtn').addEventListener('click', openLinkCreate);
|
||||
document.getElementById('lessonReportBtn').addEventListener('click', () => {
|
||||
openLessonModal({ groupId: document.getElementById('groupFilter').value });
|
||||
});
|
||||
document.querySelectorAll('#viewToggle button').forEach(b => b.addEventListener('click', () => setView(b.dataset.view)));
|
||||
document.getElementById('editCancelBtn').addEventListener('click', closeEdit);
|
||||
document.getElementById('linkCreateCloseBtn').addEventListener('click', closeLinkCreate);
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
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();
|
||||
})();
|
||||
@@ -0,0 +1,32 @@
|
||||
<title>Отчёты о занятиях — Админ-панель</title>
|
||||
<link rel="stylesheet" href="admin.css">
|
||||
</head>
|
||||
<body data-page="lessons">
|
||||
<div class="layout">
|
||||
<div class="sidebar" id="sidebar"></div>
|
||||
<div class="main">
|
||||
<div class="page-head">
|
||||
<h2>Отчёты о занятиях</h2>
|
||||
<p class="page-sub">Что прошли на занятиях — тьютор заполняет в конце занятия</p>
|
||||
</div>
|
||||
|
||||
<div class="filters">
|
||||
<div><label>Поиск</label><input type="text" id="searchInput" class="search-input" placeholder="Текст отчёта..."></div>
|
||||
<div><label>Группа</label><select id="groupFilter"><option value="">Все группы</option></select></div>
|
||||
<div><label>С</label><input type="date" id="dateFrom"></div>
|
||||
<div><label>По</label><input type="date" id="dateTo"></div>
|
||||
<div><label> </label><button type="button" class="btn-primary" id="addLessonBtn"><i data-lucide="notebook-pen"></i> Что прошли на занятии</button></div>
|
||||
</div>
|
||||
|
||||
<div class="hint" id="lessonsState" style="margin-bottom:12px"></div>
|
||||
<div class="lesson-list" id="lessonsList"><div class="empty">Загрузка…</div></div>
|
||||
<div class="pager" id="pager"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="toast" id="toast"></div>
|
||||
<script src="vendor/lucide.min.js"></script>
|
||||
<script src="admin.js"></script>
|
||||
<script src="js/lessons.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user