feat: envío masivo por fecha — Terceros + RDA en un solo paso

- automation: reescrito para filtrar por fecha (default hoy), obtener
  pacientes únicos del día vía JOIN RECEPCION+PACIENTE+CIUDAD, luego
  agrupar recepciones por IDRECEPCION y enviar Tercero/Crear + RdaPaciente/Insertar a TNS v2
- template: UI simplificada con datepicker (default hoy), tabla de
  resultados por paciente/factura con estado OK/error por ítem

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-06-25 19:08:01 -05:00
co-authored by Claude Sonnet 4.6
parent 531389bc24
commit 3755a51562
2 changed files with 308 additions and 300 deletions
+180 -161
View File
@@ -1,26 +1,78 @@
import json as json_lib import json as json_lib
import httpx from datetime import datetime, date
from datetime import datetime
from fastapi import APIRouter, Request, Form, Depends from fastapi import APIRouter, Request, Form, Depends
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
import httpx
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.firebird_service import get_firebird_from_config from app.services.firebird_service import get_firebird_from_config
from app.services.json_generator import generar_terceros, generar_transaccion, agrupar_por_factura from app.services.json_generator import (
generar_tercero_api,
generar_rda_paciente,
agrupar_por_recepcion,
)
from app.services.api_client import get_tns_token, TNS_BASE
router = APIRouter(prefix="/automation", tags=["automation"]) router = APIRouter(prefix="/automation", tags=["automation"])
# Query para obtener pacientes únicos por rango de fecha
_SQL_PACIENTES = """
SELECT DISTINCT
p.CODIGO,
p.TIPOIDENT,
p.DOCIDENT,
p.NOMBRES,
p.APELLIDOS,
p.DIRECCION,
p.CIUDAD AS COD_CIUDAD,
c.NOMBRE AS NOM_CIUDAD,
p.TELEFONOS,
p.EMAIL,
p.F_NACIMIENTO,
p.SEXO,
p.TIPORES,
p.CODETNIA
FROM PACIENTE p
JOIN RECEPCION r ON r.COD_PACIENTE = p.CODIGO
LEFT JOIN CIUDAD c ON c.CODIGO = p.CIUDAD
WHERE r.FECHA_RECEPCION BETWEEN :fecha_ini AND :fecha_fin
"""
# Query para obtener recepciones con exámenes por rango de fecha
_SQL_RDA = """
SELECT
r.IDRECEPCION,
r.PREFIJO,
r.NUM_FACTURA,
r.FECHA_RECEPCION,
r.COD_PACIENTE,
r.NIT_EMPRESA,
r.DIAG_PPAL,
r.TIPOUSU,
r.TIPOUSUSISPRO,
r.AUTORIZACION,
r.CLASEPROC,
r.HORAINICIORECEPCION,
r.VALORTOTAL,
rel.COD_EXAMEN,
rel.PRECIO,
rel.FECHA_REPORTADO,
m.COD_ESPECIALIDAD,
r.USUARIO AS profesional
FROM RECEPCION r
JOIN RELACION rel ON rel.IDRECEPCION = r.IDRECEPCION
LEFT JOIN MEDICO m ON m.CODIGO = r.COD_MEDICO
WHERE r.FECHA_RECEPCION BETWEEN :fecha_ini AND :fecha_fin
ORDER BY r.IDRECEPCION
"""
@router.get("") @router.get("")
async def automation_page(request: Request, user: dict = Depends(get_current_user)): async def automation_page(request: Request, user: dict = Depends(get_current_user)):
conn = get_connection() today = date.today().isoformat()
queries = conn.execute("SELECT * FROM queries ORDER BY query_type, name").fetchall()
configs = {row["key"]: row["value"] for row in conn.execute("SELECT * FROM config").fetchall()}
conn.close()
return request.app.state.templates.TemplateResponse("automation.html", { return request.app.state.templates.TemplateResponse("automation.html", {
"request": request, "user": user, "request": request, "user": user, "today": today,
"queries": queries, "configs": configs,
}) })
@@ -28,167 +80,134 @@ async def automation_page(request: Request, user: dict = Depends(get_current_use
async def run_automation( async def run_automation(
request: Request, request: Request,
user: dict = Depends(get_current_user), user: dict = Depends(get_current_user),
query_terceros_id: int = Form(...), fecha: str = Form(...),
query_transaccion_id: int = Form(...),
fecha_inicio: str = Form(...),
fecha_fin: str = Form(...),
factura: str = Form(""),
): ):
conn = get_connection() conn = get_connection()
q_terceros = conn.execute("SELECT * FROM queries WHERE id = ?", (query_terceros_id,)).fetchone()
q_trans = conn.execute("SELECT * FROM queries WHERE id = ?", (query_transaccion_id,)).fetchone()
configs = {row["key"]: row["value"] for row in conn.execute("SELECT * FROM config").fetchall()} configs = {row["key"]: row["value"] for row in conn.execute("SELECT * FROM config").fetchall()}
conn.close() conn.close()
if not q_terceros or not q_trans:
return JSONResponse({"success": False, "message": "Consultas no encontradas"})
fb, fb_ok, fb_msg = get_firebird_from_config(configs) fb, fb_ok, fb_msg = get_firebird_from_config(configs)
if not fb_ok: if not fb_ok:
return JSONResponse({"success": False, "message": f"Error Firebird: {fb_msg}"}) return JSONResponse({"success": False, "message": f"Error Firebird: {fb_msg}"})
api_url = configs.get("api_url", "") # Rango: día completo de la fecha seleccionada
api_key = configs.get("api_key", "") fecha_ini = f"{fecha} 00:00:00"
api_method = configs.get("api_method", "POST") fecha_fin = f"{fecha} 23:59:59"
headers = {"Content-Type": "application/json"} params = {"fecha_ini": fecha_ini, "fecha_fin": fecha_fin}
if api_key:
headers["Authorization"] = f"Bearer {api_key}"
resultado = {"paso1_terceros": {"status": "pendiente"}, "paso2_transaccion": {"status": "pendiente"}} # ── Paso 1: obtener pacientes únicos ─────────────────────────────────────
ok1, err1, rows_pac = fb.execute_query(_SQL_PACIENTES, params)
# --------------------------------------------------------------- if not ok1:
# PASO 1: Enviar TERCEROS fb.disconnect()
# --------------------------------------------------------------- return JSONResponse({"success": False, "message": f"Error Firebird pacientes: {err1}"})
params = {"fecha_ini": fecha_inicio, "fecha_fin": fecha_fin}
if ":factura" in q_terceros["query_text"] and factura:
params["factura"] = factura
if ":doc_num" in q_terceros["query_text"]:
params["doc_num"] = ""
success, error, rows = fb.execute_query(
q_terceros["query_text"],
params if ":fecha_ini" in q_terceros["query_text"] else None
)
if not success:
resultado["paso1_terceros"] = {"status": "error", "message": error}
elif not rows:
resultado["paso1_terceros"] = {"status": "error", "message": "No hay pacientes para enviar"}
else:
terceros_enviados = 0
terceros_errores = 0
pacientes_enviados = []
async with httpx.AsyncClient(timeout=int(configs.get("api_timeout", 30))) as client:
for row in rows:
tercero_json = generar_terceros(row)
doc_id = tercero_json["numDocumentoIdentificacion"]
if doc_id in pacientes_enviados:
continue
pacientes_enviados.append(doc_id)
resp_ok = False
resp_text = ""
try:
if api_method == "POST":
resp = await client.post(api_url + "/terceros", json=tercero_json, headers=headers)
else:
resp = await client.put(api_url + "/terceros", json=tercero_json, headers=headers)
resp_ok = resp.is_success
resp_text = resp.text[:1000]
if resp_ok:
terceros_enviados += 1
else:
terceros_errores += 1
except Exception as e:
terceros_errores += 1
resp_text = str(e)
conn = get_connection()
conn.execute("""
INSERT INTO envios (user_id, tipo, factura, status, json_enviado, respuesta_api, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?)
""", (
user["user_id"], "terceros", factura or "AUTO",
"success" if resp_ok else "error",
json_lib.dumps(tercero_json, indent=2, ensure_ascii=False),
resp_text,
datetime.now().isoformat(),
))
conn.commit()
conn.close()
resultado["paso1_terceros"] = {
"status": "success" if terceros_errores == 0 else "partial",
"enviados": terceros_enviados,
"errores": terceros_errores,
}
# ---------------------------------------------------------------
# PASO 2: Enviar TRANSACCION
# ---------------------------------------------------------------
params = {"fecha_ini": fecha_inicio, "fecha_fin": fecha_fin}
if ":factura" in q_trans["query_text"] and factura:
params["factura"] = factura
success, error, rows = fb.execute_query(q_trans["query_text"], params)
if not success:
resultado["paso2_transaccion"] = {"status": "error", "message": error}
elif not rows:
resultado["paso2_transaccion"] = {"status": "error", "message": "No hay servicios para enviar"}
else:
grupos = agrupar_por_factura(rows, factura)
trans_enviados = 0
trans_errores = 0
async with httpx.AsyncClient(timeout=int(configs.get("api_timeout", 30))) as client:
for (fact, doc_key), grupo in grupos.items():
paciente_data = {"tipoDocumentoIdentificacion": doc_key[0], "numDocumentoIdentificacion": doc_key[1]}
trans_json = generar_transaccion(
fact, configs.get("num_documento_obligado", ""),
paciente_data, grupo["procedimientos"],
)
status_ok = False
resp_text = ""
try:
if api_method == "POST":
resp = await client.post(api_url + "/transaccion", json=trans_json, headers=headers)
else:
resp = await client.put(api_url + "/transaccion", json=trans_json, headers=headers)
status_ok = resp.is_success
resp_text = resp.text[:1000]
except Exception as e:
resp_text = str(e)
if status_ok:
trans_enviados += 1
else:
trans_errores += 1
conn = get_connection()
conn.execute("""
INSERT INTO envios (user_id, tipo, factura, fecha_inicio, fecha_fin,
pacientes_count, servicios_count, status, json_enviado, respuesta_api, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (
user["user_id"], "transaccion", fact,
fecha_inicio, fecha_fin,
1, len(grupo["procedimientos"]),
"success" if status_ok else "error",
json_lib.dumps(trans_json, indent=2, ensure_ascii=False)[:5000],
resp_text,
datetime.now().isoformat(),
))
conn.commit()
conn.close()
resultado["paso2_transaccion"] = {
"status": "success" if trans_errores == 0 else "partial",
"enviados": trans_enviados,
"errores": trans_errores,
}
# ── Paso 2: obtener recepciones ───────────────────────────────────────────
ok2, err2, rows_rda = fb.execute_query(_SQL_RDA, params)
fb.disconnect() fb.disconnect()
if not ok2:
return JSONResponse({"success": False, "message": f"Error Firebird RDA: {err2}"})
if not rows_pac and not rows_rda:
return JSONResponse({"success": False, "message": f"No hay datos para la fecha {fecha}"})
# ── Login TNS ─────────────────────────────────────────────────────────────
token, token_err = await get_tns_token(
configs.get("tns_empresa", ""),
configs.get("tns_usuario", ""),
configs.get("tns_password", ""),
)
if not token:
return JSONResponse({"success": False, "message": f"Error login TNS: {token_err}"})
headers = {"Content-Type": "application/json", "Authorization": f"Bearer {token}"}
api_sucursal = configs.get("api_sucursal", "")
timeout = int(configs.get("api_timeout", 30))
resultado = {
"fecha": fecha,
"paso1_terceros": {"enviados": 0, "errores": 0, "detalle": []},
"paso2_rda": {"enviados": 0, "errores": 0, "detalle": []},
}
# ── PASO 1: Enviar Terceros ───────────────────────────────────────────────
async with httpx.AsyncClient(timeout=timeout) as client:
for row in rows_pac:
tercero_json = generar_tercero_api(row)
doc = tercero_json["nit"]
nombre = tercero_json["nombre"]
try:
resp = await client.post(
f"{TNS_BASE}/v2/tablas/Tercero/Crear",
json=tercero_json, headers=headers,
)
ok = resp.is_success
msg = resp.json().get("message", "") if ok else resp.text[:200]
except Exception as e:
ok = False
msg = str(e)
if ok:
resultado["paso1_terceros"]["enviados"] += 1
else:
resultado["paso1_terceros"]["errores"] += 1
resultado["paso1_terceros"]["detalle"].append({
"doc": doc, "nombre": nombre, "ok": ok, "msg": msg,
})
_guardar_envio(user["user_id"], "terceros", fecha, tercero_json, msg, ok)
# ── PASO 2: Enviar RDA Paciente ───────────────────────────────────────────
grupos = agrupar_por_recepcion(rows_rda)
endpoint = f"{TNS_BASE}/v2/rda/RdaPaciente/Insertar"
if api_sucursal:
endpoint += f"?codigosucursal={api_sucursal}"
async with httpx.AsyncClient(timeout=timeout) as client:
for id_recepcion, grupo_rows in grupos.items():
rda_json = generar_rda_paciente(grupo_rows)
factura = str(grupo_rows[0].get("NUM_FACTURA", id_recepcion))
try:
resp = await client.post(endpoint, json=rda_json, headers=headers)
ok = resp.is_success
msg = resp.json().get("message", "") if ok else resp.text[:200]
except Exception as e:
ok = False
msg = str(e)
if ok:
resultado["paso2_rda"]["enviados"] += 1
else:
resultado["paso2_rda"]["errores"] += 1
resultado["paso2_rda"]["detalle"].append({
"factura": factura,
"paciente": str(grupo_rows[0].get("COD_PACIENTE", "")),
"examenes": len(grupo_rows),
"ok": ok, "msg": msg,
})
_guardar_envio(user["user_id"], "transaccion", factura, rda_json, msg, ok,
fecha_inicio=fecha, fecha_fin=fecha,
servicios=len(grupo_rows))
return JSONResponse({"success": True, "resultado": resultado}) return JSONResponse({"success": True, "resultado": resultado})
def _guardar_envio(user_id, tipo, factura, json_data, respuesta, ok,
fecha_inicio=None, fecha_fin=None, servicios=0):
conn = get_connection()
conn.execute("""
INSERT INTO envios (user_id, tipo, factura, fecha_inicio, fecha_fin,
pacientes_count, servicios_count, status, json_enviado, respuesta_api, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (
user_id, tipo, factura, fecha_inicio, fecha_fin,
1, servicios,
"success" if ok else "error",
json_lib.dumps(json_data, indent=2, ensure_ascii=False)[:5000],
respuesta[:1000] if respuesta else "",
datetime.now().isoformat(),
))
conn.commit()
conn.close()
+128 -139
View File
@@ -1,163 +1,152 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Automatización{% endblock %} {% block title %}Envío Masivo{% endblock %}
{% block header %}Automatización Completa{% endblock %} {% block header %}Envío Masivo a TNS{% endblock %}
{% block content %} {% block content %}
<div class="max-w-4xl"> <div class="max-w-4xl space-y-6">
<!-- Panel de control -->
<div class="bg-white rounded-xl shadow-sm border border-gray-200">
<div class="px-6 py-4 border-b border-gray-200">
<h3 class="font-semibold text-gray-800">
<i class="fas fa-paper-plane mr-2 text-blue-500"></i>Envío automático por fecha
</h3>
<p class="text-xs text-gray-500 mt-1">
Selecciona una fecha, el sistema enviará todos los pacientes y sus RDA a TNS.
</p>
</div>
<div class="p-6">
<div class="flex items-end gap-4">
<div class="flex-1">
<label class="block text-sm font-medium text-gray-700 mb-1">Fecha de atención</label>
<input type="date" id="fecha-input" value="{{ today }}"
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
</div>
<button onclick="runAutomation()"
id="btn-run"
class="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors flex items-center gap-2 whitespace-nowrap">
<i class="fas fa-play"></i> Ejecutar envío
</button>
</div>
</div>
</div>
<!-- Progreso -->
<div id="progress-section" class="hidden space-y-4">
<!-- Paso 1 -->
<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 flex items-center justify-between">
<h3 class="font-semibold text-gray-800"><i class="fas fa-robot mr-2 text-blue-500"></i>Paso 1 + Paso 2 Automático</h3> <div class="flex items-center gap-3">
<p class="text-xs text-gray-500 mt-1">Selecciona las consultas y el rango de fechas. El sistema enviará primero los terceros y luego las transacciones RIPS automáticamente.</p> <div class="w-8 h-8 bg-blue-500 rounded-full flex items-center justify-center text-white text-sm font-bold">1</div>
</div> <span class="font-semibold text-gray-800">Terceros (pacientes)</span>
<div class="p-6">
<form id="form-automation" class="space-y-6">
<div class="grid grid-cols-2 gap-6">
<div class="p-4 bg-blue-50 rounded-xl border border-blue-200">
<div class="flex items-center mb-3">
<div class="w-8 h-8 bg-blue-500 rounded-full flex items-center justify-center text-white text-sm font-bold">1</div>
<span class="ml-2 font-medium text-blue-800">Paso 1: Terceros</span>
</div>
<select name="query_terceros_id" required
class="w-full px-3 py-2 border border-blue-300 rounded-lg text-sm">
<option value="">Seleccionar consulta...</option>
{% for q in queries if q.query_type == 'terceros' %}
<option value="{{ q.id }}">{{ q.name }}</option>
{% endfor %}
</select>
</div>
<div class="p-4 bg-purple-50 rounded-xl border border-purple-200">
<div class="flex items-center mb-3">
<div class="w-8 h-8 bg-purple-500 rounded-full flex items-center justify-center text-white text-sm font-bold">2</div>
<span class="ml-2 font-medium text-purple-800">Paso 2: Transacción RIPS</span>
</div>
<select name="query_transaccion_id" required
class="w-full px-3 py-2 border border-purple-300 rounded-lg text-sm">
<option value="">Seleccionar consulta...</option>
{% for q in queries if q.query_type == 'transaccion' %}
<option value="{{ q.id }}">{{ q.name }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="grid grid-cols-3 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Fecha Inicio</label>
<input type="date" name="fecha_inicio" required
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Fecha Fin</label>
<input type="date" name="fecha_fin" required
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Factura <span class="text-xs text-gray-400">(opcional)</span></label>
<input type="text" name="factura"
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm"
placeholder="Filtrar por factura">
</div>
</div>
<button type="button" onclick="runAutomation()"
class="w-full py-3 bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700 text-white font-medium rounded-lg transition-all flex items-center justify-center">
<i class="fas fa-play mr-2"></i> Ejecutar Automatización Completa
</button>
</form>
</div> </div>
<span id="p1-badge" class="text-sm text-gray-400">Esperando...</span>
</div>
<div id="p1-detalle" class="divide-y divide-gray-100 hidden">
</div>
</div> </div>
<!-- Progress --> <!-- Paso 2 -->
<div id="progress-section" class="mt-6 hidden"> <div class="bg-white rounded-xl shadow-sm border border-gray-200">
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6"> <div class="px-6 py-4 border-b border-gray-200 flex items-center justify-between">
<h4 class="font-semibold text-gray-800 mb-4"><i class="fas fa-spinner fa-spin mr-2 text-blue-500"></i>Progreso</h4> <div class="flex items-center gap-3">
<div class="space-y-4"> <div class="w-8 h-8 bg-purple-500 rounded-full flex items-center justify-center text-white text-sm font-bold">2</div>
<div id="step1" class="p-4 rounded-lg border border-gray-200"> <span class="font-semibold text-gray-800">RDA Paciente (recepciones)</span>
<div class="flex items-center">
<div id="step1-icon" class="w-8 h-8 bg-gray-200 rounded-full flex items-center justify-center text-sm">1</div>
<div class="ml-3">
<p class="font-medium text-gray-800">Paso 1: Envío de Terceros</p>
<p id="step1-msg" class="text-sm text-gray-500">Esperando...</p>
</div>
</div>
</div>
<div id="step2" class="p-4 rounded-lg border border-gray-200">
<div class="flex items-center">
<div id="step2-icon" class="w-8 h-8 bg-gray-200 rounded-full flex items-center justify-center text-sm">2</div>
<div class="ml-3">
<p class="font-medium text-gray-800">Paso 2: Envío de Transacciones</p>
<p id="step2-msg" class="text-sm text-gray-500">Esperando...</p>
</div>
</div>
</div>
</div>
</div> </div>
<span id="p2-badge" class="text-sm text-gray-400">Esperando...</span>
</div>
<div id="p2-detalle" class="divide-y divide-gray-100 hidden">
</div>
</div> </div>
</div>
</div> </div>
<script> <script>
async function runAutomation() { async function runAutomation() {
if (!confirm('¿Ejecutar la automatización completa?\n\nPaso 1: Enviar terceros\nPaso 2: Enviar transacciones RIPS')) return; const fecha = document.getElementById('fecha-input').value;
if (!fecha) { showToast('Selecciona una fecha', 'error'); return; }
const form = document.getElementById('form-automation'); if (!confirm(`¿Enviar a TNS todos los pacientes y RDA del ${fecha}?`)) return;
const data = new FormData(form);
const btn = form.querySelector('button[onclick="runAutomation()"]');
showLoading(btn);
document.getElementById('progress-section').classList.remove('hidden'); const btn = document.getElementById('btn-run');
updateStep('step1', 'processing', 'Enviando terceros...'); showLoading(btn);
document.getElementById('progress-section').classList.remove('hidden');
document.getElementById('p1-badge').textContent = 'Enviando...';
document.getElementById('p1-badge').className = 'text-sm text-blue-500';
document.getElementById('p2-badge').textContent = 'Esperando...';
document.getElementById('p2-badge').className = 'text-sm text-gray-400';
const resp = await fetch('/automation/run', {method:'POST', body: data, credentials: 'include'}); const form = new FormData();
const result = await resp.json(); form.append('fecha', fecha);
hideLoading(btn, '<i class="fas fa-play mr-2"></i> Ejecutar Automatización Completa');
if (result.success) { try {
const r = result.resultado; const resp = await fetch('/automation/run', { method: 'POST', body: form, credentials: 'include' });
const data = await resp.json();
hideLoading(btn, '<i class="fas fa-play"></i> Ejecutar envío');
if (r.paso1_terceros.status === 'success') { if (!data.success) {
updateStep('step1', 'success', `${r.paso1_terceros.enviados} terceros enviados`); showToast(data.message || 'Error', 'error');
} else if (r.paso1_terceros.status === 'partial') { document.getElementById('p1-badge').textContent = 'Error';
updateStep('step1', 'warning', `⚠️ ${r.paso1_terceros.enviados} enviados, ${r.paso1_terceros.errores} errores`); document.getElementById('p1-badge').className = 'text-sm text-red-500';
} else { return;
updateStep('step1', 'error', `${r.paso1_terceros.message || 'Error'}`);
}
if (r.paso2_transaccion.status === 'success') {
updateStep('step2', 'success', `${r.paso2_transaccion.enviados} transacciones enviadas`);
} else if (r.paso2_transaccion.status === 'partial') {
updateStep('step2', 'warning', `⚠️ ${r.paso2_transaccion.enviados} enviados, ${r.paso2_transaccion.errores} errores`);
} else {
updateStep('step2', 'error', `${r.paso2_transaccion.message || 'Error'}`);
}
showToast('Automatización completada', 'success');
} else {
updateStep('step1', 'error', 'Error en la automatización');
showToast('Error en la automatización', 'error');
} }
const r = data.resultado;
// Paso 1
renderBadge('p1-badge', r.paso1_terceros.enviados, r.paso1_terceros.errores);
renderDetalle('p1-detalle', r.paso1_terceros.detalle.map(d => ({
col1: d.doc,
col2: d.nombre,
col3: `${d.ok ? '✅' : '❌'} ${d.msg || ''}`,
ok: d.ok,
})));
// Paso 2
renderBadge('p2-badge', r.paso2_rda.enviados, r.paso2_rda.errores);
renderDetalle('p2-detalle', r.paso2_rda.detalle.map(d => ({
col1: `Fac. ${d.factura}`,
col2: `Pac. ${d.paciente}${d.examenes} examen(es)`,
col3: `${d.ok ? '✅' : '❌'} ${d.msg || ''}`,
ok: d.ok,
})));
const totalErr = r.paso1_terceros.errores + r.paso2_rda.errores;
showToast(totalErr === 0 ? 'Envío completado sin errores' : `Completado con ${totalErr} error(es)`,
totalErr === 0 ? 'success' : 'warning');
} catch (e) {
hideLoading(btn, '<i class="fas fa-play"></i> Ejecutar envío');
showToast('Error de conexión', 'error');
}
} }
function updateStep(id, status, msg) { function renderBadge(id, enviados, errores) {
const step = document.getElementById(id); const el = document.getElementById(id);
const icon = document.getElementById(id + '-icon'); if (errores === 0) {
const msgEl = document.getElementById(id + '-msg'); el.textContent = `${enviados} enviados`;
el.className = 'text-sm text-green-600 font-medium';
} else if (enviados > 0) {
el.textContent = `⚠️ ${enviados} OK / ${errores} errores`;
el.className = 'text-sm text-yellow-600 font-medium';
} else {
el.textContent = `${errores} errores`;
el.className = 'text-sm text-red-600 font-medium';
}
}
const icons = { function renderDetalle(id, items) {
processing: '<i class="fas fa-spinner fa-spin text-blue-500"></i>', const el = document.getElementById(id);
success: '<i class="fas fa-check text-green-500"></i>', if (!items || items.length === 0) { return; }
warning: '<i class="fas fa-exclamation-triangle text-yellow-500"></i>', el.innerHTML = items.map(i => `
error: '<i class="fas fa-times text-red-500"></i>', <div class="px-6 py-2 flex items-center gap-3 text-sm ${i.ok ? '' : 'bg-red-50'}">
}; <span class="text-gray-500 w-28 shrink-0">${i.col1}</span>
<span class="text-gray-700 flex-1 truncate">${i.col2}</span>
const borders = { <span class="text-xs ${i.ok ? 'text-green-600' : 'text-red-600'} shrink-0 max-w-xs truncate">${i.col3}</span>
processing: 'border-blue-300 bg-blue-50', </div>
success: 'border-green-300 bg-green-50', `).join('');
warning: 'border-yellow-300 bg-yellow-50', el.classList.remove('hidden');
error: 'border-red-300 bg-red-50',
};
icon.innerHTML = icons[status] || '1';
step.className = `p-4 rounded-lg border ${borders[status] || 'border-gray-200'}`;
msgEl.textContent = msg;
} }
</script> </script>
{% endblock %} {% endblock %}