Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2038 lines
104 KiB
PHP
2038 lines
104 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.es_toma_progresiva, 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,
|
|
med.nombres AS medico_nombres, med.apellidos AS medico_apellidos,
|
|
med.cod_especialidad AS medico_especialidad,
|
|
med.codigo AS medico_codigo, med.docidmedico AS medico_docid
|
|
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)
|
|
LEFT JOIN medicos med ON med.id = ts.medico_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'] ?? '',
|
|
'fecha_nacimiento' => $tcRow['fecha_nacimiento'] ?? '',
|
|
'telefono' => $tcRow['paciente_telefono'] ?? '',
|
|
'eps' => $tcRow['eps'] ?? '',
|
|
'medico_nombre' => trim(($tcRow['medico_nombres'] ?? '') . ' ' . ($tcRow['medico_apellidos'] ?? '')),
|
|
'medico_especialidad'=> $tcRow['medico_especialidad'] ?? '',
|
|
'medico_codigo' => $tcRow['medico_codigo'] ?? '',
|
|
'medico_docid' => $tcRow['medico_docid'] ?? '',
|
|
]]),
|
|
'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 = ($modoTurnero && !empty($tcRow['es_toma_progresiva']))
|
|
|| (!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 condición del separador para no mezclar Glicemia y Cortisol (ambas usan prefijo "Toma").
|
|
$_mpMap = [];
|
|
$_mpGroupsFull = []; // grupos sin filtrar (para la pantalla de configuración)
|
|
$_mpSkippedSepIds = []; // separadores de tomas no seleccionadas (se ocultarán en render)
|
|
$_tomasConfig = null; // configuración guardada por paciente ({grupo:[firmaId,...]})
|
|
|
|
if ($modoTurnero && $embebido && $_soloFirmaPro) {
|
|
$_mpSecs = []; $_mpLabel = null; $_mpMin = null; $_mpHoraFija = null; $_mpHora = null;
|
|
$_mpCondKey = null; $_mpCondDisplay = null; $_mpSepId = null;
|
|
foreach ($esquema as $_c) {
|
|
$_t = $_c['tipo'] ?? '';
|
|
if ($_t === 'separador') {
|
|
$_mpLabel = $_c['label'] ?? ''; $_mpMin = null; $_mpHoraFija = null; $_mpHora = null;
|
|
$_mpSepId = $_c['id'] ?? null;
|
|
$_mpCond = $_c['condicion'] ?? null;
|
|
// Clave de agrupación: usa valores de condición para distinguir Glicemia vs Cortisol
|
|
if ($_mpCond) {
|
|
$_cv = $_mpCond['valores'] ?? ($_mpCond['valor'] ? [$_mpCond['valor']] : []);
|
|
$_mpCondKey = implode('|', $_cv);
|
|
$_mpCondDisplay = trim(explode('·', $_mpLabel)[0] ?? '');
|
|
} else {
|
|
$_mpCondKey = $_mpCondDisplay = trim(explode('·', $_mpLabel)[0] ?? '');
|
|
}
|
|
if (preg_match('/[Mm]inuto\s+(\d+)/u', $_mpLabel, $_mx)) {
|
|
$_mpMin = (int)$_mx[1];
|
|
} elseif (preg_match('/(\d{1,2}):(\d{2})\s*(a\.?m\.?|p\.?m\.?)/i', $_mpLabel, $_mx)) {
|
|
$_h = (int)$_mx[1]; $_m2 = (int)$_mx[2];
|
|
$_pm = strtolower(preg_replace('/[^apm]/i', '', $_mx[3])) === 'pm';
|
|
if ($_pm && $_h < 12) $_h += 12;
|
|
$_mpHoraFija = sprintf('%02d:%02d', $_h, $_m2);
|
|
$_mpMin = PHP_INT_MAX; // sentinel: va después de todos los Minuto X
|
|
}
|
|
} elseif ($_t === 'hora' && ($_mpMin !== null || $_mpHoraFija !== null)) {
|
|
$_mpHora = $_c['id'] ?? null;
|
|
} elseif ($_t === 'firma_profesional' && $_mpHora !== null && ($_mpMin !== null || $_mpHoraFija !== null)) {
|
|
$_mpSecs[] = [
|
|
'label' => $_mpLabel,
|
|
'sep_id' => $_mpSepId,
|
|
'min' => $_mpMin ?? PHP_INT_MAX,
|
|
'hora_fija' => $_mpHoraFija,
|
|
'hora' => $_mpHora,
|
|
'firma' => $_c['id'],
|
|
'tipo' => $_mpCondKey,
|
|
'display' => $_mpCondDisplay,
|
|
];
|
|
$_mpHora = null;
|
|
}
|
|
}
|
|
$_mpGroups = [];
|
|
foreach ($_mpSecs as $_s) { $_mpGroups[$_s['tipo']][] = $_s; }
|
|
$_mpGroupsFull = $_mpGroups;
|
|
|
|
// Aplicar filtro de ciclos si el profesional ya configuró las tomas para este paciente
|
|
$__rawCfg = $datosCliente['_tomas_config'] ?? null;
|
|
if (is_array($__rawCfg)) {
|
|
$_tomasConfig = $__rawCfg;
|
|
foreach ($_mpGroups as $__gk => &$__gsecs) {
|
|
if (!isset($_tomasConfig[$__gk])) continue;
|
|
$__allowed = array_flip($_tomasConfig[$__gk]);
|
|
foreach ($__gsecs as $__s) {
|
|
if (!isset($__allowed[$__s['firma']])) {
|
|
$_mpSkippedSepIds[$__s['sep_id']] = true;
|
|
}
|
|
}
|
|
$__gsecs = array_values(array_filter($__gsecs, fn($s) => isset($__allowed[$s['firma']])));
|
|
}
|
|
unset($__gsecs);
|
|
}
|
|
|
|
// Construir $_mpMap desde grupos (posiblemente filtrados)
|
|
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'],
|
|
'next_label' => $_nx ? $_nx['label'] : null,
|
|
'next_firma_id' => $_nx ? $_nx['firma'] : null,
|
|
'is_last' => !$_nx,
|
|
'exam_type' => $_s['display'],
|
|
];
|
|
}
|
|
}
|
|
}
|
|
|
|
// Primera firma pendiente en toma progresiva (las demás se ocultan hasta su turno)
|
|
$_mpPrimeraPendiente = null;
|
|
if (!empty($_mpMap)) {
|
|
foreach ($_mpMap as $_fid => $_) {
|
|
if (empty($datosCliente[$_fid]) && empty($datosCliente[$_fid . '_svg'])) {
|
|
$_mpPrimeraPendiente = $_fid;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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} }
|
|
|
|
/* ── Panel configuración de ciclos ─────────────────── */
|
|
.mp-cfg-overlay { position:fixed;inset:0;z-index:9999;background:rgba(0,0,0,.45);
|
|
display:flex;align-items:flex-start;justify-content:center;
|
|
padding:1rem;overflow-y:auto; }
|
|
.mp-cfg-card { background:#fff;border-radius:14px;max-width:500px;width:100%;
|
|
box-shadow:0 12px 48px rgba(0,0,0,.3);margin-top:.5rem; }
|
|
.mp-cfg-head { background:<?= htmlspecialchars($docColor) ?>;color:#fff;
|
|
padding:1rem 1.25rem;border-radius:14px 14px 0 0;
|
|
font-size:1rem;font-weight:700; }
|
|
.mp-cfg-body { padding:1.25rem; }
|
|
.mp-cfg-foot { padding:0 1.25rem 1.25rem; }
|
|
.mp-cfg-gtitle { font-weight:700;font-size:.88rem;color:#1e3a5f;margin-bottom:.5rem;
|
|
border-left:3px solid <?= htmlspecialchars($docColor) ?>;padding-left:.5rem; }
|
|
.mp-cfg-item { border:1.5px solid #e5e7eb;border-radius:8px;padding:.35rem .6rem;
|
|
transition:all .15s;cursor:pointer;user-select:none; }
|
|
.mp-cfg-item:has(.form-check-input:checked) { background:#f0fdf4;border-color:#86efac; }
|
|
.mp-cfg-item label { cursor:pointer; }
|
|
</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>
|
|
|
|
<?php
|
|
// ── Panel de configuración de ciclos (se muestra antes de iniciar el protocolo) ──
|
|
$_examOptions = [];
|
|
foreach ($esquema as $_oc) {
|
|
if (($_oc['id'] ?? '') === '_c8j2g16') { $_examOptions = $_oc['options'] ?? []; break; }
|
|
}
|
|
$_examSel = (array)($datosCliente['_c8j2g16'] ?? []);
|
|
$_gruposConfig = [];
|
|
foreach ($_mpGroupsFull as $_gk => $_gsecs) {
|
|
if (empty($_gsecs)) continue;
|
|
$__parts = array_values(array_filter(explode('|', $_gk)));
|
|
$__isCond = !empty(array_intersect($__parts, $_examOptions));
|
|
$__matchSel = !empty(array_intersect($__parts, $_examSel));
|
|
if (!$__isCond || $__matchSel) {
|
|
$__title = implode(' / ', array_intersect($__parts, $_examSel));
|
|
if (!$__title) $__title = $_gsecs[0]['display'] ?? $_gk;
|
|
$_gruposConfig[$_gk] = ['title' => $__title, 'secs' => $_gsecs];
|
|
}
|
|
}
|
|
// Mostrar panel solo si al menos un grupo tiene >= 2 tomas (hay algo que configurar).
|
|
// Si todos los grupos tienen 1 sola toma, el JS auto-guarda la config sin preguntar.
|
|
$_hasMulTomas = array_reduce($_gruposConfig, fn($c, $g) => $c || count($g['secs']) >= 2, false);
|
|
$_showCfgPanel = $modoTurnero && $embebido && !empty($_gruposConfig) && $_tomasConfig === null && $_hasMulTomas;
|
|
$_autoSaveCfg = $modoTurnero && $embebido && !empty($_gruposConfig) && $_tomasConfig === null && !$_hasMulTomas;
|
|
// Config por defecto (todas las tomas) — usada tanto por auto-save como por "Usar todas"
|
|
$_cfgDefault = [];
|
|
foreach ($_gruposConfig as $_cgk => $_cg) {
|
|
$_cfgDefault[$_cgk] = array_column($_cg['secs'], 'firma');
|
|
}
|
|
?>
|
|
<?php if ($_showCfgPanel): ?>
|
|
<div class="mp-cfg-overlay" id="mp-cfg-overlay">
|
|
<div class="mp-cfg-card">
|
|
<div class="mp-cfg-head d-flex align-items-center justify-content-between">
|
|
<span><i class="fas fa-sliders-h me-2"></i>Configurar protocolo de tomas</span>
|
|
<button id="mp-cfg-todas" type="button"
|
|
title="Usar todas las tomas sin configurar"
|
|
style="background:rgba(255,255,255,.2);border:none;color:#fff;border-radius:6px;
|
|
padding:3px 10px;font-size:.8rem;cursor:pointer;white-space:nowrap">
|
|
<i class="fas fa-forward me-1"></i>Usar todas
|
|
</button>
|
|
</div>
|
|
<div class="mp-cfg-body">
|
|
<p class="text-muted small mb-3">
|
|
Seleccione las tomas a realizar. Desmárquelas si el médico indicó un protocolo reducido.
|
|
</p>
|
|
<?php foreach ($_gruposConfig as $_cgk => $_cg): ?>
|
|
<div class="mb-3">
|
|
<div class="mp-cfg-gtitle"><?= esc2($_cg['title']) ?></div>
|
|
<div class="row g-2">
|
|
<?php foreach ($_cg['secs'] as $_cs): ?>
|
|
<div class="col-6 col-sm-4">
|
|
<div class="mp-cfg-item form-check">
|
|
<input class="form-check-input mp-cfg-chk" type="checkbox"
|
|
id="cfg-<?= esc2($_cs['firma']) ?>"
|
|
value="<?= esc2($_cs['firma']) ?>"
|
|
data-group="<?= esc2($_cgk) ?>"
|
|
checked>
|
|
<label class="form-check-label small" for="cfg-<?= esc2($_cs['firma']) ?>">
|
|
<?= esc2($_cs['label']) ?>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
<div class="mp-cfg-foot">
|
|
<button class="btn btn-success btn-lg w-100" id="mp-cfg-iniciar">
|
|
<i class="fas fa-play-circle me-2"></i>Iniciar protocolo
|
|
</button>
|
|
<div id="mp-cfg-msg" class="text-center mt-2 small"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<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;
|
|
$_saltarSeccion = false;
|
|
foreach ($esquema as $campo):
|
|
$tipo = $campo['tipo'] ?? '';
|
|
if ($tipo === 'separador'):
|
|
$cond = $campo['condicion'] ?? null;
|
|
if ($cond) {
|
|
$vCtrl = $todos[$cond['campo_id']] ?? null;
|
|
$vCond = $cond['valores'] ?? ($cond['valor'] ? [$cond['valor']] : []);
|
|
$_saltarSeccion = is_array($vCtrl)
|
|
? empty(array_intersect($vCond, $vCtrl))
|
|
: !in_array($vCtrl, $vCond, true);
|
|
} else {
|
|
$_saltarSeccion = false;
|
|
}
|
|
// Ocultar secciones de tomas excluidas por la configuración de ciclos del paciente
|
|
if (!$_saltarSeccion && isset($_mpSkippedSepIds[$campo['id'] ?? ''])) {
|
|
$_saltarSeccion = true;
|
|
}
|
|
if ($_saltarSeccion) continue; ?>
|
|
<div class="esquema-sep<?= $compact ? ' campo-full' : '' ?>" data-campo-id="<?= esc2($campo['id'] ?? '') ?>"><?= esc2($campo['label'] ?? '') ?></div>
|
|
<?php continue; endif;
|
|
if ($_saltarSeccion) continue;
|
|
|
|
// ── 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] ?? $datosCliente[$cid . '_svg'] ?? null)
|
|
: ($datosCliente[$cid] ?? $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 (sin importar si también hay firma paciente)
|
|
// Pro canvas: mostrar si el profesional aún no firmó (incluso si el paciente ya firmó via WA y estado='firmado')
|
|
$_mostrarCanvasPro = !$yaHayCanvasPro && (
|
|
(!$modoTurnero && !$modoPublico) ||
|
|
($modoTurnero && $embebido && isUserLoggedIn())
|
|
);
|
|
if ($_mostrarCanvasPro): ?>
|
|
<?php if ($firmaProfPreguardada): ?>
|
|
<script>window._fpwPreFirma = <?= json_encode($firmaProfPreguardada) ?>;</script>
|
|
<?php endif; ?>
|
|
<!-- Canvas del profesional -->
|
|
<?php
|
|
$_mpBloqueado = !empty($_mpMap)
|
|
&& $_mpPrimeraPendiente !== null
|
|
&& $cid !== $_mpPrimeraPendiente
|
|
&& empty($datosCliente[$cid])
|
|
&& empty($datosCliente[$cid . '_svg']);
|
|
?>
|
|
<div class="firma-pro-widget no-print" id="fpw-<?= htmlspecialchars($cid) ?>"
|
|
data-envio="<?= (int)$envio['id'] ?>" data-campo="<?= htmlspecialchars($cid) ?>"
|
|
data-solo-pro="<?= $modoTurnero ? '1' : '0' ?>"
|
|
data-turno="<?= isset($tcRow) ? (int)$tcRow['turno_id'] : '' ?>"
|
|
data-formulario="<?= isset($tcRow) ? (int)$tcRow['formulario_id'] : '' ?>"
|
|
<?= $_mpBloqueado ? 'style="display:none"' : '' ?>>
|
|
<?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 ($lk === 'fecha_nacimiento' && $lval && preg_match('/^\d{4}-\d{2}-\d{2}/', $lval)) {
|
|
$lval = date('d/m/Y', strtotime($lval));
|
|
}
|
|
if ($lval !== ''):
|
|
?>
|
|
<div class="campo-linked" data-campo-id="<?= esc2($cid) ?>">
|
|
<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' : '' ?>" data-campo-id="<?= esc2($cid) ?>">
|
|
<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' : '' ?>" data-campo-id="<?= esc2($cid) ?>">
|
|
<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' : '' ?>" data-campo-id="<?= esc2($cid) ?>">
|
|
<label><?= $label ?></label>
|
|
<div class="d-flex flex-wrap gap-2">
|
|
<?php foreach ($opts as $opt): ?>
|
|
<div class="form-check form-check-inline m-0">
|
|
<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>
|
|
</div>
|
|
<?php continue; endif;
|
|
if ($tipo === 'select'):
|
|
$opts = $campo['opciones'] ?? $campo['options'] ?? [];
|
|
?>
|
|
<div class="campo-edit" data-campo-id="<?= esc2($cid) ?>">
|
|
<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" data-campo-id="<?= esc2($cid) ?>">
|
|
<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 && !$_soloFirmaPro): ?>
|
|
<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) ?> •
|
|
<?php endif; ?>
|
|
<i class="fas fa-shield-alt me-1"></i>Generado el <?= date('d/m/Y H:i') ?> • ID #<?= $envio['id'] ?>
|
|
<?php if ($numeroOrden): ?>
|
|
• 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)): ?>
|
|
/* ── Panel configuración de ciclos + auto-save ── */
|
|
<?php if (($_showCfgPanel || $_autoSaveCfg) ?? false): ?>
|
|
var _mpCfgDefault = <?= json_encode($_cfgDefault, JSON_UNESCAPED_UNICODE) ?>;
|
|
var _mpCfgToken = <?= json_encode($tokenTurnero) ?>;
|
|
|
|
function _mpGuardarConfig(config, onOk, onErr) {
|
|
var base = window.location.href.split('/ver_formulario_enviado.php')[0];
|
|
fetch(base + '/modules/turnero/api/configurar_tomas.php', {
|
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ token: _mpCfgToken, tomas_config: config })
|
|
})
|
|
.then(function(r) { return r.json(); })
|
|
.then(function(d) { d.ok ? onOk() : onErr(d.error || 'Error desconocido'); })
|
|
.catch(function() { onErr('Error de conexión.'); });
|
|
}
|
|
|
|
<?php if ($_autoSaveCfg ?? false): ?>
|
|
// Todos los grupos tienen una sola toma → guardar config default sin mostrar panel
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
_mpGuardarConfig(_mpCfgDefault, function() { location.reload(); }, function(e) {
|
|
console.warn('mp-cfg auto-save:', e);
|
|
});
|
|
});
|
|
<?php endif; ?>
|
|
|
|
<?php if ($_showCfgPanel ?? false): ?>
|
|
(function() {
|
|
function showMsg(msg) {
|
|
var el = document.getElementById('mp-cfg-msg');
|
|
if (el) el.innerHTML = '<span class="text-danger"><i class="fas fa-exclamation-circle me-1"></i>' + msg + '</span>';
|
|
}
|
|
function setBtnLoading(btn) {
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Guardando...';
|
|
}
|
|
|
|
// Botón "Usar todas" → guarda config default y recarga
|
|
var btnTodas = document.getElementById('mp-cfg-todas');
|
|
if (btnTodas) {
|
|
btnTodas.addEventListener('click', function() {
|
|
setBtnLoading(this);
|
|
_mpGuardarConfig(_mpCfgDefault, function() { location.reload(); }, function(e) {
|
|
showMsg(e);
|
|
btnTodas.disabled = false;
|
|
btnTodas.innerHTML = '<i class="fas fa-forward me-1"></i>Usar todas';
|
|
});
|
|
});
|
|
}
|
|
|
|
// Botón "Iniciar protocolo" → lee selección actual
|
|
var btnIniciar = document.getElementById('mp-cfg-iniciar');
|
|
if (btnIniciar) {
|
|
btnIniciar.addEventListener('click', function() {
|
|
var byGroup = {};
|
|
document.querySelectorAll('.mp-cfg-chk').forEach(function(chk) {
|
|
var g = chk.dataset.group;
|
|
if (!byGroup[g]) byGroup[g] = [];
|
|
if (chk.checked) byGroup[g].push(chk.value);
|
|
});
|
|
for (var g in byGroup) {
|
|
if (!byGroup[g].length) { showMsg('Seleccione al menos una toma por protocolo.'); return; }
|
|
}
|
|
setBtnLoading(this);
|
|
_mpGuardarConfig(byGroup, function() { location.reload(); }, function(e) {
|
|
showMsg(e);
|
|
btnIniciar.disabled = false;
|
|
btnIniciar.innerHTML = '<i class="fas fa-play-circle me-2"></i>Iniciar protocolo';
|
|
});
|
|
});
|
|
}
|
|
})();
|
|
<?php endif; ?>
|
|
<?php endif; ?>
|
|
window._muestrasMap = <?= json_encode($_mpMap, JSON_UNESCAPED_UNICODE) ?>;
|
|
window._mpSavedFirmas = {};
|
|
|
|
function _mpAutoFillHora(firmaId) {
|
|
var entry = window._muestrasMap && window._muestrasMap[firmaId];
|
|
if (!entry || !entry.hora_campo) return;
|
|
var el = document.querySelector('[name="' + entry.hora_campo + '"]');
|
|
if (!el || el.value) return;
|
|
var now = new Date();
|
|
el.value = ('0'+now.getHours()).slice(-2) + ':' + ('0'+now.getMinutes()).slice(-2);
|
|
}
|
|
|
|
// Auto-scroll y auto-fill hora a la primera firma pendiente al cargar
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
setTimeout(function() {
|
|
var fw = document.querySelector('.firma-pro-widget:not([style*="display:none"])');
|
|
if (fw) {
|
|
fw.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
_mpAutoFillHora(fw.dataset.campo);
|
|
}
|
|
}, 350);
|
|
});
|
|
<?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 || {});
|
|
// Guardar con campoId directo (sin _svg) para que get_consentimientos lo cuente
|
|
dr[campoId] = svg;
|
|
(window._mpSavedFirmas = window._mpSavedFirmas||{})[campoId] = svg;
|
|
|
|
var turnoId = parseInt(widget.dataset.turno, 10);
|
|
var formularioId = parseInt(widget.dataset.formulario, 10);
|
|
var base = window.location.href.split('/ver_formulario_enviado.php')[0];
|
|
|
|
fetch(base + '/modules/turnero/api/guardar_toma.php', {
|
|
method:'POST', headers:{'Content-Type':'application/json'},
|
|
body: JSON.stringify({
|
|
turno_id: turnoId,
|
|
formulario_id: formularioId,
|
|
campo_id: campoId,
|
|
svg: svg,
|
|
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 (data.completado) {
|
|
// Toma completada: mostrar resumen de tomas y cerrar modal
|
|
var cd = document.getElementById('mp-countdown');
|
|
if (cd) { clearInterval(cd._mpTick); cd.remove(); }
|
|
_mpMostrarResumen(entry.exam_type || 'Examen');
|
|
setTimeout(function() {
|
|
try { window.parent.postMessage({ type: 'turneroFirmado' }, '*'); } catch(e) {}
|
|
}, 2200);
|
|
} else if (data.siguiente_toma_at) {
|
|
var targetMs = new Date(data.siguiente_toma_at.replace(' ', 'T')).getTime();
|
|
var minsLeft = Math.max(1, Math.round((targetMs - Date.now()) / 60000));
|
|
_mpIniciarCountdown(minsLeft, 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) : null;
|
|
if (!nw) nw = document.querySelector('.firma-pro-widget:not([style*="display:none"])');
|
|
if (nw) { nw.style.display = ''; nw.scrollIntoView({ behavior:'smooth', block:'center' }); _mpAutoFillHora(nw.dataset.campo); }
|
|
} else {
|
|
// Último firma del grupo pero hay más exámenes pendientes; desbloquear siguiente
|
|
var nw = document.querySelector('.firma-pro-widget[style*="display:none"]');
|
|
if (nw) {
|
|
nw.style.display = '';
|
|
nw.scrollIntoView({ behavior:'smooth', block:'center' });
|
|
_mpAutoFillHora(nw.dataset.campo);
|
|
} else {
|
|
// Sin más firmas pendientes → cerrar
|
|
setTimeout(function() {
|
|
try { window.parent.postMessage({ type: 'turneroFirmado' }, '*'); } catch(e) {}
|
|
}, 1200);
|
|
}
|
|
}
|
|
})
|
|
.catch(function() {
|
|
if (msgEl) msgEl.innerHTML = '<span class="text-danger">Error de conexión.</span>';
|
|
});
|
|
}
|
|
|
|
function _mpPlayBeep() {
|
|
try {
|
|
var ctx = new (window.AudioContext || window.webkitAudioContext)();
|
|
[880, 1100, 880, 1100].forEach(function(freq, i) {
|
|
var osc = ctx.createOscillator(), gain = ctx.createGain();
|
|
osc.connect(gain); gain.connect(ctx.destination);
|
|
osc.frequency.value = freq;
|
|
var t0 = ctx.currentTime + i * 0.22;
|
|
gain.gain.setValueAtTime(0.7, t0);
|
|
gain.gain.exponentialRampToValueAtTime(0.001, t0 + 0.18);
|
|
osc.start(t0); osc.stop(t0 + 0.18);
|
|
});
|
|
} catch(e) {}
|
|
}
|
|
|
|
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:0;left:0;right:0;background:#1565c0;color:#fff;'
|
|
+ 'padding:14px 20px;margin:16px -14px -12px;text-align:center;z-index:60;'
|
|
+ 'box-shadow:0 -4px 20px rgba(21,101,192,.35)';
|
|
document.querySelector('.doc-body').appendChild(box);
|
|
}
|
|
if (box._mpTick) clearInterval(box._mpTick);
|
|
|
|
// Notificar al padre que bloquee el cierre
|
|
try { window.parent.postMessage({ type: 'tomaProgresivaIniciada' }, '*'); } catch(e) {}
|
|
|
|
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 = '<div style="font-size:1.1rem;font-weight:700"><i class="fas fa-bell me-2"></i>¡Hora de la siguiente muestra!</div>'
|
|
+ (nextLabel ? '<div style="font-size:.85rem;opacity:.9;margin-top:4px">' + nextLabel + '</div>' : '');
|
|
if (navigator.vibrate) navigator.vibrate([500,150,500,150,500]);
|
|
_mpPlayBeep();
|
|
// Desbloquear y mostrar siguiente canvas
|
|
var nw = nextFirmaId ? document.getElementById('fpw-' + nextFirmaId) : null;
|
|
if (!nw) nw = document.querySelector('.firma-pro-widget[style*="display:none"]');
|
|
if (nw) {
|
|
nw.style.display = '';
|
|
nw.style.outline = '3px solid #dc3545';
|
|
nw.style.borderRadius = '8px';
|
|
_mpAutoFillHora(nw.dataset.campo);
|
|
setTimeout(function() { nw.scrollIntoView({ behavior:'smooth', block:'center' }); }, 120);
|
|
}
|
|
return;
|
|
}
|
|
var m = Math.floor(left / 60000);
|
|
var s = Math.floor((left % 60000) / 1000);
|
|
var pct = Math.max(0, left / (minutos * 60000)) * 100;
|
|
box.style.background = pct < 15 ? '#f59e0b' : '#1565c0';
|
|
box.style.animation = '';
|
|
box.innerHTML = '<div style="font-size:.75rem;opacity:.85;margin-bottom:4px;text-transform:uppercase;letter-spacing:.05em">'
|
|
+ '<i class="fas fa-clock me-1"></i>Próxima muestra' + (nextLabel ? ' · ' + nextLabel : '') + '</div>'
|
|
+ '<div style="font-size:2.4rem;font-weight:900;font-family:monospace;line-height:1;letter-spacing:.04em">'
|
|
+ String(m).padStart(2,'0') + '<span style="opacity:.6;animation:mp-pulse .8s infinite">:</span>' + String(s).padStart(2,'0')
|
|
+ '</div>';
|
|
}, 1000);
|
|
}
|
|
|
|
function _mpMostrarResumen(examType) {
|
|
if (document.getElementById('mp-resumen')) return;
|
|
// Recopilar horas y resultados de las tomas visibles
|
|
var rows = [];
|
|
if (window._muestrasMap) {
|
|
Object.keys(window._muestrasMap).forEach(function(fid) {
|
|
var entry = window._muestrasMap[fid];
|
|
if (!entry || entry.exam_type !== examType) return;
|
|
var horaEl = entry.hora_campo ? document.querySelector('[name="' + entry.hora_campo + '"]') : null;
|
|
var horaVal = horaEl ? horaEl.value : '';
|
|
// Buscar campo resultado adyacente (previo al firma widget)
|
|
var fpw = document.getElementById('fpw-' + fid);
|
|
var resVal = '';
|
|
if (fpw) {
|
|
var prev = fpw.previousElementSibling;
|
|
while (prev) {
|
|
var inp = prev.querySelector && prev.querySelector('input[type="number"],input[type="text"]');
|
|
if (inp) { resVal = inp.value; break; }
|
|
prev = prev.previousElementSibling;
|
|
}
|
|
}
|
|
// Separador label para esta firma
|
|
var secLabel = entry.next_label || ('Toma ' + fid);
|
|
// Si tiene next_label del ANTERIOR, buscamos la label de esta entrada en el DOM
|
|
var sepEl = document.querySelector('[data-campo-id]');
|
|
rows.push({ hora: horaVal, resultado: resVal, firmado: !!(window._mpSavedFirmas && window._mpSavedFirmas[fid]) });
|
|
});
|
|
}
|
|
var wrap = document.createElement('div');
|
|
wrap.id = 'mp-resumen';
|
|
wrap.className = 'alert alert-success mt-4';
|
|
var rowsHtml = rows.length
|
|
? '<table class="table table-sm mt-2 mb-0" style="font-size:.82rem"><thead><tr>'
|
|
+ '<th>Hora</th><th>Resultado</th><th></th></tr></thead><tbody>'
|
|
+ rows.map(function(r) {
|
|
return '<tr><td>' + (r.hora || '—') + '</td>'
|
|
+ '<td>' + (r.resultado || '—') + '</td>'
|
|
+ '<td style="color:#198754"><i class="fas fa-check-circle"></i></td></tr>';
|
|
}).join('') + '</tbody></table>' : '';
|
|
wrap.innerHTML = '<div class="d-flex align-items-center gap-2"><i class="fas fa-check-double fs-5"></i>'
|
|
+ '<div><strong>' + (examType || 'Examen') + ' completado.</strong><br>'
|
|
+ '<small>Todas las tomas registradas. Cerrando...</small></div></div>' + rowsHtml;
|
|
document.querySelector('.doc-body').appendChild(wrap);
|
|
// Ocultar countdown si queda
|
|
var cd = document.getElementById('mp-countdown');
|
|
if (cd) { clearInterval(cd._mpTick); cd.remove(); }
|
|
}
|
|
|
|
/* ── 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ó
|
|
|
|
// Si hay canvas del profesional en el form, guardar como borrador (el profesional finaliza)
|
|
var hayCanvasPro = !!document.querySelector('.firma-pro-widget');
|
|
var payload = hayCanvasPro
|
|
? { datos_respuestas: campos }
|
|
: { firma_svg: png, datos_respuestas: campos };
|
|
|
|
fetch(window.location.href, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload)
|
|
})
|
|
.then(function(r) { return r.json(); })
|
|
.then(function(data) {
|
|
if (data.ok) {
|
|
// Ocultar todos los canvas de firma del paciente (ya firmó)
|
|
document.querySelectorAll('.turnero-firma-item').forEach(function(w) {
|
|
w.style.display = 'none';
|
|
});
|
|
if (hayCanvasPro) {
|
|
// Mostrar mensaje de éxito parcial y hacer scroll al canvas del profesional
|
|
msgEl.innerHTML = '<span class="text-success"><i class="fas fa-check me-1"></i>Firma del paciente guardada. Ahora firma el profesional.</span>';
|
|
var fpw = document.querySelector('.firma-pro-widget');
|
|
if (fpw) setTimeout(function() { fpw.scrollIntoView({ behavior:'smooth', block:'center' }); }, 150);
|
|
} else {
|
|
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): ?>
|
|
// ── Condiciones de visibilidad (secciones condicionales) ──────
|
|
(function() {
|
|
var esquema = <?= json_encode($esquema, JSON_UNESCAPED_UNICODE) ?>;
|
|
var secciones = [], secActual = null;
|
|
esquema.forEach(function(c) {
|
|
if (c.tipo === 'separador') {
|
|
secActual = { sepId: c.id, condicion: c.condicion || null, elemIds: [] };
|
|
secciones.push(secActual);
|
|
} else if (secActual) {
|
|
secActual.elemIds.push(c.id);
|
|
}
|
|
});
|
|
var condicionales = secciones.filter(function(s) { return s.condicion; });
|
|
if (!condicionales.length) return;
|
|
|
|
function evaluar(evt) {
|
|
condicionales.forEach(function(sec) {
|
|
var cond = sec.condicion;
|
|
var valoresCond = cond.valores ? cond.valores : (cond.valor ? [cond.valor] : []);
|
|
var ctrl = document.querySelector('[data-campo-id="' + cond.campo_id + '"]');
|
|
if (!ctrl) return;
|
|
var activo = false;
|
|
var checks = ctrl.querySelectorAll('input[type="checkbox"]');
|
|
if (checks.length) {
|
|
checks.forEach(function(cb) { if (valoresCond.includes(cb.value) && cb.checked) activo = true; });
|
|
} else {
|
|
var radio = ctrl.querySelector('input[type="radio"]:checked');
|
|
var sel = ctrl.querySelector('select');
|
|
var v = radio ? radio.value : (sel ? sel.value : '');
|
|
activo = valoresCond.includes(v);
|
|
}
|
|
var ids = [sec.sepId].concat(sec.elemIds);
|
|
ids.forEach(function(id) {
|
|
var el = document.querySelector('[data-campo-id="' + id + '"]');
|
|
if (!el) return;
|
|
el.style.transition = 'opacity .2s, max-height .3s';
|
|
el.style.maxHeight = activo ? '2000px' : '0';
|
|
el.style.opacity = activo ? '1' : '0';
|
|
el.style.overflow = activo ? '' : 'hidden';
|
|
el.style.pointerEvents = activo ? '' : 'none';
|
|
});
|
|
});
|
|
|
|
// Toma progresiva: scroll a la primera firma pendiente
|
|
if (window._muestrasMap) {
|
|
setTimeout(function() {
|
|
var fw = document.querySelector('.firma-pro-widget:not([style*="display:none"])');
|
|
if (fw) {
|
|
fw.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
} else if (evt) {
|
|
// Firma no está en el DOM (examen recién seleccionado) → guardar y recargar
|
|
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;
|
|
});
|
|
fetch(window.location.href, {
|
|
method:'POST', headers:{'Content-Type':'application/json'},
|
|
body: JSON.stringify({ datos_respuestas: dr })
|
|
}).then(function() { window.location.reload(); })
|
|
.catch(function() { window.location.reload(); });
|
|
}
|
|
}, 400);
|
|
}
|
|
}
|
|
|
|
var ctrlIds = [...new Set(condicionales.map(function(s) { return s.condicion.campo_id; }))];
|
|
ctrlIds.forEach(function(cid) {
|
|
var el = document.querySelector('[data-campo-id="' + cid + '"]');
|
|
if (el) el.addEventListener('change', evaluar);
|
|
});
|
|
evaluar();
|
|
})();
|
|
// ── 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>
|