2025-05-31 23:48:42 -03:00
|
|
|
import React, { useState, useCallback, useMemo } from 'react';
|
2025-05-28 18:58:45 -03:00
|
|
|
import {
|
2025-05-31 23:48:42 -03:00
|
|
|
Box, Typography, Paper, CircularProgress, Alert, Button
|
2025-05-28 18:58:45 -03:00
|
|
|
} from '@mui/material';
|
2025-05-31 23:48:42 -03:00
|
|
|
import { DataGrid, type GridColDef, GridFooterContainer, GridFooter } from '@mui/x-data-grid';
|
|
|
|
|
import { esES } from '@mui/x-data-grid/locales';
|
2025-05-28 18:58:45 -03:00
|
|
|
import reportesService from '../../services/Reportes/reportesService';
|
|
|
|
|
import type { MovimientoBobinasPorEstadoResponseDto } from '../../models/dtos/Reportes/MovimientoBobinasPorEstadoResponseDto';
|
2025-06-03 18:42:56 -03:00
|
|
|
import type { MovimientoBobinaEstadoDetalleDto } from '../../models/dtos/Reportes/MovimientoBobinaEstadoDetalleDto';
|
|
|
|
|
import type { MovimientoBobinaEstadoTotalDto } from '../../models/dtos/Reportes/MovimientoBobinaEstadoTotalDto';
|
|
|
|
|
import { usePermissions } from '../../hooks/usePermissions';
|
2025-05-28 18:58:45 -03:00
|
|
|
import SeleccionaReporteMovimientoBobinasEstado from './SeleccionaReporteMovimientoBobinasEstado';
|
|
|
|
|
import * as XLSX from 'xlsx';
|
|
|
|
|
import axios from 'axios';
|
|
|
|
|
|
2025-05-31 23:48:42 -03:00
|
|
|
// Interfaces extendidas para DataGrid con 'id'
|
2025-06-03 18:42:56 -03:00
|
|
|
interface DetalleMovimientoDataGrid extends MovimientoBobinaEstadoDetalleDto {
|
2025-05-31 23:48:42 -03:00
|
|
|
id: string;
|
|
|
|
|
}
|
2025-06-03 18:42:56 -03:00
|
|
|
interface TotalPorEstadoDataGrid extends MovimientoBobinaEstadoTotalDto {
|
2025-05-31 23:48:42 -03:00
|
|
|
id: string;
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-28 18:58:45 -03:00
|
|
|
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;
|
2025-05-31 23:48:42 -03:00
|
|
|
nombrePlanta?: string;
|
2025-05-28 18:58:45 -03:00
|
|
|
} | null>(null);
|
2025-06-03 18:42:56 -03:00
|
|
|
const { tienePermiso, isSuperAdmin } = usePermissions();
|
|
|
|
|
const puedeVerReporte = isSuperAdmin || tienePermiso("RR006");
|
2025-05-28 18:58:45 -03:00
|
|
|
|
2025-05-31 23:48:42 -03:00
|
|
|
const numberLocaleFormatter = (value: number | null | undefined) =>
|
|
|
|
|
value != null ? Number(value).toLocaleString('es-AR') : '';
|
2025-06-03 18:42:56 -03:00
|
|
|
|
2025-05-31 23:48:42 -03:00
|
|
|
const dateLocaleFormatter = (value: string | null | undefined) =>
|
|
|
|
|
value ? new Date(value).toLocaleDateString('es-AR', { timeZone: 'UTC' }) : '-';
|
|
|
|
|
|
|
|
|
|
|
2025-05-28 18:58:45 -03:00
|
|
|
const handleGenerarReporte = useCallback(async (params: {
|
|
|
|
|
fechaDesde: string;
|
|
|
|
|
fechaHasta: string;
|
|
|
|
|
idPlanta: number;
|
|
|
|
|
}) => {
|
2025-06-03 18:42:56 -03:00
|
|
|
if (!puedeVerReporte) {
|
|
|
|
|
setError("No tiene permiso para generar este reporte.");
|
|
|
|
|
setLoading(false);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-05-28 18:58:45 -03:00
|
|
|
setLoading(true);
|
|
|
|
|
setError(null);
|
|
|
|
|
setApiErrorParams(null);
|
|
|
|
|
setCurrentParams(params);
|
|
|
|
|
try {
|
|
|
|
|
const data = await reportesService.getMovimientoBobinasEstado(params);
|
2025-06-03 18:42:56 -03:00
|
|
|
|
2025-05-31 23:48:42 -03:00
|
|
|
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)) {
|
2025-05-28 18:58:45 -03:00
|
|
|
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();
|
|
|
|
|
|
|
|
|
|
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] || {});
|
2025-05-31 23:48:42 -03:00
|
|
|
wsDetalle['!cols'] = headersDetalle.map(h => ({ wch: Math.max(...detalleToExport.map(row => (row as any)[h]?.toString().length ?? 0), h.length) + 2 }));
|
2025-05-28 18:58:45 -03:00
|
|
|
wsDetalle['!freeze'] = { xSplit: 0, ySplit: 1 };
|
|
|
|
|
XLSX.utils.book_append_sheet(wb, wsDetalle, "DetalleMovimientos");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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] || {});
|
2025-05-31 23:48:42 -03:00
|
|
|
wsTotales['!cols'] = headersTotales.map(h => ({ wch: Math.max(...totalesToExport.map(row => (row as any)[h]?.toString().length ?? 0), h.length) + 2 }));
|
2025-05-28 18:58:45 -03:00
|
|
|
wsTotales['!freeze'] = { xSplit: 0, ySplit: 1 };
|
|
|
|
|
XLSX.utils.book_append_sheet(wb, wsTotales, "TotalesPorEstado");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let fileName = "ReporteMovimientoBobinasEstado";
|
|
|
|
|
if (currentParams) {
|
2025-05-31 23:48:42 -03:00
|
|
|
fileName += `_${currentParams.nombrePlanta || `Planta${currentParams.idPlanta}`}`;
|
|
|
|
|
fileName += `_${currentParams.fechaDesde}_a_${currentParams.fechaHasta}`;
|
2025-05-28 18:58:45 -03:00
|
|
|
}
|
|
|
|
|
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]);
|
|
|
|
|
|
2025-05-31 23:48:42 -03:00
|
|
|
// 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={{
|
2025-06-03 18:42:56 -03:00
|
|
|
display: 'flex',
|
|
|
|
|
justifyContent: 'space-between',
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
width: '100%',
|
|
|
|
|
borderTop: (theme) => `1px solid ${theme.palette.divider}`,
|
|
|
|
|
minHeight: '52px',
|
2025-05-31 23:48:42 -03:00
|
|
|
}}>
|
2025-06-03 18:42:56 -03:00
|
|
|
<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>
|
2025-05-31 23:48:42 -03:00
|
|
|
</GridFooterContainer>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
2025-05-28 18:58:45 -03:00
|
|
|
if (showParamSelector) {
|
2025-06-03 18:42:56 -03:00
|
|
|
if (!loading && !puedeVerReporte) { // Si no tiene permiso Y no está cargando, muestra error
|
|
|
|
|
return <Alert severity="error" sx={{ m: 2 }}>No tiene permiso para acceder a este reporte.</Alert>;
|
|
|
|
|
}
|
2025-05-28 18:58:45 -03:00
|
|
|
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>
|
|
|
|
|
);
|
|
|
|
|
}
|
2025-06-03 18:42:56 -03:00
|
|
|
|
2025-05-28 18:58:45 -03:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Box sx={{ p: 2 }}>
|
|
|
|
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2, flexWrap: 'wrap', gap: 1 }}>
|
2025-05-31 23:48:42 -03:00
|
|
|
<Typography variant="h5">Reporte: Movimiento de Bobinas por Estado {currentParams?.nombrePlanta ? `(${currentParams.nombrePlanta})` : ''}</Typography>
|
2025-05-28 18:58:45 -03:00
|
|
|
<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>
|
|
|
|
|
|
2025-06-03 18:42:56 -03:00
|
|
|
{loading && <Box sx={{ textAlign: 'center', my: 2 }}><CircularProgress /></Box>}
|
2025-05-31 23:48:42 -03:00
|
|
|
{error && !loading && <Alert severity="info" sx={{ my: 2 }}>{error}</Alert>}
|
2025-05-28 18:58:45 -03:00
|
|
|
|
|
|
|
|
{!loading && !error && reportData && (
|
2025-05-31 23:48:42 -03:00
|
|
|
<>
|
2025-06-03 18:42:56 -03:00
|
|
|
<Typography variant="h6" gutterBottom sx={{ mt: 3 }}>
|
2025-05-28 18:58:45 -03:00
|
|
|
Detalle de Movimientos
|
|
|
|
|
</Typography>
|
2025-05-31 23:48:42 -03:00
|
|
|
{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 }}
|
2025-06-03 18:42:56 -03:00
|
|
|
|
2025-05-31 23:48:42 -03:00
|
|
|
/>
|
|
|
|
|
</Paper>
|
2025-05-28 18:58:45 -03:00
|
|
|
) : (
|
2025-05-31 23:48:42 -03:00
|
|
|
<Typography sx={{ mb: 3, fontStyle: 'italic' }}>No hay detalles de movimientos para mostrar.</Typography>
|
2025-05-28 18:58:45 -03:00
|
|
|
)}
|
|
|
|
|
|
2025-05-31 23:48:42 -03:00
|
|
|
<Typography variant="h6" gutterBottom sx={{ mt: 3 }}>
|
2025-05-28 18:58:45 -03:00
|
|
|
Totales por Estado
|
|
|
|
|
</Typography>
|
2025-05-31 23:48:42 -03:00
|
|
|
{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>
|
2025-05-28 18:58:45 -03:00
|
|
|
) : (
|
2025-06-03 18:42:56 -03:00
|
|
|
<Typography sx={{ fontStyle: 'italic' }}>No hay totales por estado para mostrar.</Typography>
|
2025-05-28 18:58:45 -03:00
|
|
|
)}
|
|
|
|
|
</>
|
|
|
|
|
)}
|
2025-06-03 18:42:56 -03:00
|
|
|
{!loading && !error && !reportData && currentParams && (<Typography sx={{ mt: 2, fontStyle: 'italic' }}>No se encontraron datos para los criterios seleccionados.</Typography>)}
|
2025-05-28 18:58:45 -03:00
|
|
|
</Box>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export default ReporteMovimientoBobinasEstadoPage;
|