Ajustes de reportes y controles.
Se implementan DataGrid a los reportes y se mejoran los controles de selección y presentación.
This commit is contained in:
@@ -1,98 +1,243 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Box, Tabs, Tab, Paper, Typography } from '@mui/material';
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import { Box, Paper, Typography, List, ListItemButton, ListItemText, Collapse, CircularProgress } from '@mui/material';
|
||||
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
|
||||
import ExpandLess from '@mui/icons-material/ExpandLess';
|
||||
import ExpandMore from '@mui/icons-material/ExpandMore';
|
||||
|
||||
const reportesSubModules = [
|
||||
{ label: 'Existencia de Papel', path: 'existencia-papel' },
|
||||
{ label: 'Movimiento de Bobinas', path: 'movimiento-bobinas' },
|
||||
{ label: 'Mov. Bobinas por Estado', path: 'movimiento-bobinas-estado' },
|
||||
{ label: 'Distribución General', path: 'listado-distribucion-general' },
|
||||
{ label: 'Distribución Canillas', path: 'listado-distribucion-canillas' },
|
||||
{ label: 'Distrib. Canillas (Importe)', path: 'listado-distribucion-canillas-importe' },
|
||||
{ label: 'Venta Mensual Secretaría', path: 'venta-mensual-secretaria' },
|
||||
{ label: 'Det. Distribución Canillas', path: 'detalle-distribucion-canillas' },
|
||||
{ label: 'Tiradas Pub./Sección', path: 'tiradas-publicaciones-secciones' },
|
||||
{ label: 'Consumo Bobinas/Sección', path: 'consumo-bobinas-seccion' },
|
||||
{ label: 'Consumo Bobinas/Pub.', path: 'consumo-bobinas-publicacion' },
|
||||
{ label: 'Comparativa Cons. Bobinas', path: 'comparativa-consumo-bobinas' },
|
||||
{ label: 'Cuentas Distribuidores', path: 'cuentas-distribuidores' },
|
||||
// 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' },
|
||||
];
|
||||
|
||||
const predefinedCategoryOrder = [
|
||||
'Balance de Cuentas',
|
||||
'Listados Distribución',
|
||||
'Ctrl. Devoluciones',
|
||||
'Existencia Papel',
|
||||
'Movimientos Bobinas',
|
||||
'Consumos Bobinas',
|
||||
'Tiradas por Publicación',
|
||||
'Secretaría',
|
||||
];
|
||||
|
||||
|
||||
const ReportesIndexPage: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const [selectedSubTab, setSelectedSubTab] = useState<number | false>(false);
|
||||
|
||||
const [expandedCategory, setExpandedCategory] = useState<string | false>(false);
|
||||
const [isLoadingInitialNavigation, setIsLoadingInitialNavigation] = useState(true);
|
||||
|
||||
const uniqueCategories = useMemo(() => predefinedCategoryOrder, []);
|
||||
|
||||
useEffect(() => {
|
||||
const currentBasePath = '/reportes';
|
||||
// Extrae la parte de la ruta que sigue a '/reportes/'
|
||||
const subPathSegment = location.pathname.startsWith(currentBasePath + '/')
|
||||
? location.pathname.substring(currentBasePath.length + 1).split('/')[0] // Toma solo el primer segmento
|
||||
: undefined;
|
||||
const pathParts = location.pathname.substring(currentBasePath.length + 1).split('/');
|
||||
const subPathSegment = pathParts[0];
|
||||
|
||||
let activeTabIndex = -1;
|
||||
let activeReportFoundInEffect = false;
|
||||
|
||||
if (subPathSegment) {
|
||||
activeTabIndex = reportesSubModules.findIndex(
|
||||
(subModule) => subModule.path === subPathSegment
|
||||
);
|
||||
}
|
||||
|
||||
if (activeTabIndex !== -1) {
|
||||
setSelectedSubTab(activeTabIndex);
|
||||
} else {
|
||||
// Si estamos exactamente en '/reportes' y hay sub-módulos, navegar al primero.
|
||||
if (location.pathname === currentBasePath && reportesSubModules.length > 0) {
|
||||
navigate(reportesSubModules[0].path, { replace: true }); // Navega a la sub-ruta
|
||||
// setSelectedSubTab(0); // Esto se manejará en la siguiente ejecución del useEffect debido al cambio de ruta
|
||||
if (subPathSegment && subPathSegment !== "") { // Asegurarse que subPathSegment no esté vacío
|
||||
const activeReport = allReportModules.find(module => module.path === subPathSegment);
|
||||
if (activeReport) {
|
||||
setExpandedCategory(activeReport.category);
|
||||
activeReportFoundInEffect = true;
|
||||
} else {
|
||||
setSelectedSubTab(false); // Ninguna sub-ruta activa o conocida, o no hay sub-módulos
|
||||
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);
|
||||
}
|
||||
}, [location.pathname, navigate]); // Solo depende de location.pathname y navigate
|
||||
|
||||
const handleSubTabChange = (_event: React.SyntheticEvent, newValue: number) => {
|
||||
// No es necesario setSelectedSubTab aquí directamente, el useEffect lo manejará.
|
||||
navigate(reportesSubModules[newValue].path);
|
||||
}, [location.pathname, navigate, uniqueCategories, isLoadingInitialNavigation]);
|
||||
|
||||
const handleCategoryClick = (categoryName: string) => {
|
||||
setExpandedCategory(prev => (prev === categoryName ? false : categoryName));
|
||||
};
|
||||
|
||||
// Si no hay sub-módulos definidos, podría ser un estado inicial
|
||||
if (reportesSubModules.length === 0) {
|
||||
const handleReportClick = (reportPath: string) => {
|
||||
navigate(reportPath);
|
||||
};
|
||||
|
||||
const isReportActive = (reportPath: string) => {
|
||||
return location.pathname === `/reportes/${reportPath}` || location.pathname.startsWith(`/reportes/${reportPath}/`);
|
||||
};
|
||||
|
||||
// Si isLoadingInitialNavigation es true Y estamos en /reportes, mostrar loader
|
||||
// Esto evita mostrar el loader si se navega directamente a un sub-reporte.
|
||||
if (isLoadingInitialNavigation && (location.pathname === '/reportes' || location.pathname === '/reportes/')) {
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Typography variant="h5" gutterBottom>Módulo de Reportes</Typography>
|
||||
<Typography>No hay reportes configurados.</Typography>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', width: '100%', height: '100%' }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
Módulo de Reportes
|
||||
</Typography>
|
||||
<Paper square elevation={1}>
|
||||
<Tabs
|
||||
value={selectedSubTab} // 'false' es un valor válido para Tabs si ninguna pestaña está seleccionada
|
||||
onChange={handleSubTabChange}
|
||||
indicatorColor="primary"
|
||||
textColor="primary"
|
||||
variant="scrollable"
|
||||
scrollButtons="auto"
|
||||
aria-label="sub-módulos de reportes"
|
||||
// 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%' }}>
|
||||
{/* Panel Lateral para Navegación */}
|
||||
<Paper
|
||||
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 },
|
||||
height: '100%', // Ocupa toda la altura del Box padre
|
||||
borderRight: (theme) => `1px solid ${theme.palette.divider}`,
|
||||
overflowY: 'auto',
|
||||
bgcolor: 'background.paper', // O el color que desees para el menú
|
||||
// display: 'flex', flexDirection: 'column' // Para que el título y la lista usen el espacio vertical
|
||||
}}
|
||||
>
|
||||
{/* 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
|
||||
}}
|
||||
>
|
||||
{reportesSubModules.map((subModule) => (
|
||||
<Tab key={subModule.path} label={subModule.label} />
|
||||
))}
|
||||
</Tabs>
|
||||
<Typography variant="h6" component="div" sx={{ fontWeight: 'medium', ml:1 /* Pequeño margen para alinear con items */ }}>
|
||||
Reportes
|
||||
</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);
|
||||
const isExpanded = expandedCategory === category;
|
||||
|
||||
return (
|
||||
<React.Fragment key={category}>
|
||||
<ListItemButton
|
||||
onClick={() => handleCategoryClick(category)}
|
||||
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',
|
||||
pr: 1, // Menos padding a la derecha para dar espacio al ícono expander
|
||||
'&:hover': {
|
||||
backgroundColor: 'action.hover'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ListItemText
|
||||
primary={category}
|
||||
primaryTypographyProps={{
|
||||
fontWeight: isExpanded ? 'bold' : 'normal',
|
||||
// color: isExpanded ? 'primary.main' : 'text.primary'
|
||||
}}
|
||||
/>
|
||||
{reportsInCategory.length > 0 && (isExpanded ? <ExpandLess /> : <ExpandMore />)}
|
||||
</ListItemButton>
|
||||
{reportsInCategory.length > 0 && (
|
||||
<Collapse in={isExpanded} timeout="auto" unmountOnExit>
|
||||
<List component="div" disablePadding dense>
|
||||
{reportsInCategory.map((report) => (
|
||||
<ListItemButton
|
||||
key={report.path}
|
||||
selected={isReportActive(report.path)}
|
||||
onClick={() => handleReportClick(report.path)}
|
||||
sx={{
|
||||
pl: 3.5, // Indentación para los reportes (ajustar si se cambió el padding del título)
|
||||
py: 0.8, // Padding vertical de items de reporte
|
||||
...(isReportActive(report.path) && {
|
||||
backgroundColor: (theme) => theme.palette.action.selected, // Un color de fondo sutil
|
||||
borderLeft: (theme) => `4px solid ${theme.palette.primary.light}`, // Un borde para el activo
|
||||
'& .MuiListItemText-primary': {
|
||||
fontWeight: 'medium', // O 'bold'
|
||||
// color: 'primary.main'
|
||||
},
|
||||
}),
|
||||
'&:hover': {
|
||||
backgroundColor: (theme) => theme.palette.action.hover
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ListItemText primary={report.label} primaryTypographyProps={{ variant: 'body2' }}/>
|
||||
</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' }}
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
) : (
|
||||
<Typography sx={{p:2, fontStyle: 'italic'}}>No hay categorías configuradas.</Typography>
|
||||
)}
|
||||
</Paper>
|
||||
<Box sx={{ pt: 2 }}>
|
||||
{/* Outlet renderizará ReporteExistenciaPapelPage u otros
|
||||
Solo renderiza el Outlet si hay una pestaña seleccionada VÁLIDA.
|
||||
Si selectedSubTab es 'false' (porque ninguna ruta coincide con los sub-módulos),
|
||||
se muestra el mensaje.
|
||||
*/}
|
||||
{selectedSubTab !== false ? <Outlet /> : <Typography sx={{p:2}}>Seleccione un reporte del menú lateral o de las pestañas.</Typography>}
|
||||
|
||||
{/* Área Principal para el Contenido del Reporte */}
|
||||
<Box
|
||||
component="main"
|
||||
sx={{
|
||||
flexGrow: 1, // Ocupa el espacio restante
|
||||
p: { xs: 1, sm: 2, md: 3 }, // Padding interno para el contenido, responsivo
|
||||
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'}}>
|
||||
{allReportModules.length > 0 ? "Seleccione una categoría y un reporte del menú lateral." : "No hay reportes configurados."}
|
||||
</Typography>
|
||||
)}
|
||||
<Outlet />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user