From 70fc84772161b499c8283a31b7a61246a6bcc46f Mon Sep 17 00:00:00 2001 From: eldiadmolinari Date: Wed, 28 May 2025 18:58:45 -0300 Subject: [PATCH] Continuidad de reportes Frontend. Se sigue.. --- .../GestionIntegral.Api.AssemblyInfo.cs | 2 +- ...stadoDistribucionCanillasPromedioDiaDto.ts | 9 + .../ListadoDistribucionCanillasResponseDto.ts | 7 + .../ListadoDistribucionCanillasSimpleDto.ts | 5 + ...istadoDistribucionGeneralPromedioDiaDto.ts | 10 + .../ListadoDistribucionGeneralResponseDto.ts | 7 + .../ListadoDistribucionGeneralResumenDto.ts | 9 + .../MovimientoBobinaEstadoDetalleDto.ts | 7 + .../MovimientoBobinaEstadoTotalDto.ts | 5 + .../dtos/Reportes/MovimientoBobinasDto.ts | 13 + .../MovimientoBobinasPorEstadoResponseDto.ts | 7 + ...ReporteListadoDistribucionCanillasPage.tsx | 225 +++++++++++++++ .../ReporteListadoDistribucionGeneralPage.tsx | 252 +++++++++++++++++ .../ReporteMovimientoBobinasEstadoPage.tsx | 257 ++++++++++++++++++ .../Reportes/ReporteMovimientoBobinasPage.tsx | 212 +++++++++++++++ .../src/pages/Reportes/ReportesIndexPage.tsx | 6 +- .../SeleccionaReporteExistenciaPapel.tsx | 4 - ...ionaReporteListadoDistribucionCanillas.tsx | 129 +++++++++ ...cionaReporteListadoDistribucionGeneral.tsx | 120 ++++++++ .../SeleccionaReporteMovimientoBobinas.tsx | 131 +++++++++ ...leccionaReporteMovimientoBobinasEstado.tsx | 131 +++++++++ Frontend/src/routes/AppRoutes.tsx | 11 +- .../src/services/Reportes/reportesService.ts | 97 ++++++- 23 files changed, 1645 insertions(+), 11 deletions(-) create mode 100644 Frontend/src/models/dtos/Reportes/ListadoDistribucionCanillasPromedioDiaDto.ts create mode 100644 Frontend/src/models/dtos/Reportes/ListadoDistribucionCanillasResponseDto.ts create mode 100644 Frontend/src/models/dtos/Reportes/ListadoDistribucionCanillasSimpleDto.ts create mode 100644 Frontend/src/models/dtos/Reportes/ListadoDistribucionGeneralPromedioDiaDto.ts create mode 100644 Frontend/src/models/dtos/Reportes/ListadoDistribucionGeneralResponseDto.ts create mode 100644 Frontend/src/models/dtos/Reportes/ListadoDistribucionGeneralResumenDto.ts create mode 100644 Frontend/src/models/dtos/Reportes/MovimientoBobinaEstadoDetalleDto.ts create mode 100644 Frontend/src/models/dtos/Reportes/MovimientoBobinaEstadoTotalDto.ts create mode 100644 Frontend/src/models/dtos/Reportes/MovimientoBobinasDto.ts create mode 100644 Frontend/src/models/dtos/Reportes/MovimientoBobinasPorEstadoResponseDto.ts create mode 100644 Frontend/src/pages/Reportes/ReporteListadoDistribucionCanillasPage.tsx create mode 100644 Frontend/src/pages/Reportes/ReporteListadoDistribucionGeneralPage.tsx create mode 100644 Frontend/src/pages/Reportes/ReporteMovimientoBobinasEstadoPage.tsx create mode 100644 Frontend/src/pages/Reportes/ReporteMovimientoBobinasPage.tsx create mode 100644 Frontend/src/pages/Reportes/SeleccionaReporteListadoDistribucionCanillas.tsx create mode 100644 Frontend/src/pages/Reportes/SeleccionaReporteListadoDistribucionGeneral.tsx create mode 100644 Frontend/src/pages/Reportes/SeleccionaReporteMovimientoBobinas.tsx create mode 100644 Frontend/src/pages/Reportes/SeleccionaReporteMovimientoBobinasEstado.tsx diff --git a/Backend/GestionIntegral.Api/obj/Debug/net9.0/GestionIntegral.Api.AssemblyInfo.cs b/Backend/GestionIntegral.Api/obj/Debug/net9.0/GestionIntegral.Api.AssemblyInfo.cs index 30bf741..4397683 100644 --- a/Backend/GestionIntegral.Api/obj/Debug/net9.0/GestionIntegral.Api.AssemblyInfo.cs +++ b/Backend/GestionIntegral.Api/obj/Debug/net9.0/GestionIntegral.Api.AssemblyInfo.cs @@ -13,7 +13,7 @@ using System.Reflection; [assembly: System.Reflection.AssemblyCompanyAttribute("GestionIntegral.Api")] [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] -[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+cdd4d3e0f71f866aabb489394a273ab4d013284c")] +[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+2273ebb1e018273a6e35d3f9ab0afe55ae1814bc")] [assembly: System.Reflection.AssemblyProductAttribute("GestionIntegral.Api")] [assembly: System.Reflection.AssemblyTitleAttribute("GestionIntegral.Api")] [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] diff --git a/Frontend/src/models/dtos/Reportes/ListadoDistribucionCanillasPromedioDiaDto.ts b/Frontend/src/models/dtos/Reportes/ListadoDistribucionCanillasPromedioDiaDto.ts new file mode 100644 index 0000000..c7d2b76 --- /dev/null +++ b/Frontend/src/models/dtos/Reportes/ListadoDistribucionCanillasPromedioDiaDto.ts @@ -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; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Reportes/ListadoDistribucionCanillasResponseDto.ts b/Frontend/src/models/dtos/Reportes/ListadoDistribucionCanillasResponseDto.ts new file mode 100644 index 0000000..5b49268 --- /dev/null +++ b/Frontend/src/models/dtos/Reportes/ListadoDistribucionCanillasResponseDto.ts @@ -0,0 +1,7 @@ +import type { ListadoDistribucionCanillasSimpleDto } from './ListadoDistribucionCanillasSimpleDto'; +import type { ListadoDistribucionCanillasPromedioDiaDto } from './ListadoDistribucionCanillasPromedioDiaDto'; + +export interface ListadoDistribucionCanillasResponseDto { + detalleSimple: ListadoDistribucionCanillasSimpleDto[]; + promediosPorDia: ListadoDistribucionCanillasPromedioDiaDto[]; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Reportes/ListadoDistribucionCanillasSimpleDto.ts b/Frontend/src/models/dtos/Reportes/ListadoDistribucionCanillasSimpleDto.ts new file mode 100644 index 0000000..8175db1 --- /dev/null +++ b/Frontend/src/models/dtos/Reportes/ListadoDistribucionCanillasSimpleDto.ts @@ -0,0 +1,5 @@ +export interface ListadoDistribucionCanillasSimpleDto { + dia: number; // Día del mes + llevados: number; + devueltos: number; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Reportes/ListadoDistribucionGeneralPromedioDiaDto.ts b/Frontend/src/models/dtos/Reportes/ListadoDistribucionGeneralPromedioDiaDto.ts new file mode 100644 index 0000000..fabd33c --- /dev/null +++ b/Frontend/src/models/dtos/Reportes/ListadoDistribucionGeneralPromedioDiaDto.ts @@ -0,0 +1,10 @@ +export interface ListadoDistribucionGeneralPromedioDiaDto { + dia: string; + cantidadDias: number; + promedioTirada: number; + promedioSinCargo: number; + promedioPerdidos: number; + promedioLlevados: number; + promedioDevueltos: number; + promedioVendidos: number; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Reportes/ListadoDistribucionGeneralResponseDto.ts b/Frontend/src/models/dtos/Reportes/ListadoDistribucionGeneralResponseDto.ts new file mode 100644 index 0000000..50d9cb7 --- /dev/null +++ b/Frontend/src/models/dtos/Reportes/ListadoDistribucionGeneralResponseDto.ts @@ -0,0 +1,7 @@ +import type { ListadoDistribucionGeneralResumenDto } from './ListadoDistribucionGeneralResumenDto'; +import type { ListadoDistribucionGeneralPromedioDiaDto } from './ListadoDistribucionGeneralPromedioDiaDto'; + +export interface ListadoDistribucionGeneralResponseDto { + resumen: ListadoDistribucionGeneralResumenDto[]; + promediosPorDia: ListadoDistribucionGeneralPromedioDiaDto[]; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Reportes/ListadoDistribucionGeneralResumenDto.ts b/Frontend/src/models/dtos/Reportes/ListadoDistribucionGeneralResumenDto.ts new file mode 100644 index 0000000..78a42d6 --- /dev/null +++ b/Frontend/src/models/dtos/Reportes/ListadoDistribucionGeneralResumenDto.ts @@ -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; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Reportes/MovimientoBobinaEstadoDetalleDto.ts b/Frontend/src/models/dtos/Reportes/MovimientoBobinaEstadoDetalleDto.ts new file mode 100644 index 0000000..15a60ca --- /dev/null +++ b/Frontend/src/models/dtos/Reportes/MovimientoBobinaEstadoDetalleDto.ts @@ -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" +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Reportes/MovimientoBobinaEstadoTotalDto.ts b/Frontend/src/models/dtos/Reportes/MovimientoBobinaEstadoTotalDto.ts new file mode 100644 index 0000000..fc008d4 --- /dev/null +++ b/Frontend/src/models/dtos/Reportes/MovimientoBobinaEstadoTotalDto.ts @@ -0,0 +1,5 @@ +export interface MovimientoBobinaEstadoTotalDto { + tipoMovimiento: string; // "Ingresos", "Utilizadas", "Dañadas" + totalBobinas: number; + totalKilos: number; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Reportes/MovimientoBobinasDto.ts b/Frontend/src/models/dtos/Reportes/MovimientoBobinasDto.ts new file mode 100644 index 0000000..71313aa --- /dev/null +++ b/Frontend/src/models/dtos/Reportes/MovimientoBobinasDto.ts @@ -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; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Reportes/MovimientoBobinasPorEstadoResponseDto.ts b/Frontend/src/models/dtos/Reportes/MovimientoBobinasPorEstadoResponseDto.ts new file mode 100644 index 0000000..eed6dcb --- /dev/null +++ b/Frontend/src/models/dtos/Reportes/MovimientoBobinasPorEstadoResponseDto.ts @@ -0,0 +1,7 @@ +import type { MovimientoBobinaEstadoDetalleDto } from "./MovimientoBobinaEstadoDetalleDto"; +import type { MovimientoBobinaEstadoTotalDto } from "./MovimientoBobinaEstadoTotalDto"; + +export interface MovimientoBobinasPorEstadoResponseDto { + detalle: MovimientoBobinaEstadoDetalleDto[]; + totales: MovimientoBobinaEstadoTotalDto[]; +} \ No newline at end of file diff --git a/Frontend/src/pages/Reportes/ReporteListadoDistribucionCanillasPage.tsx b/Frontend/src/pages/Reportes/ReporteListadoDistribucionCanillasPage.tsx new file mode 100644 index 0000000..a48d92f --- /dev/null +++ b/Frontend/src/pages/Reportes/ReporteListadoDistribucionCanillasPage.tsx @@ -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(null); + const [loading, setLoading] = useState(false); + const [loadingPdf, setLoadingPdf] = useState(false); + const [error, setError] = useState(null); + const [apiErrorParams, setApiErrorParams] = useState(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 ( + + + + + + ); + } + + return ( + + + Reporte: Listado Distribución Canillitas + + + + + + + + {loading && } + {error && !loading && {error}} + + {!loading && !error && reportData && ( + <> + Detalle Diario + {reportData.detalleSimple && reportData.detalleSimple.length > 0 ? ( + + + + + Día + Llevados + Devueltos + Vendidos + + + + {reportData.detalleSimple.map((row, idx) => ( + + {row.dia} + {row.llevados.toLocaleString('es-AR')} + {row.devueltos.toLocaleString('es-AR')} + {(row.llevados - row.devueltos).toLocaleString('es-AR')} + + ))} + +
+
+ ) : (No hay datos de detalle diario.)} + + Promedios por Día de Semana + {reportData.promediosPorDia && reportData.promediosPorDia.length > 0 ? ( + + + + + Día Semana + Cant. Días + Prom. Llevados + Prom. Devueltos + Prom. Ventas + + + + {reportData.promediosPorDia.map((row, idx) => ( + + {row.dia} + {row.cant.toLocaleString('es-AR')} + {row.promedio_Llevados.toLocaleString('es-AR')} + {row.promedio_Devueltos.toLocaleString('es-AR')} + {row.promedio_Ventas.toLocaleString('es-AR')} + + ))} + +
+
+ ) : (No hay datos de promedios por día.)} + + )} +
+ ); +}; + +export default ReporteListadoDistribucionCanillasPage; \ No newline at end of file diff --git a/Frontend/src/pages/Reportes/ReporteListadoDistribucionGeneralPage.tsx b/Frontend/src/pages/Reportes/ReporteListadoDistribucionGeneralPage.tsx new file mode 100644 index 0000000..c313e4b --- /dev/null +++ b/Frontend/src/pages/Reportes/ReporteListadoDistribucionGeneralPage.tsx @@ -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(null); + const [loading, setLoading] = useState(false); + const [loadingPdf, setLoadingPdf] = useState(false); + const [error, setError] = useState(null); + const [apiErrorParams, setApiErrorParams] = useState(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 ( + + + + + + ); + } + + return ( + + + Reporte: Listado Distribución General + + + + + + + + {loading && } + {error && !loading && {error}} + + {!loading && !error && reportData && ( + <> + Resumen Diario + {reportData.resumen && reportData.resumen.length > 0 ? ( + + + + + Fecha + Tirada + Sin Cargo + Perdidos + Llevados + Devueltos + Vendidos + + + + {reportData.resumen.map((row, idx) => ( + + {row.fecha ? new Date(row.fecha).toLocaleDateString('es-AR', { timeZone: 'UTC' }) : '-'} + {row.cantidadTirada.toLocaleString('es-AR')} + {row.sinCargo.toLocaleString('es-AR')} + {row.perdidos.toLocaleString('es-AR')} + {row.llevados.toLocaleString('es-AR')} + {row.devueltos.toLocaleString('es-AR')} + {row.vendidos.toLocaleString('es-AR')} + + ))} + +
+
+ ) : (No hay datos de resumen diario.)} + + Promedios por Día de Semana + {reportData.promediosPorDia && reportData.promediosPorDia.length > 0 ? ( + + + + + Día + Cant. Días + Prom. Tirada + Prom. Sin Cargo + Prom. Perdidos + Prom. Llevados + Prom. Devueltos + Prom. Vendidos + + + + {reportData.promediosPorDia.map((row, idx) => ( + + {row.dia} + {row.cantidadDias.toLocaleString('es-AR')} + {row.promedioTirada.toLocaleString('es-AR')} + {row.promedioSinCargo.toLocaleString('es-AR')} + {row.promedioPerdidos.toLocaleString('es-AR')} + {row.promedioLlevados.toLocaleString('es-AR')} + {row.promedioDevueltos.toLocaleString('es-AR')} + {row.promedioVendidos.toLocaleString('es-AR')} + + ))} + +
+
+ ) : (No hay datos de promedios por día.)} + + )} +
+ ); +}; + +export default ReporteListadoDistribucionGeneralPage; \ No newline at end of file diff --git a/Frontend/src/pages/Reportes/ReporteMovimientoBobinasEstadoPage.tsx b/Frontend/src/pages/Reportes/ReporteMovimientoBobinasEstadoPage.tsx new file mode 100644 index 0000000..2a8c54e --- /dev/null +++ b/Frontend/src/pages/Reportes/ReporteMovimientoBobinasEstadoPage.tsx @@ -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(null); + const [loading, setLoading] = useState(false); + const [loadingPdf, setLoadingPdf] = useState(false); + const [error, setError] = useState(null); + const [apiErrorParams, setApiErrorParams] = useState(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 ( + + + + + + ); + } + + return ( + + + Reporte: Movimiento de Bobinas por Estado + + + + + + + + {loading && } + {error && !loading && {error}} + + {!loading && !error && reportData && ( + <> {/* Usamos un Fragmento React para agrupar los elementos sin añadir un div extra */} + {/* Tabla de Detalle de Movimientos */} + + Detalle de Movimientos + + {reportData.detalle && reportData.detalle.length > 0 ? ( + {/* Añadido mb: 3 para espaciado */} + + + + Tipo Bobina + Nro Remito + Fecha Movimiento + Cantidad + Tipo Movimiento + + + + {reportData.detalle.map((row, idx) => ( + + {row.tipoBobina} + {row.numeroRemito} + {row.fechaMovimiento ? new Date(row.fechaMovimiento).toLocaleDateString('es-AR', { timeZone: 'UTC' }) : '-'} + {row.cantidad.toLocaleString('es-AR')} + {row.tipoMovimiento} + + ))} + +
+
+ ) : ( + No hay detalles de movimientos para mostrar. + )} + + {/* Tabla de Totales por Estado */} + + Totales por Estado + + {reportData.totales && reportData.totales.length > 0 ? ( + {/* Limitamos el ancho para tablas pequeñas */} + + + + Tipo Movimiento + Total Bobinas + Total Kilos + + + + {reportData.totales.map((row, idx) => ( + + {row.tipoMovimiento} + {row.totalBobinas.toLocaleString('es-AR')} + {row.totalKilos.toLocaleString('es-AR')} + + ))} + +
+
+ ) : ( + No hay totales para mostrar. + )} + + )} +
+ ); +}; + +export default ReporteMovimientoBobinasEstadoPage; \ No newline at end of file diff --git a/Frontend/src/pages/Reportes/ReporteMovimientoBobinasPage.tsx b/Frontend/src/pages/Reportes/ReporteMovimientoBobinasPage.tsx new file mode 100644 index 0000000..574e296 --- /dev/null +++ b/Frontend/src/pages/Reportes/ReporteMovimientoBobinasPage.tsx @@ -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([]); + const [loading, setLoading] = useState(false); + const [loadingPdf, setLoadingPdf] = useState(false); + const [error, setError] = useState(null); + const [apiErrorParams, setApiErrorParams] = useState(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 ( + + + + + + ); + } + + return ( + + + Reporte: Movimiento de Bobinas + + + + + + + + {loading && } + {error && !loading && {error}} + + {!loading && !error && ( + + + + + Tipo Bobina + Cant. Ini. + Kg Ini. + Compradas + Kg Compr. + Consum. + Kg Consum. + Dañadas + Kg Dañ. + Cant. Fin. + Kg Finales + + + + {reportData.map((row, idx) => ( + + {row.tipoBobina} + {row.bobinasIniciales.toLocaleString('es-AR')} + {row.kilosIniciales.toLocaleString('es-AR')} + {row.bobinasCompradas.toLocaleString('es-AR')} + {row.kilosComprados.toLocaleString('es-AR')} + {row.bobinasConsumidas.toLocaleString('es-AR')} + {row.kilosConsumidos.toLocaleString('es-AR')} + {row.bobinasDaniadas.toLocaleString('es-AR')} + {row.kilosDaniados.toLocaleString('es-AR')} + {row.bobinasFinales.toLocaleString('es-AR')} + {row.kilosFinales.toLocaleString('es-AR')} + + ))} + +
+
+ )} +
+ ); +}; + +export default ReporteMovimientoBobinasPage; \ No newline at end of file diff --git a/Frontend/src/pages/Reportes/ReportesIndexPage.tsx b/Frontend/src/pages/Reportes/ReportesIndexPage.tsx index ab09e77..c10afa6 100644 --- a/Frontend/src/pages/Reportes/ReportesIndexPage.tsx +++ b/Frontend/src/pages/Reportes/ReportesIndexPage.tsx @@ -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 = () => { diff --git a/Frontend/src/pages/Reportes/SeleccionaReporteExistenciaPapel.tsx b/Frontend/src/pages/Reportes/SeleccionaReporteExistenciaPapel.tsx index d2e6596..f475a48 100644 --- a/Frontend/src/pages/Reportes/SeleccionaReporteExistenciaPapel.tsx +++ b/Frontend/src/pages/Reportes/SeleccionaReporteExistenciaPapel.tsx @@ -20,7 +20,6 @@ interface SeleccionaReporteExistenciaPapelProps { const SeleccionaReporteExistenciaPapel: React.FC = ({ onGenerarReporte, - onCancel, isLoading, apiErrorMessage }) => { @@ -142,9 +141,6 @@ const SeleccionaReporteExistenciaPapel: React.FC{localErrors.dropdowns}} - diff --git a/Frontend/src/pages/Reportes/SeleccionaReporteListadoDistribucionCanillas.tsx b/Frontend/src/pages/Reportes/SeleccionaReporteListadoDistribucionCanillas.tsx new file mode 100644 index 0000000..436d3e3 --- /dev/null +++ b/Frontend/src/pages/Reportes/SeleccionaReporteListadoDistribucionCanillas.tsx @@ -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; + onCancel: () => void; + isLoading?: boolean; + apiErrorMessage?: string | null; +} + +const SeleccionaReporteListadoDistribucionCanillas: React.FC = ({ + onGenerarReporte, + isLoading, + apiErrorMessage +}) => { + const [idPublicacion, setIdPublicacion] = useState(''); + const [fechaDesde, setFechaDesde] = useState(new Date().toISOString().split('T')[0]); + const [fechaHasta, setFechaHasta] = useState(new Date().toISOString().split('T')[0]); + + const [publicaciones, setPublicaciones] = useState([]); + 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 ( + + + Parámetros: Listado Distribución Canillitas + + + Publicación + + {localErrors.idPublicacion && {localErrors.idPublicacion}} + + { 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 }} + /> + { 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 && {apiErrorMessage}} + {localErrors.dropdowns && {localErrors.dropdowns}} + + + + + + ); +}; + +export default SeleccionaReporteListadoDistribucionCanillas; \ No newline at end of file diff --git a/Frontend/src/pages/Reportes/SeleccionaReporteListadoDistribucionGeneral.tsx b/Frontend/src/pages/Reportes/SeleccionaReporteListadoDistribucionGeneral.tsx new file mode 100644 index 0000000..1409cd7 --- /dev/null +++ b/Frontend/src/pages/Reportes/SeleccionaReporteListadoDistribucionGeneral.tsx @@ -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; + onCancel: () => void; + isLoading?: boolean; + apiErrorMessage?: string | null; +} + +const SeleccionaReporteListadoDistribucionGeneral: React.FC = ({ + onGenerarReporte, + isLoading, + apiErrorMessage +}) => { + const [idPublicacion, setIdPublicacion] = useState(''); + // Para el selector de mes/año, usamos un input type="month" + const [mesAnio, setMesAnio] = useState(new Date().toISOString().substring(0, 7)); // Formato "YYYY-MM" + + const [publicaciones, setPublicaciones] = useState([]); + 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 ( + + + Parámetros: Listado Distribución General + + + Publicación + + {localErrors.idPublicacion && {localErrors.idPublicacion}} + + + { 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 && {apiErrorMessage}} + {localErrors.dropdowns && {localErrors.dropdowns}} + + + + + + ); +}; + +export default SeleccionaReporteListadoDistribucionGeneral; \ No newline at end of file diff --git a/Frontend/src/pages/Reportes/SeleccionaReporteMovimientoBobinas.tsx b/Frontend/src/pages/Reportes/SeleccionaReporteMovimientoBobinas.tsx new file mode 100644 index 0000000..f298390 --- /dev/null +++ b/Frontend/src/pages/Reportes/SeleccionaReporteMovimientoBobinas.tsx @@ -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; + onCancel: () => void; + isLoading?: boolean; + apiErrorMessage?: string | null; +} + +const SeleccionaReporteMovimientoBobinas: React.FC = ({ + onGenerarReporte, + isLoading, + apiErrorMessage +}) => { + const [fechaDesde, setFechaDesde] = useState(new Date().toISOString().split('T')[0]); + const [fechaHasta, setFechaHasta] = useState(new Date().toISOString().split('T')[0]); + const [idPlanta, setIdPlanta] = useState(''); // Puede ser string inicialmente por el MenuItem vacío + + const [plantas, setPlantas] = useState([]); + 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 ( + + + Parámetros: Movimiento de Bobinas + + { 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 }} + /> + { setFechaHasta(e.target.value); setLocalErrors(p => ({ ...p, fechaHasta: null })); }} + margin="normal" + fullWidth + required + error={!!localErrors.fechaHasta} + helperText={localErrors.fechaHasta} + disabled={isLoading} + InputLabelProps={{ shrink: true }} + /> + + Planta + + {localErrors.idPlanta && {localErrors.idPlanta}} + + + {apiErrorMessage && {apiErrorMessage}} + {localErrors.dropdowns && {localErrors.dropdowns}} + + + + + + ); +}; + +export default SeleccionaReporteMovimientoBobinas; \ No newline at end of file diff --git a/Frontend/src/pages/Reportes/SeleccionaReporteMovimientoBobinasEstado.tsx b/Frontend/src/pages/Reportes/SeleccionaReporteMovimientoBobinasEstado.tsx new file mode 100644 index 0000000..99d62ed --- /dev/null +++ b/Frontend/src/pages/Reportes/SeleccionaReporteMovimientoBobinasEstado.tsx @@ -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; + onCancel: () => void; + isLoading?: boolean; + apiErrorMessage?: string | null; +} + +const SeleccionaReporteMovimientoBobinasEstado: React.FC = ({ + onGenerarReporte, + isLoading, + apiErrorMessage +}) => { + const [fechaDesde, setFechaDesde] = useState(new Date().toISOString().split('T')[0]); + const [fechaHasta, setFechaHasta] = useState(new Date().toISOString().split('T')[0]); + const [idPlanta, setIdPlanta] = useState(''); + + const [plantas, setPlantas] = useState([]); + 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 ( + + + Parámetros: Movimiento de Bobinas por Estado + + { 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 }} + /> + { setFechaHasta(e.target.value); setLocalErrors(p => ({ ...p, fechaHasta: null })); }} + margin="normal" + fullWidth + required + error={!!localErrors.fechaHasta} + helperText={localErrors.fechaHasta} + disabled={isLoading} + InputLabelProps={{ shrink: true }} + /> + + Planta + + {localErrors.idPlanta && {localErrors.idPlanta}} + + + {apiErrorMessage && {apiErrorMessage}} + {localErrors.dropdowns && {localErrors.dropdowns}} + + + + + + ); +}; + +export default SeleccionaReporteMovimientoBobinasEstado; \ No newline at end of file diff --git a/Frontend/src/routes/AppRoutes.tsx b/Frontend/src/routes/AppRoutes.tsx index eda7361..85f063d 100644 --- a/Frontend/src/routes/AppRoutes.tsx +++ b/Frontend/src/routes/AppRoutes.tsx @@ -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 = () => { }> {/* Página principal del módulo */} Seleccione un reporte del menú lateral.} /> {/* Placeholder */} } /> - {/* Aquí se añadirán las rutas para otros reportes */} + } /> + } /> + } /> + } /> {/* Módulo de Radios (anidado) */} diff --git a/Frontend/src/services/Reportes/reportesService.ts b/Frontend/src/services/Reportes/reportesService.ts index 0fa1d48..bd86514 100644 --- a/Frontend/src/services/Reportes/reportesService.ts +++ b/Frontend/src/services/Reportes/reportesService.ts @@ -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 => { + const response = await apiClient.get('/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 => { + 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 => { // <- Devuelve el DTO combinado + const response = await apiClient.get('/reportes/movimiento-bobinas-estado', { params }); + return response.data; +}; + +const getMovimientoBobinasEstadoPdf = async (params: { + fechaDesde: string; + fechaHasta: string; + idPlanta: number; +}): Promise => { + 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 => { + const response = await apiClient.get('/reportes/listado-distribucion-general', { params }); + return response.data; +}; + +const getListadoDistribucionGeneralPdf = async (params: { + idPublicacion: number; + fechaDesde: string; + fechaHasta: string; +}): Promise => { + 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 => { + const response = await apiClient.get('/reportes/listado-distribucion-canillas', { params }); + return response.data; +}; + + +const getListadoDistribucionCanillasPdf = async (params: { + idPublicacion: number; + fechaDesde: string; + fechaHasta: string; +}): Promise => { + 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; \ No newline at end of file