From 975a1e6d260f3c5746b17dcabad0aa85fbe9c807 Mon Sep 17 00:00:00 2001 From: dmolinari Date: Thu, 19 Jun 2025 14:47:43 -0300 Subject: [PATCH] Test Reportes con Razor y Puppeteer --- .../Reportes/ReportesController.cs | 81 ++++++------ .../Templates/ReporteExistenciaPapel.cshtml | 116 ++++++++++++++++++ .../ReporteExistenciaPapelConsolidado.cshtml | 115 +++++++++++++++++ .../Reportes/ReportesRepository.cs | 74 +++++------ Backend/GestionIntegral.Api/Dockerfile | 26 ++-- .../GestionIntegral.Api.csproj | 4 + .../ExistenciaPapelConsolidadoViewModel.cs | 13 ++ .../ViewModels/ExistenciaPapelViewModel.cs | 14 +++ Backend/GestionIntegral.Api/Program.cs | 3 + .../Services/Pdf/IPdfGeneratorService.cs | 35 ++++++ .../Services/Pdf/PuppeteerPdfGenerator.cs | 98 +++++++++++++++ 11 files changed, 493 insertions(+), 86 deletions(-) create mode 100644 Backend/GestionIntegral.Api/Controllers/Reportes/Templates/ReporteExistenciaPapel.cshtml create mode 100644 Backend/GestionIntegral.Api/Controllers/Reportes/Templates/ReporteExistenciaPapelConsolidado.cshtml create mode 100644 Backend/GestionIntegral.Api/Models/Dtos/Reportes/ViewModels/ExistenciaPapelConsolidadoViewModel.cs create mode 100644 Backend/GestionIntegral.Api/Models/Dtos/Reportes/ViewModels/ExistenciaPapelViewModel.cs create mode 100644 Backend/GestionIntegral.Api/Services/Pdf/IPdfGeneratorService.cs create mode 100644 Backend/GestionIntegral.Api/Services/Pdf/PuppeteerPdfGenerator.cs diff --git a/Backend/GestionIntegral.Api/Controllers/Reportes/ReportesController.cs b/Backend/GestionIntegral.Api/Controllers/Reportes/ReportesController.cs index fadd032..cf46328 100644 --- a/Backend/GestionIntegral.Api/Controllers/Reportes/ReportesController.cs +++ b/Backend/GestionIntegral.Api/Controllers/Reportes/ReportesController.cs @@ -12,6 +12,8 @@ using System.IO; using System.Linq; using GestionIntegral.Api.Data.Repositories.Distribucion; using GestionIntegral.Api.Services.Distribucion; +using GestionIntegral.Api.Services.Pdf; +using GestionIntegral.Api.Dtos.Reportes.ViewModels; namespace GestionIntegral.Api.Controllers { @@ -27,7 +29,7 @@ namespace GestionIntegral.Api.Controllers private readonly IEmpresaRepository _empresaRepository; private readonly IDistribuidorRepository _distribuidorRepository; // Para obtener el nombre del distribuidor private readonly INovedadCanillaService _novedadCanillaService; - + private readonly IPdfGeneratorService _pdfGeneratorService; // Permisos private const string PermisoVerReporteExistenciaPapel = "RR005"; @@ -48,7 +50,8 @@ namespace GestionIntegral.Api.Controllers IPlantaRepository plantaRepository, IPublicacionRepository publicacionRepository, IEmpresaRepository empresaRepository, - IDistribuidorRepository distribuidorRepository) + IDistribuidorRepository distribuidorRepository, + IPdfGeneratorService pdfGeneratorService) { _reportesService = reportesService; _novedadCanillaService = novedadCanillaService; @@ -57,6 +60,7 @@ namespace GestionIntegral.Api.Controllers _publicacionRepository = publicacionRepository; _empresaRepository = empresaRepository; _distribuidorRepository = distribuidorRepository; + _pdfGeneratorService = pdfGeneratorService; } private bool TienePermiso(string codAcc) => User.IsInRole("SuperAdmin") || User.HasClaim(c => c.Type == "permission" && c.Value == codAcc); @@ -94,14 +98,14 @@ namespace GestionIntegral.Api.Controllers [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] public async Task GetReporteExistenciaPapelPdf( - [FromQuery] DateTime fechaDesde, - [FromQuery] DateTime fechaHasta, - [FromQuery] int? idPlanta, - [FromQuery] bool consolidado = false) + [FromQuery] DateTime fechaDesde, + [FromQuery] DateTime fechaHasta, + [FromQuery] int? idPlanta, + [FromQuery] bool consolidado = false) { if (!TienePermiso(PermisoVerReporteExistenciaPapel)) return Forbid(); - var (data, error) = await _reportesService.ObtenerExistenciaPapelAsync(fechaDesde, fechaHasta, idPlanta, consolidado); // <--- CORREGIDO + var (data, error) = await _reportesService.ObtenerExistenciaPapelAsync(fechaDesde, fechaHasta, idPlanta, consolidado); if (error != null) { @@ -114,43 +118,50 @@ namespace GestionIntegral.Api.Controllers try { - LocalReport report = new LocalReport(); - string rdlcPath = consolidado ? "Controllers/Reportes/RDLC/ReporteExistenciaPapelConsolidado.rdlc" : "Controllers/Reportes/RDLC/ReporteExistenciaPapel.rdlc"; - - using (var fs = new FileStream(rdlcPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) - { - report.LoadReportDefinition(fs); - } - report.DataSources.Add(new ReportDataSource("DSConsumoBobinas", data)); - - var parameters = new List(); - parameters.Add(new ReportParameter("FechaDesde", fechaDesde.ToString("dd/MM/yyyy"))); - parameters.Add(new ReportParameter("FechaHasta", fechaHasta.ToString("dd/MM/yyyy"))); - + // Determinar la plantilla y el nombre de la planta + string templatePath; string nombrePlantaParam = "Consolidado"; - if (!consolidado && idPlanta.HasValue) + if (consolidado) { - var planta = await _plantaRepository.GetByIdAsync(idPlanta.Value); - nombrePlantaParam = planta?.Nombre ?? "N/A"; - } - else if (consolidado) - { - // Para el consolidado, el RDLC ReporteExistenciaPapelConsolidado.txt NO espera NomPlanta + templatePath = Path.Combine("Controllers", "Reportes", "Templates", "ReporteExistenciaPapelConsolidado.cshtml"); } else - { // No consolidado pero idPlanta es NULL (aunque el servicio ya valida esto) - nombrePlantaParam = "N/A"; - } - // Solo añadir NomPlanta si NO es consolidado, porque el RDLC consolidado no lo tiene. - if (!consolidado) { - parameters.Add(new ReportParameter("NomPlanta", nombrePlantaParam)); + templatePath = Path.Combine("Controllers", "Reportes", "Templates", "ReporteExistenciaPapel.cshtml"); + if (idPlanta.HasValue) + { + var planta = await _plantaRepository.GetByIdAsync(idPlanta.Value); + nombrePlantaParam = planta?.Nombre ?? "N/A"; + } } + // Crear el ViewModel para la plantilla Razor + var viewModel = new ExistenciaPapelViewModel + { + Existencias = data, + NombrePlanta = nombrePlantaParam, + FechaDesde = fechaDesde.ToString("dd/MM/yyyy"), + FechaHasta = fechaHasta.ToString("dd/MM/yyyy") + }; - report.SetParameters(parameters); + // Configurar opciones de PDF (márgenes, etc.) + var pdfOptions = new PdfGenerationOptions + { + Margin = new PuppeteerSharp.Media.MarginOptions + { + Top = "2cm", + Bottom = "2cm", + Left = "1cm", + Right = "1cm" + }, + Format = PuppeteerSharp.Media.PaperFormat.A4, + // Podríamos agregar un encabezado/pie de página aquí si fuera necesario + // FooterTemplate = "
Página de
" + }; + + // Generar el PDF + byte[] pdfBytes = await _pdfGeneratorService.GeneratePdfFromRazorTemplateAsync(templatePath, viewModel, pdfOptions); - byte[] pdfBytes = report.Render("PDF"); string fileName = $"ExistenciaPapel_{fechaDesde:yyyyMMdd}_{fechaHasta:yyyyMMdd}_{(consolidado ? "Consolidado" : $"Planta{idPlanta}")}.pdf"; return File(pdfBytes, "application/pdf", fileName); } diff --git a/Backend/GestionIntegral.Api/Controllers/Reportes/Templates/ReporteExistenciaPapel.cshtml b/Backend/GestionIntegral.Api/Controllers/Reportes/Templates/ReporteExistenciaPapel.cshtml new file mode 100644 index 0000000..530ba11 --- /dev/null +++ b/Backend/GestionIntegral.Api/Controllers/Reportes/Templates/ReporteExistenciaPapel.cshtml @@ -0,0 +1,116 @@ +@using GestionIntegral.Api.Dtos.Reportes.ViewModels + +@model ExistenciaPapelViewModel + + + + + + + + +
+
+

Reporte de Existencias de Papel

+

Planta: @Model.NombrePlanta

+
+ +
+

Fecha del Reporte: @Model.FechaReporte

+

Periodo Consultado: Desde @Model.FechaDesde Hasta @Model.FechaHasta

+
+ + + + + + + + + + + + + + @foreach (var item in Model.Existencias) + { + + + + + + + + + } + + + @{ + var totalBobinas = Model.Existencias.Sum(e => e.BobinasEnStock ?? 0); + var totalKilos = Model.Existencias.Sum(e => e.TotalKilosEnStock ?? 0); + var totalConsumo = Model.Existencias.Sum(e => e.ConsumoAcumulado ?? 0); + } + + + + + + + + + +
Tipo BobinaCant. StockKg. StockConsumo Acumulado (Kg)Días DisponiblesFin Stock Estimado
@item.TipoBobina@(item.BobinasEnStock?.ToString("N0") ?? "0")@(item.TotalKilosEnStock?.ToString("N0") ?? "0")@(item.ConsumoAcumulado?.ToString("N0") ?? "0")@(item.PromedioDiasDisponibles?.ToString("N0") ?? "N/A")@(item.FechaEstimacionFinStock?.ToString("dd/MM/yyyy") ?? "N/A")
Totales@totalBobinas.ToString("N0")@totalKilos.ToString("N0")@totalConsumo.ToString("N0")
+
+ + \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Controllers/Reportes/Templates/ReporteExistenciaPapelConsolidado.cshtml b/Backend/GestionIntegral.Api/Controllers/Reportes/Templates/ReporteExistenciaPapelConsolidado.cshtml new file mode 100644 index 0000000..f707378 --- /dev/null +++ b/Backend/GestionIntegral.Api/Controllers/Reportes/Templates/ReporteExistenciaPapelConsolidado.cshtml @@ -0,0 +1,115 @@ +@using GestionIntegral.Api.Dtos.Reportes.ViewModels + +@model ExistenciaPapelViewModel + + + + + + + + +
+
+

Reporte de Existencias de Papel

+
+ +
+

Fecha del Reporte: @Model.FechaReporte

+

Periodo Consultado: Desde @Model.FechaDesde Hasta @Model.FechaHasta

+
+ + + + + + + + + + + + + + @foreach (var item in Model.Existencias) + { + + + + + + + + + } + + + @{ + var totalBobinas = Model.Existencias.Sum(e => e.BobinasEnStock ?? 0); + var totalKilos = Model.Existencias.Sum(e => e.TotalKilosEnStock ?? 0); + var totalConsumo = Model.Existencias.Sum(e => e.ConsumoAcumulado ?? 0); + } + + + + + + + + + +
Tipo BobinaCant. StockKg. StockConsumo Acumulado (Kg)Días DisponiblesFin Stock Estimado
@item.TipoBobina@(item.BobinasEnStock?.ToString("N0") ?? "0")@(item.TotalKilosEnStock?.ToString("N0") ?? "0")@(item.ConsumoAcumulado?.ToString("N0") ?? "0")@(item.PromedioDiasDisponibles?.ToString("N0") ?? "N/A")@(item.FechaEstimacionFinStock?.ToString("dd/MM/yyyy") ?? "N/A")
Totales@totalBobinas.ToString("N0")@totalKilos.ToString("N0")@totalConsumo.ToString("N0")
+
+ + \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Reportes/ReportesRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Reportes/ReportesRepository.cs index 8792ec9..6faf180 100644 --- a/Backend/GestionIntegral.Api/Data/Repositories/Reportes/ReportesRepository.cs +++ b/Backend/GestionIntegral.Api/Data/Repositories/Reportes/ReportesRepository.cs @@ -44,7 +44,7 @@ namespace GestionIntegral.Api.Data.Repositories.Reportes try { using var connection = _dbConnectionFactory.CreateConnection(); - var result = await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure); + var result = await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure, commandTimeout: 120); _logger.LogInformation("SP {SPName} ejecutado, {Count} filas devueltas.", spName, result.Count()); return result; } @@ -68,7 +68,7 @@ namespace GestionIntegral.Api.Data.Repositories.Reportes try { using var connection = _dbConnectionFactory.CreateConnection(); - return await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure); + return await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure, commandTimeout: 120); } catch (Exception ex) { @@ -84,7 +84,7 @@ namespace GestionIntegral.Api.Data.Repositories.Reportes return await conn.QueryAsync( "SP_DistCanillasCantidadEntradaSalidaOtrosDias", parametros, - commandType: CommandType.StoredProcedure + commandType: CommandType.StoredProcedure, commandTimeout: 120 ); } @@ -101,7 +101,7 @@ namespace GestionIntegral.Api.Data.Repositories.Reportes try { using var connection = _dbConnectionFactory.CreateConnection(); - return await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure); + return await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure, commandTimeout: 120); } catch (Exception ex) { @@ -123,7 +123,7 @@ namespace GestionIntegral.Api.Data.Repositories.Reportes try { using var connection = _dbConnectionFactory.CreateConnection(); - return await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure); + return await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure, commandTimeout: 120); } catch (Exception ex) { @@ -140,7 +140,7 @@ namespace GestionIntegral.Api.Data.Repositories.Reportes parameters.Add("@Mes", fechaDesde.Month, DbType.Int32); parameters.Add("@Anio", fechaDesde.Year, DbType.Int32); // El SP no usa fechaHasta explícitamente, calcula el fin de mes internamente - try { using var connection = _dbConnectionFactory.CreateConnection(); return await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure); } + try { using var connection = _dbConnectionFactory.CreateConnection(); return await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure, commandTimeout: 120); } catch (Exception ex) { _logger.LogError(ex, "Error SP {SPName}", spName); return Enumerable.Empty(); } } @@ -151,7 +151,7 @@ namespace GestionIntegral.Api.Data.Repositories.Reportes parameters.Add("@Id_Publicacion", idPublicacion, DbType.Int32); parameters.Add("@Mes", fechaDesde.Month, DbType.Int32); parameters.Add("@Anio", fechaDesde.Year, DbType.Int32); - try { using var connection = _dbConnectionFactory.CreateConnection(); return await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure); } + try { using var connection = _dbConnectionFactory.CreateConnection(); return await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure, commandTimeout: 120); } catch (Exception ex) { _logger.LogError(ex, "Error SP {SPName}", spName); return Enumerable.Empty(); } } @@ -162,7 +162,7 @@ namespace GestionIntegral.Api.Data.Repositories.Reportes parameters.Add("@idPublicacion", idPublicacion, DbType.Int32); parameters.Add("@fechaDesde", fechaDesde, DbType.DateTime); parameters.Add("@fechaHasta", fechaHasta, DbType.DateTime); - try { using var connection = _dbConnectionFactory.CreateConnection(); return await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure); } + try { using var connection = _dbConnectionFactory.CreateConnection(); return await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure, commandTimeout: 120); } catch (Exception ex) { _logger.LogError(ex, "Error SP {SPName}", spName); return Enumerable.Empty(); } } @@ -173,7 +173,7 @@ namespace GestionIntegral.Api.Data.Repositories.Reportes parameters.Add("@idPublicacion", idPublicacion, DbType.Int32); parameters.Add("@fechaDesde", fechaDesde, DbType.DateTime); parameters.Add("@fechaHasta", fechaHasta, DbType.DateTime); - try { using var connection = _dbConnectionFactory.CreateConnection(); return await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure); } + try { using var connection = _dbConnectionFactory.CreateConnection(); return await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure, commandTimeout: 120); } catch (Exception ex) { _logger.LogError(ex, "Error SP {SPName}", spName); return Enumerable.Empty(); } } @@ -185,7 +185,7 @@ namespace GestionIntegral.Api.Data.Repositories.Reportes parameters.Add("@fechaDesde", fechaDesde, DbType.DateTime); parameters.Add("@fechaHasta", fechaHasta, DbType.DateTime); parameters.Add("@accionista", esAccionista, DbType.Boolean); - try { using var connection = _dbConnectionFactory.CreateConnection(); return await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure); } + try { using var connection = _dbConnectionFactory.CreateConnection(); return await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure, commandTimeout: 120); } catch (Exception ex) { _logger.LogError(ex, "Error SP {SPName}", spName); return Enumerable.Empty(); } } @@ -195,7 +195,7 @@ namespace GestionIntegral.Api.Data.Repositories.Reportes var parameters = new DynamicParameters(); parameters.Add("@fechaDesde", fechaDesde, DbType.DateTime); parameters.Add("@fechaHasta", fechaHasta, DbType.DateTime); - try { using var connection = _dbConnectionFactory.CreateConnection(); return await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure); } + try { using var connection = _dbConnectionFactory.CreateConnection(); return await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure, commandTimeout: 120); } catch (Exception ex) { _logger.LogError(ex, "Error SP {SPName}", spName); return Enumerable.Empty(); } } @@ -205,7 +205,7 @@ namespace GestionIntegral.Api.Data.Repositories.Reportes var parameters = new DynamicParameters(); parameters.Add("@fechaDesde", fechaDesde, DbType.DateTime); parameters.Add("@fechaHasta", fechaHasta, DbType.DateTime); - try { using var connection = _dbConnectionFactory.CreateConnection(); return await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure); } + try { using var connection = _dbConnectionFactory.CreateConnection(); return await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure, commandTimeout: 120); } catch (Exception ex) { _logger.LogError(ex, "Error SP {SPName}", spName); return Enumerable.Empty(); } } @@ -215,7 +215,7 @@ namespace GestionIntegral.Api.Data.Repositories.Reportes var parameters = new DynamicParameters(); parameters.Add("@fechaDesde", fechaDesde, DbType.DateTime); parameters.Add("@fechaHasta", fechaHasta, DbType.DateTime); - try { using var connection = _dbConnectionFactory.CreateConnection(); return await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure); } + try { using var connection = _dbConnectionFactory.CreateConnection(); return await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure, commandTimeout: 120); } catch (Exception ex) { _logger.LogError(ex, "Error SP {SPName}", spName); return Enumerable.Empty(); } } @@ -225,7 +225,7 @@ namespace GestionIntegral.Api.Data.Repositories.Reportes var parameters = new DynamicParameters(); parameters.Add("@fecha", fecha, DbType.DateTime); parameters.Add("@idEmpresa", idEmpresa, DbType.Int32); - try { using var connection = _dbConnectionFactory.CreateConnection(); return await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure); } + try { using var connection = _dbConnectionFactory.CreateConnection(); return await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure, commandTimeout: 120); } catch (Exception ex) { _logger.LogError(ex, "Error SP {SPName}", spName); return Enumerable.Empty(); } } @@ -235,7 +235,7 @@ namespace GestionIntegral.Api.Data.Repositories.Reportes var parameters = new DynamicParameters(); parameters.Add("@fecha", fecha, DbType.DateTime); parameters.Add("@idEmpresa", idEmpresa, DbType.Int32); - try { using var connection = _dbConnectionFactory.CreateConnection(); return await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure); } + try { using var connection = _dbConnectionFactory.CreateConnection(); return await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure, commandTimeout: 120); } catch (Exception ex) { _logger.LogError(ex, "Error SP {SPName}", spName); return Enumerable.Empty(); } } @@ -245,7 +245,7 @@ namespace GestionIntegral.Api.Data.Repositories.Reportes var parameters = new DynamicParameters(); parameters.Add("@fecha", fecha, DbType.DateTime); parameters.Add("@idEmpresa", idEmpresa, DbType.Int32); - try { using var connection = _dbConnectionFactory.CreateConnection(); return await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure); } + try { using var connection = _dbConnectionFactory.CreateConnection(); return await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure, commandTimeout: 120); } catch (Exception ex) { _logger.LogError(ex, "Error SP {SPName}", spName); return Enumerable.Empty(); } } @@ -255,7 +255,7 @@ namespace GestionIntegral.Api.Data.Repositories.Reportes var parameters = new DynamicParameters(); parameters.Add("@fecha", fechaLiquidacion, DbType.DateTime); parameters.Add("@idEmpresa", idEmpresa, DbType.Int32); - try { using var connection = _dbConnectionFactory.CreateConnection(); return await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure); } + try { using var connection = _dbConnectionFactory.CreateConnection(); return await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure, commandTimeout: 120); } catch (Exception ex) { _logger.LogError(ex, "Error SP {SPName}", spName); return Enumerable.Empty(); } } @@ -265,7 +265,7 @@ namespace GestionIntegral.Api.Data.Repositories.Reportes var parameters = new DynamicParameters(); parameters.Add("@fecha", fechaLiquidacion, DbType.DateTime); parameters.Add("@idEmpresa", idEmpresa, DbType.Int32); - try { using var connection = _dbConnectionFactory.CreateConnection(); return await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure); } + try { using var connection = _dbConnectionFactory.CreateConnection(); return await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure, commandTimeout: 120); } catch (Exception ex) { _logger.LogError(ex, "Error SP {SPName}", spName); return Enumerable.Empty(); } } @@ -275,7 +275,7 @@ namespace GestionIntegral.Api.Data.Repositories.Reportes var parameters = new DynamicParameters(); parameters.Add("@Fecha", fecha, DbType.DateTime); parameters.Add("@IdEmpresa", idEmpresa, DbType.Int32); - try { using var connection = _dbConnectionFactory.CreateConnection(); return await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure); } + try { using var connection = _dbConnectionFactory.CreateConnection(); return await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure, commandTimeout: 120); } catch (Exception ex) { _logger.LogError(ex, "Error SP {SPName}", spName); return Enumerable.Empty(); } } @@ -285,7 +285,7 @@ namespace GestionIntegral.Api.Data.Repositories.Reportes var parameters = new DynamicParameters(); parameters.Add("@fecha", fecha, DbType.DateTime); parameters.Add("@idEmpresa", idEmpresa, DbType.Int32); - try { using var connection = _dbConnectionFactory.CreateConnection(); return await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure); } + try { using var connection = _dbConnectionFactory.CreateConnection(); return await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure, commandTimeout: 120); } catch (Exception ex) { _logger.LogError(ex, "Error SP {SPName}", spName); return Enumerable.Empty(); } } @@ -297,7 +297,7 @@ namespace GestionIntegral.Api.Data.Repositories.Reportes parameters.Add("@FechaInicio", fechaDesde, DbType.Date); parameters.Add("@FechaFin", fechaHasta, DbType.Date); parameters.Add("@IdPlanta", idPlanta, DbType.Int32); - try { using var connection = _dbConnectionFactory.CreateConnection(); return await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure); } + try { using var connection = _dbConnectionFactory.CreateConnection(); return await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure, commandTimeout: 120); } catch (Exception ex) { _logger.LogError(ex, "Error SP {SPName}", spName); return Enumerable.Empty(); } } @@ -308,7 +308,7 @@ namespace GestionIntegral.Api.Data.Repositories.Reportes parameters.Add("@IdPublicacion", idPublicacion, DbType.Int32); parameters.Add("@FechaInicio", fechaDesde, DbType.Date); parameters.Add("@FechaFin", fechaHasta, DbType.Date); - try { using var connection = _dbConnectionFactory.CreateConnection(); return await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure); } + try { using var connection = _dbConnectionFactory.CreateConnection(); return await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure, commandTimeout: 120); } catch (Exception ex) { _logger.LogError(ex, "Error SP {SPName}", spName); return Enumerable.Empty(); } } @@ -319,7 +319,7 @@ namespace GestionIntegral.Api.Data.Repositories.Reportes parameters.Add("@FechaInicio", fechaDesde, DbType.DateTime2); parameters.Add("@FechaFin", fechaHasta, DbType.DateTime2); parameters.Add("@idPlanta", idPlanta, DbType.Int32); - try { using var connection = _dbConnectionFactory.CreateConnection(); return await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure); } + try { using var connection = _dbConnectionFactory.CreateConnection(); return await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure, commandTimeout: 120); } catch (Exception ex) { _logger.LogError(ex, "Error SP {SPName}", spName); return Enumerable.Empty(); } } @@ -329,7 +329,7 @@ namespace GestionIntegral.Api.Data.Repositories.Reportes var parameters = new DynamicParameters(); parameters.Add("@FechaInicio", fechaDesde, DbType.DateTime2); parameters.Add("@FechaFin", fechaHasta, DbType.DateTime2); - try { using var connection = _dbConnectionFactory.CreateConnection(); return await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure); } + try { using var connection = _dbConnectionFactory.CreateConnection(); return await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure, commandTimeout: 120); } catch (Exception ex) { _logger.LogError(ex, "Error SP {SPName}", spName); return Enumerable.Empty(); } } @@ -339,7 +339,7 @@ namespace GestionIntegral.Api.Data.Repositories.Reportes var parameters = new DynamicParameters(); parameters.Add("@FechaInicio", fechaDesde, DbType.DateTime2); parameters.Add("@FechaFin", fechaHasta, DbType.DateTime2); - try { using var connection = _dbConnectionFactory.CreateConnection(); return await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure); } + try { using var connection = _dbConnectionFactory.CreateConnection(); return await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure, commandTimeout: 120); } catch (Exception ex) { _logger.LogError(ex, "Error SP {SPName}", spName); return Enumerable.Empty(); } } @@ -352,7 +352,7 @@ namespace GestionIntegral.Api.Data.Repositories.Reportes parameters.Add("@FechaInicioMesB", fechaInicioMesB, DbType.Date); parameters.Add("@FechaFinMesB", fechaFinMesB, DbType.Date); parameters.Add("@IdPlanta", idPlanta, DbType.Int32); - try { using var connection = _dbConnectionFactory.CreateConnection(); return await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure); } + try { using var connection = _dbConnectionFactory.CreateConnection(); return await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure, commandTimeout: 120); } catch (Exception ex) { _logger.LogError(ex, "Error SP {SPName}", spName); return Enumerable.Empty(); } } @@ -364,7 +364,7 @@ namespace GestionIntegral.Api.Data.Repositories.Reportes parameters.Add("@FechaFinMesA", fechaFinMesA, DbType.Date); parameters.Add("@FechaInicioMesB", fechaInicioMesB, DbType.Date); parameters.Add("@FechaFinMesB", fechaFinMesB, DbType.Date); - try { using var connection = _dbConnectionFactory.CreateConnection(); return await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure); } + try { using var connection = _dbConnectionFactory.CreateConnection(); return await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure, commandTimeout: 120); } catch (Exception ex) { _logger.LogError(ex, "Error SP {SPName}", spName); return Enumerable.Empty(); } } @@ -377,7 +377,7 @@ namespace GestionIntegral.Api.Data.Repositories.Reportes parameters.Add("@idEmpresa", idEmpresa, DbType.Int32); parameters.Add("@fechaDesde", fechaDesde, DbType.DateTime); parameters.Add("@fechaHasta", fechaHasta, DbType.DateTime); - try { using var connection = _dbConnectionFactory.CreateConnection(); return await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure); } + try { using var connection = _dbConnectionFactory.CreateConnection(); return await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure, commandTimeout: 120); } catch (Exception ex) { _logger.LogError(ex, "Error SP {SPName}", spName); return Enumerable.Empty(); } } @@ -390,7 +390,7 @@ namespace GestionIntegral.Api.Data.Repositories.Reportes parameters.Add("@idEmpresa", idEmpresa, DbType.Int32); parameters.Add("@fechaDesde", fechaDesde, DbType.DateTime); parameters.Add("@fechaHasta", fechaHasta, DbType.DateTime); - try { using var connection = _dbConnectionFactory.CreateConnection(); return await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure); } + try { using var connection = _dbConnectionFactory.CreateConnection(); return await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure, commandTimeout: 120); } catch (Exception ex) { _logger.LogError(ex, "Error SP {SPName}", spName); return Enumerable.Empty(); } } @@ -403,7 +403,7 @@ namespace GestionIntegral.Api.Data.Repositories.Reportes parameters.Add("@idEmpresa", idEmpresa, DbType.Int32); parameters.Add("@fechaDesde", fechaDesde, DbType.DateTime); parameters.Add("@fechaHasta", fechaHasta, DbType.DateTime); - try { using var connection = _dbConnectionFactory.CreateConnection(); return await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure); } + try { using var connection = _dbConnectionFactory.CreateConnection(); return await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure, commandTimeout: 120); } catch (Exception ex) { _logger.LogError(ex, "Error SP {SPName}", spName); return Enumerable.Empty(); } } @@ -415,7 +415,7 @@ namespace GestionIntegral.Api.Data.Repositories.Reportes parameters.Add("@Destino", destino, DbType.String); parameters.Add("@idDestino", idDestino, DbType.Int32); parameters.Add("@idEmpresa", idEmpresa, DbType.Int32); - try { using var connection = _dbConnectionFactory.CreateConnection(); return await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure); } + try { using var connection = _dbConnectionFactory.CreateConnection(); return await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure, commandTimeout: 120); } catch (Exception ex) { _logger.LogError(ex, "Error SP {SPName}", spName); return Enumerable.Empty(); } } @@ -430,7 +430,7 @@ namespace GestionIntegral.Api.Data.Repositories.Reportes try { using var connection = _dbConnectionFactory.CreateConnection(); // <--- CORREGIDO AQUÍ - return await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure); + return await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure, commandTimeout: 120); } catch (Exception ex) { @@ -450,7 +450,7 @@ namespace GestionIntegral.Api.Data.Repositories.Reportes try { using var connection = _dbConnectionFactory.CreateConnection(); // <--- CORREGIDO AQUÍ - return await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure); + return await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure, commandTimeout: 120); } catch (Exception ex) { @@ -489,7 +489,7 @@ namespace GestionIntegral.Api.Data.Repositories.Reportes try { using var connection = _dbConnectionFactory.CreateConnection(); - return await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure); + return await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure, commandTimeout: 120); } catch (Exception ex) { @@ -507,7 +507,7 @@ namespace GestionIntegral.Api.Data.Repositories.Reportes try { using var connection = _dbConnectionFactory.CreateConnection(); - return await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure); + return await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure, commandTimeout: 120); } catch (Exception ex) { @@ -528,7 +528,7 @@ namespace GestionIntegral.Api.Data.Repositories.Reportes return await connection.QueryAsync( "dbo.SP_DistCanillasAccConImporteEntreFechasDiarios", parameters, - commandType: CommandType.StoredProcedure + commandType: CommandType.StoredProcedure, commandTimeout: 120 ); } @@ -544,7 +544,7 @@ namespace GestionIntegral.Api.Data.Repositories.Reportes return await connection.QueryAsync( "dbo.SP_DistCanillasAccConImporteEntreFechas", parameters, - commandType: CommandType.StoredProcedure + commandType: CommandType.StoredProcedure, commandTimeout: 120 ); } } diff --git a/Backend/GestionIntegral.Api/Dockerfile b/Backend/GestionIntegral.Api/Dockerfile index b573593..29f931c 100644 --- a/Backend/GestionIntegral.Api/Dockerfile +++ b/Backend/GestionIntegral.Api/Dockerfile @@ -27,22 +27,20 @@ RUN dotnet publish "GestionIntegral.Api.csproj" -c Release -o /app/publish /p:Us # Usamos la imagen de runtime de ASP.NET, que es mucho más ligera que el SDK. FROM mcr.microsoft.com/dotnet/aspnet:9.0 WORKDIR /app - -# Instalamos libgdiplus + sus deps mínimas y creamos el symlink que .NET espera. -RUN apt-get update \ - && apt-get install -y --no-install-recommends \ - libgdiplus \ - libc6-dev \ - libx11-dev \ - libxrender1 \ - libxtst6 \ - libxi6 \ - fontconfig \ - && ln -s /usr/lib/libgdiplus.so /usr/lib/gdiplus.dll \ - && rm -rf /var/lib/apt/lists/* - COPY --from=publish /app/publish . +# Instalar dependencias de Chromium en la imagen de ASP.NET (basada en Debian) +RUN apt-get update && apt-get install -y \ + libgconf-2-4 \ + libgdk-pixbuf2.0-0 \ + libgtk-3-0 \ + libnss3 \ + libxss1 \ + libasound2 \ + libxtst6 \ + --no-install-recommends && \ + rm -rf /var/lib/apt/lists/* + # El puerto en el que la API escuchará DENTRO del contenedor. # Usaremos 8080 para evitar conflictos si en el futuro corres algo en el puerto 80. EXPOSE 8080 diff --git a/Backend/GestionIntegral.Api/GestionIntegral.Api.csproj b/Backend/GestionIntegral.Api/GestionIntegral.Api.csproj index e262496..1cc4e47 100644 --- a/Backend/GestionIntegral.Api/GestionIntegral.Api.csproj +++ b/Backend/GestionIntegral.Api/GestionIntegral.Api.csproj @@ -4,14 +4,18 @@ net9.0 enable enable + true + + + diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Reportes/ViewModels/ExistenciaPapelConsolidadoViewModel.cs b/Backend/GestionIntegral.Api/Models/Dtos/Reportes/ViewModels/ExistenciaPapelConsolidadoViewModel.cs new file mode 100644 index 0000000..bbef499 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Reportes/ViewModels/ExistenciaPapelConsolidadoViewModel.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; + +namespace GestionIntegral.Api.Dtos.Reportes.ViewModels +{ + public class ExistenciaPapelConsolidadoViewModel + { + public IEnumerable Existencias { get; set; } = new List(); + public string FechaDesde { get; set; } = string.Empty; + public string FechaHasta { get; set; } = string.Empty; + public string FechaReporte { get; set; } = DateTime.Now.ToString("dd/MM/yyyy"); + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Reportes/ViewModels/ExistenciaPapelViewModel.cs b/Backend/GestionIntegral.Api/Models/Dtos/Reportes/ViewModels/ExistenciaPapelViewModel.cs new file mode 100644 index 0000000..e4cfb58 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Reportes/ViewModels/ExistenciaPapelViewModel.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; + +namespace GestionIntegral.Api.Dtos.Reportes.ViewModels +{ + public class ExistenciaPapelViewModel + { + public IEnumerable Existencias { get; set; } = new List(); + public string NombrePlanta { get; set; } = string.Empty; + public string FechaDesde { get; set; } = string.Empty; + public string FechaHasta { get; set; } = string.Empty; + public string FechaReporte { get; set; } = DateTime.Now.ToString("dd/MM/yyyy"); + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Program.cs b/Backend/GestionIntegral.Api/Program.cs index 201134e..140d25f 100644 --- a/Backend/GestionIntegral.Api/Program.cs +++ b/Backend/GestionIntegral.Api/Program.cs @@ -15,6 +15,7 @@ using GestionIntegral.Api.Data.Repositories.Usuarios; using Microsoft.OpenApi.Models; using GestionIntegral.Api.Data.Repositories.Reportes; using GestionIntegral.Api.Services.Reportes; +using GestionIntegral.Api.Services.Pdf; var builder = WebApplication.CreateBuilder(args); @@ -92,6 +93,8 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); // Servicios de Reportes builder.Services.AddScoped(); +// Servicio de PDF +builder.Services.AddScoped(); // --- Configuración de Autenticación JWT --- var jwtSettings = builder.Configuration.GetSection("Jwt"); diff --git a/Backend/GestionIntegral.Api/Services/Pdf/IPdfGeneratorService.cs b/Backend/GestionIntegral.Api/Services/Pdf/IPdfGeneratorService.cs new file mode 100644 index 0000000..47fbe58 --- /dev/null +++ b/Backend/GestionIntegral.Api/Services/Pdf/IPdfGeneratorService.cs @@ -0,0 +1,35 @@ +using Microsoft.AspNetCore.Mvc; +using System.Threading.Tasks; +using PuppeteerSharp.Media; + +namespace GestionIntegral.Api.Services.Pdf +{ + /// + /// Define las opciones de configuración para la generación de un PDF. + /// + public class PdfGenerationOptions + { + public PaperFormat? Format { get; set; } = PaperFormat.A4; + public MarginOptions? Margin { get; set; } + public string? HeaderTemplate { get; set; } + public string? FooterTemplate { get; set; } + public bool PrintBackground { get; set; } = true; + public bool Landscape { get; set; } = false; + } + + /// + /// Servicio para generar documentos PDF a partir de plantillas Razor. + /// + public interface IPdfGeneratorService + { + /// + /// Genera un archivo PDF a partir de una plantilla Razor y un modelo de datos. + /// + /// El tipo del modelo de datos. + /// La ruta relativa de la plantilla Razor (ej: "Controllers/Reportes/Templates/MiReporte.cshtml"). + /// El objeto con los datos para rellenar la plantilla. + /// Opciones de configuración para el PDF (márgenes, formato, etc.). + /// Un array de bytes representando el archivo PDF generado. + Task GeneratePdfFromRazorTemplateAsync(string templatePath, T model, PdfGenerationOptions? options = null); + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Services/Pdf/PuppeteerPdfGenerator.cs b/Backend/GestionIntegral.Api/Services/Pdf/PuppeteerPdfGenerator.cs new file mode 100644 index 0000000..5e789b3 --- /dev/null +++ b/Backend/GestionIntegral.Api/Services/Pdf/PuppeteerPdfGenerator.cs @@ -0,0 +1,98 @@ +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using PuppeteerSharp; +using PuppeteerSharp.Media; +using RazorLight; +using System; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Services.Pdf +{ + public class PuppeteerPdfGenerator : IPdfGeneratorService + { + private readonly IRazorLightEngine _razorEngine; + private readonly ILogger _logger; + + public PuppeteerPdfGenerator(IHostEnvironment hostEnvironment, ILogger logger) + { + _logger = logger; + var rootPath = hostEnvironment.ContentRootPath; + _razorEngine = new RazorLightEngineBuilder() + .UseFileSystemProject(rootPath) + .UseMemoryCachingProvider() + .Build(); + + _logger.LogInformation("Verificando caché de Chromium…"); + new BrowserFetcher().DownloadAsync().Wait(); + _logger.LogInformation("Chromium listo en caché."); + } + + public async Task GeneratePdfFromRazorTemplateAsync(string templatePath, T model, PdfGenerationOptions? options = null) + { + if (string.IsNullOrEmpty(templatePath)) + throw new ArgumentNullException(nameof(templatePath), "La ruta de la plantilla no puede ser nula o vacía."); + + if (model == null) + throw new ArgumentNullException(nameof(model), "El modelo de datos no puede ser nulo."); + + options ??= new PdfGenerationOptions(); + + string htmlContent; + try + { + htmlContent = await _razorEngine.CompileRenderAsync(templatePath, model); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al compilar la plantilla Razor: {TemplatePath}", templatePath); + throw new InvalidOperationException($"No se pudo renderizar la plantilla Razor '{templatePath}'.", ex); + } + + IBrowser? browser = null; + try + { + var launchOptions = new LaunchOptions + { + Headless = true, + Args = new[] { "--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage" } + }; + + _logger.LogInformation("Lanzando Chromium headless…"); + browser = await Puppeteer.LaunchAsync(launchOptions); + await using var page = await browser.NewPageAsync(); + + _logger.LogInformation("Estableciendo contenido HTML en la página."); + await page.SetContentAsync(htmlContent, new NavigationOptions { WaitUntil = new[] { WaitUntilNavigation.Networkidle0 } }); + + _logger.LogInformation("Generando PDF…"); + var pdfOptions = new PdfOptions + { + Format = options.Format, + HeaderTemplate = options.HeaderTemplate, + FooterTemplate = options.FooterTemplate, + PrintBackground = options.PrintBackground, + Landscape = options.Landscape, + MarginOptions = options.Margin ?? new MarginOptions() + }; + + var pdfBytes = await page.PdfDataAsync(pdfOptions); + _logger.LogInformation("PDF generado exitosamente ({Length} bytes).", pdfBytes.Length); + + return pdfBytes; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error durante la generación del PDF con PuppeteerSharp."); + throw; + } + finally + { + if (browser is not null && !browser.IsClosed) + { + await browser.CloseAsync(); + _logger.LogInformation("Navegador cerrado."); + } + } + } + } +} \ No newline at end of file