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