Compare commits

...

2 Commits

Author SHA1 Message Date
9f8d577265 Limpieza de Comantarios
All checks were successful
Optimized Build and Deploy / remote-build-and-deploy (push) Successful in 7m59s
2025-08-11 15:44:16 -03:00
b594a48fde 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.
2025-08-11 15:42:23 -03:00
2 changed files with 69 additions and 95 deletions

View File

@@ -3,92 +3,77 @@ 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';
import { usePermissions } from '../../hooks/usePermissions';
// Definición de los módulos de reporte con sus categorías, etiquetas y rutas const allReportModules: { category: string; label: string; path: string; requiredPermission: string; }[] = [
const allReportModules: { category: string; label: string; path: string }[] = [ { category: 'Existencia Papel', label: 'Existencia de Papel', path: 'existencia-papel', requiredPermission: 'RR005' },
{ category: 'Existencia Papel', label: 'Existencia de Papel', path: 'existencia-papel' }, { category: 'Movimientos Bobinas', label: 'Movimiento de Bobinas', path: 'movimiento-bobinas', requiredPermission: 'RR006' },
{ category: 'Movimientos Bobinas', label: 'Movimiento de Bobinas', path: 'movimiento-bobinas' }, { category: 'Movimientos Bobinas', label: 'Mov. Bobinas por Estado', path: 'movimiento-bobinas-estado', requiredPermission: 'RR006' },
{ category: 'Movimientos Bobinas', label: 'Mov. Bobinas por Estado', path: 'movimiento-bobinas-estado' }, { category: 'Listados Distribución', label: 'Distribución Distribuidores', path: 'listado-distribucion-distribuidores', requiredPermission: 'RR002' },
{ category: 'Listados Distribución', label: 'Distribución Distribuidores', path: 'listado-distribucion-distribuidores' }, { category: 'Listados Distribución', label: 'Distribución Canillas', path: 'listado-distribucion-canillas', requiredPermission: 'RR002' },
{ category: 'Listados Distribución', label: 'Distribución Canillas', path: 'listado-distribucion-canillas' }, { category: 'Listados Distribución', label: 'Distribución General', path: 'listado-distribucion-general', requiredPermission: 'RR002' },
{ category: 'Listados Distribución', label: 'Distribución General', path: 'listado-distribucion-general' }, { category: 'Listados Distribución', label: 'Distrib. Canillas (Importe)', path: 'listado-distribucion-canillas-importe', requiredPermission: 'RR002' },
{ category: 'Listados Distribución', label: 'Distrib. Canillas (Importe)', path: 'listado-distribucion-canillas-importe' }, { category: 'Listados Distribución', label: 'Detalle Distribución Canillas', path: 'detalle-distribucion-canillas', requiredPermission: 'MC005' },
{ category: 'Listados Distribución', label: 'Detalle Distribución Canillas', path: 'detalle-distribucion-canillas' }, { category: 'Secretaría', label: 'Venta Mensual Secretaría', path: 'venta-mensual-secretaria', requiredPermission: 'RR002' },
{ category: 'Secretaría', label: 'Venta Mensual Secretaría', path: 'venta-mensual-secretaria' }, { category: 'Tiradas por Publicación', label: 'Tiradas Publicación/Sección', path: 'tiradas-publicaciones-secciones', requiredPermission: 'RR008' },
{ category: 'Tiradas por Publicación', label: 'Tiradas Publicación/Sección', path: 'tiradas-publicaciones-secciones' }, { category: 'Consumos Bobinas', label: 'Consumo Bobinas/Sección', path: 'consumo-bobinas-seccion', requiredPermission: 'RR007' },
{ category: 'Consumos Bobinas', label: 'Consumo Bobinas/Sección', path: 'consumo-bobinas-seccion' }, { category: 'Consumos Bobinas', label: 'Consumo Bobinas/Publicación', path: 'consumo-bobinas-publicacion', requiredPermission: 'RR007' },
{ category: 'Consumos Bobinas', label: 'Consumo Bobinas/PubPublicación', path: 'consumo-bobinas-publicacion' }, { category: 'Consumos Bobinas', label: 'Comparativa Consumo Bobinas', path: 'comparativa-consumo-bobinas', requiredPermission: 'RR007' },
{ category: 'Consumos Bobinas', label: 'Comparativa Consumo Bobinas', path: 'comparativa-consumo-bobinas' }, { category: 'Balance de Cuentas', label: 'Cuentas Distribuidores', path: 'cuentas-distribuidores', requiredPermission: 'RR001' },
{ category: 'Balance de Cuentas', label: 'Cuentas Distribuidores', path: 'cuentas-distribuidores' }, { category: 'Ctrl. Devoluciones', label: 'Control de Devoluciones', path: 'control-devoluciones', requiredPermission: 'RR003' },
{ category: 'Ctrl. Devoluciones', label: 'Control de Devoluciones', path: 'control-devoluciones' }, { category: 'Novedades de Canillitas', label: 'Novedades de Canillitas', path: 'novedades-canillas', requiredPermission: 'RR004' },
{ category: 'Novedades de Canillitas', label: 'Novedades de Canillitas', path: 'novedades-canillas' }, { category: 'Listados Distribución', label: 'Dist. Mensual Can/Acc', path: 'listado-distribucion-mensual', requiredPermission: 'RR009' },
{ category: 'Listados Distribución', label: 'Dist. Mensual Can/Acc', path: 'listado-distribucion-mensual' }, { category: 'Suscripciones', label: 'Facturas para Publicidad', path: 'suscripciones-facturas-publicidad', requiredPermission: 'RR010' },
{ category: 'Suscripciones', label: 'Facturas para Publicidad', path: 'suscripciones-facturas-publicidad' }, { category: 'Suscripciones', label: 'Distribución de Suscripciones', path: 'suscripciones-distribucion', requiredPermission: 'RR011' },
{ category: 'Suscripciones', label: 'Distribución de Suscripciones', path: 'suscripciones-distribucion' },
]; ];
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 { tienePermiso, isSuperAdmin } = usePermissions();
const uniqueCategories = useMemo(() => predefinedCategoryOrder, []); // 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]);
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);
} }
}, [location.pathname, navigate, uniqueCategories, isLoadingInitialNavigation]); setIsLoadingInitialNavigation(false);
}, [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 +131,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 +156,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 +186,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