- Hub /envios/tns con 5 sub-tabs (Terceros, Transacción RIPS, Facturas Venta, Prueba RDA, Automatización) via iframe lazy - Hub /envios/erp con sub-tabs Pacientes y Sync Automático (historial scheduler) - Modo embed (?embed=1) en base.html para cargar páginas sin sidebar/header dentro de iframes - Sidebar simplificado: 6 ítems individuales reemplazados por módulos TNS y ERP Lab - Scheduler y endpoint /pacientes/examenes ahora incluyen médico ordenante (DOCIDMEDICO), empresa/EPS (NIT_EMPRESA) y valor total (VALORTOTAL) desde Firebird Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
199 lines
11 KiB
HTML
199 lines
11 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}ERP Lab{% endblock %}
|
|
{% block header %}<i class="fas fa-flask mr-2 text-green-400"></i> ERP Lab · Ximena Caicedo{% endblock %}
|
|
{% block content %}
|
|
<div class="-m-8 flex flex-col" style="height: calc(100vh - 64px)">
|
|
|
|
<!-- Barra de sub-tabs -->
|
|
<div class="bg-white border-b border-gray-200 px-4 flex-shrink-0">
|
|
<div class="flex">
|
|
<button id="btn-pacientes" onclick="switchTab('pacientes')"
|
|
class="tab-btn flex items-center gap-2 px-4 py-3 text-sm font-medium border-b-2 transition-colors whitespace-nowrap">
|
|
<i class="fas fa-users text-xs"></i> Pacientes
|
|
</button>
|
|
<button id="btn-scheduler" onclick="switchTab('scheduler')"
|
|
class="tab-btn flex items-center gap-2 px-4 py-3 text-sm font-medium border-b-2 transition-colors whitespace-nowrap">
|
|
<i class="fas fa-sync-alt text-xs"></i> Sync Automático
|
|
<span class="text-xs bg-green-100 text-green-700 px-1.5 py-0.5 rounded-full font-semibold">1 min</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Paneles -->
|
|
<div class="flex-1 relative overflow-hidden bg-gray-50">
|
|
|
|
<!-- Tab: Pacientes (iframe) -->
|
|
<div id="panel-pacientes" class="tab-panel absolute inset-0 hidden">
|
|
<iframe id="iframe-pacientes" class="w-full h-full border-0"></iframe>
|
|
</div>
|
|
|
|
<!-- Tab: Sync Automático (inline) -->
|
|
<div id="panel-scheduler" class="tab-panel absolute inset-0 hidden overflow-y-auto">
|
|
<div class="p-6 space-y-5 max-w-5xl mx-auto">
|
|
|
|
<!-- Estado del scheduler -->
|
|
<div class="bg-white rounded-xl shadow-sm border border-gray-200">
|
|
<div class="px-6 py-4 border-b border-gray-200 flex items-center justify-between">
|
|
<h3 class="font-semibold text-gray-800">
|
|
<i class="fas fa-robot mr-2 text-green-500"></i>Estado del Scheduler
|
|
</h3>
|
|
<span class="flex items-center gap-2 text-sm text-green-600 font-medium">
|
|
<span class="w-2 h-2 rounded-full bg-green-400 animate-pulse"></span>
|
|
Activo — cada 1 minuto
|
|
</span>
|
|
</div>
|
|
<div class="p-5 grid grid-cols-1 sm:grid-cols-3 gap-4 text-sm text-gray-600">
|
|
<div class="flex items-start gap-3">
|
|
<i class="fas fa-database text-blue-400 mt-0.5"></i>
|
|
<div>
|
|
<p class="font-medium text-gray-700">Fuente</p>
|
|
<p class="text-xs text-gray-500">Firebird · DBLAB_XIMENA_FB25</p>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-start gap-3">
|
|
<i class="fas fa-filter text-purple-400 mt-0.5"></i>
|
|
<div>
|
|
<p class="font-medium text-gray-700">Ventana de captura</p>
|
|
<p class="text-xs text-gray-500">Recepciones con HORAINICIORECEPCION en los últimos 2 minutos</p>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-start gap-3">
|
|
<i class="fas fa-paper-plane text-green-400 mt-0.5"></i>
|
|
<div>
|
|
<p class="font-medium text-gray-700">Destino</p>
|
|
<p class="text-xs text-gray-500">WhatsApp Lab · <code>ingest_paciente.php</code></p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="px-5 pb-4 border-t border-gray-100 pt-3">
|
|
<p class="text-xs text-gray-500 font-medium mb-2">Datos sincronizados por recepción:</p>
|
|
<div class="flex flex-wrap gap-2">
|
|
<span class="text-xs bg-blue-50 text-blue-700 px-2 py-1 rounded">Paciente (upsert)</span>
|
|
<span class="text-xs bg-purple-50 text-purple-700 px-2 py-1 rounded">Exámenes + CUPS</span>
|
|
<span class="text-xs bg-orange-50 text-orange-700 px-2 py-1 rounded">Diagnóstico CIE-10</span>
|
|
<span class="text-xs bg-green-50 text-green-700 px-2 py-1 rounded">Médico ordenante</span>
|
|
<span class="text-xs bg-teal-50 text-teal-700 px-2 py-1 rounded">Empresa / EPS</span>
|
|
<span class="text-xs bg-yellow-50 text-yellow-700 px-2 py-1 rounded">Valor total</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Historial de sync automático -->
|
|
<div class="bg-white rounded-xl shadow-sm border border-gray-200">
|
|
<div class="px-6 py-4 border-b border-gray-200 flex items-center justify-between">
|
|
<h3 class="font-semibold text-gray-800">
|
|
<i class="fas fa-history mr-2 text-gray-400"></i>Historial de sync automático
|
|
</h3>
|
|
<button onclick="cargarHistorialScheduler()"
|
|
class="text-sm text-blue-600 hover:underline flex items-center gap-1">
|
|
<i class="fas fa-refresh text-xs"></i> Actualizar
|
|
</button>
|
|
</div>
|
|
<div id="scheduler-hist" class="p-4">
|
|
<p class="text-sm text-gray-400 text-center py-4">Cargando…</p>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const _TAB_ACTIVE = 'border-green-500 text-green-700 bg-green-50';
|
|
const _TAB_INACTIVE = 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300';
|
|
|
|
let _schedulerLoaded = false;
|
|
|
|
function switchTab(name) {
|
|
document.querySelectorAll('.tab-panel').forEach(p => p.classList.add('hidden'));
|
|
document.querySelectorAll('.tab-btn').forEach(b => {
|
|
b.className = b.className.replace(_TAB_ACTIVE, '').replace(_TAB_INACTIVE, '').trim();
|
|
b.classList.add(..._TAB_INACTIVE.split(' '));
|
|
});
|
|
|
|
document.getElementById('panel-' + name).classList.remove('hidden');
|
|
const btn = document.getElementById('btn-' + name);
|
|
btn.className = btn.className.replace(_TAB_INACTIVE, '').trim();
|
|
btn.classList.add(..._TAB_ACTIVE.split(' '));
|
|
|
|
if (name === 'pacientes') {
|
|
const iframe = document.getElementById('iframe-pacientes');
|
|
if (!iframe.src) iframe.src = '/pacientes?embed=1';
|
|
}
|
|
if (name === 'scheduler' && !_schedulerLoaded) {
|
|
_schedulerLoaded = true;
|
|
cargarHistorialScheduler();
|
|
}
|
|
|
|
sessionStorage.setItem('erp-tab', name);
|
|
}
|
|
|
|
const _origenLabel = { manual: 'Manual', tercero: 'Tercero', scheduler: 'Scheduler', automation: 'Automatización' };
|
|
const _modoLabel = { insertar: 'Solo nuevos', upsert: 'Upsert' };
|
|
|
|
async function cargarHistorialScheduler() {
|
|
const el = document.getElementById('scheduler-hist');
|
|
el.innerHTML = '<p class="text-sm text-gray-400 text-center py-4">Cargando…</p>';
|
|
try {
|
|
const resp = await fetch('/pacientes/historial');
|
|
const rows = await resp.json();
|
|
const sched = rows.filter(r => r.origen === 'scheduler');
|
|
if (!sched.length) {
|
|
el.innerHTML = '<p class="text-sm text-gray-400 text-center py-6">Sin ejecuciones automáticas registradas aún.</p>';
|
|
return;
|
|
}
|
|
el.innerHTML = `
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full text-sm">
|
|
<thead>
|
|
<tr class="text-left text-xs text-gray-500 border-b border-gray-100">
|
|
<th class="pb-2 pr-4 font-medium">Fecha</th>
|
|
<th class="pb-2 pr-3 font-medium text-right">Procesados</th>
|
|
<th class="pb-2 pr-3 font-medium text-right text-blue-600">Nuevos</th>
|
|
<th class="pb-2 pr-3 font-medium text-right text-gray-400">Omitidos</th>
|
|
<th class="pb-2 pr-3 font-medium text-right text-green-600">Actualizados</th>
|
|
<th class="pb-2 pr-3 font-medium text-right text-red-500">Errores</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-gray-50">
|
|
${sched.map(r => {
|
|
const fecha = r.created_at.replace('T', ' ').slice(0, 16);
|
|
const errDet = r.errores_det ? JSON.parse(r.errores_det) : [];
|
|
return `
|
|
<tr class="hover:bg-gray-50">
|
|
<td class="py-2 pr-4 text-gray-500 whitespace-nowrap font-mono text-xs">${fecha}</td>
|
|
<td class="py-2 pr-3 text-right font-medium">${r.total}</td>
|
|
<td class="py-2 pr-3 text-right text-blue-600 font-medium">${r.created}</td>
|
|
<td class="py-2 pr-3 text-right text-gray-400">${r.skipped}</td>
|
|
<td class="py-2 pr-3 text-right text-green-600">${r.updated}</td>
|
|
<td class="py-2 pr-3 text-right ${r.errores > 0 ? 'text-red-500 font-medium' : 'text-gray-300'}">
|
|
${r.errores > 0 && errDet.length ? `
|
|
<details>
|
|
<summary class="cursor-pointer">${r.errores}</summary>
|
|
<div class="absolute z-10 bg-white border border-red-100 rounded-lg shadow-lg p-2 text-xs mt-1 max-w-xs">
|
|
${errDet.map(e => `<div class="text-red-600">${e.doc} — ${e.msg}</div>`).join('')}
|
|
</div>
|
|
</details>` : r.errores || '—'}
|
|
</td>
|
|
</tr>`;
|
|
}).join('')}
|
|
</tbody>
|
|
</table>
|
|
</div>`;
|
|
} catch (e) {
|
|
el.innerHTML = `<p class="text-sm text-red-500 text-center py-4">Error: ${e.message}</p>`;
|
|
}
|
|
}
|
|
|
|
(function init() {
|
|
document.querySelectorAll('.tab-btn').forEach(b => {
|
|
b.classList.add(..._TAB_INACTIVE.split(' '), 'border-b-2');
|
|
});
|
|
const saved = sessionStorage.getItem('erp-tab') || 'pacientes';
|
|
switchTab(saved);
|
|
})();
|
|
</script>
|
|
{% endblock %}
|