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 import json as json_lib
from datetime import datetime
import httpx
from fastapi import APIRouter, Request, Depends from fastapi import APIRouter, Request, Depends
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from app.database import get_connection from app.database import get_connection
from app.auth import get_current_user from app.auth import get_current_user
from app.services.json_generator import _clean_times 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"]) 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}") @router.get("/detalle/{envio_id}")
async def detalle_envio(envio_id: int, request: Request, user: dict = Depends(get_current_user)): async def detalle_envio(envio_id: int, request: Request, user: dict = Depends(get_current_user)):
conn = get_connection() conn = get_connection()
+37 -2
View File
@@ -135,11 +135,17 @@
</td> </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 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 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 }})" <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> <i class="fas fa-eye"></i>
</button> </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> </td>
</tr> </tr>
{% endfor %} {% endfor %}
@@ -204,6 +210,35 @@
</div> </div>
<script> <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) { async function verDetalle(id) {
const resp = await fetch(`/logs/detalle/${id}`, {credentials: 'include'}); const resp = await fetch(`/logs/detalle/${id}`, {credentials: 'include'});
const data = await resp.json(); const data = await resp.json();