feat: módulos de envío TNS/ERP Lab + sync de médico, empresa y valor total
- Hub /envios/tns con 5 sub-tabs (Terceros, Transacción RIPS, Facturas Venta, Prueba RDA, Automatización) via iframe lazy - Hub /envios/erp con sub-tabs Pacientes y Sync Automático (historial scheduler) - Modo embed (?embed=1) en base.html para cargar páginas sin sidebar/header dentro de iframes - Sidebar simplificado: 6 ítems individuales reemplazados por módulos TNS y ERP Lab - Scheduler y endpoint /pacientes/examenes ahora incluyen médico ordenante (DOCIDMEDICO), empresa/EPS (NIT_EMPRESA) y valor total (VALORTOTAL) desde Firebird Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
31c0b71c90
commit
8e27225326
@@ -0,0 +1,26 @@
|
||||
from fastapi import APIRouter, Request, Depends
|
||||
from app.auth import get_current_user
|
||||
from app.database import get_connection
|
||||
|
||||
router = APIRouter(prefix="/envios", tags=["envios"])
|
||||
|
||||
|
||||
@router.get("/tns")
|
||||
async def envios_tns(request: Request, user: dict = Depends(get_current_user)):
|
||||
return request.app.state.templates.TemplateResponse("envios_tns.html", {
|
||||
"request": request, "user": user,
|
||||
})
|
||||
|
||||
|
||||
@router.get("/erp")
|
||||
async def envios_erp(request: Request, user: dict = Depends(get_current_user)):
|
||||
conn = get_connection()
|
||||
configs = {row["key"]: row["value"] for row in conn.execute("SELECT * FROM config").fetchall()}
|
||||
conn.close()
|
||||
wa_url = configs.get("whatsapp_url", "")
|
||||
wa_key = configs.get("whatsapp_api_key", "")
|
||||
return request.app.state.templates.TemplateResponse("envios_erp.html", {
|
||||
"request": request, "user": user,
|
||||
"wa_configurado": bool(wa_url and wa_key),
|
||||
"wa_url": wa_url,
|
||||
})
|
||||
+25
-13
@@ -39,14 +39,21 @@ _SQL_EXAMENES_CEDULA = """
|
||||
SELECT
|
||||
r.IDRECEPCION,
|
||||
r.HORAINICIORECEPCION,
|
||||
TRIM(rel.COD_EXAMEN) AS COD_EXAMEN,
|
||||
TRIM(ex.NOMBRE) AS NOM_EXAMEN,
|
||||
COALESCE(NULLIF(TRIM(ex.NUM_ISS), ''), TRIM(rel.COD_EXAMEN)) AS CUPS,
|
||||
rel.PRECIO
|
||||
TRIM(rel.COD_EXAMEN) AS COD_EXAMEN,
|
||||
TRIM(ex.NOMBRE) AS NOM_EXAMEN,
|
||||
COALESCE(NULLIF(TRIM(ex.NUM_ISS), ''), TRIM(rel.COD_EXAMEN)) AS CUPS,
|
||||
rel.PRECIO,
|
||||
TRIM(r.DIAG_PPAL) AS DIAG_PPAL,
|
||||
TRIM(d.CONCEPTO) AS DIAG_CONCEPTO,
|
||||
TRIM(m.DOCIDMEDICO) AS DOCIDMEDICO,
|
||||
TRIM(r.NIT_EMPRESA) AS NIT_EMPRESA,
|
||||
r.VALORTOTAL
|
||||
FROM RECEPCION r
|
||||
JOIN RELACION rel ON rel.IDRECEPCION = r.IDRECEPCION
|
||||
LEFT JOIN EXAMEN ex ON TRIM(ex.CODIGO) = TRIM(rel.COD_EXAMEN)
|
||||
JOIN PACIENTE p ON p.CODIGO = r.COD_PACIENTE
|
||||
JOIN RELACION rel ON rel.IDRECEPCION = r.IDRECEPCION
|
||||
LEFT JOIN EXAMEN ex ON TRIM(ex.CODIGO) = TRIM(rel.COD_EXAMEN)
|
||||
LEFT JOIN DIAGNOSTICO d ON TRIM(d.COD_DIAG) = TRIM(r.DIAG_PPAL)
|
||||
LEFT JOIN MEDICO m ON m.CODIGO = r.COD_MEDICO
|
||||
JOIN PACIENTE p ON p.CODIGO = r.COD_PACIENTE
|
||||
WHERE TRIM(p.DOCIDENT) = :cedula
|
||||
AND r.FECHA_RECEPCION = CURRENT_DATE
|
||||
ORDER BY r.IDRECEPCION DESC
|
||||
@@ -123,12 +130,17 @@ async def examenes_paciente(request: Request, cedula: str = ""):
|
||||
if hora_dt and hora_dt < limite:
|
||||
continue
|
||||
examenes.append({
|
||||
"recepcion_id": row.get("IDRECEPCION"),
|
||||
"hora": row.get("HORAINICIORECEPCION"),
|
||||
"cod_examen": (row.get("COD_EXAMEN") or "").strip(),
|
||||
"nombre": (row.get("NOM_EXAMEN") or "").strip(),
|
||||
"cups": (row.get("CUPS") or "").strip(),
|
||||
"precio": row.get("PRECIO"),
|
||||
"recepcion_id": row.get("IDRECEPCION"),
|
||||
"hora": row.get("HORAINICIORECEPCION"),
|
||||
"cod_examen": (row.get("COD_EXAMEN") or "").strip(),
|
||||
"nombre": (row.get("NOM_EXAMEN") or "").strip(),
|
||||
"cups": (row.get("CUPS") or "").strip(),
|
||||
"precio": row.get("PRECIO"),
|
||||
"diagnostico_cod": (row.get("DIAG_PPAL") or "").strip(),
|
||||
"diagnostico_nombre": (row.get("DIAG_CONCEPTO") or "").strip(),
|
||||
"medico_docidmedico": (row.get("DOCIDMEDICO") or "").strip(),
|
||||
"nit_empresa": (row.get("NIT_EMPRESA") or "").strip(),
|
||||
"valor_total": row.get("VALORTOTAL"),
|
||||
})
|
||||
|
||||
return JSONResponse({"ok": True, "examenes": examenes})
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
"""
|
||||
Scheduler: sync automático de pacientes con recepción reciente → WhatsApp Lab.
|
||||
Corre cada minuto y envía solo los pacientes con HORAINICIORECEPCION en los últimos 2 min.
|
||||
"""
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from app.database import get_connection
|
||||
from app.services.firebird_service import get_firebird_from_config
|
||||
from app.services.whatsapp_sync import sync_todos, guardar_sync_log
|
||||
|
||||
_SQL_HOY = """
|
||||
SELECT DISTINCT
|
||||
p.CODIGO,
|
||||
p.TIPOIDENT,
|
||||
p.DOCIDENT,
|
||||
p.NOMBRES,
|
||||
p.APELLIDOS,
|
||||
p.DIRECCION,
|
||||
p.CIUDAD AS COD_CIUDAD,
|
||||
c.NOMBRE AS NOM_CIUDAD,
|
||||
p.TELEFONOS,
|
||||
p.EMAIL,
|
||||
p.F_NACIMIENTO,
|
||||
p.SEXO,
|
||||
p.TIPORES,
|
||||
p.CODETNIA,
|
||||
r.HORAINICIORECEPCION
|
||||
FROM PACIENTE p
|
||||
LEFT JOIN CIUDAD c ON c.CODIGO = p.CIUDAD
|
||||
JOIN RECEPCION r ON r.COD_PACIENTE = p.CODIGO
|
||||
WHERE r.FECHA_RECEPCION = CURRENT_DATE
|
||||
AND r.NUM_FACTURA > 0
|
||||
"""
|
||||
|
||||
_SQL_EXAMENES_HOY = """
|
||||
SELECT
|
||||
TRIM(p.DOCIDENT) AS DOCIDENT,
|
||||
r.IDRECEPCION,
|
||||
r.HORAINICIORECEPCION,
|
||||
TRIM(rel.COD_EXAMEN) AS COD_EXAMEN,
|
||||
TRIM(ex.NOMBRE) AS NOM_EXAMEN,
|
||||
COALESCE(NULLIF(TRIM(ex.NUM_ISS), ''), TRIM(rel.COD_EXAMEN)) AS CUPS,
|
||||
rel.PRECIO,
|
||||
TRIM(r.DIAG_PPAL) AS DIAG_PPAL,
|
||||
TRIM(d.CONCEPTO) AS DIAG_CONCEPTO,
|
||||
TRIM(m.DOCIDMEDICO) AS DOCIDMEDICO,
|
||||
TRIM(r.NIT_EMPRESA) AS NIT_EMPRESA,
|
||||
r.VALORTOTAL
|
||||
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 DIAGNOSTICO d ON TRIM(d.COD_DIAG) = TRIM(r.DIAG_PPAL)
|
||||
LEFT JOIN MEDICO m ON m.CODIGO = r.COD_MEDICO
|
||||
JOIN PACIENTE p ON p.CODIGO = r.COD_PACIENTE
|
||||
WHERE r.FECHA_RECEPCION = CURRENT_DATE
|
||||
AND r.NUM_FACTURA > 0
|
||||
ORDER BY r.IDRECEPCION
|
||||
"""
|
||||
|
||||
|
||||
def _parse_hora(val) -> datetime | None:
|
||||
if not val:
|
||||
return None
|
||||
s = str(val)
|
||||
try:
|
||||
if "T" in s:
|
||||
dt = datetime.fromisoformat(s)
|
||||
ahora = datetime.now()
|
||||
return ahora.replace(hour=dt.hour, minute=dt.minute, second=dt.second, microsecond=0)
|
||||
partes = s.split(":")
|
||||
ahora = datetime.now()
|
||||
return ahora.replace(hour=int(partes[0]), minute=int(partes[1]),
|
||||
second=int(partes[2].split(".")[0]), microsecond=0)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
async def sync_recientes():
|
||||
"""Job que corre cada minuto: sincroniza pacientes con recepción en los últimos 2 min."""
|
||||
conn = get_connection()
|
||||
configs = {row["key"]: row["value"] for row in conn.execute("SELECT * FROM config").fetchall()}
|
||||
conn.close()
|
||||
|
||||
wa_url = configs.get("whatsapp_url", "").rstrip("/")
|
||||
wa_key = configs.get("whatsapp_api_key", "")
|
||||
if not wa_url or not wa_key:
|
||||
return
|
||||
|
||||
fb, ok, _ = get_firebird_from_config(configs)
|
||||
if not ok:
|
||||
return
|
||||
|
||||
ok_pac, _, rows_pac = fb.execute_query(_SQL_HOY, None)
|
||||
ok_ex, _, rows_ex = fb.execute_query(_SQL_EXAMENES_HOY, None)
|
||||
fb.disconnect()
|
||||
|
||||
if not ok_pac or not rows_pac:
|
||||
return
|
||||
|
||||
limite = datetime.now() - timedelta(minutes=2)
|
||||
|
||||
recientes = [
|
||||
row for row in rows_pac
|
||||
if (h := _parse_hora(row.get("HORAINICIORECEPCION"))) is None or h >= limite
|
||||
]
|
||||
if not recientes:
|
||||
return
|
||||
|
||||
# Construir mapa DOCIDENT → lista de exámenes filtrados por la misma ventana de 2 min
|
||||
examenes_map: dict[str, list] = {}
|
||||
if ok_ex and rows_ex:
|
||||
for ex in rows_ex:
|
||||
h = _parse_hora(ex.get("HORAINICIORECEPCION"))
|
||||
if h is not None and h < limite:
|
||||
continue
|
||||
doc = str(ex.get("DOCIDENT") or "").strip()
|
||||
if not doc:
|
||||
continue
|
||||
examenes_map.setdefault(doc, []).append({
|
||||
"cod_examen": (ex.get("COD_EXAMEN") or "").strip(),
|
||||
"nombre": (ex.get("NOM_EXAMEN") or "").strip(),
|
||||
"cups": (ex.get("CUPS") or "").strip(),
|
||||
"precio": ex.get("PRECIO"),
|
||||
"recepcion_id": ex.get("IDRECEPCION"),
|
||||
"hora": str(ex.get("HORAINICIORECEPCION") or "")[:8],
|
||||
"diagnostico_cod": (ex.get("DIAG_PPAL") or "").strip(),
|
||||
"diagnostico_nombre": (ex.get("DIAG_CONCEPTO") or "").strip(),
|
||||
"medico_docidmedico": (ex.get("DOCIDMEDICO") or "").strip(),
|
||||
"nit_empresa": (ex.get("NIT_EMPRESA") or "").strip(),
|
||||
"valor_total": ex.get("VALORTOTAL"),
|
||||
})
|
||||
|
||||
ingest_url = f"{wa_url}/api/lab/ingest_paciente.php"
|
||||
timeout = int(configs.get("api_timeout", 30))
|
||||
resultado = await sync_todos(recientes, ingest_url, wa_key, timeout,
|
||||
modo="upsert", examenes_map=examenes_map)
|
||||
|
||||
if resultado["total"] > 0:
|
||||
guardar_sync_log(resultado, 0, origen="scheduler", modo="upsert")
|
||||
@@ -106,10 +106,13 @@ async def sync_paciente(
|
||||
api_key: str,
|
||||
client: Optional[httpx.AsyncClient] = None,
|
||||
modo: str = "upsert",
|
||||
examenes: Optional[list] = None,
|
||||
) -> dict:
|
||||
"""Envía un paciente al endpoint de WhatsApp. Retorna {ok, action, message}."""
|
||||
payload = mapear_paciente(row)
|
||||
payload["modo"] = modo
|
||||
if examenes:
|
||||
payload["examenes"] = examenes
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"X-Lab-Key": api_key,
|
||||
@@ -139,13 +142,16 @@ async def sync_paciente(
|
||||
}
|
||||
|
||||
|
||||
async def sync_todos(rows: list, url: str, api_key: str, timeout: int = 30, modo: str = "upsert") -> dict:
|
||||
async def sync_todos(rows: list, url: str, api_key: str, timeout: int = 30, modo: str = "upsert",
|
||||
examenes_map: Optional[dict] = None) -> dict:
|
||||
"""Envía una lista de filas de pacientes. Retorna resumen {total, created, skipped, updated, errores}."""
|
||||
resultado = {"total": len(rows), "created": 0, "skipped": 0, "updated": 0, "errores": 0, "detalle": []}
|
||||
|
||||
async with httpx.AsyncClient(timeout=timeout) as client:
|
||||
for row in rows:
|
||||
r = await sync_paciente(row, url, api_key, client, modo)
|
||||
doc = str(row.get("DOCIDENT") or "").strip()
|
||||
examenes = examenes_map.get(doc) if examenes_map else None
|
||||
r = await sync_paciente(row, url, api_key, client, modo, examenes)
|
||||
if r["ok"]:
|
||||
action = r["action"]
|
||||
if action == "created":
|
||||
|
||||
+163
-149
@@ -1,151 +1,165 @@
|
||||
<!DOCTYPE html>␍
|
||||
<html lang="es" class="h-full bg-gray-50">␍
|
||||
<head>␍
|
||||
<meta charset="UTF-8">␍
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">␍
|
||||
<title>RIPS Manager - {% block title %}Dashboard{% endblock %}</title>␍
|
||||
<script src="https://cdn.tailwindcss.com"></script>␍
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/codemirror.min.css">␍
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/codemirror.min.js"></script>␍
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/sql/sql.min.js"></script>␍
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/addon/hint/show-hint.min.js"></script>␍
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/addon/hint/sql-hint.min.js"></script>␍
|
||||
<style>␍
|
||||
.CodeMirror { border: 1px solid #d1d5db; border-radius: 0.5rem; font-size: 13px; height: auto; min-height: 200px; }␍
|
||||
.CodeMirror-hints { font-size: 12px; }␍
|
||||
</style>␍
|
||||
<script>␍
|
||||
tailwind.config = {␍
|
||||
theme: {␍
|
||||
extend: {␍
|
||||
colors: {␍
|
||||
primary: {'50': '#eff6ff','100': '#dbeafe','200': '#bfdbfe','300': '#93c5fd','400': '#60a5fa','500': '#3b82f6','600': '#2563eb','700': '#1d4ed8','800': '#1e40af','900': '#1e3a8a'}␍
|
||||
}␍
|
||||
}␍
|
||||
}␍
|
||||
}␍
|
||||
</script>␍
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">␍
|
||||
</head>␍
|
||||
<body class="h-full">␍
|
||||
<div class="min-h-full">␍
|
||||
{% if user %}␍
|
||||
<!-- Sidebar -->␍
|
||||
<div class="fixed inset-y-0 left-0 w-64 bg-gray-900 text-white z-30">␍
|
||||
<div class="flex items-center h-16 px-6 border-b border-gray-700">␍
|
||||
<i class="fas fa-file-medical text-blue-400 text-xl mr-3"></i>␍
|
||||
<span class="font-bold text-lg">RIPS Manager</span>␍
|
||||
</div>␍
|
||||
<nav class="mt-4 px-3 space-y-1">␍
|
||||
<a href="/dashboard" class="flex items-center px-3 py-2.5 rounded-lg text-sm font-medium {% if request.url.path == '/dashboard' %}bg-blue-600 text-white{% else %}text-gray-300 hover:bg-gray-700{% endif %}">␍
|
||||
<i class="fas fa-chart-pie w-5 mr-2"></i> Dashboard␍
|
||||
</a>␍
|
||||
<a href="/config" class="flex items-center px-3 py-2.5 rounded-lg text-sm font-medium {% if request.url.path == '/config' %}bg-blue-600 text-white{% else %}text-gray-300 hover:bg-gray-700{% endif %}">␍
|
||||
<i class="fas fa-cog w-5 mr-2"></i> Configuración␍
|
||||
</a>␍
|
||||
<a href="/queries" class="flex items-center px-3 py-2.5 rounded-lg text-sm font-medium {% if request.url.path == '/queries' %}bg-blue-600 text-white{% else %}text-gray-300 hover:bg-gray-700{% endif %}">␍
|
||||
<i class="fas fa-database w-5 mr-2"></i> Consultas SQL␍
|
||||
</a>␍
|
||||
<a href="/contratos" class="flex items-center px-3 py-2.5 rounded-lg text-sm font-medium {% if request.url.path == '/contratos' %}bg-blue-600 text-white{% else %}text-gray-300 hover:bg-gray-700{% endif %}">␍
|
||||
<i class="fas fa-file-contract w-5 mr-2"></i> Contratos␍
|
||||
</a>␍
|
||||
<hr class="my-3 border-gray-700">␍
|
||||
<p class="px-3 text-xs font-semibold text-gray-400 uppercase tracking-wider">Envíos</p>␍
|
||||
<a href="/terceros" class="flex items-center px-3 py-2.5 rounded-lg text-sm font-medium {% if request.url.path == '/terceros' %}bg-blue-600 text-white{% else %}text-gray-300 hover:bg-gray-700{% endif %}">␍
|
||||
<i class="fas fa-user w-5 mr-2"></i> Terceros␍
|
||||
</a>␍
|
||||
<a href="/transaccion" class="flex items-center px-3 py-2.5 rounded-lg text-sm font-medium {% if request.url.path == '/transaccion' %}bg-blue-600 text-white{% else %}text-gray-300 hover:bg-gray-700{% endif %}">␍
|
||||
<i class="fas fa-exchange-alt w-5 mr-2"></i> Transacción RIPS␍
|
||||
</a>␍
|
||||
<a href="/ventas" class="flex items-center px-3 py-2.5 rounded-lg text-sm font-medium {% if request.url.path == '/ventas' %}bg-blue-600 text-white{% else %}text-gray-300 hover:bg-gray-700{% endif %}">
|
||||
<i class="fas fa-file-invoice-dollar w-5 mr-2"></i> Facturas Venta
|
||||
<!DOCTYPE html>
|
||||
<html lang="es" class="h-full bg-gray-50">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>RIPS Manager - {% block title %}Dashboard{% endblock %}</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/codemirror.min.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/codemirror.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/sql/sql.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/addon/hint/show-hint.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/addon/hint/sql-hint.min.js"></script>
|
||||
<style>
|
||||
.CodeMirror { border: 1px solid #d1d5db; border-radius: 0.5rem; font-size: 13px; height: auto; min-height: 200px; }
|
||||
.CodeMirror-hints { font-size: 12px; }
|
||||
</style>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: {'50': '#eff6ff','100': '#dbeafe','200': '#bfdbfe','300': '#93c5fd','400': '#60a5fa','500': '#3b82f6','600': '#2563eb','700': '#1d4ed8','800': '#1e40af','900': '#1e3a8a'}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||
</head>
|
||||
<body class="h-full">
|
||||
{% set embedded = request.query_params.get('embed') == '1' %}
|
||||
{% set cur = request.url.path %}
|
||||
<div class="min-h-full">
|
||||
{% if user %}
|
||||
{% if not embedded %}
|
||||
<!-- Sidebar -->
|
||||
<div class="fixed inset-y-0 left-0 w-64 bg-gray-900 text-white z-30">
|
||||
<div class="flex items-center h-16 px-6 border-b border-gray-700">
|
||||
<i class="fas fa-file-medical text-blue-400 text-xl mr-3"></i>
|
||||
<span class="font-bold text-lg">RIPS Manager</span>
|
||||
</div>
|
||||
<nav class="mt-4 px-3 space-y-1 overflow-y-auto" style="max-height: calc(100vh - 130px)">
|
||||
<a href="/dashboard" class="flex items-center px-3 py-2.5 rounded-lg text-sm font-medium {% if cur == '/dashboard' %}bg-blue-600 text-white{% else %}text-gray-300 hover:bg-gray-700{% endif %}">
|
||||
<i class="fas fa-chart-pie w-5 mr-2"></i> Dashboard
|
||||
</a>
|
||||
<a href="/automation" class="flex items-center px-3 py-2.5 rounded-lg text-sm font-medium {% if request.url.path == '/automation' %}bg-blue-600 text-white{% else %}text-gray-300 hover:bg-gray-700{% endif %}">␍
|
||||
<i class="fas fa-robot w-5 mr-2"></i> Automatización␍
|
||||
</a>
|
||||
<a href="/pacientes" class="flex items-center px-3 py-2.5 rounded-lg text-sm font-medium {% if request.url.path == '/pacientes' %}bg-blue-600 text-white{% else %}text-gray-300 hover:bg-gray-700{% endif %}">
|
||||
<i class="fas fa-users w-5 mr-2"></i> Pacientes <span class="ml-auto text-xs bg-green-500 text-white px-1.5 rounded-full">WA</span>
|
||||
<a href="/config" class="flex items-center px-3 py-2.5 rounded-lg text-sm font-medium {% if cur == '/config' %}bg-blue-600 text-white{% else %}text-gray-300 hover:bg-gray-700{% endif %}">
|
||||
<i class="fas fa-cog w-5 mr-2"></i> Configuración
|
||||
</a>
|
||||
<a href="/test-rda" class="flex items-center px-3 py-2.5 rounded-lg text-sm font-medium {% if request.url.path == '/test-rda' %}bg-blue-600 text-white{% else %}text-gray-300 hover:bg-gray-700{% endif %}">
|
||||
<i class="fas fa-flask w-5 mr-2"></i> Prueba RDA
|
||||
</a>␍
|
||||
<hr class="my-3 border-gray-700">␍
|
||||
<a href="/logs" class="flex items-center px-3 py-2.5 rounded-lg text-sm font-medium {% if request.url.path == '/logs' %}bg-blue-600 text-white{% else %}text-gray-300 hover:bg-gray-700{% endif %}">␍
|
||||
<i class="fas fa-history w-5 mr-2"></i> Historial␍
|
||||
</a>␍
|
||||
<a href="/logs/actividad" class="flex items-center px-3 py-2.5 rounded-lg text-sm font-medium {% if request.url.path == '/logs/actividad' %}bg-blue-600 text-white{% else %}text-gray-300 hover:bg-gray-700{% endif %}">␍
|
||||
<i class="fas fa-user-clock w-5 mr-2"></i> Actividad␍
|
||||
</a>␍
|
||||
<a href="/auth/logout" class="flex items-center px-3 py-2.5 rounded-lg text-sm font-medium text-gray-300 hover:bg-gray-700">␍
|
||||
<i class="fas fa-sign-out-alt w-5 mr-2"></i> Salir␍
|
||||
</a>␍
|
||||
</nav>␍
|
||||
<div class="absolute bottom-0 left-0 right-0 px-4 py-3 border-t border-gray-700">␍
|
||||
<div class="flex items-center">␍
|
||||
<div class="w-8 h-8 bg-blue-500 rounded-full flex items-center justify-center text-sm font-bold">␍
|
||||
{{ user.username[0]|upper }}␍
|
||||
</div>␍
|
||||
<div class="ml-3">␍
|
||||
<p class="text-sm font-medium text-white">{{ user.username }}</p>␍
|
||||
<p class="text-xs text-gray-400">Sesión activa</p>␍
|
||||
</div>␍
|
||||
</div>␍
|
||||
</div>␍
|
||||
</div>␍
|
||||
␍
|
||||
<!-- Main content -->␍
|
||||
<div class="pl-64">␍
|
||||
<header class="bg-white shadow-sm border-b border-gray-200">␍
|
||||
<div class="flex items-center justify-between h-16 px-8">␍
|
||||
<h1 class="text-xl font-semibold text-gray-800">{% block header %}Dashboard{% endblock %}</h1>␍
|
||||
<div class="flex items-center space-x-4">␍
|
||||
<span class="text-sm text-gray-500">␍
|
||||
<i class="far fa-calendar-alt mr-1"></i>␍
|
||||
<script>document.write(new Date().toLocaleDateString('es-CO', {year:'numeric',month:'long',day:'numeric'}))</script>␍
|
||||
</span>␍
|
||||
</div>␍
|
||||
</div>␍
|
||||
</header>␍
|
||||
<main class="p-8">␍
|
||||
{% block content %}{% endblock %}␍
|
||||
</main>␍
|
||||
</div>␍
|
||||
{% else %}␍
|
||||
<main>␍
|
||||
{% block auth_content %}{% endblock %}␍
|
||||
</main>␍
|
||||
{% endif %}␍
|
||||
</div>␍
|
||||
␍
|
||||
<script>␍
|
||||
console.log('%c RIPS Manager v1.0 ', 'background:#1e3a8a;color:white;font-size:18px;font-weight:bold;padding:8px 12px;border-radius:4px;');␍
|
||||
console.log('%c Desarrollado por U-SITE SAS BIC', 'font-size:14px;');␍
|
||||
console.log('%c https://u-site.app/', 'color:#2563eb;font-size:12px;');␍
|
||||
console.log('%c Sistema de generacion y envio de RIPS (Res. 2275/2023)', 'font-style:italic;font-size:12px;');␍
|
||||
␍
|
||||
async function fetchJSON(url, options = {}) {␍
|
||||
const resp = await fetch(url, options);␍
|
||||
return await resp.json();␍
|
||||
}␍
|
||||
␍
|
||||
function showToast(message, type = 'success') {␍
|
||||
const colors = {success: 'bg-green-500', error: 'bg-red-500', info: 'bg-blue-500', warning: 'bg-yellow-500'};␍
|
||||
const toast = document.createElement('div');␍
|
||||
toast.className = `fixed top-4 right-4 z-50 ${colors[type]} text-white px-6 py-3 rounded-lg shadow-lg transition-all duration-500`;␍
|
||||
toast.textContent = message;␍
|
||||
document.body.appendChild(toast);␍
|
||||
setTimeout(() => { toast.style.opacity = '0'; setTimeout(() => toast.remove(), 500); }, 4000);␍
|
||||
}␍
|
||||
␍
|
||||
function showLoading(btn) {␍
|
||||
btn.disabled = true;␍
|
||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i> Procesando...';␍
|
||||
}␍
|
||||
␍
|
||||
function hideLoading(btn, text) {␍
|
||||
btn.disabled = false;␍
|
||||
btn.innerHTML = text;␍
|
||||
}␍
|
||||
</script>␍
|
||||
</body>␍
|
||||
</html>␍
|
||||
<a href="/queries" class="flex items-center px-3 py-2.5 rounded-lg text-sm font-medium {% if cur == '/queries' %}bg-blue-600 text-white{% else %}text-gray-300 hover:bg-gray-700{% endif %}">
|
||||
<i class="fas fa-database w-5 mr-2"></i> Consultas SQL
|
||||
</a>
|
||||
<a href="/contratos" class="flex items-center px-3 py-2.5 rounded-lg text-sm font-medium {% if cur == '/contratos' %}bg-blue-600 text-white{% else %}text-gray-300 hover:bg-gray-700{% endif %}">
|
||||
<i class="fas fa-file-contract w-5 mr-2"></i> Contratos
|
||||
</a>
|
||||
<hr class="my-3 border-gray-700">
|
||||
<p class="px-3 text-xs font-semibold text-gray-400 uppercase tracking-wider mb-1">Módulos de Envío</p>
|
||||
|
||||
<!-- Módulo TNS -->
|
||||
<a href="/envios/tns" class="flex items-center px-3 py-2.5 rounded-lg text-sm font-medium {% if cur == '/envios/tns' %}bg-blue-600 text-white{% else %}text-gray-300 hover:bg-gray-700{% endif %}">
|
||||
<i class="fas fa-paper-plane w-5 mr-2"></i>
|
||||
<span class="flex-1">TNS</span>
|
||||
<span class="text-xs px-1.5 py-0.5 rounded {% if cur == '/envios/tns' %}bg-blue-500 text-blue-100{% else %}bg-gray-700 text-gray-400{% endif %}">5</span>
|
||||
</a>
|
||||
{% if cur == '/envios/tns' %}
|
||||
<div class="ml-4 border-l border-gray-700 pl-3">
|
||||
<p class="text-xs text-gray-500 py-0.5">Terceros · Transacción · Ventas</p>
|
||||
<p class="text-xs text-gray-500 py-0.5">Prueba RDA · Automatización</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Módulo ERP Lab -->
|
||||
<a href="/envios/erp" class="flex items-center px-3 py-2.5 rounded-lg text-sm font-medium {% if cur == '/envios/erp' %}bg-green-700 text-white{% else %}text-gray-300 hover:bg-gray-700{% endif %}">
|
||||
<i class="fas fa-flask w-5 mr-2"></i>
|
||||
<span class="flex-1">ERP Lab</span>
|
||||
<span class="text-xs px-1.5 py-0.5 rounded {% if cur == '/envios/erp' %}bg-green-500 text-green-100{% else %}bg-gray-700 text-gray-400{% endif %}">WA</span>
|
||||
</a>
|
||||
{% if cur == '/envios/erp' %}
|
||||
<div class="ml-4 border-l border-gray-700 pl-3">
|
||||
<p class="text-xs text-gray-500 py-0.5">Pacientes · Sync Automático</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<hr class="my-3 border-gray-700">
|
||||
<a href="/logs" class="flex items-center px-3 py-2.5 rounded-lg text-sm font-medium {% if cur == '/logs' %}bg-blue-600 text-white{% else %}text-gray-300 hover:bg-gray-700{% endif %}">
|
||||
<i class="fas fa-history w-5 mr-2"></i> Historial TNS
|
||||
</a>
|
||||
<a href="/logs/actividad" class="flex items-center px-3 py-2.5 rounded-lg text-sm font-medium {% if cur == '/logs/actividad' %}bg-blue-600 text-white{% else %}text-gray-300 hover:bg-gray-700{% endif %}">
|
||||
<i class="fas fa-user-clock w-5 mr-2"></i> Actividad
|
||||
</a>
|
||||
<a href="/auth/logout" class="flex items-center px-3 py-2.5 rounded-lg text-sm font-medium text-gray-300 hover:bg-gray-700">
|
||||
<i class="fas fa-sign-out-alt w-5 mr-2"></i> Salir
|
||||
</a>
|
||||
</nav>
|
||||
<div class="absolute bottom-0 left-0 right-0 px-4 py-3 border-t border-gray-700">
|
||||
<div class="flex items-center">
|
||||
<div class="w-8 h-8 bg-blue-500 rounded-full flex items-center justify-center text-sm font-bold">
|
||||
{{ user.username[0]|upper }}
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm font-medium text-white">{{ user.username }}</p>
|
||||
<p class="text-xs text-gray-400">Sesión activa</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Main content -->
|
||||
<div class="{% if not embedded %}pl-64{% endif %}">
|
||||
{% if not embedded %}
|
||||
<header class="bg-white shadow-sm border-b border-gray-200">
|
||||
<div class="flex items-center justify-between h-16 px-8">
|
||||
<h1 class="text-xl font-semibold text-gray-800">{% block header %}Dashboard{% endblock %}</h1>
|
||||
<div class="flex items-center space-x-4">
|
||||
<span class="text-sm text-gray-500">
|
||||
<i class="far fa-calendar-alt mr-1"></i>
|
||||
<script>document.write(new Date().toLocaleDateString('es-CO', {year:'numeric',month:'long',day:'numeric'}))</script>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
{% endif %}
|
||||
<main class="{% if not embedded %}p-8{% else %}p-4{% endif %}">
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
</div>
|
||||
{% else %}
|
||||
<main>
|
||||
{% block auth_content %}{% endblock %}
|
||||
</main>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
console.log('%c RIPS Manager v1.0 ', 'background:#1e3a8a;color:white;font-size:18px;font-weight:bold;padding:8px 12px;border-radius:4px;');
|
||||
console.log('%c Desarrollado por U-SITE SAS BIC', 'font-size:14px;');
|
||||
console.log('%c https://u-site.app/', 'color:#2563eb;font-size:12px;');
|
||||
console.log('%c Sistema de generacion y envio de RIPS (Res. 2275/2023)', 'font-style:italic;font-size:12px;');
|
||||
|
||||
async function fetchJSON(url, options = {}) {
|
||||
const resp = await fetch(url, options);
|
||||
return await resp.json();
|
||||
}
|
||||
|
||||
function showToast(message, type = 'success') {
|
||||
const colors = {success: 'bg-green-500', error: 'bg-red-500', info: 'bg-blue-500', warning: 'bg-yellow-500'};
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `fixed top-4 right-4 z-50 ${colors[type]} text-white px-6 py-3 rounded-lg shadow-lg transition-all duration-500`;
|
||||
toast.textContent = message;
|
||||
document.body.appendChild(toast);
|
||||
setTimeout(() => { toast.style.opacity = '0'; setTimeout(() => toast.remove(), 500); }, 4000);
|
||||
}
|
||||
|
||||
function showLoading(btn) {
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i> Procesando...';
|
||||
}
|
||||
|
||||
function hideLoading(btn, text) {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = text;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}ERP Lab{% endblock %}
|
||||
{% block header %}<i class="fas fa-flask mr-2 text-green-400"></i> ERP Lab · Ximena Caicedo{% endblock %}
|
||||
{% block content %}
|
||||
<div class="-m-8 flex flex-col" style="height: calc(100vh - 64px)">
|
||||
|
||||
<!-- Barra de sub-tabs -->
|
||||
<div class="bg-white border-b border-gray-200 px-4 flex-shrink-0">
|
||||
<div class="flex">
|
||||
<button id="btn-pacientes" onclick="switchTab('pacientes')"
|
||||
class="tab-btn flex items-center gap-2 px-4 py-3 text-sm font-medium border-b-2 transition-colors whitespace-nowrap">
|
||||
<i class="fas fa-users text-xs"></i> Pacientes
|
||||
</button>
|
||||
<button id="btn-scheduler" onclick="switchTab('scheduler')"
|
||||
class="tab-btn flex items-center gap-2 px-4 py-3 text-sm font-medium border-b-2 transition-colors whitespace-nowrap">
|
||||
<i class="fas fa-sync-alt text-xs"></i> Sync Automático
|
||||
<span class="text-xs bg-green-100 text-green-700 px-1.5 py-0.5 rounded-full font-semibold">1 min</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Paneles -->
|
||||
<div class="flex-1 relative overflow-hidden bg-gray-50">
|
||||
|
||||
<!-- Tab: Pacientes (iframe) -->
|
||||
<div id="panel-pacientes" class="tab-panel absolute inset-0 hidden">
|
||||
<iframe id="iframe-pacientes" class="w-full h-full border-0"></iframe>
|
||||
</div>
|
||||
|
||||
<!-- Tab: Sync Automático (inline) -->
|
||||
<div id="panel-scheduler" class="tab-panel absolute inset-0 hidden overflow-y-auto">
|
||||
<div class="p-6 space-y-5 max-w-5xl mx-auto">
|
||||
|
||||
<!-- Estado del scheduler -->
|
||||
<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">
|
||||
<h3 class="font-semibold text-gray-800">
|
||||
<i class="fas fa-robot mr-2 text-green-500"></i>Estado del Scheduler
|
||||
</h3>
|
||||
<span class="flex items-center gap-2 text-sm text-green-600 font-medium">
|
||||
<span class="w-2 h-2 rounded-full bg-green-400 animate-pulse"></span>
|
||||
Activo — cada 1 minuto
|
||||
</span>
|
||||
</div>
|
||||
<div class="p-5 grid grid-cols-1 sm:grid-cols-3 gap-4 text-sm text-gray-600">
|
||||
<div class="flex items-start gap-3">
|
||||
<i class="fas fa-database text-blue-400 mt-0.5"></i>
|
||||
<div>
|
||||
<p class="font-medium text-gray-700">Fuente</p>
|
||||
<p class="text-xs text-gray-500">Firebird · DBLAB_XIMENA_FB25</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-start gap-3">
|
||||
<i class="fas fa-filter text-purple-400 mt-0.5"></i>
|
||||
<div>
|
||||
<p class="font-medium text-gray-700">Ventana de captura</p>
|
||||
<p class="text-xs text-gray-500">Recepciones con HORAINICIORECEPCION en los últimos 2 minutos</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-start gap-3">
|
||||
<i class="fas fa-paper-plane text-green-400 mt-0.5"></i>
|
||||
<div>
|
||||
<p class="font-medium text-gray-700">Destino</p>
|
||||
<p class="text-xs text-gray-500">WhatsApp Lab · <code>ingest_paciente.php</code></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-5 pb-4 border-t border-gray-100 pt-3">
|
||||
<p class="text-xs text-gray-500 font-medium mb-2">Datos sincronizados por recepción:</p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<span class="text-xs bg-blue-50 text-blue-700 px-2 py-1 rounded">Paciente (upsert)</span>
|
||||
<span class="text-xs bg-purple-50 text-purple-700 px-2 py-1 rounded">Exámenes + CUPS</span>
|
||||
<span class="text-xs bg-orange-50 text-orange-700 px-2 py-1 rounded">Diagnóstico CIE-10</span>
|
||||
<span class="text-xs bg-green-50 text-green-700 px-2 py-1 rounded">Médico ordenante</span>
|
||||
<span class="text-xs bg-teal-50 text-teal-700 px-2 py-1 rounded">Empresa / EPS</span>
|
||||
<span class="text-xs bg-yellow-50 text-yellow-700 px-2 py-1 rounded">Valor total</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Historial de sync automático -->
|
||||
<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">
|
||||
<h3 class="font-semibold text-gray-800">
|
||||
<i class="fas fa-history mr-2 text-gray-400"></i>Historial de sync automático
|
||||
</h3>
|
||||
<button onclick="cargarHistorialScheduler()"
|
||||
class="text-sm text-blue-600 hover:underline flex items-center gap-1">
|
||||
<i class="fas fa-refresh text-xs"></i> Actualizar
|
||||
</button>
|
||||
</div>
|
||||
<div id="scheduler-hist" class="p-4">
|
||||
<p class="text-sm text-gray-400 text-center py-4">Cargando…</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const _TAB_ACTIVE = 'border-green-500 text-green-700 bg-green-50';
|
||||
const _TAB_INACTIVE = 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300';
|
||||
|
||||
let _schedulerLoaded = false;
|
||||
|
||||
function switchTab(name) {
|
||||
document.querySelectorAll('.tab-panel').forEach(p => p.classList.add('hidden'));
|
||||
document.querySelectorAll('.tab-btn').forEach(b => {
|
||||
b.className = b.className.replace(_TAB_ACTIVE, '').replace(_TAB_INACTIVE, '').trim();
|
||||
b.classList.add(..._TAB_INACTIVE.split(' '));
|
||||
});
|
||||
|
||||
document.getElementById('panel-' + name).classList.remove('hidden');
|
||||
const btn = document.getElementById('btn-' + name);
|
||||
btn.className = btn.className.replace(_TAB_INACTIVE, '').trim();
|
||||
btn.classList.add(..._TAB_ACTIVE.split(' '));
|
||||
|
||||
if (name === 'pacientes') {
|
||||
const iframe = document.getElementById('iframe-pacientes');
|
||||
if (!iframe.src) iframe.src = '/pacientes?embed=1';
|
||||
}
|
||||
if (name === 'scheduler' && !_schedulerLoaded) {
|
||||
_schedulerLoaded = true;
|
||||
cargarHistorialScheduler();
|
||||
}
|
||||
|
||||
sessionStorage.setItem('erp-tab', name);
|
||||
}
|
||||
|
||||
const _origenLabel = { manual: 'Manual', tercero: 'Tercero', scheduler: 'Scheduler', automation: 'Automatización' };
|
||||
const _modoLabel = { insertar: 'Solo nuevos', upsert: 'Upsert' };
|
||||
|
||||
async function cargarHistorialScheduler() {
|
||||
const el = document.getElementById('scheduler-hist');
|
||||
el.innerHTML = '<p class="text-sm text-gray-400 text-center py-4">Cargando…</p>';
|
||||
try {
|
||||
const resp = await fetch('/pacientes/historial');
|
||||
const rows = await resp.json();
|
||||
const sched = rows.filter(r => r.origen === 'scheduler');
|
||||
if (!sched.length) {
|
||||
el.innerHTML = '<p class="text-sm text-gray-400 text-center py-6">Sin ejecuciones automáticas registradas aún.</p>';
|
||||
return;
|
||||
}
|
||||
el.innerHTML = `
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="text-left text-xs text-gray-500 border-b border-gray-100">
|
||||
<th class="pb-2 pr-4 font-medium">Fecha</th>
|
||||
<th class="pb-2 pr-3 font-medium text-right">Procesados</th>
|
||||
<th class="pb-2 pr-3 font-medium text-right text-blue-600">Nuevos</th>
|
||||
<th class="pb-2 pr-3 font-medium text-right text-gray-400">Omitidos</th>
|
||||
<th class="pb-2 pr-3 font-medium text-right text-green-600">Actualizados</th>
|
||||
<th class="pb-2 pr-3 font-medium text-right text-red-500">Errores</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-50">
|
||||
${sched.map(r => {
|
||||
const fecha = r.created_at.replace('T', ' ').slice(0, 16);
|
||||
const errDet = r.errores_det ? JSON.parse(r.errores_det) : [];
|
||||
return `
|
||||
<tr class="hover:bg-gray-50">
|
||||
<td class="py-2 pr-4 text-gray-500 whitespace-nowrap font-mono text-xs">${fecha}</td>
|
||||
<td class="py-2 pr-3 text-right font-medium">${r.total}</td>
|
||||
<td class="py-2 pr-3 text-right text-blue-600 font-medium">${r.created}</td>
|
||||
<td class="py-2 pr-3 text-right text-gray-400">${r.skipped}</td>
|
||||
<td class="py-2 pr-3 text-right text-green-600">${r.updated}</td>
|
||||
<td class="py-2 pr-3 text-right ${r.errores > 0 ? 'text-red-500 font-medium' : 'text-gray-300'}">
|
||||
${r.errores > 0 && errDet.length ? `
|
||||
<details>
|
||||
<summary class="cursor-pointer">${r.errores}</summary>
|
||||
<div class="absolute z-10 bg-white border border-red-100 rounded-lg shadow-lg p-2 text-xs mt-1 max-w-xs">
|
||||
${errDet.map(e => `<div class="text-red-600">${e.doc} — ${e.msg}</div>`).join('')}
|
||||
</div>
|
||||
</details>` : r.errores || '—'}
|
||||
</td>
|
||||
</tr>`;
|
||||
}).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>`;
|
||||
} catch (e) {
|
||||
el.innerHTML = `<p class="text-sm text-red-500 text-center py-4">Error: ${e.message}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
(function init() {
|
||||
document.querySelectorAll('.tab-btn').forEach(b => {
|
||||
b.classList.add(..._TAB_INACTIVE.split(' '), 'border-b-2');
|
||||
});
|
||||
const saved = sessionStorage.getItem('erp-tab') || 'pacientes';
|
||||
switchTab(saved);
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,103 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Módulo TNS{% endblock %}
|
||||
{% block header %}<i class="fas fa-paper-plane mr-2 text-blue-400"></i> Módulo TNS{% endblock %}
|
||||
{% block content %}
|
||||
<div class="-m-8 flex flex-col" style="height: calc(100vh - 64px)">
|
||||
|
||||
<!-- Barra de sub-tabs -->
|
||||
<div class="bg-white border-b border-gray-200 px-4 flex-shrink-0">
|
||||
<div class="flex">
|
||||
<button id="btn-terceros" onclick="switchTab('terceros', '/terceros?embed=1')"
|
||||
class="tab-btn flex items-center gap-2 px-4 py-3 text-sm font-medium border-b-2 transition-colors whitespace-nowrap">
|
||||
<i class="fas fa-user text-xs"></i> Terceros
|
||||
</button>
|
||||
<button id="btn-transaccion" onclick="switchTab('transaccion', '/transaccion?embed=1')"
|
||||
class="tab-btn flex items-center gap-2 px-4 py-3 text-sm font-medium border-b-2 transition-colors whitespace-nowrap">
|
||||
<i class="fas fa-exchange-alt text-xs"></i> Transacción RIPS
|
||||
</button>
|
||||
<button id="btn-ventas" onclick="switchTab('ventas', '/ventas?embed=1')"
|
||||
class="tab-btn flex items-center gap-2 px-4 py-3 text-sm font-medium border-b-2 transition-colors whitespace-nowrap">
|
||||
<i class="fas fa-file-invoice-dollar text-xs"></i> Facturas Venta
|
||||
</button>
|
||||
<button id="btn-rda" onclick="switchTab('rda', '/test-rda?embed=1')"
|
||||
class="tab-btn flex items-center gap-2 px-4 py-3 text-sm font-medium border-b-2 transition-colors whitespace-nowrap">
|
||||
<i class="fas fa-flask text-xs"></i> Prueba RDA
|
||||
</button>
|
||||
<button id="btn-automation" onclick="switchTab('automation', '/automation?embed=1')"
|
||||
class="tab-btn flex items-center gap-2 px-4 py-3 text-sm font-medium border-b-2 transition-colors whitespace-nowrap">
|
||||
<i class="fas fa-robot text-xs"></i> Automatización
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Paneles de iframe -->
|
||||
<div class="flex-1 relative overflow-hidden bg-gray-50">
|
||||
|
||||
<div id="panel-terceros" class="tab-panel absolute inset-0 hidden">
|
||||
<iframe id="iframe-terceros" class="w-full h-full border-0"></iframe>
|
||||
</div>
|
||||
|
||||
<div id="panel-transaccion" class="tab-panel absolute inset-0 hidden">
|
||||
<iframe id="iframe-transaccion" class="w-full h-full border-0"></iframe>
|
||||
</div>
|
||||
|
||||
<div id="panel-ventas" class="tab-panel absolute inset-0 hidden">
|
||||
<iframe id="iframe-ventas" class="w-full h-full border-0"></iframe>
|
||||
</div>
|
||||
|
||||
<div id="panel-rda" class="tab-panel absolute inset-0 hidden">
|
||||
<iframe id="iframe-rda" class="w-full h-full border-0"></iframe>
|
||||
</div>
|
||||
|
||||
<div id="panel-automation" class="tab-panel absolute inset-0 hidden">
|
||||
<iframe id="iframe-automation" class="w-full h-full border-0"></iframe>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const _TAB_ACTIVE = 'border-blue-500 text-blue-600 bg-blue-50';
|
||||
const _TAB_INACTIVE = 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300';
|
||||
|
||||
function switchTab(name, src) {
|
||||
document.querySelectorAll('.tab-panel').forEach(p => p.classList.add('hidden'));
|
||||
document.querySelectorAll('.tab-btn').forEach(b => {
|
||||
b.className = b.className.replace(_TAB_ACTIVE, '').replace(_TAB_INACTIVE, '').trim();
|
||||
b.classList.add(..._TAB_INACTIVE.split(' '));
|
||||
});
|
||||
|
||||
const panel = document.getElementById('panel-' + name);
|
||||
const btn = document.getElementById('btn-' + name);
|
||||
const iframe = document.getElementById('iframe-' + name);
|
||||
|
||||
panel.classList.remove('hidden');
|
||||
btn.className = btn.className.replace(_TAB_INACTIVE, '').trim();
|
||||
btn.classList.add(..._TAB_ACTIVE.split(' '));
|
||||
|
||||
if (src && !iframe.src) {
|
||||
iframe.src = src;
|
||||
}
|
||||
|
||||
sessionStorage.setItem('tns-tab', name);
|
||||
sessionStorage.setItem('tns-src-' + name, src);
|
||||
}
|
||||
|
||||
(function init() {
|
||||
// Establecer clases base en todos los botones
|
||||
document.querySelectorAll('.tab-btn').forEach(b => {
|
||||
b.classList.add(..._TAB_INACTIVE.split(' '), 'border-b-2');
|
||||
});
|
||||
|
||||
const saved = sessionStorage.getItem('tns-tab') || 'terceros';
|
||||
const srcMap = {
|
||||
terceros: '/terceros?embed=1',
|
||||
transaccion:'/transaccion?embed=1',
|
||||
ventas: '/ventas?embed=1',
|
||||
rda: '/test-rda?embed=1',
|
||||
automation: '/automation?embed=1',
|
||||
};
|
||||
switchTab(saved, srcMap[saved] || srcMap.terceros);
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -8,11 +8,14 @@ from fastapi import FastAPI, Request
|
||||
from fastapi.responses import RedirectResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
import uvicorn
|
||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||
|
||||
from app.database import init_db
|
||||
from app.auth import decode_token
|
||||
from app.services.scheduler import sync_recientes
|
||||
|
||||
app = FastAPI(title="RIPS Manager", version="1.0.0")
|
||||
_scheduler = AsyncIOScheduler()
|
||||
|
||||
templates = Jinja2Templates(
|
||||
directory=os.path.join(os.path.dirname(__file__), "app", "templates")
|
||||
@@ -60,6 +63,14 @@ async def startup():
|
||||
config_defaults()
|
||||
query_defaults()
|
||||
contratos_defaults()
|
||||
_scheduler.add_job(sync_recientes, "interval", minutes=1, id="wa_sync_recientes",
|
||||
max_instances=1, coalesce=True)
|
||||
_scheduler.start()
|
||||
|
||||
|
||||
@app.on_event("shutdown")
|
||||
async def shutdown():
|
||||
_scheduler.shutdown(wait=False)
|
||||
|
||||
|
||||
@app.get("/")
|
||||
@@ -67,12 +78,13 @@ async def root():
|
||||
return RedirectResponse(url="/dashboard")
|
||||
|
||||
|
||||
from app.routes import auth, dashboard, config, queries, terceros, transaccion, logs, automation, test_rda, debug_fb, pacientes, contratos, ventas
|
||||
from app.routes import auth, dashboard, config, queries, terceros, transaccion, logs, automation, test_rda, debug_fb, pacientes, contratos, ventas, envios
|
||||
|
||||
app.include_router(auth.router)
|
||||
app.include_router(dashboard.router)
|
||||
app.include_router(config.router)
|
||||
app.include_router(queries.router)
|
||||
app.include_router(envios.router)
|
||||
app.include_router(terceros.router)
|
||||
app.include_router(transaccion.router)
|
||||
app.include_router(logs.router)
|
||||
|
||||
Reference in New Issue
Block a user