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
This commit is contained in:
2026-07-08 10:28:47 -04:00
parent c1e35445f6
commit 202a59326b
6 changed files with 363 additions and 45 deletions
+9 -8
View File
@@ -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 |
| | | | |
| | | | |
@@ -18,29 +18,33 @@ public class EmpresaReportController : ControllerBase
[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");
}
=> File(await _empresaReportService.ContratoAbiertoPdfAsync(id), "application/pdf", $"contrato_abierto_{id}.pdf");
[HttpGet("contrato-abierto-sin-sence/{id}")]
public async Task<IActionResult> ContratoAbiertoSinSencePdf(int id)
=> File(await _empresaReportService.ContratoAbiertoSinSencePdfAsync(id), "application/pdf", $"caemp_{id}.pdf");
[HttpGet("contrato-abierto-con-sence/{id}")]
public async Task<IActionResult> ContratoAbiertoConSencePdf(int id)
=> File(await _empresaReportService.ContratoAbiertoConSencePdfAsync(id), "application/pdf", $"caempsnc_{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");
}
=> File(await _empresaReportService.ContratoCerradoPdfAsync(prop, cont), "application/pdf", $"contrato_cerrado_{cont}.pdf");
[HttpGet("contrato-cerrado-descuento/{prop}/{cont}")]
public async Task<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> PropuestaComercialPdf(int prop, int cotz, int cont)
=> File(await _empresaReportService.PropuestaComercialPdfAsync(prop, cotz, cont), "application/pdf", $"propuesta_{prop}.pdf");
}
@@ -18,30 +18,30 @@ public class ReportController : ControllerBase
[HttpGet("contrato/{id}")]
public async Task<IActionResult> 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<IActionResult> ContratoBlackPdf(int id)
=> File(await _reportService.GenerarContratoBlackPdfAsync(id), "application/pdf", $"contrato_black_{id}.pdf");
[HttpGet("cotizacion/{id}")]
public async Task<IActionResult> 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<IActionResult> AnexoPdf(int contratoId, int anexoId)
=> File(await _reportService.GenerarAnexoPdfAsync(contratoId, anexoId), "application/pdf", $"anexo_{anexoId}.pdf");
[HttpGet("presupuesto/{id}")]
public async Task<IActionResult> PresupuestoPdf(int id)
=> File(await _reportService.GenerarPresupuestoPdfAsync(id), "application/pdf", $"presupuesto_{id}.pdf");
[HttpPost("arqueo")]
public async Task<IActionResult> ArqueoPdf(
[FromQuery] string usuario,
[FromQuery] DateTime fecha,
[FromQuery] string usuario, [FromQuery] DateTime fecha,
[FromBody] List<Dictionary<string, object>> 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");
}
+2 -1
View File
@@ -45,7 +45,8 @@ builder.Services.AddScoped<AlumnoService>(sp =>
builder.Services.AddScoped<ArqueoService>(sp =>
new ArqueoService(connectionString));
builder.Services.AddScoped<InformeService>();
builder.Services.AddScoped<ReportService>();
builder.Services.AddScoped<ReportService>(sp =>
new ReportService(sp.GetRequiredService<ContratoService>(), sp.GetRequiredService<CotizacionService>(), connectionString));
builder.Services.AddScoped<EmpresaReportService>(sp =>
new EmpresaReportService(sp.GetRequiredService<IInformeRepository>(), connectionString));
@@ -287,4 +287,145 @@ public class EmpresaReportService
});
});
}
// Variantes CAEMP/CAEMPSNC/CAEMPV2 — mismo SP, layout con/sin SENCE
public async Task<byte[]> 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<byte[]> 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<byte[]> ContratoCerradoGeneralPdfAsync(int propuestaId, int contratoId)
=> await ContratoCerradoPdfAsync(propuestaId, contratoId);
public async Task<byte[]> 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<byte[]> 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();
}
}
+172 -1
View File
@@ -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<IEnumerable<Dictionary<string, object>>> 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<string, object>)(IDictionary<string, object>)r!);
}
public async Task<byte[]> GenerarContratoPdfAsync(int contratoId)
@@ -287,4 +298,164 @@ public class ReportService
});
});
}
public async Task<byte[]> 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<string, object> row,
IEnumerable<Dictionary<string, object>> programas, IEnumerable<Dictionary<string, object>> jornadas,
IEnumerable<Dictionary<string, object>> 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<byte[]> 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<string, object> 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<byte[]> 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<string, object> 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() ?? "");
}
});
});
}
}