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).
This commit is contained in:
Soporte ZH Consultores
2026-05-11 20:12:46 +00:00
parent 1bb63f12f6
commit 3a342cb6a8
+286 -226
View File
@@ -78,6 +78,87 @@ header {
}
.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);
@@ -357,6 +438,11 @@ tr.editing-row td textarea.editable {
<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">
@@ -364,6 +450,7 @@ tr.editing-row td textarea.editable {
<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>
@@ -412,7 +499,7 @@ tr.editing-row td textarea.editable {
<option value="">Todos</option><option>Sí</option><option>No</option>
</select>
</div>
<button class="btn-new" onclick="newRow()">
<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>
@@ -497,6 +584,21 @@ tr.editing-row td textarea.editable {
</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'];
@@ -507,7 +609,9 @@ const POLL_MS = 15000;
let allRows = [];
let authUser = { email:'', name:'' };
let pollTimer = null;
let isEditing = false; // true mientras hay fila nueva sin guardar
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) {
@@ -561,9 +665,8 @@ async function loadAll(showOverlay=false) {
const rows = await apiFetch('GET','list');
if (rows === null) return;
allRows = rows || [];
// No re-renderizar si el usuario está editando (fila nueva o existente).
// Los datos quedan en memoria; se aplicarán al salir de edición.
if (isEditing || editingRows.size > 0) {
// 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 {
@@ -578,9 +681,9 @@ async function loadAll(showOverlay=false) {
}
}
// Si el poll trajo datos mientras se estaba editando, aplicarlos al terminar
// Si el poll trajo datos durante una edición, aplicarlos al cerrar el modal
function maybeFlushRefresh() {
if (pendingRefresh && !isEditing && editingRows.size === 0) {
if (pendingRefresh && !editModalOpen) {
pendingRefresh = false;
render();
setSyncState('ok','Sincronizado');
@@ -593,19 +696,17 @@ async function saveRow(row) {
const method = row.id ? 'PUT' : 'POST';
const action = row.id ? 'update' : 'create';
const saved = await apiFetch(method, action, row);
if (!saved) return;
// Reemplazar fila temporal (id null) o actualizar existente
const nullIdx = allRows.findIndex(r=>r.id===null);
if (nullIdx >= 0) allRows.splice(nullIdx, 1);
if (!saved) return null;
const idx = allRows.findIndex(r=>r.id===saved.id);
if (idx>=0) allRows[idx]=saved; else allRows.unshift(saved);
isEditing = false; // fila guardada, reanudar polling
setSyncState('ok','Guardado');
toast('Trámite guardado correctamente');
render();
return saved;
} catch(e) {
setSyncState('error','Error al guardar');
toast('Error al guardar: '+e.message,'error');
throw e;
}
}
@@ -624,46 +725,154 @@ async function deleteRow(id) {
}
}
function saveNewRow(btn) {
const tr = btn.closest('tr');
const fields = getRowData(tr);
if (!fields.tramite || !fields.tramite.trim()) {
// highlight tramite field
const inp = tr.querySelector('[data-field="tramite"]');
if (inp) {
inp.style.borderColor = '#ef5350';
inp.focus();
setTimeout(() => { inp.style.borderColor = ''; }, 2000);
}
toast('El nombre del trámite es requerido', 'error');
return;
}
const base = allRows.find(r => r.id === null) || {};
saveRow(Object.assign({}, base, fields, { id: null }));
}
function cancelNewRow() {
allRows = allRows.filter(r=>r.id!==null);
isEditing = false;
render();
}
function doLogout() {
if (confirm('¿Cerrar sesión?')) window.location.href = 'api.php?action=logout';
}
function newRow() {
isEditing = true;
allRows.unshift({ id:null, tramite:'', cliente:'', abogado:'Abrego', notaria:'',
inscribe:'Sí', altera_turno:'No', instrucciones:'',
fecha_solicitud: new Date().toISOString(), estatus:'Solicitado',
entrada:'', observaciones:'', creado_por: authUser.email });
render();
// ── 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 inp = document.querySelector('tr.data-row [data-field="tramite"]');
if (inp) inp.focus();
toast('Llena los campos y haz clic en Guardar · Ctrl+Enter para guardar rápido');
}, 50);
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() {
@@ -695,123 +904,42 @@ function render() {
empty.style.display='none';
tbody.innerHTML = filtered.map((r,i)=>{
const isNew = r.id === null;
const dis = isNew ? '' : 'disabled style="pointer-events:none;background:transparent;border-color:transparent;"';
return `
<tr class="data-row${r.estatus==='Inscrito'?' inscrito-row':''}" data-id="${r.id||''}">
<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><input class="editable" data-field="tramite" value="${esc(r.tramite)}" placeholder="Nombre del trámite" style="min-width:150px;" ${dis}></td>
<td><input class="editable" data-field="cliente" value="${esc(r.cliente)}" placeholder="Cliente" style="min-width:120px;" ${dis}></td>
<td>
<select class="editable" data-field="abogado" style="min-width:100px;" ${dis}>
${ABOGADOS.map(a=>`<option${r.abogado===a?' selected':''}>${a}</option>`).join('')}
</select>
</td>
<td><input class="editable" data-field="notaria" value="${esc(r.notaria)}" placeholder="Notaría" style="min-width:80px;" ${dis}></td>
<td style="text-align:center;">
<select class="editable" data-field="inscribe" style="width:52px;" ${dis}>
<option${r.inscribe==='Sí'?' selected':''}>Sí</option>
<option${r.inscribe==='No'?' selected':''}>No</option>
</select><br>
<span class="pill ${r.inscribe==='Sí'?'pill-si':'pill-no'}">${esc(r.inscribe)}</span>
</td>
<td style="text-align:center;">
<select class="editable" data-field="altera_turno" style="width:52px;" ${dis}>
<option${r.altera_turno==='Sí'?' selected':''}>Sí</option>
<option${r.altera_turno==='No'?' selected':''}>No</option>
</select><br>
<span class="pill ${r.altera_turno==='Sí'?'pill-alt':'pill-no'}">${esc(r.altera_turno)}</span>
</td>
<td style="min-width:160px;max-width:220px;">
<textarea class="editable" data-field="instrucciones" rows="2" placeholder="Instrucciones especiales…" ${dis}>${esc(r.instrucciones)}</textarea>
</td>
<td 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>
<select class="editable" data-field="estatus" style="min-width:120px;" ${dis}>
${ESTATUS.map(e=>`<option${r.estatus===e?' selected':''}>${e}</option>`).join('')}
</select><br>
<span class="badge ${badgeClass(r.estatus)}">${esc(r.estatus)}</span>
</td>
<td style="min-width:110px;">
<input class="editable" data-field="entrada" value="${esc(r.entrada||'')}"
placeholder="########/####" pattern="^\\d{1,8}/\\d{4}$" maxlength="13"
title="Hasta 8 dígitos, slash, y año de 4 dígitos. Ej: 12345678/2026"
style="min-width:100px;font-variant-numeric:tabular-nums;" ${dis}>
</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;">
<textarea class="editable" data-field="observaciones" rows="2" placeholder="Observaciones…" ${dis}>${esc(r.observaciones)}</textarea>
</td>
<td class="user-col" title="${esc(r.creado_por)}">${esc(r.creado_por||'—')}</td>
<td 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>
${r.id === null ? `
<div class="new-row-actions">
<button class="btn-save-row" onclick="saveNewRow(this)">Guardar</button>
<button class="btn-cancel-row" onclick="cancelNewRow()">✕</button>
</div>
` : `
<div class="row-actions" data-id="${r.id}">
<button class="ra ra-save" title="Guardar cambios" onclick="saveExistingRow(${r.id})">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>
</button>
<button class="ra ra-edit" title="Editar fila" onclick="toggleEditRow(${r.id}, this)">
<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="showHistorial(${r.id}, '${esc(r.tramite)}')">
<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="duplicateRow(${r.id})">
<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="deleteRow(${r.id})">
<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('');
tbody.querySelectorAll('.editable').forEach(el=>{
el.addEventListener('keydown', onKeydown);
// Mark row dirty when any field changes (for save indicator)
el.addEventListener('input', e => {
const tr = e.target.closest('tr');
const id = parseInt(tr.dataset.id) || null;
if (id) {
const saveBtn = tr.querySelector('.ra-save');
if (saveBtn) saveBtn.classList.add('pending');
}
});
});
}
function getRowData(tr) {
const f={};
tr.querySelectorAll('.editable').forEach(el=>{ f[el.dataset.field]=el.value; });
return f;
}
function onFieldChange(e) {
const tr=e.target.closest('tr'); const id=parseInt(tr.dataset.id)||null;
const row=allRows.find(r=>r.id===id)||allRows[0];
saveRow(Object.assign({},row,getRowData(tr),{id}));
}
function onFieldBlur(e) {
// Blur no guarda — las filas nuevas usan botón Guardar explícito
// Las filas existentes se guardan con onFieldChange (change event)
}
function onKeydown(e) {
const tr = e.target.closest('tr');
const rowId = parseInt(tr.dataset.id) || null;
if (e.key === 'Escape' && !rowId) cancelNewRow();
if (e.key === 'Enter' && !rowId && e.ctrlKey) {
// Ctrl+Enter guarda la fila nueva
const btn = tr.querySelector('.btn-save-row');
if (btn) saveNewRow(btn);
}
}
['search','fStatus','fAbogado','fInscribe','fAltera'].forEach(id=>{
@@ -820,93 +948,23 @@ function onKeydown(e) {
});
// ── ROW ACTIONS ──────────────────────────────────────────────────────────
let editingRows = new Set(); // track which rows are in edit mode
let pendingRefresh = false; // poll trajo datos mientras alguien editaba
function toggleEditRow(id, btn) {
const tr = document.querySelector(`tr[data-id="${id}"]`);
if (!tr) return;
const isEditing = editingRows.has(id);
if (isEditing) {
// Exit edit mode without saving
editingRows.delete(id);
tr.classList.remove('editing-row');
// Restore original values from allRows
const r = allRows.find(r => r.id === id);
if (r) {
tr.querySelectorAll('.editable').forEach(el => {
el.value = r[el.dataset.field] || '';
el.disabled = true;
el.style.pointerEvents = 'none';
el.style.background = 'transparent';
el.style.borderColor = 'transparent';
});
}
btn.title = 'Editar fila';
btn.style.background = '';
btn.style.borderColor = '';
// Aplicar cambios remotos pendientes si los hubo
maybeFlushRefresh();
} else {
// Enter edit mode
editingRows.add(id);
tr.classList.add('editing-row');
tr.querySelectorAll('.editable').forEach(el => {
el.disabled = false;
el.style.pointerEvents = '';
el.style.background = '';
el.style.borderColor = '';
});
btn.title = 'Cancelar edición';
btn.style.background = '#FAEEDA';
btn.style.borderColor = '#fac775';
// Focus first text input
const first = tr.querySelector('input.editable');
if (first) first.focus();
}
}
function saveExistingRow(id) {
const tr = document.querySelector(`tr[data-id="${id}"]`);
if (!tr) return;
const base = allRows.find(r => r.id === id);
if (!base) return;
const fields = getRowData(tr);
if (!fields.tramite || !fields.tramite.trim()) {
const inp = tr.querySelector('[data-field="tramite"]');
if (inp) { inp.style.borderColor='#ef5350'; inp.focus(); setTimeout(()=>{inp.style.borderColor='';},2000); }
toast('El nombre del trámite es requerido','error');
return;
}
// Exit edit mode
editingRows.delete(id);
tr.classList.remove('editing-row');
const saveBtn = tr.querySelector('.ra-save');
if (saveBtn) saveBtn.classList.remove('pending');
// saveRow llama a render() al terminar, lo que aplica también cambios remotos
pendingRefresh = false;
saveRow(Object.assign({}, base, fields, { id }));
}
function duplicateRow(id) {
const r = allRows.find(r => r.id === id);
if (!r) return;
isEditing = true;
const dup = Object.assign({}, r, {
id: null,
// 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 : '',
fecha_solicitud: new Date().toISOString(),
estatus: 'Solicitado',
creado_por: authUser.email
entrada: '',
});
allRows.unshift(dup);
render();
// Focus tramite of new row
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 inp = document.querySelector('tr[data-id=""] [data-field="tramite"], tr[data-id="0"] [data-field="tramite"]');
if (inp) { inp.focus(); inp.select(); }
toast('Trámite duplicado — revisa los datos y haz clic en Guardar');
const f = document.getElementById('f-tramite');
if (f) { f.focus(); f.select(); }
}, 60);
}
@@ -956,9 +1014,11 @@ function closeModal(e) {
if (e.target === document.getElementById('modal-overlay')) closeHistorial();
}
// Cerrar con Escape
// Cerrar con Escape — prioriza el modal de edición si está abierto
document.addEventListener('keydown', e => {
if (e.key === 'Escape') closeHistorial();
if (e.key !== 'Escape') return;
if (editModalOpen) closeEditModal();
else closeHistorial();
});
// ── EXPORT ───────────────────────────────────────────────────────────────