Compare commits
5 Commits
b2825c5ff3
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 86027ea20a | |||
| 62b52b4f2c | |||
| 4363f87336 | |||
| d03c6d4be4 | |||
| 6ab3c22362 |
@@ -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ó
|
||||
}
|
||||
@@ -37,6 +37,6 @@ services:
|
||||
container_name: gestor_facturas_web
|
||||
restart: always
|
||||
ports:
|
||||
- "80:80"
|
||||
- "8080:80"
|
||||
depends_on:
|
||||
- backend
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
@@ -37,14 +51,14 @@ export default function ExecutionCard({ ultimaEjecucion, proximaEjecucion, enEje
|
||||
</div>
|
||||
|
||||
<span className={`text-[10px] font-bold px-1.5 py-0.5 rounded border ${enEjecucion
|
||||
? 'bg-green-50 text-green-700 border-green-200'
|
||||
: 'bg-gray-50 text-gray-600 border-gray-200'
|
||||
? 'bg-green-50 text-green-700 border-green-200'
|
||||
: 'bg-gray-50 text-gray-600 border-gray-200'
|
||||
}`}>
|
||||
{enEjecucion ? 'AUTO' : 'MANUAL'}
|
||||
</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">
|
||||
|
||||
Reference in New Issue
Block a user