feat(access): Фаза 2b — поиск/группировка по предмету + «эффективный доступ»
Режим «По контенту»: поиск по названию в левой колонке (обновляет только список, фокус сохраняется) + подзаголовки по предмету (Математика/Физика/…). У раскрытого класса рядом с tri-state каждого ученика — бейдж итогового доступа «видит/не видит · лично|по классу|по умолч.» (считается клиентски из загруженных правил) — снимает путаницу «наследовать/открыт/закрыт». Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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}</span>`;
|
||||
}
|
||||
|
||||
/* ── список контента в левой колонке (с поиском + подзаголовками по предмету) ── */
|
||||
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 `<button class="acc-item${active ? ' active' : ''}" onclick="accSelContent('${type}','${esc(ref)}')"
|
||||
style="display:flex;width:100%;align-items:center;justify-content:space-between;gap:8px;text-align:left;border:none;
|
||||
background:${active ? 'var(--accent-soft,#eef2ff)' : 'transparent'};padding:8px 10px;border-radius:8px;
|
||||
cursor:pointer;font-family:inherit;font-size:13.5px;color:var(--text-1);margin-bottom:2px">
|
||||
<span style="font-weight:${active ? 600 : 500};overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(it.title)}</span>
|
||||
${badge(open, total)}</button>`;
|
||||
}
|
||||
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 += `<div style="font-weight:600;font-size:13px;color:var(--text-3);padding:6px 8px">Учебники</div>`;
|
||||
let lastSubj = null;
|
||||
tbs.forEach(it => {
|
||||
const sj = it.subject || '';
|
||||
if (sj !== lastSubj) {
|
||||
lastSubj = sj;
|
||||
html += `<div style="font-size:10.5px;color:var(--muted);padding:6px 10px 2px;text-transform:uppercase;letter-spacing:.04em">${esc(SUBJ_LABEL[sj] || sj || 'Прочее')}</div>`;
|
||||
}
|
||||
html += contentItemBtn('textbook', it, total);
|
||||
});
|
||||
}
|
||||
if (exs.length) {
|
||||
html += `<div style="font-weight:600;font-size:13px;color:var(--text-3);padding:12px 8px 6px">Экзамены</div>`;
|
||||
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 `<button class="acc-item${active ? ' active' : ''}" onclick="accSelContent('${type}','${esc(ref)}')"
|
||||
style="display:flex;width:100%;align-items:center;justify-content:space-between;gap:8px;text-align:left;border:none;
|
||||
background:${active ? 'var(--accent-soft,#eef2ff)' : 'transparent'};padding:8px 10px;border-radius:8px;
|
||||
cursor:pointer;font-family:inherit;font-size:13.5px;color:var(--text-1);margin-bottom:2px">
|
||||
<span style="font-weight:${active ? 600 : 500};overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(it.title)}</span>
|
||||
${badge(open, total)}</button>`;
|
||||
}).join('');
|
||||
left.innerHTML = `
|
||||
<div style="font-weight:600;font-size:13px;color:var(--text-3);padding:6px 8px">Учебники</div>
|
||||
${list('textbook', _catalog.textbooks || []) || empty('Нет учебников')}
|
||||
<div style="font-weight:600;font-size:13px;color:var(--text-3);padding:12px 8px 6px">Экзамены</div>
|
||||
${list('exam', _catalog.exams || []) || empty('Нет экзаменов')}`;
|
||||
<input type="text" placeholder="Поиск по названию…" value="${esc(_leftSearch)}" oninput="accLeftSearch(this.value)"
|
||||
style="width:100%;box-sizing:border-box;margin-bottom:8px;padding:7px 10px;border:1px solid var(--border);border-radius:8px;background:var(--card);color:var(--text-1);font-family:inherit;font-size:13px">
|
||||
<div id="acc-left-list">${contentLeftList()}</div>`;
|
||||
} else {
|
||||
const classes = _targets.classes || [];
|
||||
left.innerHTML = `
|
||||
@@ -157,6 +193,17 @@
|
||||
${btn('null', 'Наследовать', state === 'inherit')}${btn(1, 'Открыт', state === 'open')}${btn(0, 'Закрыт', state === 'closed')}</span>`;
|
||||
}
|
||||
|
||||
/* эффективный доступ ученика: что он реально видит и почему */
|
||||
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 `<span title="итоговый доступ" style="font-size:11px;padding:2px 7px;border-radius:20px;white-space:nowrap;
|
||||
background:${open ? 'var(--ok-soft,#dcfce7)' : 'var(--border-h,#eee)'};color:${open ? 'var(--ok,#16a34a)' : 'var(--muted)'}">${open ? 'видит' : 'не видит'} · ${why}</span>`;
|
||||
}
|
||||
|
||||
function classRowContent(c) {
|
||||
const openToClass = _rules.classRules[c.id] === 1;
|
||||
const expanded = _open.has(c.id);
|
||||
@@ -165,7 +212,8 @@
|
||||
<div style="padding:6px 0 10px 26px">
|
||||
${students.length ? students.map(s => `
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;gap:10px;padding:5px 0;border-top:1px solid var(--border-soft,#f0f0f0)">
|
||||
<span style="font-size:13px;color:var(--text-1)">${esc(s.name || s.email)}</span>${studentTri(s.id)}
|
||||
<span style="font-size:13px;color:var(--text-1)">${esc(s.name || s.email)}</span>
|
||||
<span style="display:inline-flex;align-items:center;gap:8px">${effBadge(s.id, openToClass)}${studentTri(s.id)}</span>
|
||||
</div>`).join('') : '<p style="color:var(--muted);font-size:12px;margin:4px 0">В классе нет учеников</p>'}
|
||||
</div>` : '';
|
||||
return `
|
||||
@@ -419,6 +467,7 @@
|
||||
window.accClassBulk = classBulk;
|
||||
window.accMx = mxToggle;
|
||||
window.accMxSearch = mxSearch;
|
||||
window.accLeftSearch = leftSearch;
|
||||
|
||||
window.AdminSections = window.AdminSections || {};
|
||||
window.AdminSections.access = {
|
||||
|
||||
Reference in New Issue
Block a user