Feat: Mejora UI de Cuenta Corriente y corrige colores en email de aviso
Este commit introduce significativas mejoras de usabilidad en la página de gestión de ajustes del suscriptor y corrige la representación visual de los ajustes en el email de notificación mensual. ### ✨ Nuevas Características y Mejoras de UI - **Nuevos Filtros en Cuenta Corriente del Suscriptor:** - Se han añadido nuevos filtros desplegables en la página de "Cuenta Corriente" para filtrar los ajustes por **Estado** (`Pendiente`, `Aplicado`, `Anulado`) y por **Tipo** (`Crédito`, `Débito`). - Esta mejora permite a los usuarios encontrar registros específicos de manera mucho más rápida y eficiente, especialmente para suscriptores con un largo historial de ajustes. - **Visualización Mejorada del Estado de Ajuste:** - La columna "Estado" en la tabla de ajustes ahora muestra el número de factura oficial (ej. `A-0001-12345`) si un ajuste ha sido aplicado y la factura ya está numerada. - Si la factura aún no tiene un número oficial, se muestra una referencia al ID interno (ej. `ID Interno #64`) para mantener la trazabilidad. - Para soportar esto, se ha enriquecido el `AjusteDto` en el backend para incluir el `NumeroFacturaAplicado`. ### 🐛 Corrección y Refactorización - **Corrección de Colores en Email de Aviso:** - Se han invertido los colores de los montos de ajuste en el email de aviso mensual enviado al cliente para alinearlos con la perspectiva del usuario. - **Créditos** (descuentos a favor del cliente) ahora se muestran en **verde** (positivo). - **Débitos** (cargos extra) ahora se muestran en **rojo** (negativo). - Este cambio mejora drásticamente la claridad del resumen de cuenta y evita posibles confusiones. ### ⚙️ Cambios Técnicos de Soporte - Se ha añadido el método `GetByIdsAsync` al `IFacturaRepository` para optimizar la obtención de datos de múltiples facturas en una sola consulta, evitando el problema N+1. - El `AjusteService` ha sido actualizado para utilizar este nuevo método y poblar eficientemente la información de la factura en el DTO de ajuste que se envía al frontend.
This commit is contained in:
@@ -244,5 +244,16 @@ namespace GestionIntegral.Api.Data.Repositories.Suscripciones
|
|||||||
return Enumerable.Empty<Factura>();
|
return Enumerable.Empty<Factura>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<Factura>> GetByIdsAsync(IEnumerable<int> ids)
|
||||||
|
{
|
||||||
|
if (ids == null || !ids.Any())
|
||||||
|
{
|
||||||
|
return Enumerable.Empty<Factura>();
|
||||||
|
}
|
||||||
|
const string sql = "SELECT * FROM dbo.susc_Facturas WHERE IdFactura IN @Ids;";
|
||||||
|
using var connection = _connectionFactory.CreateConnection();
|
||||||
|
return await connection.QueryAsync<Factura>(sql, new { Ids = ids });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@ namespace GestionIntegral.Api.Data.Repositories.Suscripciones
|
|||||||
public interface IFacturaRepository
|
public interface IFacturaRepository
|
||||||
{
|
{
|
||||||
Task<Factura?> GetByIdAsync(int idFactura);
|
Task<Factura?> GetByIdAsync(int idFactura);
|
||||||
|
Task<IEnumerable<Factura>> GetByIdsAsync(IEnumerable<int> ids);
|
||||||
Task<IEnumerable<Factura>> GetByPeriodoAsync(string periodo);
|
Task<IEnumerable<Factura>> GetByPeriodoAsync(string periodo);
|
||||||
Task<Factura?> GetBySuscriptorYPeriodoAsync(int idSuscriptor, string periodo, IDbTransaction transaction);
|
Task<Factura?> GetBySuscriptorYPeriodoAsync(int idSuscriptor, string periodo, IDbTransaction transaction);
|
||||||
Task<IEnumerable<Factura>> GetListBySuscriptorYPeriodoAsync(int idSuscriptor, string periodo);
|
Task<IEnumerable<Factura>> GetListBySuscriptorYPeriodoAsync(int idSuscriptor, string periodo);
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ namespace GestionIntegral.Api.Dtos.Suscripciones
|
|||||||
public string Motivo { get; set; } = string.Empty;
|
public string Motivo { get; set; } = string.Empty;
|
||||||
public string Estado { get; set; } = string.Empty;
|
public string Estado { get; set; } = string.Empty;
|
||||||
public int? IdFacturaAplicado { get; set; }
|
public int? IdFacturaAplicado { get; set; }
|
||||||
|
public string? NumeroFacturaAplicado { get; set; }
|
||||||
public string FechaAlta { get; set; } = string.Empty;
|
public string FechaAlta { get; set; } = string.Empty;
|
||||||
public string NombreUsuarioAlta { get; set; } = string.Empty;
|
public string NombreUsuarioAlta { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
|||||||
private readonly ISuscriptorRepository _suscriptorRepository;
|
private readonly ISuscriptorRepository _suscriptorRepository;
|
||||||
private readonly IUsuarioRepository _usuarioRepository;
|
private readonly IUsuarioRepository _usuarioRepository;
|
||||||
private readonly IEmpresaRepository _empresaRepository;
|
private readonly IEmpresaRepository _empresaRepository;
|
||||||
|
private readonly IFacturaRepository _facturaRepository;
|
||||||
private readonly DbConnectionFactory _connectionFactory;
|
private readonly DbConnectionFactory _connectionFactory;
|
||||||
private readonly ILogger<AjusteService> _logger;
|
private readonly ILogger<AjusteService> _logger;
|
||||||
|
|
||||||
@@ -22,6 +23,7 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
|||||||
ISuscriptorRepository suscriptorRepository,
|
ISuscriptorRepository suscriptorRepository,
|
||||||
IUsuarioRepository usuarioRepository,
|
IUsuarioRepository usuarioRepository,
|
||||||
IEmpresaRepository empresaRepository,
|
IEmpresaRepository empresaRepository,
|
||||||
|
IFacturaRepository facturaRepository,
|
||||||
DbConnectionFactory connectionFactory,
|
DbConnectionFactory connectionFactory,
|
||||||
ILogger<AjusteService> logger)
|
ILogger<AjusteService> logger)
|
||||||
{
|
{
|
||||||
@@ -29,6 +31,7 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
|||||||
_suscriptorRepository = suscriptorRepository;
|
_suscriptorRepository = suscriptorRepository;
|
||||||
_usuarioRepository = usuarioRepository;
|
_usuarioRepository = usuarioRepository;
|
||||||
_empresaRepository = empresaRepository;
|
_empresaRepository = empresaRepository;
|
||||||
|
_facturaRepository = facturaRepository;
|
||||||
_connectionFactory = connectionFactory;
|
_connectionFactory = connectionFactory;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
@@ -63,27 +66,34 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
|||||||
return Enumerable.Empty<AjusteDto>();
|
return Enumerable.Empty<AjusteDto>();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. Recolectar IDs únicos de usuarios Y empresas de la lista de ajustes
|
// 1. Recolectar IDs de usuarios, empresas Y FACTURAS
|
||||||
var idsUsuarios = ajustes.Select(a => a.IdUsuarioAlta).Distinct().ToList();
|
var idsUsuarios = ajustes.Select(a => a.IdUsuarioAlta).Distinct().ToList();
|
||||||
var idsEmpresas = ajustes.Select(a => a.IdEmpresa).Distinct().ToList();
|
var idsEmpresas = ajustes.Select(a => a.IdEmpresa).Distinct().ToList();
|
||||||
|
var idsFacturas = ajustes.Where(a => a.IdFacturaAplicado.HasValue)
|
||||||
|
.Select(a => a.IdFacturaAplicado!.Value)
|
||||||
|
.Distinct().ToList();
|
||||||
|
|
||||||
// 2. Obtener todos los usuarios y empresas necesarios en dos consultas masivas.
|
// 2. Obtener todos los datos necesarios en consultas masivas
|
||||||
var usuariosTask = _usuarioRepository.GetByIdsAsync(idsUsuarios);
|
var usuariosTask = _usuarioRepository.GetByIdsAsync(idsUsuarios);
|
||||||
var empresasTask = _empresaRepository.GetAllAsync(null, null); // Asumiendo que GetAllAsync es suficiente o crear un GetByIds.
|
var empresasTask = _empresaRepository.GetAllAsync(null, null);
|
||||||
|
var facturasTask = _facturaRepository.GetByIdsAsync(idsFacturas);
|
||||||
|
|
||||||
// Esperamos a que ambas consultas terminen
|
await Task.WhenAll(usuariosTask, empresasTask, facturasTask);
|
||||||
await Task.WhenAll(usuariosTask, empresasTask);
|
|
||||||
|
|
||||||
// Convertimos los resultados a diccionarios para búsqueda rápida
|
// 3. Convertir a diccionarios para búsqueda rápida
|
||||||
var usuariosDict = (await usuariosTask).ToDictionary(u => u.Id);
|
var usuariosDict = (await usuariosTask).ToDictionary(u => u.Id);
|
||||||
var empresasDict = (await empresasTask).ToDictionary(e => e.IdEmpresa);
|
var empresasDict = (await empresasTask).ToDictionary(e => e.IdEmpresa);
|
||||||
|
var facturasDict = (await facturasTask).ToDictionary(f => f.IdFactura);
|
||||||
|
|
||||||
// 3. Mapear en memoria, ahora con toda la información disponible.
|
// 4. Mapear en memoria, ahora con la información de la factura disponible
|
||||||
var dtos = ajustes.Select(ajuste =>
|
var dtos = ajustes.Select(ajuste =>
|
||||||
{
|
{
|
||||||
usuariosDict.TryGetValue(ajuste.IdUsuarioAlta, out var usuario);
|
usuariosDict.TryGetValue(ajuste.IdUsuarioAlta, out var usuario);
|
||||||
empresasDict.TryGetValue(ajuste.IdEmpresa, out var empresa);
|
empresasDict.TryGetValue(ajuste.IdEmpresa, out var empresa);
|
||||||
|
|
||||||
|
// Buscar la factura en el diccionario si el ajuste está aplicado
|
||||||
|
facturasDict.TryGetValue(ajuste.IdFacturaAplicado ?? 0, out var factura);
|
||||||
|
|
||||||
return new AjusteDto
|
return new AjusteDto
|
||||||
{
|
{
|
||||||
IdAjuste = ajuste.IdAjuste,
|
IdAjuste = ajuste.IdAjuste,
|
||||||
@@ -96,6 +106,7 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
|||||||
Motivo = ajuste.Motivo,
|
Motivo = ajuste.Motivo,
|
||||||
Estado = ajuste.Estado,
|
Estado = ajuste.Estado,
|
||||||
IdFacturaAplicado = ajuste.IdFacturaAplicado,
|
IdFacturaAplicado = ajuste.IdFacturaAplicado,
|
||||||
|
NumeroFacturaAplicado = factura?.NumeroFactura,
|
||||||
FechaAlta = ajuste.FechaAlta.ToString("yyyy-MM-dd HH:mm"),
|
FechaAlta = ajuste.FechaAlta.ToString("yyyy-MM-dd HH:mm"),
|
||||||
NombreUsuarioAlta = usuario != null ? $"{usuario.Nombre} {usuario.Apellido}" : "N/A"
|
NombreUsuarioAlta = usuario != null ? $"{usuario.Nombre} {usuario.Apellido}" : "N/A"
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -314,7 +314,6 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
|||||||
var periodo = $"{anio}-{mes:D2}";
|
var periodo = $"{anio}-{mes:D2}";
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// 1. Reemplazamos la llamada original por la nueva, que ya trae toda la información necesaria.
|
|
||||||
var facturasConEmpresa = await _facturaRepository.GetFacturasConEmpresaAsync(idSuscriptor, periodo);
|
var facturasConEmpresa = await _facturaRepository.GetFacturasConEmpresaAsync(idSuscriptor, periodo);
|
||||||
if (!facturasConEmpresa.Any()) return (false, "No se encontraron facturas para este suscriptor en el período.");
|
if (!facturasConEmpresa.Any()) return (false, "No se encontraron facturas para este suscriptor en el período.");
|
||||||
|
|
||||||
@@ -324,36 +323,33 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
|||||||
var resumenHtml = new StringBuilder();
|
var resumenHtml = new StringBuilder();
|
||||||
var adjuntos = new List<(byte[] content, string name)>();
|
var adjuntos = new List<(byte[] content, string name)>();
|
||||||
|
|
||||||
// 2. Iteramos sobre la nueva lista de tuplas.
|
|
||||||
foreach (var item in facturasConEmpresa.Where(f => f.Factura.EstadoPago != "Anulada"))
|
foreach (var item in facturasConEmpresa.Where(f => f.Factura.EstadoPago != "Anulada"))
|
||||||
{
|
{
|
||||||
var factura = item.Factura;
|
var factura = item.Factura;
|
||||||
var nombreEmpresa = item.NombreEmpresa;
|
var nombreEmpresa = item.NombreEmpresa;
|
||||||
|
|
||||||
// 3. Eliminamos la lógica compleja y propensa a errores para obtener la empresa.
|
|
||||||
// La llamada a GetDetallesPorFacturaIdAsync sigue siendo necesaria para el cuerpo del email.
|
|
||||||
var detalles = await _facturaDetalleRepository.GetDetallesPorFacturaIdAsync(factura.IdFactura);
|
var detalles = await _facturaDetalleRepository.GetDetallesPorFacturaIdAsync(factura.IdFactura);
|
||||||
|
|
||||||
// Título mejorado para claridad
|
|
||||||
resumenHtml.Append($"<h4 style='margin-top: 20px; margin-bottom: 10px; color: #34515e;'>Resumen para {nombreEmpresa}</h4>");
|
resumenHtml.Append($"<h4 style='margin-top: 20px; margin-bottom: 10px; color: #34515e;'>Resumen para {nombreEmpresa}</h4>");
|
||||||
resumenHtml.Append("<table style='width: 100%; border-collapse: collapse; font-size: 0.9em;'>");
|
resumenHtml.Append("<table style='width: 100%; border-collapse: collapse; font-size: 0.9em;'>");
|
||||||
|
|
||||||
// 1. Mostrar Detalles de Suscripciones
|
|
||||||
foreach (var detalle in detalles)
|
foreach (var detalle in detalles)
|
||||||
{
|
{
|
||||||
resumenHtml.Append($"<tr><td style='padding: 5px; border-bottom: 1px solid #eee;'>{detalle.Descripcion}</td><td style='padding: 5px; border-bottom: 1px solid #eee; text-align: right;'>${detalle.ImporteNeto:N2}</td></tr>");
|
resumenHtml.Append($"<tr><td style='padding: 5px; border-bottom: 1px solid #eee;'>{detalle.Descripcion}</td><td style='padding: 5px; border-bottom: 1px solid #eee; text-align: right;'>${detalle.ImporteNeto:N2}</td></tr>");
|
||||||
}
|
}
|
||||||
|
|
||||||
var ajustes = await _ajusteRepository.GetAjustesPorIdFacturaAsync(factura.IdFactura);
|
var ajustes = await _ajusteRepository.GetAjustesPorIdFacturaAsync(factura.IdFactura);
|
||||||
if (ajustes.Any())
|
if (ajustes.Any())
|
||||||
{
|
{
|
||||||
foreach (var ajuste in ajustes)
|
foreach (var ajuste in ajustes)
|
||||||
{
|
{
|
||||||
bool esCredito = ajuste.TipoAjuste == "Credito";
|
bool esCredito = ajuste.TipoAjuste == "Credito";
|
||||||
string colorMonto = esCredito ? "#d9534f" : "#5cb85c";
|
string colorMonto = esCredito ? "#5cb85c" : "#d9534f";
|
||||||
string signo = esCredito ? "-" : "+";
|
string signo = esCredito ? "-" : "+";
|
||||||
resumenHtml.Append($"<tr><td style='padding: 5px; border-bottom: 1px solid #eee; font-style: italic;'>Ajuste: {ajuste.Motivo}</td><td style='padding: 5px; border-bottom: 1px solid #eee; text-align: right; color: {colorMonto}; font-style: italic;'>{signo} ${ajuste.Monto:N2}</td></tr>");
|
resumenHtml.Append($"<tr><td style='padding: 5px; border-bottom: 1px solid #eee; font-style: italic;'>Ajuste: {ajuste.Motivo}</td><td style='padding: 5px; border-bottom: 1px solid #eee; text-align: right; color: {colorMonto}; font-style: italic;'>{signo} ${ajuste.Monto:N2}</td></tr>");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
resumenHtml.Append($"<tr style='font-weight: bold;'><td style='padding: 5px;'>Subtotal</td><td style='padding: 5px; text-align: right;'>${factura.ImporteFinal:N2}</td></tr>");
|
resumenHtml.Append($"<tr style='font-weight: bold;'><td style='padding: 5px;'>Subtotal</td><td style='padding: 5px; text-align: right;'>${factura.ImporteFinal:N2}</td></tr>");
|
||||||
resumenHtml.Append("</table>");
|
resumenHtml.Append("</table>");
|
||||||
|
|
||||||
@@ -363,7 +359,6 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
|||||||
if (File.Exists(rutaCompleta))
|
if (File.Exists(rutaCompleta))
|
||||||
{
|
{
|
||||||
byte[] pdfBytes = await File.ReadAllBytesAsync(rutaCompleta);
|
byte[] pdfBytes = await File.ReadAllBytesAsync(rutaCompleta);
|
||||||
// Usamos el nombre de la empresa para un nombre de archivo más claro
|
|
||||||
string pdfFileName = $"Factura_{nombreEmpresa.Replace(" ", "")}_{factura.NumeroFactura}.pdf";
|
string pdfFileName = $"Factura_{nombreEmpresa.Replace(" ", "")}_{factura.NumeroFactura}.pdf";
|
||||||
adjuntos.Add((pdfBytes, pdfFileName));
|
adjuntos.Add((pdfBytes, pdfFileName));
|
||||||
_logger.LogInformation("PDF adjuntado: {FileName}", pdfFileName);
|
_logger.LogInformation("PDF adjuntado: {FileName}", pdfFileName);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export interface AjusteDto {
|
|||||||
motivo: string;
|
motivo: string;
|
||||||
estado: 'Pendiente' | 'Aplicado' | 'Anulado';
|
estado: 'Pendiente' | 'Aplicado' | 'Anulado';
|
||||||
idFacturaAplicado?: number | null;
|
idFacturaAplicado?: number | null;
|
||||||
|
numeroFacturaAplicado?: string | null;
|
||||||
fechaAlta: string; // "yyyy-MM-dd HH:mm"
|
fechaAlta: string; // "yyyy-MM-dd HH:mm"
|
||||||
nombreUsuarioAlta: string;
|
nombreUsuarioAlta: string;
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
import React, { useState, useEffect, useCallback } from 'react';
|
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
import { Box, Typography, Button, Paper, CircularProgress, Alert, Table, TableContainer, TableHead, TableRow, TableCell, TableBody, Chip, Tooltip, IconButton, TextField } from '@mui/material';
|
import {
|
||||||
|
Box, Typography, Button, Paper, CircularProgress, Alert, Table, TableContainer, TableHead,
|
||||||
|
TableRow, TableCell, TableBody, Chip, Tooltip, IconButton, TextField,
|
||||||
|
FormControl, InputLabel, Select, MenuItem } from '@mui/material';
|
||||||
import AddIcon from '@mui/icons-material/Add';
|
import AddIcon from '@mui/icons-material/Add';
|
||||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||||
import EditIcon from '@mui/icons-material/Edit';
|
import EditIcon from '@mui/icons-material/Edit';
|
||||||
@@ -44,16 +47,23 @@ const CuentaCorrienteSuscriptorPage: React.FC = () => {
|
|||||||
const [filtroFechaDesde, setFiltroFechaDesde] = useState<string>(getInitialDateRange().fechaDesde);
|
const [filtroFechaDesde, setFiltroFechaDesde] = useState<string>(getInitialDateRange().fechaDesde);
|
||||||
const [filtroFechaHasta, setFiltroFechaHasta] = useState<string>(getInitialDateRange().fechaHasta);
|
const [filtroFechaHasta, setFiltroFechaHasta] = useState<string>(getInitialDateRange().fechaHasta);
|
||||||
|
|
||||||
|
const [filtroEstado, setFiltroEstado] = useState<string>('Todos'); // 'Todos', 'Pendiente', etc.
|
||||||
|
const [filtroTipo, setFiltroTipo] = useState<string>('Todos'); // 'Todos', 'Credito', 'Debito'
|
||||||
|
|
||||||
const { tienePermiso, isSuperAdmin } = usePermissions();
|
const { tienePermiso, isSuperAdmin } = usePermissions();
|
||||||
const puedeGestionar = isSuperAdmin || tienePermiso("SU011");
|
const puedeGestionar = isSuperAdmin || tienePermiso("SU011");
|
||||||
|
|
||||||
const cargarDatos = useCallback(async () => {
|
const cargarDatos = useCallback(async () => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (filtroFechaDesde) params.append('fechaDesde', filtroFechaDesde);
|
||||||
|
if (filtroFechaHasta) params.append('fechaHasta', filtroFechaHasta);
|
||||||
|
// NOTA: El filtrado por estado y tipo se hará en el frontend por simplicidad,
|
||||||
|
// pero si la cantidad de ajustes es muy grande, se deberían pasar estos filtros al backend.
|
||||||
if (isNaN(idSuscriptor)) {
|
if (isNaN(idSuscriptor)) {
|
||||||
setError("ID de Suscriptor inválido."); setLoading(false); return;
|
setError("ID de Suscriptor inválido."); setLoading(false); return;
|
||||||
}
|
}
|
||||||
setLoading(true); setApiErrorMessage(null); setError(null);
|
setLoading(true); setApiErrorMessage(null); setError(null);
|
||||||
try {
|
try {
|
||||||
// Usamos Promise.all para cargar todo en paralelo y mejorar el rendimiento
|
|
||||||
const [suscriptorData, ajustesData, empresasData] = await Promise.all([
|
const [suscriptorData, ajustesData, empresasData] = await Promise.all([
|
||||||
suscriptorService.getSuscriptorById(idSuscriptor),
|
suscriptorService.getSuscriptorById(idSuscriptor),
|
||||||
ajusteService.getAjustesPorSuscriptor(idSuscriptor, filtroFechaDesde || undefined, filtroFechaHasta || undefined),
|
ajusteService.getAjustesPorSuscriptor(idSuscriptor, filtroFechaDesde || undefined, filtroFechaHasta || undefined),
|
||||||
@@ -63,7 +73,6 @@ const CuentaCorrienteSuscriptorPage: React.FC = () => {
|
|||||||
setSuscriptor(suscriptorData);
|
setSuscriptor(suscriptorData);
|
||||||
setAjustes(ajustesData);
|
setAjustes(ajustesData);
|
||||||
setEmpresas(empresasData);
|
setEmpresas(empresasData);
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError("Error al cargar los datos.");
|
setError("Error al cargar los datos.");
|
||||||
} finally {
|
} finally {
|
||||||
@@ -73,6 +82,26 @@ const CuentaCorrienteSuscriptorPage: React.FC = () => {
|
|||||||
|
|
||||||
useEffect(() => { cargarDatos(); }, [cargarDatos]);
|
useEffect(() => { cargarDatos(); }, [cargarDatos]);
|
||||||
|
|
||||||
|
// Lógica de filtrado en el cliente usando useMemo para eficiencia
|
||||||
|
const ajustesFiltrados = useMemo(() => {
|
||||||
|
return ajustes.filter(a => {
|
||||||
|
const estadoMatch = filtroEstado === 'Todos' || a.estado === filtroEstado;
|
||||||
|
const tipoMatch = filtroTipo === 'Todos' || a.tipoAjuste === filtroTipo;
|
||||||
|
return estadoMatch && tipoMatch;
|
||||||
|
});
|
||||||
|
}, [ajustes, filtroEstado, filtroTipo]);
|
||||||
|
|
||||||
|
// Función para renderizar la celda de Estado de forma inteligente
|
||||||
|
const renderEstadoCell = (ajuste: AjusteDto) => {
|
||||||
|
if (ajuste.estado !== 'Aplicado' || !ajuste.idFacturaAplicado) {
|
||||||
|
return ajuste.estado;
|
||||||
|
}
|
||||||
|
if (ajuste.numeroFacturaAplicado) {
|
||||||
|
return `Aplicado (Fact. ${ajuste.numeroFacturaAplicado})`;
|
||||||
|
}
|
||||||
|
return `Aplicado (ID Interno #${ajuste.idFacturaAplicado})`;
|
||||||
|
};
|
||||||
|
|
||||||
// --- INICIO DE LA LÓGICA DE SINCRONIZACIÓN DE FECHAS ---
|
// --- INICIO DE LA LÓGICA DE SINCRONIZACIÓN DE FECHAS ---
|
||||||
const handleFechaDesdeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleFechaDesdeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const nuevaFechaDesde = e.target.value;
|
const nuevaFechaDesde = e.target.value;
|
||||||
@@ -152,7 +181,10 @@ const CuentaCorrienteSuscriptorPage: React.FC = () => {
|
|||||||
<Typography variant="h4" color="primary" gutterBottom>{suscriptor?.nombreCompleto || ''}</Typography>
|
<Typography variant="h4" color="primary" gutterBottom>{suscriptor?.nombreCompleto || ''}</Typography>
|
||||||
|
|
||||||
<Paper sx={{ p: 2, mb: 2 }}>
|
<Paper sx={{ p: 2, mb: 2 }}>
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap' }}>
|
{/* Panel de filtros reorganizado */}
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', flexWrap: 'wrap' }}>
|
||||||
|
<Box>
|
||||||
|
<Typography variant="subtitle1" gutterBottom>Filtros</Typography>
|
||||||
<Box sx={{ display: 'flex', gap: 2, alignItems: 'center', flexWrap: 'wrap' }}>
|
<Box sx={{ display: 'flex', gap: 2, alignItems: 'center', flexWrap: 'wrap' }}>
|
||||||
<TextField
|
<TextField
|
||||||
label="Fecha Desde"
|
label="Fecha Desde"
|
||||||
@@ -170,9 +202,27 @@ const CuentaCorrienteSuscriptorPage: React.FC = () => {
|
|||||||
onChange={handleFechaHastaChange}
|
onChange={handleFechaHastaChange}
|
||||||
InputLabelProps={{ shrink: true }}
|
InputLabelProps={{ shrink: true }}
|
||||||
/>
|
/>
|
||||||
|
<FormControl size="small" sx={{ minWidth: 150 }}>
|
||||||
|
<InputLabel>Estado</InputLabel>
|
||||||
|
<Select value={filtroEstado} label="Estado" onChange={(e) => setFiltroEstado(e.target.value)}>
|
||||||
|
<MenuItem value="Todos">Todos</MenuItem>
|
||||||
|
<MenuItem value="Pendiente">Pendiente</MenuItem>
|
||||||
|
<MenuItem value="Aplicado">Aplicado</MenuItem>
|
||||||
|
<MenuItem value="Anulado">Anulado</MenuItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl size="small" sx={{ minWidth: 150 }}>
|
||||||
|
<InputLabel>Tipo</InputLabel>
|
||||||
|
<Select value={filtroTipo} label="Tipo" onChange={(e) => setFiltroTipo(e.target.value)}>
|
||||||
|
<MenuItem value="Todos">Todos</MenuItem>
|
||||||
|
<MenuItem value="Credito">Crédito</MenuItem>
|
||||||
|
<MenuItem value="Debito">Débito</MenuItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
{puedeGestionar && (
|
{puedeGestionar && (
|
||||||
<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()} sx={{ mt: { xs: 2, sm: 0 } }}>
|
<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()} sx={{ mt: { xs: 2, md: 3.5 } }}>
|
||||||
Nuevo Ajuste
|
Nuevo Ajuste
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
@@ -198,10 +248,10 @@ const CuentaCorrienteSuscriptorPage: React.FC = () => {
|
|||||||
<TableBody>
|
<TableBody>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<TableRow><TableCell colSpan={8} align="center"><CircularProgress size={24} /></TableCell></TableRow>
|
<TableRow><TableCell colSpan={8} align="center"><CircularProgress size={24} /></TableCell></TableRow>
|
||||||
) : ajustes.length === 0 ? (
|
) : ajustesFiltrados.length === 0 ? (
|
||||||
<TableRow><TableCell colSpan={8} align="center">No se encontraron ajustes para los filtros seleccionados.</TableCell></TableRow>
|
<TableRow><TableCell colSpan={8} align="center">No se encontraron ajustes para los filtros seleccionados.</TableCell></TableRow>
|
||||||
) : (
|
) : (
|
||||||
ajustes.map(a => (
|
ajustesFiltrados.map(a => (
|
||||||
<TableRow key={a.idAjuste} sx={{ '& .MuiTableCell-root': { color: a.estado === 'Anulado' ? 'text.disabled' : 'inherit' }, textDecoration: a.estado === 'Anulado' ? 'line-through' : 'none' }}>
|
<TableRow key={a.idAjuste} sx={{ '& .MuiTableCell-root': { color: a.estado === 'Anulado' ? 'text.disabled' : 'inherit' }, textDecoration: a.estado === 'Anulado' ? 'line-through' : 'none' }}>
|
||||||
<TableCell>{formatDisplayDate(a.fechaAjuste)}</TableCell>
|
<TableCell>{formatDisplayDate(a.fechaAjuste)}</TableCell>
|
||||||
<TableCell>{a.nombreEmpresa || 'N/A'}</TableCell>
|
<TableCell>{a.nombreEmpresa || 'N/A'}</TableCell>
|
||||||
@@ -210,7 +260,7 @@ const CuentaCorrienteSuscriptorPage: React.FC = () => {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{a.motivo}</TableCell>
|
<TableCell>{a.motivo}</TableCell>
|
||||||
<TableCell align="right">${a.monto.toFixed(2)}</TableCell>
|
<TableCell align="right">${a.monto.toFixed(2)}</TableCell>
|
||||||
<TableCell>{a.estado}{a.idFacturaAplicado ? ` (Fact. #${a.idFacturaAplicado})` : ''}</TableCell>
|
<TableCell>{renderEstadoCell(a)}</TableCell>
|
||||||
<TableCell>{a.nombreUsuarioAlta}</TableCell>
|
<TableCell>{a.nombreUsuarioAlta}</TableCell>
|
||||||
<TableCell align="right">
|
<TableCell align="right">
|
||||||
{a.estado === 'Pendiente' && puedeGestionar && (
|
{a.estado === 'Pendiente' && puedeGestionar && (
|
||||||
|
|||||||
Reference in New Issue
Block a user