Files
GestionIntegralWeb/Frontend/src/pages/Reportes/ReporteMovimientoBobinasEstadoPage.tsx

257 lines
10 KiB
TypeScript
Raw Normal View History

import React, { useState, useCallback } from 'react';
import {
Box, Typography, Paper, CircularProgress, Alert, Button,
TableContainer, Table, TableHead, TableRow, TableCell, TableBody
} from '@mui/material';
import reportesService from '../../services/Reportes/reportesService';
import type { MovimientoBobinasPorEstadoResponseDto } from '../../models/dtos/Reportes/MovimientoBobinasPorEstadoResponseDto';
import SeleccionaReporteMovimientoBobinasEstado from './SeleccionaReporteMovimientoBobinasEstado';
import * as XLSX from 'xlsx';
import axios from 'axios';
const ReporteMovimientoBobinasEstadoPage: React.FC = () => {
const [reportData, setReportData] = useState<MovimientoBobinasPorEstadoResponseDto | null>(null);
const [loading, setLoading] = useState(false);
const [loadingPdf, setLoadingPdf] = useState(false);
const [error, setError] = useState<string | null>(null);
const [apiErrorParams, setApiErrorParams] = useState<string | null>(null);
const [showParamSelector, setShowParamSelector] = useState(true);
const [currentParams, setCurrentParams] = useState<{
fechaDesde: string;
fechaHasta: string;
idPlanta: number;
} | null>(null);
const handleGenerarReporte = useCallback(async (params: {
fechaDesde: string;
fechaHasta: string;
idPlanta: number;
}) => {
setLoading(true);
setError(null);
setApiErrorParams(null);
setCurrentParams(params);
try {
const data = await reportesService.getMovimientoBobinasEstado(params);
setReportData(data);
if ((!data.detalle || data.detalle.length === 0) && (!data.totales || data.totales.length === 0)) {
setError("No se encontraron datos para los parámetros seleccionados.");
}
setShowParamSelector(false);
} catch (err: any) {
const message = axios.isAxiosError(err) && err.response?.data?.message
? err.response.data.message
: 'Ocurrió un error al generar el reporte.';
setApiErrorParams(message);
setReportData(null);
} finally {
setLoading(false);
}
}, []);
const handleVolverAParametros = useCallback(() => {
setShowParamSelector(true);
setReportData(null);
setError(null);
setApiErrorParams(null);
setCurrentParams(null);
}, []);
const handleExportToExcel = useCallback(() => {
if (!reportData || (!reportData.detalle?.length && !reportData.totales?.length)) {
alert("No hay datos para exportar.");
return;
}
const wb = XLSX.utils.book_new();
// Hoja de Detalles
if (reportData.detalle?.length) {
const detalleToExport = reportData.detalle.map(item => ({
"Tipo Bobina": item.tipoBobina,
"Nro Remito": item.numeroRemito,
"Fecha Movimiento": item.fechaMovimiento ? new Date(item.fechaMovimiento).toLocaleDateString('es-AR', { timeZone: 'UTC' }) : '-',
"Cantidad": item.cantidad,
"Tipo Movimiento": item.tipoMovimiento,
}));
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['!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,
"Total Bobinas": item.totalBobinas,
"Total Kilos": item.totalKilos,
}));
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['!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 += ".xlsx";
XLSX.writeFile(wb, fileName);
}, [reportData, currentParams]);
const handleGenerarYAbrirPdf = useCallback(async () => {
if (!currentParams) {
setError("Primero debe generar el reporte en pantalla o seleccionar parámetros.");
return;
}
setLoadingPdf(true);
setError(null);
try {
const blob = await reportesService.getMovimientoBobinasEstadoPdf(currentParams);
if (blob.type === "application/json") {
const text = await blob.text();
const msg = JSON.parse(text).message ?? "Error inesperado al generar PDF.";
setError(msg);
} else {
const url = URL.createObjectURL(blob);
const w = window.open(url, '_blank');
if (!w) alert("Permite popups para ver el PDF.");
}
} catch {
setError('Ocurrió un error al generar el PDF.');
} finally {
setLoadingPdf(false);
}
}, [currentParams]);
if (showParamSelector) {
return (
<Box sx={{ p: 2, display: 'flex', justifyContent: 'center', mt: 2 }}>
<Paper sx={{ width: '100%', maxWidth: 600 }} elevation={3}>
<SeleccionaReporteMovimientoBobinasEstado
onGenerarReporte={handleGenerarReporte}
onCancel={handleVolverAParametros}
isLoading={loading}
apiErrorMessage={apiErrorParams}
/>
</Paper>
</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>
<Box sx={{ display: 'flex', gap: 1 }}>
<Button
onClick={handleGenerarYAbrirPdf}
variant="contained"
disabled={loadingPdf || !reportData || (!reportData.detalle?.length && !reportData.totales?.length) || !!error}
size="small"
>
{loadingPdf ? <CircularProgress size={20} color="inherit" /> : "Abrir PDF"}
</Button>
<Button
onClick={handleExportToExcel}
variant="outlined"
disabled={!reportData || (!reportData.detalle?.length && !reportData.totales?.length) || !!error}
size="small"
>
Exportar a Excel
</Button>
<Button onClick={handleVolverAParametros} variant="outlined" color="secondary" size="small">
Nuevos Parámetros
</Button>
</Box>
</Box>
{loading && <Box sx={{ textAlign: 'center' }}><CircularProgress /></Box>}
{error && !loading && <Alert severity="error" 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>
) : (
<Typography sx={{ mb: 3 }}>No hay detalles de movimientos para mostrar.</Typography>
)}
{/* Tabla de Totales por Estado */}
<Typography variant="h6" gutterBottom>
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>
) : (
<Typography>No hay totales para mostrar.</Typography>
)}
</>
)}
</Box>
);
};
export default ReporteMovimientoBobinasEstadoPage;