Feat: Se modifican visual de menú reportes

- Se limita la visual del menú de reportes a los usuarios según los permisos de acceso.
- Se soluciona bug en mensaje al ingresar usuario y/o clave inválidos.
This commit is contained in:
2025-08-11 15:42:23 -03:00
parent 2e7d1e36be
commit b594a48fde
2 changed files with 77 additions and 95 deletions

View File

@@ -3,92 +3,85 @@ import { Box, Paper, Typography, List, ListItemButton, ListItemText, Collapse, C
import { Outlet, useNavigate, useLocation } from 'react-router-dom'; import { Outlet, useNavigate, useLocation } from 'react-router-dom';
import ExpandLess from '@mui/icons-material/ExpandLess'; import ExpandLess from '@mui/icons-material/ExpandLess';
import ExpandMore from '@mui/icons-material/ExpandMore'; import ExpandMore from '@mui/icons-material/ExpandMore';
// --- INICIO DE LA MODIFICACIÓN ---
import { usePermissions } from '../../hooks/usePermissions';
// --- FIN DE LA MODIFICACIÓN ---
// Definición de los módulos de reporte con sus categorías, etiquetas y rutas // --- INICIO DE LA MODIFICACIÓN ---
const allReportModules: { category: string; label: string; path: string }[] = [ // Ahora cada reporte tiene su permiso requerido asociado.
{ category: 'Existencia Papel', label: 'Existencia de Papel', path: 'existencia-papel' }, const allReportModules: { category: string; label: string; path: string; requiredPermission: string; }[] = [
{ category: 'Movimientos Bobinas', label: 'Movimiento de Bobinas', path: 'movimiento-bobinas' }, { category: 'Existencia Papel', label: 'Existencia de Papel', path: 'existencia-papel', requiredPermission: 'RR005' },
{ category: 'Movimientos Bobinas', label: 'Mov. Bobinas por Estado', path: 'movimiento-bobinas-estado' }, { category: 'Movimientos Bobinas', label: 'Movimiento de Bobinas', path: 'movimiento-bobinas', requiredPermission: 'RR006' },
{ category: 'Listados Distribución', label: 'Distribución Distribuidores', path: 'listado-distribucion-distribuidores' }, { category: 'Movimientos Bobinas', label: 'Mov. Bobinas por Estado', path: 'movimiento-bobinas-estado', requiredPermission: 'RR006' },
{ category: 'Listados Distribución', label: 'Distribución Canillas', path: 'listado-distribucion-canillas' }, { category: 'Listados Distribución', label: 'Distribución Distribuidores', path: 'listado-distribucion-distribuidores', requiredPermission: 'RR002' },
{ category: 'Listados Distribución', label: 'Distribución General', path: 'listado-distribucion-general' }, { category: 'Listados Distribución', label: 'Distribución Canillas', path: 'listado-distribucion-canillas', requiredPermission: 'RR002' },
{ category: 'Listados Distribución', label: 'Distrib. Canillas (Importe)', path: 'listado-distribucion-canillas-importe' }, { category: 'Listados Distribución', label: 'Distribución General', path: 'listado-distribucion-general', requiredPermission: 'RR002' },
{ category: 'Listados Distribución', label: 'Detalle Distribución Canillas', path: 'detalle-distribucion-canillas' }, { category: 'Listados Distribución', label: 'Distrib. Canillas (Importe)', path: 'listado-distribucion-canillas-importe', requiredPermission: 'RR002' },
{ category: 'Secretaría', label: 'Venta Mensual Secretaría', path: 'venta-mensual-secretaria' }, { category: 'Listados Distribución', label: 'Detalle Distribución Canillas', path: 'detalle-distribucion-canillas', requiredPermission: 'MC005' },
{ category: 'Tiradas por Publicación', label: 'Tiradas Publicación/Sección', path: 'tiradas-publicaciones-secciones' }, { category: 'Secretaría', label: 'Venta Mensual Secretaría', path: 'venta-mensual-secretaria', requiredPermission: 'RR002' },
{ category: 'Consumos Bobinas', label: 'Consumo Bobinas/Sección', path: 'consumo-bobinas-seccion' }, { category: 'Tiradas por Publicación', label: 'Tiradas Publicación/Sección', path: 'tiradas-publicaciones-secciones', requiredPermission: 'RR008' },
{ category: 'Consumos Bobinas', label: 'Consumo Bobinas/PubPublicación', path: 'consumo-bobinas-publicacion' }, { category: 'Consumos Bobinas', label: 'Consumo Bobinas/Sección', path: 'consumo-bobinas-seccion', requiredPermission: 'RR007' },
{ category: 'Consumos Bobinas', label: 'Comparativa Consumo Bobinas', path: 'comparativa-consumo-bobinas' }, { category: 'Consumos Bobinas', label: 'Consumo Bobinas/Publicación', path: 'consumo-bobinas-publicacion', requiredPermission: 'RR007' },
{ category: 'Balance de Cuentas', label: 'Cuentas Distribuidores', path: 'cuentas-distribuidores' }, { category: 'Consumos Bobinas', label: 'Comparativa Consumo Bobinas', path: 'comparativa-consumo-bobinas', requiredPermission: 'RR007' },
{ category: 'Ctrl. Devoluciones', label: 'Control de Devoluciones', path: 'control-devoluciones' }, { category: 'Balance de Cuentas', label: 'Cuentas Distribuidores', path: 'cuentas-distribuidores', requiredPermission: 'RR001' },
{ category: 'Novedades de Canillitas', label: 'Novedades de Canillitas', path: 'novedades-canillas' }, { category: 'Ctrl. Devoluciones', label: 'Control de Devoluciones', path: 'control-devoluciones', requiredPermission: 'RR003' },
{ category: 'Listados Distribución', label: 'Dist. Mensual Can/Acc', path: 'listado-distribucion-mensual' }, { category: 'Novedades de Canillitas', label: 'Novedades de Canillitas', path: 'novedades-canillas', requiredPermission: 'RR004' },
{ category: 'Suscripciones', label: 'Facturas para Publicidad', path: 'suscripciones-facturas-publicidad' }, { category: 'Listados Distribución', label: 'Dist. Mensual Can/Acc', path: 'listado-distribucion-mensual', requiredPermission: 'RR009' },
{ category: 'Suscripciones', label: 'Distribución de Suscripciones', path: 'suscripciones-distribucion' }, { category: 'Suscripciones', label: 'Facturas para Publicidad', path: 'suscripciones-facturas-publicidad', requiredPermission: 'RR010' },
{ category: 'Suscripciones', label: 'Distribución de Suscripciones', path: 'suscripciones-distribucion', requiredPermission: 'RR011' },
]; ];
// --- FIN DE LA MODIFICACIÓN ---
const predefinedCategoryOrder = [ const predefinedCategoryOrder = [
'Balance de Cuentas', 'Balance de Cuentas', 'Listados Distribución', 'Ctrl. Devoluciones',
'Listados Distribución', 'Novedades de Canillitas', 'Suscripciones', 'Existencia Papel',
'Ctrl. Devoluciones', 'Movimientos Bobinas', 'Consumos Bobinas', 'Tiradas por Publicación', 'Secretaría',
'Novedades de Canillitas',
'Suscripciones',
'Existencia Papel',
'Movimientos Bobinas',
'Consumos Bobinas',
'Tiradas por Publicación',
'Secretaría',
]; ];
const ReportesIndexPage: React.FC = () => { const ReportesIndexPage: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const [expandedCategory, setExpandedCategory] = useState<string | false>(false); const [expandedCategory, setExpandedCategory] = useState<string | false>(false);
const [isLoadingInitialNavigation, setIsLoadingInitialNavigation] = useState(true); const [isLoadingInitialNavigation, setIsLoadingInitialNavigation] = useState(true);
const uniqueCategories = useMemo(() => predefinedCategoryOrder, []); // --- INICIO DE LA MODIFICACIÓN ---
const { tienePermiso, isSuperAdmin } = usePermissions();
// 1. Creamos una lista memoizada de reportes a los que el usuario SÍ tiene acceso.
const accessibleReportModules = useMemo(() => {
return allReportModules.filter(module =>
isSuperAdmin || tienePermiso(module.requiredPermission)
);
}, [isSuperAdmin, tienePermiso]);
// 2. Creamos una lista de categorías que SÍ tienen al menos un reporte accesible.
const accessibleCategories = useMemo(() => {
const categoriesWithAccess = new Set(accessibleReportModules.map(r => r.category));
return predefinedCategoryOrder.filter(category => categoriesWithAccess.has(category));
}, [accessibleReportModules]);
// --- FIN DE LA MODIFICACIÓN ---
useEffect(() => { useEffect(() => {
const currentBasePath = '/reportes'; const currentBasePath = '/reportes';
const pathParts = location.pathname.substring(currentBasePath.length + 1).split('/'); const pathParts = location.pathname.substring(currentBasePath.length + 1).split('/');
const subPathSegment = pathParts[0]; const subPathSegment = pathParts[0];
let activeReportFoundInEffect = false; if (subPathSegment) {
const activeReport = accessibleReportModules.find(module => module.path === subPathSegment);
if (subPathSegment && subPathSegment !== "") { // Asegurarse que subPathSegment no esté vacío
const activeReport = allReportModules.find(module => module.path === subPathSegment);
if (activeReport) { if (activeReport) {
setExpandedCategory(activeReport.category); setExpandedCategory(activeReport.category);
activeReportFoundInEffect = true;
} else {
setExpandedCategory(false);
} }
} else {
setExpandedCategory(false);
} }
if (location.pathname === currentBasePath && allReportModules.length > 0 && isLoadingInitialNavigation) { // 4. Navegamos al PRIMER REPORTE ACCESIBLE si estamos en la ruta base.
let firstReportToNavigate: { category: string; label: string; path: string } | null = null; if (location.pathname === currentBasePath && accessibleReportModules.length > 0 && isLoadingInitialNavigation) {
for (const category of uniqueCategories) { const firstReportToNavigate = accessibleReportModules[0];
const reportsInCat = allReportModules.filter(r => r.category === category); navigate(firstReportToNavigate.path, { replace: true });
if (reportsInCat.length > 0) {
firstReportToNavigate = reportsInCat[0];
break;
}
}
if (firstReportToNavigate) {
navigate(firstReportToNavigate.path, { replace: true });
activeReportFoundInEffect = true;
}
}
// Solo se establece a false si no estamos en el proceso de navegación inicial O si no se encontró reporte
if (!activeReportFoundInEffect || location.pathname !== currentBasePath) {
setIsLoadingInitialNavigation(false);
} }
setIsLoadingInitialNavigation(false);
}, [location.pathname, navigate, uniqueCategories, isLoadingInitialNavigation]); }, [location.pathname, navigate, accessibleReportModules, isLoadingInitialNavigation]);
const handleCategoryClick = (categoryName: string) => { const handleCategoryClick = (categoryName: string) => {
setExpandedCategory(prev => (prev === categoryName ? false : categoryName)); setExpandedCategory(prev => (prev === categoryName ? false : categoryName));
@@ -146,11 +139,15 @@ const ReportesIndexPage: React.FC = () => {
</Typography> </Typography>
</Box> </Box>
{/* Lista de Categorías y Reportes */} {/* 5. Renderizamos el menú usando la lista de categorías ACCESIBLES. */}
{uniqueCategories.length > 0 ? ( {accessibleCategories.length > 0 ? (
<List component="nav" dense sx={{ pt: 0 }} /* Quitar padding superior de la lista si el título ya lo tiene */ > <List component="nav" dense sx={{ pt: 0 }}>
{uniqueCategories.map((category) => { {accessibleCategories.map((category) => {
const reportsInCategory = allReportModules.filter(r => r.category === category); // 6. Obtenemos los reportes de esta categoría de la lista ACCESIBLE.
const reportsInCategory = accessibleReportModules.filter(r => r.category === category);
// Ya no es necesario el `if (reportsInCategory.length === 0) return null;` porque `accessibleCategories` ya está filtrado.
const isExpanded = expandedCategory === category; const isExpanded = expandedCategory === category;
return ( return (
@@ -167,17 +164,10 @@ const ReportesIndexPage: React.FC = () => {
} }
}} }}
> >
<ListItemText <ListItemText primary={category} primaryTypographyProps={{ fontWeight: isExpanded ? 'bold' : 'normal' }}/>
primary={category} {isExpanded ? <ExpandLess /> : <ExpandMore />}
primaryTypographyProps={{
fontWeight: isExpanded ? 'bold' : 'normal',
// color: isExpanded ? 'primary.main' : 'text.primary'
}}
/>
{reportsInCategory.length > 0 && (isExpanded ? <ExpandLess /> : <ExpandMore />)}
</ListItemButton> </ListItemButton>
{reportsInCategory.length > 0 && ( <Collapse in={isExpanded} timeout="auto" unmountOnExit>
<Collapse in={isExpanded} timeout="auto" unmountOnExit>
<List component="div" disablePadding dense> <List component="div" disablePadding dense>
{reportsInCategory.map((report) => ( {reportsInCategory.map((report) => (
<ListItemButton <ListItemButton
@@ -204,20 +194,13 @@ const ReportesIndexPage: React.FC = () => {
</ListItemButton> </ListItemButton>
))} ))}
</List> </List>
</Collapse> </Collapse>
)}
{reportsInCategory.length === 0 && isExpanded && (
<ListItemText
primary="No hay reportes en esta categoría."
sx={{ pl: 3.5, fontStyle: 'italic', color: 'text.secondary', py:1, typography: 'body2' }}
/>
)}
</React.Fragment> </React.Fragment>
); );
})} })}
</List> </List>
) : ( ) : (
<Typography sx={{p:2, fontStyle: 'italic'}}>No hay categorías configuradas.</Typography> <Typography sx={{p:2, fontStyle: 'italic'}}>No tiene acceso a ningún reporte.</Typography>
)} )}
</Paper> </Paper>

View File

@@ -33,19 +33,18 @@ apiClient.interceptors.response.use(
(error) => { (error) => {
// Cualquier código de estado que este fuera del rango de 2xx causa la ejecución de esta función // Cualquier código de estado que este fuera del rango de 2xx causa la ejecución de esta función
if (axios.isAxiosError(error) && error.response) { if (axios.isAxiosError(error) && error.response) {
if (error.response.status === 401) { // Verificamos si la petición fallida NO fue al endpoint de login.
// Token inválido o expirado const isLoginAttempt = error.config?.url?.endsWith('/auth/login');
console.warn("Error 401: Token inválido o expirado. Deslogueando...");
// Solo activamos el deslogueo automático si el error 401 NO es de un intento de login.
if (error.response.status === 401 && !isLoginAttempt) {
console.warn("Error 401 (Token inválido o expirado) detectado. Deslogueando...");
// Limpiar localStorage y recargar la página.
// AuthContext se encargará de redirigir a /login al recargar porque no encontrará token.
localStorage.removeItem('authToken'); localStorage.removeItem('authToken');
localStorage.removeItem('authUser'); // Asegurar limpiar también el usuario localStorage.removeItem('authUser');
// Forzar un hard refresh para que AuthContext se reinicialice y redirija
// Esto también limpiará cualquier estado de React.
// --- Mostrar mensaje antes de redirigir ---
alert("Tu sesión ha expirado o no es válida. Serás redirigido a la página de inicio de sesión."); alert("Tu sesión ha expirado o no es válida. Serás redirigido a la página de inicio de sesión.");
window.location.href = '/login'; // Redirección más directa window.location.href = '/login';
} }
} }
// Es importante devolver el error para que el componente que hizo la llamada pueda manejarlo también si es necesario // Es importante devolver el error para que el componente que hizo la llamada pueda manejarlo también si es necesario