Files
whatsapp/ver_formulario_enviado.php
T

3192 lines
167 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,
tc.siguiente_toma_at, tc.toma_inicio_at,
f.nombre AS form_nombre, f.categoria, f.descripcion AS form_descripcion,
f.esquema, f.es_toma_progresiva, f.solo_profesional, 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 (in_array($tcRow['estado'], ['firmado', 'cerrado_anticipado'])) {
echo json_encode(['ok' => false, 'error' => 'Este consentimiento ya fue finalizado']); exit;
}
$input = json_decode(file_get_contents('php://input'), true) ?? [];
// Cierre anticipado de toma progresiva con justificación
if (isset($input['mp_cerrar_anticipado'])) {
$motivo = trim($input['motivo'] ?? '');
if (!$motivo) { echo json_encode(['ok' => false, 'error' => 'Motivo requerido']); exit; }
$existing = json_decode($tcRow['datos_respuestas'] ?? '{}', true) ?: [];
$existing['_cierre_anticipado'] = ['motivo' => $motivo, 'cerrado_at' => date('Y-m-d H:i:s')];
$db->getConnection()
->prepare("UPDATE turnero_consentimientos SET estado='cerrado_anticipado', firmado_at=NOW(), datos_respuestas=? WHERE token=?")
->execute([json_encode($existing, JSON_UNESCAPED_UNICODE), $tokenTurnero]);
require_once __DIR__ . '/modules/turnero/api/_helpers.php';
notificarSSE((int)$tcRow['sesion_id']);
echo json_encode(['ok' => true, 'mp_cerrado' => true]); exit;
}
// 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) ?: [];
$allowSave = $tcRow['estado'] === 'pendiente'
|| ($modoTurnero && in_array($tcRow['estado'], ['en_progreso', 'enviado']));
if ($allowSave) {
$db->getConnection()
->prepare("UPDATE turnero_consentimientos SET datos_respuestas=? WHERE token=?")
->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 && !in_array($envio['estado'], ['firmado', 'cerrado_anticipado']);
$embebido = isset($_GET['embed']) && $_GET['embed'] === '1';
$compact = $embebido && isset($_GET['compact']);
$autoprint = isset($_GET['autoprint']) && $_GET['autoprint'] === '1';
// Firma pre-guardada del profesional logueado (para botón de 1 clic)
$firmaProfPreguardada = null;
$profNombre = null;
$profDocumento = null;
if ($modoTurnero && $embebido && isUserLoggedIn()) {
$uid = (int)($_SESSION['admin_user']['id'] ?? 0);
if ($uid) {
try {
$stmt = $db->getConnection()->prepare(
"SELECT au.firma_svg, au.full_name,
le.nombre_completo AS pro_nombre, le.numero_documento AS pro_documento
FROM admin_users au
LEFT JOIN lab_enfermeras le ON le.id = au.enfermera_id
WHERE au.id = ? LIMIT 1"
);
$stmt->execute([$uid]);
$pRow = $stmt->fetch(PDO::FETCH_ASSOC) ?: [];
$firmaProfPreguardada = $pRow['firma_svg'] ?: null;
$profNombre = $pRow['pro_nombre'] ?: ($pRow['full_name'] ?: null);
$profDocumento = $pRow['pro_documento'] ?: null;
} catch (\Throwable $_) {}
}
}
// Formulario marcado explícitamente como solo-profesional (sin firma de ningún tipo)
$_formSoloPro = $modoTurnero && $embebido && !empty($tcRow['solo_profesional']);
// 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 (stripos($_mpLabel, 'Otro') !== false) {
// Si el usuario ingresó la hora de "Otro", usarla para ordenar cronológicamente
$_otroValor = trim((string)($datosCliente['_otro_hora'] ?? ''));
$_mpMin = is_numeric($_otroValor) ? (int)$_otroValor : PHP_INT_MAX;
}
} 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;
}
}
// Ordenar por tiempo antes de agrupar para garantizar orden cronológico
usort($_mpSecs, fn($a, $b) => $a['min'] <=> $b['min']);
// Mapa sep_id → minuto para re-ordenar secciones en el render
$_mpSepOrder = [];
foreach ($_mpSecs as $_s) { $_mpSepOrder[$_s['sep_id']] = $_s['min']; }
$_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; // exported to JS as-is (nested format)
// Flatten nested { examName: { groupKey: [fids] } } → { groupKey: [fids] } for rendering
$__first = reset($__rawCfg);
$__isNested = is_array($__first) && !empty($__first) && is_array(reset($__first));
$__flatCfg = [];
if ($__isNested) {
foreach ($__rawCfg as $__examCfg) {
if (!is_array($__examCfg)) continue;
foreach ($__examCfg as $__gk => $__fids) {
foreach ($__fids as $__fid) $__flatCfg[$__gk][] = $__fid;
}
}
foreach ($__flatCfg as &$__v) $__v = array_values(array_unique($__v));
unset($__v);
} else {
$__flatCfg = $__rawCfg;
}
foreach ($_mpGroups as $__gk => &$__gsecs) {
if (!isset($__flatCfg[$__gk])) {
foreach ($__gsecs as $__s) { $_mpSkippedSepIds[$__s['sep_id']] = true; }
$__gsecs = [];
continue;
}
$__allowed = array_flip($__flatCfg[$__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']] = [
'label' => $_s['label'],
'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 firma_id → obs_campo_id (campo texto/textarea que acompaña cada firma)
$_mpFirmaObs = [];
if (!empty($esquema) && !empty($_mpMap)) {
$_prevFirma = null; $_pendingObs = null;
foreach ($esquema as $_oc) {
$_ot = $_oc['tipo'] ?? '';
if ($_ot === 'separador') {
$_prevFirma = null; $_pendingObs = null;
} elseif (($_ot === 'texto' || $_ot === 'textarea') && isset($_oc['id'])) {
if ($_prevFirma !== null) {
$_mpFirmaObs[$_prevFirma] = $_oc['id'];
$_prevFirma = null;
} else {
$_pendingObs = $_oc['id'];
}
} elseif ($_ot === 'firma_profesional' && isset($_oc['id']) && isset($_mpMap[$_oc['id']])) {
if ($_pendingObs !== null) { $_mpFirmaObs[$_oc['id']] = $_pendingObs; }
$_prevFirma = $_oc['id']; $_pendingObs = null;
} elseif (!in_array($_ot, ['hora'])) {
$_prevFirma = null;
}
}
foreach ($_mpFirmaObs as $_fid => $_obsId) {
if (isset($_mpMap[$_fid])) $_mpMap[$_fid]['obs_campo'] = $_obsId;
}
}
// Horas de tomas ya firmadas → read-only en edición (no se pueden corregir si ya se firmó esa toma)
$_signedHoraCampos = [];
foreach ($_mpMap as $_fid => $_mpe) {
if ((!empty($datosCliente[$_fid]) && strlen($datosCliente[$_fid]) > 10)
|| (!empty($datosCliente[$_fid . '_svg']) && strlen($datosCliente[$_fid . '_svg']) > 10)) {
if (!empty($_mpe['hora_campo'])) $_signedHoraCampos[$_mpe['hora_campo']] = true;
}
}
$_hcToFirmaIds = [];
foreach ($_mpMap as $_fid => $_mpe) {
if (!empty($_mpe['hora_campo'])) $_hcToFirmaIds[$_mpe['hora_campo']][] = $_fid;
}
// siguiente_toma_at para recuperar countdown si el modal fue cerrado y reabierto
$_mpSiguienteTomAt = null;
if ($modoTurnero && $embebido && !empty($_mpMap) && $_mpPrimeraPendiente !== null) {
$raw = $tcRow['siguiente_toma_at'] ?? null;
if ($raw && $raw > date('Y-m-d H:i:s')) {
$_mpSiguienteTomAt = $raw;
}
}
// 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);
}
function _addExamWizardHtml(string $cid): string {
return '
<div id="mp-exam-badges" class="mp-exam-badges"></div>
<div class="mt-2" id="aew-wrap-' . $cid . '">
<button type="button" class="btn btn-link btn-sm p-0 text-muted" onclick="aewOpen(\'' . $cid . '\')">
<i class="fas fa-plus-circle me-1"></i>Añadir tipo de examen…
</button>
<div id="aew-box-' . $cid . '" class="d-none mt-2 border rounded p-3 bg-light" style="max-width:480px">
<!-- Paso 1: nombre -->
<div id="aew-s1-' . $cid . '">
<div class="fw-semibold small mb-2"><i class="fas fa-vials me-1 text-primary"></i>Nuevo tipo de examen</div>
<div class="mb-2">
<label class="form-label small mb-1">Nombre del examen</label>
<input type="text" id="aew-name-' . $cid . '" class="form-control form-control-sm"
placeholder="Ej: Curva de Insulina" maxlength="80">
</div>
<div class="mb-2">
<label class="form-label small mb-1">¿Cuántas tomas?</label>
<div class="d-flex align-items-center gap-2">
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="aewAddToma(\'' . $cid . '\',-1)"><i class="fas fa-minus"></i></button>
<span id="aew-count-' . $cid . '" class="fw-bold" style="min-width:20px;text-align:center">1</span>
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="aewAddToma(\'' . $cid . '\',1)"><i class="fas fa-plus"></i></button>
</div>
</div>
<div id="aew-tomas-' . $cid . '" class="mb-2"></div>
<div class="d-flex gap-2">
<button type="button" class="btn btn-success btn-sm" id="aew-save-' . $cid . '"
onclick="aewSave(\'' . $cid . '\')">
<i class="fas fa-check me-1"></i>Crear examen
</button>
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="aewClose(\'' . $cid . '\')">
Cancelar
</button>
</div>
<div id="aew-msg-' . $cid . '" class="small mt-2"></div>
</div>
</div>
</div>';
}
?>
<!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 (3 columnas para inputs cortos) ── */
@media (min-width: 480px) {
.campos-grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 3px 10px; align-items: start; }
.campos-grid .campo-full { grid-column: span 3; }
/* ponytail: :has() override — 4-col only when campo-cuarto present */
.campos-grid:has(.campo-cuarto) { grid-template-columns: 1fr 1fr 1fr 1fr; }
.campos-grid:has(.campo-cuarto) .campo-full { grid-column: span 4; }
}
.campo-edit { margin-bottom: 3px !important; }
.esquema-sep { margin: 8px 0 4px !important; }
.section-title { margin: 6px 0 4px !important; }
.section-title:first-child { margin-top: 0 !important; }
/* ── Checkboxes y radios con área táctil mayor ── */
.campo-edit .form-check {
padding-left: 2.2em;
min-height: 2.2rem;
display: flex;
align-items: center;
gap: 0;
}
/* radios en fila: override display:flex → inline-flex */
.campo-edit .d-flex .form-check {
display: inline-flex;
width: auto;
}
.campo-edit .form-check-input {
width: 1.35em;
height: 1.35em;
margin-top: 0;
margin-left: -2.2em;
cursor: pointer;
flex-shrink: 0;
}
.campo-edit .form-check-label {
font-size: 15px;
line-height: 1.4;
padding-left: 0.5em;
cursor: pointer;
}
<?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-chips { display:flex;flex-wrap:wrap;gap:.4rem;padding:.25rem 0; }
.mp-chip-grp { display:inline-flex;align-items:center; }
.mp-chip { padding:.3rem .75rem;border:1.5px solid #cbd5e1;border-radius:2rem;
background:#f8fafc;color:#475569;font-size:.82rem;cursor:pointer;transition:all .15s;line-height:1.4; }
.mp-chip.active { background:#1e3a5f;border-color:#1e3a5f;color:#fff;font-weight:600; }
.mp-chip.signed { background:#f0fdf4;border-color:#86efac;color:#166534;cursor:default; }
.mp-chip:not(.signed):not(:disabled):hover { border-color:#1e3a5f; }
.mp-chip-otro-inp { display:none;width:72px;vertical-align:middle;margin-left:.3rem; }
.mp-chip-otro-unit { vertical-align:middle;margin-left:.2rem;font-size:.8rem;color:#64748b; }
.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; }
/* ── Badges de examen seleccionado ─────────────────── */
.mp-exam-badges { margin-top:.6rem; display:flex; flex-direction:column; gap:.4rem; }
.mp-exam-badge { display:flex;align-items:center;gap:.5rem;flex-wrap:wrap;
background:#f0f9ff;border:1.5px solid #bae6fd;border-radius:8px;
padding:.35rem .7rem;font-size:.82rem; }
.mp-exam-badge.configured { background:#f0fdf4;border-color:#86efac; }
.mp-exam-badge-name { font-weight:700;color:#1e3a5f; }
.mp-exam-badge-info { color:#64748b;font-size:.78rem; }
.mp-exam-badge-btn { margin-left:auto;background:<?= htmlspecialchars($docColor) ?>;color:#fff;
border:none;border-radius:6px;padding:2px 10px;font-size:.78rem;
cursor:pointer;white-space:nowrap; }
.mp-exam-badge-btn:hover { opacity:.85; }
/* ── Tarjetas por toma ──────────────────────────────── */
:root { --mp-color: <?= htmlspecialchars($docColor) ?>; }
.toma-card { border-radius:10px;border:2px solid #e2e8f0;margin-bottom:.6rem;overflow:hidden;transition:border-color .2s,box-shadow .2s; }
.toma-card--signed { border-color:#86efac; }
.toma-card--active { border-color:var(--mp-color);box-shadow:0 0 0 3px color-mix(in srgb,var(--mp-color) 20%,transparent);animation:tc-pulse 2.5s ease-in-out infinite; }
.toma-card--locked { border-color:#e2e8f0;opacity:.6; }
@keyframes tc-pulse { 0%,100%{box-shadow:0 0 0 3px color-mix(in srgb,var(--mp-color) 15%,transparent)} 50%{box-shadow:0 0 0 6px color-mix(in srgb,var(--mp-color) 25%,transparent)} }
.toma-card-hdr { display:flex;align-items:center;gap:.45rem;padding:.5rem .85rem;font-size:.82rem;flex-wrap:wrap; }
.toma-card--signed .toma-card-hdr { background:#f0fdf4;cursor:pointer; }
.toma-card--active .toma-card-hdr { background:var(--mp-color);color:#fff; }
.toma-card--locked .toma-card-hdr { background:#f8fafc;color:#94a3b8; }
.toma-card-num { width:1.4rem;height:1.4rem;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:.7rem;font-weight:800;flex-shrink:0;background:rgba(0,0,0,.1); }
.toma-card--active .toma-card-num { background:rgba(255,255,255,.2);color:#fff; }
.toma-card--signed .toma-card-num { background:#bbf7d0;color:#166534; }
.toma-card--locked .toma-card-num { background:#e2e8f0;color:#94a3b8; }
.toma-card-lbl { font-weight:700;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap; }
.toma-card-hora { font-size:.75rem;opacity:.75;white-space:nowrap; }
.toma-card-obs { font-size:.72rem;opacity:.65;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:120px; }
.toma-card-badge { font-size:.7rem;padding:2px 8px;border-radius:20px;white-space:nowrap;flex-shrink:0; }
.toma-badge-done { background:#dcfce7;color:#166534; }
.toma-badge-active { background:rgba(255,255,255,.2);color:#fff;border:1px solid rgba(255,255,255,.35); }
.toma-badge-wait { background:#f1f5f9;color:#64748b; }
.toma-card-cd { font-size:.72rem;opacity:.9;margin-left:auto;white-space:nowrap; }
.toma-card-body { padding:.7rem .85rem;display:flex;flex-direction:column;gap:.5rem; }
.toma-card--signed .toma-card-body { display:none; }
.toma-card--signed.tc-expanded .toma-card-body { display:flex; }
.toma-card-body .esquema-sep { display:none !important; }
.toma-card-body .campo-edit,.toma-card-body .campo-full { display:block !important; }
/* ── Barra de progreso ──────────────────────────────── */
.mp-progress-wrap { margin-bottom:.75rem; }
.mp-prog-hdr { display:flex;align-items:center;justify-content:space-between;margin-bottom:.35rem; }
.mp-prog-text { font-size:.78rem;color:#475569; }
.mp-steps { display:flex;gap:.3rem;align-items:center; }
.mp-step { width:9px;height:9px;border-radius:50%;transition:all .3s; }
.mp-step--done { background:#22c55e; }
.mp-step--act { background:var(--mp-color);box-shadow:0 0 0 2px color-mix(in srgb,var(--mp-color) 30%,transparent);width:11px;height:11px; }
.mp-step--wait { background:#e2e8f0; }
.mp-prog-bar { height:5px;background:#e2e8f0;border-radius:3px;overflow:hidden; }
.mp-prog-fill { height:100%;background:linear-gradient(90deg,var(--mp-color),#22c55e);border-radius:3px;transition:width .5s ease; }
</style>
</head>
<?php $zoom = isset($_GET['zoom']) ? (float)$_GET['zoom'] : 1.0; ?>
<body<?= $zoom !== 1.0 ? ' style="zoom:' . $zoom . '"' : '' ?>>
<!-- ── 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>
<a href="<?= htmlspecialchars('https://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']) ?>"
target="_blank" rel="noopener"
style="font-size:12px;color:rgba(255,255,255,.85);text-decoration:none;border:1px solid rgba(255,255,255,.4);padding:3px 8px;border-radius:5px;white-space:nowrap"
title="Abrir en el navegador externo si la página no carga bien">
<i class="fas fa-external-link-alt me-1"></i>Abrir en navegador
</a>
<?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
// Datos para el panel de ciclos dinámico (renderizado por JS al seleccionar examen)
$_examOptions = [];
foreach ($esquema as $_oc) {
if (($_oc['id'] ?? '') === '_c8j2g16') { $_examOptions = $_oc['options'] ?? []; break; }
}
?>
<!-- Panel de ciclos — renderizado dinámicamente por JS al seleccionar tipo de examen -->
<div class="mp-cfg-overlay d-none" id="mp-cfg-overlay">
<div class="mp-cfg-card">
<div class="mp-cfg-head d-flex align-items-center justify-content-between gap-2">
<span id="mp-cfg-title"><i class="fas fa-sliders-h me-2"></i>Configurar protocolo de tomas</span>
<div class="d-flex align-items-center gap-2">
<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>
<button id="mp-cfg-cerrar" type="button"
title="Cancelar"
style="background:rgba(255,255,255,.15);border:none;color:#fff;border-radius:6px;
padding:3px 8px;font-size:1rem;cursor:pointer;line-height:1">&times;</button>
</div>
</div>
<div class="mp-cfg-body">
<p class="text-muted small mb-3">Marque las tomas que va a realizar.</p>
<div id="mp-cfg-grupos"></div>
</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>
<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 solo para registros sin datos por-campo (_svg).
$firmaSharedPaciente = $envio['firma_svg'] ?? null;
// Si datos_respuestas ya tiene algún campo _svg, no usar firma_svg global como fallback
// (evita mostrar la firma del disentimiento en el campo de consentimiento y viceversa).
$_hayDatosPerCampoSvg = !empty(array_filter(array_keys($datosCliente ?? []), fn($k) => str_ends_with($k, '_svg')));
// 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;
// Re-ordenar $esquema para que las secciones de toma aparezcan en orden cronológico
if (!empty($_mpSepOrder)) {
$__secs = []; $__cur = ['is_toma' => false, 'min' => null, 'campos' => []];
foreach ($esquema as $__c) {
if (($__c['tipo'] ?? '') === 'separador') {
if (!empty($__cur['campos'])) $__secs[] = $__cur;
$__it = isset($_mpSepOrder[$__c['id'] ?? '']);
$__cur = ['is_toma' => $__it, 'min' => $__it ? $_mpSepOrder[$__c['id']] : null, 'campos' => [$__c]];
} else {
$__cur['campos'][] = $__c;
}
}
if (!empty($__cur['campos'])) $__secs[] = $__cur;
$__tomaIdxs = []; $__tomaList = [];
foreach ($__secs as $i => $s) { if ($s['is_toma']) { $__tomaIdxs[] = $i; $__tomaList[] = $s; } }
usort($__tomaList, fn($a, $b) => $a['min'] <=> $b['min']);
foreach ($__tomaIdxs as $j => $i) { $__secs[$i] = $__tomaList[$j]; }
$esquema = array_merge(...array_column($__secs, 'campos'));
}
$_saltarSeccion = false;
$_mpSkipSec = false; // toma excluida por _tomas_config; siempre ocultar sin importar modoEditar
foreach ($esquema as $campo):
$tipo = $campo['tipo'] ?? '';
if ($tipo === 'separador'):
$_mpSkipSec = false;
$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;
$_mpSkipSec = true;
}
if (($_saltarSeccion && !$modoEditar) || $_mpSkipSec) continue; ?>
<div class="esquema-sep<?= $compact ? ' campo-full' : '' ?>" data-campo-id="<?= esc2($campo['id'] ?? '') ?>"
<?= ($_saltarSeccion && $modoEditar) ? 'style="max-height:0;opacity:0;overflow:hidden;pointer-events:none;margin:0!important;padding:0!important"' : '' ?>><?= esc2($campo['label'] ?? '') ?></div>
<?php continue; endif;
if (($_saltarSeccion && !$modoEditar) || $_mpSkipSec) 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 && !$_hayDatosPerCampoSvg ? $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 if ($isPro):
$_pn = $datosCliente['_pro_nombre'] ?? $profNombre ?? null;
$_pd = $datosCliente['_pro_cedula'] ?? $profDocumento ?? null;
if ($_pn || $_pd): ?>
<div class="firma-pro-ident" style="font-size:13px;line-height:1.5;margin-top:4px">
<?php if ($_pn): ?><strong><?= esc2($_pn) ?></strong><br><?php endif; ?>
<?php if ($_pd): ?><span style="color:#6c757d;font-size:12px">C.C. <?= esc2($_pd) ?></span><?php endif; ?>
</div>
<?php endif; endif; ?>
<?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): ?>
<!-- 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 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) > 2) ? ' campo-full' : '' ?>" data-campo-id="<?= esc2($cid) ?>">
<label><?= $label ?></label>
<div class="d-flex flex-wrap gap-3">
<?php foreach ($opts as $opt): ?>
<div class="form-check form-check-inline m-0">
<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 if ($cid === '_c8j2g16'): ?>
<?= _addExamWizardHtml($cid) ?>
<?php endif; ?>
</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>
<?php if ($cid === '_c8j2g16'): ?>
<?= _addExamWizardHtml($cid) ?>
<?php endif; ?>
</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'
};
// texto libre ocupa fila completa; tipos cortos caben 3 por fila
$inputFullClass = ($compact && $tipo === 'texto') ? ' campo-full' : '';
$extraClase = htmlspecialchars($campo['clase'] ?? '', ENT_QUOTES);
?>
<?php if ($inputType === 'time' && isset($_signedHoraCampos[$cid])): ?>
<div class="campo-edit<?= $inputFullClass ?><?= $extraClase ? ' '.$extraClase : '' ?>" data-campo-id="<?= esc2($cid) ?>">
<label><?= $label ?></label>
<div class="d-flex gap-1 align-items-center">
<input type="time" class="form-control form-control-sm" readonly
value="<?= esc2($todos[$cid] ?? '') ?>" style="background:#f1f5f9;color:#64748b;cursor:not-allowed">
<?php if ($modoTurnero && isset($_hcToFirmaIds[$cid])): ?>
<button type="button" class="btn btn-outline-warning btn-sm flex-shrink-0 btn-olvidar-toma"
data-hc="<?= esc2($cid) ?>"
data-fids="<?= esc2(json_encode($_hcToFirmaIds[$cid])) ?>"
title="Olvidar firma — permite editar este tiempo">
<i class="fas fa-undo"></i>
</button>
<?php endif; ?>
</div>
</div>
<?php else: ?>
<div class="campo-edit<?= $inputFullClass ?><?= $extraClase ? ' '.$extraClase : '' ?>" 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 endif; ?>
<?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 ($_formSoloPro && $envio['estado'] !== 'firmado'): ?>
<div class="mt-4 no-print" id="btn-completar-pro-wrap">
<button class="btn btn-success fw-semibold" id="btn-completar-pro" style="font-size:1rem;padding:.6rem 1.4rem">
<i class="fas fa-check-circle me-2"></i>Guardar y completar
</button>
<div id="btn-completar-pro-msg" class="mt-2 small"></div>
</div>
<?php elseif ($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'] === 'cerrado_anticipado'): ?>
<?php $_ca = $datosCliente['_cierre_anticipado'] ?? null; ?>
<div class="alert alert-warning mt-4 d-flex align-items-start gap-3 no-print" style="border-left:4px solid #f59e0b">
<i class="fas fa-exclamation-triangle fs-4 flex-shrink-0 mt-1 text-warning"></i>
<div>
<strong>Toma cerrada anticipadamente</strong><br>
<?php if (!empty($_ca['motivo'])): ?>
<div class="mt-1 text-secondary" style="font-size:.9rem">
<i class="fas fa-comment-alt me-1"></i><?= esc2($_ca['motivo']) ?>
</div>
<?php endif; ?>
<button onclick="window.close()" class="btn btn-outline-secondary btn-sm fw-semibold mt-2">
<i class="fas fa-times me-1"></i>Cerrar ventana
</button>
</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; ?>
<?php if ($modoTurnero && $embebido && !empty($_mpMap) && $envio['estado'] === 'en_progreso'): ?>
<div class="mt-2 no-print text-end">
<button class="btn btn-outline-primary btn-sm" id="btn-guardar-tiempos">
<i class="fas fa-save me-1"></i>Guardar tiempos
</button>
</div>
<?php endif; ?>
<?php if ($modoTurnero && $embebido && !empty($_mpMap) && !in_array($envio['estado'], ['firmado','cerrado_anticipado'])): ?>
<div class="mt-3 no-print text-end">
<button class="btn btn-outline-danger btn-sm" id="btn-cerrar-anticip">
<i class="fas fa-times-circle me-1"></i>Cerrar anticipadamente
</button>
</div>
<!-- Modal cierre anticipado -->
<div id="modal-cerrar-anticip" style="position:fixed;inset:0;z-index:9999;background:rgba(0,0,0,.5);display:none;align-items:center;justify-content:center">
<div style="background:#fff;border-radius:12px;padding:1.5rem;max-width:380px;width:94%;box-shadow:0 8px 32px rgba(0,0,0,.25)">
<h6 class="fw-bold mb-2"><i class="fas fa-exclamation-triangle text-warning me-2"></i>Cerrar anticipadamente</h6>
<p class="text-muted small mb-3">Explique por qué no se completaron todas las tomas. Este mensaje quedará visible en toma de muestras y recepción.</p>
<textarea id="mca-motivo" rows="3" class="form-control mb-2" placeholder="Ej: El paciente no toleró más extracciones…"></textarea>
<div id="mca-error" class="text-danger small mb-2 d-none"></div>
<div class="d-flex gap-2 justify-content-end">
<button class="btn btn-outline-secondary btn-sm" id="mca-cancelar">Cancelar</button>
<button class="btn btn-danger btn-sm fw-semibold" id="mca-confirmar">
<i class="fas fa-times-circle me-1"></i>Confirmar cierre
</button>
</div>
</div>
</div>
<?php endif; ?>
</div><!-- /doc-body -->
<div class="doc-footer">
<span>
<?php if ($docPiePagina): ?>
<?= esc2($docPiePagina) ?> &nbsp;&bull;&nbsp;
<?php endif; ?>
<i class="fas fa-shield-alt me-1"></i>Generado el <?= date('d/m/Y H:i') ?> &nbsp;&bull;&nbsp; ID #<?= $envio['id'] ?>
<?php if ($numeroOrden): ?>
&nbsp;&bull;&nbsp; Orden: <strong><?= esc2($numeroOrden) ?></strong>
<?php endif; ?>
</span>
<?php if ($envio['hash_verificacion']): ?>
<span class="hash-short" title="Hash SHA-256"><?= substr($envio['hash_verificacion'],0,16) ?>...</span>
<?php endif; ?>
</div>
</div><!-- /doc-wrap -->
<script>
<?php if (!empty($_mpMap)): ?>
/* ── Panel de ciclos dinámico ── */
var _mpAllGroups = <?= json_encode($_mpGroupsFull, JSON_UNESCAPED_UNICODE) ?>;
var _mpExamOptions = <?= json_encode($_examOptions, JSON_UNESCAPED_UNICODE) ?>;
var _mpTomasConfig = <?= json_encode($_tomasConfig, JSON_UNESCAPED_UNICODE) ?>;
var _mpCfgToken = <?= json_encode($tokenTurnero, JSON_UNESCAPED_UNICODE) ?>;
var _mpModoTurnero = <?= json_encode($modoTurnero && $embebido) ?>;
var _mpOtroHora = <?= json_encode($datosCliente['_otro_hora'] ?? null, JSON_UNESCAPED_UNICODE) ?>;
function _mpGuardarConfig(config, examType, onOk, onErr, extra) {
var base = window.location.href.split('/ver_formulario_enviado.php')[0];
var allExams = Array.from(document.querySelectorAll('[name="_c8j2g16[]"]:checked,[name="_c8j2g16"]:checked')).map(function(el){ return el.value; });
var payload = Object.assign({ token: _mpCfgToken, tomas_config: config, exam_type: examType, exam_types: allExams }, extra || {});
fetch(base + '/modules/turnero/api/configurar_tomas.php', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
})
.then(function(r) { return r.json(); })
.then(function(d) {
if (d.ok) { try { sessionStorage.setItem('_mpExamRestore', allExams.join('||') || examType || ''); } catch(e) {} onOk(); }
else onErr(d.error || 'Error desconocido');
})
.catch(function() { onErr('Error de conexión.'); });
}
function _mpChipLabel(label) {
var m = label.match(/Minuto\s+(\d+)/i);
if (m) return m[1];
var pm = label.match(/(\d{1,2}):\d{2}\s*p\.?m/i);
if (pm) return pm[1] + 'pm';
if (/otro/i.test(label)) return 'Otro';
return label.replace(/^Toma\s*·\s*/i, '').trim();
}
var _mpActiveConfigExam = '';
function _mpShowCfgPanel(examName, groups, isModify) {
_mpActiveConfigExam = examName;
var savedFirmas = window._mpSavedFirmas || {};
var html = '';
var multiGrupo = Object.keys(groups).length > 1;
for (var gk in groups) {
var secs = groups[gk];
html += '<div class="mb-3">'
+ (multiGrupo ? '<div class="mp-cfg-gtitle">' + (secs[0] ? (secs[0].display || gk) : gk) + '</div>' : '')
+ '<div class="mp-chips">';
for (var i = 0; i < secs.length; i++) {
var s = secs[i];
var isSigned = !!savedFirmas[s.firma];
var _examCfg = _mpTomasConfig && _mpTomasConfig[examName];
var inSaved = _examCfg && _examCfg[gk] && _examCfg[gk].indexOf(s.firma) !== -1;
var isMin0 = _mpChipLabel(s.label || '') === '0';
var isActive = isSigned || inSaved || (!isModify && isMin0);
var isOtro = /otro/i.test(s.label || '');
var chipLbl = _mpChipLabel(s.label || ('Toma ' + (i+1)));
// orden flex: numérico para Minuto X, valor guardado para Otro, 9999 si sin valor
var chipOrder = isOtro
? ((_mpOtroHora && isActive) ? parseInt(_mpOtroHora) : 9999)
: (parseInt(chipLbl) === chipLbl >> 0 ? parseInt(chipLbl) : i * 10);
html += '<span class="mp-chip-grp"' + (isOtro ? ' data-otro-grp="1"' : '') + ' style="order:' + chipOrder + '">';
// checkbox oculto — sigue siendo la fuente de verdad para el guardado
html += '<input type="checkbox" class="mp-cfg-chk d-none" id="cfg-' + s.firma + '"'
+ ' value="' + s.firma + '" data-group="' + gk + '"'
+ (isActive ? ' checked' : '') + (isSigned ? ' disabled' : '') + '>';
html += '<button type="button" class="mp-chip' + (isActive ? ' active' : '') + (isSigned ? ' signed' : '') + '"'
+ ' data-target="cfg-' + s.firma + '"' + (isOtro ? ' data-otro="1"' : '')
+ (isSigned ? ' disabled title="Ya firmada"' : '') + '>'
+ chipLbl + (isSigned ? ' <i class="fas fa-check ms-1 small"></i>' : '') + '</button>';
if (isOtro) {
var otroVal = (isActive && _mpOtroHora) ? _mpOtroHora : '';
html += '<input type="number" class="form-control form-control-sm mp-chip-otro-inp"'
+ ' placeholder="min" min="1" step="1"'
+ (otroVal ? ' value="' + otroVal + '"' : '')
+ (isActive ? ' style="display:inline-block"' : '') + '>'
+ '<span class="mp-chip-otro-unit' + (isActive ? '' : ' d-none') + '">min</span>';
}
html += '</span>';
}
html += '</div></div>';
}
var grupos = document.getElementById('mp-cfg-grupos');
grupos.innerHTML = html;
document.getElementById('mp-cfg-title').innerHTML = '<i class="fas fa-sliders-h me-2"></i>' + examName;
document.getElementById('mp-cfg-overlay').classList.remove('d-none');
}
// Click en chips de tiempo (delegado al documento para evitar múltiples listeners)
document.addEventListener('click', function(e) {
var chip = e.target.closest('.mp-chip');
if (!chip || chip.disabled || !chip.closest('#mp-cfg-grupos')) return;
var chk = document.getElementById(chip.getAttribute('data-target'));
if (!chk || chk.disabled) return;
chk.checked = !chk.checked;
chip.classList.toggle('active', chk.checked);
if (chip.dataset.otro) {
var grp = chip.closest('.mp-chip-grp');
var inp = grp && grp.querySelector('.mp-chip-otro-inp');
var unit = grp && grp.querySelector('.mp-chip-otro-unit');
if (inp) {
inp.style.display = chk.checked ? 'inline-block' : 'none';
if (unit) unit.classList.toggle('d-none', !chk.checked);
if (chk.checked) setTimeout(function() { inp.focus(); }, 50);
}
}
});
// Reposicionar chip "Otro" en tiempo real según el minuto digitado
document.addEventListener('input', function(e) {
if (!e.target.classList.contains('mp-chip-otro-inp')) return;
var grp = e.target.closest('.mp-chip-grp');
if (!grp) return;
var v = parseInt(e.target.value);
grp.style.order = (v >= 1) ? v : 9999;
});
function _mpGetGroupsForExam(examName) {
var result = {};
for (var gk in _mpAllGroups) {
var parts = gk.split('|').filter(Boolean);
var isCond = parts.some(function(p) { return _mpExamOptions.indexOf(p) !== -1; });
if (!isCond || parts.indexOf(examName) !== -1) {
result[gk] = _mpAllGroups[gk];
}
}
return result;
}
// Visibilidad de secciones de tomas según examen seleccionado.
// Oculta TODOS los sep_ids de _mpAllGroups por defecto;
// muestra solo los del grupo cuya clave coincide con el examen.
function _mpApplyExamVisibility(selectedExam) {
// 1. Todos los sep_ids que son secciones de toma
var allTomaSeps = {};
for (var gk in _mpAllGroups) {
_mpAllGroups[gk].forEach(function(s) { if (s.sep_id) allTomaSeps[s.sep_id] = gk; });
}
if (!Object.keys(allTomaSeps).length) return;
// 2. Sep_ids del examen seleccionado
var examSeps = {};
if (selectedExam) {
var groups = _mpGetGroupsForExam(selectedExam);
for (var gk2 in groups) {
groups[gk2].forEach(function(s) { if (s.sep_id) examSeps[s.sep_id] = true; });
}
}
// 3. Recorrer hijos directos del contenedor de campos (.campos-grid)
var container = document.querySelector('.campos-grid') || document.querySelector('.doc-body');
if (!container) return;
var inHidden = false;
Array.from(container.children).forEach(function(child) {
if (child.classList.contains('esquema-sep')) {
var cid = child.getAttribute('data-campo-id') || '';
inHidden = allTomaSeps[cid] !== undefined ? !examSeps[cid] : false;
child.style.display = inHidden ? 'none' : '';
} else {
child.style.display = inHidden ? 'none' : '';
}
});
}
// Helper: leer examen seleccionado (funciona para radio name=_c8j2g16 y checkbox name=_c8j2g16[])
function _mpGetExamName() {
var el = document.querySelector('[name="_c8j2g16"]:checked,[name="_c8j2g16[]"]:checked');
return el ? el.value : null;
}
function _mpSetExamChecked(name) {
var esc = name.replace(/"/g, '\\"');
var el = document.querySelector('[name="_c8j2g16"][value="' + esc + '"],[name="_c8j2g16[]"][value="' + esc + '"]');
if (el) el.checked = true;
}
// ── Badges de estado de examen ───────────────────────────────────────
function _mpRenderBadges() {
var wrap = document.getElementById('mp-exam-badges');
if (!wrap) return;
var checked = Array.from(document.querySelectorAll('[name="_c8j2g16[]"]:checked,[name="_c8j2g16"]:checked'));
if (!checked.length) { wrap.innerHTML = ''; return; }
var html = '';
checked.forEach(function(el) {
var name = el.value;
var groups = _mpGetGroupsForExam(name);
var totalTomas = Object.values(groups).reduce(function(s, g) { return s + g.length; }, 0);
var groupKeys = Object.keys(groups);
// Verificar si este examen ya está configurado en _mpTomasConfig (formato anidado por examen)
var _examConf = _mpTomasConfig && _mpTomasConfig[name];
var configured = !!_examConf && groupKeys.every(function(k) { return _examConf[k]; });
var tomasCount = 0;
if (configured) {
groupKeys.forEach(function(k) { if (_examConf[k]) tomasCount += _examConf[k].length; });
}
html += '<div class="mp-exam-badge' + (configured ? ' configured' : '') + '">'
+ '<span class="mp-exam-badge-name">' + name + '</span>';
if (configured) {
html += '<span class="mp-exam-badge-info"><i class="fas fa-check-circle text-success me-1"></i>'
+ tomasCount + ' toma' + (tomasCount !== 1 ? 's' : '') + ' configurada' + (tomasCount !== 1 ? 's' : '') + '</span>'
+ '<button class="mp-exam-badge-btn" data-exam="' + name.replace(/"/g, '&quot;') + '" data-modify="1">'
+ '<i class="fas fa-edit me-1"></i>Modificar</button>';
} else {
html += '<span class="mp-exam-badge-info text-warning"><i class="fas fa-clock me-1"></i>Pendiente configurar ('
+ totalTomas + ' tomas disponibles)</span>'
+ '<button class="mp-exam-badge-btn" data-exam="' + name.replace(/"/g, '&quot;') + '">'
+ '<i class="fas fa-sliders-h me-1"></i>Configurar tiempos</button>';
}
html += '</div>';
});
wrap.innerHTML = html;
}
// Escuchar selección de tipo de examen
document.addEventListener('change', function(e) {
var el = e.target;
if (el.name.replace(/\[\]$/, '') !== '_c8j2g16') return;
var examName = _mpGetExamName();
_mpApplyExamVisibility(examName);
if (_mpModoTurnero) _mpRenderBadges();
});
// Click en botón "Configurar tiempos" o "Modificar" del badge
document.addEventListener('click', function(e) {
var btn = e.target.closest('.mp-exam-badge-btn');
if (!btn) return;
var examName = btn.getAttribute('data-exam');
var isModify = btn.getAttribute('data-modify') === '1';
var groups = _mpGetGroupsForExam(examName);
if (Object.keys(groups).length) _mpShowCfgPanel(examName, groups, isModify);
});
// Al cargar: aplicar visibilidad y renderizar badges
document.addEventListener('DOMContentLoaded', function() {
var examName = _mpGetExamName();
// Restaurar selección de examen(es) tras el reload del panel de config
try {
var saved = sessionStorage.getItem('_mpExamRestore');
if (saved) {
sessionStorage.removeItem('_mpExamRestore');
saved.split('||').filter(Boolean).forEach(function(n) { _mpSetExamChecked(n); });
if (!examName) examName = saved.split('||').filter(Boolean)[0] || '';
}
} catch(e) {}
_mpApplyExamVisibility(examName);
if (_mpModoTurnero) {
_mpRenderBadges();
// Auto-abrir solo si hay un examen sin configurar (nuevo). Si todos configurados, no abrir.
var _chkd = Array.from(document.querySelectorAll('[name="_c8j2g16[]"]:checked,[name="_c8j2g16"]:checked'));
var _autoT = null;
for (var _i = 0; _i < _chkd.length && !_autoT; _i++) {
var _n = _chkd[_i].value, _g = _mpGetGroupsForExam(_n);
if (!Object.keys(_g).length) continue;
var _eCfg = _mpTomasConfig && _mpTomasConfig[_n];
var _conf = !!_eCfg && Object.keys(_g).every(function(k){ return _eCfg[k]; });
if (!_conf) _autoT = { name: _n, groups: _g, isModify: false };
}
if (_autoT) _mpShowCfgPanel(_autoT.name, _autoT.groups, _autoT.isModify);
}
});
// Botón "Usar todas"
document.getElementById('mp-cfg-todas').addEventListener('click', function() {
var examName = _mpActiveConfigExam || _mpGetExamName() || '';
var cfg = {};
document.querySelectorAll('.mp-cfg-chk').forEach(function(chk) {
if (!cfg[chk.dataset.group]) cfg[chk.dataset.group] = [];
cfg[chk.dataset.group].push(chk.value);
});
var btn = this;
btn.disabled = true; btn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Guardando...';
_mpGuardarConfig(cfg, examName, function() { location.reload(); }, function(e) {
document.getElementById('mp-cfg-msg').innerHTML = '<span class="text-danger">' + e + '</span>';
btn.disabled = false; btn.innerHTML = '<i class="fas fa-forward me-1"></i>Usar todas';
});
});
// Botón "Iniciar protocolo"
document.getElementById('mp-cfg-iniciar').addEventListener('click', function() {
var examName = _mpActiveConfigExam || _mpGetExamName() || '';
var byGroup = {};
document.querySelectorAll('.mp-cfg-chk').forEach(function(chk) {
if (!byGroup[chk.dataset.group]) byGroup[chk.dataset.group] = [];
if (chk.checked) byGroup[chk.dataset.group].push(chk.value);
});
for (var g in byGroup) {
if (!byGroup[g].length) {
document.getElementById('mp-cfg-msg').innerHTML =
'<span class="text-danger"><i class="fas fa-exclamation-circle me-1"></i>Seleccione al menos una toma por protocolo.</span>';
return;
}
}
// Validar que "Otro" tenga minutos ingresados
var otroHora = '';
var otroBtn = document.querySelector('#mp-cfg-grupos .mp-chip[data-otro="1"]');
if (otroBtn) {
var otroChk = document.getElementById(otroBtn.getAttribute('data-target'));
if (otroChk && otroChk.checked) {
var otroInp = otroBtn.parentElement.querySelector('.mp-chip-otro-inp');
if (otroInp) otroHora = otroInp.value.trim();
if (!otroHora || isNaN(parseInt(otroHora)) || parseInt(otroHora) < 1) {
document.getElementById('mp-cfg-msg').innerHTML =
'<span class="text-danger"><i class="fas fa-exclamation-circle me-1"></i>Ingrese los minutos para la toma "Otro" (ej: 2).</span>';
return;
}
}
}
var btn = this;
btn.disabled = true; btn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Guardando...';
_mpGuardarConfig(byGroup, examName, function() { location.reload(); }, function(e) {
document.getElementById('mp-cfg-msg').innerHTML = '<span class="text-danger">' + e + '</span>';
btn.disabled = false; btn.innerHTML = '<i class="fas fa-play-circle me-2"></i>Iniciar protocolo';
}, otroHora ? { otro_hora: otroHora } : {});
});
// Botón cerrar panel de config
document.getElementById('mp-cfg-cerrar').addEventListener('click', function() {
document.getElementById('mp-cfg-overlay').classList.add('d-none');
document.querySelectorAll('[name="_c8j2g16[]"]').forEach(function(el) { el.checked = false; });
_mpApplyExamVisibility(null);
});
/* ── Wizard: añadir tipo de examen ── */
var _aewState = {}; // {cid: {count, rows}}
function aewOpen(cid) {
var box = document.getElementById('aew-box-' + cid);
if (!box) return;
box.classList.remove('d-none');
_aewState[cid] = { count: 1 };
aewRenderTomas(cid);
var ni = document.getElementById('aew-name-' + cid);
if (ni) ni.focus();
}
function aewClose(cid) {
var box = document.getElementById('aew-box-' + cid);
if (box) box.classList.add('d-none');
}
function aewAddToma(cid, delta) {
if (!_aewState[cid]) return;
var n = (_aewState[cid].count || 1) + delta;
if (n < 1) n = 1; if (n > 12) n = 12;
_aewState[cid].count = n;
var el = document.getElementById('aew-count-' + cid);
if (el) el.textContent = n;
aewRenderTomas(cid);
}
function aewRenderTomas(cid) {
var n = (_aewState[cid] || {}).count || 1;
var cnt = document.getElementById('aew-tomas-' + cid);
if (!cnt) return;
// Guardar valores actuales antes de re-renderizar
var prev = [];
cnt.querySelectorAll('.aew-row').forEach(function(r) {
prev.push({
lbl: r.querySelector('.aew-lbl') ? r.querySelector('.aew-lbl').value : '',
tipo: r.querySelector('.aew-tipo') ? r.querySelector('.aew-tipo').value : 'minutos',
val: r.querySelector('.aew-val') ? r.querySelector('.aew-val').value : '',
});
});
var html = '';
for (var i = 0; i < n; i++) {
var p = prev[i] || {};
var lbl = p.lbl || (i === 0 ? 'Minuto 0' : 'Minuto ' + (i * 30));
var tipo = p.tipo || 'minutos';
var val = p.val !== undefined ? p.val : (i === 0 ? '0' : String(i * 30));
html += '<div class="aew-row border rounded p-2 mb-1 d-flex gap-2 align-items-end flex-wrap">'
+ '<div style="flex:2;min-width:120px"><label class="form-label small mb-1">Label toma ' + (i+1) + '</label>'
+ '<input class="form-control form-control-sm aew-lbl" value="' + lbl.replace(/"/g,'&quot;') + '" placeholder="Ej: Minuto 0"></div>'
+ '<div style="flex:1;min-width:100px"><label class="form-label small mb-1">Tipo</label>'
+ '<select class="form-select form-select-sm aew-tipo" onchange="aewTipoChange(this)">'
+ '<option value="minutos"' + (tipo==='minutos'?' selected':'') + '>Minutos</option>'
+ '<option value="hora_fija"' + (tipo==='hora_fija'?' selected':'') + '>Hora fija</option>'
+ '</select></div>'
+ '<div style="flex:1;min-width:80px"><label class="form-label small mb-1 aew-val-lbl">' + (tipo==='hora_fija'?'HH:MM':'Minutos') + '</label>'
+ '<input class="form-control form-control-sm aew-val" value="' + String(val).replace(/"/g,'&quot;') + '" placeholder="' + (tipo==='hora_fija'?'08:00':'0') + '"></div>'
+ '</div>';
}
cnt.innerHTML = html;
}
function aewTipoChange(sel) {
var row = sel.closest('.aew-row');
var lbl = row.querySelector('.aew-val-lbl');
var inp = row.querySelector('.aew-val');
if (sel.value === 'hora_fija') { lbl.textContent = 'HH:MM'; inp.placeholder = '08:00'; }
else { lbl.textContent = 'Minutos'; inp.placeholder = '0'; }
}
function aewSave(cid) {
var name = (document.getElementById('aew-name-' + cid) || {}).value;
if (name) name = name.trim();
var msg = document.getElementById('aew-msg-' + cid);
function showErr(e) { if (msg) msg.innerHTML = '<span class="text-danger"><i class="fas fa-exclamation-circle me-1"></i>' + e + '</span>'; }
if (!name) { showErr('Ingrese el nombre del examen.'); return; }
var tomas = [];
document.querySelectorAll('#aew-tomas-' + cid + ' .aew-row').forEach(function(row) {
tomas.push({
label: (row.querySelector('.aew-lbl') || {}).value || '',
tipo: (row.querySelector('.aew-tipo') || {}).value || 'minutos',
valor: (row.querySelector('.aew-val') || {}).value || '0',
});
});
if (!tomas.length) { showErr('Configure al menos una toma.'); return; }
for (var i = 0; i < tomas.length; i++) {
if (!tomas[i].label.trim()) { showErr('Ingrese el label de la toma ' + (i+1) + '.'); return; }
}
var btn = document.getElementById('aew-save-' + cid);
if (btn) { btn.disabled = true; btn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Guardando…'; }
if (msg) msg.innerHTML = '';
var base = window.location.href.split('/ver_formulario_enviado.php')[0];
fetch(base + '/api/lab/save_tomas_config.php', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ action: 'add_exam', exam_name: name, tomas: tomas })
})
.then(function(r) { return r.json(); })
.then(function(d) {
if (d.ok) {
if (msg) msg.innerHTML = '<span class="text-success"><i class="fas fa-check me-1"></i>Examen creado. Recargando…</span>';
// Guardar selección en sessionStorage para auto-seleccionar al recargar
try { sessionStorage.setItem('_aew_preselect', name); } catch(e) {}
setTimeout(function() { location.reload(); }, 900);
} else {
showErr(d.error || 'Error desconocido');
if (btn) { btn.disabled = false; btn.innerHTML = '<i class="fas fa-check me-1"></i>Crear examen'; }
}
})
.catch(function() {
showErr('Error de conexión.');
if (btn) { btn.disabled = false; btn.innerHTML = '<i class="fas fa-check me-1"></i>Crear examen'; }
});
}
// Auto-seleccionar el examen recién creado tras recargar
(function() {
try {
var pre = sessionStorage.getItem('_aew_preselect');
if (!pre) return;
sessionStorage.removeItem('_aew_preselect');
_mpSetExamChecked(pre);
var el = document.querySelector('[name="_c8j2g16"]:checked,[name="_c8j2g16[]"]:checked');
if (el) el.dispatchEvent(new Event('change', {bubbles:true}));
} catch(e) {}
})();
window._muestrasMap = <?= json_encode($_mpMap, JSON_UNESCAPED_UNICODE) ?>;
window._mpSavedFirmas = {};
window._mpTomaData = {};
window._mpPrimeraPendiente = <?= json_encode($_mpPrimeraPendiente) ?>;
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);
}
// ── Tarjetas de toma ─────────────────────────────────────────────────
function _mpBuildSepFirmaMap() {
var m = {};
for (var gk in _mpAllGroups) {
(_mpAllGroups[gk] || []).forEach(function(s) {
if (s.sep_id && s.firma && window._muestrasMap[s.firma]) m[s.sep_id] = s.firma;
});
}
return m;
}
function _mpEffectiveLabel(label) {
return (/otro/i.test(label) && _mpOtroHora) ? label + ' - ' + _mpOtroHora : label;
}
function _mpCardHdrHtml(fid, num, label, state) {
var badge = state === 'signed'
? '<span class="toma-card-badge toma-badge-done"><i class="fas fa-check-circle me-1"></i>Firmado</span>'
: state === 'active'
? '<span class="toma-card-badge toma-badge-active"><i class="fas fa-pen me-1"></i>Firmar ahora</span>'
: '<span class="toma-card-badge toma-badge-wait"><i class="fas fa-hourglass-half me-1"></i>En espera</span>';
var extra = state === 'locked' ? '<span class="toma-card-cd" id="tc-cd-' + fid + '"></span>' : '';
var hora = state === 'signed' ? '<span class="toma-card-hora"></span>' : '';
var obs = state === 'signed' ? '<span class="toma-card-obs" id="tc-obs-' + fid + '"></span>' : '';
return '<span class="toma-card-num">' + num + '</span>'
+ '<span class="toma-card-lbl">' + label + '</span>'
+ hora + obs + extra + badge;
}
function _mpRenderCards() {
if (!window._muestrasMap || !Object.keys(window._muestrasMap).length) return;
var container = document.querySelector('.campos-grid');
if (!container || container.querySelector('.toma-card')) return;
var sepToFirma = _mpBuildSepFirmaMap();
if (!Object.keys(sepToFirma).length) return;
var nodes = Array.from(container.childNodes);
// Segment by toma separators
var segs = [], cur = null;
nodes.forEach(function(node) {
if (node.nodeType !== 1) return;
var sid = node.getAttribute('data-campo-id') || '';
if (sid && sepToFirma[sid]) { cur = { fid: sepToFirma[sid], nodes: [node] }; segs.push(cur); }
else if (cur) { cur.nodes.push(node); }
});
if (!segs.length) return;
// Pre-toma nodes
var firstNode = segs[0].nodes[0], preNodes = [];
for (var i = 0; i < nodes.length; i++) {
if (nodes[i] === firstNode) break;
if (nodes[i].nodeType === 1) preNodes.push(nodes[i]);
}
container.innerHTML = '';
preNodes.forEach(function(n) { container.appendChild(n); });
// Progress placeholder
var progDiv = document.createElement('div');
progDiv.id = 'mp-progress'; progDiv.className = 'mp-progress-wrap campo-full';
container.appendChild(progDiv);
var signedCount = 0;
segs.forEach(function(seg, idx) {
var fid = seg.fid, entry = window._muestrasMap[fid] || {};
// State: signed if firma-box img exists OR saved this session; active if first pending
var hasImg = seg.nodes.some(function(n) { return n.querySelector && !!n.querySelector('.firma-box img'); });
var isSigned = hasImg || !!((window._mpSavedFirmas || {})[fid]);
if (!isSigned) {
// Check if fpw widget exists at all (if not → signed pre-load, edge case)
var hasFpw = seg.nodes.some(function(n) {
return n.id === ('fpw-' + fid) || !!(n.querySelector && n.querySelector('#fpw-' + fid));
});
if (!hasFpw) isSigned = true;
}
var isActive = !isSigned && fid === window._mpPrimeraPendiente;
var state = isSigned ? 'signed' : isActive ? 'active' : 'locked';
if (isSigned) signedCount++;
var card = document.createElement('div');
card.className = 'toma-card toma-card--' + state + ' campo-full';
card.id = 'tc-' + fid;
card.setAttribute('data-firma', fid);
var hdr = document.createElement('div');
hdr.className = 'toma-card-hdr';
hdr.innerHTML = _mpCardHdrHtml(fid, idx + 1, _mpEffectiveLabel(entry.label || ''), state);
if (isSigned) {
// Fill hora in header
var horaEl = null;
if (entry.hora_campo) seg.nodes.forEach(function(n) { if (!horaEl) horaEl = n.querySelector ? n.querySelector('[name="' + entry.hora_campo + '"]') : null; });
if (horaEl && horaEl.value) { var hs = hdr.querySelector('.toma-card-hora'); if (hs) hs.textContent = horaEl.value; }
// Fill obs in header
if (entry.obs_campo) {
var obsEl = null;
seg.nodes.forEach(function(n) { if (!obsEl && n.querySelector) obsEl = n.querySelector('[name="' + entry.obs_campo + '"]'); });
if (obsEl && obsEl.value) { var os = hdr.querySelector('.toma-card-obs'); if (os) os.textContent = obsEl.value; }
}
hdr.addEventListener('click', function() { card.classList.toggle('tc-expanded'); });
}
var body = document.createElement('div');
body.className = 'toma-card-body';
seg.nodes.forEach(function(n) { body.appendChild(n); });
card.appendChild(hdr); card.appendChild(body);
container.appendChild(card);
});
_mpUpdateProgress(signedCount, segs.map(function(s) { return s.fid; }));
}
function _mpUpdateProgress(signedCount, firmaIds) {
var wrap = document.getElementById('mp-progress');
if (!wrap) return;
var total = firmaIds.length;
var pct = total ? Math.round(signedCount / total * 100) : 0;
var steps = firmaIds.map(function(fid) {
var isSigned = !!((window._mpSavedFirmas || {})[fid])
|| (function(){ var c=document.getElementById('tc-'+fid); return c&&c.classList.contains('toma-card--signed'); }());
var isActive = fid === window._mpPrimeraPendiente && !isSigned;
var cls = isSigned ? 'done' : isActive ? 'act' : 'wait';
var lbl = _mpEffectiveLabel(((window._muestrasMap || {})[fid] || {}).label || '');
return '<span class="mp-step mp-step--' + cls + '" title="' + lbl.replace(/"/g,'&quot;') + '"></span>';
}).join('');
wrap.innerHTML = '<div class="mp-prog-hdr"><span class="mp-prog-text">Toma <strong>' + signedCount + '</strong> de <strong>' + total + '</strong></span><div class="mp-steps">' + steps + '</div></div>'
+ '<div class="mp-prog-bar"><div class="mp-prog-fill" style="width:' + pct + '%"></div></div>';
}
function _mpRefreshCards(signedFid, nextFid) {
window._mpPrimeraPendiente = nextFid || null;
// Update signed card
var sc = document.getElementById('tc-' + signedFid);
if (sc) {
sc.classList.remove('toma-card--active'); sc.classList.add('toma-card--signed');
var shdr = sc.querySelector('.toma-card-hdr');
if (shdr) {
var se = window._muestrasMap[signedFid] || {}, snum = (sc.querySelector('.toma-card-num') || {}).textContent || '1';
shdr.innerHTML = _mpCardHdrHtml(signedFid, parseInt(snum), _mpEffectiveLabel(se.label || ''), 'signed');
if (se.hora_campo) { var horaEl = sc.querySelector('[name="' + se.hora_campo + '"]'); if (horaEl && horaEl.value) { var hs = shdr.querySelector('.toma-card-hora'); if (hs) hs.textContent = horaEl.value; } }
if (se.obs_campo) { var obsEl = sc.querySelector('[name="' + se.obs_campo + '"]'); if (obsEl && obsEl.value) { var os = shdr.querySelector('.toma-card-obs'); if (os) os.textContent = obsEl.value; } }
shdr.addEventListener('click', function() { sc.classList.toggle('tc-expanded'); });
}
}
// Update next card
if (nextFid) {
var nc = document.getElementById('tc-' + nextFid);
if (nc) {
nc.classList.remove('toma-card--locked'); nc.classList.add('toma-card--active');
var nhdr = nc.querySelector('.toma-card-hdr');
if (nhdr) { var ne = window._muestrasMap[nextFid] || {}, nnum = (nc.querySelector('.toma-card-num') || {}).textContent || '1'; nhdr.innerHTML = _mpCardHdrHtml(nextFid, parseInt(nnum), _mpEffectiveLabel(ne.label || ''), 'active'); }
setTimeout(function() { nc.scrollIntoView({ behavior:'smooth', block:'center' }); }, 150);
}
}
// Refresh progress
var cards = document.querySelectorAll('.toma-card');
var fids = [], signed = 0;
cards.forEach(function(c) { var fid = c.getAttribute('data-firma'); if (fid) { fids.push(fid); if (c.classList.contains('toma-card--signed')) signed++; } });
_mpUpdateProgress(signed, fids);
}
// Auto-scroll y auto-fill hora a la primera firma pendiente al cargar
document.addEventListener('DOMContentLoaded', function() {
_mpRenderCards();
setTimeout(function() {
var fw = document.querySelector('.firma-pro-widget:not([style*="display:none"])');
if (!fw) return;
// Siempre auto-llenar la hora (incluso en Minuto 0 sin firmas previas)
_mpAutoFillHora(fw.dataset.campo);
// Solo hacer scroll si ya hay alguna firma iniciada
var hayFirmaIniciada = Object.keys(window._mpSavedFirmas || {}).length > 0
|| !!document.querySelector('.firma-box img');
if (hayFirmaIniciada) {
fw.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}, 350);
});
<?php endif; ?>
window._fpwPreFirma = <?= json_encode($firmaProfPreguardada) ?>;
window._profNombre = <?= json_encode($profNombre) ?>;
window._profDocumento = <?= json_encode($profDocumento) ?>;
<?php if (!empty($_mpSiguienteTomAt)): ?>
// Fix 1: recuperar countdown si el modal fue cerrado y reabierto mientras una toma estaba en curso
(function() {
var targetMs = new Date('<?= str_replace(' ', 'T', $_mpSiguienteTomAt) ?>').getTime();
if (targetMs <= Date.now()) return;
var pendingId = <?= json_encode($_mpPrimeraPendiente) ?>;
var entry = window._muestrasMap && window._muestrasMap[pendingId];
document.addEventListener('DOMContentLoaded', function() {
_mpIniciarCountdown(targetMs, entry ? _mpEffectiveLabel(entry.label || '') : null, pendingId);
});
})();
<?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;
});
}
if (window._profNombre) datosRespuestas['_pro_nombre'] = window._profNombre;
if (window._profDocumento) datosRespuestas['_pro_cedula'] = window._profDocumento;
const payload = isTurnero
? { turno_id: parseInt(turnoId), formulario_id: parseInt(formularioId), svg,
datos_respuestas: datosRespuestas,
...(soloPro ? { solo_profesional: true } : {}) }
: { 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);
var wrap = document.createElement('div');
wrap.appendChild(box);
if (window._profNombre || window._profDocumento) {
var ident = document.createElement('div');
ident.className = 'firma-pro-ident';
ident.style.cssText = 'font-size:13px;line-height:1.5;margin-top:4px';
if (window._profNombre) { var s = document.createElement('strong'); s.textContent = window._profNombre; ident.appendChild(s); ident.appendChild(document.createElement('br')); }
if (window._profDocumento) { var sp = document.createElement('span'); sp.style.cssText = 'color:#6c757d;font-size:12px'; sp.textContent = 'C.C. ' + window._profDocumento; ident.appendChild(sp); }
wrap.appendChild(ident);
}
widget.replaceWith(wrap);
}
} 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;
// Fix 5: cachear hora en el momento de guardar para el resumen final
(window._mpTomaData = window._mpTomaData||{})[campoId] = {
hora: entry.hora_campo ? (dr[entry.hora_campo] || '') : ''
};
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);
var wrap = document.createElement('div');
wrap.appendChild(box);
if (window._profNombre || window._profDocumento) {
var ident = document.createElement('div');
ident.className = 'firma-pro-ident';
ident.style.cssText = 'font-size:13px;line-height:1.5;margin-top:4px';
if (window._profNombre) { var s = document.createElement('strong'); s.textContent = window._profNombre; ident.appendChild(s); ident.appendChild(document.createElement('br')); }
if (window._profDocumento) { var sp = document.createElement('span'); sp.style.cssText = 'color:#6c757d;font-size:12px'; sp.textContent = 'C.C. ' + window._profDocumento; ident.appendChild(sp); }
wrap.appendChild(ident);
}
widget.replaceWith(wrap);
// Actualizar tarjetas
_mpRefreshCards(campoId, data.completado ? null : entry.next_firma_id);
if (data.completado) {
// Toma completada: mostrar resumen 10 s y luego cerrar modal
var cd = document.getElementById('mp-countdown');
if (cd) { clearInterval(cd._mpTick); cd.remove(); }
_mpMostrarResumen(entry.exam_type || 'Examen', 10);
setTimeout(function() {
try { window.parent.postMessage({ type: 'turneroFirmado' }, '*'); } catch(e) {}
}, 10000);
} else if (data.siguiente_toma_at) {
// Fix 2: pasar timestamp exacto; evita el redondeo de Math.round
var targetMs = new Date(data.siguiente_toma_at.replace(' ', 'T')).getTime();
_mpIniciarCountdown(targetMs, _mpEffectiveLabel(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
// Fix 4: unificar delay con el caso data.completado (ambos 2000ms)
setTimeout(function() {
try { window.parent.postMessage({ type: 'turneroFirmado' }, '*'); } catch(e) {}
}, 2000);
}
}
})
.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)();
var comp = ctx.createDynamicsCompressor();
comp.threshold.value = -6; comp.ratio.value = 12; comp.attack.value = 0;
comp.connect(ctx.destination);
// 3 ciclos de alarma, cada ciclo 3 notas; 2 osciladores por nota para más volumen
var notes = [1200, 880, 1200];
for (var cycle = 0; cycle < 3; cycle++) {
notes.forEach(function(freq, i) {
var t0 = ctx.currentTime + cycle * 1.0 + i * 0.28;
[0, 6].forEach(function(detune) { // dos osciladores ligeramente desafinados
var osc = ctx.createOscillator();
var g = ctx.createGain();
osc.type = 'sawtooth';
osc.frequency.value = freq;
osc.detune.value = detune;
osc.connect(g); g.connect(comp);
g.gain.setValueAtTime(1.0, t0);
g.gain.exponentialRampToValueAtTime(0.001, t0 + 0.24);
osc.start(t0); osc.stop(t0 + 0.24);
});
});
}
} catch(e) {}
}
// Fix 2: acepta timestamp (ms > 1e10) o minutos (número pequeño)
function _mpIniciarCountdown(targetMsOrMins, nextLabel, nextFirmaId) {
var targetMs = targetMsOrMins > 1e10 ? targetMsOrMins : Date.now() + targetMsOrMins * 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', targetMs: targetMs, label: nextLabel || '' }, '*'); } catch(e) {}
var _tcCdEl = nextFirmaId ? document.getElementById('tc-cd-' + nextFirmaId) : null;
box._mpTick = setInterval(function() {
var left = targetMs - Date.now();
if (left <= 0) {
if (_tcCdEl) _tcCdEl.textContent = '';
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([900,150,900,150,900,150,900,150,900]);
_mpPlayBeep();
try { window.parent.postMessage({ type: 'tomaProgresivaAlerta', label: nextLabel || '¡Siguiente muestra!' }, '*'); } catch(e) {}
// 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 = 0;
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>';
// Mini countdown en la tarjeta de la próxima toma
if (_tcCdEl) _tcCdEl.textContent = '⏱ ' + String(m).padStart(2,'0') + ':' + String(s).padStart(2,'0');
}, 1000);
}
function _mpMostrarResumen(examType, segs) {
if (document.getElementById('mp-resumen')) return;
segs = segs || 10;
// 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;
// Fix 5: usar hora cacheada al momento de guardar; evita traversal DOM frágil
var cached = window._mpTomaData && window._mpTomaData[fid];
var horaVal = (cached && cached.hora) || '';
if (!horaVal && entry.hora_campo) {
var horaEl = document.querySelector('[name="' + entry.hora_campo + '"]');
horaVal = horaEl ? horaEl.value : '';
}
rows.push({ hora: horaVal, 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></th></tr></thead><tbody>'
+ rows.map(function(r) {
return '<tr><td>' + (r.hora || '—') + '</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 en <span id="mp-resumen-seg">' + segs + '</span>s…</small></div></div>'
+ rowsHtml
+ '<div style="margin-top:10px;height:5px;background:#c3e6cb;border-radius:3px">'
+ '<div id="mp-resumen-bar" style="height:100%;width:100%;background:#198754;border-radius:3px;'
+ 'transition:width ' + segs + 's linear"></div></div>';
document.querySelector('.doc-body').appendChild(wrap);
// Animar barra y cuenta regresiva
requestAnimationFrame(function() { requestAnimationFrame(function() {
var bar = document.getElementById('mp-resumen-bar');
if (bar) bar.style.width = '0%';
}); });
var left = segs;
var tick = setInterval(function() {
left--;
var el = document.getElementById('mp-resumen-seg');
if (el) el.textContent = left;
if (left <= 0) clearInterval(tick);
}, 1000);
window.addEventListener('unload', function() { clearInterval(tick); });
// 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;
});
});
});
<?php if ($_formSoloPro): ?>
(function() {
var btn = document.getElementById('btn-completar-pro');
if (!btn) return;
btn.addEventListener('click', function() {
var msgEl = document.getElementById('btn-completar-pro-msg');
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Guardando...';
var campos = {};
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(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({ mp_completar: true, datos_respuestas: campos })
})
.then(function(r) { return r.json(); })
.then(function(d) {
if (d.ok) {
document.getElementById('btn-completar-pro-wrap').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>Completado correctamente.</strong></div>';
document.getElementById('btn-completar-pro-wrap').parentNode.appendChild(ok);
try { window.parent.postMessage({ type: 'turneroFirmado' }, '*'); } catch(e) {}
} else {
msgEl.innerHTML = '<span class="text-danger">' + (d.error || 'Error al guardar') + '</span>';
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-check-circle me-2"></i>Guardar y completar';
}
})
.catch(function() {
msgEl.innerHTML = '<span class="text-danger">Error de conexión.</span>';
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-check-circle me-2"></i>Guardar y completar';
});
});
})();
<?php endif; ?>
<?php if ($modoTurnero && $embebido && !empty($_mpMap) && $envio['estado'] === 'en_progreso'): ?>
(function() {
var btn = document.getElementById('btn-guardar-tiempos');
if (!btn) return;
btn.addEventListener('click', function() {
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;
});
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Guardando...';
fetch(window.location.href, {
method: 'POST', headers: {'Content-Type':'application/json'},
body: JSON.stringify({ datos_respuestas: dr })
})
.then(function(r){ return r.json(); })
.then(function(d) {
if (d.ok) {
// Actualizar obs en headers de tarjetas firmadas
if (window._muestrasMap) {
document.querySelectorAll('.toma-card--signed').forEach(function(card) {
var fid = card.getAttribute('data-firma');
var se = fid && window._muestrasMap[fid];
if (se && se.obs_campo) {
var obsEl = card.querySelector('[name="' + se.obs_campo + '"]');
var os = card.querySelector('.toma-card-obs');
if (obsEl && os) os.textContent = obsEl.value || '';
}
});
}
btn.innerHTML = '<i class="fas fa-check me-1"></i>Guardado';
setTimeout(function(){ btn.disabled=false; btn.innerHTML='<i class="fas fa-save me-1"></i>Guardar tiempos'; }, 2000);
} else {
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-save me-1"></i>Guardar tiempos';
}
})
.catch(function(){ btn.disabled=false; btn.innerHTML='<i class="fas fa-save me-1"></i>Guardar tiempos'; });
});
})();
<?php endif; ?>
<?php if ($modoTurnero && $embebido && !empty($_mpMap) && $envio['estado'] === 'en_progreso'): ?>
(function() {
var base = window.location.href.split('/ver_formulario_enviado.php')[0];
var token = <?= json_encode($tokenTurnero) ?>;
document.querySelectorAll('.btn-olvidar-toma').forEach(function(btn) {
btn.addEventListener('click', function() {
var hc = btn.dataset.hc;
var fids = JSON.parse(btn.dataset.fids || '[]');
var campos = [hc];
fids.forEach(function(fid) { campos.push(fid); campos.push(fid + '_svg'); });
if (!confirm('¿Olvidar esta toma? Se borrará la firma y podrás editar el tiempo.')) return;
btn.disabled = true;
fetch(base + '/modules/turnero/api/resetear_toma.php', {
method: 'POST', headers: {'Content-Type':'application/json'},
body: JSON.stringify({ token: token, campos: campos })
})
.then(function(r) { return r.json(); })
.then(function(d) {
if (d.ok) { window.location.reload(); }
else { alert(d.error || 'Error al olvidar'); btn.disabled = false; }
})
.catch(function(e) { alert(e.message); btn.disabled = false; });
});
});
})();
<?php endif; ?>
<?php if ($modoTurnero && $embebido && !empty($_mpMap) && !in_array($envio['estado'], ['firmado','cerrado_anticipado'])): ?>
(function() {
var btn = document.getElementById('btn-cerrar-anticip');
var modal = document.getElementById('modal-cerrar-anticip');
if (!btn || !modal) return;
btn.addEventListener('click', function() {
document.getElementById('mca-motivo').value = '';
document.getElementById('mca-error').classList.add('d-none');
modal.style.display = 'flex';
setTimeout(function() { document.getElementById('mca-motivo').focus(); }, 50);
});
document.getElementById('mca-cancelar').addEventListener('click', function() {
modal.style.display = 'none';
});
document.getElementById('mca-confirmar').addEventListener('click', function() {
var motivo = document.getElementById('mca-motivo').value.trim();
var errEl = document.getElementById('mca-error');
if (!motivo) { errEl.textContent = 'El motivo es obligatorio.'; errEl.classList.remove('d-none'); return; }
errEl.classList.add('d-none');
var confirmBtn = this;
confirmBtn.disabled = true;
confirmBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Guardando...';
fetch(window.location.href, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ mp_cerrar_anticipado: true, motivo: motivo })
})
.then(function(r) { return r.json(); })
.then(function(d) {
if (d.ok) {
modal.style.display = 'none';
try { window.parent.postMessage({ type: 'turneroFirmado' }, '*'); } catch(e) {}
location.reload();
} else {
errEl.textContent = d.error || 'Error al guardar.';
errEl.classList.remove('d-none');
confirmBtn.disabled = false;
confirmBtn.innerHTML = '<i class="fas fa-times-circle me-1"></i>Confirmar cierre';
}
})
.catch(function() {
errEl.textContent = 'Error de conexión.';
errEl.classList.remove('d-none');
confirmBtn.disabled = false;
confirmBtn.innerHTML = '<i class="fas fa-times-circle me-1"></i>Confirmar cierre';
});
});
})();
<?php endif; ?>
</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">
<canvas id="topaz-canvas" width="500" height="150"
style="border:1px solid #e2e8f0;border-radius:6px;background:#fff;max-width:100%;display:block;margin:0 auto"></canvas>
<div id="topaz-status-msg" style="font-size:.9rem;color:#64748b;font-weight:600;margin-top:8px">
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 = '<?= rtrim(defined('APP_URL') ? APP_URL : '', '/') ?>/assets/js/SigWebTablet.js';
let _loaded = false, _target = null, _poll = null, _tmr = null;
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);
});
}
// Detectar disponibilidad al cargar; ocultar botones si no hay pad, o marcar en línea
_loadScript().then(ok => {
document.querySelectorAll('.btn-topaz').forEach(b => {
if (!ok) { b.style.display = 'none'; return; }
b.innerHTML = '<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:#22c55e;margin-right:5px;vertical-align:middle;box-shadow:0 0 4px #22c55e"></span>'
+ '<i class="fas fa-tablet-alt me-1"></i>Tableta en línea';
});
});
function _toast(msg) {
const t = document.createElement('div');
t.style.cssText = 'position:fixed;bottom:24px;left:50%;transform:translateX(-50%);'
+ 'background:#1e293b;color:#fff;padding:12px 22px;border-radius:10px;'
+ 'font-size:.88rem;z-index:99999;box-shadow:0 4px 16px rgba(0,0,0,.3);'
+ 'max-width:90vw;text-align:center;line-height:1.5';
t.innerHTML = '<i class="fas fa-exclamation-circle me-2" style="color:#f59e0b"></i>' + msg;
document.body.appendChild(t);
setTimeout(() => t.remove(), 5000);
}
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) {
_toast('Pad biométrico no disponible en este equipo.<br>Instala el servicio Topaz SigWeb o usa la firma manual.');
return;
}
_target = target;
try {
const canvas = document.getElementById('topaz-canvas');
const canvasCtx = canvas.getContext('2d');
canvasCtx.clearRect(0, 0, canvas.width, canvas.height);
SetImageXSize(500);
SetImageYSize(150);
SetImagePenWidth(3);
ClearTablet();
_tmr = SetTabletState(1, canvasCtx, 50);
} catch(e) {
_toast('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);
let _lastPts = -1;
_poll = setInterval(() => {
try {
const pts = NumberOfTabletPoints();
document.getElementById('topaz-pts').textContent = pts;
document.getElementById('topaz-btn-aceptar').disabled = pts === 0;
if (pts > 0) {
_setStatus('✅ Firma detectada — presione Aceptar', true);
if (pts !== _lastPts) {
_lastPts = pts;
GetSigImageB64(function(b64) {
if (!b64) return;
const img = new Image();
img.onload = () => {
const cv = document.getElementById('topaz-canvas');
const cx = cv.getContext('2d');
cx.clearRect(0, 0, cv.width, cv.height);
cx.drawImage(img, 0, 0, cv.width, cv.height);
};
img.src = 'data:image/png;base64,' + b64;
});
}
}
} catch(e) { _stopPoll(); }
}, 400);
}
function _stopPoll() { if (_poll) { clearInterval(_poll); _poll = null; } }
function _cerrar() {
_stopPoll();
try { SetTabletState(0, _tmr); } catch(e) {}
_tmr = 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() {
const target = _target;
_cerrar();
try {
GetSigImageB64(function(b64) {
if (!b64) { _toast('No se capturó ninguna firma.'); return; }
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;
});
} catch(e) {
_toast('Error al capturar la firma: ' + e.message);
}
}
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';
if (activo) { el.style.removeProperty('margin'); el.style.removeProperty('padding'); }
else { el.style.setProperty('margin','0','important'); el.style.setProperty('padding','0','important'); }
});
});
// Toma progresiva: scroll a la primera firma pendiente
if (window._muestrasMap) {
setTimeout(function() {
var hayFirmaIniciada = Object.keys(window._mpSavedFirmas || {}).length > 0
|| !!document.querySelector('.firma-box img');
var fw = hayFirmaIniciada
? document.querySelector('.firma-pro-widget:not([style*="display:none"])') : null;
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>
<?php if ($autoprint): ?>
<script src="https://cdn.jsdelivr.net/npm/qz-tray@2.2.4/qz-tray.js"></script>
<script src="/assets/js/qz-print.js"></script>
<script>
window.addEventListener('load', function() {
setTimeout(function() {
if (typeof qz !== 'undefined') { qzPrint(); }
else { window.print(); }
}, 800);
});
</script>
<?php endif; ?>
</body>
</html>