Files
GestionIntegralWeb/Backend/GestionIntegral.Api/Controllers/Reportes/PdfTemplates/DistribucionSuscripcionesDocument.cs
dmolinari 1a288fcfa5 Feat: Implementa auditoría de envíos masivos y mejora de procesos
Se introduce un sistema completo para auditar los envíos masivos de correos durante el cierre mensual y se refactoriza la interfaz de usuario de procesos para una mayor claridad y escalabilidad. Además, se mejora la lógica de negocio para la gestión de bajas de suscripciones.

###  Nuevas Características

- **Auditoría de Envíos Masivos (Cierre Mensual):**
    - Se crea una nueva tabla `com_LotesDeEnvio` para registrar cada ejecución del proceso de facturación mensual.
    - El `FacturacionService` ahora crea un "lote" al iniciar el cierre, registra el resultado de cada envío de email individual asociándolo a dicho lote, y actualiza las estadísticas finales (enviados, fallidos) al terminar.
    - Se implementa un nuevo `LotesEnvioController` con un endpoint para consultar los detalles de cualquier lote de envío histórico.

### 🔄 Refactorización y Mejoras

- **Rediseño de la Página de Procesos:**
    - La antigua página "Facturación" se renombra a `CierreYProcesosPage` y se rediseña completamente utilizando una interfaz de Pestañas (Tabs).
    - **Pestaña "Procesos Mensuales":** Aisla las acciones principales (Generar Cierre, Archivo de Débito, Procesar Respuesta), mostrando un resumen del resultado del último envío.
    - **Pestaña "Historial de Cierres":** Muestra una tabla con todos los lotes de envío pasados y permite al usuario ver los detalles de cada uno en un modal.

- **Filtros para el Historial de Cierres:**
    - Se añaden filtros por Mes y Año a la pestaña de "Historial de Cierres", permitiendo al usuario buscar y auditar procesos pasados de manera eficiente. El filtrado se realiza en el backend para un rendimiento óptimo.

- **Lógica de `FechaFin` Obligatoria para Bajas:**
    - Se implementa una regla de negocio crucial: al cambiar el estado de una suscripción a "Pausada" o "Cancelada", ahora es obligatorio establecer una `FechaFin`.
    - **Frontend:** El modal de suscripciones ahora gestiona esto automáticamente, haciendo el campo `FechaFin` requerido y visible según el estado seleccionado.
    - **Backend:** Se añade una validación en `SuscripcionService` como segunda capa de seguridad para garantizar la integridad de los datos.

### 🐛 Corrección de Errores

- **Reporte de Distribución:** Se corrigió un bug en la generación del PDF donde la columna de fecha no mostraba la "Fecha de Baja" para las suscripciones finalizadas. Ahora se muestra la fecha correcta según la sección (Altas o Bajas).
- **Errores de Compilación y Dependencias:** Se solucionaron varios errores de compilación en el backend, principalmente relacionados con la falta de registro de los nuevos repositorios (`ILoteDeEnvioRepository`, `IEmailLogService`, etc.) en el contenedor de inyección de dependencias (`Program.cs`).
- **Errores de Tipado en Frontend:** Se corrigieron múltiples errores de TypeScript en `CierreYProcesosPage` debidos a la inconsistencia entre `PascalCase` (C#) y `camelCase` (JSON/TypeScript), asegurando un mapeo correcto de los datos de la API.
2025-08-11 11:14:03 -03:00

152 lines
7.0 KiB
C#

using GestionIntegral.Api.Dtos.Reportes.ViewModels;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates
{
public class DistribucionSuscripcionesDocument : IDocument
{
public DistribucionSuscripcionesViewModel Model { get; }
public DistribucionSuscripcionesDocument(DistribucionSuscripcionesViewModel model)
{
Model = model;
}
public DocumentMetadata GetMetadata() => DocumentMetadata.Default;
public void Compose(IDocumentContainer container)
{
container.Page(page =>
{
page.Margin(1, Unit.Centimetre);
page.DefaultTextStyle(x => x.FontFamily("Arial").FontSize(9));
page.Header().Element(ComposeHeader);
page.Content().Element(ComposeContent);
page.Footer().AlignCenter().Text(x => { x.Span("Página "); x.CurrentPageNumber(); });
});
}
void ComposeHeader(IContainer container)
{
container.Column(column =>
{
column.Item().Row(row =>
{
row.RelativeItem().Column(col =>
{
col.Item().Text("Reporte de Distribución de Suscripciones").SemiBold().FontSize(14);
col.Item().Text($"Período: {Model.FechaDesde} al {Model.FechaHasta}").FontSize(11);
});
row.ConstantItem(150).AlignRight().Text($"Generado: {Model.FechaGeneracion}");
});
column.Item().PaddingTop(5).BorderBottom(1).BorderColor(Colors.Grey.Lighten2);
});
}
void ComposeContent(IContainer container)
{
container.PaddingTop(10).Column(column =>
{
column.Spacing(20); // Espacio entre elementos principales (sección de altas y sección de bajas)
// --- Sección 1: Altas y Activas ---
column.Item().Column(colAltas =>
{
colAltas.Item().Text("Altas y Suscripciones Activas en el Período").Bold().FontSize(14).Underline();
colAltas.Item().PaddingBottom(10).Text("Listado de suscriptores que deben recibir entregas en el período seleccionado.");
if (!Model.DatosAgrupadosAltas.Any())
{
colAltas.Item().PaddingTop(10).Text("No se encontraron suscripciones activas para este período.").Italic();
}
else
{
foreach (var empresa in Model.DatosAgrupadosAltas)
{
colAltas.Item().Element(c => ComposeTablaEmpresa(c, empresa, esBaja: false));
}
}
});
// --- Sección 2: Bajas ---
if (Model.DatosAgrupadosBajas.Any())
{
column.Item().PageBreak(); // Salto de página para separar las secciones
column.Item().Column(colBajas =>
{
colBajas.Item().Text("Bajas de Suscripciones en el Período").Bold().FontSize(14).Underline().FontColor(Colors.Red.Medium);
colBajas.Item().PaddingBottom(10).Text("Listado de suscriptores cuya suscripción finalizó. NO se les debe entregar a partir de su 'Fecha de Baja'.");
foreach (var empresa in Model.DatosAgrupadosBajas)
{
colBajas.Item().Element(c => ComposeTablaEmpresa(c, empresa, esBaja: true));
}
});
}
});
}
void ComposeTablaEmpresa(IContainer container, GrupoEmpresa empresa, bool esBaja)
{
container.Column(column =>
{
// Cabecera de la EMPRESA (ej. EL DIA)
column.Item().Background(Colors.Grey.Lighten2).Padding(5).Text(empresa.NombreEmpresa).Bold().FontSize(12);
// Contenedor para las tablas de las publicaciones de esta empresa
column.Item().PaddingTop(5).Column(colPub =>
{
colPub.Spacing(10); // Espacio entre cada tabla de publicación
foreach (var publicacion in empresa.Publicaciones)
{
colPub.Item().Element(c => ComposeTablaPublicacion(c, publicacion, esBaja));
}
});
});
}
void ComposeTablaPublicacion(IContainer container, GrupoPublicacion publicacion, bool esBaja)
{
// Se envuelve la tabla en una columna para poder ponerle un título simple arriba.
container.Column(column =>
{
column.Item().PaddingLeft(2).PaddingBottom(2).Text(publicacion.NombrePublicacion).SemiBold().FontSize(10);
column.Item().Table(table =>
{
table.ColumnsDefinition(columns =>
{
columns.RelativeColumn(2.5f); // Nombre
columns.RelativeColumn(3); // Dirección
columns.RelativeColumn(1.5f); // Teléfono
columns.ConstantColumn(65); // Fecha Inicio / Baja
columns.RelativeColumn(1.5f); // Días
columns.RelativeColumn(2.5f); // Observaciones
});
table.Header(header =>
{
header.Cell().BorderBottom(1).Padding(2).Text("Suscriptor").SemiBold();
header.Cell().BorderBottom(1).Padding(2).Text("Dirección").SemiBold();
header.Cell().BorderBottom(1).Padding(2).Text("Teléfono").SemiBold();
header.Cell().BorderBottom(1).Padding(2).Text(esBaja ? "Fecha de Baja" : "Fecha Inicio").SemiBold();
header.Cell().BorderBottom(1).Padding(2).Text("Días Entrega").SemiBold();
header.Cell().BorderBottom(1).Padding(2).Text("Observaciones").SemiBold();
});
foreach (var item in publicacion.Suscripciones)
{
table.Cell().Padding(2).Text(item.NombreSuscriptor);
table.Cell().Padding(2).Text(item.Direccion);
table.Cell().Padding(2).Text(item.Telefono ?? "-");
var fecha = esBaja ? item.FechaFin : item.FechaInicio;
table.Cell().Padding(2).Text(fecha?.ToString("dd/MM/yyyy") ?? "-");
table.Cell().Padding(2).Text(item.DiasEntrega);
table.Cell().Padding(2).Text(item.Observaciones ?? "-");
}
});
});
}
}
}