// 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 (
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.
Usuarios
{users.length.toLocaleString("es-MX")}
{/* 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 => (
))}
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("")}
{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;