feature/rbac y perfiles implementados

This commit is contained in:
2026-05-21 08:00:43 -06:00
parent 546a411df8
commit dc5f9fd6ce
29 changed files with 2007 additions and 977 deletions

View File

@@ -1,23 +1,21 @@
import React, { useState, useEffect } from 'react';
import { Link, useLocation, useNavigate } from 'react-router-dom';
import { useUser } from '../context/UserContext';
import { fetchWithAuth } from '../fetchWithAuth';
export default function Sidebar({ isMobileOpen, onMobileClose }) {
// Leer si el usuario es importador desde localStorage
const isImportador = typeof window !== 'undefined' && localStorage.getItem('user_is_importador') === 'true';
// Leer grupos del usuario desde localStorage
let userGroups = [];
if (typeof window !== 'undefined') {
try {
userGroups = JSON.parse(localStorage.getItem('user_groups') || '[]');
} catch {
userGroups = [];
}
}
// Si los grupos son exactamente [3,5]
const isGroup35 = Array.isArray(userGroups) && userGroups.length === 2 && userGroups.includes(3) && userGroups.includes(5);
// Leer DEBUG_MODE desde variables de entorno
const isDebugMode = import.meta.env.VITE_DEBUG_MODE === 'true';
// Permisos RBAC — cargados desde /rbac/my-permissions/ al hacer login
const [userPermissions, setUserPermissions] = useState(() => {
try {
return JSON.parse(localStorage.getItem('user_permissions') || '[]');
} catch {
return [];
}
});
const hasPermission = (codename) => userPermissions.includes(codename);
// Estados para responsividad
const [isCollapsed, setIsCollapsed] = useState(false);
@@ -34,6 +32,14 @@ export default function Sidebar({ isMobileOpen, onMobileClose }) {
const handleLogout = () => {
localStorage.removeItem('access');
localStorage.removeItem('refresh');
localStorage.removeItem('user_id');
localStorage.removeItem('user_is_importador');
localStorage.removeItem('user_groups');
localStorage.removeItem('user_permissions');
localStorage.removeItem('username');
localStorage.removeItem('user_email');
localStorage.removeItem('user_first_name');
localStorage.removeItem('user_last_name');
window.dispatchEvent(new CustomEvent('authStateChanged'));
navigate('/login');
};
@@ -55,6 +61,22 @@ export default function Sidebar({ isMobileOpen, onMobileClose }) {
handleMobileClose();
}, [location.pathname]);
// Si no hay permisos en localStorage (sesión previa al RBAC), los carga del servidor
useEffect(() => {
if (userPermissions.length === 0 && localStorage.getItem('access')) {
const apiUrl = import.meta.env.VITE_EFC_API_URL || '';
fetchWithAuth(`${apiUrl}/rbac/my-permissions/`)
.then(res => res.ok ? res.json() : null)
.then(data => {
if (data && Array.isArray(data.permissions)) {
localStorage.setItem('user_permissions', JSON.stringify(data.permissions));
setUserPermissions(data.permissions);
}
})
.catch(() => {});
}
}, []);
// El usuario y loading ahora vienen del contexto global
// Definir todas las secciones
@@ -71,9 +93,8 @@ export default function Sidebar({ isMobileOpen, onMobileClose }) {
</svg>
)
},
// Ocultar 'Mi Organización' si es importador o si esGroup35
...(
(!isImportador && !isGroup35)
hasPermission('organizacion.view')
? [
{
name: 'Mi Organización',
@@ -101,15 +122,19 @@ export default function Sidebar({ isMobileOpen, onMobileClose }) {
</svg>
)
},
{
name: 'Auditor',
path: '/auditor',
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01" />
</svg>
)
}
...(
hasPermission('auditoria.view')
? [{
name: 'Auditor',
path: '/auditor',
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01" />
</svg>
)
}]
: []
)
]
},
{
@@ -158,9 +183,9 @@ export default function Sidebar({ isMobileOpen, onMobileClose }) {
}
]
},
// Nueva sección Tableros - Solo mostrar si DEBUG_MODE es true
// Nueva sección Tableros - Solo mostrar si DEBUG_MODE es true y tiene cards.view
...(
(isDebugMode && !isGroup35)
(isDebugMode && hasPermission('cards.view'))
? [
{
title: 'Tableros',
@@ -179,71 +204,70 @@ export default function Sidebar({ isMobileOpen, onMobileClose }) {
]
: []
),
...(
isGroup35
? []
: [
{
title: 'Acceso a Usuarios',
items: [
// Botón Importadores como primer elemento
{
name: 'Importadores',
path: '/importers',
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M16 7a4 4 0 01-8 0M12 11v10m-6 0h12a2 2 0 002-2v-5a2 2 0 00-2-2H6a2 2 0 00-2 2v5a2 2 0 002 2z" />
</svg>
)
},
...(
isImportador
? []
: [
{
name: 'Usuarios',
path: '/users',
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z" />
</svg>
)
}
]
),
{
name: 'Ventanilla Única',
path: '/vucem',
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="2" fill="none" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M8 12h8M12 8v8" />
</svg>
)
}
]
}
]
)
{
title: 'Acceso a Usuarios',
items: [
...(
hasPermission('importadores.view')
? [{
name: 'Importadores',
path: '/importers',
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M16 7a4 4 0 01-8 0M12 11v10m-6 0h12a2 2 0 002-2v-5a2 2 0 00-2-2H6a2 2 0 00-2 2v5a2 2 0 002 2z" />
</svg>
)
}]
: []
),
...(
hasPermission('usuarios.view')
? [{
name: 'Usuarios',
path: '/users',
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z" />
</svg>
)
}]
: []
),
...(
hasPermission('usuarios.manage_roles')
? [{
name: 'Perfiles',
path: '/profiles',
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
)
}]
: []
),
...(
hasPermission('vucem.view')
? [{
name: 'Ventanilla Única',
path: '/vucem',
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="2" fill="none" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M8 12h8M12 8v8" />
</svg>
)
}]
: []
),
]
}
];
// Filtrar secciones según si es importador y modo debug
// Modificar items según si es importador
const menuSections = allMenuSections
.map(section => {
if (section.title === 'Organización') {
return {
...section,
items: section.items.filter(item => !(isImportador && item.name === 'Mi Organización'))
};
}
// Para Tableros, filtrar la sección si es importador o si no está en modo debug
if (section.title === 'Tableros' && (isImportador || !isDebugMode)) {
return null;
}
return section;
})
.filter(Boolean);
// Ocultar secciones que no tienen ningún item visible
const menuSections = allMenuSections.filter(
section => section && section.items && section.items.length > 0
);
return (
<>