Учитель
diff --git a/frontend/js/admin/sections/permissions.js b/frontend/js/admin/sections/permissions.js
index ce0bcb0..2bb820f 100644
--- a/frontend/js/admin/sections/permissions.js
+++ b/frontend/js/admin/sections/permissions.js
@@ -8,11 +8,15 @@
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)}
`;
@@ -220,6 +224,109 @@
}
}
+ /* ════════ Конструктор ролей (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;
diff --git a/frontend/js/admin/sections/users.js b/frontend/js/admin/sections/users.js
index c18a300..14bec6a 100644
--- a/frontend/js/admin/sections/users.js
+++ b/frontend/js/admin/sections/users.js
@@ -6,6 +6,7 @@
let _usersPage = 1;
const _USERS_PER_PAGE = 50;
+ let _customRoles = []; // кастомные роли для выпадающего списка назначения
/* ── one-time CSS injection for hover row-actions (shared with sessions) ── */
function ensureRowActionsStyles() {
@@ -65,6 +66,7 @@
try {
const r = await LS.adminGetUsers({ page: _usersPage, limit: _USERS_PER_PAGE });
const users = r.users || [];
+ try { _customRoles = (await LS.listRoles()).filter(x => !x.isBuiltin); } catch { _customRoles = []; }
const tbody = document.getElementById('users-body');
if (!users.length) {
tbody.innerHTML = '
Пользователей нет |
';
@@ -77,10 +79,11 @@
const avatarBg = u.role==='admin' ? 'linear-gradient(135deg,#9B5DE5,#c084fc)' : u.role==='teacher' ? 'linear-gradient(135deg,#06D6E0,#9B5DE5)' : u.role==='free_student' ? 'linear-gradient(135deg,#10B981,#059669)' : 'linear-gradient(135deg,#8898AA,#3D4F6B)';
const roleCell = isAdmin && u.id !== user.id
? `
`
: `
${{student:'Ученик',free_student:'Своб. ученик',teacher:'Учитель',admin:'Админ'}[u.role]||u.role}`;
return `
diff --git a/js/api.js b/js/api.js
index a9c5b7c..88c6d34 100644
--- a/js/api.js
+++ b/js/api.js
@@ -1034,6 +1034,7 @@ window.LS = {
getFolderAccess, clearFolderAccess, assignFolder, unassignFolder, getStudentsList,
submitWork, resubmitWork, getMySubmissions, getClassSubmissions, reviewSubmission, deleteSubmission, submissionDownloadUrl,
getPermissions, permissionsLog, setClassPermission, permissionsPresets, applyClassPreset, setPermission, getUserPermissions, setUserPermission, resetUserPermissions,
+ listRoles, createRole, updateRoleDef, deleteRole, rolePermissions,
accessCatalog, accessTargets, accessSummary, accessClassOpen, accessMatrix, accessLog, accessRules, accessSetRule,
getCourseTemplates, saveCourseTemplate, createFromCourseTemplate, deleteCourseTemplate,
getLessonTemplates, saveLessonTemplate, createFromLessonTemplate, deleteLessonTemplate,
@@ -1298,6 +1299,11 @@ async function getPermissions() { return req('GET',
async function permissionsLog(userId) { return req('GET', userId ? `/permissions/log?user_id=${userId}` : '/permissions/log'); }
async function setClassPermission(classId, permission, enabled) { return req('POST', `/permissions/class/${classId}/bulk`, { permission, enabled }); }
async function permissionsPresets() { return req('GET', '/permissions/presets'); }
+async function listRoles() { return req('GET', '/roles'); }
+async function createRole(name, label, baseRoles) { return req('POST', '/roles', { name, label, baseRoles }); }
+async function updateRoleDef(name, body) { return req('PUT', `/roles/${encodeURIComponent(name)}`, body); }
+async function deleteRole(name) { return req('DELETE', `/roles/${encodeURIComponent(name)}`); }
+async function rolePermissions(name) { return req('GET', `/roles/${encodeURIComponent(name)}/permissions`); }
async function applyClassPreset(classId, preset) { return req('POST', `/permissions/class/${classId}/preset`, { preset }); }
async function setPermission(role, permission, enabled) { return req('POST', '/permissions', { role, permission, enabled }); }
async function getUserPermissions(uid) { return req('GET', `/permissions/users/${uid}`); }