Files
whatsapp/ver_formulario_enviado.php
T
Lizandro GuarnizoandClaude Sonnet 4.6 3dbf40e4fa feat(formulario): firma profesional directa en modal embebido (formularios solo-pro)
- ver_formulario_enviado.php: pre-scan $_soloFirmaPro; muestra canvas pro cuando modoTurnero+embebido+sesión activa+sin firma paciente
- fpw-save JS: recopila campos del form y envía solo_profesional=true; al guardar muestra éxito y postMessage turneroFirmado
- firmar_profesional_consentimiento.php: si solo_profesional=true guarda datos_respuestas y marca estado=firmado

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-03 12:49:39 -05:00

1088 lines
56 KiB
PHP

<?php
/**
* ver_formulario_enviado.php — Ver respuesta de un formulario (imprimible / PDF)
* Acceso admin/enfermero: ver_formulario_enviado.php?id=ENVIO_ID (requiere sesión)
* Acceso público cliente: ver_formulario_enviado.php?t=TOKEN (sin sesión, solo firmado/completado)
* Acceso turnero (consent): ver_formulario_enviado.php?token=UUID (firma de consentimiento turnero)
*/
require_once 'config/config.php';
// ── TURNERO: Consentimiento por token UUID (detección previa al flujo normal) ──
$tokenTurnero = trim($_GET['token'] ?? '');
$modoTurnero = ($tokenTurnero !== '');
if ($modoTurnero && !preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i', $tokenTurnero)) {
http_response_code(400); die('Token de consentimiento inválido');
}
$tokenPublico = trim($_GET['t'] ?? '');
$modoPublico = ($modoTurnero || $tokenPublico !== '');
if (!$modoPublico && !isUserLoggedIn()) {
header('Location: login.php'); exit;
}
// ── Cargar datos ──────────────────────────────────────────────────────
$db = Database::getInstance();
if ($modoTurnero) {
// ── Turnero: cargar desde turnero_consentimientos ──────────────────
$tcRow = $db->fetch(
"SELECT tc.id, tc.turno_id, tc.formulario_id, tc.token, tc.estado,
tc.enviado_at, tc.firmado_at, tc.ip_firma, tc.ua_firma, tc.firma_svg,
tc.firma_profesional_svg, tc.firmado_profesional_at, tc.datos_respuestas,
f.nombre AS form_nombre, f.categoria, f.descripcion AS form_descripcion,
f.esquema, f.doc_encabezado, f.doc_subtitulo, f.doc_logo_base64, f.doc_color, f.doc_pie_pagina,
p.nombre_completo AS paciente_nombre,
p.numero_documento, p.tipo_documento,
p.fecha_nacimiento, p.telefono AS paciente_telefono, p.eps,
t.sesion_id
FROM turnero_consentimientos tc
JOIN lab_formularios f ON f.id = tc.formulario_id
JOIN turnero_turnos t ON t.id = tc.turno_id
LEFT JOIN turnero_solicitudes ts ON ts.turno_id = tc.turno_id
LEFT JOIN lab_pacientes p ON p.id = COALESCE(ts.paciente_id, t.paciente_id)
WHERE tc.token = ?",
[$tokenTurnero]
);
if (!$tcRow) { http_response_code(404); die('Consentimiento no encontrado'); }
// ── POST: guardar firma y marcar como firmado ──────────────────────
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
header('Content-Type: application/json; charset=utf-8');
if ($tcRow['estado'] === 'firmado') {
echo json_encode(['ok' => false, 'error' => 'Este consentimiento ya fue firmado']); exit;
}
$input = json_decode(file_get_contents('php://input'), true) ?? [];
$firmaSvg = trim($input['firma_svg'] ?? '');
if (strlen($firmaSvg) < 100) {
echo json_encode(['ok' => false, 'error' => 'Firma requerida']); exit;
}
$datosRespuestas = isset($input['datos_respuestas']) && is_array($input['datos_respuestas'])
? json_encode($input['datos_respuestas'], JSON_UNESCAPED_UNICODE)
: null;
$ip = filter_var(
$_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'] ?? '',
FILTER_VALIDATE_IP
) ?: ($_SERVER['REMOTE_ADDR'] ?? '');
$ua = substr($_SERVER['HTTP_USER_AGENT'] ?? '', 0, 500);
$pdo = $db->getConnection();
$stmt = $pdo->prepare(
"UPDATE turnero_consentimientos
SET estado='firmado', firmado_at=NOW(), firma_svg=?,
ip_firma=?, ua_firma=?, version_formulario=?, datos_respuestas=?
WHERE token=?"
);
$stmt->execute([$firmaSvg, $ip, $ua, (int)$tcRow['formulario_id'], $datosRespuestas, $tokenTurnero]);
require_once __DIR__ . '/modules/turnero/api/_helpers.php';
notificarSSE((int)$tcRow['sesion_id']);
echo json_encode(['ok' => true, 'message' => 'Consentimiento firmado correctamente']); exit;
}
// ── GET: construir $envio compatible con la plantilla ─────────────
$envio = [
'id' => (int)$tcRow['id'],
'estado' => $tcRow['estado'],
'form_nombre' => $tcRow['form_nombre'],
'form_descripcion' => $tcRow['form_descripcion'],
'categoria' => $tcRow['categoria'],
'esquema' => $tcRow['esquema'],
'firma_svg' => $tcRow['firma_svg'],
'doc_color' => $tcRow['doc_color'],
'doc_encabezado' => $tcRow['doc_encabezado'],
'doc_subtitulo' => $tcRow['doc_subtitulo'],
'doc_logo_base64' => $tcRow['doc_logo_base64'],
'doc_pie_pagina' => $tcRow['doc_pie_pagina'],
'paciente_nombre' => $tcRow['paciente_nombre'],
'numero_documento' => $tcRow['numero_documento'],
'tipo_documento' => $tcRow['tipo_documento'],
'fecha_nacimiento' => $tcRow['fecha_nacimiento'],
'paciente_telefono' => $tcRow['paciente_telefono'],
'eps' => $tcRow['eps'],
'enviado_por_nombre'=> $_SESSION['admin_user']['full_name'] ?? $_SESSION['admin_user']['username'] ?? 'Turnero',
'enviado_por_email' => $_SESSION['admin_user']['email'] ?? null,
'firma_profesional_svg' => $tcRow['firma_profesional_svg'] ?? null,
'datos_cliente' => $tcRow['datos_respuestas'] ?: '{}',
'datos_prefilled' => json_encode(['__paciente' => [
'nombre_completo' => $tcRow['paciente_nombre'] ?? '',
'numero_documento' => $tcRow['numero_documento'] ?? '',
'tipo_documento' => $tcRow['tipo_documento'] ?? '',
'telefono' => $tcRow['paciente_telefono'] ?? '',
'eps' => $tcRow['eps'] ?? '',
]]),
'created_at' => $tcRow['enviado_at'] ?? date('Y-m-d H:i:s'),
'completado_en' => $tcRow['firmado_at'],
'ip_cliente' => $tcRow['ip_firma'],
'hash_verificacion' => null,
'permite_firma' => 1,
'requiere_firma' => 1,
];
} elseif ($modoPublico) {
// Acceso por token: solo si el formulario ya fue firmado/completado
if (!preg_match('/^[a-f0-9]{64}$/i', $tokenPublico)) {
http_response_code(400); die('Token inválido');
}
$envio = $db->fetch(
"SELECT e.*, f.nombre AS form_nombre, f.categoria, f.descripcion AS form_descripcion,
f.esquema, f.permite_firma, f.requiere_firma,
f.doc_encabezado, f.doc_subtitulo, f.doc_logo_base64, f.doc_color, f.doc_pie_pagina,
p.nombre_completo AS paciente_nombre, p.numero_documento, p.tipo_documento,
p.fecha_nacimiento, p.telefono AS paciente_telefono, p.eps,
u.full_name AS enviado_por_nombre, u.email AS enviado_por_email
FROM lab_form_envios e
JOIN lab_formularios f ON f.id = e.formulario_id
LEFT JOIN lab_pacientes p ON p.id = e.paciente_id
LEFT JOIN admin_users u ON u.id = e.enviado_por
WHERE e.token = ?",
[$tokenPublico]
);
if (!$envio) { http_response_code(404); die('Formulario no encontrado'); }
if (!in_array($envio['estado'], ['firmado', 'completado'])) {
http_response_code(403); die('El formulario aún no ha sido completado.');
}
} else {
$id = (int)($_GET['id'] ?? 0);
if (!$id) { http_response_code(400); die('ID requerido'); }
$envio = $db->fetch(
"SELECT e.*, f.nombre AS form_nombre, f.categoria, f.descripcion AS form_descripcion,
f.esquema, f.permite_firma, f.requiere_firma,
f.doc_encabezado, f.doc_subtitulo, f.doc_logo_base64, f.doc_color, f.doc_pie_pagina,
p.nombre_completo AS paciente_nombre, p.numero_documento, p.tipo_documento,
p.fecha_nacimiento, p.telefono AS paciente_telefono, p.eps,
u.full_name AS enviado_por_nombre, u.email AS enviado_por_email
FROM lab_form_envios e
JOIN lab_formularios f ON f.id = e.formulario_id
LEFT JOIN lab_pacientes p ON p.id = e.paciente_id
LEFT JOIN admin_users u ON u.id = e.enviado_por
WHERE e.id = ?",
[$id]
);
if (!$envio) { http_response_code(404); die('Formulario no encontrado'); }
// Enfermero solo puede ver sus propios envíos
if (isEnfermero()) {
$uid = (int)($_SESSION['admin_user']['id'] ?? 0);
if ($envio['enviado_por'] != $uid) { http_response_code(403); die('Sin acceso'); }
}
}
// ── Config global del lab ─────────────────────────────────────────────
$cfgRows = $db->fetchAll('SELECT clave, valor FROM lab_config WHERE valor != ""');
$cfg = [];
foreach ($cfgRows as $r) { $cfg[$r['clave']] = $r['valor']; }
// Merge: per-form override > global config
$docColor = $envio['doc_color'] ?: ($cfg['doc_color'] ?? '#1565c0');
$docEncabezado = $envio['doc_encabezado'] ?: ($cfg['empresa_nombre'] ?? '');
$docSubtitulo = $envio['doc_subtitulo'] ?: ($cfg['empresa_subtitulo'] ?? '');
$docLogo = $envio['doc_logo_base64'] ?: ($cfg['doc_logo_base64'] ?? '');
$docPiePagina = $envio['doc_pie_pagina'] ?: ($cfg['doc_pie_pagina'] ?? '');
$docDireccion = $cfg['empresa_direccion'] ?? '';
$docTelefono = $cfg['empresa_telefono'] ?? '';
$docEmail = $cfg['empresa_email'] ?? '';
$docCiudad = $cfg['empresa_ciudad'] ?? '';
$esquema = json_decode($envio['esquema'], true) ?? [];
$datosCliente = json_decode($envio['datos_cliente'] ?? '{}', true) ?? [];
$datosPrefilled = json_decode($envio['datos_prefilled'] ?? '{}', true) ?? [];
$todos = array_merge($datosPrefilled, $datosCliente);
$modoEditar = $modoTurnero && $envio['estado'] !== 'firmado';
$embebido = isset($_GET['embed']) && $_GET['embed'] === '1';
// Pre-scan: formulario que solo requiere firma del profesional (sin firma paciente)
$_soloFirmaPro = !empty(array_filter($esquema, fn($c) => ($c['tipo'] ?? '') === 'firma_profesional'))
&& empty(array_filter($esquema, fn($c) => ($c['tipo'] ?? '') === 'firma'));
// Mapa id → label
$labelMap = [];
foreach ($esquema as $c) {
if (!empty($c['id'])) $labelMap[$c['id']] = $c['label'] ?? $c['id'];
}
$completado = ($envio['completado_en'] ?? '') ? date('d/m/Y H:i', strtotime($envio['completado_en'])) : '—';
$creado = ($envio['created_at'] ?? '') ? date('d/m/Y H:i', strtotime($envio['created_at'])) : '—';
function esc2(mixed $v): string {
if (is_array($v)) return htmlspecialchars(implode(', ', $v), ENT_QUOTES);
return htmlspecialchars((string)($v ?? ''), ENT_QUOTES);
}
?>
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= esc2($envio['form_nombre']) ?> — Respuesta</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>
body { font-size: 14px; background: #f4f7fc; }
/* ── Barra de acciones (se oculta al imprimir) ── */
.action-bar {
position: sticky; top: 0; z-index: 100;
background: <?= htmlspecialchars($docColor) ?>; color: #fff;
padding: 10px 20px; display: flex; align-items: center; gap: 12px;
}
.action-bar a { color: rgba(255,255,255,.8); text-decoration: none; font-size: 13px; }
.action-bar a:hover { color: #fff; }
/* ── Documento ──────────────────────────────────── */
.doc-wrap { max-width: 780px; margin: 24px auto; background: #fff;
border-radius: 10px; box-shadow: 0 2px 20px rgba(0,0,0,.12);
overflow: hidden; }
.doc-header { background: <?= htmlspecialchars($docColor) ?>;
color: #fff; padding: 24px 32px 20px; }
.doc-header-inner { display: flex; align-items: flex-start; gap: 16px; }
.doc-header-logo { flex-shrink: 0; }
.doc-header-logo img { max-height: 64px; max-width: 100px; border-radius: 6px;
background: rgba(255,255,255,.15); padding: 4px; }
.doc-header-text { flex: 1; }
.doc-header-text h4 { margin: 0 0 2px; font-size: 15px; font-weight: 700;
opacity: .9; letter-spacing: .01em; }
.doc-header-text .doc-subtitulo { font-size: 12px; opacity: .75; margin-bottom: 4px; }
.doc-header-text .doc-contacto { font-size: 11px; opacity: .7; }
.doc-header-text h3 { margin: 8px 0 0; font-size: 18px; font-weight: 700;
border-top: 1px solid rgba(255,255,255,.3); padding-top: 8px; }
.doc-header-badge { flex-shrink: 0; }
.doc-body { padding: 28px 32px; }
/* ── Sección ────────────────────────────────────── */
.section-title { font-size: 11px; font-weight: 700; text-transform: uppercase;
letter-spacing: .05em; color: #6c757d; border-bottom: 1px solid #dee2e6;
padding-bottom: 6px; margin: 24px 0 14px; }
.section-title:first-child { margin-top: 0; }
/* ── Fila campo ─────────────────────────────────── */
.campo-row { display: flex; gap: 16px; padding: 6px 0;
border-bottom: 1px solid #f0f0f0; }
.campo-row:last-child { border-bottom: none; }
.campo-label { flex: 0 0 38%; font-size: 12px; color: #6c757d; padding-top: 1px; }
.campo-valor { flex: 1; font-size: 13px; font-weight: 600; color: #212529;
word-break: break-word; }
/* ── Separador del esquema ──────────────────────── */
.esquema-sep { font-size: 11px; font-weight: 700; text-transform: uppercase;
letter-spacing: .05em; color: <?= htmlspecialchars($docColor) ?>; margin: 20px 0 8px;
border-bottom: 2px solid <?= htmlspecialchars($docColor) ?>; padding-bottom: 4px; }
/* ── Estado badge ───────────────────────────────── */
.estado-badge { display: inline-block; padding: 3px 10px; border-radius: 20px;
font-size: 11px; font-weight: 700; text-transform: uppercase; }
.estado-firmado { background: #d1fae5; color: #065f46; }
.estado-completado { background: #dbeafe; color: #1e40af; }
.estado-pendiente { background: #fef3c7; color: #92400e; }
/* ── Firma ──────────────────────────────────────── */
.firma-box { background: #f8faff; border: 1px solid #c9d8ff;
border-radius: 8px; padding: 14px; display: inline-block; margin-top: 8px; }
.firma-box img { max-height: 140px; max-width: 340px; display: block; }
/* ── Footer del doc ─────────────────────────────── */
.doc-footer { background: #f8faff; border-top: 1px solid #e9ecef;
padding: 14px 32px; font-size: 11px; color: #6c757d;
display: flex; justify-content: space-between; flex-wrap: wrap; gap: 6px; }
.hash-short { font-family: monospace; opacity: .7; }
/* ── Sello SHA-256 ──────────────────────────────── */
.hash-seal { border: 1px solid #c3d3f7; border-radius: 8px; overflow: hidden; }
.hash-seal-header { background: <?= htmlspecialchars($docColor) ?>; color: #fff; padding: 8px 14px;
font-size: 11px; font-weight: 700; text-transform: uppercase;
letter-spacing: .05em; }
.hash-seal-body { background: #f0f5ff; padding: 12px 14px; }
.hash-label { font-size: 11px; color: #6c757d; margin-bottom: 4px; }
.hash-value { display: block; font-family: monospace; font-size: 11px;
color: #1e3a6e; word-break: break-all; background: #fff;
border: 1px solid #d0dcf7; border-radius: 4px; padding: 6px 8px; }
.hash-hint { font-size: 11px; color: #6c757d; }
.hash-hint a { color: #1565c0; word-break: break-all; }
@media print {
.hash-seal { border-color: #000; }
.hash-seal-header { background: #000 !important; -webkit-print-color-adjust:exact; print-color-adjust:exact; }
}
<?php if ($embebido): ?>
/* ── Modo embebido (iframe) ── */
body { background: #fff; font-size: 12px; }
.action-bar { display: none !important; }
.doc-wrap { margin: 0; border-radius: 0; box-shadow: none; max-width: 100%; }
.doc-header { padding: 10px 14px 8px; }
.doc-header-text h3 { font-size: 14px; }
.doc-header-text h4 { font-size: 12px; }
.doc-body { padding: 12px 14px; }
.doc-footer { display: none; }
.section-title { font-size: 10px; margin: 14px 0 8px; }
.campo-label { font-size: 11px; }
.campo-valor { font-size: 12px; }
.campo-edit { margin-bottom: 8px; }
.campo-edit label { font-size: 11px; margin-bottom: 2px; }
.campo-edit .form-control,
.campo-edit .form-select { font-size: 12px; padding: 3px 7px; }
.campo-edit .form-check-label { font-size: 12px; }
.campo-row { padding: 3px 0; }
.turnero-cv { height: 110px !important; }
.btn { font-size: 12px; padding: 3px 10px; }
.hash-seal { display: none; }
.alert { font-size: 12px; padding: 6px 10px; }
<?php endif; ?>
/* ── Canvas firma profesional ───────────────────── */
.firma-pro-widget { max-width: 520px; margin-top: 8px; }
.fpw-canvas { border: 2px solid #198754; border-radius: 8px; background: #f8fff9;
cursor: crosshair; display: block; max-width: 100%; touch-action: none; }
/* ── Campos interactivos (modo turnero editar) ──── */
.campo-edit { margin-bottom: 14px; }
.campo-edit label { display: block; font-size: 12px; color: #6c757d;
font-weight: 600; margin-bottom: 4px; }
.campo-edit .form-control,
.campo-edit .form-select { font-size: 13px; border-color: #c3d3f7;
background: #f8faff; }
.campo-edit .form-control:focus,
.campo-edit .form-select:focus { border-color: #1565c0;
box-shadow: 0 0 0 3px rgba(21,101,192,.12); }
.campo-edit .form-check-label { font-size: 13px; }
.campo-linked { display: flex; gap: 16px; padding: 6px 0;
border-bottom: 1px solid #f0f0f0; }
.campo-linked-label { flex: 0 0 38%; font-size: 12px; color: #6c757d; }
.campo-linked-valor { flex: 1; font-size: 13px; font-weight: 600;
color: #1565c0; }
/* ═══════ ESTILOS DE IMPRESIÓN ═══════════════════ */
@media print {
body { background: #fff !important; font-size: 12px; }
.action-bar { display: none !important; }
.doc-wrap { margin: 0; border-radius: 0; box-shadow: none; max-width: 100%; }
.doc-header { -webkit-print-color-adjust: exact; print-color-adjust: exact; padding: 18px 22px; }
.doc-body { padding: 18px 22px; }
.doc-footer { padding: 10px 22px; }
@page { margin: 1cm; }
}
</style>
</head>
<body>
<!-- ── Barra de acciones ──────────────────────────────────────────── -->
<div class="action-bar no-print">
<?php if ($modoTurnero): ?>
<span style="font-size:13px;opacity:.85">
<i class="fas fa-file-signature me-1"></i>
<?= $envio['estado'] === 'firmado' ? 'Consentimiento firmado ✓' : 'Lea el documento y firme al final' ?>
</span>
<div style="flex:1"></div>
<?php if ($envio['estado'] === 'firmado'): ?>
<button onclick="window.print()" class="btn btn-warning btn-sm fw-semibold">
<i class="fas fa-file-pdf me-2"></i>Imprimir
</button>
<?php endif; ?>
<?php elseif (!$modoPublico): ?>
<a href="lab_formularios.php#envios"><i class="fas fa-arrow-left me-1"></i>Volver</a>
<div style="flex:1"></div>
<button onclick="window.print()" class="btn btn-warning btn-sm fw-semibold">
<i class="fas fa-file-pdf me-2"></i>Descargar / Imprimir PDF
</button>
<?php else: ?>
<span style="font-size:13px;opacity:.85"><i class="fas fa-file-alt me-1"></i>Documento firmado</span>
<div style="flex:1"></div>
<button onclick="window.print()" class="btn btn-warning btn-sm fw-semibold">
<i class="fas fa-file-pdf me-2"></i>Descargar / Imprimir PDF
</button>
<?php endif; ?>
</div>
<!-- ── Documento ─────────────────────────────────────────────────── -->
<div class="doc-wrap">
<!-- Header -->
<div class="doc-header">
<div class="doc-header-inner">
<?php if ($docLogo): ?>
<div class="doc-header-logo">
<img src="<?= htmlspecialchars($docLogo) ?>" alt="Logo">
</div>
<?php endif; ?>
<div class="doc-header-text">
<?php if ($docEncabezado): ?>
<h4><?= esc2($docEncabezado) ?></h4>
<?php endif; ?>
<?php if ($docSubtitulo): ?>
<div class="doc-subtitulo"><?= esc2($docSubtitulo) ?></div>
<?php endif; ?>
<?php
$contacto = array_filter([$docDireccion, $docCiudad, $docTelefono, $docEmail]);
if ($contacto): ?>
<div class="doc-contacto"><?= esc2(implode(' · ', $contacto)) ?></div>
<?php endif; ?>
<h3><?= esc2($envio['form_nombre']) ?></h3>
<?php if ($envio['form_descripcion']): ?>
<div style="font-size:12px;opacity:.8;margin-top:4px"><?= esc2($envio['form_descripcion']) ?></div>
<?php endif; ?>
</div>
<div class="doc-header-badge" style="text-align:right">
<?php
$eBadge = match($envio['estado']) {
'firmado' => 'estado-firmado',
'completado' => 'estado-completado',
default => 'estado-pendiente',
};
?>
<span class="estado-badge <?= $eBadge ?>"><?= esc2($envio['estado']) ?></span>
<div style="font-size:10px;opacity:.7;margin-top:4px"><?= esc2($envio['categoria'] ?? '') ?></div>
</div>
</div>
</div>
<div class="doc-body">
<!-- Info del paciente -->
<?php if ($envio['paciente_nombre']): ?>
<div class="section-title"><i class="fas fa-user me-1"></i>Datos del paciente</div>
<div class="campo-row">
<div class="campo-label">Nombre completo</div>
<div class="campo-valor"><?= esc2($envio['paciente_nombre']) ?></div>
</div>
<?php if ($envio['numero_documento']): ?>
<div class="campo-row">
<div class="campo-label"><?= esc2($envio['tipo_documento'] ?? 'Documento') ?></div>
<div class="campo-valor"><?= esc2($envio['numero_documento']) ?></div>
</div>
<?php endif; ?>
<?php if ($envio['fecha_nacimiento']): ?>
<div class="campo-row">
<div class="campo-label">Fecha de nacimiento</div>
<div class="campo-valor"><?= esc2(date('d/m/Y', strtotime($envio['fecha_nacimiento']))) ?></div>
</div>
<?php endif; ?>
<?php if ($envio['paciente_telefono']): ?>
<div class="campo-row">
<div class="campo-label">Teléfono</div>
<div class="campo-valor"><?= esc2($envio['paciente_telefono']) ?></div>
</div>
<?php endif; ?>
<?php if ($envio['eps']): ?>
<div class="campo-row">
<div class="campo-label">EPS / Aseguradora</div>
<div class="campo-valor"><?= esc2($envio['eps']) ?></div>
</div>
<?php endif; ?>
<?php endif; ?>
<!-- Respuestas del formulario (en orden del esquema) -->
<div class="section-title">
<i class="fas fa-wpforms me-1"></i>
<?= $modoEditar ? 'Formulario de consentimiento' : 'Respuestas del formulario' ?>
</div>
<?php
$paciente = $datosPrefilled['__paciente'] ?? [];
// Firma global del profesional (un solo valor para todos los campos firma_profesional)
$firmaSharedProfesional = $envio['firma_profesional_svg'] ?? null;
// Firma global del paciente: fallback para cuando no hay datos_cliente por campo
$firmaSharedPaciente = $envio['firma_svg'] ?? null;
// Rastrear si ya se renderizó la firma inline (para no duplicarla al pie)
$_renderedFirmaProfesional = false;
$_renderedFirmaPaciente = false;
$_esquemaTieneFirmaPaciente = false;
$_esquemaTieneFirmaAlguna = false; // true si hay cualquier campo firma/firma_profesional
$_firmaGlobalPacienteUsada = false; // la firma global solo va al primer campo firma
foreach ($esquema as $campo):
$tipo = $campo['tipo'] ?? '';
if ($tipo === 'separador'): ?>
<div class="esquema-sep"><?= esc2($campo['label'] ?? '') ?></div>
<?php continue; endif;
// ── Firmas inline: cada campo muestra su propia firma ──
if ($tipo === 'firma' || $tipo === 'firma_profesional'):
$cid = $campo['id'] ?? null;
if (!$cid) continue;
$isPro = ($tipo === 'firma_profesional');
// Marcar presencia de campos firma en el esquema
$_esquemaTieneFirmaAlguna = true;
if (!$isPro) {
$_esquemaTieneFirmaPaciente = true;
}
// La firma profesional global solo se muestra en el PRIMER campo firma_profesional.
// Campos posteriores (ej. desistimiento) solo muestran su propia firma por campo.
$fSvg = $isPro
? (!$_renderedFirmaProfesional ? ($firmaSharedProfesional ?? $datosCliente[$cid . '_svg'] ?? null) : ($datosCliente[$cid . '_svg'] ?? null))
: ($datosCliente[$cid . '_svg'] ?? (!$_firmaGlobalPacienteUsada ? $firmaSharedPaciente : null) ?? null);
$fFoto = $datosCliente[$cid . '_foto'] ?? null;
$fIcon = $isPro ? 'fa-user-md' : 'fa-signature';
$fColor = $isPro ? '#198754' : '#1565c0';
$fLabel = htmlspecialchars($campo['label'] ?? ($isPro ? 'Firma profesional' : 'Firma paciente'));
// Campo paciente sin firma:
// - En turnero pendiente: todos los campos firma muestran canvas.
// - En vista normal: omitir campos sin firma.
if (!$fSvg && !$fFoto && !$isPro) {
if (!($modoTurnero && $modoEditar)) {
continue; // no-turnero: omitir campos sin firma
}
}
$yaHayCanvasPro = $isPro && $_renderedFirmaProfesional;
if ($isPro) $_renderedFirmaProfesional = true;
if (!$isPro && $fSvg) {
$_renderedFirmaPaciente = true;
$_firmaGlobalPacienteUsada = true;
}
?>
<div class="section-title mt-3" style="color:<?= $fColor ?>">
<i class="fas <?= $fIcon ?> me-1"></i><?= $fLabel ?>
</div>
<?php if ($fSvg): ?>
<div class="firma-box" style="border-color:<?= $fColor ?>">
<img src="<?= htmlspecialchars($fSvg) ?>" alt="<?= $fLabel ?>">
</div>
<?php elseif ($fFoto): ?>
<div class="firma-box mt-2" style="border-color:<?= $fColor ?>">
<img src="<?= htmlspecialchars($fFoto) ?>" alt="Foto - <?= $fLabel ?>">
</div>
<?php elseif (!$isPro && $modoTurnero && $modoEditar): ?>
<div class="turnero-firma-item no-print" data-cid="<?= esc2($cid) ?>">
<p class="text-muted small mb-2"><i class="fas fa-pen me-1"></i>He leído el documento. Dibuje su firma:</p>
<canvas class="turnero-cv" width="500" height="160"
style="border:2px solid #1565c0;border-radius:8px;background:#f0f4ff;
cursor:crosshair;display:block;max-width:100%;touch-action:none"></canvas>
<div class="mt-2 d-flex gap-2 flex-wrap">
<button class="btn btn-outline-secondary btn-sm turnero-limpiar">
<i class="fas fa-eraser me-1"></i>Limpiar
</button>
<button class="btn btn-success fw-semibold turnero-firmar">
<i class="fas fa-check-circle me-1"></i>Confirmar: <?= $fLabel ?>
</button>
</div>
<div class="turnero-msg mt-2 small"></div>
</div>
<?php elseif ($isPro): ?>
<?php
// Mostrar canvas pro si: admin normal, O turnero+embebido+sesión activa+solo firma pro
$_mostrarCanvasPro = !$yaHayCanvasPro && (
(!$modoTurnero && !$modoPublico) ||
($modoTurnero && $embebido && isUserLoggedIn() && $_soloFirmaPro && $modoEditar)
);
if ($_mostrarCanvasPro): ?>
<!-- Canvas del profesional -->
<div class="firma-pro-widget no-print" id="fpw-<?= htmlspecialchars($cid) ?>"
data-envio="<?= (int)$envio['id'] ?>" data-campo="<?= htmlspecialchars($cid) ?>"
data-solo-pro="<?= ($modoTurnero && $_soloFirmaPro) ? '1' : '0' ?>"
data-turno="<?= isset($tcRow) ? (int)$tcRow['turno_id'] : '' ?>"
data-formulario="<?= isset($tcRow) ? (int)$tcRow['formulario_id'] : '' ?>">
<p class="text-muted small mb-2"><i class="fas fa-pen me-1"></i>Dibuje su firma en el recuadro:</p>
<canvas class="fpw-canvas" width="500" height="150"></canvas>
<div class="mt-2 d-flex gap-2">
<button class="btn btn-outline-secondary btn-sm fpw-clear"><i class="fas fa-eraser me-1"></i>Limpiar</button>
<button class="btn btn-success btn-sm fpw-save"><i class="fas fa-check me-1"></i>Guardar firma</button>
</div>
<div class="fpw-msg mt-2 small"></div>
</div>
<?php elseif ($modoPublico): ?>
<div class="alert py-2 px-3 no-print" style="background:#fff8e1;border:1.5px dashed #f59e0b;border-radius:8px;color:#92400e;font-size:.85rem">
<i class="fas fa-hourglass-half me-2"></i>
<strong>Pendiente de firma del profesional.</strong>
</div>
<?php endif; ?>
<?php endif; ?>
<?php continue; endif;
// ── Parrafo estático ──────────────────────────────────
if ($tipo === 'parrafo'):
$ws = !empty($campo['flujoLibre']) ? 'normal' : 'pre-wrap';
?>
<div class="mb-3" style="font-size:.88rem;line-height:1.75;color:#444;text-align:justify;white-space:<?= $ws ?>"><?= htmlspecialchars($campo['contenido'] ?? '', ENT_QUOTES) ?></div>
<?php continue; endif;
// ── Parrafo inline (texto con marcadores {key}) ───────
if ($tipo === 'parrafo_inline'):
$contenido = $campo['contenido'] ?? '';
// Sustituir {key} con valor del paciente o de $todos
$rendered = preg_replace_callback('/\{([a-z_]+)\}/', function($m) use ($paciente, $todos) {
return $paciente[$m[1]] ?? $todos[$m[1]] ?? $m[0];
}, $contenido);
// Convertir saltos de línea a <br>
$rendered = nl2br(htmlspecialchars($rendered, ENT_QUOTES));
?>
<div class="mb-3" style="font-size:.88rem;line-height:2;color:#444;text-align:justify"><?= $rendered ?></div>
<?php continue; endif;
$cid = $campo['id'] ?? null;
if (!$cid) continue;
// ── Modo editar (turnero o envío normal): campos interactivos ──
if ($modoEditar):
$prefill = $todos[$cid] ?? '';
$label = esc2($campo['label'] ?? $cid);
$req = !empty($campo['required']) ? ' required' : '';
if ($tipo === 'linked'):
$lk = $campo['linked_key'] ?? '';
$lval = $paciente[$lk] ?? $todos[$cid] ?? '';
if ($lval !== ''):
?>
<div class="campo-linked">
<div class="campo-linked-label"><?= $label ?></div>
<div class="campo-linked-valor"><?= esc2($lval) ?></div>
</div>
<?php endif; continue; endif; // linked
if ($tipo === 'textarea'):
?>
<div class="campo-edit">
<label><?= $label ?></label>
<textarea class="form-control form-control-sm" name="<?= esc2($cid) ?>"
rows="3"<?= $req ?>><?= esc2($prefill) ?></textarea>
</div>
<?php continue; endif;
if ($tipo === 'radio'):
$opts = $campo['opciones'] ?? $campo['options'] ?? [];
?>
<div class="campo-edit">
<label><?= $label ?></label>
<?php foreach ($opts as $opt): ?>
<div class="form-check">
<input class="form-check-input" type="radio"
name="<?= esc2($cid) ?>" value="<?= esc2($opt) ?>"
<?= ($prefill === $opt ? 'checked' : '') . $req ?>>
<label class="form-check-label"><?= esc2($opt) ?></label>
</div>
<?php endforeach; ?>
</div>
<?php continue; endif;
if ($tipo === 'checkbox' || $tipo === 'lista_marcable'):
$opts = $campo['opciones'] ?? $campo['options'] ?? $campo['items'] ?? [];
$checkedArr = is_array($prefill) ? $prefill
: (is_string($prefill) && $prefill !== '' ? (json_decode($prefill, true) ?: [$prefill]) : []);
?>
<div class="campo-edit">
<label><?= $label ?></label>
<?php foreach ($opts as $opt): ?>
<div class="form-check">
<input class="form-check-input" type="checkbox"
name="<?= esc2($cid) ?>[]" value="<?= esc2($opt) ?>"
<?= in_array($opt, $checkedArr, true) ? 'checked' : '' ?>>
<label class="form-check-label"><?= esc2($opt) ?></label>
</div>
<?php endforeach; ?>
</div>
<?php continue; endif;
if ($tipo === 'select'):
$opts = $campo['opciones'] ?? $campo['options'] ?? [];
?>
<div class="campo-edit">
<label><?= $label ?></label>
<select class="form-select form-select-sm" name="<?= esc2($cid) ?>"<?= $req ?>>
<option value="">— Seleccione —</option>
<?php foreach ($opts as $opt): ?>
<option value="<?= esc2($opt) ?>"<?= $prefill === $opt ? ' selected' : '' ?>><?= esc2($opt) ?></option>
<?php endforeach; ?>
</select>
</div>
<?php continue; endif;
// Calculados: pre-llenar si no tienen valor aún
if ($tipo === 'fecha_hoy' && $prefill === '') {
$prefill = date('Y-m-d');
}
if ($tipo === 'edad' && $prefill === '') {
$fnac = $paciente['fecha_nacimiento'] ?? '';
if ($fnac) {
$hoy = new DateTime();
$bday = new DateTime($fnac);
$prefill = (string)$hoy->diff($bday)->y;
}
}
// texto, numero, fecha, hora, y calculados → input
$inputType = match($tipo) {
'numero', 'edad' => 'number',
'fecha', 'fecha_hoy' => 'date',
'hora' => 'time',
default => 'text'
};
?>
<div class="campo-edit">
<label><?= $label ?></label>
<input type="<?= $inputType ?>" class="form-control form-control-sm"
name="<?= esc2($cid) ?>" value="<?= esc2($prefill) ?>"<?= $req ?>
<?= in_array($tipo, ['edad']) ? 'min="0" max="120"' : '' ?>>
</div>
<?php continue;
endif; // modoEditar
// ── Modo vista: mostrar valores existentes ─────────────
if ($tipo === 'linked') {
$lk = $campo['linked_key'] ?? '';
$valor = $paciente[$lk] ?? $todos[$cid] ?? null;
} elseif ($tipo === 'fecha_hoy') {
$valor = $todos[$cid] ?? date('Y-m-d');
} elseif ($tipo === 'edad') {
$valor = $todos[$cid] ?? '';
if ($valor === '') {
$fnac = $paciente['fecha_nacimiento'] ?? '';
if ($fnac) {
$valor = (string)(new DateTime())->diff(new DateTime($fnac))->y;
}
}
} else {
$valor = $todos[$cid] ?? null;
}
if ($valor === null || $valor === '') continue;
$display = is_array($valor) ? implode(', ', $valor) : (string)$valor;
?>
<div class="campo-row">
<div class="campo-label"><?= esc2($campo['label'] ?? $cid) ?></div>
<div class="campo-valor"><?= esc2($display) ?></div>
</div>
<?php endforeach; ?>
<!-- Firma digital global (solo si el esquema no tiene campo firma propio) -->
<?php if ($envio['firma_svg'] && !$_renderedFirmaPaciente): ?>
<div class="section-title mt-4"><i class="fas fa-signature me-1"></i>Firma digital (dibujo)</div>
<div class="firma-box">
<img src="<?= htmlspecialchars($envio['firma_svg']) ?>" alt="Firma digital">
</div>
<?php endif; ?>
<!-- Firma foto global -->
<?php if (!empty($datosCliente['__firma_foto'])): ?>
<div class="section-title mt-4"><i class="fas fa-camera me-1"></i>Foto de firma / documento</div>
<div class="firma-box">
<img src="<?= htmlspecialchars($datosCliente['__firma_foto']) ?>" alt="Foto de firma">
</div>
<?php endif; ?>
<!-- Info del envío -->
<div class="section-title mt-4"><i class="fas fa-clock me-1"></i>Información del envío</div>
<div class="campo-row">
<div class="campo-label">Enviado por</div>
<div class="campo-valor"><?= esc2($envio['enviado_por_nombre'] ?? '—') ?></div>
</div>
<div class="campo-row">
<div class="campo-label">Fecha de envío</div>
<div class="campo-valor"><?= esc2($creado) ?></div>
</div>
<?php if ($envio['completado_en']): ?>
<div class="campo-row">
<div class="campo-label">Completado el</div>
<div class="campo-valor"><?= esc2($completado) ?></div>
</div>
<?php endif; ?>
<?php if ($envio['ip_cliente']): ?>
<div class="campo-row">
<div class="campo-label">IP del cliente</div>
<div class="campo-valor text-muted small fw-normal"><?= esc2($envio['ip_cliente']) ?></div>
</div>
<?php endif; ?>
<?php if ($envio['hash_verificacion']): ?>
<!-- Sello de integridad SHA-256 -->
<div class="hash-seal mt-4">
<div class="hash-seal-header">
<i class="fas fa-shield-alt me-1"></i>
Sello de integridad del documento
</div>
<div class="hash-seal-body">
<div class="hash-label">Hash SHA-256 de verificación:</div>
<code class="hash-value"><?= esc2($envio['hash_verificacion']) ?></code>
<div class="hash-hint mt-2">
Verifique la autenticidad en:
<a href="verificar_formulario.php?h=<?= urlencode($envio['hash_verificacion']) ?>" target="_blank">
<?php
$protocol2 = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS']!=='off') ? 'https' : 'http';
echo $protocol2.'://'.$_SERVER['HTTP_HOST'].'/verificar_formulario.php?h='.urlencode($envio['hash_verificacion']);
?>
</a>
</div>
</div>
</div>
<?php endif; ?>
<!-- ── Widget firma turnero (fallback solo si el esquema no tiene NINGÚN campo firma) ── -->
<?php if ($modoTurnero && $envio['estado'] !== 'firmado' && !$_esquemaTieneFirmaAlguna): ?>
<div class="mt-4 no-print turnero-firma-item" data-cid="__firma_global">
<div class="section-title" style="color:#1565c0">
<i class="fas fa-pen me-1"></i>Firma del paciente / responsable
</div>
<p class="text-muted small mb-2">
He leído y comprendido el contenido de este documento.
Por favor dibuje su firma en el recuadro:
</p>
<canvas class="turnero-cv" width="500" height="160"
style="border:2px solid #1565c0;border-radius:8px;background:#f0f4ff;
cursor:crosshair;display:block;max-width:100%;touch-action:none">
</canvas>
<div class="mt-2 d-flex gap-2 flex-wrap">
<button class="btn btn-outline-secondary btn-sm turnero-limpiar">
<i class="fas fa-eraser me-1"></i>Limpiar
</button>
<button class="btn btn-success fw-semibold turnero-firmar">
<i class="fas fa-check-circle me-1"></i>Confirmar y firmar
</button>
</div>
<div class="turnero-msg mt-2 small"></div>
</div>
<?php elseif ($modoTurnero && $envio['estado'] === 'firmado'): ?>
<div class="alert alert-success mt-4 d-flex align-items-center gap-2 no-print">
<i class="fas fa-check-circle fs-4"></i>
<div>
<strong>Consentimiento firmado</strong><br>
Puede cerrar esta ventana. El personal de salud ha sido notificado.
</div>
</div>
<?php endif; ?>
</div><!-- /doc-body -->
<div class="doc-footer">
<span>
<?php if ($docPiePagina): ?>
<?= esc2($docPiePagina) ?> &nbsp;&bull;&nbsp;
<?php endif; ?>
<i class="fas fa-shield-alt me-1"></i>Generado el <?= date('d/m/Y H:i') ?> &nbsp;&bull;&nbsp; ID #<?= $envio['id'] ?>
</span>
<?php if ($envio['hash_verificacion']): ?>
<span class="hash-short" title="Hash SHA-256"><?= substr($envio['hash_verificacion'],0,16) ?>...</span>
<?php endif; ?>
</div>
</div><!-- /doc-wrap -->
<script>
/* ── Canvas firma del profesional ───────────────────────────────── */
(function () {
document.querySelectorAll('.firma-pro-widget').forEach(function (widget) {
const canvas = widget.querySelector('.fpw-canvas');
const ctx = canvas.getContext('2d');
const btnClear = widget.querySelector('.fpw-clear');
const btnSave = widget.querySelector('.fpw-save');
const msg = widget.querySelector('.fpw-msg');
const envioId = parseInt(widget.dataset.envio, 10);
const campoId = widget.dataset.campo;
let drawing = false;
// Ajustar resolución para HiDPI
(function scaleCanvas() {
const ratio = window.devicePixelRatio || 1;
const w = canvas.offsetWidth || canvas.width;
const h = canvas.offsetHeight || canvas.height;
canvas.width = w * ratio;
canvas.height = h * ratio;
canvas.style.width = w + 'px';
canvas.style.height = h + 'px';
ctx.scale(ratio, ratio);
ctx.strokeStyle = '#000';
ctx.lineWidth = 2;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
})();
function getPos(e) {
const r = canvas.getBoundingClientRect();
const src = e.touches ? e.touches[0] : e;
return { x: src.clientX - r.left, y: src.clientY - r.top };
}
canvas.addEventListener('mousedown', function(e){ drawing=true; ctx.beginPath(); const p=getPos(e); ctx.moveTo(p.x,p.y); });
canvas.addEventListener('mousemove', function(e){ if(!drawing) return; const p=getPos(e); ctx.lineTo(p.x,p.y); ctx.stroke(); });
canvas.addEventListener('mouseup', function(){ drawing=false; });
canvas.addEventListener('mouseleave', function(){ drawing=false; });
canvas.addEventListener('touchstart', function(e){ e.preventDefault(); drawing=true; ctx.beginPath(); const p=getPos(e); ctx.moveTo(p.x,p.y); }, {passive:false});
canvas.addEventListener('touchmove', function(e){ e.preventDefault(); if(!drawing) return; const p=getPos(e); ctx.lineTo(p.x,p.y); ctx.stroke(); }, {passive:false});
canvas.addEventListener('touchend', function(){ drawing=false; });
btnClear.addEventListener('click', function() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
msg.textContent = '';
});
btnSave.addEventListener('click', function() {
const svg = canvas.toDataURL('image/png');
if (svg.length < 1000) {
msg.innerHTML = '<span class="text-danger"><i class="fas fa-exclamation-triangle me-1"></i>Por favor dibuje su firma antes de guardar.</span>';
return;
}
btnSave.disabled = true;
btnSave.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Guardando...';
msg.textContent = '';
const turnoId = widget.dataset.turno;
const formularioId = widget.dataset.formulario;
const soloPro = widget.dataset.soloPro === '1';
const isTurnero = !!(turnoId && formularioId);
const saveUrl = isTurnero
? 'modules/turnero/api/firmar_profesional_consentimiento.php'
: 'api/lab/firmar_profesional.php';
// Recopilar campos del formulario cuando el pro es el firmante final
var datosRespuestas = {};
if (soloPro) {
document.querySelectorAll('[name]').forEach(function(el) {
var raw = el.name;
var isArr = raw.slice(-2) === '[]';
var name = isArr ? raw.slice(0, -2) : raw;
if (el.type === 'checkbox') {
if (el.checked) { if (!Array.isArray(datosRespuestas[name])) datosRespuestas[name] = []; datosRespuestas[name].push(el.value); }
} else if (el.type === 'radio') {
if (el.checked) datosRespuestas[name] = el.value;
} else if (el.value !== '') {
datosRespuestas[name] = el.value;
}
});
}
const savePayload = isTurnero
? { turno_id: parseInt(turnoId), formulario_id: parseInt(formularioId), svg: svg,
...(soloPro ? { solo_profesional: true, datos_respuestas: datosRespuestas } : {}) }
: { envio_id: envioId, campo_id: campoId, svg: svg };
fetch(saveUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(savePayload)
})
.then(function(r){ return r.json(); })
.then(function(data) {
if (data.ok) {
if (soloPro) {
// Modo solo-profesional: mostrar éxito y notificar al padre
document.querySelectorAll('.firma-pro-widget, .campo-edit, .section-title').forEach(function(el) {
el.style.display = 'none';
});
var ok = document.createElement('div');
ok.className = 'alert alert-success mt-4 d-flex align-items-center gap-2 no-print';
ok.innerHTML = '<i class="fas fa-check-circle fs-4"></i><div><strong>Firmado correctamente.</strong><br>Puede cerrar esta ventana.</div>';
widget.parentNode.insertBefore(ok, widget.nextSibling);
widget.style.display = 'none';
try { window.parent.postMessage({ type: 'turneroFirmado' }, '*'); } catch(e) {}
} else {
// Reemplazar canvas con imagen firmada
var img = document.createElement('img');
img.src = svg; img.alt = 'Firma profesional';
img.style.maxHeight = '140px'; img.style.maxWidth = '340px'; img.style.display = 'block';
var box = document.createElement('div');
box.className = 'firma-box'; box.style.borderColor = '#198754';
box.appendChild(img);
widget.replaceWith(box);
}
} else {
msg.innerHTML = '<span class="text-danger"><i class="fas fa-times me-1"></i>' + (data.error||'Error al guardar') + '</span>';
btnSave.disabled = false;
btnSave.innerHTML = '<i class="fas fa-check me-1"></i>Guardar firma';
}
})
.catch(function() {
msg.innerHTML = '<span class="text-danger"><i class="fas fa-times me-1"></i>Error de conexión.</span>';
btnSave.disabled = false;
btnSave.innerHTML = '<i class="fas fa-check me-1"></i>Guardar firma';
});
});
});
})();
/* ── Firma de consentimiento turnero (soporta múltiples campos firma) ── */
document.querySelectorAll('.turnero-firma-item').forEach(function(widget) {
var canvas = widget.querySelector('.turnero-cv');
if (!canvas) return;
var cid = widget.dataset.cid || '__firma';
var ctx = canvas.getContext('2d');
var btnLimp = widget.querySelector('.turnero-limpiar');
var btnFirm = widget.querySelector('.turnero-firmar');
var msgEl = widget.querySelector('.turnero-msg');
var drawing = false;
var btnLabel = btnFirm ? btnFirm.innerHTML : '';
(function scaleCanvas() {
var ratio = window.devicePixelRatio || 1;
var w = canvas.offsetWidth || canvas.width;
var h = canvas.offsetHeight || canvas.height;
canvas.width = w * ratio;
canvas.height = h * ratio;
canvas.style.width = w + 'px';
canvas.style.height = h + 'px';
ctx.scale(ratio, ratio);
ctx.strokeStyle = '#1565c0';
ctx.lineWidth = 2.5;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
})();
function getPos(e) {
var r = canvas.getBoundingClientRect();
var src = e.touches ? e.touches[0] : e;
return { x: src.clientX - r.left, y: src.clientY - r.top };
}
canvas.addEventListener('mousedown', function(e){ drawing=true; ctx.beginPath(); var p=getPos(e); ctx.moveTo(p.x,p.y); });
canvas.addEventListener('mousemove', function(e){ if(!drawing) return; var p=getPos(e); ctx.lineTo(p.x,p.y); ctx.stroke(); });
canvas.addEventListener('mouseup', function(){ drawing=false; });
canvas.addEventListener('mouseleave', function(){ drawing=false; });
canvas.addEventListener('touchstart', function(e){ e.preventDefault(); drawing=true; ctx.beginPath(); var p=getPos(e); ctx.moveTo(p.x,p.y); }, {passive:false});
canvas.addEventListener('touchmove', function(e){ e.preventDefault(); if(!drawing) return; var p=getPos(e); ctx.lineTo(p.x,p.y); ctx.stroke(); }, {passive:false});
canvas.addEventListener('touchend', function(){ drawing=false; });
if (btnLimp) btnLimp.addEventListener('click', function() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
msgEl.textContent = '';
});
if (btnFirm) btnFirm.addEventListener('click', function() {
var png = canvas.toDataURL('image/png');
if (png.length < 1500) {
msgEl.innerHTML = '<span class="text-danger"><i class="fas fa-exclamation-triangle me-1"></i>Por favor dibuje su firma antes de confirmar.</span>';
return;
}
btnFirm.disabled = true;
btnFirm.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Guardando...';
msgEl.textContent = '';
var campos = {};
document.querySelectorAll('[name]').forEach(function(el) {
var rawName = el.name;
var isArr = rawName.slice(-2) === '[]';
var name = isArr ? rawName.slice(0, -2) : rawName;
if (el.type === 'checkbox') {
if (el.checked) { if (!Array.isArray(campos[name])) campos[name] = []; campos[name].push(el.value); }
} else if (el.type === 'radio') {
if (el.checked) campos[name] = el.value;
} else if (el.value !== '') {
campos[name] = el.value;
}
});
campos[cid + '_svg'] = png; // identificar qué campo firmó
fetch(window.location.href, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ firma_svg: png, datos_respuestas: campos })
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.ok) {
// Ocultar todos los demás widgets de firma (ya no se puede firmar dos veces)
document.querySelectorAll('.turnero-firma-item').forEach(function(w) {
w.style.display = 'none';
});
var ok = document.createElement('div');
ok.className = 'alert alert-success mt-4 d-flex align-items-center gap-2 no-print';
ok.innerHTML = '<i class="fas fa-check-circle fs-4"></i><div><strong>Firmado correctamente.</strong><br>Puede cerrar esta ventana.</div>';
widget.parentNode.insertBefore(ok, widget.nextSibling);
try { window.parent.postMessage({ type: 'turneroFirmado' }, '*'); } catch(e) {}
} else {
msgEl.innerHTML = '<span class="text-danger"><i class="fas fa-times me-1"></i>' + (data.error || 'Error al guardar') + '</span>';
btnFirm.disabled = false;
btnFirm.innerHTML = btnLabel;
}
})
.catch(function() {
msgEl.innerHTML = '<span class="text-danger"><i class="fas fa-times me-1"></i>Error de conexión.</span>';
btnFirm.disabled = false;
btnFirm.innerHTML = btnLabel;
});
});
});
</script>
</body>
</html>