Files
rips_manager/app/templates/logs.html
T
Lizandro GuarnizoandClaude Sonnet 4.6 6a452cd950 feat: sistema de 3 estados (success/warning/error) para envíos TNS
- _parse_tns_resp retorna 'success'|'warning'|'error' en lugar de bool
- 'ya existe/registrado/autorización' → warning (sin reenvío, no es error real)
- errores reales → error (con reenvío)
- Guarda JSON crudo completo en respuesta_api
- logs.html: badge amarillo para warning, verde para success, rojo para error
- Filtro de historial incluye opción 'warning'
- reenvío en logs.py solo disponible para status='error'
- reenvío en logs.py usa misma lógica _parse_tns_resp y soporta ventas

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-24 10:45:15 -05:00

315 lines
17 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block title %}Historial{% endblock %}
{% block header %}Historial de Envíos{% endblock %}
{% block content %}
<!-- Stats rápidos -->
{% if stats %}
<div class="flex flex-wrap gap-3 mb-5">
{% for s in stats %}
<div class="bg-white rounded-lg border border-gray-200 px-4 py-2.5 shadow-sm flex items-center gap-3">
<span class="px-2 py-0.5 rounded text-xs font-medium
{% if s.tipo == 'transaccion' %}bg-purple-100 text-purple-700
{% elif s.tipo == 'ventas' %}bg-emerald-100 text-emerald-700
{% else %}bg-blue-100 text-blue-700{% endif %}">{{ s.tipo }}</span>
<span class="{% if s.status == 'success' %}text-green-600{% elif s.status == 'warning' %}text-yellow-600{% else %}text-red-600{% endif %} font-bold text-sm">
{{ s.cnt }}
</span>
<span class="text-gray-400 text-xs">{{ 'enviados' if s.status == 'success' else ('ya existe' if s.status == 'warning' else 'errores') }}</span>
</div>
{% endfor %}
<div class="bg-white rounded-lg border border-gray-200 px-4 py-2.5 shadow-sm flex items-center gap-2 text-gray-500 text-sm">
<i class="fas fa-database text-gray-400 text-xs"></i> {{ total }} total
</div>
</div>
{% endif %}
<!-- Filtros -->
<div class="bg-white rounded-xl shadow-sm border border-gray-200 mb-5">
<form method="get" action="/logs" class="p-4">
<input type="hidden" name="offset" value="0">
<div class="flex flex-wrap items-end gap-3">
<div>
<label class="block text-xs font-medium text-gray-500 mb-1">Tipo</label>
<select name="tipo" class="px-3 py-1.5 border border-gray-300 rounded-lg text-sm min-w-[130px]">
<option value="">Todos los tipos</option>
<option value="terceros" {{ 'selected' if filtro_tipo == 'terceros' }}>Terceros</option>
<option value="transaccion" {{ 'selected' if filtro_tipo == 'transaccion' }}>Transacción</option>
<option value="ventas" {{ 'selected' if filtro_tipo == 'ventas' }}>Ventas</option>
</select>
</div>
<div>
<label class="block text-xs font-medium text-gray-500 mb-1">Estado</label>
<select name="status" class="px-3 py-1.5 border border-gray-300 rounded-lg text-sm min-w-[120px]">
<option value="">Todos</option>
<option value="success" {{ 'selected' if filtro_status == 'success' }}>✓ Enviado</option>
<option value="warning" {{ 'selected' if filtro_status == 'warning' }}>! Ya existe</option>
<option value="error" {{ 'selected' if filtro_status == 'error' }}>✗ Error</option>
</select>
</div>
<div>
<label class="block text-xs font-medium text-gray-500 mb-1">Desde</label>
<input type="date" name="fecha_desde" value="{{ filtro_fecha_desde }}"
class="px-3 py-1.5 border border-gray-300 rounded-lg text-sm">
</div>
<div>
<label class="block text-xs font-medium text-gray-500 mb-1">Hasta</label>
<input type="date" name="fecha_hasta" value="{{ filtro_fecha_hasta }}"
class="px-3 py-1.5 border border-gray-300 rounded-lg text-sm">
</div>
<div>
<label class="block text-xs font-medium text-gray-500 mb-1">Factura</label>
<input type="text" name="factura" value="{{ filtro_factura }}" placeholder="Nro. factura"
class="px-3 py-1.5 border border-gray-300 rounded-lg text-sm w-28">
</div>
<div>
<label class="block text-xs font-medium text-gray-500 mb-1">Cédula paciente</label>
<input type="text" name="paciente" value="{{ filtro_paciente }}" placeholder="Nro. documento"
class="px-3 py-1.5 border border-gray-300 rounded-lg text-sm w-36">
</div>
<div class="flex gap-2">
<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>
<a href="/logs" class="px-3 py-1.5 bg-gray-100 text-gray-600 rounded-lg text-sm hover:bg-gray-200" title="Limpiar filtros">
<i class="fas fa-times"></i>
</a>
</div>
</div>
</form>
</div>
<!-- Tabla -->
<div class="bg-white rounded-xl shadow-sm border border-gray-200">
<div class="px-6 py-3 border-b border-gray-100 flex items-center justify-between">
<span class="text-xs text-gray-500">
{% if total %}
Mostrando {{ offset + 1 }}{{ [offset + page_size, total]|min }} de {{ total }}
{% else %}Sin resultados{% endif %}
</span>
</div>
{% if envios %}
<div class="overflow-x-auto">
<table class="w-full text-xs">
<thead>
<tr class="text-left text-gray-400 border-b border-gray-200 bg-gray-50/60">
<th class="px-4 py-3 font-medium">#</th>
<th class="px-2 py-3 font-medium">Tipo</th>
<th class="px-2 py-3 font-medium">Factura</th>
<th class="px-2 py-3 font-medium">IDRECEP.</th>
<th class="px-2 py-3 font-medium">Contrato</th>
<th class="px-2 py-3 font-medium">Estado</th>
<th class="px-2 py-3 font-medium">Mensaje TNS</th>
<th class="px-2 py-3 font-medium">Fecha</th>
<th class="px-2 py-3 font-medium">Usuario</th>
<th class="px-2 py-3"></th>
</tr>
</thead>
<tbody>
{% for e in envios %}
<tr class="border-b border-gray-50 hover:bg-gray-50 transition-colors
{% if e.status == 'success' %}bg-green-50/25{% elif e.status == 'warning' %}bg-yellow-50/40{% elif e.status == 'error' %}bg-red-50/25{% endif %}">
<td class="px-4 py-2.5 text-gray-400 font-mono">{{ e.id }}</td>
<td class="px-2 py-2.5">
<span class="px-2 py-0.5 rounded text-xs font-medium
{% if e.tipo == 'terceros' %}bg-blue-100 text-blue-700
{% elif e.tipo == 'ventas' %}bg-emerald-100 text-emerald-700
{% else %}bg-purple-100 text-purple-700{% endif %}">
{{ e.tipo }}
</span>
</td>
<td class="px-2 py-2.5 text-gray-600 font-medium">{{ e.factura or '-' }}</td>
<td class="px-2 py-2.5 text-gray-500 font-mono">{{ e.idrecepcion or '-' }}</td>
<td class="px-2 py-2.5 text-gray-500">{{ e.contrato or '-' }}</td>
<td class="px-2 py-2.5">
{% if e.status == 'success' %}
<span class="px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-700">✓ Enviado</span>
{% elif e.status == 'warning' %}
<span class="px-2 py-0.5 rounded text-xs font-medium bg-yellow-100 text-yellow-700">! Ya existe</span>
{% else %}
<span class="px-2 py-0.5 rounded text-xs font-medium bg-red-100 text-red-700">✗ Error</span>
{% endif %}
</td>
<td class="px-2 py-2.5 text-gray-500">
{% if e.mensaje_tns %}
<span class="block max-w-[180px] truncate" title="{{ e.mensaje_tns }}">
{{ e.mensaje_tns[:55] }}{% if e.mensaje_tns|length > 55 %}…{% endif %}
</span>
{% else %}<span class="text-gray-300"></span>{% endif %}
</td>
<td class="px-2 py-2.5 text-gray-400 whitespace-nowrap">{{ e.created_at[:16] if e.created_at else '-' }}</td>
<td class="px-2 py-2.5 text-gray-400">{{ e.username or '-' }}</td>
<td class="px-2 py-2.5 flex items-center gap-1.5">
<button onclick="verDetalle({{ e.id }})"
class="px-2 py-1 bg-gray-100 hover:bg-gray-200 text-gray-600 rounded" title="Ver detalle">
<i class="fas fa-eye"></i>
</button>
{% if e.status == 'error' and e.json_enviado %}
<button onclick="reenviar({{ e.id }}, this)"
class="px-2 py-1 bg-orange-100 hover:bg-orange-200 text-orange-700 rounded" title="Reenviar a TNS">
<i class="fas fa-redo text-xs"></i>
</button>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Paginación -->
<div class="px-6 py-4 border-t border-gray-100 flex items-center justify-between">
<div class="flex gap-2">
{% if offset > 0 %}
<a href="/logs?tipo={{ filtro_tipo }}&status={{ filtro_status }}&factura={{ filtro_factura }}&paciente={{ filtro_paciente }}&fecha_desde={{ filtro_fecha_desde }}&fecha_hasta={{ filtro_fecha_hasta }}&offset={{ [0, offset - page_size]|max }}"
class="px-3 py-1.5 bg-gray-100 hover:bg-gray-200 text-gray-600 rounded-lg text-sm">
<i class="fas fa-chevron-left mr-1"></i> Anterior
</a>
{% endif %}
{% if has_more %}
<a href="/logs?tipo={{ filtro_tipo }}&status={{ filtro_status }}&factura={{ filtro_factura }}&paciente={{ filtro_paciente }}&fecha_desde={{ filtro_fecha_desde }}&fecha_hasta={{ filtro_fecha_hasta }}&offset={{ offset + page_size }}"
class="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm">
Siguiente <i class="fas fa-chevron-right ml-1"></i>
</a>
{% endif %}
</div>
<span class="text-xs text-gray-400">Pág. {{ (offset // page_size) + 1 }}</span>
</div>
{% else %}
<div class="text-center py-16 text-gray-400">
<i class="fas fa-inbox text-5xl mb-4 block opacity-30"></i>
<p class="text-sm">Sin registros con los filtros seleccionados</p>
<a href="/logs" class="mt-3 inline-block text-blue-600 hover:underline text-sm">Quitar filtros</a>
</div>
{% endif %}
</div>
<!-- Modal 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-[88vh] 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 text-sm"></h3>
<button onclick="closeModal()" class="text-gray-400 hover:text-gray-600 text-xl leading-none">×</button>
</div>
<div id="modal-info" class="px-6 py-2.5 bg-gray-50 border-b border-gray-200 shrink-0 text-xs text-gray-500 flex flex-wrap gap-4"></div>
<div class="flex border-b border-gray-200 shrink-0 px-4">
<button onclick="showTab('t-json')" id="btn-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('t-tns')" id="btn-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>
<button onclick="showTab('t-msg')" id="btn-t-msg"
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">
Mensaje</button>
</div>
<div class="overflow-y-auto flex-1 p-5">
<pre id="t-json" class="tab-pane text-xs font-mono bg-gray-50 rounded-lg p-4 overflow-x-auto whitespace-pre-wrap"></pre>
<pre id="t-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>
<pre id="t-msg" class="tab-pane hidden text-xs font-mono bg-blue-50 rounded-lg p-4 overflow-x-auto whitespace-pre-wrap text-blue-900"></pre>
</div>
</div>
</div>
<script>
async function reenviar(id, btn) {
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-spinner fa-spin text-xs"></i>';
try {
const resp = await fetch(`/logs/reenviar/${id}`, {method: 'POST', credentials: 'include'});
const data = await resp.json();
if (data.success) {
showToast('Reenvío exitoso', 'success');
const row = btn.closest('tr');
row.classList.remove('bg-red-50/25');
row.classList.add('bg-green-50/25');
const badge = row.querySelector('td:nth-child(6) span');
if (badge) {
badge.textContent = '✓ OK';
badge.className = 'px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-700';
}
btn.remove();
} else {
showToast(data.message || 'Error al reenviar', 'error');
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-redo text-xs"></i>';
}
} catch(e) {
showToast('Error de conexión', 'error');
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-redo text-xs"></i>';
}
}
async function verDetalle(id) {
const resp = await fetch(`/logs/detalle/${id}`, {credentials: 'include'});
const data = await resp.json();
if (data.error) { showToast(data.error, 'error'); return; }
const estadoLabel = data.status === 'success' ? '✓ Enviado' : data.status === 'warning' ? '! Ya existe' : '✗ Error';
document.getElementById('modal-titulo').innerHTML =
`<i class="fas fa-file-alt mr-2 text-blue-500"></i>Envío #${data.id} — <b>${data.tipo}</b> — <span class="${data.status === 'success' ? 'text-green-600' : data.status === 'warning' ? 'text-yellow-600' : 'text-red-600'}">${estadoLabel}</span>`;
const info = [
data.factura ? `<span><b>Factura:</b> ${data.factura}</span>` : '',
data.idrecepcion ? `<span><b>IDRECEPCION:</b> ${data.idrecepcion}</span>` : '',
data.contrato ? `<span><b>Contrato:</b> ${data.contrato}</span>` : '',
(data.fecha_inicio && data.fecha_fin) ? `<span><b>Período:</b> ${data.fecha_inicio}${data.fecha_fin}</span>` : '',
data.username ? `<span><b>Usuario:</b> ${data.username}</span>` : '',
data.created_at ? `<span><b>Enviado:</b> ${data.created_at.slice(0, 16)}</span>` : '',
].filter(Boolean).join('');
document.getElementById('modal-info').innerHTML = info;
const jsonPre = document.getElementById('t-json');
if (data.json_enviado) {
try { jsonPre.innerHTML = syntaxHighlight(JSON.parse(data.json_enviado)); }
catch(e) { jsonPre.textContent = data.json_enviado; }
} else {
jsonPre.textContent = '(sin JSON guardado)';
}
const tnsPre = document.getElementById('t-tns');
const raw = data.respuesta_api || '(sin respuesta)';
try { tnsPre.textContent = JSON.stringify(JSON.parse(raw), null, 2); }
catch(e) { tnsPre.textContent = raw; }
document.getElementById('t-msg').textContent = data.mensaje_tns || '(sin mensaje)';
showTab('t-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 btn = document.getElementById('btn-' + tabId);
if (btn) {
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 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-700">$2</span>')
.replace(/: (\d+(?:\.\d+)?)/g,': <span class="text-orange-600">$1</span>')
.replace(/: (null|true|false)/g,': <span class="text-purple-600">$1</span>');
}
</script>
{% endblock %}