Files
whatsapp/form_cliente.php
T
2026-03-10 18:42:51 -05:00

832 lines
40 KiB
PHP

<?php
/**
* form_cliente.php — Página pública para que el cliente llene / firme el formulario
* NO REQUIERE AUTENTICACIÓN. Acceso via token: ?t=TOKEN
*/
$token = trim($_GET['t'] ?? '');
?>
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Formulario</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
<style>
/* ── Base ──────────────────────────────────────── */
body { background:#f0f4ff; min-height:100vh; font-size:16px; }
.form-wrap { max-width:540px; margin:0 auto; padding:1rem; }
/* ── Header ────────────────────────────────────── */
.form-header { background:linear-gradient(135deg,#1565c0,#0288d1);
border-radius:12px 12px 0 0; padding:24px 20px; }
/* ── Card ───────────────────────────────────────── */
.form-card { background:#fff; border-radius:0 0 12px 12px;
padding:20px; box-shadow:0 4px 20px rgba(0,0,0,.12); }
/* ── Linked field (read-only) ───────────────────── */
.field-linked { background:#eef3ff; border-color:#93b4f7; }
/* ── Inline linked field dentro de párrafo ──────── */
.inline-linked {
display:inline-block;
border:none;
border-bottom:1.5px solid #555;
background:transparent;
min-width:80px;
font-size:inherit;
line-height:inherit;
color:#1a56db;
font-weight:600;
padding:0 3px;
vertical-align:baseline;
outline:none;
}
.inline-linked:empty { min-width:80px; }
.inline-linked[data-editable] { border-bottom-color:#1a56db; cursor:text; }
/* ── Firma canvas ───────────────────────────────── */
#firma-canvas { border:2px solid #1565c0; border-radius:8px;
cursor:crosshair; touch-action:none;
background:#fff; display:block; width:100%; height:150px; }
#firma-canvas.empty { border-style:dashed; border-color:#adb5bd; }
/* ── Firma modos ─────────────────────────────── */
.firma-modo-btns { display:flex; gap:8px; margin-bottom:12px; }
.firma-modo-btn { flex:1; padding:7px 0; border:2px solid #dee2e6; border-radius:8px;
background:#f8f9fa; cursor:pointer; font-size:.85rem; font-weight:600;
color:#6c757d; transition:all .2s; text-align:center; }
.firma-modo-btn.activo { border-color:#1565c0; background:#e8f0fe; color:#1565c0; }
.firma-foto-drop { border:2px dashed #adb5bd; border-radius:8px; padding:28px 16px;
text-align:center; color:#6c757d; cursor:pointer;
transition:border-color .2s; background:#fff; display:block; }
.firma-foto-drop:hover, .firma-foto-drop.dragover { border-color:#1565c0; color:#1565c0; }
#firma-foto-preview { max-width:100%; max-height:160px; border-radius:8px;
border:2px solid #1565c0; display:none; margin-top:8px; object-fit:contain; }
/* ── Estado chips ────────────────────────────────── */
.chip-estado { display:inline-flex; align-items:center; gap:6px;
padding:6px 14px; border-radius:100px; font-size:.85rem;
font-weight:600; }
/* ── Success overlay ─────────────────────────────── */
.check-circle { width:80px; height:80px; border-radius:50%;
background:#e8f5e9; display:flex; align-items:center;
justify-content:center; margin:0 auto; }
@keyframes pop { 0%{transform:scale(0)} 80%{transform:scale(1.15)} 100%{transform:scale(1)} }
.check-circle svg { animation:pop .4s ease both; }
/* ── Error screen ─────────────────────────────────── */
/* ── Loading screen ──────────────────────────────── */
#loading-screen { min-height:50vh; display:flex; flex-direction:column;
align-items:center; justify-content:center; gap:12px; }
</style>
</head>
<body>
<div class="form-wrap">
<!-- ── LOADING ──────────────────────────────────────── -->
<div id="loading-screen">
<div class="spinner-border text-primary" style="width:3rem;height:3rem;"></div>
<p class="text-muted">Cargando formulario…</p>
</div>
<!-- ── ERROR ─────────────────────────────────────────── -->
<div id="error-screen" style="display:none">
<div class="text-center py-5">
<div style="font-size:4rem">😔</div>
<h5 id="err-title" class="mt-3 fw-bold text-danger">Formulario no disponible</h5>
<p id="err-msg" class="text-muted"></p>
</div>
</div>
<!-- ── YA FIRMADO (bloqueado) ───────────────────── -->
<div id="ya-firmado-screen" style="display:none">
<div class="form-header text-white" style="border-radius:12px 12px 0 0">
<!-- Encabezado empresa -->
<div id="yf-empresa-header" style="display:none;border-bottom:1px solid rgba(255,255,255,.3);margin-bottom:10px;padding-bottom:10px" class="d-flex align-items-center gap-2">
<img id="yf-empresa-logo" src="" alt="" style="max-height:40px;border-radius:5px;background:rgba(255,255,255,.2);padding:3px;display:none">
<div>
<div id="yf-empresa-nombre" class="fw-bold" style="font-size:.9rem"></div>
<div id="yf-empresa-subtitulo" class="opacity-75" style="font-size:.75rem"></div>
</div>
</div>
<h5 class="fw-bold mb-0 text-center">
<i class="fas fa-lock me-2"></i>Formulario ya completado
</h5>
</div>
<div class="form-card text-center">
<div class="signed-icon mx-auto mb-3">
<i class="fas fa-file-signature" style="font-size:2.5rem;color:#1565c0"></i>
</div>
<h5 class="fw-bold mb-1" id="yf-titulo">Formulario</h5>
<p class="text-muted small mb-3" id="yf-paciente"></p>
<span id="yf-estado-badge" class="badge mb-3" style="font-size:.85rem"></span>
<div id="yf-firma-thumb" style="display:none" class="mb-3">
<p class="small text-muted fw-semibold mb-1">Firma registrada:</p>
<img id="yf-firma-img" src="" alt="Firma"
class="border rounded p-2" style="max-height:120px;max-width:100%">
</div>
<a id="yf-pdf-btn" href="#" target="_blank"
class="btn btn-danger w-100 mb-2 fw-semibold">
<i class="fas fa-file-pdf me-2"></i>Ver documento firmado (PDF)
</a>
<div id="yf-hash-box" style="display:none" class="mt-3 text-start">
<p class="small text-muted mb-1"><i class="fas fa-shield-alt me-1"></i>Sello de integridad SHA-256:</p>
<code id="yf-hash" class="d-block" style="font-size:10px;word-break:break-all;
background:#f0f5ff;border:1px solid #c3d3f7;border-radius:4px;padding:6px 8px;
color:#1e3a6e">—</code>
</div>
<div class="alert alert-warning border mt-3 text-start small">
<i class="fas fa-lock me-1"></i>
Este formulario ya fue firmado y <strong>no puede editarse</strong>.
Una copia del documento fue registrada con sello digital.
</div>
</div>
</div>
<!-- ── FORMULARIO ────────────────────────────────────── -->
<div id="form-screen" style="display:none">
<div class="form-header text-white mb-0" id="form-main-header">
<!-- Encabezado empresa (se muestra con config) -->
<div id="f-empresa-header" style="display:none;border-bottom:1px solid rgba(255,255,255,.3);margin-bottom:10px;padding-bottom:10px" class="d-flex align-items-center gap-2">
<img id="f-empresa-logo" src="" alt="" style="max-height:48px;border-radius:5px;background:rgba(255,255,255,.2);padding:3px;display:none">
<div>
<div id="f-empresa-nombre" class="fw-bold" style="font-size:.95rem"></div>
<div id="f-empresa-subtitulo" class="opacity-75" style="font-size:.78rem"></div>
</div>
</div>
<div class="d-flex align-items-start justify-content-between">
<div>
<h5 class="fw-bold mb-1" id="f-titulo">Formulario</h5>
<p class="mb-0 opacity-75 small" id="f-desc"></p>
</div>
<div id="f-badge"></div>
</div>
<div class="mt-3 pt-2 border-top border-white border-opacity-25 small opacity-75" id="f-paciente-info"></div>
</div>
<div class="form-card">
<!-- Nota para el paciente -->
<div class="alert alert-info py-2 small mb-3" id="instruccion-nota">
<i class="fas fa-info-circle me-1"></i>
Por favor, completa los campos y firma al final.
</div>
<!-- Campos del formulario -->
<form id="form-main" novalidate>
<div id="campos-container"></div>
<!-- ── Firma ─────────────────────────────── -->
<div id="firma-container" style="display:none" class="mt-4">
<hr>
<label class="form-label fw-semibold">
<i class="fas fa-signature me-1 text-primary"></i>
Firma digital
<span id="firma-req-badge" class="badge bg-danger ms-1 small" style="display:none">Requerida</span>
</label>
<!-- área canvas (dibujo) -->
<div id="firma-canvas-area">
<p class="text-muted small mb-2">✍️ Dibuja tu firma con el dedo o el mouse.</p>
<canvas id="firma-canvas" class="empty"></canvas>
<div class="d-flex gap-2 mt-2">
<button type="button" class="btn btn-outline-secondary btn-sm"
onclick="firma.limpiarCanvas()">
<i class="fas fa-eraser me-1"></i>Limpiar firma
</button>
<span id="firma-status" class="small text-muted align-self-center">Sin firma</span>
</div>
</div>
<!-- área foto -->
<div id="firma-foto-area" class="mt-3">
<p class="text-muted small mb-2">📷 Adjunta una foto de tu firma o documento.</p>
<label class="firma-foto-drop" id="firma-foto-drop" for="firma-foto-input">
<i class="fas fa-camera fa-2x d-block mb-2"></i>
Toca para abrir cámara o galería
</label>
<input type="file" id="firma-foto-input" accept="image/*" capture="environment"
class="d-none" onchange="firma._onFotoChange(this)">
<img id="firma-foto-preview" src="" alt="Vista previa de firma">
<div class="d-flex gap-2 mt-2">
<button type="button" class="btn btn-outline-secondary btn-sm"
onclick="firma.limpiarFoto()">
<i class="fas fa-eraser me-1"></i>Quitar foto
</button>
<span id="firma-foto-status" class="small text-muted align-self-center">Sin foto</span>
</div>
</div>
</div>
<!-- ── Submit ─────────────────────────────── -->
<div class="d-grid mt-4">
<button type="button" class="btn btn-primary btn-lg fw-bold"
id="btn-enviar" onclick="formCliente.enviar()">
<i class="fas fa-check-circle me-2"></i>Enviar formulario
</button>
</div>
<p class="text-muted text-center small mt-2">
<i class="fas fa-lock me-1"></i>Tus datos están protegidos
</p>
</form>
</div>
</div>
<!-- ── ÉXITO ─────────────────────────────────────────── -->
<div id="success-screen" style="display:none">
<div class="form-header text-white" style="border-radius:12px 12px 0 0">
<h5 class="fw-bold mb-0 text-center"><i class="fas fa-check-circle me-2"></i>¡Formulario enviado!</h5>
</div>
<div class="form-card text-center">
<div class="check-circle mt-2 mb-3">
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 52 52">
<circle cx="26" cy="26" r="25" fill="none" stroke="#4caf50" stroke-width="2"/>
<path fill="none" stroke="#4caf50" stroke-width="4" stroke-linecap="round"
stroke-linejoin="round" d="M14 27 l7 7 l17-17"/>
</svg>
</div>
<h5 class="fw-bold">¡Gracias! 🎉</h5>
<p class="text-muted" id="success-msg">Tu respuesta ha sido registrada correctamente.</p>
<div id="success-firma-thumb" class="mt-3" style="display:none">
<p class="small text-muted fw-semibold">Firma registrada:</p>
<img id="success-firma-img" src="" alt="Firma"
class="border rounded p-2" style="max-height:120px;max-width:100%">
</div>
<!-- Link PDF -->
<div id="success-pdf-row" style="display:none" class="mt-3">
<a id="success-pdf-btn" href="#" target="_blank"
class="btn btn-danger w-100 fw-semibold">
<i class="fas fa-file-pdf me-2"></i>Descargar documento firmado (PDF)
</a>
</div>
<!-- Hash SHA-256 -->
<div id="success-hash-box" style="display:none" class="mt-3 text-start">
<p class="small text-muted mb-1 fw-semibold">
<i class="fas fa-shield-alt me-1 text-success"></i>Sello de integridad del documento:
</p>
<code id="success-hash" class="d-block" style="font-size:10px;word-break:break-all;
background:#f0f5ff;border:1px solid #c3d3f7;border-radius:4px;padding:6px 8px;
color:#1e3a6e"></code>
</div>
<div class="alert alert-light border mt-4 text-start small">
<i class="fas fa-clock me-1 text-muted"></i>
Tu información ha sido recibida y está en manos del equipo médico.
Puedes cerrar esta página.
</div>
</div>
</div>
</div>
<script>
// ══════════════════════════════════════════════════════════════════════
// CONFIG
// ══════════════════════════════════════════════════════════════════════
const TOKEN = <?= json_encode($token) ?>;
const API = 'api/lab/submit_formulario.php';
let _data = null; // respuesta de verificar token
let _cfg = {}; // config de diseño del laboratorio
let _firmaDibujada = false;
// ══════════════════════════════════════════════════════════════════════
// HELPERS
// ══════════════════════════════════════════════════════════════════════
const $ = id => document.getElementById(id);
function esc(s) {
return String(s||'').replace(/[<>&"']/g, c=>
({'<':'&lt;','>':'&gt;','&':'&amp;','"':'&quot;',"'":'&#39;'}[c])
);
}
function show(id) { $(id).style.display = ''; }
function hide(id) { $(id).style.display = 'none'; }
function showBlock(id) { $(id).style.display = 'block'; }
// ══════════════════════════════════════════════════════════════════════
// FIRMA DIGITAL
// ══════════════════════════════════════════════════════════════════════
const firma = (() => {
let canvas, ctx, drawing = false, lastX = 0, lastY = 0;
function init() {
canvas = $('firma-canvas');
ctx = canvas.getContext('2d');
// Escalar para retina
const ratio = window.devicePixelRatio || 1;
canvas.width = canvas.offsetWidth * ratio;
canvas.height = canvas.offsetHeight * ratio;
ctx.scale(ratio, ratio);
ctx.strokeStyle = '#1a1a2e';
ctx.lineWidth = 2.5;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
const getPos = e => {
const rect = canvas.getBoundingClientRect();
if (e.touches) {
const scX = canvas.offsetWidth / rect.width;
const scY = canvas.offsetHeight / rect.height;
return {
x: (e.touches[0].clientX - rect.left) * scX,
y: (e.touches[0].clientY - rect.top) * scY,
};
}
return { x: e.offsetX, y: e.offsetY };
};
// Mouse
canvas.addEventListener('mousedown', e => { drawing = true; const p=getPos(e); lastX=p.x; lastY=p.y; });
canvas.addEventListener('mousemove', e => { if (!drawing) return; trazo(getPos(e)); });
canvas.addEventListener('mouseup', ()=> { drawing = false; ctx.beginPath(); });
canvas.addEventListener('mouseleave', ()=> { drawing = false; ctx.beginPath(); });
// Touch
canvas.addEventListener('touchstart', e => { e.preventDefault(); drawing=true; const p=getPos(e); lastX=p.x; lastY=p.y; }, {passive:false});
canvas.addEventListener('touchmove', e => { e.preventDefault(); if (!drawing) return; trazo(getPos(e)); }, {passive:false});
canvas.addEventListener('touchend', ()=> { drawing=false; ctx.beginPath(); });
}
function trazo(p) {
ctx.beginPath();
ctx.moveTo(lastX, lastY);
ctx.lineTo(p.x, p.y);
ctx.stroke();
lastX = p.x; lastY = p.y;
_firmaDibujada = true;
canvas.classList.remove('empty');
$('firma-status').textContent = '✅ Firma lista';
$('firma-status').className = 'small text-success align-self-center fw-semibold';
}
let _fotoDato = null;
// setModos: controla qué secciones se muestran (sin tabs exclusivos)
function setModos(modos) {
const m = Array.isArray(modos) && modos.length ? modos : ['canvas','foto'];
$('firma-canvas-area').style.display = m.includes('canvas') ? '' : 'none';
$('firma-foto-area').style.display = m.includes('foto') ? '' : 'none';
}
function _onFotoChange(input) {
const file = input.files && input.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = e => {
_fotoDato = e.target.result;
const preview = $('firma-foto-preview');
preview.src = _fotoDato;
preview.style.display = 'block';
$('firma-foto-drop').style.display = 'none';
$('firma-foto-status').textContent = '✅ Foto lista';
$('firma-foto-status').className = 'small text-success align-self-center fw-semibold';
};
reader.readAsDataURL(file);
}
function limpiarCanvas() {
ctx.clearRect(0, 0, canvas.offsetWidth, canvas.offsetHeight);
_firmaDibujada = false;
canvas.classList.add('empty');
$('firma-status').textContent = 'Sin firma';
$('firma-status').className = 'small text-muted align-self-center';
}
function limpiarFoto() {
_fotoDato = null;
$('firma-foto-preview').src = '';
$('firma-foto-preview').style.display = 'none';
$('firma-foto-drop').style.display = 'block';
$('firma-foto-status').textContent = 'Sin foto';
$('firma-foto-status').className = 'small text-muted align-self-center';
$('firma-foto-input').value = '';
}
function obtenerSVG() { if (!_firmaDibujada) return null; return canvas.toDataURL('image/png'); }
function obtenerFoto() { return _fotoDato || null; }
return { init, limpiarCanvas, limpiarFoto, obtenerSVG, obtenerFoto, setModos, _onFotoChange };
})();
// ══════════════════════════════════════════════════════════════════════
// RENDER CAMPOS
// ══════════════════════════════════════════════════════════════════════
function renderCampos(esquema, prefilled) {
const cont = $('campos-container');
let html = '';
const pad = prefilled || {};
esquema.forEach(c => {
if (c.tipo === 'separador') {
html += `<div class="mt-4 mb-2">
<p class="fw-bold text-secondary small text-uppercase mb-0">${esc(c.label)}</p>
<hr class="mt-1 mb-3">
</div>`;
return;
}
if (c.tipo === 'firma') return; // se maneja aparte
if (c.tipo === 'parrafo') {
const ws = c.flujoLibre ? 'normal' : 'pre-wrap';
html += `<div class="mb-3" style="font-size:.88rem;line-height:1.75;color:#222;text-align:justify;white-space:${ws}">${esc(c.contenido||'')}</div>`;
return;
}
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);
const val = pad['__paciente']?.[key] || pad[key] || '';
const hasVal = val.trim() !== '';
const w = Math.max(80, val.length * 9 + 24);
if (hasVal) {
return `<input type="text" class="inline-linked" readonly name="${c.id}_${key}" value="${esc(val)}" style="min-width:${w}px">`;
} else {
return `<input type="text" class="inline-linked" data-editable name="${c.id}_${key}" placeholder="${esc(key.replace(/_/g,' '))}" style="min-width:${w}px">`;
}
}
return esc(p);
}).join('');
const inlineHtml = (c.contenido||'').split('\n').map(renderLine).join('<br>');
html += `<div class="mb-3" style="font-size:.88rem;line-height:2.4;color:#222;text-align:justify">${inlineHtml}</div>`;
return;
}
if (c.tipo === 'lista_marcable') {
const star = c.required ? '<span class="text-danger ms-1">*</span>' : '';
const lbl = `<label class="form-label small fw-semibold mb-1">${esc(c.label)}${star}</label>`;
const opts = (c.items||[]).map((it, i) => `
<div class="form-check">
<input class="form-check-input" type="checkbox" name="${c.id}" value="${esc(it)}">
<label class="form-check-label">${i+1}. ${esc(it)}</label>
</div>`).join('');
html += `<div class="mb-3">${lbl}${opts}</div>`;
return;
}
const req = c.required ? 'required' : '';
const star = c.required ? '<span class="text-danger ms-1">*</span>' : '';
// Valor pre-llenado (linked usa __paciente.xxx o key directo)
let val = '';
if (c.tipo === 'linked') {
val = pad['__paciente']?.[c.linked_key] || pad[c.linked_key] || '';
} else {
val = pad[c.id] || '';
}
const isLinked = c.tipo === 'linked';
const isReadOnly = isLinked;
const linkedNote = isLinked
? '<small class="text-info d-block mt-1"><i class="fas fa-link me-1"></i>Campo autocompletado</small>' : '';
const lbl = `<label class="form-label small fw-semibold mb-1">${esc(c.label)}${star}</label>`;
if (c.tipo === 'textarea') {
html += `<div class="mb-3">${lbl}
<textarea class="form-control ${isReadOnly?'field-linked':''}"
name="${c.id}" rows="3" ${req} ${isReadOnly?'readonly':''}
placeholder="${esc(c.placeholder||'')}">${esc(val)}</textarea>${linkedNote}
</div>`;
} else if (c.tipo === 'select') {
const opts = (c.options||[]).map(o =>
`<option value="${esc(o)}" ${o===val?'selected':''}>${esc(o)}</option>`
).join('');
html += `<div class="mb-3">${lbl}
<select class="form-select ${isReadOnly?'field-linked':''}"
name="${c.id}" ${req} ${isReadOnly?'disabled':''}>
<option value="">— seleccionar —</option>${opts}
</select>${linkedNote}
</div>`;
} else if (c.tipo === 'radio') {
const opts = (c.options||[]).map(o => `
<div class="form-check">
<input class="form-check-input" type="radio" name="${c.id}" value="${esc(o)}"
${o===val?'checked':''} ${isReadOnly?'disabled':''}>
<label class="form-check-label">${esc(o)}</label>
</div>`).join('');
html += `<div class="mb-3">${lbl}${opts}${linkedNote}</div>`;
} else if (c.tipo === 'checkbox') {
const vals = Array.isArray(val) ? val : [];
const opts = (c.options||[]).map(o => `
<div class="form-check">
<input class="form-check-input" type="checkbox" name="${c.id}" value="${esc(o)}"
${vals.includes(o)?'checked':''} ${isReadOnly?'disabled':''}>
<label class="form-check-label">${esc(o)}</label>
</div>`).join('');
html += `<div class="mb-3">${lbl}${opts}${linkedNote}</div>`;
} else {
// texto, numero, fecha, hora, linked
const t = c.tipo === 'numero' ? 'number'
: (c.tipo === 'fecha' || c.linked_key === 'fecha_nacimiento') ? 'date'
: c.tipo === 'hora' ? 'time' : 'text';
html += `<div class="mb-3">${lbl}
<input type="${t}" class="form-control ${isReadOnly?'field-linked':''}"
name="${c.id}" value="${esc(val)}" ${req} ${isReadOnly?'readonly':''}
placeholder="${esc(c.placeholder||'')}">
${linkedNote}
</div>`;
}
});
cont.innerHTML = html;
}
// ══════════════════════════════════════════════════════════════════════
// formCliente
// ══════════════════════════════════════════════════════════════════════
const formCliente = {
async cargar() {
if (!TOKEN) {
$('err-title').textContent = 'Token inválido';
$('err-msg').textContent = 'El enlace no es válido o ha expirado.';
hide('loading-screen'); show('error-screen'); return;
}
try {
const r = await fetch(`${API}?t=${encodeURIComponent(TOKEN)}`);
const d = await r.json();
if (!d.success) {
$('err-title').textContent = d.error || 'No disponible';
$('err-msg').textContent = d.detalle || 'El formulario no está disponible.';
hide('loading-screen'); show('error-screen'); return;
}
_data = d;
_cfg = d.config || {};
// Si ya fue firmado/completado: mostrar pantalla bloqueada
if (['firmado','completado'].includes(d.envio.estado)) {
this._mostrarYaFirmado(d);
return;
}
this._renderForm(d);
} catch(e) {
$('err-title').textContent = 'Error de conexión';
$('err-msg').textContent = 'No pudimos cargar el formulario. Intenta de nuevo.';
hide('loading-screen'); show('error-screen');
}
},
_aplicarDiseño(prefixId) {
const cfg = _cfg;
// Color del header
if (cfg.doc_color) {
document.querySelectorAll('.form-header').forEach(h => h.style.background = cfg.doc_color);
const canvas = document.getElementById('firma-canvas');
if (canvas) canvas.style.borderColor = cfg.doc_color;
}
// Nombr empresa, logo
const hdr = $(prefixId + '-empresa-header');
if (hdr && cfg.doc_encabezado) {
$(prefixId + '-empresa-nombre').textContent = cfg.doc_encabezado;
$(prefixId + '-empresa-subtitulo').textContent = cfg.doc_subtitulo || '';
if (cfg.doc_logo_base64) {
const img = $(prefixId + '-empresa-logo');
img.src = cfg.doc_logo_base64;
img.style.display = '';
}
hdr.style.display = '';
}
},
_mostrarYaFirmado(d) {
this._aplicarDiseño('yf');
const env = d.envio;
const form = d.formulario;
$('yf-titulo').textContent = form.nombre || 'Formulario';
const pac = (d.prefilled || {})['__paciente'];
if (pac?.nombre_completo) {
$('yf-paciente').textContent = pac.nombre_completo +
(pac.numero_documento ? ' · ' + pac.numero_documento : '');
}
const badge = $('yf-estado-badge');
if (env.estado === 'firmado') {
badge.textContent = '✍️ Firmado digitalmente';
badge.style.background = '#198754';
badge.style.color = '#fff';
} else {
badge.textContent = '✅ Completado';
badge.style.background = '#0d6efd';
badge.style.color = '#fff';
}
if (env.firma_svg) {
$('yf-firma-img').src = env.firma_svg;
show('yf-firma-thumb');
}
// Link al PDF (requiere sesión admin, se muestra de todas formas)
$('yf-pdf-btn').href = 'ver_formulario_enviado.php?id=' + env.id;
if (env.hash_verificacion) {
$('yf-hash').textContent = env.hash_verificacion;
show('yf-hash-box');
}
hide('loading-screen');
show('ya-firmado-screen');
},
_renderForm(d) {
const form = d.formulario;
const env = d.envio;
const prefilled = d.prefilled || {};
// Diseño doc (color, logo, empresa)
this._aplicarDiseño('f');
// Header
$('f-titulo').textContent = form.nombre;
$('f-desc').textContent = form.descripcion || '';
// Info paciente
const pac = prefilled['__paciente'];
if (pac?.nombre_completo) {
$('f-paciente-info').innerHTML =
`<i class="fas fa-user me-1"></i>${esc(pac.nombre_completo)}` +
(pac.numero_documento ? ` &nbsp;·&nbsp; ${esc(pac.numero_documento)}` : '');
} else {
hide('f-paciente-info');
}
// Si ya está completado/firmado
if (['completado','firmado'].includes(env.estado)) {
$('instruccion-nota').innerHTML =
`<i class="fas fa-check-circle text-success me-1"></i>
Este formulario ya fue <strong>completado</strong>. Puedes ver tus respuestas abajo.`;
$('btn-enviar').disabled = true;
$('btn-enviar').textContent = 'Ya enviado';
// Mostrar firma almacenada si existe
if (env.firma_svg) {
show('firma-container');
const canvas = $('firma-canvas');
if (canvas) canvas.style.display = 'none';
// Insertar imagen de la firma almacenada
const existingImg = document.getElementById('_firma-guardada');
if (!existingImg) {
const img = document.createElement('img');
img.id = '_firma-guardada';
img.src = env.firma_svg;
img.alt = 'Firma';
img.className = 'border rounded p-2 mt-2';
img.style.cssText = 'max-height:140px;max-width:100%;display:block;margin:0 auto';
canvas.parentNode.insertBefore(img, canvas.nextSibling);
}
}
}
// Campos
renderCampos(form.esquema_decoded, prefilled);
// Firma (se inicializa DESPUÉS de mostrar el form para que offsetWidth sea correcto)
if (form.permite_firma) {
show('firma-container');
if (form.requiere_firma) show('firma-req-badge');
// Configurar modos según el campo firma del esquema
const firmaCampo = form.esquema_decoded.find(c => c.tipo === 'firma');
firma.setModos(firmaCampo?.modos);
}
hide('loading-screen');
show('form-screen');
if (form.permite_firma) {
// Pequeño delay para asegurar que el layout ya renderizó
requestAnimationFrame(() => requestAnimationFrame(() => firma.init()));
}
},
_recopilar() {
const datos = {};
const form = document.getElementById('form-main');
const campos = _data.formulario.esquema_decoded;
campos.forEach(c => {
if (c.tipo === 'separador' || c.tipo === 'firma' || c.tipo === 'parrafo') return;
if (c.tipo === 'linked') { return; } // linked viene del prefilled
// parrafo_inline: recolectar solo los inputs editables (los que el usuario llenó)
if (c.tipo === 'parrafo_inline') {
form.querySelectorAll(`input[data-editable][name^="${c.id}_"]`).forEach(inp => {
const key = inp.name.slice(c.id.length + 1);
if (inp.value.trim()) datos[key] = inp.value.trim();
});
return;
}
if (c.tipo === 'checkbox' || c.tipo === 'lista_marcable') {
const checks = [...form.querySelectorAll(`input[name="${c.id}"]:checked`)];
datos[c.id] = checks.map(el => el.value);
} else if (c.tipo === 'radio') {
const r = form.querySelector(`input[name="${c.id}"]:checked`);
datos[c.id] = r ? r.value : '';
} else {
const el = form.querySelector(`[name="${c.id}"]`);
datos[c.id] = el ? el.value : '';
}
});
return datos;
},
_validar(datos) {
const campos = _data.formulario.esquema_decoded;
for (const c of campos) {
if (!c.required) continue;
if (c.tipo === 'separador' || c.tipo === 'linked' || c.tipo === 'firma' || c.tipo === 'parrafo' || c.tipo === 'parrafo_inline') continue;
const v = datos[c.id];
const vacio = Array.isArray(v) ? v.length === 0 : !v || !String(v).trim();
if (vacio) {
const el = document.querySelector(`[name="${c.id}"]`);
if (el) { el.focus(); el.scrollIntoView({block:'center', behavior:'smooth'}); }
return `El campo "${c.label}" es obligatorio.`;
}
}
if (_data.formulario.requiere_firma && !firma.obtenerSVG() && !firma.obtenerFoto()) {
$('firma-container').scrollIntoView({block:'center', behavior:'smooth'});
return 'La firma es obligatoria en este formulario.';
}
return null;
},
async enviar() {
const datos = this._recopilar();
const err = this._validar(datos);
if (err) {
alert(err); return;
}
const payload = {
token: TOKEN,
datos_cliente: datos,
firma_svg: firma.obtenerSVG() || null,
firma_foto: firma.obtenerFoto() || null,
};
const btn = $('btn-enviar');
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Enviando…';
try {
const r = await fetch(API, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
const d = await r.json();
if (d.success) {
this._mostrarExito(d, payload.firma_svg);
} else {
alert(d.error || 'Error al enviar. Intenta de nuevo.');
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-check-circle me-2"></i>Enviar formulario';
}
} catch(e) {
alert('Error de conexión. Revisa tu internet e intenta de nuevo.');
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-check-circle me-2"></i>Enviar formulario';
}
},
_mostrarExito(d, firmaSvg) {
hide('form-screen');
const msg = d.firmado
? '¡Tu formulario fue firmado y enviado exitosamente!'
: 'Tus respuestas fueron registradas correctamente.';
$('success-msg').textContent = msg;
if (firmaSvg) {
$('success-firma-img').src = firmaSvg;
$('success-firma-thumb').style.display = '';
}
// Hash de verificación
if (d.hash) {
$('success-hash').textContent = d.hash;
show('success-hash-box');
}
// Link al PDF
if (d.envio_id) {
const pdfBtn = $('success-pdf-btn');
pdfBtn.href = 'ver_formulario_enviado.php?id=' + d.envio_id;
show('success-pdf-row');
}
show('success-screen');
window.scrollTo({top: 0, behavior: 'smooth'});
},
};
// ══════════════════════════════════════════════════════════════════════
// INIT
// ══════════════════════════════════════════════════════════════════════
document.addEventListener('DOMContentLoaded', () => formCliente.cargar());
</script>
</body>
</html>