Files
whatsapp/ver_formulario_enviado.php
T
Lizandro GuarnizoandClaude Sonnet 4.6 092dfd3074 fix: firma del paciente aparece en su posición del esquema en turnero
El campo tipo=firma se saltaba cuando no había firma existente, dejando
el canvas al fondo de la página fuera de lugar. Ahora en modoTurnero
pendiente el canvas aparece donde el esquema lo define. El widget del
fondo es solo fallback cuando el esquema no tiene campo firma propio.
Los campos siguen siendo editables como corresponde.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 01:14:23 -05:00

990 lines
50 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';
// 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; }
}
/* ── 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; // el esquema tiene campo tipo=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');
// 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'] ?? $firmaSharedPaciente ?? 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: omitir en vista normal.
// En turnero pendiente se muestra el canvas en su posición del esquema.
if (!$fSvg && !$fFoto && !$isPro) {
if ($modoTurnero && $modoEditar) {
$_esquemaTieneFirmaPaciente = true;
} else {
continue;
}
}
$yaHayCanvasPro = $isPro && $_renderedFirmaProfesional;
if ($isPro) $_renderedFirmaProfesional = true;
if (!$isPro && $fSvg) $_renderedFirmaPaciente = 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): ?>
<!-- Canvas firma paciente en la posición del esquema -->
<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 id="turneroCv" 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 id="turneroLimpiar" class="btn btn-outline-secondary btn-sm">
<i class="fas fa-eraser me-1"></i>Limpiar
</button>
<button id="turneroFirmar" class="btn btn-success fw-semibold">
<i class="fas fa-check-circle me-1"></i>Confirmar y firmar consentimiento
</button>
</div>
<div id="turneroMsg" class="mt-2 small"></div>
<?php elseif ($isPro): ?>
<?php if (($modoTurnero && isUserLoggedIn()) || (!$modoPublico && !$yaHayCanvasPro)): ?>
<!-- Canvas del profesional: solo aparece una vez -->
<div class="firma-pro-widget no-print" id="fpw-<?= htmlspecialchars($cid) ?>"
data-envio="<?= (int)$envio['id'] ?>" data-campo="<?= htmlspecialchars($cid) ?>"
<?php if ($modoTurnero): ?>
data-turno="<?= (int)$tcRow['turno_id'] ?>"
data-formulario="<?= (int)$tcRow['formulario_id'] ?>"
<?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">
<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'] ?? [];
?>
<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'] ?? [];
$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'] ?? [];
?>
<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;
// texto, numero, fecha, hora, y cualquier otro → input
$inputType = match($tipo) {
'numero' => 'number', 'fecha' => '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 ?>>
</div>
<?php continue;
endif; // modoEditar
// ── Modo vista: mostrar valores existentes ─────────────
if ($tipo === 'linked') {
$lk = $campo['linked_key'] ?? '';
$valor = $paciente[$lk] ?? $todos[$cid] ?? null;
} 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 si el esquema no tiene campo firma) ── -->
<?php if ($modoTurnero && $envio['estado'] !== 'firmado' && !$_esquemaTieneFirmaPaciente): ?>
<div class="mt-4 no-print" id="turneroFirmaWidget">
<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 id="turneroCv" 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 id="turneroLimpiar" class="btn btn-outline-secondary btn-sm">
<i class="fas fa-eraser me-1"></i>Limpiar
</button>
<button id="turneroFirmar" class="btn btn-success fw-semibold">
<i class="fas fa-check-circle me-1"></i>Confirmar y firmar consentimiento
</button>
</div>
<div id="turneroMsg" class="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');
// Verificar que no esté vacío (al menos 1000 bytes de data)
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 isTurnero = !!(turnoId && formularioId);
const saveUrl = isTurnero
? 'modules/turnero/api/firmar_profesional_consentimiento.php'
: 'api/lab/firmar_profesional.php';
const savePayload = isTurnero
? { turno_id: parseInt(turnoId), formulario_id: parseInt(formularioId), svg: svg }
: { 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) {
// Reemplazar el canvas con la imagen firmada
const img = document.createElement('img');
img.src = svg;
img.alt = 'Firma profesional';
img.style.maxHeight = '140px';
img.style.maxWidth = '340px';
img.style.display = 'block';
const 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 ────────────────────────────────── */
(function () {
var widget = document.getElementById('turneroFirmaWidget');
if (!widget) return;
var canvas = document.getElementById('turneroCv');
var ctx = canvas.getContext('2d');
var btnLimp = document.getElementById('turneroLimpiar');
var btnFirm = document.getElementById('turneroFirmar');
var msgEl = document.getElementById('turneroMsg');
var drawing = false;
(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; });
btnLimp.addEventListener('click', function() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
msgEl.textContent = '';
});
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 = '';
// Recopilar respuestas de campos interactivos
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;
}
});
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) {
widget.outerHTML =
'<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 correctamente.</strong><br>' +
'Puede cerrar esta ventana.</div></div>';
// Notificar ventana padre si está en iframe/modal
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 = '<i class="fas fa-check-circle me-1"></i>Confirmar y firmar consentimiento';
}
})
.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 = '<i class="fas fa-check-circle me-1"></i>Confirmar y firmar consentimiento';
});
});
})();
</script>
</body>
</html>