feat: paso 4 ventas en automation + fixes varios

- Automation: agrega Paso 4 (Ventas/Crear) con fetch _SQL_VENTAS,
  filtro por excluded_ventas_set, loop de envío y log de actividad
- Automation: _guardar_envio ahora almacena idrecepcion y contrato
  para que /ventas reconozca registros enviados por automation
- Contratos: corrige NameError en toggle_excluir (faltaba request: Request)
- Ventas: agrega check if not q en send_one y send para evitar TypeError
- Ventas HTML: agrega columna Contrato en la tabla de facturas
- json_generator: normaliza etnia >2 chars a '99' (ej: 999 → 99)
- json_generator: agrega campo ciudadExp usando el mismo cod_ciudad

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-07-11 14:29:39 -05:00
co-authored by Claude Sonnet 4.6
parent 21eb99d18d
commit 73062af8bc
7 changed files with 249 additions and 68 deletions
+114 -9
View File
@@ -11,10 +11,11 @@ from app.services.json_generator import (
generar_tercero_api,
generar_rda_paciente,
agrupar_por_recepcion,
generar_factura_venta,
)
from app.services.api_client import get_tns_token, TNS_BASE
from app.services.whatsapp_sync import sync_paciente, sync_todos, guardar_sync_log
from app.routes.contratos import load_contrato_map, load_excluded_set, load_sin_contrato_set
from app.routes.contratos import load_contrato_map, load_excluded_set, load_sin_contrato_set, load_excluded_ventas_set
from app.utils.activity import log_activity, get_ip
router = APIRouter(prefix="/automation", tags=["automation"])
@@ -152,6 +153,31 @@ ORDER BY ps.ID_PS, rel.COD_EXAMEN
"""
_SQL_VENTAS = """
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.FECHA_RECEPCION BETWEEN :fecha_ini AND :fecha_fin
ORDER BY r.IDRECEPCION
"""
def _agrupar_por_presserv(rows: list) -> dict:
"""Agrupa filas de pre-servicios por ID_PS."""
from collections import defaultdict
@@ -267,11 +293,17 @@ async def preview_automation(
return JSONResponse({"success": False, "message": f"Error BD RDA: {err2}"})
ok3, err3, rows_ps = fb.execute_query(_SQL_PRESERV, {"fecha_ini": fecha_ini, "fecha_fin": fecha_fin})
fb.disconnect()
if not ok3:
fb.disconnect()
return JSONResponse({"success": False, "message": f"Error BD Pre-servicios: {err3}"})
ok4, err4, rows_vta = fb.execute_query(_SQL_VENTAS, {"fecha_ini": fecha_ini, "fecha_fin": fecha_fin})
fb.disconnect()
if not ok4:
return JSONResponse({"success": False, "message": f"Error BD Ventas: {err4}"})
excluded = load_excluded_set()
excluded_ventas = load_excluded_ventas_set()
grupos_all = agrupar_por_recepcion(rows_rda)
grupos = {k: v for k, v in grupos_all.items()
@@ -345,6 +377,25 @@ async def preview_automation(
if rda_por_pac.get(str(p.get("CODIGO", "")))
]
grupos_vta_all = agrupar_por_recepcion(rows_vta)
grupos_vta = {k: v for k, v in grupos_vta_all.items()
if str(v[0].get("CODCONTRATO") or "").strip() not in excluded_ventas}
ventas_preview = []
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)
ventas_preview.append({
"factura": f"{prefijo}-{num_fac}",
"paciente": str(grupo_rows[0].get("COD_PACIENTE", "")),
"fecha": str(grupo_rows[0].get("FECHA_RECEPCION", ""))[:10],
"contrato": str(grupo_rows[0].get("CODCONTRATO") or "").strip(),
"examenes": len(grupo_rows),
"json": venta_json,
})
return JSONResponse({
"success": True,
"fecha": fecha,
@@ -353,8 +404,10 @@ async def preview_automation(
"examenes": total_examenes,
"excluidos": excluidos_count,
"preservicios": len(grupos_ps),
"ventas": len(grupos_vta),
"pacientes_preview": pacientes_preview,
"preservicios_preview": preservicios_preview,
"ventas_preview": ventas_preview,
})
@@ -401,15 +454,21 @@ async def run_automation(
# ── Pacientes EPS (para registrar terceros en paso 1) ─────────────────────
ok4, err4, rows_pac_eps = fb.execute_query(_SQL_PACIENTES_EPS, {"fecha_ini": fecha_ini, "fecha_fin": fecha_fin})
fb.disconnect()
if not ok4:
fb.disconnect()
return JSONResponse({"success": False, "message": f"Error Firebird pacientes EPS: {err4}"})
# ── Ventas (Factura Venta) ────────────────────────────────────────────────
ok5, err5, rows_vta = fb.execute_query(_SQL_VENTAS, {"fecha_ini": fecha_ini, "fecha_fin": fecha_fin})
fb.disconnect()
if not ok5:
return JSONResponse({"success": False, "message": f"Error Firebird Ventas: {err5}"})
# Fusionar pacientes: particulares + EPS, deduplicando por CODIGO
codigos_vistos = {str(r.get("CODIGO")) for r in rows_pac}
rows_pac_todos = list(rows_pac) + [r for r in rows_pac_eps if str(r.get("CODIGO")) not in codigos_vistos]
if not rows_pac_todos and not rows_rda and not rows_ps:
if not rows_pac_todos and not rows_rda and not rows_ps and not rows_vta:
return JSONResponse({"success": False, "message": f"No hay datos para la fecha {fecha}"})
# ── Login TNS ─────────────────────────────────────────────────────────────
@@ -430,6 +489,7 @@ async def run_automation(
"paso1_terceros": {"enviados": 0, "errores": 0, "detalle": []},
"paso2_rda": {"enviados": 0, "errores": 0, "detalle": []},
"paso3_preserv": {"enviados": 0, "errores": 0, "detalle": []},
"paso4_ventas": {"enviados": 0, "errores": 0, "detalle": []},
}
# ── PASO 1: Enviar Terceros (particulares + EPS) ─────────────────────────
@@ -558,27 +618,72 @@ async def run_automation(
fecha_inicio=fecha, fecha_fin=fecha,
servicios=len(grupo_rows))
# ── PASO 4: Enviar Facturas Venta ─────────────────────────────────────────
excluded_ventas = load_excluded_ventas_set()
grupos_vta_all = agrupar_por_recepcion(rows_vta)
grupos_vta = {k: v for k, v in grupos_vta_all.items()
if str(v[0].get("CODCONTRATO") or "").strip() not in excluded_ventas}
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 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,
})
_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"]
r3 = resultado["paso3_preserv"]
r4 = resultado["paso4_ventas"]
log_activity(user["user_id"], user["username"], "automation_run",
f"Fecha {fecha} | Terceros: {r1['enviados']} OK/{r1['errores']} err | "
f"RDA: {r2['enviados']} OK/{r2['errores']} err | "
f"PreServ: {r3['enviados']} OK/{r3['errores']} err",
f"PreServ: {r3['enviados']} OK/{r3['errores']} err | "
f"Ventas: {r4['enviados']} OK/{r4['errores']} err",
get_ip(request))
return JSONResponse({"success": True, "resultado": resultado})
def _guardar_envio(user_id, tipo, factura, json_data, respuesta, ok,
fecha_inicio=None, fecha_fin=None, servicios=0):
fecha_inicio=None, fecha_fin=None, servicios=0,
idrecepcion=None, contrato=None):
try:
conn = get_connection()
conn.execute("""
INSERT INTO envios (user_id, tipo, factura, fecha_inicio, fecha_fin,
INSERT INTO envios (user_id, tipo, factura, idrecepcion, contrato,
fecha_inicio, fecha_fin,
pacientes_count, servicios_count, status, json_enviado, respuesta_api, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (
user_id, tipo, factura, fecha_inicio, fecha_fin,
user_id, tipo, factura, idrecepcion, contrato,
fecha_inicio, fecha_fin,
1, servicios,
"success" if ok else "error",
json_lib.dumps(json_data, indent=2, ensure_ascii=False)[:10000],
+46 -45
View File
@@ -7,52 +7,53 @@ from app.utils.activity import log_activity, get_ip
router = APIRouter(prefix="/contratos", tags=["contratos"])
# (numero_contrato, nit_empresa, tipo_usuario, descripcion, excluir_rda, sin_contrato, excluir_ventas)
# excluir_ventas=0 → activo para Ventas/Crear | excluir_ventas=1 → bloqueado para ventas
_SEED = [
("001", "860078828", "11", "EPS SANITAS", 0, 0, 0),
("002", "830054904", "11", "EPS COMPENSAR", 0, 0, 0),
("003", "900278729", "12", "PARTICULAR", 0, 0, 0),
("004", "800106339", "11", "EPS NUEVA EPS", 0, 0, 0),
("005", "800153424", "11", "EPS SURA", 0, 0, 0),
("006", "805009741", "11", "EPS COOMEVA", 0, 0, 0),
("007", "860002183", "11", "EPS FAMISANAR", 0, 0, 0),
("008", "860002503", "11", "EPS CRUZ BLANCA", 0, 0, 0),
("009", "860027404", "11", "EPS COLSANITAS", 0, 0, 0),
("010", "860039988", "11", "EPS SALUD TOTAL", 0, 0, 0),
("011", "890903790", "11", "EPS SAVIA SALUD", 0, 0, 0),
("012", "900178724", "11", "EPS MUTUAL SER", 0, 0, 0),
("034", "860078828", "11", "EPS SANITAS (alt)", 0, 0, 0),
("035", "860078828", "11", "EPS SANITAS (alt)", 0, 0, 0),
("036", "860078828", "11", "EPS SANITAS (alt)", 0, 0, 0),
("20062026", "800182856", "01", "SUBSIDIADO", 0, 0, 0),
("CW225489", "899999068", "07", "POLIZA / SEGURO", 0, 0, 0),
# Contratos excluidos del envío TNS
("013", "", "11", "MEDILAVORO S.A.S", 0, 0, 0),
("014", "", "11", "LABORATORIO UROCLINICO",0, 0, 0),
("015", "", "11", "ANDRES AFANADOR VILLAMIZAR", 1, 0, 0),
("016", "", "11", "OMAR FERNANDO RIBERO GOMEZ", 1, 0, 0),
("017", "", "11", "CLAUDIA BELEN JULIO SEPULVEDA", 1, 0, 0),
("018", "", "11", "LABORATORIO MICROBIOLOGICO - MARGIE OJEDA", 1, 0, 0),
("019", "", "11", "LABORATORIO TOXICOLOGICO - MARTHA MORALES", 1, 0, 0),
("020", "", "11", "MARTHA LUCIA GALLARDO", 1, 0, 0),
("021", "", "11", "LABORATORIO VILMA OROZCO AYALA", 1, 0, 0),
("022", "", "11", "CLINICA SAN JOSE DE CUCUTA", 1, 0, 0),
("023", "", "11", "URONORTE S.A", 1, 0, 0),
("024", "", "11", "LABORATORIO CLINICO BIOLAB S.A.S", 1, 0, 0),
("025", "", "11", "JOEL LEONARDO CARRILLO CORREDOR", 1, 0, 0),
("026", "", "11", "ROLANDO IVAN PENARANDA DEVIA", 1, 0, 0),
("027", "", "11", "JOSE WILMER GARCIA CALDERON", 1, 0, 0),
("028", "", "11", "ONCOMEDICAL IPS S.A.S", 1, 0, 0),
("029", "", "11", "GENETIX S.A.S", 1, 0, 0),
("030", "", "11", "NORFETUS S.A.S", 1, 0, 0),
("031", "", "11", "TBTB GLOBAL LAB S.A.S", 1, 0, 0),
("032", "", "11", "COLGENES S.A.S", 1, 0, 0),
("033", "", "11", "IPS FIGURAS SPA CUCUTA S.A.S", 1, 0, 0),
("037", "", "11", "CLINICA URGENCIAS LA MERCED", 1, 0, 0),
("038", "", "11", "CLINICA COLSANITAS S.A.", 1, 0, 0),
("039", "", "11", "GOMEZ GIL JOSE JESUS", 1, 0, 0),
("001", "860078828", "11", "EPS SANITAS", 0, 0, 1),
("002", "830054904", "11", "EPS COMPENSAR", 0, 0, 1),
("003", "900278729", "12", "PARTICULAR", 0, 0, 1),
("004", "800106339", "11", "EPS NUEVA EPS", 0, 0, 1),
("005", "800153424", "11", "EPS SURA", 0, 0, 1),
("006", "805009741", "11", "EPS COOMEVA", 0, 0, 1),
("007", "860002183", "11", "EPS FAMISANAR", 0, 0, 1),
("008", "860002503", "11", "EPS CRUZ BLANCA", 0, 0, 1),
("009", "860027404", "11", "EPS COLSANITAS", 0, 0, 1),
("010", "860039988", "11", "EPS SALUD TOTAL", 0, 0, 1),
("011", "890903790", "11", "EPS SAVIA SALUD", 0, 0, 1),
("012", "900178724", "11", "EPS MUTUAL SER", 0, 0, 1),
("034", "860078828", "11", "EPS SANITAS (alt)", 0, 0, 1),
("035", "860078828", "11", "EPS SANITAS (alt)", 0, 0, 1),
("036", "860078828", "11", "EPS SANITAS (alt)", 0, 0, 1),
("20062026", "800182856", "01", "SUBSIDIADO", 0, 0, 1),
("CW225489", "899999068", "07", "POLIZA / SEGURO", 0, 0, 1),
# Contratos excluidos del envío RDA
("013", "", "11", "MEDILAVORO S.A.S", 0, 0, 1),
("014", "", "11", "LABORATORIO UROCLINICO",0, 0, 1),
("015", "", "11", "ANDRES AFANADOR VILLAMIZAR", 1, 0, 1),
("016", "", "11", "OMAR FERNANDO RIBERO GOMEZ", 1, 0, 1),
("017", "", "11", "CLAUDIA BELEN JULIO SEPULVEDA", 1, 0, 1),
("018", "", "11", "LABORATORIO MICROBIOLOGICO - MARGIE OJEDA", 1, 0, 1),
("019", "", "11", "LABORATORIO TOXICOLOGICO - MARTHA MORALES", 1, 0, 1),
("020", "", "11", "MARTHA LUCIA GALLARDO", 1, 0, 1),
("021", "", "11", "LABORATORIO VILMA OROZCO AYALA", 1, 0, 1),
("022", "", "11", "CLINICA SAN JOSE DE CUCUTA", 1, 0, 1),
("023", "", "11", "URONORTE S.A", 1, 0, 1),
("024", "", "11", "LABORATORIO CLINICO BIOLAB S.A.S", 1, 0, 1),
("025", "", "11", "JOEL LEONARDO CARRILLO CORREDOR", 1, 0, 1),
("026", "", "11", "ROLANDO IVAN PENARANDA DEVIA", 1, 0, 1),
("027", "", "11", "JOSE WILMER GARCIA CALDERON", 1, 0, 1),
("028", "", "11", "ONCOMEDICAL IPS S.A.S", 1, 0, 1),
("029", "", "11", "GENETIX S.A.S", 1, 0, 1),
("030", "", "11", "NORFETUS S.A.S", 1, 0, 1),
("031", "", "11", "TBTB GLOBAL LAB S.A.S", 1, 0, 1),
("032", "", "11", "COLGENES S.A.S", 1, 0, 1),
("033", "", "11", "IPS FIGURAS SPA CUCUTA S.A.S", 1, 0, 1),
("037", "", "11", "CLINICA URGENCIAS LA MERCED", 1, 0, 1),
("038", "", "11", "CLINICA COLSANITAS S.A.", 1, 0, 1),
("039", "", "11", "GOMEZ GIL JOSE JESUS", 1, 0, 1),
("040", "", "11", "MARTHA LILIANA SALGAR GALLEGO", 1, 1, 0),
("041", "", "11", "GENCELL PHARMA S.A.S", 1, 0, 0),
("042", "", "07", "AXA COLPATRIA HYC", 1, 0, 0),
("041", "", "11", "GENCELL PHARMA S.A.S", 1, 0, 1),
("042", "", "07", "AXA COLPATRIA HYC", 1, 0, 1),
]
@@ -200,7 +201,7 @@ async def contrato_update(
@router.post("/toggle-excluir/{contrato_id}")
async def toggle_excluir(contrato_id: int, user: dict = Depends(get_current_user)):
async def toggle_excluir(contrato_id: int, request: Request, user: dict = Depends(get_current_user)):
conn = get_connection()
conn.execute(
"UPDATE contratos SET excluir = CASE WHEN excluir=1 THEN 0 ELSE 1 END WHERE id=?",
+7 -9
View File
@@ -188,7 +188,7 @@ ORDER BY ps.ID_PS, rel.COD_EXAMEN""",
"description": "Pre-servicio por número PS (buscar RCXC05291 → escribe solo el número)"
},
{
"name": "Factura Venta 040 por fecha",
"name": "Factura Venta por fecha",
"query_type": "ventas",
"query_text": """SELECT
r.IDRECEPCION,
@@ -203,19 +203,18 @@ ORDER BY ps.ID_PS, 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,
e.CODCONTRATO
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.FECHA_RECEPCION BETWEEN :fecha_ini AND :fecha_fin
AND TRIM(e.CODCONTRATO) = '040'
ORDER BY r.IDRECEPCION""",
"description": "Facturas de venta contrato 040 en un rango de fechas"
"description": "Facturas de venta en un rango de fechas (filtra por excluir_ventas en /contratos)"
},
{
"name": "Factura Venta 040 por número",
"name": "Factura Venta por número",
"query_type": "ventas",
"query_text": """SELECT
r.IDRECEPCION,
@@ -230,15 +229,14 @@ ORDER BY r.IDRECEPCION""",
COALESCE(NULLIF(TRIM(ex.NUM_ISS), ''), TRIM(rel.COD_EXAMEN)) AS CUPS,
rel.PRECIO,
COALESCE(t.VALOR, rel.PRECIO) AS PRECIO_TARIFA,
e.CODCONTRATO
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.NUM_FACTURA = :num_factura
AND TRIM(e.CODCONTRATO) = '040'""",
"description": "Factura de venta contrato 040 por número de factura"
WHERE r.NUM_FACTURA = :num_factura""",
"description": "Factura de venta por número (filtra por excluir_ventas en /contratos)"
},
]
+4
View File
@@ -188,6 +188,8 @@ async def send_one(
q = conn.execute("SELECT * FROM queries WHERE id = ?", (query_id,)).fetchone()
cfg = {r["key"]: r["value"] for r in conn.execute("SELECT * FROM config").fetchall()}
conn.close()
if not q:
return JSONResponse({"success": False, "message": "Consulta no encontrada"})
rows, err = _query_rows(cfg, q["query_text"], factura, fecha_inicio, fecha_fin)
if rows is None:
@@ -243,6 +245,8 @@ async def send_ventas(
q = conn.execute("SELECT * FROM queries WHERE id = ?", (query_id,)).fetchone()
cfg = {r["key"]: r["value"] for r in conn.execute("SELECT * FROM config").fetchall()}
conn.close()
if not q:
return JSONResponse({"success": False, "message": "Consulta no encontrada"})
rows, err = _query_rows(cfg, q["query_text"], factura, fecha_inicio, fecha_fin)
if rows is None:
+2 -1
View File
@@ -106,7 +106,8 @@ def generar_tercero_api(row: dict) -> dict:
"sexo": sexo,
"identidadGenero": identidad_genero,
"zona": zona,
"etnia": str(row.get("CODETNIA") or "99").strip(),
"etnia": (lambda v: v if v and len(v) <= 2 else "99")(str(row.get("CODETNIA") or "").strip()),
"ciudadExp": cod_ciudad,
"antecedentes": "",
"cliente": "S",
"enfermedadCronica": False,
+73 -3
View File
@@ -48,7 +48,7 @@
<span id="preview-fecha" class="text-sm text-gray-500"></span>
</div>
<div class="p-6 space-y-4">
<div class="grid grid-cols-4 gap-4">
<div class="grid grid-cols-5 gap-4">
<div class="bg-blue-50 rounded-lg p-4 text-center">
<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>
@@ -66,6 +66,11 @@
<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="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>
@@ -75,6 +80,10 @@
<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>
<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>
</div>
</div>
</div>
@@ -110,6 +119,16 @@
</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>
</div>
</div>
</div>
@@ -133,7 +152,7 @@
<strong id="modal-fecha" class="text-blue-600"></strong>:
</p>
<div class="grid grid-cols-4 gap-3">
<div class="grid grid-cols-5 gap-3">
<div class="bg-blue-50 rounded-lg p-3 text-center">
<div id="modal-cnt-pac" class="text-2xl font-bold text-blue-600">0</div>
<div class="text-xs text-blue-700">Terceros</div>
@@ -154,6 +173,11 @@
<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="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">
@@ -214,6 +238,7 @@ async function previewAutomation() {
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');
@@ -240,6 +265,31 @@ async function previewAutomation() {
psSection.classList.add('hidden');
}
const vtaTable = document.getElementById('preview-vta-table');
const vtaSection = document.getElementById('ventas-section');
if (data.ventas_preview && data.ventas_preview.length > 0) {
vtaTable.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>
<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>
</div>`;
}).join('');
vtaSection.classList.remove('hidden');
} else {
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>';
@@ -297,6 +347,7 @@ function abrirModalConfirmar() {
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 => `
<div class="flex items-center gap-3 px-3 py-1.5 border-b border-gray-50 text-xs last:border-0">
@@ -327,6 +378,8 @@ async function confirmarEnvio() {
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';
const contrato = document.getElementById('contrato-input').value.trim();
const form = new FormData();
@@ -364,8 +417,14 @@ async function confirmarEnvio() {
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;
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) {
@@ -405,6 +464,17 @@ function togglePs(id, row) {
if (chev) chev.style.transform = hidden ? 'rotate(90deg)' : '';
}
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)' : '';
}
function renderBadge(id, enviados, errores) {
const el = document.getElementById(id);
if (errores === 0) {
+3 -1
View File
@@ -178,6 +178,7 @@ function renderTabla() {
<th class="pb-2 font-medium pr-3">IDRECEP.</th>
<th class="pb-2 font-medium pr-3">Factura</th>
<th class="pb-2 font-medium pr-3">Paciente</th>
<th class="pb-2 font-medium pr-3">Contrato</th>
<th class="pb-2 font-medium pr-3">Exámenes</th>
<th class="pb-2 font-medium pr-3">Estado</th>
<th class="pb-2 font-medium"></th>
@@ -217,7 +218,8 @@ function filaHtml(item) {
<td class="py-2 pr-3 font-mono text-gray-600">${id}</td>
<td class="py-2 pr-3 text-gray-600">${escHtml(String(item.factura || '-'))}</td>
<td class="py-2 pr-3 text-gray-600">${escHtml(String(item.paciente || '-'))}</td>
<td class="py-2 pr-3 text-gray-500 max-w-[200px] truncate" title="${escAttr(examsStr)}">${escHtml(examsStr)}</td>
<td class="py-2 pr-3"><span class="px-1.5 py-0.5 bg-emerald-100 text-emerald-700 rounded text-xs font-mono">${escHtml(String(item.contrato || '-'))}</span></td>
<td class="py-2 pr-3 text-gray-500 max-w-[180px] truncate" title="${escAttr(examsStr)}">${escHtml(examsStr)}</td>
<td id="estado-${id}" class="py-2 pr-3">${badgeHtml}</td>
<td class="py-2 flex gap-1 items-center">
<button onclick="verDetalle(${id})" title="Ver JSON" class="px-2 py-1 bg-gray-100 hover:bg-gray-200 text-gray-600 rounded text-xs"><i class="fas fa-code"></i></button>