up
This commit is contained in:
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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
@@ -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">°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
@@ -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`;
|
||||
Reference in New Issue
Block a user