Continuidad de reportes Frontend. Se sigue..
This commit is contained in:
		| @@ -0,0 +1,9 @@ | ||||
| export interface ListadoDistribucionCanillasPromedioDiaDto { | ||||
|   dia: string; // Nombre del día de la semana | ||||
|   cant: number; | ||||
|   llevados: number; | ||||
|   devueltos: number; | ||||
|   promedio_Llevados: number; | ||||
|   promedio_Devueltos: number; | ||||
|   promedio_Ventas: number; | ||||
| } | ||||
| @@ -0,0 +1,7 @@ | ||||
| import type { ListadoDistribucionCanillasSimpleDto } from './ListadoDistribucionCanillasSimpleDto'; | ||||
| import type { ListadoDistribucionCanillasPromedioDiaDto } from './ListadoDistribucionCanillasPromedioDiaDto'; | ||||
|  | ||||
| export interface ListadoDistribucionCanillasResponseDto { | ||||
|   detalleSimple: ListadoDistribucionCanillasSimpleDto[]; | ||||
|   promediosPorDia: ListadoDistribucionCanillasPromedioDiaDto[]; | ||||
| } | ||||
| @@ -0,0 +1,5 @@ | ||||
| export interface ListadoDistribucionCanillasSimpleDto { | ||||
|   dia: number; // Día del mes | ||||
|   llevados: number; | ||||
|   devueltos: number; | ||||
| } | ||||
| @@ -0,0 +1,10 @@ | ||||
| export interface ListadoDistribucionGeneralPromedioDiaDto { | ||||
|   dia: string; | ||||
|   cantidadDias: number; | ||||
|   promedioTirada: number; | ||||
|   promedioSinCargo: number; | ||||
|   promedioPerdidos: number; | ||||
|   promedioLlevados: number; | ||||
|   promedioDevueltos: number; | ||||
|   promedioVendidos: number; | ||||
| } | ||||
| @@ -0,0 +1,7 @@ | ||||
| import type { ListadoDistribucionGeneralResumenDto } from './ListadoDistribucionGeneralResumenDto'; | ||||
| import type { ListadoDistribucionGeneralPromedioDiaDto } from './ListadoDistribucionGeneralPromedioDiaDto'; | ||||
|  | ||||
| export interface ListadoDistribucionGeneralResponseDto { | ||||
|   resumen: ListadoDistribucionGeneralResumenDto[]; | ||||
|   promediosPorDia: ListadoDistribucionGeneralPromedioDiaDto[]; | ||||
| } | ||||
| @@ -0,0 +1,9 @@ | ||||
| export interface ListadoDistribucionGeneralResumenDto { | ||||
|   fecha: string; // o Date, si prefieres parsear en el frontend | ||||
|   cantidadTirada: number; | ||||
|   sinCargo: number; | ||||
|   perdidos: number; | ||||
|   llevados: number; | ||||
|   devueltos: number; | ||||
|   vendidos: number; | ||||
| } | ||||
| @@ -0,0 +1,7 @@ | ||||
| export interface MovimientoBobinaEstadoDetalleDto { | ||||
|   tipoBobina: string; | ||||
|   numeroRemito: string; | ||||
|   fechaMovimiento: string; // o Date, pero string es más simple para la tabla si ya viene formateado | ||||
|   cantidad: number; | ||||
|   tipoMovimiento: string; // "Ingreso", "Utilizada", "Dañada" | ||||
| } | ||||
| @@ -0,0 +1,5 @@ | ||||
| export interface MovimientoBobinaEstadoTotalDto { | ||||
|   tipoMovimiento: string; // "Ingresos", "Utilizadas", "Dañadas" | ||||
|   totalBobinas: number; | ||||
|   totalKilos: number; | ||||
| } | ||||
							
								
								
									
										13
									
								
								Frontend/src/models/dtos/Reportes/MovimientoBobinasDto.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								Frontend/src/models/dtos/Reportes/MovimientoBobinasDto.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| export interface MovimientoBobinasDto { | ||||
|   tipoBobina: string; | ||||
|   bobinasIniciales: number; | ||||
|   kilosIniciales: number; | ||||
|   bobinasCompradas: number; | ||||
|   kilosComprados: number; | ||||
|   bobinasConsumidas: number; | ||||
|   kilosConsumidos: number; | ||||
|   bobinasDaniadas: number; | ||||
|   kilosDaniados: number; | ||||
|   bobinasFinales: number; | ||||
|   kilosFinales: number; | ||||
| } | ||||
| @@ -0,0 +1,7 @@ | ||||
| import type { MovimientoBobinaEstadoDetalleDto } from "./MovimientoBobinaEstadoDetalleDto"; | ||||
| import type { MovimientoBobinaEstadoTotalDto } from "./MovimientoBobinaEstadoTotalDto"; | ||||
|  | ||||
| export interface MovimientoBobinasPorEstadoResponseDto { | ||||
|   detalle: MovimientoBobinaEstadoDetalleDto[]; | ||||
|   totales: MovimientoBobinaEstadoTotalDto[]; | ||||
| } | ||||
| @@ -0,0 +1,225 @@ | ||||
| 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 { ListadoDistribucionCanillasResponseDto } from '../../models/dtos/Reportes/ListadoDistribucionCanillasResponseDto'; | ||||
| import SeleccionaReporteListadoDistribucionCanillas from './SeleccionaReporteListadoDistribucionCanillas'; | ||||
| import * as XLSX from 'xlsx'; | ||||
| import axios from 'axios'; | ||||
|  | ||||
| const ReporteListadoDistribucionCanillasPage: React.FC = () => { | ||||
|   const [reportData, setReportData] = useState<ListadoDistribucionCanillasResponseDto | 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<{ | ||||
|     idPublicacion: number; | ||||
|     fechaDesde: string; | ||||
|     fechaHasta: string; | ||||
|     nombrePublicacion?: string; | ||||
|   } | null>(null); | ||||
|  | ||||
|   const handleGenerarReporte = useCallback(async (params: { | ||||
|     idPublicacion: number; | ||||
|     fechaDesde: string; | ||||
|     fechaHasta: string; | ||||
|   }) => { | ||||
|     setLoading(true); | ||||
|     setError(null); | ||||
|     setApiErrorParams(null); | ||||
|  | ||||
|     const pubService = (await import('../../services/Distribucion/publicacionService')).default; | ||||
|     const pubData = await pubService.getPublicacionById(params.idPublicacion); | ||||
|      | ||||
|     setCurrentParams({...params, nombrePublicacion: pubData?.nombre }); | ||||
|     try { | ||||
|       const data = await reportesService.getListadoDistribucionCanillas(params); | ||||
|       setReportData(data); | ||||
|       if ((!data.detalleSimple || data.detalleSimple.length === 0) && (!data.promediosPorDia || data.promediosPorDia.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.detalleSimple?.length && !reportData.promediosPorDia?.length)) { | ||||
|       alert("No hay datos para exportar."); | ||||
|       return; | ||||
|     } | ||||
|     const wb = XLSX.utils.book_new(); | ||||
|  | ||||
|     if (reportData.detalleSimple?.length) { | ||||
|       const simpleToExport = reportData.detalleSimple.map(item => ({ | ||||
|         "Día": item.dia, | ||||
|         "Llevados": item.llevados, | ||||
|         "Devueltos": item.devueltos, | ||||
|         "Vendidos": item.llevados - item.devueltos, | ||||
|       })); | ||||
|       const wsSimple = XLSX.utils.json_to_sheet(simpleToExport); | ||||
|       XLSX.utils.book_append_sheet(wb, wsSimple, "DetalleDiario"); | ||||
|     } | ||||
|  | ||||
|     if (reportData.promediosPorDia?.length) { | ||||
|       const promediosToExport = reportData.promediosPorDia.map(item => ({ | ||||
|         "Día Semana": item.dia, | ||||
|         "Cant. Días": item.cant, | ||||
|         "Prom. Llevados": item.promedio_Llevados, | ||||
|         "Prom. Devueltos": item.promedio_Devueltos, | ||||
|         "Prom. Vendidos": item.promedio_Ventas, | ||||
|       })); | ||||
|       const wsPromedios = XLSX.utils.json_to_sheet(promediosToExport); | ||||
|       XLSX.utils.book_append_sheet(wb, wsPromedios, "PromediosPorDiaSemana"); | ||||
|     } | ||||
|  | ||||
|     let fileName = "ListadoDistribucionCanillas"; | ||||
|     if (currentParams) { | ||||
|       fileName += `_${currentParams.nombrePublicacion?.replace(/\s+/g, '') ?? `Pub${currentParams.idPublicacion}`}`; | ||||
|       fileName += `_${currentParams.fechaDesde}_a_${currentParams.fechaHasta}`; | ||||
|     } | ||||
|     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.getListadoDistribucionCanillasPdf(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}> | ||||
|           <SeleccionaReporteListadoDistribucionCanillas | ||||
|             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: Listado Distribución Canillitas</Typography> | ||||
|         <Box sx={{ display: 'flex', gap: 1 }}> | ||||
|           <Button onClick={handleGenerarYAbrirPdf} variant="contained" disabled={loadingPdf || !reportData || !!error} size="small"> | ||||
|             {loadingPdf ? <CircularProgress size={20} color="inherit" /> : "Abrir PDF"} | ||||
|           </Button> | ||||
|           <Button onClick={handleExportToExcel} variant="outlined" disabled={!reportData || !!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 && ( | ||||
|         <> | ||||
|           <Typography variant="h6" gutterBottom sx={{ mt: 2 }}>Detalle Diario</Typography> | ||||
|           {reportData.detalleSimple && reportData.detalleSimple.length > 0 ? ( | ||||
|             <TableContainer component={Paper} sx={{ maxHeight: '300px', mb: 3 }}> | ||||
|               <Table stickyHeader size="small"> | ||||
|                 <TableHead> | ||||
|                   <TableRow> | ||||
|                     <TableCell>Día</TableCell> | ||||
|                     <TableCell align="right">Llevados</TableCell> | ||||
|                     <TableCell align="right">Devueltos</TableCell> | ||||
|                     <TableCell align="right">Vendidos</TableCell> | ||||
|                   </TableRow> | ||||
|                 </TableHead> | ||||
|                 <TableBody> | ||||
|                   {reportData.detalleSimple.map((row, idx) => ( | ||||
|                     <TableRow key={`simple-${idx}`}> | ||||
|                       <TableCell>{row.dia}</TableCell> | ||||
|                       <TableCell align="right">{row.llevados.toLocaleString('es-AR')}</TableCell> | ||||
|                       <TableCell align="right">{row.devueltos.toLocaleString('es-AR')}</TableCell> | ||||
|                       <TableCell align="right">{(row.llevados - row.devueltos).toLocaleString('es-AR')}</TableCell> | ||||
|                     </TableRow> | ||||
|                   ))} | ||||
|                 </TableBody> | ||||
|               </Table> | ||||
|             </TableContainer> | ||||
|           ) : (<Typography>No hay datos de detalle diario.</Typography>)} | ||||
|  | ||||
|           <Typography variant="h6" gutterBottom sx={{ mt: 2 }}>Promedios por Día de Semana</Typography> | ||||
|           {reportData.promediosPorDia && reportData.promediosPorDia.length > 0 ? ( | ||||
|             <TableContainer component={Paper} sx={{ maxHeight: '300px' }}> | ||||
|               <Table stickyHeader size="small"> | ||||
|                 <TableHead> | ||||
|                   <TableRow> | ||||
|                     <TableCell>Día Semana</TableCell> | ||||
|                     <TableCell align="right">Cant. Días</TableCell> | ||||
|                     <TableCell align="right">Prom. Llevados</TableCell> | ||||
|                     <TableCell align="right">Prom. Devueltos</TableCell> | ||||
|                     <TableCell align="right">Prom. Ventas</TableCell> | ||||
|                   </TableRow> | ||||
|                 </TableHead> | ||||
|                 <TableBody> | ||||
|                   {reportData.promediosPorDia.map((row, idx) => ( | ||||
|                     <TableRow key={`promedio-${idx}`}> | ||||
|                       <TableCell>{row.dia}</TableCell> | ||||
|                       <TableCell align="right">{row.cant.toLocaleString('es-AR')}</TableCell> | ||||
|                       <TableCell align="right">{row.promedio_Llevados.toLocaleString('es-AR')}</TableCell> | ||||
|                       <TableCell align="right">{row.promedio_Devueltos.toLocaleString('es-AR')}</TableCell> | ||||
|                       <TableCell align="right">{row.promedio_Ventas.toLocaleString('es-AR')}</TableCell> | ||||
|                     </TableRow> | ||||
|                   ))} | ||||
|                 </TableBody> | ||||
|               </Table> | ||||
|             </TableContainer> | ||||
|           ) : (<Typography>No hay datos de promedios por día.</Typography>)} | ||||
|         </> | ||||
|       )} | ||||
|     </Box> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export default ReporteListadoDistribucionCanillasPage; | ||||
| @@ -0,0 +1,252 @@ | ||||
| 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 { ListadoDistribucionGeneralResponseDto } from '../../models/dtos/Reportes/ListadoDistribucionGeneralResponseDto'; | ||||
| import SeleccionaReporteListadoDistribucionGeneral from './SeleccionaReporteListadoDistribucionGeneral'; | ||||
| import * as XLSX from 'xlsx'; | ||||
| import axios from 'axios'; | ||||
|  | ||||
| const ReporteListadoDistribucionGeneralPage: React.FC = () => { | ||||
|   const [reportData, setReportData] = useState<ListadoDistribucionGeneralResponseDto | 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<{ | ||||
|     idPublicacion: number; | ||||
|     fechaDesde: string; // Primer día del mes | ||||
|     fechaHasta: string; // Último día del mes | ||||
|     nombrePublicacion?: string; // Para el nombre del archivo | ||||
|     mesAnioParaNombreArchivo?: string; // Para el nombre del archivo (ej. YYYY-MM) | ||||
|   } | null>(null); | ||||
|  | ||||
|   const handleGenerarReporte = useCallback(async (params: { | ||||
|     idPublicacion: number; | ||||
|     fechaDesde: string; | ||||
|     fechaHasta: string; | ||||
|   }) => { | ||||
|     setLoading(true); | ||||
|     setError(null); | ||||
|     setApiErrorParams(null); | ||||
|      | ||||
|     // Para el nombre del archivo y título del PDF | ||||
|     const pubService = (await import('../../services/Distribucion/publicacionService')).default; | ||||
|     const pubData = await pubService.getPublicacionById(params.idPublicacion); | ||||
|     const mesAnioParts = params.fechaDesde.split('-'); // YYYY-MM-DD -> [YYYY, MM, DD] | ||||
|     const mesAnioNombre = `${mesAnioParts[1]}/${mesAnioParts[0]}`; | ||||
|  | ||||
|  | ||||
|     setCurrentParams({...params, nombrePublicacion: pubData?.nombre, mesAnioParaNombreArchivo: mesAnioNombre }); | ||||
|     try { | ||||
|       const data = await reportesService.getListadoDistribucionGeneral(params); | ||||
|       setReportData(data); | ||||
|       if ((!data.resumen || data.resumen.length === 0) && (!data.promediosPorDia || data.promediosPorDia.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.resumen?.length && !reportData.promediosPorDia?.length)) { | ||||
|       alert("No hay datos para exportar."); | ||||
|       return; | ||||
|     } | ||||
|     const wb = XLSX.utils.book_new(); | ||||
|  | ||||
|     if (reportData.resumen?.length) { | ||||
|       const resumenToExport = reportData.resumen.map(item => ({ | ||||
|         "Fecha": item.fecha ? new Date(item.fecha).toLocaleDateString('es-AR', { timeZone: 'UTC' }) : '-', | ||||
|         "Tirada": item.cantidadTirada, | ||||
|         "Sin Cargo": item.sinCargo, | ||||
|         "Perdidos": item.perdidos, | ||||
|         "Llevados": item.llevados, | ||||
|         "Devueltos": item.devueltos, | ||||
|         "Vendidos": item.vendidos, | ||||
|       })); | ||||
|       const wsResumen = XLSX.utils.json_to_sheet(resumenToExport); | ||||
|       XLSX.utils.book_append_sheet(wb, wsResumen, "ResumenDiario"); | ||||
|     } | ||||
|  | ||||
|     if (reportData.promediosPorDia?.length) { | ||||
|       const promediosToExport = reportData.promediosPorDia.map(item => ({ | ||||
|         "Día Semana": item.dia, | ||||
|         "Cant. Días": item.cantidadDias, | ||||
|         "Prom. Tirada": item.promedioTirada, | ||||
|         "Prom. Sin Cargo": item.promedioSinCargo, | ||||
|         "Prom. Perdidos": item.promedioPerdidos, | ||||
|         "Prom. Llevados": item.promedioLlevados, | ||||
|         "Prom. Devueltos": item.promedioDevueltos, | ||||
|         "Prom. Vendidos": item.promedioVendidos, | ||||
|       })); | ||||
|       const wsPromedios = XLSX.utils.json_to_sheet(promediosToExport); | ||||
|       XLSX.utils.book_append_sheet(wb, wsPromedios, "PromediosPorDia"); | ||||
|     } | ||||
|  | ||||
|     let fileName = "ListadoDistribucionGeneral"; | ||||
|     if (currentParams) { | ||||
|       fileName += `_${currentParams.nombrePublicacion?.replace(/\s+/g, '') ?? `Pub${currentParams.idPublicacion}`}`; | ||||
|       fileName += `_${currentParams.mesAnioParaNombreArchivo?.replace('/', '-')}`; | ||||
|     } | ||||
|     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.getListadoDistribucionGeneralPdf({ | ||||
|         idPublicacion: currentParams.idPublicacion, | ||||
|         fechaDesde: currentParams.fechaDesde, // El servicio y SP esperan fechaDesde para el mes/año | ||||
|         fechaHasta: currentParams.fechaHasta // El SP no usa esta, pero el servicio de reporte sí para el nombre | ||||
|       }); | ||||
|       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}> | ||||
|           <SeleccionaReporteListadoDistribucionGeneral | ||||
|             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: Listado Distribución General</Typography> | ||||
|         <Box sx={{ display: 'flex', gap: 1 }}> | ||||
|           <Button onClick={handleGenerarYAbrirPdf} variant="contained" disabled={loadingPdf || !reportData || !!error} size="small"> | ||||
|             {loadingPdf ? <CircularProgress size={20} color="inherit" /> : "Abrir PDF"} | ||||
|           </Button> | ||||
|           <Button onClick={handleExportToExcel} variant="outlined" disabled={!reportData || !!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 && ( | ||||
|         <> | ||||
|           <Typography variant="h6" gutterBottom sx={{ mt: 2 }}>Resumen Diario</Typography> | ||||
|           {reportData.resumen && reportData.resumen.length > 0 ? ( | ||||
|             <TableContainer component={Paper} sx={{ maxHeight: '300px', mb: 3 }}> | ||||
|               <Table stickyHeader size="small"> | ||||
|                 <TableHead> | ||||
|                   <TableRow> | ||||
|                     <TableCell>Fecha</TableCell> | ||||
|                     <TableCell align="right">Tirada</TableCell> | ||||
|                     <TableCell align="right">Sin Cargo</TableCell> | ||||
|                     <TableCell align="right">Perdidos</TableCell> | ||||
|                     <TableCell align="right">Llevados</TableCell> | ||||
|                     <TableCell align="right">Devueltos</TableCell> | ||||
|                     <TableCell align="right">Vendidos</TableCell> | ||||
|                   </TableRow> | ||||
|                 </TableHead> | ||||
|                 <TableBody> | ||||
|                   {reportData.resumen.map((row, idx) => ( | ||||
|                     <TableRow key={`resumen-${idx}`}> | ||||
|                       <TableCell>{row.fecha ? new Date(row.fecha).toLocaleDateString('es-AR', { timeZone: 'UTC' }) : '-'}</TableCell> | ||||
|                       <TableCell align="right">{row.cantidadTirada.toLocaleString('es-AR')}</TableCell> | ||||
|                       <TableCell align="right">{row.sinCargo.toLocaleString('es-AR')}</TableCell> | ||||
|                       <TableCell align="right">{row.perdidos.toLocaleString('es-AR')}</TableCell> | ||||
|                       <TableCell align="right">{row.llevados.toLocaleString('es-AR')}</TableCell> | ||||
|                       <TableCell align="right">{row.devueltos.toLocaleString('es-AR')}</TableCell> | ||||
|                       <TableCell align="right">{row.vendidos.toLocaleString('es-AR')}</TableCell> | ||||
|                     </TableRow> | ||||
|                   ))} | ||||
|                 </TableBody> | ||||
|               </Table> | ||||
|             </TableContainer> | ||||
|           ) : (<Typography>No hay datos de resumen diario.</Typography>)} | ||||
|  | ||||
|           <Typography variant="h6" gutterBottom sx={{ mt: 2 }}>Promedios por Día de Semana</Typography> | ||||
|           {reportData.promediosPorDia && reportData.promediosPorDia.length > 0 ? ( | ||||
|             <TableContainer component={Paper} sx={{ maxHeight: '300px' }}> | ||||
|               <Table stickyHeader size="small"> | ||||
|                 <TableHead> | ||||
|                   <TableRow> | ||||
|                     <TableCell>Día</TableCell> | ||||
|                     <TableCell align="right">Cant. Días</TableCell> | ||||
|                     <TableCell align="right">Prom. Tirada</TableCell> | ||||
|                     <TableCell align="right">Prom. Sin Cargo</TableCell> | ||||
|                     <TableCell align="right">Prom. Perdidos</TableCell> | ||||
|                     <TableCell align="right">Prom. Llevados</TableCell> | ||||
|                     <TableCell align="right">Prom. Devueltos</TableCell> | ||||
|                     <TableCell align="right">Prom. Vendidos</TableCell> | ||||
|                   </TableRow> | ||||
|                 </TableHead> | ||||
|                 <TableBody> | ||||
|                   {reportData.promediosPorDia.map((row, idx) => ( | ||||
|                     <TableRow key={`promedio-${idx}`}> | ||||
|                       <TableCell>{row.dia}</TableCell> | ||||
|                       <TableCell align="right">{row.cantidadDias.toLocaleString('es-AR')}</TableCell> | ||||
|                       <TableCell align="right">{row.promedioTirada.toLocaleString('es-AR')}</TableCell> | ||||
|                       <TableCell align="right">{row.promedioSinCargo.toLocaleString('es-AR')}</TableCell> | ||||
|                       <TableCell align="right">{row.promedioPerdidos.toLocaleString('es-AR')}</TableCell> | ||||
|                       <TableCell align="right">{row.promedioLlevados.toLocaleString('es-AR')}</TableCell> | ||||
|                       <TableCell align="right">{row.promedioDevueltos.toLocaleString('es-AR')}</TableCell> | ||||
|                       <TableCell align="right">{row.promedioVendidos.toLocaleString('es-AR')}</TableCell> | ||||
|                     </TableRow> | ||||
|                   ))} | ||||
|                 </TableBody> | ||||
|               </Table> | ||||
|             </TableContainer> | ||||
|           ) : (<Typography>No hay datos de promedios por día.</Typography>)} | ||||
|         </> | ||||
|       )} | ||||
|     </Box> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export default ReporteListadoDistribucionGeneralPage; | ||||
| @@ -0,0 +1,257 @@ | ||||
| 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; | ||||
							
								
								
									
										212
									
								
								Frontend/src/pages/Reportes/ReporteMovimientoBobinasPage.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										212
									
								
								Frontend/src/pages/Reportes/ReporteMovimientoBobinasPage.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,212 @@ | ||||
| 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 { MovimientoBobinasDto } from '../../models/dtos/Reportes/MovimientoBobinasDto'; | ||||
| import SeleccionaReporteMovimientoBobinas from './SeleccionaReporteMovimientoBobinas'; | ||||
| import * as XLSX from 'xlsx'; | ||||
| import axios from 'axios'; | ||||
|  | ||||
| const ReporteMovimientoBobinasPage: React.FC = () => { | ||||
|   const [reportData, setReportData] = useState<MovimientoBobinasDto[]>([]); | ||||
|   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.getMovimientoBobinas(params); | ||||
|       setReportData(data); | ||||
|       if (data.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([]); | ||||
|     } 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; | ||||
|     } | ||||
|     const dataToExport = reportData.map(item => ({ | ||||
|       "Tipo Bobina": item.tipoBobina, | ||||
|       "Bobinas Iniciales": item.bobinasIniciales, | ||||
|       "Kg Iniciales": item.kilosIniciales, | ||||
|       "Bobinas Compradas": item.bobinasCompradas, | ||||
|       "Kg Comprados": item.kilosComprados, | ||||
|       "Bobinas Consumidas": item.bobinasConsumidas, | ||||
|       "Kg Consumidos": item.kilosConsumidos, | ||||
|       "Bobinas Dañadas": item.bobinasDaniadas, | ||||
|       "Kg Dañados": item.kilosDaniados, | ||||
|       "Bobinas Finales": item.bobinasFinales, | ||||
|       "Kg Finales": item.kilosFinales, | ||||
|     })); | ||||
|  | ||||
|     const ws = XLSX.utils.json_to_sheet(dataToExport); | ||||
|     const headers = Object.keys(dataToExport[0]); | ||||
|     ws['!cols'] = headers.map(h => { | ||||
|       const maxLen = dataToExport.reduce((prev, row) => { | ||||
|         const cell = (row as any)[h]?.toString() ?? ''; | ||||
|         return Math.max(prev, cell.length); | ||||
|       }, 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, "MovimientoBobinas"); | ||||
|     let fileName = "ReporteMovimientoBobinas"; | ||||
|     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.getMovimientoBobinasPdf(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}> | ||||
|           <SeleccionaReporteMovimientoBobinas | ||||
|             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</Typography> | ||||
|         <Box sx={{ display: 'flex', gap: 1 }}> | ||||
|           <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" | ||||
|           > | ||||
|             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 && ( | ||||
|         <TableContainer component={Paper} sx={{ maxHeight: 'calc(100vh - 240px)' }}> | ||||
|           <Table stickyHeader size="small"> | ||||
|             <TableHead> | ||||
|               <TableRow> | ||||
|                 <TableCell>Tipo Bobina</TableCell> | ||||
|                 <TableCell align="right">Cant. Ini.</TableCell> | ||||
|                 <TableCell align="right">Kg Ini.</TableCell> | ||||
|                 <TableCell align="right">Compradas</TableCell> | ||||
|                 <TableCell align="right">Kg Compr.</TableCell> | ||||
|                 <TableCell align="right">Consum.</TableCell> | ||||
|                 <TableCell align="right">Kg Consum.</TableCell> | ||||
|                 <TableCell align="right">Dañadas</TableCell> | ||||
|                 <TableCell align="right">Kg Dañ.</TableCell> | ||||
|                 <TableCell align="right">Cant. Fin.</TableCell> | ||||
|                 <TableCell align="right">Kg Finales</TableCell> | ||||
|               </TableRow> | ||||
|             </TableHead> | ||||
|             <TableBody> | ||||
|               {reportData.map((row, idx) => ( | ||||
|                 <TableRow key={row.tipoBobina + idx}> | ||||
|                   <TableCell>{row.tipoBobina}</TableCell> | ||||
|                   <TableCell align="right">{row.bobinasIniciales.toLocaleString('es-AR')}</TableCell> | ||||
|                   <TableCell align="right">{row.kilosIniciales.toLocaleString('es-AR')}</TableCell> | ||||
|                   <TableCell align="right">{row.bobinasCompradas.toLocaleString('es-AR')}</TableCell> | ||||
|                   <TableCell align="right">{row.kilosComprados.toLocaleString('es-AR')}</TableCell> | ||||
|                   <TableCell align="right">{row.bobinasConsumidas.toLocaleString('es-AR')}</TableCell> | ||||
|                   <TableCell align="right">{row.kilosConsumidos.toLocaleString('es-AR')}</TableCell> | ||||
|                   <TableCell align="right">{row.bobinasDaniadas.toLocaleString('es-AR')}</TableCell> | ||||
|                   <TableCell align="right">{row.kilosDaniados.toLocaleString('es-AR')}</TableCell> | ||||
|                   <TableCell align="right">{row.bobinasFinales.toLocaleString('es-AR')}</TableCell> | ||||
|                   <TableCell align="right">{row.kilosFinales.toLocaleString('es-AR')}</TableCell> | ||||
|                 </TableRow> | ||||
|               ))} | ||||
|             </TableBody> | ||||
|           </Table> | ||||
|         </TableContainer> | ||||
|       )} | ||||
|     </Box> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export default ReporteMovimientoBobinasPage; | ||||
| @@ -4,8 +4,10 @@ import { Outlet, useNavigate, useLocation } from 'react-router-dom'; | ||||
|  | ||||
| const reportesSubModules = [ | ||||
|   { label: 'Existencia de Papel', path: 'existencia-papel' }, | ||||
|   // { label: 'Consumo Bobinas Mensual', path: 'consumo-bobinas-mensual' }, // Ejemplo | ||||
|   // ... agregar otros reportes aquí a medida que se implementen | ||||
|   { label: 'Movimiento de Bobinas', path: 'movimiento-bobinas' }, | ||||
|   { label: 'Mov. Bobinas por Estado', path: 'movimiento-bobinas-estado' }, | ||||
|   { label: 'Distribución General', path: 'listado-distribucion-general' }, | ||||
|   { label: 'Distribución Canillas', path: 'listado-distribucion-canillas' }, | ||||
| ]; | ||||
|  | ||||
| const ReportesIndexPage: React.FC = () => { | ||||
|   | ||||
| @@ -20,7 +20,6 @@ interface SeleccionaReporteExistenciaPapelProps { | ||||
|  | ||||
| const SeleccionaReporteExistenciaPapel: React.FC<SeleccionaReporteExistenciaPapelProps> = ({ | ||||
|   onGenerarReporte, | ||||
|   onCancel, | ||||
|   isLoading, | ||||
|   apiErrorMessage | ||||
| }) => { | ||||
| @@ -142,9 +141,6 @@ const SeleccionaReporteExistenciaPapel: React.FC<SeleccionaReporteExistenciaPape | ||||
|       {localErrors.dropdowns && <Alert severity="warning" sx={{ mt: 1 }}>{localErrors.dropdowns}</Alert>} | ||||
|  | ||||
|       <Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', gap: 1 }}> | ||||
|         <Button onClick={onCancel} color="secondary" disabled={isLoading}> | ||||
|           Cancelar | ||||
|         </Button> | ||||
|         <Button onClick={handleGenerar} variant="contained" disabled={isLoading || loadingDropdowns}> | ||||
|           {isLoading ? <CircularProgress size={24} /> : 'Generar Reporte'} | ||||
|         </Button> | ||||
|   | ||||
| @@ -0,0 +1,129 @@ | ||||
| import React, { useState, useEffect } from 'react'; | ||||
| import { | ||||
|     Box, Typography, TextField, Button, CircularProgress, Alert, | ||||
|     FormControl, InputLabel, Select, MenuItem | ||||
| } from '@mui/material'; | ||||
| import type { PublicacionDto } from '../../models/dtos/Distribucion/PublicacionDto'; | ||||
| import publicacionService from '../../services/Distribucion/publicacionService'; | ||||
|  | ||||
| interface SeleccionaReporteListadoDistribucionCanillasProps { | ||||
|   onGenerarReporte: (params: { | ||||
|     idPublicacion: number; | ||||
|     fechaDesde: string; | ||||
|     fechaHasta: string; | ||||
|   }) => Promise<void>; | ||||
|   onCancel: () => void; | ||||
|   isLoading?: boolean; | ||||
|   apiErrorMessage?: string | null; | ||||
| } | ||||
|  | ||||
| const SeleccionaReporteListadoDistribucionCanillas: React.FC<SeleccionaReporteListadoDistribucionCanillasProps> = ({ | ||||
|   onGenerarReporte, | ||||
|   isLoading, | ||||
|   apiErrorMessage | ||||
| }) => { | ||||
|   const [idPublicacion, setIdPublicacion] = useState<number | string>(''); | ||||
|   const [fechaDesde, setFechaDesde] = useState<string>(new Date().toISOString().split('T')[0]); | ||||
|   const [fechaHasta, setFechaHasta] = useState<string>(new Date().toISOString().split('T')[0]); | ||||
|  | ||||
|   const [publicaciones, setPublicaciones] = useState<PublicacionDto[]>([]); | ||||
|   const [loadingDropdowns, setLoadingDropdowns] = useState(false); | ||||
|   const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({}); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     const fetchPublicaciones = async () => { | ||||
|       setLoadingDropdowns(true); | ||||
|       try { | ||||
|         const data = await publicacionService.getAllPublicaciones(undefined, undefined, true); | ||||
|         setPublicaciones(data.map(p => p)); | ||||
|       } catch (error) { | ||||
|         console.error("Error al cargar publicaciones:", error); | ||||
|         setLocalErrors(prev => ({ ...prev, dropdowns: 'Error al cargar publicaciones.' })); | ||||
|       } finally { | ||||
|         setLoadingDropdowns(false); | ||||
|       } | ||||
|     }; | ||||
|     fetchPublicaciones(); | ||||
|   }, []); | ||||
|  | ||||
|   const validate = (): boolean => { | ||||
|     const errors: { [key: string]: string | null } = {}; | ||||
|     if (!idPublicacion) errors.idPublicacion = 'Debe seleccionar una publicación.'; | ||||
|     if (!fechaDesde) errors.fechaDesde = 'Fecha Desde es obligatoria.'; | ||||
|     if (!fechaHasta) errors.fechaHasta = 'Fecha Hasta es obligatoria.'; | ||||
|     if (fechaDesde && fechaHasta && new Date(fechaDesde) > new Date(fechaHasta)) { | ||||
|       errors.fechaHasta = 'Fecha Hasta no puede ser anterior a Fecha Desde.'; | ||||
|     } | ||||
|     setLocalErrors(errors); | ||||
|     return Object.keys(errors).length === 0; | ||||
|   }; | ||||
|  | ||||
|   const handleGenerar = () => { | ||||
|     if (!validate()) return; | ||||
|     onGenerarReporte({ | ||||
|       idPublicacion: Number(idPublicacion), | ||||
|       fechaDesde, | ||||
|       fechaHasta | ||||
|     }); | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <Box sx={{ p: 2, border: '1px solid #ccc', borderRadius: '4px', minWidth: 380 }}> | ||||
|       <Typography variant="h6" gutterBottom> | ||||
|         Parámetros: Listado Distribución Canillitas | ||||
|       </Typography> | ||||
|       <FormControl fullWidth margin="normal" error={!!localErrors.idPublicacion} disabled={isLoading || loadingDropdowns}> | ||||
|         <InputLabel id="publicacion-select-label-can" required>Publicación</InputLabel> | ||||
|         <Select | ||||
|           labelId="publicacion-select-label-can" | ||||
|           label="Publicación" | ||||
|           value={idPublicacion} | ||||
|           onChange={(e) => { setIdPublicacion(e.target.value as number); setLocalErrors(p => ({ ...p, idPublicacion: null })); }} | ||||
|         > | ||||
|           <MenuItem value="" disabled><em>Seleccione una publicación</em></MenuItem> | ||||
|           {publicaciones.map((p) => ( | ||||
|             <MenuItem key={p.idPublicacion} value={p.idPublicacion}>{p.nombre}</MenuItem> | ||||
|           ))} | ||||
|         </Select> | ||||
|         {localErrors.idPublicacion && <Typography color="error" variant="caption" sx={{ml:1.5}}>{localErrors.idPublicacion}</Typography>} | ||||
|       </FormControl> | ||||
|       <TextField | ||||
|         label="Fecha Desde" | ||||
|         type="date" | ||||
|         value={fechaDesde} | ||||
|         onChange={(e) => { setFechaDesde(e.target.value); setLocalErrors(p => ({ ...p, fechaDesde: null, fechaHasta: null })); }} | ||||
|         margin="normal" | ||||
|         fullWidth | ||||
|         required | ||||
|         error={!!localErrors.fechaDesde} | ||||
|         helperText={localErrors.fechaDesde} | ||||
|         disabled={isLoading} | ||||
|         InputLabelProps={{ shrink: true }} | ||||
|       /> | ||||
|       <TextField | ||||
|         label="Fecha Hasta" | ||||
|         type="date" | ||||
|         value={fechaHasta} | ||||
|         onChange={(e) => { setFechaHasta(e.target.value); setLocalErrors(p => ({ ...p, fechaHasta: null })); }} | ||||
|         margin="normal" | ||||
|         fullWidth | ||||
|         required | ||||
|         error={!!localErrors.fechaHasta} | ||||
|         helperText={localErrors.fechaHasta} | ||||
|         disabled={isLoading} | ||||
|         InputLabelProps={{ shrink: true }} | ||||
|       /> | ||||
|  | ||||
|       {apiErrorMessage && <Alert severity="error" sx={{ mt: 2 }}>{apiErrorMessage}</Alert>} | ||||
|       {localErrors.dropdowns && <Alert severity="warning" sx={{ mt: 1 }}>{localErrors.dropdowns}</Alert>} | ||||
|  | ||||
|       <Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', gap: 1 }}> | ||||
|         <Button onClick={handleGenerar} variant="contained" disabled={isLoading || loadingDropdowns}> | ||||
|           {isLoading ? <CircularProgress size={24} /> : 'Generar Reporte'} | ||||
|         </Button> | ||||
|       </Box> | ||||
|     </Box> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export default SeleccionaReporteListadoDistribucionCanillas; | ||||
| @@ -0,0 +1,120 @@ | ||||
| // src/pages/Reportes/SeleccionaReporteListadoDistribucionGeneral.tsx | ||||
| import React, { useState, useEffect } from 'react'; | ||||
| import { | ||||
|     Box, Typography, Button, CircularProgress, Alert, | ||||
|     FormControl, InputLabel, Select, MenuItem, TextField | ||||
| } from '@mui/material'; | ||||
| import type { PublicacionDto } from '../../models/dtos/Distribucion/PublicacionDto'; | ||||
| import publicacionService from '../../services/Distribucion/publicacionService'; | ||||
|  | ||||
| interface SeleccionaReporteListadoDistribucionGeneralProps { | ||||
|   onGenerarReporte: (params: { | ||||
|     idPublicacion: number; | ||||
|     fechaDesde: string; // Será el primer día del mes seleccionado | ||||
|     fechaHasta: string; // Será el último día del mes seleccionado | ||||
|   }) => Promise<void>; | ||||
|   onCancel: () => void; | ||||
|   isLoading?: boolean; | ||||
|   apiErrorMessage?: string | null; | ||||
| } | ||||
|  | ||||
| const SeleccionaReporteListadoDistribucionGeneral: React.FC<SeleccionaReporteListadoDistribucionGeneralProps> = ({ | ||||
|   onGenerarReporte, | ||||
|   isLoading, | ||||
|   apiErrorMessage | ||||
| }) => { | ||||
|   const [idPublicacion, setIdPublicacion] = useState<number | string>(''); | ||||
|   // Para el selector de mes/año, usamos un input type="month" | ||||
|   const [mesAnio, setMesAnio] = useState<string>(new Date().toISOString().substring(0, 7)); // Formato "YYYY-MM" | ||||
|  | ||||
|   const [publicaciones, setPublicaciones] = useState<PublicacionDto[]>([]); | ||||
|   const [loadingDropdowns, setLoadingDropdowns] = useState(false); | ||||
|   const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({}); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     const fetchPublicaciones = async () => { | ||||
|       setLoadingDropdowns(true); | ||||
|       try { | ||||
|         // Asumiendo que quieres solo publicaciones habilitadas | ||||
|         const data = await publicacionService.getAllPublicaciones(undefined, undefined, true); | ||||
|         setPublicaciones(data.map(p => p)); // El servicio devuelve tupla | ||||
|       } catch (error) { | ||||
|         console.error("Error al cargar publicaciones:", error); | ||||
|         setLocalErrors(prev => ({ ...prev, dropdowns: 'Error al cargar publicaciones.' })); | ||||
|       } finally { | ||||
|         setLoadingDropdowns(false); | ||||
|       } | ||||
|     }; | ||||
|     fetchPublicaciones(); | ||||
|   }, []); | ||||
|  | ||||
|   const validate = (): boolean => { | ||||
|     const errors: { [key: string]: string | null } = {}; | ||||
|     if (!idPublicacion) errors.idPublicacion = 'Debe seleccionar una publicación.'; | ||||
|     if (!mesAnio) errors.mesAnio = 'Debe seleccionar un Mes/Año.'; | ||||
|     setLocalErrors(errors); | ||||
|     return Object.keys(errors).length === 0; | ||||
|   }; | ||||
|  | ||||
|   const handleGenerar = () => { | ||||
|     if (!validate()) return; | ||||
|  | ||||
|     const [year, month] = mesAnio.split('-').map(Number); | ||||
|     const fechaDesde = new Date(year, month - 1, 1).toISOString().split('T')[0]; | ||||
|     const fechaHasta = new Date(year, month, 0).toISOString().split('T')[0]; // Último día del mes | ||||
|  | ||||
|     onGenerarReporte({ | ||||
|       idPublicacion: Number(idPublicacion), | ||||
|       fechaDesde, | ||||
|       fechaHasta | ||||
|     }); | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <Box sx={{ p: 2, border: '1px solid #ccc', borderRadius: '4px', minWidth: 380 }}> | ||||
|       <Typography variant="h6" gutterBottom> | ||||
|         Parámetros: Listado Distribución General | ||||
|       </Typography> | ||||
|       <FormControl fullWidth margin="normal" error={!!localErrors.idPublicacion} disabled={isLoading || loadingDropdowns}> | ||||
|         <InputLabel id="publicacion-select-label" required>Publicación</InputLabel> | ||||
|         <Select | ||||
|           labelId="publicacion-select-label" | ||||
|           label="Publicación" | ||||
|           value={idPublicacion} | ||||
|           onChange={(e) => { setIdPublicacion(e.target.value as number); setLocalErrors(p => ({ ...p, idPublicacion: null })); }} | ||||
|         > | ||||
|           <MenuItem value="" disabled><em>Seleccione una publicación</em></MenuItem> | ||||
|           {publicaciones.map((p) => ( | ||||
|             <MenuItem key={p.idPublicacion} value={p.idPublicacion}>{p.nombre}</MenuItem> | ||||
|           ))} | ||||
|         </Select> | ||||
|         {localErrors.idPublicacion && <Typography color="error" variant="caption" sx={{ml:1.5}}>{localErrors.idPublicacion}</Typography>} | ||||
|       </FormControl> | ||||
|  | ||||
|       <TextField | ||||
|         label="Mes y Año" | ||||
|         type="month" // Esto generará un selector de mes/año nativo del navegador | ||||
|         value={mesAnio} | ||||
|         onChange={(e) => { setMesAnio(e.target.value); setLocalErrors(p => ({ ...p, mesAnio: null })); }} | ||||
|         margin="normal" | ||||
|         fullWidth | ||||
|         required | ||||
|         error={!!localErrors.mesAnio} | ||||
|         helperText={localErrors.mesAnio} | ||||
|         disabled={isLoading} | ||||
|         InputLabelProps={{ shrink: true }} | ||||
|       /> | ||||
|  | ||||
|       {apiErrorMessage && <Alert severity="error" sx={{ mt: 2 }}>{apiErrorMessage}</Alert>} | ||||
|       {localErrors.dropdowns && <Alert severity="warning" sx={{ mt: 1 }}>{localErrors.dropdowns}</Alert>} | ||||
|  | ||||
|       <Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', gap: 1 }}> | ||||
|         <Button onClick={handleGenerar} variant="contained" disabled={isLoading || loadingDropdowns}> | ||||
|           {isLoading ? <CircularProgress size={24} /> : 'Generar Reporte'} | ||||
|         </Button> | ||||
|       </Box> | ||||
|     </Box> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export default SeleccionaReporteListadoDistribucionGeneral; | ||||
| @@ -0,0 +1,131 @@ | ||||
| import React, { useState, useEffect } from 'react'; | ||||
| import { | ||||
|     Box, Typography, TextField, Button, CircularProgress, Alert, | ||||
|     FormControl, InputLabel, Select, MenuItem | ||||
| } from '@mui/material'; | ||||
| import type { PlantaDto } from '../../models/dtos/Impresion/PlantaDto'; // Asumo que ya tienes este DTO | ||||
| import plantaService from '../../services/Impresion/plantaService';   // Asumo que ya tienes este servicio | ||||
|  | ||||
| interface SeleccionaReporteMovimientoBobinasProps { | ||||
|   onGenerarReporte: (params: { | ||||
|     fechaDesde: string; | ||||
|     fechaHasta: string; | ||||
|     idPlanta: number; // idPlanta es obligatoria aquí | ||||
|   }) => Promise<void>; | ||||
|   onCancel: () => void; | ||||
|   isLoading?: boolean; | ||||
|   apiErrorMessage?: string | null; | ||||
| } | ||||
|  | ||||
| const SeleccionaReporteMovimientoBobinas: React.FC<SeleccionaReporteMovimientoBobinasProps> = ({ | ||||
|   onGenerarReporte, | ||||
|   isLoading, | ||||
|   apiErrorMessage | ||||
| }) => { | ||||
|   const [fechaDesde, setFechaDesde] = useState<string>(new Date().toISOString().split('T')[0]); | ||||
|   const [fechaHasta, setFechaHasta] = useState<string>(new Date().toISOString().split('T')[0]); | ||||
|   const [idPlanta, setIdPlanta] = useState<number | string>(''); // Puede ser string inicialmente por el MenuItem vacío | ||||
|  | ||||
|   const [plantas, setPlantas] = useState<PlantaDto[]>([]); | ||||
|   const [loadingDropdowns, setLoadingDropdowns] = useState(false); | ||||
|   const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({}); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     const fetchPlantas = async () => { | ||||
|       setLoadingDropdowns(true); | ||||
|       try { | ||||
|         const plantasData = await plantaService.getAllPlantas(); // Asumiendo que esto devuelve todas | ||||
|         setPlantas(plantasData); | ||||
|       } catch (error) { | ||||
|         console.error("Error al cargar plantas:", error); | ||||
|         setLocalErrors(prev => ({ ...prev, dropdowns: 'Error al cargar plantas.' })); | ||||
|       } finally { | ||||
|         setLoadingDropdowns(false); | ||||
|       } | ||||
|     }; | ||||
|     fetchPlantas(); | ||||
|   }, []); | ||||
|  | ||||
|   const validate = (): boolean => { | ||||
|     const errors: { [key: string]: string | null } = {}; | ||||
|     if (!fechaDesde) errors.fechaDesde = 'Fecha Desde es obligatoria.'; | ||||
|     if (!fechaHasta) errors.fechaHasta = 'Fecha Hasta es obligatoria.'; | ||||
|     if (fechaDesde && fechaHasta && new Date(fechaDesde) > new Date(fechaHasta)) { | ||||
|       errors.fechaHasta = 'Fecha Hasta no puede ser anterior a Fecha Desde.'; | ||||
|     } | ||||
|     if (!idPlanta) { | ||||
|       errors.idPlanta = 'Debe seleccionar una planta.'; | ||||
|     } | ||||
|     setLocalErrors(errors); | ||||
|     return Object.keys(errors).length === 0; | ||||
|   }; | ||||
|  | ||||
|   const handleGenerar = () => { | ||||
|     if (!validate()) return; | ||||
|     onGenerarReporte({ | ||||
|       fechaDesde, | ||||
|       fechaHasta, | ||||
|       idPlanta: Number(idPlanta) // Asegurarse de que es un número | ||||
|     }); | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <Box sx={{ p: 2, border: '1px solid #ccc', borderRadius: '4px', minWidth: 380 }}> | ||||
|       <Typography variant="h6" gutterBottom> | ||||
|         Parámetros: Movimiento de Bobinas | ||||
|       </Typography> | ||||
|       <TextField | ||||
|         label="Fecha Desde" | ||||
|         type="date" | ||||
|         value={fechaDesde} | ||||
|         onChange={(e) => { setFechaDesde(e.target.value); setLocalErrors(p => ({ ...p, fechaDesde: null, fechaHasta: null })); }} | ||||
|         margin="normal" | ||||
|         fullWidth | ||||
|         required | ||||
|         error={!!localErrors.fechaDesde} | ||||
|         helperText={localErrors.fechaDesde} | ||||
|         disabled={isLoading} | ||||
|         InputLabelProps={{ shrink: true }} | ||||
|       /> | ||||
|       <TextField | ||||
|         label="Fecha Hasta" | ||||
|         type="date" | ||||
|         value={fechaHasta} | ||||
|         onChange={(e) => { setFechaHasta(e.target.value); setLocalErrors(p => ({ ...p, fechaHasta: null })); }} | ||||
|         margin="normal" | ||||
|         fullWidth | ||||
|         required | ||||
|         error={!!localErrors.fechaHasta} | ||||
|         helperText={localErrors.fechaHasta} | ||||
|         disabled={isLoading} | ||||
|         InputLabelProps={{ shrink: true }} | ||||
|       /> | ||||
|       <FormControl fullWidth margin="normal" error={!!localErrors.idPlanta} disabled={isLoading || loadingDropdowns}> | ||||
|         <InputLabel id="planta-select-label" required>Planta</InputLabel> | ||||
|         <Select | ||||
|           labelId="planta-select-label" | ||||
|           label="Planta" | ||||
|           value={idPlanta} | ||||
|           onChange={(e) => { setIdPlanta(e.target.value as number); setLocalErrors(p => ({ ...p, idPlanta: null })); }} | ||||
|         > | ||||
|           <MenuItem value="" disabled><em>Seleccione una planta</em></MenuItem> | ||||
|           {plantas.map((p) => ( | ||||
|             <MenuItem key={p.idPlanta} value={p.idPlanta}>{p.nombre}</MenuItem> | ||||
|           ))} | ||||
|         </Select> | ||||
|         {localErrors.idPlanta && <Typography color="error" variant="caption" sx={{ml:1.5}}>{localErrors.idPlanta}</Typography>} | ||||
|       </FormControl> | ||||
|  | ||||
|       {apiErrorMessage && <Alert severity="error" sx={{ mt: 2 }}>{apiErrorMessage}</Alert>} | ||||
|       {localErrors.dropdowns && <Alert severity="warning" sx={{ mt: 1 }}>{localErrors.dropdowns}</Alert>} | ||||
|  | ||||
|       <Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', gap: 1 }}> | ||||
|         <Button onClick={handleGenerar} variant="contained" disabled={isLoading || loadingDropdowns}> | ||||
|           {isLoading ? <CircularProgress size={24} /> : 'Generar Reporte'} | ||||
|         </Button> | ||||
|       </Box> | ||||
|     </Box> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export default SeleccionaReporteMovimientoBobinas; | ||||
| @@ -0,0 +1,131 @@ | ||||
| import React, { useState, useEffect } from 'react'; | ||||
| import { | ||||
|     Box, Typography, TextField, Button, CircularProgress, Alert, | ||||
|     FormControl, InputLabel, Select, MenuItem | ||||
| } from '@mui/material'; | ||||
| import type { PlantaDto } from '../../models/dtos/Impresion/PlantaDto'; | ||||
| import plantaService from '../../services/Impresion/plantaService'; | ||||
|  | ||||
| interface SeleccionaReporteMovimientoBobinasEstadoProps { | ||||
|   onGenerarReporte: (params: { | ||||
|     fechaDesde: string; | ||||
|     fechaHasta: string; | ||||
|     idPlanta: number; | ||||
|   }) => Promise<void>; | ||||
|   onCancel: () => void; | ||||
|   isLoading?: boolean; | ||||
|   apiErrorMessage?: string | null; | ||||
| } | ||||
|  | ||||
| const SeleccionaReporteMovimientoBobinasEstado: React.FC<SeleccionaReporteMovimientoBobinasEstadoProps> = ({ | ||||
|   onGenerarReporte, | ||||
|   isLoading, | ||||
|   apiErrorMessage | ||||
| }) => { | ||||
|   const [fechaDesde, setFechaDesde] = useState<string>(new Date().toISOString().split('T')[0]); | ||||
|   const [fechaHasta, setFechaHasta] = useState<string>(new Date().toISOString().split('T')[0]); | ||||
|   const [idPlanta, setIdPlanta] = useState<number | string>(''); | ||||
|  | ||||
|   const [plantas, setPlantas] = useState<PlantaDto[]>([]); | ||||
|   const [loadingDropdowns, setLoadingDropdowns] = useState(false); | ||||
|   const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({}); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     const fetchPlantas = async () => { | ||||
|       setLoadingDropdowns(true); | ||||
|       try { | ||||
|         const plantasData = await plantaService.getAllPlantas(); | ||||
|         setPlantas(plantasData); | ||||
|       } catch (error) { | ||||
|         console.error("Error al cargar plantas:", error); | ||||
|         setLocalErrors(prev => ({ ...prev, dropdowns: 'Error al cargar plantas.' })); | ||||
|       } finally { | ||||
|         setLoadingDropdowns(false); | ||||
|       } | ||||
|     }; | ||||
|     fetchPlantas(); | ||||
|   }, []); | ||||
|  | ||||
|   const validate = (): boolean => { | ||||
|     const errors: { [key: string]: string | null } = {}; | ||||
|     if (!fechaDesde) errors.fechaDesde = 'Fecha Desde es obligatoria.'; | ||||
|     if (!fechaHasta) errors.fechaHasta = 'Fecha Hasta es obligatoria.'; | ||||
|     if (fechaDesde && fechaHasta && new Date(fechaDesde) > new Date(fechaHasta)) { | ||||
|       errors.fechaHasta = 'Fecha Hasta no puede ser anterior a Fecha Desde.'; | ||||
|     } | ||||
|     if (!idPlanta) { | ||||
|       errors.idPlanta = 'Debe seleccionar una planta.'; | ||||
|     } | ||||
|     setLocalErrors(errors); | ||||
|     return Object.keys(errors).length === 0; | ||||
|   }; | ||||
|  | ||||
|   const handleGenerar = () => { | ||||
|     if (!validate()) return; | ||||
|     onGenerarReporte({ | ||||
|       fechaDesde, | ||||
|       fechaHasta, | ||||
|       idPlanta: Number(idPlanta) | ||||
|     }); | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <Box sx={{ p: 2, border: '1px solid #ccc', borderRadius: '4px', minWidth: 380 }}> | ||||
|       <Typography variant="h6" gutterBottom> | ||||
|         Parámetros: Movimiento de Bobinas por Estado | ||||
|       </Typography> | ||||
|       <TextField | ||||
|         label="Fecha Desde" | ||||
|         type="date" | ||||
|         value={fechaDesde} | ||||
|         onChange={(e) => { setFechaDesde(e.target.value); setLocalErrors(p => ({ ...p, fechaDesde: null, fechaHasta: null })); }} | ||||
|         margin="normal" | ||||
|         fullWidth | ||||
|         required | ||||
|         error={!!localErrors.fechaDesde} | ||||
|         helperText={localErrors.fechaDesde} | ||||
|         disabled={isLoading} | ||||
|         InputLabelProps={{ shrink: true }} | ||||
|       /> | ||||
|       <TextField | ||||
|         label="Fecha Hasta" | ||||
|         type="date" | ||||
|         value={fechaHasta} | ||||
|         onChange={(e) => { setFechaHasta(e.target.value); setLocalErrors(p => ({ ...p, fechaHasta: null })); }} | ||||
|         margin="normal" | ||||
|         fullWidth | ||||
|         required | ||||
|         error={!!localErrors.fechaHasta} | ||||
|         helperText={localErrors.fechaHasta} | ||||
|         disabled={isLoading} | ||||
|         InputLabelProps={{ shrink: true }} | ||||
|       /> | ||||
|       <FormControl fullWidth margin="normal" error={!!localErrors.idPlanta} disabled={isLoading || loadingDropdowns}> | ||||
|         <InputLabel id="planta-select-label-estado" required>Planta</InputLabel> | ||||
|         <Select | ||||
|           labelId="planta-select-label-estado" | ||||
|           label="Planta" | ||||
|           value={idPlanta} | ||||
|           onChange={(e) => { setIdPlanta(e.target.value as number); setLocalErrors(p => ({ ...p, idPlanta: null })); }} | ||||
|         > | ||||
|           <MenuItem value="" disabled><em>Seleccione una planta</em></MenuItem> | ||||
|           {plantas.map((p) => ( | ||||
|             <MenuItem key={p.idPlanta} value={p.idPlanta}>{p.nombre}</MenuItem> | ||||
|           ))} | ||||
|         </Select> | ||||
|         {localErrors.idPlanta && <Typography color="error" variant="caption" sx={{ml:1.5}}>{localErrors.idPlanta}</Typography>} | ||||
|       </FormControl> | ||||
|  | ||||
|       {apiErrorMessage && <Alert severity="error" sx={{ mt: 2 }}>{apiErrorMessage}</Alert>} | ||||
|       {localErrors.dropdowns && <Alert severity="warning" sx={{ mt: 1 }}>{localErrors.dropdowns}</Alert>} | ||||
|  | ||||
|       <Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', gap: 1 }}> | ||||
|         <Button onClick={handleGenerar} variant="contained" disabled={isLoading || loadingDropdowns}> | ||||
|           {isLoading ? <CircularProgress size={24} /> : 'Generar Reporte'} | ||||
|         </Button> | ||||
|       </Box> | ||||
|     </Box> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export default SeleccionaReporteMovimientoBobinasEstado; | ||||
| @@ -53,8 +53,12 @@ import GestionarCancionesPage from '../pages/Radios/GestionarCancionesPage'; | ||||
| import GenerarListasRadioPage from '../pages/Radios/GenerarListasRadioPage'; | ||||
|  | ||||
| // Reportes | ||||
| import ReportesIndexPage from '../pages/Reportes/ReportesIndexPage'; // Crear este si no existe | ||||
| import ReportesIndexPage from '../pages/Reportes/ReportesIndexPage'; | ||||
| import ReporteExistenciaPapelPage from '../pages/Reportes/ReporteExistenciaPapelPage'; | ||||
| import ReporteMovimientoBobinasPage from '../pages/Reportes/ReporteMovimientoBobinasPage'; | ||||
| import ReporteMovimientoBobinasEstadoPage from '../pages/Reportes/ReporteMovimientoBobinasEstadoPage'; | ||||
| import ReporteListadoDistribucionGeneralPage from '../pages/Reportes/ReporteListadoDistribucionGeneralPage'; | ||||
| import ReporteListadoDistribucionCanillasPage from '../pages/Reportes/ReporteListadoDistribucionCanillasPage'; | ||||
|  | ||||
| // Auditorias | ||||
| import GestionarAuditoriaUsuariosPage from '../pages/Usuarios/Auditoria/GestionarAuditoriaUsuariosPage'; | ||||
| @@ -153,7 +157,10 @@ const AppRoutes = () => { | ||||
|           <Route path="reportes" element={<ReportesIndexPage />}> {/* Página principal del módulo */} | ||||
|             <Route index element={<Typography sx={{p:2}}>Seleccione un reporte del menú lateral.</Typography>} /> {/* Placeholder */} | ||||
|             <Route path="existencia-papel" element={<ReporteExistenciaPapelPage />} /> | ||||
|             {/* Aquí se añadirán las rutas para otros reportes */} | ||||
|             <Route path="movimiento-bobinas" element={<ReporteMovimientoBobinasPage />} /> | ||||
|             <Route path="movimiento-bobinas-estado" element={<ReporteMovimientoBobinasEstadoPage />} /> | ||||
|             <Route path="listado-distribucion-general" element={<ReporteListadoDistribucionGeneralPage />} /> | ||||
|             <Route path="listado-distribucion-canillas" element={<ReporteListadoDistribucionCanillasPage />} /> | ||||
|           </Route> | ||||
|  | ||||
|           {/* Módulo de Radios (anidado) */} | ||||
|   | ||||
| @@ -1,5 +1,9 @@ | ||||
| import apiClient from '../apiClient'; | ||||
| import type { ExistenciaPapelDto } from '../../models/dtos/Reportes/ExistenciaPapelDto'; | ||||
| import type { MovimientoBobinasDto } from '../../models/dtos/Reportes/MovimientoBobinasDto'; | ||||
| import type { MovimientoBobinasPorEstadoResponseDto } from '../../models/dtos/Reportes/MovimientoBobinasPorEstadoResponseDto'; | ||||
| import type { ListadoDistribucionGeneralResponseDto } from '../../models/dtos/Reportes/ListadoDistribucionGeneralResponseDto'; | ||||
| import type { ListadoDistribucionCanillasResponseDto } from '../../models/dtos/Reportes/ListadoDistribucionCanillasResponseDto'; | ||||
|  | ||||
| interface GetExistenciaPapelParams { | ||||
|     fechaDesde: string; // yyyy-MM-dd | ||||
| @@ -40,13 +44,102 @@ const getExistenciaPapel = async (params: GetExistenciaPapelParams): Promise<Exi | ||||
|     return response.data; | ||||
| }; | ||||
|  | ||||
| const getMovimientoBobinas = async (params: { | ||||
|     fechaDesde: string; | ||||
|     fechaHasta: string; | ||||
|     idPlanta: number; | ||||
| }): Promise<MovimientoBobinasDto[]> => { | ||||
|     const response = await apiClient.get<MovimientoBobinasDto[]>('/reportes/movimiento-bobinas', { params }); | ||||
|     return response.data; | ||||
| }; | ||||
|  | ||||
| // ... Aquí irán los métodos para otros reportes ... | ||||
| const getMovimientoBobinasPdf = async (params: { | ||||
|     fechaDesde: string; | ||||
|     fechaHasta: string; | ||||
|     idPlanta: number; | ||||
| }): Promise<Blob> => { | ||||
|     const response = await apiClient.get('/reportes/movimiento-bobinas/pdf', { | ||||
|         params, | ||||
|         responseType: 'blob', | ||||
|     }); | ||||
|     return response.data; | ||||
| }; | ||||
|  | ||||
| const getMovimientoBobinasEstado = async (params: { | ||||
|     fechaDesde: string; | ||||
|     fechaHasta: string; | ||||
|     idPlanta: number; | ||||
| }): Promise<MovimientoBobinasPorEstadoResponseDto> => { // <- Devuelve el DTO combinado | ||||
|     const response = await apiClient.get<MovimientoBobinasPorEstadoResponseDto>('/reportes/movimiento-bobinas-estado', { params }); | ||||
|     return response.data; | ||||
| }; | ||||
|  | ||||
| const getMovimientoBobinasEstadoPdf = async (params: { | ||||
|     fechaDesde: string; | ||||
|     fechaHasta: string; | ||||
|     idPlanta: number; | ||||
| }): Promise<Blob> => { | ||||
|     const response = await apiClient.get('/reportes/movimiento-bobinas-estado/pdf', { | ||||
|         params, | ||||
|         responseType: 'blob', | ||||
|     }); | ||||
|     return response.data; | ||||
| }; | ||||
|  | ||||
| const getListadoDistribucionGeneral = async (params: { | ||||
|     idPublicacion: number; | ||||
|     fechaDesde: string; // YYYY-MM-DD (primer día del mes) | ||||
|     fechaHasta: string; // YYYY-MM-DD (último día del mes) | ||||
| }): Promise<ListadoDistribucionGeneralResponseDto> => { | ||||
|     const response = await apiClient.get<ListadoDistribucionGeneralResponseDto>('/reportes/listado-distribucion-general', { params }); | ||||
|     return response.data; | ||||
| }; | ||||
|  | ||||
| const getListadoDistribucionGeneralPdf = async (params: { | ||||
|     idPublicacion: number; | ||||
|     fechaDesde: string; | ||||
|     fechaHasta: string; | ||||
| }): Promise<Blob> => { | ||||
|     const response = await apiClient.get('/reportes/listado-distribucion-general/pdf', { | ||||
|         params, | ||||
|         responseType: 'blob', | ||||
|     }); | ||||
|     return response.data; | ||||
| }; | ||||
|  | ||||
| const getListadoDistribucionCanillas = async (params: { | ||||
|     idPublicacion: number; | ||||
|     fechaDesde: string; | ||||
|     fechaHasta: string; | ||||
| }): Promise<ListadoDistribucionCanillasResponseDto> => { | ||||
|     const response = await apiClient.get<ListadoDistribucionCanillasResponseDto>('/reportes/listado-distribucion-canillas', { params }); | ||||
|     return response.data; | ||||
| }; | ||||
|  | ||||
|  | ||||
| const getListadoDistribucionCanillasPdf = async (params: { | ||||
|     idPublicacion: number; | ||||
|     fechaDesde: string; | ||||
|     fechaHasta: string; | ||||
| }): Promise<Blob> => { | ||||
|     const response = await apiClient.get('/reportes/listado-distribucion-canillas/pdf', { | ||||
|         params, | ||||
|         responseType: 'blob', | ||||
|     }); | ||||
|     return response.data; | ||||
| }; | ||||
|  | ||||
| const reportesService = { | ||||
|     getExistenciaPapel, | ||||
|     getExistenciaPapelPdf, | ||||
|     // ... | ||||
|     getMovimientoBobinas, | ||||
|     getMovimientoBobinasPdf, | ||||
|     getMovimientoBobinasEstado, | ||||
|     getMovimientoBobinasEstadoPdf, | ||||
|     getListadoDistribucionGeneral, | ||||
|     getListadoDistribucionGeneralPdf, | ||||
|     getListadoDistribucionCanillas, | ||||
|     getListadoDistribucionCanillasPdf | ||||
| }; | ||||
|  | ||||
| export default reportesService; | ||||
		Reference in New Issue
	
	Block a user