Files
WhatIDo/public/admin.js
T
dev e6291a0235 feat(modules): module topics for student form, admin CRUD with pagination
- db: modules table (name, lessons_count) + entries.module_id (ON DELETE SET NULL), migration + idempotent startup ensure
- api: GET /api/modules (public, search + limit/offset, entries_count), POST/PUT/DELETE (admin, audit-logged)
- entries: accept/validate module_id on create/update, return module_name, module_id filter
- backup/restore: include modules and entries.module_id
- student form: required module select, hidden while no modules exist
- admin: modules.html + js/modules.js list with pagination, search, create/edit/delete modal
- journal: module filter, module select in edit modal, module badge, CSV column
2026-09-18 18:56:42 +03:00

212 lines
7.6 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.
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: 'students', label: 'Ученики', icon: 'graduation-cap' },
{ page: 'groups', label: 'Группы', icon: 'users' },
{ 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 nav = [...base, ...(isAdmin() ? adminNav : [])];
const userName = currentUser?.name || currentUser?.username || '';
const roleLabel = isAdmin() ? 'Администратор' : 'Тутор';
el.innerHTML = `
<div class="logo">Админ-панель</div>
<div class="user-box">
<div class="user-name">${esc(userName)}</div>
<div class="user-role">${roleLabel}</div>
</div>
<nav>
${nav.map(n => `<a href="${n.page}.html" data-nav="${n.page}" class="${n.page === active ? 'active' : ''}"><i data-lucide="${n.icon}"></i>${n.label}</a>`).join('')}
</nav>
<div class="bottom">
<a href="/" data-nav="home"><i data-lucide="home"></i>На главную</a>
<a class="logout" href="#" data-action="logout"><i data-lucide="log-out"></i>Выйти</a>
</div>`;
renderIcons();
}
// --- 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);
}
}
function renderPager(el, page, totalPages, onPage) {
if (!el) return;
if (totalPages <= 1) { el.innerHTML = ''; return; }
let html = `<button ${page === 1 ? 'disabled' : ''} data-nav="-1">‹</button>`;
for (let i = 1; i <= totalPages; i++) {
if (totalPages > 9 && i > 2 && i < totalPages - 1 && Math.abs(i - page) > 2) {
if (i === 3) html += `<button disabled>…</button>`;
continue;
}
html += `<button class="${i === page ? 'active' : ''}" data-page="${i}">${i}</button>`;
}
html += `<button ${page === totalPages ? 'disabled' : ''} data-nav="1">›</button>`;
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(); }
});