This commit is contained in:
Lizandro Guarnizo
2026-04-07 15:57:53 -05:00
parent ded060e161
commit 4f1b2e146c
5 changed files with 321 additions and 98 deletions
+8 -4
View File
@@ -26,9 +26,11 @@ const TRANSICIONES_ENFERMERO = [
try {
$datos = inputJson();
$domId = (int)($datos['domicilio_id'] ?? 0);
$estado = trim($datos['nuevo_estado'] ?? '');
$notas = trim($datos['notas'] ?? '');
$domId = (int)($datos['domicilio_id'] ?? 0);
$estado = trim($datos['nuevo_estado'] ?? '');
$notas = trim($datos['notas'] ?? '');
$tempIn = isset($datos['temperatura_inicio']) ? (float)$datos['temperatura_inicio'] : null;
$tempOut= isset($datos['temperatura_salida']) ? (float)$datos['temperatura_salida'] : null;
if (!$domId) jsonError('domicilio_id requerido');
if (!$estado) jsonError('nuevo_estado requerido');
@@ -67,7 +69,9 @@ try {
// Actualizar domicilio
$campos = ['estado' => $estado];
if ($notas) $campos['notas_admin'] = $notas;
if ($notas) $campos['notas_admin'] = $notas;
if ($tempIn !== null) $campos['temperatura_inicio'] = $tempIn;
if ($tempOut !== null) $campos['temperatura_salida'] = $tempOut;
if ($estado === 'completado') {
$campos['hora_salida'] = date('H:i:s');
}
+2
View File
@@ -116,7 +116,9 @@ class Enfermera {
d.pago_monto,
d.pago_notas,
d.hora_llegada,
d.temperatura_inicio,
d.hora_salida,
d.temperatura_salida,
p.id AS paciente_id,
p.nombre_completo AS paciente_nombre,
p.telefono AS paciente_telefono,
+95 -13
View File
@@ -185,6 +185,13 @@ $hoy = date('Y-m-d');
transition:transform .2s; }
.fin-section-toggle.open .fin-chev { transform:rotate(180deg); }
#seccion-fin { display:none; }
/* ── Modal temperatura ── */
#modalTemperatura .temp-value-wrap { position:relative; }
#modalTemperatura .temp-unit { position:absolute; right:12px; top:50%; transform:translateY(-50%);
font-weight:700; color:#6b7280; pointer-events:none; }
#modalTemperatura input[type=number] { padding-right:36px; font-size:1.25rem;
font-weight:700; text-align:center; letter-spacing:2px; }
</style>
</head>
<body>
@@ -241,7 +248,35 @@ $hoy = date('Y-m-d');
</div>
</div>
<!-- ═══════════════════════ MODAL: Confirmar cancel ══════════════════ -->
<!-- ═══════════════════════ MODAL: Temperatura ═════════════════════ -->
<div class="modal fade" id="modalTemperatura" tabindex="-1" data-bs-backdrop="static" data-bs-keyboard="false">
<div class="modal-dialog modal-sm modal-dialog-centered">
<div class="modal-content">
<div class="modal-header py-2" style="background:#0d6efd;color:#fff">
<h6 class="modal-title mb-0" id="temp-modal-titulo">🌡️ Temperatura</h6>
</div>
<div class="modal-body text-center py-4">
<p class="text-muted small mb-3">Registra la temperatura corporal del paciente</p>
<div class="temp-value-wrap d-inline-block">
<input type="number" id="temp-modal-valor" class="form-control form-control-lg"
step="0.1" min="30" max="45" placeholder="36.5"
onkeydown="if(event.key==='Enter')tempModal.confirmar()">
<span class="temp-unit">&deg;C</span>
</div>
<div id="temp-modal-error" class="text-danger small mt-2" style="display:none">
Ingresa un valor válido entre 30 y 45 °C
</div>
</div>
<div class="modal-footer py-2 justify-content-center">
<button class="btn btn-primary px-5" onclick="tempModal.confirmar()">
<i class="fas fa-check me-1"></i>Confirmar
</button>
</div>
</div>
</div>
</div>
<!-- ═══════════════════════ MODAL: Confirmar cancel ═════════════════════ -->
<div class="modal fade" id="modalCancelar" tabindex="-1">
<div class="modal-dialog modal-sm">
<div class="modal-content">
@@ -622,7 +657,7 @@ const portal = {
<!-- Horas de llegada / salida (auto-registradas) -->
${(item.hora_llegada || item.hora_salida) ? `
<div class="d-flex gap-3 small mt-1 mb-2">
<div class="d-flex gap-3 small mt-1 mb-1 flex-wrap">
${item.hora_llegada ? `<span class="text-success"><i class="fas fa-sign-in-alt me-1"></i>Llegada: <strong>${item.hora_llegada.slice(0,5)}</strong></span>` : ''}
${item.hora_salida ? `<span class="text-danger"><i class="fas fa-sign-out-alt me-1"></i>Salida: <strong>${item.hora_salida.slice(0,5)}</strong></span>` : ''}
${item.hora_llegada && item.hora_salida ? (() => {
@@ -632,7 +667,12 @@ const portal = {
if (mins > 0) return `<span class="text-info"><i class="fas fa-stopwatch me-1"></i>${mins>=60?Math.floor(mins/60)+'h '+mins%60+'min':mins+' min'}</span>`;
return '';
})() : ''}
</div>` : ''}
</div>
${(item.temperatura_inicio || item.temperatura_salida) ? `
<div class="d-flex gap-3 small mb-2">
${item.temperatura_inicio ? `<span class="text-primary"><i class="fas fa-thermometer-half me-1"></i>Tº inicio: <strong>${item.temperatura_inicio}°C</strong></span>` : ''}
${item.temperatura_salida ? `<span class="text-warning"><i class="fas fa-thermometer-full me-1"></i>Tº salida: <strong>${item.temperatura_salida}°C</strong></span>` : ''}
</div>` : ''}` : ''}
<!-- Órdenes médicas (multi-archivo) -->
${renderOrdenesEnfermero(item)}
@@ -695,10 +735,17 @@ const portal = {
},
// ── Cambiar estado ─────────────────────────────────────────────────────
async actualizarEstado(domId, nuevoEstado, notas = '') {
// ── BLOQUEO: para completar es obligatorio tener al menos una nota ──
async actualizarEstado(domId, nuevoEstado, notas = '', extraDatos = {}) {
// ── BLOQUEO 1: en_domicilio requiere temperatura de inicio ──
if (nuevoEstado === 'en_domicilio' && extraDatos.temperatura_inicio === undefined) {
tempModal.abrir('🌡️ Temperatura de inicio', async (temp) => {
await portal.actualizarEstado(domId, nuevoEstado, notas, { temperatura_inicio: temp });
});
return;
}
// ── BLOQUEO 2: para completar es obligatorio tener ficha clínica ──
if (nuevoEstado === 'completado') {
// Verificar en caché o cargar desde API
let cache = notasManager._cache[domId];
if (!cache) {
try {
@@ -714,12 +761,9 @@ const portal = {
cache = { clinica: null, libres: [] };
}
}
const tieneNota = cache.clinica !== null;
if (!tieneNota) {
// Abrir la ficha clínica directamente (es la nota obligatoria)
if (!cache.clinica) {
notasManager._pendingCompleteId = domId;
notasManager.abrirFicha(domId);
// Colocar banner de advertencia en la parte superior del sheet
const inner = document.getElementById('nota-sheet-inner');
if (inner) {
const banner = document.createElement('div');
@@ -730,18 +774,30 @@ const portal = {
}
return;
}
// ── BLOQUEO 3: completado requiere temperatura de salida ──
if (extraDatos.temperatura_salida === undefined) {
tempModal.abrir('🌡️ Temperatura de salida', async (temp) => {
await portal.actualizarEstado(domId, nuevoEstado, notas, { ...extraDatos, temperatura_salida: temp });
});
return;
}
}
try {
const r = await fetch('api/lab/update_domicilio_enfermero.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ domicilio_id: domId, nuevo_estado: nuevoEstado, notas }),
body: JSON.stringify({ domicilio_id: domId, nuevo_estado: nuevoEstado, notas, ...extraDatos }),
});
const d = await r.json();
if (!d.success) throw new Error(d.error);
// Actualizar estado en memoria y re-render
// Actualizar estado y temperaturas en memoria
const item = this._agenda.find(a => a.domicilio_id == domId);
if (item) item.domicilio_estado = nuevoEstado;
if (item) {
item.domicilio_estado = nuevoEstado;
if (extraDatos.temperatura_inicio !== undefined) item.temperatura_inicio = extraDatos.temperatura_inicio;
if (extraDatos.temperatura_salida !== undefined) item.temperatura_salida = extraDatos.temperatura_salida;
}
this._renderLista();
this._renderResumen(
Object.fromEntries(
@@ -1475,6 +1531,32 @@ const agendaNueva = {
};
</script>
<script>
// ══════════════════════════════════════════════════════════════════════
// tempModal — Modal de captura de temperatura (inicio / salida)
// ══════════════════════════════════════════════════════════════════════
const tempModal = {
_modal: null,
_cb: null,
abrir(titulo, cb) {
this._cb = cb;
document.getElementById('temp-modal-titulo').textContent = titulo;
document.getElementById('temp-modal-valor').value = '';
document.getElementById('temp-modal-error').style.display = 'none';
if (!this._modal) this._modal = new bootstrap.Modal('#modalTemperatura', { backdrop: 'static', keyboard: false });
this._modal.show();
setTimeout(() => document.getElementById('temp-modal-valor').focus(), 350);
},
confirmar() {
const val = parseFloat(document.getElementById('temp-modal-valor').value);
const err = document.getElementById('temp-modal-error');
if (!val || val < 30 || val > 45) { err.style.display = ''; return; }
this._modal.hide();
if (this._cb) this._cb(val);
},
};
</script>
<script>
// ═══════════════════════════════════════════════════════
// formEnvio — Lógica de envío de formularios
+204 -81
View File
@@ -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');
@@ -0,0 +1,12 @@
-- ============================================================
-- Migration: 20260407_lab_temperatura_domicilio
-- Agrega campos de temperatura de inicio y salida al domicilio
-- ============================================================
ALTER TABLE `lab_domicilios`
ADD COLUMN `temperatura_inicio` DECIMAL(4,1) DEFAULT NULL
COMMENT 'Temperatura del paciente al inicio del domicilio (°C)'
AFTER `hora_llegada`,
ADD COLUMN `temperatura_salida` DECIMAL(4,1) DEFAULT NULL
COMMENT 'Temperatura del paciente al finalizar el domicilio (°C)'
AFTER `hora_salida`;