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
This commit is contained in:
2026-07-08 09:27:26 -04:00
parent e1367fc23a
commit df028eb4dd
4 changed files with 488 additions and 1 deletions
+3 -1
View File
@@ -60,7 +60,9 @@
- [x] Claims → cookies HttpOnly - [x] Claims → cookies HttpOnly
- [x] **1.7 Reportes QuestPDF** (~2-3 sem) - [x] **1.7 Reportes QuestPDF** (~2-3 sem)
- [x] ReportService.cs (Contrato, Cotización, Arqueo en QuestPDF) - [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 - [ ] Dockerfile Backend
### FASE 2: SERVICES EXTERNOS API (~2-3 semanas) ### FASE 2: SERVICES EXTERNOS API (~2-3 semanas)
@@ -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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> CotizacionPlanCentralPdf(int id)
{
var pdf = await _empresaReportService.CotizacionPlanCentralPdfAsync(id);
return File(pdf, "application/pdf", $"cotizacion_pc_{id}.pdf");
}
}
+2
View File
@@ -35,6 +35,8 @@ builder.Services.AddScoped<AlumnoService>(sp =>
builder.Services.AddScoped<InformeService>(sp => builder.Services.AddScoped<InformeService>(sp =>
new InformeService(connectionString)); new InformeService(connectionString));
builder.Services.AddScoped<ReportService>(); builder.Services.AddScoped<ReportService>();
builder.Services.AddScoped<EmpresaReportService>(sp =>
new EmpresaReportService(connectionString));
var jwtSecret = builder.Configuration["Jwt:Secret"] ?? "default-dev-secret-change-in-production"; var jwtSecret = builder.Configuration["Jwt:Secret"] ?? "default-dev-secret-change-in-production";
var jwtExpiration = int.Parse(builder.Configuration["Jwt:ExpirationMinutes"] ?? "30"); var jwtExpiration = int.Parse(builder.Configuration["Jwt:ExpirationMinutes"] ?? "30");
@@ -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<IEnumerable<Dictionary<string, object>>> 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<string, object>)(IDictionary<string, object>)r!);
}
private async Task<IEnumerable<Dictionary<string, object>>> 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<string, object>)(IDictionary<string, object>)r!);
}
private static string GetString(Dictionary<string, object> row, string key) =>
row.GetValueOrDefault(key)?.ToString() ?? "";
public async Task<byte[]> 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<byte[]> 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<byte[]> 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<byte[]> 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<string, object> header,
IEnumerable<Dictionary<string, object>> cursos, IEnumerable<Dictionary<string, object>> horarios,
IEnumerable<Dictionary<string, object>> 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<string, object> header,
IEnumerable<Dictionary<string, object>> cursos, IEnumerable<Dictionary<string, object>> 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<string, object> header,
IEnumerable<Dictionary<string, object>> 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<string, object> header,
IEnumerable<Dictionary<string, object>> 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"));
}
});
});
}
}