From 596e8d8b3006bbdc6dbe65368791e42b5e726141 Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Wed, 3 Jun 2026 12:50:57 +0300 Subject: [PATCH] =?UTF-8?q?feat(access):=20=D0=A4=D0=B0=D0=B7=D0=B0=202b?= =?UTF-8?q?=20=E2=80=94=20=D0=BF=D0=BE=D0=B8=D1=81=D0=BA/=D0=B3=D1=80?= =?UTF-8?q?=D1=83=D0=BF=D0=BF=D0=B8=D1=80=D0=BE=D0=B2=D0=BA=D0=B0=20=D0=BF?= =?UTF-8?q?=D0=BE=20=D0=BF=D1=80=D0=B5=D0=B4=D0=BC=D0=B5=D1=82=D1=83=20+?= =?UTF-8?q?=20=C2=AB=D1=8D=D1=84=D1=84=D0=B5=D0=BA=D1=82=D0=B8=D0=B2=D0=BD?= =?UTF-8?q?=D1=8B=D0=B9=20=D0=B4=D0=BE=D1=81=D1=82=D1=83=D0=BF=C2=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Режим «По контенту»: поиск по названию в левой колонке (обновляет только список, фокус сохраняется) + подзаголовки по предмету (Математика/Физика/…). У раскрытого класса рядом с tri-state каждого ученика — бейдж итогового доступа «видит/не видит · лично|по классу|по умолч.» (считается клиентски из загруженных правил) — снимает путаницу «наследовать/открыт/закрыт». Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/js/admin/sections/access.js | 83 ++++++++++++++++++++++------ 1 file changed, 66 insertions(+), 17 deletions(-) diff --git a/frontend/js/admin/sections/access.js b/frontend/js/admin/sections/access.js index f8c8462..07e8444 100644 --- a/frontend/js/admin/sections/access.js +++ b/frontend/js/admin/sections/access.js @@ -27,6 +27,12 @@ let _matrix = null; // { classes:[{id,name}], open:{ [cid]:{textbook:[],exam:[]} } } let _mSearch = ''; + // content-mode left search + let _leftSearch = ''; + const SUBJ_LABEL = { math: 'Математика', physics: 'Физика', phys: 'Физика', chemistry: 'Химия', + chem: 'Химия', biology: 'Биология', bio: 'Биология', informatics: 'Информатика', + russian: 'Русский язык', english: 'Английский', geography: 'География', history: 'История' }; + const esc = (s) => (window.LS && LS.esc ? LS.esc(s) : String(s == null ? '' : s)); const bucket = (type) => (type === 'textbook' ? 'textbooks' : 'exams'); const keyName = (type) => (type === 'textbook' ? 'slug' : 'exam_key'); @@ -83,27 +89,57 @@ color:${has ? 'var(--ok,#16a34a)' : 'var(--muted)'}">${open}/${total}`; } + /* ── список контента в левой колонке (с поиском + подзаголовками по предмету) ── */ + function contentItemBtn(type, it, total) { + const ref = it[keyName(type)]; + const active = _selContent && _selContent.type === type && _selContent.ref === ref; + const open = (_summary[bucket(type)] || {})[ref] || 0; + return ``; + } + function contentLeftList() { + const total = _summary.totalClasses || 0; + const term = _leftSearch.trim().toLowerCase(); + const match = (it) => !term || (it.title || '').toLowerCase().includes(term); + const tbs = (_catalog.textbooks || []).filter(match); + const exs = (_catalog.exams || []).filter(match); + let html = ''; + if (tbs.length) { + html += `
Учебники
`; + let lastSubj = null; + tbs.forEach(it => { + const sj = it.subject || ''; + if (sj !== lastSubj) { + lastSubj = sj; + html += `
${esc(SUBJ_LABEL[sj] || sj || 'Прочее')}
`; + } + html += contentItemBtn('textbook', it, total); + }); + } + if (exs.length) { + html += `
Экзамены
`; + exs.forEach(it => { html += contentItemBtn('exam', it, total); }); + } + return html || empty('Ничего не найдено'); + } + function leftSearch(v) { + _leftSearch = v; + const el = document.getElementById('acc-left-list'); + if (el) el.innerHTML = contentLeftList(); + } + /* ── ЛЕВАЯ колонка ── */ function renderLeft() { const left = document.getElementById('acc-left'); if (_mode === 'content') { - const total = _summary.totalClasses || 0; - const list = (type, items) => items.map(it => { - const ref = it[keyName(type)]; - const active = _selContent && _selContent.type === type && _selContent.ref === ref; - const open = (_summary[bucket(type)] || {})[ref] || 0; - return ``; - }).join(''); left.innerHTML = ` -
Учебники
- ${list('textbook', _catalog.textbooks || []) || empty('Нет учебников')} -
Экзамены
- ${list('exam', _catalog.exams || []) || empty('Нет экзаменов')}`; + +
${contentLeftList()}
`; } else { const classes = _targets.classes || []; left.innerHTML = ` @@ -157,6 +193,17 @@ ${btn('null', 'Наследовать', state === 'inherit')}${btn(1, 'Открыт', state === 'open')}${btn(0, 'Закрыт', state === 'closed')}`; } + /* эффективный доступ ученика: что он реально видит и почему */ + function effBadge(uid, classOpen) { + const v = _rules.studentRules[uid]; + let open, why; + if (v === 1) { open = true; why = 'лично'; } + else if (v === 0) { open = false; why = 'лично'; } + else { open = !!classOpen; why = classOpen ? 'по классу' : 'по умолч.'; } + return `${open ? 'видит' : 'не видит'} · ${why}`; + } + function classRowContent(c) { const openToClass = _rules.classRules[c.id] === 1; const expanded = _open.has(c.id); @@ -165,7 +212,8 @@
${students.length ? students.map(s => `
- ${esc(s.name || s.email)}${studentTri(s.id)} + ${esc(s.name || s.email)} + ${effBadge(s.id, openToClass)}${studentTri(s.id)}
`).join('') : '

В классе нет учеников

'}
` : ''; return ` @@ -419,6 +467,7 @@ window.accClassBulk = classBulk; window.accMx = mxToggle; window.accMxSearch = mxSearch; + window.accLeftSearch = leftSearch; window.AdminSections = window.AdminSections || {}; window.AdminSections.access = {