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:
+304
-244
@@ -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,11 +438,17 @@ tr.editing-row td textarea.editable {
|
||||
<div class="toast" id="toast"></div>
|
||||
|
||||
<header>
|
||||
<div class="logo">
|
||||
<div class="logo-mark">A</div>
|
||||
<div class="logo-text">
|
||||
<span class="logo-firm">AALN Law</span>
|
||||
<span class="logo-tool">Control de Escrituras Públicas</span>
|
||||
<div 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">
|
||||
@@ -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'];
|
||||
@@ -504,10 +606,12 @@ const ESTATUS = ['Solicitado','Recibido','En Proceso','Protocolizado','En Regis
|
||||
const API = 'api.php';
|
||||
const POLL_MS = 15000;
|
||||
|
||||
let allRows = [];
|
||||
let authUser = { email:'', name:'' };
|
||||
let pollTimer = null;
|
||||
let isEditing = false; // true mientras hay fila nueva sin guardar
|
||||
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,'&').replace(/"/g,'"').replace(/</g,'<').replace(/>/g,'>'); }
|
||||
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:'',
|
||||
// ── 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:'',
|
||||
fecha_solicitud: new Date().toISOString(), estatus:'Solicitado',
|
||||
entrada:'', observaciones:'', creado_por: authUser.email });
|
||||
render();
|
||||
setTimeout(()=>{
|
||||
const inp = document.querySelector('tr.data-row [data-field="tramite"]');
|
||||
if (inp) inp.focus();
|
||||
toast('Llena los campos y haz clic en Guardar · Ctrl+Enter para guardar rápido');
|
||||
}, 50);
|
||||
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)">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
|
||||
</button>
|
||||
<button class="ra ra-hist" title="Ver historial" onclick="showHistorial(${r.id}, '${esc(r.tramite)}')">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
|
||||
</button>
|
||||
<button class="ra ra-dup" title="Duplicar trámite" onclick="duplicateRow(${r.id})">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
||||
</button>
|
||||
<button class="ra ra-del" title="Eliminar trámite" onclick="deleteRow(${r.id})">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6M14 11v6"/><path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
`}
|
||||
<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('');
|
||||
|
||||
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 ───────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user