Feat: Implementa auditoría de envíos de email y mejora la UX
Se introduce un sistema completo de logging para todas las comunicaciones por correo electrónico y se realizan mejoras significativas en la experiencia del usuario, tanto en la retroalimentación del sistema como en la estética de los emails enviados al cliente. ### ✨ Nuevas Características - **Auditoría y Log de Envíos de Email:** - Se ha creado una nueva tabla `com_EmailLogs` en la base de datos para registrar cada intento de envío de correo. - El `EmailService` ahora centraliza toda la lógica de logging, registrando automáticamente la fecha, destinatario, asunto, estado (`Enviado` o `Fallido`), y mensajes de error detallados. - Se implementó un nuevo `EmailLogService` y `EmailLogRepository` para gestionar estos registros. - **Historial de Envíos en la Interfaz de Usuario:** - Se añade un nuevo ícono de "Historial" (<span style="color: #607d8b;">📧</span>) junto a cada factura en la página de "Consulta de Facturas". - Al hacer clic, se abre un modal que muestra una tabla detallada con todos los intentos de envío para esa factura, incluyendo el estado y el motivo del error (si lo hubo). - Esto proporciona una trazabilidad completa y una herramienta de diagnóstico para el usuario final. ### 🔄 Refactorización y Mejoras - **Mensajes de Éxito Dinámicos:** - Se ha mejorado la retroalimentación al enviar una factura por PDF. El sistema ahora muestra un mensaje de éxito específico, como "El email... se ha enviado correctamente a suscriptor@email.com", en lugar de un mensaje técnico genérico. - Se ajustó la cadena de llamadas (`Controller` -> `Service`) para que el email del destinatario esté disponible para la respuesta de la API. - **Diseño Unificado de Emails:** - Se ha rediseñado el template HTML para el "Aviso de Cuenta Mensual" para que coincida con la estética del email de "Envío de Factura PDF". - Ambos correos ahora presentan un diseño profesional y consistente, con cabecera, logo y pie de página, reforzando la imagen de marca. - **Manejo de Errores de Email Mejorado:** - El `EmailService` ahora captura excepciones específicas de la librería `MailKit` (ej. `SmtpCommandException`). - Esto permite registrar en el log errores mucho más precisos y útiles, como rechazos de destinatarios por parte del servidor (`User unknown`), fallos de autenticación, etc., que ahora son visibles en el `Tooltip` del historial.
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
import React from 'react';
|
||||
import { Modal, Box, Typography, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, Chip, Tooltip, IconButton, CircularProgress, Alert } from '@mui/material';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import type { EmailLogDto } from '../../../models/dtos/Comunicaciones/EmailLogDto';
|
||||
|
||||
interface HistorialEnviosModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
logs: EmailLogDto[];
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
titulo: string;
|
||||
}
|
||||
|
||||
const modalStyle = {
|
||||
position: 'absolute' as 'absolute',
|
||||
top: '50%', left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: { xs: '95%', sm: '80%', md: '700px' },
|
||||
bgcolor: 'background.paper',
|
||||
boxShadow: 24, p: 4,
|
||||
borderRadius: 2,
|
||||
};
|
||||
|
||||
const HistorialEnviosModal: React.FC<HistorialEnviosModalProps> = ({ open, onClose, logs, isLoading, error, titulo }) => {
|
||||
const formatDisplayDateTime = (dateString: string): string => {
|
||||
if (!dateString) return '';
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleString('es-AR', {
|
||||
day: '2-digit', month: '2-digit', year: 'numeric',
|
||||
hour: '2-digit', minute: '2-digit'
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<Box sx={modalStyle}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
||||
<Typography variant="h6" component="h2">{titulo}</Typography>
|
||||
<IconButton onClick={onClose}><CloseIcon /></IconButton>
|
||||
</Box>
|
||||
{isLoading ? (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', my: 4 }}><CircularProgress /></Box>
|
||||
) : error ? (
|
||||
<Alert severity="error">{error}</Alert>
|
||||
) : (
|
||||
<TableContainer component={Paper}>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell sx={{ fontWeight: 'bold' }}>Fecha de Envío</TableCell>
|
||||
<TableCell sx={{ fontWeight: 'bold' }}>Estado</TableCell>
|
||||
<TableCell sx={{ fontWeight: 'bold' }}>Destinatario</TableCell>
|
||||
<TableCell sx={{ fontWeight: 'bold' }}>Asunto</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{logs.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={4} align="center">No se han registrado envíos.</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
logs.map((log, index) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell>{formatDisplayDateTime(log.fechaEnvio)}</TableCell>
|
||||
<TableCell>
|
||||
<Tooltip title={log.estado === 'Fallido' ? (log.error || 'Error desconocido') : ''} arrow>
|
||||
<Chip
|
||||
label={log.estado}
|
||||
color={log.estado === 'Enviado' ? 'success' : 'error'}
|
||||
size="small"
|
||||
/>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
<TableCell>{log.destinatarioEmail}</TableCell>
|
||||
<TableCell>{log.asunto}</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
)}
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default HistorialEnviosModal;
|
||||
8
Frontend/src/models/dtos/Comunicaciones/EmailLogDto.ts
Normal file
8
Frontend/src/models/dtos/Comunicaciones/EmailLogDto.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export interface EmailLogDto {
|
||||
fechaEnvio: string; // Formato ISO de fecha y hora
|
||||
estado: 'Enviado' | 'Fallido';
|
||||
asunto: string;
|
||||
destinatarioEmail: string;
|
||||
error?: string | null;
|
||||
nombreUsuarioDisparo?: string | null;
|
||||
}
|
||||
@@ -1,17 +1,20 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { Box, Typography, Paper, CircularProgress, Alert, Select, MenuItem, FormControl, InputLabel, Table, TableContainer, TableHead, TableRow, TableCell, TableBody, Chip, IconButton, Menu, ListItemIcon, ListItemText, Collapse, TextField } from '@mui/material';
|
||||
import { Box, Typography, Paper, CircularProgress, Alert, Select, MenuItem, FormControl, InputLabel, Table, TableContainer, TableHead, TableRow, TableCell, TableBody, Chip, IconButton, Menu, ListItemIcon, ListItemText, Collapse, TextField, Tooltip } from '@mui/material';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import PaymentIcon from '@mui/icons-material/Payment';
|
||||
import EmailIcon from '@mui/icons-material/Email';
|
||||
import EditNoteIcon from '@mui/icons-material/EditNote';
|
||||
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
|
||||
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
|
||||
import MailOutlineIcon from '@mui/icons-material/MailOutline';
|
||||
import facturacionService from '../../services/Suscripciones/facturacionService';
|
||||
import { usePermissions } from '../../hooks/usePermissions';
|
||||
import axios from 'axios';
|
||||
import type { ResumenCuentaSuscriptorDto, FacturaConsolidadaDto } from '../../models/dtos/Suscripciones/ResumenCuentaSuscriptorDto';
|
||||
import PagoManualModal from '../../components/Modals/Suscripciones/PagoManualModal';
|
||||
import type { CreatePagoDto } from '../../models/dtos/Suscripciones/CreatePagoDto';
|
||||
import type { EmailLogDto } from '../../models/dtos/Comunicaciones/EmailLogDto';
|
||||
import PagoManualModal from '../../components/Modals/Suscripciones/PagoManualModal';
|
||||
import HistorialEnviosModal from '../../components/Modals/Suscripciones/HistorialEnviosModal';
|
||||
|
||||
const anios = Array.from({ length: 10 }, (_, i) => new Date().getFullYear() - i);
|
||||
const meses = [
|
||||
@@ -27,7 +30,8 @@ const estadosFacturacion = ['Pendiente de Facturar', 'Facturado'];
|
||||
const SuscriptorRow: React.FC<{
|
||||
resumen: ResumenCuentaSuscriptorDto;
|
||||
handleMenuOpen: (event: React.MouseEvent<HTMLElement>, factura: FacturaConsolidadaDto) => void;
|
||||
}> = ({ resumen, handleMenuOpen }) => {
|
||||
handleOpenHistorial: (factura: FacturaConsolidadaDto) => void;
|
||||
}> = ({ resumen, handleMenuOpen, handleOpenHistorial }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
return (
|
||||
<React.Fragment>
|
||||
@@ -38,7 +42,6 @@ const SuscriptorRow: React.FC<{
|
||||
<Typography variant="body2" sx={{ fontWeight: 'bold', color: resumen.saldoPendienteTotal > 0 ? 'error.main' : 'success.main' }}>${resumen.saldoPendienteTotal.toFixed(2)}</Typography>
|
||||
<Typography variant="caption" color="text.secondary">de ${resumen.importeTotal.toFixed(2)}</Typography>
|
||||
</TableCell>
|
||||
{/* La cabecera principal ya no tiene acciones */}
|
||||
<TableCell colSpan={5}></TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
@@ -63,10 +66,14 @@ const SuscriptorRow: React.FC<{
|
||||
<TableCell><Chip label={factura.estadoFacturacion} size="small" color={factura.estadoFacturacion === 'Facturado' ? 'info' : 'warning'} /></TableCell>
|
||||
<TableCell>{factura.numeroFactura || '-'}</TableCell>
|
||||
<TableCell align="right">
|
||||
{/* El menú de acciones vuelve a estar aquí, por factura */}
|
||||
<IconButton onClick={(e) => handleMenuOpen(e, factura)} disabled={factura.estadoPago === 'Anulada'}>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
<Tooltip title="Ver Historial de Envíos">
|
||||
<IconButton onClick={() => handleOpenHistorial(factura)}>
|
||||
<MailOutlineIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
@@ -92,20 +99,25 @@ const ConsultaFacturasPage: React.FC = () => {
|
||||
const puedeGestionarFactura = isSuperAdmin || tienePermiso("SU006");
|
||||
const puedeRegistrarPago = isSuperAdmin || tienePermiso("SU008");
|
||||
const puedeEnviarEmail = isSuperAdmin || tienePermiso("SU009");
|
||||
const [pagoModalOpen, setPagoModalOpen] = useState(false);
|
||||
const [selectedFactura, setSelectedFactura] = useState<FacturaConsolidadaDto | null>(null);
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const [filtroNombre, setFiltroNombre] = useState('');
|
||||
const [filtroEstadoPago, setFiltroEstadoPago] = useState('');
|
||||
const [filtroEstadoFacturacion, setFiltroEstadoFacturacion] = useState('');
|
||||
|
||||
const [selectedFactura, setSelectedFactura] = useState<FacturaConsolidadaDto | null>(null);
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const [pagoModalOpen, setPagoModalOpen] = useState(false);
|
||||
const [historialModalOpen, setHistorialModalOpen] = useState(false);
|
||||
const [logs, setLogs] = useState<EmailLogDto[]>([]);
|
||||
const [loadingLogs, setLoadingLogs] = useState(false);
|
||||
const [logError, setLogError] = useState<string | null>(null);
|
||||
|
||||
const cargarResumenesDelPeriodo = useCallback(async () => {
|
||||
if (!puedeConsultar) return;
|
||||
setLoading(true);
|
||||
setApiError(null);
|
||||
try {
|
||||
const data = await facturacionService.getResumenesDeCuentaPorPeriodo(
|
||||
selectedAnio,
|
||||
selectedAnio,
|
||||
selectedMes,
|
||||
filtroNombre || undefined,
|
||||
filtroEstadoPago || undefined,
|
||||
@@ -118,13 +130,12 @@ const ConsultaFacturasPage: React.FC = () => {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [selectedAnio, selectedMes, puedeConsultar, filtroNombre, filtroEstadoPago, filtroEstadoFacturacion]);
|
||||
}, [selectedAnio, selectedMes, puedeConsultar, filtroNombre, filtroEstadoPago, filtroEstadoFacturacion]);
|
||||
|
||||
useEffect(() => {
|
||||
// Ejecutar la búsqueda cuando los filtros cambian
|
||||
const timer = setTimeout(() => {
|
||||
cargarResumenesDelPeriodo();
|
||||
}, 500); // Debounce para no buscar en cada tecla
|
||||
}, 500);
|
||||
return () => clearTimeout(timer);
|
||||
}, [cargarResumenesDelPeriodo]);
|
||||
|
||||
@@ -137,11 +148,26 @@ const ConsultaFacturasPage: React.FC = () => {
|
||||
const handleOpenPagoModal = () => { setPagoModalOpen(true); handleMenuClose(); };
|
||||
const handleClosePagoModal = () => { setPagoModalOpen(false); setSelectedFactura(null); };
|
||||
|
||||
const handleOpenHistorial = async (factura: FacturaConsolidadaDto) => {
|
||||
setSelectedFactura(factura);
|
||||
setHistorialModalOpen(true);
|
||||
setLoadingLogs(true);
|
||||
setLogError(null);
|
||||
try {
|
||||
const data = await facturacionService.getHistorialEnvios(factura.idFactura);
|
||||
setLogs(data);
|
||||
} catch (err) {
|
||||
setLogError("Error al cargar el historial de envíos.");
|
||||
} finally {
|
||||
setLoadingLogs(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmitPagoModal = async (data: CreatePagoDto) => {
|
||||
setApiError(null);
|
||||
try {
|
||||
await facturacionService.registrarPagoManual(data);
|
||||
setApiMessage(`Pago para la factura #${data.idFactura} registrado exitosamente.`);
|
||||
setApiMessage(`Pago para la factura registrado exitosamente.`);
|
||||
cargarResumenesDelPeriodo();
|
||||
} catch (err: any) {
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al registrar el pago.';
|
||||
@@ -157,7 +183,7 @@ const ConsultaFacturasPage: React.FC = () => {
|
||||
setApiError(null);
|
||||
try {
|
||||
await facturacionService.actualizarNumeroFactura(factura.idFactura, nuevoNumero.trim());
|
||||
setApiMessage(`Número de factura #${factura.idFactura} actualizado.`);
|
||||
setApiMessage(`Número de factura actualizado.`);
|
||||
cargarResumenesDelPeriodo();
|
||||
} catch (err: any) {
|
||||
setApiError(err.response?.data?.message || 'Error al actualizar el número de factura.');
|
||||
@@ -166,12 +192,12 @@ const ConsultaFacturasPage: React.FC = () => {
|
||||
};
|
||||
|
||||
const handleSendEmail = async (idFactura: number) => {
|
||||
if (!window.confirm(`¿Está seguro de enviar la factura #${idFactura} por email? Se adjuntará el PDF si se encuentra.`)) return;
|
||||
if (!window.confirm(`¿Está seguro de enviar la factura por email? Se adjuntará el PDF si se encuentra.`)) return;
|
||||
setApiMessage(null);
|
||||
setApiError(null);
|
||||
try {
|
||||
await facturacionService.enviarFacturaPdfPorEmail(idFactura);
|
||||
setApiMessage(`El email para la factura #${idFactura} ha sido enviado a la cola de procesamiento.`);
|
||||
const respuesta = await facturacionService.enviarFacturaPdfPorEmail(idFactura);
|
||||
setApiMessage(respuesta.message);
|
||||
} catch (err: any) {
|
||||
setApiError(err.response?.data?.message || 'Error al intentar enviar el email.');
|
||||
} finally {
|
||||
@@ -186,30 +212,12 @@ const ConsultaFacturasPage: React.FC = () => {
|
||||
<Typography variant="h5" gutterBottom>Consulta de Facturas de Suscripciones</Typography>
|
||||
<Paper sx={{ p: 2, mb: 2 }}>
|
||||
<Typography variant="h6">Filtros</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 2, my: 2, alignItems: 'center' }}>
|
||||
<Box sx={{ display: 'flex', gap: 2, my: 2, alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
<FormControl sx={{ minWidth: 150 }} size="small"><InputLabel>Mes</InputLabel><Select value={selectedMes} label="Mes" onChange={(e) => setSelectedMes(e.target.value as number)}>{meses.map(m => <MenuItem key={m.value} value={m.value}>{m.label}</MenuItem>)}</Select></FormControl>
|
||||
<FormControl sx={{ minWidth: 120 }} size="small"><InputLabel>Año</InputLabel><Select value={selectedAnio} label="Año" onChange={(e) => setSelectedAnio(e.target.value as number)}>{anios.map(a => <MenuItem key={a} value={a}>{a}</MenuItem>)}</Select></FormControl>
|
||||
<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 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>
|
||||
<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 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>
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
@@ -218,34 +226,26 @@ const ConsultaFacturasPage: React.FC = () => {
|
||||
|
||||
<TableContainer component={Paper}>
|
||||
<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={5}></TableCell></TableRow></TableHead>
|
||||
<TableBody>
|
||||
{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.map(resumen => (<SuscriptorRow key={resumen.idSuscriptor} resumen={resumen} handleMenuOpen={handleMenuOpen} />)))}
|
||||
: (resumenes.map(resumen => (
|
||||
<SuscriptorRow
|
||||
key={resumen.idSuscriptor}
|
||||
resumen={resumen}
|
||||
handleMenuOpen={handleMenuOpen}
|
||||
handleOpenHistorial={handleOpenHistorial}
|
||||
/>
|
||||
)))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
{/* El menú de acciones ahora opera sobre la 'selectedFactura' */}
|
||||
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
|
||||
{selectedFactura && puedeRegistrarPago && (<MenuItem onClick={handleOpenPagoModal} disabled={selectedFactura.estadoPago === 'Pagada' || selectedFactura.estadoPago === 'Anulada'}><ListItemIcon><PaymentIcon fontSize="small" /></ListItemIcon><ListItemText>Registrar Pago Manual</ListItemText></MenuItem>)}
|
||||
{selectedFactura && puedeGestionarFactura && (<MenuItem onClick={() => handleUpdateNumeroFactura(selectedFactura)} disabled={selectedFactura.estadoPago === 'Anulada'}><ListItemIcon><EditNoteIcon fontSize="small" /></ListItemIcon><ListItemText>Cargar/Modificar Nro. Factura</ListItemText></MenuItem>)}
|
||||
{selectedFactura && puedeEnviarEmail && (
|
||||
<MenuItem
|
||||
onClick={() => handleSendEmail(selectedFactura.idFactura)}
|
||||
disabled={!selectedFactura.numeroFactura || selectedFactura.estadoPago === 'Anulada'}>
|
||||
<ListItemIcon><EmailIcon fontSize="small" /></ListItemIcon>
|
||||
<ListItemText>Enviar Factura (PDF)</ListItemText>
|
||||
</MenuItem>
|
||||
)}
|
||||
{selectedFactura && puedeEnviarEmail && (<MenuItem onClick={() => handleSendEmail(selectedFactura.idFactura)} disabled={!selectedFactura.numeroFactura || selectedFactura.estadoPago === 'Anulada'}><ListItemIcon><EmailIcon fontSize="small" /></ListItemIcon><ListItemText>Enviar Factura (PDF)</ListItemText></MenuItem>)}
|
||||
</Menu>
|
||||
|
||||
<PagoManualModal
|
||||
@@ -255,12 +255,10 @@ const ConsultaFacturasPage: React.FC = () => {
|
||||
factura={
|
||||
selectedFactura ? {
|
||||
idFactura: selectedFactura.idFactura,
|
||||
nombreSuscriptor: 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,
|
||||
// Calculamos el saldo pendiente aquí
|
||||
saldoPendiente: selectedFactura.estadoPago === 'Pagada' ? 0 : selectedFactura.importeFinal, // Simplificación
|
||||
// Rellenamos los campos restantes que el modal podría necesitar, aunque no los use.
|
||||
idSuscriptor: selectedFactura.idSuscriptor, // Corregido para coincidir con FacturaDto
|
||||
saldoPendiente: selectedFactura.estadoPago === 'Pagada' ? 0 : selectedFactura.importeFinal,
|
||||
idSuscriptor: resumenes.find(res => res.facturas.some(f => f.idFactura === selectedFactura.idFactura))?.idSuscriptor || 0,
|
||||
periodo: '',
|
||||
fechaEmision: '',
|
||||
fechaVencimiento: '',
|
||||
@@ -272,7 +270,17 @@ const ConsultaFacturasPage: React.FC = () => {
|
||||
} : null
|
||||
}
|
||||
errorMessage={apiError}
|
||||
clearErrorMessage={() => setApiError(null)} />
|
||||
clearErrorMessage={() => setApiError(null)}
|
||||
/>
|
||||
|
||||
<HistorialEnviosModal
|
||||
open={historialModalOpen}
|
||||
onClose={() => setHistorialModalOpen(false)}
|
||||
logs={logs}
|
||||
isLoading={loadingLogs}
|
||||
error={logError}
|
||||
titulo={`Historial de Envíos para Factura ${selectedFactura?.numeroFactura || `#${selectedFactura?.idFactura}`}`}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { PagoDto } from '../../models/dtos/Suscripciones/PagoDto';
|
||||
import type { CreatePagoDto } from '../../models/dtos/Suscripciones/CreatePagoDto';
|
||||
import type { ProcesamientoLoteResponseDto } from '../../models/dtos/Suscripciones/ProcesamientoLoteResponseDto';
|
||||
import type { ResumenCuentaSuscriptorDto } from '../../models/dtos/Suscripciones/ResumenCuentaSuscriptorDto';
|
||||
import type { EmailLogDto } from '../../models/dtos/Comunicaciones/EmailLogDto';
|
||||
|
||||
const API_URL = '/facturacion';
|
||||
const DEBITOS_URL = '/debitos';
|
||||
@@ -66,8 +67,14 @@ const enviarAvisoCuentaPorEmail = async (anio: number, mes: number, idSuscriptor
|
||||
await apiClient.post(`${API_URL}/${anio}/${mes}/suscriptor/${idSuscriptor}/enviar-aviso`);
|
||||
};
|
||||
|
||||
const enviarFacturaPdfPorEmail = async (idFactura: number): Promise<void> => {
|
||||
await apiClient.post(`${API_URL}/${idFactura}/enviar-factura-pdf`);
|
||||
const enviarFacturaPdfPorEmail = async (idFactura: number): Promise<{ message: string }> => {
|
||||
const response = await apiClient.post<{ message: string }>(`${API_URL}/${idFactura}/enviar-factura-pdf`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const getHistorialEnvios = async (idFactura: number): Promise<EmailLogDto[]> => {
|
||||
const response = await apiClient.get<EmailLogDto[]>(`${API_URL}/${idFactura}/historial-envios`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export default {
|
||||
@@ -79,4 +86,5 @@ export default {
|
||||
actualizarNumeroFactura,
|
||||
enviarAvisoCuentaPorEmail,
|
||||
enviarFacturaPdfPorEmail,
|
||||
getHistorialEnvios,
|
||||
};
|
||||
Reference in New Issue
Block a user