Files
rips_manager/app/templates/resumen.html
T
Lizandro GuarnizoandClaude Sonnet 4.6 531fb29122 fix: group terceros by cedula not fecha-as-factura
For terceros, _guardar_envio stores the automation date in the factura
field (not a patient id), so COALESCE(factura, cedula) still collapsed
all terceros into one row. Now GROUP BY uses cedula for tipo=terceros
and factura for transaccion/ventas. Template hides the date in the
factura column for terceros rows.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-26 21:45:14 -05:00

275 lines
15 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 %}Resumen Diario{% endblock %}
{% block header %}Resumen Diario TNS{% endblock %}
{% block content %}
<!-- Stats -->
<div class="flex flex-wrap gap-3 mb-5">
<div class="bg-white rounded-lg border border-gray-200 px-4 py-2.5 shadow-sm flex items-center gap-3">
<i class="fas fa-layer-group text-gray-400 text-sm"></i>
<span class="font-bold text-sm text-gray-700">{{ total }}</span>
<span class="text-gray-400 text-xs">facturas procesadas</span>
</div>
<div class="bg-white rounded-lg border border-green-200 px-4 py-2.5 shadow-sm flex items-center gap-3">
<i class="fas fa-check-circle text-green-500 text-sm"></i>
<span class="font-bold text-sm text-green-600">{{ exitosos }}</span>
<span class="text-gray-400 text-xs">exitosas</span>
</div>
<div class="bg-white rounded-lg border border-red-200 px-4 py-2.5 shadow-sm flex items-center gap-3">
<i class="fas fa-times-circle text-red-500 text-sm"></i>
<span class="font-bold text-sm text-red-600">{{ errores }}</span>
<span class="text-gray-400 text-xs">error persistente</span>
</div>
</div>
<!-- Filtros -->
<div class="bg-white rounded-xl shadow-sm border border-gray-200 mb-5">
<form method="get" action="/logs/resumen" class="p-4">
<div class="flex flex-wrap items-end gap-3">
<div>
<label class="block text-xs font-medium text-gray-500 mb-1">Fecha</label>
<input type="date" name="fecha" value="{{ fecha }}"
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">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</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 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> Consultar
</button>
<a href="/logs/resumen" class="px-3 py-1.5 bg-gray-100 text-gray-600 rounded-lg text-sm hover:bg-gray-200" title="Hoy">
<i class="fas fa-calendar-day"></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 %}{{ total }} registro{{ 's' if total != 1 }} — {{ fecha }}{% else %}Sin registros para {{ fecha }}{% endif %}
</span>
<span class="text-xs text-gray-400">
<i class="fas fa-info-circle mr-1"></i>
Estado final: si hubo al menos un envío exitoso, se marca como exitoso
</span>
</div>
{% if rows %}
<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">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">Cédula</th>
<th class="px-2 py-3 font-medium text-center">Intentos</th>
<th class="px-2 py-3 font-medium">Estado final</th>
<th class="px-2 py-3 font-medium">Primer envío</th>
<th class="px-2 py-3 font-medium">Último envío</th>
<th class="px-2 py-3"></th>
</tr>
</thead>
<tbody>
{% for r in rows %}
<tr class="border-b border-gray-50 hover:bg-gray-50 transition-colors
{% if r.estado_final == 'success' %}bg-green-50/20
{% elif r.estado_final == 'warning' %}bg-yellow-50/30
{% else %}bg-red-50/20{% endif %}"
data-error-id="{{ r.ultimo_error_id or '' }}"
data-exitoso-id="{{ r.exitoso_id or '' }}">
<td class="px-4 py-2.5">
<span class="px-2 py-0.5 rounded text-xs font-medium
{% if r.tipo == 'terceros' %}bg-blue-100 text-blue-700
{% elif r.tipo == 'ventas' %}bg-emerald-100 text-emerald-700
{% else %}bg-purple-100 text-purple-700{% endif %}">
{{ r.tipo }}
</span>
</td>
<td class="px-2 py-2.5 text-gray-700 font-medium">{{ '—' if r.tipo == 'terceros' else (r.factura or '—') }}</td>
<td class="px-2 py-2.5 text-gray-500 font-mono">{{ r.idrecepcion or '—' }}</td>
<td class="px-2 py-2.5 text-gray-500">{{ r.contrato or '—' }}</td>
<td class="px-2 py-2.5 text-gray-500 font-mono">{{ r.cedula or '—' }}</td>
<td class="px-2 py-2.5 text-center">
<span class="px-1.5 py-0.5 rounded bg-gray-100 text-gray-600 font-mono">{{ r.intentos }}</span>
{% if r.exitos > 0 and r.intentos > r.exitos %}
<span class="text-gray-400 text-[10px] ml-0.5">({{ r.exitos }} ok)</span>
{% endif %}
</td>
<td class="px-2 py-2.5">
{% if r.estado_final == 'success' %}
<span class="px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-700">✓ Enviado</span>
{% elif r.estado_final == '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 persistente</span>
{% endif %}
</td>
<td class="px-2 py-2.5 text-gray-400 whitespace-nowrap">{{ r.primer_envio[:16] if r.primer_envio else '—' }}</td>
<td class="px-2 py-2.5 text-gray-400 whitespace-nowrap">{{ r.ultimo_envio[:16] if r.ultimo_envio else '—' }}</td>
<td class="px-2 py-2.5 flex items-center gap-1.5">
{% set ver_id = r.exitoso_id or r.ultimo_error_id %}
{% if ver_id %}
<button onclick="verDetalle({{ ver_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>
{% endif %}
{% if r.estado_final == 'error' and r.ultimo_error_id %}
<button onclick="reenviar({{ r.ultimo_error_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>
{% else %}
<div class="text-center py-16 text-gray-400">
<i class="fas fa-calendar-times text-5xl mb-4 block opacity-30"></i>
<p class="text-sm">Sin envíos registrados para {{ fecha }}</p>
</div>
{% endif %}
</div>
<!-- Modal detalle (idéntico al de logs.html) -->
<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/20');
row.classList.add('bg-green-50/20');
const badge = row.querySelector('td:nth-child(7) span');
if (badge) {
badge.textContent = '✓ Enviado';
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 %}