test-rda: add preview, ver más pagination, sent tracking, default contrato=011

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-06-26 19:32:53 -05:00
co-authored by Claude Sonnet 4.6
parent e477aa2bea
commit ef71a1db56
3 changed files with 233 additions and 47 deletions
+10
View File
@@ -39,6 +39,16 @@ def init_db():
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS rda_test_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
idrecepcion INTEGER NOT NULL,
factura TEXT,
contrato TEXT,
ok INTEGER NOT NULL DEFAULT 0,
mensaje TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS envios (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
+49 -4
View File
@@ -70,9 +70,21 @@ async def test_page(request: Request, user: dict = Depends(get_current_user)):
)
@router.get("/enviados")
async def get_enviados(request: Request, user: dict = Depends(get_current_user)):
db = get_connection()
rows = db.execute(
"SELECT idrecepcion, ok FROM rda_test_log ORDER BY created_at DESC"
).fetchall()
db.close()
enviados = {r["idrecepcion"]: bool(r["ok"]) for r in rows}
return JSONResponse({"enviados": enviados})
@router.get("/candidatos")
async def get_candidatos(request: Request, user: dict = Depends(get_current_user),
contrato: str = "", factura: str = "", articulos: str = ""):
contrato: str = "", factura: str = "", articulos: str = "",
offset: int = 0):
db = get_connection()
cfg = {r[0]: r[1] for r in db.execute("SELECT key, value FROM config").fetchall()}
db.close()
@@ -102,8 +114,8 @@ async def get_candidatos(request: Request, user: dict = Depends(get_current_user
filtros.append("r.NUM_FACTURA = :factura")
params["factura"] = int(factura)
sql_final = """
SELECT FIRST 10
sql_final = f"""
SELECT FIRST 10 SKIP {int(offset)}
r.IDRECEPCION, r.NUM_FACTURA, r.FECHA_RECEPCION, r.COD_PACIENTE, r.NIT_EMPRESA,
e.CODCONTRATO,
LIST(DISTINCT rel.COD_EXAMEN, ', ') AS EXAMENES,
@@ -111,7 +123,7 @@ async def get_candidatos(request: Request, user: dict = Depends(get_current_user
FROM RECEPCION r
JOIN RELACION rel ON rel.IDRECEPCION = r.IDRECEPCION
LEFT JOIN EMPRESA e ON e.NIT = r.NIT_EMPRESA
WHERE """ + " AND ".join(filtros) + """
WHERE {" AND ".join(filtros)}
GROUP BY r.IDRECEPCION, r.NUM_FACTURA, r.FECHA_RECEPCION, r.COD_PACIENTE, r.NIT_EMPRESA, e.CODCONTRATO
ORDER BY r.IDRECEPCION DESC
"""
@@ -138,6 +150,32 @@ async def get_candidatos(request: Request, user: dict = Depends(get_current_user
return JSONResponse({"candidatos": candidatos})
@router.get("/preview")
async def preview_rda(request: Request, user: dict = Depends(get_current_user),
idrecepcion: int = 0):
if not idrecepcion:
return JSONResponse({"error": "Falta idrecepcion"})
db = get_connection()
cfg = {r[0]: r[1] for r in db.execute("SELECT key, value FROM config").fetchall()}
db.close()
prof_def = cfg.get("profesional_default", "XIMENA")
esp_def = cfg.get("especialidad_default", "02")
remis_def = cfg.get("remisionante_default", "00")
prefijo_def = cfg.get("prefijo_tns_default", "00")
fb, fb_ok, fb_msg = get_firebird_from_config(cfg)
if not fb_ok:
return JSONResponse({"error": fb_msg})
ok2, err2, rows_rda = fb.execute_query(
_SQL_RDA_BY_WHERE.format(where="r.IDRECEPCION = :val"), {"val": idrecepcion}
)
fb.disconnect()
if not ok2 or not rows_rda:
return JSONResponse({"error": err2 or "Sin registros"})
grupos = agrupar_por_recepcion(rows_rda)
rda_json = generar_rda_paciente(list(grupos.values())[0], prof_def, esp_def, remis_def, prefijo_def)
return JSONResponse({"json": rda_json})
@router.post("/enviar")
async def test_enviar(request: Request, user: dict = Depends(get_current_user)):
form = await request.form()
@@ -259,6 +297,13 @@ async def test_enviar(request: Request, user: dict = Depends(get_current_user)):
"respuesta_tns": raw_resp,
"json_enviado": rda_json,
})
db_log = get_connection()
db_log.execute(
"INSERT OR REPLACE INTO rda_test_log (idrecepcion, factura, contrato, ok, mensaje) VALUES (?,?,?,?,?)",
(id_rec, rda_json.get("numero", ""), rda_json.get("numeroContrato", ""), 1 if ok_rda else 0, msg_rda)
)
db_log.commit()
db_log.close()
except Exception as ex:
pasos.append({
"tipo": "rda", "idrecepcion": id_rec, "ok": False,
+174 -43
View File
@@ -6,15 +6,15 @@
<!-- Filtros y candidatos -->
<div class="bg-white rounded-xl shadow-sm border border-gray-200">
<div class="px-6 py-4 border-b border-gray-200 flex items-center justify-between">
<div class="px-6 py-4 border-b border-gray-200 flex items-center justify-between flex-wrap gap-2">
<h3 class="font-semibold text-gray-800"><i class="fas fa-list mr-2 text-blue-500"></i>Candidatos disponibles</h3>
<div class="flex gap-2 items-center flex-wrap">
<input id="filtro-contrato" type="text" placeholder="Contrato (ej: 011)" value="011"
class="px-3 py-1.5 border border-gray-300 rounded-lg text-sm w-32">
<input id="filtro-articulos" type="text" placeholder="Artículos (ej: CH4,TSH,GLI)"
class="px-3 py-1.5 border border-gray-300 rounded-lg text-sm w-56"
title="Filtrar por artículos configurados en TNS. Vacío = lista predeterminada.">
<button onclick="cargarCandidatos()"
class="px-3 py-1.5 border border-gray-300 rounded-lg text-sm w-28">
<input id="filtro-articulos" type="text" placeholder="Artículos (vacío = lista TNS)"
class="px-3 py-1.5 border border-gray-300 rounded-lg text-sm w-52"
title="Vacío = lista predeterminada de artículos configurados en TNS.">
<button onclick="cargarCandidatos(true)"
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> Buscar
</button>
@@ -35,23 +35,47 @@
<th class="pb-2 pr-3">Paciente</th>
<th class="pb-2 pr-3">Contrato</th>
<th class="pb-2 pr-3">Exámenes</th>
<th class="pb-2"></th>
<th class="pb-2 text-right">Acciones</th>
</tr>
</thead>
<tbody id="tbody-candidatos" class="divide-y divide-gray-100"></tbody>
</table>
<div class="mt-3 text-center">
<button id="btn-ver-mas" onclick="verMas()"
class="hidden px-4 py-1.5 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg text-sm">
<i class="fas fa-chevron-down mr-1"></i> Ver más
</button>
</div>
</div>
<p id="sin-candidatos" class="hidden text-center py-6 text-gray-400 text-sm">No se encontraron registros con los filtros aplicados.</p>
</div>
</div>
<!-- Preview JSON -->
<div id="preview-box" class="hidden bg-white rounded-xl shadow-sm border border-blue-200">
<div class="px-6 py-4 border-b border-blue-100 flex items-center justify-between">
<h3 class="font-semibold text-gray-800"><i class="fas fa-eye mr-2 text-blue-500"></i>Preview — JSON a enviar <span id="preview-id" class="text-blue-600 font-mono text-sm"></span></h3>
<div class="flex gap-2">
<button onclick="enviarDesdePreview()"
class="px-4 py-1.5 bg-green-600 text-white rounded-lg text-sm hover:bg-green-700 font-medium">
<i class="fas fa-paper-plane mr-1"></i> Confirmar envío
</button>
<button onclick="document.getElementById('preview-box').classList.add('hidden')"
class="px-3 py-1.5 bg-gray-100 text-gray-600 rounded-lg text-sm hover:bg-gray-200">
Cancelar
</button>
</div>
</div>
<pre id="preview-json" class="p-4 text-xs overflow-x-auto whitespace-pre-wrap bg-gray-50 text-gray-800 rounded-b-xl"></pre>
</div>
<!-- Envío manual -->
<div class="bg-white rounded-xl shadow-sm border border-gray-200">
<div class="px-6 py-4 border-b border-gray-200">
<h3 class="font-semibold text-gray-800"><i class="fas fa-paper-plane mr-2 text-green-500"></i>Enviar RDA</h3>
<h3 class="font-semibold text-gray-800"><i class="fas fa-paper-plane mr-2 text-green-500"></i>Enviar RDA directo</h3>
</div>
<div class="p-6">
<div class="flex gap-3 items-end">
<div class="flex gap-3 items-end flex-wrap">
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">Tipo</label>
<select id="modo" class="px-3 py-2 border border-gray-300 rounded-lg text-sm">
@@ -64,6 +88,10 @@
<input id="valor" type="text" placeholder="ej: 314863"
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500">
</div>
<button onclick="previewManual()"
class="px-4 py-2 bg-blue-100 text-blue-700 rounded-lg text-sm hover:bg-blue-200 font-medium">
<i class="fas fa-eye mr-1"></i> Preview
</button>
<button onclick="enviarRDA()"
class="px-5 py-2 bg-green-600 text-white rounded-lg text-sm hover:bg-green-700 font-medium">
<i class="fas fa-paper-plane mr-1"></i> Enviar
@@ -84,14 +112,71 @@
</div>
<script>
async function cargarCandidatos() {
const contrato = document.getElementById('filtro-contrato').value.trim();
const articulos = document.getElementById('filtro-articulos').value.trim();
let _offset = 0;
let _enviados = {}; // { idrecepcion: true/false }
let _previewId = null;
async function cargarEnviados() {
const r = await fetch('/test-rda/enviados');
const d = await r.json();
_enviados = d.enviados || {};
}
function marcarFila(idrecepcion, ok) {
_enviados[idrecepcion] = ok;
const tr = document.getElementById('fila_' + idrecepcion);
if (!tr) return;
const btns = tr.querySelector('.acciones');
if (!btns) return;
if (ok) {
btns.innerHTML = '<span class="px-3 py-1 bg-green-100 text-green-700 rounded text-xs font-medium">✅ Enviado</span>';
} else {
btns.innerHTML = '<span class="px-3 py-1 bg-red-100 text-red-700 rounded text-xs font-medium">❌ Error</span>'
+ btns.innerHTML;
}
}
function renderFila(c) {
const ya = _enviados.hasOwnProperty(c.idrecepcion);
const ok = _enviados[c.idrecepcion];
let acciones = '';
if (ya && ok) {
acciones = '<span class="px-3 py-1 bg-green-100 text-green-700 rounded text-xs font-medium">✅ Enviado</span>';
} else {
acciones = `
<button onclick="mostrarPreview(${c.idrecepcion})"
class="px-2 py-1 bg-blue-100 text-blue-700 rounded text-xs hover:bg-blue-200 mr-1">
<i class="fas fa-eye"></i>
</button>
<button onclick="enviarDesde(${c.idrecepcion})"
class="px-3 py-1 bg-green-100 text-green-700 rounded text-xs hover:bg-green-200 font-medium">
<i class="fas fa-paper-plane mr-1"></i>Enviar
</button>`;
if (ya && !ok) {
acciones = '<span class="px-2 py-1 bg-red-100 text-red-700 rounded text-xs mr-1">❌</span>' + acciones;
}
}
return `<tr id="fila_${c.idrecepcion}" class="hover:bg-gray-50 ${ya && ok ? 'opacity-50' : ''}">
<td class="py-2 pr-3 font-mono text-blue-700 font-medium">${c.idrecepcion}</td>
<td class="py-2 pr-3 font-mono">${c.factura}</td>
<td class="py-2 pr-3 text-gray-500 text-xs">${c.fecha}</td>
<td class="py-2 pr-3 text-gray-600 text-xs">${c.paciente}</td>
<td class="py-2 pr-3"><span class="px-2 py-0.5 bg-blue-50 text-blue-700 rounded text-xs font-mono">${c.contrato}</span></td>
<td class="py-2 pr-3 text-gray-500 text-xs max-w-xs truncate" title="${c.examenes}">${c.examenes} <span class="text-gray-400">(${c.total})</span></td>
<td class="py-2 text-right acciones">${acciones}</td>
</tr>`;
}
async function cargarCandidatos(reset = false) {
if (reset) _offset = 0;
await cargarEnviados();
document.getElementById('cargando-candidatos').classList.remove('hidden');
document.getElementById('tabla-candidatos').classList.add('hidden');
document.getElementById('sin-candidatos').classList.add('hidden');
const params = new URLSearchParams();
const contrato = document.getElementById('filtro-contrato').value.trim();
const articulos = document.getElementById('filtro-articulos').value.trim();
const params = new URLSearchParams({ offset: _offset });
if (contrato) params.append('contrato', contrato);
if (articulos) params.append('articulos', articulos);
@@ -106,43 +191,86 @@ async function cargarCandidatos() {
}
const candidatos = data.candidatos || [];
if (!candidatos.length) {
if (!candidatos.length && _offset === 0) {
document.getElementById('sin-candidatos').classList.remove('hidden');
return;
}
const tbody = document.getElementById('tbody-candidatos');
tbody.innerHTML = '';
candidatos.forEach(c => {
const tr = document.createElement('tr');
tr.className = 'hover:bg-gray-50';
tr.innerHTML = `
<td class="py-2 pr-3 font-mono text-blue-700 font-medium">${c.idrecepcion}</td>
<td class="py-2 pr-3 font-mono">${c.factura}</td>
<td class="py-2 pr-3 text-gray-500">${c.fecha}</td>
<td class="py-2 pr-3 text-gray-600">${c.paciente}</td>
<td class="py-2 pr-3">
<span class="px-2 py-0.5 bg-blue-50 text-blue-700 rounded text-xs font-mono">${c.contrato}</span>
</td>
<td class="py-2 pr-3 text-gray-500 text-xs max-w-xs truncate" title="${c.examenes}">${c.examenes} <span class="text-gray-400">(${c.total})</span></td>
<td class="py-2">
<button onclick="enviarDesde(${c.idrecepcion})"
class="px-3 py-1 bg-green-100 text-green-700 rounded text-xs hover:bg-green-200 font-medium">
<i class="fas fa-paper-plane mr-1"></i>Enviar
</button>
</td>
`;
tbody.appendChild(tr);
});
if (_offset === 0) tbody.innerHTML = '';
candidatos.forEach(c => { tbody.innerHTML += renderFila(c); });
document.getElementById('tabla-candidatos').classList.remove('hidden');
const btnMas = document.getElementById('btn-ver-mas');
if (candidatos.length === 10) {
btnMas.classList.remove('hidden');
} else {
btnMas.classList.add('hidden');
}
}
async function verMas() {
_offset += 10;
await cargarEnviados();
const contrato = document.getElementById('filtro-contrato').value.trim();
const articulos = document.getElementById('filtro-articulos').value.trim();
const params = new URLSearchParams({ offset: _offset });
if (contrato) params.append('contrato', contrato);
if (articulos) params.append('articulos', articulos);
const resp = await fetch('/test-rda/candidatos?' + params.toString());
const data = await resp.json();
if (data.error || !data.candidatos?.length) {
document.getElementById('btn-ver-mas').classList.add('hidden');
return;
}
const tbody = document.getElementById('tbody-candidatos');
data.candidatos.forEach(c => { tbody.innerHTML += renderFila(c); });
if (data.candidatos.length < 10) document.getElementById('btn-ver-mas').classList.add('hidden');
}
async function mostrarPreview(idrecepcion) {
_previewId = idrecepcion;
document.getElementById('preview-id').textContent = '#' + idrecepcion;
document.getElementById('preview-json').textContent = 'Cargando...';
document.getElementById('preview-box').classList.remove('hidden');
document.getElementById('preview-box').scrollIntoView({ behavior: 'smooth' });
const resp = await fetch('/test-rda/preview?idrecepcion=' + idrecepcion);
const data = await resp.json();
if (data.error) {
document.getElementById('preview-json').textContent = 'Error: ' + data.error;
} else {
document.getElementById('preview-json').textContent = JSON.stringify(data.json, null, 2);
}
}
async function enviarDesdePreview() {
if (!_previewId) return;
document.getElementById('preview-box').classList.add('hidden');
document.getElementById('modo').value = 'idrecepcion';
document.getElementById('valor').value = _previewId;
await enviarRDA();
}
function enviarDesde(idrecepcion) {
_previewId = null;
document.getElementById('modo').value = 'idrecepcion';
document.getElementById('valor').value = idrecepcion;
enviarRDA();
}
async function previewManual() {
const valor = document.getElementById('valor').value.trim();
const modo = document.getElementById('modo').value;
if (!valor) { alert('Ingresa un número'); return; }
if (modo === 'idrecepcion') {
mostrarPreview(parseInt(valor));
} else {
alert('Preview solo disponible por IDRECEPCION');
}
}
async function enviarRDA() {
const modo = document.getElementById('modo').value;
const valor = document.getElementById('valor').value.trim();
@@ -154,6 +282,7 @@ async function enviarRDA() {
box.classList.remove('hidden');
contenido.innerHTML = '<div class="text-center py-8 text-gray-400"><i class="fas fa-spinner fa-spin text-3xl"></i><p class="mt-2">Enviando...</p></div>';
badges.innerHTML = '';
box.scrollIntoView({ behavior: 'smooth' });
const fd = new FormData();
fd.append('modo', modo);
@@ -180,16 +309,19 @@ async function enviarRDA() {
if (p.tipo === 'tercero') {
html += `
<div class="flex items-start gap-3 p-3 rounded-lg border ${p.ok ? 'border-green-100 bg-green-50' : 'border-red-100 bg-red-50'}">
<span class="text-lg">${icon}</span>
<div class="flex-1 min-w-0">
<span>${icon}</span>
<div>
<p class="text-sm font-medium text-gray-700">Tercero: <span class="font-mono">${p.codigo}</span> — ${p.nombre}</p>
<p class="text-xs text-gray-500 mt-0.5">${p.mensaje}</p>
<p class="text-xs text-gray-500">${p.mensaje}</p>
</div>
</div>`;
} else {
// Marcar fila como enviada
marcarFila(p.idrecepcion, p.ok);
const jsonStr = JSON.stringify(p.json_enviado || {}, null, 2);
const uid = 'json_' + p.idrecepcion;
const uid2 = 'resp_' + p.idrecepcion;
const uid = 'json_' + p.idrecepcion + '_' + Date.now();
const uid2 = 'resp_' + p.idrecepcion + '_' + Date.now();
let respTns = '';
try { respTns = JSON.stringify(JSON.parse(p.respuesta_tns || '{}'), null, 2); } catch(e) { respTns = p.respuesta_tns || ''; }
html += `
@@ -223,7 +355,6 @@ async function enviarRDA() {
contenido.innerHTML = html;
}
// Cargar candidatos al abrir la página
cargarCandidatos();
cargarCandidatos(true);
</script>
{% endblock %}