This commit is contained in:
Lizandro Guarnizo
2026-06-18 12:25:44 -05:00
parent 65ff7995e8
commit 408ea50d21
12 changed files with 1042 additions and 1100 deletions
+9 -16
View File
@@ -1,10 +1,13 @@
import os
import bcrypt import bcrypt
from datetime import datetime, timedelta from datetime import datetime, timedelta, timezone
from typing import Optional
from jose import JWTError, jwt from jose import JWTError, jwt
from fastapi import Depends, HTTPException, status from fastapi import Depends, HTTPException, Request, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
SECRET_KEY = "rips-manager-secret-key-change-in-production" # ponytail: fallback inseguro — set RIPS_SECRET_KEY en producción
SECRET_KEY = os.environ.get("RIPS_SECRET_KEY", "rips-manager-secret-key-change-in-production")
ALGORITHM = "HS256" ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_HOURS = 12 ACCESS_TOKEN_EXPIRE_HOURS = 12
@@ -23,12 +26,11 @@ def create_token(user_id: int, username: str) -> str:
payload = { payload = {
"user_id": user_id, "user_id": user_id,
"username": username, "username": username,
"exp": datetime.now(timezone.utc) + timedelta(hours=ACCESS_TOKEN_EXPIRE_HOURS),
} }
return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM) return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)
from typing import Optional
def decode_token(token: str) -> Optional[dict]: def decode_token(token: str) -> Optional[dict]:
try: try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
@@ -37,23 +39,14 @@ def decode_token(token: str) -> Optional[dict]:
return None return None
from fastapi import Request
def get_current_user(request: Request, credentials: HTTPAuthorizationCredentials = Depends(security)): def get_current_user(request: Request, credentials: HTTPAuthorizationCredentials = Depends(security)):
if hasattr(request.state, "user") and request.state.user: if hasattr(request.state, "user") and request.state.user:
return request.state.user return request.state.user
if credentials is None: if credentials is None:
raise HTTPException( raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Not authenticated")
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Not authenticated",
)
payload = decode_token(credentials.credentials) payload = decode_token(credentials.credentials)
if payload is None: if payload is None:
raise HTTPException( raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token")
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token",
)
return payload return payload
-13
View File
@@ -39,19 +39,6 @@ def init_db():
created_at TEXT NOT NULL DEFAULT (datetime('now')) created_at TEXT NOT NULL DEFAULT (datetime('now'))
); );
CREATE TABLE IF NOT EXISTS logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
action TEXT NOT NULL,
step TEXT,
status TEXT NOT NULL CHECK(status IN ('success','error')),
payload TEXT,
response TEXT,
error_message TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY (user_id) REFERENCES users(id)
);
CREATE TABLE IF NOT EXISTS envios ( CREATE TABLE IF NOT EXISTS envios (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER, user_id INTEGER,
-18
View File
@@ -13,26 +13,8 @@ class UserLogin(BaseModel):
password: str password: str
class ConfigUpdate(BaseModel):
key: str
value: str
class QueryCreate(BaseModel): class QueryCreate(BaseModel):
name: str name: str
query_type: str query_type: str
query_text: str query_text: str
description: Optional[str] = None description: Optional[str] = None
class QueryUpdate(BaseModel):
name: Optional[str] = None
query_text: Optional[str] = None
description: Optional[str] = None
class SendRequest(BaseModel):
tipo: str
fecha_inicio: str
fecha_fin: str
factura: Optional[str] = None
+77 -87
View File
@@ -1,9 +1,12 @@
import json as json_lib
import httpx
from datetime import datetime
from fastapi import APIRouter, Request, Form, Depends from fastapi import APIRouter, Request, Form, Depends
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from app.database import get_connection from app.database import get_connection
from app.auth import get_current_user from app.auth import get_current_user
from app.services.firebird_service import FirebirdService from app.services.firebird_service import get_firebird_from_config
from app.services.json_generator import generar_terceros, generar_transaccion from app.services.json_generator import generar_terceros, generar_transaccion, agrupar_por_factura
router = APIRouter(prefix="/automation", tags=["automation"]) router = APIRouter(prefix="/automation", tags=["automation"])
@@ -31,11 +34,6 @@ async def run_automation(
fecha_fin: str = Form(...), fecha_fin: str = Form(...),
factura: str = Form(""), factura: str = Form(""),
): ):
import json as json_lib
import httpx
from datetime import datetime
from collections import defaultdict
conn = get_connection() conn = get_connection()
q_terceros = conn.execute("SELECT * FROM queries WHERE id = ?", (query_terceros_id,)).fetchone() q_terceros = conn.execute("SELECT * FROM queries WHERE id = ?", (query_terceros_id,)).fetchone()
q_trans = conn.execute("SELECT * FROM queries WHERE id = ?", (query_transaccion_id,)).fetchone() q_trans = conn.execute("SELECT * FROM queries WHERE id = ?", (query_transaccion_id,)).fetchone()
@@ -45,15 +43,8 @@ async def run_automation(
if not q_terceros or not q_trans: if not q_terceros or not q_trans:
return JSONResponse({"success": False, "message": "Consultas no encontradas"}) return JSONResponse({"success": False, "message": "Consultas no encontradas"})
fb = FirebirdService() fb, fb_ok, fb_msg = get_firebird_from_config(configs)
fb_success, fb_msg = fb.connect( if not fb_ok:
configs.get("firebird_host", "localhost"),
int(configs.get("firebird_port", 3050)),
configs.get("firebird_database", ""),
configs.get("firebird_user", "SYSDBA"),
configs.get("firebird_password", "masterkey"),
)
if not fb_success:
return JSONResponse({"success": False, "message": f"Error Firebird: {fb_msg}"}) return JSONResponse({"success": False, "message": f"Error Firebird: {fb_msg}"})
api_url = configs.get("api_url", "") api_url = configs.get("api_url", "")
@@ -74,7 +65,10 @@ async def run_automation(
if ":doc_num" in q_terceros["query_text"]: if ":doc_num" in q_terceros["query_text"]:
params["doc_num"] = "" params["doc_num"] = ""
success, error, rows = fb.execute_query(q_terceros["query_text"], params if ":fecha_ini" in q_terceros["query_text"] else None) success, error, rows = fb.execute_query(
q_terceros["query_text"],
params if ":fecha_ini" in q_terceros["query_text"] else None
)
if not success: if not success:
resultado["paso1_terceros"] = {"status": "error", "message": error} resultado["paso1_terceros"] = {"status": "error", "message": error}
@@ -85,40 +79,44 @@ async def run_automation(
terceros_errores = 0 terceros_errores = 0
pacientes_enviados = [] pacientes_enviados = []
for row in rows: async with httpx.AsyncClient(timeout=int(configs.get("api_timeout", 30))) as client:
tercero_json = generar_terceros(row) for row in rows:
doc_id = tercero_json["numDocumentoIdentificacion"] tercero_json = generar_terceros(row)
if doc_id in pacientes_enviados: doc_id = tercero_json["numDocumentoIdentificacion"]
continue if doc_id in pacientes_enviados:
pacientes_enviados.append(doc_id) continue
pacientes_enviados.append(doc_id)
try: resp_ok = False
async with httpx.AsyncClient(timeout=int(configs.get("api_timeout", 30))) as client: resp_text = ""
try:
if api_method == "POST": if api_method == "POST":
resp = await client.post(api_url + "/terceros", json=tercero_json, headers=headers) resp = await client.post(api_url + "/terceros", json=tercero_json, headers=headers)
else: else:
resp = await client.put(api_url + "/terceros", json=tercero_json, headers=headers) resp = await client.put(api_url + "/terceros", json=tercero_json, headers=headers)
resp_ok = resp.is_success
if resp.is_success: resp_text = resp.text[:1000]
terceros_enviados += 1 if resp_ok:
else: terceros_enviados += 1
else:
terceros_errores += 1
except Exception as e:
terceros_errores += 1 terceros_errores += 1
except Exception as e: resp_text = str(e)
terceros_errores += 1
conn = get_connection() conn = get_connection()
conn.execute(""" conn.execute("""
INSERT INTO envios (user_id, tipo, factura, status, json_enviado, respuesta_api, created_at) INSERT INTO envios (user_id, tipo, factura, status, json_enviado, respuesta_api, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?)
""", ( """, (
user["user_id"], "terceros", factura or "AUTO", user["user_id"], "terceros", factura or "AUTO",
"success" if resp.is_success else "error", "success" if resp_ok else "error",
json_lib.dumps(tercero_json, indent=2, ensure_ascii=False), json_lib.dumps(tercero_json, indent=2, ensure_ascii=False),
resp.text[:1000] if resp.is_success else str(e), resp_text,
datetime.now().isoformat(), datetime.now().isoformat(),
)) ))
conn.commit() conn.commit()
conn.close() conn.close()
resultado["paso1_terceros"] = { resultado["paso1_terceros"] = {
"status": "success" if terceros_errores == 0 else "partial", "status": "success" if terceros_errores == 0 else "partial",
@@ -140,59 +138,51 @@ async def run_automation(
elif not rows: elif not rows:
resultado["paso2_transaccion"] = {"status": "error", "message": "No hay servicios para enviar"} resultado["paso2_transaccion"] = {"status": "error", "message": "No hay servicios para enviar"}
else: else:
grupos = defaultdict(lambda: {"factura": "", "procedimientos": [], "paciente": {}}) grupos = agrupar_por_factura(rows, factura)
for row in rows:
doc_key = (row.get("tipo_doc_paciente", "CC"), row.get("num_doc_paciente", ""))
fact = row.get("num_factura", factura)
grupos[(fact, doc_key)]["factura"] = fact
grupos[(fact, doc_key)]["procedimientos"].append(dict(row))
trans_enviados = 0 trans_enviados = 0
trans_errores = 0 trans_errores = 0
for (fact, doc_key), grupo in grupos.items(): async with httpx.AsyncClient(timeout=int(configs.get("api_timeout", 30))) as client:
paciente_data = {"tipoDocumentoIdentificacion": doc_key[0], "numDocumentoIdentificacion": doc_key[1]} for (fact, doc_key), grupo in grupos.items():
trans_json = generar_transaccion( paciente_data = {"tipoDocumentoIdentificacion": doc_key[0], "numDocumentoIdentificacion": doc_key[1]}
fact, trans_json = generar_transaccion(
configs.get("num_documento_obligado", ""), fact, configs.get("num_documento_obligado", ""),
paciente_data, paciente_data, grupo["procedimientos"],
grupo["procedimientos"], )
)
try: status_ok = False
async with httpx.AsyncClient(timeout=int(configs.get("api_timeout", 30))) as client: resp_text = ""
try:
if api_method == "POST": if api_method == "POST":
resp = await client.post(api_url + "/transaccion", json=trans_json, headers=headers) resp = await client.post(api_url + "/transaccion", json=trans_json, headers=headers)
else: else:
resp = await client.put(api_url + "/transaccion", json=trans_json, headers=headers) resp = await client.put(api_url + "/transaccion", json=trans_json, headers=headers)
status_ok = resp.is_success
resp_text = resp.text[:1000]
except Exception as e:
resp_text = str(e)
status_ok = resp.is_success if status_ok:
resp_text = resp.text[:1000] trans_enviados += 1
except Exception as e: else:
status_ok = False trans_errores += 1
resp_text = str(e)
if status_ok: conn = get_connection()
trans_enviados += 1 conn.execute("""
else: INSERT INTO envios (user_id, tipo, factura, fecha_inicio, fecha_fin,
trans_errores += 1 pacientes_count, servicios_count, status, json_enviado, respuesta_api, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
conn = get_connection() """, (
conn.execute(""" user["user_id"], "transaccion", fact,
INSERT INTO envios (user_id, tipo, factura, fecha_inicio, fecha_fin, fecha_inicio, fecha_fin,
pacientes_count, servicios_count, status, json_enviado, respuesta_api, created_at) 1, len(grupo["procedimientos"]),
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) "success" if status_ok else "error",
""", ( json_lib.dumps(trans_json, indent=2, ensure_ascii=False)[:5000],
user["user_id"], "transaccion", fact, resp_text,
fecha_inicio, fecha_fin, datetime.now().isoformat(),
1, len(grupo["procedimientos"]), ))
"success" if status_ok else "error", conn.commit()
json_lib.dumps(trans_json, indent=2, ensure_ascii=False)[:5000], conn.close()
resp_text,
datetime.now().isoformat(),
))
conn.commit()
conn.close()
resultado["paso2_transaccion"] = { resultado["paso2_transaccion"] = {
"status": "success" if trans_errores == 0 else "partial", "status": "success" if trans_errores == 0 else "partial",
-1
View File
@@ -32,7 +32,6 @@ def ensure_defaults():
@router.get("") @router.get("")
async def config_page(request: Request, user: dict = Depends(get_current_user)): async def config_page(request: Request, user: dict = Depends(get_current_user)):
ensure_defaults()
conn = get_connection() conn = get_connection()
configs = conn.execute("SELECT * FROM config ORDER BY key").fetchall() configs = conn.execute("SELECT * FROM config ORDER BY key").fetchall()
conn.close() conn.close()
+324 -325
View File
@@ -1,325 +1,324 @@
from fastapi import APIRouter, Request, Form, Depends from fastapi import APIRouter, Request, Form, Depends
from fastapi.responses import RedirectResponse, JSONResponse from fastapi.responses import RedirectResponse, JSONResponse
from app.database import get_connection from app.database import get_connection
from app.auth import get_current_user from app.auth import get_current_user
from app.models import QueryCreate from app.models import QueryCreate
router = APIRouter(prefix="/queries", tags=["queries"]) router = APIRouter(prefix="/queries", tags=["queries"])
QUERY_DEFAULTS = [ QUERY_DEFAULTS = [
{ {
"name": "Terceros - Datos del paciente", "name": "Terceros - Datos del paciente",
"query_type": "terceros", "query_type": "terceros",
"query_text": """SELECT␍␍␍␍ "query_text": """SELECT
p.TIPO_DOCUMENTO as tipo_documento,␍␍␍␍ p.TIPO_DOCUMENTO as tipo_documento,
p.NUMERO_DOCUMENTO as numero_documento,␍␍␍␍ p.NUMERO_DOCUMENTO as numero_documento,
p.PRIMER_NOMBRE as primer_nombre,␍␍␍␍ p.PRIMER_NOMBRE as primer_nombre,
p.SEGUNDO_NOMBRE as segundo_nombre,␍␍␍␍ p.SEGUNDO_NOMBRE as segundo_nombre,
p.PRIMER_APELLIDO as primer_apellido,␍␍␍␍ p.PRIMER_APELLIDO as primer_apellido,
p.SEGUNDO_APELLIDO as segundo_apellido,␍␍␍␍ p.SEGUNDO_APELLIDO as segundo_apellido,
p.FECHA_NACIMIENTO as fecha_nacimiento,␍␍␍␍ p.FECHA_NACIMIENTO as fecha_nacimiento,
p.SEXO as cod_sexo,␍␍␍␍ p.SEXO as cod_sexo,
p.COD_ENTIDAD as cod_entidad,␍␍␍␍ p.COD_ENTIDAD as cod_entidad,
p.TIPO_USUARIO as tipo_usuario,␍␍␍␍ p.TIPO_USUARIO as tipo_usuario,
p.COD_MUNICIPIO as cod_municipio,␍␍␍␍ p.COD_MUNICIPIO as cod_municipio,
p.ZONA as cod_zona,␍␍␍␍ p.ZONA as cod_zona,
p.DIRECCION as direccion␍␍␍␍ p.DIRECCION as direccion
FROM USUAHOS p␍␍␍␍ FROM USUAHOS p
WHERE p.NUMERO_DOCUMENTO = :doc_num""", WHERE p.NUMERO_DOCUMENTO = :doc_num""",
"description": "Consulta datos maestros del paciente por documento" "description": "Consulta datos maestros del paciente por documento"
}, },
{ {
"name": "Procedimientos por factura", "name": "Procedimientos por factura",
"query_type": "transaccion", "query_type": "transaccion",
"query_text": """SELECT␍␍␍␍ "query_text": """SELECT
s.CODIGO_CUP as cod_procedimiento,␍␍␍␍ s.CODIGO_CUP as cod_procedimiento,
s.FECHA_ATENCION as fecha_atencion,␍␍␍␍ s.FECHA_ATENCION as fecha_atencion,
s.COD_DIAGNOSTICO as cod_diagnostico,␍␍␍␍ s.COD_DIAGNOSTICO as cod_diagnostico,
s.FINALIDAD as finalidad,␍␍␍␍ s.FINALIDAD as finalidad,
s.VIA_INGRESO as via_ingreso,␍␍␍␍ s.VIA_INGRESO as via_ingreso,
s.MODALIDAD as modalidad,␍␍␍␍ s.MODALIDAD as modalidad,
s.GRUPO_SERVICIO as grupo_servicio,␍␍␍␍ s.GRUPO_SERVICIO as grupo_servicio,
s.COD_SERVICIO as cod_servicio,␍␍␍␍ s.COD_SERVICIO as cod_servicio,
s.COD_PRESTADOR as cod_prestador,␍␍␍␍ s.COD_PRESTADOR as cod_prestador,
s.TIPO_DOC_PROFESIONAL as tipo_doc_profesional,␍␍␍␍ s.TIPO_DOC_PROFESIONAL as tipo_doc_profesional,
s.NUM_DOC_PROFESIONAL as num_doc_profesional,␍␍␍␍ s.NUM_DOC_PROFESIONAL as num_doc_profesional,
s.VR_SERVICIO as vr_servicio,␍␍␍␍ s.VR_SERVICIO as vr_servicio,
s.VALOR_PAGO_MODERADOR as valor_pago_moderador,␍␍␍␍ s.VALOR_PAGO_MODERADOR as valor_pago_moderador,
s.CONCEPTO_RECAUDO as concepto_recaudo,␍␍␍␍ s.CONCEPTO_RECAUDO as concepto_recaudo,
s.NUM_AUTORIZACION as num_autorizacion␍␍␍␍ s.NUM_AUTORIZACION as num_autorizacion
FROM SERVICIOS s␍␍␍␍ FROM SERVICIOS s
WHERE s.NUM_FACTURA = :factura␍␍␍␍ WHERE s.NUM_FACTURA = :factura
AND s.FECHA_ATENCION BETWEEN :fecha_ini AND :fecha_fin""", AND s.FECHA_ATENCION BETWEEN :fecha_ini AND :fecha_fin""",
"description": "Consulta procedimientos por factura y rango de fechas" "description": "Consulta procedimientos por factura y rango de fechas"
}, },
{ {
"name": "Procedimientos por fecha", "name": "Procedimientos por fecha",
"query_type": "transaccion", "query_type": "transaccion",
"query_text": """SELECT␍␍␍␍ "query_text": """SELECT
s.FACTURA as num_factura,␍␍␍␍ s.FACTURA as num_factura,
s.CODIGO_CUP as cod_procedimiento,␍␍␍␍ s.CODIGO_CUP as cod_procedimiento,
s.FECHA_ATENCION as fecha_atencion,␍␍␍␍ s.FECHA_ATENCION as fecha_atencion,
s.COD_DIAGNOSTICO as cod_diagnostico,␍␍␍␍ s.COD_DIAGNOSTICO as cod_diagnostico,
s.FINALIDAD as finalidad,␍␍␍␍ s.FINALIDAD as finalidad,
s.VIA_INGRESO as via_ingreso,␍␍␍␍ s.VIA_INGRESO as via_ingreso,
s.MODALIDAD as modalidad,␍␍␍␍ s.MODALIDAD as modalidad,
s.GRUPO_SERVICIO as grupo_servicio,␍␍␍␍ s.GRUPO_SERVICIO as grupo_servicio,
s.COD_SERVICIO as cod_servicio,␍␍␍␍ s.COD_SERVICIO as cod_servicio,
s.COD_PRESTADOR as cod_prestador,␍␍␍␍ s.COD_PRESTADOR as cod_prestador,
s.TIPO_DOC_PROFESIONAL as tipo_doc_profesional,␍␍␍␍ s.TIPO_DOC_PROFESIONAL as tipo_doc_profesional,
s.NUM_DOC_PROFESIONAL as num_doc_profesional,␍␍␍␍ s.NUM_DOC_PROFESIONAL as num_doc_profesional,
s.VR_SERVICIO as vr_servicio,␍␍␍␍ s.VR_SERVICIO as vr_servicio,
p.TIPO_DOCUMENTO as tipo_doc_paciente,␍␍␍␍ p.TIPO_DOCUMENTO as tipo_doc_paciente,
p.NUMERO_DOCUMENTO as num_doc_paciente␍␍␍␍ p.NUMERO_DOCUMENTO as num_doc_paciente
FROM SERVICIOS s␍␍␍␍ FROM SERVICIOS s
JOIN USUAHOS p ON s.COD_PACIENTE = p.COD_PACIENTE␍␍␍␍ JOIN USUAHOS p ON s.COD_PACIENTE = p.COD_PACIENTE
WHERE s.FECHA_ATENCION BETWEEN :fecha_ini AND :fecha_fin""", WHERE s.FECHA_ATENCION BETWEEN :fecha_ini AND :fecha_fin""",
"description": "Consulta todos los procedimientos en rango de fechas" "description": "Consulta todos los procedimientos en rango de fechas"
}, },
] ]
def ensure_defaults(): def ensure_defaults():
conn = get_connection() conn = get_connection()
for q in QUERY_DEFAULTS: for q in QUERY_DEFAULTS:
exists = conn.execute( exists = conn.execute(
"SELECT id FROM queries WHERE name = ?", (q["name"],) "SELECT id FROM queries WHERE name = ?", (q["name"],)
).fetchone() ).fetchone()
if not exists: if not exists:
conn.execute( conn.execute(
"INSERT INTO queries (name, query_type, query_text, description) VALUES (?, ?, ?, ?)", "INSERT INTO queries (name, query_type, query_text, description) VALUES (?, ?, ?, ?)",
(q["name"], q["query_type"], q["query_text"], q["description"]), (q["name"], q["query_type"], q["query_text"], q["description"]),
) )
conn.commit() conn.commit()
conn.close() conn.close()
@router.get("") @router.get("")
async def queries_page(request: Request, user: dict = Depends(get_current_user)): async def queries_page(request: Request, user: dict = Depends(get_current_user)):
ensure_defaults() conn = get_connection()
conn = get_connection() queries = conn.execute("SELECT * FROM queries ORDER BY query_type, name").fetchall()
queries = conn.execute("SELECT * FROM queries ORDER BY query_type, name").fetchall() conn.close()
conn.close() return request.app.state.templates.TemplateResponse("queries.html", {
return request.app.state.templates.TemplateResponse("queries.html", { "request": request, "user": user, "queries": queries
"request": request, "user": user, "queries": queries })
})
@router.post("/create")
@router.post("/create") async def query_create(
async def query_create( request: Request,
request: Request, user: dict = Depends(get_current_user),
user: dict = Depends(get_current_user), name: str = Form(...),
name: str = Form(...), query_type: str = Form(...),
query_type: str = Form(...), query_text: str = Form(...),
query_text: str = Form(...), description: str = Form(""),
description: str = Form(""), ):
): conn = get_connection()
conn = get_connection() conn.execute(
conn.execute( "INSERT INTO queries (name, query_type, query_text, description) VALUES (?, ?, ?, ?)",
"INSERT INTO queries (name, query_type, query_text, description) VALUES (?, ?, ?, ?)", (name, query_type, query_text, description),
(name, query_type, query_text, description), )
) conn.commit()
conn.commit() conn.close()
conn.close() return RedirectResponse("/queries", status_code=302)
return RedirectResponse("/queries", status_code=302)
@router.post("/update/{query_id}")
@router.post("/update/{query_id}") async def query_update(
async def query_update( query_id: int,
query_id: int, request: Request,
request: Request, user: dict = Depends(get_current_user),
user: dict = Depends(get_current_user), name: str = Form(...),
name: str = Form(...), query_text: str = Form(...),
query_text: str = Form(...), description: str = Form(""),
description: str = Form(""), ):
): conn = get_connection()
conn = get_connection() conn.execute(
conn.execute( "UPDATE queries SET name = ?, query_text = ?, description = ? WHERE id = ?",
"UPDATE queries SET name = ?, query_text = ?, description = ? WHERE id = ?", (name, query_text, description, query_id),
(name, query_text, description, query_id), )
) conn.commit()
conn.commit() conn.close()
conn.close() return RedirectResponse("/queries", status_code=302)
return RedirectResponse("/queries", status_code=302)
@router.post("/run-sql")
@router.post("/run-sql") async def run_sql(
async def run_sql( request: Request,
request: Request, sql: str = Form(...),
sql: str = Form(...), user: dict = Depends(get_current_user),
user: dict = Depends(get_current_user), ):
): from app.services.firebird_service import FirebirdService, get_firebird_from_config
from app.services.firebird_service import FirebirdService conn = get_connection()
conn = get_connection() configs = {row["key"]: row["value"] for row in conn.execute("SELECT * FROM config").fetchall()}
configs = {row["key"]: row["value"] for row in conn.execute("SELECT * FROM config").fetchall()} conn.close()
conn.close()
fb = FirebirdService()
fb = FirebirdService() fb_success, fb_msg = fb.connect(
fb_success, fb_msg = fb.connect( configs.get("firebird_host", "localhost"),
configs.get("firebird_host", "localhost"), int(configs.get("firebird_port", 3050)),
int(configs.get("firebird_port", 3050)), configs.get("firebird_database", ""),
configs.get("firebird_database", ""), configs.get("firebird_user", "SYSDBA"),
configs.get("firebird_user", "SYSDBA"), configs.get("firebird_password", "masterkey"),
configs.get("firebird_password", "masterkey"), )
) if not fb_success:
if not fb_success: return JSONResponse({"error": f"Error Firebird: {fb_msg}"})
return JSONResponse({"error": f"Error Firebird: {fb_msg}"})
success, fb_err, rows = fb.execute_query(sql)
success, fb_err, rows = fb.execute_query(sql) fb.disconnect()
fb.disconnect()
if not success:
if not success: return JSONResponse({"error": fb_err})
return JSONResponse({"error": fb_err})
limit = rows[:200] if rows else []
limit = rows[:200] if rows else [] return JSONResponse({
return JSONResponse({ "rows": limit,
"rows": limit, "total": len(rows),
"total": len(rows), "columns": list(limit[0].keys()) if limit else []
"columns": list(limit[0].keys()) if limit else [] })
})
@router.post("/delete/{query_id}")
@router.post("/delete/{query_id}") async def query_delete(query_id: int, user: dict = Depends(get_current_user)):
async def query_delete(query_id: int, user: dict = Depends(get_current_user)): conn = get_connection()
conn = get_connection() conn.execute("DELETE FROM queries WHERE id = ?", (query_id,))
conn.execute("DELETE FROM queries WHERE id = ?", (query_id,)) conn.commit()
conn.commit() conn.close()
conn.close() return RedirectResponse("/queries", status_code=302)
return RedirectResponse("/queries", status_code=302)
@router.post("/esquema")
@router.post("/esquema") async def obtener_esquema(user: dict = Depends(get_current_user)):
async def obtener_esquema(user: dict = Depends(get_current_user)): from app.services.firebird_service import FirebirdService, get_firebird_from_config
from app.services.firebird_service import FirebirdService conn = get_connection()
conn = get_connection() configs = {row["key"]: row["value"] for row in conn.execute("SELECT * FROM config").fetchall()}
configs = {row["key"]: row["value"] for row in conn.execute("SELECT * FROM config").fetchall()} conn.close()
conn.close()
fb = FirebirdService()
fb = FirebirdService() fb_success, fb_msg = fb.connect(
fb_success, fb_msg = fb.connect( configs.get("firebird_host", "localhost"),
configs.get("firebird_host", "localhost"), int(configs.get("firebird_port", 3050)),
int(configs.get("firebird_port", 3050)), configs.get("firebird_database", ""),
configs.get("firebird_database", ""), configs.get("firebird_user", "SYSDBA"),
configs.get("firebird_user", "SYSDBA"), configs.get("firebird_password", "masterkey"),
configs.get("firebird_password", "masterkey"), )
) if not fb_success:
if not fb_success: return JSONResponse({"error": f"Error Firebird: {fb_msg}"}, status_code=400)
return JSONResponse({"error": f"Error Firebird: {fb_msg}"}, status_code=400)
success, fb_err, tablas = fb.execute_query("""
success, fb_err, tablas = fb.execute_query("""␍␍␍ SELECT RDB$RELATION_NAME as nombre
SELECT RDB$RELATION_NAME as nombre␍␍␍ FROM RDB$RELATIONS
FROM RDB$RELATIONS␍␍␍ WHERE RDB$SYSTEM_FLAG = 0
WHERE RDB$SYSTEM_FLAG = 0␍␍␍ AND RDB$RELATION_NAME NOT LIKE 'RDB$%'
AND RDB$RELATION_NAME NOT LIKE 'RDB$%'␍␍␍ ORDER BY 1
ORDER BY 1␍␍␍ """, {})
""", {})
if not success:
if not success: fb.disconnect()
fb.disconnect() return JSONResponse({"error": fb_err}, status_code=400)
return JSONResponse({"error": fb_err}, status_code=400)
resultado = []
resultado = [] for t in tablas:
for t in tablas: nombre = t["NOMBRE"].strip()
nombre = t["NOMBRE"].strip() ok, err, cols = fb.execute_query("""
ok, err, cols = fb.execute_query(f"""␍␍ SELECT
SELECT␍␍ rf.RDB$FIELD_NAME as COLUMN_NAME,
rf.RDB$FIELD_NAME as COLUMN_NAME,␍␍ f.RDB$FIELD_TYPE as FIELD_TYPE,
f.RDB$FIELD_TYPE as FIELD_TYPE,␍␍ f.RDB$FIELD_LENGTH as FIELD_LENGTH
f.RDB$FIELD_LENGTH as FIELD_LENGTH␍␍ FROM RDB$RELATION_FIELDS rf
FROM RDB$RELATION_FIELDS rf␍␍ JOIN RDB$FIELDS f ON rf.RDB$FIELD_SOURCE = f.RDB$FIELD_NAME
JOIN RDB$FIELDS f ON rf.RDB$FIELD_SOURCE = f.RDB$FIELD_NAME␍␍ WHERE rf.RDB$RELATION_NAME = :nombre
WHERE rf.RDB$RELATION_NAME = '{nombre}'␍␍ ORDER BY rf.RDB$FIELD_POSITION
ORDER BY rf.RDB$FIELD_POSITION␍␍ """, {"nombre": nombre})
""") columnas = []
columnas = [] if ok:
if ok: for c in cols:
for c in cols: tiponum = c["FIELD_TYPE"]
tiponum = c["FIELD_TYPE"] tipos = {7: "SMALLINT", 8: "INTEGER", 10: "FLOAT", 12: "DATE", 13: "TIME",
tipos = {7: "SMALLINT", 8: "INTEGER", 10: "FLOAT", 12: "DATE", 13: "TIME", 14: "CHAR", 16: "BIGINT", 27: "DOUBLE", 35: "TIMESTAMP", 37: "VARCHAR",
14: "CHAR", 16: "BIGINT", 27: "DOUBLE", 35: "TIMESTAMP", 37: "VARCHAR", 40: "BLOB", 45: "BLOB_ID", 261: "BLOB"}
40: "BLOB", 45: "BLOB_ID", 261: "BLOB"} columnas.append({
columnas.append({ "nombre": c["COLUMN_NAME"].strip(),
"nombre": c["COLUMN_NAME"].strip(), "tipo": tipos.get(tiponum, f"UNKNOWN({tiponum})"),
"tipo": tipos.get(tiponum, f"UNKNOWN({tiponum})"), "longitud": c["FIELD_LENGTH"]
"longitud": c["FIELD_LENGTH"] })
}) resultado.append({"tabla": nombre, "columnas": columnas})
resultado.append({"tabla": nombre, "columnas": columnas})
fb.disconnect()
fb.disconnect() return JSONResponse({"tablas": resultado})
return JSONResponse({"tablas": resultado})
@router.post("/tablas")
@router.post("/tablas") async def listar_tablas(user: dict = Depends(get_current_user)):
async def listar_tablas(user: dict = Depends(get_current_user)): from app.services.firebird_service import FirebirdService, get_firebird_from_config
from app.services.firebird_service import FirebirdService conn = get_connection()
conn = get_connection() configs = {row["key"]: row["value"] for row in conn.execute("SELECT * FROM config").fetchall()}
configs = {row["key"]: row["value"] for row in conn.execute("SELECT * FROM config").fetchall()} conn.close()
conn.close()
fb = FirebirdService()
fb = FirebirdService() fb_success, fb_msg = fb.connect(
fb_success, fb_msg = fb.connect( configs.get("firebird_host", "localhost"),
configs.get("firebird_host", "localhost"), int(configs.get("firebird_port", 3050)),
int(configs.get("firebird_port", 3050)), configs.get("firebird_database", ""),
configs.get("firebird_database", ""), configs.get("firebird_user", "SYSDBA"),
configs.get("firebird_user", "SYSDBA"), configs.get("firebird_password", "masterkey"),
configs.get("firebird_password", "masterkey"), )
) if not fb_success:
if not fb_success: return JSONResponse({"error": f"Error Firebird: {fb_msg}"}, status_code=400)
return JSONResponse({"error": f"Error Firebird: {fb_msg}"}, status_code=400)
success, fb_err, rows = fb.execute_query("""
success, fb_err, rows = fb.execute_query("""␍␍␍␍ SELECT RDB$RELATION_NAME as nombre
SELECT RDB$RELATION_NAME as nombre␍␍␍␍ FROM RDB$RELATIONS
FROM RDB$RELATIONS␍␍␍␍ WHERE RDB$SYSTEM_FLAG = 0
WHERE RDB$SYSTEM_FLAG = 0␍␍␍␍ AND RDB$RELATION_NAME NOT LIKE 'RDB$%'
AND RDB$RELATION_NAME NOT LIKE 'RDB$%'␍␍␍␍ ORDER BY 1
ORDER BY 1␍␍␍␍ """, {})
""", {}) fb.disconnect()
fb.disconnect()
if not success:
if not success: return JSONResponse({"error": fb_err}, status_code=400)
return JSONResponse({"error": fb_err}, status_code=400)
return JSONResponse({
return JSONResponse({ "tablas": [r["NOMBRE"].strip() for r in rows]
"tablas": [r["NOMBRE"].strip() for r in rows] })
})
@router.post("/test")
@router.post("/test") async def query_test(
async def query_test( request: Request,
request: Request, query_id: int = Form(...),
query_id: int = Form(...), user: dict = Depends(get_current_user),
user: dict = Depends(get_current_user), ):
): from app.services.firebird_service import FirebirdService, get_firebird_from_config
from app.services.firebird_service import FirebirdService
conn = get_connection()
conn = get_connection() q = conn.execute("SELECT * FROM queries WHERE id = ?", (query_id,)).fetchone()
q = conn.execute("SELECT * FROM queries WHERE id = ?", (query_id,)).fetchone() configs = {row["key"]: row["value"] for row in conn.execute("SELECT * FROM config").fetchall()}
configs = {row["key"]: row["value"] for row in conn.execute("SELECT * FROM config").fetchall()} conn.close()
conn.close()
if not q:
if not q: return JSONResponse({"error": "Consulta no encontrada"}, status_code=404)
return JSONResponse({"error": "Consulta no encontrada"}, status_code=404)
fb = FirebirdService()
fb = FirebirdService() fb_success, fb_msg = fb.connect(
fb_success, fb_msg = fb.connect( configs.get("firebird_host", "localhost"),
configs.get("firebird_host", "localhost"), int(configs.get("firebird_port", 3050)),
int(configs.get("firebird_port", 3050)), configs.get("firebird_database", ""),
configs.get("firebird_database", ""), configs.get("firebird_user", "SYSDBA"),
configs.get("firebird_user", "SYSDBA"), configs.get("firebird_password", "masterkey"),
configs.get("firebird_password", "masterkey"), )
) if not fb_success:
if not fb_success: return JSONResponse({"error": f"Error Firebird: {fb_msg}"}, status_code=400)
return JSONResponse({"error": f"Error Firebird: {fb_msg}"}, status_code=400)
success, fb_err, rows = fb.execute_query(q["query_text"], {})
success, fb_err, rows = fb.execute_query(q["query_text"], {}) fb.disconnect()
fb.disconnect()
if not success:
if not success: return JSONResponse({"error": fb_err}, status_code=400)
return JSONResponse({"error": fb_err}, status_code=400)
limit = rows[:20] if rows else []
limit = rows[:20] if rows else [] return JSONResponse({
return JSONResponse({ "rows": limit,
"rows": limit, "total": len(rows),
"total": len(rows), "columns": list(limit[0].keys()) if limit else []
"columns": list(limit[0].keys()) if limit else [] })
})
+33 -43
View File
@@ -1,13 +1,23 @@
import json
import httpx
from datetime import datetime
from fastapi import APIRouter, Request, Form, Depends from fastapi import APIRouter, Request, Form, Depends
from fastapi.responses import RedirectResponse, JSONResponse from fastapi.responses import JSONResponse
from app.database import get_connection from app.database import get_connection
from app.auth import get_current_user from app.auth import get_current_user
from app.services.firebird_service import FirebirdService from app.services.firebird_service import get_firebird_from_config
from app.services.json_generator import generar_terceros from app.services.json_generator import generar_terceros
router = APIRouter(prefix="/terceros", tags=["terceros"]) router = APIRouter(prefix="/terceros", tags=["terceros"])
def _load_configs() -> dict:
conn = get_connection()
configs = {row["key"]: row["value"] for row in conn.execute("SELECT * FROM config").fetchall()}
conn.close()
return configs
@router.get("") @router.get("")
async def terceros_page(request: Request, user: dict = Depends(get_current_user)): async def terceros_page(request: Request, user: dict = Depends(get_current_user)):
conn = get_connection() conn = get_connection()
@@ -41,6 +51,7 @@ async def test_connection(
fb_user: str = Form(...), fb_user: str = Form(...),
fb_password: str = Form(...), fb_password: str = Form(...),
): ):
from app.services.firebird_service import FirebirdService
fb = FirebirdService() fb = FirebirdService()
success, msg = fb.connect(host, port, database, fb_user, fb_password) success, msg = fb.connect(host, port, database, fb_user, fb_password)
if success: if success:
@@ -63,16 +74,9 @@ async def preview_query(
if not q: if not q:
return JSONResponse({"success": False, "message": "Consulta no encontrada"}) return JSONResponse({"success": False, "message": "Consulta no encontrada"})
fb = FirebirdService() fb, ok, msg = get_firebird_from_config(configs)
fb_success, fb_msg = fb.connect( if not ok:
configs.get("firebird_host", "localhost"), return JSONResponse({"success": False, "message": f"Error Firebird: {msg}"})
int(configs.get("firebird_port", 3050)),
configs.get("firebird_database", ""),
configs.get("firebird_user", "SYSDBA"),
configs.get("firebird_password", "masterkey"),
)
if not fb_success:
return JSONResponse({"success": False, "message": f"Error Firebird: {fb_msg}"})
params = {} params = {}
if ":doc_num" in q["query_text"] and doc_num: if ":doc_num" in q["query_text"] and doc_num:
@@ -89,9 +93,7 @@ async def preview_query(
if not success: if not success:
return JSONResponse({"success": False, "message": error}) return JSONResponse({"success": False, "message": error})
json_result = None json_result = generar_terceros(rows[0]) if rows else None
if rows:
json_result = generar_terceros(rows[0])
return JSONResponse({ return JSONResponse({
"success": True, "success": True,
@@ -109,10 +111,6 @@ async def send_terceros(
query_id: int = Form(...), query_id: int = Form(...),
doc_num: str = Form(""), doc_num: str = Form(""),
): ):
import json
import httpx
from datetime import datetime
conn = get_connection() conn = get_connection()
q = conn.execute("SELECT * FROM queries WHERE id = ?", (query_id,)).fetchone() q = conn.execute("SELECT * FROM queries WHERE id = ?", (query_id,)).fetchone()
configs = {row["key"]: row["value"] for row in conn.execute("SELECT * FROM config").fetchall()} configs = {row["key"]: row["value"] for row in conn.execute("SELECT * FROM config").fetchall()}
@@ -121,16 +119,9 @@ async def send_terceros(
if not q: if not q:
return JSONResponse({"success": False, "message": "Consulta no encontrada"}) return JSONResponse({"success": False, "message": "Consulta no encontrada"})
fb = FirebirdService() fb, ok, msg = get_firebird_from_config(configs)
fb_success, fb_msg = fb.connect( if not ok:
configs.get("firebird_host", "localhost"), return JSONResponse({"success": False, "message": f"Error Firebird: {msg}"})
int(configs.get("firebird_port", 3050)),
configs.get("firebird_database", ""),
configs.get("firebird_user", "SYSDBA"),
configs.get("firebird_password", "masterkey"),
)
if not fb_success:
return JSONResponse({"success": False, "message": f"Error Firebird: {fb_msg}"})
params = {} params = {}
if ":doc_num" in q["query_text"]: if ":doc_num" in q["query_text"]:
@@ -141,51 +132,50 @@ async def send_terceros(
if not success: if not success:
return JSONResponse({"success": False, "message": error}) return JSONResponse({"success": False, "message": error})
if not rows: if not rows:
return JSONResponse({"success": False, "message": "No se encontraron datos"}) return JSONResponse({"success": False, "message": "No se encontraron datos"})
# Generar JSON terceros
tercero_json = generar_terceros(rows[0]) tercero_json = generar_terceros(rows[0])
# Enviar a API
api_url = configs.get("api_url", "") api_url = configs.get("api_url", "")
api_key = configs.get("api_key", "") api_key = configs.get("api_key", "")
api_method = configs.get("api_method", "POST") api_method = configs.get("api_method", "POST")
headers = {"Content-Type": "application/json"} headers = {"Content-Type": "application/json"}
if api_key: if api_key:
headers["Authorization"] = f"Bearer {api_key}" headers["Authorization"] = f"Bearer {api_key}"
resp_ok = False
resp_text = ""
resp_code = 0
try: try:
async with httpx.AsyncClient(timeout=int(configs.get("api_timeout", 30))) as client: async with httpx.AsyncClient(timeout=int(configs.get("api_timeout", 30))) as client:
if api_method == "POST": if api_method == "POST":
resp = await client.post(api_url + "/terceros", json=tercero_json, headers=headers) resp = await client.post(api_url + "/terceros", json=tercero_json, headers=headers)
else: else:
resp = await client.put(api_url + "/terceros", json=tercero_json, headers=headers) resp = await client.put(api_url + "/terceros", json=tercero_json, headers=headers)
resp_ok = resp.is_success
result = resp.status_code, resp.is_success, resp.text resp_text = resp.text
resp_code = resp.status_code
except Exception as e: except Exception as e:
result = (0, False, str(e)) resp_text = str(e)
# Guardar log
conn = get_connection() conn = get_connection()
conn.execute(""" conn.execute("""
INSERT INTO envios (user_id, tipo, status, json_enviado, respuesta_api, created_at) INSERT INTO envios (user_id, tipo, status, json_enviado, respuesta_api, created_at)
VALUES (?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?)
""", ( """, (
user["user_id"], "terceros", user["user_id"], "terceros",
"success" if result[1] else "error", "success" if resp_ok else "error",
json.dumps(tercero_json, indent=2, ensure_ascii=False), json.dumps(tercero_json, indent=2, ensure_ascii=False),
str(result[2])[:1000], resp_text[:1000],
datetime.now().isoformat(), datetime.now().isoformat(),
)) ))
conn.commit() conn.commit()
conn.close() conn.close()
return JSONResponse({ return JSONResponse({
"success": result[1], "success": resp_ok,
"status_code": result[0], "status_code": resp_code,
"message": "Envío exitoso" if result[1] else f"Error: {result[2]}", "message": "Envío exitoso" if resp_ok else f"Error: {resp_text}",
"cuv": result[2][:200] if result[1] else None, "cuv": resp_text[:200] if resp_ok else None,
}) })
+52 -90
View File
@@ -1,9 +1,12 @@
import json as json_lib
import httpx
from datetime import datetime
from fastapi import APIRouter, Request, Form, Depends from fastapi import APIRouter, Request, Form, Depends
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from app.database import get_connection from app.database import get_connection
from app.auth import get_current_user from app.auth import get_current_user
from app.services.firebird_service import FirebirdService from app.services.firebird_service import get_firebird_from_config
from app.services.json_generator import generar_terceros, generar_transaccion from app.services.json_generator import generar_transaccion, agrupar_por_factura
router = APIRouter(prefix="/transaccion", tags=["transaccion"]) router = APIRouter(prefix="/transaccion", tags=["transaccion"])
@@ -45,16 +48,9 @@ async def preview_transaccion(
if not q: if not q:
return JSONResponse({"success": False, "message": "Consulta no encontrada"}) return JSONResponse({"success": False, "message": "Consulta no encontrada"})
fb = FirebirdService() fb, ok, msg = get_firebird_from_config(configs)
fb_success, fb_msg = fb.connect( if not ok:
configs.get("firebird_host", "localhost"), return JSONResponse({"success": False, "message": f"Error Firebird: {msg}"})
int(configs.get("firebird_port", 3050)),
configs.get("firebird_database", ""),
configs.get("firebird_user", "SYSDBA"),
configs.get("firebird_password", "masterkey"),
)
if not fb_success:
return JSONResponse({"success": False, "message": f"Error Firebird: {fb_msg}"})
params = {"fecha_ini": fecha_inicio, "fecha_fin": fecha_fin} params = {"fecha_ini": fecha_inicio, "fecha_fin": fecha_fin}
if ":factura" in q["query_text"] and factura: if ":factura" in q["query_text"] and factura:
@@ -66,26 +62,14 @@ async def preview_transaccion(
if not success: if not success:
return JSONResponse({"success": False, "message": error}) return JSONResponse({"success": False, "message": error})
# Agrupar por paciente y factura grupos = agrupar_por_factura(rows, factura)
from collections import defaultdict
grupos = defaultdict(lambda: {"factura": "", "procedimientos": [], "paciente": {}})
for row in rows:
doc_key = (row.get("tipo_doc_paciente", "CC"), row.get("num_doc_paciente", ""))
fact = row.get("num_factura", factura)
grupos[(fact, doc_key)]["factura"] = fact
grupos[(fact, doc_key)]["procedimientos"].append(dict(row))
json_result = [] json_result = []
for (fact, doc_key), grupo in grupos.items(): for (fact, doc_key), grupo in grupos.items():
paciente_data = {"tipoDocumentoIdentificacion": doc_key[0], "numDocumentoIdentificacion": doc_key[1]} paciente_data = {"tipoDocumentoIdentificacion": doc_key[0], "numDocumentoIdentificacion": doc_key[1]}
trans = generar_transaccion( json_result.append(generar_transaccion(
fact, fact, configs.get("num_documento_obligado", ""),
configs.get("num_documento_obligado", ""), paciente_data, grupo["procedimientos"],
paciente_data, ))
grupo["procedimientos"],
)
json_result.append(trans)
return JSONResponse({ return JSONResponse({
"success": True, "success": True,
@@ -107,11 +91,6 @@ async def send_transaccion(
fecha_inicio: str = Form(...), fecha_inicio: str = Form(...),
fecha_fin: str = Form(...), fecha_fin: str = Form(...),
): ):
import json as json_lib
import httpx
from datetime import datetime
from collections import defaultdict
conn = get_connection() conn = get_connection()
q = conn.execute("SELECT * FROM queries WHERE id = ?", (query_id,)).fetchone() q = conn.execute("SELECT * FROM queries WHERE id = ?", (query_id,)).fetchone()
configs = {row["key"]: row["value"] for row in conn.execute("SELECT * FROM config").fetchall()} configs = {row["key"]: row["value"] for row in conn.execute("SELECT * FROM config").fetchall()}
@@ -120,16 +99,9 @@ async def send_transaccion(
if not q: if not q:
return JSONResponse({"success": False, "message": "Consulta no encontrada"}) return JSONResponse({"success": False, "message": "Consulta no encontrada"})
fb = FirebirdService() fb, ok, msg = get_firebird_from_config(configs)
fb_success, fb_msg = fb.connect( if not ok:
configs.get("firebird_host", "localhost"), return JSONResponse({"success": False, "message": f"Error Firebird: {msg}"})
int(configs.get("firebird_port", 3050)),
configs.get("firebird_database", ""),
configs.get("firebird_user", "SYSDBA"),
configs.get("firebird_password", "masterkey"),
)
if not fb_success:
return JSONResponse({"success": False, "message": f"Error Firebird: {fb_msg}"})
params = {"fecha_ini": fecha_inicio, "fecha_fin": fecha_fin} params = {"fecha_ini": fecha_inicio, "fecha_fin": fecha_fin}
if ":factura" in q["query_text"] and factura: if ":factura" in q["query_text"] and factura:
@@ -143,14 +115,7 @@ async def send_transaccion(
if not rows: if not rows:
return JSONResponse({"success": False, "message": "No se encontraron datos"}) return JSONResponse({"success": False, "message": "No se encontraron datos"})
# Agrupar grupos = agrupar_por_factura(rows, factura)
grupos = defaultdict(lambda: {"factura": "", "procedimientos": [], "paciente": {}})
for row in rows:
doc_key = (row.get("tipo_doc_paciente", "CC"), row.get("num_doc_paciente", ""))
fact = row.get("num_factura", factura)
grupos[(fact, doc_key)]["factura"] = fact
grupos[(fact, doc_key)]["procedimientos"].append(dict(row))
api_url = configs.get("api_url", "") api_url = configs.get("api_url", "")
api_key = configs.get("api_key", "") api_key = configs.get("api_key", "")
api_method = configs.get("api_method", "POST") api_method = configs.get("api_method", "POST")
@@ -162,52 +127,49 @@ async def send_transaccion(
total_errores = 0 total_errores = 0
resultados = [] resultados = []
for (fact, doc_key), grupo in grupos.items(): async with httpx.AsyncClient(timeout=int(configs.get("api_timeout", 30))) as client:
paciente_data = {"tipoDocumentoIdentificacion": doc_key[0], "numDocumentoIdentificacion": doc_key[1]} for (fact, doc_key), grupo in grupos.items():
trans_json = generar_transaccion( paciente_data = {"tipoDocumentoIdentificacion": doc_key[0], "numDocumentoIdentificacion": doc_key[1]}
fact, trans_json = generar_transaccion(
configs.get("num_documento_obligado", ""), fact, configs.get("num_documento_obligado", ""),
paciente_data, paciente_data, grupo["procedimientos"],
grupo["procedimientos"], )
)
try: status_ok = False
async with httpx.AsyncClient(timeout=int(configs.get("api_timeout", 30))) as client: response_text = ""
try:
if api_method == "POST": if api_method == "POST":
resp = await client.post(api_url + "/transaccion", json=trans_json, headers=headers) resp = await client.post(api_url + "/transaccion", json=trans_json, headers=headers)
else: else:
resp = await client.put(api_url + "/transaccion", json=trans_json, headers=headers) resp = await client.put(api_url + "/transaccion", json=trans_json, headers=headers)
status_ok = resp.is_success
response_text = resp.text[:1000]
except Exception as e:
response_text = str(e)
status_ok = resp.is_success if status_ok:
response_text = resp.text[:1000] total_enviados += 1
except Exception as e: else:
status_ok = False total_errores += 1
response_text = str(e)
if status_ok: resultados.append({"factura": fact, "success": status_ok})
total_enviados += 1
else:
total_errores += 1
resultados.append({"factura": fact, "success": status_ok}) conn = get_connection()
conn.execute("""
# Guardar log INSERT INTO envios (user_id, tipo, factura, fecha_inicio, fecha_fin,
conn = get_connection() pacientes_count, servicios_count, status, json_enviado, respuesta_api, created_at)
conn.execute(""" VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
INSERT INTO envios (user_id, tipo, factura, fecha_inicio, fecha_fin, """, (
pacientes_count, servicios_count, status, json_enviado, respuesta_api, created_at) user["user_id"], "transaccion", fact,
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) fecha_inicio, fecha_fin,
""", ( 1, len(grupo["procedimientos"]),
user["user_id"], "transaccion", fact, "success" if status_ok else "error",
fecha_inicio, fecha_fin, json_lib.dumps(trans_json, indent=2, ensure_ascii=False)[:5000],
1, len(grupo["procedimientos"]), response_text,
"success" if status_ok else "error", datetime.now().isoformat(),
json_lib.dumps(trans_json, indent=2, ensure_ascii=False)[:5000], ))
response_text, conn.commit()
datetime.now().isoformat(), conn.close()
))
conn.commit()
conn.close()
return JSONResponse({ return JSONResponse({
"success": total_errores == 0, "success": total_errores == 0,
+13 -1
View File
@@ -44,7 +44,7 @@ class FirebirdService:
return False, "No hay conexión activa", [] return False, "No hay conexión activa", []
try: try:
cur = self.conn.cursor() cur = self.conn.cursor()
if params: if params is not None:
cur.execute(query, params) cur.execute(query, params)
else: else:
cur.execute(query) cur.execute(query)
@@ -53,3 +53,15 @@ class FirebirdService:
return True, "", [dict(zip(columns, row)) for row in rows] return True, "", [dict(zip(columns, row)) for row in rows]
except Exception as e: except Exception as e:
return False, str(e), [] return False, str(e), []
def get_firebird_from_config(configs: dict) -> tuple:
fb = FirebirdService()
ok, msg = fb.connect(
configs.get("firebird_host", "localhost"),
int(configs.get("firebird_port", 3050)),
configs.get("firebird_database", ""),
configs.get("firebird_user", "SYSDBA"),
configs.get("firebird_password", "masterkey"),
)
return fb, ok, msg
+11
View File
@@ -1,3 +1,4 @@
from collections import defaultdict
from datetime import datetime from datetime import datetime
from typing import Optional from typing import Optional
@@ -49,6 +50,16 @@ def generar_procedimiento(row: dict, consecutivo: int) -> dict:
} }
def agrupar_por_factura(rows: list, factura_default: str = "") -> dict:
grupos = defaultdict(lambda: {"factura": "", "procedimientos": []})
for row in rows:
doc_key = (row.get("tipo_doc_paciente", "CC"), row.get("num_doc_paciente", ""))
fact = row.get("num_factura", factura_default)
grupos[(fact, doc_key)]["factura"] = fact
grupos[(fact, doc_key)]["procedimientos"].append(dict(row))
return grupos
def generar_transaccion( def generar_transaccion(
factura: str, factura: str,
num_doc_obligado: str, num_doc_obligado: str,
+434 -403
View File
@@ -1,403 +1,434 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Consultas SQL{% endblock %} {% block title %}SQL Workbench{% endblock %}
{% block header %}Consultas SQL{% endblock %} {% block header %}SQL Workbench{% endblock %}
{% block content %} {% block content %}
<div class="grid grid-cols-1 lg:grid-cols-4 gap-6"> <style>
<div class="lg:col-span-3"> .CodeMirror { height: 180px; font-size: 13px; }
<div class="flex space-x-1 border-b border-gray-200 mb-4"> .cm-s-default .cm-keyword { color: #0000ff; font-weight: bold; }
<button onclick="cambiarTab('probador')" class="tab-btn px-4 py-2 text-sm font-medium border-b-2 border-blue-600 text-blue-600" id="tab-probador"><i class="fas fa-play mr-1"></i> Probador</button> .schema-col:hover { background: #eff6ff; }
<button onclick="cambiarTab('constructor')" class="tab-btn px-4 py-2 text-sm font-medium text-gray-500 hover:text-gray-700 border-b-2 border-transparent" id="tab-constructor"><i class="fas fa-wrench mr-1"></i> Constructor</button> .schema-table-row:hover { background: #f3f4f6; }
<button onclick="cambiarTab('editor')" class="tab-btn px-4 py-2 text-sm font-medium text-gray-500 hover:text-gray-700 border-b-2 border-transparent" id="tab-editor"><i class="fas fa-code mr-1"></i> Editor</button> #result-table table { border-collapse: collapse; width: 100%; }
</div> #result-table th { position: sticky; top: 0; background: #f9fafb; z-index: 1; }
#result-table td, #result-table th { padding: 4px 10px; border-bottom: 1px solid #e5e7eb; white-space: nowrap; font-size: 12px; text-align: left; }
<div id="panel-probador"> #result-table tr:hover td { background: #eff6ff; }
<div class="bg-white rounded-xl shadow-sm border border-gray-200"> #result-table tr:nth-child(even) td { background: #fafafa; }
<div class="p-4 border-b border-gray-200 flex items-center justify-between"> #result-table tr:nth-child(even):hover td { background: #eff6ff; }
<h3 class="text-sm font-semibold text-gray-700"><i class="fas fa-terminal mr-2 text-green-500"></i>Escribí tu SQL y ejecutalo</h3> </style>
<div class="flex space-x-2">
<button onclick="ejecutarProbador()" class="px-4 py-1.5 bg-green-600 hover:bg-green-700 text-white text-xs rounded-lg"><i class="fas fa-play mr-1"></i> Ejecutar</button> <div class="flex gap-3" style="height: calc(100vh - 148px);">
<button onclick="limpiarProbador()" class="px-3 py-1.5 bg-gray-200 hover:bg-gray-300 text-gray-700 text-xs rounded-lg"><i class="fas fa-eraser mr-1"></i> Limpiar</button>
</div> <!-- ── LEFT PANEL ─────────────────────────────────────────────── -->
</div> <div class="flex flex-col gap-3" style="width: 240px; flex-shrink: 0;">
<div>
<textarea id="probador-sql" rows="6" class="w-full px-4 py-3 text-sm font-mono border-0 focus:ring-0" placeholder="SELECT * FROM ..."></textarea> <!-- Schema Browser -->
</div> <div class="bg-white rounded-xl border border-gray-200 flex flex-col" style="flex: 1; min-height: 0;">
<div id="probador-resultados" class="border-t border-gray-200 overflow-x-auto max-h-96 overflow-y-auto"></div> <div class="px-3 py-2 border-b border-gray-200 flex items-center justify-between flex-shrink-0">
<div id="probador-info" class="px-4 py-2 text-xs text-gray-500 border-t border-gray-200"></div> <span class="text-xs font-semibold text-gray-600 uppercase tracking-wide">Esquema</span>
</div> <button id="btn-conectar" onclick="cargarEsquema()"
</div> class="text-xs text-blue-600 hover:text-blue-800 flex items-center gap-1">
<i class="fas fa-plug"></i> Conectar
<div id="panel-constructor" class="hidden"> </button>
<div class="bg-white rounded-xl shadow-sm border border-gray-200"> </div>
<div class="px-4 py-3 border-b border-gray-200 flex items-center justify-between"> <div id="schema-search-wrap" class="hidden px-2 pt-2 flex-shrink-0">
<h3 class="text-sm font-semibold text-gray-700"><i class="fas fa-wrench mr-2 text-blue-500"></i>Constructor Visual</h3> <input id="schema-search" type="text" placeholder="Buscar tabla..."
<button onclick="cargarEsquema()" class="px-3 py-1 bg-green-600 hover:bg-green-700 text-white text-xs rounded-lg"><i class="fas fa-sync mr-1"></i> Cargar esquema</button> oninput="filtrarTablas(this.value)"
</div> class="w-full text-xs border border-gray-200 rounded px-2 py-1 focus:outline-none focus:border-blue-400">
<div class="p-4"> </div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4"> <div id="schema-tree" class="flex-1 overflow-y-auto p-2 text-xs">
<div> <p class="text-gray-400 italic text-center mt-4">Presioná Conectar para<br>cargar el esquema Firebird</p>
<h4 class="text-xs font-semibold text-gray-700 mb-2">Tablas</h4> </div>
<div id="builder-tablas" class="space-y-1 max-h-64 overflow-y-auto border rounded p-2 bg-gray-50 text-xs"> </div>
<p class="text-gray-400 italic">Presiona "Cargar esquema"</p>
</div> <!-- Saved Queries -->
</div> <div class="bg-white rounded-xl border border-gray-200 flex flex-col" style="max-height: 38%; min-height: 120px;">
<div> <div class="px-3 py-2 border-b border-gray-200 flex items-center justify-between flex-shrink-0">
<h4 class="text-xs font-semibold text-gray-700 mb-2">Columnas</h4> <span class="text-xs font-semibold text-gray-600 uppercase tracking-wide">Consultas</span>
<div id="builder-columnas" class="border rounded p-2 bg-gray-50 min-h-[100px] text-xs"> <button onclick="abrirModalNueva()"
<p class="text-gray-400 italic">Seleccioná una tabla</p> class="text-xs text-blue-600 hover:text-blue-800"><i class="fas fa-plus"></i></button>
</div> </div>
<h4 class="text-xs font-semibold text-gray-700 mt-3 mb-2">Filtros</h4> <div class="overflow-y-auto flex-1">
<div id="builder-where" class="space-y-1 text-xs"></div> {% for q in queries %}
<button onclick="agregarFiltro()" class="mt-1 text-xs text-blue-600 hover:text-blue-800"><i class="fas fa-plus mr-1"></i> Agregar filtro</button> <div class="group flex items-center px-3 py-1.5 hover:bg-gray-50 cursor-pointer border-b border-gray-100 last:border-0"
</div> onclick="cargarConsulta({{ q.query_text | tojson }}, {{ q.name | tojson }}, {{ q.query_type | tojson }})">
<div> <div class="flex-1 min-w-0">
<h4 class="text-xs font-semibold text-gray-700 mb-2">SQL</h4> <div class="flex items-center gap-1.5">
<textarea id="builder-sql" rows="4" readonly class="w-full px-2 py-1 border rounded text-xs font-mono bg-gray-100"></textarea> <span class="text-xs px-1 rounded {% if q.query_type == 'terceros' %}bg-blue-100 text-blue-600{% else %}bg-purple-100 text-purple-600{% endif %}">
<p id="builder-params" class="text-xs text-blue-600 mt-1"></p> {{ q.query_type[:4] }}
<button onclick="ejecutarBuilder()" class="mt-2 px-3 py-1 bg-green-600 hover:bg-green-700 text-white text-xs rounded-lg"><i class="fas fa-play mr-1"></i> Ejecutar</button> </span>
<button onclick="usarSqlEditor()" class="mt-2 px-3 py-1 bg-blue-600 hover:bg-blue-700 text-white text-xs rounded-lg ml-1"><i class="fas fa-arrow-right mr-1"></i> Enviar al Editor</button> <span class="text-xs text-gray-800 truncate">{{ q.name }}</span>
</div> </div>
</div> </div>
<div id="builder-resultados" class="mt-3 border-t border-gray-200 pt-3 overflow-x-auto max-h-60 overflow-y-auto"></div> <div class="hidden group-hover:flex items-center gap-1 ml-1 flex-shrink-0">
</div> <button onclick="event.stopPropagation();editarConsulta({{ q.id }}, {{ q.name | tojson }}, {{ q.query_type | tojson }}, {{ q.query_text | tojson }}, {{ (q.description or '') | tojson }})"
</div> class="text-blue-400 hover:text-blue-600 p-0.5"><i class="fas fa-edit text-xs"></i></button>
</div> <form method="POST" action="/queries/delete/{{ q.id }}" onsubmit="return confirm('¿Eliminar consulta?')" class="inline">
<button type="submit" class="text-red-400 hover:text-red-600 p-0.5"><i class="fas fa-trash text-xs"></i></button>
<div id="panel-editor" class="hidden"> </form>
<div class="bg-white rounded-xl shadow-sm border border-gray-200"> </div>
<div class="px-4 py-3 border-b border-gray-200"> </div>
<h3 class="text-sm font-semibold text-gray-700"><i class="fas fa-save mr-2 text-blue-500"></i>Guardar consulta</h3> {% else %}
</div> <p class="text-xs text-gray-400 italic p-3 text-center">Sin consultas guardadas</p>
<div class="p-4"> {% endfor %}
<form method="POST" action="/queries/create" class="space-y-3"> </div>
<input type="hidden" name="query_id" id="query_id" value=""> </div>
<div class="grid grid-cols-2 gap-3"> </div>
<div>
<label class="block text-xs font-medium text-gray-700 mb-1">Nombre</label> <!-- ── RIGHT PANEL ────────────────────────────────────────────── -->
<input type="text" name="name" id="q_name" required class="w-full px-3 py-1.5 border border-gray-300 rounded-lg text-sm" placeholder="Nombre"> <div class="flex-1 flex flex-col gap-3 min-w-0">
</div>
<div> <!-- Toolbar -->
<label class="block text-xs font-medium text-gray-700 mb-1">Tipo</label> <div class="bg-white rounded-xl border border-gray-200 px-4 py-2.5 flex items-center gap-2 flex-shrink-0">
<select name="query_type" id="q_type" class="w-full px-3 py-1.5 border border-gray-300 rounded-lg text-sm"> <button onclick="ejecutar()" id="btn-run"
<option value="terceros">Terceros</option> class="flex items-center gap-2 px-4 py-1.5 bg-green-600 hover:bg-green-700 text-white text-sm rounded-lg font-medium">
<option value="transaccion">Transacción</option> <i class="fas fa-play text-xs"></i> Ejecutar
</select> </button>
</div> <span class="text-gray-200 select-none">|</span>
</div> <input type="text" id="q-name" placeholder="Nombre de la consulta"
<div> class="text-sm border border-gray-200 rounded-lg px-3 py-1.5 w-44 focus:outline-none focus:border-blue-400">
<label class="block text-xs font-medium text-gray-700 mb-1">SQL</label> <select id="q-type" class="text-sm border border-gray-200 rounded-lg px-3 py-1.5 focus:outline-none focus:border-blue-400">
<textarea name="query_text" id="q_text" rows="8" required class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm font-mono" placeholder="PEGA ACA LA SQL YA PROBADA"></textarea> <option value="terceros">Terceros</option>
</div> <option value="transaccion">Transacción</option>
<div> </select>
<label class="block text-xs font-medium text-gray-700 mb-1">Descripción</label> <button onclick="guardar()"
<input type="text" name="description" id="q_desc" class="w-full px-3 py-1.5 border border-gray-300 rounded-lg text-sm" placeholder="Descripción"> class="flex items-center gap-1 px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white text-sm rounded-lg">
</div> <i class="fas fa-save text-xs"></i> Guardar
<div class="flex space-x-2"> </button>
<button type="submit" class="px-4 py-1.5 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm"><i class="fas fa-save mr-1"></i> Guardar</button> <button id="btn-cancel" onclick="cancelarEdicion()"
<button type="button" onclick="cancelEdit()" class="px-4 py-1.5 bg-gray-100 text-gray-700 rounded-lg text-sm hover:bg-gray-200 hidden" id="btn-cancel"><i class="fas fa-times mr-1"></i> Cancelar</button> class="hidden items-center gap-1 px-3 py-1.5 bg-gray-100 hover:bg-gray-200 text-gray-700 text-sm rounded-lg">
</div> <i class="fas fa-times text-xs"></i> Cancelar
</form> </button>
</div> <span class="text-xs text-gray-400 ml-1 hidden" id="editing-label"></span>
</div> <div class="ml-auto text-xs" id="status-bar"></div>
</div> </div>
</div>
<!-- Editor -->
<div class="lg:col-span-1"> <div class="bg-white rounded-xl border border-gray-200 flex-shrink-0">
<div class="bg-white rounded-xl shadow-sm border border-gray-200"> <div class="flex items-center justify-between px-3 py-1.5 border-b border-gray-100">
<div class="px-4 py-3 border-b border-gray-200 flex items-center justify-between"> <span class="text-xs text-gray-500 font-mono" id="editor-label">SQL</span>
<h3 class="text-sm font-semibold text-gray-800"><i class="fas fa-list mr-2 text-blue-500"></i>Consultas</h3> <div class="flex items-center gap-2">
</div> <span class="text-xs text-gray-400">Ctrl+Enter para ejecutar · Ctrl+Space autocomplete</span>
<div class="p-3 space-y-2 max-h-[calc(100vh-200px)] overflow-y-auto"> <button onclick="limpiar()" class="text-xs text-gray-400 hover:text-gray-600"><i class="fas fa-eraser"></i></button>
{% for q in queries %} </div>
<div class="p-2 rounded-lg border border-gray-200 text-xs"> </div>
<div onclick="cargarConsulta({{ q.id }}, '{{ q.name }}', '{{ q.query_type }}', `{{ q.query_text|e }}`)" class="cursor-pointer"> <textarea id="sql-editor" class="hidden"></textarea>
<div class="flex items-center justify-between"> </div>
<span class="font-medium text-gray-800 truncate">{{ q.name }}</span>
<span class="px-1.5 py-0.5 rounded text-xs {% if q.query_type == 'terceros' %}bg-blue-100 text-blue-700{% else %}bg-purple-100 text-purple-700{% endif %}">{{ q.query_type[:4] }}</span> <!-- Results -->
</div> <div class="bg-white rounded-xl border border-gray-200 flex-1 flex flex-col min-h-0">
<p class="text-gray-500 mt-0.5 truncate">{{ q.description or 'Sin descripción' }}</p> <div class="px-4 py-2 border-b border-gray-100 flex items-center justify-between flex-shrink-0">
</div> <span class="text-xs font-semibold text-gray-600 uppercase tracking-wide">Resultados</span>
<div class="flex space-x-2 mt-1"> <span id="result-info" class="text-xs text-gray-400"></span>
<button onclick="event.stopPropagation();ejecutarConsultaId({{ q.id }})" class="text-green-600 hover:text-green-800"><i class="fas fa-play"></i></button> </div>
<button onclick="event.stopPropagation();cargarConsulta({{ q.id }}, '{{ q.name }}', '{{ q.query_type }}', `{{ q.query_text|e }}`)" class="text-blue-600 hover:text-blue-800"><i class="fas fa-edit"></i></button> <div id="result-table" class="flex-1 overflow-auto">
<form method="POST" action="/queries/delete/{{ q.id }}" class="inline" onsubmit="return confirm('Eliminar?')"> <div class="flex flex-col items-center justify-center h-full text-gray-300 select-none">
<button type="submit" class="text-red-500 hover:text-red-700"><i class="fas fa-trash"></i></button> <i class="fas fa-table text-4xl mb-3"></i>
</form> <p class="text-sm">Ejecutá una consulta para ver resultados</p>
</div> </div>
</div> </div>
{% endfor %} </div>
</div> </div>
</div> </div>
</div>
</div> <!-- ── MODAL GUARDAR ──────────────────────────────────────────────── -->
<div id="modal-save" class="hidden fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-40">
<script> <div class="bg-white rounded-xl shadow-xl w-full max-w-lg mx-4 p-6">
let sqlEditor = null; <h3 class="font-semibold text-gray-800 mb-4 text-base" id="modal-title">Guardar consulta</h3>
let esquema = []; <form id="form-save" method="POST" action="/queries/create" class="space-y-3">
let tablasSeleccionadas = {}; <input type="text" name="name" id="form-name" required placeholder="Nombre *"
let filtros = []; class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:border-blue-400">
let aliasCounter = 0; <div class="grid grid-cols-2 gap-3">
<select name="query_type" id="form-type"
function cambiarTab(tab) { class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:border-blue-400">
['probador','constructor','editor'].forEach(t => { <option value="terceros">Terceros</option>
document.getElementById('panel-'+t).classList.toggle('hidden', t !== tab); <option value="transaccion">Transacción</option>
const btn = document.getElementById('tab-'+t); </select>
if (t === tab) { <input type="text" name="description" id="form-desc" placeholder="Descripción"
btn.className = 'tab-btn px-4 py-2 text-sm font-medium border-b-2 border-blue-600 text-blue-600'; class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:border-blue-400">
} else { </div>
btn.className = 'tab-btn px-4 py-2 text-sm font-medium text-gray-500 hover:text-gray-700 border-b-2 border-transparent'; <textarea name="query_text" id="form-sql" rows="5"
} class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm font-mono focus:outline-none focus:border-blue-400"></textarea>
}); <div class="flex justify-end gap-2 pt-1">
if (tab === 'constructor' && !esquema.length) cargarEsquema(); <button type="button" onclick="cerrarModal()"
} class="px-4 py-2 bg-gray-100 text-gray-700 rounded-lg text-sm hover:bg-gray-200">Cancelar</button>
<button type="submit"
// ───── PROBADOR ───── class="px-4 py-2 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700">Guardar</button>
function ejecutarProbador() { </div>
const sql = document.getElementById('probador-sql').value.trim(); </form>
if (!sql) { showToast('Escribí una consulta SQL', 'warning'); return; } </div>
ejecutarSql(sql, 'probador-resultados', 'probador-info'); </div>
}
<script>
function limpiarProbador() { let editor, esquema = [], editandoId = null;
document.getElementById('probador-sql').value = '';
document.getElementById('probador-resultados').innerHTML = ''; // ── INIT CODEMIRROR ──────────────────────────────────────────────────
document.getElementById('probador-info').innerHTML = ''; document.addEventListener('DOMContentLoaded', () => {
} editor = CodeMirror.fromTextArea(document.getElementById('sql-editor'), {
mode: 'text/x-sql',
async function ejecutarSql(sql, outputId, infoId) { lineNumbers: true,
const outputDiv = document.getElementById(outputId); autofocus: true,
const infoDiv = document.getElementById(infoId); tabSize: 4,
outputDiv.innerHTML = '<div class="p-4 text-center"><i class="fas fa-spinner fa-spin mr-2"></i>Ejecutando...</div>'; indentWithTabs: false,
if (infoDiv) infoDiv.innerHTML = ''; lineWrapping: true,
try { extraKeys: {
const form = new FormData(); 'Ctrl-Space': 'autocomplete',
form.append('sql', sql); 'Cmd-Space': 'autocomplete',
const resp = await fetch('/queries/run-sql', { method: 'POST', body: form }); },
const data = await resp.json(); hintOptions: { tables: {} }
if (data.error) { });
outputDiv.innerHTML = `<div class="p-4 text-red-600 text-sm"><i class="fas fa-exclamation-circle mr-1"></i> ${data.error}</div>`; editor.setSize(null, 180);
return; editor.on('keydown', (cm, e) => {
} if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
let html = '<table class="w-full text-xs border-collapse"><thead><tr class="bg-gray-100">'; e.preventDefault();
data.columns.forEach(c => { html += '<th class="p-2 border text-left font-semibold whitespace-nowrap">' + c + '</th>'; }); ejecutar();
html += '</tr></thead><tbody>'; }
data.rows.slice(0, 200).forEach(r => { });
html += '<tr class="hover:bg-gray-50">'; });
data.columns.forEach(c => { html += '<td class="p-2 border whitespace-nowrap">' + (r[c] ?? '') + '</td>'; });
html += '</tr>'; // ── SCHEMA BROWSER ───────────────────────────────────────────────────
}); async function cargarEsquema() {
html += '</tbody></table>'; const btn = document.getElementById('btn-conectar');
outputDiv.innerHTML = html; const tree = document.getElementById('schema-tree');
if (infoDiv) infoDiv.innerHTML = `${data.total} registros (mostrando ${Math.min(data.total, 200)})`; btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
if (data.total > 200) infoDiv.innerHTML += ' <span class="text-yellow-600">- Resultado truncado a 200 filas</span>'; btn.disabled = true;
} catch(e) { tree.innerHTML = '<p class="text-gray-400 italic text-center mt-4">Conectando...</p>';
outputDiv.innerHTML = `<div class="p-4 text-red-600 text-sm">Error: ${e.message}</div>`;
} try {
} const resp = await fetch('/queries/esquema', { method: 'POST' });
const data = await resp.json();
// ───── CONSTRUCTOR ───── if (data.error) {
async function cargarEsquema() { tree.innerHTML = `<div class="p-2 text-red-500 text-xs">${data.error}</div>`;
const btn = document.querySelector('#panel-constructor .bg-green-600'); showToast(data.error, 'error');
btn.disabled = true; btn.innerHTML = '<i class="fas fa-spinner fa-spin mr-1"></i> Cargando...'; return;
try { }
const resp = await fetch('/queries/esquema', { method: 'POST' }); esquema = data.tablas;
const data = await resp.json();
if (data.error) { showToast(data.error, 'error'); return; } // feed schema into CodeMirror autocomplete
esquema = data.tablas; const tablesHint = {};
renderizarTablas(); esquema.forEach(t => { tablesHint[t.tabla] = t.columnas.map(c => c.nombre); });
} catch(e) { showToast('Error: ' + e.message, 'error'); } editor.setOption('hintOptions', { tables: tablesHint });
finally { btn.disabled = false; btn.innerHTML = '<i class="fas fa-sync mr-1"></i> Cargar esquema'; }
} document.getElementById('schema-search-wrap').classList.remove('hidden');
renderSchemaTree(esquema);
function renderizarTablas() { btn.innerHTML = '<i class="fas fa-sync mr-1"></i>Refrescar';
const div = document.getElementById('builder-tablas'); showToast(`${esquema.length} tablas cargadas`);
div.innerHTML = ''; } catch(e) {
esquema.forEach(t => { tree.innerHTML = `<div class="p-2 text-red-500 text-xs">Error: ${e.message}</div>`;
const id = 'tab-' + t.tabla.replace(/[^a-zA-Z0-9]/g, '_'); showToast('Error: ' + e.message, 'error');
const checked = tablasSeleccionadas[t.tabla] ? 'checked' : ''; } finally {
const card = document.createElement('div'); btn.disabled = false;
card.className = 'border rounded p-1.5 ' + (checked ? 'border-blue-300 bg-blue-50' : 'border-gray-200 bg-white'); }
card.innerHTML = ` }
<label class="flex items-center space-x-1 cursor-pointer">
<input type="checkbox" ${checked} onchange="toggleTabla('${t.tabla}', this.checked)" class="rounded"> function filtrarTablas(q) {
<span class="font-medium text-xs">${t.tabla}</span> const filtradas = q.trim()
<span class="text-gray-400 text-xs">(${t.columnas.length})</span> ? esquema.filter(t => t.tabla.toLowerCase().includes(q.toLowerCase()))
</label> : esquema;
<div id="${id}" class="ml-4 ${checked ? '' : 'hidden'}">${renderizarColumnas(t.tabla, t.columnas)}</div>`; renderSchemaTree(filtradas);
div.appendChild(card); }
});
actualizarBuilder(); function renderSchemaTree(tablas) {
} const tree = document.getElementById('schema-tree');
if (!tablas.length) {
function renderizarColumnas(tabla, columnas) { tree.innerHTML = '<p class="text-gray-400 italic text-center mt-4 text-xs">Sin resultados</p>';
let html = ''; return;
columnas.forEach(c => { }
const sel = tablasSeleccionadas[tabla]?.columnas?.[c.nombre] ? 'checked' : ''; tree.innerHTML = '';
html += `<label class="flex items-center space-x-1 py-0.5 cursor-pointer hover:bg-blue-100 px-1 rounded text-xs"> tablas.forEach(t => {
<input type="checkbox" ${sel} onchange="toggleColumna('${tabla}','${c.nombre}',this.checked)" class="rounded"> const key = t.tabla.replace(/\W/g, '_');
<span>${c.nombre} <span class="text-gray-400">(${c.tipo}${c.longitud ? ','+c.longitud : ''})</span></span> const wrap = document.createElement('div');
</label>`; wrap.className = 'mb-0.5';
}); wrap.innerHTML = `
return html; <div class="schema-table-row flex items-center gap-1 px-2 py-1 rounded cursor-pointer group select-none"
} onclick="toggleCols('${key}')">
<i id="chev-${key}" class="fas fa-chevron-right text-gray-300 text-xs transition-transform duration-150" style="width:10px"></i>
function toggleTabla(tabla, checked) { <i class="fas fa-table text-blue-400 text-xs"></i>
if (checked) tablasSeleccionadas[tabla] = { alias: 't' + (++aliasCounter), columnas: {} }; <span class="font-semibold text-gray-700 flex-1 text-xs">${t.tabla}</span>
else delete tablasSeleccionadas[tabla]; <span class="text-gray-300 text-xs">${t.columnas.length}</span>
renderizarTablas(); actualizarBuilder(); <button onclick="event.stopPropagation();selectAll('${t.tabla}',${JSON.stringify(t.columnas.map(c=>c.nombre))})"
} title="SELECT *" class="hidden group-hover:inline text-blue-400 hover:text-blue-600 text-xs ml-0.5">
<i class="fas fa-code"></i>
function toggleColumna(tabla, columna, checked) { </button>
if (!tablasSeleccionadas[tabla]) return; </div>
if (checked) tablasSeleccionadas[tabla].columnas[columna] = true; <div id="cols-${key}" class="hidden ml-4 border-l border-gray-200 pl-2">
else delete tablasSeleccionadas[tabla].columnas[columna]; ${t.columnas.map(c => `
renderizarTablas(); actualizarBuilder(); <div class="schema-col flex items-center gap-1 px-1 py-0.5 rounded cursor-pointer text-xs"
} onclick="insertarTexto('${c.nombre}')" title="Insertar ${c.nombre}">
<i class="fas fa-columns text-gray-300" style="font-size:9px;width:10px"></i>
function agregarFiltro() { <span class="text-gray-700 flex-1">${c.nombre}</span>
filtros.push({ campo: '', operador: '=', parametro: '', condicion: 'AND' }); <span class="text-gray-400" style="font-size:10px">${c.tipo}</span>
renderizarFiltros(); actualizarBuilder(); </div>`).join('')}
} </div>`;
tree.appendChild(wrap);
function renderizarFiltros() { });
const div = document.getElementById('builder-where'); }
div.innerHTML = '';
if (!filtros.length) { div.innerHTML = '<p class="text-gray-400 italic">Sin filtros</p>'; return; } function toggleCols(key) {
filtros.forEach((f, i) => { const div = document.getElementById('cols-' + key);
const opts = []; const chev = document.getElementById('chev-' + key);
Object.entries(tablasSeleccionadas).forEach(([t, info]) => { const open = !div.classList.contains('hidden');
const tInfo = esquema.find(e => e.tabla === t); div.classList.toggle('hidden', open);
if (tInfo) tInfo.columnas.forEach(c => { chev.style.transform = open ? '' : 'rotate(90deg)';
opts.push({ value: `${t}.${c.nombre}`, label: `${info.alias}.${c.nombre}` }); }
});
}); function insertarTexto(texto) {
const row = document.createElement('div'); if (!editor) return;
row.className = 'flex items-center space-x-1 text-xs'; editor.replaceSelection(texto);
row.innerHTML = ` editor.focus();
<select onchange="actualizarFiltro(${i},'campo',this.value)" class="border rounded px-1 py-0.5 text-xs max-w-[100px]"> }
<option value="">--campo--</option>
${opts.map(o => `<option value="${o.value}" ${f.campo===o.value?'selected':''}>${o.label}</option>`).join('')} function selectAll(tabla, columnas) {
</select> const cols = columnas.map(c => ` ${c}`).join(',\n');
<select onchange="actualizarFiltro(${i},'operador',this.value)" class="border rounded px-1 py-0.5 text-xs w-14"> const sql = `SELECT\n${cols}\nFROM ${tabla}\nROWS 50`;
${['=','>','<','>=','<=','<>','LIKE','BETWEEN'].map(o => `<option ${f.operador===o?'selected':''}>${o}</option>`).join('')} editor.setValue(sql);
</select> editor.focus();
<input type="text" value="${f.parametro}" onchange="actualizarFiltro(${i},'parametro',this.value)" placeholder=":param" class="border rounded px-1 py-0.5 text-xs font-mono w-20"> }
<button onclick="filtros.splice(${i},1);renderizarFiltros();actualizarBuilder();" class="text-red-500 hover:text-red-700"><i class="fas fa-times"></i></button>`;
div.appendChild(row); // ── EJECUTAR ─────────────────────────────────────────────────────────
if (i < filtros.length - 1) { async function ejecutar() {
const sep = document.createElement('div'); const sql = editor.getValue().trim();
sep.className = 'ml-1'; if (!sql) { showToast('Escribí una consulta SQL', 'warning'); return; }
sep.innerHTML = `<select onchange="actualizarFiltro(${i+1},'condicion',this.value)" class="border rounded px-1 py-0.5 text-xs w-14">
<option value="AND" ${filtros[i+1]?.condicion==='AND'?'selected':''}>AND</option> const resultDiv = document.getElementById('result-table');
<option value="OR" ${filtros[i+1]?.condicion==='OR'?'selected':''}>OR</option> const infoDiv = document.getElementById('result-info');
</select>`; const statusBar = document.getElementById('status-bar');
div.appendChild(sep); const btnRun = document.getElementById('btn-run');
}
}); btnRun.disabled = true;
} btnRun.innerHTML = '<i class="fas fa-spinner fa-spin text-xs mr-2"></i>Ejecutando...';
resultDiv.innerHTML = '<div class="flex items-center justify-center h-full text-gray-400 text-sm gap-2"><i class="fas fa-spinner fa-spin"></i> Ejecutando...</div>';
function actualizarFiltro(i, prop, val) { infoDiv.textContent = '';
if (!filtros[i]) filtros[i] = { campo: '', operador: '=', parametro: '', condicion: 'AND' }; statusBar.innerHTML = '';
filtros[i][prop] = val;
actualizarBuilder(); const t0 = performance.now();
} try {
const form = new FormData();
function generarSql() { form.append('sql', sql);
const tabs = Object.entries(tablasSeleccionadas); const resp = await fetch('/queries/run-sql', { method: 'POST', body: form });
if (!tabs.length) return { sql: '-- Seleccioná al menos una tabla', params: [] }; const data = await resp.json();
const selects = [], froms = [], allParams = new Set(); const ms = performance.now() - t0;
tabs.forEach(([tabla, info]) => { const elapsed = ms < 1000 ? `${Math.round(ms)}ms` : `${(ms/1000).toFixed(2)}s`;
froms.push({ tabla, alias: info.alias });
Object.keys(info.columnas).forEach(c => selects.push(`${info.alias}.${c}`)); if (data.error) {
}); resultDiv.innerHTML = `<div class="p-4 text-red-600 text-sm flex items-start gap-2"><i class="fas fa-exclamation-circle mt-0.5"></i><pre class="whitespace-pre-wrap font-mono text-xs">${escHtml(data.error)}</pre></div>`;
if (!selects.length) return { sql: '-- Seleccioná al menos una columna', params: [] }; statusBar.innerHTML = `<span class="text-red-500"><i class="fas fa-times-circle mr-1"></i>Error · ${elapsed}</span>`;
let sql = 'SELECT\n ' + selects.join(',\n ') + '\nFROM '; return;
sql += froms.map((f, i) => i === 0 ? `${f.tabla} ${f.alias}` : `JOIN ${f.tabla} ${f.alias} ON 1=1`).join('\n'); }
const wheres = [];
filtros.forEach(f => { if (!data.rows || !data.rows.length) {
if (f.campo && f.parametro) { resultDiv.innerHTML = '<div class="flex items-center justify-center h-full text-gray-400 text-sm">Sin resultados</div>';
const [t, c] = f.campo.split('.'); infoDiv.textContent = '0 filas';
const info = tablasSeleccionadas[t]; statusBar.innerHTML = `<span class="text-gray-500">0 filas · ${elapsed}</span>`;
if (info) { wheres.push(`${info.alias}.${c} ${f.operador} ${f.parametro}`); if (f.parametro.startsWith(':')) allParams.add(f.parametro); } return;
} }
});
if (wheres.length) sql += '\nWHERE ' + wheres.join(' '); let html = '<table><thead><tr>';
sql += '\nROWS 200'; data.columns.forEach(c => {
return { sql, params: Array.from(allParams) }; html += `<th class="text-gray-700 font-semibold">${escHtml(c)}</th>`;
} });
html += '</tr></thead><tbody>';
function actualizarBuilder() { data.rows.forEach(r => {
const { sql, params } = generarSql(); html += '<tr>';
document.getElementById('builder-sql').value = sql; data.columns.forEach(c => {
document.getElementById('builder-params').textContent = params.length ? 'Params: ' + params.join(', ') : ''; const v = r[c] ?? '';
const vs = String(v);
const tabs = Object.entries(tablasSeleccionadas); html += `<td title="${escAttr(vs)}">${escHtml(vs)}</td>`;
const div = document.getElementById('builder-columnas'); });
if (!tabs.length) { div.innerHTML = '<p class="text-gray-400 italic">Seleccioná una tabla</p>'; return; } html += '</tr>';
let html = ''; });
tabs.forEach(([tabla, info]) => { html += '</tbody></table>';
const cols = Object.keys(info.columnas); resultDiv.innerHTML = html;
if (cols.length) html += `<div class="mb-0.5 text-xs"><span class="font-medium">${info.alias}.</span> ${cols.map(c => `<code class="bg-blue-100 px-1 rounded">${c}</code>`).join(' ')}</div>`;
}); const shown = Math.min(data.total, 200);
div.innerHTML = html || '<p class="text-gray-400 italic">Seleccioná columnas</p>'; const trunc = data.total > 200 ? ` <span class="text-yellow-600 font-medium">(truncado a 200)</span>` : '';
} infoDiv.innerHTML = `${data.total} fila${data.total !== 1 ? 's' : ''}${trunc}`;
statusBar.innerHTML = `<span class="text-green-600 font-medium"><i class="fas fa-check-circle mr-1"></i>${shown} fila${shown !== 1 ? 's' : ''} · ${elapsed}</span>`;
function ejecutarBuilder() {
const sql = document.getElementById('builder-sql').value; } catch(e) {
if (!sql || sql.startsWith('--')) { showToast('No hay SQL generado', 'warning'); return; } resultDiv.innerHTML = `<div class="p-4 text-red-600 text-sm">Error de red: ${escHtml(e.message)}</div>`;
ejecutarSql(sql, 'builder-resultados', null); } finally {
} btnRun.disabled = false;
btnRun.innerHTML = '<i class="fas fa-play text-xs"></i> Ejecutar';
function usarSqlEditor() { }
const sql = document.getElementById('builder-sql').value; }
if (!sql || sql.startsWith('--')) { showToast('No hay SQL generado', 'warning'); return; }
document.getElementById('q_text').value = sql; function limpiar() {
document.getElementById('probador-sql').value = sql; if (editor) editor.setValue('');
cambiarTab('probador'); document.getElementById('result-table').innerHTML = '<div class="flex flex-col items-center justify-center h-full text-gray-300 select-none"><i class="fas fa-table text-4xl mb-3"></i><p class="text-sm">Ejecutá una consulta para ver resultados</p></div>';
showToast('SQL enviado al Probador'); document.getElementById('result-info').textContent = '';
} document.getElementById('status-bar').innerHTML = '';
}
// ───── CONSULTAS GUARDADAS ─────
function cargarConsulta(id, name, type, text) { // ── GUARDAR / EDITAR ──────────────────────────────────────────────────
document.getElementById('query_id').value = id; function abrirModalNueva() {
document.getElementById('q_name').value = name; editandoId = null;
document.getElementById('q_type').value = type; document.getElementById('form-save').action = '/queries/create';
document.getElementById('q_text').value = text; document.getElementById('modal-title').textContent = 'Nueva consulta';
document.getElementById('q_desc').value = ''; document.getElementById('form-name').value = document.getElementById('q-name').value;
document.getElementById('probador-sql').value = text; document.getElementById('form-type').value = document.getElementById('q-type').value;
document.getElementById('btn-cancel').classList.remove('hidden'); document.getElementById('form-sql').value = editor ? editor.getValue() : '';
document.querySelector('#panel-editor form').action = '/queries/update/' + id; document.getElementById('form-desc').value = '';
cambiarTab('probador'); document.getElementById('modal-save').classList.remove('hidden');
} setTimeout(() => document.getElementById('form-name').focus(), 50);
}
function cancelEdit() {
document.getElementById('query_id').value = ''; function guardar() {
document.getElementById('q_name').value = ''; const sql = editor ? editor.getValue().trim() : '';
document.getElementById('q_text').value = ''; if (!sql) { showToast('No hay SQL para guardar', 'warning'); return; }
document.getElementById('q_desc').value = ''; document.getElementById('form-sql').value = sql;
document.getElementById('btn-cancel').classList.add('hidden'); document.getElementById('form-name').value = document.getElementById('q-name').value;
document.querySelector('#panel-editor form').action = '/queries/create'; document.getElementById('form-type').value = document.getElementById('q-type').value;
} if (editandoId) {
document.getElementById('form-save').action = '/queries/update/' + editandoId;
async function ejecutarConsultaId(id) { document.getElementById('modal-title').textContent = 'Actualizar consulta';
const form = new FormData(); form.append('sql', ''); } else {
const resp = await fetch('/queries/test', { method: 'POST', body: (()=>{const f=new FormData();f.append('query_id',id);return f;})() }); document.getElementById('form-save').action = '/queries/create';
const data = await resp.json(); document.getElementById('modal-title').textContent = 'Guardar consulta';
if (data.error) { showToast(data.error, 'error'); return; } }
let html = '<table class="w-full text-xs border-collapse"><thead><tr class="bg-gray-100">'; document.getElementById('modal-save').classList.remove('hidden');
data.columns.forEach(c => { html += '<th class="p-2 border text-left font-semibold">' + c + '</th>'; }); setTimeout(() => document.getElementById('form-name').focus(), 50);
html += '</tr></thead><tbody>'; }
data.rows.slice(0,200).forEach(r => {
html += '<tr class="hover:bg-gray-50">'; function cerrarModal() {
data.columns.forEach(c => { html += '<td class="p-2 border whitespace-nowrap">' + (r[c] ?? '') + '</td>'; }); document.getElementById('modal-save').classList.add('hidden');
html += '</tr>'; }
});
html += '</tbody></table>'; function cargarConsulta(sql, name, type) {
const modal = document.createElement('div'); if (editor) editor.setValue(sql);
modal.className = 'fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50'; document.getElementById('q-name').value = name;
modal.innerHTML = '<div class="bg-white rounded-xl shadow-xl max-w-5xl w-full mx-4 max-h-[80vh] overflow-auto p-4"><div class="flex justify-between items-center mb-3"><h3 class="font-semibold text-sm">Resultado</h3><button onclick="this.closest(\'.fixed\').remove()" class="text-gray-400 hover:text-gray-600"><i class="fas fa-times text-lg"></i></button></div>' + html + '<p class="text-xs text-gray-500 mt-2">Total: ' + data.total + ' registros (mostrando ' + Math.min(data.total, 200) + ')</p></div>'; document.getElementById('q-type').value = type;
document.body.appendChild(modal); editor && editor.focus();
} }
</script>
{% endblock %} function editarConsulta(id, name, type, sql, desc) {
editandoId = id;
cargarConsulta(sql, name, type);
document.getElementById('form-desc').value = desc;
document.getElementById('btn-cancel').classList.replace('hidden', 'flex');
document.getElementById('editing-label').textContent = `Editando: ${name}`;
document.getElementById('editing-label').classList.remove('hidden');
}
function cancelarEdicion() {
editandoId = null;
limpiar();
document.getElementById('q-name').value = '';
document.getElementById('btn-cancel').classList.replace('flex', 'hidden');
document.getElementById('editing-label').classList.add('hidden');
}
// ── UTILS ─────────────────────────────────────────────────────────────
function escHtml(s) {
return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
function escAttr(s) {
return String(s).replace(/"/g,'&quot;').replace(/'/g,'&#39;');
}
// close modal on backdrop click
document.getElementById('modal-save').addEventListener('click', e => {
if (e.target === e.currentTarget) cerrarModal();
});
</script>
{% endblock %}
+89 -103
View File
@@ -1,103 +1,89 @@
import os import os
import sys import sys
from pathlib import Path from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent)) sys.path.insert(0, str(Path(__file__).parent))
from fastapi import FastAPI, Request, Depends from fastapi import FastAPI, Request
from fastapi.responses import RedirectResponse from fastapi.responses import RedirectResponse
from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates
from fastapi.templating import Jinja2Templates import uvicorn
from fastapi.middleware.cors import CORSMiddleware
import uvicorn from app.database import init_db
from app.auth import decode_token
from app.database import init_db
from app.auth import decode_token app = FastAPI(title="RIPS Manager", version="1.0.0")
app = FastAPI(title="RIPS Manager", version="1.0.0") templates = Jinja2Templates(
directory=os.path.join(os.path.dirname(__file__), "app", "templates")
app.add_middleware( )
CORSMiddleware, app.state.templates = templates
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"], def get_user_from_request(request: Request):
allow_headers=["*"], token = request.cookies.get("token")
) if not token:
auth = request.headers.get("Authorization", "")
templates = Jinja2Templates( if auth.startswith("Bearer "):
directory=os.path.join(os.path.dirname(__file__), "app", "templates") token = auth[7:]
) if token:
app.state.templates = templates decoded = decode_token(token)
if decoded:
return decoded
def get_user_from_request(request: Request): print(f" [AUTH] Invalid token: {token[:30]}...", flush=True)
cookies = dict(request.cookies) return None
token = cookies.get("token")
if not token:
auth = request.headers.get("Authorization", "") @app.middleware("http")
if auth.startswith("Bearer "): async def auth_middleware(request: Request, call_next):
token = auth[7:] if request.url.path.startswith("/auth") or request.url.path.startswith("/static"):
if token: return await call_next(request)
decoded = decode_token(token)
if decoded: user = get_user_from_request(request)
return decoded print(f" [AUTH] path={request.url.path} user={user['username'] if user else None}", flush=True)
print(f" [AUTH] Invalid token: {token[:30]}...", flush=True) if not user:
return None if request.url.path.startswith("/api/"):
from fastapi.responses import JSONResponse
return JSONResponse({"detail": "Not authenticated"}, status_code=401)
@app.middleware("http") return RedirectResponse(url="/auth/login")
async def auth_middleware(request: Request, call_next):
public_paths = ["/auth/login", "/auth/register", "/auth/api/login"] request.state.user = user
if request.url.path in public_paths or request.url.path.startswith("/static"): return await call_next(request)
return await call_next(request)
if request.url.path.startswith("/auth"): @app.on_event("startup")
return await call_next(request) async def startup():
init_db()
user = get_user_from_request(request) from app.routes.config import ensure_defaults as config_defaults
print(f" [AUTH] path={request.url.path} user={user['username'] if user else None} cookies={dict(request.cookies)}", flush=True) from app.routes.queries import ensure_defaults as query_defaults
if not user: config_defaults()
if request.url.path.startswith("/api/"): query_defaults()
from fastapi.responses import JSONResponse
print(f" [AUTH] -> 401 JSON for /api/ path", flush=True)
return JSONResponse({"detail": "Not authenticated"}, status_code=401) @app.get("/")
print(f" [AUTH] -> 307 redirect to /auth/login", flush=True) async def root():
return RedirectResponse(url="/auth/login") return RedirectResponse(url="/dashboard")
request.state.user = user
return await call_next(request) from app.routes import auth, dashboard, config, queries, terceros, transaccion, logs, automation
app.include_router(auth.router)
@app.on_event("startup") app.include_router(dashboard.router)
async def startup(): app.include_router(config.router)
init_db() app.include_router(queries.router)
app.include_router(terceros.router)
app.include_router(transaccion.router)
@app.get("/") app.include_router(logs.router)
async def root(): app.include_router(automation.router)
return RedirectResponse(url="/dashboard")
if __name__ == "__main__":
# Register routes import socket
from app.routes import auth, dashboard, config, queries, terceros, transaccion, logs, automation hostname = socket.gethostname()
try:
app.include_router(auth.router) lan_ip = socket.gethostbyname(hostname)
app.include_router(dashboard.router) except Exception:
app.include_router(config.router) lan_ip = "0.0.0.0"
app.include_router(queries.router) print(f" Local: http://localhost:8080")
app.include_router(terceros.router) print(f" Red: http://{lan_ip}:8080")
app.include_router(transaccion.router) uvicorn.run("main:app", host="0.0.0.0", port=8080, reload=False)
app.include_router(logs.router)
app.include_router(automation.router)
if __name__ == "__main__":
import socket
hostname = socket.gethostname()
try:
lan_ip = socket.gethostbyname(hostname)
except:
lan_ip = "0.0.0.0"
print(f" Local: http://localhost:8080")
print(f" Red: http://{lan_ip}:8080")
uvicorn.run("main:app", host="0.0.0.0", port=8080, reload=False)