Initial commit: RIPS Manager web system
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
venv/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.db-shm
|
||||
*.db-wal
|
||||
.env
|
||||
python/
|
||||
*.zip
|
||||
@@ -0,0 +1,347 @@
|
||||
# RIPS Manager — Documentación Técnica
|
||||
|
||||
Sistema web para generar y enviar JSON de RIPS (Res. 2275/2023) a una API, conectándose a una base de datos Firebird.
|
||||
|
||||
---
|
||||
|
||||
## Arquitectura
|
||||
|
||||
```
|
||||
┌─────────────┐ ┌──────────────────┐ ┌──────────────┐
|
||||
│ Navegador │────▶│ FastAPI (8080) │────▶│ Firebird BD │
|
||||
│ (Tailwind) │◀────│ + Jinja2 │◀────│ (datos) │
|
||||
└─────────────┘ └──────────────────┘ └──────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────┐
|
||||
│ SQLite │
|
||||
│ (app.db) │
|
||||
└──────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────┐
|
||||
│ API externa │
|
||||
│ (envío RIPS)│
|
||||
└──────────────┘
|
||||
```
|
||||
|
||||
## Stack técnico
|
||||
|
||||
| Componente | Tecnología |
|
||||
|---|---|
|
||||
| Backend | Python 3.9+ / FastAPI |
|
||||
| Frontend | Jinja2 + Tailwind CSS (CDN) + FontAwesome |
|
||||
| Base de datos app | SQLite (usuarios, config, logs, queries) |
|
||||
| Base de datos datos | Firebird (vía `fdb`) |
|
||||
| Autenticación | JWT + bcrypt + HttpOnly cookies |
|
||||
| Cliente HTTP | httpx (async) |
|
||||
| Servidor | uvicorn |
|
||||
|
||||
---
|
||||
|
||||
## Endpoints (20 totales)
|
||||
|
||||
### Autenticación (6 endpoints)
|
||||
|
||||
| Método | Ruta | Descripción | Autenticación |
|
||||
|---|---|---|---|
|
||||
| GET | `/auth/login` | Página de inicio de sesión | Pública |
|
||||
| POST | `/auth/login` | Procesa login, devuelve cookie JWT | Pública |
|
||||
| GET | `/auth/register` | Página de registro | Pública |
|
||||
| POST | `/auth/register` | Registra nuevo usuario | Pública |
|
||||
| GET | `/auth/logout` | Cierra sesión (elimina cookie) | Pública |
|
||||
| POST | `/auth/api/login` | Login vía API (devuelve token JSON) | Pública |
|
||||
|
||||
### Dashboard (1 endpoint)
|
||||
|
||||
| Método | Ruta | Descripción | Autenticación |
|
||||
|---|---|---|---|
|
||||
| GET | `/dashboard` | Estadísticas de envíos, últimos registros | Requerida |
|
||||
|
||||
### Configuración (2 endpoints)
|
||||
|
||||
| Método | Ruta | Descripción | Autenticación |
|
||||
|---|---|---|---|
|
||||
| GET | `/config` | Página de configuración (Firebird + API + prestador) | Requerida |
|
||||
| POST | `/config/save` | Guarda toda la configuración | Requerida |
|
||||
|
||||
### Consultas SQL (3 endpoints)
|
||||
|
||||
| Método | Ruta | Descripción | Autenticación |
|
||||
|---|---|---|---|
|
||||
| GET | `/queries` | Página de gestión de consultas SQL | Requerida |
|
||||
| POST | `/queries/create` | Crea nueva consulta SQL | Requerida |
|
||||
| POST | `/queries/delete/{id}` | Elimina consulta SQL | Requerida |
|
||||
| POST | `/queries/update/{id}` | Actualiza consulta SQL | Requerida |
|
||||
|
||||
### Terceros (3 endpoints)
|
||||
|
||||
| Método | Ruta | Descripción | Autenticación |
|
||||
|---|---|---|---|
|
||||
| GET | `/terceros` | Página de envío de terceros | Requerida |
|
||||
| POST | `/terceros/test-connection` | Prueba conexión Firebird | Requerida |
|
||||
| POST | `/terceros/preview` | Vista previa del JSON de terceros | Requerida |
|
||||
| POST | `/terceros/send` | Genera y envía JSON de terceros a la API | Requerida |
|
||||
|
||||
### Transacción RIPS (3 endpoints)
|
||||
|
||||
| Método | Ruta | Descripción | Autenticación |
|
||||
|---|---|---|---|
|
||||
| GET | `/transaccion` | Página de envío de transacción RIPS | Requerida |
|
||||
| POST | `/transaccion/preview` | Vista previa del JSON de transacción | Requerida |
|
||||
| POST | `/transaccion/send` | Genera y envía JSON de transacción a la API | Requerida |
|
||||
|
||||
### Automatización (2 endpoints)
|
||||
|
||||
| Método | Ruta | Descripción | Autenticación |
|
||||
|---|---|---|---|
|
||||
| GET | `/automation` | Página de automatización 2 pasos | Requerida |
|
||||
| POST | `/automation/run` | Ejecuta Paso 1 (terceros) + Paso 2 (transacción) | Requerida |
|
||||
|
||||
### Logs (1 endpoint)
|
||||
|
||||
| Método | Ruta | Descripción | Autenticación |
|
||||
|---|---|---|---|
|
||||
| GET | `/logs` | Historial de envíos con filtros | Requerida |
|
||||
|
||||
---
|
||||
|
||||
## Flujo de autenticación
|
||||
|
||||
```
|
||||
Navegador Servidor
|
||||
│ │
|
||||
│── GET /auth/login ───────▶│
|
||||
│◀──── HTML login page ─────│
|
||||
│ │
|
||||
│── POST /auth/login ───────▶│ (username + password)
|
||||
│ │── verifica credenciales
|
||||
│ │── genera JWT
|
||||
│◀── 302 /dashboard ────────│ (Set-Cookie: token=JWT; HttpOnly)
|
||||
│ │
|
||||
│── GET /dashboard ────────▶│ (Cookie: token=JWT)
|
||||
│ │── middleware verifica JWT
|
||||
│ │── setea request.state.user
|
||||
│◀──── HTML dashboard ──────│
|
||||
```
|
||||
|
||||
- **Cookie HttpOnly**: no accesible desde JavaScript (seguridad XSS)
|
||||
- **JWT expira en 12 horas**
|
||||
- **Bearer token** también soportado para llamadas API (`Authorization: Bearer <token>`)
|
||||
|
||||
---
|
||||
|
||||
## Flujo de envío de RIPS
|
||||
|
||||
### Manual (página Terceros + Transacción)
|
||||
|
||||
```
|
||||
1. Configurar conexión Firebird y API (una vez)
|
||||
2. Definir consultas SQL en "Consultas SQL"
|
||||
3. Ir a "Terceros" → seleccionar consulta → "Vista Previa" → "Enviar a API"
|
||||
4. Ir a "Transacción RIPS" → seleccionar consulta + fechas → "Enviar a API"
|
||||
```
|
||||
|
||||
### Automatizado (página Automatización)
|
||||
|
||||
```
|
||||
1. Seleccionar consulta de terceros (Paso 1)
|
||||
2. Seleccionar consulta de transacción (Paso 2)
|
||||
3. Definir rango de fechas
|
||||
4. "Ejecutar Automatización Completa"
|
||||
└── Paso 1: Envía todos los terceros del período
|
||||
└── Paso 2: Envía todas las transacciones del período
|
||||
```
|
||||
|
||||
### Estructura de los JSON generados
|
||||
|
||||
#### JSON Terceros
|
||||
```json
|
||||
{
|
||||
"tipoDocumentoIdentificacion": "CC",
|
||||
"numDocumentoIdentificacion": "27765610",
|
||||
"primerNombre": "MARIA",
|
||||
"segundoNombre": "ELENA",
|
||||
"primerApellido": "GOMEZ",
|
||||
"segundoApellido": "RUIZ",
|
||||
"fechaNacimiento": "1954-01-19",
|
||||
"codSexo": "F",
|
||||
"codEntidadAdministradora": "EPS010",
|
||||
"tipoUsuario": "12",
|
||||
"codPaisResidencia": "170",
|
||||
"codMunicipioResidencia": "54001",
|
||||
"codZonaTerritorialResidencia": "02",
|
||||
"incapacidad": "NO",
|
||||
"codPaisOrigen": "170",
|
||||
"direccionResidencia": "CRA 5 #10-20",
|
||||
"codZonaResidencia": "02"
|
||||
}
|
||||
```
|
||||
|
||||
#### JSON Transacción RIPS
|
||||
```json
|
||||
{
|
||||
"numDocumentoIdObligado": "900278729",
|
||||
"numFactura": "LHXC03404",
|
||||
"tipoNota": null,
|
||||
"numNota": null,
|
||||
"usuarios": [{
|
||||
"tipoDocumentoIdentificacion": "CC",
|
||||
"numDocumentoIdentificacion": "27765610",
|
||||
"codEntidadAdministradora": "EPS010",
|
||||
"tipoUsuario": "12",
|
||||
"fechaNacimiento": "1954-01-19",
|
||||
"codSexo": "F",
|
||||
"codPaisResidencia": "170",
|
||||
"codMunicipioResidencia": "54001",
|
||||
"codZonaTerritorialResidencia": "02",
|
||||
"incapacidad": "NO",
|
||||
"consecutivo": 1,
|
||||
"codPaisOrigen": "170",
|
||||
"servicios": {
|
||||
"procedimientos": [{
|
||||
"consecutivo": 1,
|
||||
"codProcedimiento": "903841",
|
||||
"fechaInicioAtencion": "2026-06-11 00:00",
|
||||
"codDiagnosticoPrincipal": "R790",
|
||||
"codDiagnosticoRelacionado": null,
|
||||
"finalidadTecnologiaSalud": "23",
|
||||
"viaIngresoServicioSalud": "02",
|
||||
"modalidadGrupoServicioTecSal": "01",
|
||||
"grupoServicios": "02",
|
||||
"codServicio": 706,
|
||||
"codPrestador": "540010152002",
|
||||
"tipoDocumentoIdentificacion": "CC",
|
||||
"numDocumentoIdentificacion": "27898369",
|
||||
"vrServicio": 8000,
|
||||
"valorPagoModerador": 0,
|
||||
"conceptoRecaudo": "05",
|
||||
"numAutorizacion": null,
|
||||
"idMIPRES": null,
|
||||
"codComplicacion": null,
|
||||
"numFEVPagoModerador": null
|
||||
}]
|
||||
}
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Base de datos SQLite (app)
|
||||
|
||||
### Tabla `users`
|
||||
| Columna | Tipo | Descripción |
|
||||
|---|---|---|
|
||||
| id | INTEGER PK | Auto-incrementable |
|
||||
| username | TEXT UNIQUE | Nombre de usuario |
|
||||
| email | TEXT UNIQUE | Correo electrónico |
|
||||
| password_hash | TEXT | Hash bcrypt de la contraseña |
|
||||
| created_at | TEXT | Fecha de creación |
|
||||
|
||||
### Tabla `config`
|
||||
| Columna | Tipo | Descripción |
|
||||
|---|---|---|
|
||||
| id | INTEGER PK | Auto-incrementable |
|
||||
| key | TEXT UNIQUE | Clave de configuración |
|
||||
| value | TEXT | Valor |
|
||||
|
||||
Claves predefinidas:
|
||||
- `firebird_host`, `firebird_port`, `firebird_database`, `firebird_user`, `firebird_password`
|
||||
- `api_url`, `api_method`, `api_key`, `api_timeout`
|
||||
- `num_documento_obligado`, `cod_prestador`
|
||||
|
||||
### Tabla `queries`
|
||||
| Columna | Tipo | Descripción |
|
||||
|---|---|---|
|
||||
| id | INTEGER PK | Auto-incrementable |
|
||||
| name | TEXT | Nombre descriptivo |
|
||||
| query_type | TEXT | `terceros` o `transaccion` |
|
||||
| query_text | TEXT | Sentencia SQL con parámetros |
|
||||
| description | TEXT | Descripción |
|
||||
| created_at | TEXT | Fecha de creación |
|
||||
|
||||
### Tabla `envios`
|
||||
| Columna | Tipo | Descripción |
|
||||
|---|---|---|
|
||||
| id | INTEGER PK | Auto-incrementable |
|
||||
| user_id | INTEGER FK | Usuario que realizó el envío |
|
||||
| tipo | TEXT | `terceros` o `transaccion` |
|
||||
| factura | TEXT | Número de factura |
|
||||
| fecha_inicio / fecha_fin | TEXT | Rango de fechas del envío |
|
||||
| pacientes_count / servicios_count | INTEGER | Cantidades |
|
||||
| status | TEXT | `success` o `error` |
|
||||
| json_enviado | TEXT | JSON completo enviado |
|
||||
| respuesta_api | TEXT | Respuesta de la API |
|
||||
| codigo_cuv | TEXT | Código CUV (si aplica) |
|
||||
| created_at | TEXT | Fecha del envío |
|
||||
|
||||
---
|
||||
|
||||
## Parámetros en consultas SQL
|
||||
|
||||
Las consultas pueden usar estos parámetros que el sistema reemplaza automáticamente:
|
||||
|
||||
| Parámetro | Ejemplo | Uso |
|
||||
|---|---|---|
|
||||
| `:doc_num` | `WHERE doc = :doc_num` | Filtrar por documento de paciente |
|
||||
| `:factura` | `WHERE fact = :factura` | Filtrar por número de factura |
|
||||
| `:fecha_ini` | `WHERE fecha >= :fecha_ini` | Fecha inicio del rango |
|
||||
| `:fecha_fin` | `WHERE fecha <= :fecha_fin` | Fecha fin del rango |
|
||||
|
||||
---
|
||||
|
||||
## Estructura del proyecto
|
||||
|
||||
```
|
||||
rips_manager/
|
||||
├── main.py # Entry point, middleware, rutas
|
||||
├── requirements.txt # Dependencias Python
|
||||
├── rips_manager.db # SQLite (autogenerado)
|
||||
├── DOCUMENTACION.md # Este archivo
|
||||
├── README.md # Documentación de RIPS
|
||||
├── ejemplo_terceros.json # JSON de ejemplo terceros
|
||||
├── ejemplo_transaccion_rips.json # JSON de ejemplo transacción
|
||||
├── app/
|
||||
│ ├── __init__.py
|
||||
│ ├── auth.py # JWT, bcrypt, get_current_user
|
||||
│ ├── database.py # SQLite init y conexión
|
||||
│ ├── models.py # Pydantic models
|
||||
│ ├── routes/
|
||||
│ │ ├── auth.py # Login, register, logout
|
||||
│ │ ├── dashboard.py # Página principal con stats
|
||||
│ │ ├── config.py # Configuración Firebird + API
|
||||
│ │ ├── queries.py # CRUD de consultas SQL
|
||||
│ │ ├── terceros.py # Generar y enviar JSON terceros
|
||||
│ │ ├── transaccion.py # Generar y enviar JSON transacción
|
||||
│ │ ├── automation.py # Paso 1 + Paso 2 automático
|
||||
│ │ └── logs.py # Historial de envíos
|
||||
│ ├── services/
|
||||
│ │ ├── firebird_service.py # Conexión y consultas Firebird
|
||||
│ │ ├── json_generator.py # Construcción de JSON RIPS
|
||||
│ │ └── api_client.py # Envío HTTP a API externa
|
||||
│ └── templates/ # 10 plantillas Jinja2 + Tailwind
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Cómo ejecutar
|
||||
|
||||
```bash
|
||||
cd /Users/lizandro/Documents/proyectos/rips_manager
|
||||
pip3 install -r requirements.txt
|
||||
python3 main.py
|
||||
```
|
||||
|
||||
Abrir en el navegador: **http://localhost:8080**
|
||||
|
||||
---
|
||||
|
||||
## Secuencia para primer uso
|
||||
|
||||
1. Abrir http://localhost:8080/auth/register
|
||||
2. Crear usuario y contraseña
|
||||
3. Iniciar sesión
|
||||
4. Ir a **Configuración** → configurar Firebird, API, datos del prestador
|
||||
5. Ir a **Consultas SQL** → ajustar las queries a los nombres reales de tablas
|
||||
6. Usar **Terceros**, **Transacción RIPS** o **Automatización**
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
import bcrypt
|
||||
from datetime import datetime, timedelta
|
||||
from jose import JWTError, jwt
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
|
||||
SECRET_KEY = "rips-manager-secret-key-change-in-production"
|
||||
ALGORITHM = "HS256"
|
||||
ACCESS_TOKEN_EXPIRE_HOURS = 12
|
||||
|
||||
security = HTTPBearer(auto_error=False)
|
||||
|
||||
|
||||
def hash_password(password: str) -> str:
|
||||
return bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode()
|
||||
|
||||
|
||||
def verify_password(password: str, password_hash: str) -> bool:
|
||||
return bcrypt.checkpw(password.encode(), password_hash.encode())
|
||||
|
||||
|
||||
def create_token(user_id: int, username: str) -> str:
|
||||
payload = {
|
||||
"user_id": user_id,
|
||||
"username": username,
|
||||
"exp": datetime.utcnow() + timedelta(hours=ACCESS_TOKEN_EXPIRE_HOURS),
|
||||
}
|
||||
return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)
|
||||
|
||||
|
||||
from typing import Optional
|
||||
|
||||
def decode_token(token: str) -> Optional[dict]:
|
||||
try:
|
||||
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
||||
return payload
|
||||
except JWTError:
|
||||
return None
|
||||
|
||||
|
||||
from fastapi import Request
|
||||
|
||||
|
||||
def get_current_user(request: Request, credentials: HTTPAuthorizationCredentials = Depends(security)):
|
||||
if hasattr(request.state, "user") and request.state.user:
|
||||
return request.state.user
|
||||
if credentials is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Not authenticated",
|
||||
)
|
||||
payload = decode_token(credentials.credentials)
|
||||
if payload is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid token",
|
||||
)
|
||||
return payload
|
||||
|
||||
|
||||
def optional_user(request: Request, credentials: HTTPAuthorizationCredentials = Depends(security)):
|
||||
if hasattr(request.state, "user") and request.state.user:
|
||||
return request.state.user
|
||||
if credentials is None:
|
||||
return None
|
||||
return decode_token(credentials.credentials)
|
||||
@@ -0,0 +1,73 @@
|
||||
import sqlite3
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
DB_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), "rips_manager.db")
|
||||
|
||||
|
||||
def get_connection():
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
conn.row_factory = sqlite3.Row
|
||||
conn.execute("PRAGMA journal_mode=WAL")
|
||||
conn.execute("PRAGMA foreign_keys=ON")
|
||||
return conn
|
||||
|
||||
|
||||
def init_db():
|
||||
conn = get_connection()
|
||||
conn.executescript("""
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
email TEXT UNIQUE NOT NULL,
|
||||
password_hash TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS config (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
key TEXT UNIQUE NOT NULL,
|
||||
value TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS queries (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
query_type TEXT NOT NULL CHECK(query_type IN ('terceros','transaccion')),
|
||||
query_text TEXT NOT NULL,
|
||||
description TEXT,
|
||||
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 (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER,
|
||||
tipo TEXT NOT NULL CHECK(tipo IN ('terceros','transaccion')),
|
||||
factura TEXT,
|
||||
fecha_inicio TEXT,
|
||||
fecha_fin TEXT,
|
||||
pacientes_count INTEGER DEFAULT 0,
|
||||
servicios_count INTEGER DEFAULT 0,
|
||||
status TEXT NOT NULL DEFAULT 'pendiente',
|
||||
json_enviado TEXT,
|
||||
respuesta_api TEXT,
|
||||
codigo_cuv TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
);
|
||||
""")
|
||||
conn.commit()
|
||||
conn.close()
|
||||
@@ -0,0 +1,38 @@
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class UserCreate(BaseModel):
|
||||
username: str
|
||||
email: str
|
||||
password: str
|
||||
|
||||
|
||||
class UserLogin(BaseModel):
|
||||
username: str
|
||||
password: str
|
||||
|
||||
|
||||
class ConfigUpdate(BaseModel):
|
||||
key: str
|
||||
value: str
|
||||
|
||||
|
||||
class QueryCreate(BaseModel):
|
||||
name: str
|
||||
query_type: str
|
||||
query_text: str
|
||||
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
|
||||
@@ -0,0 +1,96 @@
|
||||
from fastapi import APIRouter, Request, Form, Depends, HTTPException
|
||||
from fastapi.responses import RedirectResponse, JSONResponse
|
||||
from app.database import get_connection
|
||||
from app.auth import hash_password, verify_password, create_token, get_current_user
|
||||
|
||||
router = APIRouter(prefix="/auth", tags=["auth"])
|
||||
|
||||
|
||||
@router.get("/login")
|
||||
async def login_page(request: Request):
|
||||
return request.app.state.templates.TemplateResponse("login.html", {"request": request})
|
||||
|
||||
|
||||
@router.get("/register")
|
||||
async def register_page(request: Request):
|
||||
return request.app.state.templates.TemplateResponse("register.html", {"request": request})
|
||||
|
||||
|
||||
@router.post("/register")
|
||||
async def register(
|
||||
request: Request,
|
||||
username: str = Form(...),
|
||||
email: str = Form(...),
|
||||
password: str = Form(...),
|
||||
confirm_password: str = Form(...),
|
||||
):
|
||||
if password != confirm_password:
|
||||
return request.app.state.templates.TemplateResponse("register.html", {
|
||||
"request": request, "error": "Las contraseñas no coinciden"
|
||||
})
|
||||
|
||||
conn = get_connection()
|
||||
try:
|
||||
existing = conn.execute(
|
||||
"SELECT id FROM users WHERE username = ? OR email = ?",
|
||||
(username, email)
|
||||
).fetchone()
|
||||
if existing:
|
||||
return request.app.state.templates.TemplateResponse("register.html", {
|
||||
"request": request, "error": "Usuario o email ya registrado"
|
||||
})
|
||||
|
||||
conn.execute(
|
||||
"INSERT INTO users (username, email, password_hash) VALUES (?, ?, ?)",
|
||||
(username, email, hash_password(password))
|
||||
)
|
||||
conn.commit()
|
||||
return RedirectResponse("/auth/login", status_code=302)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@router.post("/login")
|
||||
async def login(
|
||||
request: Request,
|
||||
username: str = Form(...),
|
||||
password: str = Form(...),
|
||||
):
|
||||
conn = get_connection()
|
||||
try:
|
||||
user = conn.execute(
|
||||
"SELECT * FROM users WHERE username = ?", (username,)
|
||||
).fetchone()
|
||||
if not user or not verify_password(password, user["password_hash"]):
|
||||
return request.app.state.templates.TemplateResponse("login.html", {
|
||||
"request": request, "error": "Usuario o contraseña incorrectos"
|
||||
})
|
||||
|
||||
token = create_token(user["id"], user["username"])
|
||||
resp = RedirectResponse("/dashboard", status_code=302)
|
||||
resp.set_cookie(key="token", value=token, httponly=True, max_age=43200)
|
||||
return resp
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@router.get("/logout")
|
||||
async def logout():
|
||||
resp = RedirectResponse("/auth/login", status_code=302)
|
||||
resp.delete_cookie("token")
|
||||
return resp
|
||||
|
||||
|
||||
@router.post("/api/login")
|
||||
async def api_login(username: str = Form(...), password: str = Form(...)):
|
||||
conn = get_connection()
|
||||
try:
|
||||
user = conn.execute(
|
||||
"SELECT * FROM users WHERE username = ?", (username,)
|
||||
).fetchone()
|
||||
if not user or not verify_password(password, user["password_hash"]):
|
||||
raise HTTPException(status_code=401, detail="Invalid credentials")
|
||||
token = create_token(user["id"], user["username"])
|
||||
return {"access_token": token, "token_type": "bearer"}
|
||||
finally:
|
||||
conn.close()
|
||||
@@ -0,0 +1,204 @@
|
||||
from fastapi import APIRouter, Request, Form, Depends
|
||||
from fastapi.responses import JSONResponse
|
||||
from app.database import get_connection
|
||||
from app.auth import get_current_user
|
||||
from app.services.firebird_service import FirebirdService
|
||||
from app.services.json_generator import generar_terceros, generar_transaccion
|
||||
|
||||
router = APIRouter(prefix="/automation", tags=["automation"])
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def automation_page(request: Request, user: dict = Depends(get_current_user)):
|
||||
conn = get_connection()
|
||||
queries = conn.execute("SELECT * FROM queries ORDER BY query_type, name").fetchall()
|
||||
configs = {row["key"]: row["value"] for row in conn.execute("SELECT * FROM config").fetchall()}
|
||||
conn.close()
|
||||
|
||||
return request.app.state.templates.TemplateResponse("automation.html", {
|
||||
"request": request, "user": user,
|
||||
"queries": queries, "configs": configs,
|
||||
})
|
||||
|
||||
|
||||
@router.post("/run")
|
||||
async def run_automation(
|
||||
request: Request,
|
||||
user: dict = Depends(get_current_user),
|
||||
query_terceros_id: int = Form(...),
|
||||
query_transaccion_id: int = Form(...),
|
||||
fecha_inicio: str = Form(...),
|
||||
fecha_fin: str = Form(...),
|
||||
factura: str = Form(""),
|
||||
):
|
||||
import json as json_lib
|
||||
import httpx
|
||||
from datetime import datetime
|
||||
from collections import defaultdict
|
||||
|
||||
conn = get_connection()
|
||||
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()
|
||||
configs = {row["key"]: row["value"] for row in conn.execute("SELECT * FROM config").fetchall()}
|
||||
conn.close()
|
||||
|
||||
if not q_terceros or not q_trans:
|
||||
return JSONResponse({"success": False, "message": "Consultas no encontradas"})
|
||||
|
||||
fb = FirebirdService()
|
||||
fb_success, fb_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"),
|
||||
)
|
||||
if not fb_success:
|
||||
return JSONResponse({"success": False, "message": f"Error Firebird: {fb_msg}"})
|
||||
|
||||
api_url = configs.get("api_url", "")
|
||||
api_key = configs.get("api_key", "")
|
||||
api_method = configs.get("api_method", "POST")
|
||||
headers = {"Content-Type": "application/json"}
|
||||
if api_key:
|
||||
headers["Authorization"] = f"Bearer {api_key}"
|
||||
|
||||
resultado = {"paso1_terceros": {"status": "pendiente"}, "paso2_transaccion": {"status": "pendiente"}}
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
# PASO 1: Enviar TERCEROS
|
||||
# ---------------------------------------------------------------
|
||||
params = {"fecha_ini": fecha_inicio, "fecha_fin": fecha_fin}
|
||||
if ":factura" in q_terceros["query_text"] and factura:
|
||||
params["factura"] = factura
|
||||
if ":doc_num" in q_terceros["query_text"]:
|
||||
params["doc_num"] = ""
|
||||
|
||||
success, error, rows = fb.execute_query(q_terceros["query_text"], params if ":fecha_ini" in q_terceros["query_text"] else None)
|
||||
|
||||
if not success:
|
||||
resultado["paso1_terceros"] = {"status": "error", "message": error}
|
||||
elif not rows:
|
||||
resultado["paso1_terceros"] = {"status": "error", "message": "No hay pacientes para enviar"}
|
||||
else:
|
||||
terceros_enviados = 0
|
||||
terceros_errores = 0
|
||||
pacientes_enviados = []
|
||||
|
||||
for row in rows:
|
||||
tercero_json = generar_terceros(row)
|
||||
doc_id = tercero_json["numDocumentoIdentificacion"]
|
||||
if doc_id in pacientes_enviados:
|
||||
continue
|
||||
pacientes_enviados.append(doc_id)
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=int(configs.get("api_timeout", 30))) as client:
|
||||
if api_method == "POST":
|
||||
resp = await client.post(api_url + "/terceros", json=tercero_json, headers=headers)
|
||||
else:
|
||||
resp = await client.put(api_url + "/terceros", json=tercero_json, headers=headers)
|
||||
|
||||
if resp.is_success:
|
||||
terceros_enviados += 1
|
||||
else:
|
||||
terceros_errores += 1
|
||||
except Exception as e:
|
||||
terceros_errores += 1
|
||||
|
||||
conn = get_connection()
|
||||
conn.execute("""
|
||||
INSERT INTO envios (user_id, tipo, factura, status, json_enviado, respuesta_api, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
""", (
|
||||
user["user_id"], "terceros", factura or "AUTO",
|
||||
"success" if resp.is_success else "error",
|
||||
json_lib.dumps(tercero_json, indent=2, ensure_ascii=False),
|
||||
resp.text[:1000] if resp.is_success else str(e),
|
||||
datetime.now().isoformat(),
|
||||
))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
resultado["paso1_terceros"] = {
|
||||
"status": "success" if terceros_errores == 0 else "partial",
|
||||
"enviados": terceros_enviados,
|
||||
"errores": terceros_errores,
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
# PASO 2: Enviar TRANSACCION
|
||||
# ---------------------------------------------------------------
|
||||
params = {"fecha_ini": fecha_inicio, "fecha_fin": fecha_fin}
|
||||
if ":factura" in q_trans["query_text"] and factura:
|
||||
params["factura"] = factura
|
||||
|
||||
success, error, rows = fb.execute_query(q_trans["query_text"], params)
|
||||
|
||||
if not success:
|
||||
resultado["paso2_transaccion"] = {"status": "error", "message": error}
|
||||
elif not rows:
|
||||
resultado["paso2_transaccion"] = {"status": "error", "message": "No hay servicios para enviar"}
|
||||
else:
|
||||
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))
|
||||
|
||||
trans_enviados = 0
|
||||
trans_errores = 0
|
||||
|
||||
for (fact, doc_key), grupo in grupos.items():
|
||||
paciente_data = {"tipoDocumentoIdentificacion": doc_key[0], "numDocumentoIdentificacion": doc_key[1]}
|
||||
trans_json = generar_transaccion(
|
||||
fact,
|
||||
configs.get("num_documento_obligado", ""),
|
||||
paciente_data,
|
||||
grupo["procedimientos"],
|
||||
)
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=int(configs.get("api_timeout", 30))) as client:
|
||||
if api_method == "POST":
|
||||
resp = await client.post(api_url + "/transaccion", json=trans_json, headers=headers)
|
||||
else:
|
||||
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:
|
||||
status_ok = False
|
||||
resp_text = str(e)
|
||||
|
||||
if status_ok:
|
||||
trans_enviados += 1
|
||||
else:
|
||||
trans_errores += 1
|
||||
|
||||
conn = get_connection()
|
||||
conn.execute("""
|
||||
INSERT INTO envios (user_id, tipo, factura, fecha_inicio, fecha_fin,
|
||||
pacientes_count, servicios_count, status, json_enviado, respuesta_api, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""", (
|
||||
user["user_id"], "transaccion", fact,
|
||||
fecha_inicio, fecha_fin,
|
||||
1, len(grupo["procedimientos"]),
|
||||
"success" if status_ok else "error",
|
||||
json_lib.dumps(trans_json, indent=2, ensure_ascii=False)[:5000],
|
||||
resp_text,
|
||||
datetime.now().isoformat(),
|
||||
))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
resultado["paso2_transaccion"] = {
|
||||
"status": "success" if trans_errores == 0 else "partial",
|
||||
"enviados": trans_enviados,
|
||||
"errores": trans_errores,
|
||||
}
|
||||
|
||||
fb.disconnect()
|
||||
return JSONResponse({"success": True, "resultado": resultado})
|
||||
@@ -0,0 +1,57 @@
|
||||
from fastapi import APIRouter, Request, Form, Depends
|
||||
from fastapi.responses import RedirectResponse
|
||||
from app.database import get_connection
|
||||
from app.auth import get_current_user
|
||||
|
||||
router = APIRouter(prefix="/config", tags=["config"])
|
||||
|
||||
DEFAULT_KEYS = [
|
||||
("firebird_host", "localhost"),
|
||||
("firebird_port", "3050"),
|
||||
("firebird_database", "/path/to/database.fdb"),
|
||||
("firebird_user", "SYSDBA"),
|
||||
("firebird_password", "masterkey"),
|
||||
("api_url", "https://api.example.com/rips"),
|
||||
("api_method", "POST"),
|
||||
("api_key", ""),
|
||||
("api_timeout", "30"),
|
||||
("num_documento_obligado", ""),
|
||||
("cod_prestador", ""),
|
||||
]
|
||||
|
||||
|
||||
def ensure_defaults():
|
||||
conn = get_connection()
|
||||
for key, default in DEFAULT_KEYS:
|
||||
exists = conn.execute("SELECT id FROM config WHERE key = ?", (key,)).fetchone()
|
||||
if not exists:
|
||||
conn.execute("INSERT INTO config (key, value) VALUES (?, ?)", (key, default))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def config_page(request: Request, user: dict = Depends(get_current_user)):
|
||||
ensure_defaults()
|
||||
conn = get_connection()
|
||||
configs = conn.execute("SELECT * FROM config ORDER BY key").fetchall()
|
||||
conn.close()
|
||||
return request.app.state.templates.TemplateResponse("config.html", {
|
||||
"request": request, "user": user, "configs": configs
|
||||
})
|
||||
|
||||
|
||||
@router.post("/save")
|
||||
async def config_save(request: Request, user: dict = Depends(get_current_user)):
|
||||
form = await request.form()
|
||||
conn = get_connection()
|
||||
for key, value in form.multi_items():
|
||||
if key.startswith("config_"):
|
||||
real_key = key.replace("config_", "", 1)
|
||||
conn.execute(
|
||||
"UPDATE config SET value = ? WHERE key = ?",
|
||||
(value, real_key)
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return RedirectResponse("/config", status_code=302)
|
||||
@@ -0,0 +1,31 @@
|
||||
from fastapi import APIRouter, Request, Depends
|
||||
from app.database import get_connection
|
||||
from app.auth import get_current_user
|
||||
|
||||
router = APIRouter(tags=["dashboard"])
|
||||
|
||||
|
||||
@router.get("/dashboard")
|
||||
async def dashboard(request: Request, user: dict = Depends(get_current_user)):
|
||||
conn = get_connection()
|
||||
try:
|
||||
stats = conn.execute("""
|
||||
SELECT
|
||||
(SELECT COUNT(*) FROM envios WHERE status = 'success') as total_exitosos,
|
||||
(SELECT COUNT(*) FROM envios WHERE status = 'error') as total_errores,
|
||||
(SELECT COUNT(*) FROM envios WHERE tipo = 'terceros') as total_terceros,
|
||||
(SELECT COUNT(*) FROM envios WHERE tipo = 'transaccion') as total_transacciones,
|
||||
(SELECT COUNT(DISTINCT factura) FROM envios WHERE factura IS NOT NULL) as total_facturas
|
||||
""").fetchone()
|
||||
|
||||
ultimos = conn.execute("""
|
||||
SELECT e.*, u.username FROM envios e
|
||||
LEFT JOIN users u ON e.user_id = u.id
|
||||
ORDER BY e.created_at DESC LIMIT 10
|
||||
""").fetchall()
|
||||
|
||||
return request.app.state.templates.TemplateResponse("dashboard.html", {
|
||||
"request": request, "user": user, "stats": stats, "ultimos": ultimos
|
||||
})
|
||||
finally:
|
||||
conn.close()
|
||||
@@ -0,0 +1,55 @@
|
||||
from fastapi import APIRouter, Request, Depends
|
||||
from app.database import get_connection
|
||||
from app.auth import get_current_user
|
||||
|
||||
router = APIRouter(prefix="/logs", tags=["logs"])
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def logs_page(
|
||||
request: Request,
|
||||
user: dict = Depends(get_current_user),
|
||||
tipo: str = "",
|
||||
status: str = "",
|
||||
factura: str = "",
|
||||
):
|
||||
conn = get_connection()
|
||||
|
||||
where = ["1=1"]
|
||||
params = []
|
||||
|
||||
if tipo:
|
||||
where.append("e.tipo = ?")
|
||||
params.append(tipo)
|
||||
if status:
|
||||
where.append("e.status = ?")
|
||||
params.append(status)
|
||||
if factura:
|
||||
where.append("e.factura LIKE ?")
|
||||
params.append(f"%{factura}%")
|
||||
|
||||
envios = conn.execute(f"""
|
||||
SELECT e.*, u.username FROM envios e
|
||||
LEFT JOIN users u ON e.user_id = u.id
|
||||
WHERE {' AND '.join(where)}
|
||||
ORDER BY e.created_at DESC LIMIT 100
|
||||
""", params).fetchall()
|
||||
|
||||
stats = conn.execute("""
|
||||
SELECT
|
||||
tipo,
|
||||
status,
|
||||
COUNT(*) as total,
|
||||
SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) as exitosos,
|
||||
SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END) as fallidos
|
||||
FROM envios
|
||||
GROUP BY tipo, status
|
||||
""").fetchall()
|
||||
|
||||
conn.close()
|
||||
|
||||
return request.app.state.templates.TemplateResponse("logs.html", {
|
||||
"request": request, "user": user,
|
||||
"envios": envios, "stats": stats,
|
||||
"filtro_tipo": tipo, "filtro_status": status, "filtro_factura": factura,
|
||||
})
|
||||
@@ -0,0 +1,152 @@
|
||||
from fastapi import APIRouter, Request, Form, Depends
|
||||
from fastapi.responses import RedirectResponse, JSONResponse
|
||||
from app.database import get_connection
|
||||
from app.auth import get_current_user
|
||||
from app.models import QueryCreate
|
||||
|
||||
router = APIRouter(prefix="/queries", tags=["queries"])
|
||||
|
||||
QUERY_DEFAULTS = [
|
||||
{
|
||||
"name": "Terceros - Datos del paciente",
|
||||
"query_type": "terceros",
|
||||
"query_text": """SELECT
|
||||
p.TIPO_DOCUMENTO as tipo_documento,
|
||||
p.NUMERO_DOCUMENTO as numero_documento,
|
||||
p.PRIMER_NOMBRE as primer_nombre,
|
||||
p.SEGUNDO_NOMBRE as segundo_nombre,
|
||||
p.PRIMER_APELLIDO as primer_apellido,
|
||||
p.SEGUNDO_APELLIDO as segundo_apellido,
|
||||
p.FECHA_NACIMIENTO as fecha_nacimiento,
|
||||
p.SEXO as cod_sexo,
|
||||
p.COD_ENTIDAD as cod_entidad,
|
||||
p.TIPO_USUARIO as tipo_usuario,
|
||||
p.COD_MUNICIPIO as cod_municipio,
|
||||
p.ZONA as cod_zona,
|
||||
p.DIRECCION as direccion
|
||||
FROM USUAHOS p
|
||||
WHERE p.NUMERO_DOCUMENTO = :doc_num""",
|
||||
"description": "Consulta datos maestros del paciente por documento"
|
||||
},
|
||||
{
|
||||
"name": "Procedimientos por factura",
|
||||
"query_type": "transaccion",
|
||||
"query_text": """SELECT
|
||||
s.CODIGO_CUP as cod_procedimiento,
|
||||
s.FECHA_ATENCION as fecha_atencion,
|
||||
s.COD_DIAGNOSTICO as cod_diagnostico,
|
||||
s.FINALIDAD as finalidad,
|
||||
s.VIA_INGRESO as via_ingreso,
|
||||
s.MODALIDAD as modalidad,
|
||||
s.GRUPO_SERVICIO as grupo_servicio,
|
||||
s.COD_SERVICIO as cod_servicio,
|
||||
s.COD_PRESTADOR as cod_prestador,
|
||||
s.TIPO_DOC_PROFESIONAL as tipo_doc_profesional,
|
||||
s.NUM_DOC_PROFESIONAL as num_doc_profesional,
|
||||
s.VR_SERVICIO as vr_servicio,
|
||||
s.VALOR_PAGO_MODERADOR as valor_pago_moderador,
|
||||
s.CONCEPTO_RECAUDO as concepto_recaudo,
|
||||
s.NUM_AUTORIZACION as num_autorizacion
|
||||
FROM SERVICIOS s
|
||||
WHERE s.NUM_FACTURA = :factura
|
||||
AND s.FECHA_ATENCION BETWEEN :fecha_ini AND :fecha_fin""",
|
||||
"description": "Consulta procedimientos por factura y rango de fechas"
|
||||
},
|
||||
{
|
||||
"name": "Procedimientos por fecha",
|
||||
"query_type": "transaccion",
|
||||
"query_text": """SELECT
|
||||
s.FACTURA as num_factura,
|
||||
s.CODIGO_CUP as cod_procedimiento,
|
||||
s.FECHA_ATENCION as fecha_atencion,
|
||||
s.COD_DIAGNOSTICO as cod_diagnostico,
|
||||
s.FINALIDAD as finalidad,
|
||||
s.VIA_INGRESO as via_ingreso,
|
||||
s.MODALIDAD as modalidad,
|
||||
s.GRUPO_SERVICIO as grupo_servicio,
|
||||
s.COD_SERVICIO as cod_servicio,
|
||||
s.COD_PRESTADOR as cod_prestador,
|
||||
s.TIPO_DOC_PROFESIONAL as tipo_doc_profesional,
|
||||
s.NUM_DOC_PROFESIONAL as num_doc_profesional,
|
||||
s.VR_SERVICIO as vr_servicio,
|
||||
p.TIPO_DOCUMENTO as tipo_doc_paciente,
|
||||
p.NUMERO_DOCUMENTO as num_doc_paciente
|
||||
FROM SERVICIOS s
|
||||
JOIN USUAHOS p ON s.COD_PACIENTE = p.COD_PACIENTE
|
||||
WHERE s.FECHA_ATENCION BETWEEN :fecha_ini AND :fecha_fin""",
|
||||
"description": "Consulta todos los procedimientos en rango de fechas"
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def ensure_defaults():
|
||||
conn = get_connection()
|
||||
for q in QUERY_DEFAULTS:
|
||||
exists = conn.execute(
|
||||
"SELECT id FROM queries WHERE name = ?", (q["name"],)
|
||||
).fetchone()
|
||||
if not exists:
|
||||
conn.execute(
|
||||
"INSERT INTO queries (name, query_type, query_text, description) VALUES (?, ?, ?, ?)",
|
||||
(q["name"], q["query_type"], q["query_text"], q["description"]),
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def queries_page(request: Request, user: dict = Depends(get_current_user)):
|
||||
ensure_defaults()
|
||||
conn = get_connection()
|
||||
queries = conn.execute("SELECT * FROM queries ORDER BY query_type, name").fetchall()
|
||||
conn.close()
|
||||
return request.app.state.templates.TemplateResponse("queries.html", {
|
||||
"request": request, "user": user, "queries": queries
|
||||
})
|
||||
|
||||
|
||||
@router.post("/create")
|
||||
async def query_create(
|
||||
request: Request,
|
||||
user: dict = Depends(get_current_user),
|
||||
name: str = Form(...),
|
||||
query_type: str = Form(...),
|
||||
query_text: str = Form(...),
|
||||
description: str = Form(""),
|
||||
):
|
||||
conn = get_connection()
|
||||
conn.execute(
|
||||
"INSERT INTO queries (name, query_type, query_text, description) VALUES (?, ?, ?, ?)",
|
||||
(name, query_type, query_text, description),
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return RedirectResponse("/queries", status_code=302)
|
||||
|
||||
|
||||
@router.post("/update/{query_id}")
|
||||
async def query_update(
|
||||
query_id: int,
|
||||
request: Request,
|
||||
user: dict = Depends(get_current_user),
|
||||
name: str = Form(...),
|
||||
query_text: str = Form(...),
|
||||
description: str = Form(""),
|
||||
):
|
||||
conn = get_connection()
|
||||
conn.execute(
|
||||
"UPDATE queries SET name = ?, query_text = ?, description = ? WHERE id = ?",
|
||||
(name, query_text, description, query_id),
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return RedirectResponse("/queries", status_code=302)
|
||||
|
||||
|
||||
@router.post("/delete/{query_id}")
|
||||
async def query_delete(query_id: int, user: dict = Depends(get_current_user)):
|
||||
conn = get_connection()
|
||||
conn.execute("DELETE FROM queries WHERE id = ?", (query_id,))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return RedirectResponse("/queries", status_code=302)
|
||||
@@ -0,0 +1,191 @@
|
||||
from fastapi import APIRouter, Request, Form, Depends
|
||||
from fastapi.responses import RedirectResponse, JSONResponse
|
||||
from app.database import get_connection
|
||||
from app.auth import get_current_user
|
||||
from app.services.firebird_service import FirebirdService
|
||||
from app.services.json_generator import generar_terceros
|
||||
|
||||
router = APIRouter(prefix="/terceros", tags=["terceros"])
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def terceros_page(request: Request, user: dict = Depends(get_current_user)):
|
||||
conn = get_connection()
|
||||
envios = conn.execute("""
|
||||
SELECT * FROM envios WHERE tipo = 'terceros'
|
||||
ORDER BY created_at DESC LIMIT 20
|
||||
""").fetchall()
|
||||
queries = conn.execute(
|
||||
"SELECT * FROM queries WHERE query_type = 'terceros' ORDER BY name"
|
||||
).fetchall()
|
||||
configs = {row["key"]: row["value"] for row in conn.execute("SELECT * FROM config").fetchall()}
|
||||
conn.close()
|
||||
|
||||
return request.app.state.templates.TemplateResponse("terceros.html", {
|
||||
"request": request, "user": user,
|
||||
"envios": envios, "queries": queries,
|
||||
"configs": configs,
|
||||
"firebird_host": configs.get("firebird_host", "localhost"),
|
||||
"firebird_port": configs.get("firebird_port", "3050"),
|
||||
"firebird_database": configs.get("firebird_database", ""),
|
||||
})
|
||||
|
||||
|
||||
@router.post("/test-connection")
|
||||
async def test_connection(
|
||||
request: Request,
|
||||
user: dict = Depends(get_current_user),
|
||||
host: str = Form(...),
|
||||
port: int = Form(...),
|
||||
database: str = Form(...),
|
||||
fb_user: str = Form(...),
|
||||
fb_password: str = Form(...),
|
||||
):
|
||||
fb = FirebirdService()
|
||||
success, msg = fb.connect(host, port, database, fb_user, fb_password)
|
||||
if success:
|
||||
fb.disconnect()
|
||||
return JSONResponse({"success": success, "message": msg})
|
||||
|
||||
|
||||
@router.post("/preview")
|
||||
async def preview_query(
|
||||
request: Request,
|
||||
user: dict = Depends(get_current_user),
|
||||
query_id: int = Form(...),
|
||||
doc_num: str = Form(""),
|
||||
):
|
||||
conn = get_connection()
|
||||
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()}
|
||||
conn.close()
|
||||
|
||||
if not q:
|
||||
return JSONResponse({"success": False, "message": "Consulta no encontrada"})
|
||||
|
||||
fb = FirebirdService()
|
||||
fb_success, fb_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"),
|
||||
)
|
||||
if not fb_success:
|
||||
return JSONResponse({"success": False, "message": f"Error Firebird: {fb_msg}"})
|
||||
|
||||
params = {}
|
||||
if ":doc_num" in q["query_text"] and doc_num:
|
||||
params["doc_num"] = doc_num
|
||||
if ":fecha_ini" in q["query_text"]:
|
||||
params["fecha_ini"] = "1900-01-01"
|
||||
params["fecha_fin"] = "2100-12-31"
|
||||
if ":factura" in q["query_text"]:
|
||||
params["factura"] = doc_num if doc_num else ""
|
||||
|
||||
success, error, rows = fb.execute_query(q["query_text"], params if params else None)
|
||||
fb.disconnect()
|
||||
|
||||
if not success:
|
||||
return JSONResponse({"success": False, "message": error})
|
||||
|
||||
json_result = None
|
||||
if rows:
|
||||
json_result = generar_terceros(rows[0])
|
||||
|
||||
return JSONResponse({
|
||||
"success": True,
|
||||
"rows_count": len(rows),
|
||||
"columns": list(rows[0].keys()) if rows else [],
|
||||
"preview": rows[:5],
|
||||
"generated_json": json_result,
|
||||
})
|
||||
|
||||
|
||||
@router.post("/send")
|
||||
async def send_terceros(
|
||||
request: Request,
|
||||
user: dict = Depends(get_current_user),
|
||||
query_id: int = Form(...),
|
||||
doc_num: str = Form(""),
|
||||
):
|
||||
import json
|
||||
import httpx
|
||||
from datetime import datetime
|
||||
|
||||
conn = get_connection()
|
||||
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()}
|
||||
conn.close()
|
||||
|
||||
if not q:
|
||||
return JSONResponse({"success": False, "message": "Consulta no encontrada"})
|
||||
|
||||
fb = FirebirdService()
|
||||
fb_success, fb_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"),
|
||||
)
|
||||
if not fb_success:
|
||||
return JSONResponse({"success": False, "message": f"Error Firebird: {fb_msg}"})
|
||||
|
||||
params = {}
|
||||
if ":doc_num" in q["query_text"]:
|
||||
params["doc_num"] = doc_num or configs.get("doc_num_default", "")
|
||||
|
||||
success, error, rows = fb.execute_query(q["query_text"], params if params else None)
|
||||
fb.disconnect()
|
||||
|
||||
if not success:
|
||||
return JSONResponse({"success": False, "message": error})
|
||||
|
||||
if not rows:
|
||||
return JSONResponse({"success": False, "message": "No se encontraron datos"})
|
||||
|
||||
# Generar JSON terceros
|
||||
tercero_json = generar_terceros(rows[0])
|
||||
|
||||
# Enviar a API
|
||||
api_url = configs.get("api_url", "")
|
||||
api_key = configs.get("api_key", "")
|
||||
api_method = configs.get("api_method", "POST")
|
||||
|
||||
headers = {"Content-Type": "application/json"}
|
||||
if api_key:
|
||||
headers["Authorization"] = f"Bearer {api_key}"
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=int(configs.get("api_timeout", 30))) as client:
|
||||
if api_method == "POST":
|
||||
resp = await client.post(api_url + "/terceros", json=tercero_json, headers=headers)
|
||||
else:
|
||||
resp = await client.put(api_url + "/terceros", json=tercero_json, headers=headers)
|
||||
|
||||
result = resp.status_code, resp.is_success, resp.text
|
||||
except Exception as e:
|
||||
result = (0, False, str(e))
|
||||
|
||||
# Guardar log
|
||||
conn = get_connection()
|
||||
conn.execute("""
|
||||
INSERT INTO envios (user_id, tipo, status, json_enviado, respuesta_api, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
""", (
|
||||
user["user_id"], "terceros",
|
||||
"success" if result[1] else "error",
|
||||
json.dumps(tercero_json, indent=2, ensure_ascii=False),
|
||||
str(result[2])[:1000],
|
||||
datetime.now().isoformat(),
|
||||
))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
return JSONResponse({
|
||||
"success": result[1],
|
||||
"status_code": result[0],
|
||||
"message": "Envío exitoso" if result[1] else f"Error: {result[2]}",
|
||||
"cuv": result[2][:200] if result[1] else None,
|
||||
})
|
||||
@@ -0,0 +1,218 @@
|
||||
from fastapi import APIRouter, Request, Form, Depends
|
||||
from fastapi.responses import JSONResponse
|
||||
from app.database import get_connection
|
||||
from app.auth import get_current_user
|
||||
from app.services.firebird_service import FirebirdService
|
||||
from app.services.json_generator import generar_terceros, generar_transaccion
|
||||
|
||||
router = APIRouter(prefix="/transaccion", tags=["transaccion"])
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def transaccion_page(request: Request, user: dict = Depends(get_current_user)):
|
||||
conn = get_connection()
|
||||
envios = conn.execute("""
|
||||
SELECT * FROM envios WHERE tipo = 'transaccion'
|
||||
ORDER BY created_at DESC LIMIT 20
|
||||
""").fetchall()
|
||||
queries = conn.execute(
|
||||
"SELECT * FROM queries WHERE query_type = 'transaccion' ORDER BY name"
|
||||
).fetchall()
|
||||
configs = {row["key"]: row["value"] for row in conn.execute("SELECT * FROM config").fetchall()}
|
||||
conn.close()
|
||||
|
||||
return request.app.state.templates.TemplateResponse("transaccion.html", {
|
||||
"request": request, "user": user,
|
||||
"envios": envios, "queries": queries,
|
||||
"configs": configs,
|
||||
})
|
||||
|
||||
|
||||
@router.post("/preview")
|
||||
async def preview_transaccion(
|
||||
request: Request,
|
||||
user: dict = Depends(get_current_user),
|
||||
query_id: int = Form(...),
|
||||
factura: str = Form(""),
|
||||
fecha_inicio: str = Form(...),
|
||||
fecha_fin: str = Form(...),
|
||||
):
|
||||
conn = get_connection()
|
||||
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()}
|
||||
conn.close()
|
||||
|
||||
if not q:
|
||||
return JSONResponse({"success": False, "message": "Consulta no encontrada"})
|
||||
|
||||
fb = FirebirdService()
|
||||
fb_success, fb_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"),
|
||||
)
|
||||
if not fb_success:
|
||||
return JSONResponse({"success": False, "message": f"Error Firebird: {fb_msg}"})
|
||||
|
||||
params = {"fecha_ini": fecha_inicio, "fecha_fin": fecha_fin}
|
||||
if ":factura" in q["query_text"] and factura:
|
||||
params["factura"] = factura
|
||||
|
||||
success, error, rows = fb.execute_query(q["query_text"], params)
|
||||
fb.disconnect()
|
||||
|
||||
if not success:
|
||||
return JSONResponse({"success": False, "message": error})
|
||||
|
||||
# Agrupar por paciente y 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 = []
|
||||
for (fact, doc_key), grupo in grupos.items():
|
||||
paciente_data = {"tipoDocumentoIdentificacion": doc_key[0], "numDocumentoIdentificacion": doc_key[1]}
|
||||
trans = generar_transaccion(
|
||||
fact,
|
||||
configs.get("num_documento_obligado", ""),
|
||||
paciente_data,
|
||||
grupo["procedimientos"],
|
||||
)
|
||||
json_result.append(trans)
|
||||
|
||||
return JSONResponse({
|
||||
"success": True,
|
||||
"rows_count": len(rows),
|
||||
"grupos_count": len(grupos),
|
||||
"columns": list(rows[0].keys()) if rows else [],
|
||||
"preview": rows[:5],
|
||||
"generated_json": json_result[0] if json_result else None,
|
||||
"total_json": len(json_result),
|
||||
})
|
||||
|
||||
|
||||
@router.post("/send")
|
||||
async def send_transaccion(
|
||||
request: Request,
|
||||
user: dict = Depends(get_current_user),
|
||||
query_id: int = Form(...),
|
||||
factura: str = Form(""),
|
||||
fecha_inicio: str = Form(...),
|
||||
fecha_fin: str = Form(...),
|
||||
):
|
||||
import json as json_lib
|
||||
import httpx
|
||||
from datetime import datetime
|
||||
from collections import defaultdict
|
||||
|
||||
conn = get_connection()
|
||||
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()}
|
||||
conn.close()
|
||||
|
||||
if not q:
|
||||
return JSONResponse({"success": False, "message": "Consulta no encontrada"})
|
||||
|
||||
fb = FirebirdService()
|
||||
fb_success, fb_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"),
|
||||
)
|
||||
if not fb_success:
|
||||
return JSONResponse({"success": False, "message": f"Error Firebird: {fb_msg}"})
|
||||
|
||||
params = {"fecha_ini": fecha_inicio, "fecha_fin": fecha_fin}
|
||||
if ":factura" in q["query_text"] and factura:
|
||||
params["factura"] = factura
|
||||
|
||||
success, error, rows = fb.execute_query(q["query_text"], params)
|
||||
fb.disconnect()
|
||||
|
||||
if not success:
|
||||
return JSONResponse({"success": False, "message": error})
|
||||
if not rows:
|
||||
return JSONResponse({"success": False, "message": "No se encontraron datos"})
|
||||
|
||||
# Agrupar
|
||||
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_key = configs.get("api_key", "")
|
||||
api_method = configs.get("api_method", "POST")
|
||||
headers = {"Content-Type": "application/json"}
|
||||
if api_key:
|
||||
headers["Authorization"] = f"Bearer {api_key}"
|
||||
|
||||
total_enviados = 0
|
||||
total_errores = 0
|
||||
resultados = []
|
||||
|
||||
for (fact, doc_key), grupo in grupos.items():
|
||||
paciente_data = {"tipoDocumentoIdentificacion": doc_key[0], "numDocumentoIdentificacion": doc_key[1]}
|
||||
trans_json = generar_transaccion(
|
||||
fact,
|
||||
configs.get("num_documento_obligado", ""),
|
||||
paciente_data,
|
||||
grupo["procedimientos"],
|
||||
)
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=int(configs.get("api_timeout", 30))) as client:
|
||||
if api_method == "POST":
|
||||
resp = await client.post(api_url + "/transaccion", json=trans_json, headers=headers)
|
||||
else:
|
||||
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:
|
||||
status_ok = False
|
||||
response_text = str(e)
|
||||
|
||||
if status_ok:
|
||||
total_enviados += 1
|
||||
else:
|
||||
total_errores += 1
|
||||
|
||||
resultados.append({"factura": fact, "success": status_ok})
|
||||
|
||||
# Guardar log
|
||||
conn = get_connection()
|
||||
conn.execute("""
|
||||
INSERT INTO envios (user_id, tipo, factura, fecha_inicio, fecha_fin,
|
||||
pacientes_count, servicios_count, status, json_enviado, respuesta_api, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""", (
|
||||
user["user_id"], "transaccion", fact,
|
||||
fecha_inicio, fecha_fin,
|
||||
1, len(grupo["procedimientos"]),
|
||||
"success" if status_ok else "error",
|
||||
json_lib.dumps(trans_json, indent=2, ensure_ascii=False)[:5000],
|
||||
response_text,
|
||||
datetime.now().isoformat(),
|
||||
))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
return JSONResponse({
|
||||
"success": total_errores == 0,
|
||||
"total_enviados": total_enviados,
|
||||
"total_errores": total_errores,
|
||||
"resultados": resultados,
|
||||
"message": f"Enviados: {total_enviados}, Errores: {total_errores}",
|
||||
})
|
||||
@@ -0,0 +1,35 @@
|
||||
import httpx
|
||||
import json
|
||||
from typing import Optional
|
||||
|
||||
|
||||
async def send_json(
|
||||
url: str,
|
||||
json_data: dict,
|
||||
method: str = "POST",
|
||||
headers: Optional[dict] = None,
|
||||
timeout: int = 30,
|
||||
) -> dict:
|
||||
default_headers = {"Content-Type": "application/json"}
|
||||
if headers:
|
||||
default_headers.update(headers)
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=timeout) as client:
|
||||
if method == "POST":
|
||||
resp = await client.post(url, json=json_data, headers=default_headers)
|
||||
elif method == "PUT":
|
||||
resp = await client.put(url, json=json_data, headers=default_headers)
|
||||
else:
|
||||
resp = await client.get(url, headers=default_headers)
|
||||
|
||||
return {
|
||||
"status_code": resp.status_code,
|
||||
"success": resp.is_success,
|
||||
"body": resp.text,
|
||||
"headers": dict(resp.headers),
|
||||
}
|
||||
except httpx.TimeoutException:
|
||||
return {"status_code": 0, "success": False, "body": "Timeout", "headers": {}}
|
||||
except Exception as e:
|
||||
return {"status_code": 0, "success": False, "body": str(e), "headers": {}}
|
||||
@@ -0,0 +1,55 @@
|
||||
import fdb
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class FirebirdService:
|
||||
def __init__(self):
|
||||
self.conn = None
|
||||
|
||||
def connect(self, host: str, port: int, database: str, user: str, password: str):
|
||||
try:
|
||||
self.conn = fdb.connect(
|
||||
host=host,
|
||||
port=port,
|
||||
database=database,
|
||||
user=user,
|
||||
password=password,
|
||||
charset="UTF8",
|
||||
)
|
||||
return True, "Conexión exitosa"
|
||||
except Exception as e:
|
||||
return False, str(e)
|
||||
|
||||
def disconnect(self):
|
||||
if self.conn:
|
||||
self.conn.close()
|
||||
self.conn = None
|
||||
|
||||
def is_connected(self) -> bool:
|
||||
return self.conn is not None
|
||||
|
||||
def test_connection(self) -> tuple:
|
||||
if not self.conn:
|
||||
return False, "No hay conexión activa"
|
||||
try:
|
||||
cur = self.conn.cursor()
|
||||
cur.execute("SELECT 1 FROM RDB$DATABASE")
|
||||
cur.fetchone()
|
||||
return True, "Conexión OK"
|
||||
except Exception as e:
|
||||
return False, str(e)
|
||||
|
||||
def execute_query(self, query: str, params: Optional[dict] = None) -> tuple:
|
||||
if not self.conn:
|
||||
return False, "No hay conexión activa", []
|
||||
try:
|
||||
cur = self.conn.cursor()
|
||||
if params:
|
||||
cur.execute(query, params)
|
||||
else:
|
||||
cur.execute(query)
|
||||
columns = [desc[0] for desc in cur.description] if cur.description else []
|
||||
rows = cur.fetchall()
|
||||
return True, "", [dict(zip(columns, row)) for row in rows]
|
||||
except Exception as e:
|
||||
return False, str(e), []
|
||||
@@ -0,0 +1,89 @@
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
|
||||
def generar_terceros(row: dict) -> dict:
|
||||
return {
|
||||
"tipoDocumentoIdentificacion": row.get("tipo_documento", "CC"),
|
||||
"numDocumentoIdentificacion": row.get("numero_documento", ""),
|
||||
"primerNombre": (row.get("primer_nombre") or "").upper(),
|
||||
"segundoNombre": (row.get("segundo_nombre") or "").upper(),
|
||||
"primerApellido": (row.get("primer_apellido") or "").upper(),
|
||||
"segundoApellido": (row.get("segundo_apellido") or "").upper(),
|
||||
"fechaNacimiento": str(row.get("fecha_nacimiento", ""))[:10],
|
||||
"codSexo": row.get("cod_sexo", "M"),
|
||||
"codEntidadAdministradora": row.get("cod_entidad", ""),
|
||||
"tipoUsuario": row.get("tipo_usuario", "01"),
|
||||
"codPaisResidencia": row.get("cod_pais", "170"),
|
||||
"codMunicipioResidencia": row.get("cod_municipio", ""),
|
||||
"codZonaTerritorialResidencia": row.get("cod_zona", "01"),
|
||||
"incapacidad": row.get("incapacidad", "NO"),
|
||||
"codPaisOrigen": row.get("cod_pais_origen", "170"),
|
||||
"direccionResidencia": row.get("direccion", ""),
|
||||
"codZonaResidencia": row.get("cod_zona", "01"),
|
||||
}
|
||||
|
||||
|
||||
def generar_procedimiento(row: dict, consecutivo: int) -> dict:
|
||||
return {
|
||||
"consecutivo": consecutivo,
|
||||
"codProcedimiento": row.get("cod_procedimiento", ""),
|
||||
"fechaInicioAtencion": str(row.get("fecha_atencion", datetime.now().strftime("%Y-%m-%d %H:%M")))[:16],
|
||||
"codDiagnosticoPrincipal": row.get("cod_diagnostico", "R790"),
|
||||
"codDiagnosticoRelacionado": row.get("cod_diagnostico_rel", None),
|
||||
"finalidadTecnologiaSalud": row.get("finalidad", "23"),
|
||||
"viaIngresoServicioSalud": row.get("via_ingreso", "02"),
|
||||
"modalidadGrupoServicioTecSal": row.get("modalidad", "01"),
|
||||
"grupoServicios": row.get("grupo_servicio", "02"),
|
||||
"codServicio": int(row.get("cod_servicio", 706)),
|
||||
"codPrestador": row.get("cod_prestador", ""),
|
||||
"tipoDocumentoIdentificacion": row.get("tipo_doc_profesional", "CC"),
|
||||
"numDocumentoIdentificacion": row.get("num_doc_profesional", ""),
|
||||
"vrServicio": int(row.get("vr_servicio", 0)),
|
||||
"valorPagoModerador": int(row.get("valor_pago_moderador", 0)),
|
||||
"conceptoRecaudo": row.get("concepto_recaudo", "05"),
|
||||
"numAutorizacion": row.get("num_autorizacion", None),
|
||||
"idMIPRES": row.get("id_mipres", None),
|
||||
"codComplicacion": row.get("cod_complicacion", None),
|
||||
"numFEVPagoModerador": row.get("num_fev_pago_moderador", None),
|
||||
}
|
||||
|
||||
|
||||
def generar_transaccion(
|
||||
factura: str,
|
||||
num_doc_obligado: str,
|
||||
rows_tercero: Optional[dict],
|
||||
rows_procedimientos: list,
|
||||
) -> dict:
|
||||
transaccion = {
|
||||
"numDocumentoIdObligado": num_doc_obligado,
|
||||
"numFactura": factura,
|
||||
"tipoNota": None,
|
||||
"numNota": None,
|
||||
"usuarios": [],
|
||||
}
|
||||
|
||||
if rows_tercero and rows_procedimientos:
|
||||
usuario = {
|
||||
"tipoDocumentoIdentificacion": rows_tercero.get("tipoDocumentoIdentificacion", "CC"),
|
||||
"numDocumentoIdentificacion": rows_tercero.get("numDocumentoIdentificacion", ""),
|
||||
"codEntidadAdministradora": rows_tercero.get("codEntidadAdministradora", ""),
|
||||
"tipoUsuario": rows_tercero.get("tipoUsuario", "01"),
|
||||
"fechaNacimiento": rows_tercero.get("fechaNacimiento", ""),
|
||||
"codSexo": rows_tercero.get("codSexo", "M"),
|
||||
"codPaisResidencia": rows_tercero.get("codPaisResidencia", "170"),
|
||||
"codMunicipioResidencia": rows_tercero.get("codMunicipioResidencia", ""),
|
||||
"codZonaTerritorialResidencia": rows_tercero.get("codZonaTerritorialResidencia", "01"),
|
||||
"incapacidad": rows_tercero.get("incapacidad", "NO"),
|
||||
"consecutivo": 1,
|
||||
"codPaisOrigen": rows_tercero.get("codPaisOrigen", "170"),
|
||||
"servicios": {
|
||||
"procedimientos": [
|
||||
generar_procedimiento(p, i + 1)
|
||||
for i, p in enumerate(rows_procedimientos)
|
||||
]
|
||||
},
|
||||
}
|
||||
transaccion["usuarios"].append(usuario)
|
||||
|
||||
return transaccion
|
||||
@@ -0,0 +1,163 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Automatización{% endblock %}
|
||||
{% block header %}Automatización Completa{% endblock %}
|
||||
{% block content %}
|
||||
<div class="max-w-4xl">
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h3 class="font-semibold text-gray-800"><i class="fas fa-robot mr-2 text-blue-500"></i>Paso 1 + Paso 2 Automático</h3>
|
||||
<p class="text-xs text-gray-500 mt-1">Selecciona las consultas y el rango de fechas. El sistema enviará primero los terceros y luego las transacciones RIPS automáticamente.</p>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<form id="form-automation" class="space-y-6">
|
||||
<div class="grid grid-cols-2 gap-6">
|
||||
<div class="p-4 bg-blue-50 rounded-xl border border-blue-200">
|
||||
<div class="flex items-center mb-3">
|
||||
<div class="w-8 h-8 bg-blue-500 rounded-full flex items-center justify-center text-white text-sm font-bold">1</div>
|
||||
<span class="ml-2 font-medium text-blue-800">Paso 1: Terceros</span>
|
||||
</div>
|
||||
<select name="query_terceros_id" required
|
||||
class="w-full px-3 py-2 border border-blue-300 rounded-lg text-sm">
|
||||
<option value="">Seleccionar consulta...</option>
|
||||
{% for q in queries if q.query_type == 'terceros' %}
|
||||
<option value="{{ q.id }}">{{ q.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="p-4 bg-purple-50 rounded-xl border border-purple-200">
|
||||
<div class="flex items-center mb-3">
|
||||
<div class="w-8 h-8 bg-purple-500 rounded-full flex items-center justify-center text-white text-sm font-bold">2</div>
|
||||
<span class="ml-2 font-medium text-purple-800">Paso 2: Transacción RIPS</span>
|
||||
</div>
|
||||
<select name="query_transaccion_id" required
|
||||
class="w-full px-3 py-2 border border-purple-300 rounded-lg text-sm">
|
||||
<option value="">Seleccionar consulta...</option>
|
||||
{% for q in queries if q.query_type == 'transaccion' %}
|
||||
<option value="{{ q.id }}">{{ q.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Fecha Inicio</label>
|
||||
<input type="date" name="fecha_inicio" required
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Fecha Fin</label>
|
||||
<input type="date" name="fecha_fin" required
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Factura <span class="text-xs text-gray-400">(opcional)</span></label>
|
||||
<input type="text" name="factura"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm"
|
||||
placeholder="Filtrar por factura">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="button" onclick="runAutomation()"
|
||||
class="w-full py-3 bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700 text-white font-medium rounded-lg transition-all flex items-center justify-center">
|
||||
<i class="fas fa-play mr-2"></i> Ejecutar Automatización Completa
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Progress -->
|
||||
<div id="progress-section" class="mt-6 hidden">
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
|
||||
<h4 class="font-semibold text-gray-800 mb-4"><i class="fas fa-spinner fa-spin mr-2 text-blue-500"></i>Progreso</h4>
|
||||
<div class="space-y-4">
|
||||
<div id="step1" class="p-4 rounded-lg border border-gray-200">
|
||||
<div class="flex items-center">
|
||||
<div id="step1-icon" class="w-8 h-8 bg-gray-200 rounded-full flex items-center justify-center text-sm">1</div>
|
||||
<div class="ml-3">
|
||||
<p class="font-medium text-gray-800">Paso 1: Envío de Terceros</p>
|
||||
<p id="step1-msg" class="text-sm text-gray-500">Esperando...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="step2" class="p-4 rounded-lg border border-gray-200">
|
||||
<div class="flex items-center">
|
||||
<div id="step2-icon" class="w-8 h-8 bg-gray-200 rounded-full flex items-center justify-center text-sm">2</div>
|
||||
<div class="ml-3">
|
||||
<p class="font-medium text-gray-800">Paso 2: Envío de Transacciones</p>
|
||||
<p id="step2-msg" class="text-sm text-gray-500">Esperando...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function runAutomation() {
|
||||
if (!confirm('¿Ejecutar la automatización completa?\n\nPaso 1: Enviar terceros\nPaso 2: Enviar transacciones RIPS')) return;
|
||||
|
||||
const form = document.getElementById('form-automation');
|
||||
const data = new FormData(form);
|
||||
const btn = form.querySelector('button[onclick="runAutomation()"]');
|
||||
showLoading(btn);
|
||||
|
||||
document.getElementById('progress-section').classList.remove('hidden');
|
||||
updateStep('step1', 'processing', 'Enviando terceros...');
|
||||
|
||||
const resp = await fetch('/automation/run', {method:'POST', body: data, credentials: 'include'});
|
||||
const result = await resp.json();
|
||||
hideLoading(btn, '<i class="fas fa-play mr-2"></i> Ejecutar Automatización Completa');
|
||||
|
||||
if (result.success) {
|
||||
const r = result.resultado;
|
||||
|
||||
if (r.paso1_terceros.status === 'success') {
|
||||
updateStep('step1', 'success', `✅ ${r.paso1_terceros.enviados} terceros enviados`);
|
||||
} else if (r.paso1_terceros.status === 'partial') {
|
||||
updateStep('step1', 'warning', `⚠️ ${r.paso1_terceros.enviados} enviados, ${r.paso1_terceros.errores} errores`);
|
||||
} else {
|
||||
updateStep('step1', 'error', `❌ ${r.paso1_terceros.message || 'Error'}`);
|
||||
}
|
||||
|
||||
if (r.paso2_transaccion.status === 'success') {
|
||||
updateStep('step2', 'success', `✅ ${r.paso2_transaccion.enviados} transacciones enviadas`);
|
||||
} else if (r.paso2_transaccion.status === 'partial') {
|
||||
updateStep('step2', 'warning', `⚠️ ${r.paso2_transaccion.enviados} enviados, ${r.paso2_transaccion.errores} errores`);
|
||||
} else {
|
||||
updateStep('step2', 'error', `❌ ${r.paso2_transaccion.message || 'Error'}`);
|
||||
}
|
||||
|
||||
showToast('Automatización completada', 'success');
|
||||
} else {
|
||||
updateStep('step1', 'error', 'Error en la automatización');
|
||||
showToast('Error en la automatización', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function updateStep(id, status, msg) {
|
||||
const step = document.getElementById(id);
|
||||
const icon = document.getElementById(id + '-icon');
|
||||
const msgEl = document.getElementById(id + '-msg');
|
||||
|
||||
const icons = {
|
||||
processing: '<i class="fas fa-spinner fa-spin text-blue-500"></i>',
|
||||
success: '<i class="fas fa-check text-green-500"></i>',
|
||||
warning: '<i class="fas fa-exclamation-triangle text-yellow-500"></i>',
|
||||
error: '<i class="fas fa-times text-red-500"></i>',
|
||||
};
|
||||
|
||||
const borders = {
|
||||
processing: 'border-blue-300 bg-blue-50',
|
||||
success: 'border-green-300 bg-green-50',
|
||||
warning: 'border-yellow-300 bg-yellow-50',
|
||||
error: 'border-red-300 bg-red-50',
|
||||
};
|
||||
|
||||
icon.innerHTML = icons[status] || '1';
|
||||
step.className = `p-4 rounded-lg border ${borders[status] || 'border-gray-200'}`;
|
||||
msgEl.textContent = msg;
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,122 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es" class="h-full bg-gray-50">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>RIPS Manager - {% block title %}Dashboard{% endblock %}</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: {'50': '#eff6ff','100': '#dbeafe','200': '#bfdbfe','300': '#93c5fd','400': '#60a5fa','500': '#3b82f6','600': '#2563eb','700': '#1d4ed8','800': '#1e40af','900': '#1e3a8a'}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||
</head>
|
||||
<body class="h-full">
|
||||
<div class="min-h-full">
|
||||
{% if user %}
|
||||
<!-- Sidebar -->
|
||||
<div class="fixed inset-y-0 left-0 w-64 bg-gray-900 text-white z-30">
|
||||
<div class="flex items-center h-16 px-6 border-b border-gray-700">
|
||||
<i class="fas fa-file-medical text-blue-400 text-xl mr-3"></i>
|
||||
<span class="font-bold text-lg">RIPS Manager</span>
|
||||
</div>
|
||||
<nav class="mt-4 px-3 space-y-1">
|
||||
<a href="/dashboard" class="flex items-center px-3 py-2.5 rounded-lg text-sm font-medium {% if request.url.path == '/dashboard' %}bg-blue-600 text-white{% else %}text-gray-300 hover:bg-gray-700{% endif %}">
|
||||
<i class="fas fa-chart-pie w-5 mr-2"></i> Dashboard
|
||||
</a>
|
||||
<a href="/config" class="flex items-center px-3 py-2.5 rounded-lg text-sm font-medium {% if request.url.path == '/config' %}bg-blue-600 text-white{% else %}text-gray-300 hover:bg-gray-700{% endif %}">
|
||||
<i class="fas fa-cog w-5 mr-2"></i> Configuración
|
||||
</a>
|
||||
<a href="/queries" class="flex items-center px-3 py-2.5 rounded-lg text-sm font-medium {% if request.url.path == '/queries' %}bg-blue-600 text-white{% else %}text-gray-300 hover:bg-gray-700{% endif %}">
|
||||
<i class="fas fa-database w-5 mr-2"></i> Consultas SQL
|
||||
</a>
|
||||
<hr class="my-3 border-gray-700">
|
||||
<p class="px-3 text-xs font-semibold text-gray-400 uppercase tracking-wider">Envíos</p>
|
||||
<a href="/terceros" class="flex items-center px-3 py-2.5 rounded-lg text-sm font-medium {% if request.url.path == '/terceros' %}bg-blue-600 text-white{% else %}text-gray-300 hover:bg-gray-700{% endif %}">
|
||||
<i class="fas fa-user w-5 mr-2"></i> Terceros
|
||||
</a>
|
||||
<a href="/transaccion" class="flex items-center px-3 py-2.5 rounded-lg text-sm font-medium {% if request.url.path == '/transaccion' %}bg-blue-600 text-white{% else %}text-gray-300 hover:bg-gray-700{% endif %}">
|
||||
<i class="fas fa-exchange-alt w-5 mr-2"></i> Transacción RIPS
|
||||
</a>
|
||||
<a href="/automation" class="flex items-center px-3 py-2.5 rounded-lg text-sm font-medium {% if request.url.path == '/automation' %}bg-blue-600 text-white{% else %}text-gray-300 hover:bg-gray-700{% endif %}">
|
||||
<i class="fas fa-robot w-5 mr-2"></i> Automatización
|
||||
</a>
|
||||
<hr class="my-3 border-gray-700">
|
||||
<a href="/logs" class="flex items-center px-3 py-2.5 rounded-lg text-sm font-medium {% if request.url.path == '/logs' %}bg-blue-600 text-white{% else %}text-gray-300 hover:bg-gray-700{% endif %}">
|
||||
<i class="fas fa-history w-5 mr-2"></i> Historial
|
||||
</a>
|
||||
<a href="/auth/logout" class="flex items-center px-3 py-2.5 rounded-lg text-sm font-medium text-gray-300 hover:bg-gray-700">
|
||||
<i class="fas fa-sign-out-alt w-5 mr-2"></i> Salir
|
||||
</a>
|
||||
</nav>
|
||||
<div class="absolute bottom-0 left-0 right-0 px-4 py-3 border-t border-gray-700">
|
||||
<div class="flex items-center">
|
||||
<div class="w-8 h-8 bg-blue-500 rounded-full flex items-center justify-center text-sm font-bold">
|
||||
{{ user.username[0]|upper }}
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm font-medium text-white">{{ user.username }}</p>
|
||||
<p class="text-xs text-gray-400">Sesión activa</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main content -->
|
||||
<div class="pl-64">
|
||||
<header class="bg-white shadow-sm border-b border-gray-200">
|
||||
<div class="flex items-center justify-between h-16 px-8">
|
||||
<h1 class="text-xl font-semibold text-gray-800">{% block header %}Dashboard{% endblock %}</h1>
|
||||
<div class="flex items-center space-x-4">
|
||||
<span class="text-sm text-gray-500">
|
||||
<i class="far fa-calendar-alt mr-1"></i>
|
||||
<script>document.write(new Date().toLocaleDateString('es-CO', {year:'numeric',month:'long',day:'numeric'}))</script>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<main class="p-8">
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
</div>
|
||||
{% else %}
|
||||
<main>
|
||||
{% block auth_content %}{% endblock %}
|
||||
</main>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function fetchJSON(url, options = {}) {
|
||||
const resp = await fetch(url, options);
|
||||
return await resp.json();
|
||||
}
|
||||
|
||||
function showToast(message, type = 'success') {
|
||||
const colors = {success: 'bg-green-500', error: 'bg-red-500', info: 'bg-blue-500', warning: 'bg-yellow-500'};
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `fixed top-4 right-4 z-50 ${colors[type]} text-white px-6 py-3 rounded-lg shadow-lg transition-all duration-500`;
|
||||
toast.textContent = message;
|
||||
document.body.appendChild(toast);
|
||||
setTimeout(() => { toast.style.opacity = '0'; setTimeout(() => toast.remove(), 500); }, 4000);
|
||||
}
|
||||
|
||||
function showLoading(btn) {
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i> Procesando...';
|
||||
}
|
||||
|
||||
function hideLoading(btn, text) {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = text;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,137 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Configuración{% endblock %}
|
||||
{% block header %}Configuración{% endblock %}
|
||||
{% block content %}
|
||||
<div class="max-w-3xl">
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h3 class="font-semibold text-gray-800"><i class="fas fa-database mr-2 text-blue-500"></i>Conexión Firebird</h3>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<form method="POST" action="/config/save" class="space-y-6">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Host</label>
|
||||
<input type="text" name="config_firebird_host"
|
||||
value="{{ configs|selectattr('key', 'equalto', 'firebird_host')|map(attribute='value')|first }}"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Puerto</label>
|
||||
<input type="text" name="config_firebird_port"
|
||||
value="{{ configs|selectattr('key', 'equalto', 'firebird_port')|map(attribute='value')|first }}"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 text-sm">
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Base de Datos (ruta)</label>
|
||||
<input type="text" name="config_firebird_database"
|
||||
value="{{ configs|selectattr('key', 'equalto', 'firebird_database')|map(attribute='value')|first }}"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 text-sm">
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Usuario</label>
|
||||
<input type="text" name="config_firebird_user"
|
||||
value="{{ configs|selectattr('key', 'equalto', 'firebird_user')|map(attribute='value')|first }}"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Contraseña</label>
|
||||
<input type="password" name="config_firebird_password"
|
||||
value="{{ configs|selectattr('key', 'equalto', 'firebird_password')|map(attribute='value')|first }}"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 text-sm">
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-3">
|
||||
<button type="button" onclick="testFirebird()"
|
||||
class="px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 text-sm font-medium">
|
||||
<i class="fas fa-plug mr-1"></i> Probar Conexión
|
||||
</button>
|
||||
<span id="fb-status" class="text-sm"></span>
|
||||
</div>
|
||||
|
||||
<hr class="border-gray-200">
|
||||
|
||||
<h4 class="font-medium text-gray-800"><i class="fas fa-cloud mr-2 text-blue-500"></i>API de Envío</h4>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">URL Base de la API</label>
|
||||
<input type="text" name="config_api_url"
|
||||
value="{{ configs|selectattr('key', 'equalto', 'api_url')|map(attribute='value')|first }}"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 text-sm"
|
||||
placeholder="https://api.ejemplo.com">
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Método HTTP</label>
|
||||
<select name="config_api_method"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 text-sm">
|
||||
<option value="POST" {% if (configs|selectattr('key', 'equalto', 'api_method')|map(attribute='value')|first) == 'POST' %}selected{% endif %}>POST</option>
|
||||
<option value="PUT" {% if (configs|selectattr('key', 'equalto', 'api_method')|map(attribute='value')|first) == 'PUT' %}selected{% endif %}>PUT</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Timeout (seg)</label>
|
||||
<input type="number" name="config_api_timeout"
|
||||
value="{{ configs|selectattr('key', 'equalto', 'api_timeout')|map(attribute='value')|first }}"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 text-sm">
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">API Key (Bearer token)</label>
|
||||
<input type="text" name="config_api_key"
|
||||
value="{{ configs|selectattr('key', 'equalto', 'api_key')|map(attribute='value')|first }}"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 text-sm">
|
||||
</div>
|
||||
|
||||
<hr class="border-gray-200">
|
||||
|
||||
<h4 class="font-medium text-gray-800"><i class="fas fa-building mr-2 text-blue-500"></i>Datos del Prestador</h4>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">NIT / Documento Obligado</label>
|
||||
<input type="text" name="config_num_documento_obligado"
|
||||
value="{{ configs|selectattr('key', 'equalto', 'num_documento_obligado')|map(attribute='value')|first }}"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Código Prestador</label>
|
||||
<input type="text" name="config_cod_prestador"
|
||||
value="{{ configs|selectattr('key', 'equalto', 'cod_prestador')|map(attribute='value')|first }}"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 text-sm">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit"
|
||||
class="px-6 py-2.5 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors">
|
||||
<i class="fas fa-save mr-2"></i> Guardar Configuración
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function testFirebird() {
|
||||
const btn = event.target;
|
||||
const status = document.getElementById('fb-status');
|
||||
btn.disabled = true;
|
||||
status.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Probando...';
|
||||
|
||||
const form = btn.closest('form');
|
||||
const data = new FormData();
|
||||
data.append('host', form.querySelector('[name="config_firebird_host"]').value);
|
||||
data.append('port', form.querySelector('[name="config_firebird_port"]').value);
|
||||
data.append('database', form.querySelector('[name="config_firebird_database"]').value);
|
||||
data.append('fb_user', form.querySelector('[name="config_firebird_user"]').value);
|
||||
data.append('fb_password', form.querySelector('[name="config_firebird_password"]').value);
|
||||
|
||||
const resp = await fetch('/terceros/test-connection', {method:'POST', body: data});
|
||||
const result = await resp.json();
|
||||
btn.disabled = false;
|
||||
status.innerHTML = result.success
|
||||
? '<span class="text-green-600"><i class="fas fa-check-circle"></i> Conexión exitosa</span>'
|
||||
: '<span class="text-red-600"><i class="fas fa-times-circle"></i> ' + result.message + '</span>';
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,139 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Dashboard{% endblock %}
|
||||
{% block header %}Dashboard{% endblock %}
|
||||
{% block content %}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
|
||||
<div class="flex items-center">
|
||||
<div class="w-12 h-12 bg-green-100 rounded-lg flex items-center justify-center">
|
||||
<i class="fas fa-check-circle text-green-600 text-xl"></i>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm text-gray-500 font-medium">Envíos Exitosos</p>
|
||||
<p class="text-2xl font-bold text-gray-800">{{ stats.total_exitosos or 0 }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
|
||||
<div class="flex items-center">
|
||||
<div class="w-12 h-12 bg-red-100 rounded-lg flex items-center justify-center">
|
||||
<i class="fas fa-times-circle text-red-600 text-xl"></i>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm text-gray-500 font-medium">Envíos Fallidos</p>
|
||||
<p class="text-2xl font-bold text-gray-800">{{ stats.total_errores or 0 }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
|
||||
<div class="flex items-center">
|
||||
<div class="w-12 h-12 bg-blue-100 rounded-lg flex items-center justify-center">
|
||||
<i class="fas fa-user text-blue-600 text-xl"></i>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm text-gray-500 font-medium">Terceros</p>
|
||||
<p class="text-2xl font-bold text-gray-800">{{ stats.total_terceros or 0 }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
|
||||
<div class="flex items-center">
|
||||
<div class="w-12 h-12 bg-purple-100 rounded-lg flex items-center justify-center">
|
||||
<i class="fas fa-file-invoice text-purple-600 text-xl"></i>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm text-gray-500 font-medium">Transacciones</p>
|
||||
<p class="text-2xl font-bold text-gray-800">{{ stats.total_transacciones or 0 }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h3 class="font-semibold text-gray-800"><i class="fas fa-history mr-2 text-blue-500"></i>Últimos Envíos</h3>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
{% if ultimos %}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="text-left text-gray-500 border-b border-gray-100">
|
||||
<th class="pb-3 font-medium">Tipo</th>
|
||||
<th class="pb-3 font-medium">Factura</th>
|
||||
<th class="pb-3 font-medium">Estado</th>
|
||||
<th class="pb-3 font-medium">Fecha</th>
|
||||
<th class="pb-3 font-medium">Usuario</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for e in ultimos %}
|
||||
<tr class="border-b border-gray-50 hover:bg-gray-50">
|
||||
<td class="py-3">
|
||||
<span class="px-2 py-1 rounded text-xs font-medium {% if e.tipo == 'terceros' %}bg-blue-100 text-blue-700{% else %}bg-purple-100 text-purple-700{% endif %}">
|
||||
{{ e.tipo }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="py-3 text-gray-600">{{ e.factura or '-' }}</td>
|
||||
<td class="py-3">
|
||||
<span class="px-2 py-1 rounded text-xs font-medium {% if e.status == 'success' %}bg-green-100 text-green-700{% else %}bg-red-100 text-red-700{% endif %}">
|
||||
{{ 'Exitoso' if e.status == 'success' else 'Error' }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="py-3 text-gray-500 text-xs">{{ e.created_at[:19] }}</td>
|
||||
<td class="py-3 text-gray-500">{{ e.username }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center py-8 text-gray-400">
|
||||
<i class="fas fa-inbox text-4xl mb-3 block"></i>
|
||||
<p>No hay envíos registrados</p>
|
||||
<p class="text-xs mt-1">Usa el menú lateral para comenzar</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h3 class="font-semibold text-gray-800"><i class="fas fa-rocket mr-2 text-blue-500"></i>Acciones Rápidas</h3>
|
||||
</div>
|
||||
<div class="p-6 space-y-4">
|
||||
<a href="/terceros" class="block p-4 bg-blue-50 rounded-xl hover:bg-blue-100 transition-colors">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-user text-blue-500 text-xl mr-4"></i>
|
||||
<div>
|
||||
<p class="font-medium text-gray-800">Enviar Terceros</p>
|
||||
<p class="text-xs text-gray-500">Genera y envía datos maestros de pacientes</p>
|
||||
</div>
|
||||
<i class="fas fa-chevron-right text-blue-400 ml-auto"></i>
|
||||
</div>
|
||||
</a>
|
||||
<a href="/transaccion" class="block p-4 bg-purple-50 rounded-xl hover:bg-purple-100 transition-colors">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-exchange-alt text-purple-500 text-xl mr-4"></i>
|
||||
<div>
|
||||
<p class="font-medium text-gray-800">Enviar Transacción RIPS</p>
|
||||
<p class="text-xs text-gray-500">Genera y envía procedimientos por factura</p>
|
||||
</div>
|
||||
<i class="fas fa-chevron-right text-purple-400 ml-auto"></i>
|
||||
</div>
|
||||
</a>
|
||||
<a href="/automation" class="block p-4 bg-green-50 rounded-xl hover:bg-green-100 transition-colors">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-robot text-green-500 text-xl mr-4"></i>
|
||||
<div>
|
||||
<p class="font-medium text-gray-800">Automatización Completa</p>
|
||||
<p class="text-xs text-gray-500">Paso 1 (terceros) + Paso 2 (transacción) automático</p>
|
||||
</div>
|
||||
<i class="fas fa-chevron-right text-green-400 ml-auto"></i>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,52 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Iniciar Sesión{% endblock %}
|
||||
{% block auth_content %}
|
||||
<div class="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-600 to-indigo-900">
|
||||
<div class="w-full max-w-md">
|
||||
<div class="bg-white rounded-2xl shadow-2xl p-8">
|
||||
<div class="text-center mb-8">
|
||||
<div class="inline-flex items-center justify-center w-16 h-16 bg-blue-100 rounded-full mb-4">
|
||||
<i class="fas fa-file-medical text-blue-600 text-2xl"></i>
|
||||
</div>
|
||||
<h2 class="text-2xl font-bold text-gray-800">RIPS Manager</h2>
|
||||
<p class="text-sm text-gray-500 mt-1">Gestión de envío de RIPS</p>
|
||||
</div>
|
||||
|
||||
{% if error %}
|
||||
<div class="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm flex items-center">
|
||||
<i class="fas fa-exclamation-circle mr-2"></i> {{ error }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="POST" action="/auth/login" class="space-y-5">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Usuario</label>
|
||||
<div class="relative">
|
||||
<i class="fas fa-user absolute left-3 top-3 text-gray-400"></i>
|
||||
<input type="text" name="username" required
|
||||
class="w-full pl-10 pr-3 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm"
|
||||
placeholder="Ingresa tu usuario">
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Contraseña</label>
|
||||
<div class="relative">
|
||||
<i class="fas fa-lock absolute left-3 top-3 text-gray-400"></i>
|
||||
<input type="password" name="password" required
|
||||
class="w-full pl-10 pr-3 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm"
|
||||
placeholder="Ingresa tu contraseña">
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit"
|
||||
class="w-full py-2.5 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors flex items-center justify-center">
|
||||
<i class="fas fa-sign-in-alt mr-2"></i> Iniciar Sesión
|
||||
</button>
|
||||
</form>
|
||||
<p class="mt-4 text-center text-sm text-gray-500">
|
||||
¿No tienes cuenta? <a href="/auth/register" class="text-blue-600 hover:underline font-medium">Regístrate</a>
|
||||
</p>
|
||||
</div>
|
||||
<p class="text-center text-white/70 text-xs mt-4">RIPS Manager v1.0 — Resolución 2275/2023</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,132 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Historial{% endblock %}
|
||||
{% block header %}Historial de Envíos{% endblock %}
|
||||
{% block content %}
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<form class="flex flex-wrap items-end gap-4">
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">Tipo</label>
|
||||
<select name="tipo" class="px-3 py-1.5 border border-gray-300 rounded-lg text-sm">
|
||||
<option value="">Todos</option>
|
||||
<option value="terceros" {{ 'selected' if filtro_tipo == 'terceros' }}>Terceros</option>
|
||||
<option value="transaccion" {{ 'selected' if filtro_tipo == 'transaccion' }}>Transacción</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">Estado</label>
|
||||
<select name="status" class="px-3 py-1.5 border border-gray-300 rounded-lg text-sm">
|
||||
<option value="">Todos</option>
|
||||
<option value="success" {{ 'selected' if filtro_status == 'success' }}>Exitoso</option>
|
||||
<option value="error" {{ 'selected' if filtro_status == 'error' }}>Error</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">Factura</label>
|
||||
<input type="text" name="factura" value="{{ filtro_factura }}"
|
||||
class="px-3 py-1.5 border border-gray-300 rounded-lg text-sm" placeholder="Buscar factura...">
|
||||
</div>
|
||||
<button type="submit" class="px-4 py-1.5 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700">
|
||||
<i class="fas fa-search mr-1"></i> Filtrar
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
{% if envios %}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="text-left text-gray-500 border-b border-gray-200">
|
||||
<th class="pb-3 font-medium">ID</th>
|
||||
<th class="pb-3 font-medium">Tipo</th>
|
||||
<th class="pb-3 font-medium">Factura</th>
|
||||
<th class="pb-3 font-medium">Estado</th>
|
||||
<th class="pb-3 font-medium">Pacientes</th>
|
||||
<th class="pb-3 font-medium">Servicios</th>
|
||||
<th class="pb-3 font-medium">Respuesta API</th>
|
||||
<th class="pb-3 font-medium">Fecha</th>
|
||||
<th class="pb-3 font-medium">Usuario</th>
|
||||
<th class="pb-3 font-medium"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for e in envios %}
|
||||
<tr class="border-b border-gray-50 hover:bg-gray-50">
|
||||
<td class="py-3 text-gray-600">{{ e.id }}</td>
|
||||
<td class="py-3">
|
||||
<span class="px-2 py-1 rounded text-xs font-medium {% if e.tipo == 'terceros' %}bg-blue-100 text-blue-700{% else %}bg-purple-100 text-purple-700{% endif %}">
|
||||
{{ e.tipo }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="py-3 text-gray-600 font-medium">{{ e.factura or '-' }}</td>
|
||||
<td class="py-3">
|
||||
<span class="px-2 py-1 rounded text-xs font-medium {% if e.status == 'success' %}bg-green-100 text-green-700{% else %}bg-red-100 text-red-700{% endif %}">
|
||||
{{ 'Exitoso' if e.status == 'success' else 'Error' }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="py-3 text-gray-600">{{ e.pacientes_count or 0 }}</td>
|
||||
<td class="py-3 text-gray-600">{{ e.servicios_count or 0 }}</td>
|
||||
<td class="py-3 text-gray-500 text-xs max-w-xs truncate">{{ e.respuesta_api[:80] if e.respuesta_api else '-' }}</td>
|
||||
<td class="py-3 text-gray-500 text-xs">{{ e.created_at[:19] }}</td>
|
||||
<td class="py-3 text-gray-500">{{ e.username }}</td>
|
||||
<td class="py-3">
|
||||
<button onclick='showDetail({{ e.id|tojson }}, {{ e.json_enviado|tojson if e.json_enviado else "null" }})'
|
||||
class="text-blue-600 hover:text-blue-800">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center py-12 text-gray-400">
|
||||
<i class="fas fa-inbox text-5xl mb-4 block"></i>
|
||||
<p>No hay registros con los filtros seleccionados</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal JSON -->
|
||||
<div id="json-modal" class="fixed inset-0 z-50 hidden">
|
||||
<div class="absolute inset-0 bg-black/50" onclick="closeModal()"></div>
|
||||
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full max-w-2xl bg-white rounded-xl shadow-2xl max-h-[80vh] overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-gray-200 flex justify-between items-center">
|
||||
<h3 class="font-semibold text-gray-800"><i class="fas fa-code mr-2 text-blue-500"></i>JSON Enviado</h3>
|
||||
<button onclick="closeModal()" class="text-gray-400 hover:text-gray-600"><i class="fas fa-times"></i></button>
|
||||
</div>
|
||||
<div class="p-6 overflow-y-auto max-h-[calc(80vh-80px)]">
|
||||
<pre id="modal-json" class="text-xs font-mono bg-gray-50 rounded-lg p-4 overflow-x-auto"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function showDetail(id, json) {
|
||||
if (!json) { showToast('No hay JSON disponible', 'info'); return; }
|
||||
const pre = document.getElementById('modal-json');
|
||||
try {
|
||||
const obj = typeof json === 'string' ? JSON.parse(json) : json;
|
||||
pre.innerHTML = syntaxHighlight(obj);
|
||||
} catch(e) {
|
||||
pre.textContent = json;
|
||||
}
|
||||
document.getElementById('json-modal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
document.getElementById('json-modal').classList.add('hidden');
|
||||
}
|
||||
|
||||
function syntaxHighlight(obj) {
|
||||
let json = JSON.stringify(obj, null, 2);
|
||||
json = json.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
return json.replace(/("(?:[^"\\]|\\.)*")(?=\s*:)/g, '<span class="text-blue-600">$1</span>')
|
||||
.replace(/:(\s*)("(?:[^"\\]|\\.)*")/g, ': $1<span class="text-green-600">$2</span>')
|
||||
.replace(/:(\s*)(\d+)/g, ': $1<span class="text-orange-600">$2</span>')
|
||||
.replace(/:(\s*)(null|true|false)/g, ': $1<span class="text-purple-600">$2</span>');
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,110 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Consultas SQL{% endblock %}
|
||||
{% block header %}Consultas SQL{% endblock %}
|
||||
{% block content %}
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<div class="lg:col-span-1">
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200">
|
||||
<div class="px-6 py-4 border-b border-gray-200 flex items-center justify-between">
|
||||
<h3 class="font-semibold text-gray-800"><i class="fas fa-list mr-2 text-blue-500"></i>Mis Consultas</h3>
|
||||
<button onclick="document.getElementById('modal-new-query').classList.remove('hidden')"
|
||||
class="text-blue-600 hover:text-blue-800"><i class="fas fa-plus"></i></button>
|
||||
</div>
|
||||
<div class="p-4 space-y-2">
|
||||
{% for q in queries %}
|
||||
<div class="p-3 rounded-lg border border-gray-200 hover:border-blue-300 cursor-pointer"
|
||||
onclick="editQuery({{ q.id }}, '{{ q.name }}', '{{ q.query_type }}', `{{ q.query_text|e }}`, `{{ q.description|e }}`)">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="font-medium text-sm text-gray-800">{{ q.name }}</span>
|
||||
<span class="px-2 py-0.5 rounded text-xs font-medium {% if q.query_type == 'terceros' %}bg-blue-100 text-blue-700{% else %}bg-purple-100 text-purple-700{% endif %}">
|
||||
{{ q.query_type }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 mt-1 truncate">{{ q.description or 'Sin descripción' }}</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="lg:col-span-2">
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h3 class="font-semibold text-gray-800"><i class="fas fa-code mr-2 text-blue-500"></i>Editor SQL</h3>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<form method="POST" action="/queries/create" class="space-y-4">
|
||||
<input type="hidden" name="query_id" id="query_id" value="">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Nombre</label>
|
||||
<input type="text" name="name" id="q_name" required
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm" placeholder="Nombre descriptivo">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Tipo</label>
|
||||
<select name="query_type" id="q_type"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm">
|
||||
<option value="terceros">Terceros (datos paciente)</option>
|
||||
<option value="transaccion">Transacción (procedimientos)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Consulta SQL</label>
|
||||
<textarea name="query_text" id="q_text" rows="10" required
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm font-mono bg-gray-50"
|
||||
placeholder="SELECT ... FROM ... WHERE ..."></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Descripción</label>
|
||||
<input type="text" name="description" id="q_desc"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm"
|
||||
placeholder="¿Qué hace esta consulta?">
|
||||
</div>
|
||||
<div class="flex space-x-3">
|
||||
<button type="submit"
|
||||
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm font-medium">
|
||||
<i class="fas fa-save mr-1"></i> Guardar
|
||||
</button>
|
||||
<button type="button" onclick="cancelEdit()"
|
||||
class="px-4 py-2 bg-gray-100 text-gray-700 rounded-lg text-sm font-medium hover:bg-gray-200 hidden" id="btn-cancel">
|
||||
<i class="fas fa-times mr-1"></i> Cancelar
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 bg-blue-50 rounded-xl border border-blue-200 p-4">
|
||||
<h4 class="text-sm font-medium text-blue-800"><i class="fas fa-info-circle mr-1"></i> Parámetros disponibles</h4>
|
||||
<p class="text-xs text-blue-600 mt-1">
|
||||
Usa <code class="bg-blue-100 px-1 rounded">:doc_num</code> para filtro por documento,
|
||||
<code class="bg-blue-100 px-1 rounded">:factura</code> para filtro por factura,
|
||||
<code class="bg-blue-100 px-1 rounded">:fecha_ini</code> y <code class="bg-blue-100 px-1 rounded">:fecha_fin</code> para rango de fechas.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function editQuery(id, name, type, text, desc) {
|
||||
document.getElementById('query_id').value = id;
|
||||
document.getElementById('q_name').value = name;
|
||||
document.getElementById('q_type').value = type;
|
||||
document.getElementById('q_text').value = text;
|
||||
document.getElementById('q_desc').value = desc;
|
||||
document.getElementById('btn-cancel').classList.remove('hidden');
|
||||
document.querySelector('form').action = '/queries/update/' + id;
|
||||
}
|
||||
|
||||
function cancelEdit() {
|
||||
document.getElementById('query_id').value = '';
|
||||
document.getElementById('q_name').value = '';
|
||||
document.getElementById('q_text').value = '';
|
||||
document.getElementById('q_desc').value = '';
|
||||
document.getElementById('btn-cancel').classList.add('hidden');
|
||||
document.querySelector('form').action = '/queries/create';
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,69 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Registro{% endblock %}
|
||||
{% block auth_content %}
|
||||
<div class="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-600 to-indigo-900">
|
||||
<div class="w-full max-w-md">
|
||||
<div class="bg-white rounded-2xl shadow-2xl p-8">
|
||||
<div class="text-center mb-8">
|
||||
<div class="inline-flex items-center justify-center w-16 h-16 bg-blue-100 rounded-full mb-4">
|
||||
<i class="fas fa-user-plus text-blue-600 text-2xl"></i>
|
||||
</div>
|
||||
<h2 class="text-2xl font-bold text-gray-800">Crear Cuenta</h2>
|
||||
<p class="text-sm text-gray-500 mt-1">Regístrate para gestionar RIPS</p>
|
||||
</div>
|
||||
|
||||
{% if error %}
|
||||
<div class="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm flex items-center">
|
||||
<i class="fas fa-exclamation-circle mr-2"></i> {{ error }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="POST" action="/auth/register" class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Usuario</label>
|
||||
<div class="relative">
|
||||
<i class="fas fa-user absolute left-3 top-3 text-gray-400"></i>
|
||||
<input type="text" name="username" required
|
||||
class="w-full pl-10 pr-3 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm"
|
||||
placeholder="Nombre de usuario">
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Email</label>
|
||||
<div class="relative">
|
||||
<i class="fas fa-envelope absolute left-3 top-3 text-gray-400"></i>
|
||||
<input type="email" name="email" required
|
||||
class="w-full pl-10 pr-3 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm"
|
||||
placeholder="correo@ejemplo.com">
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Contraseña</label>
|
||||
<div class="relative">
|
||||
<i class="fas fa-lock absolute left-3 top-3 text-gray-400"></i>
|
||||
<input type="password" name="password" required
|
||||
class="w-full pl-10 pr-3 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm"
|
||||
placeholder="Mínimo 6 caracteres">
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Confirmar Contraseña</label>
|
||||
<div class="relative">
|
||||
<i class="fas fa-lock absolute left-3 top-3 text-gray-400"></i>
|
||||
<input type="password" name="confirm_password" required
|
||||
class="w-full pl-10 pr-3 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm"
|
||||
placeholder="Repite la contraseña">
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit"
|
||||
class="w-full py-2.5 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors flex items-center justify-center">
|
||||
<i class="fas fa-user-plus mr-2"></i> Crear Cuenta
|
||||
</button>
|
||||
</form>
|
||||
<p class="mt-4 text-center text-sm text-gray-500">
|
||||
¿Ya tienes cuenta? <a href="/auth/login" class="text-blue-600 hover:underline font-medium">Inicia Sesión</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,130 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Terceros{% endblock %}
|
||||
{% block header %}Envío de Terceros{% endblock %}
|
||||
{% block content %}
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h3 class="font-semibold text-gray-800"><i class="fas fa-cog mr-2 text-blue-500"></i>Generar y Enviar</h3>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<form id="form-terceros" class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Consulta SQL</label>
|
||||
<select name="query_id" required
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm">
|
||||
<option value="">Seleccionar consulta...</option>
|
||||
{% for q in queries %}
|
||||
<option value="{{ q.id }}">{{ q.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Número de Documento <span class="text-xs text-gray-400">(opcional, según consulta)</span>
|
||||
</label>
|
||||
<input type="text" name="doc_num"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm"
|
||||
placeholder="Ej: 27765610">
|
||||
</div>
|
||||
<div class="flex space-x-3">
|
||||
<button type="button" onclick="previewTerceros()"
|
||||
class="px-4 py-2 bg-gray-100 text-gray-700 rounded-lg text-sm font-medium hover:bg-gray-200">
|
||||
<i class="fas fa-eye mr-1"></i> Vista Previa
|
||||
</button>
|
||||
<button type="button" onclick="sendTerceros()"
|
||||
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm font-medium">
|
||||
<i class="fas fa-paper-plane mr-1"></i> Enviar a API
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 bg-white rounded-xl shadow-sm border border-gray-200">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h3 class="font-semibold text-gray-800"><i class="fas fa-eye mr-2 text-blue-500"></i>JSON Generado</h3>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<pre id="json-preview" class="text-xs font-mono bg-gray-50 rounded-lg p-4 overflow-x-auto max-h-96 text-gray-600"><i class="fas fa-info-circle mr-1"></i> Haz clic en "Vista Previa" para ver el JSON</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h3 class="font-semibold text-gray-800"><i class="fas fa-history mr-2 text-blue-500"></i>Últimos Envíos</h3>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
{% if envios %}
|
||||
<div class="space-y-2">
|
||||
{% for e in envios %}
|
||||
<div class="p-3 rounded-lg border border-gray-100 text-sm">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="px-2 py-0.5 rounded text-xs font-medium {% if e.status == 'success' %}bg-green-100 text-green-700{% else %}bg-red-100 text-red-700{% endif %}">
|
||||
{{ 'Exitoso' if e.status == 'success' else 'Error' }}
|
||||
</span>
|
||||
<span class="text-xs text-gray-400">{{ e.created_at[:19] }}</span>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 mt-1 truncate">{{ e.respuesta_api[:100] if e.respuesta_api else 'Sin respuesta' }}</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-center text-gray-400 py-4 text-sm">No hay envíos de terceros aún</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function previewTerceros() {
|
||||
const form = document.getElementById('form-terceros');
|
||||
const data = new FormData(form);
|
||||
const btn = form.querySelector('[onclick="previewTerceros()"]');
|
||||
showLoading(btn);
|
||||
|
||||
const resp = await fetch('/terceros/preview', {method:'POST', body: data, credentials: 'include'});
|
||||
const result = await resp.json();
|
||||
hideLoading(btn, '<i class="fas fa-eye mr-1"></i> Vista Previa');
|
||||
|
||||
const pre = document.getElementById('json-preview');
|
||||
if (result.success && result.generated_json) {
|
||||
pre.innerHTML = syntaxHighlight(result.generated_json);
|
||||
} else {
|
||||
pre.innerHTML = '<span class="text-red-600">Error: ' + (result.message || 'Sin datos') + '</span>';
|
||||
}
|
||||
}
|
||||
|
||||
async function sendTerceros() {
|
||||
if (!confirm('¿Enviar terceros a la API?')) return;
|
||||
const form = document.getElementById('form-terceros');
|
||||
const data = new FormData(form);
|
||||
const btn = form.querySelector('[onclick="sendTerceros()"]');
|
||||
showLoading(btn);
|
||||
|
||||
const resp = await fetch('/terceros/send', {method:'POST', body: data, credentials: 'include'});
|
||||
const result = await resp.json();
|
||||
hideLoading(btn, '<i class="fas fa-paper-plane mr-1"></i> Enviar a API');
|
||||
|
||||
if (result.success) {
|
||||
showToast('Envío exitoso! CUV: ' + (result.cuv || ''), 'success');
|
||||
setTimeout(() => location.reload(), 1500);
|
||||
} else {
|
||||
showToast('Error: ' + result.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function syntaxHighlight(obj) {
|
||||
let json = JSON.stringify(obj, null, 2);
|
||||
json = json.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
return json.replace(/("(?:[^"\\]|\\.)*")(?=\s*:)/g, '<span class="text-blue-600">$1</span>')
|
||||
.replace(/:(\s*)("(?:[^"\\]|\\.)*")/g, ': $1<span class="text-green-600">$2</span>')
|
||||
.replace(/:(\s*)(\d+)/g, ': $1<span class="text-orange-600">$2</span>')
|
||||
.replace(/:(\s*)(null|true|false)/g, ': $1<span class="text-purple-600">$2</span>');
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,148 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Transacción RIPS{% endblock %}
|
||||
{% block header %}Envío de Transacción RIPS{% endblock %}
|
||||
{% block content %}
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h3 class="font-semibold text-gray-800"><i class="fas fa-cog mr-2 text-blue-500"></i>Generar y Enviar</h3>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<form id="form-transaccion" class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Consulta SQL</label>
|
||||
<select name="query_id" required
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm">
|
||||
<option value="">Seleccionar consulta...</option>
|
||||
{% for q in queries %}
|
||||
<option value="{{ q.id }}">{{ q.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Factura <span class="text-xs text-gray-400">(opcional)</span></label>
|
||||
<input type="text" name="factura"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm"
|
||||
placeholder="Ej: LHXC03404">
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Fecha Inicio</label>
|
||||
<input type="date" name="fecha_inicio" required
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Fecha Fin</label>
|
||||
<input type="date" name="fecha_fin" required
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm">
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex space-x-3">
|
||||
<button type="button" onclick="previewTransaccion()"
|
||||
class="px-4 py-2 bg-gray-100 text-gray-700 rounded-lg text-sm font-medium hover:bg-gray-200">
|
||||
<i class="fas fa-eye mr-1"></i> Vista Previa
|
||||
</button>
|
||||
<button type="button" onclick="sendTransaccion()"
|
||||
class="px-4 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-lg text-sm font-medium">
|
||||
<i class="fas fa-paper-plane mr-1"></i> Enviar a API
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 bg-white rounded-xl shadow-sm border border-gray-200">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h3 class="font-semibold text-gray-800"><i class="fas fa-eye mr-2 text-blue-500"></i>JSON Generado</h3>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div id="preview-info" class="text-xs text-gray-500 mb-2"></div>
|
||||
<pre id="json-preview" class="text-xs font-mono bg-gray-50 rounded-lg p-4 overflow-x-auto max-h-96 text-gray-600"><i class="fas fa-info-circle mr-1"></i> Haz clic en "Vista Previa" para ver el JSON</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h3 class="font-semibold text-gray-800"><i class="fas fa-history mr-2 text-blue-500"></i>Últimos Envíos</h3>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
{% if envios %}
|
||||
<div class="space-y-2">
|
||||
{% for e in envios %}
|
||||
<div class="p-3 rounded-lg border border-gray-100 text-sm">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="font-medium text-gray-700 text-xs">{{ e.factura or 'N/A' }}</span>
|
||||
<span class="px-2 py-0.5 rounded text-xs font-medium {% if e.status == 'success' %}bg-green-100 text-green-700{% else %}bg-red-100 text-red-700{% endif %}">
|
||||
{{ 'Exitoso' if e.status == 'success' else 'Error' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-xs text-gray-400 mt-1">
|
||||
<span>{{ e.pacientes_count or 0 }} pacientes / {{ e.servicios_count or 0 }} servicios</span>
|
||||
<span>{{ e.created_at[:19] }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-center text-gray-400 py-4 text-sm">No hay envíos de transacciones aún</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function previewTransaccion() {
|
||||
const form = document.getElementById('form-transaccion');
|
||||
const data = new FormData(form);
|
||||
const btn = form.querySelector('[onclick="previewTransaccion()"]');
|
||||
showLoading(btn);
|
||||
|
||||
const resp = await fetch('/transaccion/preview', {method:'POST', body: data, credentials: 'include'});
|
||||
const result = await resp.json();
|
||||
hideLoading(btn, '<i class="fas fa-eye mr-1"></i> Vista Previa');
|
||||
|
||||
document.getElementById('preview-info').innerHTML = result.success
|
||||
? `<span class="text-green-600"><i class="fas fa-check-circle"></i> ${result.rows_count} registros, ${result.grupos_count} grupos, ${result.total_json} JSON(s)</span>`
|
||||
: `<span class="text-red-600">Error: ${result.message}</span>`;
|
||||
|
||||
const pre = document.getElementById('json-preview');
|
||||
if (result.success && result.generated_json) {
|
||||
pre.innerHTML = syntaxHighlight(result.generated_json);
|
||||
} else {
|
||||
pre.innerHTML = '<span class="text-red-600">Error: ' + (result.message || 'Sin datos') + '</span>';
|
||||
}
|
||||
}
|
||||
|
||||
async function sendTransaccion() {
|
||||
if (!confirm('¿Enviar transacción(es) RIPS a la API?')) return;
|
||||
const form = document.getElementById('form-transaccion');
|
||||
const data = new FormData(form);
|
||||
const btn = form.querySelector('[onclick="sendTransaccion()"]');
|
||||
showLoading(btn);
|
||||
|
||||
const resp = await fetch('/transaccion/send', {method:'POST', body: data, credentials: 'include'});
|
||||
const result = await resp.json();
|
||||
hideLoading(btn, '<i class="fas fa-paper-plane mr-1"></i> Enviar a API');
|
||||
|
||||
if (result.success) {
|
||||
showToast(`Envío exitoso! ${result.total_enviados} transacciones`, 'success');
|
||||
setTimeout(() => location.reload(), 1500);
|
||||
} else {
|
||||
showToast(`Enviados: ${result.total_enviados}, Errores: ${result.total_errores}`, result.total_errores > 0 ? 'warning' : 'success');
|
||||
}
|
||||
}
|
||||
|
||||
function syntaxHighlight(obj) {
|
||||
let json = JSON.stringify(obj, null, 2);
|
||||
json = json.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
return json.replace(/("(?:[^"\\]|\\.)*")(?=\s*:)/g, '<span class="text-blue-600">$1</span>')
|
||||
.replace(/:(\s*)("(?:[^"\\]|\\.)*")/g, ': $1<span class="text-green-600">$2</span>')
|
||||
.replace(/:(\s*)(\d+)/g, ': $1<span class="text-orange-600">$2</span>')
|
||||
.replace(/:(\s*)(null|true|false)/g, ': $1<span class="text-purple-600">$2</span>');
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
python3 -m venv venv 2>/dev/null || python -m venv venv
|
||||
source venv/bin/activate 2>/dev/null || source venv/Scripts/activate
|
||||
pip install -q -r requirements.txt
|
||||
uvicorn main:app --host 0.0.0.0 --port 8080
|
||||
@@ -0,0 +1,95 @@
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
|
||||
from fastapi import FastAPI, Request, Depends
|
||||
from fastapi.responses import RedirectResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
import uvicorn
|
||||
|
||||
from app.database import init_db
|
||||
from app.auth import decode_token
|
||||
|
||||
app = FastAPI(title="RIPS Manager", version="1.0.0")
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
templates = Jinja2Templates(
|
||||
directory=os.path.join(os.path.dirname(__file__), "app", "templates")
|
||||
)
|
||||
app.state.templates = templates
|
||||
|
||||
|
||||
def get_user_from_request(request: Request):
|
||||
cookies = dict(request.cookies)
|
||||
token = cookies.get("token")
|
||||
if not token:
|
||||
auth = request.headers.get("Authorization", "")
|
||||
if auth.startswith("Bearer "):
|
||||
token = auth[7:]
|
||||
if token:
|
||||
decoded = decode_token(token)
|
||||
if decoded:
|
||||
return decoded
|
||||
print(f" [AUTH] Invalid token: {token[:30]}...", flush=True)
|
||||
return None
|
||||
|
||||
|
||||
@app.middleware("http")
|
||||
async def auth_middleware(request: Request, call_next):
|
||||
public_paths = ["/auth/login", "/auth/register", "/auth/api/login"]
|
||||
if request.url.path in public_paths or request.url.path.startswith("/static"):
|
||||
return await call_next(request)
|
||||
|
||||
if request.url.path.startswith("/auth"):
|
||||
return await call_next(request)
|
||||
|
||||
user = get_user_from_request(request)
|
||||
print(f" [AUTH] path={request.url.path} user={user['username'] if user else None} cookies={dict(request.cookies)}", flush=True)
|
||||
if not user:
|
||||
if request.url.path.startswith("/api/"):
|
||||
from fastapi.responses import JSONResponse
|
||||
print(f" [AUTH] -> 401 JSON for /api/ path", flush=True)
|
||||
return JSONResponse({"detail": "Not authenticated"}, status_code=401)
|
||||
print(f" [AUTH] -> 307 redirect to /auth/login", flush=True)
|
||||
return RedirectResponse(url="/auth/login")
|
||||
|
||||
request.state.user = user
|
||||
return await call_next(request)
|
||||
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup():
|
||||
init_db()
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return RedirectResponse(url="/dashboard")
|
||||
|
||||
|
||||
# Register routes
|
||||
from app.routes import auth, dashboard, config, queries, terceros, transaccion, logs, automation
|
||||
|
||||
app.include_router(auth.router)
|
||||
app.include_router(dashboard.router)
|
||||
app.include_router(config.router)
|
||||
app.include_router(queries.router)
|
||||
app.include_router(terceros.router)
|
||||
app.include_router(transaccion.router)
|
||||
app.include_router(logs.router)
|
||||
app.include_router(automation.router)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run("main:app", host="0.0.0.0", port=8080, reload=True)
|
||||
@@ -0,0 +1,9 @@
|
||||
fastapi==0.115.6
|
||||
uvicorn[standard]==0.34.0
|
||||
jinja2==3.1.5
|
||||
python-multipart==0.0.20
|
||||
aiofiles==24.1.0
|
||||
bcrypt==4.2.1
|
||||
python-jose[cryptography]==3.3.0
|
||||
httpx==0.28.1
|
||||
fdb>=2.0.0
|
||||
Binary file not shown.
@@ -0,0 +1,48 @@
|
||||
@echo off
|
||||
chcp 65001 >nul
|
||||
title RIPS Manager - Iniciando...
|
||||
|
||||
:: Verificar si ya existe Python portable
|
||||
if exist python\python.exe goto :start
|
||||
|
||||
echo ==========================================
|
||||
echo Descargando Python portable...
|
||||
echo ==========================================
|
||||
echo.
|
||||
|
||||
:: Descargar Python embeddable (64-bit)
|
||||
powershell -Command "& {Invoke-WebRequest -Uri 'https://www.python.org/ftp/python/3.12.9/python-3.12.9-embed-amd64.zip' -OutFile 'python.zip'}"
|
||||
|
||||
if not exist python.zip (
|
||||
echo ERROR: No se pudo descargar Python
|
||||
pause
|
||||
exit /b
|
||||
)
|
||||
|
||||
echo.
|
||||
echo Extrayendo...
|
||||
powershell -Command "& {Expand-Archive -Path 'python.zip' -DestinationPath 'python' -Force}"
|
||||
del python.zip
|
||||
|
||||
:: Habilitar pip (descomentar línea en el archivo de configuración)
|
||||
set PYTHON_EXE=%~dp0python\python.exe
|
||||
set PTH_FILE=%~dp0python\python312._pth
|
||||
powershell -Command "& {(Get-Content '%PTH_FILE%') -replace '#import site','import site' | Set-Content '%PTH_FILE%'}"
|
||||
|
||||
:: Instalar pip
|
||||
echo.
|
||||
echo Instalando pip...
|
||||
%PYTHON_EXE% -c "import urllib.request; exec(urllib.request.urlopen('https://bootstrap.pypa.io/get-pip.py').read())"
|
||||
|
||||
:start
|
||||
echo.
|
||||
echo Instalando dependencias...
|
||||
python\python.exe -m pip install -q -r requirements.txt 2>nul
|
||||
|
||||
echo.
|
||||
echo ==========================================
|
||||
echo Servidor iniciado en http://localhost:8080
|
||||
echo Presiona Ctrl+C para detener
|
||||
echo ==========================================
|
||||
python\python.exe main.py
|
||||
pause
|
||||
Reference in New Issue
Block a user