Rol nuevo para consultar turnos y tiempos de atención, de solo lectura y con acceso únicamente a dashboard e historial. Se le permite exportar el historial, que es la forma de analizar tiempos fuera del sistema. Al crearlo salió que las vistas del turnero no verificaban nada por su cuenta: el control del ERP es por módulo, así que cualquiera con acceso al turnero podía abrir Configuración —lugares, dispositivos, plantillas— escribiendo la URL, aunque el menú no se la mostrara. Aplicaba a todos los roles, no solo al nuevo. Se agrega _acceso.php con una verificación de rol que usan las tres pantallas que operan sobre la atención o cambian configuración. Verificado que cada rol conserva lo que ya usaba: recepcionista entra a recepción, bacteriólogo a las estaciones, supervisor y administradores a todo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
3233 lines
160 KiB
PHP
3233 lines
160 KiB
PHP
<?php
|
|
/**
|
|
* Puesto de Recepción — Módulo Turnero
|
|
* Requiere login + módulo turnero.
|
|
*/
|
|
require_once __DIR__ . '/../../../config/config.php';
|
|
if (!isUserLoggedIn()) {
|
|
header('Location: ' . BASE_URL . 'login.php');
|
|
exit;
|
|
}
|
|
|
|
require_once __DIR__ . '/../_acceso.php';
|
|
turneroExigirRol(['recepcionista', 'lab_recepcion', 'supervisor']);
|
|
|
|
// Tablet asignada → forzar su escritorio por token de navegador (o IP como fallback)
|
|
$_recepForzado = 0;
|
|
try {
|
|
$_dispPdo = Database::getInstance()->getConnection();
|
|
$_dispRow = null;
|
|
// 1. Chequeo por token de navegador (cookie)
|
|
$_devToken = trim($_COOKIE['turnero_token'] ?? '');
|
|
if ($_devToken) {
|
|
$_s = $_dispPdo->prepare(
|
|
"SELECT td.lugar_id, td.nombre, tl.tipo
|
|
FROM turnero_dispositivos td
|
|
JOIN turnero_lugares tl ON tl.id = td.lugar_id
|
|
WHERE td.token = ? AND td.activo = 1 LIMIT 1"
|
|
);
|
|
$_s->execute([$_devToken]);
|
|
$_dispRow = $_s->fetch(PDO::FETCH_ASSOC) ?: null;
|
|
}
|
|
// 2. Fallback por IP
|
|
if (!$_dispRow) {
|
|
$_clientIp = trim(explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR'] ?? '')[0]);
|
|
$_s = $_dispPdo->prepare(
|
|
"SELECT td.lugar_id, td.nombre, tl.tipo
|
|
FROM turnero_dispositivos td
|
|
JOIN turnero_lugares tl ON tl.id = td.lugar_id
|
|
WHERE td.ip = ? AND td.token IS NULL AND td.activo = 1 LIMIT 1"
|
|
);
|
|
$_s->execute([$_clientIp]);
|
|
$_dispRow = $_s->fetch(PDO::FETCH_ASSOC) ?: null;
|
|
}
|
|
if ($_dispRow) {
|
|
$_SESSION['turnero_dispositivo'] = $_dispRow;
|
|
$_forzado = (int)$_dispRow['lugar_id'];
|
|
if ($_dispRow['tipo'] !== 'recepcion') {
|
|
header('Location: ' . BASE_URL . 'erp.php?m=turnero&v=lugar&lugar_id=' . $_forzado); exit;
|
|
}
|
|
if ((int)($_GET['desk_id'] ?? 0) !== $_forzado) {
|
|
header('Location: ' . BASE_URL . 'erp.php?m=turnero&v=recepcion&desk_id=' . $_forzado); exit;
|
|
}
|
|
$_recepForzado = $_forzado;
|
|
}
|
|
} catch (\Throwable $_) {}
|
|
// Restricción a nivel de usuario (turnero_lugar_id en admin_users)
|
|
if (!$_recepForzado) {
|
|
$_userDesk = (int)($_SESSION['admin_user']['turnero_lugar_id'] ?? 0);
|
|
if ($_userDesk) {
|
|
if ((int)($_GET['desk_id'] ?? 0) !== $_userDesk) {
|
|
header('Location: ' . BASE_URL . 'erp.php?m=turnero&v=recepcion&desk_id=' . $_userDesk); exit;
|
|
}
|
|
$_recepForzado = $_userDesk;
|
|
}
|
|
}
|
|
|
|
// Cargar catálogos para la ficha
|
|
try {
|
|
$pdo = Database::getInstance()->getConnection();
|
|
|
|
$lugarRecepcion = $pdo->query(
|
|
"SELECT id, nombre FROM turnero_lugares WHERE tipo='recepcion' AND activo=1 ORDER BY sort_order LIMIT 1"
|
|
)->fetch(PDO::FETCH_ASSOC);
|
|
|
|
// Destinos agrupados: un representante por grupo_id, individuales si grupo_id=NULL
|
|
$lugaresDestino = $pdo->query(
|
|
"SELECT MIN(id) AS id,
|
|
CASE WHEN grupo_id IS NOT NULL THEN 'Toma de Muestras' ELSE nombre END AS nombre,
|
|
grupo_id
|
|
FROM turnero_lugares
|
|
WHERE tipo='muestras' AND activo=1
|
|
GROUP BY COALESCE(grupo_id, id)
|
|
ORDER BY MIN(sort_order)"
|
|
)->fetchAll(PDO::FETCH_ASSOC);
|
|
$lugarMuestras = $lugaresDestino[0] ?? null;
|
|
|
|
$examenes = $pdo->query(
|
|
"SELECT et.id, et.codigo, et.nombre, et.categoria, et.cups, et.genero_permitido,
|
|
IF(COUNT(etc.formulario_id) > 0, 1, 0) AS tiene_consentimiento
|
|
FROM exam_tipos et
|
|
LEFT JOIN exam_tipo_consentimientos etc ON etc.exam_tipo_id = et.id
|
|
WHERE et.activo = 1
|
|
GROUP BY et.id
|
|
ORDER BY et.categoria, et.nombre"
|
|
)->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
// Agrupar exámenes por categoría
|
|
$examenesAgrupados = [];
|
|
foreach ($examenes as $ex) {
|
|
$cat = $ex['categoria'] ?: 'General';
|
|
$examenesAgrupados[$cat][] = $ex;
|
|
}
|
|
|
|
// Escritorio de recepción (vía ?desk_id=X)
|
|
$deskId = (int)($_GET['desk_id'] ?? 0);
|
|
$desk = null;
|
|
$desksRecepcion = $pdo->query(
|
|
"SELECT id, nombre FROM turnero_lugares WHERE tipo='recepcion' AND activo=1 ORDER BY sort_order"
|
|
)->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
if ($deskId) {
|
|
foreach ($desksRecepcion as $d) {
|
|
if ($d['id'] === $deskId) { $desk = $d; break; }
|
|
}
|
|
if (!$desk) { $deskId = 0; }
|
|
}
|
|
// Si solo hay un escritorio, asignarlo automáticamente
|
|
if (!$deskId && count($desksRecepcion) === 1) {
|
|
$desk = $desksRecepcion[0];
|
|
$deskId = (int)$desk['id'];
|
|
}
|
|
|
|
$prioridades = $pdo->query(
|
|
"SELECT codigo, nombre, color FROM turnero_prioridades WHERE activo=1 ORDER BY orden_peso"
|
|
)->fetchAll(PDO::FETCH_ASSOC);
|
|
} catch (\Throwable $e) {
|
|
$lugares = [];
|
|
$examenesAgrupados = [];
|
|
$deskId = 0;
|
|
$desk = null;
|
|
$lugarRecepcion = null;
|
|
$lugarMuestras = null;
|
|
$prioridades = [];
|
|
}
|
|
|
|
$deskNombre = $desk['nombre'] ?? null; // null = vista genérica
|
|
$adminNombre = $_SESSION['admin_user']['full_name'] ?? $_SESSION['admin_user']['username'] ?? 'Operador';
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="es">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title><?= htmlspecialchars($deskNombre ?? 'Recepción') ?> — Turnero</title>
|
|
<script>(function(){try{var m=window.matchMedia('(prefers-color-scheme: dark)');function a(d){document.documentElement.setAttribute('data-bs-theme',d?'dark':'light');}a(m.matches);m.addEventListener('change',function(e){a(e.matches);});}catch(e){}})();</script>
|
|
<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">
|
|
<link href="<?= BASE_URL ?>assets/css/styles.css?v=15" rel="stylesheet">
|
|
<link href="https://cdn.jsdelivr.net/npm/tom-select@2.4.3/dist/css/tom-select.bootstrap5.min.css" rel="stylesheet">
|
|
<script src="https://cdn.jsdelivr.net/npm/tom-select@2.4.3/dist/js/tom-select.complete.min.js"></script>
|
|
<style>
|
|
/* ── Layout de dos columnas ── */
|
|
.rec-layout {
|
|
display: grid;
|
|
grid-template-columns: 370px 1fr;
|
|
height: calc(100vh - 56px);
|
|
overflow: hidden;
|
|
}
|
|
@media (max-width: 680px) {
|
|
.rec-layout { grid-template-columns: 1fr; position: relative; overflow: hidden; }
|
|
.rec-cola { transition: transform .2s ease; }
|
|
.rec-cola.mobile-oculta { transform: translateX(-100%); position: absolute; inset: 0; pointer-events: none; }
|
|
.rec-ficha { position: absolute; inset: 0; background: #fff; z-index: 10; transform: translateX(100%); transition: transform .2s ease; overflow-y: auto; }
|
|
.rec-ficha.mobile-visible { transform: translateX(0); }
|
|
.btn-volver-cola { display: inline-flex !important; align-items: center; gap: 6px; }
|
|
}
|
|
/* En pantalla 2 columnas el botón de topbar queda oculto (hay uno en la cola) */
|
|
@media (min-width: 681px) { #btn-llamar { display: none !important; } }
|
|
|
|
/* ── Columna cola ── */
|
|
.rec-cola {
|
|
background: #f8fafc;
|
|
border-right: 1px solid #e2e8f0;
|
|
display: flex; flex-direction: column;
|
|
overflow: hidden;
|
|
}
|
|
.rec-cola-header {
|
|
padding: 1rem 1.2rem .6rem;
|
|
background: #fff; border-bottom: 1px solid #e2e8f0;
|
|
flex-shrink: 0;
|
|
}
|
|
.cola-items { flex: 1; overflow-y: auto; padding: .5rem; }
|
|
.cola-card {
|
|
background: #fff; border: 1px solid #e2e8f0;
|
|
border-radius: 11px; padding: .65rem .9rem;
|
|
margin-bottom: .4rem; cursor: default;
|
|
display: flex; align-items: center; gap: .6rem;
|
|
transition: box-shadow .15s;
|
|
}
|
|
.cola-card.activo { border-color: #3b82f6; box-shadow: 0 0 0 2px #bfdbfe; }
|
|
.cola-card.en-atencion { background: #eff6ff; border-color: #93c5fd; border-left: 4px solid #2563eb; }
|
|
.atencion-chip { font-size: .6rem; font-weight: 700; border-radius: 99px;
|
|
padding: 0 6px; line-height: 1.7;
|
|
background: #dbeafe; color: #1d4ed8; }
|
|
.cola-card .prio-dot {
|
|
width: 36px; height: 36px; border-radius: 9px;
|
|
display: flex; align-items: center; justify-content: center;
|
|
font-size: 1.1rem; font-weight: 800; color: #fff; flex-shrink: 0;
|
|
}
|
|
.cola-card .turno-info { flex: 1; min-width: 0; }
|
|
.cola-card .turno-info .cod { font-size: 1rem; font-weight: 700; }
|
|
.cola-card .turno-info .pac { font-size: .78rem; color: #64748b;
|
|
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
.cola-vacia { text-align: center; padding: 3rem 1rem; color: #94a3b8; }
|
|
.turno-info .t-badge-row { display: flex; align-items: center; gap: .3rem; margin-top: 2px; flex-wrap: wrap; }
|
|
.tiempo-chip { font-size: .6rem; font-weight: 700; border-radius: 99px; padding: 0 6px; line-height: 1.7; }
|
|
.tiempo-chip.ok { background: #f0fdf4; color: #166534; }
|
|
.tiempo-chip.warn { background: #fffbeb; color: #92400e; }
|
|
.tiempo-chip.crit { background: #fef2f2; color: #991b1b; }
|
|
|
|
/* ── Columna ficha ── */
|
|
.rec-ficha {
|
|
overflow-y: auto; padding: 1.5rem;
|
|
background: #fff;
|
|
}
|
|
.ficha-placeholder {
|
|
display: flex; flex-direction: column;
|
|
align-items: center; justify-content: center;
|
|
height: 100%; color: #94a3b8; text-align: center;
|
|
}
|
|
|
|
/* ── Secciones de la ficha ── */
|
|
.ficha-section {
|
|
background: #f8fafc; border: 1px solid #e2e8f0;
|
|
border-radius: 12px; padding: 1.2rem; margin-bottom: 1rem;
|
|
}
|
|
.ficha-section h6 { color: #475569; font-size: .78rem;
|
|
text-transform: uppercase; letter-spacing: .08em; margin-bottom: .8rem; }
|
|
|
|
/* ── Búsqueda de paciente ── */
|
|
.pac-resultado {
|
|
border: 1px solid #e2e8f0; border-radius: 8px; padding: .5rem .75rem;
|
|
cursor: pointer; margin-top: .25rem; transition: background .12s;
|
|
}
|
|
.pac-resultado:hover { background: #f1f5f9; }
|
|
.pac-resultado .pac-nombre { font-weight: 600; font-size: .9rem; }
|
|
.pac-resultado .pac-doc { font-size: .78rem; color: #64748b; }
|
|
.pac-seleccionado {
|
|
background: #eff6ff; border: 1px solid #93c5fd;
|
|
border-radius: 8px; padding: .65rem .9rem;
|
|
display: flex; align-items: center; justify-content: space-between;
|
|
}
|
|
|
|
/* ── Tom Select — exámenes ── */
|
|
#wrap-examenes .ts-wrapper { font-size: .88rem; }
|
|
#wrap-examenes .ts-control {
|
|
min-height: 44px; border-radius: 8px;
|
|
flex-direction: column; align-items: stretch; gap: 3px;
|
|
}
|
|
#wrap-examenes .ts-control input { order: -1; }
|
|
#wrap-examenes .ts-dropdown { font-size: .88rem; }
|
|
#wrap-examenes .ts-dropdown .optgroup-header {
|
|
font-size: .7rem; font-weight: 700; color: #94a3b8;
|
|
text-transform: uppercase; letter-spacing: .08em;
|
|
}
|
|
/* Item seleccionado — fila completa */
|
|
#wrap-examenes .ts-control .item {
|
|
display: flex; align-items: center; width: 100%;
|
|
background: #eff6ff; color: #1e40af;
|
|
border: 1px solid #bfdbfe; border-radius: 6px;
|
|
padding: 3px 6px; gap: 6px; box-sizing: border-box;
|
|
}
|
|
#wrap-examenes .ts-control .item .ts-exam-cups {
|
|
font-size: .65rem; font-weight: 700; color: #64748b;
|
|
background: #e2e8f0; border-radius: 3px; padding: 0 4px;
|
|
white-space: nowrap; flex-shrink: 0;
|
|
}
|
|
#wrap-examenes .ts-control .item .ts-exam-nom { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
#wrap-examenes .ts-control .item .remove { margin-left: auto; flex-shrink: 0; color: #93c5fd; }
|
|
/* Dropdown: CUPS chip al lado del nombre */
|
|
#wrap-examenes .ts-dropdown .ts-exam-cups-drop {
|
|
font-size: .65rem; font-weight: 600; color: #64748b;
|
|
background: #f1f5f9; border-radius: 3px; padding: 0 4px;
|
|
white-space: nowrap; margin-left: 4px;
|
|
}
|
|
.ts-consent-badge {
|
|
font-size: .65rem; font-weight: 700; border-radius: 4px;
|
|
padding: 1px 5px; margin-left: 5px;
|
|
background: #fef3c7; color: #92400e; border: 1px solid #fde68a;
|
|
white-space: nowrap;
|
|
}
|
|
.ts-consent-dot { margin-left: 3px; color: #d97706; font-size: .75rem; }
|
|
#wrap-examenes.disabled { opacity: .4; pointer-events: none; }
|
|
|
|
/* ── Consentimientos ── */
|
|
.consent-item {
|
|
display: flex; align-items: center; gap: .5rem;
|
|
padding: .5rem .7rem; border-radius: 10px;
|
|
font-size: .84rem; margin-bottom: .4rem;
|
|
border: 1px solid transparent;
|
|
}
|
|
.consent-item.firmado { background: #f0fdf4; color: #166534; border-color: #bbf7d0; }
|
|
.consent-item.pendiente { background: #fffbeb; color: #92400e; border-color: #fde68a; }
|
|
.consent-item.enviado { background: #eff6ff; color: #1e40af; border-color: #bfdbfe; }
|
|
.consent-item.visto { background: #faf5ff; color: #6b21a8; border-color: #ddd6fe; }
|
|
.consent-item.rechazado { background: #f8fafc; color: #64748b; border-color: #e2e8f0; }
|
|
.consent-item .c-nom { flex: 1; font-weight: 500; }
|
|
.consent-item .c-badge { font-size: .7rem; font-weight: 700; padding: 1px 8px;
|
|
border-radius: 99px; border: 1px solid currentColor; opacity: .85; }
|
|
.consent-acciones { display: flex; gap: .3rem; flex-shrink: 0; }
|
|
.consent-acciones .btn { font-size: .82rem; padding: 5px 12px; border-radius: 7px; }
|
|
.consent-prof-row { display:flex; align-items:center; gap:.4rem; font-size:.75rem;
|
|
margin-top:.25rem; padding-top:.25rem; border-top:1px solid rgba(0,0,0,.07); }
|
|
.consent-prof-row .badge { font-size:.65rem; }
|
|
|
|
/* ── Barra de acciones ── */
|
|
.ficha-acciones {
|
|
position: sticky; bottom: 0;
|
|
background: rgba(255,255,255,.97);
|
|
backdrop-filter: blur(6px);
|
|
border-top: 1px solid #e2e8f0;
|
|
padding: .8rem 1.4rem .9rem;
|
|
margin-top: 1rem;
|
|
}
|
|
|
|
/* ── Badge de turno activo en topbar ── */
|
|
.badge-turno-activo {
|
|
background: #eff6ff; color: #1d4ed8;
|
|
border: 1px solid #bfdbfe; border-radius: 8px;
|
|
padding: .25rem .75rem; font-size: .82rem; font-weight: 700;
|
|
}
|
|
|
|
/* ── Toast ── */
|
|
.rec-toast {
|
|
position: fixed; bottom: 2rem; right: 2rem; z-index: 9999;
|
|
display: flex; align-items: center; gap: .7rem;
|
|
padding: .8rem 1.2rem; border-radius: 12px;
|
|
background: #fff; border: 1px solid #e2e8f0;
|
|
box-shadow: 0 8px 30px rgba(0,0,0,.12);
|
|
font-size: .9rem; font-weight: 500;
|
|
transform: translateY(120%); opacity: 0;
|
|
transition: transform .3s ease, opacity .3s ease;
|
|
pointer-events: none;
|
|
}
|
|
.rec-toast.show {
|
|
transform: translateY(0); opacity: 1;
|
|
}
|
|
.rec-toast.success { border-left: 4px solid #22c55e; }
|
|
.rec-toast.info { border-left: 4px solid #3b82f6; }
|
|
.rec-toast.warn { border-left: 4px solid #f59e0b; }
|
|
.rec-toast.error { border-left: 4px solid #ef4444; }
|
|
.rec-toast .ico { font-size: 1.2rem; }
|
|
.rec-toast.success .ico { color: #22c55e; }
|
|
.rec-toast.info .ico { color: #3b82f6; }
|
|
.rec-toast.warn .ico { color: #f59e0b; }
|
|
.rec-toast.error .ico { color: #ef4444; }
|
|
|
|
/* ── Historial del paciente ── */
|
|
.hist-toggle-btn {
|
|
display: flex; align-items: center; gap: 6px;
|
|
width: 100%; padding: 6px 10px; margin-top: 8px;
|
|
background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px;
|
|
font-size: .78rem; font-weight: 600; color: #475569;
|
|
cursor: pointer; text-align: left; transition: background .15s;
|
|
}
|
|
.hist-toggle-btn:hover { background: #f1f5f9; }
|
|
.hist-toggle-btn .hist-chev { margin-left: auto; transition: transform .2s; }
|
|
.hist-toggle-btn.open .hist-chev { transform: rotate(180deg); }
|
|
.hist-pac-body {
|
|
border: 1px solid #e2e8f0; border-top: none;
|
|
border-radius: 0 0 8px 8px; background: #fff;
|
|
max-height: 260px; overflow-y: auto;
|
|
}
|
|
.pac-timeline { padding: 10px 12px; }
|
|
.timeline-item {
|
|
display: flex; gap: 10px; align-items: flex-start;
|
|
padding-bottom: 10px; position: relative;
|
|
}
|
|
.timeline-item:not(:last-child)::before {
|
|
content: ''; position: absolute; left: 6px; top: 14px;
|
|
width: 2px; bottom: 0; background: #e2e8f0;
|
|
}
|
|
.timeline-dot {
|
|
width: 14px; height: 14px; border-radius: 50%; flex-shrink: 0;
|
|
margin-top: 2px; border: 2px solid #fff;
|
|
box-shadow: 0 0 0 2px currentColor;
|
|
}
|
|
.timeline-content { flex: 1; min-width: 0; }
|
|
.timeline-fecha { font-size: .68rem; color: #94a3b8; }
|
|
.timeline-codigo { font-size: .82rem; font-weight: 700; }
|
|
.timeline-lugar { font-size: .73rem; color: #64748b; }
|
|
.timeline-eb { font-size: .62rem; padding: 1px 7px; border-radius: 99px; font-weight: 700; display: inline-block; margin-top: 2px; }
|
|
|
|
/* ── Botón "Llamar siguiente" en columna de cola ── */
|
|
.btn-llamar-cola {
|
|
width: 100%; padding: .65rem 1rem; border: none; border-radius: 10px;
|
|
background: #2563eb; color: #fff; font-weight: 700; font-size: .92rem;
|
|
cursor: pointer; display: flex; align-items: center; justify-content: center; gap: .5rem;
|
|
transition: background .15s, transform .1s; margin-top: .5rem;
|
|
}
|
|
.btn-llamar-cola:hover { background: #1d4ed8; }
|
|
.btn-llamar-cola:active { transform: scale(.98); }
|
|
.btn-llamar-cola:disabled { background: #94a3b8; cursor: not-allowed; }
|
|
|
|
/* ── Barra de acciones: botones primarios/secundarios touch-friendly ── */
|
|
.accion-primary-row { margin-bottom: .5rem; }
|
|
.btn-accion-primary {
|
|
width: 100%; padding: .8rem; border: none; border-radius: 11px;
|
|
font-size: 1rem; font-weight: 700; cursor: pointer;
|
|
display: flex; align-items: center; justify-content: center; gap: .45rem;
|
|
transition: background .15s, transform .1s, opacity .15s;
|
|
}
|
|
.btn-accion-primary:active { transform: scale(.98); }
|
|
.btn-accion-primary:disabled { opacity: .5; cursor: not-allowed; }
|
|
.btn-accion-primary.verde { background: #16a34a; color: #fff; }
|
|
.btn-accion-primary.verde:hover { background: #15803d; }
|
|
.btn-accion-primary.azul { background: #2563eb; color: #fff; }
|
|
.btn-accion-primary.azul:hover { background: #1d4ed8; }
|
|
.accion-secondary-row { display: flex; gap: .4rem; flex-wrap: wrap; }
|
|
.btn-accion-sec {
|
|
flex: 1; min-width: 80px; padding: .55rem .5rem; border-radius: 9px;
|
|
font-size: .82rem; font-weight: 600; cursor: pointer;
|
|
display: flex; align-items: center; justify-content: center; gap: .35rem;
|
|
border: 1.5px solid; background: transparent; transition: background .12s;
|
|
}
|
|
.btn-accion-sec.warning { color: #92400e; border-color: #fcd34d; }
|
|
.btn-accion-sec.warning:hover { background: #fffbeb; }
|
|
.btn-accion-sec.info { color: #1e40af; border-color: #93c5fd; }
|
|
.btn-accion-sec.info:hover { background: #eff6ff; }
|
|
.btn-accion-sec.danger { color: #991b1b; border-color: #fca5a5; }
|
|
.btn-accion-sec.danger:hover { background: #fef2f2; }
|
|
|
|
/* ── Mejoras de touch target (tablet) ── */
|
|
.consent-item { min-height: 46px; }
|
|
.pac-resultado { min-height: 50px; }
|
|
.cola-card { min-height: 62px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<?php
|
|
$SIDEBAR_TITLE = 'Turnero';
|
|
$SIDEBAR_ICON = 'fas fa-ticket-alt';
|
|
require_once __DIR__ . '/../../../shared/components/sidebar.php';
|
|
?>
|
|
|
|
<!-- Modal: Nuevo turno desde recepción -->
|
|
<div class="modal fade" id="modalNuevoTurno" tabindex="-1">
|
|
<div class="modal-dialog modal-dialog-centered modal-sm">
|
|
<div class="modal-content">
|
|
<div class="modal-header py-2">
|
|
<h6 class="modal-title fw-semibold"><i class="fas fa-plus-circle me-2 text-success"></i>Nuevo turno</h6>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="mb-3">
|
|
<label class="form-label small fw-semibold">Prioridad</label>
|
|
<select id="mnt-prio" class="form-select form-select-sm">
|
|
<?php foreach ($prioridades as $p): ?>
|
|
<option value="<?= $p['codigo'] ?>" data-color="<?= htmlspecialchars($p['color']) ?>">
|
|
<?= $p['codigo'] ?> — <?= htmlspecialchars($p['nombre']) ?>
|
|
</option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
<div class="mb-1">
|
|
<label class="form-label small fw-semibold">Cédula / Nombre <span class="text-muted fw-normal">(opcional)</span></label>
|
|
<input type="text" id="mnt-doc" class="form-control form-control-sm"
|
|
placeholder="Número de documento o nombre" maxlength="150">
|
|
</div>
|
|
<div id="mnt-msg" class="small mt-2 d-none"></div>
|
|
</div>
|
|
<div class="modal-footer py-2">
|
|
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Cancelar</button>
|
|
<button type="button" class="btn btn-success btn-sm" id="mnt-btn-crear" onclick="crearNuevoTurno()">
|
|
<i class="fas fa-ticket-alt me-1"></i>Crear turno
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<?php if (!$deskId && count($desksRecepcion) > 1): ?>
|
|
<!-- Modal selector de escritorio (se muestra si no hay desk_id en URL) -->
|
|
<div class="modal fade" id="modalDeskSelect" tabindex="-1" data-bs-backdrop="static" data-bs-keyboard="false">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-header" style="background:var(--doc-color,#1565c0);color:#fff">
|
|
<h5 class="modal-title"><i class="fas fa-desktop me-2"></i>Seleccione su escritorio</h5>
|
|
</div>
|
|
<div class="modal-body pb-2">
|
|
<p class="text-muted small mb-3">¿Desde qué escritorio de recepción está trabajando hoy?</p>
|
|
<div class="d-grid gap-2">
|
|
<?php foreach ($desksRecepcion as $d): ?>
|
|
<a href="?m=turnero&v=recepcion&desk_id=<?= $d['id'] ?>"
|
|
class="btn btn-outline-primary btn-lg">
|
|
<i class="fas fa-chair me-2"></i><?= htmlspecialchars($d['nombre']) ?>
|
|
</a>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
new bootstrap.Modal(document.getElementById('modalDeskSelect')).show();
|
|
});
|
|
</script>
|
|
<?php endif; ?>
|
|
|
|
<main class="main-content" style="padding:0">
|
|
|
|
<!-- ── Top bar ──────────────────────────────────────────── -->
|
|
<div class="content-header d-flex align-items-center justify-content-between px-3 py-2 border-bottom bg-white"
|
|
style="position:sticky;top:0;z-index:100">
|
|
<div class="d-flex align-items-center gap-2">
|
|
<i class="fas fa-concierge-bell text-primary"></i>
|
|
<strong><?= htmlspecialchars($deskNombre ?? 'Recepción') ?></strong>
|
|
<span id="badge-turno-activo" class="badge-turno-activo d-none">
|
|
Turno: <span id="badge-codigo">—</span>
|
|
</span>
|
|
</div>
|
|
<div class="d-flex align-items-center gap-2">
|
|
<span id="sigweb-badge" style="font-size:.75rem;color:#dc2626;font-weight:600;white-space:nowrap">
|
|
<span id="sigweb-dot" style="display:inline-block;width:7px;height:7px;border-radius:50%;background:#dc2626;margin-right:4px;vertical-align:middle"></span><span id="sigweb-lbl">Topaz desconectado</span>
|
|
</span>
|
|
<span class="text-muted small"><?= htmlspecialchars($adminNombre) ?></span>
|
|
<button class="btn btn-primary btn-sm" id="btn-llamar" onclick="llamarSiguiente()">
|
|
<i class="fas fa-bell me-1"></i>Llamar siguiente
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="rec-layout">
|
|
|
|
<!-- ══ Columna izquierda: cola ════════════════════════ -->
|
|
<div class="rec-cola">
|
|
<div class="rec-cola-header">
|
|
<div class="d-flex align-items-center justify-content-between">
|
|
<span class="fw-semibold text-secondary small">
|
|
<i class="fas fa-list-ol me-1"></i>EN ESPERA
|
|
</span>
|
|
<span class="badge bg-primary rounded-pill" id="badge-count">0</span>
|
|
</div>
|
|
<div class="d-flex gap-2">
|
|
<button class="btn-llamar-cola" id="btn-llamar-cola" onclick="llamarSiguiente()">
|
|
<i class="fas fa-bell"></i>Llamar siguiente
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-success" onclick="abrirModalNuevoTurno()" title="Crear nuevo turno">
|
|
<i class="fas fa-plus"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="cola-items" id="cola-items">
|
|
<div class="cola-vacia">
|
|
<i class="fas fa-hourglass-start fa-2x mb-2 d-block"></i>
|
|
Cola vacía
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ══ Columna derecha: ficha del turno ═══════════════ -->
|
|
<div class="rec-ficha" id="rec-ficha">
|
|
<button class="btn btn-sm btn-outline-secondary btn-volver-cola mb-3"
|
|
style="display:none" onclick="volverACola()">
|
|
<i class="fas fa-arrow-left"></i> Cola
|
|
</button>
|
|
|
|
<div class="ficha-placeholder" id="ficha-placeholder">
|
|
<i class="fas fa-ticket-alt fa-3x mb-3" style="color:#cbd5e1"></i>
|
|
<p class="mb-0 fw-semibold">Sin turno activo</p>
|
|
<small class="text-muted">Haga clic en <i class="fas fa-bell"></i> de un turno para llamarlo, o use "Llamar siguiente"</small>
|
|
</div>
|
|
|
|
<!-- Ficha de turno (oculta hasta llamar) -->
|
|
<div id="ficha-turno" class="d-none">
|
|
|
|
<!-- Encabezado del turno -->
|
|
<div class="d-flex align-items-center gap-3 mb-3">
|
|
<div id="ficha-prio-dot" class="prio-dot"
|
|
style="width:52px;height:52px;border-radius:12px;display:flex;align-items:center;
|
|
justify-content:center;font-size:1.6rem;font-weight:900;color:#fff;background:#3b82f6">
|
|
—
|
|
</div>
|
|
<div class="flex-grow-1">
|
|
<div class="h4 mb-0 fw-bold" id="ficha-codigo">—</div>
|
|
<div class="text-muted small" id="ficha-prio-nombre">—</div>
|
|
</div>
|
|
<button class="btn btn-outline-primary btn-sm" onclick="rellamarActivo()"
|
|
title="Volver a llamar este turno (suena en pantalla)">
|
|
<i class="fas fa-bell"></i> Re-llamar
|
|
</button>
|
|
</div>
|
|
|
|
<!-- ── Sección 1: Paciente ── -->
|
|
<!-- ── Número de orden ── -->
|
|
<div class="ficha-section" style="padding-bottom:10px;border-bottom:1px solid #f1f5f9;margin-bottom:4px">
|
|
<div class="d-flex align-items-center gap-2">
|
|
<label style="font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:#94a3b8;white-space:nowrap;margin:0">
|
|
<i class="fas fa-hashtag me-1"></i>N.° Orden
|
|
</label>
|
|
<input type="text" id="inp-consecutivo"
|
|
class="form-control form-control-sm"
|
|
style="max-width:140px;font-weight:700;font-size:.95rem;letter-spacing:.04em"
|
|
placeholder="Cargando…" autocomplete="off">
|
|
<button class="btn btn-outline-secondary btn-sm" title="Regenerar siguiente" onclick="recargarConsecutivo()" type="button">
|
|
<i class="fas fa-rotate-right" style="font-size:.75rem"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="ficha-section">
|
|
<h6><i class="fas fa-user me-1"></i>Paciente</h6>
|
|
|
|
<!-- Si el kiosko capturó un nombre -->
|
|
<div id="bloque-nombre-kiosko" class="d-none mb-2">
|
|
<small class="text-muted">Kiosko:</small>
|
|
<div class="fw-semibold" id="lbl-nombre-kiosko"></div>
|
|
<div id="bloque-pac-kiosko" class="d-none mt-1">
|
|
<small class="text-muted">Paciente:</small>
|
|
<div class="fw-semibold text-primary" id="lbl-pac-kiosko-nombre"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="bloque-pac-no-vinculado">
|
|
<div class="input-group mb-2">
|
|
<input type="text" id="inp-buscar-pac"
|
|
class="form-control form-control-sm"
|
|
placeholder="Buscar por nombre o documento…"
|
|
autocomplete="off">
|
|
<button class="btn btn-outline-secondary btn-sm" onclick="buscarPaciente()" type="button">
|
|
<i class="fas fa-search"></i>
|
|
</button>
|
|
</div>
|
|
<div id="lista-pacientes-res"></div>
|
|
</div>
|
|
|
|
<div id="bloque-pac-seleccionado" class="d-none">
|
|
<div class="pac-seleccionado">
|
|
<div>
|
|
<div class="fw-semibold" id="lbl-pac-nombre">—</div>
|
|
<div class="text-muted small" id="lbl-pac-doc">—</div>
|
|
<div class="text-muted small" id="lbl-pac-cel"></div>
|
|
</div>
|
|
<div class="d-flex gap-1">
|
|
<button class="btn btn-outline-primary btn-sm" onclick="abrirModalPaciente(pacienteActivo?.id)" title="Ver / Editar paciente">
|
|
<i class="fas fa-pen fa-xs"></i>
|
|
</button>
|
|
<button class="btn btn-outline-secondary btn-sm" onclick="cambiarPaciente()" title="Buscar otro paciente">
|
|
<i class="fas fa-exchange-alt fa-xs"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Historial colapsable del paciente -->
|
|
<div id="sec-historial-pac" class="d-none">
|
|
<button class="hist-toggle-btn" id="btn-hist-toggle" onclick="toggleHistorialPaciente()">
|
|
<i class="fas fa-history" style="color:#6366f1"></i>
|
|
Historial del paciente
|
|
<i class="fas fa-chevron-down hist-chev"></i>
|
|
</button>
|
|
<div id="hist-pac-body" class="hist-pac-body d-none">
|
|
<div id="hist-pac-content" class="pac-timeline">
|
|
<div class="text-center text-muted py-2 small">
|
|
<i class="fas fa-spinner fa-spin me-1"></i>Cargando…
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Embarazo -->
|
|
<div class="mt-2" id="bloque-embarazada">
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" id="chk-embarazada">
|
|
<label class="form-check-label small fw-semibold text-danger" for="chk-embarazada">
|
|
<i class="fas fa-baby me-1"></i>Paciente embarazada
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Médico tratante -->
|
|
<div class="mt-2" id="bloque-medico">
|
|
<div class="small fw-semibold text-secondary mb-1">
|
|
<i class="fas fa-user-md me-1"></i>Médico tratante <span class="text-muted fw-normal">(opcional)</span>
|
|
</div>
|
|
<div id="medico-seleccionado" class="d-none mb-1">
|
|
<span class="badge text-bg-light border" style="font-size:.8rem;padding:5px 9px" id="medico-badge-txt"></span>
|
|
<button type="button" class="btn btn-link btn-sm p-0 ms-1 text-danger" onclick="quitarMedico()" title="Quitar médico"><i class="fas fa-times"></i></button>
|
|
</div>
|
|
<div id="medico-buscador">
|
|
<input type="text" id="inp-buscar-medico" class="form-control form-control-sm"
|
|
placeholder="Buscar por nombre, código o doc…"
|
|
autocomplete="off" oninput="buscarMedico()"
|
|
onkeydown="if(event.key==='Enter'&&_medicoResultados.length){event.preventDefault();const m=_medicoResultados[0];seleccionarMedico(m.id,m.codigo,m.nombres+' '+m.apellidos,m.cod_especialidad||'');}">
|
|
<div id="medico-dropdown" class="position-relative">
|
|
<ul id="medico-resultados" class="list-unstyled mb-0 border rounded bg-white shadow-sm position-absolute w-100 d-none"
|
|
style="z-index:200;max-height:180px;overflow-y:auto;top:2px"></ul>
|
|
</div>
|
|
</div>
|
|
<input type="hidden" id="inp-medico-id">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Banner RIPS: exámenes detectados automáticamente -->
|
|
<div id="banner-rips" class="d-none mt-1 mb-1 p-2 rounded d-flex align-items-center justify-content-between gap-2"
|
|
style="background:#f0fdf4;border:1px solid #86efac;font-size:.82rem">
|
|
<div>
|
|
<i class="fas fa-flask text-success me-1"></i>
|
|
<span id="banner-rips-txt" class="fw-semibold"></span>
|
|
<span id="banner-rips-warn" class="d-none text-warning ms-2"></span>
|
|
</div>
|
|
<div class="d-flex gap-1 flex-shrink-0">
|
|
<button class="btn btn-success btn-sm py-0 px-2" onclick="cargarExamenesRips()">
|
|
<i class="fas fa-check me-1"></i>Cargar
|
|
</button>
|
|
<button class="btn btn-outline-secondary btn-sm py-0 px-2" onclick="descartarRips()">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── Sección 3: Exámenes ── -->
|
|
<div class="ficha-section">
|
|
<h6><i class="fas fa-vial me-1"></i>Exámenes solicitados
|
|
<span id="exam-count-badge" class="badge bg-primary ms-1 d-none"></span>
|
|
</h6>
|
|
<div class="form-check mb-2">
|
|
<input class="form-check-input" type="checkbox" id="chk-solo-muestras" onchange="toggleSoloMuestras()">
|
|
<label class="form-check-label small fw-semibold text-warning" for="chk-solo-muestras">
|
|
<i class="fas fa-vial me-1"></i>Solo entrega de muestras
|
|
</label>
|
|
</div>
|
|
<div id="wrap-examenes">
|
|
<select id="sel-examenes" multiple placeholder="Buscar y seleccionar exámenes…">
|
|
<?php foreach ($examenesAgrupados as $cat => $items): ?>
|
|
<optgroup label="<?= htmlspecialchars($cat) ?>">
|
|
<?php foreach ($items as $ex): ?>
|
|
<option value="<?= (int)$ex['id'] ?>"
|
|
data-consent="<?= (int)$ex['tiene_consentimiento'] ?>"
|
|
data-cups="<?= htmlspecialchars($ex['cups'] ?? '') ?>"
|
|
data-codigo="<?= htmlspecialchars($ex['codigo'] ?? '') ?>"
|
|
data-genero="<?= htmlspecialchars($ex['genero_permitido'] ?? '') ?>">
|
|
<?= htmlspecialchars($ex['codigo']) ?> — <?= htmlspecialchars($ex['nombre']) ?><?= $ex['cups'] ? ' ' . htmlspecialchars($ex['cups']) : '' ?>
|
|
</option>
|
|
<?php endforeach; ?>
|
|
</optgroup>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
<div class="mt-2">
|
|
<button class="btn btn-outline-secondary btn-sm" onclick="limpiarExamenes()">
|
|
<i class="fas fa-times me-1"></i>Limpiar selección
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── Sección 3.5: Empresa / Convenio ── -->
|
|
<div class="ficha-section" id="sec-empresa">
|
|
<h6><i class="fas fa-building me-1"></i>Empresa / Convenio <span class="text-muted fw-normal">(opcional)</span></h6>
|
|
<!-- Buscador empresa -->
|
|
<div id="empresa-seleccionada" class="d-none mb-2">
|
|
<div class="d-flex align-items-center gap-2 p-2 rounded border" style="background:#f0f9ff">
|
|
<div class="flex-grow-1">
|
|
<div class="fw-semibold small" id="empresa-badge-nombre"></div>
|
|
<div class="text-muted" style="font-size:.75rem" id="empresa-badge-meta"></div>
|
|
</div>
|
|
<button type="button" class="btn btn-link btn-sm p-0 text-danger" onclick="quitarEmpresa()" title="Quitar empresa">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div id="empresa-buscador">
|
|
<input type="text" id="inp-buscar-empresa" class="form-control form-control-sm"
|
|
placeholder="Buscar empresa por nombre o NIT…"
|
|
autocomplete="off" oninput="buscarEmpresa()"
|
|
onkeydown="if(event.key==='Enter'&&_empresaResultados.length){event.preventDefault();_seleccionarEmpresaIdx(0);}"
|
|
>
|
|
<div class="position-relative">
|
|
<ul id="empresa-resultados" class="list-unstyled mb-0 border rounded bg-white shadow-sm position-absolute w-100 d-none"
|
|
style="z-index:200;max-height:200px;overflow-y:auto;top:2px"></ul>
|
|
</div>
|
|
</div>
|
|
<input type="hidden" id="inp-empresa-nit">
|
|
|
|
<!-- Subgrupo (se muestra cuando hay subgrupos) -->
|
|
<div id="wrap-subgrupo" class="d-none mt-2">
|
|
<label class="form-label mb-1 small fw-semibold">Subgrupo</label>
|
|
<select id="sel-subgrupo" class="form-select form-select-sm" onchange="recalcularPrecios()">
|
|
<option value="">— Sin subgrupo —</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Autorización -->
|
|
<div id="wrap-autorizacion" class="d-none mt-2">
|
|
<label class="form-label mb-1 small fw-semibold text-warning">
|
|
<i class="fas fa-key me-1"></i>N.° Autorización <span class="text-danger">*</span>
|
|
</label>
|
|
<input type="text" id="inp-autorizacion" class="form-control form-control-sm"
|
|
placeholder="Número de autorización EPS" maxlength="100">
|
|
</div>
|
|
|
|
<!-- Diagnóstico -->
|
|
<div id="wrap-diag" class="d-none mt-2">
|
|
<label class="form-label mb-1 small fw-semibold">Diagnóstico (CIE-10)</label>
|
|
<input type="text" id="inp-diag" class="form-control form-control-sm"
|
|
placeholder="Código o descripción CIE-10…"
|
|
autocomplete="off"
|
|
oninput="buscarDiag()"
|
|
onblur="setTimeout(()=>document.getElementById('diag-resultados').classList.add('d-none'),150)"
|
|
onkeydown="if(event.key==='Enter'&&_diagResultados.length){event.preventDefault();seleccionarDiag(_diagResultados[0].cod_diag);}">
|
|
<div class="position-relative">
|
|
<ul id="diag-resultados" class="list-unstyled mb-0 border rounded bg-white shadow-sm position-absolute w-100 d-none"
|
|
style="z-index:200;max-height:200px;overflow-y:auto;top:2px"></ul>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tabla de precios calculados -->
|
|
<div id="panel-precios" class="d-none mt-3">
|
|
<div class="d-flex align-items-center justify-content-between mb-1">
|
|
<span class="small fw-semibold text-muted text-uppercase" style="font-size:.7rem;letter-spacing:.06em">
|
|
Precios
|
|
</span>
|
|
<span class="badge bg-light text-dark border small" id="precio-tarifa-label"></span>
|
|
</div>
|
|
<div id="tabla-precios" class="mb-1" style="font-size:.82rem"></div>
|
|
<div class="d-flex justify-content-between align-items-center pt-1 border-top">
|
|
<span class="fw-bold small">Total calculado</span>
|
|
<span class="fw-bold text-success" id="precio-total-calc">$0</span>
|
|
</div>
|
|
<div id="precio-dcto-row" class="d-none d-flex justify-content-between text-muted" style="font-size:.78rem">
|
|
<span>Descuento empresa (<span id="precio-dcto-pct"></span>%)</span>
|
|
<span id="precio-dcto-val" class="text-danger"></span>
|
|
</div>
|
|
<button type="button" class="btn btn-outline-primary btn-sm w-100 mt-2"
|
|
onclick="aplicarPrecioCalculado()" id="btn-aplicar-precio">
|
|
<i class="fas fa-check me-1"></i>Aplicar al cobro
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── Sección 4: Pago ── -->
|
|
<div class="ficha-section">
|
|
<h6><i class="fas fa-dollar-sign me-1"></i>Cobro (opcional)</h6>
|
|
<div class="row g-2">
|
|
<div class="col-6">
|
|
<input type="number" id="inp-total" class="form-control form-control-sm"
|
|
placeholder="Total $" step="100" min="0">
|
|
</div>
|
|
<div class="col-6">
|
|
<select id="sel-pago" class="form-select form-select-sm" onchange="togglePagoCombinado()">
|
|
<option value="">— Forma de pago —</option>
|
|
<option value="efectivo">Efectivo</option>
|
|
<option value="transferencia">Transferencia</option>
|
|
<option value="tarjeta">Tarjeta</option>
|
|
<option value="eps">EPS / Convenio</option>
|
|
<option value="cortesia">Cortesía</option>
|
|
<option value="bono">Bono</option>
|
|
<option value="combinado">Pago combinado</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<!-- N.º recibo tarjeta / transferencia -->
|
|
<div id="panel-recibo" class="d-none mt-2">
|
|
<input type="text" id="inp-recibo" class="form-control form-control-sm"
|
|
placeholder="N.º recibo / aprobación" maxlength="40">
|
|
</div>
|
|
<!-- Panel pago combinado -->
|
|
<div id="panel-combinado" class="d-none mt-2 p-2 border rounded" style="background:#f8fafc">
|
|
<div class="row g-2 align-items-center mb-1">
|
|
<div class="col-5"><label class="form-label mb-0 small fw-semibold"><i class="fas fa-money-bill-wave me-1 text-success"></i>Efectivo</label></div>
|
|
<div class="col-7"><input type="number" id="comb-efectivo" class="form-control form-control-sm" placeholder="$0" min="0" step="100" oninput="sumarCombinado()"></div>
|
|
</div>
|
|
<div class="row g-2 align-items-center mb-1">
|
|
<div class="col-5"><label class="form-label mb-0 small fw-semibold"><i class="fas fa-university me-1 text-primary"></i>Transferencia</label></div>
|
|
<div class="col-4"><input type="number" id="comb-transferencia" class="form-control form-control-sm" placeholder="$0" min="0" step="100" oninput="sumarCombinado()"></div>
|
|
<div class="col-3"><input type="text" id="comb-transferencia-ref" class="form-control form-control-sm" placeholder="Ref." maxlength="40"></div>
|
|
</div>
|
|
<div class="row g-2 align-items-center mb-1">
|
|
<div class="col-5"><label class="form-label mb-0 small fw-semibold"><i class="fas fa-credit-card me-1 text-warning"></i>Tarjeta</label></div>
|
|
<div class="col-4"><input type="number" id="comb-tarjeta" class="form-control form-control-sm" placeholder="$0" min="0" step="100" oninput="sumarCombinado()"></div>
|
|
<div class="col-3"><input type="text" id="comb-tarjeta-ref" class="form-control form-control-sm" placeholder="Ref." maxlength="40"></div>
|
|
</div>
|
|
<div class="row g-2 align-items-center">
|
|
<div class="col-5"><label class="form-label mb-0 small fw-semibold"><i class="fas fa-ticket-alt me-1 text-info"></i>Bono</label></div>
|
|
<div class="col-7"><input type="number" id="comb-bono" class="form-control form-control-sm" placeholder="$0" min="0" step="100" oninput="sumarCombinado()"></div>
|
|
</div>
|
|
</div>
|
|
<textarea id="inp-obs" class="form-control form-control-sm mt-2"
|
|
rows="2" placeholder="Observaciones…" maxlength="500"></textarea>
|
|
</div>
|
|
|
|
<!-- ── Sección 5: Consentimientos ── -->
|
|
<div class="ficha-section" id="sec-consentimientos" style="display:none">
|
|
<div class="d-flex align-items-center justify-content-between mb-2">
|
|
<h6 class="mb-0"><i class="fas fa-file-signature me-1"></i>Consentimientos informados</h6>
|
|
<div class="d-flex gap-1">
|
|
<button class="btn btn-sm btn-outline-secondary py-0 px-2"
|
|
onclick="refrescarConsentimientosLugar()"
|
|
title="Actualizar estado">
|
|
<i class="fas fa-sync-alt"></i>
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-primary py-0 px-2"
|
|
id="btn-reenviar-consent" onclick="enviarConsentimientosTodos()"
|
|
title="Enviar todos por WhatsApp">
|
|
<i class="fas fa-paper-plane me-1"></i>Enviar todos
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div id="lista-consentimientos"></div>
|
|
</div>
|
|
|
|
<!-- ── Lugar destino (al final, justo antes de acciones) ── -->
|
|
<div class="ficha-section">
|
|
<h6><i class="fas fa-map-marker-alt me-1"></i>Lugar destino</h6>
|
|
<select id="sel-lugar" class="form-select form-select-sm">
|
|
<option value="">— Seleccione destino —</option>
|
|
<?php foreach ($lugaresDestino as $ld): ?>
|
|
<option value="<?= (int)$ld['id'] ?>"><?= htmlspecialchars($ld['nombre']) ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- ── Acciones ── -->
|
|
<div class="ficha-acciones">
|
|
<div class="accion-primary-row">
|
|
<button class="btn-accion-primary verde" id="btn-guardar" onclick="guardarSolicitud()">
|
|
<i class="fas fa-save"></i>Guardar solicitud
|
|
</button>
|
|
<button class="btn-accion-primary azul d-none" id="btn-pasar-lugar" onclick="pasarALugar()">
|
|
<i class="fas fa-arrow-right"></i>Pasar a lugar
|
|
</button>
|
|
</div>
|
|
<div class="accion-secondary-row">
|
|
<button class="btn-accion-sec warning d-none" id="btn-enviar-consent" onclick="enviarConsentimientos()">
|
|
<i class="fas fa-paper-plane"></i>Consentimientos
|
|
</button>
|
|
<button class="btn-accion-sec info" id="btn-soltar" onclick="soltarTurno()" title="Liberar turno para que otro escritorio lo llame">
|
|
<i class="fas fa-undo-alt"></i>Soltar
|
|
</button>
|
|
<button class="btn-accion-sec danger" onclick="marcarAusente()">
|
|
<i class="fas fa-user-slash"></i>Ausente
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div><!-- /ficha-turno -->
|
|
</div><!-- /rec-ficha -->
|
|
|
|
</div><!-- /rec-layout -->
|
|
|
|
<!-- ══ Panel resumen toma de muestras ════════════════════════ -->
|
|
<div id="panel-muestras-resumen" style="padding:6px 12px;border-top:1px solid #e2e8f0;background:#f8fafc;display:flex;gap:8px;align-items:center;flex-wrap:wrap;min-height:38px">
|
|
<span class="text-muted small fw-semibold me-1" style="white-space:nowrap"><i class="fas fa-vial me-1"></i>Muestras:</span>
|
|
<div id="muestras-estaciones" class="d-flex gap-2 flex-wrap align-items-center"></div>
|
|
<span id="muestras-cola-badge" class="badge bg-warning text-dark ms-auto d-none" title="En espera de ser llamados"></span>
|
|
<span id="muestras-prog-badge" class="badge bg-info text-dark d-none" title="Esperando siguiente muestra (toma progresiva)"></span>
|
|
</div>
|
|
</main>
|
|
|
|
<!-- ═══ Modal firma enfermero ═══════════════════════════════════════ -->
|
|
<div class="modal fade" id="modalFirmaEnfermero" tabindex="-1" aria-hidden="true">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-header py-2">
|
|
<h6 class="modal-title mb-0"><i class="fas fa-pen-nib me-1"></i>Firma del enfermero</h6>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body text-center">
|
|
<p class="text-muted small mb-2" id="fe-nombre-consentimiento"></p>
|
|
<canvas id="fe-canvas" width="440" height="180"
|
|
style="border:1px solid #cbd5e1;border-radius:8px;touch-action:none;cursor:crosshair;max-width:100%"></canvas>
|
|
<div class="d-flex justify-content-between mt-2">
|
|
<button class="btn btn-sm btn-outline-secondary" onclick="feCanvas.limpiar()">
|
|
<i class="fas fa-eraser me-1"></i>Limpiar
|
|
</button>
|
|
<span class="text-muted small align-self-center">Firme en el recuadro</span>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer py-2">
|
|
<button type="button" class="btn btn-outline-secondary btn-sm" data-bs-dismiss="modal">Cancelar</button>
|
|
<button type="button" class="btn btn-primary btn-sm" onclick="feGuardarFirma()">
|
|
<i class="fas fa-check me-1"></i>Guardar firma
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ═══ Modal Paciente (crear / ver / editar) ════════════════════════════ -->
|
|
<div class="modal fade" id="modalPacienteRec" tabindex="-1">
|
|
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
|
<div class="modal-content border-0 shadow-lg overflow-hidden" style="border-radius:1rem">
|
|
|
|
<!-- Header con avatar -->
|
|
<div id="mpac-header"
|
|
style="background:linear-gradient(135deg,#1043a0 0%,#1565c0 100%);
|
|
color:#fff;padding:1.3rem 1.5rem 1rem;
|
|
display:flex;align-items:center;gap:.9rem">
|
|
<div id="mpac-avatar"
|
|
style="width:52px;height:52px;border-radius:50%;
|
|
background:rgba(255,255,255,.18);display:flex;
|
|
align-items:center;justify-content:center;
|
|
font-size:1.4rem;font-weight:800;color:#fff;
|
|
border:2px solid rgba(255,255,255,.35);flex-shrink:0">?</div>
|
|
<div class="flex-grow-1">
|
|
<div style="font-size:1rem;font-weight:700" id="mpac-titulo">Nuevo Paciente</div>
|
|
<div style="font-size:.75rem;opacity:.75" id="mpac-subtitulo">Completa los datos del paciente</div>
|
|
</div>
|
|
<button type="button" class="btn-close btn-close-white ms-auto" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
|
|
<div class="modal-body py-3 px-4">
|
|
<form id="form-paciente-rec" autocomplete="off">
|
|
<input type="hidden" id="mpac-id">
|
|
|
|
<!-- Datos personales -->
|
|
<div class="ficha-section mb-3">
|
|
<h6><i class="fas fa-id-card me-1"></i>Datos personales</h6>
|
|
<div class="row g-2">
|
|
<div class="col-12">
|
|
<div class="form-floating">
|
|
<input type="text" class="form-control form-control-sm" id="mpac-nombre"
|
|
required placeholder="Nombre completo"
|
|
oninput="mpacAvatar(this.value)">
|
|
<label>Nombre completo <span class="text-danger">*</span></label>
|
|
</div>
|
|
</div>
|
|
<div class="col-sm-4">
|
|
<div class="form-floating">
|
|
<select class="form-select form-select-sm" id="mpac-tipo-doc">
|
|
<option value="CC">CC — Cédula</option>
|
|
<option value="CE">CE — Extranjería</option>
|
|
<option value="TI">TI — Tarjeta Identidad</option>
|
|
<option value="PA">PA — Pasaporte</option>
|
|
<option value="DE">DE — Doc. Extranjero</option>
|
|
<option value="NIT">NIT</option>
|
|
<option value="RC">RC — Reg. Civil</option>
|
|
</select>
|
|
<label>Tipo documento</label>
|
|
</div>
|
|
</div>
|
|
<div class="col-sm-4">
|
|
<div class="form-floating">
|
|
<input type="text" class="form-control form-control-sm" id="mpac-doc" placeholder="Número documento">
|
|
<label>Número documento</label>
|
|
</div>
|
|
</div>
|
|
<div class="col-sm-4">
|
|
<div class="form-floating">
|
|
<input type="date" class="form-control form-control-sm" id="mpac-fnac" placeholder="Fecha nacimiento">
|
|
<label>Fecha nacimiento</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Contacto -->
|
|
<div class="ficha-section mb-3">
|
|
<h6><i class="fas fa-phone me-1"></i>Contacto</h6>
|
|
<div class="row g-2">
|
|
<div class="col-sm-5">
|
|
<label class="form-label small fw-semibold mb-1" style="color:#64748b">Teléfono</label>
|
|
<div class="input-group input-group-sm">
|
|
<select id="mpac-tel-prefijo" class="form-select" style="max-width:82px">
|
|
<option value="57" selected>🇨🇴 57</option>
|
|
<option value="58">🇻🇪 58</option>
|
|
<option value="1">🇺🇸 1</option>
|
|
</select>
|
|
<input type="tel" class="form-control" id="mpac-tel"
|
|
placeholder="3001234567" onblur="mpacVerificarWA()">
|
|
</div>
|
|
<div id="mpac-wa-badge" style="font-size:.78rem;min-height:18px;margin-top:3px"></div>
|
|
<input type="hidden" id="mpac-user-id">
|
|
</div>
|
|
<div class="col-sm-4">
|
|
<div class="form-floating">
|
|
<input type="email" class="form-control form-control-sm" id="mpac-email" placeholder="Email">
|
|
<label>Email</label>
|
|
</div>
|
|
</div>
|
|
<div class="col-sm-3">
|
|
<div class="form-floating">
|
|
<select class="form-select form-select-sm" id="mpac-genero">
|
|
<option value="">—</option>
|
|
<option value="M">Masculino</option>
|
|
<option value="F">Femenino</option>
|
|
<option value="O">Otro</option>
|
|
</select>
|
|
<label>Género</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Ubicación y EPS -->
|
|
<div class="ficha-section mb-2">
|
|
<h6><i class="fas fa-map-marker-alt me-1"></i>Ubicación y EPS</h6>
|
|
<div class="row g-2">
|
|
<div class="col-sm-4">
|
|
<div class="form-floating">
|
|
<select class="form-select form-select-sm" id="mpac-eps">
|
|
<option value="">— Sin EPS —</option>
|
|
</select>
|
|
<label>EPS</label>
|
|
</div>
|
|
</div>
|
|
<div class="col-sm-4">
|
|
<div class="form-floating">
|
|
<select class="form-select form-select-sm" id="mpac-ciudad">
|
|
<option value="Cúcuta">Cúcuta</option>
|
|
</select>
|
|
<label>Ciudad</label>
|
|
</div>
|
|
</div>
|
|
<div class="col-sm-4">
|
|
<div class="form-floating">
|
|
<input type="text" class="form-control form-control-sm" id="mpac-barrio" placeholder="Barrio">
|
|
<label>Barrio</label>
|
|
</div>
|
|
</div>
|
|
<div class="col-12">
|
|
<div class="form-floating">
|
|
<input type="text" class="form-control form-control-sm" id="mpac-dir" placeholder="Dirección">
|
|
<label>Dirección</label>
|
|
</div>
|
|
</div>
|
|
<div class="col-12">
|
|
<div class="form-floating">
|
|
<textarea class="form-control form-control-sm" id="mpac-notas"
|
|
placeholder="Notas internas" style="height:68px"></textarea>
|
|
<label>Notas internas</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</form>
|
|
</div>
|
|
|
|
<div class="modal-footer" style="background:#f8faff;border-top:1px solid #e8edf5;padding:.8rem 1.25rem">
|
|
<button type="button" class="btn btn-sm btn-light px-3" data-bs-dismiss="modal">
|
|
<i class="fas fa-times me-1"></i>Cancelar
|
|
</button>
|
|
<button type="button" class="btn btn-sm btn-primary px-4" id="mpac-btn-guardar" onclick="mpacGuardar()">
|
|
<i class="fas fa-save me-1"></i>Guardar paciente
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="rec-toast" id="rec-toast">
|
|
<span class="ico" id="toast-ico"><i class="fas fa-check-circle"></i></span>
|
|
<span id="toast-msg"></span>
|
|
</div>
|
|
|
|
<script>
|
|
// ── Estado ────────────────────────────────────────────────────
|
|
let turnoActivo = null;
|
|
let pacienteActivo = null;
|
|
let solicitudActiva = null;
|
|
let consentimientos = [];
|
|
let pollingColaId = null;
|
|
|
|
const API = '<?= BASE_URL ?>modules/turnero/api/';
|
|
const API_PAC = '<?= BASE_URL ?>api/lab/get_pacientes.php';
|
|
const BASE_WA = '<?= BASE_URL ?>';
|
|
const DESK_ID = <?= $deskId ?: 'null' ?>;
|
|
const DESK_NOMBRE = '<?= addslashes($deskNombre ?? 'Recepción') ?>';
|
|
|
|
// ── Arranque ──────────────────────────────────────────────────
|
|
let pollingConsentimientosId = null;
|
|
|
|
let examTS;
|
|
|
|
function _actualizarContadorExamenes() {
|
|
const badge = document.getElementById('exam-count-badge');
|
|
if (!badge || !examTS) return;
|
|
const n = examTS.items.length;
|
|
badge.textContent = n;
|
|
badge.classList.toggle('d-none', n === 0);
|
|
}
|
|
|
|
/**
|
|
* Limpia la selección de exámenes dejando contador y precios en sincronía.
|
|
* examTS.clear() quita los ítems en modo silencioso, así que no dispara
|
|
* onItemRemove: sin esto la insignia conserva el número anterior y el panel
|
|
* de precios sigue mostrando exámenes que ya no están seleccionados.
|
|
*/
|
|
function limpiarExamenes() {
|
|
if (!examTS) return;
|
|
examTS.clear();
|
|
_actualizarContadorExamenes();
|
|
recalcularPrecios();
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
examTS = new TomSelect('#sel-examenes', {
|
|
plugins: ['remove_button'],
|
|
maxOptions: null,
|
|
selectOnTab: false,
|
|
|
|
onItemAdd() { recalcularPrecios(); _actualizarContadorExamenes(); examTS.setTextboxValue(''); examTS.refreshOptions(false); examTS.close(); },
|
|
onItemRemove() { recalcularPrecios(); _actualizarContadorExamenes(); },
|
|
onType(str) {
|
|
if (!str) return;
|
|
requestAnimationFrame(() => {
|
|
const first = examTS.dropdown_content.querySelector('.option:not(.disabled)');
|
|
if (first) examTS.setActiveOption(first);
|
|
});
|
|
},
|
|
render: {
|
|
option(data, escape) {
|
|
const consent = data['data-consent'] == '1' || data.consent == '1';
|
|
const cups = data['data-cups'] || data.cups || '';
|
|
const gp = (data['data-genero'] || '').toUpperCase();
|
|
const pg = (pacienteActivo?.genero || '').toUpperCase();
|
|
const blocked = gp && pg && gp !== pg;
|
|
const gpLabel = gp === 'M' ? '♂ Solo hombres' : gp === 'F' ? '♀ Solo mujeres' : '';
|
|
return `<div class="d-flex align-items-center justify-content-between${blocked ? ' opacity-50' : ''}">
|
|
<span>${escape(data.text)}${cups ? `<span class="ts-exam-cups-drop">${escape(cups)}</span>` : ''}</span>
|
|
${blocked ? `<span class="ts-consent-badge bg-danger">${gpLabel}</span>` : consent ? '<span class="ts-consent-badge"><i class="fas fa-file-signature me-1"></i>Consentimiento</span>' : ''}
|
|
</div>`;
|
|
},
|
|
item(data, escape) {
|
|
const consent = data['data-consent'] == '1' || data.consent == '1';
|
|
const cups = data['data-cups'] || data.cups || '';
|
|
const codigo = data['data-codigo'] || data.codigo || '';
|
|
const label = cups || codigo;
|
|
return `<div class="ts-exam-nom">${label ? `<span class="ts-exam-cups">${escape(label)}</span>` : ''}${escape(data.text)}${consent ? '<span class="ts-consent-dot" title="Requiere consentimiento"> ⚠</span>' : ''}</div>`;
|
|
}
|
|
}
|
|
});
|
|
|
|
cargarCola();
|
|
pollingColaId = setInterval(cargarCola, 3000);
|
|
_cargarResumenMuestras();
|
|
setInterval(_cargarResumenMuestras, 7000);
|
|
let _pacBusqTimer;
|
|
const _inpPac = document.getElementById('inp-buscar-pac');
|
|
_inpPac.addEventListener('input', () => {
|
|
clearTimeout(_pacBusqTimer);
|
|
_pacBusqTimer = setTimeout(buscarPaciente, 300);
|
|
});
|
|
_inpPac.addEventListener('keydown', e => {
|
|
if (e.key !== 'Enter') return;
|
|
e.preventDefault();
|
|
const first = document.querySelector('#lista-pacientes-res .pac-resultado');
|
|
if (first) { first.click(); return; }
|
|
buscarPaciente();
|
|
});
|
|
document.getElementById('sel-lugar')
|
|
.addEventListener('change', onLugarChange);
|
|
document.addEventListener('visibilitychange', () => {
|
|
if (!document.hidden && turnoActivo) refrescarConsentimientosLugar();
|
|
});
|
|
});
|
|
|
|
// ── Polling automático de consentimientos cuando hay uno abierto ──
|
|
async function refrescarConsentimientosLugar() {
|
|
if (!turnoActivo) return;
|
|
try {
|
|
const res = await fetch(`${API}get_consentimientos.php?turno_id=${turnoActivo.id}`);
|
|
const json = await res.json();
|
|
if (json.ok && json.consentimientos) {
|
|
consentimientos = json.consentimientos;
|
|
renderConsentimientos(consentimientos);
|
|
}
|
|
} catch (_) {}
|
|
}
|
|
// ── Formularios del lugar (aparecen al seleccionar lugar) ────
|
|
// El dropdown sel-lugar es solo para elegir destino del paciente.
|
|
// Los consentimientos del desk se cargan via cargarConsentimientosDesk() y no dependen de este cambio.
|
|
function onLugarChange() {}
|
|
async function abrirFormularioLugar(formularioId, nombre) {
|
|
if (!turnoActivo) { mostrarError('Llama un turno primero.'); return; }
|
|
if (!pacienteActivo) { mostrarError('Vincula el paciente antes de abrir el consentimiento.'); return; }
|
|
|
|
try {
|
|
const res = await fetch(API + 'create_consent_token.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ turno_id: turnoActivo.id, formulario_id: formularioId, paciente_id: pacienteActivo?.id || null }),
|
|
});
|
|
const json = await res.json();
|
|
if (json.ok && (json.data?.url || json.url)) {
|
|
window.open(json.data?.url || json.url, '_blank');
|
|
// Polling automático para detectar la firma
|
|
if (!pollingConsentimientosId) {
|
|
pollingConsentimientosId = setInterval(refrescarConsentimientosLugar, 3000);
|
|
}
|
|
} else {
|
|
mostrarError(json.error || 'No se pudo generar el enlace de firma.');
|
|
}
|
|
} catch (err) {
|
|
mostrarError('Error al generar enlace: ' + err.message);
|
|
}
|
|
}
|
|
|
|
// ── Cola ──────────────────────────────────────────────────────
|
|
async function cargarCola() {
|
|
try {
|
|
const res = await fetch(API + 'get_cola.php?area=recepcion');
|
|
const json = await res.json();
|
|
if (!json.ok) return;
|
|
renderCola(json);
|
|
} catch (_) {}
|
|
}
|
|
|
|
function tiempoEspera(iso) {
|
|
if (!iso) return null;
|
|
const mins = Math.floor((Date.now() - new Date(iso).getTime()) / 60000);
|
|
if (mins < 1) return { txt: 'recién', cls: 'ok' };
|
|
if (mins < 10) return { txt: mins + ' min', cls: 'ok' };
|
|
if (mins < 25) return { txt: mins + ' min', cls: 'warn' };
|
|
return { txt: mins + ' min', cls: 'crit' };
|
|
}
|
|
|
|
function renderCola(snap) {
|
|
const lista = document.getElementById('cola-items');
|
|
const todos = snap.cola || [];
|
|
const badge = document.getElementById('badge-count');
|
|
|
|
// Filtrar: 'espera' todos; 'en_recepcion' solo los de este escritorio
|
|
const visibles = todos.filter(t =>
|
|
t.estado === 'espera' ||
|
|
(t.estado === 'en_recepcion' && (t.recepcion_desk_id === null || t.recepcion_desk_id == DESK_ID))
|
|
);
|
|
badge.textContent = visibles.length;
|
|
|
|
if (visibles.length === 0) {
|
|
lista.innerHTML = `<div class="cola-vacia">
|
|
<i class="fas fa-hourglass-start fa-2x mb-2 d-block"></i>Cola vacía</div>`;
|
|
return;
|
|
}
|
|
lista.innerHTML = visibles.map(t => {
|
|
const enAtencion = t.estado === 'en_recepcion';
|
|
const te = tiempoEspera(t.creado_at);
|
|
const badgeRow = enAtencion
|
|
? `<div class="t-badge-row"><span class="atencion-chip"><i class="fas fa-circle" style="font-size:.45rem;vertical-align:middle;margin-right:3px"></i>EN ATENCIÓN</span></div>`
|
|
: (te ? `<div class="t-badge-row"><span class="tiempo-chip ${te.cls}">${escHtml(te.txt)}</span></div>` : '');
|
|
const esPrioEspecial = t.prioridad_codigo !== 'E';
|
|
const prioStyle = esPrioEspecial && !enAtencion
|
|
? `border-left:4px solid ${t.prioridad_color};background:${t.prioridad_color}18;`
|
|
: '';
|
|
const clases = ['cola-card', turnoActivo?.id === t.id ? 'activo' : '', enAtencion ? 'en-atencion' : ''].filter(Boolean).join(' ');
|
|
return `
|
|
<div class="${clases}" data-id="${t.id}" style="${prioStyle}">
|
|
<div class="prio-dot" style="background:${t.prioridad_color};cursor:pointer" onclick="seleccionarTurno(${t.id})">${t.prioridad_codigo}</div>
|
|
<div class="turno-info" style="cursor:pointer" onclick="seleccionarTurno(${t.id})">
|
|
<div class="cod">${escHtml(t.codigo)}</div>
|
|
<div class="pac">${escHtml(t.paciente_nombre || 'Sin nombre')}</div>
|
|
${badgeRow}
|
|
</div>
|
|
<button onclick="llamarTurnoEspecifico(${t.id}, '${t.estado}')"
|
|
title="${enAtencion ? 'Re-llamar este turno' : 'Llamar este turno ahora'}"
|
|
style="flex-shrink:0;background:${enAtencion ? '#1d4ed8' : '#2563eb'};border:none;color:#fff;
|
|
border-radius:7px;padding:5px 9px;font-size:.78rem;cursor:pointer">
|
|
<i class="fas ${enAtencion ? 'fa-volume-up' : 'fa-bell'}"></i>
|
|
</button>
|
|
</div>`;
|
|
}).join('');
|
|
}
|
|
|
|
// ── Nuevo turno desde recepción ───────────────────────────────
|
|
let _modalNuevoTurno;
|
|
function abrirModalNuevoTurno() {
|
|
if (!_modalNuevoTurno) _modalNuevoTurno = new bootstrap.Modal(document.getElementById('modalNuevoTurno'));
|
|
document.getElementById('mnt-doc').value = '';
|
|
document.getElementById('mnt-msg').className = 'small mt-2 d-none';
|
|
document.getElementById('mnt-btn-crear').disabled = false;
|
|
// Pre-fill doc si hay turno activo
|
|
const doc = turnoActivo?.paciente_nombre;
|
|
if (doc) document.getElementById('mnt-doc').value = doc;
|
|
_modalNuevoTurno.show();
|
|
}
|
|
async function crearNuevoTurno() {
|
|
const btn = document.getElementById('mnt-btn-crear');
|
|
const msg = document.getElementById('mnt-msg');
|
|
const prio = document.getElementById('mnt-prio').value;
|
|
const doc = document.getElementById('mnt-doc').value.trim();
|
|
btn.disabled = true;
|
|
msg.className = 'small mt-2 d-none';
|
|
try {
|
|
const r = await fetch(API + 'create_turno.php', {
|
|
method: 'POST',
|
|
headers: {'Content-Type':'application/json'},
|
|
body: JSON.stringify({ prioridad_codigo: prio, paciente_nombre: doc || undefined }),
|
|
});
|
|
const j = await r.json();
|
|
if (j.ok) {
|
|
msg.className = 'small mt-2 text-success';
|
|
msg.textContent = `Turno ${j.turno.codigo} creado`;
|
|
setTimeout(() => _modalNuevoTurno.hide(), 1200);
|
|
} else {
|
|
msg.className = 'small mt-2 text-danger';
|
|
msg.textContent = j.error || 'Error al crear turno';
|
|
btn.disabled = false;
|
|
}
|
|
} catch {
|
|
msg.className = 'small mt-2 text-danger';
|
|
msg.textContent = 'Error de conexión';
|
|
btn.disabled = false;
|
|
}
|
|
}
|
|
|
|
// ── Llamar siguiente ──────────────────────────────────────────
|
|
async function llamarSiguiente() {
|
|
const btn = document.getElementById('btn-llamar');
|
|
const btn2 = document.getElementById('btn-llamar-cola');
|
|
if (btn) btn.disabled = true;
|
|
if (btn2) btn2.disabled = true;
|
|
mostrarToast('Consultando siguiente turno…', 'info', 1500);
|
|
try {
|
|
const res = await fetch(API + 'llamar_turno.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ area: 'recepcion', desk_id: DESK_ID }),
|
|
});
|
|
const json = await res.json();
|
|
if (!json.ok) { mostrarError(json.error); return; }
|
|
|
|
const turnoLlamado = json.turno ?? json.data?.turno;
|
|
if (!turnoLlamado) {
|
|
mostrarAviso('Cola vacía', 'No hay turnos en espera.');
|
|
resetPlaceholder();
|
|
return;
|
|
}
|
|
mostrarLlamando(turnoLlamado.codigo);
|
|
abrirFicha(turnoLlamado);
|
|
cargarCola();
|
|
} catch (err) {
|
|
mostrarError(err.message);
|
|
} finally {
|
|
if (btn) btn.disabled = false;
|
|
if (btn2) btn2.disabled = false;
|
|
}
|
|
}
|
|
|
|
// ── Seleccionar turno de la cola (ver ficha sin cambiar estado) ─────
|
|
async function seleccionarTurno(turnoId) {
|
|
try {
|
|
const res = await fetch(API + 'get_turno.php?id=' + turnoId);
|
|
const json = await res.json();
|
|
if (!json.ok) { mostrarError(json.error || 'No se pudo cargar el turno'); return; }
|
|
abrirFicha(json.turno ?? json.data?.turno);
|
|
} catch (err) {
|
|
mostrarError('Error al cargar turno: ' + err.message);
|
|
}
|
|
}
|
|
|
|
// ── Llamar turno específico (cambia estado → en_recepcion) ─────────
|
|
async function llamarTurnoEspecifico(turnoId, estadoActual) {
|
|
// Mostrar feedback inmediato
|
|
const card = document.querySelector(`.cola-card[data-id="${turnoId}"]`);
|
|
if (card) {
|
|
const cod = card.querySelector('.cod')?.textContent || '';
|
|
mostrarLlamando(cod);
|
|
}
|
|
// Si ya está en recepción: re-llamar (actualiza llamado_at para que la pantalla TV reactive)
|
|
if (estadoActual === 'en_recepcion') {
|
|
try {
|
|
const res = await fetch(API + 'rellamar.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ turno_id: turnoId, desk_id: DESK_ID }),
|
|
});
|
|
const json = await res.json();
|
|
if (json.ok && json.turno) { abrirFicha(json.turno); return; }
|
|
} catch (_) {}
|
|
// Fallback: solo abrir ficha
|
|
seleccionarTurno(turnoId);
|
|
return;
|
|
}
|
|
try {
|
|
const res = await fetch(API + 'cambiar_estado.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ turno_id: turnoId, nuevo_estado: 'en_recepcion', desk_id: DESK_ID }),
|
|
});
|
|
const json = await res.json();
|
|
if (!json.ok) { mostrarError(json.error); return; }
|
|
abrirFicha(json.turno);
|
|
cargarCola();
|
|
} catch (err) {
|
|
mostrarError('Error al llamar turno: ' + err.message);
|
|
}
|
|
}
|
|
|
|
// ── Re-llamar desde la ficha ────────────────────────────────
|
|
async function rellamarActivo() {
|
|
if (!turnoActivo) return;
|
|
mostrarLlamando(turnoActivo.codigo);
|
|
try {
|
|
await fetch(API + 'rellamar.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ turno_id: turnoActivo.id, desk_id: DESK_ID }),
|
|
});
|
|
} catch (_) {}
|
|
}
|
|
|
|
// ── Abrir ficha ───────────────────────────────────────────────
|
|
async function cargarConsecutivo() {
|
|
const inp = document.getElementById('inp-consecutivo');
|
|
if (!inp) return;
|
|
inp.value = '…';
|
|
try {
|
|
const r = await fetch(API + 'get_consecutivo.php?tipo=F');
|
|
const d = await r.json();
|
|
if (d.ok) inp.value = d.consecutivo;
|
|
} catch(_) { inp.value = ''; }
|
|
}
|
|
function recargarConsecutivo() { cargarConsecutivo(); }
|
|
|
|
function abrirFicha(turno) {
|
|
turnoActivo = turno;
|
|
pacienteActivo = null;
|
|
solicitudActiva = null;
|
|
|
|
document.getElementById('ficha-placeholder').classList.add('d-none');
|
|
document.getElementById('ficha-turno').classList.remove('d-none');
|
|
|
|
cargarConsecutivo();
|
|
|
|
// Datos del turno
|
|
document.getElementById('ficha-codigo').textContent = turno.codigo;
|
|
document.getElementById('ficha-prio-nombre').textContent = turno.prioridad_nombre || '';
|
|
const dot = document.getElementById('ficha-prio-dot');
|
|
dot.textContent = turno.prioridad_codigo || '—';
|
|
dot.style.background = turno.prioridad_color || '#3b82f6';
|
|
|
|
// Badge top
|
|
document.getElementById('badge-turno-activo').classList.remove('d-none');
|
|
document.getElementById('badge-codigo').textContent = turno.codigo;
|
|
|
|
// Nombre del kiosko (valor capturado en kiosko)
|
|
if (turno.paciente_nombre) {
|
|
document.getElementById('bloque-nombre-kiosko').classList.remove('d-none');
|
|
document.getElementById('lbl-nombre-kiosko').textContent = turno.paciente_nombre;
|
|
document.getElementById('bloque-pac-kiosko').classList.add('d-none');
|
|
document.getElementById('inp-buscar-pac').value = turno.paciente_nombre;
|
|
}
|
|
|
|
// Reset secciones
|
|
resetCheckboxes();
|
|
document.getElementById('sec-consentimientos').style.removeProperty('display');
|
|
document.getElementById('sec-consentimientos').style.display = 'none';
|
|
document.getElementById('btn-pasar-lugar').classList.add('d-none');
|
|
document.getElementById('btn-enviar-consent').classList.add('d-none');
|
|
document.getElementById('btn-guardar').classList.remove('d-none');
|
|
document.getElementById('btn-guardar').disabled = false;
|
|
desvincularPaciente();
|
|
|
|
// Auto-vincular paciente si ya viene con ID desde el kiosko
|
|
if (turno.paciente_id) {
|
|
fetch(`${API_PAC}?id=${turno.paciente_id}`)
|
|
.then(r => r.json())
|
|
.then(json => {
|
|
const data = (json.data || json.registros || [])[0];
|
|
if (data && !pacienteActivo) seleccionarPaciente(data);
|
|
})
|
|
.catch(() => {});
|
|
} else if (turno.paciente_nombre && /^\d{5,15}$/.test(turno.paciente_nombre.trim())) {
|
|
// El kiosko capturó una cédula como nombre — consultar RIPS de inmediato
|
|
consultarExamenesRips(turno.paciente_nombre.trim());
|
|
}
|
|
|
|
// Cargar consentimientos del desk actual para este turno
|
|
if (DESK_ID) cargarConsentimientosDesk(turno.id);
|
|
mostrarFichaMobile();
|
|
}
|
|
|
|
// ── Cargar consentimientos del desk para el turno ─────────────
|
|
async function cargarConsentimientosDesk(turnoId) {
|
|
const sec = document.getElementById('sec-consentimientos');
|
|
const lista = document.getElementById('lista-consentimientos');
|
|
|
|
try {
|
|
// 1. Si el turno ya tiene registros creados, mostrarlos con su estado real
|
|
const resExist = await fetch(`${API}get_consentimientos.php?turno_id=${turnoId}`);
|
|
const jsonExist = await resExist.json();
|
|
if (jsonExist.ok && jsonExist.consentimientos?.length) {
|
|
consentimientos = jsonExist.consentimientos.map(c => ({
|
|
...c,
|
|
formulario_nombre: c.formulario_nombre || c.nombre,
|
|
}));
|
|
renderConsentimientos(consentimientos);
|
|
sec.style.display = '';
|
|
return;
|
|
}
|
|
|
|
// 2. Aún no existen registros: mostrar los configurados para el desk como pendientes
|
|
const resDesk = await fetch(`${API}get_lugar_formularios.php?lugar_id=${DESK_ID}`);
|
|
const jsonDesk = await resDesk.json();
|
|
if (!jsonDesk.ok || !jsonDesk.formularios?.length) {
|
|
sec.style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
lista.innerHTML = jsonDesk.formularios.map(f => {
|
|
const nom = escHtml(f.nombre);
|
|
return `<div class="consent-item pendiente">
|
|
<i class="fas fa-file-signature"></i>
|
|
<span class="c-nom">${nom}</span>
|
|
<span class="c-badge">Pendiente</span>
|
|
<div class="consent-acciones">
|
|
<button class="btn btn-outline-primary"
|
|
onclick="abrirFormularioLugar(${f.id}, '${nom.replace(/'/g, "\\'")}')"
|
|
title="Abrir formulario en nueva pestaña">
|
|
<i class="fas fa-external-link-alt"></i> Firmar
|
|
</button>
|
|
</div>
|
|
</div>`;
|
|
}).join('');
|
|
sec.style.display = '';
|
|
} catch (_) {
|
|
sec.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
// ── Buscador de paciente ──────────────────────────────────────
|
|
async function buscarPaciente() {
|
|
const q = document.getElementById('inp-buscar-pac').value.trim();
|
|
if (q.length < 2) return;
|
|
try {
|
|
const res = await fetch(`${API_PAC}?busqueda=${encodeURIComponent(q)}&limit=8`);
|
|
const json = await res.json();
|
|
const lista = document.getElementById('lista-pacientes-res');
|
|
const datos = json.data || json.registros || [];
|
|
if (!datos.length) {
|
|
lista.innerHTML = `<div class="text-muted small mt-1">Sin resultados.
|
|
<a href="#" onclick="abrirModalPaciente(null,'${escHtml(q).replace(/'/g,"\\'")}');return false">Crear nuevo</a></div>`;
|
|
return;
|
|
}
|
|
lista.innerHTML = datos.map(p => `
|
|
<div class="pac-resultado" onclick="seleccionarPaciente(${JSON.stringify(p).replace(/"/g,'"')})">
|
|
<div class="pac-nombre">${escHtml(p.nombre_completo||p.full_name||p.nombre||'')}</div>
|
|
<div class="pac-doc">${escHtml(p.tipo_documento||'')} ${escHtml(p.documento||p.numero_documento||'')}
|
|
${p.telefono||p.celular ? '· ' + escHtml(p.telefono||p.celular) : ''}</div>
|
|
</div>`).join('');
|
|
} catch (_) {}
|
|
}
|
|
|
|
function seleccionarPaciente(pac) {
|
|
pacienteActivo = pac;
|
|
// Persistir vínculo en BD para que create_consent_token pueda verificar paciente_id
|
|
if (turnoActivo?.id && pac?.id) {
|
|
fetch(API + 'vincular_paciente.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ turno_id: turnoActivo.id, paciente_id: pac.id }),
|
|
}).catch(() => {});
|
|
}
|
|
document.getElementById('bloque-pac-no-vinculado').classList.add('d-none');
|
|
document.getElementById('bloque-pac-seleccionado').classList.remove('d-none');
|
|
const esMasculino = (pac.genero || '').toUpperCase() === 'M';
|
|
const bloqueEmb = document.getElementById('bloque-embarazada');
|
|
bloqueEmb.style.display = esMasculino ? 'none' : '';
|
|
if (esMasculino) document.getElementById('chk-embarazada').checked = false;
|
|
const pacNombre = (pac.full_name || pac.nombre_completo || (pac.nombre||'') + ' ' + (pac.apellido||'')).trim();
|
|
document.getElementById('lbl-pac-nombre').textContent = pacNombre;
|
|
document.getElementById('lbl-pac-doc').textContent =
|
|
(pac.tipo_documento||'') + ' ' + (pac.documento||pac.numero_documento||'');
|
|
document.getElementById('lbl-pac-cel').textContent =
|
|
pac.telefono || pac.celular || '';
|
|
document.getElementById('lista-pacientes-res').innerHTML = '';
|
|
// Mostrar nombre del paciente vinculado debajo del valor del kiosko
|
|
if (!document.getElementById('bloque-nombre-kiosko').classList.contains('d-none')) {
|
|
document.getElementById('lbl-pac-kiosko-nombre').textContent = pacNombre;
|
|
document.getElementById('bloque-pac-kiosko').classList.remove('d-none');
|
|
}
|
|
|
|
// Mostrar historial del paciente
|
|
const secHist = document.getElementById('sec-historial-pac');
|
|
secHist.classList.remove('d-none');
|
|
// Resetear estado colapsado
|
|
document.getElementById('hist-pac-body').classList.add('d-none');
|
|
document.getElementById('btn-hist-toggle').classList.remove('open');
|
|
// Precargar en background (usuario decide si abre)
|
|
_historialPacienteCargado = false;
|
|
_historialPacienteId = pac.id;
|
|
|
|
// Consultar exámenes recientes en RIPS (últimos 5 min)
|
|
const cedula = (pac.numero_documento || pac.documento || '').toString().trim();
|
|
if (cedula) consultarExamenesRips(cedula);
|
|
filtrarExamenesPorGenero();
|
|
}
|
|
|
|
function filtrarExamenesPorGenero() {
|
|
if (!examTS) return;
|
|
const genero = (pacienteActivo?.genero || '').toUpperCase();
|
|
const bloqueados = [];
|
|
Object.entries(examTS.options).forEach(([val, opt]) => {
|
|
const gp = (opt['data-genero'] || '').toUpperCase();
|
|
const incompatible = !!(gp && genero && gp !== genero);
|
|
opt.disabled = incompatible;
|
|
if (incompatible && examTS.items.includes(String(val))) bloqueados.push(String(val));
|
|
});
|
|
bloqueados.forEach(id => examTS.removeItem(id, true));
|
|
if (bloqueados.length) recalcularPrecios();
|
|
examTS.clearCache();
|
|
examTS.refreshOptions(false);
|
|
}
|
|
|
|
// ── Exámenes desde RIPS ───────────────────────────────────────
|
|
let _ripsData = null;
|
|
|
|
async function consultarExamenesRips(cedula) {
|
|
_ripsData = null;
|
|
document.getElementById('banner-rips').classList.add('d-none');
|
|
if (!cedula) return;
|
|
try {
|
|
const r = await fetch(`${BASE_WA}api/lab/get_examenes_rips.php?cedula=${encodeURIComponent(cedula)}`);
|
|
const d = await r.json();
|
|
if (!d.ok || !d.encontrados?.length) return;
|
|
_ripsData = d;
|
|
const hora = d.hora ? ' (' + String(d.hora).slice(0, 5) + ')' : '';
|
|
const warn = document.getElementById('banner-rips-warn');
|
|
if (d.no_mapeados?.length) {
|
|
warn.textContent = `⚠ Sin mapeo: ${d.no_mapeados.join(', ')}`;
|
|
warn.classList.remove('d-none');
|
|
} else {
|
|
warn.classList.add('d-none');
|
|
}
|
|
// Si los exámenes vienen del cache local (enviados por RIPS scheduler) se cargan
|
|
// automáticamente. Si vienen del pull en vivo se muestra el banner para confirmación.
|
|
if (d.fuente === 'cache') {
|
|
await cargarExamenesRips();
|
|
} else {
|
|
document.getElementById('banner-rips-txt').textContent =
|
|
`${d.encontrados.length} examen(es) de RIPS${hora} — ¿Cargar?`;
|
|
document.getElementById('banner-rips').classList.remove('d-none');
|
|
}
|
|
} catch (_) {}
|
|
}
|
|
|
|
async function cargarExamenesRips() {
|
|
if (!_ripsData?.encontrados?.length || !examTS) return;
|
|
// Se agregan en modo silencioso para no recalcular precios en cada examen;
|
|
// el recálculo y el contador se hacen una sola vez al terminar.
|
|
_ripsData.encontrados.forEach(e => examTS.addItem(String(e.exam_tipo_id), true));
|
|
_actualizarContadorExamenes();
|
|
if (_ripsData.diagnostico_cod) {
|
|
document.getElementById('inp-diag').value = _ripsData.diagnostico_cod;
|
|
}
|
|
if (_ripsData.medico) {
|
|
const m = _ripsData.medico;
|
|
seleccionarMedico(m.id, m.codigo, m.nombre, m.especialidad || '');
|
|
}
|
|
if (_ripsData.empresa) {
|
|
await seleccionarEmpresa(_ripsData.empresa);
|
|
} else {
|
|
recalcularPrecios();
|
|
}
|
|
if (_ripsData.valor_total > 0) {
|
|
document.getElementById('inp-total').value = Math.round(_ripsData.valor_total);
|
|
}
|
|
// Marcar registro RIPS como consumido para no reutilizarlo en otro turno
|
|
const cedula = pacienteActivo?.numero_documento || pacienteActivo?.documento
|
|
|| turnoActivo?.paciente_nombre || '';
|
|
if (cedula && turnoActivo?.id) {
|
|
fetch(`${BASE_WA}api/lab/marcar_rips_usado.php`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ cedula, turno_id: turnoActivo.id }),
|
|
}).catch(() => {});
|
|
}
|
|
descartarRips();
|
|
}
|
|
|
|
function descartarRips() {
|
|
_ripsData = null;
|
|
document.getElementById('banner-rips').classList.add('d-none');
|
|
}
|
|
|
|
function togglePagoCombinado() {
|
|
const metodo = document.getElementById('sel-pago').value;
|
|
const combinado = metodo === 'combinado';
|
|
const tarjeta = metodo === 'tarjeta';
|
|
const transferencia = metodo === 'transferencia';
|
|
const showRecibo = tarjeta || transferencia;
|
|
document.getElementById('panel-combinado').classList.toggle('d-none', !combinado);
|
|
document.getElementById('panel-recibo').classList.toggle('d-none', !showRecibo);
|
|
document.getElementById('inp-recibo').placeholder = transferencia
|
|
? 'N.º comprobante transferencia' : 'N.º recibo / aprobación tarjeta';
|
|
if (!showRecibo) document.getElementById('inp-recibo').value = '';
|
|
const inp = document.getElementById('inp-total');
|
|
inp.readOnly = combinado;
|
|
inp.style.background = combinado ? '#e9ecef' : '';
|
|
if (combinado) sumarCombinado();
|
|
}
|
|
function sumarCombinado() {
|
|
const v = id => parseFloat(document.getElementById(id).value) || 0;
|
|
const total = v('comb-efectivo') + v('comb-transferencia') + v('comb-tarjeta') + v('comb-bono');
|
|
document.getElementById('inp-total').value = total || '';
|
|
}
|
|
function resetPagoCombinado() {
|
|
['comb-efectivo','comb-transferencia','comb-transferencia-ref','comb-tarjeta','comb-tarjeta-ref','comb-bono'].forEach(id => document.getElementById(id).value = '');
|
|
document.getElementById('panel-combinado').classList.add('d-none');
|
|
const inp = document.getElementById('inp-total');
|
|
inp.readOnly = false;
|
|
inp.style.background = '';
|
|
}
|
|
|
|
function desvincularPaciente() {
|
|
pacienteActivo = null;
|
|
_historialPacienteId = null;
|
|
descartarRips();
|
|
_historialPacienteCargado = false;
|
|
document.getElementById('bloque-pac-no-vinculado').classList.remove('d-none');
|
|
document.getElementById('bloque-pac-seleccionado').classList.add('d-none');
|
|
document.getElementById('bloque-pac-kiosko').classList.add('d-none');
|
|
document.getElementById('sec-historial-pac').classList.add('d-none');
|
|
document.getElementById('hist-pac-body').classList.add('d-none');
|
|
document.getElementById('btn-hist-toggle').classList.remove('open');
|
|
document.getElementById('lista-pacientes-res').innerHTML = '';
|
|
document.getElementById('inp-buscar-pac').value = '';
|
|
document.getElementById('chk-embarazada').checked = false;
|
|
document.getElementById('bloque-embarazada').style.display = '';
|
|
}
|
|
|
|
function cambiarPaciente() {
|
|
desvincularPaciente();
|
|
setTimeout(() => document.getElementById('inp-buscar-pac').focus(), 80);
|
|
}
|
|
|
|
// ── Historial del paciente ──────────────────────────────────────
|
|
let _historialPacienteId = null;
|
|
let _historialPacienteCargado = false;
|
|
|
|
async function toggleHistorialPaciente() {
|
|
const body = document.getElementById('hist-pac-body');
|
|
const btn = document.getElementById('btn-hist-toggle');
|
|
const open = !body.classList.contains('d-none');
|
|
body.classList.toggle('d-none', open);
|
|
btn.classList.toggle('open', !open);
|
|
if (!open && !_historialPacienteCargado && _historialPacienteId) {
|
|
_historialPacienteCargado = true;
|
|
await cargarHistorialPaciente(_historialPacienteId);
|
|
}
|
|
}
|
|
|
|
async function cargarHistorialPaciente(pacienteId) {
|
|
const content = document.getElementById('hist-pac-content');
|
|
content.innerHTML = '<div class="text-center text-muted py-2 small"><i class="fas fa-spinner fa-spin me-1"></i>Cargando historial…</div>';
|
|
try {
|
|
const res = await fetch(`${API}get_historial.php?paciente_id=${pacienteId}&per_page=5&page=1`);
|
|
const json = await res.json();
|
|
if (!json.ok || !json.turnos || !json.turnos.length) {
|
|
content.innerHTML = '<div class="text-center text-muted py-3 small"><i class="fas fa-inbox me-1"></i>Sin visitas anteriores registradas</div>';
|
|
return;
|
|
}
|
|
content.innerHTML = renderHistorialTimeline(json.turnos, json.total);
|
|
} catch(_) {
|
|
content.innerHTML = '<div class="text-center text-danger py-2 small">Error al cargar historial</div>';
|
|
}
|
|
}
|
|
|
|
function renderHistorialTimeline(turnos, total) {
|
|
const ESTADO_LBL = {
|
|
espera:'Espera', en_recepcion:'Recepción', en_espera_lugar:'Esp. lugar',
|
|
en_servicio:'En servicio', finalizado:'Finalizado', ausente:'Ausente', cancelado:'Cancelado',
|
|
};
|
|
const ESTADO_STYLE = {
|
|
finalizado: 'background:#f1f5f9;color:#334155',
|
|
ausente: 'background:#fee2e2;color:#991b1b',
|
|
cancelado: 'background:#f1f5f9;color:#9ca3af',
|
|
en_servicio: 'background:#dcfce7;color:#166534',
|
|
en_espera_lugar:'background:#fde68a;color:#92400e',
|
|
en_recepcion: 'background:#dbeafe;color:#1e40af',
|
|
espera: 'background:#fef9c3;color:#78350f',
|
|
};
|
|
const CONS_ICON = {
|
|
pendiente: '📋', enviado: '📤', visto: '👁', firmado: '✅', rechazado: '❌'
|
|
};
|
|
const totalLabel = total > turnos.length ? `<div class="text-end px-3 pb-1" style="font-size:.67rem;color:#94a3b8">Mostrando ${turnos.length} de ${total} visitas</div>` : '';
|
|
const items = turnos.map(t => {
|
|
const color = t.prioridad_color || '#6366f1';
|
|
const lugar = escHtml(t.lugar_nombre || 'Recepción');
|
|
const fecha = t.fecha_sesion || '—';
|
|
const est = t.estado || '';
|
|
const eStyle = ESTADO_STYLE[est] || 'background:#f1f5f9;color:#334155';
|
|
const eLbl = ESTADO_LBL[est] || est;
|
|
const mins = t.servicio_min != null ? `<span style="font-size:.65rem;color:#94a3b8;margin-left:4px">${t.servicio_min} min</span>` : '';
|
|
|
|
// Exámenes
|
|
let examStr = '';
|
|
if (t.examenes && t.examenes.length) {
|
|
examStr = `<div style="margin-top:6px;font-size:.75rem">
|
|
<strong style="color:#64748b">Exámenes:</strong>
|
|
<div style="color:#475569">${t.examenes.map(e => escHtml(e.nombre)).join(', ')}</div>
|
|
</div>`;
|
|
}
|
|
|
|
// Comentarios
|
|
let comStr = '';
|
|
if (t.comentarios && t.comentarios.length) {
|
|
comStr = `<div style="margin-top:6px;font-size:.72rem;border-top:1px solid #e2e8f0;padding-top:6px">
|
|
<strong style="color:#64748b">📝 Últimos comentarios:</strong>
|
|
<div style="color:#475569;margin-top:4px">${t.comentarios.map(c =>
|
|
`<div style="margin-bottom:3px"><strong>${escHtml(c.usuario_nombre)}</strong>: ${escHtml(c.comentario)}</div>`
|
|
).join('')}</div>
|
|
</div>`;
|
|
}
|
|
|
|
return `<div class="timeline-item">
|
|
<div class="timeline-dot" style="color:${color};background:${color}"></div>
|
|
<div class="timeline-content">
|
|
<div class="timeline-fecha">${escHtml(fecha)}</div>
|
|
<div class="timeline-codigo" style="color:${color}">${escHtml(t.codigo)}</div>
|
|
<div class="timeline-lugar">${lugar}</div>
|
|
<span class="timeline-eb" style="${eStyle}">${escHtml(eLbl)}</span>${mins}
|
|
${examStr}${comStr}
|
|
</div>
|
|
</div>`;
|
|
}).join('');
|
|
return `<div class="pac-timeline">${items}</div>${totalLabel}`;
|
|
}
|
|
|
|
// ── Guardar solicitud ─────────────────────────────────────────
|
|
async function guardarSolicitud() {
|
|
if (!turnoActivo) return;
|
|
if (!pacienteActivo) { mostrarError('Seleccione un paciente antes de guardar.'); return; }
|
|
|
|
const lugarId = parseInt(document.getElementById('sel-lugar').value);
|
|
if (!lugarId) { mostrarError('Seleccione el lugar destino.'); return; }
|
|
|
|
const soloMuestras = document.getElementById('chk-solo-muestras').checked;
|
|
|
|
// ✅ VALIDACIÓN: verificar que los consentimientos del lugar estén firmados (omitir en solo muestras)
|
|
if (!soloMuestras) {
|
|
const consentPendientes = Array.from(document.querySelectorAll('.consent-item.pendiente, .consent-item.enviado, .consent-item.visto')).length;
|
|
if (consentPendientes > 0) {
|
|
mostrarError('⚠️ El paciente debe firmar los consentimientos del lugar antes de guardar la solicitud.');
|
|
return;
|
|
}
|
|
}
|
|
const examIds = examTS ? examTS.getValue().map(v => parseInt(v)) : [];
|
|
if (!soloMuestras && !examIds.length) { mostrarError('Seleccione al menos un examen.'); return; }
|
|
|
|
const btn = document.getElementById('btn-guardar');
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Guardando…';
|
|
|
|
try {
|
|
const res = await fetch(API + 'create_solicitud.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
turno_id: turnoActivo.id,
|
|
paciente_id: pacienteActivo.id,
|
|
lugar_id: lugarId,
|
|
exam_tipo_ids: examIds,
|
|
numero_orden: document.getElementById('inp-consecutivo').value.trim() || null,
|
|
total_cobrado: parseFloat(document.getElementById('inp-total').value) || null,
|
|
metodo_pago: document.getElementById('sel-pago').value || null,
|
|
numero_recibo: document.getElementById('inp-recibo').value.trim() || null,
|
|
pagos_detalle: document.getElementById('sel-pago').value === 'combinado' ? {
|
|
efectivo: parseFloat(document.getElementById('comb-efectivo').value) || 0,
|
|
transferencia: parseFloat(document.getElementById('comb-transferencia').value) || 0,
|
|
ref_transferencia: document.getElementById('comb-transferencia-ref').value.trim() || null,
|
|
tarjeta: parseFloat(document.getElementById('comb-tarjeta').value) || 0,
|
|
ref_tarjeta: document.getElementById('comb-tarjeta-ref').value.trim() || null,
|
|
bono: parseFloat(document.getElementById('comb-bono').value) || 0,
|
|
} : null,
|
|
observaciones: document.getElementById('inp-obs').value.trim() || null,
|
|
embarazada: document.getElementById('chk-embarazada').checked ? 1 : 0,
|
|
solo_muestras: soloMuestras ? 1 : 0,
|
|
medico_id: parseInt(document.getElementById('inp-medico-id').value) || null,
|
|
nit_empresa: document.getElementById('inp-empresa-nit').value.trim() || null,
|
|
subgrupo_id: parseInt(document.getElementById('sel-subgrupo').value) || null,
|
|
autorizacion: document.getElementById('inp-autorizacion').value.trim() || null,
|
|
diag_ppal: document.getElementById('inp-diag').value.trim() || null,
|
|
items_precio: _preciosActivos.length ? _preciosActivos : null,
|
|
}),
|
|
});
|
|
const json = await res.json();
|
|
if (!json.ok) { mostrarError(json.error); return; }
|
|
|
|
solicitudActiva = json.solicitud ?? json.data?.solicitud;
|
|
consentimientos = json.consentimientos_requeridos ?? json.data?.consentimientos_requeridos ?? [];
|
|
|
|
// Asegurar que turnoActivo tiene el estado correcto (en_recepcion)
|
|
if (turnoActivo) turnoActivo.estado = 'en_recepcion';
|
|
|
|
btn.innerHTML = '<i class="fas fa-check me-1"></i>Guardado';
|
|
|
|
// Confirmación de recepción generada
|
|
if (json.recepcion_id) {
|
|
mostrarToast(`Recepción #${json.recepcion_id} generada`, 'ok', 3000);
|
|
}
|
|
|
|
// Detener polling de consentimientos (ya se guardó)
|
|
if (pollingConsentimientosId) {
|
|
clearInterval(pollingConsentimientosId);
|
|
pollingConsentimientosId = null;
|
|
}
|
|
|
|
// Siempre pasar al lugar directamente; consentimientos se gestionan en toma de muestras
|
|
await pasarALugar();
|
|
|
|
} catch (err) {
|
|
mostrarError(err.message);
|
|
btn.disabled = false;
|
|
btn.innerHTML = '<i class="fas fa-save me-1"></i>Guardar solicitud';
|
|
}
|
|
}
|
|
|
|
// ── Labels / iconos para estados ─────────────────────────────
|
|
const CONSENT_META = {
|
|
firmado : { cls:'firmado', ico:'fa-check-circle', lbl:'Firmado' },
|
|
enviado : { cls:'enviado', ico:'fa-envelope', lbl:'Enviado' },
|
|
visto : { cls:'visto', ico:'fa-eye', lbl:'Visto' },
|
|
rechazado : { cls:'rechazado', ico:'fa-ban', lbl:'Rechazado' },
|
|
pendiente : { cls:'pendiente', ico:'fa-clock', lbl:'Pendiente' },
|
|
cerrado_anticipado : { cls:'rechazado', ico:'fa-exclamation-triangle', lbl:'Cerrado anticipadamente' },
|
|
};
|
|
|
|
function renderConsentimientos(lista) {
|
|
const el = document.getElementById('lista-consentimientos');
|
|
// Solo mostrar consentimientos de examen (origen_lugar_id null); los de toma de muestras se gestionan en lugar.php
|
|
lista = lista.filter(c => !c.origen_lugar_id);
|
|
if (!lista.length) {
|
|
el.innerHTML = '<div class="text-muted small"><i class="fas fa-check-circle text-success me-1"></i>Sin consentimientos requeridos</div>';
|
|
return;
|
|
}
|
|
el.innerHTML = lista.map(c => {
|
|
const m = CONSENT_META[c.estado] || CONSENT_META.pendiente;
|
|
const ya = ['firmado','rechazado','cerrado_anticipado'].includes(c.estado);
|
|
const token = escHtml(c.token || '');
|
|
const nom = escHtml(c.formulario_nombre || 'Consentimiento');
|
|
const fId = c.formulario_id;
|
|
const nomJs = (c.formulario_nombre || 'Consentimiento').replace(/'/g, "\\'");
|
|
|
|
// Formulario solo profesional: no requiere acción del paciente en recepción
|
|
if (!c.requiere_firma_paciente) {
|
|
const labelPro = ya
|
|
? `<span class="badge bg-success ms-1"><i class="fas fa-check me-1"></i>Completado</span>`
|
|
: `<span class="badge bg-secondary ms-1" style="font-size:.75rem"><i class="fas fa-flask me-1"></i>Completa en laboratorio</span>`;
|
|
return `<div class="consent-item ${m.cls}" style="flex-wrap:wrap">
|
|
<div style="display:flex;align-items:center;gap:.5rem;width:100%">
|
|
<i class="fas ${m.ico}"></i>
|
|
<span class="c-nom">${nom}</span>
|
|
${labelPro}
|
|
</div>
|
|
</div>`;
|
|
}
|
|
|
|
// Botón de firma paciente (solo si no está firmado/rechazado)
|
|
const btnFirmar = (!ya)
|
|
? `<button class="btn btn-outline-primary" onclick="firmarConsentimiento(${fId}, '${nomJs}')"
|
|
title="Abrir firma en nueva pestaña">
|
|
<i class="fas fa-external-link-alt"></i> Firmar
|
|
</button>` : '';
|
|
|
|
// Botón ver/enviar WhatsApp
|
|
const btnWa = ya
|
|
? (c.token ? `<a href="${BASE_WA}ver_formulario_enviado.php?token=${token}" target="_blank"
|
|
class="btn btn-outline-secondary" title="Ver formulario firmado">
|
|
<i class="fas fa-eye"></i> Ver
|
|
</a>` : '')
|
|
: `<button class="btn btn-outline-success" onclick="enviarConsentimientoUno(${c.turno_id || turnoActivo?.id})"
|
|
title="Enviar enlace de firma por WhatsApp">
|
|
<i class="fab fa-whatsapp"></i> WhatsApp
|
|
</button>`;
|
|
|
|
const btnOlvidar = (ya && c.token && c.estado !== 'rechazado')
|
|
? `<button class="btn btn-outline-warning btn-sm" title="Olvidar firma — permite re-firmar"
|
|
onclick="_olvidarConsentimientoRec('${token}', '${escHtml(c.formulario_nombre || '')}')">
|
|
<i class="fas fa-undo"></i>
|
|
</button>` : '';
|
|
|
|
// Fila de firma del profesional (solo si el formulario la requiere y el paciente ya firmó)
|
|
let profRow = '';
|
|
if (c.estado === 'firmado' && c.requiere_firma_profesional) {
|
|
const tienePro = c.tiene_firma_profesional == 1 || c.tiene_firma_profesional === true;
|
|
if (tienePro) {
|
|
profRow = `<div class="consent-prof-row">
|
|
<i class="fas fa-user-nurse text-success"></i>
|
|
<span class="text-success">Firma enfermero</span>
|
|
<span class="badge bg-success ms-1">OK</span>
|
|
</div>`;
|
|
} else {
|
|
profRow = `<div class="consent-prof-row">
|
|
<i class="fas fa-user-nurse text-warning"></i>
|
|
<span class="text-warning fw-semibold">Falta firma enfermero</span>
|
|
<button class="btn btn-warning btn-sm ms-auto" style="font-size:.7rem;padding:1px 7px"
|
|
onclick="abrirFirmaEnfermero(${fId}, '${nomJs}')">
|
|
<i class="fas fa-pen-nib me-1"></i>Firmar
|
|
</button>
|
|
</div>`;
|
|
}
|
|
}
|
|
|
|
const motivoRec = (c.estado === 'cerrado_anticipado' && c.cierre_anticipado?.motivo)
|
|
? `<div style="font-size:.75rem;color:#92400e;background:#fef3c7;border-radius:4px;padding:3px 8px;margin-top:3px;width:100%">
|
|
<i class="fas fa-comment-alt me-1"></i>${escHtml(c.cierre_anticipado.motivo)}
|
|
</div>` : '';
|
|
return `<div class="consent-item ${m.cls}" style="flex-wrap:wrap">
|
|
<div style="display:flex;align-items:center;gap:.5rem;width:100%">
|
|
<i class="fas ${m.ico}"></i>
|
|
<span class="c-nom">${nom}</span>
|
|
<span class="c-badge">${m.lbl}</span>
|
|
<div class="consent-acciones">${btnFirmar}${btnWa}${btnOlvidar}</div>
|
|
</div>
|
|
${profRow}
|
|
${motivoRec}
|
|
</div>`;
|
|
}).join('');
|
|
}
|
|
|
|
// ── Firma del enfermero: canvas modal ─────────────────────────
|
|
let _feFormularioId = null;
|
|
const feCanvas = {
|
|
_el: null, _ctx: null, _drawing: false,
|
|
init() {
|
|
this._el = document.getElementById('fe-canvas');
|
|
this._ctx = this._el.getContext('2d');
|
|
this._ctx.strokeStyle = '#1e293b';
|
|
this._ctx.lineWidth = 2;
|
|
this._ctx.lineCap = 'round';
|
|
this._el.addEventListener('pointerdown', e => { this._drawing = true; this._ctx.beginPath(); this._move(e); });
|
|
this._el.addEventListener('pointermove', e => { if (!this._drawing) return; this._ctx.lineTo(...this._pos(e)); this._ctx.stroke(); });
|
|
['pointerup','pointerleave'].forEach(ev => this._el.addEventListener(ev, () => { this._drawing = false; }));
|
|
},
|
|
_pos(e) { const r = this._el.getBoundingClientRect(); return [e.clientX - r.left, e.clientY - r.top]; },
|
|
_move(e) { const [x,y] = this._pos(e); this._ctx.moveTo(x, y); },
|
|
limpiar() { this._ctx.clearRect(0, 0, this._el.width, this._el.height); },
|
|
vacio() {
|
|
const d = this._ctx.getImageData(0, 0, this._el.width, this._el.height).data;
|
|
return !d.some(v => v !== 0);
|
|
},
|
|
png() { return this._el.toDataURL('image/png'); },
|
|
};
|
|
|
|
function abrirFirmaEnfermero(formularioId, nombre) {
|
|
if (!turnoActivo) { mostrarError('No hay turno activo.'); return; }
|
|
_feFormularioId = formularioId;
|
|
document.getElementById('fe-nombre-consentimiento').textContent = nombre;
|
|
if (!feCanvas._el) feCanvas.init();
|
|
feCanvas.limpiar();
|
|
const modal = bootstrap.Modal.getOrCreateInstance(document.getElementById('modalFirmaEnfermero'));
|
|
modal.show();
|
|
}
|
|
|
|
async function feGuardarFirma() {
|
|
if (!turnoActivo || !_feFormularioId) return;
|
|
if (feCanvas.vacio()) { mostrarError('Dibuja la firma antes de guardar.'); return; }
|
|
const png = feCanvas.png();
|
|
const btn = document.querySelector('#modalFirmaEnfermero .btn-primary');
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Guardando…';
|
|
try {
|
|
const res = await fetch(API + 'firmar_profesional_consentimiento.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ turno_id: turnoActivo.id, formulario_id: _feFormularioId, svg: png }),
|
|
});
|
|
const json = await res.json();
|
|
if (json.ok) {
|
|
bootstrap.Modal.getOrCreateInstance(document.getElementById('modalFirmaEnfermero')).hide();
|
|
mostrarToast('Firma del enfermero guardada', 'success');
|
|
await refrescarConsentimientosLugar();
|
|
} else {
|
|
mostrarError(json.error || 'No se pudo guardar la firma.');
|
|
}
|
|
} catch(e) {
|
|
mostrarError('Error al guardar: ' + e.message);
|
|
} finally {
|
|
btn.disabled = false;
|
|
btn.innerHTML = '<i class="fas fa-check me-1"></i>Guardar firma';
|
|
}
|
|
}
|
|
|
|
// ── Firmar en nueva pestaña ──────────────────────────────────
|
|
async function firmarConsentimiento(formularioId, nombre) {
|
|
if (!turnoActivo) { mostrarError('No hay turno activo.'); return; }
|
|
try {
|
|
const res = await fetch(API + 'create_consent_token.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ turno_id: turnoActivo.id, formulario_id: formularioId, paciente_id: pacienteActivo?.id || null }),
|
|
});
|
|
const json = await res.json();
|
|
if (json.ok && (json.url || json.data?.url)) {
|
|
window.open(json.url || json.data.url, '_blank');
|
|
// Iniciar polling automático para detectar firma
|
|
if (!pollingConsentimientosId) {
|
|
pollingConsentimientosId = setInterval(() => {
|
|
refrescarConsentimientosLugar();
|
|
}, 3000);
|
|
}
|
|
} else {
|
|
mostrarError(json.error || 'No se pudo crear el token de firma');
|
|
}
|
|
} catch (err) {
|
|
mostrarError('Error al abrir firma: ' + err.message);
|
|
}
|
|
}
|
|
function verFirmado(token) {
|
|
window.open(BASE_WA + 'ver_formulario_enviado.php?token=' + encodeURIComponent(token), '_blank');
|
|
}
|
|
|
|
// ── Refrescar estado de consentimientos ───────────────────────
|
|
async function refrescarConsentimientos(turnoId) {
|
|
try {
|
|
const res = await fetch(`${API}get_consentimientos.php?turno_id=${turnoId}`);
|
|
const json = await res.json();
|
|
if (json.ok) {
|
|
consentimientos = json.consentimientos || [];
|
|
renderConsentimientos(consentimientos);
|
|
}
|
|
} catch(_) {}
|
|
}
|
|
|
|
// ── Enviar consentimientos (todos) ───────────────────────────
|
|
async function enviarConsentimientosTodos() {
|
|
if (!turnoActivo) return;
|
|
const btn = document.getElementById('btn-reenviar-consent');
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Enviando…';
|
|
try {
|
|
const res = await fetch(API + 'send_consentimiento.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ turno_id: turnoActivo.id }),
|
|
});
|
|
const json = await res.json();
|
|
if (!json.ok) { mostrarError(json.error); return; }
|
|
consentimientos = json.consentimientos ?? consentimientos;
|
|
renderConsentimientos(consentimientos);
|
|
btn.innerHTML = '<i class="fas fa-check me-1"></i>Enviado';
|
|
setTimeout(() => { btn.innerHTML = '<i class="fas fa-paper-plane me-1"></i>Enviar todos'; }, 3000);
|
|
} catch (err) {
|
|
mostrarError(err.message);
|
|
btn.innerHTML = '<i class="fas fa-paper-plane me-1"></i>Enviar todos';
|
|
} finally {
|
|
btn.disabled = false;
|
|
}
|
|
}
|
|
|
|
// ── Enviar consentimiento individual ─────────────────────────
|
|
async function enviarConsentimientoUno(turnoId) {
|
|
if (!turnoId) return;
|
|
try {
|
|
const res = await fetch(API + 'send_consentimiento.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ turno_id: turnoId }),
|
|
});
|
|
const json = await res.json();
|
|
if (!json.ok) { mostrarError(json.error); return; }
|
|
consentimientos = json.consentimientos ?? consentimientos;
|
|
renderConsentimientos(consentimientos);
|
|
} catch (err) {
|
|
mostrarError(err.message);
|
|
}
|
|
}
|
|
|
|
// ── Enviar consentimientos (legacy — usado por btn-enviar-consent) ──
|
|
async function enviarConsentimientos() { return enviarConsentimientosTodos(); }
|
|
|
|
// ── Olvidar (resetear) consentimiento ────────────────────────
|
|
async function _olvidarConsentimientoRec(token, nombre) {
|
|
if (!confirm(`¿Seguro que deseas descartar la firma de "${nombre}"?\nEl paciente podrá volver a firmarlo.`)) return;
|
|
try {
|
|
const r = await fetch('modules/turnero/api/resetear_consentimiento.php', {
|
|
method: 'POST', headers: {'Content-Type':'application/json'},
|
|
body: JSON.stringify({ token })
|
|
});
|
|
const j = await r.json();
|
|
if (!j.ok) { alert(j.error || 'Error al resetear'); return; }
|
|
await cargarConsentimientos();
|
|
} catch(e) { alert(e.message); }
|
|
}
|
|
|
|
// ── Pasar a lugar ─────────────────────────────────────────────
|
|
async function pasarALugar() {
|
|
if (!turnoActivo || !solicitudActiva) return;
|
|
|
|
const btn = document.getElementById('btn-pasar-lugar');
|
|
if (btn) btn.disabled = true;
|
|
|
|
// Leer lugar del select (tiene prioridad sobre lo guardado en BD)
|
|
const lugarSeleccionado = parseInt(document.getElementById('sel-lugar').value) || solicitudActiva.lugar_id;
|
|
|
|
try {
|
|
const res = await fetch(API + 'cambiar_estado.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
turno_id: turnoActivo.id,
|
|
nuevo_estado: 'en_espera_lugar',
|
|
lugar_id: lugarSeleccionado,
|
|
}),
|
|
});
|
|
const json = await res.json();
|
|
if (!json.ok) { mostrarError(json.error); if (btn) btn.disabled = false; return; }
|
|
|
|
// Reiniciar ficha
|
|
turnoActivo = null;
|
|
solicitudActiva = null;
|
|
document.getElementById('ficha-turno').classList.add('d-none');
|
|
document.getElementById('badge-turno-activo').classList.add('d-none');
|
|
|
|
mostrarToast('Turno pasado al lugar.', 'success', 2000);
|
|
|
|
setTimeout(() => {
|
|
cargarCola();
|
|
resetPlaceholder();
|
|
}, 500);
|
|
|
|
} catch (err) {
|
|
mostrarError(err.message);
|
|
if (btn) btn.disabled = false;
|
|
}
|
|
}
|
|
|
|
// ── Ausente ───────────────────────────────────────────────────
|
|
async function marcarAusente() {
|
|
if (!turnoActivo) return;
|
|
|
|
// El motivo se guarda como comentario de recepción: así aparece en la
|
|
// bandeja, el historial y el dashboard sin duplicar el dato.
|
|
const motivo = prompt(
|
|
`Marcar el turno ${turnoActivo.codigo} como AUSENTE.\n\n`
|
|
+ '¿Por qué? (no respondió al llamado, se retiró, reprogramó…)');
|
|
if (motivo === null) return; // canceló
|
|
const motivoLimpio = motivo.trim();
|
|
if (!motivoLimpio) { mostrarError('Indique el motivo para marcar como ausente.'); return; }
|
|
|
|
const turnoId = turnoActivo.id;
|
|
|
|
await fetch(API + 'comentarios.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
turno_id: turnoId,
|
|
comentario: 'Marcado ausente: ' + motivoLimpio,
|
|
tipo: 'recepcion',
|
|
}),
|
|
}).catch(() => {});
|
|
|
|
await fetch(API + 'cambiar_estado.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ turno_id: turnoId, nuevo_estado: 'ausente' }),
|
|
});
|
|
|
|
turnoActivo = null;
|
|
solicitudActiva = null;
|
|
document.getElementById('ficha-turno').classList.add('d-none');
|
|
document.getElementById('ficha-placeholder').classList.remove('d-none');
|
|
document.getElementById('badge-turno-activo').classList.add('d-none');
|
|
cargarCola();
|
|
volverACola();
|
|
}
|
|
|
|
// ── Soltar turno ──────────────────────────────────────────────
|
|
async function soltarTurno() {
|
|
if (!turnoActivo) return;
|
|
if (!confirm(`¿Soltar turno ${turnoActivo.codigo}? Otro escritorio podrá llamarlo.`)) return;
|
|
|
|
await fetch(API + 'cambiar_estado.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ turno_id: turnoActivo.id, nuevo_estado: 'espera' }),
|
|
});
|
|
|
|
turnoActivo = null;
|
|
solicitudActiva = null;
|
|
document.getElementById('ficha-turno').classList.add('d-none');
|
|
document.getElementById('ficha-placeholder').classList.remove('d-none');
|
|
document.getElementById('badge-turno-activo').classList.add('d-none');
|
|
mostrarToast('Turno liberado', 'success');
|
|
cargarCola();
|
|
volverACola();
|
|
}
|
|
|
|
// ── Helpers ───────────────────────────────────────────────────
|
|
// ── Médico tratante ─────────────────────────────────────────────
|
|
let _medicoTimer = null;
|
|
let _medicoResultados = [];
|
|
const API_MEDICOS = '<?= BASE_URL ?>modules/medicos/api/list.php';
|
|
|
|
function buscarMedico() {
|
|
clearTimeout(_medicoTimer);
|
|
const q = document.getElementById('inp-buscar-medico').value.trim();
|
|
const ul = document.getElementById('medico-resultados');
|
|
if (q.length < 2) { ul.classList.add('d-none'); ul.innerHTML = ''; return; }
|
|
_medicoTimer = setTimeout(async () => {
|
|
const res = await fetch(API_MEDICOS + '?q=' + encodeURIComponent(q));
|
|
const json = await res.json();
|
|
const rows = (json.data || []).slice(0, 8);
|
|
_medicoResultados = rows;
|
|
if (!rows.length) { ul.innerHTML = '<li class="px-3 py-2 text-muted small">Sin resultados</li>'; ul.classList.remove('d-none'); return; }
|
|
ul.innerHTML = rows.map(m =>
|
|
`<li class="px-3 py-2 small" style="cursor:pointer;border-bottom:1px solid #f1f5f9"
|
|
onmousedown="seleccionarMedico(${m.id},'${escJs(m.codigo)}','${escJs(m.nombres)} ${escJs(m.apellidos)}','${escJs(m.cod_especialidad||'')}')">
|
|
<span class="fw-semibold">${esc(m.nombres)} ${esc(m.apellidos)}</span>
|
|
<span class="text-muted ms-1">[${esc(m.codigo)}]</span>
|
|
${m.cod_especialidad ? `<span class="badge text-bg-light ms-1" style="font-size:.7rem">${esc(m.cod_especialidad)}</span>` : ''}
|
|
</li>`
|
|
).join('');
|
|
ul.classList.remove('d-none');
|
|
}, 250);
|
|
}
|
|
|
|
function seleccionarMedico(id, codigo, nombre, esp) {
|
|
document.getElementById('inp-medico-id').value = id;
|
|
document.getElementById('medico-badge-txt').textContent = nombre + (esp ? ' · ' + esp : '') + ' [' + codigo + ']';
|
|
document.getElementById('medico-seleccionado').classList.remove('d-none');
|
|
document.getElementById('medico-buscador').classList.add('d-none');
|
|
document.getElementById('medico-resultados').classList.add('d-none');
|
|
}
|
|
|
|
function quitarMedico() {
|
|
document.getElementById('inp-medico-id').value = '';
|
|
document.getElementById('medico-seleccionado').classList.add('d-none');
|
|
document.getElementById('medico-buscador').classList.remove('d-none');
|
|
document.getElementById('inp-buscar-medico').value = '';
|
|
}
|
|
|
|
function resetMedico() {
|
|
quitarMedico();
|
|
}
|
|
|
|
function escJs(s) { return String(s||'').replace(/\\/g,'\\\\').replace(/'/g,"\\'"); }
|
|
|
|
function toggleSoloMuestras() {
|
|
const solo = document.getElementById('chk-solo-muestras').checked;
|
|
const wrap = document.getElementById('wrap-examenes');
|
|
if (examTS) { limpiarExamenes(); solo ? examTS.disable() : examTS.enable(); }
|
|
wrap.classList.toggle('disabled', solo);
|
|
document.getElementById('bloque-medico').style.display = solo ? 'none' : '';
|
|
if (solo) quitarMedico();
|
|
}
|
|
|
|
function resetCheckboxes() {
|
|
document.getElementById('chk-solo-muestras').checked = false;
|
|
if (examTS) { limpiarExamenes(); examTS.enable(); }
|
|
document.getElementById('wrap-examenes').classList.remove('disabled');
|
|
document.getElementById('bloque-medico').style.display = '';
|
|
descartarRips();
|
|
document.getElementById('sel-lugar').value = '';
|
|
document.getElementById('sel-pago').value = '';
|
|
document.getElementById('inp-total').value = '';
|
|
document.getElementById('inp-recibo').value = '';
|
|
document.getElementById('panel-recibo').classList.add('d-none');
|
|
document.getElementById('inp-obs').value = '';
|
|
resetPagoCombinado();
|
|
resetMedico();
|
|
quitarEmpresa();
|
|
}
|
|
|
|
// ── Empresa / Convenio ────────────────────────────────────────
|
|
let _empresaActiva = null;
|
|
let _empresaBusqTimer;
|
|
let _empresaResultados = [];
|
|
|
|
function buscarEmpresa() {
|
|
clearTimeout(_empresaBusqTimer);
|
|
_empresaBusqTimer = setTimeout(_doEmpresaSearch, 280);
|
|
}
|
|
|
|
async function _doEmpresaSearch() {
|
|
const q = document.getElementById('inp-buscar-empresa').value.trim();
|
|
const ul = document.getElementById('empresa-resultados');
|
|
if (q.length < 2) { ul.classList.add('d-none'); return; }
|
|
const res = await fetch(`${BASE_URL_API_LAB}empresas.php?action=list&search=${encodeURIComponent(q)}&activa=1&limit=10`);
|
|
const j = await res.json();
|
|
_empresaResultados = j.empresas || [];
|
|
if (!_empresaResultados.length) { ul.innerHTML = '<li class="px-3 py-2 text-muted small">Sin resultados</li>'; ul.classList.remove('d-none'); return; }
|
|
ul.innerHTML = _empresaResultados.map((e, i) => `
|
|
<li class="px-3 py-2 cursor-pointer hover-bg"
|
|
style="cursor:pointer;border-bottom:1px solid #f1f5f9"
|
|
onmousedown="_seleccionarEmpresaIdx(${i})">
|
|
<div class="fw-semibold small">${escHtml(e.nombre)}</div>
|
|
<div class="text-muted" style="font-size:.75rem">NIT ${escHtml(e.nit)}${e.tarifa_nombre ? ' · ' + escHtml(e.tarifa_nombre) : ''}</div>
|
|
</li>
|
|
`).join('');
|
|
ul.classList.remove('d-none');
|
|
}
|
|
|
|
function _seleccionarEmpresaIdx(i) {
|
|
seleccionarEmpresa(_empresaResultados[i]);
|
|
}
|
|
|
|
async function seleccionarEmpresa(e) {
|
|
document.getElementById('empresa-resultados').classList.add('d-none');
|
|
document.getElementById('empresa-buscador').classList.add('d-none');
|
|
document.getElementById('empresa-seleccionada').classList.remove('d-none');
|
|
document.getElementById('empresa-badge-nombre').textContent = e.nombre;
|
|
document.getElementById('empresa-badge-meta').textContent =
|
|
`NIT ${e.nit}${e.tarifa_nombre ? ' · ' + e.tarifa_nombre : ''}${parseFloat(e.descuento_pct) > 0 ? ' · Dcto: ' + e.descuento_pct + '%' : ''}`;
|
|
document.getElementById('inp-empresa-nit').value = e.nit;
|
|
_empresaActiva = e;
|
|
|
|
// Mostrar autorización y diagnóstico si la empresa los exige
|
|
document.getElementById('wrap-autorizacion').classList.toggle('d-none', !parseInt(e.req_autoriza));
|
|
document.getElementById('wrap-diag').classList.remove('d-none');
|
|
|
|
// Cargar subgrupos
|
|
const res = await fetch(`${BASE_URL_API_LAB}empresa_subgrupos.php?nit_empresa=${encodeURIComponent(e.nit)}`);
|
|
const j = await res.json();
|
|
const subs = j.subgrupos || [];
|
|
const wrapSub = document.getElementById('wrap-subgrupo');
|
|
const selSub = document.getElementById('sel-subgrupo');
|
|
if (subs.length) {
|
|
selSub.innerHTML = '<option value="">— Sin subgrupo —</option>' +
|
|
subs.map(s => `<option value="${s.id}">${escHtml(s.subgrupo)}${s.tarifa_nombre ? ' · ' + escHtml(s.tarifa_nombre) : ''}</option>`).join('');
|
|
wrapSub.classList.remove('d-none');
|
|
} else {
|
|
selSub.innerHTML = '<option value="">— Sin subgrupo —</option>';
|
|
wrapSub.classList.add('d-none');
|
|
}
|
|
|
|
// Si es EPS, preseleccionar forma de pago
|
|
if (document.getElementById('sel-pago').value === '') {
|
|
document.getElementById('sel-pago').value = 'eps';
|
|
togglePagoCombinado();
|
|
}
|
|
|
|
recalcularPrecios();
|
|
}
|
|
|
|
function quitarEmpresa() {
|
|
_empresaActiva = null;
|
|
document.getElementById('inp-empresa-nit').value = '';
|
|
document.getElementById('inp-buscar-empresa').value = '';
|
|
document.getElementById('empresa-seleccionada').classList.add('d-none');
|
|
document.getElementById('empresa-buscador').classList.remove('d-none');
|
|
document.getElementById('empresa-resultados').classList.add('d-none');
|
|
document.getElementById('wrap-subgrupo').classList.add('d-none');
|
|
document.getElementById('wrap-autorizacion').classList.add('d-none');
|
|
document.getElementById('wrap-diag').classList.add('d-none');
|
|
document.getElementById('panel-precios').classList.add('d-none');
|
|
document.getElementById('sel-subgrupo').innerHTML = '<option value="">— Sin subgrupo —</option>';
|
|
document.getElementById('inp-autorizacion').value = '';
|
|
document.getElementById('inp-diag').value = '';
|
|
}
|
|
|
|
// ── CIE-10 autocomplete ───────────────────────────────────────
|
|
let _diagTimer = null, _diagResultados = [];
|
|
function buscarDiag() {
|
|
clearTimeout(_diagTimer);
|
|
_diagTimer = setTimeout(_doDiagSearch, 250);
|
|
}
|
|
async function _doDiagSearch() {
|
|
const q = document.getElementById('inp-diag').value.trim();
|
|
const ul = document.getElementById('diag-resultados');
|
|
if (q.length < 2) { ul.classList.add('d-none'); return; }
|
|
const res = await fetch(`${API}buscar_cie10.php?q=${encodeURIComponent(q)}`);
|
|
const j = await res.json();
|
|
_diagResultados = j.items || [];
|
|
if (!_diagResultados.length) {
|
|
ul.innerHTML = '<li class="px-3 py-2 text-muted small">Sin resultados</li>';
|
|
ul.classList.remove('d-none'); return;
|
|
}
|
|
ul.innerHTML = _diagResultados.map(d => `
|
|
<li class="px-3 py-2 diag-item" style="cursor:pointer;border-bottom:1px solid #f1f5f9"
|
|
data-cod="${escHtml(d.cod_diag)}" data-concepto="${escHtml(d.concepto)}">
|
|
<span class="fw-semibold small">${escHtml(d.cod_diag)}</span>
|
|
<span class="text-muted small ms-2">${escHtml(d.concepto)}</span>
|
|
</li>`).join('');
|
|
ul.classList.remove('d-none');
|
|
}
|
|
function seleccionarDiag(cod) {
|
|
document.getElementById('inp-diag').value = cod;
|
|
document.getElementById('diag-resultados').classList.add('d-none');
|
|
_diagResultados = [];
|
|
}
|
|
document.getElementById('diag-resultados').addEventListener('mousedown', function(e) {
|
|
const li = e.target.closest('.diag-item');
|
|
if (li) { e.preventDefault(); seleccionarDiag(li.dataset.cod); }
|
|
});
|
|
|
|
// ── Motor de precios ──────────────────────────────────────────
|
|
let _recalcTimer;
|
|
let _preciosActivos = [];
|
|
|
|
function recalcularPrecios() {
|
|
clearTimeout(_recalcTimer);
|
|
_recalcTimer = setTimeout(_doRecalcular, 200);
|
|
}
|
|
|
|
async function _doRecalcular() {
|
|
const examIds = examTS ? examTS.getValue() : [];
|
|
const nit = document.getElementById('inp-empresa-nit').value.trim();
|
|
const subId = document.getElementById('sel-subgrupo').value;
|
|
|
|
if (!examIds.length) {
|
|
document.getElementById('panel-precios').classList.add('d-none');
|
|
return;
|
|
}
|
|
|
|
const params = new URLSearchParams({ nit_empresa: nit });
|
|
examIds.forEach(id => params.append('exam_ids[]', id));
|
|
if (subId) params.append('subgrupo_id', subId);
|
|
|
|
const res = await fetch(`${API}get_precios_examenes.php?${params}`);
|
|
const j = await res.json();
|
|
if (!j.ok) return;
|
|
|
|
_preciosActivos = j.items || [];
|
|
|
|
// Tabla
|
|
const tabla = document.getElementById('tabla-precios');
|
|
const tienePrecios = j.items.some(i => i.tiene_precio);
|
|
if (!tienePrecios) {
|
|
tabla.innerHTML = '<div class="text-muted small text-center py-1">Sin precios configurados para esta tarifa</div>';
|
|
} else {
|
|
tabla.innerHTML = j.items.map(i => `
|
|
<div class="d-flex justify-content-between py-1" style="border-bottom:1px solid #f1f5f9">
|
|
<span class="text-truncate me-2" style="max-width:200px" title="${escHtml(i.nombre)}">${escHtml(i.nombre)}</span>
|
|
<span class="${i.tiene_precio ? 'fw-semibold' : 'text-muted'}">
|
|
${i.tiene_precio ? '$' + fmt(i.valor_final) : 'Sin precio'}
|
|
</span>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
// Totales
|
|
document.getElementById('precio-tarifa-label').textContent = j.tarifa_nombre || '';
|
|
document.getElementById('precio-total-calc').textContent = '$' + fmt(j.subtotal);
|
|
const dctoRow = document.getElementById('precio-dcto-row');
|
|
if (j.descuento_pct > 0) {
|
|
document.getElementById('precio-dcto-pct').textContent = j.descuento_pct;
|
|
document.getElementById('precio-dcto-val').textContent = '-$' + fmt(j.descuento_val);
|
|
document.getElementById('precio-total-calc').textContent = '$' + fmt(j.total);
|
|
dctoRow.classList.remove('d-none');
|
|
} else {
|
|
dctoRow.classList.add('d-none');
|
|
}
|
|
|
|
document.getElementById('panel-precios').classList.remove('d-none');
|
|
|
|
// Auto-aplicar total al campo de cobro
|
|
const totalFinal = j.total || j.subtotal || 0;
|
|
if (totalFinal > 0) {
|
|
document.getElementById('inp-total').value = Math.round(totalFinal);
|
|
}
|
|
}
|
|
|
|
function aplicarPrecioCalculado() {
|
|
const total = _preciosActivos.reduce((sum, i) => sum + (i.valor_final || 0), 0);
|
|
const nit = document.getElementById('inp-empresa-nit').value.trim();
|
|
const emp = _empresaActiva;
|
|
const dcto = parseFloat(emp?.descuento_pct || 0);
|
|
const final = Math.round(total * (1 - dcto / 100));
|
|
document.getElementById('inp-total').value = final || '';
|
|
if (emp && !document.getElementById('sel-pago').value) {
|
|
document.getElementById('sel-pago').value = 'eps';
|
|
togglePagoCombinado();
|
|
}
|
|
mostrarToast('Total aplicado: $' + fmt(final), 'ok', 2500);
|
|
}
|
|
|
|
function fmt(n) {
|
|
return Math.round(n || 0).toLocaleString('es-CO');
|
|
}
|
|
|
|
const BASE_URL_API_LAB = '<?= BASE_URL ?>/api/lab/';
|
|
|
|
// ── Cargar EPS y ciudades una vez al inicio ───────────────────
|
|
(async function() {
|
|
try {
|
|
const [rEps, rCiu] = await Promise.all([
|
|
fetch(BASE_URL_API_LAB + 'eps.php?action=list&solo_activas=1'),
|
|
fetch(BASE_URL_API_LAB + 'ciudades.php?action=list&solo_activas=1'),
|
|
]);
|
|
const [jEps, jCiu] = await Promise.all([rEps.json(), rCiu.json()]);
|
|
|
|
const selEps = document.getElementById('mpac-eps');
|
|
(jEps.eps || []).forEach(e => {
|
|
const o = document.createElement('option');
|
|
o.value = e.nombre; o.textContent = e.nombre;
|
|
selEps.appendChild(o);
|
|
});
|
|
|
|
const selCiu = document.getElementById('mpac-ciudad');
|
|
selCiu.innerHTML = '<option value="">— Seleccionar —</option>';
|
|
(jCiu.ciudades || []).forEach(c => {
|
|
const o = document.createElement('option');
|
|
o.value = c.nombre; o.textContent = c.nombre;
|
|
if (c.nombre === 'Cúcuta') o.selected = true;
|
|
selCiu.appendChild(o);
|
|
});
|
|
} catch(_) {}
|
|
})();
|
|
|
|
function escHtml(str) {
|
|
const d = document.createElement('div');
|
|
d.appendChild(document.createTextNode(String(str)));
|
|
return d.innerHTML;
|
|
}
|
|
const esc = escHtml;
|
|
|
|
function resetPlaceholder() {
|
|
const ph = document.getElementById('ficha-placeholder');
|
|
ph.innerHTML = `
|
|
<i class="fas fa-ticket-alt fa-3x mb-3" style="color:#cbd5e1"></i>
|
|
<p class="mb-0 fw-semibold">Sin turno activo</p>
|
|
<small class="text-muted">Haga clic en "Llamar siguiente" o en <i class="fas fa-bell"></i> de un turno de la cola</small>`;
|
|
volverACola();
|
|
}
|
|
|
|
function esMobile() { return window.innerWidth <= 680; }
|
|
function mostrarFichaMobile() {
|
|
if (!esMobile()) return;
|
|
document.getElementById('rec-ficha').classList.add('mobile-visible');
|
|
document.querySelector('.rec-cola').classList.add('mobile-oculta');
|
|
}
|
|
function volverACola() {
|
|
document.getElementById('rec-ficha').classList.remove('mobile-visible');
|
|
document.querySelector('.rec-cola').classList.remove('mobile-oculta');
|
|
}
|
|
|
|
function mostrarError(msg) {
|
|
mostrarToast(msg, 'error', 4000);
|
|
}
|
|
function mostrarAviso(titulo, msg) {
|
|
mostrarToast(titulo + ': ' + msg, 'warn', 3500);
|
|
}
|
|
function mostrarLlamando(codigo) {
|
|
mostrarToast('Llamando turno ' + codigo + '…', 'info', 2000);
|
|
}
|
|
|
|
/* ── Ocupación exclusiva del escritorio ──────────────────────── */
|
|
(function(_ID, _NOMBRE, _VIEW, _PARAM) {
|
|
if (!_ID) return;
|
|
|
|
function _e(s) {
|
|
const d = document.createElement('div');
|
|
d.appendChild(document.createTextNode(String(s ?? '')));
|
|
return d.innerHTML;
|
|
}
|
|
function _post(body) {
|
|
return fetch(API + 'ocupar_lugar.php', {
|
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(Object.assign({ lugar_id: _ID }, body)),
|
|
}).then(r => r.json());
|
|
}
|
|
function _redirigirSiguiente(disponibles) {
|
|
if (disponibles && disponibles.length) {
|
|
window.location.href = BASE_WA + 'erp.php?m=turnero&v=' + _VIEW + '&' + _PARAM + '=' + disponibles[0].id;
|
|
} else {
|
|
window.location.href = BASE_WA + 'erp.php?m=turnero';
|
|
}
|
|
}
|
|
|
|
/* Pantalla de bloqueo */
|
|
function _mostrarBloqueado(ocupadoPor) {
|
|
document.body.innerHTML =
|
|
'<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;' +
|
|
'min-height:100vh;background:#f8faff;font-family:sans-serif;gap:14px;text-align:center;padding:28px">' +
|
|
'<div style="font-size:4rem">🔒</div>' +
|
|
'<h2 style="color:#1565c0;margin:0">' + _e(_NOMBRE) + ' está abierta en otro equipo</h2>' +
|
|
'<p style="color:#555;font-size:1.05rem;max-width:440px;line-height:1.6">' +
|
|
'Actualmente usada por <strong>' + _e(ocupadoPor) + '</strong>.<br>' +
|
|
'Puedes solicitar que cierre su sesión o ir al siguiente escritorio disponible.</p>' +
|
|
'<div style="display:flex;gap:12px;flex-wrap:wrap;justify-content:center;margin-top:4px">' +
|
|
'<button id="_btn-solicitar" onclick="window._ocToma()" ' +
|
|
'style="padding:12px 26px;background:#1565c0;color:#fff;border:none;border-radius:8px;font-size:1rem;cursor:pointer;font-weight:600">' +
|
|
'📢 Solicitar cierre en ese equipo</button>' +
|
|
'<button id="_btn-siguiente" onclick="window._ocSiguiente()" ' +
|
|
'style="padding:12px 26px;background:#f0f4ff;color:#1565c0;border:2px solid #1565c0;border-radius:8px;font-size:1rem;cursor:pointer;font-weight:600">' +
|
|
'➡ Ir al siguiente disponible</button>' +
|
|
'</div>' +
|
|
'<div id="_oc-estado" style="min-height:24px;color:#777;font-size:.95rem;margin-top:4px"></div>' +
|
|
'</div>';
|
|
}
|
|
function _setEstado(msg, color) {
|
|
const el = document.getElementById('_oc-estado');
|
|
if (el) el.innerHTML = '<span style="color:' + (color||'#555') + '">' + msg + '</span>';
|
|
}
|
|
|
|
/* Solicitar toma de control */
|
|
let _checkTransferTimer = null;
|
|
window._ocToma = async function() {
|
|
const btn = document.getElementById('_btn-solicitar');
|
|
if (btn) btn.disabled = true;
|
|
_setEstado('Enviando solicitud…', '#555');
|
|
try {
|
|
const j = await _post({ action: 'solicitar' });
|
|
if (!j.ok) { _setEstado('Error al enviar solicitud.', '#c62828'); if (btn) btn.disabled = false; return; }
|
|
if (j.transferido) { location.reload(); return; }
|
|
_setEstado('✉ Solicitud enviada. El otro equipo tiene 5 segundos para responder…', '#1565c0');
|
|
clearInterval(_checkTransferTimer);
|
|
_checkTransferTimer = setInterval(async () => {
|
|
try {
|
|
const r = await _post({ action: 'check_transfer' });
|
|
if (r.disponible) {
|
|
clearInterval(_checkTransferTimer);
|
|
_setEstado('✅ Escritorio liberado. Tomando control…', '#2e7d32');
|
|
const rc = await _post({ action: 'claim' });
|
|
if (rc.disponible) location.reload();
|
|
else _setEstado('No se pudo tomar el control. Recargue la página.', '#c62828');
|
|
} else if (r.denegado) {
|
|
clearInterval(_checkTransferTimer);
|
|
_setEstado('❌ La solicitud fue denegada.', '#c62828');
|
|
if (btn) { btn.disabled = false; btn.textContent = '📢 Volver a solicitar'; }
|
|
}
|
|
} catch(_) {}
|
|
}, 1500);
|
|
} catch(e) {
|
|
_setEstado('Error de red: ' + e.message, '#c62828');
|
|
if (btn) btn.disabled = false;
|
|
}
|
|
};
|
|
|
|
/* Ir al siguiente disponible */
|
|
window._ocSiguiente = async function() {
|
|
const btn = document.getElementById('_btn-siguiente');
|
|
if (btn) btn.disabled = true;
|
|
_setEstado('Buscando escritorio disponible…', '#555');
|
|
try {
|
|
const j = await _post({ action: 'disponibles' });
|
|
if (!j.ok || !j.disponibles.length) {
|
|
_setEstado('No hay otros escritorios disponibles.', '#c62828');
|
|
if (btn) btn.disabled = false;
|
|
return;
|
|
}
|
|
_redirigirSiguiente(j.disponibles);
|
|
} catch(e) {
|
|
_setEstado('Error de red.', '#c62828');
|
|
if (btn) btn.disabled = false;
|
|
}
|
|
};
|
|
|
|
/* Modal de solicitud entrante (para el ocupante actual) */
|
|
let _modalActivo = false;
|
|
function _mostrarModalSolicitud(solicitante) {
|
|
if (_modalActivo) return;
|
|
_modalActivo = true;
|
|
const modal = document.createElement('div');
|
|
modal.style.cssText =
|
|
'position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:99999;display:flex;align-items:center;justify-content:center';
|
|
modal.innerHTML =
|
|
'<div style="background:#fff;border-radius:14px;padding:32px 28px;max-width:420px;width:92%;text-align:center;box-shadow:0 8px 32px rgba(0,0,0,.25)">' +
|
|
'<div style="font-size:2.5rem">⚠️</div>' +
|
|
'<h3 style="color:#c62828;margin:10px 0 6px">Solicitud de transferencia</h3>' +
|
|
'<p style="color:#333;margin:0 0 6px"><strong>' + _e(solicitante) + '</strong> quiere usar este escritorio.</p>' +
|
|
'<p style="color:#555;font-size:.95rem">Si no responde, se cerrará automáticamente en <strong id="_sol-cnt">5</strong> segundos.</p>' +
|
|
'<div style="display:flex;gap:10px;justify-content:center;margin-top:18px">' +
|
|
'<button id="_sol-aceptar" style="padding:10px 22px;background:#c62828;color:#fff;border:none;border-radius:8px;font-size:1rem;cursor:pointer;font-weight:600">Aceptar y ceder</button>' +
|
|
'<button id="_sol-denegar" style="padding:10px 22px;background:#f5f5f5;color:#333;border:1px solid #ccc;border-radius:8px;font-size:1rem;cursor:pointer">Denegar</button>' +
|
|
'</div></div>';
|
|
document.body.appendChild(modal);
|
|
|
|
let secs = 5;
|
|
const tick = setInterval(() => {
|
|
secs--;
|
|
const el = document.getElementById('_sol-cnt');
|
|
if (el) el.textContent = secs;
|
|
if (secs <= 0) { clearInterval(tick); _liberar(); }
|
|
}, 1000);
|
|
|
|
async function _liberar() {
|
|
modal.remove();
|
|
await _post({ action: 'aceptar' }).catch(() => {});
|
|
try {
|
|
const j = await _post({ action: 'disponibles' });
|
|
_redirigirSiguiente(j.disponibles || []);
|
|
} catch(_) {
|
|
window.location.href = BASE_WA + 'erp.php?m=turnero';
|
|
}
|
|
}
|
|
|
|
document.getElementById('_sol-aceptar').onclick = () => { clearInterval(tick); _liberar(); };
|
|
document.getElementById('_sol-denegar').onclick = async () => {
|
|
clearInterval(tick);
|
|
modal.remove();
|
|
_modalActivo = false;
|
|
await _post({ action: 'denegar' }).catch(() => {});
|
|
};
|
|
}
|
|
|
|
/* Arranque */
|
|
async function _iniciar() {
|
|
try {
|
|
const j = await _post({ action: 'claim' });
|
|
if (!j.ok) return;
|
|
if (!j.disponible) { _mostrarBloqueado(j.ocupado_por); return; }
|
|
// Heartbeat cada 60 s
|
|
setInterval(() => _post({ action: 'ping' }).catch(() => {}), 60000);
|
|
// Polling de solicitudes entrantes cada 2 s
|
|
setInterval(async () => {
|
|
if (_modalActivo) return;
|
|
try {
|
|
const r = await _post({ action: 'check_solicitud' });
|
|
if (r.hay_solicitud) _mostrarModalSolicitud(r.solicitante);
|
|
} catch(_) {}
|
|
}, 2000);
|
|
} catch(_) {}
|
|
}
|
|
|
|
window.addEventListener('beforeunload', () => {
|
|
navigator.sendBeacon(API + 'ocupar_lugar.php',
|
|
new Blob([JSON.stringify({ lugar_id: _ID, action: 'release' })], { type: 'application/json' }));
|
|
});
|
|
|
|
_iniciar();
|
|
})(DESK_ID, DESK_NOMBRE, 'recepcion', 'desk_id');
|
|
|
|
// ── Modal paciente (crear / ver / editar) ────────────────────
|
|
const _mpacModal = () => bootstrap.Modal.getOrCreateInstance(document.getElementById('modalPacienteRec'));
|
|
|
|
function mpacAvatar(nombre) {
|
|
const parts = nombre.trim().split(/\s+/).filter(Boolean);
|
|
document.getElementById('mpac-avatar').textContent = parts.length >= 2
|
|
? (parts[0][0] + parts[1][0]).toUpperCase()
|
|
: (parts[0]?.[0] || '?').toUpperCase();
|
|
}
|
|
|
|
async function abrirModalPaciente(id, nombrePrefill) {
|
|
// Reset form
|
|
document.getElementById('form-paciente-rec').reset();
|
|
document.getElementById('mpac-id').value = '';
|
|
document.getElementById('mpac-avatar').textContent = '?';
|
|
document.getElementById('mpac-wa-badge').innerHTML = '';
|
|
document.getElementById('mpac-user-id').value = '';
|
|
document.getElementById('mpac-ciudad').value = 'Cúcuta';
|
|
document.getElementById('mpac-eps').value = '';
|
|
|
|
if (id) {
|
|
document.getElementById('mpac-titulo').textContent = 'Cargando…';
|
|
document.getElementById('mpac-subtitulo').textContent = '';
|
|
_mpacModal().show();
|
|
try {
|
|
const r = await fetch(`${API_PAC}?id=${id}`);
|
|
const d = await r.json();
|
|
const p = (d.data || d.registros || [])[0];
|
|
if (!p) { mostrarError('No se pudo cargar el paciente.'); return; }
|
|
mpacRellenar(p);
|
|
} catch (_) { mostrarError('Error al cargar paciente.'); }
|
|
} else {
|
|
document.getElementById('mpac-titulo').textContent = 'Nuevo Paciente';
|
|
document.getElementById('mpac-subtitulo').textContent = 'Completa los datos del paciente';
|
|
if (nombrePrefill) {
|
|
// Si es solo dígitos, viene del kiosko como cédula → ir al campo documento
|
|
if (/^\d+$/.test(nombrePrefill.trim())) {
|
|
document.getElementById('mpac-doc').value = nombrePrefill.trim();
|
|
} else {
|
|
document.getElementById('mpac-nombre').value = nombrePrefill;
|
|
mpacAvatar(nombrePrefill);
|
|
}
|
|
}
|
|
// Pre-llenar cédula del kiosko si el turno activo la tiene y no se pasó ya
|
|
const docKiosko = turnoActivo?.paciente_nombre;
|
|
if (docKiosko && /^\d+$/.test(docKiosko.trim()) && !document.getElementById('mpac-doc').value) {
|
|
document.getElementById('mpac-doc').value = docKiosko.trim();
|
|
}
|
|
_mpacModal().show();
|
|
}
|
|
// Foco: si ya tiene doc pero no nombre, ir al nombre; si no, al doc
|
|
setTimeout(() => {
|
|
const focusEl = document.getElementById('mpac-doc').value && !document.getElementById('mpac-nombre').value
|
|
? document.getElementById('mpac-nombre')
|
|
: document.getElementById('mpac-doc').value
|
|
? document.getElementById('mpac-nombre')
|
|
: document.getElementById('mpac-doc');
|
|
focusEl.focus();
|
|
}, 300);
|
|
}
|
|
|
|
function _selectOrAdd(selId, val) {
|
|
const sel = document.getElementById(selId);
|
|
if (!val) { sel.value = ''; return; }
|
|
if ([...sel.options].some(o => o.value === val)) { sel.value = val; return; }
|
|
const o = document.createElement('option');
|
|
o.value = val; o.textContent = val;
|
|
sel.appendChild(o);
|
|
sel.value = val;
|
|
}
|
|
|
|
function mpacRellenar(p) {
|
|
document.getElementById('mpac-id').value = p.id;
|
|
document.getElementById('mpac-nombre').value = p.nombre_completo || '';
|
|
document.getElementById('mpac-doc').value = p.numero_documento || '';
|
|
document.getElementById('mpac-tipo-doc').value = p.tipo_documento || 'CC';
|
|
document.getElementById('mpac-fnac').value = (p.fecha_nacimiento || '').slice(0, 10);
|
|
document.getElementById('mpac-email').value = p.email || '';
|
|
document.getElementById('mpac-genero').value = p.genero || '';
|
|
_selectOrAdd('mpac-eps', p.eps || '');
|
|
_selectOrAdd('mpac-ciudad', p.ciudad || 'Cúcuta');
|
|
document.getElementById('mpac-barrio').value = p.barrio || '';
|
|
document.getElementById('mpac-dir').value = p.direccion || '';
|
|
document.getElementById('mpac-notas').value = p.notas_admin || '';
|
|
|
|
// Teléfono con prefijo
|
|
let tel = p.telefono || '';
|
|
const prefijos = ['57','58','1'];
|
|
let prefijo = '57';
|
|
for (const pf of prefijos) {
|
|
if (tel.startsWith(pf) && tel.length > pf.length) {
|
|
prefijo = pf; tel = tel.slice(pf.length); break;
|
|
}
|
|
}
|
|
document.getElementById('mpac-tel-prefijo').value = prefijo;
|
|
document.getElementById('mpac-tel').value = tel;
|
|
|
|
mpacAvatar(p.nombre_completo || '');
|
|
document.getElementById('mpac-titulo').textContent = 'Editar Paciente';
|
|
document.getElementById('mpac-subtitulo').textContent = p.nombre_completo || '';
|
|
}
|
|
|
|
async function mpacVerificarWA() {
|
|
const prefijo = document.getElementById('mpac-tel-prefijo').value || '57';
|
|
let tel = (document.getElementById('mpac-tel').value || '').replace(/[^0-9]/g, '');
|
|
if (tel.length === 10) tel = prefijo + tel;
|
|
const badge = document.getElementById('mpac-wa-badge');
|
|
if (!tel || tel.length < 10) { badge.innerHTML = ''; return; }
|
|
badge.innerHTML = '<span class="text-muted"><i class="fas fa-spinner fa-spin me-1"></i>Verificando…</span>';
|
|
try {
|
|
const r = await fetch(`<?= BASE_URL ?>api/lab/check_whatsapp.php?phone=${encodeURIComponent(tel)}`);
|
|
const d = await r.json();
|
|
if (d.ok && d.found) {
|
|
badge.innerHTML = '<span class="text-success"><i class="fab fa-whatsapp me-1"></i>Registrado en WhatsApp</span>';
|
|
document.getElementById('mpac-user-id').value = d.user_id;
|
|
} else {
|
|
badge.innerHTML = '<span class="text-muted"><i class="fas fa-times-circle me-1"></i>Sin WhatsApp registrado</span>';
|
|
document.getElementById('mpac-user-id').value = '';
|
|
}
|
|
} catch(_) { badge.innerHTML = ''; }
|
|
}
|
|
|
|
async function mpacGuardar() {
|
|
const nombre = document.getElementById('mpac-nombre').value.trim();
|
|
if (!nombre) { mostrarError('El nombre es obligatorio.'); return; }
|
|
if (!/^[\p{L}\s'\-\.]+$/u.test(nombre)) {
|
|
mostrarError('El nombre solo debe contener letras, tildes y espacios.'); return;
|
|
}
|
|
if (nombre.split(/\s+/).filter(Boolean).length < 2) {
|
|
mostrarError('Ingresa nombre y apellido (mínimo 2 palabras).'); return;
|
|
}
|
|
|
|
const tipoDoc = document.getElementById('mpac-tipo-doc').value;
|
|
const docVal = document.getElementById('mpac-doc').value.trim();
|
|
if (docVal && ['CC','TI','RC','CE'].includes(tipoDoc)) {
|
|
const digits = docVal.replace(/[^0-9]/g, '');
|
|
if (digits.length < 4 || digits.length > 12) {
|
|
mostrarError('El documento debe tener entre 4 y 12 dígitos.'); return;
|
|
}
|
|
}
|
|
|
|
const emailVal = document.getElementById('mpac-email').value.trim();
|
|
if (emailVal && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(emailVal)) {
|
|
mostrarError('El correo electrónico no es válido.'); return;
|
|
}
|
|
|
|
const prefijo = document.getElementById('mpac-tel-prefijo').value || '57';
|
|
let tel = document.getElementById('mpac-tel').value.replace(/[^0-9]/g, '');
|
|
if (tel.length === 10) tel = prefijo + tel;
|
|
if (tel && prefijo === '57') {
|
|
const sinPref = tel.replace(/^57/, '');
|
|
if (!/^3\d{9}$/.test(sinPref)) {
|
|
mostrarError('El celular colombiano debe comenzar por 3 y tener 10 dígitos.'); return;
|
|
}
|
|
}
|
|
|
|
const id = document.getElementById('mpac-id').value;
|
|
const datos = {
|
|
nombre_completo: nombre,
|
|
tipo_documento: tipoDoc,
|
|
numero_documento: docVal || null,
|
|
fecha_nacimiento: document.getElementById('mpac-fnac').value || null,
|
|
telefono: tel || null,
|
|
email: emailVal || null,
|
|
genero: document.getElementById('mpac-genero').value || null,
|
|
eps: document.getElementById('mpac-eps').value.trim() || null,
|
|
ciudad: document.getElementById('mpac-ciudad').value.trim() || null,
|
|
barrio: document.getElementById('mpac-barrio').value.trim() || null,
|
|
direccion: document.getElementById('mpac-dir').value.trim() || null,
|
|
notas_admin: document.getElementById('mpac-notas').value.trim() || null,
|
|
user_id: document.getElementById('mpac-user-id').value || null,
|
|
};
|
|
if (id) datos.id = parseInt(id);
|
|
|
|
const btn = document.getElementById('mpac-btn-guardar');
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Guardando…';
|
|
|
|
try {
|
|
const r = await fetch('<?= BASE_URL ?>api/lab/save_paciente.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(datos),
|
|
});
|
|
const d = await r.json();
|
|
|
|
if (!d.success && !d.ok) {
|
|
mostrarError(d.error || 'Error al guardar el paciente.');
|
|
return;
|
|
}
|
|
|
|
_mpacModal().hide();
|
|
mostrarToast(d.message || 'Paciente guardado.', 'success');
|
|
|
|
if (id) {
|
|
// Edición: actualizar datos en pantalla si es el paciente activo
|
|
if (pacienteActivo && pacienteActivo.id == id) {
|
|
pacienteActivo.nombre_completo = nombre;
|
|
pacienteActivo.numero_documento = docVal;
|
|
pacienteActivo.tipo_documento = tipoDoc;
|
|
pacienteActivo.telefono = tel;
|
|
pacienteActivo.genero = document.getElementById('mpac-genero').value;
|
|
document.getElementById('lbl-pac-nombre').textContent =
|
|
(pacienteActivo.full_name || nombre).trim();
|
|
document.getElementById('lbl-pac-doc').textContent =
|
|
tipoDoc + ' ' + (docVal || '');
|
|
document.getElementById('lbl-pac-cel').textContent = tel || '';
|
|
const esMasc = (pacienteActivo.genero || '').toUpperCase() === 'M';
|
|
document.getElementById('bloque-embarazada').style.display = esMasc ? 'none' : '';
|
|
if (esMasc) document.getElementById('chk-embarazada').checked = false;
|
|
}
|
|
} else {
|
|
// Creación: vincular al turno automáticamente
|
|
const nuevoPac = {
|
|
id: d.id || d.data?.id,
|
|
nombre_completo: nombre,
|
|
full_name: nombre,
|
|
tipo_documento: tipoDoc,
|
|
numero_documento: docVal,
|
|
telefono: tel,
|
|
genero: document.getElementById('mpac-genero').value,
|
|
};
|
|
seleccionarPaciente(nuevoPac);
|
|
}
|
|
} catch (err) {
|
|
mostrarError('Error de red: ' + err.message);
|
|
} finally {
|
|
btn.disabled = false;
|
|
btn.innerHTML = '<i class="fas fa-save me-1"></i>Guardar paciente';
|
|
}
|
|
}
|
|
|
|
// ── Toast ────────────────────────────────────────────────────
|
|
let toastTimer = null;
|
|
async function _cargarResumenMuestras() {
|
|
try {
|
|
const r = await fetch(API + 'get_resumen_muestras.php');
|
|
const j = await r.json();
|
|
if (!j.ok) return;
|
|
const cont = document.getElementById('muestras-estaciones');
|
|
if (!cont) return;
|
|
cont.innerHTML = (j.por_prioridad || []).map(p =>
|
|
`<span style="display:inline-flex;align-items:center;gap:4px;font-size:.78rem;font-weight:700;
|
|
padding:2px 8px;border-radius:99px;background:${escHtml(p.color)}22;color:${escHtml(p.color)};
|
|
border:1.5px solid ${escHtml(p.color)}55;white-space:nowrap">
|
|
${escHtml(p.codigo)}: ${escHtml(p.ultimo_codigo)}
|
|
</span>`
|
|
).join('');
|
|
const cb = document.getElementById('muestras-cola-badge');
|
|
cb.textContent = `${j.en_espera} en espera`;
|
|
cb.classList.toggle('d-none', j.en_espera === 0);
|
|
const pb = document.getElementById('muestras-prog-badge');
|
|
pb.textContent = `${j.en_espera_muestra} prog.`;
|
|
pb.classList.toggle('d-none', j.en_espera_muestra === 0);
|
|
} catch {}
|
|
}
|
|
|
|
function mostrarToast(msg, type = 'info', duration = 2500) {
|
|
const el = document.getElementById('rec-toast');
|
|
const ico = document.getElementById('toast-ico');
|
|
const txt = document.getElementById('toast-msg');
|
|
const icons = { success: 'fa-check-circle', info: 'fa-info-circle', warn: 'fa-exclamation-triangle', error: 'fa-times-circle' };
|
|
el.className = 'rec-toast ' + type;
|
|
ico.innerHTML = '<i class="fas ' + (icons[type] || icons.info) + '"></i>';
|
|
txt.textContent = msg;
|
|
el.classList.add('show');
|
|
clearTimeout(toastTimer);
|
|
toastTimer = setTimeout(() => el.classList.remove('show'), duration);
|
|
}
|
|
</script>
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
|
<script src="<?= defined('APP_URL') ? rtrim(APP_URL,'/') : '' ?>/assets/js/lab-sidebar.js"></script>
|
|
<script>
|
|
(function() {
|
|
// Carga desde nuestro servidor primero; fallback a SigREST local si aún no se sube el archivo.
|
|
var URLS = [
|
|
'<?= rtrim(defined('APP_URL') ? APP_URL : '', '/') ?>/assets/js/SigWebTablet.js',
|
|
'https://localhost:47290/SigWeb/SigWebTablet.js',
|
|
'http://localhost:47289/SigWeb/SigWebTablet.js'
|
|
];
|
|
var t0 = Date.now();
|
|
console.group('[SigWeb] Detección de pad biométrico');
|
|
console.log('→ Intento iniciado:', new Date().toLocaleTimeString());
|
|
|
|
function sigwebOnLoad(urlUsada) {
|
|
var ms = Date.now() - t0;
|
|
console.log('✅ Script cargado desde', urlUsada, 'en ' + ms + 'ms');
|
|
try {
|
|
var ctx = new SigWebTablet();
|
|
var state = ctx.GetTabletState();
|
|
console.log(' GetTabletState():', state, state === 0 ? '← pad NO detectado' : '← pad activo');
|
|
if (state === 0) {
|
|
console.warn(' ⚠ SigWeb corre pero el pad físico no responde. Revisa USB y drivers.');
|
|
} else {
|
|
ctx.SetImageXSize(500);
|
|
console.log(' ✅ Pad respondiendo correctamente');
|
|
}
|
|
} catch(e) {
|
|
console.warn(' ⚠ Error al instanciar SigWebTablet:', e.message);
|
|
}
|
|
console.groupEnd();
|
|
var b = document.getElementById('sigweb-badge');
|
|
var d = document.getElementById('sigweb-dot');
|
|
var l = document.getElementById('sigweb-lbl');
|
|
if (b) b.style.color = '#16a34a';
|
|
if (d) { d.style.background = '#22c55e'; d.style.boxShadow = '0 0 4px #22c55e'; }
|
|
if (l) l.textContent = 'Topaz en línea';
|
|
}
|
|
|
|
function intentar(idx) {
|
|
if (idx >= URLS.length) {
|
|
var ms = Date.now() - t0;
|
|
console.warn('❌ SigWeb no respondió en ningún puerto después de ' + ms + 'ms.');
|
|
console.warn(' Si usas Chrome/HTTPS, abre primero: https://localhost:47290/SigWeb/');
|
|
console.warn(' y acepta el certificado auto-firmado de Topaz.');
|
|
console.warn(' Si el problema persiste: instala/inicia el servicio Topaz SigWeb.');
|
|
console.groupEnd();
|
|
return;
|
|
}
|
|
var url = URLS[idx];
|
|
console.log('→ Intentando:', url);
|
|
var s = document.createElement('script');
|
|
s.src = url;
|
|
s.onload = function() { sigwebOnLoad(url); };
|
|
s.onerror = function() {
|
|
console.warn(' ✗ Falló:', url);
|
|
intentar(idx + 1);
|
|
};
|
|
document.head.appendChild(s);
|
|
}
|
|
|
|
intentar(0);
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|