Compare commits
8 Commits
9f8d577265
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| c27dc2a0ba | |||
| 24b1c07342 | |||
| cb64bbc1f5 | |||
| 057310ca47 | |||
| e95c851e5b | |||
| 038faefd35 | |||
| da50c052f1 | |||
| 5781713b13 |
@@ -26,12 +26,6 @@ jobs:
|
|||||||
set -e
|
set -e
|
||||||
echo "--- INICIO DEL DESPLIEGUE OPTIMIZADO ---"
|
echo "--- INICIO DEL DESPLIEGUE OPTIMIZADO ---"
|
||||||
|
|
||||||
# --- Asegurar que el Stack de la Base de Datos esté corriendo ---
|
|
||||||
echo "Asegurando que el stack de la base de datos esté activo..."
|
|
||||||
cd /opt/shared-services/database
|
|
||||||
# El comando 'up -d' es idempotente. Si ya está corriendo, no hace nada.
|
|
||||||
docker compose up -d
|
|
||||||
|
|
||||||
# 1. Preparar entorno
|
# 1. Preparar entorno
|
||||||
TEMP_DIR=$(mktemp -d)
|
TEMP_DIR=$(mktemp -d)
|
||||||
REPO_OWNER="dmolinari"
|
REPO_OWNER="dmolinari"
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
# ================================================
|
|
||||||
# VARIABLES DE ENTORNO PARA LA CONFIGURACIÓN DE CORREO
|
|
||||||
# ================================================
|
|
||||||
# El separador de doble guion bajo (__) se usa para mapear la jerarquía del JSON.
|
|
||||||
# MailSettings:SmtpHost se convierte en MailSettings__SmtpHost
|
|
||||||
|
|
||||||
MailSettings__SmtpHost="192.168.5.201"
|
|
||||||
MailSettings__SmtpPort=587
|
|
||||||
MailSettings__SenderName="Club - Diario El Día"
|
|
||||||
MailSettings__SenderEmail="alertas@eldia.com"
|
|
||||||
MailSettings__SmtpUser="alertas@eldia.com"
|
|
||||||
MailSettings__SmtpPass="@Alertas713550@"
|
|
||||||
@@ -75,12 +75,13 @@ namespace GestionIntegral.Api.Controllers.Suscripciones
|
|||||||
int anio, int mes,
|
int anio, int mes,
|
||||||
[FromQuery] string? nombreSuscriptor,
|
[FromQuery] string? nombreSuscriptor,
|
||||||
[FromQuery] string? estadoPago,
|
[FromQuery] string? estadoPago,
|
||||||
[FromQuery] string? estadoFacturacion)
|
[FromQuery] string? estadoFacturacion,
|
||||||
|
[FromQuery] string? tipoFactura)
|
||||||
{
|
{
|
||||||
if (!TienePermiso(PermisoGestionarFacturacion)) return Forbid();
|
if (!TienePermiso(PermisoGestionarFacturacion)) return Forbid();
|
||||||
if (anio < 2020 || mes < 1 || mes > 12) return BadRequest(new { message = "El período no es válido." });
|
if (anio < 2020 || mes < 1 || mes > 12) return BadRequest(new { message = "El período no es válido." });
|
||||||
|
|
||||||
var resumenes = await _facturacionService.ObtenerResumenesDeCuentaPorPeriodo(anio, mes, nombreSuscriptor, estadoPago, estadoFacturacion);
|
var resumenes = await _facturacionService.ObtenerResumenesDeCuentaPorPeriodo(anio, mes, nombreSuscriptor, estadoPago, estadoFacturacion, tipoFactura);
|
||||||
return Ok(resumenes);
|
return Ok(resumenes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -59,10 +59,15 @@ namespace GestionIntegral.Api.Data.Repositories.Suscripciones
|
|||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(transaction), "La transacción o su conexión no pueden ser nulas.");
|
throw new ArgumentNullException(nameof(transaction), "La transacción o su conexión no pueden ser nulas.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const string sqlInsert = @"
|
const string sqlInsert = @"
|
||||||
INSERT INTO dbo.susc_Facturas (IdSuscriptor, Periodo, FechaEmision, FechaVencimiento, ImporteBruto, DescuentoAplicado, ImporteFinal, EstadoPago, EstadoFacturacion)
|
INSERT INTO dbo.susc_Facturas
|
||||||
|
(IdSuscriptor, Periodo, FechaEmision, FechaVencimiento, ImporteBruto,
|
||||||
|
DescuentoAplicado, ImporteFinal, EstadoPago, EstadoFacturacion, TipoFactura)
|
||||||
OUTPUT INSERTED.*
|
OUTPUT INSERTED.*
|
||||||
VALUES (@IdSuscriptor, @Periodo, @FechaEmision, @FechaVencimiento, @ImporteBruto, @DescuentoAplicado, @ImporteFinal, @EstadoPago, @EstadoFacturacion);";
|
VALUES
|
||||||
|
(@IdSuscriptor, @Periodo, @FechaEmision, @FechaVencimiento, @ImporteBruto,
|
||||||
|
@DescuentoAplicado, @ImporteFinal, @EstadoPago, @EstadoFacturacion, @TipoFactura);";
|
||||||
|
|
||||||
return await transaction.Connection.QuerySingleAsync<Factura>(sqlInsert, nuevaFactura, transaction);
|
return await transaction.Connection.QuerySingleAsync<Factura>(sqlInsert, nuevaFactura, transaction);
|
||||||
}
|
}
|
||||||
@@ -104,7 +109,8 @@ namespace GestionIntegral.Api.Data.Repositories.Suscripciones
|
|||||||
return rowsAffected == idsFacturas.Count();
|
return rowsAffected == idsFacturas.Count();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<(Factura Factura, string NombreSuscriptor, int IdEmpresa, decimal TotalPagado)>> GetByPeriodoEnrichedAsync(string periodo, string? nombreSuscriptor, string? estadoPago, string? estadoFacturacion)
|
public async Task<IEnumerable<(Factura Factura, string NombreSuscriptor, int IdEmpresa, decimal TotalPagado)>> GetByPeriodoEnrichedAsync(
|
||||||
|
string periodo, string? nombreSuscriptor, string? estadoPago, string? estadoFacturacion, string? tipoFactura)
|
||||||
{
|
{
|
||||||
var sqlBuilder = new StringBuilder(@"
|
var sqlBuilder = new StringBuilder(@"
|
||||||
WITH FacturaConEmpresa AS (
|
WITH FacturaConEmpresa AS (
|
||||||
@@ -149,6 +155,12 @@ namespace GestionIntegral.Api.Data.Repositories.Suscripciones
|
|||||||
parameters.Add("EstadoFacturacion", estadoFacturacion);
|
parameters.Add("EstadoFacturacion", estadoFacturacion);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(tipoFactura))
|
||||||
|
{
|
||||||
|
sqlBuilder.Append(" AND f.TipoFactura = @TipoFactura");
|
||||||
|
parameters.Add("TipoFactura", tipoFactura);
|
||||||
|
}
|
||||||
|
|
||||||
sqlBuilder.Append(" ORDER BY s.NombreCompleto, f.IdFactura;");
|
sqlBuilder.Append(" ORDER BY s.NombreCompleto, f.IdFactura;");
|
||||||
|
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ namespace GestionIntegral.Api.Data.Repositories.Suscripciones
|
|||||||
Task<bool> UpdateEstadoPagoAsync(int idFactura, string nuevoEstadoPago, IDbTransaction transaction);
|
Task<bool> UpdateEstadoPagoAsync(int idFactura, string nuevoEstadoPago, IDbTransaction transaction);
|
||||||
Task<bool> UpdateNumeroFacturaAsync(int idFactura, string numeroFactura, IDbTransaction transaction);
|
Task<bool> UpdateNumeroFacturaAsync(int idFactura, string numeroFactura, IDbTransaction transaction);
|
||||||
Task<bool> UpdateLoteDebitoAsync(IEnumerable<int> idsFacturas, int idLoteDebito, IDbTransaction transaction);
|
Task<bool> UpdateLoteDebitoAsync(IEnumerable<int> idsFacturas, int idLoteDebito, IDbTransaction transaction);
|
||||||
Task<IEnumerable<(Factura Factura, string NombreSuscriptor, int IdEmpresa, decimal TotalPagado)>> GetByPeriodoEnrichedAsync(string periodo, string? nombreSuscriptor, string? estadoPago, string? estadoFacturacion);
|
Task<IEnumerable<(Factura Factura, string NombreSuscriptor, int IdEmpresa, decimal TotalPagado)>> GetByPeriodoEnrichedAsync(
|
||||||
|
string periodo, string? nombreSuscriptor, string? estadoPago, string? estadoFacturacion, string? tipoFactura);
|
||||||
Task<bool> UpdateEstadoYMotivoAsync(int idFactura, string nuevoEstadoPago, string? motivoRechazo, IDbTransaction transaction);
|
Task<bool> UpdateEstadoYMotivoAsync(int idFactura, string nuevoEstadoPago, string? motivoRechazo, IDbTransaction transaction);
|
||||||
Task<string?> GetUltimoPeriodoFacturadoAsync();
|
Task<string?> GetUltimoPeriodoFacturadoAsync();
|
||||||
Task<IEnumerable<Factura>> GetFacturasPagadasPendientesDeFacturar(string periodo);
|
Task<IEnumerable<Factura>> GetFacturasPagadasPendientesDeFacturar(string periodo);
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AspNetCore.HealthChecks.SqlServer" Version="9.0.0" />
|
<PackageReference Include="AspNetCore.HealthChecks.SqlServer" Version="9.0.0" />
|
||||||
<PackageReference Include="Dapper" Version="2.1.66" />
|
<PackageReference Include="Dapper" Version="2.1.66" />
|
||||||
<PackageReference Include="DotNetEnv" Version="3.1.1" />
|
|
||||||
<PackageReference Include="MailKit" Version="4.13.0" />
|
<PackageReference Include="MailKit" Version="4.13.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.4" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.4" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.3" />
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.3" />
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ namespace GestionIntegral.Api.Dtos.Suscripciones
|
|||||||
public string EstadoFacturacion { get; set; } = string.Empty;
|
public string EstadoFacturacion { get; set; } = string.Empty;
|
||||||
public string? NumeroFactura { get; set; }
|
public string? NumeroFactura { get; set; }
|
||||||
public decimal TotalPagado { get; set; }
|
public decimal TotalPagado { get; set; }
|
||||||
|
public string TipoFactura { get; set; } = string.Empty;
|
||||||
|
public int IdSuscriptor { get; set; }
|
||||||
public List<FacturaDetalleDto> Detalles { get; set; } = new List<FacturaDetalleDto>();
|
public List<FacturaDetalleDto> Detalles { get; set; } = new List<FacturaDetalleDto>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -15,5 +15,6 @@ namespace GestionIntegral.Api.Models.Suscripciones
|
|||||||
public string? NumeroFactura { get; set; }
|
public string? NumeroFactura { get; set; }
|
||||||
public int? IdLoteDebito { get; set; }
|
public int? IdLoteDebito { get; set; }
|
||||||
public string? MotivoRechazo { get; set; }
|
public string? MotivoRechazo { get; set; }
|
||||||
|
public string TipoFactura { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -24,10 +24,6 @@ using GestionIntegral.Api.Models.Comunicaciones;
|
|||||||
using GestionIntegral.Api.Services.Comunicaciones;
|
using GestionIntegral.Api.Services.Comunicaciones;
|
||||||
using GestionIntegral.Api.Data.Repositories.Comunicaciones;
|
using GestionIntegral.Api.Data.Repositories.Comunicaciones;
|
||||||
|
|
||||||
// Carga las variables de entorno desde el archivo .env al inicio de la aplicación.
|
|
||||||
// Debe ser la primera línea para que la configuración esté disponible para el 'builder'.
|
|
||||||
DotNetEnv.Env.Load();
|
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
// --- Registros de Servicios ---
|
// --- Registros de Servicios ---
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ using MailKit.Net.Smtp;
|
|||||||
using MailKit.Security;
|
using MailKit.Security;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using MimeKit;
|
using MimeKit;
|
||||||
|
using System.Net.Security;
|
||||||
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
|
||||||
namespace GestionIntegral.Api.Services.Comunicaciones
|
namespace GestionIntegral.Api.Services.Comunicaciones
|
||||||
{
|
{
|
||||||
@@ -88,6 +90,30 @@ namespace GestionIntegral.Api.Services.Comunicaciones
|
|||||||
using var smtp = new SmtpClient();
|
using var smtp = new SmtpClient();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
// Se añade una política de validación de certificado personalizada.
|
||||||
|
// Esto es necesario para entornos de desarrollo o redes internas donde
|
||||||
|
// el nombre del host al que nos conectamos (ej. una IP) no coincide
|
||||||
|
// con el nombre en el certificado SSL (ej. mail.eldia.com).
|
||||||
|
smtp.ServerCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) =>
|
||||||
|
{
|
||||||
|
// Si no hay errores, el certificado es válido.
|
||||||
|
if (sslPolicyErrors == SslPolicyErrors.None)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// Si el único error es que el nombre no coincide (RemoteCertificateNameMismatch)
|
||||||
|
// Y el certificado es el que esperamos (emitido para "mail.eldia.com"),
|
||||||
|
// entonces lo aceptamos como válido.
|
||||||
|
if (sslPolicyErrors.HasFlag(SslPolicyErrors.RemoteCertificateNameMismatch) && certificate != null && certificate.Subject.Contains("CN=mail.eldia.com"))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Se aceptó un certificado SSL con 'Name Mismatch' para el host de confianza 'mail.eldia.com'.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Para cualquier otro error, rechazamos el certificado.
|
||||||
|
_logger.LogError("Error de validación de certificado SSL: {Errors}", sslPolicyErrors);
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
await smtp.ConnectAsync(_mailSettings.SmtpHost, _mailSettings.SmtpPort, SecureSocketOptions.StartTls);
|
await smtp.ConnectAsync(_mailSettings.SmtpHost, _mailSettings.SmtpPort, SecureSocketOptions.StartTls);
|
||||||
await smtp.AuthenticateAsync(_mailSettings.SmtpUser, _mailSettings.SmtpPass);
|
await smtp.AuthenticateAsync(_mailSettings.SmtpUser, _mailSettings.SmtpPass);
|
||||||
await smtp.SendAsync(emailMessage);
|
await smtp.SendAsync(emailMessage);
|
||||||
@@ -95,20 +121,6 @@ namespace GestionIntegral.Api.Services.Comunicaciones
|
|||||||
log.Estado = "Enviado";
|
log.Estado = "Enviado";
|
||||||
_logger.LogInformation("Email enviado exitosamente a {Destinatario}. Asunto: {Asunto}", destinatario, emailMessage.Subject);
|
_logger.LogInformation("Email enviado exitosamente a {Destinatario}. Asunto: {Asunto}", destinatario, emailMessage.Subject);
|
||||||
}
|
}
|
||||||
catch (SmtpCommandException scEx)
|
|
||||||
{
|
|
||||||
_logger.LogError(scEx, "Error de comando SMTP al enviar a {Destinatario}. StatusCode: {StatusCode}", destinatario, scEx.StatusCode);
|
|
||||||
log.Estado = "Fallido";
|
|
||||||
log.Error = $"Error del servidor: ({scEx.StatusCode}) {scEx.Message}";
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
catch (AuthenticationException authEx)
|
|
||||||
{
|
|
||||||
_logger.LogError(authEx, "Error de autenticación con el servidor SMTP.");
|
|
||||||
log.Estado = "Fallido";
|
|
||||||
log.Error = "Error de autenticación. Revise las credenciales de correo.";
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Error general al enviar email a {Destinatario}. Asunto: {Asunto}", destinatario, emailMessage.Subject);
|
_logger.LogError(ex, "Error general al enviar email a {Destinatario}. Asunto: {Asunto}", destinatario, emailMessage.Subject);
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
|||||||
private readonly DbConnectionFactory _connectionFactory;
|
private readonly DbConnectionFactory _connectionFactory;
|
||||||
private readonly ILogger<DebitoAutomaticoService> _logger;
|
private readonly ILogger<DebitoAutomaticoService> _logger;
|
||||||
|
|
||||||
private const string NRO_PRESTACION = "123456";
|
private const string NRO_PRESTACION = "26435"; // Reemplazar por el número real
|
||||||
private const string ORIGEN_EMPRESA = "ELDIA";
|
private const string ORIGEN_EMPRESA = "EMPRESA";
|
||||||
|
|
||||||
public DebitoAutomaticoService(
|
public DebitoAutomaticoService(
|
||||||
IFacturaRepository facturaRepository,
|
IFacturaRepository facturaRepository,
|
||||||
@@ -40,9 +40,7 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
|||||||
|
|
||||||
public async Task<(string? ContenidoArchivo, string? NombreArchivo, string? Error)> GenerarArchivoPagoDirecto(int anio, int mes, int idUsuario)
|
public async Task<(string? ContenidoArchivo, string? NombreArchivo, string? Error)> GenerarArchivoPagoDirecto(int anio, int mes, int idUsuario)
|
||||||
{
|
{
|
||||||
// Se define la identificación del archivo.
|
// Este número debe ser gestionado para no repetirse. Por ahora, lo mantenemos como 1.
|
||||||
// Este número debe ser gestionado para no repetirse en archivos generados
|
|
||||||
// para la misma prestación y fecha.
|
|
||||||
const int identificacionArchivo = 1;
|
const int identificacionArchivo = 1;
|
||||||
|
|
||||||
var periodo = $"{anio}-{mes:D2}";
|
var periodo = $"{anio}-{mes:D2}";
|
||||||
@@ -62,8 +60,6 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
|||||||
|
|
||||||
var importeTotal = facturasParaDebito.Sum(f => f.Factura.ImporteFinal);
|
var importeTotal = facturasParaDebito.Sum(f => f.Factura.ImporteFinal);
|
||||||
var cantidadRegistros = facturasParaDebito.Count();
|
var cantidadRegistros = facturasParaDebito.Count();
|
||||||
|
|
||||||
// Se utiliza la variable 'identificacionArchivo' para nombrar el archivo.
|
|
||||||
var nombreArchivo = $"{NRO_PRESTACION}{fechaGeneracion:yyyyMMdd}{identificacionArchivo}.txt";
|
var nombreArchivo = $"{NRO_PRESTACION}{fechaGeneracion:yyyyMMdd}{identificacionArchivo}.txt";
|
||||||
|
|
||||||
var nuevoLote = new LoteDebito
|
var nuevoLote = new LoteDebito
|
||||||
@@ -78,13 +74,11 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
|||||||
if (loteCreado == null) throw new DataException("No se pudo crear el registro del lote de débito.");
|
if (loteCreado == null) throw new DataException("No se pudo crear el registro del lote de débito.");
|
||||||
|
|
||||||
var sb = new StringBuilder();
|
var sb = new StringBuilder();
|
||||||
// Se pasa la 'identificacionArchivo' al método que crea el Header.
|
|
||||||
sb.Append(CrearRegistroHeader(fechaGeneracion, importeTotal, cantidadRegistros, identificacionArchivo));
|
sb.Append(CrearRegistroHeader(fechaGeneracion, importeTotal, cantidadRegistros, identificacionArchivo));
|
||||||
foreach (var item in facturasParaDebito)
|
foreach (var item in facturasParaDebito)
|
||||||
{
|
{
|
||||||
sb.Append(CrearRegistroDetalle(item.Factura, item.Suscriptor));
|
sb.Append(CrearRegistroDetalle(item.Factura, item.Suscriptor));
|
||||||
}
|
}
|
||||||
// Se pasa la 'identificacionArchivo' al método que crea el Trailer.
|
|
||||||
sb.Append(CrearRegistroTrailer(fechaGeneracion, importeTotal, cantidadRegistros, identificacionArchivo));
|
sb.Append(CrearRegistroTrailer(fechaGeneracion, importeTotal, cantidadRegistros, identificacionArchivo));
|
||||||
|
|
||||||
var idsFacturas = facturasParaDebito.Select(f => f.Factura.IdFactura);
|
var idsFacturas = facturasParaDebito.Select(f => f.Factura.IdFactura);
|
||||||
@@ -108,17 +102,18 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
|||||||
var facturas = await _facturaRepository.GetByPeriodoAsync(periodo);
|
var facturas = await _facturaRepository.GetByPeriodoAsync(periodo);
|
||||||
var resultado = new List<(Factura, Suscriptor)>();
|
var resultado = new List<(Factura, Suscriptor)>();
|
||||||
|
|
||||||
foreach (var f in facturas.Where(fa => fa.EstadoPago == "Pendiente"))
|
// Filtramos por estado Y POR TIPO DE FACTURA
|
||||||
|
foreach (var f in facturas.Where(fa =>
|
||||||
|
(fa.EstadoPago == "Pendiente" || fa.EstadoPago == "Pagada Parcialmente" || fa.EstadoPago == "Rechazada") &&
|
||||||
|
fa.TipoFactura == "Mensual"
|
||||||
|
))
|
||||||
{
|
{
|
||||||
var suscriptor = await _suscriptorRepository.GetByIdAsync(f.IdSuscriptor);
|
var suscriptor = await _suscriptorRepository.GetByIdAsync(f.IdSuscriptor);
|
||||||
|
|
||||||
// Se valida que el CBU de Banelco (22 caracteres) exista antes de intentar la conversión.
|
|
||||||
if (suscriptor == null || string.IsNullOrWhiteSpace(suscriptor.CBU) || suscriptor.CBU.Length != 22)
|
if (suscriptor == null || string.IsNullOrWhiteSpace(suscriptor.CBU) || suscriptor.CBU.Length != 22)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Suscriptor ID {IdSuscriptor} omitido del lote de débito por CBU inválido o ausente (se esperan 22 dígitos).", suscriptor?.IdSuscriptor);
|
_logger.LogWarning("Suscriptor ID {IdSuscriptor} omitido del lote de débito por CBU inválido o ausente (se esperan 22 dígitos).", f.IdSuscriptor);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var formaPago = await _formaPagoRepository.GetByIdAsync(suscriptor.IdFormaPagoPreferida);
|
var formaPago = await _formaPagoRepository.GetByIdAsync(suscriptor.IdFormaPagoPreferida);
|
||||||
if (formaPago != null && formaPago.RequiereCBU)
|
if (formaPago != null && formaPago.RequiereCBU)
|
||||||
{
|
{
|
||||||
@@ -128,26 +123,13 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
|||||||
return resultado;
|
return resultado;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lógica de conversión de CBU.
|
|
||||||
private string ConvertirCbuBanelcoASnp(string cbu22)
|
private string ConvertirCbuBanelcoASnp(string cbu22)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(cbu22) || cbu22.Length != 22)
|
if (string.IsNullOrEmpty(cbu22) || cbu22.Length != 22) return "".PadRight(26);
|
||||||
{
|
|
||||||
_logger.LogError("Se intentó convertir un CBU inválido de {Length} caracteres. Se devolverá un campo vacío.", cbu22?.Length ?? 0);
|
|
||||||
// Devolver un string de 26 espacios/ceros según la preferencia del banco para campos erróneos.
|
|
||||||
return "".PadRight(26);
|
|
||||||
}
|
|
||||||
|
|
||||||
// El formato SNP de 26 se obtiene insertando un "0" al inicio y "000" después del 8vo caracter del CBU de 22.
|
|
||||||
// Formato Banelco (22): [BBBSSSSX] [T....Y]
|
|
||||||
// Posiciones: (0-7) (8-21)
|
|
||||||
// Formato SNP (26): 0[BBBSSSSX]000[T....Y]
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
string bloque1 = cbu22.Substring(0, 8); // Contiene código de banco, sucursal y DV del bloque 1.
|
string bloque1 = cbu22.Substring(0, 8);
|
||||||
string bloque2 = cbu22.Substring(8); // Contiene el resto de la cadena.
|
string bloque2 = cbu22.Substring(8);
|
||||||
|
|
||||||
// Reconstruir en formato SNP de 26 dígitos según el instructivo.
|
|
||||||
return $"0{bloque1}000{bloque2}";
|
return $"0{bloque1}000{bloque2}";
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -157,9 +139,10 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Métodos de Formateo y Mapeo ---
|
// --- Helpers de Formateo ---
|
||||||
private string FormatString(string? value, int length) => (value ?? "").PadRight(length);
|
private string FormatString(string? value, int length) => (value ?? "").PadRight(length);
|
||||||
private string FormatNumeric(long value, int length) => value.ToString().PadLeft(length, '0');
|
private string FormatNumeric(long value, int length) => value.ToString().PadLeft(length, '0');
|
||||||
|
private string FormatNumericString(string? value, int length) => (value ?? "").PadLeft(length, '0');
|
||||||
private string MapTipoDocumento(string tipo) => tipo.ToUpper() switch
|
private string MapTipoDocumento(string tipo) => tipo.ToUpper() switch
|
||||||
{
|
{
|
||||||
"DNI" => "0096",
|
"DNI" => "0096",
|
||||||
@@ -167,17 +150,17 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
|||||||
"CUIL" => "0086",
|
"CUIL" => "0086",
|
||||||
"LE" => "0089",
|
"LE" => "0089",
|
||||||
"LC" => "0090",
|
"LC" => "0090",
|
||||||
_ => "0000" // Tipo no especificado o C.I. Policía Federal según anexo.
|
_ => "0000"
|
||||||
};
|
};
|
||||||
|
|
||||||
private string CrearRegistroHeader(DateTime fechaGeneracion, decimal importeTotal, int cantidadRegistros, int identificacionArchivo)
|
private string CrearRegistroHeader(DateTime fechaGeneracion, decimal importeTotal, int cantidadRegistros, int identificacionArchivo)
|
||||||
{
|
{
|
||||||
var sb = new StringBuilder();
|
var sb = new StringBuilder();
|
||||||
sb.Append("00"); // Tipo de Registro Header
|
sb.Append("00");
|
||||||
sb.Append(FormatString(NRO_PRESTACION, 6));
|
sb.Append(FormatNumericString(NRO_PRESTACION, 6));
|
||||||
sb.Append("C"); // Servicio: Sistema Nacional de Pagos
|
sb.Append("C");
|
||||||
sb.Append(fechaGeneracion.ToString("yyyyMMdd"));
|
sb.Append(fechaGeneracion.ToString("yyyyMMdd"));
|
||||||
sb.Append(FormatString(identificacionArchivo.ToString(), 1)); // Identificación de Archivo
|
sb.Append(FormatString(identificacionArchivo.ToString(), 1));
|
||||||
sb.Append(FormatString(ORIGEN_EMPRESA, 7));
|
sb.Append(FormatString(ORIGEN_EMPRESA, 7));
|
||||||
sb.Append(FormatNumeric((long)(importeTotal * 100), 14));
|
sb.Append(FormatNumeric((long)(importeTotal * 100), 14));
|
||||||
sb.Append(FormatNumeric(cantidadRegistros, 7));
|
sb.Append(FormatNumeric(cantidadRegistros, 7));
|
||||||
@@ -188,35 +171,33 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
|||||||
|
|
||||||
private string CrearRegistroDetalle(Factura factura, Suscriptor suscriptor)
|
private string CrearRegistroDetalle(Factura factura, Suscriptor suscriptor)
|
||||||
{
|
{
|
||||||
// Convertimos el CBU de 22 (Banelco) a 26 (SNP) antes de usarlo.
|
|
||||||
string cbu26 = ConvertirCbuBanelcoASnp(suscriptor.CBU!);
|
string cbu26 = ConvertirCbuBanelcoASnp(suscriptor.CBU!);
|
||||||
|
|
||||||
var sb = new StringBuilder();
|
var sb = new StringBuilder();
|
||||||
sb.Append("0370"); // Tipo de Registro Detalle (Orden de Débito)
|
sb.Append("0370");
|
||||||
sb.Append(FormatString(suscriptor.IdSuscriptor.ToString(), 22)); // Identificación de Cliente
|
sb.Append(FormatString(suscriptor.IdSuscriptor.ToString(), 22));
|
||||||
sb.Append(FormatString(cbu26, 26)); // CBU en formato SNP de 26 caracteres.
|
sb.Append(cbu26);
|
||||||
sb.Append(FormatString($"SUSC-{factura.IdFactura}", 15)); // Referencia Unívoca de la factura.
|
sb.Append(FormatString($"SUSC-{factura.IdFactura}", 15));
|
||||||
sb.Append(factura.FechaVencimiento.ToString("yyyyMMdd"));
|
sb.Append(factura.FechaVencimiento.ToString("yyyyMMdd"));
|
||||||
sb.Append(FormatNumeric((long)(factura.ImporteFinal * 100), 14));
|
sb.Append(FormatNumeric((long)(factura.ImporteFinal * 100), 14));
|
||||||
sb.Append(FormatNumeric(0, 8)); // Fecha 2do Vencimiento
|
sb.Append(FormatNumeric(0, 8)); // Fecha 2do Vencimiento
|
||||||
sb.Append(FormatNumeric(0, 14)); // Importe 2do Vencimiento
|
sb.Append(FormatNumeric(0, 14)); // Importe 2do Vencimiento
|
||||||
sb.Append(FormatNumeric(0, 8)); // Fecha 3er Vencimiento
|
sb.Append(FormatNumeric(0, 8)); // Fecha 3er Vencimiento
|
||||||
sb.Append(FormatNumeric(0, 14)); // Importe 3er Vencimiento
|
sb.Append(FormatNumeric(0, 14)); // Importe 3er Vencimiento
|
||||||
sb.Append("0"); // Moneda (0 = Pesos)
|
sb.Append("0");
|
||||||
sb.Append(FormatString("", 3)); // Motivo Rechazo (vacío en el envío)
|
sb.Append(FormatString("", 3));
|
||||||
sb.Append(FormatString(MapTipoDocumento(suscriptor.TipoDocumento), 4));
|
sb.Append(FormatString(MapTipoDocumento(suscriptor.TipoDocumento), 4));
|
||||||
sb.Append(FormatString(suscriptor.NroDocumento, 11));
|
sb.Append(FormatNumericString(suscriptor.NroDocumento, 11));
|
||||||
sb.Append(FormatString("", 22)); // Nueva ID Cliente
|
sb.Append(FormatString("", 22));
|
||||||
sb.Append(FormatString("", 26)); // Nueva CBU
|
sb.Append(FormatString("", 26));
|
||||||
sb.Append(FormatNumeric(0, 14)); // Importe Mínimo
|
sb.Append(FormatNumeric(0, 14));
|
||||||
sb.Append(FormatNumeric(0, 8)); // Fecha Próximo Vencimiento
|
sb.Append(FormatNumeric(0, 8));
|
||||||
sb.Append(FormatString("", 22)); // Identificación Cuenta Anterior
|
sb.Append(FormatString("", 22));
|
||||||
sb.Append(FormatString("", 40)); // Mensaje ATM
|
sb.Append(FormatString("", 40));
|
||||||
sb.Append(FormatString($"Susc.{factura.Periodo}", 10)); // Concepto Factura
|
sb.Append(FormatString($"Susc.{factura.Periodo}", 10));
|
||||||
sb.Append(FormatNumeric(0, 8)); // Fecha de Cobro
|
sb.Append(FormatNumeric(0, 8));
|
||||||
sb.Append(FormatNumeric(0, 14)); // Importe Cobrado
|
sb.Append(FormatNumeric(0, 14));
|
||||||
sb.Append(FormatNumeric(0, 8)); // Fecha de Acreditamiento
|
sb.Append(FormatNumeric(0, 8));
|
||||||
sb.Append(FormatString("", 26)); // Libre
|
sb.Append(FormatString("", 26));
|
||||||
sb.Append("\r\n");
|
sb.Append("\r\n");
|
||||||
return sb.ToString();
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
@@ -224,16 +205,16 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
|||||||
private string CrearRegistroTrailer(DateTime fechaGeneracion, decimal importeTotal, int cantidadRegistros, int identificacionArchivo)
|
private string CrearRegistroTrailer(DateTime fechaGeneracion, decimal importeTotal, int cantidadRegistros, int identificacionArchivo)
|
||||||
{
|
{
|
||||||
var sb = new StringBuilder();
|
var sb = new StringBuilder();
|
||||||
sb.Append("99"); // Tipo de Registro Trailer
|
sb.Append("99");
|
||||||
sb.Append(FormatString(NRO_PRESTACION, 6));
|
sb.Append(FormatNumericString(NRO_PRESTACION, 6));
|
||||||
sb.Append("C"); // Servicio: Sistema Nacional de Pagos
|
sb.Append("C");
|
||||||
sb.Append(fechaGeneracion.ToString("yyyyMMdd"));
|
sb.Append(fechaGeneracion.ToString("yyyyMMdd"));
|
||||||
sb.Append(FormatString(identificacionArchivo.ToString(), 1)); // Identificación de Archivo
|
sb.Append(FormatString(identificacionArchivo.ToString(), 1));
|
||||||
sb.Append(FormatString(ORIGEN_EMPRESA, 7));
|
sb.Append(FormatString(ORIGEN_EMPRESA, 7));
|
||||||
sb.Append(FormatNumeric((long)(importeTotal * 100), 14));
|
sb.Append(FormatNumeric((long)(importeTotal * 100), 14));
|
||||||
sb.Append(FormatNumeric(cantidadRegistros, 7));
|
sb.Append(FormatNumeric(cantidadRegistros, 7));
|
||||||
sb.Append(FormatString("", 304));
|
sb.Append(FormatString("", 304));
|
||||||
// La última línea del archivo no lleva salto de línea (\r\n).
|
sb.Append("\r\n");
|
||||||
return sb.ToString();
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -171,10 +171,13 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
|||||||
DescuentoAplicado = descuentoPromocionesTotal,
|
DescuentoAplicado = descuentoPromocionesTotal,
|
||||||
ImporteFinal = importeFinal,
|
ImporteFinal = importeFinal,
|
||||||
EstadoPago = "Pendiente",
|
EstadoPago = "Pendiente",
|
||||||
EstadoFacturacion = "Pendiente de Facturar"
|
EstadoFacturacion = "Pendiente de Facturar",
|
||||||
|
TipoFactura = "Mensual"
|
||||||
};
|
};
|
||||||
|
|
||||||
var facturaCreada = await _facturaRepository.CreateAsync(nuevaFactura, transaction);
|
var facturaCreada = await _facturaRepository.CreateAsync(nuevaFactura, transaction);
|
||||||
if (facturaCreada == null) throw new DataException($"No se pudo crear la factura para suscriptor ID {idSuscriptor} y empresa ID {idEmpresa}");
|
if (facturaCreada == null) throw new DataException($"No se pudo crear la factura para suscriptor ID {idSuscriptor} y empresa ID {idEmpresa}");
|
||||||
|
|
||||||
facturasCreadas.Add(facturaCreada);
|
facturasCreadas.Add(facturaCreada);
|
||||||
foreach (var detalle in detallesParaFactura)
|
foreach (var detalle in detallesParaFactura)
|
||||||
{
|
{
|
||||||
@@ -278,10 +281,11 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<ResumenCuentaSuscriptorDto>> ObtenerResumenesDeCuentaPorPeriodo(int anio, int mes, string? nombreSuscriptor, string? estadoPago, string? estadoFacturacion)
|
public async Task<IEnumerable<ResumenCuentaSuscriptorDto>> ObtenerResumenesDeCuentaPorPeriodo(
|
||||||
|
int anio, int mes, string? nombreSuscriptor, string? estadoPago, string? estadoFacturacion, string? tipoFactura)
|
||||||
{
|
{
|
||||||
var periodo = $"{anio}-{mes:D2}";
|
var periodo = $"{anio}-{mes:D2}";
|
||||||
var facturasData = await _facturaRepository.GetByPeriodoEnrichedAsync(periodo, nombreSuscriptor, estadoPago, estadoFacturacion);
|
var facturasData = await _facturaRepository.GetByPeriodoEnrichedAsync(periodo, nombreSuscriptor, estadoPago, estadoFacturacion, tipoFactura);
|
||||||
var detallesData = await _facturaDetalleRepository.GetDetallesPorPeriodoAsync(periodo);
|
var detallesData = await _facturaDetalleRepository.GetDetallesPorPeriodoAsync(periodo);
|
||||||
var empresas = await _empresaRepository.GetAllAsync(null, null);
|
var empresas = await _empresaRepository.GetAllAsync(null, null);
|
||||||
|
|
||||||
@@ -302,10 +306,17 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
|||||||
EstadoFacturacion = itemFactura.Factura.EstadoFacturacion,
|
EstadoFacturacion = itemFactura.Factura.EstadoFacturacion,
|
||||||
NumeroFactura = itemFactura.Factura.NumeroFactura,
|
NumeroFactura = itemFactura.Factura.NumeroFactura,
|
||||||
TotalPagado = itemFactura.TotalPagado,
|
TotalPagado = itemFactura.TotalPagado,
|
||||||
|
|
||||||
|
// Faltaba esta línea para pasar el tipo de factura al frontend.
|
||||||
|
TipoFactura = itemFactura.Factura.TipoFactura,
|
||||||
|
|
||||||
Detalles = detallesData
|
Detalles = detallesData
|
||||||
.Where(d => d.IdFactura == itemFactura.Factura.IdFactura)
|
.Where(d => d.IdFactura == itemFactura.Factura.IdFactura)
|
||||||
.Select(d => new FacturaDetalleDto { Descripcion = d.Descripcion, ImporteNeto = d.ImporteNeto })
|
.Select(d => new FacturaDetalleDto { Descripcion = d.Descripcion, ImporteNeto = d.ImporteNeto })
|
||||||
.ToList()
|
.ToList(),
|
||||||
|
|
||||||
|
// Pasamos el id del suscriptor para facilitar las cosas en el frontend
|
||||||
|
IdSuscriptor = itemFactura.Factura.IdSuscriptor
|
||||||
};
|
};
|
||||||
}).ToList();
|
}).ToList();
|
||||||
|
|
||||||
@@ -579,7 +590,7 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<decimal> CalcularImporteParaSuscripcion(Suscripcion suscripcion, int anio, int mes, IDbTransaction transaction)
|
public async Task<decimal> CalcularImporteParaSuscripcion(Suscripcion suscripcion, int anio, int mes, IDbTransaction transaction)
|
||||||
{
|
{
|
||||||
decimal importeTotal = 0;
|
decimal importeTotal = 0;
|
||||||
var diasDeEntrega = suscripcion.DiasEntrega.Split(',').ToHashSet();
|
var diasDeEntrega = suscripcion.DiasEntrega.Split(',').ToHashSet();
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
using System.Data;
|
||||||
using GestionIntegral.Api.Dtos.Comunicaciones;
|
using GestionIntegral.Api.Dtos.Comunicaciones;
|
||||||
using GestionIntegral.Api.Dtos.Suscripciones;
|
using GestionIntegral.Api.Dtos.Suscripciones;
|
||||||
|
using GestionIntegral.Api.Models.Suscripciones;
|
||||||
|
|
||||||
namespace GestionIntegral.Api.Services.Suscripciones
|
namespace GestionIntegral.Api.Services.Suscripciones
|
||||||
{
|
{
|
||||||
@@ -7,8 +9,10 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
|||||||
{
|
{
|
||||||
Task<(bool Exito, string? Mensaje, LoteDeEnvioResumenDto? ResultadoEnvio)> GenerarFacturacionMensual(int anio, int mes, int idUsuario);
|
Task<(bool Exito, string? Mensaje, LoteDeEnvioResumenDto? ResultadoEnvio)> GenerarFacturacionMensual(int anio, int mes, int idUsuario);
|
||||||
Task<IEnumerable<LoteDeEnvioHistorialDto>> ObtenerHistorialLotesEnvio(int? anio, int? mes);
|
Task<IEnumerable<LoteDeEnvioHistorialDto>> ObtenerHistorialLotesEnvio(int? anio, int? mes);
|
||||||
Task<IEnumerable<ResumenCuentaSuscriptorDto>> ObtenerResumenesDeCuentaPorPeriodo(int anio, int mes, string? nombreSuscriptor, string? estadoPago, string? estadoFacturacion);
|
Task<IEnumerable<ResumenCuentaSuscriptorDto>> ObtenerResumenesDeCuentaPorPeriodo(
|
||||||
|
int anio, int mes, string? nombreSuscriptor, string? estadoPago, string? estadoFacturacion, string? tipoFactura);
|
||||||
Task<(bool Exito, string? Error, string? EmailDestino)> EnviarFacturaPdfPorEmail(int idFactura, int idUsuario);
|
Task<(bool Exito, string? Error, string? EmailDestino)> EnviarFacturaPdfPorEmail(int idFactura, int idUsuario);
|
||||||
Task<(bool Exito, string? Error)> ActualizarNumeroFactura(int idFactura, string numeroFactura, int idUsuario);
|
Task<(bool Exito, string? Error)> ActualizarNumeroFactura(int idFactura, string numeroFactura, int idUsuario);
|
||||||
|
Task<decimal> CalcularImporteParaSuscripcion(Suscripcion suscripcion, int anio, int mes, IDbTransaction transaction);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,12 +3,8 @@ using GestionIntegral.Api.Data.Repositories.Distribucion;
|
|||||||
using GestionIntegral.Api.Data.Repositories.Suscripciones;
|
using GestionIntegral.Api.Data.Repositories.Suscripciones;
|
||||||
using GestionIntegral.Api.Dtos.Suscripciones;
|
using GestionIntegral.Api.Dtos.Suscripciones;
|
||||||
using GestionIntegral.Api.Models.Suscripciones;
|
using GestionIntegral.Api.Models.Suscripciones;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Data;
|
using System.Data;
|
||||||
using System.Linq;
|
using System.Globalization;
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace GestionIntegral.Api.Services.Suscripciones
|
namespace GestionIntegral.Api.Services.Suscripciones
|
||||||
{
|
{
|
||||||
@@ -18,23 +14,32 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
|||||||
private readonly ISuscriptorRepository _suscriptorRepository;
|
private readonly ISuscriptorRepository _suscriptorRepository;
|
||||||
private readonly IPublicacionRepository _publicacionRepository;
|
private readonly IPublicacionRepository _publicacionRepository;
|
||||||
private readonly IPromocionRepository _promocionRepository;
|
private readonly IPromocionRepository _promocionRepository;
|
||||||
private readonly DbConnectionFactory _connectionFactory;
|
private readonly IFacturaRepository _facturaRepository;
|
||||||
|
private readonly IFacturaDetalleRepository _facturaDetalleRepository;
|
||||||
|
private readonly IFacturacionService _facturacionService;
|
||||||
private readonly ILogger<SuscripcionService> _logger;
|
private readonly ILogger<SuscripcionService> _logger;
|
||||||
|
private readonly DbConnectionFactory _connectionFactory;
|
||||||
|
|
||||||
public SuscripcionService(
|
public SuscripcionService(
|
||||||
ISuscripcionRepository suscripcionRepository,
|
ISuscripcionRepository suscripcionRepository,
|
||||||
ISuscriptorRepository suscriptorRepository,
|
ISuscriptorRepository suscriptorRepository,
|
||||||
IPublicacionRepository publicacionRepository,
|
IPublicacionRepository publicacionRepository,
|
||||||
IPromocionRepository promocionRepository,
|
IPromocionRepository promocionRepository,
|
||||||
DbConnectionFactory connectionFactory,
|
IFacturaRepository facturaRepository,
|
||||||
ILogger<SuscripcionService> logger)
|
IFacturaDetalleRepository facturaDetalleRepository,
|
||||||
|
IFacturacionService facturacionService,
|
||||||
|
ILogger<SuscripcionService> logger,
|
||||||
|
DbConnectionFactory connectionFactory)
|
||||||
{
|
{
|
||||||
_suscripcionRepository = suscripcionRepository;
|
_suscripcionRepository = suscripcionRepository;
|
||||||
_suscriptorRepository = suscriptorRepository;
|
_suscriptorRepository = suscriptorRepository;
|
||||||
_publicacionRepository = publicacionRepository;
|
_publicacionRepository = publicacionRepository;
|
||||||
_promocionRepository = promocionRepository;
|
_promocionRepository = promocionRepository;
|
||||||
_connectionFactory = connectionFactory;
|
_facturaRepository = facturaRepository;
|
||||||
|
_facturaDetalleRepository = facturaDetalleRepository;
|
||||||
|
_facturacionService = facturacionService;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_connectionFactory = connectionFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
private PromocionDto MapPromocionToDto(Promocion promo) => new PromocionDto
|
private PromocionDto MapPromocionToDto(Promocion promo) => new PromocionDto
|
||||||
@@ -122,6 +127,53 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
|||||||
var creada = await _suscripcionRepository.CreateAsync(nuevaSuscripcion, transaction);
|
var creada = await _suscripcionRepository.CreateAsync(nuevaSuscripcion, transaction);
|
||||||
if (creada == null) throw new DataException("Error al crear la suscripción.");
|
if (creada == null) throw new DataException("Error al crear la suscripción.");
|
||||||
|
|
||||||
|
var ultimoPeriodoFacturadoStr = await _facturaRepository.GetUltimoPeriodoFacturadoAsync();
|
||||||
|
if (ultimoPeriodoFacturadoStr != null)
|
||||||
|
{
|
||||||
|
var ultimoPeriodo = DateTime.ParseExact(ultimoPeriodoFacturadoStr, "yyyy-MM", CultureInfo.InvariantCulture);
|
||||||
|
var periodoSuscripcion = new DateTime(creada.FechaInicio.Year, creada.FechaInicio.Month, 1);
|
||||||
|
|
||||||
|
if (periodoSuscripcion <= ultimoPeriodo)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Suscripción en período ya cerrado detectada. Generando factura de alta pro-rata.");
|
||||||
|
|
||||||
|
decimal importeProporcional = await _facturacionService.CalcularImporteParaSuscripcion(creada, creada.FechaInicio.Year, creada.FechaInicio.Month, transaction);
|
||||||
|
|
||||||
|
if (importeProporcional > 0)
|
||||||
|
{
|
||||||
|
var facturaDeAlta = new Factura
|
||||||
|
{
|
||||||
|
IdSuscriptor = creada.IdSuscriptor,
|
||||||
|
Periodo = creada.FechaInicio.ToString("yyyy-MM"),
|
||||||
|
FechaEmision = DateTime.Now.Date,
|
||||||
|
FechaVencimiento = DateTime.Now.AddDays(10).Date,
|
||||||
|
ImporteBruto = importeProporcional,
|
||||||
|
ImporteFinal = importeProporcional,
|
||||||
|
EstadoPago = "Pendiente",
|
||||||
|
EstadoFacturacion = "Pendiente de Facturar",
|
||||||
|
TipoFactura = "Alta"
|
||||||
|
};
|
||||||
|
|
||||||
|
var facturaCreada = await _facturaRepository.CreateAsync(facturaDeAlta, transaction);
|
||||||
|
if (facturaCreada == null) throw new DataException("No se pudo crear la factura de alta.");
|
||||||
|
|
||||||
|
var publicacion = await _publicacionRepository.GetByIdSimpleAsync(creada.IdPublicacion);
|
||||||
|
var finDeMes = new DateTime(creada.FechaInicio.Year, creada.FechaInicio.Month, 1).AddMonths(1).AddDays(-1);
|
||||||
|
|
||||||
|
await _facturaDetalleRepository.CreateAsync(new FacturaDetalle
|
||||||
|
{
|
||||||
|
IdFactura = facturaCreada.IdFactura,
|
||||||
|
IdSuscripcion = creada.IdSuscripcion,
|
||||||
|
Descripcion = $"Suscripción proporcional {publicacion?.Nombre} ({creada.FechaInicio:dd/MM} al {finDeMes:dd/MM})",
|
||||||
|
ImporteBruto = importeProporcional,
|
||||||
|
ImporteNeto = importeProporcional,
|
||||||
|
DescuentoAplicado = 0
|
||||||
|
}, transaction);
|
||||||
|
|
||||||
|
_logger.LogInformation("Factura de alta #{IdFactura} por ${Importe} generada para la nueva suscripción #{IdSuscripcion}.", facturaCreada.IdFactura, importeProporcional, creada.IdSuscripcion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
transaction.Commit();
|
transaction.Commit();
|
||||||
_logger.LogInformation("Suscripción ID {Id} creada por Usuario ID {UserId}.", creada.IdSuscripcion, idUsuario);
|
_logger.LogInformation("Suscripción ID {Id} creada por Usuario ID {UserId}.", creada.IdSuscripcion, idUsuario);
|
||||||
return (await MapToDto(creada), null);
|
return (await MapToDto(creada), null);
|
||||||
|
|||||||
@@ -16,11 +16,11 @@
|
|||||||
},
|
},
|
||||||
"AllowedHosts": "*",
|
"AllowedHosts": "*",
|
||||||
"MailSettings": {
|
"MailSettings": {
|
||||||
"SmtpHost": "",
|
"SmtpHost": "192.168.5.201",
|
||||||
"SmtpPort": 0,
|
"SmtpPort": 587,
|
||||||
"SenderName": "",
|
"SenderName": "Club - Diario El Día",
|
||||||
"SenderEmail": "",
|
"SenderEmail": "alertas@eldia.com",
|
||||||
"SmtpUser": "",
|
"SmtpUser": "alertas@eldia.com",
|
||||||
"SmtpPass": ""
|
"SmtpPass": "@Alertas713550@"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Modal, Box, Typography, TextField, Button, CircularProgress, Alert, FormControl, InputLabel, Select, MenuItem, type SelectChangeEvent, InputAdornment } from '@mui/material';
|
import { Modal, Box, Typography, TextField, Button, CircularProgress, Alert, FormControl, InputLabel, Select, MenuItem, type SelectChangeEvent, InputAdornment } from '@mui/material';
|
||||||
import type { FacturaDto } from '../../../models/dtos/Suscripciones/FacturaDto';
|
import type { FacturaConsolidadaDto } from '../../../models/dtos/Suscripciones/ResumenCuentaSuscriptorDto';
|
||||||
import type { CreatePagoDto } from '../../../models/dtos/Suscripciones/CreatePagoDto';
|
import type { CreatePagoDto } from '../../../models/dtos/Suscripciones/CreatePagoDto';
|
||||||
import type { FormaPagoDto } from '../../../models/dtos/Suscripciones/FormaPagoDto';
|
import type { FormaPagoDto } from '../../../models/dtos/Suscripciones/FormaPagoDto';
|
||||||
import formaPagoService from '../../../services/Suscripciones/formaPagoService';
|
import formaPagoService from '../../../services/Suscripciones/formaPagoService';
|
||||||
@@ -23,17 +23,19 @@ interface PagoManualModalProps {
|
|||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onSubmit: (data: CreatePagoDto) => Promise<void>;
|
onSubmit: (data: CreatePagoDto) => Promise<void>;
|
||||||
factura: FacturaDto | null;
|
factura: FacturaConsolidadaDto | null;
|
||||||
|
nombreSuscriptor: string; // Se pasa el nombre del suscriptor como prop-
|
||||||
errorMessage?: string | null;
|
errorMessage?: string | null;
|
||||||
clearErrorMessage: () => void;
|
clearErrorMessage: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PagoManualModal: React.FC<PagoManualModalProps> = ({ open, onClose, onSubmit, factura, errorMessage, clearErrorMessage }) => {
|
const PagoManualModal: React.FC<PagoManualModalProps> = ({ open, onClose, onSubmit, factura, nombreSuscriptor, errorMessage, clearErrorMessage }) => {
|
||||||
const [formData, setFormData] = useState<Partial<CreatePagoDto>>({});
|
const [formData, setFormData] = useState<Partial<CreatePagoDto>>({});
|
||||||
const [formasDePago, setFormasDePago] = useState<FormaPagoDto[]>([]);
|
const [formasDePago, setFormasDePago] = useState<FormaPagoDto[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [loadingFormasPago, setLoadingFormasPago] = useState(false);
|
const [loadingFormasPago, setLoadingFormasPago] = useState(false);
|
||||||
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
|
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
|
||||||
|
const saldoPendiente = factura ? factura.importeFinal - factura.totalPagado : 0;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchFormasDePago = async () => {
|
const fetchFormasDePago = async () => {
|
||||||
@@ -52,12 +54,12 @@ const PagoManualModal: React.FC<PagoManualModalProps> = ({ open, onClose, onSubm
|
|||||||
fetchFormasDePago();
|
fetchFormasDePago();
|
||||||
setFormData({
|
setFormData({
|
||||||
idFactura: factura.idFactura,
|
idFactura: factura.idFactura,
|
||||||
monto: factura.saldoPendiente, // Usar el saldo pendiente como valor por defecto
|
monto: saldoPendiente,
|
||||||
fechaPago: new Date().toISOString().split('T')[0]
|
fechaPago: new Date().toISOString().split('T')[0]
|
||||||
});
|
});
|
||||||
setLocalErrors({});
|
setLocalErrors({});
|
||||||
}
|
}
|
||||||
}, [open, factura]);
|
}, [open, factura, saldoPendiente]);
|
||||||
|
|
||||||
const validate = (): boolean => {
|
const validate = (): boolean => {
|
||||||
const errors: { [key: string]: string | null } = {};
|
const errors: { [key: string]: string | null } = {};
|
||||||
@@ -65,13 +67,11 @@ const PagoManualModal: React.FC<PagoManualModalProps> = ({ open, onClose, onSubm
|
|||||||
if (!formData.fechaPago) errors.fechaPago = "La fecha de pago es obligatoria.";
|
if (!formData.fechaPago) errors.fechaPago = "La fecha de pago es obligatoria.";
|
||||||
|
|
||||||
const monto = formData.monto ?? 0;
|
const monto = formData.monto ?? 0;
|
||||||
const saldo = factura?.saldoPendiente ?? 0;
|
|
||||||
|
|
||||||
if (monto <= 0) {
|
if (monto <= 0) {
|
||||||
errors.monto = "El monto debe ser mayor a cero.";
|
errors.monto = "El monto debe ser mayor a cero.";
|
||||||
} else if (monto > saldo) {
|
} else if (monto > saldoPendiente) {
|
||||||
// Usamos toFixed(2) para mostrar el formato de moneda correcto en el mensaje
|
errors.monto = `El monto no puede superar el saldo pendiente de $${saldoPendiente.toFixed(2)}.`;
|
||||||
errors.monto = `El monto no puede superar el saldo pendiente de $${saldo.toFixed(2)}.`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setLocalErrors(errors);
|
setLocalErrors(errors);
|
||||||
@@ -117,8 +117,11 @@ const PagoManualModal: React.FC<PagoManualModalProps> = ({ open, onClose, onSubm
|
|||||||
<Modal open={open} onClose={onClose}>
|
<Modal open={open} onClose={onClose}>
|
||||||
<Box sx={modalStyle}>
|
<Box sx={modalStyle}>
|
||||||
<Typography variant="h6">Registrar Pago Manual</Typography>
|
<Typography variant="h6">Registrar Pago Manual</Typography>
|
||||||
<Typography variant="subtitle1" gutterBottom sx={{fontWeight: 'bold'}}>
|
<Typography variant="body1" color="text.secondary" gutterBottom>
|
||||||
Saldo Pendiente: ${factura.saldoPendiente.toFixed(2)}
|
Para: {nombreSuscriptor}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="subtitle1" gutterBottom sx={{ fontWeight: 'bold' }}>
|
||||||
|
Saldo Pendiente: ${saldoPendiente.toFixed(2)}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 2 }}>
|
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 2 }}>
|
||||||
<TextField name="fechaPago" label="Fecha de Pago" type="date" value={formData.fechaPago || ''} onChange={handleInputChange} required fullWidth margin="dense" InputLabelProps={{ shrink: true }} error={!!localErrors.fechaPago} helperText={localErrors.fechaPago} />
|
<TextField name="fechaPago" label="Fecha de Pago" type="date" value={formData.fechaPago || ''} onChange={handleInputChange} required fullWidth margin="dense" InputLabelProps={{ shrink: true }} error={!!localErrors.fechaPago} helperText={localErrors.fechaPago} />
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import {
|
import { Modal, Box, Typography, TextField, Button, CircularProgress, Alert,
|
||||||
Modal, Box, Typography, TextField, Button, CircularProgress, Alert,
|
|
||||||
FormControl, InputLabel, Select, MenuItem, FormControlLabel, Checkbox
|
FormControl, InputLabel, Select, MenuItem, FormControlLabel, Checkbox
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import type { UsuarioDto } from '../../../models/dtos/Usuarios/UsuarioDto';
|
import type { UsuarioDto } from '../../../models/dtos/Usuarios/UsuarioDto';
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
// src/hooks/usePermissions.ts
|
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
export const usePermissions = () => {
|
export const usePermissions = () => {
|
||||||
const { user } = useAuth(); // user aquí es de tipo UserContextData | null
|
const { user } = useAuth();
|
||||||
|
|
||||||
const tienePermiso = (codigoPermisoRequerido: string): boolean => {
|
// Envolvemos la función en useCallback.
|
||||||
if (!user) { // Si no hay usuario logueado
|
// Su dependencia es [user], por lo que la función solo se
|
||||||
|
// volverá a crear si el objeto 'user' cambia (ej. al iniciar/cerrar sesión).
|
||||||
|
const tienePermiso = useCallback((codigoPermisoRequerido: string): boolean => {
|
||||||
|
if (!user) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (user.esSuperAdmin) { // SuperAdmin tiene todos los permisos
|
if (user.esSuperAdmin) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// Verificar si la lista de permisos del usuario incluye el código requerido
|
|
||||||
return user.permissions?.includes(codigoPermisoRequerido) ?? false;
|
return user.permissions?.includes(codigoPermisoRequerido) ?? false;
|
||||||
};
|
}, [user]);
|
||||||
|
|
||||||
// También puede exportar el objeto user completo si se necesita en otros lugares
|
|
||||||
// o propiedades específicas como idPerfil, esSuperAdmin.
|
|
||||||
return {
|
return {
|
||||||
tienePermiso,
|
tienePermiso,
|
||||||
isSuperAdmin: user?.esSuperAdmin ?? false,
|
isSuperAdmin: user?.esSuperAdmin ?? false,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export interface FacturaConsolidadaDto {
|
|||||||
estadoFacturacion: string;
|
estadoFacturacion: string;
|
||||||
numeroFactura?: string | null;
|
numeroFactura?: string | null;
|
||||||
totalPagado: number;
|
totalPagado: number;
|
||||||
|
tipoFactura: 'Mensual' | 'Alta';
|
||||||
detalles: FacturaDetalleDto[];
|
detalles: FacturaDetalleDto[];
|
||||||
// Añadimos el id del suscriptor para que sea fácil pasarlo a los handlers
|
// Añadimos el id del suscriptor para que sea fácil pasarlo a los handlers
|
||||||
idSuscriptor: number;
|
idSuscriptor: number;
|
||||||
|
|||||||
@@ -40,14 +40,12 @@ const ReportesIndexPage: React.FC = () => {
|
|||||||
const [isLoadingInitialNavigation, setIsLoadingInitialNavigation] = useState(true);
|
const [isLoadingInitialNavigation, setIsLoadingInitialNavigation] = useState(true);
|
||||||
const { tienePermiso, isSuperAdmin } = usePermissions();
|
const { tienePermiso, isSuperAdmin } = usePermissions();
|
||||||
|
|
||||||
// 1. Creamos una lista memoizada de reportes a los que el usuario SÍ tiene acceso.
|
|
||||||
const accessibleReportModules = useMemo(() => {
|
const accessibleReportModules = useMemo(() => {
|
||||||
return allReportModules.filter(module =>
|
return allReportModules.filter(module =>
|
||||||
isSuperAdmin || tienePermiso(module.requiredPermission)
|
isSuperAdmin || tienePermiso(module.requiredPermission)
|
||||||
);
|
);
|
||||||
}, [isSuperAdmin, tienePermiso]);
|
}, [isSuperAdmin, tienePermiso]);
|
||||||
|
|
||||||
// 2. Creamos una lista de categorías que SÍ tienen al menos un reporte accesible.
|
|
||||||
const accessibleCategories = useMemo(() => {
|
const accessibleCategories = useMemo(() => {
|
||||||
const categoriesWithAccess = new Set(accessibleReportModules.map(r => r.category));
|
const categoriesWithAccess = new Set(accessibleReportModules.map(r => r.category));
|
||||||
return predefinedCategoryOrder.filter(category => categoriesWithAccess.has(category));
|
return predefinedCategoryOrder.filter(category => categoriesWithAccess.has(category));
|
||||||
@@ -62,18 +60,23 @@ const ReportesIndexPage: React.FC = () => {
|
|||||||
const activeReport = accessibleReportModules.find(module => module.path === subPathSegment);
|
const activeReport = accessibleReportModules.find(module => module.path === subPathSegment);
|
||||||
if (activeReport) {
|
if (activeReport) {
|
||||||
setExpandedCategory(activeReport.category);
|
setExpandedCategory(activeReport.category);
|
||||||
|
} else {
|
||||||
|
// Si la URL apunta a un reporte que no es accesible, no expandimos nada
|
||||||
|
setExpandedCategory(false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Si estamos en la página base (/reportes), expandimos la primera categoría disponible.
|
||||||
|
if (accessibleCategories.length > 0) {
|
||||||
|
setExpandedCategory(accessibleCategories[0]);
|
||||||
|
} else {
|
||||||
|
setExpandedCategory(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Navegamos al PRIMER REPORTE ACCESIBLE si estamos en la ruta base.
|
// No hay navegación automática, solo manejamos el estado de carga.
|
||||||
if (location.pathname === currentBasePath && accessibleReportModules.length > 0 && isLoadingInitialNavigation) {
|
|
||||||
const firstReportToNavigate = accessibleReportModules[0];
|
|
||||||
navigate(firstReportToNavigate.path, { replace: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsLoadingInitialNavigation(false);
|
setIsLoadingInitialNavigation(false);
|
||||||
|
|
||||||
}, [location.pathname, navigate, accessibleReportModules, isLoadingInitialNavigation]);
|
}, [location.pathname, accessibleReportModules, accessibleCategories]);
|
||||||
|
|
||||||
const handleCategoryClick = (categoryName: string) => {
|
const handleCategoryClick = (categoryName: string) => {
|
||||||
setExpandedCategory(prev => (prev === categoryName ? false : categoryName));
|
setExpandedCategory(prev => (prev === categoryName ? false : categoryName));
|
||||||
@@ -84,12 +87,10 @@ const ReportesIndexPage: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const isReportActive = (reportPath: string) => {
|
const isReportActive = (reportPath: string) => {
|
||||||
return location.pathname === `/reportes/${reportPath}` || location.pathname.startsWith(`/reportes/${reportPath}/`);
|
return location.pathname.startsWith(`/reportes/${reportPath}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Si isLoadingInitialNavigation es true Y estamos en /reportes, mostrar loader
|
if (isLoadingInitialNavigation) {
|
||||||
// Esto evita mostrar el loader si se navega directamente a un sub-reporte.
|
|
||||||
if (isLoadingInitialNavigation && (location.pathname === '/reportes' || location.pathname === '/reportes/')) {
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', width: '100%', height: '100%' }}>
|
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', width: '100%', height: '100%' }}>
|
||||||
<CircularProgress />
|
<CircularProgress />
|
||||||
@@ -98,48 +99,25 @@ const ReportesIndexPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// Contenedor principal que se adaptará a su padre
|
|
||||||
// Eliminamos 'height: calc(100vh - 64px)' y cualquier margen/padding que controle el espacio exterior
|
|
||||||
<Box sx={{ display: 'flex', width: '100%', height: '100%' }}>
|
<Box sx={{ display: 'flex', width: '100%', height: '100%' }}>
|
||||||
{/* Panel Lateral para Navegación */}
|
<Paper elevation={0} square sx={{
|
||||||
<Paper
|
width: { xs: 220, sm: 250, md: 280 },
|
||||||
elevation={0} // Sin elevación para que se sienta más integrado si el fondo es el mismo
|
|
||||||
square // Bordes rectos
|
|
||||||
sx={{
|
|
||||||
width: { xs: 220, sm: 250, md: 280 }, // Ancho responsivo del panel lateral
|
|
||||||
minWidth: { xs: 200, sm: 220 },
|
minWidth: { xs: 200, sm: 220 },
|
||||||
height: '100%', // Ocupa toda la altura del Box padre
|
height: '100%',
|
||||||
borderRight: (theme) => `1px solid ${theme.palette.divider}`,
|
borderRight: (theme) => `1px solid ${theme.palette.divider}`,
|
||||||
overflowY: 'auto',
|
overflowY: 'auto',
|
||||||
bgcolor: 'background.paper', // O el color que desees para el menú
|
bgcolor: 'background.paper',
|
||||||
// display: 'flex', flexDirection: 'column' // Para que el título y la lista usen el espacio vertical
|
}}>
|
||||||
}}
|
<Box sx={{ p: 1.5 }}>
|
||||||
>
|
<Typography variant="h6" component="div" sx={{ fontWeight: 'medium', ml:1 }}>
|
||||||
{/* Título del Menú Lateral */}
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
p: 1.5, // Padding interno para el título
|
|
||||||
// borderBottom: (theme) => `1px solid ${theme.palette.divider}`, // Opcional: separador
|
|
||||||
// position: 'sticky', // Si quieres que el título quede fijo al hacer scroll en la lista
|
|
||||||
// top: 0,
|
|
||||||
// zIndex: 1,
|
|
||||||
// bgcolor: 'background.paper' // Necesario si es sticky y tiene scroll la lista
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography variant="h6" component="div" sx={{ fontWeight: 'medium', ml:1 /* Pequeño margen para alinear con items */ }}>
|
|
||||||
Reportes
|
Reportes
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* 5. Renderizamos el menú usando la lista de categorías ACCESIBLES. */}
|
|
||||||
{accessibleCategories.length > 0 ? (
|
{accessibleCategories.length > 0 ? (
|
||||||
<List component="nav" dense sx={{ pt: 0 }}>
|
<List component="nav" dense sx={{ pt: 0 }}>
|
||||||
{accessibleCategories.map((category) => {
|
{accessibleCategories.map((category) => {
|
||||||
// 6. Obtenemos los reportes de esta categoría de la lista ACCESIBLE.
|
|
||||||
const reportsInCategory = accessibleReportModules.filter(r => r.category === category);
|
const reportsInCategory = accessibleReportModules.filter(r => r.category === category);
|
||||||
|
|
||||||
// Ya no es necesario el `if (reportsInCategory.length === 0) return null;` porque `accessibleCategories` ya está filtrado.
|
|
||||||
|
|
||||||
const isExpanded = expandedCategory === category;
|
const isExpanded = expandedCategory === category;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -147,15 +125,10 @@ const ReportesIndexPage: React.FC = () => {
|
|||||||
<ListItemButton
|
<ListItemButton
|
||||||
onClick={() => handleCategoryClick(category)}
|
onClick={() => handleCategoryClick(category)}
|
||||||
sx={{
|
sx={{
|
||||||
// py: 1.2, // Ajustar padding vertical de items de categoría
|
|
||||||
// backgroundColor: isExpanded ? 'action.selected' : 'transparent',
|
|
||||||
borderLeft: isExpanded ? (theme) => `4px solid ${theme.palette.primary.main}` : '4px solid transparent',
|
borderLeft: isExpanded ? (theme) => `4px solid ${theme.palette.primary.main}` : '4px solid transparent',
|
||||||
pr: 1, // Menos padding a la derecha para dar espacio al ícono expander
|
pr: 1,
|
||||||
'&:hover': {
|
'&:hover': { backgroundColor: 'action.hover' }
|
||||||
backgroundColor: 'action.hover'
|
}}>
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ListItemText primary={category} primaryTypographyProps={{ fontWeight: isExpanded ? 'bold' : 'normal' }}/>
|
<ListItemText primary={category} primaryTypographyProps={{ fontWeight: isExpanded ? 'bold' : 'normal' }}/>
|
||||||
{isExpanded ? <ExpandLess /> : <ExpandMore />}
|
{isExpanded ? <ExpandLess /> : <ExpandMore />}
|
||||||
</ListItemButton>
|
</ListItemButton>
|
||||||
@@ -167,21 +140,14 @@ const ReportesIndexPage: React.FC = () => {
|
|||||||
selected={isReportActive(report.path)}
|
selected={isReportActive(report.path)}
|
||||||
onClick={() => handleReportClick(report.path)}
|
onClick={() => handleReportClick(report.path)}
|
||||||
sx={{
|
sx={{
|
||||||
pl: 3.5, // Indentación para los reportes (ajustar si se cambió el padding del título)
|
pl: 3.5, py: 0.8,
|
||||||
py: 0.8, // Padding vertical de items de reporte
|
|
||||||
...(isReportActive(report.path) && {
|
...(isReportActive(report.path) && {
|
||||||
backgroundColor: (theme) => theme.palette.action.selected, // Un color de fondo sutil
|
backgroundColor: (theme) => theme.palette.action.selected,
|
||||||
borderLeft: (theme) => `4px solid ${theme.palette.primary.light}`, // Un borde para el activo
|
borderLeft: (theme) => `4px solid ${theme.palette.primary.light}`,
|
||||||
'& .MuiListItemText-primary': {
|
'& .MuiListItemText-primary': { fontWeight: 'medium' },
|
||||||
fontWeight: 'medium', // O 'bold'
|
|
||||||
// color: 'primary.main'
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
'&:hover': {
|
'&:hover': { backgroundColor: (theme) => theme.palette.action.hover }
|
||||||
backgroundColor: (theme) => theme.palette.action.hover
|
}}>
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ListItemText primary={report.label} primaryTypographyProps={{ variant: 'body2' }}/>
|
<ListItemText primary={report.label} primaryTypographyProps={{ variant: 'body2' }}/>
|
||||||
</ListItemButton>
|
</ListItemButton>
|
||||||
))}
|
))}
|
||||||
@@ -196,26 +162,17 @@ const ReportesIndexPage: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
{/* Área Principal para el Contenido del Reporte */}
|
<Box component="main" sx={{
|
||||||
<Box
|
flexGrow: 1, p: { xs: 1, sm: 2, md: 3 },
|
||||||
component="main"
|
overflowY: 'auto', height: '100%', bgcolor: 'grey.100'
|
||||||
sx={{
|
}}>
|
||||||
flexGrow: 1, // Ocupa el espacio restante
|
{/* Lógica para mostrar el mensaje de bienvenida */}
|
||||||
p: { xs: 1, sm: 2, md: 3 }, // Padding interno para el contenido, responsivo
|
{location.pathname === '/reportes' && !isLoadingInitialNavigation && (
|
||||||
overflowY: 'auto',
|
|
||||||
height: '100%', // Ocupa toda la altura del Box padre
|
|
||||||
bgcolor: 'grey.100' // Un color de fondo diferente para distinguir el área de contenido
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* El Outlet renderiza el componente del reporte específico */}
|
|
||||||
{(!location.pathname.startsWith('/reportes/') || !allReportModules.some(r => isReportActive(r.path))) && location.pathname !== '/reportes/' && location.pathname !== '/reportes' && !isLoadingInitialNavigation && (
|
|
||||||
<Typography sx={{p:2, textAlign:'center', mt: 4, color: 'text.secondary'}}>
|
<Typography sx={{p:2, textAlign:'center', mt: 4, color: 'text.secondary'}}>
|
||||||
El reporte solicitado no existe o la ruta no es válida.
|
{accessibleReportModules.length > 0
|
||||||
</Typography>
|
? "Seleccione una categoría y un reporte del menú lateral."
|
||||||
)}
|
: "No tiene acceso a ningún reporte."
|
||||||
{(location.pathname === '/reportes/' || location.pathname === '/reportes') && !allReportModules.some(r => isReportActive(r.path)) && !isLoadingInitialNavigation && (
|
}
|
||||||
<Typography sx={{p:2, textAlign:'center', mt: 4, color: 'text.secondary'}}>
|
|
||||||
{allReportModules.length > 0 ? "Seleccione una categoría y un reporte del menú lateral." : "No hay reportes configurados."}
|
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
<Outlet />
|
<Outlet />
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ const meses = [
|
|||||||
|
|
||||||
const estadosPago = ['Pendiente', 'Pagada', 'Pagada Parcialmente', 'Rechazada', 'Anulada'];
|
const estadosPago = ['Pendiente', 'Pagada', 'Pagada Parcialmente', 'Rechazada', 'Anulada'];
|
||||||
const estadosFacturacion = ['Pendiente de Facturar', 'Facturado'];
|
const estadosFacturacion = ['Pendiente de Facturar', 'Facturado'];
|
||||||
|
const tiposFactura = ['Mensual', 'Alta'];
|
||||||
|
|
||||||
const SuscriptorRow: React.FC<{
|
const SuscriptorRow: React.FC<{
|
||||||
resumen: ResumenCuentaSuscriptorDto;
|
resumen: ResumenCuentaSuscriptorDto;
|
||||||
@@ -33,8 +34,6 @@ const SuscriptorRow: React.FC<{
|
|||||||
handleOpenHistorial: (factura: FacturaConsolidadaDto) => void;
|
handleOpenHistorial: (factura: FacturaConsolidadaDto) => void;
|
||||||
}> = ({ resumen, handleMenuOpen, handleOpenHistorial }) => {
|
}> = ({ resumen, handleMenuOpen, handleOpenHistorial }) => {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
// Función para formatear moneda
|
|
||||||
const formatCurrency = (value: number) => `$${value.toFixed(2)}`;
|
const formatCurrency = (value: number) => `$${value.toFixed(2)}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -43,20 +42,16 @@ const SuscriptorRow: React.FC<{
|
|||||||
<TableCell><IconButton size="small" onClick={() => setOpen(!open)}>{open ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}</IconButton></TableCell>
|
<TableCell><IconButton size="small" onClick={() => setOpen(!open)}>{open ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}</IconButton></TableCell>
|
||||||
<TableCell component="th" scope="row">{resumen.nombreSuscriptor}</TableCell>
|
<TableCell component="th" scope="row">{resumen.nombreSuscriptor}</TableCell>
|
||||||
<TableCell align="right">
|
<TableCell align="right">
|
||||||
<Typography variant="body2" sx={{ fontWeight: 'bold', color: resumen.saldoPendienteTotal > 0 ? 'error.main' : 'success.main' }}>
|
<Typography variant="body2" sx={{ fontWeight: 'bold', color: resumen.saldoPendienteTotal > 0 ? 'error.main' : 'success.main' }}>{formatCurrency(resumen.saldoPendienteTotal)}</Typography>
|
||||||
{formatCurrency(resumen.saldoPendienteTotal)}
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="caption" color="text.secondary">de {formatCurrency(resumen.importeTotal)}</Typography>
|
<Typography variant="caption" color="text.secondary">de {formatCurrency(resumen.importeTotal)}</Typography>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell colSpan={7}></TableCell>
|
<TableCell colSpan={6}></TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell style={{ paddingBottom: 0, paddingTop: 0 }} colSpan={10}>
|
<TableCell style={{ paddingBottom: 0, paddingTop: 0 }} colSpan={9}> {/* <-- Ajustado para la nueva columna */}
|
||||||
<Collapse in={open} timeout="auto" unmountOnExit>
|
<Collapse in={open} timeout="auto" unmountOnExit>
|
||||||
<Box sx={{ margin: 1, padding: 2, backgroundColor: 'grey.50', borderRadius: 1 }}>
|
<Box sx={{ margin: 1, padding: 2, backgroundColor: 'grey.50', borderRadius: 1 }}>
|
||||||
<Typography variant="h6" gutterBottom component="div" sx={{ fontSize: '1rem', fontWeight: 'bold' }}>
|
<Typography variant="h6" gutterBottom component="div" sx={{ fontSize: '1rem', fontWeight: 'bold' }}>Facturas del Período para {resumen.nombreSuscriptor}</Typography>
|
||||||
Facturas del Período para {resumen.nombreSuscriptor}
|
|
||||||
</Typography>
|
|
||||||
<Table size="small">
|
<Table size="small">
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
@@ -64,6 +59,7 @@ const SuscriptorRow: React.FC<{
|
|||||||
<TableCell align="right">Importe Total</TableCell>
|
<TableCell align="right">Importe Total</TableCell>
|
||||||
<TableCell align="right">Pagado</TableCell>
|
<TableCell align="right">Pagado</TableCell>
|
||||||
<TableCell align="right">Saldo</TableCell>
|
<TableCell align="right">Saldo</TableCell>
|
||||||
|
<TableCell>Tipo Factura</TableCell>
|
||||||
<TableCell>Estado Pago</TableCell>
|
<TableCell>Estado Pago</TableCell>
|
||||||
<TableCell>Estado Facturación</TableCell>
|
<TableCell>Estado Facturación</TableCell>
|
||||||
<TableCell>Nro. Factura</TableCell>
|
<TableCell>Nro. Factura</TableCell>
|
||||||
@@ -77,35 +73,17 @@ const SuscriptorRow: React.FC<{
|
|||||||
<TableRow key={factura.idFactura}>
|
<TableRow key={factura.idFactura}>
|
||||||
<TableCell sx={{ fontWeight: 'medium' }}>{factura.nombreEmpresa}</TableCell>
|
<TableCell sx={{ fontWeight: 'medium' }}>{factura.nombreEmpresa}</TableCell>
|
||||||
<TableCell align="right">{formatCurrency(factura.importeFinal)}</TableCell>
|
<TableCell align="right">{formatCurrency(factura.importeFinal)}</TableCell>
|
||||||
<TableCell align="right" sx={{ color: 'success.dark' }}>
|
<TableCell align="right" sx={{ color: 'success.dark' }}>{formatCurrency(factura.totalPagado)}</TableCell>
|
||||||
{formatCurrency(factura.totalPagado)}
|
<TableCell align="right" sx={{ fontWeight: 'bold', color: saldo > 0 ? 'error.main' : 'inherit' }}>{formatCurrency(saldo)}</TableCell>
|
||||||
</TableCell>
|
|
||||||
<TableCell align="right" sx={{ fontWeight: 'bold', color: saldo > 0 ? 'error.main' : 'inherit' }}>
|
|
||||||
{formatCurrency(saldo)}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Chip
|
<Chip label={factura.tipoFactura} size="small" color={factura.tipoFactura === 'Alta' ? 'secondary' : 'default'} />
|
||||||
label={factura.estadoPago}
|
|
||||||
size="small"
|
|
||||||
color={
|
|
||||||
factura.estadoPago === 'Pagada' ? 'success' :
|
|
||||||
factura.estadoPago === 'Pagada Parcialmente' ? 'primary' :
|
|
||||||
factura.estadoPago === 'Rechazada' ? 'error' :
|
|
||||||
'default'
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
<TableCell><Chip label={factura.estadoPago} size="small" color={factura.estadoPago === 'Pagada' ? 'success' : (factura.estadoPago === 'Pagada Parcialmente' ? 'primary' : (factura.estadoPago === 'Rechazada' ? 'error' : 'default'))} /></TableCell>
|
||||||
<TableCell><Chip label={factura.estadoFacturacion} size="small" color={factura.estadoFacturacion === 'Facturado' ? 'info' : 'warning'} /></TableCell>
|
<TableCell><Chip label={factura.estadoFacturacion} size="small" color={factura.estadoFacturacion === 'Facturado' ? 'info' : 'warning'} /></TableCell>
|
||||||
<TableCell>{factura.numeroFactura || '-'}</TableCell>
|
<TableCell>{factura.numeroFactura || '-'}</TableCell>
|
||||||
<TableCell align="right">
|
<TableCell align="right">
|
||||||
<IconButton onClick={(e) => handleMenuOpen(e, factura)} disabled={factura.estadoPago === 'Anulada'}>
|
<IconButton onClick={(e) => handleMenuOpen(e, factura)} disabled={factura.estadoPago === 'Anulada'}><MoreVertIcon /></IconButton>
|
||||||
<MoreVertIcon />
|
<Tooltip title="Ver Historial de Envíos"><IconButton onClick={() => handleOpenHistorial(factura)}><MailOutlineIcon /></IconButton></Tooltip>
|
||||||
</IconButton>
|
|
||||||
<Tooltip title="Ver Historial de Envíos">
|
|
||||||
<IconButton onClick={() => handleOpenHistorial(factura)}>
|
|
||||||
<MailOutlineIcon />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
);
|
);
|
||||||
@@ -135,6 +113,7 @@ const ConsultaFacturasPage: React.FC = () => {
|
|||||||
const [filtroNombre, setFiltroNombre] = useState('');
|
const [filtroNombre, setFiltroNombre] = useState('');
|
||||||
const [filtroEstadoPago, setFiltroEstadoPago] = useState('');
|
const [filtroEstadoPago, setFiltroEstadoPago] = useState('');
|
||||||
const [filtroEstadoFacturacion, setFiltroEstadoFacturacion] = useState('');
|
const [filtroEstadoFacturacion, setFiltroEstadoFacturacion] = useState('');
|
||||||
|
const [filtroTipoFactura, setFiltroTipoFactura] = useState('');
|
||||||
|
|
||||||
const [selectedFactura, setSelectedFactura] = useState<FacturaConsolidadaDto | null>(null);
|
const [selectedFactura, setSelectedFactura] = useState<FacturaConsolidadaDto | null>(null);
|
||||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||||
@@ -154,7 +133,8 @@ const ConsultaFacturasPage: React.FC = () => {
|
|||||||
selectedMes,
|
selectedMes,
|
||||||
filtroNombre || undefined,
|
filtroNombre || undefined,
|
||||||
filtroEstadoPago || undefined,
|
filtroEstadoPago || undefined,
|
||||||
filtroEstadoFacturacion || undefined
|
filtroEstadoFacturacion || undefined,
|
||||||
|
filtroTipoFactura || undefined
|
||||||
);
|
);
|
||||||
setResumenes(data);
|
setResumenes(data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -163,7 +143,7 @@ const ConsultaFacturasPage: React.FC = () => {
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [selectedAnio, selectedMes, puedeConsultar, filtroNombre, filtroEstadoPago, filtroEstadoFacturacion]);
|
}, [selectedAnio, selectedMes, puedeConsultar, filtroNombre, filtroEstadoPago, filtroEstadoFacturacion, filtroTipoFactura]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
@@ -251,6 +231,17 @@ const ConsultaFacturasPage: React.FC = () => {
|
|||||||
<TextField label="Buscar por Suscriptor" size="small" value={filtroNombre} onChange={(e) => setFiltroNombre(e.target.value)} sx={{ flexGrow: 1, minWidth: '200px' }} />
|
<TextField label="Buscar por Suscriptor" size="small" value={filtroNombre} onChange={(e) => setFiltroNombre(e.target.value)} sx={{ flexGrow: 1, minWidth: '200px' }} />
|
||||||
<FormControl sx={{ minWidth: 200 }} size="small"><InputLabel>Estado de Pago</InputLabel><Select value={filtroEstadoPago} label="Estado de Pago" onChange={(e) => setFiltroEstadoPago(e.target.value)}><MenuItem value=""><em>Todos</em></MenuItem>{estadosPago.map(e => <MenuItem key={e} value={e}>{e}</MenuItem>)}</Select></FormControl>
|
<FormControl sx={{ minWidth: 200 }} size="small"><InputLabel>Estado de Pago</InputLabel><Select value={filtroEstadoPago} label="Estado de Pago" onChange={(e) => setFiltroEstadoPago(e.target.value)}><MenuItem value=""><em>Todos</em></MenuItem>{estadosPago.map(e => <MenuItem key={e} value={e}>{e}</MenuItem>)}</Select></FormControl>
|
||||||
<FormControl sx={{ minWidth: 200 }} size="small"><InputLabel>Estado de Facturación</InputLabel><Select value={filtroEstadoFacturacion} label="Estado de Facturación" onChange={(e) => setFiltroEstadoFacturacion(e.target.value)}><MenuItem value=""><em>Todos</em></MenuItem>{estadosFacturacion.map(e => <MenuItem key={e} value={e}>{e}</MenuItem>)}</Select></FormControl>
|
<FormControl sx={{ minWidth: 200 }} size="small"><InputLabel>Estado de Facturación</InputLabel><Select value={filtroEstadoFacturacion} label="Estado de Facturación" onChange={(e) => setFiltroEstadoFacturacion(e.target.value)}><MenuItem value=""><em>Todos</em></MenuItem>{estadosFacturacion.map(e => <MenuItem key={e} value={e}>{e}</MenuItem>)}</Select></FormControl>
|
||||||
|
<FormControl sx={{ minWidth: 200 }} size="small">
|
||||||
|
<InputLabel>Tipo de Factura</InputLabel>
|
||||||
|
<Select
|
||||||
|
value={filtroTipoFactura}
|
||||||
|
label="Tipo de Factura"
|
||||||
|
onChange={(e) => setFiltroTipoFactura(e.target.value)}
|
||||||
|
>
|
||||||
|
<MenuItem value=""><em>Todos</em></MenuItem>
|
||||||
|
{tiposFactura.map(t => <MenuItem key={t} value={t}>{t}</MenuItem>)}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
</Box>
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
@@ -259,7 +250,7 @@ const ConsultaFacturasPage: React.FC = () => {
|
|||||||
|
|
||||||
<TableContainer component={Paper}>
|
<TableContainer component={Paper}>
|
||||||
<Table aria-label="collapsible table">
|
<Table aria-label="collapsible table">
|
||||||
<TableHead><TableRow><TableCell /><TableCell>Suscriptor</TableCell><TableCell align="right">Saldo Total / Importe Total</TableCell><TableCell colSpan={5}></TableCell></TableRow></TableHead>
|
<TableHead><TableRow><TableCell /><TableCell>Suscriptor</TableCell><TableCell align="right">Saldo Total / Importe Total</TableCell><TableCell colSpan={6}></TableCell></TableRow></TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{loading ? (<TableRow><TableCell colSpan={8} align="center"><CircularProgress /></TableCell></TableRow>)
|
{loading ? (<TableRow><TableCell colSpan={8} align="center"><CircularProgress /></TableCell></TableRow>)
|
||||||
: resumenes.length === 0 ? (<TableRow><TableCell colSpan={8} align="center">No hay facturas para el período seleccionado.</TableCell></TableRow>)
|
: resumenes.length === 0 ? (<TableRow><TableCell colSpan={8} align="center">No hay facturas para el período seleccionado.</TableCell></TableRow>)
|
||||||
@@ -285,22 +276,9 @@ const ConsultaFacturasPage: React.FC = () => {
|
|||||||
open={pagoModalOpen}
|
open={pagoModalOpen}
|
||||||
onClose={handleClosePagoModal}
|
onClose={handleClosePagoModal}
|
||||||
onSubmit={handleSubmitPagoModal}
|
onSubmit={handleSubmitPagoModal}
|
||||||
factura={
|
factura={selectedFactura}
|
||||||
selectedFactura ? {
|
nombreSuscriptor={
|
||||||
idFactura: selectedFactura.idFactura,
|
resumenes.find(r => r.idSuscriptor === selectedFactura?.idSuscriptor)?.nombreSuscriptor || ''
|
||||||
nombreSuscriptor: resumenes.find(r => r.idSuscriptor === resumenes.find(res => res.facturas.some(f => f.idFactura === selectedFactura.idFactura))?.idSuscriptor)?.nombreSuscriptor || '',
|
|
||||||
importeFinal: selectedFactura.importeFinal,
|
|
||||||
saldoPendiente: selectedFactura.importeFinal - selectedFactura.totalPagado,
|
|
||||||
totalPagado: selectedFactura.totalPagado,
|
|
||||||
idSuscriptor: resumenes.find(res => res.facturas.some(f => f.idFactura === selectedFactura.idFactura))?.idSuscriptor || 0,
|
|
||||||
periodo: '',
|
|
||||||
fechaEmision: '',
|
|
||||||
fechaVencimiento: '',
|
|
||||||
estadoPago: selectedFactura.estadoPago,
|
|
||||||
estadoFacturacion: selectedFactura.estadoFacturacion,
|
|
||||||
numeroFactura: selectedFactura.numeroFactura,
|
|
||||||
detalles: selectedFactura.detalles,
|
|
||||||
} : null
|
|
||||||
}
|
}
|
||||||
errorMessage={apiError}
|
errorMessage={apiError}
|
||||||
clearErrorMessage={() => setApiError(null)}
|
clearErrorMessage={() => setApiError(null)}
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
// src/routes/AppRoutes.tsx
|
|
||||||
import React, { type JSX } from 'react';
|
import React, { type JSX } from 'react';
|
||||||
import { BrowserRouter, Routes, Route, Navigate, Outlet } from 'react-router-dom';
|
import { BrowserRouter, Routes, Route, Navigate, Outlet } from 'react-router-dom';
|
||||||
import LoginPage from '../pages/LoginPage';
|
import LoginPage from '../pages/LoginPage';
|
||||||
import HomePage from '../pages/HomePage';
|
import HomePage from '../pages/HomePage';
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
import MainLayout from '../layouts/MainLayout';
|
import MainLayout from '../layouts/MainLayout';
|
||||||
import { Typography } from '@mui/material';
|
|
||||||
import SectionProtectedRoute from './SectionProtectedRoute';
|
import SectionProtectedRoute from './SectionProtectedRoute';
|
||||||
|
|
||||||
// Distribución
|
// Distribución
|
||||||
@@ -267,7 +265,6 @@ const AppRoutes = () => {
|
|||||||
<ReportesIndexPage />
|
<ReportesIndexPage />
|
||||||
</SectionProtectedRoute>}
|
</SectionProtectedRoute>}
|
||||||
>
|
>
|
||||||
<Route index element={<Typography sx={{ p: 2 }}>Seleccione un reporte del menú lateral.</Typography>} /> {/* Placeholder */}
|
|
||||||
<Route path="existencia-papel" element={<ReporteExistenciaPapelPage />} />
|
<Route path="existencia-papel" element={<ReporteExistenciaPapelPage />} />
|
||||||
<Route path="movimiento-bobinas" element={<ReporteMovimientoBobinasPage />} />
|
<Route path="movimiento-bobinas" element={<ReporteMovimientoBobinasPage />} />
|
||||||
<Route path="movimiento-bobinas-estado" element={<ReporteMovimientoBobinasEstadoPage />} />
|
<Route path="movimiento-bobinas-estado" element={<ReporteMovimientoBobinasEstadoPage />} />
|
||||||
|
|||||||
@@ -19,11 +19,16 @@ const procesarArchivoRespuesta = async (archivo: File): Promise<ProcesamientoLot
|
|||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getResumenesDeCuentaPorPeriodo = async (anio: number, mes: number, nombreSuscriptor?: string, estadoPago?: string, estadoFacturacion?: string): Promise<ResumenCuentaSuscriptorDto[]> => {
|
const getResumenesDeCuentaPorPeriodo = async (
|
||||||
|
anio: number, mes: number,
|
||||||
|
nombreSuscriptor?: string, estadoPago?: string, estadoFacturacion?: string,
|
||||||
|
tipoFactura?: string
|
||||||
|
): Promise<ResumenCuentaSuscriptorDto[]> => {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
if (nombreSuscriptor) params.append('nombreSuscriptor', nombreSuscriptor);
|
if (nombreSuscriptor) params.append('nombreSuscriptor', nombreSuscriptor);
|
||||||
if (estadoPago) params.append('estadoPago', estadoPago);
|
if (estadoPago) params.append('estadoPago', estadoPago);
|
||||||
if (estadoFacturacion) params.append('estadoFacturacion', estadoFacturacion);
|
if (estadoFacturacion) params.append('estadoFacturacion', estadoFacturacion);
|
||||||
|
if (tipoFactura) params.append('tipoFactura', tipoFactura);
|
||||||
|
|
||||||
const queryString = params.toString();
|
const queryString = params.toString();
|
||||||
const url = `${API_URL}/${anio}/${mes}${queryString ? `?${queryString}` : ''}`;
|
const url = `${API_URL}/${anio}/${mes}${queryString ? `?${queryString}` : ''}`;
|
||||||
|
|||||||
16
README.md
16
README.md
@@ -60,6 +60,22 @@ El sistema está organizado en varios módulos clave para cubrir todas las área
|
|||||||
- **Autenticación Segura:** Mediante JSON Web Tokens (JWT).
|
- **Autenticación Segura:** Mediante JSON Web Tokens (JWT).
|
||||||
- **Auditoría:** Todas las modificaciones a los datos maestros y transacciones importantes se registran en tablas de historial (`_H`).
|
- **Auditoría:** Todas las modificaciones a los datos maestros y transacciones importantes se registran en tablas de historial (`_H`).
|
||||||
|
|
||||||
|
### 📨 Suscripciones
|
||||||
|
- **Gestión de Suscriptores:** ABM completo de clientes, incluyendo datos de contacto, dirección de entrega y forma de pago preferida.
|
||||||
|
- **Ciclo de Vida de la Suscripción:** Creación y administración de suscripciones por cliente y publicación, con fechas de inicio, fin, días de entrega y estados (`Activa`, `Pausada`, `Cancelada`).
|
||||||
|
- **Facturación Proporcional (Pro-rata):** El sistema genera automáticamente una "Factura de Alta" por el monto proporcional cuando un cliente se suscribe en un período ya cerrado, evitando cobros excesivos en la primera factura.
|
||||||
|
- **Gestión de Promociones:** ABM de promociones (ej. descuentos porcentuales, bonificación de días) y asignación a suscripciones específicas con vigencia definida.
|
||||||
|
- **Cuenta Corriente del Suscriptor:** Administración de ajustes manuales (`Crédito`/`Débito`) para manejar situaciones excepcionales como notas de crédito, devoluciones o cargos especiales.
|
||||||
|
- **Procesos de Cierre Mensual:**
|
||||||
|
- **Generación de Cierre:** Proceso masivo que calcula y genera todas las facturas del período, aplicando promociones y ajustes.
|
||||||
|
- **Notificaciones Automáticas:** Envío automático de resúmenes de cuenta por email a cada suscriptor al generar el cierre.
|
||||||
|
- **Gestión de Débito Automático:**
|
||||||
|
- **Generación de Archivo:** Creación del archivo de texto plano en formato "Pago Directo" para el Banco Galicia. Las "Facturas de Alta" se excluyen automáticamente de este proceso.
|
||||||
|
- **Procesamiento de Respuesta:** Herramienta para procesar el archivo de respuesta del banco, actualizando los estados de pago (`Pagada`/`Rechazada`) de forma masiva.
|
||||||
|
- **Auditoría de Comunicaciones:**
|
||||||
|
- **Log de Envíos:** Registro detallado de cada correo electrónico enviado (individual o masivo), incluyendo estado (`Enviado`/`Fallido`) y mensajes de error.
|
||||||
|
- **Historial de Envíos:** Interfaz para consultar el historial de notificaciones enviadas por cada factura o por cada lote de cierre mensual.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🛠️ Stack Tecnológico
|
## 🛠️ Stack Tecnológico
|
||||||
|
|||||||
Reference in New Issue
Block a user