Mejora Transacción y Historial: estado por fila, envío individual y filtros avanzados
- Transacción: tabla con estado Enviado/Pendiente/Error por IDRECEPCION, botón enviar fila a fila o masivo, modal con JSON + respuesta TNS por registro
- Historial: filtros por fecha, tipo, IDRECEPCION/contrato; paginación 50 por página; modal con 3 tabs (JSON enviado / Respuesta TNS / Mensaje)
- DB: migración automática para agregar columnas idrecepcion, contrato, mensaje_tns a tabla envios
- Logs route: nuevo endpoint /logs/detalle/{id} para cargar detalle completo vía JS
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
ef71a1db56
commit
7699303a53
@@ -13,6 +13,17 @@ def get_connection():
|
||||
return conn
|
||||
|
||||
|
||||
def _migrate(conn):
|
||||
cols = {r[1] for r in conn.execute("PRAGMA table_info(envios)")}
|
||||
if "idrecepcion" not in cols:
|
||||
conn.execute("ALTER TABLE envios ADD COLUMN idrecepcion INTEGER")
|
||||
if "contrato" not in cols:
|
||||
conn.execute("ALTER TABLE envios ADD COLUMN contrato TEXT")
|
||||
if "mensaje_tns" not in cols:
|
||||
conn.execute("ALTER TABLE envios ADD COLUMN mensaje_tns TEXT")
|
||||
conn.commit()
|
||||
|
||||
|
||||
def init_db():
|
||||
conn = get_connection()
|
||||
conn.executescript("""
|
||||
@@ -67,4 +78,5 @@ def init_db():
|
||||
);
|
||||
""")
|
||||
conn.commit()
|
||||
_migrate(conn)
|
||||
conn.close()
|
||||
|
||||
+60
-12
@@ -1,9 +1,12 @@
|
||||
from fastapi import APIRouter, Request, Depends
|
||||
from fastapi.responses import JSONResponse
|
||||
from app.database import get_connection
|
||||
from app.auth import get_current_user
|
||||
|
||||
router = APIRouter(prefix="/logs", tags=["logs"])
|
||||
|
||||
PAGE_SIZE = 50
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def logs_page(
|
||||
@@ -12,9 +15,12 @@ async def logs_page(
|
||||
tipo: str = "",
|
||||
status: str = "",
|
||||
factura: str = "",
|
||||
paciente: str = "",
|
||||
fecha_desde: str = "",
|
||||
fecha_hasta: str = "",
|
||||
offset: int = 0,
|
||||
):
|
||||
conn = get_connection()
|
||||
|
||||
where = ["1=1"]
|
||||
params = []
|
||||
|
||||
@@ -27,29 +33,71 @@ async def logs_page(
|
||||
if factura:
|
||||
where.append("e.factura LIKE ?")
|
||||
params.append(f"%{factura}%")
|
||||
if paciente:
|
||||
where.append("(CAST(e.idrecepcion AS TEXT) LIKE ? OR e.contrato LIKE ?)")
|
||||
params += [f"%{paciente}%", f"%{paciente}%"]
|
||||
if fecha_desde:
|
||||
where.append("DATE(e.created_at) >= ?")
|
||||
params.append(fecha_desde)
|
||||
if fecha_hasta:
|
||||
where.append("DATE(e.created_at) <= ?")
|
||||
params.append(fecha_hasta)
|
||||
|
||||
w = " AND ".join(where)
|
||||
|
||||
total = conn.execute(f"SELECT COUNT(*) FROM envios e WHERE {w}", params).fetchone()[0]
|
||||
|
||||
envios = conn.execute(f"""
|
||||
SELECT e.*, u.username FROM envios e
|
||||
LEFT JOIN users u ON e.user_id = u.id
|
||||
WHERE {' AND '.join(where)}
|
||||
ORDER BY e.created_at DESC LIMIT 100
|
||||
""", params).fetchall()
|
||||
WHERE {w}
|
||||
ORDER BY e.created_at DESC
|
||||
LIMIT ? OFFSET ?
|
||||
""", params + [PAGE_SIZE, offset]).fetchall()
|
||||
|
||||
stats = conn.execute("""
|
||||
SELECT
|
||||
tipo,
|
||||
status,
|
||||
COUNT(*) as total,
|
||||
SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) as exitosos,
|
||||
SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END) as fallidos
|
||||
SELECT tipo, status, COUNT(*) as cnt
|
||||
FROM envios
|
||||
GROUP BY tipo, status
|
||||
ORDER BY tipo, status
|
||||
""").fetchall()
|
||||
|
||||
conn.close()
|
||||
|
||||
has_more = (offset + PAGE_SIZE) < total
|
||||
|
||||
return request.app.state.templates.TemplateResponse("logs.html", {
|
||||
"request": request, "user": user,
|
||||
"envios": envios, "stats": stats,
|
||||
"filtro_tipo": tipo, "filtro_status": status, "filtro_factura": factura,
|
||||
"envios": envios, "stats": stats, "total": total,
|
||||
"filtro_tipo": tipo, "filtro_status": status,
|
||||
"filtro_factura": factura, "filtro_paciente": paciente,
|
||||
"filtro_fecha_desde": fecha_desde, "filtro_fecha_hasta": fecha_hasta,
|
||||
"offset": offset, "page_size": PAGE_SIZE, "has_more": has_more,
|
||||
})
|
||||
|
||||
|
||||
@router.get("/detalle/{envio_id}")
|
||||
async def detalle_envio(envio_id: int, request: Request, user: dict = Depends(get_current_user)):
|
||||
conn = get_connection()
|
||||
row = conn.execute(
|
||||
"SELECT e.*, u.username FROM envios e LEFT JOIN users u ON e.user_id = u.id WHERE e.id = ?",
|
||||
(envio_id,)
|
||||
).fetchone()
|
||||
conn.close()
|
||||
if not row:
|
||||
return JSONResponse({"error": "No encontrado"}, status_code=404)
|
||||
return JSONResponse({
|
||||
"id": row["id"],
|
||||
"tipo": row["tipo"],
|
||||
"factura": row["factura"],
|
||||
"idrecepcion": row["idrecepcion"],
|
||||
"contrato": row["contrato"],
|
||||
"status": row["status"],
|
||||
"mensaje_tns": row["mensaje_tns"],
|
||||
"respuesta_api": row["respuesta_api"],
|
||||
"json_enviado": row["json_enviado"],
|
||||
"fecha_inicio": row["fecha_inicio"],
|
||||
"fecha_fin": row["fecha_fin"],
|
||||
"created_at": row["created_at"],
|
||||
"username": row["username"],
|
||||
})
|
||||
|
||||
+211
-132
@@ -12,194 +12,273 @@ from app.services.api_client import get_tns_token, TNS_BASE
|
||||
router = APIRouter(prefix="/transaccion", tags=["transaccion"])
|
||||
|
||||
|
||||
def _cfg_and_fb():
|
||||
conn = get_connection()
|
||||
cfg = {r["key"]: r["value"] for r in conn.execute("SELECT * FROM config").fetchall()}
|
||||
conn.close()
|
||||
return cfg
|
||||
|
||||
|
||||
def _query_rows(cfg, query_text, factura, fecha_inicio, fecha_fin):
|
||||
import re as _re
|
||||
fb, ok, msg = get_firebird_from_config(cfg)
|
||||
if not ok:
|
||||
return None, msg, None
|
||||
nums = _re.findall(r'\d+', factura or "")
|
||||
num_val = int(nums[-1]) if nums else (factura or "")
|
||||
params = {"fecha_ini": f"{fecha_inicio} 00:00:00", "fecha_fin": f"{fecha_fin} 23:59:59"}
|
||||
if ":num_factura" in query_text:
|
||||
params["num_factura"] = num_val
|
||||
ok2, err, rows = fb.execute_query(query_text, params)
|
||||
fb.disconnect()
|
||||
if not ok2:
|
||||
return None, err, None
|
||||
return rows, None, fb
|
||||
|
||||
|
||||
def _is_sent(idrecepcion: int) -> dict:
|
||||
"""Returns the latest envio record for this idrecepcion, or None."""
|
||||
conn = get_connection()
|
||||
row = conn.execute(
|
||||
"SELECT status, mensaje_tns, created_at FROM envios WHERE idrecepcion=? AND tipo='transaccion' ORDER BY id DESC LIMIT 1",
|
||||
(idrecepcion,)
|
||||
).fetchone()
|
||||
conn.close()
|
||||
if row:
|
||||
return {"status": row["status"], "mensaje": row["mensaje_tns"], "at": row["created_at"]}
|
||||
return None
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def transaccion_page(request: Request, user: dict = Depends(get_current_user)):
|
||||
conn = get_connection()
|
||||
envios = conn.execute("""
|
||||
SELECT * FROM envios WHERE tipo = 'transaccion'
|
||||
ORDER BY created_at DESC LIMIT 20
|
||||
""").fetchall()
|
||||
queries = conn.execute(
|
||||
"SELECT * FROM queries WHERE query_type = 'transaccion' ORDER BY name"
|
||||
).fetchall()
|
||||
configs = {row["key"]: row["value"] for row in conn.execute("SELECT * FROM config").fetchall()}
|
||||
conn.close()
|
||||
|
||||
return request.app.state.templates.TemplateResponse("transaccion.html", {
|
||||
"request": request, "user": user,
|
||||
"envios": envios, "queries": queries,
|
||||
"configs": configs,
|
||||
"request": request, "user": user, "queries": queries, "configs": configs,
|
||||
})
|
||||
|
||||
|
||||
@router.post("/preview")
|
||||
async def preview_transaccion(
|
||||
request: Request,
|
||||
user: dict = Depends(get_current_user),
|
||||
query_id: int = Form(...),
|
||||
factura: str = Form(""),
|
||||
fecha_inicio: str = Form(...),
|
||||
fecha_fin: str = Form(...),
|
||||
request: Request, user: dict = Depends(get_current_user),
|
||||
query_id: int = Form(...), factura: str = Form(""),
|
||||
fecha_inicio: str = Form(...), fecha_fin: str = Form(...),
|
||||
):
|
||||
conn = get_connection()
|
||||
q = conn.execute("SELECT * FROM queries WHERE id = ?", (query_id,)).fetchone()
|
||||
configs = {row["key"]: row["value"] for row in conn.execute("SELECT * FROM config").fetchall()}
|
||||
cfg = {r["key"]: r["value"] for r in conn.execute("SELECT * FROM config").fetchall()}
|
||||
conn.close()
|
||||
|
||||
if not q:
|
||||
return JSONResponse({"success": False, "message": "Consulta no encontrada"})
|
||||
|
||||
fb, ok, msg = get_firebird_from_config(configs)
|
||||
if not ok:
|
||||
return JSONResponse({"success": False, "message": f"Error Firebird: {msg}"})
|
||||
|
||||
if ":num_factura" in q["query_text"] and not factura:
|
||||
fb.disconnect()
|
||||
return JSONResponse({"success": False, "message": "Ingresa el número de factura"})
|
||||
|
||||
# Extraer solo la parte numérica: "LHXC 3918" o "LHXC-3918" → 3918
|
||||
import re as _re
|
||||
nums = _re.findall(r'\d+', factura)
|
||||
num_factura_val = int(nums[-1]) if nums else factura
|
||||
|
||||
params = {
|
||||
"fecha_ini": f"{fecha_inicio} 00:00:00",
|
||||
"fecha_fin": f"{fecha_fin} 23:59:59",
|
||||
}
|
||||
if ":num_factura" in q["query_text"]:
|
||||
params["num_factura"] = num_factura_val
|
||||
|
||||
success, error, rows = fb.execute_query(q["query_text"], params)
|
||||
fb.disconnect()
|
||||
|
||||
if not success:
|
||||
return JSONResponse({"success": False, "message": error})
|
||||
rows, err, _ = _query_rows(cfg, q["query_text"], factura, fecha_inicio, fecha_fin)
|
||||
if rows is None:
|
||||
return JSONResponse({"success": False, "message": err})
|
||||
if not rows:
|
||||
return JSONResponse({"success": False, "message": "Sin datos"})
|
||||
|
||||
grupos = agrupar_por_recepcion(rows)
|
||||
prof_def = configs.get("profesional_default", "")
|
||||
esp_def = configs.get("especialidad_default", "")
|
||||
json_result = [generar_rda_paciente(grupo, prof_def, esp_def) for grupo in grupos.values()]
|
||||
prof_def = cfg.get("profesional_default", "")
|
||||
esp_def = cfg.get("especialidad_default", "")
|
||||
remis_def = cfg.get("remisionante_default", "00")
|
||||
prefijo_def = cfg.get("prefijo_tns_default", "00")
|
||||
|
||||
items = []
|
||||
for id_rec, grupo_rows in grupos.items():
|
||||
enviado = _is_sent(id_rec)
|
||||
rda = generar_rda_paciente(grupo_rows, prof_def, esp_def, remis_def, prefijo_def)
|
||||
items.append({
|
||||
"idrecepcion": id_rec,
|
||||
"factura": grupo_rows[0].get("NUM_FACTURA", ""),
|
||||
"paciente": grupo_rows[0].get("COD_PACIENTE", ""),
|
||||
"contrato": grupo_rows[0].get("CODCONTRATO", ""),
|
||||
"examenes": [r.get("COD_EXAMEN", "") for r in grupo_rows],
|
||||
"valor": float(grupo_rows[0].get("VALORTOTAL") or 0),
|
||||
"enviado": enviado,
|
||||
"json": rda,
|
||||
})
|
||||
|
||||
pendientes = sum(1 for i in items if not i["enviado"])
|
||||
enviados_ok = sum(1 for i in items if i["enviado"] and i["enviado"]["status"] == "success")
|
||||
enviados_err = sum(1 for i in items if i["enviado"] and i["enviado"]["status"] == "error")
|
||||
|
||||
return JSONResponse({
|
||||
"success": True,
|
||||
"rows_count": len(rows),
|
||||
"grupos_count": len(grupos),
|
||||
"columns": list(rows[0].keys()) if rows else [],
|
||||
"preview": rows[:5],
|
||||
"generated_json": json_result[0] if json_result else None,
|
||||
"total_json": len(json_result),
|
||||
"total": len(items),
|
||||
"pendientes": pendientes,
|
||||
"enviados_ok": enviados_ok,
|
||||
"enviados_err": enviados_err,
|
||||
"items": items,
|
||||
})
|
||||
|
||||
|
||||
@router.post("/send")
|
||||
async def send_transaccion(
|
||||
request: Request,
|
||||
user: dict = Depends(get_current_user),
|
||||
query_id: int = Form(...),
|
||||
factura: str = Form(""),
|
||||
fecha_inicio: str = Form(...),
|
||||
fecha_fin: str = Form(...),
|
||||
@router.post("/send-one")
|
||||
async def send_one(
|
||||
request: Request, user: dict = Depends(get_current_user),
|
||||
idrecepcion: int = Form(...), query_id: int = Form(...),
|
||||
factura: str = Form(""), fecha_inicio: str = Form(...), fecha_fin: str = Form(...),
|
||||
):
|
||||
conn = get_connection()
|
||||
q = conn.execute("SELECT * FROM queries WHERE id = ?", (query_id,)).fetchone()
|
||||
configs = {row["key"]: row["value"] for row in conn.execute("SELECT * FROM config").fetchall()}
|
||||
cfg = {r["key"]: r["value"] for r in conn.execute("SELECT * FROM config").fetchall()}
|
||||
conn.close()
|
||||
|
||||
if not q:
|
||||
return JSONResponse({"success": False, "message": "Consulta no encontrada"})
|
||||
|
||||
fb, ok, msg = get_firebird_from_config(configs)
|
||||
if not ok:
|
||||
return JSONResponse({"success": False, "message": f"Error Firebird: {msg}"})
|
||||
|
||||
if ":num_factura" in q["query_text"] and not factura:
|
||||
fb.disconnect()
|
||||
return JSONResponse({"success": False, "message": "Ingresa el número de factura"})
|
||||
|
||||
# Extraer solo la parte numérica: "LHXC 3918" o "LHXC-3918" → 3918
|
||||
import re as _re
|
||||
nums = _re.findall(r'\d+', factura)
|
||||
num_factura_val = int(nums[-1]) if nums else factura
|
||||
|
||||
params = {
|
||||
"fecha_ini": f"{fecha_inicio} 00:00:00",
|
||||
"fecha_fin": f"{fecha_fin} 23:59:59",
|
||||
}
|
||||
if ":num_factura" in q["query_text"]:
|
||||
params["num_factura"] = num_factura_val
|
||||
|
||||
success, error, rows = fb.execute_query(q["query_text"], params)
|
||||
fb.disconnect()
|
||||
|
||||
if not success:
|
||||
return JSONResponse({"success": False, "message": error})
|
||||
if not rows:
|
||||
return JSONResponse({"success": False, "message": "No se encontraron datos"})
|
||||
rows, err, _ = _query_rows(cfg, q["query_text"], factura, fecha_inicio, fecha_fin)
|
||||
if rows is None:
|
||||
return JSONResponse({"success": False, "message": err})
|
||||
|
||||
grupos = agrupar_por_recepcion(rows)
|
||||
grupo_rows = grupos.get(idrecepcion)
|
||||
if not grupo_rows:
|
||||
return JSONResponse({"success": False, "message": f"IDRECEPCION {idrecepcion} no encontrado"})
|
||||
|
||||
token, token_err = await get_tns_token(
|
||||
configs.get("tns_empresa", ""),
|
||||
configs.get("tns_usuario", ""),
|
||||
configs.get("tns_password", ""),
|
||||
cfg.get("tns_empresa", ""), cfg.get("tns_usuario", ""), cfg.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", "") or "00"
|
||||
api_sucursal = cfg.get("api_sucursal", "") or "00"
|
||||
endpoint = f"{TNS_BASE}/v2/rda/RdaPaciente/Insertar?codigosucursal={api_sucursal}"
|
||||
|
||||
rda_json = generar_rda_paciente(
|
||||
grupo_rows,
|
||||
cfg.get("profesional_default", ""),
|
||||
cfg.get("especialidad_default", ""),
|
||||
cfg.get("remisionante_default", "00"),
|
||||
cfg.get("prefijo_tns_default", "00"),
|
||||
)
|
||||
|
||||
raw_resp = ""
|
||||
ok_rda = False
|
||||
msg_tns = ""
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=int(cfg.get("api_timeout", 30))) as client:
|
||||
r = await client.post(endpoint, json=rda_json, headers=headers)
|
||||
raw_resp = r.text
|
||||
try:
|
||||
data = r.json()
|
||||
ok_rda = bool(data.get("status") or (data.get("data") or {}).get("success", False))
|
||||
msg_tns = ((data.get("data") or {}).get("response") or data.get("message") or raw_resp[:300])
|
||||
except Exception:
|
||||
ok_rda = r.status_code < 300
|
||||
msg_tns = raw_resp[:300]
|
||||
except Exception as ex:
|
||||
msg_tns = str(ex)
|
||||
|
||||
conn = get_connection()
|
||||
conn.execute("""
|
||||
INSERT INTO envios (user_id, tipo, factura, idrecepcion, contrato,
|
||||
fecha_inicio, fecha_fin, pacientes_count, servicios_count,
|
||||
status, json_enviado, respuesta_api, mensaje_tns, created_at)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
""", (
|
||||
user["user_id"], "transaccion",
|
||||
str(grupo_rows[0].get("NUM_FACTURA", idrecepcion)),
|
||||
idrecepcion,
|
||||
str(grupo_rows[0].get("CODCONTRATO", "")),
|
||||
fecha_inicio, fecha_fin, 1, len(grupo_rows),
|
||||
"success" if ok_rda else "error",
|
||||
json_lib.dumps(rda_json, ensure_ascii=False)[:5000],
|
||||
raw_resp[:2000],
|
||||
msg_tns,
|
||||
datetime.now().isoformat(),
|
||||
))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
return JSONResponse({"success": ok_rda, "message": msg_tns, "raw_tns": raw_resp, "idrecepcion": idrecepcion})
|
||||
|
||||
|
||||
@router.post("/send")
|
||||
async def send_transaccion(
|
||||
request: Request, user: dict = Depends(get_current_user),
|
||||
query_id: int = Form(...), factura: str = Form(""),
|
||||
fecha_inicio: str = Form(...), fecha_fin: str = Form(...),
|
||||
solo_pendientes: str = Form("0"),
|
||||
):
|
||||
conn = get_connection()
|
||||
q = conn.execute("SELECT * FROM queries WHERE id = ?", (query_id,)).fetchone()
|
||||
cfg = {r["key"]: r["value"] for r in conn.execute("SELECT * FROM config").fetchall()}
|
||||
conn.close()
|
||||
|
||||
rows, err, _ = _query_rows(cfg, q["query_text"], factura, fecha_inicio, fecha_fin)
|
||||
if rows is None:
|
||||
return JSONResponse({"success": False, "message": err})
|
||||
if not rows:
|
||||
return JSONResponse({"success": False, "message": "Sin datos"})
|
||||
|
||||
grupos = agrupar_por_recepcion(rows)
|
||||
if solo_pendientes == "1":
|
||||
grupos = {k: v for k, v in grupos.items() if not _is_sent(k)}
|
||||
|
||||
token, 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: {token_err}"})
|
||||
|
||||
headers = {"Content-Type": "application/json", "Authorization": f"Bearer {token}"}
|
||||
api_sucursal = cfg.get("api_sucursal", "") or "00"
|
||||
endpoint = f"{TNS_BASE}/v2/rda/RdaPaciente/Insertar?codigosucursal={api_sucursal}"
|
||||
|
||||
total_enviados = 0
|
||||
total_errores = 0
|
||||
resultados = []
|
||||
prof_def = configs.get("profesional_default", "")
|
||||
esp_def = configs.get("especialidad_default", "")
|
||||
remis_def = configs.get("remisionante_default", "00")
|
||||
prefijo_def = configs.get("prefijo_tns_default", "00")
|
||||
|
||||
async with httpx.AsyncClient(timeout=int(configs.get("api_timeout", 30))) as client:
|
||||
for id_recepcion, grupo_rows in grupos.items():
|
||||
trans_json = generar_rda_paciente(grupo_rows, prof_def, esp_def, remis_def, prefijo_def)
|
||||
|
||||
status_ok = False
|
||||
response_text = ""
|
||||
async with httpx.AsyncClient(timeout=int(cfg.get("api_timeout", 30))) as client:
|
||||
for id_rec, grupo_rows in grupos.items():
|
||||
rda_json = generar_rda_paciente(
|
||||
grupo_rows,
|
||||
cfg.get("profesional_default", ""),
|
||||
cfg.get("especialidad_default", ""),
|
||||
cfg.get("remisionante_default", "00"),
|
||||
cfg.get("prefijo_tns_default", "00"),
|
||||
)
|
||||
raw_resp = ""
|
||||
ok_rda = False
|
||||
msg_tns = ""
|
||||
try:
|
||||
resp = await client.post(endpoint, json=trans_json, headers=headers)
|
||||
status_ok = resp.is_success
|
||||
response_text = resp.text[:1000]
|
||||
except Exception as e:
|
||||
response_text = str(e)
|
||||
|
||||
if status_ok:
|
||||
total_enviados += 1
|
||||
else:
|
||||
total_errores += 1
|
||||
|
||||
fact = str(grupo_rows[0].get("NUM_FACTURA", id_recepcion))
|
||||
resultados.append({"factura": fact, "success": status_ok, "msg": response_text})
|
||||
r = await client.post(endpoint, json=rda_json, headers=headers)
|
||||
raw_resp = r.text
|
||||
try:
|
||||
data = r.json()
|
||||
ok_rda = bool(data.get("status") or (data.get("data") or {}).get("success", False))
|
||||
msg_tns = ((data.get("data") or {}).get("response") or data.get("message") or raw_resp[:300])
|
||||
except Exception:
|
||||
ok_rda = r.status_code < 300
|
||||
msg_tns = raw_resp[:300]
|
||||
except Exception as ex:
|
||||
msg_tns = str(ex)
|
||||
|
||||
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO envios (user_id, tipo, factura, idrecepcion, contrato,
|
||||
fecha_inicio, fecha_fin, pacientes_count, servicios_count,
|
||||
status, json_enviado, respuesta_api, mensaje_tns, created_at)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
""", (
|
||||
user["user_id"], "transaccion", fact,
|
||||
fecha_inicio, fecha_fin,
|
||||
1, len(grupo_rows),
|
||||
"success" if status_ok else "error",
|
||||
json_lib.dumps(trans_json, indent=2, ensure_ascii=False)[:5000],
|
||||
response_text,
|
||||
user["user_id"], "transaccion",
|
||||
str(grupo_rows[0].get("NUM_FACTURA", id_rec)),
|
||||
id_rec,
|
||||
str(grupo_rows[0].get("CODCONTRATO", "")),
|
||||
fecha_inicio, fecha_fin, 1, len(grupo_rows),
|
||||
"success" if ok_rda else "error",
|
||||
json_lib.dumps(rda_json, ensure_ascii=False)[:5000],
|
||||
raw_resp[:2000], msg_tns,
|
||||
datetime.now().isoformat(),
|
||||
))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
resultados.append({"idrecepcion": id_rec, "success": ok_rda, "msg": msg_tns})
|
||||
|
||||
ok_count = sum(1 for r in resultados if r["success"])
|
||||
err_count = len(resultados) - ok_count
|
||||
return JSONResponse({
|
||||
"success": total_errores == 0,
|
||||
"total_enviados": total_enviados,
|
||||
"total_errores": total_errores,
|
||||
"success": err_count == 0,
|
||||
"total_enviados": ok_count,
|
||||
"total_errores": err_count,
|
||||
"resultados": resultados,
|
||||
"message": f"Enviados: {total_enviados}, Errores: {total_errores}",
|
||||
})
|
||||
|
||||
+236
-95
@@ -2,131 +2,272 @@
|
||||
{% block title %}Historial{% endblock %}
|
||||
{% block header %}Historial de Envíos{% endblock %}
|
||||
{% block content %}
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<form class="flex flex-wrap items-end gap-4">
|
||||
|
||||
<!-- Stats rápidos -->
|
||||
{% if stats %}
|
||||
<div class="flex flex-wrap gap-3 mb-5">
|
||||
{% for s in stats %}
|
||||
<div class="bg-white rounded-lg border border-gray-200 px-4 py-2.5 shadow-sm flex items-center gap-3">
|
||||
<span class="px-2 py-0.5 rounded text-xs font-medium
|
||||
{% if s.tipo == 'transaccion' %}bg-purple-100 text-purple-700
|
||||
{% else %}bg-blue-100 text-blue-700{% endif %}">{{ s.tipo }}</span>
|
||||
<span class="{% if s.status == 'success' %}text-green-600{% else %}text-red-600{% endif %} font-bold text-sm">
|
||||
{{ s.cnt }}
|
||||
</span>
|
||||
<span class="text-gray-400 text-xs">{{ 'exitosos' if s.status == 'success' else 'errores' }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div class="bg-white rounded-lg border border-gray-200 px-4 py-2.5 shadow-sm flex items-center gap-2 text-gray-500 text-sm">
|
||||
<i class="fas fa-database text-gray-400 text-xs"></i> {{ total }} total
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Filtros -->
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200 mb-5">
|
||||
<form method="get" action="/logs" class="p-4">
|
||||
<input type="hidden" name="offset" value="0">
|
||||
<div class="flex flex-wrap items-end gap-3">
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">Tipo</label>
|
||||
<select name="tipo" class="px-3 py-1.5 border border-gray-300 rounded-lg text-sm">
|
||||
<option value="">Todos</option>
|
||||
<label class="block text-xs font-medium text-gray-500 mb-1">Tipo</label>
|
||||
<select name="tipo" class="px-3 py-1.5 border border-gray-300 rounded-lg text-sm min-w-[130px]">
|
||||
<option value="">Todos los tipos</option>
|
||||
<option value="terceros" {{ 'selected' if filtro_tipo == 'terceros' }}>Terceros</option>
|
||||
<option value="transaccion" {{ 'selected' if filtro_tipo == 'transaccion' }}>Transacción</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">Estado</label>
|
||||
<select name="status" class="px-3 py-1.5 border border-gray-300 rounded-lg text-sm">
|
||||
<label class="block text-xs font-medium text-gray-500 mb-1">Estado</label>
|
||||
<select name="status" class="px-3 py-1.5 border border-gray-300 rounded-lg text-sm min-w-[120px]">
|
||||
<option value="">Todos</option>
|
||||
<option value="success" {{ 'selected' if filtro_status == 'success' }}>Exitoso</option>
|
||||
<option value="error" {{ 'selected' if filtro_status == 'error' }}>Error</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">Factura</label>
|
||||
<input type="text" name="factura" value="{{ filtro_factura }}"
|
||||
class="px-3 py-1.5 border border-gray-300 rounded-lg text-sm" placeholder="Buscar factura...">
|
||||
<label class="block text-xs font-medium text-gray-500 mb-1">Desde</label>
|
||||
<input type="date" name="fecha_desde" value="{{ filtro_fecha_desde }}"
|
||||
class="px-3 py-1.5 border border-gray-300 rounded-lg text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-500 mb-1">Hasta</label>
|
||||
<input type="date" name="fecha_hasta" value="{{ filtro_fecha_hasta }}"
|
||||
class="px-3 py-1.5 border border-gray-300 rounded-lg text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-500 mb-1">Factura</label>
|
||||
<input type="text" name="factura" value="{{ filtro_factura }}" placeholder="Nro. factura"
|
||||
class="px-3 py-1.5 border border-gray-300 rounded-lg text-sm w-28">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-500 mb-1">IDRECEP / Contrato</label>
|
||||
<input type="text" name="paciente" value="{{ filtro_paciente }}" placeholder="ID o contrato"
|
||||
class="px-3 py-1.5 border border-gray-300 rounded-lg text-sm w-36">
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button type="submit"
|
||||
class="px-4 py-1.5 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700">
|
||||
<i class="fas fa-search mr-1"></i> Filtrar
|
||||
</button>
|
||||
<a href="/logs" class="px-3 py-1.5 bg-gray-100 text-gray-600 rounded-lg text-sm hover:bg-gray-200" title="Limpiar filtros">
|
||||
<i class="fas fa-times"></i>
|
||||
</a>
|
||||
</div>
|
||||
<button type="submit" class="px-4 py-1.5 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700">
|
||||
<i class="fas fa-search mr-1"></i> Filtrar
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
{% if envios %}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="text-left text-gray-500 border-b border-gray-200">
|
||||
<th class="pb-3 font-medium">ID</th>
|
||||
<th class="pb-3 font-medium">Tipo</th>
|
||||
<th class="pb-3 font-medium">Factura</th>
|
||||
<th class="pb-3 font-medium">Estado</th>
|
||||
<th class="pb-3 font-medium">Pacientes</th>
|
||||
<th class="pb-3 font-medium">Servicios</th>
|
||||
<th class="pb-3 font-medium">Respuesta API</th>
|
||||
<th class="pb-3 font-medium">Fecha</th>
|
||||
<th class="pb-3 font-medium">Usuario</th>
|
||||
<th class="pb-3 font-medium"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for e in envios %}
|
||||
<tr class="border-b border-gray-50 hover:bg-gray-50">
|
||||
<td class="py-3 text-gray-600">{{ e.id }}</td>
|
||||
<td class="py-3">
|
||||
<span class="px-2 py-1 rounded text-xs font-medium {% if e.tipo == 'terceros' %}bg-blue-100 text-blue-700{% else %}bg-purple-100 text-purple-700{% endif %}">
|
||||
{{ e.tipo }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="py-3 text-gray-600 font-medium">{{ e.factura or '-' }}</td>
|
||||
<td class="py-3">
|
||||
<span class="px-2 py-1 rounded text-xs font-medium {% if e.status == 'success' %}bg-green-100 text-green-700{% else %}bg-red-100 text-red-700{% endif %}">
|
||||
{{ 'Exitoso' if e.status == 'success' else 'Error' }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="py-3 text-gray-600">{{ e.pacientes_count or 0 }}</td>
|
||||
<td class="py-3 text-gray-600">{{ e.servicios_count or 0 }}</td>
|
||||
<td class="py-3 text-gray-500 text-xs max-w-xs truncate">{{ e.respuesta_api[:80] if e.respuesta_api else '-' }}</td>
|
||||
<td class="py-3 text-gray-500 text-xs">{{ e.created_at[:19] }}</td>
|
||||
<td class="py-3 text-gray-500">{{ e.username }}</td>
|
||||
<td class="py-3">
|
||||
<button onclick='showDetail({{ e.id|tojson }}, {{ e.json_enviado|tojson if e.json_enviado else "null" }})'
|
||||
class="text-blue-600 hover:text-blue-800">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center py-12 text-gray-400">
|
||||
<i class="fas fa-inbox text-5xl mb-4 block"></i>
|
||||
<p>No hay registros con los filtros seleccionados</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Modal JSON -->
|
||||
<div id="json-modal" class="fixed inset-0 z-50 hidden">
|
||||
<div class="absolute inset-0 bg-black/50" onclick="closeModal()"></div>
|
||||
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full max-w-2xl bg-white rounded-xl shadow-2xl max-h-[80vh] overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-gray-200 flex justify-between items-center">
|
||||
<h3 class="font-semibold text-gray-800"><i class="fas fa-code mr-2 text-blue-500"></i>JSON Enviado</h3>
|
||||
<button onclick="closeModal()" class="text-gray-400 hover:text-gray-600"><i class="fas fa-times"></i></button>
|
||||
<!-- Tabla -->
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200">
|
||||
<div class="px-6 py-3 border-b border-gray-100 flex items-center justify-between">
|
||||
<span class="text-xs text-gray-500">
|
||||
{% if total %}
|
||||
Mostrando {{ offset + 1 }}–{{ [offset + page_size, total]|min }} de {{ total }}
|
||||
{% else %}Sin resultados{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{% if envios %}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-xs">
|
||||
<thead>
|
||||
<tr class="text-left text-gray-400 border-b border-gray-200 bg-gray-50/60">
|
||||
<th class="px-4 py-3 font-medium">#</th>
|
||||
<th class="px-2 py-3 font-medium">Tipo</th>
|
||||
<th class="px-2 py-3 font-medium">Factura</th>
|
||||
<th class="px-2 py-3 font-medium">IDRECEP.</th>
|
||||
<th class="px-2 py-3 font-medium">Contrato</th>
|
||||
<th class="px-2 py-3 font-medium">Estado</th>
|
||||
<th class="px-2 py-3 font-medium">Mensaje TNS</th>
|
||||
<th class="px-2 py-3 font-medium">Fecha</th>
|
||||
<th class="px-2 py-3 font-medium">Usuario</th>
|
||||
<th class="px-2 py-3"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for e in envios %}
|
||||
<tr class="border-b border-gray-50 hover:bg-gray-50 transition-colors
|
||||
{% if e.status == 'success' %}bg-green-50/25{% elif e.status == 'error' %}bg-red-50/25{% endif %}">
|
||||
<td class="px-4 py-2.5 text-gray-400 font-mono">{{ e.id }}</td>
|
||||
<td class="px-2 py-2.5">
|
||||
<span class="px-2 py-0.5 rounded text-xs font-medium
|
||||
{% if e.tipo == 'terceros' %}bg-blue-100 text-blue-700
|
||||
{% else %}bg-purple-100 text-purple-700{% endif %}">
|
||||
{{ e.tipo }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-2 py-2.5 text-gray-600 font-medium">{{ e.factura or '-' }}</td>
|
||||
<td class="px-2 py-2.5 text-gray-500 font-mono">{{ e.idrecepcion or '-' }}</td>
|
||||
<td class="px-2 py-2.5 text-gray-500">{{ e.contrato or '-' }}</td>
|
||||
<td class="px-2 py-2.5">
|
||||
<span class="px-2 py-0.5 rounded text-xs font-medium
|
||||
{% if e.status == 'success' %}bg-green-100 text-green-700
|
||||
{% else %}bg-red-100 text-red-700{% endif %}">
|
||||
{{ '✓ OK' if e.status == 'success' else '✗ Error' }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-2 py-2.5 text-gray-500">
|
||||
{% if e.mensaje_tns %}
|
||||
<span class="block max-w-[180px] truncate" title="{{ e.mensaje_tns }}">
|
||||
{{ e.mensaje_tns[:55] }}{% if e.mensaje_tns|length > 55 %}…{% endif %}
|
||||
</span>
|
||||
{% else %}<span class="text-gray-300">—</span>{% endif %}
|
||||
</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">
|
||||
<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">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Paginación -->
|
||||
<div class="px-6 py-4 border-t border-gray-100 flex items-center justify-between">
|
||||
<div class="flex gap-2">
|
||||
{% if offset > 0 %}
|
||||
<a href="/logs?tipo={{ filtro_tipo }}&status={{ filtro_status }}&factura={{ filtro_factura }}&paciente={{ filtro_paciente }}&fecha_desde={{ filtro_fecha_desde }}&fecha_hasta={{ filtro_fecha_hasta }}&offset={{ [0, offset - page_size]|max }}"
|
||||
class="px-3 py-1.5 bg-gray-100 hover:bg-gray-200 text-gray-600 rounded-lg text-sm">
|
||||
<i class="fas fa-chevron-left mr-1"></i> Anterior
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if has_more %}
|
||||
<a href="/logs?tipo={{ filtro_tipo }}&status={{ filtro_status }}&factura={{ filtro_factura }}&paciente={{ filtro_paciente }}&fecha_desde={{ filtro_fecha_desde }}&fecha_hasta={{ filtro_fecha_hasta }}&offset={{ offset + page_size }}"
|
||||
class="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm">
|
||||
Siguiente <i class="fas fa-chevron-right ml-1"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="p-6 overflow-y-auto max-h-[calc(80vh-80px)]">
|
||||
<pre id="modal-json" class="text-xs font-mono bg-gray-50 rounded-lg p-4 overflow-x-auto"></pre>
|
||||
<span class="text-xs text-gray-400">Pág. {{ (offset // page_size) + 1 }}</span>
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
<div class="text-center py-16 text-gray-400">
|
||||
<i class="fas fa-inbox text-5xl mb-4 block opacity-30"></i>
|
||||
<p class="text-sm">Sin registros con los filtros seleccionados</p>
|
||||
<a href="/logs" class="mt-3 inline-block text-blue-600 hover:underline text-sm">Quitar filtros</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Modal detalle -->
|
||||
<div id="modal-detalle" class="fixed inset-0 z-50 hidden">
|
||||
<div class="absolute inset-0 bg-black/60" onclick="closeModal()"></div>
|
||||
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full max-w-3xl bg-white rounded-xl shadow-2xl max-h-[88vh] overflow-hidden flex flex-col">
|
||||
<div class="px-6 py-4 border-b border-gray-200 flex justify-between items-center shrink-0">
|
||||
<h3 id="modal-titulo" class="font-semibold text-gray-800 text-sm"></h3>
|
||||
<button onclick="closeModal()" class="text-gray-400 hover:text-gray-600 text-xl leading-none">×</button>
|
||||
</div>
|
||||
<div id="modal-info" class="px-6 py-2.5 bg-gray-50 border-b border-gray-200 shrink-0 text-xs text-gray-500 flex flex-wrap gap-4"></div>
|
||||
<div class="flex border-b border-gray-200 shrink-0 px-4">
|
||||
<button onclick="showTab('t-json')" id="btn-t-json"
|
||||
class="tab-btn px-4 py-2 text-sm font-medium border-b-2 border-blue-500 text-blue-600 -mb-px">
|
||||
JSON enviado</button>
|
||||
<button onclick="showTab('t-tns')" id="btn-t-tns"
|
||||
class="tab-btn px-4 py-2 text-sm font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700 -mb-px">
|
||||
Respuesta TNS</button>
|
||||
<button onclick="showTab('t-msg')" id="btn-t-msg"
|
||||
class="tab-btn px-4 py-2 text-sm font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700 -mb-px">
|
||||
Mensaje</button>
|
||||
</div>
|
||||
<div class="overflow-y-auto flex-1 p-5">
|
||||
<pre id="t-json" class="tab-pane text-xs font-mono bg-gray-50 rounded-lg p-4 overflow-x-auto whitespace-pre-wrap"></pre>
|
||||
<pre id="t-tns" class="tab-pane hidden text-xs font-mono bg-yellow-50 rounded-lg p-4 overflow-x-auto whitespace-pre-wrap text-yellow-900"></pre>
|
||||
<pre id="t-msg" class="tab-pane hidden text-xs font-mono bg-blue-50 rounded-lg p-4 overflow-x-auto whitespace-pre-wrap text-blue-900"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function showDetail(id, json) {
|
||||
if (!json) { showToast('No hay JSON disponible', 'info'); return; }
|
||||
const pre = document.getElementById('modal-json');
|
||||
try {
|
||||
const obj = typeof json === 'string' ? JSON.parse(json) : json;
|
||||
pre.innerHTML = syntaxHighlight(obj);
|
||||
} catch(e) {
|
||||
pre.textContent = json;
|
||||
async function verDetalle(id) {
|
||||
const resp = await fetch(`/logs/detalle/${id}`, {credentials: 'include'});
|
||||
const data = await resp.json();
|
||||
if (data.error) { showToast(data.error, 'error'); return; }
|
||||
|
||||
const estadoLabel = data.status === 'success' ? '✓ Exitoso' : '✗ Error';
|
||||
document.getElementById('modal-titulo').innerHTML =
|
||||
`<i class="fas fa-file-alt mr-2 text-blue-500"></i>Envío #${data.id} — <b>${data.tipo}</b> — <span class="${data.status === 'success' ? 'text-green-600' : 'text-red-600'}">${estadoLabel}</span>`;
|
||||
|
||||
const info = [
|
||||
data.factura ? `<span><b>Factura:</b> ${data.factura}</span>` : '',
|
||||
data.idrecepcion ? `<span><b>IDRECEPCION:</b> ${data.idrecepcion}</span>` : '',
|
||||
data.contrato ? `<span><b>Contrato:</b> ${data.contrato}</span>` : '',
|
||||
(data.fecha_inicio && data.fecha_fin) ? `<span><b>Período:</b> ${data.fecha_inicio} → ${data.fecha_fin}</span>` : '',
|
||||
data.username ? `<span><b>Usuario:</b> ${data.username}</span>` : '',
|
||||
data.created_at ? `<span><b>Enviado:</b> ${data.created_at.slice(0, 16)}</span>` : '',
|
||||
].filter(Boolean).join('');
|
||||
document.getElementById('modal-info').innerHTML = info;
|
||||
|
||||
const jsonPre = document.getElementById('t-json');
|
||||
if (data.json_enviado) {
|
||||
try { jsonPre.innerHTML = syntaxHighlight(JSON.parse(data.json_enviado)); }
|
||||
catch(e) { jsonPre.textContent = data.json_enviado; }
|
||||
} else {
|
||||
jsonPre.textContent = '(sin JSON guardado)';
|
||||
}
|
||||
|
||||
const tnsPre = document.getElementById('t-tns');
|
||||
const raw = data.respuesta_api || '(sin respuesta)';
|
||||
try { tnsPre.textContent = JSON.stringify(JSON.parse(raw), null, 2); }
|
||||
catch(e) { tnsPre.textContent = raw; }
|
||||
|
||||
document.getElementById('t-msg').textContent = data.mensaje_tns || '(sin mensaje)';
|
||||
|
||||
showTab('t-json');
|
||||
document.getElementById('modal-detalle').classList.remove('hidden');
|
||||
}
|
||||
|
||||
function showTab(tabId) {
|
||||
document.querySelectorAll('.tab-pane').forEach(p => p.classList.add('hidden'));
|
||||
document.querySelectorAll('.tab-btn').forEach(b => {
|
||||
b.classList.remove('border-blue-500','text-blue-600');
|
||||
b.classList.add('border-transparent','text-gray-500');
|
||||
});
|
||||
document.getElementById(tabId).classList.remove('hidden');
|
||||
const btn = document.getElementById('btn-' + tabId);
|
||||
if (btn) {
|
||||
btn.classList.add('border-blue-500','text-blue-600');
|
||||
btn.classList.remove('border-transparent','text-gray-500');
|
||||
}
|
||||
document.getElementById('json-modal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
document.getElementById('json-modal').classList.add('hidden');
|
||||
document.getElementById('modal-detalle').classList.add('hidden');
|
||||
}
|
||||
|
||||
function syntaxHighlight(obj) {
|
||||
let json = JSON.stringify(obj, null, 2);
|
||||
json = json.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
return json.replace(/("(?:[^"\\]|\\.)*")(?=\s*:)/g, '<span class="text-blue-600">$1</span>')
|
||||
.replace(/:(\s*)("(?:[^"\\]|\\.)*")/g, ': $1<span class="text-green-600">$2</span>')
|
||||
.replace(/:(\s*)(\d+)/g, ': $1<span class="text-orange-600">$2</span>')
|
||||
.replace(/:(\s*)(null|true|false)/g, ': $1<span class="text-purple-600">$2</span>');
|
||||
json = json.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
||||
return json
|
||||
.replace(/("(?:[^"\\]|\\.)*")(?=\s*:)/g,'<span class="text-blue-600">$1</span>')
|
||||
.replace(/:(\s*)("(?:[^"\\]|\\.)*")/g,': $1<span class="text-green-700">$2</span>')
|
||||
.replace(/:(\s*)(\d+(?:\.\d+)?)/g,': $1<span class="text-orange-600">$2</span>')
|
||||
.replace(/:(\s*)(null|true|false)/g,': $1<span class="text-purple-600">$2</span>');
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
+316
-103
@@ -2,147 +2,360 @@
|
||||
{% block title %}Transacción RIPS{% endblock %}
|
||||
{% block header %}Envío de Transacción RIPS{% endblock %}
|
||||
{% block content %}
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
|
||||
<!-- Formulario -->
|
||||
<div class="lg:col-span-1 space-y-4">
|
||||
<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-cog mr-2 text-blue-500"></i>Generar y Enviar</h3>
|
||||
<h3 class="font-semibold text-gray-800"><i class="fas fa-cog mr-2 text-blue-500"></i>Parámetros</h3>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<form id="form-transaccion" class="space-y-4">
|
||||
<div class="p-5 space-y-4">
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">Consulta SQL</label>
|
||||
<select id="f-query" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm">
|
||||
<option value="">Seleccionar consulta...</option>
|
||||
{% for q in queries %}
|
||||
<option value="{{ q.id }}">{{ q.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">Factura <span class="text-gray-400">(opcional)</span></label>
|
||||
<input id="f-factura" type="text" placeholder="Ej: LHXC03404"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm">
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Consulta SQL</label>
|
||||
<select name="query_id" required
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm">
|
||||
<option value="">Seleccionar consulta...</option>
|
||||
{% for q in queries %}
|
||||
<option value="{{ q.id }}">{{ q.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">Fecha inicio</label>
|
||||
<input id="f-fi" type="date" 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="Ej: LHXC03404">
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">Fecha fin</label>
|
||||
<input id="f-ff" type="date" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm">
|
||||
</div>
|
||||
<div class="grid grid-cols-2 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>
|
||||
<div class="flex space-x-3">
|
||||
<button type="button" onclick="previewTransaccion()"
|
||||
class="px-4 py-2 bg-gray-100 text-gray-700 rounded-lg text-sm font-medium hover:bg-gray-200">
|
||||
<i class="fas fa-eye mr-1"></i> Vista Previa
|
||||
</button>
|
||||
<button type="button" onclick="sendTransaccion()"
|
||||
class="px-4 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-lg text-sm font-medium">
|
||||
<i class="fas fa-paper-plane mr-1"></i> Enviar a API
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<button onclick="cargarRegistros()" id="btn-cargar"
|
||||
class="w-full px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm font-medium">
|
||||
<i class="fas fa-search mr-1"></i> Cargar registros
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 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-eye mr-2 text-blue-500"></i>JSON Generado</h3>
|
||||
<!-- Resumen -->
|
||||
<div id="resumen-box" class="hidden bg-white rounded-xl shadow-sm border border-gray-200 p-5">
|
||||
<div class="grid grid-cols-3 gap-2 text-center mb-4">
|
||||
<div class="bg-gray-50 rounded-lg p-3">
|
||||
<div id="res-total" class="text-xl font-bold text-gray-700">0</div>
|
||||
<div class="text-xs text-gray-500">Total</div>
|
||||
</div>
|
||||
<div class="bg-green-50 rounded-lg p-3">
|
||||
<div id="res-ok" class="text-xl font-bold text-green-600">0</div>
|
||||
<div class="text-xs text-gray-500">Enviados</div>
|
||||
</div>
|
||||
<div class="bg-yellow-50 rounded-lg p-3">
|
||||
<div id="res-pend" class="text-xl font-bold text-yellow-600">0</div>
|
||||
<div class="text-xs text-gray-500">Pendientes</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div id="preview-info" class="text-xs text-gray-500 mb-2"></div>
|
||||
<pre id="json-preview" class="text-xs font-mono bg-gray-50 rounded-lg p-4 overflow-x-auto max-h-96 text-gray-600"><i class="fas fa-info-circle mr-1"></i> Haz clic en "Vista Previa" para ver el JSON</pre>
|
||||
<div id="res-err-wrap" class="hidden mb-3">
|
||||
<div class="bg-red-50 rounded-lg p-3 text-center">
|
||||
<div id="res-err" class="text-xl font-bold text-red-600">0</div>
|
||||
<div class="text-xs text-gray-500">Con error</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button onclick="enviarTodosPendientes()" id="btn-enviar-pend"
|
||||
class="flex-1 px-3 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-lg text-xs font-medium">
|
||||
<i class="fas fa-paper-plane mr-1"></i> Enviar pendientes
|
||||
</button>
|
||||
<button onclick="cargarRegistros()"
|
||||
class="px-3 py-2 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg text-xs" title="Actualizar">
|
||||
<i class="fas fa-sync"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<!-- Tabla -->
|
||||
<div class="lg:col-span-2">
|
||||
<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-history mr-2 text-blue-500"></i>Últimos Envíos</h3>
|
||||
<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-list mr-2 text-blue-500"></i>Registros</h3>
|
||||
<span id="tabla-count" class="text-xs text-gray-400"></span>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
{% if envios %}
|
||||
<div class="space-y-2">
|
||||
{% for e in envios %}
|
||||
<div class="p-3 rounded-lg border border-gray-100 text-sm">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="font-medium text-gray-700 text-xs">{{ e.factura or 'N/A' }}</span>
|
||||
<span class="px-2 py-0.5 rounded text-xs font-medium {% if e.status == 'success' %}bg-green-100 text-green-700{% else %}bg-red-100 text-red-700{% endif %}">
|
||||
{{ 'Exitoso' if e.status == 'success' else 'Error' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-xs text-gray-400 mt-1">
|
||||
<span>{{ e.pacientes_count or 0 }} pacientes / {{ e.servicios_count or 0 }} servicios</span>
|
||||
<span>{{ e.created_at[:19] }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div id="tabla-wrap" class="p-6">
|
||||
<div class="text-center py-12 text-gray-400 text-sm">
|
||||
<i class="fas fa-table text-4xl block mb-3 opacity-30"></i>
|
||||
Carga los registros para ver el estado de envío
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-center text-gray-400 py-4 text-sm">No hay envíos de transacciones aún</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal JSON / detalle -->
|
||||
<div id="modal-detalle" class="fixed inset-0 z-50 hidden">
|
||||
<div class="absolute inset-0 bg-black/60" onclick="closeModal()"></div>
|
||||
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full max-w-3xl bg-white rounded-xl shadow-2xl max-h-[85vh] overflow-hidden flex flex-col">
|
||||
<div class="px-6 py-4 border-b border-gray-200 flex justify-between items-center shrink-0">
|
||||
<h3 id="modal-titulo" class="font-semibold text-gray-800"><i class="fas fa-code mr-2 text-blue-500"></i>Detalle</h3>
|
||||
<button onclick="closeModal()" class="text-gray-400 hover:text-gray-600"><i class="fas fa-times"></i></button>
|
||||
</div>
|
||||
<!-- Tabs -->
|
||||
<div class="flex border-b border-gray-200 shrink-0 px-4">
|
||||
<button onclick="showTab('tab-json')" id="t-json" class="tab-btn px-4 py-2 text-sm font-medium border-b-2 border-blue-500 text-blue-600 -mb-px">JSON enviado</button>
|
||||
<button onclick="showTab('tab-tns')" id="t-tns" class="tab-btn px-4 py-2 text-sm font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700 -mb-px">Respuesta TNS</button>
|
||||
</div>
|
||||
<div class="overflow-y-auto flex-1 p-5">
|
||||
<pre id="tab-json" class="tab-pane text-xs font-mono bg-gray-50 rounded-lg p-4 overflow-x-auto whitespace-pre-wrap"></pre>
|
||||
<pre id="tab-tns" class="tab-pane hidden text-xs font-mono bg-yellow-50 rounded-lg p-4 overflow-x-auto whitespace-pre-wrap text-yellow-900"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function previewTransaccion() {
|
||||
const form = document.getElementById('form-transaccion');
|
||||
const data = new FormData(form);
|
||||
const btn = form.querySelector('[onclick="previewTransaccion()"]');
|
||||
showLoading(btn);
|
||||
let allItems = [];
|
||||
let formParams = {};
|
||||
|
||||
const resp = await fetch('/transaccion/preview', {method:'POST', body: data, credentials: 'include'});
|
||||
const result = await resp.json();
|
||||
hideLoading(btn, '<i class="fas fa-eye mr-1"></i> Vista Previa');
|
||||
// Set today as default end date, start of month as default start
|
||||
(function() {
|
||||
const today = new Date();
|
||||
const ymd = d => d.toISOString().slice(0, 10);
|
||||
document.getElementById('f-ff').value = ymd(today);
|
||||
const startOfMonth = new Date(today.getFullYear(), today.getMonth(), 1);
|
||||
document.getElementById('f-fi').value = ymd(startOfMonth);
|
||||
})();
|
||||
|
||||
document.getElementById('preview-info').innerHTML = result.success
|
||||
? `<span class="text-green-600"><i class="fas fa-check-circle"></i> ${result.rows_count} registros, ${result.grupos_count} grupos, ${result.total_json} JSON(s)</span>`
|
||||
: `<span class="text-red-600">Error: ${result.message}</span>`;
|
||||
|
||||
const pre = document.getElementById('json-preview');
|
||||
if (result.success && result.generated_json) {
|
||||
pre.innerHTML = syntaxHighlight(result.generated_json);
|
||||
} else {
|
||||
pre.innerHTML = '<span class="text-red-600">Error: ' + (result.message || 'Sin datos') + '</span>';
|
||||
}
|
||||
function getParams() {
|
||||
return {
|
||||
query_id: document.getElementById('f-query').value,
|
||||
factura: document.getElementById('f-factura').value,
|
||||
fecha_inicio: document.getElementById('f-fi').value,
|
||||
fecha_fin: document.getElementById('f-ff').value,
|
||||
};
|
||||
}
|
||||
|
||||
async function sendTransaccion() {
|
||||
if (!confirm('¿Enviar transacción(es) RIPS a la API?')) return;
|
||||
const form = document.getElementById('form-transaccion');
|
||||
const data = new FormData(form);
|
||||
const btn = form.querySelector('[onclick="sendTransaccion()"]');
|
||||
showLoading(btn);
|
||||
async function cargarRegistros() {
|
||||
formParams = getParams();
|
||||
if (!formParams.query_id) { showToast('Selecciona una consulta SQL', 'warning'); return; }
|
||||
if (!formParams.fecha_inicio || !formParams.fecha_fin) { showToast('Ingresa las fechas', 'warning'); return; }
|
||||
|
||||
const resp = await fetch('/transaccion/send', {method:'POST', body: data, credentials: 'include'});
|
||||
document.getElementById('tabla-wrap').innerHTML = `
|
||||
<div class="text-center py-12 text-gray-400 text-sm">
|
||||
<i class="fas fa-spinner fa-spin text-3xl block mb-3"></i>Consultando Firebird...
|
||||
</div>`;
|
||||
document.getElementById('resumen-box').classList.add('hidden');
|
||||
|
||||
const resp = await fetch('/transaccion/preview', {
|
||||
method: 'POST', credentials: 'include',
|
||||
body: new URLSearchParams(formParams),
|
||||
});
|
||||
const result = await resp.json();
|
||||
hideLoading(btn, '<i class="fas fa-paper-plane mr-1"></i> Enviar a API');
|
||||
|
||||
if (!result.success) {
|
||||
document.getElementById('tabla-wrap').innerHTML = `
|
||||
<div class="text-center py-10 text-red-500 text-sm">
|
||||
<i class="fas fa-exclamation-circle text-3xl block mb-2"></i>
|
||||
<strong>Error:</strong> ${escHtml(result.message)}
|
||||
</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
allItems = result.items;
|
||||
renderTabla();
|
||||
actualizarResumen();
|
||||
document.getElementById('resumen-box').classList.remove('hidden');
|
||||
}
|
||||
|
||||
function renderTabla() {
|
||||
if (!allItems.length) {
|
||||
document.getElementById('tabla-wrap').innerHTML = '<div class="text-center py-10 text-gray-400 text-sm">Sin registros para los parámetros indicados</div>';
|
||||
return;
|
||||
}
|
||||
document.getElementById('tabla-count').textContent = `${allItems.length} registros`;
|
||||
|
||||
let html = `<div class="overflow-x-auto">
|
||||
<table class="w-full text-xs">
|
||||
<thead><tr class="text-left text-gray-400 border-b border-gray-200">
|
||||
<th class="pb-2 font-medium pr-3">IDRECEP.</th>
|
||||
<th class="pb-2 font-medium pr-3">Factura</th>
|
||||
<th class="pb-2 font-medium pr-3">Paciente</th>
|
||||
<th class="pb-2 font-medium pr-3">Contrato</th>
|
||||
<th class="pb-2 font-medium pr-3">Exámenes</th>
|
||||
<th class="pb-2 font-medium pr-3">Estado</th>
|
||||
<th class="pb-2 font-medium"></th>
|
||||
</tr></thead>
|
||||
<tbody id="tbody-main">`;
|
||||
|
||||
for (const item of allItems) {
|
||||
html += filaHtml(item);
|
||||
}
|
||||
html += '</tbody></table></div>';
|
||||
document.getElementById('tabla-wrap').innerHTML = html;
|
||||
}
|
||||
|
||||
function filaHtml(item) {
|
||||
const id = item.idrecepcion;
|
||||
const env = item.enviado;
|
||||
const examsStr = Array.isArray(item.examenes) ? item.examenes.join(', ') : (item.examenes || '');
|
||||
|
||||
let badgeHtml, rowCls, accionHtml;
|
||||
if (!env) {
|
||||
badgeHtml = `<span class="px-2 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-700">Pendiente</span>`;
|
||||
rowCls = '';
|
||||
accionHtml = `<button onclick="enviarUno(${id})" title="Enviar" class="px-2 py-1 bg-purple-600 hover:bg-purple-700 text-white rounded text-xs"><i class="fas fa-paper-plane"></i></button>`;
|
||||
} else if (env.status === 'success') {
|
||||
const at = (env.at || '').slice(0, 16).replace('T', ' ');
|
||||
badgeHtml = `<span class="px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-700" title="Enviado ${at}">✓ Enviado</span>`;
|
||||
rowCls = 'bg-green-50/50 opacity-80';
|
||||
accionHtml = '';
|
||||
} else {
|
||||
const tip = escAttr(env.mensaje || 'Error');
|
||||
badgeHtml = `<span class="px-2 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-700" title="${tip}">✗ Error</span>`;
|
||||
rowCls = 'bg-red-50/50';
|
||||
accionHtml = `<button onclick="enviarUno(${id})" title="Reintentar" class="px-2 py-1 bg-orange-500 hover:bg-orange-600 text-white rounded text-xs"><i class="fas fa-redo"></i></button>`;
|
||||
}
|
||||
|
||||
return `<tr id="row-${id}" class="border-b border-gray-50 hover:bg-gray-50/80 transition-colors ${rowCls}">
|
||||
<td class="py-2 pr-3 font-mono text-gray-600">${id}</td>
|
||||
<td class="py-2 pr-3 text-gray-600">${escHtml(String(item.factura || '-'))}</td>
|
||||
<td class="py-2 pr-3 text-gray-600">${escHtml(String(item.paciente || '-'))}</td>
|
||||
<td class="py-2 pr-3 text-gray-500">${escHtml(String(item.contrato || '-'))}</td>
|
||||
<td class="py-2 pr-3 text-gray-500 max-w-[200px] truncate" title="${escAttr(examsStr)}">${escHtml(examsStr)}</td>
|
||||
<td id="estado-${id}" class="py-2 pr-3">${badgeHtml}</td>
|
||||
<td class="py-2 flex gap-1 items-center">
|
||||
<button onclick="verDetalle(${id})" title="Ver JSON" class="px-2 py-1 bg-gray-100 hover:bg-gray-200 text-gray-600 rounded text-xs"><i class="fas fa-code"></i></button>
|
||||
<span id="btnenv-${id}">${accionHtml}</span>
|
||||
</td>
|
||||
</tr>`;
|
||||
}
|
||||
|
||||
function actualizarResumen() {
|
||||
const total = allItems.length;
|
||||
const ok = allItems.filter(i => i.enviado && i.enviado.status === 'success').length;
|
||||
const pend = allItems.filter(i => !i.enviado).length;
|
||||
const err = allItems.filter(i => i.enviado && i.enviado.status !== 'success').length;
|
||||
document.getElementById('res-total').textContent = total;
|
||||
document.getElementById('res-ok').textContent = ok;
|
||||
document.getElementById('res-pend').textContent = pend;
|
||||
document.getElementById('res-err').textContent = err;
|
||||
document.getElementById('res-err-wrap').classList.toggle('hidden', err === 0);
|
||||
}
|
||||
|
||||
function marcarFila(id, ok, mensaje, rawTns) {
|
||||
const idx = allItems.findIndex(i => i.idrecepcion === id);
|
||||
if (idx !== -1) {
|
||||
allItems[idx].enviado = { status: ok ? 'success' : 'error', mensaje, at: new Date().toISOString() };
|
||||
if (rawTns !== undefined) allItems[idx]._rawTns = rawTns;
|
||||
}
|
||||
const row = document.getElementById(`row-${id}`);
|
||||
const estadoCell = document.getElementById(`estado-${id}`);
|
||||
const btnWrap = document.getElementById(`btnenv-${id}`);
|
||||
if (!row || !estadoCell) return;
|
||||
|
||||
if (ok) {
|
||||
row.className = row.className.replace(/bg-red-50\/50|bg-green-50\/50/g, '') + ' bg-green-50/50 opacity-80';
|
||||
estadoCell.innerHTML = `<span class="px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-700">✓ Enviado</span>`;
|
||||
if (btnWrap) btnWrap.innerHTML = '';
|
||||
} else {
|
||||
row.className = row.className.replace(/bg-red-50\/50|bg-green-50\/50/g, '') + ' bg-red-50/50';
|
||||
const tip = escAttr(mensaje || 'Error');
|
||||
estadoCell.innerHTML = `<span class="px-2 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-700" title="${tip}">✗ Error</span>`;
|
||||
if (btnWrap) btnWrap.innerHTML = `<button onclick="enviarUno(${id})" title="Reintentar" class="px-2 py-1 bg-orange-500 hover:bg-orange-600 text-white rounded text-xs"><i class="fas fa-redo"></i></button>`;
|
||||
}
|
||||
actualizarResumen();
|
||||
}
|
||||
|
||||
async function enviarUno(id) {
|
||||
const btnWrap = document.getElementById(`btnenv-${id}`);
|
||||
if (btnWrap) btnWrap.innerHTML = '<span class="px-2 py-1 text-gray-400 text-xs"><i class="fas fa-spinner fa-spin"></i></span>';
|
||||
|
||||
const fd = new URLSearchParams({ ...formParams, idrecepcion: id });
|
||||
let result;
|
||||
try {
|
||||
const resp = await fetch('/transaccion/send-one', { method: 'POST', credentials: 'include', body: fd });
|
||||
result = await resp.json();
|
||||
} catch (e) {
|
||||
result = { success: false, message: String(e) };
|
||||
}
|
||||
|
||||
marcarFila(id, result.success, result.message, result.raw_tns);
|
||||
if (result.success) {
|
||||
showToast(`Envío exitoso! ${result.total_enviados} transacciones`, 'success');
|
||||
setTimeout(() => location.reload(), 1500);
|
||||
showToast(`IDRECEPCION ${id}: enviado`, 'success');
|
||||
} else {
|
||||
showToast(`Enviados: ${result.total_enviados}, Errores: ${result.total_errores}`, result.total_errores > 0 ? 'warning' : 'success');
|
||||
showToast(`IDRECEPCION ${id}: ${result.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function enviarTodosPendientes() {
|
||||
const pendientes = allItems.filter(i => !i.enviado);
|
||||
if (!pendientes.length) { showToast('No hay registros pendientes', 'info'); return; }
|
||||
if (!confirm(`¿Enviar ${pendientes.length} registros pendientes?`)) return;
|
||||
|
||||
document.getElementById('btn-enviar-pend').disabled = true;
|
||||
document.getElementById('btn-enviar-pend').innerHTML = '<i class="fas fa-spinner fa-spin mr-1"></i> Enviando...';
|
||||
|
||||
let ok = 0, err = 0;
|
||||
for (const item of pendientes) {
|
||||
await enviarUno(item.idrecepcion);
|
||||
if (allItems.find(i => i.idrecepcion === item.idrecepcion)?.enviado?.status === 'success') ok++; else err++;
|
||||
}
|
||||
|
||||
document.getElementById('btn-enviar-pend').disabled = false;
|
||||
document.getElementById('btn-enviar-pend').innerHTML = '<i class="fas fa-paper-plane mr-1"></i> Enviar pendientes';
|
||||
showToast(`Completado: ${ok} enviados, ${err} errores`, err === 0 ? 'success' : 'warning');
|
||||
}
|
||||
|
||||
function verDetalle(id) {
|
||||
const item = allItems.find(i => i.idrecepcion === id);
|
||||
if (!item) return;
|
||||
document.getElementById('modal-titulo').innerHTML = `<i class="fas fa-code mr-2 text-blue-500"></i>IDRECEPCION ${id} — ${item.paciente || ''} — Factura ${item.factura || '-'}`;
|
||||
const jsonPre = document.getElementById('tab-json');
|
||||
jsonPre.innerHTML = syntaxHighlight(item.json || {});
|
||||
const tnsPre = document.getElementById('tab-tns');
|
||||
if (item.enviado) {
|
||||
let tnsText = item._rawTns || item.enviado.mensaje || '(sin respuesta)';
|
||||
try { tnsText = JSON.stringify(JSON.parse(tnsText), null, 2); } catch(e) {}
|
||||
tnsPre.textContent = tnsText;
|
||||
const tab = document.getElementById('t-tns');
|
||||
tab.classList.add('text-yellow-700');
|
||||
tab.classList.remove('text-gray-500');
|
||||
} else {
|
||||
tnsPre.textContent = '(aún no enviado)';
|
||||
}
|
||||
showTab('tab-json');
|
||||
document.getElementById('modal-detalle').classList.remove('hidden');
|
||||
}
|
||||
|
||||
function showTab(tabId) {
|
||||
document.querySelectorAll('.tab-pane').forEach(p => p.classList.add('hidden'));
|
||||
document.querySelectorAll('.tab-btn').forEach(b => {
|
||||
b.classList.remove('border-blue-500', 'text-blue-600');
|
||||
b.classList.add('border-transparent', 'text-gray-500');
|
||||
});
|
||||
document.getElementById(tabId).classList.remove('hidden');
|
||||
const btnId = tabId === 'tab-json' ? 't-json' : 't-tns';
|
||||
const btn = document.getElementById(btnId);
|
||||
btn.classList.add('border-blue-500', 'text-blue-600');
|
||||
btn.classList.remove('border-transparent', 'text-gray-500');
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
document.getElementById('modal-detalle').classList.add('hidden');
|
||||
}
|
||||
|
||||
function escHtml(s) { return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
||||
function escAttr(s) { return String(s).replace(/"/g,'"').replace(/'/g,'''); }
|
||||
|
||||
function syntaxHighlight(obj) {
|
||||
let json = JSON.stringify(obj, null, 2);
|
||||
json = json.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
return json.replace(/("(?:[^"\\]|\\.)*")(?=\s*:)/g, '<span class="text-blue-600">$1</span>')
|
||||
.replace(/:(\s*)("(?:[^"\\]|\\.)*")/g, ': $1<span class="text-green-600">$2</span>')
|
||||
.replace(/:(\s*)(\d+)/g, ': $1<span class="text-orange-600">$2</span>')
|
||||
.replace(/:(\s*)(null|true|false)/g, ': $1<span class="text-purple-600">$2</span>');
|
||||
json = json.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
||||
return json
|
||||
.replace(/("(?:[^"\\]|\\.)*")(?=\s*:)/g,'<span class="text-blue-600">$1</span>')
|
||||
.replace(/:(\s*)("(?:[^"\\]|\\.)*")/g,': $1<span class="text-green-700">$2</span>')
|
||||
.replace(/:(\s*)(\d+(?:\.\d+)?)/g,': $1<span class="text-orange-600">$2</span>')
|
||||
.replace(/:(\s*)(null|true|false)/g,': $1<span class="text-purple-600">$2</span>');
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user