Files
donna/views/escrituras.html
T
Soporte ZH Consultores 3a342cb6a8 feat(escrituras): edición por modal + botón "← Portal"
* Reemplaza la edición inline (15 columnas con inputs editables) por
  un modal popup tipo Control de Trámites:
    - Click ✏️ o doble-click sobre la fila → abre el modal con el form
    - "Nuevo trámite" abre el mismo modal vacío
    - Duplicar abre el modal pre-poblado con "Copia — <nombre>"
    - Esc cierra el modal (prioritizando edición sobre historial)
    - Validación en el form: nombre requerido, patrón ########/####
      para Entrada antes de POST/PUT
* La tabla queda read-only — más limpia visualmente y sin riesgo de
  edits accidentales por click en una celda.
* Eliminadas funciones obsoletas: newRow/saveNewRow/cancelNewRow/
  toggleEditRow/saveExistingRow/getRowData/onFieldChange/onFieldBlur/
  onKeydown y el Set editingRows. Reemplazadas por openEditModal/
  closeEditModal/buildEditForm/saveEditModal.
* maybeFlushRefresh ahora solo chequea editModalOpen — más simple.
* Header: botón "← Portal" arriba a la izquierda (igual estilo de
  retorno que tiene Trámites Legales).
2026-05-11 20:12:46 +00:00

1184 lines
50 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; }
/* BACK TO PORTAL */
.header-left { display:flex; align-items:center; gap:14px; }
.btn-back {
display:inline-flex; align-items:center; gap:6px;
background:rgba(255,255,255,0.08); border:1px solid rgba(255,255,255,0.18);
color:rgba(255,255,255,0.9); padding:6px 12px 6px 9px; border-radius:8px;
font-size:12px; text-decoration:none; transition:all .15s;
font-family:'DM Sans',sans-serif; white-space:nowrap;
}
.btn-back:hover { background:var(--gold); border-color:var(--gold); color:var(--navy); }
.btn-back svg { width:14px; height:14px; }
/* EDIT MODAL */
.edit-modal-overlay {
display:none; position:fixed; inset:0;
background:rgba(17,36,70,0.6); z-index:1000;
align-items:center; justify-content:center; padding:24px;
}
.edit-modal-overlay.open { display:flex; }
.edit-modal {
background:#fff; border-radius:14px;
width:100%; max-width:620px; max-height:90vh;
overflow:hidden; display:flex; flex-direction:column;
box-shadow:0 20px 60px rgba(0,0,0,0.3);
}
.edit-modal-header {
background:var(--navy); color:#fff;
padding:16px 22px; display:flex; justify-content:space-between; align-items:center;
border-bottom:2px solid var(--gold);
}
.edit-modal-header h3 {
font-family:'DM Serif Display',serif; font-size:18px; font-weight:400; margin:0;
}
.edit-modal-close {
background:transparent; border:none; color:rgba(255,255,255,0.7);
font-size:22px; cursor:pointer; line-height:1; padding:0 4px;
transition:color .15s;
}
.edit-modal-close:hover { color:#fff; }
.edit-modal-body {
padding:22px; overflow-y:auto; flex:1;
}
.fg { margin-bottom:14px; }
.fg label {
display:block; font-size:10.5px; font-weight:600; color:var(--navy);
margin-bottom:5px; text-transform:uppercase; letter-spacing:.08em;
}
.fg input, .fg select, .fg textarea {
width:100%; padding:8px 11px;
border:1px solid var(--gray-200); border-radius:8px;
font-family:'DM Sans',sans-serif; font-size:13px; color:var(--gray-900);
background:var(--white); transition:border-color .15s, box-shadow .15s;
}
.fg input:focus, .fg select:focus, .fg textarea:focus {
outline:none; border-color:var(--navy-l);
box-shadow:0 0 0 3px rgba(27,58,107,0.10);
}
.fg textarea { resize:vertical; min-height:64px; font-family:'DM Sans',sans-serif; }
.fg-row { display:grid; grid-template-columns:1fr 1fr; gap:12px; }
@media (max-width:560px) { .fg-row { grid-template-columns:1fr; } }
.fg-hint { font-size:10.5px; color:var(--gray-400); margin-top:4px; font-style:italic; }
.edit-modal-footer {
padding:14px 22px; border-top:1px solid var(--gray-100);
display:flex; justify-content:flex-end; gap:10px; background:var(--off);
}
.btn-cancel {
padding:8px 18px; background:transparent; color:var(--gray-700);
border:1px solid var(--gray-200); border-radius:8px;
font-size:13px; font-family:'DM Sans',sans-serif; cursor:pointer;
transition:all .15s;
}
.btn-cancel:hover { background:var(--gray-100); }
.btn-save {
padding:8px 20px; background:var(--navy); color:#fff;
border:none; border-radius:8px;
font-size:13px; font-weight:500; font-family:'DM Sans',sans-serif; cursor:pointer;
transition:background .15s;
}
.btn-save:hover { background:var(--navy-d); }
.btn-save:disabled { opacity:.6; cursor:not-allowed; }
/* 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="header-left">
<a class="btn-back" href="/" title="Volver al portal Donna">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg>
Portal
</a>
<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>
<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="openEditModal(null)">
<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>Entrada</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>
<!-- EDIT MODAL -->
<div class="edit-modal-overlay" id="edit-modal-overlay" onclick="closeEditModalBackdrop(event)">
<div class="edit-modal">
<div class="edit-modal-header">
<h3 id="edit-modal-title">Nuevo trámite</h3>
<button class="edit-modal-close" onclick="closeEditModal()" title="Cerrar (Esc)">✕</button>
</div>
<div class="edit-modal-body" id="edit-modal-body"></div>
<div class="edit-modal-footer">
<button class="btn-cancel" onclick="closeEditModal()">Cancelar</button>
<button class="btn-save" onclick="saveEditModal()">Guardar trámite</button>
</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 editModalOpen = false; // modal de edición/nuevo abierta
let editingId = null; // id del trámite siendo editado (null = nuevo)
let pendingRefresh = false; // poll trajo datos durante una edición
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 || [];
// Si el modal de edición está abierto, no re-renderizar (preserva el form)
if (editModalOpen) {
pendingRefresh = true;
setSyncState('ok','Edición en curso · refresco pausado');
} else {
render();
setSyncState('ok','Sincronizado');
}
} catch(e) {
setSyncState('error','Sin conexión');
toast('Error al cargar datos: '+e.message,'error');
} finally {
if (showOverlay) setOverlay(false);
}
}
// Si el poll trajo datos durante una edición, aplicarlos al cerrar el modal
function maybeFlushRefresh() {
if (pendingRefresh && !editModalOpen) {
pendingRefresh = false;
render();
setSyncState('ok','Sincronizado');
}
}
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 null;
const idx = allRows.findIndex(r=>r.id===saved.id);
if (idx>=0) allRows[idx]=saved; else allRows.unshift(saved);
setSyncState('ok','Guardado');
toast('Trámite guardado correctamente');
render();
return saved;
} catch(e) {
setSyncState('error','Error al guardar');
toast('Error al guardar: '+e.message,'error');
throw e;
}
}
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 doLogout() {
if (confirm('¿Cerrar sesión?')) window.location.href = 'api.php?action=logout';
}
// ── EDIT MODAL ───────────────────────────────────────────────────────────
function openEditModal(id) {
editingId = id;
const r = id ? allRows.find(x => x.id === id) : null;
document.getElementById('edit-modal-title').textContent = r
? `Editar trámite #${id}`
: 'Nuevo trámite';
document.getElementById('edit-modal-body').innerHTML = buildEditForm(r);
document.getElementById('edit-modal-overlay').classList.add('open');
editModalOpen = true;
setTimeout(() => {
const f = document.getElementById('f-tramite');
if (f) { f.focus(); if (r) f.select(); }
}, 60);
}
function closeEditModal() {
document.getElementById('edit-modal-overlay').classList.remove('open');
editModalOpen = false;
editingId = null;
maybeFlushRefresh();
}
function closeEditModalBackdrop(e) {
if (e.target === document.getElementById('edit-modal-overlay')) closeEditModal();
}
function buildEditForm(r) {
const v = r || {
tramite:'', cliente:'', abogado:'Abrego', notaria:'',
inscribe:'Sí', altera_turno:'No', instrucciones:'',
estatus:'Solicitado', entrada:'', observaciones:''
};
const ab = ABOGADOS.map(a=>`<option${v.abogado===a?' selected':''}>${a}</option>`).join('');
const st = ESTATUS .map(s=>`<option${v.estatus===s?' selected':''}>${s}</option>`).join('');
return `
<div class="fg">
<label>Nombre del Trámite *</label>
<input type="text" id="f-tramite" value="${esc(v.tramite)}" placeholder="Nombre descriptivo del trámite" required>
</div>
<div class="fg-row">
<div class="fg">
<label>Cliente</label>
<input type="text" id="f-cliente" value="${esc(v.cliente)}" placeholder="Nombre del cliente">
</div>
<div class="fg">
<label>Abogado Solicitante</label>
<select id="f-abogado">${ab}</select>
</div>
</div>
<div class="fg-row">
<div class="fg">
<label>Notaría</label>
<input type="text" id="f-notaria" value="${esc(v.notaria)}" placeholder="Ej: Notaría Quinta">
</div>
<div class="fg">
<label>Estatus</label>
<select id="f-estatus">${st}</select>
</div>
</div>
<div class="fg-row">
<div class="fg">
<label>Se Inscribe</label>
<select id="f-inscribe">
<option${v.inscribe==='Sí'?' selected':''}>Sí</option>
<option${v.inscribe==='No'?' selected':''}>No</option>
</select>
</div>
<div class="fg">
<label>Alteración de Turno</label>
<select id="f-altera">
<option${v.altera_turno==='Sí'?' selected':''}>Sí</option>
<option${v.altera_turno==='No'?' selected':''}>No</option>
</select>
</div>
</div>
<div class="fg">
<label>Entrada</label>
<input type="text" id="f-entrada" value="${esc(v.entrada||'')}"
pattern="^\\d{1,8}/\\d{4}$" maxlength="13"
placeholder="########/####" style="font-variant-numeric:tabular-nums;">
<div class="fg-hint">Hasta 8 dígitos, slash, y año de 4 dígitos. Ej: 12345678/2026 — opcional.</div>
</div>
<div class="fg">
<label>Instrucciones Especiales</label>
<textarea id="f-instrucciones" rows="3" placeholder="Notas internas para el equipo…">${esc(v.instrucciones)}</textarea>
</div>
<div class="fg">
<label>Observaciones</label>
<textarea id="f-observaciones" rows="3" placeholder="Observaciones públicas o de seguimiento…">${esc(v.observaciones)}</textarea>
</div>
`;
}
function gv(id){ const el=document.getElementById(id); return el?el.value:''; }
async function saveEditModal() {
const fields = {
tramite: gv('f-tramite').trim(),
cliente: gv('f-cliente').trim(),
abogado: gv('f-abogado'),
notaria: gv('f-notaria').trim(),
inscribe: gv('f-inscribe'),
altera_turno: gv('f-altera'),
instrucciones: gv('f-instrucciones'),
estatus: gv('f-estatus'),
entrada: gv('f-entrada').trim(),
observaciones: gv('f-observaciones'),
};
if (!fields.tramite) {
const i = document.getElementById('f-tramite');
if (i) {
i.style.borderColor = '#ef5350';
i.focus();
setTimeout(() => { i.style.borderColor = ''; }, 2000);
}
toast('El nombre del trámite es requerido', 'error');
return;
}
if (fields.entrada && !/^\d{1,8}\/\d{4}$/.test(fields.entrada)) {
const i = document.getElementById('f-entrada');
if (i) {
i.style.borderColor = '#ef5350';
i.focus();
setTimeout(() => { i.style.borderColor = ''; }, 2000);
}
toast('Entrada debe tener formato ########/####','error');
return;
}
const saveBtn = document.querySelector('.btn-save');
if (saveBtn) saveBtn.disabled = true;
try {
const base = editingId ? (allRows.find(r=>r.id===editingId) || {}) : {};
const payload = Object.assign({}, base, fields, { id: editingId });
await saveRow(payload);
closeEditModal();
} catch (e) {
// saveRow ya mostró el toast
} finally {
if (saveBtn) saveBtn.disabled = false;
}
}
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)=>{
return `
<tr class="data-row${r.estatus==='Inscrito'?' inscrito-row':''}" data-id="${r.id}" ondblclick="openEditModal(${r.id})">
<td class="num-col">${i+1}</td>
<td style="min-width:150px;"><b>${esc(r.tramite)||'—'}</b></td>
<td style="min-width:120px;">${esc(r.cliente)||'—'}</td>
<td>${esc(r.abogado)||'—'}</td>
<td>${esc(r.notaria)||'—'}</td>
<td style="text-align:center;"><span class="pill ${r.inscribe==='Sí'?'pill-si':'pill-no'}">${esc(r.inscribe)}</span></td>
<td style="text-align:center;"><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;white-space:pre-wrap;font-size:12px;color:var(--gray-700);">${esc(r.instrucciones)||'—'}</td>
<td class="ts">${fmtDate(r.fecha_solicitud)}</td>
<td><span class="badge ${badgeClass(r.estatus)}">${esc(r.estatus)}</span></td>
<td style="font-variant-numeric:tabular-nums;color:var(--gray-700);">${esc(r.entrada||'')||'—'}</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;white-space:pre-wrap;font-size:12px;color:var(--gray-700);">${esc(r.observaciones)||'—'}</td>
<td class="user-col" title="${esc(r.creado_por)}">${esc(r.creado_por ? r.creado_por.split('@')[0] : '—')}</td>
<td>
<div class="row-actions" data-id="${r.id}">
<button class="ra ra-edit" title="Editar trámite" onclick="event.stopPropagation();openEditModal(${r.id})">
<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="event.stopPropagation();showHistorial(${r.id}, '${esc(r.tramite).replace(/'/g,'&#39;')}')">
<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="event.stopPropagation();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="event.stopPropagation();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('');
}
['search','fStatus','fAbogado','fInscribe','fAltera'].forEach(id=>{
document.getElementById(id).addEventListener('input',()=>render());
document.getElementById(id).addEventListener('change',()=>render());
});
// ── ROW ACTIONS ──────────────────────────────────────────────────────────
function duplicateRow(id) {
const r = allRows.find(r => r.id === id);
if (!r) return;
// Abrir modal pre-poblada con copia (sin id → al guardar crea uno nuevo)
editingId = null;
const draft = Object.assign({}, r, {
tramite: r.tramite ? 'Copia — ' + r.tramite : '',
estatus: 'Solicitado',
entrada: '',
});
document.getElementById('edit-modal-title').textContent = 'Duplicar trámite';
document.getElementById('edit-modal-body').innerHTML = buildEditForm(draft);
document.getElementById('edit-modal-overlay').classList.add('open');
editModalOpen = true;
setTimeout(() => {
const f = document.getElementById('f-tramite');
if (f) { f.focus(); f.select(); }
}, 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 — prioriza el modal de edición si está abierto
document.addEventListener('keydown', e => {
if (e.key !== 'Escape') return;
if (editModalOpen) closeEditModal();
else 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','Entrada','Ú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, r.entrada || '',
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 (#, Trámite, Cliente, Abogado, Notaría,
// Inscribe, Altera, Instrucciones, Fecha, Estatus,
// Entrada, Últ.Act., Observaciones, Creado por)
ws['!cols'] = [
{wch:4},{wch:30},{wch:22},{wch:18},{wch:12},
{wch:10},{wch:12},{wch:35},{wch:18},{wch:16},
{wch:14},{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)
// [#, Trámite, Cliente, Abogado, Notaría, Inscribe, Altera, Estatus, Entrada, Observaciones]
const pdfCols = [0,1,2,3,4,5,6,9,10,12];
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>