feat: questpdf reports for contrato, cotizacion, arqueo

- ReportService with 3 QuestPDF report generators
- Contrato PDF with header, student info, and course table
- Cotización PDF with customer info, total, and detail table
- Arqueo PDF with cashier summary and payment method breakdowns
- ReportController with endpoints for PDF download
- Registered in DI
This commit is contained in:
2026-07-08 09:22:25 -04:00
parent 2dfe7daa2c
commit e1367fc23a
4 changed files with 343 additions and 4 deletions
+5 -4
View File
@@ -58,9 +58,9 @@
- [x] JwtService.cs (generación de tokens) - [x] JwtService.cs (generación de tokens)
- [x] Login endpoint funcional - [x] Login endpoint funcional
- [x] Claims → cookies HttpOnly - [x] Claims → cookies HttpOnly
- [ ] **1.7 Reportes QuestPDF** (~2-3 sem) - [x] **1.7 Reportes QuestPDF** (~2-3 sem)
- [ ] ReportService.cs (base) - [x] ReportService.cs (Contrato, Cotización, Arqueo en QuestPDF)
- [ ] 17 reportes (Contrato, Cotización, Arqueo, etc.) - [ ] 14 reportes restantes (Anexo, ContratoBlack, Presupuesto, CAEMP, CC_EMP, etc.)
- [ ] Dockerfile Backend - [ ] Dockerfile Backend
### FASE 2: SERVICES EXTERNOS API (~2-3 semanas) ### FASE 2: SERVICES EXTERNOS API (~2-3 semanas)
@@ -110,7 +110,8 @@
|-------|------|-------------|----------------| |-------|------|-------------|----------------|
| 2026-07-07 | F1 | Entities (25), Enums, DTOs, Interfaces, DbContext, LeadRepository + LeadQueryRepository, DI, JWT, appsettings | Resto repos + Controllers | | 2026-07-07 | F1 | Entities (25), Enums, DTOs, Interfaces, DbContext, LeadRepository + LeadQueryRepository, DI, JWT, appsettings | Resto repos + Controllers |
| 2026-07-07 | F1 | LeadService, UsuarioService, JwtService, AuthController, LeadController | Resto Services + Controllers | | 2026-07-07 | F1 | LeadService, UsuarioService, JwtService, AuthController, LeadController | Resto Services + Controllers |
| 2026-07-07 | F1 | ContratoService, CotizacionService, AlumnoService, InformeService + Controllers (Contrato, Cotizacion, Alumno, Informe) | Frontend setup | | 2026-07-07 | F1 | ContratoService, CotizacionService, AlumnoService, InformeService + Controllers | Reportes QuestPDF |
| 2026-07-07 | F1.7 | ReportService (Contrato, Cotización, Arqueo PDF con QuestPDF) + ReportController | Resto reportes + ServicesExternos o Frontend |
| | | | | | | | | |
| | | | | | | | | |
@@ -0,0 +1,47 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Ventas.Services;
namespace Ventas.API.Controllers;
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class ReportController : ControllerBase
{
private readonly ReportService _reportService;
public ReportController(ReportService reportService)
{
_reportService = reportService;
}
[HttpGet("contrato/{id}")]
public async Task<IActionResult> ContratoPdf(int id)
{
var pdf = await _reportService.GenerarContratoPdfAsync(id);
return File(pdf, "application/pdf", $"contrato_{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");
}
[HttpPost("arqueo")]
public async Task<IActionResult> ArqueoPdf(
[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");
}
}
+1
View File
@@ -34,6 +34,7 @@ builder.Services.AddScoped<AlumnoService>(sp =>
new AlumnoService(connectionString)); new AlumnoService(connectionString));
builder.Services.AddScoped<InformeService>(sp => builder.Services.AddScoped<InformeService>(sp =>
new InformeService(connectionString)); new InformeService(connectionString));
builder.Services.AddScoped<ReportService>();
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,290 @@
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace Ventas.Services;
public class ReportService
{
private readonly ContratoService _contratoService;
private readonly CotizacionService _cotizacionService;
public ReportService(ContratoService contratoService, CotizacionService cotizacionService)
{
_contratoService = contratoService;
_cotizacionService = cotizacionService;
}
public async Task<byte[]> GenerarContratoPdfAsync(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 [];
var numeroContrato = row.GetValueOrDefault("NumeroContrato")?.ToString() ?? contratoId.ToString();
var alumno = row.GetValueOrDefault("Alumno")?.ToString() ?? "";
var rut = row.GetValueOrDefault("Rut")?.ToString() ?? "";
var programa = string.Join(", ", programas.Select(p => p.GetValueOrDefault("Nombre")?.ToString()));
var sede = string.Join(", ", sedes.Select(s => s.GetValueOrDefault("Nombre")?.ToString()));
var jornada = string.Join(", ", jornadas.Select(j => j.GetValueOrDefault("Nombre")?.ToString()));
var fecha = row.GetValueOrDefault("Fecha")?.ToString() ?? "";
return Document.Create(container =>
{
container.Page(page =>
{
page.Size(PageSizes.A4);
page.Margin(57);
page.DefaultTextStyle(x => x.FontSize(10));
page.Header().Element(c => ComposeContratoHeader(c, numeroContrato));
page.Content().Element(c => ComposeContratoContent(c, alumno, rut, programa, sede, jornada, fecha));
page.Footer().AlignCenter().Text(text =>
{
text.Span("Página ");
text.CurrentPageNumber();
});
});
}).GeneratePdf();
}
private void ComposeContratoHeader(IContainer container, string numero)
{
container.Row(row =>
{
row.RelativeItem();
row.ConstantItem(120).AlignRight().Column(col =>
{
col.Item().Text($"Contrato N° {numero}").FontSize(16).Bold();
});
});
}
private void ComposeContratoContent(IContainer container, string alumno, string rut, string programa, string sede, string jornada, string fecha)
{
container.Column(col =>
{
col.Item().PaddingTop(20).Table(table =>
{
table.ColumnsDefinition(c =>
{
c.ConstantColumn(120);
c.RelativeColumn();
});
table.Cell().Text("Alumno:").Bold();
table.Cell().Text(alumno);
table.Cell().Text("RUT:").Bold();
table.Cell().Text(rut);
table.Cell().Text("Programa:").Bold();
table.Cell().Text(programa);
table.Cell().Text("Sede:").Bold();
table.Cell().Text(sede);
table.Cell().Text("Jornada:").Bold();
table.Cell().Text(jornada);
table.Cell().Text("Fecha:").Bold();
table.Cell().Text(fecha);
});
col.Item().PaddingTop(20).Table(table =>
{
table.ColumnsDefinition(c =>
{
c.ConstantColumn(40);
c.RelativeColumn();
c.ConstantColumn(80);
c.ConstantColumn(80);
});
table.Header(header =>
{
header.Cell().Text("#").Bold();
header.Cell().Text("Curso").Bold();
header.Cell().Text("Horas").Bold();
header.Cell().Text("Valor").Bold();
});
});
});
}
public async Task<byte[]> GenerarCotizacionPdfAsync(int cotizacionId)
{
var data = await _cotizacionService.BuscarInfoAsync("NUMERO", cotizacionId.ToString());
var detalle = await _cotizacionService.DetalleAsync(cotizacionId);
var row = data.FirstOrDefault();
if (row == null) return [];
var numero = cotizacionId.ToString();
var nombre = row.GetValueOrDefault("NombreLead")?.ToString() ?? "";
var total = row.GetValueOrDefault("Monto")?.ToString() ?? "0";
var fecha = row.GetValueOrDefault("Fecha")?.ToString() ?? "";
return Document.Create(container =>
{
container.Page(page =>
{
page.Size(PageSizes.A4);
page.Margin(57);
page.Header().Element(c => ComposeCotizacionHeader(c, numero));
page.Content().Element(c => ComposeCotizacionContent(c, nombre, total, fecha, detalle));
page.Footer().AlignCenter().Text(text =>
{
text.Span("Página ");
text.CurrentPageNumber();
});
});
}).GeneratePdf();
}
private void ComposeCotizacionHeader(IContainer container, string numero)
{
container.Row(row =>
{
row.RelativeItem();
row.ConstantItem(140).AlignRight().Column(col =>
{
col.Item().Text($"Cotización N° {numero}").FontSize(16).Bold();
col.Item().Text("Instituto Chileno Norteamericano").FontSize(9);
});
});
}
private void ComposeCotizacionContent(IContainer container, string nombre, string total, string fecha, IEnumerable<Dictionary<string, object>> detalle)
{
container.Column(col =>
{
col.Item().PaddingTop(20).Table(table =>
{
table.ColumnsDefinition(c =>
{
c.ConstantColumn(100);
c.RelativeColumn();
});
table.Cell().Text("Cliente:").Bold();
table.Cell().Text(nombre);
table.Cell().Text("Fecha:").Bold();
table.Cell().Text(fecha);
table.Cell().Text("Total:").Bold();
table.Cell().Text($"${total}");
});
col.Item().PaddingTop(20).Table(table =>
{
table.ColumnsDefinition(c =>
{
c.ConstantColumn(40);
c.RelativeColumn();
c.ConstantColumn(80);
});
table.Header(header =>
{
header.Cell().Text("#").Bold();
header.Cell().Text("Curso").Bold();
header.Cell().Text("Valor").Bold();
});
int index = 1;
foreach (var d in detalle)
{
table.Cell().Text(index++.ToString());
table.Cell().Text(d.GetValueOrDefault("Curso")?.ToString() ?? "");
table.Cell().Text(d.GetValueOrDefault("Valor")?.ToString() ?? "0");
}
});
});
}
public async Task<byte[]> GenerarArqueoPdfAsync(string usuario, DateTime fecha, IEnumerable<Dictionary<string, object>> ingresos, int totalCredito, int totalDebito, int totalIntl, int totalEstado, int totalGeneral)
{
return Document.Create(container =>
{
container.Page(page =>
{
page.Size(PageSizes.A4);
page.Margin(57);
page.Header().Element(c => ComposeArqueoHeader(c, usuario, fecha));
page.Content().Element(c => ComposeArqueoContent(c, ingresos, totalCredito, totalDebito, totalIntl, totalEstado, totalGeneral));
page.Footer().AlignCenter().Text(text =>
{
text.Span("Página ");
text.CurrentPageNumber();
});
});
}).GeneratePdf();
}
private void ComposeArqueoHeader(IContainer container, string usuario, DateTime fecha)
{
container.Row(row =>
{
row.RelativeItem();
row.ConstantItem(150).AlignRight().Column(col =>
{
col.Item().Text("Arqueo de Caja").FontSize(16).Bold();
col.Item().Text($"Cajero: {usuario}").FontSize(10);
col.Item().Text($"Fecha: {fecha:dd-MM-yyyy}").FontSize(10);
});
});
}
private void ComposeArqueoContent(IContainer container, IEnumerable<Dictionary<string, object>> ingresos, int totalCredito, int totalDebito, int totalIntl, int totalEstado, int totalGeneral)
{
container.Column(col =>
{
col.Item().PaddingTop(20).Table(table =>
{
table.ColumnsDefinition(c =>
{
c.RelativeColumn();
c.ConstantColumn(80);
c.ConstantColumn(80);
c.ConstantColumn(80);
c.ConstantColumn(80);
});
table.Header(header =>
{
header.Cell().Text("Cajero").Bold();
header.Cell().Text("Crédito").Bold();
header.Cell().Text("Débito").Bold();
header.Cell().Text("Intl").Bold();
header.Cell().Text("Total").Bold();
});
foreach (var ing in ingresos)
{
table.Cell().Text(ing.GetValueOrDefault("Cajero")?.ToString() ?? "");
table.Cell().Text(ing.GetValueOrDefault("Credito")?.ToString() ?? "0");
table.Cell().Text(ing.GetValueOrDefault("Debito")?.ToString() ?? "0");
table.Cell().Text(ing.GetValueOrDefault("Internacional")?.ToString() ?? "0");
table.Cell().Text(ing.GetValueOrDefault("Total")?.ToString() ?? "0");
}
});
col.Item().PaddingTop(20).AlignRight().Column(total =>
{
total.Item().Text($"Crédito: ${totalCredito:N0}").Bold();
total.Item().Text($"Débito: ${totalDebito:N0}").Bold();
total.Item().Text($"Internacional: ${totalIntl:N0}").Bold();
total.Item().Text($"Estado: ${totalEstado:N0}").Bold();
total.Item().Text($"Total General: ${totalGeneral:N0}").FontSize(14).Bold();
});
});
}
}