// Admin panel — gestión de permisos por rol × área (matriz editable), // usuarios y bitácora de auditoría. const { useState: useStateAd, useMemo: useMemoAd, useEffect: useEffectAd } = React; /* ------------------------------------------------------------------ */ /* Helpers de permisos */ /* ------------------------------------------------------------------ */ function getLevel(permissions, roleId, scopeId, fallbackScopeId = null) { const perms = permissions[roleId] || {}; if (scopeId in perms) return perms[scopeId]; if (fallbackScopeId && fallbackScopeId in perms) return perms[fallbackScopeId]; return "none"; } function levelInfo(permissionLevels, levelShort) { return permissionLevels.find(l => l.short === levelShort) || permissionLevels[0]; } /* ------------------------------------------------------------------ */ /* Permission pill */ /* ------------------------------------------------------------------ */ function PermPill({ level, info, onClick, mini = false, inherited = false }) { const isNone = level === "none"; const isHigh = level === "admin"; return ( ); } /* ------------------------------------------------------------------ */ /* Level Editor popover */ /* ------------------------------------------------------------------ */ function LevelEditor({ permissionLevels, current, onPick, onClose, anchor }) { if (!anchor) return null; const rect = anchor.getBoundingClientRect(); return ( <>
Cambiar nivel de acceso
{permissionLevels.map(l => ( ))}
); } /* ------------------------------------------------------------------ */ /* Admin shell */ /* ------------------------------------------------------------------ */ function AdminPanel({ sections, allFolders, roles, areas, users: initialUsers, permissionLevels, currentUser, onLogout, onBackToLibrary }) { const [tab, setTab] = useStateAd("roles"); const [permissions, setPerms] = useStateAd({}); const [loadingPerms, setLoadingPerms] = useStateAd(false); const [openRole, setOpenRole] = useStateAd(null); const [editor, setEditor] = useStateAd(null); const [query, setQuery] = useStateAd(""); const [toast, setToast] = useStateAd(null); const [users, setUsers] = useStateAd(initialUsers || []); const showToast = function(msg) { setToast(msg); setTimeout(() => setToast(null), 2000); }; // Cargar permisos de todos los roles useEffectAd(() => { if (!roles.length) return; setLoadingPerms(true); Promise.all(roles.map(r => API.Permissions.matrix(r.id))) .then(function(matrices) { const merged = {}; roles.forEach(function(r, i) { const matrix = matrices[i]?.matrix || []; merged[r.id] = {}; matrix.forEach(function(entry) { merged[r.id][entry.scope_id] = entry.level; (entry.folders || []).forEach(function(f) { if (f.level !== null) { merged[r.id][f.scope_id] = f.level; } }); }); }); setPerms(merged); }) .catch(function() {}) .finally(function() { setLoadingPerms(false); }); }, [roles]); const setLevel = async function(roleId, scopeId, levelShort, scopeType) { // Actualización optimista setPerms(function(prev) { return Object.assign({}, prev, { [roleId]: Object.assign({}, prev[roleId] || {}, { [scopeId]: levelShort }), }); }); try { await API.Permissions.patch({ role_id: roleId, scope_type: scopeType || 'section', scope_id: scopeId, level: levelShort, }); const info = levelInfo(permissionLevels, levelShort); showToast("Permiso actualizado: " + (info?.label || levelShort)); } catch(err) { showToast("Error al guardar permiso"); } }; return (
setQuery(e.target.value)} placeholder="Buscar rol, usuario o área…"/> ⌘ K
Biblioteca Administración {tab === "roles" ? "Roles y permisos" : tab === "usuarios" ? "Usuarios" : "Bitácora de auditoría"}
Panel de administración

Control de acceso

Define qué áreas y archivos puede ver, editar o administrar cada rol del sistema.

Roles
{roles.length}
Usuarios
{users.length.toLocaleString("es-MX")}
Áreas
{areas.length}
{/* Tabs */}
{tab === "roles" && ( loadingPerms ?
Cargando permisos…
: setEditor({ roleId, scopeId, scopeType, anchor })} openRole={openRole} setOpenRole={setOpenRole} /> )} {tab === "usuarios" && ( )} {tab === "auditoria" && }
{/* Popover editor */} {editor && ( setEditor(null)} anchor={editor.anchor} /> )} {toast && (
{toast}
)}
); } /* ------------------------------------------------------------------ */ /* Admin sidebar */ /* ------------------------------------------------------------------ */ function AdminSidebar({ sections, areas, roles, users, currentUser, tab, setTab, onLogout, onBackToLibrary }) { return ( ); } /* ------------------------------------------------------------------ */ /* Vista: Roles × Secciones (matriz) */ /* ------------------------------------------------------------------ */ function RolesView({ sections, allFolders, roles, permissions, permissionLevels, query, onCellClick, openRole, setOpenRole }) { const q = query.toLowerCase().trim(); const filteredRoles = q ? roles.filter(r => r.name.toLowerCase().includes(q) || (r.description||"").toLowerCase().includes(q)) : roles; return ( <> {/* Leyenda */}
Niveles de acceso
{permissionLevels.map(l => (
{l.label}
{l.desc}
))}
Matriz: roles × secciones {filteredRoles.length} roles · {sections.length} secciones
Rol / Sección
{sections.map(s => (
{s.label}
{(s.items_count||0).toLocaleString("es-MX")} archivos
))}
Detalle
{filteredRoles.map(role => (
{role.name}{role.system && SISTEMA}
{role.description}
{role.members_count || 0} {(role.members_count||0) === 1 ? "usuario" : "usuarios"}
{sections.map(s => { const level = getLevel(permissions, role.id, s.id); const info = levelInfo(permissionLevels, level); return (
onCellClick(role.id, s.id, 'section', e.currentTarget)} />
); })}
{openRole === role.id && (
{sections.map(s => { const sectionFolders = allFolders[s.id] || []; return (
{s.label}
Por defecto · {levelInfo(permissionLevels, getLevel(permissions, role.id, s.id))?.label || "Sin acceso"}
{sectionFolders.map(f => { const inherited = !((permissions[role.id] || {})[f.id]); const level = getLevel(permissions, role.id, f.id, s.id); const info = levelInfo(permissionLevels, level); return (
{f.name}
{f.items_count || 0} elementos
onCellClick(role.id, f.id, 'folder', e.currentTarget)} />
); })} {sectionFolders.length === 0 && (
Sin carpetas en esta sección
)}
); })}
)}
))}
); } /* ------------------------------------------------------------------ */ /* Vista: Usuarios */ /* ------------------------------------------------------------------ */ function UsersView({ users, roles, areas, query, onUpdate, showToast }) { const q = query.toLowerCase().trim(); const filtered = q ? users.filter(u => u.name.toLowerCase().includes(q) || u.email.toLowerCase().includes(q)) : users; const handleStatusChange = async function(user) { const newStatus = user.status === 'active' ? 'disabled' : 'active'; try { const updated = await API.Users.update(user.id, { status: newStatus }); onUpdate(updated); showToast("Estado actualizado: " + (newStatus === 'active' ? 'Activo' : 'Desactivado')); } catch(e) { showToast("Error al actualizar usuario"); } }; return ( <>
Usuarios del sistema {filtered.length} cuentas
Usuario
Rol
Área
Última actividad
Estado
{filtered.map(u => { const role = roles.find(r => r.id === (u.role?.id || u.role_id)); const area = areas.find(a => a.id === (u.area?.id || u.area_id)); const lastActive = u.last_active_at ? new Date(u.last_active_at).toLocaleDateString("es-MX", {day:"2-digit",month:"short",year:"numeric"}) : "Nunca"; return (
{u.initials || u.name.split(" ").map(n=>n[0]).slice(0,2).join("")}
{u.name}
{u.email}
{role?.name || u.role?.name || "—"}
{area?.name || u.area?.name || "—"}
{lastActive}
{u.status === "active" ? "Activo" : u.status === "pending" ? "Pendiente" : "Inactivo"}
); })}
); } /* ------------------------------------------------------------------ */ /* Vista: Auditoría (carga desde API) */ /* ------------------------------------------------------------------ */ function AuditView() { const [entries, setEntries] = useStateAd([]); const [loading, setLoading] = useStateAd(true); const [total, setTotal] = useStateAd(0); useEffectAd(() => { API.Audit.list({ per_page: 50 }) .then(function(res) { setEntries(res.data || []); setTotal(res.total || 0); }) .catch(function() {}) .finally(function() { setLoading(false); }); }, []); const handleExport = function() { window.open(API.Audit.exportCsvUrl(), "_blank"); }; if (loading) { return
Cargando auditoría…
; } return ( <>
Bitácora de cambios {total} eventos en total
{entries.length === 0 && (
Sin eventos registrados.
)} {entries.map(function(e) { return (
{e.ts}
{e.actor?.name || "Sistema"} · {e.action} {e.target_label && {e.target_label}}
{(e.from_value || e.to_value) && (
{e.from_value && {e.from_value}} {e.from_value && e.to_value && } {e.to_value && {e.to_value}}
)}
); })}
); } window.AdminPanel = AdminPanel;