Files
whatsapp/ver_formulario_enviado.php
T
Lizandro GuarnizoandClaude Sonnet 4.6 18d50a3a16 fix: evitar datos del paciente duplicados en formularios con campos linked
Cuando el esquema tiene campos tipo linked que ya muestran nombre, documento,
etc., el bloque top Datos del paciente queda oculto para no repetir la info.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 22:51:09 -05:00

1587 lines
80 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,
ts.numero_orden
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) ?? [];
// Borrador: guardar campos sin firma (auto-save desde el formulario)
// Muestras Prolongadas: marcar completo sin SVG global
if (isset($input['mp_completar'])) {
$dr = is_array($input['datos_respuestas'] ?? null) ? $input['datos_respuestas'] : [];
$existing = json_decode($tcRow['datos_respuestas'] ?? '{}', true) ?: [];
$db->getConnection()
->prepare("UPDATE turnero_consentimientos SET estado='firmado', firmado_at=NOW(), datos_respuestas=? WHERE token=?")
->execute([json_encode(array_merge($existing, $dr), JSON_UNESCAPED_UNICODE), $tokenTurnero]);
require_once __DIR__ . '/modules/turnero/api/_helpers.php';
notificarSSE((int)$tcRow['sesion_id']);
echo json_encode(['ok' => true, 'mp_completado' => true]); exit;
}
if (!array_key_exists('firma_svg', $input) && isset($input['datos_respuestas']) && is_array($input['datos_respuestas'])) {
$existing = json_decode($tcRow['datos_respuestas'] ?? '{}', true) ?: [];
$db->getConnection()
->prepare("UPDATE turnero_consentimientos SET datos_respuestas=? WHERE token=? AND estado='pendiente'")
->execute([json_encode(array_merge($existing, $input['datos_respuestas']), JSON_UNESCAPED_UNICODE), $tokenTurnero]);
echo json_encode(['ok' => true, 'draft' => true]); exit;
}
$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,
ld.numero_orden
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
LEFT JOIN lab_domicilios ld ON ld.id = e.domicilio_id
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,
ld.numero_orden
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
LEFT JOIN lab_domicilios ld ON ld.id = e.domicilio_id
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'); }
}
}
// ── Consecutivo de orden (D-... domicilio | F-... turnero) ────────────
$numeroOrden = $modoTurnero
? ($tcRow['numero_orden'] ?? null)
: ($envio['numero_orden'] ?? null);
// ── 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';
$compact = $embebido && isset($_GET['compact']);
// Firma pre-guardada del profesional logueado (para botón de 1 clic)
$firmaProfPreguardada = null;
if ($modoTurnero && $embebido && isUserLoggedIn()) {
$uid = (int)($_SESSION['admin_user']['id'] ?? 0);
if ($uid) {
try {
$stmt = $db->getConnection()->prepare("SELECT firma_svg FROM admin_users WHERE id = ? LIMIT 1");
$stmt->execute([$uid]);
$firmaProfPreguardada = $stmt->fetchColumn() ?: null;
} catch (\Throwable $_) {}
}
}
// 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'));
// Muestras Prolongadas: detectar secciones hora+firma_profesional con intervalos de minutos
// Agrupa por prefijo de sección (ej. "Taukit", "Heliprobe") para calcular esperas correctas
$_mpMap = [];
if ($modoTurnero && $embebido && $_soloFirmaPro) {
$_mpSecs = []; $_mpLabel = null; $_mpMin = null; $_mpHora = null;
foreach ($esquema as $_c) {
$_t = $_c['tipo'] ?? '';
if ($_t === 'separador') {
$_mpLabel = $_c['label'] ?? ''; $_mpMin = null; $_mpHora = null;
if (preg_match('/[Mm]inuto\s+(\d+)/u', $_mpLabel, $_mx)) $_mpMin = (int)$_mx[1];
} elseif ($_t === 'hora' && $_mpMin !== null) {
$_mpHora = $_c['id'] ?? null;
} elseif ($_t === 'firma_profesional' && $_mpHora !== null && $_mpMin !== null) {
$_prefix = trim(explode('·', $_mpLabel)[0] ?? '');
$_mpSecs[] = ['label' => $_mpLabel, 'min' => $_mpMin, 'hora' => $_mpHora, 'firma' => $_c['id'], 'tipo' => $_prefix];
$_mpHora = null;
}
}
$_mpGroups = [];
foreach ($_mpSecs as $_s) { $_mpGroups[$_s['tipo']][] = $_s; }
foreach ($_mpGroups as $_tipo => $_secs) {
for ($i = 0, $n = count($_secs); $i < $n; $i++) {
$_s = $_secs[$i]; $_nx = $_secs[$i + 1] ?? null;
$_mpMap[$_s['firma']] = [
'hora_campo' => $_s['hora'],
'esperar_min' => $_nx ? ($_nx['min'] - $_s['min']) : 0,
'next_label' => $_nx ? $_nx['label'] : null,
'next_firma_id' => $_nx ? $_nx['firma'] : null,
'is_last' => !$_nx,
'exam_type' => $_tipo,
];
}
}
}
// 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; ?>
<?php if ($compact): ?>
/* ── Modo compact (tablet, 2 columnas) ── */
@media (min-width: 480px) {
.campos-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 4px 14px; align-items: start; }
.campos-grid .campo-full { grid-column: span 2; }
}
<?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; }
}
/* ── Topaz SigWeb overlay ─────────────────────────── */
#topaz-overlay { display:none; position:fixed; inset:0; z-index:9999;
background:rgba(0,0,0,.55); align-items:center;
justify-content:center; padding:16px; }
.topaz-modal { background:#fff; border-radius:16px; width:100%;
max-width:400px; box-shadow:0 12px 40px rgba(0,0,0,.3); overflow:hidden; }
.topaz-modal-hdr { background:linear-gradient(135deg,#1565c0,#0288d1);
color:#fff; padding:14px 18px; font-weight:700; font-size:.95rem;
display:flex; align-items:center; gap:8px; }
.topaz-modal-body { padding:20px 18px; }
.topaz-pad-area { border:2px dashed #adb5bd; border-radius:10px; padding:24px 16px;
text-align:center; background:#f8fafc; min-height:100px;
display:flex; flex-direction:column; align-items:center;
justify-content:center; gap:6px; transition:border-color .2s; }
.topaz-pad-area.has-sig { border-color:#198754; border-style:solid; background:#f0fff4; }
.topaz-pts-badge { font-size:.8rem; color:#64748b; }
.topaz-modal-footer { display:flex; gap:8px; justify-content:flex-end;
padding:12px 18px; border-top:1px solid #f1f5f9; flex-wrap:wrap; }
.btn-topaz { font-size:.83rem; padding:6px 13px; border-radius:8px; border:1.5px solid #0288d1;
background:#fff; color:#0288d1; cursor:pointer; font-weight:600;
display:inline-flex; align-items:center; gap:5px; transition:all .15s; }
.btn-topaz:hover { background:#e0f2fe; }
@keyframes mp-pulse { 0%,100%{opacity:1} 50%{opacity:.65} }
</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 if ($numeroOrden): ?>
<?php
$esDomicilio = str_starts_with($numeroOrden, 'D-');
$ordenColor = $esDomicilio ? '#bbf7d0' : '#bfdbfe';
$ordenTxt = $esDomicilio ? '#14532d' : '#1e3a8a';
$ordenLabel = $esDomicilio ? 'Domicilio' : 'Turnero';
?>
<div style="background:<?= $ordenColor ?>;color:<?= $ordenTxt ?>;border-radius:8px;
padding:4px 10px;font-size:12px;font-weight:700;letter-spacing:.03em;
margin-bottom:6px;font-family:monospace">
<?= esc2($numeroOrden) ?>
</div>
<div style="font-size:10px;opacity:.75;margin-bottom:4px"><?= $ordenLabel ?></div>
<?php endif; ?>
<?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: omitir si el esquema ya tiene campos linked que la muestran -->
<?php
$_tieneLinked = !empty(array_filter($esquema, fn($c) => ($c['tipo'] ?? '') === 'linked'));
?>
<?php if ($envio['paciente_nombre'] && !$_tieneLinked): ?>
<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
if ($compact): ?><div class="campos-grid"><?php endif;
foreach ($esquema as $campo):
$tipo = $campo['tipo'] ?? '';
if ($tipo === 'separador'): ?>
<div class="esquema-sep<?= $compact ? ' campo-full' : '' ?>"><?= 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
? (empty($_mpMap) && !$_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 && empty($_mpMap);
if ($isPro) $_renderedFirmaProfesional = true;
if (!$isPro && $fSvg) {
$_renderedFirmaPaciente = true;
$_firmaGlobalPacienteUsada = true;
}
?>
<?php if ($compact): ?><div class="campo-full"><?php endif; ?>
<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-topaz turnero-topaz" title="Usar pad biométrico Topaz">
<i class="fas fa-tablet-alt"></i>Tableta
</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): ?>
<?php if ($firmaProfPreguardada): ?>
<script>window._fpwPreFirma = <?= json_encode($firmaProfPreguardada) ?>;</script>
<?php endif; ?>
<!-- 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'] : '' ?>">
<?php if ($firmaProfPreguardada): ?>
<div class="fpw-oneclic">
<button class="btn btn-success fpw-oneclic-btn w-100" style="font-size:1rem;padding:.55rem 1rem">
<i class="fas fa-check-circle me-2"></i>Firmar consentimiento
</button>
<div class="text-center mt-1">
<a href="#" class="small text-muted fpw-usar-otra">✎ Usar otra firma</a>
</div>
</div>
<div class="fpw-canvas-wrap" style="display:none">
<?php endif; ?>
<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 flex-wrap">
<button class="btn btn-outline-secondary btn-sm fpw-clear"><i class="fas fa-eraser me-1"></i>Limpiar</button>
<button class="btn-topaz fpw-topaz" title="Usar pad biométrico Topaz">
<i class="fas fa-tablet-alt"></i>Tableta
</button>
<button class="btn btn-success btn-sm fpw-save"><i class="fas fa-check me-1"></i>Guardar firma</button>
</div>
<?php if ($firmaProfPreguardada): ?></div><?php endif; ?>
<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 if ($compact): ?></div><?php endif; ?>
<?php continue; endif;
// ── Parrafo estático ──────────────────────────────────
if ($tipo === 'parrafo'):
$ws = !empty($campo['flujoLibre']) ? 'normal' : 'pre-wrap';
?>
<div class="mb-3<?= $compact ? ' campo-full' : '' ?>" 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<?= $compact ? ' campo-full' : '' ?>" 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] ?? '';
// Auto-llenar campos del profesional desde la sesión
if ($prefill === '' && !empty($campo['auto_profesional']) && isUserLoggedIn()) {
$prefill = match($campo['auto_profesional']) {
'nombre' => $_SESSION['admin_user']['full_name'] ?? '',
'cargo' => $_SESSION['admin_user']['cargo'] ?? '',
default => ''
};
}
$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<?= $compact ? ' campo-full' : '' ?>">
<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<?= ($compact && count($opts) > 3) ? ' campo-full' : '' ?>">
<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<?= $compact ? ' campo-full' : '' ?>">
<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; ?>
<?php if ($compact): ?></div><?php endif; ?>
<!-- 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-start gap-3 no-print">
<i class="fas fa-check-circle fs-4 flex-shrink-0 mt-1"></i>
<div>
<strong>Consentimiento firmado</strong><br>
<span class="text-success-emphasis">El personal de salud ha sido notificado.</span>
<div class="mt-2 d-flex gap-2 flex-wrap">
<button onclick="window.print()" class="btn btn-warning btn-sm fw-semibold">
<i class="fas fa-file-pdf me-1"></i>Descargar PDF
</button>
<button onclick="window.close()" class="btn btn-outline-secondary btn-sm fw-semibold">
<i class="fas fa-times me-1"></i>Cerrar ventana
</button>
</div>
</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'] ?>
<?php if ($numeroOrden): ?>
&nbsp;&bull;&nbsp; Orden: <strong><?= esc2($numeroOrden) ?></strong>
<?php endif; ?>
</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>
<?php if (!empty($_mpMap)): ?>
window._muestrasMap = <?= json_encode($_mpMap, JSON_UNESCAPED_UNICODE) ?>;
window._mpSavedFirmas = {};
<?php endif; ?>
/* ── Firma del profesional: función compartida canvas/1-clic ────── */
function _guardarFirmaPro(widget, svg, msgEl) {
const turnoId = widget.dataset.turno;
const formularioId = widget.dataset.formulario;
const envioId = parseInt(widget.dataset.envio, 10);
const campoId = widget.dataset.campo;
const _mpEntry = window._muestrasMap && window._muestrasMap[campoId];
if (_mpEntry) { _guardarFirmaMP(widget, svg, msgEl, _mpEntry, campoId); return; }
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';
var datosRespuestas = {};
if (soloPro) {
document.querySelectorAll('[name]').forEach(function(el) {
var raw = el.name, isArr = raw.slice(-2) === '[]', 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 payload = isTurnero
? { turno_id: parseInt(turnoId), formulario_id: parseInt(formularioId), svg,
...(soloPro ? { solo_profesional: true, datos_respuestas: datosRespuestas } : {}) }
: { envio_id: envioId, campo_id: campoId, svg };
fetch(saveUrl, { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload) })
.then(r => r.json())
.then(function(data) {
if (data.ok) {
if (soloPro) {
document.querySelectorAll('.firma-pro-widget, .campo-edit, .section-title').forEach(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 {
var img = document.createElement('img');
img.src = svg; img.alt = 'Firma profesional';
img.style.cssText = 'max-height:140px;max-width:340px;display:block';
var box = document.createElement('div');
box.className = 'firma-box'; box.style.borderColor = '#198754';
box.appendChild(img);
widget.replaceWith(box);
}
} else {
if (msgEl) msgEl.innerHTML = '<span class="text-danger"><i class="fas fa-times me-1"></i>' + (data.error||'Error al guardar') + '</span>';
widget.querySelectorAll('button').forEach(b => { b.disabled=false; });
var btnOC = widget.querySelector('.fpw-oneclic-btn');
if (btnOC) btnOC.innerHTML = '<i class="fas fa-check-circle me-2"></i>Firmar consentimiento';
}
})
.catch(function() {
if (msgEl) msgEl.innerHTML = '<span class="text-danger"><i class="fas fa-times me-1"></i>Error de conexión.</span>';
widget.querySelectorAll('button').forEach(b => { b.disabled=false; });
});
}
/* ── Muestras Prolongadas: guardar firma por campo + countdown ───── */
function _guardarFirmaMP(widget, svg, msgEl, entry, campoId) {
var dr = {};
document.querySelectorAll('[name]').forEach(function(el) {
var n = el.name.endsWith('[]') ? el.name.slice(0,-2) : el.name;
if (el.type==='checkbox'){ if(el.checked){ if(!Array.isArray(dr[n]))dr[n]=[]; dr[n].push(el.value); } }
else if (el.type==='radio'){ if(el.checked) dr[n]=el.value; }
else if (el.value!=='') dr[n]=el.value;
});
Object.assign(dr, window._mpSavedFirmas || {});
dr[campoId + '_svg'] = svg;
(window._mpSavedFirmas = window._mpSavedFirmas||{})[campoId + '_svg'] = svg;
fetch(window.location.href, {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({ datos_respuestas: dr })
})
.then(function(r){ return r.json(); })
.then(function(data) {
if (!data.ok) {
if (msgEl) msgEl.innerHTML = '<span class="text-danger">Error al guardar</span>';
widget.querySelectorAll('button').forEach(function(b){ b.disabled=false; });
var bc = widget.querySelector('.fpw-oneclic-btn');
if (bc) bc.innerHTML = '<i class="fas fa-check-circle me-2"></i>Firmar consentimiento';
return;
}
// Mostrar firma guardada en lugar del widget
var img = document.createElement('img');
img.src = svg; img.style.cssText = 'max-height:80px;max-width:240px;display:block';
var box = document.createElement('div');
box.className = 'firma-box'; box.style.borderColor = '#198754';
box.appendChild(img);
widget.replaceWith(box);
if (!entry.is_last && entry.esperar_min > 0) {
_mpIniciarCountdown(entry.esperar_min, entry.next_label, entry.next_firma_id);
} else if (!entry.is_last) {
var nw = entry.next_firma_id ? document.getElementById('fpw-' + entry.next_firma_id) : document.querySelector('.firma-pro-widget');
if (nw) nw.scrollIntoView({ behavior:'smooth', block:'center' });
} else {
_mpMostrarFinalizar(entry.exam_type);
}
})
.catch(function() {
if (msgEl) msgEl.innerHTML = '<span class="text-danger">Error de conexión.</span>';
});
}
function _mpIniciarCountdown(minutos, nextLabel, nextFirmaId) {
var targetMs = Date.now() + minutos * 60000;
var box = document.getElementById('mp-countdown');
if (!box) {
box = document.createElement('div');
box.id = 'mp-countdown';
box.style.cssText = 'position:sticky;bottom:12px;background:#1565c0;color:#fff;padding:10px 16px;border-radius:8px;margin:12px 0;text-align:center;font-size:.9rem;z-index:50';
document.querySelector('.doc-body').appendChild(box);
}
if (box._mpTick) clearInterval(box._mpTick);
box._mpTick = setInterval(function() {
var left = targetMs - Date.now();
if (left <= 0) {
clearInterval(box._mpTick);
box.style.background = '#dc3545';
box.style.animation = 'mp-pulse 1s ease-in-out infinite';
box.innerHTML = '<strong><i class="fas fa-bell me-2"></i>¡Hora de la siguiente muestra!</strong>'
+ (nextLabel ? '<br><small>' + nextLabel + '</small>' : '');
if (navigator.vibrate) navigator.vibrate([400,200,400,200,400]);
var nw = nextFirmaId ? document.getElementById('fpw-' + nextFirmaId) : document.querySelector('.firma-pro-widget');
if (nw) {
nw.scrollIntoView({ behavior:'smooth', block:'center' });
nw.style.outline = '3px solid #dc3545';
nw.style.borderRadius = '6px';
}
return;
}
var m = Math.floor(left / 60000);
var s = Math.floor((left % 60000) / 1000);
box.innerHTML = '⏱ Próxima muestra'
+ (nextLabel ? ' <small>(' + nextLabel + ')</small>' : '')
+ ' en <strong>' + String(m).padStart(2,'0') + ':' + String(s).padStart(2,'0') + '</strong>';
}, 1000);
}
function _mpMostrarFinalizar(examType) {
if (document.getElementById('mp-finalizar')) return;
var cd = document.getElementById('mp-countdown');
if (cd) { clearInterval(cd._mpTick); cd.remove(); }
var wrap = document.createElement('div');
wrap.id = 'mp-finalizar';
wrap.style.cssText = 'position:sticky;bottom:12px;margin:12px 0;text-align:center;z-index:50';
var btn = document.createElement('button');
btn.className = 'btn btn-success fw-bold px-4 py-2';
btn.style.fontSize = '1rem';
btn.innerHTML = '<i class="fas fa-check-double me-2"></i>Finalizar toma · ' + (examType || 'Muestras');
btn.addEventListener('click', function() {
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Guardando...';
var dr = {};
document.querySelectorAll('[name]').forEach(function(el) {
var n = el.name.endsWith('[]') ? el.name.slice(0,-2) : el.name;
if (el.type==='checkbox'){ if(el.checked){ if(!Array.isArray(dr[n]))dr[n]=[]; dr[n].push(el.value); } }
else if (el.type==='radio'){ if(el.checked) dr[n]=el.value; }
else if (el.value!=='') dr[n]=el.value;
});
Object.assign(dr, window._mpSavedFirmas || {});
fetch(window.location.href, {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({ mp_completar: true, datos_respuestas: dr })
})
.then(function(r){ return r.json(); })
.then(function(data) {
if (data.ok) {
wrap.innerHTML = '<div class="alert alert-success py-2 mb-0"><i class="fas fa-check-circle me-2"></i>Toma completada correctamente.</div>';
try { window.parent.postMessage({ type:'turneroFirmado' }, '*'); } catch(e) {}
}
})
.catch(function(){});
});
wrap.appendChild(btn);
document.querySelector('.doc-body').appendChild(wrap);
}
/* ── 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 = '';
});
var btnTopazPro = widget.querySelector('.fpw-topaz');
if (btnTopazPro) btnTopazPro.addEventListener('click', function() {
topaz.activar({ canvas: canvas, onAccept: function() {} });
});
// ── Botón 1 clic: firma pre-guardada ──────────────────────
const btnOneClic = widget.querySelector('.fpw-oneclic-btn');
const btnUsarOtra = widget.querySelector('.fpw-usar-otra');
if (btnOneClic) {
btnOneClic.addEventListener('click', function() {
const preFirma = window._fpwPreFirma;
if (!preFirma) return;
btnOneClic.disabled = true;
btnOneClic.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Firmando...';
_guardarFirmaPro(widget, preFirma, msg);
});
}
if (btnUsarOtra) {
btnUsarOtra.addEventListener('click', function(e) {
e.preventDefault();
widget.querySelector('.fpw-oneclic').style.display = 'none';
widget.querySelector('.fpw-canvas-wrap').style.display = '';
});
}
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 = '';
_guardarFirmaPro(widget, svg, msg);
});
});
})();
/* ── 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 = '';
});
var btnTopazPac = widget.querySelector('.turnero-topaz');
if (btnTopazPac) btnTopazPac.addEventListener('click', function() {
topaz.activar({ canvas: canvas, onAccept: function() {} });
});
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-start gap-3 no-print';
var _enIframe = (function(){ try { return window.self !== window.top; } catch(e){ return true; } })();
ok.innerHTML = '<i class="fas fa-check-circle fs-4 flex-shrink-0 mt-1"></i>'
+ '<div><strong>Firmado correctamente.</strong><br>'
+ '<span>El personal de salud ha sido notificado.</span>'
+ '<div class="mt-2 d-flex gap-2 flex-wrap">'
+ '<button onclick="window.print()" class="btn btn-warning btn-sm fw-semibold">'
+ '<i class=\"fas fa-file-pdf me-1\"></i>Descargar PDF</button>'
+ (!_enIframe ? '<button onclick="window.close()" class="btn btn-outline-secondary btn-sm fw-semibold">'
+ '<i class=\"fas fa-times me-1\"></i>Cerrar</button>' : '')
+ '</div></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>
<!-- ── Topaz SigWeb overlay ───────────────────────────────────────── -->
<div id="topaz-overlay" style="display:none">
<div class="topaz-modal">
<div class="topaz-modal-hdr">
<i class="fas fa-tablet-alt"></i> Pad biométrico Topaz
</div>
<div class="topaz-modal-body">
<div class="topaz-pad-area" id="topaz-pad-area">
<i class="fas fa-signature fa-2x" style="color:#adb5bd" id="topaz-pad-icon"></i>
<div id="topaz-status-msg" style="font-size:.9rem;color:#64748b;font-weight:600">
Firme en el pad biométrico
</div>
<div class="topaz-pts-badge">Trazos: <span id="topaz-pts">0</span></div>
</div>
</div>
<div class="topaz-modal-footer">
<button class="btn btn-outline-secondary btn-sm" onclick="topaz.cancelar()">
<i class="fas fa-times me-1"></i>Cancelar
</button>
<button class="btn btn-outline-secondary btn-sm" onclick="topaz.limpiarPad()">
<i class="fas fa-eraser me-1"></i>Limpiar
</button>
<button class="btn btn-success btn-sm fw-semibold" id="topaz-btn-aceptar"
onclick="topaz.aceptar()" disabled>
<i class="fas fa-check me-1"></i>Aceptar firma
</button>
</div>
</div>
</div>
<script>
// ── Topaz SigWeb ────────────────────────────────────────────────────
const topaz = (() => {
const SIGWEB_URL = 'http://localhost:47289/SigWeb/SigWebTablet.js';
let _loaded = false, _ctx = null, _target = null, _poll = null;
// _target: { type: 'turnero'|'pro', canvas: CanvasElement, onAccept: fn }
function _call(fn, ...args) {
if (_ctx && typeof _ctx[fn] === 'function') return _ctx[fn](...args);
const g = window[fn];
if (typeof g === 'function') return _ctx ? g(...args, _ctx) : g(...args);
throw new Error('SigWeb: ' + fn + ' no encontrado');
}
function _loadScript() {
if (_loaded) return Promise.resolve(true);
return new Promise(res => {
const s = document.createElement('script');
s.src = SIGWEB_URL;
s.onload = () => { _loaded = true; res(true); };
s.onerror = () => res(false);
document.head.appendChild(s);
});
}
function _setStatus(msg, ok) {
const el = document.getElementById('topaz-status-msg');
if (el) { el.textContent = msg; el.style.color = ok ? '#198754' : '#64748b'; }
const area = document.getElementById('topaz-pad-area');
if (area) area.classList.toggle('has-sig', !!ok);
const icon = document.getElementById('topaz-pad-icon');
if (icon) icon.style.color = ok ? '#198754' : '#adb5bd';
}
async function activar(target) {
const ok = await _loadScript();
if (!ok) {
alert('No se detectó SigWeb.\nInstala el servicio Topaz SigWeb y vuelve a intentarlo.');
return;
}
_target = target;
try {
_ctx = typeof SigWebTablet !== 'undefined' ? new SigWebTablet() : null;
_call('SetImageXSize', 500);
_call('SetImageYSize', 150);
_call('SetImagePenWidth', 3);
_call('SetTabletState', 1);
_call('ClearTablet');
} catch(e) {
alert('Error al activar el pad: ' + e.message); return;
}
document.getElementById('topaz-overlay').style.display = 'flex';
document.getElementById('topaz-btn-aceptar').disabled = true;
document.getElementById('topaz-pts').textContent = '0';
_setStatus('Firme en el pad biométrico', false);
_poll = setInterval(() => {
try {
const pts = _call('GetSigTotalPoints');
document.getElementById('topaz-pts').textContent = pts;
document.getElementById('topaz-btn-aceptar').disabled = pts === 0;
if (pts > 0) _setStatus('✅ Firma detectada — presione Aceptar', true);
} catch(e) { _stopPoll(); }
}, 400);
}
function _stopPoll() { if (_poll) { clearInterval(_poll); _poll = null; } }
function _cerrar() {
_stopPoll();
try { if (_ctx) _call('SetTabletState', 0); } catch(e) {}
_ctx = null; _target = null;
document.getElementById('topaz-overlay').style.display = 'none';
}
function cancelar() { _cerrar(); }
function limpiarPad() {
try { _call('ClearTablet'); } catch(e) {}
document.getElementById('topaz-pts').textContent = '0';
document.getElementById('topaz-btn-aceptar').disabled = true;
_setStatus('Firme en el pad biométrico', false);
}
function aceptar() {
let b64;
try { b64 = _call('GetSigImageB64'); } catch(e) {
alert('Error al capturar la firma: ' + e.message); _cerrar(); return;
}
if (!b64) { alert('No se capturó ninguna firma.'); return; }
const target = _target;
_cerrar();
const img = new Image();
img.onload = () => {
const c = target.canvas;
const ctx = c.getContext('2d');
ctx.clearRect(0, 0, c.width, c.height);
ctx.drawImage(img, 0, 0, c.width, c.height);
c.classList.add('has-sig');
if (typeof target.onAccept === 'function') target.onAccept();
};
img.src = 'data:image/png;base64,' + b64;
}
return { activar, cancelar, limpiarPad, aceptar };
})();
<?php if ($modoTurnero && $modoEditar): ?>
// ── Auto-save borrador (campo a campo, sin firma) ─────────────
(function() {
var _t;
function leerCampos() {
var c = {};
document.querySelectorAll('[name]').forEach(function(el) {
var n = el.name.endsWith('[]') ? el.name.slice(0, -2) : el.name;
if (el.type === 'checkbox') {
if (el.checked) { if (!Array.isArray(c[n])) c[n] = []; c[n].push(el.value); }
} else if (el.type === 'radio') {
if (el.checked) c[n] = el.value;
} else if (el.value !== '') {
c[n] = el.value;
}
});
return c;
}
function guardar() {
fetch(window.location.href, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ datos_respuestas: leerCampos() })
}).catch(function() {});
}
document.addEventListener('input', function() { clearTimeout(_t); _t = setTimeout(guardar, 1200); });
document.addEventListener('change', function() { clearTimeout(_t); _t = setTimeout(guardar, 400); });
})();
<?php endif; ?>
</script>
</body>
</html>