fix(tramites): buscador conserva el foco al tipear

Cada tecla en el input de búsqueda disparaba renderPanel() que
reescribía panel.innerHTML — incluyendo el input mismo — así que el
navegador descartaba el foco (y la posición del cursor). Los usuarios
tenían que hacer clic después de cada letra.

Cambios:
- Nuevo filterState por depto en memoria (no lee del DOM al filtrar)
- renderPanel() reconstruye todo solo cuando cambia el conjunto de
  datos (carga inicial, guardar, borrar, cambio de tab)
- Nuevo renderPanelBody() que solo repinta contadores + tbody
- Los handlers de search/status/abogado/esfera actualizan
  filterState y llaman renderPanelBody, dejando el toolbar intacto

Efecto: al tipear en el buscador, el input mantiene foco y cursor.
This commit is contained in:
Soporte ZH Consultores
2026-07-09 09:30:09 -05:00
parent 7e8500c7fb
commit dbeacdde7b
+87 -27
View File
@@ -372,49 +372,64 @@ function checkDeepLink(){
function renderAll(){['adm','corp','mig','lit'].forEach(renderPanel);}
function renderPanel(dept){
const panel=document.getElementById('panel-'+dept);
const rows=data[dept];
const statuses=STATUS_MAP[dept];
const search=(panel.querySelector('.search-input')||{value:''}).value.toLowerCase();
const fStatus=(panel.querySelector('.f-status')||{value:''}).value;
const fAbog=(panel.querySelector('.f-abog')||{value:''}).value;
const fEsfera=(panel.querySelector('.f-esfera')||{value:''}).value;
// Estado de filtros por depto (persiste entre re-renders — no vive en el DOM,
// para que actualizar el <tbody> no destruya el input de búsqueda).
const filterState = {
adm: { search:'', status:'', abog:'', esfera:'' },
corp: { search:'', status:'', abog:'', esfera:'' },
mig: { search:'', status:'', abog:'', esfera:'' },
lit: { search:'', status:'', abog:'', esfera:'' },
};
let filtered=rows.filter(r=>{
const txt=((r.nombre||r.tipoTramite||r.tipoProceso||'')+(r.cliente||'')+(r.abogado||'')+(r.id||'')).toLowerCase();
if(search && !txt.includes(search)) return false;
if(fStatus && r.estatus!==fStatus) return false;
if(fAbog && r.abogado!==fAbog) return false;
if(dept==='lit' && fEsfera && r.esfera!==fEsfera) return false;
function computeFiltered(dept){
const rows = data[dept];
const f = filterState[dept];
const q = f.search.toLowerCase();
const filtered = rows.filter(r => {
const txt = ((r.nombre||r.tipoTramite||r.tipoProceso||'')+(r.cliente||'')+(r.abogado||'')+(r.id||'')).toLowerCase();
if (q && !txt.includes(q)) return false;
if (f.status && r.estatus !== f.status) return false;
if (f.abog && r.abogado !== f.abog) return false;
if (dept === 'lit' && f.esfera && r.esfera !== f.esfera) return false;
return true;
});
const total=rows.length;
const activos=rows.filter(r=>r.estatus!=='Culminado'&&r.estatus!=='Facturado').length;
const culminados=rows.filter(r=>r.estatus==='Culminado'||r.estatus==='Facturado').length;
window.__tramFiltered = window.__tramFiltered || {};
window.__tramFiltered[dept] = filtered;
return filtered;
}
// Reconstruye toda la pestaña. Se usa solo cuando cambia el conjunto de datos
// (carga inicial, guardar/borrar, cambio de tab). NO se llama al teclear.
function renderPanel(dept){
const panel = document.getElementById('panel-'+dept);
const rows = data[dept];
const statuses = STATUS_MAP[dept];
const f = filterState[dept];
const total = rows.length;
const activos = rows.filter(r=>r.estatus!=='Culminado'&&r.estatus!=='Facturado').length;
const culminados = rows.filter(r=>r.estatus==='Culminado'||r.estatus==='Facturado').length;
const filtered = computeFiltered(dept);
panel.innerHTML=`
<div class="counts">
<div class="counts" id="counts-${dept}">
<div class="count-card"><b>${total}</b>Total</div>
<div class="count-card"><b>${activos}</b>Activos</div>
<div class="count-card"><b>${culminados}</b>Cerrados</div>
</div>
<div class="toolbar">
<input class="search-input" placeholder="Buscar..." oninput="renderPanel('${dept}')" value="${esc(search)}">
<select class="f-status" onchange="renderPanel('${dept}')">
<input class="search-input" placeholder="Buscar..." value="${esc(f.search)}">
<select class="f-status">
<option value="">— Estatus —</option>
${statuses.map(s=>`<option${fStatus===s?' selected':''}>${s}</option>`).join('')}
${statuses.map(s=>`<option${f.status===s?' selected':''}>${s}</option>`).join('')}
</select>
<select class="f-abog" onchange="renderPanel('${dept}')">
<select class="f-abog">
<option value="">— Abogado —</option>
${ABOGADOS.map(a=>`<option${fAbog===a?' selected':''}>${a}</option>`).join('')}
${ABOGADOS.map(a=>`<option${f.abog===a?' selected':''}>${a}</option>`).join('')}
</select>
${dept==='lit'?`<select class="f-esfera" onchange="renderPanel('${dept}')">
${dept==='lit'?`<select class="f-esfera">
<option value="">— Esfera —</option>
<option${fEsfera==='Civil'?' selected':''}>Civil</option><option${fEsfera==='Penal'?' selected':''}>Penal</option>
<option${f.esfera==='Civil'?' selected':''}>Civil</option><option${f.esfera==='Penal'?' selected':''}>Penal</option>
</select>`:''}
${CAN_CREATE ? `<button class="btn btn-primary" onclick="openModal('${dept}',null)">+ Nuevo</button>` : ''}
${DonnaExport.buttonHtml('export-wrap-'+dept)}
@@ -431,6 +446,51 @@ function renderPanel(dept){
onExcel: () => tramExportExcel(dept),
onPDF: () => tramExportPDF(dept),
});
// Handlers de filtro: al cambiar solo repintan la tabla, no el toolbar.
// Así el input de búsqueda conserva foco y posición del cursor al tipear.
const searchEl = panel.querySelector('.search-input');
if (searchEl) searchEl.addEventListener('input', e => {
filterState[dept].search = e.target.value;
renderPanelBody(dept);
});
const statusEl = panel.querySelector('.f-status');
if (statusEl) statusEl.addEventListener('change', e => {
filterState[dept].status = e.target.value;
renderPanelBody(dept);
});
const abogEl = panel.querySelector('.f-abog');
if (abogEl) abogEl.addEventListener('change', e => {
filterState[dept].abog = e.target.value;
renderPanelBody(dept);
});
const esferaEl = panel.querySelector('.f-esfera');
if (esferaEl) esferaEl.addEventListener('change', e => {
filterState[dept].esfera = e.target.value;
renderPanelBody(dept);
});
}
// Solo actualiza contadores y tbody — deja el toolbar intacto.
function renderPanelBody(dept){
const rows = data[dept];
const filtered = computeFiltered(dept);
const counts = document.getElementById('counts-'+dept);
if (counts) {
const total = rows.length;
const activos = rows.filter(r=>r.estatus!=='Culminado'&&r.estatus!=='Facturado').length;
const culminados = rows.filter(r=>r.estatus==='Culminado'||r.estatus==='Facturado').length;
counts.innerHTML =
`<div class="count-card"><b>${total}</b>Total</div>`+
`<div class="count-card"><b>${activos}</b>Activos</div>`+
`<div class="count-card"><b>${culminados}</b>Cerrados</div>`;
}
const tbody = document.getElementById('tbody-'+dept);
if (tbody) {
tbody.innerHTML = filtered.length
? filtered.map(r=>buildRow(dept,r)).join('')
: `<tr><td colspan="20"><div class="empty-state">Sin registros. Haz clic en <b>+ Nuevo</b> para agregar.</div></td></tr>`;
}
}
// ── EXPORT (Trámites) ────────────────────────────────────────────────────