Files
whatsapp/modules/turnero/views/lugar.php
T
Lizandro GuarnizoandClaude Sonnet 4.6 7a4413e173 fix: banner toma no se cierra al abrir directamente por 'Siguiente toma'
El banner de alerta solo se cerraba vía 'Ver paciente'. Si el profesional
hacía clic en 'Siguiente toma' directamente, el banner seguía parpadeando
y sonando aunque el formulario ya estuviera abierto.

- abrirTomaProgresiva() ahora llama cerrarBannerToma() + _tomaAlertados.delete()
- renderConsentimientos() preserva alertado:true si la siguiente_toma_at no
  cambió, evitando beep doble al re-sincronizar después de firmar

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-16 13:05:26 -05:00

2378 lines
114 KiB
PHP

<?php
/**
* Puesto Lugar / Estación de Servicio — Módulo Turnero
* Genérica: sirve para Toma de Muestras 1, 2, Rayos X, etc.
*/
require_once __DIR__ . '/../../../config/config.php';
if (!isUserLoggedIn()) {
header('Location: ' . BASE_URL . 'login.php');
exit;
}
// Tablet asignada → forzar su lugar por token de navegador (o IP como fallback)
$lugarForzado = 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=recepcion&desk_id=' . $_forzado); exit;
}
if ((int)($_GET['lugar_id'] ?? 0) !== $_forzado) {
header('Location: ' . BASE_URL . 'erp.php?m=turnero&v=lugar&lugar_id=' . $_forzado); exit;
}
$lugarForzado = $_forzado;
}
} catch (\Throwable $_) {}
try {
$pdo = Database::getInstance()->getConnection();
$lugares = $pdo->query(
"SELECT id, nombre, descripcion, formulario_modo, tipo FROM turnero_lugares WHERE activo = 1 ORDER BY sort_order ASC"
)->fetchAll(PDO::FETCH_ASSOC);
} catch (\Throwable) {
$lugares = [];
}
$lugarIdParam = isset($_GET['lugar_id']) ? (int)$_GET['lugar_id'] : 0;
$lugarNombre = 'Estación de Servicio';
$lugarFormModo = 'link';
$lugarTipo = 'muestras';
foreach ($lugares as $l) {
if ((int)$l['id'] === $lugarIdParam) {
$lugarNombre = $l['nombre'];
$lugarFormModo = $l['formulario_modo'] ?? 'link';
$lugarTipo = $l['tipo'] ?? 'muestras';
break;
}
}
$adminNombre = $_SESSION['admin_user']['full_name'] ?? $_SESSION['admin_user']['username'] ?? 'Operador';
$adminFirmaSvg = null;
try {
$stmt = Database::getInstance()->getConnection()
->prepare("SELECT firma_svg FROM admin_users WHERE id = ? LIMIT 1");
$stmt->execute([(int)($_SESSION['admin_user']['id'] ?? 0)]);
$adminFirmaSvg = $stmt->fetchColumn() ?: null;
} catch (\Throwable $_) {}
?>
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= htmlspecialchars($lugarNombre) ?> — 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">
<style>
/* ── Layout ─────────────────────────────────────────────── */
.lugar-layout {
display: grid;
grid-template-columns: 370px 1fr;
height: calc(100vh - 52px);
overflow: hidden;
}
@media (max-width: 680px) {
.lugar-layout { grid-template-columns: 1fr; position: relative; overflow: hidden; }
.lugar-cola { transition: transform .2s ease; }
.lugar-cola.mobile-oculta { transform: translateX(-100%); position: absolute; inset: 0; pointer-events: none; }
.lugar-ficha { position: absolute; inset: 0; background: #fff; z-index: 10;
transform: translateX(100%); transition: transform .2s ease; overflow-y: auto; }
.lugar-ficha.mobile-visible { transform: translateX(0); }
}
/* ── Cola ───────────────────────────────────────────────── */
.lugar-cola {
background: #f8fafc;
border-right: 1px solid #e2e8f0;
display: flex; flex-direction: column; overflow: hidden;
}
.lugar-cola-hdr {
padding: .85rem 1rem .65rem;
background: #fff; border-bottom: 1px solid #e2e8f0;
flex-shrink: 0;
}
.cola-hdr-row {
display: flex; align-items: center; justify-content: space-between; margin-bottom: .65rem;
}
.cola-hdr-title {
font-size: .78rem; text-transform: uppercase; letter-spacing: .08em;
color: #64748b; font-weight: 700;
}
.cola-counts { display: flex; gap: .4rem; align-items: center; }
.cola-count-chip {
font-size: .68rem; font-weight: 700; border-radius: 99px;
padding: 1px 9px; border: 1px solid;
}
.cola-count-chip.espera { background: #eff6ff; color: #1d4ed8; border-color: #bfdbfe; }
.cola-count-chip.servicio { background: #f0fdf4; color: #166534; border-color: #bbf7d0; }
.btn-llamar-next {
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;
}
.btn-llamar-next:hover { background: #1d4ed8; }
.btn-llamar-next:active { transform: scale(.98); }
.btn-llamar-next:disabled { background: #94a3b8; cursor: not-allowed; }
.cola-items { flex: 1; overflow-y: auto; padding: .5rem; }
.cola-section-sep {
font-size: .65rem; text-transform: uppercase; letter-spacing: .06em; font-weight: 700;
color: #94a3b8; padding: .3rem .4rem .15rem; margin-top: .25rem;
}
.cola-card {
background: #fff; border: 1px solid #e2e8f0; border-radius: 11px;
padding: .65rem .9rem; margin-bottom: .3rem;
display: flex; align-items: center; gap: .55rem;
cursor: pointer; transition: border-color .15s, box-shadow .15s, background .15s;
min-height: 62px;
}
.cola-card:hover { border-color: #a5b4fc; box-shadow: 0 2px 8px rgba(99,102,241,.12); }
.cola-card.activo { border-color: #6366f1; box-shadow: 0 0 0 2px #c7d2fe; }
.cola-card.en-servicio-card { border-color: #16a34a; background: #f0fdf4; }
.cola-card.en-servicio-card:hover { border-color: #15803d; }
.cola-card.en-servicio-card.activo { box-shadow: 0 0 0 2px #bbf7d0; }
.prio-dot {
width: 36px; height: 36px; border-radius: 9px; flex-shrink: 0;
display: flex; align-items: center; justify-content: center;
font-size: 1rem; font-weight: 800; color: #fff;
}
.turno-info { flex: 1; min-width: 0; }
.turno-info .cod { font-size: .95rem; font-weight: 700; line-height: 1.2; }
.turno-info .pac {
font-size: .74rem; color: #64748b;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.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; }
.muestra-chip {
font-size: .6rem; font-weight: 700; color: #ea580c;
background: #fff7ed; border-radius: 99px; padding: 0 6px; line-height: 1.7;
}
.toma-prog-chip {
font-size: .6rem; font-weight: 700; border-radius: 99px;
padding: 0 6px; line-height: 1.7; white-space: nowrap;
}
.toma-prog-chip.ok { background: #eff6ff; color: #1d4ed8; }
.toma-prog-chip.warn { background: #fffbeb; color: #92400e; }
.toma-prog-chip.crit { background: #fef2f2; color: #991b1b; animation: parpadeo-alerta 1s infinite; }
@keyframes parpadeo-alerta { 0%,100%{opacity:1} 50%{opacity:.55} }
.cola-card.toma-prog { border-color: #f97316 !important; }
.cola-card.toma-prog.crit-card { border-color: #dc2626 !important; background: #fef2f2 !important; }
.card-en-servicio-pill {
flex-shrink: 0; display: flex; flex-direction: column; align-items: center;
background: #16a34a; color: #fff; border-radius: 7px;
padding: 3px 7px; font-size: .6rem; font-weight: 800; letter-spacing: .03em;
gap: 1px; text-align: center;
}
.card-en-servicio-pill i { font-size: .75rem; }
.card-bell-btn {
flex-shrink: 0; background: #2563eb; border: none; color: #fff;
border-radius: 10px; width: 44px; height: 44px;
display: flex; align-items: center; justify-content: center;
font-size: .95rem; cursor: pointer; transition: background .12s;
}
.card-bell-btn:hover { background: #1d4ed8; }
.cola-vacia { text-align: center; padding: 2.5rem 1rem; color: #94a3b8; font-size: .88rem; }
/* ── Ficha ──────────────────────────────────────────────── */
.lugar-ficha { overflow-y: auto; padding: 0; background: #fff; display: flex; flex-direction: column; }
.ficha-inner { flex: 1; padding: 1.2rem 1.4rem 1rem; }
.ficha-placeholder {
flex: 1; display: flex; flex-direction: column;
align-items: center; justify-content: center;
color: #94a3b8; text-align: center; padding: 3rem 1rem;
}
/* Mobile top bar (back button) */
.ficha-mobile-topbar {
display: none; align-items: center; gap: .6rem;
padding: .55rem 1rem; background: #f8fafc;
border-bottom: 1px solid #e2e8f0; flex-shrink: 0;
position: sticky; top: 0; z-index: 20;
}
@media (max-width: 680px) {
.ficha-mobile-topbar { display: flex; }
}
.ficha-mobile-topbar .btn-back {
background: none; border: none; padding: 4px 6px;
color: #2563eb; font-size: .88rem; font-weight: 600;
cursor: pointer; display: flex; align-items: center; gap: .3rem;
}
.ficha-mobile-topbar .m-cod { font-size: .78rem; color: #64748b; font-weight: 600; }
/* ── Ficha header ───────────────────────────────────────── */
.ficha-turno-hdr {
background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 13px;
padding: .9rem 1.1rem; margin-bottom: 1rem;
}
.fth-top {
display: flex; align-items: center; gap: .6rem; margin-bottom: .5rem;
}
.prio-dot-sm {
width: 30px; height: 30px; border-radius: 7px; flex-shrink: 0;
display: flex; align-items: center; justify-content: center;
font-size: .82rem; font-weight: 800; color: #fff;
}
.fth-meta { flex: 1; display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
.fth-cod { font-size: .82rem; font-weight: 800; color: #334155; }
.fth-prio { font-size: .75rem; color: #64748b; }
.fth-pac-nom {
font-size: 1.35rem; font-weight: 800; color: #1e293b; line-height: 1.2;
letter-spacing: -.01em;
}
.fth-pac-nom.loading {
height: 22px; width: 60%; border-radius: 6px;
background: linear-gradient(90deg,#f0f0f0 25%,#e8e8e8 50%,#f0f0f0 75%);
background-size: 200% 100%; animation: skel-wave 1.4s infinite;
}
@keyframes skel-wave {
0% { background-position: 200% 0 }
100% { background-position: -200% 0 }
}
/* ── Secciones ──────────────────────────────────────────── */
.ficha-sec {
background: #f8fafc; border: 1px solid #e2e8f0;
border-radius: 12px; padding: 1.2rem; margin-bottom: .85rem;
}
.ficha-sec-hdr {
display: flex; align-items: center; gap: .4rem;
font-size: .78rem; text-transform: uppercase; letter-spacing: .08em;
color: #475569; font-weight: 700; margin-bottom: .7rem;
}
.ficha-sec-hdr i { font-size: .85rem; }
/* ── Datos paciente ── */
.pac-dato { display: flex; gap: .5rem; align-items: baseline; margin-bottom: .25rem; }
.pac-dato .lbl { font-size: .72rem; color: #94a3b8; min-width: 78px; flex-shrink: 0; }
.pac-dato .val { font-size: .87rem; color: #1e293b; font-weight: 500; }
/* ── Exámenes ── */
.exam-pill {
display: inline-flex; align-items: center; gap: .3rem;
background: #eff6ff; color: #1d4ed8; border-radius: 20px;
padding: .22rem .7rem; font-size: .78rem; font-weight: 600; margin: .15rem;
border: 1px solid #bfdbfe;
}
/* ── Consentimientos ── */
.consent-row {
display: flex; align-items: center; gap: .6rem;
padding: .5rem .7rem; border-radius: 10px; margin-bottom: .35rem;
font-size: .84rem; border: 1px solid transparent; min-height: 46px;
}
.consent-row.firmado { background: #f0fdf4; color: #166534; border-color: #bbf7d0; }
.consent-row.rechazado { background: #f8fafc; color: #64748b; border-color: #e2e8f0; }
.consent-row.enviado { background: #eff6ff; color: #1e40af; border-color: #bfdbfe; }
.consent-row.pendiente { background: #fffbeb; color: #92400e; border-color: #fde68a; }
.consent-row.visto { background: #faf5ff; color: #6b21a8; border-color: #ddd6fe; }
.consent-row.en_progreso { background: #fff7ed; color: #9a3412; border-color: #fed7aa; }
.consent-row.en_progreso.alerta { background: #fef2f2; color: #991b1b; border-color: #fca5a5;
animation: parpadeo-alerta 1s infinite; }
@keyframes parpadeo-alerta { 0%,100%{opacity:1} 50%{opacity:.6} }
.toma-progreso { font-size:.67rem; background:#ea580c; color:#fff;
border-radius:99px; padding:1px 7px; font-weight:700; white-space:nowrap; }
.toma-countdown { font-size:.67rem; color:inherit; opacity:.75; white-space:nowrap; }
.consent-row .nom-form { flex: 1; font-weight: 500; }
.consent-row .c-badge { font-size: .67rem; padding: 1px 8px; border-radius: 99px;
border: 1px solid currentColor; font-weight: 700; opacity: .85; }
.consent-row .acciones-consent { display: flex; gap: .3rem; flex-shrink: 0; }
.consent-row .acciones-consent .btn { font-size: .72rem; padding: 2px 9px; border-radius: 7px; }
/* ── 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: auto; flex-shrink: 0;
}
.accion-consent-warn {
display: flex; align-items: center; gap: .5rem;
background: #fffbeb; border: 1px solid #fcd34d; border-radius: 9px;
padding: .5rem .8rem; font-size: .8rem; color: #92400e; font-weight: 600;
margin-bottom: .65rem;
}
.accion-consent-warn i { color: #f59e0b; flex-shrink: 0; }
.accion-primary-row { margin-bottom: .45rem; }
.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.azul { background: #2563eb; color: #fff; }
.btn-accion-primary.azul:hover { background: #1d4ed8; }
.btn-accion-primary.verde { background: #16a34a; color: #fff; }
.btn-accion-primary.verde:hover { background: #15803d; }
.accion-secondary-row {
display: flex; gap: .4rem; align-items: center; 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; }
/* ── Bloqueo banner ── */
.bloqueo-banner {
background: #fef2f2; border: 1px solid #fca5a5; border-radius: 10px;
padding: .6rem .9rem; font-size: .82rem; color: #991b1b;
display: flex; align-items: center; gap: .5rem; margin-bottom: .7rem;
}
/* ── Selector ── */
.selector-lugar-overlay {
position: fixed; inset: 0; background: #f1f5f9;
z-index: 200; display: flex; align-items: center; justify-content: center;
}
.selector-lugar-box {
background: #fff; border-radius: 20px; padding: 2.5rem;
width: min(90vw, 480px); box-shadow: 0 8px 40px rgba(0,0,0,.12); text-align: center;
}
.selector-lugar-box h2 { font-size: 1.4rem; font-weight: 700; margin-bottom: 1.5rem; }
/* ── Modal paciente ── */
#modal-pac-lugar { display:none; position:fixed; inset:0; z-index:9500;
background:rgba(0,0,0,.55); align-items:center; justify-content:center; padding:16px; }
#modal-pac-lugar.show { display:flex; }
.mpac-dialog {
background:#fff; border-radius:16px; width:min(96vw,540px);
max-height:90vh; display:flex; flex-direction:column; overflow:hidden;
box-shadow:0 8px 40px rgba(0,0,0,.22);
}
.mpac-hdr {
display:flex; align-items:center; justify-content:space-between;
padding:1.1rem 1.25rem; border-bottom:1px solid rgba(255,255,255,.15); flex-shrink:0;
background:linear-gradient(135deg,#1043a0 0%,#1565c0 100%);
}
.mpac-hdr-title { font-weight:700; font-size:.97rem; color:#fff; display:flex; align-items:center; gap:.5rem; }
.mpac-close { background:rgba(255,255,255,.15); border:none; font-size:1.1rem; color:#fff; cursor:pointer; padding:4px 8px; border-radius:6px; }
.mpac-close:hover { background:rgba(255,255,255,.25); }
.mpac-body { overflow-y:auto; padding:18px; flex:1; }
.mpac-dato { display:flex; gap:.5rem; align-items:baseline; margin-bottom:.4rem; }
.mpac-dato .lbl { font-size:.72rem; color:#94a3b8; min-width:95px; flex-shrink:0; }
.mpac-dato .val { font-size:.88rem; color:#1e293b; font-weight:500; }
.mpac-sec-title {
font-size:.7rem; text-transform:uppercase; letter-spacing:.07em;
color:#475569; font-weight:700; margin:14px 0 8px;
display:flex; align-items:center; gap:.4rem;
}
/* Historial timeline */
.mpac-timeline { padding: 0; }
.mtl-item {
display:flex; gap:10px; align-items:flex-start;
padding-bottom:12px; position:relative;
}
.mtl-item:not(:last-child)::before {
content:''; position:absolute; left:6px; top:14px;
width:2px; bottom:0; background:#e2e8f0;
}
.mtl-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;
}
.mtl-content { flex:1; min-width:0; }
.mtl-fecha { font-size:.68rem; color:#94a3b8; }
.mtl-codigo { font-size:.82rem; font-weight:700; }
.mtl-lugar { font-size:.73rem; color:#64748b; }
.mtl-eb { font-size:.62rem; padding:1px 7px; border-radius:99px; font-weight:700; display:inline-block; margin-top:2px; }
/* ── Banner toma alerta ── */
#banner-toma-alerta {
display: none; position: fixed; top: 52px; left: 0; right: 0; z-index: 8900;
background: #dc2626; color: #fff;
padding: .65rem 1.2rem; display: none;
align-items: center; gap: .75rem; font-size: .9rem; font-weight: 600;
box-shadow: 0 4px 20px rgba(220,38,38,.45);
animation: parpadeo-alerta 1s infinite;
}
#banner-toma-alerta.show { display: flex; }
#banner-toma-alerta .btn-banner-pac {
background: rgba(255,255,255,.2); border: 1.5px solid rgba(255,255,255,.5);
color: #fff; border-radius: 8px; padding: 3px 12px; font-size: .8rem;
cursor: pointer; font-weight: 700; transition: background .12s;
}
#banner-toma-alerta .btn-banner-pac:hover { background: rgba(255,255,255,.35); }
#banner-toma-alerta .btn-banner-x {
margin-left: auto; background: none; border: none; color: rgba(255,255,255,.8);
font-size: 1.1rem; cursor: pointer; padding: 0 4px;
}
/* ── 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; }
/* ── Widget muestras ────────────────────────────────────── */
.muestra-row {
display: flex; align-items: center; gap: .6rem;
padding: .5rem .7rem; border-radius: 10px; margin-bottom: .35rem;
font-size: .84rem; border: 1px solid transparent;
}
.muestra-row.pendiente { background: #fffbeb; border-color: #fde68a; }
.muestra-row.recibida { background: #f0fdf4; border-color: #bbf7d0; color: #166534; }
.muestra-row.rechazada { background: #fef2f2; border-color: #fca5a5; color: #991b1b; }
.muestra-info { flex: 1; min-width: 0; }
.muestra-label { font-weight: 600; font-size: .82rem; font-family: monospace; }
.muestra-motivo { font-size: .7rem; margin-top: 2px; opacity: .85; }
.muestra-ts { font-size: .68rem; color: #94a3b8; flex-shrink: 0; }
.btn-muestra-accion {
border: none; border-radius: 8px; padding: 3px 11px;
font-size: .75rem; font-weight: 700; cursor: pointer;
display: flex; align-items: center; gap: .25rem;
transition: opacity .12s;
}
.btn-muestra-accion:active { opacity: .75; }
.btn-muestra-accion.recibir { background: #16a34a; color: #fff; }
.btn-muestra-accion.rechazar { background: #fff; color: #991b1b; border: 1.5px solid #fca5a5; }
.muestras-summary {
font-size: .72rem; color: #64748b; margin-bottom: .5rem;
display: flex; align-items: center; gap: .5rem; flex-wrap: wrap;
}
.muestras-summary .mc { font-weight: 700; border-radius: 99px; padding: 1px 8px;
font-size: .68rem; }
.muestras-summary .mc.pend { background: #fef9c3; color: #854d0e; }
.muestras-summary .mc.rec { background: #dcfce7; color: #166534; }
.muestras-summary .mc.rech { background: #fee2e2; color: #991b1b; }
</style>
</head>
<body>
<?php
$SIDEBAR_TITLE = 'Turnero';
$SIDEBAR_ICON = 'fas fa-ticket-alt';
require_once __DIR__ . '/../../../shared/components/sidebar.php';
?>
<!-- ══ Selector inicial ══════════════════════════════════════════ -->
<div id="overlay-selector" class="selector-lugar-overlay" <?= $lugarIdParam ? 'style="display:none"' : '' ?>>
<div class="selector-lugar-box">
<i class="fas fa-map-marker-alt fa-2x text-primary mb-3"></i>
<h2>Seleccione su estación</h2>
<select id="sel-lugar-init" class="form-select mb-3">
<option value="">— Elija un lugar —</option>
<?php foreach ($lugares as $l): ?>
<option value="<?= (int)$l['id'] ?>"><?= htmlspecialchars($l['nombre']) ?></option>
<?php endforeach; ?>
</select>
<button class="btn btn-primary w-100" onclick="confirmarLugar()">
<i class="fas fa-check me-1"></i>Confirmar
</button>
<button id="btn-cancelar-selector" class="btn btn-outline-secondary w-100 mt-2"
onclick="document.getElementById('overlay-selector').style.display='none'"
style="display:none">
<i class="fas fa-times me-1"></i>Cancelar
</button>
</div>
</div>
<!-- ── Banner alerta toma progresiva ─────────────────────────────── -->
<div id="banner-toma-alerta" role="alert">
<i class="fas fa-syringe fa-lg"></i>
<span id="banner-toma-txt">¡Hora de la siguiente muestra!</span>
<button class="btn-banner-pac" id="banner-toma-btn" onclick="verPacienteToma()">Ver paciente</button>
<button class="btn-banner-x" onclick="cerrarBannerToma()" title="Cerrar"><i class="fas fa-times"></i></button>
</div>
<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;min-height:52px">
<div class="d-flex align-items-center gap-2">
<i class="fas fa-map-marker-alt text-primary"></i>
<strong id="lbl-lugar-titulo"><?= htmlspecialchars($lugarNombre) ?></strong>
<span id="badge-turno-activo" class="badge bg-primary ms-1 d-none"></span>
</div>
<div class="d-flex align-items-center gap-2">
<span class="text-muted small d-none d-md-inline"><?= htmlspecialchars($adminNombre) ?></span>
<button class="btn btn-sm <?= $adminFirmaSvg ? 'btn-outline-success' : 'btn-warning' ?>"
onclick="abrirModalMiFirma()" title="<?= $adminFirmaSvg ? 'Ver / cambiar mi firma' : 'Configurar mi firma' ?>">
<i class="fas fa-signature me-1"></i><?= $adminFirmaSvg ? 'Mi firma' : 'Configurar firma' ?>
</button>
<?php if (!$lugarForzado): ?>
<button class="btn btn-sm btn-outline-secondary" onclick="cambiarLugar()">
<i class="fas fa-exchange-alt me-1"></i>Cambiar
</button>
<?php endif; ?>
</div>
</div>
<div class="lugar-layout">
<!-- ══ Cola ═══════════════════════════════════════════════ -->
<div class="lugar-cola">
<div class="lugar-cola-hdr">
<div class="cola-hdr-row">
<span class="cola-hdr-title"><i class="fas fa-list-ol me-1"></i>Cola</span>
<div class="cola-counts">
<span class="cola-count-chip espera" id="badge-count">0 espera</span>
<span class="cola-count-chip servicio d-none" id="badge-servicio">0 servicio</span>
</div>
</div>
<button class="btn-llamar-next" id="btn-llamar" onclick="llamarSiguiente()">
<i class="fas fa-bell"></i>Llamar siguiente
</button>
</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>
<!-- ══ Ficha del turno ════════════════════════════════════ -->
<div class="lugar-ficha" id="lugar-ficha">
<!-- Barra back en móvil -->
<div class="ficha-mobile-topbar" id="ficha-mobile-topbar">
<button class="btn-back" onclick="volverACola()">
<i class="fas fa-arrow-left"></i> Cola
</button>
<span class="m-cod" id="ficha-mobile-cod"></span>
</div>
<!-- Estado vacío -->
<div class="ficha-placeholder" id="ficha-placeholder">
<i class="fas fa-stethoscope fa-3x mb-3" style="color:#cbd5e1"></i>
<p class="fw-semibold mb-1">Sin turno activo</p>
<small class="text-muted">Pulse "Llamar siguiente" o seleccione un turno de la cola</small>
</div>
<div id="ficha-turno" class="d-none" style="display:flex;flex-direction:column;flex:1">
<div class="ficha-inner">
<!-- ── Header: paciente + turno ── -->
<div class="ficha-turno-hdr">
<div class="fth-top">
<div id="ficha-prio-dot" class="prio-dot-sm" style="background:#6366f1">—</div>
<div class="fth-meta">
<span class="fth-cod" id="ficha-codigo">—</span>
<span class="fth-prio" id="ficha-prio-nombre"></span>
<span id="ficha-orden" class="d-none"
style="font-size:.72rem;background:#eff6ff;color:#1d4ed8;border:1px solid #bfdbfe;
border-radius:20px;padding:1px 10px;font-weight:700;white-space:nowrap">
<i class="fas fa-hashtag" style="font-size:.63rem"></i>
<span id="ficha-orden-val"></span>
</span>
</div>
<span class="badge bg-success-subtle text-success border border-success-subtle"
id="ficha-estado-badge">en espera</span>
<span id="badge-solo-muestras" class="d-none"
style="font-size:.7rem;background:#fff7ed;color:#c2410c;border:1px solid #fed7aa;
border-radius:20px;padding:1px 10px;font-weight:700;white-space:nowrap">
<i class="fas fa-flask me-1"></i>Solo entrega de muestras
</span>
</div>
<div id="ficha-pac-nombre-hdr" class="fth-pac-nom loading">&nbsp;</div>
</div>
<!-- ── Paciente ── -->
<div class="ficha-sec">
<div class="ficha-sec-hdr">
<i class="fas fa-user"></i>Paciente
<button id="btn-ver-paciente" class="btn btn-sm btn-outline-primary ms-auto d-none"
onclick="abrirModalPaciente()" style="font-size:.72rem;padding:2px 10px;border-radius:20px">
<i class="fas fa-eye me-1"></i>Ver
</button>
</div>
<div id="bloque-pac-info">
<div class="pac-dato"><span class="lbl">Nombre</span><span class="val" id="pac-nombre">—</span></div>
<div class="pac-dato"><span class="lbl">Documento</span><span class="val" id="pac-doc">—</span></div>
<div class="pac-dato d-none"><span class="lbl">Fecha nac.</span><span class="val" id="pac-fec">—</span></div>
<div class="pac-dato d-none"><span class="lbl">Celular</span><span class="val" id="pac-cel">—</span></div>
</div>
<div id="pac-embarazada" class="d-none mt-2">
<span class="badge text-bg-danger"><i class="fas fa-baby me-1"></i>Paciente embarazada</span>
</div>
<div id="pac-medico" class="d-none mt-2">
<div class="pac-dato">
<span class="lbl">Médico</span>
<span class="val" id="pac-medico-nombre" style="color:#4f46e5"></span>
</div>
</div>
<div id="pac-obs-recepcion" class="d-none mt-2">
<div style="background:#fffbeb;border-left:3px solid #f59e0b;border-radius:4px;padding:7px 10px;font-size:.8rem">
<div style="font-weight:600;color:#92400e;margin-bottom:2px"><i class="fas fa-comment-alt me-1"></i>Nota de recepción</div>
<div id="pac-obs-recepcion-texto" style="color:#78350f;white-space:pre-line"></div>
</div>
</div>
<div id="bloque-pac-sin" class="text-muted small">
<i class="fas fa-info-circle me-1"></i>Sin paciente vinculado
</div>
</div>
<!-- ── Exámenes ── -->
<div class="ficha-sec">
<div class="ficha-sec-hdr"><i class="fas fa-vial"></i>Exámenes solicitados</div>
<div id="lista-examenes-ficha">
<span class="text-muted small">Sin exámenes registrados</span>
</div>
</div>
<!-- ── Muestras (solo estaciones tipo muestras) ── -->
<div class="ficha-sec d-none" id="sec-muestras">
<div class="ficha-sec-hdr">
<i class="fas fa-flask" style="color:#ea580c"></i>
Recepción de muestras
</div>
<div id="muestras-summary" class="muestras-summary"></div>
<div id="lista-muestras"></div>
</div>
<!-- ── Formulario embebido (iframe, modo link) ── -->
<div class="ficha-sec d-none" id="sec-form-embebido">
<div class="ficha-sec-hdr"><i class="fas fa-file-alt"></i>Formulario de consentimiento</div>
<div id="form-embebido-loading" class="text-muted small py-2">
<i class="fas fa-spinner fa-spin me-1"></i>Cargando formulario...
</div>
<iframe id="form-embebido-iframe" src="" frameborder="0"
style="width:100%;min-height:520px;border-radius:8px;border:1px solid #e2e8f0;display:none"
onload="this.style.display='block';document.getElementById('form-embebido-loading').style.display='none'">
</iframe>
</div>
<!-- ── Consentimientos ── -->
<div class="ficha-sec" id="sec-consent">
<div class="ficha-sec-hdr"><i class="fas fa-file-signature"></i>Consentimientos informados</div>
<div id="bloqueo-banner" class="bloqueo-banner d-none">
<i class="fas fa-lock"></i>
<span>Hay consentimientos pendientes. No puede <strong>finalizar</strong> hasta que estén <strong>firmados</strong> o <strong>rechazados</strong>.</span>
</div>
<div id="lista-consent"></div>
<div id="sin-consent" class="text-muted small">
<i class="fas fa-check-circle text-success me-1"></i>No se requieren consentimientos
</div>
</div>
<!-- ── Comentarios ── -->
<div class="ficha-sec" id="sec-comentarios">
<div class="ficha-sec-hdr"><i class="fas fa-comments" style="color:#8b5cf6"></i>Comentarios del seguimiento</div>
<div style="display:flex;gap:8px;margin-bottom:10px">
<textarea id="inp-comentario" class="form-control form-control-sm"
rows="2" placeholder="Añade un comentario sobre este turno…"
maxlength="500" style="resize:vertical"
oninput="draftGuardar()"></textarea>
<button class="btn btn-sm btn-primary" onclick="guardarComentario()"
style="height:fit-content;align-self:flex-end">
<i class="fas fa-paper-plane"></i>
</button>
</div>
<div id="lista-comentarios" class="text-muted small py-2">
<i class="fas fa-spinner fa-spin me-1"></i>Cargando comentarios…
</div>
</div>
</div><!-- /ficha-inner -->
<!-- ── Barra de acciones sticky ── -->
<div class="ficha-acciones">
<!-- Aviso de consentimientos pendientes -->
<div id="accion-consent-warn" class="accion-consent-warn d-none">
<i class="fas fa-exclamation-triangle"></i>
<span id="accion-warn-txt">Consentimientos pendientes</span> — No puede finalizar la atención
</div>
<!-- Botón primario -->
<div class="accion-primary-row">
<button class="btn-accion-primary azul" id="btn-iniciar" onclick="iniciarAtencion()">
<i class="fas fa-volume-up"></i>Anunciar turno
</button>
<button class="btn-accion-primary verde d-none" id="btn-finalizar" onclick="finalizarAtencion()">
<i class="fas fa-flag-checkered"></i>Finalizar atención
</button>
</div>
<!-- Botones secundarios -->
<div class="accion-secondary-row">
<button class="btn-accion-sec info d-none" id="btn-rellamar" onclick="rellamarTurno()">
<i class="fas fa-bullhorn"></i>Re-llamar
</button>
<button class="btn-accion-sec warning d-none" id="btn-regresar" onclick="regresarCola()">
<i class="fas fa-undo"></i>Regresar cola
</button>
<button class="btn-accion-sec danger" id="btn-ausente" onclick="marcarAusente()">
<i class="fas fa-user-slash"></i>Ausente
</button>
</div>
</div>
</div><!-- /ficha-turno -->
</div><!-- /lugar-ficha -->
</div><!-- /lugar-layout -->
</main>
<!-- ── Modal datos + historial del paciente ─────────────────────── -->
<div id="modal-pac-lugar" onclick="if(event.target===this)cerrarModalPaciente()">
<div class="mpac-dialog">
<div class="mpac-hdr">
<span class="mpac-hdr-title"><i class="fas fa-user-circle text-primary"></i><span id="mpac-nombre-titulo">Paciente</span></span>
<button class="mpac-close" onclick="cerrarModalPaciente()"><i class="fas fa-times"></i></button>
</div>
<div class="mpac-body">
<!-- Datos básicos -->
<div class="mpac-sec-title"><i class="fas fa-id-card"></i>Información del paciente</div>
<div id="mpac-datos">
<div class="mpac-dato"><span class="lbl">Nombre</span><span class="val" id="mpac-val-nombre">—</span></div>
<div class="mpac-dato"><span class="lbl">Documento</span><span class="val" id="mpac-val-doc">—</span></div>
<div class="mpac-dato"><span class="lbl">Fecha nac.</span><span class="val" id="mpac-val-fec">—</span></div>
<div class="mpac-dato"><span class="lbl">Celular</span><span class="val" id="mpac-val-cel">—</span></div>
<div class="mpac-dato" id="mpac-row-medico" style="display:none">
<span class="lbl">Médico</span><span class="val" id="mpac-val-medico" style="color:#4f46e5">—</span>
</div>
<div id="mpac-badge-emb" class="d-none mt-1">
<span class="badge text-bg-danger"><i class="fas fa-baby me-1"></i>Paciente embarazada</span>
</div>
</div>
<!-- Historial -->
<div class="mpac-sec-title"><i class="fas fa-history" style="color:#6366f1"></i>Historial de visitas</div>
<div id="mpac-historial-content">
<div class="text-center text-muted py-3 small">
<i class="fas fa-spinner fa-spin me-1"></i>Cargando historial…
</div>
</div>
</div>
</div>
</div>
<!-- ── Modal de consentimiento embebido (modo embebido) ────────── -->
<div id="modal-consentimiento" style="
display:none;position:fixed;inset:0;z-index:9000;
background:rgba(0,0,0,.55);align-items:center;justify-content:center;padding:16px">
<div style="
background:#fff;border-radius:14px;box-shadow:0 8px 40px rgba(0,0,0,.25);
width:min(96vw,860px);max-height:92vh;
display:flex;flex-direction:column;overflow:hidden">
<div style="display:flex;align-items:center;justify-content:space-between;
padding:12px 18px;border-bottom:1px solid #e2e8f0;flex-shrink:0">
<span style="font-weight:700;font-size:.95rem;color:#1e293b">
<i class="fas fa-file-signature me-2 text-primary"></i>Formulario de consentimiento
</span>
<button id="btn-cerrar-modal-consent" onclick="cerrarModalConsentimiento()"
style="background:none;border:none;font-size:1.2rem;color:#94a3b8;cursor:pointer;padding:2px 6px"
title="Cerrar">
<i class="fas fa-times"></i>
</button>
</div>
<div id="modal-consent-loading" style="padding:32px;text-align:center;color:#64748b;flex-shrink:0">
<i class="fas fa-spinner fa-spin fa-2x mb-2 d-block"></i>Cargando formulario…
</div>
<iframe id="modal-consent-iframe" src="" frameborder="0"
style="flex:1;border:none;display:none;min-height:60vh"
onload="document.getElementById('modal-consent-loading').style.display='none';this.style.display='block'">
</iframe>
</div>
</div>
<script>
// ── Estado global ─────────────────────────────────────────────
const LUGAR_FORZADO = <?= $lugarForzado ?: 0 ?>;
let lugarId = <?= $lugarIdParam ?: 0 ?>;
let turnoActivo = null;
let tieneConsent = false;
let hayPendientes = false;
let pollingColaId = null;
let pollingConsentId = null;
let _consentTokenCache = null;
let _pacienteActivo = null;
let _tomaProgresivaActiva = false;
let hayMuestrasPendientes = false;
let _solicitudActiva = null;
let _muestrasActivas = [];
const API = '<?= BASE_URL ?>modules/turnero/api/';
const BASE_WA = '<?= BASE_URL ?>';
const LUGAR_NOMBRE = document.getElementById('lbl-lugar-titulo')?.textContent || '<?= addslashes($lugarNombre) ?>';
const LUGAR_FORM_MODO = '<?= $lugarFormModo ?>';
const LUGAR_TIPO = '<?= $lugarTipo ?>';
// ── Arranque ──────────────────────────────────────────────────
document.addEventListener('DOMContentLoaded', () => {
if (lugarId) iniciarPuesto();
});
function iniciarPuesto() {
recuperarTurnoActivo();
cargarCola();
pollingColaId = setInterval(cargarCola, 7000);
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
clearInterval(pollingColaId);
clearInterval(pollingConsentId);
pollingColaId = pollingConsentId = null;
} else if (lugarId) {
cargarCola();
pollingColaId = setInterval(cargarCola, 7000);
}
});
document.addEventListener('keydown', e => {
if (e.key === 'Escape' && lugarId &&
document.getElementById('overlay-selector').style.display !== 'none') {
document.getElementById('overlay-selector').style.display = 'none';
}
});
}
async function recuperarTurnoActivo() {
try {
const res = await fetch(`${API}get_cola.php?area=lugar&lugar_id=${lugarId}`);
const json = await res.json();
if (json.ok && json.activo) await abrirFicha(json.activo);
} catch (_) {}
}
// ── Selector de lugar ─────────────────────────────────────────
function confirmarLugar() {
const sel = document.getElementById('sel-lugar-init');
const id = parseInt(sel.value);
if (!id) return mostrarError('Seleccione un lugar.');
if (LUGAR_FORZADO && id !== LUGAR_FORZADO) return mostrarError('No tiene acceso a esa estación.');
lugarId = id;
const txt = sel.options[sel.selectedIndex].text;
document.getElementById('lbl-lugar-titulo').textContent = txt;
document.getElementById('overlay-selector').style.display = 'none';
document.title = txt + ' — Turnero';
iniciarPuesto();
}
function cambiarLugar() {
clearInterval(pollingColaId);
clearInterval(pollingConsentId);
turnoActivo = null;
resetFicha();
document.getElementById('btn-cancelar-selector').style.display = '';
document.getElementById('overlay-selector').style.display = 'flex';
}
// ── Cola ──────────────────────────────────────────────────────
async function cargarCola() {
if (!lugarId) return;
try {
const res = await fetch(`${API}get_cola.php?area=lugar&lugar_id=${lugarId}`);
const json = await res.json();
if (!json.ok) return;
renderCola(json);
} catch (_) {}
}
let _colaMap = new Map();
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 items = snap.cola || [];
const enEspera = items.filter(t => t.estado === 'en_espera_lugar');
const enServ = items.filter(t => t.estado === 'en_servicio');
document.getElementById('badge-count').textContent = enEspera.length + ' espera';
const badgeServ = document.getElementById('badge-servicio');
if (enServ.length) {
badgeServ.textContent = enServ.length + ' servicio';
badgeServ.classList.remove('d-none');
} else {
badgeServ.classList.add('d-none');
}
_colaMap = new Map(items.map(t => [t.id, t]));
const lista = document.getElementById('cola-items');
if (!items.length) {
lista.innerHTML = `<div class="cola-vacia">
<i class="fas fa-hourglass-start fa-2x mb-2 d-block"></i>Cola vacía</div>`;
return;
}
let html = '';
if (enServ.length) {
html += `<div class="cola-section-sep"><i class="fas fa-stethoscope me-1"></i>En servicio</div>`;
html += enServ.map(t => renderColaCard(t)).join('');
}
if (enEspera.length) {
if (enServ.length) html += `<div class="cola-section-sep" style="margin-top:.4rem"><i class="fas fa-clock me-1"></i>En espera</div>`;
html += enEspera.map(t => renderColaCard(t)).join('');
}
lista.innerHTML = html;
}
function renderColaCard(t) {
const enServicio = t.estado === 'en_servicio';
const esActivo = turnoActivo?.id == t.id;
const esMuestra = t.solo_muestras == 1;
const enTomaProg = t.en_toma_progresiva == 1;
const sigTomaAt = t.siguiente_toma_at || null;
const tiempo = tiempoEspera(t.creado_at);
const tiempoBadge = tiempo
? `<span class="tiempo-chip ${tiempo.cls}">${escHtml(tiempo.txt)}</span>`
: '';
const muestraBadge = esMuestra
? `<span class="muestra-chip"><i class="fas fa-plus me-1"></i>MUESTRAS</span>`
: '';
const tomaBadge = enTomaProg
? `<span class="toma-prog-chip ok" data-toma-at="${escHtml(sigTomaAt || '')}" data-turno-id="${t.id}">
<i class="fas fa-syringe me-1"></i><span class="toma-cd-txt">${sigTomaAt ? '...' : '¡Ahora!'}</span>
</span>`
: '';
const badgeRow = (tiempoBadge || muestraBadge || tomaBadge)
? `<div class="t-badge-row">${tiempoBadge}${muestraBadge}${tomaBadge}</div>`
: '';
const rightEl = enServicio
? `<div class="card-en-servicio-pill"><i class="fas fa-stethoscope"></i>EN<br>SERV.</div>`
: `<button onclick="event.stopPropagation();llamarTurnoEspecifico(${t.id})"
title="Anunciar turno ${escHtml(t.codigo)}" class="card-bell-btn">
<i class="fas fa-bell"></i>
</button>`;
let bg = esMuestra ? 'border:2px solid #ea580c!important;background:#fff7ed!important' : '';
const prioBg = esMuestra ? '#ea580c' : (t.prioridad_color || '#6366f1');
const prioLbl = esMuestra ? '<i class="fas fa-plus"></i>' : escHtml(t.prioridad_codigo || '?');
const tomaClass = enTomaProg ? ' toma-prog' : '';
return `<div class="cola-card ${enServicio ? 'en-servicio-card' : ''} ${esActivo ? 'activo' : ''}${tomaClass}"
style="${bg}" onclick="seleccionarSinLlamar(${t.id})">
<div class="prio-dot" style="background:${prioBg}">${prioLbl}</div>
<div class="turno-info">
<div class="cod">${escHtml(t.codigo)}</div>
<div class="pac">${escHtml(t.paciente_nombre || 'Paciente')}</div>
${badgeRow}
</div>
${rightEl}
</div>`;
}
// ── Seleccionar sin llamar ────────────────────────────────────
async function seleccionarSinLlamar(turnoId) {
const t = _colaMap.get(turnoId);
if (!t) return;
turnoActivo = t;
mostrarCabeceraTurno(t);
await cargarFichaSolicitud(t.id);
clearInterval(pollingConsentId);
if (!_esSoloEntrega(t))
pollingConsentId = setInterval(() => actualizarConsentimientos(turnoActivo?.id), 5000);
if (LUGAR_FORM_MODO === 'embebido' && lugarId && !_esSoloEntrega(t))
cargarFormEmbebido(t.id);
mostrarFichaMobile();
}
// ── Llamar ────────────────────────────────────────────────────
async function llamarSiguiente() { await _llamar({}); }
async function llamarTurnoEspecifico(turnoId) { await _llamar({ turno_id: turnoId }); }
async function _llamar(extra) {
if (!lugarId) { mostrarError('Primero seleccione un lugar.'); return; }
const btn = document.getElementById('btn-llamar');
btn.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: 'lugar', lugar_id: lugarId, ...extra }),
});
const json = await res.json();
if (!json.ok) { mostrarError(json.error); return; }
if (!json.turno) { mostrarToast('Cola vacía — no hay turnos en espera.', 'warn', 3000); return; }
mostrarLlamando(json.turno.codigo);
await abrirFicha(json.turno);
cargarCola();
} catch (err) {
mostrarError(err.message);
} finally {
btn.disabled = false;
}
}
// ── Abrir ficha ───────────────────────────────────────────────
function _esSoloEntrega(turno) {
return turno.solo_muestras == 1 || turno.prioridad_codigo === 'F';
}
async function abrirFicha(turno) {
turnoActivo = turno;
mostrarCabeceraTurno(turno);
await cargarFichaSolicitud(turno.id);
clearInterval(pollingConsentId);
if (!_esSoloEntrega(turno))
pollingConsentId = setInterval(() => actualizarConsentimientos(turnoActivo?.id), 5000);
if (LUGAR_FORM_MODO === 'embebido' && lugarId && !_esSoloEntrega(turno))
cargarFormEmbebido(turno.id);
mostrarFichaMobile();
}
function mostrarCabeceraTurno(turno) {
document.getElementById('ficha-placeholder').classList.add('d-none');
const fichaEl = document.getElementById('ficha-turno');
fichaEl.classList.remove('d-none');
fichaEl.style.display = 'flex';
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 || '#6366f1';
const badgeCod = document.getElementById('badge-turno-activo');
badgeCod.textContent = turno.codigo;
badgeCod.classList.remove('d-none');
const nomHdr = document.getElementById('ficha-pac-nombre-hdr');
nomHdr.textContent = '';
nomHdr.classList.add('loading');
document.getElementById('ficha-mobile-cod').textContent = turno.codigo;
actualizarEstadoBadge(turno.estado);
}
// ── Cargar solicitud ──────────────────────────────────────────
async function cargarFichaSolicitud(turnoId) {
try {
const res = await fetch(`${API}get_consentimientos.php?turno_id=${turnoId}&incluir_solicitud=1`);
const json = await res.json();
if (!json.ok) return;
const sol = json.solicitud;
const pac = json.paciente;
_solicitudActiva = sol || null;
// Número de orden (F-... azul, D-... verde)
const ordenEl = document.getElementById('ficha-orden');
const ordenValEl = document.getElementById('ficha-orden-val');
if (sol && sol.numero_orden) {
ordenValEl.textContent = sol.numero_orden;
const isDom = sol.numero_orden.startsWith('D-');
ordenEl.style.background = isDom ? '#f0fdf4' : '#eff6ff';
ordenEl.style.color = isDom ? '#15803d' : '#1d4ed8';
ordenEl.style.borderColor = isDom ? '#86efac' : '#bfdbfe';
ordenEl.classList.remove('d-none');
} else {
ordenEl.classList.add('d-none');
}
const exams = json.examenes || [];
const consts = json.consentimientos || [];
// Nombre en el header
const nomHdr = document.getElementById('ficha-pac-nombre-hdr');
nomHdr.classList.remove('loading');
nomHdr.textContent = pac ? (pac.nombre_completo || pac.full_name || '—') : '—';
// Badge solo entrega de muestras
const esSoloMuestras = !!(sol && sol.solo_muestras == 1);
document.getElementById('badge-solo-muestras')?.classList.toggle('d-none', !esSoloMuestras);
// Ocultar sección consentimientos y formulario embebido cuando es solo entrega
const secConsent = document.getElementById('sec-consent');
const secForm = document.getElementById('sec-form-embebido');
if (secConsent) secConsent.classList.toggle('d-none', esSoloMuestras);
if (secForm) secForm.classList.add('d-none');
// Embarazada
const badgeEmb = document.getElementById('pac-embarazada');
if (badgeEmb) badgeEmb.classList.toggle('d-none', !(sol && sol.embarazada == 1));
// Médico
const medDiv = document.getElementById('pac-medico');
if (medDiv) {
const tieneMedico = sol && sol.medico_nombre;
medDiv.classList.toggle('d-none', !tieneMedico);
if (tieneMedico) {
const esp = sol.medico_especialidad ? ' · ' + sol.medico_especialidad : '';
document.getElementById('pac-medico-nombre').textContent = sol.medico_nombre + esp;
}
}
// Nota de recepción
const obsEl = document.getElementById('pac-obs-recepcion');
if (obsEl) {
const obs = sol?.observaciones?.trim();
obsEl.classList.toggle('d-none', !obs);
if (obs) document.getElementById('pac-obs-recepcion-texto').textContent = obs;
}
if (pac) {
_pacienteActivo = pac;
document.getElementById('bloque-pac-info').style.display = '';
document.getElementById('bloque-pac-sin').classList.add('d-none');
document.getElementById('pac-nombre').textContent =
pac.nombre_completo || pac.full_name || '—';
document.getElementById('pac-doc').textContent =
((pac.tipo_documento || '') + ' ' + (pac.numero_documento || pac.documento || '')).trim() || '—';
document.getElementById('pac-fec').textContent = pac.fecha_nacimiento || '—';
document.getElementById('pac-cel').textContent = pac.telefono || pac.celular || '—';
document.getElementById('btn-ver-paciente').classList.remove('d-none');
} else {
_pacienteActivo = null;
document.getElementById('bloque-pac-info').style.display = 'none';
document.getElementById('bloque-pac-sin').classList.remove('d-none');
document.getElementById('btn-ver-paciente').classList.add('d-none');
}
// Exámenes
const listaEx = document.getElementById('lista-examenes-ficha');
if (exams.length) {
listaEx.innerHTML = exams.map(e =>
`<span class="exam-pill"><i class="fas fa-vial"></i>${escHtml(e.codigo)} ${escHtml(e.nombre)}</span>`
).join('');
} else {
listaEx.innerHTML = '<span class="text-muted small">Sin exámenes registrados</span>';
}
renderConsentimientos(consts);
renderMuestras(json.muestras || []);
cargarComentarios(turnoId);
draftRestaurar(turnoId);
} catch (_) {}
}
// ── Consentimientos ───────────────────────────────────────────
const CONSENT_IC = {
firmado:'fa-check-circle', rechazado:'fa-ban',
enviado:'fa-envelope', visto:'fa-eye', pendiente:'fa-clock',
en_progreso:'fa-hourglass-half',
};
const CONSENT_LBL = {
firmado:'Firmado', rechazado:'Rechazado', enviado:'Enviado', visto:'Visto', pendiente:'Pendiente',
en_progreso:'En progreso',
};
// ── Countdown en tarjetas de cola (toma progresiva) ──────────
const _tomaAlertados = new Set();
let _bannerTurnoId = null;
function _playBeepLugar() {
try {
const ctx = new (window.AudioContext || window.webkitAudioContext)();
[880, 1100, 880, 1100].forEach(function(freq, i) {
const osc = ctx.createOscillator(), gain = ctx.createGain();
osc.connect(gain); gain.connect(ctx.destination);
osc.frequency.value = freq;
const t0 = ctx.currentTime + i * 0.22;
gain.gain.setValueAtTime(0.7, t0);
gain.gain.exponentialRampToValueAtTime(0.001, t0 + 0.18);
osc.start(t0); osc.stop(t0 + 0.18);
});
} catch(e) {}
}
function verPacienteToma() {
cerrarBannerToma();
if (_bannerTurnoId) seleccionarSinLlamar(_bannerTurnoId);
}
function cerrarBannerToma() {
const b = document.getElementById('banner-toma-alerta');
if (b) b.classList.remove('show');
}
setInterval(function() {
const ahora = Date.now();
document.querySelectorAll('.toma-prog-chip[data-toma-at]').forEach(function(chip) {
const at = chip.dataset.tomaAt;
const turnoId = chip.dataset.turnoId ? parseInt(chip.dataset.turnoId) : null;
const txt = chip.querySelector('.toma-cd-txt');
if (!at || !txt) return;
const diff = Math.floor((new Date(at.replace(' ','T')).getTime() - ahora) / 1000);
const card = chip.closest('.cola-card');
if (diff <= 0) {
txt.textContent = '¡Ahora!';
chip.className = 'toma-prog-chip crit';
if (card) card.classList.add('crit-card');
// Alerta única por turno
if (turnoId && !_tomaAlertados.has(turnoId)) {
_tomaAlertados.add(turnoId);
_bannerTurnoId = turnoId;
const pacNombre = card?.querySelector('.pac')?.textContent || 'Paciente';
const banner = document.getElementById('banner-toma-alerta');
const bannerTxt = document.getElementById('banner-toma-txt');
if (banner && bannerTxt) {
bannerTxt.textContent = '¡Hora de la siguiente muestra! — ' + pacNombre;
banner.classList.add('show');
}
_playBeepLugar();
if (navigator.vibrate) navigator.vibrate([400, 150, 400]);
}
} else {
// Si el tiempo se actualizó (nueva toma programada), resetear la alerta
if (turnoId) _tomaAlertados.delete(turnoId);
const m = Math.floor(diff / 60), s = diff % 60;
txt.textContent = m + ':' + String(s).padStart(2,'0');
chip.className = diff < 120 ? 'toma-prog-chip warn' : 'toma-prog-chip ok';
if (card) card.classList.remove('crit-card');
}
});
}, 1000);
// ── Tomas progresivas: actualiza countdowns cada segundo ──────
let _tomaTimers = {}; // consent_id → { siguiente_toma_at, es_alerta }
setInterval(() => {
const ahora = Date.now();
for (const [cid, info] of Object.entries(_tomaTimers)) {
const row = document.querySelector(`[data-consent-id="${cid}"]`);
if (!row) continue;
const cdEl = row.querySelector('.toma-countdown');
if (!cdEl) continue;
if (!info.siguiente_toma_at) { cdEl.textContent = ''; continue; }
const diff = Math.floor((info.siguiente_toma_at - ahora) / 1000);
if (diff <= 0) {
cdEl.textContent = '¡Hora de toma!';
row.classList.add('alerta');
if (!info.alertado) {
info.alertado = true;
_playBeepLugar();
}
} else {
row.classList.remove('alerta');
const m = Math.floor(diff / 60), s = diff % 60;
cdEl.textContent = `próx. toma en ${m}:${String(s).padStart(2,'0')}`;
}
}
}, 1000);
function _syncBtnFinalizar() {
const btnFin = document.getElementById('btn-finalizar');
const warnStrip = document.getElementById('accion-consent-warn');
const warnTxt = document.getElementById('accion-warn-txt');
const bloqueado = hayPendientes || hayMuestrasPendientes;
if (btnFin) btnFin.disabled = bloqueado;
if (!warnStrip || !warnTxt) return;
if (bloqueado) {
const msgs = [];
if (hayPendientes) msgs.push('consentimientos pendientes');
if (hayMuestrasPendientes) msgs.push('muestras sin decisión');
warnTxt.textContent = msgs.join(' · ');
warnStrip.classList.remove('d-none');
} else {
warnStrip.classList.add('d-none');
}
}
function renderConsentimientos(lista) {
// Mostrar consentimientos de examen (origen null) + los de esta estación
lista = lista.filter(c => !c.origen_lugar_id || c.origen_lugar_id == lugarId);
tieneConsent = lista.length > 0;
hayPendientes = lista.some(c => !['firmado','rechazado'].includes(c.estado));
// en_progreso bloquea finalizar pero no muestra aviso de "pendiente sin acción"
const pendCount = lista.filter(c => ['pendiente','enviado','visto'].includes(c.estado)).length;
const listEl = document.getElementById('lista-consent');
const sinEl = document.getElementById('sin-consent');
const banner = document.getElementById('bloqueo-banner');
const btnFin = document.getElementById('btn-finalizar');
_syncBtnFinalizar();
if (!tieneConsent) {
listEl.innerHTML = '';
sinEl.classList.remove('d-none');
banner.classList.add('d-none');
return;
}
sinEl.classList.add('d-none');
banner.classList.toggle('d-none', !hayPendientes);
// Actualizar mapa de timers para tomas progresivas (preservar alertado si ya sonó)
const _prevTimers = _tomaTimers;
_tomaTimers = {};
lista.forEach(c => {
if (c.es_toma_progresiva && c.estado === 'en_progreso') {
const prev = _prevTimers[c.id];
const newAt = c.siguiente_toma_at ? new Date(c.siguiente_toma_at.replace(' ', 'T')).getTime() : null;
// Si la siguiente_toma_at cambió (firmó una toma), resetear alertado para el nuevo tiempo
const sameTime = prev && prev.siguiente_toma_at === newAt;
_tomaTimers[c.id] = {
siguiente_toma_at: newAt,
alertado: sameTime ? (prev.alertado || false) : false,
};
}
});
listEl.innerHTML = lista.map(c => {
const ya = ['firmado','rechazado'].includes(c.estado);
const ico = CONSENT_IC[c.estado] || 'fa-clock';
const label = CONSENT_LBL[c.estado] || c.estado;
const token = escHtml(c.token || '');
const nomJs = JSON.stringify(c.formulario_nombre || 'Consentimiento');
const idJs = parseInt(c.id) || 0;
const tId = parseInt(c.turno_id) || (turnoActivo?.id) || 0;
const fId = parseInt(c.formulario_id) || 0;
// Badge de progreso para tomas progresivas
let progresoBadge = '';
if (c.es_toma_progresiva && c.tomas_total > 0) {
progresoBadge = `<span class="toma-progreso">${c.tomas_firmadas}/${c.tomas_total}</span>`;
}
const countdownEl = (c.es_toma_progresiva && c.estado === 'en_progreso')
? `<span class="toma-countdown"></span>` : '';
// Botón ver
const btnVer = (ya && c.token)
? `<a href="${BASE_WA}ver_formulario_enviado.php?token=${token}" target="_blank"
class="btn btn-outline-secondary" title="Ver firmado">
<i class="fas fa-eye"></i> Ver
</a>` : '';
// Acción de firma según modo
let btnAccion = '';
const puedeFirmar = !ya || (c.es_toma_progresiva && c.estado === 'en_progreso');
if (puedeFirmar && c.token) {
if (c.es_toma_progresiva) {
// Toma progresiva: siempre abre el modal embebido para firmar la siguiente toma
btnAccion = `<button class="btn btn-primary" title="Registrar siguiente toma"
onclick="abrirTomaProgresiva('${token}', ${tId}, ${fId})">
<i class="fas fa-flask me-1"></i>Siguiente toma
</button>`;
} else if (LUGAR_FORM_MODO === 'embebido') {
btnAccion = `<button class="btn btn-primary" title="Abrir formulario"
onclick="abrirModalConsentimientoPorToken('${token}')">
<i class="fas fa-pen me-1"></i>Firmar
</button>`;
} else {
btnAccion = `<button class="btn btn-outline-primary" title="Firmar aquí"
onclick="abrirFirmaPresencial('${token}', ${idJs}, ${nomJs.replace(/"/g,'&quot;')})">
<i class="fas fa-signature"></i> Firmar
</button>
${c.requiere_firma_paciente ? `<button class="btn btn-outline-success" title="Reenviar por WhatsApp"
onclick="reenviarConsentimiento(${tId})">
<i class="fab fa-whatsapp"></i> WA
</button>` : ''}`;
}
}
return `<div class="consent-row ${c.estado}" data-consent-id="${c.id}">
<i class="fas ${ico}"></i>
<span class="nom-form">${escHtml(c.formulario_nombre || 'Consentimiento')}</span>
${progresoBadge}
${countdownEl}
<span class="c-badge">${label}</span>
<div class="acciones-consent">${btnAccion}${btnVer}</div>
</div>`;
}).join('');
}
async function actualizarConsentimientos(turnoId) {
if (!turnoId) return;
try {
const res = await fetch(`${API}get_consentimientos.php?turno_id=${turnoId}`);
const json = await res.json();
if (!json.ok) return;
renderConsentimientos(json.consentimientos || []);
} catch (_) {}
}
async function reenviarConsentimiento(turnoId) {
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; }
await actualizarConsentimientos(turnoId);
} catch (err) {
mostrarError(err.message);
}
}
async function abrirFirmaPresencial(token, consentId, nombreForm) {
try {
const res = await fetch(API + 'crear_envio_presencial.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ consent_token: token }),
});
const json = await res.json();
if (!json.ok || !json.form_token) throw new Error(json.error || 'Error al crear envío');
window.open(BASE_WA + 'form_cliente.php?t=' + encodeURIComponent(json.form_token), '_blank');
} catch (err) {
alert('No se pudo abrir el formulario: ' + err.message);
}
}
// ── Modal de consentimiento embebido ─────────────────────────
function _abrirModalConToken(token) {
const modal = document.getElementById('modal-consentimiento');
const iframe = document.getElementById('modal-consent-iframe');
const load = document.getElementById('modal-consent-loading');
iframe.style.display = 'none';
iframe.src = '';
load.style.display = '';
modal.style.display = 'flex';
setTimeout(() => {
iframe.src = `${BASE_WA}ver_formulario_enviado.php?token=${encodeURIComponent(token)}&embed=1&compact=1`;
}, 80);
}
function abrirModalConsentimientoPorToken(token) {
_abrirModalConToken(token);
}
// ── Toma progresiva: abre el modal con modo especial ──────────
function abrirTomaProgresiva(token, turnoId, formularioId) {
cerrarBannerToma();
_tomaAlertados.delete(turnoId);
const modal = document.getElementById('modal-consentimiento');
const iframe = document.getElementById('modal-consent-iframe');
const load = document.getElementById('modal-consent-loading');
iframe.style.display = 'none';
iframe.src = '';
load.style.display = '';
modal.style.display = 'flex';
_tomaProgresivaActiva = true;
_setBtnCerrarConsent(true);
setTimeout(() => {
iframe.src = `${BASE_WA}ver_formulario_enviado.php?token=${encodeURIComponent(token)}&embed=1&compact=1&modo=toma_progresiva`;
}, 80);
}
function _setBtnCerrarConsent(habilitado) {
const btn = document.getElementById('btn-cerrar-modal-consent');
if (!btn) return;
btn.disabled = !habilitado;
btn.style.opacity = habilitado ? '' : '.25';
btn.title = habilitado ? 'Cerrar' : 'Cerrando...';
}
function cerrarModalConsentimiento(forzar) {
_tomaProgresivaActiva = false;
_setBtnCerrarConsent(true);
const modal = document.getElementById('modal-consentimiento');
const iframe = document.getElementById('modal-consent-iframe');
modal.style.display = 'none';
iframe.src = '';
iframe.style.display = 'none';
document.getElementById('modal-consent-loading').style.display = '';
if (turnoActivo) actualizarConsentimientos(turnoActivo.id);
}
window.addEventListener('message', function(e) {
if (e.data && e.data.type === 'tomaProgresivaIniciada') {
_tomaProgresivaActiva = true;
// Botón cerrar permanece habilitado; el operador puede cerrar en cualquier momento
}
if (e.data && e.data.type === 'turneroFirmado') {
_tomaProgresivaActiva = false;
_setBtnCerrarConsent(true);
cerrarModalConsentimiento(true);
cerrarBannerToma();
_tomaAlertados.clear();
mostrarToast('Tomas completadas ✓', 'success', 3000);
if (turnoActivo) actualizarConsentimientos(turnoActivo.id);
}
});
// ── Formulario embebido (iframe, modo link) ───────────────────
async function cargarFormEmbebido(turnoId) {
if (LUGAR_FORM_MODO === 'embebido') return; // token llega vía renderConsentimientos → modal
const sec = document.getElementById('sec-form-embebido');
const iframe = document.getElementById('form-embebido-iframe');
const load = document.getElementById('form-embebido-loading');
if (!sec || !iframe) return;
sec.classList.remove('d-none');
iframe.style.display = 'none';
load.style.display = '';
try {
const res = await fetch(`${API}get_consent_token.php?turno_id=${turnoId}&lugar_id=${lugarId}`);
const json = await res.json();
if (json.ok && json.token) {
iframe.src = `${BASE_WA}ver_formulario_enviado.php?token=${encodeURIComponent(json.token)}`;
} else {
load.innerHTML = '<span class="text-danger small"><i class="fas fa-exclamation-triangle me-1"></i>' + escHtml(json.error || 'No hay formulario configurado') + '</span>';
}
} catch (e) {
load.innerHTML = '<span class="text-danger small"><i class="fas fa-exclamation-triangle me-1"></i>Error al cargar formulario</span>';
}
}
// ── Acciones del turno ────────────────────────────────────────
async function iniciarAtencion() {
if (!turnoActivo) return;
await _llamar({ turno_id: turnoActivo.id });
}
async function finalizarAtencion() {
if (!turnoActivo) return;
if (hayPendientes) { mostrarError('Debe firmar todos los consentimientos antes de finalizar.'); return; }
if (hayMuestrasPendientes) { mostrarError('Debe marcar cada muestra como recibida o rechazada antes de finalizar.'); return; }
if (!confirm(`¿Finalizar atención del turno ${turnoActivo.codigo}?`)) return;
await cambiarEstadoTurno('finalizado');
}
async function marcarAusente() {
if (!turnoActivo) return;
if (!confirm(`¿Marcar turno ${turnoActivo.codigo} como AUSENTE?`)) return;
await cambiarEstadoTurno('ausente');
}
async function regresarCola() {
if (!turnoActivo) return;
if (!confirm(`¿Regresar turno ${turnoActivo.codigo} a la cola?`)) return;
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: lugarId }),
});
const json = await res.json();
if (!json.ok) { mostrarError(json.error); return; }
resetFicha();
cargarCola();
} catch (err) {
mostrarError(err.message);
}
}
async function cambiarEstadoTurno(nuevoEstado) {
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: nuevoEstado }),
});
const json = await res.json();
if (!json.ok) { mostrarError(json.error); return; }
resetFicha();
cargarCola();
} catch (err) {
mostrarError(err.message);
}
}
// ── Reset ficha ───────────────────────────────────────────────
function resetFicha() {
clearInterval(pollingConsentId);
turnoActivo = null;
tieneConsent = false;
hayPendientes = false;
hayMuestrasPendientes = false;
_consentTokenCache = null;
_pacienteActivo = null;
_solicitudActiva = null;
_muestrasActivas = [];
const secMuestras = document.getElementById('sec-muestras');
if (secMuestras) secMuestras.classList.add('d-none');
// Restaurar sección consent (puede haber sido ocultada por turno solo-muestras)
document.getElementById('sec-consent')?.classList.remove('d-none');
document.getElementById('badge-solo-muestras')?.classList.add('d-none');
document.getElementById('ficha-orden').classList.add('d-none');
const obsRec = document.getElementById('pac-obs-recepcion');
if (obsRec) obsRec.classList.add('d-none');
cerrarModalPaciente();
document.getElementById('btn-ver-paciente').classList.add('d-none');
cerrarModalConsentimiento();
const iframe = document.getElementById('form-embebido-iframe');
const sec = document.getElementById('sec-form-embebido');
const load = document.getElementById('form-embebido-loading');
if (iframe) { iframe.src = ''; iframe.style.display = 'none'; }
if (sec) { sec.classList.add('d-none'); }
if (load) { load.style.display = ''; load.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Cargando formulario...'; }
const fichaEl = document.getElementById('ficha-turno');
fichaEl.classList.add('d-none');
fichaEl.style.display = 'none';
document.getElementById('ficha-placeholder').classList.remove('d-none');
document.getElementById('badge-turno-activo').classList.add('d-none');
const nomHdr = document.getElementById('ficha-pac-nombre-hdr');
if (nomHdr) { nomHdr.textContent = ''; nomHdr.classList.remove('loading'); }
const warn = document.getElementById('accion-consent-warn');
if (warn) warn.classList.add('d-none');
const inpCom = document.getElementById('inp-comentario');
if (inpCom) { inpCom.value = ''; inpCom.style.borderColor = ''; inpCom.title = ''; }
document.getElementById('btn-iniciar').classList.remove('d-none');
document.getElementById('btn-iniciar').disabled = false;
document.getElementById('btn-finalizar').classList.add('d-none');
document.getElementById('btn-regresar').classList.add('d-none');
document.getElementById('btn-rellamar').classList.add('d-none');
volverACola();
}
// ── Mobile ────────────────────────────────────────────────────
function esMobile() { return window.innerWidth <= 680; }
function mostrarFichaMobile() {
if (!esMobile()) return;
document.getElementById('lugar-ficha').classList.add('mobile-visible');
document.querySelector('.lugar-cola').classList.add('mobile-oculta');
}
function volverACola() {
document.getElementById('lugar-ficha').classList.remove('mobile-visible');
document.querySelector('.lugar-cola').classList.remove('mobile-oculta');
}
// ── Estado badge + botones ────────────────────────────────────
function actualizarEstadoBadge(estado) {
const el = document.getElementById('ficha-estado-badge');
const mapa = {
'en_espera_lugar': ['bg-warning-subtle text-warning border border-warning-subtle', 'En espera'],
'en_servicio': ['bg-success-subtle text-success border border-success-subtle', 'En servicio'],
'finalizado': ['bg-secondary-subtle text-secondary border', 'Finalizado'],
'ausente': ['bg-danger-subtle text-danger border border-danger-subtle', 'Ausente'],
};
const [cls, lbl] = mapa[estado] || ['bg-secondary-subtle text-secondary border', estado];
el.className = 'badge ' + cls;
el.textContent = lbl;
const btnIni = document.getElementById('btn-iniciar');
const btnFin = document.getElementById('btn-finalizar');
const btnReg = document.getElementById('btn-regresar');
const btnRellamar = document.getElementById('btn-rellamar');
if (estado === 'en_espera_lugar') {
btnIni.classList.remove('d-none'); btnIni.disabled = false;
btnFin.classList.add('d-none');
btnReg.classList.add('d-none');
btnRellamar.classList.add('d-none');
} else if (estado === 'en_servicio') {
btnIni.classList.add('d-none');
btnFin.classList.remove('d-none');
btnReg.classList.remove('d-none');
btnRellamar.classList.remove('d-none');
}
}
async function rellamarTurno() {
if (!turnoActivo) return;
const btn = document.getElementById('btn-rellamar');
btn.disabled = true;
try {
const res = await fetch(API + 'llamar_turno.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ area: 'lugar', lugar_id: lugarId, turno_id: turnoActivo.id }),
});
const json = await res.json();
if (!json.ok) mostrarError(json.error);
else if (json.turno) mostrarLlamando(json.turno.codigo);
} catch (err) {
mostrarError(err.message);
} finally {
btn.disabled = false;
}
}
// ── Comentarios ───────────────────────────────────────────────
async function cargarComentarios(turnoId) {
if (!turnoId) return;
const cont = document.getElementById('lista-comentarios');
cont.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Cargando…';
try {
const res = await fetch(`${API}comentarios.php?turno_id=${turnoId}`);
const json = await res.json();
if (!json.ok || !json.comentarios) { cont.innerHTML = '<small class="text-muted">Sin comentarios</small>'; return; }
if (!json.comentarios.length) { cont.innerHTML = '<small class="text-muted">Sin comentarios aún</small>'; return; }
cont.innerHTML = json.comentarios.map(c => `
<div style="background:#f8fafc;border-left:3px solid #8b5cf6;padding:8px 10px;margin-bottom:8px;border-radius:4px">
<div style="font-size:.7rem;color:#94a3b8;margin-bottom:2px">${escHtml(c.usuario_nombre)} • ${new Date(c.creado_at).toLocaleString()}</div>
<div style="font-size:.82rem;color:#334155">${escHtml(c.comentario)}</div>
</div>
`).join('');
} catch (_) {
cont.innerHTML = '<small class="text-danger">Error al cargar comentarios</small>';
}
}
async function guardarComentario() {
if (!turnoActivo) return;
const inp = document.getElementById('inp-comentario');
const txt = (inp.value || '').trim();
if (!txt) { mostrarError('El comentario no puede estar vacío'); return; }
try {
const res = await fetch(API + 'comentarios.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ turno_id: turnoActivo.id, comentario: txt, tipo: 'muestras' }),
});
const json = await res.json();
if (!json.ok) { mostrarError(json.error || 'Error al guardar'); return; }
inp.value = '';
draftBorrar();
await cargarComentarios(turnoActivo.id);
mostrarToast('Comentario guardado', 'success', 2000);
} catch (err) {
mostrarError(err.message);
}
}
// ── Borrador de comentario (localStorage) ─────────────────────
function _draftKey() { return turnoActivo ? 'toma_draft_' + turnoActivo.id : null; }
function draftGuardar() {
const key = _draftKey();
if (!key) return;
const val = (document.getElementById('inp-comentario')?.value || '').trim();
if (val) localStorage.setItem(key, val);
else localStorage.removeItem(key);
}
function draftRestaurar(turnoId) {
const key = 'toma_draft_' + turnoId;
const val = localStorage.getItem(key);
const inp = document.getElementById('inp-comentario');
if (!inp) return;
if (val) {
inp.value = val;
inp.style.borderColor = '#f59e0b';
inp.title = 'Borrador restaurado — no guardado aún';
} else {
inp.value = '';
inp.style.borderColor = '';
inp.title = '';
}
}
function draftBorrar() {
const key = _draftKey();
if (key) localStorage.removeItem(key);
const inp = document.getElementById('inp-comentario');
if (inp) { inp.style.borderColor = ''; inp.title = ''; }
}
// ── Modal paciente ────────────────────────────────────────────
function abrirModalPaciente() {
if (!_pacienteActivo) return;
const pac = _pacienteActivo;
const sol = _solicitudActiva;
document.getElementById('mpac-nombre-titulo').textContent =
pac.nombre_completo || pac.full_name || 'Paciente';
document.getElementById('mpac-val-nombre').textContent =
pac.nombre_completo || pac.full_name || '—';
document.getElementById('mpac-val-doc').textContent =
((pac.tipo_documento || '') + ' ' + (pac.numero_documento || pac.documento || '')).trim() || '—';
document.getElementById('mpac-val-fec').textContent = pac.fecha_nacimiento || '—';
document.getElementById('mpac-val-cel').textContent = pac.telefono || pac.celular || '—';
const rowMed = document.getElementById('mpac-row-medico');
if (sol && sol.medico_nombre) {
rowMed.style.display = '';
const esp = sol.medico_especialidad ? ' · ' + sol.medico_especialidad : '';
document.getElementById('mpac-val-medico').textContent = sol.medico_nombre + esp;
} else {
rowMed.style.display = 'none';
}
const badgeEmb = document.getElementById('mpac-badge-emb');
badgeEmb.classList.toggle('d-none', !(sol && sol.embarazada == 1));
document.getElementById('mpac-historial-content').innerHTML =
'<div class="text-center text-muted py-3 small"><i class="fas fa-spinner fa-spin me-1"></i>Cargando historial…</div>';
document.getElementById('modal-pac-lugar').classList.add('show');
cargarHistorialModal(pac.id);
}
function cerrarModalPaciente() {
document.getElementById('modal-pac-lugar').classList.remove('show');
}
async function cargarHistorialModal(pacienteId) {
const cont = document.getElementById('mpac-historial-content');
try {
const res = await fetch(`${API}get_historial.php?paciente_id=${pacienteId}&per_page=10&page=1`);
const json = await res.json();
if (!json.ok || !json.turnos || !json.turnos.length) {
cont.innerHTML = '<div class="text-center text-muted py-3 small"><i class="fas fa-inbox me-1"></i>Sin visitas anteriores registradas</div>';
return;
}
cont.innerHTML = renderHistorialTimeline(json.turnos, json.total);
} catch(_) {
cont.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 totalLabel = total > turnos.length
? `<div style="font-size:.67rem;color:#94a3b8;text-align:right;padding:4px 0">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 = escHtml(t.fecha_sesion || '—');
const est = t.estado || '';
const eStyle = ESTADO_STYLE[est] || 'background:#f1f5f9;color:#334155';
const eLbl = escHtml(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>` : '';
let examStr = '';
if (t.examenes && t.examenes.length) {
examStr = `<div style="margin-top:5px;font-size:.74rem">
<strong style="color:#64748b">Exámenes:</strong>
<span style="color:#475569">${t.examenes.map(e => escHtml(e.nombre)).join(', ')}</span>
</div>`;
}
let comStr = '';
if (t.comentarios && t.comentarios.length) {
comStr = `<div style="margin-top:5px;font-size:.71rem;border-top:1px solid #e2e8f0;padding-top:5px">
<strong style="color:#64748b">📝 Comentarios:</strong>
<div style="color:#475569;margin-top:3px">${t.comentarios.map(c =>
`<div style="margin-bottom:2px"><strong>${escHtml(c.usuario_nombre)}</strong>: ${escHtml(c.comentario)}</div>`
).join('')}</div>
</div>`;
}
return `<div class="mtl-item">
<div class="mtl-dot" style="color:${color};background:${color}"></div>
<div class="mtl-content">
<div class="mtl-fecha">${fecha}</div>
<div class="mtl-codigo" style="color:${color}">${escHtml(t.codigo)}</div>
<div class="mtl-lugar">${lugar}</div>
<span class="mtl-eb" style="${eStyle}">${eLbl}</span>${mins}
${examStr}${comStr}
</div>
</div>`;
}).join('');
return `<div class="mpac-timeline">${items}</div>${totalLabel}`;
}
// ── Muestras ──────────────────────────────────────────────────
function renderMuestras(lista) {
_muestrasActivas = lista || [];
const sec = document.getElementById('sec-muestras');
const cont = document.getElementById('lista-muestras');
const summary = document.getElementById('muestras-summary');
if (!sec) return;
// Solo mostrar en estaciones tipo "muestras"
if (LUGAR_TIPO !== 'muestras' || !lista.length) {
sec.classList.add('d-none');
return;
}
sec.classList.remove('d-none');
const nPend = lista.filter(m => m.estado === 'pendiente').length;
const nRec = lista.filter(m => m.estado === 'recibida').length;
const nRech = lista.filter(m => m.estado === 'rechazada').length;
hayMuestrasPendientes = nPend > 0;
_syncBtnFinalizar();
let chips = '';
if (nPend) chips += `<span class="mc pend">${nPend} pendiente${nPend > 1 ? 's' : ''}</span>`;
if (nRec) chips += `<span class="mc rec">${nRec} recibida${nRec > 1 ? 's' : ''}</span>`;
if (nRech) chips += `<span class="mc rech">${nRech} rechazada${nRech > 1 ? 's' : ''}</span>`;
summary.innerHTML = chips;
cont.innerHTML = lista.map(m => {
const esPend = m.estado === 'pendiente';
const esPrevio = !!m.es_pendiente_anterior;
const label = escHtml(m.label || m.tipo_muestra || 'MUESTRA');
const origenTag = esPrevio
? `<span style="font-size:.68rem;background:#fef3c7;color:#92400e;border:1px solid #fde68a;
border-radius:10px;padding:1px 7px;margin-left:5px;white-space:nowrap">
<i class="fas fa-history me-1"></i>visita anterior${m.solicitud_orden_anterior ? ' · ' + escHtml(m.solicitud_orden_anterior) : ''}
</span>`
: '';
const tsHtml = m.recibida_at
? `<span class="muestra-ts">${formatHora(m.recibida_at)}</span>`
: '';
const motivoHtml = m.motivo_rechazo
? `<div class="muestra-motivo">${escHtml(m.motivo_rechazo)}</div>` : '';
const acciones = esPend
? `<div style="display:flex;gap:5px;flex-shrink:0">
<button class="btn-muestra-accion recibir"
onclick="marcarMuestra(${m.id},'recibida')">
<i class="fas fa-check"></i> Recibida
</button>
<button class="btn-muestra-accion rechazar"
onclick="pedirRechazo(${m.id},'${label.replace(/'/g,'\\\'')}')" >
<i class="fas fa-times"></i> Rechazar
</button>
</div>`
: `${tsHtml}
<button class="btn-muestra-accion"
style="background:#f1f5f9;color:#64748b;border:1px solid #e2e8f0"
onclick="marcarMuestra(${m.id},'pendiente')" title="Revertir a pendiente">
<i class="fas fa-undo"></i>
</button>`;
const ico = m.estado === 'recibida'
? '<i class="fas fa-check-circle" style="color:#16a34a;flex-shrink:0"></i>'
: m.estado === 'rechazada'
? '<i class="fas fa-times-circle" style="color:#dc2626;flex-shrink:0"></i>'
: '<i class="fas fa-clock" style="color:#d97706;flex-shrink:0"></i>';
return `<div class="muestra-row ${m.estado}" data-muestra-id="${m.id}">
${ico}
<div class="muestra-info">
<span class="muestra-label">${label}</span>${origenTag}
${motivoHtml}
</div>
${acciones}
</div>`;
}).join('');
}
async function marcarMuestra(muestraId, estado, motivo = null) {
try {
const body = { muestra_id: muestraId, estado };
if (motivo) body.motivo_rechazo = motivo;
const res = await fetch(API + 'update_muestra_estado.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
const json = await res.json();
if (!json.ok) { mostrarError(json.error); return; }
// Actualizar estado local sin re-fetch
const m = _muestrasActivas.find(x => x.id == muestraId);
if (m) {
m.estado = estado;
m.motivo_rechazo = motivo;
m.recibida_at = estado !== 'pendiente' ? new Date().toISOString() : null;
}
renderMuestras(_muestrasActivas);
const msg = estado === 'recibida'
? 'Muestra recibida ✓'
: estado === 'rechazada' ? 'Muestra rechazada' : 'Revertida a pendiente';
mostrarToast(msg, estado === 'recibida' ? 'success' : estado === 'rechazada' ? 'warn' : 'info', 2000);
} catch (err) {
mostrarError(err.message);
}
}
function pedirRechazo(muestraId, tipoMuestra) {
const motivo = prompt(`Motivo de rechazo para "${tipoMuestra}":\n(hemólisis, coagulado, volumen insuficiente…)`);
if (motivo === null) return;
marcarMuestra(muestraId, 'rechazada', motivo.trim() || null);
}
function formatHora(isoStr) {
if (!isoStr) return '';
try {
return new Date(isoStr).toLocaleTimeString('es-CO', { hour: '2-digit', minute: '2-digit' });
} catch (_) { return ''; }
}
// ── Helpers ───────────────────────────────────────────────────
function escHtml(str) {
const d = document.createElement('div');
d.appendChild(document.createTextNode(String(str ?? '')));
return d.innerHTML;
}
let _toastTimer = null;
function mostrarToast(msg, type = 'info', duration = 2500) {
const el = document.getElementById('lug-toast');
const ico = document.getElementById('lug-toast-ico');
const txt = document.getElementById('lug-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);
}
function mostrarError(msg) { mostrarToast(msg, 'error', 4000); }
function mostrarLlamando(cod) { mostrarToast('Llamando turno ' + cod + '…', 'info', 2000); }
/* ── Ocupación exclusiva del puesto ──────────────────────────── */
(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';
}
}
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 puesto 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>';
}
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('✅ Puesto 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;
}
};
window._ocSiguiente = async function() {
const btn = document.getElementById('_btn-siguiente');
if (btn) btn.disabled = true;
_setEstado('Buscando puesto disponible…', '#555');
try {
const j = await _post({ action: 'disponibles' });
if (!j.ok || !j.disponibles.length) {
_setEstado('No hay otros puestos disponibles.', '#c62828');
if (btn) btn.disabled = false;
return;
}
_redirigirSiguiente(j.disponibles);
} catch (e) {
_setEstado('Error de red.', '#c62828');
if (btn) btn.disabled = false;
}
};
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 puesto.</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(() => {});
};
}
async function _iniciar() {
try {
const j = await _post({ action: 'claim' });
if (!j.ok) return;
if (!j.disponible) { _mostrarBloqueado(j.ocupado_por); return; }
setInterval(() => _post({ action: 'ping' }).catch(() => {}), 60000);
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();
})(lugarId, LUGAR_NOMBRE, 'lugar', 'lugar_id');
</script>
<div class="rec-toast" id="lug-toast">
<span class="ico" id="lug-toast-ico"><i class="fas fa-info-circle"></i></span>
<span id="lug-toast-msg"></span>
</div>
<!-- ── Modal Mi Firma ─────────────────────────────────────── -->
<div id="modal-mi-firma" style="display:none;position:fixed;inset:0;z-index:9500;
background:rgba(0,0,0,.55);align-items:center;justify-content:center;padding:16px">
<div style="background:#fff;border-radius:14px;box-shadow:0 8px 40px rgba(0,0,0,.25);
width:min(96vw,480px);display:flex;flex-direction:column;overflow:hidden">
<div style="display:flex;align-items:center;justify-content:space-between;
padding:12px 18px;border-bottom:1px solid #e2e8f0">
<span style="font-weight:700;font-size:.95rem;color:#1e293b">
<i class="fas fa-signature me-2 text-success"></i>Mi firma pre-guardada
</span>
<button onclick="cerrarModalMiFirma()"
style="background:none;border:none;font-size:1.2rem;color:#94a3b8;cursor:pointer;padding:2px 6px">
<i class="fas fa-times"></i>
</button>
</div>
<div style="padding:16px">
<?php if ($adminFirmaSvg): ?>
<div id="mf-preview" style="text-align:center;margin-bottom:12px">
<p class="small text-success mb-1"><i class="fas fa-check-circle me-1"></i>Firma guardada</p>
<img src="<?= htmlspecialchars($adminFirmaSvg) ?>" alt="Mi firma"
style="max-height:80px;max-width:100%;border:1px solid #c9d8ff;border-radius:8px;padding:6px">
<br><a href="#" class="small text-muted mt-1 d-inline-block" onclick="document.getElementById('mf-preview').style.display='none';document.getElementById('mf-canvas-wrap').style.display='';return false">✎ Dibujar nueva</a>
</div>
<div id="mf-canvas-wrap" style="display:none">
<?php else: ?>
<div id="mf-canvas-wrap">
<?php endif; ?>
<p class="small text-muted mb-2">Dibuje su firma en el recuadro:</p>
<canvas id="mf-canvas" width="420" height="140"
style="border:2px solid #198754;border-radius:8px;background:#f8fff9;
cursor:crosshair;display:block;width:100%;touch-action:none"></canvas>
<div class="mt-2 d-flex gap-2">
<button class="btn btn-outline-secondary btn-sm" onclick="mfLimpiar()">
<i class="fas fa-eraser me-1"></i>Limpiar
</button>
<button class="btn btn-success btn-sm ms-auto" id="mf-btn-save" onclick="mfGuardar()">
<i class="fas fa-save me-1"></i>Guardar firma
</button>
</div>
</div>
<div id="mf-msg" class="mt-2 small"></div>
</div>
</div>
</div>
<script>
// ── Modal Mi Firma ────────────────────────────────────────────
(function() {
var canvas, ctx, drawing = false;
function init() {
canvas = document.getElementById('mf-canvas');
if (!canvas) return;
const ratio = window.devicePixelRatio || 1;
const w = canvas.offsetWidth || 420, h = canvas.offsetHeight || 140;
canvas.width = w * ratio; canvas.height = h * ratio;
canvas.style.width = w + 'px'; canvas.style.height = h + 'px';
ctx = canvas.getContext('2d');
ctx.scale(ratio, ratio);
ctx.strokeStyle = '#000'; ctx.lineWidth = 2.5; ctx.lineCap = 'round'; ctx.lineJoin = 'round';
function pos(e) { const r = canvas.getBoundingClientRect(), s = e.touches?e.touches[0]:e; return {x:s.clientX-r.left, y:s.clientY-r.top}; }
canvas.addEventListener('mousedown', e => { drawing=true; ctx.beginPath(); const p=pos(e); ctx.moveTo(p.x,p.y); });
canvas.addEventListener('mousemove', e => { if(!drawing) return; const p=pos(e); ctx.lineTo(p.x,p.y); ctx.stroke(); });
canvas.addEventListener('mouseup', () => drawing=false);
canvas.addEventListener('mouseleave', () => drawing=false);
canvas.addEventListener('touchstart', e => { e.preventDefault(); drawing=true; ctx.beginPath(); const p=pos(e); ctx.moveTo(p.x,p.y); }, {passive:false});
canvas.addEventListener('touchmove', e => { e.preventDefault(); if(!drawing) return; const p=pos(e); ctx.lineTo(p.x,p.y); ctx.stroke(); }, {passive:false});
canvas.addEventListener('touchend', () => drawing=false);
}
window.abrirModalMiFirma = function() {
document.getElementById('modal-mi-firma').style.display = 'flex';
if (!ctx) init();
};
window.cerrarModalMiFirma = function() {
document.getElementById('modal-mi-firma').style.display = 'none';
};
window.mfLimpiar = function() {
if (ctx) ctx.clearRect(0, 0, canvas.width, canvas.height);
};
window.mfGuardar = function() {
const png = canvas.toDataURL('image/png');
if (png.length < 1000) { document.getElementById('mf-msg').innerHTML = '<span class="text-danger">Dibuje su firma primero.</span>'; return; }
const btn = document.getElementById('mf-btn-save');
btn.disabled = true; btn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Guardando...';
fetch('<?= BASE_URL ?>modules/turnero/api/save_firma_profesional.php', {
method: 'POST', headers: {'Content-Type':'application/json'},
body: JSON.stringify({ firma_svg: png })
})
.then(r => r.json())
.then(d => {
if (d.ok) {
window._fpwPreFirma = png;
document.getElementById('mf-msg').innerHTML = '<span class="text-success"><i class="fas fa-check me-1"></i>Firma guardada. Se usará en el próximo consentimiento.</span>';
btn.innerHTML = '<i class="fas fa-check me-1"></i>Guardado';
// Actualizar preview si existe
var prev = document.getElementById('mf-preview');
if (prev) { prev.querySelector('img').src = png; prev.style.display=''; document.getElementById('mf-canvas-wrap').style.display='none'; }
} else {
document.getElementById('mf-msg').innerHTML = '<span class="text-danger">' + (d.error||'Error') + '</span>';
btn.disabled = false; btn.innerHTML = '<i class="fas fa-save me-1"></i>Guardar firma';
}
})
.catch(() => {
document.getElementById('mf-msg').innerHTML = '<span class="text-danger">Error de conexión.</span>';
btn.disabled = false; btn.innerHTML = '<i class="fas fa-save me-1"></i>Guardar firma';
});
};
})();
</script>
<script src="<?= defined('APP_URL') ? rtrim(APP_URL,'/') : '' ?>/assets/js/lab-sidebar.js"></script>
</body>
</html>