- automation: filtro contrato en SQL pacientes+RDA, numero LHXC en run() - transaccion: filtro contrato en preview/send-one/send - test-rda: filtro tipo_usuario (11/07/12), default contrato "12", acepta "12"/"012" - json_generator: detecta particular por CODCONTRATO "12"/"012" además de NIT_EMPRESA PART; fuerza tipousuario="12", codFormaPago="CLIP", plazoDias="0" Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
368 lines
18 KiB
HTML
368 lines
18 KiB
HTML
{% 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-3 gap-6">
|
|
|
|
<!-- Formulario -->
|
|
<div class="lg:col-span-1 space-y-4">
|
|
<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>Parámetros</h3>
|
|
</div>
|
|
<div class="p-5 space-y-4">
|
|
<div>
|
|
<label class="block text-xs font-medium text-gray-600 mb-1">Consulta SQL</label>
|
|
<select id="f-query" 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-xs font-medium text-gray-600 mb-1">Convenio <span class="text-gray-400">(vacío = todos)</span></label>
|
|
<input id="f-contrato" type="text" placeholder="ej: 011"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm">
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-medium text-gray-600 mb-1">Factura <span class="text-gray-400">(opcional)</span></label>
|
|
<input id="f-factura" type="text" placeholder="Ej: LHXC03404"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm">
|
|
</div>
|
|
<div class="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<label class="block text-xs font-medium text-gray-600 mb-1">Fecha inicio</label>
|
|
<input id="f-fi" type="date" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm">
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-medium text-gray-600 mb-1">Fecha fin</label>
|
|
<input id="f-ff" type="date" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm">
|
|
</div>
|
|
</div>
|
|
<button onclick="cargarRegistros()" id="btn-cargar"
|
|
class="w-full px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm font-medium">
|
|
<i class="fas fa-search mr-1"></i> Cargar registros
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Resumen -->
|
|
<div id="resumen-box" class="hidden bg-white rounded-xl shadow-sm border border-gray-200 p-5">
|
|
<div class="grid grid-cols-3 gap-2 text-center mb-4">
|
|
<div class="bg-gray-50 rounded-lg p-3">
|
|
<div id="res-total" class="text-xl font-bold text-gray-700">0</div>
|
|
<div class="text-xs text-gray-500">Total</div>
|
|
</div>
|
|
<div class="bg-green-50 rounded-lg p-3">
|
|
<div id="res-ok" class="text-xl font-bold text-green-600">0</div>
|
|
<div class="text-xs text-gray-500">Enviados</div>
|
|
</div>
|
|
<div class="bg-yellow-50 rounded-lg p-3">
|
|
<div id="res-pend" class="text-xl font-bold text-yellow-600">0</div>
|
|
<div class="text-xs text-gray-500">Pendientes</div>
|
|
</div>
|
|
</div>
|
|
<div id="res-err-wrap" class="hidden mb-3">
|
|
<div class="bg-red-50 rounded-lg p-3 text-center">
|
|
<div id="res-err" class="text-xl font-bold text-red-600">0</div>
|
|
<div class="text-xs text-gray-500">Con error</div>
|
|
</div>
|
|
</div>
|
|
<div class="flex gap-2">
|
|
<button onclick="enviarTodosPendientes()" id="btn-enviar-pend"
|
|
class="flex-1 px-3 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-lg text-xs font-medium">
|
|
<i class="fas fa-paper-plane mr-1"></i> Enviar pendientes
|
|
</button>
|
|
<button onclick="cargarRegistros()"
|
|
class="px-3 py-2 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg text-xs" title="Actualizar">
|
|
<i class="fas fa-sync"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tabla -->
|
|
<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 flex items-center justify-between">
|
|
<h3 class="font-semibold text-gray-800"><i class="fas fa-list mr-2 text-blue-500"></i>Registros</h3>
|
|
<span id="tabla-count" class="text-xs text-gray-400"></span>
|
|
</div>
|
|
<div id="tabla-wrap" class="p-6">
|
|
<div class="text-center py-12 text-gray-400 text-sm">
|
|
<i class="fas fa-table text-4xl block mb-3 opacity-30"></i>
|
|
Carga los registros para ver el estado de envío
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal JSON / detalle -->
|
|
<div id="modal-detalle" class="fixed inset-0 z-50 hidden">
|
|
<div class="absolute inset-0 bg-black/60" onclick="closeModal()"></div>
|
|
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full max-w-3xl bg-white rounded-xl shadow-2xl max-h-[85vh] overflow-hidden flex flex-col">
|
|
<div class="px-6 py-4 border-b border-gray-200 flex justify-between items-center shrink-0">
|
|
<h3 id="modal-titulo" class="font-semibold text-gray-800"><i class="fas fa-code mr-2 text-blue-500"></i>Detalle</h3>
|
|
<button onclick="closeModal()" class="text-gray-400 hover:text-gray-600"><i class="fas fa-times"></i></button>
|
|
</div>
|
|
<!-- Tabs -->
|
|
<div class="flex border-b border-gray-200 shrink-0 px-4">
|
|
<button onclick="showTab('tab-json')" id="t-json" class="tab-btn px-4 py-2 text-sm font-medium border-b-2 border-blue-500 text-blue-600 -mb-px">JSON enviado</button>
|
|
<button onclick="showTab('tab-tns')" id="t-tns" class="tab-btn px-4 py-2 text-sm font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700 -mb-px">Respuesta TNS</button>
|
|
</div>
|
|
<div class="overflow-y-auto flex-1 p-5">
|
|
<pre id="tab-json" class="tab-pane text-xs font-mono bg-gray-50 rounded-lg p-4 overflow-x-auto whitespace-pre-wrap"></pre>
|
|
<pre id="tab-tns" class="tab-pane hidden text-xs font-mono bg-yellow-50 rounded-lg p-4 overflow-x-auto whitespace-pre-wrap text-yellow-900"></pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
let allItems = [];
|
|
let formParams = {};
|
|
|
|
// Set today as default end date, start of month as default start
|
|
(function() {
|
|
const today = new Date();
|
|
const ymd = d => d.toISOString().slice(0, 10);
|
|
document.getElementById('f-ff').value = ymd(today);
|
|
const startOfMonth = new Date(today.getFullYear(), today.getMonth(), 1);
|
|
document.getElementById('f-fi').value = ymd(startOfMonth);
|
|
})();
|
|
|
|
function getParams() {
|
|
return {
|
|
query_id: document.getElementById('f-query').value,
|
|
contrato: document.getElementById('f-contrato').value.trim(),
|
|
factura: document.getElementById('f-factura').value,
|
|
fecha_inicio: document.getElementById('f-fi').value,
|
|
fecha_fin: document.getElementById('f-ff').value,
|
|
};
|
|
}
|
|
|
|
async function cargarRegistros() {
|
|
formParams = getParams();
|
|
if (!formParams.query_id) { showToast('Selecciona una consulta SQL', 'warning'); return; }
|
|
if (!formParams.fecha_inicio || !formParams.fecha_fin) { showToast('Ingresa las fechas', 'warning'); return; }
|
|
|
|
document.getElementById('tabla-wrap').innerHTML = `
|
|
<div class="text-center py-12 text-gray-400 text-sm">
|
|
<i class="fas fa-spinner fa-spin text-3xl block mb-3"></i>Consultando Firebird...
|
|
</div>`;
|
|
document.getElementById('resumen-box').classList.add('hidden');
|
|
|
|
const resp = await fetch('/transaccion/preview', {
|
|
method: 'POST', credentials: 'include',
|
|
body: new URLSearchParams(formParams),
|
|
});
|
|
const result = await resp.json();
|
|
|
|
if (!result.success) {
|
|
document.getElementById('tabla-wrap').innerHTML = `
|
|
<div class="text-center py-10 text-red-500 text-sm">
|
|
<i class="fas fa-exclamation-circle text-3xl block mb-2"></i>
|
|
<strong>Error:</strong> ${escHtml(result.message)}
|
|
</div>`;
|
|
return;
|
|
}
|
|
|
|
allItems = result.items;
|
|
renderTabla();
|
|
actualizarResumen();
|
|
document.getElementById('resumen-box').classList.remove('hidden');
|
|
}
|
|
|
|
function renderTabla() {
|
|
if (!allItems.length) {
|
|
document.getElementById('tabla-wrap').innerHTML = '<div class="text-center py-10 text-gray-400 text-sm">Sin registros para los parámetros indicados</div>';
|
|
return;
|
|
}
|
|
document.getElementById('tabla-count').textContent = `${allItems.length} registros`;
|
|
|
|
let html = `<div class="overflow-x-auto">
|
|
<table class="w-full text-xs">
|
|
<thead><tr class="text-left text-gray-400 border-b border-gray-200">
|
|
<th class="pb-2 font-medium pr-3">IDRECEP.</th>
|
|
<th class="pb-2 font-medium pr-3">Factura</th>
|
|
<th class="pb-2 font-medium pr-3">Paciente</th>
|
|
<th class="pb-2 font-medium pr-3">Contrato</th>
|
|
<th class="pb-2 font-medium pr-3">Exámenes</th>
|
|
<th class="pb-2 font-medium pr-3">Estado</th>
|
|
<th class="pb-2 font-medium"></th>
|
|
</tr></thead>
|
|
<tbody id="tbody-main">`;
|
|
|
|
for (const item of allItems) {
|
|
html += filaHtml(item);
|
|
}
|
|
html += '</tbody></table></div>';
|
|
document.getElementById('tabla-wrap').innerHTML = html;
|
|
}
|
|
|
|
function filaHtml(item) {
|
|
const id = item.idrecepcion;
|
|
const env = item.enviado;
|
|
const examsStr = Array.isArray(item.examenes) ? item.examenes.join(', ') : (item.examenes || '');
|
|
|
|
let badgeHtml, rowCls, accionHtml;
|
|
if (!env) {
|
|
badgeHtml = `<span class="px-2 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-700">Pendiente</span>`;
|
|
rowCls = '';
|
|
accionHtml = `<button onclick="enviarUno(${id})" title="Enviar" class="px-2 py-1 bg-purple-600 hover:bg-purple-700 text-white rounded text-xs"><i class="fas fa-paper-plane"></i></button>`;
|
|
} else if (env.status === 'success') {
|
|
const at = (env.at || '').slice(0, 16).replace('T', ' ');
|
|
badgeHtml = `<span class="px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-700" title="Enviado ${at}">✓ Enviado</span>`;
|
|
rowCls = 'bg-green-50/50 opacity-80';
|
|
accionHtml = '';
|
|
} else {
|
|
const tip = escAttr(env.mensaje || 'Error');
|
|
badgeHtml = `<span class="px-2 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-700" title="${tip}">✗ Error</span>`;
|
|
rowCls = 'bg-red-50/50';
|
|
accionHtml = `<button onclick="enviarUno(${id})" title="Reintentar" class="px-2 py-1 bg-orange-500 hover:bg-orange-600 text-white rounded text-xs"><i class="fas fa-redo"></i></button>`;
|
|
}
|
|
|
|
return `<tr id="row-${id}" class="border-b border-gray-50 hover:bg-gray-50/80 transition-colors ${rowCls}">
|
|
<td class="py-2 pr-3 font-mono text-gray-600">${id}</td>
|
|
<td class="py-2 pr-3 text-gray-600">${escHtml(String(item.factura || '-'))}</td>
|
|
<td class="py-2 pr-3 text-gray-600">${escHtml(String(item.paciente || '-'))}</td>
|
|
<td class="py-2 pr-3 text-gray-500">${escHtml(String(item.contrato || '-'))}</td>
|
|
<td class="py-2 pr-3 text-gray-500 max-w-[200px] truncate" title="${escAttr(examsStr)}">${escHtml(examsStr)}</td>
|
|
<td id="estado-${id}" class="py-2 pr-3">${badgeHtml}</td>
|
|
<td class="py-2 flex gap-1 items-center">
|
|
<button onclick="verDetalle(${id})" title="Ver JSON" class="px-2 py-1 bg-gray-100 hover:bg-gray-200 text-gray-600 rounded text-xs"><i class="fas fa-code"></i></button>
|
|
<span id="btnenv-${id}">${accionHtml}</span>
|
|
</td>
|
|
</tr>`;
|
|
}
|
|
|
|
function actualizarResumen() {
|
|
const total = allItems.length;
|
|
const ok = allItems.filter(i => i.enviado && i.enviado.status === 'success').length;
|
|
const pend = allItems.filter(i => !i.enviado).length;
|
|
const err = allItems.filter(i => i.enviado && i.enviado.status !== 'success').length;
|
|
document.getElementById('res-total').textContent = total;
|
|
document.getElementById('res-ok').textContent = ok;
|
|
document.getElementById('res-pend').textContent = pend;
|
|
document.getElementById('res-err').textContent = err;
|
|
document.getElementById('res-err-wrap').classList.toggle('hidden', err === 0);
|
|
}
|
|
|
|
function marcarFila(id, ok, mensaje, rawTns) {
|
|
const idx = allItems.findIndex(i => i.idrecepcion === id);
|
|
if (idx !== -1) {
|
|
allItems[idx].enviado = { status: ok ? 'success' : 'error', mensaje, at: new Date().toISOString() };
|
|
if (rawTns !== undefined) allItems[idx]._rawTns = rawTns;
|
|
}
|
|
const row = document.getElementById(`row-${id}`);
|
|
const estadoCell = document.getElementById(`estado-${id}`);
|
|
const btnWrap = document.getElementById(`btnenv-${id}`);
|
|
if (!row || !estadoCell) return;
|
|
|
|
if (ok) {
|
|
row.className = row.className.replace(/bg-red-50\/50|bg-green-50\/50/g, '') + ' bg-green-50/50 opacity-80';
|
|
estadoCell.innerHTML = `<span class="px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-700">✓ Enviado</span>`;
|
|
if (btnWrap) btnWrap.innerHTML = '';
|
|
} else {
|
|
row.className = row.className.replace(/bg-red-50\/50|bg-green-50\/50/g, '') + ' bg-red-50/50';
|
|
const tip = escAttr(mensaje || 'Error');
|
|
estadoCell.innerHTML = `<span class="px-2 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-700" title="${tip}">✗ Error</span>`;
|
|
if (btnWrap) btnWrap.innerHTML = `<button onclick="enviarUno(${id})" title="Reintentar" class="px-2 py-1 bg-orange-500 hover:bg-orange-600 text-white rounded text-xs"><i class="fas fa-redo"></i></button>`;
|
|
}
|
|
actualizarResumen();
|
|
}
|
|
|
|
async function enviarUno(id) {
|
|
const btnWrap = document.getElementById(`btnenv-${id}`);
|
|
if (btnWrap) btnWrap.innerHTML = '<span class="px-2 py-1 text-gray-400 text-xs"><i class="fas fa-spinner fa-spin"></i></span>';
|
|
|
|
const fd = new URLSearchParams({ ...formParams, idrecepcion: id });
|
|
let result;
|
|
try {
|
|
const resp = await fetch('/transaccion/send-one', { method: 'POST', credentials: 'include', body: fd });
|
|
result = await resp.json();
|
|
} catch (e) {
|
|
result = { success: false, message: String(e) };
|
|
}
|
|
|
|
marcarFila(id, result.success, result.message, result.raw_tns);
|
|
if (result.success) {
|
|
showToast(`IDRECEPCION ${id}: enviado`, 'success');
|
|
} else {
|
|
showToast(`IDRECEPCION ${id}: ${result.message}`, 'error');
|
|
}
|
|
}
|
|
|
|
async function enviarTodosPendientes() {
|
|
const pendientes = allItems.filter(i => !i.enviado);
|
|
if (!pendientes.length) { showToast('No hay registros pendientes', 'info'); return; }
|
|
if (!confirm(`¿Enviar ${pendientes.length} registros pendientes?`)) return;
|
|
|
|
document.getElementById('btn-enviar-pend').disabled = true;
|
|
document.getElementById('btn-enviar-pend').innerHTML = '<i class="fas fa-spinner fa-spin mr-1"></i> Enviando...';
|
|
|
|
let ok = 0, err = 0;
|
|
for (const item of pendientes) {
|
|
await enviarUno(item.idrecepcion);
|
|
if (allItems.find(i => i.idrecepcion === item.idrecepcion)?.enviado?.status === 'success') ok++; else err++;
|
|
}
|
|
|
|
document.getElementById('btn-enviar-pend').disabled = false;
|
|
document.getElementById('btn-enviar-pend').innerHTML = '<i class="fas fa-paper-plane mr-1"></i> Enviar pendientes';
|
|
showToast(`Completado: ${ok} enviados, ${err} errores`, err === 0 ? 'success' : 'warning');
|
|
}
|
|
|
|
function verDetalle(id) {
|
|
const item = allItems.find(i => i.idrecepcion === id);
|
|
if (!item) return;
|
|
document.getElementById('modal-titulo').innerHTML = `<i class="fas fa-code mr-2 text-blue-500"></i>IDRECEPCION ${id} — ${item.paciente || ''} — Factura ${item.factura || '-'}`;
|
|
const jsonPre = document.getElementById('tab-json');
|
|
jsonPre.innerHTML = syntaxHighlight(item.json || {});
|
|
const tnsPre = document.getElementById('tab-tns');
|
|
if (item.enviado) {
|
|
let tnsText = item._rawTns || item.enviado.mensaje || '(sin respuesta)';
|
|
try { tnsText = JSON.stringify(JSON.parse(tnsText), null, 2); } catch(e) {}
|
|
tnsPre.textContent = tnsText;
|
|
const tab = document.getElementById('t-tns');
|
|
tab.classList.add('text-yellow-700');
|
|
tab.classList.remove('text-gray-500');
|
|
} else {
|
|
tnsPre.textContent = '(aún no enviado)';
|
|
}
|
|
showTab('tab-json');
|
|
document.getElementById('modal-detalle').classList.remove('hidden');
|
|
}
|
|
|
|
function showTab(tabId) {
|
|
document.querySelectorAll('.tab-pane').forEach(p => p.classList.add('hidden'));
|
|
document.querySelectorAll('.tab-btn').forEach(b => {
|
|
b.classList.remove('border-blue-500', 'text-blue-600');
|
|
b.classList.add('border-transparent', 'text-gray-500');
|
|
});
|
|
document.getElementById(tabId).classList.remove('hidden');
|
|
const btnId = tabId === 'tab-json' ? 't-json' : 't-tns';
|
|
const btn = document.getElementById(btnId);
|
|
btn.classList.add('border-blue-500', 'text-blue-600');
|
|
btn.classList.remove('border-transparent', 'text-gray-500');
|
|
}
|
|
|
|
function closeModal() {
|
|
document.getElementById('modal-detalle').classList.add('hidden');
|
|
}
|
|
|
|
function escHtml(s) { return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
|
function escAttr(s) { return String(s).replace(/"/g,'"').replace(/'/g,'''); }
|
|
|
|
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-700">$2</span>')
|
|
.replace(/:(\s*)(\d+(?:\.\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 %}
|