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 || '{}');