diff --git a/ROADMAP.md b/ROADMAP.md index eb1bf8e..b1b01c7 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -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 | | | | | | | | | | | diff --git a/backend/src/Ventas.API/Controllers/ReportController.cs b/backend/src/Ventas.API/Controllers/ReportController.cs new file mode 100644 index 0000000..d9b111e --- /dev/null +++ b/backend/src/Ventas.API/Controllers/ReportController.cs @@ -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 ContratoPdf(int id) + { + var pdf = await _reportService.GenerarContratoPdfAsync(id); + return File(pdf, "application/pdf", $"contrato_{id}.pdf"); + } + + [HttpGet("cotizacion/{id}")] + public async Task CotizacionPdf(int id) + { + var pdf = await _reportService.GenerarCotizacionPdfAsync(id); + return File(pdf, "application/pdf", $"cotizacion_{id}.pdf"); + } + + [HttpPost("arqueo")] + public async Task ArqueoPdf( + [FromQuery] string usuario, + [FromQuery] DateTime fecha, + [FromBody] List> 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"); + } +} diff --git a/backend/src/Ventas.API/Program.cs b/backend/src/Ventas.API/Program.cs index c828976..2df0b91 100644 --- a/backend/src/Ventas.API/Program.cs +++ b/backend/src/Ventas.API/Program.cs @@ -34,6 +34,7 @@ builder.Services.AddScoped(sp => new AlumnoService(connectionString)); builder.Services.AddScoped(sp => new InformeService(connectionString)); +builder.Services.AddScoped(); var jwtSecret = builder.Configuration["Jwt:Secret"] ?? "default-dev-secret-change-in-production"; var jwtExpiration = int.Parse(builder.Configuration["Jwt:ExpirationMinutes"] ?? "30"); diff --git a/backend/src/Ventas.Services/ReportService.cs b/backend/src/Ventas.Services/ReportService.cs new file mode 100644 index 0000000..20fc72e --- /dev/null +++ b/backend/src/Ventas.Services/ReportService.cs @@ -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 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 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> 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 GenerarArqueoPdfAsync(string usuario, DateTime fecha, IEnumerable> 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> 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(); + }); + }); + } +}