Files
whatsapp/modules/turnero/views/lugar.php
T
Lizandro GuarnizoandClaude Sonnet 4.6 a526a29a68 feat: botón Ver paciente con historial en vista lugar
Agrega botón "Ver" en la sección Paciente de la ficha del turno.
Abre modal con datos completos del paciente (nombre, doc, fecha nac,
celular, médico, embarazo) y timeline de historial de visitas anterior
(misma lógica que recepción), cargado desde get_historial.php.

El botón solo aparece cuando hay un paciente vinculado al turno.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-03 17:55:51 -05:00

1677 lines
79 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;
}
try {
$pdo = Database::getInstance()->getConnection();
$lugares = $pdo->query(
"SELECT id, nombre, descripcion, formulario_modo 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';
foreach ($lugares as $l) {
if ((int)$l['id'] === $lugarIdParam) {
$lugarNombre = $l['nombre'];
$lugarFormModo = $l['formulario_modo'] ?? 'link';
break;
}
}
$adminNombre = $_SESSION['admin_user']['full_name'] ?? $_SESSION['admin_user']['username'] ?? 'Operador';
?>
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= htmlspecialchars($lugarNombre) ?> — Turnero</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
<link href="<?= BASE_URL ?>assets/css/styles.css?v=12" rel="stylesheet">
<style>
/* ── Layout ─────────────────────────────────────────────── */
.lugar-layout {
display: grid;
grid-template-columns: 290px 1fr;
height: calc(100vh - 52px);
overflow: hidden;
}
@media (max-width: 860px) {
.lugar-layout { grid-template-columns: 1fr; position: relative; overflow: hidden; }
.lugar-cola { transition: transform .22s 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 .22s 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: .72rem; text-transform: uppercase; letter-spacing: .06em;
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: .6rem 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: 1.5px solid #e2e8f0; border-radius: 11px;
padding: .6rem .8rem .6rem .7rem; margin-bottom: .3rem;
display: flex; align-items: center; gap: .55rem;
cursor: pointer; transition: border-color .15s, box-shadow .15s, background .15s;
min-height: 58px;
}
.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;
}
.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: 8px; width: 34px; height: 34px;
display: flex; align-items: center; justify-content: center;
font-size: .82rem; 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: 860px) {
.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: 1rem 1.1rem; margin-bottom: .85rem;
}
.ficha-sec-hdr {
display: flex; align-items: center; gap: .4rem;
font-size: .72rem; text-transform: uppercase; letter-spacing: .07em;
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: .55rem .7rem; border-radius: 10px; margin-bottom: .35rem;
font-size: .84rem; border: 1px solid transparent;
}
.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 .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: .7rem; border: none; border-radius: 11px;
font-size: .97rem; 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: .45rem .5rem; border-radius: 9px;
font-size: .8rem; 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:13px 18px; border-bottom:1px solid #e2e8f0; flex-shrink:0; background:#f8fafc;
}
.mpac-hdr-title { font-weight:700; font-size:.97rem; color:#1e293b; display:flex; align-items:center; gap:.5rem; }
.mpac-close { background:none; border:none; font-size:1.2rem; color:#94a3b8; cursor:pointer; padding:2px 6px; border-radius:6px; }
.mpac-close:hover { background:#f1f5f9; color:#475569; }
.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; }
/* ── 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; }
</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>
</div>
</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 btn-outline-secondary" onclick="cambiarLugar()">
<i class="fas fa-exchange-alt me-1"></i>Cambiar
</button>
</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>
</div>
<span class="badge bg-success-subtle text-success border border-success-subtle"
id="ficha-estado-badge">en espera</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"><span class="lbl">Fecha nac.</span><span class="val" id="pac-fec">—</span></div>
<div class="pac-dato"><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="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>
<!-- ── 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"></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,680px);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 onclick="cerrarModalConsentimiento()"
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 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 ─────────────────────────────────────────────
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 _solicitudActiva = null;
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 ?>';
// ── Arranque ──────────────────────────────────────────────────
document.addEventListener('DOMContentLoaded', () => {
if (lugarId) iniciarPuesto();
});
function iniciarPuesto() {
recuperarTurnoActivo();
cargarCola();
pollingColaId = setInterval(cargarCola, 7000);
}
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.');
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('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 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 badgeRow = (tiempoBadge || muestraBadge)
? `<div class="t-badge-row">${tiempoBadge}${muestraBadge}</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>`;
const 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 || '?');
return `<div class="cola-card ${enServicio ? 'en-servicio-card' : ''} ${esActivo ? 'activo' : ''}"
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);
pollingConsentId = setInterval(() => actualizarConsentimientos(turnoActivo?.id), 5000);
if (LUGAR_FORM_MODO === 'embebido' && lugarId) 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 ───────────────────────────────────────────────
async function abrirFicha(turno) {
turnoActivo = turno;
mostrarCabeceraTurno(turno);
await cargarFichaSolicitud(turno.id);
clearInterval(pollingConsentId);
pollingConsentId = setInterval(() => actualizarConsentimientos(turnoActivo?.id), 5000);
if (LUGAR_FORM_MODO === 'embebido' && lugarId) 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;
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 || '—') : '—';
// 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;
}
}
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);
cargarComentarios(turnoId);
} catch (_) {}
}
// ── Consentimientos ───────────────────────────────────────────
const CONSENT_IC = {
firmado:'fa-check-circle', rechazado:'fa-ban',
enviado:'fa-envelope', visto:'fa-eye', pendiente:'fa-clock',
};
const CONSENT_LBL = {
firmado:'Firmado', rechazado:'Rechazado', enviado:'Enviado', visto:'Visto', pendiente:'Pendiente',
};
function renderConsentimientos(lista) {
tieneConsent = lista.length > 0;
hayPendientes = lista.some(c => !['firmado','rechazado'].includes(c.estado));
const pendCount = lista.filter(c => !['firmado','rechazado'].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');
// Aviso en barra de acciones
const warnStrip = document.getElementById('accion-consent-warn');
const warnTxt = document.getElementById('accion-warn-txt');
if (warnStrip) {
if (hayPendientes) {
warnStrip.classList.remove('d-none');
warnTxt.textContent = pendCount + ' consentimiento' + (pendCount > 1 ? 's' : '') + ' pendiente' + (pendCount > 1 ? 's' : '');
} else {
warnStrip.classList.add('d-none');
}
}
if (!tieneConsent) {
listEl.innerHTML = '';
sinEl.classList.remove('d-none');
banner.classList.add('d-none');
if (btnFin) btnFin.disabled = false;
return;
}
sinEl.classList.add('d-none');
banner.classList.toggle('d-none', !hayPendientes);
if (btnFin) btnFin.disabled = hayPendientes;
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;
// 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 = '';
if (!ya && c.token) {
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>
<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>
<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`;
}, 80);
}
function abrirModalConsentimientoPorToken(token) {
_abrirModalConToken(token);
}
function cerrarModalConsentimiento() {
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 === 'turneroFirmado') {
cerrarModalConsentimiento();
mostrarToast('Consentimiento firmado ✓', '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 (!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;
_consentTokenCache = null;
_pacienteActivo = null;
_solicitudActiva = null;
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');
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 <= 860; }
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 = '';
await cargarComentarios(turnoActivo.id);
mostrarToast('Comentario guardado', 'success', 2000);
} catch (err) {
mostrarError(err.message);
}
}
// ── 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}`;
}
// ── 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>
<script src="<?= defined('APP_URL') ? rtrim(APP_URL,'/') : '' ?>/assets/js/lab-sidebar.js"></script>
</body>
</html>