827 lines
42 KiB
PHP
827 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();
|
|
?>
|
|
<!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 ($esAdmin): ?>
|
|
<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' ?>;
|
|
|
|
// 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.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>
|
|
${ES_ADMIN ? '<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>
|
|
${ES_ADMIN ? `
|
|
<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 (!ES_ADMIN) return;
|
|
this._abrirVentana('lab_formulario_builder.php');
|
|
},
|
|
|
|
editar(id) {
|
|
if (!ES_ADMIN) 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>
|
|
·
|
|
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>
|