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:
2025-05-31 23:48:42 -03:00
parent 1182a4cdee
commit 99532b03f1
35 changed files with 4132 additions and 1363 deletions

View File

@@ -1,16 +1,22 @@
import React, { useState, useCallback } from 'react';
import React, { useState, useCallback, useMemo } from 'react';
import {
Box, Typography, Paper, CircularProgress, Alert, Button,
TableContainer, Table, TableHead, TableRow, TableCell, TableBody
Box, Typography, Paper, CircularProgress, Alert, Button
} from '@mui/material';
import { DataGrid, type GridColDef, GridFooterContainer, GridFooter } from '@mui/x-data-grid'; // Importaciones para DataGrid
import { esES } from '@mui/x-data-grid/locales'; // Para localización
import reportesService from '../../services/Reportes/reportesService';
import type { MovimientoBobinasDto } from '../../models/dtos/Reportes/MovimientoBobinasDto';
import SeleccionaReporteMovimientoBobinas from './SeleccionaReporteMovimientoBobinas';
import * as XLSX from 'xlsx';
import axios from 'axios';
// Definición de la interfaz extendida para DataGrid (con 'id')
interface MovimientoBobinasDataGridDto extends MovimientoBobinasDto {
id: string;
}
const ReporteMovimientoBobinasPage: React.FC = () => {
const [reportData, setReportData] = useState<MovimientoBobinasDto[]>([]);
const [reportData, setReportData] = useState<MovimientoBobinasDataGridDto[]>([]); // Usar el tipo extendido
const [loading, setLoading] = useState(false);
const [loadingPdf, setLoadingPdf] = useState(false);
const [error, setError] = useState<string | null>(null);
@@ -20,8 +26,12 @@ const ReporteMovimientoBobinasPage: React.FC = () => {
fechaDesde: string;
fechaHasta: string;
idPlanta: number;
nombrePlanta?: string;
} | null>(null);
const numberLocaleFormatter = (value: number | null | undefined) =>
value != null ? Number(value).toLocaleString('es-AR') : '';
const handleGenerarReporte = useCallback(async (params: {
fechaDesde: string;
fechaHasta: string;
@@ -30,11 +40,20 @@ const ReporteMovimientoBobinasPage: React.FC = () => {
setLoading(true);
setError(null);
setApiErrorParams(null);
// Opcional: Obtener nombre de la planta
// const plantaService = (await import('../../services/Maestros/plantaService')).default;
// const plantaData = await plantaService.getPlantaById(params.idPlanta);
// setCurrentParams({...params, nombrePlanta: plantaData?.nombre});
setCurrentParams(params);
try {
const data = await reportesService.getMovimientoBobinas(params);
setReportData(data);
if (data.length === 0) {
// Añadir 'id' único a cada fila para DataGrid
const dataWithIds = data.map((item, index) => ({
...item,
id: `${item.tipoBobina}-${index}` // Asumiendo que tipoBobina es único por reporte o combinar con index
}));
setReportData(dataWithIds);
if (dataWithIds.length === 0) {
setError("No se encontraron datos para los parámetros seleccionados.");
}
setShowParamSelector(false);
@@ -64,18 +83,35 @@ const ReporteMovimientoBobinasPage: React.FC = () => {
}
const dataToExport = reportData.map(item => ({
"Tipo Bobina": item.tipoBobina,
"Bobinas Iniciales": item.bobinasIniciales,
"Cant. Inicial": item.bobinasIniciales,
"Kg Iniciales": item.kilosIniciales,
"Bobinas Compradas": item.bobinasCompradas,
"Compradas": item.bobinasCompradas,
"Kg Comprados": item.kilosComprados,
"Bobinas Consumidas": item.bobinasConsumidas,
"Consumidas": item.bobinasConsumidas,
"Kg Consumidos": item.kilosConsumidos,
"Bobinas Dañadas": item.bobinasDaniadas,
"Dañadas": item.bobinasDaniadas,
"Kg Dañados": item.kilosDaniados,
"Bobinas Finales": item.bobinasFinales,
"Cant. Final": item.bobinasFinales,
"Kg Finales": item.kilosFinales,
}));
// Añadir fila de totales
const totalesRow = {
"Tipo Bobina": "Totales",
"Cant. Inicial": reportData.reduce((sum, item) => sum + item.bobinasIniciales, 0),
"Kg Iniciales": reportData.reduce((sum, item) => sum + item.kilosIniciales, 0),
"Compradas": reportData.reduce((sum, item) => sum + item.bobinasCompradas, 0),
"Kg Comprados": reportData.reduce((sum, item) => sum + item.kilosComprados, 0),
"Consumidas": reportData.reduce((sum, item) => sum + item.bobinasConsumidas, 0),
"Kg Consumidos": reportData.reduce((sum, item) => sum + item.kilosConsumidos, 0),
"Dañadas": reportData.reduce((sum, item) => sum + item.bobinasDaniadas, 0),
"Kg Dañados": reportData.reduce((sum, item) => sum + item.kilosDaniados, 0),
"Cant. Final": reportData.reduce((sum, item) => sum + item.bobinasFinales, 0),
"Kg Finales": reportData.reduce((sum, item) => sum + item.kilosFinales, 0),
};
dataToExport.push(totalesRow);
const ws = XLSX.utils.json_to_sheet(dataToExport);
const headers = Object.keys(dataToExport[0]);
ws['!cols'] = headers.map(h => {
@@ -91,7 +127,9 @@ const ReporteMovimientoBobinasPage: React.FC = () => {
XLSX.utils.book_append_sheet(wb, ws, "MovimientoBobinas");
let fileName = "ReporteMovimientoBobinas";
if (currentParams) {
fileName += `_${currentParams.fechaDesde}_a_${currentParams.fechaHasta}_Planta${currentParams.idPlanta}`;
// Asumiendo que currentParams.nombrePlanta está disponible o se usa idPlanta
fileName += `_${currentParams.nombrePlanta || `Planta${currentParams.idPlanta}`}`;
fileName += `_${currentParams.fechaDesde}_a_${currentParams.fechaHasta}`;
}
fileName += ".xlsx";
XLSX.writeFile(wb, fileName);
@@ -122,6 +160,137 @@ const ReporteMovimientoBobinasPage: React.FC = () => {
}
}, [currentParams]);
// Definiciones de Columnas para DataGrid
const columns: GridColDef[] = [
{ field: 'tipoBobina', headerName: 'Tipo Bobina', width: 200, flex: 1.5 },
{ field: 'bobinasIniciales', headerName: 'Cant. Ini.', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberLocaleFormatter(Number(value)) },
{ field: 'kilosIniciales', headerName: 'Kg Ini.', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberLocaleFormatter(Number(value)) },
{ field: 'bobinasCompradas', headerName: 'Compradas', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberLocaleFormatter(Number(value)) },
{ field: 'kilosComprados', headerName: 'Kg Compr.', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberLocaleFormatter(Number(value)) },
{ field: 'bobinasConsumidas', headerName: 'Consum.', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberLocaleFormatter(Number(value)) },
{ field: 'kilosConsumidos', headerName: 'Kg Consum.', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberLocaleFormatter(Number(value)) },
{ field: 'bobinasDaniadas', headerName: 'Dañadas', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberLocaleFormatter(Number(value)) },
{ field: 'kilosDaniados', headerName: 'Kg Dañ.', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberLocaleFormatter(Number(value)) },
{ field: 'bobinasFinales', headerName: 'Cant. Fin.', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberLocaleFormatter(Number(value)) },
{ field: 'kilosFinales', headerName: 'Kg Finales', type: 'number', width: 110, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberLocaleFormatter(Number(value)) },
];
const rows = useMemo(() => reportData, [reportData]);
// Calcular totales para el footer
const totales = useMemo(() => {
if (reportData.length === 0) return null;
return {
bobinasIniciales: reportData.reduce((sum, item) => sum + item.bobinasIniciales, 0),
kilosIniciales: reportData.reduce((sum, item) => sum + item.kilosIniciales, 0),
bobinasCompradas: reportData.reduce((sum, item) => sum + item.bobinasCompradas, 0),
kilosComprados: reportData.reduce((sum, item) => sum + item.kilosComprados, 0),
bobinasConsumidas: reportData.reduce((sum, item) => sum + item.bobinasConsumidas, 0),
kilosConsumidos: reportData.reduce((sum, item) => sum + item.kilosConsumidos, 0),
bobinasDaniadas: reportData.reduce((sum, item) => sum + item.bobinasDaniadas, 0),
kilosDaniados: reportData.reduce((sum, item) => sum + item.kilosDaniados, 0),
bobinasFinales: reportData.reduce((sum, item) => sum + item.bobinasFinales, 0),
kilosFinales: reportData.reduce((sum, item) => sum + item.kilosFinales, 0),
};
}, [reportData]);
// eslint-disable-next-line react/display-name
const CustomFooter = () => {
if (!totales) return null;
const getCellStyle = (field: (typeof columns)[number]['field'] | 'label', isLabel: boolean = false) => {
const colConfig = columns.find(c => c.field === field);
let targetWidth: number | string = 'auto'; // Por defecto, dejar que el contenido decida
let targetMinWidth: number | string = 'auto';
if (isLabel) {
// Para la etiqueta "TOTALES:", un ancho más ajustado.
// Podrías basarlo en el ancho de la primera columna si es consistentemente la de "Tipo Bobina"
// o un valor fijo que sepas que funciona.
targetWidth = colConfig?.width ? Math.max(80, colConfig.width * 0.6) : 120; // Ej: 60% del ancho de la columna o 120px
targetMinWidth = 80; // Un mínimo razonable para "TOTALES:"
} else if (colConfig) {
// Para los valores numéricos, podemos ser un poco más conservadores que el ancho de la columna.
// O usar el ancho de la columna si es pequeño.
targetWidth = colConfig.width ? Math.max(70, colConfig.width * 0.85) : 90; // Ej: 85% del ancho de la columna o 90px
targetMinWidth = 70; // Un mínimo para números
}
return {
minWidth: targetMinWidth,
width: targetWidth,
textAlign: isLabel ? 'left' : (colConfig?.align || 'right') as 'right' | 'left' | 'center',
pr: isLabel ? 1 : (field === 'kilosFinales' ? 0 : 1), // padding-right
fontWeight: 'bold',
// Añadimos overflow y textOverflow para manejar texto largo en la etiqueta si fuera necesario
overflow: isLabel ? 'hidden' : undefined,
textOverflow: isLabel ? 'ellipsis' : undefined,
whiteSpace: 'nowrap', // Asegurar que no haya saltos de línea en los totales
};
};
return (
<GridFooterContainer sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
width: '100%',
borderTop: (theme) => `1px solid ${theme.palette.divider}`,
minHeight: '52px',
}}>
{/* Box para la paginación estándar */}
<Box sx={{
display: 'flex',
alignItems: 'center',
flexShrink: 0,
overflow: 'hidden',
px:1,
// Para asegurar que la paginación no se coma todo el espacio si es muy ancha:
// Podríamos darle un flex-basis o un maxWidth si los totales necesitan más espacio garantizado.
// Por ejemplo:
// flexBasis: '50%', // Ocupa el 50% del espacio disponible si no hay otros factores
// maxWidth: '600px', // Un máximo absoluto
}}>
<GridFooter
sx={{
borderTop: 'none',
width: '100%',
'& .MuiToolbar-root': {
paddingLeft: 0,
paddingRight: 0,
},
'& .MuiDataGrid-selectedRowCount': { display: 'none' },
}}
/>
</Box>
{/* Box para los totales personalizados */}
<Box sx={{
display: 'flex',
alignItems: 'center',
fontWeight: 'bold',
whiteSpace: 'nowrap', // Ya estaba, es importante
overflowX: 'auto',
px:1,
flexShrink: 1, // Permitir que este contenedor se encoja si es necesario
// maxWidth: 'calc(100% - ANCHO_PAGINACION_ESTIMADO)' // Si quieres ser muy preciso
}}>
<Typography variant="subtitle2" sx={getCellStyle('label', true)}>TOTALES:</Typography>
<Typography variant="subtitle2" sx={getCellStyle('bobinasIniciales')}>{numberLocaleFormatter(totales.bobinasIniciales)}</Typography>
<Typography variant="subtitle2" sx={getCellStyle('kilosIniciales')}>{numberLocaleFormatter(totales.kilosIniciales)}</Typography>
<Typography variant="subtitle2" sx={getCellStyle('bobinasCompradas')}>{numberLocaleFormatter(totales.bobinasCompradas)}</Typography>
<Typography variant="subtitle2" sx={getCellStyle('kilosComprados')}>{numberLocaleFormatter(totales.kilosComprados)}</Typography>
<Typography variant="subtitle2" sx={getCellStyle('bobinasConsumidas')}>{numberLocaleFormatter(totales.bobinasConsumidas)}</Typography>
<Typography variant="subtitle2" sx={getCellStyle('kilosConsumidos')}>{numberLocaleFormatter(totales.kilosConsumidos)}</Typography>
<Typography variant="subtitle2" sx={getCellStyle('bobinasDaniadas')}>{numberLocaleFormatter(totales.bobinasDaniadas)}</Typography>
<Typography variant="subtitle2" sx={getCellStyle('kilosDaniados')}>{numberLocaleFormatter(totales.kilosDaniados)}</Typography>
<Typography variant="subtitle2" sx={getCellStyle('bobinasFinales')}>{numberLocaleFormatter(totales.bobinasFinales)}</Typography>
<Typography variant="subtitle2" sx={getCellStyle('kilosFinales')}>{numberLocaleFormatter(totales.kilosFinales)}</Typography>
</Box>
</GridFooterContainer>
);
};
if (showParamSelector) {
return (
<Box sx={{ p: 2, display: 'flex', justifyContent: 'center', mt: 2 }}>
@@ -140,7 +309,7 @@ const ReporteMovimientoBobinasPage: React.FC = () => {
return (
<Box sx={{ p: 2 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2, flexWrap: 'wrap', gap: 1 }}>
<Typography variant="h5">Reporte: Movimiento de Bobinas</Typography>
<Typography variant="h5">Reporte: Movimiento de Bobinas {currentParams?.nombrePlanta ? `(${currentParams.nombrePlanta})` : ''}</Typography>
<Box sx={{ display: 'flex', gap: 1 }}>
<Button
onClick={handleGenerarYAbrirPdf}
@@ -164,47 +333,24 @@ const ReporteMovimientoBobinasPage: React.FC = () => {
</Box>
</Box>
{loading && <Box sx={{ textAlign: 'center' }}><CircularProgress /></Box>}
{error && !loading && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
{loading && <Box sx={{ textAlign: 'center', my:2 }}><CircularProgress /></Box>}
{error && !loading && <Alert severity="info" sx={{ my: 2 }}>{error}</Alert>}
{!loading && !error && (
<TableContainer component={Paper} sx={{ maxHeight: 'calc(100vh - 240px)' }}>
<Table stickyHeader size="small">
<TableHead>
<TableRow>
<TableCell>Tipo Bobina</TableCell>
<TableCell align="right">Cant. Ini.</TableCell>
<TableCell align="right">Kg Ini.</TableCell>
<TableCell align="right">Compradas</TableCell>
<TableCell align="right">Kg Compr.</TableCell>
<TableCell align="right">Consum.</TableCell>
<TableCell align="right">Kg Consum.</TableCell>
<TableCell align="right">Dañadas</TableCell>
<TableCell align="right">Kg Dañ.</TableCell>
<TableCell align="right">Cant. Fin.</TableCell>
<TableCell align="right">Kg Finales</TableCell>
</TableRow>
</TableHead>
<TableBody>
{reportData.map((row, idx) => (
<TableRow key={row.tipoBobina + idx}>
<TableCell>{row.tipoBobina}</TableCell>
<TableCell align="right">{row.bobinasIniciales.toLocaleString('es-AR')}</TableCell>
<TableCell align="right">{row.kilosIniciales.toLocaleString('es-AR')}</TableCell>
<TableCell align="right">{row.bobinasCompradas.toLocaleString('es-AR')}</TableCell>
<TableCell align="right">{row.kilosComprados.toLocaleString('es-AR')}</TableCell>
<TableCell align="right">{row.bobinasConsumidas.toLocaleString('es-AR')}</TableCell>
<TableCell align="right">{row.kilosConsumidos.toLocaleString('es-AR')}</TableCell>
<TableCell align="right">{row.bobinasDaniadas.toLocaleString('es-AR')}</TableCell>
<TableCell align="right">{row.kilosDaniados.toLocaleString('es-AR')}</TableCell>
<TableCell align="right">{row.bobinasFinales.toLocaleString('es-AR')}</TableCell>
<TableCell align="right">{row.kilosFinales.toLocaleString('es-AR')}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
{!loading && !error && reportData.length > 0 && (
<Paper sx={{ width: '100%', mt: 2 }}>
<DataGrid
rows={rows}
columns={columns}
localeText={esES.components.MuiDataGrid.defaultProps.localeText}
slots={{ footer: CustomFooter }}
density="compact"
autoHeight // Para que se ajuste al contenido y al footer
hideFooterSelectedRowCount
disableRowSelectionOnClick
/>
</Paper>
)}
{!loading && !error && reportData.length === 0 && currentParams && (<Typography sx={{mt: 2, fontStyle: 'italic'}}>No se encontraron datos para los criterios seleccionados.</Typography>)}
</Box>
);
};