diff --git a/Backend/GestionIntegral.Api/Controllers/Reportes/PdfTemplates/ExistenciaPapelConsolidadoDocument.cs b/Backend/GestionIntegral.Api/Controllers/Reportes/PdfTemplates/ExistenciaPapelConsolidadoDocument.cs new file mode 100644 index 0000000..34e9c9a --- /dev/null +++ b/Backend/GestionIntegral.Api/Controllers/Reportes/PdfTemplates/ExistenciaPapelConsolidadoDocument.cs @@ -0,0 +1,114 @@ +using GestionIntegral.Api.Dtos.Reportes.ViewModels; +using QuestPDF.Fluent; +using QuestPDF.Helpers; +using QuestPDF.Infrastructure; +using System.Linq; + +namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates +{ + public class ExistenciaPapelConsolidadoDocument : IDocument + { + public ExistenciaPapelConsolidadoViewModel Model { get; } + + public ExistenciaPapelConsolidadoDocument(ExistenciaPapelConsolidadoViewModel model) + { + Model = model; + } + + public DocumentMetadata GetMetadata() => DocumentMetadata.Default; + public DocumentSettings GetSettings() => DocumentSettings.Default; + + public void Compose(IDocumentContainer container) + { + container + .Page(page => + { + page.Margin(1.5f, Unit.Centimetre); + page.PageColor(Colors.White); + page.DefaultTextStyle(x => x.FontFamily("Roboto").FontSize(10)); + + page.Header().Element(ComposeHeader); + page.Content().Element(ComposeContent); + + page.Footer() + .AlignCenter() + .Text(x => + { + x.Span("Página "); + x.CurrentPageNumber(); + }); + }); + } + + void ComposeHeader(IContainer container) + { + container.Column(column => + { + column.Spacing(5); + + column.Item().AlignCenter().Text("Reporte de Existencias de Papel").SemiBold().FontSize(14); + // La versión consolidada no muestra la planta, sino el texto "Consolidado" + column.Item().AlignCenter().Text("Consolidado").FontSize(12); + + column.Item().PaddingTop(2, Unit.Millimetre).Row(row => + { + row.RelativeItem().Column(col => + { + col.Item().Text(text => + { + text.Span("Fecha del Reporte: ").SemiBold(); + text.Span(Model.FechaReporte); + }); + col.Item().Text($"Periodo Consultado: Desde {Model.FechaDesde} Hasta {Model.FechaHasta}"); + }); + }); + }); + } + + void ComposeContent(IContainer container) + { + container.PaddingTop(1, Unit.Centimetre).Table(table => + { + table.ColumnsDefinition(columns => + { + columns.RelativeColumn(); + columns.ConstantColumn(65); + columns.ConstantColumn(70); + columns.ConstantColumn(85); + columns.ConstantColumn(65); + columns.ConstantColumn(80); + }); + + table.Header(header => + { + header.Cell().Background(Colors.Grey.Lighten3).Padding(4).Text("Tipo de Bobina"); + header.Cell().Background(Colors.Grey.Lighten3).Padding(4).AlignRight().Text("Cant. Stock"); + header.Cell().Background(Colors.Grey.Lighten3).Padding(4).AlignRight().Text("Kg. Stock"); + header.Cell().Background(Colors.Grey.Lighten3).Padding(4).AlignRight().Text("Consumo (Kg)"); + header.Cell().Background(Colors.Grey.Lighten3).Padding(4).AlignRight().Text("Días Disp."); + header.Cell().Background(Colors.Grey.Lighten3).Padding(4).AlignCenter().Text("Fin Estimado"); + }); + + foreach (var item in Model.Existencias) + { + table.Cell().BorderBottom(1).BorderColor(Colors.Grey.Lighten2).Padding(4).Text(item.TipoBobina); + table.Cell().BorderBottom(1).BorderColor(Colors.Grey.Lighten2).Padding(4).AlignRight().Text(item.BobinasEnStock?.ToString("N0") ?? "0"); + table.Cell().BorderBottom(1).BorderColor(Colors.Grey.Lighten2).Padding(4).AlignRight().Text(item.TotalKilosEnStock?.ToString("N0") ?? "0"); + table.Cell().BorderBottom(1).BorderColor(Colors.Grey.Lighten2).Padding(4).AlignRight().Text(item.ConsumoAcumulado?.ToString("N0") ?? "0"); + table.Cell().BorderBottom(1).BorderColor(Colors.Grey.Lighten2).Padding(4).AlignRight().Text(item.PromedioDiasDisponibles?.ToString("N0") ?? "N/A"); + table.Cell().BorderBottom(1).BorderColor(Colors.Grey.Lighten2).Padding(4).AlignCenter().Text(item.FechaEstimacionFinStock?.ToString("dd/MM/yyyy") ?? "N/A"); + } + + 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); + + table.Cell().BorderTop(1).BorderColor(Colors.Grey.Lighten2).Padding(4).AlignRight().Text(t => t.Span("Totales").SemiBold()); + table.Cell().BorderTop(1).BorderColor(Colors.Grey.Lighten2).Padding(4).AlignRight().Text(t => t.Span(totalBobinas.ToString("N0")).SemiBold()); + table.Cell().BorderTop(1).BorderColor(Colors.Grey.Lighten2).Padding(4).AlignRight().Text(t => t.Span(totalKilos.ToString("N0")).SemiBold()); + table.Cell().BorderTop(1).BorderColor(Colors.Grey.Lighten2).Padding(4).AlignRight().Text(t => t.Span(totalConsumo.ToString("N0")).SemiBold()); + table.Cell().ColumnSpan(2).BorderTop(1).BorderColor(Colors.Grey.Lighten2); + }); + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Controllers/Reportes/PdfTemplates/ExistenciaPapelDocument.cs b/Backend/GestionIntegral.Api/Controllers/Reportes/PdfTemplates/ExistenciaPapelDocument.cs new file mode 100644 index 0000000..36e5f6e --- /dev/null +++ b/Backend/GestionIntegral.Api/Controllers/Reportes/PdfTemplates/ExistenciaPapelDocument.cs @@ -0,0 +1,117 @@ +using GestionIntegral.Api.Dtos.Reportes.ViewModels; +using QuestPDF.Fluent; +using QuestPDF.Helpers; +using QuestPDF.Infrastructure; +using System.Linq; + +namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates +{ + // Usamos una clase parcial para organizar mejor el código, aunque no es estrictamente necesario aquí. + public class ExistenciaPapelDocument : IDocument + { + // Usamos una propiedad pública con 'init' en lugar de un campo privado. + // Esto es más limpio y funciona mejor con el análisis de código. + public ExistenciaPapelViewModel Model { get; } + + public ExistenciaPapelDocument(ExistenciaPapelViewModel model) + { + Model = model; + } + + public DocumentMetadata GetMetadata() => DocumentMetadata.Default; + public DocumentSettings GetSettings() => DocumentSettings.Default; + + public void Compose(IDocumentContainer container) + { + container + .Page(page => + { + page.Margin(1.5f, Unit.Centimetre); + page.PageColor(Colors.White); + page.DefaultTextStyle(x => x.FontFamily("Roboto").FontSize(10)); + + page.Header().Element(ComposeHeader); + page.Content().Element(ComposeContent); + + page.Footer() + .AlignCenter() + .Text(x => + { + x.Span("Página "); + x.CurrentPageNumber(); + }); + }); + } + + // Ahora los métodos usan 'Model' (la propiedad pública) en lugar de '_model'. + void ComposeHeader(IContainer container) + { + container.Column(column => + { + column.Spacing(5); + + column.Item().AlignCenter().Text("Reporte de Existencias de Papel").SemiBold().FontSize(14); + column.Item().AlignCenter().Text($"Planta: {Model.NombrePlanta}").FontSize(12); + + column.Item().PaddingTop(2, Unit.Millimetre).Row(row => + { + row.RelativeItem().Column(col => + { + col.Item().Text(text => + { + text.Span("Fecha del Reporte: ").SemiBold(); + text.Span(Model.FechaReporte); + }); + col.Item().Text($"Periodo Consultado: Desde {Model.FechaDesde} Hasta {Model.FechaHasta}"); + }); + }); + }); + } + + void ComposeContent(IContainer container) + { + container.PaddingTop(1, Unit.Centimetre).Table(table => + { + table.ColumnsDefinition(columns => + { + columns.RelativeColumn(); + columns.ConstantColumn(65); + columns.ConstantColumn(70); + columns.ConstantColumn(85); + columns.ConstantColumn(65); + columns.ConstantColumn(80); + }); + + table.Header(header => + { + header.Cell().Background(Colors.Grey.Lighten3).Padding(4).Text("Tipo de Bobina"); + header.Cell().Background(Colors.Grey.Lighten3).Padding(4).AlignRight().Text("Cant. Stock"); + header.Cell().Background(Colors.Grey.Lighten3).Padding(4).AlignRight().Text("Kg. Stock"); + header.Cell().Background(Colors.Grey.Lighten3).Padding(4).AlignRight().Text("Consumo (Kg)"); + header.Cell().Background(Colors.Grey.Lighten3).Padding(4).AlignRight().Text("Días Disp."); + header.Cell().Background(Colors.Grey.Lighten3).Padding(4).AlignCenter().Text("Fin Estimado"); + }); + + foreach (var item in Model.Existencias) + { + table.Cell().BorderBottom(1).BorderColor(Colors.Grey.Lighten2).Padding(4).Text(item.TipoBobina); + table.Cell().BorderBottom(1).BorderColor(Colors.Grey.Lighten2).Padding(4).AlignRight().Text(item.BobinasEnStock?.ToString("N0") ?? "0"); + table.Cell().BorderBottom(1).BorderColor(Colors.Grey.Lighten2).Padding(4).AlignRight().Text(item.TotalKilosEnStock?.ToString("N0") ?? "0"); + table.Cell().BorderBottom(1).BorderColor(Colors.Grey.Lighten2).Padding(4).AlignRight().Text(item.ConsumoAcumulado?.ToString("N0") ?? "0"); + table.Cell().BorderBottom(1).BorderColor(Colors.Grey.Lighten2).Padding(4).AlignRight().Text(item.PromedioDiasDisponibles?.ToString("N0") ?? "N/A"); + table.Cell().BorderBottom(1).BorderColor(Colors.Grey.Lighten2).Padding(4).AlignCenter().Text(item.FechaEstimacionFinStock?.ToString("dd/MM/yyyy") ?? "N/A"); + } + + 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); + + table.Cell().BorderTop(1).BorderColor(Colors.Grey.Lighten2).Padding(4).AlignRight().Text(t => t.Span("Totales").SemiBold()); + table.Cell().BorderTop(1).BorderColor(Colors.Grey.Lighten2).Padding(4).AlignRight().Text(t => t.Span(totalBobinas.ToString("N0")).SemiBold()); + table.Cell().BorderTop(1).BorderColor(Colors.Grey.Lighten2).Padding(4).AlignRight().Text(t => t.Span(totalKilos.ToString("N0")).SemiBold()); + table.Cell().BorderTop(1).BorderColor(Colors.Grey.Lighten2).Padding(4).AlignRight().Text(t => t.Span(totalConsumo.ToString("N0")).SemiBold()); + table.Cell().ColumnSpan(2).BorderTop(1).BorderColor(Colors.Grey.Lighten2); + }); + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Controllers/Reportes/ReportesController.cs b/Backend/GestionIntegral.Api/Controllers/Reportes/ReportesController.cs index 5580a58..5b1cc34 100644 --- a/Backend/GestionIntegral.Api/Controllers/Reportes/ReportesController.cs +++ b/Backend/GestionIntegral.Api/Controllers/Reportes/ReportesController.cs @@ -14,6 +14,8 @@ using GestionIntegral.Api.Data.Repositories.Distribucion; using GestionIntegral.Api.Services.Distribucion; using GestionIntegral.Api.Services.Pdf; using GestionIntegral.Api.Dtos.Reportes.ViewModels; +using GestionIntegral.Api.Controllers.Reportes.PdfTemplates; +using QuestPDF.Infrastructure; namespace GestionIntegral.Api.Controllers { @@ -29,7 +31,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; + private readonly IQuestPdfGenerator _pdfGenerator; // Permisos private const string PermisoVerReporteExistenciaPapel = "RR005"; @@ -51,7 +53,7 @@ namespace GestionIntegral.Api.Controllers IPublicacionRepository publicacionRepository, IEmpresaRepository empresaRepository, IDistribuidorRepository distribuidorRepository, - IPdfGeneratorService pdfGeneratorService) + IQuestPdfGenerator pdfGenerator) { _reportesService = reportesService; _novedadCanillaService = novedadCanillaService; @@ -60,7 +62,7 @@ namespace GestionIntegral.Api.Controllers _publicacionRepository = publicacionRepository; _empresaRepository = empresaRepository; _distribuidorRepository = distribuidorRepository; - _pdfGeneratorService = pdfGeneratorService; + _pdfGenerator = pdfGenerator; } private bool TienePermiso(string codAcc) => User.IsInRole("SuperAdmin") || User.HasClaim(c => c.Type == "permission" && c.Value == codAcc); @@ -98,76 +100,58 @@ 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); - if (error != null) - { - return BadRequest(new { message = error }); - } - if (data == null || !data.Any()) - { - return NotFound(new { message = "No hay datos para generar el PDF con los parámetros seleccionados." }); - } + if (error != null) return BadRequest(new { message = error }); + if (data == null || !data.Any()) return NotFound(new { message = "No hay datos para generar el PDF." }); try { - // Determinar la plantilla y el nombre de la planta - string templatePath; - string nombrePlantaParam = "Consolidado"; + IDocument document; // Se declara aquí + if (consolidado) { - templatePath = "Controllers/Reportes/Templates/ReporteExistenciaPapelConsolidado.cshtml"; + var viewModel = new ExistenciaPapelConsolidadoViewModel + { + Existencias = data, + FechaDesde = fechaDesde.ToString("dd/MM/yyyy"), + FechaHasta = fechaHasta.ToString("dd/MM/yyyy") + }; + document = new ExistenciaPapelConsolidadoDocument(viewModel); } else { - templatePath = "Controllers/Reportes/Templates/ReporteExistenciaPapel.cshtml"; - if (idPlanta.HasValue) + if (!idPlanta.HasValue) { - var planta = await _plantaRepository.GetByIdAsync(idPlanta.Value); - nombrePlantaParam = planta?.Nombre ?? "N/A"; + return BadRequest(new { message = "El idPlanta es requerido para reportes no consolidados." }); } + + var planta = await _plantaRepository.GetByIdAsync(idPlanta.Value); + var viewModel = new ExistenciaPapelViewModel + { + Existencias = data, + NombrePlanta = planta?.Nombre ?? $"Planta ID {idPlanta.Value}", // Manejo por si no se encuentra + FechaDesde = fechaDesde.ToString("dd/MM/yyyy"), + FechaHasta = fechaHasta.ToString("dd/MM/yyyy") + }; + document = new ExistenciaPapelDocument(viewModel); } - // 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") - }; - - // 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 = await _pdfGenerator.GeneratePdfAsync(document); string fileName = $"ExistenciaPapel_{fechaDesde:yyyyMMdd}_{fechaHasta:yyyyMMdd}_{(consolidado ? "Consolidado" : $"Planta{idPlanta}")}.pdf"; return File(pdfBytes, "application/pdf", fileName); } catch (Exception ex) { - _logger.LogError(ex, "Error al generar PDF para Existencia de Papel."); + _logger.LogError(ex, "Error al generar PDF con QuestPDF para Existencia de Papel."); return StatusCode(StatusCodes.Status500InternalServerError, "Error interno al generar el PDF del reporte."); } } diff --git a/Backend/GestionIntegral.Api/Controllers/Reportes/Templates/ReporteExistenciaPapel.cshtml b/Backend/GestionIntegral.Api/Controllers/Reportes/Templates/ReporteExistenciaPapel.cshtml deleted file mode 100644 index 530ba11..0000000 --- a/Backend/GestionIntegral.Api/Controllers/Reportes/Templates/ReporteExistenciaPapel.cshtml +++ /dev/null @@ -1,116 +0,0 @@ -@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 deleted file mode 100644 index f707378..0000000 --- a/Backend/GestionIntegral.Api/Controllers/Reportes/Templates/ReporteExistenciaPapelConsolidado.cshtml +++ /dev/null @@ -1,115 +0,0 @@ -@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/Dockerfile b/Backend/GestionIntegral.Api/Dockerfile index e8ede6c..3d06910 100644 --- a/Backend/GestionIntegral.Api/Dockerfile +++ b/Backend/GestionIntegral.Api/Dockerfile @@ -23,14 +23,6 @@ RUN dotnet publish "GestionIntegral.Api.csproj" -c Release -o /app/publish /p:Us # --- Etapa 3: Final --- FROM mcr.microsoft.com/dotnet/aspnet:9.0 WORKDIR /app - -# Instala dependencias optimizadas -RUN apt-get update && apt-get install -y --no-install-recommends \ - libgbm1 libgconf-2-4 libgdk-pixbuf2.0-0 libgtk-3-0 \ - libnss3 libxss1 libasound2 libxtst6 \ - ca-certificates fonts-liberation lsb-release xdg-utils wget \ - && rm -rf /var/lib/apt/lists/* - COPY --from=publish /app/publish . EXPOSE 8080 diff --git a/Backend/GestionIntegral.Api/GestionIntegral.Api.csproj b/Backend/GestionIntegral.Api/GestionIntegral.Api.csproj index 8a4d8db..299b044 100644 --- a/Backend/GestionIntegral.Api/GestionIntegral.Api.csproj +++ b/Backend/GestionIntegral.Api/GestionIntegral.Api.csproj @@ -3,7 +3,7 @@ net9.0 enable - enable + enable @@ -13,22 +13,20 @@ - - + + false - true @@ -45,5 +43,5 @@ PreserveNewest - +--> \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Program.cs b/Backend/GestionIntegral.Api/Program.cs index 140d25f..142c51f 100644 --- a/Backend/GestionIntegral.Api/Program.cs +++ b/Backend/GestionIntegral.Api/Program.cs @@ -93,8 +93,8 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); // Servicios de Reportes builder.Services.AddScoped(); -// Servicio de PDF -builder.Services.AddScoped(); +// QuestPDF +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 deleted file mode 100644 index 47fbe58..0000000 --- a/Backend/GestionIntegral.Api/Services/Pdf/IPdfGeneratorService.cs +++ /dev/null @@ -1,35 +0,0 @@ -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 deleted file mode 100644 index 5e789b3..0000000 --- a/Backend/GestionIntegral.Api/Services/Pdf/PuppeteerPdfGenerator.cs +++ /dev/null @@ -1,98 +0,0 @@ -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 diff --git a/Backend/GestionIntegral.Api/Services/Pdf/QuestPdfGenerator.cs b/Backend/GestionIntegral.Api/Services/Pdf/QuestPdfGenerator.cs new file mode 100644 index 0000000..20ccd3a --- /dev/null +++ b/Backend/GestionIntegral.Api/Services/Pdf/QuestPdfGenerator.cs @@ -0,0 +1,46 @@ +using Microsoft.Extensions.Logging; +using QuestPDF.Fluent; +using QuestPDF.Infrastructure; +using System; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Services.Pdf +{ + // Una interfaz simple para mantener la inyección de dependencias + public interface IQuestPdfGenerator + { + Task GeneratePdfAsync(IDocument document); + } + + public class QuestPdfGenerator : IQuestPdfGenerator + { + private readonly ILogger _logger; + + public QuestPdfGenerator(ILogger logger) + { + _logger = logger; + + // --- Configuración de la licencia --- + QuestPDF.Settings.License = LicenseType.Community; + _logger.LogInformation("QuestPDF inicializado con licencia Community."); + } + + public async Task GeneratePdfAsync(IDocument document) + { + _logger.LogInformation("Generando PDF con QuestPDF para el documento de tipo {DocumentType}.", document.GetType().Name); + try + { + // GeneratePdf() es síncrono, pero lo envolvemos en un Task para mantener la interfaz asíncrona. + byte[] pdfBytes = await Task.Run(() => document.GeneratePdf()); + + _logger.LogInformation("PDF generado exitosamente con QuestPDF ({Length} bytes).", pdfBytes.Length); + return pdfBytes; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error durante la generación del PDF con QuestPDF."); + throw; + } + } + } +} \ No newline at end of file