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,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> | ||||
|   ); | ||||
| }; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user