Files
donna/views/escrituras.html
T
Soporte ZH Consultores 4ee32aec6d Initial commit — Donna portal v1
Setup completo del portal interno donna.aal-law.com:

* Landing público con OAuth login + grid de apps autenticado
* Flujo OAuth2 contra Mailcow (mail.aal-law.com)
* Sesiones SSO compartidas en el dominio (cookie donna_session, 8h)
* Whitelist de dominios aal-law.com / aalnlaw.com
* App "Control de Escrituras" migrada desde ZH-Consultores/control-escrituras
  (mismo schema, mismo frontend, ahora con shared Auth/Db/Config libs)
* Migraciones SQL para users, escrituras_tramites, escrituras_historial
* Caddy on :80 con rewrite extless → .php
* Backup diario MariaDB (systemd timer + script) con retención 14d
* Documentación de arquitectura en README + infra files en docs/infra
2026-05-11 19:10:36 +00:00

1093 lines
46 KiB
HTML

<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Control de Escrituras — AALN Law</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display&family=DM+Sans:wght@300;400;500;600&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--navy: #1B3A6B;
--navy-d: #112446;
--navy-l: #2D5090;
--gold: #C9A84C;
--gold-l: #E8C96A;
--gold-d: #9E7C2E;
--white: #FFFFFF;
--off: #F7F6F3;
--gray-100: #EEEDE9;
--gray-200: #D6D4CC;
--gray-400: #9B9A96;
--gray-700: #4A4946;
--gray-900: #1A1916;
--s-solicitado: #F1EFE8; --st-solicitado: #5F5E5A;
--s-recibido: #E6F1FB; --st-recibido: #0C447C;
--s-proceso: #FAEEDA; --st-proceso: #7A4F10;
--s-protocolizado: #EAF3DE; --st-protocolizado: #2E5E0E;
--s-registro: #E1F5EE; --st-registro: #085041;
--s-inscrito: #EEEDFE; --st-inscrito: #3C3489;
--radius: 10px;
--shadow: 0 1px 3px rgba(0,0,0,0.06), 0 4px 16px rgba(0,0,0,0.06);
}
body { font-family:'DM Sans',sans-serif; background:var(--off); color:var(--gray-900); font-size:14px; min-height:100vh; }
/* HEADER */
header {
background:var(--navy); border-bottom:3px solid var(--gold);
padding:0 24px; display:flex; align-items:center; justify-content:space-between;
height:58px; position:sticky; top:0; z-index:200;
}
.logo { display:flex; align-items:center; gap:14px; }
.logo-mark {
width:32px; height:32px; background:var(--gold); border-radius:6px;
display:flex; align-items:center; justify-content:center;
font-family:'DM Serif Display',serif; font-size:15px; color:var(--navy); flex-shrink:0;
}
.logo-text { display:flex; flex-direction:column; line-height:1.2; }
.logo-firm { font-size:11px; font-weight:600; color:var(--gold); letter-spacing:.08em; text-transform:uppercase; }
.logo-tool { font-size:13px; font-weight:400; color:rgba(255,255,255,0.85); }
.header-right { display:flex; align-items:center; gap:16px; }
.sync-status { display:flex; align-items:center; gap:6px; font-size:11px; color:rgba(255,255,255,0.55); }
.sync-dot { width:7px; height:7px; border-radius:50%; background:#4CAF50; animation:pulse 2.5s ease-in-out infinite; }
.sync-dot.syncing { background:var(--gold); }
.sync-dot.error { background:#ef5350; animation:none; }
@keyframes pulse { 0%,100%{opacity:1}50%{opacity:0.4} }
@keyframes spin { from{transform:rotate(0deg)}to{transform:rotate(360deg)} }
/* USER CHIP */
.user-chip {
display:flex; align-items:center; gap:8px;
background:rgba(255,255,255,0.1); border:1px solid rgba(255,255,255,0.15);
border-radius:20px; padding:5px 12px 5px 6px;
}
.user-avatar {
width:24px; height:24px; border-radius:50%;
background:var(--gold); color:var(--navy);
display:flex; align-items:center; justify-content:center;
font-size:11px; font-weight:700; flex-shrink:0;
}
.user-name { font-size:12px; color:rgba(255,255,255,0.85); max-width:160px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.btn-logout {
font-size:11px; padding:4px 10px;
border:1px solid rgba(255,255,255,0.2); border-radius:6px;
background:transparent; color:rgba(255,255,255,0.6);
cursor:pointer; font-family:'DM Sans',sans-serif;
transition:all .15s;
}
.btn-logout:hover { background:rgba(255,255,255,0.1); color:#fff; }
/* TOOLBAR */
.toolbar {
background:var(--white); border-bottom:1px solid var(--gray-100);
padding:12px 24px; display:flex; align-items:center; gap:10px; flex-wrap:wrap;
box-shadow:var(--shadow);
}
.search-wrap { position:relative; flex:1; min-width:200px; max-width:320px; }
.search-wrap svg { position:absolute; left:10px; top:50%; transform:translateY(-50%); width:15px; height:15px; color:var(--gray-400); pointer-events:none; }
.search-wrap input {
width:100%; padding:7px 10px 7px 32px;
border:1px solid var(--gray-200); border-radius:var(--radius);
font-family:'DM Sans',sans-serif; font-size:13px;
background:var(--off); color:var(--gray-900); transition:border-color .15s;
}
.search-wrap input:focus { outline:none; border-color:var(--navy-l); background:var(--white); }
.filter-group { display:flex; align-items:center; gap:6px; }
.filter-label { font-size:11px; font-weight:600; color:var(--gray-400); text-transform:uppercase; letter-spacing:.06em; white-space:nowrap; }
select.flt {
font-family:'DM Sans',sans-serif; font-size:12px; padding:6px 10px;
border:1px solid var(--gray-200); border-radius:var(--radius);
background:var(--off); color:var(--gray-700); cursor:pointer; transition:border-color .15s;
}
select.flt:focus { outline:none; border-color:var(--navy-l); }
.btn-new {
margin-left:auto; display:flex; align-items:center; gap:7px;
padding:8px 18px; background:var(--navy); color:var(--white);
border:none; border-radius:var(--radius);
font-family:'DM Sans',sans-serif; font-size:13px; font-weight:500;
cursor:pointer; transition:background .15s, transform .1s; white-space:nowrap;
}
.btn-new:hover { background:var(--navy-d); }
.btn-new:active { transform:scale(0.98); }
.btn-new svg { width:14px; height:14px; }
/* KPI STRIP */
.kpi-strip { display:flex; gap:10px; padding:16px 24px 0; flex-wrap:wrap; }
.kpi { background:var(--white); border:1px solid var(--gray-100); border-radius:var(--radius); padding:14px 20px; flex:1; min-width:110px; box-shadow:var(--shadow); }
.kpi-label { font-size:10px; font-weight:600; color:var(--gray-400); text-transform:uppercase; letter-spacing:.07em; margin-bottom:4px; }
.kpi-val { font-family:'DM Serif Display',serif; font-size:28px; color:var(--navy); line-height:1; }
.kpi-val.gold { color:var(--gold-d); }
.kpi-val.purple { color:var(--st-inscrito); }
/* TABLE */
.main { padding:16px 24px 40px; }
.table-card { background:var(--white); border:1px solid var(--gray-100); border-radius:var(--radius); box-shadow:var(--shadow); overflow:hidden; }
.table-wrap { overflow-x:auto; }
table { width:100%; border-collapse:collapse; min-width:1400px; }
thead tr { background:var(--navy); }
th {
padding:11px 12px; text-align:left; font-size:10.5px; font-weight:600;
color:rgba(255,255,255,0.7); letter-spacing:.07em; text-transform:uppercase;
white-space:nowrap; border-right:1px solid rgba(255,255,255,0.06);
}
th:last-child { border-right:none; }
td { padding:9px 12px; border-bottom:1px solid var(--gray-100); vertical-align:middle; color:var(--gray-700); font-size:13px; }
tr:last-child td { border-bottom:none; }
tr.data-row:hover td { background:#F4F6FB; }
tr.data-row.inscrito-row td { background:#F7FBF4; }
tr.data-row.inscrito-row:hover td { background:#EEF7E8; }
.editable {
background:transparent; border:1px solid transparent; border-radius:6px;
padding:4px 7px; font-family:'DM Sans',sans-serif; font-size:13px; color:var(--gray-700);
width:100%; transition:border-color .15s, background .15s;
}
.editable:focus { outline:none; border-color:var(--navy-l); background:var(--white); box-shadow:0 0 0 3px rgba(27,58,107,0.08); }
textarea.editable { resize:none; min-height:36px; line-height:1.4; }
select.editable { cursor:pointer; appearance:none; }
.badge { display:inline-block; font-size:11px; font-weight:600; padding:3px 10px; border-radius:20px; white-space:nowrap; letter-spacing:.02em; }
.b-Solicitado { background:var(--s-solicitado); color:var(--st-solicitado); }
.b-Recibido { background:var(--s-recibido); color:var(--st-recibido); }
.b-EnProceso { background:var(--s-proceso); color:var(--st-proceso); }
.b-Protocolizado { background:var(--s-protocolizado); color:var(--st-protocolizado); }
.b-EnRegistro { background:var(--s-registro); color:var(--st-registro); }
.b-Inscrito { background:var(--s-inscrito); color:var(--st-inscrito); }
.pill { display:inline-block; font-size:11px; font-weight:600; padding:2px 9px; border-radius:20px; }
.pill-si { background:#EAF3DE; color:#2E5E0E; }
.pill-no { background:#F1EFE8; color:#5F5E5A; }
.pill-alt { background:#FAEEDA; color:#7A4F10; }
.num-col { color:var(--gray-400); font-size:11px; text-align:center; width:36px; }
.ts { font-size:11px; color:var(--gray-400); white-space:nowrap; }
.user-col { font-size:11px; color:var(--gray-400); white-space:nowrap; max-width:120px; overflow:hidden; text-overflow:ellipsis; }
.btn-del {
width:28px; height:28px; border-radius:6px; border:1px solid var(--gray-200);
background:transparent; color:var(--gray-400); cursor:pointer; font-size:13px;
display:flex; align-items:center; justify-content:center; transition:all .15s;
}
.btn-del:hover { background:#FCEBEB; color:#791F1F; border-color:#F09595; }
.empty { text-align:center; padding:60px 24px; color:var(--gray-400); }
.empty-icon { font-size:36px; margin-bottom:12px; opacity:.4; }
.loading-overlay {
position:fixed; inset:0; background:rgba(27,58,107,0.5);
display:flex; align-items:center; justify-content:center;
z-index:999; opacity:0; pointer-events:none; transition:opacity .2s;
}
.loading-overlay.visible { opacity:1; pointer-events:all; }
.spinner { width:40px; height:40px; border:3px solid rgba(201,168,76,0.3); border-top-color:var(--gold); border-radius:50%; animation:spin .7s linear infinite; }
.toast {
position:fixed; bottom:24px; right:24px;
background:var(--navy-d); color:var(--white);
padding:12px 20px; border-radius:var(--radius); font-size:13px;
border-left:3px solid var(--gold); box-shadow:0 4px 20px rgba(0,0,0,0.2);
transform:translateY(20px); opacity:0; transition:all .3s; z-index:500;
}
.toast.show { transform:translateY(0); opacity:1; }
.toast.error { border-left-color:#ef5350; }
footer { text-align:center; padding:14px; font-size:11px; color:var(--gray-400); border-top:1px solid var(--gray-100); }
/* ── ROW ACTION BUTTONS ─────────────────────────────────────────────── */
.row-actions {
display:flex; gap:4px; align-items:center;
}
.ra {
width:28px; height:28px; border-radius:6px;
display:flex; align-items:center; justify-content:center;
border:1px solid var(--gray-200); background:transparent;
cursor:pointer; transition:all .15s; flex-shrink:0;
font-size:13px; color:var(--gray-400);
}
.ra svg { width:13px; height:13px; pointer-events:none; }
.ra:hover { border-color:transparent; }
.ra-save { color:#2E5E0E; }
.ra-save:hover { background:#EAF3DE; border-color:#b5d97b; color:#2E5E0E; }
.ra-edit { color:var(--navy); }
.ra-edit:hover { background:#E6F1FB; border-color:#85b7eb; color:var(--navy); }
.ra-dup { color:var(--gray-700); }
.ra-dup:hover { background:#FAEEDA; border-color:#fac775; color:#7A4F10; }
.ra-del { color:var(--gray-400); }
.ra-hist { color:#085041; }
.ra-hist:hover { background:#E1F5EE; border-color:#5DCAA5; color:#085041; }
/* ── HISTORIAL MODAL ────────────────────────────────────────────────── */
.modal-overlay {
position:fixed; inset:0; background:rgba(17,36,70,0.55);
z-index:1000; display:flex; align-items:center; justify-content:center;
opacity:0; pointer-events:none; transition:opacity .2s;
}
.modal-overlay.open { opacity:1; pointer-events:all; }
.modal {
background:var(--white); border-radius:var(--radius);
width:min(700px, 95vw); max-height:80vh;
display:flex; flex-direction:column;
box-shadow:0 20px 60px rgba(0,0,0,0.2);
transform:translateY(16px); transition:transform .2s;
}
.modal-overlay.open .modal { transform:translateY(0); }
.modal-header {
display:flex; align-items:center; justify-content:space-between;
padding:18px 24px; border-bottom:1px solid var(--gray-100);
flex-shrink:0;
}
.modal-header h3 {
font-family:'DM Serif Display',serif; font-size:18px;
color:var(--navy); font-weight:400;
}
.modal-subtitle { font-size:12px; color:var(--gray-400); margin-top:2px; }
.modal-close {
width:32px; height:32px; border-radius:6px;
border:1px solid var(--gray-200); background:transparent;
cursor:pointer; font-size:16px; color:var(--gray-400);
display:flex; align-items:center; justify-content:center;
transition:all .15s;
}
.modal-close:hover { background:#FCEBEB; color:#791F1F; border-color:#F09595; }
.modal-body { overflow-y:auto; padding:0; flex:1; }
.hist-empty {
text-align:center; padding:40px 24px;
color:var(--gray-400); font-size:13px;
}
.hist-entry {
display:grid;
grid-template-columns:160px 140px 1fr;
gap:0;
border-bottom:1px solid var(--gray-100);
padding:12px 24px;
align-items:start;
transition:background .1s;
}
.hist-entry:hover { background:var(--off); }
.hist-entry:last-child { border-bottom:none; }
.hist-fecha { font-size:11px; color:var(--gray-400); padding-top:1px; }
.hist-user {
font-size:11px; color:var(--navy); font-weight:500;
white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
}
.hist-campo { font-size:12px; font-weight:600; color:var(--gray-700); margin-bottom:3px; }
.hist-cambio { display:flex; align-items:center; gap:6px; flex-wrap:wrap; }
.hist-ant {
font-size:11px; background:#FCEBEB; color:#791F1F;
padding:2px 8px; border-radius:12px; text-decoration:line-through;
max-width:180px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
.hist-nvo {
font-size:11px; background:#EAF3DE; color:#2E5E0E;
padding:2px 8px; border-radius:12px;
max-width:180px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
.hist-arrow { font-size:11px; color:var(--gray-400); }
.hist-created {
font-size:11px; background:#E6F1FB; color:#0C447C;
padding:2px 10px; border-radius:12px; display:inline-block;
}
.modal-loading { text-align:center; padding:40px; color:var(--gray-400); font-size:13px; }
.ra-del:hover { background:#FCEBEB; border-color:#F09595; color:#791F1F; }
/* editing mode */
tr.editing-row td { background:#FAFBFF !important; }
tr.editing-row td input.editable,
tr.editing-row td select.editable,
tr.editing-row td textarea.editable {
border-color: var(--navy-l) !important;
background: var(--white) !important;
}
.ra-save.pending { animation:pulse 1.5s ease-in-out infinite; }
.new-row-actions {
display:flex; gap:5px; align-items:center; white-space:nowrap;
}
.btn-save-row {
padding:5px 12px; background:var(--navy); color:var(--white);
border:none; border-radius:6px; font-family:'DM Sans',sans-serif;
font-size:12px; font-weight:500; cursor:pointer; transition:background .15s;
white-space:nowrap;
}
.btn-save-row:hover { background:var(--navy-d); }
.btn-cancel-row {
padding:5px 10px; background:transparent; color:var(--gray-400);
border:1px solid var(--gray-200); border-radius:6px;
font-family:'DM Sans',sans-serif; font-size:12px; cursor:pointer;
transition:all .15s;
}
.btn-cancel-row:hover { background:#FCEBEB; color:#791F1F; border-color:#F09595; }
.unsaved-indicator {
display:inline-block; width:7px; height:7px; border-radius:50%;
background:var(--gold); margin-right:4px; flex-shrink:0;
animation:pulse 1.5s ease-in-out infinite;
}
.export-wrap { position:relative; }
.btn-export {
display:flex; align-items:center; gap:7px;
padding:8px 16px; background:var(--white); color:var(--navy);
border:1px solid var(--navy); border-radius:var(--radius);
font-family:'DM Sans',sans-serif; font-size:13px; font-weight:500;
cursor:pointer; transition:background .15s; white-space:nowrap;
}
.btn-export:hover { background:var(--off); }
.btn-export svg { width:14px; height:14px; }
.export-menu {
position:absolute; right:0; top:calc(100% + 6px);
background:var(--white); border:1px solid var(--gray-200);
border-radius:var(--radius); box-shadow:0 4px 20px rgba(0,0,0,0.1);
min-width:210px; z-index:300; overflow:hidden;
}
.export-menu button {
width:100%; display:flex; align-items:center; gap:10px;
padding:11px 16px; background:transparent; border:none;
font-family:'DM Sans',sans-serif; font-size:13px; color:var(--gray-700);
cursor:pointer; text-align:left; transition:background .12s;
}
.export-menu button:hover { background:var(--off); color:var(--navy); }
.export-menu button svg { width:15px; height:15px; flex-shrink:0; }
.export-menu button:first-child { color:#1D6F42; }
.export-menu button:first-child:hover { background:#EAF3DE; }
.export-menu button:last-child { color:#C0392B; }
.export-menu button:last-child:hover { background:#FCEBEB; }
</style>
</head>
<body>
<div class="loading-overlay" id="overlay"><div class="spinner"></div></div>
<div class="toast" id="toast"></div>
<header>
<div class="logo">
<div class="logo-mark">A</div>
<div class="logo-text">
<span class="logo-firm">AALN Law</span>
<span class="logo-tool">Control de Escrituras Públicas</span>
</div>
</div>
<div class="header-right">
<div class="sync-status">
<div class="sync-dot" id="sync-dot"></div>
<span id="sync-label">Conectando…</span>
</div>
<div class="user-chip">
<div class="user-avatar" id="user-avatar">?</div>
<span class="user-name" id="user-name">Cargando…</span>
</div>
<button class="btn-logout" onclick="doLogout()">Cerrar sesión</button>
</div>
</header>
<div class="toolbar">
<div class="search-wrap">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
<input type="text" id="search" placeholder="Buscar trámite, cliente, notaría...">
</div>
<div class="filter-group">
<span class="filter-label">Estatus</span>
<select class="flt" id="fStatus">
<option value="">Todos</option>
<option>Solicitado</option><option>Recibido</option><option>En Proceso</option>
<option>Protocolizado</option><option>En Registro</option><option>Inscrito</option>
</select>
</div>
<div class="filter-group">
<span class="filter-label">Abogado</span>
<select class="flt" id="fAbogado">
<option value="">Todos</option>
<option>Arias</option><option>Abrego</option><option>Lopez</option><option>Altafulla</option>
<option>Álvarez</option><option>Gutiérrez</option><option>Cedeño</option><option>Mariscal</option>
<option>Ávila</option><option>Agudo</option><option>Martínez</option><option>De Gracia</option>
<option>Góndola</option><option>González</option><option>Valdés</option>
</select>
</div>
<div class="filter-group">
<span class="filter-label">Inscribe</span>
<select class="flt" id="fInscribe">
<option value="">Todos</option><option>Sí</option><option>No</option>
</select>
</div>
<div class="filter-group">
<span class="filter-label">Alt. Turno</span>
<select class="flt" id="fAltera">
<option value="">Todos</option><option>Sí</option><option>No</option>
</select>
</div>
<button class="btn-new" onclick="newRow()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M12 5v14M5 12h14"/></svg>
Nuevo trámite
</button>
<div class="export-wrap" id="export-wrap">
<button class="btn-export" onclick="toggleExportMenu()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
Exportar
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" style="width:12px;height:12px;margin-left:2px;"><path d="m6 9 6 6 6-6"/></svg>
</button>
<div class="export-menu" id="export-menu" style="display:none;">
<button onclick="exportExcel()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="m9 9 6 6M15 9l-6 6"/></svg>
Exportar a Excel (.xlsx)
</button>
<button onclick="exportPDF()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>
Exportar a PDF
</button>
</div>
</div>
</div>
<div class="kpi-strip">
<div class="kpi"><div class="kpi-label">Total trámites</div><div class="kpi-val" id="k-total">—</div></div>
<div class="kpi"><div class="kpi-label">Inscritos</div><div class="kpi-val purple" id="k-inscrito">—</div></div>
<div class="kpi"><div class="kpi-label">En registro</div><div class="kpi-val" id="k-registro">—</div></div>
<div class="kpi"><div class="kpi-label">Pendientes</div><div class="kpi-val" id="k-pendientes">—</div></div>
<div class="kpi"><div class="kpi-label">Alt. de turno</div><div class="kpi-val gold" id="k-altera">—</div></div>
</div>
<div class="main">
<div class="table-card">
<div class="table-wrap">
<table>
<thead>
<tr>
<th>#</th>
<th>Nombre del Trámite</th>
<th>Cliente</th>
<th>Abogado Solicitante</th>
<th>Notaría</th>
<th>Se Inscribe</th>
<th>Alt. de Turno</th>
<th>Instrucciones Especiales</th>
<th>Fecha de Solicitud</th>
<th>Estatus</th>
<th>Últ. Actualización</th>
<th>Actualizado por</th>
<th>Observaciones</th>
<th>Creado por</th>
<th></th>
</tr>
</thead>
<tbody id="tbody"></tbody>
</table>
<div id="empty" class="empty" style="display:none;">
<div class="empty-icon">📋</div>
<p>No hay trámites registrados. Haga clic en <strong>Nuevo trámite</strong> para comenzar.</p>
</div>
</div>
</div>
</div>
<footer>
Arias, Abrego, López &amp; Noriega &nbsp;·&nbsp; P.H. Midtown, Piso 16, Of. 1605, Calle 74 Este, San Francisco, Ciudad de Panamá &nbsp;·&nbsp; SPECTER Legal Assistant
</footer>
<!-- HISTORIAL MODAL -->
<div class="modal-overlay" id="modal-overlay" onclick="closeModal(event)">
<div class="modal" id="modal">
<div class="modal-header">
<div>
<h3>Historial de cambios</h3>
<div class="modal-subtitle" id="modal-subtitle"></div>
</div>
<button class="modal-close" onclick="closeHistorial()">✕</button>
</div>
<div class="modal-body" id="modal-body">
<div class="modal-loading">Cargando historial…</div>
</div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
<script>
const ABOGADOS = ['Arias','Abrego','Lopez','Altafulla','Álvarez','Gutiérrez','Cedeño','Mariscal','Ávila','Agudo','Martínez','De Gracia','Góndola','González','Valdés'];
const ESTATUS = ['Solicitado','Recibido','En Proceso','Protocolizado','En Registro','Inscrito'];
const API = 'api.php';
const POLL_MS = 15000;
let allRows = [];
let authUser = { email:'', name:'' };
let pollTimer = null;
let isEditing = false; // true mientras hay fila nueva sin guardar
function esc(s) { return (s||'').replace(/&/g,'&amp;').replace(/"/g,'&quot;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
function fmtDate(dt) {
if (!dt) return '—';
const d = new Date(dt); if (isNaN(d)) return dt;
return d.toLocaleDateString('es-PA',{day:'2-digit',month:'2-digit',year:'numeric'})
+ ' ' + d.toLocaleTimeString('es-PA',{hour:'2-digit',minute:'2-digit'});
}
function badgeClass(e) { return 'b-'+(e||'').replace(/ /g,''); }
function setSyncState(state, label) {
document.getElementById('sync-dot').className = 'sync-dot'+(state!=='ok'?' '+state:'');
document.getElementById('sync-label').textContent = label;
}
function toast(msg, type='ok') {
const el = document.getElementById('toast');
el.textContent = msg; el.className = 'toast show'+(type==='error'?' error':'');
setTimeout(()=>{ el.className='toast'; }, 3000);
}
function setOverlay(on) { document.getElementById('overlay').classList.toggle('visible', on); }
function setUser(user) {
authUser = user || { email:'', name:'' };
const name = authUser.name || authUser.email || '';
document.getElementById('user-name').textContent = name;
document.getElementById('user-avatar').textContent = name ? name[0].toUpperCase() : '?';
}
async function apiFetch(method, action, body=null, id=null) {
const url = API+'?action='+action+(id?'&id='+id:'');
const opts = {
method,
headers:{'Content-Type':'application/json','Accept':'application/json'},
credentials:'same-origin'
};
if (body) opts.body = JSON.stringify(body);
const res = await fetch(url, opts);
const json = await res.json();
if (res.status === 401 && json.auth_required && json.login_url) {
window.location.href = json.login_url;
return null;
}
if (!json.ok) throw new Error(json.error||'Error desconocido');
if (json.user) setUser(json.user);
return json.data;
}
async function loadAll(showOverlay=false) {
try {
setSyncState('syncing','Actualizando…');
if (showOverlay) setOverlay(true);
const rows = await apiFetch('GET','list');
if (rows === null) return;
allRows = rows || [];
setSyncState('ok','Sincronizado');
// No re-renderizar si el usuario está escribiendo una fila nueva
if (!isEditing) render();
} catch(e) {
setSyncState('error','Sin conexión');
toast('Error al cargar datos: '+e.message,'error');
} finally {
if (showOverlay) setOverlay(false);
}
}
async function saveRow(row) {
try {
setSyncState('syncing','Guardando…');
const method = row.id ? 'PUT' : 'POST';
const action = row.id ? 'update' : 'create';
const saved = await apiFetch(method, action, row);
if (!saved) return;
// Reemplazar fila temporal (id null) o actualizar existente
const nullIdx = allRows.findIndex(r=>r.id===null);
if (nullIdx >= 0) allRows.splice(nullIdx, 1);
const idx = allRows.findIndex(r=>r.id===saved.id);
if (idx>=0) allRows[idx]=saved; else allRows.unshift(saved);
isEditing = false; // fila guardada, reanudar polling
setSyncState('ok','Guardado');
toast('Trámite guardado correctamente');
render();
} catch(e) {
setSyncState('error','Error al guardar');
toast('Error al guardar: '+e.message,'error');
}
}
async function deleteRow(id) {
if (!confirm('¿Eliminar este trámite? Esta acción no se puede deshacer.')) return;
try {
setSyncState('syncing','Eliminando…');
await apiFetch('DELETE','delete',null,id);
allRows = allRows.filter(r=>r.id!==id);
setSyncState('ok','Sincronizado');
toast('Trámite eliminado');
render();
} catch(e) {
setSyncState('error','Error');
toast('Error al eliminar: '+e.message,'error');
}
}
function saveNewRow(btn) {
const tr = btn.closest('tr');
const fields = getRowData(tr);
if (!fields.tramite || !fields.tramite.trim()) {
// highlight tramite field
const inp = tr.querySelector('[data-field="tramite"]');
if (inp) {
inp.style.borderColor = '#ef5350';
inp.focus();
setTimeout(() => { inp.style.borderColor = ''; }, 2000);
}
toast('El nombre del trámite es requerido', 'error');
return;
}
const base = allRows.find(r => r.id === null) || {};
saveRow(Object.assign({}, base, fields, { id: null }));
}
function cancelNewRow() {
allRows = allRows.filter(r=>r.id!==null);
isEditing = false;
render();
}
function doLogout() {
if (confirm('¿Cerrar sesión?')) window.location.href = 'api.php?action=logout';
}
function newRow() {
isEditing = true;
allRows.unshift({ id:null, tramite:'', cliente:'', abogado:'Abrego', notaria:'',
inscribe:'Sí', altera_turno:'No', instrucciones:'',
fecha_solicitud: new Date().toISOString(), estatus:'Solicitado',
observaciones:'', creado_por: authUser.email });
render();
setTimeout(()=>{
const inp = document.querySelector('tr.data-row [data-field="tramite"]');
if (inp) inp.focus();
toast('Llena los campos y haz clic en Guardar · Ctrl+Enter para guardar rápido');
}, 50);
}
function updateKPIs() {
document.getElementById('k-total').textContent = allRows.length;
document.getElementById('k-inscrito').textContent = allRows.filter(r=>r.estatus==='Inscrito').length;
document.getElementById('k-registro').textContent = allRows.filter(r=>r.estatus==='En Registro').length;
document.getElementById('k-pendientes').textContent = allRows.filter(r=>['Solicitado','Recibido'].includes(r.estatus)).length;
document.getElementById('k-altera').textContent = allRows.filter(r=>r.altera_turno==='Sí').length;
}
function getFiltered() {
const q = document.getElementById('search').value.toLowerCase();
const fS = document.getElementById('fStatus').value;
const fA = document.getElementById('fAbogado').value;
const fI = document.getElementById('fInscribe').value;
const fAl= document.getElementById('fAltera').value;
return allRows.filter(r=>{
const matchQ = !q||[r.tramite,r.cliente,r.notaria,r.abogado,r.instrucciones,r.observaciones].join(' ').toLowerCase().includes(q);
return matchQ&&(!fS||r.estatus===fS)&&(!fA||r.abogado===fA)&&(!fI||r.inscribe===fI)&&(!fAl||r.altera_turno===fAl);
});
}
function render() {
updateKPIs();
const filtered = getFiltered();
const tbody = document.getElementById('tbody');
const empty = document.getElementById('empty');
if (!filtered.length) { tbody.innerHTML=''; empty.style.display='block'; return; }
empty.style.display='none';
tbody.innerHTML = filtered.map((r,i)=>{
const isNew = r.id === null;
const dis = isNew ? '' : 'disabled style="pointer-events:none;background:transparent;border-color:transparent;"';
return `
<tr class="data-row${r.estatus==='Inscrito'?' inscrito-row':''}" data-id="${r.id||''}">
<td class="num-col">${i+1}</td>
<td><input class="editable" data-field="tramite" value="${esc(r.tramite)}" placeholder="Nombre del trámite" style="min-width:150px;" ${dis}></td>
<td><input class="editable" data-field="cliente" value="${esc(r.cliente)}" placeholder="Cliente" style="min-width:120px;" ${dis}></td>
<td>
<select class="editable" data-field="abogado" style="min-width:100px;" ${dis}>
${ABOGADOS.map(a=>`<option${r.abogado===a?' selected':''}>${a}</option>`).join('')}
</select>
</td>
<td><input class="editable" data-field="notaria" value="${esc(r.notaria)}" placeholder="Notaría" style="min-width:80px;" ${dis}></td>
<td style="text-align:center;">
<select class="editable" data-field="inscribe" style="width:52px;" ${dis}>
<option${r.inscribe==='Sí'?' selected':''}>Sí</option>
<option${r.inscribe==='No'?' selected':''}>No</option>
</select><br>
<span class="pill ${r.inscribe==='Sí'?'pill-si':'pill-no'}">${esc(r.inscribe)}</span>
</td>
<td style="text-align:center;">
<select class="editable" data-field="altera_turno" style="width:52px;" ${dis}>
<option${r.altera_turno==='Sí'?' selected':''}>Sí</option>
<option${r.altera_turno==='No'?' selected':''}>No</option>
</select><br>
<span class="pill ${r.altera_turno==='Sí'?'pill-alt':'pill-no'}">${esc(r.altera_turno)}</span>
</td>
<td style="min-width:160px;max-width:220px;">
<textarea class="editable" data-field="instrucciones" rows="2" placeholder="Instrucciones especiales…" ${dis}>${esc(r.instrucciones)}</textarea>
</td>
<td class="ts">${fmtDate(r.fecha_solicitud)}</td>
<td>
<select class="editable" data-field="estatus" style="min-width:120px;" ${dis}>
${ESTATUS.map(e=>`<option${r.estatus===e?' selected':''}>${e}</option>`).join('')}
</select><br>
<span class="badge ${badgeClass(r.estatus)}">${esc(r.estatus)}</span>
</td>
<td class="ts">${fmtDate(r.ultima_actualizacion)}</td>
<td class="user-col" title="${esc(r.actualizado_por||'')}">${esc(r.actualizado_por ? r.actualizado_por.split('@')[0] : '—')}</td>
<td style="min-width:140px;max-width:180px;">
<textarea class="editable" data-field="observaciones" rows="2" placeholder="Observaciones…" ${dis}>${esc(r.observaciones)}</textarea>
</td>
<td class="user-col" title="${esc(r.creado_por)}">${esc(r.creado_por||'—')}</td>
<td>
${r.id === null ? `
<div class="new-row-actions">
<button class="btn-save-row" onclick="saveNewRow(this)">Guardar</button>
<button class="btn-cancel-row" onclick="cancelNewRow()">✕</button>
</div>
` : `
<div class="row-actions" data-id="${r.id}">
<button class="ra ra-save" title="Guardar cambios" onclick="saveExistingRow(${r.id})">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>
</button>
<button class="ra ra-edit" title="Editar fila" onclick="toggleEditRow(${r.id}, this)">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
</button>
<button class="ra ra-hist" title="Ver historial" onclick="showHistorial(${r.id}, '${esc(r.tramite)}')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
</button>
<button class="ra ra-dup" title="Duplicar trámite" onclick="duplicateRow(${r.id})">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
</button>
<button class="ra ra-del" title="Eliminar trámite" onclick="deleteRow(${r.id})">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6M14 11v6"/><path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/></svg>
</button>
</div>
`}
</td>
</tr>
`;
}).join('');
tbody.querySelectorAll('.editable').forEach(el=>{
el.addEventListener('keydown', onKeydown);
// Mark row dirty when any field changes (for save indicator)
el.addEventListener('input', e => {
const tr = e.target.closest('tr');
const id = parseInt(tr.dataset.id) || null;
if (id) {
const saveBtn = tr.querySelector('.ra-save');
if (saveBtn) saveBtn.classList.add('pending');
}
});
});
}
function getRowData(tr) {
const f={};
tr.querySelectorAll('.editable').forEach(el=>{ f[el.dataset.field]=el.value; });
return f;
}
function onFieldChange(e) {
const tr=e.target.closest('tr'); const id=parseInt(tr.dataset.id)||null;
const row=allRows.find(r=>r.id===id)||allRows[0];
saveRow(Object.assign({},row,getRowData(tr),{id}));
}
function onFieldBlur(e) {
// Blur no guarda — las filas nuevas usan botón Guardar explícito
// Las filas existentes se guardan con onFieldChange (change event)
}
function onKeydown(e) {
const tr = e.target.closest('tr');
const rowId = parseInt(tr.dataset.id) || null;
if (e.key === 'Escape' && !rowId) cancelNewRow();
if (e.key === 'Enter' && !rowId && e.ctrlKey) {
// Ctrl+Enter guarda la fila nueva
const btn = tr.querySelector('.btn-save-row');
if (btn) saveNewRow(btn);
}
}
['search','fStatus','fAbogado','fInscribe','fAltera'].forEach(id=>{
document.getElementById(id).addEventListener('input',()=>render());
document.getElementById(id).addEventListener('change',()=>render());
});
// ── ROW ACTIONS ──────────────────────────────────────────────────────────
let editingRows = new Set(); // track which rows are in edit mode
function toggleEditRow(id, btn) {
const tr = document.querySelector(`tr[data-id="${id}"]`);
if (!tr) return;
const isEditing = editingRows.has(id);
if (isEditing) {
// Exit edit mode without saving
editingRows.delete(id);
tr.classList.remove('editing-row');
// Restore original values from allRows
const r = allRows.find(r => r.id === id);
if (r) {
tr.querySelectorAll('.editable').forEach(el => {
el.value = r[el.dataset.field] || '';
el.disabled = true;
el.style.pointerEvents = 'none';
el.style.background = 'transparent';
el.style.borderColor = 'transparent';
});
}
btn.title = 'Editar fila';
btn.style.background = '';
btn.style.borderColor = '';
} else {
// Enter edit mode
editingRows.add(id);
tr.classList.add('editing-row');
tr.querySelectorAll('.editable').forEach(el => {
el.disabled = false;
el.style.pointerEvents = '';
el.style.background = '';
el.style.borderColor = '';
});
btn.title = 'Cancelar edición';
btn.style.background = '#FAEEDA';
btn.style.borderColor = '#fac775';
// Focus first text input
const first = tr.querySelector('input.editable');
if (first) first.focus();
}
}
function saveExistingRow(id) {
const tr = document.querySelector(`tr[data-id="${id}"]`);
if (!tr) return;
const base = allRows.find(r => r.id === id);
if (!base) return;
const fields = getRowData(tr);
if (!fields.tramite || !fields.tramite.trim()) {
const inp = tr.querySelector('[data-field="tramite"]');
if (inp) { inp.style.borderColor='#ef5350'; inp.focus(); setTimeout(()=>{inp.style.borderColor='';},2000); }
toast('El nombre del trámite es requerido','error');
return;
}
// Exit edit mode
editingRows.delete(id);
tr.classList.remove('editing-row');
const saveBtn = tr.querySelector('.ra-save');
if (saveBtn) saveBtn.classList.remove('pending');
saveRow(Object.assign({}, base, fields, { id }));
}
function duplicateRow(id) {
const r = allRows.find(r => r.id === id);
if (!r) return;
isEditing = true;
const dup = Object.assign({}, r, {
id: null,
tramite: r.tramite ? 'Copia — ' + r.tramite : '',
fecha_solicitud: new Date().toISOString(),
estatus: 'Solicitado',
creado_por: authUser.email
});
allRows.unshift(dup);
render();
// Focus tramite of new row
setTimeout(() => {
const inp = document.querySelector('tr[data-id=""] [data-field="tramite"], tr[data-id="0"] [data-field="tramite"]');
if (inp) { inp.focus(); inp.select(); }
toast('Trámite duplicado — revisa los datos y haz clic en Guardar');
}, 60);
}
// ── HISTORIAL ────────────────────────────────────────────────────────────
async function showHistorial(id, tramiteName) {
const overlay = document.getElementById('modal-overlay');
const body = document.getElementById('modal-body');
const subtitle= document.getElementById('modal-subtitle');
subtitle.textContent = tramiteName || `Trámite #${id}`;
body.innerHTML = '<div class="modal-loading">Cargando historial…</div>';
overlay.classList.add('open');
try {
const data = await apiFetch('GET', 'historial', null, id);
if (!data || !data.length) {
body.innerHTML = '<div class="hist-empty">No hay registros de cambios para este trámite.</div>';
return;
}
body.innerHTML = data.map(h => {
const isCreated = h.campo === 'Trámite' && h.valor_nuevo === 'Creado';
const user = (h.modificado_por || '').split('@')[0] || h.modificado_por;
const fecha = fmtDate(h.modificado_en);
const cambio = isCreated
? `<span class="hist-created">✓ Trámite creado</span>`
: `<div class="hist-campo">${esc(h.campo)}</div>
<div class="hist-cambio">
${h.valor_anterior ? `<span class="hist-ant" title="${esc(h.valor_anterior)}">${esc(h.valor_anterior)}</span><span class="hist-arrow">→</span>` : ''}
<span class="hist-nvo" title="${esc(h.valor_nuevo)}">${esc(h.valor_nuevo) || '<em>vacío</em>'}</span>
</div>`;
return `<div class="hist-entry">
<div class="hist-fecha">${fecha}</div>
<div class="hist-user" title="${esc(h.modificado_por)}">${esc(user)}</div>
<div>${cambio}</div>
</div>`;
}).join('');
} catch(e) {
body.innerHTML = `<div class="hist-empty">Error al cargar historial: ${esc(e.message)}</div>`;
}
}
function closeHistorial() {
document.getElementById('modal-overlay').classList.remove('open');
}
function closeModal(e) {
if (e.target === document.getElementById('modal-overlay')) closeHistorial();
}
// Cerrar con Escape
document.addEventListener('keydown', e => {
if (e.key === 'Escape') closeHistorial();
});
// ── EXPORT ───────────────────────────────────────────────────────────────
function toggleExportMenu() {
const menu = document.getElementById('export-menu');
menu.style.display = menu.style.display === 'none' ? 'block' : 'none';
}
document.addEventListener('click', e => {
if (!document.getElementById('export-wrap').contains(e.target)) {
document.getElementById('export-menu').style.display = 'none';
}
});
function getExportRows() {
return getFiltered(); // respeta filtros activos
}
function exportHeaders() {
return ['#','Nombre del Trámite','Cliente','Abogado Solicitante','Notaría',
'Se Inscribe','Alt. de Turno','Instrucciones Especiales',
'Fecha de Solicitud','Estatus','Últ. Actualización','Observaciones','Creado por'];
}
function rowToArray(r, i) {
return [
i+1, r.tramite, r.cliente, r.abogado, r.notaria,
r.inscribe, r.altera_turno, r.instrucciones,
fmtDate(r.fecha_solicitud), r.estatus,
fmtDate(r.ultima_actualizacion), r.observaciones, r.creado_por
];
}
function exportExcel() {
document.getElementById('export-menu').style.display = 'none';
const rows = getExportRows();
if (!rows.length) { toast('No hay datos para exportar','error'); return; }
const wb = XLSX.utils.book_new();
const data = [exportHeaders(), ...rows.map(rowToArray)];
const ws = XLSX.utils.aoa_to_sheet(data);
// Column widths
ws['!cols'] = [
{wch:4},{wch:30},{wch:22},{wch:18},{wch:12},
{wch:10},{wch:12},{wch:35},{wch:18},{wch:16},{wch:18},{wch:30},{wch:22}
];
// Header style (row 1)
const range = XLSX.utils.decode_range(ws['!ref']);
for (let C = range.s.c; C <= range.e.c; C++) {
const addr = XLSX.utils.encode_cell({r:0, c:C});
if (!ws[addr]) continue;
ws[addr].s = {
fill: { fgColor: { rgb: '1B3A6B' } },
font: { bold: true, color: { rgb: 'FFFFFF' }, sz: 11 },
alignment: { horizontal: 'center', vertical: 'center', wrapText: true }
};
}
XLSX.utils.book_append_sheet(wb, ws, 'Escrituras');
const fecha = new Date().toLocaleDateString('es-PA').replace(/\//g,'-');
XLSX.writeFile(wb, `Control_Escrituras_AALN_${fecha}.xlsx`);
toast('Excel exportado correctamente');
}
function exportPDF() {
document.getElementById('export-menu').style.display = 'none';
const rows = getExportRows();
if (!rows.length) { toast('No hay datos para exportar','error'); return; }
const fecha = new Date().toLocaleDateString('es-PA');
const headers = exportHeaders();
// Columnas a mostrar en PDF (subset para que quepa en landscape)
const pdfCols = [0,1,2,3,4,5,6,9,11];
const pdfHeaders = pdfCols.map(i=>headers[i]);
const win = window.open('','_blank');
const tableRows = rows.map((r,i) => {
const arr = rowToArray(r,i);
return '<tr>' + pdfCols.map(ci => `<td>${esc(String(arr[ci]||''))}</td>`).join('') + '</tr>';
}).join('');
const statusColors = {
'Solicitado': ['#F1EFE8','#5F5E5A'],
'Recibido': ['#E6F1FB','#0C447C'],
'En Proceso': ['#FAEEDA','#7A4F10'],
'Protocolizado': ['#EAF3DE','#2E5E0E'],
'En Registro': ['#E1F5EE','#085041'],
'Inscrito': ['#EEEDFE','#3C3489'],
};
const styledRows = rows.map((r,i) => {
const arr = rowToArray(r,i);
const cells = pdfCols.map(ci => {
let val = String(arr[ci]||'');
if (ci === 9) { // estatus
const [bg,fg] = statusColors[r.estatus] || ['#f5f5f5','#333'];
return `<td><span style="background:${bg};color:${fg};padding:2px 8px;border-radius:12px;font-size:9px;font-weight:600;white-space:nowrap;">${esc(val)}</span></td>`;
}
return `<td>${esc(val)}</td>`;
}).join('');
return `<tr class="${i%2===0?'even':''}">${cells}</tr>`;
}).join('');
win.document.write(`<!DOCTYPE html><html lang="es"><head>
<meta charset="UTF-8">
<title>Control de Escrituras — AALN Law</title>
<style>
@page { size: A4 landscape; margin: 12mm 10mm; }
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: Arial, sans-serif; font-size: 9px; color: #1a1916; }
.header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; border-bottom: 3px solid #C9A84C; padding-bottom: 8px; }
.header-left { display: flex; align-items: center; gap: 10px; }
.logo-mark { width: 28px; height: 28px; background: #1B3A6B; border-radius: 5px; display: flex; align-items: center; justify-content: center; color: #C9A84C; font-weight: 700; font-size: 14px; }
.firm-name { font-size: 13px; font-weight: 700; color: #1B3A6B; }
.doc-title { font-size: 9px; color: #666; }
.meta { font-size: 8px; color: #888; text-align: right; }
table { width: 100%; border-collapse: collapse; }
th { background: #1B3A6B; color: #fff; padding: 6px 5px; text-align: left; font-size: 8px; font-weight: 700; text-transform: uppercase; letter-spacing: .03em; white-space: nowrap; }
td { padding: 5px 5px; border-bottom: 1px solid #EEEDE9; font-size: 8.5px; vertical-align: middle; }
tr.even td { background: #F7F6F3; }
tr:hover td { background: #EEF1F8; }
.footer { margin-top: 10px; font-size: 7.5px; color: #aaa; text-align: center; border-top: 1px solid #eee; padding-top: 6px; }
@media print { .no-print { display:none; } }
</style>
</head><body>
<div class="header">
<div class="header-left">
<div class="logo-mark">A</div>
<div>
<div class="firm-name">AALN Law — Arias, Abrego, López &amp; Noriega</div>
<div class="doc-title">Control de Estatus de Escrituras Públicas</div>
</div>
</div>
<div class="meta">
Exportado: ${fecha}<br>
Total registros: ${rows.length}
</div>
</div>
<table>
<thead><tr>${pdfHeaders.map(h=>`<th>${h}</th>`).join('')}</tr></thead>
<tbody>${styledRows}</tbody>
</table>
<div class="footer">
P.H. Midtown, Piso 16, Of. 1605, Calle 74 Este, San Francisco, Ciudad de Panamá &nbsp;·&nbsp; SPECTER Legal Assistant
</div>
<script>window.onload=()=>{ window.print(); }<\/script>
</body></html>`);
win.document.close();
toast('PDF generado — revisa la ventana de impresión');
}
loadAll(true).then(()=>{ pollTimer=setInterval(()=>loadAll(false), POLL_MS); });
</script>
</body>
</html>