diff --git a/frontend/admin-textbooks.html b/frontend/admin-textbooks.html
index cc4a16a..55bc98a 100644
--- a/frontend/admin-textbooks.html
+++ b/frontend/admin-textbooks.html
@@ -2,204 +2,8 @@
-
- Управление учебниками — LearnSpace
-
-
-
-
+ Перенаправление…
+
-
-
-
-
-
-
-
-
-
-
-
+
diff --git a/frontend/textbook-progress.html b/frontend/textbook-progress.html
index b0ad270..e976649 100644
--- a/frontend/textbook-progress.html
+++ b/frontend/textbook-progress.html
@@ -2,225 +2,8 @@
-
- Прогресс по учебникам — LearnSpace
-
-
-
-
+ Перенаправление…
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Выберите учебник и класс
-
-
-
-
-
-
-
-
-
-
-
-
+
diff --git a/frontend/textbooks.html b/frontend/textbooks.html
index 1ce3929..06ac793 100644
--- a/frontend/textbooks.html
+++ b/frontend/textbooks.html
@@ -143,6 +143,117 @@
}
.tb-empty svg { width:48px; height:48px; opacity:.5; margin-bottom:14px; stroke:var(--text-3); }
+ /* ── Tabs ── */
+ .tb-tabs {
+ display:flex; gap:4px; margin-bottom:24px;
+ background:var(--surface); border:1.5px solid var(--border);
+ border-radius:12px; padding:4px;
+ }
+ .tb-tab {
+ flex:0 0 auto; padding:9px 18px; border-radius:8px;
+ border:none; background:transparent; color:var(--text-2);
+ font-family:'Manrope',sans-serif; font-size:.88rem; font-weight:700;
+ cursor:pointer; transition:all .14s;
+ display:inline-flex; align-items:center; gap:7px;
+ }
+ .tb-tab:hover { color:var(--text); }
+ .tb-tab.active {
+ background:var(--violet); color:#fff;
+ box-shadow: 0 2px 8px rgba(155,93,229,.25);
+ }
+ .tb-tab svg { width:14px; height:14px; }
+ .tb-panel { display:none; }
+ .tb-panel.active { display:block; }
+
+ /* ── Class-progress tab styles ── */
+ .tp-pickers {
+ display:flex; gap:12px; margin-bottom:24px; flex-wrap:wrap;
+ }
+ .tp-picker { flex:1; min-width:200px; }
+ .tp-picker label {
+ display:block; font-size:.72rem; font-weight:700; color:var(--text-2);
+ text-transform:uppercase; letter-spacing:.05em; margin-bottom:6px;
+ }
+ .tp-picker select {
+ width:100%; padding:10px 12px; border:1.5px solid var(--border-h);
+ border-radius:10px; background:var(--surface); color:var(--text);
+ font-family:'Manrope',sans-serif; font-size:.9rem; font-weight:600;
+ cursor:pointer;
+ }
+ .tp-picker select:focus { outline:none; border-color:var(--violet); }
+
+ .tp-table {
+ background:var(--surface); border:1.5px solid var(--border); border-radius:14px;
+ overflow:hidden;
+ }
+ .tp-row {
+ display:grid; grid-template-columns: 1.5fr 2fr 1fr 1fr;
+ padding:13px 18px; align-items:center; gap:14px;
+ border-bottom:1px solid var(--border);
+ transition: background .12s;
+ }
+ .tp-row:last-child { border-bottom:none; }
+ .tp-row:hover { background:rgba(155,93,229,.04); }
+ .tp-row.head {
+ background:rgba(155,93,229,.06); font-family:'Unbounded',sans-serif;
+ font-size:.72rem; font-weight:800; color:var(--text-2);
+ text-transform:uppercase; letter-spacing:.05em;
+ }
+ .tp-row.head:hover { background:rgba(155,93,229,.06); }
+ .tp-name { font-weight:700; font-size:.92rem; }
+ .tp-bar { height:8px; border-radius:99px; background:var(--border); overflow:hidden; }
+ .tp-bar-fill { height:100%; border-radius:99px; transition:width .3s; background:var(--violet); }
+ .tp-bar-text { font-size:.76rem; color:var(--text-3); margin-top:4px; }
+ .tp-last { font-size:.82rem; color:var(--text-2); }
+ .tp-last small { color:var(--text-3); }
+ @media (max-width: 700px) {
+ .tp-row { grid-template-columns: 1.5fr 1fr; gap:8px; }
+ .tp-row > :nth-child(3), .tp-row > :nth-child(4) { display:none; }
+ }
+
+ /* ── Admin manage tab styles ── */
+ .am-row {
+ display:grid; grid-template-columns: 2.5fr 1.2fr 1fr 1fr 0.8fr 0.8fr;
+ padding:14px 18px; align-items:center; gap:14px;
+ border-bottom:1px solid var(--border);
+ }
+ .am-row:last-child { border-bottom:none; }
+ .am-row.head {
+ background:rgba(155,93,229,.06);
+ font-family:'Unbounded',sans-serif;
+ font-size:.72rem; font-weight:800; color:var(--text-2);
+ text-transform:uppercase; letter-spacing:.05em;
+ }
+ .am-row.inactive { opacity:.55; }
+ .am-input {
+ width:100%; padding:6px 10px; border:1.5px solid var(--border);
+ border-radius:7px; background:transparent; color:var(--text);
+ font-family:'Manrope',sans-serif; font-size:.85rem;
+ }
+ .am-input:focus { outline:none; border-color:var(--violet); }
+ .am-pill {
+ display:inline-block; padding:3px 10px; border-radius:99px;
+ font-size:.72rem; font-weight:700;
+ background:rgba(155,93,229,.12); color:var(--violet);
+ }
+ .am-toggle {
+ position:relative; width:38px; height:22px; border-radius:99px;
+ background:var(--border); cursor:pointer; transition:background .15s;
+ display:inline-block;
+ }
+ .am-toggle.on { background:#06D6A0; }
+ .am-toggle::after {
+ content:''; position:absolute; top:2px; left:2px;
+ width:18px; height:18px; border-radius:50%;
+ background:#fff; transition:transform .15s;
+ box-shadow:0 1px 3px rgba(0,0,0,.2);
+ }
+ .am-toggle.on::after { transform:translateX(16px); }
+ .am-link { color:var(--violet); text-decoration:none; font-weight:700; font-size:.82rem; }
+ .am-link:hover { text-decoration:underline; }
+ .am-saved { color:#06D6A0; font-size:.72rem; margin-left:6px; opacity:0; transition:opacity .3s; }
+ .am-saved.show { opacity:1; }
+
/* ── Assign modal (reused styling from exam9) ── */
.ex-overlay {
display:none; position:fixed; inset:0;
@@ -259,13 +370,51 @@
-
-
-
-
Загрузка…
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Выберите учебник и класс
+
+
+
+
+
@@ -328,17 +477,35 @@
LS.hideDisabledFeatures();
const isTeacher = user && (user.role === 'teacher' || user.role === 'admin');
+ const isAdmin = user && user.role === 'admin';
let textbooks = [];
let teacherClasses = null;
- // Teacher-only: "Class progress" button in header
- if (isTeacher) {
- document.getElementById('tb-header-actions').innerHTML = `
-
-
- Прогресс класса
- `;
- }
+ // Reveal teacher/admin tabs
+ if (isTeacher) document.querySelectorAll('.tb-tab-teacher').forEach(el => el.style.display = '');
+ if (isAdmin) document.querySelectorAll('.tb-tab-admin').forEach(el => el.style.display = '');
+
+ /* ── Tab routing ── */
+ const VALID_TABS = ['catalog', 'progress', 'manage'];
+ let _progressInited = false;
+ let _manageInited = false;
+
+ window.setTab = function (name) {
+ if (!VALID_TABS.includes(name)) name = 'catalog';
+ if (name === 'progress' && !isTeacher) name = 'catalog';
+ if (name === 'manage' && !isAdmin) name = 'catalog';
+
+ document.querySelectorAll('.tb-tab').forEach(t => t.classList.toggle('active', t.dataset.tab === name));
+ document.querySelectorAll('.tb-panel').forEach(p => p.classList.toggle('active', p.id === 'tb-panel-' + name));
+ history.replaceState(null, '', '#' + name);
+
+ if (name === 'progress' && !_progressInited) { _progressInited = true; initProgressTab(); }
+ if (name === 'manage' && !_manageInited) { _manageInited = true; initManageTab(); }
+ };
+
+ // Initial tab from URL hash
+ const initialTab = (location.hash || '').replace('#', '') || 'catalog';
+ setTimeout(() => setTab(initialTab), 0);
function esc(s) {
return String(s || '').replace(/[&<>"']/g, c => ({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[c]));
@@ -580,6 +747,174 @@
}
};
+ /* ════════════════════════════════════════════════
+ TAB: Class progress (teacher/admin)
+ ════════════════════════════════════════════════ */
+ function fmtDate(s) {
+ if (!s) return '';
+ try { return new Date(s.includes('T') ? s : s.replace(' ', 'T') + 'Z').toLocaleString('ru-RU', { day:'numeric', month:'short', hour:'2-digit', minute:'2-digit' }); }
+ catch { return s; }
+ }
+
+ async function initProgressTab() {
+ const tbSel = document.getElementById('tp-textbook');
+ const clsSel = document.getElementById('tp-class');
+ const resEl = document.getElementById('tp-result');
+
+ // Load classes
+ const classes = await LS.api('/api/classes').catch(() => []);
+ const list = Array.isArray(classes) ? classes : [];
+ if (!list.length) {
+ resEl.innerHTML = 'У вас нет классов
';
+ return;
+ }
+
+ // Populate selects (textbooks already loaded)
+ textbooks.forEach((t, i) => {
+ const o = document.createElement('option');
+ o.value = t.slug;
+ o.textContent = `${t.title} (§1–${t.para_count})`;
+ if (i === 0) o.selected = true;
+ tbSel.appendChild(o);
+ });
+ list.forEach((c, i) => {
+ const o = document.createElement('option');
+ o.value = c.id;
+ o.textContent = `${c.name} (${c.member_count || 0} учеников)`;
+ if (i === 0) o.selected = true;
+ clsSel.appendChild(o);
+ });
+
+ const colorMap = { amber:'#d97706', blue:'#2563eb', green:'#059669', violet:'#7c3aed', pink:'#db2777' };
+
+ async function refresh() {
+ const tbSlug = tbSel.value;
+ const classId = clsSel.value;
+ if (!tbSlug || !classId) return;
+ resEl.className = 'tb-empty';
+ resEl.innerHTML = 'Загрузка…';
+ try {
+ const r = await LS.api(`/api/textbooks/${tbSlug}/class-progress?class_id=${classId}`);
+ const total = r.total_paragraphs || 0;
+ const tb = textbooks.find(t => t.slug === tbSlug);
+ const color = colorMap[tb?.color] || '#7c3aed';
+ if (!r.students.length) {
+ resEl.innerHTML = 'В классе нет учеников
'; return;
+ }
+ r.students.sort((a, b) => (b.read_count - a.read_count) || a.name.localeCompare(b.name));
+ const rows = r.students.map(s => {
+ const pct = total > 0 ? Math.round(100 * s.read_count / total) : 0;
+ return `
+
+
${esc(s.name)}
+
+
+
${s.read_count} из ${total} §
+
+
${pct}%
+
${s.last_para ? `§${s.last_para.replace('p','')}
${fmtDate(s.last_at)}` : '—'}
+
`;
+ }).join('');
+ resEl.className = 'tp-table';
+ resEl.innerHTML = `
+
+
Ученик
Прогресс
%
Последний §
+
${rows}`;
+ } catch (e) {
+ resEl.className = 'tb-empty';
+ resEl.innerHTML = 'Ошибка: ' + esc(e.message);
+ }
+ }
+ tbSel.addEventListener('change', refresh);
+ clsSel.addEventListener('change', refresh);
+ refresh();
+ }
+
+ /* ════════════════════════════════════════════════
+ TAB: Manage textbooks (admin only)
+ ════════════════════════════════════════════════ */
+ const SUBJECTS = { chemistry:'Химия', physics:'Физика', math:'Математика', biology:'Биология' };
+ let allTextbooks = [];
+
+ async function initManageTab() {
+ const el = document.getElementById('am-content');
+ try {
+ const r = await LS.api('/api/textbooks/admin/all');
+ allTextbooks = r.textbooks || [];
+ renderManage();
+ } catch (e) {
+ el.innerHTML = 'Ошибка: ' + esc(e.message);
+ }
+ }
+
+ function renderManage() {
+ const el = document.getElementById('am-content');
+ if (!allTextbooks.length) {
+ el.innerHTML = 'Учебники не добавлены
'; return;
+ }
+ const html = `
+
+
+
Учебник
Автор
Предмет
+
Класс
Читателей
Активен
+
+ ${allTextbooks.map(t => `
+
+
+
+
${esc(SUBJECTS[t.subject] || t.subject)}
+
${t.grade}
+
${t.readers || 0}
+
+
`).join('')}
+
`;
+ el.className = '';
+ el.innerHTML = html;
+ wireManageEvents();
+ }
+
+ function wireManageEvents() {
+ document.querySelectorAll('#am-content .am-toggle').forEach(t => {
+ t.addEventListener('click', async () => {
+ const row = t.closest('.am-row');
+ const id = Number(row.dataset.id);
+ const newVal = t.classList.contains('on') ? 0 : 1;
+ try {
+ await LS.api('/api/textbooks/admin/' + id, { method: 'PATCH', body: { is_active: newVal } });
+ t.classList.toggle('on', newVal === 1);
+ row.classList.toggle('inactive', !newVal);
+ flashManageSaved(id);
+ } catch (e) { alert(e.message); }
+ });
+ });
+ document.querySelectorAll('#am-content .am-input').forEach(inp => {
+ let timer;
+ inp.addEventListener('input', () => {
+ clearTimeout(timer);
+ timer = setTimeout(() => {
+ const row = inp.closest('.am-row');
+ const id = Number(row.dataset.id);
+ LS.api('/api/textbooks/admin/' + id, { method: 'PATCH', body: { [inp.dataset.field]: inp.value } })
+ .then(() => flashManageSaved(id))
+ .catch(e => alert(e.message));
+ }, 600);
+ });
+ });
+ }
+
+ function flashManageSaved(id) {
+ const el = document.getElementById('am-saved-' + id);
+ if (!el) return;
+ el.classList.add('show');
+ setTimeout(() => el.classList.remove('show'), 1500);
+ }
+
await loadTextbooks();
})();