Files
whatsapp/lab_formularios.php
T

829 lines
42 KiB
PHP

<?php
/**
* lab_formularios.php — Módulo de Formularios
* Admin: crea y diseña formularios drag & drop
* Enfermero: solo puede ver la lista y enviar formularios pre-llenados
*/
require_once 'config/config.php';
if (!isUserLoggedIn()) { header('Location: login.php'); exit; }
if (isEnfermero() && !enfermeraId()) { header('Location: enfermero_portal.php'); exit; }
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Pragma: no-cache');
$adminNombre = $_SESSION['admin_user']['full_name'] ?? 'Usuario';
$esAdmin = !isEnfermero();
$puedeEscribir = $esAdmin && hasModuleWrite('lab_formularios');
?>
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Formularios — Módulo Lab</title>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="./assets/css/styles.css?v=12" rel="stylesheet">
<style>
/* ── Tarjeta formulario ─────────────────────────── */
.form-card { border-radius:10px; border:1px solid #dee2e6; background:#fff;
transition:box-shadow .2s; }
.form-card:hover { box-shadow:0 3px 12px rgba(0,0,0,.1); }
/* ── Colores estado envío ───────────────────────── */
.badge-pendiente { background:#6c757d; }
.badge-completado { background:#0d6efd; }
.badge-firmado { background:#198754; }
.badge-expirado { background:#dc3545; }
</style>
</head>
<body>
<nav class="sidebar">
<div class="sidebar-header"><h4><i class="fas fa-flask"></i> Módulo Lab</h4></div>
<ul class="sidebar-menu">
<?php if ($esAdmin): ?>
<li><a href="lab_dashboard.php" class="nav-link"><i class="fas fa-tachometer-alt"></i> Dashboard</a></li>
<li><a href="lab_ordenes.php" class="nav-link"><i class="fas fa-file-medical"></i> Órdenes</a></li>
<li><a href="lab_pacientes.php" class="nav-link"><i class="fas fa-users"></i> Pacientes</a></li>
<li><a href="lab_domicilios.php" class="nav-link"><i class="fas fa-house-medical"></i> Domicilios</a></li>
<li><a href="lab_enfermeras.php" class="nav-link"><i class="fas fa-user-nurse"></i> Enfermeras</a></li>
<li><a href="lab_formularios.php" class="nav-link active"><i class="fas fa-wpforms"></i> Formularios</a></li>
<li><a href="lab_reportes.php" class="nav-link"><i class="fas fa-chart-bar"></i> Reportes</a></li>
<li><a href="lab_configuracion.php" class="nav-link"><i class="fas fa-sliders-h"></i> Configuración</a></li>
<li><a href="lab_usuarios.php" class="nav-link"><i class="fas fa-users-cog"></i> Usuarios</a></li>
<?php else: ?>
<li><a href="enfermero_portal.php" class="nav-link"><i class="fas fa-calendar-day"></i> Mi Agenda</a></li>
<li><a href="lab_formularios.php" class="nav-link active"><i class="fas fa-wpforms"></i> Formularios</a></li>
<?php endif; ?>
<li class="sidebar-divider"></li>
<li><a href="<?= $esAdmin ? 'index.php' : 'enfermero_portal.php' ?>" class="nav-link"><i class="fas fa-arrow-left"></i> Volver</a></li>
<li><a href="logout.php" class="nav-link logout-link" onclick="return confirm('¿Cerrar sesión?')"><i class="fas fa-sign-out-alt"></i> Cerrar Sesión</a></li>
</ul>
</nav>
<main class="main-content">
<header class="content-header d-flex align-items-center justify-content-between">
<div>
<h1><i class="fas fa-wpforms text-primary"></i> Formularios</h1>
<small class="text-muted"><?= htmlspecialchars($adminNombre) ?></small>
</div>
<?php if ($puedeEscribir): ?>
<button class="btn btn-primary btn-sm" onclick="builder.nuevo()">
<i class="fas fa-plus me-1"></i> Nuevo Formulario
</button>
<?php endif; ?>
</header>
<!-- ── TABS ─────────────────────────────────────────────────── -->
<ul class="nav nav-tabs mb-4" id="formTabs">
<li class="nav-item">
<a class="nav-link active" href="#" onclick="tabs.mostrar('lista')">
<i class="fas fa-list me-1"></i>Mis Formularios
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#" onclick="tabs.mostrar('envios')">
<i class="fas fa-paper-plane me-1"></i>Envíos
<span id="badge-envios" class="badge bg-secondary ms-1" style="display:none"></span>
</a>
</li>
</ul>
<!-- ── LISTA ──────────────────────────────────────────────── -->
<div id="panel-lista">
<div class="row g-3" id="formularios-grid">
<div class="col-12 text-center py-5 text-muted">
<div class="spinner-border spinner-border-sm"></div> Cargando…
</div>
</div>
</div>
<!-- ── ENVÍOS ─────────────────────────────────────────────── -->
<div id="panel-envios" style="display:none">
<div class="table-responsive">
<table class="table table-hover align-middle small" id="tabla-envios">
<thead class="table-light">
<tr>
<th>Formulario</th>
<th>Paciente</th>
<th>Estado</th>
<th>Enviado</th>
<th>Expira</th>
<th>Acciones</th>
</tr>
</thead>
<tbody id="tbody-envios">
<tr><td colspan="6" class="text-center text-muted py-4">Cargando…</td></tr>
</tbody>
</table>
</div>
</div>
</main>
<!-- Builder movido a lab_formulario_builder.php (ventana dedicada) -->
<!-- ═══════════════════ MODAL ENVIAR ═══════════════════════════════ -->
<div class="modal fade" id="modalEnviar" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header bg-success text-white py-2">
<h6 class="modal-title mb-0">
<i class="fas fa-paper-plane me-1"></i>Enviar formulario al cliente
</h6>
<button class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="hidden" id="env-form-id">
<!-- Paso 1: selección paciente/domicilio -->
<div id="env-paso1">
<div class="row g-3">
<div class="col-12">
<label class="fw-semibold small">Paciente</label>
<div class="input-group input-group-sm">
<input type="text" id="env-pac-busq" class="form-control"
placeholder="Buscar paciente…" autocomplete="off">
<button class="btn btn-outline-secondary" onclick="envio.buscarPaciente()">
<i class="fas fa-search"></i>
</button>
</div>
<div id="env-pac-list" class="list-group shadow mt-1"
style="position:relative;z-index:1060;max-height:150px;overflow-y:auto;display:none"></div>
<div id="env-pac-card" class="alert alert-success py-2 mt-2 d-none">
<i class="fas fa-user-check me-1"></i>
<span id="env-pac-nombre"></span>
<button class="btn-close float-end btn-sm" onclick="envio.limpiarPaciente()"></button>
</div>
</div>
<!-- Pre-llenado dinámico basado en el esquema -->
<div class="col-12" id="env-prefill-cont" style="display:none">
<p class="fw-semibold small text-secondary mb-2">
<i class="fas fa-fill-drip me-1"></i>PRE-LLENAR CAMPOS
<small class="text-muted fw-normal">(el cliente solo podrá firmar)</small>
</p>
<div id="env-prefill-fields" class="row g-2"></div>
</div>
<div class="col-12">
<label class="form-label small">Enviar vía</label>
<div class="d-flex gap-3">
<div class="form-check">
<input class="form-check-input" type="radio" name="env-via"
id="via-wa" value="whatsapp" checked>
<label class="form-check-label small" for="via-wa">
<i class="fab fa-whatsapp text-success me-1"></i>WhatsApp
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="env-via"
id="via-link" value="link">
<label class="form-check-label small" for="via-link">
<i class="fas fa-link me-1"></i>Solo link
</label>
</div>
</div>
</div>
</div>
</div>
<!-- Paso 2: resultado / link generado -->
<div id="env-paso2" style="display:none">
<div class="text-center py-3">
<div style="font-size:2.5rem">✅</div>
<h6 class="mt-2">¡Link generado!</h6>
</div>
<div class="mb-3">
<label class="form-label small fw-semibold">Link del formulario</label>
<div class="input-group">
<input type="text" class="form-control form-control-sm" id="env-link-url" readonly>
<button class="btn btn-outline-secondary btn-sm" onclick="envio.copiarLink()">
<i class="fas fa-copy"></i>
</button>
</div>
</div>
<div class="mb-3" id="env-wa-cont">
<label class="form-label small fw-semibold">Mensaje de WhatsApp listo</label>
<textarea class="form-control form-control-sm" id="env-wa-msg"
rows="5" readonly></textarea>
<div class="mt-2">
<a id="env-wa-btn" href="#" target="_blank"
class="btn btn-success btn-sm w-100">
<i class="fab fa-whatsapp me-1"></i>Abrir WhatsApp y enviar
</a>
</div>
</div>
</div>
</div>
<div class="modal-footer py-2">
<div id="env-footer1">
<button class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Cancelar</button>
<button class="btn btn-success btn-sm" onclick="envio.generar()">
<i class="fas fa-paper-plane me-1"></i>Generar link
</button>
</div>
<div id="env-footer2" style="display:none">
<button class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Cerrar</button>
<button class="btn btn-outline-primary btn-sm" onclick="envio.otroEnvio()">
<i class="fas fa-plus me-1"></i>Enviar a otro paciente
</button>
</div>
</div>
</div>
</div>
</div>
<!-- ═══════════════════ MODAL VER RESPUESTA ══════════════════════════ -->
<div class="modal fade" id="modalRespuesta" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header py-2">
<h6 class="modal-title mb-0"><i class="fas fa-file-alt me-1"></i>Respuesta del cliente</h6>
<button class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body" id="resp-body"></div>
<div class="modal-footer py-2">
<button class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Cerrar</button>
<a id="btn-pdf-link" href="#" target="_blank" class="btn btn-danger btn-sm">
<i class="fas fa-file-pdf me-1"></i>Ver / Descargar PDF
</a>
<button class="btn btn-outline-secondary btn-sm" onclick="verRespuesta.imprimir()">
<i class="fas fa-print me-1"></i>Imprimir
</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
// ══════════════════════════════════════════════════════════════════════
// CONFIGURACIÓN
// ══════════════════════════════════════════════════════════════════════
const ES_ADMIN = <?= $esAdmin ? 'true' : 'false' ?>;
const PUEDE_ESCRIBIR = <?= $puedeEscribir ? 'true' : 'false' ?>;
// Tipos de campo disponibles
const TIPOS_CAMPO = [
{ tipo:'texto', label:'Texto corto', icon:'fa-font', linked:false },
{ tipo:'textarea', label:'Texto largo', icon:'fa-align-left', linked:false },
{ tipo:'numero', label:'Número', icon:'fa-hashtag', linked:false },
{ tipo:'fecha', label:'Fecha', icon:'fa-calendar', linked:false },
{ tipo:'hora', label:'Hora', icon:'fa-clock', linked:false },
{ tipo:'select', label:'Lista desplegable',icon:'fa-list', linked:false },
{ tipo:'radio', label:'Selección única', icon:'fa-dot-circle', linked:false },
{ tipo:'checkbox', label:'Múltiple opción', icon:'fa-check-square', linked:false },
{ tipo:'firma', label:'Firma digital', icon:'fa-signature', linked:false },
{ tipo:'separador', label:'Separador / Título',icon:'fa-minus', linked:false },
];
// Campos vinculados (auto-llenados desde el paciente)
const TIPOS_LINKED = [
{ tipo:'linked', key:'nombre_completo', label:'Nombre completo (paciente)', icon:'fa-user' },
{ tipo:'linked', key:'numero_documento',label:'Número de documento', icon:'fa-id-card' },
{ tipo:'linked', key:'tipo_documento', label:'Tipo de documento', icon:'fa-id-badge' },
{ tipo:'linked', key:'fecha_nacimiento',label:'Fecha de nacimiento', icon:'fa-birthday-cake' },
{ tipo:'linked', key:'telefono', label:'Teléfono', icon:'fa-phone' },
{ tipo:'linked', key:'email', label:'Email', icon:'fa-envelope' },
{ tipo:'linked', key:'eps', label:'EPS / Aseguradora', icon:'fa-hospital' },
{ tipo:'linked', key:'direccion', label:'Dirección', icon:'fa-map-marker-alt' },
];
// ══════════════════════════════════════════════════════════════════════
// ESTADO
// ══════════════════════════════════════════════════════════════════════
let _formularios = [];
let _globalCfg = {}; // config global del lab
let _bsEnviar = null;
let _bsResp = null;
// ══════════════════════════════════════════════════════════════════════
// HELPERS DOM
// ══════════════════════════════════════════════════════════════════════
const $ = id => document.getElementById(id);
function esc(s) {
return String(s||'').replace(/[<>&"']/g, c =>
({'<':'&lt;','>':'&gt;','&':'&amp;','"':'&quot;',"'":'&#39;'}[c]));
}
function uid() { return '_' + Math.random().toString(36).slice(2, 9); }
// ══════════════════════════════════════════════════════════════════════
// TABS
// ══════════════════════════════════════════════════════════════════════
const tabs = {
mostrar(tab) {
$('panel-lista').style.display = tab === 'lista' ? '' : 'none';
$('panel-envios').style.display = tab === 'envios' ? '' : 'none';
document.querySelectorAll('#formTabs .nav-link').forEach((a,i) =>
a.classList.toggle('active', (tab==='lista') === (i===0))
);
if (tab === 'envios') envios.cargar();
}
};
// ══════════════════════════════════════════════════════════════════════
// LISTA DE FORMULARIOS
// ══════════════════════════════════════════════════════════════════════
async function cargarFormularios() {
const r = await fetch('api/lab/get_formularios.php');
const d = await r.json();
_formularios = d.data || [];
renderFormularios();
}
function renderFormularios() {
const grid = $('formularios-grid');
if (!_formularios.length) {
grid.innerHTML = `<div class="col-12 text-center py-5 text-muted">
<i class="fas fa-wpforms fa-3x mb-3 opacity-25"></i>
<p>No hay formularios creados aún.</p>
${PUEDE_ESCRIBIR ? '<button class="btn btn-primary" onclick="builder.nuevo()"><i class="fas fa-plus me-1"></i>Crear primer formulario</button>' : ''}
</div>`;
return;
}
const CATS = { consentimiento:'⚕️',historia_clinica:'📋',autorizacion:'✍️',encuesta:'📊',otro:'📄' };
grid.innerHTML = _formularios.map(f => `
<div class="col-md-4 col-lg-3">
<div class="form-card p-3 h-100">
<div class="d-flex justify-content-between align-items-start mb-2">
<span class="badge bg-light text-dark border">${CATS[f.categoria]||'📄'} ${esc(f.categoria)}</span>
${f.requiere_firma ? '<span class="badge bg-warning text-dark small"><i class="fas fa-signature me-1"></i>Firma req.</span>' : ''}
</div>
<h6 class="fw-bold mb-1">${esc(f.nombre)}</h6>
${f.descripcion ? `<p class="small text-muted mb-2">${esc(f.descripcion)}</p>` : ''}
<div class="d-flex gap-2 small text-muted mb-3">
<span title="Envíos"><i class="fas fa-paper-plane me-1"></i>${f.total_envios||0}</span>
<span title="Completados"><i class="fas fa-check-circle me-1 text-success"></i>${f.total_completados||0}</span>
<span class="ms-auto">v${f.version}</span>
</div>
<div class="d-flex gap-1 flex-wrap">
<button class="btn btn-sm btn-outline-success flex-grow-1"
onclick="envio.abrir(${f.id})">
<i class="fas fa-paper-plane me-1"></i>Enviar
</button>
${PUEDE_ESCRIBIR ? `
<button class="btn btn-sm btn-outline-primary" onclick="builder.editar(${f.id})"
title="Editar diseño"><i class="fas fa-edit"></i></button>
<button class="btn btn-sm btn-outline-danger" onclick="confirmarBorrar(${f.id},'${esc(f.nombre)}')"
title="Eliminar"><i class="fas fa-trash"></i></button>
` : ''}
</div>
</div>
</div>
`).join('');
}
async function confirmarBorrar(id, nombre) {
if (!confirm(`¿Eliminar el formulario "${nombre}"?\nLos envíos existentes se conservarán.`)) return;
const r = await fetch('api/lab/save_formulario.php', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({ id, borrar: true }),
});
const d = await r.json();
if (d.success) { cargarFormularios(); } else { alert(d.error); }
}
// ══════════════════════════════════════════════════════════════════════
// BUILDER (drag & drop)
// ══════════════════════════════════════════════════════════════════════
const builder = {
_win: null,
_abrirVentana(url) {
// Reusar ventana si ya está abierta
if (this._win && !this._win.closed) {
this._win.location.href = url;
this._win.focus();
} else {
this._win = window.open(
url,
'lab_builder',
'width=1440,height=900,resizable=yes,scrollbars=yes'
);
}
},
nuevo() {
if (!PUEDE_ESCRIBIR) return;
this._abrirVentana('lab_formulario_builder.php');
},
editar(id) {
if (!PUEDE_ESCRIBIR) return;
this._abrirVentana(`lab_formulario_builder.php?id=${id}`);
},
}; // end builder
// ENVÍOS (lista)
// ══════════════════════════════════════════════════════════════════════
const ESTADO_ENV = {
pendiente: { cls:'badge-pendiente', icon:'🕐' },
completado: { cls:'badge-completado', icon:'✅' },
firmado: { cls:'badge-firmado', icon:'✍️' },
expirado: { cls:'badge-expirado', icon:'⏰' },
};
const envios = {
_data: [],
async cargar() {
const r = await fetch('api/lab/get_formularios.php?envios=1');
const d = await r.json();
this._data = d.data || [];
this._render();
},
_render() {
const tb = $('tbody-envios');
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';
if (!this._data.length) {
tb.innerHTML = '<tr><td colspan="6" class="text-center text-muted py-4">Sin envíos aún</td></tr>';
return;
}
tb.innerHTML = this._data.map(e => {
const info = ESTADO_ENV[e.estado] || {};
return `<tr>
<td><span class="fw-semibold">${esc(e.form_nombre)}</span>
<br><small class="text-muted">${esc(e.categoria)}</small></td>
<td>${esc(e.paciente_nombre || '—')}</td>
<td><span class="badge text-white ${info.cls}">${info.icon} ${e.estado}</span></td>
<td><small>${esc((e.created_at||'').slice(0,16).replace('T',' '))}</small></td>
<td><small class="text-muted">${esc((e.expira_en||'').slice(0,10))}</small></td>
<td>
<button class="btn btn-xs btn-outline-secondary" title="Copiar link"
onclick="envios.copiarTokenLink('${esc(e.token)}')">
<i class="fas fa-copy"></i>
</button>
${['completado','firmado'].includes(e.estado)
? `<button class="btn btn-xs btn-outline-primary ms-1" title="Ver respuesta"
onclick="verRespuesta.abrir(${e.id})">
<i class="fas fa-eye"></i></button>
<a class="btn btn-xs btn-outline-danger ms-1" title="Descargar PDF"
href="ver_formulario_enviado.php?id=${e.id}" target="_blank">
<i class="fas fa-file-pdf"></i></a>`
: ''}
</td>
</tr>`;
}).join('');
},
copiarTokenLink(token) {
const url = window.location.origin + window.location.pathname.replace(/\/[^/]+$/, '')
+ `/form_cliente.php?t=${token}`;
navigator.clipboard.writeText(url).then(() => showToast('Link copiado'));
},
};
// ══════════════════════════════════════════════════════════════════════
// ENVIAR FORMULARIO (modal)
// ══════════════════════════════════════════════════════════════════════
const envio = {
_pacienteId: null,
_prefilled: {},
_esquema: [],
async abrir(formId) {
$('env-form-id').value = formId;
this._pacienteId = null;
this._prefilled = {};
// Obtener esquema para pre-llenado
const r = await fetch(`api/lab/get_formularios.php?id=${formId}`);
const d = await r.json();
this._esquema = d.formulario?.esquema_decoded || [];
// Reset UI
$('env-pac-busq').value = '';
$('env-pac-list').style.display = 'none';
$('env-pac-card').classList.add('d-none');
$('env-prefill-cont').style.display = 'none';
$('env-paso1').style.display = '';
$('env-paso2').style.display = 'none';
$('env-footer1').style.display = '';
$('env-footer2').style.display = 'none';
document.getElementById('via-wa').checked = true;
if (!_bsEnviar) _bsEnviar = new bootstrap.Modal('#modalEnviar');
_bsEnviar.show();
},
async buscarPaciente() {
const q = $('env-pac-busq').value.trim();
if (!q) return;
const r = await fetch(`api/lab/get_pacientes.php?busqueda=${encodeURIComponent(q)}&limit=6`);
const d = await r.json();
const list = $('env-pac-list');
if (!d.data?.length) {
list.innerHTML = '<a class="list-group-item text-muted disabled small py-1">Sin resultados</a>';
} else {
list.innerHTML = d.data.map(p =>
`<button type="button" class="list-group-item list-group-item-action small py-2"
data-id="${p.id}" data-nombre="${esc(p.nombre_completo)}"
data-tel="${esc(p.telefono||'')}"
data-doc="${esc((p.tipo_documento||'') + ' ' + (p.numero_documento||''))}"
data-nac="${esc(p.fecha_nacimiento||'')}" data-eps="${esc(p.eps||'')}"
data-email="${esc(p.email||'')}" data-dir="${esc(p.direccion||'')}"
data-ciudad="${esc(p.ciudad||'')}"
onclick="envio.selPaciente(this)">
<strong>${esc(p.nombre_completo)}</strong>
<span class="text-muted ms-2">${esc(p.tipo_documento||'')} ${esc(p.numero_documento||'')}</span>
</button>`
).join('');
}
list.style.display = 'block';
},
selPaciente(btn) {
this._pacienteId = +btn.dataset.id;
$('env-pac-list').style.display = 'none';
$('env-pac-busq').value = btn.dataset.nombre;
$('env-pac-nombre').textContent = btn.dataset.nombre;
$('env-pac-card').classList.remove('d-none');
// Construir campos de pre-llenado a partir del esquema
this._buildPrefillFields(btn.dataset);
},
_buildPrefillFields(pacData) {
// Campos editables que el enfermero puede pre-llenar (excluir estáticos y especiales)
const editables = this._esquema.filter(c =>
!['firma','separador','linked','parrafo','parrafo_inline','lista_marcable'].includes(c.tipo)
&& c.label // debe tener etiqueta
);
if (!editables.length) { $('env-prefill-cont').style.display = 'none'; return; }
// Auto-llenar campos linked desde datos del paciente
const linkedMap = {
nombre_completo: pacData.nombre || pacData.nombre_completo || '',
numero_documento: (pacData.doc||'').replace(/^(CC|CE|TI|PA)\s*/,''),
tipo_documento: (pacData.doc||'').split(' ')[0] || '',
telefono: pacData.tel || '',
email: pacData.email|| '',
fecha_nacimiento: pacData.nac || '',
eps: pacData.eps || '',
direccion: pacData.dir || '',
};
this._prefilled = { ...linkedMap };
const cont = $('env-prefill-fields');
cont.innerHTML = editables.slice(0, 10).map(c => {
const val = this._prefilled[c.id] || '';
if (c.tipo === 'select') {
const opts = (c.options||[]).map(o =>
`<option value="${esc(o)}" ${o===val?'selected':''}>${esc(o)}</option>`
).join('');
return `<div class="col-md-6">
<label class="form-label small">${esc(c.label)}</label>
<select class="form-select form-select-sm" data-fid="${c.id}" onchange="envio._updatePre(this)">
<option value="">— sin especificar —</option>${opts}
</select></div>`;
}
return `<div class="col-md-6">
<label class="form-label small">${esc(c.label)}</label>
<input type="${c.tipo==='numero'?'number':c.tipo==='fecha'?'date':'text'}"
class="form-control form-control-sm" data-fid="${c.id}"
value="${esc(val)}" placeholder="${esc(c.placeholder||'')}"
onchange="envio._updatePre(this)">
</div>`;
}).join('');
$('env-prefill-cont').style.display = '';
},
_updatePre(el) { this._prefilled[el.dataset.fid] = el.value; },
limpiarPaciente() {
this._pacienteId = null;
$('env-pac-busq').value = '';
$('env-pac-card').classList.add('d-none');
$('env-prefill-cont').style.display = 'none';
},
async generar() {
const formId = +$('env-form-id').value;
const via = document.querySelector('input[name="env-via"]:checked')?.value || 'link';
const datos = {
formulario_id: formId,
paciente_id: this._pacienteId || null,
datos_prefilled: this._prefilled,
enviado_via: via,
};
const r = await fetch('api/lab/send_formulario.php', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify(datos),
});
const d = await r.json();
if (!d.success) { alert(d.error); return; }
$('env-link-url').value = d.url;
$('env-wa-msg').value = d.mensaje_wa;
$('env-wa-btn').href = d.whatsapp_url;
$('env-wa-cont').style.display = via === 'whatsapp' ? '' : 'none';
$('env-paso1').style.display = 'none';
$('env-paso2').style.display = '';
$('env-footer1').style.display = 'none';
$('env-footer2').style.display = '';
envios._data = []; // invalidar caché
},
copiarLink() {
navigator.clipboard.writeText($('env-link-url').value).then(() => showToast('Link copiado'));
},
otroEnvio() {
$('env-paso1').style.display = '';
$('env-paso2').style.display = 'none';
$('env-footer1').style.display = '';
$('env-footer2').style.display = 'none';
this.limpiarPaciente();
},
};
// ══════════════════════════════════════════════════════════════════════
// VER RESPUESTA
// ══════════════════════════════════════════════════════════════════════
const verRespuesta = {
_envioId: null,
async abrir(envioId) {
this._envioId = envioId;
const e = envios._data.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 || '[]');
// Mapa id → label desde el esquema
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>
<p class="small text-muted mb-3">
Paciente: <strong>${esc(e.paciente_nombre||'—')}</strong>
&nbsp;·&nbsp;
Completado: ${esc((e.completado_en||'').slice(0,16).replace('T',' '))}
</p><hr class="my-2">`;
// Recorrer en orden del esquema
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>`;
});
} 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>`;
});
}
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">`;
}
$('resp-body').innerHTML = html;
$('btn-pdf-link').href = `ver_formulario_enviado.php?id=${envioId}`;
if (!_bsResp) _bsResp = new bootstrap.Modal('#modalRespuesta');
_bsResp.show();
},
imprimir() {
window.open(`ver_formulario_enviado.php?id=${this._envioId}`, '_blank');
},
};
// ══════════════════════════════════════════════════════════════════════
// RENDER CAMPO PARA PREVIEW/PÚBLICO
// ══════════════════════════════════════════════════════════════════════
function renderCampoPreview(c, valores) {
const v = valores[c.id] || valores[c.linked_key] || '';
if (c.tipo === 'separador') {
return `<div class="preview-field"><hr class="my-2"><p class="fw-bold small text-secondary mb-0">${esc(c.label)}</p></div>`;
}
const lbl = `<label class="form-label small fw-semibold mb-1">${esc(c.label)}${c.required?'<span class="text-danger ms-1">*</span>':''}</label>`;
if (c.tipo === 'linked') {
return `<div class="preview-field">${lbl}
<input type="text" class="form-control form-control-sm" value="${esc(v)}" disabled placeholder="Auto-llenado desde paciente">
</div>`;
}
if (c.tipo === 'textarea') {
return `<div class="preview-field">${lbl}
<textarea class="form-control form-control-sm" rows="2" disabled placeholder="${esc(c.placeholder||'')}"></textarea>
</div>`;
}
if (c.tipo === 'select') {
const opts = (c.options||[]).map(o => `<option>${esc(o)}</option>`).join('');
return `<div class="preview-field">${lbl}
<select class="form-select form-select-sm" disabled><option>— seleccionar —</option>${opts}</select>
</div>`;
}
if (c.tipo === 'radio') {
const opts = (c.options||[]).map(o =>
`<div class="form-check"><input class="form-check-input" type="radio" disabled>
<label class="form-check-label small">${esc(o)}</label></div>`
).join('');
return `<div class="preview-field">${lbl}${opts}</div>`;
}
if (c.tipo === 'checkbox') {
const opts = (c.options||[]).map(o =>
`<div class="form-check"><input class="form-check-input" type="checkbox" disabled>
<label class="form-check-label small">${esc(o)}</label></div>`
).join('');
return `<div class="preview-field">${lbl}${opts}</div>`;
}
if (c.tipo === 'firma') {
return `<div class="preview-field">${lbl}
<div class="border rounded p-2 text-center text-muted small bg-light" style="height:70px;line-height:50px">
✍️ Área de firma (paciente)</div></div>`;
}
if (c.tipo === 'firma_profesional') {
return `<div class="preview-field">${lbl}
<div class="border rounded p-2 text-center small fw-semibold" style="height:70px;line-height:50px;color:#198754;background:#f0fff4;border-color:#198754 !important">
✍️ Área de firma (profesional)</div></div>`;
}
if (c.tipo === 'parrafo') {
const txt = (c.contenido || '');
const ws = c.flujoLibre ? 'normal' : 'pre-wrap';
return `<div class="preview-field" style="font-size:11px;line-height:1.7;color:#222;text-align:justify;white-space:${ws};border-top:1px solid #eee;padding-top:6px">${esc(txt.slice(0,200))}${txt.length>200?'…':''}</div>`;
}
if (c.tipo === 'parrafo_inline') {
const renderLine = line => line.split(/(\{[a-z_]+\})/g).map((p, i) => {
if (i % 2 === 1) {
const key = p.slice(1,-1);
return `<span style="display:inline-block;min-width:60px;border-bottom:1px dashed #888;color:#0055aa;font-size:10px;font-style:italic;vertical-align:baseline;padding:0 2px">${esc(key)}</span>`;
}
return esc(p);
}).join('');
const html2 = (c.contenido||'').split('\n').map(renderLine).join('<br>');
return `<div class="preview-field" style="font-size:11px;line-height:2;color:#222;text-align:justify;border-top:1px solid #eee;padding-top:6px">${html2}</div>`;
}
if (c.tipo === 'lista_marcable') {
const items = (c.items||[]).map((it, i) =>
`<div class="form-check"><input class="form-check-input" type="checkbox" disabled>
<label class="form-check-label small">${i+1}. ${esc(it)}</label></div>`
).join('');
return `<div class="preview-field">${lbl}${items}</div>`;
}
const t = c.tipo === 'numero' ? 'number' : c.tipo === 'fecha' ? 'date' : c.tipo === 'hora' ? 'time' : 'text';
return `<div class="preview-field">${lbl}
<input type="${t}" class="form-control form-control-sm" value="${esc(v)}"
placeholder="${esc(c.placeholder||'')}" disabled>
</div>`;
}
// ══════════════════════════════════════════════════════════════════════
// HELPERS UI
// ══════════════════════════════════════════════════════════════════════
function showToast(msg) {
const t = document.createElement('div');
t.className = 'alert alert-success position-fixed bottom-0 end-0 m-3 shadow py-2 px-3';
t.style.zIndex = '9999';
t.textContent = msg;
document.body.appendChild(t);
setTimeout(() => t.remove(), 3000);
}
document.addEventListener('keydown', e => {
if (e.target.id === 'env-pac-busq' && e.key === 'Enter') envio.buscarPaciente();
});
// ══════════════════════════════════════════════════════════════════════
// INIT
// ══════════════════════════════════════════════════════════════════════
// Escuchar mensaje del builder cuando guarda un formulario
window.addEventListener('message', e => {
if (e.data === 'builder:saved') {
cargarFormularios();
showToast('✅ Formulario guardado');
}
});
document.addEventListener('DOMContentLoaded', async () => {
// Cargar config global primero
try {
const r = await fetch('api/lab/get_config.php');
const d = await r.json();
if (d.success) _globalCfg = d.config || {};
} catch(e) {}
cargarFormularios();
});
</script>
<script src="assets/js/lab-sidebar.js"></script>
</body>
</html>