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:
+5
-4
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user