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,17 +1,23 @@
|
||||
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,
|
||||
TableFooter
|
||||
Box, Typography, Paper, CircularProgress, Alert, Button
|
||||
} from '@mui/material';
|
||||
import { DataGrid, type GridColDef, GridFooterContainer, GridFooter } from '@mui/x-data-grid';
|
||||
import type {Theme} from '@mui/material/styles';
|
||||
import { esES } from '@mui/x-data-grid/locales';
|
||||
import reportesService from '../../services/Reportes/reportesService';
|
||||
import type { ComparativaConsumoBobinasDto } from '../../models/dtos/Reportes/ComparativaConsumoBobinasDto';
|
||||
import SeleccionaReporteComparativaConsumoBobinas from './SeleccionaReporteComparativaConsumoBobinas';
|
||||
import * as XLSX from 'xlsx';
|
||||
import axios from 'axios';
|
||||
|
||||
// Interfaz extendida para DataGrid
|
||||
interface ComparativaConsumoBobinasDataGridDto extends ComparativaConsumoBobinasDto {
|
||||
id: string;
|
||||
}
|
||||
|
||||
const ReporteComparativaConsumoBobinasPage: React.FC = () => {
|
||||
const [reportData, setReportData] = useState<ComparativaConsumoBobinasDto[]>([]);
|
||||
const [reportData, setReportData] = useState<ComparativaConsumoBobinasDataGridDto[]>([]); // Usar tipo extendido
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadingPdf, setLoadingPdf] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -21,9 +27,14 @@ const ReporteComparativaConsumoBobinasPage: React.FC = () => {
|
||||
fechaInicioMesA: string; fechaFinMesA: string;
|
||||
fechaInicioMesB: string; fechaFinMesB: string;
|
||||
idPlanta?: number | null; consolidado: boolean;
|
||||
nombrePlanta?: string; // Para el PDF
|
||||
nombrePlanta?: string;
|
||||
mesA?: string;
|
||||
mesB?: string;
|
||||
} | null>(null);
|
||||
|
||||
const numberLocaleFormatter = (value: number | null | undefined) =>
|
||||
value != null ? Number(value).toLocaleString('es-AR') : '';
|
||||
|
||||
const handleGenerarReporte = useCallback(async (params: {
|
||||
fechaInicioMesA: string; fechaFinMesA: string;
|
||||
fechaInicioMesB: string; fechaFinMesB: string;
|
||||
@@ -40,12 +51,24 @@ const ReporteComparativaConsumoBobinasPage: React.FC = () => {
|
||||
const plantaData = await plantaService.getPlantaById(params.idPlanta);
|
||||
plantaNombre = plantaData?.nombre ?? "N/A";
|
||||
}
|
||||
setCurrentParams({...params, nombrePlanta: plantaNombre});
|
||||
// Formatear nombres de meses para el PDF
|
||||
const formatMonthYear = (dateString: string) => {
|
||||
const date = new Date(dateString + 'T00:00:00'); // Asegurar que se parsea como local
|
||||
return date.toLocaleDateString('es-AR', { month: 'long', year: 'numeric', timeZone: 'UTC' });
|
||||
};
|
||||
|
||||
setCurrentParams({
|
||||
...params,
|
||||
nombrePlanta: plantaNombre,
|
||||
mesA: formatMonthYear(params.fechaInicioMesA),
|
||||
mesB: formatMonthYear(params.fechaInicioMesB)
|
||||
});
|
||||
|
||||
try {
|
||||
const data = await reportesService.getComparativaConsumoBobinas(params);
|
||||
setReportData(data);
|
||||
if (data.length === 0) {
|
||||
const dataWithIds = data.map((item, index) => ({ ...item, id: `${item.tipoBobina}-${index}` }));
|
||||
setReportData(dataWithIds);
|
||||
if (dataWithIds.length === 0) {
|
||||
setError("No se encontraron datos para los parámetros seleccionados.");
|
||||
}
|
||||
setShowParamSelector(false);
|
||||
@@ -72,7 +95,7 @@ const ReporteComparativaConsumoBobinasPage: React.FC = () => {
|
||||
alert("No hay datos para exportar.");
|
||||
return;
|
||||
}
|
||||
const dataToExport = reportData.map(item => ({
|
||||
const dataToExport = reportData.map(({ ...rest }) => rest).map(item => ({
|
||||
"Tipo Bobina": item.tipoBobina,
|
||||
"Cant. Mes A": item.bobinasUtilizadasMesA,
|
||||
"Cant. Mes B": item.bobinasUtilizadasMesB,
|
||||
@@ -82,7 +105,6 @@ const ReporteComparativaConsumoBobinasPage: React.FC = () => {
|
||||
"Dif. Kg": item.diferenciaKilosUtilizados,
|
||||
}));
|
||||
|
||||
// Totales
|
||||
const totales = dataToExport.reduce((acc, row) => {
|
||||
acc.cantA += Number(row["Cant. Mes A"]); acc.cantB += Number(row["Cant. Mes B"]);
|
||||
acc.difCant += Number(row["Dif. Cant."]); acc.kgA += Number(row["Kg Mes A"]);
|
||||
@@ -96,7 +118,6 @@ const ReporteComparativaConsumoBobinasPage: React.FC = () => {
|
||||
"Dif. Kg": totales.difKg,
|
||||
});
|
||||
|
||||
|
||||
const ws = XLSX.utils.json_to_sheet(dataToExport);
|
||||
const headers = Object.keys(dataToExport[0] || {});
|
||||
ws['!cols'] = headers.map(h => {
|
||||
@@ -104,7 +125,6 @@ const ReporteComparativaConsumoBobinasPage: React.FC = () => {
|
||||
return { wch: maxLen + 2 };
|
||||
});
|
||||
ws['!freeze'] = { xSplit: 0, ySplit: 1 };
|
||||
|
||||
const wb = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(wb, ws, "ComparativaConsumo");
|
||||
let fileName = "ReporteComparativaConsumoBobinas";
|
||||
@@ -141,6 +161,118 @@ const ReporteComparativaConsumoBobinasPage: React.FC = () => {
|
||||
}
|
||||
}, [currentParams]);
|
||||
|
||||
const columns: GridColDef<ComparativaConsumoBobinasDataGridDto>[] = [ // Tipar con la interfaz correcta
|
||||
{ field: 'tipoBobina', headerName: 'Tipo Bobina', width: 250, flex: 1.5 },
|
||||
{ field: 'bobinasUtilizadasMesA', headerName: 'Cant. Mes A', type: 'number', width: 120, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberLocaleFormatter(Number(value)) },
|
||||
{ field: 'bobinasUtilizadasMesB', headerName: 'Cant. Mes B', type: 'number', width: 120, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberLocaleFormatter(Number(value)) },
|
||||
{ field: 'diferenciaBobinasUtilizadas', headerName: 'Dif. Cant.', type: 'number', width: 110, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberLocaleFormatter(Number(value)) },
|
||||
{ field: 'kilosUtilizadosMesA', headerName: 'Kg Mes A', type: 'number', width: 120, align: 'right', headerAlign: 'right', cellClassName: 'separator-left', headerClassName: 'separator-left', valueFormatter: (value) => numberLocaleFormatter(Number(value)) },
|
||||
{ field: 'kilosUtilizadosMesB', headerName: 'Kg Mes B', type: 'number', width: 120, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberLocaleFormatter(Number(value)) },
|
||||
{ field: 'diferenciaKilosUtilizados', headerName: 'Dif. Kg', type: 'number', width: 110, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberLocaleFormatter(Number(value)) },
|
||||
];
|
||||
|
||||
const rows = useMemo(() => reportData, [reportData]);
|
||||
|
||||
// Calcular totales para el footer
|
||||
const totalesGenerales = useMemo(() => {
|
||||
if (reportData.length === 0) return null;
|
||||
return {
|
||||
bobinasUtilizadasMesA: reportData.reduce((sum, item) => sum + item.bobinasUtilizadasMesA, 0),
|
||||
bobinasUtilizadasMesB: reportData.reduce((sum, item) => sum + item.bobinasUtilizadasMesB, 0),
|
||||
diferenciaBobinasUtilizadas: reportData.reduce((sum, item) => sum + item.diferenciaBobinasUtilizadas, 0),
|
||||
kilosUtilizadosMesA: reportData.reduce((sum, item) => sum + item.kilosUtilizadosMesA, 0),
|
||||
kilosUtilizadosMesB: reportData.reduce((sum, item) => sum + item.kilosUtilizadosMesB, 0),
|
||||
diferenciaKilosUtilizados: reportData.reduce((sum, item) => sum + item.diferenciaKilosUtilizados, 0),
|
||||
};
|
||||
}, [reportData]);
|
||||
|
||||
|
||||
// eslint-disable-next-line react/display-name
|
||||
const CustomFooter = () => {
|
||||
if (!totalesGenerales) return null;
|
||||
|
||||
const getCellStyle = (field: (typeof columns)[number]['field'] | 'label', isLabel: boolean = false) => {
|
||||
const colConfig = columns.find(c => c.field === field);
|
||||
// Ajustar anchos para los totales para que sean más compactos
|
||||
let targetWidth: number | string = 'auto';
|
||||
let targetMinWidth: number | string = 'auto';
|
||||
|
||||
if (isLabel) {
|
||||
targetWidth = colConfig?.width ? Math.max(80, colConfig.width * 0.5) : 100; // Más corto para "TOTALES:"
|
||||
targetMinWidth = 80;
|
||||
} else if (colConfig) {
|
||||
targetWidth = colConfig.width ? Math.max(70, colConfig.width * 0.75) : 90; // 75% del ancho de columna, mínimo 70
|
||||
targetMinWidth = 70;
|
||||
}
|
||||
|
||||
const style: React.CSSProperties = {
|
||||
minWidth: targetMinWidth,
|
||||
width: targetWidth,
|
||||
textAlign: isLabel ? 'left' : (colConfig?.align || 'right') as 'left' | 'right' | 'center',
|
||||
paddingRight: isLabel ? 1 : (field === 'diferenciaKilosUtilizados' ? 0 : 1), // pr en theme units
|
||||
fontWeight: 'bold',
|
||||
whiteSpace: 'nowrap',
|
||||
};
|
||||
|
||||
// Aplicar el separador si es la columna 'kilosUtilizadosMesA'
|
||||
if (field === 'kilosUtilizadosMesA') {
|
||||
style.borderLeft = `2px solid grey`; // O theme.palette.divider
|
||||
style.paddingLeft = '8px'; // Espacio después del separador
|
||||
}
|
||||
return style;
|
||||
};
|
||||
|
||||
return (
|
||||
<GridFooterContainer sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
borderTop: (theme) => `1px solid ${theme.palette.divider}`,
|
||||
minHeight: '52px',
|
||||
}}>
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexShrink: 0,
|
||||
overflow: 'hidden',
|
||||
px:1,
|
||||
}}>
|
||||
<GridFooter
|
||||
sx={{
|
||||
borderTop: 'none',
|
||||
width: 'auto',
|
||||
'& .MuiToolbar-root': {
|
||||
paddingLeft: 0,
|
||||
paddingRight: 0,
|
||||
},
|
||||
'& .MuiDataGrid-selectedRowCount': { display: 'none' },
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
fontWeight: 'bold',
|
||||
whiteSpace: 'nowrap',
|
||||
overflowX: 'auto',
|
||||
px:1,
|
||||
flexShrink: 1,
|
||||
}}>
|
||||
<Typography variant="subtitle2" sx={getCellStyle('label', true)}>TOTALES:</Typography>
|
||||
<Typography variant="subtitle2" sx={getCellStyle('bobinasUtilizadasMesA')}>{numberLocaleFormatter(totalesGenerales.bobinasUtilizadasMesA)}</Typography>
|
||||
<Typography variant="subtitle2" sx={getCellStyle('bobinasUtilizadasMesB')}>{numberLocaleFormatter(totalesGenerales.bobinasUtilizadasMesB)}</Typography>
|
||||
<Typography variant="subtitle2" sx={getCellStyle('diferenciaBobinasUtilizadas')}>{numberLocaleFormatter(totalesGenerales.diferenciaBobinasUtilizadas)}</Typography>
|
||||
<Typography variant="subtitle2" sx={getCellStyle('kilosUtilizadosMesA')}>{numberLocaleFormatter(totalesGenerales.kilosUtilizadosMesA)}</Typography>
|
||||
<Typography variant="subtitle2" sx={getCellStyle('kilosUtilizadosMesB')}>{numberLocaleFormatter(totalesGenerales.kilosUtilizadosMesB)}</Typography>
|
||||
<Typography variant="subtitle2" sx={getCellStyle('diferenciaKilosUtilizados')}>{numberLocaleFormatter(totalesGenerales.diferenciaKilosUtilizados)}</Typography>
|
||||
</Box>
|
||||
</GridFooterContainer>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
if (showParamSelector) {
|
||||
return (
|
||||
<Box sx={{ p: 2, display: 'flex', justifyContent: 'center', mt: 2 }}>
|
||||
@@ -161,7 +293,7 @@ const ReporteComparativaConsumoBobinasPage: React.FC = () => {
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2, flexWrap: 'wrap', gap: 1 }}>
|
||||
<Typography variant="h5">Reporte: Comparativa Consumo de Bobinas</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<Button onClick={handleGenerarYAbrirPdf} variant="contained" disabled={loadingPdf || reportData.length === 0 || !!error} size="small">
|
||||
<Button onClick={handleGenerarYAbrirPdf} variant="contained" disabled={loadingPdf || reportData.length === 0 || !!error} size="small">
|
||||
{loadingPdf ? <CircularProgress size={20} color="inherit" /> : "Abrir PDF"}
|
||||
</Button>
|
||||
<Button onClick={handleExportToExcel} variant="outlined" disabled={reportData.length === 0 || !!error} size="small">
|
||||
@@ -173,51 +305,36 @@ const ReporteComparativaConsumoBobinasPage: 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 && reportData.length > 0 && (
|
||||
<TableContainer component={Paper} sx={{ maxHeight: 'calc(100vh - 240px)' }}>
|
||||
<Table stickyHeader size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Tipo Bobina</TableCell>
|
||||
<TableCell align="right">Cant. Mes A</TableCell>
|
||||
<TableCell align="right">Cant. Mes B</TableCell>
|
||||
<TableCell align="right">Dif. Cant.</TableCell>
|
||||
<TableCell align="right" sx={{ borderLeft: '2px solid grey' }}>Kg Mes A</TableCell>
|
||||
<TableCell align="right">Kg Mes B</TableCell>
|
||||
<TableCell align="right">Dif. Kg</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{reportData.map((row, idx) => (
|
||||
<TableRow key={`${row.tipoBobina}-${idx}`}>
|
||||
<TableCell>{row.tipoBobina}</TableCell>
|
||||
<TableCell align="right">{row.bobinasUtilizadasMesA.toLocaleString('es-AR')}</TableCell>
|
||||
<TableCell align="right">{row.bobinasUtilizadasMesB.toLocaleString('es-AR')}</TableCell>
|
||||
<TableCell align="right">{row.diferenciaBobinasUtilizadas.toLocaleString('es-AR')}</TableCell>
|
||||
<TableCell align="right" sx={{ borderLeft: '2px solid grey' }}>{row.kilosUtilizadosMesA.toLocaleString('es-AR')}</TableCell>
|
||||
<TableCell align="right">{row.kilosUtilizadosMesB.toLocaleString('es-AR')}</TableCell>
|
||||
<TableCell align="right">{row.diferenciaKilosUtilizados.toLocaleString('es-AR')}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
<TableFooter>
|
||||
<TableRow sx={{backgroundColor: 'grey.300'}}>
|
||||
<TableCell align="right" sx={{fontWeight: 'bold', fontSize: '1.1rem'}}>TOTALES:</TableCell>
|
||||
<TableCell align="right" sx={{fontWeight: 'bold', fontSize: '1.1rem'}}>{reportData.reduce((sum, item) => sum + item.bobinasUtilizadasMesA, 0).toLocaleString('es-AR')}</TableCell>
|
||||
<TableCell align="right" sx={{fontWeight: 'bold', fontSize: '1.1rem'}}>{reportData.reduce((sum, item) => sum + item.bobinasUtilizadasMesB, 0).toLocaleString('es-AR')}</TableCell>
|
||||
<TableCell align="right" sx={{fontWeight: 'bold', fontSize: '1.1rem'}}>{reportData.reduce((sum, item) => sum + item.diferenciaBobinasUtilizadas, 0).toLocaleString('es-AR')}</TableCell>
|
||||
<TableCell align="right" sx={{fontWeight: 'bold', fontSize: '1.1rem', borderLeft: '2px solid grey'}}>{reportData.reduce((sum, item) => sum + item.kilosUtilizadosMesA, 0).toLocaleString('es-AR')}</TableCell>
|
||||
<TableCell align="right" sx={{fontWeight: 'bold', fontSize: '1.1rem'}}>{reportData.reduce((sum, item) => sum + item.kilosUtilizadosMesB, 0).toLocaleString('es-AR')}</TableCell>
|
||||
<TableCell align="right" sx={{fontWeight: 'bold', fontSize: '1.1rem'}}>{reportData.reduce((sum, item) => sum + item.diferenciaKilosUtilizados, 0).toLocaleString('es-AR')}</TableCell>
|
||||
</TableRow>
|
||||
</TableFooter>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<Paper sx={{
|
||||
width: '100%',
|
||||
mt: 2,
|
||||
'& .separator-left': {
|
||||
borderLeft: (theme: Theme) => `2px solid ${theme.palette.divider}`,
|
||||
},
|
||||
}}>
|
||||
<DataGrid
|
||||
rows={rows}
|
||||
columns={columns}
|
||||
localeText={esES.components.MuiDataGrid.defaultProps.localeText}
|
||||
slots={{ footer: CustomFooter }}
|
||||
density="compact"
|
||||
sx={{ height: 'calc(100vh - 300px)' }} // Ajusta esta altura según sea necesario
|
||||
initialState={{
|
||||
pagination: {
|
||||
paginationModel: { pageSize: 10, page: 0 },
|
||||
},
|
||||
}}
|
||||
pageSizeOptions={[5, 10, 25, 50]}
|
||||
disableRowSelectionOnClick
|
||||
// hideFooterSelectedRowCount // Ya se maneja en CustomFooter
|
||||
/>
|
||||
</Paper>
|
||||
)}
|
||||
{!loading && !error && reportData.length === 0 && (<Typography>No se encontraron datos para los criterios seleccionados.</Typography>)}
|
||||
{!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