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 ExpandLess from '@mui/icons-material/ExpandLess';
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 }[] = [
{ category: 'Existencia Papel', label: 'Existencia de Papel', path: 'existencia-papel' },
{ category: 'Movimientos Bobinas', label: 'Movimiento de Bobinas', path: 'movimiento-bobinas' },
{ category: 'Movimientos Bobinas', label: 'Mov. Bobinas por Estado', path: 'movimiento-bobinas-estado' },
{ category: 'Listados Distribución', label: 'Distribución Distribuidores', path: 'listado-distribucion-distribuidores' },
{ category: 'Listados Distribución', label: 'Distribución Canillas', path: 'listado-distribucion-canillas' },
{ 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' },
{ 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' },
{ 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' },
{ category: 'Consumos Bobinas', label: 'Consumo Bobinas/PubPublicación', path: 'consumo-bobinas-publicacion' },
{ category: 'Consumos Bobinas', label: 'Comparativa Consumo Bobinas', path: 'comparativa-consumo-bobinas' },
{ category: 'Balance de Cuentas', label: 'Cuentas Distribuidores', path: 'cuentas-distribuidores' },
{ category: 'Ctrl. Devoluciones', label: 'Control de Devoluciones', path: 'control-devoluciones' },
{ category: 'Novedades de Canillitas', label: 'Novedades de Canillitas', path: 'novedades-canillas' },
{ category: 'Listados Distribución', label: 'Dist. Mensual Can/Acc', path: 'listado-distribucion-mensual' },
{ category: 'Suscripciones', label: 'Facturas para Publicidad', path: 'suscripciones-facturas-publicidad' },
{ category: 'Suscripciones', label: 'Distribución de Suscripciones', path: 'suscripciones-distribucion' },
const allReportModules: { category: string; label: string; path: string; requiredPermission: string; }[] = [
{ category: 'Existencia Papel', label: 'Existencia de Papel', path: 'existencia-papel', requiredPermission: 'RR005' },
{ category: 'Movimientos Bobinas', label: 'Movimiento de Bobinas', path: 'movimiento-bobinas', requiredPermission: 'RR006' },
{ category: 'Movimientos Bobinas', label: 'Mov. Bobinas por Estado', path: 'movimiento-bobinas-estado', requiredPermission: 'RR006' },
{ category: 'Listados Distribución', label: 'Distribución Distribuidores', path: 'listado-distribucion-distribuidores', requiredPermission: 'RR002' },
{ category: 'Listados Distribución', label: 'Distribución Canillas', path: 'listado-distribucion-canillas', requiredPermission: 'RR002' },
{ category: 'Listados Distribución', label: 'Distribución General', path: 'listado-distribucion-general', requiredPermission: 'RR002' },
{ category: 'Listados Distribución', label: 'Distrib. Canillas (Importe)', path: 'listado-distribucion-canillas-importe', requiredPermission: 'RR002' },
{ category: 'Listados Distribución', label: 'Detalle Distribución Canillas', path: 'detalle-distribucion-canillas', requiredPermission: 'MC005' },
{ category: 'Secretaría', label: 'Venta Mensual Secretaría', path: 'venta-mensual-secretaria', requiredPermission: 'RR002' },
{ category: 'Tiradas por Publicación', label: 'Tiradas Publicación/Sección', path: 'tiradas-publicaciones-secciones', requiredPermission: 'RR008' },
{ category: 'Consumos Bobinas', label: 'Consumo Bobinas/Sección', path: 'consumo-bobinas-seccion', requiredPermission: 'RR007' },
{ category: 'Consumos Bobinas', label: 'Consumo Bobinas/Publicación', path: 'consumo-bobinas-publicacion', requiredPermission: 'RR007' },
{ category: 'Consumos Bobinas', label: 'Comparativa Consumo Bobinas', path: 'comparativa-consumo-bobinas', requiredPermission: 'RR007' },
{ category: 'Balance de Cuentas', label: 'Cuentas Distribuidores', path: 'cuentas-distribuidores', requiredPermission: 'RR001' },
{ category: 'Ctrl. Devoluciones', label: 'Control de Devoluciones', path: 'control-devoluciones', requiredPermission: 'RR003' },
{ category: 'Novedades de Canillitas', label: 'Novedades de Canillitas', path: 'novedades-canillas', requiredPermission: 'RR004' },
{ category: 'Listados Distribución', label: 'Dist. Mensual Can/Acc', path: 'listado-distribucion-mensual', requiredPermission: 'RR009' },
{ category: 'Suscripciones', label: 'Facturas para Publicidad', path: 'suscripciones-facturas-publicidad', requiredPermission: 'RR010' },
{ category: 'Suscripciones', label: 'Distribución de Suscripciones', path: 'suscripciones-distribucion', requiredPermission: 'RR011' },
];
const predefinedCategoryOrder = [
'Balance de Cuentas',
'Listados Distribución',
'Ctrl. Devoluciones',
'Novedades de Canillitas',
'Suscripciones',
'Existencia Papel',
'Movimientos Bobinas',
'Consumos Bobinas',
'Tiradas por Publicación',
'Secretaría',
'Balance de Cuentas', 'Listados Distribución', 'Ctrl. Devoluciones',
'Novedades de Canillitas', 'Suscripciones', 'Existencia Papel',
'Movimientos Bobinas', 'Consumos Bobinas', 'Tiradas por Publicación', 'Secretaría',
];
const ReportesIndexPage: React.FC = () => {
const navigate = useNavigate();
const location = useLocation();
const [expandedCategory, setExpandedCategory] = useState<string | false>(false);
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(() => {
const currentBasePath = '/reportes';
const pathParts = location.pathname.substring(currentBasePath.length + 1).split('/');
const subPathSegment = pathParts[0];
let activeReportFoundInEffect = false;
if (subPathSegment && subPathSegment !== "") { // Asegurarse que subPathSegment no esté vacío
const activeReport = allReportModules.find(module => module.path === subPathSegment);
if (subPathSegment) {
const activeReport = accessibleReportModules.find(module => module.path === subPathSegment);
if (activeReport) {
setExpandedCategory(activeReport.category);
activeReportFoundInEffect = true;
} else {
setExpandedCategory(false);
}
} else {
setExpandedCategory(false);
}
if (location.pathname === currentBasePath && allReportModules.length > 0 && isLoadingInitialNavigation) {
let firstReportToNavigate: { category: string; label: string; path: string } | null = null;
for (const category of uniqueCategories) {
const reportsInCat = allReportModules.filter(r => r.category === category);
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);
// 4. Navegamos al PRIMER REPORTE ACCESIBLE si estamos en la ruta base.
if (location.pathname === currentBasePath && accessibleReportModules.length > 0 && isLoadingInitialNavigation) {
const firstReportToNavigate = accessibleReportModules[0];
navigate(firstReportToNavigate.path, { replace: true });
}
setIsLoadingInitialNavigation(false);
}, [location.pathname, navigate, uniqueCategories, isLoadingInitialNavigation]);
}, [location.pathname, navigate, accessibleReportModules, isLoadingInitialNavigation]);
const handleCategoryClick = (categoryName: string) => {
setExpandedCategory(prev => (prev === categoryName ? false : categoryName));
@@ -146,11 +131,15 @@ const ReportesIndexPage: React.FC = () => {
</Typography>
</Box>
{/* Lista de Categorías y Reportes */}
{uniqueCategories.length > 0 ? (
<List component="nav" dense sx={{ pt: 0 }} /* Quitar padding superior de la lista si el título ya lo tiene */ >
{uniqueCategories.map((category) => {
const reportsInCategory = allReportModules.filter(r => r.category === category);
{/* 5. Renderizamos el menú usando la lista de categorías ACCESIBLES. */}
{accessibleCategories.length > 0 ? (
<List component="nav" dense sx={{ pt: 0 }}>
{accessibleCategories.map((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;
return (
@@ -167,17 +156,10 @@ const ReportesIndexPage: React.FC = () => {
}
}}
>
<ListItemText
primary={category}
primaryTypographyProps={{
fontWeight: isExpanded ? 'bold' : 'normal',
// color: isExpanded ? 'primary.main' : 'text.primary'
}}
/>
{reportsInCategory.length > 0 && (isExpanded ? <ExpandLess /> : <ExpandMore />)}
<ListItemText primary={category} primaryTypographyProps={{ fontWeight: isExpanded ? 'bold' : 'normal' }}/>
{isExpanded ? <ExpandLess /> : <ExpandMore />}
</ListItemButton>
{reportsInCategory.length > 0 && (
<Collapse in={isExpanded} timeout="auto" unmountOnExit>
<Collapse in={isExpanded} timeout="auto" unmountOnExit>
<List component="div" disablePadding dense>
{reportsInCategory.map((report) => (
<ListItemButton
@@ -204,20 +186,13 @@ const ReportesIndexPage: React.FC = () => {
</ListItemButton>
))}
</List>
</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' }}
/>
)}
</Collapse>
</React.Fragment>
);
})}
</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>

View File

@@ -33,19 +33,18 @@ apiClient.interceptors.response.use(
(error) => {
// 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 (error.response.status === 401) {
// Token inválido o expirado
console.warn("Error 401: Token inválido o expirado. Deslogueando...");
// Verificamos si la petición fallida NO fue al endpoint de login.
const isLoginAttempt = error.config?.url?.endsWith('/auth/login');
// 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('authUser'); // Asegurar limpiar también el usuario
// Forzar un hard refresh para que AuthContext se reinicialice y redirija
// Esto también limpiará cualquier estado de React.
// --- Mostrar mensaje antes de redirigir ---
localStorage.removeItem('authUser');
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