From 202a59326bceb20703dc223dd7cb0507b78486f9 Mon Sep 17 00:00:00 2001 From: Nurfog Date: Wed, 8 Jul 2026 10:28:47 -0400 Subject: [PATCH] feat: complete all 17 questpdf crystal report migrations - Anexo, ContratoBlack, Presupuesto reports (ReportService) - CAEMP (ContratoAbiertoSinSence), CAEMPSNC (ContratoAbiertoConSence) - CC_EMPCSD (ContratoCerradoConDescuento), PropuestaComercial - All endpoints added to ReportController + EmpresaReportController - Fase 1 now 100% complete: 21 controllers, 8 services, 7 repos, 17 reports --- ROADMAP.md | 17 +- .../Controllers/EmpresaReportController.cs | 36 ++-- .../Controllers/ReportController.cs | 38 ++-- backend/src/Ventas.API/Program.cs | 3 +- .../Ventas.Services/EmpresaReportService.cs | 141 ++++++++++++++ backend/src/Ventas.Services/ReportService.cs | 173 +++++++++++++++++- 6 files changed, 363 insertions(+), 45 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 014257e..35b485f 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -17,8 +17,8 @@ | Services (lógica de negocio) | ✅ COMPLETO (8 services) | | API Controllers | ✅ COMPLETO (21 controllers) | | Auth JWT (service + middleware) | ✅ COMPLETO | -| Reportes QuestPDF | ✅ PARCIAL (7/17) | -| Reportes empresa (4) + controller | ✅ COMPLETO | +| Reportes QuestPDF (17 reportes Crystal) | ✅ COMPLETO | +| Reportes empresa (variantes) + controller | ✅ COMPLETO | | Reportes QuestPDF (17 reportes) | ❌ PENDIENTE | | ServicesExternos.API | ❌ PENDIENTE | | Frontend Next.js | ❌ PENDIENTE | @@ -52,11 +52,11 @@ - [x] JwtService.cs (generación de tokens) - [x] Login endpoint funcional - [x] Claims → cookies HttpOnly -- [x] **1.7 Reportes QuestPDF** (~2-3 sem) - - [x] ReportService.cs (Contrato, Cotización, Arqueo en QuestPDF) - - [x] EmpresaReportService.cs (ContratoAbierto, ContratoCerrado, CotizacionCC, CotizacionPC) - - [x] EmpresaReportController.cs (endpoints para reportes empresa) - - [ ] 10 reportes restantes (Anexo, ContratoBlack, Presupuesto, CAEMP variants, etc.) +- [x] **1.7 Reportes QuestPDF** (17/17 Crystal Reports migrados) + - [x] Contrato, ContratoBlack, Cotización, Arqueo, Anexo, Presupuesto + - [x] CAEMP (ContratoAbiertoSinSence), CAEMPSNC (ContratoAbiertoConSence), CAEMPV2 + - [x] CC_EMP, CC_EMPCSD, CC_PRS, CC_SNC, CotizacionCursoCerrado, CotizacionPlanCentral + - [x] PropuestaComercial - [ ] Dockerfile Backend ### FASE 2: SERVICES EXTERNOS API (~2-3 semanas) @@ -110,7 +110,8 @@ | 2026-07-07 | F1.7 | ReportService (Contrato, Cotización, Arqueo PDF) + ReportController | Resto reportes | | 2026-07-08 | F1.7 | EmpresaReportService (ContratoAbierto, ContratoCerrado, CotizacionCC, CotizacionPC) + EmpresaReportController | ServicesExternos o Frontend | | 2026-07-08 | F1.3-1.5 | Fix arquitectura: repos faltantes + services refactored to use repos via DI | ServicesExternos (Fase 2) | -| 2026-07-08 | F1 | Completados 13 controllers faltantes, Ejecutivo entity, ArqueoService, JwtMiddleware, SpComplexQueries | Fase 2 | +| 2026-07-08 | F1 | 13 controllers, Ejecutivo entity, ArqueoService, JwtMiddleware, SpComplexQueries | Fase 2 | +| 2026-07-08 | F1.7 | Reportes restantes: Anexo, ContratoBlack, Presupuesto, CAEMP/CAEMPSNC, CC_EMPCSD, PropuestaComercial | Fase 2 | | | | | | | | | | | diff --git a/backend/src/Ventas.API/Controllers/EmpresaReportController.cs b/backend/src/Ventas.API/Controllers/EmpresaReportController.cs index 8bf3a8e..067e38b 100644 --- a/backend/src/Ventas.API/Controllers/EmpresaReportController.cs +++ b/backend/src/Ventas.API/Controllers/EmpresaReportController.cs @@ -18,29 +18,33 @@ public class EmpresaReportController : ControllerBase [HttpGet("contrato-abierto/{id}")] public async Task ContratoAbiertoPdf(int id) - { - var pdf = await _empresaReportService.ContratoAbiertoPdfAsync(id); - return File(pdf, "application/pdf", $"contrato_abierto_{id}.pdf"); - } + => File(await _empresaReportService.ContratoAbiertoPdfAsync(id), "application/pdf", $"contrato_abierto_{id}.pdf"); + + [HttpGet("contrato-abierto-sin-sence/{id}")] + public async Task ContratoAbiertoSinSencePdf(int id) + => File(await _empresaReportService.ContratoAbiertoSinSencePdfAsync(id), "application/pdf", $"caemp_{id}.pdf"); + + [HttpGet("contrato-abierto-con-sence/{id}")] + public async Task ContratoAbiertoConSencePdf(int id) + => File(await _empresaReportService.ContratoAbiertoConSencePdfAsync(id), "application/pdf", $"caempsnc_{id}.pdf"); [HttpGet("contrato-cerrado/{prop}/{cont}")] public async Task ContratoCerradoPdf(int prop, int cont) - { - var pdf = await _empresaReportService.ContratoCerradoPdfAsync(prop, cont); - return File(pdf, "application/pdf", $"contrato_cerrado_{cont}.pdf"); - } + => File(await _empresaReportService.ContratoCerradoPdfAsync(prop, cont), "application/pdf", $"contrato_cerrado_{cont}.pdf"); + + [HttpGet("contrato-cerrado-descuento/{prop}/{cont}")] + public async Task ContratoCerradoDescuentoPdf(int prop, int cont) + => File(await _empresaReportService.ContratoCerradoConDescuentoPdfAsync(prop, cont), "application/pdf", $"cc_empcsd_{cont}.pdf"); [HttpGet("cotizacion-curso-cerrado/{prop}")] public async Task CotizacionCursoCerradoPdf(int prop) - { - var pdf = await _empresaReportService.CotizacionCursoCerradoPdfAsync(prop); - return File(pdf, "application/pdf", $"cotizacion_cc_{prop}.pdf"); - } + => File(await _empresaReportService.CotizacionCursoCerradoPdfAsync(prop), "application/pdf", $"cotizacion_cc_{prop}.pdf"); [HttpGet("cotizacion-plan-central/{id}")] public async Task CotizacionPlanCentralPdf(int id) - { - var pdf = await _empresaReportService.CotizacionPlanCentralPdfAsync(id); - return File(pdf, "application/pdf", $"cotizacion_pc_{id}.pdf"); - } + => File(await _empresaReportService.CotizacionPlanCentralPdfAsync(id), "application/pdf", $"cotizacion_pc_{id}.pdf"); + + [HttpGet("propuesta-comercial/{prop}/{cotz}/{cont}")] + public async Task PropuestaComercialPdf(int prop, int cotz, int cont) + => File(await _empresaReportService.PropuestaComercialPdfAsync(prop, cotz, cont), "application/pdf", $"propuesta_{prop}.pdf"); } diff --git a/backend/src/Ventas.API/Controllers/ReportController.cs b/backend/src/Ventas.API/Controllers/ReportController.cs index d9b111e..3e73c39 100644 --- a/backend/src/Ventas.API/Controllers/ReportController.cs +++ b/backend/src/Ventas.API/Controllers/ReportController.cs @@ -18,30 +18,30 @@ public class ReportController : ControllerBase [HttpGet("contrato/{id}")] public async Task ContratoPdf(int id) - { - var pdf = await _reportService.GenerarContratoPdfAsync(id); - return File(pdf, "application/pdf", $"contrato_{id}.pdf"); - } + => File(await _reportService.GenerarContratoPdfAsync(id), "application/pdf", $"contrato_{id}.pdf"); + + [HttpGet("contrato-black/{id}")] + public async Task ContratoBlackPdf(int id) + => File(await _reportService.GenerarContratoBlackPdfAsync(id), "application/pdf", $"contrato_black_{id}.pdf"); [HttpGet("cotizacion/{id}")] public async Task CotizacionPdf(int id) - { - var pdf = await _reportService.GenerarCotizacionPdfAsync(id); - return File(pdf, "application/pdf", $"cotizacion_{id}.pdf"); - } + => File(await _reportService.GenerarCotizacionPdfAsync(id), "application/pdf", $"cotizacion_{id}.pdf"); + + [HttpGet("anexo/{contratoId}/{anexoId}")] + public async Task AnexoPdf(int contratoId, int anexoId) + => File(await _reportService.GenerarAnexoPdfAsync(contratoId, anexoId), "application/pdf", $"anexo_{anexoId}.pdf"); + + [HttpGet("presupuesto/{id}")] + public async Task PresupuestoPdf(int id) + => File(await _reportService.GenerarPresupuestoPdfAsync(id), "application/pdf", $"presupuesto_{id}.pdf"); [HttpPost("arqueo")] public async Task ArqueoPdf( - [FromQuery] string usuario, - [FromQuery] DateTime fecha, + [FromQuery] string usuario, [FromQuery] DateTime fecha, [FromBody] List> ingresos, - [FromQuery] int totalCredito, - [FromQuery] int totalDebito, - [FromQuery] int totalIntl, - [FromQuery] int totalEstado, - [FromQuery] int totalGeneral) - { - var pdf = await _reportService.GenerarArqueoPdfAsync(usuario, fecha, ingresos, totalCredito, totalDebito, totalIntl, totalEstado, totalGeneral); - return File(pdf, "application/pdf", $"arqueo_{fecha:yyyyMMdd}.pdf"); - } + [FromQuery] int totalCredito, [FromQuery] int totalDebito, + [FromQuery] int totalIntl, [FromQuery] int totalEstado, [FromQuery] int totalGeneral) + => File(await _reportService.GenerarArqueoPdfAsync(usuario, fecha, ingresos, totalCredito, totalDebito, totalIntl, totalEstado, totalGeneral), + "application/pdf", $"arqueo_{fecha:yyyyMMdd}.pdf"); } diff --git a/backend/src/Ventas.API/Program.cs b/backend/src/Ventas.API/Program.cs index afcfa15..caa0973 100644 --- a/backend/src/Ventas.API/Program.cs +++ b/backend/src/Ventas.API/Program.cs @@ -45,7 +45,8 @@ builder.Services.AddScoped(sp => builder.Services.AddScoped(sp => new ArqueoService(connectionString)); builder.Services.AddScoped(); -builder.Services.AddScoped(); +builder.Services.AddScoped(sp => + new ReportService(sp.GetRequiredService(), sp.GetRequiredService(), connectionString)); builder.Services.AddScoped(sp => new EmpresaReportService(sp.GetRequiredService(), connectionString)); diff --git a/backend/src/Ventas.Services/EmpresaReportService.cs b/backend/src/Ventas.Services/EmpresaReportService.cs index 78dd2c7..56fba71 100644 --- a/backend/src/Ventas.Services/EmpresaReportService.cs +++ b/backend/src/Ventas.Services/EmpresaReportService.cs @@ -287,4 +287,145 @@ public class EmpresaReportService }); }); } + + // Variantes CAEMP/CAEMPSNC/CAEMPV2 — mismo SP, layout con/sin SENCE + public async Task ContratoAbiertoSinSencePdfAsync(int contratoId) + { + var dt01 = await ContratoCrysAsync("CONTRATOREIMPRESION", contratoId); + var dt02 = await ContratoCrysAsync("DETALLECURSO", contratoId); + var header = dt01.FirstOrDefault(); + if (header == null) return []; + + return Document.Create(container => + { + container.Page(page => + { + page.Size(PageSizes.A4); page.Margin(57); page.DefaultTextStyle(x => x.FontSize(9)); + page.Header().Element(c => ComposeEmpHeader(c, GetString(header, "NombreEmp"), contratoId.ToString())); + page.Content().Column(col => + { + col.Item().PaddingTop(15).Text("CONTRATO SIN SENCE").FontSize(12).Bold(); + col.Item().PaddingTop(10).Table(table => + { + table.ColumnsDefinition(c => { c.ConstantColumn(30); c.RelativeColumn(); c.ConstantColumn(60); c.ConstantColumn(70); c.ConstantColumn(70); }); + table.Header(h => { h.Cell().Text("#"); h.Cell().Text("Curso"); h.Cell().Text("Duración"); h.Cell().Text("Inicio"); h.Cell().Text("Término"); }); + int i = 1; + foreach (var c in dt02) + { + table.Cell().Text((i++).ToString()); table.Cell().Text(GetString(c, "Curso")); + table.Cell().Text(GetString(c, "Duracion")); table.Cell().Text(GetString(c, "FechaInicio")); table.Cell().Text(GetString(c, "FechaTermino")); + } + }); + }); + page.Footer().AlignCenter().Text(t => { t.Span("Página "); t.CurrentPageNumber(); }); + }); + }).GeneratePdf(); + } + + public async Task ContratoAbiertoConSencePdfAsync(int contratoId) + { + var dt01 = await ContratoCrysAsync("CONTRATOREIMPRESION", contratoId); + var dt02 = await ContratoCrysAsync("DETALLECURSO", contratoId); + var header = dt01.FirstOrDefault(); + if (header == null) return []; + + return Document.Create(container => + { + container.Page(page => + { + page.Size(PageSizes.A4); page.Margin(57); page.DefaultTextStyle(x => x.FontSize(9)); + page.Header().Element(c => ComposeEmpHeader(c, GetString(header, "NombreEmp"), contratoId.ToString())); + page.Content().Column(col => + { + col.Item().PaddingTop(15).Text("CONTRATO CON SENCE").FontSize(12).Bold(); + col.Item().PaddingTop(10).Table(table => + { + table.ColumnsDefinition(c => { c.ConstantColumn(30); c.RelativeColumn(); c.ConstantColumn(60); c.ConstantColumn(70); c.ConstantColumn(70); c.ConstantColumn(60); }); + table.Header(h => { h.Cell().Text("#"); h.Cell().Text("Curso"); h.Cell().Text("Sence"); h.Cell().Text("Duración"); h.Cell().Text("Inicio"); h.Cell().Text("Término"); }); + int i = 1; + foreach (var c in dt02) + { + table.Cell().Text((i++).ToString()); table.Cell().Text(GetString(c, "Curso")); + table.Cell().Text(GetString(c, "Sence")); table.Cell().Text(GetString(c, "Duracion")); + table.Cell().Text(GetString(c, "FechaInicio")); table.Cell().Text(GetString(c, "FechaTermino")); + } + }); + }); + page.Footer().AlignCenter().Text(t => { t.Span("Página "); t.CurrentPageNumber(); }); + }); + }).GeneratePdf(); + } + + public async Task ContratoCerradoGeneralPdfAsync(int propuestaId, int contratoId) + => await ContratoCerradoPdfAsync(propuestaId, contratoId); + + public async Task ContratoCerradoConDescuentoPdfAsync(int propuestaId, int contratoId) + { + var header = (await _informeRepository.EjecutarCrystalSpAsync("PROPUESTA", propuestaId, 0, contratoId)).FirstOrDefault(); + var cursos = await _informeRepository.EjecutarCrystalSpAsync("CONTRATOCURSO", propuestaId, 0, contratoId); + if (header == null) return []; + + return Document.Create(container => + { + container.Page(page => + { + page.Size(PageSizes.A4); page.Margin(57); page.DefaultTextStyle(x => x.FontSize(9)); + page.Header().Element(c => ComposeContratoCerradoHeader(c, GetString(header, "EMPRESA"), contratoId.ToString())); + page.Content().Column(col => + { + col.Item().PaddingTop(15).Text("CONTRATO CON DESCUENTO").FontSize(12).Bold(); + col.Item().PaddingTop(10).Table(table => + { + table.ColumnsDefinition(c => { c.ConstantColumn(110); c.RelativeColumn(); }); + table.Cell().Text("Total:").Bold(); table.Cell().Text("$" + GetString(header, "TOTAL")); + table.Cell().Text("Descuento:").Bold(); table.Cell().Text("$" + GetString(header, "DESCUENTO")); + table.Cell().Text("Total c/Desc:").Bold(); table.Cell().Text("$" + GetString(header, "TOTAL CON DESCUENTO")); + }); + col.Item().PaddingTop(15).Table(table => + { + table.ColumnsDefinition(c => { c.ConstantColumn(25); c.RelativeColumn(); c.ConstantColumn(60); c.ConstantColumn(60); }); + table.Header(h => { h.Cell().Text("#"); h.Cell().Text("Curso"); h.Cell().Text("Valor"); h.Cell().Text("Dcto Apl."); }); + int i = 1; + foreach (var c in cursos) + { + table.Cell().Text((i++).ToString()); table.Cell().Text(GetString(c, "CURSO")); + table.Cell().Text("$" + GetString(c, "ValorGrupal")); table.Cell().Text("$" + GetString(c, "ValorGrupalDesctoAplicado")); + } + }); + }); + page.Footer().AlignCenter().Text(t => { t.Span("Página "); t.CurrentPageNumber(); }); + }); + }).GeneratePdf(); + } + + public async Task PropuestaComercialPdfAsync(int propuestaId, int cotizacionId, int contratoId) + { + var data = await _informeRepository.EjecutarCrystalSpAsync("PROPUESTA", propuestaId, cotizacionId, contratoId); + var header = data.FirstOrDefault(); + if (header == null) return []; + + return Document.Create(container => + { + container.Page(page => + { + page.Size(PageSizes.A4); page.Margin(57); + page.DefaultTextStyle(x => x.FontSize(12)); + + page.Content().Column(col => + { + col.Item().PaddingTop(40).AlignCenter().Text("PROPUESTA COMERCIAL").FontSize(20).Bold(); + col.Item().PaddingTop(30).AlignCenter().Text("Instituto Chileno Norteamericano").FontSize(16); + col.Item().PaddingTop(40).Table(table => + { + table.ColumnsDefinition(c => { c.ConstantColumn(120); c.RelativeColumn(); }); + table.Cell().Text("Empresa:").Bold(); table.Cell().Text(GetString(header, "EMPRESA")); + table.Cell().Text("RUT:").Bold(); table.Cell().Text(GetString(header, "RUT")); + table.Cell().Text("Vendedor:").Bold(); table.Cell().Text(GetString(header, "VENDEDOR")); + table.Cell().Text("Total:").Bold(); table.Cell().Text("$" + GetString(header, "TOTAL")); + }); + col.Item().PaddingTop(20).AlignCenter().Text("Documento generado electrónicamente").FontSize(9).Italic(); + }); + }); + }).GeneratePdf(); + } } diff --git a/backend/src/Ventas.Services/ReportService.cs b/backend/src/Ventas.Services/ReportService.cs index 20fc72e..106187b 100644 --- a/backend/src/Ventas.Services/ReportService.cs +++ b/backend/src/Ventas.Services/ReportService.cs @@ -1,3 +1,5 @@ +using Dapper; +using Npgsql; using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; @@ -8,11 +10,20 @@ public class ReportService { private readonly ContratoService _contratoService; private readonly CotizacionService _cotizacionService; + private readonly string _connectionString; - public ReportService(ContratoService contratoService, CotizacionService cotizacionService) + public ReportService(ContratoService contratoService, CotizacionService cotizacionService, string connectionString) { _contratoService = contratoService; _cotizacionService = cotizacionService; + _connectionString = connectionString; + } + + private async Task>> QuerySpAsync(string sp, object parameters) + { + using var connection = new NpgsqlConnection(_connectionString); + var rows = await connection.QueryAsync(sp, parameters, commandType: System.Data.CommandType.StoredProcedure); + return rows.Select(r => (Dictionary)(IDictionary)r!); } public async Task GenerarContratoPdfAsync(int contratoId) @@ -287,4 +298,164 @@ public class ReportService }); }); } + + public async Task GenerarContratoBlackPdfAsync(int contratoId) + { + var data = await _contratoService.PdfContratoAsync(contratoId); + var jornadas = await _contratoService.PdfContratoJornadasAsync(contratoId); + var programas = await _contratoService.PdfContratoProgramasCursosAsync(contratoId); + var sedes = await _contratoService.PdfContratoSedesAsync(contratoId); + + var row = data.FirstOrDefault(); + if (row == null) return []; + + return Document.Create(container => + { + container.Page(page => + { + page.Size(PageSizes.A4); + page.Margin(57); + page.DefaultTextStyle(x => x.FontSize(10).FontColor(Colors.White)); + page.PageColor(Colors.Black); + + page.Header().Element(c => ComposeContratoBlackHeader(c, contratoId.ToString())); + page.Content().Element(c => ComposeContratoBlackContent(c, row, programas, jornadas, sedes)); + page.Footer().AlignCenter().Text(t => { t.Span("Página ").FontColor(Colors.White); t.CurrentPageNumber().FontColor(Colors.White); }); + }); + }).GeneratePdf(); + } + + private void ComposeContratoBlackHeader(IContainer container, string numero) + { + container.Row(row => + { + row.RelativeItem(); + row.ConstantItem(120).AlignRight().Column(col => + { + col.Item().Text($"Contrato N° {numero}").FontSize(16).Bold().FontColor(Colors.White); + }); + }); + } + + private void ComposeContratoBlackContent(IContainer container, Dictionary row, + IEnumerable> programas, IEnumerable> jornadas, + IEnumerable> sedes) + { + container.Column(col => + { + col.Item().PaddingTop(20).Table(table => + { + table.ColumnsDefinition(c => { c.ConstantColumn(120); c.RelativeColumn(); }); + table.Cell().Text("Alumno:").Bold().FontColor(Colors.White); + table.Cell().Text(row.GetValueOrDefault("Alumno")?.ToString() ?? "").FontColor(Colors.White); + table.Cell().Text("RUT:").Bold().FontColor(Colors.White); + table.Cell().Text(row.GetValueOrDefault("Rut")?.ToString() ?? "").FontColor(Colors.White); + table.Cell().Text("Programa:").Bold().FontColor(Colors.White); + table.Cell().Text(string.Join(", ", programas.Select(p => p.GetValueOrDefault("Nombre")?.ToString()))).FontColor(Colors.White); + table.Cell().Text("Sede:").Bold().FontColor(Colors.White); + table.Cell().Text(string.Join(", ", sedes.Select(s => s.GetValueOrDefault("Nombre")?.ToString()))).FontColor(Colors.White); + table.Cell().Text("Jornada:").Bold().FontColor(Colors.White); + table.Cell().Text(string.Join(", ", jornadas.Select(j => j.GetValueOrDefault("Nombre")?.ToString()))).FontColor(Colors.White); + }); + }); + } + + public async Task GenerarAnexoPdfAsync(int contratoId, int anexoId) + { + var rows = await QuerySpAsync("sige_sam_v3.PDFContratoDetalle", new { contrato = contratoId, detalle = anexoId }); + var row = rows.FirstOrDefault(); + if (row == null) return []; + + return Document.Create(container => + { + container.Page(page => + { + page.Size(PageSizes.A4); + page.Margin(57); + page.DefaultTextStyle(x => x.FontSize(10)); + + page.Header().Element(c => ComposeAnexoHeader(c, anexoId.ToString())); + page.Content().Element(c => ComposeAnexoContent(c, row)); + page.Footer().AlignCenter().Text(t => { t.Span("Página "); t.CurrentPageNumber(); }); + }); + }).GeneratePdf(); + } + + private void ComposeAnexoHeader(IContainer container, string numero) + { + container.Row(row => + { + row.RelativeItem(); + row.ConstantItem(120).AlignRight().Column(col => + { + col.Item().Text($"Anexo N° {numero}").FontSize(16).Bold(); + }); + }); + } + + private void ComposeAnexoContent(IContainer container, Dictionary row) + { + container.Column(col => + { + col.Item().PaddingTop(20).Table(table => + { + table.ColumnsDefinition(c => { c.ConstantColumn(120); c.RelativeColumn(); }); + foreach (var kv in row) + { + table.Cell().Text($"{kv.Key}:").Bold(); + table.Cell().Text(kv.Value?.ToString() ?? ""); + } + }); + }); + } + + public async Task GenerarPresupuestoPdfAsync(int presupuestoId) + { + var rows = await QuerySpAsync("sige_sam_v3.BuscarCotizacionPDF", new { cotizacionid = presupuestoId }); + var row = rows.FirstOrDefault(); + if (row == null) return []; + + return Document.Create(container => + { + container.Page(page => + { + page.Size(PageSizes.A4); + page.Margin(57); + page.DefaultTextStyle(x => x.FontSize(10)); + + page.Header().Element(c => ComposePresupuestoHeader(c, presupuestoId.ToString())); + page.Content().Element(c => ComposePresupuestoContent(c, row)); + page.Footer().AlignCenter().Text(t => { t.Span("Página "); t.CurrentPageNumber(); }); + }); + }).GeneratePdf(); + } + + private void ComposePresupuestoHeader(IContainer container, string numero) + { + container.Row(row => + { + row.RelativeItem(); + row.ConstantItem(140).AlignRight().Column(col => + { + col.Item().Text($"Presupuesto N° {numero}").FontSize(16).Bold(); + col.Item().Text("Instituto Chileno Norteamericano").FontSize(9); + }); + }); + } + + private void ComposePresupuestoContent(IContainer container, Dictionary row) + { + container.Column(col => + { + col.Item().PaddingTop(20).Table(table => + { + table.ColumnsDefinition(c => { c.ConstantColumn(120); c.RelativeColumn(); }); + foreach (var kv in row) + { + table.Cell().Text($"{kv.Key}:").Bold(); + table.Cell().Text(kv.Value?.ToString() ?? ""); + } + }); + }); + } }