Marcar ausente solo pedía confirmación: el turno quedaba registrado sin ninguna explicación, y hoy son 6 en un día. Ahora se pide el motivo y se guarda como comentario de recepción, no como columna nueva: así aparece en todas las pantallas que ya muestran comentarios, sin duplicar el dato ni tocar el esquema. Dashboard e historial mostraban las notas sin indicar de dónde venían, así que no se distinguía una observación de toma de muestras de una de recepción. Se agrega la insignia de origen con los mismos colores que ya usa la bandeja, y el título pasa a "Notas y observaciones" con su cantidad. El historial además traía solo las 3 últimas por turno y en orden inverso. Con recepción empezando a dejar notas eso habría escondido justamente el motivo de una ausencia sin que nada indicara que faltaban; ahora trae hasta 20, en orden cronológico. De las 114 notas existentes, todas son de toma de muestras: recepción nunca dejó ninguna porque no tenía dónde hacerlo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
3230 lines
160 KiB
PHP
3230 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;
|
|
}
|
|
|
|
// 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>
|