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] Login endpoint funcional
|
||||
- [x] Claims → cookies HttpOnly
|
||||
- [ ] **1.7 Reportes QuestPDF** (~2-3 sem)
|
||||
- [ ] ReportService.cs (base)
|
||||
- [ ] 17 reportes (Contrato, Cotización, Arqueo, etc.)
|
||||
- [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.)
|
||||
- [ ] Dockerfile Backend
|
||||
|
||||
### 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 | 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));
|
||||
builder.Services.AddScoped<InformeService>(sp =>
|
||||
new InformeService(connectionString));
|
||||
builder.Services.AddScoped<ReportService>();
|
||||
|
||||
var jwtSecret = builder.Configuration["Jwt:Secret"] ?? "default-dev-secret-change-in-production";
|
||||
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