Initial commit: RIPS Manager web system

This commit is contained in:
Lizandro Guarnizo
2026-06-17 13:22:39 -05:00
commit 8ae3b44bc9
34 changed files with 3076 additions and 0 deletions
+163
View File
@@ -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 %}
+122
View File
@@ -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>
+137
View File
@@ -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 %}
+139
View File
@@ -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 %}
+52
View File
@@ -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 %}
+132
View File
@@ -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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
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 %}
+110
View File
@@ -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 %}
+69
View File
@@ -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 %}
+130
View File
@@ -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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
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 %}
+148
View File
@@ -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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
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 %}