Add reenviar button in /logs for failed envios

POST /logs/reenviar/{id} re-sends stored JSON to TNS and updates
status to 'success' in-place if it succeeds. Button appears only
on error rows with saved JSON; updates row color and badge without reload.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-07-11 09:24:22 -05:00
co-authored by Claude Sonnet 4.6
parent 3d848fbf2d
commit cb3b327938
2 changed files with 112 additions and 2 deletions
+75
View File
@@ -1,9 +1,13 @@
import json as json_lib
from datetime import datetime
import httpx
from fastapi import APIRouter, Request, Depends
from fastapi.responses import JSONResponse
from app.database import get_connection
from app.auth import get_current_user
from app.services.json_generator import _clean_times
from app.services.api_client import get_tns_token, TNS_BASE
from app.utils.activity import log_activity, get_ip
router = APIRouter(prefix="/logs", tags=["logs"])
@@ -133,6 +137,77 @@ async def actividad_page(
})
@router.post("/reenviar/{envio_id}")
async def reenviar_envio(envio_id: int, request: Request, user: dict = Depends(get_current_user)):
conn = get_connection()
row = conn.execute("SELECT * FROM envios WHERE id = ?", (envio_id,)).fetchone()
conn.close()
if not row:
return JSONResponse({"success": False, "message": "Registro no encontrado"})
if row["status"] == "success":
return JSONResponse({"success": False, "message": "Ya fue enviado exitosamente"})
if not row["json_enviado"]:
return JSONResponse({"success": False, "message": "Sin JSON guardado para reenviar"})
try:
rda_json = json_lib.loads(row["json_enviado"])
except Exception:
return JSONResponse({"success": False, "message": "JSON inválido en registro"})
conn_cfg = get_connection()
cfg = {r["key"]: r["value"] for r in conn_cfg.execute("SELECT * FROM config").fetchall()}
conn_cfg.close()
token, err = await get_tns_token(
cfg.get("tns_empresa", ""), cfg.get("tns_usuario", ""), cfg.get("tns_password", "")
)
if not token:
return JSONResponse({"success": False, "message": f"Error login TNS: {err}"})
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
api_sucursal = cfg.get("api_sucursal", "00") or "00"
tipo = row["tipo"]
endpoint = (
f"{TNS_BASE}/v2/rda/RdaPaciente/Insertar?codigosucursal={api_sucursal}"
if tipo == "transaccion"
else f"{TNS_BASE}/v2/tablas/Tercero/Crear"
)
ok = False
msg = ""
raw_resp = ""
try:
async with httpx.AsyncClient(timeout=30) as client:
r = await client.post(endpoint, json=rda_json, headers=headers)
raw_resp = r.text
try:
data = r.json()
if tipo == "transaccion":
ok = bool(data.get("status") or (data.get("data") or {}).get("success", False))
msg = ((data.get("data") or {}).get("response") or data.get("message") or raw_resp[:300])
else:
ok = r.is_success
msg = data.get("message", "") or raw_resp[:200]
except Exception:
ok = r.status_code < 300
msg = raw_resp[:300]
except Exception as ex:
msg = str(ex)
if ok:
conn = get_connection()
conn.execute(
"UPDATE envios SET status='success', mensaje_tns=?, respuesta_api=?, created_at=? WHERE id=?",
(msg, raw_resp[:2000], datetime.now().isoformat(), envio_id)
)
conn.commit()
conn.close()
log_activity(user["user_id"], user["username"], "reenvio_ok",
f"Envío #{envio_id} factura {row['factura']} reenvío exitoso", get_ip(request))
return JSONResponse({"success": ok, "message": msg})
@router.get("/detalle/{envio_id}")
async def detalle_envio(envio_id: int, request: Request, user: dict = Depends(get_current_user)):
conn = get_connection()
+37 -2
View File
@@ -135,11 +135,17 @@
</td>
<td class="px-2 py-2.5 text-gray-400 whitespace-nowrap">{{ e.created_at[:16] if e.created_at else '-' }}</td>
<td class="px-2 py-2.5 text-gray-400">{{ e.username or '-' }}</td>
<td class="px-2 py-2.5">
<td class="px-2 py-2.5 flex items-center gap-1.5">
<button onclick="verDetalle({{ e.id }})"
class="px-2 py-1 bg-gray-100 hover:bg-gray-200 text-gray-600 rounded" title="Ver detalle completo">
class="px-2 py-1 bg-gray-100 hover:bg-gray-200 text-gray-600 rounded" title="Ver detalle">
<i class="fas fa-eye"></i>
</button>
{% if e.status == 'error' and e.json_enviado %}
<button onclick="reenviar({{ e.id }}, this)"
class="px-2 py-1 bg-orange-100 hover:bg-orange-200 text-orange-700 rounded" title="Reenviar a TNS">
<i class="fas fa-redo text-xs"></i>
</button>
{% endif %}
</td>
</tr>
{% endfor %}
@@ -204,6 +210,35 @@
</div>
<script>
async function reenviar(id, btn) {
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-spinner fa-spin text-xs"></i>';
try {
const resp = await fetch(`/logs/reenviar/${id}`, {method: 'POST', credentials: 'include'});
const data = await resp.json();
if (data.success) {
showToast('Reenvío exitoso', 'success');
const row = btn.closest('tr');
row.classList.remove('bg-red-50/25');
row.classList.add('bg-green-50/25');
const badge = row.querySelector('td:nth-child(6) span');
if (badge) {
badge.textContent = '✓ OK';
badge.className = 'px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-700';
}
btn.remove();
} else {
showToast(data.message || 'Error al reenviar', 'error');
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-redo text-xs"></i>';
}
} catch(e) {
showToast('Error de conexión', 'error');
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-redo text-xs"></i>';
}
}
async function verDetalle(id) {
const resp = await fetch(`/logs/detalle/${id}`, {credentials: 'include'});
const data = await resp.json();