@@ -1,85 +1,152 @@
'use strict' ;
/* admin → access section — открыть/закрыть доступ к учебникам и экзаменам
* для классов и отдельных учеников. Модель allowlist: по умолчанию закрыто,
* правило ученика важнее правила класса. */
* правило ученика важнее правила класса.
*
* Два режима:
* • «По контенту» — выбрать учебник/экзамен → раздать классам (+ ученики).
* • «По классу» — выбрать класс → отметить доступный ему контент. */
( function ( ) {
'use strict' ;
let inited = false ;
let _catalog = null ; // { textbooks:[], exams:[] }
let _targets = null ; // { classes:[{id,name,students:[]}], looseStudents:[] }
let _sel = null ; // { type:'textbook'|'exam', ref, title }
let _summary = { totalClasses : 0 , textbooks : { } , exams : { } } ;
let _mode = 'content' ; // 'content' | 'class'
// content-mode state
let _selContent = null ; // { type, ref, title }
let _rules = { classRules : { } , studentRules : { } } ;
const _open = new Set ( ) ; // class ids развёрнутых строк
const _open = new Set ( ) ; // развёрнутые классы (показ учеников)
// class-mode state
let _selClass = null ; // { id, name }
let _classOpen = { textbooks : new Set ( ) , exams : new Set ( ) } ;
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' ) ;
const itemsOf = ( type ) => ( type === 'textbook' ? _catalog . textbooks : _catalog . exams ) || [ ] ;
function contentTitle ( type , ref ) {
const it = itemsOf ( type ) . find ( x => x [ keyName ( type ) ] === ref ) ;
return it ? it . title : ref ;
}
async function load ( ) {
const root = document . getElementById ( 'acc-root' ) ;
try {
[ _catalog , _targets ] = await Promise . all ( [ LS . accessCatalog ( ) , LS . accessTargets ( ) ] ) ;
renderList ( ) ;
[ _catalog , _targets , _summary ] = await Promise . all ( [
LS . accessCatalog ( ) , LS . accessTargets ( ) , LS . accessSummary ( ) ,
] ) ;
renderRoot ( ) ;
} catch ( e ) {
document . getElementById ( 'acc-textbooks' ) . innerHTML =
` <p style="color:var(--danger);font-size:13px">Ошибка загрузки: ${ esc ( e . message ) } </p> ` ;
root . innerHTML = ` <p style="color:var(--danger);font-size:13px">Ошибка загрузки: ${ esc ( e . message ) } </p> ` ;
}
}
function itemBtn ( type , ref , title , sub ) {
const active = _sel && _sel . type === type && _sel . ref === ref ;
return ` <button class="acc-item ${ active ? ' active' : '' } " data-type=" ${ type } " data-ref=" ${ esc ( ref ) } "
onclick="accSelect(' ${ type } ',' ${ esc ( ref ) } ')"
style="display:block;width:100%;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 } "> ${ esc ( title ) } </span>
${ sub ? ` <span style="color:var(--muted);font-size:12px"> · ${ esc ( sub ) } </spa n> ` : '' }
</button> ` ;
/* ── каркас: переключатель режимов + две колонки ── */
function renderRoot ( ) {
const root = document . getElementById ( 'acc-root' ) ;
const seg = ( m , label ) =>
` <button onclick="accMode(' ${ m } ')" style="border:1px solid var(--border) ;
background: ${ _mode === m ? 'var(--accent,#4f46e5)' : 'transparent' } ;color: ${ _mode === m ? '#fff' : 'var(--text-3)' } ;
font-size:13px;padding:6px 16px;cursor:pointer;font-family:inherit;
${ m === 'content' ? 'border-radius:8px 0 0 8px' : 'border-radius:0 8px 8px 0;border-left:none' } "> ${ label } </butto n> ` ;
root . innerHTML = `
<div style="margin-bottom:16px;display:inline-flex">
${ seg ( 'content' , 'По контенту' ) } ${ seg ( 'class' , 'По классу' ) }
</div>
<div class="acc-layout" style="display:flex;gap:20px;align-items:flex-start;flex-wrap:wrap">
<div class="adm-panel" id="acc-left" style="flex:0 0 290px;max-width:330px;padding:10px"></div>
<div class="adm-panel" id="acc-right" style="flex:1;min-width:340px;padding:18px"></div>
</div> ` ;
renderLeft ( ) ;
renderRight ( ) ;
}
function renderList ( ) {
const tb = document . getElementById ( 'acc-textbooks' ) ;
const ex = document . getElementById ( 'acc-exams' ) ;
tb . innerHTML = ( _catalog . textbooks || [ ] )
. map ( t => itemBtn ( 'textbook' , t . slug , t . title , t . grade ? t . grade + ' кл.' : '' ) ) . join ( '' )
|| '<p style="color:var(--muted);font-size:12px;padding:6px 10px">Нет учебников</p>' ;
ex . innerHTML = ( _catalog . exams || [ ] )
. map ( e => itemBtn ( 'exam' , e . exam _key , e . title , e . grade ? e . grade + ' кл.' : '' ) ) . join ( '' )
|| '<p style="color:var(--muted);font-size:12px;padding:6px 10px">Нет экзаменов</p>' ;
/* ── badge «N/M» ── */
function badge ( open , total ) {
const has = open > 0 ;
return ` <span style="font-size:11px;font-weight:600;padding:2px 8px;border-radius:20px;
background: ${ has ? 'var(--ok-soft,#dcfce7)' : 'var(--border-h,#eee)' } ;
color: ${ has ? 'var(--ok,#16a34a)' : 'var(--muted)' } "> ${ open } / ${ total } </span> ` ;
}
async function select ( type , ref ) {
const src = type === 'textbook' ? _catalog . textbooks : _catalog . exams ;
const keyName = type === 'textbook' ? 'slug' : 'exam_key' ;
const item = ( src || [ ] ) . find ( x => x [ keyName ] === ref ) ;
_sel = { type , ref , title : item ? item . title : ref } ;
renderList ( ) ;
document . getElementById ( 'acc-detail-empty' ) . style . display = 'none' ;
const det = document . getElementById ( 'acc-detail' ) ;
det . style . display = '' ;
det . innerHTML = '<p style="color:var(--muted);font-size:13px">Загрузка…</p>' ;
/* ── ЛЕВАЯ колонка ── */
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 ( 'Нет экзаменов' ) } ` ;
} else {
const classes = _targets . classes || [ ] ;
left . innerHTML = `
<div style="font-weight:600;font-size:13px;color:var(--text-3);padding:6px 8px">Классы</div>
${ classes . length ? classes . map ( c => {
const active = _selClass && _selClass . id === c . id ;
return ` <button class="acc-item ${ active ? ' active' : '' } " onclick="accSelClass( ${ c . id } )"
style="display:block;width:100%;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 } "> ${ esc ( c . name ) } </span>
${ c . teacher _name ? ` <span style="color:var(--muted);font-size:12px"> · ${ esc ( c . teacher _name ) } </span> ` : '' } </button> ` ;
} ).join('') : empty('Нет классов')} ` ;
}
}
const empty = ( t ) => ` <p style="color:var(--muted);font-size:12px;padding:6px 10px"> ${ t } </p> ` ;
/* ── ПРАВАЯ колонка ── */
function renderRight ( ) {
const right = document . getElementById ( 'acc-right' ) ;
if ( _mode === 'content' ) {
if ( ! _selContent ) { right . innerHTML = hint ( 'Выберите учебник или экзамен слева, чтобы настроить доступ.' ) ; return ; }
renderContentDetail ( right ) ;
} else {
if ( ! _selClass ) { right . innerHTML = hint ( 'Выберите класс слева, чтобы открыть ему учебники и экзамены.' ) ; return ; }
renderClassDetail ( right ) ;
}
}
const hint = ( t ) => ` <div style="color:var(--muted);font-size:14px"> ${ t } </div> ` ;
/* ════════ режим «По контенту» ════════ */
async function selContent ( type , ref ) {
_selContent = { type , ref , title : contentTitle ( type , ref ) } ;
renderLeft ( ) ;
const right = document . getElementById ( 'acc-right' ) ;
right . innerHTML = '<p style="color:var(--muted);font-size:13px">Загрузка…</p>' ;
try {
_rules = await LS . accessRules ( type , ref ) ;
renderDetail ( ) ;
} catch ( e ) {
det . innerHTML = ` <p style="color:var(--danger);font-size:13px">Ошибка: ${ esc ( e . message ) } </p> ` ;
}
renderRight ( ) ;
} catch ( e ) { right . innerHTML = ` <p style="color:var(--danger);font-size:13px">Ошибка: ${ esc ( e . message ) } </p> ` ; }
}
/* tri-state кнопки для ученика внутри класса */
function studentTri ( uid ) {
const v = _rules . studentRules [ uid ] ; // 1 | 0 | undefined
const v = _rules . studentRules [ uid ] ;
const state = v === 1 ? 'open' : v === 0 ? 'closed' : 'inherit' ;
const btn = ( val , label , on ) =>
` <button onclick="accSetStudent( ${ uid } , ${ val } )"
style="border:1px solid var(--border);background: ${ on ? 'var(--accent,#4f46e5)' : 'transparent' } ;
color: ${ on ? '#fff' : 'var(--text-3)' } ;font-size:11.5px;padding:3px 9px;cursor:pointer;font-family:inherit;
${ val === " null" ? 'border-radius:7px 0 0 7px' : val === 0 ? 'border-radius:0 7px 7px 0;border-left:none' : 'border-left:none' } "> ${ label } </button> ` ;
return ` <span class="acc-tri" style="display:inline-flex">
${ btn ( 'null' , 'Наследовать' , state === 'inherit' ) }
${ btn ( 1 , 'Открыт' , state === 'open' ) }
${ btn ( 0 , 'Закрыт' , state === 'closed' ) }
</span> ` ;
${ val === ' null' ? 'border-radius:7px 0 0 7px' : val === 0 ? 'border-radius:0 7px 7px 0;border-left:none' : 'border-left:none' } "> ${ label } </button> ` ;
return ` <span style="display:inline-flex">
${ btn ( 'null' , 'Наследовать' , state === 'inherit' ) } ${ btn ( 1 , 'Открыт' , state === 'open' ) } ${ btn ( 0 , 'Закрыт' , state === 'closed' ) } </span> ` ;
}
function classRow ( c ) {
function classRowContent ( c ) {
const openToClass = _rules . classRules [ c . id ] === 1 ;
const expanded = _open . has ( c . id ) ;
const students = c . students || [ ] ;
@@ -87,25 +154,22 @@
<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 ) }
</div> ` ) . join ( '' )
: '<p style="color:var(--muted);font-size:12px;margin:4px 0">В классе нет учеников</p>' }
<span style="font-size:13px;color:var(--text-1)"> ${ esc ( s . name || s . email ) } </span> ${ studentTri ( s . id ) }
</div> ` ) . join ( '' ) : '<p style="color:var(--muted);font-size:12px;margin:4px 0">В классе нет учеников</p>' }
</div> ` : '' ;
return `
<div class="acc-class" style="border:1px solid var(--border);border-radius:10px;margin-bottom:10px;padding:10px 12px">
<div style="border:1px solid var(--border);border-radius:10px;margin-bottom:10px;padding:10px 12px">
<div style="display:flex;align-items:center;justify-content:space-between;gap:12px">
<button onclick="accToggleExpand( ${ c . id } )"
style="border:none;background:transparent;cursor:pointer;font-family:inherit;font-size:14px;font-weight:600;color:var(--text-1);display:flex;align-items:center;gap:6px">
<span style="display:inline-block;transition:transform .15s;transform:rotate( ${ expanded ? 90 : 0 } deg)">▸</span>
${ esc ( c . name ) } ${ c . teacher _name ? ` <span style="font-weight:400;color:var(--muted);font-size:12px">· ${ esc ( c . teacher _name ) } </span> ` : '' }
</button>
<label class="acc-switch" style="display:inline-flex;align-items:center;gap:8px;font-size:12.5px;color:var(--text-3);cursor:pointer">
<label style="display:inline-flex;align-items:center;gap:8px;font-size:12.5px;color:var(--text-3);cursor:pointer">
<span> ${ openToClass ? 'Открыт' : 'Закрыт' } </span>
<input type="checkbox" ${ openToClass ? 'checked' : '' } onchange="accSetClass( ${ c . id } , this.checked)">
</label>
</div>
${ studentsHtml }
</div> ${ studentsHtml }
</div> ` ;
}
@@ -121,60 +185,163 @@
</div> ` ;
}
function renderDetail ( ) {
const det = document . getElementById ( 'acc-detail' ) ;
function renderContentDetail ( right ) {
const classes = _targets . classes || [ ] ;
const loose = _targets . looseStudents || [ ] ;
det . innerHTML = `
right . innerHTML = `
<div style="display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:14px">
<div style="font-size:16px;font-weight:700;color:var(--text-1)"> ${ esc ( _sel . title ) } </div>
<span class="badge ${ _sel . type === 'exam' ? 'badge-warn' : 'badge-info' } " style="font-size:12px"> ${ _sel . type === 'exam' ? 'Экзамен' : 'Учебник' } </span>
</div>
<div class="acc-classes">
${ classes . length ? classes . map ( classRow ) . join ( '' )
: '<p style="color:var(--muted);font-size:13px">Нет классов.</p>' }
<div style="font-size:16px;font-weight:700;color:var(--text-1)"> ${ esc ( _selContent . title ) } </div>
<span class="badge ${ _selContent . type === 'exam' ? 'badge-warn' : 'badge-info' } " style="font-size:12px"> ${ _selContent . type === 'exam' ? 'Экзамен' : 'Учебник' } </span>
</div>
${ classes . length ? `
<div style="display:flex;gap:8px;margin-bottom:14px">
<button class="adm-btn adm-btn-small" onclick="accBulk(1)">Открыть всем классам</button>
<button class="adm-btn adm-btn-small" style="background:var(--border-h);color:var(--text-3)" onclick="accBulk(0)">Закрыть у всех</button>
</div> ` : '' }
${ classes . length ? classes . map ( classRowContent ) . join ( '' ) : '<p style="color:var(--muted);font-size:13px">Нет классов.</p>' }
${ loose . length ? `
<div style="margin-top:18px">
<div style="font-weight:600;font-size:13px;color:var(--text-3);margin-bottom:8px">Отдельные ученики (без класса)</div>
${ loose . map ( looseRow ) . join ( '' ) }
</div> ` : '' }
` ;
</div> ` : '' } ` ;
}
/* пересчёт бейджа для текущего контента по отображаемым классам */
function recountContent ( ) {
if ( ! _selContent ) return ;
const open = ( _targets . classes || [ ] ) . filter ( c => _rules . classRules [ c . id ] === 1 ) . length ;
_summary [ bucket ( _selContent . type ) ] [ _selContent . ref ] = open ;
}
/* ── handlers (optimistic update) ── */
async function setClass ( classId , checked ) {
const allow = checked ? 1 : null ;
try {
await LS . accessSetRule ( _sel . type , _sel . ref , 'class' , classId , allow ) ;
if ( allow === 1 ) _rules . classRules [ classId ] = 1 ;
else delete _rules . classRules [ classId ] ;
renderDetail ( ) ;
LS . toast ( checked ? 'Открыт классу' : 'Закрыт для класса' , 'success' ) ;
} catch ( e ) { LS . toast ( 'Ошибка: ' + e . message , 'error' ) ; renderDetail ( ) ; }
await LS . accessSetRule ( _selContent . type , _selContent . ref , 'class' , classId , allow ) ;
if ( allow === 1 ) _rules . classRules [ classId ] = 1 ; else delete _rules . classRules [ classId ] ;
recountContent ( ) ; renderLeft ( ) ; renderRight ( ) ;
} catch ( e ) { LS . toast ( 'Ошибка: ' + e . message , 'error' ) ; renderRight ( ) ; }
}
async function bulk ( allow ) {
const classes = _targets . classes || [ ] ;
try {
await Promise . all ( classes . map ( c =>
LS . accessSetRule ( _selContent . type , _selContent . ref , 'class' , c . id , allow ? 1 : null ) ) ) ;
for ( const c of classes ) { if ( allow ) _rules . classRules [ c . id ] = 1 ; else delete _rules . classRules [ c . id ] ; }
recountContent ( ) ; renderLeft ( ) ; renderRight ( ) ;
LS . toast ( allow ? 'Открыто всем классам' : 'Закрыто у всех классов' , 'success' ) ;
} catch ( e ) { LS . toast ( 'Ошибка: ' + e . message , 'error' ) ; selContent ( _selContent . type , _selContent . ref ) ; }
}
async function setStudent ( uid , allow ) {
// allow: 1 | 0 | null (строка 'null' приходит из tri-кнопок)
if ( allow === 'null' ) allow = null ;
try {
await LS . accessSetRule ( _sel . type , _sel . ref , 'student' , uid , allow ) ;
await LS . accessSetRule ( _selContent . type , _selContent . ref , 'student' , uid , allow ) ;
if ( allow === 1 ) _rules . studentRules [ uid ] = 1 ;
else if ( allow === 0 ) _rules . studentRules [ uid ] = 0 ;
else delete _rules . studentRules [ uid ] ;
renderDetail ( ) ;
} catch ( e ) { LS . toast ( 'Ошибка: ' + e . message , 'error' ) ; renderDetail ( ) ; }
renderRight ( ) ;
} catch ( e ) { LS . toast ( 'Ошибка: ' + e . message , 'error' ) ; renderRight ( ) ; }
}
function toggleExpand ( classId ) {
if ( _open . has ( classId ) ) _open . delete ( classId ) ; else _open . add ( classId ) ;
renderDetail ( ) ;
renderRight ( ) ;
}
window . accSelect = select ;
/* ════════ режим «По классу» ════════ */
async function selClass ( id ) {
const c = ( _targets . classes || [ ] ) . find ( x => x . id === id ) ;
_selClass = { id , name : c ? c . name : ( '#' + id ) } ;
renderLeft ( ) ;
const right = document . getElementById ( 'acc-right' ) ;
right . innerHTML = '<p style="color:var(--muted);font-size:13px">Загрузка…</p>' ;
try {
const open = await LS . accessClassOpen ( id ) ;
_classOpen = { textbooks : new Set ( open . textbooks || [ ] ) , exams : new Set ( open . exams || [ ] ) } ;
renderRight ( ) ;
} catch ( e ) { right . innerHTML = ` <p style="color:var(--danger);font-size:13px">Ошибка: ${ esc ( e . message ) } </p> ` ; }
}
function classContentRow ( type , it ) {
const ref = it [ keyName ( type ) ] ;
const open = _classOpen [ bucket ( type ) ] . has ( ref ) ;
return `
<div style="display:flex;align-items:center;justify-content:space-between;gap:10px;padding:8px 12px;border:1px solid var(--border);border-radius:9px;margin-bottom:6px">
<span style="font-size:13.5px;color:var(--text-1)"> ${ esc ( it . title ) }
<span class="badge ${ type === 'exam' ? 'badge-warn' : 'badge-info' } " style="font-size:10.5px;margin-left:6px"> ${ type === 'exam' ? 'Экзамен' : 'Учебник' } </span></span>
<label style="display:inline-flex;align-items:center;gap:8px;font-size:12.5px;color:var(--text-3);cursor:pointer">
<span> ${ open ? 'Открыт' : 'Закрыт' } </span>
<input type="checkbox" ${ open ? 'checked' : '' } onchange="accClassToggle(' ${ type } ',' ${ esc ( ref ) } ', this.checked)">
</label>
</div> ` ;
}
function renderClassDetail ( right ) {
const tb = _catalog . textbooks || [ ] , ex = _catalog . exams || [ ] ;
right . innerHTML = `
<div style="font-size:16px;font-weight:700;color:var(--text-1);margin-bottom:14px">Класс « ${ esc ( _selClass . name ) } »</div>
<div style="display:flex;gap:8px;margin-bottom:14px">
<button class="adm-btn adm-btn-small" onclick="accClassBulk(1)">Открыть весь контент</button>
<button class="adm-btn adm-btn-small" style="background:var(--border-h);color:var(--text-3)" onclick="accClassBulk(0)">Закрыть весь</button>
</div>
<div style="font-weight:600;font-size:13px;color:var(--text-3);margin:4px 0 8px">Учебники</div>
${ tb . length ? tb . map ( it => classContentRow ( 'textbook' , it ) ) . join ( '' ) : empty ( 'Нет учебников' ) }
<div style="font-weight:600;font-size:13px;color:var(--text-3);margin:14px 0 8px">Экзамены</div>
${ ex . length ? ex . map ( it => classContentRow ( 'exam' , it ) ) . join ( '' ) : empty ( 'Нет экзаменов' ) } ` ;
}
function bumpSummary ( type , ref , delta ) {
const b = _summary [ bucket ( type ) ] ;
const cur = b [ ref ] || 0 ;
b [ ref ] = Math . max ( 0 , Math . min ( _summary . totalClasses || 0 , cur + delta ) ) ;
}
async function classToggle ( type , ref , checked ) {
try {
await LS . accessSetRule ( type , ref , 'class' , _selClass . id , checked ? 1 : null ) ;
const set = _classOpen [ bucket ( type ) ] ;
const was = set . has ( ref ) ;
if ( checked ) set . add ( ref ) ; else set . delete ( ref ) ;
if ( checked && ! was ) bumpSummary ( type , ref , + 1 ) ;
if ( ! checked && was ) bumpSummary ( type , ref , - 1 ) ;
renderRight ( ) ;
} catch ( e ) { LS . toast ( 'Ошибка: ' + e . message , 'error' ) ; selClass ( _selClass . id ) ; }
}
async function classBulk ( allow ) {
const all = [ ... ( _catalog . textbooks || [ ] ) . map ( it => [ 'textbook' , it [ keyName ( 'textbook' ) ] ] ) ,
... ( _catalog . exams || [ ] ) . map ( it => [ 'exam' , it [ keyName ( 'exam' ) ] ] ) ] ;
try {
await Promise . all ( all . map ( ( [ type , ref ] ) =>
LS . accessSetRule ( type , ref , 'class' , _selClass . id , allow ? 1 : null ) ) ) ;
for ( const [ type , ref ] of all ) {
const set = _classOpen [ bucket ( type ) ] ;
const was = set . has ( ref ) ;
if ( allow ) { set . add ( ref ) ; if ( ! was ) bumpSummary ( type , ref , + 1 ) ; }
else { set . delete ( ref ) ; if ( was ) bumpSummary ( type , ref , - 1 ) ; }
}
renderRight ( ) ;
LS . toast ( allow ? 'Открыт весь контент классу' : 'Закрыт весь контент' , 'success' ) ;
} catch ( e ) { LS . toast ( 'Ошибка: ' + e . message , 'error' ) ; selClass ( _selClass . id ) ; }
}
/* ── режим ── */
function setMode ( m ) {
if ( m === _mode ) return ;
_mode = m ;
renderRoot ( ) ;
}
window . accMode = setMode ;
window . accSelContent = selContent ;
window . accSetClass = setClass ;
window . accBulk = bulk ;
window . accSetStudent = setStudent ;
window . accToggleExpand = toggleExpand ;
window . accSelClass = selClass ;
window . accClassToggle = classToggle ;
window . accClassBulk = classBulk ;
window . AdminSections = window . AdminSections || { } ;
window . AdminSections . access = {