Fix: Menú Reportes
All checks were successful
Optimized Build and Deploy / remote-build-and-deploy (push) Successful in 7m12s

- Fix del menú de reportes que impedía el recorrido del mismo.
- Se quita la apertura predeterminada de una opción del menú de Reportes.
This commit is contained in:
2025-08-12 10:33:36 -03:00
parent 9f8d577265
commit 5781713b13
3 changed files with 49 additions and 95 deletions

View File

@@ -1,22 +1,22 @@
// src/hooks/usePermissions.ts
import { useAuth } from '../contexts/AuthContext'; import { useAuth } from '../contexts/AuthContext';
import { useCallback } from 'react';
export const usePermissions = () => { export const usePermissions = () => {
const { user } = useAuth(); // user aquí es de tipo UserContextData | null const { user } = useAuth();
const tienePermiso = (codigoPermisoRequerido: string): boolean => { // Envolvemos la función en useCallback.
if (!user) { // Si no hay usuario logueado // Su dependencia es [user], por lo que la función solo se
// volverá a crear si el objeto 'user' cambia (ej. al iniciar/cerrar sesión).
const tienePermiso = useCallback((codigoPermisoRequerido: string): boolean => {
if (!user) {
return false; return false;
} }
if (user.esSuperAdmin) { // SuperAdmin tiene todos los permisos if (user.esSuperAdmin) {
return true; return true;
} }
// Verificar si la lista de permisos del usuario incluye el código requerido
return user.permissions?.includes(codigoPermisoRequerido) ?? false; return user.permissions?.includes(codigoPermisoRequerido) ?? false;
}; }, [user]);
// También puede exportar el objeto user completo si se necesita en otros lugares
// o propiedades específicas como idPerfil, esSuperAdmin.
return { return {
tienePermiso, tienePermiso,
isSuperAdmin: user?.esSuperAdmin ?? false, isSuperAdmin: user?.esSuperAdmin ?? false,

View File

@@ -40,14 +40,12 @@ const ReportesIndexPage: React.FC = () => {
const [isLoadingInitialNavigation, setIsLoadingInitialNavigation] = useState(true); const [isLoadingInitialNavigation, setIsLoadingInitialNavigation] = useState(true);
const { tienePermiso, isSuperAdmin } = usePermissions(); const { tienePermiso, isSuperAdmin } = usePermissions();
// 1. Creamos una lista memoizada de reportes a los que el usuario SÍ tiene acceso.
const accessibleReportModules = useMemo(() => { const accessibleReportModules = useMemo(() => {
return allReportModules.filter(module => return allReportModules.filter(module =>
isSuperAdmin || tienePermiso(module.requiredPermission) isSuperAdmin || tienePermiso(module.requiredPermission)
); );
}, [isSuperAdmin, tienePermiso]); }, [isSuperAdmin, tienePermiso]);
// 2. Creamos una lista de categorías que SÍ tienen al menos un reporte accesible.
const accessibleCategories = useMemo(() => { const accessibleCategories = useMemo(() => {
const categoriesWithAccess = new Set(accessibleReportModules.map(r => r.category)); const categoriesWithAccess = new Set(accessibleReportModules.map(r => r.category));
return predefinedCategoryOrder.filter(category => categoriesWithAccess.has(category)); return predefinedCategoryOrder.filter(category => categoriesWithAccess.has(category));
@@ -62,18 +60,23 @@ const ReportesIndexPage: React.FC = () => {
const activeReport = accessibleReportModules.find(module => module.path === subPathSegment); const activeReport = accessibleReportModules.find(module => module.path === subPathSegment);
if (activeReport) { if (activeReport) {
setExpandedCategory(activeReport.category); setExpandedCategory(activeReport.category);
} else {
// Si la URL apunta a un reporte que no es accesible, no expandimos nada
setExpandedCategory(false);
}
} else {
// Si estamos en la página base (/reportes), expandimos la primera categoría disponible.
if (accessibleCategories.length > 0) {
setExpandedCategory(accessibleCategories[0]);
} else {
setExpandedCategory(false);
} }
} }
// 4. Navegamos al PRIMER REPORTE ACCESIBLE si estamos en la ruta base. // No hay navegación automática, solo manejamos el estado de carga.
if (location.pathname === currentBasePath && accessibleReportModules.length > 0 && isLoadingInitialNavigation) {
const firstReportToNavigate = accessibleReportModules[0];
navigate(firstReportToNavigate.path, { replace: true });
}
setIsLoadingInitialNavigation(false); setIsLoadingInitialNavigation(false);
}, [location.pathname, navigate, accessibleReportModules, isLoadingInitialNavigation]); }, [location.pathname, accessibleReportModules, accessibleCategories]);
const handleCategoryClick = (categoryName: string) => { const handleCategoryClick = (categoryName: string) => {
setExpandedCategory(prev => (prev === categoryName ? false : categoryName)); setExpandedCategory(prev => (prev === categoryName ? false : categoryName));
@@ -84,12 +87,10 @@ const ReportesIndexPage: React.FC = () => {
}; };
const isReportActive = (reportPath: string) => { const isReportActive = (reportPath: string) => {
return location.pathname === `/reportes/${reportPath}` || location.pathname.startsWith(`/reportes/${reportPath}/`); return location.pathname.startsWith(`/reportes/${reportPath}`);
}; };
// Si isLoadingInitialNavigation es true Y estamos en /reportes, mostrar loader if (isLoadingInitialNavigation) {
// Esto evita mostrar el loader si se navega directamente a un sub-reporte.
if (isLoadingInitialNavigation && (location.pathname === '/reportes' || location.pathname === '/reportes/')) {
return ( return (
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', width: '100%', height: '100%' }}> <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', width: '100%', height: '100%' }}>
<CircularProgress /> <CircularProgress />
@@ -98,48 +99,25 @@ const ReportesIndexPage: React.FC = () => {
} }
return ( return (
// Contenedor principal que se adaptará a su padre
// Eliminamos 'height: calc(100vh - 64px)' y cualquier margen/padding que controle el espacio exterior
<Box sx={{ display: 'flex', width: '100%', height: '100%' }}> <Box sx={{ display: 'flex', width: '100%', height: '100%' }}>
{/* Panel Lateral para Navegación */} <Paper elevation={0} square sx={{
<Paper width: { xs: 220, sm: 250, md: 280 },
elevation={0} // Sin elevación para que se sienta más integrado si el fondo es el mismo
square // Bordes rectos
sx={{
width: { xs: 220, sm: 250, md: 280 }, // Ancho responsivo del panel lateral
minWidth: { xs: 200, sm: 220 }, minWidth: { xs: 200, sm: 220 },
height: '100%', // Ocupa toda la altura del Box padre height: '100%',
borderRight: (theme) => `1px solid ${theme.palette.divider}`, borderRight: (theme) => `1px solid ${theme.palette.divider}`,
overflowY: 'auto', overflowY: 'auto',
bgcolor: 'background.paper', // O el color que desees para el menú bgcolor: 'background.paper',
// display: 'flex', flexDirection: 'column' // Para que el título y la lista usen el espacio vertical }}>
}} <Box sx={{ p: 1.5 }}>
> <Typography variant="h6" component="div" sx={{ fontWeight: 'medium', ml:1 }}>
{/* Título del Menú Lateral */}
<Box
sx={{
p: 1.5, // Padding interno para el título
// borderBottom: (theme) => `1px solid ${theme.palette.divider}`, // Opcional: separador
// position: 'sticky', // Si quieres que el título quede fijo al hacer scroll en la lista
// top: 0,
// zIndex: 1,
// bgcolor: 'background.paper' // Necesario si es sticky y tiene scroll la lista
}}
>
<Typography variant="h6" component="div" sx={{ fontWeight: 'medium', ml:1 /* Pequeño margen para alinear con items */ }}>
Reportes Reportes
</Typography> </Typography>
</Box> </Box>
{/* 5. Renderizamos el menú usando la lista de categorías ACCESIBLES. */}
{accessibleCategories.length > 0 ? ( {accessibleCategories.length > 0 ? (
<List component="nav" dense sx={{ pt: 0 }}> <List component="nav" dense sx={{ pt: 0 }}>
{accessibleCategories.map((category) => { {accessibleCategories.map((category) => {
// 6. Obtenemos los reportes de esta categoría de la lista ACCESIBLE.
const reportsInCategory = accessibleReportModules.filter(r => r.category === category); 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 (
@@ -147,15 +125,10 @@ const ReportesIndexPage: React.FC = () => {
<ListItemButton <ListItemButton
onClick={() => handleCategoryClick(category)} onClick={() => handleCategoryClick(category)}
sx={{ sx={{
// py: 1.2, // Ajustar padding vertical de items de categoría
// backgroundColor: isExpanded ? 'action.selected' : 'transparent',
borderLeft: isExpanded ? (theme) => `4px solid ${theme.palette.primary.main}` : '4px solid transparent', borderLeft: isExpanded ? (theme) => `4px solid ${theme.palette.primary.main}` : '4px solid transparent',
pr: 1, // Menos padding a la derecha para dar espacio al ícono expander pr: 1,
'&:hover': { '&:hover': { backgroundColor: 'action.hover' }
backgroundColor: 'action.hover' }}>
}
}}
>
<ListItemText primary={category} primaryTypographyProps={{ fontWeight: isExpanded ? 'bold' : 'normal' }}/> <ListItemText primary={category} primaryTypographyProps={{ fontWeight: isExpanded ? 'bold' : 'normal' }}/>
{isExpanded ? <ExpandLess /> : <ExpandMore />} {isExpanded ? <ExpandLess /> : <ExpandMore />}
</ListItemButton> </ListItemButton>
@@ -167,21 +140,14 @@ const ReportesIndexPage: React.FC = () => {
selected={isReportActive(report.path)} selected={isReportActive(report.path)}
onClick={() => handleReportClick(report.path)} onClick={() => handleReportClick(report.path)}
sx={{ sx={{
pl: 3.5, // Indentación para los reportes (ajustar si se cambió el padding del título) pl: 3.5, py: 0.8,
py: 0.8, // Padding vertical de items de reporte
...(isReportActive(report.path) && { ...(isReportActive(report.path) && {
backgroundColor: (theme) => theme.palette.action.selected, // Un color de fondo sutil backgroundColor: (theme) => theme.palette.action.selected,
borderLeft: (theme) => `4px solid ${theme.palette.primary.light}`, // Un borde para el activo borderLeft: (theme) => `4px solid ${theme.palette.primary.light}`,
'& .MuiListItemText-primary': { '& .MuiListItemText-primary': { fontWeight: 'medium' },
fontWeight: 'medium', // O 'bold'
// color: 'primary.main'
},
}), }),
'&:hover': { '&:hover': { backgroundColor: (theme) => theme.palette.action.hover }
backgroundColor: (theme) => theme.palette.action.hover }}>
}
}}
>
<ListItemText primary={report.label} primaryTypographyProps={{ variant: 'body2' }}/> <ListItemText primary={report.label} primaryTypographyProps={{ variant: 'body2' }}/>
</ListItemButton> </ListItemButton>
))} ))}
@@ -196,26 +162,17 @@ const ReportesIndexPage: React.FC = () => {
)} )}
</Paper> </Paper>
{/* Área Principal para el Contenido del Reporte */} <Box component="main" sx={{
<Box flexGrow: 1, p: { xs: 1, sm: 2, md: 3 },
component="main" overflowY: 'auto', height: '100%', bgcolor: 'grey.100'
sx={{ }}>
flexGrow: 1, // Ocupa el espacio restante {/* Lógica para mostrar el mensaje de bienvenida */}
p: { xs: 1, sm: 2, md: 3 }, // Padding interno para el contenido, responsivo {location.pathname === '/reportes' && !isLoadingInitialNavigation && (
overflowY: 'auto',
height: '100%', // Ocupa toda la altura del Box padre
bgcolor: 'grey.100' // Un color de fondo diferente para distinguir el área de contenido
}}
>
{/* El Outlet renderiza el componente del reporte específico */}
{(!location.pathname.startsWith('/reportes/') || !allReportModules.some(r => isReportActive(r.path))) && location.pathname !== '/reportes/' && location.pathname !== '/reportes' && !isLoadingInitialNavigation && (
<Typography sx={{p:2, textAlign:'center', mt: 4, color: 'text.secondary'}}>
El reporte solicitado no existe o la ruta no es válida.
</Typography>
)}
{(location.pathname === '/reportes/' || location.pathname === '/reportes') && !allReportModules.some(r => isReportActive(r.path)) && !isLoadingInitialNavigation && (
<Typography sx={{p:2, textAlign:'center', mt: 4, color: 'text.secondary'}}> <Typography sx={{p:2, textAlign:'center', mt: 4, color: 'text.secondary'}}>
{allReportModules.length > 0 ? "Seleccione una categoría y un reporte del menú lateral." : "No hay reportes configurados."} {accessibleReportModules.length > 0
? "Seleccione una categoría y un reporte del menú lateral."
: "No tiene acceso a ningún reporte."
}
</Typography> </Typography>
)} )}
<Outlet /> <Outlet />

View File

@@ -1,11 +1,9 @@
// src/routes/AppRoutes.tsx
import React, { type JSX } from 'react'; import React, { type JSX } from 'react';
import { BrowserRouter, Routes, Route, Navigate, Outlet } from 'react-router-dom'; import { BrowserRouter, Routes, Route, Navigate, Outlet } from 'react-router-dom';
import LoginPage from '../pages/LoginPage'; import LoginPage from '../pages/LoginPage';
import HomePage from '../pages/HomePage'; import HomePage from '../pages/HomePage';
import { useAuth } from '../contexts/AuthContext'; import { useAuth } from '../contexts/AuthContext';
import MainLayout from '../layouts/MainLayout'; import MainLayout from '../layouts/MainLayout';
import { Typography } from '@mui/material';
import SectionProtectedRoute from './SectionProtectedRoute'; import SectionProtectedRoute from './SectionProtectedRoute';
// Distribución // Distribución
@@ -267,7 +265,6 @@ const AppRoutes = () => {
<ReportesIndexPage /> <ReportesIndexPage />
</SectionProtectedRoute>} </SectionProtectedRoute>}
> >
<Route index element={<Typography sx={{ p: 2 }}>Seleccione un reporte del menú lateral.</Typography>} /> {/* Placeholder */}
<Route path="existencia-papel" element={<ReporteExistenciaPapelPage />} /> <Route path="existencia-papel" element={<ReporteExistenciaPapelPage />} />
<Route path="movimiento-bobinas" element={<ReporteMovimientoBobinasPage />} /> <Route path="movimiento-bobinas" element={<ReporteMovimientoBobinasPage />} />
<Route path="movimiento-bobinas-estado" element={<ReporteMovimientoBobinasEstadoPage />} /> <Route path="movimiento-bobinas-estado" element={<ReporteMovimientoBobinasEstadoPage />} />