Compare commits

...

5 Commits

Author SHA1 Message Date
86027ea20a Fix Facturas de Fechas Futuras
- Se cambia la consulta a la vista para capturar la totalidad de las facturas de 10 días atras en adelante de forma indefinida.
2025-12-30 11:50:40 -03:00
62b52b4f2c Fix: index title 2025-12-15 12:36:21 -03:00
4363f87336 Merge branch 'main' of https://repo.eldiaservicios.com/dmolinari/GestorWebFacturas 2025-12-15 12:20:17 -03:00
d03c6d4be4 Fix ExecutionCard Refresh 2025-12-15 12:19:57 -03:00
6ab3c22362 Fix: Conteo y Clasificación de Eventos y Docker Compose Puerto 2025-12-15 11:40:37 -03:00
4 changed files with 73 additions and 47 deletions

View File

@@ -37,6 +37,12 @@ public class ProcesadorFacturasService : IProcesadorFacturasService
public async Task EjecutarProcesoAsync(DateTime fechaDesde)
{
// Contadores inicializados AL PRINCIPIO
int copiadas = 0;
int omitidas = 0;
int errores = 0;
// No declarar 'int procesadas = 0' porque es redundante con copiadas/omitidas
var config = await _context.Configuraciones.FirstOrDefaultAsync(c => c.Id == 1);
if (config == null)
{
@@ -44,7 +50,6 @@ public class ProcesadorFacturasService : IProcesadorFacturasService
return;
}
// Actualizamos la fecha de ejecución
config.UltimaEjecucion = DateTime.Now;
await _context.SaveChangesAsync();
@@ -64,20 +69,27 @@ public class ProcesadorFacturasService : IProcesadorFacturasService
await RegistrarEventoAsync($"Se encontraron {facturas.Count} facturas para procesar", TipoEvento.Info);
int procesadas = 0;
int errores = 0;
List<FacturaParaProcesar> pendientes = new();
List<string> detallesErroresParaMail = new();
// Lista para rastrear las entidades de Evento y actualizar su flag 'Enviado' luego
List<Evento> eventosDeErrorParaActualizar = new();
// --- 1. Primer intento ---
foreach (var factura in facturas)
{
bool exito = await ProcesarFacturaAsync(factura, config);
if (exito) procesadas++;
else pendientes.Add(factura);
var resultado = await ProcesarFacturaAsync(factura, config);
switch (resultado)
{
case ResultadoProceso.Copiado:
copiadas++;
break;
case ResultadoProceso.Omitido:
omitidas++;
break;
case ResultadoProceso.Error:
pendientes.Add(factura);
break;
}
}
// --- 2. Sistema de Reintentos ---
@@ -93,11 +105,14 @@ public class ProcesadorFacturasService : IProcesadorFacturasService
foreach (var factura in pendientes)
{
bool exito = await ProcesarFacturaAsync(factura, config);
if (exito)
var resultado = await ProcesarFacturaAsync(factura, config);
if (resultado != ResultadoProceso.Error)
{
procesadas++;
_logger.LogInformation("Archivo encontrado en reintento {intento}: {archivo}", intento, factura.NombreArchivoOrigen);
if (resultado == ResultadoProceso.Copiado) copiadas++;
if (resultado == ResultadoProceso.Omitido) omitidas++;
_logger.LogInformation("Archivo recuperado en reintento {intento}: {archivo}", intento, factura.NombreArchivoOrigen);
}
else
{
@@ -110,7 +125,7 @@ public class ProcesadorFacturasService : IProcesadorFacturasService
// --- 3. Registro de Errores Finales ---
if (pendientes.Count > 0)
{
errores = pendientes.Count;
errores = pendientes.Count; // Aquí usamos la variable ya declarada arriba
foreach (var factura in pendientes)
{
string msgError = $"El archivo NO EXISTE después de {MAX_REINTENTOS} intentos: {factura.NombreArchivoOrigen}";
@@ -124,8 +139,6 @@ public class ProcesadorFacturasService : IProcesadorFacturasService
};
_context.Eventos.Add(eventoError);
// Guardamos referencias
eventosDeErrorParaActualizar.Add(eventoError);
detallesErroresParaMail.Add(factura.NombreArchivoOrigen);
}
@@ -151,40 +164,31 @@ public class ProcesadorFacturasService : IProcesadorFacturasService
catch { }
// --- 6. Evento Final ---
var mensajeFinal = $"Proceso finalizado. Procesadas: {procesadas}, Errores: {errores}";
var mensajeFinal = $"Proceso finalizado. Nuevas: {copiadas}, Verificadas: {omitidas}, Errores: {errores}";
await RegistrarEventoAsync(mensajeFinal, errores > 0 ? TipoEvento.Warning : TipoEvento.Info);
// --- 7. Envío de Mail Inteligente (Solo 1 vez por archivo) ---
// --- 7. Envío de Mail Inteligente ---
if (errores > 0 && config.AvisoMail && !string.IsNullOrEmpty(config.SMTPDestinatario))
{
// 1. Buscamos en la historia de la DB si estos archivos ya fueron reportados previamente.
// Buscamos en TODOS los logs disponibles (que suelen ser los últimos 30 días según la limpieza).
// Filtramos por Tipo Error y Enviado=true.
var historialErroresEnviados = await _context.Eventos
.Where(e => e.Tipo == TipoEvento.Error.ToString() && e.Enviado == true)
.Select(e => e.Mensaje)
.ToListAsync();
// 2. Filtramos la lista actual:
// Solo queremos los archivos que NO aparezcan en ningún mensaje del historial.
var archivosNuevosParaNotificar = detallesErroresParaMail.Where(archivoFallido =>
{
// El mensaje en BD es: "El archivo NO EXISTE...: nombre_archivo.pdf"
// Chequeamos si el nombre del archivo está contenido en algún mensaje viejo.
bool yaFueNotificado = historialErroresEnviados.Any(msgHistorico => msgHistorico.Contains(archivoFallido));
return !yaFueNotificado;
}).ToList();
// 3. Decidir si enviar mail
bool mailEnviado = false;
if (archivosNuevosParaNotificar.Count > 0)
{
// Si hay archivos NUEVOS, enviamos mail SOLO con esos.
// Nota: Pasamos 'errores' (total técnico) y 'archivosNuevosParaNotificar' (detalle visual)
// Pasamos 'copiadas' en lugar de 'procesadas' para el mail, o la suma de ambas si prefieres
mailEnviado = await EnviarNotificacionErroresAsync(
config.SMTPDestinatario,
procesadas,
copiadas + omitidas,
errores,
archivosNuevosParaNotificar
);
@@ -196,14 +200,10 @@ public class ProcesadorFacturasService : IProcesadorFacturasService
}
else
{
_logger.LogInformation("Se omitió el envío de correo: Los {count} errores ya fueron notificados anteriormente.", errores);
// Simulamos que se "envió" (se gestionó) para marcar los flags en BD
_logger.LogInformation("Se omitió el envío de correo por errores repetidos.");
mailEnviado = true;
}
// 4. Actualizar flag en BD (CRÍTICO)
// Si gestionamos la notificación correctamente (ya sea enviándola o detectando que ya estaba enviada),
// marcamos los eventos actuales como Enviado=true para que pasen al historial y no se vuelvan a procesar.
if (mailEnviado && eventosDeErrorParaActualizar.Count > 0)
{
foreach (var evento in eventosDeErrorParaActualizar)
@@ -259,7 +259,6 @@ public class ProcesadorFacturasService : IProcesadorFacturasService
AND NRO_CAI IS NOT NULL
AND NRO_CAI != ''
AND FECHACOMPLETA_FACTURA >= @FechaDesde
AND CONVERT(DATE, FECHACOMPLETA_FACTURA) <= CONVERT(DATE, GETDATE())
ORDER BY FECHACOMPLETA_FACTURA DESC";
using var comando = new SqlCommand(query, conexion);
@@ -296,7 +295,7 @@ public class ProcesadorFacturasService : IProcesadorFacturasService
return facturas;
}
private async Task<bool> ProcesarFacturaAsync(FacturaParaProcesar factura, Configuracion config)
private async Task<ResultadoProceso> ProcesarFacturaAsync(FacturaParaProcesar factura, Configuracion config)
{
try
{
@@ -306,7 +305,8 @@ public class ProcesadorFacturasService : IProcesadorFacturasService
string rutaOrigen = Path.Combine(rutaBaseOrigen, factura.NombreArchivoOrigen);
string carpetaDestinoFinal = Path.Combine(rutaBaseDestino, factura.CarpetaDestino);
if (!File.Exists(rutaOrigen)) return false;
// Si no existe origen, es un error (para reintento)
if (!File.Exists(rutaOrigen)) return ResultadoProceso.Error;
if (!Directory.Exists(carpetaDestinoFinal))
{
@@ -319,16 +319,21 @@ public class ProcesadorFacturasService : IProcesadorFacturasService
if (infoDestino.Exists)
{
if (infoDestino.Length == infoOrigen.Length) return true;
// Si ya existe y es igual, lo OMITIMOS
if (infoDestino.Length == infoOrigen.Length)
{
return ResultadoProceso.Omitido;
}
}
// Si llegamos acá, copiamos
File.Copy(rutaOrigen, rutaDestinoCompleta, overwrite: true);
return true;
return ResultadoProceso.Copiado;
}
catch (Exception ex)
{
_logger.LogDebug(ex, "Fallo al copiar {archivo}", factura.NombreArchivoOrigen);
return false;
return ResultadoProceso.Error;
}
}
@@ -481,3 +486,10 @@ public class FacturaParaProcesar
public string NombreArchivoDestino { get; set; } = string.Empty;
public string CarpetaDestino { get; set; } = string.Empty;
}
public enum ResultadoProceso
{
Copiado, // Era nuevo y se copió
Omitido, // Ya existía y estaba bien
Error // No se encontró o falló
}

View File

@@ -37,6 +37,6 @@ services:
container_name: gestor_facturas_web
restart: always
ports:
- "80:80"
- "8080:80"
depends_on:
- backend

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/png" href="/folder.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>frontend</title>
<title>Gestor de Facturas en Directorios</title>
</head>
<body>
<div id="root"></div>

View File

@@ -1,3 +1,4 @@
import { useState, useEffect } from 'react'; // Importar useState y useEffect
import { Clock, PauseCircle, Hourglass } from 'lucide-react';
import { format, formatDistanceToNow, isPast, addSeconds } from 'date-fns';
import { es } from 'date-fns/locale';
@@ -9,15 +10,28 @@ interface ExecutionCardProps {
}
export default function ExecutionCard({ ultimaEjecucion, proximaEjecucion, enEjecucion }: ExecutionCardProps) {
// Estado para forzar la actualización de la UI cada X segundos
const [, setTick] = useState(0);
useEffect(() => {
// Actualizar cada 5 segundos para que el "hace X tiempo" y el estado "En cola"
// se sientan en tiempo real sin esperar al refetch del backend.
const interval = setInterval(() => {
setTick(t => t + 1);
}, 5000);
return () => clearInterval(interval);
}, []);
const formatDate = (dateString: string) => {
return format(new Date(dateString), "dd/MM, HH:mm", { locale: es }); // Formato más corto
return format(new Date(dateString), "dd/MM, HH:mm", { locale: es });
};
const formatRelative = (dateString: string) => {
return formatDistanceToNow(new Date(dateString), { addSuffix: true, locale: es });
};
// Esta lógica se recalculará cada 5 segundos gracias al setTick
const isOverdue = proximaEjecucion ? isPast(addSeconds(new Date(proximaEjecucion), -10)) : false;
return (
@@ -25,10 +39,10 @@ export default function ExecutionCard({ ultimaEjecucion, proximaEjecucion, enEje
{/* Barra de estado */}
<div className={`h-1 w-full ${enEjecucion ? 'bg-green-500' : 'bg-gray-300'}`} />
<div className="p-4 flex-1 flex flex-col"> {/* Padding reducido a p-4 */}
<div className="p-4 flex-1 flex flex-col">
{/* Encabezado Compacto */}
<div className="flex justify-between items-center mb-3"> {/* Margin reducido */}
<div className="flex justify-between items-center mb-3">
<div className="flex items-center gap-2">
<Clock className="w-4 h-4 text-gray-400" />
<span className="text-xs font-bold text-gray-500 uppercase tracking-wider">
@@ -44,7 +58,7 @@ export default function ExecutionCard({ ultimaEjecucion, proximaEjecucion, enEje
</span>
</div>
<div className="space-y-4 pl-1 flex-1 flex flex-col justify-center"> {/* Espaciado reducido a space-y-4 */}
<div className="space-y-4 pl-1 flex-1 flex flex-col justify-center">
{/* ÚLTIMA EJECUCIÓN */}
<div className="relative pl-5 border-l-2 border-gray-100">