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:
@@ -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<string | false>(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 });
|
||||
}
|
||||
|
||||
}, [location.pathname, navigate, uniqueCategories, isLoadingInitialNavigation]);
|
||||
setIsLoadingInitialNavigation(false);
|
||||
|
||||
}, [location.pathname, navigate, accessibleReportModules, isLoadingInitialNavigation]);
|
||||
|
||||
const handleCategoryClick = (categoryName: string) => {
|
||||
setExpandedCategory(prev => (prev === categoryName ? false : categoryName));
|
||||
@@ -146,11 +139,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 +164,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 +194,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>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user