feat: add CIE-10 sync endpoint and migration button in ERP tab

- POST /envios/erp/sync-diagnosticos: reads all 12422 diagnosticos from
  Firebird, sends to WhatsApp in batches of 500 via ingest_diagnosticos.php
- envios_erp.html: new "Migración CIE-10" card with Migrar button and result

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-07-17 10:27:52 -05:00
co-authored by Claude Sonnet 4.6
parent b65fdf0d9c
commit 9a7e4e219b
2 changed files with 113 additions and 2 deletions
+63 -2
View File
@@ -1,8 +1,12 @@
import httpx
import traceback as tb
from fastapi import APIRouter, Request, Depends, Query
from fastapi.responses import JSONResponse
from app.auth import get_current_user
from app.database import get_connection
from app.services.scheduler import sync_recientes
from app.services.firebird_service import get_firebird_from_config
router = APIRouter(prefix="/envios", tags=["envios"])
@@ -33,9 +37,66 @@ async def erp_sync_now(
ventana_min: int = Query(default=30, ge=1, le=1440),
user: dict = Depends(get_current_user),
):
import traceback as tb
try:
resultado = await sync_recientes(ventana_min=ventana_min)
return JSONResponse(resultado)
except Exception as e:
return JSONResponse({"ok": False, "error": str(e), "traceback": tb.format_exc()}, status_code=200)
return JSONResponse({"ok": False, "error": str(e), "traceback": tb.format_exc()})
@router.post("/erp/sync-diagnosticos")
async def erp_sync_diagnosticos(user: dict = Depends(get_current_user)):
"""Migra los 12.422 diagnósticos CIE-10 de Firebird → WhatsApp en lotes de 500."""
try:
conn = get_connection()
configs = {row["key"]: row["value"] for row in conn.execute("SELECT * FROM config").fetchall()}
conn.close()
wa_url = configs.get("whatsapp_url", "").rstrip("/")
wa_key = configs.get("whatsapp_api_key", "")
if not wa_url or not wa_key:
return JSONResponse({"ok": False, "error": "whatsapp_url o whatsapp_api_key no configurados"})
fb, ok, msg = get_firebird_from_config(configs)
if not ok:
return JSONResponse({"ok": False, "error": f"Firebird: {msg}"})
ok_r, err_r, rows = fb.execute_query(
"SELECT TRIM(d.COD_DIAG) AS COD_DIAG, TRIM(d.CONCEPTO) AS CONCEPTO "
"FROM DIAGNOSTICO d ORDER BY d.COD_DIAG", None
)
fb.disconnect()
if not ok_r:
return JSONResponse({"ok": False, "error": f"Query Firebird: {err_r}"})
diagnosticos = [{"cod": r["COD_DIAG"], "concepto": r["CONCEPTO"]} for r in (rows or [])]
total = len(diagnosticos)
batch_size = 500
url = f"{wa_url}/api/lab/ingest_diagnosticos.php"
headers = {"Content-Type": "application/json", "X-Lab-Key": wa_key}
insertados = 0
errores = []
async with httpx.AsyncClient(timeout=60) as client:
for i in range(0, total, batch_size):
lote = diagnosticos[i:i + batch_size]
try:
resp = await client.post(url, json={"rows": lote}, headers=headers)
data = resp.json()
if data.get("ok"):
insertados += data.get("insertados", len(lote))
else:
errores.append(f"Lote {i}-{i+len(lote)}: {data.get('error', '?')}")
except Exception as ex:
errores.append(f"Lote {i}-{i+len(lote)}: {ex}")
return JSONResponse({
"ok": len(errores) == 0,
"total": total,
"insertados": insertados,
"lotes": (total + batch_size - 1) // batch_size,
"errores": errores,
})
except Exception as e:
return JSONResponse({"ok": False, "error": str(e), "traceback": tb.format_exc()})
+50
View File
@@ -78,6 +78,26 @@
</div>
</div>
<!-- Migración diagnósticos CIE-10 -->
<div class="bg-white rounded-xl shadow-sm border border-gray-200">
<div class="px-6 py-4 border-b border-gray-200 flex items-center justify-between">
<h3 class="font-semibold text-gray-800">
<i class="fas fa-stethoscope mr-2 text-orange-400"></i>Migración CIE-10
</h3>
<span class="text-xs text-gray-400">12.422 diagnósticos de Firebird → WhatsApp</span>
</div>
<div class="p-5 flex flex-wrap items-center gap-4">
<p class="text-sm text-gray-500 flex-1">Ejecutar una vez para poblar la tabla <code class="bg-gray-100 px-1 rounded">cie10_diagnosticos</code> en WhatsApp. Requiere correr la migración SQL primero.</p>
<button id="btn-sync-diag" onclick="ejecutarSyncDiag()"
class="flex items-center gap-2 bg-orange-500 hover:bg-orange-600 text-white text-sm font-medium px-4 py-2 rounded-lg transition-colors whitespace-nowrap">
<i class="fas fa-upload text-xs"></i> Migrar diagnósticos
</button>
</div>
<div id="sync-diag-result" class="hidden px-5 pb-4">
<div id="sync-diag-inner"></div>
</div>
</div>
<!-- Prueba manual -->
<div class="bg-white rounded-xl shadow-sm border border-gray-200">
<div class="px-6 py-4 border-b border-gray-200">
@@ -158,6 +178,36 @@ function switchTab(name) {
const _origenLabel = { manual: 'Manual', tercero: 'Tercero', scheduler: 'Scheduler', automation: 'Automatización' };
const _modoLabel = { insertar: 'Solo nuevos', upsert: 'Upsert' };
async function ejecutarSyncDiag() {
const btn = document.getElementById('btn-sync-diag');
const resBox = document.getElementById('sync-diag-result');
const inner = document.getElementById('sync-diag-inner');
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-spinner fa-spin text-xs"></i> Migrando…';
resBox.classList.remove('hidden');
inner.innerHTML = '<p class="text-sm text-gray-400">Enviando 12.422 diagnósticos en lotes de 500…</p>';
try {
const resp = await fetch('/envios/erp/sync-diagnosticos', { method: 'POST' });
const d = await resp.json();
if (d.ok) {
inner.innerHTML = `<div class="text-sm text-green-700 bg-green-50 border border-green-200 rounded-lg px-4 py-3">
<i class="fas fa-check-circle mr-1"></i>
<strong>${d.insertados.toLocaleString()}</strong> diagnósticos migrados en <strong>${d.lotes}</strong> lotes.
</div>`;
} else {
inner.innerHTML = `<div class="text-sm text-red-600 bg-red-50 border border-red-200 rounded-lg px-4 py-3">
<i class="fas fa-exclamation-circle mr-1"></i>${d.error || 'Error'}
${d.errores?.length ? '<ul class="mt-1 ml-4 list-disc text-xs">' + d.errores.map(e => `<li>${e}</li>`).join('') + '</ul>' : ''}
</div>`;
}
} catch(e) {
inner.innerHTML = `<p class="text-sm text-red-500">Error de red: ${e.message}</p>`;
} finally {
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-upload text-xs"></i> Migrar diagnósticos';
}
}
function toggleDet(id) {
const row = document.getElementById(id);
if (row) row.classList.toggle('hidden');