- Todos los strings de branding actualizados: - "AALN" / "AALN Law" → "AAL" - "Arias, Abrego, López & Noriega" → "Arias, Abrego & López" - "M. Noriega" (placeholder en sample card del landing) → "M. Arias" - Website link cambia de aalnlaw.com a aal-law.com en landing y footer del portal - Filenames de exportación a Excel/PDF: *_AALN → *_AAL - Footer del PDF compartido (DonnaExport.toPDF) actualizado - Mensajes de denegación de OAuth callback actualizados - README refleja nuevo nombre y URL del firm OAuth sigue aceptando @aal-law.com y @aalnlaw.com como dominios válidos (configurado en /etc/donna/donna.env) para no romper logins durante la transición de correos.
1060 lines
45 KiB
HTML
1060 lines
45 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 — AAL</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; }
|
|
.btn-gear, .btn-clock {
|
|
display:inline-flex; align-items:center; justify-content:center;
|
|
width:34px; height:34px; border:1px solid rgba(255,255,255,0.18);
|
|
border-radius:8px; color:rgba(255,255,255,0.85);
|
|
transition:all .15s; text-decoration:none; cursor:pointer;
|
|
font-family:'DM Sans',sans-serif;
|
|
}
|
|
.btn-gear:hover, .btn-clock:hover { background:var(--gold); border-color:var(--gold); color:var(--navy); }
|
|
.btn-gear svg, .btn-clock svg { width:16px; height:16px; }
|
|
|
|
/* 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;
|
|
}
|
|
</style>
|
|
<link rel="stylesheet" href="/assets/donna-export.css">
|
|
</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">AAL</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 id="export-mount"></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 · P.H. Midtown, Piso 16, Of. 1605, Calle 74 Este, San Francisco, Ciudad de Panamá · 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 src="/assets/donna-export.js"></script>
|
|
<script>
|
|
const ABOGADOS = (window.DONNA && window.DONNA.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
|
|
let firstLoadDone = false; // deep-link ?edit=ID solo aplica en la primera carga
|
|
|
|
function esc(s) { return (s||'').replace(/&/g,'&').replace(/"/g,'"').replace(/</g,'<').replace(/>/g,'>'); }
|
|
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');
|
|
if (!firstLoadDone) {
|
|
firstLoadDone = true;
|
|
checkDeepLink();
|
|
}
|
|
}
|
|
} 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';
|
|
}
|
|
|
|
// Si el usuario llegó desde el Calendario con ?edit=ID, abre la modal sobre esa fila
|
|
function checkDeepLink() {
|
|
const params = new URLSearchParams(location.search);
|
|
const raw = params.get('edit');
|
|
if (!raw) return;
|
|
const id = parseInt(raw, 10);
|
|
if (Number.isNaN(id)) return;
|
|
const r = allRows.find(x => x.id === id);
|
|
if (r) {
|
|
openEditModal(id);
|
|
} else {
|
|
toast('Trámite #' + id + ' no encontrado','error');
|
|
}
|
|
// Limpiar la URL para no re-disparar en refresh
|
|
history.replaceState({}, '', location.pathname);
|
|
}
|
|
|
|
// ── 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,''')}')">
|
|
<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 ───────────────────────────────────────────────────────────────
|
|
document.getElementById('export-mount').innerHTML = DonnaExport.buttonHtml();
|
|
DonnaExport.attach('export-wrap', { onExcel: exportExcel, onPDF: exportPDF });
|
|
|
|
const EXP_HEADERS = ['#','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'];
|
|
const EXP_WIDTHS = [4,30,22,18,12,10,12,35,18,16,14,18,30,22];
|
|
const EXP_PDF_COLS = [0,1,2,3,4,5,6,9,10,12];
|
|
const EXP_STATUS_COLORS = {
|
|
'Solicitado': ['#F1EFE8','#5F5E5A'],
|
|
'Recibido': ['#E6F1FB','#0C447C'],
|
|
'En Proceso': ['#FAEEDA','#7A4F10'],
|
|
'Protocolizado': ['#EAF3DE','#2E5E0E'],
|
|
'En Registro': ['#E1F5EE','#085041'],
|
|
'Inscrito': ['#EEEDFE','#3C3489'],
|
|
};
|
|
function expRows() {
|
|
return getFiltered().map((r, i) => [
|
|
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(){
|
|
DonnaExport.toExcel({
|
|
filename: 'Control_Escrituras_AAL', sheetName: 'Escrituras',
|
|
headers: EXP_HEADERS, rows: expRows(), columnWidths: EXP_WIDTHS,
|
|
onEmpty: () => toast('No hay datos para exportar','error'),
|
|
});
|
|
}
|
|
function exportPDF(){
|
|
DonnaExport.toPDF({
|
|
filename: 'Control_Escrituras_AAL',
|
|
title: 'Control de Estatus de Escrituras Públicas',
|
|
headers: EXP_HEADERS, rows: expRows(),
|
|
pdfCols: EXP_PDF_COLS, statusColIndex: 9, statusColors: EXP_STATUS_COLORS,
|
|
onEmpty: () => toast('No hay datos para exportar','error'),
|
|
});
|
|
}
|
|
|
|
loadAll(true).then(()=>{ pollTimer=setInterval(()=>loadAll(false), POLL_MS); });
|
|
</script>
|
|
</body>
|
|
</html>
|