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:
@@ -9,6 +9,7 @@ export interface AjusteDto {
|
||||
motivo: string;
|
||||
estado: 'Pendiente' | 'Aplicado' | 'Anulado';
|
||||
idFacturaAplicado?: number | null;
|
||||
numeroFacturaAplicado?: string | null;
|
||||
fechaAlta: string; // "yyyy-MM-dd HH:mm"
|
||||
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 { 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 ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
@@ -44,26 +47,32 @@ const CuentaCorrienteSuscriptorPage: React.FC = () => {
|
||||
const [filtroFechaDesde, setFiltroFechaDesde] = useState<string>(getInitialDateRange().fechaDesde);
|
||||
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 puedeGestionar = isSuperAdmin || tienePermiso("SU011");
|
||||
|
||||
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)) {
|
||||
setError("ID de Suscriptor inválido."); setLoading(false); return;
|
||||
}
|
||||
setLoading(true); setApiErrorMessage(null); setError(null);
|
||||
try {
|
||||
// Usamos Promise.all para cargar todo en paralelo y mejorar el rendimiento
|
||||
const [suscriptorData, ajustesData, empresasData] = await Promise.all([
|
||||
suscriptorService.getSuscriptorById(idSuscriptor),
|
||||
ajusteService.getAjustesPorSuscriptor(idSuscriptor, filtroFechaDesde || undefined, filtroFechaHasta || undefined),
|
||||
empresaService.getEmpresasDropdown()
|
||||
]);
|
||||
|
||||
|
||||
setSuscriptor(suscriptorData);
|
||||
setAjustes(ajustesData);
|
||||
setEmpresas(empresasData);
|
||||
|
||||
} catch (err) {
|
||||
setError("Error al cargar los datos.");
|
||||
} finally {
|
||||
@@ -73,6 +82,26 @@ const CuentaCorrienteSuscriptorPage: React.FC = () => {
|
||||
|
||||
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 ---
|
||||
const handleFechaDesdeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const nuevaFechaDesde = e.target.value;
|
||||
@@ -152,27 +181,48 @@ const CuentaCorrienteSuscriptorPage: React.FC = () => {
|
||||
<Typography variant="h4" color="primary" gutterBottom>{suscriptor?.nombreCompleto || ''}</Typography>
|
||||
|
||||
<Paper sx={{ p: 2, mb: 2 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
<Box sx={{ display: 'flex', gap: 2, alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
<TextField
|
||||
label="Fecha Desde"
|
||||
type="date"
|
||||
size="small"
|
||||
value={filtroFechaDesde}
|
||||
onChange={handleFechaDesdeChange}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
<TextField
|
||||
label="Fecha Hasta"
|
||||
type="date"
|
||||
size="small"
|
||||
value={filtroFechaHasta}
|
||||
onChange={handleFechaHastaChange}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
{/* 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' }}>
|
||||
<TextField
|
||||
label="Fecha Desde"
|
||||
type="date"
|
||||
size="small"
|
||||
value={filtroFechaDesde}
|
||||
onChange={handleFechaDesdeChange}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
<TextField
|
||||
label="Fecha Hasta"
|
||||
type="date"
|
||||
size="small"
|
||||
value={filtroFechaHasta}
|
||||
onChange={handleFechaHastaChange}
|
||||
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>
|
||||
{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
|
||||
</Button>
|
||||
)}
|
||||
@@ -198,10 +248,10 @@ const CuentaCorrienteSuscriptorPage: React.FC = () => {
|
||||
<TableBody>
|
||||
{loading ? (
|
||||
<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>
|
||||
) : (
|
||||
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' }}>
|
||||
<TableCell>{formatDisplayDate(a.fechaAjuste)}</TableCell>
|
||||
<TableCell>{a.nombreEmpresa || 'N/A'}</TableCell>
|
||||
@@ -210,7 +260,7 @@ const CuentaCorrienteSuscriptorPage: React.FC = () => {
|
||||
</TableCell>
|
||||
<TableCell>{a.motivo}</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 align="right">
|
||||
{a.estado === 'Pendiente' && puedeGestionar && (
|
||||
|
||||
Reference in New Issue
Block a user