El acceso quedaba solo en la tarjeta del paciente de la estación: al ver un dato mal mientras se llenaba el formulario había que cerrarlo, buscar el botón y volver a abrirlo. Ahora los datos del paciente dentro del formulario llevan un lápiz que abre el mismo modal, ya en modo edición, sin cerrar lo que se está llenando. El formulario va embebido, así que no puede abrir el modal por su cuenta: le avisa al contenedor por el mismo mecanismo que ya usa para informar una firma. El modal queda por encima del formulario (z-index 9500 contra 9000). De paso se corrige un error del commit anterior: el refresco tras guardar buscaba el iframe con un selector equivocado —el modal es modal-consentimiento, no modal-consent—, así que el formulario nunca tomaba el dato corregido sin recargar a mano. Ahora se recargan los dos iframes posibles por su id real. El lápiz aparece una sola vez, en el primer campo vinculado, y no se imprime. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
3346 lines
176 KiB
PHP
3346 lines
176 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.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 '
|
|
<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; }
|
|
.lnk-corregir { background:none; border:none; color:#94a3b8; cursor:pointer;
|
|
padding:0 4px; font-size:11px; flex-shrink:0; }
|
|
.lnk-corregir:hover { color:#1565c0; }
|
|
.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">×</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 if ($modoTurnero && $modoEditar && !empty($tcRow['paciente_id'])): ?>
|
|
<button type="button" id="btnCargarAnterior" class="btn btn-outline-primary btn-sm no-print mb-3"
|
|
data-paciente="<?= (int)$tcRow['paciente_id'] ?>"
|
|
data-formulario="<?= (int)$tcRow['formulario_id'] ?>"
|
|
data-turno="<?= (int)$tcRow['turno_id'] ?>">
|
|
<i class="fas fa-history me-1"></i>Cargar datos de la visita anterior
|
|
</button>
|
|
<div id="cargarAnteriorMsg" class="small text-muted mb-2" style="display:none"></div>
|
|
<?php endif; ?>
|
|
<?php
|
|
// Los campos vinculados leen de aquí. En los envíos de formulario,
|
|
// datos_prefilled es una copia tomada al enviarlo, que puede no incluir
|
|
// claves agregadas después; se completa con los datos vivos del paciente
|
|
// para que un campo automático no quede vacío por eso.
|
|
$paciente = ($datosPrefilled['__paciente'] ?? []) + array_filter([
|
|
'nombre_completo' => $envio['paciente_nombre'] ?? null,
|
|
'numero_documento' => $envio['numero_documento'] ?? null,
|
|
'tipo_documento' => $envio['tipo_documento'] ?? null,
|
|
'fecha_nacimiento' => $envio['fecha_nacimiento'] ?? null,
|
|
'telefono' => $envio['paciente_telefono'] ?? null,
|
|
'eps' => $envio['eps'] ?? null,
|
|
'direccion' => $envio['direccion'] ?? null,
|
|
'genero' => $envio['genero'] ?? null,
|
|
], fn($v) => $v !== null && $v !== '');
|
|
|
|
// 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)
|
|
$_lnkBtnPuesto = false; // el lápiz de corregir va una sola vez
|
|
$_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
|
|
// Atributos heredados por los campos de la sección actual. Sin esto, una
|
|
// sección cuya condición no se cumple ocultaba solo su encabezado y dejaba
|
|
// sus campos (hora, observaciones, firma) sueltos y visibles.
|
|
$_secAttr = '';
|
|
foreach ($esquema as $campo):
|
|
$tipo = $campo['tipo'] ?? '';
|
|
if ($tipo === 'separador'):
|
|
$_mpSkipSec = false;
|
|
$cond = $campo['condicion'] ?? null;
|
|
if ($cond) {
|
|
$vCtrl = $todos[$cond['campo_id']] ?? null;
|
|
// La condición puede depender de un campo vinculado (p. ej. el
|
|
// sexo del paciente, que gobierna la sección obstétrica). Esos
|
|
// no están en las respuestas: su valor sale de la ficha.
|
|
if ($vCtrl === null) {
|
|
foreach ($esquema as $__c) {
|
|
if (($__c['id'] ?? '') === $cond['campo_id']
|
|
&& ($__c['tipo'] ?? '') === 'linked') {
|
|
$vCtrl = $paciente[$__c['linked_key'] ?? ''] ?? null;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
$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;
|
|
}
|
|
// La sección queda en el DOM (el JS puede mostrarla si cambian de examen)
|
|
// pero oculta, y sus campos heredan el mismo estado.
|
|
$_secAttr = ' data-sep-id="' . esc2($campo['id'] ?? '') . '"'
|
|
. (($_saltarSeccion && $modoEditar) ? ' style="display:none"' : '');
|
|
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"<?= $_secAttr ?>><?php endif; ?>
|
|
<div class="section-title mt-3" style="color:<?= $fColor ?>"<?= $compact ? '' : $_secAttr ?>>
|
|
<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):
|
|
// Sin respaldo al usuario de sesión actual: si no quedó guardado el
|
|
// firmante real de esta toma, no se adivina (evita atribuir la firma
|
|
// a quien simplemente está viendo el documento ahora).
|
|
$_pn = $datosCliente[$cid . '_pro_nombre'] ?? $datosCliente['_pro_nombre'] ?? null;
|
|
$_pd = $datosCliente[$cid . '_pro_cedula'] ?? $datosCliente['_pro_cedula'] ?? 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 ?>"<?= $_secAttr ?>><?= 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"<?= $_secAttr ?>><?= $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>
|
|
<?php if ($modoTurnero && $embebido && $modoEditar && !$_lnkBtnPuesto): $_lnkBtnPuesto = true; ?>
|
|
<button type="button" class="lnk-corregir no-print" onclick="pedirCorregirPaciente()"
|
|
title="Corregir los datos del paciente">
|
|
<i class="fas fa-pen"></i>
|
|
</button>
|
|
<?php endif; ?>
|
|
</div>
|
|
<?php endif; continue; endif; // linked
|
|
if ($tipo === 'textarea'):
|
|
?>
|
|
<div class="campo-edit campo-full" data-campo-id="<?= esc2($cid) ?>"<?= $_secAttr ?>>
|
|
<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) ?>"<?= $_secAttr ?>>
|
|
<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) ?>"<?= $_secAttr ?>>
|
|
<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) ?>"<?= $_secAttr ?>>
|
|
<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) ?>"<?= $_secAttr ?>>
|
|
<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) ?>"<?= $_secAttr ?>>
|
|
<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) ?> •
|
|
<?php endif; ?>
|
|
<i class="fas fa-shield-alt me-1"></i>Generado el <?= date('d/m/Y H:i') ?> • ID #<?= $envio['id'] ?>
|
|
<?php if ($numeroOrden): ?>
|
|
• Orden: <strong><?= esc2($numeroOrden) ?></strong>
|
|
<?php endif; ?>
|
|
</span>
|
|
<?php if ($envio['hash_verificacion']): ?>
|
|
<span class="hash-short" title="Hash SHA-256"><?= substr($envio['hash_verificacion'],0,16) ?>...</span>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
</div><!-- /doc-wrap -->
|
|
|
|
<script>
|
|
<?php if (!empty($_mpMap)): ?>
|
|
/* ── Panel 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.
|
|
// Acepta un examen o varios: un paciente puede tener Glicemia y Prolactina a la
|
|
// vez, y cada uno aporta sus propias secciones de toma.
|
|
function _mpApplyExamVisibility(selectedExams) {
|
|
var exams = selectedExams == null ? []
|
|
: (Array.isArray(selectedExams) ? selectedExams : [selectedExams]);
|
|
|
|
// 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 de todos los exámenes seleccionados
|
|
var examSeps = {};
|
|
exams.forEach(function(ex) {
|
|
if (!ex) return;
|
|
var groups = _mpGetGroupsForExam(ex);
|
|
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).
|
|
// Los campos llevan data-sep-id, así que se ocultan junto a su encabezado
|
|
// en vez de quedar sueltos cuando la sección no aplica.
|
|
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;
|
|
} else {
|
|
var sid = child.getAttribute('data-sep-id');
|
|
if (sid && allTomaSeps[sid] !== undefined) inHidden = !examSeps[sid];
|
|
}
|
|
child.style.display = inHidden ? 'none' : '';
|
|
});
|
|
}
|
|
|
|
// Helper: exámenes seleccionados (radio name=_c8j2g16 o checkbox name=_c8j2g16[])
|
|
function _mpGetExamNames() {
|
|
return Array.from(document.querySelectorAll('[name="_c8j2g16"]:checked,[name="_c8j2g16[]"]:checked'))
|
|
.map(function(el) { return el.value; });
|
|
}
|
|
// Compatibilidad: primer examen seleccionado
|
|
function _mpGetExamName() {
|
|
var n = _mpGetExamNames();
|
|
return n.length ? n[0] : 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, '"') + '" 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, '"') + '">'
|
|
+ '<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;
|
|
_mpApplyExamVisibility(_mpGetExamNames());
|
|
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) {}
|
|
// Los checkboxes ya quedaron restaurados arriba: aplicar sobre todos los
|
|
// marcados, no solo el primero.
|
|
_mpApplyExamVisibility(_mpGetExamNames());
|
|
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,'"') + '" 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,'"') + '" 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,'"') + '"></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() {
|
|
// Aplicar visibilidad al abrir: antes solo corría al cambiar el examen o al
|
|
// usar el panel de configuración, así que un formulario ya configurado que se
|
|
// abría desde bandeja o historial mostraba secciones que no correspondían.
|
|
_mpApplyExamVisibility(_mpGetExamNames());
|
|
_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; ?>
|
|
/* El formulario va embebido en la estación: no puede abrir el modal de
|
|
corrección por su cuenta, le pide al contenedor que lo haga. */
|
|
function pedirCorregirPaciente() {
|
|
try { window.parent.postMessage({ type: 'corregirPaciente' }, '*'); } catch (e) {}
|
|
}
|
|
|
|
window._fpwPreFirma = <?= json_encode($firmaProfPreguardada) ?>;
|
|
window._profNombre = <?= json_encode($profNombre) ?>;
|
|
window._profDocumento = <?= json_encode($profDocumento) ?>;
|
|
|
|
// ── Cargar datos de la visita anterior (mismo formulario, mismo paciente) ──
|
|
(function() {
|
|
var btn = document.getElementById('btnCargarAnterior');
|
|
if (!btn) return;
|
|
btn.addEventListener('click', function() {
|
|
var base = window.location.href.split('/ver_formulario_enviado.php')[0];
|
|
var msg = document.getElementById('cargarAnteriorMsg');
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Buscando…';
|
|
var params = new URLSearchParams({
|
|
paciente_id: btn.dataset.paciente,
|
|
formulario_id: btn.dataset.formulario,
|
|
exclude_turno_id: btn.dataset.turno,
|
|
});
|
|
fetch(base + '/modules/turnero/api/get_formulario_anterior.php?' + params)
|
|
.then(function(r) { return r.json(); })
|
|
.then(function(json) {
|
|
btn.disabled = false;
|
|
btn.innerHTML = '<i class="fas fa-history me-1"></i>Cargar datos de la visita anterior';
|
|
if (!json.ok || !json.encontrado) {
|
|
msg.style.display = '';
|
|
msg.textContent = 'No hay una visita anterior con este formulario firmado.';
|
|
return;
|
|
}
|
|
var datos = json.datos_respuestas || {};
|
|
document.querySelectorAll('[name]').forEach(function(el) {
|
|
var raw = el.name, isArr = raw.slice(-2) === '[]', name = isArr ? raw.slice(0, -2) : raw;
|
|
if (!(name in datos)) return;
|
|
var val = datos[name];
|
|
if (el.type === 'checkbox') {
|
|
var arr = Array.isArray(val) ? val : [val];
|
|
el.checked = arr.includes(el.value);
|
|
} else if (el.type === 'radio') {
|
|
el.checked = (el.value === val);
|
|
} else {
|
|
el.value = val;
|
|
}
|
|
});
|
|
msg.style.display = '';
|
|
msg.textContent = 'Datos cargados de la visita ' + (json.turno_codigo || '') + '. Revise y firme.';
|
|
})
|
|
.catch(function() {
|
|
btn.disabled = false;
|
|
btn.innerHTML = '<i class="fas fa-history me-1"></i>Cargar datos de la visita anterior';
|
|
msg.style.display = '';
|
|
msg.textContent = 'Error de conexión.';
|
|
});
|
|
});
|
|
})();
|
|
<?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,
|
|
pro_nombre: window._profNombre || null, pro_cedula: window._profDocumento || null };
|
|
|
|
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;
|
|
// Identidad del firmante por toma (puede cambiar de profesional entre tomas)
|
|
if (window._profNombre) { dr[campoId + '_pro_nombre'] = window._profNombre; dr['_pro_nombre'] = window._profNombre; }
|
|
if (window._profDocumento) { dr[campoId + '_pro_cedula'] = window._profDocumento; dr['_pro_cedula'] = window._profDocumento; }
|
|
(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 txt = ctrl.querySelector('input[type="text"], textarea');
|
|
// Un campo vinculado se muestra como texto, no como control:
|
|
// es el caso del sexo, que gobierna la sección obstétrica.
|
|
var lnk = ctrl.classList.contains('campo-linked')
|
|
? ctrl.querySelector('.campo-linked-valor') : null;
|
|
var v = radio ? radio.value
|
|
: sel ? sel.value
|
|
: lnk ? lnk.textContent.trim()
|
|
: txt ? txt.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 PHP marca con display:none los campos de secciones cuya
|
|
// condición no se cumple. Esta función anima con max-height y
|
|
// opacity, así que sin quitar el display el campo nunca aparece.
|
|
if (activo) el.style.removeProperty('display');
|
|
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>
|