diff --git a/app/routes/automation.py b/app/routes/automation.py
index 84d7a85..4ee4c3d 100644
--- a/app/routes/automation.py
+++ b/app/routes/automation.py
@@ -177,6 +177,79 @@ WHERE r.FECHA_RECEPCION BETWEEN :fecha_ini AND :fecha_fin
ORDER BY r.IDRECEPCION
"""
+_SQL_RDA_BY_ID = """
+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, r.VALORDESC,
+ rel.COD_EXAMEN,
+ COALESCE(NULLIF(TRIM(ex.NUM_ISS), ''), TRIM(rel.COD_EXAMEN)) AS CUPS,
+ rel.PRECIO, COALESCE(t.VALOR, rel.PRECIO) AS PRECIO_TARIFA,
+ rel.FECHA_REPORTADO, m.COD_ESPECIALIDAD,
+ COALESCE(NULLIF(TRIM(m.CODIGO), ''), NULLIF(TRIM(r.USUARIO), ''), '') AS profesional,
+ e.CODCONTRATO, TRIM(m.DOCIDMEDICO) AS DOCIDMEDICO
+FROM RECEPCION r
+JOIN RELACION rel ON rel.IDRECEPCION = r.IDRECEPCION
+LEFT JOIN EXAMEN ex ON TRIM(ex.CODIGO) = TRIM(rel.COD_EXAMEN)
+LEFT JOIN MEDICO m ON m.CODIGO = r.COD_MEDICO
+LEFT JOIN EMPRESA e ON e.NIT = r.NIT_EMPRESA
+LEFT JOIN TARIFA t ON TRIM(t.COD_EXAMEN) = TRIM(rel.COD_EXAMEN) AND t.TARIFA = e.TARIFA
+WHERE r.IDRECEPCION = :idrecepcion
+ORDER BY rel.COD_EXAMEN
+"""
+
+_SQL_PRESERV_BY_ID = """
+SELECT
+ ps.ID_PS, ps.PS_PREFIJO, ps.PS_NUMERO,
+ r.IDRECEPCION, r.FECHA_RECEPCION, r.COD_PACIENTE, r.NIT_EMPRESA,
+ r.DIAG_PPAL, r.TIPOUSU, r.TIPOUSUSISPRO, r.AUTORIZACION,
+ r.CLASEPROC, r.HORAINICIORECEPCION, r.VALORTOTAL, r.VALORDESC,
+ COALESCE((SELECT FIRST 1 pg.VALOR FROM PAGOS pg
+ WHERE pg.NUMRECEP = r.IDRECEPCION AND pg.ESINICIAL = 'T'), 0) AS COPAGO,
+ rel.COD_EXAMEN,
+ COALESCE(NULLIF(TRIM(ex.NUM_ISS), ''), TRIM(rel.COD_EXAMEN)) AS CUPS,
+ rel.PRECIO, COALESCE(t.VALOR, rel.PRECIO) AS PRECIO_TARIFA,
+ rel.FECHA_REPORTADO, m.COD_ESPECIALIDAD,
+ COALESCE(NULLIF(TRIM(m.CODIGO), ''), NULLIF(TRIM(r.USUARIO), ''), '') AS profesional,
+ e.CODCONTRATO, TRIM(m.DOCIDMEDICO) AS DOCIDMEDICO
+FROM PRESSERV_DIAN ps
+JOIN RECEPCION r ON r.IDRECEPCION = ps.ID_RECEP
+JOIN RELACION rel ON rel.IDRECEPCION = r.IDRECEPCION
+LEFT JOIN EXAMEN ex ON TRIM(ex.CODIGO) = TRIM(rel.COD_EXAMEN)
+LEFT JOIN MEDICO m ON m.CODIGO = r.COD_MEDICO
+LEFT JOIN EMPRESA e ON e.NIT = r.NIT_EMPRESA
+LEFT JOIN TARIFA t ON TRIM(t.COD_EXAMEN) = TRIM(rel.COD_EXAMEN) AND t.TARIFA = e.TARIFA
+WHERE ps.ID_PS = :id_ps
+ AND (ps.PS_ANULADA IS NULL OR ps.PS_ANULADA = 'F')
+ORDER BY rel.COD_EXAMEN
+"""
+
+_SQL_VENTAS_BY_ID = """
+SELECT
+ r.IDRECEPCION, r.PREFIJO, r.NUM_FACTURA, r.FECHA_RECEPCION,
+ r.COD_PACIENTE, r.NIT_EMPRESA, r.VALORTOTAL, r.VALORDESC,
+ rel.COD_EXAMEN,
+ COALESCE(NULLIF(TRIM(ex.NUM_ISS), ''), TRIM(rel.COD_EXAMEN)) AS CUPS,
+ rel.PRECIO, COALESCE(t.VALOR, rel.PRECIO) AS PRECIO_TARIFA,
+ TRIM(e.CODCONTRATO) AS CODCONTRATO
+FROM RECEPCION r
+JOIN RELACION rel ON rel.IDRECEPCION = r.IDRECEPCION
+LEFT JOIN EXAMEN ex ON TRIM(ex.CODIGO) = TRIM(rel.COD_EXAMEN)
+LEFT JOIN EMPRESA e ON e.NIT = r.NIT_EMPRESA
+LEFT JOIN TARIFA t ON TRIM(t.COD_EXAMEN) = TRIM(rel.COD_EXAMEN) AND t.TARIFA = e.TARIFA
+WHERE r.IDRECEPCION = :idrecepcion
+"""
+
+_SQL_PAC_BY_CODIGO = """
+SELECT
+ 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
+LEFT JOIN CIUDAD c ON c.CODIGO = p.CIUDAD
+WHERE p.CODIGO = :codigo
+"""
+
def _agrupar_por_presserv(rows: list) -> dict:
"""Agrupa filas de pre-servicios por ID_PS."""
@@ -417,6 +490,7 @@ async def run_automation(
user: dict = Depends(get_current_user),
fecha: str = Form(...),
contrato: str = Form(""),
+ pasos: str = Form("terceros,rda,preserv,ventas"),
):
conn = get_connection()
configs = {row["key"]: row["value"] for row in conn.execute("SELECT * FROM config").fetchall()}
@@ -484,8 +558,10 @@ async def run_automation(
api_sucursal = configs.get("api_sucursal", "")
timeout = int(configs.get("api_timeout", 30))
+ pasos_set = {p.strip() for p in pasos.split(",")}
resultado = {
"fecha": fecha,
+ "pasos": list(pasos_set),
"paso1_terceros": {"enviados": 0, "errores": 0, "detalle": []},
"paso2_rda": {"enviados": 0, "errores": 0, "detalle": []},
"paso3_preserv": {"enviados": 0, "errores": 0, "detalle": []},
@@ -493,37 +569,39 @@ async def run_automation(
}
# ── PASO 1: Enviar Terceros (particulares + EPS) ─────────────────────────
- async with httpx.AsyncClient(timeout=timeout) as client:
- for row in rows_pac_todos:
- 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 "terceros" in pasos_set:
+ async with httpx.AsyncClient(timeout=timeout) as client:
+ for row in rows_pac_todos:
+ tercero_json = generar_tercero_api(row)
+ doc = tercero_json["nit"]
+ nombre = tercero_json["nombre"]
+ codigo_pac = str(row.get("CODIGO", ""))
+ 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
+ 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,
- })
+ resultado["paso1_terceros"]["detalle"].append({
+ "codigo": codigo_pac, "doc": doc, "nombre": nombre, "ok": ok, "msg": msg,
+ })
- _guardar_envio(user["user_id"], "terceros", fecha, tercero_json, msg, ok)
+ _guardar_envio(user["user_id"], "terceros", fecha, tercero_json, msg, ok)
# ── Sync paralelo a WhatsApp Lab (silencioso) ─────────────────────────────
wa_url = configs.get("whatsapp_url", "").rstrip("/")
wa_key = configs.get("whatsapp_api_key", "")
- if wa_url and wa_key and rows_pac:
+ if "terceros" in pasos_set and wa_url and wa_key and rows_pac:
ingest_url = f"{wa_url}/api/lab/ingest_paciente.php"
try:
wa_resultado = await sync_todos(rows_pac, ingest_url, wa_key, timeout, modo="upsert")
@@ -546,35 +624,37 @@ async def run_automation(
prefijo_def = configs.get("prefijo_tns_default", "00")
contrato_map = load_contrato_map()
- async with httpx.AsyncClient(timeout=timeout) as client:
- for id_recepcion, grupo_rows in grupos.items():
- num_fac = str(grupo_rows[0].get("NUM_FACTURA") or "").strip()
- num_override = num_fac
- rda_json = generar_rda_paciente(grupo_rows, prof_def, esp_def, remis_def, prefijo_def, num_override, contrato_map=contrato_map, sin_contrato_set=sin_contrato_set)
- factura = num_override or str(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 "rda" in pasos_set:
+ async with httpx.AsyncClient(timeout=timeout) as client:
+ for id_recepcion, grupo_rows in grupos.items():
+ num_fac = str(grupo_rows[0].get("NUM_FACTURA") or "").strip()
+ num_override = num_fac
+ rda_json = generar_rda_paciente(grupo_rows, prof_def, esp_def, remis_def, prefijo_def, num_override, contrato_map=contrato_map, sin_contrato_set=sin_contrato_set)
+ factura = num_override or str(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
+ 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,
- })
+ resultado["paso2_rda"]["detalle"].append({
+ "idrecepcion": id_recepcion,
+ "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))
+ _guardar_envio(user["user_id"], "transaccion", factura, rda_json, msg, ok,
+ fecha_inicio=fecha, fecha_fin=fecha,
+ servicios=len(grupo_rows))
# ── PASO 3: Enviar Pre-servicios (RCXC / SC) ──────────────────────────────
grupos_ps_all = _agrupar_por_presserv(rows_ps)
@@ -582,41 +662,43 @@ async def run_automation(
if str(v[0].get("CODCONTRATO") or "").strip() not in excluded}
resultado["paso3_preserv"]["excluidos"] = len(grupos_ps_all) - len(grupos_ps)
- async with httpx.AsyncClient(timeout=timeout) as client:
- for id_ps, grupo_rows in grupos_ps.items():
- ps_prefijo = str(grupo_rows[0].get("PS_PREFIJO") or "SC").strip()
- ps_numero = str(grupo_rows[0].get("PS_NUMERO") or "").strip()
- rda_json = generar_rda_paciente(
- grupo_rows, prof_def, esp_def, remis_def, prefijo_def,
- numero_override=ps_numero,
- contrato_map=contrato_map,
- prefijo_override="00" if ps_prefijo == "RCXC" else ps_prefijo,
- sin_contrato_set=sin_contrato_set,
- )
- factura_ps = f"{ps_prefijo}-{ps_numero.zfill(5)}"
- 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 "preserv" in pasos_set:
+ async with httpx.AsyncClient(timeout=timeout) as client:
+ for id_ps, grupo_rows in grupos_ps.items():
+ ps_prefijo = str(grupo_rows[0].get("PS_PREFIJO") or "SC").strip()
+ ps_numero = str(grupo_rows[0].get("PS_NUMERO") or "").strip()
+ rda_json = generar_rda_paciente(
+ grupo_rows, prof_def, esp_def, remis_def, prefijo_def,
+ numero_override=ps_numero,
+ contrato_map=contrato_map,
+ prefijo_override="00" if ps_prefijo == "RCXC" else ps_prefijo,
+ sin_contrato_set=sin_contrato_set,
+ )
+ factura_ps = f"{ps_prefijo}-{ps_numero.zfill(5)}"
+ 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["paso3_preserv"]["enviados"] += 1
- else:
- resultado["paso3_preserv"]["errores"] += 1
+ if ok:
+ resultado["paso3_preserv"]["enviados"] += 1
+ else:
+ resultado["paso3_preserv"]["errores"] += 1
- resultado["paso3_preserv"]["detalle"].append({
- "factura": factura_ps,
- "paciente": str(grupo_rows[0].get("COD_PACIENTE", "")),
- "examenes": len(grupo_rows),
- "ok": ok, "msg": msg,
- })
+ resultado["paso3_preserv"]["detalle"].append({
+ "idrecepcion": id_ps,
+ "factura": factura_ps,
+ "paciente": str(grupo_rows[0].get("COD_PACIENTE", "")),
+ "examenes": len(grupo_rows),
+ "ok": ok, "msg": msg,
+ })
- _guardar_envio(user["user_id"], "transaccion", factura_ps, rda_json, msg, ok,
- fecha_inicio=fecha, fecha_fin=fecha,
- servicios=len(grupo_rows))
+ _guardar_envio(user["user_id"], "transaccion", factura_ps, rda_json, msg, ok,
+ fecha_inicio=fecha, fecha_fin=fecha,
+ servicios=len(grupo_rows))
# ── PASO 4: Enviar Facturas Venta ─────────────────────────────────────────
excluded_ventas = load_excluded_ventas_set()
@@ -626,37 +708,39 @@ async def run_automation(
resultado["paso4_ventas"]["excluidos"] = len(grupos_vta_all) - len(grupos_vta)
endpoint_venta = f"{TNS_BASE}/v2/facturacion/Ventas/Crear"
- async with httpx.AsyncClient(timeout=timeout) as client:
- for id_rec, grupo_rows in grupos_vta.items():
- num_fac = str(grupo_rows[0].get("NUM_FACTURA") or "").strip()
- prefijo = str(grupo_rows[0].get("PREFIJO") or "").strip()
- venta_json = generar_factura_venta(grupo_rows, default_vendedor="00",
- default_prefijo=prefijo_def, numero_override=num_fac)
- factura_display = f"{prefijo}-{num_fac}"
- contrato_vta = str(grupo_rows[0].get("CODCONTRATO") or "").strip()
- try:
- resp = await client.post(endpoint_venta, json=venta_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 "ventas" in pasos_set:
+ async with httpx.AsyncClient(timeout=timeout) as client:
+ for id_rec, grupo_rows in grupos_vta.items():
+ num_fac = str(grupo_rows[0].get("NUM_FACTURA") or "").strip()
+ prefijo = str(grupo_rows[0].get("PREFIJO") or "").strip()
+ venta_json = generar_factura_venta(grupo_rows, default_vendedor="00",
+ default_prefijo=prefijo_def, numero_override=num_fac)
+ factura_display = f"{prefijo}-{num_fac}"
+ contrato_vta = str(grupo_rows[0].get("CODCONTRATO") or "").strip()
+ try:
+ resp = await client.post(endpoint_venta, json=venta_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["paso4_ventas"]["enviados"] += 1
- else:
- resultado["paso4_ventas"]["errores"] += 1
+ if ok:
+ resultado["paso4_ventas"]["enviados"] += 1
+ else:
+ resultado["paso4_ventas"]["errores"] += 1
- resultado["paso4_ventas"]["detalle"].append({
- "factura": factura_display,
- "paciente": str(grupo_rows[0].get("COD_PACIENTE", "")),
- "examenes": len(grupo_rows),
- "ok": ok, "msg": msg,
- })
+ resultado["paso4_ventas"]["detalle"].append({
+ "idrecepcion": id_rec,
+ "factura": factura_display,
+ "paciente": str(grupo_rows[0].get("COD_PACIENTE", "")),
+ "examenes": len(grupo_rows),
+ "ok": ok, "msg": msg,
+ })
- _guardar_envio(user["user_id"], "ventas", factura_display, venta_json, msg, ok,
- fecha_inicio=fecha, fecha_fin=fecha, servicios=len(grupo_rows),
- idrecepcion=id_rec, contrato=contrato_vta)
+ _guardar_envio(user["user_id"], "ventas", factura_display, venta_json, msg, ok,
+ fecha_inicio=fecha, fecha_fin=fecha, servicios=len(grupo_rows),
+ idrecepcion=id_rec, contrato=contrato_vta)
r1 = resultado["paso1_terceros"]
r2 = resultado["paso2_rda"]
@@ -694,3 +778,189 @@ def _guardar_envio(user_id, tipo, factura, json_data, respuesta, ok,
conn.close()
except Exception:
pass
+
+
+@router.post("/reenviar-tercero")
+async def reenviar_tercero(
+ request: Request,
+ user: dict = Depends(get_current_user),
+ codigo: str = Form(...),
+):
+ conn = get_connection()
+ configs = {row["key"]: row["value"] for row in conn.execute("SELECT * FROM config").fetchall()}
+ conn.close()
+
+ fb, fb_ok, fb_msg = get_firebird_from_config(configs)
+ if not fb_ok:
+ return JSONResponse({"success": False, "message": f"Error Firebird: {fb_msg}"})
+
+ ok, err, rows = fb.execute_query(_SQL_PAC_BY_CODIGO, {"codigo": codigo.strip()})
+ fb.disconnect()
+
+ if not ok or not rows:
+ return JSONResponse({"success": False, "message": err or "Paciente no encontrado"})
+
+ tercero_json = generar_tercero_api(dict(rows[0]))
+ 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"Token TNS: {token_err}"})
+
+ headers = {"Content-Type": "application/json", "Authorization": f"Bearer {token}"}
+ try:
+ async with httpx.AsyncClient(timeout=int(configs.get("api_timeout", 30))) as client:
+ resp = await client.post(f"{TNS_BASE}/v2/tablas/Tercero/Crear", json=tercero_json, headers=headers)
+ ok_r = resp.is_success
+ msg = resp.json().get("message", "") if ok_r else resp.text[:200]
+ except Exception as e:
+ ok_r = False
+ msg = str(e)
+
+ _guardar_envio(user["user_id"], "terceros", codigo, tercero_json, msg, ok_r)
+ return JSONResponse({"success": ok_r, "message": msg})
+
+
+@router.post("/reenviar-rda")
+async def reenviar_rda(
+ request: Request,
+ user: dict = Depends(get_current_user),
+ idrecepcion: int = Form(...),
+ tipo: str = Form("rda"),
+):
+ conn = get_connection()
+ configs = {row["key"]: row["value"] for row in conn.execute("SELECT * FROM config").fetchall()}
+ conn.close()
+
+ fb, fb_ok, fb_msg = get_firebird_from_config(configs)
+ if not fb_ok:
+ return JSONResponse({"success": False, "message": f"Error Firebird: {fb_msg}"})
+
+ if tipo == "preserv":
+ ok, err, rows = fb.execute_query(_SQL_PRESERV_BY_ID, {"id_ps": idrecepcion})
+ else:
+ ok, err, rows = fb.execute_query(_SQL_RDA_BY_ID, {"idrecepcion": idrecepcion})
+ fb.disconnect()
+
+ if not ok or not rows:
+ return JSONResponse({"success": False, "message": err or "Sin datos"})
+
+ excluded = load_excluded_set()
+ contrato = str(rows[0].get("CODCONTRATO") or "").strip()
+ if contrato in excluded:
+ return JSONResponse({"success": False, "message": f"Contrato {contrato} excluido"})
+
+ 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")
+ contrato_map = load_contrato_map()
+ sin_contrato_set = load_sin_contrato_set()
+ api_sucursal = configs.get("api_sucursal", "") or "00"
+
+ if tipo == "preserv":
+ from collections import defaultdict
+ grupos_ps: dict = defaultdict(list)
+ for row in rows:
+ grupos_ps[row.get("ID_PS")].append(dict(row))
+ grupo_rows = list(grupos_ps.values())[0]
+ ps_prefijo = str(grupo_rows[0].get("PS_PREFIJO") or "SC").strip()
+ ps_numero = str(grupo_rows[0].get("PS_NUMERO") or "").strip()
+ rda_json = generar_rda_paciente(
+ grupo_rows, prof_def, esp_def, remis_def, prefijo_def,
+ numero_override=ps_numero, contrato_map=contrato_map,
+ prefijo_override="00" if ps_prefijo == "RCXC" else ps_prefijo,
+ sin_contrato_set=sin_contrato_set,
+ )
+ factura = f"{ps_prefijo}-{ps_numero.zfill(5)}"
+ else:
+ grupos = agrupar_por_recepcion(rows)
+ grupo_rows = list(grupos.values())[0]
+ num_fac = str(grupo_rows[0].get("NUM_FACTURA") or "").strip()
+ rda_json = generar_rda_paciente(
+ grupo_rows, prof_def, esp_def, remis_def, prefijo_def,
+ numero_override=num_fac, contrato_map=contrato_map,
+ sin_contrato_set=sin_contrato_set,
+ )
+ factura = num_fac or str(idrecepcion)
+
+ 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"Token TNS: {token_err}"})
+
+ headers = {"Content-Type": "application/json", "Authorization": f"Bearer {token}"}
+ endpoint = f"{TNS_BASE}/v2/rda/RdaPaciente/Insertar?codigosucursal={api_sucursal}"
+ try:
+ async with httpx.AsyncClient(timeout=int(configs.get("api_timeout", 30))) as client:
+ resp = await client.post(endpoint, json=rda_json, headers=headers)
+ ok_r = resp.is_success
+ msg = resp.json().get("message", "") if ok_r else resp.text[:200]
+ except Exception as e:
+ ok_r = False
+ msg = str(e)
+
+ _guardar_envio(user["user_id"], "transaccion", factura, rda_json, msg, ok_r,
+ idrecepcion=idrecepcion, contrato=contrato)
+ return JSONResponse({"success": ok_r, "message": msg})
+
+
+@router.post("/reenviar-venta")
+async def reenviar_venta(
+ request: Request,
+ user: dict = Depends(get_current_user),
+ idrecepcion: int = Form(...),
+):
+ conn = get_connection()
+ configs = {row["key"]: row["value"] for row in conn.execute("SELECT * FROM config").fetchall()}
+ conn.close()
+
+ fb, fb_ok, fb_msg = get_firebird_from_config(configs)
+ if not fb_ok:
+ return JSONResponse({"success": False, "message": f"Error Firebird: {fb_msg}"})
+
+ ok, err, rows = fb.execute_query(_SQL_VENTAS_BY_ID, {"idrecepcion": idrecepcion})
+ fb.disconnect()
+
+ if not ok or not rows:
+ return JSONResponse({"success": False, "message": err or "Sin datos"})
+
+ excluded_ventas = load_excluded_ventas_set()
+ contrato_vta = str(rows[0].get("CODCONTRATO") or "").strip()
+ if contrato_vta in excluded_ventas:
+ return JSONResponse({"success": False, "message": f"Contrato {contrato_vta} excluido"})
+
+ from collections import defaultdict
+ grupos: dict = defaultdict(list)
+ for row in rows:
+ grupos[row.get("IDRECEPCION")].append(dict(row))
+ grupo_rows = list(grupos.values())[0]
+
+ prefijo_def = configs.get("prefijo_tns_default", "00")
+ num_fac = str(grupo_rows[0].get("NUM_FACTURA") or "").strip()
+ prefijo = str(grupo_rows[0].get("PREFIJO") or "").strip()
+ venta_json = generar_factura_venta(grupo_rows, default_vendedor="00",
+ default_prefijo=prefijo_def, numero_override=num_fac)
+ factura_display = f"{prefijo}-{num_fac}"
+
+ 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"Token TNS: {token_err}"})
+
+ headers = {"Content-Type": "application/json", "Authorization": f"Bearer {token}"}
+ endpoint = f"{TNS_BASE}/v2/facturacion/Ventas/Crear"
+ try:
+ async with httpx.AsyncClient(timeout=int(configs.get("api_timeout", 30))) as client:
+ resp = await client.post(endpoint, json=venta_json, headers=headers)
+ ok_r = resp.is_success
+ msg = resp.json().get("message", "") if ok_r else resp.text[:200]
+ except Exception as e:
+ ok_r = False
+ msg = str(e)
+
+ _guardar_envio(user["user_id"], "ventas", factura_display, venta_json, msg, ok_r,
+ idrecepcion=idrecepcion, contrato=contrato_vta)
+ return JSONResponse({"success": ok_r, "message": msg})
diff --git a/app/templates/automation.html b/app/templates/automation.html
index cb44b18..e30c25c 100644
--- a/app/templates/automation.html
+++ b/app/templates/automation.html
@@ -4,82 +4,108 @@
{% block content %}
-
+
Envío automático por fecha
- Selecciona una fecha para ver cuántos pacientes y RDA se enviarán a TNS.
+ Selecciona fecha, pasos activos y convenio (opcional) para enviar a TNS.
-
-
-
+
+
+
+
+
+
+ Pasos:
+
+ Terceros
+
+
+ RDA Servicios
+
+
+ Pre-servicios
+
+
+ Ventas
-
+
Resumen de la fecha
+
-
+
-
+
-
+
-
+
0
Pre-servicios
RCXC / SC
-
-
-
Pacientes — clic para ver sus recepciones:
+
+
+
+
Pacientes — clic para ver recepciones:
+
+
Pre-servicios RCXC / SC — clic para ver JSON:
+
+
Facturas Venta — clic para ver JSON:
@@ -87,56 +113,49 @@
-
-
-
-
-
-
1
-
Terceros (pacientes)
-
-
Esperando...
+
+
+
+
+
-
-
-
-
2
-
RDA Paciente (recepciones)
+
+
+
-
+
Fallos
+
+
+
-
-
-
-
3
-
Pre-servicios RDA (RCXC / SC)
+
+
+
-
-
-
-
-
+
Enviados correctamente
+
+
+
-
+
-
Validar envío
@@ -145,49 +164,38 @@
-
- Se enviará la siguiente información a TNS (api.tns.co) para la fecha
- :
+ Se enviará a TNS para la fecha :
-
-
+
0
Terceros
-
Tercero/Crear
-
+
0
-
Recepciones
-
RdaPaciente/Insertar
+
RDA
-
+
0
Exámenes
-
detallePedido
-
+
0
Pre-servicios
-
RCXC / SC
-
-
- Esta acción enviará datos reales a TNS. Los registros que ya existan serán actualizados. No se puede deshacer.
+ Esta acción enviará datos reales a TNS. Los registros existentes serán actualizados. No se puede deshacer.
-
-
+
-
@@ -205,20 +213,48 @@
let _previewData = null;
let _rdaJsonStore = {};
-function escHtml(s) {
- return s.replace(/&/g,'&').replace(//g,'>');
+// ── Estado de pasos ──────────────────────────────────────────────────────────
+const _pasos = new Set(['terceros','rda','preserv','ventas']);
+const _pasoMeta = {
+ terceros: { border:'border-blue-500', bg:'bg-blue-500', text:'text-blue-600' },
+ rda: { border:'border-purple-500', bg:'bg-purple-500', text:'text-purple-600' },
+ preserv: { border:'border-orange-500', bg:'bg-orange-500', text:'text-orange-600' },
+ ventas: { border:'border-emerald-500',bg:'bg-emerald-500', text:'text-emerald-600'},
+};
+
+function togglePaso(paso) {
+ if (_pasos.has(paso)) _pasos.delete(paso);
+ else _pasos.add(paso);
+ const m = _pasoMeta[paso];
+ const btn = document.getElementById('chip-' + paso);
+ if (_pasos.has(paso)) {
+ btn.className = `inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-semibold border-2 transition-all ${m.border} ${m.bg} text-white`;
+ } else {
+ btn.className = `inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-semibold border-2 transition-all ${m.border} bg-white ${m.text}`;
+ }
+ document.getElementById('preview-section').classList.add('hidden');
+ document.getElementById('results-section').classList.add('hidden');
+ document.getElementById('btn-run').disabled = true;
+ _previewData = null;
}
+function escHtml(s) {
+ return String(s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"');
+}
+
+// ── Vista previa ─────────────────────────────────────────────────────────────
async function previewAutomation() {
const fecha = document.getElementById('fecha-input').value;
if (!fecha) { showToast('Selecciona una fecha', 'error'); return; }
+ if (_pasos.size === 0) { showToast('Activa al menos un paso', 'error'); return; }
const btn = document.getElementById('btn-preview');
showLoading(btn);
document.getElementById('preview-section').classList.add('hidden');
- document.getElementById('progress-section').classList.add('hidden');
+ document.getElementById('results-section').classList.add('hidden');
document.getElementById('btn-run').disabled = true;
_previewData = null;
+ _rdaJsonStore = {};
const contrato = document.getElementById('contrato-input').value.trim();
const form = new FormData();
@@ -233,28 +269,82 @@ async function previewAutomation() {
if (!data.success) { showToast(data.message || 'Error al consultar', 'error'); return; }
_previewData = data;
- _rdaJsonStore = {};
- document.getElementById('cnt-pacientes').textContent = data.pacientes;
- document.getElementById('cnt-recepciones').textContent = data.recepciones;
- document.getElementById('cnt-examenes').textContent = data.examenes;
- document.getElementById('cnt-preservicios').textContent = data.preservicios || 0;
- document.getElementById('cnt-ventas').textContent = data.ventas || 0;
- document.getElementById('preview-fecha').textContent = data.fecha;
- const psTable = document.getElementById('preview-ps-table');
+ // Stat cards
+ document.getElementById('cnt-pacientes').textContent = data.pacientes;
+ document.getElementById('cnt-recepciones').textContent = data.recepciones;
+ document.getElementById('cnt-examenes').textContent = data.examenes;
+ document.getElementById('cnt-preservicios').textContent= data.preservicios || 0;
+ document.getElementById('cnt-ventas').textContent = data.ventas || 0;
+ document.getElementById('preview-fecha').textContent = data.fecha;
+
+ // Opacidad según pasos activos
+ document.getElementById('card-terceros').style.opacity = _pasos.has('terceros') ? '1' : '0.3';
+ document.getElementById('card-rda').style.opacity = _pasos.has('rda') ? '1' : '0.3';
+ document.getElementById('card-examenes').style.opacity = (_pasos.has('rda') || _pasos.has('preserv')) ? '1' : '0.3';
+ document.getElementById('card-preserv').style.opacity = _pasos.has('preserv') ? '1' : '0.3';
+ document.getElementById('card-ventas').style.opacity = _pasos.has('ventas') ? '1' : '0.3';
+
+ // Sección Terceros/RDA
+ const secTR = document.getElementById('sec-terceros-rda');
+ if (_pasos.has('terceros') || _pasos.has('rda')) {
+ secTR.classList.remove('hidden');
+ const table = document.getElementById('preview-table');
+ if (!data.pacientes_preview || data.pacientes_preview.length === 0) {
+ table.innerHTML = 'No hay pacientes para esta fecha
';
+ } else {
+ table.innerHTML = data.pacientes_preview.map((p, i) => {
+ const rdaRows = (p.rda || []).map((r, j) => {
+ const key = `rda-json-${i}-${j}`;
+ _rdaJsonStore[key] = r.json;
+ return `
+
+
+
+
+ ${escHtml(r.factura)}
+ ${escHtml(r.fecha)}
+ ${escHtml(r.contrato||'—')}
+ ${r.examenes} exam.
+ ${escHtml(r.diag||'')}
+ $${Number(r.valor||0).toLocaleString('es-CO')}
+
+
+
`;
+ }).join('');
+ return `
+
+
+
+ ${escHtml(p.tipo)}
+ ${escHtml(p.doc)}
+ ${escHtml(p.nombre)}
+ ${(p.rda||[]).length} RDA
+
+
${rdaRows||'
Sin recepciones
'}
+
`;
+ }).join('');
+ }
+ } else {
+ secTR.classList.add('hidden');
+ }
+
+ // Pre-servicios
const psSection = document.getElementById('preservicios-section');
- if (data.preservicios_preview && data.preservicios_preview.length > 0) {
- psTable.innerHTML = data.preservicios_preview.map((ps, i) => {
+ if (_pasos.has('preserv') && data.preservicios_preview && data.preservicios_preview.length > 0) {
+ document.getElementById('preview-ps-table').innerHTML = data.preservicios_preview.map((ps, i) => {
const key = `ps-json-${i}`;
_rdaJsonStore[key] = ps.json;
return `
-
- ${ps.factura}
- ${ps.fecha}
- Pac. ${ps.paciente}
+ onclick="toggleJsonBlock('${key}','pschev-${i}')">
+
+ ${escHtml(ps.factura)}
+ ${escHtml(ps.fecha)}
+ Pac. ${escHtml(String(ps.paciente))}
${ps.examenes} examen(es)
@@ -265,21 +355,21 @@ async function previewAutomation() {
psSection.classList.add('hidden');
}
- const vtaTable = document.getElementById('preview-vta-table');
+ // Ventas
const vtaSection = document.getElementById('ventas-section');
- if (data.ventas_preview && data.ventas_preview.length > 0) {
- vtaTable.innerHTML = data.ventas_preview.map((vta, i) => {
+ if (_pasos.has('ventas') && data.ventas_preview && data.ventas_preview.length > 0) {
+ document.getElementById('preview-vta-table').innerHTML = data.ventas_preview.map((vta, i) => {
const key = `vta-json-${i}`;
_rdaJsonStore[key] = vta.json;
return `
-
- ${vta.factura}
- ${vta.fecha}
- ${vta.contrato || '—'}
- Pac. ${vta.paciente}
+ onclick="toggleJsonBlock('${key}','vtachev-${i}')">
+
+ ${escHtml(vta.factura)}
+ ${escHtml(vta.fecha)}
+ ${escHtml(vta.contrato||'—')}
+ Pac. ${escHtml(String(vta.paciente))}
${vta.examenes} examen(es)
@@ -290,73 +380,37 @@ async function previewAutomation() {
vtaSection.classList.add('hidden');
}
- const table = document.getElementById('preview-table');
- if (data.pacientes === 0) {
- table.innerHTML = '
No hay pacientes para esta fecha
';
- showToast('No hay datos para esa fecha', 'warning');
- } else {
- table.innerHTML = data.pacientes_preview.map((p, i) => {
- const rdaRows = p.rda.map((r, j) => {
- const key = `rda-json-${i}-${j}`;
- _rdaJsonStore[key] = r.json;
- return `
-
-
-
-
- ${r.factura}
- ${r.fecha}
- ${r.contrato || '—'}
- ${r.examenes} examen(es)
- ${escHtml(r.diag||'')}
- $${Number(r.valor).toLocaleString('es-CO')}
-
-
-
`;
- }).join('');
- return `
-
-
-
- ${p.tipo}
- ${p.doc}
- ${p.nombre}
- ${p.rda.length} RDA
-
-
${rdaRows || '
Sin recepciones
'}
-
`;
- }).join('');
-
- document.getElementById('btn-run').disabled = false;
- showToast(`${data.pacientes} paciente(s) · ${data.recepciones} recepción(es)`, 'success');
- }
+ document.getElementById('btn-run').disabled = false;
document.getElementById('preview-section').classList.remove('hidden');
- } catch (e) {
+ showToast(`${data.pacientes} pac · ${data.recepciones} RDA · ${data.preservicios||0} PS · ${data.ventas||0} VTA`, 'success');
+ } catch(e) {
hideLoading(btn, '
Vista previa');
showToast('Error de conexión', 'error');
}
}
+// ── Modal ────────────────────────────────────────────────────────────────────
function abrirModalConfirmar() {
if (!_previewData) return;
const d = _previewData;
document.getElementById('modal-fecha').textContent = d.fecha;
- document.getElementById('modal-cnt-pac').textContent = d.pacientes;
- document.getElementById('modal-cnt-rec').textContent = d.recepciones;
- document.getElementById('modal-cnt-exa').textContent = d.examenes;
- document.getElementById('modal-cnt-ps').textContent = d.preservicios || 0;
- document.getElementById('modal-cnt-vta').textContent = d.ventas || 0;
-
- document.getElementById('modal-lista-pac').innerHTML = d.pacientes_preview.map(p => `
+ document.getElementById('mc-terceros').style.opacity = _pasos.has('terceros') ? '1' : '0.3';
+ document.getElementById('mc-rda').style.opacity = _pasos.has('rda') ? '1' : '0.3';
+ document.getElementById('mc-examenes').style.opacity = (_pasos.has('rda')||_pasos.has('preserv')) ? '1' : '0.3';
+ document.getElementById('mc-preserv').style.opacity = _pasos.has('preserv') ? '1' : '0.3';
+ document.getElementById('mc-ventas').style.opacity = _pasos.has('ventas') ? '1' : '0.3';
+ document.getElementById('modal-cnt-pac').textContent = _pasos.has('terceros') ? d.pacientes : '—';
+ document.getElementById('modal-cnt-rec').textContent = _pasos.has('rda') ? d.recepciones : '—';
+ document.getElementById('modal-cnt-exa').textContent = (_pasos.has('rda')||_pasos.has('preserv')) ? d.examenes : '—';
+ document.getElementById('modal-cnt-ps').textContent = _pasos.has('preserv') ? (d.preservicios||0) : '—';
+ document.getElementById('modal-cnt-vta').textContent = _pasos.has('ventas') ? (d.ventas||0) : '—';
+ document.getElementById('modal-lista-pac').innerHTML = (d.pacientes_preview || []).map(p => `
- ${p.tipo}
- ${p.doc}
- ${p.nombre}
- ${p.rda.length} RDA
+ ${escHtml(p.tipo)}
+ ${escHtml(p.doc)}
+ ${escHtml(p.nombre)}
+ ${(p.rda||[]).length} RDA
`).join('');
-
document.getElementById('modal-confirmar').classList.remove('hidden');
}
@@ -364,141 +418,237 @@ function cerrarModalConfirmar() {
document.getElementById('modal-confirmar').classList.add('hidden');
}
+// ── Ejecutar envío ───────────────────────────────────────────────────────────
async function confirmarEnvio() {
cerrarModalConfirmar();
const fecha = document.getElementById('fecha-input').value;
-
+ const contrato = document.getElementById('contrato-input').value.trim();
const btn = document.getElementById('btn-run');
showLoading(btn);
document.getElementById('btn-preview').disabled = true;
- 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';
- document.getElementById('p3-badge').textContent = 'Esperando...';
- document.getElementById('p3-badge').className = 'text-sm text-gray-400';
- document.getElementById('p4-badge').textContent = 'Esperando...';
- document.getElementById('p4-badge').className = 'text-sm text-gray-400';
+ document.getElementById('results-section').classList.add('hidden');
- const contrato = document.getElementById('contrato-input').value.trim();
const form = new FormData();
form.append('fecha', fecha);
if (contrato) form.append('contrato', contrato);
+ form.append('pasos', [..._pasos].join(','));
try {
const resp = await fetch('/automation/run', { method: 'POST', body: form, credentials: 'include' });
const data = await resp.json();
- hideLoading(btn, '
Ejecutar envío');
+ hideLoading(btn, '
Ejecutar');
document.getElementById('btn-preview').disabled = false;
if (!data.success) {
showToast(data.message || 'Error', 'error');
- document.getElementById('p1-badge').textContent = 'Error';
- document.getElementById('p1-badge').className = 'text-sm text-red-500';
return;
}
-
- const r = data.resultado;
- 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,
- })));
- 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,
- })));
- renderBadge('p3-badge', r.paso3_preserv.enviados, r.paso3_preserv.errores);
- renderDetalle('p3-detalle', r.paso3_preserv.detalle.map(d => ({
- col1: `${d.factura}`,
- col2: `Pac. ${d.paciente} — ${d.examenes} examen(es)`,
- col3: `${d.ok ? '✅' : '❌'} ${d.msg || ''}`, ok: d.ok,
- })));
- renderBadge('p4-badge', r.paso4_ventas.enviados, r.paso4_ventas.errores);
- renderDetalle('p4-detalle', r.paso4_ventas.detalle.map(d => ({
- col1: `${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 + r.paso3_preserv.errores + r.paso4_ventas.errores;
- showToast(totalErr === 0 ? 'Envío completado sin errores' : `Completado con ${totalErr} error(es)`,
- totalErr === 0 ? 'success' : 'warning');
- } catch (e) {
- hideLoading(btn, '
Ejecutar envío');
+ renderResults(data.resultado);
+ } catch(e) {
+ hideLoading(btn, '
Ejecutar');
document.getElementById('btn-preview').disabled = false;
showToast('Error de conexión', 'error');
}
}
-function toggleRda(id, row) {
- const div = document.getElementById(id);
- const chev = row.querySelector('[id^="chev-"]');
- const hidden = div.classList.contains('hidden');
- div.classList.toggle('hidden', !hidden);
- if (chev) chev.style.transform = hidden ? 'rotate(90deg)' : '';
-}
+// ── Renderizar resultados ────────────────────────────────────────────────────
+const _STEP_DEFS = [
+ { pasoKey:'terceros', resultKey:'paso1_terceros', label:'Terceros', color:'blue', tipo:'tercero', idField:'codigo' },
+ { pasoKey:'rda', resultKey:'paso2_rda', label:'RDA', color:'purple', tipo:'rda', idField:'idrecepcion' },
+ { pasoKey:'preserv', resultKey:'paso3_preserv', label:'Pre-servicios', color:'orange', tipo:'preserv', idField:'idrecepcion' },
+ { pasoKey:'ventas', resultKey:'paso4_ventas', label:'Ventas', color:'emerald',tipo:'venta', idField:'idrecepcion' },
+];
-function toggleRdaJson(id, row) {
- const div = document.getElementById(id);
- const chev = row.querySelector('[id^="rdachev-"]');
- const hidden = div.classList.contains('hidden');
- if (hidden && _rdaJsonStore[id]) {
- div.textContent = JSON.stringify(_rdaJsonStore[id], null, 2);
- }
- div.classList.toggle('hidden', !hidden);
- if (chev) chev.style.transform = hidden ? 'rotate(90deg)' : '';
-}
+const _COLOR_BADGE = {
+ blue: 'bg-blue-100 text-blue-700',
+ purple: 'bg-purple-100 text-purple-700',
+ orange: 'bg-orange-100 text-orange-700',
+ emerald:'bg-emerald-100 text-emerald-700',
+};
+const _COLOR_SUM = {
+ blue: 'bg-blue-100 text-blue-800',
+ purple: 'bg-purple-100 text-purple-800',
+ orange: 'bg-orange-100 text-orange-800',
+ emerald:'bg-emerald-100 text-emerald-800',
+};
-function togglePs(id, row) {
- const div = document.getElementById(id);
- const chev = row.querySelector('[id^="pschev-"]');
- const hidden = div.classList.contains('hidden');
- if (hidden && _rdaJsonStore[id]) {
- div.textContent = JSON.stringify(_rdaJsonStore[id], null, 2);
- }
- div.classList.toggle('hidden', !hidden);
- if (chev) chev.style.transform = hidden ? 'rotate(90deg)' : '';
-}
+function renderResults(r) {
+ const activePasos = r.pasos || _STEP_DEFS.map(s => s.pasoKey);
+ const activeSteps = _STEP_DEFS.filter(s => activePasos.includes(s.pasoKey));
-function toggleVta(id, row) {
- const div = document.getElementById(id);
- const chev = row.querySelector('[id^="vtachev-"]');
- const hidden = div.classList.contains('hidden');
- if (hidden && _rdaJsonStore[id]) {
- div.textContent = JSON.stringify(_rdaJsonStore[id], null, 2);
- }
- div.classList.toggle('hidden', !hidden);
- if (chev) chev.style.transform = hidden ? 'rotate(90deg)' : '';
-}
+ // Chips de resumen
+ document.getElementById('res-summary').innerHTML = activeSteps.map(s => {
+ const d = r[s.resultKey];
+ return `
+ ${s.label}
+ ✓${d.enviados}
+ ${d.errores > 0 ? `✗${d.errores} ` : ''}
+ `;
+ }).join('');
-function renderBadge(id, enviados, errores) {
- const el = document.getElementById(id);
- if (errores === 0) {
- 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';
+ // Clasificar items
+ const failures = [], successes = [];
+ activeSteps.forEach(s => {
+ (r[s.resultKey].detalle || []).forEach((item, idx) => {
+ const entry = { item, step: s, idx: `${s.pasoKey}-${idx}` };
+ (item.ok ? successes : failures).push(entry);
+ });
+ });
+
+ // Panel fallos
+ const failPanel = document.getElementById('failures-panel');
+ const failBody = document.getElementById('failures-body');
+ if (failures.length > 0) {
+ document.getElementById('failures-title').textContent = `Fallos (${failures.length})`;
+ failBody.innerHTML = failures.map(({ item, step: s, idx }) => {
+ let label1 = '', label2 = '';
+ let dtype = s.tipo, did = '';
+ if (s.tipo === 'tercero') {
+ label1 = item.doc || '';
+ label2 = item.nombre || '';
+ did = escHtml(String(item.codigo || ''));
+ } else {
+ label1 = item.factura || '';
+ label2 = `Pac.${item.paciente||''} · ${item.examenes||0} exam.`;
+ did = escHtml(String(item.idrecepcion || 0));
+ }
+ return `
+
+
+ ${s.label}
+ ${escHtml(label1)}
+ ${escHtml(label2)}
+ ${escHtml((item.msg||'Sin detalle').slice(0,70))}
+
+ Reenviar
+
+
`;
+ }).join('');
+ failPanel.classList.remove('hidden');
} else {
- el.textContent = `❌ ${errores} errores`;
- el.className = 'text-sm text-red-600 font-medium';
+ failPanel.classList.add('hidden');
+ }
+
+ // Panel exitosos
+ const okPanel = document.getElementById('successes-panel');
+ const okBody = document.getElementById('successes-body');
+ if (successes.length > 0) {
+ document.getElementById('successes-title').textContent = `Enviados correctamente (${successes.length})`;
+ okBody.innerHTML = successes.map(({ item, step: s }) => {
+ let label1 = '', label2 = '';
+ if (s.tipo === 'tercero') { label1 = item.doc||''; label2 = item.nombre||''; }
+ else { label1 = item.factura||''; label2 = `Pac.${item.paciente||''} · ${item.examenes||0} exam.`; }
+ return `
+
+
+ ${s.label}
+ ${escHtml(label1)}
+ ${escHtml(label2)}
+ ${escHtml((item.msg||'').slice(0,60))}
+
`;
+ }).join('');
+ okPanel.classList.remove('hidden');
+ } else {
+ okPanel.classList.add('hidden');
+ }
+
+ document.getElementById('results-section').classList.remove('hidden');
+ const totalErr = failures.length;
+ showToast(
+ totalErr === 0 ? `Envío completado · ${successes.length} registros OK` : `${totalErr} fallo(s) · ${successes.length} OK`,
+ totalErr === 0 ? 'success' : 'warning'
+ );
+}
+
+// ── Reenviar item individual ─────────────────────────────────────────────────
+async function reenviarItem(btn) {
+ const tipo = btn.dataset.tipo;
+ const id = btn.dataset.id;
+ const row = btn.closest('[data-resend-row]');
+ const origHtml = btn.innerHTML;
+ btn.innerHTML = '
';
+ btn.disabled = true;
+
+ const form = new FormData();
+ let endpoint = '';
+
+ if (tipo === 'tercero') {
+ endpoint = '/automation/reenviar-tercero';
+ form.append('codigo', String(id));
+ } else if (tipo === 'rda') {
+ endpoint = '/automation/reenviar-rda';
+ form.append('idrecepcion', String(id));
+ form.append('tipo', 'rda');
+ } else if (tipo === 'preserv') {
+ endpoint = '/automation/reenviar-rda';
+ form.append('idrecepcion', String(id));
+ form.append('tipo', 'preserv');
+ } else {
+ endpoint = '/automation/reenviar-venta';
+ form.append('idrecepcion', String(id));
+ }
+
+ try {
+ const resp = await fetch(endpoint, { method: 'POST', body: form, credentials: 'include' });
+ const data = await resp.json();
+
+ if (data.success) {
+ row.classList.remove('bg-red-50');
+ row.classList.add('bg-green-50');
+ const icon = row.querySelector('.fail-status-icon');
+ if (icon) icon.innerHTML = '
';
+ const msgEl = row.querySelector('.fail-msg');
+ if (msgEl) { msgEl.textContent = (data.message||'OK').slice(0,60); msgEl.className = 'text-green-600 text-xs max-w-[200px] truncate mt-0.5'; }
+ btn.innerHTML = '
OK';
+ btn.className = 'shrink-0 flex items-center gap-1.5 px-2.5 py-1.5 bg-green-50 border border-green-200 text-xs text-green-600 rounded-lg font-medium cursor-default';
+ showToast('Reenviado correctamente', 'success');
+ } else {
+ btn.innerHTML = origHtml;
+ btn.disabled = false;
+ const msgEl = row.querySelector('.fail-msg');
+ if (msgEl) msgEl.textContent = (data.message||'Error').slice(0,70);
+ showToast(data.message || 'Error al reenviar', 'error');
+ }
+ } catch(e) {
+ btn.innerHTML = origHtml;
+ btn.disabled = false;
+ showToast('Error de conexión', 'error');
}
}
-function renderDetalle(id, items) {
- const el = document.getElementById(id);
- if (!items || items.length === 0) return;
- el.innerHTML = items.map(i => `
-
- ${i.col1}
- ${i.col2}
- ${i.col3}
-
`).join('');
- el.classList.remove('hidden');
+// ── Helpers UI ───────────────────────────────────────────────────────────────
+function togglePanel(bodyId, chevId) {
+ const body = document.getElementById(bodyId);
+ const chev = document.getElementById(chevId);
+ const hidden = body.classList.contains('hidden');
+ body.classList.toggle('hidden', !hidden);
+ if (chev) {
+ if (hidden) {
+ chev.classList.replace('fa-chevron-right','fa-chevron-down');
+ } else {
+ chev.classList.replace('fa-chevron-down','fa-chevron-right');
+ }
+ }
+}
+
+function toggleSubRow(id, chevId) {
+ const div = document.getElementById(id);
+ const chev = document.getElementById(chevId);
+ const hidden = div.classList.contains('hidden');
+ div.classList.toggle('hidden', !hidden);
+ if (chev) chev.style.transform = hidden ? 'rotate(90deg)' : '';
+}
+
+function toggleJsonBlock(id, chevId) {
+ const div = document.getElementById(id);
+ const chev = document.getElementById(chevId);
+ const hidden = div.classList.contains('hidden');
+ if (hidden && _rdaJsonStore[id]) div.textContent = JSON.stringify(_rdaJsonStore[id], null, 2);
+ div.classList.toggle('hidden', !hidden);
+ if (chev) chev.style.transform = hidden ? 'rotate(90deg)' : '';
}
{% endblock %}