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 import APIRouter, Request, Depends, Query
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from app.auth import get_current_user from app.auth import get_current_user
from app.database import get_connection from app.database import get_connection
from app.services.scheduler import sync_recientes from app.services.scheduler import sync_recientes
from app.services.firebird_service import get_firebird_from_config
router = APIRouter(prefix="/envios", tags=["envios"]) 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), ventana_min: int = Query(default=30, ge=1, le=1440),
user: dict = Depends(get_current_user), user: dict = Depends(get_current_user),
): ):
import traceback as tb
try: try:
resultado = await sync_recientes(ventana_min=ventana_min) resultado = await sync_recientes(ventana_min=ventana_min)
return JSONResponse(resultado) return JSONResponse(resultado)
except Exception as e: 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>
</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 --> <!-- Prueba manual -->
<div class="bg-white rounded-xl shadow-sm border border-gray-200"> <div class="bg-white rounded-xl shadow-sm border border-gray-200">
<div class="px-6 py-4 border-b 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 _origenLabel = { manual: 'Manual', tercero: 'Tercero', scheduler: 'Scheduler', automation: 'Automatización' };
const _modoLabel = { insertar: 'Solo nuevos', upsert: 'Upsert' }; 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) { function toggleDet(id) {
const row = document.getElementById(id); const row = document.getElementById(id);
if (row) row.classList.toggle('hidden'); if (row) row.classList.toggle('hidden');