'use strict'; /* admin → permissions section (role-based teacher/student permissions) */ (function () { 'use strict'; let inited = false; let _permData = null; let _bulkTargets = null; // { classes:[{id,name,students:[]}] } let _bulkClassId = null; let _presets = null; // { student:[{id,label,desc,perms}] } let _roles = null; // [{name,label,baseRoles,isBuiltin,users}] let _roleEdit = null; // { name, base, permissions, definitions } — открытый редактор прав роли async function load() { try { _permData = await LS.getPermissions(); renderPermissions(); loadBulk(); loadRoles(); } catch(e) { document.getElementById('perm-teacher').innerHTML = `

Ошибка загрузки: ${esc(e.message)}

`; } } /* ── Массово по классу ── */ async function loadBulk() { const root = document.getElementById('perm-bulk'); if (!root) return; try { _bulkTargets = await LS.accessTargets(); } catch { _bulkTargets = { classes: [] }; } if (!_presets) { try { _presets = await LS.permissionsPresets(); } catch { _presets = { student: [] }; } } renderBulk(); } function renderBulk() { const root = document.getElementById('perm-bulk'); if (!root || !_permData) return; const classes = (_bulkTargets && _bulkTargets.classes) || []; const studentDefs = (_permData.definitions || []).filter(d => d.role === 'student'); const opts = classes.map(c => ``).join(''); const cls = classes.find(c => c.id === _bulkClassId); const btn = (key, val, label, bg) => ``; const rows = (cls ? studentDefs : []).map(d => `
${esc(d.label)} ${btn(d.key, 1, 'включить всем', 'transparent')}${btn(d.key, 0, 'выключить всем', 'transparent')}${btn(d.key, 'null', 'сбросить', 'var(--border-h,#eee)')}
`).join(''); const presets = (_presets && _presets.student) || []; const presetBar = (cls && presets.length) ? `
Пресет-профиль: ${presets.map(p => ``).join('')}
` : ''; root.innerHTML = `
Массово по классу
Выставить личное правило сразу всем ученикам класса (переопределяет роль). «Сбросить» — вернуть наследование роли.
${cls ? (presetBar + (rows || '

Нет студенческих прав.

')) : '

Выберите класс.

'}
`; } function selBulkClass(v) { _bulkClassId = v ? Number(v) : null; renderBulk(); } async function bulkPerm(permission, enabled) { if (!_bulkClassId) return; if (enabled === 'null') enabled = null; const cls = ((_bulkTargets && _bulkTargets.classes) || []).find(c => c.id === _bulkClassId); const clsName = cls ? cls.name : ('#' + _bulkClassId); if (enabled === 0) { const ok = await LS.confirm(`Выключить это право всем ученикам класса «${clsName}»?`, { title: 'Подтвердите', confirmText: 'Выключить всем' }); if (!ok) return; } try { const r = await LS.setClassPermission(_bulkClassId, permission, enabled); LS.toast(`Готово: затронуто учеников — ${r.affected}`, 'success'); } catch (e) { LS.toast('Ошибка: ' + e.message, 'error'); } } async function applyPreset(presetId) { if (!_bulkClassId) return; const cls = ((_bulkTargets && _bulkTargets.classes) || []).find(c => c.id === _bulkClassId); const p = ((_presets && _presets.student) || []).find(x => x.id === presetId); const ok = await LS.confirm( `Применить пресет «${p ? p.label : presetId}» ко всем ученикам класса «${cls ? cls.name : ''}»?`, { title: 'Применить пресет', confirmText: 'Применить' }); if (!ok) return; try { const r = await LS.applyClassPreset(_bulkClassId, presetId); LS.toast(`Пресет применён: ${r.affected} учеников`, 'success'); } catch (e) { LS.toast('Ошибка: ' + e.message, 'error'); } } function permCard(role, def, en, labelOf) { const enabled = en[def.key]; const reqs = def.requires || []; const unmet = reqs.filter(r => !en[r]); const blocked = unmet.length > 0; // зависимость не выполнена → право неактивно const effective = enabled && !blocked; const isModified = (enabled ? 1 : 0) !== def.default; const modDot = isModified ? `` : ''; const reqNote = reqs.length ? `
${blocked ? 'Требует: ' + unmet.map(r => esc(labelOf[r] || r)).join(', ') : 'Зависит от: ' + reqs.map(r => esc(labelOf[r] || r)).join(', ')}
` : ''; return `
${esc(def.label)}${modDot}
${esc(def.desc)}
${reqNote}
`; } function renderPermissions() { if (!_permData) return; const { permissions, definitions } = _permData; ['teacher', 'student'].forEach(role => { const container = document.getElementById('perm-' + role); const defs = definitions.filter(d => d.role === role); const en = {}, labelOf = {}; defs.forEach(d => { en[d.key] = permissions[role]?.[d.key] ?? d.default; labelOf[d.key] = d.label; }); // группировка по def.group (порядок групп — по первому появлению) const order = [], byGroup = {}; defs.forEach(d => { const g = d.group || 'Прочее'; if (!byGroup[g]) { byGroup[g] = []; order.push(g); } byGroup[g].push(d); }); const grpBtn = (g, on) => ``; container.style.display = 'block'; // секции групп блоками (контейнер сам — .perm-grid) container.innerHTML = order.map(g => `
${esc(g)}
${grpBtn(g, true)}${grpBtn(g, false)}
${byGroup[g].map(def => permCard(role, def, en, labelOf)).join('')}
`).join(''); }); } async function togglePermGroup(role, group, enable) { const defs = (_permData.definitions || []).filter(d => d.role === role && (d.group || 'Прочее') === group); if (!defs.length) return; if (!enable && defs.some(d => d.requireConfirmOff)) { const roleLabel = role === 'teacher' ? 'Учитель' : 'Ученик'; const ok = await LS.confirm( `Выключить все права группы «${group}» для роли «${roleLabel}»? Затронет всех пользователей роли.`, { title: 'Подтвердите выключение группы', confirmText: 'Выключить все' } ); if (!ok) return; } try { await Promise.all(defs.map(d => LS.setPermission(role, d.key, enable))); if (!_permData.permissions[role]) _permData.permissions[role] = {}; defs.forEach(d => { _permData.permissions[role][d.key] = enable; }); renderPermissions(); LS.toast(enable ? `Группа «${group}» включена` : `Группа «${group}» выключена`, 'success'); } catch (e) { LS.toast('Ошибка: ' + e.message, 'error'); load(); } } async function togglePermission(role, key, enabled, checkbox) { if (!enabled) { const def = (_permData.definitions || []).find(d => d.role === role && d.key === key); if (def && def.requireConfirmOff) { const roleLabel = role === 'teacher' ? 'Учитель' : 'Ученик'; const ok = await LS.confirm( `Выключение «${def.label}» затронет всех пользователей роли «${roleLabel}». Они потеряют доступ. Продолжить?`, { title: 'Подтвердите выключение права', confirmText: 'Выключить' } ); if (!ok) { checkbox.checked = true; return; } } } checkbox.disabled = true; try { await LS.setPermission(role, key, enabled); if (!_permData.permissions[role]) _permData.permissions[role] = {}; _permData.permissions[role][key] = enabled; const safeKey = key.replace('.', '_'); const card = document.getElementById(`perm-card-${role}-${safeKey}`); if (card) card.classList.toggle('enabled', enabled); // Re-render to refresh the modified-dot indicator across all cards. renderPermissions(); LS.toast(enabled ? 'Право включено' : 'Право отключено', 'success'); } catch(e) { checkbox.checked = !enabled; LS.toast('Ошибка: ' + e.message, 'error'); } finally { checkbox.disabled = false; } } function filterPermissions(query) { const q = (query || '').trim().toLowerCase(); ['teacher', 'student'].forEach(role => { const block = document.querySelector(`#perm-${role}`)?.closest('.perm-role-block'); const cards = document.querySelectorAll(`#perm-${role} .perm-card`); let visibleCount = 0; cards.forEach(card => { const label = (card.querySelector('.perm-label')?.textContent || '').toLowerCase(); const desc = (card.querySelector('.perm-desc')?.textContent || '').toLowerCase(); const show = !q || label.includes(q) || desc.includes(q); card.style.display = show ? '' : 'none'; if (show) visibleCount++; }); if (block) block.style.display = visibleCount === 0 ? 'none' : ''; }); } async function loadPermLog() { const box = document.getElementById('perm-log'); if (!box) return; box.innerHTML = '

Загрузка…

'; try { const log = await LS.permissionsLog(); box.innerHTML = log.length ? log.map(e => `
${esc(e.actor)} ${esc(e.text)} · ${esc((e.at || '').replace('T', ' ').slice(0, 16))}
`).join('') : '

Изменений пока нет.

'; } catch (e) { box.innerHTML = `

Ошибка: ${esc(e.message)}

`; } } /* ════════ Конструктор ролей (C-4) ════════ */ const BASE_OPTS = [['teacher', 'Учитель'], ['student', 'Ученик'], ['free_student', 'Свободный ученик'], ['admin', 'Администратор']]; async function loadRoles() { const root = document.getElementById('perm-roles'); if (!root) return; try { _roles = await LS.listRoles(); } catch { _roles = []; } renderRoles(); } function renderRoles() { const root = document.getElementById('perm-roles'); if (!root) return; const customs = (_roles || []).filter(r => !r.isBuiltin); const baseChecks = BASE_OPTS.map(([v, lbl]) => ``).join(''); const rolesList = customs.length ? customs.map(r => `
${esc(r.label)} (${esc(r.name)} · база: ${(r.baseRoles || []).join(', ')} · ${r.users} польз.)
`).join('') : '

Кастомных ролей пока нет.

'; root.innerHTML = `
Конструктор ролей
Кастомная роль проходит гейты своих «базовых ролей» и имеет свой набор прав (изначально — копия базы). Назначается пользователю в его карточке.
${baseChecks}
${rolesList}
${_roleEdit ? roleEditorHtml() : ''}
`; } function roleEditorHtml() { const defs = _roleEdit.definitions || []; const map = _roleEdit.permissions || {}; const order = [], byGroup = {}; defs.forEach(d => { const g = d.group || 'Прочее'; if (!byGroup[g]) { byGroup[g] = []; order.push(g); } byGroup[g].push(d); }); const card = (d) => `
${esc(d.label)}
${esc(d.desc)}
`; const groups = order.map(g => `
${esc(g)}
${byGroup[g].map(card).join('')}
`).join(''); return `
Права роли «${esc(_roleEdit.name)}» (база: ${esc(_roleEdit.base)})
${defs.length ? groups : '

У базы нет настраиваемых прав.

'}
`; } async function createRoleUI() { const name = (document.getElementById('nr-name').value || '').trim(); const label = (document.getElementById('nr-label').value || '').trim(); const bases = [...document.querySelectorAll('#perm-roles .nr-base:checked')].map(c => c.value); if (!name) { LS.toast('Укажите имя роли', 'error'); return; } if (!bases.length) { LS.toast('Отметьте хотя бы одну базовую роль', 'error'); return; } try { await LS.createRole(name, label || name, bases); await loadRoles(); LS.toast('Роль создана', 'success'); } catch (e) { LS.toast('Ошибка: ' + e.message, 'error'); } } async function deleteRoleUI(name) { if (!await LS.confirm(`Удалить роль «${name}»? Пользователи вернутся на её базовую роль.`, { title: 'Удалить роль', confirmText: 'Удалить' })) return; try { const r = await LS.deleteRole(name); if (_roleEdit && _roleEdit.name === name) _roleEdit = null; await loadRoles(); LS.toast(`Роль удалена (вернулось ${r.reassigned || 0} польз.)`, 'success'); } catch (e) { LS.toast('Ошибка: ' + e.message, 'error'); } } async function configureRole(name) { try { _roleEdit = await LS.rolePermissions(name); renderRoles(); } catch (e) { LS.toast('Ошибка: ' + e.message, 'error'); } } function closeRoleEditor() { _roleEdit = null; renderRoles(); } async function toggleRolePerm(roleName, key, enabled, checkbox) { checkbox.disabled = true; try { await LS.setPermission(roleName, key, enabled); if (_roleEdit && _roleEdit.name === roleName) _roleEdit.permissions[key] = enabled; const editor = document.getElementById('role-editor'); if (editor) editor.innerHTML = _roleEdit ? roleEditorHtml() : ''; LS.toast(enabled ? 'Право включено' : 'Право отключено', 'success'); } catch (e) { checkbox.checked = !enabled; LS.toast('Ошибка: ' + e.message, 'error'); } finally { checkbox.disabled = false; } } window.createRoleUI = createRoleUI; window.deleteRoleUI = deleteRoleUI; window.configureRole = configureRole; window.closeRoleEditor = closeRoleEditor; window.toggleRolePerm = toggleRolePerm; window.togglePermission = togglePermission; window.togglePermGroup = togglePermGroup; window.filterPermissions = filterPermissions; window.loadPermLog = loadPermLog; window.selBulkClass = selBulkClass; window.bulkPerm = bulkPerm; window.applyPreset = applyPreset; window.AdminSections = window.AdminSections || {}; window.AdminSections.permissions = { init: async () => { if (inited) return; inited = true; await load(); }, reload: load, }; })();