feat: store and display per-patient sync detail in historial
- Add detalle_json column to sync_wa_log via migration - guardar_sync_log() now stores doc, nombre, action, examenes count per patient - Historial table shows "Ver N" button that expands a row with patient list, action badge (Nuevo/Actualizado/Omitido) and exam count per patient Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
29b6aa4016
commit
e044f21e3e
@@ -14,6 +14,10 @@ def get_connection():
|
||||
|
||||
|
||||
def _migrate(conn):
|
||||
sync_cols = {r[1] for r in conn.execute("PRAGMA table_info(sync_wa_log)")}
|
||||
if "detalle_json" not in sync_cols:
|
||||
conn.execute("ALTER TABLE sync_wa_log ADD COLUMN detalle_json TEXT")
|
||||
|
||||
cols = {r[1] for r in conn.execute("PRAGMA table_info(envios)")}
|
||||
if "idrecepcion" not in cols:
|
||||
conn.execute("ALTER TABLE envios ADD COLUMN idrecepcion INTEGER")
|
||||
|
||||
@@ -14,22 +14,34 @@ from app.database import get_connection
|
||||
|
||||
def guardar_sync_log(
|
||||
resultado: dict,
|
||||
user_id: int,
|
||||
user_id,
|
||||
origen: str = "manual",
|
||||
modo: str = "insertar",
|
||||
) -> None:
|
||||
detalle_raw = resultado.get("detalle", [])
|
||||
|
||||
errores_det = None
|
||||
errores = [d for d in resultado.get("detalle", []) if not d.get("ok")]
|
||||
errores = [d for d in detalle_raw if not d.get("ok")]
|
||||
if errores:
|
||||
errores_det = json.dumps(
|
||||
[{"doc": e["doc"], "nombre": e["nombre"], "msg": e["message"]} for e in errores],
|
||||
ensure_ascii=False,
|
||||
)
|
||||
|
||||
detalle_json = None
|
||||
if detalle_raw:
|
||||
detalle_json = json.dumps(
|
||||
[{"doc": d["doc"], "nombre": d["nombre"], "action": d.get("action", ""),
|
||||
"examenes": d.get("examenes_guardados", 0)}
|
||||
for d in detalle_raw],
|
||||
ensure_ascii=False,
|
||||
)
|
||||
|
||||
conn = get_connection()
|
||||
conn.execute(
|
||||
"""INSERT INTO sync_wa_log
|
||||
(user_id, origen, modo, total, created, skipped, updated, errores, errores_det)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
||||
(user_id, origen, modo, total, created, skipped, updated, errores, errores_det, detalle_json)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
||||
(
|
||||
user_id,
|
||||
origen,
|
||||
@@ -40,6 +52,7 @@ def guardar_sync_log(
|
||||
resultado.get("updated", 0),
|
||||
resultado.get("errores", 0),
|
||||
errores_det,
|
||||
detalle_json,
|
||||
),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
@@ -158,6 +158,11 @@ function switchTab(name) {
|
||||
const _origenLabel = { manual: 'Manual', tercero: 'Tercero', scheduler: 'Scheduler', automation: 'Automatización' };
|
||||
const _modoLabel = { insertar: 'Solo nuevos', upsert: 'Upsert' };
|
||||
|
||||
function toggleDet(id) {
|
||||
const row = document.getElementById(id);
|
||||
if (row) row.classList.toggle('hidden');
|
||||
}
|
||||
|
||||
async function ejecutarSyncNow() {
|
||||
const btn = document.getElementById('btn-sync-now');
|
||||
const ventana = document.getElementById('sel-ventana').value;
|
||||
@@ -235,34 +240,58 @@ async function cargarHistorialScheduler() {
|
||||
<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">Usuario</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>
|
||||
<th class="pb-2 font-medium text-right">Detalle</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-50">
|
||||
${sched.map(r => {
|
||||
${sched.map((r, i) => {
|
||||
const fecha = r.created_at.replace('T', ' ').slice(0, 16);
|
||||
const errDet = r.errores_det ? JSON.parse(r.errores_det) : [];
|
||||
const pacientes = r.detalle_json ? JSON.parse(r.detalle_json) : [];
|
||||
const rowId = 'hist-det-' + i;
|
||||
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-xs text-gray-400">${r.username || '—'}</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 || '—'}
|
||||
${r.errores > 0 && errDet.length
|
||||
? errDet.map(e => `<div class="text-red-600 text-xs">${e.doc} — ${e.msg}</div>`).join('')
|
||||
: r.errores || '—'}
|
||||
</td>
|
||||
</tr>`;
|
||||
<td class="py-2 text-right">
|
||||
${pacientes.length ? `
|
||||
<button onclick="toggleDet('${rowId}')"
|
||||
class="text-xs text-blue-500 hover:underline">Ver ${pacientes.length}</button>` : '—'}
|
||||
</td>
|
||||
</tr>
|
||||
${pacientes.length ? `
|
||||
<tr id="${rowId}" class="hidden bg-gray-50">
|
||||
<td colspan="7" class="px-4 pb-3 pt-1">
|
||||
<div class="text-xs text-gray-500 space-y-0.5 max-h-48 overflow-y-auto">
|
||||
${pacientes.map(p => {
|
||||
const badge = p.action === 'created' ? '<span class="text-blue-600">Nuevo</span>'
|
||||
: p.action === 'updated' ? '<span class="text-green-600">Actualizado</span>'
|
||||
: p.action === 'skipped' ? '<span class="text-gray-400">Omitido</span>'
|
||||
: `<span class="text-red-500">${p.action}</span>`;
|
||||
const exBadge = p.examenes > 0
|
||||
? `<span class="text-purple-500">${p.examenes} exám.</span>` : '';
|
||||
return `<div class="flex items-center gap-3 py-0.5 border-b border-gray-100">
|
||||
<span class="text-gray-400 w-28 shrink-0">${p.doc}</span>
|
||||
<span class="flex-1 truncate">${p.nombre}</span>
|
||||
${badge} ${exBadge}
|
||||
</div>`;
|
||||
}).join('')}
|
||||
</div>
|
||||
</td>
|
||||
</tr>` : ''}`;
|
||||
}).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
Reference in New Issue
Block a user