From ded060e1614c16fcd98fc60c158cb5ce2a1bbd19 Mon Sep 17 00:00:00 2001 From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com> Date: Tue, 7 Apr 2026 15:12:22 -0500 Subject: [PATCH] lab_formularios --- api/lab/get_formularios.php | 6 +- classes/lab/Formulario.php | 32 +++- lab_formularios.php | 293 ++++++++++++++---------------------- 3 files changed, 147 insertions(+), 184 deletions(-) diff --git a/api/lab/get_formularios.php b/api/lab/get_formularios.php index 3c62720..4921304 100644 --- a/api/lab/get_formularios.php +++ b/api/lab/get_formularios.php @@ -17,11 +17,15 @@ try { if (!empty($_GET['paciente_id'])) $filtros['paciente_id'] = (int)$_GET['paciente_id']; if (!empty($_GET['estado'])) $filtros['estado'] = $_GET['estado']; if (!empty($_GET['fecha_desde'])) $filtros['fecha_desde'] = $_GET['fecha_desde']; + if (!empty($_GET['fecha_hasta'])) $filtros['fecha_hasta'] = $_GET['fecha_hasta']; + if (!empty($_GET['paciente'])) $filtros['paciente'] = trim($_GET['paciente']); // Enfermero solo ve los suyos if (userRole() === 'enfermero') { $filtros['enviado_por'] = adminId(); } - jsonOk(['data' => $form->listarEnvios($filtros)]); + $pagina = max(1, (int)($_GET['page'] ?? 1)); + $porPagina = !empty($_GET['export']) ? 9999 : max(1, min(100, (int)($_GET['limit'] ?? 20))); + jsonOk($form->listarEnvios($filtros, $pagina, $porPagina)); } // Plantilla específica diff --git a/classes/lab/Formulario.php b/classes/lab/Formulario.php index 852fc2f..2d4f533 100644 --- a/classes/lab/Formulario.php +++ b/classes/lab/Formulario.php @@ -173,7 +173,7 @@ class Formulario { /** * Listar envíos (admin/enfermero). */ - public function listarEnvios(array $filtros = []): array { + public function listarEnvios(array $filtros = [], int $pagina = 1, int $porPagina = 20): array { $where = ['1=1']; $params = []; if (!empty($filtros['formulario_id'])) { $where[] = 'e.formulario_id = ?'; $params[] = $filtros['formulario_id']; } @@ -181,8 +181,22 @@ class Formulario { if (!empty($filtros['enviado_por'])) { $where[] = 'e.enviado_por = ?'; $params[] = $filtros['enviado_por']; } if (!empty($filtros['estado'])) { $where[] = 'e.estado = ?'; $params[] = $filtros['estado']; } if (!empty($filtros['fecha_desde'])) { $where[] = 'DATE(e.created_at) >= ?'; $params[] = $filtros['fecha_desde']; } - $w = implode(' AND ', $where); - return $this->db->fetchAll(" + if (!empty($filtros['fecha_hasta'])) { $where[] = 'DATE(e.created_at) <= ?'; $params[] = $filtros['fecha_hasta']; } + if (!empty($filtros['paciente'])) { $where[] = 'p.nombre_completo LIKE ?'; $params[] = '%' . $filtros['paciente'] . '%'; } + $w = implode(' AND ', $where); + $offset = ($pagina - 1) * $porPagina; + + $total = $this->db->fetch( + "SELECT COUNT(*) AS n + FROM lab_form_envios e + JOIN lab_formularios f ON f.id = e.formulario_id + LEFT JOIN lab_pacientes p ON p.id = e.paciente_id + LEFT JOIN admin_users u ON u.id = e.enviado_por + WHERE $w", + $params + )['n'] ?? 0; + + $rows = $this->db->fetchAll(" SELECT e.*, f.nombre AS form_nombre, f.categoria, f.esquema, p.nombre_completo AS paciente_nombre, u.full_name AS enviado_por_nombre @@ -192,8 +206,16 @@ class Formulario { LEFT JOIN admin_users u ON u.id = e.enviado_por WHERE $w ORDER BY e.created_at DESC - LIMIT 200 - ", $params); + LIMIT ? OFFSET ? + ", array_merge($params, [$porPagina, $offset])); + + return [ + 'data' => $rows, + 'total' => (int)$total, + 'pagina' => $pagina, + 'por_pagina' => $porPagina, + 'paginas' => (int)ceil($total / max(1, $porPagina)), + ]; } // ───────────────────────────────────────────────────────────────────────── diff --git a/lab_formularios.php b/lab_formularios.php index 9bf2c85..aaca33d 100644 --- a/lab_formularios.php +++ b/lab_formularios.php @@ -29,6 +29,10 @@ $puedeEscribir = $esAdmin && hasModuleWrite('lab_formularios'); transition:box-shadow .2s; } .form-card:hover { box-shadow:0 3px 12px rgba(0,0,0,.1); } + /* ── Altura uniforme filtros ─────────────────────── */ + #panel-envios .form-control-sm, + #panel-envios .form-select-sm { height:31px; padding-top:.25rem; padding-bottom:.25rem; } + /* ── Colores estado envío ───────────────────────── */ .badge-pendiente { background:#6c757d; } .badge-completado { background:#0d6efd; } @@ -401,7 +405,7 @@ const tabs = { document.querySelectorAll('#formTabs .nav-link').forEach((a,i) => a.classList.toggle('active', (tab==='lista') === (i===0)) ); - if (tab === 'envios') envios.cargar(); + if (tab === 'envios') { envios.poblarSelectFormulario(); envios.cargar(1); } } }; @@ -508,182 +512,130 @@ const ESTADO_ENV = { firmado: { cls:'badge-firmado', icon:'✍️' }, expirado: { cls:'badge-expirado', icon:'⏰' }, }; -const POR_PAGINA = 15; +const POR_PAGINA = 20; const envios = { - _data: [], - _filtrado: [], - _pagina: 1, + _pagina: 1, + _total: 0, + _paginas: 1, + _paginaData: [], + _debTimer: null, - async cargar() { - // Mostrar spinner, ocultar tabla + _params() { + return new URLSearchParams({ + envios: 1, + page: this._pagina, + limit: POR_PAGINA, + fecha_desde: $('env-f-desde').value || '', + fecha_hasta: $('env-f-hasta').value || '', + paciente: $('env-f-paciente').value.trim() || '', + formulario_id: $('env-f-formulario').value || '', + estado: $('env-f-estado').value || '', + }); + }, + + async cargar(pag = 1) { + this._pagina = pag; const spinner = $('env-spinner'); - const tabla = $('env-tabla-wrap'); + const wrap = $('env-tabla-wrap'); if (spinner) spinner.style.display = ''; - if (tabla) tabla.style.display = 'none'; - - const r = await fetch('api/lab/get_formularios.php?envios=1'); - const d = await r.json(); - this._data = d.data || []; - this._pagina = 1; - this._poblarSelectFormulario(); - this._aplicarFiltros(); - - if (spinner) spinner.style.display = 'none'; - if (tabla) tabla.style.display = ''; + if (wrap) wrap.style.display = 'none'; + try { + const r = await fetch('api/lab/get_formularios.php?' + this._params()); + const d = await r.json(); + this._paginaData = d.data || []; + this._total = d.total || 0; + this._paginas = d.paginas || 1; + this._render(); + } catch(e) { + const tb = $('tbody-envios'); + if (tb) tb.innerHTML = 'Error al cargar envíos'; + } finally { + if (spinner) spinner.style.display = 'none'; + if (wrap) wrap.style.display = ''; + } }, filtrar() { - this._pagina = 1; - this._aplicarFiltros(); + clearTimeout(this._debTimer); + this._debTimer = setTimeout(() => this.cargar(1), 350); }, limpiarFiltros() { - $('env-f-desde').value = ''; - $('env-f-hasta').value = ''; - $('env-f-paciente').value = ''; - $('env-f-formulario').value = ''; - $('env-f-estado').value = ''; - this.filtrar(); + $('env-f-desde').value = ''; + $('env-f-hasta').value = ''; + $('env-f-paciente').value = ''; + $('env-f-formulario').value = ''; + $('env-f-estado').value = ''; + this.cargar(1); }, - _poblarSelectFormulario() { + poblarSelectFormulario() { const sel = $('env-f-formulario'); - if (!sel) return; - const valorActual = sel.value; - sel.innerHTML = ''; - const vistos = new Set(); - this._data.forEach(e => { - if (e.formulario_id && !vistos.has(e.formulario_id)) { - vistos.add(e.formulario_id); - const opt = document.createElement('option'); - opt.value = e.formulario_id; - opt.textContent = e.form_nombre || ('Formulario #' + e.formulario_id); - sel.appendChild(opt); - } + if (!sel || sel.options.length > 1) return; + (_formularios || []).forEach(f => { + const opt = document.createElement('option'); + opt.value = f.id; + opt.textContent = f.nombre || ('Formulario #' + f.id); + sel.appendChild(opt); }); - if (valorActual) sel.value = valorActual; }, - _aplicarFiltros() { - const desde = $('env-f-desde').value; - const hasta = $('env-f-hasta').value; - const paciente = $('env-f-paciente').value.trim().toLowerCase(); - const formId = $('env-f-formulario').value; - const estadoFilt = $('env-f-estado').value; + async exportarExcel() { + const btnExcel = document.querySelector('[onclick="envios.exportarExcel()"]'); + if (btnExcel) { btnExcel.disabled = true; btnExcel.innerHTML = 'Exportando…'; } + let rows = []; + try { + const p = this._params(); + p.set('export', '1'); p.delete('page'); p.delete('limit'); + const r = await fetch('api/lab/get_formularios.php?' + p); + const d = await r.json(); + rows = d.data || []; + } finally { + if (btnExcel) { btnExcel.disabled = false; btnExcel.innerHTML = 'Excel'; } + } + if (!rows.length) { showToast('Sin datos para exportar', 'warning'); return; } - this._filtrado = this._data.filter(e => { - const fecha = (e.created_at || '').slice(0, 10); - if (desde && fecha < desde) return false; - if (hasta && fecha > hasta) return false; - if (paciente && !(e.paciente_nombre || '').toLowerCase().includes(paciente)) return false; - if (formId && String(e.formulario_id) !== String(formId)) return false; - if (estadoFilt && e.estado !== estadoFilt) return false; - return true; - }); - - // badge de pendientes sobre el total sin filtrar - const badgePend = $('badge-envios'); - const pend = this._data.filter(e => e.estado === 'pendiente').length; - if (pend) { badgePend.textContent = pend; badgePend.style.display = ''; } - else badgePend.style.display = 'none'; - - this._render(); - }, - - exportarExcel() { - const rows = this._filtrado; - if (!rows.length) { showToast('Sin datos para exportar'); return; } - - // Columnas fijas const fixedCols = ['Formulario','Categoría','Paciente','Estado','Fecha envío','Fecha completado']; - - // Recolectar todas las columnas dinámicas (campos del esquema) en orden de aparición - const fieldMap = new Map(); // label → id (primer visto) + const fieldMap = new Map(); rows.forEach(e => { - try { - const esquema = JSON.parse(e.esquema || '[]'); - esquema.forEach(c => { - if (!['separador','firma','firma_profesional','parrafo', - 'parrafo_inline','lista_marcable'].includes(c.tipo) - && c.id && c.label && !fieldMap.has(c.label)) { - fieldMap.set(c.label, c.id); - } - }); - } catch(_) {} + try { JSON.parse(e.esquema||'[]').forEach(c => { + if (!['separador','firma','firma_profesional','parrafo','parrafo_inline','lista_marcable'].includes(c.tipo) + && c.id && c.label && !fieldMap.has(c.label)) fieldMap.set(c.label, c.id); + }); } catch(_) {} }); - - const fieldCols = [...fieldMap.entries()]; // [[label, id], ...] - const allCols = [...fixedCols, ...fieldCols.map(([l]) => l)]; - - const csvRows = [allCols]; - + const fieldCols = [...fieldMap.entries()]; + const csvRows = [[...fixedCols, ...fieldCols.map(([l])=>l)]]; rows.forEach(e => { - let cliente = {}, prefill = {}; - try { cliente = JSON.parse(e.datos_cliente || '{}'); } catch(_) {} - try { prefill = JSON.parse(e.datos_prefilled || '{}'); } catch(_) {} - const todos = { ...prefill, ...cliente }; - - // Para este envío construir mapa label → id - const labelToId = new Map(); - try { - JSON.parse(e.esquema || '[]').forEach(c => { - if (c.id && c.label) labelToId.set(c.label, c.id); - }); - } catch(_) {} - - const fixed = [ - e.form_nombre || '', - e.categoria || '', - e.paciente_nombre || '', - e.estado || '', - (e.created_at || '').slice(0, 16).replace('T', ' '), - (e.completado_en || '').slice(0, 16).replace('T', ' '), - ]; - - const campos = fieldCols.map(([label, defaultId]) => { - const id = labelToId.get(label) || defaultId; - const val = todos[id]; - if (val === undefined || val === null) return ''; - return Array.isArray(val) ? val.join('; ') : String(val); - }); - - csvRows.push([...fixed, ...campos]); + let cliente={}, prefill={}; + try { cliente=JSON.parse(e.datos_cliente||'{}'); } catch(_) {} + try { prefill=JSON.parse(e.datos_prefilled||'{}'); } catch(_) {} + const todos = {...prefill,...cliente}; + const lmap = new Map(); + try { JSON.parse(e.esquema||'[]').forEach(c=>{if(c.id&&c.label)lmap.set(c.label,c.id);}); } catch(_) {} + csvRows.push([ + e.form_nombre||'', e.categoria||'', e.paciente_nombre||'', e.estado||'', + (e.created_at||'').slice(0,16).replace('T',' '), (e.completado_en||'').slice(0,16).replace('T',' '), + ...fieldCols.map(([l,d])=>{ const v=todos[lmap.get(l)||d]; return v==null?'':(Array.isArray(v)?v.join('; '):String(v)); }), + ]); }); - - // Generar CSV con BOM UTF-8 para Excel - const csv = '\uFEFF' + csvRows.map(row => - row.map(v => '"' + String(v).replace(/"/g, '""') + '"').join(',') - ).join('\r\n'); - - const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }); + const csv = '\uFEFF' + csvRows.map(r=>r.map(v=>'"'+String(v).replace(/"/g,'""')+'"').join(',')).join('\r\n'); + const blob = new Blob([csv],{type:'text/csv;charset=utf-8;'}); const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = 'envios_formularios_' + new Date().toISOString().slice(0, 10) + '.csv'; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); + const a = Object.assign(document.createElement('a'),{href:url,download:'envios_formularios_'+new Date().toISOString().slice(0,10)+'.csv'}); + document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); showToast('✅ Excel descargado — ' + rows.length + ' registro(s)', 'success', 4000); }, _render() { const tb = $('tbody-envios'); - const total = this._filtrado.length; - - if (!total) { + if (!this._paginaData.length) { tb.innerHTML = 'Sin envíos para los filtros seleccionados'; $('env-pag-info').textContent = ''; $('env-paginacion').innerHTML = ''; return; } - - const totalPag = Math.ceil(total / POR_PAGINA); - if (this._pagina > totalPag) this._pagina = totalPag; const inicio = (this._pagina - 1) * POR_PAGINA; - const pagina = this._filtrado.slice(inicio, inicio + POR_PAGINA); - - tb.innerHTML = pagina.map(e => { + tb.innerHTML = this._paginaData.map(e => { const info = ESTADO_ENV[e.estado] || {}; return ` ${esc(e.form_nombre)} @@ -694,54 +646,39 @@ const envios = { ${esc((e.expira_en||'').slice(0,10))} ${['completado','firmado'].includes(e.estado) - ? ` - - ` + ? ` + ` : ''} `; }).join(''); - // Info $('env-pag-info').textContent = - `Mostrando ${inicio + 1}–${Math.min(inicio + POR_PAGINA, total)} de ${total} envíos`; + `Mostrando ${inicio+1}–${inicio+this._paginaData.length} de ${this._total} envío(s)`; - // Paginación - const ul = $('env-paginacion'); + const ul = $('env-paginacion'); + const pag = this._pagina, tot = this._paginas; ul.innerHTML = ''; - const agregar = (label, pag, disabled, active) => { - const li = document.createElement('li'); - li.className = `page-item${disabled?' disabled':''}${active?' active':''}`; - li.innerHTML = `${label}`; - ul.appendChild(li); + if (tot <= 1) return; + const li = (lbl, p, dis, act) => { + const el = document.createElement('li'); + el.className = `page-item${dis?' disabled':''}${act?' active':''}`; + el.innerHTML = `${lbl}`; + ul.appendChild(el); }; - agregar('«', 1, this._pagina === 1, false); - agregar('‹', this._pagina-1, this._pagina === 1, false); - - // páginas con ventana deslizante ±2 - const rango = 2; - const pMin = Math.max(1, this._pagina - rango); - const pMax = Math.min(totalPag, this._pagina + rango); - if (pMin > 1) { agregar('1', 1, false, false); if (pMin > 2) agregar('…', null, true, false); } - for (let p = pMin; p <= pMax; p++) agregar(p, p, false, p === this._pagina); - if (pMax < totalPag) { if (pMax < totalPag-1) agregar('…', null, true, false); agregar(totalPag, totalPag, false, false); } - - agregar('›', this._pagina+1, this._pagina === totalPag, false); - agregar('»', totalPag, this._pagina === totalPag, false); + li('«',1,pag===1,false); li('‹',pag-1,pag===1,false); + const pMin=Math.max(1,pag-2), pMax=Math.min(tot,pag+2); + if(pMin>1){li('1',1,false,false);if(pMin>2)li('…',null,true,false);} + for(let p=pMin;p<=pMax;p++) li(p,p,false,p===pag); + if(pMax x.id == envioId); + const e = envios._paginaData.find(x => x.id == envioId); if (!e) return; const cliente = JSON.parse(e.datos_cliente || '{}');