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):
|
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)")}
|
cols = {r[1] for r in conn.execute("PRAGMA table_info(envios)")}
|
||||||
if "idrecepcion" not in cols:
|
if "idrecepcion" not in cols:
|
||||||
conn.execute("ALTER TABLE envios ADD COLUMN idrecepcion INTEGER")
|
conn.execute("ALTER TABLE envios ADD COLUMN idrecepcion INTEGER")
|
||||||
|
|||||||
@@ -14,22 +14,34 @@ from app.database import get_connection
|
|||||||
|
|
||||||
def guardar_sync_log(
|
def guardar_sync_log(
|
||||||
resultado: dict,
|
resultado: dict,
|
||||||
user_id: int,
|
user_id,
|
||||||
origen: str = "manual",
|
origen: str = "manual",
|
||||||
modo: str = "insertar",
|
modo: str = "insertar",
|
||||||
) -> None:
|
) -> None:
|
||||||
|
detalle_raw = resultado.get("detalle", [])
|
||||||
|
|
||||||
errores_det = None
|
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:
|
if errores:
|
||||||
errores_det = json.dumps(
|
errores_det = json.dumps(
|
||||||
[{"doc": e["doc"], "nombre": e["nombre"], "msg": e["message"]} for e in errores],
|
[{"doc": e["doc"], "nombre": e["nombre"], "msg": e["message"]} for e in errores],
|
||||||
ensure_ascii=False,
|
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 = get_connection()
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"""INSERT INTO sync_wa_log
|
"""INSERT INTO sync_wa_log
|
||||||
(user_id, origen, modo, total, created, skipped, updated, errores, errores_det)
|
(user_id, origen, modo, total, created, skipped, updated, errores, errores_det, detalle_json)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
||||||
(
|
(
|
||||||
user_id,
|
user_id,
|
||||||
origen,
|
origen,
|
||||||
@@ -40,6 +52,7 @@ def guardar_sync_log(
|
|||||||
resultado.get("updated", 0),
|
resultado.get("updated", 0),
|
||||||
resultado.get("errores", 0),
|
resultado.get("errores", 0),
|
||||||
errores_det,
|
errores_det,
|
||||||
|
detalle_json,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|||||||
@@ -158,6 +158,11 @@ function switchTab(name) {
|
|||||||
const _origenLabel = { manual: 'Manual', tercero: 'Tercero', scheduler: 'Scheduler', automation: 'Automatización' };
|
const _origenLabel = { manual: 'Manual', tercero: 'Tercero', scheduler: 'Scheduler', automation: 'Automatización' };
|
||||||
const _modoLabel = { insertar: 'Solo nuevos', upsert: 'Upsert' };
|
const _modoLabel = { insertar: 'Solo nuevos', upsert: 'Upsert' };
|
||||||
|
|
||||||
|
function toggleDet(id) {
|
||||||
|
const row = document.getElementById(id);
|
||||||
|
if (row) row.classList.toggle('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
async function ejecutarSyncNow() {
|
async function ejecutarSyncNow() {
|
||||||
const btn = document.getElementById('btn-sync-now');
|
const btn = document.getElementById('btn-sync-now');
|
||||||
const ventana = document.getElementById('sel-ventana').value;
|
const ventana = document.getElementById('sel-ventana').value;
|
||||||
@@ -235,34 +240,58 @@ async function cargarHistorialScheduler() {
|
|||||||
<thead>
|
<thead>
|
||||||
<tr class="text-left text-xs text-gray-500 border-b border-gray-100">
|
<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-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">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-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-green-600">Actualizados</th>
|
||||||
<th class="pb-2 pr-3 font-medium text-right text-red-500">Errores</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>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y divide-gray-50">
|
<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 fecha = r.created_at.replace('T', ' ').slice(0, 16);
|
||||||
const errDet = r.errores_det ? JSON.parse(r.errores_det) : [];
|
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 `
|
return `
|
||||||
<tr class="hover:bg-gray-50">
|
<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-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 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-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 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'}">
|
<td class="py-2 pr-3 text-right ${r.errores > 0 ? 'text-red-500 font-medium' : 'text-gray-300'}">
|
||||||
${r.errores > 0 && errDet.length ? `
|
${r.errores > 0 && errDet.length
|
||||||
<details>
|
? errDet.map(e => `<div class="text-red-600 text-xs">${e.doc} — ${e.msg}</div>`).join('')
|
||||||
<summary class="cursor-pointer">${r.errores}</summary>
|
: r.errores || '—'}
|
||||||
<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>
|
</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('')}
|
}).join('')}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
Reference in New Issue
Block a user