const API = location.origin; let token = sessionStorage.getItem('authToken'); let currentUser = null; function hdr() { return { 'X-Auth-Token': token }; } function hdrJson() { return { 'Content-Type': 'application/json', 'X-Auth-Token': token }; } function esc(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; } function thumbSrc(p) { if (!p) return p; if (p.startsWith('/uploads/')) return `${API}/uploads/thumb/${p.slice('/uploads/'.length)}`; if (p.includes('/api/files/')) return p + (p.includes('?') ? '&' : '?') + 'thumb=1'; return p; } // --- Auth check --- async function checkAuth() { if (!token) { location.href = 'login.html'; return false; } try { const res = await fetch(`${API}/api/auth/me`, { headers: hdr() }); if (!res.ok) { cleanupSession(); return false; } currentUser = await res.json(); return true; } catch { cleanupSession(); return false; } } function cleanupSession() { sessionStorage.removeItem('authToken'); token = null; currentUser = null; location.href = 'login.html'; } function isAdmin() { return currentUser && currentUser.role === 'admin'; } async function requireAdminPage() { if (!(await checkAuth())) return false; if (!isAdmin()) { location.href = 'journal.html'; return false; } return true; } async function doLogout() { try { await fetch(`${API}/api/auth/logout`, { method: 'POST', headers: hdr() }); } catch {} cleanupSession(); } // --- Sidebar --- function buildSidebar(active) { const el = document.getElementById('sidebar'); if (!el) return; 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' }, { page: 'photos', label: 'Фото', icon: 'Image' }, { page: 'files', label: 'Файлы', icon: 'folder' }, { page: 'links', label: 'Ссылки', icon: 'link' }, { page: 'trash', label: 'Корзина', icon: 'trash-2' }, ]; const adminNav = [ { page: 'modules', label: 'Темы модулей', icon: 'layers' }, { page: 'branches', label: 'Филиалы', icon: 'building-2' }, { page: 'users', label: 'Пользователи', icon: 'user-cog' }, { page: 'worker', label: 'Воркер ИИ', icon: 'bot' }, { page: 'audit', label: 'Аудит', icon: 'scroll-text' }, { page: 'bans', label: 'Блокировки', icon: 'shield-off' }, { page: 'settings', label: 'Настройки', icon: 'settings' }, ]; const userName = currentUser?.name || currentUser?.username || ''; const roleLabel = isAdmin() ? 'Администратор' : 'Тутор'; const renderItem = n => { const isAct = n.page === active; const badgeHtml = n.badge ? ` 0 ? '' : 'hidden'}>${notifyUnread > 99 ? '99+' : notifyUnread}` : ''; return ` ${n.label} ${badgeHtml} `; }; el.innerHTML = `
${esc(userName)}
${roleLabel}
Уведомления 0 ? '' : 'hidden'}>${notifyUnread > 99 ? '99+' : notifyUnread} На главную
`; renderIcons(); loadVersion(); loadBrand(); initNotifications(); } async function loadBrand() { const logoEl = document.getElementById('sidebarLogo'); const nameEl = document.getElementById('sidebarName'); if (!logoEl || !nameEl) return; try { const res = await fetch(`${API}/api/public-settings`); if (!res.ok) return; const s = await res.json(); if ((s.system_name || '').trim()) nameEl.textContent = s.system_name; if (s.system_logo) { const img = document.createElement('img'); img.src = s.system_logo; img.alt = ''; logoEl.classList.add('has-logo'); logoEl.appendChild(img); } } catch {} } async function loadVersion() { const el = document.getElementById('sidebarVersion'); if (!el) return; try { const res = await fetch(`${API}/version.json?t=${Date.now()}`); if (!res.ok) { el.textContent = ''; return; } const v = await res.json(); if (!v.short) { el.textContent = ''; return; } const d = (v.date || '').slice(0, 10); el.textContent = `v${v.short}${d ? ' · ' + d : ''}`; el.title = `${v.short} · ${v.date || ''}`; } catch { el.textContent = ''; } } // --- Notifications --- const NOTIFY_ICONS = { 'entry.new': 'book-open', 'entry.ai.corrected': 'sparkles', 'entry.ai.error': 'bot', 'photo.job.done': 'image', 'photo.job.error': 'image-off', 'ip.ban': 'shield-off', 'backup.restore': 'database', 'backup.create': 'download', 'system.test': 'send', 'lesson.report': 'notebook-pen', 'lesson.ai.formatted': 'file-check', }; const NOTIFY_LEVELS = { warning: 'Предупреждение', critical: 'Важно', info: '' }; let notifyList = []; let notifyUnread = 0; let notifyStream = null; let notifyReconnectTimer = null; let desktopNotify = localStorage.getItem('notifyDesktop') === '1'; function notifyIcon(type) { return NOTIFY_ICONS[type] || 'bell'; } function attrEsc(s) { return esc(String(s === null || s === undefined ? '' : s).replace(/"/g, '"')); } function notifyTime(ts) { const d = new Date(ts); if (Number.isNaN(d.getTime())) return ''; const diff = Date.now() - d.getTime(); if (diff < 60000) return 'только что'; if (diff < 3600000) return `${Math.floor(diff / 60000)} мин назад`; if (diff < 86400000) return `${Math.floor(diff / 3600000)} ч назад`; return d.toLocaleString('ru-RU', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' }); } function setNotifyUnread(n) { notifyUnread = Number(n) || 0; const badge = document.getElementById('notifyBadge'); if (badge) { badge.textContent = notifyUnread > 99 ? '99+' : String(notifyUnread); badge.hidden = notifyUnread <= 0; } const navBadge = document.getElementById('sidebarNotifyBadge'); if (navBadge) { navBadge.textContent = notifyUnread > 99 ? '99+' : String(notifyUnread); navBadge.hidden = notifyUnread <= 0; } } function notifyItemHtml(n, expanded) { const level = NOTIFY_LEVELS[n.level] ? ` level-${n.level}` : ''; return ` ${esc(n.title)} ${n.body ? `${esc(n.body)}` : ''} ${esc(notifyTime(n.created_at))} `; } function notifyPanelEl() { let panel = document.getElementById('notifyPanel'); if (panel) return panel; panel = document.createElement('div'); panel.className = 'notify-panel'; panel.id = 'notifyPanel'; panel.hidden = true; document.body.appendChild(panel); return panel; } function renderNotifyPanel() { const panel = notifyPanelEl(); if (!panel) return; const items = notifyList.slice(0, 12); panel.innerHTML = `
Уведомления
${items.length ? items.map(n => notifyItemHtml(n, false)).join('') : '
Пока уведомлений нет
'}
Все уведомления${notifyUnread ? ` (${notifyUnread})` : ''}
`; renderIcons(); } async function loadNotifications() { try { const res = await fetch(`${API}/api/notifications?limit=20`, { headers: hdr() }); if (!res.ok) return; const data = await res.json(); notifyList = data.items || []; setNotifyUnread(data.unread); renderNotifyPanel(); } catch {} } function toggleNotifyPanel(force) { const panel = notifyPanelEl(); if (!panel) return; const open = force === undefined ? panel.hidden : !!force; panel.hidden = !open; if (open) { renderNotifyPanel(); loadNotifications(); } } async function markNotifyRead(id) { const item = notifyList.find(n => n.id === id); if (item && !item.read) { item.read = true; setNotifyUnread(Math.max(0, notifyUnread - 1)); renderNotifyPanel(); } try { await fetch(`${API}/api/notifications/${id}/read`, { method: 'POST', headers: hdr() }); } catch {} } async function markAllNotifyRead() { try { const res = await fetch(`${API}/api/notifications/read-all`, { method: 'POST', headers: hdr() }); if (!res.ok) return; notifyList.forEach(n => { n.read = true; }); setNotifyUnread(0); renderNotifyPanel(); if (typeof onNotifyListChanged === 'function') onNotifyListChanged(); } catch {} } function showDesktopNotification(n) { if (!desktopNotify) return; if (typeof Notification === 'undefined' || Notification.permission !== 'granted') return; try { const note = new Notification(n.title, { body: n.body || '', tag: 'whatido-' + n.id }); note.onclick = () => { window.focus(); if (n.link) location.href = n.link; note.close(); }; } catch {} } async function toggleDesktopNotifications() { if (desktopNotify) { desktopNotify = false; localStorage.setItem('notifyDesktop', '0'); renderNotifyPanel(); return; } if (typeof Notification === 'undefined') { showToast('Браузер не поддерживает уведомления'); return; } let perm = Notification.permission; if (perm === 'default') { try { perm = await Notification.requestPermission(); } catch {} } if (perm !== 'granted') { showToast('Разрешение на уведомления не выдано'); return; } desktopNotify = true; localStorage.setItem('notifyDesktop', '1'); renderNotifyPanel(); showToast('Уведомления в браузере включены'); } function handleIncomingNotification(n) { notifyList.unshift(n); if (notifyList.length > 40) notifyList.length = 40; setNotifyUnread(notifyUnread + 1); renderNotifyPanel(); if (n.level === 'warning' || n.level === 'critical') showToast(n.title); showDesktopNotification(n); if (typeof onNotificationArrived === 'function') onNotificationArrived(n); } function connectNotifyStream() { if (notifyStream) { notifyStream.close(); notifyStream = null; } const connect = () => { const t = sessionStorage.getItem('authToken'); if (!t) return; notifyStream = new EventSource(`${API}/api/notifications/stream?token=${encodeURIComponent(t)}`); notifyStream.addEventListener('ready', ev => { try { setNotifyUnread(JSON.parse(ev.data).unread); } catch {} }); notifyStream.addEventListener('notification', ev => { let n = null; try { n = JSON.parse(ev.data); } catch { return; } if (!n || !n.id) return; handleIncomingNotification(n); }); notifyStream.onerror = () => { if (notifyStream) { notifyStream.close(); notifyStream = null; } clearTimeout(notifyReconnectTimer); notifyReconnectTimer = setTimeout(connect, 5000); }; }; connect(); window.addEventListener('beforeunload', () => { if (notifyStream) notifyStream.close(); }); } function initNotifications() { const bell = document.getElementById('notifyBell'); const panel = notifyPanelEl(); if (!bell || !panel) return; bell.addEventListener('click', e => { e.preventDefault(); toggleNotifyPanel(); }); panel.addEventListener('click', e => { if (e.target.closest('#notifyReadAll')) { markAllNotifyRead(); return; } if (e.target.closest('#notifyDesktopToggle')) { toggleDesktopNotifications(); return; } const item = e.target.closest('[data-notify-id]'); if (!item) return; e.preventDefault(); const id = parseInt(item.dataset.notifyId, 10); const link = item.dataset.notifyLink; toggleNotifyPanel(false); markNotifyRead(id); if (link) location.href = link; }); document.addEventListener('click', e => { if (panel.hidden) return; if (e.target.closest('#notifyPanel') || e.target.closest('#notifyBell')) return; toggleNotifyPanel(false); }); document.addEventListener('keydown', e => { if (e.key === 'Escape' && !panel.hidden) toggleNotifyPanel(false); }); loadNotifications(); connectNotifyStream(); } // --- Common UI --- function renderIcons() { if (window.lucide && typeof window.lucide.createIcons === 'function') { window.lucide.createIcons(); } } function showToast(msg) { const t = document.getElementById('toast'); if (!t) return; t.textContent = msg; t.classList.add('show'); setTimeout(() => t.classList.remove('show'), 3000); } function showImg(src) { const img = document.getElementById('imgModalSrc'); const m = document.getElementById('imgModal'); if (img && m) { img.src = src; m.classList.add('open'); } } async function fillAutocomplete() { const res = await fetch(`${API}/api/students`, { headers: hdr() }); const students = await res.json(); const names = students.map(s => s.name); const dlEdit = document.getElementById('studentEditList'); if (dlEdit) { dlEdit.innerHTML = ''; names.forEach(n => { const o = document.createElement('option'); o.value = n; dlEdit.appendChild(o); }); } const dlAdd = document.getElementById('studentAddList'); if (dlAdd) { dlAdd.innerHTML = ''; names.forEach(n => { const o = document.createElement('option'); o.value = n; dlAdd.appendChild(o); }); } } function copyStudentLink(name) { const url = `${location.origin}/?nameInput=${encodeURIComponent(name)}`; const done = () => showToast('Ссылка скопирована: ' + url); if (navigator.clipboard && window.isSecureContext) { navigator.clipboard.writeText(url).then(done).catch(() => fallbackCopy(url, done)); } else { fallbackCopy(url, done); } } function fallbackCopy(text, done) { const ta = document.createElement('textarea'); ta.value = text; ta.style.position = 'fixed'; ta.style.opacity = '0'; document.body.appendChild(ta); ta.select(); try { document.execCommand('copy'); done(); } catch { prompt('Скопируйте вручную:', text); } document.body.removeChild(ta); } function studentUrlContent(name) { const url = `${location.origin}/?nameInput=${encodeURIComponent(name)}`; return `[InternetShortcut]\r\nURL=${url}\r\n`; } function safeWindowsName(name) { return String(name).replace(/[\\/:*?"<>|]/g, '_').replace(/\s+/g, '_'); } function downloadBlob(blob, filename) { const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; a.click(); URL.revokeObjectURL(url); } async function exportStudentUrlZip(students, zipName) { if (typeof JSZip === 'undefined') { alert('JSZip не загружен'); return; } const zip = new JSZip(); students.forEach(s => { zip.file(safeWindowsName(s.name) + '.url', studentUrlContent(s.name)); }); const blob = await zip.generateAsync({ type: 'blob' }); downloadBlob(blob, zipName); } function copyText(text) { const done = () => showToast('Скопировано: ' + text); if (navigator.clipboard && window.isSecureContext) { navigator.clipboard.writeText(text).then(done).catch(() => fallbackCopy(text, done)); } else { fallbackCopy(text, done); } } 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 = `

Что прошли на занятии

0 / 5000
`; 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); modal.querySelector('#lessonVersionsBtn').addEventListener('click', () => { const id = modal.querySelector('#lessonId').value; if (id) openLessonVersions(id); }); 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 aiCheckEl = modal.querySelector('#lessonAiCheck'); 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; const aiCheck = !!aiCheckEl && aiCheckEl.checked; 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, ai_check: aiCheck }), }); 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(); if (data.ai_status === 'pending' || data.ai_status === 'processing') { showToast('Отчёт сохранён — ИИ проверяет текст по шаблону'); } else if (data.ai_status === 'error') { showToast(`Отчёт сохранён, проверка по шаблону не удалась: ${data.ai_error || 'ошибка ИИ'}`); } else { 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')); } const aiCheck = modal.querySelector('#lessonAiCheck'); if (aiCheck) aiCheck.checked = false; updateLessonAiRow(modal, r); modal.querySelector('#lessonModalTitle').textContent = 'Редактирование отчёта'; modal.querySelector('#lessonSubmit').textContent = 'Обновить'; } function lessonAiStatusText(r) { if (!r || !r.ai_status || r.ai_status === 'none') return ''; if (r.ai_status === 'pending') return 'ИИ проверяет текст по шаблону…'; if (r.ai_status === 'processing') return 'ИИ оформляет текст по шаблону…'; if (r.ai_status === 'done' && r.text_ai) return 'ИИ оформил текст по шаблону'; if (r.ai_status === 'skipped') return 'Текст уже соответствует шаблону'; if (r.ai_status === 'reverted') return 'Возвращена исходная версия текста'; if (r.ai_status === 'error') return `Проверка по шаблону не удалась: ${r.ai_error || 'ошибка ИИ'}`; return `Статус ИИ: ${r.ai_status}`; } function updateLessonAiRow(modal, r) { const row = modal.querySelector('#lessonAiRow'); const status = modal.querySelector('#lessonAiStatus'); const versionsBtn = modal.querySelector('#lessonVersionsBtn'); if (row) row.hidden = false; const text = lessonAiStatusText(r); if (status) { status.textContent = text; status.hidden = !text; status.classList.toggle('err', !!r && r.ai_status === 'error'); } if (versionsBtn) versionsBtn.hidden = !modal.querySelector('#lessonId').value; } 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')); const aiCheck = modal.querySelector('#lessonAiCheck'); if (aiCheck) aiCheck.checked = true; updateLessonAiRow(modal, null); modal.querySelector('#lessonModalTitle').textContent = 'Что прошли на занятии'; modal.querySelector('#lessonSubmit').textContent = 'Сохранить'; } const LESSON_VERSION_LABELS = { manual: 'Тьютор', ai: 'ИИ по шаблону', restore: 'Восстановлено' }; let currentLessonVersionId = ''; function ensureLessonVersionsModal() { let modal = document.getElementById('lessonVersionsModal'); if (modal) return modal; modal = document.createElement('div'); modal.className = 'modal-overlay'; modal.id = 'lessonVersionsModal'; modal.innerHTML = `

История версий отчёта

Загрузка…
`; document.body.appendChild(modal); modal.addEventListener('click', e => { if (e.target === modal) closeLessonVersions(); }); modal.querySelector('#lessonVersionsClose').addEventListener('click', closeLessonVersions); return modal; } function closeLessonVersions() { const modal = document.getElementById('lessonVersionsModal'); if (modal) modal.classList.remove('open'); } function lessonVersionItemHtml(v, index) { const label = LESSON_VERSION_LABELS[v.source] || v.source || 'Версия'; const who = v.author_name || v.author_username || ''; return `
${esc(label)} ${esc(notifyTime(v.created_at))}
${esc(v.text || '')}
${who ? `${esc(who)}` : ''} ${index === 0 ? 'текущая' : ''} ${index === 0 ? '' : ``}
`; } async function openLessonVersions(id) { currentLessonVersionId = String(id || ''); const modal = ensureLessonVersionsModal(); const list = modal.querySelector('#lessonVersionsList'); list.innerHTML = '
Загрузка…
'; modal.classList.add('open'); renderIcons(); try { const res = await fetch(`${API}/api/lesson-reports/${id}/versions`, { headers: hdr() }); const data = await res.json().catch(() => ({})); if (!res.ok) { list.innerHTML = `
${esc(data.error || 'Не удалось загрузить историю')}
`; return; } const items = data.items || []; if (!items.length) { list.innerHTML = '
Версий пока нет
'; return; } list.innerHTML = items.map(lessonVersionItemHtml).join(''); renderIcons(); } catch { list.innerHTML = '
Ошибка сети
'; } } async function restoreLessonVersion(id, versionId, btn) { if (!confirm('Восстановить эту версию отчёта?')) return; if (btn) btn.disabled = true; try { const res = await fetch(`${API}/api/lesson-reports/${id}/versions/${versionId}/restore`, { method: 'POST', headers: hdr() }); const data = await res.json().catch(() => ({})); if (!res.ok) { showToast(data.error || 'Не удалось восстановить версию'); return; } closeLessonVersions(); showToast('Версия отчёта восстановлена'); if (typeof onLessonReportSaved === 'function') onLessonReportSaved(); } catch { showToast('Ошибка сети'); } finally { if (btn) btn.disabled = false; } } document.addEventListener('click', e => { const btn = e.target.closest('[data-restore]'); if (!btn) return; e.preventDefault(); let id = btn.dataset.report || currentLessonVersionId; if (!id) { const lessonModal = document.getElementById('lessonModal'); id = (lessonModal && lessonModal.querySelector('#lessonId').value) || ''; } if (!id) return; restoreLessonVersion(id, btn.dataset.restore, btn); }); 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; } let html = ``; for (let i = 1; i <= totalPages; i++) { if (totalPages > 9 && i > 2 && i < totalPages - 1 && Math.abs(i - page) > 2) { if (i === 3) html += ``; continue; } html += ``; } html += ``; el.innerHTML = html; el._onPage = onPage; el._page = page; if (!el._bound) { el._bound = true; el.addEventListener('click', e => { const btn = e.target.closest('button'); if (!btn || btn.disabled) return; const p = btn.dataset.page ? parseInt(btn.dataset.page, 10) : el._page + parseInt(btn.dataset.nav, 10); if (el._onPage) el._onPage(p); }); } } document.addEventListener('click', e => { const imgEl = e.target.closest('[data-img]'); if (imgEl) { e.preventDefault(); showImg(imgEl.dataset.img); return; } const out = e.target.closest('[data-action="logout"]'); if (out) { e.preventDefault(); doLogout(); } });