- Panel de protocolo ahora tiene botón "Usar todas" para cerrar sin configurar - Si todos los grupos ya tienen una sola toma, se guarda config silenciosamente y se recarga sin mostrar panel - Lógica de guardado extraída a _mpGuardarConfig() compartida entre ambos botones - Botón de acceso a lab_tomas_config.php en tarjeta del formulario es_toma_progresiva Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1068 lines
54 KiB
PHP
1068 lines
54 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 (!defined('APP_ROOT')) { define('APP_ROOT', __DIR__); }
|
||
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); }
|
||
|
||
/* ── Altura uniforme filtros ─────────────────────── */
|
||
#panel-envios .form-control-sm,
|
||
#panel-envios .form-select-sm { height:31px; padding-top:.25rem; padding-bottom:.25rem; }
|
||
|
||
/* ── Colores estado envío ───────────────────────── */
|
||
.badge-pendiente { background:#6c757d; }
|
||
.badge-completado { background:#0d6efd; }
|
||
.badge-firmado { background:#198754; }
|
||
.badge-expirado { background:#dc3545; }
|
||
|
||
/* ── Animación toast ────────────────────────────── */
|
||
@keyframes fadeInUp {
|
||
from { opacity:0; transform:translateY(12px); }
|
||
to { opacity:1; transform:translateY(0); }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<?php
|
||
$SIDEBAR_TITLE = 'Panel Lab';
|
||
$SIDEBAR_ICON = 'fas fa-flask';
|
||
require_once __DIR__ . '/shared/components/sidebar.php';
|
||
?>
|
||
|
||
<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="mb-3">
|
||
<div class="input-group input-group-sm" style="max-width:320px">
|
||
<span class="input-group-text"><i class="fas fa-search"></i></span>
|
||
<input type="text" id="inp-buscar-form" class="form-control"
|
||
placeholder="Buscar formulario…" oninput="filtrarFormularios(this.value)" autocomplete="off">
|
||
</div>
|
||
</div>
|
||
<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">
|
||
<!-- Filtros en una sola fila compacta -->
|
||
<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="env-f-desde" class="form-control form-control-sm" oninput="envios.filtrar()">
|
||
</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="env-f-hasta" class="form-control form-control-sm" oninput="envios.filtrar()">
|
||
</div>
|
||
<div class="col-12 col-sm-3">
|
||
<label class="form-label form-label-sm mb-1 text-muted">Formulario</label>
|
||
<select id="env-f-formulario" class="form-select form-select-sm" onchange="envios.filtrar()">
|
||
<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="env-f-estado" class="form-select form-select-sm" onchange="envios.filtrar()">
|
||
<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-2">
|
||
<label class="form-label form-label-sm mb-1 text-muted">Origen</label>
|
||
<select id="env-f-origen" class="form-select form-select-sm" onchange="envios.filtrar()">
|
||
<option value="">Todos</option>
|
||
<option value="turnero">Turnero</option>
|
||
<option value="otros">Otros</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="env-f-paciente" class="form-control form-control-sm"
|
||
placeholder="Buscar…" oninput="envios.filtrar()">
|
||
</div>
|
||
<div class="col-12 col-sm-auto ms-sm-auto d-flex gap-2">
|
||
<button class="btn btn-success btn-sm" onclick="envios.exportarExcel()" 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="envios.limpiarFiltros()" title="Limpiar filtros">
|
||
<i class="fas fa-times"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Spinner de carga -->
|
||
<div id="env-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 envíos…</p>
|
||
</div>
|
||
|
||
<div class="table-responsive" id="env-tabla-wrap">
|
||
<table class="table table-hover align-middle small" id="tabla-envios">
|
||
<thead class="table-light">
|
||
<tr>
|
||
<th>Formulario</th>
|
||
<th>Paciente</th>
|
||
<th>Origen</th>
|
||
<th>Estado</th>
|
||
<th>Fecha</th>
|
||
<th>Acciones</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="tbody-envios">
|
||
<tr><td colspan="7" class="text-center text-muted py-4">Cargando…</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<!-- Paginación -->
|
||
<div class="d-flex align-items-center justify-content-between mt-3 flex-wrap gap-2">
|
||
<small class="text-muted" id="env-pag-info"></small>
|
||
<nav>
|
||
<ul class="pagination pagination-sm mb-0" id="env-paginacion"></ul>
|
||
</nav>
|
||
</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 =>
|
||
({'<':'<','>':'>','&':'&','"':'"',"'":'''}[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.poblarSelectFormulario(); envios.cargar(1); }
|
||
}
|
||
};
|
||
|
||
// ══════════════════════════════════════════════════════════════════════
|
||
// 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 filtrarFormularios(q) {
|
||
q = q.trim().toLowerCase();
|
||
const grid = $('formularios-grid');
|
||
if (!q) { renderFormularios(); return; }
|
||
const filtrados = _formularios.filter(f =>
|
||
(f.nombre || '').toLowerCase().includes(q) ||
|
||
(f.tipo || '').toLowerCase().includes(q)
|
||
);
|
||
const orig = _formularios;
|
||
_formularios = filtrados;
|
||
renderFormularios();
|
||
_formularios = orig;
|
||
}
|
||
|
||
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>
|
||
${f.es_toma_progresiva && PUEDE_ESCRIBIR ? `
|
||
<a class="btn btn-sm btn-outline-info" href="lab_tomas_config.php"
|
||
title="Configurar tipos de examen de tomas prolongadas">
|
||
<i class="fas fa-vials"></i>
|
||
</a>` : ''}
|
||
${PUEDE_ESCRIBIR ? `
|
||
<button class="btn btn-sm btn-outline-secondary" onclick="clonarFormulario(${f.id},'${esc(f.nombre)}')"
|
||
title="Clonar formulario"><i class="fas fa-copy"></i></button>
|
||
<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 clonarFormulario(id, nombre) {
|
||
if (!confirm(`¿Clonar el formulario "${nombre}"?\nSe creará una copia con el nombre "Copia de ${nombre}".`)) return;
|
||
const r = await fetch('api/lab/save_formulario.php', {
|
||
method:'POST', headers:{'Content-Type':'application/json'},
|
||
body: JSON.stringify({ id, clonar: true }),
|
||
});
|
||
const d = await r.json();
|
||
if (d.success || d.ok) {
|
||
showToast('✅ Formulario clonado');
|
||
cargarFormularios();
|
||
} else {
|
||
alert(d.error || 'Error al clonar');
|
||
}
|
||
}
|
||
|
||
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 POR_PAGINA = 20;
|
||
const envios = {
|
||
_pagina: 1,
|
||
_total: 0,
|
||
_paginas: 1,
|
||
_paginaData: [],
|
||
_debTimer: null,
|
||
|
||
_params() {
|
||
return new URLSearchParams({
|
||
envios: 1,
|
||
page: this._pagina,
|
||
limit: POR_PAGINA,
|
||
fecha_desde: $('env-f-desde').value || '',
|
||
fecha_hasta: $('env-f-hasta').value || '',
|
||
paciente: $('env-f-paciente').value.trim() || '',
|
||
formulario_id: $('env-f-formulario').value || '',
|
||
estado: $('env-f-estado').value || '',
|
||
origen: $('env-f-origen').value || '',
|
||
});
|
||
},
|
||
|
||
async cargar(pag = 1) {
|
||
this._pagina = pag;
|
||
const spinner = $('env-spinner');
|
||
const wrap = $('env-tabla-wrap');
|
||
if (spinner) spinner.style.display = '';
|
||
if (wrap) wrap.style.display = 'none';
|
||
try {
|
||
const r = await fetch('api/lab/get_formularios.php?' + this._params());
|
||
const d = await r.json();
|
||
this._paginaData = d.data || [];
|
||
this._total = d.total || 0;
|
||
this._paginas = d.paginas || 1;
|
||
this._render();
|
||
} catch(e) {
|
||
const tb = $('tbody-envios');
|
||
if (tb) tb.innerHTML = '<tr><td colspan="7" class="text-center text-danger py-4">Error al cargar envíos</td></tr>';
|
||
} finally {
|
||
if (spinner) spinner.style.display = 'none';
|
||
if (wrap) wrap.style.display = '';
|
||
}
|
||
},
|
||
|
||
filtrar() {
|
||
clearTimeout(this._debTimer);
|
||
this._debTimer = setTimeout(() => this.cargar(1), 350);
|
||
},
|
||
|
||
limpiarFiltros() {
|
||
$('env-f-desde').value = '';
|
||
$('env-f-hasta').value = '';
|
||
$('env-f-paciente').value = '';
|
||
$('env-f-formulario').value = '';
|
||
$('env-f-estado').value = '';
|
||
$('env-f-origen').value = '';
|
||
this.cargar(1);
|
||
},
|
||
|
||
poblarSelectFormulario() {
|
||
const sel = $('env-f-formulario');
|
||
if (!sel || sel.options.length > 1) return;
|
||
(_formularios || []).forEach(f => {
|
||
const opt = document.createElement('option');
|
||
opt.value = f.id;
|
||
opt.textContent = f.nombre || ('Formulario #' + f.id);
|
||
sel.appendChild(opt);
|
||
});
|
||
},
|
||
|
||
async exportarExcel() {
|
||
const btnExcel = document.querySelector('[onclick="envios.exportarExcel()"]');
|
||
if (btnExcel) { btnExcel.disabled = true; btnExcel.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Exportando…'; }
|
||
let rows = [];
|
||
try {
|
||
const p = this._params();
|
||
p.set('export', '1'); p.delete('page'); p.delete('limit');
|
||
const r = await fetch('api/lab/get_formularios.php?' + p);
|
||
const d = await r.json();
|
||
rows = d.data || [];
|
||
} finally {
|
||
if (btnExcel) { btnExcel.disabled = false; btnExcel.innerHTML = '<i class="fas fa-file-excel me-1"></i><span class="d-none d-md-inline">Excel</span>'; }
|
||
}
|
||
if (!rows.length) { showToast('Sin datos para exportar', 'warning'); return; }
|
||
|
||
const fixedCols = ['Formulario','Categoría','Paciente','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.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:'envios_formularios_'+new Date().toISOString().slice(0,10)+'.csv'});
|
||
document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url);
|
||
showToast('✅ Excel descargado — ' + rows.length + ' registro(s)', 'success', 4000);
|
||
},
|
||
|
||
_render() {
|
||
const tb = $('tbody-envios');
|
||
if (!this._paginaData.length) {
|
||
tb.innerHTML = '<tr><td colspan="7" class="text-center text-muted py-4">Sin envíos para los filtros seleccionados</td></tr>';
|
||
$('env-pag-info').textContent = '';
|
||
$('env-paginacion').innerHTML = '';
|
||
return;
|
||
}
|
||
const inicio = (this._pagina - 1) * POR_PAGINA;
|
||
tb.innerHTML = this._paginaData.map(e => {
|
||
const info = ESTADO_ENV[e.estado] || {};
|
||
const esTurnero = e.origen === 'turnero';
|
||
const origenBadge = esTurnero
|
||
? `<span class="badge bg-info text-dark">Turnero${e.turno_codigo ? ' #'+esc(e.turno_codigo):''}</span>`
|
||
: `<span class="badge bg-secondary">Otros</span>`;
|
||
const viewHref = esTurnero
|
||
? `ver_formulario_enviado.php?token=${esc(e.token)}`
|
||
: `ver_formulario_enviado.php?id=${e.id}`;
|
||
const fecha = (e.fecha||'').slice(0,16).replace('T',' ');
|
||
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>${origenBadge}</td>
|
||
<td><span class="badge text-white ${info.cls}">${info.icon} ${e.estado}</span></td>
|
||
<td><small>${esc(fecha)}</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)
|
||
? (esTurnero
|
||
? `<a class="btn btn-xs btn-outline-danger ms-1" href="${viewHref}" target="_blank"><i class="fas fa-file-pdf"></i></a>`
|
||
: `<button class="btn btn-xs btn-outline-primary ms-1" onclick="verRespuesta.abrir(${e.id})"><i class="fas fa-eye"></i></button>
|
||
<a class="btn btn-xs btn-outline-danger ms-1" href="${viewHref}" target="_blank"><i class="fas fa-file-pdf"></i></a>`)
|
||
: ''}
|
||
</td>
|
||
</tr>`;
|
||
}).join('');
|
||
|
||
$('env-pag-info').textContent =
|
||
`Mostrando ${inicio+1}–${inicio+this._paginaData.length} de ${this._total} envío(s)`;
|
||
|
||
const ul = $('env-paginacion');
|
||
const pag = this._pagina, tot = this._paginas;
|
||
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?`envios.cargar(${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);
|
||
},
|
||
|
||
irAPagina(p) { this.cargar(p); $('panel-envios').scrollIntoView({behavior:'smooth',block:'start'}); },
|
||
|
||
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._paginaData.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>
|
||
·
|
||
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, tipo = 'success', ms = 3000) {
|
||
const t = document.createElement('div');
|
||
t.className = `alert alert-${tipo} d-flex align-items-center gap-2 position-fixed bottom-0 end-0 m-3 shadow-sm py-2 px-3`;
|
||
t.style.cssText = 'z-index:9999;min-width:220px;max-width:360px;font-size:.875rem;border-radius:8px;animation:fadeInUp .2s ease';
|
||
t.innerHTML = `<span style="flex:1">${esc(msg)}</span><button type="button" class="btn-close btn-close-sm ms-2" style="font-size:.65rem" onclick="this.closest('.alert').remove()"></button>`;
|
||
document.body.appendChild(t);
|
||
setTimeout(() => t && t.remove(), ms);
|
||
}
|
||
|
||
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>
|