feat(automation): paso chips, results failure-first con reenvío por item
- 4 chips toggleables (Terceros/RDA/Pre-servicios/Ventas) que controlan qué pasos se ejecutan en vista previa y en el envío masivo - Backend: parámetro `pasos` en run_automation para saltarse pasos no seleccionados; agrega `idrecepcion`/`codigo` a cada detalle item - Nuevos endpoints POST /automation/reenviar-tercero, /reenviar-rda y /reenviar-venta para reenvío individual con SQL BY_ID para cada tipo - Panel de resultados rediseñado: barra de resumen por paso, sección de fallos expandida con botón "Reenviar" por item (actualiza row in- place), sección de exitosos colapsada por defecto Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
f8c76f7ce2
commit
fe31af2f9a
+380
-110
@@ -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})
|
||||
|
||||
+414
-264
@@ -4,82 +4,108 @@
|
||||
{% block content %}
|
||||
<div class="max-w-4xl space-y-6">
|
||||
|
||||
<!-- Panel de control -->
|
||||
<!-- ─── Panel de control ─────────────────────────────────────────────────── -->
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h3 class="font-semibold text-gray-800">
|
||||
<i class="fas fa-paper-plane mr-2 text-blue-500"></i>Envío automático por fecha
|
||||
</h3>
|
||||
<p class="text-xs text-gray-500 mt-1">
|
||||
Selecciona una fecha para ver cuántos pacientes y RDA se enviarán a TNS.
|
||||
Selecciona fecha, pasos activos y convenio (opcional) para enviar a TNS.
|
||||
</p>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div class="flex items-end gap-4">
|
||||
<div class="flex-1">
|
||||
<div class="p-6 space-y-4">
|
||||
<!-- Inputs + Botones -->
|
||||
<div class="flex items-end gap-3 flex-wrap">
|
||||
<div class="flex-1 min-w-36">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Fecha de atención</label>
|
||||
<input type="date" id="fecha-input" value="{{ today }}"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Convenio <span class="text-xs text-gray-400">(vacío = todos)</span></label>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Convenio <span class="text-xs text-gray-400">(vacío=todos)</span></label>
|
||||
<input type="text" id="contrato-input" placeholder="ej: 011"
|
||||
class="w-28 px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
</div>
|
||||
<button onclick="previewAutomation()"
|
||||
id="btn-preview"
|
||||
class="px-6 py-2 bg-gray-600 hover:bg-gray-700 text-white font-medium rounded-lg transition-colors flex items-center gap-2 whitespace-nowrap">
|
||||
<button onclick="previewAutomation()" id="btn-preview"
|
||||
class="px-5 py-2 bg-gray-600 hover:bg-gray-700 text-white font-medium rounded-lg transition-colors flex items-center gap-2 whitespace-nowrap">
|
||||
<i class="fas fa-eye"></i> Vista previa
|
||||
</button>
|
||||
<button onclick="abrirModalConfirmar()"
|
||||
id="btn-run"
|
||||
class="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors flex items-center gap-2 whitespace-nowrap disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
disabled>
|
||||
<i class="fas fa-play"></i> Ejecutar envío
|
||||
<button onclick="abrirModalConfirmar()" id="btn-run" disabled
|
||||
class="px-5 py-2 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors flex items-center gap-2 whitespace-nowrap disabled:opacity-40 disabled:cursor-not-allowed">
|
||||
<i class="fas fa-play"></i> Ejecutar
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Paso chips -->
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="text-xs text-gray-500 font-medium mr-1">Pasos:</span>
|
||||
<button id="chip-terceros" onclick="togglePaso('terceros')"
|
||||
class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-semibold border-2 transition-all border-blue-500 bg-blue-500 text-white">
|
||||
<i class="fas fa-user"></i> Terceros
|
||||
</button>
|
||||
<button id="chip-rda" onclick="togglePaso('rda')"
|
||||
class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-semibold border-2 transition-all border-purple-500 bg-purple-500 text-white">
|
||||
<i class="fas fa-exchange-alt"></i> RDA Servicios
|
||||
</button>
|
||||
<button id="chip-preserv" onclick="togglePaso('preserv')"
|
||||
class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-semibold border-2 transition-all border-orange-500 bg-orange-500 text-white">
|
||||
<i class="fas fa-file-alt"></i> Pre-servicios
|
||||
</button>
|
||||
<button id="chip-ventas" onclick="togglePaso('ventas')"
|
||||
class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-semibold border-2 transition-all border-emerald-500 bg-emerald-500 text-white">
|
||||
<i class="fas fa-file-invoice-dollar"></i> Ventas
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Vista previa de datos -->
|
||||
<!-- ─── Vista previa ─────────────────────────────────────────────────────── -->
|
||||
<div id="preview-section" class="hidden bg-white rounded-xl shadow-sm border border-gray-200">
|
||||
<div class="px-6 py-4 border-b border-gray-200 flex items-center justify-between">
|
||||
<h3 class="font-semibold text-gray-800"><i class="fas fa-search mr-2 text-gray-500"></i>Resumen de la fecha</h3>
|
||||
<span id="preview-fecha" class="text-sm text-gray-500"></span>
|
||||
</div>
|
||||
<div class="p-6 space-y-4">
|
||||
<!-- Stat cards -->
|
||||
<div class="grid grid-cols-5 gap-4">
|
||||
<div class="bg-blue-50 rounded-lg p-4 text-center">
|
||||
<div id="card-terceros" class="bg-blue-50 rounded-lg p-4 text-center transition-opacity">
|
||||
<div id="cnt-pacientes" class="text-3xl font-bold text-blue-600">0</div>
|
||||
<div class="text-sm text-blue-700 mt-1">Pacientes</div>
|
||||
</div>
|
||||
<div class="bg-purple-50 rounded-lg p-4 text-center">
|
||||
<div id="card-rda" class="bg-purple-50 rounded-lg p-4 text-center transition-opacity">
|
||||
<div id="cnt-recepciones" class="text-3xl font-bold text-purple-600">0</div>
|
||||
<div class="text-sm text-purple-700 mt-1">Recepciones</div>
|
||||
</div>
|
||||
<div class="bg-green-50 rounded-lg p-4 text-center">
|
||||
<div id="card-examenes" class="bg-green-50 rounded-lg p-4 text-center transition-opacity">
|
||||
<div id="cnt-examenes" class="text-3xl font-bold text-green-600">0</div>
|
||||
<div class="text-sm text-green-700 mt-1">Exámenes</div>
|
||||
</div>
|
||||
<div class="bg-orange-50 rounded-lg p-4 text-center">
|
||||
<div id="card-preserv" class="bg-orange-50 rounded-lg p-4 text-center transition-opacity">
|
||||
<div id="cnt-preservicios" class="text-3xl font-bold text-orange-600">0</div>
|
||||
<div class="text-sm text-orange-700 mt-1">Pre-servicios</div>
|
||||
<div class="text-xs text-orange-400">RCXC / SC</div>
|
||||
</div>
|
||||
<div class="bg-emerald-50 rounded-lg p-4 text-center">
|
||||
<div id="card-ventas" class="bg-emerald-50 rounded-lg p-4 text-center transition-opacity">
|
||||
<div id="cnt-ventas" class="text-3xl font-bold text-emerald-600">0</div>
|
||||
<div class="text-sm text-emerald-700 mt-1">Ventas</div>
|
||||
<div class="text-xs text-emerald-400">Ventas/Crear</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-medium text-gray-600 mb-2">Pacientes — clic para ver sus recepciones:</p>
|
||||
|
||||
<!-- Terceros / RDA -->
|
||||
<div id="sec-terceros-rda">
|
||||
<p class="text-xs font-medium text-gray-600 mb-2">Pacientes — clic para ver recepciones:</p>
|
||||
<div id="preview-table" class="border border-gray-100 rounded-lg overflow-hidden text-sm"></div>
|
||||
</div>
|
||||
|
||||
<!-- Pre-servicios -->
|
||||
<div id="preservicios-section" class="hidden">
|
||||
<p class="text-xs font-medium text-orange-600 mb-2 mt-2">Pre-servicios RCXC / SC — clic para ver JSON:</p>
|
||||
<div id="preview-ps-table" class="border border-orange-100 rounded-lg overflow-hidden text-sm"></div>
|
||||
</div>
|
||||
|
||||
<!-- Ventas -->
|
||||
<div id="ventas-section" class="hidden">
|
||||
<p class="text-xs font-medium text-emerald-600 mb-2 mt-2">Facturas Venta — clic para ver JSON:</p>
|
||||
<div id="preview-vta-table" class="border border-emerald-100 rounded-lg overflow-hidden text-sm"></div>
|
||||
@@ -87,56 +113,49 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Progreso -->
|
||||
<div id="progress-section" class="hidden 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 flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-8 h-8 bg-blue-500 rounded-full flex items-center justify-center text-white text-sm font-bold">1</div>
|
||||
<span class="font-semibold text-gray-800">Terceros (pacientes)</span>
|
||||
</div>
|
||||
<span id="p1-badge" class="text-sm text-gray-400">Esperando...</span>
|
||||
<!-- ─── Resultados ────────────────────────────────────────────────────────── -->
|
||||
<div id="results-section" class="hidden space-y-3">
|
||||
|
||||
<!-- Barra de resumen -->
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200 px-5 py-4">
|
||||
<div class="flex items-center gap-3 flex-wrap">
|
||||
<span class="text-xs font-semibold text-gray-500 shrink-0">RESULTADO</span>
|
||||
<div id="res-summary" class="flex flex-wrap gap-2"></div>
|
||||
</div>
|
||||
<div id="p1-detalle" class="divide-y divide-gray-100 hidden"></div>
|
||||
</div>
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200">
|
||||
<div class="px-6 py-4 border-b border-gray-200 flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-8 h-8 bg-purple-500 rounded-full flex items-center justify-center text-white text-sm font-bold">2</div>
|
||||
<span class="font-semibold text-gray-800">RDA Paciente (recepciones)</span>
|
||||
|
||||
<!-- Panel de fallos -->
|
||||
<div id="failures-panel" class="hidden bg-white rounded-xl shadow-sm border border-red-200 overflow-hidden">
|
||||
<button onclick="togglePanel('failures-body','failures-chev')"
|
||||
class="w-full px-5 py-3.5 flex items-center gap-3 text-left bg-red-50 hover:bg-red-100 transition-colors rounded-t-xl border-b border-red-100">
|
||||
<div class="w-6 h-6 bg-red-500 rounded-full flex items-center justify-center text-white text-xs shrink-0">
|
||||
<i class="fas fa-exclamation"></i>
|
||||
</div>
|
||||
<span id="p2-badge" class="text-sm text-gray-400">Esperando...</span>
|
||||
</div>
|
||||
<div id="p2-detalle" class="divide-y divide-gray-100 hidden"></div>
|
||||
<span class="font-semibold text-red-700 flex-1 text-sm" id="failures-title">Fallos</span>
|
||||
<i id="failures-chev" class="fas fa-chevron-down text-red-400 text-xs transition-transform"></i>
|
||||
</button>
|
||||
<div id="failures-body" class="divide-y divide-red-50"></div>
|
||||
</div>
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200">
|
||||
<div class="px-6 py-4 border-b border-gray-200 flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-8 h-8 bg-orange-500 rounded-full flex items-center justify-center text-white text-sm font-bold">3</div>
|
||||
<span class="font-semibold text-gray-800">Pre-servicios RDA (RCXC / SC)</span>
|
||||
|
||||
<!-- Panel de exitosos (colapsado) -->
|
||||
<div id="successes-panel" class="hidden bg-white rounded-xl shadow-sm border border-green-200 overflow-hidden">
|
||||
<button onclick="togglePanel('successes-body','successes-chev')"
|
||||
class="w-full px-5 py-3.5 flex items-center gap-3 text-left hover:bg-green-50 transition-colors rounded-xl border-b border-transparent">
|
||||
<div class="w-6 h-6 bg-green-500 rounded-full flex items-center justify-center text-white text-xs shrink-0">
|
||||
<i class="fas fa-check"></i>
|
||||
</div>
|
||||
<span id="p3-badge" class="text-sm text-gray-400">Esperando...</span>
|
||||
</div>
|
||||
<div id="p3-detalle" class="divide-y divide-gray-100 hidden"></div>
|
||||
</div>
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200">
|
||||
<div class="px-6 py-4 border-b border-gray-200 flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-8 h-8 bg-emerald-500 rounded-full flex items-center justify-center text-white text-sm font-bold">4</div>
|
||||
<span class="font-semibold text-gray-800">Facturas Venta</span>
|
||||
</div>
|
||||
<span id="p4-badge" class="text-sm text-gray-400">Esperando...</span>
|
||||
</div>
|
||||
<div id="p4-detalle" class="divide-y divide-gray-100 hidden"></div>
|
||||
<span class="font-semibold text-green-700 flex-1 text-sm" id="successes-title">Enviados correctamente</span>
|
||||
<i id="successes-chev" class="fas fa-chevron-right text-green-400 text-xs transition-transform"></i>
|
||||
</button>
|
||||
<div id="successes-body" class="hidden divide-y divide-green-50"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal de confirmación -->
|
||||
<!-- ─── Modal de confirmación ────────────────────────────────────────────── -->
|
||||
<div id="modal-confirmar" class="fixed inset-0 z-50 hidden">
|
||||
<div class="absolute inset-0 bg-black/50" onclick="cerrarModalConfirmar()"></div>
|
||||
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full max-w-lg bg-white rounded-xl shadow-2xl">
|
||||
|
||||
<div class="px-6 py-4 border-b border-gray-200 flex items-center justify-between">
|
||||
<h3 class="font-semibold text-gray-800 flex items-center gap-2">
|
||||
<i class="fas fa-shield-alt text-blue-500"></i> Validar envío
|
||||
@@ -145,49 +164,38 @@
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="p-6 space-y-4">
|
||||
<p class="text-sm text-gray-600">
|
||||
Se enviará la siguiente información a <strong>TNS (api.tns.co)</strong> para la fecha
|
||||
<strong id="modal-fecha" class="text-blue-600"></strong>:
|
||||
Se enviará a <strong>TNS</strong> para la fecha <strong id="modal-fecha" class="text-blue-600"></strong>:
|
||||
</p>
|
||||
|
||||
<div class="grid grid-cols-5 gap-3">
|
||||
<div class="bg-blue-50 rounded-lg p-3 text-center">
|
||||
<div id="mc-terceros" class="bg-blue-50 rounded-lg p-3 text-center transition-opacity">
|
||||
<div id="modal-cnt-pac" class="text-2xl font-bold text-blue-600">0</div>
|
||||
<div class="text-xs text-blue-700">Terceros</div>
|
||||
<div class="text-xs text-blue-400">Tercero/Crear</div>
|
||||
</div>
|
||||
<div class="bg-purple-50 rounded-lg p-3 text-center">
|
||||
<div id="mc-rda" class="bg-purple-50 rounded-lg p-3 text-center transition-opacity">
|
||||
<div id="modal-cnt-rec" class="text-2xl font-bold text-purple-600">0</div>
|
||||
<div class="text-xs text-purple-700">Recepciones</div>
|
||||
<div class="text-xs text-purple-400">RdaPaciente/Insertar</div>
|
||||
<div class="text-xs text-purple-700">RDA</div>
|
||||
</div>
|
||||
<div class="bg-green-50 rounded-lg p-3 text-center">
|
||||
<div id="mc-examenes" class="bg-green-50 rounded-lg p-3 text-center transition-opacity">
|
||||
<div id="modal-cnt-exa" class="text-2xl font-bold text-green-600">0</div>
|
||||
<div class="text-xs text-green-700">Exámenes</div>
|
||||
<div class="text-xs text-green-400">detallePedido</div>
|
||||
</div>
|
||||
<div class="bg-orange-50 rounded-lg p-3 text-center">
|
||||
<div id="mc-preserv" class="bg-orange-50 rounded-lg p-3 text-center transition-opacity">
|
||||
<div id="modal-cnt-ps" class="text-2xl font-bold text-orange-600">0</div>
|
||||
<div class="text-xs text-orange-700">Pre-servicios</div>
|
||||
<div class="text-xs text-orange-400">RCXC / SC</div>
|
||||
</div>
|
||||
<div class="bg-emerald-50 rounded-lg p-3 text-center">
|
||||
<div id="mc-ventas" class="bg-emerald-50 rounded-lg p-3 text-center transition-opacity">
|
||||
<div id="modal-cnt-vta" class="text-2xl font-bold text-emerald-600">0</div>
|
||||
<div class="text-xs text-emerald-700">Ventas</div>
|
||||
<div class="text-xs text-emerald-400">Ventas/Crear</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-amber-50 border border-amber-200 rounded-lg px-4 py-3 text-xs text-amber-700 flex items-start gap-2">
|
||||
<i class="fas fa-exclamation-triangle mt-0.5 shrink-0"></i>
|
||||
<span>Esta acción enviará datos reales a TNS. Los registros que ya existan serán actualizados. No se puede deshacer.</span>
|
||||
<span>Esta acción enviará datos reales a TNS. Los registros existentes serán actualizados. No se puede deshacer.</span>
|
||||
</div>
|
||||
|
||||
<div class="max-h-40 overflow-y-auto border border-gray-100 rounded-lg" id="modal-lista-pac"></div>
|
||||
<div class="max-h-36 overflow-y-auto border border-gray-100 rounded-lg" id="modal-lista-pac"></div>
|
||||
</div>
|
||||
|
||||
<div class="px-6 py-4 border-t border-gray-100 flex justify-end gap-3">
|
||||
<button onclick="cerrarModalConfirmar()"
|
||||
class="px-5 py-2 bg-gray-100 text-gray-700 rounded-lg text-sm font-medium hover:bg-gray-200">
|
||||
@@ -205,20 +213,48 @@
|
||||
let _previewData = null;
|
||||
let _rdaJsonStore = {};
|
||||
|
||||
function escHtml(s) {
|
||||
return s.replace(/&/g,'&').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,'>').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 = '<div class="p-4 text-gray-400 text-center">No hay pacientes para esta fecha</div>';
|
||||
} 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 `
|
||||
<div>
|
||||
<div class="flex items-center gap-3 px-6 py-1.5 bg-blue-50 border-t border-blue-100 text-xs cursor-pointer hover:bg-blue-100 select-none"
|
||||
onclick="toggleJsonBlock('${key}','rdachev-${i}-${j}')">
|
||||
<i class="fas fa-chevron-right text-blue-300 text-xs transition-transform" id="rdachev-${i}-${j}"></i>
|
||||
<span class="text-blue-400 w-4"><i class="fas fa-flask"></i></span>
|
||||
<span class="font-mono text-blue-700 w-28">${escHtml(r.factura)}</span>
|
||||
<span class="text-blue-600 w-24">${escHtml(r.fecha)}</span>
|
||||
<span class="font-mono bg-blue-100 text-blue-800 px-1.5 py-0.5 rounded w-14 text-center">${escHtml(r.contrato||'—')}</span>
|
||||
<span class="text-blue-600 w-20">${r.examenes} exam.</span>
|
||||
<span class="text-blue-600 flex-1 truncate">${escHtml(r.diag||'')}</span>
|
||||
<span class="text-blue-700 font-medium">$${Number(r.valor||0).toLocaleString('es-CO')}</span>
|
||||
</div>
|
||||
<div id="${key}" class="hidden bg-gray-900 text-green-300 text-xs font-mono p-4 overflow-x-auto whitespace-pre border-b border-blue-100"></div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
return `
|
||||
<div class="divide-y divide-gray-100">
|
||||
<div class="flex items-center gap-3 px-4 py-2.5 hover:bg-gray-50 cursor-pointer select-none"
|
||||
onclick="toggleSubRow('rda-${i}','chev-${i}')">
|
||||
<i class="fas fa-chevron-right text-gray-300 text-xs transition-transform" id="chev-${i}"></i>
|
||||
<span class="text-gray-400 text-xs w-8">${escHtml(p.tipo)}</span>
|
||||
<span class="text-gray-500 w-32 font-mono text-xs">${escHtml(p.doc)}</span>
|
||||
<span class="text-gray-700 flex-1">${escHtml(p.nombre)}</span>
|
||||
<span class="text-xs ${p.rda&&p.rda.length>0?'text-blue-600':'text-gray-300'}">${(p.rda||[]).length} RDA</span>
|
||||
</div>
|
||||
<div id="rda-${i}" class="hidden">${rdaRows||'<div class="px-6 py-2 text-xs text-gray-400 bg-gray-50">Sin recepciones</div>'}</div>
|
||||
</div>`;
|
||||
}).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 `
|
||||
<div>
|
||||
<div class="flex items-center gap-3 px-4 py-2.5 hover:bg-orange-50 cursor-pointer select-none border-b border-orange-100"
|
||||
onclick="togglePs('${key}', this)">
|
||||
<i class="fas fa-chevron-right text-orange-300 text-xs transition-transform duration-150" id="pschev-${i}"></i>
|
||||
<span class="font-mono text-orange-700 font-medium w-32">${ps.factura}</span>
|
||||
<span class="text-gray-500 w-24 text-xs">${ps.fecha}</span>
|
||||
<span class="text-gray-600 flex-1 text-xs">Pac. ${ps.paciente}</span>
|
||||
onclick="toggleJsonBlock('${key}','pschev-${i}')">
|
||||
<i class="fas fa-chevron-right text-orange-300 text-xs transition-transform" id="pschev-${i}"></i>
|
||||
<span class="font-mono text-orange-700 font-medium w-32">${escHtml(ps.factura)}</span>
|
||||
<span class="text-gray-500 w-24 text-xs">${escHtml(ps.fecha)}</span>
|
||||
<span class="text-gray-600 flex-1 text-xs">Pac. ${escHtml(String(ps.paciente))}</span>
|
||||
<span class="text-xs text-orange-500">${ps.examenes} examen(es)</span>
|
||||
</div>
|
||||
<div id="${key}" class="hidden bg-gray-900 text-green-300 text-xs font-mono p-4 overflow-x-auto whitespace-pre border-b border-orange-100"></div>
|
||||
@@ -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 `
|
||||
<div>
|
||||
<div class="flex items-center gap-3 px-4 py-2.5 hover:bg-emerald-50 cursor-pointer select-none border-b border-emerald-100"
|
||||
onclick="toggleVta('${key}', this)">
|
||||
<i class="fas fa-chevron-right text-emerald-300 text-xs transition-transform duration-150" id="vtachev-${i}"></i>
|
||||
<span class="font-mono text-emerald-700 font-medium w-32">${vta.factura}</span>
|
||||
<span class="text-gray-500 w-24 text-xs">${vta.fecha}</span>
|
||||
<span class="font-mono bg-emerald-100 text-emerald-800 px-1.5 py-0.5 rounded text-xs w-14 text-center">${vta.contrato || '—'}</span>
|
||||
<span class="text-gray-600 flex-1 text-xs">Pac. ${vta.paciente}</span>
|
||||
onclick="toggleJsonBlock('${key}','vtachev-${i}')">
|
||||
<i class="fas fa-chevron-right text-emerald-300 text-xs transition-transform" id="vtachev-${i}"></i>
|
||||
<span class="font-mono text-emerald-700 font-medium w-32">${escHtml(vta.factura)}</span>
|
||||
<span class="text-gray-500 w-24 text-xs">${escHtml(vta.fecha)}</span>
|
||||
<span class="font-mono bg-emerald-100 text-emerald-800 px-1.5 py-0.5 rounded text-xs w-14 text-center">${escHtml(vta.contrato||'—')}</span>
|
||||
<span class="text-gray-600 flex-1 text-xs">Pac. ${escHtml(String(vta.paciente))}</span>
|
||||
<span class="text-xs text-emerald-500">${vta.examenes} examen(es)</span>
|
||||
</div>
|
||||
<div id="${key}" class="hidden bg-gray-900 text-green-300 text-xs font-mono p-4 overflow-x-auto whitespace-pre border-b border-emerald-100"></div>
|
||||
@@ -290,73 +380,37 @@ async function previewAutomation() {
|
||||
vtaSection.classList.add('hidden');
|
||||
}
|
||||
|
||||
const table = document.getElementById('preview-table');
|
||||
if (data.pacientes === 0) {
|
||||
table.innerHTML = '<div class="p-4 text-gray-400 text-center">No hay pacientes para esta fecha</div>';
|
||||
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 `
|
||||
<div class="divide-y divide-blue-100">
|
||||
<div class="flex items-center gap-3 px-6 py-1.5 bg-blue-50 border-t border-blue-100 text-xs cursor-pointer hover:bg-blue-100 select-none"
|
||||
onclick="toggleRdaJson('${key}', this)">
|
||||
<i class="fas fa-chevron-right text-blue-300 text-xs transition-transform duration-150" id="rdachev-${i}-${j}"></i>
|
||||
<span class="text-blue-400 w-4"><i class="fas fa-flask"></i></span>
|
||||
<span class="font-mono text-blue-700 w-28">${r.factura}</span>
|
||||
<span class="text-blue-600 w-24">${r.fecha}</span>
|
||||
<span class="font-mono bg-blue-100 text-blue-800 px-1.5 py-0.5 rounded w-14 text-center">${r.contrato || '—'}</span>
|
||||
<span class="text-blue-600 w-20">${r.examenes} examen(es)</span>
|
||||
<span class="text-blue-600 flex-1">${escHtml(r.diag||'')}</span>
|
||||
<span class="text-blue-700 font-medium">$${Number(r.valor).toLocaleString('es-CO')}</span>
|
||||
</div>
|
||||
<div id="${key}" class="hidden bg-gray-900 text-green-300 text-xs font-mono p-4 overflow-x-auto whitespace-pre"></div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
return `
|
||||
<div class="divide-y divide-gray-100">
|
||||
<div class="flex items-center gap-3 px-4 py-2.5 hover:bg-gray-50 cursor-pointer select-none"
|
||||
onclick="toggleRda('rda-${i}', this)">
|
||||
<i class="fas fa-chevron-right text-gray-300 text-xs transition-transform duration-150" id="chev-${i}"></i>
|
||||
<span class="text-gray-400 text-xs w-8">${p.tipo}</span>
|
||||
<span class="text-gray-500 w-32 font-mono text-xs">${p.doc}</span>
|
||||
<span class="text-gray-700 flex-1">${p.nombre}</span>
|
||||
<span class="text-xs ${p.rda.length > 0 ? 'text-blue-600' : 'text-gray-300'}">${p.rda.length} RDA</span>
|
||||
</div>
|
||||
<div id="rda-${i}" class="hidden">${rdaRows || '<div class="px-6 py-2 text-xs text-gray-400 bg-gray-50">Sin recepciones</div>'}</div>
|
||||
</div>`;
|
||||
}).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, '<i class="fas fa-eye"></i> 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 => `
|
||||
<div class="flex items-center gap-3 px-3 py-1.5 border-b border-gray-50 text-xs last:border-0">
|
||||
<span class="text-gray-400 w-6">${p.tipo}</span>
|
||||
<span class="font-mono text-gray-500 w-28">${p.doc}</span>
|
||||
<span class="text-gray-700 flex-1 truncate">${p.nombre}</span>
|
||||
<span class="text-blue-500">${p.rda.length} RDA</span>
|
||||
<span class="text-gray-400 w-6">${escHtml(p.tipo)}</span>
|
||||
<span class="font-mono text-gray-500 w-28">${escHtml(p.doc)}</span>
|
||||
<span class="text-gray-700 flex-1 truncate">${escHtml(p.nombre)}</span>
|
||||
<span class="text-blue-500">${(p.rda||[]).length} RDA</span>
|
||||
</div>`).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, '<i class="fas fa-play"></i> Ejecutar envío');
|
||||
hideLoading(btn, '<i class="fas fa-play"></i> 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, '<i class="fas fa-play"></i> Ejecutar envío');
|
||||
renderResults(data.resultado);
|
||||
} catch(e) {
|
||||
hideLoading(btn, '<i class="fas fa-play"></i> 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 `<span class="${_COLOR_SUM[s.color]} rounded-full px-3 py-1 text-xs font-semibold inline-flex items-center gap-1.5">
|
||||
${s.label}
|
||||
<span class="text-green-700 font-bold">✓${d.enviados}</span>
|
||||
${d.errores > 0 ? `<span class="text-red-600 font-bold">✗${d.errores}</span>` : ''}
|
||||
</span>`;
|
||||
}).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 `
|
||||
<div data-resend-row class="px-5 py-3 flex items-start gap-3 bg-red-50 border-b border-red-100 last:border-0">
|
||||
<span class="text-red-400 mt-0.5 shrink-0 text-base fail-status-icon"><i class="fas fa-times-circle"></i></span>
|
||||
<span class="${_COLOR_BADGE[s.color]} text-xs font-medium px-2 py-0.5 rounded shrink-0 mt-0.5">${s.label}</span>
|
||||
<span class="font-mono text-gray-700 text-xs w-28 shrink-0 mt-0.5 truncate">${escHtml(label1)}</span>
|
||||
<span class="text-gray-600 text-xs flex-1 mt-0.5">${escHtml(label2)}</span>
|
||||
<span class="text-red-500 text-xs max-w-[200px] truncate mt-0.5 fail-msg" title="${escHtml(item.msg||'')}">${escHtml((item.msg||'Sin detalle').slice(0,70))}</span>
|
||||
<button id="rbtn-${idx}"
|
||||
data-tipo="${dtype}" data-id="${did}"
|
||||
onclick="reenviarItem(this)"
|
||||
class="shrink-0 flex items-center gap-1.5 px-2.5 py-1.5 bg-white border border-gray-200 hover:bg-blue-50 hover:border-blue-300 text-xs text-gray-600 hover:text-blue-700 rounded-lg transition-colors font-medium">
|
||||
<i class="fas fa-redo-alt text-xs"></i> Reenviar
|
||||
</button>
|
||||
</div>`;
|
||||
}).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 `
|
||||
<div class="px-5 py-2.5 flex items-center gap-3 border-b border-green-50 last:border-0">
|
||||
<span class="text-green-500 shrink-0"><i class="fas fa-check-circle"></i></span>
|
||||
<span class="${_COLOR_BADGE[s.color]} text-xs font-medium px-2 py-0.5 rounded shrink-0">${s.label}</span>
|
||||
<span class="font-mono text-gray-700 text-xs w-28 shrink-0 truncate">${escHtml(label1)}</span>
|
||||
<span class="text-gray-500 text-xs flex-1 truncate">${escHtml(label2)}</span>
|
||||
<span class="text-green-600 text-xs truncate max-w-xs">${escHtml((item.msg||'').slice(0,60))}</span>
|
||||
</div>`;
|
||||
}).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 = '<i class="fas fa-spinner fa-spin text-xs"></i>';
|
||||
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 = '<i class="fas fa-check-circle text-green-500"></i>';
|
||||
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 = '<i class="fas fa-check text-green-500 text-xs"></i> 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 => `
|
||||
<div class="px-6 py-2 flex items-center gap-3 text-sm ${i.ok ? '' : 'bg-red-50'}">
|
||||
<span class="text-gray-500 w-28 shrink-0">${i.col1}</span>
|
||||
<span class="text-gray-700 flex-1 truncate">${i.col2}</span>
|
||||
<span class="text-xs ${i.ok ? 'text-green-600' : 'text-red-600'} shrink-0 max-w-xs truncate">${i.col3}</span>
|
||||
</div>`).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)' : '';
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user