up
This commit is contained in:
+204
-81
@@ -32,6 +32,7 @@ $domIdParam = (int)($_GET['id'] ?? 0);
|
||||
.agenda-card.en_camino { border-left-color: #0d6efd; }
|
||||
.agenda-card.completado { border-left-color: #198754; }
|
||||
.agenda-card.cancelado { border-left-color: #dc3545; }
|
||||
#seccion-formularios .ff-ctrl { height:31px; padding-top:.25rem; padding-bottom:.25rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -184,34 +185,58 @@ $domIdParam = (int)($_GET['id'] ?? 0);
|
||||
|
||||
<!-- ═══════════ SECCIÓN FORMULARIOS RECIBIDOS ═══════════ -->
|
||||
<div class="container-fluid py-3" id="seccion-formularios" style="display:none">
|
||||
<!-- Filtros -->
|
||||
<div class="row g-2 mb-3">
|
||||
<div class="col-md-3">
|
||||
<select id="ff-plantilla" class="form-select form-select-sm" onchange="cargarFormulariosEnviados()">
|
||||
<option value="">Todos los formularios</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<select id="ff-estado" class="form-select form-select-sm" onchange="cargarFormulariosEnviados()">
|
||||
<option value="">Todos los estados</option>
|
||||
<option value="pendiente">⏳ Pendiente</option>
|
||||
<option value="completado">✅ Completado</option>
|
||||
<option value="firmado">✍️ Firmado</option>
|
||||
<option value="expirado">❌ Expirado</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<input type="date" id="ff-fecha-desde" class="form-control form-control-sm"
|
||||
placeholder="Desde" onchange="cargarFormulariosEnviados()">
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<button class="btn btn-sm btn-secondary w-100" onclick="document.getElementById('ff-fecha-desde').value='';cargarFormulariosEnviados()">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
<!-- Filtros compactos -->
|
||||
<div class="card border-0 bg-light mb-3 px-3 py-2">
|
||||
<div class="row g-2 align-items-end">
|
||||
<div class="col-6 col-sm-2">
|
||||
<label class="form-label form-label-sm mb-1 text-muted">Desde</label>
|
||||
<input type="date" id="ff-fecha-desde" class="form-control form-control-sm ff-ctrl" onchange="ffFiltrar()">
|
||||
</div>
|
||||
<div class="col-6 col-sm-2">
|
||||
<label class="form-label form-label-sm mb-1 text-muted">Hasta</label>
|
||||
<input type="date" id="ff-fecha-hasta" class="form-control form-control-sm ff-ctrl" onchange="ffFiltrar()">
|
||||
</div>
|
||||
<div class="col-12 col-sm-3">
|
||||
<label class="form-label form-label-sm mb-1 text-muted">Formulario</label>
|
||||
<select id="ff-plantilla" class="form-select form-select-sm ff-ctrl" onchange="ffFiltrar()">
|
||||
<option value="">Todos los formularios</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-6 col-sm-2">
|
||||
<label class="form-label form-label-sm mb-1 text-muted">Estado</label>
|
||||
<select id="ff-estado" class="form-select form-select-sm ff-ctrl" onchange="ffFiltrar()">
|
||||
<option value="">Todos</option>
|
||||
<option value="pendiente">⏳ Pendiente</option>
|
||||
<option value="completado">✅ Completado</option>
|
||||
<option value="firmado">✍️ Firmado</option>
|
||||
<option value="expirado">❌ Expirado</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-6 col-sm-3">
|
||||
<label class="form-label form-label-sm mb-1 text-muted">Paciente</label>
|
||||
<input type="text" id="ff-paciente" class="form-control form-control-sm ff-ctrl"
|
||||
placeholder="Buscar…" oninput="ffFiltrar()">
|
||||
</div>
|
||||
<div class="col-12 col-sm-auto ms-sm-auto d-flex gap-2">
|
||||
<button class="btn btn-success btn-sm" onclick="ffExportarExcel()" title="Exportar a Excel">
|
||||
<i class="fas fa-file-excel me-1"></i><span class="d-none d-md-inline">Excel</span>
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary btn-sm" onclick="ffLimpiar()" title="Limpiar">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card border-0 shadow-sm">
|
||||
<!-- Spinner -->
|
||||
<div id="ff-spinner" class="text-center py-5" style="display:none">
|
||||
<div class="spinner-border text-primary" role="status" style="width:2rem;height:2rem">
|
||||
<span class="visually-hidden">Cargando…</span>
|
||||
</div>
|
||||
<p class="text-muted small mt-2 mb-0">Cargando formularios…</p>
|
||||
</div>
|
||||
|
||||
<div class="card border-0 shadow-sm" id="ff-tabla-wrap">
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
@@ -230,7 +255,11 @@ $domIdParam = (int)($_GET['id'] ?? 0);
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="px-3 py-2 border-top text-muted small" id="ff-total"></div>
|
||||
<!-- Paginación -->
|
||||
<div class="d-flex align-items-center justify-content-between px-3 py-2 border-top flex-wrap gap-2">
|
||||
<small class="text-muted" id="ff-info"></small>
|
||||
<nav><ul class="pagination pagination-sm mb-0" id="ff-paginacion"></ul></nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1360,9 +1389,27 @@ function switchTab(tab, linkEl) {
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
// FORMULARIOS RECIBIDOS
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
let _ffData = [];
|
||||
const FF_POR_PAG = 20;
|
||||
let _ffPagina = 1;
|
||||
let _ffTotal = 0;
|
||||
let _ffPaginas = 1;
|
||||
let _ffPaginaData = [];
|
||||
let _ffDebTimer = null;
|
||||
let _ffPlantillasCargadas = false;
|
||||
|
||||
function ffParams(pag = _ffPagina) {
|
||||
return new URLSearchParams({
|
||||
envios: 1,
|
||||
page: pag,
|
||||
limit: FF_POR_PAG,
|
||||
fecha_desde: document.getElementById('ff-fecha-desde').value || '',
|
||||
fecha_hasta: document.getElementById('ff-fecha-hasta').value || '',
|
||||
formulario_id: document.getElementById('ff-plantilla').value || '',
|
||||
estado: document.getElementById('ff-estado').value || '',
|
||||
paciente: (document.getElementById('ff-paciente')?.value || '').trim(),
|
||||
});
|
||||
}
|
||||
|
||||
async function cargarPlantillasFF() {
|
||||
if (_ffPlantillasCargadas) return;
|
||||
_ffPlantillasCargadas = true;
|
||||
@@ -1370,77 +1417,159 @@ async function cargarPlantillasFF() {
|
||||
const d = await r.json();
|
||||
const sel = document.getElementById('ff-plantilla');
|
||||
(d.data || []).forEach(f => {
|
||||
sel.insertAdjacentHTML('beforeend', `<option value="${f.id}">${esc(f.nombre)}</option>`);
|
||||
const opt = document.createElement('option');
|
||||
opt.value = f.id;
|
||||
opt.textContent = f.nombre;
|
||||
sel.appendChild(opt);
|
||||
});
|
||||
}
|
||||
|
||||
async function cargarFormulariosEnviados() {
|
||||
async function cargarFormulariosEnviados(pag = 1) {
|
||||
_ffPagina = pag;
|
||||
const spinner = document.getElementById('ff-spinner');
|
||||
const wrap = document.getElementById('ff-tabla-wrap');
|
||||
if (spinner) spinner.style.display = '';
|
||||
if (wrap) wrap.style.display = 'none';
|
||||
|
||||
try {
|
||||
const r = await fetch('api/lab/get_formularios.php?' + ffParams(pag));
|
||||
const d = await r.json();
|
||||
_ffPaginaData = d.data || [];
|
||||
_ffTotal = d.total || 0;
|
||||
_ffPaginas = d.paginas || 1;
|
||||
ffRender();
|
||||
} catch(e) {
|
||||
const tb = document.getElementById('ff-tbody');
|
||||
if (tb) tb.innerHTML = '<tr><td colspan="6" class="text-center text-danger py-4">Error al cargar</td></tr>';
|
||||
} finally {
|
||||
if (spinner) spinner.style.display = 'none';
|
||||
if (wrap) wrap.style.display = '';
|
||||
}
|
||||
}
|
||||
|
||||
function ffFiltrar() {
|
||||
clearTimeout(_ffDebTimer);
|
||||
_ffDebTimer = setTimeout(() => cargarFormulariosEnviados(1), 350);
|
||||
}
|
||||
|
||||
function ffLimpiar() {
|
||||
['ff-fecha-desde','ff-fecha-hasta','ff-paciente'].forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.value = '';
|
||||
});
|
||||
document.getElementById('ff-plantilla').value = '';
|
||||
document.getElementById('ff-estado').value = '';
|
||||
cargarFormulariosEnviados(1);
|
||||
}
|
||||
|
||||
async function ffExportarExcel() {
|
||||
const btn = document.querySelector('[onclick="ffExportarExcel()"]');
|
||||
if (btn) { btn.disabled = true; btn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Exportando…'; }
|
||||
let rows = [];
|
||||
try {
|
||||
const p = ffParams(1);
|
||||
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 (btn) { btn.disabled = false; btn.innerHTML = '<i class="fas fa-file-excel me-1"></i><span class="d-none d-md-inline">Excel</span>'; }
|
||||
}
|
||||
if (!rows.length) { mostrarToast('Sin datos para exportar', 'warning'); return; }
|
||||
|
||||
const fixedCols = ['Formulario','Categoría','Paciente','Enviado por','Estado','Fecha envío','Fecha completado'];
|
||||
const fieldMap = new Map();
|
||||
rows.forEach(e => {
|
||||
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()];
|
||||
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};
|
||||
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.enviado_por_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)); }),
|
||||
]);
|
||||
});
|
||||
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 = Object.assign(document.createElement('a'),{href:url,download:'formularios_domicilios_'+new Date().toISOString().slice(0,10)+'.csv'});
|
||||
document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url);
|
||||
mostrarToast('✅ Excel descargado — ' + rows.length + ' registro(s)', 'success');
|
||||
}
|
||||
|
||||
const FF_ESTADO_BADGE = { pendiente:'badge bg-warning text-dark', completado:'badge bg-success', firmado:'badge bg-primary', expirado:'badge bg-secondary' };
|
||||
const FF_ESTADO_ICON = { pendiente:'⏳', completado:'✅', firmado:'✍️', expirado:'❌' };
|
||||
|
||||
function ffRender() {
|
||||
const tbody = document.getElementById('ff-tbody');
|
||||
tbody.innerHTML = '<tr><td colspan="6" class="text-center py-3 text-muted"><i class="fas fa-spinner fa-spin"></i></td></tr>';
|
||||
|
||||
const params = new URLSearchParams({ envios: 1 });
|
||||
const estado = document.getElementById('ff-estado').value;
|
||||
const plantilla = document.getElementById('ff-plantilla').value;
|
||||
const fechaDesde = document.getElementById('ff-fecha-desde').value;
|
||||
if (estado) params.set('estado', estado);
|
||||
if (plantilla) params.set('formulario_id', plantilla);
|
||||
if (fechaDesde) params.set('fecha_desde', fechaDesde);
|
||||
|
||||
const r = await fetch(`api/lab/get_formularios.php?${params}`);
|
||||
const d = await r.json();
|
||||
_ffData = d.data || [];
|
||||
|
||||
document.getElementById('ff-total').textContent = `${_ffData.length} registro(s)`;
|
||||
|
||||
if (!_ffData.length) {
|
||||
tbody.innerHTML = '<tr><td colspan="6" class="text-center py-4 text-muted">Sin registros</td></tr>';
|
||||
if (!_ffPaginaData.length) {
|
||||
tbody.innerHTML = '<tr><td colspan="6" class="text-center py-4 text-muted">Sin registros para los filtros seleccionados</td></tr>';
|
||||
document.getElementById('ff-info').textContent = '';
|
||||
document.getElementById('ff-paginacion').innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const ESTADO_BADGE = {
|
||||
pendiente: 'badge bg-warning text-dark',
|
||||
completado: 'badge bg-success',
|
||||
firmado: 'badge bg-primary',
|
||||
expirado: 'badge bg-secondary',
|
||||
};
|
||||
const ESTADO_ICON = { pendiente:'⏳', completado:'✅', firmado:'✍️', expirado:'❌' };
|
||||
|
||||
tbody.innerHTML = _ffData.map(e => {
|
||||
const inicio = (_ffPagina - 1) * FF_POR_PAG;
|
||||
tbody.innerHTML = _ffPaginaData.map(e => {
|
||||
const fecha = (e.created_at || '').slice(0, 16).replace('T', ' ');
|
||||
const estadoBadge = ESTADO_BADGE[e.estado] || 'badge bg-secondary';
|
||||
const estadoIcon = ESTADO_ICON[e.estado] || '—';
|
||||
const puedeVer = e.estado === 'completado' || e.estado === 'firmado';
|
||||
const puedeVer = e.estado === 'completado' || e.estado === 'firmado';
|
||||
return `<tr>
|
||||
<td class="small text-nowrap">${esc(fecha)}</td>
|
||||
<td class="small">${esc(e.paciente_nombre || '—')}</td>
|
||||
<td class="small">${esc(e.form_nombre)}</td>
|
||||
<td class="small">${esc(e.enviado_por_nombre || '—')}</td>
|
||||
<td><span class="${estadoBadge}">${estadoIcon} ${esc(e.estado)}</span></td>
|
||||
<td><span class="${FF_ESTADO_BADGE[e.estado]||'badge bg-secondary'}">${FF_ESTADO_ICON[e.estado]||''} ${esc(e.estado)}</span></td>
|
||||
<td>
|
||||
${puedeVer
|
||||
? `<button class="btn btn-xs btn-outline-primary py-0 px-2 me-1" onclick="verRespuestaFF(${e.id})">
|
||||
<i class="fas fa-eye me-1"></i>Ver
|
||||
</button>
|
||||
<a href="ver_formulario_enviado.php?id=${e.id}" target="_blank"
|
||||
class="btn btn-xs btn-outline-secondary py-0 px-2">
|
||||
<i class="fas fa-print"></i>
|
||||
</a>`
|
||||
? `<button class="btn btn-xs btn-outline-primary py-0 px-2 me-1" onclick="verRespuestaFF(${e.id})"><i class="fas fa-eye me-1"></i>Ver</button>
|
||||
<a href="ver_formulario_enviado.php?id=${e.id}" target="_blank" class="btn btn-xs btn-outline-secondary py-0 px-2"><i class="fas fa-print"></i></a>`
|
||||
: '—'}
|
||||
</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
|
||||
document.getElementById('ff-info').textContent =
|
||||
`Mostrando ${inicio+1}–${inicio+_ffPaginaData.length} de ${_ffTotal} registro(s)`;
|
||||
|
||||
// Paginación
|
||||
const ul = document.getElementById('ff-paginacion');
|
||||
const pag = _ffPagina, tot = _ffPaginas;
|
||||
ul.innerHTML = '';
|
||||
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 = `<a class="page-link" href="#" onclick="event.preventDefault();${!dis?`cargarFormulariosEnviados(${p})`:''}"
|
||||
>${lbl}</a>`;
|
||||
ul.appendChild(el);
|
||||
};
|
||||
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<tot){if(pMax<tot-1)li('…',null,true,false);li(tot,tot,false,false);}
|
||||
li('›',pag+1,pag===tot,false); li('»',tot,pag===tot,false);
|
||||
}
|
||||
|
||||
function verRespuestaFF(envioId) {
|
||||
const e = _ffData.find(x => x.id == envioId);
|
||||
const e = _ffPaginaData.find(x => x.id == envioId);
|
||||
if (!e) return;
|
||||
|
||||
const cliente = JSON.parse(e.datos_cliente || '{}');
|
||||
const prefill = JSON.parse(e.datos_prefilled || '{}');
|
||||
const todos = { ...prefill, ...cliente };
|
||||
const esquema = JSON.parse(e.esquema || '[]');
|
||||
|
||||
const labels = {};
|
||||
const labels = {};
|
||||
esquema.forEach(c => { if (c.id) labels[c.id] = c.label || c.id; });
|
||||
|
||||
let html = `<h6 class="fw-bold mb-1">${esc(e.form_nombre)}</h6>
|
||||
@@ -1453,33 +1582,27 @@ function verRespuestaFF(envioId) {
|
||||
const ordenados = esquema.filter(c =>
|
||||
c.tipo !== 'separador' && c.tipo !== 'firma' && todos[c.id] !== undefined
|
||||
);
|
||||
|
||||
if (ordenados.length) {
|
||||
ordenados.forEach(c => {
|
||||
const v = todos[c.id];
|
||||
const display = Array.isArray(v) ? v.join(', ') : String(v ?? '—');
|
||||
html += `<div class="row mb-2">
|
||||
<div class="col-5 text-muted small">${esc(c.label || c.id)}</div>
|
||||
<div class="col-7 small fw-semibold">${esc(display)}</div>
|
||||
<div class="col-7 small fw-semibold">${esc(Array.isArray(v)?v.join(', '):String(v??'—'))}</div>
|
||||
</div>`;
|
||||
});
|
||||
} else {
|
||||
Object.entries(todos).forEach(([k, v]) => {
|
||||
if (k.startsWith('__')) return;
|
||||
const display = Array.isArray(v) ? v.join(', ') : String(v ?? '—');
|
||||
html += `<div class="row mb-2">
|
||||
<div class="col-5 text-muted small">${esc(labels[k] || k)}</div>
|
||||
<div class="col-7 small fw-semibold">${esc(display)}</div>
|
||||
<div class="col-5 text-muted small">${esc(labels[k]||k)}</div>
|
||||
<div class="col-7 small fw-semibold">${esc(Array.isArray(v)?v.join(', '):String(v??'—'))}</div>
|
||||
</div>`;
|
||||
});
|
||||
}
|
||||
|
||||
if (e.firma_svg) {
|
||||
html += `<hr class="my-3"><p class="small fw-semibold text-muted">Firma digital:</p>
|
||||
<img src="${e.firma_svg}" class="border rounded p-2"
|
||||
style="max-width:260px;max-height:160px;display:block">`;
|
||||
<img src="${e.firma_svg}" class="border rounded p-2" style="max-width:260px;max-height:160px;display:block">`;
|
||||
}
|
||||
|
||||
document.getElementById('resp-form-body').innerHTML = html;
|
||||
document.getElementById('btn-resp-pdf').href = `ver_formulario_enviado.php?id=${envioId}`;
|
||||
if (!_modalRespForm) _modalRespForm = new bootstrap.Modal('#modalRespuestaForm');
|
||||
|
||||
Reference in New Issue
Block a user