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.id AS paciente_id, p.nombre_completo AS paciente_nombre, p.numero_documento, p.tipo_documento, p.fecha_nacimiento, p.telefono AS paciente_telefono, p.eps, p.direccion, p.genero, 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, cu.full_name AS creador_nombre 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 LEFT JOIN admin_users cu ON cu.id = tc.creado_por 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'=> $tcRow['creador_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'] ?? '', 'direccion' => $tcRow['direccion'] ?? '', 'genero' => $tcRow['genero'] ?? '', '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, p.direccion, p.genero, 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, p.direccion, p.genero, 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) // Aplica tanto al turnero embebido como a la firma directa fuera del turnero // (staff logueado viendo/firmando un formulario por ?id=), para que el // nombre y cédula del firmante salgan en cualquier formato. $firmaProfPreguardada = null; $profNombre = null; $profDocumento = null; if ((($modoTurnero && $embebido) || (!$modoTurnero && !$modoPublico)) && isUserLoggedIn()) { $uid = (int)($_SESSION['admin_user']['id'] ?? 0); if ($uid) { try { $stmt = $db->getConnection()->prepare( "SELECT au.firma_svg, au.full_name, au.cedula AS au_cedula, 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['au_cedula'] ?: ($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 '
He leído el documento. Dibuje su firma:
He leído y comprendido el contenido de este documento. Por favor dibuje su firma en el recuadro: