2025-05-31 23:48:42 -03:00
import React , { useState , useCallback , useMemo } from 'react' ;
2025-05-29 15:10:02 -03:00
import {
2025-05-31 23:48:42 -03:00
Box , Typography , Paper , CircularProgress , Alert , Button
2025-05-29 15:10:02 -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 type { Theme } from '@mui/material/styles' ;
import { esES } from '@mui/x-data-grid/locales' ;
2025-05-29 15:10:02 -03:00
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' ;
2025-05-31 23:48:42 -03:00
// Interfaz extendida para DataGrid
interface ComparativaConsumoBobinasDataGridDto extends ComparativaConsumoBobinasDto {
id : string ;
}
2025-05-29 15:10:02 -03:00
const ReporteComparativaConsumoBobinasPage : React.FC = ( ) = > {
2025-05-31 23:48:42 -03:00
const [ reportData , setReportData ] = useState < ComparativaConsumoBobinasDataGridDto [ ] > ( [ ] ) ; // Usar tipo extendido
2025-05-29 15:10:02 -03:00
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 < {
fechaInicioMesA : string ; fechaFinMesA : string ;
fechaInicioMesB : string ; fechaFinMesB : string ;
idPlanta? : number | null ; consolidado : boolean ;
2025-05-31 23:48:42 -03:00
nombrePlanta? : string ;
mesA? : string ;
mesB? : string ;
2025-05-29 15:10:02 -03:00
} | null > ( null ) ;
2025-05-31 23:48:42 -03:00
const numberLocaleFormatter = ( value : number | null | undefined ) = >
value != null ? Number ( value ) . toLocaleString ( 'es-AR' ) : '' ;
2025-05-29 15:10:02 -03:00
const handleGenerarReporte = useCallback ( async ( params : {
fechaInicioMesA : string ; fechaFinMesA : string ;
fechaInicioMesB : string ; fechaFinMesB : string ;
idPlanta? : number | null ; consolidado : boolean ;
} ) = > {
setLoading ( true ) ;
setError ( null ) ;
setApiErrorParams ( null ) ;
setReportData ( [ ] ) ;
let plantaNombre = "Consolidado" ;
if ( ! params . consolidado && params . idPlanta ) {
const plantaService = ( await import ( '../../services/Impresion/plantaService' ) ) . default ;
const plantaData = await plantaService . getPlantaById ( params . idPlanta ) ;
plantaNombre = plantaData ? . nombre ? ? "N/A" ;
}
2025-05-31 23:48:42 -03:00
// 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 )
} ) ;
2025-05-29 15:10:02 -03:00
try {
const data = await reportesService . getComparativaConsumoBobinas ( params ) ;
2025-05-31 23:48:42 -03:00
const dataWithIds = data . map ( ( item , index ) = > ( { . . . item , id : ` ${ item . tipoBobina } - ${ index } ` } ) ) ;
setReportData ( dataWithIds ) ;
if ( dataWithIds . length === 0 ) {
2025-05-29 15:10:02 -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 ) ;
} finally {
setLoading ( false ) ;
}
} , [ ] ) ;
const handleVolverAParametros = useCallback ( ( ) = > {
setShowParamSelector ( true ) ;
setReportData ( [ ] ) ;
setError ( null ) ;
setApiErrorParams ( null ) ;
setCurrentParams ( null ) ;
} , [ ] ) ;
const handleExportToExcel = useCallback ( ( ) = > {
if ( reportData . length === 0 ) {
alert ( "No hay datos para exportar." ) ;
return ;
}
2025-05-31 23:48:42 -03:00
const dataToExport = reportData . map ( ( { . . . rest } ) = > rest ) . map ( item = > ( {
2025-05-29 15:10:02 -03:00
"Tipo Bobina" : item . tipoBobina ,
"Cant. Mes A" : item . bobinasUtilizadasMesA ,
"Cant. Mes B" : item . bobinasUtilizadasMesB ,
"Dif. Cant." : item . diferenciaBobinasUtilizadas ,
"Kg Mes A" : item . kilosUtilizadosMesA ,
"Kg Mes B" : item . kilosUtilizadosMesB ,
"Dif. Kg" : item . diferenciaKilosUtilizados ,
} ) ) ;
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" ] ) ;
acc . kgB += Number ( row [ "Kg Mes B" ] ) ; acc . difKg += Number ( row [ "Dif. Kg" ] ) ;
return acc ;
} , { cantA :0 , cantB :0 , difCant :0 , kgA :0 , kgB :0 , difKg :0 } ) ;
dataToExport . push ( {
"Tipo Bobina" : "TOTALES" , "Cant. Mes A" : totales . cantA , "Cant. Mes B" : totales . cantB ,
"Dif. Cant." : totales . difCant , "Kg Mes A" : totales . kgA , "Kg Mes B" : totales . kgB ,
"Dif. Kg" : totales . difKg ,
} ) ;
const ws = XLSX . utils . json_to_sheet ( dataToExport ) ;
const headers = Object . keys ( dataToExport [ 0 ] || { } ) ;
ws [ '!cols' ] = headers . map ( h = > {
const maxLen = Math . max ( . . . dataToExport . map ( row = > ( row as any ) [ h ] ? . toString ( ) . length ? ? 0 ) , h . length ) ;
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" ;
if ( currentParams ) {
fileName += ` _ ${ currentParams . consolidado ? 'Consolidado' : ` Planta ${ currentParams . idPlanta } ` } ` ;
fileName += ` _ ${ currentParams . fechaInicioMesA } _vs_ ${ currentParams . fechaInicioMesB } ` ;
}
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 . getComparativaConsumoBobinasPdf ( 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
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 >
) ;
} ;
2025-05-29 15:10:02 -03:00
if ( showParamSelector ) {
return (
< Box sx = { { p : 2 , display : 'flex' , justifyContent : 'center' , mt : 2 } } >
< Paper sx = { { width : '100%' , maxWidth : 600 } } elevation = { 3 } >
< SeleccionaReporteComparativaConsumoBobinas
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 : Comparativa Consumo de Bobinas < / Typography >
< Box sx = { { display : 'flex' , gap : 1 } } >
2025-05-31 23:48:42 -03:00
< Button onClick = { handleGenerarYAbrirPdf } variant = "contained" disabled = { loadingPdf || reportData . length === 0 || ! ! error } size = "small" >
2025-05-29 15:10:02 -03:00
{ loadingPdf ? < CircularProgress size = { 20 } color = "inherit" / > : "Abrir PDF" }
< / Button >
< Button onClick = { handleExportToExcel } variant = "outlined" disabled = { reportData . length === 0 || ! ! error } size = "small" >
Exportar a Excel
< / Button >
< Button onClick = { handleVolverAParametros } variant = "outlined" color = "secondary" size = "small" >
Nuevos Parámetros
< / Button >
< / Box >
< / Box >
2025-05-31 23:48:42 -03:00
{ loading && < Box sx = { { textAlign : 'center' , my :2 } } > < CircularProgress / > < / Box > }
{ error && ! loading && < Alert severity = "info" sx = { { my : 2 } } > { error } < / Alert > }
2025-05-29 15:10:02 -03:00
{ ! loading && ! error && reportData . length > 0 && (
2025-05-31 23:48:42 -03:00
< 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 >
2025-05-29 15:10:02 -03:00
) }
2025-05-31 23:48:42 -03:00
{ ! loading && ! error && reportData . length === 0 && currentParams && ( < Typography sx = { { mt :2 , fontStyle : 'italic' } } > No se encontraron datos para los criterios seleccionados . < / Typography > ) }
2025-05-29 15:10:02 -03:00
< / Box >
) ;
} ;
export default ReporteComparativaConsumoBobinasPage ;