From b594a48fde4149b850ff3bc51a5df06ca2af80f2 Mon Sep 17 00:00:00 2001 From: dmolinari Date: Mon, 11 Aug 2025 15:42:23 -0300 Subject: [PATCH] =?UTF-8?q?Feat:=20Se=20modifican=20visual=20de=20men?= =?UTF-8?q?=C3=BA=20reportes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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. --- .../src/pages/Reportes/ReportesIndexPage.tsx | 153 ++++++++---------- Frontend/src/services/apiClient.ts | 19 ++- 2 files changed, 77 insertions(+), 95 deletions(-) diff --git a/Frontend/src/pages/Reportes/ReportesIndexPage.tsx b/Frontend/src/pages/Reportes/ReportesIndexPage.tsx index 452515b..135fadf 100644 --- a/Frontend/src/pages/Reportes/ReportesIndexPage.tsx +++ b/Frontend/src/pages/Reportes/ReportesIndexPage.tsx @@ -3,92 +3,85 @@ 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'; +// --- 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 -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' }, +// --- INICIO DE LA MODIFICACIÓN --- +// Ahora cada reporte tiene su permiso requerido asociado. +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' }, ]; +// --- FIN DE LA MODIFICACIÓN --- 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(false); 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(() => { 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 +139,15 @@ const ReportesIndexPage: React.FC = () => { - {/* Lista de Categorías y Reportes */} - {uniqueCategories.length > 0 ? ( - - {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 ? ( + + {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 +164,10 @@ const ReportesIndexPage: React.FC = () => { } }} > - - {reportsInCategory.length > 0 && (isExpanded ? : )} + + {isExpanded ? : } - {reportsInCategory.length > 0 && ( - + {reportsInCategory.map((report) => ( { ))} - - )} - {reportsInCategory.length === 0 && isExpanded && ( - - )} + ); })} ) : ( - No hay categorías configuradas. + No tiene acceso a ningún reporte. )} diff --git a/Frontend/src/services/apiClient.ts b/Frontend/src/services/apiClient.ts index 3f648a9..f1649c1 100644 --- a/Frontend/src/services/apiClient.ts +++ b/Frontend/src/services/apiClient.ts @@ -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