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,14 +1,27 @@
|
||||
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';
|
||||
import { esES } from '@mui/x-data-grid/locales';
|
||||
import reportesService from '../../services/Reportes/reportesService';
|
||||
// Corregir importaciones de DTOs
|
||||
import type { MovimientoBobinasPorEstadoResponseDto } from '../../models/dtos/Reportes/MovimientoBobinasPorEstadoResponseDto';
|
||||
import type { MovimientoBobinaEstadoDetalleDto } from '../../models/dtos/Reportes/MovimientoBobinaEstadoDetalleDto';
|
||||
import type { MovimientoBobinaEstadoTotalDto } from '../../models/dtos/Reportes/MovimientoBobinaEstadoTotalDto';
|
||||
|
||||
import SeleccionaReporteMovimientoBobinasEstado from './SeleccionaReporteMovimientoBobinasEstado';
|
||||
import * as XLSX from 'xlsx';
|
||||
import axios from 'axios';
|
||||
|
||||
// Interfaces extendidas para DataGrid con 'id'
|
||||
interface DetalleMovimientoDataGrid extends MovimientoBobinaEstadoDetalleDto { // Usar el DTO correcto
|
||||
id: string;
|
||||
}
|
||||
interface TotalPorEstadoDataGrid extends MovimientoBobinaEstadoTotalDto { // Usar el DTO correcto
|
||||
id: string;
|
||||
}
|
||||
|
||||
const ReporteMovimientoBobinasEstadoPage: React.FC = () => {
|
||||
const [reportData, setReportData] = useState<MovimientoBobinasPorEstadoResponseDto | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -20,8 +33,16 @@ const ReporteMovimientoBobinasEstadoPage: 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 dateLocaleFormatter = (value: string | null | undefined) =>
|
||||
value ? new Date(value).toLocaleDateString('es-AR', { timeZone: 'UTC' }) : '-';
|
||||
|
||||
|
||||
const handleGenerarReporte = useCallback(async (params: {
|
||||
fechaDesde: string;
|
||||
fechaHasta: string;
|
||||
@@ -33,8 +54,14 @@ const ReporteMovimientoBobinasEstadoPage: React.FC = () => {
|
||||
setCurrentParams(params);
|
||||
try {
|
||||
const data = await reportesService.getMovimientoBobinasEstado(params);
|
||||
setReportData(data);
|
||||
if ((!data.detalle || data.detalle.length === 0) && (!data.totales || data.totales.length === 0)) {
|
||||
|
||||
const processedData: MovimientoBobinasPorEstadoResponseDto = {
|
||||
detalle: data.detalle?.map((item, index) => ({ ...item, id: `detalle-${index}-${item.numeroRemito}-${item.tipoBobina}` })) || [],
|
||||
totales: data.totales?.map((item, index) => ({ ...item, id: `total-${index}-${item.tipoMovimiento}` })) || []
|
||||
};
|
||||
setReportData(processedData);
|
||||
|
||||
if ((!processedData.detalle || processedData.detalle.length === 0) && (!processedData.totales || processedData.totales.length === 0)) {
|
||||
setError("No se encontraron datos para los parámetros seleccionados.");
|
||||
}
|
||||
setShowParamSelector(false);
|
||||
@@ -65,7 +92,6 @@ const ReporteMovimientoBobinasEstadoPage: React.FC = () => {
|
||||
|
||||
const wb = XLSX.utils.book_new();
|
||||
|
||||
// Hoja de Detalles
|
||||
if (reportData.detalle?.length) {
|
||||
const detalleToExport = reportData.detalle.map(item => ({
|
||||
"Tipo Bobina": item.tipoBobina,
|
||||
@@ -76,18 +102,11 @@ const ReporteMovimientoBobinasEstadoPage: React.FC = () => {
|
||||
}));
|
||||
const wsDetalle = XLSX.utils.json_to_sheet(detalleToExport);
|
||||
const headersDetalle = Object.keys(detalleToExport[0] || {});
|
||||
wsDetalle['!cols'] = headersDetalle.map(h => {
|
||||
const maxLen = detalleToExport.reduce((prev, row) => {
|
||||
const cell = (row as any)[h]?.toString() ?? '';
|
||||
return Math.max(prev, cell.length);
|
||||
}, h.length);
|
||||
return { wch: maxLen + 2 };
|
||||
});
|
||||
wsDetalle['!cols'] = headersDetalle.map(h => ({ wch: Math.max(...detalleToExport.map(row => (row as any)[h]?.toString().length ?? 0), h.length) + 2 }));
|
||||
wsDetalle['!freeze'] = { xSplit: 0, ySplit: 1 };
|
||||
XLSX.utils.book_append_sheet(wb, wsDetalle, "DetalleMovimientos");
|
||||
}
|
||||
|
||||
// Hoja de Totales
|
||||
if (reportData.totales?.length) {
|
||||
const totalesToExport = reportData.totales.map(item => ({
|
||||
"Tipo Movimiento": item.tipoMovimiento,
|
||||
@@ -96,20 +115,15 @@ const ReporteMovimientoBobinasEstadoPage: React.FC = () => {
|
||||
}));
|
||||
const wsTotales = XLSX.utils.json_to_sheet(totalesToExport);
|
||||
const headersTotales = Object.keys(totalesToExport[0] || {});
|
||||
wsTotales['!cols'] = headersTotales.map(h => {
|
||||
const maxLen = totalesToExport.reduce((prev, row) => {
|
||||
const cell = (row as any)[h]?.toString() ?? '';
|
||||
return Math.max(prev, cell.length);
|
||||
}, h.length);
|
||||
return { wch: maxLen + 2 };
|
||||
});
|
||||
wsTotales['!cols'] = headersTotales.map(h => ({ wch: Math.max(...totalesToExport.map(row => (row as any)[h]?.toString().length ?? 0), h.length) + 2 }));
|
||||
wsTotales['!freeze'] = { xSplit: 0, ySplit: 1 };
|
||||
XLSX.utils.book_append_sheet(wb, wsTotales, "TotalesPorEstado");
|
||||
}
|
||||
|
||||
let fileName = "ReporteMovimientoBobinasEstado";
|
||||
if (currentParams) {
|
||||
fileName += `_${currentParams.fechaDesde}_a_${currentParams.fechaHasta}_Planta${currentParams.idPlanta}`;
|
||||
fileName += `_${currentParams.nombrePlanta || `Planta${currentParams.idPlanta}`}`;
|
||||
fileName += `_${currentParams.fechaDesde}_a_${currentParams.fechaHasta}`;
|
||||
}
|
||||
fileName += ".xlsx";
|
||||
XLSX.writeFile(wb, fileName);
|
||||
@@ -140,6 +154,56 @@ const ReporteMovimientoBobinasEstadoPage: React.FC = () => {
|
||||
}
|
||||
}, [currentParams]);
|
||||
|
||||
// Columnas para DataGrid de Detalle de Movimientos
|
||||
const columnsDetalle: GridColDef<DetalleMovimientoDataGrid>[] = [ // Tipar con la interfaz correcta
|
||||
{ field: 'tipoBobina', headerName: 'Tipo Bobina', width: 220, flex: 1.5 },
|
||||
{ field: 'numeroRemito', headerName: 'Nro Remito', width: 130, flex: 0.8 },
|
||||
{ field: 'fechaMovimiento', headerName: 'Fecha Movimiento', width: 150, flex: 1, valueFormatter: (value) => dateLocaleFormatter(value as string) },
|
||||
{ field: 'cantidad', headerName: 'Cantidad', type: 'number', width: 120, align: 'right', headerAlign: 'right', flex: 0.7, valueFormatter: (value) => numberLocaleFormatter(Number(value)) },
|
||||
{ field: 'tipoMovimiento', headerName: 'Tipo Movimiento', width: 150, flex: 1 },
|
||||
];
|
||||
|
||||
// Columnas para DataGrid de Totales por Estado
|
||||
const columnsTotales: GridColDef<TotalPorEstadoDataGrid>[] = [ // Tipar con la interfaz correcta
|
||||
{ field: 'tipoMovimiento', headerName: 'Tipo Movimiento', width: 200, flex: 1 },
|
||||
{ field: 'totalBobinas', headerName: 'Total Bobinas', type: 'number', width: 150, align: 'right', headerAlign: 'right', flex: 0.8, valueFormatter: (value) => numberLocaleFormatter(Number(value)) },
|
||||
{ field: 'totalKilos', headerName: 'Total Kilos', type: 'number', width: 150, align: 'right', headerAlign: 'right', flex: 0.8, valueFormatter: (value) => numberLocaleFormatter(Number(value)) },
|
||||
];
|
||||
|
||||
const rowsDetalle = useMemo(() => (reportData?.detalle as DetalleMovimientoDataGrid[]) || [], [reportData]);
|
||||
const rowsTotales = useMemo(() => (reportData?.totales as TotalPorEstadoDataGrid[]) || [], [reportData]);
|
||||
|
||||
// eslint-disable-next-line react/display-name
|
||||
const CustomFooterDetalle = () => (
|
||||
<GridFooterContainer sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
borderTop: (theme) => `1px solid ${theme.palette.divider}`,
|
||||
minHeight: '52px',
|
||||
}}>
|
||||
<Box sx={{
|
||||
flexGrow:1,
|
||||
display:'flex',
|
||||
justifyContent:'flex-start',
|
||||
}}>
|
||||
<GridFooter
|
||||
sx={{
|
||||
borderTop: 'none',
|
||||
width: 'auto',
|
||||
'& .MuiToolbar-root': {
|
||||
paddingLeft: (theme) => theme.spacing(1),
|
||||
paddingRight: (theme) => theme.spacing(1),
|
||||
},
|
||||
'& .MuiDataGrid-selectedRowCount': { display: 'none' },
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</GridFooterContainer>
|
||||
);
|
||||
|
||||
|
||||
if (showParamSelector) {
|
||||
return (
|
||||
<Box sx={{ p: 2, display: 'flex', justifyContent: 'center', mt: 2 }}>
|
||||
@@ -154,11 +218,12 @@ const ReporteMovimientoBobinasEstadoPage: React.FC = () => {
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
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 por Estado</Typography>
|
||||
<Typography variant="h5">Reporte: Movimiento de Bobinas por Estado {currentParams?.nombrePlanta ? `(${currentParams.nombrePlanta})` : ''}</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<Button
|
||||
onClick={handleGenerarYAbrirPdf}
|
||||
@@ -182,74 +247,51 @@ const ReporteMovimientoBobinasEstadoPage: 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 && (
|
||||
<> {/* Usamos un Fragmento React para agrupar los elementos sin añadir un div extra */}
|
||||
{/* Tabla de Detalle de Movimientos */}
|
||||
<>
|
||||
<Typography variant="h6" gutterBottom sx={{ mt: 3 }}>
|
||||
Detalle de Movimientos
|
||||
</Typography>
|
||||
{reportData.detalle && reportData.detalle.length > 0 ? (
|
||||
<TableContainer component={Paper} sx={{ maxHeight: '400px', mb: 3 }}> {/* Añadido mb: 3 para espaciado */}
|
||||
<Table stickyHeader size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Tipo Bobina</TableCell>
|
||||
<TableCell>Nro Remito</TableCell>
|
||||
<TableCell>Fecha Movimiento</TableCell>
|
||||
<TableCell align="right">Cantidad</TableCell>
|
||||
<TableCell>Tipo Movimiento</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{reportData.detalle.map((row, idx) => (
|
||||
<TableRow key={`detalle-${idx}`}>
|
||||
<TableCell>{row.tipoBobina}</TableCell>
|
||||
<TableCell>{row.numeroRemito}</TableCell>
|
||||
<TableCell>{row.fechaMovimiento ? new Date(row.fechaMovimiento).toLocaleDateString('es-AR', { timeZone: 'UTC' }) : '-'}</TableCell>
|
||||
<TableCell align="right">{row.cantidad.toLocaleString('es-AR')}</TableCell>
|
||||
<TableCell>{row.tipoMovimiento}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
{rowsDetalle.length > 0 ? (
|
||||
<Paper sx={{ width: '100%', mb: 3 }}>
|
||||
<DataGrid
|
||||
rows={rowsDetalle}
|
||||
columns={columnsDetalle}
|
||||
localeText={esES.components.MuiDataGrid.defaultProps.localeText}
|
||||
density="compact"
|
||||
sx={{ height: 'calc(100vh - 350px)' }}
|
||||
slots={{ footer: CustomFooterDetalle }}
|
||||
|
||||
/>
|
||||
</Paper>
|
||||
) : (
|
||||
<Typography sx={{ mb: 3 }}>No hay detalles de movimientos para mostrar.</Typography>
|
||||
<Typography sx={{ mb: 3, fontStyle: 'italic' }}>No hay detalles de movimientos para mostrar.</Typography>
|
||||
)}
|
||||
|
||||
{/* Tabla de Totales por Estado */}
|
||||
<Typography variant="h6" gutterBottom>
|
||||
<Typography variant="h6" gutterBottom sx={{ mt: 3 }}>
|
||||
Totales por Estado
|
||||
</Typography>
|
||||
{reportData.totales && reportData.totales.length > 0 ? (
|
||||
<TableContainer component={Paper} sx={{ maxWidth: '600px' }}> {/* Limitamos el ancho para tablas pequeñas */}
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Tipo Movimiento</TableCell>
|
||||
<TableCell align="right">Total Bobinas</TableCell>
|
||||
<TableCell align="right">Total Kilos</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{reportData.totales.map((row, idx) => (
|
||||
<TableRow key={`total-${idx}`}>
|
||||
<TableCell>{row.tipoMovimiento}</TableCell>
|
||||
<TableCell align="right">{row.totalBobinas.toLocaleString('es-AR')}</TableCell>
|
||||
<TableCell align="right">{row.totalKilos.toLocaleString('es-AR')}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
{rowsTotales.length > 0 ? (
|
||||
<Paper sx={{ width: '100%', maxWidth: '700px' }}>
|
||||
<DataGrid
|
||||
rows={rowsTotales}
|
||||
columns={columnsTotales}
|
||||
localeText={esES.components.MuiDataGrid.defaultProps.localeText}
|
||||
density="compact"
|
||||
autoHeight
|
||||
hideFooter
|
||||
disableRowSelectionOnClick
|
||||
/>
|
||||
</Paper>
|
||||
) : (
|
||||
<Typography>No hay totales para mostrar.</Typography>
|
||||
<Typography sx={{fontStyle: 'italic'}}>No hay totales por estado para mostrar.</Typography>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{!loading && !error && !reportData && currentParams && (<Typography sx={{mt: 2, fontStyle: 'italic'}}>No se encontraron datos para los criterios seleccionados.</Typography>)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user