From df028eb4ddf196cbba9b257146e10fb5177b5db7 Mon Sep 17 00:00:00 2001 From: Nurfog Date: Wed, 8 Jul 2026 09:27:26 -0400 Subject: [PATCH] feat: enterprise questpdf reports - EmpresaReportService with 4 enterprise PDF reports - ContratoAbierto: contract header, course detail, schedules, students - ContratoCerrado: proposal info, courses, student list - CotizacionCursoCerrado: quote header, course detail table - CotizacionPlanCentral: company info, payment plan, course schedule - EmpresaReportController with download endpoints - All using ModuloEmpresa_PropuestaCrystalReport SP via Dapper --- ROADMAP.md | 4 +- .../Controllers/EmpresaReportController.cs | 46 ++ backend/src/Ventas.API/Program.cs | 2 + .../Ventas.Services/EmpresaReportService.cs | 437 ++++++++++++++++++ 4 files changed, 488 insertions(+), 1 deletion(-) create mode 100644 backend/src/Ventas.API/Controllers/EmpresaReportController.cs create mode 100644 backend/src/Ventas.Services/EmpresaReportService.cs diff --git a/ROADMAP.md b/ROADMAP.md index b1b01c7..a230263 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -60,7 +60,9 @@ - [x] Claims → cookies HttpOnly - [x] **1.7 Reportes QuestPDF** (~2-3 sem) - [x] ReportService.cs (Contrato, Cotización, Arqueo en QuestPDF) - - [ ] 14 reportes restantes (Anexo, ContratoBlack, Presupuesto, CAEMP, CC_EMP, etc.) + - [x] EmpresaReportService.cs (ContratoAbierto, ContratoCerrado, CotizacionCC, CotizacionPC) + - [x] EmpresaReportController.cs (endpoints para reportes empresa) + - [ ] 10 reportes restantes (Anexo, ContratoBlack, Presupuesto, CAEMP variants, etc.) - [ ] Dockerfile Backend ### FASE 2: SERVICES EXTERNOS API (~2-3 semanas) diff --git a/backend/src/Ventas.API/Controllers/EmpresaReportController.cs b/backend/src/Ventas.API/Controllers/EmpresaReportController.cs new file mode 100644 index 0000000..8bf3a8e --- /dev/null +++ b/backend/src/Ventas.API/Controllers/EmpresaReportController.cs @@ -0,0 +1,46 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Ventas.Services; + +namespace Ventas.API.Controllers; + +[ApiController] +[Route("api/reportes/empresa")] +[Authorize] +public class EmpresaReportController : ControllerBase +{ + private readonly EmpresaReportService _empresaReportService; + + public EmpresaReportController(EmpresaReportService empresaReportService) + { + _empresaReportService = empresaReportService; + } + + [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"); + } + + [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"); + } + + [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"); + } + + [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"); + } +} diff --git a/backend/src/Ventas.API/Program.cs b/backend/src/Ventas.API/Program.cs index 2df0b91..a908680 100644 --- a/backend/src/Ventas.API/Program.cs +++ b/backend/src/Ventas.API/Program.cs @@ -35,6 +35,8 @@ builder.Services.AddScoped(sp => builder.Services.AddScoped(sp => new InformeService(connectionString)); builder.Services.AddScoped(); +builder.Services.AddScoped(sp => + new EmpresaReportService(connectionString)); var jwtSecret = builder.Configuration["Jwt:Secret"] ?? "default-dev-secret-change-in-production"; var jwtExpiration = int.Parse(builder.Configuration["Jwt:ExpirationMinutes"] ?? "30"); diff --git a/backend/src/Ventas.Services/EmpresaReportService.cs b/backend/src/Ventas.Services/EmpresaReportService.cs new file mode 100644 index 0000000..55820b8 --- /dev/null +++ b/backend/src/Ventas.Services/EmpresaReportService.cs @@ -0,0 +1,437 @@ +using Dapper; +using Npgsql; +using QuestPDF.Fluent; +using QuestPDF.Helpers; +using QuestPDF.Infrastructure; + +namespace Ventas.Services; + +public class EmpresaReportService +{ + private readonly string _connectionString; + + public EmpresaReportService(string connectionString) + { + _connectionString = connectionString; + } + + private async Task>> EjecutarCrystalSpAsync(string tipo, int prop, int cotz, int cont) + { + using var connection = new NpgsqlConnection(_connectionString); + var rows = await connection.QueryAsync( + "ModuloEmpresa_PropuestaCrystalReport", + new { tipo, prop, cotz, cont }, + commandType: System.Data.CommandType.StoredProcedure); + return rows.Select(r => (Dictionary)(IDictionary)r!); + } + + private async Task>> EjecutarContratoCrysAsync(string tipo, int cont) + { + using var connection = new NpgsqlConnection(_connectionString); + var rows = await connection.QueryAsync( + "Empresa_ContratoCrys", + new { tipo, cont }, + commandType: System.Data.CommandType.StoredProcedure); + return rows.Select(r => (Dictionary)(IDictionary)r!); + } + + private static string GetString(Dictionary row, string key) => + row.GetValueOrDefault(key)?.ToString() ?? ""; + + public async Task ContratoAbiertoPdfAsync(int contratoId) + { + var dt01 = await EjecutarContratoCrysAsync("CONTRATOREIMPRESION", contratoId); + var dt02 = await EjecutarContratoCrysAsync("DETALLECURSO", contratoId); + var dt03 = await EjecutarContratoCrysAsync("HORARIO", contratoId); + var dt04 = await EjecutarContratoCrysAsync("ALUMNO", 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().Element(c => ComposeEmpContent(c, header, dt02, dt03, dt04)); + page.Footer().AlignCenter().Text(t => { t.Span("Página "); t.CurrentPageNumber(); }); + }); + }).GeneratePdf(); + } + + public async Task ContratoCerradoPdfAsync(int propuestaId, int contratoId) + { + var infoPropuesta = await EjecutarCrystalSpAsync("PROPUESTA", propuestaId, 0, contratoId); + var infoCursos = await EjecutarCrystalSpAsync("CONTRATOCURSO", propuestaId, 0, contratoId); + var infoAlumnos = await EjecutarCrystalSpAsync("CONTRATOALUMNO", propuestaId, 0, contratoId); + + var header = infoPropuesta.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 => ComposeContratoCerradoHeader(c, GetString(header, "EMPRESA"), contratoId.ToString())); + page.Content().Element(c => ComposeContratoCerradoContent(c, header, infoCursos, infoAlumnos)); + page.Footer().AlignCenter().Text(t => { t.Span("Página "); t.CurrentPageNumber(); }); + }); + }).GeneratePdf(); + } + + public async Task CotizacionCursoCerradoPdfAsync(int propuestaId) + { + var data = await EjecutarCrystalSpAsync("PROPUESTA", propuestaId, 0, 0); + var detalle = await EjecutarCrystalSpAsync("PROPUESTADET", propuestaId, 0, 0); + + 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(9)); + + page.Header().Element(c => ComposeCotizacionCCHeader(c, GetString(header, "EMPRESA"), propuestaId.ToString())); + page.Content().Element(c => ComposeCotizacionCCContent(c, header, detalle)); + page.Footer().AlignCenter().Text(t => { t.Span("Página "); t.CurrentPageNumber(); }); + }); + }).GeneratePdf(); + } + + public async Task CotizacionPlanCentralPdfAsync(int cotizacionId) + { + var data = await EjecutarCrystalSpAsync("COTIZACIONV2", 0, cotizacionId, 0); + var detalle = await EjecutarCrystalSpAsync("COTIZACIONDETV2", 0, cotizacionId, 0); + + 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(9)); + + page.Header().Element(c => ComposePlanCentralHeader(c, GetString(header, "Razon Social"), cotizacionId.ToString())); + page.Content().Element(c => ComposePlanCentralContent(c, header, detalle)); + page.Footer().AlignCenter().Text(t => { t.Span("Página "); t.CurrentPageNumber(); }); + }); + }).GeneratePdf(); + } + + private static void ComposeEmpHeader(IContainer container, string empresa, string numContrato) + { + container.Row(row => + { + row.RelativeItem().Column(col => + { + col.Item().Text("Instituto Chileno Norteamericano").FontSize(12).Bold(); + col.Item().Text("Módulo Empresa").FontSize(8); + }); + row.ConstantItem(140).AlignRight().Column(col => + { + col.Item().Text($"Contrato N° {numContrato}").FontSize(14).Bold(); + }); + }); + } + + private static void ComposeEmpContent(IContainer container, Dictionary header, + IEnumerable> cursos, IEnumerable> horarios, + IEnumerable> alumnos) + { + container.Column(col => + { + col.Item().PaddingTop(15).Table(table => + { + table.ColumnsDefinition(c => { c.ConstantColumn(110); c.RelativeColumn(); }); + table.Cell().Text("Empresa:").Bold(); + table.Cell().Text(GetString(header, "NombreEmp")); + table.Cell().Text("RUT:").Bold(); + table.Cell().Text(GetString(header, "idEmpresa")); + table.Cell().Text("Dirección:").Bold(); + table.Cell().Text(GetString(header, "DireccionEmp")); + table.Cell().Text("Representante:").Bold(); + table.Cell().Text(GetString(header, "Representante")); + table.Cell().Text("Vendedor:").Bold(); + table.Cell().Text(GetString(header, "Vendedor")); + }); + + col.Item().PaddingTop(15).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("#").Bold(); + h.Cell().Text("Curso").Bold(); + h.Cell().Text("Duración").Bold(); + h.Cell().Text("Inicio").Bold(); + h.Cell().Text("Término").Bold(); + }); + + int idx = 1; + foreach (var c in cursos) + { + table.Cell().Text(idx++.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")); + } + }); + + col.Item().PaddingTop(10).Text("Horarios:").Bold(); + foreach (var h in horarios) + { + col.Item().PaddingLeft(10).Text(GetString(h, "Horario")); + } + + col.Item().PaddingTop(10).Text("Alumnos:").Bold(); + foreach (var a in alumnos) + { + col.Item().PaddingLeft(10).Text(GetString(a, "Alumno")); + } + }); + } + + private static void ComposeContratoCerradoHeader(IContainer container, string empresa, string numContrato) + { + container.Row(row => + { + row.RelativeItem().Column(col => + { + col.Item().Text("Instituto Chileno Norteamericano").FontSize(12).Bold(); + col.Item().Text("Módulo Empresa - Contrato Cerrado").FontSize(8); + }); + row.ConstantItem(140).AlignRight().Column(col => + { + col.Item().Text($"Contrato N° {numContrato}").FontSize(14).Bold(); + }); + }); + } + + private static void ComposeContratoCerradoContent(IContainer container, Dictionary header, + IEnumerable> cursos, IEnumerable> alumnos) + { + container.Column(col => + { + col.Item().PaddingTop(15).Table(table => + { + table.ColumnsDefinition(c => { c.ConstantColumn(110); 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("Dirección:").Bold(); + table.Cell().Text(GetString(header, "DIRECCION")); + 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(15).Table(table => + { + table.ColumnsDefinition(c => + { + c.ConstantColumn(25); + c.RelativeColumn(); + c.ConstantColumn(50); + c.ConstantColumn(60); + c.ConstantColumn(60); + }); + + table.Header(h => + { + h.Cell().Text("#").Bold(); + h.Cell().Text("Curso").Bold(); + h.Cell().Text("Horas").Bold(); + h.Cell().Text("Inicio").Bold(); + h.Cell().Text("Valor").Bold(); + }); + + int idx = 1; + foreach (var c in cursos) + { + table.Cell().Text(idx++.ToString()); + table.Cell().Text(GetString(c, "CURSO")); + table.Cell().Text(GetString(c, "Duracion")); + table.Cell().Text(GetString(c, "FechaInicio")); + table.Cell().Text("$" + GetString(c, "ValorGrupal")); + } + }); + + col.Item().PaddingTop(15).Table(table => + { + table.ColumnsDefinition(c => { c.ConstantColumn(25); c.RelativeColumn(); }); + table.Header(h => + { + h.Cell().Text("#").Bold(); + h.Cell().Text("Alumno").Bold(); + }); + int idx = 1; + foreach (var a in alumnos) + { + table.Cell().Text(idx++.ToString()); + table.Cell().Text(GetString(a, "Alumnos")); + } + }); + }); + } + + private static void ComposeCotizacionCCHeader(IContainer container, string empresa, string numProp) + { + container.Row(row => + { + row.RelativeItem().Column(col => + { + col.Item().Text("Instituto Chileno Norteamericano").FontSize(12).Bold(); + col.Item().Text("Cotización Curso Cerrado").FontSize(8); + }); + row.ConstantItem(140).AlignRight().Column(col => + { + col.Item().Text($"Cotización N° {numProp}").FontSize(14).Bold(); + }); + }); + } + + private static void ComposeCotizacionCCContent(IContainer container, Dictionary header, + IEnumerable> detalle) + { + container.Column(col => + { + col.Item().PaddingTop(15).Table(table => + { + table.ColumnsDefinition(c => { c.ConstantColumn(100); 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")); + table.Cell().Text("Vigencia:").Bold(); + table.Cell().Text(GetString(header, "VALIDEZ")); + }); + + 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("#").Bold(); + h.Cell().Text("Curso").Bold(); + h.Cell().Text("Jornada").Bold(); + h.Cell().Text("Valor").Bold(); + }); + + int idx = 1; + foreach (var d in detalle) + { + table.Cell().Text(idx++.ToString()); + table.Cell().Text(GetString(d, "CURSO")); + table.Cell().Text(GetString(d, "JORNADA")); + table.Cell().Text("$" + GetString(d, "VALOR CURSO")); + } + }); + }); + } + + private static void ComposePlanCentralHeader(IContainer container, string empresa, string numCotizacion) + { + container.Row(row => + { + row.RelativeItem().Column(col => + { + col.Item().Text("Instituto Chileno Norteamericano").FontSize(12).Bold(); + col.Item().Text("Cotización Plan Central").FontSize(8); + }); + row.ConstantItem(140).AlignRight().Column(col => + { + col.Item().Text($"Cotización N° {numCotizacion}").FontSize(14).Bold(); + }); + }); + } + + private static void ComposePlanCentralContent(IContainer container, Dictionary header, + IEnumerable> detalle) + { + container.Column(col => + { + col.Item().PaddingTop(15).Table(table => + { + table.ColumnsDefinition(c => { c.ConstantColumn(110); c.RelativeColumn(); }); + table.Cell().Text("Empresa:").Bold(); + table.Cell().Text(GetString(header, "Razon Social")); + table.Cell().Text("RUT:").Bold(); + table.Cell().Text(GetString(header, "R.U.T.")); + table.Cell().Text("Ejecutivo:").Bold(); + table.Cell().Text(GetString(header, "Ejecutivo")); + table.Cell().Text("Total:").Bold(); + table.Cell().Text("$" + GetString(header, "Total con descuento")); + table.Cell().Text("Vigencia:").Bold(); + table.Cell().Text(GetString(header, "Vigencia")); + }); + + col.Item().PaddingTop(15).Table(table => + { + table.ColumnsDefinition(c => + { + c.ConstantColumn(25); + c.RelativeColumn(); + c.ConstantColumn(50); + c.ConstantColumn(60); + c.ConstantColumn(60); + }); + + table.Header(h => + { + h.Cell().Text("#").Bold(); + h.Cell().Text("Curso").Bold(); + h.Cell().Text("Periodo").Bold(); + h.Cell().Text("Inicio").Bold(); + h.Cell().Text("Valor").Bold(); + }); + + int idx = 1; + foreach (var d in detalle) + { + table.Cell().Text(idx++.ToString()); + table.Cell().Text(GetString(d, "NombreCurso")); + table.Cell().Text(GetString(d, "Periodo")); + table.Cell().Text(GetString(d, "FechaInicio")); + table.Cell().Text("$" + GetString(d, "ValorCurso")); + } + }); + }); + } +}