Feat(suscripciones): Implementa manejo de pagos parciales en facturas
All checks were successful
Optimized Build and Deploy / remote-build-and-deploy (push) Successful in 8m3s
All checks were successful
Optimized Build and Deploy / remote-build-and-deploy (push) Successful in 8m3s
Se introduce una refactorización completa del sistema de registro de pagos para manejar correctamente los abonos parciales, asegurando que el estado de la factura y el saldo pendiente se reflejen con precisión tanto en el backend como en la interfaz de usuario. ### 🐛 Problema Solucionado - Anteriormente, el sistema no reconocía los pagos parciales. Una factura permanecía en estado "Pendiente" hasta que el monto total era cubierto, y la interfaz de usuario siempre mostraba el 100% del saldo como pendiente, lo cual era incorrecto y confuso. ### ✨ Nuevas Características y Mejoras - **Nuevo Estado de Factura "Pagada Parcialmente":** - Se introduce un nuevo estado para las facturas que han recibido uno o más pagos pero cuyo saldo aún no es cero. - El `PagoService` ahora actualiza el estado de la factura a "Pagada Parcialmente" cuando recibe un abono que no cubre el total. - **Mejoras en la Interfaz de Usuario (`ConsultaFacturasPage`):** - **Nuevas Columnas:** Se han añadido las columnas "Pagado" y "Saldo" a la tabla de detalle de facturas, mostrando explícitamente el monto abonado y el restante. - **Visualización de Estado:** El `Chip` de estado ahora muestra "Pagada Parcialmente" con un color distintivo (azul/primary) para una rápida identificación visual. - **Cálculo de Saldo Correcto:** El saldo pendiente total por suscriptor y el saldo para el modal de pago manual ahora se calculan correctamente, restando el `totalPagado` del `importeFinal`. ### 🔄 Cambios en el Backend - **`PagoService`:** Se actualizó la lógica para establecer el estado de la factura (`Pendiente`, `Pagada Parcialmente`, `Pagada`) basado en el `nuevoTotalPagado` después de registrar un pago. - **`FacturacionService`:** El método `ObtenerResumenesDeCuentaPorPeriodo` ahora calcula correctamente el `SaldoPendienteTotal` y pasa la propiedad `TotalPagado` al DTO del frontend. - **DTOs:** Se actualizó `FacturaConsolidadaDto` para incluir la propiedad `TotalPagado`.
This commit is contained in:
@@ -12,6 +12,7 @@ export interface FacturaConsolidadaDto {
|
||||
estadoPago: string;
|
||||
estadoFacturacion: string;
|
||||
numeroFactura?: string | null;
|
||||
totalPagado: number;
|
||||
detalles: FacturaDetalleDto[];
|
||||
// Añadimos el id del suscriptor para que sea fácil pasarlo a los handlers
|
||||
idSuscriptor: number;
|
||||
|
||||
@@ -24,7 +24,7 @@ const meses = [
|
||||
{ value: 10, label: 'Octubre' }, { value: 11, label: 'Noviembre' }, { value: 12, label: 'Diciembre' }
|
||||
];
|
||||
|
||||
const estadosPago = ['Pendiente', 'Pagada', 'Rechazada', 'Anulada'];
|
||||
const estadosPago = ['Pendiente', 'Pagada', 'Pagada Parcialmente', 'Rechazada', 'Anulada'];
|
||||
const estadosFacturacion = ['Pendiente de Facturar', 'Facturado'];
|
||||
|
||||
const SuscriptorRow: React.FC<{
|
||||
@@ -33,50 +33,83 @@ const SuscriptorRow: React.FC<{
|
||||
handleOpenHistorial: (factura: FacturaConsolidadaDto) => void;
|
||||
}> = ({ resumen, handleMenuOpen, handleOpenHistorial }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
// Función para formatear moneda
|
||||
const formatCurrency = (value: number) => `$${value.toFixed(2)}`;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<TableRow sx={{ '& > *': { borderBottom: 'unset' } }} hover>
|
||||
<TableCell><IconButton size="small" onClick={() => setOpen(!open)}>{open ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}</IconButton></TableCell>
|
||||
<TableCell component="th" scope="row">{resumen.nombreSuscriptor}</TableCell>
|
||||
<TableCell align="right">
|
||||
<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>
|
||||
<Typography variant="body2" sx={{ fontWeight: 'bold', color: resumen.saldoPendienteTotal > 0 ? 'error.main' : 'success.main' }}>
|
||||
{formatCurrency(resumen.saldoPendienteTotal)}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">de {formatCurrency(resumen.importeTotal)}</Typography>
|
||||
</TableCell>
|
||||
<TableCell colSpan={5}></TableCell>
|
||||
<TableCell colSpan={7}></TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell style={{ paddingBottom: 0, paddingTop: 0 }} colSpan={8}>
|
||||
<TableCell style={{ paddingBottom: 0, paddingTop: 0 }} colSpan={10}>
|
||||
<Collapse in={open} timeout="auto" unmountOnExit>
|
||||
<Box sx={{ margin: 1, padding: 2, backgroundColor: 'grey.50', borderRadius: 1 }}>
|
||||
<Typography variant="h6" gutterBottom component="div" sx={{ fontSize: '1rem', fontWeight: 'bold' }}>Facturas del Período para {resumen.nombreSuscriptor}</Typography>
|
||||
<Typography variant="h6" gutterBottom component="div" sx={{ fontSize: '1rem', fontWeight: 'bold' }}>
|
||||
Facturas del Período para {resumen.nombreSuscriptor}
|
||||
</Typography>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Empresa</TableCell><TableCell align="right">Importe</TableCell>
|
||||
<TableCell>Estado Pago</TableCell><TableCell>Estado Facturación</TableCell>
|
||||
<TableCell>Nro. Factura</TableCell><TableCell align="right">Acciones</TableCell>
|
||||
<TableCell>Empresa</TableCell>
|
||||
<TableCell align="right">Importe Total</TableCell>
|
||||
<TableCell align="right">Pagado</TableCell>
|
||||
<TableCell align="right">Saldo</TableCell>
|
||||
<TableCell>Estado Pago</TableCell>
|
||||
<TableCell>Estado Facturación</TableCell>
|
||||
<TableCell>Nro. Factura</TableCell>
|
||||
<TableCell align="right">Acciones</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{resumen.facturas.map((factura) => (
|
||||
<TableRow key={factura.idFactura}>
|
||||
<TableCell sx={{ fontWeight: 'medium' }}>{factura.nombreEmpresa}</TableCell>
|
||||
<TableCell align="right">${factura.importeFinal.toFixed(2)}</TableCell>
|
||||
<TableCell><Chip label={factura.estadoPago} size="small" color={factura.estadoPago === 'Pagada' ? 'success' : (factura.estadoPago === 'Rechazada' ? 'error' : 'default')} /></TableCell>
|
||||
<TableCell><Chip label={factura.estadoFacturacion} size="small" color={factura.estadoFacturacion === 'Facturado' ? 'info' : 'warning'} /></TableCell>
|
||||
<TableCell>{factura.numeroFactura || '-'}</TableCell>
|
||||
<TableCell align="right">
|
||||
<IconButton onClick={(e) => handleMenuOpen(e, factura)} disabled={factura.estadoPago === 'Anulada'}>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
<Tooltip title="Ver Historial de Envíos">
|
||||
<IconButton onClick={() => handleOpenHistorial(factura)}>
|
||||
<MailOutlineIcon />
|
||||
{resumen.facturas.map((factura) => {
|
||||
const saldo = factura.importeFinal - factura.totalPagado;
|
||||
return (
|
||||
<TableRow key={factura.idFactura}>
|
||||
<TableCell sx={{ fontWeight: 'medium' }}>{factura.nombreEmpresa}</TableCell>
|
||||
<TableCell align="right">{formatCurrency(factura.importeFinal)}</TableCell>
|
||||
<TableCell align="right" sx={{ color: 'success.dark' }}>
|
||||
{formatCurrency(factura.totalPagado)}
|
||||
</TableCell>
|
||||
<TableCell align="right" sx={{ fontWeight: 'bold', color: saldo > 0 ? 'error.main' : 'inherit' }}>
|
||||
{formatCurrency(saldo)}
|
||||
</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>{factura.numeroFactura || '-'}</TableCell>
|
||||
<TableCell align="right">
|
||||
<IconButton onClick={(e) => handleMenuOpen(e, factura)} disabled={factura.estadoPago === 'Anulada'}>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
<Tooltip title="Ver Historial de Envíos">
|
||||
<IconButton onClick={() => handleOpenHistorial(factura)}>
|
||||
<MailOutlineIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Box>
|
||||
@@ -162,7 +195,7 @@ const ConsultaFacturasPage: React.FC = () => {
|
||||
setLoadingLogs(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const handleSubmitPagoModal = async (data: CreatePagoDto) => {
|
||||
setApiError(null);
|
||||
try {
|
||||
@@ -231,10 +264,10 @@ const ConsultaFacturasPage: React.FC = () => {
|
||||
{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}
|
||||
<SuscriptorRow
|
||||
key={resumen.idSuscriptor}
|
||||
resumen={resumen}
|
||||
handleMenuOpen={handleMenuOpen}
|
||||
handleOpenHistorial={handleOpenHistorial}
|
||||
/>
|
||||
)))}
|
||||
@@ -257,12 +290,12 @@ const ConsultaFacturasPage: React.FC = () => {
|
||||
idFactura: selectedFactura.idFactura,
|
||||
nombreSuscriptor: resumenes.find(r => r.idSuscriptor === resumenes.find(res => res.facturas.some(f => f.idFactura === selectedFactura.idFactura))?.idSuscriptor)?.nombreSuscriptor || '',
|
||||
importeFinal: selectedFactura.importeFinal,
|
||||
saldoPendiente: selectedFactura.estadoPago === 'Pagada' ? 0 : 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: '',
|
||||
totalPagado: selectedFactura.importeFinal - (selectedFactura.estadoPago === 'Pagada' ? 0 : selectedFactura.importeFinal),
|
||||
estadoPago: selectedFactura.estadoPago,
|
||||
estadoFacturacion: selectedFactura.estadoFacturacion,
|
||||
numeroFactura: selectedFactura.numeroFactura,
|
||||
@@ -272,7 +305,7 @@ const ConsultaFacturasPage: React.FC = () => {
|
||||
errorMessage={apiError}
|
||||
clearErrorMessage={() => setApiError(null)}
|
||||
/>
|
||||
|
||||
|
||||
<HistorialEnviosModal
|
||||
open={historialModalOpen}
|
||||
onClose={() => setHistorialModalOpen(false)}
|
||||
|
||||
Reference in New Issue
Block a user